Skip to main content

Middleware

It's a common pattern: middlware. EmbraceSQL uses this pattern inside the database invocation stack to allow you to:

  • view and modify parameters
  • view and modify values
  • provide security
  • run additional SQL statements in the same transaction as your actual query

Concepts

Middleware are just asynchronous functions with two parameters (context, next).

Middleware acts by working on a shared context. Modifying this shared context affects the action of subsequent middlware and the final call of your actual query.

When your middleware is complete, you call next which hands off to the next middleware.

You can throw and Error at any time to abort processing a request.

Parameters

When EmbraceSQL says parameters, it means data used to filter data in WHERE clauses.

Middleware provides the ability to get and set parameters.

You can use this for:

  • dynamic query alternation
  • security
  • logging

Values

When EmbraceSQL says values it means data that will be sent to and stored in the database via INSERT and UPDATE.

Middleware provides the ability to get and set values.

You can use this for:

  • an in-code alternative to row modifying triggers
  • default values that rely on code outside the database
  • validating input that relies on code outside the database
  • calling external APIs before finally going to the database

Example

Here is an example of using middleware to affect parameters and values hoisted up from a unit test.

import { Database, Api } from "../src/marshalling";

describe("Database middleware can", () => {
let db: Database;
beforeAll(async () => {
db = await Database.connect(
"postgres://postgres:postgres@localhost:5432/marshalling",
);
});
afterAll(async () => {
await db.disconnect();
});
it("change a parameter", async () => {
// set up middleware that will change the message parameter
db.clear();
db.use(async (context, next) => {
// gonna change that parameter -- but middleware is pretty open type
// so cast/ check -- you can figure this type by 'hovering' over
// `.call` in VSCode -- and they follow a generated pattern
const parameters = context.request
?.parameters as Api.Procedures.Echo.Parameters;
if (parameters.message) {
// this clearly makes it more exciting
parameters.message = `${parameters.message}‼️`;
}
return next();
});
const ret = await db.Api.Procedures.Echo.call(
{
message: "Hello",
},
{ headers: { ROLE: "postgres" } },
);
expect(ret).toBe("Hello‼️");
});
it("change a value", async () => {
// set up value changing middleware -- this will be on an
// AutoCRUD table that contains a single geometric point
db.clear();
db.use(async (context, next) => {
// pick out the values and cast them to a values type
// use this to check that we are actually working on Points
const values = context.request?.values as Api.Types.Points;
if (values.point) {
values.point.x += 1;
values.point.y += 2;
}
return next();
});
// creating a row reads back what is inserted
const ret = await db.Api.Tables.Points.create({
point: {
x: 0,
y: 0,
},
});
expect(ret).toMatchObject({ point: { x: 1, y: 2 } });
});
});