sql-love 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -1
- package/README.md +253 -27
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/src/SelectBuilder/SelectBuilder.d.ts +30 -7
- package/dist/src/SelectBuilder/SelectBuilder.js +51 -22
- package/dist/src/extractGrouped/extractGrouped.d.ts +17 -0
- package/dist/src/extractGrouped/extractGrouped.js +31 -0
- package/dist/src/extractIndexed/extractIndexed.d.ts +12 -0
- package/dist/src/extractIndexed/extractIndexed.js +23 -0
- package/dist/src/getPagination/getPagination.d.ts +16 -0
- package/dist/src/getPagination/getPagination.js +25 -0
- package/dist/src/runMysql/runMysql.d.ts +46 -0
- package/dist/src/runMysql/runMysql.js +49 -0
- package/dist/src/runPrisma/runPrisma.d.ts +26 -0
- package/dist/src/runPrisma/runPrisma.js +18 -0
- package/index.ts +4 -0
- package/package.json +1 -1
- package/src/SelectBuilder/SelectBuilder.spec.ts +47 -3
- package/src/SelectBuilder/SelectBuilder.ts +85 -26
- package/src/extractGrouped/extractGrouped.spec.ts +43 -0
- package/src/extractGrouped/extractGrouped.ts +36 -0
- package/src/extractIndexed/extractIndexed.spec.ts +40 -0
- package/src/extractIndexed/extractIndexed.ts +28 -0
- package/src/getPagination/getPagination.spec.ts +61 -0
- package/src/getPagination/getPagination.ts +30 -0
- package/src/runMysql/runMysql.spec.ts +164 -0
- package/src/runMysql/runMysql.ts +94 -0
- package/src/runPrisma/runPrisma.spec.ts +67 -0
- package/src/runPrisma/runPrisma.ts +39 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index each database row by a column value
|
|
3
|
+
* @param field The database column to index by
|
|
4
|
+
* @param records Records from the database
|
|
5
|
+
* @example
|
|
6
|
+
* extractIndexed('id', users); // result:
|
|
7
|
+
* {
|
|
8
|
+
* "1": { id: 1, name: "John" },
|
|
9
|
+
* "2": { id: 2, name: "Jane" },
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
export default function extractIndexed<DbRecord extends Record<string, any>>(field: keyof DbRecord, records: DbRecord[]): Record<string, DbRecord>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index each database row by a column value
|
|
3
|
+
* @param field The database column to index by
|
|
4
|
+
* @param records Records from the database
|
|
5
|
+
* @example
|
|
6
|
+
* extractIndexed('id', users); // result:
|
|
7
|
+
* {
|
|
8
|
+
* "1": { id: 1, name: "John" },
|
|
9
|
+
* "2": { id: 2, name: "Jane" },
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
export default function extractIndexed(field, records) {
|
|
13
|
+
let idx = 0;
|
|
14
|
+
const indexed = {};
|
|
15
|
+
for (const record of records) {
|
|
16
|
+
if (!(field in record)) {
|
|
17
|
+
throw new Error(`Column "${String(field)}" is not present in record ${idx}`);
|
|
18
|
+
}
|
|
19
|
+
indexed[record[field]] = record;
|
|
20
|
+
idx++;
|
|
21
|
+
}
|
|
22
|
+
return indexed;
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type SelectBuilder from '../SelectBuilder/SelectBuilder';
|
|
2
|
+
/**
|
|
3
|
+
* Given a foundRows number of records and a limit, return the foundRows number of pages
|
|
4
|
+
* @param query The query that was just run
|
|
5
|
+
* @param total The number of records if there were no limit
|
|
6
|
+
*/
|
|
7
|
+
export default function getPagination(query: SelectBuilder, total: BigInt | number): {
|
|
8
|
+
page: number;
|
|
9
|
+
prevPage: number;
|
|
10
|
+
nextPage: number;
|
|
11
|
+
perPage: number;
|
|
12
|
+
numPages: number;
|
|
13
|
+
total: number;
|
|
14
|
+
isFirst: boolean;
|
|
15
|
+
isLast: boolean;
|
|
16
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Given a foundRows number of records and a limit, return the foundRows number of pages
|
|
3
|
+
* @param query The query that was just run
|
|
4
|
+
* @param total The number of records if there were no limit
|
|
5
|
+
*/
|
|
6
|
+
export default function getPagination(query, total) {
|
|
7
|
+
total = Number(total) || 0;
|
|
8
|
+
const page = Number(query._page) || 1;
|
|
9
|
+
const perPage = Number(query._limit);
|
|
10
|
+
const numPages = Math.ceil(total / perPage) || 0;
|
|
11
|
+
const isFirst = page === 1;
|
|
12
|
+
const isLast = page === numPages;
|
|
13
|
+
const prevPage = page - 1 || null;
|
|
14
|
+
const nextPage = page + 1 <= numPages ? page + 1 : null;
|
|
15
|
+
return {
|
|
16
|
+
page,
|
|
17
|
+
prevPage,
|
|
18
|
+
nextPage,
|
|
19
|
+
perPage,
|
|
20
|
+
numPages,
|
|
21
|
+
total,
|
|
22
|
+
isFirst,
|
|
23
|
+
isLast,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type SelectBuilder from '../SelectBuilder/SelectBuilder';
|
|
2
|
+
import type { EngineStyle } from '../SelectBuilder/SelectBuilder';
|
|
3
|
+
interface MysqlClient {
|
|
4
|
+
query: (sql: string, bindings: any[], callback: (err: Error | null, results: Array<Record<string, any>>, fields: any[]) => void) => void;
|
|
5
|
+
}
|
|
6
|
+
interface MysqlAsyncClient {
|
|
7
|
+
query: (sql: string, bindings: any[]) => Promise<[Array<Record<string, any>>, any[]]>;
|
|
8
|
+
}
|
|
9
|
+
export declare function runMysql(mysqlClient: MysqlClient, query: SelectBuilder): Promise<[Array<Record<string, any>>, any[]]>;
|
|
10
|
+
export declare function runMysqlWithCount(mysqlClient: MysqlClient, query: SelectBuilder, { countExpr }?: {
|
|
11
|
+
countExpr?: string;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
records: Record<string, any>[];
|
|
14
|
+
total: any;
|
|
15
|
+
pagination: {
|
|
16
|
+
page: number;
|
|
17
|
+
prevPage: number;
|
|
18
|
+
nextPage: number;
|
|
19
|
+
perPage: number;
|
|
20
|
+
numPages: number;
|
|
21
|
+
total: number;
|
|
22
|
+
isFirst: boolean;
|
|
23
|
+
isLast: boolean;
|
|
24
|
+
};
|
|
25
|
+
fields: any[];
|
|
26
|
+
}>;
|
|
27
|
+
export declare function runMysqlAsync(mysqlClient: MysqlAsyncClient, query: SelectBuilder): Promise<[Array<Record<string, any>>, any[]]>;
|
|
28
|
+
export declare function runMysqlAsyncWithCount(mysqlClient: MysqlAsyncClient, query: SelectBuilder, { engine, countExpr, }?: {
|
|
29
|
+
engine?: EngineStyle;
|
|
30
|
+
countExpr?: string;
|
|
31
|
+
}): Promise<{
|
|
32
|
+
records: Record<string, any>[];
|
|
33
|
+
total: any;
|
|
34
|
+
pagination: {
|
|
35
|
+
page: number;
|
|
36
|
+
prevPage: number;
|
|
37
|
+
nextPage: number;
|
|
38
|
+
perPage: number;
|
|
39
|
+
numPages: number;
|
|
40
|
+
total: number;
|
|
41
|
+
isFirst: boolean;
|
|
42
|
+
isLast: boolean;
|
|
43
|
+
};
|
|
44
|
+
fields: any[];
|
|
45
|
+
}>;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import getPagination from '../getPagination/getPagination';
|
|
2
|
+
function _queryToPromise(mysqlClient, sql, bindings) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
mysqlClient.query(sql, bindings, (err, rows, fields) => {
|
|
5
|
+
if (err) {
|
|
6
|
+
reject(err);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
resolve([rows, fields]);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export async function runMysql(mysqlClient, query) {
|
|
14
|
+
const { sql, bindings } = query.compile({ engine: 'mysql' });
|
|
15
|
+
return _queryToPromise(mysqlClient, sql, bindings);
|
|
16
|
+
}
|
|
17
|
+
export async function runMysqlWithCount(mysqlClient, query, { countExpr = '*' } = {}) {
|
|
18
|
+
const { sql, bindings } = query.compile({ engine: 'mysql' });
|
|
19
|
+
const [records, fields] = await _queryToPromise(mysqlClient, sql, bindings);
|
|
20
|
+
if (records.length > 0) {
|
|
21
|
+
const { sql, bindings } = query.compileCount({
|
|
22
|
+
engine: 'mysql',
|
|
23
|
+
countExpr,
|
|
24
|
+
});
|
|
25
|
+
const [rows] = await _queryToPromise(mysqlClient, sql, bindings);
|
|
26
|
+
const total = rows[0].found_rows;
|
|
27
|
+
return { records, total, pagination: getPagination(query, total), fields };
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return { records, total: 0, pagination: getPagination(query, 0), fields };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function runMysqlAsync(mysqlClient, query) {
|
|
34
|
+
const { sql, bindings } = query.compile({ engine: 'mysql' });
|
|
35
|
+
return mysqlClient.query(sql, bindings);
|
|
36
|
+
}
|
|
37
|
+
export async function runMysqlAsyncWithCount(mysqlClient, query, { engine = 'mysql', countExpr = '*', } = {}) {
|
|
38
|
+
const { sql, bindings } = query.compile({ engine });
|
|
39
|
+
const [records, fields] = await mysqlClient.query(sql, bindings);
|
|
40
|
+
if (records.length > 0) {
|
|
41
|
+
const { sql, bindings } = query.compileCount({ engine, countExpr });
|
|
42
|
+
const [rows] = await mysqlClient.query(sql, bindings);
|
|
43
|
+
const total = rows[0].found_rows;
|
|
44
|
+
return { records, total, pagination: getPagination(query, total), fields };
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
return { records, total: 0, pagination: getPagination(query, 0), fields };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type SelectBuilder from '../SelectBuilder/SelectBuilder';
|
|
2
|
+
import type { EngineStyle } from '../SelectBuilder/SelectBuilder';
|
|
3
|
+
interface PrismaClient {
|
|
4
|
+
$queryRawUnsafe: (sql: string, ...bindings: any[]) => Promise<Array<Record<string, any>>>;
|
|
5
|
+
}
|
|
6
|
+
export declare function runPrisma(prisma: PrismaClient, query: SelectBuilder, { engine }?: {
|
|
7
|
+
engine?: EngineStyle;
|
|
8
|
+
}): Promise<Array<Record<string, any>>>;
|
|
9
|
+
export declare function runPrismaWithCount(prisma: PrismaClient, query: SelectBuilder, { engine, countExpr, }?: {
|
|
10
|
+
engine?: EngineStyle;
|
|
11
|
+
countExpr?: string;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
records: Record<string, any>[];
|
|
14
|
+
total: any;
|
|
15
|
+
pagination: {
|
|
16
|
+
page: number;
|
|
17
|
+
prevPage: number;
|
|
18
|
+
nextPage: number;
|
|
19
|
+
perPage: number;
|
|
20
|
+
numPages: number;
|
|
21
|
+
total: number;
|
|
22
|
+
isFirst: boolean;
|
|
23
|
+
isLast: boolean;
|
|
24
|
+
};
|
|
25
|
+
}>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import getPagination from '../getPagination/getPagination';
|
|
2
|
+
export async function runPrisma(prisma, query, { engine = undefined } = {}) {
|
|
3
|
+
const { sql, bindings } = query.compile({ engine });
|
|
4
|
+
return prisma.$queryRawUnsafe(sql, ...bindings);
|
|
5
|
+
}
|
|
6
|
+
export async function runPrismaWithCount(prisma, query, { engine = undefined, countExpr = '*', } = {}) {
|
|
7
|
+
const { sql, bindings } = query.compile({ engine });
|
|
8
|
+
const records = await prisma.$queryRawUnsafe(sql, ...bindings);
|
|
9
|
+
if (records.length > 0) {
|
|
10
|
+
const { sql, bindings } = query.compileCount({ engine, countExpr });
|
|
11
|
+
const result = await prisma.$queryRawUnsafe(sql, ...bindings);
|
|
12
|
+
const total = result[0].found_rows;
|
|
13
|
+
return { records, total, pagination: getPagination(query, total) };
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
return { records, total: 0, pagination: getPagination(query, 0) };
|
|
17
|
+
}
|
|
18
|
+
}
|
package/index.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { default as Parser } from './src/Parser/Parser';
|
|
2
2
|
export { default as SelectBuilder } from './src/SelectBuilder/SelectBuilder';
|
|
3
|
+
export { default as extractGrouped } from './src/extractGrouped/extractGrouped';
|
|
4
|
+
export { default as extractIndexed } from './src/extractIndexed/extractIndexed';
|
|
5
|
+
export { default as getPagination } from './src/getPagination/getPagination';
|
|
6
|
+
export { runPrisma, runPrismaWithCount } from './src/runPrisma/runPrisma';
|
package/package.json
CHANGED
|
@@ -31,7 +31,7 @@ describe('where() with arguments', () => {
|
|
|
31
31
|
id: 123,
|
|
32
32
|
}
|
|
33
33
|
);
|
|
34
|
-
const { sql, bindings } = query.compile('pg');
|
|
34
|
+
const { sql, bindings } = query.compile({ engine: 'pg' });
|
|
35
35
|
expect(trim(sql)).toBe('SELECT * FROM users WHERE id = $1 AND age >= $2');
|
|
36
36
|
expect(bindings).toEqual([123, 18]);
|
|
37
37
|
});
|
|
@@ -267,6 +267,14 @@ describe('orWhere()', () => {
|
|
|
267
267
|
expect(trim(sql)).toBe('SELECT * FROM users WHERE (a > ? OR b = ?)');
|
|
268
268
|
expect(bindings).toEqual([1, 2]);
|
|
269
269
|
});
|
|
270
|
+
it('should handle an object', () => {
|
|
271
|
+
const query = new SelectBuilder();
|
|
272
|
+
query.table('users');
|
|
273
|
+
query.orWhere({ 'a >': 1, b: 2 });
|
|
274
|
+
const { sql, bindings } = query.compile();
|
|
275
|
+
expect(trim(sql)).toBe('SELECT * FROM users WHERE (a > ? OR b = ?)');
|
|
276
|
+
expect(bindings).toEqual([1, 2]);
|
|
277
|
+
});
|
|
270
278
|
});
|
|
271
279
|
describe('having()', () => {
|
|
272
280
|
it('should handle expression', () => {
|
|
@@ -520,7 +528,7 @@ describe('LIMIT and OFFSET', () => {
|
|
|
520
528
|
const query = new SelectBuilder('SELECT * FROM posts');
|
|
521
529
|
query.limit(2);
|
|
522
530
|
query.offset(4);
|
|
523
|
-
const { sql, bindings } = query.compile('mssql');
|
|
531
|
+
const { sql, bindings } = query.compile({ engine: 'mssql' });
|
|
524
532
|
expect(trim(sql)).toBe(
|
|
525
533
|
'SELECT * FROM posts OFFSET 4 ROWS FETCH NEXT 2 ROWS ONLY'
|
|
526
534
|
);
|
|
@@ -600,7 +608,10 @@ describe('compileCount()', () => {
|
|
|
600
608
|
});
|
|
601
609
|
it('should handle a distinct', () => {
|
|
602
610
|
const query = new SelectBuilder('SELECT * FROM posts');
|
|
603
|
-
const { sql, bindings } = query.compileCount(
|
|
611
|
+
const { sql, bindings } = query.compileCount({
|
|
612
|
+
countExpr: 'DISTINCT group_id',
|
|
613
|
+
engine: 'mysql',
|
|
614
|
+
});
|
|
604
615
|
expect(trim(sql)).toBe(
|
|
605
616
|
'SELECT COUNT(DISTINCT group_id) AS found_rows FROM posts'
|
|
606
617
|
);
|
|
@@ -627,6 +638,23 @@ describe('compileCount()', () => {
|
|
|
627
638
|
});
|
|
628
639
|
});
|
|
629
640
|
|
|
641
|
+
describe('compileUnion()', () => {
|
|
642
|
+
it('should error on non-number', () => {
|
|
643
|
+
const query1 = new SelectBuilder(
|
|
644
|
+
'SELECT first, last, email FROM users WHERE org = :org AND group = :group',
|
|
645
|
+
{ org: 'ACME', group: 2 }
|
|
646
|
+
);
|
|
647
|
+
const query2 = new SelectBuilder(
|
|
648
|
+
'SELECT first, last, email FROM contacts WHERE org = :org AND group = :group',
|
|
649
|
+
{ org: 'ACME', group: 2 }
|
|
650
|
+
);
|
|
651
|
+
const { sql, bindings } = query1.compileUnion(query2, { engine: 'pg' });
|
|
652
|
+
expect(trim(sql)).toBe(
|
|
653
|
+
`(SELECT first, last, email FROM users WHERE org = $1 AND group = $2) UNION (SELECT first, last, email FROM contacts WHERE org = $3 AND group = $4)`
|
|
654
|
+
);
|
|
655
|
+
expect(bindings).toEqual(['ACME', 2, 'ACME', 2]);
|
|
656
|
+
});
|
|
657
|
+
});
|
|
630
658
|
describe('reset()', () => {
|
|
631
659
|
it('should reset all', () => {
|
|
632
660
|
const query = new SelectBuilder('SELECT id FROM a');
|
|
@@ -739,3 +767,19 @@ describe('tables()', () => {
|
|
|
739
767
|
expect(bindings).toEqual([]);
|
|
740
768
|
});
|
|
741
769
|
});
|
|
770
|
+
describe('setDefaultEngine()', () => {
|
|
771
|
+
it('should set ok', () => {
|
|
772
|
+
SelectBuilder.setDefaultEngine('pg');
|
|
773
|
+
const query = new SelectBuilder('SELECT * FROM users');
|
|
774
|
+
query.where('id', 123);
|
|
775
|
+
const { sql, bindings } = query.compile();
|
|
776
|
+
expect(trim(sql)).toBe('SELECT * FROM users WHERE id = $1');
|
|
777
|
+
expect(bindings).toEqual([123]);
|
|
778
|
+
|
|
779
|
+
// and set engine to something else then compile again
|
|
780
|
+
SelectBuilder.setDefaultEngine('oracle');
|
|
781
|
+
const compiled = query.compile();
|
|
782
|
+
expect(trim(compiled.sql)).toBe('SELECT * FROM users WHERE id = ?');
|
|
783
|
+
expect(compiled.bindings).toEqual([123]);
|
|
784
|
+
});
|
|
785
|
+
});
|
|
@@ -33,6 +33,8 @@ export type WhereCriteria =
|
|
|
33
33
|
|
|
34
34
|
const isNullish = v => v === undefined || v === null || v === false || isNaN(v);
|
|
35
35
|
|
|
36
|
+
let defaultEngine: EngineStyle = 'mysql';
|
|
37
|
+
|
|
36
38
|
/**
|
|
37
39
|
* Build a select query
|
|
38
40
|
* Class Select
|
|
@@ -119,12 +121,27 @@ export default class SelectBuilder {
|
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Set the default database engine style (mssql, mysql, oracle, pg)
|
|
126
|
+
* @param engine
|
|
127
|
+
*/
|
|
128
|
+
static setDefaultEngine(engine: EngineStyle) {
|
|
129
|
+
defaultEngine = engine;
|
|
130
|
+
return SelectBuilder;
|
|
131
|
+
}
|
|
132
|
+
|
|
122
133
|
/**
|
|
123
134
|
* Get the SQL as a pretty-printed string alongside an array of bindings
|
|
124
|
-
* @param
|
|
135
|
+
* @param engine The database engine style (mssql, mysql, oracle, pg)
|
|
125
136
|
* @return The SQL query as a string
|
|
126
137
|
*/
|
|
127
|
-
compile(
|
|
138
|
+
compile({
|
|
139
|
+
engine = defaultEngine,
|
|
140
|
+
bindingIndex = 1,
|
|
141
|
+
}: {
|
|
142
|
+
engine?: EngineStyle;
|
|
143
|
+
bindingIndex?: number;
|
|
144
|
+
} = {}) {
|
|
128
145
|
// build all the lines
|
|
129
146
|
const lines = [
|
|
130
147
|
'SELECT',
|
|
@@ -139,28 +156,28 @@ export default class SelectBuilder {
|
|
|
139
156
|
];
|
|
140
157
|
if (this._page > 0) {
|
|
141
158
|
const offset = (this._page - 1) * this._limit;
|
|
142
|
-
lines.push(this._compileOFFSET(
|
|
143
|
-
lines.push(this._compileLIMIT(
|
|
159
|
+
lines.push(this._compileOFFSET(engine, offset));
|
|
160
|
+
lines.push(this._compileLIMIT(engine, this._limit));
|
|
144
161
|
} else {
|
|
145
162
|
if (this._offset) {
|
|
146
|
-
lines.push(this._compileOFFSET(
|
|
163
|
+
lines.push(this._compileOFFSET(engine, this._offset));
|
|
147
164
|
}
|
|
148
165
|
if (this._limit) {
|
|
149
|
-
lines.push(this._compileLIMIT(
|
|
166
|
+
lines.push(this._compileLIMIT(engine, this._limit));
|
|
150
167
|
}
|
|
151
168
|
}
|
|
152
169
|
|
|
153
170
|
const pretty = lines.filter(Boolean).join('\n').trim();
|
|
154
171
|
let replacer: (match0: string, match1: string) => string;
|
|
155
172
|
const bindings: any = [];
|
|
156
|
-
if (
|
|
157
|
-
let idx =
|
|
158
|
-
replacer = (
|
|
173
|
+
if (engine === 'pg') {
|
|
174
|
+
let idx = bindingIndex;
|
|
175
|
+
replacer = (_, $1) => {
|
|
159
176
|
bindings.push(this._bindings[$1]);
|
|
160
177
|
return '$' + idx++;
|
|
161
178
|
};
|
|
162
179
|
} else {
|
|
163
|
-
replacer = (
|
|
180
|
+
replacer = (_, $1) => {
|
|
164
181
|
bindings.push(this._bindings[$1]);
|
|
165
182
|
return '?';
|
|
166
183
|
};
|
|
@@ -190,9 +207,17 @@ export default class SelectBuilder {
|
|
|
190
207
|
* @param engineStyle The database engine style (mssql, mysql, oracle, pg)
|
|
191
208
|
* @param countExpr The expression to use inside the COUNT()
|
|
192
209
|
*/
|
|
193
|
-
compileCount(
|
|
210
|
+
compileCount({
|
|
211
|
+
countExpr = '*',
|
|
212
|
+
engine = defaultEngine,
|
|
213
|
+
bindingIndex = 1,
|
|
214
|
+
}: {
|
|
215
|
+
countExpr?: string;
|
|
216
|
+
engine?: EngineStyle;
|
|
217
|
+
bindingIndex?: number;
|
|
218
|
+
} = {}) {
|
|
194
219
|
const query = this.getFoundRowsQuery(countExpr);
|
|
195
|
-
const { sql, bindings } = query.compile(
|
|
220
|
+
const { sql, bindings } = query.compile({ engine, bindingIndex });
|
|
196
221
|
if (this._havings.length === 0) {
|
|
197
222
|
return { sql, bindings };
|
|
198
223
|
} else {
|
|
@@ -204,6 +229,32 @@ export default class SelectBuilder {
|
|
|
204
229
|
}
|
|
205
230
|
}
|
|
206
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Get SQL needed to union this query with another
|
|
234
|
+
* @param query The query to union
|
|
235
|
+
* @param engine The database engine style (mssql, mysql, oracle, pg)
|
|
236
|
+
*/
|
|
237
|
+
compileUnion(
|
|
238
|
+
query: SelectBuilder,
|
|
239
|
+
{
|
|
240
|
+
engine = defaultEngine,
|
|
241
|
+
bindingIndex = 1,
|
|
242
|
+
}: {
|
|
243
|
+
engine?: EngineStyle;
|
|
244
|
+
bindingIndex?: number;
|
|
245
|
+
} = {}
|
|
246
|
+
) {
|
|
247
|
+
const first = this.compile({ engine, bindingIndex });
|
|
248
|
+
const second = query.compile({
|
|
249
|
+
engine,
|
|
250
|
+
bindingIndex: first.bindings.length + bindingIndex,
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
sql: `(${first.sql})\nUNION\n(${second.sql})`,
|
|
254
|
+
bindings: [...first.bindings, ...second.bindings],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
207
258
|
/**
|
|
208
259
|
* @param [fieldOrFields] If given, reset the given component(s), otherwise reset all query components
|
|
209
260
|
* Valid components: option, column, table, where, orWhere, having, groupBy, orderBy, limit, offset, page
|
|
@@ -676,13 +727,23 @@ export default class SelectBuilder {
|
|
|
676
727
|
* Add a series of WHERE clauses joined by OR (See _conditions for usage)
|
|
677
728
|
* @param conditions
|
|
678
729
|
*/
|
|
679
|
-
_orConditions(
|
|
730
|
+
_orConditions(
|
|
731
|
+
conditions: WhereCriteria[] | Record<string, any>[] | Record<string, any>
|
|
732
|
+
) {
|
|
680
733
|
const items = [];
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
734
|
+
if (Array.isArray(conditions)) {
|
|
735
|
+
for (const condition of conditions) {
|
|
736
|
+
if (Array.isArray(condition)) {
|
|
737
|
+
this._conditions(items, condition as WhereCriteria);
|
|
738
|
+
} else {
|
|
739
|
+
this._conditions(items, [condition]);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
} else {
|
|
743
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
744
|
+
const one = [];
|
|
745
|
+
this._conditions(one, [key, value]);
|
|
746
|
+
items.push(one[0]);
|
|
686
747
|
}
|
|
687
748
|
}
|
|
688
749
|
const joined = items.join(' OR ');
|
|
@@ -698,7 +759,9 @@ export default class SelectBuilder {
|
|
|
698
759
|
* Add WHERE conditions to place inside an OR block (See _conditions for usage)
|
|
699
760
|
* @param conditions A list or object where each item is an array with parameters that would be taken by where()
|
|
700
761
|
*/
|
|
701
|
-
orWhere(
|
|
762
|
+
orWhere(
|
|
763
|
+
conditions: WhereCriteria[] | Record<string, any>[] | Record<string, any>
|
|
764
|
+
) {
|
|
702
765
|
const joined = this._orConditions(conditions);
|
|
703
766
|
this.where(`(${joined})`);
|
|
704
767
|
return this;
|
|
@@ -708,7 +771,9 @@ export default class SelectBuilder {
|
|
|
708
771
|
* Add an OR with conditions under the HAVING clause
|
|
709
772
|
* @param conditions A list or object where each item is an array with parameters that would be taken by having()
|
|
710
773
|
*/
|
|
711
|
-
orHaving(
|
|
774
|
+
orHaving(
|
|
775
|
+
conditions: WhereCriteria[] | Record<string, any>[] | Record<string, any>
|
|
776
|
+
) {
|
|
712
777
|
const joined = this._orConditions(conditions);
|
|
713
778
|
this.having(`(${joined})`);
|
|
714
779
|
return this;
|
|
@@ -842,10 +907,4 @@ export default class SelectBuilder {
|
|
|
842
907
|
}
|
|
843
908
|
return this;
|
|
844
909
|
}
|
|
845
|
-
|
|
846
|
-
fetch(engine) {
|
|
847
|
-
if (engine.$queryRawUnsafe) {
|
|
848
|
-
//
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
910
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import extractGrouped from './extractGrouped';
|
|
2
|
+
|
|
3
|
+
describe('extractGrouped()', () => {
|
|
4
|
+
it('should handle grouping by string', () => {
|
|
5
|
+
const records = [
|
|
6
|
+
{ id: 1, name: 'John', dept: 'Marketing' },
|
|
7
|
+
{ id: 2, name: 'Jane', dept: 'Finance' },
|
|
8
|
+
{ id: 3, name: 'Tim', dept: 'Marketing' },
|
|
9
|
+
];
|
|
10
|
+
const grouped = extractGrouped('dept', records);
|
|
11
|
+
expect(grouped).toEqual({
|
|
12
|
+
Marketing: [
|
|
13
|
+
{ id: 1, name: 'John', dept: 'Marketing' },
|
|
14
|
+
{ id: 3, name: 'Tim', dept: 'Marketing' },
|
|
15
|
+
],
|
|
16
|
+
Finance: [{ id: 2, name: 'Jane', dept: 'Finance' }],
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it('should handle grouping by integer', () => {
|
|
20
|
+
const records = [
|
|
21
|
+
{ id: 1, name: 'John', deptId: 1 },
|
|
22
|
+
{ id: 2, name: 'Jane', deptId: 2 },
|
|
23
|
+
{ id: 3, name: 'Tim', deptId: 1 },
|
|
24
|
+
];
|
|
25
|
+
const grouped = extractGrouped('deptId', records);
|
|
26
|
+
expect(grouped).toEqual({
|
|
27
|
+
1: [
|
|
28
|
+
{ id: 1, name: 'John', deptId: 1 },
|
|
29
|
+
{ id: 3, name: 'Tim', deptId: 1 },
|
|
30
|
+
],
|
|
31
|
+
2: [{ id: 2, name: 'Jane', deptId: 2 }],
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it('should throw error if column is not present in every record', () => {
|
|
35
|
+
const records = [
|
|
36
|
+
{ id: 1, name: 'John', deptId: 1 },
|
|
37
|
+
{ id: 2, name: 'Jane', deptId: 2 },
|
|
38
|
+
{ id: 3, name: 'Tim' },
|
|
39
|
+
];
|
|
40
|
+
const thrower = () => extractGrouped('deptId', records);
|
|
41
|
+
expect(thrower).toThrow();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group database rows by a column value
|
|
3
|
+
* @param field The database column to group by
|
|
4
|
+
* @param records Records from the database
|
|
5
|
+
* @example
|
|
6
|
+
* extractGrouped('org', users); // result:
|
|
7
|
+
* {
|
|
8
|
+
* "Marketing": [
|
|
9
|
+
* { id: 1, name: "John", org: "Marketing" },
|
|
10
|
+
* { id: 2, name: "Jane", org: "Marketing" },
|
|
11
|
+
* ],
|
|
12
|
+
* "Finance": [
|
|
13
|
+
* { id: 3, name: "Jose", org: "Finance" },
|
|
14
|
+
* ],
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
export default function extractGrouped<DbRecord extends Record<string, any>>(
|
|
18
|
+
field: keyof DbRecord,
|
|
19
|
+
records: DbRecord[]
|
|
20
|
+
): Record<string, DbRecord[]> {
|
|
21
|
+
let idx = 0;
|
|
22
|
+
const grouped: Record<string, DbRecord[]> = {};
|
|
23
|
+
for (const record of records) {
|
|
24
|
+
if (!(field in record)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Column "${String(field)}" is not present in record ${idx}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (!grouped[record[field]]) {
|
|
30
|
+
grouped[record[field]] = [];
|
|
31
|
+
}
|
|
32
|
+
grouped[record[field]].push(record);
|
|
33
|
+
idx++;
|
|
34
|
+
}
|
|
35
|
+
return grouped;
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import extractIndexed from './extractIndexed';
|
|
2
|
+
|
|
3
|
+
describe('extractIndexed()', () => {
|
|
4
|
+
it('should handle indexing by string', () => {
|
|
5
|
+
const records = [
|
|
6
|
+
{ uuid: '2d49f286-790d-4aee-b38b-94d0872e9bd8', name: 'John' },
|
|
7
|
+
{ uuid: '7d02664a-d03e-4882-88e2-23c701c314b5', name: 'Jane' },
|
|
8
|
+
];
|
|
9
|
+
const indexed = extractIndexed('uuid', records);
|
|
10
|
+
expect(indexed).toEqual({
|
|
11
|
+
'2d49f286-790d-4aee-b38b-94d0872e9bd8': {
|
|
12
|
+
uuid: '2d49f286-790d-4aee-b38b-94d0872e9bd8',
|
|
13
|
+
name: 'John',
|
|
14
|
+
},
|
|
15
|
+
'7d02664a-d03e-4882-88e2-23c701c314b5': {
|
|
16
|
+
uuid: '7d02664a-d03e-4882-88e2-23c701c314b5',
|
|
17
|
+
name: 'Jane',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('should handle indexing by integer', () => {
|
|
22
|
+
const records = [
|
|
23
|
+
{ id: 1, name: 'John' },
|
|
24
|
+
{ id: 2, name: 'Jane' },
|
|
25
|
+
];
|
|
26
|
+
const indexed = extractIndexed('id', records);
|
|
27
|
+
expect(indexed).toEqual({
|
|
28
|
+
1: { id: 1, name: 'John' },
|
|
29
|
+
2: { id: 2, name: 'Jane' },
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it('should throw error if column is not present in every record', () => {
|
|
33
|
+
const records = [
|
|
34
|
+
{ id: 1, name: 'John', employeeId: 'js102' },
|
|
35
|
+
{ id: 2, name: 'Jane' },
|
|
36
|
+
];
|
|
37
|
+
const thrower = () => extractIndexed('employeeId', records);
|
|
38
|
+
expect(thrower).toThrow();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index each database row by a column value
|
|
3
|
+
* @param field The database column to index by
|
|
4
|
+
* @param records Records from the database
|
|
5
|
+
* @example
|
|
6
|
+
* extractIndexed('id', users); // result:
|
|
7
|
+
* {
|
|
8
|
+
* "1": { id: 1, name: "John" },
|
|
9
|
+
* "2": { id: 2, name: "Jane" },
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
export default function extractIndexed<DbRecord extends Record<string, any>>(
|
|
13
|
+
field: keyof DbRecord,
|
|
14
|
+
records: DbRecord[]
|
|
15
|
+
) {
|
|
16
|
+
let idx = 0;
|
|
17
|
+
const indexed: Record<string, DbRecord> = {};
|
|
18
|
+
for (const record of records) {
|
|
19
|
+
if (!(field in record)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Column "${String(field)}" is not present in record ${idx}`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
indexed[record[field]] = record;
|
|
25
|
+
idx++;
|
|
26
|
+
}
|
|
27
|
+
return indexed;
|
|
28
|
+
}
|