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 and Effects written in JS to make your application do exactly what it needs to.
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.
However, if you need to write code, don't hesitate to do so. Gadget strives to be an excellent neighbor 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, Gadget gives you all the tools to use it.
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 lets you make changes and 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.
No bundling or build step is 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
🎉
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 shouldn't 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 are 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.
Validation examples
Here's a validation that tests if a record's full name is not null and its last name is also not null:
JavaScript
module.exports=async({ record, errors })=>{
if(record.firstName&&!record.lastName){
errors.add("lastName","must be set if the first name is set");
}
};
Here's a validation that tests if a record's phone number is valid:
JavaScript
1constPhoneNumber=require("awesome-phonenumber");
2
3module.exports=async({ record, errors })=>{
4const number =newPhoneNumber(record["customerPhoneNumber"]);
5if(!number.isValid()){
6 errors.add("customerPhoneNumber","is not a valid phone number");
7}
8};
If a validation function throws an error while executing, the action validating the current record will fail, and its effects wil roll back.
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 should be written in code to solve a problem well. Run Code Effects are where you should put this code. Each Run Code Effect is created by adding one with the plus button to one of the Action Effect lists:
Run Effects generally run in a database transaction, and Success Effects run after the transaction has been committed. For more information about the differences and Action transaction boundaries, see the Actions Guide. The interface for Effect code is the same regardless of which Effect list an Effect runs in.
Effect functions
Like Validations, 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. Once you name the file, it 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/yourFileName.js. Existing files in the widget/update directory will be shown; however, if you'd like to use an existing file that is not located in this directory, you may start typing to search for the file.
For example, we could create a Run Code Effect on an update Action of a model named Widget like so:
widget/update/example-effect.js
JavaScript
module.exports=({ record, logger })=>{
logger.info({record: record.id},"Hello from an Effect!");
};
Effect context
Run Code functions get passed one argument, which is a big EffectContext context object describing the current Action execution. Effects get passed one object with all the necessary keys instead of individual arguments. This is because there are many different elements you may or may not need. Most Gadget users use JavaScript destructuring to pull out just the context keys that they require.
The EffectContext object passed into each Effect function has the following keys:
api: A connected, authorized instance of the generated API client for the current Gadget application. See the API Reference for more details on this object's interface.
params: The incoming data from the API call invoking this action.
record: The root record this action is operating on.
session: A record representing the current user's session, if there is one.
config: An object of all the environment variables created in Gadget's Environment Variables editor.
connections: An object containing client objects for all connections. Read the connections guide to see what each connection provides.
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.
currentAppUrl: The current url for the environment. e.g. https://my-app.gadget.app
trigger: An object containing what event invoked the model's Action to run and trigger the Run Effect.
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 within the context. Note that the record is not automatically saved and that you must use a Create Record or Update Record Effect or API calls to persist record changes.
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 in an Effect as it does elsewhere and has all the same functions documented in the API Reference. You can use the api object to fetch other data in an Effect:
JavaScript
1// example: send a notification to the user who owns the record if they've opted in
9body:`Notification: ${record.title} was updated`,
10});
11}
12};
Or, you can use the api object to change other data in your application as a result of the current Action:
JavaScript
1// example: save an audit log record when this record changes
2module.exports=async({ api, record, model })=>{
3const changes = record.changes();
4
5// run a nested `create` action on the `auditLog` model
6await api.auditLogs.create({
7action:"Update",
8model: model.apiIdentifier,
9record:{_link: record.id},
10changes: changes.toJSON(),
11});
12};
Sub-Action execution in Gadget is still governed by the same rules as top-level Action execution.
Using the Public API vs the Internal API
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.someModel.create, and sometimes it's appropriate to use the Internal API via api.internal.someModel.create.
Since Effects are used to run the important business logic of your application, you will generally want to use the Public API.
The Public API for a Gadget application invokes Model Actions and Global Actions with all their Effects. This means that if you call api.someModel.create() within an Effect, you are invoking another Action from within the currently invoking Action. This is supported by Gadget and is often the right thing to do if your business logic needs to trigger other high-level logic.
Sometimes, though, you may want to skip executing the Effects that make up an Action. The Internal API does just this. Internal API endpoints make low-level changes to the Gadget database and don't run Effects of any Action. The Internal API can be used safely within Effects by running api.internal.someModel.<action name>. You can think of the Internal API like raw SQL statements in other frameworks where you might run explicit INSERT or UPDATE statements that skip over the other bits of the framework. The Internal API is significantly faster and is often used for building high-volume scripts that need to import data or touch many records quickly. However, because the Internal API skips important logic for Actions, it is generally not recommended for use unless you have an explicit reason.
Action infinite loops
Be careful when invoking Actions within Effects, especially if invoking the same Action from within itself.
Invoking Actions within Actions can cause infinite loops, so care must be taken to avoid this. If you invoke the currently underway Action from within itself, your Effects will run again, potentially invoking the Action again in a chain. Or, if you invoke an Action in model B from model A, but model B invokes an Action in model A, you can create a chain of action invocations that never ends. Action chains like this can show up in the logs as timeouts or errors that happen at an arbitrary point in an Action.
To avoid infinite loops, you must break the loop somehow. There are a couple of options for this:
Use record.changed('someField') to only run a nested Action some of the time when data you care about has changed
Use the Internal API instead of the Public API (api.internal.someModel.update instead of api.someModel.update) to make changes, skipping the effect stack that retriggers the loop
Connection Action infinite loops
Be careful when invoking remote APIs within Effects if those remote APIs might fire webhooks that call back to your application,
re-invoking the same Action.
Calling third-party APIs from your Effects is common, but it's important to be aware that third-party APIs may trigger subsequent Actions in your Gadget app in response to changes.
For example, if you have a Gadget app connected to a Shopify store with a connected Shopify Product model, every time Shopify sends the app a product/updated webhook, Gadget will run the update Action. If the business logic inside the update Action makes API calls back to Shopify to update the product again (say to update the tags of a product in response to a change in the other fields), the API call back to Shopify will trigger a second webhook sent back to Gadget. Without care, this can cause an infinite loop of Actions triggering webhooks that retrigger the Actions.
Read more about the logger object in the Logging guide.
trigger use in Effects
Each Action Effect is passed a trigger object that contains information related to what event triggered the Action to run. The trigger object is different for each type of trigger that can call an action and has a type object describing which type of trigger fired.
someModel/someActionEffect.js
JavaScript
module.exports=({ api, record, trigger })=>{
console.log(trigger);// will log an object like { "type": "api", "rootModel": "someModel", "rootAction": "someAction", etc ... }
};
API triggers
{"type": "api"} triggers describe calls to your Gadget app's GraphQL API, like those made by the JS client or in the GraphQL playground. The API trigger has the following fields:
type: will always be set to "api"
mutationName: the string name of the mutation called in the API
rootModel: the API identifier of the Model the mutation was called on. Can be different than the root-level model when invoking Nested Actions. Is not set for Global Actions.
rootAction: the API identifier of the Action triggered by the mutation. Can be different than the root-level action when invoking Nested Actions.
rawParams: the params passed to this API call, including any data for nested actions if passed
An example API trigger
json
1{
2"type":"api",
3"mutationName":"updateWidget",
4"rootModel":"widget",
5"rootAction":"update",
6"rawParams":{
7"id":"123",
8"widget":{
9"title":"New Widget Title",
10"inventoryCount":10
11}
12}
13}
When making API calls that invoke Nested Actions, the trigger object will describe the root level trigger and the root level params.
For example, if calling a root-level create action on a Widget model, and passing params that will create a nested Gizmo model, both actions run will receive the same trigger object with the same rawParams:
{"type": "scheduler"} triggers describe actions invoked by the built-in Scheduler within Gadget. No other data is currently passed with this type of trigger.
Shopify sync triggers
{"type": "shopify_sync"} triggers describe actions run by Gadget's Shopify Sync, including daily syncs and manual syncs. The Shopify sync trigger object has the following fields:
type: will always be set to "shopify_sync"
shopId: the identifier of the Shopify shop being synced
apiVersion: the version of the Shopify API being used for the sync
shopifyScopes: the available OAuth scopes of the Shopify shop being synced
syncId: the identifier of the sync record tracking the state of this sync (optional, only available if set)
syncSince: the specified date range of this sync (optional, only set if specified when the sync was started)
models: the list of model API identifiers that this sync will work on
force: indicates if this sync is being run in 'force' mode, which will always run actions even if the 'updated_at' timestamps match between Gadget and Shopify
startReason: the string describing the reason why this sync was started (optional, only set if specified when the sync began)
10"startReason": undefined // will be "scheduled" if Action ran via daily sync
11}
Shopify webhook triggers
{"type": "shopify_webhook"} triggers describe actions that occur in response to Shopify webhooks, such as 'products/update' or 'orders/create'. The Shopify Webhook trigger has the following fields:
type: will always be set to "shopify_webhook"
topic: the string representing the topic of the incoming webhook from Shopify, like products/update or orders/create
payload: the raw incoming payload from Shopify, which includes all the data sent by the webhook unchanged
shopId: the identifier for the Shopify store that received the webhook
retries: the number of times this webhook has been retried
json
1// An example Shopify Webhook trigger
2{
3"type":"shopify_webhook",
4"topic":"products/update",
5"payload":{
6"id":788032119674292900,
7"title":"Example T-Shirt",
8"body_html":"An example T-Shirt",
9"vendor":"Acme",
10"product_type":"Shirts",
11"created_at":null,
12"handle":"example-t-shirt"
13// ... etc matching Shopify's format exactly
14},
15"shopId":"shop123",
16"retries":0
17}
Shopify oauth install triggers
{"type": "shopify_oauth"} triggers describe actions invoked during the installation of an app through the Shopify Partners connection process. No other data is currently passed with this type of trigger.
Shopify admin install triggers
{"type": "shopify_admin"} triggers describe actions invoked during the installation of a Shopify app provisioned in the Shopify Admin. No other data is currently passed with this type of trigger.
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 that creates a Shopify client for this shop, and makes an API call to Shopify like so:
Visit the Connections guide to learn more about each of the specific clients.
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.
Specifying custom params
Sometimes actions need to accept parameters that aren't directly stored in the model, like a sendNotifications param that might enable or disable some business logic for sending emails. Custom params are added to an Action by exporting a params object from a custom Code Effect on the action. Gadget expects the exported params object to be a subset of a JSON schema specification. For example, if you want to expose a boolean flag and name object in your Action or Global Action:
JavaScript
1module.exports=async({ params })=>{
2if(params.sendNotifications){
3// ...
4}
5};
6
7module.exports.params={
8sendNotifications:{type:"boolean"},
9fullName:{
10type:"object",
11properties:{
12first:{type:"string"},
13last:{type:"string"},
14},
15},
16};
With that in place in your Code Effect, you can now make the following GraphQL call:
The params specification must come after the default export, module.exports = ..., so that there's something to attach the params specification to. Note from the above example that the schema is written as a JavaScript object.
Gadget currently supports the following types from JSON schema:
object
string
integer
number
boolean
array
Only these primitive types are currently supported by Gadget. No other features of JSON schema are currently available, so don't use
validations, required and/or schema composition primitives like allOf. If you want to validate parameter values, you can do so in the
code for the Effect.
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.
Adding custom params to a Global Action works similarly to specifying custom params. For example:
JavaScript
1/**
2 * Effect code for global action globalActionA
3 * @param { import("gadget-server").GlobalActionAGlobalActionContext } context - Everything for running this effect, like the api client, current record, params, etc
6 logger.info({ params },"params will be written to the Logs");
7};
8
9module.exports.params={
10student:{
11type:"object",
12properties:{
13firstName:{type:"string"},
14lastName:{type:"string"},
15age:{type:"number"},
16},
17},
18};
A GraphQL mutation for this Global Action would look like this:
GraphQL
1mutation($student:GlobalActionAStudentInput){
2globalActionA(student:$student){
3success
4errors{
5message
6}
7result
8}
9}
And uses the following variables:
json
1{
2"student":{
3"firstName":"Arnold",
4"lastName":"Schwarzenegger",
5"age":28
6}
7}
Return data from a Global Action
To return data from a Global Action, you need to use the scope parameter to define a result. For example:
JavaScript
1/**
2 * Effect code for global action globalActionA
3 * @param { import("gadget-server").GlobalActionAGlobalActionContext } context - Everything for running this effect, like the api client, current record, params, etc
Including the result in a GraphQL request will return the result added to scope:
GraphQL
1mutation{
2globalActionA{
3success
4errors{
5message
6}
7result
8}
9}
And the response will include the result:
json
1{
2"data":{
3"globalActionA":{
4"success":true,
5"errors":null,
6"result":{
7"student":{
8"firstName":"Carl",
9"lastName":"Weathers"
10}
11}
12}
13}
14}
Effect examples
Here's an example Effect that updates a user's fullName field to be a combination of the firstName field and lastName field:
JavaScript
module.exports=async({ api, record, session })=>{
await api.internal.user.update(record.id,{
fullName: record.firstName+" "+ record.lastName,
});
};
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 that 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:
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 that uses the Akismet spam detection API to ensure a comment isn't spammy during the Create Action:
JavaScript
1const{AkismetClient}=require("akismet-api");
2
3// instantiate the client for the remote service
4const client =newAkismetClient({
5key:"my-api-key",
6blog:"https://myblog.com",
7});
8
9module.exports=async({ api, record })=>{
10const isSpam =await client.checkSpam({
11content: record.body,
12email: record.email,
13});
14if(isSpam){
15// throwing this error will roll back the transaction, aborting any in progress creates or updates
16thrownewError("Can't save the comment because it seems spammy");
17}
18};
We model spam detection here as an Effect 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 the 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:
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:
and then require it in widget/create/run-A.js and widget/update/run-A.js:
widget/create/run-A.js and widget/update/run-A.js
JavaScript
const{ leftPad }=require("../utils");
module.exports=async({ api, scope })=>{
// ... effect code that uses `leftPad`
};
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 and Effect code so that when Gadget generates the boilerplate, it will be close to the other code that touches the same model.
Sharing Code Effects between models
If you want to share a code file between Actions on different models and need to customize the behavior of the code file based on the model you're working with, you can do so by discriminating on the __typename property of your record.
In JavaScript Code Effects, you can use jsdocs to @typedef both context types that use this Code Effect and define a union type for the context param.
The editor will typecheck your record, and the fields available in each block of the if statement will be narrowed to fields on that model. The __typename of each record is the camelized apiIdentifier of its 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 as 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)",
then we can require it in a validation or Effect file with require:
some effect file
JavaScript
const _ =require("lodash");
module.exports=async({ api, scope })=>{
// ... effect code that uses `_.defaults` or `_.groupBy` or any other Lodash helper function.
};
Gadget also requires that your app depends 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.
Environment variables
Environment variables are variables whose values are set outside of the code that's leveraging them. Environment variables are often used to store sensitive information that the developer does not want to expose in code, say API credentials.
Gadget offers built-in support for storing and accessing environment variables.
Managing environment variables
To store environment variables in Gadget, click on Environment Variables found under Settings in the Config section of the navigation menu. To add a new environment variable, click on + Add Variable and complete the form as displayed below.
If the variable in question is sensitive and you wish to hide its value from being displayed in the Gadget editor, you can simply click on the lock icon next to the value input. You can also edit environment variables by overriding the value in the input field. Finally, environment variables can be deleted, simply by clicking Remove.
You can add environment variable values for both Development and Production environments. When you change an environment variable's Production value, you need to re-deploy so that the updated value is used by the Production instance of your Gadget app. Changes to environment variables that have not been deployed are marked as Pending Deploy to let you know that these values are not yet available in your Gadget app's production environment. After deploying, the Pending Deploy status changes to Production.
Using environment variables in code
Gadget allows you to set environment variables for use within Code Effects and all JavaScript files. These values are accessible using two different methods:
First, your app's environment variables are available within the config property of an Action's Effect context. Here's an example use case within a Global Action:
4 * @param {ExampleGlobalActionContext} context - Everything for running this effect, like the api client, current record, params, etc
5 */
6module.exports=async({ logger, config })=>{
7 logger.info({value: config.FOO},"logging an environment value");
8};
Second, to allow access outside of Action contexts, such as in shared library code, all environment variables are also available within the process' environment, which you can access with process.env:
Gadget sets up each application with the NODE_ENV environment variable set to development for the Development environment and production for the Production environment. This is a common convention for node.js applications that instructs node modules to run a potentially slower but more explanatory version of code in development and a higher performance but maybe less debuggable version in production.
If you'd like to opt out of this behavior and run with a different value of NODE_ENV across your environments, you can adjust the value of the NODE_ENV environment variable on your Environment Variables page.
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 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.
Module system
The node.js runtime powering your Gadget application supports code written in the CommonJS style (with require statements) as well as the ECMAScript Modules style (with import statements) out of the box. Gadget compiles your code down to CommonJS to execute on node, so if you use import syntax or other newer ESM features, they will be transpiled to code that node supports out of the box, as opposed to running as they are.
For example, if you want to use ESM to write Effect functions for a Gadget model, you can use import and export: