From cbb7a18bfd233e5dfb39828b3f7f8e04e94ca66d Mon Sep 17 00:00:00 2001 From: Sebastian Sebald Date: Tue, 19 Feb 2019 14:14:29 +0100 Subject: [PATCH] Add slonik (v15.4) typings --- types/slonik/index.d.ts | 396 +++++++++++++++++++++++++++++++++++ types/slonik/slonik-tests.ts | 287 +++++++++++++++++++++++++ types/slonik/symbols.d.ts | 7 + types/slonik/tsconfig.json | 23 ++ types/slonik/tslint.json | 1 + 5 files changed, 714 insertions(+) create mode 100644 types/slonik/index.d.ts create mode 100644 types/slonik/slonik-tests.ts create mode 100644 types/slonik/symbols.d.ts create mode 100644 types/slonik/tsconfig.json create mode 100644 types/slonik/tslint.json diff --git a/types/slonik/index.d.ts b/types/slonik/index.d.ts new file mode 100644 index 0000000000..228d6a12d7 --- /dev/null +++ b/types/slonik/index.d.ts @@ -0,0 +1,396 @@ +// Type definitions for slonik 15.4 +// Project: https://github.com/gajus/slonik#readme +// Definitions by: Sebastian Sebald +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 3.0 +import * as SlonikSymbol from './symbols'; + +// +// HELPER +// ---------------------------------------------------------------------- +// This is `roarr` (https://github.com/gajus/roarr) in flow +export type LoggerType = (...args: string[]) => never; +export type MaybePromiseType = T | Promise; + +// +// EXPRESSIONS AND TOKENS +// ---------------------------------------------------------------------- +export type PrimitiveValueExpressionType = string | number | boolean | null; + +export type ValueExpressionType = + | PrimitiveValueExpressionType + | IdentifierTokenType + | RawSqlTokenType + | SqlSqlTokenType + | TupleListSqlTokenType + | TupleSqlTokenType + | UnnestSqlTokenType + | ValueListSqlTokenType; + +export interface IdentifierTokenType { + names: ReadonlyArray; + type: typeof SlonikSymbol.IdentifierTokenSymbol; +} + +export interface SqlSqlTokenType { + sql: string; + type: typeof SlonikSymbol.SqlTokenSymbol; + values: PrimitiveValueExpressionType[]; +} + +export interface RawSqlTokenType { + sql: string; + type: typeof SlonikSymbol.RawSqlTokenSymbol; + values: PrimitiveValueExpressionType[]; +} + +export interface ValueListSqlTokenType { + values: PrimitiveValueExpressionType[]; + type: typeof SlonikSymbol.ValueListTokenSymbol; +} + +export interface TupleSqlTokenType { + values: PrimitiveValueExpressionType[]; + type: typeof SlonikSymbol.TupleTokenSymbol; +} + +export interface TupleListSqlTokenType { + tuples: PrimitiveValueExpressionType[]; + type: typeof SlonikSymbol.TupleListTokenSymbol; +} + +export interface UnnestSqlTokenType { + columnTypes: string[]; + tuples: PrimitiveValueExpressionType[][]; + type: typeof SlonikSymbol.UnnestTokenSymbol; +} + +// +// DATABASE +// ---------------------------------------------------------------------- +export interface FieldType { + columnID: number; + dataTypeID: number; + dataTypeModifier: number; + dataTypeSize: number; + format: string; + name: string; + tableID: number; +} + +export type DatabaseTransactionConnectionType = CommonQueryMethodsType & { + transaction: (handler: TransactionFunctionType) => Promise; +}; + +export type DatabasePoolConnectionType = CommonQueryMethodsType & { + transaction: (handler: TransactionFunctionType) => Promise; +}; + +export type ConnectionRoutineType = (connection: DatabasePoolConnectionType) => Promise; + +export type DatabasePoolType = CommonQueryMethodsType & { + connect: (connectionRoutine: ConnectionRoutineType) => Promise; + transaction: (handler: TransactionFunctionType) => Promise; +}; + +export type DatabaseConfigurationType = + | string + | { + database?: string; + host?: string; + idleTimeoutMillis?: number; + max?: number; + password?: string; + port?: number; + user?: string; + }; + +export type ConnectionTypeType = + | 'EXPLICIT' + | 'IMPLICIT_QUERY' + | 'IMPLICIT_TRANSACTION'; + +export interface ConnectionContextType { + /** + * Unique connection ID + */ + connectionId: string; + connectionType: ConnectionTypeType; + /** + * Instance of Roarr logger with bound connection context parameters + */ + log: LoggerType; + /** + * Unique connection pool ID + */ + poolId: string; +} + +export type DatabaseConnectionType = DatabasePoolConnectionType & DatabasePoolType; + +// +// QUERY +// ---------------------------------------------------------------------- +export type QueryIdType = string; + +export interface QueryType { + sql: string; + values?: ReadonlyArray; +} + +export type QueryMethodType = ( + sql: TaggedTemplateLiteralInvocationType, + values?: PrimitiveValueExpressionType[] +) => Promise; + +export interface NoticeType { + code: string; + length: number; + message: string; + name: string; + severity: string; + where: string; +} + +export type QueryResultType = Readonly<{ + command: 'DELETE' | 'INSERT' | 'SELECT' | 'UPDATE'; + fields: ReadonlyArray; + notices: ReadonlyArray, + oid: number | null; + rowAsArray: boolean; + rowCount: number; + rows: ReadonlyArray; +}>; + +export type QueryResultRowColumnType = string | number; +export type QueryResultRowType = { + [name in ColumnName]: QueryResultRowColumnType; +}; + +// TODO: Infer column names via generic +export type QueryAnyFirstFunctionType = QueryMethodType; +export type QueryAnyFunctionType = QueryMethodType; +export type QueryFunctionType = QueryMethodType; +export type QueryManyFirstFunctionType = QueryMethodType; +export type QueryManyFunctionType = QueryMethodType; +export type QueryMaybeOneFirstFunctionType = QueryMethodType; +export type QueryMaybeOneFunctionType = QueryMethodType; +export type QueryOneFirstFunctionType = QueryMethodType; +export type QueryOneFunctionType = QueryMethodType; + +export interface CommonQueryMethodsType { + any: QueryAnyFunctionType; + anyFirst: QueryAnyFirstFunctionType; + many: QueryManyFunctionType; + manyFirst: QueryManyFirstFunctionType; + maybeOne: QueryMaybeOneFunctionType; + maybeOneFirst: QueryMaybeOneFirstFunctionType; + one: QueryOneFunctionType; + oneFirst: QueryOneFirstFunctionType; + query: QueryFunctionType; +} + +export interface CallSiteType { + columnNumber: number; + fileName: string | null; + lineNumber: number; +} + +export interface QueryContextType { + /** + * Unique connection ID + */ + connectionId: string; + /** + * Instance of Roarr logger with bound query context parameters + */ + log: LoggerType; + /** + * A copy of the query before `transformQuery` middleware + */ + originalQuery: QueryType; + /** + * Unique connection pool ID + */ + poolId: string; + /** + * Unique query ID + */ + queryId: QueryIdType; + stackTrace: CallSiteType[] | null; + /** + * `process.hrtime.bigint()` for when query was received. + */ + queryInputTime: number; + /** + * Unique transaction ID + */ + transactionId?: string; +} + +// +// SQL (TAGGED TEMPLATE) +// ---------------------------------------------------------------------- +export interface TaggedTemplateLiteralInvocationType { + sql: string; + type: typeof SlonikSymbol.SqlTokenSymbol; + values: ValueExpressionType[]; +} + +export const sql: SqlTaggedTemplateType; + +export interface SqlTaggedTemplateType { + (template: TemplateStringsArray, ...vals: ValueExpressionType[]): SqlSqlTokenType; + identifier: (names: string[]) => IdentifierTokenType; + raw: (rawSql: string, values?: PrimitiveValueExpressionType[]) => RawSqlTokenType; + tuple: (values: PrimitiveValueExpressionType[]) => TupleSqlTokenType; + tupleList: (tuples: PrimitiveValueExpressionType[][]) => TupleListSqlTokenType; + unnest: (tuples: PrimitiveValueExpressionType[][], columnTypes: string[]) => UnnestSqlTokenType; + valueList: (values: PrimitiveValueExpressionType[]) => ValueListSqlTokenType; +} + +export interface SqlFragmentType { + parameters: PrimitiveValueExpressionType[]; + sql: string; +} + +// +// POOL +// ---------------------------------------------------------------------- +export interface PoolContextType { + /** + * Instance of Roarr logger with bound connection context parameters + */ + log: LoggerType; + /** + * Unique connection pool ID + */ + poolId: string; + /** + * The query that is initiating the connection + */ + query: TaggedTemplateLiteralInvocationType | null; +} + +export function createPool( + connectionConfiguration: DatabaseConfigurationType, + clientUserConfiguration?: ClientUserConfigurationType +): DatabasePoolType; + +// +// TRANSACTION +// ---------------------------------------------------------------------- +export type TransactionFunctionType = ( + connection: DatabaseTransactionConnectionType +) => Promise; + +// +// INTERCEPTOR +// ---------------------------------------------------------------------- +export interface InterceptorType { + afterPoolConnection?: ( + connectionContext: ConnectionContextType, + connection: DatabasePoolConnectionType + ) => MaybePromiseType; + afterQueryExecution?: ( + queryContext: QueryContextType, + query: QueryType, + result: QueryResultType + ) => MaybePromiseType>; + beforePoolConnection?: ( + connectionContext: PoolContextType + ) => MaybePromiseType; + beforePoolConnectionRelease?: ( + connectionContext: ConnectionContextType, + connection: DatabasePoolConnectionType + ) => MaybePromiseType; + beforeQueryExecution?: ( + queryContext: QueryContextType, + query: QueryType + ) => MaybePromiseType | undefined>; + transformQuery?: ( + queryContext: QueryContextType, + query: QueryType + ) => MaybePromiseType; +} + +/** + * Default interceptors: + * - [Field name transformation interceptor](https://github.com/gajus/slonik#field-name-transformation-interceptor) + * - [Query normalization interceptor](https://github.com/gajus/slonik#query-normalization-interceptor) + * + * See official [readme](https://github.com/gajus/slonik#default-interceptors) for more information. + */ +export function createInterceptorPreset(): InterceptorType[]; + +export function createFieldNameTransformationInterceptor(configuration: { + format: string, + test?: (field: FieldType) => boolean +}): InterceptorType; +export function createQueryNormalizationInterceptor(configuration?: { + stripComments?: boolean; +}): InterceptorType; +export function createBenchmarkingInterceptor(): InterceptorType; + +// +// TYPE PARSER +// ---------------------------------------------------------------------- +export interface TypeParserType { + /** + * Value of "pg_type"."typname" (e.g. "int8", "timestamp", "timestamptz") + */ + name: string; + parse: (value: string) => T; +} + +export function createBigintTypeParser(): TypeParserType; +export function createTimestampTypeParser(): TypeParserType; +export function createTimestampWithTimeZoneTypeParser(): TypeParserType; + +/** + * Default type parsers. + * + * These interceptors are enabled by default: + * - `int8`: Produces an integer. + * - `timestamp`: Produces a unix timestamp (in milliseconds). + * - `timestamptz`: Produces a unix timestamp (in milliseconds). + */ +export function createTypeParserPreset(): TypeParserType[]; + +// +// CLIENT +// ---------------------------------------------------------------------- +export interface ClientConfigurationType { + captureStackTrace?: boolean; + /** + * An array of [Slonik interceptors](https://github.com/gajus/slonik#slonik-interceptors) + */ + interceptors?: InterceptorType[]; + /** + * An array of [Slonik type parsers](https://github.com/gajus/slonik#slonik-type-parsers) + */ + typeParsers?: TypeParserType[]; +} + +export interface ClientUserConfigurationType { + /** + * An array of [Slonik interceptors](https://github.com/gajus/slonik#slonik-interceptors) + */ + interceptors?: InterceptorType[]; + /** + * An array of [Slonik type parsers](https://github.com/gajus/slonik#slonik-type-parsers) + */ + typeParsers?: TypeParserType[]; +} + +// +// ERRORS +// ---------------------------------------------------------------------- +export class SlonikError extends Error {} +export class NotFoundError extends SlonikError {} +export class DataIntegrityError extends SlonikError {} +export class IntegrityConstraintViolationError extends SlonikError {} +export class NotNullIntegrityConstraintViolationError extends IntegrityConstraintViolationError {} +export class ForeignKeyIntegrityConstraintViolationError extends IntegrityConstraintViolationError {} +export class UniqueIntegrityConstraintViolationError extends IntegrityConstraintViolationError {} +export class CheckIntegrityConstraintViolationError extends IntegrityConstraintViolationError {} diff --git a/types/slonik/slonik-tests.ts b/types/slonik/slonik-tests.ts new file mode 100644 index 0000000000..30caf65f33 --- /dev/null +++ b/types/slonik/slonik-tests.ts @@ -0,0 +1,287 @@ +import { + CheckIntegrityConstraintViolationError, + createBenchmarkingInterceptor, + createBigintTypeParser, + createFieldNameTransformationInterceptor, + createInterceptorPreset, + createPool, + createQueryNormalizationInterceptor, + createTimestampTypeParser, + createTimestampWithTimeZoneTypeParser, + DatabaseConnectionType, + DatabasePoolConnectionType, + DatabasePoolType, + DatabaseTransactionConnectionType, + createTypeParserPreset, + DataIntegrityError, + ForeignKeyIntegrityConstraintViolationError, + IntegrityConstraintViolationError, + InterceptorType, + NotFoundError, + NotNullIntegrityConstraintViolationError, + SlonikError, + sql, + TypeParserType, + UniqueIntegrityConstraintViolationError +} from 'slonik'; + +const VALUE = 'foo'; + +// +// POOL +// ---------------------------------------------------------------------- +const pool = createPool('postgres://localhost'); + +pool.connect(async (connection) => { + const result = await connection.query(sql`SELECT 1`); + // $ExpectType QueryResultRowType + result; + + connection.query(sql` + SELECT 1 + FROM foo + WHERE bar = ${'baz'} + `); + + // Query methods + await connection.any(sql`SELECT foo`); + await connection.anyFirst(sql`SELECT foo`); + await connection.many(sql`SELECT foo`); + await connection.manyFirst(sql`SELECT foo`); + await connection.maybeOne(sql`SELECT foo`); + await connection.maybeOneFirst(sql`SELECT foo`); + await connection.one(sql`SELECT foo`); + await connection.oneFirst(sql`SELECT foo`); + + await connection.transaction(async (transactionConnection) => { + await transactionConnection.query(sql`INSERT INTO foo (bar) VALUES ('baz')`); + await transactionConnection.query(sql`INSERT INTO qux (quux) VALUES ('corge')`); + }); + + await connection.transaction(async (t1) => { + await t1.query(sql`INSERT INTO foo (bar) VALUES ('baz')`); + + return t1.transaction((t2) => { + return t2.query(sql`INSERT INTO qux (quux) VALUES ('corge')`); + }); + }); + + await connection.transaction(async (t1) => { + await t1.query(sql`INSERT INTO foo (bar) VALUES ('baz')`); + + try { + await t1.transaction(async (t2) => { + await t2.query(sql`INSERT INTO qux (quux) VALUES ('corge')`); + + return Promise.reject(new Error('foo')); + }); + } catch (error) { /* empty */ } + }); + }); +pool.query(sql`SELECT * FROM table WHERE name = '${VALUE}'`); + +createPool('postgres://localhost', { + interceptors: [ + { + afterPoolConnection: async (ctx, connection) => { + await connection.query(sql`LOAD 'auto_explain'`); + await connection.query(sql`SET auto_explain.log_analyze=true`); + await connection.query(sql`SET auto_explain.log_format=json`); + await connection.query(sql`SET auto_explain.log_min_duration=0`); + await connection.query(sql`SET auto_explain.log_timing=true`); + await connection.query(sql`SET client_min_messages=log`); + } + } + ] +}); + +// +// INTERCEPTOR +// ---------------------------------------------------------------------- +createPool('postgres://', { + interceptors: [] +}); + +createPool('postgres://', { + interceptors: [ + ...createInterceptorPreset() + ] +}); + +const interceptors = [ + createBenchmarkingInterceptor(), + createQueryNormalizationInterceptor(), + createFieldNameTransformationInterceptor({ + format: 'CAMEL_CASE' + }) +]; + +const connection = createPool('postgres://', { + interceptors +}); + +connection.any(sql` + SELECT + id, + full_name + FROM person +`); + +// +// TYPE PARSER +// ---------------------------------------------------------------------- +const typeParser: TypeParserType = { + name: 'int8', + parse: value => { + // $ExpectType string + value; + return parseInt(value, 10); + } +}; + +createPool('postgres://', { + typeParsers: [typeParser] +}); + +createPool('postgres://', { + typeParsers: [ + ...createTypeParserPreset() + ] +}); + +createBigintTypeParser(); +createTimestampTypeParser(); +createTimestampWithTimeZoneTypeParser(); + +// +// RECIPES +// ---------------------------------------------------------------------- +(async () => { + await connection.query(sql` + INSERT INTO (foo, bar, baz) + VALUES ${sql.tupleList([ + [1, 2, 3], + [4, 5, 6] + ])} + `); +})(); + +(async () => { + await connection.query(sql` + INSERT INTO (foo, bar, baz) + SELECT * + FROM ${sql.unnest( + [ + [1, 2, 3], + [4, 5, 6] + ], + [ + 'int4', + 'int4', + 'int4' + ] + )} + `); +})(); + +(async () => { + const uniquePairs = [ + ['a', 1], + ['b', 2] + ]; + + let placeholderIndex = 1; + + const whereConditionSql = uniquePairs + .map(needleColumns => { + return needleColumns + .map((column) => { + return `${column} = $${placeholderIndex++}`; + }) + .join(' AND '); + }) + .join(' OR '); + + const values = []; + + for (const pairValues of uniquePairs) { + values.push(...pairValues); + } + + const query = sql` + SELECT + id + FROM foo + WHERE + ${sql.raw(whereConditionSql, values)} + `; + + await connection.any(query); +})(); + +// +// SQL +// ---------------------------------------------------------------------- +(async () => { + // ExpectType SqlSqlTokenType + const query0 = sql`SELECT ${'foo'} FROM bar`; + // ExpectType SqlSqlTokenType + const query1 = sql`SELECT ${'baz'} FROM (${query0})`; + + await connection.query(sql` + SELECT (${sql.valueList([1, 2, 3])}) + `); + + await connection.query(sql` + INSERT INTO (foo, bar, baz) + VALUES ${sql.tuple([1, 2, 3])} + `); + + await connection.query(sql` + INSERT INTO (foo, bar, baz) + VALUES ${sql.tupleList([ + [1, 2, 3], + [4, 5, 6] + ])} + `); + + await connection.query(sql` + SELECT bar, baz + FROM ${sql.unnest( + [ + [1, 'foo'], + [2, 'bar'] + ], + [ + 'int4', + 'text' + ] + )} AS foo(bar, baz) + `); + + sql` + SELECT 1 + FROM ${sql.identifier(['bar', 'baz'])} + `; + + sql` + SELECT 1 + FROM ${sql.raw('"bar"')} + `; + + sql` + SELECT ${sql.raw('$1', [1])} + `; +})(); + +// +// ERRORS +// ---------------------------------------------------------------------- +new SlonikError(); +new NotFoundError(); +new DataIntegrityError(); +new IntegrityConstraintViolationError(); +new NotNullIntegrityConstraintViolationError(); +new ForeignKeyIntegrityConstraintViolationError(); +new UniqueIntegrityConstraintViolationError(); +new CheckIntegrityConstraintViolationError(); diff --git a/types/slonik/symbols.d.ts b/types/slonik/symbols.d.ts new file mode 100644 index 0000000000..028adcb021 --- /dev/null +++ b/types/slonik/symbols.d.ts @@ -0,0 +1,7 @@ +export const SqlTokenSymbol: symbol; +export const RawSqlTokenSymbol: symbol; +export const IdentifierTokenSymbol: symbol; +export const ValueListTokenSymbol: symbol; +export const TupleTokenSymbol: symbol; +export const TupleListTokenSymbol: symbol; +export const UnnestTokenSymbol: symbol; diff --git a/types/slonik/tsconfig.json b/types/slonik/tsconfig.json new file mode 100644 index 0000000000..326d2a1023 --- /dev/null +++ b/types/slonik/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "baseUrl": "../", + "typeRoots": [ + "../" + ], + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "slonik-tests.ts" + ] +} diff --git a/types/slonik/tslint.json b/types/slonik/tslint.json new file mode 100644 index 0000000000..3db14f85ea --- /dev/null +++ b/types/slonik/tslint.json @@ -0,0 +1 @@ +{ "extends": "dtslint/dt.json" }