@awsmbiz/inbis-db 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,96 @@
1
- # Temporary Holding Version
1
+ # @awsmbiz/inbis-db
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ D1 + Drizzle helpers. Peer dependency: `drizzle-orm` ^0.45.4. No tenant columns and no
4
+ generic repository: write queries with Drizzle in your own repository layer.
5
+
6
+ ## Database
7
+
8
+ ```ts
9
+ import { createDatabase } from "@awsmbiz/inbis-db";
10
+ import * as schema from "./schema";
11
+
12
+ const db = createDatabase(env.DB, { schema }); // Drizzle D1 instance; db.query.* enabled by schema
13
+ ```
14
+
15
+ Create it per request from the binding you were given. The options (`CreateDatabaseOptions`)
16
+ take the Drizzle `schema`.
17
+
18
+ ## Columns
19
+
20
+ ```ts
21
+ import { sqliteTable, text } from "drizzle-orm/sqlite-core";
22
+ import { idColumn, timestampColumns } from "@awsmbiz/inbis-db";
23
+
24
+ export const widgets = sqliteTable("widgets", {
25
+ id: idColumn(), // TEXT PRIMARY KEY, UUIDv7 generated on insert
26
+ name: text("name").notNull(),
27
+ ...timestampColumns(), // created_at / updated_at, epoch ms, as Date
28
+ });
29
+ ```
30
+
31
+ ## Batch (atomic writes)
32
+
33
+ D1 has no interactive transactions. `batch(db, [stmt, ...])` runs the statements
34
+ all-or-nothing in one round trip and returns their typed results in order. An empty
35
+ list returns `[]`. A unique-constraint violation becomes a `409 CONFLICT` `AppError`;
36
+ other errors are rethrown unchanged.
37
+
38
+ ## Keyset pagination
39
+
40
+ ```ts
41
+ import { gt } from "drizzle-orm";
42
+ import { parsePageRequest, toPage } from "@awsmbiz/inbis-db";
43
+
44
+ const page = parsePageRequest({ limit: query.limit, cursor: query.cursor }, { defaultLimit: 20, maxLimit: 100 });
45
+ const rows = await db.select().from(widgets)
46
+ .where(page.after === undefined ? undefined : gt(widgets.id, page.after))
47
+ .orderBy(widgets.id)
48
+ .limit(page.fetchLimit);
49
+ return toPage(rows, page, (w) => w.id); // { items, nextCursor: string | null }
50
+ ```
51
+
52
+ Cursors are opaque. An invalid cursor gives `400 INVALID_CURSOR`, and an invalid limit
53
+ (not a positive integer) gives `422 VALIDATION_FAILED`. Limits above `maxLimit` are clamped.
54
+
55
+ ## Migrations: the `inbis-db` CLI
56
+
57
+ Platform packages that own tables ship their migrations. Each migration source is a D1
58
+ binding to the app's one database, with its own `migrations_dir` and `migrations_table`
59
+ (ADR-4, `docs/adr/0004-package-owned-migrations.md`). `inbis-db` is a Node bin that drives
60
+ the app's `wrangler`. It keeps no history of its own.
61
+
62
+ ```sh
63
+ inbis-db migrate [--local|--remote] [--env <env>] [--config <path>] [--persist-to <dir>]
64
+ inbis-db status [same options]
65
+ inbis-db lock <dir>
66
+ inbis-db check-lock <dir> [--base <git-ref>]
67
+ ```
68
+
69
+ - **`migrate`** runs `wrangler d1 migrations apply <BINDING>` for each source of each
70
+ database, in `wrangler.jsonc` declaration order (platform bindings first, then the app). It
71
+ stops at the first failure, then prints the status table. It refuses to start if a file
72
+ locked in a source's `migrations.lock.json` has changed (checksum drift).
73
+ - **`status`** prints, per source, every migration as `applied` or `pending`, plus any applied
74
+ migration whose file is missing:
75
+
76
+ ```
77
+ Migration status for D1 database "identity-playground-dev" (local):
78
+ binding table migration status
79
+ AUTH_DB _inbis_auth_migrations 0001_auth_initial.sql applied
80
+ DB d1_migrations 0001_app_profile.sql pending
81
+ 1 applied, 1 pending
82
+ ```
83
+
84
+ - **Timeout:** each wrangler run may take 5 minutes (`INBIS_DB_WRANGLER_TIMEOUT_MS` to
85
+ change it); a run past it is stopped and reported with its output so far (exit code 124),
86
+ so a hung wrangler cannot hang CI. Reading the status is one `wrangler d1 execute`
87
+ (two when a migrations table does not exist yet).
88
+ - **Layout checks** (both commands): a binding may not be named like its database; bindings of
89
+ one `database_name` must resolve to the same database (share `database_id`); sources may not
90
+ share a migrations table or directory; `migrations_pattern` is not supported (use flat `*.sql`).
91
+ - **`lock`** adds sha256 checksums for new `*.sql` files to `<dir>/migrations.lock.json` and
92
+ refuses if a locked file changed. **`check-lock`** fails on drift, deleted or unlocked files.
93
+ With `--base`, it also fails if any entry locked at that git ref (the released migrations)
94
+ changed, even when the lock was edited too.
95
+
96
+ Requires `wrangler` (optional peer dependency, ^4.156.0) in the app.
package/dist/cli.mjs ADDED
@@ -0,0 +1,485 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { parseArgs } from "node:util";
6
+ import { createHash } from "node:crypto";
7
+ import { execFileSync, spawn } from "node:child_process";
8
+ import { pathToFileURL } from "node:url";
9
+ // cli/src/errors.ts
10
+ /** An expected failure: printed without a stack trace, exit code 1. */
11
+ var CliError = class extends Error {
12
+ name = "CliError";
13
+ };
14
+ // cli/src/sources.ts
15
+ const DEFAULT_MIGRATIONS_TABLE = "d1_migrations";
16
+ /** The database a binding resolves to, mirroring wrangler (local: preview id, then id, then binding). */
17
+ function databaseKey(entry, target) {
18
+ return target === "local" ? entry.preview_database_id ?? entry.database_id ?? entry.binding : entry.database_id ?? entry.binding;
19
+ }
20
+ /**
21
+ * Groups the config's D1 bindings by physical database, keeping declaration
22
+ * order (declare platform bindings before the app's). Rejects layouts where
23
+ * histories would collide or bindings silently miss each other (ADR-4).
24
+ */
25
+ function databaseSources(config, target) {
26
+ const base = dirname(config.configPath ?? ".");
27
+ const groups = /* @__PURE__ */ new Map();
28
+ const keyByName = /* @__PURE__ */ new Map();
29
+ for (const entry of config.d1_databases) {
30
+ const name = entry.database_name ?? entry.binding;
31
+ if (entry.binding === entry.database_name) throw new CliError(`D1 binding "${entry.binding}" has the same name as its database. Rename the binding: commands target bindings, and a database name always selects the first binding.`);
32
+ if (entry.migrations_pattern !== void 0) throw new CliError(`D1 binding "${entry.binding}" sets migrations_pattern, which inbis-db does not support. Use flat *.sql files in migrations_dir.`);
33
+ const key = databaseKey(entry, target);
34
+ const knownKey = keyByName.get(name);
35
+ if (knownKey !== void 0 && knownKey !== key) throw new CliError(`D1 bindings for database "${name}" resolve to different ${target} databases. Give every binding of one database the same database_id.`);
36
+ keyByName.set(name, key);
37
+ const group = groups.get(key) ?? {
38
+ database: name,
39
+ sources: []
40
+ };
41
+ groups.set(key, group);
42
+ const source = {
43
+ binding: entry.binding,
44
+ table: entry.migrations_table ?? DEFAULT_MIGRATIONS_TABLE,
45
+ dir: resolve(base, entry.migrations_dir ?? "migrations")
46
+ };
47
+ for (const other of group.sources) {
48
+ if (other.table === source.table) throw new CliError(`D1 bindings "${other.binding}" and "${source.binding}" share the migrations table "${source.table}". Each migration source needs its own migrations_table.`);
49
+ if (other.dir === source.dir) throw new CliError(`D1 bindings "${other.binding}" and "${source.binding}" share the migrations directory ${source.dir}.`);
50
+ }
51
+ group.sources.push(source);
52
+ }
53
+ return [...groups.values()];
54
+ }
55
+ /** Migration file names in a source, in apply order (the files wrangler applies). */
56
+ function migrationFiles(source) {
57
+ if (!existsSync(source.dir)) return [];
58
+ return readdirSync(source.dir, { withFileTypes: true }).filter((entry) => entry.isFile() && !entry.name.startsWith(".") && entry.name.endsWith(".sql")).map((entry) => entry.name).sort();
59
+ }
60
+ // cli/src/lock.ts
61
+ const LOCK_FILE = "migrations.lock.json";
62
+ /** sha256 of a migration file; line endings are normalised so checkouts on any OS agree. */
63
+ function checksum(contents) {
64
+ return `sha256:${createHash("sha256").update(contents.replaceAll("\r\n", "\n")).digest("hex")}`;
65
+ }
66
+ function readLock(dir) {
67
+ const path = join(dir, LOCK_FILE);
68
+ return existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : void 0;
69
+ }
70
+ /** Problems with locked files: changed (drift) or deleted. */
71
+ function lockDrift(dir, lock) {
72
+ return Object.entries(lock).flatMap(([name, expected]) => {
73
+ const path = join(dir, name);
74
+ if (!existsSync(path)) return [`${name} is locked but missing`];
75
+ return checksum(readFileSync(path, "utf8")) === expected ? [] : [`${name} changed after it was locked (checksum drift)`];
76
+ });
77
+ }
78
+ /** Every problem `check-lock` reports: drift, deleted files and files missing from the lock. */
79
+ function lockProblems(dir, lock) {
80
+ const unlocked = migrationFiles({
81
+ binding: "",
82
+ table: "",
83
+ dir
84
+ }).filter((name) => !(name in lock)).map((name) => `${name} is not in ${LOCK_FILE}; run inbis-db lock`);
85
+ return [...lockDrift(dir, lock), ...unlocked];
86
+ }
87
+ /** Adds checksums for new migration files. Refuses if a locked file has changed. */
88
+ function updateLock(dir) {
89
+ const lock = readLock(dir) ?? {};
90
+ const drift = lockDrift(dir, lock);
91
+ if (drift.length > 0) throw new CliError(`Cannot update ${LOCK_FILE}:\n ${drift.join("\n ")}`);
92
+ const added = migrationFiles({
93
+ binding: "",
94
+ table: "",
95
+ dir
96
+ }).filter((name) => !(name in lock));
97
+ for (const name of added) lock[name] = checksum(readFileSync(join(dir, name), "utf8"));
98
+ const sorted = Object.fromEntries(Object.entries(lock).sort(([a], [b]) => a.localeCompare(b)));
99
+ writeFileSync(join(dir, LOCK_FILE), `${JSON.stringify(sorted, null, 2)}\n`);
100
+ return { added };
101
+ }
102
+ function git(dir, args) {
103
+ return execFileSync("git", args, {
104
+ cwd: dir,
105
+ encoding: "utf8",
106
+ stdio: [
107
+ "ignore",
108
+ "pipe",
109
+ "pipe"
110
+ ]
111
+ });
112
+ }
113
+ /**
114
+ * Released-migration guard: every entry in the lock at `baseRef` (e.g. the
115
+ * default branch) must still be present and unchanged, so editing a released
116
+ * file together with its lock entry is still caught.
117
+ */
118
+ function releasedChanges(dir, lock, baseRef) {
119
+ try {
120
+ git(dir, [
121
+ "rev-parse",
122
+ "--verify",
123
+ "--quiet",
124
+ `${baseRef}^{commit}`
125
+ ]);
126
+ } catch {
127
+ throw new CliError(`Base ref "${baseRef}" not found. Fetch it first (git fetch origin <branch>).`);
128
+ }
129
+ let released;
130
+ try {
131
+ released = JSON.parse(git(dir, ["show", `${baseRef}:./${LOCK_FILE}`]));
132
+ } catch {
133
+ return [];
134
+ }
135
+ return Object.entries(released).flatMap(([name, expected]) => lock[name] === expected ? [] : [`${name} was released at ${baseRef} and must not change (add a new migration instead)`]);
136
+ }
137
+ // cli/src/status.ts
138
+ function quoteIdentifier(name) {
139
+ return `"${name.replaceAll("\"", "\"\"")}"`;
140
+ }
141
+ function quoteLiteral(value) {
142
+ return `'${value.replaceAll("'", "''")}'`;
143
+ }
144
+ async function execute(wrangler, binding, sql, target) {
145
+ return wrangler.exec([
146
+ "d1",
147
+ "execute",
148
+ binding,
149
+ "--json",
150
+ "--command",
151
+ sql,
152
+ ...target
153
+ ]);
154
+ }
155
+ function rowsOf(stdout) {
156
+ return JSON.parse(stdout).flatMap((statement) => statement.results);
157
+ }
158
+ async function query(wrangler, binding, sql, target) {
159
+ const result = await execute(wrangler, binding, sql, target);
160
+ if (result.code !== 0) throw new CliError(`wrangler d1 execute ${binding} failed:
161
+ ${result.stdout}${result.stderr}`);
162
+ return rowsOf(result.stdout);
163
+ }
164
+ const appliedSql = (sources) => sources.map((source) => `SELECT ${quoteLiteral(source.binding)} AS binding, name FROM ${quoteIdentifier(source.table)}`).join(" UNION ALL ");
165
+ /**
166
+ * The applied migrations of every source. Usually one wrangler run (each run
167
+ * is a cold start): every tracking table is read at once. Only when one does
168
+ * not exist yet (never migrated) are the existing tables looked up first.
169
+ */
170
+ async function readApplied(wrangler, database, binding, target) {
171
+ const all = await execute(wrangler, binding, appliedSql(database.sources), target);
172
+ if (all.code === 0) return rowsOf(all.stdout);
173
+ if (!/no such table/i.test(`${all.stdout}${all.stderr}`)) throw new CliError(`wrangler d1 execute ${binding} failed:
174
+ ${all.stdout}${all.stderr}`);
175
+ const tables = new Set((await query(wrangler, binding, "SELECT name FROM sqlite_master WHERE type = 'table'", target)).map((row) => row.name));
176
+ const tracked = database.sources.filter((source) => tables.has(source.table));
177
+ return tracked.length === 0 ? [] : query(wrangler, binding, appliedSql(tracked), target);
178
+ }
179
+ /** Applied and pending migrations of every source of one database, read through wrangler. */
180
+ async function readStatus(wrangler, database, target) {
181
+ const [first] = database.sources;
182
+ if (first === void 0) return [];
183
+ const applied = await readApplied(wrangler, database, first.binding, target);
184
+ return database.sources.flatMap((source) => {
185
+ const appliedHere = new Set(applied.filter((row) => row.binding === source.binding).map((row) => row.name));
186
+ const files = migrationFiles(source);
187
+ const rows = files.map((migration) => ({
188
+ binding: source.binding,
189
+ table: source.table,
190
+ migration,
191
+ status: appliedHere.has(migration) ? "applied" : "pending"
192
+ }));
193
+ for (const migration of appliedHere) if (!files.includes(migration)) rows.push({
194
+ binding: source.binding,
195
+ table: source.table,
196
+ migration,
197
+ status: "applied, file missing"
198
+ });
199
+ return rows;
200
+ });
201
+ }
202
+ /** Renders the status as an aligned plain-text table (stable for CI logs and tests). */
203
+ function formatStatus(database, target, rows) {
204
+ const header = [
205
+ "binding",
206
+ "table",
207
+ "migration",
208
+ "status"
209
+ ];
210
+ const cells = [header, ...rows.map((row) => [
211
+ row.binding,
212
+ row.table,
213
+ row.migration,
214
+ row.status
215
+ ])];
216
+ const widths = header.map((_, column) => Math.max(...cells.map((line) => line[column]?.length ?? 0)));
217
+ const lines = cells.map((line) => line.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd());
218
+ const pending = rows.filter((row) => row.status === "pending").length;
219
+ return [
220
+ `Migration status for D1 database "${database}" (${target}):`,
221
+ ...lines,
222
+ `${rows.length - pending} applied, ${pending} pending`,
223
+ ""
224
+ ].join("\n");
225
+ }
226
+ // cli/src/wrangler.ts
227
+ /**
228
+ * Locates the application's wrangler: the one installed next to its config,
229
+ * falling back to the one installed with this package.
230
+ */
231
+ function resolveWrangler(fromDir) {
232
+ for (const base of [join(fromDir, "noop.js"), import.meta.url]) try {
233
+ const require = createRequire(base);
234
+ return {
235
+ root: dirname(require.resolve("wrangler/package.json")),
236
+ main: require.resolve("wrangler")
237
+ };
238
+ } catch {}
239
+ throw new Error("wrangler is not installed. Add it to your app: pnpm add -D wrangler");
240
+ }
241
+ /**
242
+ * How long one wrangler run may take (default 5 min, room for a slow remote
243
+ * `migrations apply`). Override with `INBIS_DB_WRANGLER_TIMEOUT_MS`.
244
+ */
245
+ const DEFAULT_WRANGLER_TIMEOUT_MS = 3e5;
246
+ function timeoutFromEnv() {
247
+ const value = Number(process.env["INBIS_DB_WRANGLER_TIMEOUT_MS"]);
248
+ return Number.isInteger(value) && value > 0 ? value : DEFAULT_WRANGLER_TIMEOUT_MS;
249
+ }
250
+ var Wrangler = class {
251
+ #root;
252
+ #main;
253
+ #cwd;
254
+ #timeoutMs;
255
+ constructor(cwd, { timeoutMs = timeoutFromEnv() } = {}) {
256
+ this.#cwd = cwd;
257
+ this.#timeoutMs = timeoutMs;
258
+ ({root: this.#root, main: this.#main} = resolveWrangler(cwd));
259
+ }
260
+ async readConfig(configPath, env) {
261
+ return (await import(pathToFileURL(this.#main).href)).unstable_readConfig(env === void 0 ? { config: configPath } : {
262
+ config: configPath,
263
+ env
264
+ }, { hideWarnings: true });
265
+ }
266
+ /**
267
+ * Runs `wrangler <args>`; resolves with its exit code and combined output. A
268
+ * run that exceeds the timeout is stopped (SIGTERM, then SIGKILL) and resolves
269
+ * with code {@link TIMED_OUT_CODE}, its output so far and a note in `stderr`.
270
+ */
271
+ exec(args) {
272
+ return new Promise((resolve, reject) => {
273
+ const child = spawn(process.execPath, [join(this.#root, "bin", "wrangler.js"), ...args], {
274
+ cwd: this.#cwd,
275
+ env: {
276
+ ...process.env,
277
+ WRANGLER_SEND_METRICS: "false",
278
+ FORCE_COLOR: "0"
279
+ },
280
+ stdio: [
281
+ "ignore",
282
+ "pipe",
283
+ "pipe"
284
+ ]
285
+ });
286
+ let stdout = "";
287
+ let stderr = "";
288
+ let timedOut = false;
289
+ let kill;
290
+ const timer = setTimeout(() => {
291
+ timedOut = true;
292
+ child.kill("SIGTERM");
293
+ kill = setTimeout(() => child.kill("SIGKILL"), 5e3);
294
+ }, this.#timeoutMs);
295
+ child.stdout.on("data", (chunk) => {
296
+ stdout += chunk.toString();
297
+ });
298
+ child.stderr.on("data", (chunk) => {
299
+ stderr += chunk.toString();
300
+ });
301
+ child.on("error", (error) => {
302
+ clearTimeout(timer);
303
+ clearTimeout(kill);
304
+ reject(error);
305
+ });
306
+ child.on("close", (code) => {
307
+ clearTimeout(timer);
308
+ clearTimeout(kill);
309
+ if (!timedOut) {
310
+ resolve({
311
+ code: code ?? 1,
312
+ stdout,
313
+ stderr
314
+ });
315
+ return;
316
+ }
317
+ const note = `wrangler ${args.join(" ")} did not finish within ${this.#timeoutMs / 1e3} s and was stopped.`;
318
+ resolve({
319
+ code: 124,
320
+ stdout,
321
+ stderr: `${stderr}${stderr === "" || stderr.endsWith("\n") ? "" : "\n"}${note}\n`
322
+ });
323
+ });
324
+ });
325
+ }
326
+ };
327
+ // cli/src/run.ts
328
+ const USAGE = `Usage: inbis-db <command> [options]
329
+
330
+ Commands:
331
+ migrate Apply every migration source of each D1 database in declaration
332
+ order (platform bindings first, then the app), then print the status.
333
+ status Print applied and pending migrations per source.
334
+ lock <dir> Add checksums of new migration files to <dir>/migrations.lock.json.
335
+ check-lock <dir> [--base <git-ref>]
336
+ Fail if a locked migration changed or was deleted, a file is not
337
+ locked, or (with --base) a migration locked at <git-ref> changed.
338
+
339
+ migrate refuses to run if a locked migration file has changed.
340
+
341
+ Options:
342
+ --local | --remote Target local (default) or remote D1.
343
+ --env <name> Wrangler environment.
344
+ --config <path> Wrangler config (default: wrangler.jsonc/.json/.toml in cwd).
345
+ --persist-to <dir> Local D1 state directory (wrangler --persist-to).
346
+ `;
347
+ const options = {
348
+ local: { type: "boolean" },
349
+ remote: { type: "boolean" },
350
+ env: { type: "string" },
351
+ config: { type: "string" },
352
+ "persist-to": { type: "string" },
353
+ base: { type: "string" },
354
+ help: {
355
+ type: "boolean",
356
+ short: "h"
357
+ }
358
+ };
359
+ function findConfig(cwd, explicit) {
360
+ if (explicit !== void 0) return resolve(cwd, explicit);
361
+ for (const name of [
362
+ "wrangler.jsonc",
363
+ "wrangler.json",
364
+ "wrangler.toml"
365
+ ]) if (existsSync(join(cwd, name))) return join(cwd, name);
366
+ throw new CliError(`No wrangler config found in ${cwd}. Pass --config <path>.`);
367
+ }
368
+ async function context(values, io) {
369
+ if (values.local === true && values.remote === true) throw new CliError("Pass either --local or --remote, not both.");
370
+ const target = values.remote === true ? "remote" : "local";
371
+ const configPath = findConfig(io.cwd, values.config);
372
+ const wrangler = new Wrangler(io.cwd);
373
+ const config = await wrangler.readConfig(configPath, values.env);
374
+ const targetArgs = [
375
+ `--${target}`,
376
+ "--config",
377
+ configPath
378
+ ];
379
+ if (values.env !== void 0) targetArgs.push("--env", values.env);
380
+ if (values["persist-to"] !== void 0) targetArgs.push("--persist-to", resolve(io.cwd, values["persist-to"]));
381
+ return {
382
+ io,
383
+ wrangler,
384
+ target,
385
+ databases: databaseSources(config, target),
386
+ targetArgs
387
+ };
388
+ }
389
+ async function printStatus(ctx, database) {
390
+ const rows = await readStatus(ctx.wrangler, database, ctx.targetArgs);
391
+ ctx.io.stdout(formatStatus(database.database, ctx.target, rows));
392
+ return rows;
393
+ }
394
+ async function migrate(ctx) {
395
+ let failed = false;
396
+ for (const database of ctx.databases) {
397
+ const drift = database.sources.flatMap((source) => {
398
+ const lock = readLock(source.dir);
399
+ return lock === void 0 ? [] : lockDrift(source.dir, lock).map((problem) => `${source.binding}: ${problem}`);
400
+ });
401
+ if (drift.length > 0) {
402
+ ctx.io.stderr(`Refusing to migrate "${database.database}"; locked migrations differ from ${LOCK_FILE}:\n ${drift.join("\n ")}\n`);
403
+ failed = true;
404
+ }
405
+ for (const source of database.sources) {
406
+ if (failed) break;
407
+ if (migrationFiles(source).length === 0) continue;
408
+ ctx.io.stdout(`Applying ${source.binding} migrations (${source.table})...\n`);
409
+ const result = await ctx.wrangler.exec([
410
+ "d1",
411
+ "migrations",
412
+ "apply",
413
+ source.binding,
414
+ ...ctx.targetArgs
415
+ ]);
416
+ if (result.code !== 0) {
417
+ ctx.io.stderr(`${result.stdout}${result.stderr}\nApplying ${source.binding} migrations failed; later sources were not applied.\n`);
418
+ failed = true;
419
+ }
420
+ }
421
+ await printStatus(ctx, database);
422
+ }
423
+ return failed ? 1 : 0;
424
+ }
425
+ function lockDir(positionals, io) {
426
+ const dir = positionals[1];
427
+ if (dir === void 0) throw new CliError("Pass the migrations directory.");
428
+ return resolve(io.cwd, dir);
429
+ }
430
+ function lock(positionals, io) {
431
+ const { added } = updateLock(lockDir(positionals, io));
432
+ io.stdout(added.length === 0 ? "Lock is up to date.\n" : `Locked ${added.join(", ")}.\n`);
433
+ return 0;
434
+ }
435
+ function checkLock(positionals, values, io) {
436
+ const dir = lockDir(positionals, io);
437
+ const current = readLock(dir);
438
+ if (current === void 0) throw new CliError(`${join(dir, LOCK_FILE)} not found; run inbis-db lock.`);
439
+ const problems = [...lockProblems(dir, current), ...values.base === void 0 ? [] : releasedChanges(dir, current, values.base)];
440
+ if (problems.length > 0) {
441
+ io.stderr(`Migration lock check failed for ${dir}:\n ${problems.join("\n ")}\n`);
442
+ return 1;
443
+ }
444
+ io.stdout(`${Object.keys(current).length} locked migrations unchanged in ${dir}.\n`);
445
+ return 0;
446
+ }
447
+ async function status(ctx) {
448
+ for (const database of ctx.databases) await printStatus(ctx, database);
449
+ return 0;
450
+ }
451
+ /** Runs the inbis-db CLI with `argv` (without node and script); resolves with the exit code. */
452
+ async function run(argv, io) {
453
+ try {
454
+ const { values, positionals } = parseArgs({
455
+ args: argv,
456
+ options,
457
+ allowPositionals: true
458
+ });
459
+ const [command] = positionals;
460
+ if (values.help === true || command === void 0) {
461
+ io.stdout(USAGE);
462
+ return command === void 0 && values.help !== true ? 1 : 0;
463
+ }
464
+ switch (command) {
465
+ case "migrate": return await migrate(await context(values, io));
466
+ case "status": return await status(await context(values, io));
467
+ case "lock": return lock(positionals, io);
468
+ case "check-lock": return checkLock(positionals, values, io);
469
+ default: throw new CliError(`Unknown command "${command}".\n\n${USAGE}`);
470
+ }
471
+ } catch (error) {
472
+ if (error instanceof CliError || error instanceof TypeError && "code" in error) {
473
+ io.stderr(`inbis-db: ${error.message}\n`);
474
+ return 1;
475
+ }
476
+ throw error;
477
+ }
478
+ }
479
+ // cli/src/main.ts
480
+ process.exitCode = await run(process.argv.slice(2), {
481
+ cwd: process.cwd(),
482
+ stdout: (text) => process.stdout.write(text),
483
+ stderr: (text) => process.stderr.write(text)
484
+ });
485
+ export {};
@@ -0,0 +1,64 @@
1
+ import { DrizzleD1Database } from "drizzle-orm/d1";
2
+ import { BatchItem, BatchResponse } from "drizzle-orm/batch";
3
+ type Database<TSchema extends Record<string, unknown> = Record<string, never>> = DrizzleD1Database<TSchema>;
4
+ interface CreateDatabaseOptions<TSchema extends Record<string, unknown>> {
5
+ /** Drizzle tables (and relations) to enable the `db.query.*` API for. */
6
+ schema?: TSchema;
7
+ }
8
+ /**
9
+ * Wraps a D1 binding in a Drizzle instance. Create one per request from the
10
+ * binding you were given; never read bindings from globals.
11
+ */
12
+ export declare function createDatabase<TSchema extends Record<string, unknown> = Record<string, never>>(d1: D1Database, options?: CreateDatabaseOptions<TSchema>): Database<TSchema>;
13
+ /**
14
+ * Runs statements atomically in one D1 round trip. D1 has no interactive
15
+ * transactions, so this is the way to make several writes all-or-nothing.
16
+ *
17
+ * Unique-constraint violations become a 409 `CONFLICT` {@link AppError};
18
+ * other database errors are rethrown unchanged.
19
+ */
20
+ export declare function batch<TSchema extends Record<string, unknown>, const T extends readonly BatchItem<"sqlite">[]>(db: Database<TSchema>, statements: T): Promise<BatchResponse<T>>;
21
+ /** Text primary key filled with a time-ordered UUIDv7 (see `createId` in `@awsmbiz/inbis-core`). */
22
+ export declare function idColumn(name?: string): import("drizzle-orm").HasRuntimeDefault<import("drizzle-orm").HasDefault<import("drizzle-orm").IsPrimaryKey<import("drizzle-orm").NotNull<import("drizzle-orm/sqlite-core").SQLiteTextBuilderInitial<string, [string, ...string[]], number | undefined>>>>>;
23
+ /**
24
+ * `createdAt` / `updatedAt` columns (`created_at` / `updated_at`, epoch
25
+ * milliseconds). Both are set on insert; `updatedAt` is refreshed on update.
26
+ */
27
+ export declare function timestampColumns(): {
28
+ createdAt: import("drizzle-orm").HasRuntimeDefault<import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/sqlite-core").SQLiteTimestampBuilderInitial<"created_at">>>>;
29
+ updatedAt: import("drizzle-orm").HasDefault<import("drizzle-orm").HasRuntimeDefault<import("drizzle-orm").HasDefault<import("drizzle-orm").NotNull<import("drizzle-orm/sqlite-core").SQLiteTimestampBuilderInitial<"updated_at">>>>>;
30
+ };
31
+ /** Raw paging input, typically straight from the query string. */
32
+ interface PageInput {
33
+ limit?: string | number | undefined;
34
+ cursor?: string | undefined;
35
+ }
36
+ interface PageOptions {
37
+ /** Items per page when no limit is given. Default 20. */
38
+ defaultLimit?: number;
39
+ /** Larger limits are clamped to this. Default 100. */
40
+ maxLimit?: number;
41
+ }
42
+ interface PageRequest {
43
+ /** Items to return. */
44
+ limit: number;
45
+ /** Rows to fetch: `limit + 1`, so {@link toPage} can tell whether more exist. */
46
+ fetchLimit: number;
47
+ /** Sort key of the last item on the previous page; fetch rows strictly after it. */
48
+ after: string | undefined;
49
+ }
50
+ interface Page<T> {
51
+ items: T[];
52
+ /** Opaque cursor for the next page, or `null` on the last page. */
53
+ nextCursor: string | null;
54
+ }
55
+ /**
56
+ * Validates paging input for keyset pagination over a unique, ordered key
57
+ * (e.g. a UUIDv7 `id`). Query `WHERE key > after ORDER BY key LIMIT fetchLimit`,
58
+ * then pass the rows to {@link toPage}.
59
+ */
60
+ export declare function parsePageRequest(input: PageInput, options?: PageOptions): PageRequest;
61
+ /** Trims the extra fetched row and derives the next cursor from the last item's key. */
62
+ export declare function toPage<T>(rows: T[], request: PageRequest, keyOf: (row: T) => string): Page<T>;
63
+ export type { CreateDatabaseOptions, Database, Page, PageInput, PageOptions, PageRequest };
64
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,105 @@
1
+ import { AppError, createId, validationFailed } from "@awsmbiz/inbis-core";
2
+ import { integer, text } from "drizzle-orm/sqlite-core";
3
+ import { drizzle } from "drizzle-orm/d1";
4
+ // src/batch.ts
5
+ /**
6
+ * Runs statements atomically in one D1 round trip. D1 has no interactive
7
+ * transactions, so this is the way to make several writes all-or-nothing.
8
+ *
9
+ * Unique-constraint violations become a 409 `CONFLICT` {@link AppError};
10
+ * other database errors are rethrown unchanged.
11
+ */
12
+ async function batch(db, statements) {
13
+ if (statements.length === 0) return [];
14
+ try {
15
+ return await db.batch.bind(db)(statements);
16
+ } catch (error) {
17
+ throw toAppError(error);
18
+ }
19
+ }
20
+ function toAppError(error) {
21
+ for (let current = error; current instanceof Error; current = current.cause) if (current.message.includes("UNIQUE constraint failed")) return new AppError({
22
+ code: "CONFLICT",
23
+ status: 409,
24
+ message: "The resource already exists.",
25
+ cause: error
26
+ });
27
+ return error;
28
+ }
29
+ // src/columns.ts
30
+ /** Text primary key filled with a time-ordered UUIDv7 (see `createId` in `@awsmbiz/inbis-core`). */
31
+ function idColumn(name = "id") {
32
+ return text(name).primaryKey().$defaultFn(() => createId());
33
+ }
34
+ /**
35
+ * `createdAt` / `updatedAt` columns (`created_at` / `updated_at`, epoch
36
+ * milliseconds). Both are set on insert; `updatedAt` is refreshed on update.
37
+ */
38
+ function timestampColumns() {
39
+ return {
40
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
41
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdateFn(() => /* @__PURE__ */ new Date())
42
+ };
43
+ }
44
+ // src/database.ts
45
+ /**
46
+ * Wraps a D1 binding in a Drizzle instance. Create one per request from the
47
+ * binding you were given; never read bindings from globals.
48
+ */
49
+ function createDatabase(d1, options = {}) {
50
+ return options.schema === void 0 ? drizzle(d1) : drizzle(d1, { schema: options.schema });
51
+ }
52
+ // src/pagination.ts
53
+ /**
54
+ * Validates paging input for keyset pagination over a unique, ordered key
55
+ * (e.g. a UUIDv7 `id`). Query `WHERE key > after ORDER BY key LIMIT fetchLimit`,
56
+ * then pass the rows to {@link toPage}.
57
+ */
58
+ function parsePageRequest(input, options = {}) {
59
+ const maxLimit = options.maxLimit ?? 100;
60
+ const limit = Math.min(parseLimit(input.limit, options.defaultLimit ?? 20), maxLimit);
61
+ const after = input.cursor === void 0 || input.cursor === "" ? void 0 : decodeCursor(input.cursor);
62
+ return {
63
+ limit,
64
+ fetchLimit: limit + 1,
65
+ after
66
+ };
67
+ }
68
+ /** Trims the extra fetched row and derives the next cursor from the last item's key. */
69
+ function toPage(rows, request, keyOf) {
70
+ const items = rows.slice(0, request.limit);
71
+ const last = items.at(-1);
72
+ return {
73
+ items,
74
+ nextCursor: rows.length > request.limit && last !== void 0 ? encodeCursor(keyOf(last)) : null
75
+ };
76
+ }
77
+ function parseLimit(raw, fallback) {
78
+ if (raw === void 0 || raw === "") return fallback;
79
+ const value = typeof raw === "number" ? raw : Number(raw);
80
+ if (!Number.isInteger(value) || value < 1) throw validationFailed([{
81
+ path: ["limit"],
82
+ message: "Must be a positive integer."
83
+ }]);
84
+ return value;
85
+ }
86
+ function encodeCursor(key) {
87
+ const bytes = new TextEncoder().encode(JSON.stringify({ k: key }));
88
+ return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
89
+ }
90
+ function decodeCursor(cursor) {
91
+ try {
92
+ const binary = atob(cursor.replaceAll("-", "+").replaceAll("_", "/"));
93
+ const json = new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
94
+ const parsed = JSON.parse(json);
95
+ if (typeof parsed === "object" && parsed !== null && "k" in parsed && typeof parsed.k === "string") return parsed.k;
96
+ } catch {}
97
+ throw new AppError({
98
+ code: "INVALID_CURSOR",
99
+ status: 400,
100
+ message: "The page cursor is invalid."
101
+ });
102
+ }
103
+ export { batch, createDatabase, idColumn, parsePageRequest, timestampColumns, toPage };
104
+
105
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/batch.ts","../src/columns.ts","../src/database.ts","../src/pagination.ts"],"sourcesContent":["import { AppError } from \"@awsmbiz/inbis-core\";\nimport type { BatchItem, BatchResponse } from \"drizzle-orm/batch\";\nimport type { Database } from \"./database.js\";\n\n/**\n * Runs statements atomically in one D1 round trip. D1 has no interactive\n * transactions, so this is the way to make several writes all-or-nothing.\n *\n * Unique-constraint violations become a 409 `CONFLICT` {@link AppError};\n * other database errors are rethrown unchanged.\n */\nexport async function batch<\n TSchema extends Record<string, unknown>,\n const T extends readonly BatchItem<\"sqlite\">[],\n>(db: Database<TSchema>, statements: T): Promise<BatchResponse<T>> {\n if (statements.length === 0) return [] as BatchResponse<T>;\n try {\n const run = db.batch.bind(db) as unknown as (items: T) => Promise<BatchResponse<T>>;\n return await run(statements);\n } catch (error) {\n throw toAppError(error);\n }\n}\n\nfunction toAppError(error: unknown): unknown {\n for (let current: unknown = error; current instanceof Error; current = current.cause) {\n if (current.message.includes(\"UNIQUE constraint failed\")) {\n return new AppError({\n code: \"CONFLICT\",\n status: 409,\n message: \"The resource already exists.\",\n cause: error,\n });\n }\n }\n return error;\n}\n","import { createId } from \"@awsmbiz/inbis-core\";\nimport { integer, text } from \"drizzle-orm/sqlite-core\";\n\n/** Text primary key filled with a time-ordered UUIDv7 (see `createId` in `@awsmbiz/inbis-core`). */\nexport function idColumn(name = \"id\") {\n return text(name)\n .primaryKey()\n .$defaultFn(() => createId());\n}\n\n/**\n * `createdAt` / `updatedAt` columns (`created_at` / `updated_at`, epoch\n * milliseconds). Both are set on insert; `updatedAt` is refreshed on update.\n */\nexport function timestampColumns() {\n return {\n createdAt: integer(\"created_at\", { mode: \"timestamp_ms\" })\n .notNull()\n .$defaultFn(() => new Date()),\n updatedAt: integer(\"updated_at\", { mode: \"timestamp_ms\" })\n .notNull()\n .$defaultFn(() => new Date())\n .$onUpdateFn(() => new Date()),\n };\n}\n","import { drizzle, type DrizzleD1Database } from \"drizzle-orm/d1\";\n\nexport type Database<TSchema extends Record<string, unknown> = Record<string, never>> =\n DrizzleD1Database<TSchema>;\n\nexport interface CreateDatabaseOptions<TSchema extends Record<string, unknown>> {\n /** Drizzle tables (and relations) to enable the `db.query.*` API for. */\n schema?: TSchema;\n}\n\n/**\n * Wraps a D1 binding in a Drizzle instance. Create one per request from the\n * binding you were given; never read bindings from globals.\n */\nexport function createDatabase<TSchema extends Record<string, unknown> = Record<string, never>>(\n d1: D1Database,\n options: CreateDatabaseOptions<TSchema> = {},\n): Database<TSchema> {\n return options.schema === undefined\n ? (drizzle(d1))\n : drizzle(d1, { schema: options.schema });\n}\n","import { AppError, validationFailed } from \"@awsmbiz/inbis-core\";\n\n/** Raw paging input, typically straight from the query string. */\nexport interface PageInput {\n limit?: string | number | undefined;\n cursor?: string | undefined;\n}\n\nexport interface PageOptions {\n /** Items per page when no limit is given. Default 20. */\n defaultLimit?: number;\n /** Larger limits are clamped to this. Default 100. */\n maxLimit?: number;\n}\n\nexport interface PageRequest {\n /** Items to return. */\n limit: number;\n /** Rows to fetch: `limit + 1`, so {@link toPage} can tell whether more exist. */\n fetchLimit: number;\n /** Sort key of the last item on the previous page; fetch rows strictly after it. */\n after: string | undefined;\n}\n\nexport interface Page<T> {\n items: T[];\n /** Opaque cursor for the next page, or `null` on the last page. */\n nextCursor: string | null;\n}\n\n/**\n * Validates paging input for keyset pagination over a unique, ordered key\n * (e.g. a UUIDv7 `id`). Query `WHERE key > after ORDER BY key LIMIT fetchLimit`,\n * then pass the rows to {@link toPage}.\n */\nexport function parsePageRequest(input: PageInput, options: PageOptions = {}): PageRequest {\n const maxLimit = options.maxLimit ?? 100;\n const limit = Math.min(parseLimit(input.limit, options.defaultLimit ?? 20), maxLimit);\n const after = input.cursor === undefined || input.cursor === \"\" ? undefined : decodeCursor(input.cursor);\n return { limit, fetchLimit: limit + 1, after };\n}\n\n/** Trims the extra fetched row and derives the next cursor from the last item's key. */\nexport function toPage<T>(rows: T[], request: PageRequest, keyOf: (row: T) => string): Page<T> {\n const items = rows.slice(0, request.limit);\n const last = items.at(-1);\n const hasMore = rows.length > request.limit && last !== undefined;\n return { items, nextCursor: hasMore ? encodeCursor(keyOf(last)) : null };\n}\n\nfunction parseLimit(raw: string | number | undefined, fallback: number): number {\n if (raw === undefined || raw === \"\") return fallback;\n const value = typeof raw === \"number\" ? raw : Number(raw);\n if (!Number.isInteger(value) || value < 1) {\n throw validationFailed([{ path: [\"limit\"], message: \"Must be a positive integer.\" }]);\n }\n return value;\n}\n\nfunction encodeCursor(key: string): string {\n const bytes = new TextEncoder().encode(JSON.stringify({ k: key }));\n return btoa(String.fromCharCode(...bytes))\n .replaceAll(\"+\", \"-\")\n .replaceAll(\"/\", \"_\")\n .replace(/=+$/, \"\");\n}\n\nfunction decodeCursor(cursor: string): string {\n try {\n const binary = atob(cursor.replaceAll(\"-\", \"+\").replaceAll(\"_\", \"/\"));\n const json = new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));\n const parsed: unknown = JSON.parse(json);\n if (typeof parsed === \"object\" && parsed !== null && \"k\" in parsed && typeof parsed.k === \"string\") {\n return parsed.k;\n }\n } catch {\n // fall through to the typed error below\n }\n throw new AppError({ code: \"INVALID_CURSOR\", status: 400, message: \"The page cursor is invalid.\" });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@awsmbiz/inbis-db",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "D1 + Drizzle helpers for Inbis packages: database factory, column and pagination helpers, batch wrapper, and the inbis-db migration CLI.",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/awsmbiz/inbis-platform.git",
9
+ "directory": "packages/db"
10
+ },
11
+ "publishConfig": {
12
+ "registry": "https://registry.npmjs.org/",
13
+ "access": "public"
14
+ },
15
+ "type": "module",
16
+ "sideEffects": false,
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "dependencies": {
28
+ "@awsmbiz/inbis-core": "0.2.0"
29
+ },
30
+ "peerDependencies": {
31
+ "drizzle-orm": "^0.45.4",
32
+ "wrangler": "^4.156.0"
33
+ },
34
+ "devDependencies": {
35
+ "@arethetypeswrong/cli": "0.18.5",
36
+ "@cloudflare/vitest-plugin": "1.4.11",
37
+ "@cloudflare/workers-types": "5.20261030.1",
38
+ "@awsmbiz/inbis-eslint-config": "0.2.0",
39
+ "@awsmbiz/inbis-tsconfig": "0.2.0",
40
+ "@types/node": "24.19.2",
41
+ "drizzle-orm": "0.45.4",
42
+ "eslint": "10.14.0",
43
+ "publint": "0.3.25",
44
+ "tsdown": "0.23.4",
45
+ "typescript": "6.0.3",
46
+ "vitest": "5.0.5",
47
+ "wrangler": "4.156.0"
48
+ },
49
+ "bin": {
50
+ "inbis-db": "./dist/cli.mjs"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "wrangler": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "scripts": {
58
+ "build": "tsdown",
59
+ "typecheck": "tsc && tsc -p cli",
60
+ "lint": "eslint . --max-warnings 0",
61
+ "test": "vitest run && vitest run --config cli/vitest.config.ts",
62
+ "check:exports": "publint --strict && attw --pack . --profile esm-only"
63
+ }
6
64
  }