UNPKG

@athenox/talos

Version:

Talos: a backend you describe instead of write. Declare a schema, write the rules in plain language, map URLs to sentences; a small built-in model turns them into query plans.

89 lines (88 loc) • 4.64 kB
const GRAMMAR = new Set(['at', 'is', 'has', 'of', 'on', 'the', 'by']); /** * Talos tables read off a Drizzle schema, so an app does not declare its * data twice. * * const tables = fromDrizzle(schema, { blogPosts: { as: 'articles', says: ['blog'] } }) * * Every table of the module becomes a Talos table: its columns (logical name = * the column's own name with spaces for underscores, physical = the column), * enum domains, and how booleans, dates and JSON read back. Keys of `extra` are * the module's export names; what they hold is merged over the derived table — * the word the model sees (`as`), synonyms, rules, relations, lists. `false` * leaves a table out. * * Duck-typed on Drizzle's `Symbol.for('drizzle:…')` markers, so Talos does not * depend on drizzle-orm and works with any of its dialects. */ export function fromDrizzle(schema, extra = {}) { const NAME = Symbol.for('drizzle:Name'), COLUMNS = Symbol.for('drizzle:Columns'); const tables = []; for (const [key, t] of Object.entries(schema)) { if (!t || typeof t !== 'object' || !(NAME in t) || !(COLUMNS in t)) continue; const more = extra[key]; if (more === false) continue; const table = t[NAME]; const cols = t[COLUMNS]; const columns = {}; const enums = {}; const types = {}; const read = {}, write = {}; const defaults = { insert: {}, update: {} }; // The word the model sees: the column's name less the words that only // carry grammar (`created_at` → created, `is_published` → published) — // "at" and "is" were landing in filters as values. A name that would clash // keeps its full form. const wordsOf = (name) => { const full = name.replace(/_/g, ' '); // `author_id` is the author, to a reader. const words = full.split(' '); const short = words.filter(w => !GRAMMAR.has(w) && !(w === 'id' && words.length > 1)).join(' '); return short && !(short in columns) ? short : full; }; for (const c of Object.values(cols)) { const logical = wordsOf(String(c.name)); columns[logical] = c.name; // Values the schema computes itself, applied when a write leaves them out. if (typeof c.defaultFn === 'function') defaults.insert[c.name] = () => c.defaultFn(); if (typeof c.onUpdateFn === 'function') defaults.update[c.name] = () => c.onUpdateFn(); if (Array.isArray(c.enumValues) && c.enumValues.length) enums[logical] = c.enumValues; const type = c.dataType === 'boolean' ? 'boolean' : c.dataType === 'date' ? 'date' : c.dataType === 'json' ? 'json' : undefined; if (type) types[c.name] = type; // The column's own conversions: what a custom date type stores, what a // boolean or a timestamp reads back as. Plain text and numbers need none. if (type || c.dataType === 'custom') { if (typeof c.mapFromDriverValue === 'function') read[c.name] = v => c.mapFromDriverValue(v); if (typeof c.mapToDriverValue === 'function') write[c.name] = v => c.mapToDriverValue(v); } } // An extra `columns: { title: 'name' }` renames: the physical column keeps // one logical name, the app's. const renamed = { ...columns }; for (const [logical, physical] of Object.entries(more && more.columns || {})) for (const k of Object.keys(renamed)) if (renamed[k] === physical && k !== logical) delete renamed[k]; const logicalOf = (physical) => Object.keys({ ...renamed, ...(more && more.columns) }).find(k => ({ ...renamed, ...(more && more.columns) })[k] === physical); const writable = Object.values(cols).filter(c => !c.primary).map(c => logicalOf(c.name)); tables.push({ as: table.replace(/_/g, ' '), table, columns, writable, defaults, ...(Object.keys(enums).length ? { enums } : {}), types, read, write, ...more, // Merged, not replaced: an app adds a column's synonyms without losing the rest. ...(more?.columns ? { columns: { ...renamed, ...more.columns } } : {}), ...(more?.enums ? { enums: { ...enums, ...more.enums } } : {}), ...(more?.types ? { types: { ...types, ...more.types } } : {}), }); } return tables; }