UNPKG

@overture-stack/lyric

Version:
340 lines (339 loc) 11.2 kB
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, };