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).
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.
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.