@zeitlosapp/cli 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- const e=process.versions.node.split(`.`).map(Number),t=e[0]??0,n=e[1]??0;(t<22||t===22&&n<5)&&(process.stderr.write(`\n The Zeitlos CLI requires Node 22.5 or newer — found ${process.versions.node}.\n It uses Node's built-in SQLite (added in 22.5). Please upgrade Node.\n\n`),process.exit(1));const r=process.emit;process.emit=function(e,...t){let n=t[0];return e===`warning`&&n?.name===`ExperimentalWarning`&&/\bSQLite\b/.test(String(n?.message))?!1:r.call(this,e,...t)};try{let{run:e}=await import(`./main-Bi4aESU5.mjs`);await e()}catch(e){console.error(e instanceof Error?e.message:e),process.exit(1)}export{};
2
+ const e=process.versions.node.split(`.`).map(Number),t=e[0]??0,n=e[1]??0;(t<22||t===22&&n<5)&&(process.stderr.write(`\n The Zeitlos CLI requires Node 22.5 or newer — found ${process.versions.node}.\n It uses Node's built-in SQLite (added in 22.5). Please upgrade Node.\n\n`),process.exit(1));const r=process.emit;process.emit=function(e,...t){let n=t[0];return e===`warning`&&n?.name===`ExperimentalWarning`&&/\bSQLite\b/.test(String(n?.message))?!1:r.call(this,e,...t)};try{let{run:e}=await import(`./main-C5fFfaXW.mjs`);await e()}catch(e){console.error(e instanceof Error?e.message:e),process.exit(1)}export{};
@@ -0,0 +1,58 @@
1
+ import{parseArgs as e}from"node:util";import{existsSync as t,mkdirSync as n,readFileSync as r,rmSync as i,writeFileSync as a}from"node:fs";import{dirname as o,join as s,relative as c,resolve as l}from"node:path";import{fileURLToPath as u}from"node:url";import{z as d}from"zod";import{createHash as f,randomBytes as p}from"node:crypto";import m from"express";import{DatabaseSync as h}from"node:sqlite";import{StreamableHTTPServerTransport as ee}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{McpServer as te}from"@modelcontextprotocol/sdk/server/mcp.js";const ne=[`eq`,`neq`,`in`,`nin`,`gt`,`gte`,`lt`,`lte`,`contains`,`startsWith`,`endsWith`,`isNull`],re=[`text`,`number`,`boolean`,`datetime`,`date`,`email`,`url`,`select`,`relation`,`json`],g=[`eq`,`neq`,`in`,`nin`,`contains`,`startsWith`,`endsWith`,`isNull`],_=[`eq`,`neq`,`gt`,`gte`,`lt`,`lte`,`in`,`nin`,`isNull`],ie=[`eq`,`neq`,`in`,`nin`,`isNull`],v={text:{pgType:`text`,ops:g,sortable:!0,uniqueable:!0,indexable:!0},number:{pgType:`numeric`,ops:_,sortable:!0,uniqueable:!0,indexable:!0},boolean:{pgType:`boolean`,ops:[`eq`,`isNull`],sortable:!0,uniqueable:!1,indexable:!0},datetime:{pgType:`timestamptz`,ops:_,sortable:!0,uniqueable:!0,indexable:!0},date:{pgType:`date`,ops:_,sortable:!0,uniqueable:!0,indexable:!0},email:{pgType:`citext`,ops:g,sortable:!0,uniqueable:!0,indexable:!0},url:{pgType:`text`,ops:g,sortable:!0,uniqueable:!0,indexable:!0},select:{pgType:`text`,ops:ie,sortable:!0,uniqueable:!0,indexable:!0},relation:{pgType:`uuid`,ops:ie,sortable:!1,uniqueable:!0,indexable:!0},json:{pgType:`jsonb`,ops:[`isNull`],sortable:!1,uniqueable:!1,indexable:!1}},ae=Object.values({id:`id`,createdAt:`created_at`,updatedAt:`updated_at`,createdBy:`created_by`,updatedBy:`updated_by`}),oe=/^[a-z][a-z0-9_]*$/;function y(e){return oe.test(e)&&e.length<=48&&!e.startsWith(`_`)}function se(e){return e.startsWith(`_`)}function ce(e){return ae.includes(e)||e.startsWith(`_`)}const b=d.lazy(()=>d.union([d.string(),d.number(),d.boolean(),d.null(),d.array(b),d.record(d.string(),b)])),le=d.record(d.enum(ne),b),ue=d.union([b,le]),x=d.lazy(()=>d.object({and:d.array(x).optional(),or:d.array(x).optional(),not:x.optional()}).catchall(ue)),de=d.array(d.string().min(1)),fe=d.object({filter:x.optional(),sort:de.optional(),limit:d.number().int().positive().max(200).optional(),cursor:d.string().optional(),select:d.array(d.string().min(1)).optional(),expand:d.array(d.string().min(1)).optional(),count:d.boolean().optional()}).strict(),S=d.record(d.string(),b);d.object({data:S}),d.object({data:d.array(S).min(1)}),d.object({id:d.string(),patch:S}),d.object({filter:x,patch:S,confirmToken:d.string().optional()}),d.object({filter:x,confirmToken:d.string().optional()});const pe={validation_failed:400,database_not_enabled:409,table_not_found:404,table_exists:409,field_not_found:404,field_exists:409,reserved_name:400,invalid_filter:400,record_not_found:404,unique_violation:409,forbidden:403,quota_exceeded:429,confirmation_required:409,lossy_type_change:409,internal:500};var C=class extends Error{code;details;constructor(e,t,n){super(t),this.name=`BaasError`,this.code=e,this.details=n}get httpStatus(){return pe[this.code]}toBody(){return{code:this.code,message:this.message,details:this.details}}};const me=[`list`,`read`,`create`,`update`,`delete`],he=[`public`,`authenticated`,`owner`,`none`];function ge(e,t,n,r){if(n!==`service`&&(e[t]??`none`)!==`public`)throw new C(`forbidden`,`not permitted: ${t} on ${r}`)}const _e=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;function ve(e){try{return new URL(e),!0}catch{return!1}}function ye(e,t,n,r){if(n===null)return;let i=t=>{throw new C(`validation_failed`,`field '${e}': ${t}`)};switch(t){case`number`:(typeof n!=`number`||!Number.isFinite(n))&&i(`expected a finite number`);return;case`boolean`:typeof n!=`boolean`&&i(`expected a boolean`);return;case`text`:typeof n!=`string`&&i(`expected a string`);return;case`email`:(typeof n!=`string`||!_e.test(n))&&i(`expected a valid email`);return;case`url`:(typeof n!=`string`||!ve(n))&&i(`expected a valid URL`);return;case`select`:typeof n!=`string`&&i(`expected a string`),r&&!r.includes(n)&&i(`not one of the allowed options`);return;case`datetime`:case`date`:(typeof n!=`string`||Number.isNaN(Date.parse(n)))&&i(`expected an ISO date string`);return;case`relation`:typeof n!=`string`&&i(`expected a related record id`);return;case`json`:return}}const be=/^[a-z_][a-z0-9_]*$/;function w(e){if(!be.test(e)||e.length>63)throw Error(`unsafe SQL identifier: ${e}`);return`"${e}"`}function T(e,t){let n=`${e}_${t}_idx`;if(n.length<=63)return n;let r=`_${f(`sha256`).update(`${e}.${t}`).digest(`hex`).slice(0,6)}_idx`;return`${e}_${t}`.slice(0,63-r.length)+r}function xe(){let e=BigInt(Date.now()),t=p(16);t[0]=Number(e>>40n&255n),t[1]=Number(e>>32n&255n),t[2]=Number(e>>24n&255n),t[3]=Number(e>>16n&255n),t[4]=Number(e>>8n&255n),t[5]=Number(e&255n),t[6]=t[6]&15|112,t[8]=t[8]&63|128;let n=t.toString(`hex`);return`${n.slice(0,8)}-${n.slice(8,12)}-${n.slice(12,16)}-${n.slice(16,20)}-${n.slice(20)}`}const E=e=>e.store;function D(e){if(e.tier!==`service`)throw new C(`forbidden`,`this operation requires a service key`)}async function O(e,t,n){let r=e;r.tier!==`service`&&ge(await r.store.getRules(t),n,r.tier,t)}function Se(){let e=m.Router();return e.get(`/tables`,async(e,t)=>{D(e),t.json({tables:await E(e).listTables()})}),e.get(`/migrations`,async(e,t)=>{D(e),t.json({migrations:await E(e).migrationHistory()})}),e.get(`/trash`,async(e,t)=>{D(e),t.json({trash:await E(e).listTrash()})}),e.get(`/tables/:table`,async(e,t)=>{D(e),t.json({table:await E(e).describeTable(String(e.params.table))})}),e.post(`/tables`,async(e,t)=>{D(e),await E(e).createTable(e.body?.name),t.status(201).json({ok:!0})}),e.post(`/tables/:table/fields`,async(e,t)=>{D(e);let{name:n,kind:r,required:i,unique:a,indexed:o,options:s,target:c}=e.body??{};await E(e).addField(String(e.params.table),n,r,{required:i,unique:a,indexed:o,options:s,target:c}),t.status(201).json({ok:!0})}),e.delete(`/tables/:table`,async(e,t)=>{D(e),await E(e).dropTable(String(e.params.table)),t.status(204).end()}),e.post(`/tables/:table/restore`,async(e,t)=>{D(e),await E(e).restoreTable(String(e.params.table)),t.json({ok:!0})}),e.post(`/tables/:table/rules`,async(e,t)=>{D(e),await E(e).setAccessRule(String(e.params.table),e.body?.op,e.body?.rule),t.json({ok:!0})}),e.post(`/tables/:table/fields/:field/rename`,async(e,t)=>{D(e),await E(e).renameField(String(e.params.table),String(e.params.field),e.body?.to),t.json({ok:!0})}),e.post(`/tables/:table/fields/:field/change-type`,async(e,t)=>{D(e),await E(e).changeType(String(e.params.table),String(e.params.field),e.body?.kind),t.json({ok:!0})}),e.post(`/tables/:table/fields/:field/index`,async(e,t)=>{D(e),await E(e).createIndex(String(e.params.table),String(e.params.field)),t.status(201).json({ok:!0})}),e.delete(`/tables/:table/fields/:field`,async(e,t)=>{D(e),await E(e).dropField(String(e.params.table),String(e.params.field)),t.status(204).end()}),e.post(`/tables/:table/fields/:field/restore`,async(e,t)=>{D(e),await E(e).restoreField(String(e.params.table),String(e.params.field)),t.json({ok:!0})}),e.post(`/tables/:table/records`,async(e,t)=>{let n=String(e.params.table);await O(e,n,`create`),t.status(201).json({record:await E(e).createRecord(n,e.body?.data)})}),e.post(`/tables/:table/query`,async(e,t)=>{let n=String(e.params.table);await O(e,n,`list`);let r=fe.safeParse(e.body??{});if(!r.success)throw new C(`validation_failed`,r.error.message);t.json(await E(e).queryRecords(n,r.data))}),e.get(`/tables/:table/records/:id`,async(e,t)=>{let n=String(e.params.table);await O(e,n,`read`),t.json({record:await E(e).getRecord(n,String(e.params.id))})}),e.patch(`/tables/:table/records/:id`,async(e,t)=>{let n=String(e.params.table);await O(e,n,`update`);let r=await E(e).updateRecord(n,String(e.params.id),e.body?.data);t.json({record:r})}),e.delete(`/tables/:table/records/:id`,async(e,t)=>{let n=String(e.params.table);await O(e,n,`delete`),await E(e).deleteRecord(n,String(e.params.id)),t.status(204).end()}),e}function Ce(e,t,n,r){if(e instanceof C){n.status(e.httpStatus).json(e.toBody());return}console.error(`unhandled error`,e),n.status(500).json({code:`internal`,message:`internal error`})}function we(e){let t=m();t.disable(`x-powered-by`),t.use(m.json());let n=m.Router();return n.use(async(t,n,r)=>{try{let{store:n,tier:i}=await e.resolve(t);t.store=n,t.tier=i,r()}catch(e){r(e)}}),n.use(Se()),t.use(`/v1`,n),t.use(Ce),t}const Te={id:`text`,created_at:`datetime`,updated_at:`datetime`};function k(e,t){if(t==null)return null;switch(e){case`boolean`:return+!!t;case`json`:return JSON.stringify(t);case`datetime`:return new Date(t).toISOString();default:return t}}function Ee(e,t){if(t==null)return null;switch(e){case`boolean`:return t===1||t===!0;case`json`:return typeof t==`string`?JSON.parse(t):t;case`number`:return typeof t==`string`?Number(t):t;default:return t}}function A(e,t){let n=[];for(let[r,i]of Object.entries(e))if(r===`and`||r===`or`){if(!Array.isArray(i))throw new C(`invalid_filter`,`'${r}' expects an array of filters`);let e=i.map(e=>A(e,t)).filter(e=>e!==void 0);if(e.length>0){let t=r===`and`?` and `:` or `;n.push({sql:`(${e.map(e=>e.sql).join(t)})`,params:e.flatMap(e=>e.params)})}}else if(r===`not`){let e=A(i,t);e&&n.push({sql:`not (${e.sql})`,params:e.params})}else{let e=t.kinds.get(r);if(!e)throw new C(`invalid_filter`,`unknown field: ${r}`);n.push(De(r,i,e))}if(n.length!==0)return{sql:n.map(e=>e.sql).join(` and `),params:n.flatMap(e=>e.params)}}function De(e,t,n){if(Array.isArray(t))return j(e,`in`,t,n);if(typeof t!=`object`||!t)return j(e,`eq`,t,n);let r=Object.entries(t).map(([t,r])=>j(e,t,r,n));if(r.length===0)throw new C(`invalid_filter`,`empty condition for field: ${e}`);return{sql:r.map(e=>e.sql).join(` and `),params:r.flatMap(e=>e.params)}}function j(e,t,n,r){if(!v[r].ops.includes(t))throw new C(`invalid_filter`,`operator '${t}' is not valid for a ${r} field`);let i=w(e),a=e=>k(r,e);switch(t){case`eq`:return{sql:`${i} = ?`,params:[a(n)]};case`neq`:return{sql:`${i} <> ?`,params:[a(n)]};case`gt`:return{sql:`${i} > ?`,params:[a(n)]};case`gte`:return{sql:`${i} >= ?`,params:[a(n)]};case`lt`:return{sql:`${i} < ?`,params:[a(n)]};case`lte`:return{sql:`${i} <= ?`,params:[a(n)]};case`in`:case`nin`:{if(!Array.isArray(n))throw new C(`invalid_filter`,`'${t}' expects an array`);if(n.length===0)return{sql:t===`in`?`1 = 0`:`1 = 1`,params:[]};let e=n.map(()=>`?`).join(`, `);return{sql:`${i} ${t===`in`?`in`:`not in`} (${e})`,params:n.map(a)}}case`contains`:return{sql:`instr(${i}, ?) > 0`,params:[String(n)]};case`startsWith`:return{sql:`substr(${i}, 1, length(?)) = ?`,params:[String(n),String(n)]};case`endsWith`:return{sql:`substr(${i}, -length(?)) = ?`,params:[String(n),String(n)]};case`isNull`:return{sql:n?`${i} is null`:`${i} is not null`,params:[]}}}function Oe(e,t){let n=[];for(let r of e??[]){let e=r.startsWith(`-`)?`desc`:`asc`,i=r.startsWith(`-`)?r.slice(1):r,a=t.kinds.get(i);if(!a)throw new C(`invalid_filter`,`unknown sort field: ${i}`);if(!v[a].sortable)throw new C(`invalid_filter`,`field is not sortable: ${i}`);n.push({field:i,dir:e})}return n.push({field:`id`,dir:`asc`}),n}function ke(e){return e.map(e=>`${w(e.field)} ${e.dir}`).join(`, `)}function Ae(e,t,n){let r=[],i=[];for(let a=0;a<e.length;a++){let o=[];for(let r=0;r<a;r++)o.push(`${w(e[r].field)} = ?`),i.push(k(je(n,e[r].field),t[r]));let s=w(e[a].field);o.push(e[a].dir===`asc`?`${s} > ?`:`${s} < ?`),i.push(k(je(n,e[a].field),t[a])),r.push(`(${o.join(` and `)})`)}return{sql:r.join(` or `),params:i}}function je(e,t){let n=e.kinds.get(t);if(!n)throw new C(`invalid_filter`,`unknown field: ${t}`);return n}const Me=e=>Buffer.from(JSON.stringify(e)).toString(`base64url`);function Ne(e){try{let t=JSON.parse(Buffer.from(e,`base64url`).toString(`utf8`));if(!Array.isArray(t))throw Error(`not an array`);return t}catch{throw new C(`validation_failed`,`invalid cursor`)}}function Pe(e,t){let n={};for(let r of t)n[r]=e[r];return n}const Fe=new Set([`text`,`number`,`boolean`,`datetime`,`date`,`email`,`url`]),Ie=10080*60*1e3;var Le=class{db;constructor(e=`:memory:`){this.db=new h(e),this.db.exec(`pragma foreign_keys = on`),this.db.exec(`pragma journal_mode = wal`),this.ensureMeta()}close(){this.db.close()}all(e,t=[]){return this.db.prepare(e).all(...t)}get(e,t=[]){return this.db.prepare(e).get(...t)}run(e,t=[]){return this.db.prepare(e).run(...t)}tx(e){this.db.exec(`begin`);try{let t=e();return this.db.exec(`commit`),t}catch(e){throw this.db.exec(`rollback`),e}}now(){return new Date().toISOString()}ensureMeta(){this.db.exec(`create table if not exists "_meta_table" (
2
+ "name" text primary key,
3
+ "system" integer not null default 0,
4
+ "rules" text,
5
+ "created_at" text not null
6
+ )`),this.db.exec(`create table if not exists "_meta_field" (
7
+ "table_name" text not null,
8
+ "name" text not null,
9
+ "kind" text not null,
10
+ "required" integer not null default 0,
11
+ "options" text,
12
+ "relation_target" text,
13
+ "created_at" text not null,
14
+ primary key ("table_name", "name")
15
+ )`),this.db.exec(`create table if not exists "_migrations" (
16
+ "id" integer primary key autoincrement,
17
+ "op" text not null, "target" text not null, "detail" text, "at" text not null
18
+ )`),this.db.exec(`create table if not exists "_trash" (
19
+ "id" integer primary key autoincrement,
20
+ "kind" text not null, "target" text not null, "physical_name" text not null,
21
+ "snapshot" text not null, "trashed_at" text not null, "purge_after" text not null
22
+ )`)}recordMigration(e,t,n){this.run(`insert into "_migrations" ("op","target","detail","at") values (?,?,?,?)`,[e,t,n??null,this.now()])}tableShape(e){if(!this.get(`select 1 from "_meta_table" where "name" = ?`,[e]))throw new C(`table_not_found`,`table not found: ${e}`);let t=this.all(`select "name","kind","required","options","relation_target" from "_meta_field" where "table_name" = ?`,[e]),n=new Map(Object.entries(Te)),r=new Set,i=new Map,a=new Map,o=new Set;for(let e of t)n.set(e.name,e.kind),r.add(e.name),e.options&&i.set(e.name,JSON.parse(e.options)),e.relation_target&&a.set(e.name,e.relation_target),e.required&&o.add(e.name);return{kinds:n,userFields:r,selectOptions:i,relationTargets:a,required:o}}pickUserData(e,t){let n={};for(let[r,i]of Object.entries(t)){let t=e.kinds.get(r);if(!t||!e.userFields.has(r))throw new C(`validation_failed`,`unknown or non-writable field: ${r}`);ye(r,t,i,e.selectOptions.get(r)),n[r]=i}return n}coerceRow(e,t){let n={};for(let[r,i]of Object.entries(t)){let t=e.kinds.get(r);t&&(n[r]=Ee(t,i))}return n}mapDbError(e){let t=String(e.message??e);throw t.includes(`UNIQUE constraint`)?new C(`unique_violation`,`a unique constraint was violated`):t.includes(`FOREIGN KEY constraint`)?new C(`validation_failed`,`a related record does not exist`):e}async createRecord(e,t){if(typeof t!=`object`||!t||Array.isArray(t))throw new C(`validation_failed`,`record data must be an object`);return this.tx(()=>{let n=this.tableShape(e),r=this.pickUserData(n,t);for(let e of n.required)if(r[e]===void 0||r[e]===null)throw new C(`validation_failed`,`a required field is missing`);let i=this.now(),a=[`id`,`created_at`,`updated_at`,...Object.keys(r)],o=[xe(),i,i,...Object.keys(r).map(e=>k(n.kinds.get(e),r[e]))],s=a.map(e=>w(e)).join(`, `),c=a.map(()=>`?`).join(`, `);try{this.run(`insert into ${w(e)} (${s}) values (${c})`,o)}catch(e){this.mapDbError(e)}let l=this.get(`select * from ${w(e)} where "id" = ?`,[o[0]]);return this.coerceRow(n,l)})}async getRecord(e,t){let n=this.tableShape(e),r=this.get(`select * from ${w(e)} where "id" = ?`,[t]);if(!r)throw new C(`record_not_found`,`record not found: ${t}`);return this.coerceRow(n,r)}async updateRecord(e,t,n){if(typeof n!=`object`||!n||Array.isArray(n))throw new C(`validation_failed`,`patch must be an object`);return this.tx(()=>{let r=this.tableShape(e),i=this.pickUserData(r,n);for(let[e,t]of Object.entries(i))if(r.required.has(e)&&t===null)throw new C(`validation_failed`,`a required field is missing`);let a=this.now(),o=Object.keys(i),s=[...o.map(e=>`${w(e)} = ?`),`"updated_at" = ?`].join(`, `),c=[...o.map(e=>k(r.kinds.get(e),i[e])),a,t],l;try{l=this.run(`update ${w(e)} set ${s} where "id" = ?`,c)}catch(e){this.mapDbError(e)}if(Number(l.changes)===0)throw new C(`record_not_found`,`record not found: ${t}`);let u=this.get(`select * from ${w(e)} where "id" = ?`,[t]);return this.coerceRow(r,u)})}async deleteRecord(e,t){this.tableShape(e);let n=this.run(`delete from ${w(e)} where "id" = ?`,[t]);if(Number(n.changes)===0)throw new C(`record_not_found`,`record not found: ${t}`)}async queryRecords(e,t){let n=this.tableShape(e),r=Math.min(t.limit??50,200),i=Oe(t.sort,n);for(let e of t.select??[])if(!n.kinds.has(e))throw new C(`invalid_filter`,`unknown select field: ${e}`);let a=t.cursor?Ne(t.cursor):void 0;if(a&&a.length!==i.length)throw new C(`validation_failed`,`cursor does not match the sort`);let o=t.filter?A(t.filter,n):void 0,s=Re([o,a?Ae(i,a,n):void 0]),c=ke(i),l=this.all(`select * from ${w(e)} ${s?`where ${s.sql}`:``} order by ${c} limit ?`,[...s?.params??[],r+1]),u;if(t.count){let t=this.get(`select count(*) as c from ${w(e)} ${o?`where ${o.sql}`:``}`,o?.params??[]);u=Number(t?.c??0)}let d=l.length>r,f=d?l.slice(0,r):l,p=f.map(e=>this.coerceRow(n,e));t.expand&&t.expand.length>0&&await this.applyExpand(n,p,t.expand);let m={records:t.select?p.map(e=>Pe(e,t.select)):p},h=f[f.length-1];return d&&h&&(m.nextCursor=Me(i.map(e=>this.coerceRow(n,h)[e.field]))),u!==void 0&&(m.count=u),m}async applyExpand(e,t,n){for(let r of n){let n=e.relationTargets.get(r);if(e.kinds.get(r)!==`relation`||!n)throw new C(`invalid_filter`,`not a relation field: ${r}`);let i=[...new Set(t.map(e=>e[r]).filter(e=>e!=null))],a=new Map;if(i.length>0){let e=this.tableShape(n),t=i.map(()=>`?`).join(`, `),r=this.all(`select * from ${w(n)} where "id" in (${t})`,i);for(let t of r){let n=this.coerceRow(e,t);a.set(n.id,n)}}for(let e of t)e[r]=e[r]==null?null:a.get(e[r])??null}}async createTable(e){if(typeof e!=`string`||!y(e)||se(e))throw new C(`reserved_name`,`invalid or reserved table name: ${String(e)}`);this.tx(()=>{if(this.get(`select 1 from "_meta_table" where "name" = ?`,[e]))throw new C(`table_exists`,`table already exists: ${e}`);this.db.exec(`create table ${w(e)} (
23
+ "id" text primary key not null,
24
+ "created_at" text not null,
25
+ "updated_at" text not null
26
+ )`),this.run(`insert into "_meta_table" ("name","system","created_at") values (?,0,?)`,[e,this.now()]),this.recordMigration(`create_table`,e)})}async addField(e,t,n,r={}){if(typeof t!=`string`||!y(t)||ce(t))throw new C(`reserved_name`,`invalid or reserved field name: ${String(t)}`);if(typeof n!=`string`||!(n in v))throw new C(`validation_failed`,`unknown field kind: ${String(n)}`);let i=null;if(n===`select`){if(!Array.isArray(r.options)||r.options.length===0||!r.options.every(e=>typeof e==`string`))throw new C(`validation_failed`,`a select field requires a non-empty options array`);i=r.options}let a=null;if(n===`relation`){if(typeof r.target!=`string`||!y(r.target))throw new C(`validation_failed`,`a relation field requires a valid target table`);a=r.target}this.tx(()=>{if(!this.get(`select 1 from "_meta_table" where "name" = ?`,[e]))throw new C(`table_not_found`,`table not found: ${e}`);if(this.get(`select 1 from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]))throw new C(`field_exists`,`field already exists: ${t}`);let o=w(t);if(a){if(!this.get(`select 1 from "_meta_table" where "name" = ?`,[a]))throw new C(`table_not_found`,`relation target not found: ${a}`);o=`${w(t)} references ${w(a)}("id") on delete set null`}this.db.exec(`alter table ${w(e)} add column ${o}`),this.run(`insert into "_meta_field" ("table_name","name","kind","required","options","relation_target","created_at")
27
+ values (?,?,?,?,?,?,?)`,[e,t,n,+!!r.required,i?JSON.stringify(i):null,a,this.now()]),this.createFieldIndex(e,t,!!r.unique,!!r.indexed),this.recordMigration(`add_field`,`${e}.${t}`,n)})}createFieldIndex(e,t,n,r){n?this.db.exec(`create unique index ${w(T(e,t))} on ${w(e)} (${w(t)})`):r&&this.db.exec(`create index ${w(T(e,t))} on ${w(e)} (${w(t)})`)}dropFieldIndex(e,t){this.db.exec(`drop index if exists ${w(T(e,t))}`)}fieldIndexState(e,t){for(let n of this.all(`select "name","unique" from pragma_index_list(?)`,[e])){let e=this.all(`select "name" from pragma_index_info(?)`,[String(n.name)]);if(e.length===1&&String(e[0].name)===t)return{unique:!!n.unique,indexed:!0}}return{unique:!1,indexed:!1}}fieldSnapshot(e,t){return{...t,...this.fieldIndexState(e,t.name)}}async renameField(e,t,n){if(typeof n!=`string`||!y(n)||ce(n))throw new C(`reserved_name`,`invalid or reserved field name: ${String(n)}`);this.tx(()=>{if(!this.get(`select 1 from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]))throw new C(`field_not_found`,`field not found: ${t}`);if(this.get(`select 1 from "_meta_field" where "table_name" = ? and "name" = ?`,[e,n]))throw new C(`field_exists`,`field already exists: ${n}`);let r=this.fieldIndexState(e,t);this.dropFieldIndex(e,t),this.db.exec(`alter table ${w(e)} rename column ${w(t)} to ${w(n)}`),this.createFieldIndex(e,n,r.unique,r.indexed),this.run(`update "_meta_field" set "name" = ? where "table_name" = ? and "name" = ?`,[n,e,t]),this.recordMigration(`rename_field`,`${e}.${t}`,n)})}async changeType(e,t,n){if(typeof n!=`string`||!(n in v))throw new C(`validation_failed`,`unknown field kind: ${String(n)}`);if(!Fe.has(n))throw new C(`validation_failed`,`cannot change a field to '${n}'`);this.tx(()=>{let r=this.get(`select "kind" from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]);if(!r)throw new C(`field_not_found`,`field not found: ${t}`);if(!Fe.has(r.kind))throw new C(`validation_failed`,`cannot change a '${r.kind}' field`);let i=this.all(`select "id", ${w(t)} as v from ${w(e)}`);for(let r of i){if(r.v===null||r.v===void 0)continue;let i=ze(n,r.v);if(i===void 0)throw new C(`lossy_type_change`,`existing data can't be cast to ${n}`);this.run(`update ${w(e)} set ${w(t)} = ? where "id" = ?`,[k(n,i),r.id])}this.run(`update "_meta_field" set "kind" = ? where "table_name" = ? and "name" = ?`,[n,e,t]),this.recordMigration(`change_type`,`${e}.${t}`,n)})}async createIndex(e,t){this.tx(()=>{if(!this.get(`select 1 from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]))throw new C(`field_not_found`,`field not found: ${t}`);this.db.exec(`create index if not exists ${w(T(e,t))} on ${w(e)} (${w(t)})`),this.recordMigration(`create_index`,`${e}.${t}`)})}async setAccessRule(e,t,n){if(typeof t!=`string`||!me.includes(t))throw new C(`validation_failed`,`invalid operation: ${String(t)}`);if(typeof n!=`string`||!he.includes(n))throw new C(`validation_failed`,`invalid rule: ${String(n)}`);this.tx(()=>{let r=this.get(`select "rules" from "_meta_table" where "name" = ?`,[e]);if(!r)throw new C(`table_not_found`,`table not found: ${e}`);let i=r.rules?JSON.parse(r.rules):{};i[t]=n,this.run(`update "_meta_table" set "rules" = ? where "name" = ?`,[JSON.stringify(i),e]),this.recordMigration(`set_access_rule`,`${e}.${t}`,n)})}trashedName(e){return`_trashed_${e.slice(0,20)}_${p(6).toString(`hex`)}`}async dropField(e,t){this.tx(()=>{let n=this.all(`select "table_name","name","kind","required","options","relation_target"
28
+ from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]);if(n.length===0)throw new C(`field_not_found`,`field not found: ${t}`);let r=this.fieldSnapshot(e,n[0]);this.dropFieldIndex(e,t);let i=this.trashedName(t);this.db.exec(`alter table ${w(e)} rename column ${w(t)} to ${w(i)}`),this.run(`delete from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]),this.run(`insert into "_trash" ("kind","target","physical_name","snapshot","trashed_at","purge_after") values ('field',?,?,?,?,?)`,[`${e}.${t}`,i,JSON.stringify(r),this.now(),new Date(Date.now()+Ie).toISOString()]),this.recordMigration(`drop_field`,`${e}.${t}`)})}async dropTable(e){this.tx(()=>{let t=this.get(`select "system","rules" from "_meta_table" where "name" = ?`,[e]);if(!t)throw new C(`table_not_found`,`table not found: ${e}`);let n=this.all(`select "table_name","name","kind","required","options","relation_target"
29
+ from "_meta_field" where "table_name" = ?`,[e]),r=this.trashedName(e),i=n.map(t=>this.fieldSnapshot(e,t));for(let t of n)this.dropFieldIndex(e,t.name);this.db.exec(`alter table ${w(e)} rename to ${w(r)}`),this.run(`delete from "_meta_field" where "table_name" = ?`,[e]),this.run(`delete from "_meta_table" where "name" = ?`,[e]),this.run(`insert into "_trash" ("kind","target","physical_name","snapshot","trashed_at","purge_after") values ('table',?,?,?,?,?)`,[e,r,JSON.stringify({table:{system:t.system,rules:t.rules},fields:i}),this.now(),new Date(Date.now()+Ie).toISOString()]),this.recordMigration(`drop_table`,e)})}async restoreTable(e){this.tx(()=>{if(this.get(`select 1 from "_meta_table" where "name" = ?`,[e]))throw new C(`table_exists`,`a table already exists with this name: ${e}`);let t=this.takeTrash(`table`,e);if(!t)throw new C(`table_not_found`,`nothing to restore for table: ${e}`);let n=JSON.parse(t.snapshot);this.db.exec(`alter table ${w(t.physical_name)} rename to ${w(e)}`),this.run(`insert into "_meta_table" ("name","system","rules","created_at") values (?,?,?,?)`,[e,+!!n.table.system,typeof n.table.rules==`string`?n.table.rules:JSON.stringify(n.table.rules??null),this.now()]);for(let t of n.fields)this.reinsertMetaField(t),this.createFieldIndex(e,t.name,t.unique,t.indexed);this.run(`delete from "_trash" where "id" = ?`,[t.id]),this.recordMigration(`restore_table`,e)})}async restoreField(e,t){this.tx(()=>{if(this.get(`select 1 from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]))throw new C(`field_exists`,`a field already exists with this name: ${t}`);let n=this.takeTrash(`field`,`${e}.${t}`);if(!n)throw new C(`field_not_found`,`nothing to restore for field: ${e}.${t}`);this.db.exec(`alter table ${w(e)} rename column ${w(n.physical_name)} to ${w(t)}`);let r=JSON.parse(n.snapshot);this.reinsertMetaField(r),this.createFieldIndex(e,t,r.unique,r.indexed),this.run(`delete from "_trash" where "id" = ?`,[n.id]),this.recordMigration(`restore_field`,`${e}.${t}`)})}takeTrash(e,t){let n=this.get(`select "id","physical_name","snapshot" from "_trash" where "kind" = ? and "target" = ? order by "id" desc limit 1`,[e,t]);return n?{id:Number(n.id),physical_name:String(n.physical_name),snapshot:String(n.snapshot)}:null}reinsertMetaField(e){this.run(`insert into "_meta_field" ("table_name","name","kind","required","options","relation_target","created_at")
30
+ values (?,?,?,?,?,?,?)`,[e.table_name,e.name,e.kind,e.required,e.options,e.relation_target,this.now()])}async purgeExpiredTrash(){let e=this.all(`select "id","kind","physical_name","target" from "_trash" where "purge_after" <= ? order by "id"`,[this.now()]),t=0;for(let n of e)this.tx(()=>{if(n.kind===`table`)this.db.exec(`drop table if exists ${w(String(n.physical_name))}`);else{let e=String(n.target),t=e.slice(0,e.lastIndexOf(`.`)),r=e.slice(e.lastIndexOf(`.`)+1);this.dropFieldIndex(t,r),this.db.exec(`alter table ${w(t)} drop column ${w(String(n.physical_name))}`)}this.run(`delete from "_trash" where "id" = ?`,[n.id])}),t+=1;return t}async listTables(){return this.all(`select "name" from "_meta_table" order by "name"`).map(e=>String(e.name))}async migrationHistory(){return this.all(`select "op","target","detail","at" from "_migrations" order by "id"`).map(e=>({op:String(e.op),target:String(e.target),detail:e.detail===null?null:String(e.detail),at:String(e.at)}))}async listTrash(){return this.all(`select "kind","target","trashed_at","purge_after" from "_trash" order by "id" desc`).map(e=>({kind:e.kind,target:String(e.target),trashedAt:String(e.trashed_at),purgeAfter:String(e.purge_after)}))}async getRules(e){let t=this.get(`select "rules" from "_meta_table" where "name" = ?`,[e]);if(!t)throw new C(`table_not_found`,`table not found: ${e}`);return t.rules?JSON.parse(t.rules):{}}async describeTable(e){if(!this.get(`select 1 from "_meta_table" where "name" = ?`,[e]))throw new C(`table_not_found`,`table not found: ${e}`);let t=this.all(`select "name","kind","options","relation_target" from "_meta_field" where "table_name" = ?`,[e]),n=new Map(t.map(e=>[String(e.name),e])),r=this.all(`select "name","notnull" from pragma_table_info(?)`,[e]),i=new Map;for(let t of this.all(`select "name","unique" from pragma_index_list(?)`,[e])){let e=this.all(`select "name" from pragma_index_info(?)`,[String(t.name)]);if(e.length===1){let n=String(e[0].name);i.set(n,!!t.unique||(i.get(n)??!1))}}let a=[];for(let t of r){let r=String(t.name),o=n.get(r),s=o?.kind??Te[r];if(!s)continue;let c={name:r,kind:s,required:o?this.fieldRequired(e,r):t.notnull===1,unique:i.get(r)===!0,indexed:i.has(r),system:!o};o?.options&&(c.options=JSON.parse(String(o.options))),o?.relation_target&&(c.relationTarget=String(o.relation_target)),a.push(c)}return{name:e,fields:a}}fieldRequired(e,t){return!!this.get(`select "required" from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t])?.required}async tablesOverview(){let e=await this.listTables(),t=[];for(let n of e){let e=await this.describeTable(n),r=this.get(`select count(*) as c from ${w(n)}`);t.push({name:n,recordCount:Number(r?.c??0),fields:e.fields})}return t}};function Re(e){let t=e.filter(e=>e!==void 0);if(t.length!==0)return{sql:t.map(e=>`(${e.sql})`).join(` and `),params:t.flatMap(e=>e.params)}}function ze(e,t){switch(e){case`number`:{let e=typeof t==`number`?t:Number(t);return Number.isFinite(e)?e:void 0}case`boolean`:return typeof t==`boolean`?t:t===1||t===`true`?!0:t===0||t===`false`?!1:void 0;default:return String(t)}}const Be="# >>> zeitlos (managed — edited by `zeitlos database`) >>>",Ve=`# <<< zeitlos <<<`,He=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`);function Ue(e,n){let i=s(e,`.env.local`),o=`${Be}\n${Object.entries(n).map(([e,t])=>`${e}=${t}`).join(`
31
+ `)}\n${Ve}`,c=t(i)?r(i,`utf8`):``,l=RegExp(`${He(Be)}[\\s\\S]*?${He(Ve)}`);return a(i,l.test(c)?c.replace(l,o):c.replace(/\s*$/,``)+(c.trim()?`
32
+
33
+ `:``)+`${o}\n`),i}var M=class extends Error{code;constructor(e,t){super(t),this.name=`GatewayError`,this.code=e}};function We(e){return{async call(t,n,r,i){let a;try{a=await fetch(e.resolveUrl(t,r),{method:n,headers:{"content-type":`application/json`,authorization:`Bearer ${e.authToken}`},body:i===void 0?void 0:JSON.stringify(i)})}catch{throw new M(`gateway_unreachable`,e.unreachableMessage)}if(a.status===204)return null;let o=await a.text(),s=o?JSON.parse(o):null;if(!a.ok)throw new M(s?.code??`internal`,s?.message??`request failed (${a.status})`);return s}}}const N={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},P={readOnlyHint:!1,destructiveHint:!1,openWorldHint:!1};function F(e,t){return{content:[{type:`text`,text:t??JSON.stringify(e,null,2)}],structuredContent:e}}function I(e){return{content:[{type:`text`,text:e}],isError:!0}}function Ge(e){return console.error(`mcp tool: unexpected error`,e),I(`An unexpected error occurred while handling this request.`)}const L={readOnlyHint:!1,destructiveHint:!0,openWorldHint:!1},Ke=[`list`,`read`,`create`,`update`,`delete`],qe=[`public`,`authenticated`,`owner`,`none`];async function R(e,t,n){if(!await e.authorizeProject(t))return I(`Project not found or you do not have access to it.`);try{return await n(e.gateway)}catch(e){if(e instanceof M)return I(e.message);throw e}}function z(e){return F({confirmationRequired:!0,message:`${e} Re-call this tool with confirm: true to proceed.`})}const B=d.string().describe(`The project id, e.g. prj-abc123.`),V=d.string().describe(`The table (collection) name.`),H=d.string().describe(`The field (column) name.`),U=d.boolean().optional().describe(`Must be true to actually perform this destructive operation.`),W=d.record(d.string(),d.unknown()),Je=d.object({name:d.string(),fields:d.array(d.record(d.unknown()))}),Ye={publishableKey:d.string().optional().describe(`Browser-safe anon key (dpk_…). Public by design; always retrievable.`),schema:d.string().optional(),role:d.string().optional()},G={ok:d.boolean().optional(),confirmationRequired:d.boolean().optional(),message:d.string().optional()},Xe=[{name:`enable_database`,title:`Enable the database`,description:`Enable the built-in database for a project (idempotent). Returns the publishable key (dpk_) — browser-safe, governed by per-table access rules; it's public by design and always retrievable. The backend service key is NOT returned here: it's injected into the app automatically at deploy time (as ZEITLOS_DATABASE_KEY) and rotates each deploy.`,inputSchema:{projectId:B},outputSchema:Ye,annotations:{...P,idempotentHint:!0,title:`Enable the database`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`POST`,`/enable`)))},{name:`list_tables`,title:`List tables`,description:`List the tables (collections) in a project's database.`,inputSchema:{projectId:B},outputSchema:{tables:d.array(d.string())},annotations:{...N,title:`List tables`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`GET`,`/tables`)))},{name:`describe_table`,title:`Describe a table`,description:`Get a table's full schema: every field with its kind, required/unique/indexed flags, select options, and relation target.`,inputSchema:{projectId:B,table:V},outputSchema:{table:Je},annotations:{...N,title:`Describe a table`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`GET`,`/tables/${encodeURIComponent(t.table)}`)))},{name:`migration_history`,title:`Schema change history`,description:`List the project's schema-change history (each create/alter/drop op, in order).`,inputSchema:{projectId:B},outputSchema:{migrations:d.array(d.record(d.unknown()))},annotations:{...N,title:`Schema change history`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`GET`,`/migrations`)))},{name:`list_trash`,title:`List restorable items`,description:`List soft-deleted tables and fields still inside the 7-day undo window, with when each will be permanently purged. Use restore_table / restore_field to bring one back.`,inputSchema:{projectId:B},outputSchema:{trash:d.array(d.record(d.unknown()))},annotations:{...N,title:`List restorable items`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`GET`,`/trash`)))},{name:`create_table`,title:`Create a table`,description:`Create a new table (collection). It starts with the system fields id, created_at, and updated_at; add your own with add_field. All access is locked to service keys until you open it with set_access_rule.`,inputSchema:{projectId:B,name:V},outputSchema:{ok:d.boolean()},annotations:{...P,idempotentHint:!1,title:`Create a table`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables`,{name:t.name}),F({ok:!0})))},{name:`drop_table`,title:`Drop a table`,description:`Soft-delete a table. Its data is preserved and restorable for 7 days (restore_table) before being permanently purged. Requires confirm: true.`,inputSchema:{projectId:B,table:V,confirm:U},outputSchema:G,annotations:{...L,idempotentHint:!1,title:`Drop a table`},handler:(e,t)=>R(e,t.projectId,async e=>t.confirm===!0?(await e.call(t.projectId,`DELETE`,`/tables/${encodeURIComponent(t.table)}`),F({ok:!0})):z(`This soft-deletes "${t.table}"; it is restorable for 7 days, then purged.`))},{name:`restore_table`,title:`Restore a table`,description:`Restore a soft-deleted table (and its data) within the undo window.`,inputSchema:{projectId:B,table:V},outputSchema:{ok:d.boolean()},annotations:{...P,idempotentHint:!1,title:`Restore a table`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/restore`),F({ok:!0})))},{name:`add_field`,title:`Add a field`,description:`Add a field (column) to a table. A 'select' field requires options; a 'relation' field requires target (the table it points at).`,inputSchema:{projectId:B,table:V,name:H,kind:d.enum(re).describe(`The field type.`),required:d.boolean().optional().describe(`Reject rows without a value. Default false.`),unique:d.boolean().optional().describe(`Enforce uniqueness. Default false.`),indexed:d.boolean().optional().describe(`Create an index for faster filtering.`),options:d.array(d.string()).optional().describe(`Allowed values for a 'select' field (required for select).`),target:d.string().optional().describe(`Target table for a 'relation' field (required for relation).`)},outputSchema:{ok:d.boolean()},annotations:{...P,idempotentHint:!1,title:`Add a field`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/fields`,{name:t.name,kind:t.kind,required:t.required,unique:t.unique,indexed:t.indexed,options:t.options,target:t.target}),F({ok:!0})))},{name:`rename_field`,title:`Rename a field`,description:`Rename a field, preserving its data.`,inputSchema:{projectId:B,table:V,field:H,to:d.string().describe(`New name.`)},outputSchema:{ok:d.boolean()},annotations:{...P,idempotentHint:!1,title:`Rename a field`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/fields/${encodeURIComponent(t.field)}/rename`,{to:t.to}),F({ok:!0})))},{name:`change_field_type`,title:`Change a field's type`,description:`Change a scalar field's type, casting existing data. Fails (without changing anything) if the existing data can't be cast to the new type.`,inputSchema:{projectId:B,table:V,field:H,kind:d.enum(re)},outputSchema:{ok:d.boolean()},annotations:{...P,idempotentHint:!0,title:`Change a field's type`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/fields/${encodeURIComponent(t.field)}/change-type`,{kind:t.kind}),F({ok:!0})))},{name:`index_field`,title:`Index a field`,description:`Add an index to a field for faster filtering and sorting.`,inputSchema:{projectId:B,table:V,field:H},outputSchema:{ok:d.boolean()},annotations:{...P,idempotentHint:!0,title:`Index a field`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/fields/${encodeURIComponent(t.field)}/index`),F({ok:!0})))},{name:`drop_field`,title:`Drop a field`,description:`Soft-delete a field. Its data is preserved and restorable for 7 days (restore_field) before being permanently purged. Requires confirm: true.`,inputSchema:{projectId:B,table:V,field:H,confirm:U},outputSchema:G,annotations:{...L,idempotentHint:!1,title:`Drop a field`},handler:(e,t)=>R(e,t.projectId,async e=>t.confirm===!0?(await e.call(t.projectId,`DELETE`,`/tables/${encodeURIComponent(t.table)}/fields/${encodeURIComponent(t.field)}`),F({ok:!0})):z(`This soft-deletes "${t.table}.${t.field}"; it is restorable for 7 days, then purged.`))},{name:`restore_field`,title:`Restore a field`,description:`Restore a soft-deleted field (and its data) within the undo window.`,inputSchema:{projectId:B,table:V,field:H},outputSchema:{ok:d.boolean()},annotations:{...P,idempotentHint:!1,title:`Restore a field`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/fields/${encodeURIComponent(t.field)}/restore`),F({ok:!0})))},{name:`set_access_rule`,title:`Set a table's access rule`,description:`Set who may perform an operation on a table via the publishable (anon) key. Rules: 'public' (anyone), 'authenticated' (any signed-in user), 'owner' (the record's owner), 'none' (service keys only). Ops start at 'none'.`,inputSchema:{projectId:B,table:V,op:d.enum(Ke).describe(`The row operation to govern.`),rule:d.enum(qe).describe(`Who may perform it.`)},outputSchema:{ok:d.boolean()},annotations:{...P,idempotentHint:!0,title:`Set a table's access rule`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/rules`,{op:t.op,rule:t.rule}),F({ok:!0})))},{name:`create_record`,title:`Create a record`,description:"Insert a record into a table. `data` maps field names to values.",inputSchema:{projectId:B,table:V,data:W.describe(`Field -> value.`)},outputSchema:{record:W},annotations:{...P,idempotentHint:!1,title:`Create a record`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/records`,{data:t.data})))},{name:`get_record`,title:`Get a record`,description:`Fetch a single record by id.`,inputSchema:{projectId:B,table:V,id:d.string().describe(`The record id.`)},outputSchema:{record:W},annotations:{...N,title:`Get a record`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`GET`,`/tables/${encodeURIComponent(t.table)}/records/${encodeURIComponent(t.id)}`)))},{name:`query_records`,title:`Query records`,description:"Query a table with filtering, sorting, and cursor pagination. `filter` uses the query DSL: a map of field -> value (equals) or field -> {op: value} (ops: eq, neq, in, nin, gt, gte, lt, lte, contains, startsWith, endsWith, isNull), composed with and/or/not. `sort` takes field names ('-field' for descending). Use `expand` to inline relations.",inputSchema:{projectId:B,table:V,filter:d.record(d.string(),d.unknown()).optional().describe(`Query DSL filter.`),sort:d.array(d.string()).optional().describe(`Sort fields; prefix '-' for descending.`),limit:d.number().int().positive().max(200).optional().describe(`Page size (max 200).`),cursor:d.string().optional().describe(`Opaque cursor from a prior page's nextCursor.`),select:d.array(d.string()).optional().describe(`Only return these fields.`),expand:d.array(d.string()).optional().describe(`Relation fields to inline.`),count:d.boolean().optional().describe(`Also return the total match count.`)},outputSchema:{records:d.array(W),nextCursor:d.string().optional(),count:d.number().optional()},annotations:{...N,title:`Query records`},handler:(e,t)=>R(e,t.projectId,async e=>{let n={};for(let e of[`filter`,`sort`,`limit`,`cursor`,`select`,`expand`,`count`])t[e]!==void 0&&(n[e]=t[e]);return F(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/query`,n))})},{name:`update_record`,title:`Update a record`,description:"Patch a record by id. `data` holds only the fields to change.",inputSchema:{projectId:B,table:V,id:d.string().describe(`The record id.`),data:W.describe(`Fields to change -> new values.`)},outputSchema:{record:W},annotations:{...P,idempotentHint:!0,title:`Update a record`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`PATCH`,`/tables/${encodeURIComponent(t.table)}/records/${encodeURIComponent(t.id)}`,{data:t.data})))},{name:`delete_record`,title:`Delete a record`,description:`Permanently delete a record by id. This is NOT undoable. Requires confirm: true.`,inputSchema:{projectId:B,table:V,id:d.string().describe(`The record id.`),confirm:U},outputSchema:G,annotations:{...L,idempotentHint:!0,title:`Delete a record`},handler:(e,t)=>R(e,t.projectId,async e=>t.confirm===!0?(await e.call(t.projectId,`DELETE`,`/tables/${encodeURIComponent(t.table)}/records/${encodeURIComponent(t.id)}`),F({ok:!0})):z(`This permanently deletes the record; it cannot be undone.`))}];function Ze(e,t,n=Xe){for(let r of n)e.registerTool(r.name,{title:r.title,description:r.description,inputSchema:r.inputSchema,outputSchema:r.outputSchema,annotations:r.annotations},async e=>{try{return await r.handler(t,e)}catch(e){return Ge(e)}})}function Qe(e,t={}){let n=new te(t.serverInfo??{name:`zeitlos`,version:`0.0.0`},{instructions:t.instructions??`Manage your Zeitlos database — create tables and fields, set access rules, and read/write records.`});return Ze(n,e,t.tools),n}function $e(e,t){let n=e.replace(/\/$/,``);return We({resolveUrl:(e,t)=>`${n}/v1${t}`,authToken:t,unreachableMessage:`The local database is not reachable.`})}const et=Xe.filter(e=>e.name!==`enable_database`).map(e=>{let{projectId:t,...n}=e.inputSchema;return{...e,inputSchema:n,handler:(t,n)=>e.handler(t,{...n,projectId:`local`})}});function tt(e,t){let n={gateway:$e(t.url,t.key),authorizeProject:()=>Promise.resolve(!0)};e.post(`/mcp`,async(e,t)=>{let r=Qe(n,{tools:et,serverInfo:{name:`zeitlos-local`,version:`0.0.0`},instructions:`This is the developer's LOCAL Zeitlos database — a single-project SQLite emulator running on localhost for local development. It is NOT the production/cloud database, and changes here affect only the local dev environment. Use it to build and test schema and data locally. If a production Zeitlos MCP is also connected, use this one for local development and that one for the deployed project's real data. Manage tables, fields, access rules, and records here.`}),i=new ee({sessionIdGenerator:void 0});t.on(`close`,()=>{i.close(),r.close()}),await r.connect(i),await i.handleRequest(e,t,e.body)})}function K(e,t){let n=c(e,t);return n&&!n.startsWith(`..`)?n:t}function q(e=process.cwd()){let n=l(e);for(;;){if(t(s(n,`package.json`))||t(s(n,`.zeitlos`)))return n;let r=o(n);if(r===n)return l(e);n=r}}function J(e,t){return e.db?l(e.db):process.env.ZEITLOS_DB_PATH?l(process.env.ZEITLOS_DB_PATH):s(t,`.zeitlos`,`${e.env||`dev`}.db`)}function nt(e){n(o(e),{recursive:!0})}function rt(e){let n=s(e,`.gitignore`),i=`.zeitlos/`,o=t(n)?r(n,`utf8`):``;o.split(/\r?\n/).some(e=>e.trim()===i||e.trim()===`.zeitlos`)||a(n,o.replace(/\s*$/,``)+(o.trim()?`
34
+ `:``)+`${i}\n`)}const it=process.env.NO_COLOR?!1:process.env.FORCE_COLOR?process.env.FORCE_COLOR!==`0`:!!process.stdout.isTTY,Y=(e,t)=>n=>it?`\u001b[${e}m${n}\u001b[${t}m`:n,X=Y(1,22),Z=Y(2,22),at=Y(36,39),ot=Y(32,39),st=Y(33,39),ct=Y(35,39),Q=`dsk_local_dev`,lt=(()=>{try{let e=s(o(u(import.meta.url)),`..`,`package.json`),{version:t}=JSON.parse(r(e,`utf8`));return t??``}catch{return``}})();async function ut(e){let t=q(),n=J(e,t);nt(n),rt(t);let r=new Le(n),i=we({resolve:async()=>({store:r,tier:`service`})}),{server:a,port:o}=await ft(i,e.port?Number(e.port):Number(process.env.ZEITLOS_DB_PORT||8093)),s=`http://localhost:${o}`;tt(i,{url:s,key:Q});let c=`${s}/mcp`,l=!e[`no-env`]&&Ue(t,{ZEITLOS_DATABASE_URL:s,ZEITLOS_DATABASE_KEY:Q})?[` ${Z(`They have been written to .env.local, which is auto-loaded by Next.js, Vite, and other frameworks.`)}`,` ${Z(`If your framework doesn't auto-load it, you need to make them available (e.g. node --env-file / dotenv).`)}`]:[` ${Z(`Not written to a file — set these however your app reads env variables.`)}`];console.log([``,` ${ct(`◆`)} ${X(`Zeitlos database - Local emulator`)}${lt?Z(` v${lt}`):``} ${ct(`◆`)}`,` Local DB location: ${Z(K(process.cwd(),n))}`,``,` ${X(`Environment`)}`,` ${Z(`The following environment variables need to be set for your app to connect to the local database:`)}`,``,` ZEITLOS_DATABASE_URL=${at(s)}`,` ZEITLOS_DATABASE_KEY=${st(Q)}`,``,...l,``,` ${X(`Connect your AI assistant`)} ${Z(`to manage schema and data:`)}`,` ${ot(`claude mcp add --transport http zeitlos-local ${c}`)}`,` ${Z(`or .mcp.json: { "mcpServers": { "zeitlos-local": { "type": "http", "url": "${c}" } } }`)}`,``,` ${Z(`Press`)} ${X(`Ctrl-C`)} ${Z(`to stop.`)}`,``].join(`
35
+ `));let u=()=>{a.close(),r.close(),process.exit(0)};process.on(`SIGINT`,u),process.on(`SIGTERM`,u)}function dt(e){let n=J(e,q()),r=!1;for(let e of[n,`${n}-wal`,`${n}-shm`])t(e)&&(i(e),r=!0);console.log(r?`${ot(`✓`)} reset ${Z(K(process.cwd(),n))}`:Z(`nothing to reset (no local database yet)`))}function ft(e,t,n=50){return new Promise((r,i)=>{let a=e.listen(t);a.once(`listening`,()=>r({server:a,port:t})),a.once(`error`,a=>{a.code===`EADDRINUSE`&&n>0?r(ft(e,t+1,n-1)):i(a)})})}async function pt(e){let n=J(e,q());if(!t(n))throw Error(`no local database at ${n} — run \`zeitlos database\` and create some tables first`);let r=new Le(n),i;try{i=await r.tablesOverview()}finally{r.close()}let o=l(process.cwd(),e.out??`zeitlos.d.ts`);a(o,mt(i));let s=c(process.cwd(),o),u=K(process.cwd(),o),d=`./${s.replace(/\.(d\.ts|ts)$/,``)}`;console.log(`✓ wrote ${u} — Database type for ${i.length} table(s)`),console.log(` Use it:`),console.log(` import { createClient } from "@zeitlosapp/sdk";`),console.log(` import type { Database } from "${d}";`),console.log(` const db = createClient<Database>();`)}function mt(e){let t="// Generated by `zeitlos gen types`. Do not edit by hand.\n// Regenerate after schema changes: `zeitlos gen types`.\n\n";return e.length===0?`${t}// No tables yet — create some, then regenerate.\nexport type Database = Record<string, never>;\n`:`${t}export type Database = {\n${e.map(e=>{let t=e.fields.map(e=>` ${gt(e.name)}: ${ht(e)}${e.required?``:` | null`};`).join(`
36
+ `);return` ${gt(e.name)}: {\n${t}\n };`}).join(`
37
+ `)}\n};\n`}function ht(e){switch(e.kind){case`text`:case`email`:case`url`:case`date`:case`datetime`:return`string`;case`relation`:return`string`;case`number`:return`number`;case`boolean`:return`boolean`;case`json`:return`unknown`;case`select`:return e.options?.length?e.options.map(e=>JSON.stringify(e)).join(` | `):`string`;default:{let t=e.kind;throw Error(`unmapped field kind: ${String(t)}`)}}}function gt(e){return/^[A-Za-z_$][\w$]*$/.test(e)?e:JSON.stringify(e)}const $=`zeitlos — local development for the Zeitlos built-in database
38
+
39
+ Usage
40
+ zeitlos database Start the local database emulator (foreground)
41
+ zeitlos database reset Delete the local database and start fresh
42
+ zeitlos gen types Generate a TypeScript Database type from the local schema
43
+
44
+ Options
45
+ --db <path> Use a specific database file
46
+ --env <name> Named local database (default: dev) → .zeitlos/<name>.db
47
+ --port <n> Preferred port (auto-bumps if taken; default 8093)
48
+ --no-env Don't write ZEITLOS_DATABASE_* into .env.local
49
+ --out <path> (gen types) Output file (default: zeitlos.d.ts)
50
+ -h, --help Show this help
51
+
52
+ The database lives at <project-root>/.zeitlos/<env>.db (gitignored). Your app
53
+ connects with @zeitlosapp/sdk via the ZEITLOS_DATABASE_URL/KEY written to .env.local.
54
+
55
+ To mirror an existing project's schema into your local database, connect your AI
56
+ assistant to both the production and local MCP servers and ask it to recreate the
57
+ tables — see the @zeitlosapp/sdk AGENTS.md.
58
+ `;async function _t(){let{values:t,positionals:n}=e({allowPositionals:!0,options:{db:{type:`string`},env:{type:`string`},port:{type:`string`},"no-env":{type:`boolean`},out:{type:`string`},help:{type:`boolean`,short:`h`}}}),r=n[0];if(t.help||!r){process.stdout.write($);return}if(r===`gen`){if(n[1]===`types`){await pt(t);return}process.stderr.write(`unknown gen target: ${n[1]??`(none)`}\n\n${$}`),process.exitCode=1;return}if(r===`database`){if(n[1]===`reset`){dt(t);return}await ut(t);return}process.stderr.write(`unknown command: ${r}\n\n${$}`),process.exitCode=1}export{_t as run};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeitlosapp/cli",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
4
4
  "description": "Local development CLI for the Zeitlos built-in database — run your app's database locally with a single command.",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,58 +0,0 @@
1
- import{parseArgs as e}from"node:util";import{existsSync as t,mkdirSync as n,readFileSync as r,rmSync as i,writeFileSync as a}from"node:fs";import{z as o}from"zod";import{randomBytes as s}from"node:crypto";import c from"express";import{DatabaseSync as l}from"node:sqlite";import{dirname as u,join as d,relative as f,resolve as p}from"node:path";import{StreamableHTTPServerTransport as m}from"@modelcontextprotocol/sdk/server/streamableHttp.js";import{McpServer as h}from"@modelcontextprotocol/sdk/server/mcp.js";const ee=[`eq`,`neq`,`in`,`nin`,`gt`,`gte`,`lt`,`lte`,`contains`,`startsWith`,`endsWith`,`isNull`],te=[`text`,`number`,`boolean`,`datetime`,`date`,`email`,`url`,`select`,`relation`,`json`],g=[`eq`,`neq`,`in`,`nin`,`contains`,`startsWith`,`endsWith`,`isNull`],_=[`eq`,`neq`,`gt`,`gte`,`lt`,`lte`,`in`,`nin`,`isNull`],ne=[`eq`,`neq`,`in`,`nin`,`isNull`],v={text:{pgType:`text`,ops:g,sortable:!0,uniqueable:!0,indexable:!0},number:{pgType:`numeric`,ops:_,sortable:!0,uniqueable:!0,indexable:!0},boolean:{pgType:`boolean`,ops:[`eq`,`isNull`],sortable:!0,uniqueable:!1,indexable:!0},datetime:{pgType:`timestamptz`,ops:_,sortable:!0,uniqueable:!0,indexable:!0},date:{pgType:`date`,ops:_,sortable:!0,uniqueable:!0,indexable:!0},email:{pgType:`citext`,ops:g,sortable:!0,uniqueable:!0,indexable:!0},url:{pgType:`text`,ops:g,sortable:!0,uniqueable:!0,indexable:!0},select:{pgType:`text`,ops:ne,sortable:!0,uniqueable:!0,indexable:!0},relation:{pgType:`uuid`,ops:ne,sortable:!1,uniqueable:!0,indexable:!0},json:{pgType:`jsonb`,ops:[`isNull`],sortable:!1,uniqueable:!1,indexable:!1}},re=Object.values({id:`id`,createdAt:`created_at`,updatedAt:`updated_at`,createdBy:`created_by`,updatedBy:`updated_by`}),ie=/^[a-z][a-z0-9_]*$/;function y(e){return ie.test(e)&&e.length<=48&&!e.startsWith(`_`)}function ae(e){return e.startsWith(`_`)}function oe(e){return re.includes(e)||e.startsWith(`_`)}const b=o.lazy(()=>o.union([o.string(),o.number(),o.boolean(),o.null(),o.array(b),o.record(o.string(),b)])),se=o.record(o.enum(ee),b),ce=o.union([b,se]),x=o.lazy(()=>o.object({and:o.array(x).optional(),or:o.array(x).optional(),not:x.optional()}).catchall(ce)),le=o.array(o.string().min(1)),ue=o.object({filter:x.optional(),sort:le.optional(),limit:o.number().int().positive().max(200).optional(),cursor:o.string().optional(),select:o.array(o.string().min(1)).optional(),expand:o.array(o.string().min(1)).optional(),count:o.boolean().optional()}).strict(),S=o.record(o.string(),b);o.object({data:S}),o.object({data:o.array(S).min(1)}),o.object({id:o.string(),patch:S}),o.object({filter:x,patch:S,confirmToken:o.string().optional()}),o.object({filter:x,confirmToken:o.string().optional()});const de={validation_failed:400,database_not_enabled:409,table_not_found:404,table_exists:409,field_not_found:404,field_exists:409,reserved_name:400,invalid_filter:400,record_not_found:404,unique_violation:409,forbidden:403,quota_exceeded:429,confirmation_required:409,lossy_type_change:409,internal:500};var C=class extends Error{code;details;constructor(e,t,n){super(t),this.name=`BaasError`,this.code=e,this.details=n}get httpStatus(){return de[this.code]}toBody(){return{code:this.code,message:this.message,details:this.details}}};const fe=[`list`,`read`,`create`,`update`,`delete`],pe=[`public`,`authenticated`,`owner`,`none`];function me(e,t,n,r){if(n!==`service`&&(e[t]??`none`)!==`public`)throw new C(`forbidden`,`not permitted: ${t} on ${r}`)}const he=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;function ge(e){try{return new URL(e),!0}catch{return!1}}function _e(e,t,n,r){if(n===null)return;let i=t=>{throw new C(`validation_failed`,`field '${e}': ${t}`)};switch(t){case`number`:(typeof n!=`number`||!Number.isFinite(n))&&i(`expected a finite number`);return;case`boolean`:typeof n!=`boolean`&&i(`expected a boolean`);return;case`text`:typeof n!=`string`&&i(`expected a string`);return;case`email`:(typeof n!=`string`||!he.test(n))&&i(`expected a valid email`);return;case`url`:(typeof n!=`string`||!ge(n))&&i(`expected a valid URL`);return;case`select`:typeof n!=`string`&&i(`expected a string`),r&&!r.includes(n)&&i(`not one of the allowed options`);return;case`datetime`:case`date`:(typeof n!=`string`||Number.isNaN(Date.parse(n)))&&i(`expected an ISO date string`);return;case`relation`:typeof n!=`string`&&i(`expected a related record id`);return;case`json`:return}}const ve=/^[a-z_][a-z0-9_]*$/;function w(e){if(!ve.test(e)||e.length>63)throw Error(`unsafe SQL identifier: ${e}`);return`"${e}"`}function ye(){let e=BigInt(Date.now()),t=s(16);t[0]=Number(e>>40n&255n),t[1]=Number(e>>32n&255n),t[2]=Number(e>>24n&255n),t[3]=Number(e>>16n&255n),t[4]=Number(e>>8n&255n),t[5]=Number(e&255n),t[6]=t[6]&15|112,t[8]=t[8]&63|128;let n=t.toString(`hex`);return`${n.slice(0,8)}-${n.slice(8,12)}-${n.slice(12,16)}-${n.slice(16,20)}-${n.slice(20)}`}const T=e=>e.store;function E(e){if(e.tier!==`service`)throw new C(`forbidden`,`this operation requires a service key`)}async function D(e,t,n){let r=e;r.tier!==`service`&&me(await r.store.getRules(t),n,r.tier,t)}function be(){let e=c.Router();return e.get(`/tables`,async(e,t)=>{E(e),t.json({tables:await T(e).listTables()})}),e.get(`/migrations`,async(e,t)=>{E(e),t.json({migrations:await T(e).migrationHistory()})}),e.get(`/trash`,async(e,t)=>{E(e),t.json({trash:await T(e).listTrash()})}),e.get(`/tables/:table`,async(e,t)=>{E(e),t.json({table:await T(e).describeTable(String(e.params.table))})}),e.post(`/tables`,async(e,t)=>{E(e),await T(e).createTable(e.body?.name),t.status(201).json({ok:!0})}),e.post(`/tables/:table/fields`,async(e,t)=>{E(e);let{name:n,kind:r,required:i,unique:a,indexed:o,options:s,target:c}=e.body??{};await T(e).addField(String(e.params.table),n,r,{required:i,unique:a,indexed:o,options:s,target:c}),t.status(201).json({ok:!0})}),e.delete(`/tables/:table`,async(e,t)=>{E(e),await T(e).dropTable(String(e.params.table)),t.status(204).end()}),e.post(`/tables/:table/restore`,async(e,t)=>{E(e),await T(e).restoreTable(String(e.params.table)),t.json({ok:!0})}),e.post(`/tables/:table/rules`,async(e,t)=>{E(e),await T(e).setAccessRule(String(e.params.table),e.body?.op,e.body?.rule),t.json({ok:!0})}),e.post(`/tables/:table/fields/:field/rename`,async(e,t)=>{E(e),await T(e).renameField(String(e.params.table),String(e.params.field),e.body?.to),t.json({ok:!0})}),e.post(`/tables/:table/fields/:field/change-type`,async(e,t)=>{E(e),await T(e).changeType(String(e.params.table),String(e.params.field),e.body?.kind),t.json({ok:!0})}),e.post(`/tables/:table/fields/:field/index`,async(e,t)=>{E(e),await T(e).createIndex(String(e.params.table),String(e.params.field)),t.status(201).json({ok:!0})}),e.delete(`/tables/:table/fields/:field`,async(e,t)=>{E(e),await T(e).dropField(String(e.params.table),String(e.params.field)),t.status(204).end()}),e.post(`/tables/:table/fields/:field/restore`,async(e,t)=>{E(e),await T(e).restoreField(String(e.params.table),String(e.params.field)),t.json({ok:!0})}),e.post(`/tables/:table/records`,async(e,t)=>{let n=String(e.params.table);await D(e,n,`create`),t.status(201).json({record:await T(e).createRecord(n,e.body?.data)})}),e.post(`/tables/:table/query`,async(e,t)=>{let n=String(e.params.table);await D(e,n,`list`);let r=ue.safeParse(e.body??{});if(!r.success)throw new C(`validation_failed`,r.error.message);t.json(await T(e).queryRecords(n,r.data))}),e.get(`/tables/:table/records/:id`,async(e,t)=>{let n=String(e.params.table);await D(e,n,`read`),t.json({record:await T(e).getRecord(n,String(e.params.id))})}),e.patch(`/tables/:table/records/:id`,async(e,t)=>{let n=String(e.params.table);await D(e,n,`update`);let r=await T(e).updateRecord(n,String(e.params.id),e.body?.data);t.json({record:r})}),e.delete(`/tables/:table/records/:id`,async(e,t)=>{let n=String(e.params.table);await D(e,n,`delete`),await T(e).deleteRecord(n,String(e.params.id)),t.status(204).end()}),e}function xe(e,t,n,r){if(e instanceof C){n.status(e.httpStatus).json(e.toBody());return}console.error(`unhandled error`,e),n.status(500).json({code:`internal`,message:`internal error`})}function Se(e){let t=c();t.disable(`x-powered-by`),t.use(c.json());let n=c.Router();return n.use(async(t,n,r)=>{try{let{store:n,tier:i}=await e.resolve(t);t.store=n,t.tier=i,r()}catch(e){r(e)}}),n.use(be()),t.use(`/v1`,n),t.use(xe),t}const O={id:`text`,created_at:`datetime`,updated_at:`datetime`};function k(e,t){if(t==null)return null;switch(e){case`boolean`:return+!!t;case`json`:return JSON.stringify(t);case`datetime`:return new Date(t).toISOString();default:return t}}function Ce(e,t){if(t==null)return null;switch(e){case`boolean`:return t===1||t===!0;case`json`:return typeof t==`string`?JSON.parse(t):t;case`number`:return typeof t==`string`?Number(t):t;default:return t}}function A(e,t){let n=[];for(let[r,i]of Object.entries(e))if(r===`and`||r===`or`){if(!Array.isArray(i))throw new C(`invalid_filter`,`'${r}' expects an array of filters`);let e=i.map(e=>A(e,t)).filter(e=>e!==void 0);if(e.length>0){let t=r===`and`?` and `:` or `;n.push({sql:`(${e.map(e=>e.sql).join(t)})`,params:e.flatMap(e=>e.params)})}}else if(r===`not`){let e=A(i,t);e&&n.push({sql:`not (${e.sql})`,params:e.params})}else{let e=t.kinds.get(r);if(!e)throw new C(`invalid_filter`,`unknown field: ${r}`);n.push(we(r,i,e))}if(n.length!==0)return{sql:n.map(e=>e.sql).join(` and `),params:n.flatMap(e=>e.params)}}function we(e,t,n){if(Array.isArray(t))return j(e,`in`,t,n);if(typeof t!=`object`||!t)return j(e,`eq`,t,n);let r=Object.entries(t).map(([t,r])=>j(e,t,r,n));if(r.length===0)throw new C(`invalid_filter`,`empty condition for field: ${e}`);return{sql:r.map(e=>e.sql).join(` and `),params:r.flatMap(e=>e.params)}}function j(e,t,n,r){if(!v[r].ops.includes(t))throw new C(`invalid_filter`,`operator '${t}' is not valid for a ${r} field`);let i=w(e),a=e=>k(r,e);switch(t){case`eq`:return{sql:`${i} = ?`,params:[a(n)]};case`neq`:return{sql:`${i} <> ?`,params:[a(n)]};case`gt`:return{sql:`${i} > ?`,params:[a(n)]};case`gte`:return{sql:`${i} >= ?`,params:[a(n)]};case`lt`:return{sql:`${i} < ?`,params:[a(n)]};case`lte`:return{sql:`${i} <= ?`,params:[a(n)]};case`in`:case`nin`:{if(!Array.isArray(n))throw new C(`invalid_filter`,`'${t}' expects an array`);if(n.length===0)return{sql:t===`in`?`1 = 0`:`1 = 1`,params:[]};let e=n.map(()=>`?`).join(`, `);return{sql:`${i} ${t===`in`?`in`:`not in`} (${e})`,params:n.map(a)}}case`contains`:return{sql:`instr(${i}, ?) > 0`,params:[String(n)]};case`startsWith`:return{sql:`substr(${i}, 1, length(?)) = ?`,params:[String(n),String(n)]};case`endsWith`:return{sql:`substr(${i}, -length(?)) = ?`,params:[String(n),String(n)]};case`isNull`:return{sql:n?`${i} is null`:`${i} is not null`,params:[]}}}function Te(e,t){let n=[];for(let r of e??[]){let e=r.startsWith(`-`)?`desc`:`asc`,i=r.startsWith(`-`)?r.slice(1):r,a=t.kinds.get(i);if(!a)throw new C(`invalid_filter`,`unknown sort field: ${i}`);if(!v[a].sortable)throw new C(`invalid_filter`,`field is not sortable: ${i}`);n.push({field:i,dir:e})}return n.push({field:`id`,dir:`asc`}),n}function Ee(e){return e.map(e=>`${w(e.field)} ${e.dir}`).join(`, `)}function De(e,t,n){let r=[],i=[];for(let a=0;a<e.length;a++){let o=[];for(let r=0;r<a;r++)o.push(`${w(e[r].field)} = ?`),i.push(k(Oe(n,e[r].field),t[r]));let s=w(e[a].field);o.push(e[a].dir===`asc`?`${s} > ?`:`${s} < ?`),i.push(k(Oe(n,e[a].field),t[a])),r.push(`(${o.join(` and `)})`)}return{sql:r.join(` or `),params:i}}function Oe(e,t){let n=e.kinds.get(t);if(!n)throw new C(`invalid_filter`,`unknown field: ${t}`);return n}const ke=e=>Buffer.from(JSON.stringify(e)).toString(`base64url`);function Ae(e){try{let t=JSON.parse(Buffer.from(e,`base64url`).toString(`utf8`));if(!Array.isArray(t))throw Error(`not an array`);return t}catch{throw new C(`validation_failed`,`invalid cursor`)}}function je(e,t){let n={};for(let r of t)n[r]=e[r];return n}const Me=new Set([`text`,`number`,`boolean`,`datetime`,`date`,`email`,`url`]),Ne=10080*60*1e3;var Pe=class{db;constructor(e=`:memory:`){this.db=new l(e),this.db.exec(`pragma foreign_keys = on`),this.db.exec(`pragma journal_mode = wal`),this.ensureMeta()}close(){this.db.close()}all(e,t=[]){return this.db.prepare(e).all(...t)}get(e,t=[]){return this.db.prepare(e).get(...t)}run(e,t=[]){return this.db.prepare(e).run(...t)}tx(e){this.db.exec(`begin`);try{let t=e();return this.db.exec(`commit`),t}catch(e){throw this.db.exec(`rollback`),e}}now(){return new Date().toISOString()}ensureMeta(){this.db.exec(`create table if not exists "_meta_table" (
2
- "name" text primary key,
3
- "system" integer not null default 0,
4
- "rules" text,
5
- "created_at" text not null
6
- )`),this.db.exec(`create table if not exists "_meta_field" (
7
- "table_name" text not null,
8
- "name" text not null,
9
- "kind" text not null,
10
- "required" integer not null default 0,
11
- "options" text,
12
- "relation_target" text,
13
- "created_at" text not null,
14
- primary key ("table_name", "name")
15
- )`),this.db.exec(`create table if not exists "_migrations" (
16
- "id" integer primary key autoincrement,
17
- "op" text not null, "target" text not null, "detail" text, "at" text not null
18
- )`),this.db.exec(`create table if not exists "_trash" (
19
- "id" integer primary key autoincrement,
20
- "kind" text not null, "target" text not null, "physical_name" text not null,
21
- "snapshot" text not null, "trashed_at" text not null, "purge_after" text not null
22
- )`)}recordMigration(e,t,n){this.run(`insert into "_migrations" ("op","target","detail","at") values (?,?,?,?)`,[e,t,n??null,this.now()])}tableShape(e){if(!this.get(`select 1 from "_meta_table" where "name" = ?`,[e]))throw new C(`table_not_found`,`table not found: ${e}`);let t=this.all(`select "name","kind","required","options","relation_target" from "_meta_field" where "table_name" = ?`,[e]),n=new Map(Object.entries(O)),r=new Set,i=new Map,a=new Map,o=new Set;for(let e of t)n.set(e.name,e.kind),r.add(e.name),e.options&&i.set(e.name,JSON.parse(e.options)),e.relation_target&&a.set(e.name,e.relation_target),e.required&&o.add(e.name);return{kinds:n,userFields:r,selectOptions:i,relationTargets:a,required:o}}pickUserData(e,t){let n={};for(let[r,i]of Object.entries(t)){let t=e.kinds.get(r);if(!t||!e.userFields.has(r))throw new C(`validation_failed`,`unknown or non-writable field: ${r}`);_e(r,t,i,e.selectOptions.get(r)),n[r]=i}return n}coerceRow(e,t){let n={};for(let[r,i]of Object.entries(t)){let t=e.kinds.get(r);t&&(n[r]=Ce(t,i))}return n}mapDbError(e){let t=String(e.message??e);throw t.includes(`UNIQUE constraint`)?new C(`unique_violation`,`a unique constraint was violated`):t.includes(`FOREIGN KEY constraint`)?new C(`validation_failed`,`a related record does not exist`):e}async createRecord(e,t){if(typeof t!=`object`||!t||Array.isArray(t))throw new C(`validation_failed`,`record data must be an object`);return this.tx(()=>{let n=this.tableShape(e),r=this.pickUserData(n,t);for(let e of n.required)if(r[e]===void 0||r[e]===null)throw new C(`validation_failed`,`a required field is missing`);let i=this.now(),a=[`id`,`created_at`,`updated_at`,...Object.keys(r)],o=[ye(),i,i,...Object.keys(r).map(e=>k(n.kinds.get(e),r[e]))],s=a.map(e=>w(e)).join(`, `),c=a.map(()=>`?`).join(`, `);try{this.run(`insert into ${w(e)} (${s}) values (${c})`,o)}catch(e){this.mapDbError(e)}let l=this.get(`select * from ${w(e)} where "id" = ?`,[o[0]]);return this.coerceRow(n,l)})}async getRecord(e,t){let n=this.tableShape(e),r=this.get(`select * from ${w(e)} where "id" = ?`,[t]);if(!r)throw new C(`record_not_found`,`record not found: ${t}`);return this.coerceRow(n,r)}async updateRecord(e,t,n){if(typeof n!=`object`||!n||Array.isArray(n))throw new C(`validation_failed`,`patch must be an object`);return this.tx(()=>{let r=this.tableShape(e),i=this.pickUserData(r,n);for(let[e,t]of Object.entries(i))if(r.required.has(e)&&t===null)throw new C(`validation_failed`,`a required field is missing`);let a=this.now(),o=Object.keys(i),s=[...o.map(e=>`${w(e)} = ?`),`"updated_at" = ?`].join(`, `),c=[...o.map(e=>k(r.kinds.get(e),i[e])),a,t],l;try{l=this.run(`update ${w(e)} set ${s} where "id" = ?`,c)}catch(e){this.mapDbError(e)}if(Number(l.changes)===0)throw new C(`record_not_found`,`record not found: ${t}`);let u=this.get(`select * from ${w(e)} where "id" = ?`,[t]);return this.coerceRow(r,u)})}async deleteRecord(e,t){this.tableShape(e);let n=this.run(`delete from ${w(e)} where "id" = ?`,[t]);if(Number(n.changes)===0)throw new C(`record_not_found`,`record not found: ${t}`)}async queryRecords(e,t){let n=this.tableShape(e),r=Math.min(t.limit??50,200),i=Te(t.sort,n);for(let e of t.select??[])if(!n.kinds.has(e))throw new C(`invalid_filter`,`unknown select field: ${e}`);let a=t.cursor?Ae(t.cursor):void 0;if(a&&a.length!==i.length)throw new C(`validation_failed`,`cursor does not match the sort`);let o=t.filter?A(t.filter,n):void 0,s=Fe([o,a?De(i,a,n):void 0]),c=Ee(i),l=this.all(`select * from ${w(e)} ${s?`where ${s.sql}`:``} order by ${c} limit ?`,[...s?.params??[],r+1]),u;if(t.count){let t=this.get(`select count(*) as c from ${w(e)} ${o?`where ${o.sql}`:``}`,o?.params??[]);u=Number(t?.c??0)}let d=l.length>r,f=d?l.slice(0,r):l,p=f.map(e=>this.coerceRow(n,e));t.expand&&t.expand.length>0&&await this.applyExpand(n,p,t.expand);let m={records:t.select?p.map(e=>je(e,t.select)):p},h=f[f.length-1];return d&&h&&(m.nextCursor=ke(i.map(e=>this.coerceRow(n,h)[e.field]))),u!==void 0&&(m.count=u),m}async applyExpand(e,t,n){for(let r of n){let n=e.relationTargets.get(r);if(e.kinds.get(r)!==`relation`||!n)throw new C(`invalid_filter`,`not a relation field: ${r}`);let i=[...new Set(t.map(e=>e[r]).filter(e=>e!=null))],a=new Map;if(i.length>0){let e=this.tableShape(n),t=i.map(()=>`?`).join(`, `),r=this.all(`select * from ${w(n)} where "id" in (${t})`,i);for(let t of r){let n=this.coerceRow(e,t);a.set(n.id,n)}}for(let e of t)e[r]=e[r]==null?null:a.get(e[r])??null}}async createTable(e){if(typeof e!=`string`||!y(e)||ae(e))throw new C(`reserved_name`,`invalid or reserved table name: ${String(e)}`);this.tx(()=>{if(this.get(`select 1 from "_meta_table" where "name" = ?`,[e]))throw new C(`table_exists`,`table already exists: ${e}`);this.db.exec(`create table ${w(e)} (
23
- "id" text primary key not null,
24
- "created_at" text not null,
25
- "updated_at" text not null
26
- )`),this.run(`insert into "_meta_table" ("name","system","created_at") values (?,0,?)`,[e,this.now()]),this.recordMigration(`create_table`,e)})}async addField(e,t,n,r={}){if(typeof t!=`string`||!y(t)||oe(t))throw new C(`reserved_name`,`invalid or reserved field name: ${String(t)}`);if(typeof n!=`string`||!(n in v))throw new C(`validation_failed`,`unknown field kind: ${String(n)}`);let i=null;if(n===`select`){if(!Array.isArray(r.options)||r.options.length===0||!r.options.every(e=>typeof e==`string`))throw new C(`validation_failed`,`a select field requires a non-empty options array`);i=r.options}let a=null;if(n===`relation`){if(typeof r.target!=`string`||!y(r.target))throw new C(`validation_failed`,`a relation field requires a valid target table`);a=r.target}this.tx(()=>{if(!this.get(`select 1 from "_meta_table" where "name" = ?`,[e]))throw new C(`table_not_found`,`table not found: ${e}`);if(this.get(`select 1 from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]))throw new C(`field_exists`,`field already exists: ${t}`);let o=w(t);if(a){if(!this.get(`select 1 from "_meta_table" where "name" = ?`,[a]))throw new C(`table_not_found`,`relation target not found: ${a}`);o=`${w(t)} references ${w(a)}("id") on delete set null`}this.db.exec(`alter table ${w(e)} add column ${o}`),this.run(`insert into "_meta_field" ("table_name","name","kind","required","options","relation_target","created_at")
27
- values (?,?,?,?,?,?,?)`,[e,t,n,+!!r.required,i?JSON.stringify(i):null,a,this.now()]),this.createFieldIndex(e,t,!!r.unique,!!r.indexed),this.recordMigration(`add_field`,`${e}.${t}`,n)})}indexName(e,t){return`${e}_${t}_idx`}createFieldIndex(e,t,n,r){n?this.db.exec(`create unique index ${w(this.indexName(e,t))} on ${w(e)} (${w(t)})`):r&&this.db.exec(`create index ${w(this.indexName(e,t))} on ${w(e)} (${w(t)})`)}dropFieldIndex(e,t){this.db.exec(`drop index if exists ${w(this.indexName(e,t))}`)}fieldIndexState(e,t){for(let n of this.all(`select "name","unique" from pragma_index_list(?)`,[e])){let e=this.all(`select "name" from pragma_index_info(?)`,[String(n.name)]);if(e.length===1&&String(e[0].name)===t)return{unique:!!n.unique,indexed:!0}}return{unique:!1,indexed:!1}}fieldSnapshot(e,t){return{...t,...this.fieldIndexState(e,t.name)}}async renameField(e,t,n){if(typeof n!=`string`||!y(n)||oe(n))throw new C(`reserved_name`,`invalid or reserved field name: ${String(n)}`);this.tx(()=>{if(!this.get(`select 1 from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]))throw new C(`field_not_found`,`field not found: ${t}`);if(this.get(`select 1 from "_meta_field" where "table_name" = ? and "name" = ?`,[e,n]))throw new C(`field_exists`,`field already exists: ${n}`);let r=this.fieldIndexState(e,t);this.dropFieldIndex(e,t),this.db.exec(`alter table ${w(e)} rename column ${w(t)} to ${w(n)}`),this.createFieldIndex(e,n,r.unique,r.indexed),this.run(`update "_meta_field" set "name" = ? where "table_name" = ? and "name" = ?`,[n,e,t]),this.recordMigration(`rename_field`,`${e}.${t}`,n)})}async changeType(e,t,n){if(typeof n!=`string`||!(n in v))throw new C(`validation_failed`,`unknown field kind: ${String(n)}`);if(!Me.has(n))throw new C(`validation_failed`,`cannot change a field to '${n}'`);this.tx(()=>{let r=this.get(`select "kind" from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]);if(!r)throw new C(`field_not_found`,`field not found: ${t}`);if(!Me.has(r.kind))throw new C(`validation_failed`,`cannot change a '${r.kind}' field`);let i=this.all(`select "id", ${w(t)} as v from ${w(e)}`);for(let r of i){if(r.v===null||r.v===void 0)continue;let i=Ie(n,r.v);if(i===void 0)throw new C(`lossy_type_change`,`existing data can't be cast to ${n}`);this.run(`update ${w(e)} set ${w(t)} = ? where "id" = ?`,[k(n,i),r.id])}this.run(`update "_meta_field" set "kind" = ? where "table_name" = ? and "name" = ?`,[n,e,t]),this.recordMigration(`change_type`,`${e}.${t}`,n)})}async createIndex(e,t){this.tx(()=>{if(!this.get(`select 1 from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]))throw new C(`field_not_found`,`field not found: ${t}`);this.db.exec(`create index if not exists ${w(this.indexName(e,t))} on ${w(e)} (${w(t)})`),this.recordMigration(`create_index`,`${e}.${t}`)})}async setAccessRule(e,t,n){if(typeof t!=`string`||!fe.includes(t))throw new C(`validation_failed`,`invalid operation: ${String(t)}`);if(typeof n!=`string`||!pe.includes(n))throw new C(`validation_failed`,`invalid rule: ${String(n)}`);this.tx(()=>{let r=this.get(`select "rules" from "_meta_table" where "name" = ?`,[e]);if(!r)throw new C(`table_not_found`,`table not found: ${e}`);let i=r.rules?JSON.parse(r.rules):{};i[t]=n,this.run(`update "_meta_table" set "rules" = ? where "name" = ?`,[JSON.stringify(i),e]),this.recordMigration(`set_access_rule`,`${e}.${t}`,n)})}trashedName(e){return`_trashed_${e.slice(0,20)}_${s(6).toString(`hex`)}`}async dropField(e,t){this.tx(()=>{let n=this.all(`select "table_name","name","kind","required","options","relation_target"
28
- from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]);if(n.length===0)throw new C(`field_not_found`,`field not found: ${t}`);let r=this.fieldSnapshot(e,n[0]);this.dropFieldIndex(e,t);let i=this.trashedName(t);this.db.exec(`alter table ${w(e)} rename column ${w(t)} to ${w(i)}`),this.run(`delete from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]),this.run(`insert into "_trash" ("kind","target","physical_name","snapshot","trashed_at","purge_after") values ('field',?,?,?,?,?)`,[`${e}.${t}`,i,JSON.stringify(r),this.now(),new Date(Date.now()+Ne).toISOString()]),this.recordMigration(`drop_field`,`${e}.${t}`)})}async dropTable(e){this.tx(()=>{let t=this.get(`select "system","rules" from "_meta_table" where "name" = ?`,[e]);if(!t)throw new C(`table_not_found`,`table not found: ${e}`);let n=this.all(`select "table_name","name","kind","required","options","relation_target"
29
- from "_meta_field" where "table_name" = ?`,[e]),r=this.trashedName(e),i=n.map(t=>this.fieldSnapshot(e,t));for(let t of n)this.dropFieldIndex(e,t.name);this.db.exec(`alter table ${w(e)} rename to ${w(r)}`),this.run(`delete from "_meta_field" where "table_name" = ?`,[e]),this.run(`delete from "_meta_table" where "name" = ?`,[e]),this.run(`insert into "_trash" ("kind","target","physical_name","snapshot","trashed_at","purge_after") values ('table',?,?,?,?,?)`,[e,r,JSON.stringify({table:{system:t.system,rules:t.rules},fields:i}),this.now(),new Date(Date.now()+Ne).toISOString()]),this.recordMigration(`drop_table`,e)})}async restoreTable(e){this.tx(()=>{if(this.get(`select 1 from "_meta_table" where "name" = ?`,[e]))throw new C(`table_exists`,`a table already exists with this name: ${e}`);let t=this.takeTrash(`table`,e);if(!t)throw new C(`table_not_found`,`nothing to restore for table: ${e}`);let n=JSON.parse(t.snapshot);this.db.exec(`alter table ${w(t.physical_name)} rename to ${w(e)}`),this.run(`insert into "_meta_table" ("name","system","rules","created_at") values (?,?,?,?)`,[e,+!!n.table.system,typeof n.table.rules==`string`?n.table.rules:JSON.stringify(n.table.rules??null),this.now()]);for(let t of n.fields)this.reinsertMetaField(t),this.createFieldIndex(e,t.name,t.unique,t.indexed);this.run(`delete from "_trash" where "id" = ?`,[t.id]),this.recordMigration(`restore_table`,e)})}async restoreField(e,t){this.tx(()=>{if(this.get(`select 1 from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t]))throw new C(`field_exists`,`a field already exists with this name: ${t}`);let n=this.takeTrash(`field`,`${e}.${t}`);if(!n)throw new C(`field_not_found`,`nothing to restore for field: ${e}.${t}`);this.db.exec(`alter table ${w(e)} rename column ${w(n.physical_name)} to ${w(t)}`);let r=JSON.parse(n.snapshot);this.reinsertMetaField(r),this.createFieldIndex(e,t,r.unique,r.indexed),this.run(`delete from "_trash" where "id" = ?`,[n.id]),this.recordMigration(`restore_field`,`${e}.${t}`)})}takeTrash(e,t){let n=this.get(`select "id","physical_name","snapshot" from "_trash" where "kind" = ? and "target" = ? order by "id" desc limit 1`,[e,t]);return n?{id:Number(n.id),physical_name:String(n.physical_name),snapshot:String(n.snapshot)}:null}reinsertMetaField(e){this.run(`insert into "_meta_field" ("table_name","name","kind","required","options","relation_target","created_at")
30
- values (?,?,?,?,?,?,?)`,[e.table_name,e.name,e.kind,e.required,e.options,e.relation_target,this.now()])}async purgeExpiredTrash(){let e=this.all(`select "id","kind","physical_name","target" from "_trash" where "purge_after" <= ? order by "id"`,[this.now()]),t=0;for(let n of e)this.tx(()=>{if(n.kind===`table`)this.db.exec(`drop table if exists ${w(String(n.physical_name))}`);else{let e=String(n.target),t=e.slice(0,e.lastIndexOf(`.`)),r=e.slice(e.lastIndexOf(`.`)+1);this.dropFieldIndex(t,r),this.db.exec(`alter table ${w(t)} drop column ${w(String(n.physical_name))}`)}this.run(`delete from "_trash" where "id" = ?`,[n.id])}),t+=1;return t}async listTables(){return this.all(`select "name" from "_meta_table" order by "name"`).map(e=>String(e.name))}async migrationHistory(){return this.all(`select "op","target","detail","at" from "_migrations" order by "id"`).map(e=>({op:String(e.op),target:String(e.target),detail:e.detail===null?null:String(e.detail),at:String(e.at)}))}async listTrash(){return this.all(`select "kind","target","trashed_at","purge_after" from "_trash" order by "id" desc`).map(e=>({kind:e.kind,target:String(e.target),trashedAt:String(e.trashed_at),purgeAfter:String(e.purge_after)}))}async getRules(e){let t=this.get(`select "rules" from "_meta_table" where "name" = ?`,[e]);if(!t)throw new C(`table_not_found`,`table not found: ${e}`);return t.rules?JSON.parse(t.rules):{}}async describeTable(e){if(!this.get(`select 1 from "_meta_table" where "name" = ?`,[e]))throw new C(`table_not_found`,`table not found: ${e}`);let t=this.all(`select "name","kind","options","relation_target" from "_meta_field" where "table_name" = ?`,[e]),n=new Map(t.map(e=>[String(e.name),e])),r=this.all(`select "name","notnull" from pragma_table_info(?)`,[e]),i=new Map;for(let t of this.all(`select "name","unique" from pragma_index_list(?)`,[e])){let e=this.all(`select "name" from pragma_index_info(?)`,[String(t.name)]);if(e.length===1){let n=String(e[0].name);i.set(n,!!t.unique||(i.get(n)??!1))}}let a=[];for(let t of r){let r=String(t.name),o=n.get(r),s=o?.kind??O[r];if(!s)continue;let c={name:r,kind:s,required:o?this.fieldRequired(e,r):t.notnull===1,unique:i.get(r)===!0,indexed:i.has(r),system:!o};o?.options&&(c.options=JSON.parse(String(o.options))),o?.relation_target&&(c.relationTarget=String(o.relation_target)),a.push(c)}return{name:e,fields:a}}fieldRequired(e,t){return!!this.get(`select "required" from "_meta_field" where "table_name" = ? and "name" = ?`,[e,t])?.required}async tablesOverview(){let e=await this.listTables(),t=[];for(let n of e){let e=await this.describeTable(n),r=this.get(`select count(*) as c from ${w(n)}`);t.push({name:n,recordCount:Number(r?.c??0),fields:e.fields})}return t}};function Fe(e){let t=e.filter(e=>e!==void 0);if(t.length!==0)return{sql:t.map(e=>`(${e.sql})`).join(` and `),params:t.flatMap(e=>e.params)}}function Ie(e,t){switch(e){case`number`:{let e=typeof t==`number`?t:Number(t);return Number.isFinite(e)?e:void 0}case`boolean`:return typeof t==`boolean`?t:t===1||t===`true`?!0:t===0||t===`false`?!1:void 0;default:return String(t)}}const Le="# >>> zeitlos (managed — edited by `zeitlos database`) >>>",Re=`# <<< zeitlos <<<`,ze=e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`);function Be(e,n){let i=d(e,`.env.local`),o=`${Le}\n${Object.entries(n).map(([e,t])=>`${e}=${t}`).join(`
31
- `)}\n${Re}`,s=t(i)?r(i,`utf8`):``,c=RegExp(`${ze(Le)}[\\s\\S]*?${ze(Re)}`);return a(i,c.test(s)?s.replace(c,o):s.replace(/\s*$/,``)+(s.trim()?`
32
-
33
- `:``)+`${o}\n`),i}var M=class extends Error{code;constructor(e,t){super(t),this.name=`GatewayError`,this.code=e}};function Ve(e){return{async call(t,n,r,i){let a;try{a=await fetch(e.resolveUrl(t,r),{method:n,headers:{"content-type":`application/json`,authorization:`Bearer ${e.authToken}`},body:i===void 0?void 0:JSON.stringify(i)})}catch{throw new M(`gateway_unreachable`,e.unreachableMessage)}if(a.status===204)return null;let o=await a.text(),s=o?JSON.parse(o):null;if(!a.ok)throw new M(s?.code??`internal`,s?.message??`request failed (${a.status})`);return s}}}const N={readOnlyHint:!0,destructiveHint:!1,idempotentHint:!0,openWorldHint:!1},P={readOnlyHint:!1,destructiveHint:!1,openWorldHint:!1};function F(e,t){return{content:[{type:`text`,text:t??JSON.stringify(e,null,2)}],structuredContent:e}}function I(e){return{content:[{type:`text`,text:e}],isError:!0}}function He(e){return console.error(`mcp tool: unexpected error`,e),I(`An unexpected error occurred while handling this request.`)}const L={readOnlyHint:!1,destructiveHint:!0,openWorldHint:!1},Ue=[`list`,`read`,`create`,`update`,`delete`],We=[`public`,`authenticated`,`owner`,`none`];async function R(e,t,n){if(!await e.authorizeProject(t))return I(`Project not found or you do not have access to it.`);try{return await n(e.gateway)}catch(e){if(e instanceof M)return I(e.message);throw e}}function z(e){return F({confirmationRequired:!0,message:`${e} Re-call this tool with confirm: true to proceed.`})}const B=o.string().describe(`The project id, e.g. prj-abc123.`),V=o.string().describe(`The table (collection) name.`),H=o.string().describe(`The field (column) name.`),U=o.boolean().optional().describe(`Must be true to actually perform this destructive operation.`),W=o.record(o.string(),o.unknown()),Ge=o.object({name:o.string(),fields:o.array(o.record(o.unknown()))}),Ke={publishableKey:o.string().optional().describe(`Browser-safe anon key (dpk_…). Public by design; always retrievable.`),schema:o.string().optional(),role:o.string().optional()},G={ok:o.boolean().optional(),confirmationRequired:o.boolean().optional(),message:o.string().optional()},qe=[{name:`enable_database`,title:`Enable the database`,description:`Enable the built-in database for a project (idempotent). Returns the publishable key (dpk_) — browser-safe, governed by per-table access rules; it's public by design and always retrievable. The backend service key is NOT returned here: it's injected into the app automatically at deploy time (as ZEITLOS_DATABASE_KEY) and rotates each deploy.`,inputSchema:{projectId:B},outputSchema:Ke,annotations:{...P,idempotentHint:!0,title:`Enable the database`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`POST`,`/enable`)))},{name:`list_tables`,title:`List tables`,description:`List the tables (collections) in a project's database.`,inputSchema:{projectId:B},outputSchema:{tables:o.array(o.string())},annotations:{...N,title:`List tables`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`GET`,`/tables`)))},{name:`describe_table`,title:`Describe a table`,description:`Get a table's full schema: every field with its kind, required/unique/indexed flags, select options, and relation target.`,inputSchema:{projectId:B,table:V},outputSchema:{table:Ge},annotations:{...N,title:`Describe a table`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`GET`,`/tables/${encodeURIComponent(t.table)}`)))},{name:`migration_history`,title:`Schema change history`,description:`List the project's schema-change history (each create/alter/drop op, in order).`,inputSchema:{projectId:B},outputSchema:{migrations:o.array(o.record(o.unknown()))},annotations:{...N,title:`Schema change history`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`GET`,`/migrations`)))},{name:`list_trash`,title:`List restorable items`,description:`List soft-deleted tables and fields still inside the 7-day undo window, with when each will be permanently purged. Use restore_table / restore_field to bring one back.`,inputSchema:{projectId:B},outputSchema:{trash:o.array(o.record(o.unknown()))},annotations:{...N,title:`List restorable items`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`GET`,`/trash`)))},{name:`create_table`,title:`Create a table`,description:`Create a new table (collection). It starts with the system fields id, created_at, and updated_at; add your own with add_field. All access is locked to service keys until you open it with set_access_rule.`,inputSchema:{projectId:B,name:V},outputSchema:{ok:o.boolean()},annotations:{...P,idempotentHint:!1,title:`Create a table`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables`,{name:t.name}),F({ok:!0})))},{name:`drop_table`,title:`Drop a table`,description:`Soft-delete a table. Its data is preserved and restorable for 7 days (restore_table) before being permanently purged. Requires confirm: true.`,inputSchema:{projectId:B,table:V,confirm:U},outputSchema:G,annotations:{...L,idempotentHint:!1,title:`Drop a table`},handler:(e,t)=>R(e,t.projectId,async e=>t.confirm===!0?(await e.call(t.projectId,`DELETE`,`/tables/${encodeURIComponent(t.table)}`),F({ok:!0})):z(`This soft-deletes "${t.table}"; it is restorable for 7 days, then purged.`))},{name:`restore_table`,title:`Restore a table`,description:`Restore a soft-deleted table (and its data) within the undo window.`,inputSchema:{projectId:B,table:V},outputSchema:{ok:o.boolean()},annotations:{...P,idempotentHint:!1,title:`Restore a table`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/restore`),F({ok:!0})))},{name:`add_field`,title:`Add a field`,description:`Add a field (column) to a table. A 'select' field requires options; a 'relation' field requires target (the table it points at).`,inputSchema:{projectId:B,table:V,name:H,kind:o.enum(te).describe(`The field type.`),required:o.boolean().optional().describe(`Reject rows without a value. Default false.`),unique:o.boolean().optional().describe(`Enforce uniqueness. Default false.`),indexed:o.boolean().optional().describe(`Create an index for faster filtering.`),options:o.array(o.string()).optional().describe(`Allowed values for a 'select' field (required for select).`),target:o.string().optional().describe(`Target table for a 'relation' field (required for relation).`)},outputSchema:{ok:o.boolean()},annotations:{...P,idempotentHint:!1,title:`Add a field`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/fields`,{name:t.name,kind:t.kind,required:t.required,unique:t.unique,indexed:t.indexed,options:t.options,target:t.target}),F({ok:!0})))},{name:`rename_field`,title:`Rename a field`,description:`Rename a field, preserving its data.`,inputSchema:{projectId:B,table:V,field:H,to:o.string().describe(`New name.`)},outputSchema:{ok:o.boolean()},annotations:{...P,idempotentHint:!1,title:`Rename a field`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/fields/${encodeURIComponent(t.field)}/rename`,{to:t.to}),F({ok:!0})))},{name:`change_field_type`,title:`Change a field's type`,description:`Change a scalar field's type, casting existing data. Fails (without changing anything) if the existing data can't be cast to the new type.`,inputSchema:{projectId:B,table:V,field:H,kind:o.enum(te)},outputSchema:{ok:o.boolean()},annotations:{...P,idempotentHint:!0,title:`Change a field's type`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/fields/${encodeURIComponent(t.field)}/change-type`,{kind:t.kind}),F({ok:!0})))},{name:`index_field`,title:`Index a field`,description:`Add an index to a field for faster filtering and sorting.`,inputSchema:{projectId:B,table:V,field:H},outputSchema:{ok:o.boolean()},annotations:{...P,idempotentHint:!0,title:`Index a field`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/fields/${encodeURIComponent(t.field)}/index`),F({ok:!0})))},{name:`drop_field`,title:`Drop a field`,description:`Soft-delete a field. Its data is preserved and restorable for 7 days (restore_field) before being permanently purged. Requires confirm: true.`,inputSchema:{projectId:B,table:V,field:H,confirm:U},outputSchema:G,annotations:{...L,idempotentHint:!1,title:`Drop a field`},handler:(e,t)=>R(e,t.projectId,async e=>t.confirm===!0?(await e.call(t.projectId,`DELETE`,`/tables/${encodeURIComponent(t.table)}/fields/${encodeURIComponent(t.field)}`),F({ok:!0})):z(`This soft-deletes "${t.table}.${t.field}"; it is restorable for 7 days, then purged.`))},{name:`restore_field`,title:`Restore a field`,description:`Restore a soft-deleted field (and its data) within the undo window.`,inputSchema:{projectId:B,table:V,field:H},outputSchema:{ok:o.boolean()},annotations:{...P,idempotentHint:!1,title:`Restore a field`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/fields/${encodeURIComponent(t.field)}/restore`),F({ok:!0})))},{name:`set_access_rule`,title:`Set a table's access rule`,description:`Set who may perform an operation on a table via the publishable (anon) key. Rules: 'public' (anyone), 'authenticated' (any signed-in user), 'owner' (the record's owner), 'none' (service keys only). Ops start at 'none'.`,inputSchema:{projectId:B,table:V,op:o.enum(Ue).describe(`The row operation to govern.`),rule:o.enum(We).describe(`Who may perform it.`)},outputSchema:{ok:o.boolean()},annotations:{...P,idempotentHint:!0,title:`Set a table's access rule`},handler:(e,t)=>R(e,t.projectId,async e=>(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/rules`,{op:t.op,rule:t.rule}),F({ok:!0})))},{name:`create_record`,title:`Create a record`,description:"Insert a record into a table. `data` maps field names to values.",inputSchema:{projectId:B,table:V,data:W.describe(`Field -> value.`)},outputSchema:{record:W},annotations:{...P,idempotentHint:!1,title:`Create a record`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/records`,{data:t.data})))},{name:`get_record`,title:`Get a record`,description:`Fetch a single record by id.`,inputSchema:{projectId:B,table:V,id:o.string().describe(`The record id.`)},outputSchema:{record:W},annotations:{...N,title:`Get a record`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`GET`,`/tables/${encodeURIComponent(t.table)}/records/${encodeURIComponent(t.id)}`)))},{name:`query_records`,title:`Query records`,description:"Query a table with filtering, sorting, and cursor pagination. `filter` uses the query DSL: a map of field -> value (equals) or field -> {op: value} (ops: eq, neq, in, nin, gt, gte, lt, lte, contains, startsWith, endsWith, isNull), composed with and/or/not. `sort` takes field names ('-field' for descending). Use `expand` to inline relations.",inputSchema:{projectId:B,table:V,filter:o.record(o.string(),o.unknown()).optional().describe(`Query DSL filter.`),sort:o.array(o.string()).optional().describe(`Sort fields; prefix '-' for descending.`),limit:o.number().int().positive().max(200).optional().describe(`Page size (max 200).`),cursor:o.string().optional().describe(`Opaque cursor from a prior page's nextCursor.`),select:o.array(o.string()).optional().describe(`Only return these fields.`),expand:o.array(o.string()).optional().describe(`Relation fields to inline.`),count:o.boolean().optional().describe(`Also return the total match count.`)},outputSchema:{records:o.array(W),nextCursor:o.string().optional(),count:o.number().optional()},annotations:{...N,title:`Query records`},handler:(e,t)=>R(e,t.projectId,async e=>{let n={};for(let e of[`filter`,`sort`,`limit`,`cursor`,`select`,`expand`,`count`])t[e]!==void 0&&(n[e]=t[e]);return F(await e.call(t.projectId,`POST`,`/tables/${encodeURIComponent(t.table)}/query`,n))})},{name:`update_record`,title:`Update a record`,description:"Patch a record by id. `data` holds only the fields to change.",inputSchema:{projectId:B,table:V,id:o.string().describe(`The record id.`),data:W.describe(`Fields to change -> new values.`)},outputSchema:{record:W},annotations:{...P,idempotentHint:!0,title:`Update a record`},handler:(e,t)=>R(e,t.projectId,async e=>F(await e.call(t.projectId,`PATCH`,`/tables/${encodeURIComponent(t.table)}/records/${encodeURIComponent(t.id)}`,{data:t.data})))},{name:`delete_record`,title:`Delete a record`,description:`Permanently delete a record by id. This is NOT undoable. Requires confirm: true.`,inputSchema:{projectId:B,table:V,id:o.string().describe(`The record id.`),confirm:U},outputSchema:G,annotations:{...L,idempotentHint:!0,title:`Delete a record`},handler:(e,t)=>R(e,t.projectId,async e=>t.confirm===!0?(await e.call(t.projectId,`DELETE`,`/tables/${encodeURIComponent(t.table)}/records/${encodeURIComponent(t.id)}`),F({ok:!0})):z(`This permanently deletes the record; it cannot be undone.`))}];function Je(e,t,n=qe){for(let r of n)e.registerTool(r.name,{title:r.title,description:r.description,inputSchema:r.inputSchema,outputSchema:r.outputSchema,annotations:r.annotations},async e=>{try{return await r.handler(t,e)}catch(e){return He(e)}})}function Ye(e,t={}){let n=new h(t.serverInfo??{name:`zeitlos`,version:`0.0.0`},{instructions:t.instructions??`Manage your Zeitlos database — create tables and fields, set access rules, and read/write records.`});return Je(n,e,t.tools),n}function Xe(e,t){let n=e.replace(/\/$/,``);return Ve({resolveUrl:(e,t)=>`${n}/v1${t}`,authToken:t,unreachableMessage:`The local database is not reachable.`})}const Ze=qe.filter(e=>e.name!==`enable_database`).map(e=>{let{projectId:t,...n}=e.inputSchema;return{...e,inputSchema:n,handler:(t,n)=>e.handler(t,{...n,projectId:`local`})}});function Qe(e,t){let n={gateway:Xe(t.url,t.key),authorizeProject:()=>Promise.resolve(!0)};e.post(`/mcp`,async(e,t)=>{let r=Ye(n,{tools:Ze,serverInfo:{name:`zeitlos-local`,version:`0.0.0`},instructions:`This is the developer's LOCAL Zeitlos database — a single-project SQLite emulator running on localhost for local development. It is NOT the production/cloud database, and changes here affect only the local dev environment. Use it to build and test schema and data locally. If a production Zeitlos MCP is also connected, use this one for local development and that one for the deployed project's real data. Manage tables, fields, access rules, and records here.`}),i=new m({sessionIdGenerator:void 0});t.on(`close`,()=>{i.close(),r.close()}),await r.connect(i),await i.handleRequest(e,t,e.body)})}function K(e,t){let n=f(e,t);return n&&!n.startsWith(`..`)?n:t}function q(e=process.cwd()){let n=p(e);for(;;){if(t(d(n,`package.json`))||t(d(n,`.zeitlos`)))return n;let r=u(n);if(r===n)return p(e);n=r}}function J(e,t){return e.db?p(e.db):process.env.ZEITLOS_DB_PATH?p(process.env.ZEITLOS_DB_PATH):d(t,`.zeitlos`,`${e.env||`dev`}.db`)}function $e(e){n(u(e),{recursive:!0})}function et(e){let n=d(e,`.gitignore`),i=`.zeitlos/`,o=t(n)?r(n,`utf8`):``;o.split(/\r?\n/).some(e=>e.trim()===i||e.trim()===`.zeitlos`)||a(n,o.replace(/\s*$/,``)+(o.trim()?`
34
- `:``)+`${i}\n`)}const tt=process.env.NO_COLOR?!1:process.env.FORCE_COLOR?process.env.FORCE_COLOR!==`0`:!!process.stdout.isTTY,Y=(e,t)=>n=>tt?`\u001b[${e}m${n}\u001b[${t}m`:n,X=Y(1,22),Z=Y(2,22),nt=Y(36,39),rt=Y(32,39),it=Y(33,39),at=Y(35,39),Q=`dsk_local_dev`;async function ot(e){let t=q(),n=J(e,t);$e(n),et(t);let r=new Pe(n),i=Se({resolve:async()=>({store:r,tier:`service`})}),{server:a,port:o}=await ct(i,e.port?Number(e.port):Number(process.env.ZEITLOS_DB_PORT||8093)),s=`http://localhost:${o}`;Qe(i,{url:s,key:Q});let c=`${s}/mcp`,l=!e[`no-env`]&&Be(t,{ZEITLOS_DATABASE_URL:s,ZEITLOS_DATABASE_KEY:Q})?[` ${Z(`They have been written to .env.local, which is auto-loaded by Next.js, Vite, and other frameworks.`)}`,` ${Z(`If your framework doesn't auto-load it, you need to make them available (e.g. node --env-file / dotenv).`)}`]:[` ${Z(`Not written to a file — set these however your app reads env variables.`)}`];console.log([``,` ${at(`◆`)} ${X(`Zeitlos database - Local emulator`)}`,` Local DB location: ${Z(K(process.cwd(),n))}`,``,` ${X(`Environment`)}`,` ${Z(`The following environment variables need to be set for your app to connect to the local database:`)}`,``,` ZEITLOS_DATABASE_URL=${nt(s)}`,` ZEITLOS_DATABASE_KEY=${it(Q)}`,``,...l,``,` ${X(`Connect your AI assistant`)} ${Z(`to manage schema and data:`)}`,` ${rt(`claude mcp add --transport http zeitlos-local ${c}`)}`,` ${Z(`or .mcp.json: { "mcpServers": { "zeitlos-local": { "type": "http", "url": "${c}" } } }`)}`,``,` ${Z(`Press`)} ${X(`Ctrl-C`)} ${Z(`to stop.`)}`,``].join(`
35
- `));let u=()=>{a.close(),r.close(),process.exit(0)};process.on(`SIGINT`,u),process.on(`SIGTERM`,u)}function st(e){let n=J(e,q()),r=!1;for(let e of[n,`${n}-wal`,`${n}-shm`])t(e)&&(i(e),r=!0);console.log(r?`${rt(`✓`)} reset ${Z(K(process.cwd(),n))}`:Z(`nothing to reset (no local database yet)`))}function ct(e,t,n=50){return new Promise((r,i)=>{let a=e.listen(t);a.once(`listening`,()=>r({server:a,port:t})),a.once(`error`,a=>{a.code===`EADDRINUSE`&&n>0?r(ct(e,t+1,n-1)):i(a)})})}async function lt(e){let n=J(e,q());if(!t(n))throw Error(`no local database at ${n} — run \`zeitlos database\` and create some tables first`);let r=new Pe(n),i;try{i=await r.tablesOverview()}finally{r.close()}let o=p(process.cwd(),e.out??`zeitlos.d.ts`);a(o,ut(i));let s=f(process.cwd(),o),c=K(process.cwd(),o),l=`./${s.replace(/\.(d\.ts|ts)$/,``)}`;console.log(`✓ wrote ${c} — Database type for ${i.length} table(s)`),console.log(` Use it:`),console.log(` import { createClient } from "@zeitlosapp/sdk";`),console.log(` import type { Database } from "${l}";`),console.log(` const db = createClient<Database>();`)}function ut(e){let t="// Generated by `zeitlos gen types`. Do not edit by hand.\n// Regenerate after schema changes: `zeitlos gen types`.\n\n";return e.length===0?`${t}// No tables yet — create some, then regenerate.\nexport type Database = Record<string, never>;\n`:`${t}export type Database = {\n${e.map(e=>{let t=e.fields.map(e=>` ${ft(e.name)}: ${dt(e)}${e.required?``:` | null`};`).join(`
36
- `);return` ${ft(e.name)}: {\n${t}\n };`}).join(`
37
- `)}\n};\n`}function dt(e){switch(e.kind){case`text`:case`email`:case`url`:case`date`:case`datetime`:return`string`;case`relation`:return`string`;case`number`:return`number`;case`boolean`:return`boolean`;case`json`:return`unknown`;case`select`:return e.options?.length?e.options.map(e=>JSON.stringify(e)).join(` | `):`string`;default:{let t=e.kind;throw Error(`unmapped field kind: ${String(t)}`)}}}function ft(e){return/^[A-Za-z_$][\w$]*$/.test(e)?e:JSON.stringify(e)}const $=`zeitlos — local development for the Zeitlos built-in database
38
-
39
- Usage
40
- zeitlos database Start the local database emulator (foreground)
41
- zeitlos database reset Delete the local database and start fresh
42
- zeitlos gen types Generate a TypeScript Database type from the local schema
43
-
44
- Options
45
- --db <path> Use a specific database file
46
- --env <name> Named local database (default: dev) → .zeitlos/<name>.db
47
- --port <n> Preferred port (auto-bumps if taken; default 8093)
48
- --no-env Don't write ZEITLOS_DATABASE_* into .env.local
49
- --out <path> (gen types) Output file (default: zeitlos.d.ts)
50
- -h, --help Show this help
51
-
52
- The database lives at <project-root>/.zeitlos/<env>.db (gitignored). Your app
53
- connects with @zeitlosapp/sdk via the ZEITLOS_DATABASE_URL/KEY written to .env.local.
54
-
55
- To mirror an existing project's schema into your local database, connect your AI
56
- assistant to both the production and local MCP servers and ask it to recreate the
57
- tables — see the @zeitlosapp/sdk AGENTS.md.
58
- `;async function pt(){let{values:t,positionals:n}=e({allowPositionals:!0,options:{db:{type:`string`},env:{type:`string`},port:{type:`string`},"no-env":{type:`boolean`},out:{type:`string`},help:{type:`boolean`,short:`h`}}}),r=n[0];if(t.help||!r){process.stdout.write($);return}if(r===`gen`){if(n[1]===`types`){await lt(t);return}process.stderr.write(`unknown gen target: ${n[1]??`(none)`}\n\n${$}`),process.exitCode=1;return}if(r===`database`){if(n[1]===`reset`){st(t);return}await ot(t);return}process.stderr.write(`unknown command: ${r}\n\n${$}`),process.exitCode=1}export{pt as run};