@lobomfz/db 0.3.3 → 0.3.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/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/plugin.ts +202 -3
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { Database } from "./database";
|
|
2
2
|
export { generated, type GeneratedPreset } from "./generated";
|
|
3
3
|
export { JsonParseError } from "./errors";
|
|
4
|
-
export { sql, type Selectable, type Insertable, type Updateable } from "kysely";
|
|
4
|
+
export { sql, type Selectable, type Insertable, type Updateable, type Kysely } from "kysely";
|
|
5
5
|
export { type } from "arktype";
|
|
6
6
|
export { JsonValidationError } from "./validation-error";
|
|
7
7
|
export type { DbFieldMeta } from "./env";
|
package/src/plugin.ts
CHANGED
|
@@ -1,23 +1,35 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
import type { Type } from "arktype";
|
|
1
3
|
import {
|
|
2
4
|
type KyselyPlugin,
|
|
5
|
+
type OperationNode,
|
|
3
6
|
type RootOperationNode,
|
|
4
7
|
type UnknownRow,
|
|
5
8
|
type QueryId,
|
|
9
|
+
AggregateFunctionNode,
|
|
6
10
|
TableNode,
|
|
7
11
|
AliasNode,
|
|
8
12
|
ValuesNode,
|
|
9
13
|
ValueNode,
|
|
10
14
|
ColumnNode,
|
|
15
|
+
IdentifierNode,
|
|
16
|
+
ReferenceNode,
|
|
17
|
+
ParensNode,
|
|
18
|
+
CastNode,
|
|
19
|
+
SelectQueryNode,
|
|
11
20
|
} from "kysely";
|
|
12
|
-
|
|
13
|
-
import type { Type } from "arktype";
|
|
21
|
+
|
|
14
22
|
import { JsonParseError } from "./errors";
|
|
15
|
-
import { JsonValidationError } from "./validation-error";
|
|
16
23
|
import type { JsonValidation } from "./types";
|
|
24
|
+
import { JsonValidationError } from "./validation-error";
|
|
17
25
|
|
|
18
26
|
export type ColumnCoercion = "boolean" | "date" | { type: "json"; schema: Type };
|
|
19
27
|
export type ColumnsMap = Map<string, Map<string, ColumnCoercion>>;
|
|
20
28
|
|
|
29
|
+
type ResolvedCoercion = { table: string; coercion: ColumnCoercion };
|
|
30
|
+
|
|
31
|
+
const typePreservingAggregateFunctions = new Set(["max", "min"]);
|
|
32
|
+
|
|
21
33
|
export class DeserializePlugin implements KyselyPlugin {
|
|
22
34
|
private queryNodes = new WeakMap<QueryId, RootOperationNode>();
|
|
23
35
|
|
|
@@ -198,6 +210,185 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
198
210
|
}
|
|
199
211
|
}
|
|
200
212
|
|
|
213
|
+
private getIdentifierName(node: OperationNode | undefined) {
|
|
214
|
+
if (!node || !IdentifierNode.is(node)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return node.name;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private addTableScopeEntry(scope: Map<string, string>, node: OperationNode) {
|
|
222
|
+
if (AliasNode.is(node) && TableNode.is(node.node)) {
|
|
223
|
+
const alias = this.getIdentifierName(node.alias);
|
|
224
|
+
const table = node.node.table.identifier.name;
|
|
225
|
+
|
|
226
|
+
scope.set(table, table);
|
|
227
|
+
|
|
228
|
+
if (alias) {
|
|
229
|
+
scope.set(alias, table);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (TableNode.is(node)) {
|
|
236
|
+
const table = node.table.identifier.name;
|
|
237
|
+
scope.set(table, table);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private getTableScope(node: SelectQueryNode) {
|
|
242
|
+
const scope = new Map<string, string>();
|
|
243
|
+
|
|
244
|
+
for (const fromNode of node.from?.froms ?? []) {
|
|
245
|
+
this.addTableScopeEntry(scope, fromNode);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const join of node.joins ?? []) {
|
|
249
|
+
this.addTableScopeEntry(scope, join.table);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return scope;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private resolveReferenceCoercion(node: ReferenceNode | ColumnNode, scope: Map<string, string>) {
|
|
256
|
+
const column = ColumnNode.is(node)
|
|
257
|
+
? node.column.name
|
|
258
|
+
: ColumnNode.is(node.column)
|
|
259
|
+
? node.column.column.name
|
|
260
|
+
: null;
|
|
261
|
+
|
|
262
|
+
if (!column) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (ReferenceNode.is(node) && node.table) {
|
|
267
|
+
const tableRef = node.table.table.identifier.name;
|
|
268
|
+
const table = scope.get(tableRef) ?? tableRef;
|
|
269
|
+
const coercion = this.columns.get(table)?.get(column);
|
|
270
|
+
|
|
271
|
+
if (!coercion) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { table, coercion } satisfies ResolvedCoercion;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let match: ResolvedCoercion | null = null;
|
|
279
|
+
const resolvedTables = new Set<string>();
|
|
280
|
+
|
|
281
|
+
for (const table of scope.values()) {
|
|
282
|
+
if (resolvedTables.has(table)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
resolvedTables.add(table);
|
|
287
|
+
|
|
288
|
+
const coercion = this.columns.get(table)?.get(column);
|
|
289
|
+
|
|
290
|
+
if (!coercion) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (match) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
match = { table, coercion };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return match;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private resolveSelectionCoercion(node: OperationNode, scope: Map<string, string>): ResolvedCoercion | null {
|
|
305
|
+
if (AliasNode.is(node)) {
|
|
306
|
+
return this.resolveSelectionCoercion(node.node, scope);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (ReferenceNode.is(node) || ColumnNode.is(node)) {
|
|
310
|
+
return this.resolveReferenceCoercion(node, scope);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (SelectQueryNode.is(node)) {
|
|
314
|
+
return this.resolveScalarSubqueryCoercion(node);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (AggregateFunctionNode.is(node)) {
|
|
318
|
+
if (
|
|
319
|
+
node.aggregated.length !== 1 ||
|
|
320
|
+
!typePreservingAggregateFunctions.has(node.func.toLowerCase())
|
|
321
|
+
) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return this.resolveSelectionCoercion(node.aggregated[0]!, scope);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (ParensNode.is(node)) {
|
|
329
|
+
return this.resolveSelectionCoercion(node.node, scope);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (CastNode.is(node)) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private resolveScalarSubqueryCoercion(node: SelectQueryNode) {
|
|
340
|
+
if (!node.selections || node.selections.length !== 1) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return this.resolveSelectionCoercion(
|
|
345
|
+
node.selections[0]!.selection,
|
|
346
|
+
this.getTableScope(node),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private getSelectionOutputName(node: OperationNode) {
|
|
351
|
+
if (AliasNode.is(node)) {
|
|
352
|
+
return this.getIdentifierName(node.alias);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (ReferenceNode.is(node) && ColumnNode.is(node.column)) {
|
|
356
|
+
return node.column.column.name;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (ColumnNode.is(node)) {
|
|
360
|
+
return node.column.name;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private getSelectCoercions(node: RootOperationNode) {
|
|
367
|
+
const result = new Map<string, ResolvedCoercion>();
|
|
368
|
+
|
|
369
|
+
if (node.kind !== "SelectQueryNode" || !node.selections) {
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const scope = this.getTableScope(node);
|
|
374
|
+
|
|
375
|
+
for (const selectionNode of node.selections) {
|
|
376
|
+
const output = this.getSelectionOutputName(selectionNode.selection);
|
|
377
|
+
|
|
378
|
+
if (!output) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const resolved = this.resolveSelectionCoercion(selectionNode.selection, scope);
|
|
383
|
+
|
|
384
|
+
if (resolved) {
|
|
385
|
+
result.set(output, resolved);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
|
|
201
392
|
transformResult: KyselyPlugin["transformResult"] = async (args) => {
|
|
202
393
|
const node = this.queryNodes.get(args.queryId);
|
|
203
394
|
|
|
@@ -213,6 +404,7 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
213
404
|
|
|
214
405
|
const mainCols = this.columns.get(table);
|
|
215
406
|
const mainTableColumns = this.tableColumns.get(table);
|
|
407
|
+
const selectCoercions = this.getSelectCoercions(node);
|
|
216
408
|
|
|
217
409
|
for (const row of args.result.rows) {
|
|
218
410
|
if (mainCols) {
|
|
@@ -220,6 +412,13 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
220
412
|
}
|
|
221
413
|
|
|
222
414
|
for (const col of Object.keys(row)) {
|
|
415
|
+
const resolved = selectCoercions.get(col);
|
|
416
|
+
|
|
417
|
+
if (resolved) {
|
|
418
|
+
this.coerceSingle(resolved.table, row, col, resolved.coercion);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
223
422
|
if (mainTableColumns?.has(col)) {
|
|
224
423
|
continue;
|
|
225
424
|
}
|