# GASsma Documentation (Full Content) A machine-readable version of the entire GASsma documentation bundled into a single file. Index: https://gassma.io/en/llms.txt --- # What is GASsma (https://gassma.io/en/docs/intro) Overview and motivation: why plain GAS spreadsheet code is hard to maintain (getRange counting mistakes, formula injection) and how GASsma solves it GASsma is a library that lets you manipulate Google Spreadsheet tables like "Prisma", one of the popular ORM libraries for Node.js. By operating spreadsheets like an ORM, it aims to make writing GoogleAppsScript (GAS): - More manageable - Less error-prone - More secure ## Getting Started Create a new spreadsheet and enter the following data. (Name the sheet "sheet1") ![Example Sheet](./リファレンス/img/exampleSheet.png) Then open `Extensions` > `Apps Script` to launch the GAS editor. Once open, follow [this page](./installation) to install GASsma. Now let's consider how to extract and format data from the sheet we just created: 1. Extract rows where age is 25 or older 2. Sort the extracted rows in ascending order by name 3. Convert to an associative array keyed by column names Let's write this with GASsma. Write the following code and run `myFunction`: ```ts const gassma = new Gassma.GassmaClient(); function myFunction() { const result = gassma.sheet1.findMany({ where: { age: { gte: 25, }, }, orderBy: { name: "asc", }, }); console.log(result); } ``` That's it. **Simply create an instance and call the findMany method to extract data.** The library automatically reads column names for you. ## Why is GASsma Needed There are mainly 3 difficulties when manipulating spreadsheets with existing GAS: ### 1. Complexity of Code Management Writing the above example in standard GAS would look like this: ```ts function myFunction() { const sheet = SpreadsheetApp.getActiveSpreadsheet(); const hogeSheet = sheet.getSheetByName("sheet1"); const rowLength = hogeSheet.getLastRow() - 1; if (rowLength === 0) { console.log([]); return; } // Extract data from specified range const data = hogeSheet.getRange(2, 1, rowLength, 4).getValues(); // Filter rows where age is 25 or older const gte25Data = data.filter((row) => row[1] >= 25); // Sort const gte25DataSorted = gte25Data.sort((a, b) => (a[0] >= b[0] ? 1 : -1)); // Convert to associative array const gte25DataSortedDict = gte25DataSorted.map((row) => { return { name: row[0], age: row[1], pref: row[2], postNumber: row[3], }; }); console.log(gte25DataSortedDict); } ``` However, this is still relatively short code with simple logic. If additional conditions like: **"If names are the same, sort by age in ascending order"** keep increasing, or if there are complex requirements like: **"Find the average age of people aged 25-60 whose prefecture is Tokyo"** the code becomes complex and difficult to manage. ### 2. Error-Prone Nature of Code GAS spreadsheet operations are inherently error-prone. When extracting data from a specified range in a spreadsheet, you use `getRange()`. `getRange()` is a method that extracts cells from a specified range by taking row and column numbers as arguments. In other words, every time you use `getRange()`, you need to check the row and column numbers of cells on the spreadsheet. This means there's a risk of miscounting. The more you use `getRange()` in your code, the higher this risk becomes. ### 3. Security Awareness Required Consider inserting data submitted from a Google Form into a spreadsheet. For example, the following code has a problem. Can you spot it? ```ts function myFunction(e) { // Get values submitted from Google Form const values = e.namedValues; const newValues = [values["名前"], values["年齢"], values["都道府県"], values["郵便番号"]]; const sheet = SpreadsheetApp.getActiveSpreadsheet(); const hogeSheet = sheet.getSheetByName("シート名"); const newRow = hogeSheet.getLastRow() + 1; // Insert into sheet hogeSheet.getRange(newRow, 1, 1, 4).setValues([newValues]); } ``` The answer is: if a malicious user enters a spreadsheet formula like `=C1` in the form response field, arbitrary unauthorized operations can be executed. (Formula Injection)
The library that solves these problems is **"GASsma"**. Note that formula injection is prevented by automatic escaping at write time. If you intentionally want to write a formula, see [raw](/docs/reference/raw). # Installation (https://gassma.io/en/docs/installation) How to install GASsma in the GAS script editor or via npm for local development First, after opening Apps Script, click the "+" button in the Libraries section. ![Click the + button](./img/plusButton.png) A dialog like the one below will appear. Enter the following in the "Script ID" field and press the search button. ``` 1ZVuWMUYs4hVKDCcP3nVw74AY48VqLm50wRceKIQLFKL0wf4Hyou-FIBH ``` ![Enter the ID](./img/inputId.png) The following screen will appear. Press the "Add" button. ![Press the Add button](./img/addLibrary.png) If "Gassma" appears in the Libraries section, you're all set! ![Success](./img/installSuccess.png) ## CLI Tool Installation When developing GoogleAppsScript locally using tools like clasp, you can install GASsma's TypeScript type file auto-generation tool with the following command. For detailed usage, see [here](./reference/type-generation) ```bash npm i gassma ``` # Basic (https://gassma.io/en/docs/reference/basic) Initializing GassmaClient, accessing sheets, constructor options, and the common query options (where, select, omit, orderBy, take, skip) ## Creating an Instance If you have created a GAS on a specific spreadsheet and want to work with that spreadsheet, you can create an instance as follows: ```ts const gassma = new Gassma.GassmaClient(); ``` Alternatively, if you have created a GAS in a location other than a spreadsheet, or if you want to work with a spreadsheet located elsewhere, you can create an instance by passing the target spreadsheet's ID as an argument: ```ts const gassma = new Gassma.GassmaClient("XXXXXXXXXXXXXXXXXXX"); ``` ### Initialization with an Options Object When you need advanced configuration such as relation definitions or global omit, pass an options object: ```ts const gassma = new Gassma.GassmaClient({ id: "XXXXXXXXXXXXXXXXXXX", // Optional relations: { // Relation definitions (see the relation definition reference for details) }, omit: { // Global omit settings (see the global omit reference for details) Users: { password: true }, }, }); ``` | Option | Description | Reference | | --- | --- | --- | | `id` | Spreadsheet ID (uses the active spreadsheet when omitted) | - | | `relations` | Relation definitions | [Relation Definition](/docs/reference/relation/definition) | | `omit` | Global omit settings | [Global omit](/docs/reference/config/global-omit) | | `defaults` | Default values for fields | [defaults](/docs/reference/config/defaults) | | `updatedAt` | Auto-update timestamps | [updatedAt](/docs/reference/config/updated-at) | | `ignore` | Field-level exclusion | [ignore](/docs/reference/config/ignore) | | `ignoreSheets` | Sheet-level exclusion | [ignore](/docs/reference/config/ignore) | | `map` | Field name mapping | [map](/docs/reference/config/map) | | `mapSheets` | Sheet name mapping | [map](/docs/reference/config/map) | | `autoincrement` | Auto-increment | [autoincrement](/docs/reference/config/autoincrement) | | `strictUndefinedChecks` | Turns explicit `undefined` in query inputs into runtime errors | [strictUndefinedChecks / Gassma.skip](/docs/reference/config/strict-undefined-checks) | ## Checking Date Values GASsma runs as a GAS library in a script context separate from the calling script. As a result, checking a `Date` value returned by GASsma with `instanceof Date` evaluates to `false`. Use `Object.prototype.toString` instead: ```ts const user = gassma.Users.findFirst({ where: { id: 1 } }); user.createdAt instanceof Date; // => false (a Date crossing the library boundary cannot be checked with instanceof) Object.prototype.toString.call(user.createdAt) === "[object Date]"; // => true ``` This limitation applies to `instanceof` on **built-in types** such as `Date`. GASsma's exported error classes (e.g., `Gassma.GassmaMissingArgumentError`) are referenced via the `Gassma` namespace (the library's global), so they can be checked with `instanceof`. For details, see the [error list](/docs/reference/errors). # create() (https://gassma.io/en/docs/reference/crud/create/create) Create a single record; supports select/omit/include and nested writes Used to add a new single row to the target sheet. ## Available Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | data | Specifies the data to register | Required | | | select | Display settings for return value columns | Optional | Cannot be used with `omit` / `include` | | omit | Exclusion settings for return value columns | Optional | Cannot be used with `select` | | include | Retrieve related records | Optional | [Details here](/docs/reference/relation/include) | `data` is required. Omitting it throws `GassmaMissingArgumentError` (message: Argument `data` is missing.). `data` values must be scalar values a cell can hold (string, number, boolean, `null`, `Date`). Passing an object other than `Date` — `Map` / `Set` / `RegExp` / a class instance / a wrapper object such as `new String("x")` — throws a `GassmaInvalidValueError`. ```ts gassma.sheet1.create({ data: { name: new Map() } }); // => Invalid value for argument `name`. Expected a scalar value, but received a Map. gassma.sheet1.create({ data: { name: new Point(1, 2) } }); // => Invalid value for argument `name`. Expected a scalar value, but received an object. ``` `Gassma.raw` (see [raw](/docs/reference/raw)) and `fields` (see [fields](/docs/reference/fields)) can be passed as-is when writing. For the other affected values, see the [error list](/docs/reference/errors#values-a-cell-cannot-hold). ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to add the following row to the above example: - name => **Shibata** - age => **23** - pref => **Shimane** - postNumber => **690-8540** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.create const result = gassma.sheet1.create({ data: { name: "Shibata", age: 23, pref: "Shimane", postNumber: "690-8540", }, }); ``` The return value has the following format: ```ts { name: 'Shibata', age: 23, pref: 'Shimane', postNumber: '690-8540' } ``` The data of the created row is returned. Also, if you omit the age as follows, the `age` column of that row will be empty. Passing `undefined` as the value is treated the same as omission: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.create gassma.sheet1.create({ data: { name: "Shibata", pref: "Shimane", postNumber: "690-8540", }, }); ``` The return value has the following format: ```ts { name: 'Shibata', age: null, pref: 'Shimane', postNumber: '690-8540' } ``` ## Nested Write When relation definitions exist, you can describe operations to simultaneously create and associate records in relation targets within `data`. For details, see the [Nested Write reference](/docs/reference/relation/nested-write). # createMany() (https://gassma.io/en/docs/reference/crud/create/createMany) Create multiple records at once and get the created count Used to add multiple rows to the target sheet simultaneously. ## Available Keys | Key | Description | Optional | | --- | --- | --- | | data | Specifies the data to register | Required | `data` is required. Omitting it throws `GassmaMissingArgumentError` (message: Argument `data` is missing.). ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to add the following rows to the above example: - Row 1 - name => **Shibata** - age => **23** - pref => **Shimane** - postNumber => **690-8540** - Row 2 - name => **Suzuhara** - age => **25** - pref => **Tottori** - postNumber => **680-8571** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.createMany const result = gassma.sheet1.createMany({ data: [ { name: "Shibata", age: 23, pref: "Shimane", postNumber: "690-8540", }, { name: "Suzuhara", age: 25, pref: "Tottori", postNumber: "680-8571", }, ], }); ``` The return value has the following format: ```ts { count: 1; } ``` The number of created rows is returned. `createMany` does not support [Nested Write](/docs/reference/relation/nested-write). Use `create` if you need to operate on related records simultaneously. # createManyAndReturn() (https://gassma.io/en/docs/reference/crud/create/createManyAndReturn) Create multiple records and return the created records Used to add multiple rows to the target sheet simultaneously and retrieve all created records as an array. Performs the same write operation as `createMany`, but differs in the return value. ## Available Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | data | Specifies the data to register | Required | | | select | Display settings for return value columns | Optional | Cannot be used with `omit` / `include` | | omit | Exclusion settings for return value columns | Optional | Cannot be used with `select` | | include | Retrieve related records | Optional | [Details here](/docs/reference/relation/include) | `data` is required. Omitting it throws `GassmaMissingArgumentError` (message: Argument `data` is missing.). ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to add the following rows to the above example: - Row 1 - name => **Shibata** - age => **23** - pref => **Shimane** - postNumber => **690-8540** - Row 2 - name => **Suzuhara** - age => **25** - pref => **Tottori** - postNumber => **680-8571** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.createManyAndReturn const result = gassma.sheet1.createManyAndReturn({ data: [ { name: "Shibata", age: 23, pref: "Shimane", postNumber: "690-8540", }, { name: "Suzuhara", age: 25, pref: "Tottori", postNumber: "680-8571", }, ], }); ``` The return value has the following format: ```ts [ { name: "Shibata", age: 23, pref: "Shimane", postNumber: "690-8540" }, { name: "Suzuhara", age: 25, pref: "Tottori", postNumber: "680-8571" }, ]; ``` All created records are returned as an array. ## Differences from createMany | Method | Return Value | | --- | --- | | `createMany` | `{ count: number }` | | `createManyAndReturn` | Array of created records | The behavior also differs when passing an empty array: ```ts // createMany gassma.sheet1.createMany({ data: [] }); // => { count: 0 } // createManyAndReturn gassma.sheet1.createManyAndReturn({ data: [] }); // => [] ``` Fields not specified in data are returned as `null`: ```ts const result = gassma.sheet1.createManyAndReturn({ data: [{ name: "Shibata" }], }); // => [{ name: "Shibata", age: null, pref: null, postNumber: null }] ``` `createManyAndReturn` does not support [Nested Write](/docs/reference/relation/nested-write). Use `create` if you need to operate on related records simultaneously. # findMany() (https://gassma.io/en/docs/reference/crud/read/findMany) Get multiple records with where, orderBy, take/skip, cursor pagination, and distinct Used to retrieve all rows matching specific conditions. ## Available Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | where | Specifies query conditions | Optional | Retrieves all rows if omitted or when `where: {}` is passed | | select | Display settings for columns | Optional | Cannot be used with `omit` / `include`. Supports relation field options | | omit | Exclusion settings for columns | Optional | Cannot be used with `select` | | include | Retrieve related records | Optional | [Details here](/docs/reference/relation/include) | | orderBy | Sort settings | Optional | Array can be omitted when specifying a single column | | take | Limit number of records | Optional | Negative values fetch from the end | | skip | Number of records to skip | Optional | Negative values cause an error | | distinct | Deduplication settings | Optional | Array can be omitted when specifying a single column | | cursor | Cursor position | Optional | Cursor-based pagination | ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to retrieve rows from the above example with the following condition: - pref => **Tokyo** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { pref: "Tokyo", }, }); ``` The return value has the following format: ```ts [ { name: "sato", age: 31, pref: "Tokyo", postNumber: "160-0023" }, { name: "endo", age: 55, pref: "Tokyo", postNumber: "160-0023" }, ]; ``` To specify multiple conditions: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { pref: "Tokyo", 年齢: 31, }, }); ``` ## Operators and Partial Matching Conditional searches using greater than/less than and partial matching are also possible. For example, to retrieve rows with the following conditions: - age => **20 or older** - age => **30 or younger** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, lte: 30, }, }, }); ``` The keys related to conditional searches are as follows: | Key | Description | Example | | --- | --- | --- | | equals | Equal to | equals: 20 | | not | Not equal to | not: 20 | | in | Contained in the specified list | in: [20, 21, 22] | | notIn | Not contained in the specified list | notIn: [23, 24, 25] | | lt | Less than | lt: 30 | | lte | Less than or equal to | lte: 30 | | gt | Greater than | gt: 20 | | gte | Greater than or equal to | gte: 20 | | contains | Whether the target data contains the specified string | contains: "AB" | | startsWith | Whether the target data starts with the specified string | startsWith: "AB" | | endsWith | Whether the target data ends with the specified string | endsWith: "YZ" | | mode | Case sensitivity settings | mode: "insensitive" | In addition to fixed values, you can use the `fields` property to specify a value from another column in the same row. For details, see the [fields reference](/docs/reference/fields). ### mode: "insensitive" By specifying `mode: "insensitive"` with `equals`, `not`, `contains`, `startsWith`, or `endsWith`, you can compare without case sensitivity. ```ts const gassma = new Gassma.GassmaClient(); // Matches "alice", "Alice", "ALICE", etc. const result = gassma.sheet1.findMany({ where: { name: { equals: "alice", mode: "insensitive", }, }, }); ``` It can also be used with `contains`, `startsWith`, and `endsWith`: ```ts // Matches "Hello World", "HELLO WORLD", etc. const result = gassma.sheet1.findMany({ where: { title: { contains: "hello", mode: "insensitive", }, }, }); ``` If `mode` is not specified or set to the default `mode: "default"`, case is distinguished. `where` values cannot be `NaN` / `Infinity` / `-Infinity`, invalid Dates (Invalid Date), arrays (except the arrays of `in` / `notIn`), functions, Symbols, or BigInts. Passing one throws a `GassmaInvalidValueError` (the same applies to `cursor` / `having`). `Gassma.raw` cannot be used in `where` either (see [raw](/docs/reference/raw)). Objects a cell cannot hold are rejected as well. Every object other than `Date` and `fields` (FieldRef) — `Map` / `Set` / `RegExp` / `Error` / class instances / wrapper objects such as `new String("x")` — throws a `GassmaInvalidValueError`. ```ts gassma.sheet1.findMany({ where: { name: new Map() } }); // => Invalid value for argument `name`. Expected a scalar value, but received a Map. gassma.sheet1.findMany({ where: { name: new Point(1, 2) } }); // => Invalid value for argument `name`. Expected a scalar value, but received an object. ``` Conditions whose value is `undefined` are treated as "not specified". For details, see [strictUndefinedChecks / Gassma.skip](/docs/reference/config/strict-undefined-checks). ## AND, OR, NOT Searches with multiple conditions are also possible. ### AND For example, to retrieve rows with the following conditions: - age => **22** - pref => **Ibaraki** Using AND: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { AND: [ { age: 22, }, { pref: "Ibaraki", }, ], }, }); ``` ### OR For example, to retrieve rows with the following condition: - age => **22 or 40** Using OR: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { OR: [ { age: 22, }, { age: 40, }, ], }, }); ``` ### NOT For example, to retrieve rows with the following conditions: - age => **not 22** - age => **not 40** Using NOT: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { NOT: [ { age: 22, }, { age: 40, }, ], }, }); ``` ### Nesting AND, OR, NOT You can nest OR or NOT inside AND, for example. This nesting structure can be infinitely deep as long as the GAS call stack allows. ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { NOT: { AND: [ { name: "akahoshi", }, { age: 22, }, ], }, }, }); ``` ### Empty AND, OR, NOT and Branches with No Conditions An empty `AND` / `NOT` (`[]` or `{}`) is treated as **always true** (matches every row), and an empty `OR: []` is treated as **always false** (matches nothing) — same as Prisma. Branches that generate no conditions at all (`{}`, or objects whose values are only empty objects / `undefined`) are removed from the `AND` / `OR` / `NOT` arrays. If the `OR` array becomes empty as a result, no rows match. ```ts gassma.sheet1.findMany({ where: { NOT: {} } }); // => every row gassma.sheet1.findMany({ where: { AND: [] } }); // => every row gassma.sheet1.findMany({ where: { OR: [] } }); // => [] gassma.sheet1.findMany({ where: { OR: [{ age: {} }] } }); // => [] (the condition-less branch is removed, leaving an empty OR) gassma.sheet1.findMany({ where: { OR: [{}, { name: "akahoshi" }] } }); // => only the rows where name is "akahoshi" gassma.sheet1.findMany({ where: { NOT: [{ age: {} }] } }); // => every row gassma.sheet1.findMany({ where: { AND: [{ OR: [] }] } }); // => every row ``` `OR` only accepts an array. Passing a non-array such as `OR: {}` throws a `GassmaInvalidValueError`. ### Relation Filters in where When relation definitions exist, you can filter using conditions on related records within `where` (`some`, `every`, `none`, `is`, `isNot`). For details, see the [where relation filter reference](/docs/reference/relation/where-relation-filter). ## Handling of null Whether `null` is accepted depends on whether it sits in a value position or a structural position. ### `null` in a value position (valid) Passing `null` as a column value is a legitimate specification and finds rows whose cell is empty. ```ts gassma.sheet1.findMany({ where: { age: null } }); gassma.sheet1.findMany({ where: { age: { equals: null } } }); gassma.sheet1.findMany({ where: { age: { not: null } } }); ``` Passing `null` to `is` / `isNot` on a to-one relation (manyToOne / oneToOne) is valid in the same way (see [where relation filters](/docs/reference/relation/where-relation-filter)). Column values in `having`, and column values in `data` when writing, also accept `null`. ### `null` in a structural position (error) Passing `null` to an argument that expects an object or an array throws a `GassmaInvalidValueError`. ```ts gassma.sheet1.findMany({ where: null }); // => GassmaInvalidValueError: // Invalid value for argument `where`. Expected an object, but received null. gassma.sheet1.findMany({ where: { AND: null } }); // => Invalid value for argument `AND`. Expected an object or an array, but received null. gassma.sheet1.findMany({ where: { name: { contains: null } } }); // => Invalid value for argument `contains`. Expected a string, but received null. ``` This covers top-level arguments (`where` / `orderBy` / `cursor` / `distinct` / `by` / `having` / `data` / `create` / `update`), `AND` / `OR` / `NOT`, to-many relation filters (`some` / `every` / `none`), nested write verbs (`create` / `connect` / `connectOrCreate` / `set` / `disconnect` / `delete` / `update` / `deleteMany` / `updateMany` / `createMany`), and the string and number operators (`contains` / `startsWith` / `endsWith` / `gt` / `gte` / `lt` / `lte` / `increment` / `decrement` / `multiply` / `divide`). Putting `null` in an array element raises the same error. For the `{expected}` wording of each argument, see the [error list](/docs/reference/errors#null-where-an-argument-expects-a-structure). `cursor` is the one exception: **its column values cannot be `null` either**. Because `cursor` identifies a single record, a `null` value throws a `GassmaInvalidValueError` (Invalid value for argument \`id\`. Expected a scalar value, but received null.). Here `{argumentName}` is the column name. A `null` written directly under `select` / `include` / `omit` is still ignored (that field is treated as not specified). ## select You can limit the data returned in the response. For example, to retrieve only `age` and `pref`: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, }, }, select: { name: true, pref: true, }, }); ``` The return value would be: ```ts [ { name: "akahoshi", pref: "Ibaraki" }, { name: "sato", pref: "Tokyo" }, { name: "suzuki", pref: "Osaka" }, { name: "yamamoto", pref: "Aichi" }, { name: "ono", pref: "Shiga" }, { name: "kudo", pref: "Kyoto" }, { name: "kondo", pref: "Tottori" }, { name: "endo", pref: "Tokyo" }, { name: "murakami", pref: "Fukuoka" }, ]; ``` A `select` with no selected fields at all, such as `select: {}`, throws a `GassmaInvalidValueError` (Invalid value for argument `select`. Expected at least one selected field.). The same applies when all keys become empty through `undefined`. ### Relation Options within select When relation definitions exist, you can specify options similar to `include` for relation fields within `select`. Instead of specifying `include` separately, you can control related data retrieval within `select`. ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId" }, }, }, }); const result = gassma.Users.findMany({ select: { id: true, name: true, posts: { select: { id: true, title: true }, where: { published: true }, orderBy: { id: "desc" }, }, _count: true, }, }); ``` The options available for relation fields are the same as [include options](/docs/reference/relation/include) (`select`, `where`, `orderBy`, `include`, `omit`, `take`, `skip`). Deep nesting is also supported: ```ts const result = gassma.Users.findMany({ select: { id: true, posts: { select: { id: true, comments: { select: { id: true, text: true }, }, }, }, }, }); ``` Relation fields can also be specified with `true`, which retrieves all scalar columns of the related model (this works at any depth, just like the top-level `select`): ```ts const result = gassma.Users.findMany({ select: { posts: { select: { title: true, comments: true, // retrieves all scalar columns of comments }, }, }, }); ``` Top-level `select` and `include` cannot be used simultaneously. If you need related data, specify relation options within `select` or use `include` alone. ## orderBy You can sort the retrieved rows. For example, to sort by `age` in ascending order: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, }, }, orderBy: { age: "asc", }, }); ``` The available values are: | Key | Meaning | | --- | --- | | asc | Ascending order | | desc | Descending order | ### Null Value Sort Order Control You can control the position of null values by specifying the `nulls` option in object format: ```ts const gassma = new Gassma.GassmaClient(); // Place null values at the end const result = gassma.sheet1.findMany({ orderBy: { age: { sort: "asc", nulls: "last" }, }, }); // => [20, 22, 31, 40, 55, null, null] ``` | nulls value | Behavior | | --- | --- | | `"first"` | Place null values at the beginning | | `"last"` | Place null values at the end | When `nulls` is not specified, null values are placed at the beginning for `asc` and at the end for `desc`. `NaN` and invalid Dates (Invalid Date) are also treated as "missing values" like null, and are placed in the same position as null (they are also subject to the `nulls` option). You can also specify multiple sort conditions. For example, to: - Sort by `age` in ascending order - If `age` values are the same, sort those rows by `name` in ascending order The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, }, }, orderBy: [{ age: "asc" }, { name: "asc" }], }); ``` *Sort priority follows the order of index numbers (lower index = higher priority). An empty `orderBy` such as `orderBy: {}` is ignored (no sorting is performed). If an entry in the array becomes empty after removing `undefined`, only that entry is ignored and the remaining entries are used for sorting. ### Sorting by Relation Fields When relation definitions exist, you can sort by manyToOne / oneToOne relation target fields: ```ts const gassma = new Gassma.GassmaClient({ relations: { Posts: { author: { type: "manyToOne", to: "Users", field: "authorId", reference: "id", }, }, }, }); // Sort posts by author name in ascending order const result = gassma.Posts.findMany({ orderBy: { author: { name: "asc" } }, }); ``` Records with null FK are placed at the beginning for `asc` and at the end for `desc`. Field sorting is not available for oneToMany / manyToMany relations. `RelationOrderByUnsupportedTypeError` will be thrown. Passing a non-object to a relation name throws a `GassmaInvalidValueError`. ```ts gassma.Posts.findMany({ orderBy: { author: new Date() } }); // => Invalid value for argument `author`. Expected a relation orderBy object. ``` To sort by a field of the related record, pass an object such as `orderBy: { author: { name: "asc" } }`. ### Sorting by _count You can sort by the number of records in oneToMany / manyToMany relations: ```ts // Sort users by number of posts in descending order const result = gassma.Users.findMany({ orderBy: { posts: { _count: "desc" } }, }); ``` It can also be combined with scalar sorting: ```ts // Sort by post count descending → then by name ascending for ties const result = gassma.Users.findMany({ orderBy: [ { posts: { _count: "desc" } }, { name: "asc" }, ], }); ``` `_count` sorting is not available for manyToOne / oneToOne relations. `RelationOrderByCountUnsupportedTypeError` will be thrown. ## take You can specify the number of records to retrieve. Records are taken from the top of the sheet. For example, to get the top 2 rows from matching records: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, }, }, take: 2, }); ``` ### Negative take Values Specifying a negative value for `take` retrieves N records from the end: ```ts // Get the last 2 records matching the condition const result = gassma.sheet1.findMany({ where: { age: { gte: 20 }, }, take: -2, }); ``` When `take` is negative, the direction of `skip` is also reversed. `skip` becomes the number of records to exclude from the end: ```ts // After excluding the last 1 record, get the remaining last 2 records const result = gassma.sheet1.findMany({ take: -2, skip: 1, }); ``` ## skip You can skip specific rows from the retrieved results. For example, to skip the first matching row: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, }, }, skip: 1, }); ``` Specifying a finite negative value for `skip` throws `GassmaSkipNegativeError`. ### Invalid take / skip values Passing `NaN` / `Infinity` / `-Infinity` / `null` to `take` / `skip` throws a `GassmaInvalidValueError`. ```ts gassma.sheet1.findMany({ take: NaN }); // => Invalid value for argument `take`. Expected a finite number, but received NaN. gassma.sheet1.findMany({ skip: null }); // => Invalid value for argument `skip`. Expected a number, but received null. ``` | Value | Behaviour | | --- | --- | | `NaN` / `Infinity` / `-Infinity` | `GassmaInvalidValueError` (`Expected a finite number, but received ...`) | | `null` | `GassmaInvalidValueError` (`Expected a number, but received null.`) | | A finite negative number | `take` reads from the end; `skip` throws `GassmaSkipNegativeError` | | `undefined` | Treated as "not specified" and ignored | `skip: -Infinity` used to throw `GassmaSkipNegativeError`, but the finiteness check now runs first, so it throws `GassmaInvalidValueError`. `GassmaSkipNegativeError` is thrown only for **finite** negative numbers. The same validation applies to `take` / `skip` in `count` / `aggregate` / `groupBy`. The `take` of `findFirst` has [a different restriction](./findFirst#take). ## cursor Enables cursor-based pagination. Specify an object that uniquely identifies a record in `cursor` to use that record as the starting point: ```ts const gassma = new Gassma.GassmaClient(); // Starting from the record with id: 3, retrieve 5 records const result = gassma.sheet1.findMany({ cursor: { id: 3 }, take: 5, }); ``` When `take` is positive, records are retrieved toward the end from the cursor position. When `take` is negative, records from the beginning up to the cursor position are retrieved: ```ts // Starting from id: 3, retrieve records toward the beginning const result = gassma.sheet1.findMany({ cursor: { id: 3 }, take: -5, }); ``` Combined with `skip`, you can skip further from the cursor position: ```ts // Starting from id: 3, skip 1 record and retrieve 5 records const result = gassma.sheet1.findMany({ cursor: { id: 3 }, skip: 1, take: 5, }); ``` If the record specified in cursor is not found, an empty array is returned. A `cursor` with no columns at all, such as `cursor: {}`, throws a `GassmaInvalidValueError` (Invalid value for argument `cursor`. Expected at least one column.). The same applies when all keys become empty through `undefined`. Passing an incomparable value such as `NaN` or an invalid Date (Invalid Date) as a `cursor` value also throws a `GassmaInvalidValueError`. ### Processing Order The execution order when combining `where`, `orderBy`, `cursor`, `distinct`, `skip`, and `take`: 1. `where` - Filter 2. `orderBy` - Sort 3. Reverse the order when `take` is negative 4. `cursor` - Slice at cursor position (inclusive of the cursor itself) 5. `distinct` - Deduplication 6. `skip` - Skip 7. `take` - Limit (when negative, takes the absolute number of records, then restores the order to normal at the end) 8. `select` / `omit` - Field shaping `distinct` is applied **after** `cursor`. Duplicates are removed within the range sliced by the cursor. ## omit You can exclude specific columns from the return value. This is the inverse of `select`. For example, to exclude `postNumber`: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { pref: "Tokyo", }, omit: { postNumber: true, }, }); ``` The return value would be: ```ts [ { name: "sato", age: 31, pref: "Tokyo" }, { name: "endo", age: 55, pref: "Tokyo" }, ]; ``` `select` and `omit` cannot be used simultaneously. Specifying both throws `GassmaFindSelectOmitConflictError`. When [global omit](/docs/reference/config/global-omit) is configured, you can override it with `{ field: false }` in the query's `omit`. For details, see [overriding global omit with query omit](/docs/reference/config/global-omit#overriding-global-omit-with-query-omit). ## distinct Specify column names to remove rows with duplicate values. When duplicates exist, data from the upper row takes priority. For example, to remove `age` duplicates: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, }, }, distinct: ["age"], }); ``` Because `distinct` is applied after `cursor`, duplicates are removed within the range sliced by the cursor (see "Processing Order" above). When `take` is negative, deduplication runs on the reversed order, so the remaining "first occurrence" is the record on the tail side. The final output is restored to normal order. Duplicates are detected using normalized keys. - Dates with the same time are considered the same value even if they are different instances. A Date and an ISO string of the same time are different values. - The number `1` and the string `"1"` are different values. - `NaN` values collapse into one, and invalid Dates (Invalid Date) collapse into one. `NaN`, `null`, and Invalid Date are all distinct from each other. ## include When relation definitions exist, you can retrieve related data together. For details, see the [include reference](/docs/reference/relation/include). # findFirst() (https://gassma.io/en/docs/reference/crud/read/findFirst) Get the first record matching a where filter, or null if none matches Used to retrieve the first row matching specific conditions. ## Available Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | where | Specifies query conditions | Optional | Retrieves all rows if omitted | | select | Display settings for columns | Optional | Cannot be used with `omit` / `include`. Supports relation field options | | omit | Exclusion settings for columns | Optional | Cannot be used with `select` | | include | Retrieve related records | Optional | [Details here](/docs/reference/relation/include) | | orderBy | Sort settings | Optional | Array can be omitted when specifying a single column | | take | Limit number of records | Optional | Only `1` or `-1` can be specified. See below | | skip | Number of records to skip | Optional | Negative values cause an error | | distinct | Deduplication settings | Optional | Array can be omitted when specifying a single column | | cursor | Cursor-based pagination | Optional | See [findMany cursor](/docs/reference/crud/read/findMany#cursor) for details | ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to retrieve a row from the above example with the following condition: - age => **20 or older** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findFirst const result = gassma.sheet1.findFirst({ where: { age: { gte: 20, }, }, }); ``` The return value has the following format: ```ts { name: 'akahoshi', age: 22, pref: 'Ibaraki', postNumber: '310-8555' } ``` ## take For `findFirst`, `take` can only be **`1` or `-1`**. Specifying any other value throws `GassmaFindFirstTakeError`. - `1`: Retrieves the first record in the current order (same behavior as when omitted). - `-1`: Reverses the order and then retrieves the first record, i.e., the record at the end. ```ts // Retrieve the record at the end (highest age) after sorting age ascending const result = gassma.sheet1.findFirst({ orderBy: { age: "asc" }, take: -1, }); ``` Specifying anything other than `1` / `-1` throws `GassmaFindFirstTakeError`. Unlike `findMany`'s `take`, you cannot specify a number of records. `NaN` / `Infinity` / `-Infinity` are also values other than `1` / `-1`, so they throw `GassmaFindFirstTakeError` too (a different error class from `findMany`'s `take`). Only `take: null` throws a `GassmaInvalidValueError` (Invalid value for argument \`take\`. Expected a number, but received null.). ## skip Retrieves the first record after skipping `skip` records from the beginning. If no records remain after skipping, `null` is returned. ```ts // From matching rows, skip the first 2 and retrieve the next 1 const result = gassma.sheet1.findFirst({ where: { age: { gte: 20 } }, skip: 2, }); ``` Specifying a finite negative value for `skip` throws `GassmaSkipNegativeError`. `NaN` / `Infinity` / `-Infinity` / `null` throw `GassmaInvalidValueError` instead (see [Invalid take / skip values in findMany](./findMany#invalid-take--skip-values)). ## distinct Retrieves the first record after excluding rows with duplicate values in the specified columns. Usage is the same as [findMany's distinct](./findMany#distinct). ## Processing Order `findFirst` is processed in the following order, ultimately returning the first record (or `null` if none matches): 1. `where` - Filter 2. `orderBy` - Sort 3. `take` - Reverses the order when `-1` 4. `cursor` - Slice at cursor position (inclusive of the cursor itself) 5. `distinct` - Deduplication 6. `skip` - Skip the specified number of records 7. Take the first record 8. `select` / `omit` - Field shaping For key options and other specifications, see [findMany()](./findMany). # findFirstOrThrow() (https://gassma.io/en/docs/reference/crud/read/findFirstOrThrow) Like findFirst but throws NotFoundError when no record matches Used to retrieve the first row matching specific conditions. Works the same as `findFirst`, but throws an error instead of returning `null` when no record is found. ## Available Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | where | Specifies query conditions | Optional | Retrieves all rows if omitted | | select | Display settings for columns | Optional | Cannot be used with `omit` / `include` | | omit | Exclusion settings for columns | Optional | Cannot be used with `select` | | include | Retrieve related records | Optional | [Details here](/docs/reference/relation/include) | | orderBy | Sort settings | Optional | Array can be omitted when specifying a single column | | take | Limit number of records | Optional | | | skip | Number of records to skip | Optional | | | distinct | Deduplication settings | Optional | Array can be omitted when specifying a single column | ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to retrieve a row from the above example with the following condition: - age => **20 or older** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findFirstOrThrow const result = gassma.sheet1.findFirstOrThrow({ where: { age: { gte: 20, }, }, }); ``` The return value has the following format: ```ts { name: 'akahoshi', age: 22, pref: 'Ibaraki', postNumber: '310-8555' } ``` ## Differences from findFirst The behavior differs when no record is found: ```ts // findFirst → returns null const result = gassma.sheet1.findFirst({ where: { name: "nonexistent" }, }); // => null // findFirstOrThrow → throws NotFoundError const result = gassma.sheet1.findFirstOrThrow({ where: { name: "nonexistent" }, }); // => NotFoundError: No record found ``` For other specifications, see [findMany()](./findMany). # update() (https://gassma.io/en/docs/reference/crud/update/update) Update a single record; supports atomic number operations (increment/decrement/multiply/divide) and nested writes Updates the **first row** matching the specified conditions and retrieves the updated record. Returns `null` if no matching record is found. ## Available Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | where | Specifies query conditions | Required | If multiple rows match, only the first row is updated | | data | Data to update | Required | | | select | Display settings for return value columns | Optional | Cannot be used with `omit` / `include` | | omit | Exclusion settings for return value columns | Optional | Cannot be used with `select` | | include | Retrieve related records | Optional | [Details here](/docs/reference/relation/include) | `where` and `data` are required. Omitting either throws `GassmaMissingArgumentError` (e.g., Argument `where` is missing.). A `where` with no conditions at all (`where: {}`) throws `GassmaInvalidValueError` (Invalid value for argument `where`. Expected at least one condition.). The same applies when `where` becomes empty after removing `undefined` / `Gassma.skip`. ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to perform the following operation on the above example: - Set the age of the row where name is **akahoshi** to **23** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.update const result = gassma.sheet1.update({ where: { name: "akahoshi", }, data: { age: 23, }, }); ``` The return value has the following format: ```ts { name: 'akahoshi', age: 23, pref: 'Ibaraki', postNumber: '310-8555' } ``` The updated record is returned. Fields that were not updated retain their original values. Fields whose `data` value is `undefined` are treated as "not specified" and are not updated. ```ts const result = gassma.sheet1.update({ where: { name: "akahoshi" }, data: { name: undefined, age: 23 }, }); // => name keeps its original value; only age is updated to 23 ``` If no matching record is found, `null` is returned: ```ts const result = gassma.sheet1.update({ where: { name: "nonexistent" }, data: { age: 99 }, }); // => null ``` The `where` specification follows [findMany()](../read/findMany). However, unlike `findMany`, a `where` with no conditions at all (`where: {}`) throws an error (see the note above). ## Atomic Number Operations By specifying `increment` / `decrement` / `multiply` / `divide` in `data`, you can perform operations on the current value: ```ts // Increment age by 1 const result = gassma.sheet1.update({ where: { name: "akahoshi" }, data: { age: { increment: 1 }, }, }); // age: 22 → 23 ``` | Operation | Behavior | Example | | --- | --- | --- | | increment | Addition | `{ increment: 5 }` → current value + 5 | | decrement | Subtraction | `{ decrement: 3 }` → current value - 3 | | multiply | Multiplication | `{ multiply: 2 }` → current value × 2 | | divide | Division | `{ divide: 4 }` → current value ÷ 4 | If the current value is not a number, `0` is used as the base for calculations. ### The value passed to an operator Passing `NaN` / `Infinity` / `-Infinity` as the argument of an operator such as `increment` throws a `GassmaInvalidValueError`. Here `{argumentName}` is the **operator key**. ```ts gassma.sheet1.update({ where: { name: "akahoshi" }, data: { age: { increment: NaN } } }); // => Invalid value for argument `increment`. Expected a finite number, but received NaN. ``` ### The result of the operation If the **result** of the operation is `NaN` / `Infinity` / `-Infinity`, a `GassmaInvalidValueError` is thrown as well. Here `{argumentName}` is the **column name**, not the operator key. ```ts gassma.sheet1.update({ where: { name: "akahoshi" }, data: { age: { divide: 0 } } }); // => Invalid value for argument `age`. Expected a finite number, but received Infinity. ``` If the current value is `0` and you specify `divide: 0`, the result is `0 / 0`, which is `NaN`. ```ts // against a row whose age is 0 data: { age: { divide: 0 } }; // => Invalid value for argument `age`. Expected a finite number, but received NaN. ``` Overflow is covered too. If the result exceeds the representable range of a number it becomes `Infinity` / `-Infinity`, which is an error. ```ts // against a row whose age is 20 data: { age: { multiply: 1e308 } }; // => Invalid value for argument `age`. Expected a finite number, but received Infinity. ``` If the result is a finite number, the update proceeds as before. When the error is thrown, no row is rewritten. This validation runs in `update` / `updateMany` / `updateManyAndReturn`, in the update branch of `upsert`, and in the `update` of [Nested Write (update)](/docs/reference/relation/nested-write-update). You can also combine it with regular value assignments: ```ts const result = gassma.sheet1.update({ where: { name: "akahoshi" }, data: { age: { increment: 1 }, pref: "Tokyo", }, }); ``` Number operations can only be used on **numeric columns**. Specifying `increment` and the like on a string column results in a type error. Columns made into a composite type that includes a number via `@gassma.addType` (e.g., `number | string`) are also eligible for number operations. Number operations can be used not only in `update` but also in `updateMany` / `updateManyAndReturn`, the `update` of `upsert`, and the `data` of the `update` operation in [Nested Write (update)](/docs/reference/relation/nested-write-update). ## Nested Write When relation definitions exist, you can describe operations on related records within `data`. In addition to `create`'s Nested Write, `update` / `delete` / `deleteMany` / `disconnect` / `set` operations are available. For details, see the [Nested Write (update) reference](/docs/reference/relation/nested-write-update). # updateMany() (https://gassma.io/en/docs/reference/crud/update/updateMany) Update all matching records and get the updated count; supports limit Updates all rows matching the specified conditions to the given values. ## Available Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | where | Specifies update conditions | Optional | Targets all rows if omitted | | data | Data to update | Required | | | limit | Maximum number of records to update | Optional | Negative values cause an error | `data` is required. Omitting it throws `GassmaMissingArgumentError` (message: Argument `data` is missing.). `where` is optional; if omitted, all rows are targeted. The same applies to `where: {}` and to a `where` that became empty because its conditions were all `undefined`. Fields whose `data` value is `undefined` are treated as "not specified" and are not updated. ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to perform the following operation on the above example: - age => **Change 20 to 21** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.updateMany const result = gassma.sheet1.updateMany({ where: { age: 20, }, data: { age: 21, }, }); ``` The return value has the following format: ```ts { count: 1; } ``` The number of updated rows is returned. The `where` specification follows [findMany()](../read/findMany). ## limit You can specify the maximum number of records to update: ```ts // Update at most 2 records const result = gassma.sheet1.updateMany({ where: { pref: "Tokyo", }, data: { age: 99, }, limit: 2, }); ``` Specifying `limit: 0` results in 0 updates (nothing is updated). Specifying a finite negative value for `limit` throws `GassmaLimitNegativeError`. `NaN` / `Infinity` / `-Infinity` / `null` throw `GassmaInvalidValueError` instead. In that case no rows are updated at all. ```ts gassma.sheet1.updateMany({ data: { age: 1 }, limit: NaN }); // => Invalid value for argument `limit`. Expected a finite number, but received NaN. gassma.sheet1.updateMany({ data: { age: 1 }, limit: null }); // => Invalid value for argument `limit`. Expected a number, but received null. ``` `limit: -Infinity` used to throw `GassmaLimitNegativeError`, but the finiteness check now runs first, so it throws `GassmaInvalidValueError`. `undefined` is still ignored (no upper bound). ## Atomic Number Operations By specifying `increment` / `decrement` / `multiply` / `divide` in `data`, you can perform operations on the current value: ```ts // Increment everyone's age by 1 const result = gassma.sheet1.updateMany({ data: { age: { increment: 1 }, }, }); ``` | Operation | Behavior | Example | | --- | --- | --- | | increment | Addition | `{ increment: 5 }` → current value + 5 | | decrement | Subtraction | `{ decrement: 3 }` → current value - 3 | | multiply | Multiplication | `{ multiply: 2 }` → current value × 2 | | divide | Division | `{ divide: 4 }` → current value ÷ 4 | If the current value is not a number, `0` is used as the base for calculations. For details, see [update()](/docs/reference/crud/update/update). # updateManyAndReturn() (https://gassma.io/en/docs/reference/crud/update/updateManyAndReturn) Update all matching records and return the updated records Updates all rows matching the specified conditions and retrieves the updated records as an array. Performs the same update operation as `updateMany`, but differs in the return value. ## Available Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | where | Specifies update conditions | Optional | Targets all rows if omitted | | data | Data to update | Required | | | limit | Maximum number of records to update | Optional | Negative values cause an error | `data` is required. Omitting it throws `GassmaMissingArgumentError` (message: Argument `data` is missing.). `where` is optional; if omitted, all rows are targeted. ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to perform the following operation on the above example: - Set the age of rows where pref is **Tokyo** to **99** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.updateManyAndReturn const result = gassma.sheet1.updateManyAndReturn({ where: { pref: "Tokyo", }, data: { age: 99, }, }); ``` The return value has the following format: ```ts [ { name: "sato", age: 99, pref: "Tokyo", postNumber: "160-0023" }, { name: "endo", age: 99, pref: "Tokyo", postNumber: "160-0023" }, ]; ``` All updated records are returned as an array. Fields that were not updated retain their original values. ## Differences from updateMany | Method | Return Value | | --- | --- | | `updateMany` | `{ count: number }` | | `updateManyAndReturn` | Array of updated records | An empty array is returned if no matching records are found: ```ts const result = gassma.sheet1.updateManyAndReturn({ where: { name: "nonexistent" }, data: { age: 99 }, }); // => [] ``` Omitting `where` targets all rows and returns all records: ```ts const result = gassma.sheet1.updateManyAndReturn({ data: { age: 99 }, }); // => All records returned with age: 99 ``` The `where` specification follows [findMany()](../read/findMany). ## limit You can specify the maximum number of records to update. For details, see [updateMany()](/docs/reference/crud/update/updateMany). `NaN` / `Infinity` / `-Infinity` / `null` are handled the same way as in `updateMany`. ## Atomic Number Operations You can specify `increment` / `decrement` / `multiply` / `divide` in `data`. For details, see [update()](/docs/reference/crud/update/update). # upsert() (https://gassma.io/en/docs/reference/crud/update/upsert) Update a record if it exists, otherwise create it Updates a record if it matches the specified conditions, or creates a new one if it doesn't exist. Returns the resulting record. ## Available Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | where | Specifies search conditions | Required | | | create | Data for creation when not found | Required | | | update | Data for update when found | Required | | | select | Display settings for return value columns | Optional | Cannot be used with `include` | | omit | Exclusion settings for return value columns | Optional | Cannot be used with `select` | | include | Retrieve related records | Optional | [Details here](/docs/reference/relation/include) | `where` / `create` / `update` are all required. Omitting any of them throws `GassmaMissingArgumentError` (e.g., Argument `create` is missing.). A `where` with no conditions at all (`where: {}`) throws `GassmaInvalidValueError` (Invalid value for argument `where`. Expected at least one condition.). The same applies when `where` becomes empty after removing `undefined` / `Gassma.skip`. ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to perform the following operation on the above example: - Set the age of **akahoshi** to **23** - Create a new record if it doesn't exist The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.upsert const result = gassma.sheet1.upsert({ where: { name: "akahoshi", }, update: { age: 23, }, create: { name: "akahoshi", age: 23, pref: "Ibaraki", postNumber: "310-8555", }, }); ``` When the record exists, the updated record is returned: ```ts { name: 'akahoshi', age: 23, pref: 'Ibaraki', postNumber: '310-8555' } ``` When the record doesn't exist, it's created with the `create` data and the created record is returned: ```ts const result = gassma.sheet1.upsert({ where: { name: "newuser" }, update: { age: 30 }, create: { name: "newuser", age: 30, pref: "Tokyo", postNumber: "100-0001", }, }); // => { name: "newuser", age: 30, pref: "Tokyo", postNumber: "100-0001" } ``` ## Nested Write When relation definitions exist, Nested Write can be used within `create` / `update`: - On `create`: Equivalent to [create's Nested Write](/docs/reference/relation/nested-write) - On `update`: Equivalent to [update's Nested Write](/docs/reference/relation/nested-write-update) The `where` specification follows [findMany()](../read/findMany). However, unlike `findMany`, a `where` with no conditions at all (`where: {}`) throws an error (see the note above). # delete() (https://gassma.io/en/docs/reference/crud/delete/delete) Delete a single record and return it Deletes the **first row** matching the specified conditions and retrieves the deleted record. Returns `null` if no matching record is found. ## Available Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | where | Specifies deletion conditions | Required | | | select | Display settings for return value columns | Optional | Cannot be used with `include` | | omit | Exclusion settings for return value columns | Optional | Cannot be used with `select` | | include | Retrieve related records | Optional | [Details here](/docs/reference/relation/include) | `where` is required. Omitting it throws `GassmaMissingArgumentError` (message: Argument `where` is missing.) and **never deletes all rows implicitly**. A `where` with no conditions at all (`where: {}`) throws `GassmaInvalidValueError` (Invalid value for argument `where`. Expected at least one condition.) and no row is deleted. The same applies when `where` becomes empty after removing `undefined` / `Gassma.skip`. ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to perform the following operation on the above example: - Delete the row where name is **akahoshi** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.delete const result = gassma.sheet1.delete({ where: { name: "akahoshi", }, }); ``` The return value has the following format: ```ts { name: 'akahoshi', age: 22, pref: 'Ibaraki', postNumber: '310-8555' } ``` The deleted record is returned. If no matching record is found, `null` is returned: ```ts const result = gassma.sheet1.delete({ where: { name: "nonexistent" }, }); // => null ``` Even if multiple records match the condition, **only the first one** is deleted. ## select / omit You can control the fields in the return value: ```ts const result = gassma.sheet1.delete({ where: { name: "akahoshi" }, select: { name: true, age: true }, }); // => { name: "akahoshi", age: 22 } ``` ## onDelete When `onDelete` is configured in relation definitions, the referential action is also executed on `delete`. For details, see the [onDelete reference](/docs/reference/relation/on-delete). The `where` specification follows [findMany()](../read/findMany). However, unlike `findMany`, a `where` with no conditions at all (`where: {}`) throws an error (see the caution above). # deleteMany() (https://gassma.io/en/docs/reference/crud/delete/deleteMany) Delete all matching records and get the deleted count; supports limit Used to delete all rows matching the specified conditions. ## Available Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | where | Specifies deletion conditions | Optional | Targets all rows if omitted | | limit | Maximum number of records to delete | Optional | Negative values cause an error | `where: {}`, and a `where` that became empty because its conditions were only `undefined` / `Gassma.skip`, also target **every row for deletion**. To catch unintended `undefined` values, enable [strictUndefinedChecks](/docs/reference/config/strict-undefined-checks). ## Example Sheet ![Example Sheet](../../img/exampleSheet.png) ## Description Suppose you want to perform the following operation on the above example: - age => **Delete rows with value 20** The code would be: ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.deleteMany const result = gassma.sheet1.deleteMany({ where: { age: 20, }, }); ``` The return value has the following format: ```ts { count: 1; } ``` The number of deleted rows is returned. ## limit You can specify the maximum number of records to delete: ```ts // Delete at most 3 records const result = gassma.sheet1.deleteMany({ where: { pref: "Tokyo", }, limit: 3, }); ``` Specifying `limit: 0` results in 0 deletions (nothing is deleted). Specifying a finite negative value for `limit` throws `GassmaLimitNegativeError`. `NaN` / `Infinity` / `-Infinity` / `null` throw `GassmaInvalidValueError` instead. In that case no rows are deleted at all. ```ts gassma.sheet1.deleteMany({ limit: NaN }); // => Invalid value for argument `limit`. Expected a finite number, but received NaN. gassma.sheet1.deleteMany({ limit: null }); // => Invalid value for argument `limit`. Expected a number, but received null. ``` `limit: -Infinity` used to throw `GassmaLimitNegativeError`, but the finiteness check now runs first, so it throws `GassmaInvalidValueError`. `undefined` is still ignored (no upper bound). The `where` specification follows [findMany()](../read/findMany). # aggregate() (https://gassma.io/en/docs/reference/statistics/aggregate) Compute aggregations such as _avg, _sum, _min, _max, and _count Use this when you want to perform statistical calculations such as averages and maximum values. ## Available Keys | Key Name | Description | Optional | Notes | | -------- | -------------------------------- | -------- | ----------------------------------------------------------------------------- | | where | Specify retrieval conditions | Yes | If omitted, all rows are retrieved | | orderBy | Sort settings | Yes | If specifying only one column, the array can be omitted | | take | Set the number of records to retrieve | Yes | | | skip | Set the number of records to skip | Yes | | | cursor | Cursor-based pagination | Yes | See [findMany cursor](/docs/reference/crud/read/findMany#cursor) for details | | \_avg | Average display settings | Yes | | | \_count | Hit count display settings | Yes | `_all` and the `true` shorthand are also available. See [\_count](#_count) for details | | \_max | Maximum value display settings | Yes | | | \_min | Minimum value display settings | Yes | | | \_sum | Sum display settings | Yes | | In `where`, you can also use [relation filters](/docs/reference/relation/where-relation-filter) (`some` / `every` / `none` / `is` / `isNot`). ## Example Sheet ![Example Sheet](../img/exampleSheet.png) ## Explanation Suppose you want to perform the following operations from the example above. - age => **Calculate the average** - age => **Calculate the maximum value** - age => **Calculate the minimum value** The code would be as follows. ```ts // gassma.{{TARGET_SHEET_NAME}}.aggregate const result = gassma.sheet1.aggregate({ _avg: { age: true, }, _max: { age: true, }, _min: { age: true, }, }); ``` The return value is in the following format. ```ts { _avg: { age: 33.333333333333336 }, _max: { age: 55 }, _min: { age: 20 } } ``` In `_avg` / `_sum` / `_max` / `_min`, `NaN` / invalid Dates (Invalid Date) are excluded from aggregation as missing values, just like null. If every aggregated value is missing, the result is null. ## _count Use this when you want to get the number of matching rows. ### Counting a Specific Column If you specify a column name in `_count`, only rows whose value in that column is not a missing value — null (an empty cell), `NaN`, or an invalid Date (Invalid Date) — are counted. ```ts // gassma.{{TARGET_SHEET_NAME}}.aggregate const result = gassma.sheet1.aggregate({ _count: { age: true, }, }); ``` The return value is in the following format. ```ts { _count: { age: 9 } } ``` ### Counting All Rows with _all If you specify `_all: true`, all rows are counted, including null. ```ts // gassma.{{TARGET_SHEET_NAME}}.aggregate const result = gassma.sheet1.aggregate({ _count: { _all: true, postNumber: true, }, }); ``` The return value is in the following format. ```ts { _count: { _all: 9, postNumber: 9 } } ``` Since column counts skip null rows, on a sheet where, for example, two rows have an empty postNumber, the results would differ like `{ _all: 9, postNumber: 7 }`. ### The true Shorthand If you specify `_count: true`, the total number of rows is returned directly as a number. ```ts // gassma.{{TARGET_SHEET_NAME}}.aggregate const result = gassma.sheet1.aggregate({ _count: true, }); ``` The return value is in the following format. ```ts { _count: 9 } ``` `_all` and the `true` shorthand are exclusive to `_count` and cannot be used with `_avg` / `_max` / `_min` / `_sum`. `_count` counts rows, so "all rows including null" is meaningful, while the other aggregations target the values of a specific column. # count() (https://gassma.io/en/docs/reference/statistics/count) Count records matching a filter Use this when you want to get the number of matching records. ## Available Keys | Key Name | Description | Optional | Notes | | -------- | -------------------------------- | -------- | ----------------------------------------------------------------------------- | | where | Specify retrieval conditions | Yes | If omitted, all rows are retrieved | | orderBy | Sort settings | Yes | If specifying only one column, the array can be omitted | | take | Set the number of records to retrieve | Yes | | | skip | Set the number of records to skip | Yes | | | cursor | Cursor-based pagination | Yes | See [findMany cursor](/docs/reference/crud/read/findMany#cursor) for details | In `where`, you can also use [relation filters](/docs/reference/relation/where-relation-filter) (`some` / `every` / `none` / `is` / `isNot`). ## Example Sheet ![Example Sheet](../img/exampleSheet.png) ## Explanation Suppose you want to perform the following operation from the example above. - age => **20 or greater** The code would be as follows. ```ts // gassma.{{TARGET_SHEET_NAME}}.count const result = gassma.sheet1.count({ where: { age: { gte: 20, }, }, }); ``` The return value is in the following format. ``` 9 ``` # groupBy() (https://gassma.io/en/docs/reference/statistics/groupBy) Group records by fields, aggregate per group, and filter groups with having Use this when you want to group data. ## Available Keys | Key Name | Description | Optional | Notes | | -------- | --------------------------------------------- | -------- | ----------------------------------------------------------------------------- | | where | Specify retrieval conditions | Yes | If omitted, all rows are retrieved | | orderBy | Sort settings | Yes | If specifying only one column, the array can be omitted | | take | Set the number of records to retrieve | Yes | | | skip | Set the number of records to skip | Yes | | | \_avg | Average display settings | Yes | | | \_count | Hit count display settings | Yes | `_all` and the `true` shorthand are also available. See [\_count](#_count) for details | | \_max | Maximum value display settings | Yes | | | \_min | Minimum value display settings | Yes | | | \_sum | Sum display settings | Yes | | | by | Specify grouping conditions | No | | | having | Specify conditions after grouping | Yes | If omitted, all data is retrieved | `by` is required. Omitting it throws `GassmaMissingArgumentError` (message: Argument `by` is missing.). In `where`, you can also use [relation filters](/docs/reference/relation/where-relation-filter) (`some` / `every` / `none` / `is` / `isNot`). ## Example Sheet ![Example Sheet](../img/exampleSheet.png) ## Explanation Suppose you want to perform the following operation from the example above. - Group by pref The code would be as follows. ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: "pref", }); ``` The return value is in the following format. ```ts [ { pref: "Ibaraki" }, { pref: "Tokyo" }, { pref: "Osaka" }, { pref: "Aichi" }, { pref: "Shiga" }, { pref: "Kyoto" }, { pref: "Tottori" }, { pref: "Fukuoka" }, ]; ``` You can also specify multiple fields. Suppose you want to perform the following operations. - Group by pref - Additionally group by age The code would be as follows. ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref", "age"], }); ``` The return value is in the following format. ```ts [ { pref: "Ibaraki", age: 22 }, { pref: "Tokyo", age: 31 }, { pref: "Tokyo", age: 55 }, { pref: "Osaka", age: 20 }, { pref: "Aichi", age: 40 }, { pref: "Shiga", age: 25 }, { pref: "Kyoto", age: 45 }, { pref: "Tottori", age: 29 }, { pref: "Fukuoka", age: 33 }, ]; ``` ### Missing Values as Group Keys (null / NaN / Invalid Date) Rows whose value in a `by` column is null / `NaN` / an invalid Date (Invalid Date) are also grouped rather than dropped. - Rows with `NaN` collapse into a single group. - Rows with Invalid Date also collapse into a single group, even across different instances. - `NaN`, null, and Invalid Date form **separate groups** from each other. ### having Use this when you want to extract data that meets specific conditions from grouped data. For example, suppose you want to extract data with the following conditions. - Group by pref - (After grouping) age => **average is 30 or less** The code would be as follows. ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref"], having: { age: { _avg: { lte: 30, }, }, }, }); ``` The return value is in the following format. ```ts [ { pref: "Ibaraki" }, { pref: "Osaka" }, { pref: "Shiga" }, { pref: "Tottori" }, ]; ``` ### AND, OR, NOT in having You can also use AND, OR, and NOT. For example, suppose you want to perform the following operation. - Group by pref - (After grouping) age => **average is NOT 30 or less** The code would be as follows. ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref"], having: { NOT: { age: { _avg: { lte: 30, }, }, }, }, }); ``` The return value would be as follows. ```ts [{ pref: "Tokyo" }, { pref: "Aichi" }, { pref: "Kyoto" }, { pref: "Fukuoka" }]; ``` Also, just like `where`, you can nest AND inside NOT and create other nested combinations. Passing an incomparable value such as `NaN` or an invalid Date (Invalid Date) as a `having` value throws a `GassmaInvalidValueError` (same as `where`). ### Displaying Statistics You can also display statistics such as averages, just like with aggregate. For example, suppose you want to perform the following operations. - Group by pref - Display the average of age The code would be as follows. ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref"], _avg: { age: true }, }); ``` The return value is in the following format. ```ts [ { pref: "Ibaraki", _avg: { age: 22 } }, { pref: "Tokyo", _avg: { age: 43 } }, { pref: "Osaka", _avg: { age: 20 } }, { pref: "Aichi", _avg: { age: 40 } }, { pref: "Shiga", _avg: { age: 25 } }, { pref: "Kyoto", _avg: { age: 45 } }, { pref: "Tottori", _avg: { age: 29 } }, { pref: "Fukuoka", _avg: { age: 33 } }, ]; ``` In `_avg` / `_sum` / `_max` / `_min` and column-specified `_count`, `NaN` / invalid Dates (Invalid Date) are excluded from aggregation as missing values, just like null. If every aggregated value is missing, the result is null. `_count: { _all: true }` still counts those rows. ### _count With `_count`, you can count the number of rows in each group. If you specify a column name, only rows whose value in that column is not a missing value — null (an empty cell), `NaN`, or an invalid Date (Invalid Date) — are counted, while `_all: true` counts all rows, including missing values. For example, suppose you want to perform the following operations. - Group by pref - Display the number of rows in each group The code would be as follows. ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref"], _count: { _all: true }, }); ``` The return value is in the following format. ```ts [ { pref: "Ibaraki", _count: { _all: 1 } }, { pref: "Tokyo", _count: { _all: 2 } }, { pref: "Osaka", _count: { _all: 1 } }, { pref: "Aichi", _count: { _all: 1 } }, { pref: "Shiga", _count: { _all: 1 } }, { pref: "Kyoto", _count: { _all: 1 } }, { pref: "Tottori", _count: { _all: 1 } }, { pref: "Fukuoka", _count: { _all: 1 } }, ]; ``` If you use the `_count: true` shorthand, the row count is returned directly as a number. ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref"], _count: true, }); ``` The return value is in the following format. ```ts [ { pref: "Ibaraki", _count: 1 }, { pref: "Tokyo", _count: 2 }, { pref: "Osaka", _count: 1 }, { pref: "Aichi", _count: 1 }, { pref: "Shiga", _count: 1 }, { pref: "Kyoto", _count: 1 }, { pref: "Tottori", _count: 1 }, { pref: "Fukuoka", _count: 1 }, ]; ``` `_all` and the `true` shorthand are exclusive to `_count` and cannot be used with `_avg` / `_max` / `_min` / `_sum`. See [\_count in aggregate](/docs/reference/statistics/aggregate#_count) for details. # Relation Definition (https://gassma.io/en/docs/reference/relation/definition) Defining oneToMany, oneToOne, manyToOne, and manyToMany relations between sheets By defining relations between multiple sheets, you can retrieve related data with `include` and filter using relation conditions in `where`. ## Example Sheets The following sheets are used as examples throughout the relation documentation. ### Users Sheet | id | name | email | | --- | --- | --- | | 1 | Alice | alice@example.com | | 2 | Bob | bob@example.com | | 3 | Charlie | charlie@example.com | ### Posts Sheet | id | title | authorId | published | | --- | --- | --- | --- | | 1 | First Post | 1 | true | | 2 | How to use GAS | 1 | true | | 3 | Draft Article | 2 | false | ### Profiles Sheet | id | userId | bio | | --- | --- | --- | | 1 | 1 | I'm an engineer | | 2 | 2 | I'm a designer | ### Tags Sheet | id | name | | --- | --- | | 1 | GAS | | 2 | JavaScript | ### PostTags Sheet (Junction Table) | postId | tagId | | --- | --- | | 1 | 1 | | 1 | 2 | | 2 | 1 | ## Basic Definition You can define relations between sheets by passing a `relations` option to the `GassmaClient` constructor. ```ts const gassma = new Gassma.GassmaClient({ relations: { // Sheet name (must match the actual sheet name in the spreadsheet) Users: { // Relation name (any name you choose; becomes the key used in include and where) posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", }, }, }, }); ``` When specifying a spreadsheet ID, pass `id` along with it: ```ts const gassma = new Gassma.GassmaClient({ id: "XXXXXXXXXXXXXXXXXXX", relations: { // ... }, }); ``` ## Relation Definition Keys | Key | Description | Optional | Notes | | --- | --- | --- | --- | | type | Type of relation | Required | `oneToMany` / `oneToOne` / `manyToOne` / `manyToMany` | | to | Target sheet name | Required | | | field | Column name on the source sheet | Required | FK or PK | | reference | Column name on the target sheet | Required | | | through | Junction table settings | Optional | Required for `manyToMany` | | onDelete | Action on delete | Optional | `Cascade` / `SetNull` / `Restrict` / `NoAction` | | onUpdate | Action on PK update | Optional | `Cascade` / `SetNull` / `Restrict` / `NoAction` | ## Relation Types ### oneToMany (One-to-Many) A relationship where one parent record has multiple child records. Example: One user has multiple posts ```ts relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", // Users PK reference: "authorId", // Posts FK }, }, } ``` Returns an array when retrieved with `include`. ### manyToOne (Many-to-One) The reverse direction of `oneToMany`. Defines a reference from child records to the parent record. Example: Retrieve the author (user) from a post ```ts relations: { Posts: { author: { type: "manyToOne", to: "Users", field: "authorId", // Posts FK reference: "id", // Users PK }, }, } ``` Returns a single object or `null` when retrieved with `include`. `manyToOne` is the definition for the **side holding the FK** (the side that declares `@relation(fields: ...)` in a Prisma schema). Even in a one-to-one relationship, the FK-holding side is defined as `manyToOne`. ### oneToOne (One-to-One) A relationship where one record is linked to exactly one other record. `oneToOne` is reserved exclusively for the side of a one-to-one relationship that does **not** hold the FK. Example: User and profile (the FK `userId` is held by the Profiles side) ```ts relations: { Users: { profile: { type: "oneToOne", to: "Profiles", field: "id", // Users PK reference: "userId", // Profiles FK }, }, } ``` Returns a single object or `null` when retrieved with `include`. An error is thrown if multiple records have the same `reference` value. When defining the reverse relation from the FK-holding side (Profiles in the example above), use `manyToOne` even for a one-to-one relationship. ```ts relations: { Profiles: { user: { type: "manyToOne", to: "Users", field: "userId", // Profiles FK reference: "id", // Users PK }, }, } ``` ### manyToMany (Many-to-Many) Defines a many-to-many relationship through a junction table. Example: Posts and tags ```ts relations: { Posts: { tags: { type: "manyToMany", to: "Tags", field: "id", // Posts PK reference: "id", // Tags PK through: { sheet: "PostTags", // Junction table sheet name field: "postId", // FK for Posts in the junction table reference: "tagId", // FK for Tags in the junction table }, }, }, } ``` Returns an array when retrieved with `include`. ## Defining Multiple Relations You can define multiple relations for a single sheet and across multiple sheets. ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", }, profile: { type: "oneToOne", to: "Profiles", field: "id", reference: "userId", }, }, Posts: { author: { type: "manyToOne", to: "Users", field: "authorId", reference: "id", }, tags: { type: "manyToMany", to: "Tags", field: "id", reference: "id", through: { sheet: "PostTags", field: "postId", reference: "tagId", }, }, }, }, }); ``` ## Validation If there are errors in the relation definition, an error is thrown when creating the `GassmaClient` instance. | Error | Cause | | --- | --- | | `RelationSheetNotFoundError` | Sheet name specified in `relations` key, `to`, or `through.sheet` does not exist | | `RelationMissingPropertyError` | `type` / `to` / `field` / `reference` is missing. `through` is missing for manyToMany | | `RelationInvalidPropertyTypeError` | Property type is not string | | `RelationInvalidTypeError` | `type` is not one of the 4 types | | `RelationInvalidOnDeleteError` | `onDelete` is not one of the 4 types | | `RelationInvalidOnUpdateError` | `onUpdate` is not one of the 4 types | | `RelationColumnNotFoundError` | Column specified in `field` / `reference` does not exist in the sheet | # include (https://gassma.io/en/docs/reference/relation/include) Fetch related records with where/orderBy/select options, nested include, and relation _count Used to retrieve related data together with `findMany` / `findFirst`. Requires a prior [relation definition](/docs/reference/relation/definition). ## Example Sheets Uses the sheet examples from [relation definition](/docs/reference/relation/definition). ## Basic Usage Specify a relation name in `include` with a value of `true` to retrieve all related data. ```ts const result = gassma.Users.findMany({ include: { posts: true, }, }); ``` The return value has the following format: ```ts [ { id: 1, name: "Alice", email: "alice@example.com", posts: [ { id: 1, title: "First Post", authorId: 1, published: true }, { id: 2, title: "How to use GAS", authorId: 1, published: true }, ], }, { id: 2, name: "Bob", email: "bob@example.com", posts: [ { id: 3, title: "Draft Article", authorId: 2, published: false }, ], }, { id: 3, name: "Charlie", email: "charlie@example.com", posts: [], }, ]; ``` The returned shape differs by relation type: | Relation Type | Returned Shape | | --- | --- | | oneToMany | Array | | manyToMany | Array | | oneToOne | Single object or `null` | | manyToOne | Single object or `null` | ### manyToOne Example ```ts const result = gassma.Posts.findMany({ include: { author: true, }, }); ``` The return value has the following format: ```ts [ { id: 1, title: "First Post", authorId: 1, published: true, author: { id: 1, name: "Alice", email: "alice@example.com" }, }, { id: 3, title: "Draft Article", authorId: 2, published: false, author: { id: 2, name: "Bob", email: "bob@example.com" }, }, // ... ]; ``` ## include Options Instead of `true`, you can pass an object to apply conditions to the related data. ### Available Keys | Key | Description | Optional | | --- | --- | --- | | where | Query conditions for related data | Optional | | orderBy | Sort order for related data | Optional | | skip | Number of related records to skip | Optional | | take | Number of related records to retrieve | Optional | | select | Display settings for related columns | Optional | | omit | Exclusion settings for related columns | Optional | | include | Retrieve deeper nested relations | Optional | ### where Apply conditions to filter related data: ```ts const result = gassma.Users.findMany({ include: { posts: { where: { published: true }, }, }, }); ``` The return value has the following format: ```ts [ { id: 1, name: "Alice", email: "alice@example.com", posts: [ { id: 1, title: "First Post", authorId: 1, published: true }, { id: 2, title: "How to use GAS", authorId: 1, published: true }, ], }, { id: 2, name: "Bob", email: "bob@example.com", posts: [], // published: false articles are filtered out }, // ... ]; ``` ### orderBy Sort related data: ```ts const result = gassma.Users.findMany({ include: { posts: { orderBy: { title: "desc" }, }, }, }); ``` ### skip / take Paginate related data using `skip` and `take` together: ```ts const result = gassma.Users.findMany({ include: { posts: { orderBy: { id: "asc" }, skip: 1, take: 1, }, }, }); ``` The above example orders each user's posts by id ascending, skips the first one, and retrieves only the next one. `skip` / `take` are available for oneToMany and manyToMany. They are not applicable to oneToOne / manyToOne as they return a single record. Passing a non-number to `skip` / `take` in `include` throws an `IncludeInvalidOptionTypeError`. The message differs depending on the value. ```ts gassma.Users.findMany({ include: { posts: { take: NaN } } }); // => IncludeInvalidOptionTypeError: // Include "posts": option "take" must be a finite number gassma.Users.findMany({ include: { posts: { take: null } } }); // => IncludeInvalidOptionTypeError: // Include "posts": option "take" must be a number ``` | Value | Message | | --- | --- | | `NaN` / `Infinity` / `-Infinity` | `must be a finite number` | | `null` or any non-number | `must be a number` | | `undefined` | Treated as "not specified" and ignored | Finite negative numbers are not errors: `take` reads from the end, and a negative `skip` throws `GassmaSkipNegativeError`. Options of nested `include` receive the same validation. ### select Specify which columns to retrieve from related data: ```ts const result = gassma.Users.findMany({ include: { posts: { select: { title: true }, }, }, }); ``` The return value has the following format: ```ts [ { id: 1, name: "Alice", email: "alice@example.com", posts: [ { title: "First Post" }, { title: "How to use GAS" }, ], }, // ... ]; ``` ### omit Exclude specific columns from related data: ```ts const result = gassma.Users.findMany({ include: { posts: { omit: { authorId: true }, }, }, }); ``` `omit` is merged with the related model's [global omit](/docs/reference/config/global-omit). As with the top-level `omit`, specifying `false` re-includes a field hidden by the global omit for this fetch only: ```ts // With a global omit excluding Posts.content const result = gassma.Users.findMany({ include: { posts: { omit: { content: false }, // content is returned (overrides the global omit) }, }, }); ``` `select` and `omit` cannot be specified simultaneously. ### Nested include You can specify `include` within `include` to retrieve deep relation hierarchies. For example, you can retrieve Users → Posts → Tags in a single query: ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", }, }, Posts: { tags: { type: "manyToMany", to: "Tags", field: "id", reference: "id", through: { sheet: "PostTags", field: "postId", reference: "tagId", }, }, }, }, }); const result = gassma.Users.findMany({ include: { posts: { include: { tags: true, }, }, }, }); ``` The return value has the following format: ```ts [ { id: 1, name: "Alice", email: "alice@example.com", posts: [ { id: 1, title: "First Post", authorId: 1, published: true, tags: [ { id: 1, name: "GAS" }, { id: 2, name: "JavaScript" }, ], }, { id: 2, title: "How to use GAS", authorId: 1, published: true, tags: [ { id: 1, name: "GAS" }, ], }, ], }, // ... ]; ``` `select` and `include` cannot be specified simultaneously. ## _count Retrieve the count of related records. ### Count All Relations Specify `_count: true` to get the record count for all defined relations: ```ts const result = gassma.Users.findMany({ include: { _count: true, }, }); ``` The return value has the following format: ```ts [ { id: 1, name: "Alice", email: "alice@example.com", _count: { posts: 2, profile: 1 }, }, { id: 2, name: "Bob", email: "bob@example.com", _count: { posts: 1, profile: 0 }, }, // ... ]; ``` ### Count Specific Relations Use `_count: { select: { ... } }` to specify which relations to count: ```ts const result = gassma.Users.findMany({ include: { _count: { select: { posts: true }, }, }, }); ``` ### Count with where Filter You can also add conditions to the count target: ```ts const result = gassma.Users.findMany({ include: { _count: { select: { posts: { where: { published: true }, }, }, }, }, }); ``` The return value has the following format: ```ts [ { id: 1, name: "Alice", email: "alice@example.com", _count: { posts: 2 }, // Only published: true posts are counted }, // ... ]; ``` ### Combining select and _count You can combine top-level `select` with `_count`: ```ts const result = gassma.Users.findMany({ select: { name: true, _count: { select: { posts: true }, }, }, }); ``` The return value has the following format: ```ts [ { name: "Alice", _count: { posts: 2 } }, { name: "Bob", _count: { posts: 1 } }, // ... ]; ``` `_count` supports all relation types (oneToMany / oneToOne / manyToOne / manyToMany). ## Retrieving Multiple Relations Simultaneously You can retrieve multiple relations in a single query: ```ts const result = gassma.Users.findMany({ include: { posts: true, profile: true, }, }); ``` ## Restrictions on include and select **Top-level** `select` and `include` cannot be used simultaneously. ```ts // This will throw an error gassma.Users.findMany({ select: { name: true }, include: { posts: true }, }); ``` ## Validation | Error | Cause | | --- | --- | | `IncludeWithoutRelationsError` | Used `include` without relation definitions | | `GassmaIncludeSelectConflictError` | Used `include` and `select` simultaneously at the top level | | `IncludeSelectOmitConflictError` | Used `select` and `omit` simultaneously within include | | `IncludeSelectIncludeConflictError` | Used `select` and `include` simultaneously within include | | `IncludeInvalidOptionTypeError` | Invalid type for include value or option | | `GassmaRelationNotFoundError` | Specified relation name is not defined | # where Relation Filter (https://gassma.io/en/docs/reference/relation/where-relation-filter) Filter by related records using some/every/none (list relations) and is/isNot (single relations) Used to filter based on related data within `where` conditions. Requires a prior [relation definition](/docs/reference/relation/definition). ## Example Sheets Uses the sheet examples from [relation definition](/docs/reference/relation/definition). ## Supported Methods where relation filters can be used with all the following methods: - `findMany` / `findFirst` - `update` / `updateMany` / `deleteMany` - `aggregate` / `count` / `groupBy` ## Filter Types Available filters differ by relation type: | Filter | oneToMany | manyToMany | oneToOne | manyToOne | | --- | --- | --- | --- | --- | | some | Available | Available | - | - | | every | Available | Available | - | - | | none | Available | Available | - | - | | is | - | - | Available | Available | | isNot | - | - | Available | Available | ## List Relation Filters (oneToMany / manyToMany) ### some Retrieves records where **at least one** related record matches the condition. Example: Get users who have at least one published post ```ts const result = gassma.Users.findMany({ where: { posts: { some: { published: true }, }, }, }); ``` The return value has the following format: ```ts [ { id: 1, name: "Alice", email: "alice@example.com" }, ]; ``` Alice is retrieved because she has published posts. Bob only has `published: false` posts, and Charlie has no posts, so they are excluded. ### every Retrieves records where **all** related records match the condition. Records with 0 related records are also treated as matching. Example: Get users whose all posts are published ```ts const result = gassma.Users.findMany({ where: { posts: { every: { published: true }, }, }, }); ``` The return value has the following format: ```ts [ { id: 1, name: "Alice", email: "alice@example.com" }, { id: 3, name: "Charlie", email: "charlie@example.com" }, ]; ``` Alice is retrieved because all her posts are `published: true`. Charlie is retrieved because he has 0 posts (= all satisfy the condition). ### none Retrieves records where **not a single** related record matches the condition. Example: Get users who have no published posts ```ts const result = gassma.Users.findMany({ where: { posts: { none: { published: true }, }, }, }); ``` The return value has the following format: ```ts [ { id: 2, name: "Bob", email: "bob@example.com" }, { id: 3, name: "Charlie", email: "charlie@example.com" }, ]; ``` When the parent record's own join key (the column specified as `reference` in the relation definition) is null, its related records are treated as 0. Such parents are therefore included in the results of `every` (all records satisfy the condition) and `none` (no record matches), and excluded from `some` (same as Prisma). ## Single Relation Filters (oneToOne / manyToOne) ### is Retrieves records where the related record matches the condition. Specifying `null` retrieves records where no related record exists. Example: Get posts whose author name is "Alice" ```ts const result = gassma.Posts.findMany({ where: { author: { is: { name: "Alice" }, }, }, }); ``` The return value has the following format: ```ts [ { id: 1, title: "First Post", authorId: 1, published: true }, { id: 2, title: "How to use GAS", authorId: 1, published: true }, ]; ``` ### is: null Retrieves records where no related record exists. For `manyToOne` (the FK-holding side), this matches records whose FK is null; for `oneToOne` (the side without the FK), this matches records for which **no related record exists**. Example: Get posts whose author (the FK `authorId`) is null ```ts const result = gassma.Posts.findMany({ where: { author: { is: null, }, }, }); ``` Example: Get users who have no profile (`oneToOne` on the non-FK side) ```ts const result = gassma.Users.findMany({ where: { profile: { is: null, }, }, }); ``` The return value has the following format: ```ts [ { id: 3, name: "Charlie", email: "charlie@example.com" }, ]; ``` For `oneToOne` (the side without the FK), records whose own join key (the column specified as `reference`) is null are also included in the `is: null` results as "no related record exists". ### isNot Retrieves records where the related record does **not** match the condition. Specifying `null` retrieves records where a related record exists. Example: Get posts whose author is not "Alice" ```ts const result = gassma.Posts.findMany({ where: { author: { isNot: { name: "Alice" }, }, }, }); ``` The return value has the following format: ```ts [ { id: 3, title: "Draft Article", authorId: 2, published: false }, ]; ``` Rows without a related record (for `manyToOne`, rows whose FK is null; for `oneToOne`, rows with no related record) are also included in the results of `isNot: `. Since no related record exists, they count as "having no related record that matches the condition" (same as Prisma). ### isNot: null Retrieves records where a related record exists. For `manyToOne` (the FK-holding side), this matches records whose FK is not null; for `oneToOne` (the side without the FK), this matches records for which **a related record exists**. ```ts const result = gassma.Posts.findMany({ where: { author: { isNot: null, }, }, }); ``` ## Combining with AND / OR / NOT Relation filters can be freely combined with AND / OR / NOT. ### Combining with OR Example: Get users who have published posts OR whose name is "Charlie" ```ts const result = gassma.Users.findMany({ where: { OR: [ { posts: { some: { published: true } } }, { name: "Charlie" }, ], }, }); ``` ### Combining with NOT Example: Get users who don't have any draft articles ```ts const result = gassma.Users.findMany({ where: { NOT: { posts: { some: { published: false } }, }, }, }); ``` ## Usage Examples Beyond findMany / findFirst ### updateMany Update email addresses of users who have published posts: ```ts gassma.Users.updateMany({ where: { posts: { some: { published: true } }, }, data: { email: "updated@example.com", }, }); ``` ### count Count users who have published posts: ```ts const count = gassma.Users.count({ where: { posts: { some: { published: true } }, }, }); ``` ## Validation | Error | Cause | | --- | --- | | `WhereRelationWithoutContextError` | Used relation filter syntax without relation definitions | | `WhereRelationInvalidFilterError` | Used list filters (some/every/none) on oneToOne/manyToOne, or used single filters (is/isNot) on oneToMany/manyToMany | # onDelete (https://gassma.io/en/docs/reference/relation/on-delete) Referential actions on delete (Cascade, SetNull, Restrict, NoAction) Defines how related records should be handled when records are deleted with `deleteMany`. ## Example Sheets Uses the sheet examples from [relation definition](/docs/reference/relation/definition). ## Basic Usage Specify `onDelete` in the [relation definition](/docs/reference/relation/definition): ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "Cascade", }, }, }, }); ``` onDelete does not fire on relation definitions of the FK-holding side (`manyToOne`). Specify it on the referenced side (`oneToMany` / the non-FK side `oneToOne` / `manyToMany`). ## Action Types | Action | Behavior | | --- | --- | | Cascade | Delete related records together | | SetNull | Set the FK of related records to null | | Restrict | Prevent deletion with an error if related records exist | | NoAction | Do nothing (default) | ## Cascade When a parent record is deleted, related child records are automatically deleted as well. This writes to multiple sheets, but if it fails partway through, none of the sheets are modified (see [Write Atomicity and Concurrency](/docs/reference/write-atomicity)): ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "Cascade", }, }, }, }); // Deleting Alice also deletes all of Alice's posts gassma.Users.deleteMany({ where: { name: "Alice" }, }); ``` Executing the above deletes Alice from the Users sheet, and also deletes all records in the Posts sheet with `authorId: 1`. ### manyToMany Case When Cascade is specified for manyToMany, records in the **junction table** are deleted. Records in the target table (relation destination) are not deleted. ```ts relations: { Posts: { tags: { type: "manyToMany", to: "Tags", field: "id", reference: "id", through: { sheet: "PostTags", field: "postId", reference: "tagId", }, onDelete: "Cascade", }, }, } // Deleting a post also deletes related rows in PostTags (Tags remain) gassma.Posts.deleteMany({ where: { id: 1 }, }); ``` ## SetNull When a parent record is deleted, the FK of related child records is updated to `null`: ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "SetNull", }, }, }, }); // Deleting Alice sets the authorId of Alice's posts to null gassma.Users.deleteMany({ where: { name: "Alice" }, }); ``` The Posts sheet after execution: | id | title | authorId | published | | --- | --- | --- | --- | | 1 | First Post | null | true | | 2 | How to use GAS | null | true | | 3 | Draft Article | 2 | false | When SetNull is specified for manyToMany, nothing happens (because setting the junction table FK to null is meaningless). ## Restrict If even one related record exists, the deletion is rejected and an error is thrown: ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "Restrict", }, }, }, }); // Alice has posts, so an error is thrown gassma.Users.deleteMany({ where: { name: "Alice" }, }); // => RelationOnDeleteRestrictError // Charlie has no posts, so deletion proceeds normally gassma.Users.deleteMany({ where: { name: "Charlie" }, }); ``` Restrict checks are performed **first** for all relations. Therefore, even if an error occurs, side effects (Cascade of other relations, etc.) are not executed. ## NoAction Does nothing. Same behavior as not specifying `onDelete`: ```ts relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "NoAction", // Same as not specifying }, }, } ``` ## onDelete with Multiple Relations You can define multiple relations for a single sheet with different onDelete settings: ```ts relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "Cascade", // Posts are deleted together }, profile: { type: "oneToOne", to: "Profiles", field: "id", reference: "userId", onDelete: "SetNull", // Profile FK is set to null }, }, } ``` # Nested Write (create) (https://gassma.io/en/docs/reference/relation/nested-write) Write related records inside create using create/connect/connectOrCreate Used to simultaneously create and associate records in relation targets within the `create` method. Requires a prior [relation definition](/docs/reference/relation/definition). ## Example Sheets Uses the sheet examples from [relation definition](/docs/reference/relation/definition). ## Available Operations | Operation | Description | | --- | --- | | create | Create a new related record and associate it | | createMany | Create multiple new related records and associate them | | connect | Associate an existing related record | | connectOrCreate | Associate if existing record found, otherwise create and associate | ### Compatibility by Relation Type | Operation | manyToOne | oneToOne | oneToMany | manyToMany | | --- | --- | --- | --- | --- | | create | Single only | Single only | Single/Array | Single/Array | | createMany | - | - | Supported | - | | connect | Supported | Supported | Single/Array | Single/Array | | connectOrCreate | Supported | Supported | Single/Array | Single/Array | The behavior of to-one relations differs depending on where the FK lives. - **manyToOne (FK-holding side)**: the value is set on the **own record's FK** - **oneToOne (non-FK side)**: the FK of the related record holding the FK is rewritten. The own record is not modified `oneToOne` is reserved for the non-FK side of a one-to-one relationship (see [relation definition](/docs/reference/relation/definition)). ### Behavior of oneToOne (Non-FK Side) | Operation | Behavior | When the target record does not exist | | --- | --- | --- | | create | Creates the related record with the FK automatically set | - | | connect | Replaces (nulls the FK of the currently connected record, then sets the target record's FK to the parent) | `NestedWriteConnectNotFoundError` | | connectOrCreate | Same replacement as connect if found, otherwise creates with the FK automatically set | - | Specifying `createMany` or array forms results in `NestedWriteInvalidOperationError`. ## create ### create with manyToOne Example of creating a post while also creating the associated author. In manyToOne, this is used in reverse (creating the author from the post side). ```ts const result = gassma.Posts.create({ data: { id: 4, title: "New Article", published: true, author: { create: { id: 4, name: "Dave", email: "dave@example.com", }, }, }, }); ``` Executing the above performs the following: 1. Dave is created in the Users sheet 2. Dave's `id` (= 4) is automatically set as the Posts `authorId` 3. The new article is created in the Posts sheet The return value has the following format: ```ts { id: 4, title: "New Article", authorId: 4, published: true, } ``` ### create with oneToOne Create a profile simultaneously when creating a user. With oneToOne (non-FK side), the parent's value is automatically set as the FK of the related record: ```ts const result = gassma.Users.create({ data: { id: 4, name: "Dave", email: "dave@example.com", profile: { create: { id: 3, bio: "I just joined" }, }, }, }); ``` Executing the above performs the following: 1. Dave is created in the Users sheet 2. `{ id: 3, userId: 4, bio: "I just joined" }` is created in the Profiles sheet (`userId` is automatically set to Dave's `id` = 4) ### create with oneToMany Create posts simultaneously when creating a user: ```ts const result = gassma.Users.create({ data: { id: 4, name: "Dave", email: "dave@example.com", posts: { create: [ { id: 4, title: "Dave's Article 1", published: true }, { id: 5, title: "Dave's Article 2", published: false }, ], }, }, }); ``` Executing the above performs the following: 1. Dave is created in the Users sheet 2. 2 articles are created in the Posts sheet (`authorId` is automatically set to Dave's `id` = 4) You can also create a single record with an object instead of an array: ```ts posts: { create: { id: 4, title: "Dave's Article", published: true }, } ``` ### create with manyToMany Create tags simultaneously when creating a post, and associate them in the junction table: ```ts const result = gassma.Posts.create({ data: { id: 4, title: "New Article", authorId: 1, published: true, tags: { create: { id: 3, name: "TypeScript" }, }, }, }); ``` Executing the above performs the following: 1. The new article is created in the Posts sheet 2. The "TypeScript" tag is created in the Tags sheet 3. `{ postId: 4, tagId: 3 }` is created in the PostTags sheet ## createMany Bulk create multiple child records with oneToMany: ```ts const result = gassma.Users.create({ data: { id: 4, name: "Dave", email: "dave@example.com", posts: { createMany: { data: [ { id: 4, title: "Article 1", published: true }, { id: 5, title: "Article 2", published: false }, ], }, }, }, }); ``` Dave's `id` is automatically set as `authorId` for each record. ## connect Associates existing records. Specify the target record with `where` conditions. ### connect with manyToOne Create a post linked to an existing user: ```ts const result = gassma.Posts.create({ data: { id: 4, title: "New Article", published: true, author: { connect: { name: "Alice" }, }, }, }); ``` Executing the above performs the following: 1. Search for a record with `name: "Alice"` in the Users sheet 2. Alice's `id` (= 1) is automatically set as the Posts `authorId` 3. The new article is created in the Posts sheet If no record matching the condition is found, `NestedWriteConnectNotFoundError` is thrown. ### connect with oneToOne Create a user and simultaneously link an existing profile: ```ts const result = gassma.Users.create({ data: { id: 4, name: "Dave", email: "dave@example.com", profile: { connect: { id: 1 }, }, }, }); ``` The above updates the `userId` of `id: 1` in the Profiles sheet to Dave's `id` (= 4). connect on oneToOne behaves as a **replacement**. If a related record is already connected to the parent, its FK is set to `null` before the target record's FK is set to the parent. If no record matching the condition is found, `NestedWriteConnectNotFoundError` is thrown. ### connect with oneToMany Create a user and simultaneously link existing posts: ```ts const result = gassma.Users.create({ data: { id: 4, name: "Dave", email: "dave@example.com", posts: { connect: [ { title: "Draft Article" }, ], }, }, }); ``` The above updates the `authorId` of "Draft Article" in the Posts sheet to Dave's `id` (= 4). ### connect with manyToMany Associate existing tags with a post: ```ts const result = gassma.Posts.create({ data: { id: 4, title: "New Article", authorId: 1, published: true, tags: { connect: [ { name: "GAS" }, { name: "JavaScript" }, ], }, }, }); ``` Executing the above performs the following: 1. The new article is created in the Posts sheet 2. `{ postId: 4, tagId: 1 }` and `{ postId: 4, tagId: 2 }` are created in the PostTags sheet The records in the Tags sheet are not modified. ## connectOrCreate Associates if an existing record is found, otherwise creates a new one and associates it: ```ts const result = gassma.Posts.create({ data: { id: 4, title: "New Article", published: true, author: { connectOrCreate: { where: { name: "Alice" }, create: { id: 4, name: "Alice", email: "alice-new@example.com", }, }, }, }, }); ``` In the above case, since Alice exists in the Users sheet, it behaves the same as connect. If she doesn't exist, a new record is created with the `create` data. Likewise for oneToOne (non-FK side): if the record is found, it behaves as the same replacement as connect; if not found, the related record is created with the FK automatically set. For oneToMany / manyToMany, you can specify multiple with an array: ```ts tags: { connectOrCreate: [ { where: { name: "GAS" }, create: { id: 3, name: "GAS" }, }, { where: { name: "New Tag" }, create: { id: 4, name: "New Tag" }, }, ], } ``` ## Deep Nesting Nested write is processed recursively, so you can create deep relation hierarchies at once. For example, creating User → Posts → Tags at once: ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", }, }, Posts: { tags: { type: "manyToMany", to: "Tags", field: "id", reference: "id", through: { sheet: "PostTags", field: "postId", reference: "tagId", }, }, }, }, }); const result = gassma.Users.create({ data: { id: 4, name: "Dave", email: "dave@example.com", posts: { create: { id: 4, title: "Dave's Article", published: true, tags: { create: { id: 3, name: "TypeScript" }, }, }, }, }, }); ``` The above is processed in the following order: 1. Dave is created in the Users sheet 2. The article is created in the Posts sheet (`authorId: 4` is automatically set) 3. "TypeScript" is created in the Tags sheet 4. A relation row is created in the PostTags sheet ## Notes - Nested write is only available in the `create` method. It cannot be used in `createMany` / `updateMany`, etc. - FK is automatically set, but PK (id, etc.) must be explicitly specified. There is no auto-increment feature. - If no record matching the `where` condition in `connect` is found, `NestedWriteConnectNotFoundError` is thrown. - A nested write writes to multiple sheets, but if it fails partway through, not a single row is written to any of them. See [Write Atomicity and Concurrency](/docs/reference/write-atomicity) for details. ## Validation | Error | Cause | | --- | --- | | `NestedWriteWithoutRelationsError` | Used nested write syntax without relation definitions | | `NestedWriteConnectNotFoundError` | Record not found with `connect` / `connectOrCreate` where condition | | `NestedWriteInvalidOperationError` | Specified an operation not supported for the relation type (e.g., `createMany` or array forms on oneToOne) | # onUpdate (https://gassma.io/en/docs/reference/relation/on-update) Referential actions on update (Cascade, SetNull, Restrict, NoAction) Defines how related records should be handled when a record's PK (primary key) is changed with `update` / `updateMany` / `updateManyAndReturn`. ## Example Sheets Uses the sheet examples from [relation definition](/docs/reference/relation/definition). ## Basic Usage Specify `onUpdate` in the [relation definition](/docs/reference/relation/definition): ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onUpdate: "Cascade", }, }, }, }); ``` onUpdate does not fire on relation definitions of the FK-holding side (`manyToOne`). Specify it on the referenced side (`oneToMany` / the non-FK side `oneToOne` / `manyToMany`). ## Action Types | Action | Behavior | | --- | --- | | Cascade | Automatically update the FK of related records to the new value | | SetNull | Set the FK of related records to null | | Restrict | Prevent the update with an error if related records exist | | NoAction | Do nothing (default) | ## Cascade When a parent record's PK is changed, the FK of related child records is automatically updated to the new value. This writes to multiple sheets, but if it fails partway through, none of the sheets are modified (see [Write Atomicity and Concurrency](/docs/reference/write-atomicity)): ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onUpdate: "Cascade", }, }, }, }); // Changing Alice's id from 1 to 10 also updates Posts authorId: 1 to authorId: 10 gassma.Users.updateMany({ where: { name: "Alice" }, data: { id: 10 }, }); ``` The Posts sheet after update: | id | title | authorId | published | | --- | --- | --- | --- | | 1 | First Post | 10 | true | | 2 | How to use GAS | 10 | true | | 3 | Draft Article | 2 | false | ### manyToMany Case When Cascade is specified for manyToMany, the corresponding column in the **junction table** is updated to the new value: ```ts relations: { Posts: { tags: { type: "manyToMany", to: "Tags", field: "id", reference: "id", through: { sheet: "PostTags", field: "postId", reference: "tagId", }, onUpdate: "Cascade", }, }, } // Changing post id from 1 to 100 also updates PostTags postId: 1 to postId: 100 gassma.Posts.updateMany({ where: { id: 1 }, data: { id: 100 }, }); ``` ## SetNull When a parent record's PK is changed, the FK of related child records is updated to `null`: ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onUpdate: "SetNull", }, }, }, }); // Changing Alice's id sets the authorId of Alice's posts to null gassma.Users.updateMany({ where: { name: "Alice" }, data: { id: 10 }, }); ``` The Posts sheet after update: | id | title | authorId | published | | --- | --- | --- | --- | | 1 | First Post | null | true | | 2 | How to use GAS | null | true | | 3 | Draft Article | 2 | false | When SetNull is specified for manyToMany, nothing happens (because setting the junction table FK to null is meaningless). ## Restrict If even one related record exists, the update is rejected and an error is thrown: ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onUpdate: "Restrict", }, }, }, }); // Alice has posts, so changing the id causes an error gassma.Users.updateMany({ where: { name: "Alice" }, data: { id: 10 }, }); // => RelationOnUpdateRestrictError // Charlie has no posts, so the update proceeds normally gassma.Users.updateMany({ where: { name: "Charlie" }, data: { id: 10 }, }); ``` Restrict checks are performed **first** for all relations. Therefore, even if an error occurs, side effects (Cascade of other relations, etc.) are not executed. ## NoAction Does nothing. Same behavior as not specifying `onUpdate`: ```ts relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onUpdate: "NoAction", // Same as not specifying }, }, } ``` ## Combining with onDelete `onDelete` and `onUpdate` can be specified simultaneously in the same relation definition: ```ts relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "Cascade", // On delete: Posts are also deleted onUpdate: "Cascade", // On PK update: Posts FK is also updated }, }, } ``` # Nested Write (update) (https://gassma.io/en/docs/reference/relation/nested-write-update) Modify related records inside update using update/delete/deleteMany/disconnect/set You can simultaneously operate on related records within the `data` of the `update` method. In addition to the operations available in [create's Nested Write](/docs/reference/relation/nested-write), `update` / `delete` / `deleteMany` / `disconnect` / `set` operations are available. These operations write to multiple sheets, but if one fails partway through, not a single row is written to any of them. See [Write Atomicity and Concurrency](/docs/reference/write-atomicity) for details. ## Example Sheets Uses the sheet examples from [relation definition](/docs/reference/relation/definition). ## Available Operations | Operation | manyToOne / oneToOne | oneToMany | manyToMany | | --- | --- | --- | --- | | create | Single only | Single / Array | Single / Array | | createMany | - | Supported | - | | connect | Supported | Single / Array | Single / Array | | connectOrCreate | Supported | Single / Array | Single / Array | | update | Supported | Single / Array | - | | delete | Supported | Single / Array | - | | deleteMany | - | Single / Array | - | | disconnect | Supported | Single / Array | Single / Array | | set | - | Supported | Supported | manyToOne and oneToOne accept the same operation shapes but behave differently. manyToOne (FK-holding side) operates on the **own record's FK**, while oneToOne (non-FK side) operates on the **related record holding the FK** (see [relation definition](/docs/reference/relation/definition)). ### Behavior of oneToOne (Non-FK Side) | Operation | Behavior | When the target record does not exist | | --- | --- | --- | | create | Creates the related record with the FK automatically set | - | | connect | Replaces (nulls the FK of the currently connected record, then sets the target record's FK to the parent) | `NestedWriteConnectNotFoundError` | | connectOrCreate | Same replacement as connect if found, otherwise creates with the FK automatically set | - | | update | Updates the related record (specify data directly) | `NestedWriteTargetNotFoundError` | | disconnect: true | Nulls the FK of the related record | Does nothing | | delete: true | Deletes the related record | `NestedWriteTargetNotFoundError` | Specifying `set` / `deleteMany` / `createMany` / array forms results in `NestedWriteInvalidOperationError`. ## create Creates a new related record and associates it. Same behavior as [create's Nested Write](/docs/reference/relation/nested-write). ```ts // oneToMany: Create a new post when updating a user gassma.Users.update({ where: { name: "Alice" }, data: { posts: { create: { id: 4, title: "New Post", published: true }, }, }, }); ``` ## connect Associates an existing related record: ```ts // manyToOne: Change the post's author to an existing user gassma.Posts.update({ where: { id: 1 }, data: { author: { connect: { name: "Bob" }, }, }, }); ``` With oneToOne (non-FK side), connect behaves as a **replacement**. The FK of the currently connected related record is set to `null`, then the target record's FK is set to the parent: ```ts // oneToOne: Replace the user's profile with another profile gassma.Users.update({ where: { name: "Alice" }, data: { profile: { connect: { id: 2 }, }, }, }); // => After the userId of Profiles id: 1 (currently connected) becomes null, // the userId of id: 2 is updated to Alice's id (= 1) ``` ## connectOrCreate Associates if existing record found, otherwise creates and associates: ```ts // manyToOne: Connect to author if exists, otherwise create gassma.Posts.update({ where: { id: 1 }, data: { author: { connectOrCreate: { where: { name: "Dave" }, create: { id: 4, name: "Dave", email: "dave@example.com" }, }, }, }, }); ``` ## update Updates related records. ### manyToOne (FK-Holding Side) Specify the update data directly. The record referenced by the own record's FK is updated: ```ts // manyToOne: Update the post's author name gassma.Posts.update({ where: { id: 1 }, data: { author: { update: { name: "Alice Updated" }, }, }, }); ``` ### oneToOne (Non-FK Side) Likewise, specify the update data directly. The related record referencing the parent is updated: ```ts // oneToOne: Update the user's profile gassma.Users.update({ where: { name: "Alice" }, data: { profile: { update: { bio: "Updated bio" }, }, }, }); ``` If no connected related record exists, `NestedWriteTargetNotFoundError` is thrown. ### oneToMany Specify `where` and `data` to narrow down the update target. Multiple specifications with arrays are also possible: ```ts // oneToMany: Update a specific post gassma.Users.update({ where: { name: "Alice" }, data: { posts: { update: { where: { id: 1 }, data: { title: "Updated Title" }, }, }, }, }); // Update multiple posts simultaneously gassma.Users.update({ where: { name: "Alice" }, data: { posts: { update: [ { where: { id: 1 }, data: { title: "Title A" } }, { where: { id: 2 }, data: { title: "Title B" } }, ], }, }, }); ``` ## delete Deletes related records. ### manyToOne (FK-Holding Side) Specify `delete: true` to delete the related record and set the own FK to `null`: ```ts // manyToOne: Delete the post's author (post's authorId becomes null) gassma.Posts.update({ where: { id: 1 }, data: { author: { delete: true }, }, }); ``` ### oneToOne (Non-FK Side) Specify `delete: true` to delete the related record referencing the parent. The own record is not modified: ```ts // oneToOne: Delete the user's profile gassma.Users.update({ where: { name: "Alice" }, data: { profile: { delete: true }, }, }); // => The record with userId: 1 in Profiles is deleted ``` If no connected related record exists, `NestedWriteTargetNotFoundError` is thrown. ### oneToMany Specify `where` conditions to narrow down deletion targets. Multiple specifications with arrays are also possible: ```ts // oneToMany: Delete a specific post gassma.Users.update({ where: { name: "Alice" }, data: { posts: { delete: { id: 3 }, }, }, }); // Delete multiple gassma.Users.update({ where: { name: "Alice" }, data: { posts: { delete: [{ id: 2 }, { id: 3 }], }, }, }); ``` ## deleteMany Bulk deletes related records matching conditions. Only available for oneToMany: ```ts // oneToMany: Delete all unpublished posts gassma.Users.update({ where: { name: "Alice" }, data: { posts: { deleteMany: { published: false }, }, }, }); // Delete with multiple conditions gassma.Users.update({ where: { name: "Alice" }, data: { posts: { deleteMany: [ { published: false }, { title: "Draft" }, ], }, }, }); ``` ## disconnect Removes the relation association. The record itself is not deleted. ### manyToOne (FK-Holding Side) Specify `disconnect: true` to set the own FK to `null`: ```ts // manyToOne: Remove the association between post and author gassma.Posts.update({ where: { id: 1 }, data: { author: { disconnect: true }, }, }); // => Posts authorId becomes null ``` ### oneToOne (Non-FK Side) Specify `disconnect: true` to set the FK of the related record referencing the parent to `null`: ```ts // oneToOne: Remove the association between user and profile gassma.Users.update({ where: { name: "Alice" }, data: { profile: { disconnect: true }, }, }); // => The userId: 1 in Profiles becomes null ``` If no connected record exists, nothing happens (no error is thrown). ### oneToMany Specify `where` conditions to set the FK of related records to `null`: ```ts // oneToMany: Remove association for specific posts gassma.Users.update({ where: { name: "Alice" }, data: { posts: { disconnect: { id: 1 }, }, }, }); // => Posts id: 1 authorId becomes null // Remove multiple associations gassma.Users.update({ where: { name: "Alice" }, data: { posts: { disconnect: [{ id: 1 }, { id: 2 }], }, }, }); ``` ### manyToMany Deletes the junction table records: ```ts // manyToMany: Remove tag association gassma.Posts.update({ where: { id: 1 }, data: { tags: { disconnect: { id: 3 }, }, }, }); // => The corresponding record is deleted from the PostTags table ``` ## set Replaces all relation associations. Only available for oneToMany and manyToMany. ### oneToMany Sets all child records' FK to `null`, then sets the FK of specified records to the parent: ```ts // oneToMany: Replace Alice's posts with only id: 1 and id: 2 gassma.Users.update({ where: { name: "Alice" }, data: { posts: { set: [{ id: 1 }, { id: 2 }], }, }, }); // => All existing posts' authorId becomes null, then // id: 1 and id: 2's authorId is set to Alice's id ``` ### manyToMany Deletes all junction table records, then creates new associations with specified records: ```ts // manyToMany: Completely replace post tags gassma.Posts.update({ where: { id: 1 }, data: { tags: { set: [{ id: 10 }, { id: 11 }], }, }, }); // => All records for post id: 1 are deleted from PostTags, then // new association records are created ``` ## Combining Multiple Operations You can combine multiple relation operations within a single update: ```ts gassma.Users.update({ where: { name: "Alice" }, data: { name: "Alice Updated", posts: { create: { id: 5, title: "New Article", published: true }, update: { where: { id: 1 }, data: { title: "Updated" } }, delete: { id: 3 }, }, }, }); ``` ## Errors | Error | Cause | | --- | --- | | `NestedWriteWithoutRelationsError` | Executed Nested Write without relation definitions | | `NestedWriteInvalidOperationError` | Specified an operation not supported for the relation type | | `NestedWriteConnectNotFoundError` | Target record not found for connect / connectOrCreate | | `NestedWriteTargetNotFoundError` | No related record exists for update / delete on the non-FK side of a oneToOne | # changeSettings() (https://gassma.io/en/docs/reference/settings/changeSettings) Configure the data range of a sheet (start row and column range) Use this when you want to configure the reading range of the spreadsheet. ## Arguments | Argument Name | Description | Type | Notes | | ---------------- | --------------------------------------------------- | ------------------ | -------------------------------------------------------- | | startRowNumber | Specify the row number where column names are written | `number` | | startColumnValue | Specify the column where the first column name is | `number \| string` | Specify either the column number or the column letter | | endColumnValue | Specify the column where the last column name is | `number \| string` | Specify either the column number or the column letter | ## Specifying Columns For `startColumnValue` / `endColumnValue`, you can specify either a **column number (`number`)** or a **column letter (`string`)**. Letters are case-insensitive and interpreted with the same base-26 scheme as spreadsheet column headers. `"A"` through `"Z"` are 1 through 26, and `"AA"` rolls over into two characters. | Value | Column Number | | ------ | ------------- | | `"A"` | 1 | | `"Z"` | 26 | | `"AA"` | 27 | | `"AZ"` | 52 | | `"BA"` | 53 | ```ts // The following two are equivalent gassma.sheet1.changeSettings(1, "B", "E"); gassma.sheet1.changeSettings(1, 2, 5); ``` Specifying a string that contains non-letter characters (digits, symbols, empty string, etc.) throws `GassmaInValidColumnValueError`. To specify by number, pass a `number` rather than a string. ## When to Use If your spreadsheet is in any of the following states, you **must** call `changeSettings()` before performing any operations on the sheet. ### 1. When the Table Is Not in the Top-Left Corner ![Table not in top-left corner](./img/settingExample.png) For a table like the one above, you can read the table correctly by writing the following code. ```ts const gassma = new Gassma.GassmaClient(); // Must be called before any sheet operations gassma.sheet1.changeSettings(4, "B", "E"); const result = gassma.sheet1.findMany({}); ``` ### 2. When There Is Other Data (e.g., Notes) to the Right ![Table with other data](./img/settingExample2.png) ```ts const gassma = new Gassma.GassmaClient(); // Must be called before any sheet operations gassma.sheet1.changeSettings(1, "A", "D"); const result = gassma.sheet1.findMany({}); ``` # Global omit (https://gassma.io/en/docs/reference/config/global-omit) Exclude fields from results by default per sheet By specifying `omit` in the `GassmaClient` constructor, default field exclusion is applied to all queries for the specified sheets. ## Basic Usage ```ts const gassma = new Gassma.GassmaClient({ omit: { Users: { password: true, secret: true, }, Posts: { internalNotes: true, }, }, }); // When fetching from the Users sheet, password and secret are automatically excluded const users = gassma.Users.findMany({}); // => [{ id: 1, name: "Alice", email: "alice@example.com" }, ...] // password and secret fields are not included ``` ## Priority Global omit, query-level `omit`, and `select` have the following priority order. | Priority | Condition | Behavior | | --- | --- | --- | | 1 (highest) | `select` is specified | Ignores both global omit and query omit | | 2 | Query `omit` is specified | Merged with global omit | | 3 | Neither is specified | Global omit is applied as-is | ### Overriding Global omit with select When `select` is specified, global omit is not applied. ```ts // Global omit: { password: true } const result = gassma.Users.findMany({ select: { name: true, password: true }, }); // => [{ name: "Alice", password: "secret123" }] // select takes highest priority, so password is also retrieved ``` ### Overriding Global omit with Query omit You can disable global omit on a per-field basis by specifying `false` in the query-level `omit`. ```ts // Global omit: { password: true, secret: true } const result = gassma.Users.findMany({ omit: { password: false }, }); // => [{ id: 1, name: "Alice", password: "secret123" }] // Global omit for password is disabled; only secret is excluded ``` You can specify additional fields to exclude by setting `true` in the query `omit`. ```ts // Global omit: { password: true } const result = gassma.Users.findMany({ omit: { email: true }, }); // => [{ id: 1, name: "Alice" }] // Both password (global) and email (query) are excluded ``` ## Supported Methods Global omit is applied to the following methods. | Method | Applied | | --- | --- | | `findMany` | ✅ | | `findFirst` / `findFirstOrThrow` | ✅ | | `create` | ✅ | | `update` | ✅ | | `upsert` | ✅ | | `delete` | ✅ | | `createManyAndReturn` | ✅ | | `updateManyAndReturn` | ✅ | `createMany`, `updateMany`, and `deleteMany` return `{ count: number }`, so they are not affected by global omit. ## Combining with Relations ```ts const gassma = new Gassma.GassmaClient({ omit: { Users: { password: true }, }, relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId" }, }, }, }); // Can be used together with relations const result = gassma.Users.findMany({ include: { posts: true }, }); // => [{ id: 1, name: "Alice", posts: [...] }] // password is excluded while relation data is also retrieved ``` ## Validation | Error | Cause | | --- | --- | | `GassmaFindSelectOmitConflictError` | `select` and `omit` are specified simultaneously at the query level | # defaults (@default) (https://gassma.io/en/docs/reference/config/defaults) Auto-set field values on create with static values or functions (@default) This is the equivalent of Prisma's `@default()`. It automatically sets default values for fields during `create` operations. ## Basic Usage ```ts const gassma = new Gassma.GassmaClient({ defaults: { Users: { role: "USER", createdAt: () => new Date(), }, }, }); // Default values are automatically applied during create gassma.Users.create({ data: { name: "Alice" }, }); // => { name: "Alice", role: "USER", createdAt: 2026-03-14T... } ``` ## Static Values and Functions You can specify both fixed values and functions as default values. | Type | Example | Behavior | | --- | --- | --- | | Static value | `role: "USER"` | Sets the same value every time | | Function | `createdAt: () => new Date()` | Evaluated on each invocation | ## Applicable Methods | Method | Applied | | --- | --- | | `create` | ✅ | | `createMany` / `createManyAndReturn` | ✅ | | `upsert` (create part only) | ✅ | ## Behavior with Explicit Values If a field is explicitly specified (including `null`), the default value is not applied. ```ts gassma.Users.create({ data: { name: "Alice", role: "ADMIN" }, }); // => role is "ADMIN" (default value "USER" is not applied) ``` ## Validation of Default Values Values produced as defaults go through the same validation as values written directly in `data`. If a default returns something a cell cannot hold, a `GassmaInvalidValueError` is thrown and no rows are written. ```ts const gassma = new Gassma.GassmaClient({ defaults: { Users: { age: () => NaN }, }, }); gassma.Users.createMany({ data: [{ name: "Alice" }] }); // => Invalid value for argument `age`. Expected a finite number, but received NaN. ``` Here `{argumentName}` is the column name. `NaN` / `Infinity` / `-Infinity`, invalid Dates (Invalid Date) and objects other than `Date` are covered. For details, see the [error list](/docs/reference/errors#values-a-cell-cannot-hold). # updatedAt (@updatedAt) (https://gassma.io/en/docs/reference/config/updated-at) Auto-set timestamps on create/update (@updatedAt) This is the equivalent of Prisma's `@updatedAt`. It automatically sets the current timestamp on specified columns when a record is created or updated. ## Basic Usage ```ts const gassma = new Gassma.GassmaClient({ updatedAt: { Users: "updatedAt", }, }); // Current timestamp is automatically set during create / update gassma.Users.create({ data: { name: "Alice" }, }); // => { name: "Alice", updatedAt: 2026-03-14T... } gassma.Users.update({ where: { name: "Alice" }, data: { name: "Bob" }, }); // => { name: "Bob", updatedAt: 2026-03-14T... } (automatically updated) ``` ## Multiple Columns You can specify multiple columns using an array. ```ts const gassma = new Gassma.GassmaClient({ updatedAt: { Posts: ["updatedAt", "lastModified"], }, }); ``` ## Applicable Methods | Method | Applied | | --- | --- | | `create` / `createMany` / `createManyAndReturn` | ✅ | | `update` / `updateMany` / `updateManyAndReturn` | ✅ | | `upsert` (both create and update) | ✅ | ## Behavior with Explicit Values If a value is explicitly specified by the user, the explicit value takes priority. ## Notes `updatedAt` is not applied during cascading updates from `onDelete` / `onUpdate` (same behavior as Prisma). # ignore / ignoreSheets (@ignore / @@ignore) (https://gassma.io/en/docs/reference/config/ignore) Exclude fields or whole sheets from all operations (@ignore / @@ignore) This is the equivalent of Prisma's `@ignore` (field-level) and `@@ignore` (model-level). ## ignore (Field-Level) Completely excludes specified fields from all operations. ```ts const gassma = new Gassma.GassmaClient({ ignore: { Users: ["secretColumn", "internalData"], }, }); // Excluded from read results gassma.Users.findMany({}); // => [{ id: 1, name: "Alice" }] (secretColumn, internalData are not included) // Also excluded from write data gassma.Users.create({ data: { name: "Alice", secretColumn: "xxx" }, }); // => secretColumn is ignored ``` For a single column, you can specify it as a string. ```ts ignore: { Users: "secretColumn", } ``` ### Where Exclusion Applies - **Read results**: Return values of find / create / update / delete / upsert - **Write data**: data of create / createMany / upsert - **where conditions**: Also excluded from where clauses ### Difference from Global omit | | `ignore` | Global `omit` | | --- | --- | --- | | Override | Not possible | Can be disabled with `omit: \{field: false\}` | | Write exclusion | ✅ | ❌ (read only) | | where exclusion | ✅ | ❌ | ## ignoreSheets (Model-Level) Completely excludes specified sheets from the client. ```ts const gassma = new Gassma.GassmaClient({ ignoreSheets: ["Logs", "Temp"], }); // gassma.Logs → undefined (excluded) // gassma.Users → available as usual ``` For a single sheet, you can specify it as a string. ```ts ignoreSheets: "Logs", ``` # map / mapSheets (@map / @@map) (https://gassma.io/en/docs/reference/config/map) Map code-side names to spreadsheet headers and sheet names (@map / @@map) This is the equivalent of Prisma's `@map("name")` (field-level) and `@@map("name")` (model-level). It maps names in your code to names in the spreadsheet. ## map (Field-Level) Maps field names in your code to different header names in the spreadsheet. ```ts const gassma = new Gassma.GassmaClient({ map: { Users: { firstName: "名前", lastName: "名字", }, }, }); // Use English names in your code gassma.Users.create({ data: { firstName: "Alice", lastName: "Smith" }, }); // → Written to the "名前" and "名字" columns in the spreadsheet gassma.Users.findFirst({ where: { firstName: "Alice" }, }); // => \{ firstName: "Alice", lastName: "Smith" \} ``` ### Where Conversion Applies - **Write data**: Converts code names to header names in create / createMany / update / updateMany / upsert - **Read results**: Converts header names to code names in return values of find / create / update / upsert - **where conditions**: Converts code names to header names before filtering ## mapSheets (Model-Level) Maps model names in your code to sheet names in the spreadsheet. ```ts const gassma = new Gassma.GassmaClient({ mapSheets: { Users: "ユーザー一覧", Posts: "投稿データ", }, }); // Access using English names in your code gassma.Users.findMany({}); // → Internally operates on the sheet named "ユーザー一覧" ``` ### Combining with Other Options When `mapSheets` is specified, other options (`omit`, `defaults`, `updatedAt`, `ignore`, `map`, etc.) should use the code names. ```ts const gassma = new Gassma.GassmaClient({ mapSheets: { Users: "ユーザー一覧", }, defaults: { Users: { role: "USER" }, // ← Use "Users", not "ユーザー一覧" }, }); ``` # autoincrement (https://gassma.io/en/docs/reference/config/autoincrement) Auto-increment fields using LockService + PropertiesService (GAS only) This is the equivalent of Prisma's `autoincrement()`. It automatically assigns unique, monotonically increasing values during `create` operations. ## Basic Usage ```ts const gassma = new Gassma.GassmaClient({ autoincrement: { Users: "id", }, }); // id is automatically assigned during create gassma.Users.create({ data: { name: "Alice" }, }); // => \{ id: 1, name: "Alice" \} gassma.Users.create({ data: { name: "Bob" }, }); // => \{ id: 2, name: "Bob" \} ``` ## Multiple Columns You can specify multiple columns using an array. ```ts autoincrement: { Users: ["id", "seq"], } ``` ## Behavior with createMany With `createMany`, counters for all rows are reserved at once before being assigned to each row. ```ts gassma.Users.createMany({ data: [{ name: "Alice" }, { name: "Bob" }], }); // => id: 1, 2 are assigned respectively ``` ## How It Works 1. Exclusive control via `LockService.getScriptLock().waitLock(10000)` 2. Read the counter from `PropertiesService.getScriptProperties()` 3. Increment by +1 (+N for createMany) and write back 4. Release the lock This feature only works in the GAS environment because it uses GAS's `LockService` and `PropertiesService`. ## Behavior with Explicit Values If a value is explicitly specified for a field, auto-increment is skipped. ```ts gassma.Users.create({ data: { id: 100, name: "Alice" }, }); // => id is 100 (auto-increment is not applied) ``` # strictUndefinedChecks / Gassma.skip (https://gassma.io/en/docs/reference/config/strict-undefined-checks) This feature corresponds to Prisma's strictUndefinedChecks (Preview feature) and Prisma.skip. It detects unintended undefined values that slip into query inputs as runtime errors, and lets you explicitly omit a field with Gassma.skip. This feature corresponds to Prisma's `strictUndefinedChecks` (Preview feature) and `Prisma.skip`. It detects unintended `undefined` values that slip into query inputs as runtime errors, and lets you explicitly omit a field with `Gassma.skip`. ## Gassma.skip `Gassma.skip` is a symbol that, when passed as a field value in a query, treats that field as "not specified". ```ts const search: string | undefined = getSearchWord(); const users = gassma.Users.findMany({ where: { // If search is missing, the name condition itself is omitted name: search ?? Gassma.skip, }, }); ``` `Gassma.skip` can always be used regardless of whether `strictUndefinedChecks` is enabled. It is available anywhere in query inputs, including `where` / `data` / `create` / `update` / `select` / `omit` / `orderBy`. ## Enabling strictUndefinedChecks `strictUndefinedChecks` is an opt-in feature. There are two ways to enable it. ### Enable via previewFeatures (with CLI) If you are doing [local development with a Prisma schema](/docs/reference/type-generation), add `previewFeatures` to the `generator` block in `schema.prisma` (same syntax as Prisma). ```prisma generator client { provider = "prisma-client-js" output = "./generated/gassma" previewFeatures = ["strictUndefinedChecks"] } ``` When you run `npx gassma generate`, `strictUndefinedChecks: true` is automatically embedded into the generated client. The generated type definitions also accept `Gassma.skip` (via `Gassma.SkipValue`). ### Enable via the constructor (without CLI) In a setup without the CLI, specify it in the `GassmaClient` constructor. ```ts const gassma = new Gassma.GassmaClient({ strictUndefinedChecks: true, }); ``` ## Behavior When Enabled When enabled, a `GassmaUndefinedValueError` is thrown if a query input contains an **explicit `undefined`**. Nested inputs (Nested Writes, `select` inside `include`, etc.) are checked recursively. ```ts const userName = undefined; gassma.Users.deleteMany({ where: { name: userName }, }); // => GassmaUndefinedValueError: // Invalid value for argument `where.name`: explicitly `undefined` values are not allowed. ``` When disabled (the default), `undefined` is treated as "this field was not specified", as described below. An unintended `undefined` slipping into a query can silently drop a condition and widen the affected rows (in the example above, the `deleteMany` would delete every row). With this feature enabled, such bugs are detected immediately at runtime. If you want to omit a field, use `Gassma.skip` instead of `undefined`. ```ts gassma.Users.deleteMany({ where: { name: userName ?? Gassma.skip }, }); // Executed with the name condition omitted ``` ## Behavior When Disabled (Default) When `strictUndefinedChecks` is disabled, an `undefined` in a query input is treated as **"this field was not specified"**, same as Prisma. This applies everywhere in query inputs: `where` conditions, inside operators (`equals` / `gt` / `in`, etc.), inside `AND` / `OR` / `NOT`, relation filters, `orderBy`, `select` keys, and so on. ```ts // The age condition is treated as "not specified", so every row is returned gassma.Users.findMany({ where: { age: undefined }, }); ``` Likewise, passing `undefined` in `update`'s `data` means **that field is not updated** (the cell value is preserved). ```ts gassma.Users.update({ where: { id: 1 }, data: { name: undefined, age: 21 }, }); // => name keeps its original value; only age is updated to 21 ``` If the `where` conditions become empty because they consisted only of `undefined` (or `Gassma.skip`), `findMany` / `updateMany` / `deleteMany` and similar operations target **every row**. For the single-row operations `update` / `delete` / `upsert`, an empty `where` throws a `GassmaInvalidValueError` (see [update](/docs/reference/crud/update/update)). ## Recommendation: exactOptionalPropertyTypes To fully forbid assigning `undefined` at the type level as well, we recommend enabling `exactOptionalPropertyTypes` in your project's `tsconfig.json` (same as Prisma). ```json { "compilerOptions": { "exactOptionalPropertyTypes": true } } ``` With this setting, code that explicitly passes `undefined` to an optional field becomes a compile error. ## Not Usable Inside Arrays `Gassma.skip` cannot be passed as an array element. A `GassmaSkipInArrayError` is thrown regardless of whether `strictUndefinedChecks` is enabled. Use `null` or filter it out of the array beforehand. ```ts gassma.Users.findMany({ where: { id: { in: [1, Gassma.skip, 3] }, }, }); // => GassmaSkipInArrayError: // Invalid value for argument `where.id.in[1]`: Can not use `Gassma.skip` value // within array. Use `null` or filter out `Gassma.skip` values. ``` The same applies to `undefined` as an array element. If an array such as `in` / `notIn` / `AND` / `OR` / `NOT` / `orderBy` / `distinct` contains an `undefined` element, a `GassmaUndefinedValueError` is thrown regardless of whether `strictUndefinedChecks` is enabled (same behavior as Prisma). ```ts gassma.Users.findMany({ where: { id: { in: [1, undefined, 3] }, }, }); // => GassmaUndefinedValueError: // Invalid value for argument `where.id.in[1]`: explicitly `undefined` values are not allowed. ``` ## Validation | Error | Cause | | --- | --- | | `GassmaUndefinedValueError` | An explicit `undefined` is specified in a query input while `strictUndefinedChecks` is enabled. For array elements, `undefined` throws whether enabled or not | | `GassmaSkipInArrayError` | `Gassma.skip` is specified as an array element (occurs whether enabled or not) | | `GassmaInvalidValueError` | The `where` of `update` / `delete` / `upsert` became empty after removing `undefined` / `Gassma.skip` | # $extends (query) (https://gassma.io/en/docs/reference/client-extensions/query) The query component of $extends lets you register query hooks that intercept the execution of each operation. It corresponds to Prisma's client extensions (the query component of $extends). Inside a hook you can rewrite args, transform the result, or short-circuit without running the actual operation. The `query` component of `$extends` lets you register query hooks that intercept the execution of each operation. It corresponds to Prisma's client extensions (the `query` component of `$extends`). Inside a hook you can rewrite `args`, transform the result, or short-circuit without running the actual operation. To add computed fields instead, see [$extends (result)](/docs/reference/client-extensions/result). ## Basic Usage Calling `gassma.$extends({ query: {...} })` returns a **new client** with the hooks applied. The original `gassma` is left unchanged. ```ts const extended = gassma.$extends({ query: { Users: { findMany({ model, operation, args, query }) { // You can modify args before running the actual query return query(args); }, }, }, }); const users = extended.Users.findMany(); ``` ## Hook Shape A hook is a function that receives `{ model, operation, args, query }`. | Property | Description | | --- | --- | | `model` | The model name of the operation (the sheet's code name) | | `operation` | The operation name (such as `findMany`) | | `args` | The arguments passed to the operation | | `query` | A function that runs the actual operation (or the next hook) | Calling `query(args)` runs the actual operation with the given `args` and returns its result. Before calling `query`, you can rewrite `args`; you can also transform the return value of `query`, or return your own value without calling `query` to short-circuit. ```ts const extended = gassma.$extends({ query: { Users: { findMany({ args, query }) { const result = query(args); // Runs the actual findMany return result; }, }, }, }); ``` ## Target Operations Query hooks can be registered for the following 15 operations. | Category | Operations | | --- | --- | | Read | `findFirst` / `findFirstOrThrow` / `findMany` | | Create | `create` / `createMany` / `createManyAndReturn` | | Update | `update` / `updateMany` / `updateManyAndReturn` | | Upsert | `upsert` | | Delete | `delete` / `deleteMany` | | Aggregation | `count` / `aggregate` / `groupBy` | ## Structure `query` is specified in the form "model name → operation name → hook". In addition to a specific model and operation, you can use `$allOperations` to target all operations within a model, and `$allModels` to target all models. ```ts const extended = gassma.$extends({ query: { // A specific operation on a specific model Users: { findMany({ args, query }) { return query(args); }, // All operations within the model $allOperations({ operation, args, query }) { return query(args); }, }, // All models $allModels: { // A specific operation across all models findMany({ model, args, query }) { return query(args); }, // All operations across all models $allOperations({ model, operation, args, query }) { return query(args); }, }, }, }); ``` ## Composition Order When multiple hooks match a single operation, they are not overwritten; instead, **all of them are chained**. The order of the chain is as follows: - **The earlier an extension is applied, the more outer it is** (the later an extension is applied, the more inner it is, closer to the actual operation). - Within a single extension, **the more specific a hook is, the more outer it is**. The priority is `model.operation` > `model.$allOperations` > `$allModels.operation` > `$allModels.$allOperations`. When an outer hook calls `query(args)`, the next inner hook runs, and when the innermost hook calls `query(args)`, the actual operation runs. ```ts const extended = gassma .$extends({ query: { Users: { findMany({ args, query }) { Logger.log("A: outer"); return query(args); }, }, }, }) .$extends({ query: { Users: { findMany({ args, query }) { Logger.log("B: inner"); return query(args); }, }, }, }); extended.Users.findMany(); // Log output: "A: outer" → "B: inner" → the actual findMany ``` ## Runs Synchronously Because it runs on GAS, query hooks are executed **synchronously**. `query(args)` returns the result itself synchronously, not a Promise. No `async` / `await` is needed. ```ts const extended = gassma.$extends({ query: { Users: { findMany({ args, query }) { const result = query(args); // The result is returned synchronously return result; }, }, }, }); ``` ## args Is Passed by Reference `args` is passed to the hook **by reference** (it is not deep cloned). If you mutate `args` destructively, it also affects the object held by the caller. If you want to modify `args`, we recommend building a new object and passing it to `query`. ```ts const extended = gassma.$extends({ query: { Users: { findMany({ args, query }) { // Bad: mutating the passed args directly // args.where = { ...args.where, deleted: false }; // Good: build a new object and pass it const nextArgs = Object.assign({}, args, { where: Object.assign({}, args.where, { deleted: false }), }); return query(nextArgs); }, }, }, }); ``` ## Chainable `$extends` can be called repeatedly. Each call returns a new client. ```ts const extended = gassma.$extends(extensionA).$extends(extensionB); ``` ## Internal Relations of include Do Not Go Through Hooks Query hooks only target the **operation invoked at the top level**. The retrieval of related records resolved internally by `include` does not go through the hooks. ```ts const extended = gassma.$extends({ query: { Posts: { findMany({ args, query }) { // For include: { posts: true } on Users.findMany, // this Posts.findMany hook is not called return query(args); }, }, }, }); ``` ## Practical Examples ### Soft Delete Inject a default condition into the `where` of `findMany` to exclude deleted records by default. ```ts const extended = gassma.$extends({ query: { Users: { findMany({ args, query }) { const nextArgs = Object.assign({}, args, { where: Object.assign({ deleted: false }, args.where), }); return query(nextArgs); }, }, }, }); ``` ### Audit Log Use `$allOperations` under `$allModels` to record logs before and after every operation on every model. ```ts const extended = gassma.$extends({ query: { $allModels: { $allOperations({ model, operation, args, query }) { Logger.log(`${model}.${operation} start`); const result = query(args); Logger.log(`${model}.${operation} done`); return result; }, }, }, }); ``` # $extends (result) (https://gassma.io/en/docs/reference/client-extensions/result) The result component of $extends lets you add computed fields to the records in query results. It corresponds to Prisma's client extensions (the result component of $extends). You can compute new fields from existing scalar fields and include them in the result. The `result` component of `$extends` lets you add **computed fields** to the records in query results. It corresponds to Prisma's client extensions (the `result` component of `$extends`). You can compute new fields from existing scalar fields and include them in the result. To intercept the query execution itself, see [$extends (query)](/docs/reference/client-extensions/query). ## Basic Usage Calling `gassma.$extends({ result: {...} })` returns a **new client with a new result type** that includes the computed fields. The original `gassma` is left unchanged. A computed field is defined as a pair of `needs` (the scalar fields required for the computation) and `compute` (the computation function). ```ts const extended = gassma.$extends({ result: { Users: { greeting: { needs: { name: true }, compute(user) { return `Hi ${user.name}`; }, }, }, }, }); const user = extended.Users.findFirst({ where: { id: 1 } }); user.greeting; // "Hi Alice" ``` ## needs and compute | Key | Description | | --- | --- | | `needs` | Declares the **scalar fields** required for the computation, as `{ fieldName: true }` | | `compute` | Receives a record containing only the fields declared in `needs`, and returns the computed value | The record passed to `compute` contains only the fields declared in `needs`, and its **type is derived from `needs` as well**. No type annotation is needed. ```ts const extended = gassma.$extends({ result: { Users: { greeting: { needs: { name: true }, // user is typed as { name: string } compute(user) { return `Hi ${user.name}`; }, }, }, }, }); ``` Only **scalar fields** can be specified in `needs`. Relations cannot be specified. ## Added as Plain Properties Computed fields are not getters; they are added to the result as **plain properties computed on the spot**. As a result, they behave stably with `Logger.log` / `JSON.stringify` / the spread syntax. ```ts const user = extended.Users.findFirst({ where: { id: 1 } }); Logger.log(user.greeting); // "Hi Alice" JSON.stringify(user); // includes greeting const copy = { ...user }; // copy.greeting is preserved ``` ## Operations That Get Computed Fields Computed fields are added to the results of **operations that return records**. | Added | Not added | | --- | --- | | `findFirst` / `findFirstOrThrow` / `findMany` | `count` / `aggregate` / `groupBy` | | `create` / `createManyAndReturn` | `createMany` | | `update` / `updateManyAndReturn` | `updateMany` | | `upsert` / `delete` | `deleteMany` | They are not added to `createMany` / `updateMany` / `deleteMany`, which return only counts, nor to the aggregations `count` / `aggregate` / `groupBy`. ## Overriding Existing Fields If you define a computed field with the same name as an existing field, you can **override** its value. ```ts const extended = gassma.$extends({ result: { Users: { // Replace the existing name with an uppercase version name: { needs: { name: true }, compute(user) { return user.name.toUpperCase(); }, }, }, }, }); ``` ## Dependencies Between Computed Fields A computed field can depend on another computed field. Put the dependency in `needs`. ```ts const extended = gassma .$extends({ result: { Users: { fullName: { needs: { firstName: true, lastName: true }, compute(user) { return `${user.firstName} ${user.lastName}`; }, }, }, }, }) .$extends({ result: { Users: { greeting: { needs: { fullName: true }, // Depends on another computed field compute(user) { return `Hi ${user.fullName}`; }, }, }, }, }); ``` For a dependency **across chained `$extends` (a separate `$extends` call)**, the type of the dependency's value is fully applied as well. On the other hand, if you make computed fields depend on each other **within the same `$extends` call**, it works at runtime, but if the dependency's `compute` has no parameter annotation, the type of the dependency value is **not applied** (it becomes `never`). If you also need the type, add a parameter annotation to the dependency's `compute`, or split them into chained `$extends`. This is a TypeScript limitation, and Prisma behaves the same way. ## Adding to All Models with $allModels With `$allModels`, you can add a computed field common to all models. If a computed field with the same name is also defined for a specific model, the model-specific one takes precedence. ```ts const extended = gassma.$extends({ result: { $allModels: { fetchedAt: { compute() { return new Date(); }, }, }, }, }); ``` ## Working with select / omit If you specify `select`, only the **selected computed fields** are included in the result. If you do not specify `select`, all computed fields are included. ```ts const user = extended.Users.findFirst({ where: { id: 1 }, select: { greeting: true }, // Only greeting is returned }); ``` You can also exclude computed fields with `omit`. ```ts const user = extended.Users.findFirst({ where: { id: 1 }, omit: { greeting: true }, // Excludes greeting }); ``` The scalar fields specified in `needs` are read internally for `compute` even if they are not selected with `select` or are excluded with `omit` (compute still works correctly). ## Added to Nested include As Well Computed fields are also added to related records retrieved with `include`. When nested deeply, they are added to the records at each level. ```ts const result = extended.Users.findMany({ include: { posts: true, // Each post also gets the computed fields of Posts }, }); ``` For details on `include`, see [include](/docs/reference/relation/include). ## Combining with query `query` and `result` can be specified together. ```ts const extended = gassma.$extends({ query: { Users: { findMany({ args, query }) { return query(args); }, }, }, result: { Users: { greeting: { needs: { name: true }, compute(user) { return `Hi ${user.name}`; }, }, }, }, }); ``` ## Limitations Computed fields cannot be used in `where` / `orderBy` / aggregations (`count` / `aggregate` / `groupBy`). Also, only scalar fields can be specified in `needs` (relations are not allowed). ## Practical Examples ### fullName Add a `fullName` that joins `firstName` and `lastName` (when the Users sheet has `firstName` / `lastName` columns). ```ts const extended = gassma.$extends({ result: { Users: { fullName: { needs: { firstName: true, lastName: true }, compute(user) { return `${user.firstName} ${user.lastName}`; }, }, }, }, }); const user = extended.Users.findFirst({ where: { id: 1 } }); user.fullName; // "Alice Smith" ``` ### Derived Field Add a value derived from an existing field. ```ts const extended = gassma.$extends({ result: { Posts: { excerpt: { needs: { content: true }, compute(post) { return post.content.slice(0, 20); }, }, }, }, }); ``` # $transaction (Transactions) (https://gassma.io/en/docs/reference/transaction) Commit all writes inside a callback at once with $transaction. On error, not a single cell is written. maxWait / timeout / rollback options `$transaction` lets you run multiple operations together as a single transaction. It corresponds to Prisma's interactive transactions (the callback form of `$transaction`). Write operations inside the callback are not applied to the sheets immediately; they are **written to the sheets all at once when the callback finishes successfully** (commit). If the callback throws, **not a single cell is written** to the sheets. ## Basic Usage Call it as `gassma.$transaction((tx) => {...})`. The callback receives a transaction client `tx`, and the callback's return value is returned as the return value of `$transaction`. ```ts const user = gassma.$transaction((tx) => { const created = tx.Users.create({ data: { id: 1, name: "Tanaka" }, }); tx.Posts.create({ data: { id: 10, title: "Hello", authorId: created.id }, }); return created; }); // Users and Posts are written together once the callback finishes successfully ``` Because it runs on GAS, unlike Prisma, `$transaction` executes **synchronously**. Neither the callback nor the return value of `$transaction` is a Promise. No `async` / `await` is needed. ## Cancel Everything with throw If an error is thrown in the middle of the callback, all writes so far are discarded and nothing is applied to the sheets. ```ts try { gassma.$transaction((tx) => { tx.Users.create({ data: { id: 1, name: "Tanaka" } }); tx.Posts.create({ data: { id: 10, title: "Hello", authorId: 1 } }); throw new Error("cancel"); }); } catch (e) { // Not a single row has been written to Users or Posts } ``` ## The tx Client `tx` exposes the same models (sheets) as the regular client. `$extends` is also available, letting you build an extended client that applies only within the transaction. ```ts gassma.$transaction((tx) => { const extended = tx.$extends({ query: { Users: { findMany({ args, query }) { return query(args); }, }, }, }); return extended.Users.findMany(); }); ``` On the other hand, `tx` has no `$transaction` (it does not exist on the type either). Nested transactions are not supported; calling `$transaction` inside a transaction throws `GassmaNestedTransactionError`. ### Reads Inside a Transaction (read-your-writes) Reads inside a transaction (`findMany` / `include` / relation filters, etc.) **see the uncommitted changes**. Clients outside the transaction, on the other hand, do not see the changes until they are committed. ```ts gassma.$transaction((tx) => { tx.Users.create({ data: { id: 1, name: "Tanaka" } }); // Reads inside tx see the uncommitted changes const found = tx.Users.findFirst({ where: { id: 1 } }); // found // Clients outside tx do not see them until commit const outside = gassma.Users.findFirst({ where: { id: 1 } }); // null }); ``` ## Options Options can be passed as the second argument. ```ts gassma.$transaction( (tx) => { // ... }, { maxWait: 10000, timeout: 120000, rollback: false }, ); ``` | Option | Default | Description | | --- | --- | --- | | `maxWait` | `20000` (ms) | Maximum time to wait for the transaction to start (lock acquisition). Throws `GassmaTransactionLockTimeoutError` when exceeded | | `timeout` | `60000` (ms) | Maximum execution time for the whole transaction. Throws `GassmaTransactionTimeoutError` when exceeded | | `rollback` | `true` | Whether to enable the commit-time backup and automatic restore on failure (see [below](#rollback)) | ### maxWait A transaction starts only after acquiring a script lock. If another execution holds the same lock (such as another running `$transaction`), it waits up to `maxWait` milliseconds for the lock to be released, and throws `GassmaTransactionLockTimeoutError` if it still cannot be acquired. ### timeout When the elapsed time since the transaction started exceeds `timeout` milliseconds, `GassmaTransactionTimeoutError` is thrown. The check is cooperative: the elapsed time is checked **when each tx operation is called** and **right before commit**. Because the check is cooperative, an overrun cannot be detected in the middle of code that does not call tx methods (such as a long computation loop). Even in that case, it is detected at the next tx operation or at the check right before commit. ### rollback With `rollback: true` (the default), a commit proceeds as follows. 1. Duplicate the sheets to be written into temporary backups (hidden sheets named `_gassma_tx_...`) 2. Write to the sheets 3. On success, automatically delete the backups If the write fails partway through, the sheets are **automatically restored from the backups** and the original error is re-thrown. The sheets are restored on the spot, and formula cells are preserved. If even the restore fails, `GassmaTransactionRollbackError` is thrown **with the backup sheets left in place**. The error's `backupSheetNames` property contains the list of remaining backup sheet names, from which you can recover manually. Specifying `{ rollback: false }` skips this mechanism entirely (faster). However, if the write fails partway through, the sheets may be left in a partially written state. With `rollback` enabled, the cost of duplicating the backups (`copyTo`) is **proportional to the size of the sheets being written**, and it also consumes GAS's 6-minute execution limit. For transactions on large sheets or high-frequency transactions, consider `rollback: false`. ## Limitations - The lock only serializes work that goes through GASsma. It cannot stop manual edits to the spreadsheet or changes from other scripts that do not use GASsma. See [Write Atomicity and Concurrency](/docs/reference/write-atomicity) for what can happen under concurrent access. - **The lock belongs to the GASsma library, not to your project.** GASsma runs as a library, so the lock `$transaction` takes is the library's own and is shared with every script project that uses GASsma. If a transaction is running in someone else's project, yours waits for it. - If the execution is forcibly terminated (such as by GAS's 6-minute execution limit), backup sheets may be left behind. A warning is logged the next time `$transaction` runs. Leftover `_gassma_tx_...` sheets can be deleted manually after checking their contents. - Only cell values and formulas are restored (formatting and the like are not). - Settings changed at runtime with [changeSettings](/docs/reference/settings/changeSettings) are not carried over into the transaction. - The form that takes an array of operations (Prisma's sequential operations) is not supported. Only the callback form is available. - `isolationLevel` is not supported (execution is always serialized by the lock). ## Related Errors For details on `GassmaTransactionLockTimeoutError` / `GassmaTransactionTimeoutError` / `GassmaNestedTransactionError` / `GassmaTransactionRollbackError`, see the [Error List](/docs/reference/errors). # Local Development with Prisma Schema (https://gassma.io/en/docs/reference/type-generation) Generate a type-safe client from a Prisma-format schema with the gassma CLI (generate/init/validate/format) and gassma.config.ts GASsma provides the ability to auto-generate type-safe client code from Prisma-format schema files when developing GAS locally using TypeScript with tools such as clasp+esbuild. By using this feature, settings such as relation definitions, defaults, and map are auto-generated from the schema, eliminating the need to manually write GASsma-specific constructor options (`relations`, `defaults`, `updatedAt`, `ignore`, `map`, etc.). As long as you know Prisma's schema syntax, you can start developing with the same workflow as Prisma. **Without CLI (manual configuration):** ```ts import { Gassma } from "gassma"; const gassma = new Gassma.GassmaClient({ id: "SPREAD_SHEET_ID", relations: { User: { posts: { type: "oneToMany", to: "Post", field: "id", reference: "authorId", onDelete: "Cascade" }, }, Post: { author: { type: "manyToOne", to: "User", field: "authorId", reference: "id" }, }, }, defaults: { User: { role: "USER" } }, updatedAt: { Post: "updatedAt" }, map: { User: { firstName: "名前" } }, }); ``` **With CLI (auto-generated from schema):** ```ts import { GassmaClient } from "./generated/gassma/schemaClient"; // Relations, defaults, updatedAt, map, etc. are all auto-injected const gassma = new GassmaClient(); ``` ## Prerequisites Install the GASsma CLI tool with the following command: ``` $ npm i gassma ``` ## Creating a Schema File Create a `.prisma` file in your project. By default, the `./gassma` directory is searched. ``` my-project/ ├── gassma/ │ └── schema.prisma ← Write your schema here ├── package.json └── ... ``` ### Basic Syntax Define models using Prisma's syntax. Specify the output directory in the `generator` block's `output` field. ```prisma generator client { provider = "prisma-client-js" output = "./generated/gassma" } model User { id Int @id name String email String? age Int } ``` ### previewFeatures You can enable opt-in features by specifying `previewFeatures` in the `generator` block (same syntax as Prisma's `previewFeatures`). ```prisma generator client { provider = "prisma-client-js" output = "./generated/gassma" previewFeatures = ["strictUndefinedChecks"] } ``` Currently supported features: | Feature | Description | Reference | | --- | --- | --- | | `strictUndefinedChecks` | Turns explicit `undefined` in query inputs into runtime errors | [strictUndefinedChecks / Gassma.skip](/docs/reference/config/strict-undefined-checks) | When enabled, `strictUndefinedChecks: true` is embedded into the generated client JS, and the generated type definitions also accept `Gassma.skip`. ### Type Mapping Prisma types are converted to the following TypeScript types: | Prisma Type | TypeScript Type | | --- | --- | | `Int` | `number` | | `Float` | `number` | | `Decimal` | `number` | | `BigInt` | `number` | | `String` | `string` | | `Boolean` | `boolean` | | `DateTime` | `Date` | | `Json` | `string` | | `Bytes` | `string` | Adding `?` makes the field optional (`null` is allowed). ### Relation Definitions Using Prisma's `@relation` attribute, relation information is automatically extracted and injected into the generated client. ```prisma generator client { provider = "prisma-client-js" output = "./generated/gassma" } model User { id Int @id name String posts Post[] } model Post { id Int @id title String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) authorId Int } ``` The following relation settings are auto-generated from the above definition: - `User.posts`: oneToMany (User -> Post) - `Post.author`: manyToOne (Post -> User, onDelete: Cascade) #### Implicit Many-to-Many When bidirectional array references exist, an implicit Many-to-Many relation is automatically detected. ```prisma model Post { id Int @id tags Tag[] } model Tag { id Int @id name String posts Post[] } ``` In the generated client, the junction table name is automatically resolved as `_PostToTag` (model names in alphabetical order). If you name the relation like `@relation("PostTags")`, the relation name is used as the junction table name (`_PostTags`) — the same rule as Prisma. The junction table (sheet) itself can be created automatically with [migrate / db push](/docs/reference/migrate) (you can also prepare a sheet with the same name manually in the spreadsheet). If you want to change the junction table name, give the relation a name with `@relation`. ### enum Literal union types are auto-generated from Prisma's `enum` definitions. ```prisma enum Role { ADMIN USER MODERATOR } model User { id Int @id role Role } ``` Generated type: ```ts "role": "ADMIN" | "USER" | "MODERATOR" ``` #### enum @map Adding `@map` to enum members allows you to map between the name used in code and the value stored in the spreadsheet. ```prisma enum Role { admin @map("ADMIN") user @map("USER") moderator @map("MODERATOR") } ``` Generated constant: ```ts const Role = { admin: "ADMIN", user: "USER", moderator: "MODERATOR", } as const; ``` The `@map` values are used in the type definition: ```ts "role": "ADMIN" | "USER" | "MODERATOR" ``` ### @gassma.addType By writing `@gassma.addType` in a Prisma field comment (`///`), you can add union types to the field's type. ```prisma model User { /// @gassma.addType string id Int @id // Generated type: number | string /// @gassma.addType string, boolean score Int // Generated type: number | string | boolean name String // Generated type: string (normal when no comment) } ``` ### @gassma.replaceType While `@gassma.addType` creates a union with the base type, `@gassma.replaceType` replaces the base type and generates only the specified types. ```prisma model User { /// @gassma.replaceType "admin", "user", "moderator" role String } ``` Generated type: ```ts "role": "admin" | "user" | "moderator" // Does not include string ``` Priority: enum > replaceType > addType. When an enum exists, replaceType / addType are ignored. ### @default Fields with `@default()` become optional (`?`) in the generated Create input type. ```prisma model User { id Int @id @default(autoincrement()) name String isActive Boolean @default(true) createdAt DateTime @default(now()) } ``` Generated type: ```ts "isActive"?: boolean // @default(true) -> Optional "createdAt"?: Date // @default(now()) -> Optional ``` The defaults settings are automatically embedded in the generated client JS. | `@default()` | Generated JS | | --- | --- | | `@default(true)` / `@default(false)` | `true` / `false` | | `@default(0)` (number) | `0` | | `@default("USER")` (string) | `"USER"` | | `@default(ADMIN)` (enum value) | `"ADMIN"` | | `@default(active)` (enum value with `active @map("ACTIVE")`) | `"ACTIVE"` (the `@map`-applied value) | | `@default(now())` | `() => new Date()` | | `@default(uuid())` | `() => Utilities.getUuid()` | | `@default(autoincrement())` | Generated separately as an autoincrement setting | ### @updatedAt Fields with `@updatedAt` become optional in the Create input type, and the updatedAt setting is embedded in the generated client JS. ```prisma model Post { id Int @id title String updatedAt DateTime @updatedAt } ``` ### @ignore Fields with `@ignore` are completely excluded from the type definition, and the ignore setting is embedded in the generated client JS. ```prisma model User { id Int @id name String secret String @ignore // Not included in the type definition } ``` ### @map `@map("name")` defines a field name mapping. The map setting is embedded in the generated client JS. ```prisma model User { id Int @id firstName String @map("名前") lastName String @map("名字") } ``` In code, you work with `firstName` / `lastName`, which correspond to the columns named "名前" and "名字" in the spreadsheet. ### @@ignore The model-level `@@ignore` excludes an entire sheet. The ignoreSheets setting is embedded in the generated client JS. ```prisma model Logs { id Int @id message String @@ignore } ``` ### @@map The model-level `@@map("name")` maps a sheet name. ```prisma model Users { id Int @id name String @@map("ユーザー一覧") } ``` In code, you access it as `Users`, which corresponds to the sheet named "ユーザー一覧" in the spreadsheet. ## CLI Commands ### gassma generate Generates type files and client code. ``` $ npx gassma generate ``` By default, `.prisma` files in the `./gassma` directory are searched. You can specify a particular schema file or directory using the `--schema` option (equivalent to Prisma's `prisma generate --schema`). ``` $ npx gassma generate --schema gassma/user.prisma $ npx gassma generate --schema ./schemas ``` The `--watch` option monitors schema file changes and automatically regenerates. ``` $ npx gassma generate --watch ``` It can also be combined with `--schema`. The `--config` option lets you explicitly specify the path to the config file (equivalent to Prisma's `--config`). ``` $ npx gassma generate --config configs/gassma.config.ts ``` If the specified file does not exist, a `ConfigFileNotFoundError` is thrown. When omitted, the default locations are searched as before (see "Config File Search Rules" below). ### gassma init Initializes a project and auto-generates a schema file and configuration file. ``` $ npx gassma init ``` The following files are generated: - `gassma/schema.prisma` -- Initial schema - `gassma.config.ts` -- Configuration file | Option | Description | | --- | --- | | `--output ` | Customize the output path | | `--with-model` | Generate a schema with a sample User model | If `schema.prisma` already exists, it safely stops with an error. ### gassma validate Performs syntax checking and consistency checking of the schema file (equivalent to Prisma's `prisma validate`). ``` $ npx gassma validate ``` ``` $ npx gassma validate --schema gassma/test.prisma ``` You can also specify the path to the config file with the `--config` option. Check items: - Syntax errors (parser error detection) - `generator` block existence check - `output` field required check - At least one model is defined On success, the following is output: ``` The schema at /path/to/gassma/test.prisma is valid 🚀 ``` ### gassma format Formats `.prisma` files with the same formatting as the official Prisma formatter (uses `@prisma/internals`' `formatSchema`). ``` $ npx gassma format ``` | Option | Description | | --- | --- | | `--schema ` | Format only a specific file | | `--config ` | Specify the path to the config file | | `--check` | Check if already formatted (for CI; exits with code 1 if unformatted) | ### gassma studio Opens the spreadsheet configured in `datasource` in your OS's default browser. ``` $ npx gassma studio ``` | Option | Description | | --- | --- | | `--config ` | Specify the path to the config file | The URL is resolved in the following order: 1. The `url` of the `datasource` block in the schema 2. `datasource.url` in `gassma.config.ts` If `url` is a full URL (`https://...`), it is opened as-is; if it is a spreadsheet ID, `https://docs.google.com/spreadsheets/d//edit` is constructed and opened. If neither has a URL set, a `NoDatasourceUrlError` occurs. ### gassma version Displays the GASsma CLI version. ``` $ npx gassma version ``` You can also check with the `--version` / `-V` flag. | Option | Description | | --- | --- | | `--json` | Output version information as JSON | With `--json`, the version is output as JSON (`{"gassma":""}`). ``` $ npx gassma version --json {"gassma":"1.2.3"} ``` ### Generated Files The following files are generated based on the schema file name. For example, for `schema.prisma`: | File | Content | | --- | --- | | `schema.d.ts` | Type definitions (model types, query types, common types) | | `schemaClient.js` | Client implementation (with auto-injected relation definitions) | | `schemaClient.d.ts` | Client type definitions | The output directory is the directory specified by the `generator` block's `output`. ## Using the Generated Client Import `GassmaClient` from the generated client file and use it directly. Relation definitions are auto-injected. ```ts import { GassmaClient } from "./generated/gassma/schemaClient"; const gassma = new GassmaClient(); // Access sheets with type safety const users = gassma.User.findMany({ where: { age: { gte: 20 } }, select: { name: true, email: true }, }); ``` You can instantiate it using the same pattern as Prisma. ```ts // Prisma import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); // GASsma (same pattern) import { GassmaClient } from "./generated/gassma/schemaClient"; const gassma = new GassmaClient(); ``` ### Initialization with Options ```ts // Specify spreadsheet ID const gassma = new GassmaClient("SPREAD_SHEET_ID"); // Options object const gassma = new GassmaClient({ id: "SPREAD_SHEET_ID", omit: { User: { password: true }, }, }); ``` ## Configuration File (gassma.config.ts) By placing `gassma.config.ts` at the project root, you can centrally manage CLI settings (equivalent to Prisma's `prisma.config.ts`). Extensions other than TypeScript (`.js` / `.mjs` / `.cjs` / `.mts` / `.cts`) and placement in the `.config/` directory are also supported (see "Config File Search Rules" below). ### Configuration Interface There are two ways to write the configuration file. **1. Using the `defineConfig` helper (recommended):** ```ts import { defineConfig } from "gassma/config"; export default defineConfig({ schema: "gassma/schema.prisma", datasource: { url: "https://docs.google.com/spreadsheets/d/XXXXX/edit", }, }); ``` **2. Using the `satisfies` operator:** ```ts import type { GassmaConfig } from "gassma"; export default { schema: "gassma/schema.prisma", datasource: { url: "https://docs.google.com/spreadsheets/d/XXXXX/edit", }, } satisfies GassmaConfig; ``` The `GassmaConfig` type can be imported from the root of the `gassma` package. ### Configuration Options | Option | Type | Required | Description | | --- | --- | --- | --- | | `schema` | `string` | No | Path to the schema file or directory (default: `./gassma`) | | `datasource.url` | `string` | No | Spreadsheet URL or ID | ### Config File Search Rules The config file is searched in the following order, and the **first file found** is used: 1. `gassma.config.js` 2. `gassma.config.ts` 3. `gassma.config.mjs` 4. `gassma.config.cjs` 5. `gassma.config.mts` 6. `gassma.config.cts` 7. `.config/gassma.js` 8. `.config/gassma.ts` 9. `.config/gassma.mjs` 10. `.config/gassma.cjs` 11. `.config/gassma.mts` 12. `.config/gassma.cts` All extensions of `gassma.config.*` directly under the project root are searched first, followed by `gassma.*` in the `.config/` directory. This is the same order as Prisma's config file search, including `.js` taking precedence over `.ts`. ### --config Option The `generate` (including `--watch`), `validate`, `format`, and `studio` commands accept the `--config` option to explicitly specify the path to the config file (equivalent to Prisma's `--config`). ``` $ npx gassma generate --config configs/gassma.config.ts ``` - Relative paths are resolved from the current working directory. - If the specified file does not exist, a `ConfigFileNotFoundError` is thrown. - When omitted, the default locations are searched according to the search rules above. ### Load Behavior When running `gassma generate`, the following is displayed when the config file is loaded successfully: ``` ⚙️ Loaded config from gassma.config.ts ``` - If the config file has a syntax or runtime error, or if a known key (`schema` / `datasource.url`) has an invalid type, a `GassmaConfigLoadError` is thrown. - If the config contains unknown keys, a warning is displayed and those keys are ignored (no error is thrown). ``` Warning: Unknown property `outut` in /path/to/gassma.config.ts. Known properties are: schema, datasource. It will be ignored. ``` ### env() Helper Using the `env()` function, you can retrieve the spreadsheet URL from an environment variable (equivalent to Prisma's `env()`). ```ts import "dotenv/config"; import { defineConfig, env } from "gassma/config"; export default defineConfig({ schema: "gassma", datasource: { url: env("SPREADSHEET_URL"), }, }); ``` It can also be used with the `satisfies` pattern. ```ts import "dotenv/config"; import type { GassmaConfig } from "gassma"; import { env } from "gassma/config"; export default { schema: "gassma", datasource: { url: env("SPREADSHEET_URL"), }, } satisfies GassmaConfig; ``` #### Typed env() By passing an interface of your environment variables as a type argument, the names you can pass to `env()` are restricted to its keys, and you get autocompletion. ```ts import "dotenv/config"; import { defineConfig, env } from "gassma/config"; interface Env { SPREADSHEET_URL: string; } export default defineConfig({ schema: "gassma", datasource: { url: env("SPREADSHEET_URL"), }, }); ``` Only keys whose values are of type `string` (or `string | undefined`) can be specified. Specifying a nonexistent key results in a compile error. `env()` throws a `GassmaConfigEnvError` if the environment variable is not set or is an empty string. For optional environment variables, use `process.env` directly. ### datasource.url When you specify a spreadsheet URL or ID in `datasource.url`, the `id` is automatically embedded in the generated client JS. This allows you to connect to the target spreadsheet with just `new GassmaClient()`. Both full URLs and spreadsheet IDs are supported. ```ts // Full URL datasource: { url: "https://docs.google.com/spreadsheets/d/XXXXX/edit", } // Direct ID specification datasource: { url: "XXXXX", } ``` ### datasource Block in Schema You can also specify the URL by writing a `datasource` block in the schema file. ```prisma datasource db { provider = "google-spreadsheet" url = "https://docs.google.com/spreadsheets/d/XXXXX/edit" } ``` #### URL Resolution Priority 1. `datasource` block in the schema (highest priority) 2. `datasource.url` in `gassma.config.ts` ### Schema Resolution Priority 1. `--schema` option (highest priority) 2. `schema` setting in `gassma.config.ts` 3. Default `./gassma` directory Relative paths are resolved from different base directories: the `--schema` option is resolved from the current working directory, while `schema` in the config file is resolved **relative to the location of the config file** (same as Prisma). Running `gassma init` also auto-generates `gassma.config.ts`. ## Multi-file Schema When you place multiple `.prisma` files in the same directory (and subdirectories), they are automatically **merged into a single schema**. This is equivalent to Prisma's [Multi-file schema](https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema) feature. ``` gassma/ ├── schema.prisma ← Write the generator block here ├── models/ │ ├── user.prisma ← User, Profile models │ └── post.prisma ← Post, Comment models ``` The `generator` block only needs to be written in one file and is shared across all files. All models are consolidated into a single client output. ## Multiple Schemas (Multiple Spreadsheets) When working with different spreadsheets, separate schemas into different directories and generate them individually. Type names are prefixed with the schema name, so there are no conflicts even with models of the same name. ``` schemas/ ├── user/ │ └── schema.prisma → userClient.js, user.d.ts └── order/ └── schema.prisma → orderClient.js, order.d.ts ``` ```ts import { GassmaClient as UserClient } from "./generated/user/schemaClient"; import { GassmaClient as OrderClient } from "./generated/order/schemaClient"; const userGassma = new UserClient(); const orderGassma = new OrderClient(); ``` ## Overview of Generated Types The generated `.d.ts` includes the following types: - **Model types**: Type definitions for each field (`GassmaUserUse`, etc.) - **Query types**: `FindData`, `CreateData`, `UpdateData`, `DeleteData`, `UpsertData`, etc. - **Select / Omit types**: Types for field selection and exclusion - **Filter types**: `WhereUse`, `FilterConditions` (including `FieldRef` support) - **OrderBy types**: Sort conditions (relation sort, `_count` sort, nulls control included) - **Include types**: Types for relation fetching (including `_count`) - **Nested Write types**: Create, connect, update, and delete operations for related records - **Numeric operation types**: `NumberOperation` (increment / decrement / multiply / divide) - **Common types**: `FieldRef`, `GassmaClientOptions`, error class group - **Configuration types**: `DefaultsConfig`, `UpdatedAtConfig`, `IgnoreConfig`, `AutoincrementConfig`, `MapConfig`, etc. - **Controller types**: Argument and return value types for all methods # Write Atomicity and Concurrency (https://gassma.io/en/docs/reference/write-atomicity) Nested writes and cascades buffer their writes across multiple sheets; on error, not a single row is written. Cases this guarantee does not cover (API failures, concurrent edits) and how $transaction helps [Nested writes](/docs/reference/relation/nested-write) (such as `posts: { create: [...] }` inside a `create`) and `Cascade` in [onDelete](/docs/reference/relation/on-delete) / [onUpdate](/docs/reference/relation/on-update) **write to multiple sheets** in a single operation. This page explains what these operations guarantee — and what they do not. ## Nothing Is Written on Error Operations that write to multiple sheets buffer their writes internally and **apply them to the sheets all at once when the whole operation succeeds**. If an error occurs partway through, **not a single row is written**. ```ts // The child create fails gassma.Users.create({ data: { id: 4, name: "Dave", posts: { create: [{ id: 4, titel: "..." }], // error: misspelled column }, }, }); // → Error. Nothing is written to Users or Posts ``` This is the same guarantee Prisma provides by wrapping nested writes in an implicit transaction. ## Cases This Guarantee Does Not Cover ### The Spreadsheet API Fails During the Write If the Google Sheets API fails while the buffered writes are being flushed to the sheets, **the writes made up to that point remain on the sheets**. Using [$transaction](/docs/reference/transaction) with `rollback: true` (the default) restores the sheets from a backup taken before the write (see [rollback](/docs/reference/transaction#rollback) for details). ### Another Process or Person Modifies the Sheet Mid-Operation GASsma identifies the rows it updates or deletes **by position (row number)**. If someone else **inserts or deletes rows** between the moment GASsma reads the target rows and the moment it writes, GASsma may **write to a different row than intended**. ``` When GASsma reads: row 1 Alice / row 2 Bob / row 3 Carol → "update Carol on row 3" Someone deletes row 1: row 1 Bob / row 2 Carol When GASsma writes: writes to row 3 → now empty, or a different row ``` Because multi-sheet operations buffer their writes, **the window between reading and writing is longer** for them. Keep this in mind in environments where writes can happen concurrently. ## Wrap Writes in $transaction When Concurrent Writes Are Possible In a system where writes can happen concurrently, wrap them in [$transaction](/docs/reference/transaction). ```ts gassma.$transaction((tx) => { tx.Users.create({ data: { id: 4, name: "Dave", posts: { create: [{ id: 4, title: "Dave's post", published: true }], }, }, }); }); ``` `$transaction` acquires a lock, so **writes that go through GASsma are serialized with each other**. The read-then-write window described above can no longer be interrupted by another GASsma write. This lock only serializes **operations that use GASsma**. It has no effect on: - **a person editing the spreadsheet by hand** - **another script writing to the sheet without GASsma** GASsma cannot do anything about these. If the sheets may be edited by hand while your operations run, the protection has to come from the design side — for example, **arranging things so the sheets are simply not touched concurrently**, or **running your operations during hours when edits are not accepted**. # bootstrap (Local Development Environment Setup) (https://gassma.io/en/docs/reference/bootstrap) Set up a local development environment with clasp + esbuild + TypeScript + GASsma in one shot with npx gassma bootstrap `npx gassma bootstrap` is a command that sets up a local GAS development environment (clasp + esbuild + TypeScript + GASsma library) for a new project in one shot. ``` $ npx gassma bootstrap my-app # create my-app/ and set up inside it $ npx gassma bootstrap # ask for a directory first (default: gassma-project) $ npx gassma bootstrap . # set up in the current directory ``` Since it runs via `npx`, no prior installation is required. When executed, it walks you through interactive questions and completes everything from creating the target directory and the Apps Script project to build configuration, schema file generation, and dependency installation. The target directory is created by the command itself, so you do not need to prepare one in advance. Passing `.` sets up in the existing current directory. Existing files are skipped or merged instead of overwritten (see "Directory Handling" and "Idempotency" below). ## Prerequisites [clasp](https://github.com/google/clasp) must be installed and you must be logged in. ``` $ npm install -g @google/clasp $ clasp login ``` Also, enable the Apps Script API on the [Apps Script API settings page](https://script.google.com/home/usersettings) (clasp needs it to create projects). If clasp is not found, the command shows the following message and exits safely (no files are modified). ``` clasp is required but was not found in your PATH. Install it with: npm install -g @google/clasp Then log in with: clasp login ``` ## Interactive Flow You will be asked the following questions in order (the prompts are the actual wording). ### 1. Project directory? The directory to set up the project in. Defaults to `gassma-project`; entering `.` sets up in the current directory. A directory that does not exist is created, and if it exists and is not empty, a confirmation is asked before continuing (see "Directory Handling" below). If you pass a directory as a command argument (`npx gassma bootstrap my-app` or `npx gassma bootstrap .`), this question is skipped. ### 2. Project title? The title of the Apps Script project to create. Defaults to the target directory name (the name of the directory given as the argument or in question 1). ### 3. Create a new spreadsheet as well? With **Yes** (the default), a new spreadsheet is created together with a container-bound script attached to it (`clasp create-script --type sheets`). With **No**, a standalone script is created (`--type standalone`). After this, `clasp create-script` runs and generates `.clasp.json` (with `rootDir` set to `./dist`). Then the GASsma library dependency and other settings are automatically applied to `dist/appsscript.json` (see "What Gets Generated" below). If `.clasp.json` already exists, this question and `clasp create-script` are skipped (`Found an existing .clasp.json. Skipping clasp create-script.`). ### 4. Function exposure style? Choose how functions are exposed to GAS. Sample code for each style is shown before the choices. **export (recommended)** — Uses `@gassma/gas-esbuild-plugin`; exported functions become GAS global functions as-is. ```ts export const main = () => console.log("Hello GAS!"); ``` **global** (esbuild-gas-plugin style) — Uses `esbuild-gas-plugin`; functions are exposed by assigning to the `global` object. ```ts const main = () => console.log("Hello GAS!"); interface Global { main: typeof main; } declare const global: Global; global.main = main; ``` The plugin in the generated `esbuild.mjs` and the devDependencies in `package.json` are switched according to the selected style. ### 5. Linter and formatter setup? Choose the linter and formatter setup. | Choice | Description | | --- | --- | | `oxlint + oxfmt` | recommended (the default) | | `eslint + prettier` | ESLint (typescript-eslint) + Prettier | | `none` | No linter or formatter | With `--yes`, the default `oxlint + oxfmt` is selected. The devDependencies in `package.json`, the `lint` / `lint:fix` / `format` / `format:check` scripts, and the generated config files change according to the choice (see "Linter and Formatter Choice" below). ### 6. Generate a sample src/index.ts? With **Yes** (the default), sample code for the selected style is generated as `src/index.ts`. ### 7. Install dependencies now? With **Yes** (the default), dependencies are installed with the detected package manager (e.g. `Install dependencies now? (npm install)`). This question is not asked when `--skip-install` is specified. ## What Gets Generated | File | Description | | --- | --- | | `.clasp.json` | Generated by `clasp create-script` (`rootDir: ./dist`) | | `dist/appsscript.json` | GASsma library dependency, `timeZone`, `exceptionLogging: STACKDRIVER`, and `runtimeVersion: V8` are set automatically | | `package.json` | `build` / `push` / `open` / `deploy` scripts and dependencies (plus lint / format scripts depending on question 5) | | `esbuild.mjs` | Build configuration for the selected style | | `tsconfig.json` | TypeScript configuration for GAS (`@types/google-apps-script`) | | `.gitignore` | `.clasp.json` / `.clasprc.json` / `.env` / `node_modules/` / `dist/*` (except `dist/appsscript.json`) | | `.oxlintrc.json` | oxlint configuration (only if you chose `oxlint + oxfmt` in question 5) | | `eslint.config.mjs` / `.prettierrc` | ESLint / Prettier configuration (only if you chose `eslint + prettier` in question 5) | | `src/index.ts` | Sample code (only if you answered Yes to question 6) | | `AGENTS.md` | Project guidance for coding agents (commands, development flow, constraints, and a pointer to the GASsma reference) | | `gassma/schema.prisma` / `gassma.config.ts` | Equivalent to `gassma init` (schema with a sample User model and the config file) | ### dist/appsscript.json The `timeZone` is detected automatically from your environment (falls back to `America/New_York` if detection fails). The GASsma library is added as the following entry. ```json { "userSymbol": "Gassma", "libraryId": "1ZVuWMUYs4hVKDCcP3nVw74AY48VqLm50wRceKIQLFKL0wf4Hyou-FIBH", "version": "", "developmentMode": false } ``` If the existing manifest already has `exceptionLogging` or `runtimeVersion` set, those values are preserved. ### package.json Scripts | Script | Description | | --- | --- | | `build` | `node esbuild.mjs` (bundles `src/index.ts` into `dist/index.js`) | | `push` | `clasp push` | | `open` | `clasp open-script` | | `deploy` | `npm run build && npm run push` | If you chose `oxlint + oxfmt` or `eslint + prettier` in question 5, `lint` / `lint:fix` / `format` / `format:check` are added on top of these (see the next section for their contents). Note that in a newly generated `package.json`, `devDependencies` are written out sorted alphabetically (formatter checks expect them to be sorted). ### Linter and Formatter Choice The devDependencies, npm scripts, and config files that get added depend on your answer to question 5. #### oxlint + oxfmt (recommended) `oxlint@^1.76.0` and `oxfmt@^0.61.0` are added to devDependencies. | Script | Description | | --- | --- | | `lint` | `oxlint` | | `lint:fix` | `oxlint --fix` | | `format` | `oxfmt` | | `format:check` | `oxfmt --check` | `.oxlintrc.json` is generated. ```json { "plugins": ["typescript"], "categories": { "correctness": "error" }, "ignorePatterns": ["dist/**", "src/generated/**"] } ``` #### eslint + prettier `eslint@^10.8.0` / `eslint-config-prettier@^10.1.8` / `prettier@^3.9.6` / `typescript-eslint@^8.65.0` are added to devDependencies. | Script | Description | | --- | --- | | `lint` | `eslint .` | | `lint:fix` | `eslint . --fix` | | `format` | `prettier --write .` | | `format:check` | `prettier --check .` | `eslint.config.mjs` and `.prettierrc` are generated. ```js import { defineConfig, globalIgnores } from "eslint/config"; import prettier from "eslint-config-prettier/flat"; import tseslint from "typescript-eslint"; export default defineConfig([ globalIgnores(["dist/**", "src/generated/**"]), { files: ["**/*.ts"], extends: [tseslint.configs.recommended, prettier], }, ]); ``` ```json { "semi": true, "singleQuote": false, "trailingComma": "all" } ``` #### none No devDependencies, scripts, or config files are added. The above is all bootstrap sets up. The following are **not** included (add them yourself if you need them). - pre-commit hooks (husky / lint-staged and the like) - oxlint type-aware linting (`oxlint-tsgolint`) - an oxfmt config file (it is used with its defaults) ### About .gitignore `.clasp.json` and `.clasprc.json` are gitignored following the official clasp CI guide (they contain credentials and the script ID). When sharing the project with your team, restore them from your team's secret store. The same guidance is shown when setup completes. ``` Note: .clasp.json is gitignored. Restore it from your team's secret store when sharing this project. ``` ## Arguments and Options | Argument | Description | | --- | --- | | `[directory]` | Directory to set up the project in (`.` for the current directory). Omitting it prompts for one | | Option | Description | | --- | --- | | `--yes` | Answer all prompts with their default values (non-interactive mode). Without an argument, `./gassma-project` is created and set up, and the non-empty directory confirmation is answered with continue | | `--skip-install` | Skip dependency installation | | `--dry-run` | Show planned actions without writing files, creating directories, or running commands (directory creation is also shown as a plan entry like `create directory my-app`) | In a non-interactive terminal (such as CI), `--yes` is required. Without it, the command exits with `An interactive terminal is required. Run with --yes for non-interactive mode.` ## Behavior Details ### Directory Handling - If the specified directory does not exist, it is created and the project is set up inside it. - If it exists and is not empty, `Directory "my-app" is not empty. Continue?` (default **No**) is asked. Choosing No exits safely with `Bootstrap cancelled.` without changing anything. With `--yes`, the command continues automatically. - Because of this, an interrupted setup can be resumed simply by running the same command again and answering Yes to the confirmation (already generated files are skipped/merged as described in "Idempotency" below). - If a **non-directory file** with the same name already exists, the command exits with the error `"my-app" already exists and is not a directory.` ### Idempotency The command is designed to be safe to re-run. - If `.clasp.json` exists, `clasp create-script` is skipped. - `esbuild.mjs` / `tsconfig.json` / `src/index.ts` / `gassma/schema.prisma` / `AGENTS.md` are skipped if they already exist. - The linter / formatter config files (`.oxlintrc.json` / `eslint.config.mjs` / `.prettierrc`, generated only for the choice made in question 5) are likewise skipped if they already exist. - If `package.json` already exists, the bootstrap settings are **merged** into it (existing values win). - If `.gitignore` already exists, only the missing entries are appended. - If `dist/appsscript.json` already has the GASsma library entry, it is not added twice. ### GASsma Library Version Resolution The latest GASsma library version written to `dist/appsscript.json` is resolved automatically at run time. 1. Fetch the latest library version number via `clasp list-versions` 2. If that fails, fetch it from GASsma's `package.json` on GitHub If both fail (e.g. offline), adding the library entry is skipped and manual instructions for adding it in the Apps Script editor (including the script ID) are shown. Re-running `gassma bootstrap` while online adds the entry automatically. ### Package Manager Auto-Detection npm / pnpm / yarn / bun are detected automatically (falling back to npm), and the install command and the completion guidance reflect the detected package manager. ## Next Steps After Setup When setup completes, the following steps are shown. 1. Edit `gassma/schema.prisma` to define your models 2. Run `npx gassma generate` to generate the typed client 3. Run `npm run deploy` to build and push to Apps Script 4. Run `npm run open` to open the project in the Apps Script editor For how to write schemas and the details of `gassma generate`, see [Local Development with Prisma Schema](/docs/reference/type-generation). # fields (Column Comparison) (https://gassma.io/en/docs/reference/fields) Compare columns within the same row in where filters using FieldRef Use the `fields` property within `where` conditions when you want to compare against **the value of another column in the same row** instead of a fixed value. ## Basic Usage Obtain a `FieldRef` from the `fields` property of each sheet controller and pass it as a value in filter conditions. ```ts const gassma = new Gassma.GassmaClient(); const userSheet = gassma.Users; // Search for users where firstName equals lastName const result = userSheet.findMany({ where: { firstName: { equals: userSheet.fields.lastName }, }, }); ``` The above example compares `firstName` and `lastName` values for each row, returning only rows where they match. ## Available Operators `FieldRef` can be used with the following operators: | Operator | Description | Example | | --- | --- | --- | | equals | Equal to | `{ equals: sheet.fields.otherColumn }` | | lt | Less than | `{ lt: sheet.fields.maxValue }` | | lte | Less than or equal to | `{ lte: sheet.fields.maxValue }` | | gt | Greater than | `{ gt: sheet.fields.minValue }` | | gte | Greater than or equal to | `{ gte: sheet.fields.minValue }` | | contains | Contains the string | `{ contains: sheet.fields.keyword }` | | startsWith | Starts with the string | `{ startsWith: sheet.fields.prefix }` | | endsWith | Ends with the string | `{ endsWith: sheet.fields.suffix }` | `FieldRef` cannot be used with `not`, `in`, or `notIn`. ## Numeric Comparison ```ts // Search for users where age is less than maxAge const result = userSheet.findMany({ where: { age: { lt: userSheet.fields.maxAge }, }, }); ``` ## String Comparison ```ts // Search for records where fullName contains the firstName value const result = userSheet.findMany({ where: { fullName: { contains: userSheet.fields.firstName }, }, }); ``` ## Combining with mode: "insensitive" `FieldRef` can be combined with `mode: "insensitive"` for case-insensitive comparison: ```ts const result = userSheet.findMany({ where: { firstName: { equals: userSheet.fields.lastName, mode: "insensitive", }, }, }); ``` ## Usage with AND / OR / NOT `FieldRef` can also be used within logical operators: ```ts const result = userSheet.findMany({ where: { OR: [ { firstName: { equals: userSheet.fields.lastName } }, { age: { gt: userSheet.fields.minAge } }, ], }, }); ``` ## Supported Methods `fields` can be used with all methods that support `where`: - `findMany` / `findFirst` / `findFirstOrThrow` - `update` / `updateMany` / `updateManyAndReturn` - `delete` / `deleteMany` - `upsert` - `count` / `aggregate` / `groupBy` ## When Referenced Column Doesn't Exist If the column name specified in `FieldRef` does not exist in the sheet, the condition will not match (no error is thrown). # migrate / db push (Syncing Sheets) (https://gassma.io/en/docs/reference/migrate) Generate a GAS function that syncs your spreadsheet's sheets and columns with the schema using npx gassma migrate / npx gassma db push `npx gassma migrate` and `npx gassma db push` are commands that generate a GAS function that syncs your spreadsheet's sheets and columns with the Prisma schema (the counterparts of Prisma's `prisma migrate dev` / `prisma db push`). ``` $ npx gassma migrate # generate and record a trail (migrations/) $ npx gassma migrate --name add_tags # name the trail entry $ npx gassma db push # generate without recording a trail ``` The only difference between the two commands is **whether a trail (the `migrations/` directory) is recorded**. `migrate` leaves a `migration.js` under `migrations/` every time the schema changes, while `db push` never touches `migrations/`. The generated runnable stub is identical for both. Use `db push` when you do not need a migration history. The commands themselves do not access the spreadsheet. The sheets are synced the moment you run the generated `gassmaMigrate` function once on the Apps Script side. `clasp push` is not run automatically either (see "After Generating" below). ## What Gets Generated A runnable stub `gassma-migration.js` (plain JS) is generated in the output directory. Its content is a single `gassmaMigrate` function that calls the GASsma library's `migrateSheets`, with the sheet and column names extracted from the schema embedded in it. ```prisma model User { id Int @id name String posts Post[] } model Post { id Int @id title String author User @relation(fields: [authorId], references: [id]) authorId Int } ``` The schema above generates the following. ```js function gassmaMigrate() { Gassma.migrateSheets({ spreadsheetId: "XXXXX", models: [ { name: "User", columns: ["id", "name"] }, { name: "Post", columns: ["id", "title", "authorId"] } ] }); } ``` - The spreadsheet ID is resolved from the `datasource` block in the schema first, then from `datasource.url` in `gassma.config.ts`. If neither is set, it is not embedded and the spreadsheet bound to the script is targeted at runtime. - Running the function requires the GASsma library to be registered in the GAS project (an environment set up with [bootstrap](/docs/reference/bootstrap) works as-is). The call symbol (the `Gassma.` part in the example) is automatically resolved from the `userSymbol` of the library registered in `appsscript.json` in the output directory (falling back to `Gassma` when not found). ### Output Directory Resolution 1. `--output ` (highest priority) 2. `rootDir` in `.clasp.json` in the current directory If neither is available, a `MigrateOutputDirError` is raised. ### Sheet and Column Extraction Rules - One sheet per model. When `@@map` / `@map` are used, the mapped physical names are used. - Only scalar fields become columns. Relation fields (`posts` / `author` in the example above) do not become columns, while foreign key columns (`authorId`) do. - Fields and models with `@ignore` / `@@ignore` are **also included as creation targets**. As in Prisma, they are only excluded from the client and still exist physically in the spreadsheet. - Junction sheets for [implicit Many-to-Many](/docs/reference/type-generation#implicit-many-to-many) relations are also created (e.g. `_PostToTag`, with columns `postId`, `tagId` in alphabetical order of the model names). ## After Generating Run the two Next steps shown on success, and the sheets get synced. ``` ✅ Migration generated Next steps: 1. Run "clasp push" (or "npm run push") to upload gassma-migration.js 2. In the Apps Script editor, run the "gassmaMigrate" function once ``` Push with `clasp push` directly (or `npm run push`, which only calls `clasp push`). A full clean build (such as `npm run deploy` generated by bootstrap) may wipe the output directory and delete `gassma-migration.js` before it is pushed. ## Sync Rules The sync performed by `gassmaMigrate` (`Gassma.migrateSheets`) is idempotent and safe to run any number of times. - Sheets that are in the schema but not in the spreadsheet are created, with the headers written to row 1. - On existing sheets, only the missing columns are appended to the right end of the header row. **Existing columns are never reordered**, and data rows are never written to. - Columns and sheets that are not in the schema are not deleted by default; a warning is logged and they are left untouched. ``` Gassma.migrateSheets: column "legacy" on sheet "User" is not in the schema. It is left untouched. ``` ## Deleting Data (--accept-data-loss) Pass `--accept-data-loss` to delete columns and sheets that are not in the schema. `acceptDataLoss: true` is embedded in the stub, and the deletion is performed when `gassmaMigrate` runs. Columns and sheets that still contain data are deleted after a warning that reports how much is left (the number of non-empty cells for a column, the number of data rows for a sheet). Empty ones are deleted without a warning. ``` Gassma.migrateSheets: You are about to drop the column "legacy" on the sheet "User", which still contains 12 non-empty values. ``` ## Migration Trail (migrations/) `migrate` creates `[_name]/migration.js` under the `migrations/` directory next to the schema. Its content is identical to the runnable stub. ``` gassma/ ├── schema.prisma └── migrations/ ├── 20260801120000_init/ │ └── migration.js └── 20260802093000_add_tags/ └── migration.js ``` - If the content is the same as the latest trail entry, no new entry is created (`Already in sync, no schema change or pending migration was found.` is shown). The runnable stub itself is rewritten every time. - `--name` names the trail entry. The name is sanitized by splitting camelCase, lowercasing, and replacing runs of non-alphanumeric characters with `_` (e.g. `--name "Add UserRole!"` → `20260801120000_add_user_role`). ## Options ### migrate | Option | Description | | --- | --- | | `--name ` | Name for the new migration | | `--output ` | Directory to write `gassma-migration.js` (defaults to `rootDir` in `.clasp.json`) | | `--schema ` | Path to a specific `.prisma` file to migrate from | | `--config ` | Custom path to your GASsma config file | | `--accept-data-loss` | Delete sheets and columns that are not in the schema | ### db push | Option | Description | | --- | --- | | `--output ` | Directory to write `gassma-migration.js` (defaults to `rootDir` in `.clasp.json`) | | `--schema ` | Path to a specific `.prisma` file to push from | | `--config ` | Custom path to your GASsma config file | | `--accept-data-loss` | Delete sheets and columns that are not in the schema | ## Limitations - The header row is assumed to be **row 1 starting at column A** on every sheet. Header positions moved via [changeSettings](/docs/reference/settings/changeSettings) are not supported. - Since a spreadsheet must contain at least one sheet, the last remaining sheet is never deleted even with `--accept-data-loss`; only a warning is logged. ## Gassma.migrateSheets (Library API) `Gassma.migrateSheets`, which the stub calls, is a public GASsma API. You can also call it directly with the sheets and columns you want to sync, without using the CLI. ```ts Gassma.migrateSheets({ spreadsheetId: "SPREAD_SHEET_ID", // defaults to the bound spreadsheet models: [{ name: "User", columns: ["id", "name"] }], acceptDataLoss: false, }); ``` `models` is required; omitting it raises a `GassmaMissingArgumentError`. The sync rules and limitations are the same as when going through the CLI. # raw (Writing Formulas) (https://gassma.io/en/docs/reference/raw) Use Gassma.raw to opt out of formula-injection escaping per cell and write a formula as-is `Gassma.raw(value)` is a helper that skips the automatic formula-injection escaping for that cell only and writes the value to the sheet **as-is**. When you pass a string starting with `=`, the cell holds a **live spreadsheet formula**. ## Default Protection (Automatic Escaping) When writing, GASsma escapes any string starting with `=`, `+`, `-`, or `@` by prepending a `'` (single quote). This prevents strings like `=IMPORTRANGE(...)` slipped into form input from being executed as formulas (formula injection). - Only **strings** are escaped. Numbers, booleans, and Dates are written unchanged. However, `NaN` / `Infinity` / `-Infinity` and invalid Dates (Invalid Date) are rejected with a `GassmaInvalidValueError` before escaping — the write itself fails. - The `'` does not appear in the spreadsheet's display or when reading, so reading an escaped cell back through GASsma returns the original string (e.g. `"=1+2"`). Since this protection is always on, it gets in the way when you intentionally want to write an aggregation formula. `Gassma.raw` is the opt-out for that. ## Basic Usage Wrap a `data` value in `Gassma.raw()` and that cell alone skips escaping. The main use case is writing an aggregation formula into a number column. ```ts const gassma = new Gassma.GassmaClient(); gassma.Report.create({ data: { title: userInput, // escaped as usual total: Gassma.raw("=SUM(B2:B10)"), // this cell alone is written as a formula }, }); ``` Within the same write, columns that do not use `Gassma.raw()` (like `title` above) remain protected as before. It can be used with all `create` and `update` methods (`create` / `createMany` / `createManyAndReturn` / `update` / `updateMany` / `updateManyAndReturn` / `upsert`), and with nested write `create` / `createMany` / `connectOrCreate`'s `create`. ## The Return Value Is Not the Computed Result The return value of `create` / `update` etc. is an **echo of what was written**. GASsma does not re-read the sheet after writing, so the **computed result of the formula is not returned**. If you write a formula into a number column, the return value is the formula **string**. ```ts const created = gassma.FormulaCell.create({ data: { id: 4, label: "delta", amount: 60, total: Gassma.raw("=C5*2") }, }); created.total; // "=C5*2" (the formula string, not the computed result) ``` If you need the computed result, read the record back after writing. ```ts const readBack = gassma.FormulaCell.findFirstOrThrow({ where: { id: 4 } }); readBack.total; // 120 (the result computed on the cell) ``` ## The Effect of raw Is Limited to That One Cell The effect of `Gassma.raw()` is limited to **the exact cell** it was passed for. - Other columns in the same row are escaped as usual. - Other writes referencing a raw cell — such as FK values handed down to child rows in nested writes — are never treated as raw either. ```ts gassma.FormulaCell.create({ data: { id: 4, label: "=1+2", // escaped; stays the literal string "=1+2" amount: 60, total: Gassma.raw("=C5*2"), // written as a formula and computed on the cell }, }); ``` ## Combining with $transaction It can also be used inside [$transaction](/docs/reference/transaction). Raw values are written to the sheet together at commit time, and become formulas at that point. Reads inside the tx before commit (read-your-writes) still see the **formula string**, because nothing has been written to the sheet yet. ```ts gassma.$transaction((tx) => { tx.FormulaCell.create({ data: { id: 4, label: "delta", amount: 60, total: Gassma.raw("=C5*2") }, }); const buffered = tx.FormulaCell.findFirstOrThrow({ where: { id: 4 } }); buffered.total; // "=C5*2" (not computed yet — before commit) }); // After commit the cell holds a live formula and the computed result can be read const readBack = gassma.FormulaCell.findFirstOrThrow({ where: { id: 4 } }); readBack.total; // 120 ``` ## Types `Gassma.raw(value: string)` returns a `Gassma.RawValue`. - In the generated types, **every column** in `data` accepts `Gassma.RawValue`. Since a formula can produce a value of any type on the cell, you can pass it to number, boolean, and Date columns as well. - It cannot be used in `where`. It is write-only. Passing it as a `where` value throws a `GassmaInvalidValueError` (Expected a scalar value, but received a Gassma.raw value.). ## Never Use It with Untrusted Input **Never pass user input** to `Gassma.raw()`. Because it bypasses the default protection, it makes you vulnerable to formula injection. ```ts // DANGEROUS: passing form input straight into raw function onFormSubmit(e) { const gassma = new Gassma.GassmaClient(); gassma.Answers.create({ data: { // If a malicious user enters "=IMPORTRANGE(...)", // it will be executed as a formula name: Gassma.raw(e.namedValues["名前"][0]), }, }); } ``` Use `Gassma.raw()` only with **trusted values**, such as fixed formulas you wrote yourself. Pass user input as-is without wrapping it in `Gassma.raw()`, and the default protection applies. # Error List (https://gassma.io/en/docs/reference/errors) List of error classes thrown by GASsma and when they occur A list of error classes that can occur in GASsma. ## Catching Errors GASsma's error classes are exported from the `Gassma` namespace. You can catch them with `try` / `catch` and determine the error type with `instanceof`. ```ts try { gassma.sheet1.findFirst({ take: 5 }); } catch (e) { if (e instanceof Gassma.GassmaFindFirstTakeError) { // Handle an invalid take for findFirst } } ``` There are 51 exported error classes; together with `GassmaClient` / `GassmaController` / `FieldRef` / `skip`, the `Gassma` namespace exposes 55 public entities. This `instanceof` limitation applies to built-in types such as `Date`, which evaluate to `false` across the library boundary (see [Basic](/docs/reference/basic)). GASsma's error classes, on the other hand, are referenced via the `Gassma` namespace (the library's global), so they can be checked correctly with `instanceof`. ## Search / Query Errors | Error | Message | Trigger Condition | | --- | --- | --- | | `GassmaFindSelectOmitConflictError` | Cannot use both select and omit in the same query | `select` and `omit` are specified at the same time | | `NotFoundError` | An operation failed because it depends on one or more records that were required but not found. | No record found with `findFirstOrThrow` | | `GassmaSkipNegativeError` | Invalid value for skip argument: Value can only be positive, found: \{value\} | A **finite** negative number is specified for `skip` (also applies to `skip` in `include`). `NaN` / `Infinity` / `-Infinity` / `null` raise `GassmaInvalidValueError` instead | | `GassmaLimitNegativeError` | Invalid value for limit argument: Value can only be positive, found: \{value\} | A **finite** negative number is specified for `limit`. `NaN` / `Infinity` / `-Infinity` / `null` raise `GassmaInvalidValueError` instead | | `GassmaFindFirstTakeError` | The 'findFirst' operation cannot be used with a 'take' argument that isn't 1 or -1 | A value other than 1 / -1 is specified for `take` in `findFirst` (including `NaN` / `Infinity` / `-Infinity`). Only `take: null` raises `GassmaInvalidValueError` | ## strictUndefinedChecks / Gassma.skip Errors For details, see [strictUndefinedChecks / Gassma.skip](/docs/reference/config/strict-undefined-checks). | Error | Message | Trigger Condition | | --- | --- | --- | | `GassmaUndefinedValueError` | Invalid value for argument \`\{path\}\`: explicitly \`undefined\` values are not allowed. | An explicit `undefined` is specified in a query input while `strictUndefinedChecks` is enabled. For array elements (`in` / `AND` / `OR` / `orderBy`, etc.), `undefined` throws whether enabled or not | | `GassmaSkipInArrayError` | Invalid value for argument \`\{path\}\`: Can not use \`Gassma.skip\` value within array. Use \`null\` or filter out \`Gassma.skip\` values. | `Gassma.skip` is specified as an array element (occurs whether `strictUndefinedChecks` is enabled or not) | ## orderBy Errors | Error | Message | Trigger Condition | | --- | --- | --- | | `RelationOrderByUnsupportedTypeError` | Cannot use orderBy on "\{relationName\}" (type: \{relationType\}). Only manyToOne and oneToOne are supported. | Field sort is used on a oneToMany / manyToMany relation | | `RelationOrderByCountUnsupportedTypeError` | Cannot use \_count orderBy on "\{relationName\}" (type: \{relationType\}). Only oneToMany and manyToMany are supported. | `_count` sort is used on a manyToOne / oneToOne relation | ## Aggregation Errors | Error | Message | Trigger Condition | | --- | --- | --- | | `GassmaAggregateMaxError` | Cannot produce a maximum value of more than one type. | Mixed types in `_max` | | `GassmaAggregateMinError` | Cannot produce a maximum value of more than one type. | Mixed types in `_min` | | `GassmaAggregateSumError` | Cannot produce a maximum value of more than one type. | Non-numeric types mixed in `_sum` | | `GassmaAggregateAvgError` | Cannot produce a maximum value of more than one type. | Non-numeric types mixed in `_avg` | | `GassmaAggregateTypeError` | Only "number", "string", "boolean", and "Date" types are supported. | Unsupported type in `_max` / `_min` | | `GassmaAggregateSumTypeError` | Only "number" type is supported. | Non-numeric type in `_sum` | | `GassmaAggregateAvgTypeError` | Only "number" type is supported. | Non-numeric type in `_avg` | | `GassmaAggregateSelectionRequiredError` | At least one aggregation is required: specify \`_avg\`, \`_count\`, \`_max\`, \`_min\`, or \`_sum\` with at least one field. | `aggregate` is called without any of `_avg` / `_count` / `_max` / `_min` / `_sum` pointing at a field (including calls with only `where` / `orderBy` / `take`, or empty selections such as `_count: {}`) | `GassmaAggregateMinError` / `GassmaAggregateSumError` / `GassmaAggregateAvgError` extend `GassmaAggregateMaxError`, and `GassmaAggregateAvgTypeError` extends `GassmaAggregateSumTypeError`. As a result, an `instanceof` check against a base class also catches its subclasses. ## groupBy Errors | Error | Message | Trigger Condition | | --- | --- | --- | | `GassmaGroupByHavingDontWriteByError` | When using "having" other than "\_avg", "\_count", "\_max", "\_min", and "\_sum", column names can be used only if they are written in the "by" field. | A column not included in `by` is used in `having` | ## Configuration Errors | Error | Message | Trigger Condition | | --- | --- | --- | | `GassmaInValidColumnValueError` | startColumnValue and endColumnValue can only use number, \[a-z\] and \[A-Z\]. | An invalid column value is specified in `changeSettings` | ## Argument Errors | Error | Message | Trigger Condition | | --- | --- | --- | | `GassmaMissingArgumentError` | Argument \`\{argumentName\}\` is missing. | A required argument (`data` / `where` / `create` / `update` / `by`, etc.) is omitted | | `GassmaUnknownArgumentError` | Unknown argument \`\{argumentName\}\`. Did you mean \`\{suggestion\}\`? Available: \{availableArguments\} | An unknown key is used in a query input (a top-level argument, a column name in `where` / `data` / `select` / `omit` / `orderBy`, a filter operator, an update operator such as `increment`, etc.). Also thrown when a `GassmaClient` option (`map` / `defaults` / `updatedAt` / `autoincrement` / `ignore` / `omit`) refers to a column that does not exist | | `GassmaInvalidValueError` | Invalid value for argument \`\{argumentName\}\`. Expected \{expected\}. | An argument value has an unacceptable shape. There are many trigger conditions, so they are collected [below](#trigger-conditions-for-gassmainvalidvalueerror) | In the `GassmaUnknownArgumentError` message, `Did you mean ...?` only appears when a close match is found, and `Available: ...` only appears when the list of candidates is non-empty. ### Trigger Conditions for GassmaInvalidValueError The message always has the form Invalid value for argument \`\{argumentName\}\`. Expected \{expected\}.. The tables below show the `{expected}` part. #### Arguments with an invalid shape | Condition | `{argumentName}` | `{expected}` | | --- | --- | --- | | A non-array is given to `OR` / `AND` / `NOT` | `OR`, etc. | an array | | The `orderBy` value is not `"asc"` / `"desc"` (including when an array is passed) | `orderBy` | "asc" \| "desc" | | The `sort` in `orderBy` is not `"asc"` / `"desc"` | `sort` | "asc" \| "desc" | | The `nulls` in `orderBy` is not `"first"` / `"last"` | `nulls` | "first" \| "last" | | A non-object is given to a relation key in `orderBy` | the relation name | a relation orderBy object | | `select` selects no fields at all | `select` | at least one selected field | | `cursor` has no columns at all | `cursor` | at least one column | | The `where` of a single-row operation (`update` / `delete` / `upsert`) has no conditions at all | `where` | at least one condition | #### Invalid pagination values (`take` / `skip` / `limit`) | Value | `{expected}` | | --- | --- | | `NaN` / `Infinity` / `-Infinity` | a finite number, but received NaN | | `null` | a number, but received null | `take` / `skip` apply to `findMany` / `findFirst` / `count` / `aggregate` / `groupBy`, and `limit` applies to `updateMany` / `updateManyAndReturn` / `deleteMany`. `undefined` is still treated as "not specified" and ignored. For `take` in `findFirst`, the `1` / `-1` check runs first, so `NaN` / `Infinity` / `-Infinity` raise `GassmaFindFirstTakeError` (only `take: null` raises `GassmaInvalidValueError`). `take` / `skip` inside `include` raise `IncludeInvalidOptionTypeError`. #### `null` where an argument expects a structure `, but received null` is appended to `{expected}` (for example, Invalid value for argument \`where\`. Expected an object, but received null.). | `{argumentName}` | `{expected}` | | --- | --- | | `where` / `cursor` / `having` / `some` / `every` / `none` / `createMany` | an object | | `data` / `create` / `update` / `connect` / `connectOrCreate` / `set` / `deleteMany` / `updateMany` / `AND` / `OR` / `NOT` | an object or an array | | `orderBy` | an object or an array | | `distinct` / `by` | a field name or an array of field names | | `disconnect` / `delete` | a boolean or an object | | `contains` / `startsWith` / `endsWith` | a string | | `gt` / `gte` / `lt` / `lte` | a comparable value | | `increment` / `decrement` / `multiply` / `divide` | a number | | A column value in `cursor` | a scalar value | Putting `null` in an array element raises the same error (`AND: [null]`, `distinct: [null]`, `data: [null]` in `createMany`, and so on). For details, see [Handling of null](/docs/reference/crud/read/findMany#handling-of-null). #### Values a cell cannot hold This applies to write (`data`) and query (`where` / `cursor` / `having`) values. | Value | `{expected}` | | --- | --- | | `NaN` / `Infinity` / `-Infinity` | a finite number, but received NaN | | Invalid Date | a valid Date, but the provided Date object is invalid | | An array | a scalar value, but received an array | | A function | a scalar value, but received a function | | A Symbol | a scalar value, but received a symbol | | A BigInt | a scalar value, but received a bigint | | Built-in objects such as `Map` / `Set` / `RegExp` / `Error` / `Promise` | a scalar value, but received a Map | | Any other object, such as a class instance | a scalar value, but received an object | | `Gassma.raw` (in `where` / `cursor` / `having` only) | a scalar value, but received a Gassma.raw value | `Date`, `Gassma.raw` (in `data` only) and `FieldRef` are objects but can be passed as-is. For built-in objects, the name in the message is the internal type of the value (`Set` produces `but received a Set.`). #### Arithmetic operation results Raised when the **result** of `increment` / `decrement` / `multiply` / `divide` is `NaN` / `Infinity` / `-Infinity`. Here `{argumentName}` is the **column name**. | `{expected}` | | --- | | a finite number, but received Infinity | For details, see [update()](/docs/reference/crud/update/update#atomic-number-operations). ## Relation Definition Errors | Error | Message | Trigger Condition | | --- | --- | --- | | `RelationSheetNotFoundError` | Sheet "\{sheetName\}" is not found in the spreadsheet | The sheet specified in the relation definition does not exist | | `RelationMissingPropertyError` | Relation "\{relationName\}" on sheet "\{sheetName\}" is missing required property "\{property\}" | A required property is missing in the relation definition | | `RelationInvalidPropertyTypeError` | Relation "\{relationName\}" on sheet "\{sheetName\}": property "\{property\}" must be a \{expectedType\} | The property type in the relation definition is invalid | | `RelationInvalidTypeError` | Relation "\{relationName\}" on sheet "\{sheetName\}": type "\{value\}" is not valid. Must be one of: oneToMany, oneToOne, manyToOne, manyToMany | The relation `type` is invalid | | `RelationColumnNotFoundError` | Column "\{columnName\}" is not found in sheet "\{sheetName\}" | The column specified in `field` / `reference` of the relation definition does not exist | | `RelationInvalidOnDeleteError` | Relation "\{relationName\}" on sheet "\{sheetName\}": onDelete "\{value\}" is not valid. Must be one of: Cascade, SetNull, Restrict, NoAction | The `onDelete` value is invalid | | `RelationInvalidOnUpdateError` | Relation "\{relationName\}" on sheet "\{sheetName\}": onUpdate "\{value\}" is not valid. Must be one of: Cascade, SetNull, Restrict, NoAction | The `onUpdate` value is invalid | | `RelationIgnoredColumnError` | Relation "\{relationName\}" on sheet "\{sheetName\}": column "\{columnName\}" is ignored on sheet "\{ignoredSheetName\}". Ignored columns are stripped from where conditions, so relation processing (onDelete/onUpdate/nested writes) could modify all rows in sheet "\{ignoredSheetName\}". Remove "\{columnName\}" from the ignore option or remove this relation | The relation's `field` / `reference` (or `through.field` / `through.reference` for manyToMany) refers to a column listed in the `ignore` option (detected at client initialization) | ## Relation Operation Errors | Error | Message | Trigger Condition | | --- | --- | --- | | `GassmaRelationNotFoundError` | Relation "\{relationName\}" is not defined for sheet "\{sheetName\}" | An undefined relation name is specified in `include` | | `GassmaRelationDuplicateError` | Duplicate value "\{value\}" found in "\{sheetName\}.\{field\}" for a unique relation | Duplicate values exist in the target of a oneToOne / manyToOne relation | | `GassmaThroughRequiredError` | Relation "\{relationName\}" is manyToMany but "through" is not defined | `through` (junction table) is not defined for a manyToMany relation | | `RelationOnDeleteRestrictError` | Cannot delete: related records exist for relation "\{relationName\}" (onDelete: Restrict) | Attempting to delete when related records exist with `onDelete: "Restrict"` | | `RelationOnUpdateRestrictError` | Cannot update: related records exist for relation "\{relationName\}" (onUpdate: Restrict) | Attempting to update a PK when related records exist with `onUpdate: "Restrict"` | ## include Errors | Error | Message | Trigger Condition | | --- | --- | --- | | `IncludeWithoutRelationsError` | Cannot use include without defining relations in GassmaClient | `include` is used without defining relations | | `GassmaIncludeSelectConflictError` | Cannot use both include and select in the same query | `include` and `select` are used simultaneously at the top level | | `IncludeInvalidOptionTypeError` | Include "\{relationName\}": option "\{option\}" must be \{expectedType\} | The option value type in `include` is invalid | | `IncludeSelectOmitConflictError` | Include "\{relationName\}": cannot use both select and omit at the same time | `select` and `omit` are specified simultaneously within `include` | | `IncludeSelectIncludeConflictError` | Include "\{relationName\}": cannot use both select and include at the same time | `select` and `include` are specified simultaneously within `include` | ## where Relation Filter Errors | Error | Message | Trigger Condition | | --- | --- | --- | | `WhereRelationInvalidFilterError` | Filter "\{filterType\}" cannot be used on relation "\{relationName\}" of type "\{relationType\}" | An inappropriate filter is used for the relation type (e.g., using `is` on a oneToMany relation) | | `WhereRelationWithoutContextError` | Cannot use relation filters in where clause without defining relations | Relation filters are used without defining relations | ## Nested Write Errors | Error | Message | Trigger Condition | | --- | --- | --- | | `NestedWriteWithoutRelationsError` | Cannot use nested write operations without defining relations in GassmaClient | Nested Write is used without defining relations | | `NestedWriteConnectNotFoundError` | Nested write connect failed: no record found in "\{sheetName\}" | The target record is not found with `connect` / `connectOrCreate` | | `NestedWriteRelationNotFoundError` | Nested write failed: "\{fieldName\}" is not a defined relation | An undefined relation name is used in Nested Write | | `NestedWriteInvalidOperationError` | Nested write: operation "\{operation\}" is not valid for relation "\{relationName\}" of type "\{relationType\}" | An unsupported operation is used for the relation type (e.g., using `delete` on a manyToMany relation) | | `NestedWriteTargetNotFoundError` | Nested write \{operation\} failed: no record found in "\{sheetName\}" | No related record exists for a nested `update` / `delete` on the non-FK side of a oneToOne relation | ## Transaction Errors For details, see [$transaction](/docs/reference/transaction). | Error | Message | Trigger Condition | | --- | --- | --- | | `GassmaTransactionLockTimeoutError` | Transaction API error: Unable to start a transaction in the given time. The maxWait for this transaction was \{maxWaitMs\} ms. | The script lock could not be acquired within `maxWait` when starting `$transaction` | | `GassmaTransactionTimeoutError` | Transaction API error: A \{phase\} cannot be executed on an expired transaction. The timeout for this transaction was \{timeoutMs\} ms, however \{elapsedMs\} ms passed since the start of the transaction. Consider increasing the transaction timeout or doing less work in the transaction. | The elapsed time since the transaction started exceeds `timeout` (detected when a tx operation is called or right before commit) | | `GassmaNestedTransactionError` | Transaction API error: Nested transactions are not supported. Do not call $transaction inside an active transaction. | `$transaction` is called inside a transaction | | `GassmaTransactionRollbackError` | Transaction API error: The transaction failed during commit and automatic rollback also failed. The affected sheets may be in an inconsistent state. Backup sheets are preserved for manual recovery: \{backupSheetNames\} | The automatic restore from the backups also failed after a write failure during commit (the `backupSheetNames` property holds the list of remaining backup sheet names) | ## CLI Configuration File Errors Errors that occur when running CLI commands (such as `gassma generate`). | Error | Message | Trigger Condition | | --- | --- | --- | | `ConfigFileNotFoundError` | GASsmaConfigFileNotFoundError: config file not found at \{configPath\} | The config file specified with `--config` does not exist | | `GassmaConfigLoadError` | GASsmaConfigLoadError: Failed to load config file at \{configPath\}. \{detail\} | A syntax or runtime error in the config file, an invalid type for a known key (`schema` / `datasource.url`), or the config file does not export a config object | | `GassmaConfigEnvError` | Cannot resolve environment variable: \{name\}. | The environment variable referenced by `env()` is not set or is an empty string |