From d59e4867299d10be68dc104db6879e96ff04f577 Mon Sep 17 00:00:00 2001 From: Javier Date: Tue, 10 Aug 2021 18:40:39 +0200 Subject: [PATCH] Modular types --- db.mjs | 8 + src/db/conditions.ts | 76 ++-- src/db/core.ts | 83 +++- src/db/shortcuts.ts | 755 ++++++++++++++++++++++------------ src/generate/config.ts | 8 +- src/generate/tables.ts | 77 ++-- src/generate/tsOutput.ts | 6 +- src/typings/zapatos/schema.ts | 22 - tsconfig.json | 9 +- 9 files changed, 656 insertions(+), 388 deletions(-) delete mode 100644 src/typings/zapatos/schema.ts diff --git a/db.mjs b/db.mjs index 2c7fb6c..fd9c93b 100644 --- a/db.mjs +++ b/db.mjs @@ -20,6 +20,12 @@ export const constraint = mod.constraint; export const count = mod.count; export const deletes = mod.deletes; export const doNothing = mod.doNothing; +export const genericAvg = mod.genericAvg; +export const genericCount = mod.genericCount; +export const genericMax = mod.genericMax; +export const genericMin = mod.genericMin; +export const genericSelectExactlyOne = mod.genericSelectExactlyOne; +export const genericSum = mod.genericSum; export const getConfig = mod.getConfig; export const insert = mod.insert; export const isDatabaseError = mod.isDatabaseError; @@ -43,6 +49,8 @@ export const setConfig = mod.setConfig; export const sql = mod.sql; export const strict = mod.strict; export const sum = mod.sum; +export const table = mod.table; +export const tables = mod.tables; export const toBuffer = mod.toBuffer; export const toDate = mod.toDate; export const toString = mod.toString; diff --git a/src/db/conditions.ts b/src/db/conditions.ts index 6d0a8df..f66843e 100644 --- a/src/db/conditions.ts +++ b/src/db/conditions.ts @@ -10,7 +10,7 @@ import { Parameter, param, sql, - SQL, + GenericSQL, self, vals, } from './core'; @@ -20,47 +20,47 @@ import { mapWithSeparator } from './utils'; const conditionalParam = (a: any) => a instanceof SQLFragment || a instanceof ParentColumn || a instanceof Parameter ? a : param(a); -export const isNull = sql`${self} IS NULL`; -export const isNotNull = sql`${self} IS NOT NULL`; -export const isTrue = sql`${self} IS TRUE`; -export const isNotTrue = sql`${self} IS NOT TRUE`; -export const isFalse = sql`${self} IS FALSE`; -export const isNotFalse = sql`${self} IS NOT FALSE`; -export const isUnknown = sql`${self} IS UNKNOWN`; -export const isNotUnknown = sql`${self} IS NOT UNKNOWN`; +export const isNull = sql`${self} IS NULL`; +export const isNotNull = sql`${self} IS NOT NULL`; +export const isTrue = sql`${self} IS TRUE`; +export const isNotTrue = sql`${self} IS NOT TRUE`; +export const isFalse = sql`${self} IS FALSE`; +export const isNotFalse = sql`${self} IS NOT FALSE`; +export const isUnknown = sql`${self} IS UNKNOWN`; +export const isNotUnknown = sql`${self} IS NOT UNKNOWN`; -export const isDistinctFrom = (a: T) => sql`${self} IS DISTINCT FROM ${conditionalParam(a)}`; -export const isNotDistinctFrom = (a: T) => sql`${self} IS NOT DISTINCT FROM ${conditionalParam(a)}`; +export const isDistinctFrom = (a: T) => sql`${self} IS DISTINCT FROM ${conditionalParam(a)}`; +export const isNotDistinctFrom = (a: T) => sql`${self} IS NOT DISTINCT FROM ${conditionalParam(a)}`; -export const eq = (a: T) => sql`${self} = ${conditionalParam(a)}`; -export const ne = (a: T) => sql`${self} <> ${conditionalParam(a)}`; -export const gt = (a: T) => sql`${self} > ${conditionalParam(a)}`; -export const gte = (a: T) => sql`${self} >= ${conditionalParam(a)}`; -export const lt = (a: T) => sql`${self} < ${conditionalParam(a)}`; -export const lte = (a: T) => sql`${self} <= ${conditionalParam(a)}`; +export const eq = (a: T) => sql`${self} = ${conditionalParam(a)}`; +export const ne = (a: T) => sql`${self} <> ${conditionalParam(a)}`; +export const gt = (a: T) => sql`${self} > ${conditionalParam(a)}`; +export const gte = (a: T) => sql`${self} >= ${conditionalParam(a)}`; +export const lt = (a: T) => sql`${self} < ${conditionalParam(a)}`; +export const lte = (a: T) => sql`${self} <= ${conditionalParam(a)}`; -export const between = (a: T, b: T) => sql `${self} BETWEEN (${conditionalParam(a)}) AND (${conditionalParam(b)})`; -export const betweenSymmetric = (a: T, b: T) => sql `${self} BETWEEN SYMMETRIC (${conditionalParam(a)}) AND (${conditionalParam(b)})`; -export const notBetween = (a: T, b: T) => sql `${self} NOT BETWEEN (${conditionalParam(a)}) AND (${conditionalParam(b)})`; -export const notBetweenSymmetric = (a: T, b: T) => sql `${self} NOT BETWEEN SYMMETRIC (${conditionalParam(a)}) AND (${conditionalParam(b)})`; +export const between = (a: T, b: T) => sql `${self} BETWEEN (${conditionalParam(a)}) AND (${conditionalParam(b)})`; +export const betweenSymmetric = (a: T, b: T) => sql `${self} BETWEEN SYMMETRIC (${conditionalParam(a)}) AND (${conditionalParam(b)})`; +export const notBetween = (a: T, b: T) => sql `${self} NOT BETWEEN (${conditionalParam(a)}) AND (${conditionalParam(b)})`; +export const notBetweenSymmetric = (a: T, b: T) => sql `${self} NOT BETWEEN SYMMETRIC (${conditionalParam(a)}) AND (${conditionalParam(b)})`; -export const like = (a: T) => sql`${self} LIKE ${conditionalParam(a)}`; -export const notLike = (a: T) => sql`${self} NOT LIKE ${conditionalParam(a)}`; -export const ilike = (a: T) => sql`${self} ILIKE ${conditionalParam(a)}`; -export const notIlike = (a: T) => sql`${self} NOT ILIKE ${conditionalParam(a)}`; -export const similarTo = (a: T) => sql`${self} SIMILAR TO ${conditionalParam(a)}`; -export const notSimilarTo = (a: T) => sql`${self} NOT SIMILAR TO ${conditionalParam(a)}`; -export const reMatch = (a: T) => sql`${self} ~ ${conditionalParam(a)}`; -export const reImatch = (a: T) => sql`${self} ~* ${conditionalParam(a)}`; -export const notReMatch = (a: T) => sql`${self} !~ ${conditionalParam(a)}`; -export const notReImatch = (a: T) => sql`${self} !~* ${conditionalParam(a)}`; +export const like = (a: T) => sql`${self} LIKE ${conditionalParam(a)}`; +export const notLike = (a: T) => sql`${self} NOT LIKE ${conditionalParam(a)}`; +export const ilike = (a: T) => sql`${self} ILIKE ${conditionalParam(a)}`; +export const notIlike = (a: T) => sql`${self} NOT ILIKE ${conditionalParam(a)}`; +export const similarTo = (a: T) => sql`${self} SIMILAR TO ${conditionalParam(a)}`; +export const notSimilarTo = (a: T) => sql`${self} NOT SIMILAR TO ${conditionalParam(a)}`; +export const reMatch = (a: T) => sql`${self} ~ ${conditionalParam(a)}`; +export const reImatch = (a: T) => sql`${self} ~* ${conditionalParam(a)}`; +export const notReMatch = (a: T) => sql`${self} !~ ${conditionalParam(a)}`; +export const notReImatch = (a: T) => sql`${self} !~* ${conditionalParam(a)}`; -export const isIn = (a: readonly T[]) => a.length > 0 ? sql`${self} IN (${vals(a)})` : sql`false`; -export const isNotIn = (a: readonly T[]) => a.length > 0 ? sql`${self} NOT IN (${vals(a)})` : sql`true`; +export const isIn = (a: readonly T[]) => a.length > 0 ? sql`${self} IN (${vals(a)})` : sql`false`; +export const isNotIn = (a: readonly T[]) => a.length > 0 ? sql`${self} NOT IN (${vals(a)})` : sql`true`; -export const or = (...conditions: SQLFragment[]) => sql`(${mapWithSeparator(conditions, sql` OR `, c => c)})`; -export const and = (...conditions: SQLFragment[]) => sql`(${mapWithSeparator(conditions, sql` AND `, c => c)})`; -export const not = (condition: SQLFragment) => sql`(NOT ${condition})`; +export const or = (...conditions: SQLFragment[]) => sql`(${mapWithSeparator(conditions, sql` OR `, c => c)})`; +export const and = (...conditions: SQLFragment[]) => sql`(${mapWithSeparator(conditions, sql` AND `, c => c)})`; +export const not = (condition: SQLFragment) => sql`(NOT ${condition})`; // things that aren't genuinely conditions type IntervalUnit = 'microsecond' | 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year' | 'decade' | 'century' | 'millennium'; @@ -69,5 +69,5 @@ export const after = gt; export const before = lt; // these are really more operations than conditions, but we sneak them in here for now, for use e.g. in UPDATE queries -export const add = (a: T) => sql`${self} + ${conditionalParam(a)}`; -export const subtract = (a: T) => sql`${self} - ${conditionalParam(a)}`; +export const add = (a: T) => sql`${self} + ${conditionalParam(a)}`; +export const subtract = (a: T) => sql`${self} - ${conditionalParam(a)}`; diff --git a/src/db/core.ts b/src/db/core.ts index 30b2825..0ea077b 100644 --- a/src/db/core.ts +++ b/src/db/core.ts @@ -10,16 +10,17 @@ import { performance } from 'perf_hooks'; import { getConfig, SQLQuery } from './config'; import { isPOJO, NoInfer } from './utils'; -import type { - Updatable, - Whereable, - Table, - Column, -} from 'zapatos/schema'; - // === symbols, types, wrapper classes and shortcuts === +declare global { + namespace Zapatos { + /** Injects additional structures on Zapatos.StructureMap */ + interface StructureMap { + } + } +} + /** * Compiles to `DEFAULT` for use in `INSERT`/`UPDATE` queries. */ @@ -73,6 +74,8 @@ export type NumberRangeString = RangeString; */ export type ByteArrayString = `\\x${string}`; +export type Column = Exclude; + /** * Make a function `STRICT` in the Postgres sense — where it's an alias for * `RETURNS NULL ON NULL INPUT` — with appropriate typing. @@ -189,17 +192,44 @@ export function vals(x: T) { return new ColumnValues(x); } * Compiles to the name of the column it wraps in the table of the parent query. * @param value The column name */ -export class ParentColumn { constructor(public value: T) { } } +export class ParentColumn = Column> { constructor(public value: T) { } } /** * Returns a `ParentColumn` instance, wrapping a column name, which compiles to * that column name of the table of the parent query. */ -export function parent(x: T) { return new ParentColumn(x); } +export function parent = Column>(x: T) { return new ParentColumn(x); } export type GenericSQLExpression = SQLFragment | Parameter | DefaultType | DangerousRawString | SelfType; -export type SQLExpression = Table | ColumnNames | ColumnValues | Whereable | Column | GenericSQLExpression; -export type SQL = SQLExpression | SQLExpression[]; + +export interface GenericSQLStructure { + Schema: string; + Table: string; + Selectable: { [k: string]: any }; + JSONSelectable: object; + Whereable: object; + Insertable: object; + Updatable: object; + UniqueIndex: string; +} + +export type SQLExpressionForStructure = GenericSQLExpression | ColumnNames | ColumnValues | S['Table'] | S['Schema'] | S['Whereable'] | Column; +export type SQLForStructure = SQLExpressionForStructure | Array>; + +export type GenericSQL = + | GenericSQLExpression + | ColumnNames + | ColumnValues + | GenericSQLStructure['Table'] + | GenericSQLStructure['Schema'] + | GenericSQLStructure['Whereable'] + | Column; + +// eslint-disable-next-line no-undef +export type SQLStructure = { [Key in keyof Zapatos.StructureMap]: Zapatos.StructureMap[Key] }[keyof Zapatos.StructureMap]; + +export type SQL = SQLForStructure; export type Queryable = pg.ClientBase | pg.Pool; @@ -212,14 +242,23 @@ export type Queryable = pg.ClientBase | pg.Pool; * defines what type the `SQLFragment` produces, where relevant (i.e. when * calling `.run(...)` on it, or using it as the value of an `extras` object). */ -export function sql< - Interpolations = SQL, - RunResult = pg.QueryResult['rows'], - Constraint = never, - >(literals: TemplateStringsArray, ...expressions: NoInfer[]) { - return new SQLFragment(Array.prototype.slice.apply(literals), expressions); +interface SqlSignatures { + < + Interpolations = SQL, + RunResult = pg.QueryResult['rows'], + Constraint = never, + >(literals: TemplateStringsArray, ...expressions: NoInfer[]): SQLFragment; + < + Structure extends GenericSQLStructure = SQLStructure, + RunResult = pg.QueryResult['rows'], + Constraint = never, + >(literals: TemplateStringsArray, ...expressions: NoInfer>[]): SQLFragment; } +export const sql: SqlSignatures = (literals: TemplateStringsArray, ...expressions: NoInfer[]) => { + return new SQLFragment(Array.prototype.slice.apply(literals), expressions); +}; + let preparedNameSeq = 0; export class SQLFragment { @@ -239,7 +278,7 @@ export class SQLFragment noop = false; // if true, bypass actually running the query unless forced to e.g. for empty INSERTs noopResult: any; // if noop is true and DB is bypassed, what should be returned? - constructor(protected literals: string[], protected expressions: SQL[]) { } + constructor(protected literals: string[], protected expressions: GenericSQL[]) { } /** * Instruct Postgres to treat this as a prepared statement: see @@ -286,7 +325,7 @@ export class SQLFragment * that could be passed to the `pg` query function. Arguments are generally * only passed when the function calls itself recursively. */ - compile = (result: SQLQuery = { text: '', values: [] }, parentTable?: string, currentColumn?: Column) => { + compile = (result: SQLQuery = { text: '', values: [] }, parentTable?: string, currentColumn?: Column) => { if (this.parentTable) parentTable = this.parentTable; if (this.noop) result.text += "/* marked no-op: won't hit DB unless forced -> */ "; @@ -301,7 +340,7 @@ export class SQLFragment return result; }; - compileExpression = (expression: SQL, result: SQLQuery = { text: '', values: [] }, parentTable?: string, currentColumn?: Column) => { + compileExpression = (expression: GenericSQL, result: SQLQuery = { text: '', values: [] }, parentTable?: string, currentColumn?: Column) => { if (this.parentTable) parentTable = this.parentTable; if (expression instanceof SQLFragment) { @@ -380,7 +419,7 @@ export class SQLFragment } else { const - columnNames = Object.keys(expression.value).sort(), + columnNames = []>Object.keys(expression.value).sort(), columnValues = columnNames.map(k => (expression.value)[k]); for (let i = 0, len = columnValues.length; i < len; i++) { @@ -397,7 +436,7 @@ export class SQLFragment } else if (typeof expression === 'object') { // must be a Whereable object, so put together a WHERE clause - const columnNames = Object.keys(expression).sort(); + const columnNames = []>Object.keys(expression).sort(); if (columnNames.length) { // if the object is not empty result.text += '('; diff --git a/src/db/shortcuts.ts b/src/db/shortcuts.ts index 3b3b828..83353fb 100644 --- a/src/db/shortcuts.ts +++ b/src/db/shortcuts.ts @@ -4,25 +4,13 @@ Copyright (C) 2020 - 2021 George MacKerron Released under the MIT licence: see LICENCE file */ -import type { - JSONSelectableForTable, - WhereableForTable, - InsertableForTable, - UpdatableForTable, - ColumnForTable, - UniqueIndexForTable, - SQLForTable, - Insertable, - Updatable, - Whereable, - Table, - Column, -} from 'zapatos/schema'; - import { AllType, all, - SQL, + GenericSQL, + GenericSQLStructure, + SQLForStructure, + SQLStructure, SQLFragment, sql, cols, @@ -30,6 +18,7 @@ import { raw, param, Default, + Column } from './core'; import { @@ -38,49 +27,50 @@ import { } from './utils'; -export type JSONOnlyColsForTable[]` gives errors here for reasons I haven't got to the bottom of */> = - Pick, C[number]>; +export type JSONColumn = Exclude; +export type JSONOnlyColsForTable[]> = + Pick; export interface SQLFragmentMap { [k: string]: SQLFragment } -export interface SQLFragmentOrColumnMap { [k: string]: SQLFragment | ColumnForTable } +export interface SQLFragmentOrColumnMap { [k: string]: SQLFragment | Column } export type RunResultForSQLFragment> = T extends SQLFragment ? RunResult : never; export type LateralResult = { [K in keyof L]: RunResultForSQLFragment }; -export type ExtrasResult> = { [K in keyof E]: - E[K] extends SQLFragment ? RunResultForSQLFragment : E[K] extends keyof JSONSelectableForTable ? JSONSelectableForTable[E[K]] : never; +export type ExtrasResult> = { [K in keyof E]: + E[K] extends SQLFragment ? RunResultForSQLFragment : E[K] extends keyof S['JSONSelectable'] ? S['JSONSelectable'][E[K]] : never; }; -type ExtrasOption = SQLFragmentOrColumnMap | undefined; -type ColumnsOption = ColumnForTable[] | undefined; +type ExtrasOption = SQLFragmentOrColumnMap | undefined; +type ColumnsOption = Column[] | undefined; type LimitedLateralOption = SQLFragmentMap | undefined; type FullLateralOption = LimitedLateralOption | SQLFragment; type LateralOption< - C extends ColumnsOption, - E extends ExtrasOption
, + C extends ColumnsOption, + E extends ExtrasOption, > = undefined extends C ? undefined extends E ? FullLateralOption : LimitedLateralOption : LimitedLateralOption; -export interface ReturningOptionsForTable, E extends ExtrasOption> { +export interface ReturningOptionsForTable, E extends ExtrasOption> { returning?: C; extras?: E; }; -type ReturningTypeForTable, E extends ExtrasOption> = - (undefined extends C ? JSONSelectableForTable : - C extends ColumnForTable[] ? JSONOnlyColsForTable : +type ReturningTypeForTable[] | undefined, E extends ExtrasOption> = + (undefined extends C ? S['JSONSelectable'] : + C extends JSONColumn[] ? JSONOnlyColsForTable : never) & (undefined extends E ? {} : - E extends SQLFragmentOrColumnMap ? ExtrasResult : + E extends SQLFragmentOrColumnMap ? ExtrasResult : never); -function SQLForColumnsOfTable(columns: Column[] | undefined, table: Table) { +function SQLForColumnsOfTable(columns: Column[] | undefined, table: GenericSQLStructure['Table']) { return columns === undefined ? sql`to_jsonb(${table}.*)` : sql`jsonb_build_object(${mapWithSeparator(columns, sql`, `, c => sql`${param(c)}::text, ${c}`)})`; } -function SQLForExtras(extras: ExtrasOption) { +function SQLForExtras(extras: ExtrasOption) { return extras === undefined ? [] : sql` || jsonb_build_object(${mapWithSeparator( Object.keys(extras), sql`, `, k => sql`${param(k)}::text, ${extras[k]}`)})`; @@ -89,30 +79,25 @@ function SQLForExtras(extras: ExtrasOption) { /* === insert === */ -interface InsertSignatures { - , E extends ExtrasOption>( +interface InsertSignatures { + , C extends ColumnsOption, E extends ExtrasOption>( table: T, - values: InsertableForTable, - options?: ReturningOptionsForTable - ): SQLFragment>; + values: S['Insertable'], + options?: ReturningOptionsForTable + ): SQLFragment>; - , E extends ExtrasOption>( + , C extends ColumnsOption, E extends ExtrasOption>( table: T, - values: InsertableForTable[], - options?: ReturningOptionsForTable - ): SQLFragment[]>; + values: S['Insertable'][], + options?: ReturningOptionsForTable + ): SQLFragment[]>; } -/** - * Generate an `INSERT` query `SQLFragment`. - * @param table The table into which to insert - * @param values The `Insertable` values (or array thereof) to be inserted - */ -export const insert: InsertSignatures = function ( - table: Table, - values: Insertable | Insertable[], - options?: ReturningOptionsForTable, ExtrasOption
> -): SQLFragment { +const genericInsert = ( + table: GenericSQLStructure['Table'], + values: GenericSQLStructure['Insertable'] | GenericSQLStructure['Insertable'][], + options?: ReturningOptionsForTable, ExtrasOption> +): SQLFragment => { let query; if (Array.isArray(values) && values.length === 0) { @@ -125,7 +110,7 @@ export const insert: InsertSignatures = function ( completedValues = Array.isArray(values) ? completeKeysWithDefaultValue(values, Default) : values, colsSQL = cols(Array.isArray(completedValues) ? completedValues[0] : completedValues), valuesSQL = Array.isArray(completedValues) ? - mapWithSeparator(completedValues as Insertable[], sql`, `, v => sql`(${vals(v)})`) : + mapWithSeparator(completedValues as GenericSQLStructure['Insertable'][], sql`, `, v => sql`(${vals(v)})`) : sql`(${vals(completedValues)})`, returningSQL = SQLForColumnsOfTable(options?.returning, table), extrasSQL = SQLForExtras(options?.extras); @@ -140,6 +125,13 @@ export const insert: InsertSignatures = function ( return query; }; +/** + * Generate an `INSERT` query `SQLFragment`. + * @param table The table into which to insert + * @param values The `Insertable` values (or array thereof) to be inserted + */ +export const insert: InsertSignatures = genericInsert; + /* === upsert === */ @@ -147,88 +139,78 @@ export const insert: InsertSignatures = function ( * Wraps a unique index of the target table for use as the arbiter constraint * of an `upsert` shortcut query. */ -export class Constraint { constructor(public value: UniqueIndexForTable) { } } +export class Constraint { constructor(public value: S['UniqueIndex']) { } } /** * Returns a `Constraint` instance, wrapping a unique index of the target table * for use as the arbiter constraint of an `upsert` shortcut query. */ -export function constraint(x: UniqueIndexForTable) { return new Constraint(x); } +export function constraint(x: S['UniqueIndex']) { return new Constraint(x); } export interface UpsertAction { $action: 'INSERT' | 'UPDATE' } type UpsertReportAction = 'suppress'; type UpsertReturnableForTable< - T extends Table, - C extends ColumnsOption, - E extends ExtrasOption, + S extends GenericSQLStructure, + C extends ColumnsOption, + E extends ExtrasOption, RA extends UpsertReportAction | undefined > = - ReturningTypeForTable & (undefined extends RA ? UpsertAction : {}); + ReturningTypeForTable & (undefined extends RA ? UpsertAction : {}); -type UpsertConflictTargetForTable = Constraint | ColumnForTable | ColumnForTable[]; -type UpdateColumns = ColumnForTable | ColumnForTable[]; +type UpsertConflictTargetForStructure = Constraint | Column | Column[]; +type UpdateColumns = Column | Column[]; interface UpsertOptions< - T extends Table, - C extends ColumnsOption, - E extends ExtrasOption, - UC extends UpdateColumns | undefined, + S extends GenericSQLStructure, + C extends ColumnsOption, + E extends ExtrasOption, + UC extends UpdateColumns | undefined, RA extends UpsertReportAction | undefined, - > extends ReturningOptionsForTable { - updateValues?: UpdatableForTable; + > extends ReturningOptionsForTable { + updateValues?: S['Updatable']; updateColumns?: UC; - noNullUpdateColumns?: ColumnForTable | ColumnForTable[]; + noNullUpdateColumns?: Column | Column[]; reportAction?: RA; } -interface UpsertSignatures { - , - E extends ExtrasOption, - UC extends UpdateColumns | undefined, +interface UpsertSignatures { + , + C extends ColumnsOption, + E extends ExtrasOption, + UC extends UpdateColumns | undefined, RA extends UpsertReportAction | undefined >( table: T, - values: InsertableForTable, - conflictTarget: UpsertConflictTargetForTable, - options?: UpsertOptions - ): SQLFragment | (UC extends never[] ? undefined : never)>; - - , - E extends ExtrasOption, - UC extends UpdateColumns | undefined, + values: S['Insertable'], + conflictTarget: UpsertConflictTargetForStructure, + options?: UpsertOptions + ): SQLFragment | (UC extends never[] ? undefined : never)>; + + , + C extends ColumnsOption, + E extends ExtrasOption, + UC extends UpdateColumns | undefined, RA extends UpsertReportAction | undefined >( table: T, - values: InsertableForTable[], - conflictTarget: UpsertConflictTargetForTable, - options?: UpsertOptions - ): SQLFragment[]>; + values: S['Insertable'][], + conflictTarget: UpsertConflictTargetForStructure, + options?: UpsertOptions + ): SQLFragment[]>; } export const doNothing = []; -/** - * Generate an 'upsert' (`INSERT ... ON CONFLICT ...`) query `SQLFragment`. - * @param table The table to update or insert into - * @param values An `Insertable` of values (or an array thereof) to be inserted - * or updated - * @param conflictTarget A `UNIQUE`-indexed column (or array thereof) or a - * `UNIQUE` index (wrapped in `db.constraint(...)`) that determines whether we - * get an `UPDATE` (when there's a matching existing value) or an `INSERT` - * (when there isn't) - * @param options Optionally, an object with any of the keys `updateColumns`, - * `noNullUpdateColumns` and `updateValues` (see documentation). - */ -export const upsert: UpsertSignatures = function ( - table: Table, - values: Insertable | Insertable[], - conflictTarget: Column | Column[] | Constraint
, - options?: UpsertOptions, ExtrasOption
, UpdateColumns
, UpsertReportAction> +const genericUpsert = function ( + table: GenericSQLStructure['Table'], + values: GenericSQLStructure['Insertable'] | GenericSQLStructure['Insertable'][], + conflictTarget: Column | Column[] | Constraint, + options?: UpsertOptions, ExtrasOption, UpdateColumns, UpsertReportAction> ): SQLFragment { - if (Array.isArray(values) && values.length === 0) return insert(table, values); // punt a no-op to plain insert + if (Array.isArray(values) && values.length === 0) return genericInsert(table, values); // punt a no-op to plain insert if (typeof conflictTarget === 'string') conflictTarget = [conflictTarget]; // now either Column[] or Constraint let noNullUpdateColumns = options?.noNullUpdateColumns ?? []; @@ -242,7 +224,7 @@ export const upsert: UpsertSignatures = function ( firstRow = completedValues[0], insertColsSQL = cols(firstRow), insertValuesSQL = mapWithSeparator(completedValues, sql`, `, v => sql`(${vals(v)})`), - colNames = Object.keys(firstRow) as Column[], + colNames = Object.keys(firstRow) as Column[], updateColumns = specifiedUpdateColumns as string[] ?? colNames, conflictTargetSQL = Array.isArray(conflictTarget) ? sql`(${mapWithSeparator(conflictTarget, sql`, `, c => c)})` : @@ -250,9 +232,11 @@ export const upsert: UpsertSignatures = function ( updateColsSQL = mapWithSeparator(updateColumns, sql`, `, c => c), updateValues = options?.updateValues ?? {}, updateValuesSQL = mapWithSeparator(updateColumns, sql`, `, c => - updateValues[c] !== undefined ? updateValues[c] : - noNullUpdateColumns.includes(c) ? sql`CASE WHEN EXCLUDED.${c} IS NULL THEN ${table}.${c} ELSE EXCLUDED.${c} END` : - sql`EXCLUDED.${c}`), + (updateValues as { [k: string]: any })[c] !== undefined + ? (updateValues as { [k: string]: any })[c] + : noNullUpdateColumns.includes(c) + ? sql`CASE WHEN EXCLUDED.${c} IS NULL THEN ${table}.${c} ELSE EXCLUDED.${c} END` + : sql`EXCLUDED.${c}`), returningSQL = SQLForColumnsOfTable(options?.returning, table), extrasSQL = SQLForExtras(options?.extras), suppressReport = options?.reportAction === 'suppress'; @@ -275,29 +259,37 @@ export const upsert: UpsertSignatures = function ( return query; }; +/** + * Generate an 'upsert' (`INSERT ... ON CONFLICT ...`) query `SQLFragment`. + * @param table The table to update or insert into + * @param values An `Insertable` of values (or an array thereof) to be inserted + * or updated + * @param conflictTarget A `UNIQUE`-indexed column (or array thereof) or a + * `UNIQUE` index (wrapped in `db.constraint(...)`) that determines whether we + * get an `UPDATE` (when there's a matching existing value) or an `INSERT` + * (when there isn't) + * @param options Optionally, an object with any of the keys `updateColumns`, + * `noNullUpdateColumns` and `updateValues` (see documentation). + */ +export const upsert: UpsertSignatures = genericUpsert; + /* === update === */ -interface UpdateSignatures { - , E extends ExtrasOption>( +interface UpdateSignatures { + , C extends ColumnsOption, E extends ExtrasOption>( table: T, - values: UpdatableForTable, - where: WhereableForTable | SQLFragment, - options?: ReturningOptionsForTable - ): SQLFragment[]>; + values: S['Updatable'], + where: S['Whereable'] | SQLFragment, + options?: ReturningOptionsForTable + ): SQLFragment[]>; } -/** - * Generate an `UPDATE` query `SQLFragment`. - * @param table The table to update - * @param values An `Updatable` of the new values with which to update the table - * @param where A `Whereable` (or `SQLFragment`) defining which rows to update - */ -export const update: UpdateSignatures = function ( - table: Table, - values: Updatable, - where: Whereable | SQLFragment, - options?: ReturningOptionsForTable, ExtrasOption
> +const genericUpdate = function ( + table: GenericSQLStructure['Table'], + values: GenericSQLStructure['Updatable'], + where: GenericSQLStructure['Whereable'] | SQLFragment, + options?: ReturningOptionsForTable, ExtrasOption> ): SQLFragment { // note: the ROW() constructor below is required in Postgres 10+ if we're updating a single column @@ -306,150 +298,168 @@ export const update: UpdateSignatures = function ( const returningSQL = SQLForColumnsOfTable(options?.returning, table), extrasSQL = SQLForExtras(options?.extras), - query = sql`UPDATE ${table} SET (${cols(values)}) = ROW(${vals(values)}) WHERE ${where} RETURNING ${returningSQL}${extrasSQL} AS result`; + query = sql`UPDATE ${table} SET (${cols(values)}) = ROW(${vals(values)}) WHERE ${where} RETURNING ${returningSQL}${extrasSQL} AS result`; query.runResultTransform = (qr) => qr.rows.map(r => r.result); return query; }; +/** + * Generate an `UPDATE` query `SQLFragment`. + * @param table The table to update + * @param values An `Updatable` of the new values with which to update the table + * @param where A `Whereable` (or `SQLFragment`) defining which rows to update + */ +export const update: UpdateSignatures = genericUpdate; + /* === delete === */ -export interface DeleteSignatures { - , E extends ExtrasOption>( +export interface DeleteSignatures { + , C extends ColumnsOption, E extends ExtrasOption>( table: T, - where: WhereableForTable | SQLFragment, - options?: ReturningOptionsForTable - ): SQLFragment[]>; + where: S['Whereable'] | SQLFragment, + options?: ReturningOptionsForTable + ): SQLFragment[]>; } -/** - * Generate an `DELETE` query `SQLFragment` (plain 'delete' is a reserved word) - * @param table The table to delete from - * @param where A `Whereable` (or `SQLFragment`) defining which rows to delete - */ -export const deletes: DeleteSignatures = function ( - table: Table, - where: Whereable | SQLFragment, - options?: ReturningOptionsForTable, ExtrasOption
> +const genericDeletes = function ( + table: GenericSQLStructure['Table'], + where: GenericSQLStructure['Whereable'] | SQLFragment, + options?: ReturningOptionsForTable, ExtrasOption> ): SQLFragment { const returningSQL = SQLForColumnsOfTable(options?.returning, table), extrasSQL = SQLForExtras(options?.extras), - query = sql`DELETE FROM ${table} WHERE ${where} RETURNING ${returningSQL}${extrasSQL} AS result`; + query = sql`DELETE FROM ${table} WHERE ${where} RETURNING ${returningSQL}${extrasSQL} AS result`; query.runResultTransform = (qr) => qr.rows.map(r => r.result); return query; }; +/** + * Generate an `DELETE` query `SQLFragment` (plain 'delete' is a reserved word) + * @param table The table to delete from + * @param where A `Whereable` (or `SQLFragment`) defining which rows to delete + */ +export const deletes: DeleteSignatures = genericDeletes; + /* === truncate === */ type TruncateIdentityOpts = 'CONTINUE IDENTITY' | 'RESTART IDENTITY'; type TruncateForeignKeyOpts = 'RESTRICT' | 'CASCADE'; -interface TruncateSignatures { - (table: Table | Table[]): SQLFragment; - (table: Table | Table[], optId: TruncateIdentityOpts): SQLFragment; - (table: Table | Table[], optFK: TruncateForeignKeyOpts): SQLFragment; - (table: Table | Table[], optId: TruncateIdentityOpts, optFK: TruncateForeignKeyOpts): SQLFragment; +interface TruncateSignatures { + (table: T | T[]): SQLFragment; + (table: T | T[], optId: TruncateIdentityOpts): SQLFragment; + (table: T | T[], optFK: TruncateForeignKeyOpts): SQLFragment; + (table: T | T[], optId: TruncateIdentityOpts, optFK: TruncateForeignKeyOpts): SQLFragment; } -/** - * Generate a `TRUNCATE` query `SQLFragment`. - * @param table The table (or array thereof) to truncate - * @param opts Options: 'CONTINUE IDENTITY'/'RESTART IDENTITY' and/or - * 'RESTRICT'/'CASCADE' - */ -export const truncate: TruncateSignatures = function ( - table: Table | Table[], +const genericTruncate = function ( + table: GenericSQLStructure['Table'] | GenericSQLStructure['Table'][], ...opts: string[] ): SQLFragment { if (!Array.isArray(table)) table = [table]; const tables = mapWithSeparator(table, sql`, `, t => t), - query = sql`TRUNCATE ${tables}${raw((opts.length ? ' ' : '') + opts.join(' '))}`; + query = sql`TRUNCATE ${tables}${raw((opts.length ? ' ' : '') + opts.join(' '))}`; return query; }; +/** + * Generate a `TRUNCATE` query `SQLFragment`. + * @param table The table (or array thereof) to truncate + * @param opts Options: 'CONTINUE IDENTITY'/'RESTART IDENTITY' and/or + * 'RESTRICT'/'CASCADE' + */ +export const truncate: TruncateSignatures = genericTruncate; + /* === select === */ -interface OrderSpecForTable { - by: SQLForTable; +interface OrderSpecForTable { + by: SQLForStructure; direction: 'ASC' | 'DESC'; nulls?: 'FIRST' | 'LAST'; } -export interface SelectLockingOptions { +export interface SelectLockingOptions { for: 'UPDATE' | 'NO KEY UPDATE' | 'SHARE' | 'KEY SHARE'; - of?: Table | Table[]; + of?: O['Table'] | O['Table'][]; wait?: 'NOWAIT' | 'SKIP LOCKED'; } export interface SelectOptionsForTable< - T extends Table, - C extends ColumnsOption, + S extends GenericSQLStructure, + O extends GenericSQLStructure, + C extends ColumnsOption, L extends LateralOption, - E extends ExtrasOption, + E extends ExtrasOption, > { - distinct?: boolean | ColumnForTable | ColumnForTable[] | SQLFragment; - order?: OrderSpecForTable | OrderSpecForTable[]; + distinct?: boolean | Column | Column[] | SQLFragment; + order?: OrderSpecForTable | OrderSpecForTable[]; limit?: number; offset?: number; withTies?: boolean; columns?: C; extras?: E; - groupBy?: ColumnForTable | ColumnForTable[] | SQLFragment; - having?: WhereableForTable | SQLFragment; + groupBy?: Column | Column[] | SQLFragment; + having?: S['Whereable'] | SQLFragment; lateral?: L; alias?: string; - lock?: SelectLockingOptions | SelectLockingOptions[]; + lock?: SelectLockingOptions | SelectLockingOptions[]; }; type SelectReturnTypeForTable< - T extends Table, - C extends ColumnsOption, + S extends GenericSQLStructure, + C extends ColumnsOption, L extends LateralOption, - E extends ExtrasOption, + E extends ExtrasOption, > = - (undefined extends L ? ReturningTypeForTable : - L extends SQLFragmentMap ? ReturningTypeForTable & LateralResult : + (undefined extends L ? ReturningTypeForTable : + L extends SQLFragmentMap ? ReturningTypeForTable & LateralResult : L extends SQLFragment ? RunResultForSQLFragment : never); export enum SelectResultMode { Many, One, ExactlyOne, Numeric } export type FullSelectReturnTypeForTable< - T extends Table, - C extends ColumnsOption, + S extends GenericSQLStructure, + C extends ColumnsOption, L extends LateralOption, - E extends ExtrasOption, + E extends ExtrasOption, M extends SelectResultMode, > = { - [SelectResultMode.Many]: SelectReturnTypeForTable[]; - [SelectResultMode.ExactlyOne]: SelectReturnTypeForTable; - [SelectResultMode.One]: SelectReturnTypeForTable | undefined; + [SelectResultMode.Many]: SelectReturnTypeForTable[]; + [SelectResultMode.ExactlyOne]: SelectReturnTypeForTable; + [SelectResultMode.One]: SelectReturnTypeForTable | undefined; [SelectResultMode.Numeric]: number; }[M]; -export interface SelectSignatures { - , +export interface SelectSignatures < + BaseSQLStructure extends GenericSQLStructure, + OtherSQLStructure extends GenericSQLStructure + > { + , + O extends OtherSQLStructure, + C extends ColumnsOption, L extends LateralOption, - E extends ExtrasOption, + E extends ExtrasOption, M extends SelectResultMode = SelectResultMode.Many >( table: T, - where: WhereableForTable | SQLFragment | AllType, - options?: SelectOptionsForTable, + where: S['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, mode?: M, aggregate?: string, - ): SQLFragment>; + ): SQLFragment>; } export class NotExactlyOneError extends Error { @@ -463,31 +473,10 @@ export class NotExactlyOneError extends Error { } } -/** - * Generate a `SELECT` query `SQLFragment`. This can be nested with other - * `select`/`selectOne`/`count` queries using the `lateral` option. - * @param table The table to select from - * @param where A `Whereable` or `SQLFragment` defining the rows to be selected, - * or `all` - * @param options Options object. Keys (all optional) are: - * * `columns` — an array of column names: only these columns will be returned - * * `order` – an array of `OrderSpec` objects, such as - * `{ by: 'column', direction: 'ASC' }` - * * `limit` and `offset` – numbers: apply this limit and offset to the query - * * `lateral` — either an object mapping keys to nested `select`/`selectOne`/ - * `count` queries to be `LATERAL JOIN`ed, or a single `select`/`selectOne`/ - * `count` query whose result will be passed through directly as the result of - * the containing query - * * `alias` — table alias (string): required if using `lateral` to join a table - * to itself - * * `extras` — an object mapping key(s) to `SQLFragment`s, so that derived - * quantities can be included in the JSON result - * @param mode (Used internally by `selectOne` and `count`) - */ -export const select: SelectSignatures = function ( - table: Table, - where: Whereable | SQLFragment | AllType = all, - options: SelectOptionsForTable, LateralOption, ExtrasOption
>, ExtrasOption
> = {}, +const genericSelect = function ( + table: GenericSQLStructure['Table'], + where: GenericSQLStructure['Whereable'] | SQLFragment | AllType = all, + options: SelectOptionsForTable, LateralOption, ExtrasOption>, ExtrasOption> = {}, mode: SelectResultMode = SelectResultMode.Many, aggregate: string = 'count', ) { @@ -505,30 +494,30 @@ export const select: SelectSignatures = function ( colsSQL = lateral instanceof SQLFragment ? [] : mode === SelectResultMode.Numeric ? (columns ? sql`${raw(aggregate)}(${cols(columns)})` : sql`${raw(aggregate)}(${alias}.*)`) : - SQLForColumnsOfTable(columns, alias as Table), + SQLForColumnsOfTable(columns, alias as GenericSQLStructure['Table']), colsExtraSQL = lateral instanceof SQLFragment || mode === SelectResultMode.Numeric ? [] : SQLForExtras(extras), colsLateralSQL = lateral === undefined || mode === SelectResultMode.Numeric ? [] : lateral instanceof SQLFragment ? sql`"ljoin_passthru".result` : sql` || jsonb_build_object(${mapWithSeparator( Object.keys(lateral).sort(), sql`, `, (k, i) => sql`${param(k)}::text, "ljoin_${raw(String(i))}".result`)})`, allColsSQL = sql`${colsSQL}${colsExtraSQL}${colsLateralSQL}`, - whereSQL = where === all ? [] : sql` WHERE ${where}`, + whereSQL = where === all ? [] : sql` WHERE ${where}`, groupBySQL = !groupBy ? [] : sql` GROUP BY ${groupBy instanceof SQLFragment || typeof groupBy === 'string' ? groupBy : cols(groupBy)}`, havingSQL = !having ? [] : sql` HAVING ${having}`, orderSQL = order === undefined ? [] : - sql` ORDER BY ${mapWithSeparator(order as OrderSpecForTable
[], sql`, `, o => { // `as` clause is required when TS not strict + sql` ORDER BY ${mapWithSeparator(order as OrderSpecForTable[], sql`, `, o => { // `as` clause is required when TS not strict if (!['ASC', 'DESC'].includes(o.direction)) throw new Error(`Direction must be ASC/DESC, not '${o.direction}'`); if (o.nulls && !['FIRST', 'LAST'].includes(o.nulls)) throw new Error(`Nulls must be FIRST/LAST/undefined, not '${o.nulls}'`); - return sql`${o.by} ${raw(o.direction)}${o.nulls ? sql` NULLS ${raw(o.nulls)}` : []}`; + return sql`${o.by} ${raw(o.direction)}${o.nulls ? sql` NULLS ${raw(o.nulls)}` : []}`; })}`, limitSQL = allOptions.limit === undefined ? [] : allOptions.withTies ? sql` FETCH FIRST ${param(allOptions.limit)} ROWS WITH TIES` : sql` LIMIT ${param(allOptions.limit)}`, // compatibility with pg pre-10.5; and fewer bytes! offsetSQL = allOptions.offset === undefined ? [] : sql` OFFSET ${param(allOptions.offset)}`, // pg is lax about OFFSET following FETCH, and we exploit that - lockSQL = lock === undefined ? [] : (lock as SelectLockingOptions[]).map(lock => { // `as` clause is required when TS not strict + lockSQL = lock === undefined ? [] : (lock as SelectLockingOptions[]).map(lock => { // `as` clause is required when TS not strict const ofTables = lock.of === undefined || Array.isArray(lock.of) ? lock.of : [lock.of], - ofClause = ofTables === undefined ? [] : sql` OF ${mapWithSeparator(ofTables as Table[], sql`, `, t => t)}`; // `as` clause is required when TS not strict + ofClause = ofTables === undefined ? [] : sql` OF ${mapWithSeparator(ofTables as GenericSQLStructure['Table'][], sql`, `, t => t)}`; // `as` clause is required when TS not strict return sql` FOR ${raw(lock.for)}${ofClause}${lock.wait ? sql` ${raw(lock.wait)}` : []}`; }), lateralSQL = lateral === undefined ? [] : @@ -543,10 +532,10 @@ export const select: SelectSignatures = function ( }); const - rowsQuery = sql`SELECT${distinctSQL} ${allColsSQL} AS result FROM ${table}${tableAliasSQL}${lateralSQL}${whereSQL}${groupBySQL}${havingSQL}${orderSQL}${limitSQL}${offsetSQL}${lockSQL}`, + rowsQuery = sql`SELECT${distinctSQL} ${allColsSQL} AS result FROM ${table}${tableAliasSQL}${lateralSQL}${whereSQL}${groupBySQL}${havingSQL}${orderSQL}${limitSQL}${offsetSQL}${lockSQL}`, query = mode !== SelectResultMode.Many ? rowsQuery : // we need the aggregate to sit in a sub-SELECT in order to keep ORDER and LIMIT working as usual - sql`SELECT coalesce(jsonb_agg(result), '[]') AS result FROM (${rowsQuery}) AS ${raw(`"sq_${alias}"`)}`; + sql`SELECT coalesce(jsonb_agg(result), '[]') AS result FROM (${rowsQuery}) AS ${raw(`"sq_${alias}"`)}`; query.runResultTransform = @@ -567,22 +556,66 @@ export const select: SelectSignatures = function ( return query; }; +/** + * Generate a `SELECT` query `SQLFragment`. This can be nested with other + * `select`/`selectOne`/`count` queries using the `lateral` option. + * @param table The table to select from + * @param where A `Whereable` or `SQLFragment` defining the rows to be selected, + * or `all` + * @param options Options object. Keys (all optional) are: + * * `columns` — an array of column names: only these columns will be returned + * * `order` – an array of `OrderSpec` objects, such as + * `{ by: 'column', direction: 'ASC' }` + * * `limit` and `offset` – numbers: apply this limit and offset to the query + * * `lateral` — either an object mapping keys to nested `select`/`selectOne`/ + * `count` queries to be `LATERAL JOIN`ed, or a single `select`/`selectOne`/ + * `count` query whose result will be passed through directly as the result of + * the containing query + * * `alias` — table alias (string): required if using `lateral` to join a table + * to itself + * * `extras` — an object mapping key(s) to `SQLFragment`s, so that derived + * quantities can be included in the JSON result + * @param mode (Used internally by `selectOne` and `count`) + */ + +export const select: SelectSignatures = genericSelect; + /* === selectOne === */ -export interface SelectOneSignatures { +export interface SelectOneSignatures < + BaseSQLStructure extends GenericSQLStructure, + OtherSQLStructure extends GenericSQLStructure + > { < - T extends Table, - C extends ColumnsOption, + T extends BaseSQLStructure['Table'], + S extends Extract, + O extends OtherSQLStructure, + C extends ColumnsOption, L extends LateralOption, - E extends ExtrasOption, + E extends ExtrasOption, >( table: T, - where: WhereableForTable | SQLFragment | AllType, - options?: SelectOptionsForTable, - ): SQLFragment>; + where: S['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, + ): SQLFragment>; } +const genericSelectOne = function ( + table: GenericSQLStructure['Table'], + where: GenericSQLStructure['Whereable'] | SQLFragment | AllType, + options: SelectOptionsForTable, LateralOption, ExtrasOption>, ExtrasOption> = {} +): SQLFragment { + // you might argue that 'selectOne' offers little that you can't get with + // destructuring assignment and plain 'select' + // -- e.g.let[x] = async select(...).run(pool); -- but something worth having + // is '| undefined' in the return signature, because the result of indexing + // never includes undefined (until 4.1 and --noUncheckedIndexedAccess) + // (see https://github.com/Microsoft/TypeScript/issues/13778) + + return genericSelect(table, where, options, SelectResultMode.One); +}; + /** * Generate a `SELECT` query `SQLFragment` that returns only a single result (or * undefined). A `LIMIT 1` clause is added automatically. This can be nested with @@ -592,33 +625,37 @@ export interface SelectOneSignatures { * or `all` * @param options Options object. See documentation for `select` for details. */ -export const selectOne: SelectOneSignatures = function (table, where, options = {}) { - // you might argue that 'selectOne' offers little that you can't get with - // destructuring assignment and plain 'select' - // -- e.g.let[x] = async select(...).run(pool); -- but something worth having - // is '| undefined' in the return signature, because the result of indexing - // never includes undefined (until 4.1 and --noUncheckedIndexedAccess) - // (see https://github.com/Microsoft/TypeScript/issues/13778) - - return select(table, where, options, SelectResultMode.One); -}; +export const selectOne: SelectOneSignatures = genericSelectOne; /* === selectExactlyOne === */ -export interface SelectExactlyOneSignatures { +export interface SelectExactlyOneSignatures < + BaseSQLStructure extends GenericSQLStructure, + OtherSQLStructure extends GenericSQLStructure + > { < - T extends Table, - C extends ColumnsOption, + T extends BaseSQLStructure['Table'], + S extends Extract, + O extends OtherSQLStructure, + C extends ColumnsOption, L extends LateralOption, - E extends ExtrasOption, + E extends ExtrasOption, >( table: T, - where: WhereableForTable | SQLFragment | AllType, - options?: SelectOptionsForTable, - ): SQLFragment>; + where: S['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, + ): SQLFragment>; } +export const genericSelectExactlyOne = function ( + table: GenericSQLStructure['Table'], + where: GenericSQLStructure['Whereable'] | SQLFragment | AllType, + options: SelectOptionsForTable, LateralOption, ExtrasOption>, ExtrasOption> = {} +): SQLFragment { + return genericSelect(table, where, options, SelectResultMode.ExactlyOne); +}; + /** * Generate a `SELECT` query `SQLFragment` that returns a single result or * throws an error. A `LIMIT 1` clause is added automatically. This can be @@ -630,26 +667,37 @@ export interface SelectExactlyOneSignatures { * @param options Options object. See documentation for `select` for details. */ -export const selectExactlyOne: SelectExactlyOneSignatures = function (table, where, options = {}) { - return select(table, where, options, SelectResultMode.ExactlyOne); -}; +export const selectExactlyOne: SelectExactlyOneSignatures = genericSelectExactlyOne; /* === count, sum, avg === */ -export interface NumericAggregateSignatures { +export interface NumericAggregateSignatures < + BaseSQLStructure extends GenericSQLStructure, + OtherSQLStructure extends GenericSQLStructure + > { < - T extends Table, - C extends ColumnsOption, + T extends BaseSQLStructure['Table'], + S extends Extract, + O extends OtherSQLStructure, + C extends ColumnsOption, L extends LateralOption, - E extends ExtrasOption, + E extends ExtrasOption, >( table: T, - where: WhereableForTable | SQLFragment | AllType, - options?: SelectOptionsForTable, + where: S['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, ): SQLFragment; } +export const genericCount = function ( + table: GenericSQLStructure['Table'], + where: GenericSQLStructure['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, LateralOption, ExtrasOption>, ExtrasOption> +) { + return genericSelect(table, where, options, SelectResultMode.Numeric); +}; + /** * Generate a `SELECT` query `SQLFragment` that returns a count. This can be * nested in other `select`/`selectOne` queries using their `lateral` option. @@ -658,8 +706,14 @@ export interface NumericAggregateSignatures { * or `all` * @param options Options object. Useful keys may be: `columns`, `alias`. */ -export const count: NumericAggregateSignatures = function (table, where, options?) { - return select(table, where, options, SelectResultMode.Numeric); +export const count: NumericAggregateSignatures = genericCount; + +export const genericSum = function ( + table: GenericSQLStructure['Table'], + where: GenericSQLStructure['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, LateralOption, ExtrasOption>, ExtrasOption> +) { + return genericSelect(table, where, options, SelectResultMode.Numeric, 'sum'); }; /** @@ -670,8 +724,14 @@ export const count: NumericAggregateSignatures = function (table, where, options * aggregated, or `all` * @param options Options object. Useful keys may be: `columns`, `alias`. */ -export const sum: NumericAggregateSignatures = function (table, where, options?) { - return select(table, where, options, SelectResultMode.Numeric, 'sum'); +export const sum: NumericAggregateSignatures = genericSum; + +export const genericAvg = function ( + table: GenericSQLStructure['Table'], + where: GenericSQLStructure['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, LateralOption, ExtrasOption>, ExtrasOption> +) { + return genericSelect(table, where, options, SelectResultMode.Numeric, 'avg'); }; /** @@ -683,8 +743,14 @@ export const sum: NumericAggregateSignatures = function (table, where, options?) * aggregated, or `all` * @param options Options object. Useful keys may be: `columns`, `alias`. */ -export const avg: NumericAggregateSignatures = function (table, where, options?) { - return select(table, where, options, SelectResultMode.Numeric, 'avg'); +export const avg: NumericAggregateSignatures = genericAvg; + +export const genericMin = function ( + table: GenericSQLStructure['Table'], + where: GenericSQLStructure['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, LateralOption, ExtrasOption>, ExtrasOption> +) { + return genericSelect(table, where, options, SelectResultMode.Numeric, 'min'); }; /** @@ -696,8 +762,14 @@ export const avg: NumericAggregateSignatures = function (table, where, options?) * aggregated, or `all` * @param options Options object. Useful keys may be: `columns`, `alias`. */ -export const min: NumericAggregateSignatures = function (table, where, options?) { - return select(table, where, options, SelectResultMode.Numeric, 'min'); +export const min: NumericAggregateSignatures = genericMin; + +export const genericMax = function ( + table: GenericSQLStructure['Table'], + where: GenericSQLStructure['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, LateralOption, ExtrasOption>, ExtrasOption> +) { + return genericSelect(table, where, options, SelectResultMode.Numeric, 'max'); }; /** @@ -709,6 +781,169 @@ export const min: NumericAggregateSignatures = function (table, where, options?) * aggregated, or `all` * @param options Options object. Useful keys may be: `columns`, `alias`. */ -export const max: NumericAggregateSignatures = function (table, where, options?) { - return select(table, where, options, SelectResultMode.Numeric, 'max'); +export const max: NumericAggregateSignatures = genericMax; + + +/* === table, tables === */ + +/* + * To allow partial type argument inference, split shortcuts into 2 chained + * functions. The first part (table) uses overridable type arguments and + * the second one (insert, select, ...) uses type arguments for inference. + * https://github.com/microsoft/TypeScript/issues/26242 + */ + +interface TableNumericAggregateSignatures < + S extends GenericSQLStructure, + O extends GenericSQLStructure + > { + < + C extends ColumnsOption, + L extends LateralOption, + E extends ExtrasOption, + >( + where: S['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, + ): SQLFragment; +} + +interface TableReturn < + S extends GenericSQLStructure, + O extends GenericSQLStructure +> { + insert, E extends ExtrasOption>( + values: S['Insertable'], + options?: ReturningOptionsForTable + ): SQLFragment>; + + insert, E extends ExtrasOption>( + values: S['Insertable'][], + options?: ReturningOptionsForTable + ): SQLFragment[]>; + + upsert< + C extends ColumnsOption, + E extends ExtrasOption, + UC extends UpdateColumns | undefined, + RA extends UpsertReportAction | undefined + >( + values: S['Insertable'], + conflictTarget: UpsertConflictTargetForStructure, + options?: UpsertOptions + ): SQLFragment | (UC extends never[] ? undefined : never)>; + + upsert< + C extends ColumnsOption, + E extends ExtrasOption, + UC extends UpdateColumns | undefined, + RA extends UpsertReportAction | undefined + >( + values: S['Insertable'][], + conflictTarget: UpsertConflictTargetForStructure, + options?: UpsertOptions + ): SQLFragment[]>; + + update, E extends ExtrasOption>( + values: S['Updatable'], + where: S['Whereable'] | SQLFragment, + options?: ReturningOptionsForTable + ): SQLFragment[]>; + + deletes, E extends ExtrasOption>( + where: S['Whereable'] | SQLFragment, + options?: ReturningOptionsForTable + ): SQLFragment[]>; + + truncate(): SQLFragment; + truncate(optId: TruncateIdentityOpts): SQLFragment; + truncate(optFK: TruncateForeignKeyOpts): SQLFragment; + truncate(optId: TruncateIdentityOpts, optFK: TruncateForeignKeyOpts): SQLFragment; + + select, + L extends LateralOption, + E extends ExtrasOption, + M extends SelectResultMode = SelectResultMode.Many + >( + where: S['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, + mode?: M, + aggregate?: string, + ): SQLFragment>; + + selectOne< + C extends ColumnsOption, + L extends LateralOption, + E extends ExtrasOption, + >( + where: S['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, + ): SQLFragment>; + + selectExactlyOne< + C extends ColumnsOption, + L extends LateralOption, + E extends ExtrasOption, + >( + where: S['Whereable'] | SQLFragment | AllType, + options?: SelectOptionsForTable, + ): SQLFragment>; + + count: TableNumericAggregateSignatures; + sum: TableNumericAggregateSignatures; + avg: TableNumericAggregateSignatures; + max: TableNumericAggregateSignatures; + min: TableNumericAggregateSignatures; +} + +interface TableSignatures { + < + S extends GenericSQLStructure = SQLStructure, + O extends GenericSQLStructure = SQLStructure + >(tables: S['Table'] + ): TableReturn; +} + +/** + * Creates a query generator for the specified table. Allows manual override of + * type arguments. + * @param table The table to query + */ +export const table: TableSignatures = (table: S['Table']): TableReturn => { + return { + insert: genericInsert.bind(undefined, table), + upsert: genericUpsert.bind(undefined, table), + update: genericUpdate.bind(undefined, table), + deletes: genericDeletes.bind(undefined, table), + truncate: genericTruncate.bind(undefined, table), + select: genericSelect.bind(undefined, table), + selectOne: genericSelectOne.bind(undefined, table), + selectExactlyOne: genericSelectExactlyOne.bind(undefined, table), + count: genericCount.bind(undefined, table), + sum: genericSum.bind(undefined, table), + avg: genericAvg.bind(undefined, table), + min: genericMin.bind(undefined, table), + max: genericMax.bind(undefined, table) + }; +}; + +interface TablesReturn { + truncate(): SQLFragment; + truncate(optId: TruncateIdentityOpts): SQLFragment; + truncate(optFK: TruncateForeignKeyOpts): SQLFragment; + truncate(optId: TruncateIdentityOpts, optFK: TruncateForeignKeyOpts): SQLFragment; +} + +interface TablesSignatures { + (tables: S['Table'][]): TablesReturn; +} + +/** + * Creates a query generator for the specified tables. Allows manual override of + * type arguments. + * @param tables The tables to query + */ +export const tables: TablesSignatures = (tables: GenericSQLStructure['Table'][]): TablesReturn => { + return { + truncate: genericTruncate.bind(undefined, tables) + }; }; diff --git a/src/generate/config.ts b/src/generate/config.ts index af000db..612d274 100644 --- a/src/generate/config.ts +++ b/src/generate/config.ts @@ -23,6 +23,11 @@ export interface OptionalConfig { customTypesTransform: 'PgMy_type' | 'my_type' | 'PgMyType' | ((s: string) => string); columnOptions: ColumnOptions; schemaJSDoc: boolean; + /** + * Prefix to apply to the table or a function to create the name from the + * table + */ + globalStructureName: string | ((table: string) => string); } interface SchemaRules { @@ -46,7 +51,7 @@ export type CompleteConfig = RequiredConfig & OptionalConfig; const defaultConfig: OptionalConfig = { outDir: '.', - outExt: '.d.ts', + outExt: '.ts', schemas: { public: { include: '*', exclude: [] } }, debugListener: false, progressListener: false, @@ -54,6 +59,7 @@ const defaultConfig: OptionalConfig = { customTypesTransform: 'PgMy_type', columnOptions: {}, schemaJSDoc: true, + globalStructureName: (table: string) => table }; export const moduleRoot = () => { diff --git a/src/generate/tables.ts b/src/generate/tables.ts index b7de61d..212a82a 100644 --- a/src/generate/tables.ts +++ b/src/generate/tables.ts @@ -155,6 +155,10 @@ export const definitionForRelationInSchema = async ( if (isUpdatable) updatables.push(`${columnDoc}${column}?: ${updatableTypes} | db.SQLFragment;`); }); + const globalStructureName = typeof config.globalStructureName === 'function' + ? config.globalStructureName(rel.name) + : `${config.globalStructureName}${rel.name}`; + const result = await queryFn({ text: ` @@ -182,30 +186,35 @@ export const definitionForRelationInSchema = async ( * - ${friendlyRelType} in database */` : ``, tableDef = `${tableComment} -export namespace ${rel.name} { - export type Table = '${rel.name}'; - export interface Selectable { +export interface ${rel.name} { + Schema: '${schemaName}'; + Table: '${rel.name}'; + Selectable: { ${selectables.join('\n ')} - } - export interface JSONSelectable { + }; + JSONSelectable: { ${JSONSelectables.join('\n ')} - } - export interface Whereable { + }; + Whereable: { ${whereables.join('\n ')} - } - export interface Insertable { + }; + Insertable: { ${insertables.length > 0 ? insertables.join('\n ') : `[key: string]: never;`} - } - export interface Updatable { + }; + Updatable: { ${updatables.length > 0 ? updatables.join('\n ') : `[key: string]: never;`} + }; + UniqueIndex: ${uniqueIndexes.length > 0 + ? uniqueIndexes.map(ui => "'" + ui.indexname + "'").join(' | ') + : 'never'}; +} + +declare global { + namespace Zapatos { + interface StructureMap { + ${globalStructureName}: ${rel.name}; + } } - export type UniqueIndex = ${uniqueIndexes.length > 0 ? - uniqueIndexes.map(ui => "'" + ui.indexname + "'").join(' | ') : - 'never'}; - export type Column = keyof Selectable; - export type OnlyCols = Pick; - export type SQLExpression = db.GenericSQLExpression | db.ColumnNames | db.ColumnValues | Table | Whereable | Column; - export type SQL = SQLExpression | SQLExpression[]; }`; return tableDef; }; @@ -228,25 +237,19 @@ const mappedUnion = (arr: Relation[], fn: (name: string) => string) => export const crossTableTypesForTables = (relations: Relation[]) => `${relations.length === 0 ? '\n// `never` rather than `any` types would be more accurate in this no-tables case, but they stop `shortcuts.ts` compiling\n' : '' } -export type Table = ${mappedUnion(relations, name => `${name}.Table`)}; -export type Selectable = ${mappedUnion(relations, name => `${name}.Selectable`)}; -export type JSONSelectable = ${mappedUnion(relations, name => `${name}.JSONSelectable`)}; -export type Whereable = ${mappedUnion(relations, name => `${name}.Whereable`)}; -export type Insertable = ${mappedUnion(relations, name => `${name}.Insertable`)}; -export type Updatable = ${mappedUnion(relations, name => `${name}.Updatable`)}; -export type UniqueIndex = ${mappedUnion(relations, name => `${name}.UniqueIndex`)}; -export type Column = ${mappedUnion(relations, name => `${name}.Column`)}; -export type AllBaseTables = [${relations.filter(rel => rel.type === 'table').map(rel => `${rel.name}.Table`).join(', ')}]; -export type AllForeignTables = [${relations.filter(rel => rel.type === 'fdw').map(rel => `${rel.name}.Table`).join(', ')}]; -export type AllViews = [${relations.filter(rel => rel.type === 'view').map(rel => `${rel.name}.Table`).join(', ')}]; -export type AllMaterializedViews = [${relations.filter(rel => rel.type === 'mview').map(rel => `${rel.name}.Table`).join(', ')}]; -export type AllTablesAndViews = [${relations.map(rel => `${rel.name}.Table`).join(', ')}]; - -${['Selectable', 'JSONSelectable', 'Whereable', 'Insertable', 'Updatable', 'UniqueIndex', 'Column', 'SQL'].map(thingable => ` -export type ${thingable}ForTable = ${relations.length === 0 ? 'any' : `{${relations.map(rel => ` - ${rel.name}: ${rel.name}.${thingable};`).join('')} -}[T]`}; -`).join('')}`; +export type Table = ${mappedUnion(relations, name => `${name}['Table']`)}; +export type Selectable = ${mappedUnion(relations, name => `${name}['Selectable']`)}; +export type JSONSelectable = ${mappedUnion(relations, name => `${name}['JSONSelectable']`)}; +export type Whereable = ${mappedUnion(relations, name => `${name}['Whereable']`)}; +export type Insertable = ${mappedUnion(relations, name => `${name}['Insertable']`)}; +export type Updatable = ${mappedUnion(relations, name => `${name}['Updatable']`)}; +export type UniqueIndex = ${mappedUnion(relations, name => `${name}['UniqueIndex']`)}; +export type Column = ${mappedUnion(relations, name => `db.Column<${name}>`)}; +export type AllBaseTables = [${relations.filter(rel => rel.type === 'table').map(rel => `${rel.name}['Table']`).join(', ')}]; +export type AllForeignTables = [${relations.filter(rel => rel.type === 'fdw').map(rel => `${rel.name}['Table']`).join(', ')}]; +export type AllViews = [${relations.filter(rel => rel.type === 'view').map(rel => `${rel.name}['Table']`).join(', ')}]; +export type AllMaterializedViews = [${relations.filter(rel => rel.type === 'mview').map(rel => `${rel.name}['Table']`).join(', ')}]; +export type AllTablesAndViews = [${relations.map(rel => `${rel.name}['Table']`).join(', ')}];`; const createColumnDoc = (schemaName: string, rel: Relation, columnDetails: Record) => { diff --git a/src/generate/tsOutput.ts b/src/generate/tsOutput.ts index 87e3f11..e895c71 100644 --- a/src/generate/tsOutput.ts +++ b/src/generate/tsOutput.ts @@ -30,6 +30,10 @@ ${declarations.replace(/^(?=[ \t]*\S)/gm, ' ')} } `; +const outputBody = (declarations: string) => ` +${declarations.replace(/^(?=[ \t]*\S)/gm, '')} +`; + const customTypeHeader = `/* ** Please edit this file as needed ** It's been generated by Zapatos as a custom type definition placeholder, and won't be overwritten @@ -88,7 +92,7 @@ export const tsForConfig = async (config: CompleteConfig, debug: (s: string) => schemaTables = schemaData.map(r => r.tables), allTables = ([] as Relation[]).concat(...schemaTables).sort((a, b) => a.name.localeCompare(b.name)), hasCustomTypes = Object.keys(customTypes).length > 0, - ts = header() + declareModule('zapatos/schema', + ts = header() + outputBody( `\nimport type * as db from 'zapatos/db';\n` + (hasCustomTypes ? `import type * as c from 'zapatos/custom';\n` : ``) + versionCanary + diff --git a/src/typings/zapatos/schema.ts b/src/typings/zapatos/schema.ts deleted file mode 100644 index b578d49..0000000 --- a/src/typings/zapatos/schema.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* -Zapatos: https://jawj.github.io/zapatos/ -Copyright (C) 2020 - 2021 George MacKerron -Released under the MIT licence: see LICENCE file -*/ - -// this file exists only to suppress type errors when compiling the files in src/db - -/* eslint-disable @typescript-eslint/no-unused-vars */ -export interface Updatable { [k: string]: any } -export interface Whereable { [k: string]: any } -export interface Insertable { [k: string]: any } -export type Table = string; -export type Column = string; -export type JSONSelectableForTable = { [k: string]: any }; -export type SelectableForTable = { [k: string]: any }; -export type WhereableForTable = { [k: string]: any }; -export type InsertableForTable = { [k: string]: any }; -export type UpdatableForTable = { [k: string]: any }; -export type ColumnForTable = string; -export type UniqueIndexForTable = string; -export type SQLForTable = any; diff --git a/tsconfig.json b/tsconfig.json index 1d6fcb2..c3db422 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,11 +10,6 @@ "module": "CommonJS", "strict": true, "declaration": true, - "noUnusedLocals": true, - "paths": { - "*": [ - "src/typings/*" - ] - } + "noUnusedLocals": true }, -} \ No newline at end of file +}