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.
- package/CHANGELOG.md +93 -1
- package/LICENSE +1 -1
- package/README.md +167 -6
- package/dist/adapter/crud.d.ts +5 -3
- package/dist/adapter/crud.d.ts.map +1 -1
- package/dist/adapter/crud.js +41 -4
- package/dist/adapter/crud.js.map +1 -1
- package/dist/adapter/sheetAdapter.d.ts +2 -0
- package/dist/adapter/sheetAdapter.d.ts.map +1 -1
- package/dist/adapter/sheetAdapter.js +54 -1
- package/dist/adapter/sheetAdapter.js.map +1 -1
- package/dist/cli/commands/export.d.ts +8 -0
- package/dist/cli/commands/export.d.ts.map +1 -0
- package/dist/cli/commands/export.js +165 -0
- package/dist/cli/commands/export.js.map +1 -0
- package/dist/cli/commands/init.d.ts +3 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +27 -5
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/mock-users.d.ts +3 -0
- package/dist/cli/commands/mock-users.d.ts.map +1 -0
- package/dist/cli/commands/mock-users.js +101 -0
- package/dist/cli/commands/mock-users.js.map +1 -0
- package/dist/cli/commands/seed.d.ts +1 -1
- package/dist/cli/commands/seed.d.ts.map +1 -1
- package/dist/cli/commands/seed.js +42 -1
- package/dist/cli/commands/seed.js.map +1 -1
- package/dist/cli/commands/sync.d.ts +3 -1
- package/dist/cli/commands/sync.d.ts.map +1 -1
- package/dist/cli/commands/sync.js +43 -2
- package/dist/cli/commands/sync.js.map +1 -1
- package/dist/cli/index.js +17 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/schema/columnBuilder.d.ts.map +1 -1
- package/dist/schema/columnBuilder.js +1 -0
- package/dist/schema/columnBuilder.js.map +1 -1
- package/dist/schema/defineTable.d.ts.map +1 -1
- package/dist/schema/defineTable.js +8 -0
- package/dist/schema/defineTable.js.map +1 -1
- package/dist/schema/types.d.ts +6 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/package.json +7 -3
- package/skills/auth/SKILL.md +142 -0
- package/skills/cli/SKILL.md +150 -0
- package/skills/core/SKILL.md +115 -0
- package/skills/crud/SKILL.md +185 -0
- 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.
|