From d3d216605885b43776ec04c7b3903d328b0135e3 Mon Sep 17 00:00:00 2001 From: Matthew Lanigan Date: Wed, 7 Dec 2022 19:20:13 -0500 Subject: [PATCH] feat!: overhaul everything to be Zod-based --- src/datasource/DBDataSource.ts | 511 ++++++++++-------- src/datasource/__tests__/DBDataSource.test.ts | 54 +- .../__snapshots__/DBDataSource.test.ts.snap | 8 +- .../__snapshots__/queries.test.ts.snap | 148 ++--- src/datasource/__tests__/finders.test.ts | 36 +- src/datasource/__tests__/integration.test.ts | 246 +++++---- src/datasource/__tests__/loaders.test.ts | 94 ++-- src/datasource/__tests__/queries.test.ts | 177 ++++-- src/datasource/loaders/FinderFactory.ts | 38 +- src/datasource/loaders/LoaderFactory.ts | 182 +++---- src/datasource/loaders/types.ts | 5 +- src/datasource/queries/QueryBuilder.ts | 199 ++++--- src/datasource/queries/types.ts | 19 + src/datasource/queries/utils.ts | 6 +- src/datasource/types.ts | 48 ++ src/generator/Generator.ts | 125 ++++- src/generator/__tests__/Generator.test.ts | 26 +- .../__snapshots__/Generator.test.ts.snap | 341 ++++++------ .../builders/ColumnMetadataBuilder.ts | 46 ++ src/generator/builders/InsertSchemaBuilder.ts | 126 +++++ src/generator/builders/NodeBuilder.ts | 16 + src/generator/builders/SelectSchemaBuilder.ts | 113 ++++ .../builders/SingleNamedImportBuilder.ts | 38 ++ .../builders/TableMetadataBuilder.ts | 89 +++ src/generator/builders/UtilityTypesBuilder.ts | 34 +- src/generator/builders/index.ts | 3 + tsconfig.json | 1 + 27 files changed, 1824 insertions(+), 905 deletions(-) create mode 100644 src/datasource/types.ts create mode 100644 src/generator/builders/ColumnMetadataBuilder.ts create mode 100644 src/generator/builders/InsertSchemaBuilder.ts create mode 100644 src/generator/builders/SelectSchemaBuilder.ts create mode 100644 src/generator/builders/SingleNamedImportBuilder.ts create mode 100644 src/generator/builders/TableMetadataBuilder.ts diff --git a/src/datasource/DBDataSource.ts b/src/datasource/DBDataSource.ts index 6ee6d76f..21ba72dc 100644 --- a/src/datasource/DBDataSource.ts +++ b/src/datasource/DBDataSource.ts @@ -1,19 +1,11 @@ -import { camel, snake } from 'case' +import { camel } from 'case' import DataLoader from 'dataloader' -import { - sql, - DatabasePool, - IdentifierNormalizer, - DatabaseTransactionConnection, -} from 'slonik' +import { sql, DatabasePool, DatabaseTransactionConnection } from 'slonik' export type { DatabasePool } from 'slonik' import { FinderFactory, LoaderFactory } from './loaders' -import QueryBuilder, { - QueryOptions as BuilderOptions, - SelectOptions, -} from './queries/QueryBuilder' +import QueryBuilder, { SelectOptions } from './queries/QueryBuilder' import { AllowSql, CountQueryRowType, @@ -21,80 +13,144 @@ import { ValueOrArray, } from './queries/types' import { AsyncLocalStorage } from 'async_hooks' -import type { z, ZodSchema } from 'zod' -import { isSqlToken } from './queries/utils' +import { z } from 'zod' import { TypedSqlQuery } from '../types' - -export interface QueryOptions - extends BuilderOptions { - eachResult?: LoaderCallback - expected?: 'one' | 'many' | 'maybeOne' | 'any' -} - -export interface KeyNormalizers { - keyToColumn: IdentifierNormalizer - columnToKey: IdentifierNormalizer -} +import { + ExtendedDatabasePool, + QueryOptions, + TableMetadata, + TableSchema, +} from './types' export { DataLoader, sql } -export type LoaderCallback = ( - value: TResultType, - index: number, - array: readonly TResultType[] -) => void - const parseTS = (value: number | string): Date | null => value === null ? null : new Date(value) -const isArray = ( - value: T | U -): value is T => Array.isArray(value) - -interface AsyncStorage { - transaction: DatabaseTransactionConnection - loaderLookups: Array<[Loader, readonly unknown[]]> -} - -type Loader = DataLoader - -interface ExtendedDatabasePool extends DatabasePool { - async: AsyncLocalStorage> -} +// class NewDBDataSource< +// Metadata extends TableMetadata, +// SelectSchema extends TableSchema, +// InsertSchema extends TableSchema +// > { +// constructor( +// pool: DatabasePool, +// protected readonly table: string, +// protected readonly metadata: Metadata, +// protected readonly selectSchema: SelectSchema, +// protected readonly insertSchema: InsertSchema +// ) { +// this.pool = pool as ExtendedDatabasePool +// this.pool.async ||= new AsyncLocalStorage() +// } + +// protected defaultOptions: QueryOptions> = {} +// protected readonly pool: ExtendedDatabasePool + +// private _loaderFactory?: LoaderFactory> +// protected get loaderFactory(): LoaderFactory> { +// this._loaderFactory ||= new LoaderFactory( +// this.getDataByColumn.bind(this), +// this.getDataByMultipleColumns.bind(this), +// { +// ...this.normalizers, +// columnTypes: this.columnTypes, +// } +// ) + +// return this._loaderFactory +// } + +// private finderFactory?: FinderFactory> +// protected get finders(): FinderFactory> { +// this.finderFactory ||= new FinderFactory() +// return this.finderFactory +// } + +// private _queryBuilder?: QueryBuilder +// protected get queryBuilder(): QueryBuilder { +// if (!this._queryBuilder) { +// this._queryBuilder = new QueryBuilder( +// this.table, +// this.metadata, +// this.selectSchema, +// this.defaultOptions +// ) +// } + +// return this._queryBuilder +// } + +// protected async getDataByMultipleColumns< +// TColumnNames extends Array>, +// TArgs extends { [K in TColumnNames[0]]: z.infer[K] } +// >( +// args: ReadonlyArray, +// columns: TColumnNames, +// types: string[], +// loader: DataLoader< +// TArgs, +// z.infer[] | z.infer | undefined +// >, +// options?: QueryOptions> & SelectOptions +// ): Promise[]> { +// const store = this.pool.async.getStore() +// if (store) { +// store.loaderLookups.push([loader, args]) +// } + +// return await this.query( +// this.queryBuilder.multiColumnBatchGet(args, columns, types, options), +// { expected: 'any' } +// ) +// } +// } + +// const DEFAULT = Symbol('DEFAULT') + +// const BlahSchema = z.object({ +// id: z.string(), +// name: z.string(), +// }) + +// const BlahMetadata: TableMetadata<'id' | 'name'> = { +// id: { +// nativeName: 'id', +// nativeType: 'int8', +// }, +// name: { +// nativeName: 'name', +// nativeType: 'text', +// }, +// } + +// const ds = new NewDBDataSource(BlahMetadata, BlahSchema, BlahSchema) export default class DBDataSource< - TRowType, - TInsertType extends { [K in keyof TRowType]?: unknown } = TRowType, - TColumnTypes extends Record = Record< - keyof TRowType, - string - > + Metadata extends TableMetadata, + SelectSchema extends TableSchema, + InsertSchema extends TableSchema > { - protected normalizers: KeyNormalizers = { + protected normalizers = { columnToKey: camel, - keyToColumn: snake, - } + } as const - protected defaultOptions: QueryOptions = {} + protected defaultOptions: QueryOptions> = {} - private _loaders?: LoaderFactory - protected get loaders(): LoaderFactory { + private _loaders?: LoaderFactory> + protected get loaders(): LoaderFactory> { if (!this._loaders) { this._loaders = new LoaderFactory( this.getDataByColumn.bind(this), this.getDataByMultipleColumns.bind(this), - { - ...this.normalizers, - columnTypes: this.columnTypes, - } + this.metadata ) } return this._loaders } - private _finders?: FinderFactory - protected get finders(): FinderFactory { + private _finders?: FinderFactory> + protected get finders(): FinderFactory> { if (!this._finders) { this._finders = new FinderFactory() } @@ -102,13 +158,13 @@ export default class DBDataSource< return this._finders } - private _builder?: QueryBuilder - protected get builder(): QueryBuilder { + private _builder?: QueryBuilder + protected get builder(): QueryBuilder { if (!this._builder) { this._builder = new QueryBuilder( this.table, - this.columnTypes, - this.normalizers.keyToColumn, + this.metadata, + this.selectSchema, this.defaultOptions ) } @@ -116,25 +172,16 @@ export default class DBDataSource< return this._builder } - protected readonly pool: ExtendedDatabasePool + protected readonly pool: ExtendedDatabasePool> constructor( pool: DatabasePool, protected readonly table: string, - /** - * Types of the columns in the database. - * Used to map values for insert and lookups on multiple values. - * - * EVERY DATASOURCE MUST PROVIDE THIS AS STUFF WILL BREAK OTHERWISE. SORRY. - */ - protected readonly columnTypes: TColumnTypes, - protected readonly columnSchemas?: { - [K in keyof TRowType as TColumnTypes[K] extends 'json' | 'jsonb' - ? K - : never]?: ZodSchema - } + protected readonly metadata: Metadata, + protected readonly selectSchema: SelectSchema, + protected readonly insertSchema: InsertSchema ) { - this.pool = pool as ExtendedDatabasePool + this.pool = pool as ExtendedDatabasePool> this.pool.async ||= new AsyncLocalStorage() } @@ -178,8 +225,9 @@ export default class DBDataSource< * @param options Query options */ protected async get( - options: QueryOptions & SelectOptions & { expected: 'one' } - ): Promise + options: QueryOptions> & + SelectOptions & { expected: 'one' } + ): Promise> /** * Possibly find a single row @@ -187,8 +235,9 @@ export default class DBDataSource< * @param options Query options */ protected async get( - options: QueryOptions & SelectOptions & { expected: 'maybeOne' } - ): Promise + options: QueryOptions> & + SelectOptions & { expected: 'maybeOne' } + ): Promise | null> /** * Find multiple rows @@ -196,21 +245,21 @@ export default class DBDataSource< * @param options Query options */ protected async get( - options?: QueryOptions & + options?: QueryOptions> & SelectOptions & { expected?: 'any' | 'many' } - ): Promise + ): Promise[]> protected async get( - options?: QueryOptions & SelectOptions - ): Promise { + options?: QueryOptions> & SelectOptions + ): Promise | readonly z.infer[] | null> { const query = this.builder.select(options) - return await this.query(query, options) + return await this.query(query, options) } protected async count( options?: Omit< - QueryOptions, + QueryOptions, CountQueryRowType>, 'expected' | 'orderBy' | 'groupBy' | 'limit' | 'having' > ): Promise { @@ -222,17 +271,26 @@ export default class DBDataSource< return result.count } - protected async countGroup>( - groupColumns: TGroup & Array, + protected async countGroup< + TGroup extends Array> + >( + groupColumns: TGroup & Array>, options?: Omit< - QueryOptions, + QueryOptions< + CountQueryRowType & { [K in TGroup[0]]: z.infer[K] } + >, 'orderBy' | 'groupBy' | 'limit' | 'having' | 'expected' > ): Promise< - ReadonlyArray + ReadonlyArray< + CountQueryRowType & { [K in TGroup[0]]: z.infer[K] } + > > { const query = this.builder.countGroup(groupColumns, options) - const result = await this.query(query, { + const parser = query.parser + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore i promise + const result = await this.query>(query, { ...options, expected: 'any', }) @@ -245,9 +303,9 @@ export default class DBDataSource< * @param options Query options */ protected async insert( - rows: AllowSql, - options?: QueryOptions & { expected?: undefined } - ): Promise + rows: AllowSql>, + options?: QueryOptions> & { expected?: undefined } + ): Promise> /** * Insert multiple rows @@ -255,9 +313,9 @@ export default class DBDataSource< * @param options Query options */ protected async insert( - rows: Array>, - options?: QueryOptions & { expected?: undefined } - ): Promise + rows: Array>>, + options?: QueryOptions> & { expected?: undefined } + ): Promise[]> /** * Expect a single row to be inserted from the given data @@ -265,9 +323,9 @@ export default class DBDataSource< * @param options Query options */ protected async insert( - rows: ValueOrArray>, - options?: QueryOptions & { expected: 'one' } - ): Promise + rows: ValueOrArray>>, + options?: QueryOptions> & { expected: 'one' } + ): Promise> /** * Expect zero or one rows to be inserted from the given data @@ -275,9 +333,9 @@ export default class DBDataSource< * @param options Query options */ protected async insert( - rows: ValueOrArray>, - options?: QueryOptions & { expected: 'maybeOne' } - ): Promise + rows: ValueOrArray>>, + options?: QueryOptions> & { expected: 'maybeOne' } + ): Promise | null> /** * Insert multiple rows @@ -285,22 +343,20 @@ export default class DBDataSource< * @param options Query options */ protected async insert( - rows: ValueOrArray>, - options?: QueryOptions & { expected: 'any' | 'many' } - ): Promise + rows: ValueOrArray>>, + options?: QueryOptions> & { expected: 'any' | 'many' } + ): Promise[]> /** * Implementation */ protected async insert( - rows: ValueOrArray>, - options: QueryOptions = {} - ): Promise { - if (!options.expected) { - options.expected = !isArray(rows) ? 'one' : 'many' - } + rows: ValueOrArray>>, + options: QueryOptions> = {} + ): Promise | readonly z.infer[] | null> { + options.expected ||= !Array.isArray(rows) ? 'one' : 'many' - if (isArray(rows) && rows.length === 0) { + if (Array.isArray(rows) && rows.length === 0) { switch (options.expected) { case 'one': // we should really raise here, strictly speaking case 'maybeOne': @@ -310,15 +366,13 @@ export default class DBDataSource< return [] } } - - if (isArray(rows)) { - rows.map((row) => this.parseColumnSchemas(row)) - } else { - rows = this.parseColumnSchemas(rows) - } + rows = Array.isArray(rows) ? rows : [rows] + rows = rows.map((row) => this.insertSchema.parse(row)) as AllowSql< + z.infer + >[] const query = this.builder.insert(rows, options) - return await this.query(query, options) + return await this.query(query, options) } /** @@ -328,9 +382,9 @@ export default class DBDataSource< * @param options Query options */ protected async update( - data: UpdateSet, - options: QueryOptions & { expected: 'one' } - ): Promise + data: UpdateSet>, + options: QueryOptions> & { expected: 'one' } + ): Promise> /** * Update a zero or one rows @@ -339,9 +393,9 @@ export default class DBDataSource< * @param options Query options */ protected async update( - data: UpdateSet, - options: QueryOptions & { expected: 'maybeOne' } - ): Promise + data: UpdateSet>, + options: QueryOptions> & { expected: 'maybeOne' } + ): Promise | null> /** * Update multiple rows @@ -350,20 +404,33 @@ export default class DBDataSource< * @param options Query options */ protected async update( - data: UpdateSet, - options?: QueryOptions & { expected?: 'any' | 'many' } - ): Promise + data: UpdateSet>, + options?: QueryOptions> & { + expected?: 'any' | 'many' + } + ): Promise[]> /** * Implementation */ protected async update( - data: UpdateSet, - options?: QueryOptions - ): Promise { - data = this.parseColumnSchemas(data) - const query = this.builder.update(data, options) - return await this.query(query, options) + data: UpdateSet>, + options?: QueryOptions> + ): Promise | readonly z.infer[] | null> { + const mask = Object.keys(data).reduce( + (res, key) => ({ + ...res, + [key]: true, + }), + {} as Record, true> + ) + const query = this.builder.update( + this.insertSchema.pick(mask).parse(data) as UpdateSet< + z.infer + >, + options + ) + return await this.query(query, options) } /** @@ -373,8 +440,8 @@ export default class DBDataSource< * @param options Query options */ protected async delete( - options: QueryOptions & { expected: 'one' } - ): Promise + options: QueryOptions> & { expected: 'one' } + ): Promise> /** * Update a zero or one rows @@ -383,8 +450,8 @@ export default class DBDataSource< * @param options Query options */ protected async delete( - options: QueryOptions & { expected: 'maybeOne' } - ): Promise + options: QueryOptions> & { expected: 'maybeOne' } + ): Promise | null> /** * Update multiple rows @@ -393,8 +460,8 @@ export default class DBDataSource< * @param options Query options */ protected async delete( - options: QueryOptions & { expected?: 'any' | 'many' } - ): Promise + options: QueryOptions> & { expected?: 'any' | 'many' } + ): Promise[]> /** * Delete every row in the table @@ -402,16 +469,21 @@ export default class DBDataSource< * @param data Update expression * @param options Query options */ - protected async delete(options: true): Promise + protected async delete( + options: true + ): Promise[]> /** * Implementation */ protected async delete( - options: QueryOptions | true - ): Promise { + options: QueryOptions> | true + ): Promise | readonly z.infer[] | null> { const query = this.builder.delete(options) - return await this.query(query, options === true ? undefined : options) + return await this.query( + query, + options === true ? undefined : options + ) } /** @@ -425,26 +497,26 @@ export default class DBDataSource< * @param query Executable query * @param options Query options */ - protected async query( - query: TypedSqlQuery, - options: QueryOptions & { expected?: 'any' | 'many' } - ): Promise - protected async query( - query: TypedSqlQuery, - options: QueryOptions & { expected: 'one' } - ): Promise - protected async query( - query: TypedSqlQuery, - options: QueryOptions & { expected: 'maybeOne' } - ): Promise - protected async query( - query: TypedSqlQuery, - options?: QueryOptions - ): Promise - protected async query( - query: TypedSqlQuery, - options?: QueryOptions - ): Promise { + protected async query( + query: TypedSqlQuery, + options: QueryOptions> & { expected?: 'any' | 'many' } + ): Promise[]> + protected async query( + query: TypedSqlQuery, + options: QueryOptions> & { expected: 'one' } + ): Promise> + protected async query( + query: TypedSqlQuery, + options: QueryOptions> & { expected: 'maybeOne' } + ): Promise | null> + protected async query( + query: TypedSqlQuery, + options?: QueryOptions> + ): Promise | null | readonly z.infer[]> + protected async query( + query: TypedSqlQuery, + options?: QueryOptions> + ): Promise | null | readonly z.infer[]> { switch (options?.expected || 'any') { case 'one': return await this.one(query, options) @@ -457,43 +529,43 @@ export default class DBDataSource< } } - private async any( - query: TypedSqlQuery, - options?: QueryOptions - ): Promise { + private async any( + query: TypedSqlQuery, + options?: QueryOptions> + ): Promise[]> { const results = (await this.connection.any(query)).map((row) => - this.transformResult(row) + this.transformResult, z.infer>(row) ) this.eachResult(results, options) return results } - private async many( - query: TypedSqlQuery, - options?: QueryOptions - ): Promise { + private async many( + query: TypedSqlQuery, + options?: QueryOptions> + ): Promise[]> { const results = (await this.connection.many(query)).map((row) => - this.transformResult(row) + this.transformResult, z.infer>(row) ) - this.eachResult(results, options) + this.eachResult>(results, options) return results } - private async one( - query: TypedSqlQuery, - options?: QueryOptions - ): Promise { - const result = this.transformResult( + private async one( + query: TypedSqlQuery, + options?: QueryOptions> + ): Promise> { + const result = this.transformResult, z.infer>( await this.connection.one(query) ) this.eachResult(result, options) return result } - private async maybeOne( - query: TypedSqlQuery, - options?: QueryOptions - ): Promise { + private async maybeOne( + query: TypedSqlQuery, + options?: QueryOptions> + ): Promise | null> { let result = await this.connection.maybeOne(query) if (result) { result = this.transformResult(result) @@ -511,24 +583,22 @@ export default class DBDataSource< if (!eachResult) { return } - - if (!isArray(results)) { - results = [results] - } - - results.filter((val): val is TData => val !== null).forEach(eachResult) + const arrayResults = Array.isArray(results) ? results : [results] + arrayResults.filter((val): val is TData => val !== null).forEach(eachResult) } - private async getDataByColumn( - args: ReadonlyArray, + private async getDataByColumn< + TColumnName extends keyof z.infer & string + >( + args: ReadonlyArray[TColumnName]>, column: TColumnName, type: string, loader: DataLoader< - TRowType[TColumnName] & (string | number), - TRowType[] | TRowType | undefined + z.infer[TColumnName] & (string | number), + z.infer[] | z.infer | undefined >, - options?: Omit, 'expected'> - ): Promise { + options?: Omit>, 'expected'> + ): Promise[]> { const store = this.pool.async.getStore() if (store) { store.loaderLookups.push([loader, args]) @@ -548,15 +618,18 @@ export default class DBDataSource< } protected async getDataByMultipleColumns< - TColumnNames extends Array, - TArgs extends { [K in TColumnNames[0]]: TRowType[K] } + TColumnNames extends Array & string>, + TArgs extends { [K in TColumnNames[0]]: z.infer[K] } >( args: ReadonlyArray, columns: TColumnNames, types: string[], - loader: DataLoader, - options?: QueryOptions & SelectOptions - ): Promise { + loader: DataLoader< + TArgs, + z.infer[] | z.infer | undefined + >, + options?: QueryOptions> & SelectOptions + ): Promise[]> { const store = this.pool.async.getStore() if (store) { store.loaderLookups.push([loader, args]) @@ -568,33 +641,15 @@ export default class DBDataSource< ) } - protected parseColumnSchemas>( - row: TRow - ): TRow { - if (!this.columnSchemas) { - return row - } - const keys = Object.keys( - this.columnSchemas - ) as (keyof typeof this.columnSchemas)[] - for (const column of keys) { - const schema = this.columnSchemas[column] - if (!schema || isSqlToken(row[column])) { - continue - } - - row[column] = schema.parse(row[column]) - } - return row - } - - private transformResult(input: TInput): TOutput { + private transformResult( + input: TInput + ): TOutput { const transform = this.normalizers.columnToKey const output = Object.keys(input).reduce((obj, key) => { const column = transform(key) const type: string | undefined = - this.columnTypes[column as keyof TRowType] + this.metadata[column as keyof z.infer]?.nativeType const value = this.mapTypeValue( column, type, diff --git a/src/datasource/__tests__/DBDataSource.test.ts b/src/datasource/__tests__/DBDataSource.test.ts index 2483d446..760544c9 100644 --- a/src/datasource/__tests__/DBDataSource.test.ts +++ b/src/datasource/__tests__/DBDataSource.test.ts @@ -1,21 +1,31 @@ +import { z } from 'zod' import { DBDataSource } from '..' import { createMockPool } from '../../testing' import { LoaderFactory } from '../loaders' -interface DummyRowType { - id: number - name: string - code: string +const DummyMetadata = { + id: { + nativeType: 'anything', + nativeName: 'id', + }, + name: { + nativeType: 'anything', + nativeName: 'name', + }, + code: { + nativeType: 'anything', + nativeName: 'code', + }, } -const columnTypes: Record = { - id: 'anything', - name: 'anything', - code: 'anything', -} +const DummyRowType = z.object({ + id: z.number(), + name: z.string(), + code: z.string(), +}) describe(DBDataSource, () => { - const dummyBatchFn = async (): Promise => { + const dummyBatchFn = async (): Promise[]> => { return [ { id: 1, name: 'aaa', code: 'abc' }, { id: 2, name: 'bbb', code: 'def' }, @@ -33,17 +43,21 @@ describe(DBDataSource, () => { ] } - const factory = new LoaderFactory(dummyBatchFn, dummyBatchFn, { - columnTypes, - }) + const factory = new LoaderFactory(dummyBatchFn, dummyBatchFn, DummyMetadata) - class DummyDBDataSource extends DBDataSource { + class DummyDBDataSource extends DBDataSource< + typeof DummyMetadata, + typeof DummyRowType, + typeof DummyRowType + > { constructor() { - super(createMockPool(), 'any_table', { - id: 'any', - name: 'any', - code: 'any', - }) + super( + createMockPool(), + 'any_table', + DummyMetadata, + DummyRowType /* select */, + DummyRowType /* insert */ + ) } protected get loaders() { @@ -66,7 +80,7 @@ describe(DBDataSource, () => { const dataSource = new TestDataSource() it('uses the default options', () => { - expect(dataSource.testBuilder.select()).toMatchSnapshot() + expect(dataSource.testBuilder.select().sql).toMatchSnapshot() }) }) }) diff --git a/src/datasource/__tests__/__snapshots__/DBDataSource.test.ts.snap b/src/datasource/__tests__/__snapshots__/DBDataSource.test.ts.snap index 7e91e05c..071f5dbc 100644 --- a/src/datasource/__tests__/__snapshots__/DBDataSource.test.ts.snap +++ b/src/datasource/__tests__/__snapshots__/DBDataSource.test.ts.snap @@ -1,8 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`DBDataSource defaultOptions uses the default options 1`] = ` -{ - "sql": " +" SELECT * FROM "any_table" @@ -10,8 +9,5 @@ exports[`DBDataSource defaultOptions uses the default options 1`] = ` ORDER BY "any_table"."id" - ", - "type": "SLONIK_TOKEN_SQL", - "values": [], -} + " `; diff --git a/src/datasource/__tests__/__snapshots__/queries.test.ts.snap b/src/datasource/__tests__/__snapshots__/queries.test.ts.snap index d1a505e1..04c47224 100644 --- a/src/datasource/__tests__/__snapshots__/queries.test.ts.snap +++ b/src/datasource/__tests__/__snapshots__/queries.test.ts.snap @@ -3,7 +3,7 @@ exports[`QueryBuilder clause generators groupBy accepts a list of column names as an array of strings 1`] = ` { "sql": "GROUP BY "any_table"."a", "any_table"."b", "any_table"."c"", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -11,7 +11,7 @@ exports[`QueryBuilder clause generators groupBy accepts a list of column names a exports[`QueryBuilder clause generators groupBy accepts a mix of different types 1`] = ` { "sql": "GROUP BY "any_table"."a", "b", c, "any_table"."d", "e", f", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -19,7 +19,7 @@ exports[`QueryBuilder clause generators groupBy accepts a mix of different types exports[`QueryBuilder clause generators groupBy accepts a single column name as a string 1`] = ` { "sql": "GROUP BY "any_table"."a"", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -27,7 +27,7 @@ exports[`QueryBuilder clause generators groupBy accepts a single column name as exports[`QueryBuilder clause generators groupBy accepts a single column name as an identifier 1`] = ` { "sql": "GROUP BY "column"", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -35,7 +35,7 @@ exports[`QueryBuilder clause generators groupBy accepts a single column name as exports[`QueryBuilder clause generators groupBy accepts a single column name as arbitrary sql 1`] = ` { "sql": "GROUP BY anything i want!", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -43,7 +43,7 @@ exports[`QueryBuilder clause generators groupBy accepts a single column name as exports[`QueryBuilder clause generators having accepts a list of expressions 1`] = ` { "sql": "HAVING (true AND false AND more raw expressions etc.)", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -51,7 +51,7 @@ exports[`QueryBuilder clause generators having accepts a list of expressions 1`] exports[`QueryBuilder clause generators having accepts a simple object 1`] = ` { "sql": "HAVING ("any_table"."id" = $1)", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ 1, ], @@ -61,7 +61,7 @@ exports[`QueryBuilder clause generators having accepts a simple object 1`] = ` exports[`QueryBuilder clause generators having handles multiple values for a column 1`] = ` { "sql": "HAVING ("any_table"."id" = ANY($1::these[]))", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ [ 1, @@ -75,7 +75,7 @@ exports[`QueryBuilder clause generators having handles multiple values for a col exports[`QueryBuilder clause generators having lets you pass in a raw expression 1`] = ` { "sql": "HAVING true", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -83,7 +83,7 @@ exports[`QueryBuilder clause generators having lets you pass in a raw expression exports[`QueryBuilder clause generators having produces a valid clause with no conditions 1`] = ` { "sql": "HAVING true", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -91,7 +91,7 @@ exports[`QueryBuilder clause generators having produces a valid clause with no c exports[`QueryBuilder clause generators having produces a valid clause with no conditions 2`] = ` { "sql": "HAVING true", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -99,7 +99,7 @@ exports[`QueryBuilder clause generators having produces a valid clause with no c exports[`QueryBuilder clause generators having sanely handles output from the \`and\` and \`or\` utilities 1`] = ` { "sql": "HAVING (("any_table"."id" = $1 AND "any_table"."name" = $2) OR ("any_table"."id" = $3 AND "any_table"."name" = $4))", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ 1, "asdf", @@ -112,7 +112,7 @@ exports[`QueryBuilder clause generators having sanely handles output from the \` exports[`QueryBuilder clause generators having uses AND for multiple columns in a simple object 1`] = ` { "sql": "HAVING ("any_table"."id" = $1 AND "any_table"."name" = $2)", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ 1, "Bob", @@ -123,7 +123,7 @@ exports[`QueryBuilder clause generators having uses AND for multiple columns in exports[`QueryBuilder clause generators limit can accept arbitrary sql 1`] = ` { "sql": "LIMIT anything, thanks", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -131,7 +131,7 @@ exports[`QueryBuilder clause generators limit can accept arbitrary sql 1`] = ` exports[`QueryBuilder clause generators limit can create a LIMIT clause 1`] = ` { "sql": "LIMIT $1", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ 1, ], @@ -141,7 +141,7 @@ exports[`QueryBuilder clause generators limit can create a LIMIT clause exports[`QueryBuilder clause generators limit can create a LIMIT ALL clause 1`] = ` { "sql": "LIMIT ALL", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -149,7 +149,7 @@ exports[`QueryBuilder clause generators limit can create a LIMIT ALL clause 1`] exports[`QueryBuilder clause generators limit can create an offset clause with limit 1`] = ` { "sql": "LIMIT $1 OFFSET $2", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ 1, 1, @@ -160,7 +160,7 @@ exports[`QueryBuilder clause generators limit can create an offset clause with l exports[`QueryBuilder clause generators limit can create an offset clause with limit 2`] = ` { "sql": "LIMIT ALL OFFSET $1", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ 1, ], @@ -170,7 +170,7 @@ exports[`QueryBuilder clause generators limit can create an offset clause with l exports[`QueryBuilder clause generators orderBy accepts a list of column names as an array of strings 1`] = ` { "sql": "ORDER BY "any_table"."a", "any_table"."b", "any_table"."c"", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -178,7 +178,7 @@ exports[`QueryBuilder clause generators orderBy accepts a list of column names a exports[`QueryBuilder clause generators orderBy accepts a mix of different types 1`] = ` { "sql": "ORDER BY "any_table"."a", "b", c, "any_table"."d", "e", f", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -186,7 +186,7 @@ exports[`QueryBuilder clause generators orderBy accepts a mix of different types exports[`QueryBuilder clause generators orderBy accepts a single column name as a string 1`] = ` { "sql": "ORDER BY "any_table"."a"", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -194,7 +194,7 @@ exports[`QueryBuilder clause generators orderBy accepts a single column name as exports[`QueryBuilder clause generators orderBy accepts a single column name as an identifier 1`] = ` { "sql": "ORDER BY "column"", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -202,7 +202,7 @@ exports[`QueryBuilder clause generators orderBy accepts a single column name as exports[`QueryBuilder clause generators orderBy accepts a single column name as arbitrary sql 1`] = ` { "sql": "ORDER BY anything i want!", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -210,7 +210,7 @@ exports[`QueryBuilder clause generators orderBy accepts a single column name as exports[`QueryBuilder clause generators orderBy can use order tuples 1`] = ` { "sql": "ORDER BY "any_table"."a" DESC, "any_table"."b", c ASC", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -218,7 +218,7 @@ exports[`QueryBuilder clause generators orderBy can use order tuples 1`] = ` exports[`QueryBuilder clause generators where accepts a list of expressions 1`] = ` { "sql": "WHERE (true AND false AND more raw expressions etc.)", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -226,7 +226,7 @@ exports[`QueryBuilder clause generators where accepts a list of expressions 1`] exports[`QueryBuilder clause generators where accepts a simple object 1`] = ` { "sql": "WHERE ("any_table"."id" = $1)", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ 1, ], @@ -236,7 +236,7 @@ exports[`QueryBuilder clause generators where accepts a simple object 1`] = ` exports[`QueryBuilder clause generators where accepts complex conditions 1`] = ` { "sql": "WHERE ("any_table"."id" = $1 AND "any_table"."nullable" IS NULL AND ("any_table"."string_or_number" = $2 OR "any_table"."string_or_number" IS NULL))", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ 1, "a", @@ -247,7 +247,7 @@ exports[`QueryBuilder clause generators where accepts complex conditions 1`] = ` exports[`QueryBuilder clause generators where correctly handles Date objects 1`] = ` { "sql": "WHERE ("any_table"."date" = $1)", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ "2020-11-30T05:00:00.000Z", ], @@ -257,7 +257,7 @@ exports[`QueryBuilder clause generators where correctly handles Date objects 1`] exports[`QueryBuilder clause generators where enables custom operators through use of sql tokens 1`] = ` { "sql": "WHERE ("any_table"."id" > 1)", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -265,7 +265,7 @@ exports[`QueryBuilder clause generators where enables custom operators through u exports[`QueryBuilder clause generators where handles multiple values for a column 1`] = ` { "sql": "WHERE ("any_table"."id" = ANY($1::these[]))", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ [ 1, @@ -279,7 +279,7 @@ exports[`QueryBuilder clause generators where handles multiple values for a colu exports[`QueryBuilder clause generators where handles null lookups correctly 1`] = ` { "sql": "WHERE ("any_table"."nullable" IS NULL)", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -287,7 +287,7 @@ exports[`QueryBuilder clause generators where handles null lookups correctly 1`] exports[`QueryBuilder clause generators where lets you pass in a raw expression 1`] = ` { "sql": "WHERE true", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -295,7 +295,7 @@ exports[`QueryBuilder clause generators where lets you pass in a raw expression exports[`QueryBuilder clause generators where produces a valid clause with no conditions 1`] = ` { "sql": "WHERE true", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -303,7 +303,7 @@ exports[`QueryBuilder clause generators where produces a valid clause with no co exports[`QueryBuilder clause generators where produces a valid clause with no conditions 2`] = ` { "sql": "WHERE true", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [], } `; @@ -311,7 +311,7 @@ exports[`QueryBuilder clause generators where produces a valid clause with no co exports[`QueryBuilder clause generators where sanely handles output from the \`and\` and \`or\` utilities 1`] = ` { "sql": "WHERE (("any_table"."id" = $1 AND "any_table"."name" = $2) OR ("any_table"."id" = $3 AND "any_table"."name" = $4))", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ 1, "asdf", @@ -324,7 +324,7 @@ exports[`QueryBuilder clause generators where sanely handles output from the \`a exports[`QueryBuilder clause generators where uses AND for multiple columns in a simple object 1`] = ` { "sql": "WHERE ("any_table"."id" = $1 AND "any_table"."name" = $2)", - "type": "SLONIK_TOKEN_SQL", + "type": "SLONIK_TOKEN_FRAGMENT", "values": [ 1, "Bob", @@ -334,12 +334,13 @@ exports[`QueryBuilder clause generators where uses AND for multiple columns in a exports[`QueryBuilder core query builders count can use where clauses 1`] = ` { + "parser": Anything, "sql": " SELECT COUNT(*) FROM "any_table" WHERE ("any_table"."id" = $1) ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ 1, ], @@ -348,25 +349,27 @@ exports[`QueryBuilder core query builders count can use where clauses 1`] = ` exports[`QueryBuilder core query builders count creates a count query 1`] = ` { + "parser": Anything, "sql": " SELECT COUNT(*) FROM "any_table" ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [], } `; exports[`QueryBuilder core query builders countGroup can use where clauses 1`] = ` { + "parser": Anything, "sql": " SELECT "any_table"."nullable", "any_table"."optional", COUNT(*) FROM "any_table" WHERE ("any_table"."id" = $1) GROUP BY "any_table"."nullable", "any_table"."optional" ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ 1, ], @@ -375,19 +378,21 @@ exports[`QueryBuilder core query builders countGroup can use where clauses 1`] = exports[`QueryBuilder core query builders countGroup creates a count query with a groupBy clause 1`] = ` { + "parser": Anything, "sql": " SELECT "any_table"."name", COUNT(*) FROM "any_table" GROUP BY "any_table"."name" ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [], } `; exports[`QueryBuilder core query builders delete builds clauses correctly 1`] = ` { + "parser": Anything, "sql": " WITH "delete_rows" AS ( @@ -402,7 +407,7 @@ exports[`QueryBuilder core query builders delete builds clauses correctly 1`] = HAVING ("any_table"."id" = $2) ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ 1, 1, @@ -412,6 +417,7 @@ exports[`QueryBuilder core query builders delete builds clauses correctly 1`] = exports[`QueryBuilder core query builders delete can be forced to delete everything 1`] = ` { + "parser": Anything, "sql": " WITH "delete_rows" AS ( @@ -426,7 +432,7 @@ exports[`QueryBuilder core query builders delete can be forced to delete everyth ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [], } `; @@ -435,12 +441,13 @@ exports[`QueryBuilder core query builders delete doesn't let you delete everythi exports[`QueryBuilder core query builders insert accepts a basic object 1`] = ` { + "parser": Anything, "sql": " WITH "insert_rows" AS ( INSERT INTO "any_table" ("id", "name", "nullable", "string_or_number") SELECT * - FROM jsonb_to_recordset($1) AS ("id" these, "name" types, "nullable" not, "stringOrNumber" here) + FROM json_to_recordset($1::json) AS ("id" these, "name" types, "nullable" not, "stringOrNumber" here) RETURNING * ) SELECT * @@ -450,7 +457,7 @@ exports[`QueryBuilder core query builders insert accepts a basic object 1`] = ` ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ "[{"id":1,"name":"name","nullable":null,"stringOrNumber":1}]", ], @@ -459,12 +466,13 @@ exports[`QueryBuilder core query builders insert accepts a basic object 1`] = ` exports[`QueryBuilder core query builders insert accepts many basic objects 1`] = ` { + "parser": Anything, "sql": " WITH "insert_rows" AS ( INSERT INTO "any_table" ("id", "name", "nullable", "string_or_number") SELECT * - FROM jsonb_to_recordset($1) AS ("id" these, "name" types, "nullable" not, "stringOrNumber" here) + FROM json_to_recordset($1::json) AS ("id" these, "name" types, "nullable" not, "stringOrNumber" here) RETURNING * ) SELECT * @@ -474,7 +482,7 @@ exports[`QueryBuilder core query builders insert accepts many basic objects 1`] ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ "[{"id":1,"name":"name","nullable":null,"stringOrNumber":1},{"stringOrNumber":"wat","id":2,"name":"name","nullable":"hi"}]", ], @@ -483,6 +491,7 @@ exports[`QueryBuilder core query builders insert accepts many basic objects 1`] exports[`QueryBuilder core query builders insert allows a single object with raw SQL values 1`] = ` { + "parser": Anything, "sql": " WITH "insert_rows" AS ( @@ -497,7 +506,7 @@ exports[`QueryBuilder core query builders insert allows a single object with raw ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ 1, ], @@ -506,6 +515,7 @@ exports[`QueryBuilder core query builders insert allows a single object with raw exports[`QueryBuilder core query builders insert allows multiple objects with a mix of value types 1`] = ` { + "parser": Anything, "sql": " WITH "insert_rows" AS ( @@ -520,7 +530,7 @@ exports[`QueryBuilder core query builders insert allows multiple objects with a ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ 1, "anything", @@ -531,6 +541,7 @@ exports[`QueryBuilder core query builders insert allows multiple objects with a exports[`QueryBuilder core query builders insert allows multiple objects with raw SQL values 1`] = ` { + "parser": Anything, "sql": " WITH "insert_rows" AS ( @@ -545,7 +556,7 @@ exports[`QueryBuilder core query builders insert allows multiple objects with ra ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ 1, 2, @@ -555,12 +566,13 @@ exports[`QueryBuilder core query builders insert allows multiple objects with ra exports[`QueryBuilder core query builders insert correctly inserts Date objects as ISO8601 strings 1`] = ` { + "parser": Anything, "sql": " WITH "insert_rows" AS ( INSERT INTO "any_table" ("id", "name", "nullable", "string_or_number", "date") SELECT * - FROM jsonb_to_recordset($1) AS ("id" these, "name" types, "nullable" not, "stringOrNumber" here, "date" date) + FROM json_to_recordset($1::json) AS ("id" these, "name" types, "nullable" not, "stringOrNumber" here, "date" date) RETURNING * ) SELECT * @@ -570,7 +582,7 @@ exports[`QueryBuilder core query builders insert correctly inserts Date objects ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ "[{"id":1,"name":"name","nullable":null,"stringOrNumber":1,"date":"2020-11-30T05:00:00.000Z"}]", ], @@ -579,13 +591,14 @@ exports[`QueryBuilder core query builders insert correctly inserts Date objects exports[`QueryBuilder core query builders multiColumnBatchGet builds the query correctly 1`] = ` { + "parser": Anything, "sql": " SELECT "any_table".* FROM "any_table", ( SELECT (conditions->>'id')::"integer" AS "id", (conditions->>'name')::"text" AS "name" - FROM jsonb_array_elements($1) AS conditions + FROM json_array_elements($1::json) AS conditions ) AS "any_table_conditions" WHERE "any_table"."id"::"integer" = "any_table_conditions"."id"::"integer" AND "any_table"."name"::"text" = "any_table_conditions"."name"::"text" @@ -594,7 +607,7 @@ exports[`QueryBuilder core query builders multiColumnBatchGet builds the query c ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ "[{"id":1,"name":"asdf"},{"id":2,"name":"blah"}]", ], @@ -603,13 +616,14 @@ exports[`QueryBuilder core query builders multiColumnBatchGet builds the query c exports[`QueryBuilder core query builders multiColumnBatchGet respects casing for column names 1`] = ` { + "parser": Anything, "sql": " SELECT "any_table".* FROM "any_table", ( SELECT (conditions->>'optionally_nullable')::"any" AS "optionally_nullable", (conditions->>'string_or_number')::"thing" AS "string_or_number" - FROM jsonb_array_elements($1) AS conditions + FROM json_array_elements($1::json) AS conditions ) AS "any_table_conditions" WHERE "any_table"."optionally_nullable"::"any" = "any_table_conditions"."optionally_nullable"::"any" AND "any_table"."string_or_number"::"thing" = "any_table_conditions"."string_or_number"::"thing" @@ -618,7 +632,7 @@ exports[`QueryBuilder core query builders multiColumnBatchGet respects casing fo ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ "[{"optionally_nullable":"anything","string_or_number":0}]", ], @@ -627,6 +641,7 @@ exports[`QueryBuilder core query builders multiColumnBatchGet respects casing fo exports[`QueryBuilder core query builders select can select for update 1`] = ` { + "parser": Anything, "sql": " SELECT * FROM "any_table" @@ -636,13 +651,14 @@ exports[`QueryBuilder core query builders select can select for update 1`] = ` FOR UPDATE ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [], } `; exports[`QueryBuilder core query builders select can select for update of another table 1`] = ` { + "parser": Anything, "sql": " SELECT * FROM "any_table" @@ -652,13 +668,14 @@ exports[`QueryBuilder core query builders select can select for update of anothe FOR UPDATE OF "another_table" ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [], } `; exports[`QueryBuilder core query builders select can select for update of multiple other tables 1`] = ` { + "parser": Anything, "sql": " SELECT * FROM "any_table" @@ -668,13 +685,14 @@ exports[`QueryBuilder core query builders select can select for update of multip FOR UPDATE OF "table", "another_table", "more_tables" ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [], } `; exports[`QueryBuilder core query builders select selects everything by default 1`] = ` { + "parser": Anything, "sql": " SELECT * FROM "any_table" @@ -684,13 +702,14 @@ exports[`QueryBuilder core query builders select selects everything by default 1 ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [], } `; exports[`QueryBuilder core query builders select supports limits 1`] = ` { + "parser": Anything, "sql": " SELECT * FROM "any_table" @@ -700,7 +719,7 @@ exports[`QueryBuilder core query builders select supports limits 1`] = ` LIMIT $1 ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ 10, ], @@ -709,6 +728,7 @@ exports[`QueryBuilder core query builders select supports limits 1`] = ` exports[`QueryBuilder core query builders update accepts a basic object 1`] = ` { + "parser": Anything, "sql": " WITH "update_rows" AS ( @@ -724,7 +744,7 @@ exports[`QueryBuilder core query builders update accepts a basic object 1`] = ` ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ 5, "any", @@ -738,6 +758,7 @@ exports[`QueryBuilder core query builders update accepts a basic object 1`] = ` exports[`QueryBuilder core query builders update accepts raw sql values 1`] = ` { + "parser": Anything, "sql": " WITH "update_rows" AS ( @@ -753,13 +774,14 @@ exports[`QueryBuilder core query builders update accepts raw sql values 1`] = ` ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [], } `; exports[`QueryBuilder core query builders update correctly updates Date objects as ISO8601 strings 1`] = ` { + "parser": Anything, "sql": " WITH "update_rows" AS ( @@ -775,7 +797,7 @@ exports[`QueryBuilder core query builders update correctly updates Date objects ", - "type": "SLONIK_TOKEN_SQL", + "type": Any, "values": [ "2020-11-30T05:00:00.000Z", ], diff --git a/src/datasource/__tests__/finders.test.ts b/src/datasource/__tests__/finders.test.ts index 9e53eca1..45a75d79 100644 --- a/src/datasource/__tests__/finders.test.ts +++ b/src/datasource/__tests__/finders.test.ts @@ -1,19 +1,29 @@ +import { z } from 'zod' import { FinderFactory, LoaderFactory } from '../loaders' -interface DummyRowType { - id: number - name: string - code: string +const DummyMetadata = { + id: { + nativeType: 'anything', + nativeName: 'id', + }, + name: { + nativeType: 'anything', + nativeName: 'name', + }, + code: { + nativeType: 'anything', + nativeName: 'code', + }, } -const columnTypes: Record = { - id: 'anything', - name: 'anything', - code: 'anything', -} +const DummyRowType = z.object({ + id: z.number(), + name: z.string(), + code: z.string(), +}) describe(LoaderFactory, () => { - const dummyBatchFn = async (): Promise => { + const dummyBatchFn = async (): Promise[]> => { return [ { id: 1, name: 'aaa', code: 'abc' }, { id: 2, name: 'bbb', code: 'def' }, @@ -31,10 +41,8 @@ describe(LoaderFactory, () => { ] } - const loaders = new LoaderFactory(dummyBatchFn, dummyBatchFn, { - columnTypes, - }) - const finders = new FinderFactory() + const loaders = new LoaderFactory(dummyBatchFn, dummyBatchFn, DummyMetadata) + const finders = new FinderFactory>() describe('create', () => { describe('multi: false', () => { diff --git a/src/datasource/__tests__/integration.test.ts b/src/datasource/__tests__/integration.test.ts index 64abd03b..cdabe1ac 100644 --- a/src/datasource/__tests__/integration.test.ts +++ b/src/datasource/__tests__/integration.test.ts @@ -1,42 +1,75 @@ import assert from 'assert' import { createPool, DatabasePool, sql, FragmentSqlToken } from 'slonik' -import { z, ZodSchema } from 'zod' +import { z } from 'zod' import { DBDataSource } from '..' -interface DummyRowType { - id: number - name: string - code: string - withDefault?: string | FragmentSqlToken - camelCase: string - tsTest: Date - dateTest: Date - jsonbTest: TSchema - nullable?: string | null -} +const FragmentSqlToken = z.object({ + sql: z.string(), + type: z.literal('SLONIK_TOKEN_FRAGMENT'), + values: z.array(z.any()), +}) -interface DefaultJsonbSchema { - a: number +const DummyMetadata = { + id: { + nativeType: 'int8', + nativeName: 'id', + }, + name: { + nativeType: 'citext', + nativeName: 'name', + }, + code: { + nativeType: 'text', + nativeName: 'code', + }, + withDefault: { + nativeType: 'text', + nativeName: 'with_default', + }, + camelCase: { + nativeType: 'text', + nativeName: 'camel_case', + }, + tsTest: { + nativeType: 'timestamptz', + nativeName: 'ts_test', + }, + dateTest: { + nativeType: 'date', + nativeName: 'date_test', + }, + jsonbTest: { + nativeType: 'jsonb', + nativeName: 'jsonb_test', + }, + nullable: { + nativeType: 'text', + nativeName: 'nullable', + }, } -const columnTypes = { - id: 'int8', - name: 'citext', - code: 'text', - withDefault: 'text', - camelCase: 'text', - tsTest: 'timestamptz', - dateTest: 'date', - jsonbTest: 'jsonb', - nullable: 'text', -} as const +const DummyRowType = z.object({ + id: z.number(), + name: z.string(), + code: z.string(), + withDefault: z.string().or(FragmentSqlToken).optional(), + camelCase: z.string(), + tsTest: z.date(), + dateTest: z.date(), + jsonbTest: z.any(), + nullable: z.string().nullish(), +}) + +const DefaultJsonbSchema = z.object({ + a: z.number(), +}) let pool: DatabasePool -const createRow = ( - values: Partial> -): DummyRowType => { +const createRow = ( + values: Partial> +): z.infer => { return { id: 1, code: '', @@ -44,7 +77,7 @@ const createRow = ( camelCase: '', tsTest: new Date('2020-12-05T00:00:00.000Z'), dateTest: new Date('2021-04-19'), - jsonbTest: { a: 1 } as unknown as TSchema, + jsonbTest: { a: 1 }, nullable: null, ...values, } @@ -84,26 +117,34 @@ afterAll(async () => { await pool.end() }) -class TestDataSource extends DBDataSource< - DummyRowType, - DummyRowType, - typeof columnTypes +class TestDataSource extends DBDataSource< + typeof DummyMetadata, + z.ZodObject< + z.extendShape, + 'strip', + z.ZodTypeAny + >, + z.ZodObject< + z.extendShape, + 'strip', + z.ZodTypeAny + > > { - constructor(columnSchemas?: { - [K in keyof DummyRowType as typeof columnTypes[K] extends - | 'json' - | 'jsonb' - ? K - : never]?: ZodSchema - }) { - super(pool, 'test_table', columnTypes, columnSchemas) + constructor(jsonbSchema: TSchema) { + const schema = DummyRowType.extend({ jsonbTest: jsonbSchema }) + + super(pool, 'test_table', DummyMetadata, schema, schema) } public idLoader = this.loaders.create('id') + public a = this.loaders.create('dateTest') public codeLoader = this.loaders.create('code', { multi: true, orderBy: ['id', 'DESC'], }) + /* + + */ public idAndCodeLoader = this.loaders.createMulti(['name', 'code'], { multi: true, @@ -124,10 +165,10 @@ class TestDataSource extends DBDataSource< public testDelete: TestDataSource['delete'] = this.delete } -let ds: TestDataSource +let ds: TestDataSource beforeEach(() => { - ds = new TestDataSource() + ds = new TestDataSource(DefaultJsonbSchema) }) afterEach(() => { @@ -148,7 +189,7 @@ describe('DBDataSource', () => { }) it('can insert new rows', async () => { - const row: DummyRowType = createRow({ + const row: z.infer = createRow({ id: 2, code: 'CODE', name: 'Test Row', @@ -162,7 +203,7 @@ describe('DBDataSource', () => { }) it('can insert rows with raw sql and without', async () => { - const row1: DummyRowType = createRow({ + const row1: z.infer = createRow({ id: 5, code: 'A', name: 'abc', @@ -170,7 +211,7 @@ describe('DBDataSource', () => { tsTest: new Date('2020-12-05T00:00:00.001Z'), jsonbTest: { a: 1 }, }) - const row2: DummyRowType = createRow({ + const row2: z.infer = createRow({ id: 6, code: 'B', name: 'def', @@ -178,7 +219,7 @@ describe('DBDataSource', () => { tsTest: new Date('2020-12-05T00:00:00.002Z'), jsonbTest: { a: 2 }, }) - const row3: DummyRowType = createRow({ + const row3: z.infer = createRow({ id: 7, code: 'C', name: 'ghi', @@ -201,7 +242,7 @@ describe('DBDataSource', () => { describe('when given an array with one input', () => { it('returns an array of results', async () => { - const row1: DummyRowType = createRow({ + const row1: z.infer = createRow({ id: 99, code: 'A', name: 'abc', @@ -215,7 +256,7 @@ describe('DBDataSource', () => { }) it('can insert rows with columns of arbitrary case', async () => { - const row: DummyRowType = createRow({ + const row: z.infer = createRow({ id: 8, code: 'D', name: 'jkl', @@ -229,7 +270,7 @@ describe('DBDataSource', () => { }) describe('when there is data in the table', () => { - const row: DummyRowType = createRow({ + const row: z.infer = createRow({ id: 2, code: 'CODE', name: 'Test Row', @@ -273,7 +314,7 @@ describe('DBDataSource', () => { it('can insert more rows', async () => { await ds.testInsert(row) - const newRow1: DummyRowType = createRow({ + const newRow1: z.infer = createRow({ id: 10, code: 'any', name: 'any', @@ -281,7 +322,7 @@ describe('DBDataSource', () => { tsTest: new Date('2020-12-05T00:00:00.001Z'), jsonbTest: { a: 1 }, }) - const newRow2: DummyRowType = createRow({ + const newRow2: z.infer = createRow({ id: 11, code: 'more', name: 'values', @@ -295,7 +336,7 @@ describe('DBDataSource', () => { }) it('can update rows', async () => { - const newRow1: DummyRowType = createRow({ + const newRow1: z.infer = createRow({ id: 10, code: 'any', name: 'any', @@ -303,7 +344,7 @@ describe('DBDataSource', () => { tsTest: new Date('2020-12-05T00:00:00.001Z'), jsonbTest: { a: 1 }, }) - const newRow2: DummyRowType = createRow({ + const newRow2: z.infer = createRow({ id: 11, code: 'more', name: 'values', @@ -333,7 +374,7 @@ describe('DBDataSource', () => { }) it('can delete rows', async () => { - const newRow1: DummyRowType = createRow({ + const newRow1: z.infer = createRow({ id: 10, code: 'any', name: 'any', @@ -341,7 +382,7 @@ describe('DBDataSource', () => { tsTest: new Date('2020-12-05T00:00:00.001Z'), jsonbTest: { a: 1 }, }) - const newRow2: DummyRowType = createRow({ + const newRow2: z.infer = createRow({ id: 11, code: 'more', name: 'values', @@ -367,7 +408,7 @@ describe('DBDataSource', () => { }) it('can use loaders to lookup rows', async () => { - const newRow1: DummyRowType = createRow({ + const newRow1: z.infer = createRow({ id: 10, code: 'any', name: 'any', @@ -375,7 +416,7 @@ describe('DBDataSource', () => { tsTest: new Date('2020-12-05T00:00:00.001Z'), jsonbTest: { a: 1 }, }) - const newRow2: DummyRowType = createRow({ + const newRow2: z.infer = createRow({ id: 11, code: 'more', name: 'values', @@ -391,7 +432,7 @@ describe('DBDataSource', () => { }) it('can set query options on a loader', async () => { - const row1: DummyRowType = createRow({ + const row1: z.infer = createRow({ id: 19, code: 'any', name: 'any', @@ -399,7 +440,7 @@ describe('DBDataSource', () => { tsTest: new Date('2020-12-05T00:00:00.001Z'), jsonbTest: { a: 1 }, }) - const row2: DummyRowType = createRow({ + const row2: z.infer = createRow({ id: 20, code: 'any', name: 'any', @@ -415,7 +456,7 @@ describe('DBDataSource', () => { }) it('can lookup rows by null values', async () => { - const row1: DummyRowType = createRow({ + const row1: z.infer = createRow({ id: 19, code: 'any', name: 'any', @@ -424,7 +465,7 @@ describe('DBDataSource', () => { jsonbTest: { a: 1 }, nullable: 'not null', }) - const row2: DummyRowType = createRow({ + const row2: z.infer = createRow({ id: 20, code: 'any', name: 'any', @@ -444,7 +485,7 @@ describe('DBDataSource', () => { }) it('can lookup rows by one of multiple values including null', async () => { - const row1: DummyRowType = createRow({ + const row1: z.infer = createRow({ id: 19, code: 'any', name: 'any', @@ -453,7 +494,7 @@ describe('DBDataSource', () => { jsonbTest: { a: 1 }, nullable: 'not null', }) - const row2: DummyRowType = createRow({ + const row2: z.infer = createRow({ id: 20, code: 'any', name: 'any', @@ -475,7 +516,7 @@ describe('DBDataSource', () => { describe('counting rows', () => { it('can count all rows in the table', async () => { - const rows: DummyRowType[] = [ + const rows = [ createRow({ id: 21 }), createRow({ id: 22 }), createRow({ id: 23 }), @@ -487,7 +528,7 @@ describe('DBDataSource', () => { }) it('can count rows in groups', async () => { - const rows: DummyRowType[] = [ + const rows = [ createRow({ id: 24, code: 'ONE' }), createRow({ id: 25, code: 'TWO' }), createRow({ id: 26, code: 'TWO' }), @@ -510,7 +551,7 @@ describe('DBDataSource', () => { describe('multi-column loader', () => { it('can query data successfully', async () => { - const rows: DummyRowType[] = [ + const rows = [ createRow({ id: 30, name: 'hello', code: 'secret' }), createRow({ id: 31, name: 'noway', code: 'secret' }), ] @@ -522,7 +563,10 @@ describe('DBDataSource', () => { }) it('can query data correctly with casing differences', async () => { - const row: DummyRowType = createRow({ id: 32, camelCase: 'value1' }) + const row: z.infer = createRow({ + id: 32, + camelCase: 'value1', + }) await ds.testInsert(row) expect( @@ -531,7 +575,7 @@ describe('DBDataSource', () => { }) it('can cast values correctly', async () => { - const row: DummyRowType = createRow({ + const row: z.infer = createRow({ id: 33, name: '7963ad1f-3289-4a50-860a-56e3571d27db', }) @@ -549,7 +593,7 @@ describe('DBDataSource', () => { describe('transaction support', () => { it('can use transactions successfully', async () => { - const row: DummyRowType = createRow({ + const row: z.infer = createRow({ id: 34, }) @@ -570,7 +614,7 @@ describe('DBDataSource', () => { }) it('handles loader cache clearing correctly', async () => { - const row: DummyRowType = createRow({ + const row: z.infer = createRow({ id: 35, }) @@ -587,7 +631,7 @@ describe('DBDataSource', () => { }) it("doesn't clear already-cached loader items", async () => { - const row: DummyRowType = createRow({ + const row: z.infer = createRow({ id: 36, }) await ds.testInsert(row) @@ -604,9 +648,9 @@ describe('DBDataSource', () => { }) it('uses transactions cross-datasource', async () => { - const ds2 = new TestDataSource() + const ds2 = new TestDataSource(DefaultJsonbSchema) - const row: DummyRowType = createRow({ + const row: z.infer = createRow({ id: 37, }) @@ -628,12 +672,14 @@ describe('DBDataSource', () => { }) describe('json column schemas', () => { - let ds: TestDataSource + const schema = z.object({ + a: z.number(), + }) + + let ds: TestDataSource beforeEach(() => { - ds = new TestDataSource({ - jsonbTest: z.object({ a: z.number() }), - }) + ds = new TestDataSource(schema) }) describe('when inserting', () => { @@ -656,7 +702,6 @@ describe('DBDataSource', () => { describe('with an invalid schema (invalid value)', () => { it('throws an error', async () => { await expect( - // @ts-expect-error testing schema parsing ds.testInsert(createRow({ id: 36, jsonbTest: { a: 'asdf' } })) ).rejects.toThrowError('Expected number, received string') }) @@ -664,7 +709,6 @@ describe('DBDataSource', () => { it("doesn't insert the row", async () => { try { await ds.testInsert( - // @ts-expect-error testing schema parsing createRow({ id: 36, jsonbTest: { a: 'asdf' } }) ) } catch { @@ -678,17 +722,13 @@ describe('DBDataSource', () => { describe('with an invalid schema (missing key)', () => { it('throws an error', async () => { await expect( - // @ts-expect-error testing schema parsing ds.testInsert(createRow({ id: 36, jsonbTest: {} })) ).rejects.toThrowError('Required') }) it("doesn't insert the row", async () => { try { - await ds.testInsert( - // @ts-expect-error testing schema parsing - createRow({ id: 36, jsonbTest: {} }) - ) + await ds.testInsert(createRow({ id: 36, jsonbTest: {} })) } catch { // swallow the error } @@ -705,19 +745,6 @@ describe('DBDataSource', () => { // @ts-expect-error testing types expect(result.jsonbTest.extra).toBeUndefined() }) - - it("doesn't insert the row", async () => { - try { - await ds.testInsert( - // @ts-expect-error testing schema parsing - createRow({ id: 36, jsonbTest: { a: 'asdf' } }) - ) - } catch { - // swallow the error - } - const result = await ds.testGet() - expect(result.length).toEqual(0) - }) }) }) @@ -750,7 +777,6 @@ describe('DBDataSource', () => { it('throws an error', async () => { await expect( ds.testInsert([ - // @ts-expect-error testing schema parsing createRow({ id: 41, jsonbTest: { a: 'asdf' } }), createRow({ id: 42, jsonbTest: { a: 2223 } }), ]) @@ -760,7 +786,6 @@ describe('DBDataSource', () => { it('inserts no rows', async () => { try { await ds.testInsert([ - // @ts-expect-error testing schema parsing createRow({ id: 43, jsonbTest: { a: 'asdf' } }), createRow({ id: 44, jsonbTest: { a: 2223 } }), ]) @@ -836,28 +861,29 @@ describe('DBDataSource', () => { type: z.literal('type1'), someKey: z.string(), }) - type Schema1 = z.infer const schema2 = z.object({ type: z.literal('type2'), someOtherKey: z.number(), }) - type Schema2 = z.infer - const schemaCombo = z.union([schema1, schema2]) - type SchemaCombo = z.infer + const schema = z.discriminatedUnion('type', [schema1, schema2]) - let ds: TestDataSource + let ds: TestDataSource beforeEach(() => { - ds = new TestDataSource({ - jsonbTest: schemaCombo, - }) + ds = new TestDataSource(schema) }) it('can process any of the given union schemas', async () => { - const rowData1: Schema1 = { type: 'type1', someKey: 'any string' } - const rowData2: Schema2 = { type: 'type2', someOtherKey: 1234 } + const rowData1 = { + type: 'type1', + someKey: 'any string', + } + const rowData2 = { + type: 'type2', + someOtherKey: 1234, + } const result = await ds.testInsert([ createRow({ id: 50, jsonbTest: rowData1 }), createRow({ id: 51, jsonbTest: rowData2 }), diff --git a/src/datasource/__tests__/loaders.test.ts b/src/datasource/__tests__/loaders.test.ts index 0418c240..a48a7d3a 100644 --- a/src/datasource/__tests__/loaders.test.ts +++ b/src/datasource/__tests__/loaders.test.ts @@ -1,44 +1,54 @@ import assert from 'assert' +import { z } from 'zod' import { LoaderFactory } from '../loaders' import { ExtendedDataLoader, GetDataMultiFunction } from '../loaders/types' import { match } from '../loaders/utils' -interface DummyRowType { - id: number - name: string - code: string +const DummyMetadata = { + id: { + nativeType: 'anything', + nativeName: 'id', + }, + name: { + nativeType: 'anything', + nativeName: 'name', + }, + code: { + nativeType: 'anything', + nativeName: 'code', + }, } -const columnTypes: Record = { - id: 'anything', - name: 'anything', - code: 'anything', -} +const DummyRowType = z.object({ + id: z.number(), + name: z.string(), + code: z.string(), +}) describe(LoaderFactory, () => { - const dummyBatchFn = jest.fn(async (): Promise => { - return [ - { id: 1, name: 'aaa', code: 'abc' }, - { id: 2, name: 'bbb', code: 'def' }, - { id: 3, name: 'Aaa', code: 'ghi' }, - { id: 4, name: 'Bbb', code: 'ABC' }, - { id: 5, name: 'AAa', code: 'DEF' }, - { id: 6, name: 'BBb', code: 'GHI' }, - { id: 7, name: 'AAA', code: 'abc' }, - { id: 8, name: 'BBB', code: 'def' }, - { id: 9, name: 'zzz', code: 'ghi' }, - { id: 10, name: 'ccc', code: 'ABC' }, - { id: 11, name: 'Ccc', code: 'DEF' }, - { id: 12, name: 'CCc', code: 'GHI' }, - { id: 13, name: 'CCC', code: 'zzz' }, - ] - }) + const dummyBatchFn = jest.fn( + async (): Promise[]> => { + return [ + { id: 1, name: 'aaa', code: 'abc' }, + { id: 2, name: 'bbb', code: 'def' }, + { id: 3, name: 'Aaa', code: 'ghi' }, + { id: 4, name: 'Bbb', code: 'ABC' }, + { id: 5, name: 'AAa', code: 'DEF' }, + { id: 6, name: 'BBb', code: 'GHI' }, + { id: 7, name: 'AAA', code: 'abc' }, + { id: 8, name: 'BBB', code: 'def' }, + { id: 9, name: 'zzz', code: 'ghi' }, + { id: 10, name: 'ccc', code: 'ABC' }, + { id: 11, name: 'Ccc', code: 'DEF' }, + { id: 12, name: 'CCc', code: 'GHI' }, + { id: 13, name: 'CCC', code: 'zzz' }, + ] + } + ) const getFactory = () => - new LoaderFactory(dummyBatchFn, dummyBatchFn, { - columnTypes, - }) - let factory: LoaderFactory + new LoaderFactory(dummyBatchFn, dummyBatchFn, DummyMetadata) + let factory: LoaderFactory> beforeEach(() => { factory = getFactory() @@ -49,7 +59,7 @@ describe(LoaderFactory, () => { let loader: ExtendedDataLoader< false, number, - DummyRowType | undefined, + z.infer | undefined, number > @@ -74,7 +84,12 @@ describe(LoaderFactory, () => { }) describe('multi: true, ignoreCase: false', () => { - let loader: ExtendedDataLoader + let loader: ExtendedDataLoader< + true, + string, + z.infer[], + string + > beforeEach(() => { loader = factory.create('code', 'any', { @@ -100,7 +115,7 @@ describe(LoaderFactory, () => { let loader: ExtendedDataLoader< false, string, - DummyRowType | undefined, + z.infer | undefined, string > @@ -125,7 +140,12 @@ describe(LoaderFactory, () => { }) describe('multi: true, ignoreCase: true', () => { - let loader: ExtendedDataLoader + let loader: ExtendedDataLoader< + true, + string, + z.infer[], + string + > beforeEach(() => { loader = factory.create('name', 'any', { @@ -165,7 +185,7 @@ describe(LoaderFactory, () => { it('accepts a query options function', async () => { const dummyRow = { id: 999, name: 'zzz', code: 'zzz' } - const getData = jest.fn((): [DummyRowType] => [dummyRow]) + const getData = jest.fn((): [z.infer] => [dummyRow]) const loader = factory.create('name', { getData }, () => ({ limit: 1 })) await loader.load('zzz') expect(getData).toHaveBeenCalledWith( @@ -237,7 +257,7 @@ describe(LoaderFactory, () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars ((_args, _columns, _types, _options) => [ dummyRow, - ]) as GetDataMultiFunction + ]) as GetDataMultiFunction> ) const loader = factory.createMulti(['name', 'code'], ['type1', 'type2'], { getData, @@ -254,7 +274,7 @@ describe(LoaderFactory, () => { it('accepts a query options function', async () => { const dummyRow = { id: 999, name: 'zzz', code: 'zzz' } - const getData = jest.fn((): [DummyRowType] => [dummyRow]) + const getData = jest.fn((): [z.infer] => [dummyRow]) const loader = factory.createMulti(['name', 'code'], { getData }, () => ({ limit: 1, })) diff --git a/src/datasource/__tests__/queries.test.ts b/src/datasource/__tests__/queries.test.ts index 65dc011a..f168c5c3 100644 --- a/src/datasource/__tests__/queries.test.ts +++ b/src/datasource/__tests__/queries.test.ts @@ -1,36 +1,51 @@ -import { snake } from 'case' import { sql } from 'slonik' +import { z } from 'zod' import { QueryBuilder } from '../queries' -interface DummyRowType { - id: number - name: string - optional?: string - nullable: string | null - optionallyNullable?: string | null - // i think this is a nonsense type but i needed to test the type system... - stringOrNumber: string | number | null - date?: Date -} - -const DummyRowColumnTypes = Object.freeze({ - id: 'these', - name: 'types', - optional: 'do', - nullable: 'not', - optionallyNullable: 'matter', - stringOrNumber: 'here', - date: 'date', +const DummyRowType = z.object({ + id: z.number(), + name: z.string(), + optional: z.string().optional(), + nullable: z.string().nullable(), + optionallyNullable: z.string().nullish(), + stringOrNumber: z.string().or(z.number()).nullable(), + date: z.date().nullable(), }) +const DummyMetadata = { + id: { + nativeType: 'these', + nativeName: 'id', + }, + name: { + nativeType: 'types', + nativeName: 'name', + }, + optional: { + nativeType: 'do', + nativeName: 'optional', + }, + nullable: { + nativeType: 'not', + nativeName: 'nullable', + }, + optionallyNullable: { + nativeType: 'matter', + nativeName: 'optionally_nullable', + }, + stringOrNumber: { + nativeType: 'here', + nativeName: 'string_or_number', + }, + date: { + nativeType: 'date', + nativeName: 'date', + }, +} + describe(QueryBuilder, () => { - const builder = new QueryBuilder( - 'any_table', - DummyRowColumnTypes, - snake, - {} - ) + const builder = new QueryBuilder('any_table', DummyMetadata, DummyRowType, {}) describe('clause generators', () => { describe('where', () => { @@ -232,7 +247,10 @@ describe(QueryBuilder, () => { describe('core query builders', () => { describe('select', () => { it('selects everything by default', () => { - expect(builder.select()).toMatchSnapshot() + expect(builder.select()).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('supports limits', () => { @@ -240,7 +258,10 @@ describe(QueryBuilder, () => { builder.select({ limit: 10, }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('can select for update', () => { @@ -248,7 +269,10 @@ describe(QueryBuilder, () => { builder.select({ forUpdate: true, }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('can select for update of another table', () => { @@ -256,7 +280,10 @@ describe(QueryBuilder, () => { builder.select({ forUpdate: 'another_table', }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('can select for update of multiple other tables', () => { @@ -264,29 +291,44 @@ describe(QueryBuilder, () => { builder.select({ forUpdate: ['table', 'another_table', 'more_tables'], }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) }) describe('count', () => { it('creates a count query', () => { - expect(builder.count()).toMatchSnapshot() + expect(builder.count()).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('can use where clauses', () => { - expect(builder.count({ where: { id: 1 } })).toMatchSnapshot() + expect(builder.count({ where: { id: 1 } })).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) }) describe('countGroup', () => { it('creates a count query with a groupBy clause', () => { - expect(builder.countGroup(['name'])).toMatchSnapshot() + expect(builder.countGroup(['name'])).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('can use where clauses', () => { expect( builder.countGroup(['nullable', 'optional'], { where: { id: 1 } }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) }) @@ -299,7 +341,10 @@ describe(QueryBuilder, () => { nullable: null, stringOrNumber: 1, }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('accepts many basic objects', () => { @@ -318,7 +363,10 @@ describe(QueryBuilder, () => { nullable: 'hi', }, ]) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('correctly inserts Date objects as ISO8601 strings', () => { @@ -330,7 +378,10 @@ describe(QueryBuilder, () => { stringOrNumber: 1, date: new Date('2020-11-30T00:00:00.000-0500'), }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('allows a single object with raw SQL values', () => { @@ -339,7 +390,10 @@ describe(QueryBuilder, () => { id: 1, name: sql.fragment`DEFAULT`, }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('allows multiple objects with raw SQL values', () => { @@ -354,7 +408,10 @@ describe(QueryBuilder, () => { name: sql.fragment`DEFAULT`, }, ]) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('allows multiple objects with a mix of value types', () => { @@ -369,7 +426,10 @@ describe(QueryBuilder, () => { name: sql.fragment`DEFAULT`, }, ]) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) }) @@ -384,19 +444,28 @@ describe(QueryBuilder, () => { optionallyNullable: null, stringOrNumber: 5, }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('accepts raw sql values', () => { expect( builder.update({ name: sql.fragment`anything i want` }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('correctly updates Date objects as ISO8601 strings', () => { expect( builder.update({ date: new Date('2020-11-30T00:00:00.000-0500') }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) }) @@ -407,7 +476,10 @@ describe(QueryBuilder, () => { }) it('can be forced to delete everything', () => { - expect(builder.delete(true)).toMatchSnapshot() + expect(builder.delete(true)).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('builds clauses correctly', () => { @@ -418,7 +490,10 @@ describe(QueryBuilder, () => { orderBy: 'id', having: { id: 1 }, }) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) }) @@ -433,7 +508,10 @@ describe(QueryBuilder, () => { ['id', 'name'], ['integer', 'text'] ) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) it('respects casing for column names', () => { @@ -443,7 +521,10 @@ describe(QueryBuilder, () => { ['optionallyNullable', 'stringOrNumber'], ['any', 'thing'] ) - ).toMatchSnapshot() + ).toMatchSnapshot({ + parser: expect.anything(), + type: expect.any(String), + }) }) }) }) diff --git a/src/datasource/loaders/FinderFactory.ts b/src/datasource/loaders/FinderFactory.ts index 2ec410a6..84edda84 100644 --- a/src/datasource/loaders/FinderFactory.ts +++ b/src/datasource/loaders/FinderFactory.ts @@ -2,8 +2,8 @@ import DataLoader from 'dataloader' import { FinderOptions, ExtendedDataLoader } from './types' -export interface FinderFunction { - (value: TInput): Promise +export interface FinderFunction { + (value: Input): Promise } export const isExtendedDataLoader = ( @@ -16,29 +16,29 @@ export const isExtendedDataLoader = ( ) } -export default class FinderFactory { +export default class FinderFactory { public create( - loader: ExtendedDataLoader, + loader: ExtendedDataLoader, options?: FinderOptions - ): FinderFunction + ): FinderFunction public create( - loader: ExtendedDataLoader, + loader: ExtendedDataLoader, options?: FinderOptions & { multi?: false } - ): FinderFunction + ): FinderFunction public create( - loader: DataLoader, + loader: DataLoader, options: FinderOptions & { multi: true } - ): FinderFunction + ): FinderFunction public create( - loader: DataLoader, + loader: DataLoader, options?: FinderOptions & { multi?: false } - ): FinderFunction + ): FinderFunction public create( loader: - | ExtendedDataLoader - | DataLoader, + | ExtendedDataLoader + | DataLoader, options: FinderOptions = {} - ): FinderFunction { + ): FinderFunction { if (isExtendedDataLoader(loader)) { options.multi = loader.isMultiLoader } @@ -46,19 +46,19 @@ export default class FinderFactory { const { multi = false } = options if (multi === true) { - return async (value: TInput): Promise => { + return async (value: TInput): Promise => { const result = await loader.load(value) if (!result) { return [] } - if (!Array.isArray(result)) { - return [result] + if (Array.isArray(result)) { + return result } - return result + return [result] } } - return async (value: TInput): Promise => { + return async (value: TInput): Promise => { const result = await loader.load(value) if (!result) { return null diff --git a/src/datasource/loaders/LoaderFactory.ts b/src/datasource/loaders/LoaderFactory.ts index 76fe3546..1f89e287 100644 --- a/src/datasource/loaders/LoaderFactory.ts +++ b/src/datasource/loaders/LoaderFactory.ts @@ -1,40 +1,36 @@ import DataLoader from 'dataloader' import { QueryOptions } from '../queries/QueryBuilder' +import { TableMetadata } from '../types' import { ExtendedDataLoader, GetDataFunction, GetDataMultiFunction, - LoaderFactoryOptions, LoaderOptions, - SearchableKeys, + // SearchableKeys, } from './types' -import { identity, match } from './utils' - -export default class LoaderFactory { - private defaultOptions = { - columnToKey: identity, - keyToColumn: identity, - } - private options: Required> +import { match } from './utils' +export default class LoaderFactory< + TableSchema, + SearchableKeys extends string & keyof TableSchema = string & keyof TableSchema +> { protected loaders: Array< - ExtendedDataLoader + ExtendedDataLoader< + boolean, + unknown, + TableSchema[] | TableSchema | undefined + > > = [] constructor( - private getData: GetDataFunction, - private getDataMulti: GetDataMultiFunction, - options: LoaderFactoryOptions - ) { - this.options = { - ...this.defaultOptions, - ...options, - } - } + private getData: GetDataFunction, + private getDataMulti: GetDataMultiFunction, + private metadata: TableMetadata + ) {} protected async autoPrimeLoaders( - result: TRowType | undefined, + result: TableSchema | undefined, loaders = this.loaders ): Promise { if (!result) { @@ -50,12 +46,12 @@ export default class LoaderFactory { } if (typeof loader.columns === 'string') { - loader.prime(result[loader.columns as keyof TRowType], result) + loader.prime(result[loader.columns as keyof TableSchema], result) } else { const key = loader.columns.reduce( (key, column) => ({ ...key, - [column]: result[column as keyof TRowType], + [column]: result[column as keyof TableSchema], }), {} ) @@ -64,53 +60,53 @@ export default class LoaderFactory { } } - public create< - TColumnName extends SearchableKeys & keyof TRowType & string - >( + public create( key: TColumnName, - options: LoaderOptions & { + options: LoaderOptions & { multi: true }, - queryOptions?: () => QueryOptions - ): ExtendedDataLoader - public create< - TColumnName extends SearchableKeys & keyof TRowType & string - >( + queryOptions?: () => QueryOptions + ): ExtendedDataLoader + public create( key: TColumnName, columnType: string, - options: LoaderOptions & { + options: LoaderOptions & { multi: true }, - queryOptions?: () => QueryOptions - ): ExtendedDataLoader - public create< - TColumnName extends SearchableKeys & keyof TRowType & string - >( + queryOptions?: () => QueryOptions + ): ExtendedDataLoader + public create( key: TColumnName, - options?: LoaderOptions & { + options?: LoaderOptions & { multi?: false }, - queryOptions?: () => QueryOptions - ): ExtendedDataLoader - public create< - TColumnName extends SearchableKeys & keyof TRowType & string - >( + queryOptions?: () => QueryOptions + ): ExtendedDataLoader< + false, + TableSchema[TColumnName], + TableSchema | undefined + > + public create( key: TColumnName, columnType?: string, - options?: LoaderOptions & { + options?: LoaderOptions & { multi?: false }, - queryOptions?: () => QueryOptions - ): ExtendedDataLoader + queryOptions?: () => QueryOptions + ): ExtendedDataLoader< + false, + TableSchema[TColumnName], + TableSchema | undefined + > public create< - TColumnName extends SearchableKeys & keyof TRowType & string, - TColType extends TRowType[TColumnName] & (string | number) + TColumnName extends SearchableKeys, + TColType extends TableSchema[TColumnName] & (string | number) >( key: TColumnName, - columnType?: string | LoaderOptions, - options?: LoaderOptions | (() => QueryOptions), - queryOptions?: () => QueryOptions - ): DataLoader { + columnType?: string | LoaderOptions, + options?: LoaderOptions | (() => QueryOptions), + queryOptions?: () => QueryOptions + ): DataLoader { if (typeof options === 'function') { queryOptions = options options = undefined @@ -124,7 +120,7 @@ export default class LoaderFactory { const getData = actualOptions.getData || this.getData - const type: string = columnType || this.options.columnTypes[key] + const type: string = columnType || this.metadata[key].nativeType const { multi = false, @@ -136,7 +132,7 @@ export default class LoaderFactory { const loader = new DataLoader< TColType, - TRowType[] | (TRowType | undefined) + TableSchema[] | (TableSchema | undefined) >(async (args: readonly TColType[]) => { const data = await getData(args, key, type, loader, { ...actualOptions, @@ -166,7 +162,7 @@ export default class LoaderFactory { }) as ExtendedDataLoader< typeof multi, TColType, - TRowType[] | (TRowType | undefined) + TableSchema[] | (TableSchema | undefined) > loader.isMultiLoader = multi loader.columns = key @@ -175,76 +171,68 @@ export default class LoaderFactory { } public createMulti< - TColumnNames extends Array< - SearchableKeys & keyof TRowType & string - >, + TColumnNames extends Array, TBatchKey extends { - [K in TColumnNames[0]]: TRowType[K] + [K in TColumnNames[0]]: TableSchema[K] } >( key: TColumnNames, - options: LoaderOptions & { + options: LoaderOptions & { multi: true }, - queryOptions?: () => QueryOptions - ): ExtendedDataLoader + queryOptions?: () => QueryOptions + ): ExtendedDataLoader public createMulti< - TColumnNames extends Array< - SearchableKeys & keyof TRowType & string - >, + TColumnNames extends Array, TBatchKey extends { - [K in TColumnNames[0]]: TRowType[K] + [K in TColumnNames[0]]: TableSchema[K] } >( key: TColumnNames, columnTypes: string[], - options: LoaderOptions & { + options: LoaderOptions & { multi: true }, - queryOptions?: () => QueryOptions - ): ExtendedDataLoader + queryOptions?: () => QueryOptions + ): ExtendedDataLoader public createMulti< - TColumnNames extends Array< - SearchableKeys & keyof TRowType & string - >, + TColumnNames extends Array, TBatchKey extends { - [K in TColumnNames[0]]: TRowType[K] + [K in TColumnNames[0]]: TableSchema[K] } >( key: TColumnNames, - options?: LoaderOptions & { + options?: LoaderOptions & { multi?: false }, - queryOptions?: () => QueryOptions - ): ExtendedDataLoader + queryOptions?: () => QueryOptions + ): ExtendedDataLoader public createMulti< - TColumnNames extends Array< - SearchableKeys & keyof TRowType & string - >, + TColumnNames extends Array, TBatchKey extends { - [K in TColumnNames[0]]: TRowType[K] + [K in TColumnNames[0]]: TableSchema[K] } >( key: TColumnNames, columnTypes?: string[], - options?: LoaderOptions & { + options?: LoaderOptions & { multi?: false }, - queryOptions?: () => QueryOptions - ): ExtendedDataLoader + queryOptions?: () => QueryOptions + ): ExtendedDataLoader public createMulti< - TColumnNames extends Array< - SearchableKeys & keyof TRowType & string - >, + TColumnNames extends Array, TBatchKey extends { - [K in TColumnNames[0]]: TRowType[K] + [K in TColumnNames[0]]: TableSchema[K] } >( keys: TColumnNames, - columnTypes?: string[] | LoaderOptions, - options?: LoaderOptions | (() => QueryOptions), - queryOptions?: () => QueryOptions - ): DataLoader { + columnTypes?: string[] | LoaderOptions, + options?: + | LoaderOptions + | (() => QueryOptions), + queryOptions?: () => QueryOptions + ): DataLoader { if (typeof options === 'function') { queryOptions = options options = undefined @@ -259,7 +247,7 @@ export default class LoaderFactory { const getData = actualOptions.getData || this.getDataMulti const types: string[] = - columnTypes || keys.map((key) => this.options.columnTypes[key]) + columnTypes || keys.map((key) => this.metadata[key].nativeType) if (types.length !== keys.length) { throw new Error('Same number of types and keys must be provided') @@ -278,7 +266,7 @@ export default class LoaderFactory { const loader = new DataLoader< TBatchKey, - TRowType[] | (TRowType | undefined), + TableSchema[] | (TableSchema | undefined), string >( async (args: readonly TBatchKey[]) => { @@ -303,11 +291,11 @@ export default class LoaderFactory { }) return args.map((batchKey) => { - const callback = (row: TRowType) => { + const callback = (row: TableSchema) => { return Object.entries(batchKey).every(([key, value]) => match( value as string | number, - row[key as keyof TRowType], + row[key as keyof TableSchema], ignoreCase ) ) @@ -323,7 +311,7 @@ export default class LoaderFactory { ) as ExtendedDataLoader< boolean, TBatchKey, - TRowType[] | (TRowType | undefined) + TableSchema[] | (TableSchema | undefined) > loader.isMultiLoader = multi loader.columns = [...keys] diff --git a/src/datasource/loaders/types.ts b/src/datasource/loaders/types.ts index c2580211..97d02316 100644 --- a/src/datasource/loaders/types.ts +++ b/src/datasource/loaders/types.ts @@ -1,5 +1,6 @@ import type DataLoader from 'dataloader' import type { QueryOptions } from '../queries/QueryBuilder' +import { TableMetadata } from '../types' export type SearchableKeys = { [K in keyof T]?: T extends { [_ in K]?: SearchableType } ? K : never @@ -31,10 +32,10 @@ export interface GetDataMultiFunction { ): readonly TRowType[] | Promise } -export interface LoaderFactoryOptions { +export interface LoaderFactoryOptions { columnToKey?: (column: string) => string keyToColumn?: (column: string) => string - columnTypes: Record + metadata: TableMetadata } export type ExtendedDataLoader< diff --git a/src/datasource/queries/QueryBuilder.ts b/src/datasource/queries/QueryBuilder.ts index c4d1cbcd..ae72ff48 100644 --- a/src/datasource/queries/QueryBuilder.ts +++ b/src/datasource/queries/QueryBuilder.ts @@ -6,7 +6,9 @@ import { SqlToken, } from 'slonik' import { raw } from 'slonik-sql-tag-raw' +import { z } from 'zod' import { TypedSqlQuery } from '../../types' +import { TableMetadata, TableSchema } from '../types' import { AllowSql, @@ -23,11 +25,11 @@ import { } from './types' import { isOrderTuple, isFragmentSqlToken, isSqlToken } from './utils' -export interface QueryOptions { - where?: Conditions | FragmentSqlToken[] | FragmentSqlToken +export interface QueryOptions { + where?: Conditions | FragmentSqlToken[] | FragmentSqlToken groupBy?: ColumnList orderBy?: OrderColumnList - having?: Conditions | FragmentSqlToken[] | FragmentSqlToken + having?: Conditions | FragmentSqlToken[] | FragmentSqlToken limit?: LimitClause } @@ -36,25 +38,37 @@ export interface SelectOptions { } const EMPTY = sql.fragment`` -const noop = (v: string): string => v const CONDITIONS_TABLE = 'conditions' +const COUNT_SCHEMA = z.object({ count: z.number() }) + +function isColumn( + column: string | undefined, + metadata: TableMetadata +): column is keyof typeof metadata { + if (!column) { + return false + } + return Object.keys(metadata).includes(column) +} export default class QueryBuilder< - TRowType, - TInsertType extends { [K in keyof TRowType]?: unknown } = TRowType + Metadata extends TableMetadata, + SelectSchema extends TableSchema, + TableColumns$$private extends string & keyof Metadata = string & + keyof Metadata > { constructor( public readonly table: string, - protected readonly columnTypes: Record, - protected readonly columnCase: (value: string) => string = noop, - protected readonly defaultOptions: QueryOptions + protected readonly metadata: Metadata, + protected readonly selectSchema: SelectSchema, + protected readonly defaultOptions: QueryOptions> ) { this.value = this.value.bind(this) } protected getOptions( - options?: QueryOptions - ): QueryOptions { + options?: QueryOptions> + ): QueryOptions> { return { ...this.defaultOptions, ...options, @@ -72,7 +86,11 @@ export default class QueryBuilder< } else if (includeTable) { names.push(this.table) } - column !== undefined && names.push(this.columnCase(column)) + if (isColumn(column, this.metadata)) { + names.push(this.metadata[column].nativeName) + } else if (column) { + names.push(column) + } return sql.identifier(names) } @@ -83,10 +101,12 @@ export default class QueryBuilder< /* Public core query builders */ - public select(options?: QueryOptions & SelectOptions) { + public select( + options?: QueryOptions> & SelectOptions + ): TypedSqlQuery { options = this.getOptions(options) - return sql.unsafe` + return sql.type(this.selectSchema)` SELECT * FROM ${this.identifier()} ${options.where ? this.where(options.where) : EMPTY} @@ -98,8 +118,8 @@ export default class QueryBuilder< } public insert( - rows: ValueOrArray>, - options?: QueryOptions + rows: ValueOrArray>>, + options?: QueryOptions> ) { options = this.getOptions(options) @@ -110,34 +130,37 @@ export default class QueryBuilder< return this.insertDefaultValues(options) } - if (!Array.isArray(rows)) { - rows = [rows] - } + const rowsArray = Array.isArray(rows) ? rows : [rows] - if (rows.length === 0) { + if (rowsArray.length === 0) { throw new Error('insert requires at least one row') } - if (this.isUniformRowset(rows)) { - return this.insertUniform(rows, options) + if (this.isUniformRowset(rowsArray)) { + return this.insertUniform(rowsArray, options) } - return this.insertNonUniform(rows, options) + return this.insertNonUniform(rowsArray, options) } - public update(values: UpdateSet, options?: QueryOptions) { + public update( + values: UpdateSet>, + options?: QueryOptions> + ): TypedSqlQuery { options = this.getOptions(options) - const updateQuery = sql.unsafe` + const updateQuery = sql.type(this.selectSchema)` UPDATE ${this.identifier()} ${this.set(values)} ${options.where ? this.where(options.where) : EMPTY} RETURNING * ` - return this.wrapCte('update', updateQuery, options) + return this.wrapCte('update', updateQuery, options, this.selectSchema) } - public delete(options: QueryOptions | true) { + public delete( + options: QueryOptions> | true + ): TypedSqlQuery { const force = options === true options = this.getOptions(options === true ? {} : options) @@ -148,41 +171,46 @@ export default class QueryBuilder< ) } - const deleteQuery = sql.unsafe` + const deleteQuery = sql.type(this.selectSchema)` DELETE FROM ${this.identifier()} ${options.where ? this.where(options.where) : EMPTY} RETURNING * ` - return this.wrapCte('delete', deleteQuery, options) + return this.wrapCte('delete', deleteQuery, options, this.selectSchema) } public count( options?: Omit< - QueryOptions, + QueryOptions>, 'orderBy' | 'groupBy' | 'limit' | 'having' > ) { options = this.getOptions(options) - return sql.unsafe` + return sql.type(COUNT_SCHEMA)` SELECT COUNT(*) FROM ${this.identifier()} ${options.where ? this.where(options.where) : EMPTY} ` } - public countGroup>( - groupColumns: TGroup & Array, + public countGroup>>( + groupColumns: TGroup & Array>, options?: Omit< - QueryOptions, + QueryOptions>, 'orderBy' | 'groupBy' | 'limit' | 'having' > ) { options = this.getOptions(options) + const mask = groupColumns.reduce( + (res, key) => ({ ...res, [key]: true }), + {} as Record + ) + const columns = this.columnList(groupColumns) - return sql.unsafe` + return sql.type(COUNT_SCHEMA.merge(this.selectSchema.pick(mask)))` SELECT ${sql.join(columns, sql.fragment`, `)}, COUNT(*) FROM ${this.identifier()} ${options.where ? this.where(options.where) : EMPTY} @@ -197,7 +225,10 @@ export default class QueryBuilder< * @param rawConditions Conditions expression */ public where( - rawConditions: Conditions | FragmentSqlToken[] | FragmentSqlToken + rawConditions: + | Conditions> + | FragmentSqlToken[] + | FragmentSqlToken ): FragmentSqlToken { const conditions = isFragmentSqlToken(rawConditions) ? rawConditions @@ -268,7 +299,10 @@ export default class QueryBuilder< } public having( - rawConditions: Conditions | FragmentSqlToken[] | FragmentSqlToken + rawConditions: + | Conditions> + | FragmentSqlToken[] + | FragmentSqlToken ): FragmentSqlToken { const conditions = isFragmentSqlToken(rawConditions) ? rawConditions @@ -314,7 +348,7 @@ export default class QueryBuilder< /* Public query-building utilities */ public and( - rawConditions: Conditions | FragmentSqlToken[] + rawConditions: Conditions> | FragmentSqlToken[] ): FragmentSqlToken { const conditions = this.conditions(rawConditions) @@ -326,7 +360,7 @@ export default class QueryBuilder< } public or( - rawConditions: Conditions | FragmentSqlToken[] + rawConditions: Conditions> | FragmentSqlToken[] ): FragmentSqlToken { const conditions = this.conditions(rawConditions) @@ -349,14 +383,15 @@ export default class QueryBuilder< /* Protected query-building utilities */ - protected wrapCte( + protected wrapCte( queryName: string, - query: TypedSqlQuery, - options: Omit, 'where'> - ) { + query: TypedSqlQuery, + options: Omit>, 'where'>, + schema: QuerySchema + ): TypedSqlQuery { const queryId = this.identifier(queryName + '_rows', false) - return sql.unsafe` + return sql.type(schema)` WITH ${queryId} AS ( ${query} ) SELECT * @@ -368,21 +403,21 @@ export default class QueryBuilder< ` } - protected insertDefaultValues(options?: QueryOptions) { + protected insertDefaultValues(options?: QueryOptions>) { options = this.getOptions(options) - const insertQuery = sql.unsafe` + const insertQuery = sql.type(this.selectSchema)` INSERT INTO ${this.identifier()} DEFAULT VALUES RETURNING * ` - return this.wrapCte('insert', insertQuery, options) + return this.wrapCte('insert', insertQuery, options, this.selectSchema) } protected insertUniform( - rows: TInsertType[], - options?: QueryOptions + rows: z.infer[], + options?: QueryOptions> ) { options = this.getOptions(options) @@ -394,26 +429,26 @@ export default class QueryBuilder< const tableExpression = columns.map((column) => { return sql.fragment`${sql.identifier([column])} ${raw( - this.columnTypes[column] + this.metadata[column].nativeType )}` }) - const insertQuery = sql.unsafe` + const insertQuery = sql.type(this.selectSchema)` INSERT INTO ${this.identifier()} (${columnExpression}) SELECT * - FROM jsonb_to_recordset(${JSON.stringify(rows)}) AS (${sql.join( + FROM json_to_recordset(${sql.json(rows)}) AS (${sql.join( tableExpression, sql.fragment`, ` )}) RETURNING * ` - return this.wrapCte('insert', insertQuery, options) + return this.wrapCte('insert', insertQuery, options, this.selectSchema) } protected insertNonUniform( - rows: AllowSql[], - options?: QueryOptions + rows: AllowSql>[], + options?: QueryOptions> ) { options = this.getOptions(options) @@ -433,19 +468,19 @@ export default class QueryBuilder< return sql.fragment`(${sql.join(values, sql.fragment`, `)})` }) - const insertQuery = sql.unsafe` + const insertQuery = sql.type(this.selectSchema)` INSERT INTO ${this.identifier()} (${columnExpression}) VALUES ${sql.join(rowExpressions, sql.fragment`, `)} RETURNING * ` - return this.wrapCte('insert', insertQuery, options) + return this.wrapCte('insert', insertQuery, options, this.selectSchema) } protected conditions( - conditions: Conditions | FragmentSqlToken[] + conditions: Conditions> | FragmentSqlToken[] ): FragmentSqlToken[] { - if (Array.isArray(conditions)) { + if (conditions.constructor === Array) { return conditions } @@ -474,7 +509,7 @@ export default class QueryBuilder< } else { sqlValue = this.any( nonNullValues, - this.columnTypes[column as keyof TRowType] + this.metadata[column as keyof z.infer].nativeType ) } @@ -499,7 +534,7 @@ export default class QueryBuilder< }) } - protected set(values: UpdateSet): FragmentSqlToken { + protected set(values: UpdateSet>): FragmentSqlToken { const pairs = Object.entries(values) .filter(([column, value]) => column !== undefined && value !== undefined) .map(([column, value]) => { @@ -525,20 +560,22 @@ export default class QueryBuilder< } public multiColumnBatchGet< - TColumnNames extends Array + TColumnNames extends Array & string> >( - args: ReadonlyArray>, + args: ReadonlyArray< + Record[TColumnNames[0]]> + >, columns: TColumnNames, types: string[], - options?: QueryOptions & SelectOptions + options?: QueryOptions> & SelectOptions ) { options = this.getOptions(options) const typedColumns = this.jsonTypedColumns(columns, types) - return sql.unsafe` + return sql.type(this.selectSchema)` SELECT ${this.identifier()}.* FROM ${this.identifier()}, ${this.jsonRowComparison( - args as ReadonlyArray>, + args as ReadonlyArray>>, typedColumns )} WHERE ${this.columnConditionsMap(columns, types)} @@ -549,13 +586,12 @@ export default class QueryBuilder< ` } - private jsonTypedColumns>( - columns: TColumnNames, - types: string[] - ): FragmentSqlToken { + private jsonTypedColumns< + TColumnNames extends Array & string> + >(columns: TColumnNames, types: string[]): FragmentSqlToken { return sql.fragment`${sql.join( columns.map((columnName, idx) => { - const column = this.columnCase(columnName) + const column = this.metadata[columnName].nativeName const type = types[idx] return sql.fragment`(conditions->>${this.jsonIdentifier( column @@ -569,27 +605,28 @@ export default class QueryBuilder< } private jsonRowComparison( - args: ReadonlyArray>, + args: ReadonlyArray>>, typedColumns: FragmentSqlToken ) { const jsonObject: SerializableValue[] = args.map((entry) => Object.entries(entry).reduce((result, [key, value]) => { return { ...result, - [this.columnCase(key)]: value as SerializableValue, + [this.metadata[key as TableColumns$$private].nativeName]: + value as SerializableValue, } }, {} as object & SerializableValue) ) - return sql.unsafe` + return sql.fragment` ( SELECT ${typedColumns} - FROM jsonb_array_elements(${sql.json(jsonObject)}) AS conditions + FROM json_array_elements(${sql.json(jsonObject)}) AS conditions ) AS ${this.identifier(`${this.table}_${CONDITIONS_TABLE}`, false)} ` } private columnConditionsMap( - columns: Array, + columns: Array & string>, types: string[] ): FragmentSqlToken { return sql.fragment`${sql.join( @@ -658,11 +695,11 @@ export default class QueryBuilder< } private rowsetKeys( - rows: AllowSql[] - ): Array { + rows: AllowSql>[] + ): Array & keyof z.infer & string> { const allKeys = rows.map(Object.keys) const keySet = new Set(...allKeys) as Set< - keyof TInsertType & keyof TRowType & string + keyof z.infer & keyof z.infer & string > return Array.from(keySet) } @@ -683,8 +720,8 @@ export default class QueryBuilder< * single parameter and only non-uniform rows spread out as normal. */ private isUniformRowset( - rowset: AllowSql[] - ): rowset is TInsertType[] { + rowset: AllowSql>[] + ): rowset is z.infer[] { // we can only parameterize the entire row if every row has only values that // are either primitive values (e.g. string, number) or are convertable // to primitive values (e.g. Date) diff --git a/src/datasource/queries/types.ts b/src/datasource/queries/types.ts index 0624b889..2670abac 100644 --- a/src/datasource/queries/types.ts +++ b/src/datasource/queries/types.ts @@ -65,3 +65,22 @@ export type LimitClause = | 'ALL' | FragmentSqlToken | [number | 'ALL', number] + +export type ColumnTypes = Record + +type IfUnknownOrAny = unknown extends T ? Y : N +type IfAny = (T extends never ? 1 : 0) extends 0 ? N : Y + +/* eslint-disable @typescript-eslint/no-explicit-any */ +type ArrayType = IfUnknownOrAny< + T, + IfAny, + Extract +> +/* eslint-enable @typescript-eslint/no-explicit-any */ + +declare global { + interface ArrayConstructor { + isArray(arg: T): arg is ArrayType + } +} diff --git a/src/datasource/queries/utils.ts b/src/datasource/queries/utils.ts index 40d1ade3..3498b57c 100644 --- a/src/datasource/queries/utils.ts +++ b/src/datasource/queries/utils.ts @@ -8,7 +8,11 @@ import { FragmentToken } from 'slonik/dist/src/tokens' import { OrderTuple } from './types' export const isSqlToken = (subject: unknown): subject is SqlToken => { - return isSlonikToken(subject) + try { + return isSlonikToken(subject) + } catch { + return false + } } export const isFragmentSqlToken = (v: unknown): v is FragmentSqlToken => { diff --git a/src/datasource/types.ts b/src/datasource/types.ts new file mode 100644 index 00000000..f403866c --- /dev/null +++ b/src/datasource/types.ts @@ -0,0 +1,48 @@ +import { AsyncLocalStorage } from 'async_hooks' +import DataLoader from 'dataloader' +import { + DatabasePool, + DatabaseTransactionConnection, + IdentifierNormalizer, +} from 'slonik' +import { z } from 'zod' + +import { QueryOptions as BuilderOptions } from './queries/QueryBuilder' + +export interface QueryOptions + extends BuilderOptions { + eachResult?: LoaderCallback + expected?: 'one' | 'many' | 'maybeOne' | 'any' +} + +export interface KeyNormalizers { + keyToColumn: IdentifierNormalizer + columnToKey: IdentifierNormalizer +} +export type LoaderCallback = ( + value: TResultType, + index: number, + array: readonly TResultType[] +) => void + +interface AsyncStorage { + transaction: DatabaseTransactionConnection + loaderLookups: Array<[Loader, readonly unknown[]]> +} + +type Loader = DataLoader + +export interface ExtendedDatabasePool extends DatabasePool { + async: AsyncLocalStorage> +} + +export type TableSchema = z.ZodObject<{ + [K in string & TableColumns]: z.ZodType +}> +export type ColumnMetadata = { + nativeName: string + nativeType: string +} +export type TableMetadata = { + [K in string & ColumnNames]: ColumnMetadata +} diff --git a/src/generator/Generator.ts b/src/generator/Generator.ts index f570fdb7..99a8a894 100644 --- a/src/generator/Generator.ts +++ b/src/generator/Generator.ts @@ -9,20 +9,36 @@ import { } from 'typescript' import * as Case from 'case' -import { EnumBuilder, InsertTypeBuilder, TableBuilder } from './builders' -import NodeBuilder from './builders/NodeBuilder' +import { + EnumBuilder, + InsertSchemaBuilder, + InsertTypeBuilder, + SelectSchemaBuilder, + TableBuilder, + TableMetadataBuilder, +} from './builders' +import { Buildable, isMultiBuildable } from './builders/NodeBuilder' import TypeObjectBuilder from './builders/TypeObjectBuilder' import { SchemaInfo, TypeRegistry } from './database' import { CaseFunction, Transformations } from './types' import UtilityTypesBuilder from './builders/UtilityTypesBuilder' import ZodSchemaBuilder from './builders/ZodSchemaBuilder' +import SingleNamedImportBuilder from './builders/SingleNamedImportBuilder' export interface GeneratorOptions { schema: SchemaInfo + genSelectSchemas?: boolean + genInsertSchemas?: boolean + genTableMetadata?: boolean + /** @deprecated */ genEnums?: boolean + /** @deprecated */ genInsertTypes?: boolean + /** @deprecated */ genTables?: boolean + /** @deprecated */ genTypeObjects?: boolean + /** @deprecated */ genSchemaObjects?: boolean transformColumns?: Transformations.Column transformEnumMembers?: Transformations.EnumMember @@ -37,10 +53,18 @@ export default class Generator { private types: TypeRegistry public readonly generate: { + selectSchemas: boolean + insertSchemas: boolean + tableMetadata: boolean + /** @deprecated */ enums: boolean + /** @deprecated */ insertTypes: boolean + /** @deprecated */ tables: boolean + /** @deprecated */ typeObjects: boolean + /** @deprecated */ schemaObjects: boolean } @@ -48,11 +72,14 @@ export default class Generator { constructor({ schema, - genEnums = true, - genInsertTypes = true, - genTables = true, - genTypeObjects = true, - genSchemaObjects = true, + genSelectSchemas: selectSchemas = true, + genInsertSchemas: insertSchemas = true, + genTableMetadata: tableMetadata = true, + genEnums = false, + genInsertTypes = false, + genTables = false, + genTypeObjects = false, + genSchemaObjects = false, transformColumns = 'none', transformEnumMembers = 'pascal', transformTypeNames = 'pascal', @@ -66,6 +93,9 @@ export default class Generator { this.types = new TypeRegistry() this.generate = Object.freeze({ + selectSchemas, + insertSchemas, + tableMetadata, enums: genEnums, insertTypes: genInsertTypes, tables: genTables, @@ -86,11 +116,11 @@ export default class Generator { } public async build(): Promise { - const statementBuilders: NodeBuilder[] = [] + const statementBuilders: Buildable[] = [] try { - await this.buildEnums(statementBuilders) - await this.buildTables(statementBuilders) + statementBuilders.push(...(await this.enumBuilders())) + statementBuilders.push(...(await this.tableBuilders())) } catch (error) { if (error instanceof Error) { console.error(error.message) @@ -98,12 +128,15 @@ export default class Generator { throw error } - const statements = statementBuilders.map((builder) => - builder.buildNode() + statementBuilders.unshift(new UtilityTypesBuilder()) + statementBuilders.unshift(...(await this.importBuilders())) + + const statements = statementBuilders.flatMap((builder) => + isMultiBuildable(builder) ? builder.buildNodes() : builder.buildNode() ) const sourceFile = factory.createSourceFile( - [...new UtilityTypesBuilder().buildNodes(), ...statements], + statements, factory.createToken(SyntaxKind.EndOfFileToken), NodeFlags.None ) @@ -111,9 +144,29 @@ export default class Generator { return this.printer.printFile(sourceFile) } - private async buildEnums(builders: NodeBuilder[]): Promise { + private async importBuilders(): Promise[]> { + const builders: Buildable[] = [] + if ( + this.generate.selectSchemas || + this.generate.insertSchemas || + this.generate.schemaObjects + ) { + builders.unshift( + new SingleNamedImportBuilder( + { name: 'z', source: 'zod' }, + this.types, + this.transform + ) + ) + } + return builders + } + + private async enumBuilders(): Promise[]> { const enums = await this.schema.getEnums() + const builders: Buildable[] = [] + enums.forEach((enumInfo) => { if (this.generate.enums) { const builder = new EnumBuilder(enumInfo, this.types, this.transform) @@ -121,14 +174,54 @@ export default class Generator { builders.push(builder) } }) + + return builders } - private async buildTables(builders: NodeBuilder[]): Promise { + private async tableBuilders(): Promise[]> { const tables = await this.schema.getTables() + const builders: Buildable[] = [] + const processedTableNames: string[] = [] tables.forEach((tableInfo) => { + if (processedTableNames.includes(tableInfo.name)) { + console.warn( + `Duplicate table name detected: ${tableInfo.name}. ` + + `This is not supported, skipping.` + ) + return + } + + if (this.generate.tableMetadata) { + const builder = new TableMetadataBuilder( + tableInfo, + this.types, + this.transform + ) + processedTableNames.push(tableInfo.name) + builders.push(builder) + } + + if (this.generate.selectSchemas) { + const builder = new SelectSchemaBuilder( + tableInfo, + this.types, + this.transform + ) + builders.push(builder) + } + + if (this.generate.insertSchemas) { + const builder = new InsertSchemaBuilder( + tableInfo, + this.types, + this.transform + ) + builders.push(builder) + } + if (this.generate.tables) { const builder = new TableBuilder(tableInfo, this.types, this.transform) this.types.add(builder.name, builder.typename().text, 'table') @@ -168,5 +261,7 @@ export default class Generator { processedTableNames.push(tableInfo.name) } }) + + return builders } } diff --git a/src/generator/__tests__/Generator.test.ts b/src/generator/__tests__/Generator.test.ts index 41f24a73..601b3975 100644 --- a/src/generator/__tests__/Generator.test.ts +++ b/src/generator/__tests__/Generator.test.ts @@ -32,7 +32,7 @@ describe(Generator, () => { schema: dummySchema, }) - it('generates everything', async () => { + it('generates metadata and select/insert schemas', async () => { expect(await instance.build()).toMatchSnapshot() }) }) @@ -40,6 +40,9 @@ describe(Generator, () => { describe('with everything disabled', () => { const instance = new Generator({ schema: dummySchema, + genTableMetadata: false, + genSelectSchemas: false, + genInsertSchemas: false, genEnums: false, genInsertTypes: false, genTables: false, @@ -55,6 +58,9 @@ describe(Generator, () => { describe('with tables enabled', () => { const instance = new Generator({ schema: dummySchema, + genTableMetadata: false, + genSelectSchemas: false, + genInsertSchemas: false, genEnums: false, genInsertTypes: false, genTables: true, @@ -71,6 +77,9 @@ describe(Generator, () => { describe('with enums enabled', () => { const instance = new Generator({ schema: dummySchema, + genTableMetadata: false, + genSelectSchemas: false, + genInsertSchemas: false, genEnums: true, genInsertTypes: false, genTables: false, @@ -86,6 +95,9 @@ describe(Generator, () => { describe('with insert types enabled', () => { const instance = new Generator({ schema: dummySchema, + genTableMetadata: false, + genSelectSchemas: false, + genInsertSchemas: false, genEnums: false, genInsertTypes: true, genTables: false, @@ -102,6 +114,9 @@ describe(Generator, () => { describe('with type objects enabled', () => { const instance = new Generator({ schema: dummySchema, + genTableMetadata: false, + genSelectSchemas: false, + genInsertSchemas: false, genEnums: false, genInsertTypes: false, genTables: false, @@ -118,6 +133,9 @@ describe(Generator, () => { describe('with schema objects enabled', () => { const instance = new Generator({ schema: dummySchema, + genTableMetadata: false, + genSelectSchemas: false, + genInsertSchemas: false, genEnums: false, genInsertTypes: false, genTables: false, @@ -177,16 +195,16 @@ describe(Generator, () => { const instance = new Generator({ schema: dummySchema, }) - const buildTablesSpy = jest.spyOn(instance, 'buildTables' as never) + const tableBuildersSpy = jest.spyOn(instance, 'tableBuilders' as never) beforeEach(() => { - buildTablesSpy.mockImplementation(() => { + tableBuildersSpy.mockImplementation(() => { throw new Error('testing error handling') }) }) afterEach(() => { - buildTablesSpy.mockReset() + tableBuildersSpy.mockReset() }) describe('when builders throw an error', () => { diff --git a/src/generator/__tests__/__snapshots__/Generator.test.ts.snap b/src/generator/__tests__/__snapshots__/Generator.test.ts.snap index 406e5220..fdcbd2ba 100644 --- a/src/generator/__tests__/__snapshots__/Generator.test.ts.snap +++ b/src/generator/__tests__/__snapshots__/Generator.test.ts.snap @@ -9,7 +9,8 @@ exports[`Generator build when builders throw an error logs the error and rethrow `; exports[`Generator when re-registering an already-registered type outputs a warning 1`] = ` -"export type PrimitiveValueType = string | number | boolean | null; +"import { z } from zod; +export type PrimitiveValueType = string | number | boolean | null; export type SimpleValueType = PrimitiveValueType | Date; export type SerializableValueType = SimpleValueType | { [key in string]: SerializableValueType | undefined; @@ -17,8 +18,9 @@ export type SerializableValueType = SimpleValueType | { export type MapToSerializable = T extends SerializableValueType ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends { [K in keyof T]: K extends string ? T[K] extends SimpleValueType ? T[K] : T[K] extends Function ? never : MapToSerializable : never; } ? T : never; -export interface SomehowDuplicatedTypeName { -} +export const DEFAULT = Symbol("DEFAULT"); +export const SomehowDuplicatedTypeName$Metadata = {} as const; +export const SomehowDuplicatedTypeName$Schema = z.object({}); export const SomehowDuplicatedTypeName$Schema = z.object({}); export interface SomehowDuplicatedTypeName { } @@ -28,13 +30,14 @@ export interface SomehowDuplicatedTypeName { exports[`Generator when re-registering an already-registered type outputs a warning 2`] = ` [ [ - "Re-registering known type 'somehow_duplicated_type_name': SomehowDuplicatedTypeName => SomehowDuplicatedTypeName", + "Duplicate table name detected: somehow_duplicated_type_name. This is not supported, skipping.", ], ] `; -exports[`Generator with default options generates everything 1`] = ` -"export type PrimitiveValueType = string | number | boolean | null; +exports[`Generator with default options generates metadata and select/insert schemas 1`] = ` +"import { z } from zod; +export type PrimitiveValueType = string | number | boolean | null; export type SimpleValueType = PrimitiveValueType | Date; export type SerializableValueType = SimpleValueType | { [key in string]: SerializableValueType | undefined; @@ -42,82 +45,81 @@ export type SerializableValueType = SimpleValueType | { export type MapToSerializable = T extends SerializableValueType ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends { [K in keyof T]: K extends string ? T[K] extends SimpleValueType ? T[K] : T[K] extends Function ? never : MapToSerializable : never; } ? T : never; -export enum TestEnum { - A = "A", - B = "b", - CamelCaseRules = "camel_case_rules" -} -export interface TableWithNoColumns { -} -export interface TableWithNoColumns$Insert { -} -export const TableWithNoColumns$Types = {} as const; +export const DEFAULT = Symbol("DEFAULT"); +export const TableWithNoColumns$Metadata = {} as const; export const TableWithNoColumns$Schema = z.object({}); -export interface TableWithNumericId { - id: number; -} -export interface TableWithNumericId$Insert { - id?: number; -} -export const TableWithNumericId$Types = { - id: "int8" +export const TableWithNoColumns$Schema = z.object({}); +export const TableWithNumericId$Metadata = { + id: { + nativeName: "id", + nativeType: "int8" + } } as const; export const TableWithNumericId$Schema = z.object({ id: z.number() }); -export interface TableWithCustomTypes { - enum_type: TestEnum; - enum_array_type: TestEnum[]; - table_type: TableWithUuidId; - table_array_type: TableWithUuidId[]; -} -export interface TableWithCustomTypes$Insert { - enum_type: TestEnum; - enum_array_type: TestEnum[]; - table_type: TableWithUuidId; - table_array_type: TableWithUuidId[]; -} -export const TableWithCustomTypes$Types = { - enum_type: "test_enum", - enum_array_type: "test_enum", - table_type: "table_with_uuid_id", - table_array_type: "table_with_uuid_id" +export const TableWithNumericId$Schema = z.object({ + id: z.number().optional().or(DEFAULT) +}); +export const TableWithCustomTypes$Metadata = { + enum_type: { + nativeName: "enum_type", + nativeType: "test_enum" + }, + enum_array_type: { + nativeName: "enum_array_type", + nativeType: "test_enum" + }, + table_type: { + nativeName: "table_type", + nativeType: "table_with_uuid_id" + }, + table_array_type: { + nativeName: "table_array_type", + nativeType: "table_with_uuid_id" + } } as const; export const TableWithCustomTypes$Schema = z.object({ - enum_type: z.nativeEnum(TestEnum), - enum_array_type: z.array(z.nativeEnum(TestEnum)), - table_type: TableWithUuidId$Schema, - table_array_type: z.array(TableWithUuidId$Schema) + enum_type: z.unknown(), + enum_array_type: z.array(z.unknown()), + table_type: z.unknown(), + table_array_type: z.array(z.unknown()) }); -export interface TableWithUuidId { - id: string; -} -export interface TableWithUuidId$Insert { - id?: string; -} -export const TableWithUuidId$Types = { - id: "uuid" +export const TableWithCustomTypes$Schema = z.object({ + enum_type: z.unknown(), + enum_array_type: z.array(z.unknown()), + table_type: z.unknown(), + table_array_type: z.array(z.unknown()) +}); +export const TableWithUuidId$Metadata = { + id: { + nativeName: "id", + nativeType: "uuid" + } } as const; export const TableWithUuidId$Schema = z.object({ id: z.string() }); -export interface TableWithNullableFields { - nullable: string | null; - nullable_with_default: string | null; - nullable_array: string[] | null; - nullable_array_with_default: string[] | null; -} -export interface TableWithNullableFields$Insert { - nullable?: string | null; - nullable_with_default?: string | null; - nullable_array?: string[] | null; - nullable_array_with_default?: string[] | null; -} -export const TableWithNullableFields$Types = { - nullable: "text", - nullable_with_default: "text", - nullable_array: "text", - nullable_array_with_default: "text" +export const TableWithUuidId$Schema = z.object({ + id: z.string().optional().or(DEFAULT) +}); +export const TableWithNullableFields$Metadata = { + nullable: { + nativeName: "nullable", + nativeType: "text" + }, + nullable_with_default: { + nativeName: "nullable_with_default", + nativeType: "text" + }, + nullable_array: { + nativeName: "nullable_array", + nativeType: "text" + }, + nullable_array_with_default: { + nativeName: "nullable_array_with_default", + nativeType: "text" + } } as const; export const TableWithNullableFields$Schema = z.object({ nullable: z.string().nullable(), @@ -125,27 +127,36 @@ export const TableWithNullableFields$Schema = z.object({ nullable_array: z.array(z.string()).nullable(), nullable_array_with_default: z.array(z.string()).nullable() }); -export interface TableWithJsonJsonb { - json: MapToSerializable; - jsonb: MapToSerializable | null; -} -export interface TableWithJsonJsonb$Insert { - json: MapToSerializable; - jsonb?: MapToSerializable | null; -} -export const TableWithJsonJsonb$Types = { - json: "json", - jsonb: "jsonb" +export const TableWithNullableFields$Schema = z.object({ + nullable: z.string().nullable(), + nullable_with_default: z.string().nullable().optional().or(DEFAULT), + nullable_array: z.array(z.string()).nullable(), + nullable_array_with_default: z.array(z.string()).nullable().optional().or(DEFAULT) +}); +export const TableWithJsonJsonb$Metadata = { + json: { + nativeName: "json", + nativeType: "json" + }, + jsonb: { + nativeName: "jsonb", + nativeType: "jsonb" + } } as const; export const TableWithJsonJsonb$Schema = z.object({ json: z.unknown(), jsonb: z.unknown().nullable() }); +export const TableWithJsonJsonb$Schema = z.object({ + json: z.unknown(), + jsonb: z.unknown().nullable().optional().or(DEFAULT) +}); " `; exports[`Generator with different case conversions properly cases members 1`] = ` -"export type PrimitiveValueType = string | number | boolean | null; +"import { z } from zod; +export type PrimitiveValueType = string | number | boolean | null; export type SimpleValueType = PrimitiveValueType | Date; export type SerializableValueType = SimpleValueType | { [key in string]: SerializableValueType | undefined; @@ -153,82 +164,81 @@ export type SerializableValueType = SimpleValueType | { export type MapToSerializable = T extends SerializableValueType ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends { [K in keyof T]: K extends string ? T[K] extends SimpleValueType ? T[K] : T[K] extends Function ? never : MapToSerializable : never; } ? T : never; -export enum TestEnum { - A = "A", - b = "b", - camel_case_rules = "camel_case_rules" -} -export interface TableWithNoColumns { -} -export interface TableWithNoColumns$Insert { -} -export const TableWithNoColumns$Types = {} as const; +export const DEFAULT = Symbol("DEFAULT"); +export const TableWithNoColumns$Metadata = {} as const; export const TableWithNoColumns$Schema = z.object({}); -export interface TableWithNumericId { - id: number; -} -export interface TableWithNumericId$Insert { - id?: number; -} -export const TableWithNumericId$Types = { - id: "int8" +export const TableWithNoColumns$Schema = z.object({}); +export const TableWithNumericId$Metadata = { + id: { + nativeName: "id", + nativeType: "int8" + } } as const; export const TableWithNumericId$Schema = z.object({ id: z.number() }); -export interface TableWithCustomTypes { - enumType: TestEnum; - enumArrayType: TestEnum[]; - tableType: TableWithUuidId; - tableArrayType: TableWithUuidId[]; -} -export interface TableWithCustomTypes$Insert { - enumType: TestEnum; - enumArrayType: TestEnum[]; - tableType: TableWithUuidId; - tableArrayType: TableWithUuidId[]; -} -export const TableWithCustomTypes$Types = { - enumType: "test_enum", - enumArrayType: "test_enum", - tableType: "table_with_uuid_id", - tableArrayType: "table_with_uuid_id" +export const TableWithNumericId$Schema = z.object({ + id: z.number().optional().or(DEFAULT) +}); +export const TableWithCustomTypes$Metadata = { + enumType: { + nativeName: "enum_type", + nativeType: "test_enum" + }, + enumArrayType: { + nativeName: "enum_array_type", + nativeType: "test_enum" + }, + tableType: { + nativeName: "table_type", + nativeType: "table_with_uuid_id" + }, + tableArrayType: { + nativeName: "table_array_type", + nativeType: "table_with_uuid_id" + } } as const; export const TableWithCustomTypes$Schema = z.object({ - enumType: z.nativeEnum(TestEnum), - enumArrayType: z.array(z.nativeEnum(TestEnum)), - tableType: TableWithUuidId$Schema, - tableArrayType: z.array(TableWithUuidId$Schema) + enumType: z.unknown(), + enumArrayType: z.array(z.unknown()), + tableType: z.unknown(), + tableArrayType: z.array(z.unknown()) }); -export interface TableWithUuidId { - id: string; -} -export interface TableWithUuidId$Insert { - id?: string; -} -export const TableWithUuidId$Types = { - id: "uuid" +export const TableWithCustomTypes$Schema = z.object({ + enumType: z.unknown(), + enumArrayType: z.array(z.unknown()), + tableType: z.unknown(), + tableArrayType: z.array(z.unknown()) +}); +export const TableWithUuidId$Metadata = { + id: { + nativeName: "id", + nativeType: "uuid" + } } as const; export const TableWithUuidId$Schema = z.object({ id: z.string() }); -export interface TableWithNullableFields { - nullable: string | null; - nullableWithDefault: string | null; - nullableArray: string[] | null; - nullableArrayWithDefault: string[] | null; -} -export interface TableWithNullableFields$Insert { - nullable?: string | null; - nullableWithDefault?: string | null; - nullableArray?: string[] | null; - nullableArrayWithDefault?: string[] | null; -} -export const TableWithNullableFields$Types = { - nullable: "text", - nullableWithDefault: "text", - nullableArray: "text", - nullableArrayWithDefault: "text" +export const TableWithUuidId$Schema = z.object({ + id: z.string().optional().or(DEFAULT) +}); +export const TableWithNullableFields$Metadata = { + nullable: { + nativeName: "nullable", + nativeType: "text" + }, + nullableWithDefault: { + nativeName: "nullable_with_default", + nativeType: "text" + }, + nullableArray: { + nativeName: "nullable_array", + nativeType: "text" + }, + nullableArrayWithDefault: { + nativeName: "nullable_array_with_default", + nativeType: "text" + } } as const; export const TableWithNullableFields$Schema = z.object({ nullable: z.string().nullable(), @@ -236,22 +246,30 @@ export const TableWithNullableFields$Schema = z.object({ nullableArray: z.array(z.string()).nullable(), nullableArrayWithDefault: z.array(z.string()).nullable() }); -export interface TableWithJsonJsonb { - json: MapToSerializable; - jsonb: MapToSerializable | null; -} -export interface TableWithJsonJsonb$Insert { - json: MapToSerializable; - jsonb?: MapToSerializable | null; -} -export const TableWithJsonJsonb$Types = { - json: "json", - jsonb: "jsonb" +export const TableWithNullableFields$Schema = z.object({ + nullable: z.string().nullable(), + nullableWithDefault: z.string().nullable().optional().or(DEFAULT), + nullableArray: z.array(z.string()).nullable(), + nullableArrayWithDefault: z.array(z.string()).nullable().optional().or(DEFAULT) +}); +export const TableWithJsonJsonb$Metadata = { + json: { + nativeName: "json", + nativeType: "json" + }, + jsonb: { + nativeName: "jsonb", + nativeType: "jsonb" + } } as const; export const TableWithJsonJsonb$Schema = z.object({ json: z.unknown(), jsonb: z.unknown().nullable() }); +export const TableWithJsonJsonb$Schema = z.object({ + json: z.unknown(), + jsonb: z.unknown().nullable().optional().or(DEFAULT) +}); " `; @@ -264,6 +282,7 @@ export type SerializableValueType = SimpleValueType | { export type MapToSerializable = T extends SerializableValueType ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends { [K in keyof T]: K extends string ? T[K] extends SimpleValueType ? T[K] : T[K] extends Function ? never : MapToSerializable : never; } ? T : never; +export const DEFAULT = Symbol("DEFAULT"); export enum TestEnum { A = "A", B = "b", @@ -281,6 +300,7 @@ export type SerializableValueType = SimpleValueType | { export type MapToSerializable = T extends SerializableValueType ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends { [K in keyof T]: K extends string ? T[K] extends SimpleValueType ? T[K] : T[K] extends Function ? never : MapToSerializable : never; } ? T : never; +export const DEFAULT = Symbol("DEFAULT"); " `; @@ -293,6 +313,7 @@ export type SerializableValueType = SimpleValueType | { export type MapToSerializable = T extends SerializableValueType ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends { [K in keyof T]: K extends string ? T[K] extends SimpleValueType ? T[K] : T[K] extends Function ? never : MapToSerializable : never; } ? T : never; +export const DEFAULT = Symbol("DEFAULT"); export interface TableWithNoColumns$Insert { } export interface TableWithNumericId$Insert { @@ -338,7 +359,8 @@ exports[`Generator with insert types enabled generates insert types 2`] = ` `; exports[`Generator with schema objects enabled generates schema objects 1`] = ` -"export type PrimitiveValueType = string | number | boolean | null; +"import { z } from zod; +export type PrimitiveValueType = string | number | boolean | null; export type SimpleValueType = PrimitiveValueType | Date; export type SerializableValueType = SimpleValueType | { [key in string]: SerializableValueType | undefined; @@ -346,6 +368,7 @@ export type SerializableValueType = SimpleValueType | { export type MapToSerializable = T extends SerializableValueType ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends { [K in keyof T]: K extends string ? T[K] extends SimpleValueType ? T[K] : T[K] extends Function ? never : MapToSerializable : never; } ? T : never; +export const DEFAULT = Symbol("DEFAULT"); export const TableWithNoColumns$Schema = z.object({}); export const TableWithNumericId$Schema = z.object({ id: z.number() @@ -398,6 +421,7 @@ export type SerializableValueType = SimpleValueType | { export type MapToSerializable = T extends SerializableValueType ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends { [K in keyof T]: K extends string ? T[K] extends SimpleValueType ? T[K] : T[K] extends Function ? never : MapToSerializable : never; } ? T : never; +export const DEFAULT = Symbol("DEFAULT"); export interface TableWithNoColumns { } export interface TableWithNumericId { @@ -445,6 +469,7 @@ export type SerializableValueType = SimpleValueType | { export type MapToSerializable = T extends SerializableValueType ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> : T extends { [K in keyof T]: K extends string ? T[K] extends SimpleValueType ? T[K] : T[K] extends Function ? never : MapToSerializable : never; } ? T : never; +export const DEFAULT = Symbol("DEFAULT"); export const TableWithNoColumns$Types = {} as const; export const TableWithNumericId$Types = { id: "int8" diff --git a/src/generator/builders/ColumnMetadataBuilder.ts b/src/generator/builders/ColumnMetadataBuilder.ts new file mode 100644 index 00000000..5cf37a9c --- /dev/null +++ b/src/generator/builders/ColumnMetadataBuilder.ts @@ -0,0 +1,46 @@ +import { + factory, + ObjectLiteralExpression, + PropertyAssignment, +} from 'typescript' +import { z } from 'zod' + +import { ColumnInfo, TypeRegistry } from '../database' +import { Transformations } from '../types' + +import NodeBuilder from './NodeBuilder' + +// eslint-disable-next-line max-len +export class ColumnMetadataBuilder extends NodeBuilder { + public readonly type: string + + constructor( + protected options: z.infer, + types: TypeRegistry, + transform: Transformations + ) { + super(options.name, types, transform) + this.type = options.type + } + + protected buildSingleProperty( + name: string, + value: string + ): PropertyAssignment { + return factory.createPropertyAssignment( + factory.createIdentifier(name), + factory.createStringLiteral(value) + ) + } + + protected buildProperties(): PropertyAssignment[] { + return [ + this.buildSingleProperty('nativeName', this.options.name), + this.buildSingleProperty('nativeType', this.options.type), + ] + } + + public buildNode(): ObjectLiteralExpression { + return factory.createObjectLiteralExpression(this.buildProperties(), true) + } +} diff --git a/src/generator/builders/InsertSchemaBuilder.ts b/src/generator/builders/InsertSchemaBuilder.ts new file mode 100644 index 00000000..6bb9e8b9 --- /dev/null +++ b/src/generator/builders/InsertSchemaBuilder.ts @@ -0,0 +1,126 @@ +import { + factory, + VariableStatement, + PropertyAssignment, + NodeFlags, + Identifier, + CallExpression, + Expression, +} from 'typescript' +import { z } from 'zod' + +import { ColumnInfo, TableInfoWithColumns, TypeRegistry } from '../database' +import { Transformations } from '../types' + +import { ExportKeyword } from './NodeBuilder' +import TypeBuilder from './TypeBuilder' + +// eslint-disable-next-line max-len +export default class InsertSchemaBuilder extends TypeBuilder { + public readonly canInsert: boolean + public readonly columns: readonly z.infer[] + + constructor( + options: z.infer, + protected types: TypeRegistry, + transform: Transformations + ) { + super(options.name, types, transform) + this.canInsert = options.canInsert + this.columns = options.columns + } + + protected zod(): Identifier { + return factory.createIdentifier('z') + } + + protected buildProperties(): PropertyAssignment[] { + return this.columns.map((columnInfo) => { + const name = factory.createIdentifier( + this.transform.columns(columnInfo.name) + ) + + let value: Expression + const nativeType = this.types.getText(columnInfo.type) + const type = this.types.getType(columnInfo.type) + + if (type === 'table') { + value = this.typename(nativeType) + } else if (type === 'enum') { + value = this.buildZodFunctionCall( + 'nativeEnum', + factory.createIdentifier(nativeType) + ) + } else { + value = this.buildZodFunctionCall(nativeType) + } + + if (columnInfo.isArray) { + value = this.buildZodFunctionCall('array', value) + } + + if (columnInfo.nullable) { + value = factory.createCallExpression( + factory.createPropertyAccessExpression(value, 'nullable'), + undefined, + undefined + ) + } + + if (columnInfo.hasDefault) { + value = factory.createCallExpression( + factory.createPropertyAccessExpression(value, 'optional'), + undefined, + undefined + ) + value = factory.createCallExpression( + factory.createPropertyAccessExpression(value, 'or'), + undefined, + [factory.createIdentifier('DEFAULT')] + ) + } + + return factory.createPropertyAssignment(name, value) + }) + } + + protected buildZodFunctionCall( + functionName: string, + ...args: readonly Expression[] + ): CallExpression { + return factory.createCallExpression( + factory.createPropertyAccessExpression(this.zod(), functionName), + undefined, + args + ) + } + + protected buildSchemaDefinition(): CallExpression { + const properties = this.buildProperties() + + return this.buildZodFunctionCall( + 'object', + factory.createObjectLiteralExpression(properties, true) + ) + } + + public typename(name: string = this.name): Identifier { + return this.createIdentifier(super.typename(name).text + '$Schema') + } + + public buildNode(): VariableStatement { + const declaration = factory.createVariableDeclaration( + this.typename(), + undefined, + undefined, + this.buildSchemaDefinition() + ) + + const declarationList = factory.createVariableDeclarationList( + [declaration], + NodeFlags.Const + ) + + return factory.createVariableStatement([ExportKeyword], declarationList) + } +} diff --git a/src/generator/builders/NodeBuilder.ts b/src/generator/builders/NodeBuilder.ts index e041a451..6adc577e 100644 --- a/src/generator/builders/NodeBuilder.ts +++ b/src/generator/builders/NodeBuilder.ts @@ -14,3 +14,19 @@ export default abstract class NodeBuilder { public abstract buildNode(): T } + +export interface SingleBuildable { + buildNode(): T +} + +export interface MultiBuildable { + buildNodes(): T[] +} + +export type Buildable = SingleBuildable | MultiBuildable + +export function isMultiBuildable( + builder: Buildable +): builder is MultiBuildable { + return Reflect.has(builder, 'buildNodes') +} diff --git a/src/generator/builders/SelectSchemaBuilder.ts b/src/generator/builders/SelectSchemaBuilder.ts new file mode 100644 index 00000000..da736e90 --- /dev/null +++ b/src/generator/builders/SelectSchemaBuilder.ts @@ -0,0 +1,113 @@ +import { + factory, + VariableStatement, + PropertyAssignment, + NodeFlags, + Identifier, + CallExpression, + Expression, +} from 'typescript' +import { z } from 'zod' + +import { ColumnInfo, TableInfoWithColumns, TypeRegistry } from '../database' +import { Transformations } from '../types' + +import { ExportKeyword } from './NodeBuilder' +import TypeBuilder from './TypeBuilder' + +// eslint-disable-next-line max-len +export default class SelectSchemaBuilder extends TypeBuilder { + public readonly canInsert: boolean + public readonly columns: readonly z.infer[] + + constructor( + options: z.infer, + protected types: TypeRegistry, + transform: Transformations + ) { + super(options.name, types, transform) + this.canInsert = options.canInsert + this.columns = options.columns + } + + protected zod(): Identifier { + return factory.createIdentifier('z') + } + + protected buildProperties(): PropertyAssignment[] { + return this.columns.map((columnInfo) => { + const name = factory.createIdentifier( + this.transform.columns(columnInfo.name) + ) + + let value: Expression + const nativeType = this.types.getText(columnInfo.type) + const type = this.types.getType(columnInfo.type) + + if (type === 'table') { + value = this.typename(nativeType) + } else if (type === 'enum') { + value = this.buildZodFunctionCall( + 'nativeEnum', + factory.createIdentifier(nativeType) + ) + } else { + value = this.buildZodFunctionCall(nativeType) + } + + if (columnInfo.isArray) { + value = this.buildZodFunctionCall('array', value) + } + + if (columnInfo.nullable) { + value = factory.createCallExpression( + factory.createPropertyAccessExpression(value, 'nullable'), + undefined, + undefined + ) + } + + return factory.createPropertyAssignment(name, value) + }) + } + + protected buildZodFunctionCall( + functionName: string, + ...args: readonly Expression[] + ): CallExpression { + return factory.createCallExpression( + factory.createPropertyAccessExpression(this.zod(), functionName), + undefined, + args + ) + } + + protected buildSchemaDefinition(): CallExpression { + const properties = this.buildProperties() + + return this.buildZodFunctionCall( + 'object', + factory.createObjectLiteralExpression(properties, true) + ) + } + + public typename(name: string = this.name): Identifier { + return this.createIdentifier(super.typename(name).text + '$Schema') + } + + public buildNode(): VariableStatement { + const declaration = factory.createVariableDeclaration( + this.typename(), + undefined, + undefined, + this.buildSchemaDefinition() + ) + + const declarationList = factory.createVariableDeclarationList( + [declaration], + NodeFlags.Const + ) + + return factory.createVariableStatement([ExportKeyword], declarationList) + } +} diff --git a/src/generator/builders/SingleNamedImportBuilder.ts b/src/generator/builders/SingleNamedImportBuilder.ts new file mode 100644 index 00000000..cb66cd5e --- /dev/null +++ b/src/generator/builders/SingleNamedImportBuilder.ts @@ -0,0 +1,38 @@ +import { factory, ImportDeclaration } from 'typescript' +import { identity } from '../../datasource/loaders/utils' + +import { TypeRegistry } from '../database' +import { Transformations } from '../types' + +import TypeBuilder from './TypeBuilder' + +export type SingleNamedImportOptions = { + name: string + source: string +} + +// eslint-disable-next-line max-len +export default class SingleNamedImportBuilder extends TypeBuilder { + constructor( + protected readonly options: SingleNamedImportOptions, + types: TypeRegistry, + transform: Transformations + ) { + super(options.name, types, { + ...transform, + typeNames: identity, + }) + } + + public buildNode(): ImportDeclaration { + const imports = factory.createNamedImports([ + factory.createImportSpecifier(false, undefined, this.typename()), + ]) + const importClause = factory.createImportClause(false, undefined, imports) + return factory.createImportDeclaration( + undefined, + importClause, + factory.createIdentifier(this.options.source) + ) + } +} diff --git a/src/generator/builders/TableMetadataBuilder.ts b/src/generator/builders/TableMetadataBuilder.ts new file mode 100644 index 00000000..d551c915 --- /dev/null +++ b/src/generator/builders/TableMetadataBuilder.ts @@ -0,0 +1,89 @@ +import { + factory, + VariableStatement, + PropertyAssignment, + SyntaxKind, + NodeFlags, + Identifier, + KeywordTypeSyntaxKind, + AsExpression, +} from 'typescript' +import { z } from 'zod' + +import { ColumnInfo, TableInfoWithColumns, TypeRegistry } from '../database' +import { Transformations } from '../types' + +import { ColumnMetadataBuilder } from './ColumnMetadataBuilder' +import { ExportKeyword } from './NodeBuilder' +import TypeBuilder from './TypeBuilder' + +export default class TypeObjectBuilder extends TypeBuilder { + public readonly canInsert: boolean + public readonly columns: readonly z.infer[] + + constructor( + options: z.infer, + types: TypeRegistry, + transform: Transformations + ) { + super(options.name, types, transform) + this.canInsert = options.canInsert + this.columns = options.columns + } + + protected buildSingleProperty( + name: string, + value: string + ): PropertyAssignment { + return factory.createPropertyAssignment( + factory.createIdentifier(name), + factory.createStringLiteral(value) + ) + } + + protected buildProperties(): PropertyAssignment[] { + return this.columns.map((columnInfo) => { + const builder = new ColumnMetadataBuilder( + columnInfo, + this.types, + this.transform + ) + const metadataNode = builder.buildNode() + return factory.createPropertyAssignment( + this.transform.columns(columnInfo.name), + metadataNode + ) + }) + } + + protected buildObjectLiteral(): AsExpression { + const properties = this.buildProperties() + + return factory.createAsExpression( + factory.createObjectLiteralExpression(properties, true), + factory.createKeywordTypeNode( + SyntaxKind.ConstKeyword as KeywordTypeSyntaxKind + ) + ) + } + + public typename(name: string = this.name): Identifier { + return this.createIdentifier(super.typename(name).text + '$Metadata') + } + + public buildNode(): VariableStatement { + const declaration = factory.createVariableDeclaration( + this.typename(), + undefined, + undefined, + this.buildObjectLiteral() + ) + + const declarationList = factory.createVariableDeclarationList( + [declaration], + NodeFlags.Const + ) + + return factory.createVariableStatement([ExportKeyword], declarationList) + } +} diff --git a/src/generator/builders/UtilityTypesBuilder.ts b/src/generator/builders/UtilityTypesBuilder.ts index 4463d3f2..69ca30eb 100644 --- a/src/generator/builders/UtilityTypesBuilder.ts +++ b/src/generator/builders/UtilityTypesBuilder.ts @@ -1,9 +1,12 @@ import { factory, Identifier, + NodeFlags, + Statement, SyntaxKind, TypeAliasDeclaration, TypeNode, + VariableStatement, } from 'typescript' import { ExportKeyword } from './NodeBuilder' @@ -187,11 +190,38 @@ export default class UtilityTypesBuilder { ) } - public buildNodes(): TypeAliasDeclaration[] { + private buildDefault(): VariableStatement { + const declaration = factory.createVariableDeclaration( + 'DEFAULT', + undefined, + undefined, + factory.createCallExpression( + factory.createIdentifier('Symbol'), + undefined, + [factory.createStringLiteral('DEFAULT')] + ) + ) + + const declarationList = factory.createVariableDeclarationList( + [declaration], + NodeFlags.Const + ) + + return factory.createVariableStatement([ExportKeyword], declarationList) + } + + public buildNodes(): Statement[] { const primitiveType = this.buildPrimitiveValueType() const simpleType = this.buildSimpleValueType(primitiveType.name) const serialzableType = this.buildSerializableValueType(simpleType.name) const mapToSerialize = this.buildMapToSerializable() - return [primitiveType, simpleType, serialzableType, mapToSerialize] + const defaultSymbol = this.buildDefault() + return [ + primitiveType, + simpleType, + serialzableType, + mapToSerialize, + defaultSymbol, + ] } } diff --git a/src/generator/builders/index.ts b/src/generator/builders/index.ts index 92723317..e909741f 100644 --- a/src/generator/builders/index.ts +++ b/src/generator/builders/index.ts @@ -1,3 +1,6 @@ export { default as EnumBuilder } from './EnumBuilder' export { default as InsertTypeBuilder } from './InsertTypeBuilder' export { default as TableBuilder } from './TableBuilder' +export { default as TableMetadataBuilder } from './TableMetadataBuilder' +export { default as SelectSchemaBuilder } from './SelectSchemaBuilder' +export { default as InsertSchemaBuilder } from './InsertSchemaBuilder' diff --git a/tsconfig.json b/tsconfig.json index b49c7c1c..1feecc1d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,7 @@ "allowJs": false, "checkJs": false, "strict": true, + "noErrorTruncation": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true,