@lobomfz/db 0.3.9 → 0.4.0
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/README.md +3 -3
- package/package.json +1 -1
- package/src/database.ts +18 -30
- package/src/generated.ts +1 -1
- package/src/index.ts +1 -1
- package/src/plugin.ts +64 -7
- package/src/types.ts +17 -1
- package/src/validation-error.ts +8 -4
- package/src/write-validation-plugin.ts +327 -52
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ SQLite database with Arktype schemas and typed Kysely client for Bun.
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
bun add @lobomfz/db
|
|
8
|
+
bun add @lobomfz/db
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
@@ -65,12 +65,12 @@ type("string").configure({ unique: true }); // UNIQUE
|
|
|
65
65
|
type("number.integer").configure({ references: "users.id", onDelete: "cascade" }); // FK
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
JSON columns are validated against the schema on write by default. To also validate on read
|
|
68
|
+
JSON columns are validated against the schema on write by default. To also validate on read:
|
|
69
69
|
|
|
70
70
|
```typescript
|
|
71
71
|
new Database({
|
|
72
72
|
// ...
|
|
73
|
-
validation: { onRead: true }, // default: { onRead: false
|
|
73
|
+
validation: { onRead: true }, // default: { onRead: false }
|
|
74
74
|
});
|
|
75
75
|
```
|
|
76
76
|
|
package/package.json
CHANGED
package/src/database.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { GeneratedPreset } from "./generated.js";
|
|
|
9
9
|
import { Differ, type DesiredTable } from "./migration/diff.js";
|
|
10
10
|
import { Executor } from "./migration/execute.js";
|
|
11
11
|
import { Introspector } from "./migration/introspect.js";
|
|
12
|
-
import { ResultHydrationPlugin
|
|
12
|
+
import { ResultHydrationPlugin } from "./plugin.js";
|
|
13
13
|
import type {
|
|
14
14
|
DatabaseOptions,
|
|
15
15
|
IndexDefinition,
|
|
@@ -67,14 +67,19 @@ const defaultPragmas: DatabasePragmas = {
|
|
|
67
67
|
export class Database<T extends SchemaRecord> {
|
|
68
68
|
private sqlite: BunDatabase;
|
|
69
69
|
|
|
70
|
-
private columns: ColumnsMap = new Map();
|
|
71
|
-
private tableColumns = new Map<string, Set<string>>();
|
|
72
|
-
|
|
73
70
|
readonly infer: TablesFromSchemas<T> = undefined as any;
|
|
74
71
|
|
|
75
72
|
readonly kysely: Kysely<TablesFromSchemas<T>>;
|
|
76
73
|
|
|
77
74
|
constructor(private options: DatabaseOptions<T>) {
|
|
75
|
+
const tableSchemas = new Map<string, Type>(Object.entries(options.schema.tables));
|
|
76
|
+
const writeSchemas = new Map<string, Type>(
|
|
77
|
+
Object.entries(options.schema.tables).map(([table, schema]) => [
|
|
78
|
+
table,
|
|
79
|
+
this.createWriteSchema(schema),
|
|
80
|
+
]),
|
|
81
|
+
);
|
|
82
|
+
|
|
78
83
|
this.sqlite = new BunDatabase(options.path);
|
|
79
84
|
|
|
80
85
|
this.applyPragmas();
|
|
@@ -83,14 +88,13 @@ export class Database<T extends SchemaRecord> {
|
|
|
83
88
|
|
|
84
89
|
const validation = {
|
|
85
90
|
onRead: options.validation?.onRead ?? false,
|
|
86
|
-
onWrite: options.validation?.onWrite ?? true,
|
|
87
91
|
};
|
|
88
92
|
|
|
89
93
|
this.kysely = new Kysely<TablesFromSchemas<T>>({
|
|
90
94
|
dialect: new BunSqliteDialect({ database: this.sqlite }),
|
|
91
95
|
plugins: [
|
|
92
|
-
new WriteValidationPlugin(
|
|
93
|
-
new ResultHydrationPlugin(
|
|
96
|
+
new WriteValidationPlugin(writeSchemas),
|
|
97
|
+
new ResultHydrationPlugin(tableSchemas, validation),
|
|
94
98
|
],
|
|
95
99
|
});
|
|
96
100
|
}
|
|
@@ -293,30 +297,16 @@ export class Database<T extends SchemaRecord> {
|
|
|
293
297
|
return structureProps.map((p) => this.normalizeProp(p, schema));
|
|
294
298
|
}
|
|
295
299
|
|
|
296
|
-
private
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
for (const prop of props) {
|
|
302
|
-
if (prop.isBoolean) {
|
|
303
|
-
colMap.set(prop.key, "boolean");
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (prop.isDate) {
|
|
308
|
-
colMap.set(prop.key, "date");
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
300
|
+
private createWriteSchema(schema: Type) {
|
|
301
|
+
const autoIncrementColumns = this.parseSchemaProps(schema)
|
|
302
|
+
.filter((prop) => prop.generated === "autoincrement")
|
|
303
|
+
.map((prop) => prop.key);
|
|
311
304
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
305
|
+
if (autoIncrementColumns.length === 0) {
|
|
306
|
+
return schema;
|
|
315
307
|
}
|
|
316
308
|
|
|
317
|
-
|
|
318
|
-
this.columns.set(tableName, colMap);
|
|
319
|
-
}
|
|
309
|
+
return (schema as any).omit(...autoIncrementColumns) as Type;
|
|
320
310
|
}
|
|
321
311
|
|
|
322
312
|
private generateCreateTableSQL(tableName: string, props: Prop[]) {
|
|
@@ -343,8 +333,6 @@ export class Database<T extends SchemaRecord> {
|
|
|
343
333
|
for (const [name, schema] of Object.entries(this.options.schema.tables)) {
|
|
344
334
|
const props = this.parseSchemaProps(schema);
|
|
345
335
|
|
|
346
|
-
this.registerColumns(name, props);
|
|
347
|
-
|
|
348
336
|
const columns = props.map((prop) => {
|
|
349
337
|
const isNotNull = this.columnConstraint(prop) === "NOT NULL";
|
|
350
338
|
const defaultClause = this.defaultClause(prop);
|
package/src/generated.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ export {
|
|
|
12
12
|
} from "kysely";
|
|
13
13
|
export { type, type Type } from "arktype";
|
|
14
14
|
export { configure } from "arktype/config";
|
|
15
|
-
export {
|
|
15
|
+
export { ValidationError } from "./validation-error.js";
|
|
16
16
|
export type { DbFieldMeta } from "./env.js";
|
|
17
17
|
export type {
|
|
18
18
|
DatabaseOptions,
|
package/src/plugin.ts
CHANGED
|
@@ -18,10 +18,10 @@ import {
|
|
|
18
18
|
} from "kysely";
|
|
19
19
|
|
|
20
20
|
import { JsonParseError } from "./errors.js";
|
|
21
|
-
import {
|
|
21
|
+
import type { StructureProp } from "./types.js";
|
|
22
|
+
import { ValidationError } from "./validation-error.js";
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
export type ColumnsMap = Map<string, Map<string, ColumnCoercion>>;
|
|
24
|
+
type ColumnCoercion = "boolean" | "date" | { type: "json"; schema: Type };
|
|
25
25
|
|
|
26
26
|
type ResolvedCoercion = { table: string; coercion: ColumnCoercion };
|
|
27
27
|
|
|
@@ -79,13 +79,70 @@ const jsonObjectFromFragments = [
|
|
|
79
79
|
const typePreservingAggregateFunctions = new Set(["max", "min"]);
|
|
80
80
|
|
|
81
81
|
export class ResultHydrationPlugin implements KyselyPlugin {
|
|
82
|
+
private columns = new Map<string, Map<string, ColumnCoercion>>();
|
|
83
|
+
private tableColumns = new Map<string, Set<string>>();
|
|
82
84
|
private queryPlans = new WeakMap<QueryId, QueryPlan>();
|
|
83
85
|
|
|
84
86
|
constructor(
|
|
85
|
-
|
|
86
|
-
private tableColumns: Map<string, Set<string>>,
|
|
87
|
+
schemas: Map<string, Type>,
|
|
87
88
|
private validation: { onRead: boolean },
|
|
88
|
-
) {
|
|
89
|
+
) {
|
|
90
|
+
this.registerSchemas(schemas);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private registerSchemas(schemas: Map<string, Type>) {
|
|
94
|
+
for (const [table, schema] of schemas) {
|
|
95
|
+
const structureProps = (schema as any).structure?.props as StructureProp[] | undefined;
|
|
96
|
+
|
|
97
|
+
if (!structureProps) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.tableColumns.set(
|
|
102
|
+
table,
|
|
103
|
+
new Set(structureProps.map((prop) => prop.key)),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const columns = new Map<string, ColumnCoercion>();
|
|
107
|
+
|
|
108
|
+
for (const prop of structureProps) {
|
|
109
|
+
const coercion = this.getColumnCoercion(prop, schema);
|
|
110
|
+
|
|
111
|
+
if (!coercion) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
columns.set(prop.key, coercion);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (columns.size > 0) {
|
|
119
|
+
this.columns.set(table, columns);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private getColumnCoercion(prop: StructureProp, parentSchema: Type) {
|
|
125
|
+
const concrete = prop.value.branches.filter(
|
|
126
|
+
(branch) => branch.unit !== null && branch.domain !== "undefined",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (prop.value.proto === Date || concrete.some((branch) => branch.proto === Date)) {
|
|
130
|
+
return "date" satisfies ColumnCoercion;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (concrete.length > 0 && concrete.every((branch) => branch.domain === "boolean")) {
|
|
134
|
+
return "boolean" satisfies ColumnCoercion;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (concrete.some((branch) => !!branch.structure)) {
|
|
138
|
+
return {
|
|
139
|
+
type: "json",
|
|
140
|
+
schema: (parentSchema as any).get(prop.key) as Type,
|
|
141
|
+
} satisfies ColumnCoercion;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
89
146
|
|
|
90
147
|
transformQuery: KyselyPlugin["transformQuery"] = (args) => {
|
|
91
148
|
this.queryPlans.set(args.queryId, {
|
|
@@ -141,7 +198,7 @@ export class ResultHydrationPlugin implements KyselyPlugin {
|
|
|
141
198
|
const result = schema(value);
|
|
142
199
|
|
|
143
200
|
if (result instanceof type.errors) {
|
|
144
|
-
throw new
|
|
201
|
+
throw new ValidationError(table, result.summary, col);
|
|
145
202
|
}
|
|
146
203
|
}
|
|
147
204
|
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import type { Generated } from "kysely";
|
|
2
2
|
import type { Type } from "arktype";
|
|
3
3
|
|
|
4
|
+
export type ArkBranch = {
|
|
5
|
+
domain?: string;
|
|
6
|
+
proto?: unknown;
|
|
7
|
+
unit?: unknown;
|
|
8
|
+
structure?: unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type StructureProp = {
|
|
12
|
+
key: string;
|
|
13
|
+
required: boolean;
|
|
14
|
+
inner: { default?: unknown };
|
|
15
|
+
value: Type & {
|
|
16
|
+
branches: ArkBranch[];
|
|
17
|
+
proto?: unknown;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
4
21
|
type ExtractInput<T> = T extends { inferIn: infer I } ? I : never;
|
|
5
22
|
type ExtractOutput<T> = T extends { infer: infer O } ? O : never;
|
|
6
23
|
|
|
@@ -60,7 +77,6 @@ export type DatabaseSchema<T extends SchemaRecord> = {
|
|
|
60
77
|
|
|
61
78
|
export type JsonValidation = {
|
|
62
79
|
onRead?: boolean;
|
|
63
|
-
onWrite?: boolean;
|
|
64
80
|
};
|
|
65
81
|
|
|
66
82
|
export type DatabaseOptions<T extends SchemaRecord> = {
|
package/src/validation-error.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
export class
|
|
1
|
+
export class ValidationError extends Error {
|
|
2
2
|
constructor(
|
|
3
3
|
readonly table: string,
|
|
4
|
-
readonly column: string,
|
|
5
4
|
readonly summary: string,
|
|
5
|
+
readonly column: string | null = null,
|
|
6
6
|
) {
|
|
7
|
-
super(
|
|
7
|
+
super(
|
|
8
|
+
column
|
|
9
|
+
? `Validation failed for ${table}.${column}: ${summary}`
|
|
10
|
+
: `Validation failed for ${table}: ${summary}`,
|
|
11
|
+
);
|
|
8
12
|
|
|
9
|
-
this.name = "
|
|
13
|
+
this.name = "ValidationError";
|
|
10
14
|
}
|
|
11
15
|
}
|
|
@@ -1,35 +1,84 @@
|
|
|
1
1
|
import { type, type Type } from "arktype";
|
|
2
2
|
import {
|
|
3
3
|
type KyselyPlugin,
|
|
4
|
+
type InsertQueryNode as KyselyInsertQueryNode,
|
|
5
|
+
type UpdateQueryNode as KyselyUpdateQueryNode,
|
|
6
|
+
type OnConflictNode as KyselyOnConflictNode,
|
|
7
|
+
type ColumnUpdateNode as KyselyColumnUpdateNode,
|
|
8
|
+
type ValuesItemNode,
|
|
9
|
+
type OperationNode,
|
|
4
10
|
type RootOperationNode,
|
|
5
11
|
ColumnNode,
|
|
12
|
+
ColumnUpdateNode,
|
|
6
13
|
DefaultInsertValueNode,
|
|
14
|
+
InsertQueryNode,
|
|
15
|
+
OnConflictNode,
|
|
16
|
+
PrimitiveValueListNode,
|
|
17
|
+
ReferenceNode,
|
|
7
18
|
TableNode,
|
|
19
|
+
ValueListNode,
|
|
8
20
|
ValueNode,
|
|
9
21
|
ValuesNode,
|
|
10
22
|
} from "kysely";
|
|
11
23
|
|
|
12
|
-
import type {
|
|
13
|
-
import {
|
|
24
|
+
import type { StructureProp } from "./types.js";
|
|
25
|
+
import { ValidationError } from "./validation-error.js";
|
|
26
|
+
|
|
27
|
+
type TableWriteSchema = {
|
|
28
|
+
schema: Type;
|
|
29
|
+
columns: string[];
|
|
30
|
+
columnSet: Set<string>;
|
|
31
|
+
optionalNonNullColumns: Set<string>;
|
|
32
|
+
insertNullColumns: Set<string>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type InsertRow = {
|
|
36
|
+
values: Record<string, unknown>;
|
|
37
|
+
passthrough: Map<string, OperationNode>;
|
|
38
|
+
};
|
|
14
39
|
|
|
15
40
|
export class WriteValidationPlugin implements KyselyPlugin {
|
|
16
|
-
|
|
17
|
-
private columns: ColumnsMap,
|
|
18
|
-
private validation: { onWrite: boolean },
|
|
19
|
-
) {}
|
|
41
|
+
private schemas = new Map<string, TableWriteSchema>();
|
|
20
42
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
43
|
+
constructor(schemas: Map<string, Type>) {
|
|
44
|
+
this.registerSchemas(schemas);
|
|
45
|
+
}
|
|
25
46
|
|
|
26
|
-
|
|
47
|
+
transformQuery: KyselyPlugin["transformQuery"] = (args) => {
|
|
48
|
+
return this.transformWriteNode(args.node);
|
|
27
49
|
};
|
|
28
50
|
|
|
29
51
|
transformResult: KyselyPlugin["transformResult"] = async (args) => {
|
|
30
52
|
return args.result;
|
|
31
53
|
};
|
|
32
54
|
|
|
55
|
+
private registerSchemas(schemas: Map<string, Type>) {
|
|
56
|
+
for (const [table, schema] of schemas) {
|
|
57
|
+
const structureProps = (schema as any).structure?.props as StructureProp[] | undefined;
|
|
58
|
+
const columns = structureProps?.map((prop) => prop.key) ?? [];
|
|
59
|
+
|
|
60
|
+
this.schemas.set(table, {
|
|
61
|
+
schema,
|
|
62
|
+
columns,
|
|
63
|
+
columnSet: new Set(columns),
|
|
64
|
+
optionalNonNullColumns: new Set(
|
|
65
|
+
structureProps
|
|
66
|
+
?.filter((prop) => !prop.required && !this.acceptsNull(prop.value))
|
|
67
|
+
.map((prop) => prop.key) ?? [],
|
|
68
|
+
),
|
|
69
|
+
insertNullColumns: new Set(
|
|
70
|
+
structureProps
|
|
71
|
+
?.filter((prop) => this.acceptsNull(prop.value) && prop.inner.default === undefined)
|
|
72
|
+
.map((prop) => prop.key) ?? [],
|
|
73
|
+
),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private acceptsNull(field: StructureProp["value"]) {
|
|
79
|
+
return field.branches.some((branch) => branch.unit === null || branch.domain === "null");
|
|
80
|
+
}
|
|
81
|
+
|
|
33
82
|
private getTableFromNode(node: RootOperationNode) {
|
|
34
83
|
switch (node.kind) {
|
|
35
84
|
case "InsertQueryNode":
|
|
@@ -48,90 +97,316 @@ export class WriteValidationPlugin implements KyselyPlugin {
|
|
|
48
97
|
}
|
|
49
98
|
}
|
|
50
99
|
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
|
|
100
|
+
private firstErrorColumn(result: { [index: number]: { path: ArrayLike<unknown> } | undefined }) {
|
|
101
|
+
const column = result[0]?.path[0];
|
|
102
|
+
|
|
103
|
+
if (typeof column === "string") {
|
|
104
|
+
return column;
|
|
54
105
|
}
|
|
55
106
|
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private morph(table: string, schema: Type, value: Record<string, unknown>) {
|
|
56
111
|
const result = schema(value);
|
|
57
112
|
|
|
58
113
|
if (result instanceof type.errors) {
|
|
59
|
-
throw new
|
|
114
|
+
throw new ValidationError(table, result.summary, this.firstErrorColumn(result));
|
|
60
115
|
}
|
|
116
|
+
|
|
117
|
+
return result as Record<string, unknown>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private pickSchema(schema: Type, columns: Iterable<string>) {
|
|
121
|
+
return (schema as any).pick(...columns) as Type;
|
|
61
122
|
}
|
|
62
123
|
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
|
|
124
|
+
private stripNullOptionalFields(tableSchema: TableWriteSchema, value: Record<string, unknown>) {
|
|
125
|
+
const stripped = { ...value };
|
|
126
|
+
|
|
127
|
+
for (const column of tableSchema.optionalNonNullColumns) {
|
|
128
|
+
if (stripped[column] === null) {
|
|
129
|
+
delete stripped[column];
|
|
130
|
+
}
|
|
66
131
|
}
|
|
67
132
|
|
|
133
|
+
return stripped;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private prepareInsertValue(tableSchema: TableWriteSchema, value: Record<string, unknown>) {
|
|
137
|
+
const prepared = this.stripNullOptionalFields(tableSchema, value);
|
|
138
|
+
|
|
139
|
+
for (const column of tableSchema.insertNullColumns) {
|
|
140
|
+
if (!Object.prototype.hasOwnProperty.call(prepared, column)) {
|
|
141
|
+
prepared[column] = null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return prepared;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private transformWriteNode(node: RootOperationNode) {
|
|
68
149
|
const table = this.getTableFromNode(node);
|
|
69
150
|
|
|
70
151
|
if (!table) {
|
|
71
|
-
return;
|
|
152
|
+
return node;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (node.kind === "InsertQueryNode") {
|
|
156
|
+
return this.transformInsert(node, table);
|
|
72
157
|
}
|
|
73
158
|
|
|
74
|
-
|
|
159
|
+
if (node.kind === "UpdateQueryNode") {
|
|
160
|
+
return this.transformUpdate(node, table);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return node;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private getInsertRow(columns: string[], valueList: ValuesItemNode) {
|
|
167
|
+
const row: InsertRow = {
|
|
168
|
+
values: {},
|
|
169
|
+
passthrough: new Map(),
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (PrimitiveValueListNode.is(valueList)) {
|
|
173
|
+
for (let i = 0; i < columns.length; i++) {
|
|
174
|
+
row.values[columns[i]!] = valueList.values[i];
|
|
175
|
+
}
|
|
75
176
|
|
|
76
|
-
|
|
77
|
-
return;
|
|
177
|
+
return row;
|
|
78
178
|
}
|
|
79
179
|
|
|
80
|
-
|
|
81
|
-
|
|
180
|
+
if (!ValueListNode.is(valueList)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
82
183
|
|
|
83
|
-
|
|
184
|
+
for (let i = 0; i < columns.length; i++) {
|
|
185
|
+
const column = columns[i]!;
|
|
186
|
+
const value = valueList.values[i];
|
|
187
|
+
|
|
188
|
+
if (!value || DefaultInsertValueNode.is(value)) {
|
|
84
189
|
continue;
|
|
85
190
|
}
|
|
86
191
|
|
|
87
|
-
|
|
192
|
+
if (ValueNode.is(value)) {
|
|
193
|
+
row.values[column] = value.value;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
row.passthrough.set(column, value);
|
|
88
198
|
}
|
|
199
|
+
|
|
200
|
+
return row;
|
|
89
201
|
}
|
|
90
202
|
|
|
91
|
-
private
|
|
92
|
-
|
|
93
|
-
|
|
203
|
+
private morphInsertRow(table: string, tableSchema: TableWriteSchema, row: InsertRow) {
|
|
204
|
+
const schemaLiteralValues = Object.fromEntries(
|
|
205
|
+
Object.entries(row.values).filter(([column]) => tableSchema.columnSet.has(column)),
|
|
206
|
+
);
|
|
207
|
+
const nonSchemaLiteralValues = Object.fromEntries(
|
|
208
|
+
Object.entries(row.values).filter(([column]) => !tableSchema.columnSet.has(column)),
|
|
209
|
+
);
|
|
210
|
+
const schemaLiteralColumns = Object.keys(schemaLiteralValues);
|
|
94
211
|
|
|
95
|
-
|
|
96
|
-
|
|
212
|
+
if (schemaLiteralColumns.length === 0) {
|
|
213
|
+
return row;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const morphedSchemaValues = this.morph(
|
|
217
|
+
table,
|
|
218
|
+
this.pickSchema(tableSchema.schema, schemaLiteralColumns),
|
|
219
|
+
this.prepareInsertValue(tableSchema, schemaLiteralValues),
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
values: { ...nonSchemaLiteralValues, ...morphedSchemaValues },
|
|
224
|
+
passthrough: row.passthrough,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private getInsertColumns(
|
|
229
|
+
tableSchema: TableWriteSchema,
|
|
230
|
+
originalColumns: string[],
|
|
231
|
+
rows: InsertRow[],
|
|
232
|
+
) {
|
|
233
|
+
const columns = [...originalColumns];
|
|
234
|
+
|
|
235
|
+
for (const column of tableSchema.columns) {
|
|
236
|
+
if (columns.includes(column)) {
|
|
237
|
+
continue;
|
|
97
238
|
}
|
|
98
239
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
240
|
+
if (rows.some((row) => Object.prototype.hasOwnProperty.call(row.values, column))) {
|
|
241
|
+
columns.push(column);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const row of rows) {
|
|
246
|
+
for (const column of Object.keys(row.values)) {
|
|
247
|
+
if (!columns.includes(column)) {
|
|
248
|
+
columns.push(column);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
102
252
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
253
|
+
return columns;
|
|
254
|
+
}
|
|
107
255
|
|
|
108
|
-
|
|
256
|
+
private createInsertValueList(columns: string[], row: InsertRow) {
|
|
257
|
+
return ValueListNode.create(
|
|
258
|
+
columns.map((column) => {
|
|
259
|
+
const passthrough = row.passthrough.get(column);
|
|
109
260
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
261
|
+
if (passthrough) {
|
|
262
|
+
return passthrough;
|
|
263
|
+
}
|
|
113
264
|
|
|
114
|
-
|
|
265
|
+
if (Object.prototype.hasOwnProperty.call(row.values, column)) {
|
|
266
|
+
return ValueNode.create(row.values[column]);
|
|
115
267
|
}
|
|
268
|
+
|
|
269
|
+
return DefaultInsertValueNode.create();
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private transformInsert(node: KyselyInsertQueryNode, table: string) {
|
|
275
|
+
const onConflict = node.onConflict
|
|
276
|
+
? this.transformOnConflict(table, node.onConflict)
|
|
277
|
+
: undefined;
|
|
278
|
+
const tableSchema = this.schemas.get(table);
|
|
279
|
+
|
|
280
|
+
if (!tableSchema) {
|
|
281
|
+
if (onConflict) {
|
|
282
|
+
return InsertQueryNode.cloneWith(node, { onConflict });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return node;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const columns = node.columns?.map((column) => column.column.name);
|
|
289
|
+
|
|
290
|
+
if (!columns || !node.values || !ValuesNode.is(node.values)) {
|
|
291
|
+
if (onConflict) {
|
|
292
|
+
return InsertQueryNode.cloneWith(node, { onConflict });
|
|
116
293
|
}
|
|
117
294
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
295
|
+
return node;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const rows: InsertRow[] = [];
|
|
299
|
+
|
|
300
|
+
for (const valueList of node.values.values) {
|
|
301
|
+
const row = this.getInsertRow(columns, valueList);
|
|
302
|
+
|
|
303
|
+
if (!row) {
|
|
304
|
+
if (onConflict) {
|
|
305
|
+
return InsertQueryNode.cloneWith(node, { onConflict });
|
|
121
306
|
}
|
|
307
|
+
|
|
308
|
+
return node;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
rows.push(this.morphInsertRow(table, tableSchema, row));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const insertColumns = this.getInsertColumns(tableSchema, columns, rows);
|
|
315
|
+
|
|
316
|
+
return InsertQueryNode.cloneWith(node, {
|
|
317
|
+
columns: insertColumns.map((column) => ColumnNode.create(column)),
|
|
318
|
+
values: ValuesNode.create(rows.map((row) => this.createInsertValueList(insertColumns, row))),
|
|
319
|
+
onConflict,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private transformUpdates(table: string, updates: readonly KyselyColumnUpdateNode[]) {
|
|
324
|
+
const tableSchema = this.schemas.get(table);
|
|
325
|
+
|
|
326
|
+
if (!tableSchema) {
|
|
327
|
+
return updates;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const literalColumns = new Set<string>();
|
|
331
|
+
const literalValues: Record<string, unknown> = {};
|
|
332
|
+
const nullOptionalColumns = new Set<string>();
|
|
333
|
+
|
|
334
|
+
for (const update of updates) {
|
|
335
|
+
if (!ColumnNode.is(update.column) || !ValueNode.is(update.value)) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const column = update.column.column.name;
|
|
340
|
+
|
|
341
|
+
if (!tableSchema.columnSet.has(column)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (update.value.value === null && tableSchema.optionalNonNullColumns.has(column)) {
|
|
346
|
+
nullOptionalColumns.add(column);
|
|
347
|
+
continue;
|
|
122
348
|
}
|
|
123
349
|
|
|
124
|
-
|
|
350
|
+
literalColumns.add(column);
|
|
351
|
+
literalValues[column] = update.value.value;
|
|
125
352
|
}
|
|
126
353
|
|
|
127
|
-
if (
|
|
128
|
-
return;
|
|
354
|
+
if (literalColumns.size === 0 && nullOptionalColumns.size === 0) {
|
|
355
|
+
return updates;
|
|
129
356
|
}
|
|
130
357
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
358
|
+
const morphed =
|
|
359
|
+
literalColumns.size === 0
|
|
360
|
+
? {}
|
|
361
|
+
: this.morph(
|
|
362
|
+
table,
|
|
363
|
+
this.pickSchema(tableSchema.schema, literalColumns),
|
|
364
|
+
this.stripNullOptionalFields(tableSchema, literalValues),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
return updates.flatMap((update) => {
|
|
368
|
+
if (!ColumnNode.is(update.column) || !ValueNode.is(update.value)) {
|
|
369
|
+
return [update];
|
|
134
370
|
}
|
|
371
|
+
|
|
372
|
+
const column = update.column.column.name;
|
|
373
|
+
|
|
374
|
+
if (!literalColumns.has(column) || !tableSchema.columnSet.has(column)) {
|
|
375
|
+
if (nullOptionalColumns.has(column)) {
|
|
376
|
+
return [ColumnUpdateNode.create(update.column, ValueNode.create(null))];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return [update];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!Object.prototype.hasOwnProperty.call(morphed, column)) {
|
|
383
|
+
return [
|
|
384
|
+
ColumnUpdateNode.create(update.column, ReferenceNode.create(ColumnNode.create(column))),
|
|
385
|
+
];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return [ColumnUpdateNode.create(update.column, ValueNode.create(morphed[column]))];
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private transformOnConflict(table: string, node: KyselyOnConflictNode) {
|
|
393
|
+
if (!node.updates) {
|
|
394
|
+
return node;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return OnConflictNode.cloneWith(node, {
|
|
398
|
+
updates: this.transformUpdates(table, node.updates),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private transformUpdate(node: KyselyUpdateQueryNode, table: string) {
|
|
403
|
+
if (!node.updates) {
|
|
404
|
+
return node;
|
|
135
405
|
}
|
|
406
|
+
|
|
407
|
+
return Object.freeze({
|
|
408
|
+
...node,
|
|
409
|
+
updates: this.transformUpdates(table, node.updates),
|
|
410
|
+
});
|
|
136
411
|
}
|
|
137
412
|
}
|