@b2bc-devkit/sheetorm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/npm/SheetOrm.d.ts +41 -0
- package/dist/npm/SheetOrm.d.ts.map +1 -0
- package/dist/npm/SheetOrm.js +41 -0
- package/dist/npm/SheetOrm.js.map +1 -0
- package/dist/npm/core/Decorators.d.ts +145 -0
- package/dist/npm/core/Decorators.d.ts.map +1 -0
- package/dist/npm/core/Decorators.js +226 -0
- package/dist/npm/core/Decorators.js.map +1 -0
- package/dist/npm/core/Record.d.ts +167 -0
- package/dist/npm/core/Record.d.ts.map +1 -0
- package/dist/npm/core/Record.js +298 -0
- package/dist/npm/core/Record.js.map +1 -0
- package/dist/npm/core/RecordConstructor.d.ts +20 -0
- package/dist/npm/core/RecordConstructor.d.ts.map +1 -0
- package/dist/npm/core/RecordConstructor.js +2 -0
- package/dist/npm/core/RecordConstructor.js.map +1 -0
- package/dist/npm/core/RecordStatic.d.ts +19 -0
- package/dist/npm/core/RecordStatic.d.ts.map +1 -0
- package/dist/npm/core/RecordStatic.js +2 -0
- package/dist/npm/core/RecordStatic.js.map +1 -0
- package/dist/npm/core/Registry.d.ts +100 -0
- package/dist/npm/core/Registry.d.ts.map +1 -0
- package/dist/npm/core/Registry.js +206 -0
- package/dist/npm/core/Registry.js.map +1 -0
- package/dist/npm/core/SheetRepository.d.ts +331 -0
- package/dist/npm/core/SheetRepository.d.ts.map +1 -0
- package/dist/npm/core/SheetRepository.js +1041 -0
- package/dist/npm/core/SheetRepository.js.map +1 -0
- package/dist/npm/core/cache/MemoryCache.d.ts +67 -0
- package/dist/npm/core/cache/MemoryCache.d.ts.map +1 -0
- package/dist/npm/core/cache/MemoryCache.js +112 -0
- package/dist/npm/core/cache/MemoryCache.js.map +1 -0
- package/dist/npm/core/types/Entity.d.ts +21 -0
- package/dist/npm/core/types/Entity.d.ts.map +1 -0
- package/dist/npm/core/types/Entity.js +2 -0
- package/dist/npm/core/types/Entity.js.map +1 -0
- package/dist/npm/core/types/FieldDefinition.d.ts +21 -0
- package/dist/npm/core/types/FieldDefinition.d.ts.map +1 -0
- package/dist/npm/core/types/FieldDefinition.js +2 -0
- package/dist/npm/core/types/FieldDefinition.js.map +1 -0
- package/dist/npm/core/types/FieldType.d.ts +12 -0
- package/dist/npm/core/types/FieldType.d.ts.map +1 -0
- package/dist/npm/core/types/FieldType.js +2 -0
- package/dist/npm/core/types/FieldType.js.map +1 -0
- package/dist/npm/core/types/Filter.d.ts +16 -0
- package/dist/npm/core/types/Filter.d.ts.map +1 -0
- package/dist/npm/core/types/Filter.js +2 -0
- package/dist/npm/core/types/Filter.js.map +1 -0
- package/dist/npm/core/types/FilterOperator.d.ts +16 -0
- package/dist/npm/core/types/FilterOperator.d.ts.map +1 -0
- package/dist/npm/core/types/FilterOperator.js +2 -0
- package/dist/npm/core/types/FilterOperator.js.map +1 -0
- package/dist/npm/core/types/GroupResult.d.ts +14 -0
- package/dist/npm/core/types/GroupResult.d.ts.map +1 -0
- package/dist/npm/core/types/GroupResult.js +2 -0
- package/dist/npm/core/types/GroupResult.js.map +1 -0
- package/dist/npm/core/types/ICacheProvider.d.ts +20 -0
- package/dist/npm/core/types/ICacheProvider.d.ts.map +1 -0
- package/dist/npm/core/types/ICacheProvider.js +2 -0
- package/dist/npm/core/types/ICacheProvider.js.map +1 -0
- package/dist/npm/core/types/ISheetAdapter.d.ts +54 -0
- package/dist/npm/core/types/ISheetAdapter.d.ts.map +1 -0
- package/dist/npm/core/types/ISheetAdapter.js +2 -0
- package/dist/npm/core/types/ISheetAdapter.js.map +1 -0
- package/dist/npm/core/types/ISpreadsheetAdapter.d.ts +34 -0
- package/dist/npm/core/types/ISpreadsheetAdapter.d.ts.map +1 -0
- package/dist/npm/core/types/ISpreadsheetAdapter.js +2 -0
- package/dist/npm/core/types/ISpreadsheetAdapter.js.map +1 -0
- package/dist/npm/core/types/IndexDefinition.d.ts +16 -0
- package/dist/npm/core/types/IndexDefinition.d.ts.map +1 -0
- package/dist/npm/core/types/IndexDefinition.js +2 -0
- package/dist/npm/core/types/IndexDefinition.js.map +1 -0
- package/dist/npm/core/types/LifecycleHooks.d.ts +32 -0
- package/dist/npm/core/types/LifecycleHooks.d.ts.map +1 -0
- package/dist/npm/core/types/LifecycleHooks.js +2 -0
- package/dist/npm/core/types/LifecycleHooks.js.map +1 -0
- package/dist/npm/core/types/PaginatedResult.d.ts +18 -0
- package/dist/npm/core/types/PaginatedResult.d.ts.map +1 -0
- package/dist/npm/core/types/PaginatedResult.js +2 -0
- package/dist/npm/core/types/PaginatedResult.js.map +1 -0
- package/dist/npm/core/types/QueryOptions.d.ts +21 -0
- package/dist/npm/core/types/QueryOptions.d.ts.map +1 -0
- package/dist/npm/core/types/QueryOptions.js +2 -0
- package/dist/npm/core/types/QueryOptions.js.map +1 -0
- package/dist/npm/core/types/SortClause.d.ts +10 -0
- package/dist/npm/core/types/SortClause.d.ts.map +1 -0
- package/dist/npm/core/types/SortClause.js +2 -0
- package/dist/npm/core/types/SortClause.js.map +1 -0
- package/dist/npm/core/types/SystemColumns.d.ts +16 -0
- package/dist/npm/core/types/SystemColumns.d.ts.map +1 -0
- package/dist/npm/core/types/SystemColumns.js +16 -0
- package/dist/npm/core/types/SystemColumns.js.map +1 -0
- package/dist/npm/core/types/TableSchema.d.ts +24 -0
- package/dist/npm/core/types/TableSchema.d.ts.map +1 -0
- package/dist/npm/core/types/TableSchema.js +2 -0
- package/dist/npm/core/types/TableSchema.js.map +1 -0
- package/dist/npm/index/IndexMeta.d.ts +16 -0
- package/dist/npm/index/IndexMeta.d.ts.map +1 -0
- package/dist/npm/index/IndexMeta.js +2 -0
- package/dist/npm/index/IndexMeta.js.map +1 -0
- package/dist/npm/index/IndexStore.d.ts +399 -0
- package/dist/npm/index/IndexStore.d.ts.map +1 -0
- package/dist/npm/index/IndexStore.js +1104 -0
- package/dist/npm/index/IndexStore.js.map +1 -0
- package/dist/npm/index.d.ts +115 -0
- package/dist/npm/index.d.ts.map +1 -0
- package/dist/npm/index.js +367 -0
- package/dist/npm/index.js.map +1 -0
- package/dist/npm/query/Query.d.ts +176 -0
- package/dist/npm/query/Query.d.ts.map +1 -0
- package/dist/npm/query/Query.js +270 -0
- package/dist/npm/query/Query.js.map +1 -0
- package/dist/npm/query/QueryEngine.d.ts +140 -0
- package/dist/npm/query/QueryEngine.d.ts.map +1 -0
- package/dist/npm/query/QueryEngine.js +453 -0
- package/dist/npm/query/QueryEngine.js.map +1 -0
- package/dist/npm/storage/GoogleSheetAdapter.d.ts +120 -0
- package/dist/npm/storage/GoogleSheetAdapter.d.ts.map +1 -0
- package/dist/npm/storage/GoogleSheetAdapter.js +239 -0
- package/dist/npm/storage/GoogleSheetAdapter.js.map +1 -0
- package/dist/npm/storage/GoogleSpreadsheetAdapter.d.ts +55 -0
- package/dist/npm/storage/GoogleSpreadsheetAdapter.d.ts.map +1 -0
- package/dist/npm/storage/GoogleSpreadsheetAdapter.js +99 -0
- package/dist/npm/storage/GoogleSpreadsheetAdapter.js.map +1 -0
- package/dist/npm/testing/ParityCatalog.d.ts +38 -0
- package/dist/npm/testing/ParityCatalog.d.ts.map +1 -0
- package/dist/npm/testing/ParityCatalog.js +400 -0
- package/dist/npm/testing/ParityCatalog.js.map +1 -0
- package/dist/npm/testing/RuntimeBenchmark.d.ts +37 -0
- package/dist/npm/testing/RuntimeBenchmark.d.ts.map +1 -0
- package/dist/npm/testing/RuntimeBenchmark.js +449 -0
- package/dist/npm/testing/RuntimeBenchmark.js.map +1 -0
- package/dist/npm/testing/RuntimeParity.d.ts +62 -0
- package/dist/npm/testing/RuntimeParity.d.ts.map +1 -0
- package/dist/npm/testing/RuntimeParity.js +4030 -0
- package/dist/npm/testing/RuntimeParity.js.map +1 -0
- package/dist/npm/utils/Serialization.d.ts +111 -0
- package/dist/npm/utils/Serialization.d.ts.map +1 -0
- package/dist/npm/utils/Serialization.js +281 -0
- package/dist/npm/utils/Serialization.js.map +1 -0
- package/dist/npm/utils/SheetOrmLogger.d.ts +31 -0
- package/dist/npm/utils/SheetOrmLogger.d.ts.map +1 -0
- package/dist/npm/utils/SheetOrmLogger.js +42 -0
- package/dist/npm/utils/SheetOrmLogger.js.map +1 -0
- package/dist/npm/utils/Uuid.d.ts +29 -0
- package/dist/npm/utils/Uuid.d.ts.map +1 -0
- package/dist/npm/utils/Uuid.js +86 -0
- package/dist/npm/utils/Uuid.js.map +1 -0
- package/license.md +18 -0
- package/package.json +77 -0
- package/readme.md +672 -0
package/readme.md
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
# SheetORM
|
|
2
|
+
|
|
3
|
+
A TypeScript ORM for Google Sheets running in Google Apps Script (GAS). SheetORM brings a structured,
|
|
4
|
+
type-safe persistence layer to spreadsheet-based applications with an **ActiveRecord** API — define a class,
|
|
5
|
+
extend `Record`, and everything just works.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **ActiveRecord pattern** — Extend `Record`, define fields, and call `save()` / `find()` / `delete()`
|
|
10
|
+
directly on instances and classes
|
|
11
|
+
- **Zero configuration** — Tables, schemas, indexes, and repositories are auto-created on first use
|
|
12
|
+
- **Predictable naming** — Sheet names follow `tbl_{ClassName}s` (e.g. `tbl_Cars`); combined index sheets
|
|
13
|
+
follow `idx_{ClassName}s` (e.g. `idx_Cars`)
|
|
14
|
+
- **Fluent query builder** — `where()`, `and()`, `or()`, `orderBy()`, `limit()`, `offset()`
|
|
15
|
+
- **`Query.from()`** — Start queries from a class reference or string name
|
|
16
|
+
- **Secondary indexes** — Stored in a single combined index sheet (`idx_{ClassName}s`) for fast lookup by
|
|
17
|
+
indexed fields
|
|
18
|
+
- **N-gram text search** — Solr-like trigram search on `@Indexed` fields via `IndexStore.searchCombined()`;
|
|
19
|
+
also available as the `search` filter operator in queries
|
|
20
|
+
- **In-memory caching** — Configurable TTL cache to reduce sheet reads
|
|
21
|
+
- **Lifecycle hooks** — `beforeSave`, `afterSave`, `beforeDelete`, `afterDelete`
|
|
22
|
+
- **Batch operations** — `beginBatch` / `commitBatch` / `rollbackBatch` for safe bulk writes
|
|
23
|
+
- **Pagination & grouping** — `select()` returns `PaginatedResult<T>`, `groupBy()` returns `GroupResult<T>`
|
|
24
|
+
- **Zero runtime dependencies** — Bundles into a single `Code.js` via Vite
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### 1. Install & build
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install
|
|
32
|
+
npm run build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Deploy to Google Apps Script
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm run login # once per device
|
|
39
|
+
node scripts/publish-gas.mjs # build + push + version (+ deploy if GAS_DEPLOYMENT_ID is set)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Define a model
|
|
43
|
+
|
|
44
|
+
Extend `Record` and declare fields as plain TypeScript properties. Use `@Indexed()` for indexed fields,
|
|
45
|
+
`@Required()` for required fields, and `@Field()` for extra options. Undecorated properties are
|
|
46
|
+
auto-discovered as fields.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { Record } from "./src/core/Record";
|
|
50
|
+
import { Decorators } from "./src/core/Decorators";
|
|
51
|
+
|
|
52
|
+
const { Indexed, Required } = Decorators;
|
|
53
|
+
|
|
54
|
+
class Car extends Record {
|
|
55
|
+
@Indexed()
|
|
56
|
+
make: string;
|
|
57
|
+
|
|
58
|
+
@Required()
|
|
59
|
+
model: string;
|
|
60
|
+
|
|
61
|
+
year: number;
|
|
62
|
+
color: string;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- No `tableName` needed in the common case — the sheet name defaults to `tbl_` + class name + `s` (`Car` →
|
|
67
|
+
`tbl_Cars`)
|
|
68
|
+
- When any field is decorated with `@Indexed()`, a combined index sheet is automatically created with the name
|
|
69
|
+
`idx_` + class name + `s` (`Car` → `idx_Cars`). Classes without `@Indexed` fields do **not** get an index
|
|
70
|
+
sheet.
|
|
71
|
+
- `@Indexed()` — marks a field as a secondary index (also implies `@Field`)
|
|
72
|
+
- `@Required()` — marks a field as required using concise decorator syntax
|
|
73
|
+
- `@Field(...)` — adds extra field options like `type`, `defaultValue`, or `referenceTable`
|
|
74
|
+
- Plain properties (`year`, `color`) — auto-discovered as schema fields with type inferred at runtime
|
|
75
|
+
|
|
76
|
+
If you want a custom sheet name, override the static getters:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
class ArchivedCar extends Record {
|
|
80
|
+
static override get tableName() {
|
|
81
|
+
return "ArchivedCars";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static override get indexTableName() {
|
|
85
|
+
return "idx_ArchivedCars";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Available decorators
|
|
91
|
+
|
|
92
|
+
SheetORM supports three TypeScript property decorators on `Record` models.
|
|
93
|
+
|
|
94
|
+
#### `@Required()`
|
|
95
|
+
|
|
96
|
+
Use `@Required()` when a field must be present before `save()` succeeds.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
class Car extends Record {
|
|
100
|
+
@Required()
|
|
101
|
+
model: string;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This decorator is shorthand for `@Field({ required: true })`.
|
|
106
|
+
|
|
107
|
+
#### `@Field(options?)`
|
|
108
|
+
|
|
109
|
+
Use `@Field()` when you want to describe schema metadata explicitly.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
class Car extends Record {
|
|
113
|
+
@Field({ type: "date" })
|
|
114
|
+
purchasedAt: Date;
|
|
115
|
+
|
|
116
|
+
@Field({ type: "reference", referenceTable: "Owners" })
|
|
117
|
+
ownerId: string;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Supported options:
|
|
122
|
+
|
|
123
|
+
| Option | Type | Description |
|
|
124
|
+
| ---------------- | ----------- | ---------------------------------------- |
|
|
125
|
+
| `required` | `boolean` | Rejects save when the field is missing |
|
|
126
|
+
| `type` | `FieldType` | Overrides runtime type inference |
|
|
127
|
+
| `defaultValue` | `unknown` | Value used when the field is empty |
|
|
128
|
+
| `referenceTable` | `string` | Target table name for `reference` fields |
|
|
129
|
+
|
|
130
|
+
#### `@Indexed(options?)`
|
|
131
|
+
|
|
132
|
+
Use `@Indexed()` when the field should have a secondary index for faster lookups.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
class Car extends Record {
|
|
136
|
+
@Indexed()
|
|
137
|
+
make: string;
|
|
138
|
+
|
|
139
|
+
@Indexed({ unique: true })
|
|
140
|
+
vin: string;
|
|
141
|
+
|
|
142
|
+
@Indexed({ type: "date" })
|
|
143
|
+
registeredAt: Date;
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Supported options:
|
|
148
|
+
|
|
149
|
+
| Option | Type | Description |
|
|
150
|
+
| -------- | -------------------------------- | -------------------------------- |
|
|
151
|
+
| `unique` | `boolean` | Enforces uniqueness in the index |
|
|
152
|
+
| `type` | `"string" \| "number" \| "date"` | Controls index value typing |
|
|
153
|
+
|
|
154
|
+
#### Plain properties without annotations
|
|
155
|
+
|
|
156
|
+
Not everything needs a decorator. Plain class properties are still discovered automatically and become normal
|
|
157
|
+
schema fields:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
class Car extends Record {
|
|
161
|
+
make: string;
|
|
162
|
+
model: string;
|
|
163
|
+
year: number;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Use decorators only when you need schema metadata or indexing behaviour.
|
|
168
|
+
|
|
169
|
+
### 4. Use it
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
// Create — table auto-created on first save
|
|
173
|
+
const car = new Car();
|
|
174
|
+
car.make = "Toyota";
|
|
175
|
+
car.model = "Corolla";
|
|
176
|
+
car.year = 2024;
|
|
177
|
+
car.color = "blue";
|
|
178
|
+
car.save();
|
|
179
|
+
|
|
180
|
+
// Or use the static factory
|
|
181
|
+
const civic = Car.create({ make: "Honda", model: "Civic", year: 2023, color: "white" });
|
|
182
|
+
civic.save();
|
|
183
|
+
|
|
184
|
+
// Fluent set + save (chainable)
|
|
185
|
+
new Car().set("make", "Honda").set("model", "Civic").set("year", 2023).save();
|
|
186
|
+
|
|
187
|
+
// Static queries — return typed Car[]
|
|
188
|
+
const toyotas = Car.where("make", "=", "Toyota").execute();
|
|
189
|
+
const found = Car.findById(car.__id);
|
|
190
|
+
const all = Car.find();
|
|
191
|
+
|
|
192
|
+
// Query.from() — class ref (typed) or string
|
|
193
|
+
const recent = Query.from(Car).where("year", ">=", 2023).orderBy("year", "desc").limit(10).execute();
|
|
194
|
+
|
|
195
|
+
// Update
|
|
196
|
+
car.color = "red";
|
|
197
|
+
car.save();
|
|
198
|
+
|
|
199
|
+
// Delete
|
|
200
|
+
car.delete();
|
|
201
|
+
|
|
202
|
+
// Count, pagination, grouping
|
|
203
|
+
Car.count();
|
|
204
|
+
Car.select(0, 10);
|
|
205
|
+
Car.groupBy("make");
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
See [`examples/cars-crud.ts`](examples/cars-crud.ts) for a complete runnable example.
|
|
209
|
+
|
|
210
|
+
## Publishing (Apps Script & npm)
|
|
211
|
+
|
|
212
|
+
### Apps Script library
|
|
213
|
+
|
|
214
|
+
- Prerequisites: `npm run login` (clasp), `appsscript.json` configured for your script, and a Script ID to share.
|
|
215
|
+
- Run `npm run publish:gas` (or VS Code task “Publish: Apps Script library”). It lints, tests, builds, pushes via `clasp push -f`, creates a new version with `GAS_VERSION_MESSAGE` (default: “SheetORM Apps Script release”), and, if `GAS_DEPLOYMENT_ID` is set, deploys with that description.
|
|
216
|
+
- In the Apps Script editor, add the library by Script ID. Use `GasEntrypoints.Record`, `GasEntrypoints.Query`, and `GasEntrypoints.Decorators` in your GAS project; callable menu functions stay limited to `runTests`, `validateTests`, and `runBenchmark`.
|
|
217
|
+
|
|
218
|
+
### npm package
|
|
219
|
+
|
|
220
|
+
- Build distributable JS + type definitions with `npm run build:npm` (emits to `dist/npm`).
|
|
221
|
+
- Vite stays dedicated to the GAS bundle; the npm build is plain TypeScript output so consumers can use their own bundler.
|
|
222
|
+
- Publish with `node scripts/publish-npm.mjs` (or VS Code task “Publish: npm package”); set `NPM_TAG` to publish under a dist-tag if needed. The script runs lint, tests, build, then `npm publish --access public`.
|
|
223
|
+
- Import via the bundled aggregator:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { SheetOrm } from "sheetorm";
|
|
227
|
+
|
|
228
|
+
const { Record, Query, Decorators } = SheetOrm;
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- Or import individual surfaces directly: `import { Record } from "sheetorm/core/Record";`, `import { Query } from "sheetorm/query/Query";`, `import { Decorators } from "sheetorm/core/Decorators";`.
|
|
232
|
+
|
|
233
|
+
### VS Code tasks
|
|
234
|
+
|
|
235
|
+
Two tasks are available in `.vscode/tasks.json`:
|
|
236
|
+
|
|
237
|
+
- `Publish: Apps Script library` → runs `npm run publish:gas`
|
|
238
|
+
- `Publish: npm package` → runs `npm run publish:npm`
|
|
239
|
+
|
|
240
|
+
## Architecture
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
src/
|
|
244
|
+
core/Record.ts — ActiveRecord base class (primary API); tableName = tbl_{Name}s, indexTableName = idx_{Name}s
|
|
245
|
+
core/RecordConstructor.ts — Constructor contract for Record subclasses
|
|
246
|
+
core/RecordStatic.ts — Registry-facing static Record contract
|
|
247
|
+
core/Registry.ts — Global singleton: adapter, repos, class map
|
|
248
|
+
core/SheetRepository.ts — Generic repository: CRUD, batch, hooks, cache
|
|
249
|
+
core/types/ — One exported type / interface / class per file
|
|
250
|
+
Entity.ts — Entity
|
|
251
|
+
FieldType.ts — FieldType
|
|
252
|
+
FieldDefinition.ts — FieldDefinition
|
|
253
|
+
IndexDefinition.ts — IndexDefinition
|
|
254
|
+
TableSchema.ts — TableSchema
|
|
255
|
+
FilterOperator.ts — FilterOperator
|
|
256
|
+
Filter.ts — Filter
|
|
257
|
+
SortClause.ts — SortClause
|
|
258
|
+
QueryOptions.ts — QueryOptions
|
|
259
|
+
PaginatedResult.ts — PaginatedResult
|
|
260
|
+
GroupResult.ts — GroupResult
|
|
261
|
+
LifecycleHooks.ts — LifecycleHooks
|
|
262
|
+
ISheetAdapter.ts — ISheetAdapter
|
|
263
|
+
ISpreadsheetAdapter.ts — ISpreadsheetAdapter
|
|
264
|
+
ICacheProvider.ts — ICacheProvider
|
|
265
|
+
SystemColumns.ts — SystemColumns
|
|
266
|
+
query/Query.ts — Fluent query API + Query.from()
|
|
267
|
+
query/QueryEngine.ts — filter, sort, paginate, group pipeline
|
|
268
|
+
index/IndexMeta.ts — Index metadata contract
|
|
269
|
+
index/IndexStore.ts — Secondary index management
|
|
270
|
+
storage/GoogleSheetAdapter.ts — Native GAS sheet wrapper
|
|
271
|
+
storage/GoogleSpreadsheetAdapter.ts — Native GAS spreadsheet wrapper
|
|
272
|
+
storage/SheetsAPIv4SheetAdapter.ts — Buffered Sheets API v4 sheet wrapper
|
|
273
|
+
storage/SheetsAPIv4SpreadsheetAdapter.ts — Buffered Sheets API v4 spreadsheet wrapper
|
|
274
|
+
core/cache/MemoryCache.ts — In-memory cache implementation
|
|
275
|
+
utils/Uuid.ts — UUID v4 generation (GAS / fallback)
|
|
276
|
+
utils/Serialization.ts — Row ↔ Entity conversion
|
|
277
|
+
testing/ParityCatalog.ts — Canonical Jest ↔ runtime test case list
|
|
278
|
+
testing/RuntimeParity.ts — GAS runtime parity suite
|
|
279
|
+
testing/RuntimeBenchmark.ts — GAS runtime benchmark (Cars + Workers, 1 000 records)
|
|
280
|
+
SheetOrm.ts — npm-facing aggregator for Record/Query/Decorators
|
|
281
|
+
index.ts — `GasEntrypoints` export for GAS globals
|
|
282
|
+
examples/
|
|
283
|
+
cars-crud.ts — Full ActiveRecord example
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Code Organization Rules
|
|
287
|
+
|
|
288
|
+
Repository conventions are intentionally strict:
|
|
289
|
+
|
|
290
|
+
- one exported class / interface / type / enum per file
|
|
291
|
+
- no barrel files and no multi-export "grab bag" modules
|
|
292
|
+
- exported free functions are not allowed; behavior should live on classes as static methods
|
|
293
|
+
- filenames must match the exported artifact name (for example `Record.ts`, `QueryOptions.ts`, `SystemColumns.ts`)
|
|
294
|
+
- internal helpers may stay private to a file, but the file's public surface must stay singular and obvious
|
|
295
|
+
|
|
296
|
+
When adding new files, follow these rules first and treat them as part of the repository contract.
|
|
297
|
+
|
|
298
|
+
## API Reference
|
|
299
|
+
|
|
300
|
+
### Record (ActiveRecord base class)
|
|
301
|
+
|
|
302
|
+
Extend `Record` to define a model. Declare fields as plain class properties — they are auto-discovered. Use
|
|
303
|
+
decorators and an optional static property to customize behavior:
|
|
304
|
+
|
|
305
|
+
| Decorator / property | Description |
|
|
306
|
+
| ----------------------------- | ---------------------------------------------------------------------------- |
|
|
307
|
+
| `@Required()` | Shorthand for marking a field as required |
|
|
308
|
+
| `@Field(options?)` | Explicit field with options (required, type, defaultValue) |
|
|
309
|
+
| `@Indexed(options?)` | Secondary index (implies `@Field`); auto-creates `idx_{ClassName}s` |
|
|
310
|
+
| `static get tableName()` | Sheet name (defaults to `tbl_{ClassName}s` — e.g. `tbl_Cars` for `Car`) |
|
|
311
|
+
| `static get indexTableName()` | Combined index sheet name (defaults to `idx_{ClassName}s` — e.g. `idx_Cars`) |
|
|
312
|
+
|
|
313
|
+
#### `@Required()`
|
|
314
|
+
|
|
315
|
+
Use `@Required()` when a value must be present before saving.
|
|
316
|
+
|
|
317
|
+
| Behavior | Description |
|
|
318
|
+
| ------------------- | -------------------------------------------------------------------------- |
|
|
319
|
+
| Required validation | Rejects `save()` when the field is `undefined`, `null`, or an empty string |
|
|
320
|
+
| Equivalent form | Same as `@Field({ required: true })` |
|
|
321
|
+
|
|
322
|
+
#### `@Field` options
|
|
323
|
+
|
|
324
|
+
| Option | Type | Default | Description |
|
|
325
|
+
| ---------------- | ----------- | ----------- | -------------------------------------------------------------------------- |
|
|
326
|
+
| `required` | `boolean` | `false` | Reject saves when the value is missing |
|
|
327
|
+
| `type` | `FieldType` | auto-infer | Explicit type (`string`, `number`, `boolean`, `date`, `json`, `reference`) |
|
|
328
|
+
| `defaultValue` | `any` | `undefined` | Value used when the field is empty |
|
|
329
|
+
| `referenceTable` | `string` | `undefined` | Target table for `reference` type fields |
|
|
330
|
+
|
|
331
|
+
#### `@Indexed` options
|
|
332
|
+
|
|
333
|
+
| Option | Type | Default | Description |
|
|
334
|
+
| -------- | ----------- | ---------- | ------------------------------- |
|
|
335
|
+
| `unique` | `boolean` | `false` | Enforce uniqueness in the index |
|
|
336
|
+
| `type` | `FieldType` | auto-infer | Index value type |
|
|
337
|
+
|
|
338
|
+
#### Auto-discovered fields
|
|
339
|
+
|
|
340
|
+
Any property declared on a `Record` subclass that is **not** a system column (`__id`, `__createdAt`,
|
|
341
|
+
`__updatedAt`) and is **not** a function is automatically treated as a schema field. Its type is inferred at
|
|
342
|
+
runtime from the value (`typeof`). You only need `@Field()` when you want to set options like `required` or an
|
|
343
|
+
explicit type.
|
|
344
|
+
|
|
345
|
+
> In this repository, examples use direct source imports such as `./src/core/Record`. The GAS bundle itself
|
|
346
|
+
> exposes callable globals through `GasEntrypoints`, not a general-purpose npm package surface.
|
|
347
|
+
|
|
348
|
+
#### Instance methods
|
|
349
|
+
|
|
350
|
+
| Method | Returns | Description |
|
|
351
|
+
| ------------------- | --------- | ----------------------------- |
|
|
352
|
+
| `save()` | `this` | Insert or update (chainable) |
|
|
353
|
+
| `delete()` | `boolean` | Delete from sheet |
|
|
354
|
+
| `set(field, value)` | `this` | Set a field value (chainable) |
|
|
355
|
+
| `get(field)` | `unknown` | Get a field value |
|
|
356
|
+
| `toJSON()` | `object` | Plain object with all fields |
|
|
357
|
+
|
|
358
|
+
#### Static methods
|
|
359
|
+
|
|
360
|
+
| Method | Returns | Description |
|
|
361
|
+
| --------------------------------- | -------------------- | ------------------------------------- |
|
|
362
|
+
| `create(data)` | `T` | Factory: create instance with data |
|
|
363
|
+
| `findById(id)` | `T \| null` | Find by primary key |
|
|
364
|
+
| `find(options?)` | `T[]` | Find all (with optional filters/sort) |
|
|
365
|
+
| `findOne(options?)` | `T \| null` | Find first matching entity |
|
|
366
|
+
| `where(field, op, value)` | `Query<T>` | Start a filtered query chain |
|
|
367
|
+
| `query()` | `Query<T>` | Start an empty query chain |
|
|
368
|
+
| `count(options?)` | `number` | Count matching entities |
|
|
369
|
+
| `deleteAll(options?)` | `number` | Delete matching entities |
|
|
370
|
+
| `select(offset, limit, options?)` | `PaginatedResult<T>` | Paginated query |
|
|
371
|
+
| `groupBy(field, options?)` | `GroupResult<T>[]` | Group by field |
|
|
372
|
+
|
|
373
|
+
### Query\<T\>
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
Car.where("make", "=", "Toyota")
|
|
377
|
+
.and("year", ">=", 2020)
|
|
378
|
+
.or("color", "=", "red")
|
|
379
|
+
.orderBy("year", "desc")
|
|
380
|
+
.limit(20)
|
|
381
|
+
.offset(40)
|
|
382
|
+
.execute(); // T[]
|
|
383
|
+
// .first() // T | null
|
|
384
|
+
// .count() // number
|
|
385
|
+
// .groupBy("field") // GroupResult<T>[]
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Start from any class:
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
Query.from(Car).where("year", ">=", 2023).execute();
|
|
392
|
+
Query.from("Car").where("make", "=", "Toyota").first();
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Filter operators
|
|
396
|
+
|
|
397
|
+
`=`, `!=`, `<`, `>`, `<=`, `>=`, `contains`, `startsWith`, `in`, `search`
|
|
398
|
+
|
|
399
|
+
The `search` operator uses an in-memory **trigram (n-gram) index** built from the combined index sheet
|
|
400
|
+
(`idx_{ClassName}s`). When the target field is `@Indexed`, the query is optimised: candidates are narrowed via
|
|
401
|
+
the n-gram index first, then verified with a substring match. For non-indexed fields `search` falls back to a
|
|
402
|
+
simple case-insensitive `contains`.
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
// Fast n-gram search on @Indexed field — uses idx_Cars
|
|
406
|
+
const hits = Car.where("model", "search", "320i").execute();
|
|
407
|
+
|
|
408
|
+
// Programmatic access via IndexStore
|
|
409
|
+
const ids = indexStore.searchCombined("idx_Cars", "model", "320i");
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Field types
|
|
413
|
+
|
|
414
|
+
`string`, `number`, `boolean`, `json`, `date`, `reference`
|
|
415
|
+
|
|
416
|
+
## Testing
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
npm test
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Runs **128 unit and benchmark tests** across 9 test suites using Jest + ts-jest with in-memory mock adapters:
|
|
423
|
+
|
|
424
|
+
| Suite | Tests | Description |
|
|
425
|
+
| -------------------------- | ----- | -------------------------------------------- |
|
|
426
|
+
| `record.test.ts` | 34 | ActiveRecord API (save, find, query, Query) |
|
|
427
|
+
| `query-engine.test.ts` | 21 | Filter, sort, paginate, group |
|
|
428
|
+
| `serialization.test.ts` | 14 | Row ↔ Entity conversion |
|
|
429
|
+
| `query.test.ts` | 11 | Fluent query API |
|
|
430
|
+
| `index-store.test.ts` | 11 | Secondary index CRUD |
|
|
431
|
+
| `cache.test.ts` | 8 | MemoryCache TTL behavior |
|
|
432
|
+
| `uuid.test.ts` | 2 | UUID generation |
|
|
433
|
+
| `parity-validator.test.ts` | 3 | Jest ↔ GAS runtime parity check |
|
|
434
|
+
| `benchmark.test.ts` | 4 | 1 000-record perf benchmark: Cars vs Workers |
|
|
435
|
+
|
|
436
|
+
### Benchmark Tests (`benchmark.test.ts`)
|
|
437
|
+
|
|
438
|
+
Two benchmark suites exercise the complete Record API against **1 000 records**:
|
|
439
|
+
|
|
440
|
+
| Suite | Class | Table | Index sheet | Notes |
|
|
441
|
+
| ----------------- | -------- | ------------- | ------------- | ------------------------------------ |
|
|
442
|
+
| Cars benchmark | `Car` | `tbl_Cars` | `idx_Cars` | All fields decorated with `@Indexed` |
|
|
443
|
+
| Workers benchmark | `Worker` | `tbl_Workers` | _not created_ | No `@Indexed` fields |
|
|
444
|
+
|
|
445
|
+
Both suites emit progress logs to stdout, covering every Record API operation: `save()`, `count()`,
|
|
446
|
+
`findById()`, `find()`, `findOne()`, `where()`, `query()`, `select()`, `groupBy()`, `update via save()`,
|
|
447
|
+
`set()/get()`, `delete()`, `deleteAll()`, `Query.from()`, `toJSON()`.
|
|
448
|
+
|
|
449
|
+
A timing comparison is printed at the end:
|
|
450
|
+
|
|
451
|
+
```
|
|
452
|
+
════════════════════════════════════════
|
|
453
|
+
BENCHMARK SUMMARY
|
|
454
|
+
════════════════════════════════════════
|
|
455
|
+
tbl_Cars (with @Indexed): 311 ms
|
|
456
|
+
tbl_Workers (no @Indexed): 28 ms
|
|
457
|
+
Difference: 283 ms
|
|
458
|
+
Faster suite: tbl_Workers (by 283 ms)
|
|
459
|
+
Note: in mock environment @Indexed adds write overhead (index sheet writes).
|
|
460
|
+
In real Google Sheets, @Indexed enables faster lookups (fewer API reads).
|
|
461
|
+
════════════════════════════════════════
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
> **Note**: In the in-memory mock environment `@Indexed` adds write overhead (each indexed field value is
|
|
465
|
+
> written to the `idx_Cars` sheet). In real Google Sheets, `@Indexed` trades additional write cost for faster
|
|
466
|
+
> read lookups — beneficial when reading large datasets frequently.
|
|
467
|
+
|
|
468
|
+
### Jest ↔ GAS Runtime Parity (1:1)
|
|
469
|
+
|
|
470
|
+
Every Jest test has a matching handler in the GAS runtime parity suite. This ensures the library works
|
|
471
|
+
identically with real Google Sheets:
|
|
472
|
+
|
|
473
|
+
- `src/testing/ParityCatalog.ts` — canonical list of all Jest test cases
|
|
474
|
+
- `src/testing/RuntimeParity.ts` — runtime suite executing against real Sheets API
|
|
475
|
+
- `tests/parity-validator.test.ts` — fails when Jest and runtime cases diverge
|
|
476
|
+
|
|
477
|
+
Run locally (mock adapters):
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
npm test
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Run in Google Apps Script (real Sheets API):
|
|
484
|
+
|
|
485
|
+
- `runTests()` — executes full runtime parity suite against the active spreadsheet
|
|
486
|
+
- `validateTests()` — validates mapping only (fast drift check)
|
|
487
|
+
|
|
488
|
+
### GAS Runtime Benchmark
|
|
489
|
+
|
|
490
|
+
A runtime benchmark mirrors `tests/benchmark.test.ts` and runs against the real Sheets API:
|
|
491
|
+
|
|
492
|
+
- `src/testing/RuntimeBenchmark.ts` — benchmark runner for Cars + Workers (100 records each)
|
|
493
|
+
|
|
494
|
+
Run in Google Apps Script (real Sheets API):
|
|
495
|
+
|
|
496
|
+
- `runBenchmark()` — executes full Cars + Workers benchmark and logs a timing summary
|
|
497
|
+
|
|
498
|
+
## Exposed GAS Functions
|
|
499
|
+
|
|
500
|
+
Only three callable functions are surfaced as GAS globals (visible in the Apps Script editor Run menu and
|
|
501
|
+
callable as triggers). Everything else is an internal implementation detail bundled into `Code.js`.
|
|
502
|
+
|
|
503
|
+
| Function | Signature | Purpose | When to use |
|
|
504
|
+
| --------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
505
|
+
| `runTests` | `runTests(): void` | Executes the full runtime parity suite (all ~146 test cases) against the active Google Spreadsheet. Logs per-test PASS/FAIL to `Logger` and cleans up all temporary sheets after the run. | After deploying a new version of SheetORM to GAS to verify the real Sheets API behaves identically to the Jest mock suite. |
|
|
506
|
+
| `validateTests` | `validateTests(): void` | Checks that every Jest test case has a matching GAS handler (and vice versa). Throws immediately if the two catalogs have drifted — no Sheets API calls made. | Quick sanity check during development; runs in milliseconds without touching any spreadsheet. |
|
|
507
|
+
| `runBenchmark` | `runBenchmark(): void` | Runs a write/read/query/delete cycle for Cars (with `@Indexed`) and Workers (without `@Indexed`) using 100 records each. Logs per-operation timings and a comparison summary to `Logger`. | Measuring real-world Sheets API performance and understanding the latency trade-off of `@Indexed` versus plain tables. |
|
|
508
|
+
|
|
509
|
+
## CI
|
|
510
|
+
|
|
511
|
+
GitHub Actions workflow at `.github/workflows/ci.yml`:
|
|
512
|
+
|
|
513
|
+
1. TypeScript type-check (`tsc --noEmit`)
|
|
514
|
+
2. Unit tests (`npm test`)
|
|
515
|
+
3. Production build (`npm run build`)
|
|
516
|
+
4. Verify `Code.js` output exists
|
|
517
|
+
|
|
518
|
+
Matrix: Node 18, 20, 22.
|
|
519
|
+
|
|
520
|
+
## Available Scripts
|
|
521
|
+
|
|
522
|
+
| Script | Description |
|
|
523
|
+
| ---------------- | -------------------------------------------- |
|
|
524
|
+
| `npm run build` | Clean + compile TypeScript + bundle via Vite |
|
|
525
|
+
| `npm test` | Run all Jest tests |
|
|
526
|
+
| `npm run lint` | Lint with ESLint |
|
|
527
|
+
| `npm run format` | Format with Prettier |
|
|
528
|
+
| `npm run login` | Authenticate with Google Apps Script (once) |
|
|
529
|
+
| `npm run push` | Build + push to GAS |
|
|
530
|
+
| `npm run deploy` | Build + push + create versioned deployment |
|
|
531
|
+
|
|
532
|
+
## Sheet Layout
|
|
533
|
+
|
|
534
|
+
Each registered table occupies one sheet. Row 1 contains headers: `__id`, `__createdAt`, `__updatedAt`,
|
|
535
|
+
followed by schema-defined fields. Data starts at row 2.
|
|
536
|
+
|
|
537
|
+
Special sheets:
|
|
538
|
+
|
|
539
|
+
- `tbl_{ClassName}s` — Data sheet (e.g. `tbl_Cars` for class `Car`)
|
|
540
|
+
- `idx_{ClassName}s` — Combined secondary index sheet (e.g. `idx_Cars`). Created automatically when the class
|
|
541
|
+
has at least one `@Indexed` field. Columns: `[field, value, entityId]` — each row maps one indexed field
|
|
542
|
+
value to its owning record. Classes with no `@Indexed` fields do **not** get an index sheet.
|
|
543
|
+
|
|
544
|
+
## Storage Adapters
|
|
545
|
+
|
|
546
|
+
SheetORM separates sheet I/O behind two interfaces defined in `src/core/types/ISheetAdapter.ts` and `src/core/types/ISpreadsheetAdapter.ts`:
|
|
547
|
+
|
|
548
|
+
- **`ISpreadsheetAdapter`** — spreadsheet-level operations: `getSheetByName`, `createSheet`, `deleteSheet`, `getSheetNames`
|
|
549
|
+
- **`ISheetAdapter`** — sheet-level operations: read (`getAllData`, `getHeaders`, `getRow`, …), write (`appendRows`, `writeRowsAt`, `updateRow`, …), `clear`, `flush`
|
|
550
|
+
|
|
551
|
+
Two production adapters are provided. Both implement the full interface; they differ only in how write calls reach Google Sheets.
|
|
552
|
+
|
|
553
|
+
### `GoogleSpreadsheetAdapter` / `GoogleSheetAdapter`
|
|
554
|
+
|
|
555
|
+
**Location**: `src/storage/GoogleSpreadsheetAdapter.ts`, `src/storage/GoogleSheetAdapter.ts`
|
|
556
|
+
|
|
557
|
+
Uses the native GAS `SpreadsheetApp` / `Sheet` objects directly. This is the default adapter — no configuration required.
|
|
558
|
+
|
|
559
|
+
```ts
|
|
560
|
+
import { GoogleSpreadsheetAdapter } from "./src/storage/GoogleSpreadsheetAdapter";
|
|
561
|
+
import { Registry } from "./src/core/Registry";
|
|
562
|
+
|
|
563
|
+
Registry.getInstance().configure({
|
|
564
|
+
adapter: new GoogleSpreadsheetAdapter(), // uses active spreadsheet
|
|
565
|
+
});
|
|
566
|
+
// … or pass an explicit spreadsheet:
|
|
567
|
+
Registry.getInstance().configure({
|
|
568
|
+
adapter: new GoogleSpreadsheetAdapter(SpreadsheetApp.openById("…")),
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
| Property | Value |
|
|
573
|
+
| -------------------- | ------------------------------------------------------------------ |
|
|
574
|
+
| Write mechanism | `Range.setValues()` — GAS native API |
|
|
575
|
+
| Calls per `saveAll` | **2** — one `setValues` for entity sheet + one for index sheet |
|
|
576
|
+
| Flush required | No — writes are synchronous and immediately visible |
|
|
577
|
+
| Quota impact | Counts against SpreadsheetApp call budget (no daily UrlFetch quota)|
|
|
578
|
+
| Read operations | Native `Range.getValues()` |
|
|
579
|
+
| GAS V8 latency | ~300–500 ms per `setValues` call on large ranges |
|
|
580
|
+
| Best for | All production use; default choice |
|
|
581
|
+
|
|
582
|
+
### `SheetsAPIv4SpreadsheetAdapter` / `SheetsAPIv4SheetAdapter`
|
|
583
|
+
|
|
584
|
+
**Location**: `src/storage/SheetsAPIv4SpreadsheetAdapter.ts`, `src/storage/SheetsAPIv4SheetAdapter.ts`
|
|
585
|
+
|
|
586
|
+
Buffers all write operations (`writeRowsAt`, `appendRows`) in memory as A1-notation `ValueRange` objects and sends them all in a **single HTTP `spreadsheets.values.batchUpdate` request** when `flushAllPending()` is called. Read operations still use the native GAS API (the ORM reads from in-memory cache after `saveAll`, so extra sheet reads are rare).
|
|
587
|
+
|
|
588
|
+
```ts
|
|
589
|
+
import { SheetsAPIv4SpreadsheetAdapter } from "./src/storage/SheetsAPIv4SpreadsheetAdapter";
|
|
590
|
+
import { Registry } from "./src/core/Registry";
|
|
591
|
+
|
|
592
|
+
const v4 = new SheetsAPIv4SpreadsheetAdapter(); // uses active spreadsheet
|
|
593
|
+
Registry.getInstance().configure({ adapter: v4 });
|
|
594
|
+
|
|
595
|
+
MyRecord.saveAll(items); // buffers entity rows + index rows in memory
|
|
596
|
+
v4.flushAllPending(); // ONE HTTP batchUpdate — both sheets at once
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
| Property | Value |
|
|
600
|
+
| -------------------- | -------------------------------------------------------------------------- |
|
|
601
|
+
| Write mechanism | `UrlFetchApp.fetch()` → Sheets REST API v4 `values:batchUpdate` |
|
|
602
|
+
| Calls per `saveAll` | **1** — entity sheet + index sheet rows combined in one HTTP request |
|
|
603
|
+
| Flush required | **Yes** — call `v4.flushAllPending()` after each `saveAll` (or batch) |
|
|
604
|
+
| Auth | `ScriptApp.getOAuthToken()` — reuses the existing `spreadsheets` scope |
|
|
605
|
+
| Quota impact | `UrlFetchApp` quota: 20 000 calls/day per script |
|
|
606
|
+
| Read operations | Native `Range.getValues()` — unchanged |
|
|
607
|
+
| GAS V8 latency | 1 HTTP round-trip instead of N `setValues()` calls |
|
|
608
|
+
| Best for | Benchmarking / high-volume batch imports where minimising API calls matters |
|
|
609
|
+
|
|
610
|
+
**Required OAuth scopes** (already set in `appsscript.json`):
|
|
611
|
+
|
|
612
|
+
```json
|
|
613
|
+
"oauthScopes": [
|
|
614
|
+
"https://www.googleapis.com/auth/spreadsheets",
|
|
615
|
+
"https://www.googleapis.com/auth/script.external_request"
|
|
616
|
+
]
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### Adapter comparison
|
|
620
|
+
|
|
621
|
+
| Criterion | `GoogleSpreadsheetAdapter` | `SheetsAPIv4SpreadsheetAdapter` |
|
|
622
|
+
| --------------------------- | :------------------------: | :-----------------------------: |
|
|
623
|
+
| Setup complexity | None | Requires `flushAllPending()` call |
|
|
624
|
+
| Write batching | 2 calls per `saveAll` | 1 HTTP call per flush |
|
|
625
|
+
| Flush step needed | No | Yes |
|
|
626
|
+
| UrlFetch daily quota used | No | Yes (1 per flush) |
|
|
627
|
+
| Works without internet auth | Yes (native GAS) | No (needs `UrlFetchApp`) |
|
|
628
|
+
| In-band error visibility | Immediate on write | Deferred to flush |
|
|
629
|
+
| Recommended default | ✅ Yes | Only for bulk-write optimisation |
|
|
630
|
+
|
|
631
|
+
The `GoogleSpreadsheetAdapter` is the default and recommended choice for all production use. The `SheetsAPIv4SpreadsheetAdapter` is provided as an opt-in for scenarios where reducing the number of GAS API calls is the primary concern (e.g. large batch imports exceeding GAS execution-time limits).
|
|
632
|
+
|
|
633
|
+
## Development Notes
|
|
634
|
+
|
|
635
|
+
### Exposing Functions to GAS
|
|
636
|
+
|
|
637
|
+
`src/index.ts` now exports a single `GasEntrypoints` class. During bundling, the Vite GAS plugin maps its
|
|
638
|
+
static methods to GAS globals (`runTests`, `validateTests`, `runBenchmark`) and also emits matching stub
|
|
639
|
+
functions so that Apps Script still shows them in the Run menu.
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
export class GasEntrypoints {
|
|
643
|
+
static runTests(): void {
|
|
644
|
+
RuntimeParity.run();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
static validateTests(): void {
|
|
648
|
+
RuntimeParity.validate();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
static runBenchmark(): void {
|
|
652
|
+
RuntimeBenchmark.run();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
This keeps the source aligned with the repository rule of one public export per file while preserving the same
|
|
658
|
+
three callable GAS entrypoints.
|
|
659
|
+
|
|
660
|
+
### Circular Dependencies
|
|
661
|
+
|
|
662
|
+
Circular dependencies (files that depend on each other in a circular manner) can cause unexpected issues like
|
|
663
|
+
"X is not a function" or "X is not defined". If you are seeing these errors in your project and you know they
|
|
664
|
+
are wrong, try checking for circular dependencies using [`madge`](https://github.com/pahen/madge) (not
|
|
665
|
+
included in this template):
|
|
666
|
+
|
|
667
|
+
1. Install `madge` globally with `npm i --global madge`.
|
|
668
|
+
2. Check for circular dependencies with `madge src/index.ts --circular`.
|
|
669
|
+
|
|
670
|
+
## License
|
|
671
|
+
|
|
672
|
+
GPL-2.0 — see [license.md](license.md).
|