Skip to main content

Middleware

The generated resolvers only write data that is applicable to this application, but your database may have additional information you want to write.

To add additional data to the database mutations, you can provide middleware which transform the data before we make the query.

Middleware is also useful for adding additional functionality right before the database query is made - logging is a common use case.

Middleware overview#

A middleware is just a function that accepts some data and mutates it, or does some other additional functionality. You may be familiar with this concept if you have ever used a library like Express or Redux.

Middleware is called in a sequential chain. This means if you provide two middleware functions, the first one will be called, and the result of the first one will be passed into the second one. The return value of the last middleware is the final data. This allows you to write small, composable functions to transform your data.

Each middleware is passed a next function. This function is called when your middleware is complete. Calling next will trigger the next middleware in your chain.

Write middleware#

Write middleware can be used to add/change the data that is written to your database. These middleware functions are called before data is written to your database.

For example, if you had a column in your Documents table called ProjectId, you would need to use middleware to write this data since the generated resolvers don't handle this column by default.

To use write middleware, you can provide a writeMiddleware option to your table entity.

import SQLResolverGenerator from '@pdftron/collab-sql-resolver-generator';
const resolvers = SQLResolverGenerator({
info: {
Documents: {
table: 'Documents',
writeMiddleware: [
({ data, next, ctx }) => {
data.ProjectId = 'some-project-id';
next(data, ctx);
}
],
columns: {
...columnMap
},
},
}
})

This example will add additional data to the query, and will insert 'some-project-id' into the ProjectId column.

  • writeMiddleware (Array<(info: Object) => void>) An array of middleware
    • info (object)
      • info.data (Object) The data to be transformed. The keys are the column names we will write to, and the value is the data we will write to that column. Any additional properties that you add to this object will also be written to your database.
      • info.type ('create' | 'update') The type of mutation being executed. This allows you to do operation specific logic, such as adding an Id only when we are creating a new entity.
      • info.ctx (Object) The context of this request, see the context section for more details.
      • info.knex (Object) The Knex instance allows you to build a custom query. See the Build custom query in middleware section below for more details.
      • info.next (Function) A callback function to be called when your middleware is complete. Must be called with data as the first parameter, and ctx as the second. The values you provide are passed into the next middleware function. See examples below.

Read middleware#

Read middleware can be used to transform/edit data that is read from your database. These middleware functions are called after data is read from your database.

For example, if you are storing XFDF strings in blob storage, you can use read middleware to fetch the contents of the blob.

To use read middleware, you can provide a readeMiddleware option to your table entity.

import SQLResolverGenerator from '@pdftron/collab-sql-resolver-generator';
const resolvers = SQLResolverGenerator({
info: {
Annotations: {
table: 'Annotations',
readMiddleware: [
async ({ data, next, ctx }) => {
const filePath = data.xfdf; // If using blob storage, the XFDF may be a path to a file
const xfdfString = await fetchBlob(filePath)
data.xfdf = xfdfString
next(data, ctx);
}
],
columns: {
...columnMap
},
},
}
})
  • readMiddleware (Array<(info: Object) => void>) An array of middleware
    • info (object)
      • info.data (Object) The read data to be transformed.
      • info.ctx (Object) The context of this request, see the context section for more details.
      • info.knex (Object) The Knex instance allows you to build a custom query. See the Build custom query in middleware section below for more details.
      • info.next (Function) A callback function to be called when your middleware is complete. Must be called with data as the first parameter, and ctx as the second. The values you provide are passed into the next middleware function. See examples below.

Composing middleware#

Since middleware are just functions, you can write small, reusable logic and apply middleware to multiple tables. For example, if you wanted to append a custom ID to every new entity, you could do this:

import SQLResolverGenerator, { MutationOperationType } from '@pdftron/collab-sql-resolver-generator';
const appendId = ({data, type, next, ctx}) => {
if(type === MutationOperationType.CREATE) {
data.Id = generateId();
}
next(data, ctx);
}
const resolvers = SQLResolverGenerator({
info: {
Documents: {
table: 'Documents',
writeMiddleware: [appendId],
columns: {
...columnMap
},
},
Annotations: {
table: 'Annotations',
writeMiddleware: [appendId],
columns: {
...columnMap
},
},
...etc
}
})

Chaining middleware#

As mentioned previously, middleware is called in a chain, with the result of one being passed into the next.

const resolvers = SQLResolverGenerator({
info: {
Documents: {
table: 'Documents',
writeMiddleware: [
({ data, next, ctx }) => {
data.Count = 1
next(data, ctx);
},
({ data, next, ctx }) => {
data.Count++;
next(data, ctx);
},
({ data, next, ctx }) => {
data.Count++;
next(data, ctx);
},
({ data, next, ctx }) => {
console.log(data.Count) // 3
next(data, ctx);
}
],
columns: {
...columnMap
},
},
}
})

Custom query in middleware#

We use Knex to build queries internally. You can use the knex instance in the middleware params to build your custom query. All the Knex API docs are available here.

import SQLResolverGenerator, { MutationOperationType } from '@pdftron/collab-sql-resolver-generator';
const appendProjectId = async ({ data, type, ctx, knex, next }) => {
const [project] = await knex.select().from('Projects').where('DocumentId', data.Id);
if(project) {
data.ProjectId = project.Id;
}
next(data, ctx)
}
const resolvers = SQLResolverGenerator({
info: {
Documents: {
table: 'Documents',
writeMiddleware: [
appendProjectId
],
columns: {
...columnMap
},
},
}
})

Context#

Each middleware is passed a custom ctx (context) object. This object can be used to pass data between middleware, and more!

const userIsAdmin = ({ data, ctx, next }) => {
if(data.UserId === '123') {
ctx.isAdmin = true;
}
next(data, ctx);
}
const addDataIfAdmin = ({ data, ctx, next }) => {
if(ctx.isAdmin) {
// do some other logic here
}
next(data, ctx)
}
const resolvers = SQLResolverGenerator({
info: {
Documents: {
table: 'Documents',
writeMiddleware: [
userIsAdmin,
addDataIfAdmin
],
columns: {
...columnMap
},
},
}
})

See this guide for more information.

Examples#

Simple, detailed example (writing additional data)#

import SQLResolverGenerator, { MutationOperationType } from '@pdftron/collab-sql-resolver-generator';
const resolvers = SQLResolverGenerator({
info: {
Documents: {
table: 'Documents',
writeMiddleware: [
({data, type, ctx, next}) => {
next(data, ctx)
}
],
columns: {
...columnMap
},
},
}
})

The above code does not do any transformations to data, which means only the default data will be written to your database.

The above code will essentially execute this query when a new document is created:

INSERT INTO Documents (Id, AuthorId, CreatedAt, UpdatedAt, IsPublic, Name)
VALUES ('123', '456', '2021/06/14', '2021/06/14', true, 'mydoc.pdf')

However, if we change our middleware to alter the data object, we can change the query that is made:

import SQLResolverGenerator, { MutationOperationType } from '@pdftron/collab-sql-resolver-generator';
const resolvers = SQLResolverGenerator({
info: {
Documents: {
table: 'Documents',
writeMiddleware: [
({data, type, ctx, next}) => {
data.ProjectId = 'MyProjectId'
next(data, ctx)
}
],
columns: {
...columnMap
},
},
}
})

Notice here that we are adding a ProjectId property to data. This transforms our query into:

INSERT INTO Documents (Id, AuthorId, CreatedAt, UpdatedAt, IsPublic, Name, ProjectId)
VALUES ('123', '456', '2021/06/14', '2021/06/14', true, 'mydoc.pdf', 'MyProjectId')

Add data only on certain mutation types#

import SQLResolverGenerator, { MutationOperationType } from '@pdftron/collab-sql-resolver-generator';
const resolvers = SQLResolverGenerator({
info: {
Documents: {
table: 'Documents',
writeMiddleware: [
({data, type, ctx, next}) => {
// Assign ProjectId only if we are creating a document entity
if(type === MutationOperationType.CREATE) {
data.ProjectId = 'some-project-id';
}
// Assign ProjectId only if we are updating a document entity
if(type === MutationOperationType.UPDATE) {
data.UpdatedAt = Date.now();
}
next(data, ctx)
}
],
columns: {
...columnMap
},
},
}
})

Log all requests#

import SQLResolverGenerator, { MutationOperationType } from '@pdftron/collab-sql-resolver-generator';
const loggerMiddleware = (entityType) => ({ data, type, ctx, next }) => {
console.log(`${entityType} - ${type} - ${JSON.stringify(data)}`);
next(data, ctx)
}
const resolvers = SQLResolverGenerator({
info: {
Documents: {
table: 'Documents',
writeMiddleware: [
loggerMiddleware('Documents')
],
columns: {
...columnMap
},
},
}
})

Async middleware#

Since the next middleware is not called until the previous middleware calls next, you can make your middleware functions asynchronous.

import SQLResolverGenerator, { MutationOperationType } from '@pdftron/collab-sql-resolver-generator';
const checkPermissions = async ({ data, type, ctx, next }) => {
const isAdmin = await userIsAdmin(data.UserId);
if(isAdmin) {
ctx.isAdmin = true;
}
next(data, ctx)
}
const resolvers = SQLResolverGenerator({
info: {
Documents: {
table: 'Documents',
writeMiddleware: [
checkPermissions
],
columns: {
...columnMap
},
},
}
})

With context#

Context is extremely useful for setting custom data in your database.

First, set the context data using the setContext or updateContext function provided by the client:

// Client side code executed in browser
import { CollabClient } from '@pdftron/collab-client'
const client = new CollabClient({
...options
})
// Some time later
await client.setContext({
projectId : '123'
})

Now, you have access to projectId in all your middleware functions:

import SQLResolverGenerator, { MutationOperationType } from '@pdftron/collab-sql-resolver-generator';
const resolvers = SQLResolverGenerator({
info: {
Documents: {
table: 'Documents',
writeMiddleware: [
({ data, type, ctx, next }) => {
if(type === MutationOperationType.CREATE) {
data.ProjectId = ctx.projectId;
}
next(data, ctx)
}
],
columns: {
...columnMap
},
},
}
})

Blob storage#

When using the snapshots feature, it may be a good idea to store the XFDF strings in a blob to avoid storing large strings directly in the database.

You can combine read and write middleware to achieve this:

import SQLResolverGenerator, { MutationOperationType } from '@pdftron/collab-sql-resolver-generator';
const resolvers = SQLResolverGenerator({
info: {
Snapshots: {
table: 'Snapshots',
// Here we are using write middleware to convert XFDF into blobs.
// The file path to that blob is then stored instead of the actual XFDF
writeMiddleware: [
async ({ data, type, ctx, next }) => {
if(type === MutationOperationType.CREATE) {
const filePath = await createBlob(data.xfdf);
data.xfdf = filePath; // transform the XFDF into a file path that we can fetch later
}
next(data, ctx)
}
],
// When reading the snapshot, we now need
// to convert the blob back into a string
readMiddleware: [
async ({ data, ctx, next }) => {
const xfdf = await getBlobContents(data.xfdf)
// transform the data back into an XFDF string
data.xfdf = xfdf;
next(data, ctx)
}
]
columns: {
...columnMap
},
},
}
})