Vulcan.js 1.14: Filtering + New APIs
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 customer
object 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 _id
s, 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!