diff --git a/.gitignore b/.gitignore index 9cf6fffa..c8e16fea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ .idea +yarn.lock diff --git a/.jshintrc b/.jshintrc index a8e1e5f9..73a36c10 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,5 +1,6 @@ { "node": true, + "esversion": 8, "globals": { "describe" : true, "it" : true, diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..e69de29b diff --git a/lib/index.js b/lib/index.js index f6cf39e6..4dd87fb2 100644 --- a/lib/index.js +++ b/lib/index.js @@ -89,3 +89,21 @@ module.exports = new Sql(DEFAULT_DIALECT, {}); module.exports.create = create; module.exports.Sql = Sql; module.exports.Table = Table; + +function specializeColumn(dataType) { + let columMaker = (o) => Object.assign({}, o, { dataType: dataType }); + return columMaker; +} + +module.exports.column = { + text: specializeColumn('text'), + varchar: specializeColumn('varchar'), + uuid: specializeColumn('uuid'), + boolean: specializeColumn('boolean'), + timestamp: specializeColumn('timestamp'), + json: (def) => Object.assign({}, def, { dataType: 'json' }), + jsonb: (def) => Object.assign({}, def, { dataType: 'jsonb' }), + bytea: specializeColumn('bytea'), + integer: specializeColumn('integer'), + custom: (def) => Object.assign({}, def), +}; \ No newline at end of file diff --git a/lib/node/index.js b/lib/node/index.js index 66c0e481..96696c3e 100644 --- a/lib/node/index.js +++ b/lib/node/index.js @@ -59,6 +59,38 @@ Node.prototype.toQuery = function(dialect) { return initializeDialect(Dialect, this).getQuery(this); }; +Node.prototype._exec = function(queryable) { + let q = this.toQuery(); + return queryable.queryAsync(q); +}; + +Node.prototype.execAsync = function(queryable) { + return this._exec(queryable).then(() => {}); +}; + +Node.prototype.toArrayAsync = function(queryable) { + return this._exec(queryable); +}; + +Node.prototype.getAsync = function(queryable) { + return this._exec(queryable).then(res => res.length > 0 ? res[0] : null); +}; + +Node.prototype.firstAsync = function(queryable) { + return this._exec(queryable).then(res => { + if (res.length < 1) throw new Error('No result found'); + return res[0]; + }); +}; + +Node.prototype.singleAsync = function(queryable) { + return this._exec(queryable).then(res => { + if (res.length < 1) throw new Error('No result found'); + if (res.length > 1) throw new Error('More than one result found'); + return res[0]; + }); +}; + Node.prototype.toNamedQuery = function(name, dialect) { if (!name || typeof name !== 'string' || name === '') { throw new Error('A query name has to be a non-empty String.'); diff --git a/lib/types.d.ts b/lib/types.d.ts index 84fb7ab3..637ba0fb 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -4,218 +4,553 @@ * Whole project is MIT licensed, so, we can use it. We also feed back any * improvements, questions, concerns. */ -declare module "sql" { - - type SQLDialects = - | "mssql" - | "mysql" - | "oracle" - | "postgres" - | "sqlite" - ; - - interface OrderByValueNode {} - - interface Named { - name?: Name; - } - interface ColumnDefinition extends Named { - jsType?: Type; - dataType: string; - primaryKey?: boolean; - references?: { - table:string; - column: string; - onDelete?: 'restrict' | 'cascade' | 'no action' | 'set null' | 'set default'; - onUpdate?: 'restrict' | 'cascade' | 'no action' | 'set null' | 'set default'; - }; - notNull?: boolean; - unique?: boolean; - defaultValue?: Type; - } - - interface TableDefinition { - name: Name; - schema: string; - columns: {[CName in keyof Row]: ColumnDefinition}; - dialect?: SQLDialects; - isTemporary?: boolean; - foreignKeys?: { - table: string, - columns: (keyof Row)[], - refColumns: string[], - onDelete?: 'restrict' | 'cascade' | 'no action' | 'set null' | 'set default'; - onUpdate?: 'restrict' | 'cascade' | 'no action' | 'set null' | 'set default'; - } - } - - interface QueryLike { - values: any[] - text:string - } - - interface Executable { - toQuery():QueryLike; - } - - interface Queryable extends Executable { - where(...nodes:any[]):Query - delete():ModifyingQuery - select(star: Column): Query; - select(n1: Column):Query<{[N in N1]: T1}>; - select( - n1: Column, - n2: Column):Query<{[N in N1]: T1} & {[N in N2]: T2}> - select( - n1: Column, - n2: Column, - n3: Column):Query<{[N in N1]: T1} & {[N in N2]: T2} & {[N in N3]: T3}> - select(...nodesOrTables:any[]):Query - - } - - interface Query extends Executable, Queryable { - resultType: T; - - from(table:TableNode):Query - from(statement:string):Query - update(o:{[key: string]:any}):ModifyingQuery - update(o:{}):ModifyingQuery - group(...nodes:any[]):Query - order(...criteria:OrderByValueNode[]):Query - limit(l:number):Query - offset(o:number):Query - } - - interface SubQuery { - select(node:Column):SubQuery - select(...nodes: any[]):SubQuery - where(...nodes:any[]):SubQuery - from(table:TableNode):SubQuery - from(statement:string):SubQuery - group(...nodes:any[]):SubQuery - order(criteria:OrderByValueNode):SubQuery - exists():BinaryNode - notExists(): BinaryNode; - notExists(subQuery:SubQuery):BinaryNode - } - - - interface ModifyingQuery extends Executable { - returning(...nodes:any[]):Query - where(...nodes:any[]):ModifyingQuery - } - - interface TableNode { - join(table:TableNode):JoinTableNode - leftJoin(table:TableNode):JoinTableNode - } - - interface JoinTableNode extends TableNode { - on(filter:BinaryNode):TableNode - on(filter:string):TableNode - } - - interface CreateQuery extends Executable { - ifNotExists():Executable - } - interface DropQuery extends Executable { - ifExists():Executable - } - - type Columns = { - [Name in keyof T]: Column - } - type Table = TableNode & Queryable & Named & Columns & { - getName(): string; - getSchema(): string; - - literal(statement: string): any; - - create():CreateQuery - drop():DropQuery - as(name:OtherName):Table - update(o: Partial):ModifyingQuery - insert(row:T):ModifyingQuery - insert(rows:T[]):ModifyingQuery - select():Query - select(...nodes:any[]):Query - from(table:TableNode):Query - from(statement:string):Query - star():Column - subQuery():SubQuery - columns:Column[] - sql: SQL; - alter():AlterQuery; - indexes(): IndexQuery; - } - - interface AlterQuery extends Executable { - addColumn(column:Column): AlterQuery; - addColumn(name: string, options:string): AlterQuery; - dropColumn(column: Column|string): AlterQuery; - renameColumn(column: Column, newColumn: Column):AlterQuery; - renameColumn(column: Column, newName: string):AlterQuery; - renameColumn(name: string, newName: string):AlterQuery; - rename(newName: string): AlterQuery - } - interface IndexQuery { - create(): IndexCreationQuery; - create(indexName: string): IndexCreationQuery; - drop(indexName: string): Executable; - drop(...columns: Column[]): Executable - } - interface IndexCreationQuery extends Executable { - unique(): IndexCreationQuery; - using(name: string): IndexCreationQuery; - on(...columns: (Column|OrderByValueNode)[]): IndexCreationQuery; - withParser(parserName: string): IndexCreationQuery; - fulltext(): IndexCreationQuery; - spatial(): IndexCreationQuery; - } - - interface SQL { - functions: { - LOWER(c:Column):Column - } - } - - interface BinaryNode { - and(node:BinaryNode):BinaryNode - or(node:BinaryNode):BinaryNode - } - - interface Column { - name: Name - in(arr:T[]):BinaryNode - in(subQuery:SubQuery):BinaryNode - notIn(arr:T[]):BinaryNode - equals(node: T|Column):BinaryNode - notEquals(node: T|Column):BinaryNode - gte(node: T|Column):BinaryNode - lte(node: T|Column):BinaryNode - gt(node:T|Column):BinaryNode - lt(node: T|Column):BinaryNode - like(str:string):BinaryNode - multiply:{ - (node:Column):Column - (n:number):Column //todo check column names - } - isNull():BinaryNode - isNotNull():BinaryNode - //todo check column names - sum():Column - count():Column - count(name:string):Column - distinct():Column - as(name:OtherName):Column - ascending:OrderByValueNode - descending:OrderByValueNode - asc:OrderByValueNode - desc:OrderByValueNode - } - - function define(map:TableDefinition): Table; - function setDialect(dialect: SQLDialects): void; +export type SQLDialects = + | "mssql" + | "mysql" + | "oracle" + | "postgres" + | "sqlite" + ; + +export type DBBigInt = string; +export type DBDecimal = string; + +export type CastMappings = { + text: string; + bigint: DBBigInt; + int: number; + date: Date; + decimal: DBDecimal; +}; + + +export interface OrderByValueNode { } + +interface MaybeNamed { + name?: Name; +} + +interface Named { + name: Name; +} + +export interface ColumnDefinition extends MaybeNamed { + jsType?: Type; + dataType: string; + primaryKey?: boolean; + references?: { + table: string; + column: string; + onDelete?: 'restrict' | 'cascade' | 'no action' | 'set null' | 'set default'; + onUpdate?: 'restrict' | 'cascade' | 'no action' | 'set null' | 'set default'; + }; + notNull?: boolean; + unique?: boolean; + defaultValue?: Type; +} + +export interface TableDefinition { + name: Name; + schema: string; + columns: { [CName in keyof Row]: CName extends string ? ColumnDefinition : never }; + dialect?: SQLDialects; + isTemporary?: boolean; + foreignKeys?: { + table: string, + columns: (keyof Row)[], + refColumns: string[], + onDelete?: 'restrict' | 'cascade' | 'no action' | 'set null' | 'set default'; + onUpdate?: 'restrict' | 'cascade' | 'no action' | 'set null' | 'set default'; + } +} + +export interface QueryLike { + values: any[] + text: string +} + + +export interface QueryExecutor { + queryAsync(query: QueryLike): Promise<{ rowCount: number, rows: T[] }> +} + +export interface Executable { + execAsync(executor: QueryExecutor): Promise + /** + * Get all the results from a query into an array + * @param executor the executor (e.g. transaction) to fetch the results + * @return a promise for the list of results + */ + toArrayAsync(executor: QueryExecutor): Promise + /** + * Get the first result row from the list, if any. + * @param executor the executor (e.g. transaction) to fetch the result + * @return a promise of the result, null if a row doesn't exist + */ + getAsync(executor: QueryExecutor):Promise + /** + * Get the first result row from the list, if any. + * @param executor the executor (e.g. transaction) to fetch the result + * @return a promise of the result, or throws an error if it doesn't exist + */ + firstAsync(executor: QueryExecutor):Promise + /** + * Get the first result row from the list, if any. + * @param executor the executor (e.g. transaction) to fetch the result + * @return a promise of the result, or throws an error if no results or more than one result. + */ + singleAsync(executor: QueryExecutor):Promise + /** + * Convert the query to a Query object with the SQL text and arguments + * @return a QueryLike with text contianing argument placeholder, and an array of arguments + */ + toQuery(): QueryLike; +} + + +type TupleUnion = C[keyof C & number]; + + + +type ColumnNames[]> = TupleUnion< + { [K in keyof C]: C[K] extends Column ? Name : never } +>; + +type FindColumnWithName[]> = TupleUnion< + { [K in keyof C]: C[K] extends Column ? Value : never } +>; + +//@ts-ignore +type RowOf = { [K in ColumnNames]: FindColumnWithName }; + +type WhereCondition = BinaryNode | BinaryNode[] | Partial; + + +interface Queryable { + /** + * Change the resultset source. You may use a join of multiple tables + * + * Note that this method doesn't change the filtering (where) or projection (select) source, so + * any results returned or filters applied will be of the original table or resultset + */ + from(table: TableNode): Query; + from(statement: string): Query; + + /** + * Filter the results by the specified conditions. If multiple conditions are passed, they will + * be joined with AND. A condition may either be a BinaryNode SQL expression, or an object that + * contains the column names and their desired values e.g. `where({ email: "example@test.com" })` + * @param nodes either boolean-evaluating conditional expressions or an object + * @example + * ``` + * users.where({email: "example@test.com"}) + * users.where(user.primaryEmail.equals(user.secondaryEmail)) + * ``` + */ + where(...nodes: WhereCondition[]): Query; + /** + * Create a delete query + */ + delete(): ModifyingQuery; + /** + * Get one or more specific columns from the result set. + * + * Only use this method only after `from` and `where`, otherwise you will be modifying the result + * set shape. + * + * You may use multiple columns from several different tables as long as those tables have been + * joined in a previous `from` call. + * + * In addition you may pass aggregate columns as well as rename columns to have different names + * in the final result set. + */ + select(): Query; + select(n1: Column): Query<{ [N in N1]: T1 }>; + select( + n1: Column, + n2: Column, + ): Query<{ [N in N1]: T1 } & { [N in N2]: T2 }>; + select( + n1: Column, + n2: Column, + n3: Column, + ): Query<{ [N in N1]: T1 } & { [N in N2]: T2 } & { [N in N3]: T3 }>; + + select[]>(...cols: Cols): Query>; + select(...nodesOrTables: any[]): Query; + + /** + * Update columns of the table. + * @params o - a partial row object matching the keys and values of the table row + */ + update(o: Partial): ModifyingQuery; + + /** + * Order results by the specified order criteria. You may obtain ordering criteria by accessing + * the .asc or .desc properties of columns + * @example + * ``` + * users.where(...).order(user.dateRegistered.desc) + * ``` + */ + order(...criteria: OrderByValueNode[]): Query; + + /** + * Limit number of results + * @param l the limit + */ + limit(l: number): Query; + /** + * Getthe result starting the specified offset index + * @param o the offset + */ + offset(o: number): Query; +} + +export interface NonExecutableQuery extends Queryable { + /** + * Group by one or more columns + * @example + * ``` + * userPoints.where(userPoints.id.in(userIdList)).select(userPoints.point.sum()).group(userPoints.userId) + * ``` + */ + group(...nodes: Column[]): Query; + group(nodes: Column[]): Query; + + /** + * Get distinct result based on one or more columns. Use after select() + */ + distinctOn(...columns: Column[]): Query; // todo: Column can be more specific +} + + +export interface Query extends Queryable, NonExecutableQuery { } + +export interface SubQuery extends NonExecutableQuery { + /** + * Convert the subquery into an exists (subquery) + */ + exists(): BinaryNode; + + /** + * Convert the subquery into an NOT EXISTS (subquery) + */ + notExists(): BinaryNode; + notExists(subQuery: SubQuery): BinaryNode; +} + + +export interface ModifyingQuery extends Executable { + /** + * Pick columns to return from the modifying query, or use star to return all rows + */ + returning[]>(...cols: Cols): Query>; + returning(star: '*'): Query; + + /** + * Filter the modifications by the specified conditions. If multiple conditions are passed, they will + * be joined with AND. A condition may either be a BinaryNode SQL expression, or an object that + * contains the column names and their desired values e.g. `where({ email: "example@test.com" })` + * + * @param nodes either boolean-evaluating conditional expressions or an object + * + * @example + * ``` + * users.where({email: "example@test.com"}) + * users.where(user.primaryEmail.equals(user.secondaryEmail)) + * ``` + */ + where(...nodes: WhereCondition[]): ModifyingQuery; } + +export interface TableNode { + /** + * Within a from condition, join this table node with another table node + */ + join(table: TableNode): JoinTableNode; + /** + * Within a from condition, LEFT JOIN this table node with another table node + */ + leftJoin(table: TableNode): JoinTableNode; +} + + +export interface JoinTableNode extends TableNode { + /** + * Specify the joining condition for a join table node + * + * @param filter a binary expression describing the join condition + * + * @example + * users.from(users.join(posts).on(users.id.equals(posts.userId))) + */ + on(filter: BinaryNode): TableNode; + on(filter: string): TableNode; +} + +interface CreateQuery extends Executable { + ifNotExists(): Executable; +} +interface DropQuery extends Executable { + ifExists(): Executable; +} + +export type Columns = { + [Name in keyof T]: Column +} +export type Table = TableNode & Queryable & Named & Columns & { + getName(): string; + getSchema(): string; + + literal(statement: string): any; + + create(): CreateQuery + drop(): DropQuery + as(name: OtherName): Table + update(o: Partial): ModifyingQuery + insert(row: T): ModifyingQuery; + insert(rows: T[]): ModifyingQuery; + select(): Query + star(): Column + subQuery(): SubQuery; + columns: Column[]; + sql: SQL; + alter(): AlterQuery; + indexes(): IndexQuery; + count(): Query; +} + +export interface AlterQuery extends Executable { + addColumn(column: Column): AlterQuery; + addColumn(name: string, options: string): AlterQuery; + dropColumn(column: Column | string): AlterQuery; + renameColumn(column: Column, newColumn: Column): AlterQuery; + renameColumn(column: Column, newName: string): AlterQuery; + renameColumn(name: string, newName: string): AlterQuery; + rename(newName: string): AlterQuery; +} + +export interface IndexQuery { + create(): IndexCreationQuery; + create(indexName: string): IndexCreationQuery; + drop(indexName: string): Executable; + drop(...columns: Column[]): Executable; +} + +export interface IndexCreationQuery extends Executable { + unique(): IndexCreationQuery; + using(name: string): IndexCreationQuery; + on(...columns: (Column | OrderByValueNode)[]): IndexCreationQuery; + withParser(parserName: string): IndexCreationQuery; + fulltext(): IndexCreationQuery; + spatial(): IndexCreationQuery; +} + +export interface SQL { + functions: { + LOWER(c: Column): Column + } +} + +export interface BinaryNode { + and(node: BinaryNode): BinaryNode + or(node: BinaryNode): BinaryNode +} + +export interface Column { + name: Name; + + /** + * The column value can be found in a given array of items or in a subquery + * + * @param arr the Array + * @returns a binary node that can be used in where expressions + * + * @example + * ``` + * users.where(user.email.in(emailArray)) + * ``` + */ + in(arr: T[]): BinaryNode; + in(subQuery: SubQuery): BinaryNode; + + /** + * The column value can NOT be found in a given array of items or in a subquery + * + * @param arr the Array + * @returns a binary node that can be used in where expressions + * + * @example + * ``` + * users.where(user.email.notIn(bannedUserEmails)) + * ``` + */ + notIn(arr: T[]): BinaryNode; + + /** + * Check if the column value equals another (column) value + */ + equals(node: U | Column): BinaryNode; + + /** + * Check if the column value does NOT equal another (column) value + */ + notEquals(node: U | Column): BinaryNode; + + /** + * Check if the column value is greater than or equal to another column value + */ + gte(node: T | Column | number | Column): BinaryNode; + + /** + * Check if the column value is less than or equal to another column value + */ + lte(node: T | Column | number | Column): BinaryNode; + + /** + * Check if the column value is greater than another column value + */ + gt(node: T | Column | number | Column): BinaryNode; + + /** + * Check if the column value is less than another column value + */ + lt(node: T | Column | number | Column): BinaryNode; + + /** + * Check if the node matches a LIKE expression. See the database documentation for LIKE expression syntax + */ + like(str: string): BinaryNode; + + /** + * Check if the node does NOT match a LIKE expression. See the database documentation for LIKE expression syntax + */ + notLike(str: string): BinaryNode; + + /** + * Check if the node matches a case Insensitive LIKE expression. + * See the database documentation for LIKE expression syntax + */ + ilike(str: string): BinaryNode; + + /** + * Check if the node does NOT match a case Insensitive LIKE expression. + * See the database documentation for LIKE expression syntax + */ + notILike(str: string): BinaryNode; + + /** + * Multiply the node with another node or value + */ + multiply(node: Column | Column | T | number): Column; + + /** + * Check if the column is null + */ + isNull(): BinaryNode; + + /** + * Check if the column is NOT null + */ + isNotNull(): BinaryNode; + + /** + * Compute a sum of the column. + * @deprecated Please use the named variant! + */ + sum(): Column; + + /** + * Compute a sum of the column and give it a name + * @param name the new colum name + */ + sum(n: Name): Column; + + /** + * Compute a count of the column or results + * @deprecated Please use the named variant! + */ + count(): Column; + + /** + * Compute a count of the column or results and give it a name + * @param name the new colum name + */ + count(name: Name): Column; + + /** + * Get the distinct values of this column (without repetition + * + * @example + * ``` + * users.select(user.email.distinct()) + * ``` + */ + distinct(): Column; + + /** + * Give this column another name in the result set + * + * @param name the new name + * + * @example + * ``` + * users.select(user.email.as('electronicMail')) + * ``` + */ + as(name: OtherName): Column; + + /** + * Get an ascending ordering direction for this column + */ + ascending: OrderByValueNode; + + /** + * Get an descending ordering direction for this column + */ + descending: OrderByValueNode; + + /** + * Get an ascending ordering direction for this column + */ + asc: OrderByValueNode; + + /** + * Get an descending ordering direction for this column + */ + desc: OrderByValueNode; + + /** + * Access a JSON key within the specified column + */ + key(key: Key): Column; + + /** + * Access a JSON key within a specified column and convert it to string + */ + keyText(key: Key): Column; + + contains(key: any): Column; + cast(type: T): Column; +} + +export function define(map: TableDefinition): Table; +export function setDialect(dialect: SQLDialects): void; + +export declare type SpecializeColumn = (def?: ColumnDefinition) => ColumnDefinition; +export declare function specializeColumn(dataType: string): SpecializeColumn; +export declare let column: { + text: SpecializeColumn; + varchar: SpecializeColumn; + uuid: SpecializeColumn; + boolean: SpecializeColumn; + timestamp: SpecializeColumn; + json: (def?: ColumnDefinition) => ColumnDefinition; + jsonb: (def?: ColumnDefinition) => ColumnDefinition; + bytea: SpecializeColumn; + integer: SpecializeColumn; + custom: (def?: ColumnDefinition) => ColumnDefinition; +} + diff --git a/package.json b/package.json index addbe86b..41902605 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "main": "lib/", "types": "lib/types.d.ts", "scripts": { - "test": "node_modules/.bin/mocha", + "test": "mocha", "lint": "jshint lib test", "posttest": "jshint lib test" }, @@ -20,11 +20,12 @@ "node": "*" }, "dependencies": { - "sliced": "0.0.x", - "lodash": "^4.17.5" + "lodash": "^4.17.5", + "sliced": "0.0.x" }, "devDependencies": { "jshint": "*", - "mocha": "*" + "mocha": "*", + "prettier": "^1.17.0" } } diff --git a/runtests.js b/runtests.js deleted file mode 100644 index ac89769b..00000000 --- a/runtests.js +++ /dev/null @@ -1,18 +0,0 @@ -var childProcess = require("child_process"); -var path = require("path"); - -var env = process.env; -env.NODE_ENV = "test"; - -var options = { - env: env -}; - -var command = path.join(".", "node_modules", ".bin", "mocha"); -if (process.platform == "win32") command += ".cmd"; -var run = childProcess.spawn(command, [], options); -run.stdout.pipe(process.stdout); -run.stderr.pipe(process.stderr); -run.on('close', function(code) { - process.exit(code); -}); diff --git a/test/column-tests.js b/test/column-tests.js index d0b95a14..2295bf12 100644 --- a/test/column-tests.js +++ b/test/column-tests.js @@ -3,64 +3,64 @@ var assert = require('assert'); var sql = require(__dirname + '/../lib'); -describe('column', function() { +suite('column', function() { var table = sql.define({ name: 'user', columns: ['id', 'created', 'alias'] }); - it('can be accessed by property and array', function() { + test('can be accessed by property and array', function() { assert.equal(table.created, table.columns[1], 'should be able to access created both by array and property'); }); - describe('toQuery()', function() { - it('works', function() { + suite('toQuery()', function() { + test('works', function() { assert.equal(table.id.toQuery().text, '"user"."id"'); }); - it('works with a column name of "alias"', function() { + test('works with a column name of "alias"', function() { assert.equal(table.alias.toQuery().text, '"user"."alias"'); }); - it('respects AS rename', function() { + test('respects AS rename', function() { assert.equal(table.id.as('userId').toQuery().text, '"user"."id" AS "userId"'); }); - it('respects count and distinct', function() { + test('respects count and distinct', function() { assert.equal(table.id.count().distinct().as("userIdCount").toQuery().text, 'COUNT(DISTINCT("user"."id")) AS "userIdCount"'); }); - describe('in subquery with min', function() { + suite('in subquery with min', function() { var subquery = table.subQuery('subTable').select(table.id.min().as('subId')); var col = subquery.subId.toQuery().text; assert.equal(col, '"subTable"."subId"'); }); - describe('property', function() { + suite('property', function() { var table = sql.define({ name: 'roundtrip', columns: { column_name: { property: 'propertyName' } } }); - it('used as alias when !== column name', function() { + test('used as alias when !== column name', function() { assert.equal(table.propertyName.toQuery().text, '"roundtrip"."column_name" AS "propertyName"'); }); - it('uses explicit alias when !== column name', function() { + test('uses explicit alias when !== column name', function() { assert.equal(table.propertyName.as('alias').toQuery().text, '"roundtrip"."column_name" AS "alias"'); }); - it('maps to column name in insert', function() { + test('maps to column name in insert', function() { assert.equal(table.insert({propertyName:'propVal'}).toQuery().text, 'INSERT INTO "roundtrip" ("column_name") VALUES ($1)'); }); - it('maps to column name in update', function() { + test('maps to column name in update', function() { assert.equal(table.update({propertyName:'propVal'}).toQuery().text, 'UPDATE "roundtrip" SET "column_name" = $1'); }); - it('explicitly selected by *', function() { + test('explicitly selected by *', function() { assert.equal(table.select(table.star()).from(table).toQuery().text, 'SELECT "roundtrip"."column_name" AS "propertyName" FROM "roundtrip"'); }); }); - describe('autoGenerate', function() { + suite('autoGenerate', function() { var table = sql.define({ name: 'ag', columns: { @@ -68,42 +68,42 @@ describe('column', function() { name: {} } }); - it('does not include auto generated columns in insert', function() { + test('does not include auto generated columns in insert', function() { assert.equal(table.insert({id:0, name:'name'}).toQuery().text,'INSERT INTO "ag" ("name") VALUES ($1)'); }); - it('does not include auto generated columns in update', function() { + test('does not include auto generated columns in update', function() { assert.equal(table.update({id:0, name:'name'}).toQuery().text,'UPDATE "ag" SET "name" = $1'); }); }); - describe('white listed', function() { + suite('white listed', function() { var table = sql.define({ name: 'wl', columnWhiteList: true, columns: ['id', 'name'] }); - it('excludes insert properties that are not a column', function() { + test('excludes insert properties that are not a column', function() { assert.equal(table.insert({id:0, _private:'_private', name:'name'}).toQuery().text, 'INSERT INTO "wl" ("id", "name") VALUES ($1, $2)'); }); - it('excludes update properties that are not a column', function() { + test('excludes update properties that are not a column', function() { assert.equal(table.update({id:0, _private:'_private', name:'name'}).toQuery().text, 'UPDATE "wl" SET "id" = $1, "name" = $2'); }); }); - describe('not white listed', function() { + suite('not white listed', function() { var table = sql.define({ name: 'wl', columns: ['id', 'name'] }); - it('throws for insert properties that are not a column', function() { + test('throws for insert properties that are not a column', function() { assert.throws(function() { table.insert({id:0, _private:'_private', name:'name'}); }, Error); }); - it('throws for update properties that are not a column', function() { + test('throws for update properties that are not a column', function() { assert.throws(function() { table.update({id:0, _private:'_private', name:'name'}); }, Error); }); }); - describe('snake to camel', function() { + suite('snake to camel', function() { var table = sql.define({ name: 'sc', snakeToCamel: true, @@ -112,17 +112,17 @@ describe('column', function() { not_to_camel: {property: 'not2Cam'} } }); - it('for snake column names with no explicit property name', function(){ + test('for snake column names with no explicit property name', function(){ assert.equal(table.makeMeCamel.toQuery().text, '"sc"."make_me_camel" AS "makeMeCamel"'); }); - it('but not when with explicit property name', function() { + test('but not when with explicit property name', function() { assert.equal(table.not2Cam.toQuery().text, '"sc"."not_to_camel" AS "not2Cam"'); }); - it('does not use property alias within CASE ... END', function() { + test('does not use property alias within CASE ... END', function() { assert.equal(table.makeMeCamel.case([table.makeMeCamel.equals(0)],[table.makeMeCamel]).as('rename').toQuery().text, '(CASE WHEN ("sc"."make_me_camel" = $1) THEN "sc"."make_me_camel" END) AS "rename"'); }); - it('respects AS rename in RETURNING clause', function() { + test('respects AS rename in RETURNING clause', function() { assert.equal(table.update({makeMeCamel:0}).returning(table.makeMeCamel.as('rename')).toQuery().text, 'UPDATE "sc" SET "make_me_camel" = $1 RETURNING "make_me_camel" AS "rename"'); }); diff --git a/test/execution-tests.js b/test/execution-tests.js new file mode 100644 index 00000000..3a10ea8a --- /dev/null +++ b/test/execution-tests.js @@ -0,0 +1,51 @@ +var sql = require(__dirname + '/../lib'); +var assert = require('assert'); + +suite('execution', function() { + var table = sql.define({ + name: 'user', + columns: ['id', 'created', 'alias'] + }); + + let mkExecutor = (results) => ({ + queryAsync(q) { + return Promise.resolve(results); + } + }); + + test('execAsync', async () => { + return table.where({id: 1}).execAsync(mkExecutor([1])); + }); + + test('getAsync single', async () => { + assert.equal(await table.where({id: 1}).getAsync(mkExecutor([1])), 1); + }); + + test('getAsync no results', async () => { + assert.equal(await table.where({id: 1}).getAsync(mkExecutor([])), undefined); + }); + + test('singleAsync single', async () => { + assert.equal(await table.where({id: 1}).singleAsync(mkExecutor([1])), 1); + }); + + test('singleAsync no results', async () => { + try { + await table.where({id: 1}).singleAsync(mkExecutor([])); + } catch (e) { + assert.equal(e.message, 'No result found'); + } + }); + + test('singleAsync more results', async () => { + try { + await table.where({id: 1}).singleAsync(mkExecutor([1,2])); + } catch (e) { + assert.equal(e.message, 'More than one result found'); + } + }); + + test('toArrayAsync', async () => { + assert.deepEqual(await table.where({id: 1}).toArrayAsync(mkExecutor([1,2,3])), [1,2,3]); + }); +}); \ No newline at end of file