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 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
@@ -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"}