pqb 0.3.0 → 0.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pqb",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Postgres query builder",
5
5
  "homepage": "https://porm.netlify.app/guide/query-builder.html",
6
6
  "repository": {
@@ -3,6 +3,7 @@ import { JSONTypeAny } from './json/typeBase';
3
3
  import { ColumnsShape } from './columnsSchema';
4
4
  import { RawExpression, StringKey } from '../common';
5
5
  import { MaybeArray } from '../utils';
6
+ import { Query } from '../query';
6
7
 
7
8
  export type ColumnOutput<T extends ColumnType> = T['type'];
8
9
 
@@ -44,6 +45,7 @@ export type ColumnData = {
44
45
  collate?: string;
45
46
  compression?: string;
46
47
  foreignKey?: ForeignKey<string, string[]>;
48
+ modifyQuery?: (q: Query) => void;
47
49
  };
48
50
 
49
51
  type ForeignKeyMatch = 'FULL' | 'PARTIAL' | 'SIMPLE';
@@ -109,6 +111,16 @@ export type ForeignKeyModelWithColumns = new () => {
109
111
  export type ColumnNameOfModel<Model extends ForeignKeyModelWithColumns> =
110
112
  StringKey<keyof InstanceType<Model>['columns']['shape']>;
111
113
 
114
+ const addColumnData = <T extends ColumnType, K extends keyof ColumnData>(
115
+ q: T,
116
+ key: K,
117
+ value: T['data'][K],
118
+ ): T => {
119
+ const cloned = Object.create(q);
120
+ cloned.data = { ...q.data, [key]: value };
121
+ return cloned;
122
+ };
123
+
112
124
  export abstract class ColumnType<
113
125
  Type = unknown,
114
126
  Ops extends Operators = Operators,
@@ -217,51 +229,43 @@ export abstract class ColumnType<
217
229
  this: T,
218
230
  value: T['type'] | RawExpression,
219
231
  ): T & { hasDefault: true } {
220
- const cloned = Object.create(this);
221
- cloned.data = { ...cloned.data, default: value };
222
- return cloned;
232
+ return addColumnData(this, 'default', value as unknown) as T & {
233
+ hasDefault: true;
234
+ };
223
235
  }
224
236
 
225
237
  index<T extends ColumnType>(
226
238
  this: T,
227
239
  options: Omit<SingleColumnIndexOptions, 'column'> = {},
228
240
  ): T {
229
- const cloned = Object.create(this);
230
- cloned.data = { ...cloned.data, index: options };
231
- return cloned;
241
+ return addColumnData(this, 'index', options);
232
242
  }
233
243
 
234
244
  unique<T extends ColumnType>(
235
245
  this: T,
236
246
  options: Omit<SingleColumnIndexOptions, 'column' | 'unique'> = {},
237
247
  ): T {
238
- const cloned = Object.create(this);
239
- cloned.data = { ...cloned.data, index: { ...options, unique: true } };
240
- return cloned;
248
+ return addColumnData(this, 'index', { ...options, unique: true });
241
249
  }
242
250
 
243
251
  comment<T extends ColumnType>(this: T, comment: string): T {
244
- const cloned = Object.create(this);
245
- cloned.data = { ...cloned.data, comment };
246
- return cloned;
252
+ return addColumnData(this, 'comment', comment);
247
253
  }
248
254
 
249
255
  validationDefault<T extends ColumnType>(this: T, value: T['type']): T {
250
- const cloned = Object.create(this);
251
- cloned.data = { ...cloned.data, validationDefault: value };
252
- return cloned;
256
+ return addColumnData(this, 'validationDefault', value as unknown);
253
257
  }
254
258
 
255
259
  compression<T extends ColumnType>(this: T, compression: string): T {
256
- const cloned = Object.create(this);
257
- cloned.data = { ...cloned.data, compression };
258
- return cloned;
260
+ return addColumnData(this, 'compression', compression);
259
261
  }
260
262
 
261
263
  collate<T extends ColumnType>(this: T, collate: string): T {
262
- const cloned = Object.create(this);
263
- cloned.data = { ...cloned.data, collate };
264
- return cloned;
264
+ return addColumnData(this, 'collate', collate);
265
+ }
266
+
267
+ modifyQuery<T extends ColumnType>(this: T, cb: (q: Query) => void): T {
268
+ return addColumnData(this, 'modifyQuery', cb);
265
269
  }
266
270
 
267
271
  transform<T extends ColumnType, Transformed>(
@@ -58,7 +58,7 @@ import {
58
58
  } from './columnType';
59
59
  import { emptyObject, EmptyObject, MaybeArray, toArray } from '../utils';
60
60
  import { ColumnsShape } from './columnsSchema';
61
- import { raw } from '../common';
61
+ import { timestamps } from './timestamps';
62
62
 
63
63
  export type ColumnTypes = typeof columnTypes;
64
64
 
@@ -177,17 +177,7 @@ export const columnTypes = {
177
177
  jsonText: () => new JSONTextColumn(),
178
178
  array: <Item extends ColumnType>(item: Item) => new ArrayColumn(item),
179
179
 
180
- timestamps<T extends ColumnType>(this: {
181
- timestamp(): T;
182
- }): {
183
- createdAt: T & { hasDefault: true };
184
- updatedAt: T & { hasDefault: true };
185
- } {
186
- return {
187
- createdAt: this.timestamp().default(raw('now()')),
188
- updatedAt: this.timestamp().default(raw('now()')),
189
- };
190
- },
180
+ timestamps,
191
181
 
192
182
  primaryKey(columns: string[], options?: { name?: string }) {
193
183
  tableData.primaryKey = { columns, options };
@@ -0,0 +1,79 @@
1
+ import { db, expectSql, now, useTestDatabase } from '../test-utils';
2
+ import { raw } from '../common';
3
+
4
+ describe('timestamps', () => {
5
+ useTestDatabase();
6
+
7
+ const model = db('user', (t) => ({
8
+ name: t.string(),
9
+ ...t.timestamps(),
10
+ }));
11
+
12
+ it('should update updatedAt column when updating', async () => {
13
+ const query = model.update({}, true);
14
+ await query;
15
+
16
+ expectSql(
17
+ query.toSql(),
18
+ `
19
+ UPDATE "user"
20
+ SET "updatedAt" = now()
21
+ `,
22
+ );
23
+ });
24
+
25
+ it('should not update updatedAt column when updating it via object', async () => {
26
+ const query = model.update({ updatedAt: now }, true);
27
+ await query;
28
+
29
+ expectSql(
30
+ query.toSql(),
31
+ `
32
+ UPDATE "user"
33
+ SET "updatedAt" = $1
34
+ `,
35
+ [now],
36
+ );
37
+ });
38
+
39
+ it('should update updatedAt when updating with raw sql', async () => {
40
+ const query = model.updateRaw(raw('name = $1', 'name'), true);
41
+ await query;
42
+
43
+ expectSql(
44
+ query.toSql(),
45
+ `
46
+ UPDATE "user"
47
+ SET name = $1, "updatedAt" = now()
48
+ `,
49
+ ['name'],
50
+ );
51
+ });
52
+
53
+ it('should update updatedAt when updating with raw sql which has updatedAt somewhere but not in set', async () => {
54
+ const query = model.updateRaw(raw('"createdAt" = "updatedAt"'), true);
55
+ await query;
56
+
57
+ expectSql(
58
+ query.toSql(),
59
+ `
60
+ UPDATE "user"
61
+ SET "createdAt" = "updatedAt", "updatedAt" = now()
62
+ `,
63
+ );
64
+ });
65
+
66
+ it('should not update updatedAt column when updating with raw sql which contains `updatedAt = `', async () => {
67
+ const query = model.updateRaw(raw('"updatedAt" = $1', now), true);
68
+ await query;
69
+
70
+ expectSql(
71
+ query.toSql(),
72
+ `
73
+ UPDATE "user"
74
+ SET "updatedAt" = $1
75
+ `,
76
+ [now],
77
+ );
78
+ });
79
+ });
@@ -0,0 +1,49 @@
1
+ import { ColumnType } from './columnType';
2
+ import { getRawSql, isRaw, raw } from '../common';
3
+ import { Query } from '../query';
4
+ import { makeRegexToFindInSql, pushOrNewArrayToObject } from '../utils';
5
+ import {
6
+ UpdatedAtDataInjector,
7
+ UpdateQueryData,
8
+ UpdateQueryDataItem,
9
+ } from '../sql';
10
+
11
+ export function timestamps<T extends ColumnType>(this: {
12
+ timestamp(): T;
13
+ }): {
14
+ createdAt: T & { hasDefault: true };
15
+ updatedAt: T & { hasDefault: true };
16
+ } {
17
+ return {
18
+ createdAt: this.timestamp().default(raw('now()')),
19
+ updatedAt: this.timestamp()
20
+ .default(raw('now()'))
21
+ .modifyQuery(addHookForUpdate),
22
+ };
23
+ }
24
+
25
+ const updatedAtRegex = makeRegexToFindInSql('\\bupdatedAt\\b"?\\s*=');
26
+ const updateUpdatedAtItem = raw('"updatedAt" = now()');
27
+
28
+ const addHookForUpdate = (q: Query) => {
29
+ pushOrNewArrayToObject(
30
+ q.query as UpdateQueryData,
31
+ 'updateData',
32
+ updatedAtInjector,
33
+ );
34
+ };
35
+
36
+ const updatedAtInjector: UpdatedAtDataInjector = (data) => {
37
+ return checkIfDataHasUpdatedAt(data) ? undefined : updateUpdatedAtItem;
38
+ };
39
+
40
+ const checkIfDataHasUpdatedAt = (data: UpdateQueryDataItem[]) => {
41
+ return data.some((item) => {
42
+ if (isRaw(item)) {
43
+ updatedAtRegex.lastIndex = 0;
44
+ return updatedAtRegex.test(getRawSql(item));
45
+ } else {
46
+ return typeof item !== 'function' && item.updatedAt;
47
+ }
48
+ });
49
+ };
@@ -25,7 +25,7 @@ const quoteValue = (
25
25
  }
26
26
 
27
27
  if ('toSql' in arg) {
28
- const sql = (arg as Query).toSql(values);
28
+ const sql = (arg as Query).toSql({ values });
29
29
  return `(${sql.text})`;
30
30
  }
31
31
 
package/src/common.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Query, Selectable } from './query';
2
- import { ColumnOutput, ColumnType } from './columnSchema';
2
+ import { ColumnOutput, ColumnType } from './columnSchema/columnType';
3
3
 
4
4
  export type AliasOrTable<T extends Pick<Query, 'tableAlias' | 'table'>> =
5
5
  T['tableAlias'] extends string
@@ -81,6 +81,10 @@ export const getRaw = (raw: RawExpression, values: unknown[]) => {
81
81
  return raw.__raw;
82
82
  };
83
83
 
84
+ export const getRawSql = (raw: RawExpression) => {
85
+ return raw.__raw;
86
+ };
87
+
84
88
  export const EMPTY_OBJECT = {};
85
89
 
86
90
  export const getQueryParsers = (q: Query) => {
package/src/db.ts CHANGED
@@ -4,8 +4,16 @@ import {
4
4
  defaultsKey,
5
5
  Query,
6
6
  } from './query';
7
- import { QueryMethods } from './queryMethods/queryMethods';
8
- import { QueryData, SelectQueryData, Sql } from './sql';
7
+ import {
8
+ QueryMethods,
9
+ handleResult,
10
+ ThenResult,
11
+ WhereQueryBuilder,
12
+ OnQueryBuilder,
13
+ logParamToLogObject,
14
+ QueryLogOptions,
15
+ } from './queryMethods';
16
+ import { QueryData, SelectQueryData, Sql, ToSqlOptions } from './sql';
9
17
  import { AdapterOptions, Adapter } from './adapter';
10
18
  import {
11
19
  ColumnsShape,
@@ -17,12 +25,8 @@ import {
17
25
  ColumnTypesBase,
18
26
  getColumnTypes,
19
27
  } from './columnSchema';
20
- import { applyMixins } from './utils';
28
+ import { applyMixins, pushOrNewArray } from './utils';
21
29
  import { StringKey } from './common';
22
- import { handleResult, ThenResult } from './queryMethods/then';
23
- import { WhereQueryBuilder } from './queryMethods/where';
24
- import { OnQueryBuilder } from './queryMethods/join';
25
- import { logParamToLogObject, QueryLogOptions } from './queryMethods/log';
26
30
 
27
31
  export type DbTableOptions = {
28
32
  schema?: string;
@@ -128,26 +132,33 @@ export class Db<
128
132
 
129
133
  const columnsParsers = {} as ColumnsParsers;
130
134
  let hasParsers = false;
135
+ let modifyQuery: ((q: Query) => void)[] | undefined;
131
136
  for (const key in shape) {
132
137
  const column = shape[key];
133
138
  if (column.parseFn) {
134
139
  hasParsers = true;
135
140
  columnsParsers[key] = column.parseFn;
136
141
  }
142
+
143
+ if (column.data.modifyQuery) {
144
+ modifyQuery = pushOrNewArray(modifyQuery, column.data.modifyQuery);
145
+ }
137
146
  }
138
147
  this.columnsParsers = hasParsers ? columnsParsers : undefined;
139
148
 
140
149
  this.toSql = defaultSelect
141
- ? function <T extends Query>(this: T, values?: unknown[]): Sql {
150
+ ? function <T extends Query>(this: T, options?: ToSqlOptions): Sql {
142
151
  const q = this.clone();
143
152
  if (!(q.query as SelectQueryData).select) {
144
153
  (q.query as SelectQueryData).select = defaultSelect as string[];
145
154
  }
146
- return toSql.call(q, values);
155
+ return toSql.call(q, options);
147
156
  }
148
157
  : toSql;
149
158
 
150
159
  this.relations = {} as Relations;
160
+
161
+ modifyQuery?.forEach((cb) => cb(this));
151
162
  }
152
163
  }
153
164
 
@@ -1,5 +1,6 @@
1
1
  import { Query } from './query';
2
2
  import { QueryData } from './sql';
3
+ import { pushOrNewArrayToObject } from './utils';
3
4
 
4
5
  // TODO: remove
5
6
  export const removeFromQuery = <T extends Query>(q: T, key: string): T => {
@@ -26,10 +27,12 @@ export const pushQueryValue = <T extends { query: QueryData }>(
26
27
  key: string,
27
28
  value: unknown,
28
29
  ): T => {
29
- if (!q.query[key as keyof typeof q.query])
30
- (q.query as Record<string, unknown>)[key] = [value];
31
- else (q.query[key as keyof typeof q.query] as unknown[]).push(value);
32
- return q as unknown as T;
30
+ pushOrNewArrayToObject(
31
+ q.query as unknown as Record<string, unknown[]>,
32
+ key,
33
+ value,
34
+ );
35
+ return q;
33
36
  };
34
37
 
35
38
  export const setQueryObjectValue = <T extends { query: QueryData }>(
@@ -351,7 +351,7 @@ describe('aggregate', () => {
351
351
  expectQueryNotMutated(q);
352
352
 
353
353
  q[`_${method}` as `_count`]('name');
354
- expectSql(q.toSql(), expectedSql);
354
+ expectSql(q.toSql({ clearCache: true }), expectedSql);
355
355
  });
356
356
 
357
357
  it('should support raw sql parameter', () => {
@@ -449,7 +449,7 @@ describe('aggregate', () => {
449
449
  expectQueryNotMutated(q);
450
450
 
451
451
  q[`_${method}` as '_jsonObjectAgg']({ alias: 'name' });
452
- expectSql(q.toSql(), expectedSql, ['alias']);
452
+ expectSql(q.toSql({ clearCache: true }), expectedSql, ['alias']);
453
453
  });
454
454
 
455
455
  it('should support raw sql parameter', () => {
@@ -545,7 +545,7 @@ describe('aggregate', () => {
545
545
  expectQueryNotMutated(q);
546
546
 
547
547
  q._stringAgg('name', ' & ');
548
- expectSql(q.toSql(), expectedSql, [' & ']);
548
+ expectSql(q.toSql({ clearCache: true }), expectedSql, [' & ']);
549
549
  });
550
550
 
551
551
  it('should support raw sql parameter', async () => {
@@ -66,4 +66,24 @@ describe('callbacks', () => {
66
66
  expect(fn.mock.calls[0]).toEqual([query, result]);
67
67
  });
68
68
  });
69
+
70
+ describe('beforeDelete', () => {
71
+ it('should run callback before delete', async () => {
72
+ const fn = jest.fn();
73
+ const query = User.beforeDelete(fn).where({ id: 1 }).delete();
74
+ await query;
75
+
76
+ expect(fn.mock.calls[0]).toEqual([query]);
77
+ });
78
+ });
79
+
80
+ describe('afterDelete', () => {
81
+ it('should run callback after delete', async () => {
82
+ const fn = jest.fn();
83
+ const query = User.afterDelete(fn).where({ id: 1 }).delete();
84
+ const result = await query;
85
+
86
+ expect(fn.mock.calls[0]).toEqual([query, result]);
87
+ });
88
+ });
69
89
  });
@@ -1,11 +1,11 @@
1
1
  import { Query } from '../query';
2
2
  import { pushQueryValue } from '../queryDataUtils';
3
3
 
4
- export type BeforeCallback<T extends Query> = (
4
+ export type BeforeCallback<T extends Query = Query> = (
5
5
  query: T,
6
6
  ) => void | Promise<void>;
7
7
 
8
- export type AfterCallback<T extends Query> = (
8
+ export type AfterCallback<T extends Query = Query> = (
9
9
  query: T,
10
10
  data: unknown,
11
11
  ) => void | Promise<void>;
@@ -52,4 +52,18 @@ export class QueryCallbacks {
52
52
  _afterUpdate<T extends Query>(this: T, cb: AfterCallback<T>): T {
53
53
  return pushQueryValue(this, 'afterUpdate', cb);
54
54
  }
55
+
56
+ beforeDelete<T extends Query>(this: T, cb: BeforeCallback<T>): T {
57
+ return this.clone()._beforeDelete(cb);
58
+ }
59
+ _beforeDelete<T extends Query>(this: T, cb: BeforeCallback<T>): T {
60
+ return pushQueryValue(this, 'beforeDelete', cb);
61
+ }
62
+
63
+ afterDelete<T extends Query>(this: T, cb: AfterCallback<T>): T {
64
+ return this.clone()._afterDelete(cb);
65
+ }
66
+ _afterDelete<T extends Query>(this: T, cb: AfterCallback<T>): T {
67
+ return pushQueryValue(this, 'afterDelete', cb);
68
+ }
55
69
  }
@@ -39,7 +39,7 @@ describe('clear', () => {
39
39
 
40
40
  it('should clear increment and decrement', () => {
41
41
  const expectedSql = line(`
42
- UPDATE "user" SET "name" = $1
42
+ UPDATE "user" SET "name" = $1, "updatedAt" = now()
43
43
  `);
44
44
  const expectedValues = ['new name'];
45
45
 
@@ -28,8 +28,8 @@ export class Clear {
28
28
  removeFromQuery(this, 'or');
29
29
  } else if (clear === 'counters') {
30
30
  if ('type' in this.query && this.query.type === 'update') {
31
- this.query.data = this.query.data.filter((item) => {
32
- if (!isRaw(item)) {
31
+ this.query.updateData = this.query.updateData.filter((item) => {
32
+ if (!isRaw(item) && typeof item !== 'function') {
33
33
  let removed = false;
34
34
  for (const key in item) {
35
35
  const value = item[key] as Record<string, unknown>;
@@ -202,7 +202,7 @@ describe('having', () => {
202
202
  expectQueryNotMutated(q);
203
203
 
204
204
  q._having(raw('count(*) = 1'), raw('sum(id) = 2'));
205
- expectSql(q.toSql(), expectedSql);
205
+ expectSql(q.toSql({ clearCache: true }), expectedSql);
206
206
  });
207
207
 
208
208
  describe('havingOr', () => {
@@ -480,7 +480,7 @@ describe('queryMethods', () => {
480
480
  expectQueryNotMutated(q);
481
481
 
482
482
  q._group(raw('id'), raw('name'));
483
- expectSql(q.toSql(), expectedSql);
483
+ expectSql(q.toSql({ clearCache: true }), expectedSql);
484
484
  });
485
485
  });
486
486
 
@@ -25,6 +25,7 @@ import {
25
25
  SortDir,
26
26
  Sql,
27
27
  toSql,
28
+ ToSqlOptions,
28
29
  TruncateQueryData,
29
30
  } from '../sql';
30
31
  import {
@@ -182,8 +183,8 @@ export class QueryMethods {
182
183
  return cloned;
183
184
  }
184
185
 
185
- toSql(this: Query, values?: unknown[]): Sql {
186
- return toSql(this, values);
186
+ toSql(this: Query, options?: ToSqlOptions): Sql {
187
+ return toSql(this, options);
187
188
  }
188
189
 
189
190
  distinct<T extends Query>(this: T, ...columns: Expression<T>[]): T {
@@ -63,22 +63,23 @@ const then = async (
63
63
  let sql: Sql | undefined;
64
64
  let logData: unknown | undefined;
65
65
  try {
66
- let beforeCallbacks: BeforeCallback<Query>[] | undefined;
67
- let afterCallbacks: AfterCallback<Query>[] | undefined;
66
+ let beforeCallbacks: BeforeCallback[] | undefined;
67
+ let afterCallbacks: AfterCallback[] | undefined;
68
68
  if (q.query.type === 'insert') {
69
69
  beforeCallbacks = q.query.beforeInsert;
70
70
  afterCallbacks = q.query.afterInsert;
71
71
  } else if (q.query.type === 'update') {
72
72
  beforeCallbacks = q.query.beforeUpdate;
73
73
  afterCallbacks = q.query.afterUpdate;
74
+ } else if (q.query.type === 'delete') {
75
+ beforeCallbacks = q.query.beforeDelete;
76
+ afterCallbacks = q.query.afterDelete;
74
77
  }
75
78
 
76
- if (beforeCallbacks) {
77
- await Promise.all(beforeCallbacks.map((cb) => cb(q)));
78
- }
79
-
80
- if (q.query.beforeQuery) {
81
- await Promise.all(q.query.beforeQuery.map((cb) => cb(q)));
79
+ if (beforeCallbacks || q.query.beforeQuery) {
80
+ await Promise.all(
81
+ getCallbacks(beforeCallbacks, q.query.beforeQuery).map((cb) => cb(q)),
82
+ );
82
83
  }
83
84
 
84
85
  sql = q.toSql();
@@ -99,14 +100,12 @@ const then = async (
99
100
 
100
101
  const result = await q.query.handleResult(q, queryResult);
101
102
 
102
- if (afterCallbacks?.length || q.query.afterQuery?.length) {
103
- if (q.query.afterQuery?.length) {
104
- await Promise.all(q.query.afterQuery.map((query) => query(q, result)));
105
- }
106
-
107
- if (afterCallbacks?.length) {
108
- await Promise.all(afterCallbacks.map((query) => query(q, result)));
109
- }
103
+ if (afterCallbacks || q.query.afterQuery) {
104
+ await Promise.all(
105
+ getCallbacks(q.query.afterQuery, afterCallbacks).map((query) =>
106
+ query(q, result),
107
+ ),
108
+ );
110
109
  }
111
110
 
112
111
  resolve?.(result);
@@ -224,3 +223,12 @@ const parseValue = (value: unknown, query: Query) => {
224
223
  }
225
224
  return value;
226
225
  };
226
+
227
+ const getCallbacks = <T extends BeforeCallback[] | AfterCallback[]>(
228
+ first?: T,
229
+ second?: T,
230
+ ): T => {
231
+ return (
232
+ first && second ? [...first, ...second] : first ? first : second
233
+ ) as T;
234
+ };
@@ -45,7 +45,7 @@ import { raw } from '../common';
45
45
  );
46
46
  q[`_${what}All` as '_unionAll']([raw('SELECT 2')], true);
47
47
  expectSql(
48
- q.toSql(),
48
+ q.toSql({ clearCache: true }),
49
49
  `
50
50
  SELECT "user"."id" FROM "user"
51
51
  ${upper}