# GASsma ドキュメント(全文) GASsma の全ドキュメントを 1 ファイルにまとめた機械可読版です。 目次: https://gassma.io/llms.txt --- # GASsma とは (https://gassma.io/docs/intro) 概要と動機: 素の GAS でのスプレッドシート操作が保守しにくい理由(getRange の指定ミスや数式インジェクション)と、GASsma がそれをどう解決するか Google スプレッドシートの表を Node.js の ORM ライブラリの 1 つである「Prisma」のように操作できるライブラリです。 まるで ORM のようにスプレッドシートを操作し、 - より管理しやすく - よりミスを減らす - より安全に GoogleAppsScript(GAS)を書けるようにすることを目的としています。 ## 使ってみる 適当なスプレッドシートを新規作成し、以下のデータを記入してください。(シート名は sheet1 としてください) ![説明用シート](./リファレンス/img/exampleSheet.png) その後、`拡張機能` > `Apps Script`を開き、GAS を開いてください。 開いたら[こちらのページ](./installation)を見ながら GASsma をインストールしてください。 それでは先程作成したデータから以下のようにデータを抽出し、整形する方法を考えます。 1. 年齢が 25 歳以上の行を取り出す 2. 取り出した行は名前を基準に昇順で並び替える 3. 列名をキーとした連想配列に変換する これを GASsma で書いていきます。以下のように書き、`myFunction`を実行してみてください。 ```ts const gassma = new Gassma.GassmaClient(); function myFunction() { const result = gassma.sheet1.findMany({ where: { age: { gte: 25, }, }, orderBy: { name: "asc", }, }); console.log(result); } ``` 以上です。 **インスタンスを生成し、findMany メソッドを呼び出すだけで、データの抽出が可能です。** 列名もライブラリが自動で読んでくれます。 ## なぜ GASsma は必要なのか 既存の GAS でスプレッドシートを操作する際の大変なところは主に 3 つあります。 ### 1. コード管理の煩雑さ 上にあげた例を一般的な GAS で記述すると以下のようになります。 ```ts function myFunction() { const sheet = SpreadsheetApp.getActiveSpreadsheet(); const hogeSheet = sheet.getSheetByName("sheet1"); const rowLength = hogeSheet.getLastRow() - 1; if (rowLength === 0) { console.log([]); return; } // 指定した範囲からデータを抽出 const data = hogeSheet.getRange(2, 1, rowLength, 4).getValues(); // 25歳以上の行を取り出すフィルタリング const gte25Data = data.filter((row) => row[1] >= 25); // ソート const gte25DataSorted = gte25Data.sort((a, b) => (a[0] >= b[0] ? 1 : -1)); // 連想配列に変換 const gte25DataSortedDict = gte25DataSorted.map((row) => { return { name: row[0], age: row[1], pref: row[2], postNumber: row[3], }; }); console.log(gte25DataSortedDict); } ``` しかしこれはまだコードが短く、ロジックも簡単な方です。 ここから **「同じ名前があった場合は年齢の昇順で並び替える」** 等条件が増えていったり **「25 歳以上 60 歳以下かつ都道府県が東京の人の平均年齢を求める」** 等複雑な要件があるとコードが複雑になり、管理が難しくなります。 ### 2. コードのミスを起こしやすい性質 GAS のスプレッドシート操作はコードのミスを起こしやすい性質を持っています。 スプレッドシートから指定した範囲のデータを取り出す際、`getRange()`を利用します。 `getRange()` は引数に行番号や列番号を指定することで指定した範囲のセルを取り出すことができるメソッドです。 つまり、`getRange()` を利用する際は、その度にスプレッドシート上のセルの行番号や列番号を確認する必要があります。要するに数え間違いのリスクが発生します。これは `getRange()`をコード内で使えば使うほどそのリスクは上がります。 ### 3. ある程度セキュリティを意識しないといけない GoogleFormから提出されたデータをスプレッドシートに挿入する場合を考えます。 例えば以下のコードには問題があります。なんでしょうか? ```ts function myFunction(e) { // 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; // シート挿入 hogeSheet.getRange(newRow, 1, 1, 4).setValues([newValues]); } ``` 正解は悪意のあるユーザがフォームの解答欄に`=C1`のようなスプレッドシート関数を入れた時、任意の不正な処理ができてしまいます。(フォーミュラ・インジェクション)
これらの問題を解決してくれるのが本ライブラリ、**「GASsma」** です。 なお、数式インジェクションは書き込み時の自動エスケープで防ぎます。意図的に数式を書き込みたい場合は [raw](/docs/reference/raw) を参照してください。 # 導入方法 (https://gassma.io/docs/installation) GAS スクリプトエディタへの導入方法と、ローカル開発向けの npm でのインストール方法 まず、AppsScript を開いた後、ライブラリの「+」ボタンをクリックします。 ![+ボタンをクリック](./img/plusButton.png) すると下記画像のようなダイアログが表示されるので、「スクリプト ID」に下記を入力し、検索ボタンを押します。 ``` 1ZVuWMUYs4hVKDCcP3nVw74AY48VqLm50wRceKIQLFKL0wf4Hyou-FIBH ``` ![IDを入力する](./img/inputId.png) すると以下の画面が表示されるので、「追加」ボタンを押します。 ![追加ボタンを押す](./img/addLibrary.png) ライブラリ欄に「Gassma」と表示されていたら成功です! ![成功の様子](./img/installSuccess.png) ## CLI ツールのインストール clasp 等を用い、GoogleAppsScript をローカルで開発する場合、以下のコマンドで GASsma の TypeScript 向け型ファイル自動生成ツールをインストールすることができます。 詳しい使い方は[こちら](./reference/type-generation) ```bash npm i gassma ``` # 基本 (https://gassma.io/docs/reference/basic) GassmaClient の初期化、シートへのアクセス、コンストラクタオプション、共通クエリオプション(where, select, omit, orderBy, take, skip) ## インスタンス生成 もしあなたが、特定のスプレッドシート上に GAS を作成し、そのスプレッドシートを扱うのであれば以下の方法でインスタンス生成が可能です。 ```ts const gassma = new Gassma.GassmaClient(); ``` あるいは、スプレッドシートではない場所に GAS を作成した、あるいは別の場所にあるスプレッドシートを扱うのであれば、引数に対象のスプレッドシートの ID を挿入することでインスタンス生成が可能です。 ```ts const gassma = new Gassma.GassmaClient("XXXXXXXXXXXXXXXXXXX"); ``` ### オプションオブジェクトでの初期化 リレーション定義やグローバル omit など、高度な設定を行う場合はオプションオブジェクトを渡します。 ```ts const gassma = new Gassma.GassmaClient({ id: "XXXXXXXXXXXXXXXXXXX", // 省略可 relations: { // リレーション定義(詳細はリレーション定義のリファレンスを参照) }, omit: { // グローバル omit 設定(詳細はグローバル omit のリファレンスを参照) Users: { password: true }, }, }); ``` | オプション | 説明 | 参照 | | --- | --- | --- | | `id` | スプレッドシート ID(省略時はアクティブスプレッドシート) | - | | `relations` | リレーション定義 | [リレーション定義](/docs/reference/relation/definition) | | `omit` | グローバル omit 設定 | [グローバル omit](/docs/reference/config/global-omit) | | `defaults` | フィールドのデフォルト値 | [defaults](/docs/reference/config/defaults) | | `updatedAt` | 自動更新タイムスタンプ | [updatedAt](/docs/reference/config/updated-at) | | `ignore` | フィールドレベルの除外 | [ignore](/docs/reference/config/ignore) | | `ignoreSheets` | シートレベルの除外 | [ignore](/docs/reference/config/ignore) | | `map` | フィールド名のマッピング | [map](/docs/reference/config/map) | | `mapSheets` | シート名のマッピング | [map](/docs/reference/config/map) | | `autoincrement` | 自動採番 | [autoincrement](/docs/reference/config/autoincrement) | | `strictUndefinedChecks` | クエリ入力の明示的な `undefined` を実行時エラーにする | [strictUndefinedChecks / Gassma.skip](/docs/reference/config/strict-undefined-checks) | ## Date 値の判定 GASsma は GAS ライブラリとして、呼び出し元スクリプトとは別のスクリプトコンテキストで動作します。そのため、GASsma が返した `Date` 値を `instanceof Date` で判定すると `false` になります。判定には `Object.prototype.toString` を使用してください。 ```ts const user = gassma.Users.findFirst({ where: { id: 1 } }); user.createdAt instanceof Date; // => false(ライブラリ境界を越えた Date は instanceof で判定できない) Object.prototype.toString.call(user.createdAt) === "[object Date]"; // => true ``` この制約は `Date` などの**ビルトイン型**に対する `instanceof` の話です。GASsma が公開するエラークラス(`Gassma.GassmaMissingArgumentError` など)は `Gassma` 名前空間(ライブラリの global)経由で参照するため、`instanceof` で判定できます。詳しくは[エラー一覧](/docs/reference/errors)を参照してください。 # create() (https://gassma.io/docs/reference/crud/create/create) レコードを 1 件作成する。select/omit/include とネストされた書き込みに対応 該当シートに新しい 1 行を追加したい場合に利用します。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | ------- | -------------------------- | ---- | ------------------------------------------------ | | data | 登録するデータの指定 | 不可 | | | select | 戻り値の取得列の表示設定 | 可 | `omit` / `include` と同時に使用できません | | omit | 戻り値の取得列の除外設定 | 可 | `select` と同時に使用できません | | include | リレーション先の取得 | 可 | [詳細はこちら](/docs/reference/relation/include) | `data` は必須です。省略すると `GassmaMissingArgumentError`(メッセージ: Argument `data` is missing.)がスローされます。 `data` の値にはセルに保存できるスカラー値(文字列・数値・真偽値・`null`・`Date`)のみを指定できます。`Map` / `Set` / `RegExp` / クラスのインスタンス / `new String("x")` のようなラッパーオブジェクトなど、`Date` 以外のオブジェクトを渡すと `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`([raw](/docs/reference/raw) を参照)と `fields`([fields](/docs/reference/fields) を参照)は書き込み時にそのまま渡せます。その他の対象値は[エラー一覧](/docs/reference/errors#セルに保存できない値)を参照してください。 ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例に以下の行を追加したいとします。 - name => **Shibata** - age => **23** - pref => **Shimane** - postNumber => **690-8540** この場合以下のコードとなります。 ```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", }, }); ``` 戻り値は以下の形式です。 ```ts { name: 'Shibata', age: 23, pref: 'Shimane', postNumber: '690-8540' } ``` 作成された行のデータが返されます。 また、以下のように年齢を省くとその行の`age`列部分が空になります。値に `undefined` を渡した場合も省略と同じ扱いになります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.create gassma.sheet1.create({ data: { name: "Shibata", pref: "Shimane", postNumber: "690-8540", }, }); ``` 戻り値は以下の形式です。 ```ts { name: 'Shibata', age: null, pref: 'Shimane', postNumber: '690-8540' } ``` ## Nested Write リレーション定義がある場合、`data` の中にリレーション先のレコードを同時に作成・関連付けする操作を記述できます。 詳しくは [Nested Write のリファレンス](/docs/reference/relation/nested-write)を参照してください。 # createMany() (https://gassma.io/docs/reference/crud/create/createMany) 複数のレコードを一括作成し、作成件数を取得する 該当シートに複数行を同時に追加したい場合に利用します。 ## 使用できるキー | キー名 | 内容 | 省略 | | ------ | -------------------- | ---- | | data | 登録するデータの指定 | 不可 | `data` は必須です。省略すると `GassmaMissingArgumentError`(メッセージ: Argument `data` is missing.)がスローされます。 ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例に以下の行を追加したいとします。 - 1 行目 - name => **Shibata** - age => **23** - pref => **Shimane** - postNumber => **690-8540** - 2 行目 - name => **Suzuhara** - age => **25** - pref => **Tottori** - postNumber => **680-8571** この場合以下のコードとなります。 ```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", }, ], }); ``` 戻り値は以下の形式です。 ```ts { count: 1; } ``` 作成された行の数が返されます。 `createMany` では [Nested Write](/docs/reference/relation/nested-write) は利用できません。リレーション先を同時に操作したい場合は `create` を使用してください。 # createManyAndReturn() (https://gassma.io/docs/reference/crud/create/createManyAndReturn) 複数のレコードを一括作成し、作成したレコードを返す 該当シートに複数行を同時に追加し、作成された全レコードを配列で取得したい場合に利用します。 `createMany` と同じ書き込み処理を行いますが、戻り値が異なります。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | ------- | -------------------------- | ---- | ------------------------------------------------ | | data | 登録するデータの指定 | 不可 | | | select | 戻り値の取得列の表示設定 | 可 | `omit` / `include` と同時に使用できません | | omit | 戻り値の取得列の除外設定 | 可 | `select` と同時に使用できません | | include | リレーション先の取得 | 可 | [詳細はこちら](/docs/reference/relation/include) | `data` は必須です。省略すると `GassmaMissingArgumentError`(メッセージ: Argument `data` is missing.)がスローされます。 ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例に以下の行を追加したいとします。 - 1 行目 - name => **Shibata** - age => **23** - pref => **Shimane** - postNumber => **690-8540** - 2 行目 - name => **Suzuhara** - age => **25** - pref => **Tottori** - postNumber => **680-8571** この場合以下のコードとなります。 ```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", }, ], }); ``` 戻り値は以下の形式です。 ```ts [ { name: "Shibata", age: 23, pref: "Shimane", postNumber: "690-8540" }, { name: "Suzuhara", age: 25, pref: "Tottori", postNumber: "680-8571" }, ]; ``` 作成された全レコードが配列で返されます。 ## createMany との違い | メソッド | 戻り値 | | --- | --- | | `createMany` | `{ count: number }` | | `createManyAndReturn` | 作成されたレコードの配列 | また、空配列を渡した場合の挙動も異なります。 ```ts // createMany の場合 gassma.sheet1.createMany({ data: [] }); // => { count: 0 } // createManyAndReturn の場合 gassma.sheet1.createManyAndReturn({ data: [] }); // => [] ``` data に指定しなかったフィールドは `null` として返されます。 ```ts const result = gassma.sheet1.createManyAndReturn({ data: [{ name: "Shibata" }], }); // => [{ name: "Shibata", age: null, pref: null, postNumber: null }] ``` `createManyAndReturn` では [Nested Write](/docs/reference/relation/nested-write) は利用できません。リレーション先を同時に操作したい場合は `create` を使用してください。 # findMany() (https://gassma.io/docs/reference/crud/read/findMany) where、orderBy、take/skip、カーソルページネーション、distinct を使って複数のレコードを取得する 特定の条件に合致したすべての行を取り出したい場合に利用します。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | -------- | ---------------- | ---- | --------------------------------------------- | | where | 取得条件の指定 | 可 | 書かない場合や `where: {}` の場合は全ての行を取得します | | select | 取得列の表示設定 | 可 | `omit` / `include` と同時に使用できません。リレーションフィールドにオプション指定可 | | omit | 取得列の除外設定 | 可 | `select` と同時に使用できません | | include | リレーション先の取得 | 可 | [詳細はこちら](/docs/reference/relation/include) | | orderBy | ソート設定 | 可 | 指定する列が 1 つの場合、配列の省略が可能です | | take | 取得数の設定 | 可 | 負数で末尾から取得 | | skip | スキップ数の設定 | 可 | 負数はエラー | | distinct | 重複削除の設定 | 可 | 指定する列が 1 つの場合、配列の省略が可能です | | cursor | カーソル位置 | 可 | カーソルベースページネーション | ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例から以下の条件の行を取り出したいとします。 - pref => **Tokyo** この場合以下のコードとなります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { pref: "Tokyo", }, }); ``` 戻り値は以下の形式です。 ```ts [ { name: "sato", age: 31, pref: "Tokyo", postNumber: "160-0023" }, { name: "endo", age: 55, pref: "Tokyo", postNumber: "160-0023" }, ]; ``` 複数の条件を指定したい場合は以下のコードとなります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { pref: "Tokyo", 年齢: 31, }, }); ``` ## 演算子・部分一致 以上・以下や部分一致等の条件付き検索も可能です。例えば以下の条件で行を取り出したいとします。 - age => **20 以上** - age => **30 以下** この場合以下のコードとなります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, lte: 30, }, }, }); ``` 条件付き検索に関連するキーは以下の通りです。 | キー名 | 役割 | 例 | | ---------- | ---------------------------------------------- | ------------------- | | equals | 同値か | eauals: 20 | | not | 同値ではないか | not: 20 | | in | 指定したリストの中にあるか | in: [20, 21, 22] | | notIn | 指定したリストの中にないか | notIn: [23, 24. 25] | | lt | 未満 | lt: 30 | | lte | 以下 | lte: 30 | | gt | 超過 | gt: 20 | | gte | 以上 | gte: 20 | | contains | 対象データの中に指定した文字列が含まれているか | contains: "AB" | | startsWith | 対象データが指定した文字列から始まっているか | startsWith: "AB" | | endsWith | 対象データが指定した文字列で終わっているか | endsWith: "YZ" | | mode | 大文字小文字の区別設定 | mode: "insensitive" | 各演算子の値には固定値のほか、`fields` プロパティを使って同じ行の別の列の値を指定できます。詳しくは [fields のリファレンス](/docs/reference/fields)を参照してください。 ### mode: "insensitive" `equals`、`not`、`contains`、`startsWith`、`endsWith` に `mode: "insensitive"` を指定すると、大文字小文字を区別せずに比較できます。 ```ts const gassma = new Gassma.GassmaClient(); // "alice"、"Alice"、"ALICE" すべてにマッチ const result = gassma.sheet1.findMany({ where: { name: { equals: "alice", mode: "insensitive", }, }, }); ``` `contains`、`startsWith`、`endsWith` でも同様に使用できます。 ```ts // "Hello World"、"HELLO WORLD" などにマッチ const result = gassma.sheet1.findMany({ where: { title: { contains: "hello", mode: "insensitive", }, }, }); ``` `mode` を指定しない場合、またはデフォルトの `mode: "default"` の場合は大文字小文字が区別されます。 `where` の値には `NaN` / `Infinity` / `-Infinity`、不正な Date(Invalid Date)、配列(`in` / `notIn` の配列を除く)、関数、Symbol、BigInt を渡せません。渡すと `GassmaInvalidValueError` がスローされます(`cursor` / `having` も同様)。`Gassma.raw` も `where` では使用できません([raw](/docs/reference/raw) を参照)。 セルに保存できないオブジェクトも同様に渡せません。`Date` と `fields`(FieldRef)以外のオブジェクト —— `Map` / `Set` / `RegExp` / `Error` / クラスのインスタンス / `new String("x")` のようなラッパーオブジェクトなど —— はすべて `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. ``` また、値が `undefined` の条件は「指定しなかった」扱いになります。詳しくは [strictUndefinedChecks / Gassma.skip](/docs/reference/config/strict-undefined-checks) を参照してください。 ## AND, OR, NOT 複数条件での検索も可能です。 ### AND 例えば以下の条件で行を取り出したいとします。 - age => **22** - pref => **Ibaraki** AND を利用して検索する場合以下のようになります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { AND: [ { age: 22, }, { pref: "Ibaraki", }, ], }, }); ``` ### OR 例えば以下の条件で行を取り出したいとします。 - age => **22 または 40** OR を利用して検索する場合以下のようになります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { OR: [ { age: 22, }, { age: 40, }, ], }, }); ``` ### NOT 例えば以下の条件で行を取り出したいとします。 - age => **22 ではない** - age => **40 ではない** NOT を利用して検索する場合以下のようになります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { NOT: [ { age: 22, }, { age: 40, }, ], }, }); ``` ### AND, OR, NOT の重ねがけ 例えば AND の下に OR や NOT を入れることができます。この入れ子構造は GAS のコールスタックが許す限り無限に可能です。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { NOT: { AND: [ { name: "akahoshi", }, { age: 22, }, ], }, }, }); ``` ### 空の AND, OR, NOT と条件を持たないブランチ 空の `AND` / `NOT`(`[]` や `{}`)は**恒真**(全件にマッチ)、空の `OR: []` は**恒偽**(0 件)として扱われます(Prisma と同じです)。 また、条件を 1 つも生成しないブランチ(`{}` や、値が空オブジェクト・`undefined` だけのオブジェクト)は、`AND` / `OR` / `NOT` の配列から取り除かれます。その結果 `OR` の配列が空になった場合は 0 件になります。 ```ts gassma.sheet1.findMany({ where: { NOT: {} } }); // => 全件 gassma.sheet1.findMany({ where: { AND: [] } }); // => 全件 gassma.sheet1.findMany({ where: { OR: [] } }); // => [] gassma.sheet1.findMany({ where: { OR: [{ age: {} }] } }); // => [](条件ゼロのブランチが除去され、空の OR になる) gassma.sheet1.findMany({ where: { OR: [{}, { name: "akahoshi" }] } }); // => name が "akahoshi" の行のみ gassma.sheet1.findMany({ where: { NOT: [{ age: {} }] } }); // => 全件 gassma.sheet1.findMany({ where: { AND: [{ OR: [] }] } }); // => 全件 ``` `OR` には配列以外を渡せません。`OR: {}` のように配列以外を渡すと `GassmaInvalidValueError` がスローされます。 ### where でのリレーションフィルタ リレーション定義がある場合、`where` 内でリレーション先の条件を使ってフィルタリングできます(`some`、`every`、`none`、`is`、`isNot`)。 詳しくは [where リレーションフィルタのリファレンス](/docs/reference/relation/where-relation-filter)を参照してください。 ## null の扱い `null` を渡せるかどうかは「値の位置か、構造の位置か」で決まります。 ### 値の位置の `null`(有効) カラムの値として `null` を渡すのは正当な指定で、セルが空の行を検索できます。 ```ts gassma.sheet1.findMany({ where: { age: null } }); gassma.sheet1.findMany({ where: { age: { equals: null } } }); gassma.sheet1.findMany({ where: { age: { not: null } } }); ``` to-one リレーション(manyToOne / oneToOne)の `is` / `isNot` に `null` を渡すのも同様に有効です([where リレーションフィルタ](/docs/reference/relation/where-relation-filter)を参照)。`having` のカラムの値、書き込み時の `data` のカラムの値も `null` を渡せます。 ### 構造の位置の `null`(エラー) オブジェクトや配列を受け取る引数に `null` を渡すと `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. ``` 対象は、トップレベル引数(`where` / `orderBy` / `cursor` / `distinct` / `by` / `having` / `data` / `create` / `update`)、`AND` / `OR` / `NOT`、to-many リレーションフィルタ(`some` / `every` / `none`)、Nested Write の動詞(`create` / `connect` / `connectOrCreate` / `set` / `disconnect` / `delete` / `update` / `deleteMany` / `updateMany` / `createMany`)、文字列・数値の演算子(`contains` / `startsWith` / `endsWith` / `gt` / `gte` / `lt` / `lte` / `increment` / `decrement` / `multiply` / `divide`)です。配列の要素に `null` を入れた場合も同様にエラーになります。 各引数の `{expected}` の文言は[エラー一覧](/docs/reference/errors#構造を期待する引数への-null)を参照してください。 `cursor` だけは**カラムの値にも `null` を渡せません**。`cursor` はレコードを一意に特定するための指定なので、値が `null` の場合は `GassmaInvalidValueError`(Invalid value for argument \`id\`. Expected a scalar value, but received null.)になります。`{argumentName}` にはカラム名が入ります。 `select` / `include` / `omit` の直下に書いた `null` は従来どおり無視されます(そのフィールドを指定しなかった扱いになります)。 ## select 戻り値に返るデータを制限することができます。 例えば`age`と`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, }, }); ``` 戻り値は以下のようになります。 ```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" }, ]; ``` `select: {}` のように選択するフィールドが 1 つもない `select` は `GassmaInvalidValueError`(Invalid value for argument `select`. Expected at least one selected field.)になります。すべてのキーが `undefined` で空になった場合も同様です。 ### select 内でのリレーションオプション指定 リレーション定義がある場合、`select` 内のリレーションフィールドに `include` と同様のオプションを指定できます。`include` を別途指定する代わりに、`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, }, }); ``` リレーションフィールドに指定できるオプションは [include のオプション](/docs/reference/relation/include)と同じです(`select`、`where`、`orderBy`、`include`、`omit`、`take`、`skip`)。 深いネストも対応しています。 ```ts const result = gassma.Users.findMany({ select: { id: true, posts: { select: { id: true, comments: { select: { id: true, text: true }, }, }, }, }, }); ``` リレーションフィールドは `true` でも指定でき、その場合はリレーション先の全スカラー列を取得します(トップレベルの `select` と同様に、任意の深さで機能します)。 ```ts const result = gassma.Users.findMany({ select: { posts: { select: { title: true, comments: true, // comments のスカラー列をすべて取得 }, }, }, }); ``` トップレベルの `select` と `include` は同時に使用できません。リレーション先のデータが必要な場合は `select` 内でリレーションオプションを指定するか、`include` を単独で使用してください。 ## orderBy 取得した行をソートすることができます。 例えば`age`で昇順でソートする場合は以下のようになります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, }, }, orderBy: { age: "asc", }, }); ``` 指定できるデータは以下の通りです。 | キー名 | 意味 | | ------ | ---- | | asc | 昇順 | | desc | 降順 | ### null 値の並び順制御 オブジェクト形式で `nulls` オプションを指定すると、null 値の並び位置を制御できます。 ```ts const gassma = new Gassma.GassmaClient(); // null 値を最後に配置 const result = gassma.sheet1.findMany({ orderBy: { age: { sort: "asc", nulls: "last" }, }, }); // => [20, 22, 31, 40, 55, null, null] ``` | nulls の値 | 動作 | | --- | --- | | `"first"` | null 値を先頭に配置 | | `"last"` | null 値を末尾に配置 | `nulls` を指定しない場合、`asc` では null が先頭に、`desc` では null が末尾に配置されます。 `NaN` や不正な Date(Invalid Date)も null と同じ「欠損値」として扱われ、null と同じ位置に配置されます(`nulls` オプションの対象にもなります)。 また、複数ソートの条件を指定することもでき、例えば - `age`で昇順でソート - `age`の値が同じ行があればその部分は`name`の昇順でソート といったことを行いたい場合コードは以下のようになります。 ```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" }], }); ``` ※ソートの優先順位はインデックス番号の若い順となります。 `orderBy: {}` のように条件が空の場合は無視されます(並び替えは行われません)。配列内のエントリが `undefined` の除去によって空になった場合も、そのエントリだけが無視され、残りの指定で並び替えられます。 ### リレーションフィールドでのソート リレーション定義がある場合、manyToOne / oneToOne のリレーション先フィールドでソートできます。 ```ts const gassma = new Gassma.GassmaClient({ relations: { Posts: { author: { type: "manyToOne", to: "Users", field: "authorId", reference: "id", }, }, }, }); // 投稿を著者名の昇順でソート const result = gassma.Posts.findMany({ orderBy: { author: { name: "asc" } }, }); ``` FK が `null` のレコードは `asc` で先頭、`desc` で末尾に配置されます。 oneToMany / manyToMany のリレーションではフィールドソートはできません。`RelationOrderByUnsupportedTypeError` がスローされます。 リレーション名に対してオブジェクト以外の値を指定すると `GassmaInvalidValueError` がスローされます。 ```ts gassma.Posts.findMany({ orderBy: { author: new Date() } }); // => Invalid value for argument `author`. Expected a relation orderBy object. ``` リレーション先のフィールドを指定するには `orderBy: { author: { name: "asc" } }` のようにオブジェクトを渡してください。 ### _count でのソート oneToMany / manyToMany のリレーション件数でソートできます。 ```ts // 投稿数の多い順にユーザーをソート const result = gassma.Users.findMany({ orderBy: { posts: { _count: "desc" } }, }); ``` スカラーソートと組み合わせることもできます。 ```ts // 投稿数の降順 → 同数なら名前の昇順 const result = gassma.Users.findMany({ orderBy: [ { posts: { _count: "desc" } }, { name: "asc" }, ], }); ``` manyToOne / oneToOne のリレーションでは `_count` ソートはできません。`RelationOrderByCountUnsupportedTypeError` がスローされます。 ## take 取得数を指定できます。取得数はシートの上の行から順となります。 例えば条件に合致した行の中から上から 2 行を取得したい場合以下のようになります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, }, }, take: 2, }); ``` ### take に負数を指定した場合 `take` に負数を指定すると、末尾から N 件を取得します。 ```ts // 条件に合致した行の末尾 2 件を取得 const result = gassma.sheet1.findMany({ where: { age: { gte: 20 }, }, take: -2, }); ``` `take` が負数の場合、`skip` の方向も逆転します。`skip` は末尾から除外する件数になります。 ```ts // 末尾 1 件を除外した後、残りの末尾 2 件を取得 const result = gassma.sheet1.findMany({ take: -2, skip: 1, }); ``` ## skip 取得した行の中から特定行をスキップできます。 例えば条件に合致した行の中から上 1 つ目を省きたい場合、コードは以下のようになります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, }, }, skip: 1, }); ``` `skip` に有限の負数を指定すると `GassmaSkipNegativeError` がスローされます。 ### take / skip の異常値 `take` / `skip` に `NaN` / `Infinity` / `-Infinity` / `null` を渡すと `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. ``` | 値 | 挙動 | | --- | --- | | `NaN` / `Infinity` / `-Infinity` | `GassmaInvalidValueError`(`Expected a finite number, but received ...`) | | `null` | `GassmaInvalidValueError`(`Expected a number, but received null.`) | | 有限の負数 | `take` は末尾から取得、`skip` は `GassmaSkipNegativeError` | | `undefined` | 指定しなかった扱いになり無視されます | `skip: -Infinity` は以前 `GassmaSkipNegativeError` でしたが、有限かどうかの判定が先に行われるようになったため `GassmaInvalidValueError` になります。`GassmaSkipNegativeError` は**有限の**負数に対してのみスローされます。 同じ検証は `count` / `aggregate` / `groupBy` の `take` / `skip` にも適用されます。`findFirst` の `take` は[別の制限](./findFirst#take)があります。 ## cursor カーソルベースのページネーションを行えます。`cursor` にレコードを一意に特定するオブジェクトを指定すると、そのレコードを起点として取得します。 ```ts const gassma = new Gassma.GassmaClient(); // id: 3 のレコードを起点に、そこから 5 件取得 const result = gassma.sheet1.findMany({ cursor: { id: 3 }, take: 5, }); ``` `take` が正数の場合、cursor の位置から末尾方向に取得します。`take` が負数の場合、先頭から cursor の位置までを取得します。 ```ts // id: 3 を起点に、先頭方向のデータを取得 const result = gassma.sheet1.findMany({ cursor: { id: 3 }, take: -5, }); ``` `skip` と組み合わせると、cursor 位置からさらにスキップできます。 ```ts // id: 3 を起点に、1 件スキップして 5 件取得 const result = gassma.sheet1.findMany({ cursor: { id: 3 }, skip: 1, take: 5, }); ``` cursor に指定したレコードが見つからない場合は空配列が返されます。 `cursor: {}` のようにカラムが 1 つもない `cursor` は `GassmaInvalidValueError`(Invalid value for argument `cursor`. Expected at least one column.)になります。すべてのキーが `undefined` で空になった場合も同様です。また、`cursor` の値に `NaN` や不正な Date(Invalid Date)などの比較できない値を渡した場合も `GassmaInvalidValueError` がスローされます。 ### 処理順序 `where`・`orderBy`・`cursor`・`distinct`・`skip`・`take` を組み合わせた場合の実行順序は以下の通りです。 1. `where` - フィルター 2. `orderBy` - ソート 3. `take` が負数の場合は並びを反転 4. `cursor` - カーソル位置で切り出し(カーソル自身を含む) 5. `distinct` - 重複削除 6. `skip` - スキップ 7. `take` - 件数制限(負数の場合は絶対値の件数を取得し、最後に並びを正順へ戻す) 8. `select` / `omit` - フィールド整形 `distinct` は `cursor` の**後**に適用されます。カーソルで切り出した範囲の中で重複が削除されます。 ## omit 戻り値から特定の列を除外することができます。`select` の逆の動作です。 例えば`postNumber`を除外したい場合は以下のようになります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { pref: "Tokyo", }, omit: { postNumber: true, }, }); ``` 戻り値は以下のようになります。 ```ts [ { name: "sato", age: 31, pref: "Tokyo" }, { name: "endo", age: 55, pref: "Tokyo" }, ]; ``` `select` と `omit` は同時に使用できません。両方指定すると `GassmaFindSelectOmitConflictError` がスローされます。 [グローバル omit](/docs/reference/config/global-omit) を設定している場合、クエリの `omit` で `{ field: false }` を指定することでグローバル omit を上書きできます。詳しくは[クエリ omit でグローバル omit を上書き](/docs/reference/config/global-omit#クエリ-omit-でグローバル-omit-を上書き)を参照してください。 ## distinct 列名を指定し、もし値が被っている場合その行を省略できます。被っている場合は上の行のデータが優先されます。 例えば`age`の被りを省略する場合、コードは以下のようになります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findMany const result = gassma.sheet1.findMany({ where: { age: { gte: 20, }, }, distinct: ["age"], }); ``` `distinct` は `cursor` の後に適用されるため、カーソルで切り出した範囲内で重複が削除されます(上記の「処理順序」を参照)。 `take` に負数を指定した場合は反転した並びで重複削除が行われるため、残る「最初の 1 件」は末尾側のレコードになります。最終的な出力は正順に戻されます。 重複の判定は値を正規化したキーで行われます。 - Date は時刻が同じであれば別インスタンスでも同じ値とみなされます。Date と同時刻の ISO 文字列は別の値です。 - 数値の `1` と文字列の `"1"` は別の値です。 - `NaN` 同士、不正な Date(Invalid Date)同士は、それぞれ同じ値として畳まれます。`NaN`・`null`・Invalid Date は互いに別の値です。 ## include リレーション定義がある場合、リレーション先のデータを一緒に取得できます。 詳しくは[include のリファレンス](/docs/reference/relation/include)を参照してください。 # findFirst() (https://gassma.io/docs/reference/crud/read/findFirst) where 条件に合致する最初のレコードを取得する。合致するものがなければ null を返す 特定の条件に合致した最初の行を取り出したい場合に利用します。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | -------- | ---------------- | ---- | --------------------------------------------- | | where | 取得条件の指定 | 可 | 書かない場合は全ての行を取得します | | select | 取得列の表示設定 | 可 | `omit` / `include` と同時に使用できません。リレーションフィールドにオプション指定可 | | omit | 取得列の除外設定 | 可 | `select` と同時に使用できません | | include | リレーション先の取得 | 可 | [詳細はこちら](/docs/reference/relation/include) | | orderBy | ソート設定 | 可 | 指定する列が 1 つの場合、配列の省略が可能です | | take | 取得数の設定 | 可 | `1` または `-1` のみ指定可能。詳細は下記 | | skip | スキップ数の設定 | 可 | 負数はエラー | | distinct | 重複削除の設定 | 可 | 指定する列が 1 つの場合、配列の省略が可能です | | cursor | カーソルベースページネーション | 可 | 詳細は [findMany の cursor](/docs/reference/crud/read/findMany#cursor) を参照 | ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例から以下の条件の行を取り出したいとします。 - age => **20 以上** この場合以下のコードとなります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findFirst const result = gassma.sheet1.findFirst({ where: { age: { gte: 20, }, }, }); ``` 戻り値は以下の形式です。 ```ts { name: 'akahoshi', age: 22, pref: 'Ibaraki', postNumber: '310-8555' } ``` ## take `findFirst` の `take` には **`1` または `-1` のみ**指定できます。それ以外の値を指定すると `GassmaFindFirstTakeError` がスローされます。 - `1`: 並び順のまま先頭の 1 件を取得します(省略時と同じ挙動)。 - `-1`: 並びを反転してから先頭の 1 件、つまり末尾側の 1 件を取得します。 ```ts // age の昇順に並べた末尾(最大 age)の 1 件を取得 const result = gassma.sheet1.findFirst({ orderBy: { age: "asc" }, take: -1, }); ``` `1` / `-1` 以外を指定すると `GassmaFindFirstTakeError` がスローされます。`findMany` の `take` とは異なり、件数の指定はできません。`NaN` / `Infinity` / `-Infinity` も `1` / `-1` 以外なので `GassmaFindFirstTakeError` になります(`findMany` の `take` とは異なるエラークラスです)。 ただし `take: null` だけは `GassmaInvalidValueError`(Invalid value for argument \`take\`. Expected a number, but received null.)になります。 ## skip 先頭から `skip` 件を飛ばした最初の 1 件を取得します。スキップした結果レコードが残らない場合は `null` が返されます。 ```ts // 条件に合致した行のうち、先頭 2 件を飛ばした次の 1 件を取得 const result = gassma.sheet1.findFirst({ where: { age: { gte: 20 } }, skip: 2, }); ``` `skip` に有限の負数を指定すると `GassmaSkipNegativeError` がスローされます。`NaN` / `Infinity` / `-Infinity` / `null` を指定した場合は `GassmaInvalidValueError` です([findMany の take / skip の異常値](./findMany#take--skip-の異常値)を参照)。 ## distinct 指定した列の値が重複する行を除外した上で、最初の 1 件を取得します。使い方は [findMany の distinct](./findMany#distinct) と同じです。 ## 処理順序 `findFirst` は以下の順序で処理され、最終的に先頭の 1 件(該当がなければ `null`)を返します。 1. `where` - フィルター 2. `orderBy` - ソート 3. `take` - `-1` の場合は並びを反転 4. `cursor` - カーソル位置で切り出し(カーソル自身を含む) 5. `distinct` - 重複削除 6. `skip` - 指定件数をスキップ 7. 先頭の 1 件を取得 8. `select` / `omit` - フィールド整形 また、key のオプション等それ以外の仕様については[findMany()](./findMany)に準拠します。 # findFirstOrThrow() (https://gassma.io/docs/reference/crud/read/findFirstOrThrow) findFirst と同様だが、レコードが見つからない場合に NotFoundError を投げる 特定の条件に合致した最初の行を取り出したい場合に利用します。`findFirst` と同じ動作ですが、レコードが見つからない場合に `null` ではなくエラーをスローします。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | -------- | ---------------- | ---- | --------------------------------------------- | | where | 取得条件の指定 | 可 | 書かない場合は全ての行を取得します | | select | 取得列の表示設定 | 可 | `omit` / `include` と同時に使用できません | | omit | 取得列の除外設定 | 可 | `select` と同時に使用できません | | include | リレーション先の取得 | 可 | [詳細はこちら](/docs/reference/relation/include) | | orderBy | ソート設定 | 可 | 指定する列が 1 つの場合、配列の省略が可能です | | take | 取得数の設定 | 可 | | skip | スキップ数の設定 | 可 | | distinct | 重複削除の設定 | 可 | 指定する列が 1 つの場合、配列の省略が可能です | ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例から以下の条件の行を取り出したいとします。 - age => **20 以上** この場合以下のコードとなります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.findFirstOrThrow const result = gassma.sheet1.findFirstOrThrow({ where: { age: { gte: 20, }, }, }); ``` 戻り値は以下の形式です。 ```ts { name: 'akahoshi', age: 22, pref: 'Ibaraki', postNumber: '310-8555' } ``` ## findFirst との違い レコードが見つからない場合の動作が異なります。 ```ts // findFirst の場合 → null が返る const result = gassma.sheet1.findFirst({ where: { name: "存在しない名前" }, }); // => null // findFirstOrThrow の場合 → NotFoundError がスローされる const result = gassma.sheet1.findFirstOrThrow({ where: { name: "存在しない名前" }, }); // => NotFoundError: No record found ``` それ以外の仕様については [findMany()](./findMany) に準拠します。 # update() (https://gassma.io/docs/reference/crud/update/update) レコードを 1 件更新する。数値のアトミック操作(increment/decrement/multiply/divide)とネストされた書き込みに対応 特定の条件に合致した**最初の 1 行**を指定した値に更新し、更新後のレコードを取得します。条件に合致するレコードがない場合は `null` を返します。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | ------- | -------------------------- | ---- | ------------------------------------------------ | | where | 取得条件の指定 | 不可 | 複数行が一致する場合は最初の 1 行のみ更新されます | | data | 更新するデータ | 不可 | | | select | 戻り値の取得列の表示設定 | 可 | `omit` / `include` と同時に使用できません | | omit | 戻り値の取得列の除外設定 | 可 | `select` と同時に使用できません | | include | リレーション先の取得 | 可 | [詳細はこちら](/docs/reference/relation/include) | `where` と `data` は必須です。いずれかを省略すると `GassmaMissingArgumentError`(例: Argument `where` is missing.)がスローされます。また、条件が 1 つもない `where: {}` は `GassmaInvalidValueError`(Invalid value for argument `where`. Expected at least one condition.)がスローされます。`undefined` や `Gassma.skip` の除去によって `where` が空になった場合も同様です。 ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例から以下の処理を行いたいとします。 - name が **akahoshi** の行の age を **23** にする この場合以下のコードとなります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.update const result = gassma.sheet1.update({ where: { name: "akahoshi", }, data: { age: 23, }, }); ``` 戻り値は以下の形式です。 ```ts { name: 'akahoshi', age: 23, pref: 'Ibaraki', postNumber: '310-8555' } ``` 更新後のレコードが返されます。更新していないフィールドは元の値がそのまま保持されます。 `data` の値に `undefined` を渡したフィールドは「指定しなかった」扱いになり、更新されません。 ```ts const result = gassma.sheet1.update({ where: { name: "akahoshi" }, data: { name: undefined, age: 23 }, }); // => name は元の値のまま、age だけが 23 に更新される ``` 条件に合致するレコードがない場合は `null` が返されます。 ```ts const result = gassma.sheet1.update({ where: { name: "存在しない名前" }, data: { age: 99 }, }); // => null ``` また`where`の仕様は[findMany()の記事](../read/findMany)に準拠します。ただし `findMany` と異なり、条件が 1 つもない `where: {}` はエラーになります(上記 note を参照)。 ## 数値の原子的操作 `data` に `increment` / `decrement` / `multiply` / `divide` を指定すると、現在値に対して演算を行えます。 ```ts // age を 1 加算する const result = gassma.sheet1.update({ where: { name: "akahoshi" }, data: { age: { increment: 1 }, }, }); // age: 22 → 23 ``` | 操作 | 動作 | 例 | | --- | --- | --- | | increment | 加算 | `{ increment: 5 }` → 現在値 + 5 | | decrement | 減算 | `{ decrement: 3 }` → 現在値 - 3 | | multiply | 乗算 | `{ multiply: 2 }` → 現在値 × 2 | | divide | 除算 | `{ divide: 4 }` → 現在値 ÷ 4 | 現在値が数値でない場合は `0` をベースとして演算されます。 ### 演算子に渡す値 `increment` などの演算子の引数に `NaN` / `Infinity` / `-Infinity` を渡すと `GassmaInvalidValueError` がスローされます。このとき `{argumentName}` は**演算子のキー**になります。 ```ts gassma.sheet1.update({ where: { name: "akahoshi" }, data: { age: { increment: NaN } } }); // => Invalid value for argument `increment`. Expected a finite number, but received NaN. ``` ### 演算結果 演算の**結果**が `NaN` / `Infinity` / `-Infinity` になる場合も `GassmaInvalidValueError` がスローされます。このとき `{argumentName}` は**カラム名**になります(演算子のキーではありません)。 ```ts gassma.sheet1.update({ where: { name: "akahoshi" }, data: { age: { divide: 0 } } }); // => Invalid value for argument `age`. Expected a finite number, but received Infinity. ``` 現在値が `0` の状態で `divide: 0` を指定した場合は `0 / 0` で `NaN` になります。 ```ts // age が 0 の行に対して data: { age: { divide: 0 } }; // => Invalid value for argument `age`. Expected a finite number, but received NaN. ``` 桁あふれも対象です。演算結果が数値として表現できる範囲を超えた場合は `Infinity` / `-Infinity` になるためエラーになります。 ```ts // age が 20 の行に対して data: { age: { multiply: 1e308 } }; // => Invalid value for argument `age`. Expected a finite number, but received Infinity. ``` 結果が有限の数値に収まる場合は従来どおり更新されます。エラーになった場合、行は書き換えられません。 この検証は `update` / `updateMany` / `updateManyAndReturn`、`upsert` の更新分岐、および [Nested Write(update)](/docs/reference/relation/nested-write-update) の `update` すべてで行われます。 通常の値指定と組み合わせることもできます。 ```ts const result = gassma.sheet1.update({ where: { name: "akahoshi" }, data: { age: { increment: 1 }, pref: "Tokyo", }, }); ``` 数値操作は**数値カラムに対してのみ**利用できます。文字列カラムに `increment` などを指定すると型エラーになります。`@gassma.addType` で数値を含む複合型(例: `number | string`)にしたカラムも数値操作の対象になります。 数値操作は `update` だけでなく、`updateMany` / `updateManyAndReturn`、`upsert` の `update`、および [Nested Write(update)](/docs/reference/relation/nested-write-update) の `update` の `data` でも同様に利用できます。 ## Nested Write リレーション定義がある場合、`data` の中にリレーション先のレコードを同時に操作する記述ができます。 `create` の Nested Write に加えて、`update` / `delete` / `deleteMany` / `disconnect` / `set` 操作が利用できます。 詳しくは [Nested Write(update)のリファレンス](/docs/reference/relation/nested-write-update)を参照してください。 # updateMany() (https://gassma.io/docs/reference/crud/update/updateMany) 条件に合致するすべてのレコードを更新し、更新件数を取得する。limit に対応 特定の条件に合致した全ての行を指定した値に更新します。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | ------ | -------------------------- | ---- | ---------------------------------------- | | where | 更新条件の指定 | 可 | 書かない場合は全ての行が対象になります | | data | 更新するデータ | 不可 | | | limit | 更新する最大件数 | 可 | 負数を指定するとエラーになります | `data` は必須です。省略すると `GassmaMissingArgumentError`(メッセージ: Argument `data` is missing.)がスローされます。`where` は省略可能で、省略すると全行が対象になります。`where: {}` や、条件が `undefined` だけで空になった場合も同様に全行が対象になります。 `data` の値に `undefined` を渡したフィールドは「指定しなかった」扱いになり、更新されません。 ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例から以下の処理を行いたいとします。 - age => **20 を 21 にする** この場合以下のコードとなります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.updateMany const result = gassma.sheet1.updateMany({ where: { age: 20, }, data: { age: 21, }, }); ``` 戻り値は以下の形式です。 ```ts { count: 1; } ``` 更新された行の数が返されます。 また`where`の仕様は[findMany()の記事](../read/findMany)に準拠します。 ## limit 更新する最大件数を指定できます。 ```ts // 最大 2 件のみ更新 const result = gassma.sheet1.updateMany({ where: { pref: "Tokyo", }, data: { age: 99, }, limit: 2, }); ``` `limit: 0` を指定すると 0 件更新(何も更新しない)となります。 `limit` に有限の負数を指定すると `GassmaLimitNegativeError` がスローされます。 `NaN` / `Infinity` / `-Infinity` / `null` を指定した場合は `GassmaInvalidValueError` です。この場合、行は 1 件も更新されません。 ```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` は以前 `GassmaLimitNegativeError` でしたが、有限かどうかの判定が先に行われるようになったため `GassmaInvalidValueError` になります。`undefined` は従来どおり無視されます(上限なし)。 ## 数値の原子的操作 `data` に `increment` / `decrement` / `multiply` / `divide` を指定すると、現在値に対して演算を行えます。 ```ts // 全員の age を 1 加算する const result = gassma.sheet1.updateMany({ data: { age: { increment: 1 }, }, }); ``` | 操作 | 動作 | 例 | | --- | --- | --- | | increment | 加算 | `{ increment: 5 }` → 現在値 + 5 | | decrement | 減算 | `{ decrement: 3 }` → 現在値 - 3 | | multiply | 乗算 | `{ multiply: 2 }` → 現在値 × 2 | | divide | 除算 | `{ divide: 4 }` → 現在値 ÷ 4 | 現在値が数値でない場合は `0` をベースとして演算されます。詳しくは [update()](/docs/reference/crud/update/update) を参照してください。 # updateManyAndReturn() (https://gassma.io/docs/reference/crud/update/updateManyAndReturn) 条件に合致するすべてのレコードを更新し、更新したレコードを返す 特定の条件に合致した全ての行を指定した値に更新し、更新後のレコードを配列で取得したい場合に利用します。 `updateMany` と同じ更新処理を行いますが、戻り値が異なります。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | ------ | -------------------------- | ---- | ---------------------------------------- | | where | 更新条件の指定 | 可 | 書かない場合は全ての行が対象になります | | data | 更新するデータ | 不可 | | | limit | 更新する最大件数 | 可 | 負数を指定するとエラーになります | `data` は必須です。省略すると `GassmaMissingArgumentError`(メッセージ: Argument `data` is missing.)がスローされます。`where` は省略可能で、省略すると全行が対象になります。 ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例から以下の処理を行いたいとします。 - pref が **Tokyo** の行の age を **99** にする この場合以下のコードとなります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.updateManyAndReturn const result = gassma.sheet1.updateManyAndReturn({ where: { pref: "Tokyo", }, data: { age: 99, }, }); ``` 戻り値は以下の形式です。 ```ts [ { name: "sato", age: 99, pref: "Tokyo", postNumber: "160-0023" }, { name: "endo", age: 99, pref: "Tokyo", postNumber: "160-0023" }, ]; ``` 更新後の全レコードが配列で返されます。更新していないフィールドは元の値がそのまま保持されます。 ## updateMany との違い | メソッド | 戻り値 | | --- | --- | | `updateMany` | `{ count: number }` | | `updateManyAndReturn` | 更新後のレコードの配列 | 条件に合致するレコードがない場合は空配列が返されます。 ```ts const result = gassma.sheet1.updateManyAndReturn({ where: { name: "存在しない名前" }, data: { age: 99 }, }); // => [] ``` `where` を省略すると全行が更新対象となり、全レコードが返されます。 ```ts const result = gassma.sheet1.updateManyAndReturn({ data: { age: 99 }, }); // => 全レコードが age: 99 で返される ``` また`where`の仕様は[findMany()の記事](../read/findMany)に準拠します。 ## limit 更新する最大件数を指定できます。詳しくは [updateMany()](/docs/reference/crud/update/updateMany) を参照してください。`NaN` / `Infinity` / `-Infinity` / `null` を指定したときの扱いも `updateMany` と同じです。 ## 数値の原子的操作 `data` に `increment` / `decrement` / `multiply` / `divide` を指定できます。詳しくは [update()](/docs/reference/crud/update/update) を参照してください。 # upsert() (https://gassma.io/docs/reference/crud/update/upsert) レコードが存在すれば更新し、存在しなければ作成する 特定の条件に合致したレコードが存在すれば更新し、存在しなければ新規作成します。結果のレコードを返します。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | ------- | ------------------------ | ---- | ----------------------------------------- | | where | 検索条件の指定 | 不可 | | | create | 未存在時の作成データ | 不可 | | | update | 存在時の更新データ | 不可 | | | select | 戻り値の取得列の表示設定 | 可 | `include` と同時に使用できません | | omit | 戻り値の取得列の除外設定 | 可 | `select` と同時に使用できません | | include | リレーション先の取得 | 可 | [詳細はこちら](/docs/reference/relation/include) | `where` / `create` / `update` はいずれも必須です。省略すると `GassmaMissingArgumentError`(例: Argument `create` is missing.)がスローされます。また、条件が 1 つもない `where: {}` は `GassmaInvalidValueError`(Invalid value for argument `where`. Expected at least one condition.)がスローされます。`undefined` や `Gassma.skip` の除去によって `where` が空になった場合も同様です。 ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例から以下の処理を行いたいとします。 - name が **akahoshi** の age を **23** にする - 存在しなければ新規作成する この場合以下のコードとなります。 ```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", }, }); ``` レコードが存在する場合、更新後のレコードが返されます。 ```ts { name: 'akahoshi', age: 23, pref: 'Ibaraki', postNumber: '310-8555' } ``` レコードが存在しない場合、`create` データで新規作成され、作成されたレコードが返されます。 ```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 リレーション定義がある場合、`create` / `update` 内で Nested Write が利用できます。 - `create` 時: [create の Nested Write](/docs/reference/relation/nested-write) と同等 - `update` 時: [update の Nested Write](/docs/reference/relation/nested-write-update) と同等 また`where`の仕様は[findMany()の記事](../read/findMany)に準拠します。ただし `findMany` と異なり、条件が 1 つもない `where: {}` はエラーになります(上記 note を参照)。 # delete() (https://gassma.io/docs/reference/crud/delete/delete) レコードを 1 件削除し、削除したレコードを返す 特定の条件に合致した**最初の 1 行**を削除し、削除されたレコードを取得します。条件に合致するレコードがない場合は `null` を返します。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | ------- | ---------------------- | ---- | ----------------------------------------- | | where | 削除条件の指定 | 不可 | | | select | 戻り値の取得列の表示設定 | 可 | `include` と同時に使用できません | | omit | 戻り値の取得列の除外設定 | 可 | `select` と同時に使用できません | | include | リレーション先の取得 | 可 | [詳細はこちら](/docs/reference/relation/include) | `where` は必須です。省略すると `GassmaMissingArgumentError`(メッセージ: Argument `where` is missing.)がスローされ、**暗黙的に全件が削除されることはありません**。また、条件が 1 つもない `where: {}` は `GassmaInvalidValueError`(Invalid value for argument `where`. Expected at least one condition.)がスローされ、行は削除されません。`undefined` や `Gassma.skip` の除去によって `where` が空になった場合も同様です。 ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例から以下の処理を行いたいとします。 - name が **akahoshi** の行を削除 この場合以下のコードとなります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.delete const result = gassma.sheet1.delete({ where: { name: "akahoshi", }, }); ``` 戻り値は以下の形式です。 ```ts { name: 'akahoshi', age: 22, pref: 'Ibaraki', postNumber: '310-8555' } ``` 削除されたレコードが返されます。 条件に合致するレコードがない場合は `null` が返されます。 ```ts const result = gassma.sheet1.delete({ where: { name: "存在しない名前" }, }); // => null ``` 複数のレコードが条件に合致する場合でも、**最初の 1 件のみ**が削除されます。 ## select / omit 戻り値のフィールドを制御できます。 ```ts const result = gassma.sheet1.delete({ where: { name: "akahoshi" }, select: { name: true, age: true }, }); // => { name: "akahoshi", age: 22 } ``` ## onDelete リレーション定義で `onDelete` が設定されている場合、`delete` でも referential action が実行されます。 詳しくは [onDelete のリファレンス](/docs/reference/relation/on-delete)を参照してください。 また`where`の仕様は[findMany()の記事](../read/findMany)に準拠します。ただし `findMany` と異なり、条件が 1 つもない `where: {}` はエラーになります(上記 caution を参照)。 # deleteMany() (https://gassma.io/docs/reference/crud/delete/deleteMany) 条件に合致するすべてのレコードを削除し、削除件数を取得する。limit に対応 特定の条件に合致した全ての行を削除したい場合に利用します。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | ------ | -------------------------- | ---- | ---------------------------------------- | | where | 削除条件の指定 | 可 | 書かない場合は全ての行が対象になります | | limit | 削除する最大件数 | 可 | 負数を指定するとエラーになります | `where: {}` や、条件が `undefined` / `Gassma.skip` だけで空になった場合も**全行が削除対象**になります。意図しない `undefined` を検出したい場合は [strictUndefinedChecks](/docs/reference/config/strict-undefined-checks) を有効にしてください。 ## 説明例用のシート ![説明用シート](../../img/exampleSheet.png) ## 説明 上記例から以下の処理を行いたいとします。 - age => **20 の行を削除** この場合以下のコードとなります。 ```ts const gassma = new Gassma.GassmaClient(); // gassma.{{TARGET_SHEET_NAME}}.deleteMany const result = gassma.sheet1.deleteMany({ where: { age: 20, }, }); ``` 戻り値は以下の形式です。 ```ts { count: 1; } ``` 削除された行の数が返されます。 ## limit 削除する最大件数を指定できます。 ```ts // 最大 3 件のみ削除 const result = gassma.sheet1.deleteMany({ where: { pref: "Tokyo", }, limit: 3, }); ``` `limit: 0` を指定すると 0 件削除(何も削除しない)となります。 `limit` に有限の負数を指定すると `GassmaLimitNegativeError` がスローされます。 `NaN` / `Infinity` / `-Infinity` / `null` を指定した場合は `GassmaInvalidValueError` です。この場合、行は 1 件も削除されません。 ```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` は以前 `GassmaLimitNegativeError` でしたが、有限かどうかの判定が先に行われるようになったため `GassmaInvalidValueError` になります。`undefined` は従来どおり無視されます(上限なし)。 また`where`の仕様は[findMany()の記事](../read/findMany)に準拠します。 # aggregate() (https://gassma.io/docs/reference/statistics/aggregate) _avg、_sum、_min、_max、_count などの集計を行う 平均や最大値等の統計を行いたい場合に利用します。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | ------- | ------------------ | ---- | --------------------------------------------- | | where | 取得条件の指定 | 可 | 書かない場合は全ての行を取得します | | orderBy | ソート設定 | 可 | 指定する列が 1 つの場合、配列の省略が可能です | | take | 取得数の設定 | 可 | | skip | スキップ数の設定 | 可 | | cursor | カーソルベースページネーション | 可 | 詳細は [findMany の cursor](/docs/reference/crud/read/findMany#cursor) を参照 | | \_avg | 平均表示の設定 | 可 | | \_count | ヒット数表示の設定 | 可 | `_all` や `true` 省略形も指定可能です。詳細は [\_count](#_count) を参照 | | \_max | 最大値表示の設定 | 可 | | \_min | 最小値表示の設定 | 可 | | \_sum | 合計表示の設定 | 可 | `where` では[リレーションフィルタ](/docs/reference/relation/where-relation-filter)(`some` / `every` / `none` / `is` / `isNot`)も利用可能です。 ## 説明例用のシート ![説明用シート](../img/exampleSheet.png) ## 説明 上記例から以下の処理を行いたいとします。 - age => **平均を求める** - age => **最大値を求める** - age => **最低値を求める** この場合以下のコードとなります。 ```ts // gassma.{{TARGET_SHEET_NAME}}.aggregate const result = gassma.sheet1.aggregate({ _avg: { age: true, }, _max: { age: true, }, _min: { age: true, }, }); ``` 戻り値は以下の形式です。 ```ts { _avg: { age: 33.333333333333336 }, _max: { age: 55 }, _min: { age: 20 } } ``` `_avg` / `_sum` / `_max` / `_min` では、null に加えて `NaN` / 不正な Date(Invalid Date)も欠損値として集計から除外されます。集計対象の値がすべて欠損値の場合、結果は null になります。 ## _count ヒット数を求めたい場合に利用します。 ### 列を指定したカウント `_count` に列名を指定すると、その列の値が null(空のセル)や `NaN` / 不正な Date(Invalid Date)などの欠損値ではない行のみを数えます。 ```ts // gassma.{{TARGET_SHEET_NAME}}.aggregate const result = gassma.sheet1.aggregate({ _count: { age: true, }, }); ``` 戻り値は以下の形式です。 ```ts { _count: { age: 9 } } ``` ### _all を使った全行数のカウント `_all: true` を指定すると、null を含む全ての行数を数えます。 ```ts // gassma.{{TARGET_SHEET_NAME}}.aggregate const result = gassma.sheet1.aggregate({ _count: { _all: true, postNumber: true, }, }); ``` 戻り値は以下の形式です。 ```ts { _count: { _all: 9, postNumber: 9 } } ``` 列を指定したカウントは null の行を数えないため、例えば postNumber が空の行が 2 行あるシートでは `{ _all: 9, postNumber: 7 }` のように結果が異なります。 ### true 省略形 `_count: true` を指定すると、全行数が数値としてそのまま返されます。 ```ts // gassma.{{TARGET_SHEET_NAME}}.aggregate const result = gassma.sheet1.aggregate({ _count: true, }); ``` 戻り値は以下の形式です。 ```ts { _count: 9 } ``` `_all` と `true` 省略形は `_count` 専用で、`_avg` / `_max` / `_min` / `_sum` では利用できません。`_count` は行数を数えるため null を含む全行に意味がありますが、他の集計は特定の列の値を対象とするためです。 # count() (https://gassma.io/docs/reference/statistics/count) 条件に合致するレコードの件数を数える ヒット数を求めたい場合に利用します。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | ------- | ---------------- | ---- | --------------------------------------------- | | where | 取得条件の指定 | 可 | 書かない場合は全ての行を取得します | | orderBy | ソート設定 | 可 | 指定する列が 1 つの場合、配列の省略が可能です | | take | 取得数の設定 | 可 | | skip | スキップ数の設定 | 可 | | cursor | カーソルベースページネーション | 可 | 詳細は [findMany の cursor](/docs/reference/crud/read/findMany#cursor) を参照 | `where` では[リレーションフィルタ](/docs/reference/relation/where-relation-filter)(`some` / `every` / `none` / `is` / `isNot`)も利用可能です。 ## 説明例用のシート ![説明用シート](../img/exampleSheet.png) ## 説明 上記例から以下の処理を行いたいとします。 - age => **20 以上** この場合以下のコードとなります。 ```ts // gassma.{{TARGET_SHEET_NAME}}.count const result = gassma.sheet1.count({ where: { age: { gte: 20, }, }, }); ``` 戻り値は以下の形式です。 ``` 9 ``` # groupBy() (https://gassma.io/docs/reference/statistics/groupBy) フィールドでレコードをグループ化してグループごとに集計し、having でグループを絞り込む データをグループ化したい場合に利用します。 ## 使用できるキー | キー名 | 内容 | 省略 | 備考 | | ------- | ------------------------------ | ---- | --------------------------------------------- | | where | 取得条件の指定 | 可 | 書かない場合は全ての行を取得します | | orderBy | ソート設定 | 可 | 指定する列が 1 つの場合、配列の省略が可能です | | take | 取得数の設定 | 可 | | skip | スキップ数の設定 | 可 | | \_avg | 平均表示の設定 | 可 | | \_count | ヒット数表示の設定 | 可 | `_all` や `true` 省略形も指定可能です。詳細は [\_count](#_count) を参照 | | \_max | 最大値表示の設定 | 可 | | \_min | 最小値表示の設定 | 可 | | \_sum | 合計表示の設定 | 可 | | by | グループ化条件の指定 | 不可 | | having | グループ化した後の取得条件指定 | 可 | 書かない場合は全てのデータを取得します | `by` は必須です。省略すると `GassmaMissingArgumentError`(メッセージ: Argument `by` is missing.)がスローされます。 `where` では[リレーションフィルタ](/docs/reference/relation/where-relation-filter)(`some` / `every` / `none` / `is` / `isNot`)も利用可能です。 ## 説明例用のシート ![説明用シート](../img/exampleSheet.png) ## 説明 上記例から以下の処理を行いたいとします。 - pref でグループ化 この場合以下のコードとなります。 ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: "pref", }); ``` 戻り値は以下の形式です。 ```ts [ { pref: "Ibaraki" }, { pref: "Tokyo" }, { pref: "Osaka" }, { pref: "Aichi" }, { pref: "Shiga" }, { pref: "Kyoto" }, { pref: "Tottori" }, { pref: "Fukuoka" }, ]; ``` また、複数指定することもでき、以下の処理を行いたいとします。 - pref でグループ化 - さらに age でグループ化 この場合以下のコードとなります。 ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref", "age"], }); ``` 戻り値は以下の形式です。 ```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 }, ]; ``` ### グループ化キーの欠損値(null / NaN / Invalid Date) `by` に指定した列の値が null / `NaN` / 不正な Date(Invalid Date)の行も、落とされずにグループ化されます。 - `NaN` の行同士は 1 つのグループにまとまります。 - Invalid Date の行同士も、別インスタンスであっても 1 つのグループにまとまります。 - `NaN`・null・Invalid Date は互いに**別のグループ**です。 ### having グループ化されたデータの中で、特定の条件を満たすデータを抽出したい場合に利用します。 例えば以下の条件でデータを抽出したいとします。 - pref でグループ化 - (グループ化した後)age => **平均が 30 以下** この場合以下のコードとなります。 ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref"], having: { age: { _avg: { lte: 30, }, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { pref: "Ibaraki" }, { pref: "Osaka" }, { pref: "Shiga" }, { pref: "Tottori" }, ]; ``` ### having の AND, OR, NOT AND, OR, NOT を利用することも可能です。 例えば以下の処理を行いたいとします。 - pref でグループ化 - (グループ化した後)age => **平均が 30 以下ではない** この場合以下のコードとなります。 ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref"], having: { NOT: { age: { _avg: { lte: 30, }, }, }, }, }); ``` 戻り値は以下のようになります。 ```ts [{ pref: "Tokyo" }, { pref: "Aichi" }, { pref: "Kyoto" }, { pref: "Fukuoka" }]; ``` また、`where`と同様 NOT の下に AND を入れたりネストすることが可能です。 `having` の値に `NaN` や不正な Date(Invalid Date)などの比較できない値を渡すと `GassmaInvalidValueError` がスローされます(`where` と同様です)。 ### 統計の表示 aggregate のように平均などを表示することもできます。 例えば以下の処理を行いたいとします。 - pref でグループ化 - age の平均を表示 この場合以下のコードとなります。 ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref"], _avg: { age: true }, }); ``` 戻り値は以下の形式です。 ```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 } }, ]; ``` `_avg` / `_sum` / `_max` / `_min` および列名指定の `_count` では、null に加えて `NaN` / 不正な Date(Invalid Date)も欠損値として集計から除外されます。集計対象の値がすべて欠損値の場合、結果は null になります。`_count: { _all: true }` はこれらの行も数えます。 ### _count `_count` では各グループの行数を数えられます。列名を指定するとその列の値が null(空のセル)や `NaN` / 不正な Date(Invalid Date)などの欠損値ではない行のみを、`_all: true` を指定すると欠損値を含む全ての行数を数えます。 例えば以下の処理を行いたいとします。 - pref でグループ化 - 各グループの行数を表示 この場合以下のコードとなります。 ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref"], _count: { _all: true }, }); ``` 戻り値は以下の形式です。 ```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 } }, ]; ``` `_count: true` と省略すると、行数が数値としてそのまま返されます。 ```ts // gassma.{{TARGET_SHEET_NAME}}.groupBy const result = gassma.sheet1.groupBy({ by: ["pref"], _count: true, }); ``` 戻り値は以下の形式です。 ```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` と `true` 省略形は `_count` 専用で、`_avg` / `_max` / `_min` / `_sum` では利用できません。詳細は [aggregate の \_count](/docs/reference/statistics/aggregate#_count) を参照してください。 # リレーション定義 (https://gassma.io/docs/reference/relation/definition) シート間の oneToMany・oneToOne・manyToOne・manyToMany リレーションの定義方法 複数のシート間のリレーション(関連)を定義することで、`include` によるリレーション先データの取得や、`where` でのリレーション条件フィルタが可能になります。 ## 説明例用のシート 以降のリレーションドキュメントでは、以下のシートを例として使用します。 ### Users シート | id | name | email | | --- | --- | --- | | 1 | Alice | alice@example.com | | 2 | Bob | bob@example.com | | 3 | Charlie | charlie@example.com | ### Posts シート | id | title | authorId | published | | --- | --- | --- | --- | | 1 | 初めての投稿 | 1 | true | | 2 | GAS の使い方 | 1 | true | | 3 | 下書き記事 | 2 | false | ### Profiles シート | id | userId | bio | | --- | --- | --- | | 1 | 1 | エンジニアです | | 2 | 2 | デザイナーです | ### Tags シート | id | name | | --- | --- | | 1 | GAS | | 2 | JavaScript | ### PostTags シート(中間テーブル) | postId | tagId | | --- | --- | | 1 | 1 | | 1 | 2 | | 2 | 1 | ## 基本的な定義方法 `GassmaClient` のコンストラクタに `relations` オプションを渡すことで、シート間のリレーションを定義できます。 ```ts const gassma = new Gassma.GassmaClient({ relations: { // シート名(スプレッドシート上の実際のシート名と一致させる) Users: { // リレーション名(自由な名前を付けられます。include や where で使用するキー名になります) posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", }, }, }, }); ``` スプレッドシート ID を指定する場合は `id` も一緒に渡します。 ```ts const gassma = new Gassma.GassmaClient({ id: "XXXXXXXXXXXXXXXXXXX", relations: { // ... }, }); ``` ## リレーション定義のキー | キー名 | 内容 | 省略 | 備考 | | --- | --- | --- | --- | | type | リレーションの種類 | 不可 | `oneToMany` / `oneToOne` / `manyToOne` / `manyToMany` | | to | 関連先のシート名 | 不可 | | | field | 自シート側のカラム名 | 不可 | FK または PK | | reference | 関連先シート側のカラム名 | 不可 | | | through | 中間テーブルの設定 | 可 | `manyToMany` の場合は必須 | | onDelete | 削除時のアクション | 可 | `Cascade` / `SetNull` / `Restrict` / `NoAction` | | onUpdate | PK 更新時のアクション | 可 | `Cascade` / `SetNull` / `Restrict` / `NoAction` | ## リレーションの種類 ### oneToMany(1 対 多) 1 つの親レコードに対して、複数の子レコードが紐づく関係です。 例:1 人のユーザーが複数の投稿を持つ ```ts relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", // Users の PK reference: "authorId", // Posts の FK }, }, } ``` `include` で取得すると配列が返ります。 ### manyToOne(多 対 1) `oneToMany` の逆方向です。子レコードから親レコードへの参照を定義します。 例:投稿から著者(ユーザー)を取得 ```ts relations: { Posts: { author: { type: "manyToOne", to: "Users", field: "authorId", // Posts の FK reference: "id", // Users の PK }, }, } ``` `include` で取得すると単一オブジェクトまたは `null` が返ります。 `manyToOne` は **FK を保有する側**(Prisma スキーマで `@relation(fields: ...)` を書く側)の定義です。1 対 1 の関係であっても、FK を保有する側は `manyToOne` と定義します。 ### oneToOne(1 対 1) 1 つのレコードに対して、1 つだけ紐づくレコードがある関係です。`oneToOne` は 1 対 1 のうち **FK を持たない側**専用の定義です。 例:ユーザーとプロフィール(FK の `userId` を持つのは Profiles 側) ```ts relations: { Users: { profile: { type: "oneToOne", to: "Profiles", field: "id", // Users の PK reference: "userId", // Profiles の FK }, }, } ``` `include` で取得すると単一オブジェクトまたは `null` が返ります。同じ `reference` 値を持つレコードが複数存在する場合はエラーとなります。 FK を保有する側(上の例では Profiles)から逆方向のリレーションを定義する場合は、1 対 1 であっても `manyToOne` を使用します。 ```ts relations: { Profiles: { user: { type: "manyToOne", to: "Users", field: "userId", // Profiles の FK reference: "id", // Users の PK }, }, } ``` ### manyToMany(多 対 多) 中間テーブルを経由して、多対多の関係を定義します。 例:投稿とタグ ```ts relations: { Posts: { tags: { type: "manyToMany", to: "Tags", field: "id", // Posts の PK reference: "id", // Tags の PK through: { sheet: "PostTags", // 中間テーブルのシート名 field: "postId", // 中間テーブルにおける Posts 側の FK reference: "tagId", // 中間テーブルにおける Tags 側の FK }, }, }, } ``` `include` で取得すると配列が返ります。 ## 複数リレーションの定義 1 つのシートに複数のリレーションを定義できます。また、複数シートにまたがって定義することもできます。 ```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", }, }, }, }, }); ``` ## バリデーション リレーション定義に誤りがある場合、`GassmaClient` のインスタンス生成時にエラーがスローされます。 | エラー | 原因 | | --- | --- | | `RelationSheetNotFoundError` | `relations` のキー、`to`、`through.sheet` に指定したシート名が存在しない | | `RelationMissingPropertyError` | `type` / `to` / `field` / `reference` が欠けている。manyToMany で `through` が欠けている | | `RelationInvalidPropertyTypeError` | プロパティの型が string でない | | `RelationInvalidTypeError` | `type` が 4 種類のいずれでもない | | `RelationInvalidOnDeleteError` | `onDelete` が 4 種類のいずれでもない | | `RelationInvalidOnUpdateError` | `onUpdate` が 4 種類のいずれでもない | | `RelationColumnNotFoundError` | `field` / `reference` に指定したカラムがシート上に存在しない | # include (https://gassma.io/docs/reference/relation/include) where/orderBy/select オプション、ネストした include、リレーションの _count を使って関連レコードを取得する `findMany` / `findFirst` でリレーション先のデータを一緒に取得したい場合に利用します。 使用するには事前に[リレーション定義](/docs/reference/relation/definition)が必要です。 ## 説明例用のシート [リレーション定義](/docs/reference/relation/definition)のシート例を使用します。 ## 基本的な使い方 `include` にリレーション名を指定し、値に `true` を渡すと、リレーション先のデータが全て取得されます。 ```ts const result = gassma.Users.findMany({ include: { posts: true, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 1, name: "Alice", email: "alice@example.com", posts: [ { id: 1, title: "初めての投稿", authorId: 1, published: true }, { id: 2, title: "GASの使い方", authorId: 1, published: true }, ], }, { id: 2, name: "Bob", email: "bob@example.com", posts: [ { id: 3, title: "下書き記事", authorId: 2, published: false }, ], }, { id: 3, name: "Charlie", email: "charlie@example.com", posts: [], }, ]; ``` リレーションの種類によって返される形が異なります。 | リレーション種類 | 返される形 | | --- | --- | | oneToMany | 配列 | | manyToMany | 配列 | | oneToOne | 単一オブジェクト or `null` | | manyToOne | 単一オブジェクト or `null` | ### manyToOne の例 ```ts const result = gassma.Posts.findMany({ include: { author: true, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 1, title: "初めての投稿", authorId: 1, published: true, author: { id: 1, name: "Alice", email: "alice@example.com" }, }, { id: 3, title: "下書き記事", authorId: 2, published: false, author: { id: 2, name: "Bob", email: "bob@example.com" }, }, // ... ]; ``` ## include のオプション `true` の代わりにオブジェクトを渡すことで、リレーション先のデータに条件を付けることができます。 ### 使用できるキー | キー名 | 内容 | 省略 | | --- | --- | --- | | where | リレーション先の取得条件 | 可 | | orderBy | リレーション先のソート | 可 | | skip | リレーション先のスキップ数 | 可 | | take | リレーション先の取得数 | 可 | | select | リレーション先の取得列の表示設定 | 可 | | omit | リレーション先の取得列の除外設定 | 可 | | include | さらに深いリレーションの取得 | 可 | ### where リレーション先のデータに条件を付けて取得できます。 ```ts const result = gassma.Users.findMany({ include: { posts: { where: { published: true }, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 1, name: "Alice", email: "alice@example.com", posts: [ { id: 1, title: "初めての投稿", authorId: 1, published: true }, { id: 2, title: "GASの使い方", authorId: 1, published: true }, ], }, { id: 2, name: "Bob", email: "bob@example.com", posts: [], // published: false の記事はフィルタされる }, // ... ]; ``` ### orderBy リレーション先のデータをソートできます。 ```ts const result = gassma.Users.findMany({ include: { posts: { orderBy: { title: "desc" }, }, }, }); ``` ### skip / take リレーション先のデータをページネーションできます。`skip` と `take` を組み合わせて使用します。 ```ts const result = gassma.Users.findMany({ include: { posts: { orderBy: { id: "asc" }, skip: 1, take: 1, }, }, }); ``` 上記の例では、各ユーザーの投稿を id 昇順で並べ、最初の 1 件をスキップし、次の 1 件だけを取得します。 `skip` / `take` は oneToMany と manyToMany で利用できます。oneToOne / manyToOne は単一レコードのため対象外です。 `include` の `skip` / `take` に数値以外の値を渡すと `IncludeInvalidOptionTypeError` がスローされます。値によってメッセージが変わります。 ```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 ``` | 値 | メッセージ | | --- | --- | | `NaN` / `Infinity` / `-Infinity` | `must be a finite number` | | `null` / 数値以外 | `must be a number` | | `undefined` | 指定しなかった扱いになり無視されます | 有限の負数はエラーになりません。`take` は末尾から取得し、`skip` に負数を渡した場合は `GassmaSkipNegativeError` になります。ネストした `include` のオプションも同じ検証を受けます。 ### select リレーション先のデータの取得列を指定できます。 ```ts const result = gassma.Users.findMany({ include: { posts: { select: { title: true }, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 1, name: "Alice", email: "alice@example.com", posts: [ { title: "初めての投稿" }, { title: "GASの使い方" }, ], }, // ... ]; ``` ### omit リレーション先のデータの特定列を除外できます。 ```ts const result = gassma.Users.findMany({ include: { posts: { omit: { authorId: true }, }, }, }); ``` `omit` はリレーション先モデルの[グローバル omit](/docs/reference/config/global-omit)とマージされます。トップレベルの `omit` と同様に、`false` を指定するとグローバル omit で除外されているフィールドをこの取得に限り再表示できます。 ```ts // グローバル omit で Posts.content を除外している場合 const result = gassma.Users.findMany({ include: { posts: { omit: { content: false }, // content が返る(グローバル omit を解除) }, }, }); ``` `select` と `omit` は同時に指定できません。 ### ネストされた include `include` の中にさらに `include` を指定することで、深い階層のリレーションを取得できます。 例えば、Users → Posts → Tags のようなリレーションを一度に取得できます。 ```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, }, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 1, name: "Alice", email: "alice@example.com", posts: [ { id: 1, title: "初めての投稿", authorId: 1, published: true, tags: [ { id: 1, name: "GAS" }, { id: 2, name: "JavaScript" }, ], }, { id: 2, title: "GASの使い方", authorId: 1, published: true, tags: [ { id: 1, name: "GAS" }, ], }, ], }, // ... ]; ``` `select` と `include` は同時に指定できません。 ## _count リレーション先のレコード件数を取得できます。 ### 全リレーションのカウント `_count: true` を指定すると、定義されている全リレーションのレコード件数を取得します。 ```ts const result = gassma.Users.findMany({ include: { _count: true, }, }); ``` 戻り値は以下の形式です。 ```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: { select: { ... } }` でカウントするリレーションを指定できます。 ```ts const result = gassma.Users.findMany({ include: { _count: { select: { posts: true }, }, }, }); ``` ### where フィルタ付きカウント カウント対象に条件を付けることもできます。 ```ts const result = gassma.Users.findMany({ include: { _count: { select: { posts: { where: { published: true }, }, }, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 1, name: "Alice", email: "alice@example.com", _count: { posts: 2 }, // published: true の投稿のみカウント }, // ... ]; ``` ### select と _count の組み合わせ トップレベルの `select` と `_count` を組み合わせることもできます。 ```ts const result = gassma.Users.findMany({ select: { name: true, _count: { select: { posts: true }, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { name: "Alice", _count: { posts: 2 } }, { name: "Bob", _count: { posts: 1 } }, // ... ]; ``` `_count` は全てのリレーション種別(oneToMany / oneToOne / manyToOne / manyToMany)に対応しています。 ## 複数リレーションの同時取得 1 回のクエリで複数のリレーションを同時に取得できます。 ```ts const result = gassma.Users.findMany({ include: { posts: true, profile: true, }, }); ``` ## include と select の制限 **トップレベル**の `select` と `include` は同時に使用できません。 ```ts // これはエラーになります gassma.Users.findMany({ select: { name: true }, include: { posts: true }, }); ``` ## バリデーション | エラー | 原因 | | --- | --- | | `IncludeWithoutRelationsError` | リレーション定義なしで `include` を使用 | | `GassmaIncludeSelectConflictError` | トップレベルで `include` と `select` を同時使用 | | `IncludeSelectOmitConflictError` | include 内で `select` と `omit` を同時指定 | | `IncludeSelectIncludeConflictError` | include 内で `select` と `include` を同時指定 | | `IncludeInvalidOptionTypeError` | include の値やオプションの型が不正 | | `GassmaRelationNotFoundError` | 指定したリレーション名が定義されていない | # where リレーションフィルタ (https://gassma.io/docs/reference/relation/where-relation-filter) some/every/none(リストリレーション)と is/isNot(単一リレーション)で関連レコードによる絞り込みを行う `where` 条件の中でリレーション先のデータを基準にフィルタリングしたい場合に利用します。 使用するには事前に[リレーション定義](/docs/reference/relation/definition)が必要です。 ## 説明例用のシート [リレーション定義](/docs/reference/relation/definition)のシート例を使用します。 ## 対応するメソッド where リレーションフィルタは以下の全メソッドで利用できます。 - `findMany` / `findFirst` - `update` / `updateMany` / `deleteMany` - `aggregate` / `count` / `groupBy` ## フィルタの種類 リレーションの種類によって使用できるフィルタが異なります。 | フィルタ | oneToMany | manyToMany | oneToOne | manyToOne | | --- | --- | --- | --- | --- | | some | 使用可 | 使用可 | - | - | | every | 使用可 | 使用可 | - | - | | none | 使用可 | 使用可 | - | - | | is | - | - | 使用可 | 使用可 | | isNot | - | - | 使用可 | 使用可 | ## リストリレーションのフィルタ(oneToMany / manyToMany) ### some 関連レコードの中に**少なくとも 1 つ**条件に一致するものがあるレコードを取得します。 例:公開済みの投稿を 1 件以上持つユーザーを取得 ```ts const result = gassma.Users.findMany({ where: { posts: { some: { published: true }, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 1, name: "Alice", email: "alice@example.com" }, ]; ``` Alice は公開済みの投稿を持っているため取得されます。Bob は `published: false` の投稿のみ、Charlie は投稿なしのため除外されます。 ### every 関連レコードの**全て**が条件に一致するレコードを取得します。関連レコードが 0 件の場合も一致扱いになります。 例:全ての投稿が公開済みのユーザーを取得 ```ts const result = gassma.Users.findMany({ where: { posts: { every: { published: true }, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 1, name: "Alice", email: "alice@example.com" }, { id: 3, name: "Charlie", email: "charlie@example.com" }, ]; ``` Alice は全投稿が `published: true`、Charlie は投稿が 0 件(=全てが条件を満たす)のため取得されます。 ### none 関連レコードの中に条件に一致するものが**1 つもない**レコードを取得します。 例:公開済みの投稿を 1 件も持たないユーザーを取得 ```ts const result = gassma.Users.findMany({ where: { posts: { none: { published: true }, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 2, name: "Bob", email: "bob@example.com" }, { id: 3, name: "Charlie", email: "charlie@example.com" }, ]; ``` 親レコード自身の結合キー(リレーション定義の `reference` に指定した列)が null の場合、関連レコードは 0 件として扱われます。そのため `every`(全てが条件を満たす扱い)と `none`(一致するものがない扱い)の結果には含まれ、`some` の結果には含まれません(Prisma と同じです)。 ## 単一リレーションのフィルタ(oneToOne / manyToOne) ### is 関連レコードが条件に一致するレコードを取得します。`null` を指定すると、関連レコードが存在しないレコードを取得できます。 例:著者名が "Alice" の投稿を取得 ```ts const result = gassma.Posts.findMany({ where: { author: { is: { name: "Alice" }, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 1, title: "初めての投稿", authorId: 1, published: true }, { id: 2, title: "GASの使い方", authorId: 1, published: true }, ]; ``` ### is: null 関連レコードが存在しないレコードを取得できます。`manyToOne`(FK を保有する側)では FK が null のレコード、`oneToOne`(FK を持たない側)では**関連レコードが存在しない**レコードが対象になります。 例:著者(FK の `authorId`)が null の投稿を取得 ```ts const result = gassma.Posts.findMany({ where: { author: { is: null, }, }, }); ``` 例:プロフィールを持たないユーザーを取得(非FK側の `oneToOne`) ```ts const result = gassma.Users.findMany({ where: { profile: { is: null, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 3, name: "Charlie", email: "charlie@example.com" }, ]; ``` `oneToOne`(FK を持たない側)では、自分側の結合キー(`reference` に指定した列)が null のレコードも「関連レコードが存在しない」として `is: null` の結果に含まれます。 ### isNot 関連レコードが条件に一致**しない**レコードを取得します。`null` を指定すると、関連レコードが存在するレコードを取得できます。 例:著者名が "Alice" ではない投稿を取得 ```ts const result = gassma.Posts.findMany({ where: { author: { isNot: { name: "Alice" }, }, }, }); ``` 戻り値は以下の形式です。 ```ts [ { id: 3, title: "下書き記事", authorId: 2, published: false }, ]; ``` 関連レコードを持たない行(`manyToOne` では FK が null の行、`oneToOne` では関連レコードが存在しない行)も `isNot: <条件>` の結果に含まれます。関連レコードが存在しない = 「条件に一致する関連レコードを持たない」扱いです(Prisma と同じです)。 ### isNot: null 関連レコードが存在するレコードを取得できます。`manyToOne`(FK を保有する側)では FK が null でないレコード、`oneToOne`(FK を持たない側)では**関連レコードが存在する**レコードが対象になります。 ```ts const result = gassma.Posts.findMany({ where: { author: { isNot: null, }, }, }); ``` ## AND / OR / NOT との組み合わせ リレーションフィルタは AND / OR / NOT と自由に組み合わせることができます。 ### OR との組み合わせ 例:公開済みの投稿を持つユーザー、または名前が "Charlie" のユーザーを取得 ```ts const result = gassma.Users.findMany({ where: { OR: [ { posts: { some: { published: true } } }, { name: "Charlie" }, ], }, }); ``` ### NOT との組み合わせ 例:下書き記事を持たないユーザーを取得 ```ts const result = gassma.Users.findMany({ where: { NOT: { posts: { some: { published: false } }, }, }, }); ``` ## findMany / findFirst 以外での使用例 ### updateMany 公開済み投稿を持つユーザーのメールアドレスを更新 ```ts gassma.Users.updateMany({ where: { posts: { some: { published: true } }, }, data: { email: "updated@example.com", }, }); ``` ### count 公開済み投稿を持つユーザー数をカウント ```ts const count = gassma.Users.count({ where: { posts: { some: { published: true } }, }, }); ``` ## バリデーション | エラー | 原因 | | --- | --- | | `WhereRelationWithoutContextError` | リレーション定義なしでリレーションフィルタ構文を使用 | | `WhereRelationInvalidFilterError` | リストフィルタ (some/every/none) を oneToOne/manyToOne に使用、または単一フィルタ (is/isNot) を oneToMany/manyToMany に使用 | # onDelete (https://gassma.io/docs/reference/relation/on-delete) 削除時の参照アクション(Cascade、SetNull、Restrict、NoAction) `deleteMany` でレコードを削除する際、リレーション先のレコードをどう扱うかを定義します。 ## 説明例用のシート [リレーション定義](/docs/reference/relation/definition)のシート例を使用します。 ## 基本的な使い方 [リレーション定義](/docs/reference/relation/definition)で `onDelete` を指定します。 ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "Cascade", }, }, }, }); ``` onDelete は FK を保有する側(`manyToOne`)のリレーション定義では発火しません。参照される側(`oneToMany` / 非FK側の `oneToOne` / `manyToMany`)の定義に指定してください。 ## アクションの種類 | アクション | 動作 | | --- | --- | | Cascade | 関連レコードも一緒に削除 | | SetNull | 関連レコードの FK を null にする | | Restrict | 関連レコードが存在する場合、削除をエラーで阻止 | | NoAction | 何もしない(デフォルト) | ## Cascade 親レコードを削除すると、関連する子レコードも自動的に削除されます。複数のシートへの書き込みになりますが、途中でエラーになった場合はどのシートも変更されません(詳細は[書き込みの原子性と同時実行](/docs/reference/write-atomicity)を参照)。 ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "Cascade", }, }, }, }); // Alice を削除すると、Alice の投稿も全て削除される gassma.Users.deleteMany({ where: { name: "Alice" }, }); ``` 上記を実行すると、Users シートから Alice が削除されるのに加えて、Posts シートの `authorId: 1` のレコードも全て削除されます。 ### manyToMany の場合 manyToMany で Cascade を指定すると、**中間テーブル**のレコードが削除されます。ターゲットテーブル(リレーション先)のレコードは削除されません。 ```ts relations: { Posts: { tags: { type: "manyToMany", to: "Tags", field: "id", reference: "id", through: { sheet: "PostTags", field: "postId", reference: "tagId", }, onDelete: "Cascade", }, }, } // 投稿を削除すると、PostTags の関連行が削除される(Tags は残る) gassma.Posts.deleteMany({ where: { id: 1 }, }); ``` ## SetNull 親レコードを削除すると、関連する子レコードの FK が `null` に更新されます。 ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "SetNull", }, }, }, }); // Alice を削除すると、Alice の投稿の authorId が null になる gassma.Users.deleteMany({ where: { name: "Alice" }, }); ``` 実行後の Posts シートは以下のようになります。 | id | title | authorId | published | | --- | --- | --- | --- | | 1 | 初めての投稿 | null | true | | 2 | GAS の使い方 | null | true | | 3 | 下書き記事 | 2 | false | manyToMany で SetNull を指定した場合、何も行われません(中間テーブルの FK を null にしても意味がないため)。 ## Restrict 関連するレコードが 1 件でも存在する場合、削除を拒否してエラーをスローします。 ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "Restrict", }, }, }, }); // Alice は投稿を持っているため、エラーがスローされる gassma.Users.deleteMany({ where: { name: "Alice" }, }); // => RelationOnDeleteRestrictError // Charlie は投稿を持たないため、正常に削除される gassma.Users.deleteMany({ where: { name: "Charlie" }, }); ``` Restrict のチェックは全てのリレーションに対して**先に**行われます。そのため、エラーが発生しても副作用(他のリレーションの Cascade 等)は実行されません。 ## NoAction 何も行いません。`onDelete` を指定しない場合と同じ動作です。 ```ts relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "NoAction", // 指定しない場合と同じ }, }, } ``` ## 複数リレーションでの onDelete 1 つのシートに複数のリレーションを定義し、それぞれ異なる onDelete を設定できます。 ```ts relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "Cascade", // 投稿は一緒に削除 }, profile: { type: "oneToOne", to: "Profiles", field: "id", reference: "userId", onDelete: "SetNull", // プロフィールは FK を null に }, }, } ``` # Nested Write(create) (https://gassma.io/docs/reference/relation/nested-write) create の中で create/connect/connectOrCreate を使って関連レコードを書き込む `create` メソッド内でリレーション先のレコードを同時に作成・関連付けしたい場合に利用します。 使用するには事前に[リレーション定義](/docs/reference/relation/definition)が必要です。 ## 説明例用のシート [リレーション定義](/docs/reference/relation/definition)のシート例を使用します。 ## 使用できる操作 | 操作 | 内容 | | --- | --- | | create | リレーション先のレコードを新規作成して関連付け | | createMany | リレーション先のレコードを複数新規作成して関連付け | | connect | 既存のリレーション先レコードを関連付け | | connectOrCreate | 既存のレコードがあれば関連付け、なければ新規作成して関連付け | ### リレーション種類ごとの対応表 | 操作 | manyToOne | oneToOne | oneToMany | manyToMany | | --- | --- | --- | --- | --- | | create | 単一のみ | 単一のみ | 単一/配列 | 単一/配列 | | createMany | - | - | 対応 | - | | connect | 対応 | 対応 | 単一/配列 | 単一/配列 | | connectOrCreate | 対応 | 対応 | 単一/配列 | 単一/配列 | to-one リレーションは FK の位置によって動作が異なります。 - **manyToOne(FK 保有側)**: 自レコードの FK に値がセットされます - **oneToOne(非FK側)**: FK を保有するリレーション先レコードの FK が書き換えられます。自レコードは変更されません `oneToOne` は 1 対 1 の FK を持たない側専用の定義です([リレーション定義](/docs/reference/relation/definition)を参照)。 ### oneToOne(非FK側)の挙動 | 操作 | 動作 | 対象レコードが存在しない場合 | | --- | --- | --- | | create | リレーション先レコードを FK 自動セットで作成 | - | | connect | 置き換え(既接続レコードの FK を null 化してから、対象レコードの FK を親にセット) | `NestedWriteConnectNotFoundError` | | connectOrCreate | 存在すれば connect と同じ置き換え、なければ FK 自動セットで作成 | - | `createMany` や配列形式の指定は `NestedWriteInvalidOperationError` になります。 ## create ### manyToOne での create ユーザーを作成しながら、そのユーザーに紐づく投稿も同時に作成する例です。ただし manyToOne は逆方向(投稿側から著者を作成)として使います。 ```ts const result = gassma.Posts.create({ data: { id: 4, title: "新しい記事", published: true, author: { create: { id: 4, name: "Dave", email: "dave@example.com", }, }, }, }); ``` 上記を実行すると以下が行われます。 1. Users シートに Dave が作成される 2. Dave の `id`(= 4)が Posts の `authorId` に自動セットされる 3. Posts シートに新しい記事が作成される 戻り値は以下の形式です。 ```ts { id: 4, title: "新しい記事", authorId: 4, published: true, } ``` ### oneToOne での create ユーザー作成と同時にプロフィールも作成します。oneToOne(非FK側)では、リレーション先レコードの FK に親の値が自動セットされます。 ```ts const result = gassma.Users.create({ data: { id: 4, name: "Dave", email: "dave@example.com", profile: { create: { id: 3, bio: "新しく来ました" }, }, }, }); ``` 上記を実行すると以下が行われます。 1. Users シートに Dave が作成される 2. Profiles シートに `{ id: 3, userId: 4, bio: "新しく来ました" }` が作成される(`userId` は Dave の `id` = 4 が自動セット) ### oneToMany での create ユーザー作成と同時に投稿も作成します。 ```ts const result = gassma.Users.create({ data: { id: 4, name: "Dave", email: "dave@example.com", posts: { create: [ { id: 4, title: "Dave の記事1", published: true }, { id: 5, title: "Dave の記事2", published: false }, ], }, }, }); ``` 上記を実行すると以下が行われます。 1. Users シートに Dave が作成される 2. Posts シートに 2 件の記事が作成される(`authorId` は Dave の `id` = 4 が自動セット) 配列ではなく単一オブジェクトで 1 件だけ作成することもできます。 ```ts posts: { create: { id: 4, title: "Dave の記事", published: true }, } ``` ### manyToMany での create 投稿作成と同時にタグも作成し、中間テーブルに関連付けます。 ```ts const result = gassma.Posts.create({ data: { id: 4, title: "新しい記事", authorId: 1, published: true, tags: { create: { id: 3, name: "TypeScript" }, }, }, }); ``` 上記を実行すると以下が行われます。 1. Posts シートに新しい記事が作成される 2. Tags シートに "TypeScript" タグが作成される 3. PostTags シートに `{ postId: 4, tagId: 3 }` が作成される ## createMany oneToMany で複数の子レコードを一括作成します。 ```ts const result = gassma.Users.create({ data: { id: 4, name: "Dave", email: "dave@example.com", posts: { createMany: { data: [ { id: 4, title: "記事1", published: true }, { id: 5, title: "記事2", published: false }, ], }, }, }, }); ``` 各レコードの `authorId` には Dave の `id` が自動セットされます。 ## connect 既存のレコードを関連付けます。`where` 条件で対象レコードを指定します。 ### manyToOne での connect 既存のユーザーと紐づけて投稿を作成します。 ```ts const result = gassma.Posts.create({ data: { id: 4, title: "新しい記事", published: true, author: { connect: { name: "Alice" }, }, }, }); ``` 上記を実行すると以下が行われます。 1. Users シートから `name: "Alice"` のレコードを検索 2. 見つかった Alice の `id`(= 1)が Posts の `authorId` に自動セットされる 3. Posts シートに新しい記事が作成される 条件に一致するレコードが見つからない場合、`NestedWriteConnectNotFoundError` がスローされます。 ### oneToOne での connect ユーザー作成と同時に、既存のプロフィールを紐づけます。 ```ts const result = gassma.Users.create({ data: { id: 4, name: "Dave", email: "dave@example.com", profile: { connect: { id: 1 }, }, }, }); ``` 上記を実行すると、Profiles シートの `id: 1` の `userId` が Dave の `id`(= 4)に更新されます。 oneToOne の connect は**置き換え**として動作します。親にすでに接続されているリレーション先レコードがある場合、そのレコードの FK を `null` にしてから、対象レコードの FK を親にセットします。 条件に一致するレコードが見つからない場合、`NestedWriteConnectNotFoundError` がスローされます。 ### oneToMany での connect ユーザー作成と同時に、既存の投稿を紐づけます。 ```ts const result = gassma.Users.create({ data: { id: 4, name: "Dave", email: "dave@example.com", posts: { connect: [ { title: "下書き記事" }, ], }, }, }); ``` 上記を実行すると、Posts シートの「下書き記事」の `authorId` が Dave の `id`(= 4)に更新されます。 ### manyToMany での connect 既存のタグと投稿を関連付けます。 ```ts const result = gassma.Posts.create({ data: { id: 4, title: "新しい記事", authorId: 1, published: true, tags: { connect: [ { name: "GAS" }, { name: "JavaScript" }, ], }, }, }); ``` 上記を実行すると以下が行われます。 1. Posts シートに新しい記事が作成される 2. PostTags シートに `{ postId: 4, tagId: 1 }` と `{ postId: 4, tagId: 2 }` が作成される Tags シートのレコード自体は変更されません。 ## connectOrCreate 既存のレコードがあれば関連付け、なければ新規作成して関連付けます。 ```ts const result = gassma.Posts.create({ data: { id: 4, title: "新しい記事", published: true, author: { connectOrCreate: { where: { name: "Alice" }, create: { id: 4, name: "Alice", email: "alice-new@example.com", }, }, }, }, }); ``` 上記の場合、Users シートに Alice が存在するため connect と同じ動作になります。存在しない場合は `create` のデータで新規作成されます。 oneToOne(非FK側)でも同様に、見つかった場合は connect と同じ置き換え動作、見つからない場合はリレーション先レコードが FK 自動セットで作成されます。 oneToMany / manyToMany では配列で複数指定できます。 ```ts tags: { connectOrCreate: [ { where: { name: "GAS" }, create: { id: 3, name: "GAS" }, }, { where: { name: "新しいタグ" }, create: { id: 4, name: "新しいタグ" }, }, ], } ``` ## 深いネスト Nested write は再帰的に処理されるため、深い階層のリレーションも一度に作成できます。 例えば、ユーザー → 投稿 → タグ を一度に作成する場合: ```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 の記事", published: true, tags: { create: { id: 3, name: "TypeScript" }, }, }, }, }, }); ``` 上記を実行すると以下の順序で処理が行われます。 1. Users シートに Dave が作成される 2. Posts シートに記事が作成される(`authorId: 4` が自動セット) 3. Tags シートに "TypeScript" が作成される 4. PostTags シートに関連行が作成される ## 注意事項 - Nested write は `create` メソッドのみで利用できます。`createMany` / `updateMany` 等では利用できません。 - FK は自動セットされますが、PK(id 等)は明示的に指定する必要があります。オートインクリメント機能はありません。 - `connect` で指定した `where` 条件に一致するレコードが見つからない場合、`NestedWriteConnectNotFoundError` がスローされます。 - Nested write は複数のシートに書き込みますが、途中でエラーになった場合はどのシートにも 1 行も書かれません。詳細は[書き込みの原子性と同時実行](/docs/reference/write-atomicity)を参照してください。 ## バリデーション | エラー | 原因 | | --- | --- | | `NestedWriteWithoutRelationsError` | リレーション定義なしで nested write 構文を使用 | | `NestedWriteConnectNotFoundError` | `connect` / `connectOrCreate` の where でレコードが見つからない | | `NestedWriteInvalidOperationError` | リレーション種別に対応しない操作を指定(例: oneToOne に `createMany` や配列形式を指定) | # onUpdate (https://gassma.io/docs/reference/relation/on-update) 更新時の参照アクション(Cascade、SetNull、Restrict、NoAction) `update` / `updateMany` / `updateManyAndReturn` でレコードの PK(主キー)を変更する際、リレーション先のレコードをどう扱うかを定義します。 ## 説明例用のシート [リレーション定義](/docs/reference/relation/definition)のシート例を使用します。 ## 基本的な使い方 [リレーション定義](/docs/reference/relation/definition)で `onUpdate` を指定します。 ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onUpdate: "Cascade", }, }, }, }); ``` onUpdate は FK を保有する側(`manyToOne`)のリレーション定義では発火しません。参照される側(`oneToMany` / 非FK側の `oneToOne` / `manyToMany`)の定義に指定してください。 ## アクションの種類 | アクション | 動作 | | --- | --- | | Cascade | 関連レコードの FK を新しい値に自動更新 | | SetNull | 関連レコードの FK を null にする | | Restrict | 関連レコードが存在する場合、更新をエラーで阻止 | | NoAction | 何もしない(デフォルト) | ## Cascade 親レコードの PK を変更すると、関連する子レコードの FK が新しい値に自動的に更新されます。複数のシートへの書き込みになりますが、途中でエラーになった場合はどのシートも変更されません(詳細は[書き込みの原子性と同時実行](/docs/reference/write-atomicity)を参照)。 ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onUpdate: "Cascade", }, }, }, }); // Alice の id を 1 → 10 に変更すると、Posts の authorId: 1 も authorId: 10 に更新される gassma.Users.updateMany({ where: { name: "Alice" }, data: { id: 10 }, }); ``` 更新後の Posts シートは以下のようになります。 | id | title | authorId | published | | --- | --- | --- | --- | | 1 | 初めての投稿 | 10 | true | | 2 | GAS の使い方 | 10 | true | | 3 | 下書き記事 | 2 | false | ### manyToMany の場合 manyToMany で Cascade を指定すると、**中間テーブル**の対応カラムが新しい値に更新されます。 ```ts relations: { Posts: { tags: { type: "manyToMany", to: "Tags", field: "id", reference: "id", through: { sheet: "PostTags", field: "postId", reference: "tagId", }, onUpdate: "Cascade", }, }, } // 投稿の id を 1 → 100 に変更すると、PostTags の postId: 1 も postId: 100 に更新される gassma.Posts.updateMany({ where: { id: 1 }, data: { id: 100 }, }); ``` ## SetNull 親レコードの PK を変更すると、関連する子レコードの FK が `null` に更新されます。 ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onUpdate: "SetNull", }, }, }, }); // Alice の id を変更すると、Alice の投稿の authorId が null になる gassma.Users.updateMany({ where: { name: "Alice" }, data: { id: 10 }, }); ``` 更新後の Posts シートは以下のようになります。 | id | title | authorId | published | | --- | --- | --- | --- | | 1 | 初めての投稿 | null | true | | 2 | GAS の使い方 | null | true | | 3 | 下書き記事 | 2 | false | manyToMany で SetNull を指定した場合、何も行われません(中間テーブルの FK を null にしても意味がないため)。 ## Restrict 関連するレコードが 1 件でも存在する場合、更新を拒否してエラーをスローします。 ```ts const gassma = new Gassma.GassmaClient({ relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onUpdate: "Restrict", }, }, }, }); // Alice は投稿を持っているため、id の変更はエラーになる gassma.Users.updateMany({ where: { name: "Alice" }, data: { id: 10 }, }); // => RelationOnUpdateRestrictError // Charlie は投稿を持たないため、正常に更新される gassma.Users.updateMany({ where: { name: "Charlie" }, data: { id: 10 }, }); ``` Restrict のチェックは全てのリレーションに対して**先に**行われます。そのため、エラーが発生しても副作用(他のリレーションの Cascade 等)は実行されません。 ## NoAction 何も行いません。`onUpdate` を指定しない場合と同じ動作です。 ```ts relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onUpdate: "NoAction", // 指定しない場合と同じ }, }, } ``` ## onDelete との組み合わせ `onDelete` と `onUpdate` は同じリレーション定義に同時に指定できます。 ```ts relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId", onDelete: "Cascade", // 削除時: 投稿も一緒に削除 onUpdate: "Cascade", // PK更新時: 投稿の FK も更新 }, }, } ``` # Nested Write(update) (https://gassma.io/docs/reference/relation/nested-write-update) update の中で update/delete/deleteMany/disconnect/set を使って関連レコードを変更する `update` メソッドの `data` 内でリレーション先のレコードを同時に操作できます。 [create の Nested Write](/docs/reference/relation/nested-write) で使える操作に加えて、`update` / `delete` / `deleteMany` / `disconnect` / `set` 操作が利用できます。 複数のシートに書き込みますが、途中でエラーになった場合はどのシートにも 1 行も書かれません。詳細は[書き込みの原子性と同時実行](/docs/reference/write-atomicity)を参照してください。 ## 説明例用のシート [リレーション定義](/docs/reference/relation/definition)のシート例を使用します。 ## 使用できる操作 | 操作 | manyToOne / oneToOne | oneToMany | manyToMany | | --- | --- | --- | --- | | create | 単一のみ | 単一 / 配列 | 単一 / 配列 | | createMany | - | 対応 | - | | connect | 対応 | 単一 / 配列 | 単一 / 配列 | | connectOrCreate | 対応 | 単一 / 配列 | 単一 / 配列 | | update | 対応 | 単一 / 配列 | - | | delete | 対応 | 単一 / 配列 | - | | deleteMany | - | 単一 / 配列 | - | | disconnect | 対応 | 単一 / 配列 | 単一 / 配列 | | set | - | 対応 | 対応 | manyToOne と oneToOne は使える操作の形は同じですが、動作が異なります。manyToOne(FK 保有側)は**自レコードの FK** を操作し、oneToOne(非FK側)は **FK を保有するリレーション先レコード**を操作します([リレーション定義](/docs/reference/relation/definition)を参照)。 ### oneToOne(非FK側)の挙動 | 操作 | 動作 | 対象レコードが存在しない場合 | | --- | --- | --- | | create | リレーション先レコードを FK 自動セットで作成 | - | | connect | 置き換え(既接続レコードの FK を null 化してから、対象レコードの FK を親にセット) | `NestedWriteConnectNotFoundError` | | connectOrCreate | 存在すれば connect と同じ置き換え、なければ FK 自動セットで作成 | - | | update | リレーション先レコードを更新(data を直接指定) | `NestedWriteTargetNotFoundError` | | disconnect: true | リレーション先レコードの FK を null 化 | 何もしない | | delete: true | リレーション先レコードを削除 | `NestedWriteTargetNotFoundError` | `set` / `deleteMany` / `createMany` / 配列形式の指定は `NestedWriteInvalidOperationError` になります。 ## create リレーション先のレコードを新規作成して関連付けます。[create の Nested Write](/docs/reference/relation/nested-write) と同じ動作です。 ```ts // oneToMany: ユーザー更新時に新しい投稿を作成 gassma.Users.update({ where: { name: "Alice" }, data: { posts: { create: { id: 4, title: "新しい投稿", published: true }, }, }, }); ``` ## connect 既存のリレーション先レコードを関連付けます。 ```ts // manyToOne: 投稿の著者を既存ユーザーに変更 gassma.Posts.update({ where: { id: 1 }, data: { author: { connect: { name: "Bob" }, }, }, }); ``` oneToOne(非FK側)では**置き換え**として動作します。すでに接続されているリレーション先レコードの FK を `null` にしてから、対象レコードの FK を親にセットします。 ```ts // oneToOne: ユーザーのプロフィールを別のプロフィールに置き換え gassma.Users.update({ where: { name: "Alice" }, data: { profile: { connect: { id: 2 }, }, }, }); // => Profiles の id: 1(既接続)の userId が null になった後、 // id: 2 の userId が Alice の id(= 1)に更新される ``` ## connectOrCreate 既存のレコードがあれば関連付け、なければ新規作成して関連付けます。 ```ts // manyToOne: 著者が存在すれば接続、なければ作成 gassma.Posts.update({ where: { id: 1 }, data: { author: { connectOrCreate: { where: { name: "Dave" }, create: { id: 4, name: "Dave", email: "dave@example.com" }, }, }, }, }); ``` ## update 関連するレコードを更新します。 ### manyToOne(FK 保有側) 更新データを直接指定します。自レコードの FK が参照しているレコードが更新されます。 ```ts // manyToOne: 投稿の著者名を更新 gassma.Posts.update({ where: { id: 1 }, data: { author: { update: { name: "Alice Updated" }, }, }, }); ``` ### oneToOne(非FK側) 同じく更新データを直接指定します。親を参照しているリレーション先レコードが更新されます。 ```ts // oneToOne: ユーザーのプロフィールを更新 gassma.Users.update({ where: { name: "Alice" }, data: { profile: { update: { bio: "更新後の自己紹介" }, }, }, }); ``` 接続されているリレーション先レコードが存在しない場合、`NestedWriteTargetNotFoundError` がスローされます。 ### oneToMany `where` と `data` を指定して更新対象を絞り込みます。配列で複数指定も可能です。 ```ts // oneToMany: 特定の投稿を更新 gassma.Users.update({ where: { name: "Alice" }, data: { posts: { update: { where: { id: 1 }, data: { title: "更新後のタイトル" }, }, }, }, }); // 複数の投稿を同時に更新 gassma.Users.update({ where: { name: "Alice" }, data: { posts: { update: [ { where: { id: 1 }, data: { title: "タイトルA" } }, { where: { id: 2 }, data: { title: "タイトルB" } }, ], }, }, }); ``` ## delete 関連するレコードを削除します。 ### manyToOne(FK 保有側) `delete: true` を指定すると、関連先のレコードを削除し、自身の FK を `null` に設定します。 ```ts // manyToOne: 投稿の著者を削除(投稿の authorId は null になる) gassma.Posts.update({ where: { id: 1 }, data: { author: { delete: true }, }, }); ``` ### oneToOne(非FK側) `delete: true` を指定すると、親を参照しているリレーション先レコードを削除します。自レコードは変更されません。 ```ts // oneToOne: ユーザーのプロフィールを削除 gassma.Users.update({ where: { name: "Alice" }, data: { profile: { delete: true }, }, }); // => Profiles の userId: 1 のレコードが削除される ``` 接続されているリレーション先レコードが存在しない場合、`NestedWriteTargetNotFoundError` がスローされます。 ### oneToMany `where` 条件を指定して削除対象を絞り込みます。配列で複数指定も可能です。 ```ts // oneToMany: 特定の投稿を削除 gassma.Users.update({ where: { name: "Alice" }, data: { posts: { delete: { id: 3 }, }, }, }); // 複数削除 gassma.Users.update({ where: { name: "Alice" }, data: { posts: { delete: [{ id: 2 }, { id: 3 }], }, }, }); ``` ## deleteMany 条件に合致する関連レコードを一括削除します。oneToMany でのみ使用できます。 ```ts // oneToMany: 未公開の投稿を全て削除 gassma.Users.update({ where: { name: "Alice" }, data: { posts: { deleteMany: { published: false }, }, }, }); // 複数条件で削除 gassma.Users.update({ where: { name: "Alice" }, data: { posts: { deleteMany: [ { published: false }, { title: "下書き" }, ], }, }, }); ``` ## disconnect リレーションの関連付けを解除します。レコード自体は削除されません。 ### manyToOne(FK 保有側) `disconnect: true` を指定すると、自身の FK を `null` に設定します。 ```ts // manyToOne: 投稿と著者の関連付けを解除 gassma.Posts.update({ where: { id: 1 }, data: { author: { disconnect: true }, }, }); // => Posts の authorId が null になる ``` ### oneToOne(非FK側) `disconnect: true` を指定すると、親を参照しているリレーション先レコードの FK を `null` に設定します。 ```ts // oneToOne: ユーザーとプロフィールの関連付けを解除 gassma.Users.update({ where: { name: "Alice" }, data: { profile: { disconnect: true }, }, }); // => Profiles の userId: 1 が null になる ``` 接続されているレコードが存在しない場合は何も行われません(エラーになりません)。 ### oneToMany `where` 条件を指定して、関連レコードの FK を `null` に設定します。 ```ts // oneToMany: 特定の投稿の関連付けを解除 gassma.Users.update({ where: { name: "Alice" }, data: { posts: { disconnect: { id: 1 }, }, }, }); // => Posts の id: 1 の authorId が null になる // 複数の関連付けを解除 gassma.Users.update({ where: { name: "Alice" }, data: { posts: { disconnect: [{ id: 1 }, { id: 2 }], }, }, }); ``` ### manyToMany 中間テーブルのレコードを削除します。 ```ts // manyToMany: タグの関連付けを解除 gassma.Posts.update({ where: { id: 1 }, data: { tags: { disconnect: { id: 3 }, }, }, }); // => PostTags テーブルから対応するレコードが削除される ``` ## set リレーションの関連付けを全て入れ替えます。oneToMany と manyToMany でのみ使用できます。 ### oneToMany 全ての子レコードの FK を `null` に設定した後、指定したレコードの FK を親に設定します。 ```ts // oneToMany: Alice の投稿を id: 1 と id: 2 のみに置換 gassma.Users.update({ where: { name: "Alice" }, data: { posts: { set: [{ id: 1 }, { id: 2 }], }, }, }); // => 既存の全投稿の authorId が null になった後、 // id: 1 と id: 2 の authorId が Alice の id に設定される ``` ### manyToMany 中間テーブルのレコードを全削除した後、指定したレコードとの関連を新規作成します。 ```ts // manyToMany: 投稿のタグを完全に入れ替え gassma.Posts.update({ where: { id: 1 }, data: { tags: { set: [{ id: 10 }, { id: 11 }], }, }, }); // => PostTags から投稿 id: 1 の全レコードが削除された後、 // 新しい関連レコードが作成される ``` ## 複数操作の組み合わせ 1 つの update 内で複数のリレーション操作を組み合わせることも可能です。 ```ts gassma.Users.update({ where: { name: "Alice" }, data: { name: "Alice Updated", posts: { create: { id: 5, title: "新記事", published: true }, update: { where: { id: 1 }, data: { title: "更新済み" } }, delete: { id: 3 }, }, }, }); ``` ## エラー | エラー | 原因 | | --- | --- | | `NestedWriteWithoutRelationsError` | リレーション定義なしで Nested Write を実行した | | `NestedWriteInvalidOperationError` | リレーション種別に対応しない操作を指定した | | `NestedWriteConnectNotFoundError` | connect / connectOrCreate で対象レコードが見つからなかった | | `NestedWriteTargetNotFoundError` | 非FK側 oneToOne の update / delete でリレーション先レコードが存在しなかった | # changeSettings() (https://gassma.io/docs/reference/settings/changeSettings) シートのデータ範囲(開始行・列範囲)を設定する スプレッドシートの読み込み範囲を設定したい場合に利用します。 ## 引数 | 引数名 | 説明 | 型 | 備考 | | ---------------- | ---------------------------------- | ------------------ | ---------------------------------------------- | | startRowNumber | 列名が書いてある行番号を指定 | `number` | | startColumnValue | 最初の列名が書いてある列番号を指定 | `number \| string` | 列番号を指定するか列のアルファベットを指定する | | endColumnValue | 最後の列名が書いてある列番号を指定 | `number \| string` | 列番号を指定するか列のアルファベットを指定する | ## 列の指定方法 `startColumnValue` / `endColumnValue` には、**列番号(`number`)**または**列のアルファベット(`string`)**のどちらでも指定できます。 アルファベットは大文字・小文字を問わず、スプレッドシートの列見出しと同じ base-26 で解釈されます。`"A"`〜`"Z"` が 1〜26、`"AA"` から 2 文字に繰り上がります。 | 指定 | 列番号 | | ------ | ------ | | `"A"` | 1 | | `"Z"` | 26 | | `"AA"` | 27 | | `"AZ"` | 52 | | `"BA"` | 53 | ```ts // 以下の 2 つは同じ意味になります gassma.sheet1.changeSettings(1, "B", "E"); gassma.sheet1.changeSettings(1, 2, 5); ``` アルファベット以外の文字(数字・記号・空文字など)を含む文字列を指定すると `GassmaInValidColumnValueError` がスローされます。数値で指定したい場合は文字列ではなく `number` を渡してください。 ## どう言ったときに利用するか 以下の状態にある場合は必ずスプレッドシートの操作を行う前に**必ず**`changeSettings()`を行ってください。 ### 1. テーブルが左上にないとき ![左上にないテーブル](./img/settingExample.png) 以上のテーブルの場合は以下のようなコードを書くことで正常にテーブルを読み込めます。 ```ts const gassma = new Gassma.GassmaClient(); // シートの操作をする前に必ず記述 gassma.sheet1.changeSettings(4, "B", "E"); const result = gassma.sheet1.findMany({}); ``` ### 2. 右側にメモ書き等別のデータがあるとき ![別のデータがあるテーブル](./img/settingExample2.png) ```ts const gassma = new Gassma.GassmaClient(); // シートの操作をする前に必ず記述 gassma.sheet1.changeSettings(1, "A", "D"); const result = gassma.sheet1.findMany({}); ``` # グローバル omit (https://gassma.io/docs/reference/config/global-omit) シートごとに、結果からデフォルトで除外するフィールドを設定する `GassmaClient` のコンストラクタで `omit` を指定すると、指定したシートの全クエリでデフォルトのフィールド除外が適用されます。 ## 基本的な使い方 ```ts const gassma = new Gassma.GassmaClient({ omit: { Users: { password: true, secret: true, }, Posts: { internalNotes: true, }, }, }); // Users シートから取得時、password と secret が自動的に除外される const users = gassma.Users.findMany({}); // => [{ id: 1, name: "Alice", email: "alice@example.com" }, ...] // password と secret フィールドは含まれない ``` ## 優先順位 グローバル omit、クエリレベルの `omit`、`select` には以下の優先順位があります。 | 優先順位 | 条件 | 動作 | | --- | --- | --- | | 1(最優先) | `select` が指定 | グローバル omit とクエリ omit を無視 | | 2 | クエリ `omit` が指定 | グローバル omit とマージ | | 3 | どちらも未指定 | グローバル omit をそのまま適用 | ### select でグローバル omit を無視 `select` を指定すると、グローバル omit は適用されません。 ```ts // グローバル omit: { password: true } const result = gassma.Users.findMany({ select: { name: true, password: true }, }); // => [{ name: "Alice", password: "secret123" }] // select が最優先のため password も取得できる ``` ### クエリ omit でグローバル omit を上書き クエリレベルの `omit` で `false` を指定すると、グローバル omit を個別に無効化できます。 ```ts // グローバル omit: { password: true, secret: true } const result = gassma.Users.findMany({ omit: { password: false }, }); // => [{ id: 1, name: "Alice", password: "secret123" }] // password のグローバル omit が無効化され、secret のみ除外される ``` クエリ `omit` で `true` を指定すると、追加の除外フィールドを指定できます。 ```ts // グローバル omit: { password: true } const result = gassma.Users.findMany({ omit: { email: true }, }); // => [{ id: 1, name: "Alice" }] // password(グローバル)と email(クエリ)の両方が除外される ``` ## 対応メソッド グローバル omit は以下のメソッドに適用されます。 | メソッド | 適用 | | --- | --- | | `findMany` | ✅ | | `findFirst` / `findFirstOrThrow` | ✅ | | `create` | ✅ | | `update` | ✅ | | `upsert` | ✅ | | `delete` | ✅ | | `createManyAndReturn` | ✅ | | `updateManyAndReturn` | ✅ | `createMany`、`updateMany`、`deleteMany` は `{ count: number }` を返すため、グローバル omit の影響を受けません。 ## リレーションとの組み合わせ ```ts const gassma = new Gassma.GassmaClient({ omit: { Users: { password: true }, }, relations: { Users: { posts: { type: "oneToMany", to: "Posts", field: "id", reference: "authorId" }, }, }, }); // リレーションと一緒に使用できる const result = gassma.Users.findMany({ include: { posts: true }, }); // => [{ id: 1, name: "Alice", posts: [...] }] // password が除外されつつ、リレーション先のデータも取得 ``` ## バリデーション | エラー | 原因 | | --- | --- | | `GassmaFindSelectOmitConflictError` | クエリレベルで `select` と `omit` を同時指定 | # defaults(@default) (https://gassma.io/docs/reference/config/defaults) 作成時にフィールド値を固定値または関数で自動設定する(@default) Prisma の `@default()` に相当する機能です。`create` 時にフィールドのデフォルト値を自動設定します。 ## 基本的な使い方 ```ts const gassma = new Gassma.GassmaClient({ defaults: { Users: { role: "USER", createdAt: () => new Date(), }, }, }); // create 時にデフォルト値が自動適用 gassma.Users.create({ data: { name: "Alice" }, }); // => { name: "Alice", role: "USER", createdAt: 2026-03-14T... } ``` ## 静的値と関数 デフォルト値には固定値と関数の両方を指定できます。 | 指定方法 | 例 | 動作 | | --- | --- | --- | | 静的値 | `role: "USER"` | 毎回同じ値を設定 | | 関数 | `createdAt: () => new Date()` | 呼び出しごとに評価 | ## 適用されるメソッド | メソッド | 適用 | | --- | --- | | `create` | ✅ | | `createMany` / `createManyAndReturn` | ✅ | | `upsert`(create 部分のみ) | ✅ | ## 明示指定時の動作 フィールドが明示的に指定されている場合(`null` を含む)、デフォルト値は適用されません。 ```ts gassma.Users.create({ data: { name: "Alice", role: "ADMIN" }, }); // => role は "ADMIN"(デフォルト値 "USER" は適用されない) ``` ## デフォルト値の検証 デフォルト値として適用される値も、`data` に直接書いた値と同じ検証を受けます。セルに保存できない値を返した場合は `GassmaInvalidValueError` がスローされ、行は 1 件も書き込まれません。 ```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. ``` `{argumentName}` にはカラム名が入ります。`NaN` / `Infinity` / `-Infinity`、不正な Date(Invalid Date)、`Date` 以外のオブジェクトなどが対象です。詳しくは[エラー一覧](/docs/reference/errors#セルに保存できない値)を参照してください。 # updatedAt(@updatedAt) (https://gassma.io/docs/reference/config/updated-at) 作成・更新時にタイムスタンプを自動設定する(@updatedAt) Prisma の `@updatedAt` に相当する機能です。レコードの作成・更新時に指定カラムへ自動的に現在時刻をセットします。 ## 基本的な使い方 ```ts const gassma = new Gassma.GassmaClient({ updatedAt: { Users: "updatedAt", }, }); // 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... }(自動更新) ``` ## 複数カラム対応 配列で複数カラムを指定できます。 ```ts const gassma = new Gassma.GassmaClient({ updatedAt: { Posts: ["updatedAt", "lastModified"], }, }); ``` ## 適用されるメソッド | メソッド | 適用 | | --- | --- | | `create` / `createMany` / `createManyAndReturn` | ✅ | | `update` / `updateMany` / `updateManyAndReturn` | ✅ | | `upsert`(create・update 両方) | ✅ | ## 明示指定時の動作 ユーザーが明示的に値を指定した場合、そちらが優先されます。 ## 注意事項 `onDelete` / `onUpdate` の連鎖更新では `updatedAt` は適用されません(Prisma と同様の挙動)。 # ignore / ignoreSheets(@ignore / @@ignore) (https://gassma.io/docs/reference/config/ignore) フィールドやシート全体をすべての操作の対象から除外する(@ignore / @@ignore) Prisma の `@ignore`(フィールドレベル)と `@@ignore`(モデルレベル)に相当する機能です。 ## ignore(フィールドレベル) 指定したフィールドを全操作から完全に除外します。 ```ts const gassma = new Gassma.GassmaClient({ ignore: { Users: ["secretColumn", "internalData"], }, }); // 読み取り結果から除外される gassma.Users.findMany({}); // => [{ id: 1, name: "Alice" }](secretColumn, internalData は含まれない) // 書き込みデータからも除外される gassma.Users.create({ data: { name: "Alice", secretColumn: "xxx" }, }); // => secretColumn は無視される ``` 単一カラムの場合は文字列で指定できます。 ```ts ignore: { Users: "secretColumn", } ``` ### 除外される箇所 - **読み取り結果**: find / create / update / delete / upsert の返り値 - **書き込みデータ**: create / createMany / upsert の data - **where 条件**: where 句からも除外 ### グローバル omit との違い | | `ignore` | グローバル `omit` | | --- | --- | --- | | オーバーライド | 不可 | `omit: \{field: false\}` で無効化可能 | | 書き込み除外 | ✅ | ❌(読み取りのみ) | | where 除外 | ✅ | ❌ | ## ignoreSheets(モデルレベル) 指定したシートをクライアントから完全に除外します。 ```ts const gassma = new Gassma.GassmaClient({ ignoreSheets: ["Logs", "Temp"], }); // gassma.Logs → undefined(除外済み) // gassma.Users → 通常通り利用可能 ``` 単一シートの場合は文字列で指定できます。 ```ts ignoreSheets: "Logs", ``` # map / mapSheets(@map / @@map) (https://gassma.io/docs/reference/config/map) コード側の名前をスプレッドシートのヘッダー名・シート名に対応付ける(@map / @@map) Prisma の `@map("name")`(フィールドレベル)と `@@map("name")`(モデルレベル)に相当する機能です。コード上の名前とスプレッドシート上の名前をマッピングします。 ## map(フィールドレベル) コード上のフィールド名とスプレッドシートのヘッダー名を異なる名前でマッピングします。 ```ts const gassma = new Gassma.GassmaClient({ map: { Users: { firstName: "名前", lastName: "名字", }, }, }); // コード上は英語名で操作 gassma.Users.create({ data: { firstName: "Alice", lastName: "Smith" }, }); // → スプレッドシートの「名前」「名字」カラムに書き込まれる gassma.Users.findFirst({ where: { firstName: "Alice" }, }); // => \{ firstName: "Alice", lastName: "Smith" \} ``` ### 変換が適用される箇所 - **書き込みデータ**: create / createMany / update / updateMany / upsert でコード名→ヘッダー名に変換 - **読み取り結果**: find / create / update / upsert の返り値でヘッダー名→コード名に変換 - **where 条件**: コード名→ヘッダー名に変換してからフィルタ ## mapSheets(モデルレベル) コード上のモデル名とスプレッドシートのシート名をマッピングします。 ```ts const gassma = new Gassma.GassmaClient({ mapSheets: { Users: "ユーザー一覧", Posts: "投稿データ", }, }); // コード上は英語名でアクセス gassma.Users.findMany({}); // → 内部ではシート名「ユーザー一覧」に対して操作 ``` ### 他オプションとの組み合わせ `mapSheets` を指定した場合、他のオプション(`omit`、`defaults`、`updatedAt`、`ignore`、`map` 等)にはコード名を使用します。 ```ts const gassma = new Gassma.GassmaClient({ mapSheets: { Users: "ユーザー一覧", }, defaults: { Users: { role: "USER" }, // ← "ユーザー一覧" ではなく "Users" }, }); ``` # autoincrement (https://gassma.io/docs/reference/config/autoincrement) LockService + PropertiesService を使ったフィールドの自動インクリメント(GAS のみ) Prisma の `autoincrement()` に相当する機能です。`create` 時に一意で単調増加する値を自動的に割り当てます。 ## 基本的な使い方 ```ts const gassma = new Gassma.GassmaClient({ autoincrement: { Users: "id", }, }); // create 時に自動で id が振られる gassma.Users.create({ data: { name: "Alice" }, }); // => \{ id: 1, name: "Alice" \} gassma.Users.create({ data: { name: "Bob" }, }); // => \{ id: 2, name: "Bob" \} ``` ## 複数カラム対応 配列で複数カラムを指定できます。 ```ts autoincrement: { Users: ["id", "seq"], } ``` ## createMany での動作 `createMany` では全行分のカウンターを一括確保してから各行に割り当てます。 ```ts gassma.Users.createMany({ data: [{ name: "Alice" }, { name: "Bob" }], }); // => id: 1, 2 がそれぞれ割り当てられる ``` ## 仕組み 1. `LockService.getScriptLock().waitLock(10000)` で排他制御 2. `PropertiesService.getScriptProperties()` からカウンターを読み取り 3. +1(createMany の場合は +N)して書き込み 4. ロック解放 GAS の `LockService` と `PropertiesService` を使用するため、GAS 環境でのみ動作します。 ## 明示指定時の動作 フィールドに明示的に値を指定した場合、自動採番はスキップされます。 ```ts gassma.Users.create({ data: { id: 100, name: "Alice" }, }); // => id は 100(自動採番されない) ``` # strictUndefinedChecks / Gassma.skip (https://gassma.io/docs/reference/config/strict-undefined-checks) Prisma の strictUndefinedChecks(Preview 機能)と Prisma.skip に相当する機能です。クエリ入力に紛れ込んだ意図しない undefined を実行時エラーとして検出し、フィールドを省略したい場合は Gassma.skip で明示的に指定できるようになります。 Prisma の `strictUndefinedChecks`(Preview 機能)と `Prisma.skip` に相当する機能です。クエリ入力に紛れ込んだ意図しない `undefined` を実行時エラーとして検出し、フィールドを省略したい場合は `Gassma.skip` で明示的に指定できるようになります。 ## Gassma.skip `Gassma.skip` はクエリのフィールド値として渡すと、そのフィールドを「指定しなかった」ことにするシンボルです。 ```ts const search: string | undefined = getSearchWord(); const users = gassma.Users.findMany({ where: { // search がない場合は name の条件自体を省く name: search ?? Gassma.skip, }, }); ``` `Gassma.skip` は `strictUndefinedChecks` の有効・無効に関わらず常に使用できます。`where` / `data` / `create` / `update` / `select` / `omit` / `orderBy` など、クエリ入力のあらゆる箇所で利用可能です。 ## strictUndefinedChecks の有効化 `strictUndefinedChecks` はオプトインの機能です。有効化する方法は 2 つあります。 ### previewFeatures で有効化(CLI あり) [Prisma スキーマを利用したローカル開発](/docs/reference/type-generation)をしている場合は、`schema.prisma` の `generator` ブロックに `previewFeatures` を追加します(Prisma と同じ書き方です)。 ```prisma generator client { provider = "prisma-client-js" output = "./generated/gassma" previewFeatures = ["strictUndefinedChecks"] } ``` `npx gassma generate` を実行すると、生成されるクライアントに `strictUndefinedChecks: true` が自動的に埋め込まれます。あわせて生成される型定義にも `Gassma.skip` を受け付ける型(`Gassma.SkipValue`)が反映されます。 ### コンストラクタで有効化(CLI なし) CLI を使わない構成では、`GassmaClient` のコンストラクタで指定します。 ```ts const gassma = new Gassma.GassmaClient({ strictUndefinedChecks: true, }); ``` ## 有効時の挙動 有効化すると、クエリ入力に**明示的な `undefined`** が含まれる場合に `GassmaUndefinedValueError` がスローされます。ネストした入力(Nested Write、`include` 内の `select` など)も再帰的にチェックされます。 ```ts const userName = undefined; gassma.Users.deleteMany({ where: { name: userName }, }); // => GassmaUndefinedValueError: // Invalid value for argument `where.name`: explicitly `undefined` values are not allowed. ``` 無効時(デフォルト)は、後述のとおり `undefined` は「そのフィールドを指定しなかった」扱いになります。意図しない `undefined` が紛れ込むと、条件が静かに消えて対象行が広がる(上の例なら `deleteMany` が全件削除になる)事故につながります。有効化しておけば、こうしたバグを実行時に即座に検出できます。 フィールドを省略したい場合は、`undefined` の代わりに `Gassma.skip` を使用してください。 ```ts gassma.Users.deleteMany({ where: { name: userName ?? Gassma.skip }, }); // name の条件が省かれた状態で実行される ``` ## 無効時の挙動(デフォルト) `strictUndefinedChecks` が無効の場合、クエリ入力の `undefined` は Prisma と同じく**「そのフィールドを指定しなかった」扱い**になります。`where` の条件・演算子の中(`equals` / `gt` / `in` など)・`AND` / `OR` / `NOT` の中・リレーションフィルタ・`orderBy`・`select` のキーなど、クエリ入力のあらゆる箇所で同様です。 ```ts // age の条件は「指定しなかった」扱いになり、全件が返る gassma.Users.findMany({ where: { age: undefined }, }); ``` `update` の `data` に `undefined` を渡した場合も同様に、**そのフィールドは更新されません**(セルの値は保持されます)。 ```ts gassma.Users.update({ where: { id: 1 }, data: { name: undefined, age: 21 }, }); // => name は元の値のまま、age だけが 21 に更新される ``` `where` の条件が `undefined`(または `Gassma.skip`)だけで空になった場合、`findMany` / `updateMany` / `deleteMany` などでは**全件が対象**になります。単一行操作の `update` / `delete` / `upsert` では空の `where` は `GassmaInvalidValueError` になります([update](/docs/reference/crud/update/update) を参照)。 ## exactOptionalPropertyTypes の推奨 `undefined` の代入を型レベルでも完全に禁止するには、利用側プロジェクトの `tsconfig.json` で `exactOptionalPropertyTypes` を有効にすることを推奨します(Prisma と同じです)。 ```json { "compilerOptions": { "exactOptionalPropertyTypes": true } } ``` これにより、オプショナルなフィールドへ `undefined` を明示的に渡すコードがコンパイルエラーになります。 ## 配列内では使えない 配列の要素として `Gassma.skip` を渡すことはできません。`strictUndefinedChecks` の有効・無効に関わらず `GassmaSkipInArrayError` がスローされます。`null` を使うか、事前に配列から取り除いてください。 ```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. ``` 配列の要素としての `undefined` も同様です。`in` / `notIn` / `AND` / `OR` / `NOT` / `orderBy` / `distinct` などの配列に `undefined` の要素が含まれる場合、`strictUndefinedChecks` の有効・無効に関わらず `GassmaUndefinedValueError` がスローされます(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. ``` ## バリデーション | エラー | 原因 | | --- | --- | | `GassmaUndefinedValueError` | `strictUndefinedChecks` 有効時にクエリ入力へ明示的な `undefined` を指定。配列の要素への `undefined` は有効・無効に関わらず発生 | | `GassmaSkipInArrayError` | 配列の要素に `Gassma.skip` を指定(有効・無効に関わらず発生) | | `GassmaInvalidValueError` | `undefined` / `Gassma.skip` の除去によって `update` / `delete` / `upsert` の `where` が空になった | # $extends(query) (https://gassma.io/docs/reference/client-extensions/query) $extends の query コンポーネントは、各操作の実行に割り込むクエリフックを登録するための機能です。Prisma のクライアント拡張($extends の query)に相当します。フックの中で args を書き換える、結果を加工する、実際の操作を実行せずに短絡する、といった制御ができます。 `$extends` の `query` コンポーネントは、各操作の実行に割り込むクエリフックを登録するための機能です。Prisma のクライアント拡張(`$extends` の `query`)に相当します。フックの中で `args` を書き換える、結果を加工する、実際の操作を実行せずに短絡する、といった制御ができます。 算出フィールドを追加したい場合は[$extends(result)](/docs/reference/client-extensions/result)を参照してください。 ## 基本的な使い方 `gassma.$extends({ query: {...} })` を呼ぶと、フックを適用した**新しいクライアント**が返ります。元の `gassma` は変更されません。 ```ts const extended = gassma.$extends({ query: { Users: { findMany({ model, operation, args, query }) { // args を加工してから実際のクエリを実行できる return query(args); }, }, }, }); const users = extended.Users.findMany(); ``` ## フックの形 フックは `{ model, operation, args, query }` を受け取る関数です。 | プロパティ | 内容 | | --- | --- | | `model` | 操作対象のモデル名(シートのコード名) | | `operation` | 操作名(`findMany` など) | | `args` | その操作に渡された引数 | | `query` | 実際の操作(または次のフック)を実行する関数 | `query(args)` を呼ぶと、渡した `args` で実際の操作が実行され、その結果が返ります。`query` を呼ぶ前に `args` を書き換える、`query` の戻り値を加工する、`query` を呼ばずに独自の値を返して短絡する、といった制御が可能です。 ```ts const extended = gassma.$extends({ query: { Users: { findMany({ args, query }) { const result = query(args); // 実際の findMany を実行 return result; }, }, }, }); ``` ## 対象となる操作 `query` フックは次の 15 操作に対して登録できます。 | 分類 | 操作 | | --- | --- | | 取得 | `findFirst` / `findFirstOrThrow` / `findMany` | | 作成 | `create` / `createMany` / `createManyAndReturn` | | 更新 | `update` / `updateMany` / `updateManyAndReturn` | | upsert | `upsert` | | 削除 | `delete` / `deleteMany` | | 集計 | `count` / `aggregate` / `groupBy` | ## 構造 `query` は「モデル名 → 操作名 → フック」の形で指定します。特定のモデル・操作に加えて、モデル内の全操作をまとめて対象にする `$allOperations` と、全モデルを対象にする `$allModels` が使えます。 ```ts const extended = gassma.$extends({ query: { // 特定モデルの特定操作 Users: { findMany({ args, query }) { return query(args); }, // モデル内の全操作 $allOperations({ operation, args, query }) { return query(args); }, }, // 全モデル $allModels: { // 全モデルの特定操作 findMany({ model, args, query }) { return query(args); }, // 全モデルの全操作 $allOperations({ model, operation, args, query }) { return query(args); }, }, }, }); ``` ## フックの合成順 1 つの操作に複数のフックがマッチした場合、それらは上書きされず**すべて連鎖**します。連鎖の順序は次のとおりです。 - **先に適用した拡張ほど外側**になります(後に適用した拡張ほど、実際の操作に近い内側になります)。 - 1 つの拡張の中では、**具体的なフックほど外側**になります。優先順位は `モデル.操作` > `モデル.$allOperations` > `$allModels.操作` > `$allModels.$allOperations` です。 外側のフックが `query(args)` を呼ぶと次に内側のフックが実行され、最も内側のフックが `query(args)` を呼ぶと実際の操作が実行されます。 ```ts const extended = gassma .$extends({ query: { Users: { findMany({ args, query }) { Logger.log("A: 外側"); return query(args); }, }, }, }) .$extends({ query: { Users: { findMany({ args, query }) { Logger.log("B: 内側"); return query(args); }, }, }, }); extended.Users.findMany(); // ログ出力: "A: 外側" → "B: 内側" → 実際の findMany ``` ## 同期的に実行される GAS 上で動作するため、`query` フックは**同期的**に実行されます。`query(args)` は Promise ではなく結果そのものを同期的に返します。`async` / `await` は不要です。 ```ts const extended = gassma.$extends({ query: { Users: { findMany({ args, query }) { const result = query(args); // 同期的に結果が返る return result; }, }, }, }); ``` ## args は参照で渡される `args` はフックに**参照で渡されます**(deep clone されません)。`args` を破壊的に書き換えると、呼び出し元が持つオブジェクトにも影響します。`args` を変更したい場合は、新しいオブジェクトを作って `query` に渡すことを推奨します。 ```ts const extended = gassma.$extends({ query: { Users: { findMany({ args, query }) { // NG: 渡された args を直接書き換える // args.where = { ...args.where, deleted: false }; // OK: 新しいオブジェクトを作って渡す const nextArgs = Object.assign({}, args, { where: Object.assign({}, args.where, { deleted: false }), }); return query(nextArgs); }, }, }, }); ``` ## チェーンできる `$extends` は連続して呼び出せます。それぞれの呼び出しが新しいクライアントを返します。 ```ts const extended = gassma.$extends(extensionA).$extends(extensionB); ``` ## include の内部リレーションはフックを通らない `query` フックが対象にするのは、**最上位で呼び出した操作**だけです。`include` によって内部で解決される関連レコードの取得は、フックを通りません。 ```ts const extended = gassma.$extends({ query: { Posts: { findMany({ args, query }) { // Users.findMany に対する include: { posts: true } では // この Posts.findMany フックは呼ばれない return query(args); }, }, }, }); ``` ## 実用例 ### ソフトデリート `findMany` の `where` に既定の条件を注入して、削除済みレコードを既定で除外できます。 ```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); }, }, }, }); ``` ### 監査ログ `$allModels` の `$allOperations` で、全モデル・全操作の前後にログを記録できます。 ```ts const extended = gassma.$extends({ query: { $allModels: { $allOperations({ model, operation, args, query }) { Logger.log(`${model}.${operation} 開始`); const result = query(args); Logger.log(`${model}.${operation} 完了`); return result; }, }, }, }); ``` # $extends(result) (https://gassma.io/docs/reference/client-extensions/result) $extends の result コンポーネントは、クエリ結果のレコードに算出フィールド(computed fields)を追加するための機能です。Prisma のクライアント拡張($extends の result)に相当します。既存のスカラーフィールドから新しいフィールドを計算し、結果に含めることができます。 `$extends` の `result` コンポーネントは、クエリ結果のレコードに**算出フィールド**(computed fields)を追加するための機能です。Prisma のクライアント拡張(`$extends` の `result`)に相当します。既存のスカラーフィールドから新しいフィールドを計算し、結果に含めることができます。 クエリの実行そのものに割り込みたい場合は[$extends(query)](/docs/reference/client-extensions/query)を参照してください。 ## 基本的な使い方 `gassma.$extends({ result: {...} })` を呼ぶと、算出フィールドを含む**新しい結果型のクライアント**が返ります。元の `gassma` は変更されません。 算出フィールドは `needs`(計算に必要なスカラーフィールド)と `compute`(計算関数)のペアで定義します。 ```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 と compute | キー | 内容 | | --- | --- | | `needs` | 計算に必要な**スカラーフィールド**を `{ フィールド名: true }` で宣言する | | `compute` | `needs` で宣言したフィールドだけを持つレコードを受け取り、算出値を返す | `compute` が受け取るレコードは `needs` で宣言したフィールドだけを含み、その**型も `needs` から付きます**。型注釈は不要です。 ```ts const extended = gassma.$extends({ result: { Users: { greeting: { needs: { name: true }, // user は { name: string } として型が付く compute(user) { return `Hi ${user.name}`; }, }, }, }, }); ``` `needs` に指定できるのは**スカラーフィールドのみ**です。リレーションは指定できません。 ## 通常のプロパティとして付与される 算出フィールドは getter ではなく、**その場で計算された通常のプロパティ**として結果に付与されます。そのため `Logger.log` / `JSON.stringify` / スプレッド構文でも安定して扱えます。 ```ts const user = extended.Users.findFirst({ where: { id: 1 } }); Logger.log(user.greeting); // "Hi Alice" JSON.stringify(user); // greeting を含む const copy = { ...user }; // copy.greeting も残る ``` ## 算出フィールドが付く操作 算出フィールドは、**レコードを返す操作**の結果に付与されます。 | 付与される | 付与されない | | --- | --- | | `findFirst` / `findFirstOrThrow` / `findMany` | `count` / `aggregate` / `groupBy` | | `create` / `createManyAndReturn` | `createMany` | | `update` / `updateManyAndReturn` | `updateMany` | | `upsert` / `delete` | `deleteMany` | 件数だけを返す `createMany` / `updateMany` / `deleteMany` や、集計を行う `count` / `aggregate` / `groupBy` には付与されません。 ## 既存フィールドの上書き 既存フィールドと同じ名前の算出フィールドを定義すると、その値を**上書き**できます。 ```ts const extended = gassma.$extends({ result: { Users: { // 既存の name を大文字に置き換える name: { needs: { name: true }, compute(user) { return user.name.toUpperCase(); }, }, }, }, }); ``` ## 算出フィールドどうしの依存 算出フィールドは、別の算出フィールドに依存できます。依存先を `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 }, // 別の算出フィールドに依存 compute(user) { return `Hi ${user.fullName}`; }, }, }, }, }); ``` **チェーンした `$extends`(別の `$extends` 呼び出し)越しの依存**では、依存先の値の型も完全に付きます。 一方、**同一の `$extends` 呼び出しの中**で算出フィールドどうしを依存させると、実行時は動作しますが、依存先の `compute` に引数注釈が無い場合は依存値の**型が付きません**(`never` になります)。型も必要な場合は、依存先の `compute` に引数注釈を付けるか、チェーンした `$extends` に分けてください。これは TypeScript の制約によるもので、Prisma でも同様です。 ## $allModels で全モデルに追加 `$allModels` を使うと、すべてのモデルに共通の算出フィールドを追加できます。同名の算出フィールドがモデル固有にも定義されている場合は、モデル固有のものが優先されます。 ```ts const extended = gassma.$extends({ result: { $allModels: { fetchedAt: { compute() { return new Date(); }, }, }, }, }); ``` ## select / omit との連携 `select` を指定した場合は、**選択した算出フィールドだけ**が結果に含まれます。`select` を指定しなければ、すべての算出フィールドが含まれます。 ```ts const user = extended.Users.findFirst({ where: { id: 1 }, select: { greeting: true }, // greeting だけが返る }); ``` `omit` で算出フィールドを除外することもできます。 ```ts const user = extended.Users.findFirst({ where: { id: 1 }, omit: { greeting: true }, // greeting を除外 }); ``` `needs` に指定したスカラーフィールドは、`select` で選ばなくても `omit` で除外しても、`compute` のために内部で読み込まれます(compute は問題なく動作します)。 ## ネストした include にも付与される `include` で取得した関連レコードにも、そのモデルの算出フィールドが付与されます。深くネストした場合も、各階層のレコードに付与されます。 ```ts const result = extended.Users.findMany({ include: { posts: true, // 各 post にも Posts の算出フィールドが付く }, }); ``` `include` の詳細は[include](/docs/reference/relation/include)を参照してください。 ## query との併用 `query` と `result` は同時に指定できます。 ```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}`; }, }, }, }, }); ``` ## 制約 算出フィールドは `where` / `orderBy` / 集計(`count` / `aggregate` / `groupBy`)では使用できません。また、`needs` に指定できるのはスカラーフィールドのみです(リレーションは不可)。 ## 実用例 ### fullName `firstName` と `lastName` を結合した `fullName` を追加します(Users シートに `firstName` / `lastName` 列がある場合)。 ```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" ``` ### 派生フィールド 既存のフィールドから派生した値を追加します。 ```ts const extended = gassma.$extends({ result: { Posts: { excerpt: { needs: { content: true }, compute(post) { return post.content.slice(0, 20); }, }, }, }, }); ``` # $transaction(トランザクション) (https://gassma.io/docs/reference/transaction) $transaction でコールバック内の書き込みをまとめてコミットする。エラー時は 1 セルも書き込まれない。maxWait / timeout / rollback オプション `$transaction` は、複数の操作をまとめて 1 つのトランザクションとして実行するための機能です。Prisma のインタラクティブトランザクション(コールバック形の `$transaction`)に相当します。 コールバック内の書き込み操作は即座にはシートへ反映されず、コールバックが**正常終了した時点でまとめてシートに書き込まれます**(コミット)。コールバックが throw した場合、シートには **1 セルも書き込まれません**。 ## 基本的な使い方 `gassma.$transaction((tx) => {...})` の形で呼び出します。コールバックにはトランザクション用クライアント `tx` が渡され、コールバックの戻り値がそのまま `$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 と Posts にまとめて書き込まれる ``` GAS 上で動作するため、Prisma と異なり `$transaction` は**同期的**に実行されます。コールバックも `$transaction` の戻り値も Promise ではありません。`async` / `await` は不要です。 ## throw で全キャンセル コールバックの途中でエラーが throw されると、それまでの書き込みはすべて破棄され、シートには何も反映されません。 ```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) { // Users にも Posts にも 1 行も書き込まれていない } ``` ## tx クライアント `tx` では通常のクライアントと同じモデル群(シート)が使えます。`$extends` も使え、トランザクション内だけに適用される拡張クライアントを作れます。 ```ts gassma.$transaction((tx) => { const extended = tx.$extends({ query: { Users: { findMany({ args, query }) { return query(args); }, }, }, }); return extended.Users.findMany(); }); ``` 一方、`tx` に `$transaction` はありません(型上も存在しません)。トランザクションのネストは非対応で、トランザクション内で `$transaction` を呼ぶと `GassmaNestedTransactionError` が throw されます。 ### トランザクション内の読み取り(read-your-writes) トランザクション内の読み取り(`findMany` / `include` / リレーションフィルタなど)には、**まだコミットされていない変更が見えます**。一方、トランザクションの外のクライアントからは、コミットされるまで変更は見えません。 ```ts gassma.$transaction((tx) => { tx.Users.create({ data: { id: 1, name: "Tanaka" } }); // tx 内の読み取りには未コミットの変更が見える const found = tx.Users.findFirst({ where: { id: 1 } }); // 見つかる // tx の外のクライアントからはコミットまで見えない const outside = gassma.Users.findFirst({ where: { id: 1 } }); // null }); ``` ## オプション 第 2 引数でオプションを指定できます。 ```ts gassma.$transaction( (tx) => { // ... }, { maxWait: 10000, timeout: 120000, rollback: false }, ); ``` | オプション | 既定値 | 説明 | | --- | --- | --- | | `maxWait` | `20000`(ms) | トランザクション開始(ロック取得)を待つ時間の上限。超過すると `GassmaTransactionLockTimeoutError` | | `timeout` | `60000`(ms) | トランザクション全体の実行時間の上限。超過すると `GassmaTransactionTimeoutError` | | `rollback` | `true` | コミット時のバックアップと失敗時の自動復元を有効にするか(詳細は[後述](#rollback)) | ### maxWait トランザクションはスクリプトロックを取得してから開始されます。別の実行が同じロックを保持している場合(別の `$transaction` の実行中など)、最大 `maxWait` ミリ秒までロックの解放を待ち、それでも取得できなければ `GassmaTransactionLockTimeoutError` を throw します。 ### timeout トランザクション開始からの経過時間が `timeout` ミリ秒を超えると `GassmaTransactionTimeoutError` を throw します。チェックは協調式で、**各 tx 操作の呼び出し時**と**コミット直前**に経過時間を確認します。 協調式のため、tx のメソッドを呼ばない処理(長い計算ループなど)の途中では超過を検知できません。その場合も、次の tx 操作またはコミット直前のチェックで検知されます。 ### rollback `rollback: true`(既定)のとき、コミットは次の流れで行われます。 1. 書き込み対象のシートを一時バックアップ(`_gassma_tx_...` という名前の隠しシート)として複製する 2. シートへ書き込む 3. 成功したらバックアップを自動削除する 書き込みの途中で失敗した場合は、**バックアップから自動復元**したうえで元のエラーを再 throw します。シートはその場で復元され、数式セルも保たれます。復元まで失敗した場合は**バックアップシートを残して** `GassmaTransactionRollbackError` を throw します。このエラーの `backupSheetNames` プロパティに残されたバックアップシート名の一覧が入っており、そこから手動で復旧できます。 `{ rollback: false }` を指定すると、この仕組みを丸ごと省略します(高速)。ただし、書き込みの途中で失敗した場合はシートが部分的に書き込まれた状態になり得ます。 `rollback` 有効時は、バックアップ複製(`copyTo`)のコストが**書き込み対象シートのサイズに比例**して掛かり、GAS の 6 分実行制限も消費します。大きいシートへのトランザクションや高頻度のトランザクションでは `rollback: false` を検討してください。 ## 制限事項 - ロックが直列化するのは、GASsma を経由する処理同士だけです。スプレッドシートの手動編集や、GASsma を使わない別スクリプトからの変更は止められません。同時実行で何が起こりうるかは[書き込みの原子性と同時実行](/docs/reference/write-atomicity)を参照してください。 - **ロックは GASsma ライブラリのものです。** GASsma はライブラリとして動くため、`$transaction` が取るロックは**あなたのスクリプトプロジェクトのものではなく、GASsma 自身のもの**です。同じライブラリを使う**他のスクリプトプロジェクトとも共有**されるため、別のプロジェクトのトランザクションが実行中であれば、そちらの完了を待つことがあります。 - 実行が強制終了された場合(GAS の 6 分実行制限など)は、バックアップシートが残ることがあります。次回の `$transaction` 実行時に警告ログが出力されます。残った `_gassma_tx_...` シートは、中身を確認のうえ手動で削除して構いません。 - 復元されるのはセルの値と数式のみです(書式などは対象外)。 - [changeSettings](/docs/reference/settings/changeSettings) で実行時に変更した設定は、トランザクション内に引き継がれません。 - 操作の配列を渡す形(Prisma の sequential operations)は非対応です。コールバック形のみ使えます。 - `isolationLevel` は非対応です(常にロックによる直列実行)。 ## 関連エラー `GassmaTransactionLockTimeoutError` / `GassmaTransactionTimeoutError` / `GassmaNestedTransactionError` / `GassmaTransactionRollbackError` の詳細は[エラー一覧](/docs/reference/errors)を参照してください。 # Prisma スキーマを利用したローカル開発 (https://gassma.io/docs/reference/type-generation) gassma CLI(generate/init/validate/format)と gassma.config.ts を使い、Prisma 形式のスキーマから型安全なクライアントを生成する GASsma は clasp+esbuild 等を利用し TypeScript を使ってローカルで GAS を開発する際に、Prisma 形式のスキーマファイルから型安全なクライアントコードを自動生成する機能を提供しています。 この機能を利用すると、リレーション定義や defaults・map 等の設定がスキーマから自動生成されるため、GASsma 独自のコンストラクタオプション(`relations`、`defaults`、`updatedAt`、`ignore`、`map` など)を手動で記述する必要がなくなります。Prisma のスキーマ構文さえ知っていれば、Prisma と同じ感覚で開発を始められます。 **CLI なし(手動設定):** ```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: "名前" } }, }); ``` **CLI あり(スキーマから自動生成):** ```ts import { GassmaClient } from "./generated/gassma/schemaClient"; // リレーション・defaults・updatedAt・map 等すべて自動注入済み const gassma = new GassmaClient(); ``` ## 前提 以下のコマンドで GASsma の CLI ツールをインストールしてください。 ``` $ npm i gassma ``` ## スキーマファイルの作成 プロジェクト内に `.prisma` ファイルを作成します。デフォルトでは `./gassma` ディレクトリ配下が探索されます。 ``` my-project/ ├── gassma/ │ └── schema.prisma ← ここにスキーマを記述 ├── package.json └── ... ``` ### 基本的な書き方 Prisma の文法でモデルを定義します。`generator` ブロックの `output` で出力先を指定してください。 ```prisma generator client { provider = "prisma-client-js" output = "./generated/gassma" } model User { id Int @id name String email String? age Int } ``` ### previewFeatures `generator` ブロックに `previewFeatures` を指定すると、オプトインの機能を有効化できます(Prisma の `previewFeatures` と同じ書き方です)。 ```prisma generator client { provider = "prisma-client-js" output = "./generated/gassma" previewFeatures = ["strictUndefinedChecks"] } ``` 現在サポートされている機能: | 機能 | 説明 | 参照 | | --- | --- | --- | | `strictUndefinedChecks` | クエリ入力の明示的な `undefined` を実行時エラーにする | [strictUndefinedChecks / Gassma.skip](/docs/reference/config/strict-undefined-checks) | 有効化すると、生成されるクライアント JS に `strictUndefinedChecks: true` が埋め込まれ、生成される型定義にも `Gassma.skip` を受け付ける型が反映されます。 ### 型マッピング Prisma の型は以下の TypeScript 型に変換されます。 | Prisma 型 | TypeScript 型 | | --- | --- | | `Int` | `number` | | `Float` | `number` | | `Decimal` | `number` | | `BigInt` | `number` | | `String` | `string` | | `Boolean` | `boolean` | | `DateTime` | `Date` | | `Json` | `string` | | `Bytes` | `string` | `?` を付けるとオプショナルフィールドになります(`null` が許容されます)。 ### リレーション定義 Prisma の `@relation` 属性を使うと、リレーション情報が自動的に抽出され、生成されたクライアントに注入されます。 ```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 } ``` 上記の定義から以下のリレーション設定が自動生成されます。 - `User.posts`: oneToMany(User → Post) - `Post.author`: manyToOne(Post → User、onDelete: Cascade) #### 暗黙的 Many-to-Many 双方向の配列参照がある場合、暗黙的な Many-to-Many リレーションが自動検出されます。 ```prisma model Post { id Int @id tags Tag[] } model Tag { id Int @id name String posts Post[] } ``` 生成されるクライアントでは、中間テーブル名が `_PostToTag`(モデル名のアルファベット順)として自動的に解決されます。`@relation("PostTags")` のようにリレーションに名前を付けた場合は、リレーション名がそのまま中間テーブル名(`_PostTags`)になります(Prisma と同じ規則です)。 中間テーブル(シート)自体は [migrate / db push](/docs/reference/migrate) で自動作成できます(スプレッドシート側に同名のシートを手動で用意しても構いません)。 中間テーブル名を変更したい場合は、`@relation` でリレーションに名前を付けてください。 ### enum Prisma の `enum` 定義からリテラルユニオン型が自動生成されます。 ```prisma enum Role { ADMIN USER MODERATOR } model User { id Int @id role Role } ``` 生成される型: ```ts "role": "ADMIN" | "USER" | "MODERATOR" ``` #### enum の @map enum メンバーに `@map` を付けると、コード上の名前とスプレッドシート上の値をマッピングできます。 ```prisma enum Role { admin @map("ADMIN") user @map("USER") moderator @map("MODERATOR") } ``` 生成される定数: ```ts const Role = { admin: "ADMIN", user: "USER", moderator: "MODERATOR", } as const; ``` 型定義には `@map` の値が使用されます: ```ts "role": "ADMIN" | "USER" | "MODERATOR" ``` ### @gassma.addType Prisma のフィールドコメント(`///`)に `@gassma.addType` を記述すると、フィールドの型にユニオン型を追加できます。 ```prisma model User { /// @gassma.addType string id Int @id // 生成型: number | string /// @gassma.addType string, boolean score Int // 生成型: number | string | boolean name String // 生成型: string(コメントなしなら通常通り) } ``` ### @gassma.replaceType `@gassma.addType` は基底型とのユニオンですが、`@gassma.replaceType` は基底型を置換して指定した型のみ生成します。 ```prisma model User { /// @gassma.replaceType "admin", "user", "moderator" role String } ``` 生成される型: ```ts "role": "admin" | "user" | "moderator" // string を含まない ``` 優先順位: enum > replaceType > addType。enum がある場合は replaceType / addType は無視されます。 ### @default `@default()` が付いたフィールドは、生成される Create 入力型でオプショナル(`?`)になります。 ```prisma model User { id Int @id @default(autoincrement()) name String isActive Boolean @default(true) createdAt DateTime @default(now()) } ``` 生成される型: ```ts "isActive"?: boolean // @default(true) → オプショナル "createdAt"?: Date // @default(now()) → オプショナル ``` 生成されるクライアント JS には defaults 設定が自動的に埋め込まれます。 | `@default()` | 生成される JS | | --- | --- | | `@default(true)` / `@default(false)` | `true` / `false` | | `@default(0)` (数値) | `0` | | `@default("USER")` (文字列) | `"USER"` | | `@default(ADMIN)` (enum 値) | `"ADMIN"` | | `@default(active)` (enum 値・`active @map("ACTIVE")`) | `"ACTIVE"`(`@map` 後の値) | | `@default(now())` | `() => new Date()` | | `@default(uuid())` | `() => Utilities.getUuid()` | | `@default(autoincrement())` | autoincrement 設定として別途生成 | ### @updatedAt `@updatedAt` が付いたフィールドは Create 入力型でオプショナルになり、生成されるクライアント JS に updatedAt 設定が埋め込まれます。 ```prisma model Post { id Int @id title String updatedAt DateTime @updatedAt } ``` ### @ignore `@ignore` が付いたフィールドは型定義から完全に除外され、生成されるクライアント JS に ignore 設定が埋め込まれます。 ```prisma model User { id Int @id name String secret String @ignore // 型定義に含まれない } ``` ### @map `@map("name")` でフィールド名のマッピングを定義できます。生成されるクライアント JS に map 設定が埋め込まれます。 ```prisma model User { id Int @id firstName String @map("名前") lastName String @map("名字") } ``` コード上は `firstName` / `lastName` で操作し、スプレッドシート上は「名前」「名字」カラムに対応します。 ### @@ignore モデルレベルの `@@ignore` でシート全体を除外できます。生成されるクライアント JS に ignoreSheets 設定が埋め込まれます。 ```prisma model Logs { id Int @id message String @@ignore } ``` ### @@map モデルレベルの `@@map("name")` でシート名をマッピングできます。 ```prisma model Users { id Int @id name String @@map("ユーザー一覧") } ``` コード上は `Users` でアクセスし、スプレッドシート上は「ユーザー一覧」シートに対応します。 ## CLI コマンド ### gassma generate 型ファイルとクライアントコードを生成します。 ``` $ npx gassma generate ``` デフォルトでは `./gassma` ディレクトリ内の `.prisma` ファイルが探索されます。`--schema` オプションで特定のスキーマファイルまたはディレクトリを指定できます(Prisma の `prisma generate --schema` に相当)。 ``` $ npx gassma generate --schema gassma/user.prisma $ npx gassma generate --schema ./schemas ``` `--watch` オプションでスキーマファイルの変更を監視し、自動で再生成できます。 ``` $ npx gassma generate --watch ``` `--schema` との併用も可能です。 `--config` オプションで設定ファイルのパスを明示的に指定できます(Prisma の `--config` に相当)。 ``` $ npx gassma generate --config configs/gassma.config.ts ``` 指定したファイルが存在しない場合は `ConfigFileNotFoundError` になります。未指定時は従来どおりデフォルトの場所が探索されます(後述の「設定ファイルの探索規則」を参照)。 ### gassma init プロジェクトを初期化し、スキーマファイルと設定ファイルを自動生成します。 ``` $ npx gassma init ``` 以下のファイルが生成されます: - `gassma/schema.prisma` — 初期スキーマ - `gassma.config.ts` — 設定ファイル | オプション | 説明 | | --- | --- | | `--output ` | 生成先パスをカスタマイズ | | `--with-model` | サンプル User モデルを含むスキーマを生成 | 既に `schema.prisma` が存在する場合はエラーで安全に停止します。 ### gassma validate スキーマファイルの構文チェック・整合性チェックを行います(Prisma の `prisma validate` に相当)。 ``` $ npx gassma validate ``` ``` $ npx gassma validate --schema gassma/test.prisma ``` `--config` オプションで設定ファイルのパスを指定することもできます。 チェック項目: - 構文エラー(パーサーエラー検出) - `generator` ブロックの存在チェック - `output` フィールドの必須チェック - モデルが 1 つ以上定義されていること 成功時は以下のように出力されます: ``` The schema at /path/to/gassma/test.prisma is valid 🚀 ``` ### gassma format `.prisma` ファイルを Prisma 公式と同じフォーマットで整形します(`@prisma/internals` の `formatSchema` を使用)。 ``` $ npx gassma format ``` | オプション | 説明 | | --- | --- | | `--schema ` | 特定ファイルのみ整形 | | `--config ` | 設定ファイルのパスを指定 | | `--check` | フォーマット済みかチェック(CI 用、未整形時は exit 1) | ### gassma studio `datasource` に設定したスプレッドシートを、OS のデフォルトブラウザで開きます。 ``` $ npx gassma studio ``` | オプション | 説明 | | --- | --- | | `--config ` | 設定ファイルのパスを指定 | URL は以下の順で解決されます。 1. スキーマ内の `datasource` ブロックの `url` 2. `gassma.config.ts` の `datasource.url` `url` にフル URL(`https://...`)を指定している場合はそのまま開き、スプレッドシート ID を指定している場合は `https://docs.google.com/spreadsheets/d//edit` を組み立てて開きます。どちらにも URL が設定されていない場合は `NoDatasourceUrlError` になります。 ### gassma version GASsma CLI のバージョンを表示します。 ``` $ npx gassma version ``` `--version` / `-V` フラグでも確認できます。 | オプション | 説明 | | --- | --- | | `--json` | バージョン情報を JSON で出力 | `--json` を付けると、バージョン情報を JSON 形式(`{"gassma":""}`)で出力します。 ``` $ npx gassma version --json {"gassma":"1.2.3"} ``` ### 生成されるファイル スキーマファイル名をもとに以下のファイルが生成されます。例えば `schema.prisma` の場合: | ファイル | 内容 | | --- | --- | | `schema.d.ts` | 型定義(モデル型、クエリ型、共通型) | | `schemaClient.js` | クライアント実装(リレーション定義の自動注入込み) | | `schemaClient.d.ts` | クライアントの型定義 | 出力先は `generator` ブロックの `output` で指定したディレクトリです。 ## 生成されたクライアントの使い方 生成されたクライアントファイルから `GassmaClient` をインポートしてそのまま使えます。リレーション定義は自動注入済みです。 ```ts import { GassmaClient } from "./generated/gassma/schemaClient"; const gassma = new GassmaClient(); // 型安全にシートへアクセス const users = gassma.User.findMany({ where: { age: { gte: 20 } }, select: { name: true, email: true }, }); ``` Prisma と同じパターンでインスタンス化できます。 ```ts // Prisma import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); // GASsma(同じパターン) import { GassmaClient } from "./generated/gassma/schemaClient"; const gassma = new GassmaClient(); ``` ### オプション付きの初期化 ```ts // スプレッドシートID指定 const gassma = new GassmaClient("SPREAD_SHEET_ID"); // オプションオブジェクト const gassma = new GassmaClient({ id: "SPREAD_SHEET_ID", omit: { User: { password: true }, }, }); ``` ## 設定ファイル(gassma.config.ts) プロジェクトルートに `gassma.config.ts` を配置することで、CLI の設定を一元管理できます(Prisma の `prisma.config.ts` に相当)。TypeScript 以外の拡張子(`.js` / `.mjs` / `.cjs` / `.mts` / `.cts`)や `.config/` ディレクトリへの配置にも対応しています(後述の「設定ファイルの探索規則」を参照)。 ### 設定インターフェース 設定ファイルの記述方法は 2 つあります。 **1. `defineConfig` ヘルパーを使用(推奨):** ```ts import { defineConfig } from "gassma/config"; export default defineConfig({ schema: "gassma/schema.prisma", datasource: { url: "https://docs.google.com/spreadsheets/d/XXXXX/edit", }, }); ``` **2. `satisfies` 演算子を使用:** ```ts import type { GassmaConfig } from "gassma"; export default { schema: "gassma/schema.prisma", datasource: { url: "https://docs.google.com/spreadsheets/d/XXXXX/edit", }, } satisfies GassmaConfig; ``` `GassmaConfig` 型は `gassma` パッケージのルートから import できます。 ### 設定オプション | オプション | 型 | 必須 | 説明 | | --- | --- | --- | --- | | `schema` | `string` | いいえ | スキーマファイルまたはディレクトリのパス(デフォルト: `./gassma`) | | `datasource.url` | `string` | いいえ | スプレッドシートの URL または ID | ### 設定ファイルの探索規則 設定ファイルは以下の順序で探索され、**最初に見つかったファイル** が採用されます。 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` プロジェクトルート直下の `gassma.config.*` が全拡張子ぶん先に探索され、その後 `.config/` ディレクトリ内の `gassma.*` が探索されます。`.js` が `.ts` より先に採用される点も含め、Prisma の設定ファイル探索と同じ順序です。 ### --config オプション `generate`(`--watch` 含む)/ `validate` / `format` / `studio` の各コマンドでは、`--config` オプションで設定ファイルのパスを明示的に指定できます(Prisma の `--config` に相当)。 ``` $ npx gassma generate --config configs/gassma.config.ts ``` - 相対パスは実行時のカレントディレクトリ基準で解決されます。 - 指定したファイルが存在しない場合は `ConfigFileNotFoundError` になります。 - 未指定時は上記の探索規則に従ってデフォルトの場所が探索されます。 ### ロード時の挙動 `gassma generate` 実行時、設定ファイルのロードに成功すると以下のように表示されます。 ``` ⚙️ Loaded config from gassma.config.ts ``` - 設定ファイルに構文エラー・実行時エラーがある場合や、既知のキー(`schema` / `datasource.url`)の型が不正な場合は `GassmaConfigLoadError` になります。 - 未知のキーが含まれている場合は警告が表示され、そのキーは無視されます(エラーにはなりません)。 ``` Warning: Unknown property `outut` in /path/to/gassma.config.ts. Known properties are: schema, datasource. It will be ignored. ``` ### env() ヘルパー `env()` 関数を使うと、環境変数からスプレッドシート URL を取得できます(Prisma の `env()` に相当)。 ```ts import "dotenv/config"; import { defineConfig, env } from "gassma/config"; export default defineConfig({ schema: "gassma", datasource: { url: env("SPREADSHEET_URL"), }, }); ``` `satisfies` パターンでも使用できます。 ```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; ``` #### 型付きの env() 型引数に環境変数のインターフェースを渡すと、`env()` に指定できる名前がそのキーに限定され、補完も効くようになります。 ```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"), }, }); ``` 指定できるのは値が `string`(または `string | undefined`)型のキーのみです。存在しないキーを指定するとコンパイルエラーになります。 `env()` は環境変数が未設定または空文字の場合に `GassmaConfigEnvError` をスローします。オプショナルな環境変数には `process.env` を直接使用してください。 ### datasource.url `datasource.url` にスプレッドシートの URL または ID を指定すると、生成されるクライアント JS に `id` が自動埋め込みされます。これにより `new GassmaClient()` だけで対象スプレッドシートに接続できます。 フル URL とスプレッドシート ID の両方に対応しています。 ```ts // フル URL datasource: { url: "https://docs.google.com/spreadsheets/d/XXXXX/edit", } // ID 直接指定 datasource: { url: "XXXXX", } ``` ### スキーマ内の datasource ブロック スキーマファイル内に `datasource` ブロックを記述することでも URL を指定できます。 ```prisma datasource db { provider = "google-spreadsheet" url = "https://docs.google.com/spreadsheets/d/XXXXX/edit" } ``` #### URL 解決の優先順位 1. スキーマ内の `datasource` ブロック(最優先) 2. `gassma.config.ts` の `datasource.url` ### スキーマ解決の優先順位 1. `--schema` オプション(最優先) 2. `gassma.config.ts` の `schema` 設定 3. デフォルト `./gassma` ディレクトリ 相対パスの解決基準はそれぞれ異なります。`--schema` オプションは実行時のカレントディレクトリ基準、設定ファイルの `schema` は **設定ファイルのある場所基準** で解決されます(Prisma と同じ)。 `gassma init` を実行すると `gassma.config.ts` も自動生成されます。 ## マルチファイルスキーマ 同じディレクトリ(およびサブディレクトリ)内に複数の `.prisma` ファイルを配置すると、自動的に **1 つのスキーマとして統合** されます。Prisma の [Multi-file schema](https://www.prisma.io/docs/orm/prisma-schema/overview/location#multi-file-prisma-schema) と同等の機能です。 ``` gassma/ ├── schema.prisma ← generator ブロックをここに記述 ├── models/ │ ├── user.prisma ← User, Profile モデル │ └── post.prisma ← Post, Comment モデル ``` `generator` ブロックはいずれか 1 ファイルに記述すれば、全ファイルで共有されます。すべてのモデルが 1 つのクライアント出力にまとめられます。 ## 複数スキーマ(複数スプレッドシート) 異なるスプレッドシートを扱う場合は、スキーマを別々のディレクトリに分けて個別に生成します。型名にはスキーマ名のプレフィックスが付与されるため、同名モデルがあっても衝突しません。 ``` 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(); ``` ## 生成される型の概要 生成される `.d.ts` には以下の型が含まれます。 - **モデル型**: 各フィールドの型定義(`GassmaUserUse` 等) - **クエリ型**: `FindData`、`CreateData`、`UpdateData`、`DeleteData`、`UpsertData` 等 - **Select / Omit 型**: フィールド選択・除外の型 - **フィルタ型**: `WhereUse`、`FilterConditions`(`FieldRef` 対応含む) - **OrderBy 型**: ソート条件(リレーションソート、`_count` ソート、nulls 制御含む) - **Include 型**: リレーション取得の型(`_count` 含む) - **Nested Write 型**: リレーション先の作成・接続・更新・削除操作 - **数値操作型**: `NumberOperation`(increment / decrement / multiply / divide) - **共通型**: `FieldRef`、`GassmaClientOptions`、エラークラス群 - **設定型**: `DefaultsConfig`、`UpdatedAtConfig`、`IgnoreConfig`、`AutoincrementConfig`、`MapConfig` 等 - **コントローラー型**: 全メソッドの引数・戻り値型 # 書き込みの原子性と同時実行 (https://gassma.io/docs/reference/write-atomicity) Nested write やカスケードは複数シートへの書き込みをバッファし、エラー時は 1 行も書かれない。この保証が及ばないケース(API 失敗・同時編集)と $transaction による対策 [Nested write](/docs/reference/relation/nested-write)(`create` の中の `posts: { create: [...] }` など)や、[onDelete](/docs/reference/relation/on-delete) / [onUpdate](/docs/reference/relation/on-update) の `Cascade` は、1 回の操作で**複数のシートに書き込みます**。このページでは、こうした操作で何が保証され、何が保証されないかを説明します。 ## エラー時には何も書かれない 複数シートに書く操作は、内部で書き込みをバッファし、**操作全体が成功したときにまとめてシートに書き込みます**。途中でエラーになった場合、シートには **1 行も書かれません**。 ```ts // 子の作成でエラーになる場合 gassma.Users.create({ data: { id: 4, name: "Dave", posts: { create: [{ id: 4, titel: "..." }], // 列名の誤りでエラー }, }, }); // → エラー。Users にも Posts にも何も書かれない ``` これは、Prisma が nested write を暗黙のトランザクションで包むのと同じ保証です。 ## この保証が及ばないケース ### 書き込み中にスプレッドシートの API が失敗した場合 バッファをシートに書き出している最中に Google スプレッドシートの API が失敗すると、**そこまでの書き込みはシートに残ります**。 [$transaction](/docs/reference/transaction) を `rollback: true`(既定)で使うと、書き込み前のバックアップから自動復元されます(詳細は [rollback](/docs/reference/transaction#rollback) を参照)。 ### 操作中に他のプロセスや人がシートを書き換えた場合 GASsma は、更新・削除する行を**位置(行番号)で特定します**。対象の行を読んでから書き込むまでの間に、他の誰かが**行を挿入・削除**すると、**意図した行とは別の行に書き込む**可能性があります。 ``` GASsma が読んだ時点: 1行目 Alice / 2行目 Bob / 3行目 Carol → 「3行目の Carol を更新しよう」 他の誰かが 1行目を削除: 1行目 Bob / 2行目 Carol GASsma が書き込む: 3行目に書く → そこには誰もいない、あるいは別の行 ``` 複数シートに書く操作は、バッファするぶん**読んでから書くまでの間隔が長くなります**。同時に書き込みが起こる環境では、この点を意識してください。 ## 同時に書き込みが起こりうるなら $transaction で包む 同時に書き込みが起こりうるシステムでは、書き込みを [$transaction](/docs/reference/transaction) で包んでください。 ```ts gassma.$transaction((tx) => { tx.Users.create({ data: { id: 4, name: "Dave", posts: { create: [{ id: 4, title: "Dave の記事", published: true }], }, }, }); }); ``` `$transaction` はロックを取得するため、**GASsma を経由する書き込みどうしは直列化されます**。上の「読んでから書くまで」の割り込みが GASsma の書き込みによって起こることはなくなります。 このロックが直列化するのは **GASsma を使う処理どうしだけ**です。以下には効きません。 - **人がスプレッドシートを手で編集した場合** - **GASsma を使っていない別のスクリプトが書き込んだ場合** これらに対しては GASsma からは何もできません。操作中にシートが手編集される可能性がある場合は、**そもそも同時に触らない運用にする**、**編集を受け付けない時間帯に処理する**といった、設計側の対処が必要です。 # bootstrap(ローカル開発環境のセットアップ) (https://gassma.io/docs/reference/bootstrap) npx gassma bootstrap で clasp + esbuild + TypeScript + GASsma のローカル開発環境を一発でセットアップする `npx gassma bootstrap` は、GAS のローカル開発環境(clasp + esbuild + TypeScript + GASsma ライブラリ)をコマンド一発で新規セットアップするコマンドです。 ``` $ npx gassma bootstrap my-app # my-app/ を作成してその中に構築 $ npx gassma bootstrap # 最初に構築先ディレクトリを質問(デフォルト: gassma-project) $ npx gassma bootstrap . # カレントディレクトリに構築 ``` `npx` で実行できるため、事前のインストールは不要です。実行すると対話形式で質問が進み、構築先ディレクトリの作成・Apps Script プロジェクトの作成からビルド設定・スキーマファイルの生成・依存パッケージのインストールまでが完了します。 構築先ディレクトリはコマンドが自動で作成するため、事前にディレクトリを用意する必要はありません。`.` を指定すれば既存のカレントディレクトリにも構築できます。既存ファイルがある場合は上書きせずスキップまたはマージされます(後述の「ディレクトリの扱い」「冪等性」を参照)。 ## 前提 [clasp](https://github.com/google/clasp) がインストール済みで、ログインが済んでいる必要があります。 ``` $ npm install -g @google/clasp $ clasp login ``` また、[Apps Script API の設定ページ](https://script.google.com/home/usersettings) で Apps Script API を有効にしておいてください(clasp がプロジェクトを作成するために必要です)。 clasp が見つからない場合は、以下のメッセージを表示して安全に終了します(ファイルは一切変更されません)。 ``` clasp is required but was not found in your PATH. Install it with: npm install -g @google/clasp Then log in with: clasp login ``` ## 対話フロー 以下の順で質問されます(プロンプトは実際の文言です)。 ### 1. Project directory? プロジェクトを構築するディレクトリです。デフォルトは `gassma-project` で、`.` を入力するとカレントディレクトリに構築します。存在しないディレクトリは作成され、存在して空でない場合は続行確認が出ます(後述の「ディレクトリの扱い」を参照)。 コマンド引数でディレクトリを指定した場合(`npx gassma bootstrap my-app` や `npx gassma bootstrap .`)、この質問はスキップされます。 ### 2. Project title? 作成する Apps Script プロジェクトのタイトルです。デフォルトは構築先ディレクトリ名(引数または質問 1 で指定したディレクトリの名前)です。 ### 3. Create a new spreadsheet as well? **Yes**(デフォルト)にすると、新しいスプレッドシートを作成し、それに紐づくコンテナバインド型のスクリプトが作られます(`clasp create-script --type sheets`)。**No** の場合はスタンドアロン型(`--type standalone`)になります。 このあと `clasp create-script` が実行され、`.clasp.json` が生成されます(`rootDir` は `./dist`)。続けて `dist/appsscript.json` に GASsma ライブラリ依存などが自動設定されます(後述の「生成されるもの」を参照)。 既に `.clasp.json` が存在する場合、この質問と `clasp create-script` はスキップされます(`Found an existing .clasp.json. Skipping clasp create-script.`)。 ### 4. Function exposure style? GAS に関数を露出させるスタイルを選びます。選択肢の前に、それぞれのサンプルコードが表示されます。 **export(推奨)** — `@gassma/gas-esbuild-plugin` を使い、`export` した関数がそのまま GAS のグローバル関数になります。 ```ts export const main = () => console.log("Hello GAS!"); ``` **global**(esbuild-gas-plugin style) — `esbuild-gas-plugin` を使い、`global` オブジェクトへの代入で関数を露出します。 ```ts const main = () => console.log("Hello GAS!"); interface Global { main: typeof main; } declare const global: Global; global.main = main; ``` 選んだスタイルに応じて、生成される `esbuild.mjs` のプラグインと `package.json` の devDependencies が切り替わります。 ### 5. Linter and formatter setup? リンタとフォーマッタの構成を選びます。 | 選択肢 | 説明 | | --- | --- | | `oxlint + oxfmt` | recommended(デフォルト) | | `eslint + prettier` | ESLint(typescript-eslint)+ Prettier | | `none` | リンタもフォーマッタも導入しない | `--yes` 指定時はデフォルトの `oxlint + oxfmt` が選ばれます。選択に応じて `package.json` の devDependencies と `lint` / `lint:fix` / `format` / `format:check` スクリプト、生成される設定ファイルが変わります(後述の「リンタ / フォーマッタの選択」を参照)。 ### 6. Generate a sample src/index.ts? **Yes**(デフォルト)にすると、選んだスタイルのサンプルコードが `src/index.ts` として生成されます。 ### 7. Install dependencies now? **Yes**(デフォルト)にすると、検出されたパッケージマネージャで依存パッケージをインストールします(例: `Install dependencies now? (npm install)`)。`--skip-install` 指定時はこの質問自体が出ません。 ## 生成されるもの | ファイル | 内容 | | --- | --- | | `.clasp.json` | `clasp create-script` が生成(`rootDir: ./dist`) | | `dist/appsscript.json` | GASsma ライブラリ依存・`timeZone`・`exceptionLogging: STACKDRIVER`・`runtimeVersion: V8` を自動設定 | | `package.json` | `build` / `push` / `open` / `deploy` スクリプトと依存パッケージ(質問 5 の選択に応じて lint / format 系スクリプトも) | | `esbuild.mjs` | 選んだスタイルに応じたビルド設定 | | `tsconfig.json` | GAS 向けの TypeScript 設定(`@types/google-apps-script`) | | `.gitignore` | `.clasp.json` / `.clasprc.json` / `.env` / `node_modules/` / `dist/*`(`dist/appsscript.json` を除く) | | `.oxlintrc.json` | oxlint の設定(質問 5 で `oxlint + oxfmt` を選んだ場合のみ) | | `eslint.config.mjs` / `.prettierrc` | ESLint / Prettier の設定(質問 5 で `eslint + prettier` を選んだ場合のみ) | | `src/index.ts` | サンプルコード(質問 6 で Yes の場合のみ) | | `AGENTS.md` | コーディングエージェント向けのプロジェクト案内(コマンド・開発フロー・制約と GASsma リファレンスへの導線) | | `gassma/schema.prisma` / `gassma.config.ts` | `gassma init` 相当(サンプル User モデル入りのスキーマと設定ファイル) | ### dist/appsscript.json `timeZone` は実行環境から自動判定されます(判定できない場合は `America/New_York`)。GASsma ライブラリは以下のエントリとして追加されます。 ```json { "userSymbol": "Gassma", "libraryId": "1ZVuWMUYs4hVKDCcP3nVw74AY48VqLm50wRceKIQLFKL0wf4Hyou-FIBH", "version": "<実行時に解決された最新バージョン>", "developmentMode": false } ``` 既存のマニフェストに `exceptionLogging` や `runtimeVersion` が設定されている場合はその値が保持されます。 ### package.json のスクリプト | スクリプト | 内容 | | --- | --- | | `build` | `node esbuild.mjs`(`src/index.ts` を `dist/index.js` にバンドル) | | `push` | `clasp push` | | `open` | `clasp open-script` | | `deploy` | `npm run build && npm run push` | 質問 5 で `oxlint + oxfmt` または `eslint + prettier` を選んだ場合は、これに加えて `lint` / `lint:fix` / `format` / `format:check` が追加されます(内容は次節を参照)。 なお、新規生成される `package.json` の `devDependencies` はアルファベット順にソートされて書き出されます(フォーマッタのチェックがソート済みを前提とするため)。 ### リンタ / フォーマッタの選択 質問 5 の選択によって、追加される devDependencies・npm スクリプト・設定ファイルが変わります。 #### oxlint + oxfmt(推奨) devDependencies に `oxlint@^1.76.0` と `oxfmt@^0.61.0` が追加されます。 | スクリプト | 内容 | | --- | --- | | `lint` | `oxlint` | | `lint:fix` | `oxlint --fix` | | `format` | `oxfmt` | | `format:check` | `oxfmt --check` | `.oxlintrc.json` が生成されます。 ```json { "plugins": ["typescript"], "categories": { "correctness": "error" }, "ignorePatterns": ["dist/**", "src/generated/**"] } ``` #### eslint + prettier devDependencies に `eslint@^10.8.0` / `eslint-config-prettier@^10.1.8` / `prettier@^3.9.6` / `typescript-eslint@^8.65.0` が追加されます。 | スクリプト | 内容 | | --- | --- | | `lint` | `eslint .` | | `lint:fix` | `eslint . --fix` | | `format` | `prettier --write .` | | `format:check` | `prettier --check .` | `eslint.config.mjs` と `.prettierrc` が生成されます。 ```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 devDependencies・スクリプト・設定ファイルのいずれも追加されません。 bootstrap が用意するのは上記までです。次のものは導入しません(必要な場合はプロジェクト側で追加してください)。 - pre-commit フック(husky / lint-staged など) - oxlint の type-aware lint(`oxlint-tsgolint`) - oxfmt の設定ファイル(デフォルト設定のまま使います) ### .gitignore について `.clasp.json` と `.clasprc.json` は clasp 公式の CI ガイドに従って gitignore されます(認証情報・スクリプト ID を含むため)。チームでプロジェクトを共有する場合は、チームのシークレットストアから復元してください。セットアップ完了時にも以下の案内が表示されます。 ``` Note: .clasp.json is gitignored. Restore it from your team's secret store when sharing this project. ``` ## 引数とオプション | 引数 | 説明 | | --- | --- | | `[directory]` | プロジェクトを構築するディレクトリ(`.` でカレントディレクトリ)。省略時は対話で質問されます | | オプション | 説明 | | --- | --- | | `--yes` | すべての質問にデフォルト値で回答(非対話モード)。引数なしの場合は `./gassma-project` を作成して構築し、非空ディレクトリの続行確認も自動で続行します | | `--skip-install` | 依存パッケージのインストールをスキップ | | `--dry-run` | ファイルの書き込み・ディレクトリの作成・コマンド実行を行わず、実行予定の内容のみ表示(ディレクトリ作成も `create directory my-app` のように plan として表示されます) | 対話できないターミナル(CI など)では `--yes` が必須です。指定がない場合は `An interactive terminal is required. Run with --yes for non-interactive mode.` と表示して終了します。 ## 挙動の詳細 ### ディレクトリの扱い - 指定したディレクトリが存在しない場合は作成し、その中に構築します。 - 存在して空でない場合は `Directory "my-app" is not empty. Continue?`(デフォルト **No**)と確認されます。No を選ぶと何も変更せず `Bootstrap cancelled.` と表示して安全に終了します。`--yes` 指定時は自動的に続行します。 - このため、途中で中断したセットアップは同じコマンドをもう一度実行し、続行確認に Yes と答えるだけで再開できます(生成済みのファイルは後述の「冪等性」によりスキップ/マージされます)。 - 同名の**ディレクトリでないファイル**が既に存在する場合は `"my-app" already exists and is not a directory.` というエラーで終了します。 ### 冪等性 再実行しても安全なように設計されています。 - `.clasp.json` が存在する場合、`clasp create-script` はスキップされます。 - `esbuild.mjs` / `tsconfig.json` / `src/index.ts` / `gassma/schema.prisma` / `AGENTS.md` は、既に存在する場合スキップされます。 - リンタ / フォーマッタの設定ファイル(`.oxlintrc.json` / `eslint.config.mjs` / `.prettierrc`、質問 5 で選んだ場合に生成)も、既に存在する場合スキップされます。 - `package.json` が既に存在する場合は、bootstrap の設定が**マージ**されます(既存の値が優先されます)。 - `.gitignore` が既に存在する場合は、不足しているエントリのみ追記されます。 - `dist/appsscript.json` に GASsma ライブラリのエントリが既にある場合は、重複追加されません。 ### GASsma ライブラリバージョンの解決 `dist/appsscript.json` に設定する GASsma ライブラリの最新バージョンは、実行時に自動解決されます。 1. `clasp list-versions` でライブラリの最新バージョン番号を取得 2. 失敗した場合は GitHub 上の GASsma の `package.json` から取得 両方失敗した場合(オフライン時など)は、ライブラリのエントリ追加をスキップし、Apps Script エディタでの手動追加手順(スクリプト ID を含む)が案内されます。オンライン状態で `gassma bootstrap` を再実行すれば、エントリは自動追加されます。 ### パッケージマネージャの自動検出 npm / pnpm / yarn / bun を自動検出し(検出できない場合は npm)、インストールコマンドや完了時の案内メッセージに反映されます。 ## セットアップ後の次の一歩 セットアップが完了すると、次の手順が表示されます。 1. `gassma/schema.prisma` を編集してモデルを定義する 2. `npx gassma generate` で型付きクライアントを生成する 3. `npm run deploy` でビルドして Apps Script に push する 4. `npm run open` で Apps Script エディタを開く スキーマの書き方や `gassma generate` の詳細は [Prisma スキーマを利用したローカル開発](/docs/reference/type-generation) を参照してください。 # fields(列同士の比較) (https://gassma.io/docs/reference/fields) FieldRef を使って where 条件内で同じ行の列同士を比較する `where` 条件内で、固定値ではなく**同じ行の別の列の値**と比較したい場合に `fields` プロパティを利用します。 ## 基本的な使い方 各シートコントローラーの `fields` プロパティから `FieldRef` を取得し、フィルタ条件の値として渡します。 ```ts const gassma = new Gassma.GassmaClient(); const userSheet = gassma.Users; // firstName と lastName が同じ値のユーザーを検索 const result = userSheet.findMany({ where: { firstName: { equals: userSheet.fields.lastName }, }, }); ``` 上記の例では、各行ごとに `firstName` と `lastName` の値を比較し、一致する行のみを返します。 ## 使用できる演算子 `FieldRef` は以下の演算子で使用できます。 | 演算子 | 動作 | 例 | | --- | --- | --- | | equals | 同値か | `{ equals: sheet.fields.otherColumn }` | | lt | 未満 | `{ lt: sheet.fields.maxValue }` | | lte | 以下 | `{ lte: sheet.fields.maxValue }` | | gt | 超過 | `{ gt: sheet.fields.minValue }` | | gte | 以上 | `{ gte: sheet.fields.minValue }` | | contains | 文字列を含むか | `{ contains: sheet.fields.keyword }` | | startsWith | 文字列で始まるか | `{ startsWith: sheet.fields.prefix }` | | endsWith | 文字列で終わるか | `{ endsWith: sheet.fields.suffix }` | `not`、`in`、`notIn` では `FieldRef` は使用できません。 ## 数値の比較 ```ts // age が maxAge より小さいユーザーを検索 const result = userSheet.findMany({ where: { age: { lt: userSheet.fields.maxAge }, }, }); ``` ## 文字列の比較 ```ts // fullName に firstName の値を含むレコードを検索 const result = userSheet.findMany({ where: { fullName: { contains: userSheet.fields.firstName }, }, }); ``` ## mode: "insensitive" との組み合わせ `FieldRef` は `mode: "insensitive"` と組み合わせて大文字小文字を区別しない比較ができます。 ```ts const result = userSheet.findMany({ where: { firstName: { equals: userSheet.fields.lastName, mode: "insensitive", }, }, }); ``` ## AND / OR / NOT での使用 論理演算子の中でも `FieldRef` を使用できます。 ```ts const result = userSheet.findMany({ where: { OR: [ { firstName: { equals: userSheet.fields.lastName } }, { age: { gt: userSheet.fields.minAge } }, ], }, }); ``` ## 使用できるメソッド `fields` は `where` を使用するすべてのメソッドで利用可能です。 - `findMany` / `findFirst` / `findFirstOrThrow` - `update` / `updateMany` / `updateManyAndReturn` - `delete` / `deleteMany` - `upsert` - `count` / `aggregate` / `groupBy` ## 参照先の列が存在しない場合 `FieldRef` で指定した列名がシートに存在しない場合、その条件はマッチしません(エラーにはなりません)。 # migrate / db push(シートの同期) (https://gassma.io/docs/reference/migrate) npx gassma migrate / npx gassma db push で、スキーマに合わせてスプレッドシートのシートと列を同期する GAS 関数を生成する `npx gassma migrate` と `npx gassma db push` は、Prisma スキーマに合わせてスプレッドシートのシートと列を同期する GAS 関数を生成するコマンドです(Prisma の `prisma migrate dev` / `prisma db push` に相当)。 ``` $ npx gassma migrate # 証跡(migrations/)を記録して生成 $ npx gassma migrate --name add_tags # 証跡に名前を付ける $ npx gassma db push # 証跡を記録せずに生成 ``` 2 つのコマンドの違いは**証跡(`migrations/` ディレクトリ)を記録するかどうかだけ**です。`migrate` はスキーマが変わるたびに `migrations/` 配下へ `migration.js` を残し、`db push` は `migrations/` に一切触れません。生成される実行用スタブの内容はどちらも同一です。マイグレーション履歴が不要な場合は `db push` を使ってください。 コマンド自体はスプレッドシートに直接アクセスしません。生成された `gassmaMigrate` 関数を Apps Script 側で 1 回実行した時点でシートが同期されます。`clasp push` も自動実行されません(後述の「生成後の手順」を参照)。 ## 生成されるもの 出力ディレクトリに実行用スタブ `gassma-migration.js`(生の JS)が生成されます。中身は GASsma ライブラリの `migrateSheets` を呼ぶ `gassmaMigrate` 関数 1 つで、スキーマから抽出したシート名と列名が埋め込まれています。 ```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 } ``` 上記のスキーマからは以下が生成されます。 ```js function gassmaMigrate() { Gassma.migrateSheets({ spreadsheetId: "XXXXX", models: [ { name: "User", columns: ["id", "name"] }, { name: "Post", columns: ["id", "title", "authorId"] } ] }); } ``` - スプレッドシート ID は、スキーマ内の `datasource` ブロック → `gassma.config.ts` の `datasource.url` の順で解決されます。どちらにも設定が無い場合は埋め込まれず、実行時にスクリプトにバインドされたスプレッドシートが対象になります。 - 実行には GAS プロジェクトに GASsma ライブラリが登録されている必要があります([bootstrap](/docs/reference/bootstrap) でセットアップした環境ならそのまま動きます)。呼び出しシンボル(例の `Gassma.` の部分)は、出力ディレクトリの `appsscript.json` に登録されたライブラリの `userSymbol` から自動解決されます(見つからない場合は `Gassma`)。 ### 出力先の解決 1. `--output `(最優先) 2. カレントディレクトリの `.clasp.json` の `rootDir` どちらも無い場合は `MigrateOutputDirError` になります。 ### シートと列の抽出規則 - モデルごとに 1 シートが対象になります。`@@map` / `@map` を付けている場合はマッピング後の物理名が使われます。 - 列になるのはスカラーフィールドのみです。リレーションフィールド(上の例の `posts` / `author`)は列にならず、外部キー列(`authorId`)は列になります。 - `@ignore` / `@@ignore` の付いたフィールド・モデルも**作成対象に含まれます**。Prisma と同じく、クライアントから除外されるだけでスプレッドシート上には実体が存在するためです。 - [暗黙的 Many-to-Many](/docs/reference/type-generation#暗黙的-many-to-many) の中間シートも作成対象です(例: `_PostToTag`。列はモデル名のアルファベット順に `postId`, `tagId`)。 ## 生成後の手順 コマンド成功時に表示される Next steps の 2 ステップを実行すると、シートが同期されます。 ``` ✅ 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 には `clasp push`(または `clasp push` を呼ぶだけの `npm run push`)を直接使ってください。クリーンを伴うフルビルド(bootstrap が生成する `npm run deploy` など)は、push 前に出力ディレクトリごと `gassma-migration.js` を消してしまうことがあります。 ## 同期の規則 `gassmaMigrate`(`Gassma.migrateSheets`)による同期は冪等で、何度実行しても安全です。 - スキーマにあってスプレッドシートに無いシートを作成し、1 行目にヘッダーを書き込みます。 - 既存シートには足りない列だけをヘッダー行の右端に追記します。**既存列の並べ替えは行わず**、データ行への書き込みもありません。 - スキーマに無い列・シートは、デフォルトでは削除されず警告ログを出してそのまま残されます。 ``` Gassma.migrateSheets: column "legacy" on sheet "User" is not in the schema. It is left untouched. ``` ## データ削除(--accept-data-loss) スキーマに無い列・シートを削除したい場合は `--accept-data-loss` を付けます。スタブに `acceptDataLoss: true` が埋め込まれ、`gassmaMigrate` の実行時に削除まで行われます。 データが残っている列・シートは、残っている量(列は空でないセルの数、シートはデータ行数)を警告ログに出したうえで削除されます。空の場合は警告なしで削除されます。 ``` Gassma.migrateSheets: You are about to drop the column "legacy" on the sheet "User", which still contains 12 non-empty values. ``` ## 証跡(migrations/) `migrate` は、スキーマと同じディレクトリの `migrations/` 配下に `[_名前]/migration.js` を作成します。中身は実行用スタブと同一です。 ``` gassma/ ├── schema.prisma └── migrations/ ├── 20260801120000_init/ │ └── migration.js └── 20260802093000_add_tags/ └── migration.js ``` - 直近の証跡と内容が同じ場合、新しい証跡は作られません(`Already in sync, no schema change or pending migration was found.` と表示されます)。実行用スタブ自体は毎回書き直されます。 - `--name` で証跡に名前を付けられます。名前は camelCase の分解 → 小文字化 → 英数字以外の連続を `_` に置換、の規則でサニタイズされます(例: `--name "Add UserRole!"` → `20260801120000_add_user_role`)。 ## オプション ### migrate | オプション | 説明 | | --- | --- | | `--name ` | マイグレーションの名前 | | `--output ` | `gassma-migration.js` の出力先ディレクトリ(デフォルトは `.clasp.json` の `rootDir`) | | `--schema ` | マイグレーション対象の `.prisma` ファイルのパス | | `--config ` | GASsma config ファイルのカスタムパス | | `--accept-data-loss` | スキーマに無いシート・列を削除 | ### db push | オプション | 説明 | | --- | --- | | `--output ` | `gassma-migration.js` の出力先ディレクトリ(デフォルトは `.clasp.json` の `rootDir`) | | `--schema ` | 同期対象の `.prisma` ファイルのパス | | `--config ` | GASsma config ファイルのカスタムパス | | `--accept-data-loss` | スキーマに無いシート・列を削除 | ## 制限事項 - ヘッダー行は各シートの **1 行目・A 列開始**が前提です。[changeSettings](/docs/reference/settings/changeSettings) でヘッダー位置を変更している場合には対応していません。 - スプレッドシートには最低 1 枚のシートが必要なため、`--accept-data-loss` を付けても最後の 1 枚は削除されず、警告のみになります。 ## Gassma.migrateSheets(ライブラリ API) スタブが呼んでいる `Gassma.migrateSheets` は GASsma の公開 API です。CLI を使わずに、同期したいシートと列を直接指定して呼ぶこともできます。 ```ts Gassma.migrateSheets({ spreadsheetId: "SPREAD_SHEET_ID", // 省略時はバインドされたスプレッドシート models: [{ name: "User", columns: ["id", "name"] }], acceptDataLoss: false, }); ``` `models` は必須で、省略すると `GassmaMissingArgumentError` になります。同期の規則・制限事項は CLI 経由の場合と同じです。 # raw(数式の書き込み) (https://gassma.io/docs/reference/raw) Gassma.raw で数式インジェクション対策の自動エスケープをセル単位で回避し、セルに数式をそのまま書き込む `Gassma.raw(value)` は、そのセルに限って数式インジェクション対策の自動エスケープを回避し、値を**そのまま**シートに書き込むためのヘルパーです。`=` で始まる文字列を渡すと、セルには**本物のスプレッドシート数式**として書き込まれます。 ## 既定の保護(自動エスケープ) GASsma は書き込み時、`=`・`+`・`-`・`@` のいずれかで始まる文字列の先頭に `'`(シングルクォート)を付けてエスケープします。これにより、フォーム入力などに紛れ込んだ `=IMPORTRANGE(...)` のような文字列が数式として実行されること(数式インジェクション)を防いでいます。 - エスケープの対象は**文字列のみ**です。数値・boolean・Date はそのまま書き込まれます。ただし `NaN` / `Infinity` / `-Infinity` や不正な Date(Invalid Date)は、エスケープ以前に書き込み自体が `GassmaInvalidValueError` で拒否されます。 - `'` はスプレッドシート上の表示・読み取りには現れないため、エスケープされたセルを GASsma で読み直すと元の文字列(例: `"=1+2"`)がそのまま返ります。 この保護は常時有効なため、集計用の数式を意図的に書き込みたい場合には邪魔になります。そのためのオプトアウトが `Gassma.raw` です。 ## 基本的な使い方 `data` の値を `Gassma.raw()` で包むと、そのセルだけエスケープが行われません。主な用途は、数値カラムへの集計数式の書き込みです。 ```ts const gassma = new Gassma.GassmaClient(); gassma.Report.create({ data: { title: userInput, // 通常どおりエスケープされる total: Gassma.raw("=SUM(B2:B10)"), // このセルだけ数式として書き込まれる }, }); ``` 同じ書き込みの中でも、`Gassma.raw()` を使っていないカラム(上の `title`)は従来どおり保護されたままです。 `create` 系・`update` 系のすべてのメソッド(`create` / `createMany` / `createManyAndReturn` / `update` / `updateMany` / `updateManyAndReturn` / `upsert`)と、nested write の `create` / `createMany` / `connectOrCreate` の `create` で使用できます。 ## 戻り値は計算結果ではない `create` / `update` などの戻り値は「**書き込んだ内容のエコー**」です。GASsma は書き込み後にシートを読み直さないため、数式の**計算結果は返りません**。数値カラムに数式を書き込んだ場合、戻り値は数式の**文字列**になります。 ```ts const created = gassma.FormulaCell.create({ data: { id: 4, label: "delta", amount: 60, total: Gassma.raw("=C5*2") }, }); created.total; // "=C5*2"(数式文字列。計算結果ではない) ``` 計算結果が必要な場合は、書き込み後に読み直してください。 ```ts const readBack = gassma.FormulaCell.findFirstOrThrow({ where: { id: 4 } }); readBack.total; // 120(セル上で計算された結果が返る) ``` ## raw の効果は渡したセルだけ `Gassma.raw()` の効果は、それを渡した**そのセルだけ**に限られます。 - 同じ行の他のカラムは通常どおりエスケープされます。 - nested write で子行に引き渡される FK 値など、raw セルを参照する他の書き込みが raw として扱われることもありません。 ```ts gassma.FormulaCell.create({ data: { id: 4, label: "=1+2", // エスケープされ、文字列 "=1+2" のまま amount: 60, total: Gassma.raw("=C5*2"), // 数式として書き込まれ、セル上で計算される }, }); ``` ## $transaction との組み合わせ [$transaction](/docs/reference/transaction) 内でも使用できます。raw の値はコミット時にまとめてシートへ書き込まれ、その時点で数式になります。 コミット前の tx 内の読み取り(read-your-writes)では、まだシートに書き込まれていないため**数式文字列のまま**見えます。 ```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"(コミット前なので計算されていない) }); // コミット後はセルに数式として書き込まれ、計算結果が読める const readBack = gassma.FormulaCell.findFirstOrThrow({ where: { id: 4 } }); readBack.total; // 120 ``` ## 型 `Gassma.raw(value: string)` は `Gassma.RawValue` 型を返します。 - 生成型では、`data` の**すべてのカラム**が `Gassma.RawValue` を受け付けます。数式はセル上で任意の型の値を返せるため、数値・boolean・Date のカラムにも渡せます。 - `where` では使用できません。書き込み専用です。`where` の値に渡すと `GassmaInvalidValueError`(Expected a scalar value, but received a Gassma.raw value.)がスローされます。 ## 信頼できない入力には使わない `Gassma.raw()` に**ユーザー入力を渡してはいけません**。既定の保護を回避するため、数式インジェクションに対して脆弱になります。 ```ts // 危険: フォーム入力をそのまま raw に渡している function onFormSubmit(e) { const gassma = new Gassma.GassmaClient(); gassma.Answers.create({ data: { // 悪意のあるユーザーが "=IMPORTRANGE(...)" を入力すると // 数式として実行されてしまう name: Gassma.raw(e.namedValues["名前"][0]), }, }); } ``` `Gassma.raw()` は、自分で書いた固定の数式など、**信頼できる値のみ**に使用してください。ユーザー入力は `Gassma.raw()` で包まずそのまま渡せば、既定の保護が適用されます。 # エラー一覧 (https://gassma.io/docs/reference/errors) GASsma が投げるエラークラスの一覧と発生条件 GASsma で発生するエラークラスの一覧です。 ## エラーの捕捉 GASsma のエラークラスは `Gassma` 名前空間から公開されています。`try` / `catch` で捕捉し、`instanceof` でエラーの種類を判定できます。 ```ts try { gassma.sheet1.findFirst({ take: 5 }); } catch (e) { if (e instanceof Gassma.GassmaFindFirstTakeError) { // findFirst の take が不正なときの処理 } } ``` 公開されているエラークラスは 51 個で、これに `GassmaClient` / `GassmaController` / `FieldRef` / `skip` を加えた 55 個が `Gassma` 名前空間の公開実体です。 `instanceof Date` のようなビルトイン型の判定はライブラリ境界を越えると `false` になります([基本](/docs/reference/basic)を参照)。一方、GASsma のエラークラスは `Gassma` 名前空間(ライブラリの global)経由で参照するため、`instanceof` で正しく判定できます。 ## 検索・クエリ系 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `GassmaFindSelectOmitConflictError` | Cannot use both select and omit in the same query | `select` と `omit` を同時に指定 | | `NotFoundError` | An operation failed because it depends on one or more records that were required but not found. | `findFirstOrThrow` でレコードが見つからない | | `GassmaSkipNegativeError` | Invalid value for skip argument: Value can only be positive, found: \{value\} | `skip` に**有限の**負数を指定(`include` の `skip` も同様)。`NaN` / `Infinity` / `-Infinity` / `null` は `GassmaInvalidValueError` になります | | `GassmaLimitNegativeError` | Invalid value for limit argument: Value can only be positive, found: \{value\} | `limit` に**有限の**負数を指定。`NaN` / `Infinity` / `-Infinity` / `null` は `GassmaInvalidValueError` になります | | `GassmaFindFirstTakeError` | The 'findFirst' operation cannot be used with a 'take' argument that isn't 1 or -1 | `findFirst` の `take` に `1` / `-1` 以外を指定(`NaN` / `Infinity` / `-Infinity` を含む)。`take: null` のみ `GassmaInvalidValueError` になります | ## strictUndefinedChecks / Gassma.skip 系 詳しくは [strictUndefinedChecks / Gassma.skip](/docs/reference/config/strict-undefined-checks) を参照してください。 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `GassmaUndefinedValueError` | Invalid value for argument \`\{path\}\`: explicitly \`undefined\` values are not allowed. | `strictUndefinedChecks` 有効時にクエリ入力へ明示的な `undefined` を指定。配列の要素(`in` / `AND` / `OR` / `orderBy` など)への `undefined` は有効・無効に関わらず発生 | | `GassmaSkipInArrayError` | Invalid value for argument \`\{path\}\`: Can not use \`Gassma.skip\` value within array. Use \`null\` or filter out \`Gassma.skip\` values. | 配列の要素に `Gassma.skip` を指定(`strictUndefinedChecks` の有効・無効に関わらず発生) | ## orderBy 系 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `RelationOrderByUnsupportedTypeError` | Cannot use orderBy on "\{relationName\}" (type: \{relationType\}). Only manyToOne and oneToOne are supported. | oneToMany / manyToMany のリレーションでフィールドソートを使用 | | `RelationOrderByCountUnsupportedTypeError` | Cannot use \_count orderBy on "\{relationName\}" (type: \{relationType\}). Only oneToMany and manyToMany are supported. | manyToOne / oneToOne のリレーションで `_count` ソートを使用 | ## 集計系 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `GassmaAggregateMaxError` | Cannot produce a maximum value of more than one type. | `_max` で異なる型が混在 | | `GassmaAggregateMinError` | Cannot produce a maximum value of more than one type. | `_min` で異なる型が混在 | | `GassmaAggregateSumError` | Cannot produce a maximum value of more than one type. | `_sum` で数値以外の型が混在 | | `GassmaAggregateAvgError` | Cannot produce a maximum value of more than one type. | `_avg` で数値以外の型が混在 | | `GassmaAggregateTypeError` | Only "number", "string", "boolean", and "Date" types are supported. | `_max` / `_min` でサポートされていない型 | | `GassmaAggregateSumTypeError` | Only "number" type is supported. | `_sum` で数値以外の型 | | `GassmaAggregateAvgTypeError` | Only "number" type is supported. | `_avg` で数値以外の型 | | `GassmaAggregateSelectionRequiredError` | At least one aggregation is required: specify \`_avg\`, \`_count\`, \`_max\`, \`_min\`, or \`_sum\` with at least one field. | `aggregate` で `_avg` / `_count` / `_max` / `_min` / `_sum` のいずれもフィールドを指定していない(`where` / `orderBy` / `take` だけの指定や、`_count: {}` のような空指定も含む) | `GassmaAggregateMinError` / `GassmaAggregateSumError` / `GassmaAggregateAvgError` は `GassmaAggregateMaxError` を継承しています。また `GassmaAggregateAvgTypeError` は `GassmaAggregateSumTypeError` を継承しています。そのため、基底クラスで `instanceof` 判定すると派生クラスもまとめて捕捉できます。 ## groupBy 系 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `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. | `having` で `by` に含まれないカラムを使用 | ## 設定系 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `GassmaInValidColumnValueError` | startColumnValue and endColumnValue can only use number, \[a-z\] and \[A-Z\]. | `changeSettings` に無効な列値を指定 | ## 引数系 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `GassmaMissingArgumentError` | Argument \`\{argumentName\}\` is missing. | 必須引数(`data` / `where` / `create` / `update` / `by` など)を省略 | | `GassmaUnknownArgumentError` | Unknown argument \`\{argumentName\}\`. Did you mean \`\{suggestion\}\`? Available: \{availableArguments\} | クエリ入力に未知のキーを指定(トップレベル引数、`where` / `data` / `select` / `omit` / `orderBy` のカラム名、フィルタ演算子、`increment` などの更新演算子等)。`GassmaClient` のオプション(`map` / `defaults` / `updatedAt` / `autoincrement` / `ignore` / `omit`)が存在しないカラムを参照した場合も発生 | | `GassmaInvalidValueError` | Invalid value for argument \`\{argumentName\}\`. Expected \{expected\}. | 引数の値が受け付けられない形。発生条件が多いため[下記](#gassmainvalidvalueerror-の発生条件)にまとめています | `GassmaUnknownArgumentError` のメッセージのうち、`Did you mean ...?` は近い候補が見つかった場合のみ、`Available: ...` は候補一覧が空でない場合のみ含まれます。 ### GassmaInvalidValueError の発生条件 メッセージは常に Invalid value for argument \`\{argumentName\}\`. Expected \{expected\}. の形式です。以下の表では `{expected}` の部分を示します。 #### 形が正しくない引数 | 条件 | `{argumentName}` | `{expected}` | | --- | --- | --- | | `OR` / `AND` / `NOT` に配列以外を指定 | `OR` など | an array | | `orderBy` の値が `"asc"` / `"desc"` でない(配列を渡した場合を含む) | `orderBy` | "asc" \| "desc" | | `orderBy` の `sort` が `"asc"` / `"desc"` でない | `sort` | "asc" \| "desc" | | `orderBy` の `nulls` が `"first"` / `"last"` でない | `nulls` | "first" \| "last" | | `orderBy` のリレーションキーにオブジェクト以外を指定 | リレーション名 | a relation orderBy object | | `select` に選択するフィールドが 1 つもない | `select` | at least one selected field | | `cursor` にカラムが 1 つもない | `cursor` | at least one column | | 単一行操作(`update` / `delete` / `upsert`)の `where` に条件が 1 つもない | `where` | at least one condition | #### ページング(`take` / `skip` / `limit`)の異常値 | 値 | `{expected}` | | --- | --- | | `NaN` / `Infinity` / `-Infinity` | a finite number, but received NaN | | `null` | a number, but received null | `take` / `skip` は `findMany` / `findFirst` / `count` / `aggregate` / `groupBy`、`limit` は `updateMany` / `updateManyAndReturn` / `deleteMany` が対象です。`undefined` は従来どおり「指定しなかった」扱いで無視されます。 `findFirst` の `take` は `1` / `-1` の判定が先に行われるため、`NaN` / `Infinity` / `-Infinity` は `GassmaFindFirstTakeError` になります(`take: null` のみ `GassmaInvalidValueError`)。`include` 内の `take` / `skip` は `IncludeInvalidOptionTypeError` になります。 #### 構造を期待する引数への `null` `{expected}` は末尾に `, but received null` が付きます(例: 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 | | `cursor` のカラムの値 | a scalar value | 配列の要素に `null` を入れた場合も同じエラーになります(`AND: [null]` / `distinct: [null]` / `createMany` の `data: [null]` など)。詳しくは [null の扱い](/docs/reference/crud/read/findMany#null-の扱い)を参照してください。 #### セルに保存できない値 書き込み(`data`)およびクエリ(`where` / `cursor` / `having`)の値が対象です。 | 値 | `{expected}` | | --- | --- | | `NaN` / `Infinity` / `-Infinity` | a finite number, but received NaN | | Invalid Date | a valid Date, but the provided Date object is invalid | | 配列 | a scalar value, but received an array | | 関数 | a scalar value, but received a function | | Symbol | a scalar value, but received a symbol | | BigInt | a scalar value, but received a bigint | | `Map` / `Set` / `RegExp` / `Error` / `Promise` などの組み込みオブジェクト | a scalar value, but received a Map | | クラスインスタンスなどのその他のオブジェクト | a scalar value, but received an object | | `Gassma.raw`(`where` / `cursor` / `having` のみ) | a scalar value, but received a Gassma.raw value | `Date` / `Gassma.raw`(`data` のみ)/ `FieldRef` はオブジェクトですが、そのまま渡せます。組み込みオブジェクトの名前の部分は値の内部種別がそのまま入ります(`Set` なら `but received a Set.`)。 #### 数値演算の結果 `increment` / `decrement` / `multiply` / `divide` の**演算結果**が `NaN` / `Infinity` / `-Infinity` になる場合に発生します。`{argumentName}` は**カラム名**です。 | `{expected}` | | --- | | a finite number, but received Infinity | 詳しくは [update()](/docs/reference/crud/update/update#数値の原子的操作)を参照してください。 ## リレーション定義系 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `RelationSheetNotFoundError` | Sheet "\{sheetName\}" is not found in the spreadsheet | リレーション定義のシートが存在しない | | `RelationMissingPropertyError` | Relation "\{relationName\}" on sheet "\{sheetName\}" is missing required property "\{property\}" | リレーション定義の必須プロパティが欠落 | | `RelationInvalidPropertyTypeError` | Relation "\{relationName\}" on sheet "\{sheetName\}": property "\{property\}" must be a \{expectedType\} | リレーション定義のプロパティの型が不正 | | `RelationInvalidTypeError` | Relation "\{relationName\}" on sheet "\{sheetName\}": type "\{value\}" is not valid. Must be one of: oneToMany, oneToOne, manyToOne, manyToMany | リレーションの `type` が無効 | | `RelationColumnNotFoundError` | Column "\{columnName\}" is not found in sheet "\{sheetName\}" | リレーション定義の `field` / `reference` のカラムが存在しない | | `RelationInvalidOnDeleteError` | Relation "\{relationName\}" on sheet "\{sheetName\}": onDelete "\{value\}" is not valid. Must be one of: Cascade, SetNull, Restrict, NoAction | `onDelete` の値が無効 | | `RelationInvalidOnUpdateError` | Relation "\{relationName\}" on sheet "\{sheetName\}": onUpdate "\{value\}" is not valid. Must be one of: Cascade, SetNull, Restrict, NoAction | `onUpdate` の値が無効 | | `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 | リレーションの `field` / `reference`(manyToMany では `through` の `field` / `reference` も)が `ignore` 指定されたカラムを参照している(クライアント初期化時に検出) | ## リレーション操作系 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `GassmaRelationNotFoundError` | Relation "\{relationName\}" is not defined for sheet "\{sheetName\}" | `include` で未定義のリレーション名を指定 | | `GassmaRelationDuplicateError` | Duplicate value "\{value\}" found in "\{sheetName\}.\{field\}" for a unique relation | oneToOne / manyToOne でリレーション先に重複値が存在 | | `GassmaThroughRequiredError` | Relation "\{relationName\}" is manyToMany but "through" is not defined | manyToMany で `through`(中間テーブル)が未定義 | | `RelationOnDeleteRestrictError` | Cannot delete: related records exist for relation "\{relationName\}" (onDelete: Restrict) | `onDelete: "Restrict"` 設定時に関連レコードが存在する状態で削除 | | `RelationOnUpdateRestrictError` | Cannot update: related records exist for relation "\{relationName\}" (onUpdate: Restrict) | `onUpdate: "Restrict"` 設定時に関連レコードが存在する状態で PK を更新 | ## include 系 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `IncludeWithoutRelationsError` | Cannot use include without defining relations in GassmaClient | リレーション定義なしで `include` を使用 | | `GassmaIncludeSelectConflictError` | Cannot use both include and select in the same query | トップレベルで `include` と `select` を同時使用 | | `IncludeInvalidOptionTypeError` | Include "\{relationName\}": option "\{option\}" must be \{expectedType\} | `include` のオプション値の型が不正。`take` / `skip` に `NaN` / `Infinity` / `-Infinity` を指定すると `must be a finite number`、`null` など数値以外を指定すると `must be a number` になります | | `IncludeSelectOmitConflictError` | Include "\{relationName\}": cannot use both select and omit at the same time | `include` 内で `select` と `omit` を同時指定 | | `IncludeSelectIncludeConflictError` | Include "\{relationName\}": cannot use both select and include at the same time | `include` 内で `select` と `include` を同時指定 | ## where リレーションフィルタ系 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `WhereRelationInvalidFilterError` | Filter "\{filterType\}" cannot be used on relation "\{relationName\}" of type "\{relationType\}" | リレーション型に不適切なフィルタを使用(例: oneToMany に `is` を使用) | | `WhereRelationWithoutContextError` | Cannot use relation filters in where clause without defining relations | リレーション定義なしでリレーションフィルタを使用 | ## Nested Write 系 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `NestedWriteWithoutRelationsError` | Cannot use nested write operations without defining relations in GassmaClient | リレーション定義なしで Nested Write を使用 | | `NestedWriteConnectNotFoundError` | Nested write connect failed: no record found in "\{sheetName\}" | `connect` / `connectOrCreate` で対象レコードが見つからない | | `NestedWriteRelationNotFoundError` | Nested write failed: "\{fieldName\}" is not a defined relation | Nested Write で未定義のリレーション名を使用 | | `NestedWriteInvalidOperationError` | Nested write: operation "\{operation\}" is not valid for relation "\{relationName\}" of type "\{relationType\}" | リレーション型に非対応の操作を使用(例: manyToMany に `delete` を使用) | | `NestedWriteTargetNotFoundError` | Nested write \{operation\} failed: no record found in "\{sheetName\}" | 非FK側 oneToOne の nested `update` / `delete` でリレーション先レコードが存在しない | ## トランザクション系 詳しくは [$transaction](/docs/reference/transaction) を参照してください。 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `GassmaTransactionLockTimeoutError` | Transaction API error: Unable to start a transaction in the given time. The maxWait for this transaction was \{maxWaitMs\} ms. | `$transaction` の開始時、`maxWait` 以内にスクリプトロックを取得できない | | `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. | トランザクション開始からの経過時間が `timeout` を超過(tx 操作の呼び出し時またはコミット直前に検知) | | `GassmaNestedTransactionError` | Transaction API error: Nested transactions are not supported. Do not call $transaction inside an active transaction. | トランザクション内で `$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\} | コミット中の書き込み失敗後、バックアップからの自動復元にも失敗(`backupSheetNames` プロパティに残されたバックアップシート名の一覧) | ## CLI 設定ファイル系 CLI コマンド(`gassma generate` 等)の実行時に発生するエラーです。 | エラー | メッセージ | 発生条件 | | --- | --- | --- | | `ConfigFileNotFoundError` | GASsmaConfigFileNotFoundError: config file not found at \{configPath\} | `--config` で指定した設定ファイルが存在しない | | `GassmaConfigLoadError` | GASsmaConfigLoadError: Failed to load config file at \{configPath\}. \{detail\} | 設定ファイルの構文エラー・実行時エラー、既知キー(`schema` / `datasource.url`)の型不正、config オブジェクト以外のエクスポート | | `GassmaConfigEnvError` | Cannot resolve environment variable: \{name\}. | `env()` で参照した環境変数が未設定または空文字 |