@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
JavaScript
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;
}