Skip to main content
Version: v3.x beta

Custom Business Logic via TypeScript

Introductionโ€‹

The Node.js Lambda Connector can be added to any project to incorporate custom business logic directly into your supergraph.

This can be used to enrich data returned to clients, or to write custom mutations in TypeScript with Node.js using a pattern known as Command Query Separation (CQS).

Prerequisites

If you've never used Hasura DDN, we recommend that you first go through the quickstart. ๐Ÿ˜Š

Functions or Proceduresโ€‹

TypeScript functions exported from a file in the Node.js Lambda Connector will then be available from your Hasura DDN GraphQL API in the form of either functions or procedures.

In Hasura metadata, functions are used for read operations. They will not modify the data in the database and can only be used to retrieve data.

Conversely, procedures are used for write operations. They can modify the data in the database and can be used to create, update, or delete data.

The distinction is important in metadata because it allows the system to know what types to expect for arguments and return values.

Add the TypeScript connector to a projectโ€‹

Using the CLI in a project's directory, add the Node.js Lambda connector:

ddn add connector-manifest fx_connector --type cloud --hub-connector hasura/nodejs:v1.2.0 --subgraph app

This will add a connector manifest โ€” which contains the configuration for a connector โ€” named fx_connector to the default app subgraph for a project. Additionally, the CLI will create a functions.ts file within the connector's new subdirectory:

โ”œโ”€โ”€ app
โ”‚ย ย  โ””โ”€โ”€ fx_connector
โ”‚ย ย  โ”œโ”€โ”€ fx_connector.hml
โ”‚ย ย  โ””โ”€โ”€ connector
โ”‚ย ย  โ”œโ”€โ”€ fx_connector.build.hml
โ”‚ย ย  โ”œโ”€โ”€ functions.ts
โ”‚ย ย  โ”œโ”€โ”€ package-lock.json
โ”‚ย ย  โ”œโ”€โ”€ package.json
โ”‚ย ย  โ””โ”€โ”€ tsconfig.json

We can then run the following command to build and deploy our connector to Hasura DDN:

ddn dev

With the development server running, we can iterate on our files locally with each file save triggering a new build which is visible on our project's console.

Add a functionโ€‹

By default a function โ€” in the nomenclature of DDN โ€” is already present in our functions.ts file. It's designated as a function by the JSDoc comment using the @readonly tag:

/**
* @readonly Exposes the function as an NDC function (the function should only query data without making modifications)
*/
export function hello(name?: string) {
return `hello ${name ?? "world"}`;
}

You can add another function simply by exporting a valid TypeScript function. If ddn dev is running, the change will automatically be picked up the CLI, the associated metadata for the command will be generated, and you will be able to use your function by name in the GraphQL API.

Each function or procedure has tracked metadata
For the boilerplate hello() function included by default, you'll see a Hello.hml file in the commands subdirectory of the connector. Click here to check it out ยป
---
kind: Command
version: v1
definition:
name: Hello
outputType: String!
arguments:
- name: name
type: String
source:
dataConnectorName: fx_connector
dataConnectorCommand:
function: hello
graphql:
rootFieldName: app_hello
rootFieldKind: Query

---
kind: CommandPermissions
version: v1
definition:
commandName: Hello
permissions:
- role: admin
allowExecution: true

Add a procedureโ€‹

Inside the same functions.ts file, let's add a custom mutation in the form of a procedure.

For example, let's say you want to add a custom mutation to create a user in a PostgreSQL table on your default app subgraph and also hash their password before storing it.

You can do this by adding the following code to the functions.ts file:

import { Client } from "pg";
import bcrypt from "bcrypt";

// Define the User type using TypeScript
type User = {
name: string;
email: string;
password: string; // Add a password field to the User type
};

/**
* Inserts a user into the database and returns the inserted user.
* Hash password for secure storage.
*
* @param user The user to insert into the database. Expects a name, email, and password.
* @returns The user that was inserted into the database.
*/
export async function insertUser(user: User): Promise<Omit<User, "password">> {
// Get your database connection string.
// NB!! Use env vars for this in production NB!!
const connectionString = "postgresql://username:password@localhost:5432/mydb";

const client = new Client({
connectionString,
});
await client.connect();

// Hash the user's password before storing it
const saltRounds = 10; // Pretty decent number of rounds
const hashedPassword = await bcrypt.hash(user.password, saltRounds);

const result = await client.query(
`INSERT INTO users (name, email, password) VALUES ($1, $2, $3) RETURNING *`,
[user.name, user.email, hashedPassword] // Use the hashed password here
);

const rows = result.rows.map((row) => ({
id: row.id,
name: row.name,
email: row.email,
// Do not return the password field for security reasons
}));

await client.end();

return rows[0];
}

This:

  • creates a function called insertUser.
  • takes in a user's name, email, and password.
  • hashes their password.
  • inserts into the users table in the database.
  • returns the inserted user without password.

We're using the pg PostgreSQL client to connect to the database and run the query and bcrypt to hash the password, but since this is a Node.js Lambda function, you can use any combination of Node.js libraries you want.

After you've tracked the function and created a new build, you can use the function in your GraphQL API.

You can then repeat this process to scale your mutations independently.

Loading...