Sure. This is still VERY much a work in progress…
// zod schema object
const zodSchema = {
_id: z
.string()
.regex(/\b^[23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz]{17}\b/, { message: 'Not a valid Id' })
.describe('A Meteor generated Mongo identifier'),
name: z
.string()
.describe('Users Name'),
favoriteColor: z
.string()
.describe('users fav color')
};
// exported for use around the app
export const schemaObj = z.object(zodSchema);
export type ZodTypes = z.infer<typeof schemaObj>;
export const FavColors = new Mongo.Collection<ZodTypes>(favColors);
// generate schema
// collection name passed for error reporting
const simpleSchema = createSimpleSchemaFromZodSchema(zodSchema, 'favColors');
// @ts-expect-error expected error - missing types in Collection2
FavColors.attachSchema(simpleSchema);
Below is the big ugly function to create the schema that can be attached to collection via Collection2. Simple Schema is a massive tool, so this doesn’t work for everything. For example, it would take a lot of work to get this to work with autovalue in Simple Schema.
import SimpleSchema from 'simpl-schema';
import { ValidatorContext } from '/node_modules/simpl-schema/dist/esm/types';
import { ZodType, ZodTypeDef } from 'zod';
import { fromZodError, ValidationError } from 'zod-validation-error';
import _ from 'lodash';
import { Table } from 'console-table-printer';
import colors from '@colors/colors';
export const createSimpleSchemaFromZodSchema = (zodSchema: { [key: string]: ZodType }, collectionName: string) => {
return new SimpleSchema(
{ ...zodToSimpleSchema.call(this as unknown as ValidatorContext, zodSchema, collectionName) },
{
// All validation should happen in the simple schema custom function based on zod rules
// So all fields in simple schema are optional, and the optionality is checked in the custom function
requiredByDefault: false,
clean: {
autoConvert: false,
trimStrings: true,
filter: false
},
});
};
function zodToSimpleSchema(this: ValidatorContext, zodSchema: { [key: string]: ZodType }, collectionName: string) {
const simpleSchemaObject = {};
for (const [key, value] of Object.entries(zodSchema)) {
const simpleSchemaValidationObject: Partial<ValidatorContext> = {
type: SimpleSchema.Any,
custom() {
// The _id (Mongo identifier) should be required across the app,
// but not on database operations. It is added automatically on
// insert operations. For modifier operations like $set, check
// to see if it is the _id field and if we have a modifier object.
// Insert operations will return null
if (this.key === '_id' && this.value === undefined) return;
// Parse value against zod and save the result
const parseResult = value.safeParse(this.value);
// no errors
if (parseResult.success === true) return;
// Below this point the argument is failing validation checks
const details: ValidationError['details'] = fromZodError(parseResult.error).details;
generateAndLogErrorTable.call(this, details, value, collectionName);
return {
name: this.key,
value: this.value,
type: 'validation-error',
message: details[0].message
};
}
};
Object.assign(simpleSchemaObject, { [key]: simpleSchemaValidationObject });
}
return simpleSchemaObject;
}
function generateAndLogErrorTable(this: Partial<ValidatorContext>, details: unknown, value: ZodType<any, ZodTypeDef, any>, collectionName: string) {
Meteor.defer(() => {
// deferred to ensure it logs to console after any errors in database operation try catch blocks
// nasty code to recursively look through zod
// error objects and format to the console
const errorsToLog: any[] = [];
function printAllErrors(obj: any) {
for (const element of obj) {
if (element.expected || element.received) {
errorsToLog.push(element);
} else if (element.issues) {
printAllErrors(element.issues);
} else if (typeof element.code === 'string') {
errorsToLog.push(element);
for (const prop in element) {
if (_.isArray(element[prop]) && !_.isEmpty(element[prop])) {
printAllErrors(element[prop]);
}
}
}
}
}
printAllErrors(details);
const p = new Table({
style: {
headerTop: { left: '╔', mid: '╦', right: '╗', other: '═' },
headerBottom: { left: '╟', mid: '╬', right: '╢', other: '═' },
tableBottom: { left: '╚', mid: '╩', right: '╝', other: '═' },
vertical: '║',
},
enabledColumns: ['message','code', 'expected', 'received'],
columns: [
{ name: 'code', alignment: 'left', title: colors.white.bold('Code') },
{ name: 'message', alignment: 'left', title: colors.white.bold('Message') },
{ name: 'expected', alignment: 'center', title: colors.white.bold('Expected') },
{ name: 'received', alignment: 'center', title: colors.white.bold('Received') },
]
});
p.addRows(errorsToLog);
// print
console.log(colors.red.strikethrough.bold(' '));
console.log(colors.red.bold('Error Writing to the Database')); // outputs red underlined text
console.log(colors.yellow.bold('Collection: ') + colors.yellow(collectionName));
console.log(colors.yellow.bold('Value Provided: ') + colors.yellow(this.value));
console.log(colors.yellow.bold('Key/Field: ') + colors.yellow(this.key || 'none'));
console.log(colors.yellow.bold('Key/Field Description: ') + colors.yellow(value.description || 'none'));
console.log(colors.yellow.bold('Modifier : ') + colors.yellow(this.operator || 'none'));
p.printTable();
console.log(colors.red.strikethrough.bold(' '));
});
}