longcelot-sheet-db 0.1.5 → 0.1.8

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +93 -1
  2. package/LICENSE +1 -1
  3. package/README.md +167 -6
  4. package/dist/adapter/crud.d.ts +5 -3
  5. package/dist/adapter/crud.d.ts.map +1 -1
  6. package/dist/adapter/crud.js +41 -4
  7. package/dist/adapter/crud.js.map +1 -1
  8. package/dist/adapter/sheetAdapter.d.ts +2 -0
  9. package/dist/adapter/sheetAdapter.d.ts.map +1 -1
  10. package/dist/adapter/sheetAdapter.js +54 -1
  11. package/dist/adapter/sheetAdapter.js.map +1 -1
  12. package/dist/cli/commands/export.d.ts +8 -0
  13. package/dist/cli/commands/export.d.ts.map +1 -0
  14. package/dist/cli/commands/export.js +165 -0
  15. package/dist/cli/commands/export.js.map +1 -0
  16. package/dist/cli/commands/init.d.ts +3 -1
  17. package/dist/cli/commands/init.d.ts.map +1 -1
  18. package/dist/cli/commands/init.js +27 -5
  19. package/dist/cli/commands/init.js.map +1 -1
  20. package/dist/cli/commands/mock-users.d.ts +3 -0
  21. package/dist/cli/commands/mock-users.d.ts.map +1 -0
  22. package/dist/cli/commands/mock-users.js +101 -0
  23. package/dist/cli/commands/mock-users.js.map +1 -0
  24. package/dist/cli/commands/seed.d.ts +1 -1
  25. package/dist/cli/commands/seed.d.ts.map +1 -1
  26. package/dist/cli/commands/seed.js +42 -1
  27. package/dist/cli/commands/seed.js.map +1 -1
  28. package/dist/cli/commands/sync.d.ts +3 -1
  29. package/dist/cli/commands/sync.d.ts.map +1 -1
  30. package/dist/cli/commands/sync.js +43 -2
  31. package/dist/cli/commands/sync.js.map +1 -1
  32. package/dist/cli/index.js +17 -1
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/schema/columnBuilder.d.ts.map +1 -1
  35. package/dist/schema/columnBuilder.js +1 -0
  36. package/dist/schema/columnBuilder.js.map +1 -1
  37. package/dist/schema/defineTable.d.ts.map +1 -1
  38. package/dist/schema/defineTable.js +8 -0
  39. package/dist/schema/defineTable.js.map +1 -1
  40. package/dist/schema/types.d.ts +6 -0
  41. package/dist/schema/types.d.ts.map +1 -1
  42. package/package.json +7 -3
  43. package/skills/auth/SKILL.md +142 -0
  44. package/skills/cli/SKILL.md +150 -0
  45. package/skills/core/SKILL.md +115 -0
  46. package/skills/crud/SKILL.md +185 -0
  47. package/skills/schema/SKILL.md +129 -0
@@ -0,0 +1,185 @@
1
+ ---
2
+ name: crud
3
+ description: Perform create, read, update, and delete operations with longcelot-sheet-db. Use when writing data to Google Sheets, querying records with where/orderBy/limit/offset, updating or deleting rows, using withContext() for actor isolation, or understanding how permission checks and sheet routing work.
4
+ license: MIT
5
+ metadata:
6
+ package: longcelot-sheet-db
7
+ version: "0.1.5"
8
+ ---
9
+
10
+ # longcelot-sheet-db — CRUD Operations
11
+
12
+ All data access goes through a context-bound table instance. The pattern is always:
13
+
14
+ ```
15
+ adapter → withContext(userContext) → table(name) → create | findMany | findOne | update | delete
16
+ ```
17
+
18
+ ## withContext() — Required for Every Operation
19
+
20
+ Every operation requires an active context that determines:
21
+ - **Which sheet** to read from/write to (via `actorSheetId`)
22
+ - **Which role** is acting (used for permission checks)
23
+
24
+ ```typescript
25
+ const ctx = adapter.withContext({
26
+ userId: 'user_123', // Your app's user ID
27
+ role: 'user', // Must match schema's actor field
28
+ actorSheetId: 'sheet-id', // The user's Google Sheet ID
29
+ });
30
+ ```
31
+
32
+ For **admin** actors, `actorSheetId` is ignored — the adapter always uses `adminSheetId`:
33
+
34
+ ```typescript
35
+ const adminCtx = adapter.withContext({
36
+ userId: 'admin_001',
37
+ role: 'admin',
38
+ actorSheetId: 'ignored-for-admin',
39
+ });
40
+ ```
41
+
42
+ ## create()
43
+
44
+ ```typescript
45
+ const record = await ctx.table('bookings').create({
46
+ booking_id: 'bk_001',
47
+ service: 'Consultation',
48
+ date: new Date().toISOString(),
49
+ price: 100,
50
+ // status defaults to 'pending' (defined in schema)
51
+ });
52
+ // record._id is auto-generated (nanoid)
53
+ // record._created_at, record._updated_at set if timestamps: true
54
+ ```
55
+
56
+ **What create() does internally:**
57
+ 1. Validates required fields
58
+ 2. Applies column defaults
59
+ 3. Checks unique constraints (throws `Error: Unique constraint violation: column '...' already has value '...'`)
60
+ 4. Generates `_id` (nanoid)
61
+ 5. Sets timestamps if enabled
62
+ 6. Appends a row to the sheet
63
+
64
+ ## findMany()
65
+
66
+ ```typescript
67
+ const bookings = await ctx.table('bookings').findMany({
68
+ where: { status: 'pending' },
69
+ orderBy: 'date',
70
+ order: 'desc', // 'asc' | 'desc'
71
+ limit: 10,
72
+ offset: 0,
73
+ });
74
+ ```
75
+
76
+ All filtering, sorting, and pagination happen **in memory** after fetching all rows. Not suitable for large datasets (performance degrades beyond ~1000 rows).
77
+
78
+ ### FindOptions type
79
+
80
+ ```typescript
81
+ interface FindOptions {
82
+ where?: Partial<Record<string, any>>;
83
+ orderBy?: string;
84
+ order?: 'asc' | 'desc';
85
+ limit?: number;
86
+ offset?: number;
87
+ }
88
+ ```
89
+
90
+ Soft-deleted rows are automatically excluded when `softDelete: true` is set on the schema.
91
+
92
+ ## findOne()
93
+
94
+ Returns the **first** record matching the where clause, or `null` if none found:
95
+
96
+ ```typescript
97
+ const booking = await ctx.table('bookings').findOne({
98
+ where: { booking_id: 'bk_001' },
99
+ });
100
+ ```
101
+
102
+ ## update()
103
+
104
+ Updates **all** rows matching the where clause:
105
+
106
+ ```typescript
107
+ const updated = await ctx.table('bookings').update({
108
+ where: { booking_id: 'bk_001' },
109
+ data: { status: 'confirmed' },
110
+ });
111
+ // Returns array of updated records
112
+ ```
113
+
114
+ **Behavior:**
115
+ - Each matching row is re-validated and written individually
116
+ - `readonly()` columns are silently skipped
117
+ - `_updated_at` is refreshed automatically if `timestamps: true`
118
+ - Unique constraints are re-checked per row (excluding current row's own `_id`)
119
+
120
+ ### UpdateOptions type
121
+
122
+ ```typescript
123
+ interface UpdateOptions {
124
+ where: Partial<Record<string, any>>;
125
+ data: Partial<Record<string, any>>;
126
+ }
127
+ ```
128
+
129
+ ## delete()
130
+
131
+ ```typescript
132
+ await ctx.table('bookings').delete({
133
+ where: { booking_id: 'bk_001' },
134
+ });
135
+ ```
136
+
137
+ **Behavior depends on schema:**
138
+ - **Without `softDelete: true`**: Rows are physically removed (iterates in reverse order to avoid index shift)
139
+ - **With `softDelete: true`**: `_deleted_at` is set to the current timestamp; rows remain in the sheet and are excluded from `findMany`/`findOne` results
140
+
141
+ ### DeleteOptions type
142
+
143
+ ```typescript
144
+ interface DeleteOptions {
145
+ where: Partial<Record<string, any>>;
146
+ }
147
+ ```
148
+
149
+ ## Serialization
150
+
151
+ | TypeScript value | Stored in Sheet as |
152
+ |--|--|
153
+ | `true` / `false` | `"TRUE"` / `"FALSE"` |
154
+ | `{ key: val }` (json column) | `JSON.stringify(...)` |
155
+ | `null` / `undefined` | `""` (empty string) |
156
+ | `Date` / ISO string | Stored as-is |
157
+
158
+ All deserialization is automatic on read.
159
+
160
+ ## SheetAdapter.syncSchema()
161
+
162
+ Creates missing sheet tabs and adds missing column headers. **Never deletes data or removes columns.** Run after defining new schemas:
163
+
164
+ ```typescript
165
+ await adapter.syncSchema(bookingsSchema);
166
+ ```
167
+
168
+ Use the CLI instead where possible: `npx sheet-db sync`.
169
+
170
+ ## Performance Characteristics
171
+
172
+ | Operation | Typical latency |
173
+ |--|--|
174
+ | Read (findMany / findOne) | 200–500ms |
175
+ | Write (create / update / delete) | 300–700ms |
176
+
177
+ All reads load the entire sheet into memory. Suitable for hundreds to low thousands of rows per table.
178
+
179
+ ## Common Mistakes
180
+
181
+ - **Using `table()` without `withContext()`** — Always call `adapter.withContext(...)` first; calling `adapter.table(...)` directly bypasses permission checks.
182
+ - **Forgetting to `await`** — All CRUD methods return Promises; missing `await` silently returns a pending Promise.
183
+ - **`where` clause with no matches** — `update()` and `delete()` simply do nothing if no rows match; they do **not** throw.
184
+ - **`findOne()` returning `null`** — Always guard with a null check before accessing properties.
185
+ - **Large datasets** — All rows are loaded into memory per read. If you expect thousands of rows, consider adding `limit` to every `findMany()` call.
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: schema
3
+ description: Define tables and columns for longcelot-sheet-db using defineTable() and the fluent column builder API. Use when creating or modifying schema files, adding columns, configuring timestamps/soft-delete, or understanding column modifiers like required, unique, enum, default, ref, and index.
4
+ license: MIT
5
+ metadata:
6
+ package: longcelot-sheet-db
7
+ version: "0.1.5"
8
+ ---
9
+
10
+ # longcelot-sheet-db — Schema Definition
11
+
12
+ Schemas are the primary contract between your code and Google Sheets. Each `defineTable()` call produces one sheet (tab) inside a Google Spreadsheet.
13
+
14
+ ## defineTable()
15
+
16
+ ```typescript
17
+ import { defineTable, string, number, boolean, date, json } from 'longcelot-sheet-db';
18
+
19
+ export default defineTable({
20
+ name: 'bookings', // Sheet tab name — must be unique per actor
21
+ actor: 'user', // Which role owns this table ('admin' | your custom actors)
22
+ timestamps: true, // Adds _created_at, _updated_at columns
23
+ softDelete: true, // Adds _deleted_at; delete() sets it instead of removing the row
24
+ columns: {
25
+ booking_id: string().required().unique(),
26
+ service: string().required(),
27
+ date: date().required(),
28
+ status: string().enum(['pending', 'confirmed', 'cancelled']).default('pending'),
29
+ price: number().min(0),
30
+ notes: string(),
31
+ },
32
+ });
33
+ ```
34
+
35
+ ### Auto-generated columns
36
+
37
+ These are always present and must NOT be defined manually:
38
+
39
+ | Column | Always present | Requires option |
40
+ |--|--|--|
41
+ | `_id` | ✅ (nanoid) | — |
42
+ | `_created_at` | ✅ when `timestamps: true` | `timestamps: true` |
43
+ | `_updated_at` | ✅ when `timestamps: true` | `timestamps: true` |
44
+ | `_deleted_at` | ✅ when `softDelete: true` | `softDelete: true` |
45
+
46
+ ## Column Builders
47
+
48
+ Import individual builders from `longcelot-sheet-db`:
49
+
50
+ ```typescript
51
+ import { string, number, boolean, date, json } from 'longcelot-sheet-db';
52
+ ```
53
+
54
+ | Builder | Stored as | Notes |
55
+ |--|--|--|
56
+ | `string()` | Plain text | |
57
+ | `number()` | Numeric text | |
58
+ | `boolean()` | `"TRUE"` / `"FALSE"` | |
59
+ | `date()` | ISO 8601 string | |
60
+ | `json()` | JSON string | Serialized with `JSON.stringify` |
61
+
62
+ ## Column Modifiers (Fluent Chain)
63
+
64
+ All modifiers return `this` — chain them freely:
65
+
66
+ ```typescript
67
+ string().required().unique().min(5).max(200)
68
+ number().min(0).max(100).default(50)
69
+ string().enum(['active', 'inactive']).default('active')
70
+ string().pattern(/^[a-z0-9-]+$/)
71
+ string().ref('users._id') // Foreign key hint (not yet enforced at runtime)
72
+ string().index() // Marks column for future index support
73
+ string().readonly() // Cannot be updated after creation
74
+ string().primary() // Marks as primary key (metadata only)
75
+ ```
76
+
77
+ ### Full modifier reference
78
+
79
+ | Modifier | Applies to | Effect |
80
+ |--|--|--|
81
+ | `.required()` | all | Rejects `null`/`undefined`/`""` |
82
+ | `.unique()` | all | Throws `Error` if value already exists in column |
83
+ | `.default(value)` | all | Applied when field is omitted on `create()` |
84
+ | `.min(n)` | string, number | Min length (string) or min value (number) |
85
+ | `.max(n)` | string, number | Max length (string) or max value (number) |
86
+ | `.enum([...])` | string | Throws if value not in list |
87
+ | `.pattern(regex)` | string | Throws if value doesn't match |
88
+ | `.readonly()` | all | Field skipped during `update()` |
89
+ | `.primary()` | all | Metadata only — no enforcement |
90
+ | `.ref('table.col')` | string | Documents FK intent — not enforced yet |
91
+ | `.index()` | all | Metadata only — index support planned |
92
+
93
+ ## Actor System
94
+
95
+ The `actor` field in `defineTable()` controls which Google Sheet stores the data:
96
+
97
+ - `actor: 'admin'` → data lives in `adminSheetId` (central admin sheet)
98
+ - `actor: 'user'` (or any custom role) → data lives in the user's personal `actorSheetId`
99
+
100
+ ```typescript
101
+ // Admin-owned table: lives in the central admin spreadsheet
102
+ export default defineTable({ name: 'users', actor: 'admin', ... });
103
+
104
+ // User-owned table: lives in each user's personal sheet
105
+ export default defineTable({ name: 'profile', actor: 'user', ... });
106
+ ```
107
+
108
+ ## File Naming Conventions
109
+
110
+ - Schema files: `snake_case` matching the table name
111
+ - e.g., `student_teacher_map.ts` for `name: 'student_teacher_map'`
112
+ - Use `export default` for schema files
113
+ - Organize by actor in `schemas/` directory:
114
+ ```
115
+ schemas/
116
+ ├── admin/
117
+ │ ├── users.ts
118
+ │ └── credentials.ts
119
+ └── user/
120
+ ├── profile.ts
121
+ └── bookings.ts
122
+ ```
123
+
124
+ ## Common Mistakes
125
+
126
+ - **Defining `_id`, `_created_at`, `_updated_at`, or `_deleted_at` manually** — These are auto-generated; duplicating them causes schema errors.
127
+ - **Duplicate table names across actors** — Each `actor` has its own spreadsheet so `name` must be unique **per actor**, not globally.
128
+ - **Using `softDelete: true` then hard-deleting** — With `softDelete` enabled, `table.delete()` sets `_deleted_at` and `findMany()` auto-excludes soft-deleted rows. Use `includeSoftDeleted: true` in find options if you need them.
129
+ - **`actor` mismatch in `withContext()`** — If you call `withContext({ role: 'user' })` but access a table with `actor: 'admin'`, a `PermissionError` is thrown.