@overture-stack/lyric
Version:
Data Submission system
340 lines (339 loc) • 11.2 kB
JavaScript
import { z } 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 = z
.string()
.trim()
.min(1)
.refine((value) => isAuditEventValid(value), 'invalid Event Type');
const booleanSchema = z
.string()
.toLowerCase()
.refine((value) => value === 'true' || value === 'false');
const viewSchema = z.string().toLowerCase().trim().min(1).pipe(VIEW_TYPE);
const categoryIdSchema = z
.string()
.trim()
.min(1)
.refine((value) => {
const parsed = parseInt(value);
return isValidIdNumber(parsed);
}, 'invalid category ID');
const endDateSchema = z
.string()
.trim()
.min(1)
.refine((value) => isValidDateFormat(value), 'invalid `endDate` parameter');
const entityNameSchema = z.string().trim().min(1);
const organizationSchema = z.string().trim().min(1);
const pageSizeSchema = z.string().superRefine((value, ctx) => {
const parsed = parseInt(value);
if (isNaN(parsed)) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_type,
expected: 'number',
received: 'nan',
});
}
if (parsed < 1) {
ctx.addIssue({
code: z.ZodIssueCode.too_small,
minimum: 1,
inclusive: true,
type: 'number',
});
}
});
const indexIntegerSchema = z.string().superRefine((value, ctx) => {
const parsed = parseInt(value);
if (isNaN(parsed)) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_type,
expected: 'number',
received: 'nan',
});
}
});
const positiveInteger = z.string().superRefine((value, ctx) => {
const parsed = parseInt(value);
if (isNaN(parsed)) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_type,
expected: 'number',
received: 'nan',
});
}
if (parsed < 1) {
ctx.addIssue({
code: z.ZodIssueCode.too_small,
minimum: 1,
inclusive: true,
type: 'number',
});
}
});
const sqonSchema = z.custom((value) => {
try {
parseSQON(value);
return true;
}
catch (error) {
return false;
}
}, 'invalid SQON format');
const startDateSchema = z
.string()
.trim()
.min(1)
.refine((value) => isValidDateFormat(value), 'invalid `startDate` parameter');
const submissionActionTypeSchema = z
.string()
.trim()
.min(1)
.refine((value) => isSubmissionActionTypeValid(value), 'invalid Submission Action Type');
const submissionIdSchema = z
.string()
.trim()
.min(1)
.refine((value) => {
const parsed = parseInt(value);
return isValidIdNumber(parsed);
}, 'invalid submission ID');
const stringNotEmpty = z.string().trim().min(1);
export const categoryPathParamsSchema = z.object({
categoryId: categoryIdSchema,
});
// Common Category and Organization Path Params
export const categoryOrganizationPathParamsSchema = z.object({
categoryId: categoryIdSchema,
organization: organizationSchema,
});
// Common Category, Organization, and EntityName Path Params
export const categoryOrganizationEntityPathParamsSchema = z.object({
categoryId: categoryIdSchema,
organizationId: organizationSchema,
entityName: entityNameSchema,
});
const submissionIdPathParamSchema = z.object({
submissionId: submissionIdSchema,
});
const paginationQuerySchema = z.object({
page: positiveInteger.optional(),
pageSize: pageSizeSchema.optional(),
});
const auditQuerySchema = z
.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: z.object({
categoryName: stringNotEmpty,
dictionaryName: stringNotEmpty,
dictionaryVersion: stringNotEmpty,
defaultCentricEntity: entityNameSchema.or(z.literal('')).optional(),
}),
};
export const submissionsByCategoryRequestSchema = {
query: z.object({
onlyActive: booleanSchema.default('false'),
organization: organizationSchema.optional(),
username: stringNotEmpty.optional(),
}),
pathParams: categoryPathParamsSchema,
};
export const submissionByIdRequestSchema = {
pathParams: submissionIdPathParamSchema,
};
export const submissionDetailsRequestSchema = {
query: z
.object({
entityNames: z.union([entityNameSchema, entityNameSchema.array()]).optional(),
actionTypes: z.union([submissionActionTypeSchema, submissionActionTypeSchema.array()]).optional(),
})
.merge(paginationQuerySchema),
pathParams: submissionIdPathParamSchema,
};
export const submissionActiveByOrganizationRequestSchema = {
pathParams: categoryOrganizationPathParamsSchema,
};
export const submissionCommitRequestSchema = {
pathParams: z.object({
categoryId: categoryIdSchema,
submissionId: submissionIdSchema,
}),
};
export const submissionDeleteRequestSchema = {
pathParams: submissionIdPathParamSchema,
query: z.object({
force: booleanSchema.default('false'),
}),
};
export const submissionDeleteEntityNameRequestSchema = {
query: z.object({
entityName: entityNameSchema,
index: indexIntegerSchema.optional(),
}),
pathParams: z.object({
actionType: submissionActionTypeSchema,
submissionId: submissionIdSchema,
}),
};
const uploadSubmissionQueryParams = z.object({
entityName: entityNameSchema,
organization: organizationSchema,
});
const submissionUploadFilesQueryParams = z.object({
organization: organizationSchema,
});
export const filenameEntityPair = z.object({
filename: z.string(),
entity: z.string(),
});
const dataRecordValueSchema = z.union([
z.string(),
z.number(),
z.boolean(),
z.array(z.string()),
z.array(z.number()),
z.array(z.boolean()),
z.undefined(),
]);
const dataRecordSchema = z.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: z.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;
}
}, z.array(filenameEntityPair).optional()),
};
export const uploadSingleEntitySubmissionDataRequestSchema = {
body: dataRecordSchema.array(),
query: uploadSubmissionQueryParams,
pathParams: categoryPathParamsSchema,
};
export const dataDeleteBySystemIdRequestSchema = {
pathParams: z.object({
systemId: stringNotEmpty,
categoryId: categoryIdSchema,
}),
};
// TODO: Need type validation for the edit request schema
export const editSingleEntityRequestSchema = {
body: z.record(z.unknown()).array(),
query: uploadSubmissionQueryParams,
pathParams: categoryPathParamsSchema,
};
export const dataGetByCategoryRequestSchema = {
query: z
.object({
entityName: z.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: z.ZodIssueCode.custom,
message: 'is incompatible with `compound` view',
path: ['entityName'],
});
}
}),
pathParams: categoryPathParamsSchema,
};
export const dataGetByOrganizationRequestSchema = {
query: z
.object({
entityName: z.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: z.ZodIssueCode.custom,
message: 'is incompatible with `compound` view',
path: ['entityName'],
});
}
}),
pathParams: categoryOrganizationPathParamsSchema,
};
export const dataGetByQueryRequestSchema = {
body: sqonSchema,
query: z
.object({
entityName: z.union([entityNameSchema, entityNameSchema.array()]).optional(),
})
.merge(paginationQuerySchema),
pathParams: categoryOrganizationPathParamsSchema,
};
export const DataGetBySystemIdRequestSchema = {
query: z.object({
view: viewSchema.optional(),
}),
pathParams: z.object({
systemId: stringNotEmpty,
categoryId: categoryIdSchema,
}),
};
export const downloadDataFileTemplatesSchema = {
query: z.object({
fileType: z.enum(['csv', 'tsv']).optional(),
}),
pathParams: z.object({
categoryId: categoryIdSchema,
}),
};
export const validationPathParamsSchema = z.object({
categoryId: categoryIdSchema,
entityName: entityNameSchema,
});
const validationQuerySchema = z.object({
organization: organizationSchema,
value: stringNotEmpty,
});
export const validationRequestSchema = {
query: validationQuerySchema,
pathParams: validationPathParamsSchema,
};