pqb 0.4.7 → 0.4.9

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.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "Postgres query builder",
5
5
  "homepage": "https://porm.netlify.app/guide/query-builder.html",
6
6
  "repository": {
@@ -125,4 +125,224 @@ describe('column base', () => {
125
125
  });
126
126
  });
127
127
  });
128
+
129
+ describe('as', () => {
130
+ const numberTimestamp = columnTypes
131
+ .timestamp()
132
+ .encode((input: number) => new Date(input))
133
+ .parse(Date.parse)
134
+ .as(columnTypes.integer());
135
+
136
+ const dateTimestamp = columnTypes
137
+ .timestamp()
138
+ .parse((input) => new Date(input));
139
+
140
+ const db = createDb({
141
+ adapter,
142
+ columnTypes: {
143
+ ...columnTypes,
144
+ numberTimestamp: () => numberTimestamp,
145
+ dateTimestamp: () => dateTimestamp,
146
+ },
147
+ });
148
+
149
+ const UserWithCustomTimestamps = db('user', (t) => ({
150
+ id: t.serial().primaryKey(),
151
+ name: t.text(),
152
+ password: t.text(),
153
+ createdAt: t.numberTimestamp(),
154
+ updatedAt: t.dateTimestamp(),
155
+ }));
156
+
157
+ it('should accept only column of same type and input type', () => {
158
+ columnTypes
159
+ .timestamp()
160
+ .encode((input: number) => input.toString())
161
+ // @ts-expect-error should have both encode and parse with matching types
162
+ .as(columnTypes.integer());
163
+
164
+ // @ts-expect-error should have both encode and parse with matching types
165
+ columnTypes.timestamp().parse(Date.parse).as(columnTypes.integer());
166
+ });
167
+
168
+ it('should return same column with overridden type', () => {
169
+ const timestamp = columnTypes
170
+ .timestamp()
171
+ .encode((input: number) => new Date(input))
172
+ .parse(Date.parse);
173
+
174
+ const column = timestamp.as(columnTypes.integer());
175
+
176
+ expect(column).toBe(timestamp);
177
+ });
178
+
179
+ it('should parse correctly', async () => {
180
+ const id = await User.get('id').create(userData);
181
+
182
+ const user = await UserWithCustomTimestamps.find(id);
183
+
184
+ expect(typeof user.createdAt).toBe('number');
185
+ expect(user.updatedAt).toBeInstanceOf(Date);
186
+ });
187
+
188
+ it('should encode columns when creating', () => {
189
+ const createdAt = Date.now();
190
+ const updatedAt = new Date();
191
+
192
+ const query = UserWithCustomTimestamps.create({
193
+ ...userData,
194
+ createdAt,
195
+ updatedAt,
196
+ });
197
+
198
+ expectSql(
199
+ query.toSql(),
200
+ `
201
+ INSERT INTO "user"("name", "password", "createdAt", "updatedAt")
202
+ VALUES ($1, $2, $3, $4)
203
+ RETURNING *
204
+ `,
205
+ [userData.name, userData.password, new Date(createdAt), updatedAt],
206
+ );
207
+ });
208
+
209
+ it('should encode columns when update', async () => {
210
+ const id = await User.get('id').create(userData);
211
+ const createdAt = Date.now();
212
+ const updatedAt = new Date();
213
+
214
+ const query = UserWithCustomTimestamps.find(id).update({
215
+ createdAt,
216
+ updatedAt,
217
+ });
218
+
219
+ expectSql(
220
+ query.toSql(),
221
+ `
222
+ UPDATE "user"
223
+ SET "createdAt" = $1, "updatedAt" = $2
224
+ WHERE "user"."id" = $3
225
+ `,
226
+ [new Date(createdAt), updatedAt, id],
227
+ );
228
+ });
229
+ });
230
+
231
+ describe('timestamp().asNumber()', () => {
232
+ it('should parse and encode timestamp as a number', async () => {
233
+ const UserWithNumberTimestamp = db('user', (t) => ({
234
+ id: t.serial().primaryKey(),
235
+ name: t.text(),
236
+ password: t.text(),
237
+ createdAt: t.timestamp().asNumber(),
238
+ updatedAt: t.timestamp().asNumber(),
239
+ }));
240
+
241
+ const now = Date.now();
242
+
243
+ const createQuery = UserWithNumberTimestamp.create({
244
+ ...userData,
245
+ createdAt: now,
246
+ updatedAt: now,
247
+ });
248
+
249
+ expectSql(
250
+ createQuery.toSql(),
251
+ `
252
+ INSERT INTO "user"("name", "password", "createdAt", "updatedAt")
253
+ VALUES ($1, $2, $3, $4)
254
+ RETURNING *
255
+ `,
256
+ [userData.name, userData.password, new Date(now), new Date(now)],
257
+ );
258
+
259
+ const { id } = await createQuery;
260
+ const user = await UserWithNumberTimestamp.select(
261
+ 'createdAt',
262
+ 'updatedAt',
263
+ ).find(id);
264
+
265
+ assertType<typeof user, { createdAt: number; updatedAt: number }>();
266
+
267
+ expect(typeof user.createdAt).toBe('number');
268
+ expect(typeof user.updatedAt).toBe('number');
269
+
270
+ const updateQuery = UserWithNumberTimestamp.find(id).update({
271
+ createdAt: now,
272
+ updatedAt: now,
273
+ });
274
+
275
+ expectSql(
276
+ updateQuery.toSql(),
277
+ `
278
+ UPDATE "user"
279
+ SET "createdAt" = $1, "updatedAt" = $2
280
+ WHERE "user"."id" = $3
281
+ `,
282
+ [new Date(now), new Date(now), id],
283
+ );
284
+ });
285
+ });
286
+
287
+ describe('timestamp().asDate()', () => {
288
+ it('should parse and encode timestamp as a number', async () => {
289
+ columnTypes
290
+ .text()
291
+ .encode((input: number) => input)
292
+ .parse((text) => parseInt(text))
293
+ .as(columnTypes.integer());
294
+
295
+ const UserWithNumberTimestamp = db('user', (t) => ({
296
+ id: t.serial().primaryKey(),
297
+ name: t.text(),
298
+ password: t.text(),
299
+ createdAt: columnTypes.timestamp().asDate(),
300
+ updatedAt: columnTypes.timestamp().asDate(),
301
+ }));
302
+
303
+ const now = new Date();
304
+
305
+ const createQuery = UserWithNumberTimestamp.create({
306
+ ...userData,
307
+ createdAt: now,
308
+ updatedAt: now,
309
+ });
310
+
311
+ expectSql(
312
+ createQuery.toSql(),
313
+ `
314
+ INSERT INTO "user"("name", "password", "createdAt", "updatedAt")
315
+ VALUES ($1, $2, $3, $4)
316
+ RETURNING *
317
+ `,
318
+ [userData.name, userData.password, new Date(now), new Date(now)],
319
+ );
320
+
321
+ const { id } = await createQuery;
322
+ const user = await UserWithNumberTimestamp.select(
323
+ 'createdAt',
324
+ 'updatedAt',
325
+ ).find(id);
326
+
327
+ assertType<typeof user, { createdAt: Date; updatedAt: Date }>();
328
+
329
+ expect(user.createdAt).toBeInstanceOf(Date);
330
+ expect(user.updatedAt).toBeInstanceOf(Date);
331
+
332
+ const updateQuery = UserWithNumberTimestamp.find(id).update({
333
+ createdAt: now,
334
+ updatedAt: now,
335
+ });
336
+
337
+ expectSql(
338
+ updateQuery.toSql(),
339
+ `
340
+ UPDATE "user"
341
+ SET "createdAt" = $1, "updatedAt" = $2
342
+ WHERE "user"."id" = $3
343
+ `,
344
+ [now, now, id],
345
+ );
346
+ });
347
+ });
128
348
  });
@@ -46,6 +46,7 @@ export type ColumnData = {
46
46
  compression?: string;
47
47
  foreignKey?: ForeignKey<string, string[]>;
48
48
  modifyQuery?: (q: Query) => void;
49
+ as?: ColumnType;
49
50
  };
50
51
 
51
52
  type ForeignKeyMatch = 'FULL' | 'PARTIAL' | 'SIMPLE';
@@ -221,6 +222,13 @@ export abstract class ColumnType<
221
222
  return this as unknown as Omit<T, 'type'> & { type: Output };
222
223
  }
223
224
 
225
+ as<
226
+ T extends ColumnType,
227
+ C extends ColumnType<T['type'], Operators, T['inputType']>,
228
+ >(this: T, column: C): C {
229
+ return addColumnData(this, 'as', column) as unknown as C;
230
+ }
231
+
224
232
  toSQL() {
225
233
  return this.dataType;
226
234
  }
@@ -3,6 +3,7 @@ import { Operators } from '../columnsOperators';
3
3
  import { joinTruthy } from '../utils';
4
4
  import { dateTypeMethods } from './commonMethods';
5
5
  import { assignMethodsToClass } from './utils';
6
+ import { IntegerColumn } from './number';
6
7
 
7
8
  export type DateColumnData = ColumnData & {
8
9
  min?: Date;
@@ -22,6 +23,16 @@ export abstract class DateBaseColumn extends ColumnType<
22
23
  > {
23
24
  data = {} as DateColumnData;
24
25
  operators = Operators.date;
26
+
27
+ asNumber() {
28
+ return this.encode((input: number) => new Date(input)).parse(
29
+ Date.parse,
30
+ ) as unknown as IntegerColumn;
31
+ }
32
+
33
+ asDate<T extends ColumnType>(this: T) {
34
+ return this.parse((input) => new Date(input as string));
35
+ }
25
36
  }
26
37
 
27
38
  assignMethodsToClass(DateBaseColumn, dateTypeMethods);
package/src/db.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  ColumnTypesBase,
23
23
  getColumnTypes,
24
24
  SinglePrimaryKey,
25
+ ColumnType,
25
26
  } from './columnSchema';
26
27
  import { applyMixins, pushOrNewArray } from './utils';
27
28
  import { StringKey } from './common';
@@ -93,6 +94,8 @@ export interface Db<
93
94
  >;
94
95
  }
95
96
 
97
+ export const anyShape = {} as Record<string, ColumnType>;
98
+
96
99
  export class Db<
97
100
  Table extends string | undefined = undefined,
98
101
  Shape extends ColumnsShape = Record<string, never>,
@@ -107,7 +110,7 @@ export class Db<
107
110
  public adapter: Adapter,
108
111
  public queryBuilder: Db,
109
112
  public table: Table = undefined as Table,
110
- public shape: Shape = {} as Shape,
113
+ public shape: Shape = anyShape as Shape,
111
114
  public columnTypes: CT,
112
115
  options: DbTableOptions,
113
116
  ) {
@@ -150,6 +150,24 @@ describe('create functions', () => {
150
150
  ['name', 'override'],
151
151
  );
152
152
  });
153
+
154
+ it('should strip unknown keys', () => {
155
+ const query = User.create({
156
+ name: 'name',
157
+ password: 'password',
158
+ unknown: 'should be stripped',
159
+ } as unknown as UserRecord);
160
+
161
+ expectSql(
162
+ query.toSql(),
163
+ `
164
+ INSERT INTO "user"("name", "password")
165
+ VALUES ($1, $2)
166
+ RETURNING *
167
+ `,
168
+ ['name', 'password'],
169
+ );
170
+ });
153
171
  });
154
172
 
155
173
  describe('createMany', () => {
@@ -265,6 +283,31 @@ describe('create functions', () => {
265
283
 
266
284
  expectQueryNotMutated(q);
267
285
  });
286
+
287
+ it('should strip unknown keys', () => {
288
+ const query = User.createMany([
289
+ {
290
+ name: 'name',
291
+ password: 'password',
292
+ unknown: 'should be stripped',
293
+ },
294
+ {
295
+ name: 'name',
296
+ password: 'password',
297
+ unknown: 'should be stripped',
298
+ },
299
+ ] as unknown as UserRecord[]);
300
+
301
+ expectSql(
302
+ query.toSql(),
303
+ `
304
+ INSERT INTO "user"("name", "password")
305
+ VALUES ($1, $2), ($3, $4)
306
+ RETURNING *
307
+ `,
308
+ ['name', 'password', 'name', 'password'],
309
+ );
310
+ });
268
311
  });
269
312
 
270
313
  describe('createFrom', () => {
@@ -26,6 +26,8 @@ import { WhereArg } from './where';
26
26
  import { parseResult, queryMethodByReturnType } from './then';
27
27
  import { NotFoundError } from '../errors';
28
28
  import { RawExpression } from '../common';
29
+ import { ColumnsShape } from '../columnSchema';
30
+ import { anyShape } from '../db';
29
31
 
30
32
  export type CreateData<
31
33
  T extends Query,
@@ -172,6 +174,8 @@ type CreateCtx = {
172
174
  relations: Record<string, Relation>;
173
175
  };
174
176
 
177
+ type Encoder = (input: unknown) => unknown;
178
+
175
179
  const handleSelect = (q: Query) => {
176
180
  const select = q.query.select?.[0];
177
181
  const isCount =
@@ -191,7 +195,9 @@ const processCreateItem = (
191
195
  rowIndex: number,
192
196
  ctx: CreateCtx,
193
197
  columns: string[],
198
+ encoders: Record<string, Encoder>,
194
199
  columnsMap: Record<string, number>,
200
+ shape: ColumnsShape,
195
201
  ) => {
196
202
  Object.keys(item).forEach((key) => {
197
203
  if (ctx.relations[key]) {
@@ -222,8 +228,12 @@ const processCreateItem = (
222
228
  item[key] as NestedInsertItem,
223
229
  ]);
224
230
  }
225
- } else if (columnsMap[key] === undefined) {
231
+ } else if (
232
+ columnsMap[key] === undefined &&
233
+ (shape[key] || shape === anyShape)
234
+ ) {
226
235
  columnsMap[key] = columns.length;
236
+ encoders[key] = shape[key]?.encodeFn as Encoder;
227
237
  columns.push(key);
228
238
  }
229
239
  });
@@ -256,8 +266,19 @@ const getManyReturnType = (q: Query) => {
256
266
  }
257
267
  };
258
268
 
269
+ const mapColumnValues = (
270
+ columns: string[],
271
+ encoders: Record<string, Encoder>,
272
+ data: Record<string, unknown>,
273
+ ) => {
274
+ return columns.map((key) =>
275
+ encoders[key] ? encoders[key](data[key]) : data[key],
276
+ );
277
+ };
278
+
259
279
  const handleOneData = (q: Query, data: CreateData<Query>, ctx: CreateCtx) => {
260
280
  const columns: string[] = [];
281
+ const encoders: Record<string, Encoder> = {};
261
282
  const columnsMap: Record<string, number> = {};
262
283
  const defaults = q.query.defaults;
263
284
 
@@ -265,9 +286,9 @@ const handleOneData = (q: Query, data: CreateData<Query>, ctx: CreateCtx) => {
265
286
  data = { ...defaults, ...data };
266
287
  }
267
288
 
268
- processCreateItem(data, 0, ctx, columns, columnsMap);
289
+ processCreateItem(data, 0, ctx, columns, encoders, columnsMap, q.shape);
269
290
 
270
- const values = [columns.map((key) => (data as Record<string, unknown>)[key])];
291
+ const values = [mapColumnValues(columns, encoders, data)];
271
292
 
272
293
  return { columns, values };
273
294
  };
@@ -278,6 +299,7 @@ const handleManyData = (
278
299
  ctx: CreateCtx,
279
300
  ) => {
280
301
  const columns: string[] = [];
302
+ const encoders: Record<string, Encoder> = {};
281
303
  const columnsMap: Record<string, number> = {};
282
304
  const defaults = q.query.defaults;
283
305
 
@@ -286,13 +308,13 @@ const handleManyData = (
286
308
  }
287
309
 
288
310
  data.forEach((item, i) => {
289
- processCreateItem(item, i, ctx, columns, columnsMap);
311
+ processCreateItem(item, i, ctx, columns, encoders, columnsMap, q.shape);
290
312
  });
291
313
 
292
314
  const values = Array(data.length);
293
315
 
294
316
  data.forEach((item, i) => {
295
- (values as unknown[][])[i] = columns.map((key) => item[key]);
317
+ (values as unknown[][])[i] = mapColumnValues(columns, encoders, item);
296
318
  });
297
319
 
298
320
  return { columns, values };
@@ -5,6 +5,7 @@ import {
5
5
  expectSql,
6
6
  User,
7
7
  userData,
8
+ UserRecord,
8
9
  useTestDatabase,
9
10
  } from '../test-utils';
10
11
 
@@ -321,6 +322,23 @@ describe('update', () => {
321
322
  });
322
323
  });
323
324
 
325
+ it('should strip unknown keys', () => {
326
+ const query = User.find(1).update({
327
+ name: 'name',
328
+ unknown: 'should be stripped',
329
+ } as unknown as UserRecord);
330
+
331
+ expectSql(
332
+ query.toSql(),
333
+ `
334
+ UPDATE "user"
335
+ SET "name" = $1, "updatedAt" = now()
336
+ WHERE "user"."id" = $2
337
+ `,
338
+ ['name', 1],
339
+ );
340
+ });
341
+
324
342
  describe('increment', () => {
325
343
  it('should not mutate query', () => {
326
344
  const q = User.all();
@@ -16,6 +16,8 @@ import { EmptyObject, MaybeArray } from '../utils';
16
16
  import { CreateData } from './create';
17
17
  import { parseResult, queryMethodByReturnType } from './then';
18
18
  import { UpdateQueryData } from '../sql';
19
+ import { ColumnsShape } from '../columnSchema';
20
+ import { anyShape } from '../db';
19
21
 
20
22
  export type UpdateData<T extends Query> = {
21
23
  [K in keyof T['type']]?: T['type'][K] | RawExpression;
@@ -174,7 +176,10 @@ export class Update {
174
176
  const set: Record<string, unknown> = { ...data };
175
177
  pushQueryValue(this, 'updateData', set);
176
178
 
177
- const relations = this.relations as Record<string, Relation>;
179
+ const { relations, shape } = this as {
180
+ relations: Record<string, Relation>;
181
+ shape: ColumnsShape;
182
+ };
178
183
 
179
184
  const prependRelations: Record<string, Record<string, unknown>> = {};
180
185
  const appendRelations: Record<string, Record<string, unknown>> = {};
@@ -195,6 +200,11 @@ export class Update {
195
200
  }
196
201
  appendRelations[key] = data[key] as Record<string, unknown>;
197
202
  }
203
+ } else if (!shape[key] && shape !== anyShape) {
204
+ delete set[key];
205
+ } else {
206
+ const encode = shape[key].encodeFn;
207
+ if (encode) set[key] = encode(set[key]);
198
208
  }
199
209
  }
200
210