@dudousxd/nestjs-filter-drizzle
Version:
Drizzle ORM adapter for @dudousxd/nestjs-filter.
188 lines • 7.38 kB
JavaScript
import { Many, One, Table, createTableRelationsHelpers, extractTablesRelationalConfig, getTableColumns, getTableName, is, normalizeRelation, } from 'drizzle-orm';
/**
* Everything the adapter knows about the schema, read once from the objects the
* application already declares — Drizzle has no metadata registry to ask.
*
* - Columns come straight off the table (`getTableColumns`), so a filter works
* with no schema registered at all.
* - Relations come from the `relations()` objects in the schema passed to
* `drizzle(client, { schema })` (or handed to the adapter directly). Without
* them the adapter still filters, sorts, paginates and searches; relation
* features (includes, dot-notation, `whereHas`, aggregates) report the
* relation as unknown.
*
* Results are memoized per table: schema objects are immutable after bootstrap.
*/
export class DrizzleSchemaMetadata {
tablesConfig;
tableNamesMap;
/** Original table object → its schema key. */
schemaKeyByTable = new Map();
relationCache = new Map();
fieldCache = new Map();
keyByColumn = new Map();
pkCache = new Map();
constructor(schema) {
const source = schema ?? {};
const extracted = extractTablesRelationalConfig(source, createTableRelationsHelpers);
this.tablesConfig = extracted.tables;
this.tableNamesMap = extracted.tableNamesMap;
for (const [key, value] of Object.entries(source)) {
if (is(value, Table))
this.schemaKeyByTable.set(value, key);
}
}
/** True when the schema carries at least one `relations()` declaration. */
get hasRelations() {
return Object.values(this.tablesConfig).some((t) => Object.keys(t.relations).length > 0);
}
/** The schema key for a table (`users`), falling back to its SQL name. */
tableDisplayName(table) {
return this.schemaKeyByTable.get(table) ?? getTableName(table);
}
/** Property key → column, exactly as `getTableColumns` reports it. */
columnsOf(table) {
return getTableColumns(table);
}
/** The property key a column is declared under on its (original) table. */
keyOf(table, column) {
let map = this.keyByColumn.get(table);
if (!map) {
map = new Map();
for (const [key, col] of Object.entries(this.columnsOf(table)))
map.set(col, key);
this.keyByColumn.set(table, map);
}
const direct = map.get(column);
if (direct)
return direct;
// An aliased column is a Proxy — a different identity with the same name.
for (const [key, col] of Object.entries(this.columnsOf(table))) {
if (col.name === column.name)
return key;
}
return undefined;
}
fields(table) {
const cached = this.fieldCache.get(table);
if (cached)
return cached;
const fields = Object.entries(this.columnsOf(table)).map(([name, column]) => ({
name,
columnName: column.name,
type: classifyColumn(column),
}));
this.fieldCache.set(table, fields);
return fields;
}
/** Primary-key property keys (single or composite), `[]` when none is declared. */
primaryKeys(table) {
const cached = this.pkCache.get(table);
if (cached)
return cached;
// Reuse drizzle's own extraction for one table: it folds both column-level
// `.primaryKey()` and table-level `primaryKey({ columns })` into one list.
const single = extractTablesRelationalConfig({ t: table }, createTableRelationsHelpers).tables.t;
const keys = (single?.primaryKey ?? [])
.map((col) => this.keyOf(table, col))
.filter((k) => k !== undefined);
this.pkCache.set(table, keys);
return keys;
}
/**
* The table's relations, keyed by name — or `null` when the table is not part
* of the registered schema (so the runner can tell "no relations" from
* "relations unknown").
*/
relations(table) {
if (this.relationCache.has(table))
return this.relationCache.get(table);
const tsName = this.schemaKeyByTable.get(table);
const config = tsName ? this.tablesConfig[tsName] : undefined;
let result = null;
if (config) {
result = new Map();
for (const [name, relation] of Object.entries(config.relations)) {
const resolved = this.resolve(name, relation);
if (resolved)
result.set(name, resolved);
}
}
this.relationCache.set(table, result);
return result;
}
relation(table, name) {
return this.relations(table)?.get(name);
}
resolve(name, relation) {
let normalized;
try {
normalized = normalizeRelation(this.tablesConfig, this.tableNamesMap, relation);
}
catch {
// A `many()` whose inverse `one()` is missing (or ambiguous) cannot be
// correlated — drizzle's own relational queries reject it too. Leave it
// out rather than guess a join.
return null;
}
if (normalized.fields.length === 0)
return null;
const target = relation.referencedTable;
let kind;
if (is(relation, Many))
kind = 'one-to-many';
else if (is(relation, One) && relation.config)
kind = 'many-to-one';
else
kind = 'one-to-one';
return {
name,
kind,
target,
targetName: this.tableDisplayName(target),
sourceColumns: normalized.fields,
targetColumns: normalized.references,
};
}
}
/**
* The column declared under `key` on `table` (or an alias of it) — an OWN
* property lookup, so a client-supplied name like `constructor` or `__proto__`
* (both pass the identifier grammar) never resolves to something inherited from
* `Object.prototype`.
*/
export function columnOf(table, key) {
const columns = getTableColumns(table);
return Object.hasOwn(columns, key) ? columns[key] : undefined;
}
/**
* Maps a drizzle column onto the core's simplified type vocabulary. `dataType`
* is drizzle's own runtime classification; `columnType` refines the few cases
* where the JS-side representation hides the SQL meaning (a Postgres `numeric`
* is a `string` in JS but a number to filter on; a `timestamp` in `mode:
* 'string'` is still a date column).
*/
export function classifyColumn(column) {
const columnType = String(column.columnType ?? '');
const dataType = String(column.dataType ?? '');
if (/Numeric|Decimal|Real|Double|Float/i.test(columnType))
return 'number';
if (/Timestamp|Date|Time/i.test(columnType) && dataType !== 'number')
return 'date';
switch (dataType) {
case 'string':
return 'string';
case 'number':
case 'bigint':
return 'number';
case 'boolean':
return 'boolean';
case 'date':
return 'date';
case 'json':
return 'json';
default:
return 'unknown';
}
}
//# sourceMappingURL=schema-metadata.js.map