genjutsu-db 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/LICENSE +21 -0
- package/README.md +360 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +370 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.d.ts +11 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +163 -0
- package/dist/migrations.js.map +1 -0
- package/dist/model.d.ts +27 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +138 -0
- package/dist/model.js.map +1 -0
- package/dist/relations.d.ts +17 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +114 -0
- package/dist/relations.js.map +1 -0
- package/dist/transport.d.ts +32 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +232 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +57 -0
- package/dist/utils.js.map +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ayaz Uddin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# genjutsu-db
|
|
2
|
+
|
|
3
|
+
A TypeScript-first Google Sheets database library with zero runtime dependencies.
|
|
4
|
+
|
|
5
|
+
Use Google Sheets as a structured database with typed models, full CRUD, relations, migrations, and formatting — all from the browser or server with just `fetch`.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **`defineModel()` API** — Drizzle-style schema definitions with chainable field builders
|
|
10
|
+
- **Full CRUD** — `create`, `findById`, `findMany`, `update`, `delete`, `readAll`, `writeAll`, `append`
|
|
11
|
+
- **Relations** — Foreign key validation on write + eager loading via `include`
|
|
12
|
+
- **Migrations** — Versioned schema changes tracked in a `_genjutsu_migrations` sheet
|
|
13
|
+
- **Formatting** — Header styles, number formats, alignment rules
|
|
14
|
+
- **Token provider** — Supports static tokens or async refresh functions with automatic 401 retry
|
|
15
|
+
- **Write mutex** — Serializes concurrent writes to prevent interleaved API calls
|
|
16
|
+
- **Read-only mode** — Use `apiKey` for public sheets (writes are blocked at the client level)
|
|
17
|
+
- **Zero dependencies** — Pure `fetch`-based, works in any JS runtime (browser, Node, Bun, Deno)
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun add genjutsu-db
|
|
23
|
+
# or
|
|
24
|
+
npm install genjutsu-db
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { createClient, createSpreadsheet, defineModel, field, generateId } from "genjutsu-db";
|
|
31
|
+
|
|
32
|
+
// 1. Define models
|
|
33
|
+
const Contact = defineModel("Contacts", {
|
|
34
|
+
id: field.string().primaryKey(),
|
|
35
|
+
name: field.string(),
|
|
36
|
+
email: field.string().optional(),
|
|
37
|
+
age: field.number().optional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const Note = defineModel("Notes", {
|
|
41
|
+
id: field.string().primaryKey(),
|
|
42
|
+
contactId: field.string().references("contacts", "id"),
|
|
43
|
+
text: field.string(),
|
|
44
|
+
createdAt: field.date(),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 2. Create a spreadsheet (or use an existing one)
|
|
48
|
+
const { spreadsheetId } = await createSpreadsheet("My App", oauthToken);
|
|
49
|
+
|
|
50
|
+
// 3. Create client
|
|
51
|
+
const db = createClient({
|
|
52
|
+
spreadsheetId,
|
|
53
|
+
auth: oauthToken,
|
|
54
|
+
schemas: { contacts: Contact, notes: Note },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 4. Ensure sheet tabs exist
|
|
58
|
+
await db.ensureSchema();
|
|
59
|
+
|
|
60
|
+
// 5. CRUD
|
|
61
|
+
const alice = await db.repo("contacts").create({
|
|
62
|
+
id: generateId(),
|
|
63
|
+
name: "Alice",
|
|
64
|
+
email: "alice@example.com",
|
|
65
|
+
age: 30,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const found = await db.repo("contacts").findById(alice.id);
|
|
69
|
+
const adults = await db.repo("contacts").findMany((c) => (c.age ?? 0) >= 18);
|
|
70
|
+
const updated = await db.repo("contacts").update(alice.id, { age: 31 });
|
|
71
|
+
await db.repo("contacts").delete(alice.id);
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
### `defineModel(sheetName, fields)`
|
|
77
|
+
|
|
78
|
+
Define a typed model that maps to a Google Sheets tab.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const Task = defineModel("Tasks", {
|
|
82
|
+
id: field.string().primaryKey(),
|
|
83
|
+
title: field.string(),
|
|
84
|
+
done: field.boolean().default(false),
|
|
85
|
+
priority: field.number().optional(),
|
|
86
|
+
dueDate: field.date().optional(),
|
|
87
|
+
assigneeId: field.string().references("users", "id"),
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Field types:** `field.string()`, `field.number()`, `field.date()`, `field.boolean()`
|
|
92
|
+
|
|
93
|
+
**Field modifiers:**
|
|
94
|
+
| Method | Description |
|
|
95
|
+
|--------|-------------|
|
|
96
|
+
| `.primaryKey()` | Mark as primary key (exactly one required per model) |
|
|
97
|
+
| `.optional()` | Allow `null` values |
|
|
98
|
+
| `.default(value)` | Set a default value for missing cells |
|
|
99
|
+
| `.references(model, field)` | Declare a foreign key relation |
|
|
100
|
+
|
|
101
|
+
### `createClient(config)`
|
|
102
|
+
|
|
103
|
+
Create a client with typed repositories.
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const db = createClient({
|
|
107
|
+
spreadsheetId: "1BxiMVs0XRA...",
|
|
108
|
+
auth: token, // string or async () => Promise<string>
|
|
109
|
+
schemas: { tasks: Task, users: User },
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Option | Type | Description |
|
|
114
|
+
|--------|------|-------------|
|
|
115
|
+
| `spreadsheetId` | `string` | Google Sheets spreadsheet ID or full URL |
|
|
116
|
+
| `auth` | `string \| () => Promise<string>` | OAuth token or async token provider |
|
|
117
|
+
| `apiKey` | `string` | API key for read-only access to public sheets |
|
|
118
|
+
| `schemas` | `Record<string, SheetSchema>` | Named schemas (keys become repo names) |
|
|
119
|
+
|
|
120
|
+
> Provide `auth` for read/write or `apiKey` for read-only. At least one is required.
|
|
121
|
+
|
|
122
|
+
### Repository Methods
|
|
123
|
+
|
|
124
|
+
Access via `db.repo("modelName")`:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const repo = db.repo("tasks");
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| Method | Returns | Description |
|
|
131
|
+
|--------|---------|-------------|
|
|
132
|
+
| `create(record, options?)` | `Promise<T>` | Insert a new record (validates PK uniqueness + FK refs) |
|
|
133
|
+
| `findById(id)` | `Promise<T \| null>` | Find a single record by primary key |
|
|
134
|
+
| `findMany(filter?, options?)` | `Promise<T[]>` | Find records, optionally filtered |
|
|
135
|
+
| `update(id, changes, options?)` | `Promise<T>` | Partial update by primary key |
|
|
136
|
+
| `delete(id)` | `Promise<void>` | Delete by primary key |
|
|
137
|
+
| `readAll(options?)` | `Promise<T[]>` | Read all records |
|
|
138
|
+
| `writeAll(records)` | `Promise<void>` | Overwrite all records (clear + write) |
|
|
139
|
+
| `append(records)` | `Promise<void>` | Append records to the sheet |
|
|
140
|
+
|
|
141
|
+
### Relations & Eager Loading
|
|
142
|
+
|
|
143
|
+
Foreign keys are validated on `create()` and `update()` — the referenced record must exist.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Eager load related records
|
|
147
|
+
const contacts = await db.repo("contacts").findMany(undefined, {
|
|
148
|
+
include: { notes: true },
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Each contact now has a `notes` array attached
|
|
152
|
+
for (const contact of contacts) {
|
|
153
|
+
console.log(contact.name, contact.notes.length);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Skip FK validation when needed:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
await db.repo("notes").create(record, { skipFkValidation: true });
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Batch Sync
|
|
164
|
+
|
|
165
|
+
Overwrite multiple sheets atomically in a single API call:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
await db.batchSync({
|
|
169
|
+
tasks: allTasks,
|
|
170
|
+
users: allUsers,
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Schema Management
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// Create missing sheet tabs
|
|
178
|
+
await db.ensureSchema();
|
|
179
|
+
|
|
180
|
+
// Apply header and cell formatting
|
|
181
|
+
await db.applyFormatting();
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Migrations
|
|
185
|
+
|
|
186
|
+
Versioned schema changes with structural operations:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
await db.migrate([
|
|
190
|
+
{
|
|
191
|
+
version: 1,
|
|
192
|
+
name: "add-status-column",
|
|
193
|
+
up: async (ctx) => {
|
|
194
|
+
await ctx.addColumn("Tasks", "status", 3);
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
version: 2,
|
|
199
|
+
name: "create-tags-sheet",
|
|
200
|
+
up: async (ctx) => {
|
|
201
|
+
await ctx.createSheet("Tags");
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
]);
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Migration context operations:**
|
|
208
|
+
|
|
209
|
+
| Method | Description |
|
|
210
|
+
|--------|-------------|
|
|
211
|
+
| `createSheet(name)` | Add a new sheet tab |
|
|
212
|
+
| `addColumn(sheet, name, afterIndex?)` | Insert a column |
|
|
213
|
+
| `removeColumn(sheet, columnIndex)` | Delete a column |
|
|
214
|
+
| `renameColumn(sheet, columnIndex, newName)` | Rename a column header |
|
|
215
|
+
| `renameSheet(oldName, newName)` | Rename a sheet tab |
|
|
216
|
+
|
|
217
|
+
Applied migrations are tracked in `_genjutsu_migrations` (auto-created). Already-applied migrations are skipped on subsequent runs.
|
|
218
|
+
|
|
219
|
+
### Error Handling
|
|
220
|
+
|
|
221
|
+
All errors are typed `GenjutsuError` instances with a `kind` discriminator:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { isGenjutsuError } from "genjutsu-db";
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
await db.repo("tasks").create(record);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (isGenjutsuError(err)) {
|
|
230
|
+
switch (err.kind) {
|
|
231
|
+
case "AUTH_ERROR": // 401 — token expired or invalid
|
|
232
|
+
case "PERMISSION_ERROR": // 403 — no access to spreadsheet
|
|
233
|
+
case "RATE_LIMIT": // 429 — err.retryAfterMs available
|
|
234
|
+
case "NETWORK_ERROR": // fetch failed
|
|
235
|
+
case "VALIDATION_ERROR": // FK check failed, duplicate PK, etc.
|
|
236
|
+
case "SCHEMA_ERROR": // bad config, missing sheet
|
|
237
|
+
case "MIGRATION_ERROR": // err.migrationVersion, err.migrationName
|
|
238
|
+
case "API_ERROR": // other Google Sheets API errors
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Token Provider Pattern
|
|
245
|
+
|
|
246
|
+
For applications that need token refresh:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
const db = createClient({
|
|
250
|
+
spreadsheetId: "...",
|
|
251
|
+
auth: async () => {
|
|
252
|
+
// Return a fresh token — called on each request
|
|
253
|
+
// On 401, the library retries once with a new token
|
|
254
|
+
return await refreshOAuthToken();
|
|
255
|
+
},
|
|
256
|
+
schemas: { tasks: Task },
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Utilities
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import {
|
|
264
|
+
generateId, // Generate a random ID string
|
|
265
|
+
extractSpreadsheetId, // Parse spreadsheet ID from URL
|
|
266
|
+
createSpreadsheet, // Create a new Google Sheet
|
|
267
|
+
} from "genjutsu-db";
|
|
268
|
+
|
|
269
|
+
const id = generateId();
|
|
270
|
+
const sheetId = extractSpreadsheetId("https://docs.google.com/spreadsheets/d/abc123/edit");
|
|
271
|
+
const { spreadsheetId, spreadsheetUrl } = await createSpreadsheet("Title", token);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Raw Schemas
|
|
275
|
+
|
|
276
|
+
For full control, skip `defineModel()` and provide a raw `SheetSchema<T>`:
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { createClient, type SheetSchema } from "genjutsu-db";
|
|
280
|
+
|
|
281
|
+
interface Task {
|
|
282
|
+
id: string;
|
|
283
|
+
title: string;
|
|
284
|
+
done: boolean;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const TaskSchema: SheetSchema<Task> = {
|
|
288
|
+
sheetName: "Tasks",
|
|
289
|
+
headers: ["id", "title", "done"],
|
|
290
|
+
readRange: "Tasks!A2:C",
|
|
291
|
+
writeRange: "Tasks!A1:C",
|
|
292
|
+
clearRange: "Tasks!A2:C",
|
|
293
|
+
primaryKey: "id",
|
|
294
|
+
parseRow: (row) => {
|
|
295
|
+
if (!row[0]) return null;
|
|
296
|
+
return {
|
|
297
|
+
id: String(row[0]),
|
|
298
|
+
title: String(row[1] ?? ""),
|
|
299
|
+
done: row[2] === true || row[2] === "TRUE",
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
toRow: (t) => [t.id, t.title, t.done],
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const db = createClient({
|
|
306
|
+
spreadsheetId: "...",
|
|
307
|
+
auth: token,
|
|
308
|
+
schemas: { tasks: TaskSchema },
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Authentication
|
|
313
|
+
|
|
314
|
+
genjutsu-db requires a Google OAuth2 token with the `https://www.googleapis.com/auth/spreadsheets` scope. The library does not handle OAuth flows — you provide the token.
|
|
315
|
+
|
|
316
|
+
**Common approaches:**
|
|
317
|
+
- **Browser apps** — Use Google Identity Services (GIS) or `@react-oauth/google`
|
|
318
|
+
- **Server apps** — Use a service account or OAuth2 client credentials
|
|
319
|
+
- **Quick testing** — Use the [OAuth Playground](https://developers.google.com/oauthplayground/) to get a temporary token
|
|
320
|
+
|
|
321
|
+
## Running the Demo
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
# Get a token from https://developers.google.com/oauthplayground/
|
|
325
|
+
# Select "Google Sheets API v4" scope
|
|
326
|
+
|
|
327
|
+
GOOGLE_TOKEN="your-token" bun run demo.ts
|
|
328
|
+
|
|
329
|
+
# Or use an existing spreadsheet:
|
|
330
|
+
GOOGLE_TOKEN="your-token" SHEET_ID="spreadsheet-id" bun run demo.ts
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Development
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
bun install # Install dependencies
|
|
337
|
+
bun test # Run tests (264 tests)
|
|
338
|
+
bun test --coverage # Coverage report (99.7% lines, 100% functions)
|
|
339
|
+
bun run build # TypeScript build to dist/
|
|
340
|
+
bun run lint # Type check
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Architecture
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
src/
|
|
347
|
+
client.ts — createClient() factory, repo builder, write mutex
|
|
348
|
+
model.ts — defineModel(), field builders, parseRow/toRow/validate generation
|
|
349
|
+
relations.ts — FK validation (validateForeignKeys) + eager loading (loadRelated)
|
|
350
|
+
migrations.ts — Migration runner + MigrationContext structural operations
|
|
351
|
+
transport.ts — Google Sheets v4 REST wrappers (GET/PUT/POST/batchGet/batchUpdate)
|
|
352
|
+
errors.ts — GenjutsuError class with 8 typed error kinds
|
|
353
|
+
types.ts — All TypeScript interfaces and type definitions
|
|
354
|
+
utils.ts — generateId, date parsing, header validation helpers
|
|
355
|
+
index.ts — Public API barrel export
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## License
|
|
359
|
+
|
|
360
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client factory for the genjutsu-db library.
|
|
3
|
+
* Creates a schema-driven client with typed repositories.
|
|
4
|
+
*/
|
|
5
|
+
import type { SheetSchema, ClientConfig, GenjutsuClient } from "./types";
|
|
6
|
+
export declare function createClient<S extends Record<string, SheetSchema<any>>>(config: ClientConfig<S>): GenjutsuClient<S>;
|
|
7
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,cAAc,EAOf,MAAM,SAAS,CAAC;AAiBjB,wBAAgB,YAAY,CAC1B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,EAC1C,MAAM,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAoa5C"}
|