diff --git a/package.json b/package.json index 3e206a8..55df98b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "RaenonX", "name": "dragalia-site-back-2", "description": "Renewed backend for Dragalia Lost info website by OM.", - "version": "2.15.0-beta.1", + "version": "2.15.0", "engines": { "node": "14.x", "npm": "^7.12.0" diff --git a/src/api-def b/src/api-def index b03e69b..cf00a89 160000 --- a/src/api-def +++ b/src/api-def @@ -1 +1 @@ -Subproject commit b03e69b7223435856e9eb65d70691c90fb0a8b67 +Subproject commit cf00a893c45d18d0306e7b7631e315e64e76444d diff --git a/src/base/model/seq.ts b/src/base/model/seq.ts index 7ad2c7a..b48c7b6 100644 --- a/src/base/model/seq.ts +++ b/src/base/model/seq.ts @@ -86,7 +86,9 @@ export class SequentialDocument extends Document { throw new SeqIdSkippingError(seqId, latestSeqId + 1); } - updateOps = {$set: {[SequenceCounterKeys.counter]: seqId}}; + // Use `$max` instead of `$set` so that the sequential counter is updated + // only if the new ID is the newest + updateOps = {$max: {[SequenceCounterKeys.counter]: seqId}}; } else { updateOps = {$inc: {[SequenceCounterKeys.counter]: increase ? 1 : 0}}; } diff --git a/src/endpoints/data/keyPoint/response.ts b/src/endpoints/data/keyPoint/response.ts index 36c5451..601c129 100644 --- a/src/endpoints/data/keyPoint/response.ts +++ b/src/endpoints/data/keyPoint/response.ts @@ -18,7 +18,7 @@ export class KeyPointInfoResponse extends ApiResponse { info: KeyPointInfo; /** - * Construct an unit name update endpoint API response. + * Construct a unit name update endpoint API response. * * @param {KeyPointInfoResponseOptions} options options to construct the response */ diff --git a/src/endpoints/data/unitNameRef/get/response.ts b/src/endpoints/data/unitNameRef/get/response.ts index a578afa..eefae76 100644 --- a/src/endpoints/data/unitNameRef/get/response.ts +++ b/src/endpoints/data/unitNameRef/get/response.ts @@ -16,7 +16,7 @@ export class UnitNameRefResponse extends ApiResponse { data: UnitNameRefData; /** - * Construct an unit name reference endpoint API response. + * Construct a unit name reference endpoint API response. * * @param {UnitNameRefResponseOptions} options options to construct the response */ diff --git a/src/endpoints/data/unitNameRef/manage/response.ts b/src/endpoints/data/unitNameRef/manage/response.ts index 4ab990e..a3a1cb4 100644 --- a/src/endpoints/data/unitNameRef/manage/response.ts +++ b/src/endpoints/data/unitNameRef/manage/response.ts @@ -16,9 +16,9 @@ export class UnitNameRefManageResponse extends ApiResponse { refs: Array; /** - * Construct an unit name managing endpoint API response. + * Construct a unit name managing endpoint API response. * - * @param {UnitNameRefManageResponseOptions} options options to construct an unit name ref response + * @param {UnitNameRefManageResponseOptions} options options to construct a unit name ref response */ constructor(options: UnitNameRefManageResponseOptions) { const responseCode = ApiResponseCode.SUCCESS; diff --git a/src/endpoints/data/unitNameRef/model.ts b/src/endpoints/data/unitNameRef/model.ts index 9cbcb16..05a4e0c 100644 --- a/src/endpoints/data/unitNameRef/model.ts +++ b/src/endpoints/data/unitNameRef/model.ts @@ -37,9 +37,9 @@ export class UnitNameRefEntry extends Document { unitId: number; /** - * Construct an unit name reference entry + * Construct a unit name reference entry * - * @param {UnitNameRefEntryConstructParams} params parameters to construct an unit name ref entry + * @param {UnitNameRefEntryConstructParams} params parameters to construct a unit name ref entry */ constructor(params: UnitNameRefEntryConstructParams) { super(params); diff --git a/src/endpoints/data/unitNameRef/update/response.ts b/src/endpoints/data/unitNameRef/update/response.ts index 78f02e5..e10c480 100644 --- a/src/endpoints/data/unitNameRef/update/response.ts +++ b/src/endpoints/data/unitNameRef/update/response.ts @@ -9,7 +9,7 @@ import {ApiResponse} from '../../../../base/response'; */ export class UnitNameRefUpdateResponse extends ApiResponse { /** - * Construct an unit name update endpoint API response. + * Construct a unit name update endpoint API response. */ constructor() { super(ApiResponseCode.SUCCESS); diff --git a/src/endpoints/lookup.ts b/src/endpoints/lookup.ts index b44d557..295bd1f 100644 --- a/src/endpoints/lookup.ts +++ b/src/endpoints/lookup.ts @@ -36,6 +36,7 @@ import {handleRoot} from './root/handler'; import {handleEmitError} from './test/handler'; import {handleTierNoteEdit} from './tier/notes/edit/handler'; import {handleTierNoteGet} from './tier/notes/get/handler'; +import {handleTierNoteSingle} from './tier/notes/single/handler'; import {handleTierNoteUpdate} from './tier/notes/update/handler'; import {handleTierPointsGet} from './tier/points/get/handler'; import {handleTierPointsManage} from './tier/points/manage/handler'; @@ -86,6 +87,7 @@ export const handlerLookup: {[endpoint: string]: EndpointHandlers} = { [ApiEndPoints.POST_ANALYSIS_EDIT_DRAGON]: {POST: handleEditDragonAnalysis}, [ApiEndPoints.POST_ANALYSIS_ID_CHECK]: {GET: handleAnalysisIdCheck}, [ApiEndPoints.TIER_NOTES]: {GET: handleTierNoteGet}, + [ApiEndPoints.TIER_UNIT]: {GET: handleTierNoteSingle}, [ApiEndPoints.TIER_KEY_POINTS]: {GET: handleTierPointsGet}, [ApiEndPoints.DATA_UNIT_NAME_REF]: {GET: handleDataUnitNameRef}, [ApiEndPoints.DATA_KEY_POINT]: {GET: handleGetKeyPointData}, diff --git a/src/endpoints/post/analysis/error.ts b/src/endpoints/post/analysis/error.ts index ad09131..852378e 100644 --- a/src/endpoints/post/analysis/error.ts +++ b/src/endpoints/post/analysis/error.ts @@ -21,7 +21,7 @@ export class UnhandledUnitTypeError extends CustomError { */ export class UnitNotExistsError extends CustomError { /** - * Construct an unit not exists error. + * Construct a unit not exists error. * * @param {number} unitId non-existent unit ID */ @@ -35,7 +35,7 @@ export class UnitNotExistsError extends CustomError { */ export class UnitTypeMismatchError extends CustomError { /** - * Construct an unit type mismatch error. + * Construct a unit type mismatch error. * * @param {number} unitId unit ID that causes the error * @param {UnitType} expectedType expected unit type diff --git a/src/endpoints/post/misc/controller.test.ts b/src/endpoints/post/misc/controller.test.ts index 47c8bd9..b6a9b17 100644 --- a/src/endpoints/post/misc/controller.test.ts +++ b/src/endpoints/post/misc/controller.test.ts @@ -427,4 +427,26 @@ describe('Misc post controller', () => { expect(availability).toBe(false); }); + + it('returns correct ID check result across languages', async () => { + await MiscPostController.publishPost(app.mongoClient, {...payload, seqId: 1, lang: SupportedLanguages.CHT}); + await MiscPostController.publishPost(app.mongoClient, {...payload, seqId: 2, lang: SupportedLanguages.CHT}); + await MiscPostController.publishPost(app.mongoClient, {...payload, seqId: 1, lang: SupportedLanguages.EN}); + + const available = await MiscPostController.isPostIdAvailable(app.mongoClient, SupportedLanguages.CHT, 3); + + expect(available).toBeTruthy(); + }); + + it('publishes in correct ID order', async () => { + await MiscPostController.publishPost(app.mongoClient, {...payload, seqId: 1, lang: SupportedLanguages.CHT}); + await MiscPostController.publishPost(app.mongoClient, {...payload, seqId: 2, lang: SupportedLanguages.CHT}); + await MiscPostController.publishPost(app.mongoClient, {...payload, seqId: 1, lang: SupportedLanguages.EN}); + + const newPostId = await MiscPostController.publishPost( + app.mongoClient, {...payload, lang: SupportedLanguages.CHT}, + ); + + expect(newPostId).toBe(3); + }); }); diff --git a/src/endpoints/post/quest/controller.test.ts b/src/endpoints/post/quest/controller.test.ts index 4b5115c..b749d6d 100644 --- a/src/endpoints/post/quest/controller.test.ts +++ b/src/endpoints/post/quest/controller.test.ts @@ -9,7 +9,8 @@ import {SeqIdSkippingError} from '../error'; import {QuestPostController} from './controller'; import {QuestPost, QuestPostDocument} from './model'; -describe(`[Controller] ${QuestPostController.name}`, () => { + +describe('Quest post controller', () => { let app: Application; beforeAll(async () => { @@ -449,4 +450,26 @@ describe(`[Controller] ${QuestPostController.name}`, () => { expect(availability).toBe(false); }); + + it('returns correct ID check result across languages', async () => { + await QuestPostController.publishPost(app.mongoClient, {...payload, seqId: 1, lang: SupportedLanguages.CHT}); + await QuestPostController.publishPost(app.mongoClient, {...payload, seqId: 2, lang: SupportedLanguages.CHT}); + await QuestPostController.publishPost(app.mongoClient, {...payload, seqId: 1, lang: SupportedLanguages.EN}); + + const available = await QuestPostController.isPostIdAvailable(app.mongoClient, SupportedLanguages.CHT, 3); + + expect(available).toBeTruthy(); + }); + + it('publishes in correct ID order', async () => { + await QuestPostController.publishPost(app.mongoClient, {...payload, seqId: 1, lang: SupportedLanguages.CHT}); + await QuestPostController.publishPost(app.mongoClient, {...payload, seqId: 2, lang: SupportedLanguages.CHT}); + await QuestPostController.publishPost(app.mongoClient, {...payload, seqId: 1, lang: SupportedLanguages.EN}); + + const newPostId = await QuestPostController.publishPost( + app.mongoClient, {...payload, lang: SupportedLanguages.CHT}, + ); + + expect(newPostId).toBe(3); + }); }); diff --git a/src/endpoints/tier/notes/controller.test.ts b/src/endpoints/tier/notes/controller.test.ts index 5571c20..0d9e001 100644 --- a/src/endpoints/tier/notes/controller.test.ts +++ b/src/endpoints/tier/notes/controller.test.ts @@ -86,7 +86,7 @@ describe('Tier note data controller', () => { describe('getUnitTierNoteEdit()', () => { it('returns `null` if no existing tier note available', async () => { - const data = await TierNoteController.getUnitTierNoteEdit(app.mongoClient, SupportedLanguages.CHT, 10950101); + const data = await TierNoteController.getUnitTierNoteSingle(app.mongoClient, SupportedLanguages.CHT, 10950101); expect(data).toStrictEqual(null); }); @@ -108,7 +108,7 @@ describe('Tier note data controller', () => { ].map((entry) => entry.toObject()); await UnitTierNote.getCollection(app.mongoClient).insertMany(dataArray); - const data = await TierNoteController.getUnitTierNoteEdit(app.mongoClient, SupportedLanguages.CHT, 10950101); + const data = await TierNoteController.getUnitTierNoteSingle(app.mongoClient, SupportedLanguages.CHT, 10950101); expect(data).toStrictEqual({ points: [], diff --git a/src/endpoints/tier/notes/controller.ts b/src/endpoints/tier/notes/controller.ts index a4a6515..f25de0e 100644 --- a/src/endpoints/tier/notes/controller.ts +++ b/src/endpoints/tier/notes/controller.ts @@ -37,14 +37,14 @@ export class TierNoteController { } /** - * Get the unit tier note of a certain unit for editing. + * Get the unit tier note of a unit. * * @param {MongoClient} mongoClient mongo client * @param {SupportedLanguages} lang language of the tier note - * @param {number} unitId unit ID of the tier note to be edited + * @param {number} unitId unit ID of the tier note * @return {Promise} */ - static async getUnitTierNoteEdit( + static async getUnitTierNoteSingle( mongoClient: MongoClient, lang: SupportedLanguages, unitId: number, ): Promise { const tierNoteDoc = await UnitTierNote.getCollection(mongoClient) diff --git a/src/endpoints/tier/notes/edit/handler.ts b/src/endpoints/tier/notes/edit/handler.ts index 8a6d22b..485613a 100644 --- a/src/endpoints/tier/notes/edit/handler.ts +++ b/src/endpoints/tier/notes/edit/handler.ts @@ -11,7 +11,7 @@ export const handleTierNoteEdit = async ({ }: HandlerParams): Promise => { payload = processTierNoteEditPayload(payload); - const data = await TierNoteController.getUnitTierNoteEdit(mongoClient, payload.lang, payload.unitId); + const data = await TierNoteController.getUnitTierNoteSingle(mongoClient, payload.lang, payload.unitId); return new UnitTierNoteEditResponse({data}); }; diff --git a/src/endpoints/tier/notes/edit/response.ts b/src/endpoints/tier/notes/edit/response.ts index fcb956d..571ff04 100644 --- a/src/endpoints/tier/notes/edit/response.ts +++ b/src/endpoints/tier/notes/edit/response.ts @@ -10,7 +10,7 @@ import {ApiResponse} from '../../../../base/response'; type UnitTierNoteEditResponseOptions = Omit; /** - * API response class for the endpoint to get the unit tier notes of an unit for edit. + * API response class for the endpoint to get the unit tier notes of a unit for edit. */ export class UnitTierNoteEditResponse extends ApiResponse { data: UnitTierNote | null; diff --git a/src/endpoints/tier/notes/single/handler.test.ts b/src/endpoints/tier/notes/single/handler.test.ts new file mode 100644 index 0000000..e5c4df9 --- /dev/null +++ b/src/endpoints/tier/notes/single/handler.test.ts @@ -0,0 +1,95 @@ +import { + ApiEndPoints, + ApiResponseCode, + SupportedLanguages, + UnitTierNoteSingleResponse, +} from '../../../../api-def/api'; +import {Application, createApp} from '../../../../app'; +import {TierNote, UnitTierNote} from '../model'; + + +describe('Single unit tier note handler', () => { + let app: Application; + + beforeAll(async () => { + app = await createApp(); + }); + + beforeEach(async () => { + await app.reset(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('returns null if no corresponding tier note available yet', async () => { + const response = await app.app.inject().get(ApiEndPoints.TIER_UNIT).query({ + uid: '', + lang: SupportedLanguages.CHT, + unitId: 10950101, + }); + expect(response.statusCode).toBe(200); + + const json: UnitTierNoteSingleResponse = response.json() as UnitTierNoteSingleResponse; + expect(json.code).toBe(ApiResponseCode.SUCCESS); + expect(json.success).toBe(true); + expect(json.data).toStrictEqual(null); + }); + + it('returns data in specified language', async () => { + const dataArray = [ + new UnitTierNote({ + unitId: 10950101, + points: [], + tier: {conAi: new TierNote({ranking: 'S', note: {[SupportedLanguages.CHT]: 'A'}, isCompDependent: true})}, + lastUpdateEpoch: 0, + }), + ].map((entry) => entry.toObject()); + await UnitTierNote.getCollection(app.mongoClient).insertMany(dataArray); + + const response = await app.app.inject().get(ApiEndPoints.TIER_UNIT).query({ + uid: '', + lang: SupportedLanguages.CHT, + unitId: 10950101, + }); + expect(response.statusCode).toBe(200); + + const json: UnitTierNoteSingleResponse = response.json() as UnitTierNoteSingleResponse; + expect(json.code).toBe(ApiResponseCode.SUCCESS); + expect(json.success).toBe(true); + expect(json.data).toStrictEqual({ + points: [], + tier: {conAi: {ranking: 'S', note: 'A', isCompDependent: true}}, + lastUpdateEpoch: 0, + }); + }); + + it('returns data in alternative language if desired one does not exist', async () => { + const dataArray = [ + new UnitTierNote({ + unitId: 10950101, + points: [], + tier: {conAi: new TierNote({ranking: 'S', note: {[SupportedLanguages.CHT]: 'A'}, isCompDependent: true})}, + lastUpdateEpoch: 0, + }), + ].map((entry) => entry.toObject()); + await UnitTierNote.getCollection(app.mongoClient).insertMany(dataArray); + + const response = await app.app.inject().get(ApiEndPoints.TIER_UNIT).query({ + uid: '', + lang: SupportedLanguages.EN, + unitId: 10950101, + }); + expect(response.statusCode).toBe(200); + + const json: UnitTierNoteSingleResponse = response.json() as UnitTierNoteSingleResponse; + expect(json.code).toBe(ApiResponseCode.SUCCESS); + expect(json.success).toBe(true); + expect(json.data).toStrictEqual({ + points: [], + tier: {conAi: {ranking: 'S', note: 'A', isCompDependent: true}}, + lastUpdateEpoch: 0, + }); + }); +}); diff --git a/src/endpoints/tier/notes/single/handler.ts b/src/endpoints/tier/notes/single/handler.ts new file mode 100644 index 0000000..298c976 --- /dev/null +++ b/src/endpoints/tier/notes/single/handler.ts @@ -0,0 +1,17 @@ +import {UnitTierNoteEditPayload} from '../../../../api-def/api'; +import {processTierNoteSinglePayload} from '../../../../utils/payload/tier'; +import {HandlerParams} from '../../../lookup'; +import {TierNoteController} from '../controller'; +import {UnitTierNoteSingleResponse} from './response'; + + +export const handleTierNoteSingle = async ({ + payload, + mongoClient, +}: HandlerParams): Promise => { + payload = processTierNoteSinglePayload(payload); + + const data = await TierNoteController.getUnitTierNoteSingle(mongoClient, payload.lang, payload.unitId); + + return new UnitTierNoteSingleResponse({data}); +}; diff --git a/src/endpoints/tier/notes/single/response.ts b/src/endpoints/tier/notes/single/response.ts new file mode 100644 index 0000000..bc0f07e --- /dev/null +++ b/src/endpoints/tier/notes/single/response.ts @@ -0,0 +1,40 @@ +import { + ApiResponseCode, + BaseResponse, + UnitTierNote, + UnitTierNoteSingleResponse as UnitTierNoteSingleResponseApi, +} from '../../../../api-def/api'; +import {ApiResponse} from '../../../../base/response'; + + +type UnitTierNoteSingleResponseOptions = Omit; + +/** + * API response class for the endpoint to get the unit tier notes of a unit. + */ +export class UnitTierNoteSingleResponse extends ApiResponse { + data: UnitTierNote | null; + + /** + * Construct a single unit tier notes endpoint API response. + * + * @param {UnitTierNoteEditResponseOptions} options options to construct a single unit tier notes response + */ + constructor(options: UnitTierNoteSingleResponseOptions) { + const responseCode = ApiResponseCode.SUCCESS; + + super(responseCode); + + this.data = options.data; + } + + /** + * @inheritDoc + */ + toJson(): UnitTierNoteSingleResponseApi { + return { + ...super.toJson(), + data: this.data, + }; + } +} diff --git a/src/endpoints/tier/notes/update/response.ts b/src/endpoints/tier/notes/update/response.ts index a91fbdc..474be69 100644 --- a/src/endpoints/tier/notes/update/response.ts +++ b/src/endpoints/tier/notes/update/response.ts @@ -3,7 +3,7 @@ import {ApiResponse} from '../../../../base/response'; /** - * API response class for the endpoint to update the tier notes of an unit. + * API response class for the endpoint to update the tier notes of a unit. */ export class UnitTierNoteUpdateResponse extends ApiResponse { /** diff --git a/src/utils/payload/tier.ts b/src/utils/payload/tier.ts index 37da8ec..7902546 100644 --- a/src/utils/payload/tier.ts +++ b/src/utils/payload/tier.ts @@ -1,4 +1,4 @@ -import {UnitTierNoteEditPayload, UnitTierNoteUpdatePayload} from '../../api-def/api'; +import {UnitTierNoteEditPayload, UnitTierNoteSinglePayload, UnitTierNoteUpdatePayload} from '../../api-def/api'; import {processPayloadBase} from './base'; @@ -11,6 +11,10 @@ const processPayloadHasUnitId =

(payload: P): P => { return payload; }; +export const processTierNoteSinglePayload =

(payload: P): P => { + return processPayloadHasUnitId(payload); +}; + export const processTierNoteEditPayload =

(payload: P): P => { return processPayloadHasUnitId(payload); }; diff --git a/test/data/resources b/test/data/resources index 416a6bd..5008a95 160000 --- a/test/data/resources +++ b/test/data/resources @@ -1 +1 @@ -Subproject commit 416a6bdee0cb7b94d6543a57abc51e3eb3f39784 +Subproject commit 5008a95d96e3521ff3e6e1caa5a0d8035be9ca8a