@lobomfz/db 0.3.2 → 0.3.4
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/database.ts +4 -1
- package/src/plugin.ts +250 -28
package/package.json
CHANGED
package/src/database.ts
CHANGED
|
@@ -63,6 +63,7 @@ export class Database<T extends SchemaRecord> {
|
|
|
63
63
|
private sqlite: BunDatabase;
|
|
64
64
|
|
|
65
65
|
private columns: ColumnsMap = new Map();
|
|
66
|
+
private tableColumns = new Map<string, Set<string>>();
|
|
66
67
|
|
|
67
68
|
readonly infer: TablesFromSchemas<T> = undefined as any;
|
|
68
69
|
|
|
@@ -82,7 +83,7 @@ export class Database<T extends SchemaRecord> {
|
|
|
82
83
|
|
|
83
84
|
this.kysely = new Kysely<TablesFromSchemas<T>>({
|
|
84
85
|
dialect: new BunSqliteDialect({ database: this.sqlite }),
|
|
85
|
-
plugins: [new DeserializePlugin(this.columns, validation), new ParseJSONResultsPlugin()],
|
|
86
|
+
plugins: [new DeserializePlugin(this.columns, this.tableColumns, validation), new ParseJSONResultsPlugin()],
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
|
|
@@ -264,6 +265,8 @@ export class Database<T extends SchemaRecord> {
|
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
private registerColumns(tableName: string, props: Prop[]) {
|
|
268
|
+
this.tableColumns.set(tableName, new Set(props.map((p) => p.key)));
|
|
269
|
+
|
|
267
270
|
const colMap = new Map<string, ColumnCoercion>();
|
|
268
271
|
|
|
269
272
|
for (const prop of props) {
|
package/src/plugin.ts
CHANGED
|
@@ -1,28 +1,41 @@
|
|
|
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
|
|
|
24
36
|
constructor(
|
|
25
37
|
private columns: ColumnsMap,
|
|
38
|
+
private tableColumns: Map<string, Set<string>>,
|
|
26
39
|
private validation: Required<JsonValidation>,
|
|
27
40
|
) {}
|
|
28
41
|
|
|
@@ -149,48 +162,231 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
149
162
|
}
|
|
150
163
|
}
|
|
151
164
|
|
|
165
|
+
private coerceSingle(table: string, row: UnknownRow, col: string, coercion: ColumnCoercion) {
|
|
166
|
+
if (coercion === "boolean") {
|
|
167
|
+
if (typeof row[col] === "number") {
|
|
168
|
+
row[col] = row[col] === 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (coercion === "date") {
|
|
175
|
+
if (typeof row[col] === "number") {
|
|
176
|
+
row[col] = new Date(row[col] * 1000);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (typeof row[col] !== "string") {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const value = row[col];
|
|
187
|
+
|
|
188
|
+
let parsed: unknown;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
parsed = JSON.parse(value);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
throw new JsonParseError(table, col, value, e);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this.validation.onRead) {
|
|
197
|
+
this.validateJsonValue(table, col, parsed, coercion.schema);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
row[col] = parsed;
|
|
201
|
+
}
|
|
202
|
+
|
|
152
203
|
private coerceRow(table: string, row: UnknownRow, cols: Map<string, ColumnCoercion>) {
|
|
153
204
|
for (const [col, coercion] of cols) {
|
|
154
205
|
if (!(col in row)) {
|
|
155
206
|
continue;
|
|
156
207
|
}
|
|
157
208
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
209
|
+
this.coerceSingle(table, row, col, coercion);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
162
212
|
|
|
163
|
-
|
|
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);
|
|
164
230
|
}
|
|
165
231
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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>();
|
|
170
280
|
|
|
281
|
+
for (const table of scope.values()) {
|
|
282
|
+
if (resolvedTables.has(table)) {
|
|
171
283
|
continue;
|
|
172
284
|
}
|
|
173
285
|
|
|
174
|
-
|
|
286
|
+
resolvedTables.add(table);
|
|
287
|
+
|
|
288
|
+
const coercion = this.columns.get(table)?.get(column);
|
|
289
|
+
|
|
290
|
+
if (!coercion) {
|
|
175
291
|
continue;
|
|
176
292
|
}
|
|
177
293
|
|
|
178
|
-
|
|
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
|
+
}
|
|
179
312
|
|
|
180
|
-
|
|
313
|
+
if (SelectQueryNode.is(node)) {
|
|
314
|
+
return this.resolveScalarSubqueryCoercion(node);
|
|
315
|
+
}
|
|
181
316
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
317
|
+
if (AggregateFunctionNode.is(node)) {
|
|
318
|
+
if (
|
|
319
|
+
node.aggregated.length !== 1 ||
|
|
320
|
+
!typePreservingAggregateFunctions.has(node.func.toLowerCase())
|
|
321
|
+
) {
|
|
322
|
+
return null;
|
|
186
323
|
}
|
|
187
324
|
|
|
188
|
-
|
|
189
|
-
|
|
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;
|
|
190
380
|
}
|
|
191
381
|
|
|
192
|
-
|
|
382
|
+
const resolved = this.resolveSelectionCoercion(selectionNode.selection, scope);
|
|
383
|
+
|
|
384
|
+
if (resolved) {
|
|
385
|
+
result.set(output, resolved);
|
|
386
|
+
}
|
|
193
387
|
}
|
|
388
|
+
|
|
389
|
+
return result;
|
|
194
390
|
}
|
|
195
391
|
|
|
196
392
|
transformResult: KyselyPlugin["transformResult"] = async (args) => {
|
|
@@ -206,14 +402,40 @@ export class DeserializePlugin implements KyselyPlugin {
|
|
|
206
402
|
return args.result;
|
|
207
403
|
}
|
|
208
404
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return args.result;
|
|
213
|
-
}
|
|
405
|
+
const mainCols = this.columns.get(table);
|
|
406
|
+
const mainTableColumns = this.tableColumns.get(table);
|
|
407
|
+
const selectCoercions = this.getSelectCoercions(node);
|
|
214
408
|
|
|
215
409
|
for (const row of args.result.rows) {
|
|
216
|
-
|
|
410
|
+
if (mainCols) {
|
|
411
|
+
this.coerceRow(table, row, mainCols);
|
|
412
|
+
}
|
|
413
|
+
|
|
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
|
+
|
|
422
|
+
if (mainTableColumns?.has(col)) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for (const [otherTable, otherCols] of this.columns) {
|
|
427
|
+
if (otherTable === table) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const coercion = otherCols.get(col);
|
|
432
|
+
|
|
433
|
+
if (coercion) {
|
|
434
|
+
this.coerceSingle(otherTable, row, col, coercion);
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
217
439
|
}
|
|
218
440
|
|
|
219
441
|
return { ...args.result };
|