@overture-stack/lyric
Version:
Data Submission system
312 lines (311 loc) • 15.3 kB
JavaScript
import { and, count, eq, or, sql } from 'drizzle-orm/sql';
import * as _ from 'lodash-es';
import { auditSubmittedData, submittedData, } from '@overture-stack/lyric-data-model/models';
import { ServiceUnavailable } from '../utils/errors.js';
import { AUDIT_ACTION } from '../utils/types.js';
const repository = (dependencies) => {
const LOG_MODULE = 'SUBMITTEDDATA_REPOSITORY';
const { db, logger, features } = dependencies;
const auditDeleteSubmittedData = async (input, tx) => {
const { recordDeleted, diff, submissionId, username } = input;
const newAudit = {
action: AUDIT_ACTION.Values.DELETE,
dictionaryCategoryId: recordDeleted.dictionaryCategoryId,
entityName: recordDeleted.entityName,
lastValidSchemaId: recordDeleted.lastValidSchemaId,
newDataIsValid: false,
dataDiff: diff,
oldDataIsValid: recordDeleted.isValid,
organization: recordDeleted.organization,
originalSchemaId: recordDeleted.originalSchemaId,
submissionId: submissionId,
systemId: recordDeleted.systemId,
createdAt: new Date(),
createdBy: username,
};
return await (tx || db).insert(auditSubmittedData).values(newAudit);
};
const auditUpdateSubmittedData = async ({ dataDiff, oldIsValid, recordUpdated, submissionId, }, tx) => {
const newAudit = {
action: AUDIT_ACTION.Values.UPDATE,
dictionaryCategoryId: recordUpdated.dictionaryCategoryId,
entityName: recordUpdated.entityName,
lastValidSchemaId: recordUpdated.lastValidSchemaId,
newDataIsValid: recordUpdated.isValid,
dataDiff: dataDiff,
oldDataIsValid: oldIsValid,
organization: recordUpdated.organization,
originalSchemaId: recordUpdated.originalSchemaId,
submissionId: submissionId,
systemId: recordUpdated.systemId,
createdAt: new Date(),
createdBy: recordUpdated.updatedBy,
};
return await (tx || db).insert(auditSubmittedData).values(newAudit);
};
// Column name on the database used to build JSONB query
const jsonbColumnName = submittedData.data.name;
const paginatedColumns = {
entityName: true,
data: true,
organization: true,
isValid: true,
systemId: true,
};
/**
* Build a SQL object to search submitted data by entity Name
* @param {string[]} entityNameArray
* @returns {SQL<unknown> | undefined}
*/
const filterByEntityNameArray = (entityNameArray) => {
if (Array.isArray(entityNameArray)) {
return or(...entityNameArray
.filter((entity) => entity !== undefined)
.map((entity) => eq(submittedData.entityName, entity.trim())));
}
return undefined;
};
return {
/**
* Deletes a submitted data record by its system ID, logs the deletion, and optionally audits the deletion if auditing is enabled.
* @param params The parameters for the deletion operation.
* @param params.diff The difference between the old and new data, used for auditing
* @param params.submissionId The ID of the Submission associated with the record
* @param params.systemId The unique identifier of the record to delete
* @param params.username The name of the user performing the deletion
* @param tx The transaction to use for the operation, optional
* @returns The deleted record
*/
deleteBySystemId: async (params, tx) => {
const { diff, systemId, submissionId, username } = params;
const deletedRecord = await (tx || db)
.delete(submittedData)
.where(eq(submittedData.systemId, systemId))
.returning();
logger.info(LOG_MODULE, `Deleting SubmittedData with system ID '${systemId}' succesfully`);
if (features?.audit?.enabled) {
await auditDeleteSubmittedData({ recordDeleted: deletedRecord[0], submissionId, diff, username }, tx);
}
return deletedRecord;
},
/**
* Save new SubmittedData in Database
* @param data A SubmittedData object to be saved
* @param tx The transaction to use for the operation, optional
* @returns The created SubmittedData
*/
save: async (data, tx) => {
try {
const savedSubmittedData = await (tx || db).insert(submittedData).values(data).returning();
logger.debug(LOG_MODULE, `Submitting Data with entity name '${data.entityName}' on category '${data.dictionaryCategoryId}' saved successfully`);
return savedSubmittedData[0];
}
catch (error) {
logger.error(LOG_MODULE, `Failed Submitted Data with entity name '${data.entityName}' on category '${data.dictionaryCategoryId}'`, error);
throw error;
}
},
/**
* Returns a list of all organizations found by category ID
* @param {number} categoryId
* @returns
*/
getAllOrganizationsByCategoryId: async (categoryId) => {
try {
const resultQuery = await db
.selectDistinct({ organization: submittedData.organization })
.from(submittedData)
.where(eq(submittedData.dictionaryCategoryId, categoryId))
.orderBy(submittedData.organization);
return resultQuery.map((record) => record.organization);
}
catch (error) {
logger.error(LOG_MODULE, `Failed querying SubmittedData with categoryId '${categoryId}'`, error);
throw new ServiceUnavailable();
}
},
/**
* Find SubmittedData by category ID and organization
* @param {number} categoryId Category ID
* @param {string} organization Organization Name
* @returns The SubmittedData found
*/
getSubmittedDataByCategoryIdAndOrganization: async (categoryId, organization) => {
try {
return await db.query.submittedData.findMany({
where: and(eq(submittedData.dictionaryCategoryId, categoryId), eq(submittedData.organization, organization)),
});
}
catch (error) {
logger.error(LOG_MODULE, `Failed querying SubmittedData with categoryId '${categoryId}'`, error);
throw new ServiceUnavailable();
}
},
/**
* Find SubmittedData by category ID with pagination
* @param {number} categoryId Category ID
* @param {PaginationOptions} paginationOptions Pagination properties
* @param {object} filter Filter Options
* @param {string[] | undefined} filter.entityNames Array of entity names to filter
* @returns The SubmittedData found
*/
getSubmittedDataByCategoryIdPaginated: async (categoryId, paginationOptions, filter) => {
const { page, pageSize } = paginationOptions;
const filterEntityNameSql = filterByEntityNameArray(filter?.entityNames);
try {
return await db.query.submittedData.findMany({
where: and(eq(submittedData.dictionaryCategoryId, categoryId), filterEntityNameSql),
columns: paginatedColumns,
orderBy: (submittedData, { asc }) => [asc(submittedData.entityName), asc(submittedData.id)],
limit: pageSize,
offset: (page - 1) * pageSize,
});
}
catch (error) {
logger.error(LOG_MODULE, `Failed querying SubmittedData with categoryId '${categoryId}'`, error);
throw new ServiceUnavailable();
}
},
/**
* Find SubmittedData by category ID and Organization with pagination
* @param {number} categoryId Category ID
* @param {string} organization Organization Name
* @param {PaginationOptions} paginationOptions Pagination properties
* @param {object} filter Filter Options
* @param {SQL | undefined} filter.sql SQL command to filter
* @param {string[] | undefined} filter.entityNames Array of entity names to filter
* @returns The SubmittedData found
*/
getSubmittedDataByCategoryIdAndOrganizationPaginated: async (categoryId, organization, paginationOptions, filter) => {
const { page, pageSize } = paginationOptions;
const filterEntityNameSql = filterByEntityNameArray(filter?.entityNames);
try {
return await db.query.submittedData.findMany({
where: and(eq(submittedData.dictionaryCategoryId, categoryId), eq(submittedData.organization, organization), filter?.sql, filterEntityNameSql),
columns: paginatedColumns,
orderBy: (submittedData, { asc }) => [asc(submittedData.entityName), asc(submittedData.id)],
limit: pageSize,
offset: (page - 1) * pageSize,
});
}
catch (error) {
logger.error(LOG_MODULE, `Failed querying SubmittedData with categoryId '${categoryId}' organization '${organization}'`, error);
throw new ServiceUnavailable();
}
},
/**
* Counts the total of records found by Category and Organization
* @param {number} categoryId Category ID
* @param {string} organization Organization Name
* @param {object} filter Filter Options
* @param {SQL | undefined} filter.sql SQL command to filter
* @param {string[] | undefined} filter.entityNames Array of entity names to filter
* @returns Total number of recourds
*/
getTotalRecordsByCategoryIdAndOrganization: async (categoryId, organization, filter) => {
const filterEntityNameSql = filterByEntityNameArray(filter?.entityNames);
try {
const resultCount = await db
.select({ total: count() })
.from(submittedData)
.where(and(eq(submittedData.dictionaryCategoryId, categoryId), eq(submittedData.organization, organization), filter?.sql, filterEntityNameSql));
return resultCount[0].total;
}
catch (error) {
logger.error(LOG_MODULE, `Failed counting SubmittedData with categoryId '${categoryId}' organization '${organization}'`, error);
throw new ServiceUnavailable();
}
},
/**
* Counts the total of records found by Category
* @param {number} categoryId Category ID
* @param {object} filter Filter options
* @param {SQL | undefined} filter.sql SQL command
* @param {string[] | undefined} filter.entityNames Array of entity names to filter
* @returns Total number of recourds
*/
getTotalRecordsByCategoryId: async (categoryId, filter) => {
const filterEntityNameSql = filterByEntityNameArray(filter?.entityNames);
try {
const resultCount = await db
.select({ total: count() })
.from(submittedData)
.where(and(eq(submittedData.dictionaryCategoryId, categoryId), filterEntityNameSql));
return resultCount[0].total;
}
catch (error) {
logger.error(LOG_MODULE, `Failed counting SubmittedData with categoryId '${categoryId}'`, error);
throw new ServiceUnavailable();
}
},
/**
* Update a SubmittedData record in database
* @param submittedDataId Submitted Data ID
* @param dataDiff Difference before and after the updata
* @param newData Set fields to update
* @param oldIsValid Previous isValid value
* @param submissionId Submission ID
* @param tx The transaction to use for the operation, optional
* @returns An updated record
*/
update: async ({ submittedDataId, dataDiff, newData, oldIsValid, submissionId, }, tx) => {
try {
const updated = await (tx || db)
.update(submittedData)
.set({ ...newData, updatedAt: new Date() })
.where(eq(submittedData.id, submittedDataId))
.returning();
if (features?.audit?.enabled && !_.isEmpty(dataDiff.new) && !_.isEmpty(dataDiff.old)) {
await auditUpdateSubmittedData({ recordUpdated: updated[0], submissionId, dataDiff, oldIsValid }, tx);
}
return updated[0];
}
catch (error) {
logger.error(LOG_MODULE, `Failed updating SubmittedData`, error);
throw new ServiceUnavailable();
}
},
/**
* Query to retrieve an unique SubmittedData record searching by System ID
* Returns a SubmittedData record if found. Otherwise returns undefined
* @param {string} systemId
* @returns {Promise<SubmittedData | undefined>}
*/
getSubmittedDataBySystemId: async (systemId) => {
try {
return await db.query.submittedData.findFirst({
where: eq(submittedData.systemId, systemId),
});
}
catch (error) {
logger.error(LOG_MODULE, `Failed querying SubmittedData by systemId '${systemId}'`, error);
throw new ServiceUnavailable();
}
},
/**
* Query to retrieve submitted data filtered by JSONB field on an organization
* Returns an array of SubmittedData records found or an empty array if there are no matching records
* @param {string} organization
* @param {Object} filterData
* @param {string} filterData.entityName
* @param {string} filterData.dataField
* @param {string | undefined} filterData.dataValue
* @returns {Promise<SubmittedData[]>}
*/
getSubmittedDataFiltered: async (organization, filterData) => {
const sqlDataFilter = filterData.map((filter) => {
return and(sql.raw(`${jsonbColumnName} ->> '${filter.dataField}' IN ('${filter.dataValue}')`), eq(submittedData.entityName, filter.entityName));
});
try {
return await db.query.submittedData.findMany({
where: and(or(...sqlDataFilter), eq(submittedData.organization, organization)),
});
}
catch (error) {
logger.error(LOG_MODULE, `Failed querying SubmittedData`, error);
throw new ServiceUnavailable();
}
},
};
};
export default repository;