@overture-stack/lyric
Version:
Data Submission system
373 lines (372 loc) • 12.5 kB
JavaScript
import { z as zod } from 'zod';
import { isAuditEventValid, isSubmissionActionTypeValid } from './auditUtils.js';
import { parseSQON } from './convertSqonToQuery.js';
import { isValidDateFormat, isValidIdNumber } from './formatUtils.js';
import { VIEW_TYPE } from './types.js';
const auditEventTypeSchema = zod
.string()
.trim()
.min(1)
.refine((value) => isAuditEventValid(value), 'invalid Event Type');
const booleanSchema = zod
.string()
.toLowerCase()
.refine((value) => value === 'true' || value === 'false');
const viewSchema = zod.string().toLowerCase().trim().min(1).pipe(VIEW_TYPE);
const categoryIdSchema = zod
.string()
.trim()
.min(1)
.refine((value) => {
const parsed = parseInt(value);
return isValidIdNumber(parsed);
}, 'invalid category ID');
const endDateSchema = zod
.string()
.trim()
.min(1)
.refine((value) => isValidDateFormat(value), 'invalid `endDate` parameter');
const entityNameSchema = zod.string().trim().min(1);
const organizationSchema = zod.string().trim().min(1);
const pageSizeSchema = zod.string().superRefine((value, ctx) => {
const parsed = parseInt(value);
if (isNaN(parsed)) {
ctx.addIssue({
code: zod.ZodIssueCode.invalid_type,
expected: 'number',
received: 'nan',
});
}
if (parsed < 1) {
ctx.addIssue({
code: zod.ZodIssueCode.too_small,
minimum: 1,
inclusive: true,
type: 'number',
});
}
});
const indexIntegerSchema = zod.string().superRefine((value, ctx) => {
const parsed = parseInt(value);
if (isNaN(parsed)) {
ctx.addIssue({
code: zod.ZodIssueCode.invalid_type,
expected: 'number',
received: 'nan',
});
}
});
const positiveInteger = zod.string().superRefine((value, ctx) => {
const parsed = parseInt(value);
if (isNaN(parsed)) {
ctx.addIssue({
code: zod.ZodIssueCode.invalid_type,
expected: 'number',
received: 'nan',
});
}
if (parsed < 1) {
ctx.addIssue({
code: zod.ZodIssueCode.too_small,
minimum: 1,
inclusive: true,
type: 'number',
});
}
});
const sqonSchema = zod.custom((value) => {
try {
parseSQON(value);
return true;
}
catch (error) {
return false;
}
}, 'invalid SQON format');
const startDateSchema = zod
.string()
.trim()
.min(1)
.refine((value) => isValidDateFormat(value), 'invalid `startDate` parameter');
const submissionActionTypeSchema = zod
.string()
.trim()
.min(1)
.refine((value) => isSubmissionActionTypeValid(value), 'invalid Submission Action Type');
const submissionIdSchema = zod
.string()
.trim()
.min(1)
.refine((value) => {
const parsed = parseInt(value);
return isValidIdNumber(parsed);
}, 'invalid submission ID');
const migrationIdSchema = zod
.string()
.trim()
.min(1)
.refine((value) => {
const parsed = parseInt(value);
return isValidIdNumber(parsed);
}, 'invalid migration ID');
const stringNotEmpty = zod.string().trim().min(1);
export const categoryPathParamsSchema = zod.object({
categoryId: categoryIdSchema,
});
// Common Category and Organization Path Params
export const categoryOrganizationPathParamsSchema = zod.object({
categoryId: categoryIdSchema,
organization: organizationSchema,
});
// Common Category, Organization, and EntityName Path Params
export const categoryOrganizationEntityPathParamsSchema = zod.object({
categoryId: categoryIdSchema,
organizationId: organizationSchema,
entityName: entityNameSchema,
});
const submissionIdPathParamSchema = zod.object({
submissionId: submissionIdSchema,
});
const migrationIdPathParamSchema = zod.object({
migrationId: migrationIdSchema,
});
const paginationQuerySchema = zod.object({
page: positiveInteger.optional(),
pageSize: pageSizeSchema.optional(),
});
const auditQuerySchema = zod
.object({
entityName: entityNameSchema.optional(),
eventType: auditEventTypeSchema.optional(),
systemId: stringNotEmpty.optional(),
startDate: startDateSchema.optional(),
endDate: endDateSchema.optional(),
})
.merge(paginationQuerySchema);
export const auditByCatAndOrgRequestSchema = {
query: auditQuerySchema,
pathParams: categoryOrganizationPathParamsSchema,
};
// Category Request
export const categoryDetailsRequestSchema = {
pathParams: categoryPathParamsSchema,
};
export const dictionaryRegisterRequestSchema = {
body: zod.object({
categoryName: stringNotEmpty,
dictionaryName: stringNotEmpty,
dictionaryVersion: stringNotEmpty,
defaultCentricEntity: entityNameSchema.or(zod.literal('')).optional(),
}),
query: zod.object({
force: booleanSchema.default('false'),
}),
};
// Migration Requests
export const migrationByIdRequestSchema = {
pathParams: migrationIdPathParamSchema,
};
export const migrationsByCategoryIdRequestSchema = {
pathParams: categoryPathParamsSchema,
query: paginationQuerySchema,
};
export const migrationDataRequestSchema = {
pathParams: migrationIdPathParamSchema,
query: zod
.object({
entityNames: zod.union([entityNameSchema, entityNameSchema.array()]).optional(),
organizations: zod.union([organizationSchema, organizationSchema.array()]).optional(),
isInvalid: booleanSchema.default('false'),
})
.merge(paginationQuerySchema),
};
export const submissionsByCategoryRequestSchema = {
query: zod.object({
onlyActive: booleanSchema.default('false'),
organization: organizationSchema.optional(),
username: stringNotEmpty.optional(),
}),
pathParams: categoryPathParamsSchema,
};
export const submissionByIdRequestSchema = {
pathParams: submissionIdPathParamSchema,
};
export const submissionDetailsRequestSchema = {
query: zod
.object({
entityNames: zod.union([entityNameSchema, entityNameSchema.array()]).optional(),
actionTypes: zod.union([submissionActionTypeSchema, submissionActionTypeSchema.array()]).optional(),
})
.merge(paginationQuerySchema),
pathParams: submissionIdPathParamSchema,
};
export const submissionActiveByOrganizationRequestSchema = {
pathParams: categoryOrganizationPathParamsSchema,
};
export const submissionCommitRequestSchema = {
pathParams: zod.object({
categoryId: categoryIdSchema,
submissionId: submissionIdSchema,
}),
};
export const submissionDeleteRequestSchema = {
pathParams: submissionIdPathParamSchema,
query: zod.object({
force: booleanSchema.default('false'),
}),
};
export const submissionDeleteEntityNameRequestSchema = {
query: zod.object({
entityName: entityNameSchema,
index: indexIntegerSchema.optional(),
}),
pathParams: zod.object({
actionType: submissionActionTypeSchema,
submissionId: submissionIdSchema,
}),
};
const uploadSubmissionQueryParams = zod.object({
entityName: entityNameSchema,
organization: organizationSchema,
});
const submissionUploadFilesQueryParams = zod.object({
organization: organizationSchema,
sync: zod.enum(['true', 'false']).optional(),
});
export const filenameEntityPair = zod.object({
filename: zod.string(),
entity: zod.string(),
});
const dataRecordValueSchema = zod.union([
zod.string(),
zod.number(),
zod.boolean(),
zod.array(zod.string()),
zod.array(zod.number()),
zod.array(zod.boolean()),
zod.undefined(),
]);
const dataRecordSchema = zod.record(dataRecordValueSchema);
export const uploadSubmissionRequestSchema = {
pathParams: categoryPathParamsSchema,
query: submissionUploadFilesQueryParams,
// Multer populates req.body with string-valued form fields from multipart requests.
// When fileEntityMap is sent as a JSON-encoded form field, req.body is { fileEntityMap: '...' }.
// When no text fields are sent, req.body is an empty null-prototype object {}.
// The preprocess step extracts and parses the fileEntityMap field when present,
// and coerces all other values (empty object, non-array) to undefined.
body: zod.preprocess((value) => {
if (Array.isArray(value)) {
return value;
}
if (typeof value !== 'string') {
return undefined;
}
try {
const parsed = JSON.parse(value);
// The value provided by Swagger will be an array encoded as a string, where and the content
// of that array may be either JSON objects, or stringified JSON objects. We will accomodate
// an input of either form since the formatting is ambiguous. In particular, the swagger interface
// will stringify each element of the array so we require the extra type check inside the map over
// the parsed input, but a developer may want to simply build the entire array and then stringify
// the entire thing. Both are reasonable.
return Array.isArray(parsed)
? parsed.map((item) => (typeof item === 'string' ? JSON.parse(item) : item))
: [parsed];
}
catch {
return undefined;
}
}, zod.array(filenameEntityPair).optional()),
};
export const uploadSingleEntitySubmissionDataRequestSchema = {
body: dataRecordSchema.array(),
query: uploadSubmissionQueryParams,
pathParams: categoryPathParamsSchema,
};
export const dataDeleteBySystemIdRequestSchema = {
pathParams: zod.object({
systemId: stringNotEmpty,
categoryId: categoryIdSchema,
}),
};
// TODO: Need type validation for the edit request schema
export const editSingleEntityRequestSchema = {
body: zod.record(zod.unknown()).array(),
query: uploadSubmissionQueryParams,
pathParams: categoryPathParamsSchema,
};
export const dataGetByCategoryRequestSchema = {
query: zod
.object({
entityName: zod.union([entityNameSchema, entityNameSchema.array()]).optional(),
view: viewSchema.optional(),
})
.merge(paginationQuerySchema)
.superRefine((data, ctx) => {
if (data.view === VIEW_TYPE.Values.compound && data.entityName && data.entityName?.length > 0) {
ctx.addIssue({
code: zod.ZodIssueCode.custom,
message: 'is incompatible with `compound` view',
path: ['entityName'],
});
}
}),
pathParams: categoryPathParamsSchema,
};
export const dataGetByOrganizationRequestSchema = {
query: zod
.object({
entityName: zod.union([entityNameSchema, entityNameSchema.array()]).optional(),
view: viewSchema.optional(),
})
.merge(paginationQuerySchema)
.superRefine((data, ctx) => {
if (data.view === VIEW_TYPE.Values.compound && data.entityName && data.entityName?.length > 0) {
ctx.addIssue({
code: zod.ZodIssueCode.custom,
message: 'is incompatible with `compound` view',
path: ['entityName'],
});
}
}),
pathParams: categoryOrganizationPathParamsSchema,
};
export const dataGetByQueryRequestSchema = {
body: sqonSchema,
query: zod
.object({
entityName: zod.union([entityNameSchema, entityNameSchema.array()]).optional(),
})
.merge(paginationQuerySchema),
pathParams: categoryOrganizationPathParamsSchema,
};
export const DataGetBySystemIdRequestSchema = {
query: zod.object({
view: viewSchema.optional(),
}),
pathParams: zod.object({
systemId: stringNotEmpty,
categoryId: categoryIdSchema,
}),
};
export const downloadDataFileTemplatesSchema = {
query: zod.object({
fileType: zod.enum(['csv', 'tsv']).optional(),
}),
pathParams: zod.object({
categoryId: categoryIdSchema,
}),
};
export const validationPathParamsSchema = zod.object({
categoryId: categoryIdSchema,
entityName: entityNameSchema,
});
const validationQuerySchema = zod.object({
organization: organizationSchema,
value: stringNotEmpty,
});
export const validationRequestSchema = {
query: validationQuerySchema,
pathParams: validationPathParamsSchema,
};