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
- Install
zodern:relay
andzod
meteor add zodern:relay meteor npm install zod
- 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}
- 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.