Zodern:relay - Typesafe Meteor methods and publications

I haven’t tried using it in Meteor packages. Is the package inside the app’s packages folder, or are you using METEOR_PACKAGE_DIRS? If it is the second, you might need to also configure the babel plugin in the package’s folder.

Thanks. It’s nice to hear it is being used. I do have GitHub Sponsors setup: Sponsor @zodern on GitHub Sponsors · GitHub

3 Likes

I found the issue: Error: Method name is missing. Do you have the relay babel plugin configured? · Issue #5 · zodern/meteor-relay · GitHub

@zodern Hello, how should I do error management with the zodern-relay package. Is there a way you recommend? Errors that occur with zod appear on the client as “Interval Server Error”.

There is a discussion about zod errors at Communicating validation errors to the client · Issue #4 · zodern/meteor-relay · GitHub.

Been working with this package for the last few weeks. holy moly, it is amazing. I was able to wire Zod up to Simple Schema, so now I can define my schema in one place and have validation on database writes, methods, publications, and for client-side form validation with react-hook-forms and their built in zod-resolver.

You’ve really breathed new life into Meteor. Thank you again for this AMAZING package.

4 Likes

:+1:

I ended up subscribing to MontiAPM.

1 Like

Would you mind detailing your method for integrating Zod into Simple Schema?

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('                                                                                '));

  });
}


Interesting I’ll have to examine this closer when I have a chance. I was recently thinking about taking the opposite approach and trying to map Simple Schema definition to a Zod schema.

1 Like

That would be really interseting from our POV, and a I guess many others, who have a lot invested in simpleschema but would really like to get all the benefits of zod. Although moving forward it would be nice to have a path away from simpleschema and move to something more modern and better supported.

1 Like

I wouldn’t say that SimpleSchema isn’t modern or that it’s poorly supported. It definitely doesn’t have the type inference that zod does but on the other hand zod isn’t built with mongodb in mind like simple schema is.

A bit of an update on this…

This is completely untested at this point but it looks like SimpleSchemaZod could be accomplished by first converting to a JSON Schema, which is functionality provided by the simpl-schema package. Then you’ll just need the json-schema-to-zod package to convert the result into a zod schema.

import { toJsonSchema } from 'simpl-schema';
import { parseSchema } from 'json-schema-to-zod'

const profileSimpleSchema = new SimpleSchema({
  firstName: String,
  lastName: String,
});

const profileZodSchema = parseSchema(toJsonSchema(profileSimpleSchema));
5 Likes

I just wanted to chime in and add my appreciation for this package. I had previously tried to implement something similar using superstruct for a single in-house project and, instead of using conditional compilation with Babel, I used dynamic imports in the “handler”. This basically does what I wanted but far more polished and convenient and less chance for accidental misuse.

The Pipeline approach is also very nice – that’s another thing I hadn’t thought of at the time but since then I’ve been playing around with middleware (in e.g. Koa or Hono) more because I’m eager to take advantage of it for authentication and authorisation, and this will be perfect for it. It was interesting to see the pipeline approach instead e.g. a middleware stack, very functional :wink:

It’s also great timing because I was just looking at trpc recently. It’s good to know about Blitz too. Very hyped up because of all this.

1 Like

Just a clarification: for calling the methods I have only one argument available, right? So if I need to pass in several (unrelated) arguments, what is the solution? Nesting it in a wrapper object? Like { user: … , data: … , settings: … } ?

Yes, that is correct - it only supports one argument, and you can use a wrapper object to replace using multiple arguments.

Is there any guide on how to migrate gradually from ValidatedMethod? I tried installed zodern:relay and babel plugin and I get error

Match error: Expected function, got undefined in field [1]'

for my current methods.

Slack trace comes from ValidatedMethods when calling check function.

Would you be able to create a reproduction? Usually you can use both zodern:relay and ValidatedMethod in the same app without problems.

I solved my issue. Turns out I had defined my custom metod mixins in directory methods/mixins.js and by adding relay, all functions in methods directories resolve to undefined in front end. Therefore my old ValidatedMethods would not build properly.

Kind of an edge case, but hope this can help someone else with a similar problem.

2 Likes