@helloleo/runtime 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cf-shim.ts +72 -7
- package/src/index.ts +13 -0
package/package.json
CHANGED
package/src/cf-shim.ts
CHANGED
|
@@ -17,6 +17,8 @@ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync }
|
|
|
17
17
|
import { readdir } from "node:fs/promises";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
import { createClient } from "@libsql/client";
|
|
20
|
+
import { is, SQL } from "drizzle-orm";
|
|
21
|
+
import { getTableConfig, SQLiteSyncDialect, SQLiteTable } from "drizzle-orm/sqlite-core";
|
|
20
22
|
|
|
21
23
|
// ---- D1 (libsql) ------------------------------------------------------
|
|
22
24
|
//
|
|
@@ -28,14 +30,66 @@ import { createClient } from "@libsql/client";
|
|
|
28
30
|
const dbFile = process.env.DEV_DB_PATH ?? ".dev.db";
|
|
29
31
|
const client = createClient({ url: `file:${dbFile}` });
|
|
30
32
|
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
+
// `ready` gates every D1 query: the dev tables must exist before the first
|
|
34
|
+
// loader query runs. Two ways to populate them, both funnelled through here:
|
|
35
|
+
// 1. createDb(schema) → __pushSchema(schema): generate CREATE TABLE from the
|
|
36
|
+
// drizzle schema (the single source of truth — no schema.sql to drift).
|
|
37
|
+
// 2. legacy schema.sql, applied on boot if present (kept for back-compat).
|
|
38
|
+
let ready: Promise<void> = Promise.resolve();
|
|
39
|
+
|
|
40
|
+
// Apply legacy schema.sql on boot if present.
|
|
33
41
|
if (existsSync("schema.sql")) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
console.error("[cf-shim] failed to apply schema.sql:", e);
|
|
38
|
-
|
|
42
|
+
ready = ready
|
|
43
|
+
.then(() => client.executeMultiple(readFileSync("schema.sql", "utf-8")))
|
|
44
|
+
.then(() => undefined)
|
|
45
|
+
.catch((e) => console.error("[cf-shim] failed to apply schema.sql:", e));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Generate `CREATE TABLE IF NOT EXISTS` from a drizzle sqlite table. Covers the
|
|
49
|
+
// common scaffold cases: column types, single/composite primary keys,
|
|
50
|
+
// autoincrement, NOT NULL, and literal/SQL defaults. Indexes, FKs and unique
|
|
51
|
+
// constraints are out of scope (dev convenience, not a migration engine).
|
|
52
|
+
const dialect = new SQLiteSyncDialect();
|
|
53
|
+
|
|
54
|
+
function tableDDL(table: SQLiteTable): string {
|
|
55
|
+
const cfg = getTableConfig(table);
|
|
56
|
+
const pkCols = cfg.primaryKeys[0]?.columns?.map((c) => c.name) ?? [];
|
|
57
|
+
const compositePk = pkCols.length > 1;
|
|
58
|
+
|
|
59
|
+
const cols = cfg.columns.map((col) => {
|
|
60
|
+
const parts = [`"${col.name}"`, col.getSQLType()];
|
|
61
|
+
if (!compositePk && col.primary) {
|
|
62
|
+
parts.push("PRIMARY KEY");
|
|
63
|
+
// autoIncrement only exists on integer columns.
|
|
64
|
+
if ((col as { autoIncrement?: boolean }).autoIncrement) parts.push("AUTOINCREMENT");
|
|
65
|
+
} else if (col.notNull) {
|
|
66
|
+
parts.push("NOT NULL");
|
|
67
|
+
}
|
|
68
|
+
if (col.hasDefault && col.default !== undefined) {
|
|
69
|
+
const d = col.default;
|
|
70
|
+
const rendered = is(d, SQL)
|
|
71
|
+
? dialect.sqlToQuery(d).sql
|
|
72
|
+
: typeof d === "string"
|
|
73
|
+
? `'${d.replace(/'/g, "''")}'`
|
|
74
|
+
: typeof d === "boolean"
|
|
75
|
+
? d ? "1" : "0"
|
|
76
|
+
: String(d);
|
|
77
|
+
parts.push(`DEFAULT ${rendered}`);
|
|
78
|
+
}
|
|
79
|
+
return parts.join(" ");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (compositePk) cols.push(`PRIMARY KEY (${pkCols.map((c) => `"${c}"`).join(", ")})`);
|
|
83
|
+
return `CREATE TABLE IF NOT EXISTS "${cfg.name}" (${cols.join(", ")});`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function pushDrizzleSchema(schema: Record<string, unknown>): Promise<void> {
|
|
87
|
+
const tables = Object.values(schema).filter((v): v is SQLiteTable => is(v, SQLiteTable));
|
|
88
|
+
const ddl = tables.map(tableDDL).join("\n");
|
|
89
|
+
if (!ddl) return Promise.resolve();
|
|
90
|
+
return client.executeMultiple(ddl).catch((e) => {
|
|
91
|
+
console.error("[cf-shim] failed to push drizzle schema:", e);
|
|
92
|
+
});
|
|
39
93
|
}
|
|
40
94
|
|
|
41
95
|
// Minimal D1Database surface over libsql. Covers what drizzle-orm/d1 calls:
|
|
@@ -46,16 +100,19 @@ function makeD1() {
|
|
|
46
100
|
const args = params as Array<string | number | bigint | boolean | null | Uint8Array>;
|
|
47
101
|
return {
|
|
48
102
|
async all<T = unknown>() {
|
|
103
|
+
await ready;
|
|
49
104
|
const r = await client.execute({ sql, args });
|
|
50
105
|
return { results: r.rows as unknown as T[], success: true, meta: {} };
|
|
51
106
|
},
|
|
52
107
|
async first<T = unknown>(col?: string) {
|
|
108
|
+
await ready;
|
|
53
109
|
const r = await client.execute({ sql, args });
|
|
54
110
|
const row = r.rows[0] as Record<string, unknown> | undefined;
|
|
55
111
|
if (!row) return null;
|
|
56
112
|
return (col ? row[col] : row) as T;
|
|
57
113
|
},
|
|
58
114
|
async run() {
|
|
115
|
+
await ready;
|
|
59
116
|
const r = await client.execute({ sql, args });
|
|
60
117
|
return {
|
|
61
118
|
success: true,
|
|
@@ -67,6 +124,7 @@ function makeD1() {
|
|
|
67
124
|
};
|
|
68
125
|
},
|
|
69
126
|
async raw<T = unknown[]>() {
|
|
127
|
+
await ready;
|
|
70
128
|
const r = await client.execute({ sql, args });
|
|
71
129
|
return r.rows.map((row) =>
|
|
72
130
|
r.columns.map((c) => (row as Record<string, unknown>)[c]),
|
|
@@ -84,9 +142,16 @@ function makeD1() {
|
|
|
84
142
|
return out;
|
|
85
143
|
},
|
|
86
144
|
async exec(sql: string) {
|
|
145
|
+
await ready;
|
|
87
146
|
await client.executeMultiple(sql);
|
|
88
147
|
return { count: 0, duration: 0 };
|
|
89
148
|
},
|
|
149
|
+
// DEV-ONLY hook: createDb(schema) calls this so the drizzle schema is the
|
|
150
|
+
// single source of truth for the local tables. Chained into `ready` so
|
|
151
|
+
// queries wait for the tables. No-op shape in prod (real D1 lacks it).
|
|
152
|
+
__pushSchema(schema: Record<string, unknown>) {
|
|
153
|
+
ready = ready.then(() => pushDrizzleSchema(schema));
|
|
154
|
+
},
|
|
90
155
|
};
|
|
91
156
|
}
|
|
92
157
|
|
package/src/index.ts
CHANGED
|
@@ -26,8 +26,21 @@ export const db = lazy(() => drizzle(env.DB));
|
|
|
26
26
|
// Typed Drizzle client bound to an app schema (enables `db.query.*` relational
|
|
27
27
|
// queries + full inference). Still lazy, so safe to call at app module scope.
|
|
28
28
|
//
|
|
29
|
+
// import * as schema from "./db/schema";
|
|
29
30
|
// export const db = createDb(schema);
|
|
31
|
+
//
|
|
32
|
+
// DEV BONUS: the drizzle schema is the single source of truth for the dev
|
|
33
|
+
// tables. In the sandbox shim we push CREATE TABLE straight from `schema` —
|
|
34
|
+
// no schema.sql to hand-maintain and drift. Gated on __TARGET__ so prod
|
|
35
|
+
// (workerd) never touches `env` at module scope (it would crash at isolate
|
|
36
|
+
// boot); there the real D1 binding has no __pushSchema and migrations run at
|
|
37
|
+
// deploy instead.
|
|
38
|
+
declare const __TARGET__: "workerd" | "sandbox" | undefined;
|
|
39
|
+
|
|
30
40
|
export function createDb<TSchema extends Record<string, unknown>>(schema: TSchema) {
|
|
41
|
+
if (typeof __TARGET__ !== "undefined" && __TARGET__ === "sandbox") {
|
|
42
|
+
(env.DB as { __pushSchema?: (s: TSchema) => void }).__pushSchema?.(schema);
|
|
43
|
+
}
|
|
31
44
|
return lazy(() => drizzle(env.DB, { schema }));
|
|
32
45
|
}
|
|
33
46
|
|