zodern:relay

v1.0.1Published 2 months ago

zodern:relay - Type safe Meteor methods and publications

zodern:relay allows you to easily write methods and publications, and have typescript check the types of the args and result wherever you call the method or subscribe to a publication.

/imports/methods/projects.ts:

1import { createMethod } from 'meteor/zodern:relay';
2import { z } from 'zod';
3import { Projects } from '/shared/collections';
4
5export const createProject = createMethod({
6  name: 'projects.create',
7  schema: z.object({
8    name: z.string(),
9    description: z.string().optional(),
10    isPublic: z.boolean(),
11  }),
12  run({ name, description, isPublic }): string {
13    let id = Projects.insert({
14      name,
15
16      // Type 'string | undefined' is not assignable to type 'string'.
17      //   Type 'undefined' is not assignable to type 'string'.ts(2322)
18      description,
19      public: isPublic,
20    });
21
22    return id;
23  },
24});
25

/client/exampleProject.ts

1import { createProject } from '/imports/methods/projects';
2
3export async function createExampleProject() {
4  // id has type of string
5  const id = await createProject({
6    // Property 'isPublic' is missing in type '{ name: string; }'
7    // but required in type
8    // '{ description?: string | undefined; name: string; isPublic: boolean; }'.ts(2345)
9    name: 'Example',
10  });
11}
12

You might be wondering if this can expose server code on the client. None of the content of files in methods or publications directories are used on the client. If any of these files are imported on the client, their content is completely replaced. On the client, the files will export small functions for calling any methods or subscribing to any publications that were originally exported by the file.

Getting started

  1. Install zodern:relay and zod
meteor add zodern:relay
meteor npm install zod
  1. Set up @zodern/babel-plugin-meteor-relay
meteor npm install @zodern/babel-plugin-meteor-relay

Create a .babelrc in the root of your Meteor app:

1{
2  "plugins": [
3    "@zodern/babel-plugin-meteor-relay"
4  ]
5}
  1. Set up zodern:types

If you haven't already, add zodern:types to your app so typescript can use the type definitions from zodern:relay.

Methods

You can define methods in files in methods directories. The methods directories should not be inside a client or server folder so you can import the methods on both the client and server. The babel plugin will replace the content of these files when imported on the client, so you don't have to worry about server code being exposed.

1import { createMethod } from 'meteor/zodern:relay';
2import { z } from 'zod';
3
4export const add = createMethod({
5  name: 'add',
6  schema: z.number().array().length(2),
7  run([a, b]) {
8    return a + b;
9  },
10});

schema is always required, and must be a schema created from the zod npm package. If the method does not have any arguments, you can use zod.undefined() as the schema. If you do not want to check the arguments, you can use zod.any().

The schema is used to provide types for for the run function's parameter, and to check the arguments when calling the method.

createMethod returns a function you can use to call the method. The function returns a promise that resolves with the result. The result will have the same type as the return value of the run function.

1import { add } from '/imports/methods/add'
2
3add([1, 2]).then(result => {
4  console.log(result) // logs 3
5});

The method name, schema, or run function, are available on the config property of the function returned by createMethod:

1import { add } from '/imports/methods/add';
2
3
4console.log(
5  add.config.name,
6  add.config.schema,
7  add.config.run
8)

On the client, only config.name is defined.

Publications

You can define publications in files in publications directories. The publications directories should not be inside a client or server folder so you can import the publications on both the client and server. The babel plugin will replace the content of these files when imported on the client, so you don't have to worry about server code being exposed.

1import { createPublication } from 'meteor/zodern:relay';
2import { z } from 'zod';
3import { Projects } from '/collections';
4
5export const subscribeProject = createPublication({
6  name: 'project',
7  schema: z.object({
8    id: z.string()
9  }),
10  run({ id }) {
11    return Projects.find({ _id: id });
12  },
13});

schema is always required, and must be a schema created from the zod npm package. If the method does not have any arguments, you can use zod.undefined() as the schema. If you do not want to check the arguments, you can use zod.any().

The schema is used to provide types for for the run function's parameter, and to check the arguments when subscribing to the publication.

createPublication returns a function you can use to subscribe to the publication. This function returns a Subscription Handle, the same as Meteor.subscribe would.

1import { subscribeProject } from '/imports/publications/projects'
2
3const exampleId = 'example';
4
5subscribeProject({ id: exampleId });
6
7subscribeProject({ id: exampleId }, {
8  onStop(err) {
9    console.log('subscription stopped', err);
10  },
11  onReady() {
12    console.log('subscription ready');
13  }
14});

Like with methods, the function returned by createPublication has a config property to access the name, schema, and run function. On the client, only the name is available.

Blocking

By default, methods and publications will block, meaning each session will run one method or publication at a time, in the order the client calls or subscribes. In cases where you do not want this behavior for a specific method or subscription, you can call this.unblock().

Meteor has a bug for methods and publications with an async function where they will always be unblocked, and there is no way to have them block. zodern:relay fixes this so async methods and publications are consistent with Meteor's normal behavior.

Rate limiting

Both createMethod and createPublication support a rateLimit option:

1export const add = createMethod({
2  name: 'add',
3  schema: z.number().array().length(2),
4  rateLimit: {
5    interval: 2000,
6    limit: 10
7  },
8  run([a, b]) {
9    return a + b;
10  },
11});

interval is the number of ms before the rate limit is reset. limit is the maximum number of method calls or created subscriptions allowed per time interval.

Method Stubs

zodern:relay currently doesn't have any built in support for method stubs. You can define method stubs as you would when not using zodern:relay in files outside of methods directories.