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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sql-love",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Build SQL queries with JavaScript",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -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('DISTINCT group_id');
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 engineStyle The database engine style (mssql, mysql, oracle, pg)
135
+ * @param engine The database engine style (mssql, mysql, oracle, pg)
125
136
  * @return The SQL query as a string
126
137
  */
127
- compile(engineStyle: EngineStyle = 'mysql') {
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(engineStyle, offset));
143
- lines.push(this._compileLIMIT(engineStyle, this._limit));
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(engineStyle, this._offset));
163
+ lines.push(this._compileOFFSET(engine, this._offset));
147
164
  }
148
165
  if (this._limit) {
149
- lines.push(this._compileLIMIT(engineStyle, this._limit));
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 (engineStyle === 'pg') {
157
- let idx = 1;
158
- replacer = ($0, $1) => {
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 = ($0, $1) => {
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(engineStyle: EngineStyle = 'mysql', countExpr: string = '*') {
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(engineStyle);
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(conditions: WhereCriteria[] | Record<string, any>[]) {
730
+ _orConditions(
731
+ conditions: WhereCriteria[] | Record<string, any>[] | Record<string, any>
732
+ ) {
680
733
  const items = [];
681
- for (const condition of conditions) {
682
- if (Array.isArray(condition)) {
683
- this._conditions(items, condition as WhereCriteria);
684
- } else {
685
- this._conditions(items, [condition]);
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(conditions: WhereCriteria[] | Record<string, any>[]) {
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(conditions: WhereCriteria[] | Record<string, any>[]) {
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
+ }