@lunora/d1 1.0.0-alpha.40 → 1.0.0-alpha.42

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/dist/dialect.mjs CHANGED
@@ -1,36 +1 @@
1
- import { quoteIdentifier } from './packem_shared/quoteIdentifier-B-ZeSe1V.mjs';
2
-
3
- const sqlAffinityForKind = (kind) => {
4
- switch (kind) {
5
- case "boolean": {
6
- return "INTEGER";
7
- }
8
- case "bytes": {
9
- return "BLOB";
10
- }
11
- case "date":
12
- case "number":
13
- case "timestamp": {
14
- return "REAL";
15
- }
16
- default: {
17
- return "TEXT";
18
- }
19
- }
20
- };
21
- const frameworkColumnDdl = () => [
22
- `${quoteIdentifier("id")} TEXT PRIMARY KEY`,
23
- `${quoteIdentifier("_creationTime")} REAL NOT NULL`
24
- ];
25
- const columnRef = (field) => {
26
- if (field === "_id" || field === "id") {
27
- return quoteIdentifier("id");
28
- }
29
- if (field === "_creationTime") {
30
- return quoteIdentifier("_creationTime");
31
- }
32
- return quoteIdentifier(field);
33
- };
34
- const physicalIndexName = (tableName, indexName) => quoteIdentifier(`${tableName}_${indexName}`);
35
-
36
- export { columnRef, frameworkColumnDdl, physicalIndexName, quoteIdentifier, sqlAffinityForKind };
1
+ import{quoteIdentifier as i}from"./packem_shared/quoteIdentifier-CObIFRhb.mjs";const t=e=>{switch(e){case"boolean":return"INTEGER";case"bytes":return"BLOB";case"date":case"number":case"timestamp":return"REAL";default:return"TEXT"}},a=()=>[`${i("id")} TEXT PRIMARY KEY`,`${i("_creationTime")} REAL NOT NULL`],o=e=>e==="_id"||e==="id"?i("id"):e==="_creationTime"?i("_creationTime"):i(e),c=(e,r)=>i(`${e}_${r}`);export{o as columnRef,a as frameworkColumnDdl,c as physicalIndexName,i as quoteIdentifier,t as sqlAffinityForKind};
package/dist/index.mjs CHANGED
@@ -1,7 +1 @@
1
- export { exportGlobalRows, importGlobalRows, selectGlobalTables } from './packem_shared/exportGlobalRows-q0RgCLPM.mjs';
2
- export { D1Client, D1Session } from './packem_shared/D1Client-DSj8p7c4.mjs';
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-DnglIe4P.mjs';
5
- export { MigrationRunner } from './packem_shared/MigrationRunner-A-o1LTKj.mjs';
6
- export { default as sqliteDialect } from './packem_shared/sqliteDialect-DqYnHPuu.mjs';
7
- export { createSqlCtxDb } from '@lunora/sql-store';
1
+ import{exportGlobalRows as o,importGlobalRows as a,selectGlobalTables as t}from"./packem_shared/exportGlobalRows-CVw1hOdA.mjs";import{D1Client as i,D1Session as n}from"./packem_shared/D1Client-B9m0fFLr.mjs";import{createD1CtxDb as b,readD1CdcChanges as D,runD1AggregateMigrations as g,runD1CdcMigration as m,runD1GlobalTableMigrations as x,runD1RankMigrations as c,runD1SearchMigrations as f,trimD1CdcChanges as p}from"./packem_shared/createD1CtxDb-oii8T0ys.mjs";import{facetGlobalColumn as u,listGlobalTables as G,readGlobalTablePage as d}from"./packem_shared/facetGlobalColumn-T4arPSaV.mjs";import{MigrationRunner as R}from"./packem_shared/MigrationRunner-DvnPBDVT.mjs";import{default as h}from"./packem_shared/sqliteDialect-JvN44Hjf.mjs";import{createSqlCtxDb as q}from"@lunora/sql-store";export{i as D1Client,n as D1Session,R as MigrationRunner,b as createD1CtxDb,q as createSqlCtxDb,o as exportGlobalRows,u as facetGlobalColumn,a as importGlobalRows,G as listGlobalTables,D as readD1CdcChanges,d as readGlobalTablePage,g as runD1AggregateMigrations,m as runD1CdcMigration,x as runD1GlobalTableMigrations,c as runD1RankMigrations,f as runD1SearchMigrations,t as selectGlobalTables,h as sqliteDialect,p as trimD1CdcChanges};
@@ -0,0 +1 @@
1
+ import{drizzle as n}from"drizzle-orm/d1";const d=(t,e)=>{if(t.size<e)return;const s=t.keys().next().value;s!==void 0&&t.delete(s)},l=256,o=(t,e,s)=>{const r=t.get(s);if(r)return t.delete(s),t.set(s,r),r;const i=e(s);return d(t,l),t.set(s,i),i},a="first-unconstrained";class h{session;stmtCache=new Map;constructor(e){this.session=e}prepare(e){return o(this.stmtCache,s=>this.session.prepare(s),e)}async run(e,...s){return this.prepare(e).bind(...s).run()}async all(e,...s){return this.prepare(e).bind(...s).all()}async first(e,...s){return this.prepare(e).bind(...s).first()}getBookmark(){return this.session.getBookmark()??void 0}}class u{db;stmtCache=new Map;drizzleHandle;constructor(e){this.db=e}withSession(e){const s=this.db.withSession(e??a);return new h(s)}prepare(e){return o(this.stmtCache,s=>this.db.prepare(s),e)}get drizzle(){return this.drizzleHandle?this.drizzleHandle:(this.drizzleHandle=n(this.db,{logger:!1}),this.drizzleHandle)}drizzleSession(e){const s=this.db.withSession(e??a);return n(s,{logger:!1})}async batch(e){return this.drizzle.batch(e)}get raw(){return this.db}}export{u as D1Client,h as D1Session};
@@ -0,0 +1,2 @@
1
+ import{LunoraError as u}from"@lunora/errors";import{sql as h}from"drizzle-orm";import{D1Client as m}from"./D1Client-B9m0fFLr.mjs";const f="__drizzle_migrations",w=`CREATE TABLE IF NOT EXISTS ${f} (id INTEGER PRIMARY KEY AUTOINCREMENT, hash TEXT NOT NULL UNIQUE, created_at NUMERIC)`,d=/\s/u,g=/^[0-9a-f]{64}$/u,p=new RegExp(String.raw`UNIQUE constraint failed:\s*${f}\.hash`,"iu"),E=c=>{const n=c.sql;let i=!1,e=!1,r=!1,o=!1,a;for(let t=0;t<n.length;t+=1){const s=n[t],l=n[t+1];if(r){s===`
2
+ `&&(r=!1);continue}if(o){s==="*"&&l==="/"&&(o=!1,t+=1);continue}if(i){s==="'"&&(l==="'"?t+=1:i=!1);continue}if(e){s==='"'&&(l==='"'?t+=1:e=!1);continue}if(s==="-"&&l==="-"){r=!0,t+=1;continue}if(s==="/"&&l==="*"){o=!0,t+=1;continue}if(a!==void 0){if(s!==void 0&&!d.test(s))throw new u("INTERNAL",`Migration "${c.name}" (v${String(c.version)}) contains more than one SQL statement. Split it into separate migrations — batch() runs them atomically.`);continue}if(s==="'"){i=!0;continue}if(s==='"'){e=!0;continue}s===";"&&(a=t)}return a},S=async c=>{const n=new TextEncoder().encode(c),i=await crypto.subtle.digest("SHA-256",n);return[...new Uint8Array(i)].map(e=>e.toString(16).padStart(2,"0")).join("")};class I{client;migrations;constructor(n,i){this.client=n instanceof m?n:new m(n),this.migrations=[...i].toSorted((e,r)=>e.version-r.version),this.assertUniqueVersions(),this.assertUniqueSql()}async run(){await this.client.drizzle.run(h.raw(w));const n=await this.client.drizzle.all(h.raw(`SELECT hash FROM ${f}`)),i=new Set(n.map(a=>a.hash)),e=[],r=[],o=await Promise.all(this.migrations.map(async a=>S(a.sql)));for(const[a,t]of this.migrations.entries()){const s=o[a];if(i.has(s)){r.push({name:t.name,version:t.version});continue}await this.applyOne(t,s)?e.push({name:t.name,version:t.version}):r.push({name:t.name,version:t.version})}return{applied:e,skipped:r}}async applyOne(n,i){const e=E(n),r=(e===void 0?n.sql:n.sql.slice(0,e)).trim();if(!g.test(i))throw new u("INTERNAL",`migration "${n.name}" produced a non-hex hash; refusing to inline into SQL`);const o=`INSERT INTO ${f} (hash, created_at) VALUES ('${i}', ${String(Date.now())})`,a=[this.client.drizzle.run(h.raw(r)),this.client.drizzle.run(h.raw(o))];try{await this.client.batch(a)}catch(t){if(p.test(t instanceof Error?t.message:String(t)))return!1;throw t}return!0}assertUniqueVersions(){const n=new Set;for(const i of this.migrations){if(n.has(i.version))throw new u("INTERNAL",`Duplicate migration version ${String(i.version)}`);n.add(i.version)}}assertUniqueSql(){const n=new Map;for(const i of this.migrations){const e=n.get(i.sql);if(e!==void 0)throw new u("INTERNAL",`Migrations ${String(e)} and ${String(i.version)} have identical SQL — bump the content, not just the version.`);n.set(i.sql,i.version)}}}export{I as MigrationRunner};
@@ -0,0 +1 @@
1
+ import{createSqlCtxDb as t,readSqlCdcChanges as e,runSqlAggregateMigrations as i,runSqlCdcMigration as o,runSqlGlobalTableMigrations as s,runSqlRankMigrations as g,runSqlSearchMigrations as l,trimSqlCdcChanges as c}from"@lunora/sql-store";import{createSqlCtxDb as f,decodeGlobalRow as G}from"@lunora/sql-store";import n from"./sqliteDialect-JvN44Hjf.mjs";const D=a=>t({...a,dialect:n}),S=(a,r)=>s(a,r,n),q=(a,r)=>i(a,r,n),u=(a,r)=>g(a,r,n),M=(a,r)=>l(a,r,n),b=a=>o(a,n),m=(a,r={})=>e(a,r,n),h=(a,r)=>c(a,r,n);export{D as createD1CtxDb,f as createSqlCtxDb,G as decodeGlobalRow,m as readD1CdcChanges,q as runD1AggregateMigrations,b as runD1CdcMigration,S as runD1GlobalTableMigrations,u as runD1RankMigrations,M as runD1SearchMigrations,h as trimD1CdcChanges};
@@ -0,0 +1 @@
1
+ import{toErrorBody as u}from"@lunora/errors";import{runD1GlobalTableMigrations as m}from"./createD1CtxDb-oii8T0ys.mjs";import{quoteIdentifier as f}from"./quoteIdentifier-CObIFRhb.mjs";import{decodeGlobalRow as E}from"@lunora/sql-store";const R=200,g=(o,t)=>{const r=e=>o.tables[e]?.shardMode?.kind==="global";return t&&t.length>0?t.filter(e=>r(e)):Object.keys(o.tables).filter(e=>r(e))},k=(o,t,r)=>{const e=o.tables[t];return e?E(e,r):{_creationTime:r._creationTime,_id:r.id}},O=async function*(o,t,r){const e=g(t,r.tables),a=r.batchSize??R;await m(o,t);for(const i of e){const n=f(i);let c,s=!0;for(;s;){const l=c===void 0?await o.all(`SELECT * FROM ${n} ORDER BY "id" LIMIT ?`,[a]):await o.all(`SELECT * FROM ${n} WHERE "id" > ? ORDER BY "id" LIMIT ?`,[c,a]);for(const b of l)yield{doc:k(t,i,b),table:i};const d=l.at(-1);d!==void 0&&(c=String(d.id)),s=l.length===a}}},w=(o,t,r)=>{const e=o.tables[t];if(!e)return`unknown table: ${t}`;for(const[a,i]of Object.entries(e.shape)){const n=r[a],c=i.kind==="optional";if(n===void 0&&c)continue;const s=i.parse;if(typeof s=="function")try{s(n)}catch(l){const d=l instanceof Error?l.message:String(l);return`field "${a}": ${d}`}}},y=async(o,t,r,e)=>{try{return t?(await t.all(`SELECT 1 AS hit FROM ${f(r)} WHERE "id" = ? LIMIT 1`,[e])).length>0:await o.get(e)!==null}catch{return!1}},p=async(o,t,r,e,a)=>{const{doc:i,table:n}=e;if(t.tables[n]?.shardMode?.kind!=="global")return{kind:"skip"};if(!i||typeof i!="object"||Array.isArray(i))return{error:{code:"BAD_ROW",line:a,message:"row is missing or malformed `doc`",table:n},kind:"error"};const c=w(t,n,i);if(c!==void 0)return{error:{code:"VALIDATION_ERROR",line:a,message:c,table:n},kind:"error"};const s=typeof i._id=="string"?i._id:void 0;if(s!==void 0&&await y(o,r.exec,n,s))return{kind:"conflict"};try{return await o.insert(n,i,{allowExplicitId:!0}),{inserted:n,kind:"inserted"}}catch(l){const{body:d}=u(l,{fallbackCode:"INSERT_FAILED"});return{error:{code:d.code,line:a,message:d.message,table:n},kind:"error"}}},M=async(o,t,r)=>{const e=[],a={};let i=0,n=(r.startLine??1)-1;for(const c of r.rows){n+=1;const s=await p(o,t,r,c,n);switch(s.kind){case"conflict":{i+=1;break}case"error":{e.push(s.error);break}case"inserted":{a[s.inserted]=(a[s.inserted]??0)+1;break}}}return{conflicts:i,errors:e,inserted:a}};export{O as exportGlobalRows,M as importGlobalRows,g as selectGlobalTables};
@@ -0,0 +1 @@
1
+ import{LunoraError as _}from"@lunora/errors";import{runD1GlobalTableMigrations as T}from"./createD1CtxDb-oii8T0ys.mjs";import{quoteIdentifier as i}from"./quoteIdentifier-CObIFRhb.mjs";import{decodeGlobalRow as S}from"@lunora/sql-store";const E=(t,a)=>T(t,a),L=50,g=500,y=30,C=200,O=(t,a,o)=>Math.min(Math.max(t,a),o),A=/^sqlite_|^_cf_|^d1_|^__cdc|__agg_|__rank_|__fts_/u,U=t=>A.test(t),N=/password|secret|token|hash|salt|credential/iu,$=async t=>(await t.all("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name",[])).map(a=>String(a.name)).filter(a=>!U(a)),p=async(t,a,o="",n=[])=>{const s=await t.all(`SELECT COUNT(*) AS c FROM ${a}${o}`,n);return Number(s[0]?.c??0)},v=(t,a,o)=>t.tables[a]!==void 0&&o==="_id"?"id":o,M=(t,a,o,n)=>{if(n===void 0||n.length===0)return;const s=[],l=[];for(const e of n){if(!o.includes(e.column))throw new _("UNKNOWN_COLUMN",`unknown column: ${e.column}`,{status:404});if(t.tables[a]===void 0&&N.test(e.column))throw new _("FORBIDDEN",`cannot filter on a redacted column: ${e.column}`,{status:403});const r=i(v(t,a,e.column));e.value===null||e.value===void 0?s.push(`${r} IS NULL`):(s.push(`${r} = ?`),l.push(e.value))}return{params:l,where:s.join(" AND ")}},G=(t,a,o)=>{const n=t.tables[a];if(n)return S(n,o);const s={};for(const[l,e]of Object.entries(o))s[l]=e!=null&&N.test(l)?"•••":e;return s},R=async(t,a,o)=>{const n=a.tables[o];return n?["_id","_creationTime",...Object.keys(n.shape)]:(await t.all(`PRAGMA table_info(${i(o)})`,[])).map(s=>String(s.name))},k=async(t,a,o)=>{if(a.tables[o])return;const n=await t.all(`PRAGMA foreign_key_list(${i(o)})`,[]);if(n.length===0)return;const s={};for(const l of n){const e=String(l.from),r=String(l.table);s[e]??=r}return s},B=async(t,a)=>{await E(t,a);const o=await $(t);return Promise.all(o.map(async n=>({name:n,rowCount:await p(t,i(n))})))},P=async(t,a,o)=>{const{table:n}=o;if(await E(t,a),!(await $(t)).includes(n))throw new _("UNKNOWN_TABLE",`unknown table: ${n}`,{status:404});const s=O(Math.trunc(o.limit??L),1,g),l=Math.max(0,Math.trunc(o.offset??0)),e=i(n),r=await R(t,a,n),u=M(a,n,r,o.filters),w=u===void 0?"":` WHERE ${u.where}`,c=u?.params??[],f=await p(t,e,w,c),m=(await t.all(`SELECT * FROM ${e}${w} LIMIT ? OFFSET ?`,[...c,s,l])).map(h=>G(a,n,h)),d=await k(t,a,n);return d===void 0?{columns:r,rows:m,total:f}:{columns:r,refs:d,rows:m,total:f}},K=async(t,a,o)=>{const{column:n,table:s}=o;if(await E(t,a),!(await $(t)).includes(s))throw new _("UNKNOWN_TABLE",`unknown table: ${s}`,{status:404});const l=await R(t,a,s);if(!l.includes(n))throw new _("UNKNOWN_COLUMN",`unknown column: ${n}`,{status:404});const e=i(s),r=M(a,s,l,o.filters),u=r===void 0?"":` WHERE ${r.where}`,w=r?.params??[];if(a.tables[s]===void 0&&N.test(n)){const b=await p(t,e,u,w);return{truncated:!1,values:b===0?[]:[{count:b,value:"•••"}]}}const c=O(Math.trunc(o.limit??y),1,C),f=i(v(a,s,n)),m=await t.all(`SELECT ${f} AS value, COUNT(*) AS count FROM ${e}${u} GROUP BY ${f} ORDER BY count DESC LIMIT ?`,[...w,c+1]),d=m.length>c,h=d?m.slice(0,c):m;return{truncated:d,values:h.map(b=>({count:Number(b.count),value:b.value}))}};export{K as facetGlobalColumn,B as listGlobalTables,P as readGlobalTablePage};
@@ -0,0 +1 @@
1
+ const t=e=>`"${e.replaceAll('"','""')}"`;export{t as quoteIdentifier};
@@ -0,0 +1 @@
1
+ import{sqliteEncode as i,sqliteDecode as n}from"@lunora/sql-store";import{sql as o}from"drizzle-orm";import{sqlAffinityForKind as r}from"../dialect.mjs";const a=/unique constraint failed/iu,T={companionTypes:{autoincrementPrimaryKey:"INTEGER PRIMARY KEY AUTOINCREMENT",integer:"INTEGER",key:"TEXT",real:"REAL",text:"TEXT"},columnType:e=>r(e),decode:(e,t)=>n(e,t),encode:e=>i(e),frameworkColumns:()=>[{name:"id",type:"TEXT PRIMARY KEY"},{name:"_creationTime",type:"REAL NOT NULL"}],isUniqueViolation:e=>e instanceof Error&&a.test(e.message),name:"sqlite",supportsReturning:!0,tableExists:e=>o`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${e}`};export{T as default};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/d1",
3
- "version": "1.0.0-alpha.40",
3
+ "version": "1.0.0-alpha.42",
4
4
  "description": "D1 adapter for Lunora .global() tables, wrapping the Sessions API for read-your-writes",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -50,9 +50,9 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@lunora/do": "1.0.0-alpha.44",
53
+ "@lunora/do": "1.0.0-alpha.46",
54
54
  "@lunora/errors": "1.0.0-alpha.8",
55
- "@lunora/sql-store": "1.0.0-alpha.41",
55
+ "@lunora/sql-store": "1.0.0-alpha.43",
56
56
  "drizzle-orm": "^0.45.2"
57
57
  },
58
58
  "engines": {
@@ -1,137 +0,0 @@
1
- import { drizzle } from 'drizzle-orm/d1';
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
-
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
- };
26
- const D1_FIRST_UNCONSTRAINED = "first-unconstrained";
27
- class D1Session {
28
- session;
29
- /** See {@link D1Client.stmtCache}. Scoped per session. */
30
- stmtCache = /* @__PURE__ */ new Map();
31
- constructor(session) {
32
- this.session = session;
33
- }
34
- prepare(sql) {
35
- return prepareCached(this.stmtCache, (text) => this.session.prepare(text), sql);
36
- }
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.
38
- async run(sql, ...binds) {
39
- return this.prepare(sql).bind(...binds).run();
40
- }
41
- // 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.
42
- async all(sql, ...binds) {
43
- return this.prepare(sql).bind(...binds).all();
44
- }
45
- async first(sql, ...binds) {
46
- return this.prepare(sql).bind(...binds).first();
47
- }
48
- /**
49
- * Returns the most recent bookmark known to the session, or `undefined`
50
- * when D1 has not issued one yet.
51
- */
52
- getBookmark() {
53
- return this.session.getBookmark() ?? void 0;
54
- }
55
- }
56
- class D1Client {
57
- db;
58
- /**
59
- * SQL string -> prepared statement. Prepared statements are reusable in
60
- * D1; preparing the same SQL twice forces the worker to round-trip the
61
- * statement plan. Caching is per-instance so unit-test isolation holds.
62
- * Bounded to {@link STMT_CACHE_CAPACITY} via LRU eviction.
63
- */
64
- stmtCache = /* @__PURE__ */ new Map();
65
- /**
66
- * Lazily-built drizzle handle over the bare binding. Memoised so a single
67
- * `D1Client` reuses the same dialect/session machinery across calls.
68
- */
69
- drizzleHandle;
70
- constructor(database) {
71
- this.db = database;
72
- }
73
- /**
74
- * Open a Sessions-API scoped session. Pass the bookmark forwarded by
75
- * the client to opt into read-your-writes consistency.
76
- *
77
- * With no bookmark this is the first request of a session — there is no
78
- * prior write to read, so we open with the explicit `"first-unconstrained"`
79
- * constraint (Cloudflare's lowest-latency default: the first read may serve
80
- * from any replica). Read-your-writes for sequenced requests still flows
81
- * through the forwarded bookmark; a caller needing a strongly-consistent
82
- * very-first read should pass `"first-primary"` as the bookmark instead.
83
- */
84
- withSession(bookmark) {
85
- const session = this.db.withSession(bookmark ?? D1_FIRST_UNCONSTRAINED);
86
- return new D1Session(session);
87
- }
88
- /**
89
- * Prepare a statement, reusing a cached one when the SQL text matches.
90
- * `bind()` on a prepared statement returns a new bound statement and
91
- * leaves the underlying prepared plan reusable, so cache hits are safe
92
- * even when the previous caller already called `.bind(...).run()`.
93
- */
94
- prepare(sql) {
95
- return prepareCached(this.stmtCache, (text) => this.db.prepare(text), sql);
96
- }
97
- /**
98
- * Drizzle handle over the bare `env.DB` binding. Used for typed queries
99
- * against generated `sqliteTable` schemas; does **not** participate in the
100
- * D1 Sessions API (no bookmark pinning). For bookmark-scoped reads, use
101
- * {@link drizzleSession} instead.
102
- */
103
- get drizzle() {
104
- if (this.drizzleHandle) {
105
- return this.drizzleHandle;
106
- }
107
- this.drizzleHandle = drizzle(this.db, { logger: false });
108
- return this.drizzleHandle;
109
- }
110
- /**
111
- * Drizzle handle scoped to a D1 Sessions-API session. The bookmark, when
112
- * supplied, opts into read-your-writes consistency for follow-up reads on
113
- * the same session.
114
- *
115
- * A `D1DatabaseSession` exposes the same `prepare` / `batch` surface
116
- * drizzle calls into, so a single `unknown` cast lets us treat the session
117
- * as a `D1Database` for driver-construction purposes.
118
- */
119
- drizzleSession(bookmark) {
120
- const session = this.db.withSession(bookmark ?? D1_FIRST_UNCONSTRAINED);
121
- return drizzle(session, { logger: false });
122
- }
123
- /**
124
- * Atomic batch over the drizzle d1 driver. Mirrors `db.batch([...])`
125
- * exactly; exposed on the client so callers don't need to hold a drizzle
126
- * handle just to run a typed batch.
127
- */
128
- async batch(items) {
129
- return this.drizzle.batch(items);
130
- }
131
- /** Direct access to the underlying binding (advanced use only). */
132
- get raw() {
133
- return this.db;
134
- }
135
- }
136
-
137
- export { D1Client, D1Session };
@@ -1,169 +0,0 @@
1
- import { LunoraError } from '@lunora/errors';
2
- import { sql } from 'drizzle-orm';
3
- import { D1Client } from './D1Client-DSj8p7c4.mjs';
4
-
5
- const TRACKING_TABLE_NAME = "__drizzle_migrations";
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)`;
7
- const WHITESPACE_RE = /\s/u;
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");
10
- const assertSingleStatement = (migration) => {
11
- const text = migration.sql;
12
- let inSingle = false;
13
- let inDouble = false;
14
- let inLineComment = false;
15
- let inBlockComment = false;
16
- let terminatorIndex;
17
- for (let index = 0; index < text.length; index += 1) {
18
- const character = text[index];
19
- const next = text[index + 1];
20
- if (inLineComment) {
21
- if (character === "\n") {
22
- inLineComment = false;
23
- }
24
- continue;
25
- }
26
- if (inBlockComment) {
27
- if (character === "*" && next === "/") {
28
- inBlockComment = false;
29
- index += 1;
30
- }
31
- continue;
32
- }
33
- if (inSingle) {
34
- if (character === "'") {
35
- if (next === "'") {
36
- index += 1;
37
- } else {
38
- inSingle = false;
39
- }
40
- }
41
- continue;
42
- }
43
- if (inDouble) {
44
- if (character === '"') {
45
- if (next === '"') {
46
- index += 1;
47
- } else {
48
- inDouble = false;
49
- }
50
- }
51
- continue;
52
- }
53
- if (character === "-" && next === "-") {
54
- inLineComment = true;
55
- index += 1;
56
- continue;
57
- }
58
- if (character === "/" && next === "*") {
59
- inBlockComment = true;
60
- index += 1;
61
- continue;
62
- }
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
- }
70
- continue;
71
- }
72
- if (character === "'") {
73
- inSingle = true;
74
- continue;
75
- }
76
- if (character === '"') {
77
- inDouble = true;
78
- continue;
79
- }
80
- if (character === ";") {
81
- terminatorIndex = index;
82
- }
83
- }
84
- return terminatorIndex;
85
- };
86
- const hashMigration = async (text) => {
87
- const bytes = new TextEncoder().encode(text);
88
- const digest = await crypto.subtle.digest("SHA-256", bytes);
89
- return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
90
- };
91
- class MigrationRunner {
92
- client;
93
- migrations;
94
- /**
95
- * Accepts either a {@link D1Client} (preferred — gets typed batches +
96
- * drizzle handle for free) or a raw `D1DatabaseLike` binding (wrapped on
97
- * the caller's behalf so existing `@lunora/cli` callers keep working).
98
- */
99
- constructor(database, migrations) {
100
- this.client = database instanceof D1Client ? database : new D1Client(database);
101
- this.migrations = [...migrations].toSorted((a, b) => a.version - b.version);
102
- this.assertUniqueVersions();
103
- this.assertUniqueSql();
104
- }
105
- async run() {
106
- await this.client.drizzle.run(sql.raw(TRACKING_TABLE_DDL));
107
- const appliedRows = await this.client.drizzle.all(sql.raw(`SELECT hash FROM ${TRACKING_TABLE_NAME}`));
108
- const appliedHashes = new Set(appliedRows.map((row) => row.hash));
109
- const applied = [];
110
- const skipped = [];
111
- const hashes = await Promise.all(this.migrations.map(async (migration) => hashMigration(migration.sql)));
112
- for (const [index, migration] of this.migrations.entries()) {
113
- const hash = hashes[index];
114
- if (appliedHashes.has(hash)) {
115
- skipped.push({ name: migration.name, version: migration.version });
116
- continue;
117
- }
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
- }
124
- }
125
- return { applied, skipped };
126
- }
127
- async applyOne(migration, hash) {
128
- const terminatorIndex = assertSingleStatement(migration);
129
- const statementText = (terminatorIndex === void 0 ? migration.sql : migration.sql.slice(0, terminatorIndex)).trim();
130
- if (!SHA256_HEX_RE.test(hash)) {
131
- throw new LunoraError("INTERNAL", `migration "${migration.name}" produced a non-hex hash; refusing to inline into SQL`);
132
- }
133
- const trackingInsertSql = `INSERT INTO ${TRACKING_TABLE_NAME} (hash, created_at) VALUES ('${hash}', ${String(Date.now())})`;
134
- const items = [this.client.drizzle.run(sql.raw(statementText)), this.client.drizzle.run(sql.raw(trackingInsertSql))];
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;
144
- }
145
- assertUniqueVersions() {
146
- const seen = /* @__PURE__ */ new Set();
147
- for (const m of this.migrations) {
148
- if (seen.has(m.version)) {
149
- throw new LunoraError("INTERNAL", `Duplicate migration version ${String(m.version)}`);
150
- }
151
- seen.add(m.version);
152
- }
153
- }
154
- assertUniqueSql() {
155
- const seen = /* @__PURE__ */ new Map();
156
- for (const m of this.migrations) {
157
- const previousVersion = seen.get(m.sql);
158
- if (previousVersion !== void 0) {
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
- );
163
- }
164
- seen.set(m.sql, m.version);
165
- }
166
- }
167
- }
168
-
169
- export { MigrationRunner };
@@ -1,14 +0,0 @@
1
- import { createSqlCtxDb, readSqlCdcChanges, runSqlAggregateMigrations, runSqlCdcMigration, runSqlGlobalTableMigrations, runSqlRankMigrations, runSqlSearchMigrations, trimSqlCdcChanges } from '@lunora/sql-store';
2
- export { createSqlCtxDb, decodeGlobalRow } from '@lunora/sql-store';
3
- import sqliteDialect from './sqliteDialect-DqYnHPuu.mjs';
4
-
5
- const createD1ContextDatabase = (options) => createSqlCtxDb({ ...options, dialect: sqliteDialect });
6
- const runD1GlobalTableMigrations = (exec, schema) => runSqlGlobalTableMigrations(exec, schema, sqliteDialect);
7
- const runD1AggregateMigrations = (exec, schema) => runSqlAggregateMigrations(exec, schema, sqliteDialect);
8
- const runD1RankMigrations = (exec, schema) => runSqlRankMigrations(exec, schema, sqliteDialect);
9
- const runD1SearchMigrations = (exec, schema) => runSqlSearchMigrations(exec, schema, sqliteDialect);
10
- const runD1CdcMigration = (exec) => runSqlCdcMigration(exec, sqliteDialect);
11
- const readD1CdcChanges = (exec, options = {}) => readSqlCdcChanges(exec, options, sqliteDialect);
12
- const trimD1CdcChanges = (exec, throughSeq) => trimSqlCdcChanges(exec, throughSeq, sqliteDialect);
13
-
14
- export { createD1ContextDatabase as createD1CtxDb, readD1CdcChanges, runD1AggregateMigrations, runD1CdcMigration, runD1GlobalTableMigrations, runD1RankMigrations, runD1SearchMigrations, trimD1CdcChanges };
@@ -1,128 +0,0 @@
1
- import { toErrorBody } from '@lunora/errors';
2
- import { runD1GlobalTableMigrations } from './createD1CtxDb-BMR8J0dT.mjs';
3
- import { quoteIdentifier } from './quoteIdentifier-B-ZeSe1V.mjs';
4
- import { decodeGlobalRow } from '@lunora/sql-store';
5
-
6
- const DEFAULT_BATCH_SIZE = 200;
7
- const selectGlobalTables = (schema, requested) => {
8
- const isGlobal = (table) => schema.tables[table]?.shardMode?.kind === "global";
9
- if (requested && requested.length > 0) {
10
- return requested.filter((name) => isGlobal(name));
11
- }
12
- return Object.keys(schema.tables).filter((name) => isGlobal(name));
13
- };
14
- const decodeRow = (schema, table, row) => {
15
- const definition = schema.tables[table];
16
- if (!definition) {
17
- return { _creationTime: row["_creationTime"], _id: row["id"] };
18
- }
19
- return decodeGlobalRow(definition, row);
20
- };
21
- const exportGlobalRows = async function* (exec, schema, args) {
22
- const tables = selectGlobalTables(schema, args.tables);
23
- const batchSize = args.batchSize ?? DEFAULT_BATCH_SIZE;
24
- await runD1GlobalTableMigrations(exec, schema);
25
- for (const table of tables) {
26
- const quoted = quoteIdentifier(table);
27
- let lastId;
28
- let hasMore = true;
29
- while (hasMore) {
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]);
31
- for (const row of rows) {
32
- yield { doc: decodeRow(schema, table, row), table };
33
- }
34
- const last = rows.at(-1);
35
- if (last !== void 0) {
36
- lastId = String(last["id"]);
37
- }
38
- hasMore = rows.length === batchSize;
39
- }
40
- }
41
- };
42
- const validateRow = (schema, table, document) => {
43
- const definition = schema.tables[table];
44
- if (!definition) {
45
- return `unknown table: ${table}`;
46
- }
47
- for (const [field, validator] of Object.entries(definition.shape)) {
48
- const candidate = document[field];
49
- const optional = validator.kind === "optional";
50
- if (candidate === void 0 && optional) {
51
- continue;
52
- }
53
- const parser = validator.parse;
54
- if (typeof parser !== "function") {
55
- continue;
56
- }
57
- try {
58
- parser(candidate);
59
- } catch (error) {
60
- const message = error instanceof Error ? error.message : String(error);
61
- return `field "${field}": ${message}`;
62
- }
63
- }
64
- return void 0;
65
- };
66
- const explicitIdConflicts = async (writer, exec, table, explicitId) => {
67
- try {
68
- if (exec) {
69
- const probe = await exec.all(`SELECT 1 AS hit FROM ${quoteIdentifier(table)} WHERE "id" = ? LIMIT 1`, [explicitId]);
70
- return probe.length > 0;
71
- }
72
- const existing = await writer.get(explicitId);
73
- return existing !== null;
74
- } catch {
75
- return false;
76
- }
77
- };
78
- const importOneRow = async (writer, schema, args, row, line) => {
79
- const { doc, table } = row;
80
- if (schema.tables[table]?.shardMode?.kind !== "global") {
81
- return { kind: "skip" };
82
- }
83
- if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
84
- return { error: { code: "BAD_ROW", line, message: "row is missing or malformed `doc`", table }, kind: "error" };
85
- }
86
- const failure = validateRow(schema, table, doc);
87
- if (failure !== void 0) {
88
- return { error: { code: "VALIDATION_ERROR", line, message: failure, table }, kind: "error" };
89
- }
90
- const explicitId = typeof doc["_id"] === "string" ? doc["_id"] : void 0;
91
- if (explicitId !== void 0 && await explicitIdConflicts(writer, args.exec, table, explicitId)) {
92
- return { kind: "conflict" };
93
- }
94
- try {
95
- await writer.insert(table, doc, { allowExplicitId: true });
96
- return { inserted: table, kind: "inserted" };
97
- } catch (error) {
98
- const { body } = toErrorBody(error, { fallbackCode: "INSERT_FAILED" });
99
- return { error: { code: body.code, line, message: body.message, table }, kind: "error" };
100
- }
101
- };
102
- const importGlobalRows = async (writer, schema, args) => {
103
- const errors = [];
104
- const inserted = {};
105
- let conflicts = 0;
106
- let line = (args.startLine ?? 1) - 1;
107
- for (const row of args.rows) {
108
- line += 1;
109
- const outcome = await importOneRow(writer, schema, args, row, line);
110
- switch (outcome.kind) {
111
- case "conflict": {
112
- conflicts += 1;
113
- break;
114
- }
115
- case "error": {
116
- errors.push(outcome.error);
117
- break;
118
- }
119
- case "inserted": {
120
- inserted[outcome.inserted] = (inserted[outcome.inserted] ?? 0) + 1;
121
- break;
122
- }
123
- }
124
- }
125
- return { conflicts, errors, inserted };
126
- };
127
-
128
- export { exportGlobalRows, importGlobalRows, selectGlobalTables };
@@ -1,146 +0,0 @@
1
- import { LunoraError } from '@lunora/errors';
2
- import { runD1GlobalTableMigrations } from './createD1CtxDb-BMR8J0dT.mjs';
3
- import { quoteIdentifier } from './quoteIdentifier-B-ZeSe1V.mjs';
4
- import { decodeGlobalRow } from '@lunora/sql-store';
5
-
6
- const ensureGlobalTables = (exec, schema) => runD1GlobalTableMigrations(exec, schema);
7
- const DEFAULT_PAGE_SIZE = 50;
8
- const MAX_PAGE_SIZE = 500;
9
- const DEFAULT_FACET_LIMIT = 30;
10
- const MAX_FACET_LIMIT = 200;
11
- const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
12
- const INTERNAL_TABLE = /^sqlite_|^_cf_|^d1_|^__cdc|__agg_|__rank_|__fts_/u;
13
- const isInternalTable = (name) => INTERNAL_TABLE.test(name);
14
- const SENSITIVE_COLUMN = /password|secret|token|hash|salt|credential/iu;
15
- const listTableNames = async (exec) => {
16
- const rows = await exec.all("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name", []);
17
- return rows.map((row) => String(row["name"])).filter((name) => !isInternalTable(name));
18
- };
19
- const countRows = async (exec, quotedTable, whereSql = "", whereParams = []) => {
20
- const rows = await exec.all(`SELECT COUNT(*) AS c FROM ${quotedTable}${whereSql}`, whereParams);
21
- return Number(rows[0]?.["c"] ?? 0);
22
- };
23
- const physicalColumnName = (schema, table, displayColumn) => schema.tables[table] !== void 0 && displayColumn === "_id" ? "id" : displayColumn;
24
- const buildEqPredicate = (schema, table, displayColumns, filters) => {
25
- if (filters === void 0 || filters.length === 0) {
26
- return void 0;
27
- }
28
- const clauses = [];
29
- const params = [];
30
- for (const filter of filters) {
31
- if (!displayColumns.includes(filter.column)) {
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 });
36
- }
37
- const quoted = quoteIdentifier(physicalColumnName(schema, table, filter.column));
38
- if (filter.value === null || filter.value === void 0) {
39
- clauses.push(`${quoted} IS NULL`);
40
- } else {
41
- clauses.push(`${quoted} = ?`);
42
- params.push(filter.value);
43
- }
44
- }
45
- return { params, where: clauses.join(" AND ") };
46
- };
47
- const decodeRow = (schema, table, row) => {
48
- const definition = schema.tables[table];
49
- if (definition) {
50
- return decodeGlobalRow(definition, row);
51
- }
52
- const redacted = {};
53
- for (const [key, value] of Object.entries(row)) {
54
- redacted[key] = value !== null && value !== void 0 && SENSITIVE_COLUMN.test(key) ? "•••" : value;
55
- }
56
- return redacted;
57
- };
58
- const resolveColumns = async (exec, schema, table) => {
59
- const definition = schema.tables[table];
60
- if (definition) {
61
- return ["_id", "_creationTime", ...Object.keys(definition.shape)];
62
- }
63
- const info = await exec.all(`PRAGMA table_info(${quoteIdentifier(table)})`, []);
64
- return info.map((column) => String(column["name"]));
65
- };
66
- const resolveReferences = async (exec, schema, table) => {
67
- if (schema.tables[table]) {
68
- return void 0;
69
- }
70
- const rows = await exec.all(`PRAGMA foreign_key_list(${quoteIdentifier(table)})`, []);
71
- if (rows.length === 0) {
72
- return void 0;
73
- }
74
- const references = {};
75
- for (const row of rows) {
76
- const from = String(row["from"]);
77
- const target = String(row["table"]);
78
- references[from] ??= target;
79
- }
80
- return references;
81
- };
82
- const listGlobalTables = async (exec, schema) => {
83
- await ensureGlobalTables(exec, schema);
84
- const names = await listTableNames(exec);
85
- return Promise.all(
86
- names.map(async (name) => {
87
- return { name, rowCount: await countRows(exec, quoteIdentifier(name)) };
88
- })
89
- );
90
- };
91
- const readGlobalTablePage = async (exec, schema, options) => {
92
- const { table } = options;
93
- await ensureGlobalTables(exec, schema);
94
- const tableNames = await listTableNames(exec);
95
- if (!tableNames.includes(table)) {
96
- throw new LunoraError("UNKNOWN_TABLE", `unknown table: ${table}`, { status: 404 });
97
- }
98
- const limit = clamp(Math.trunc(options.limit ?? DEFAULT_PAGE_SIZE), 1, MAX_PAGE_SIZE);
99
- const offset = Math.max(0, Math.trunc(options.offset ?? 0));
100
- const quoted = quoteIdentifier(table);
101
- const columns = await resolveColumns(exec, schema, table);
102
- const predicate = buildEqPredicate(schema, table, columns, options.filters);
103
- const whereSql = predicate === void 0 ? "" : ` WHERE ${predicate.where}`;
104
- const whereParams = predicate?.params ?? [];
105
- const total = await countRows(exec, quoted, whereSql, whereParams);
106
- const raw = await exec.all(`SELECT * FROM ${quoted}${whereSql} LIMIT ? OFFSET ?`, [...whereParams, limit, offset]);
107
- const rows = raw.map((row) => decodeRow(schema, table, row));
108
- const references = await resolveReferences(exec, schema, table);
109
- return references === void 0 ? { columns, rows, total } : { columns, refs: references, rows, total };
110
- };
111
- const facetGlobalColumn = async (exec, schema, options) => {
112
- const { column, table } = options;
113
- await ensureGlobalTables(exec, schema);
114
- const tableNames = await listTableNames(exec);
115
- if (!tableNames.includes(table)) {
116
- throw new LunoraError("UNKNOWN_TABLE", `unknown table: ${table}`, { status: 404 });
117
- }
118
- const columns = await resolveColumns(exec, schema, table);
119
- if (!columns.includes(column)) {
120
- throw new LunoraError("UNKNOWN_COLUMN", `unknown column: ${column}`, { status: 404 });
121
- }
122
- const quoted = quoteIdentifier(table);
123
- const predicate = buildEqPredicate(schema, table, columns, options.filters);
124
- const whereSql = predicate === void 0 ? "" : ` WHERE ${predicate.where}`;
125
- const whereParams = predicate?.params ?? [];
126
- if (schema.tables[table] === void 0 && SENSITIVE_COLUMN.test(column)) {
127
- const total = await countRows(exec, quoted, whereSql, whereParams);
128
- return { truncated: false, values: total === 0 ? [] : [{ count: total, value: "•••" }] };
129
- }
130
- const limit = clamp(Math.trunc(options.limit ?? DEFAULT_FACET_LIMIT), 1, MAX_FACET_LIMIT);
131
- const physical = quoteIdentifier(physicalColumnName(schema, table, column));
132
- const rows = await exec.all(`SELECT ${physical} AS value, COUNT(*) AS count FROM ${quoted}${whereSql} GROUP BY ${physical} ORDER BY count DESC LIMIT ?`, [
133
- ...whereParams,
134
- limit + 1
135
- ]);
136
- const truncated = rows.length > limit;
137
- const kept = truncated ? rows.slice(0, limit) : rows;
138
- return {
139
- truncated,
140
- values: kept.map((row) => {
141
- return { count: Number(row["count"]), value: row["value"] };
142
- })
143
- };
144
- };
145
-
146
- export { facetGlobalColumn, listGlobalTables, readGlobalTablePage };
@@ -1,3 +0,0 @@
1
- const quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
2
-
3
- export { quoteIdentifier };
@@ -1,27 +0,0 @@
1
- import { sqliteEncode, sqliteDecode } from '@lunora/sql-store';
2
- import { sql } from 'drizzle-orm';
3
- import { sqlAffinityForKind } from '../dialect.mjs';
4
-
5
- const UNIQUE_VIOLATION_RE = /unique constraint failed/iu;
6
- const sqliteDialect = {
7
- companionTypes: {
8
- autoincrementPrimaryKey: "INTEGER PRIMARY KEY AUTOINCREMENT",
9
- integer: "INTEGER",
10
- key: "TEXT",
11
- real: "REAL",
12
- text: "TEXT"
13
- },
14
- columnType: (kind) => sqlAffinityForKind(kind),
15
- decode: (value, kind) => sqliteDecode(value, kind),
16
- encode: (value) => sqliteEncode(value),
17
- frameworkColumns: () => [
18
- { name: "id", type: "TEXT PRIMARY KEY" },
19
- { name: "_creationTime", type: "REAL NOT NULL" }
20
- ],
21
- isUniqueViolation: (error) => error instanceof Error && UNIQUE_VIOLATION_RE.test(error.message),
22
- name: "sqlite",
23
- supportsReturning: true,
24
- tableExists: (table) => sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${table}`
25
- };
26
-
27
- export { sqliteDialect as default };