Vulcan.js 1.14: Filtering + New APIs

Sacha Greif
VulcanJS
Published in
7 min readDec 4, 2019

Vulcan 1.14 brings a rethinking of the APIs for the entire data flow, from server to client.

These new APIs exist in parallel to the old ones, meaning you can progressively switch whenever you’re ready to take advantage of the new features.

Some of Vulcan’s own components have been ported to the new APIs (such as the Datatable component) while others will remain on the old APIs for now. Vulcan’s back-end layer on the other hand will be compatible with both APIs:

As you can see, while HoCs and hooks have separate versions for the old and new APIs (which means you’ll need to e.g. import withMulti2 instead of withMulti to benefit from the new features), the resolver and mutator layers can handle either APIs for now.

Filtering

The big new feature in this release is the addition of filtering to replace the old terms/parameters system.

Inspired by Hasura’s API, this means you can now do things like:

query popularMovies {
movies(input: {filter: { rating: { _gte: 4 } } }){
results {
_id
name
rating
}
}
}

This replaces the terms object, which was a blackbox JSON object and as such wasn’t validated by the GraphQL API.

The new input properties (filter, sort, limit, etc.) work for both single and multi queries, and filter is also used inside mutations.

New Hooks & HoCs

The current withMulti, withSingle, etc. HoCs (and corresponding useMulti and useSingle hooks) will still work as before by using the terms object.

In order to use the new filtering API, you should use the new withMulti2, withSingle2, etc. HoCs (and useMulti2, useSingle2, etc. hooks).

This means you don’t need to make any changes to your current codebase until you’re ready to progressively adopt the new APIs.

Nested Schemas

Vulcan forms support nested documents (such as a customerobject having multiple address fields):

export const addressSchema = new SimpleSchema({
street: {
type: String,
canRead: ['guests'],
},
country: {
type: String,
canRead: ['guests'],
},
});
const customerSchema = {
_id: {
type: String,
canRead: ['guests'],
},
name: {
type: String,
canRead: ['guests'],
},
addresses: {
type: Array,
canRead: ['guests'],
},
'addresses.$': {
type: addressSchema,
},
};
export default customerSchema;

Up to now these “sub-schemas” were treated as blackbox JSON objects (or arrays of JSON objects) by the GraphQL API. But with this new feature, GraphQL types will now be generated for every sub-schema.

The resulting type’s name will be ${typeName}${fieldName}, or in this case CustomerAddresses.

Note that currently there is no way to reuse nested GraphQL schemas, meaning twoshippingAddress and billingAddress fields will generate two CustomerShippingAddress and CustomerBillingAddress GraphQL types even if the underlying JavaScript sub-schemas objects are the same. Hopefully this is something we can improve in the next release.

Updating Your Fragments

In order to take advantage of this new feature, you’ll need to update your fragments to add the subschema’s subfields. From:

fragment CustomerItem on Customer{
addresses
}

To:

fragment CustomerItem on Customer{
addresses{
street
country
}
}

Disabling Nested Schemas

If you’d like to disable GraphQL sub-schema generation for a given schema (in other words, keep the behavior up to now) you can set blackbox: true on the field that’s using the custom sub-schema:

const customerSchema = {
_id: {
type: String,
canRead: ['guests'],
},
name: {
type: String,
canRead: ['guests'],
},
addresses: {
type: Array,
canRead: ['guests'],
blackbox: true
},
'addresses.$': {
type: addressSchema,
},
};

New Permissions API

We also have a new, simpler permissions API that is now part of createCollection and focuses on the four main CRUD operations:

const Movies = createCollection({  collectionName: 'Movies',  typeName: 'Movie',  schema,  permissions: {
canCreate: ['members'],
canRead: ['members'],
canUpdate: ['owners', 'admins'],
canDelete: ['owners', 'admins'],
},
});

Note that when no permissions are specified, multi and single resolvers will default to giving access to all documents; while create, update, and delete mutations will default to failing.

[BREAKING] Mutations Definitions on the Client

A related breaking change is that mutation and query resolvers are no longer defined on the client. This makes sense because mutations and queries are not relevant on the client, but it has the side effect of making check() functions (such as Movies.options.mutations.create.check()) unavailable on the client.

We suggest you replace these calls to your own custom helper function; or else switch to the new permission check system (which is available on the client):

Users.canUpdate({ collection: Movies, user: currentUser, document: movie})

New “Owners” Group

This new permissions API makes use of a new owners group added to the three other default groups (guests, members, and admins). This group is a bit special in that it factors in any document being operated on. In other words, a user can be considered part of the owners group for document A but not document B, depending on their userId property.

Note that the owners group feature is only available when using the new permissions API.

New Callbacks API

On the same model as the new permissions API, the new callbacks API is also integrated with createCollection:

const Movies = createCollection({  collectionName: 'Movies',  //...  callbacks: {
create: {
validate: [(errors, properties) => { return errors; }],
before: [(document, properties) => { return document; }],
after: [(document, properties) => { return document; }],
async: [(properties) => { /* no return value */ }],
}
update: {
validate: [(errors, properties) => { return errors; }],
before: [(data, properties) => { return data; }],
after: [(document, properties) => { return document; }],
async: [(properties) => { /* no return value */ }],
}
delete: {
validate: [(errors, properties) => { return errors; }],
before: [(document, properties) => { return document; }],
after: [(document, properties) => { return document; }],
async: [(properties) => { /* no return value */ }],
}
},
});

Because createCollection is shared between client and server, and callbacks are a server-only concern, you can also break them apart using the new extendCollection function:

extendCollection(Posts, { 
callbacks: {
create: {
after: [ notifyAdmins ]
}
}
});

Datatable Improvements

To go along with API-level filtering, datatables now support filtering any column. Just add filterable: true in a column definition and the datatable will show you the appropriate filtering controls based on the column type (date, string, etc.).

And if you specify query or options properties in your schema, the filtering control will use that to populate its list of options.

Datatables also support a new initialState prop that can be used to set the initial sorting/filtering state of the datatable when first rendered.

Relations

Another big new feature: Vulcan now supports hasOne and hasMany relations between collections.

This means you don’t need to specify a field’s resolver function anymore:

userId: {
type: String,
optional: true,
input: 'select',
canRead: ['guests'],
canCreate: ['members'],
resolveAs: {
fieldName: 'user',
type: 'User',
relation: 'hasOne',
},
},

Because we’re telling Vulcan that the field should resolve as a User and has a hasOne relation, Vulcan will automatically create a resolver that takes the field value and looks for a document with that _id inside the Users collection.

For fields containing an array of _ids, the same process will be used except with the hasMany relation.

Of course, manually specifying your own resolver function will still work just as it did before.

Next Steps

The next steps for relations would include automating how relations are handled in forms so that specifying query and options properties isn’t necessary anymore.

It would also be good to enable the same filter, sort, etc. filtering arguments already supported by the main query resolvers in relation fields.

Multiple Field Resolvers

Field resolvers can now take an array of resolver objects. This is useful when you want a single field to resolve as multiple GraphQL fields:

createdAt: {
type: Date,
optional: true,
canRead: ['guests'],
resolveAs: [
{
fieldName: 'createdAtFormattedShort',
type: 'String',
resolver: formatDateShort,
},
{
fieldName: 'createdAtFormattedLong',
type: 'String',
resolver: formatDateLong,
},
],
},

New createCollection() Behavior

In order to simplify frequently used code, query and mutation resolvers are now opt-out instead of opt-in. In other words, leaving the resolvers and mutations options blank in your collection will now do the same thing as calling getDefaultResolvers and getDefaultMutations.

If you don’t want query and mutation resolvers, you can instead set resolvers and/or mutations to null.

Also, mutations and query resolvers are not longer declared on the client. This means that their associated check functions (e.g. Posts.options.mutations.update.check()) are also no longer available on the client.

New Forum Example

The forum example of the starter repo has been completely overhauled to use the new APIs and serve as a good example app.

Note that some features (newsletter, click tracking, etc.) have been dropped in the process to make the transition easier, as well as make it easier to maintain the example going forward.

Collection Components (experimental)

While still experimental, this feature will make it possible to associate specific components to a collection.

For example, associating a PostTitle component to the Posts collection would mean that any part of a datatable, card, or form that needs to somehow display a post would do so using that PostTitle component instead of having to “guess” the best way to display it.

The API for this feature is still under discussion so please leave us your feedback!

Next Steps

With such a big update, the next few releases will focus on stability, code clean-up, bug fixing, as well as clearly deprecating the older APIs.

Hopefully these improvements will make Vulcan much easier to learn, and improve the experience for both new and experienced Vulcan developers!

--

--

Sacha Greif
VulcanJS

Designer/developer from Paris, now living in Osaka. Creator of Sidebar, VulcanJS, and co-author of Discover Meteor.