diff --git a/apps/server/swagger/dictionary-api.yml b/apps/server/swagger/dictionary-api.yml index a77c029..b70dbe4 100644 --- a/apps/server/swagger/dictionary-api.yml +++ b/apps/server/swagger/dictionary-api.yml @@ -2,9 +2,17 @@ /dictionary/register: post: - summary: Register new dictionary + summary: Register a dictionary to a category. Creates the category if it does not exist, or updates its active dictionary and triggers a data migration to validate and update existing data against the new dictionary. tags: - Dictionary + parameters: + - name: force + description: Retries migration for this category only when the same dictionary is already registered and the most recent migration previously failed. If there is no previous failed migration, this flag is ignored. + in: query + required: false + schema: + type: boolean + default: false requestBody: content: application/json: @@ -38,8 +46,8 @@ $ref: '#/components/responses/BadRequest' 401: $ref: '#/components/responses/UnauthorizedError' - 404: - $ref: '#/components/responses/NotFound' + 409: + $ref: '#/components/responses/StatusConflict' 500: $ref: '#/components/responses/ServerError' 503: diff --git a/packages/data-provider/docs/dictionary-registration.md b/packages/data-provider/docs/dictionary-registration.md index 4e8195e..46713e8 100644 --- a/packages/data-provider/docs/dictionary-registration.md +++ b/packages/data-provider/docs/dictionary-registration.md @@ -108,6 +108,8 @@ A Category is uniquely identified by its case-sensitive `categoryName`. Category groups data that is related and shares the same data structure, for that reason, a category must be associated to a registered dictionary. Over time, if the dictionary requires an update, the category needs to be updates accordingly, See [Dictionary Migration](#dictionary-migration) for more details. +If a category is already using the same dictionary name and version, registration returns `409 Conflict` by default. If the `force` query parameter is set to `true`, Lyric retries only when the same dictionary is already registered and the latest migration previously failed. If no prior failed migration exists, the `force` flag is ignored. + ## Centric entity Some dictionaries define a centric entity, representing the root of the data model hierarchy (used on compound views). diff --git a/packages/data-provider/src/controllers/dictionaryController.ts b/packages/data-provider/src/controllers/dictionaryController.ts index 16d9d22..5435fab 100644 --- a/packages/data-provider/src/controllers/dictionaryController.ts +++ b/packages/data-provider/src/controllers/dictionaryController.ts @@ -22,6 +22,7 @@ const controller = (dependencies: BaseDependencies) => { const dictionaryName = req.body.dictionaryName; const dictionaryVersion = req.body.dictionaryVersion; const defaultCentricEntity = req.body.defaultCentricEntity; + const forceRegistration = req.query.force?.toLowerCase() === 'true'; const user = req.user; logger.info( @@ -35,6 +36,7 @@ const controller = (dependencies: BaseDependencies) => { dictionaryVersion, defaultCentricEntity, username: user?.username, + forceRegistration, }); logger.info(LOG_MODULE, `Register Dictionary completed!`); diff --git a/packages/data-provider/src/services/dictionaryService.ts b/packages/data-provider/src/services/dictionaryService.ts index f16ccc5..c8e2bf0 100644 --- a/packages/data-provider/src/services/dictionaryService.ts +++ b/packages/data-provider/src/services/dictionaryService.ts @@ -7,7 +7,7 @@ import { BaseDependencies } from '../config/config.js'; import lecternClient from '../external/lecternClient.js'; import categoryRepository from '../repository/categoryRepository.js'; import dictionaryRepository from '../repository/dictionaryRepository.js'; -import { BadRequest } from '../utils/errors.js'; +import { BadRequest, StatusConflict } from '../utils/errors.js'; import migrationService from './migrationService.js'; const dictionaryService = (dependencies: BaseDependencies) => { @@ -99,12 +99,14 @@ const dictionaryService = (dependencies: BaseDependencies) => { dictionaryVersion, defaultCentricEntity, username, + forceRegistration = false, }: { categoryName: string; dictionaryName: string; dictionaryVersion: string; defaultCentricEntity?: string; username?: string; + forceRegistration?: boolean; }): Promise<{ dictionary: Dictionary; category: Category; migrationId?: number }> => { logger.debug( LOG_MODULE, @@ -112,7 +114,7 @@ const dictionaryService = (dependencies: BaseDependencies) => { ); const categoryRepo = categoryRepository(dependencies); - const { initiateMigration } = migrationService(dependencies); + const { initiateMigration, getMigrationsByCategoryId } = migrationService(dependencies); const dictionary = await fetchDictionaryByVersion(dictionaryName, dictionaryVersion); @@ -131,11 +133,62 @@ const dictionaryService = (dependencies: BaseDependencies) => { // Check if Category exist const foundCategory = await categoryRepo.getCategoryByName(categoryName); + const initiateMigrationOrThrow = async ({ + categoryId, + fromDictionaryId, + toDictionaryId, + }: { + categoryId: number; + fromDictionaryId: number; + toDictionaryId: number; + }): Promise => { + const resultMigration = await initiateMigration({ + categoryId, + fromDictionaryId, + toDictionaryId, + userName: username || '', + }); + + if (!resultMigration.success) { + const errorMessage = `Failed to initiate migration for category '${categoryName}' with error: ${resultMigration.data}`; + logger.error(LOG_MODULE, errorMessage); + throw new Error(errorMessage); + } + + return resultMigration.data; + }; + if (foundCategory && foundCategory.activeDictionaryId === savedDictionary.id) { // Dictionary and Category already exists logger.info(LOG_MODULE, `Dictionary and Category already exists`); - return { dictionary: savedDictionary, category: foundCategory }; + // check last migration of this category to find if it failed, if it failed and forceRegistration is true, + // we will re-initiate the migration with the new dictionary + const activeMigration = await getMigrationsByCategoryId(foundCategory.id, { pageSize: 1, page: 1 }); + + if ( + forceRegistration && + activeMigration.result.length === 1 && + activeMigration.result.at(0)?.status === 'FAILED' + ) { + logger.info( + LOG_MODULE, + `Force flag is true, initiating migration for Category '${foundCategory.name}' + with Dictionary '${savedDictionary.name}' version '${savedDictionary.version}'`, + ); + + const migrationId = await initiateMigrationOrThrow({ + categoryId: foundCategory.id, + fromDictionaryId: foundCategory.activeDictionaryId, + toDictionaryId: savedDictionary.id, + }); + + return { dictionary: savedDictionary, category: foundCategory, migrationId }; + } + + throw new StatusConflict( + `Category '${categoryName}' with Dictionary '${savedDictionary.name}' version '${savedDictionary.version}' already exists`, + ); } else if (foundCategory && foundCategory.activeDictionaryId !== savedDictionary.id) { // Update the dictionary on existing Category const updatedCategory = await categoryRepo.update(foundCategory.id, { @@ -144,25 +197,18 @@ const dictionaryService = (dependencies: BaseDependencies) => { updatedBy: username, }); - const resultMigration = await initiateMigration({ + const migrationId = await initiateMigrationOrThrow({ categoryId: updatedCategory.id, fromDictionaryId: foundCategory.activeDictionaryId, toDictionaryId: savedDictionary.id, - userName: username || '', }); - if (!resultMigration.success) { - const errorMessage = `Failed to initiate migration for category '${categoryName}' with error: ${resultMigration.data}`; - logger.error(LOG_MODULE, errorMessage); - throw new Error(errorMessage); - } - logger.info( LOG_MODULE, `Category '${updatedCategory.name}' updated successfully with Dictionary '${savedDictionary.name}' version '${savedDictionary.version}'`, ); - return { dictionary: savedDictionary, category: updatedCategory, migrationId: resultMigration.data }; + return { dictionary: savedDictionary, category: updatedCategory, migrationId }; } else { // Create a new Category const newCategory: NewCategory = { diff --git a/packages/data-provider/src/utils/schemas.ts b/packages/data-provider/src/utils/schemas.ts index 742887e..1898529 100644 --- a/packages/data-provider/src/utils/schemas.ts +++ b/packages/data-provider/src/utils/schemas.ts @@ -234,9 +234,13 @@ export interface DictionaryRegisterBodyParams { defaultCentricEntity?: string; } +export interface DictionaryRegisterQueryParams extends ParsedQs { + force?: string; +} + export const dictionaryRegisterRequestSchema: RequestValidation< DictionaryRegisterBodyParams, - ParsedQs, + DictionaryRegisterQueryParams, ParamsDictionary > = { body: z.object({ @@ -245,6 +249,9 @@ export const dictionaryRegisterRequestSchema: RequestValidation< dictionaryVersion: stringNotEmpty, defaultCentricEntity: entityNameSchema.or(z.literal('')).optional(), }), + query: z.object({ + force: booleanSchema.default('false'), + }), }; // Migration Requests