Skip to content

Commit 0ca7111

Browse files
feat: added prefix option to redis (#88)
* feat: added prefix option to redis * feat: updated mapcolonies schemas package * feat: added redis to workflow * test: added redis prefix tests
1 parent 8a066ee commit 0ca7111

18 files changed

Lines changed: 399 additions & 89 deletions

File tree

.github/workflows/pull_request.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ jobs:
7777
--health-timeout 5s
7878
--health-retries 5
7979
80+
redis:
81+
image: redis
82+
ports:
83+
- 6379:6379
84+
options: >-
85+
--health-cmd "redis-cli ping"
86+
--health-interval 10s
87+
--health-timeout 5s
88+
--health-retries 5
89+
8090
steps:
8191
- name: Check out Git repository
8292
uses: actions/checkout@v4

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ Almost all of our routes consists of the same common query parameters: `geo_cont
4141
| disable_fuzziness | Boolean | false | Fuzziness is on by default. If you want exact match, you may set `disable_fuzziness: true`. |
4242
> [!IMPORTANT]
4343
> We also have Feedback API. Each request is sent back with x-req-id which is the identifier of the request. We kindly ask our users to provide us with a request to Feedback API which contains x-api-key and clicked response. It enables us to research the request and response to be more accurate.
44-
> Feedback API source code is built in a different repository at <TODO: ADD LINK TO FEEDBACK API REPO>.
44+
> Feedback API source code is built in a different repository at [feedback-api](https://github.com/MapColonies/feedback-api).
4545
> Geocoding API inserts the request and response to Redis before the response is sent.
46+
Speaking of Redis, in the redis config you can add a `prefix` flag, if you'd like to add keys with prefixes to redis.
4647

4748
## Installation
4849
Setup Elasticsearch and S3 provider (For local environment, Minio as a personal recommendation).

config/test.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"fileName": "table.json"
3636
},
3737
"redis": {
38-
"host": "localhost",
38+
"host": "redis",
3939
"port": 6379,
4040
"username": "",
4141
"password": "",
@@ -45,7 +45,8 @@
4545
"key": "",
4646
"cert": ""
4747
},
48-
"dbIndex": 1
48+
"dbIndex": 1,
49+
"ttl": 300
4950
}
5051
},
5152
"application": {

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"@map-colonies/js-logger": "^2.0.0",
4242
"@map-colonies/openapi-express-viewer": "^4.0.0",
4343
"@map-colonies/read-pkg": "^1.0.0",
44-
"@map-colonies/schemas": "^1.12.2",
44+
"@map-colonies/schemas": "^1.14.0",
4545
"@map-colonies/telemetry": "^10.0.1",
4646
"@opentelemetry/api": "^1.9.0",
4747
"@smithy/node-http-handler": "^3.1.4",

src/common/middlewares/feedbackApi.middleware.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class FeedbackApiMiddlewareManager {
2121
const reqId = res.getHeader(XApi.REQUEST);
2222
const redisClient = this.redis;
2323
const logger = this.logger;
24+
const prefix = this.config.get(redisConfigPath).prefix;
2425

2526
const drSite = this.config.get(siteConfig);
2627

@@ -36,10 +37,11 @@ export class FeedbackApiMiddlewareManager {
3637
const { ttl: redisTtl } = this.config.get(redisConfigPath);
3738

3839
const originalJson = res.json;
40+
const fullRequestId = prefix !== undefined ? `${prefix}:${reqId as string}` : (reqId as string);
3941
const logJson = function (this: Response, body: JSON): Response {
4042
geocodingResponseDetails.response = body;
4143
redisClient
42-
.setEx(reqId as string, redisTtl ?? defaultRedisTtl, JSON.stringify(geocodingResponseDetails))
44+
.setEx(fullRequestId, redisTtl ?? defaultRedisTtl, JSON.stringify(geocodingResponseDetails))
4345
.then(() => {
4446
logger.info({ msg: `response ${reqId?.toString() ?? ''} saved to redis` });
4547
})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import jsLogger from '@map-colonies/js-logger';
2+
import { trace } from '@opentelemetry/api';
3+
import { SERVICES } from '@src/common/constants';
4+
import { S3_REPOSITORY_SYMBOL } from '@src/common/s3/s3Repository';
5+
import { cronLoadTileLatLonDataSymbol } from '@src/latLon/DAL/latLonDAL';
6+
import { GetBaseRegisterOptions } from '@tests/integration/helpers/types';
7+
8+
export const getBaseRegisterOptions: GetBaseRegisterOptions = (options = []) => {
9+
return {
10+
override: [
11+
{ token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } },
12+
{ token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } },
13+
{ token: S3_REPOSITORY_SYMBOL, provider: { useValue: {} } },
14+
{ token: SERVICES.S3_CLIENT, provider: { useValue: {} } },
15+
{ token: cronLoadTileLatLonDataSymbol, provider: { useValue: {} } },
16+
...options,
17+
],
18+
useChild: true,
19+
};
20+
};

tests/integration/control/item/item.spec.ts

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,26 @@
11
/* eslint-disable @typescript-eslint/naming-convention */
2-
import jsLogger from '@map-colonies/js-logger';
3-
import { trace } from '@opentelemetry/api';
42
import { CleanupRegistry } from '@map-colonies/cleanup-registry';
53
import { DependencyContainer } from 'tsyringe';
64
import httpStatusCodes from 'http-status-codes';
5+
import { IConfig } from 'config';
6+
import { RedisClient } from '@src/common/redis';
77
import { getApp } from '../../../../src/app';
8-
import { SERVICES } from '../../../../src/common/constants';
8+
import { redisConfigPath, SERVICES } from '../../../../src/common/constants';
99
import { GetItemsQueryParams } from '../../../../src/control/item/controllers/itemController';
1010
import { Item } from '../../../../src/control/item/models/item';
1111
import { CommonRequestParameters, GenericGeocodingResponse, GeoContext, GeoContextMode } from '../../../../src/common/interfaces';
12-
import { cronLoadTileLatLonDataSymbol } from '../../../../src/latLon/DAL/latLonDAL';
13-
import { S3_REPOSITORY_SYMBOL } from '../../../../src/common/s3/s3Repository';
1412
import { expectedResponse } from '../utils';
1513
import { ITEM_1234, ITEM_1235, ITEM_1236 } from '../../../mockObjects/items';
1614
import { ItemRequestSender } from './helpers/requestSender';
15+
import { getBaseRegisterOptions } from './helpers';
1716

1817
describe('/search/control/items', function () {
1918
let requestSender: ItemRequestSender;
2019
let depContainer: DependencyContainer;
2120

2221
beforeEach(async function () {
23-
const [app, container] = await getApp({
24-
override: [
25-
{ token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } },
26-
{ token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } },
27-
{ token: S3_REPOSITORY_SYMBOL, provider: { useValue: {} } },
28-
{ token: SERVICES.S3_CLIENT, provider: { useValue: {} } },
29-
{ token: cronLoadTileLatLonDataSymbol, provider: { useValue: {} } },
30-
],
31-
useChild: true,
32-
});
22+
const [app, container] = await getApp(getBaseRegisterOptions());
23+
3324
depContainer = container;
3425
requestSender = new ItemRequestSender(app);
3526
});
@@ -245,7 +236,47 @@ describe('/search/control/items', function () {
245236
expectedResponse(requestParams, [ITEM_1234, ITEM_1235], expect)
246237
);
247238
});
239+
240+
describe('Redis uses prefix key', () => {
241+
it('should return 200 status code and add key to Redis with prefix', async function () {
242+
const realConfig = depContainer.resolve<IConfig>(SERVICES.CONFIG);
243+
const prefix = 'test-prefix-item';
244+
245+
const configWithPrefix: IConfig = {
246+
...realConfig,
247+
get<T>(key: string): T {
248+
if (key === redisConfigPath) {
249+
const realRedisConfig = realConfig.get<RedisClient>(redisConfigPath);
250+
return { ...realRedisConfig, prefix } as T;
251+
}
252+
return realConfig.get<T>(key);
253+
},
254+
};
255+
256+
const mockRegisterOptions = getBaseRegisterOptions([
257+
{
258+
token: SERVICES.CONFIG,
259+
provider: { useValue: configWithPrefix },
260+
},
261+
]);
262+
263+
const [mockApp, localContainer] = await getApp(mockRegisterOptions);
264+
const localRequestSender = new ItemRequestSender(mockApp);
265+
266+
const redisConnection = localContainer.resolve<RedisClient>(SERVICES.REDIS);
267+
268+
const requestParams: GetItemsQueryParams = { command_name: '123', limit: 5, disable_fuzziness: false };
269+
const response = await localRequestSender.getItems(requestParams);
270+
271+
const keys = await redisConnection.keys(prefix + '*');
272+
expect(keys.length).toBeGreaterThanOrEqual(1);
273+
expect(response.status).toBe(httpStatusCodes.OK);
274+
275+
await localContainer.dispose();
276+
});
277+
});
248278
});
279+
249280
describe('Bad Path', function () {
250281
// All requests with status code of 400
251282
it("should return 400 status code and error message when item's command_name", async function () {
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import jsLogger from '@map-colonies/js-logger';
2+
import { trace } from '@opentelemetry/api';
3+
import { SERVICES } from '@src/common/constants';
4+
import { S3_REPOSITORY_SYMBOL } from '@src/common/s3/s3Repository';
5+
import { cronLoadTileLatLonDataSymbol } from '@src/latLon/DAL/latLonDAL';
6+
import { GetBaseRegisterOptions } from '@tests/integration/helpers/types';
7+
8+
export const getBaseRegisterOptions: GetBaseRegisterOptions = (options = []) => {
9+
return {
10+
override: [
11+
{ token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } },
12+
{ token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } },
13+
{ token: S3_REPOSITORY_SYMBOL, provider: { useValue: {} } },
14+
{ token: SERVICES.S3_CLIENT, provider: { useValue: {} } },
15+
{ token: cronLoadTileLatLonDataSymbol, provider: { useValue: {} } },
16+
...options,
17+
],
18+
useChild: true,
19+
};
20+
};

tests/integration/control/route/route.spec.ts

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
/* eslint-disable @typescript-eslint/naming-convention */
2-
import jsLogger from '@map-colonies/js-logger';
3-
import { trace } from '@opentelemetry/api';
42
import httpStatusCodes from 'http-status-codes';
53
import { DependencyContainer } from 'tsyringe';
64
import { CleanupRegistry } from '@map-colonies/cleanup-registry';
5+
import { IConfig } from 'config';
6+
import { RedisClient } from '@src/common/redis';
77
import { getApp } from '../../../../src/app';
8-
import { SERVICES } from '../../../../src/common/constants';
8+
import { redisConfigPath, SERVICES } from '../../../../src/common/constants';
99
import { GetRoutesQueryParams } from '../../../../src/control/route/controllers/routeController';
1010
import { Route } from '../../../../src/control/route/models/route';
1111
import { CommonRequestParameters, GenericGeocodingResponse, GeoContext, GeoContextMode } from '../../../../src/common/interfaces';
12-
import { S3_REPOSITORY_SYMBOL } from '../../../../src/common/s3/s3Repository';
13-
import { cronLoadTileLatLonDataSymbol } from '../../../../src/latLon/DAL/latLonDAL';
1412
import { expectedResponse } from '../utils';
1513
import {
1614
ROUTE_VIA_CAMILLUCCIA_A,
@@ -19,22 +17,14 @@ import {
1917
CONTROL_POINT_OLIMPIADE_112,
2018
} from '../../../mockObjects/routes';
2119
import { RouteRequestSender } from './helpers/requestSender';
20+
import { getBaseRegisterOptions } from './helpers';
2221

2322
describe('/search/control/route', function () {
2423
let requestSender: RouteRequestSender;
2524
let depContainer: DependencyContainer;
2625

2726
beforeEach(async function () {
28-
const [app, container] = await getApp({
29-
override: [
30-
{ token: SERVICES.LOGGER, provider: { useValue: jsLogger({ enabled: false }) } },
31-
{ token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } },
32-
{ token: S3_REPOSITORY_SYMBOL, provider: { useValue: {} } },
33-
{ token: SERVICES.S3_CLIENT, provider: { useValue: {} } },
34-
{ token: cronLoadTileLatLonDataSymbol, provider: { useValue: {} } },
35-
],
36-
useChild: true,
37-
});
27+
const [app, container] = await getApp(getBaseRegisterOptions());
3828

3929
depContainer = container;
4030
requestSender = new RouteRequestSender(app);
@@ -382,7 +372,47 @@ describe('/search/control/route', function () {
382372
bbox: null,
383373
});
384374
});
375+
376+
describe('Redis uses prefix key', () => {
377+
it('should return 200 status code and add key to Redis with prefix', async function () {
378+
const realConfig = depContainer.resolve<IConfig>(SERVICES.CONFIG);
379+
const prefix = 'test-prefix-route';
380+
381+
const configWithPrefix: IConfig = {
382+
...realConfig,
383+
get<T>(key: string): T {
384+
if (key === redisConfigPath) {
385+
const realRedisConfig = realConfig.get<RedisClient>(redisConfigPath);
386+
return { ...realRedisConfig, prefix } as T;
387+
}
388+
return realConfig.get<T>(key);
389+
},
390+
};
391+
392+
const mockRegisterOptions = getBaseRegisterOptions([
393+
{
394+
token: SERVICES.CONFIG,
395+
provider: { useValue: configWithPrefix },
396+
},
397+
]);
398+
399+
const [mockApp, localContainer] = await getApp(mockRegisterOptions);
400+
const localRequestSender = new RouteRequestSender(mockApp);
401+
402+
const redisConnection = localContainer.resolve<RedisClient>(SERVICES.REDIS);
403+
404+
const requestParams: GetRoutesQueryParams = { command_name: 'via camilluccia', limit: 5, disable_fuzziness: false };
405+
const response = await localRequestSender.getRoutes(requestParams);
406+
407+
const keys = await redisConnection.keys(prefix + '*');
408+
expect(keys.length).toBeGreaterThanOrEqual(1);
409+
expect(response.status).toBe(httpStatusCodes.OK);
410+
411+
await localContainer.dispose();
412+
});
413+
});
385414
});
415+
386416
describe('Bad Path', function () {
387417
// All requests with status code of 400
388418
it('should return 400 status code and error message when empty object is passed', async function () {

0 commit comments

Comments
 (0)