Record - GadgetRecord
When working with an instance of a record in Gadget (either results returned from the client package or the context.record
in your code effects), you are working with a GadgetRecord
. GadgetRecord
is a wrapper class around the properties of your object that provides additional helper functionality such as change tracking.
GadgetRecord
properties
Every GadgetRecord
shares a common set of Gadget system properties such as id
, updatedAt
, and createdAt
. In addition to these Gadget system properties, any model fields you've added to your model will also be included in the GadgetRecord
. These properties can be get and set via their apiIdentifier
.
1const user = await api.user.findOne("123");23// Gadget system properties4// "1"5console.log(user.id);6// Tue Jun 15 2021 10:30:59 GMT-04007console.log(user.createdAt);8// Tue Jun 15 2021 12:18:01 GMT-04009console.log(user.updatedAt);1011// Model field properties12// "Jane Doe"13console.log(user.name);14// "[email protected]"15console.log(user.email);
1const user = await api.user.findOne("123");23// Gadget system properties4// "1"5console.log(user.id);6// Tue Jun 15 2021 10:30:59 GMT-04007console.log(user.createdAt);8// Tue Jun 15 2021 12:18:01 GMT-04009console.log(user.updatedAt);1011// Model field properties12// "Jane Doe"13console.log(user.name);14// "[email protected]"15console.log(user.email);
Change tracking (dirty tracking)
GadgetRecord
instances track changes made to their local state, and provide a change tracking API for determining what properties have changed (locally) on a record.
You can use this change tracking in your own applications via the client package in the following manner:
1const user = await api.user.findOne("123");23user.name = "A new name";45const { changed, current, previous } = user.changes("name");67// true8console.log(changed);9// "A new name";10console.log(current);11// "The old name";12console.log(previous);
1const user = await api.user.findOne("123");23user.name = "A new name";45const { changed, current, previous } = user.changes("name");67// true8console.log(changed);9// "A new name";10console.log(current);11// "The old name";12console.log(previous);
By working with this single GadgetRecord
instance user
in your frontend, you could use the change tracking API to determine if there is anything that needs to be persisted via one of your actions.
Additionally, you are also working with GadgetRecord
instances when using the context.record
in your actions and validations.
If you wanted to determine whether or not a change was made to the record in a run effect of an update
action, you could do the following:
1export const run: ActionRun = async ({ params, record, logger, api }) => {2 if (record.changed("title")) {3 const { current, previous } = record.changes("title");45 // the new attribute set via params6 console.log(current);7 // the current value in the database8 console.log(previous);9 }10};11// ... rest of your code
1export const run: ActionRun = async ({ params, record, logger, api }) => {2 if (record.changed("title")) {3 const { current, previous } = record.changes("title");45 // the new attribute set via params6 console.log(current);7 // the current value in the database8 console.log(previous);9 }10};11// ... rest of your code
GadgetRecord
API
The GadgetRecord
API extends your records with helper functions to make working with records easier.
Get all changes to the record
Get a list of all changes, keyed by field apiIdentifier
since this record was instantiated.
1const record = new GadgetRecord({ title: "Old title", body: "Old body" });2record.title = "New title";3record.body = "Old body";4record.price = 123.45;5console.log(record.changes());6// {7// title: { changed: true, current: "New title", previous: "Old title" },8// price: { changed: true, current: 123.45, previous: undefined }9// }
1const record = new GadgetRecord({ title: "Old title", body: "Old body" });2record.title = "New title";3record.body = "Old body";4record.price = 123.45;5console.log(record.changes());6// {7// title: { changed: true, current: "New title", previous: "Old title" },8// price: { changed: true, current: 123.45, previous: undefined }9// }
Get changes to one field of the record
Get any changes made to a specific apiIdentifier
of the record since this record was instantiated.
const record = new GadgetRecord({ title: "Old title" });record.title = "New title";console.log(record.changes("title"));// { changed: true, current: "New title", previous: "Old title" }
const record = new GadgetRecord({ title: "Old title" });record.title = "New title";console.log(record.changes("title"));// { changed: true, current: "New title", previous: "Old title" }
Determine whether or not any fields changed on the record
Determine if any changes have been made to any keys on the record.
const record = new GadgetRecord({ title: "Old title" });record.title = "New title";// trueconsole.log(record.changed());
const record = new GadgetRecord({ title: "Old title" });record.title = "New title";// trueconsole.log(record.changed());
Determine if one field changed on the record
Determine if a specific apiIdentifier
changed on the record.
const record = new GadgetRecord({ title: "Old title" });record.title = "New title";// trueconsole.log(record.changed("title"));
const record = new GadgetRecord({ title: "Old title" });record.title = "New title";// trueconsole.log(record.changed("title"));
Revert all changes to the record
Resets the record state to the state it was instantiated with. All changes are reverted.
1const record = new GadgetRecord({ title: "Old title", body: "Old body" });2record.title = "New title";3record.body = "New body";4record.price = 123.45;5// {6// title: { changed: true, current: "New title", previous: "Old title" },7// body: { changed: true, current: "New body", previous: "Old body" },8// price: { changed: true, current: 123.45, previous: undefined }9// }10console.log(record.changes());1112// "New title"13console.log(record.title);14// "New body"15console.log(record.body);1617record.revertChanges();18// {}19console.log(record.changes());20// false21console.log(record.changed());2223// "Old title"24console.log(record.title);25// "Old body"26console.log(record.body);
1const record = new GadgetRecord({ title: "Old title", body: "Old body" });2record.title = "New title";3record.body = "New body";4record.price = 123.45;5// {6// title: { changed: true, current: "New title", previous: "Old title" },7// body: { changed: true, current: "New body", previous: "Old body" },8// price: { changed: true, current: 123.45, previous: undefined }9// }10console.log(record.changes());1112// "New title"13console.log(record.title);14// "New body"15console.log(record.body);1617record.revertChanges();18// {}19console.log(record.changes());20// false21console.log(record.changed());2223// "Old title"24console.log(record.title);25// "Old body"26console.log(record.body);
Flush changes to the record
Flushes all changes to the record and resets the state of change tracking to the current state of the record.
1const record = new GadgetRecord({ title: "Old title", body: "Old body" });2record.title = "New title";3record.body = "New body";4record.price = 123.45;5// {6// title: { changed: true, current: "New title", previous: "Old title" },7// body: { changed: true, current: "New body", previous: "Old body" },8// price: { changed: true, current: 123.45, previous: undefined }9// }10console.log(record.changes());1112// "New title"13console.log(record.title);14// "New body"15console.log(record.body);1617record.flushChanges();18// {}19console.log(record.changes());20// false21console.log(record.changed());2223// "New title"24console.log(record.title);25// "New body"26console.log(record.body);
1const record = new GadgetRecord({ title: "Old title", body: "Old body" });2record.title = "New title";3record.body = "New body";4record.price = 123.45;5// {6// title: { changed: true, current: "New title", previous: "Old title" },7// body: { changed: true, current: "New body", previous: "Old body" },8// price: { changed: true, current: 123.45, previous: undefined }9// }10console.log(record.changes());1112// "New title"13console.log(record.title);14// "New body"15console.log(record.body);1617record.flushChanges();18// {}19console.log(record.changes());20// false21console.log(record.changed());2223// "New title"24console.log(record.title);25// "New body"26console.log(record.body);
Get a JSON representation of the record
Return a JSON representation of the keys (by apiIdentifier
) and values of your record.
1const record = new GadgetRecord({2 title: "A title",3 tags: ["Cool", "New", "Stuff"],4 price: 123.45,5});6// {7// "title": "A title",8// "tags": ["Cool", "New", "Stuff"],9// "price": 123.4510// }11console.log(record.toJSON());
1const record = new GadgetRecord({2 title: "A title",3 tags: ["Cool", "New", "Stuff"],4 price: 123.45,5});6// {7// "title": "A title",8// "tags": ["Cool", "New", "Stuff"],9// "price": 123.4510// }11console.log(record.toJSON());
Get and set properties that share the same name as GadgetRecord functions
If the apiIdentifier
of your model field has the same name as one of the GadgetRecord
functions described above, you will need to work with it via the getField
and setField
functions.
1const record = new GadgetRecord({ title: "Something", changed: true });23// false, change tracking function uses changed()4console.log(record.changed());5// true6console.log(record.getField("changed"));7record.setField("changed", false);8// false9console.log(record.getField("changed"));
1const record = new GadgetRecord({ title: "Something", changed: true });23// false, change tracking function uses changed()4console.log(record.changed());5// true6console.log(record.getField("changed"));7record.setField("changed", false);8// false9console.log(record.getField("changed"));
ChangeTracking contexts
GadgetRecord
change tracking allows you to track changes on the record in different contexts. By default, all functions use the ChangeTracking.SinceLoaded
context. Gadget also uses the ChangeTracking.SinceLastPersisted
context to differentiate between changes that have already been persisted by the Create record
and Update record
effects, and changes that have been made to the record since the start of action execution.
For developers to worry less about the exact order in which their effects are run, the change tracking API assumes the ChangeTracking.SinceLoaded
context by default. You may also inquire about changes since ChangeTracking.SinceLastPersisted
by adding it as an option to your change tracking function calls.
1import {2 GadgetRecord,3 ChangeTracking,4} from "@gadget-client/openai-screenwriter-tutorial-v2";56function changedProperties(model: ModelBlob, record: GadgetRecord<BaseRecord>) {7 const changes = record.changes();8 const attributes = Object.keys(changes).reduce((attrs, key) => {9 attrs[key] = record[key];10 return attrs;11 }, {});12 return attributes;13}1415const record = new GadgetRecord({ id: "123", title: "Something old" });1617record.title = "Something new";18await api.record.update(record.id, { ...changedProperties(record) });19// this applies the current state of the record to the ChangeTracking.SinceLastPersisted change tracking context20record.flushChanges(ChangeTracking.SinceLastPersisted);2122// false since these changes have been flushed23console.log(record.changed(ChangeTracking.SinceLastPersisted));2425// true, ChangeTracking.SinceLoaded context hasn't changed26console.log(record.changed());27// true; equivalent to above28console.log(record.changed(ChangeTracking.SinceLoaded));2930// { title: { changed: true, current: "Something new", previous: "Something old" } }31console.log(record.changes());32// same as above33console.log(record.changes(ChangeTracking.SinceLoaded));34// {}, because we flushed it above35console.log(record.changes(ChangeTracking.SinceLastPersisted));3637// reverts changes to the record, in accordance with the ChangeTracking.SinceLoaded context38record.revertChanges();39// equivalent to line above, no effect40record.revertChanges(ChangeTracking.SinceLoaded);4142// "Something old"43console.log(record.title);44// false45console.log(record.changed());46// {}47console.log(record.changes());4849// true; reverted since we last persisted50console.log(record.changed(ChangeTracking.SinceLastPersisted));51// { changed: true, current: "Something old", previous: "Something new" }52console.log(record.changes("title", ChangeTracking.SinceLastPersisted));
GadgetRecord.touch()
Each GadgetRecord
also has a touch
function that can be used to mark the record as changed.
When the record is saved, it's updatedAt
field will be updated, even if nothing else about the record has changed.
1export const run: ActionRun = async ({ api }) => {2 // get user record from API3 let record = await api.user.findFirst();45 // mark the record as changed6 record.touch();78 // save the record, which will change it's `updatedAt`9 await save(record);10};
1export const run: ActionRun = async ({ api }) => {2 // get user record from API3 let record = await api.user.findFirst();45 // mark the record as changed6 record.touch();78 // save the record, which will change it's `updatedAt`9 await save(record);10};
This is useful when you are using realtime queries and want to trigger a re-fetch of the record without changing any of the other fields.
Working with GadgetRecord in actions
When writing actions and model field validations you have access to a shared GadgetRecord
in context.
Most commonly, you will be working with the context.record
inside actions. Gadget will instantiate an instance of a GadgetRecord
when it starts executing your action. This same instance will be shared between all validations and actions that run during that action.
The typical GadgetRecord
lifecycle is as follows:
- Instantiate a new
GadgetRecord
. All fields areundefined
, no changes are tracked. - Call the
applyParams
function. This function is added to yourcreate
andupdate
actions by default. After this function runs, yourcontext.record
will havechanges()
that correspond to the current state of the record. - Run the
save()
function.save()
will validate your record and then, if valid, persist the record in your database. It will also clear thechanges()
state of the record after persisting it.
The GadgetRecord
type
Every model will have a unique Record
type that can be imported from your @gadget-client
package.
1// import types from your client (requires a user model)2import { UserRecord } from "@gadget-client/openai-screenwriter-tutorial-v2";34// use the UserRecord type5const lowercaseName = (record: UserRecord) => {6 return record.firstName?.toLowerCase();7};89export const run: ActionRun = async ({ api }) => {10 const user = await api.user.findFirst();1112 const updatedName = lowercaseName(user);13 return updatedName;14};
You can also specify what fields on that model are to be included in the record typing using the notation ModelARecord<{fieldA: true; fieldB: true}>
. This is useful when querying for model records while using the select
option to specify fields and allows you to enforce the selection of certain fields.
For example, you could have a function in a global action that converts a firstName
field on a user
model to lowercase:
1// import types from your client (requires a user model)2import { UserRecord } from "@gadget-client/openai-screenwriter-tutorial-v2";34// use the UserRecord type and enforce selection of the firstName field5const lowercaseName = (record: UserRecord<{ firstName: true }>) => {6 return record.firstName?.toLowerCase();7};89export const run: ActionRun = async ({ api }) => {10 const user = await api.user.findFirst({11 select: {12 firstName: true,13 },14 });1516 const updatedName = lowercaseName(user);17 return updatedName;18};
GadgetRecordList
You can use GadgetRecordList
when typing lists of records, for example, the results of a findMany
request.
For example, if you wanted to pass a list of user
records to a function to check and see if their emails have been verified:
1// import types from your client (requires a user model)2import {3 UserRecord,4 GadgetRecordList,5} from "@gadget-client/openai-screenwriter-tutorial-v2";67// type the parameter as a list of User records8const checkIfVerified = (users: GadgetRecordList<UserRecord>) => {9 return users.every((user) => user.emailVerified);10};1112export const run: ActionRun = async ({ api }) => {13 // fetch a list of records using findMany (or the useFindMany hook)14 const users = await api.user.findMany({15 first: 5,16 });1718 const areUsersVerified = checkIfVerified(users);19 return areUsersVerified;20};
You can also specify what fields on that model are to be included in the record typing using the notation GadgetRecordList<ModelARecord<{fieldA: true; fieldB: true}>>
. This is useful when querying for model records while using the select
option to specify fields and allows you to enforce the selection of certain fields.
For example, you can only enforce the selection of the emailVerified
field in your typing for your user
records:
1// import types from your client (requires a user model)2import {3 UserRecord,4 GadgetRecordList,5} from "@gadget-client/openai-screenwriter-tutorial-v2";67// type the parameter as a list of records, and only include the emailVerified field8const checkIfVerified = (9 users: GadgetRecordList<UserRecord<{ emailVerified: true }>>10) => {11 return users.every((user) => user.emailVerified);12};1314export const run: ActionRun = async ({ api }) => {15 // fetch a list of records using findMany (or the useFindMany hook)16 const users = await api.user.findMany({17 select: {18 emailVerified: true,19 },20 });2122 const areUsersVerified = checkIfVerified(users);23 return areUsersVerified;24};
The generic GadgetRecord
type
You can import a generic GadgetRecord
type into your typescript files by importing it from your API client.
This GadgetRecord
type accepts a Shape
type describing the field typing for the record used in, for example, the function. Shape
requires the name of the field and the type to be specified manually.
For example, GadgetRecord<{ firstName: string | null; }>
could be used to type a record with a string field called firstName
:
1// import GadgetRecord types from your client2import { GadgetRecord } from "@gadget-client/openai-screenwriter-tutorial-v2";34// enforce type with GadgetRecord on param5const lowercaseName = (record: GadgetRecord<{ firstName: string | null }>) => {6 return record.firstName?.toLowerCase();7};89export const run: ActionRun = async ({ api }) => {10 const user = await api.user.findFirst({11 select: {12 firstName: true,13 },14 });1516 const updatedName = lowercaseName(user);17 return updatedName;18};
You can use GadgetRecord
with the GadgetListRecord
type for lists.