UNPKG

longcelot-sheet-db

Version:

Google Sheets-backed staging database adapter for Node.js with schema-first design

238 lines (178 loc) 9.63 kB
--- 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.