Skip to content

Dictionary Migration - Get migration endpoints#200

Merged
leoraba merged 42 commits into
feat/dictionary_migrationfrom
feat/get_migration_endpoints
May 12, 2026
Merged

Dictionary Migration - Get migration endpoints#200
leoraba merged 42 commits into
feat/dictionary_migrationfrom
feat/get_migration_endpoints

Conversation

@leoraba
Copy link
Copy Markdown
Contributor

@leoraba leoraba commented Apr 16, 2026

Description

This PR creates 3 new GET endpoints to retrieve migrations information.

  • GET /migration/category/{categoryId}:
  • GET /migration/{migrationId}
  • GET /migration/{migrationId}/records

Related tickets:

This PR depends on:

@leoraba leoraba changed the base branch from main to feat/migration_implementation April 16, 2026 17:26
@leoraba leoraba linked an issue Apr 16, 2026 that may be closed by this pull request
Copy link
Copy Markdown
Contributor

@joneubank joneubank left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor changes here. Good work.

Comment thread apps/server/swagger/schemas.yml Outdated
Comment on lines +498 to +500
retries:
type: number
description: Number of retries of the migration
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we explain "retries" anywhere? this is a confusing data point.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the description for this field "Number of times the migration has run. When a migration fails, it can be retried by registering again the same dictionary."

However, a proper documentation is added in the following PR, where the "retry" functionality is implemented

Comment on lines +15 to +16
const defaultPage = 1;
const defaultPageSize = 20;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe should be global constants defined with the pagination types?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved this constants to a new shared file.

try {
const migrationId = Number(req.params.migrationId);

logger.info(LOG_MODULE, `Request Migration id '${migrationId}'`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably be debug level

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment applies this to all similar request detail logging in this controller.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we add a info log for every request in every controller because we're not using express request logger.

Comment on lines +27 to +28
if (!submission) {
throw new NotFound('Submission not found');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we be logging that the migration was not found?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 code changed as suggested

Comment on lines +45 to +47
if (submissions.result.length === 0) {
throw new NotFound('Submissions not found');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the category ID exists, this should return an empty list.

If the category ID does not exist, then it should return NotFound.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code changed to return NotFound when category does not exists, also changed to return same error in other endpoints of this controller

}
};

const getMigrationById = async (migrationId: number): Promise<MigrationRecordWithRelations | undefined> => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment as previous... this needs to explain why it sometimes returns undefined instead of the returned type.

While I prefer using undefined for missing values, the rest of the Service functions return null instead. It is confusing to have both values used in an ad-hoc way. Let's consistently use either null or undefined

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for consistency, will return null when migration is not found, and will update tsdocs

const getMigrationsByCategoryId = async (
categoryId: number,
paginationOptions: PaginationOptions,
): Promise<{ metadata: { totalRecords: number; errorMessage?: string }; result: MigrationRecordWithRelations[] }> => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to create a type alias for this return and export it for use where this function is consumed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a type PaginatedResult<T> to be used in this and other functions

Comment on lines +116 to +119
} catch (error) {
logger.error(LOG_MODULE, `Error retrieving migrations for categoryId '${categoryId}'`, error);
throw error;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a troubling pattern to fall into, where every function that could throw an error just logs the error then throws it. You can get a long log where every function in the call stack logs the same error with no new information added. Generally, we want to log errors where we catch and handle them.

We could instead make a design decision here, or in any function within this call stack, that the caught error can be handled here instead of thrown again. We can replace our return value either with void return type if there is exactly only one expected failure condition, or replace the return type with a Result that has a list of known failure conditions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the try-catch here as the error is unhandled in this function, it should be handled by the upper function (controller) that uses this function, repeated this logic in other functions in this Service.

organizations?: string | string[];
isInvalid?: boolean;
},
): Promise<{ result: AuditDataResponse[]; metadata: { totalRecords: number; errorMessage?: string } }> => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another place for a helpful type alias.

* @param migration
* @returns
*/
export const formatMigrationSummary = (migration: MigrationSummary): MigrationSummary => ({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is to make the return type from the route nicely formatted for presentation. If this is important to enforce, there should be a unit test for this on the route.

Don't spend much time on this next comment, but: this may be a good use for branded types. You can create a Branded type MigrationSummaryFormatted and have the controller require that it returns this branded type, that will enforce that this function is called before returning. This will enforce the rule through the type system, but since its only used for the route return type there's not a lot gained for the amount of effort here, so its not worth doin. Writing a test is the best way to enforce this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for the purpose of this PR, I added unit test for this function to verify it, I would considerate using branded types as suggested in future PRs

Base automatically changed from feat/migration_implementation to feat/dictionary_migration April 30, 2026 04:22
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unit tests added for migration formatter functions

@leoraba
Copy link
Copy Markdown
Contributor Author

leoraba commented May 4, 2026

PR has been updated based on the feedback received, it is now ready for code review.

@leoraba leoraba requested a review from joneubank May 4, 2026 13:43
records: migrationsResult.result.map(formatMigrationSummary),
};

return res.send(response);
Copy link
Copy Markdown
Contributor

@JamesTLopez JamesTLopez May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should send a status 200

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added .status(200) in the response

* @param {string} filterOptions.systemId
* and additional filters
* @param {number} categoryId Category ID to filter the Audit records
* @param {object} filterOptions Additional filters and pagination options
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of {object} AuditFilterOptions might be better for the tsDocs

Copy link
Copy Markdown
Contributor

@joneubank joneubank May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Responding to James' comment:

Instead of {object} AuditFilterOptions might be better for the tsDocs

Alternately, TS captures our function interfaces. It is entirely acceptable to remove the param annotations, or just the type information, from the TSDoc. These properties are useful if there is something about the parameter that needs to be described - like maybe it is used in a confusing way by the function logic and the caller needs to know that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just removed the type in the tsdocs to avoid unnecessary information in the docs

status: Extract<MigrationStatus, 'COMPLETED' | 'FAILED'>;
userName: string;
}): Promise<Result<null, string>> => {
}): AsyncResult<null, string> => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use the AsyncResult pattern for the remainder of the services in this file? Or is this only used for create/update actions?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long term, I would like to. Not required for scope for this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for simplicity just using the AsyncResult pattern in the actionables functions: initiate, perform and finalize migrations.


/**
* Order the properties of the Migration Summary Record
* @param migration
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param migration
* @param {MigrationSummary} migration - The migration summary to format.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs updated as suggested

Copy link
Copy Markdown
Contributor

@joneubank joneubank left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very good. Thanks!

Comment on lines +65 to +75
Pagination:
type: object
properties:
currentPage:
type: number
pageSize:
type: number
totalPages:
type: number
totalRecords:
type: number
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment on lines +532 to +535
status:
type: string
description: Status of the Migration
enum: ['IN-PROGRESS', 'COMPLETED', 'FAILED']
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

status reports that the migration completed... is there anythign that indicates if any records have errors after the migration?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is invalidRecords I missed updating the openapi docs, now it reflects the right response

records: migrationsResult.result.map(formatMigrationSummary),
};

return res.send(response);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really minor point, but we should be using res.json(response) instead of .send.

They will be almost identical, or even exactly identical, in most cases. The point though is to indicate that we INTEND to be responding with JSON. If our response is not an object (like a string) then .send(someString) will happily send a response that does not have Content-Type application/json, and TS will not prevent us from returning this value.

There's also some weird cases where if you do send(400) it will set the 400 as the http status and send an empty body. Its best to avoid the "smart" logic in .send and use .json when you intend to return json.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code change to respond with .json(response) as suggested.

Comment on lines +53 to +63
if (organization && !isEmptyString(organization)) {
filterArray.push(eq(auditSubmittedData.organization, organization));
}

if (submissionId) {
filterArray.push(eq(auditSubmittedData.submissionId, submissionId));
}

if (newIsValid !== undefined) {
filterArray.push(eq(auditSubmittedData.newDataIsValid, newIsValid));
}
Copy link
Copy Markdown
Contributor

@joneubank joneubank May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Responding to:

The integration test is broken due to a previous change in the worker pool, it requires additional work out of the scope of this PR and should be fixed in a separate PR and extended to cover this function

Can you create an issue and link it?

* @param {string} filterOptions.systemId
* and additional filters
* @param {number} categoryId Category ID to filter the Audit records
* @param {object} filterOptions Additional filters and pagination options
Copy link
Copy Markdown
Contributor

@joneubank joneubank May 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Responding to James' comment:

Instead of {object} AuditFilterOptions might be better for the tsDocs

Alternately, TS captures our function interfaces. It is entirely acceptable to remove the param annotations, or just the type information, from the TSDoc. These properties are useful if there is something about the parameter that needs to be described - like maybe it is used in a confusing way by the function logic and the caller needs to know that.

};
}

const newIsValid = options.isInvalid !== undefined ? !options.isInvalid : undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot this. Thanks for clarification.

status: Extract<MigrationStatus, 'COMPLETED' | 'FAILED'>;
userName: string;
}): Promise<Result<null, string>> => {
}): AsyncResult<null, string> => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Long term, I would like to. Not required for scope for this PR.

@leoraba leoraba merged commit 23a3f96 into feat/dictionary_migration May 12, 2026
@leoraba leoraba deleted the feat/get_migration_endpoints branch May 12, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Part 2 - Data migration] Get Migrations endpoints

3 participants