@helloleo/runtime 0.1.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloleo/runtime",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "authors": [
5
5
  "Herbie Vine <herbie@terros>"
6
6
  ],
@@ -16,16 +16,17 @@
16
16
  "typecheck": "bunx tsc --noEmit"
17
17
  },
18
18
  "dependencies": {
19
- "aws4fetch": "^1.0.20",
20
- "drizzle-orm": "^0.36.4"
19
+ "aws4fetch": "^1.0.20"
21
20
  },
22
21
  "devDependencies": {
23
22
  "@cloudflare/workers-types": "^4.20250906.0",
24
23
  "@libsql/client": "^0.14.0",
25
- "@types/bun": "latest"
24
+ "@types/bun": "latest",
25
+ "drizzle-orm": "^0.45.2"
26
26
  },
27
27
  "peerDependencies": {
28
- "@libsql/client": "*"
28
+ "@libsql/client": "*",
29
+ "drizzle-orm": "^0.45.2"
29
30
  },
30
31
  "peerDependenciesMeta": {
31
32
  "@libsql/client": {
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
- // Apply schema.sql on boot if present, so a query in a route loader never hits
32
- // "no such table" on a fresh sandbox.
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
- try {
35
- await client.executeMultiple(readFileSync("schema.sql", "utf-8"));
36
- } catch (e) {
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