@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobomfz/db",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Bun SQLite database with Arktype schemas and typed Kysely client",
5
5
  "keywords": [
6
6
  "arktype",
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
- import { type } from "arktype";
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
  }