# 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 としてください)  その後、`拡張機能` > `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`のようなスプレッドシート関数を入れた時、任意の不正な処理ができてしまいます。(フォーミュラ・インジェクション)
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) を参照 |
## 説明例用のシート

## 説明
上記例から以下の条件の行を取り出したいとします。
- 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 つの場合、配列の省略が可能です |
## 説明例用のシート

## 説明
上記例から以下の条件の行を取り出したいとします。
- 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` が空になった場合も同様です。
## 説明例用のシート

## 説明
上記例から以下の処理を行いたいとします。
- 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` を渡したフィールドは「指定しなかった」扱いになり、更新されません。
## 説明例用のシート

## 説明
上記例から以下の処理を行いたいとします。
- 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` は省略可能で、省略すると全行が対象になります。
## 説明例用のシート

## 説明
上記例から以下の処理を行いたいとします。
- 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` が空になった場合も同様です。
## 説明例用のシート

## 説明
上記例から以下の処理を行いたいとします。
- 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` が空になった場合も同様です。
## 説明例用のシート

## 説明
上記例から以下の処理を行いたいとします。
- 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) を有効にしてください。
## 説明例用のシート

## 説明
上記例から以下の処理を行いたいとします。
- 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`)も利用可能です。
## 説明例用のシート

## 説明
上記例から以下の処理を行いたいとします。
- 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`)も利用可能です。
## 説明例用のシート

## 説明
上記例から以下の処理を行いたいとします。
- 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`)も利用可能です。
## 説明例用のシート

## 説明
上記例から以下の処理を行いたいとします。
- 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. テーブルが左上にないとき

以上のテーブルの場合は以下のようなコードを書くことで正常にテーブルを読み込めます。
```ts
const gassma = new Gassma.GassmaClient();
// シートの操作をする前に必ず記述
gassma.sheet1.changeSettings(4, "B", "E");
const result = gassma.sheet1.findMany({});
```
### 2. 右側にメモ書き等別のデータがあるとき

```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 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()` で参照した環境変数が未設定または空文字 |