longcelot-sheet-db
Version:
Google Sheets-backed staging database adapter for Node.js with schema-first design
238 lines (178 loc) • 9.63 kB
Markdown
name: core
description: Set up and configure longcelot-sheet-db. Use when installing the package, creating a SheetAdapter, providing OAuth credentials and tokens, wiring environment variables, configuring schema mismatch behavior, adding a cross-actor permissions matrix, configuring Drive folder organisation (driveFolder), Shared Drive (sharedDriveId), per-actor token persistence (tokenStore), file upload (storage / DriveStorageAdapter), or connecting the adapter to an existing backend (Express, NestJS, etc.).
license: MIT
metadata:
package: longcelot-sheet-db
version: "0.1.19"
# longcelot-sheet-db — Core Setup
`longcelot-sheet-db` is a schema-first, actor-aware database adapter that uses **Google Sheets as the storage engine**. Designed for MVPs, prototypes, staging environments, and internal tools where zero infrastructure cost is a priority.
## Installation
```bash
npm install longcelot-sheet-db
pnpm add longcelot-sheet-db
yarn add longcelot-sheet-db
bun add longcelot-sheet-db
```
## Required Environment Variables
```env
GOOGLE_CLIENT_ID=your_client_id
GOOGLE_CLIENT_SECRET=your_client_secret
GOOGLE_REDIRECT_URI=http://localhost:3000/auth/callback
ADMIN_SHEET_ID=your_central_admin_google_sheet_id
SUPER_ADMIN_EMAIL=admin@example.com
```
> `GOOGLE_REDIRECT_URI` must exactly match a URI registered in the Google Cloud Console OAuth 2.0 credentials.
## Creating the Adapter
```typescript
import { createSheetAdapter } from 'longcelot-sheet-db';
const adapter = createSheetAdapter({
adminSheetId: process.env.ADMIN_SHEET_ID!,
credentials: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: process.env.GOOGLE_REDIRECT_URI!,
},
tokens: userOAuthTokens, // obtained from OAuthManager.getTokens()
});
```
### Full SheetAdapterConfig type
```typescript
interface SheetAdapterConfig {
adminSheetId: string;
credentials: {
clientId: string;
clientSecret: string;
redirectUri: string;
};
tokens: unknown; // admin Google OAuth2 token object
onSchemaMismatch?: SchemaMismatchBehaviour; // 'warn' | 'error' | 'auto-sync'
permissions?: Record<string, ActorPermission>; // cross-actor access matrix
driveFolder?: DriveFolderConfig; // folder organisation in Drive
sharedDriveId?: string; // target a Google Workspace Shared Drive
tokenStore?: TokenStore; // per-actor OAuth token persistence
storage?: StorageAdapter; // file upload provider
sheetStyle?: SheetStyleConfig; // header color, frozen rows/columns
}
```
For Drive folder organisation, actor-owned sheets, Shared Drive, `TokenStore`, and `StorageAdapter` / `DriveStorageAdapter`, see `skills/drive/SKILL.md`.
> **Actor field naming**: every field that identifies an actor is named `name`/`actor`/`targetActor` — never `role` — because `role` reads as an application RBAC role at the point of writing the code. `lsdb.config.ts` actor entries use `{ name: 'admin', sheetIdEnv: ... }`; `withContext()` uses `actor:`/`targetActor:`. The old `role`/`targetRole` fields still work as deprecated aliases (emit `console.warn`) but should not be used in new code. Modeling RBAC sub-roles as separate actors is the most common mistake this causes — see `skills/permissions/SKILL.md`.
## Registering Schemas
Schemas **must** be registered before calling `adapter.table()`:
```typescript
import bookingsSchema from './schemas/user/bookings';
import usersSchema from './schemas/admin/users';
adapter.registerSchema(bookingsSchema);
// or register many at once:
adapter.registerSchemas([bookingsSchema, usersSchema]);
```
## Schema Mismatch Detection
`onSchemaMismatch` detects when a user's sheet has an outdated schema (e.g., missing new columns). The check runs automatically on `withContext()` for non-admin users.
```typescript
const adapter = createSheetAdapter({
// ...
onSchemaMismatch: 'warn', // log warning and continue (default)
// onSchemaMismatch: 'error', // throw SchemaMismatchError
// onSchemaMismatch: 'auto-sync', // sync the user sheet silently before proceeding
});
```
| Mode | Behaviour |
|---|---|
| `'warn'` | Logs to stderr and continues — safe for production rollouts |
| `'error'` | Throws `SchemaMismatchError` — hard-fail on stale clients |
| `'auto-sync'` | Silently syncs and updates the schema version record |
Run `lsdb sync --all-users` to push schema changes to all registered user sheets proactively.
## Cross-Actor Permissions Matrix
When one role needs to access another role's sheets (e.g., teacher accessing student data), configure a `permissions` map:
```typescript
const adapter = createSheetAdapter({
// ...
permissions: {
teacher: {
canAccess: ['student'], // roles this actor can access
tables: ['scores', 'attendance'], // omit to allow all tables
},
parent: {
canAccess: ['student'],
tables: ['scores'],
},
},
});
```
See `skills/permissions/SKILL.md` for full cross-actor CRUD examples.
## Integrating with an Existing Backend
OAuth in this package is strictly for **backend-to-Google-Sheets** communication. Your app's own authentication (JWT, sessions, etc.) is untouched.
```typescript
// Express example
app.get('/bookings', async (req, res) => {
const user = req.user; // from your JWT middleware
const ctx = adapter.withContext({
userId: user.id,
actor: user.actorType, // must match an actor `name` defined in your config
actorSheetId: user.sheetId, // Google Sheet ID for this user's actor
});
const bookings = await ctx.table('bookings').findMany();
res.json(bookings);
});
```
## Creating a User Sheet on Registration
When a new user registers, create their personal sheet and register them in the admin users table:
```typescript
// Basic — sheet created in admin's Drive (previous behaviour, unchanged)
const sheetId = await adapter.createUserSheet(userId, role, email);
// With options — sheet created in the actor's own Drive when actorTokens provided
const sheetId = await adapter.createUserSheet(userId, role, email, {
actorTokens: tokensFromGoogleCallback, // OAuthTokens from oauth.getTokens(code)
extraFields: { display_name: 'Alice' }, // extra columns in the users table row
});
// Creates a new Google Spreadsheet named '{role}-{userId}'
// Adds all tables for that role as sheet tabs with correct headers
// Inserts a row in admin 'users' table with user_id, role, email, actor_sheet_id + extraFields
// Returns the new sheetId — store it in your JWT/session for withContext() calls
```
See `skills/drive/SKILL.md` for the full actor-owned sheet pattern and `TokenStore`.
## asActor() Helper
Switch context to a different target actor without reconstructing the full context:
```typescript
const teacherCtx = adapter.withContext({
userId: 'teacher_001',
actor: 'teacher',
actorSheetId: 'teacher-sheet-id',
});
// Switch to access a student's sheet (requires permissions config)
const crossCtx = teacherCtx.asActor('student', 'student-sheet-id');
await crossCtx.table('scores').findMany();
```
## Sheet Formatting
Every tab created or extended by `syncSchema()` / `createUserSheet()` is auto-formatted: columns auto-fit to content, the header row gets a fill color and is frozen, and `boolean()`/`enum()` columns get dropdown data validation (`ONE_OF_LIST` — deliberately not a native checkbox for `boolean()`, see FAQ.md #10). No config is required.
The validation range is bounded (200 rows past the data that existed at sync time, not the whole sheet — see FAQ.md #10) but self-heals as rows are appended: `create()` automatically re-extends it every 100 rows so dropdown UI keeps up with organic growth between syncs. Bulk inserts via `createMany()` don't trigger this — run `lsdb sync` after a large seed/migration to format the new rows.
Override the defaults:
```typescript
const adapter = createSheetAdapter({
// ...
sheetStyle: {
headerColor: '#E8F0FE', // optional, this is also the built-in default
freezeHeader: true, // default: true
freezeFirstColumn: false, // default: false
booleanFormat: 'TRUE_FALSE', // default: 'TRUE_FALSE', or '1_0'
},
});
```
`booleanFormat` is the project-wide default for every `boolean()` column. Override one column without changing the rest via `boolean({ format: '1_0' })` — see `skills/schema/SKILL.md`.
## Common Mistakes
- **Missing `registerSchema()` before `table()`** — Calling `adapter.table('x')` before `registerSchema()` throws `SchemaError: Table x is not registered`.
- **Wrong `actorSheetId` in `withContext()`** — The `actorSheetId` must belong to a sheet owned by that role; using the admin sheet ID for a non-admin actor causes a `PermissionError`.
- **Stale/expired OAuth tokens** — Access tokens expire after 1 hour. Pass refreshed tokens from `OAuthManager.refreshTokens()` when constructing the adapter.
- **Not calling `registerSchema()` for FK-referenced tables** — If a schema uses `ref('users._id')`, the `users` schema must also be registered or FK validation throws `SchemaError: Referenced table 'users' is not registered`.
- **ESM/CJS mismatch** — The package ships CommonJS. Do **not** upgrade `chalk`, `inquirer`, or `nanoid` to ESM-only versions (chalk v5+, inquirer v9+, nanoid v4+) without migrating the project to ESM.