Extending with Code

Gadget can be extended in a variety of ways using JavaScript files that interplay with the existing bits of the platform. You can add Validations, Preconditions, and Effects written in JS to make your application do exactly what it needs to.

For Computed fields, refer to the Computed Fields guide.

When to write code

Code is the right tool to solve many problems for developers, and there's a whole lot of it out there already! Gadget is built for this reality. While Gadget handles a good amount of the boilerplate, there's lots of business logic unique to your problem that is best captured in code.

We recommend that when there is a platform tool that solves your problem, you try to use it before dropping down to coding your own. The Gadget team is constantly adding features and fixing bugs so that you don't have to do it yourself. Where possible, staying on the upgrade train is generally preferable so your application stays correct, performant, and secure.

If you need to write code though, you should not hesitate! Gadget strives to be an excellent neighbour and provides all the same runtime tooling for custom code as it does for the built in primitives. Your code can use the Gadget logging system, the scalable runtime, and do all the same things the builtins do like writing data to the database. If code is the right tool for the job, you should go right ahead and use it.

Runtime environment

Gadget runs JavaScript code, with support for TypeScript and other languages on the way. Gadget runs JavaScript using node.js version 16, and runs code inside containers on Linux. This means Gadget has support for:

  • JS packages from the npm package registry, see the section on Packages from npm
  • JS packages that require native extensions, like sharp or esbuild
  • including other file types in your app and reading them, like metadata files in JSON or YAML
  • including images and other assets you might need

Gadget's backend node.js processes are orchestrated using a fast, custom serverless runtime layer running in Google Cloud Platform. Each Gadget application is automatically deployed when anything changes, and each app is scaled up and down as needed without any manual knob-turning required.

There's no bundling or build step required when writing code for Gadget's runtime. So, you don't need to assemble your different files into a .zip or giant JS file before you can run your application, or fight with build tools to create exactly the right artifact for deployment 🎉

Deploying new code

Gadget is a deployless system -- all code written in the Gadget editor is running live in the cloud as soon as it is saved. This allows you to make changes and then preview your application immediately, keeping your feedback loop short and helping you get stuff done. Your code runs with the full power of Gadget's optimized, elastic cloud systems; so if you need to chew through a lot of data during a big migration, or access sensitive third party APIs, you're able to do so in development just as easily as you might in production.

Validations

Gadget has a variety of built in validations that cover the common cases for data validation. If you have a complicated or unique validation rule that governs data in your application, Gadget supports adding your own validations written in JavaScript. The Run Code validation runs a JS function each time a record is created or updated to check if that change should succeed. If the function reports no errors, then the save will complete, and if the data is somehow invalid, the validation can add structured error messages to the different fields of the record being changed.

Each Run Code validation file must export a function as the default export function from the module. The function can be marked async or return a Promise The function doesn't need to return anything, and instead, should add errors to any fields determined to be invalid using the passed in errors object. The errors object is an Errors that holds a structured set of errors keyed by field.

By convention, Run Code validations are placed in a root level folder named after the model, and then in the validations folder. For a model named Widget for example, the Gadget editor will suggest you add the first validation at widget/validations/some-useful-code.js. You can move or rename these files however you wish.

Validation functions get passed one big context object and not individual arguments. This is because there is a lot of arguments you may or may not need. Most Gadget users use JavaScript destructuring to pull out just the context keys that they need. The most common keys used from the context are the record itself at the record key, the api object which is an already set up instance of the JavaScript client for the application at api, and the Errors object at the errors key.

Examples

Here's a validation that tests if a record's full name is not null, its last name is also not null:

JavaScript
1module.exports = async ({
2 record,
3 errors,
4}) => {
5 if (
6 record.firstName &&
7 !record.lastName
8 ) {
9 errors.add(
10 "lastName",
11 "must be set if the first name is set"
12 );
13 }
14};

Here's a validation that tests if a record's phone number is valid:

JavaScript
1const PhoneNumber = require("awesome-phonenumber");
2
3module.exports = async ({
4 record,
5 errors,
6}) => {
7 const number = new PhoneNumber(
8 record["customerPhoneNumber"]
9 );
10 if (!number.isValid()) {
11 errors.add(
12 "customerPhoneNumber",
13 "is not a valid phone number"
14 );
15 }
16};

If a validation function throws an error while executing, the action validating the current record will fail and its effects rolled back.

Preconditions

Preconditions capture logic about when an action can be taken. If the preconditions fail, no action effects run. Preconditions are useful specifically because they can be run independently of the action itself to figure out if the action can currently be taken. This is great for disabling buttons that run actions in a UI or hiding bits of functionality a user is unable to access.

Preconditions are all expressed as files of JavaScript code. Each Precondition file is expected to be a valid JavaScript module that exports a function that returns true or false as its default export. The function can be asynchronous, and will be awaited by the Gadget runtime before running the rest of the preconditions or action. If the precondition returns true, the action execution will proceed, and if it returns false, the action's execution will be halted and any database effects rolled back.

When you create a precondition in the Gadget behavior system, Gadget will automatically create you a .js file to hold your preconditions's source code. It will be placed in a root level folder named after the model, and then in a folder named after the action the precondition is on, and then in the preconditions folder. For an action named Update on a model named Widget for example, you'd find the first precondition you add to Update at widget/update/preconditions/check-A.js. You can rename this file to make the name more descriptive if you like.

Precondition functions get passed one big context object and not individual arguments. This is because there is a lot of arguments you may or may not need. Most Gadget users use JavaScript destructuring to pull out just the context keys that they do need. The most common keys used from the context are the record itself at the record key and the api object which is an already set up instance of the JavaScript client for the application at api.

If a precondition function throws an error while executing, the action acting on the current record will fail, and its effects will be rolled back.

Examples

Here's a precondition which only allows a record to be updated if the record's owner field matches the current logged in user:

JavaScript
1module.exports = ({
2 api,
3 record,
4 session,
5}) => {
6 const currentUser =
7 session.get("user");
8 return (
9 record.owner._link ==
10 currentUser._link
11 );
12};

Here's a precondition which only allows blog posts to be published on weekdays:

JavaScript
1module.exports = () => {
2 const currentDay =
3 new Date().getDay();
4 return (
5 currentDay > 1 && currentDay < 6
6 );
7};

Here's a precondition which only allows tasks in a todo application to be started if their due date is present. This would allow tasks to be created in an unstarted state without due dates, but prevent them from being started until the due date is set.

JavaScript
module.exports = ({ record }) => {
return record.dueDate != null;
};

Effects

Effects do useful stuff for users of Gadget applications. Gadget has some built in effects for basic manipulation of the database, but often, specific bits of business logic need to be written in code to solve a problem well. Run Code effects are where business logic that changes things in the database or writes to third party APIs should go. Run Code effects are run in one of three separate stages: the Run Effects, the Success Effects, or the Failure Effects, and more information about the transaction boundaries around these effect lists can be found in the Behavior Guide. The interface for effect code is the same regardless of which effect list an effect runs in.

Like Validations and Preconditions, each Run Code effect file must export a function as the default export from the module. If asynchronous, this function will be awaited by the Gadget runtime during action execution. The function should run whatever logic it needs to, which most often is using the api object to make changes to the Gadget database, or inspecting the record object to send its data elsewhere.

Each Run Code effect lives in one .js file. When you create a Run Code effect, Gadget will automatically create a .js file to hold the effect's source code. This file will be placed in a root level folder named after the model, and then in a folder named after the action the effect runs within. For an action named Update on a model named Widget for example, you'd find the first Run Code effect you add to Update at widget/update/run-A.js. You can rename this file to make the name more descriptive if you like.

Effect Context

Run Code functions get passed one argument, which is a big EffectContext context object full of useful data. They don't get passed individual arguments. This is because there is a lot of different elements you may or may not need. Most Gadget users use JavaScript destructuring to pull out just the context keys that they do need. The most common keys used from the context are the record itself at the record key, and the api object which is an already set up instance of the JavaScript client for the application at api.

The EffectContext object passed into each effect function has the following keys:

  • api: An connected, authorized instance of the generated API client for the current Gadget application.
  • params: The incoming data from the API call invoking this action
  • record: The root record this action is operating on
  • session: An record representing the current user's session, if there is one
  • config: An object of all the configuration values created in Gadget's Configuration editor
  • connections: An object containing client objects for all connections
  • logger: A logger object suitable for emitting log entries viewable in Gadget's Log Viewer
  • model: An object describing the metadata for the model currently being operated on, like the fields and validations this model applies

record use in effects

For Model Actions, Gadget automatically loads the record that the action is being taken on from the database at the start of the action. The record is stored in the record key on the action context, so it is there if you'd like to access it. Effects that change the record should apply those changes to the record object on the context. Note that the record is not automatically saved, and that you must use a Create Record or Update Record effect to persist record changes, or use the api object to persist exactly the changes you want from within a Run Code effect.

The record passed to an effect may not be persisted yet. In the case of a Create action for example, there will be a record in the context, but it won't have an id assigned yet. To persist the record and assign it an id, run a Create Record effect first, or use the Internal API via api.internal to manually save the record in your code.

api use in effects

The api object that gets passed into an effect function is an instance of the generated JavaScript client for your Gadget application. It works the same way as it might in the browser or in a nodejs app connecting to Gadget over the internet, and has all the same functions documented in the API Reference. Depending on what you're trying to accomplish with an effect, it is sometimes appropriate to use the Public API of your application, via api.someModels.create, and sometimes it's appropriate to use the Internal API, via api.internal.someModel.create.

The Public API for a Gadget application invokes the Model Actions and Global Actions defined in the application. This means that if you call api.someModels.create() within an effect function, you are invoking an action from within the currently invoking action. This is supported by Gadget and often the right thing to do if your business logic needs to trigger other high level logic. It can cause infinite loops though -- if you invoke the currently underway action from within itself, you may find the action execution times out or errors because it was continuously recursing into itself.

The Internal API of a Gadget application makes low level changes to the Gadget database. Gadget's Database effects use the Internal API to implement themselves. If you want a performant way to change the database, or if you specifically want to skip validations and other high level business logic, the Internal API is the right thing to use.

connections use in effects

The connections object in an effect's context contains prebuilt client objects with the appropriate set of credentials, which you can use to make remote API calls. This is useful when the remote calls you need aren't provided as effects, or you need them to happen conditionally.

For example, suppose you have a Shopify connection to your store best-sneakers.myshopify.com and you want to update a product. You could write an effect with this:

JavaScript
1await connections[
2 "best-sneakers.myshopify.com"
3].product.update(
4 productId,
5 productParams
6);

It's important to keep in mind that changing your remote data could result in an update webhook being sent back to Gadget. Care will need to be taken to ensure you don't get into an infinite feedback loop with the remote system. One good way to ensure this doesn't happen is to make remote updates in success effects. This guarantees the record has been committed to the database, so you can check the incoming webhook to see if a relevant field has changed before making the remote call.

Visit the Connections guide to learn more about each of the specific clients.

Specifying custom params

If you would like to have a custom input param on your API, you can specify it via module.exports.params in any code effect, typically the code effect that consumes the param. We currently use a subset of the JSON schema spec for specifying these params. For example, if you want to expose a boolean flag and name object in your action or global action:

JavaScript
1module.exports = async ({ params }) => {
2 if (params.sendNotifications) {
3 // ...
4 }
5};
6
7module.exports.params = {
8 sendNotifications: {
9 type: "boolean",
10 },
11 fullName: {
12 type: "object",
13 properties: {
14 first: { type: "string" },
15 last: { type: "string" },
16 },
17 },
18};

With that in place in your code effect, you can now make the following GraphQL call:

GraphQL
1# action
2mutation {
3 myActionModelA(
4 sendNotifications: true
5 fullName: {
6 first: "Jordan"
7 last: "Stag"
8 }
9 ) {
10 success
11 # …other selections…
12 }
13}
14
15# global action
16mutation {
17 myAction(
18 sendNotifications: true
19 fullName: {
20 first: "Jordan"
21 last: "Stag"
22 }
23 ) {
24 success
25 # …other selections…
26 }
27}

The params specification must come after the default export, module.exports = ..., so that there's something to attach the params specification to. Also note from the above example that the schema is written as a JavaScript object.

We currently support the following types from JSON schema:

  • object
  • string
  • integer
  • number
  • boolean

No other features of JSON schema – such as validations, required (all params are optional), and schema composition primitives like allOf – are currently supported.

Global Action Run Code Effects

Global Actions can also use Run Code effects for whatever they need to accomplish. Run Code effects within Global Actions won't be passed a record object or a model object, but they will still be passed the params, the config, the logger, and all the associated goodies you would find in a model Run Code effect.

Examples

Here's an example effect which updates a user's fullName field to be a combination of the firstName field and lastName field:

JavaScript
1module.exports = async ({
2 api,
3 record,
4 session,
5}) => {
6 await api.internal.user.update(
7 record.id,
8 {
9 user: {
10 fullName:
11 record.firstName +
12 " " +
13 record.lastName,
14 },
15 }
16 );
17};

Note that this effect uses api.internal.user.update, so it's using the Internal API to make a fast, simple update to the User model in the Gadget database, and is not running the Update action of the User model.

Here's an example effect which checks if an incoming purchase is above a certain amount, and if so, creates a Fraud Review record to power a business process to take a look at the especially high value order:

JavaScript
1const HIGH_VALUE_THRESHOLD = 20000; // cents
2
3module.exports = async ({
4 api,
5 record,
6 session,
7}) => {
8 if (
9 record.totalPrice.amount >
10 HIGH_VALUE_THRESHOLD
11 ) {
12 await api.fraudReviews.start({
13 order: { _link: record.id },
14 });
15 }
16};

Note that this effect uses the api.fraudReviews.start function, which triggers the execution of another action, the Start action on the Fraud Review model. This is desirable because perhaps that action itself has some effects that are important, so this code can run those effects without needing to know exactly what they are.

Here's an example effect which uses the Akismet spam detection API to ensure a comment isn't spammy during the Create action:

JavaScript
1const {
2 AkismetClient,
3} = require("akismet-api");
4
5// instantiate the client for the remote service
6const client = new AkismetClient({
7 key: "my-api-key",
8 blog: "https://myblog.com",
9});
10
11module.exports = async ({
12 api,
13 record,
14}) => {
15 const isSpam = await client.checkSpam(
16 {
17 content: record.body,
18 email: record.email,
19 }
20 );
21 if (isSpam) {
22 // throwing this error will roll back the transaction, aborting any in progress creates or updates
23 throw new Error(
24 "Can't save the comment because it seems spammy"
25 );
26 }
27};

We model spam detection here as an effect, not as a precondition, because we want to allow anyone to try to create comments, and only know that they are spammy once they have given us the comment they want to create. It'd be great if somehow we knew who spammers were ahead of time, but we need them to attempt to comment before we can make that assessment.

Sharing code between files

All the code for your Gadget application is stored in a directory structure representing a plain old node.js module, so you can do all the things you normally can with a plain old node.js module like requiring files! You can create shared utilities and put them in whatever folder structure you like.

For example, if you have a Gadget application with these files:

markdown
widget/
create/
run-A.js
update/
run-B.js

and you'd like to share a utility function between those two run effects, you could create a file at widget/utils.js with the function in it:

JavaScript
module.exports.leftPad = (
string,
padding
) => string.padStart(padding);

and then require it in widget/create/run-A.js and widget/update/run-A.js:

JavaScript
1// in widget/create/run-A.js and widget/update/run-A.js
2const { leftPad } = require("../utils");
3
4module.exports = async ({
5 api,
6 scope,
7}) => {
8 // ... effect code that uses `leftPad`
9};

You can also create other folders or put source code files at the root of your project. We recommend sticking to the one-folder-per-model convention for structuring your validation, precondition, and effect code so that when Gadget generates you the boilerplate it will be close to the other code that touches the same model.

Packages from npm

Gadget supports installing node.js packages from the npm package registry like you might in other node.js systems. Each Gadget application has a standard, spec-compliant package.json file at the root which lists the packages the application needs to run. To add a package to your application, add a new key to the dependencies object inside package.json, and Gadget will install the most recent version of that package matching your version constraint. If Gadget fails to install your dependencies, a small red status icon will appear beside the package.json entry in the file tree with an error message explaining the issue.

Once installed, packages can be required by requiring them like you might in any other node.js project using require.

For example, if we add lodash to a package.json so it looks like this:

json
1{
2 "name": "an-example-gadget-app",
3 "version": "0.1.0",
4 "private": true,
5 "description": "Internal package for Gadget app an-example-gadget-app (Production environment)",
6 "dependencies": {
7 "@gadget-client/an-example-gadget-app": "link:.gadget/client",
8 "lodash": "^4.17.0"
9 }
10}

then we can require it in a validation, precondition, or effect file with require:

JavaScript
1// in some effect or precondition file
2const _ = require("lodash");
3
4module.exports = async ({
5 api,
6 scope,
7}) => {
8 // ... effect code that uses `_.defaults` or `_.groupBy` or any other Lodash helper function.
9};

Gadget also requires that your app depend on the generated Gadget API client. All Gadget apps are generated with an existing dependency on the Gadget API client, which the platform places into node_modules/.gadget automatically. This dependency shouldn't be removed.