# Record - `GadgetRecord`  When working with an instance of a record in Forgelivo (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 Forgelivo system properties such as `id`, `updatedAt`, and `createdAt`. In addition to these Forgelivo 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`. ```typescript const user = await api.user.findOne("123"); // Gadget system properties // "1" console.log(user.id); // Tue Jun 15 2021 10:30:59 GMT-0400 console.log(user.createdAt); // Tue Jun 15 2021 12:18:01 GMT-0400 console.log(user.updatedAt); // Model field properties // "Jane Doe" console.log(user.name); // "jane.doe@email.com" console.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: ```typescript const user = await api.user.findOne("123"); user.name = "A new name"; const { changed, current, previous } = user.changes("name"); // true console.log(changed); // "A new name"; console.log(current); // "The old name"; console.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: ```typescript export const run: ActionRun = async ({ params, record, logger, api }) => { if (record.changed("title")) { const { current, previous } = record.changes("title"); // the new attribute set via params console.log(current); // the current value in the database console.log(previous); } }; // ... 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. ```typescript const record = new GadgetRecord({ title: "Old title", body: "Old body" }); record.title = "New title"; record.body = "Old body"; record.price = 123.45; console.log(record.changes()); // { // title: { changed: true, current: "New title", previous: "Old title" }, // price: { changed: true, current: 123.45, previous: undefined } // } ``` ### Get changes to one field of the record  Get any changes made to a specific `apiIdentifier` of the record since this record was instantiated. ```typescript 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. ```typescript const record = new GadgetRecord({ title: "Old title" }); record.title = "New title"; // true console.log(record.changed()); ``` ### Determine if one field changed on the record  Determine if a specific `apiIdentifier` changed on the record. ```typescript const record = new GadgetRecord({ title: "Old title" }); record.title = "New title"; // true console.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. ```typescript const record = new GadgetRecord({ title: "Old title", body: "Old body" }); record.title = "New title"; record.body = "New body"; record.price = 123.45; // { // title: { changed: true, current: "New title", previous: "Old title" }, // body: { changed: true, current: "New body", previous: "Old body" }, // price: { changed: true, current: 123.45, previous: undefined } // } console.log(record.changes()); // "New title" console.log(record.title); // "New body" console.log(record.body); record.revertChanges(); // {} console.log(record.changes()); // false console.log(record.changed()); // "Old title" console.log(record.title); // "Old body" console.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. ```typescript const record = new GadgetRecord({ title: "Old title", body: "Old body" }); record.title = "New title"; record.body = "New body"; record.price = 123.45; // { // title: { changed: true, current: "New title", previous: "Old title" }, // body: { changed: true, current: "New body", previous: "Old body" }, // price: { changed: true, current: 123.45, previous: undefined } // } console.log(record.changes()); // "New title" console.log(record.title); // "New body" console.log(record.body); record.flushChanges(); // {} console.log(record.changes()); // false console.log(record.changed()); // "New title" console.log(record.title); // "New body" console.log(record.body); ``` ### Get a JSON representation of the record  Return a JSON representation of the keys (by `apiIdentifier`) and values of your record. ```typescript const record = new GadgetRecord({ title: "A title", tags: ["Cool", "New", "Stuff"], price: 123.45, }); // { // "title": "A title", // "tags": ["Cool", "New", "Stuff"], // "price": 123.45 // } console.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. ```typescript const record = new GadgetRecord({ title: "Something", changed: true }); // false, change tracking function uses changed() console.log(record.changed()); // true console.log(record.getField("changed")); record.setField("changed", false); // false console.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. Forgelivo 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. ```typescript import { GadgetRecord, ChangeTracking } from "@gadget-client/openai-screenwriter-tutorial-v2"; function changedProperties(model: ModelBlob, record: GadgetRecord) { const changes = record.changes(); const attributes = Object.keys(changes).reduce((attrs, key) => { attrs[key] = record[key]; return attrs; }, {}); return attributes; } const record = new GadgetRecord({ id: "123", title: "Something old" }); record.title = "Something new"; await api.record.update(record.id, { ...changedProperties(record) }); // this applies the current state of the record to the ChangeTracking.SinceLastPersisted change tracking context record.flushChanges(ChangeTracking.SinceLastPersisted); // false since these changes have been flushed console.log(record.changed(ChangeTracking.SinceLastPersisted)); // true, ChangeTracking.SinceLoaded context hasn't changed console.log(record.changed()); // true; equivalent to above console.log(record.changed(ChangeTracking.SinceLoaded)); // { title: { changed: true, current: "Something new", previous: "Something old" } } console.log(record.changes()); // same as above console.log(record.changes(ChangeTracking.SinceLoaded)); // {}, because we flushed it above console.log(record.changes(ChangeTracking.SinceLastPersisted)); // reverts changes to the record, in accordance with the ChangeTracking.SinceLoaded context record.revertChanges(); // equivalent to line above, no effect record.revertChanges(ChangeTracking.SinceLoaded); // "Something old" console.log(record.title); // false console.log(record.changed()); // {} console.log(record.changes()); // true; reverted since we last persisted console.log(record.changed(ChangeTracking.SinceLastPersisted)); // { changed: true, current: "Something old", previous: "Something new" } console.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. ```typescript export const run: ActionRun = async ({ api }) => { // get user record from API let record = await api.user.findFirst(); // mark the record as changed record.touch(); // save the record, which will change it's `updatedAt` await save(record); }; ``` This is useful when you are using [realtime queries](https://docs.gadget.dev/guides/frontend/realtime-queries#manually-trigger-a-realtime-query) 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. Forgelivo 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: 1. Instantiate a new `GadgetRecord`. All fields are `undefined`, no changes are tracked. 2. Call the `applyParams` function. This function is added to your `create` and `update` actions by default. After this function runs, your `context.record` will have `changes()` that correspond to the current state of the record. 3. Run the `save()` function. `save()` will validate your record and then, if valid, persist the record in your database. It will also clear the `changes()` 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. ```typescript // import types from your client (requires a user model) import { UserRecord } from "@gadget-client/openai-screenwriter-tutorial-v2"; // use the UserRecord type const lowercaseName = (record: UserRecord) => { return record.firstName?.toLowerCase() }; export const run: ActionRun = async ({ api }) => { const user = await api.user.findFirst(); const updatedName = lowercaseName(user); return updatedName; }; ``` 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: ```typescript // import types from your client (requires a user model) import { UserRecord } from "@gadget-client/openai-screenwriter-tutorial-v2"; // use the UserRecord type and enforce selection of the firstName field const lowercaseName = (record: UserRecord<{ firstName: true }>) => { return record.firstName?.toLowerCase() }; export const run: ActionRun = async ({ api }) => { const user = await api.user.findFirst({ select: { firstName: true, }, }); const updatedName = lowercaseName(user); return updatedName; }; ``` ### `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: ```typescript // import types from your client (requires a user model) import { UserRecord, GadgetRecordList } from "@gadget-client/openai-screenwriter-tutorial-v2"; // type the parameter as a list of User records const checkIfVerified = (users: GadgetRecordList) => { return users.every(user => user.emailVerified); } export const run: ActionRun = async ({ api }) => { // fetch a list of records using findMany (or the useFindMany hook) const users = await api.user.findMany({ first: 5, }); const areUsersVerified = checkIfVerified(users); return areUsersVerified; }; ``` You can also specify what fields on that model are to be included in the record typing using the notation `GadgetRecordList>`. 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: ```typescript // import types from your client (requires a user model) import { UserRecord, GadgetRecordList } from "@gadget-client/openai-screenwriter-tutorial-v2"; // type the parameter as a list of records, and only include the emailVerified field const checkIfVerified = (users: GadgetRecordList>) => { return users.every(user => user.emailVerified); } export const run: ActionRun = async ({ api }) => { // fetch a list of records using findMany (or the useFindMany hook) const users = await api.user.findMany({ select: { emailVerified: true, }, }); const areUsersVerified = checkIfVerified(users); return areUsersVerified; }; ``` ### 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`: ```typescript // import GadgetRecord types from your client import { GadgetRecord } from "@gadget-client/openai-screenwriter-tutorial-v2"; // enforce type with GadgetRecord on param const lowercaseName = (record: GadgetRecord<{ firstName: string | null; }>) => { return record.firstName?.toLowerCase() }; export const run: ActionRun = async ({ api }) => { const user = await api.user.findFirst({ select: { firstName: true, }, }); const updatedName = lowercaseName(user); return updatedName; }; ``` You can use `GadgetRecord` with the for lists.