@lobomfz/db 0.1.0 → 0.2.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 +59 -25
- package/package.json +21 -27
- package/src/database.ts +205 -108
- 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 +50 -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 +18 -6
- package/src/plugin.ts +79 -44
- package/src/types.ts +47 -16
- package/src/validation-error.ts +6 -8
- package/src/autoincrement.ts +0 -14
- package/src/serializer.ts +0 -19
package/README.md
CHANGED
|
@@ -5,34 +5,38 @@ SQLite database with Arktype schemas and typed Kysely client.
|
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
17
|
+
path: "data.db",
|
|
18
|
+
tables: {
|
|
19
|
+
users: type({
|
|
20
|
+
id: generated("autoincrement"),
|
|
21
|
+
name: "string",
|
|
22
|
+
email: type("string").configure({ unique: true }),
|
|
23
|
+
"bio?": "string",
|
|
24
|
+
created_at: generated("now"),
|
|
25
|
+
}),
|
|
26
|
+
posts: type({
|
|
27
|
+
id: generated("autoincrement"),
|
|
28
|
+
user_id: type("number.integer").configure({ references: "users.id", onDelete: "cascade" }),
|
|
29
|
+
title: "string",
|
|
30
|
+
tags: "string[]",
|
|
31
|
+
status: type("string").default("draft"),
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
indexes: {
|
|
35
|
+
posts: [{ columns: ["user_id", "status"] }, { columns: ["title"], unique: true }],
|
|
36
|
+
},
|
|
33
37
|
});
|
|
34
38
|
|
|
35
|
-
// Fully typed Kysely client
|
|
39
|
+
// Fully typed Kysely client - fields with defaults are optional on insert
|
|
36
40
|
await db.kysely.insertInto("users").values({ name: "John", email: "john@example.com" }).execute();
|
|
37
41
|
|
|
38
42
|
const users = await db.kysely.selectFrom("users").selectAll().execute();
|
|
@@ -41,22 +45,52 @@ const users = await db.kysely.selectFrom("users").selectAll().execute();
|
|
|
41
45
|
## Features
|
|
42
46
|
|
|
43
47
|
- Tables auto-created from Arktype schemas
|
|
44
|
-
- Full TypeScript inference
|
|
48
|
+
- Full TypeScript inference (insert vs select types)
|
|
45
49
|
- JSON columns with validation
|
|
46
50
|
- Foreign keys, unique constraints, defaults
|
|
47
|
-
-
|
|
51
|
+
- Composite indexes
|
|
52
|
+
|
|
53
|
+
## Generated Fields
|
|
54
|
+
|
|
55
|
+
Use `generated()` for SQL-generated values:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
generated("autoincrement"); // INTEGER PRIMARY KEY AUTOINCREMENT
|
|
59
|
+
generated("now"); // DEFAULT (unixepoch()) - Unix timestamp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Default Values
|
|
63
|
+
|
|
64
|
+
Use Arktype's `.default()` for JS defaults (also creates SQL DEFAULT):
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
type("string").default("pending");
|
|
68
|
+
type("number").default(0);
|
|
69
|
+
```
|
|
48
70
|
|
|
49
71
|
## Column Configuration
|
|
50
72
|
|
|
51
73
|
```typescript
|
|
52
|
-
type("string").configure({ unique: true })
|
|
53
|
-
type("
|
|
54
|
-
type("number.integer").configure({ default: "now" }) // Unix timestamp
|
|
55
|
-
type("number.integer").configure({ references: "users.id", onDelete: "cascade" })
|
|
74
|
+
type("string").configure({ unique: true });
|
|
75
|
+
type("number.integer").configure({ references: "users.id", onDelete: "cascade" });
|
|
56
76
|
```
|
|
57
77
|
|
|
58
78
|
`onDelete` options: `"cascade"`, `"set null"`, `"restrict"`
|
|
59
79
|
|
|
80
|
+
## Composite Indexes
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const db = new Database({
|
|
84
|
+
tables: { ... },
|
|
85
|
+
indexes: {
|
|
86
|
+
posts: [
|
|
87
|
+
{ columns: ["user_id", "category_id"], unique: true },
|
|
88
|
+
{ columns: ["created_at"] },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
60
94
|
## Errors
|
|
61
95
|
|
|
62
96
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobomfz/db",
|
|
3
|
-
"version": "0.
|
|
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.0",
|
|
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,49 @@
|
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type
|
|
5
|
+
import type { GeneratedPreset } from "./generated";
|
|
6
|
+
import { DeserializePlugin, type ColumnCoercion, type ColumnsMap } from "./plugin";
|
|
7
|
+
import type {
|
|
8
|
+
DatabaseOptions,
|
|
9
|
+
IndexDefinition,
|
|
10
|
+
SchemaRecord,
|
|
11
|
+
TablesFromSchemas,
|
|
12
|
+
DatabasePragmas,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
type ArkBranch = {
|
|
16
|
+
domain?: string;
|
|
17
|
+
proto?: unknown;
|
|
18
|
+
unit?: unknown;
|
|
19
|
+
structure?: unknown;
|
|
20
|
+
inner?: { divisor?: unknown };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type StructureProp = {
|
|
24
|
+
key: string;
|
|
25
|
+
required: boolean;
|
|
26
|
+
value: Type & {
|
|
27
|
+
branches: ArkBranch[];
|
|
28
|
+
proto?: unknown;
|
|
29
|
+
meta: Record<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
inner: { default?: unknown };
|
|
32
|
+
};
|
|
24
33
|
|
|
25
34
|
type Prop = {
|
|
26
35
|
key: string;
|
|
27
36
|
kind: "required" | "optional";
|
|
28
37
|
domain?: string;
|
|
38
|
+
nullable?: boolean;
|
|
29
39
|
isBoolean?: boolean;
|
|
30
40
|
isInteger?: boolean;
|
|
41
|
+
isDate?: boolean;
|
|
31
42
|
isJson?: boolean;
|
|
32
43
|
jsonSchema?: Type;
|
|
33
|
-
meta?:
|
|
44
|
+
meta?: Record<string, unknown>;
|
|
45
|
+
generated?: GeneratedPreset;
|
|
46
|
+
defaultValue?: unknown;
|
|
34
47
|
};
|
|
35
48
|
|
|
36
49
|
const typeMap: Record<string, string> = {
|
|
@@ -38,10 +51,14 @@ const typeMap: Record<string, string> = {
|
|
|
38
51
|
number: "REAL",
|
|
39
52
|
};
|
|
40
53
|
|
|
54
|
+
const defaultPragmas: DatabasePragmas = {
|
|
55
|
+
foreign_keys: true,
|
|
56
|
+
};
|
|
57
|
+
|
|
41
58
|
export class Database<T extends SchemaRecord> {
|
|
42
59
|
private sqlite: BunDatabase;
|
|
43
60
|
|
|
44
|
-
private
|
|
61
|
+
private columns: ColumnsMap = new Map();
|
|
45
62
|
|
|
46
63
|
readonly infer: TablesFromSchemas<T> = undefined as any;
|
|
47
64
|
|
|
@@ -50,153 +67,198 @@ export class Database<T extends SchemaRecord> {
|
|
|
50
67
|
constructor(private options: DatabaseOptions<T>) {
|
|
51
68
|
this.sqlite = new BunDatabase(options.path);
|
|
52
69
|
|
|
53
|
-
this.
|
|
70
|
+
this.applyPragmas();
|
|
54
71
|
|
|
55
72
|
this.createTables();
|
|
56
73
|
|
|
57
74
|
this.kysely = new Kysely<TablesFromSchemas<T>>({
|
|
58
75
|
dialect: new BunSqliteDialect({ database: this.sqlite }),
|
|
59
|
-
plugins: [new
|
|
76
|
+
plugins: [new DeserializePlugin(this.columns)],
|
|
60
77
|
});
|
|
61
78
|
}
|
|
62
79
|
|
|
63
|
-
private
|
|
64
|
-
|
|
80
|
+
private applyPragmas() {
|
|
81
|
+
const pragmas = { ...defaultPragmas, ...this.options.pragmas };
|
|
65
82
|
|
|
66
|
-
if (
|
|
67
|
-
|
|
83
|
+
if (pragmas.journal_mode) {
|
|
84
|
+
this.sqlite.run(`PRAGMA journal_mode = ${pragmas.journal_mode.toUpperCase()}`);
|
|
85
|
+
}
|
|
68
86
|
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
if (pragmas.synchronous) {
|
|
88
|
+
this.sqlite.run(`PRAGMA synchronous = ${pragmas.synchronous.toUpperCase()}`);
|
|
89
|
+
}
|
|
71
90
|
|
|
72
|
-
|
|
73
|
-
const { key, value } = raw;
|
|
91
|
+
this.sqlite.run(`PRAGMA foreign_keys = ${pragmas.foreign_keys ? "ON" : "OFF"}`);
|
|
74
92
|
|
|
75
|
-
if (
|
|
76
|
-
|
|
93
|
+
if (pragmas.busy_timeout_ms !== undefined) {
|
|
94
|
+
this.sqlite.run(`PRAGMA busy_timeout = ${pragmas.busy_timeout_ms}`);
|
|
77
95
|
}
|
|
96
|
+
}
|
|
78
97
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
private normalizeProp(structureProp: StructureProp, parentSchema: Type): Prop {
|
|
99
|
+
const { key, value: v, inner } = structureProp;
|
|
100
|
+
const kind = structureProp.required ? "required" : "optional";
|
|
101
|
+
const generated = v.meta._generated as GeneratedPreset | undefined;
|
|
102
|
+
const defaultValue = inner.default;
|
|
82
103
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (u.unit === false) hasFalse = true;
|
|
86
|
-
}
|
|
104
|
+
const nonNull = v.branches.filter((b) => b.unit !== null);
|
|
105
|
+
const nullable = nonNull.length < v.branches.length;
|
|
87
106
|
|
|
88
|
-
|
|
107
|
+
if (v.proto === Date || nonNull.some((b) => b.proto === Date)) {
|
|
108
|
+
return { key, kind, nullable, isDate: true, generated, defaultValue };
|
|
109
|
+
}
|
|
89
110
|
|
|
90
|
-
|
|
111
|
+
if (nonNull.length > 0 && nonNull.every((b) => b.domain === "boolean")) {
|
|
112
|
+
return { key, kind, nullable, isBoolean: true, generated, defaultValue };
|
|
91
113
|
}
|
|
92
114
|
|
|
93
|
-
if (
|
|
115
|
+
if (nonNull.some((b) => !!b.structure)) {
|
|
94
116
|
return {
|
|
95
117
|
key,
|
|
96
118
|
kind,
|
|
119
|
+
nullable,
|
|
97
120
|
isJson: true,
|
|
98
121
|
jsonSchema: (parentSchema as any).get(key) as Type,
|
|
99
|
-
meta:
|
|
122
|
+
meta: v.meta,
|
|
123
|
+
generated,
|
|
124
|
+
defaultValue,
|
|
100
125
|
};
|
|
101
126
|
}
|
|
102
127
|
|
|
103
|
-
const
|
|
128
|
+
const branch = nonNull[0];
|
|
104
129
|
|
|
105
130
|
return {
|
|
106
131
|
key,
|
|
107
132
|
kind,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
133
|
+
nullable,
|
|
134
|
+
domain: branch?.domain,
|
|
135
|
+
isInteger: !!branch?.inner?.divisor,
|
|
136
|
+
meta: v.meta,
|
|
137
|
+
generated,
|
|
138
|
+
defaultValue,
|
|
111
139
|
};
|
|
112
140
|
}
|
|
113
141
|
|
|
114
142
|
private sqlType(prop: Prop) {
|
|
115
|
-
if (prop.isJson)
|
|
116
|
-
|
|
117
|
-
|
|
143
|
+
if (prop.isJson) {
|
|
144
|
+
return "TEXT";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (prop.isDate || prop.isBoolean || prop.isInteger) {
|
|
148
|
+
return "INTEGER";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (prop.domain) {
|
|
152
|
+
return typeMap[prop.domain] ?? "TEXT";
|
|
153
|
+
}
|
|
154
|
+
|
|
118
155
|
return "TEXT";
|
|
119
156
|
}
|
|
120
157
|
|
|
121
|
-
private
|
|
122
|
-
|
|
123
|
-
|
|
158
|
+
private columnConstraint(prop: Prop): string | null {
|
|
159
|
+
if (prop.generated === "autoincrement") {
|
|
160
|
+
return "PRIMARY KEY AUTOINCREMENT";
|
|
161
|
+
}
|
|
124
162
|
|
|
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");
|
|
163
|
+
if (prop.meta?.primaryKey) {
|
|
164
|
+
return "PRIMARY KEY";
|
|
130
165
|
}
|
|
131
166
|
|
|
132
|
-
if (
|
|
167
|
+
if (prop.kind === "required" && !prop.nullable) {
|
|
168
|
+
return "NOT NULL";
|
|
169
|
+
}
|
|
133
170
|
|
|
134
|
-
|
|
135
|
-
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
136
173
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
} else {
|
|
142
|
-
val = meta.default;
|
|
143
|
-
}
|
|
174
|
+
private defaultClause(prop: Prop): string | null {
|
|
175
|
+
if (prop.generated === "now") {
|
|
176
|
+
return "DEFAULT (unixepoch())";
|
|
177
|
+
}
|
|
144
178
|
|
|
145
|
-
|
|
179
|
+
if (prop.defaultValue === undefined || prop.generated === "autoincrement") {
|
|
180
|
+
return null;
|
|
146
181
|
}
|
|
147
182
|
|
|
148
|
-
|
|
183
|
+
if (prop.defaultValue === null) {
|
|
184
|
+
return "DEFAULT NULL";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (typeof prop.defaultValue === "string") {
|
|
188
|
+
return `DEFAULT '${prop.defaultValue}'`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (typeof prop.defaultValue === "number" || typeof prop.defaultValue === "boolean") {
|
|
192
|
+
return `DEFAULT ${prop.defaultValue}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return `DEFAULT '${String(prop.defaultValue)}'`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private columnDef(prop: Prop) {
|
|
199
|
+
return [
|
|
200
|
+
`"${prop.key}"`,
|
|
201
|
+
this.sqlType(prop),
|
|
202
|
+
this.columnConstraint(prop),
|
|
203
|
+
prop.meta?.unique ? "UNIQUE" : null,
|
|
204
|
+
this.defaultClause(prop),
|
|
205
|
+
]
|
|
206
|
+
.filter(Boolean)
|
|
207
|
+
.join(" ");
|
|
149
208
|
}
|
|
150
209
|
|
|
151
210
|
private foreignKey(prop: Prop) {
|
|
152
|
-
const ref = prop.meta?.references;
|
|
153
|
-
|
|
211
|
+
const ref = prop.meta?.references as string | undefined;
|
|
212
|
+
|
|
213
|
+
if (!ref) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
154
216
|
|
|
155
217
|
const [table, column] = ref.split(".");
|
|
218
|
+
|
|
156
219
|
let fk = `FOREIGN KEY ("${prop.key}") REFERENCES "${table}"("${column}")`;
|
|
157
220
|
|
|
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;
|
|
221
|
+
const onDelete = prop.meta?.onDelete as string | undefined;
|
|
222
|
+
|
|
223
|
+
if (onDelete) {
|
|
224
|
+
fk += ` ON DELETE ${onDelete.toUpperCase()}`;
|
|
168
225
|
}
|
|
169
226
|
|
|
170
227
|
return fk;
|
|
171
228
|
}
|
|
172
229
|
|
|
173
230
|
private parseSchemaProps(schema: Type) {
|
|
174
|
-
const
|
|
175
|
-
const props: Prop[] = [];
|
|
231
|
+
const structureProps = (schema as any).structure?.props as StructureProp[] | undefined;
|
|
176
232
|
|
|
177
|
-
|
|
178
|
-
|
|
233
|
+
if (!structureProps) {
|
|
234
|
+
return [];
|
|
179
235
|
}
|
|
180
236
|
|
|
181
|
-
|
|
182
|
-
props.push(this.normalizeProp(r, "optional", schema));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return props;
|
|
237
|
+
return structureProps.map((p) => this.normalizeProp(p, schema));
|
|
186
238
|
}
|
|
187
239
|
|
|
188
|
-
private
|
|
189
|
-
const
|
|
240
|
+
private registerColumns(tableName: string, props: Prop[]) {
|
|
241
|
+
const colMap = new Map<string, ColumnCoercion>();
|
|
190
242
|
|
|
191
243
|
for (const prop of props) {
|
|
192
|
-
if (
|
|
244
|
+
if (prop.isBoolean) {
|
|
245
|
+
colMap.set(prop.key, "boolean");
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
193
248
|
|
|
194
|
-
|
|
195
|
-
|
|
249
|
+
if (prop.isDate) {
|
|
250
|
+
colMap.set(prop.key, "date");
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
196
253
|
|
|
197
|
-
|
|
254
|
+
if (prop.isJson && prop.jsonSchema) {
|
|
255
|
+
colMap.set(prop.key, { type: "json", schema: prop.jsonSchema });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
198
258
|
|
|
199
|
-
|
|
259
|
+
if (colMap.size > 0) {
|
|
260
|
+
this.columns.set(tableName, colMap);
|
|
261
|
+
}
|
|
200
262
|
}
|
|
201
263
|
|
|
202
264
|
private generateCreateTableSQL(tableName: string, props: Prop[]) {
|
|
@@ -207,29 +269,64 @@ export class Database<T extends SchemaRecord> {
|
|
|
207
269
|
columns.push(this.columnDef(prop));
|
|
208
270
|
|
|
209
271
|
const fk = this.foreignKey(prop);
|
|
210
|
-
|
|
272
|
+
|
|
273
|
+
if (fk) {
|
|
274
|
+
fks.push(fk);
|
|
275
|
+
}
|
|
211
276
|
}
|
|
212
277
|
|
|
213
278
|
return `CREATE TABLE IF NOT EXISTS "${tableName}" (${columns.concat(fks).join(", ")})`;
|
|
214
279
|
}
|
|
215
280
|
|
|
216
281
|
private createTables() {
|
|
217
|
-
for (const [name, schema] of Object.entries(this.options.tables)) {
|
|
282
|
+
for (const [name, schema] of Object.entries(this.options.schema.tables)) {
|
|
218
283
|
const props = this.parseSchemaProps(schema);
|
|
219
284
|
|
|
220
|
-
this.
|
|
285
|
+
this.registerColumns(name, props);
|
|
221
286
|
this.sqlite.run(this.generateCreateTableSQL(name, props));
|
|
222
287
|
}
|
|
288
|
+
|
|
289
|
+
this.createIndexes();
|
|
223
290
|
}
|
|
224
291
|
|
|
225
|
-
|
|
226
|
-
const
|
|
292
|
+
private generateIndexName(tableName: string, columns: string[], unique: boolean) {
|
|
293
|
+
const prefix = unique ? "ux" : "ix";
|
|
294
|
+
|
|
295
|
+
return `${prefix}_${tableName}_${columns.join("_")}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private generateCreateIndexSQL(tableName: string, indexDef: IndexDefinition) {
|
|
299
|
+
const indexName = this.generateIndexName(tableName, indexDef.columns, indexDef.unique ?? false);
|
|
300
|
+
const unique = indexDef.unique ? "UNIQUE " : "";
|
|
301
|
+
const columns = indexDef.columns.map((c) => `"${c}"`).join(", ");
|
|
302
|
+
|
|
303
|
+
return `CREATE ${unique}INDEX IF NOT EXISTS "${indexName}" ON "${tableName}" (${columns})`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private createIndexes() {
|
|
307
|
+
const indexes = this.options.schema.indexes;
|
|
308
|
+
|
|
309
|
+
if (!indexes) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const [tableName, tableIndexes] of Object.entries(indexes)) {
|
|
314
|
+
if (!tableIndexes) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const indexDef of tableIndexes) {
|
|
319
|
+
this.sqlite.run(this.generateCreateIndexSQL(tableName, indexDef));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
reset(table?: keyof T & string): void {
|
|
325
|
+
const tables = table ? [table] : Object.keys(this.options.schema.tables);
|
|
326
|
+
|
|
227
327
|
for (const t of tables) {
|
|
228
328
|
this.sqlite.run(`DELETE FROM "${t}"`);
|
|
229
329
|
}
|
|
230
330
|
}
|
|
231
331
|
|
|
232
|
-
close() {
|
|
233
|
-
this.sqlite.close();
|
|
234
|
-
}
|
|
235
332
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { 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
|
+
executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
|
|
13
|
+
const { sql, parameters } = compiledQuery;
|
|
14
|
+
const serializedParams = parameters.map(serializeParam);
|
|
15
|
+
const stmt = this.#db.query(sql);
|
|
16
|
+
|
|
17
|
+
if (stmt.columnNames.length > 0) {
|
|
18
|
+
return Promise.resolve({
|
|
19
|
+
rows: stmt.all(serializedParams as any) as O[],
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const results = stmt.run(serializedParams as any);
|
|
24
|
+
|
|
25
|
+
return Promise.resolve({
|
|
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 { sql, parameters } = compiledQuery;
|
|
34
|
+
const serializedParams = parameters.map(serializeParam);
|
|
35
|
+
const stmt = this.#db.prepare(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,50 @@
|
|
|
1
|
+
import { 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
|
+
// oxlint-disable-next-line require-await
|
|
42
|
+
async releaseConnection(): Promise<void> {
|
|
43
|
+
this.#connectionMutex.unlock();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// oxlint-disable-next-line require-await
|
|
47
|
+
async destroy(): Promise<void> {
|
|
48
|
+
this.#db?.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -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,18 @@
|
|
|
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
|
+
SqliteMasterRow,
|
|
18
|
+
} from "./types";
|
package/src/plugin.ts
CHANGED
|
@@ -1,82 +1,117 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type KyselyPlugin,
|
|
3
|
-
type PluginTransformQueryArgs,
|
|
4
|
-
type PluginTransformResultArgs,
|
|
5
|
-
type QueryResult,
|
|
6
|
-
type RootOperationNode,
|
|
7
|
-
type UnknownRow,
|
|
8
|
-
} from "kysely";
|
|
1
|
+
import { type KyselyPlugin, type RootOperationNode, type UnknownRow } from "kysely";
|
|
9
2
|
import { type } from "arktype";
|
|
10
3
|
import type { Type } from "arktype";
|
|
11
|
-
import { JsonSerializer } from "./serializer";
|
|
12
4
|
import { JsonParseError } from "./errors";
|
|
13
5
|
import { JsonValidationError } from "./validation-error";
|
|
14
6
|
|
|
15
|
-
export type
|
|
16
|
-
export type
|
|
7
|
+
export type ColumnCoercion = "boolean" | "date" | { type: "json"; schema: Type };
|
|
8
|
+
export type ColumnsMap = Map<string, Map<string, ColumnCoercion>>;
|
|
17
9
|
|
|
18
|
-
export class
|
|
19
|
-
private serializer = new JsonSerializer();
|
|
10
|
+
export class DeserializePlugin implements KyselyPlugin {
|
|
20
11
|
private queryNodes = new Map<unknown, RootOperationNode>();
|
|
21
12
|
|
|
22
|
-
constructor(private
|
|
13
|
+
constructor(private columns: ColumnsMap) {}
|
|
23
14
|
|
|
24
|
-
transformQuery(args
|
|
15
|
+
transformQuery: KyselyPlugin["transformQuery"] = (args) => {
|
|
25
16
|
this.queryNodes.set(args.queryId, args.node);
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
|
|
18
|
+
return args.node;
|
|
19
|
+
};
|
|
28
20
|
|
|
29
21
|
private getTableFromNode(node: RootOperationNode): string | null {
|
|
30
22
|
switch (node.kind) {
|
|
31
23
|
case "InsertQueryNode":
|
|
24
|
+
return (node as any).into?.table?.identifier?.name ?? null;
|
|
25
|
+
|
|
32
26
|
case "UpdateQueryNode":
|
|
33
27
|
return (node as any).table?.table?.identifier?.name ?? null;
|
|
34
28
|
|
|
35
29
|
case "SelectQueryNode":
|
|
36
|
-
case "DeleteQueryNode":
|
|
37
|
-
|
|
30
|
+
case "DeleteQueryNode": {
|
|
31
|
+
const fromNode = (node as any).from?.froms?.[0];
|
|
32
|
+
|
|
33
|
+
if (fromNode?.kind === "AliasNode") {
|
|
34
|
+
return fromNode.node?.table?.identifier?.name ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return fromNode?.table?.identifier?.name ?? null;
|
|
38
|
+
}
|
|
38
39
|
|
|
39
40
|
default:
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
private coerceRow(table: string, row: UnknownRow, cols: Map<string, ColumnCoercion>) {
|
|
46
|
+
for (const [col, coercion] of cols) {
|
|
47
|
+
if (!(col in row)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
if (coercion === "boolean") {
|
|
52
|
+
if (typeof row[col] === "number") {
|
|
53
|
+
row[col] = row[col] === 1;
|
|
54
|
+
}
|
|
50
55
|
|
|
51
|
-
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
if (coercion === "date") {
|
|
60
|
+
if (typeof row[col] === "number") {
|
|
61
|
+
row[col] = new Date(row[col] * 1000);
|
|
62
|
+
}
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
if (typeof row[col] !== "string") {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
for (const [col, info] of columns.entries()) {
|
|
61
|
-
if (!(col in row) || typeof row[col] !== "string") continue;
|
|
71
|
+
const value = row[col];
|
|
62
72
|
|
|
63
|
-
|
|
73
|
+
let parsed: unknown;
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(value);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
throw new JsonParseError(table, col, value, e);
|
|
79
|
+
}
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
row[col] = validated;
|
|
81
|
+
const validated = coercion.schema(parsed);
|
|
82
|
+
|
|
83
|
+
if (validated instanceof type.errors) {
|
|
84
|
+
throw new JsonValidationError(table, col, validated.summary);
|
|
77
85
|
}
|
|
86
|
+
|
|
87
|
+
row[col] = validated;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// oxlint-disable-next-line require-await
|
|
92
|
+
transformResult: KyselyPlugin["transformResult"] = async (args) => {
|
|
93
|
+
const node = this.queryNodes.get(args.queryId);
|
|
94
|
+
|
|
95
|
+
if (!node) {
|
|
96
|
+
return args.result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const table = this.getTableFromNode(node);
|
|
100
|
+
|
|
101
|
+
if (!table) {
|
|
102
|
+
return args.result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const cols = this.columns.get(table);
|
|
106
|
+
|
|
107
|
+
if (!cols) {
|
|
108
|
+
return args.result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const row of args.result.rows) {
|
|
112
|
+
this.coerceRow(table, row, cols);
|
|
78
113
|
}
|
|
79
114
|
|
|
80
115
|
return { ...args.result };
|
|
81
|
-
}
|
|
116
|
+
};
|
|
82
117
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,34 +1,65 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
?
|
|
15
|
-
:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
[K in keyof T]: NullableIf<TransformColumn<NonNullable<T[K]>>, IsOptional<T, K>>;
|
|
9
|
+
type TransformColumn<T> = T extends (infer U)[] ? U[] : T;
|
|
10
|
+
|
|
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>;
|
|
29
59
|
};
|
|
30
60
|
|
|
31
61
|
export type DatabaseOptions<T extends SchemaRecord> = {
|
|
32
62
|
path: string;
|
|
33
|
-
|
|
63
|
+
schema: DatabaseSchema<T>;
|
|
64
|
+
pragmas?: DatabasePragmas;
|
|
34
65
|
};
|
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
|
-
}
|