UNPKG

@dudousxd/nestjs-filter-clickhouse

Version:

ClickHouse adapter for @dudousxd/nestjs-filter: compiles structured filter input to parameterized ClickHouse SQL.

131 lines • 4.22 kB
const TABLE = Symbol.for('@dudousxd/nestjs-filter-clickhouse:table'); const SAFE_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/; /** `Nullable(LowCardinality(String))` → `{ base: 'String', nullable: true }`. */ export function unwrapType(type) { let t = type.trim(); let nullable = false; for (;;) { const m = /^(Nullable|LowCardinality)\((.*)\)$/s.exec(t); if (!m) break; if (m[1] === 'Nullable') nullable = true; t = m[2].trim(); } return { base: t, nullable }; } export function typeKind(base) { if (/^(String|FixedString\(\d+\)|UUID|Enum(8|16)?\(.*\)|IPv4|IPv6)$/s.test(base)) return 'string'; if (/^U?Int(8|16|32|64|128|256)$/.test(base)) return 'int'; if (/^(Float(32|64)|Decimal(32|64|128|256)?\(.*\)|BFloat16)$/.test(base)) return 'float'; if (base === 'Bool' || base === 'Boolean') return 'bool'; if (base === 'Date' || base === 'Date32') return 'date'; if (/^DateTime(64)?(\(.*\))?$/.test(base)) return 'datetime'; if (/^Array\(.*\)$/s.test(base)) return 'array'; return 'other'; } function resolveField(name, def) { if (!SAFE_NAME.test(name)) { throw new Error(`ClickHouse field name "${name}" must be a plain identifier.`); } const spec = typeof def === 'string' ? { type: def } : def; const { base, nullable } = unwrapType(spec.type); const kind = typeKind(base); let element; if (kind === 'array') { const inner = unwrapType(base.slice('Array('.length, -1)); element = { baseType: inner.base, kind: typeKind(inner.base) }; } return { name, type: spec.type, baseType: base, kind, ...(element && { element }), nullable, expr: spec.expr ?? `\`${name}\``, measure: spec.measure === true, searchable: spec.searchable ?? kind === 'string', }; } /** * A declared ClickHouse table — the adapter's "entity", used wherever a Drizzle table or an entity * class goes: `@Filterable({ entity: events })`, `runner.findAndCount(events, input)`. */ export class ClickHouseTable { [TABLE] = true; table; name; primaryKey; groupBy; where; fieldMap = new Map(); constructor(definition) { this.table = definition.table; this.name = definition.name ?? definition.table; this.primaryKey = definition.primaryKey; this.where = definition.where; for (const [name, def] of Object.entries(definition.fields)) { this.fieldMap.set(name, resolveField(name, def)); } this.groupBy = definition.groupBy ?? []; for (const dim of this.groupBy) { const field = this.fieldMap.get(dim); if (!field || field.measure) { throw new Error(`ClickHouse table "${this.name}": groupBy "${dim}" is not a dimension field.`); } } } field(name) { return this.fieldMap.get(name); } fields() { return [...this.fieldMap.values()]; } /** Whether the table has measures (rows are groups, measures aggregate per group). */ get aggregated() { return this.fields().some((f) => f.measure); } } /** * Declares a ClickHouse table. * * ```ts * export const events = defineClickHouseTable({ * table: 'events', * primaryKey: 'id', * fields: { * id: 'UUID', * at: "DateTime64(3, 'UTC')", * event: { type: 'LowCardinality(String)', expr: 'name' }, * day: { type: 'Date', expr: 'toDate(at)' }, * durationMs: 'UInt64', * }, * }); * * export const errorsDaily = defineClickHouseTable({ * table: 'errors_daily', * groupBy: ['event', 'provider'], * fields: { * day: 'Date', * event: { type: 'String', expr: 'name' }, * provider: 'String', * count: { type: 'UInt64', expr: 'sum(count)', measure: true }, * }, * }); * ``` */ export function defineClickHouseTable(definition) { return new ClickHouseTable(definition); } export function isClickHouseTable(value) { return (value != null && typeof value === 'object' && value[TABLE] === true); } //# sourceMappingURL=table.js.map