UNPKG

@flavoai/fastfold

Version:

Flavo frontend package

114 lines 4.63 kB
// Declarative-security schema wrappers. // // Each wrapper returns the same value Drizzle's pgTable returns, so // queries/relations/types downstream are unchanged. As a side effect the // wrapper registers a security entry keyed by table name. After migrations, // the host runs a tiny extract script that imports schema.ts and dumps the // registry into fastfold.security.json — the existing security validator // then reads that file unchanged. This keeps schema and security in one // expression so they cannot drift. import { pgTable } from 'drizzle-orm/pg-core'; const REGISTRY = {}; const DEFAULT_CRUD = ['create', 'read', 'update', 'delete']; // Canonical users-table config — the one shape that auth-sync requires. const USERS_PUBLIC_FIELDS = ['id', 'displayName', 'avatar', 'authProvider']; const USERS_OPERATIONS = ['read', 'update']; /** * User-owned data table. Each row belongs to one user; only that user can * read/update/delete it. ownerField defaults to 'email' when a column named * 'email' exists in the schema; otherwise it must be specified explicitly. * * The TypeScript constraint pins ownerField to a real column key — typo or * column rename breaks the build, not production. */ export function ownerTable(name, columns, opts) { const ownerField = opts?.ownerField ?? ('email' in columns ? 'email' : undefined); if (!ownerField) { throw new Error(`ownerTable("${name}") needs ownerField — no 'email' column in schema, ` + `pass { ownerField: 'createdBy' } (or whichever column identifies the row's owner)`); } REGISTRY[name] = { type: 'owner', ownerField, ...(opts?.publicFields ? { publicFields: opts.publicFields } : {}), operations: opts?.operations ?? DEFAULT_CRUD, }; return pgTable(name, columns); } /** * Anonymously-readable table — blog posts, product catalogs, marketing content. * Defaults to read-only because public-write tables are almost always wrong; * pass operations: ['read', 'create'] explicitly when you really need writes. */ export function publicTable(name, columns, opts) { REGISTRY[name] = { type: 'public', operations: opts?.operations ?? ['read'], }; return pgTable(name, columns); } /** * Admin-only table — review queues, audit logs, moderation surfaces. * Enforced by user.role === 'admin' on every CRUD operation. Pair with * seeding the builder's email as role: 'admin' in seed.ts. */ export function adminTable(name, columns, opts) { REGISTRY[name] = { type: 'admin', operations: opts?.operations ?? DEFAULT_CRUD, }; return pgTable(name, columns); } /** * Genuinely cross-user shared table — one company-wide feed, global * announcements. Requires { confirmed: true } so the cross-user share is an * explicit choice, not an accident. The literal `true` doubles as the * machine-readable confirmedShared flag the validator checks. */ export function sharedTable(name, columns, opts) { if (opts.confirmed !== true) { throw new Error(`sharedTable("${name}") requires { confirmed: true } — every signed-in user can read/write this table, this must be intentional`); } REGISTRY[name] = { type: 'authenticated', confirmedShared: true, operations: opts.operations ?? DEFAULT_CRUD, }; return pgTable(name, columns); } /** * The Flavo Auth users table — apply the canonical self-only config in one * call. publicFields gates which columns are surfaced through with[author] * relation joins on content tables; pass extraPublicFields to expose your * own additions (e.g. 'bio', 'tagline') without rewriting the canonical set. */ export function userTable(columns, opts) { const publicFields = [ ...USERS_PUBLIC_FIELDS, ...(opts?.extraPublicFields ?? []), ]; REGISTRY['users'] = { type: 'owner', ownerField: 'email', publicFields, operations: USERS_OPERATIONS, }; return pgTable('users', columns); } /** * Snapshot of the security registry for the host's extract script. * Returned shape matches fastfold.security.json exactly so the host can * write it to disk without transformation. */ export function getSecurityRegistry() { return { tables: { ...REGISTRY } }; } /** * Reset the registry — only used by tests. Production never imports * schema.ts twice in the same process so this is otherwise unnecessary. */ export function __resetSecurityRegistry() { for (const key of Object.keys(REGISTRY)) delete REGISTRY[key]; } //# sourceMappingURL=schema-wrappers.js.map