@lobomfz/db 0.1.0 → 0.2.1
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 +46 -33
- package/package.json +21 -27
- package/src/database.ts +208 -105
- package/src/dialect/config.ts +7 -0
- package/src/dialect/connection.ts +41 -0
- package/src/dialect/dialect.ts +37 -0
- package/src/dialect/driver.ts +48 -0
- package/src/dialect/mutex.ts +23 -0
- package/src/dialect/serialize.ts +11 -0
- package/src/env.ts +0 -1
- package/src/generated.ts +18 -0
- package/src/index.ts +19 -6
- package/src/plugin.ts +179 -40
- package/src/types.ts +52 -15
- package/src/validation-error.ts +6 -8
- package/src/autoincrement.ts +0 -14
- package/src/serializer.ts +0 -19
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lobomfz/db
|
|
2
2
|
|
|
3
|
-
SQLite database with Arktype schemas and typed Kysely client.
|
|
3
|
+
SQLite database with Arktype schemas and typed Kysely client for Bun.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -11,58 +11,71 @@ bun add @lobomfz/db arktype kysely
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import { type } from "
|
|
15
|
-
import { Database, autoIncrement } from "@lobomfz/db";
|
|
14
|
+
import { Database, generated, type } from "@lobomfz/db";
|
|
16
15
|
|
|
17
16
|
const db = new Database({
|
|
18
17
|
path: "data.db",
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
schema: {
|
|
19
|
+
tables: {
|
|
20
|
+
users: type({
|
|
21
|
+
id: generated("autoincrement"),
|
|
22
|
+
name: "string",
|
|
23
|
+
email: type("string").configure({ unique: true }),
|
|
24
|
+
"bio?": "string", // optional → nullable in SQLite
|
|
25
|
+
active: type("boolean").default(true),
|
|
26
|
+
created_at: generated("now"), // defaults to current time
|
|
27
|
+
}),
|
|
28
|
+
posts: type({
|
|
29
|
+
id: generated("autoincrement"),
|
|
30
|
+
user_id: type("number.integer").configure({ references: "users.id", onDelete: "cascade" }),
|
|
31
|
+
title: "string",
|
|
32
|
+
published_at: "Date", // native Date support
|
|
33
|
+
tags: "string[]", // JSON columns just work
|
|
34
|
+
metadata: type({ source: "string", "priority?": "number" }), // validated on write by default
|
|
35
|
+
status: type.enumerated("draft", "published").default("draft"),
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
indexes: {
|
|
39
|
+
posts: [{ columns: ["user_id", "status"] }, { columns: ["title"], unique: true }],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
pragmas: {
|
|
43
|
+
journal_mode: "wal",
|
|
44
|
+
synchronous: "normal",
|
|
32
45
|
},
|
|
33
46
|
});
|
|
34
47
|
|
|
35
|
-
// Fully typed Kysely client
|
|
48
|
+
// Fully typed Kysely client — generated/default fields are optional on insert
|
|
36
49
|
await db.kysely.insertInto("users").values({ name: "John", email: "john@example.com" }).execute();
|
|
37
50
|
|
|
38
51
|
const users = await db.kysely.selectFrom("users").selectAll().execute();
|
|
52
|
+
// users[0].active → true
|
|
53
|
+
// users[0].created_at → Date
|
|
39
54
|
```
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- Tables auto-created from Arktype schemas
|
|
44
|
-
- Full TypeScript inference
|
|
45
|
-
- JSON columns with validation
|
|
46
|
-
- Foreign keys, unique constraints, defaults
|
|
47
|
-
- `autoIncrement()` for primary keys
|
|
56
|
+
Booleans, dates, objects, arrays — everything round-trips as the type you declared. The schema is the source of truth for table creation, TypeScript types, and runtime coercion.
|
|
48
57
|
|
|
49
|
-
##
|
|
58
|
+
## API
|
|
50
59
|
|
|
51
60
|
```typescript
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
type("
|
|
55
|
-
type("
|
|
61
|
+
generated("autoincrement"); // auto-incrementing primary key
|
|
62
|
+
generated("now"); // defaults to current timestamp, returned as Date
|
|
63
|
+
type("string").default("pending"); // SQL DEFAULT
|
|
64
|
+
type("string").configure({ unique: true }); // UNIQUE
|
|
65
|
+
type("number.integer").configure({ references: "users.id", onDelete: "cascade" }); // FK
|
|
56
66
|
```
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
## Errors
|
|
68
|
+
JSON columns are validated against the schema on write by default. To also validate on read, or to disable write validation:
|
|
61
69
|
|
|
62
70
|
```typescript
|
|
63
|
-
|
|
71
|
+
new Database({
|
|
72
|
+
// ...
|
|
73
|
+
validation: { onRead: true }, // default: { onRead: false, onWrite: true }
|
|
74
|
+
});
|
|
64
75
|
```
|
|
65
76
|
|
|
77
|
+
> **Note:** Migrations are not supported yet. Tables are created with `CREATE TABLE IF NOT EXISTS`.
|
|
78
|
+
|
|
66
79
|
## License
|
|
67
80
|
|
|
68
81
|
MIT
|
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobomfz/db",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "SQLite database with Arktype schemas and typed Kysely client",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "lobomfz",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/lobomfz/db.git"
|
|
10
|
-
},
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Bun SQLite database with Arktype schemas and typed Kysely client",
|
|
11
5
|
"keywords": [
|
|
12
|
-
"sqlite",
|
|
13
|
-
"database",
|
|
14
6
|
"arktype",
|
|
15
|
-
"kysely",
|
|
16
|
-
"typescript",
|
|
17
7
|
"bun",
|
|
8
|
+
"database",
|
|
9
|
+
"kysely",
|
|
18
10
|
"schema",
|
|
11
|
+
"sqlite",
|
|
12
|
+
"typescript",
|
|
19
13
|
"validation"
|
|
20
14
|
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "lobomfz",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/lobomfz/db.git"
|
|
20
|
+
},
|
|
21
21
|
"files": [
|
|
22
22
|
"src"
|
|
23
23
|
],
|
|
@@ -25,26 +25,20 @@
|
|
|
25
25
|
"main": "src/index.ts",
|
|
26
26
|
"types": "src/index.ts",
|
|
27
27
|
"exports": {
|
|
28
|
-
".":
|
|
29
|
-
"types": "./src/index.ts",
|
|
30
|
-
"import": "./src/index.ts"
|
|
31
|
-
}
|
|
28
|
+
".": "./src/index.ts"
|
|
32
29
|
},
|
|
33
30
|
"scripts": {
|
|
34
|
-
"check": "
|
|
35
|
-
},
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"@lobomfz/kysely-bun-sqlite": "^0.4.1"
|
|
31
|
+
"check": "tsgo && oxlint"
|
|
38
32
|
},
|
|
33
|
+
"dependencies": {},
|
|
39
34
|
"devDependencies": {
|
|
40
|
-
"@types/bun": "
|
|
41
|
-
"
|
|
42
|
-
"oxfmt": "^0.
|
|
43
|
-
"oxlint": "^1.
|
|
35
|
+
"@types/bun": "^1.3.11",
|
|
36
|
+
"@typescript/native-preview": "^7.0.0-dev.20260324.1",
|
|
37
|
+
"oxfmt": "^0.42.0",
|
|
38
|
+
"oxlint": "^1.57.0"
|
|
44
39
|
},
|
|
45
40
|
"peerDependencies": {
|
|
46
|
-
"arktype": "^2.1.
|
|
47
|
-
"kysely": "^0.
|
|
48
|
-
"typescript": "^5"
|
|
41
|
+
"arktype": "^2.1.29",
|
|
42
|
+
"kysely": "^0.28.14"
|
|
49
43
|
}
|
|
50
44
|
}
|
package/src/database.ts
CHANGED
|
@@ -1,36 +1,50 @@
|
|
|
1
1
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
2
2
|
import { Kysely } from "kysely";
|
|
3
|
-
import { BunSqliteDialect } from "
|
|
3
|
+
import { BunSqliteDialect } from "./dialect/dialect";
|
|
4
4
|
import type { Type } from "arktype";
|
|
5
|
+
import type { GeneratedPreset } from "./generated";
|
|
5
6
|
import type { DbFieldMeta } from "./env";
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type
|
|
7
|
+
import { DeserializePlugin, type ColumnCoercion, type ColumnsMap } from "./plugin";
|
|
8
|
+
import type {
|
|
9
|
+
DatabaseOptions,
|
|
10
|
+
IndexDefinition,
|
|
11
|
+
SchemaRecord,
|
|
12
|
+
TablesFromSchemas,
|
|
13
|
+
DatabasePragmas,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
type ArkBranch = {
|
|
17
|
+
domain?: string;
|
|
18
|
+
proto?: unknown;
|
|
19
|
+
unit?: unknown;
|
|
20
|
+
structure?: unknown;
|
|
21
|
+
inner?: { divisor?: unknown };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type StructureProp = {
|
|
25
|
+
key: string;
|
|
26
|
+
required: boolean;
|
|
27
|
+
value: Type & {
|
|
28
|
+
branches: ArkBranch[];
|
|
29
|
+
proto?: unknown;
|
|
30
|
+
meta: DbFieldMeta & { _generated?: GeneratedPreset };
|
|
31
|
+
};
|
|
32
|
+
inner: { default?: unknown };
|
|
33
|
+
};
|
|
24
34
|
|
|
25
35
|
type Prop = {
|
|
26
36
|
key: string;
|
|
27
37
|
kind: "required" | "optional";
|
|
28
38
|
domain?: string;
|
|
39
|
+
nullable?: boolean;
|
|
29
40
|
isBoolean?: boolean;
|
|
30
41
|
isInteger?: boolean;
|
|
42
|
+
isDate?: boolean;
|
|
31
43
|
isJson?: boolean;
|
|
32
44
|
jsonSchema?: Type;
|
|
33
45
|
meta?: DbFieldMeta;
|
|
46
|
+
generated?: GeneratedPreset;
|
|
47
|
+
defaultValue?: unknown;
|
|
34
48
|
};
|
|
35
49
|
|
|
36
50
|
const typeMap: Record<string, string> = {
|
|
@@ -38,10 +52,14 @@ const typeMap: Record<string, string> = {
|
|
|
38
52
|
number: "REAL",
|
|
39
53
|
};
|
|
40
54
|
|
|
55
|
+
const defaultPragmas: DatabasePragmas = {
|
|
56
|
+
foreign_keys: true,
|
|
57
|
+
};
|
|
58
|
+
|
|
41
59
|
export class Database<T extends SchemaRecord> {
|
|
42
60
|
private sqlite: BunDatabase;
|
|
43
61
|
|
|
44
|
-
private
|
|
62
|
+
private columns: ColumnsMap = new Map();
|
|
45
63
|
|
|
46
64
|
readonly infer: TablesFromSchemas<T> = undefined as any;
|
|
47
65
|
|
|
@@ -50,153 +68,203 @@ export class Database<T extends SchemaRecord> {
|
|
|
50
68
|
constructor(private options: DatabaseOptions<T>) {
|
|
51
69
|
this.sqlite = new BunDatabase(options.path);
|
|
52
70
|
|
|
53
|
-
this.
|
|
71
|
+
this.applyPragmas();
|
|
54
72
|
|
|
55
73
|
this.createTables();
|
|
56
74
|
|
|
75
|
+
const validation = {
|
|
76
|
+
onRead: options.validation?.onRead ?? false,
|
|
77
|
+
onWrite: options.validation?.onWrite ?? true,
|
|
78
|
+
};
|
|
79
|
+
|
|
57
80
|
this.kysely = new Kysely<TablesFromSchemas<T>>({
|
|
58
81
|
dialect: new BunSqliteDialect({ database: this.sqlite }),
|
|
59
|
-
plugins: [new
|
|
82
|
+
plugins: [new DeserializePlugin(this.columns, validation)],
|
|
60
83
|
});
|
|
61
84
|
}
|
|
62
85
|
|
|
63
|
-
private
|
|
64
|
-
|
|
86
|
+
private applyPragmas() {
|
|
87
|
+
const pragmas = { ...defaultPragmas, ...this.options.pragmas };
|
|
65
88
|
|
|
66
|
-
if (
|
|
67
|
-
|
|
89
|
+
if (pragmas.journal_mode) {
|
|
90
|
+
this.sqlite.run(`PRAGMA journal_mode = ${pragmas.journal_mode.toUpperCase()}`);
|
|
91
|
+
}
|
|
68
92
|
|
|
69
|
-
|
|
70
|
-
|
|
93
|
+
if (pragmas.synchronous) {
|
|
94
|
+
this.sqlite.run(`PRAGMA synchronous = ${pragmas.synchronous.toUpperCase()}`);
|
|
95
|
+
}
|
|
71
96
|
|
|
72
|
-
|
|
73
|
-
const { key, value } = raw;
|
|
97
|
+
this.sqlite.run(`PRAGMA foreign_keys = ${pragmas.foreign_keys ? "ON" : "OFF"}`);
|
|
74
98
|
|
|
75
|
-
if (
|
|
76
|
-
|
|
99
|
+
if (pragmas.busy_timeout_ms !== undefined) {
|
|
100
|
+
this.sqlite.run(`PRAGMA busy_timeout = ${pragmas.busy_timeout_ms}`);
|
|
77
101
|
}
|
|
102
|
+
}
|
|
78
103
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
private normalizeProp(structureProp: StructureProp, parentSchema: Type) {
|
|
105
|
+
const { key, value: v, inner } = structureProp;
|
|
106
|
+
const kind: Prop["kind"] = structureProp.required ? "required" : "optional";
|
|
107
|
+
const generated = v.meta._generated;
|
|
108
|
+
const defaultValue = inner.default;
|
|
82
109
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (u.unit === false) hasFalse = true;
|
|
86
|
-
}
|
|
110
|
+
const nonNull = v.branches.filter((b) => b.unit !== null);
|
|
111
|
+
const nullable = nonNull.length < v.branches.length;
|
|
87
112
|
|
|
88
|
-
|
|
113
|
+
if (v.proto === Date || nonNull.some((b) => b.proto === Date)) {
|
|
114
|
+
return { key, kind, nullable, isDate: true, generated, defaultValue };
|
|
115
|
+
}
|
|
89
116
|
|
|
90
|
-
|
|
117
|
+
if (nonNull.length > 0 && nonNull.every((b) => b.domain === "boolean")) {
|
|
118
|
+
return { key, kind, nullable, isBoolean: true, generated, defaultValue };
|
|
91
119
|
}
|
|
92
120
|
|
|
93
|
-
if (
|
|
121
|
+
if (nonNull.some((b) => !!b.structure)) {
|
|
94
122
|
return {
|
|
95
123
|
key,
|
|
96
124
|
kind,
|
|
125
|
+
nullable,
|
|
97
126
|
isJson: true,
|
|
98
127
|
jsonSchema: (parentSchema as any).get(key) as Type,
|
|
99
|
-
meta:
|
|
128
|
+
meta: v.meta,
|
|
129
|
+
generated,
|
|
130
|
+
defaultValue,
|
|
100
131
|
};
|
|
101
132
|
}
|
|
102
133
|
|
|
103
|
-
const
|
|
134
|
+
const branch = nonNull[0];
|
|
104
135
|
|
|
105
136
|
return {
|
|
106
137
|
key,
|
|
107
138
|
kind,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
nullable,
|
|
140
|
+
domain: branch?.domain,
|
|
141
|
+
isInteger: !!branch?.inner?.divisor,
|
|
142
|
+
meta: v.meta,
|
|
143
|
+
generated,
|
|
144
|
+
defaultValue,
|
|
111
145
|
};
|
|
112
146
|
}
|
|
113
147
|
|
|
114
148
|
private sqlType(prop: Prop) {
|
|
115
|
-
if (prop.isJson)
|
|
116
|
-
|
|
117
|
-
|
|
149
|
+
if (prop.isJson) {
|
|
150
|
+
return "TEXT";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (prop.isDate || prop.isBoolean || prop.isInteger) {
|
|
154
|
+
return "INTEGER";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (prop.domain) {
|
|
158
|
+
return typeMap[prop.domain] ?? "TEXT";
|
|
159
|
+
}
|
|
160
|
+
|
|
118
161
|
return "TEXT";
|
|
119
162
|
}
|
|
120
163
|
|
|
121
|
-
private
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
private columnConstraint(prop: Prop) {
|
|
165
|
+
if (prop.generated === "autoincrement") {
|
|
166
|
+
return "PRIMARY KEY AUTOINCREMENT";
|
|
167
|
+
}
|
|
124
168
|
|
|
125
|
-
if (meta?.primaryKey) {
|
|
126
|
-
|
|
127
|
-
if ((meta as any)._autoincrement) parts.push("AUTOINCREMENT");
|
|
128
|
-
} else if (prop.kind === "required") {
|
|
129
|
-
parts.push("NOT NULL");
|
|
169
|
+
if (prop.meta?.primaryKey) {
|
|
170
|
+
return "PRIMARY KEY";
|
|
130
171
|
}
|
|
131
172
|
|
|
132
|
-
if (
|
|
173
|
+
if (prop.kind === "required" && !prop.nullable) {
|
|
174
|
+
return "NOT NULL";
|
|
175
|
+
}
|
|
133
176
|
|
|
134
|
-
|
|
135
|
-
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
136
179
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
180
|
+
private defaultClause(prop: Prop) {
|
|
181
|
+
if (prop.generated === "now") {
|
|
182
|
+
return "DEFAULT (unixepoch())";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (prop.defaultValue === undefined || prop.generated === "autoincrement") {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (prop.defaultValue === null) {
|
|
190
|
+
return "DEFAULT NULL";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (typeof prop.defaultValue === "string") {
|
|
194
|
+
return `DEFAULT '${prop.defaultValue}'`;
|
|
195
|
+
}
|
|
144
196
|
|
|
145
|
-
|
|
197
|
+
if (typeof prop.defaultValue === "number" || typeof prop.defaultValue === "boolean") {
|
|
198
|
+
return `DEFAULT ${prop.defaultValue}`;
|
|
146
199
|
}
|
|
147
200
|
|
|
148
|
-
|
|
201
|
+
throw new Error(`Unsupported default value type: ${typeof prop.defaultValue}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private columnDef(prop: Prop) {
|
|
205
|
+
return [
|
|
206
|
+
`"${prop.key}"`,
|
|
207
|
+
this.sqlType(prop),
|
|
208
|
+
this.columnConstraint(prop),
|
|
209
|
+
prop.meta?.unique ? "UNIQUE" : null,
|
|
210
|
+
this.defaultClause(prop),
|
|
211
|
+
]
|
|
212
|
+
.filter(Boolean)
|
|
213
|
+
.join(" ");
|
|
149
214
|
}
|
|
150
215
|
|
|
151
216
|
private foreignKey(prop: Prop) {
|
|
152
217
|
const ref = prop.meta?.references;
|
|
153
|
-
|
|
218
|
+
|
|
219
|
+
if (!ref) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
154
222
|
|
|
155
223
|
const [table, column] = ref.split(".");
|
|
224
|
+
|
|
156
225
|
let fk = `FOREIGN KEY ("${prop.key}") REFERENCES "${table}"("${column}")`;
|
|
157
226
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
case "set null":
|
|
163
|
-
fk += " ON DELETE SET NULL";
|
|
164
|
-
break;
|
|
165
|
-
case "restrict":
|
|
166
|
-
fk += " ON DELETE RESTRICT";
|
|
167
|
-
break;
|
|
227
|
+
const onDelete = prop.meta?.onDelete;
|
|
228
|
+
|
|
229
|
+
if (onDelete) {
|
|
230
|
+
fk += ` ON DELETE ${onDelete.toUpperCase()}`;
|
|
168
231
|
}
|
|
169
232
|
|
|
170
233
|
return fk;
|
|
171
234
|
}
|
|
172
235
|
|
|
173
236
|
private parseSchemaProps(schema: Type) {
|
|
174
|
-
const
|
|
175
|
-
const props: Prop[] = [];
|
|
176
|
-
|
|
177
|
-
for (const r of required) {
|
|
178
|
-
props.push(this.normalizeProp(r, "required", schema));
|
|
179
|
-
}
|
|
237
|
+
const structureProps = (schema as any).structure?.props as StructureProp[] | undefined;
|
|
180
238
|
|
|
181
|
-
|
|
182
|
-
|
|
239
|
+
if (!structureProps) {
|
|
240
|
+
return [];
|
|
183
241
|
}
|
|
184
242
|
|
|
185
|
-
return
|
|
243
|
+
return structureProps.map((p) => this.normalizeProp(p, schema));
|
|
186
244
|
}
|
|
187
245
|
|
|
188
|
-
private
|
|
189
|
-
const
|
|
246
|
+
private registerColumns(tableName: string, props: Prop[]) {
|
|
247
|
+
const colMap = new Map<string, ColumnCoercion>();
|
|
190
248
|
|
|
191
249
|
for (const prop of props) {
|
|
192
|
-
if (
|
|
250
|
+
if (prop.isBoolean) {
|
|
251
|
+
colMap.set(prop.key, "boolean");
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
193
254
|
|
|
194
|
-
|
|
195
|
-
|
|
255
|
+
if (prop.isDate) {
|
|
256
|
+
colMap.set(prop.key, "date");
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
196
259
|
|
|
197
|
-
|
|
260
|
+
if (prop.isJson && prop.jsonSchema) {
|
|
261
|
+
colMap.set(prop.key, { type: "json", schema: prop.jsonSchema });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
198
264
|
|
|
199
|
-
|
|
265
|
+
if (colMap.size > 0) {
|
|
266
|
+
this.columns.set(tableName, colMap);
|
|
267
|
+
}
|
|
200
268
|
}
|
|
201
269
|
|
|
202
270
|
private generateCreateTableSQL(tableName: string, props: Prop[]) {
|
|
@@ -207,29 +275,64 @@ export class Database<T extends SchemaRecord> {
|
|
|
207
275
|
columns.push(this.columnDef(prop));
|
|
208
276
|
|
|
209
277
|
const fk = this.foreignKey(prop);
|
|
210
|
-
|
|
278
|
+
|
|
279
|
+
if (fk) {
|
|
280
|
+
fks.push(fk);
|
|
281
|
+
}
|
|
211
282
|
}
|
|
212
283
|
|
|
213
284
|
return `CREATE TABLE IF NOT EXISTS "${tableName}" (${columns.concat(fks).join(", ")})`;
|
|
214
285
|
}
|
|
215
286
|
|
|
216
287
|
private createTables() {
|
|
217
|
-
for (const [name, schema] of Object.entries(this.options.tables)) {
|
|
288
|
+
for (const [name, schema] of Object.entries(this.options.schema.tables)) {
|
|
218
289
|
const props = this.parseSchemaProps(schema);
|
|
219
290
|
|
|
220
|
-
this.
|
|
291
|
+
this.registerColumns(name, props);
|
|
221
292
|
this.sqlite.run(this.generateCreateTableSQL(name, props));
|
|
222
293
|
}
|
|
294
|
+
|
|
295
|
+
this.createIndexes();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private generateIndexName(tableName: string, columns: string[], unique: boolean) {
|
|
299
|
+
const prefix = unique ? "ux" : "ix";
|
|
300
|
+
|
|
301
|
+
return `${prefix}_${tableName}_${columns.join("_")}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private generateCreateIndexSQL(tableName: string, indexDef: IndexDefinition) {
|
|
305
|
+
const indexName = this.generateIndexName(tableName, indexDef.columns, indexDef.unique ?? false);
|
|
306
|
+
const unique = indexDef.unique ? "UNIQUE " : "";
|
|
307
|
+
const columns = indexDef.columns.map((c) => `"${c}"`).join(", ");
|
|
308
|
+
|
|
309
|
+
return `CREATE ${unique}INDEX IF NOT EXISTS "${indexName}" ON "${tableName}" (${columns})`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private createIndexes() {
|
|
313
|
+
const indexes = this.options.schema.indexes;
|
|
314
|
+
|
|
315
|
+
if (!indexes) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const [tableName, tableIndexes] of Object.entries(indexes)) {
|
|
320
|
+
if (!tableIndexes) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const indexDef of tableIndexes) {
|
|
325
|
+
this.sqlite.run(this.generateCreateIndexSQL(tableName, indexDef));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
223
328
|
}
|
|
224
329
|
|
|
225
|
-
reset(table?: keyof T & string) {
|
|
226
|
-
const tables = table ? [table] : Object.keys(this.options.tables);
|
|
330
|
+
reset(table?: keyof T & string): void {
|
|
331
|
+
const tables = table ? [table] : Object.keys(this.options.schema.tables);
|
|
332
|
+
|
|
227
333
|
for (const t of tables) {
|
|
228
334
|
this.sqlite.run(`DELETE FROM "${t}"`);
|
|
229
335
|
}
|
|
230
336
|
}
|
|
231
337
|
|
|
232
|
-
close() {
|
|
233
|
-
this.sqlite.close();
|
|
234
|
-
}
|
|
235
338
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { CompiledQuery, DatabaseConnection, QueryResult } from "kysely";
|
|
3
|
+
import { serializeParam } from "./serialize";
|
|
4
|
+
|
|
5
|
+
export class BunSqliteConnection implements DatabaseConnection {
|
|
6
|
+
readonly #db: Database;
|
|
7
|
+
|
|
8
|
+
constructor(db: Database) {
|
|
9
|
+
this.#db = db;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async executeQuery<O>(compiled: CompiledQuery): Promise<QueryResult<O>> {
|
|
13
|
+
const serializedParams = compiled.parameters.map(serializeParam);
|
|
14
|
+
|
|
15
|
+
const stmt = this.#db.query(compiled.sql);
|
|
16
|
+
|
|
17
|
+
if (stmt.columnNames.length > 0) {
|
|
18
|
+
return {
|
|
19
|
+
rows: stmt.all(serializedParams as any) as O[],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const results = stmt.run(serializedParams as any);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
insertId: BigInt(results.lastInsertRowid),
|
|
27
|
+
numAffectedRows: BigInt(results.changes),
|
|
28
|
+
rows: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async *streamQuery<R>(compiledQuery: CompiledQuery): AsyncIterableIterator<QueryResult<R>> {
|
|
33
|
+
const serializedParams = compiledQuery.parameters.map(serializeParam);
|
|
34
|
+
|
|
35
|
+
const stmt = this.#db.prepare(compiledQuery.sql);
|
|
36
|
+
|
|
37
|
+
for await (const row of stmt.iterate(serializedParams as any)) {
|
|
38
|
+
yield { rows: [row as R] };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SqliteAdapter,
|
|
3
|
+
SqliteIntrospector,
|
|
4
|
+
SqliteQueryCompiler,
|
|
5
|
+
type DatabaseIntrospector,
|
|
6
|
+
type Dialect,
|
|
7
|
+
type DialectAdapter,
|
|
8
|
+
type Driver,
|
|
9
|
+
type Kysely,
|
|
10
|
+
type QueryCompiler,
|
|
11
|
+
} from "kysely";
|
|
12
|
+
import type { BunSqliteDialectConfig } from "./config";
|
|
13
|
+
import { BunSqliteDriver } from "./driver";
|
|
14
|
+
|
|
15
|
+
export class BunSqliteDialect implements Dialect {
|
|
16
|
+
readonly #config: BunSqliteDialectConfig;
|
|
17
|
+
|
|
18
|
+
constructor(config: BunSqliteDialectConfig) {
|
|
19
|
+
this.#config = { ...config };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
createDriver(): Driver {
|
|
23
|
+
return new BunSqliteDriver(this.#config);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
createQueryCompiler(): QueryCompiler {
|
|
27
|
+
return new SqliteQueryCompiler();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
createAdapter(): DialectAdapter {
|
|
31
|
+
return new SqliteAdapter();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
|
35
|
+
return new SqliteIntrospector(db);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite";
|
|
2
|
+
import { CompiledQuery, type DatabaseConnection, type Driver } from "kysely";
|
|
3
|
+
import type { BunSqliteDialectConfig } from "./config";
|
|
4
|
+
import { BunSqliteConnection } from "./connection";
|
|
5
|
+
import { ConnectionMutex } from "./mutex";
|
|
6
|
+
|
|
7
|
+
export class BunSqliteDriver implements Driver {
|
|
8
|
+
readonly #config: BunSqliteDialectConfig;
|
|
9
|
+
readonly #connectionMutex = new ConnectionMutex();
|
|
10
|
+
|
|
11
|
+
#db?: Database;
|
|
12
|
+
#connection?: DatabaseConnection;
|
|
13
|
+
|
|
14
|
+
constructor(config: BunSqliteDialectConfig) {
|
|
15
|
+
this.#config = { ...config };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async init(): Promise<void> {
|
|
19
|
+
this.#db = this.#config.database;
|
|
20
|
+
this.#connection = new BunSqliteConnection(this.#db);
|
|
21
|
+
await this.#config.onCreateConnection?.(this.#connection);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async acquireConnection(): Promise<DatabaseConnection> {
|
|
25
|
+
await this.#connectionMutex.lock();
|
|
26
|
+
return this.#connection!;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
|
30
|
+
await connection.executeQuery(CompiledQuery.raw("begin"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
|
34
|
+
await connection.executeQuery(CompiledQuery.raw("commit"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
|
38
|
+
await connection.executeQuery(CompiledQuery.raw("rollback"));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async releaseConnection(): Promise<void> {
|
|
42
|
+
this.#connectionMutex.unlock();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async destroy(): Promise<void> {
|
|
46
|
+
this.#db?.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class ConnectionMutex {
|
|
2
|
+
#promise?: Promise<void>;
|
|
3
|
+
#resolve?: () => void;
|
|
4
|
+
|
|
5
|
+
async lock(): Promise<void> {
|
|
6
|
+
while (this.#promise) {
|
|
7
|
+
await this.#promise;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
this.#promise = new Promise((resolve) => {
|
|
11
|
+
this.#resolve = resolve;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
unlock(): void {
|
|
16
|
+
const resolve = this.#resolve;
|
|
17
|
+
|
|
18
|
+
this.#promise = undefined;
|
|
19
|
+
this.#resolve = undefined;
|
|
20
|
+
|
|
21
|
+
resolve?.();
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/env.ts
CHANGED
package/src/generated.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
|
|
3
|
+
export type GeneratedPreset = "autoincrement" | "now";
|
|
4
|
+
|
|
5
|
+
const generatedTypes = {
|
|
6
|
+
autoincrement: () =>
|
|
7
|
+
type("number.integer")
|
|
8
|
+
.configure({ _generated: "autoincrement" } as any)
|
|
9
|
+
.default(0),
|
|
10
|
+
now: () =>
|
|
11
|
+
type("Date")
|
|
12
|
+
.configure({ _generated: "now" } as any)
|
|
13
|
+
.default(() => new Date(0)),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function generated<P extends GeneratedPreset>(preset: P): ReturnType<(typeof generatedTypes)[P]> {
|
|
17
|
+
return generatedTypes[preset]() as ReturnType<(typeof generatedTypes)[P]>;
|
|
18
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
-
export { Database } from "./database
|
|
2
|
-
export {
|
|
3
|
-
export { JsonParseError } from "./errors
|
|
4
|
-
export {
|
|
5
|
-
export
|
|
6
|
-
export
|
|
1
|
+
export { Database } from "./database";
|
|
2
|
+
export { generated, type GeneratedPreset } from "./generated";
|
|
3
|
+
export { JsonParseError } from "./errors";
|
|
4
|
+
export { sql, type Selectable, type Insertable, type Updateable } from "kysely";
|
|
5
|
+
export { type } from "arktype";
|
|
6
|
+
export { JsonValidationError } from "./validation-error";
|
|
7
|
+
export type { DbFieldMeta } from "./env";
|
|
8
|
+
export type {
|
|
9
|
+
DatabaseOptions,
|
|
10
|
+
SchemaRecord,
|
|
11
|
+
TablesFromSchemas,
|
|
12
|
+
InferTableType,
|
|
13
|
+
IndexDefinition,
|
|
14
|
+
IndexesConfig,
|
|
15
|
+
DatabasePragmas,
|
|
16
|
+
DatabaseSchema,
|
|
17
|
+
JsonValidation,
|
|
18
|
+
SqliteMasterRow,
|
|
19
|
+
} from "./types";
|
package/src/plugin.ts
CHANGED
|
@@ -1,82 +1,221 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type KyselyPlugin,
|
|
3
|
-
type PluginTransformQueryArgs,
|
|
4
|
-
type PluginTransformResultArgs,
|
|
5
|
-
type QueryResult,
|
|
6
3
|
type RootOperationNode,
|
|
7
4
|
type UnknownRow,
|
|
5
|
+
type QueryId,
|
|
6
|
+
TableNode,
|
|
7
|
+
AliasNode,
|
|
8
|
+
ValuesNode,
|
|
9
|
+
ValueNode,
|
|
10
|
+
ColumnNode,
|
|
8
11
|
} from "kysely";
|
|
9
12
|
import { type } from "arktype";
|
|
10
13
|
import type { Type } from "arktype";
|
|
11
|
-
import { JsonSerializer } from "./serializer";
|
|
12
14
|
import { JsonParseError } from "./errors";
|
|
13
15
|
import { JsonValidationError } from "./validation-error";
|
|
16
|
+
import type { JsonValidation } from "./types";
|
|
14
17
|
|
|
15
|
-
export type
|
|
16
|
-
export type
|
|
18
|
+
export type ColumnCoercion = "boolean" | "date" | { type: "json"; schema: Type };
|
|
19
|
+
export type ColumnsMap = Map<string, Map<string, ColumnCoercion>>;
|
|
17
20
|
|
|
18
|
-
export class
|
|
19
|
-
private
|
|
20
|
-
private queryNodes = new Map<unknown, RootOperationNode>();
|
|
21
|
+
export class DeserializePlugin implements KyselyPlugin {
|
|
22
|
+
private queryNodes = new WeakMap<QueryId, RootOperationNode>();
|
|
21
23
|
|
|
22
|
-
constructor(
|
|
24
|
+
constructor(
|
|
25
|
+
private columns: ColumnsMap,
|
|
26
|
+
private validation: Required<JsonValidation>,
|
|
27
|
+
) {}
|
|
23
28
|
|
|
24
|
-
transformQuery(args
|
|
29
|
+
transformQuery: KyselyPlugin["transformQuery"] = (args) => {
|
|
25
30
|
this.queryNodes.set(args.queryId, args.node);
|
|
26
|
-
return this.serializer.transformNode(args.node);
|
|
27
|
-
}
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
if (this.validation.onWrite) {
|
|
33
|
+
this.validateWriteNode(args.node);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return args.node;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
private getTableFromNode(node: RootOperationNode) {
|
|
30
40
|
switch (node.kind) {
|
|
31
41
|
case "InsertQueryNode":
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
return node.into?.table.identifier.name ?? null;
|
|
43
|
+
|
|
44
|
+
case "UpdateQueryNode": {
|
|
45
|
+
if (node.table && TableNode.is(node.table)) {
|
|
46
|
+
return node.table.table.identifier.name;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
34
51
|
|
|
35
52
|
case "SelectQueryNode":
|
|
36
|
-
case "DeleteQueryNode":
|
|
37
|
-
|
|
53
|
+
case "DeleteQueryNode": {
|
|
54
|
+
const fromNode = node.from?.froms[0];
|
|
55
|
+
|
|
56
|
+
if (!fromNode) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (AliasNode.is(fromNode) && TableNode.is(fromNode.node)) {
|
|
61
|
+
return fromNode.node.table.identifier.name;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (TableNode.is(fromNode)) {
|
|
65
|
+
return fromNode.table.identifier.name;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
38
70
|
|
|
39
71
|
default:
|
|
40
72
|
return null;
|
|
41
73
|
}
|
|
42
74
|
}
|
|
43
75
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
76
|
+
private validateJsonValue(table: string, col: string, value: unknown, schema: Type) {
|
|
77
|
+
if (value === null || value === undefined) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
48
80
|
|
|
49
|
-
|
|
81
|
+
const result = schema(value);
|
|
82
|
+
|
|
83
|
+
if (result instanceof type.errors) {
|
|
84
|
+
throw new JsonValidationError(table, col, result.summary);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private validateWriteNode(node: RootOperationNode) {
|
|
89
|
+
if (node.kind !== "InsertQueryNode" && node.kind !== "UpdateQueryNode") {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
50
92
|
|
|
51
93
|
const table = this.getTableFromNode(node);
|
|
52
94
|
|
|
53
|
-
if (!table)
|
|
95
|
+
if (!table) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
54
98
|
|
|
55
|
-
const
|
|
99
|
+
const cols = this.columns.get(table);
|
|
56
100
|
|
|
57
|
-
if (!
|
|
101
|
+
if (!cols) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
58
104
|
|
|
59
|
-
for (const
|
|
60
|
-
|
|
61
|
-
|
|
105
|
+
for (const [col, value] of this.writeValues(node)) {
|
|
106
|
+
const coercion = cols.get(col);
|
|
107
|
+
|
|
108
|
+
if (!coercion || typeof coercion === "string") {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
this.validateJsonValue(table, col, value, coercion.schema);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private *writeValues(node: RootOperationNode) {
|
|
117
|
+
if (node.kind === "InsertQueryNode") {
|
|
118
|
+
const columns = node.columns?.map((c) => c.column.name);
|
|
119
|
+
|
|
120
|
+
if (!columns || !node.values || !ValuesNode.is(node.values)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const valueList of node.values.values) {
|
|
125
|
+
for (let i = 0; i < columns.length; i++) {
|
|
126
|
+
const col = columns[i]!;
|
|
127
|
+
|
|
128
|
+
if (valueList.kind === "PrimitiveValueListNode") {
|
|
129
|
+
yield [col, valueList.values[i]] as [string, unknown];
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const raw = valueList.values[i];
|
|
134
|
+
yield [col, raw && ValueNode.is(raw) ? raw.value : raw] as [string, unknown];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (node.kind !== "UpdateQueryNode" || !node.updates) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const update of node.updates) {
|
|
146
|
+
if (ColumnNode.is(update.column) && ValueNode.is(update.value)) {
|
|
147
|
+
yield [update.column.column.name, update.value.value] as [string, unknown];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
62
151
|
|
|
63
|
-
|
|
152
|
+
private coerceRow(table: string, row: UnknownRow, cols: Map<string, ColumnCoercion>) {
|
|
153
|
+
for (const [col, coercion] of cols) {
|
|
154
|
+
if (!(col in row)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
64
157
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
} catch (e) {
|
|
69
|
-
throw new JsonParseError(table, col, value, e);
|
|
158
|
+
if (coercion === "boolean") {
|
|
159
|
+
if (typeof row[col] === "number") {
|
|
160
|
+
row[col] = row[col] === 1;
|
|
70
161
|
}
|
|
71
162
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (coercion === "date") {
|
|
167
|
+
if (typeof row[col] === "number") {
|
|
168
|
+
row[col] = new Date(row[col] * 1000);
|
|
75
169
|
}
|
|
76
|
-
|
|
170
|
+
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (typeof row[col] !== "string") {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const value = row[col];
|
|
179
|
+
|
|
180
|
+
let parsed: unknown;
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
parsed = JSON.parse(value);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
throw new JsonParseError(table, col, value, e);
|
|
77
186
|
}
|
|
187
|
+
|
|
188
|
+
if (this.validation.onRead) {
|
|
189
|
+
this.validateJsonValue(table, col, parsed, coercion.schema);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
row[col] = parsed;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
transformResult: KyselyPlugin["transformResult"] = async (args) => {
|
|
197
|
+
const node = this.queryNodes.get(args.queryId);
|
|
198
|
+
|
|
199
|
+
if (!node) {
|
|
200
|
+
return args.result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const table = this.getTableFromNode(node);
|
|
204
|
+
|
|
205
|
+
if (!table) {
|
|
206
|
+
return args.result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const cols = this.columns.get(table);
|
|
210
|
+
|
|
211
|
+
if (!cols) {
|
|
212
|
+
return args.result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const row of args.result.rows) {
|
|
216
|
+
this.coerceRow(table, row, cols);
|
|
78
217
|
}
|
|
79
218
|
|
|
80
219
|
return { ...args.result };
|
|
81
|
-
}
|
|
220
|
+
};
|
|
82
221
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,34 +1,71 @@
|
|
|
1
1
|
import type { Generated } from "kysely";
|
|
2
2
|
import type { Type } from "arktype";
|
|
3
|
-
import type { AutoIncrementNumber } from "./autoincrement";
|
|
4
3
|
|
|
5
|
-
type
|
|
4
|
+
type ExtractInput<T> = T extends { inferIn: infer I } ? I : never;
|
|
5
|
+
type ExtractOutput<T> = T extends { infer: infer O } ? O : never;
|
|
6
6
|
|
|
7
7
|
type IsOptional<T, K extends keyof T> = undefined extends T[K] ? true : false;
|
|
8
8
|
|
|
9
|
-
type TransformColumn<T> = T extends
|
|
10
|
-
? Generated<number>
|
|
11
|
-
: T extends boolean
|
|
12
|
-
? number
|
|
13
|
-
: T extends (infer U)[]
|
|
14
|
-
? U[]
|
|
15
|
-
: T extends object
|
|
16
|
-
? T
|
|
17
|
-
: T;
|
|
9
|
+
type TransformColumn<T> = T extends (infer U)[] ? U[] : T;
|
|
18
10
|
|
|
19
|
-
type
|
|
20
|
-
|
|
11
|
+
type TransformField<TOutput, TInput, K extends keyof TOutput & keyof TInput> =
|
|
12
|
+
IsOptional<TInput, K> extends true
|
|
13
|
+
? IsOptional<TOutput, K> extends true
|
|
14
|
+
? TransformColumn<NonNullable<TOutput[K]>> | null
|
|
15
|
+
: Generated<TransformColumn<NonNullable<TOutput[K]>>>
|
|
16
|
+
: TransformColumn<TOutput[K]>;
|
|
17
|
+
|
|
18
|
+
type TransformTable<TOutput, TInput> = {
|
|
19
|
+
[K in keyof TOutput & keyof TInput]: TransformField<TOutput, TInput, K>;
|
|
21
20
|
};
|
|
22
21
|
|
|
23
22
|
export type SchemaRecord = Record<string, Type>;
|
|
24
23
|
|
|
25
|
-
export type InferTableType<T> = T
|
|
24
|
+
export type InferTableType<T> = TransformTable<ExtractOutput<T>, ExtractInput<T>>;
|
|
25
|
+
|
|
26
|
+
export type SqliteMasterRow = {
|
|
27
|
+
type: "table" | "index" | "view" | "trigger";
|
|
28
|
+
name: string;
|
|
29
|
+
tbl_name: string;
|
|
30
|
+
rootpage: number;
|
|
31
|
+
sql: string | null;
|
|
32
|
+
};
|
|
26
33
|
|
|
27
34
|
export type TablesFromSchemas<T extends SchemaRecord> = {
|
|
28
35
|
[K in keyof T]: InferTableType<T[K]>;
|
|
36
|
+
} & { sqlite_master: SqliteMasterRow };
|
|
37
|
+
|
|
38
|
+
type TableColumns<T extends SchemaRecord, K extends keyof T> = keyof ExtractOutput<T[K]> & string;
|
|
39
|
+
|
|
40
|
+
export type IndexDefinition<Columns extends string = string> = {
|
|
41
|
+
columns: Columns[];
|
|
42
|
+
unique?: boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type IndexesConfig<T extends SchemaRecord> = {
|
|
46
|
+
[K in keyof T]?: IndexDefinition<TableColumns<T, K>>[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type DatabasePragmas = {
|
|
50
|
+
journal_mode?: "delete" | "truncate" | "persist" | "memory" | "wal" | "off";
|
|
51
|
+
synchronous?: "off" | "normal" | "full" | "extra";
|
|
52
|
+
foreign_keys?: boolean;
|
|
53
|
+
busy_timeout_ms?: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type DatabaseSchema<T extends SchemaRecord> = {
|
|
57
|
+
tables: T;
|
|
58
|
+
indexes?: IndexesConfig<T>;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type JsonValidation = {
|
|
62
|
+
onRead?: boolean;
|
|
63
|
+
onWrite?: boolean;
|
|
29
64
|
};
|
|
30
65
|
|
|
31
66
|
export type DatabaseOptions<T extends SchemaRecord> = {
|
|
32
67
|
path: string;
|
|
33
|
-
|
|
68
|
+
schema: DatabaseSchema<T>;
|
|
69
|
+
pragmas?: DatabasePragmas;
|
|
70
|
+
validation?: JsonValidation;
|
|
34
71
|
};
|
package/src/validation-error.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
export class JsonValidationError extends Error {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
constructor(
|
|
3
|
+
readonly table: string,
|
|
4
|
+
readonly column: string,
|
|
5
|
+
readonly summary: string,
|
|
6
|
+
) {
|
|
7
7
|
super(`JSON validation failed for ${table}.${column}: ${summary}`);
|
|
8
|
+
|
|
8
9
|
this.name = "JsonValidationError";
|
|
9
|
-
this.table = table;
|
|
10
|
-
this.column = column;
|
|
11
|
-
this.summary = summary;
|
|
12
10
|
}
|
|
13
11
|
}
|
package/src/autoincrement.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { type, Type } from "arktype";
|
|
2
|
-
|
|
3
|
-
declare const AutoIncrementBrand: unique symbol;
|
|
4
|
-
|
|
5
|
-
export type AutoIncrementNumber = number & { [AutoIncrementBrand]: true };
|
|
6
|
-
|
|
7
|
-
export type AutoIncrementType = Type<AutoIncrementNumber>;
|
|
8
|
-
|
|
9
|
-
export const autoIncrement = (): AutoIncrementType => {
|
|
10
|
-
return type("number.integer").configure({
|
|
11
|
-
primaryKey: true,
|
|
12
|
-
_autoincrement: true,
|
|
13
|
-
} as any) as unknown as AutoIncrementType;
|
|
14
|
-
};
|
package/src/serializer.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { OperationNodeTransformer, type PrimitiveValueListNode } from "kysely";
|
|
2
|
-
|
|
3
|
-
export class JsonSerializer extends OperationNodeTransformer {
|
|
4
|
-
protected override transformPrimitiveValueList(
|
|
5
|
-
node: PrimitiveValueListNode,
|
|
6
|
-
): PrimitiveValueListNode {
|
|
7
|
-
const values: unknown[] = [];
|
|
8
|
-
|
|
9
|
-
for (const v of node.values) {
|
|
10
|
-
if (typeof v === "object" && v !== null && !ArrayBuffer.isView(v)) {
|
|
11
|
-
values.push(JSON.stringify(v));
|
|
12
|
-
} else {
|
|
13
|
-
values.push(v);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return super.transformPrimitiveValueList({ ...node, values });
|
|
18
|
-
}
|
|
19
|
-
}
|