@flavoai/fastfold
Version:
Flavo frontend package
114 lines • 4.63 kB
JavaScript
// 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