@lunora/d1 1.0.0-alpha.3 → 1.0.0-alpha.31

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.
@@ -1,21 +1,23 @@
1
1
  /**
2
- * The Lunora **D1 dialect** the single source of truth for how `.global()`
3
- * tables are physically shaped in D1.
2
+ * Canonical SQL identifier quoter shared by `@lunora/d1` and `@lunora/do`.
4
3
  *
5
- * Both the runtime (`runD1GlobalTableMigrations` in `d1-ctx-db.ts`, which
6
- * auto-provisions tables) and the `lunora migrate generate` SQL emitter
7
- * (`@lunora/cli`'s `migration-diff.ts`) derive their DDL from these helpers, so
8
- * the table a migration writes is byte-identical to the one the runtime creates.
9
- * Previously each encoded the dialect independently and a comment begged them to
10
- * stay "in lockstep"; this module makes the lockstep structural.
4
+ * Double-quotes a SQL identifier and escapes any embedded double quotes by
5
+ * doubling them (`"` → `""`) the ANSI/SQLite/Postgres rule. This is a
6
+ * security-relevant primitive (it is the sole defense against identifier
7
+ * injection wherever a table/column name is spliced into raw SQL), so it must
8
+ * have exactly ONE definition rather than byte-identical copies that can drift.
11
9
  *
12
- * Exposed as the `@lunora/d1/dialect` subpath. Pure no runtime dependencies
13
- * so the CLI can import it without pulling the D1 runtime.
10
+ * Like `shared/stable-key.ts`, it is deliberately **not** a package: `@lunora/d1`
11
+ * and `@lunora/do` sit on the same tier with no lower-level package to host it,
12
+ * so each imports this file by relative path and the bundler (packem/rollup)
13
+ * inlines it — no runtime dependency edge, duplicated only in emitted output.
14
+ * Keep it genuinely zero-dependency (relative/built-in imports only) or inlining
15
+ * breaks. Consumers must drop `outDir`/`rootDir` from their `tsconfig.json` (a
16
+ * set `rootDir` raises TS6059 for this out-of-package file under `tsc --noEmit`).
14
17
  */
18
+ declare const quoteIdentifier: (name: string) => string;
15
19
  /** SQLite column type affinities Lunora emits. */
16
20
  type SqlAffinity = "BLOB" | "INTEGER" | "REAL" | "TEXT";
17
- /** Double-quote (and escape) a SQL identifier. */
18
- declare const quoteIdentifier: (name: string) => string;
19
21
  /**
20
22
  * SQLite affinity for a column by its validator `kind`, chosen so the value the
21
23
  * D1 layer serializes round-trips intact:
package/dist/dialect.d.ts CHANGED
@@ -1,21 +1,23 @@
1
1
  /**
2
- * The Lunora **D1 dialect** the single source of truth for how `.global()`
3
- * tables are physically shaped in D1.
2
+ * Canonical SQL identifier quoter shared by `@lunora/d1` and `@lunora/do`.
4
3
  *
5
- * Both the runtime (`runD1GlobalTableMigrations` in `d1-ctx-db.ts`, which
6
- * auto-provisions tables) and the `lunora migrate generate` SQL emitter
7
- * (`@lunora/cli`'s `migration-diff.ts`) derive their DDL from these helpers, so
8
- * the table a migration writes is byte-identical to the one the runtime creates.
9
- * Previously each encoded the dialect independently and a comment begged them to
10
- * stay "in lockstep"; this module makes the lockstep structural.
4
+ * Double-quotes a SQL identifier and escapes any embedded double quotes by
5
+ * doubling them (`"` → `""`) the ANSI/SQLite/Postgres rule. This is a
6
+ * security-relevant primitive (it is the sole defense against identifier
7
+ * injection wherever a table/column name is spliced into raw SQL), so it must
8
+ * have exactly ONE definition rather than byte-identical copies that can drift.
11
9
  *
12
- * Exposed as the `@lunora/d1/dialect` subpath. Pure no runtime dependencies
13
- * so the CLI can import it without pulling the D1 runtime.
10
+ * Like `shared/stable-key.ts`, it is deliberately **not** a package: `@lunora/d1`
11
+ * and `@lunora/do` sit on the same tier with no lower-level package to host it,
12
+ * so each imports this file by relative path and the bundler (packem/rollup)
13
+ * inlines it — no runtime dependency edge, duplicated only in emitted output.
14
+ * Keep it genuinely zero-dependency (relative/built-in imports only) or inlining
15
+ * breaks. Consumers must drop `outDir`/`rootDir` from their `tsconfig.json` (a
16
+ * set `rootDir` raises TS6059 for this out-of-package file under `tsc --noEmit`).
14
17
  */
18
+ declare const quoteIdentifier: (name: string) => string;
15
19
  /** SQLite column type affinities Lunora emits. */
16
20
  type SqlAffinity = "BLOB" | "INTEGER" | "REAL" | "TEXT";
17
- /** Double-quote (and escape) a SQL identifier. */
18
- declare const quoteIdentifier: (name: string) => string;
19
21
  /**
20
22
  * SQLite affinity for a column by its validator `kind`, chosen so the value the
21
23
  * D1 layer serializes round-trips intact:
package/dist/dialect.mjs CHANGED
@@ -1,4 +1,5 @@
1
- const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
1
+ import { quoteIdentifier } from './packem_shared/quoteIdentifier-B-ZeSe1V.mjs';
2
+
2
3
  const sqlAffinityForKind = (kind) => {
3
4
  switch (kind) {
4
5
  case "boolean": {
package/dist/index.d.mts CHANGED
@@ -52,11 +52,21 @@ interface ExportGlobalArgs {
52
52
  tables?: ReadonlyArray<string>;
53
53
  }
54
54
  /**
55
- * Yield rows from every requested `.global()` table in batches. Uses
56
- * `LIMIT ?/OFFSET ?` because D1 globals don't have a stable keyset abstraction
57
- * here (the writer's `findMany` does, but at the cost of routing through the
58
- * full validator pipeline; for a snapshot stream a plain offset scan is
59
- * sufficient and predictable).
55
+ * Yield rows from every requested `.global()` table in batches.
56
+ *
57
+ * Keyset-paginates on the physical primary key (`id` every `.global()` table
58
+ * carries it, see `frameworkColumnDdl`): `WHERE "id" > ? ORDER BY "id" LIMIT ?`,
59
+ * carrying the last id forward. A plain `LIMIT/OFFSET` scan would be wrong here —
60
+ * SQLite gives no ordering guarantee for an unordered SELECT and each page is a
61
+ * separate query, so pages could overlap or skip rows (and any concurrent
62
+ * insert/delete would shift offsets and silently drop/duplicate rows in the
63
+ * snapshot). Keyset paging is deterministic under concurrent writes and avoids
64
+ * O(n^2) OFFSET scans on large tables.
65
+ *
66
+ * Tables are provisioned first (idempotent `CREATE … IF NOT EXISTS`): `.global()`
67
+ * tables are created lazily on first write, so a fresh deployment — or any table
68
+ * never written — would otherwise abort the stream with a raw `no such table`
69
+ * instead of exporting it as empty.
60
70
  */
61
71
  declare const exportGlobalRows: (exec: SqlCtxExec, schema: SchemaLike, args: ExportGlobalArgs) => AsyncGenerator<ExportRow, void, undefined>;
62
72
  interface ImportGlobalArgs {
@@ -90,7 +100,6 @@ declare const importGlobalRows: (writer: DatabaseWriterLike, schema: SchemaLike,
90
100
  */
91
101
  interface D1DatabaseLike {
92
102
  batch?: (statements: D1PreparedStatementLike[]) => Promise<unknown[]>;
93
- exec?: (sql: string) => Promise<unknown>;
94
103
  prepare: (sql: string) => D1PreparedStatementLike;
95
104
  withSession: (bookmark?: string) => D1SessionLike;
96
105
  }
package/dist/index.d.ts CHANGED
@@ -52,11 +52,21 @@ interface ExportGlobalArgs {
52
52
  tables?: ReadonlyArray<string>;
53
53
  }
54
54
  /**
55
- * Yield rows from every requested `.global()` table in batches. Uses
56
- * `LIMIT ?/OFFSET ?` because D1 globals don't have a stable keyset abstraction
57
- * here (the writer's `findMany` does, but at the cost of routing through the
58
- * full validator pipeline; for a snapshot stream a plain offset scan is
59
- * sufficient and predictable).
55
+ * Yield rows from every requested `.global()` table in batches.
56
+ *
57
+ * Keyset-paginates on the physical primary key (`id` every `.global()` table
58
+ * carries it, see `frameworkColumnDdl`): `WHERE "id" > ? ORDER BY "id" LIMIT ?`,
59
+ * carrying the last id forward. A plain `LIMIT/OFFSET` scan would be wrong here —
60
+ * SQLite gives no ordering guarantee for an unordered SELECT and each page is a
61
+ * separate query, so pages could overlap or skip rows (and any concurrent
62
+ * insert/delete would shift offsets and silently drop/duplicate rows in the
63
+ * snapshot). Keyset paging is deterministic under concurrent writes and avoids
64
+ * O(n^2) OFFSET scans on large tables.
65
+ *
66
+ * Tables are provisioned first (idempotent `CREATE … IF NOT EXISTS`): `.global()`
67
+ * tables are created lazily on first write, so a fresh deployment — or any table
68
+ * never written — would otherwise abort the stream with a raw `no such table`
69
+ * instead of exporting it as empty.
60
70
  */
61
71
  declare const exportGlobalRows: (exec: SqlCtxExec, schema: SchemaLike, args: ExportGlobalArgs) => AsyncGenerator<ExportRow, void, undefined>;
62
72
  interface ImportGlobalArgs {
@@ -90,7 +100,6 @@ declare const importGlobalRows: (writer: DatabaseWriterLike, schema: SchemaLike,
90
100
  */
91
101
  interface D1DatabaseLike {
92
102
  batch?: (statements: D1PreparedStatementLike[]) => Promise<unknown[]>;
93
- exec?: (sql: string) => Promise<unknown>;
94
103
  prepare: (sql: string) => D1PreparedStatementLike;
95
104
  withSession: (bookmark?: string) => D1SessionLike;
96
105
  }
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- export { exportGlobalRows, importGlobalRows, selectGlobalTables } from './packem_shared/exportGlobalRows-BGCPm_nA.mjs';
2
- export { D1Client, D1Session } from './packem_shared/D1Client-DA3flo1o.mjs';
1
+ export { exportGlobalRows, importGlobalRows, selectGlobalTables } from './packem_shared/exportGlobalRows-q0RgCLPM.mjs';
2
+ export { D1Client, D1Session } from './packem_shared/D1Client-DSj8p7c4.mjs';
3
3
  export { createD1CtxDb, readD1CdcChanges, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, trimD1CdcChanges } from './packem_shared/createD1CtxDb-BMR8J0dT.mjs';
4
- export { facetGlobalColumn, listGlobalTables, readGlobalTablePage } from './packem_shared/facetGlobalColumn-C6u_WMIY.mjs';
5
- export { MigrationRunner } from './packem_shared/MigrationRunner-BkEwQ-Ya.mjs';
4
+ export { facetGlobalColumn, listGlobalTables, readGlobalTablePage } from './packem_shared/facetGlobalColumn-DnglIe4P.mjs';
5
+ export { MigrationRunner } from './packem_shared/MigrationRunner-A-o1LTKj.mjs';
6
6
  export { default as sqliteDialect } from './packem_shared/sqliteDialect-DqYnHPuu.mjs';
7
7
  export { createSqlCtxDb } from '@lunora/sql-store';
@@ -1,6 +1,28 @@
1
1
  import { drizzle } from 'drizzle-orm/d1';
2
2
 
3
+ const evictOldestEntry = (map, capacity) => {
4
+ if (map.size < capacity) {
5
+ return;
6
+ }
7
+ const oldest = map.keys().next().value;
8
+ if (oldest !== void 0) {
9
+ map.delete(oldest);
10
+ }
11
+ };
12
+
3
13
  const STMT_CACHE_CAPACITY = 256;
14
+ const prepareCached = (cache, prepare, sql) => {
15
+ const cached = cache.get(sql);
16
+ if (cached) {
17
+ cache.delete(sql);
18
+ cache.set(sql, cached);
19
+ return cached;
20
+ }
21
+ const stmt = prepare(sql);
22
+ evictOldestEntry(cache, STMT_CACHE_CAPACITY);
23
+ cache.set(sql, stmt);
24
+ return stmt;
25
+ };
4
26
  const D1_FIRST_UNCONSTRAINED = "first-unconstrained";
5
27
  class D1Session {
6
28
  session;
@@ -10,21 +32,7 @@ class D1Session {
10
32
  this.session = session;
11
33
  }
12
34
  prepare(sql) {
13
- const cached = this.stmtCache.get(sql);
14
- if (cached) {
15
- this.stmtCache.delete(sql);
16
- this.stmtCache.set(sql, cached);
17
- return cached;
18
- }
19
- const stmt = this.session.prepare(sql);
20
- if (this.stmtCache.size >= STMT_CACHE_CAPACITY) {
21
- const oldest = this.stmtCache.keys().next().value;
22
- if (oldest !== void 0) {
23
- this.stmtCache.delete(oldest);
24
- }
25
- }
26
- this.stmtCache.set(sql, stmt);
27
- return stmt;
35
+ return prepareCached(this.stmtCache, (text) => this.session.prepare(text), sql);
28
36
  }
29
37
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- T types the result rows for the caller and is forwarded to the prepared statement.
30
38
  async run(sql, ...binds) {
@@ -84,21 +92,7 @@ class D1Client {
84
92
  * even when the previous caller already called `.bind(...).run()`.
85
93
  */
86
94
  prepare(sql) {
87
- const cached = this.stmtCache.get(sql);
88
- if (cached) {
89
- this.stmtCache.delete(sql);
90
- this.stmtCache.set(sql, cached);
91
- return cached;
92
- }
93
- const stmt = this.db.prepare(sql);
94
- if (this.stmtCache.size >= STMT_CACHE_CAPACITY) {
95
- const oldest = this.stmtCache.keys().next().value;
96
- if (oldest !== void 0) {
97
- this.stmtCache.delete(oldest);
98
- }
99
- }
100
- this.stmtCache.set(sql, stmt);
101
- return stmt;
95
+ return prepareCached(this.stmtCache, (text) => this.db.prepare(text), sql);
102
96
  }
103
97
  /**
104
98
  * Drizzle handle over the bare `env.DB` binding. Used for typed queries
@@ -1,18 +1,19 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  import { sql } from 'drizzle-orm';
2
- import { D1Client } from './D1Client-DA3flo1o.mjs';
3
+ import { D1Client } from './D1Client-DSj8p7c4.mjs';
3
4
 
4
5
  const TRACKING_TABLE_NAME = "__drizzle_migrations";
5
- const TRACKING_TABLE_DDL = `CREATE TABLE IF NOT EXISTS ${TRACKING_TABLE_NAME} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL, created_at NUMERIC)`;
6
+ const TRACKING_TABLE_DDL = `CREATE TABLE IF NOT EXISTS ${TRACKING_TABLE_NAME} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL UNIQUE, created_at NUMERIC)`;
6
7
  const WHITESPACE_RE = /\s/u;
7
- const TRAILING_SEMICOLON_RE = /;\s*$/u;
8
8
  const SHA256_HEX_RE = /^[0-9a-f]{64}$/u;
9
+ const TRACKING_HASH_UNIQUE_RE = new RegExp(String.raw`UNIQUE constraint failed:\s*${TRACKING_TABLE_NAME}\.hash`, "iu");
9
10
  const assertSingleStatement = (migration) => {
10
11
  const text = migration.sql;
11
12
  let inSingle = false;
12
13
  let inDouble = false;
13
14
  let inLineComment = false;
14
15
  let inBlockComment = false;
15
- let seenStatement = false;
16
+ let terminatorIndex;
16
17
  for (let index = 0; index < text.length; index += 1) {
17
18
  const character = text[index];
18
19
  const next = text[index + 1];
@@ -49,14 +50,6 @@ const assertSingleStatement = (migration) => {
49
50
  }
50
51
  continue;
51
52
  }
52
- if (character === "'") {
53
- inSingle = true;
54
- continue;
55
- }
56
- if (character === '"') {
57
- inDouble = true;
58
- continue;
59
- }
60
53
  if (character === "-" && next === "-") {
61
54
  inLineComment = true;
62
55
  index += 1;
@@ -67,16 +60,28 @@ const assertSingleStatement = (migration) => {
67
60
  index += 1;
68
61
  continue;
69
62
  }
70
- if (character === ";") {
71
- seenStatement = true;
63
+ if (terminatorIndex !== void 0) {
64
+ if (character !== void 0 && !WHITESPACE_RE.test(character)) {
65
+ throw new LunoraError(
66
+ "INTERNAL",
67
+ `Migration "${migration.name}" (v${String(migration.version)}) contains more than one SQL statement. Split it into separate migrations — batch() runs them atomically.`
68
+ );
69
+ }
72
70
  continue;
73
71
  }
74
- if (seenStatement && character !== void 0 && !WHITESPACE_RE.test(character)) {
75
- throw new Error(
76
- `Migration "${migration.name}" (v${String(migration.version)}) contains more than one SQL statement. Split it into separate migrations — batch() runs them atomically.`
77
- );
72
+ if (character === "'") {
73
+ inSingle = true;
74
+ continue;
75
+ }
76
+ if (character === '"') {
77
+ inDouble = true;
78
+ continue;
79
+ }
80
+ if (character === ";") {
81
+ terminatorIndex = index;
78
82
  }
79
83
  }
84
+ return terminatorIndex;
80
85
  };
81
86
  const hashMigration = async (text) => {
82
87
  const bytes = new TextEncoder().encode(text);
@@ -110,26 +115,38 @@ class MigrationRunner {
110
115
  skipped.push({ name: migration.name, version: migration.version });
111
116
  continue;
112
117
  }
113
- await this.applyOne(migration, hash);
114
- applied.push({ name: migration.name, version: migration.version });
118
+ const didApply = await this.applyOne(migration, hash);
119
+ if (didApply) {
120
+ applied.push({ name: migration.name, version: migration.version });
121
+ } else {
122
+ skipped.push({ name: migration.name, version: migration.version });
123
+ }
115
124
  }
116
125
  return { applied, skipped };
117
126
  }
118
127
  async applyOne(migration, hash) {
119
- assertSingleStatement(migration);
120
- const statementText = migration.sql.replace(TRAILING_SEMICOLON_RE, "").trim();
128
+ const terminatorIndex = assertSingleStatement(migration);
129
+ const statementText = (terminatorIndex === void 0 ? migration.sql : migration.sql.slice(0, terminatorIndex)).trim();
121
130
  if (!SHA256_HEX_RE.test(hash)) {
122
- throw new Error(`migration "${migration.name}" produced a non-hex hash; refusing to inline into SQL`);
131
+ throw new LunoraError("INTERNAL", `migration "${migration.name}" produced a non-hex hash; refusing to inline into SQL`);
123
132
  }
124
133
  const trackingInsertSql = `INSERT INTO ${TRACKING_TABLE_NAME} (hash, created_at) VALUES ('${hash}', ${String(Date.now())})`;
125
134
  const items = [this.client.drizzle.run(sql.raw(statementText)), this.client.drizzle.run(sql.raw(trackingInsertSql))];
126
- await this.client.batch(items);
135
+ try {
136
+ await this.client.batch(items);
137
+ } catch (error) {
138
+ if (TRACKING_HASH_UNIQUE_RE.test(error instanceof Error ? error.message : String(error))) {
139
+ return false;
140
+ }
141
+ throw error;
142
+ }
143
+ return true;
127
144
  }
128
145
  assertUniqueVersions() {
129
146
  const seen = /* @__PURE__ */ new Set();
130
147
  for (const m of this.migrations) {
131
148
  if (seen.has(m.version)) {
132
- throw new Error(`Duplicate migration version ${String(m.version)}`);
149
+ throw new LunoraError("INTERNAL", `Duplicate migration version ${String(m.version)}`);
133
150
  }
134
151
  seen.add(m.version);
135
152
  }
@@ -139,7 +156,10 @@ class MigrationRunner {
139
156
  for (const m of this.migrations) {
140
157
  const previousVersion = seen.get(m.sql);
141
158
  if (previousVersion !== void 0) {
142
- throw new Error(`Migrations ${String(previousVersion)} and ${String(m.version)} have identical SQL — bump the content, not just the version.`);
159
+ throw new LunoraError(
160
+ "INTERNAL",
161
+ `Migrations ${String(previousVersion)} and ${String(m.version)} have identical SQL — bump the content, not just the version.`
162
+ );
143
163
  }
144
164
  seen.set(m.sql, m.version);
145
165
  }
@@ -1,7 +1,9 @@
1
+ import { toErrorBody } from '@lunora/errors';
2
+ import { runD1GlobalTableMigrations } from './createD1CtxDb-BMR8J0dT.mjs';
3
+ import { quoteIdentifier } from './quoteIdentifier-B-ZeSe1V.mjs';
1
4
  import { decodeGlobalRow } from '@lunora/sql-store';
2
5
 
3
6
  const DEFAULT_BATCH_SIZE = 200;
4
- const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
5
7
  const selectGlobalTables = (schema, requested) => {
6
8
  const isGlobal = (table) => schema.tables[table]?.shardMode?.kind === "global";
7
9
  if (requested && requested.length > 0) {
@@ -19,16 +21,21 @@ const decodeRow = (schema, table, row) => {
19
21
  const exportGlobalRows = async function* (exec, schema, args) {
20
22
  const tables = selectGlobalTables(schema, args.tables);
21
23
  const batchSize = args.batchSize ?? DEFAULT_BATCH_SIZE;
24
+ await runD1GlobalTableMigrations(exec, schema);
22
25
  for (const table of tables) {
23
- let offset = 0;
26
+ const quoted = quoteIdentifier(table);
27
+ let lastId;
24
28
  let hasMore = true;
25
29
  while (hasMore) {
26
- const rows = await exec.all(`SELECT * FROM ${quoteIdentifier(table)} LIMIT ? OFFSET ?`, [batchSize, offset]);
30
+ const rows = lastId === void 0 ? await exec.all(`SELECT * FROM ${quoted} ORDER BY "id" LIMIT ?`, [batchSize]) : await exec.all(`SELECT * FROM ${quoted} WHERE "id" > ? ORDER BY "id" LIMIT ?`, [lastId, batchSize]);
27
31
  for (const row of rows) {
28
32
  yield { doc: decodeRow(schema, table, row), table };
29
33
  }
34
+ const last = rows.at(-1);
35
+ if (last !== void 0) {
36
+ lastId = String(last["id"]);
37
+ }
30
38
  hasMore = rows.length === batchSize;
31
- offset += rows.length;
32
39
  }
33
40
  }
34
41
  };
@@ -88,9 +95,8 @@ const importOneRow = async (writer, schema, args, row, line) => {
88
95
  await writer.insert(table, doc, { allowExplicitId: true });
89
96
  return { inserted: table, kind: "inserted" };
90
97
  } catch (error) {
91
- const code = error.code ?? "INSERT_FAILED";
92
- const message = error instanceof Error ? error.message : String(error);
93
- return { error: { code, line, message, table }, kind: "error" };
98
+ const { body } = toErrorBody(error, { fallbackCode: "INSERT_FAILED" });
99
+ return { error: { code: body.code, line, message: body.message, table }, kind: "error" };
94
100
  }
95
101
  };
96
102
  const importGlobalRows = async (writer, schema, args) => {
@@ -1,4 +1,6 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  import { runD1GlobalTableMigrations } from './createD1CtxDb-BMR8J0dT.mjs';
3
+ import { quoteIdentifier } from './quoteIdentifier-B-ZeSe1V.mjs';
2
4
  import { decodeGlobalRow } from '@lunora/sql-store';
3
5
 
4
6
  const ensureGlobalTables = (exec, schema) => runD1GlobalTableMigrations(exec, schema);
@@ -6,7 +8,6 @@ const DEFAULT_PAGE_SIZE = 50;
6
8
  const MAX_PAGE_SIZE = 500;
7
9
  const DEFAULT_FACET_LIMIT = 30;
8
10
  const MAX_FACET_LIMIT = 200;
9
- const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
10
11
  const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
11
12
  const INTERNAL_TABLE = /^sqlite_|^_cf_|^d1_|^__cdc|__agg_|__rank_|__fts_/u;
12
13
  const isInternalTable = (name) => INTERNAL_TABLE.test(name);
@@ -28,7 +29,10 @@ const buildEqPredicate = (schema, table, displayColumns, filters) => {
28
29
  const params = [];
29
30
  for (const filter of filters) {
30
31
  if (!displayColumns.includes(filter.column)) {
31
- throw Object.assign(new Error(`unknown column: ${filter.column}`), { code: "UNKNOWN_COLUMN", name: "LunoraError", status: 404 });
32
+ throw new LunoraError("UNKNOWN_COLUMN", `unknown column: ${filter.column}`, { status: 404 });
33
+ }
34
+ if (schema.tables[table] === void 0 && SENSITIVE_COLUMN.test(filter.column)) {
35
+ throw new LunoraError("FORBIDDEN", `cannot filter on a redacted column: ${filter.column}`, { status: 403 });
32
36
  }
33
37
  const quoted = quoteIdentifier(physicalColumnName(schema, table, filter.column));
34
38
  if (filter.value === null || filter.value === void 0) {
@@ -89,7 +93,7 @@ const readGlobalTablePage = async (exec, schema, options) => {
89
93
  await ensureGlobalTables(exec, schema);
90
94
  const tableNames = await listTableNames(exec);
91
95
  if (!tableNames.includes(table)) {
92
- throw Object.assign(new Error(`unknown table: ${table}`), { code: "UNKNOWN_TABLE", name: "LunoraError", status: 404 });
96
+ throw new LunoraError("UNKNOWN_TABLE", `unknown table: ${table}`, { status: 404 });
93
97
  }
94
98
  const limit = clamp(Math.trunc(options.limit ?? DEFAULT_PAGE_SIZE), 1, MAX_PAGE_SIZE);
95
99
  const offset = Math.max(0, Math.trunc(options.offset ?? 0));
@@ -109,11 +113,11 @@ const facetGlobalColumn = async (exec, schema, options) => {
109
113
  await ensureGlobalTables(exec, schema);
110
114
  const tableNames = await listTableNames(exec);
111
115
  if (!tableNames.includes(table)) {
112
- throw Object.assign(new Error(`unknown table: ${table}`), { code: "UNKNOWN_TABLE", name: "LunoraError", status: 404 });
116
+ throw new LunoraError("UNKNOWN_TABLE", `unknown table: ${table}`, { status: 404 });
113
117
  }
114
118
  const columns = await resolveColumns(exec, schema, table);
115
119
  if (!columns.includes(column)) {
116
- throw Object.assign(new Error(`unknown column: ${column}`), { code: "UNKNOWN_COLUMN", name: "LunoraError", status: 404 });
120
+ throw new LunoraError("UNKNOWN_COLUMN", `unknown column: ${column}`, { status: 404 });
117
121
  }
118
122
  const quoted = quoteIdentifier(table);
119
123
  const predicate = buildEqPredicate(schema, table, columns, options.filters);
@@ -0,0 +1,3 @@
1
+ const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
2
+
3
+ export { quoteIdentifier };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/d1",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.31",
4
4
  "description": "D1 adapter for Lunora .global() tables, wrapping the Sessions API for read-your-writes",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/d1"
26
26
  },
27
27
  "files": [
28
- "dist",
28
+ "./dist",
29
29
  "README.md",
30
30
  "LICENSE.md",
31
31
  "__assets__"
@@ -50,8 +50,9 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@lunora/do": "1.0.0-alpha.3",
54
- "@lunora/sql-store": "1.0.0-alpha.3",
53
+ "@lunora/do": "1.0.0-alpha.32",
54
+ "@lunora/errors": "1.0.0-alpha.5",
55
+ "@lunora/sql-store": "1.0.0-alpha.32",
55
56
  "drizzle-orm": "^0.45.2"
56
57
  },
57
58
  "engines": {