Coder Social home page Coder Social logo

method's Introduction

Method

Method is an easy way to create Meteor methods with Optimistic UI. It's built with Meteor 3.0 in mind. It's meant to be a drop in replacement for Validated Method and comes with additional features:

  • Before and after hooks
  • Global before and after hooks for all methods
  • Pipe a series of functions
  • Authed by default (can be overriden)
  • Easily configure a rate limit
  • Optionally run a method on the server only
  • Attach the methods to Collections (optional)
  • Validate with one of the supported schema packages or a custom validate function
  • No need to use .call to invoke the method as with Validated Method

Usage

Add the package to your app

meteor add jam:method

Create a method

name is required and will be how Meteor's internals identifies it.

schema will automatically validate using a supported schema.

run will be executed when the method is called.

import { createMethod } from 'meteor/jam:method'; // can import { Methods } from 'meteor/jam:method' instead and use Methods.create if you prefer

export const create = createMethod({
  name: 'todos.create',
  schema: Todos.schema, // using jam:easy-schema in this example
  run({ text }) {
    const todo = {
      text,
      done: false,
      createdAt: new Date(),
      authorId: Meteor.userId(), // can also use this.userId instead of Meteor.userId()
    }
    const todoId = Todos.insert(todo);
    return todoId;
  }
});

Supported schemas

Currently, these schemas are supported:

If you're using jam:easy-schema, be sure to check out Using with jam:easy-schema below for details on a way to write methods with less boilerplate.

Here's a quick example of each one's syntax. They vary in features so pick the one that best fits your needs.

// jam:easy-schema. you'll attach to a Collection so you can reference one {Collection}.schema in your methods
const schema = {text: String, isPrivate: Optional(Boolean)}
// check
const schema = {text: String, isPrivate: Match.Maybe(Boolean)}
// zod
const schema = z.object({text: z.string(), isPrivate: z.boolean().optional()})
// simpl-schema
const schema = new SimpleSchema({text: String, isPrivate: {type: Boolean, optional: true}})

Custom validate function

If you're not using one of the supported schemas, you can use validate to pass in a custom validation function. Note: validate can be an async function if needed.

// import your schema from somewhere
// import your validator function from somewhere

export const create = createMethod({
  name: 'todos.create',
  validate(args) {
    validator(args, schema)
  },
  run({ text }) {
    const todo = {
      text,
      done: false,
      createdAt: new Date(),
      authorId: Meteor.userId() // can also use this.userId instead of Meteor.userId()
    }
    const todoId = Todos.insert(todo);
    return todoId;
  }
});

Async support

It also supports using *Async Collection methods, e.g.:

export const create = createMethod({
  name: 'todos.create',
  schema: Todos.schema,
  async run({ text }) {
    const todo = {
      text,
      done: false,
      createdAt: new Date(),
      authorId: Meteor.userId() // can also use this.userId instead of Meteor.userId()
    }
    const todoId = await Todos.insertAsync(todo);
    return todoId;
  }
});

Import on the client and use

import { create } from '/imports/api/todos/methods';

async function submit() {
  try {
    await create({text: 'book flight to Hawaii'})
  } catch(error) {
    alert(error)
  }
}

Before and after hooks

You can execute functions before and / or after the method's run function. before and after accept a single function or an array of functions.

When using before, the original input passed into the method will be available. The original input will be returned automatically from a before function so that run receives what was originally passed into the method.

A great use case for using before is to verify the user has permission. For example:

async function checkOwnership({ _id }) { // the original input passed into the method is available here. destructuring for _id since that's all we need for this function
  const todo = await Todos.findOneAsync(_id);
  if (todo.authorId !== Meteor.userId()) {
    throw new Meteor.Error('not-authorized')
  }

  return true; // any code executed as a before function will automatically return the original input passed into the method so that they are available in the run function
}

export const markDone = createMethod({
  name: 'todos.markDone',
  schema: Todos.schema,
  before: checkOwnership,
  async run({ _id, done }) {
    return await Todos.updateAsync(_id, {$set: {done}});
  }
});

When using after, the result of the run function will be available as the first argument and the second argument will contain the original input that was passed into the method. The result of the run function will be automatically returned from an after function.

function exampleAfter(result, context) {
  const { originalInput } = context; // the name of the method is also available here
  // do stuff

  return 'success'; // any code executed as an after function will automatically return the result of the run function
}

export const markDone = createMethod({
  name: 'todos.markDone',
  schema: Todos.schema,
  before: checkOwnership,
  async run({ _id, done }) {
    return await Todos.updateAsync(_id, {$set: {done}});
  },
  after: exampleAfter
});

Note: If you use arrow functions for before, run, or after, you'll lose access to this – the methodInvocation. You may be willing to sacrifice that because this.userId can be replaced by Meteor.userId() and this.isSimulation can be replaced by Meteor.isClient but it's worth noting.

Pipe a series of functions

Instead of using run, you can compose functions using .pipe. Each function's output will be available as an input for the next function.

// you'd define the functions in the pipe and then place them in the order you'd like them to execute within .pipe
// be sure that each function in the pipe returns what the next one expects, otherwise you can add an arrow function to the pipe to massage the data, e.g. (input) => manipulate(input)

export const create = createMethod({
  name: 'todos.create',
  schema: Todos.schema
}).pipe(
  checkOwnership,
  createTodo,
  sendNotification
)

Attach methods to its Collection (optional)

Instead of importing each method, you can attach them to the Collection. If you're using jam:easy-schema be sure to attach the schema before attaching the methods.

// /imports/api/todos/collection
import { Mongo } from 'meteor/mongo';
import { schema } from './schema';

export const Todos = new Mongo.Collection('todos');

Todos.attachSchema(schema); // if you're using jam:easy-schema
(async() => {
  const methods = await import('./methods.js') // dynamic import is recommended
  Todos.attachMethods(methods);
})();

attachMethods accepts the method options as an optional second parameter. See Configuring for a list of the options.

With the methods attached you'll use them like this on the client:

import { Todos } from '/imports/api/todos/collection';
// no need to import each method individually

async function submit() {
  try {
    await Todos.create({text: 'book flight to Hawaii'})
  } catch(error) {
    alert(error)
  }
}

Executing code on the server only

By default, methods are optimistic meaning they will execute on the client and then on the server. If there's only part of the method that should execute on the server and not on the client, then simply wrap that piece of code in a if (Meteor.isServer) block. This way you can still maintain the benefits of Optimistic UI. For example:

export const create = createMethod({
  name: 'todos.create',
  schema: Todos.schema,
  async run(args) {
    // rest of your function
    // code running on both client and server
    if (Meteor.isServer) {
      // code running on the server only
      import { secretCode } from '/server/secretCode'; // since it's in a /server folder the code will not be shipped to the client
      // do something with secretCode
    }

    // code running on both client and server
    return Todos.insertAsync(todo)
  }
});

If you prefer, you can force the entire method to execute on the server only by setting serverOnly: true. It can be used with run or .pipe. Here's an example with run:

export const create = createMethod({
  name: 'todos.create',
  schema: Todos.schema,
  serverOnly: true,
  async run(args) {
    // all code here will execute only on the server
  }
});

You can also set all methods to be serverOnly. See Configuring below.

Security note

Important: Since Meteor does not currently support tree shaking, the contents of the code inside run function or .pipe could still be visible to the client even if you use if (Meteor.isServer) or serverOnly: true. To prevent this, you have these options:

  1. Attach methods to its Collection with a dynamic import as shown above Attach methods to its Collection (optional)

  2. Import function(s) from a file within a /server folder. Any code imported from a /server folder will not be shipped to the client. The /server folder can be located anywhere within your project's file structure and you can have multiple /server folders. For example, you can co-locate with your collection folder, e.g. /imports/api/todos/server/, or it can be at the root of your project. See Secret server code for more info.

export const create = createMethod({
  name: 'todos.create',
  schema: Todos.schema,
  serverOnly: true,
  async run(args) {
    import { serverFunction } from '/server/serverFunction';

    serverFunction(args);
  }
});
  1. Dynamically import function(s). These do not have to be inside a /server folder. This will prevent the code being inspectable via the browser console.
export const create = createMethod({
  name: 'todos.create',
  schema: Todos.schema,
  serverOnly: true,
  async run(args) {
    const { serviceFunction } = await import('./services');

    serviceFunction(args);
  }
});

Changing authentication rules

By default, all methods will be protected by authentication, meaning they will throw an error if there is not a logged-in user. You can change this for an individual method by setting open: true. See Configuring below to change it for all methods.

export const create = createMethod({
  name: 'todos.create',
  schema: Todos.schema,
  open: true,
  async run({ text }) {
    // ... //
  }
});

Rate limiting

Easily rate limit a method by setting its max number of requests – the limit – within a given time period (milliseconds) – the interval.

export const create = createMethod({
  name: 'todos.create',
  schema: Todos.schema,
  rateLimit: { // rate limit to a max of 5 requests every second
    limit: 5,
    interval: 1000
  },
  async run({ text }) {
    // ... //
  }
});

Validate without executing the method

There may be occassions where you want to validate without executing the method. In these cases, you can use .validate. If you want to validate against only part of the schema, use .validate.only.

export const create = createMethod({
  name: 'todos.create',
  schema: Todos.schema,
  async run({ text }) {
    // ... //
  }
});

// validate against the schema without executing the method
create.validate({...})

// validate against only the relevant part of the schema based on the data passed in without executing the method
create.validate.only({...})

If you're using a custom validate function instead of one of the supported schemas and you'd like to use .validate.only, you can simply append an only function onto the validate function that you supply.

Options for Meteor.applyAsync

When called, the method uses Meteor.applyAsync under the hood to execute your run function or .pipe function(s). Meteor.applyAsync takes a few options which can be used to alter the way Meteor handles the method. If you want to change the defaults or include other supported options, pass in options when creating the method.

export const create = createMethod({
  name: 'todos.create',
  schema: Todos.schema,
  options: {
    // ... //
  },
  async run({ text }) {
    // ... //
  }
});

By default, this package uses the following options:

{
  // Make it possible to get the ID of an inserted item
  returnStubValue: true,

  // Don't call the server method if the client stub throws an error, so that we don't end
  // up doing validations twice
  throwStubExceptions: true,
};

See Configuring below to set options for all methods.

Configuring (optional)

If you like the defaults, then you won't need to configure anything. But there is some flexibility in how you use this package.

Here are the global defaults:

const config = {
  before: [], // global before function(s) that will run before all methods
  after: [], // global after function(s) that will run after all methods
  serverOnly: false, // globally make all methods serverOnly, aka disable Optimistic UI, by setting to true
  options: {
    returnStubValue: true, // make it possible to get the ID of an inserted item on the client before the server finishes
    throwStubExceptions: true,  // don't call the server method if the client stub throws an error, so that we don't end up doing validations twice
  },
  open: false, // by default all methods will be protected by authentication, override it for all methods by setting this to true
  loggedOutError: new Meteor.Error('logged-out', 'You must be logged in') // customize the logged out error
};

To change the global defaults, use:

// put this in a file that's imported on both the client and server
import { Methods } from 'meteor/jam:method';

Methods.configure({
  // ... change the defaults here ... //
});

Global before and after hooks

You can create before and/or after functions to run before / after all methods. Both before and after accept a single function or an array of functions.

import { Methods } from 'meteor/jam:method';

const hello = () => { console.log('hello') }
const there = () => { console.log('there') }
const world = () => { console.log('world') }

Methods.configure({
  before: [hello, there],
  after: world
});

Helpful utility function to log your methods

Here's a helpful utility function - log - that you might consider adding. It isn't included in this package but you can copy and paste it into your codebase where you see fit.

// log will simply console.log or console.error when the Method finishes
function log(input, pipeline) {
  pipeline.onResult((result) => {
    console.log(`Method ${pipeline.name} finished`, input);
    console.log('Result', result);
  });

  pipeline.onError((err) => {
    console.error(`Method ${pipeline.name} failed`);
    console.error('Error', err);
  });
};

Then you could use it like this:

import { Methods, server } from 'meteor/jam:method';

Methods.configure({
  after: server(log)
});

Alternative functional-style syntax

You can use a functional-style syntax to compose your methods if you prefer. Here's an example.

const fetchGifs = async({ searchTerm, limit }) => {...}

export const getGifs = createMethod(server(schema({ searchTerm: String, limit: Number })(fetchGifs)))

getGifs is callable from the client but will only run on the server. Internally it will be identified as fetchGifs

Note: if you pass in a named function into createMethod, then that will be used to identify the method internally. Otherwise if you pass in an anonymous function, jam:method generates a unique name based on its schema to identify it internally.

Customizing methods when using functional-style syntax

There are a few functions available when you need to customize the method: schema, server, open, close. These can be composed when needed.

schema

Specify the schema to validate against.

import { schema } from 'meteor/jam:method';

export const doSomething = schema({thing: String, num: Number})(async ({ thing, num }) => {
  // ... //
});

server

Make the method run on the server only.

import { server } from 'meteor/jam:method';

export const aServerOnlyMethod = server(async data => {
  // ... //
});

open

Make the method publically available so that a logged-in user isn't required.

import { open } from 'meteor/jam:method';

export const aPublicMethod = open(async data => {
  // ... //
});

close

Make the method check for a logged-in user.

Note: by default, all methods require a logged-in user so if you stick with that default, then you won't need to use this function. See Configuring.

import { close } from 'meteor/jam:method';

export const closedMethod = close(async data => {
  // ... //
});

Using with jam:easy-schema

jam:method integrates with jam:easy-schema and offers a way to reduce boilerplate and make your methods even easier to write (though you can still use createMethod if you prefer).

For example, instead of writing this:

export const setDone = createMethod({
  name: 'todos.setDone',
  schema: Todos.schema,
  before: checkOwnership,
  async run({ _id, done }) {
    return Todos.updateAsync({ _id }, { $set: { done } });
  }
});

You can write:

export const setDone = async ({ _id, done }) => {
  await checkOwnership({ _id });
  return Todos.updateAsync({ _id }, { $set: { done } });
};

Note: This assumes that you're attaching your methods to its collection. See Attach methods to its Collection.

When you call Todos.setDone from the client, the arguments will be automatically checked against the Todos.schema. The method will automatically be named todos.setDone internally to identify it for app performance monitoring (APM) purposes.

You can also compose with the functions available in the function-style syntax. For example:

export const setDone = server(async ({ _id, done }) => {
  await checkOwnership({ _id });
  return Todos.updateAsync({ _id }, { $set: { done } });
});

Now when you call Todos.setDone from the client it will only run on the server.

Coming from Validated Method?

You may be familiar with mixins and wondering where they are. With the features of this package - authenticated by default, before / after hooks, .pipe - your mixin code may no longer be needed or can be simplified. If you have another use case where your mixin doesn't translate, I'd love to hear it. Open a new discussion and let's chat.

method's People

Contributors

jamauro avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

epfl-si

method's Issues

Conflict: Constraint [email protected] is not satisfied by mongo 2.0.0-rc300.2.

meteor add jam:method
=> Errors while adding packages:

While selecting package versions:
error: Conflict: Constraint [email protected] is not satisfied by mongo 2.0.0-rc300.2.
Constraints on package "mongo":

Error in configure method

Configs passed to the "configure" method, are not applied to all methods

In main.js in server folder

const hello = () => { console.log('hello') }
const there = () => { console.log('there') }
const world = () => { console.log('world') }

Methods.configure({
    before: [hello, there], // global before function(s) that will run before all methods
    after: world, // global after function(s) that will run after all methods
    serverOnly: true, // globally make all methods serverOnly, aka disable Optimistic UI, by setting to true
    options: {
        returnStubValue: true, // make it possible to get the ID of an inserted item on the client before the server finishes
        throwStubExceptions: true,  // don't call the server method if the client stub throws an error, so that we don't end up doing validations twice
    },
    arePublic: false, // by default all methods will be protected by authentication, override it for all methods by setting this to true
    basePath: `/imports/methods`, // used when dynamically importing methods
    loggedOutError: new NotLoggedIn(), // customize the logged out error
});

In imports/methods/account.methods.js

export const editEmail = createMethod({
    name: AccountMethods.EDIT_EMAIL,
    validate: new SimpleSchema().validator(),
    run() {
        throw new UnimplementedMethod();
    }
});

Typescript error on Visual Studio Code

I get the following error in VS Code when trying to call the methods.
The code runs just fine.

import SimpleSchema from 'simpl-schema'
import { createMethod } from 'meteor/jam:method'
import { Contacts } from '../../database'
import { ContactsQueue } from '../collection'
import { ContactMethodTypesEnum } from '../enums'
import { increaseCounter } from '../../counters/methods'
import { CounterNamesEnum } from '../../counters/enums'

const insertContact = createMethod({
  name: 'contacts:insert',
  serverOnly: true,

  validate: new SimpleSchema({
    contact: {
      type: Object,
      blackbox: true
    }
  }).validator(),

  async run(params) {
    const { contact } = params

    const task = ContactsQueue.add(async() => {
      // Generate unique contact code.
      contact.code = `C${await increaseCounter({ name: CounterNamesEnum.CONTACTS })}`

      // Remove contact method if it is the same as the main phone number.
      contact.contactMethods.forEach((method, index) => {
        if (method.type === ContactMethodTypesEnum.PHONE && method.value === contact.phoneNumber) {
          contact.contactMethods.splice(index, 1)
        }
      })

      return { _id: await Contacts.insertAsync(contact), code: contact.code }
    })

    return task.promise
  }
})

export default insertContact

const { _id, code } = await insertContact({ contact })

This expression is not callable.
  Not all constituents of type '((...args?: any[]) => Promise<Promise<{ _id: string; code: any; }>>) | { pipe: (...fns: Function[]) => { (): any; (...args?: any[]): Promise<Promise<{ _id: string; code: any; }>>; }; }' are callable.
ts(2349)

Add a Changelog file

Please add a changelog file so we can tell what happened at different releases.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.