Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
66d4a29
feat: add config and schema
Jun 11, 2026
d1cffee
chore: update to new cleanup registry
Jun 11, 2026
736ec9c
feat: match vscode settings to boilerplate
Jun 11, 2026
e555680
feat: match config to new schema
Jun 11, 2026
52393bf
feat: update dockerfile
Jun 11, 2026
2d564aa
feat: update tsconfig
Jun 11, 2026
cfafba8
feat: update server building
Jun 11, 2026
7bfbc01
feat: update ci
Jun 11, 2026
4ef31e5
feat: add and remove important files
Jun 11, 2026
f76fe52
test: migrate to vitest
Jun 15, 2026
6736582
feat: add security to openai
Jun 15, 2026
bbcca1a
feat: add husky
Jun 15, 2026
7017656
feat: add domain to workflow
eyalr1100 Jun 15, 2026
3d97ba4
feat: upgrade redis image in workflow
Jun 15, 2026
6f0ba4e
chore: merge config exports
Jun 15, 2026
a2d2ec7
feat: fix half of the config problems
Jun 20, 2026
153f548
Merge branch 'feat/match-boilerplate' of https://github.com/MapColoni…
Jun 20, 2026
50d5d2c
fix: match configuration to new schema
eyalr1100 Jun 21, 2026
c44d5da
feat: add version
eyalr1100 Jun 21, 2026
e660cbb
feat: update redis version
eyalr1100 Jun 21, 2026
14f7287
chore: remove deprecated baseUrl
eyalr1100 Jun 21, 2026
0871236
feat: update config to match schema
eyalr1100 Jun 21, 2026
6356e02
fix: correctly use logger with await
eyalr1100 Jun 21, 2026
bf0cae9
chore: import type correctly
eyalr1100 Jun 21, 2026
09f3276
fix: import supertest correctly
eyalr1100 Jun 21, 2026
3053e0d
fix: fix ttl route
eyalr1100 Jun 21, 2026
1492acf
test: fix container registering and remove unknown
eyalr1100 Jun 21, 2026
87548b6
test: add create mock helper
eyalr1100 Jun 21, 2026
39cb378
fix: fix bug where key is read once, expires, and being read again
eyalr1100 Jun 21, 2026
88ba00c
fix: update package lock
eyalr1100 Jun 21, 2026
b4a67c4
chore: add space for workflow to register
eyalr1100 Jun 21, 2026
30fc072
feat: add type to kafka ssl in schema
Jun 24, 2026
2076f55
feat: revert to old redis host
Jun 24, 2026
fce36c6
chore: remove unnecessary resolve promisified logger
Jun 24, 2026
818b03d
chore: remove unnecessary kafka options type
Jun 24, 2026
8e49f44
chore: remove shared service name
eyalr1100 Jun 25, 2026
4b0c2d5
feat: downgrade to redis 4.7
eyalr1100 Jun 28, 2026
8e5d9bf
feat: make redis index not hardcoded 0
eyalr1100 Jun 29, 2026
d7201eb
feat: add ajv validation
Jun 30, 2026
9dbfc37
feat: move json parsing to the validator function
Jun 30, 2026
15c5d2b
feat: add set prototype to error
Jun 30, 2026
c0967f7
feat: return GeocodingResponseParseError instead of throwing
Jun 30, 2026
b4f2962
feat: make ca for kafka optional
eyalr1100 Jul 2, 2026
f3cf69a
Merge branch 'feat/match-boilerplate' into feat/add-ajv-validation
eyalr1100 Jul 2, 2026
47eb2ee
Merge remote-tracking branch 'origin/master' into feat/add-ajv-valida…
eyalr1100 Jul 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{".":"1.0.0"}
{ ".": "1.0.0" }
124 changes: 66 additions & 58 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "feedback-api",
"version": "1.0.0",
"version": "0.2.1",
"description": "Feedback-api service for MapColonies",
"main": "./src/index.ts",
"scripts": {
Expand Down Expand Up @@ -55,7 +55,9 @@
"@map-colonies/tsconfig": "^2.0.0",
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/api-metrics": "0.23.0",
"@sinclair/typebox": "^0.34.49",
"@types/body-parser": "^1.19.6",
"ajv": "^8.20.0",
"compression": "^1.8.0",
"express": "^4.21.2",
"express-openapi-validator": "^5.6.2",
Expand Down
8 changes: 8 additions & 0 deletions src/common/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,11 @@ export class BadRequestError extends Error implements HttpError {
super(message);
}
}

export class GeocodingResponseParseError extends Error {
public constructor(message: string) {
super(message);
Object.setPrototypeOf(this, GeocodingResponseParseError.prototype);
this.name = 'GeocodingResponseParseError';
Comment thread
NivGreenstein marked this conversation as resolved.
}
}
19 changes: 11 additions & 8 deletions src/common/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Type, type Static } from '@sinclair/typebox';
import type { vectorFeedbackApiV1Type } from '@map-colonies/schemas';

export interface OpenApiConfig {
Expand All @@ -17,11 +18,13 @@ export interface FeedbackResponse {
geocodingResponse: GeocodingResponse;
}

export interface GeocodingResponse {
userId?: string;
apiKey: string;
site: string;
response: JSON;
respondedAt: Date; // from Geocoding
wasUsed?: boolean;
}
export const geocodingResponseSchema = Type.Object({
userId: Type.Optional(Type.String()),
apiKey: Type.String(),
site: Type.String(),
response: Type.Any(),
respondedAt: Type.Unsafe<Date>({ type: 'string' }),
wasUsed: Type.Optional(Type.Boolean()),
});

export type GeocodingResponse = Static<typeof geocodingResponseSchema>;
15 changes: 14 additions & 1 deletion src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import { TimeoutError } from './errors';
import ajvLib from 'ajv';
import { GeocodingResponseParseError, TimeoutError } from './errors';
import { geocodingResponseSchema, type GeocodingResponse } from './interfaces';

const ajv = new ajvLib();
const validateGeocodingResponse = ajv.compile<GeocodingResponse>(geocodingResponseSchema);

export function parseGeocodingResponse(data: string): GeocodingResponse | GeocodingResponseParseError {
const parsed: unknown = JSON.parse(data);
if (!validateGeocodingResponse(parsed)) {
return new GeocodingResponseParseError(`Invalid geocoding response shape: ${ajv.errorsText(validateGeocodingResponse.errors)}`);
}
return parsed;
}

export const promiseTimeout = async <T>(ms: number, promise: Promise<T>): Promise<T> => {
// create a promise that rejects in <ms> milliseconds
Expand Down
23 changes: 16 additions & 7 deletions src/feedback/models/feedbackManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { inject, injectable } from 'tsyringe';
import { type Producer } from 'kafkajs';
import { SERVICES } from '@common/constants';
import { FeedbackResponse, GeocodingResponse } from '@common/interfaces';
import { NotFoundError, BadRequestError } from '@common/errors';
import { parseGeocodingResponse } from '@common/utils';
import { NotFoundError, BadRequestError, GeocodingResponseParseError } from '@common/errors';
import { type ConfigType } from '@src/common/config';
import { type RedisClient } from '../../redis';
import { IFeedbackModel } from './feedback';
Expand All @@ -20,12 +21,11 @@ export class FeedbackManager {
public async createFeedback(feedback: IFeedbackModel, apiKey: string): Promise<FeedbackResponse> {
const requestId = feedback.request_id;
const userId = feedback.user_id;
const raw = this.config.get('application.userValidation');
const userValidation = Array.isArray(raw) ? raw : (JSON.parse(raw) as string[]);
const userValidation = this.config.get('application.userValidation');
const ttl = this.config.get('redis.ttl');
const prefix = this.config.get('redis.prefix');

const validateUser = !userValidation.some((validEnding) => validEnding !== '' && userId.endsWith(validEnding));
const validateUser = !userValidation.some((validEnding: string) => validEnding !== '' && userId.endsWith(validEnding));
if (validateUser) {
throw new BadRequestError(`user_id not valid. valid user_id ends with "${JSON.stringify(userValidation)}"`);
}
Expand All @@ -49,9 +49,18 @@ export class FeedbackManager {

public async getGeocodingResponse(requestId: string, userId: string, apiKey: string): Promise<GeocodingResponse> {
try {
const redisResponse = (await this.redisClient.get(requestId)) as string;
if (redisResponse) {
const geocodingResponse = JSON.parse(redisResponse) as GeocodingResponse;
const redisResponse = await this.redisClient.get(requestId);
if (redisResponse != null) {
const geocodingResponse = parseGeocodingResponse(redisResponse);

if (geocodingResponse instanceof GeocodingResponseParseError) {
this.logger.error({
msg: `Error parsing geocoding response for requestId: ${requestId}, error: ${geocodingResponse.message} }`,
err: geocodingResponse,
});
throw geocodingResponse;
}

geocodingResponse.userId = userId;
geocodingResponse.apiKey = apiKey;
geocodingResponse.wasUsed = true;
Expand Down
23 changes: 19 additions & 4 deletions src/redis/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type { Producer } from 'kafkajs';
import type { ConfigType } from '@src/common/config';
import { REDIS_SUB, SERVICES } from '../common/constants';
import type { FeedbackResponse, GeocodingResponse } from '../common/interfaces';
import { NotFoundError } from '../common/errors';
import { parseGeocodingResponse } from '../common/utils';
import { GeocodingResponseParseError, NotFoundError } from '../common/errors';
import type { RedisClient } from '../redis/index';

const TTL_PREFIX = 'ttl:';
Expand Down Expand Up @@ -65,10 +66,15 @@ export const redisSubscribe = async (deps: DependencyContainer): Promise<RedisCl
try {
if (message.startsWith(prefixWithTtl) && redisClient.isOpen) {
const geocodingMessage = message.substring(prefixWithTtl.length);

const redisResponse = await redisClient.get(geocodingMessage);
if (redisResponse !== null) {
const geocodingResponse = JSON.parse(redisResponse) as GeocodingResponse;
const geocodingResponse = parseGeocodingResponse(redisResponse);

if (geocodingResponse instanceof GeocodingResponseParseError) {
logger.error({ msg: `Error parsing geocoding response error: ${geocodingResponse.message} }`, err: geocodingResponse });
throw geocodingResponse;
}

if (!(geocodingResponse.wasUsed ?? false)) {
await sendNoChosenResult(geocodingMessage, logger, config, kafkaProducer, redisClient);
}
Expand Down Expand Up @@ -104,7 +110,16 @@ export const getNoChosenGeocodingResponse = async (requestId: string, logger: Lo
try {
const redisResponse = await redisClient.get(requestId);
if (redisResponse != null) {
const geocodingResponse = JSON.parse(redisResponse) as GeocodingResponse;
const geocodingResponse = parseGeocodingResponse(redisResponse);

if (geocodingResponse instanceof GeocodingResponseParseError) {
logger.error({
msg: `Error parsing geocoding response for requestId: ${requestId}, error: ${geocodingResponse.message} }`,
err: geocodingResponse,
});
throw geocodingResponse;
}

return geocodingResponse;
}
} catch (error) {
Expand Down
10 changes: 10 additions & 0 deletions tests/helpers/createMock.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import type { Mock, MockedObject } from 'vitest';
import type { GeocodingResponse } from '@common/interfaces';

export const createMock = <T>(partial: Partial<Record<keyof T, Mock>>): MockedObject<T> => partial as MockedObject<T>;

export const makeGeocodingResponseJson = (overrides: Partial<GeocodingResponse> = {}): string =>
JSON.stringify({
apiKey: 'test-api-key',
site: 'test-site',
response: {},
respondedAt: '2024-01-01T00:00:00.000Z',
...overrides,
});
Loading
Loading