@somewhere-cloud/cli 0.38.5 → 0.38.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -48
- package/bin/engine.js +27 -0
- package/bin/somewhere.js +10 -1
- package/dist/commands/api.js +67 -2
- package/dist/commands/api.js.map +1 -1
- package/dist/commands/auth.js +18 -5
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/check.js +9 -3
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/deploy.js +145 -6
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.js +48 -7
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/docs.js +15 -17
- package/dist/commands/docs.js.map +1 -1
- package/dist/commands/env.js +9 -1
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/init.js +16 -7
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/logs.js +6 -1
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/typecheck.js +44 -20
- package/dist/commands/typecheck.js.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/advisor-context.js +0 -9
- package/dist/lib/advisor-context.js.map +1 -1
- package/dist/lib/claim-handoff.js +23 -1
- package/dist/lib/claim-handoff.js.map +1 -1
- package/dist/lib/client.js +18 -7
- package/dist/lib/client.js.map +1 -1
- package/dist/lib/config.js +15 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/deploy-check.js +45 -44
- package/dist/lib/deploy-check.js.map +1 -1
- package/dist/lib/files.js +108 -19
- package/dist/lib/files.js.map +1 -1
- package/dist/lib/init-agent-guide.js +8 -1
- package/dist/lib/init-agent-guide.js.map +1 -1
- package/dist/lib/last-run.js +54 -27
- package/dist/lib/last-run.js.map +1 -1
- package/dist/lib/temp-auth.js +16 -24
- package/dist/lib/temp-auth.js.map +1 -1
- package/dist/lib/typecheck.js +209 -78
- package/dist/lib/typecheck.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/runtime/DECLARED-DATA-VENDOR.json +18 -18
- package/runtime/SOURCE-LINT-VENDOR.json +47 -46
- package/runtime/declared-data.cjs +188 -108
- package/runtime/source-lint.cjs +311 -199
|
@@ -330,7 +330,7 @@ var require_declared_data_contract = __commonJS({
|
|
|
330
330
|
});
|
|
331
331
|
return values.length ? `{ ${values.join("; ")} }` : "Record<string, never>";
|
|
332
332
|
}
|
|
333
|
-
var LINK_CLIENT_RUNTIME = 'const b64url=s=>btoa(unescape(encodeURIComponent(s))).replace(/\\+/g,"-").replace(/\\//g,"_").replace(/=+$/,"");const createLink=async(table,id,options={})=>{const result=await invoke(table,"link_create",{id,access:options.access,...(options.expiresInDays===undefined?{}:{expires_in_days:options.expiresInDays})});const route=typeof options.returnTo==="string"?options.returnTo:location.pathname+location.search+location.hash;return{link_id:result.link_id,url:location.origin+"/__sw/link#"+result.secret+"."+b64url(route),expires_at:result.expires_at}};const linkDoor=async(path,init)=>{const response=await fetch(path,{credentials:"same-origin",cache:"no-store",...init});const payload=await response.json().catch(()=>({error:"INVALID_DATA_RESPONSE",message:"Link operation returned an invalid response"}));if(!response.ok)throw new DataError(response.status,payload);return payload};const links=Object.freeze({opened:()=>{try{const list=JSON.parse(sessionStorage.getItem("__sw_links_opened")||"[]");return Array.isArray(list)?list:[]}catch(_){return[]}},sessions:async()=>{const payload=await linkDoor("/__sw/link/sessions",{method:"GET"});return{data:payload.data}},forget:async linkId=>{await linkDoor("/__sw/link/forget",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(linkId===undefined?{}:{link_id:linkId})})}});const linkedView=spec=>link=>{if(typeof link!=="string"||!/^lnk_[0-9a-f]{32}$/.test(link))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"linked() needs a link id"});const view={};for(const[table,canUpdate,children]of spec){const relations={};const root={get:id=>invoke(table,"get",{id,link})};if(canUpdate)root.update=(id,values)=>invoke(table,"update",{id,values,link});for(const[relation,child,canCreate,canUpdateChild]of children){relations[relation]=Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id,link})});const childOps={};if(canCreate)childOps.create=values=>invoke(child,"create",{values,link});if(canUpdateChild)childOps.update=(id,values)=>invoke(child,"update",{id,values,link});if(canCreate||canUpdateChild)view[child]=Object.freeze(childOps)}if(children.length)root.relations=Object.freeze(relations);view[table]=Object.freeze(root)}return Object.freeze(view)};\n';
|
|
333
|
+
var LINK_CLIENT_RUNTIME = 'const b64url=s=>btoa(unescape(encodeURIComponent(s))).replace(/\\+/g,"-").replace(/\\//g,"_").replace(/=+$/,"");const createLink=async(table,id,options={})=>{const result=await invoke(table,"link_create",{id,access:options.access,...(options.expiresInDays===undefined?{}:{expires_in_days:options.expiresInDays})});const route=typeof options.returnTo==="string"?options.returnTo:location.pathname+location.search+location.hash;return{link_id:result.link_id,url:location.origin+"/__sw/link#"+result.secret+"."+b64url(route),expires_at:result.expires_at}};const linkDoor=async(path,init)=>{swReadSessionVoid();const response=await fetch(path,{credentials:"same-origin",cache:"no-store",...init});const payload=await response.json().catch(()=>({error:"INVALID_DATA_RESPONSE",message:"Link operation returned an invalid response"}));if(!response.ok)throw new DataError(response.status,payload);return payload};const links=Object.freeze({opened:()=>{try{const list=JSON.parse(sessionStorage.getItem("__sw_links_opened")||"[]");return Array.isArray(list)?list:[]}catch(_){return[]}},sessions:async()=>{const payload=await linkDoor("/__sw/link/sessions",{method:"GET"});return{data:payload.data}},forget:async linkId=>{await linkDoor("/__sw/link/forget",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(linkId===undefined?{}:{link_id:linkId})})}});const linkedView=spec=>link=>{if(typeof link!=="string"||!/^lnk_[0-9a-f]{32}$/.test(link))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"linked() needs a link id"});const view={};for(const[table,canUpdate,children]of spec){const relations={};const root={get:id=>invoke(table,"get",{id,link})};if(canUpdate)root.update=(id,values)=>invoke(table,"update",{id,values,link});for(const[relation,child,canCreate,canUpdateChild]of children){relations[relation]=Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id,link})});const childOps={};if(canCreate)childOps.create=values=>invoke(child,"create",{values,link});if(canUpdateChild)childOps.update=(id,values)=>invoke(child,"update",{id,values,link});if(canCreate||canUpdateChild)view[child]=Object.freeze(childOps)}if(children.length)root.relations=Object.freeze(relations);view[table]=Object.freeze(root)}return Object.freeze(view)};\n';
|
|
334
334
|
var INSERT_CLIENT_RUNTIME = String.raw`
|
|
335
335
|
function insertReceipt(value) {
|
|
336
336
|
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
@@ -388,6 +388,7 @@ async function createWithReceipt(table, values, extra, eligible) {
|
|
|
388
388
|
throw attach(insertError(410, {error:'IDEMPOTENCY_RETRY_EXPIRED', message:'This request can no longer be retried safely. Nothing was sent.'}));
|
|
389
389
|
}
|
|
390
390
|
let response, payload;
|
|
391
|
+
swReadSessionVoid();
|
|
391
392
|
try { response = await fetch('/__sw/data', {method:'POST', credentials:'same-origin', headers:{'Content-Type':'application/json'}, body}); }
|
|
392
393
|
catch (_) { throw attach(insertError(503, {error:'DB_WRITE_OUTCOME_UNKNOWN', message:'The write outcome could not be confirmed. Retry this request with its original key.', outcome:'unknown'})); }
|
|
393
394
|
try { payload = await response.json(); }
|
|
@@ -429,6 +430,7 @@ async function createAnonymous(table, values, extra) {
|
|
|
429
430
|
throw attach(insertError(410, {error:'IDEMPOTENCY_RETRY_EXPIRED', message:'This request can no longer be retried safely. Nothing was sent.'}));
|
|
430
431
|
}
|
|
431
432
|
let response, payload;
|
|
433
|
+
swReadSessionVoid();
|
|
432
434
|
try { response = await fetch('/__sw/data', {method:'POST', credentials:'same-origin', cache:'no-store', headers:{'Content-Type':'application/json'}, body}); }
|
|
433
435
|
catch (_) { throw attach(insertError(503, {error:'DB_WRITE_OUTCOME_UNKNOWN', message:'The write outcome could not be confirmed. Retry this request with its original key.', outcome:'unknown'})); }
|
|
434
436
|
try { payload = await response.json(); }
|
|
@@ -460,6 +462,7 @@ async function intake(table, reference, values, extra) {
|
|
|
460
462
|
throw attach(insertError(410, {error:'IDEMPOTENCY_RETRY_EXPIRED', message:'This request can no longer be retried safely. Nothing was sent.'}));
|
|
461
463
|
}
|
|
462
464
|
let response, payload;
|
|
465
|
+
swReadSessionVoid();
|
|
463
466
|
try { response = await fetch('/__sw/data', {method:'POST', credentials:'same-origin', cache:'no-store', headers:{'Content-Type':'application/json'}, body}); }
|
|
464
467
|
catch (_) { throw attach(insertError(503, {error:'DB_WRITE_OUTCOME_UNKNOWN', message:'The write outcome could not be confirmed. Retry this request with its original key.', outcome:'unknown'})); }
|
|
465
468
|
try { payload = await response.json(); }
|
|
@@ -476,7 +479,7 @@ async function intake(table, reference, values, extra) {
|
|
|
476
479
|
return send();
|
|
477
480
|
}
|
|
478
481
|
`;
|
|
479
|
-
var CREATOR_CLIENT_RUNTIME = 'const creatorError=(status,payload)=>{const e=new DataError(status,payload);if(payload&&["committed","unknown","read_failed","not_committed"].includes(payload.outcome))e.outcome=payload.outcome;if(payload&&payload.write_committed===true)e.write_committed=true;e.retry=false;e.retryable=false;return e};const creatorDoor=async(path,init,write)=>{const lost=(status,code)=>creatorError(status,{error:code,message:write?"The creator operation could not be confirmed. Do not repeat it automatically.":"The creator operation result could not be read.",outcome:write?"unknown":"read_failed"});let response;try{response=await fetch(path,{credentials:"same-origin",cache:"no-store",...init})}catch(_){throw lost(503,"CREATOR_OUTCOME_UNKNOWN")}let payload;try{payload=await response.json()}catch(_){throw lost(response.status,"INVALID_DATA_RESPONSE")}if(!response.ok){if(!payload||typeof payload!=="object"||typeof payload.error!=="string")throw lost(response.status,"INVALID_DATA_RESPONSE");throw creatorError(response.status,payload)}if(!payload||typeof payload!=="object"||payload.ok!==true||!("data" in payload)||payload.data===undefined)throw lost(502,"INVALID_DATA_RESPONSE");return payload.data};const creatorPost=(body,write)=>creatorDoor("/__sw/creator",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...body,contract})},write);const claimOutcomes=new Map();const creatorsOpened=()=>{try{const list=JSON.parse(sessionStorage.getItem("__sw_creators_opened")||"[]");return Array.isArray(list)?list:[]}catch(_){return[]}};const creators=Object.freeze({opened:creatorsOpened,sessions:async()=>({data:await creatorDoor("/__sw/creator/sessions",{method:"GET"})}),forget:async creatorId=>{await creatorDoor("/__sw/creator/forget",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(creatorId===undefined?{}:{creator_id:creatorId})},true)}});const creatorView=spec=>creatorId=>{if(typeof creatorId!=="string"||!/^crt_[0-9a-f]{32}$/.test(creatorId))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"creator() needs a creator id"});const view={};for(const[table,canUpdate,children]of spec){const call=async(action,fields,write)=>{if(action==="relation_list")validateListLimit(fields);return creatorPost({action,creator_id:creatorId,table,...fields},write)};const root={get:id=>call("get",{id}).then(r=>({data:r.data})),links:id=>call("link_list",{id}),revokeLink:(id,linkId)=>call("link_revoke",{id,link_id:linkId},true),createLink:async(id,options={})=>{const result=await call("link_create",{id,access:options.access,...(options.expiresInDays===undefined?{}:{expires_in_days:options.expiresInDays})},true);const route=typeof options.returnTo==="string"?options.returnTo:location.pathname+location.search+location.hash;return{link_id:result.link_id,url:location.origin+"/__sw/link#"+result.secret+"."+b64url(route),expires_at:result.expires_at}},claim:async(id,options={})=>{const key=creatorId+"|"+table+"|"+String(id);let result;try{result=await call("claim",{id,keep_shares:options.keepShares===true},true)}catch(e){const known=claimOutcomes.get(key);if(e.outcome==="committed"||e.write_committed===true)claimOutcomes.set(key,"committed");else if(e.outcome==="unknown")claimOutcomes.set(key,"unknown");else if(known){e.outcome=known==="committed"?"committed":"unknown";if(known==="committed")e.write_committed=true;e.message+=" An earlier attempt of this claim may already have applied; check claimStatus() or your account."}throw e}claimOutcomes.set(key,"committed");try{sessionStorage.setItem("__sw_creators_opened",JSON.stringify(creatorsOpened().filter(e=>e&&e.creator_id!==creatorId)))}catch(_){}return result},claimStatus:id=>call("claim_status",{id})};if(canUpdate)root.update=(id,values)=>call("update",{id,values},true);const relations={};for(const[relation,child,canCreate,canUpdateChild]of children){relations[relation]=Object.freeze({list:(parentId,options={})=>call("relation_list",{relation,parent_id:parentId,...(options.after===undefined?{}:{after:options.after}),...(options.limit===undefined?{}:{limit:options.limit})})});const childOps={};if(canCreate)childOps.create=values=>call("child_create",{child,values},true);if(canUpdateChild)childOps.update=(id,values)=>call("child_update",{child,id,values},true);if(canCreate||canUpdateChild)view[child]=Object.freeze(childOps)}if(children.length)root.relations=Object.freeze(relations);view[table]=Object.freeze(root)}return Object.freeze(view)};\n';
|
|
482
|
+
var CREATOR_CLIENT_RUNTIME = 'const creatorError=(status,payload)=>{const e=new DataError(status,payload);if(payload&&["committed","unknown","read_failed","not_committed"].includes(payload.outcome))e.outcome=payload.outcome;if(payload&&payload.write_committed===true)e.write_committed=true;e.retry=false;e.retryable=false;return e};const creatorDoor=async(path,init,write)=>{const lost=(status,code)=>creatorError(status,{error:code,message:write?"The creator operation could not be confirmed. Do not repeat it automatically.":"The creator operation result could not be read.",outcome:write?"unknown":"read_failed"});let response;swReadSessionVoid();try{response=await fetch(path,{credentials:"same-origin",cache:"no-store",...init})}catch(_){throw lost(503,"CREATOR_OUTCOME_UNKNOWN")}let payload;try{payload=await response.json()}catch(_){throw lost(response.status,"INVALID_DATA_RESPONSE")}if(!response.ok){if(!payload||typeof payload!=="object"||typeof payload.error!=="string")throw lost(response.status,"INVALID_DATA_RESPONSE");throw creatorError(response.status,payload)}if(!payload||typeof payload!=="object"||payload.ok!==true||!("data" in payload)||payload.data===undefined)throw lost(502,"INVALID_DATA_RESPONSE");return payload.data};const creatorPost=(body,write)=>creatorDoor("/__sw/creator",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...body,contract})},write);const claimOutcomes=new Map();const creatorsOpened=()=>{try{const list=JSON.parse(sessionStorage.getItem("__sw_creators_opened")||"[]");return Array.isArray(list)?list:[]}catch(_){return[]}};const creators=Object.freeze({opened:creatorsOpened,sessions:async()=>({data:await creatorDoor("/__sw/creator/sessions",{method:"GET"})}),forget:async creatorId=>{await creatorDoor("/__sw/creator/forget",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(creatorId===undefined?{}:{creator_id:creatorId})},true)}});const creatorView=spec=>creatorId=>{if(typeof creatorId!=="string"||!/^crt_[0-9a-f]{32}$/.test(creatorId))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"creator() needs a creator id"});const view={};for(const[table,canUpdate,children]of spec){const call=async(action,fields,write)=>{if(action==="relation_list")validateListLimit(fields);return creatorPost({action,creator_id:creatorId,table,...fields},write)};const root={get:id=>call("get",{id}).then(r=>({data:r.data})),links:id=>call("link_list",{id}),revokeLink:(id,linkId)=>call("link_revoke",{id,link_id:linkId},true),createLink:async(id,options={})=>{const result=await call("link_create",{id,access:options.access,...(options.expiresInDays===undefined?{}:{expires_in_days:options.expiresInDays})},true);const route=typeof options.returnTo==="string"?options.returnTo:location.pathname+location.search+location.hash;return{link_id:result.link_id,url:location.origin+"/__sw/link#"+result.secret+"."+b64url(route),expires_at:result.expires_at}},claim:async(id,options={})=>{const key=creatorId+"|"+table+"|"+String(id);let result;try{result=await call("claim",{id,keep_shares:options.keepShares===true},true)}catch(e){const known=claimOutcomes.get(key);if(e.outcome==="committed"||e.write_committed===true)claimOutcomes.set(key,"committed");else if(e.outcome==="unknown")claimOutcomes.set(key,"unknown");else if(known){e.outcome=known==="committed"?"committed":"unknown";if(known==="committed")e.write_committed=true;e.message+=" An earlier attempt of this claim may already have applied; check claimStatus() or your account."}throw e}claimOutcomes.set(key,"committed");try{sessionStorage.setItem("__sw_creators_opened",JSON.stringify(creatorsOpened().filter(e=>e&&e.creator_id!==creatorId)))}catch(_){}return result},claimStatus:id=>call("claim_status",{id})};if(canUpdate)root.update=(id,values)=>call("update",{id,values},true);const relations={};for(const[relation,child,canCreate,canUpdateChild]of children){relations[relation]=Object.freeze({list:(parentId,options={})=>call("relation_list",{relation,parent_id:parentId,...(options.after===undefined?{}:{after:options.after}),...(options.limit===undefined?{}:{limit:options.limit})})});const childOps={};if(canCreate)childOps.create=values=>call("child_create",{child,values},true);if(canUpdateChild)childOps.update=(id,values)=>call("child_update",{child,id,values},true);if(canCreate||canUpdateChild)view[child]=Object.freeze(childOps)}if(children.length)root.relations=Object.freeze(relations);view[table]=Object.freeze(root)}return Object.freeze(view)};\n';
|
|
480
483
|
var ANONYMOUS_CLIENT_DECLARATIONS = `
|
|
481
484
|
/** The private creator capability of a row created with createAnonymous. Keep it, and its url, private: anyone opening the url can read, edit, share and claim the row until it is claimed. */
|
|
482
485
|
export interface CreatorCapability { readonly grant: string; readonly secret: string; readonly url: string; }
|
|
@@ -485,12 +488,18 @@ async function intake(table, reference, values, extra) {
|
|
|
485
488
|
var INTAKE_CLIENT_DECLARATIONS = `
|
|
486
489
|
export interface IntakeResult { readonly accepted: true; readonly idempotency: InsertReceipt; readonly request: InsertRequest<IntakeResult> }
|
|
487
490
|
`;
|
|
491
|
+
var ROW_CAN_DECLARATION = " /** Row capability hint (list/get with can: true): whether the signed-in caller may update or delete this row now, by the same rule the write enforces. A hint for UI; the write is still the final answer. */\n export interface RowCan { readonly update: boolean; readonly delete: boolean; }\n";
|
|
488
492
|
var INSERT_CLIENT_DECLARATIONS = `
|
|
489
493
|
export interface InsertReceipt { readonly replayed: boolean; readonly created_at: number; readonly result_until: number; readonly refuse_until: number; }
|
|
490
494
|
export interface InsertRequest<T> { readonly key: string; readonly createdAt: number; retry(): Promise<T>; }
|
|
491
495
|
export type InsertResult<T> = T & { readonly idempotency: InsertReceipt; readonly request: InsertRequest<InsertResult<T>> };
|
|
492
496
|
`;
|
|
493
|
-
|
|
497
|
+
var CLIENT_GENERATION2 = 3;
|
|
498
|
+
var ACCEPTED_CLIENT_GENERATIONS2 = Object.freeze([3, 2, 1]);
|
|
499
|
+
var PUBLIC_BOOTSTRAP_CLIENT_RUNTIME = 'const swCanon=(v,d)=>{if(v===null||typeof v==="string"||typeof v==="boolean"||(typeof v==="number"&&Number.isFinite(v)))return JSON.stringify(v);if(d===0||typeof v!=="object"||Array.isArray(v))return null;const parts=[];for(const k of Object.keys(v).sort()){if(v[k]===undefined)continue;const e=swCanon(v[k],d-1);if(e===null)return null;parts.push(JSON.stringify(k)+":"+e)}return"{"+parts.join(",")+"}"};let swBoot;const swBootTake=body=>{try{if(swBoot===undefined){swBoot=null;const el=typeof document!=="undefined"&&document.getElementById?document.getElementById("__sw_public_data"):null;if(el&&el.type==="application/json"&&(el.parentNode===document.head||el.parentNode===document.body)){const p=JSON.parse(el.textContent||"null");if(p&&p.v===1&&p.contract===contract&&Number.isSafeInteger(p.validated_at)&&p.validated_at<=Date.now()&&Number.isInteger(p.max_age)&&p.max_age>0&&p.max_age<=120&&p.entries&&typeof p.entries==="object"&&!Array.isArray(p.entries)&&typeof p.path==="string"&&typeof location!=="undefined")swBoot={until:p.validated_at+p.max_age*1000,path:p.path,entries:new Map(Object.entries(p.entries))}}if(el&&el.parentNode)el.parentNode.removeChild(el)}if(!swBoot)return undefined;if(Date.now()>=swBoot.until||location.pathname!==swBoot.path){swBoot=null;return undefined}const key=swCanon(body,2);if(key===null||!swBoot.entries.has(key))return undefined;const value=swBoot.entries.get(key);swBoot.entries.delete(key);return value}catch(error){swBoot=null;return undefined}};';
|
|
500
|
+
var PUBLIC_BOOTSTRAP_INVOKE = 'if(operation==="list"||operation==="get"||operation==="aggregate"||operation==="relation_list"){const booted=swBootTake({...input,contract,table,operation});if(booted!==undefined)return booted}else swBoot=null;';
|
|
501
|
+
function describeDataClient2(authority, contract_digest, insertReceiptCapability, localTypes = false, generation = CLIENT_GENERATION2) {
|
|
502
|
+
if (!ACCEPTED_CLIENT_GENERATIONS2.includes(generation)) throw new Error(`Unknown data client generation ${generation}.`);
|
|
494
503
|
const canonical = authority ? canonicalize2(authority.schema, authority.intents, authority.scopes, insertReceiptCapability) : canonicalize2({}, {}, {}, insertReceiptCapability);
|
|
495
504
|
const { tables } = JSON.parse(canonical);
|
|
496
505
|
const declarations = [];
|
|
@@ -512,8 +521,16 @@ async function intake(table, reference, values, extra) {
|
|
|
512
521
|
const methods = [];
|
|
513
522
|
if (client.read !== false) {
|
|
514
523
|
const equality = readable.filter((field) => !["json", "blob"].includes(columns.find((column) => column.n === field).t));
|
|
515
|
-
|
|
516
|
-
|
|
524
|
+
if (generation >= 2) {
|
|
525
|
+
const listOptions = `limit?: L & (number extends L ? unknown : [L] extends [PageSize] ? unknown : never); after?: ${id}; where?: ${shape(columns, equality, "update")}`;
|
|
526
|
+
operations.push(`list<const L extends number = number>(options: { ${listOptions}; can: true }): Promise<{ data: (${row} & { can: RowCan })[]; next: ${id} | null; has_more: boolean }>`);
|
|
527
|
+
operations.push(`list<const L extends number = number>(options?: { ${listOptions}; can?: false }): Promise<{ data: ${row}[]; next: ${id} | null; has_more: boolean }>`);
|
|
528
|
+
operations.push(`get(id: ${id}, options: { can: true }): Promise<{ data: (${row} & { can: RowCan }) | null }>`);
|
|
529
|
+
operations.push(`get(id: ${id}, options?: { can?: false }): Promise<{ data: ${row} | null }>`);
|
|
530
|
+
} else {
|
|
531
|
+
operations.push(`list<const L extends number = number>(options?: { limit?: L & (number extends L ? unknown : [L] extends [PageSize] ? unknown : never); after?: ${id}; where?: ${shape(columns, equality, "update")} }): Promise<{ data: ${row}[]; next: ${id} | null; has_more: boolean }>`);
|
|
532
|
+
operations.push(`get(id: ${id}): Promise<{ data: ${row} | null }>`);
|
|
533
|
+
}
|
|
517
534
|
methods.push("list", "get");
|
|
518
535
|
const numeric = equality.filter((field) => ["integer", "number"].includes(columns.find((column) => column.n === field).t));
|
|
519
536
|
const fieldUnion = (list) => list.map((field) => JSON.stringify(field)).join(" | ");
|
|
@@ -645,15 +662,15 @@ async function intake(table, reference, values, extra) {
|
|
|
645
662
|
const declaration = `declare module ${JSON.stringify("somewhere:data")} {
|
|
646
663
|
type PageSize = ${Array.from({ length: 100 }, (_, i) => i + 1).join(" | ")};
|
|
647
664
|
export type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
|
|
648
|
-
${receiptCapable ? localTypes ? INSERT_CLIENT_DECLARATIONS.replace("readonly idempotency:", "readonly idempotency?:").replace("readonly request:", "readonly request?:") : INSERT_CLIENT_DECLARATIONS : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_DECLARATIONS : ""}${intakeTables.length ? INTAKE_CLIENT_DECLARATIONS : ""} export class DataError extends Error { readonly status: number; readonly code: string | null;${receiptCapable ? " readonly request?: InsertRequest<unknown>; readonly outcome?: string; readonly write_committed?: boolean; readonly retry?: false; readonly retryable?: false; readonly idempotency?: InsertReceipt;" : ""} constructor(status: number, payload: unknown); }
|
|
665
|
+
${generation >= 2 ? ROW_CAN_DECLARATION : ""}${receiptCapable ? localTypes ? INSERT_CLIENT_DECLARATIONS.replace("readonly idempotency:", "readonly idempotency?:").replace("readonly request:", "readonly request?:") : INSERT_CLIENT_DECLARATIONS : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_DECLARATIONS : ""}${intakeTables.length ? INTAKE_CLIENT_DECLARATIONS : ""} export class DataError extends Error { readonly status: number; readonly code: string | null;${receiptCapable ? " readonly request?: InsertRequest<unknown>; readonly outcome?: string; readonly write_committed?: boolean; readonly retry?: false; readonly retryable?: false; readonly idempotency?: InsertReceipt;" : ""} constructor(status: number, payload: unknown); }
|
|
649
666
|
export const data: { ${declarations.join(";\n ")} };
|
|
650
667
|
}
|
|
651
668
|
`;
|
|
652
669
|
const runtime = `const contract=${JSON.stringify(contract_digest)};
|
|
653
670
|
class DataError extends Error{constructor(status,payload){super(payload&&typeof payload.message==="string"?payload.message:payload&&typeof payload.error==="string"?payload.error:"Data operation failed");this.name="DataError";this.status=status;this.code=payload&&typeof payload.error==="string"?payload.error:null}}
|
|
654
671
|
const validateListLimit=input=>{if(input.limit!==undefined&&(!Number.isInteger(input.limit)||input.limit<1||input.limit>100))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"limit must be an integer from 1 to 100"})};
|
|
655
|
-
const invoke=async(table,operation,input)=>{if(operation==="list"||operation==="relation_list")validateListLimit(input)
|
|
656
|
-
${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : "values=>invoke(table,operation,{values})"}:${anonymousTables.length ? 'operation==="createAnonymous"?(values,...extra)=>createAnonymous(table,values,extra):' : ""}${intakeTables.length ? 'operation==="intake"?(reference,values,...extra)=>intake(table,reference,values,extra):' : ""}operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):id=>invoke(table,operation,{id})]);if(relations.length)entries.push(["relations",Object.freeze(Object.fromEntries(relations.map(relation=>[relation,Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id})})])))]);return[table,Object.freeze(Object.fromEntries(entries))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})${creatorSpec.length ? `.concat([["creators",creators],["creator",creatorView(${JSON.stringify(creatorSpec)})]])` : ""}));
|
|
672
|
+
let swReadSession=null,swInFlight=0,swStarts=0;const swReadSessionVoid=()=>{swReadSession=null;swStarts++};${generation >= 3 ? PUBLIC_BOOTSTRAP_CLIENT_RUNTIME : ""}const invoke=async(table,operation,input)=>{if(operation==="list"||operation==="relation_list")validateListLimit(input);${generation >= 3 ? PUBLIC_BOOTSTRAP_INVOKE : ""}const alone=swInFlight===0,start=++swStarts,isRead=operation==="list"||operation==="get"||operation==="aggregate"||operation==="relation_list";swInFlight++;let response;try{response=await fetch("/__sw/data",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json",...(swReadSession?{"X-Sw-Read-Session":swReadSession}:{})},body:JSON.stringify({...input,contract,table,operation})})}catch(error){if(!isRead)swReadSession=null;throw error}finally{swInFlight--}const next=response.headers&&typeof response.headers.get==="function"?response.headers.get("X-Sw-Read-Session"):null;if(!alone||swStarts!==start||swInFlight!==0)swReadSession=null;else if(next&&/^swrs1[.][A-Za-z0-9_-]+[.][A-Za-z0-9_-]+$/.test(next))swReadSession=next;else if(!isRead||!response.ok)swReadSession=null;const payload=await response.json().catch(()=>{throw new DataError(response.status,{error:"INVALID_DATA_RESPONSE",message:"Data operation returned an invalid response"})});if(!response.ok)throw new DataError(response.status,payload);return payload};
|
|
673
|
+
${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : "values=>invoke(table,operation,{values})"}:${anonymousTables.length ? 'operation==="createAnonymous"?(values,...extra)=>createAnonymous(table,values,extra):' : ""}${intakeTables.length ? 'operation==="intake"?(reference,values,...extra)=>intake(table,reference,values,extra):' : ""}operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):${generation >= 2 ? 'operation==="get"?(id,options={})=>invoke(table,operation,options&&options.can===true?{id,can:true}:{id}):' : ""}id=>invoke(table,operation,{id})]);if(relations.length)entries.push(["relations",Object.freeze(Object.fromEntries(relations.map(relation=>[relation,Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id})})])))]);return[table,Object.freeze(Object.fromEntries(entries))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})${creatorSpec.length ? `.concat([["creators",creators],["creator",creatorView(${JSON.stringify(creatorSpec)})]])` : ""}));
|
|
657
674
|
export{data,DataError};
|
|
658
675
|
`;
|
|
659
676
|
return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration, ...insertReceiptCapability === void 0 ? {} : { insert_receipt: insertReceiptCapability } } };
|
|
@@ -661,7 +678,7 @@ export{data,DataError};
|
|
|
661
678
|
function describeDataClientLocalDeclaration2(authority) {
|
|
662
679
|
return describeDataClient2(authority, "", "insert-v1", true).declaration;
|
|
663
680
|
}
|
|
664
|
-
module2.exports = { canonicalize: canonicalize2, describeDataClient: describeDataClient2, describeDataClientLocalDeclaration: describeDataClientLocalDeclaration2, validateInsertReceiptCapability };
|
|
681
|
+
module2.exports = { canonicalize: canonicalize2, describeDataClient: describeDataClient2, describeDataClientLocalDeclaration: describeDataClientLocalDeclaration2, validateInsertReceiptCapability, CLIENT_GENERATION: CLIENT_GENERATION2, ACCEPTED_CLIENT_GENERATIONS: ACCEPTED_CLIENT_GENERATIONS2 };
|
|
665
682
|
}
|
|
666
683
|
});
|
|
667
684
|
|
|
@@ -793,7 +810,31 @@ interface SomewhereAppRoleGrant {
|
|
|
793
810
|
interface SomewhereGroupInvitation {
|
|
794
811
|
invite: { id: string; email: string; status: 'pending'; redirect_uri: string; expires_at: number; created_at: number };
|
|
795
812
|
group: { group_id: string; role: SomewhereGroupRole };
|
|
796
|
-
|
|
813
|
+
/** What THIS call sent. not_sent: the address already had this pending
|
|
814
|
+
* invitation; it is returned as is and no second email goes out. */
|
|
815
|
+
delivery: 'sent' | 'pending' | 'not_sent';
|
|
816
|
+
/** With not_sent: what is known about the invitation's one email.
|
|
817
|
+
* sent/queued: the provider took it. unknown: it may have gone out.
|
|
818
|
+
* starting: a send may still begin. not_sent: no email went out.
|
|
819
|
+
* Nothing is re-sent automatically. After not_sent or unknown,
|
|
820
|
+
* revokeInvitation then invite again sends one new email with a new link;
|
|
821
|
+
* after unknown the earlier email may still arrive (its link stops working
|
|
822
|
+
* once revoked). */
|
|
823
|
+
email_state?: SomewhereGroupInvitationEmailState;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
type SomewhereGroupInvitationEmailState = 'sent' | 'queued' | 'unknown' | 'starting' | 'not_sent';
|
|
827
|
+
|
|
828
|
+
/** A pending invitation, as its sender or a member who may grant its role sees it. */
|
|
829
|
+
interface SomewhereGroupPendingInvitation {
|
|
830
|
+
invite_id: string;
|
|
831
|
+
email: string;
|
|
832
|
+
role: SomewhereGroupRole;
|
|
833
|
+
invited_by: string;
|
|
834
|
+
status: 'pending';
|
|
835
|
+
created_at: number;
|
|
836
|
+
expires_at: number;
|
|
837
|
+
email_state: SomewhereGroupInvitationEmailState;
|
|
797
838
|
}
|
|
798
839
|
|
|
799
840
|
/**
|
|
@@ -830,13 +871,20 @@ type SomewhereGroupsResult<T> = { data: T; error: null } | { data: null; error:
|
|
|
830
871
|
*/
|
|
831
872
|
interface SomewhereGroups {
|
|
832
873
|
create(req: Request, input: { name: string }): Promise<SomewhereGroupsResult<SomewhereGroupDetail>>;
|
|
833
|
-
/** Also completes (and reports) up to 3 of the signed-in user's
|
|
874
|
+
/** Also completes (and reports) up to 3 of the signed-in user's invitations not yet confirmed at sign-in. */
|
|
834
875
|
list(req: Request, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereGroup> & { invitations: SomewhereGroupInvitationOutcome[] }>>;
|
|
835
876
|
get(req: Request, groupId: string): Promise<SomewhereGroupsResult<SomewhereGroupDetail>>;
|
|
836
877
|
members(req: Request, groupId: string, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereGroupMember>>>;
|
|
837
|
-
/** Invite
|
|
838
|
-
* sends one email
|
|
878
|
+
/** Invite any email with a role the signed-in user may grant. The platform
|
|
879
|
+
* sends one email. An address that already belongs to a verified account
|
|
880
|
+
* in this app gets access right away; any other joins when it signs in with
|
|
881
|
+
* that verified email (the link, or any sign-in). The answer is the same in
|
|
882
|
+
* every case; no app call is needed. Inviting an address that already has a
|
|
883
|
+
* pending invitation sends nothing. */
|
|
839
884
|
invite(req: Request, groupId: string, input: { email: string; role: SomewhereGroupRole; redirect_uri: string; expires_in?: number }): Promise<SomewhereGroupsResult<SomewhereGroupInvitation>>;
|
|
885
|
+
/** Pending invitations of one group (oldest first) that the signed-in member could revoke:
|
|
886
|
+
* ones they sent, and ones whose role they may grant. A non-member gets GROUP_NOT_FOUND. */
|
|
887
|
+
invitations(req: Request, groupId: string, opts?: SomewhereGroupsPageOptions): Promise<SomewhereGroupsResult<SomewhereGroupsPage<SomewhereGroupPendingInvitation>>>;
|
|
840
888
|
/** Revoke in the invitation's own group, as its sender or a member who may grant its role. */
|
|
841
889
|
revokeInvitation(req: Request, inviteId: string): Promise<SomewhereGroupsResult<{ revoked: true }>>;
|
|
842
890
|
leave(req: Request, groupId: string): Promise<SomewhereGroupsResult<{ left: true }>>;
|
|
@@ -914,10 +962,17 @@ type SomewhereDbUpdate = {
|
|
|
914
962
|
| { set?: never; increment: SomewhereDbIncrement }
|
|
915
963
|
);
|
|
916
964
|
interface SomewhereDbRemove { where?: SomewhereDbWhere | null }
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
965
|
+
// One closed-batch step per declared table, checked like the matching single call
|
|
966
|
+
// (insert values, update set/increment). With no declared tables every name and
|
|
967
|
+
// value stays accepted, as before. Insert and Set pick caller or server identity.
|
|
968
|
+
type __SomewhereDbIntentFor<T extends string, Insert extends 'insert' | 'serverInsert', Set extends 'set' | 'serverSet'> =
|
|
969
|
+
| { op: 'insert'; table: T; values: __SomewhereDeclared<T, Insert, SomewhereDbValues>; options?: SomewhereDbPlainInsertOptions | null }
|
|
970
|
+
| ({ op: 'update'; table: T } & SomewhereDbUpdateFor<T, Set>)
|
|
971
|
+
| { op: 'remove'; table: T; where?: SomewhereDbWhere | null };
|
|
972
|
+
// sw.db.server.tx: server authority may name row identity (owner column, shared authorship).
|
|
973
|
+
type SomewhereDbWriteIntent = { [T in SomewhereDbTable]: __SomewhereDbIntentFor<T, 'serverInsert', 'serverSet'> }[SomewhereDbTable];
|
|
974
|
+
// sw.db.tx: the platform composes row identity from the verified user, as sw.db.insert does.
|
|
975
|
+
type SomewhereDbCallerWriteIntent = { [T in SomewhereDbTable]: __SomewhereDbIntentFor<T, 'insert', 'set'> }[SomewhereDbTable];
|
|
921
976
|
// db/schema.ts's tables, one entry each, generated beside this declaration
|
|
922
977
|
// (declaredTablesDeclaration). With no declared tables it stays empty: any
|
|
923
978
|
// table name is accepted and rows are Record<string, unknown>. Once tables are
|
|
@@ -962,6 +1017,7 @@ interface SomewhereDbResult<Row = Record<string, unknown>> {
|
|
|
962
1017
|
changes: number;
|
|
963
1018
|
last_row_id: number | string | null;
|
|
964
1019
|
live_delivery?: { delivery: 'invalidated' }
|
|
1020
|
+
| { delivery: 'queued' }
|
|
965
1021
|
| { delivery: 'resync_required'; reason: string };
|
|
966
1022
|
}
|
|
967
1023
|
// A keyed insert's result (whether an insert is keyed is decided at run time): the receipt
|
|
@@ -994,10 +1050,11 @@ interface SomewhereServerDb {
|
|
|
994
1050
|
// an unknown outcome; a rejection never promises rollback or safe replay.
|
|
995
1051
|
tx(intents: readonly SomewhereDbWriteIntent[]): Promise<SomewhereDbResult[]>;
|
|
996
1052
|
}
|
|
997
|
-
interface SomewhereCallerDb extends Omit<SomewhereServerDb, 'from' | 'count' | 'insert' | 'update'> {
|
|
1053
|
+
interface SomewhereCallerDb extends Omit<SomewhereServerDb, 'from' | 'count' | 'insert' | 'update' | 'tx'> {
|
|
998
1054
|
// The platform composes row identity from the verified user.
|
|
999
1055
|
insert<T extends SomewhereDbTable>(table: T, values: __SomewhereDeclared<T, 'insert', SomewhereDbValues>, options?: SomewhereDbInsertOptions | null): Promise<SomewhereDbInsertResult<SomewhereDbRow<T>>>;
|
|
1000
1056
|
update<T extends SomewhereDbTable>(table: T, spec: SomewhereDbUpdateFor<T, 'set'>): Promise<SomewhereDbResult<SomewhereDbRow<T>>>;
|
|
1057
|
+
tx(intents: readonly SomewhereDbCallerWriteIntent[]): Promise<SomewhereDbResult[]>;
|
|
1001
1058
|
from<T extends SomewhereDbTable, const C extends readonly SomewhereDbColumn<T>[] = readonly SomewhereDbColumn<T>[], const I extends readonly SomewhereDbRelationName<T>[] = []>(
|
|
1002
1059
|
table: T, options?: (SomewhereDbReadOptionsFor<T, C, I> & { asServer?: true }) | null): Promise<SomewhereDbResult<SomewhereDbReadRow<T, C, I>>>;
|
|
1003
1060
|
count(table: SomewhereDbTable, options?: (SomewhereDbCountOptions & { asServer?: true }) | null): Promise<{ data: number; error: null }>;
|
|
@@ -1206,14 +1263,11 @@ interface SomewhereAuthProfileUser extends SomewhereAuthUser {
|
|
|
1206
1263
|
email_verified: boolean;
|
|
1207
1264
|
banned: boolean;
|
|
1208
1265
|
metadata: unknown;
|
|
1209
|
-
plan: string;
|
|
1210
|
-
plan_status: string | null;
|
|
1211
1266
|
type: string;
|
|
1212
1267
|
created_at: number;
|
|
1213
1268
|
last_login_at: number | null;
|
|
1214
1269
|
locale?: string | null;
|
|
1215
1270
|
timezone?: string | null;
|
|
1216
|
-
entitlements: string[];
|
|
1217
1271
|
agent?: SomewhereJsonObject;
|
|
1218
1272
|
}
|
|
1219
1273
|
interface SomewhereAuthIssuedSession {
|
|
@@ -1288,6 +1342,9 @@ interface SomewhereAuthRefreshed {
|
|
|
1288
1342
|
expires_in: number;
|
|
1289
1343
|
token_type: 'Bearer';
|
|
1290
1344
|
}
|
|
1345
|
+
type SomewhereAuthRefreshedSession =
|
|
1346
|
+
| { user: SomewhereAuthUser; cookie_session: true; expires_at: number }
|
|
1347
|
+
| (SomewhereAuthRefreshed & { user: SomewhereAuthUser; cookie_session?: never; expires_at: number });
|
|
1291
1348
|
interface SomewhereAuthSignupProfile {
|
|
1292
1349
|
display_name?: string | null;
|
|
1293
1350
|
displayName?: string | null;
|
|
@@ -1402,6 +1459,7 @@ interface SomewhereRuntimeAuth {
|
|
|
1402
1459
|
login(options: SomewhereAuthCredentials): Promise<SomewhereAuthLoginResult>;
|
|
1403
1460
|
logout(options: SomewhereAuthLogoutOptions): Promise<{ logged_out: true }>;
|
|
1404
1461
|
refresh(options: { refresh_token: string }): Promise<SomewhereAuthRefreshed>;
|
|
1462
|
+
refreshSession(req: Request): Promise<SomewhereAuthRefreshedSession>;
|
|
1405
1463
|
// Verifies an app-user access token; throws on an invalid/expired token (no null result).
|
|
1406
1464
|
me(token: string, options?: { refreshToken?: string | null; noCache?: boolean } | null): Promise<{ user: SomewhereAuthProfileUser }>;
|
|
1407
1465
|
// Resolves the signed-in user from cookie or Bearer (or options.forwardedToken); null when absent/rejected.
|
|
@@ -3000,6 +3058,8 @@ interface SomewhereEmailTestInboxMessage {
|
|
|
3000
3058
|
html: string | null;
|
|
3001
3059
|
text: string | null;
|
|
3002
3060
|
magic_link: string | null;
|
|
3061
|
+
/** The code an auth message carries (email verification, sign-in code); null otherwise. */
|
|
3062
|
+
verification_code: string | null;
|
|
3003
3063
|
created_at: string;
|
|
3004
3064
|
}
|
|
3005
3065
|
interface SomewhereRuntimeEmail {
|
|
@@ -3740,13 +3800,22 @@ type __SomewhereEndpointBody<S> = {
|
|
|
3740
3800
|
type SomewhereEndpointAuth = 'none' | 'optional' | 'required';
|
|
3741
3801
|
interface SomewhereEndpointInput<Auth extends SomewhereEndpointAuth, Body> {
|
|
3742
3802
|
body: Body;
|
|
3743
|
-
user: Auth extends 'required' ? SomewhereEndpointUser : SomewhereEndpointUser | null;
|
|
3803
|
+
user: Auth extends 'required' ? SomewhereEndpointUser & { email: string } : SomewhereEndpointUser | null;
|
|
3744
3804
|
headers: Headers;
|
|
3745
3805
|
params: Record<string, string>;
|
|
3746
3806
|
request: Request;
|
|
3747
3807
|
}
|
|
3808
|
+
type SomewhereMcpJson = null | boolean | number | string | readonly SomewhereMcpJson[] | { readonly [key: string]: SomewhereMcpJson };
|
|
3748
3809
|
interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema extends SomewhereEndpointBodySchema | undefined> {
|
|
3749
3810
|
auth?: Auth;
|
|
3811
|
+
/** Explicit MCP opt-in. Only auth: required endpoints can be tools. */
|
|
3812
|
+
mcp?: Auth extends 'required' ? {
|
|
3813
|
+
name: string;
|
|
3814
|
+
description: string;
|
|
3815
|
+
inputSchema: { readonly type: 'object'; readonly [key: string]: SomewhereMcpJson };
|
|
3816
|
+
outputSchema?: { readonly type: 'object'; readonly [key: string]: SomewhereMcpJson };
|
|
3817
|
+
annotations?: { readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean };
|
|
3818
|
+
} : never;
|
|
3750
3819
|
body?: Schema;
|
|
3751
3820
|
rateLimit?: \`\${number}/\${'second' | 'minute' | 'hour' | 'day'}\${'' | 's'}\`;
|
|
3752
3821
|
cors?: 'same-origin' | '*' | readonly string[];
|
|
@@ -3784,7 +3853,7 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
|
|
|
3784
3853
|
if (owner && !columns.some((column) => column.n === owner)) columns.push({ n: owner, t: "text", optional: false, identity: true, type: "string" });
|
|
3785
3854
|
const declared = columns.filter((column) => !column.identity);
|
|
3786
3855
|
const relations = (table.relations || []).filter((relation) => Object.prototype.hasOwnProperty.call(schema, relation.table)).map((relation) => key(relation.name) + ": SomewhereDeclaredTables[" + key(relation.table) + "]['row']" + (relation.kind === "belongsTo" ? " | null" : "[]"));
|
|
3787
|
-
return " " + key(name) + ": {\n row: " + shape(columns.map((column) => field(column, false))) + ";\n insert: " + shape(declared.map((column) => field(column, column.optional))) + ";\n serverInsert: " + shape(columns.map((column) => field(column, column.optional))) + ";\n set: " + shape(declared.map((column) => field(column, true))) + ";\n serverSet: " + shape(columns.map((column) => field(column, true))) + ";\n numeric: " + union(declared.filter((column) => column.t === "integer" || column.t === "number")) + ";\n scalar: " + union(
|
|
3856
|
+
return " " + key(name) + ": {\n row: " + shape(columns.map((column) => field(column, false))) + ";\n insert: " + shape(declared.map((column) => field(column, column.optional))) + ";\n serverInsert: " + shape(columns.map((column) => field(column, column.optional))) + ";\n set: " + shape(declared.map((column) => field(column, true))) + ";\n serverSet: " + shape(columns.map((column) => field(column, true))) + ";\n numeric: " + union(declared.filter((column) => column.t === "integer" || column.t === "number")) + ";\n scalar: " + union(columns.filter((column) => (!column.identity || column.n === owner) && column.t !== "json" && column.t !== "blob")) + ";\n relations: " + (relations.length ? shape(relations) : "{}") + ";\n };\n";
|
|
3788
3857
|
});
|
|
3789
3858
|
return "interface SomewhereDeclaredTables {\n" + entries.join("") + "}\n";
|
|
3790
3859
|
}
|
|
@@ -4105,6 +4174,16 @@ var require_checker_contract = __commonJS({
|
|
|
4105
4174
|
if (typeShape.has(number)) return "type";
|
|
4106
4175
|
return null;
|
|
4107
4176
|
}
|
|
4177
|
+
function diagnosticMessage(code, message) {
|
|
4178
|
+
const number = diagnosticNumber(code);
|
|
4179
|
+
if ((number === 2322 || number === 2345) && message.includes("'string[]'") && /readonly \(\s*["'][^"']+["'](?:\s*\|\s*["'][^"']+["'])*\s*\)\[\]/.test(message)) {
|
|
4180
|
+
return message + "\nHint: For a fixed list, declare the array with `as const` so its values stay literal. For dynamic values, validate and narrow them to the allowed values; do not cast arbitrary strings.";
|
|
4181
|
+
}
|
|
4182
|
+
if ((number === 2322 || number === 2345) && message.includes("Record<string, unknown>")) {
|
|
4183
|
+
return message + "\nHint: Use an object literal or a builder with explicitly typed fields. Validate unknown input before passing it to a typed API; do not hide the mismatch with a cast.";
|
|
4184
|
+
}
|
|
4185
|
+
return message;
|
|
4186
|
+
}
|
|
4108
4187
|
module2.exports = {
|
|
4109
4188
|
TYPESCRIPT_VERSION,
|
|
4110
4189
|
REFERENCE_SOURCE,
|
|
@@ -4117,7 +4196,8 @@ var require_checker_contract = __commonJS({
|
|
|
4117
4196
|
projectTsconfigCompilerOptions,
|
|
4118
4197
|
localCheckCompilerOptions,
|
|
4119
4198
|
isIgnoredDiagnostic,
|
|
4120
|
-
classifyDiagnostic
|
|
4199
|
+
classifyDiagnostic,
|
|
4200
|
+
diagnosticMessage
|
|
4121
4201
|
};
|
|
4122
4202
|
}
|
|
4123
4203
|
});
|
|
@@ -4238,7 +4318,7 @@ function parseClientPermissions(value, columns, scope) {
|
|
|
4238
4318
|
}
|
|
4239
4319
|
}
|
|
4240
4320
|
if (!["owner", "member", "shared", "policy"].includes(scope.kind)) {
|
|
4241
|
-
return fail("serverOnly() tables cannot declare client access
|
|
4321
|
+
return fail("serverOnly() tables cannot declare client access; their rows are reached only from server functions.");
|
|
4242
4322
|
}
|
|
4243
4323
|
if (scope.policy?.kind === "all_of") {
|
|
4244
4324
|
if (input.publicRead !== void 0 && input.publicRead !== false) return fail("allOf() tables cannot declare client.publicRead; their rows are reachable only by current members.");
|
|
@@ -4296,9 +4376,9 @@ function parseClientPermissions(value, columns, scope) {
|
|
|
4296
4376
|
if (column.helper === "id" || (scope.kind === "owner" || scope.kind === "policy") && name === scope.column) {
|
|
4297
4377
|
return fail(`client.${operation} cannot write the platform-assigned identity field "${entry}".`);
|
|
4298
4378
|
}
|
|
4299
|
-
if (column.helper === "blob") return fail(`client.${operation} cannot write binary field "${entry}"
|
|
4379
|
+
if (column.helper === "blob") return fail(`client.${operation} cannot write binary field "${entry}"; uploads go to project files.`);
|
|
4300
4380
|
if (typeof publicRead === "object" && Object.prototype.hasOwnProperty.call(publicRead.where, name)) {
|
|
4301
|
-
return fail(`client.${operation} cannot write publication field "${entry}"
|
|
4381
|
+
return fail(`client.${operation} cannot write publication field "${entry}"; rows are published by a server operation.`);
|
|
4302
4382
|
}
|
|
4303
4383
|
if (operation === "update" && (scope.kind === "member" || scope.kind === "policy") && scope.group?.includes(name)) {
|
|
4304
4384
|
return fail(`client.update cannot move a row between membership groups by changing "${entry}".`);
|
|
@@ -4311,7 +4391,7 @@ function parseClientPermissions(value, columns, scope) {
|
|
|
4311
4391
|
}
|
|
4312
4392
|
if (writes.create !== null) {
|
|
4313
4393
|
if (typeof publicRead === "object" && !privateByDefault(publicRead.where, fields)) {
|
|
4314
|
-
return fail("client.create has no fixed publication-field default that guarantees a private row.
|
|
4394
|
+
return fail("client.create has no fixed publication-field default that guarantees a private row. It is accepted when at least one predicate field has a private default; otherwise rows are created by a server operation.");
|
|
4315
4395
|
}
|
|
4316
4396
|
for (const column of columns) {
|
|
4317
4397
|
if (column.helper !== "id" && !column.nullable && !column.hasDefault && !column.autoNow && !writes.create.includes(column.name)) {
|
|
@@ -4389,7 +4469,7 @@ function parseIntakePermissions(value, columns, scope, publicRead) {
|
|
|
4389
4469
|
const column = byName2.get(name);
|
|
4390
4470
|
if (!column) return `client.intake.fields names undeclared or platform-managed field "${entry}".`;
|
|
4391
4471
|
if (column.helper === "id" || name === scope.column) return `client.intake.fields cannot set the platform-assigned identity field "${entry}".`;
|
|
4392
|
-
if (column.helper === "blob") return `client.intake.fields cannot accept binary field "${entry}"
|
|
4472
|
+
if (column.helper === "blob") return `client.intake.fields cannot accept binary field "${entry}"; uploads go to project files.`;
|
|
4393
4473
|
if (column.autoNow) return `client.intake.fields cannot set "${entry}", which the platform fills in.`;
|
|
4394
4474
|
if (typeof publicRead === "object" && Object.prototype.hasOwnProperty.call(publicRead.where, name)) {
|
|
4395
4475
|
return `client.intake.fields cannot set publication field "${entry}".`;
|
|
@@ -4403,7 +4483,7 @@ function parseIntakePermissions(value, columns, scope, publicRead) {
|
|
|
4403
4483
|
}
|
|
4404
4484
|
}
|
|
4405
4485
|
if (typeof publicRead === "object" && !privateByDefault(publicRead.where, byName2)) {
|
|
4406
|
-
return "client.intake has no fixed publication-field default that guarantees a private row
|
|
4486
|
+
return "client.intake has no fixed publication-field default that guarantees a private row; it is accepted when at least one predicate field has a private default.";
|
|
4407
4487
|
}
|
|
4408
4488
|
let where;
|
|
4409
4489
|
if (input.where !== void 0) {
|
|
@@ -4623,10 +4703,10 @@ function readPaymentBindings(r, fail) {
|
|
|
4623
4703
|
fields[key] = readString(r, fail, `payments.${name}.${key}`).value.toLowerCase();
|
|
4624
4704
|
return;
|
|
4625
4705
|
}
|
|
4626
|
-
r.expectPunct("{", `opening payments.${name}.onPaid
|
|
4706
|
+
r.expectPunct("{", `opening payments.${name}.onPaid, which takes one transition: { status: { from: [...], to: '...' } }`);
|
|
4627
4707
|
const column = r.readKey(`for the status column in payments.${name}.onPaid`);
|
|
4628
4708
|
r.expectPunct(":", `after payments.${name}.onPaid.${column.name}`);
|
|
4629
|
-
r.expectPunct("{", `opening payments.${name}.onPaid.${column.name}
|
|
4709
|
+
r.expectPunct("{", `opening payments.${name}.onPaid.${column.name}, which takes { from: [...], to: '...' }`);
|
|
4630
4710
|
const transition = {};
|
|
4631
4711
|
while (!r.tryPunct("}")) {
|
|
4632
4712
|
const opt = r.readKey(`for an option of payments.${name}.onPaid.${column.name}`);
|
|
@@ -4644,7 +4724,7 @@ function readPaymentBindings(r, fail) {
|
|
|
4644
4724
|
r.tryPunct(",");
|
|
4645
4725
|
if (!r.tryPunct("}")) {
|
|
4646
4726
|
const extra = r.peek();
|
|
4647
|
-
fail(extra.line, `payments.${name}.onPaid declares one status transition only;
|
|
4727
|
+
fail(extra.line, `payments.${name}.onPaid declares one status transition only; this one has a second entry. Other paid effects run from the signed payment event.`);
|
|
4648
4728
|
}
|
|
4649
4729
|
const values = [...transition.from, transition.to];
|
|
4650
4730
|
for (const t of values) {
|
|
@@ -4680,7 +4760,7 @@ function readJobBindings(r, fail) {
|
|
|
4680
4760
|
}
|
|
4681
4761
|
function readServiceRoles(r, fail) {
|
|
4682
4762
|
const entries = readNamed(r, fail, "serviceRoles", "service role", ["tables"], (_key, fields, name) => {
|
|
4683
|
-
r.expectPunct("{", `opening serviceRoles.${name}.tables
|
|
4763
|
+
r.expectPunct("{", `opening serviceRoles.${name}.tables, which takes { table: ['read', ...] }`);
|
|
4684
4764
|
const tables = [];
|
|
4685
4765
|
const tableLines = {};
|
|
4686
4766
|
while (!r.tryPunct("}")) {
|
|
@@ -4713,7 +4793,7 @@ function validateBindings(input, tableInfo, errors) {
|
|
|
4713
4793
|
errors.push(`${at(entry, "table")}${kind} "${entry.name}" names table "${entry.table}", which is not declared in db/schema.ts.`);
|
|
4714
4794
|
return null;
|
|
4715
4795
|
}
|
|
4716
|
-
if (!info.privateUser) errors.push(`${at(entry, "table")}${kind} "${entry.name}" names table "${entry.table}", whose scope is not owner(), member(), anyOf(), group(), appRole() or parent(). It binds rows a signed-in user holds;
|
|
4796
|
+
if (!info.privateUser) errors.push(`${at(entry, "table")}${kind} "${entry.name}" names table "${entry.table}", whose scope is not owner(), member(), anyOf(), group(), appRole() or parent(). It binds rows a signed-in user holds; other tables are reached from server functions.`);
|
|
4717
4797
|
if (!info.key) errors.push(`${at(entry, "table")}${kind} "${entry.name}" names table "${entry.table}", which needs exactly one id() key.`);
|
|
4718
4798
|
return info;
|
|
4719
4799
|
};
|
|
@@ -4722,7 +4802,7 @@ function validateBindings(input, tableInfo, errors) {
|
|
|
4722
4802
|
const info = rowTable(p, "payment binding");
|
|
4723
4803
|
if (!info) continue;
|
|
4724
4804
|
if (info.table.scope.kind === "policy" && reachesAllOf(info.table.scope.policy)) {
|
|
4725
|
-
errors.push(`${at(p, "table")}payment binding "${p.name}" names table "${p.table}", which uses allOf(); payment bindings on allOf tables are not supported yet
|
|
4805
|
+
errors.push(`${at(p, "table")}payment binding "${p.name}" names table "${p.table}", which uses allOf(); payment bindings on allOf tables are not supported yet; such tables are reached from server functions.`);
|
|
4726
4806
|
continue;
|
|
4727
4807
|
}
|
|
4728
4808
|
const column = (name) => info.table.columns.find((c) => c.name === name);
|
|
@@ -4740,7 +4820,7 @@ function validateBindings(input, tableInfo, errors) {
|
|
|
4740
4820
|
if (c.helper !== helper || c.nullable) errors.push(`${at(p, field)}payment binding "${p.name}": "${p.table}"."${name}" must be ${what}.`);
|
|
4741
4821
|
if (info.identity.includes(name)) errors.push(`${at(p, field)}payment binding "${p.name}": "${p.table}"."${name}" is the table's access column; a payment never moves a row.`);
|
|
4742
4822
|
const writable = [...info.table.client?.create ?? [], ...info.table.client?.update ?? [], ...info.table.client?.intake?.fields ?? []];
|
|
4743
|
-
if (writable.includes(name)) errors.push(`${at(p, field)}payment binding "${p.name}": "${p.table}"."${name}" is browser-writable through client.create, client.update or client.intake
|
|
4823
|
+
if (writable.includes(name)) errors.push(`${at(p, field)}payment binding "${p.name}": "${p.table}"."${name}" is browser-writable through client.create, client.update or client.intake, and a payment binding requires it not to be; the server sets prices and the payment sets the status.`);
|
|
4744
4824
|
}
|
|
4745
4825
|
if ((/* @__PURE__ */ new Set([p.amount, p.currency, p.status.column])).size !== 3) errors.push(`${at(p, "")}payment binding "${p.name}" must name three different columns for amount, currency and status.`);
|
|
4746
4826
|
const status = column(p.status.column);
|
|
@@ -4751,7 +4831,7 @@ function validateBindings(input, tableInfo, errors) {
|
|
|
4751
4831
|
for (const j of input.jobs ?? []) rowTable(j, "job binding");
|
|
4752
4832
|
const taken = /* @__PURE__ */ new Set([...BUILT_IN_GROUP_ROLES, ...input.groups?.roles ?? [], ...(input.appRoles ?? []).map((role) => role.name)]);
|
|
4753
4833
|
for (const s of input.serviceRoles ?? []) {
|
|
4754
|
-
if (taken.has(s.name)) errors.push(`${at(s, "")}service role "${s.name}" has the name of a group or app role. Service roles are a separate namespace for unattended work
|
|
4834
|
+
if (taken.has(s.name)) errors.push(`${at(s, "")}service role "${s.name}" has the name of a group or app role. Service roles are a separate namespace for unattended work, so the name must differ.`);
|
|
4755
4835
|
const tableLines = lines.get(s.tables) ?? {};
|
|
4756
4836
|
for (const grant of s.tables) {
|
|
4757
4837
|
if (!tableInfo(grant.table)) errors.push(`line ${tableLines[grant.table] ?? 0}: service role "${s.name}" grants table "${grant.table}", which is not declared in db/schema.ts.`);
|
|
@@ -5095,7 +5175,7 @@ function resolveSchemaConstants(tokens, callable, maxTokens, fail) {
|
|
|
5095
5175
|
const budget = (line, adding) => {
|
|
5096
5176
|
expanded += adding;
|
|
5097
5177
|
if (expanded > maxTokens) {
|
|
5098
|
-
fail(line, "reusing constants here expands the declaration past the size db/schema.ts allows
|
|
5178
|
+
fail(line, "reusing constants here expands the declaration past the size db/schema.ts allows; fewer copies, or the shared part written once, stay within it.");
|
|
5099
5179
|
}
|
|
5100
5180
|
};
|
|
5101
5181
|
const expandRange = (from, to, into, owner) => {
|
|
@@ -5112,7 +5192,7 @@ function resolveSchemaConstants(tokens, callable, maxTokens, fail) {
|
|
|
5112
5192
|
}
|
|
5113
5193
|
const constant = lookup(target, owner);
|
|
5114
5194
|
if (opener !== "{") {
|
|
5115
|
-
fail(t.line, `...${target.value} is only supported inside an object literal { \u2026 }
|
|
5195
|
+
fail(t.line, `...${target.value} is only supported inside an object literal { \u2026 }; elsewhere ${target.value} is referenced directly or its values are written inline.`);
|
|
5116
5196
|
}
|
|
5117
5197
|
if (!constant.objectLiteral) {
|
|
5118
5198
|
fail(t.line, `...${target.value} spreads a constant that is not an object literal { \u2026 }. Only object constants can be spread.`);
|
|
@@ -5138,7 +5218,7 @@ function resolveSchemaConstants(tokens, callable, maxTokens, fail) {
|
|
|
5138
5218
|
}
|
|
5139
5219
|
if (owner === null && STATEMENT_WORDS.has(t.value)) {
|
|
5140
5220
|
if (isIdent(t, "export") && isIdent(next, "const")) {
|
|
5141
|
-
fail(t.line, '"export const" is not supported in db/schema.ts; the file exports only the schema
|
|
5221
|
+
fail(t.line, '"export const" is not supported in db/schema.ts; the file exports only the schema, and constants are declared without "export".');
|
|
5142
5222
|
}
|
|
5143
5223
|
into.push(t);
|
|
5144
5224
|
continue;
|
|
@@ -5149,7 +5229,7 @@ function resolveSchemaConstants(tokens, callable, maxTokens, fail) {
|
|
|
5149
5229
|
fail(t.line, `${t.value} is a constant, not a function \u2014 write ${t.value} without "( \u2026 )".`);
|
|
5150
5230
|
}
|
|
5151
5231
|
if (isPunct(next, ".")) {
|
|
5152
|
-
fail(t.line, `${t.value}.${tokens[k + 2]?.value ?? ""} reads a property of a constant, which db/schema.ts cannot evaluate
|
|
5232
|
+
fail(t.line, `${t.value}.${tokens[k + 2]?.value ?? ""} reads a property of a constant, which db/schema.ts cannot evaluate; that part is accepted as its own constant or written inline.`);
|
|
5153
5233
|
}
|
|
5154
5234
|
budget(t.line, constant.tokens.length);
|
|
5155
5235
|
append(into, constant.tokens);
|
|
@@ -5164,12 +5244,12 @@ function resolveSchemaConstants(tokens, callable, maxTokens, fail) {
|
|
|
5164
5244
|
continue;
|
|
5165
5245
|
}
|
|
5166
5246
|
if (isPunct(next, ".")) {
|
|
5167
|
-
fail(t.line, `${t.value}.${tokens[k + 2]?.value ?? ""} reads a value at run time; db/schema.ts is read without running it
|
|
5247
|
+
fail(t.line, `${t.value}.${tokens[k + 2]?.value ?? ""} reads a value at run time; db/schema.ts is read without running it, so it accepts only the literal value.`);
|
|
5168
5248
|
}
|
|
5169
5249
|
if (isPunct(next, "(")) {
|
|
5170
|
-
fail(t.line, `${t.value}(\u2026) is a function call; db/schema.ts is read without running it, so only the schema helpers can be called.
|
|
5250
|
+
fail(t.line, `${t.value}(\u2026) is a function call; db/schema.ts is read without running it, so only the schema helpers can be called. Values are written inline, or declared with const using schema helpers and literals.`);
|
|
5171
5251
|
}
|
|
5172
|
-
fail(t.line, `"${t.value}" is not defined in db/schema.ts.
|
|
5252
|
+
fail(t.line, `"${t.value}" is not defined in db/schema.ts. A value is either declared above export default as const ${t.value} = \u2026; or written inline.`);
|
|
5173
5253
|
}
|
|
5174
5254
|
};
|
|
5175
5255
|
const lookup = (t, owner) => {
|
|
@@ -5180,37 +5260,37 @@ function resolveSchemaConstants(tokens, callable, maxTokens, fail) {
|
|
|
5180
5260
|
fail(t.line, `constant ${t.value} refers to itself. A constant must be built from values declared above it.`);
|
|
5181
5261
|
}
|
|
5182
5262
|
if (at !== void 0) {
|
|
5183
|
-
fail(t.line, owner ? `constant ${owner} uses ${t.value}, which is declared later (line ${at})
|
|
5263
|
+
fail(t.line, owner ? `constant ${owner} uses ${t.value}, which is declared later (line ${at}); ${t.value} must come above ${owner}, and constants that refer to each other in a cycle cannot be resolved.` : `${t.value} is used before its declaration on line ${at}; a const must come above export default.`);
|
|
5184
5264
|
}
|
|
5185
|
-
return fail(t.line, `"${t.value}" is not defined in db/schema.ts.
|
|
5265
|
+
return fail(t.line, `"${t.value}" is not defined in db/schema.ts. A value is either declared above export default as const ${t.value} = \u2026; or written inline.`);
|
|
5186
5266
|
};
|
|
5187
5267
|
while (!isIdent(tokens[i], "export")) {
|
|
5188
5268
|
const t = tokens[i];
|
|
5189
5269
|
if (t.kind === "eof") return [...out, t];
|
|
5190
5270
|
if (isIdent(t, "let") || isIdent(t, "var")) {
|
|
5191
|
-
fail(t.line, `"${t.value}" declares a changeable binding; db/schema.ts allows only const
|
|
5271
|
+
fail(t.line, `"${t.value}" declares a changeable binding; db/schema.ts allows only const, not "${t.value}".`);
|
|
5192
5272
|
}
|
|
5193
5273
|
if (!isIdent(t, "const")) {
|
|
5194
5274
|
if (t.kind === "ident" && constants.has(t.value)) {
|
|
5195
|
-
fail(t.line, `${t.value} is changed after its declaration; constants in db/schema.ts cannot be reassigned or mutated
|
|
5275
|
+
fail(t.line, `${t.value} is changed after its declaration; constants in db/schema.ts cannot be reassigned or mutated, so each final value is declared once with const.`);
|
|
5196
5276
|
}
|
|
5197
5277
|
if (isIdent(t, "import")) fail(t.line, "imports must come first in db/schema.ts, before any const.");
|
|
5198
5278
|
return [...out, ...tokens.slice(i)];
|
|
5199
5279
|
}
|
|
5200
5280
|
const nameTok = tokens[i + 1];
|
|
5201
5281
|
if (!nameTok || nameTok.kind !== "ident") {
|
|
5202
|
-
fail(t.line, "destructuring (const { \u2026 } = / const [ \u2026 ] =) is not supported in db/schema.ts
|
|
5282
|
+
fail(t.line, "destructuring (const { \u2026 } = / const [ \u2026 ] =) is not supported in db/schema.ts; it accepts one constant per name: const name = \u2026;");
|
|
5203
5283
|
}
|
|
5204
5284
|
const name = nameTok.value;
|
|
5205
5285
|
if (callable.has(name) || LITERAL_WORDS.has(name) || STATEMENT_WORDS.has(name) || name === "schema") {
|
|
5206
|
-
fail(nameTok.line, `const ${name} shadows a db/schema.ts helper or keyword
|
|
5286
|
+
fail(nameTok.line, `const ${name} shadows a db/schema.ts helper or keyword; a different name, such as ${name}Rule, does not.`);
|
|
5207
5287
|
}
|
|
5208
5288
|
if (imported.has(name)) {
|
|
5209
|
-
fail(nameTok.line, `const ${name} has the same name as an import
|
|
5289
|
+
fail(nameTok.line, `const ${name} has the same name as an import; constant names must differ from imports.`);
|
|
5210
5290
|
}
|
|
5211
|
-
if (constants.has(name)) fail(nameTok.line, `const ${name} is declared twice
|
|
5291
|
+
if (constants.has(name)) fail(nameTok.line, `const ${name} is declared twice; a constant is declared once.`);
|
|
5212
5292
|
if (isPunct(tokens[i + 2], ":")) {
|
|
5213
|
-
fail(tokens[i + 2].line, `const ${name} has a type annotation; db/schema.ts constants take their type from the helpers
|
|
5293
|
+
fail(tokens[i + 2].line, `const ${name} has a type annotation; db/schema.ts constants take their type from the helpers, so ": \u2026" is not accepted.`);
|
|
5214
5294
|
}
|
|
5215
5295
|
if (!isPunct(tokens[i + 2], "=")) fail(nameTok.line, `expected "=" after const ${name}.`);
|
|
5216
5296
|
const start = i + 3;
|
|
@@ -5221,16 +5301,16 @@ function resolveSchemaConstants(tokens, callable, maxTokens, fail) {
|
|
|
5221
5301
|
if (v.kind === "punct" && OPENERS.includes(v.value)) depth++;
|
|
5222
5302
|
else if (v.kind === "punct" && CLOSERS.includes(v.value)) {
|
|
5223
5303
|
depth--;
|
|
5224
|
-
if (depth < 0) fail(v.line, `const ${name} has an unmatched "${v.value}"
|
|
5304
|
+
if (depth < 0) fail(v.line, `const ${name} has an unmatched "${v.value}"; its brackets are unbalanced.`);
|
|
5225
5305
|
} else if (depth === 0 && (isPunct(v, ";") || isPunct(v, ",") || isIdent(v, "const") && !isIdent(tokens[k - 1], "as") || isIdent(v, "let") || isIdent(v, "var") || isIdent(v, "export"))) break;
|
|
5226
|
-
if (depth === 0 && isPunct(v, "=")) fail(v.line, `${name} is assigned twice;
|
|
5306
|
+
if (depth === 0 && isPunct(v, "=")) fail(v.line, `${name} is assigned twice; each constant takes one value.`);
|
|
5227
5307
|
}
|
|
5228
|
-
if (depth !== 0) fail(nameTok.line, `const ${name} has an unclosed bracket
|
|
5308
|
+
if (depth !== 0) fail(nameTok.line, `const ${name} has an unclosed bracket; its brackets are unbalanced.`);
|
|
5229
5309
|
let end2 = k;
|
|
5230
5310
|
if (end2 - start >= 2 && isIdent(tokens[end2 - 2], "as") && isIdent(tokens[end2 - 1], "const")) end2 -= 2;
|
|
5231
5311
|
if (end2 === start) fail(nameTok.line, `const ${name} has no value.`);
|
|
5232
5312
|
if (isPunct(tokens[k], ",")) {
|
|
5233
|
-
fail(tokens[k].line, `one declaration declares several constants;
|
|
5313
|
+
fail(tokens[k].line, `one declaration declares several constants; db/schema.ts accepts one const per name (const ${name} = \u2026;).`);
|
|
5234
5314
|
}
|
|
5235
5315
|
const value = [];
|
|
5236
5316
|
expandRange(start, end2, value, name);
|
|
@@ -5239,7 +5319,7 @@ function resolveSchemaConstants(tokens, callable, maxTokens, fail) {
|
|
|
5239
5319
|
i = isPunct(tokens[k], ";") ? k + 1 : k;
|
|
5240
5320
|
}
|
|
5241
5321
|
if (isIdent(tokens[i], "export") && isIdent(tokens[i + 1], "const")) {
|
|
5242
|
-
fail(tokens[i].line, '"export const" is not supported in db/schema.ts; the file exports only the schema
|
|
5322
|
+
fail(tokens[i].line, '"export const" is not supported in db/schema.ts; the file exports only the schema, and constants are declared without "export".');
|
|
5243
5323
|
}
|
|
5244
5324
|
const body = [];
|
|
5245
5325
|
const open = tokens.findIndex((v, k) => k > i && isPunct(v, "(") && isIdent(tokens[k - 1], "schema"));
|
|
@@ -5249,12 +5329,12 @@ function resolveSchemaConstants(tokens, callable, maxTokens, fail) {
|
|
|
5249
5329
|
for (let k = end; k < tokens.length; k++) {
|
|
5250
5330
|
const v = tokens[k];
|
|
5251
5331
|
if (v.kind === "ident" && constants.has(v.value) && (isPunct(tokens[k + 1], "=") || isPunct(tokens[k + 1], "."))) {
|
|
5252
|
-
fail(v.line, `${v.value} is changed after its declaration; constants in db/schema.ts cannot be reassigned or mutated
|
|
5332
|
+
fail(v.line, `${v.value} is changed after its declaration; constants in db/schema.ts cannot be reassigned or mutated, so each final value is declared once with const.`);
|
|
5253
5333
|
}
|
|
5254
5334
|
body.push(v);
|
|
5255
5335
|
}
|
|
5256
5336
|
if (out.length + body.length > maxTokens) {
|
|
5257
|
-
fail(tokens[i].line, "reusing constants here expands the declaration past the size db/schema.ts allows
|
|
5337
|
+
fail(tokens[i].line, "reusing constants here expands the declaration past the size db/schema.ts allows; fewer copies, or the shared part written once, stay within it.");
|
|
5258
5338
|
}
|
|
5259
5339
|
return [...out, ...body];
|
|
5260
5340
|
}
|
|
@@ -5364,18 +5444,18 @@ function validateDeclaredRelations(tables, tableByName, knownColumns, markedGone
|
|
|
5364
5444
|
for (const rel of t.relations) {
|
|
5365
5445
|
if (rel.name === "__proto__" || rel.name === "prototype" || rel.name === "constructor") {
|
|
5366
5446
|
errors.push(
|
|
5367
|
-
`Relation "${t.name}"."${rel.name}" uses a reserved object name
|
|
5447
|
+
`Relation "${t.name}"."${rel.name}" uses a reserved object name; a relation name must be returnable as an ordinary row field.`
|
|
5368
5448
|
);
|
|
5369
5449
|
} else if (rootNames.has(rel.name)) {
|
|
5370
5450
|
errors.push(
|
|
5371
|
-
`Relation "${t.name}"."${rel.name}" has the same name as a column on "${t.name}".
|
|
5451
|
+
`Relation "${t.name}"."${rel.name}" has the same name as a column on "${t.name}". A relation name must differ from the columns, so nesting related rows cannot overwrite the root column.`
|
|
5372
5452
|
);
|
|
5373
5453
|
}
|
|
5374
5454
|
const related = tableByName.get(rel.table);
|
|
5375
5455
|
if (!related) {
|
|
5376
5456
|
const marked = markedGone(rel.table);
|
|
5377
5457
|
errors.push(
|
|
5378
|
-
marked ? `Relation "${t.name}"."${rel.name}" targets "${rel.table}", which this file marks ${marked}.
|
|
5458
|
+
marked ? `Relation "${t.name}"."${rel.name}" targets "${rel.table}", which this file marks ${marked}. The table can go only once no relation targets it.` : `Relation "${t.name}"."${rel.name}" targets ${rel.kind === "hasMany" ? "child" : "parent"} table "${rel.table}", which is not declared in db/schema.ts. A relation can only target a managed table in the same declaration.`
|
|
5379
5459
|
);
|
|
5380
5460
|
continue;
|
|
5381
5461
|
}
|
|
@@ -5384,7 +5464,7 @@ function validateDeclaredRelations(tables, tableByName, knownColumns, markedGone
|
|
|
5384
5464
|
const idCol = keySide.columns.find((c) => c.helper === "id");
|
|
5385
5465
|
if (!idCol) {
|
|
5386
5466
|
errors.push(
|
|
5387
|
-
rel.kind === "hasMany" ? `Table "${t.name}" declares relation "${rel.name}" but has no id() column to join on. A hasMany relation joins the child's foreign key to this table's primary key
|
|
5467
|
+
rel.kind === "hasMany" ? `Table "${t.name}" declares relation "${rel.name}" but has no id() column to join on. A hasMany relation joins the child's foreign key to this table's primary key, so the table needs an id() column.` : `Relation "${t.name}"."${rel.name}" belongsTo "${rel.table}", but "${rel.table}" has no id() column to join on. A belongsTo relation joins this table's foreign key to the parent's primary key, so "${rel.table}" needs an id() column.`
|
|
5388
5468
|
);
|
|
5389
5469
|
} else {
|
|
5390
5470
|
rel.parentKey = idCol.name;
|
|
@@ -5398,13 +5478,13 @@ function validateDeclaredRelations(tables, tableByName, knownColumns, markedGone
|
|
|
5398
5478
|
}
|
|
5399
5479
|
if (fkCol.references !== keySide.name) {
|
|
5400
5480
|
errors.push(
|
|
5401
|
-
fkCol.references === null ? `Relation "${t.name}"."${rel.name}" joins on "${fkSide.name}"."${rel.fk}", but that column declares no foreign key.
|
|
5481
|
+
fkCol.references === null ? `Relation "${t.name}"."${rel.name}" joins on "${fkSide.name}"."${rel.fk}", but that column declares no foreign key. The join key must be a proven foreign key, not a free-form join: ${rel.fk}: <type>({ references: '${keySide.name}' }).` : `Relation "${t.name}"."${rel.name}" joins on "${fkSide.name}"."${rel.fk}", but that column references "${fkCol.references}", not "${keySide.name}". The foreign key must reference the table that holds the primary key of this relation.`
|
|
5402
5482
|
);
|
|
5403
5483
|
} else if (idCol) {
|
|
5404
5484
|
const expectedHelper = idCol.uuid ? "text" : "integer";
|
|
5405
5485
|
if (fkCol.helper !== expectedHelper) {
|
|
5406
5486
|
errors.push(
|
|
5407
|
-
`Relation "${t.name}"."${rel.name}" joins "${fkSide.name}"."${rel.fk}" to "${keySide.name}"."${idCol.name}", but their key types do not match.
|
|
5487
|
+
`Relation "${t.name}"."${rel.name}" joins "${fkSide.name}"."${rel.fk}" to "${keySide.name}"."${idCol.name}", but their key types do not match. The foreign key needs ${expectedHelper}() so fetched rows can be stitched to the ${idCol.uuid ? "text" : "whole-number"} key exactly.`
|
|
5408
5488
|
);
|
|
5409
5489
|
}
|
|
5410
5490
|
}
|
|
@@ -5436,7 +5516,7 @@ function validateRowLinks(tables, tableByName, errors) {
|
|
|
5436
5516
|
}
|
|
5437
5517
|
const earlier = childOf.get(child.name);
|
|
5438
5518
|
if (earlier && earlier !== t.name) {
|
|
5439
|
-
errors.push(`Table "${child.name}" is a link child of both "${earlier}" and "${t.name}"
|
|
5519
|
+
errors.push(`Table "${child.name}" is a link child of both "${earlier}" and "${t.name}"; a table is a link child of one table only.`);
|
|
5440
5520
|
continue;
|
|
5441
5521
|
}
|
|
5442
5522
|
childOf.set(child.name, t.name);
|
|
@@ -5485,7 +5565,7 @@ function validateIntake(tables, tableByName, errors) {
|
|
|
5485
5565
|
continue;
|
|
5486
5566
|
}
|
|
5487
5567
|
if (column.helper !== "id" && !column.unique) {
|
|
5488
|
-
errors.push(`${at} names reference "${field}", which is not unique on "${target.name}".
|
|
5568
|
+
errors.push(`${at} names reference "${field}", which is not unique on "${target.name}". A reference must name one row, so the field needs { unique: true }.`);
|
|
5489
5569
|
continue;
|
|
5490
5570
|
}
|
|
5491
5571
|
if (column.nullable) {
|
|
@@ -5599,7 +5679,7 @@ function readFileOwner(r, fail, name) {
|
|
|
5599
5679
|
r.expectPunct("{", `in owner() for file collection "${name}"`);
|
|
5600
5680
|
const key = r.readKey(`in owner() for file collection "${name}"`);
|
|
5601
5681
|
if (key.name === "visitors") {
|
|
5602
|
-
fail(key.line, `owner({ visitors: true }) is not available for file collections yet: an upload needs a signed-in account
|
|
5682
|
+
fail(key.line, `owner({ visitors: true }) is not available for file collections yet: an upload needs a signed-in account, so file collections take owner().`);
|
|
5603
5683
|
}
|
|
5604
5684
|
fail(key.line, `owner() on file collection "${name}" takes no options \u2014 the owner is always the signed-in uploader.`);
|
|
5605
5685
|
}
|
|
@@ -5662,7 +5742,7 @@ function readFileScope(r, fail, name) {
|
|
|
5662
5742
|
if (owners !== 1 || !member) fail(tok.line, `anyOf() on file collection "${name}" requires exactly one owner() and one member().`);
|
|
5663
5743
|
return { kind: "owner_or_member", member };
|
|
5664
5744
|
}
|
|
5665
|
-
return fail(tok.line, `unknown scope "${tok.value}" on file collection "${name}".
|
|
5745
|
+
return fail(tok.line, `unknown scope "${tok.value}" on file collection "${name}". File collection scopes are owner(), member(), anyOf(owner(), member()), parent({ table, via }), shared(), and serverOnly().`);
|
|
5666
5746
|
}
|
|
5667
5747
|
function readCollection(r, fail, name, errors) {
|
|
5668
5748
|
const callee = r.expectIdent(`for file collection "${name}"`);
|
|
@@ -5716,11 +5796,11 @@ function readCollection(r, fail, name, errors) {
|
|
|
5716
5796
|
if (limit.name === "maxSize") {
|
|
5717
5797
|
const raw = r.readLiteral(`for limits.maxSize of "${name}"`);
|
|
5718
5798
|
limits.maxSize = parseFileSize(raw);
|
|
5719
|
-
if (limits.maxSize === null) errors.push(`File collection "${name}" has an invalid limits.maxSize:
|
|
5799
|
+
if (limits.maxSize === null) errors.push(`File collection "${name}" has an invalid limits.maxSize: it takes a whole number of bytes or a size such as '25MB'.`);
|
|
5720
5800
|
} else {
|
|
5721
5801
|
limits.types = readStringList(r, fail, `limits.types of file collection "${name}"`).map((t) => t.toLowerCase());
|
|
5722
5802
|
for (const type of limits.types) {
|
|
5723
|
-
if (!MIME_PATTERN.test(type)) errors.push(`File collection "${name}" has an invalid limits.types entry "${type}":
|
|
5803
|
+
if (!MIME_PATTERN.test(type)) errors.push(`File collection "${name}" has an invalid limits.types entry "${type}": it takes a content type such as 'image/png' or 'image/*'.`);
|
|
5724
5804
|
}
|
|
5725
5805
|
}
|
|
5726
5806
|
if (!r.tryPunct(",")) {
|
|
@@ -5741,16 +5821,16 @@ function readCollection(r, fail, name, errors) {
|
|
|
5741
5821
|
if (rootError) errors.push(rootError);
|
|
5742
5822
|
const declaresClient = FILE_OPERATIONS.some((op) => client[op]);
|
|
5743
5823
|
if (scope.kind === "server_only" && declaresClient) {
|
|
5744
|
-
errors.push(`File collection "${name}" is serverOnly() and cannot declare client operations
|
|
5824
|
+
errors.push(`File collection "${name}" is serverOnly() and cannot declare client operations; client operations need a scope the browser may use.`);
|
|
5745
5825
|
}
|
|
5746
5826
|
if (isPublic && client.read) {
|
|
5747
|
-
errors.push(`File collection "${name}" is public: true and also declares client.read. Public already lets anyone read
|
|
5827
|
+
errors.push(`File collection "${name}" is public: true and also declares client.read. Public already lets anyone read, so client.read is not accepted with it.`);
|
|
5748
5828
|
}
|
|
5749
5829
|
if (isPublic && (scope.kind === "member" || scope.kind === "owner_or_member")) {
|
|
5750
|
-
errors.push(`File collection "${name}" is public: true on a member() scope, which would let anyone read team files.
|
|
5830
|
+
errors.push(`File collection "${name}" is public: true on a member() scope, which would let anyone read team files. public: true is accepted with owner() or serverOnly().`);
|
|
5751
5831
|
}
|
|
5752
5832
|
if (isPublic && scope.kind === "parent") {
|
|
5753
|
-
errors.push(`File collection "${name}" is public: true on a parent() scope, which would let anyone read files that follow a private row
|
|
5833
|
+
errors.push(`File collection "${name}" is public: true on a parent() scope, which would let anyone read files that follow a private row, so public: true is not accepted there.`);
|
|
5754
5834
|
}
|
|
5755
5835
|
return { name, path: typeof path === "string" ? path : "", scope, public: isPublic, client, limits };
|
|
5756
5836
|
}
|
|
@@ -5761,7 +5841,7 @@ function readFileCollections(r, fail, errors) {
|
|
|
5761
5841
|
while (!r.tryPunct("}")) {
|
|
5762
5842
|
const key = r.readKey("for a file collection name");
|
|
5763
5843
|
if (!COLLECTION_NAME.test(key.name)) {
|
|
5764
|
-
errors.push(`File collection name "${key.name}" is invalid.
|
|
5844
|
+
errors.push(`File collection name "${key.name}" is invalid. A name is lowercase letters, digits and underscores, starting with a letter.`);
|
|
5765
5845
|
}
|
|
5766
5846
|
if (out.some((c) => c.name === key.name)) fail(key.line, `file collection "${key.name}" is declared twice.`);
|
|
5767
5847
|
r.expectPunct(":", `after file collection "${key.name}"`);
|
|
@@ -5798,7 +5878,7 @@ function validateFileCollections(collections, table, errors) {
|
|
|
5798
5878
|
continue;
|
|
5799
5879
|
}
|
|
5800
5880
|
if (facts.edges + 1 > MAX_FILE_PARENT_EDGES) {
|
|
5801
|
-
errors.push(`File collection "${a.name}" uses parent({ table: '${parent.table}' }), which makes a chain of ${facts.edges + 1} parent edges counting the file. At most ${MAX_FILE_PARENT_EDGES} are allowed;
|
|
5881
|
+
errors.push(`File collection "${a.name}" uses parent({ table: '${parent.table}' }), which makes a chain of ${facts.edges + 1} parent edges counting the file. At most ${MAX_FILE_PARENT_EDGES} are allowed; a table nearer the root of the chain has fewer edges.`);
|
|
5802
5882
|
continue;
|
|
5803
5883
|
}
|
|
5804
5884
|
parent.key = facts.key;
|
|
@@ -5813,7 +5893,7 @@ function validateFileCollections(collections, table, errors) {
|
|
|
5813
5893
|
continue;
|
|
5814
5894
|
}
|
|
5815
5895
|
if (membership.clientWrites) {
|
|
5816
|
-
errors.push(`Table "${m.membership}" grants membership access to file collection "${a.name}" and cannot allow client writes
|
|
5896
|
+
errors.push(`Table "${m.membership}" grants membership access to file collection "${a.name}" and cannot allow client writes; membership changes go through a server function.`);
|
|
5817
5897
|
}
|
|
5818
5898
|
if (!membership.columns.has(m.memberUser)) errors.push(`The membership table "${m.membership}" (for file collection "${a.name}") has no "${m.memberUser}" column named in member({ member_user }).`);
|
|
5819
5899
|
if (!membership.columns.has(m.memberGroup)) errors.push(`The membership table "${m.membership}" (for file collection "${a.name}") has no "${m.memberGroup}" column named in member({ member_group }).`);
|
|
@@ -5867,7 +5947,7 @@ function roleVocabularyErrors(policy) {
|
|
|
5867
5947
|
dupes(policy.adminGrantableGroupRoles, "groups.adminGrantableRoles");
|
|
5868
5948
|
dupes(policy.appRoles, "appRoles");
|
|
5869
5949
|
for (const role of policy.groupRoles) {
|
|
5870
|
-
if (BUILT_IN_GROUP_ROLES2.includes(role)) errors.push(`"${role}" is a built-in group role;
|
|
5950
|
+
if (BUILT_IN_GROUP_ROLES2.includes(role)) errors.push(`"${role}" is a built-in group role; groups.roles lists only additional roles.`);
|
|
5871
5951
|
}
|
|
5872
5952
|
for (const role of policy.adminGrantableGroupRoles) {
|
|
5873
5953
|
if (!policy.groupRoles.includes(role)) errors.push(`groups.adminGrantableRoles names "${role}", which is not in groups.roles.`);
|
|
@@ -5976,7 +6056,7 @@ function resolveGroupScopes(tables, vocabulary, errors) {
|
|
|
5976
6056
|
const where = `line ${declared.line}: `;
|
|
5977
6057
|
const column = t.columns.find((c) => c.name === node.column);
|
|
5978
6058
|
if (!column) {
|
|
5979
|
-
errors.push(`${where}Table "${t.name}" uses group({ column: '${node.column}' }), but "${node.column}" is not one of its declared columns
|
|
6059
|
+
errors.push(`${where}Table "${t.name}" uses group({ column: '${node.column}' }), but "${node.column}" is not one of its declared columns; it needs ${node.column}: text().`);
|
|
5980
6060
|
} else if (column.helper !== "text" || column.nullable) {
|
|
5981
6061
|
errors.push(`${where}Table "${t.name}" uses group({ column: '${node.column}' }), which must be a non-nullable text() column: every row belongs to exactly one group.`);
|
|
5982
6062
|
}
|
|
@@ -5984,7 +6064,7 @@ function resolveGroupScopes(tables, vocabulary, errors) {
|
|
|
5984
6064
|
const list = declared.explicit[op] ?? omitted;
|
|
5985
6065
|
for (const role of list) {
|
|
5986
6066
|
if (!allRoles.includes(role)) {
|
|
5987
|
-
errors.push(`${where}group({ roles: { ${op} } }) on table "${t.name}" names "${role}", which is not a group role.
|
|
6067
|
+
errors.push(`${where}group({ roles: { ${op} } }) on table "${t.name}" names "${role}", which is not a group role. Group roles are owner, admin, member, and the roles declared in groups.roles.`);
|
|
5988
6068
|
}
|
|
5989
6069
|
}
|
|
5990
6070
|
node.roles[op] = sorted(list);
|
|
@@ -5996,7 +6076,7 @@ function resolveGroupScopes(tables, vocabulary, errors) {
|
|
|
5996
6076
|
}
|
|
5997
6077
|
}
|
|
5998
6078
|
if ((branch || allOf) && GROUP_OPERATIONS.every((op) => node.roles[op].length === 0)) {
|
|
5999
|
-
errors.push(`${where}group() inside ${allOf ? "allOf()" : "anyOf()"} on table "${t.name}" grants no operation
|
|
6079
|
+
errors.push(`${where}group() inside ${allOf ? "allOf()" : "anyOf()"} on table "${t.name}" grants no operation; it needs the group roles that may read, update or delete every row of the group.`);
|
|
6000
6080
|
}
|
|
6001
6081
|
if (!(branch && allOf) && t.client?.create && !t.client.create.includes(node.column)) {
|
|
6002
6082
|
errors.push(`${where}Table "${t.name}" uses group() and client.create must include "${node.column}" so every new row names the group it belongs to.`);
|
|
@@ -6121,7 +6201,7 @@ function resolveAppRoleScopes(tables, appRoles, errors) {
|
|
|
6121
6201
|
if (!declared) continue;
|
|
6122
6202
|
for (const { op, name, line } of declared.names) {
|
|
6123
6203
|
if (!vocabulary.has(name)) {
|
|
6124
|
-
errors.push(`line ${line}: appRole({ roles: { ${op} } }) on table "${t.name}" names "${name}", which is not declared in the schema's appRoles option.
|
|
6204
|
+
errors.push(`line ${line}: appRole({ roles: { ${op} } }) on table "${t.name}" names "${name}", which is not declared in the schema's appRoles option. appRole() accepts only app roles declared there; group roles and admin do not apply.`);
|
|
6125
6205
|
}
|
|
6126
6206
|
}
|
|
6127
6207
|
for (const op of ["update", "delete"]) {
|
|
@@ -6131,7 +6211,7 @@ function resolveAppRoleScopes(tables, appRoles, errors) {
|
|
|
6131
6211
|
}
|
|
6132
6212
|
}
|
|
6133
6213
|
if (branch && APP_ROLE_OPERATIONS.every((op) => node.roles[op].length === 0)) {
|
|
6134
|
-
errors.push(`line ${declared.line}: appRole() inside ${root.kind === "all_of" ? "allOf()" : "anyOf()"} on table "${t.name}" grants no operation
|
|
6214
|
+
errors.push(`line ${declared.line}: appRole() inside ${root.kind === "all_of" ? "allOf()" : "anyOf()"} on table "${t.name}" grants no operation; it needs the app roles that may read, update or delete every row.`);
|
|
6135
6215
|
}
|
|
6136
6216
|
declaredLines.delete(node);
|
|
6137
6217
|
}
|
|
@@ -6184,7 +6264,7 @@ function readAppRoles(r, fail) {
|
|
|
6184
6264
|
const key = r.readKey("for an app role name");
|
|
6185
6265
|
if (out.some((role2) => role2.name === key.name)) fail(key.line, `app role "${key.name}" is declared twice.`);
|
|
6186
6266
|
r.expectPunct(":", `after app role "${key.name}"`);
|
|
6187
|
-
r.expectPunct("{", `opening app role "${key.name}"
|
|
6267
|
+
r.expectPunct("{", `opening app role "${key.name}", which takes { grantableBy: [...] }`);
|
|
6188
6268
|
const role = { name: key.name, grantableBy: [] };
|
|
6189
6269
|
const seen = /* @__PURE__ */ new Set();
|
|
6190
6270
|
while (!r.tryPunct("}")) {
|
|
@@ -6363,7 +6443,7 @@ function tokenize(source) {
|
|
|
6363
6443
|
continue;
|
|
6364
6444
|
}
|
|
6365
6445
|
if (ch === "`") {
|
|
6366
|
-
throw new SchemaTsError(`line ${line}: template literals are not allowed in db/schema.ts
|
|
6446
|
+
throw new SchemaTsError(`line ${line}: template literals are not allowed in db/schema.ts; strings are plain 'single-quoted' strings.`);
|
|
6367
6447
|
}
|
|
6368
6448
|
if (ch === "'" || ch === '"') {
|
|
6369
6449
|
const startLine = line;
|
|
@@ -6564,11 +6644,11 @@ function checkDeclaredName(kind, raw, line) {
|
|
|
6564
6644
|
throw new SchemaTsError(`line ${line}: ${kind} name "${raw.slice(0, 40)}\u2026" is longer than ${MAX_NAME_LENGTH} characters.`);
|
|
6565
6645
|
}
|
|
6566
6646
|
if (!SAFE_IDENT.test(raw)) {
|
|
6567
|
-
throw new SchemaTsError(`line ${line}: ${kind} name "${raw}" is invalid.
|
|
6647
|
+
throw new SchemaTsError(`line ${line}: ${kind} name "${raw}" is invalid. A name is letters, digits, and underscores, starting with a letter or underscore.`);
|
|
6568
6648
|
}
|
|
6569
6649
|
const name = raw.toLowerCase();
|
|
6570
6650
|
if (name.startsWith("_") || RESERVED_PREFIXES.some((p) => name.startsWith(p))) {
|
|
6571
|
-
throw new SchemaTsError(`line ${line}: ${kind} name "${raw}" uses a reserved platform prefix and cannot be declared
|
|
6651
|
+
throw new SchemaTsError(`line ${line}: ${kind} name "${raw}" uses a reserved platform prefix and cannot be declared: names starting with "app_" or "_" belong to the platform.`);
|
|
6572
6652
|
}
|
|
6573
6653
|
return name;
|
|
6574
6654
|
}
|
|
@@ -6582,12 +6662,12 @@ function readColumn(r, tableName, rawName, line) {
|
|
|
6582
6662
|
}
|
|
6583
6663
|
if (TABLE_MARKER_HELPERS.has(helper)) {
|
|
6584
6664
|
throw new SchemaTsError(
|
|
6585
|
-
`line ${helperTok.line}: "${tableName}"."${rawName}" uses ${helper}(), which marks a whole table, not a column. Removing columns from the live database is not supported yet, so
|
|
6665
|
+
`line ${helperTok.line}: "${tableName}"."${rawName}" uses ${helper}(), which marks a whole table, not a column. Removing columns from the live database is not supported yet, so "${rawName}" stays declared with its column type.`
|
|
6586
6666
|
);
|
|
6587
6667
|
}
|
|
6588
6668
|
if (!COLUMN_HELPERS.has(helper)) {
|
|
6589
6669
|
throw new SchemaTsError(
|
|
6590
|
-
`line ${helperTok.line}: unknown column type "${helper}" on "${tableName}"."${rawName}".
|
|
6670
|
+
`line ${helperTok.line}: unknown column type "${helper}" on "${tableName}"."${rawName}". Column types are id, text, number, integer, bigint, boolean, timestamp, json, and blob.`
|
|
6591
6671
|
);
|
|
6592
6672
|
}
|
|
6593
6673
|
r.expectPunct("(", `after "${helper}"`);
|
|
@@ -6644,7 +6724,7 @@ function readColumn(r, tableName, rawName, line) {
|
|
|
6644
6724
|
);
|
|
6645
6725
|
}
|
|
6646
6726
|
if (t === "integer" && typeof value === "number" && !Number.isSafeInteger(value)) {
|
|
6647
|
-
throw new SchemaTsError(Number.isInteger(value) ? `line ${optLine}: the default for integer column "${tableName}"."${rawName}" is outside JavaScript's exact whole-number range (\xB19007199254740991)
|
|
6727
|
+
throw new SchemaTsError(Number.isInteger(value) ? `line ${optLine}: the default for integer column "${tableName}"."${rawName}" is outside JavaScript's exact whole-number range (\xB19007199254740991); a bigint() column takes such a default written as a string.` : `line ${optLine}: the default for integer column "${tableName}"."${rawName}" must be a whole number.`);
|
|
6648
6728
|
}
|
|
6649
6729
|
if (t === "bigint" && typeof value === "string" && !isCanonicalBigintText(value)) {
|
|
6650
6730
|
throw new SchemaTsError(`line ${optLine}: the default for bigint column "${tableName}"."${rawName}" must be a whole number between -9223372036854775808 and 9223372036854775807, written as a string with no sign other than a leading minus and no leading zeros.`);
|
|
@@ -6684,7 +6764,7 @@ function readColumn(r, tableName, rawName, line) {
|
|
|
6684
6764
|
if (col.hasDefault && col.default === null) {
|
|
6685
6765
|
if (!col.nullable) {
|
|
6686
6766
|
throw new SchemaTsError(
|
|
6687
|
-
`line ${line}: "${tableName}"."${rawName}" is required but declares { default: null } \u2014 a required column can never hold null.
|
|
6767
|
+
`line ${line}: "${tableName}"."${rawName}" is required but declares { default: null } \u2014 a required column can never hold null. It is accepted with a non-null default or { nullable: true }.`
|
|
6688
6768
|
);
|
|
6689
6769
|
}
|
|
6690
6770
|
col.hasDefault = false;
|
|
@@ -6927,7 +7007,7 @@ function readRelations(r, tableName) {
|
|
|
6927
7007
|
while (!r.tryPunct("}")) {
|
|
6928
7008
|
const key = r.readKey(`for a relation name of table "${tableName}"`);
|
|
6929
7009
|
if (!SAFE_IDENT.test(key.name) || key.name.length > MAX_NAME_LENGTH) {
|
|
6930
|
-
throw new SchemaTsError(`line ${key.line}: relation name "${key.name}" on table "${tableName}" is invalid.
|
|
7010
|
+
throw new SchemaTsError(`line ${key.line}: relation name "${key.name}" on table "${tableName}" is invalid. A name is letters, digits, and underscores, starting with a letter or underscore.`);
|
|
6931
7011
|
}
|
|
6932
7012
|
const name = key.name.toLowerCase();
|
|
6933
7013
|
if (seen.has(name)) throw new SchemaTsError(`line ${key.line}: relation "${key.name}" on table "${tableName}" is declared twice.`);
|
|
@@ -6935,7 +7015,7 @@ function readRelations(r, tableName) {
|
|
|
6935
7015
|
r.expectPunct(":", `after relation "${key.name}" on table "${tableName}"`);
|
|
6936
7016
|
const callee = r.expectIdent(`for relation "${key.name}" on table "${tableName}"`);
|
|
6937
7017
|
if (callee.value !== "hasMany" && callee.value !== "belongsTo") {
|
|
6938
|
-
throw new SchemaTsError(`line ${callee.line}: relation "${key.name}" on table "${tableName}" uses "${callee.value}()", which is not supported.
|
|
7018
|
+
throw new SchemaTsError(`line ${callee.line}: relation "${key.name}" on table "${tableName}" uses "${callee.value}()", which is not supported. Relations are hasMany('child_table', 'foreign_key') and belongsTo('parent_table', 'foreign_key').`);
|
|
6939
7019
|
}
|
|
6940
7020
|
const kind = callee.value;
|
|
6941
7021
|
const side = kind === "hasMany" ? "child" : "parent";
|
|
@@ -6968,7 +7048,7 @@ function readScope(r, tableName) {
|
|
|
6968
7048
|
const tok = r.expectIdent(`for the scope of table "${tableName}"`);
|
|
6969
7049
|
if (!SCOPE_HELPERS.has(tok.value)) {
|
|
6970
7050
|
throw new SchemaTsError(
|
|
6971
|
-
`line ${tok.line}: unknown scope "${tok.value}" on table "${tableName}".
|
|
7051
|
+
`line ${tok.line}: unknown scope "${tok.value}" on table "${tableName}". Table scopes are owner(), group(), appRole(), member(), anyOf(), allOf(), parent(), shared(), and serverOnly().`
|
|
6972
7052
|
);
|
|
6973
7053
|
}
|
|
6974
7054
|
if (tok.value === "anyOf") return readAnyOfScope(r, tableName, tok.line);
|
|
@@ -7063,7 +7143,7 @@ function readTable(r, rawName, nameLine) {
|
|
|
7063
7143
|
const callee = r.expectIdent(`for table "${rawName}"`);
|
|
7064
7144
|
if (callee.value === "removed") {
|
|
7065
7145
|
throw new SchemaTsError(
|
|
7066
|
-
`line ${callee.line}: table "${rawName}" uses removed(), which marks a single column
|
|
7146
|
+
`line ${callee.line}: table "${rawName}" uses removed(), which marks a single column; a whole table is removed with ${rawName}: removedTable().`
|
|
7067
7147
|
);
|
|
7068
7148
|
}
|
|
7069
7149
|
if (TABLE_MARKER_HELPERS.has(callee.value)) {
|
|
@@ -7100,7 +7180,7 @@ function readTable(r, rawName, nameLine) {
|
|
|
7100
7180
|
}
|
|
7101
7181
|
}
|
|
7102
7182
|
if (columns.length === 0) {
|
|
7103
|
-
throw new SchemaTsError(`line ${nameLine}: table "${rawName}" declares no columns${removedColumns.length > 0 ? " (removal markers do not count \u2014 a table needs at least one kept column, or
|
|
7183
|
+
throw new SchemaTsError(`line ${nameLine}: table "${rawName}" declares no columns${removedColumns.length > 0 ? " (removal markers do not count \u2014 a table needs at least one kept column, or the whole table marked removedTable())" : ""}.`);
|
|
7104
7184
|
}
|
|
7105
7185
|
if (columns.length > MAX_COLUMNS) {
|
|
7106
7186
|
throw new SchemaTsError(`line ${nameLine}: table "${rawName}" declares ${columns.length} columns (max ${MAX_COLUMNS}).`);
|
|
@@ -7158,7 +7238,7 @@ function readTable(r, rawName, nameLine) {
|
|
|
7158
7238
|
const clash = columns.find((c) => c.name === managedOwner.column);
|
|
7159
7239
|
if (clash) {
|
|
7160
7240
|
throw new SchemaTsError(
|
|
7161
|
-
`line ${nameLine}: table "${rawName}" declares the column "${managedOwner.column}", but that is its owner column, which the platform creates and manages
|
|
7241
|
+
`line ${nameLine}: table "${rawName}" declares the column "${managedOwner.column}", but that is its owner column, which the platform creates and manages, so it is not declared among the columns; owner({ column: '\u2026' }) names a different owner column.`
|
|
7162
7242
|
);
|
|
7163
7243
|
}
|
|
7164
7244
|
if (removedColumns.includes(managedOwner.column)) {
|
|
@@ -7173,7 +7253,7 @@ function readTable(r, rawName, nameLine) {
|
|
|
7173
7253
|
if (!c.renamedFrom) continue;
|
|
7174
7254
|
if (declaredNow.has(c.renamedFrom)) {
|
|
7175
7255
|
throw new SchemaTsError(
|
|
7176
|
-
`line ${nameLine}: "${rawName}"."${c.name}" declares renamedFrom: '${c.renamedFrom}', but "${c.renamedFrom}" is still declared as a column. A rename replaces the old column
|
|
7256
|
+
`line ${nameLine}: "${rawName}"."${c.name}" declares renamedFrom: '${c.renamedFrom}', but "${c.renamedFrom}" is still declared as a column. A rename replaces the old column, so the old declaration is not accepted alongside it.`
|
|
7177
7257
|
);
|
|
7178
7258
|
}
|
|
7179
7259
|
if (managedOwner && c.renamedFrom === managedOwner.column) {
|
|
@@ -7183,7 +7263,7 @@ function readTable(r, rawName, nameLine) {
|
|
|
7183
7263
|
}
|
|
7184
7264
|
if (removedColumns.includes(c.renamedFrom)) {
|
|
7185
7265
|
throw new SchemaTsError(
|
|
7186
|
-
`line ${nameLine}: "${rawName}"."${c.renamedFrom}" is marked removed() AND named by renamedFrom on "${c.name}" \u2014 it cannot be both removed and renamed
|
|
7266
|
+
`line ${nameLine}: "${rawName}"."${c.renamedFrom}" is marked removed() AND named by renamedFrom on "${c.name}" \u2014 it cannot be both removed and renamed.`
|
|
7187
7267
|
);
|
|
7188
7268
|
}
|
|
7189
7269
|
if (renameSources.has(c.renamedFrom)) {
|
|
@@ -7218,7 +7298,7 @@ function readTable(r, rawName, nameLine) {
|
|
|
7218
7298
|
const single = entry.columns.length === 1 ? columns.find((c) => c.name === entry.columns[0]) : void 0;
|
|
7219
7299
|
if (single) {
|
|
7220
7300
|
if (single.helper === "id") {
|
|
7221
|
-
throw new SchemaTsError(`line ${entry.line}: the primary key "${single.name}" on table "${rawName}" is already unique
|
|
7301
|
+
throw new SchemaTsError(`line ${entry.line}: the primary key "${single.name}" on table "${rawName}" is already unique, so a one-column "unique" group on it is not accepted.`);
|
|
7222
7302
|
}
|
|
7223
7303
|
single.unique = true;
|
|
7224
7304
|
continue;
|
|
@@ -7253,7 +7333,7 @@ function readTable(r, rawName, nameLine) {
|
|
|
7253
7333
|
}
|
|
7254
7334
|
function extractSchemaTs(source, options = {}) {
|
|
7255
7335
|
if (typeof source !== "string" || source.trim().length === 0) {
|
|
7256
|
-
return { ok: false, errors: ["db/schema.ts is empty.
|
|
7336
|
+
return { ok: false, errors: ["db/schema.ts is empty. It needs at least one table; without the file, the database is unmanaged."] };
|
|
7257
7337
|
}
|
|
7258
7338
|
if (source.length > MAX_SOURCE_BYTES) {
|
|
7259
7339
|
return { ok: false, errors: [`db/schema.ts is larger than ${Math.floor(MAX_SOURCE_BYTES / 1024)}KB \u2014 that is not a schema declaration.`] };
|
|
@@ -7312,7 +7392,7 @@ function extractSchemaTs(source, options = {}) {
|
|
|
7312
7392
|
);
|
|
7313
7393
|
}
|
|
7314
7394
|
if (tables.length === 0 && removedTables.length === 0 && exportedTables.length === 0 && !managedSearch && !fileCollections?.length) {
|
|
7315
|
-
return { ok: false, errors: ["db/schema.ts declares no tables.
|
|
7395
|
+
return { ok: false, errors: ["db/schema.ts declares no tables. It needs at least one table; without the file, the database is unmanaged."] };
|
|
7316
7396
|
}
|
|
7317
7397
|
if (tables.length > MAX_TABLES) {
|
|
7318
7398
|
return { ok: false, errors: [`db/schema.ts declares ${tables.length} tables (max ${MAX_TABLES}).`] };
|
|
@@ -7324,7 +7404,7 @@ function extractSchemaTs(source, options = {}) {
|
|
|
7324
7404
|
if (c.references && !names.has(c.references)) {
|
|
7325
7405
|
const marked = removedTables.includes(c.references) || exportedTables.includes(c.references);
|
|
7326
7406
|
errors.push(
|
|
7327
|
-
marked ? `Column "${t.name}"."${c.name}" references "${c.references}", which this file marks ${removedTables.includes(c.references) ? "removedTable()" : "exported()"}.
|
|
7407
|
+
marked ? `Column "${t.name}"."${c.name}" references "${c.references}", which this file marks ${removedTables.includes(c.references) ? "removedTable()" : "exported()"}. The table can go only once no column references it (the column marked removed(), or without "references").` : `Column "${t.name}"."${c.name}" references "${c.references}", which is not declared in db/schema.ts. Foreign keys can only target tables in the same declaration.`
|
|
7328
7408
|
);
|
|
7329
7409
|
}
|
|
7330
7410
|
}
|
|
@@ -7336,7 +7416,7 @@ function extractSchemaTs(source, options = {}) {
|
|
|
7336
7416
|
if (!key) continue;
|
|
7337
7417
|
const fits = key.uuid ? c.helper === "text" : c.helper === "integer" || c.helper === "number";
|
|
7338
7418
|
if (fits) continue;
|
|
7339
|
-
errors.push(key.uuid ? `Column "${t.name}"."${c.name}" references "${c.references}", whose id() is a random text key, but is declared ${c.helper}().
|
|
7419
|
+
errors.push(key.uuid ? `Column "${t.name}"."${c.name}" references "${c.references}", whose id() is a random text key, but is declared ${c.helper}(). The column type must match the key: text({ references: '${c.references}' }), or an integer key on "${c.references}" with id({ uuid: false }).` : `Column "${t.name}"."${c.name}" references "${c.references}", whose key is an integer (id({ uuid: false })), but is declared ${c.helper}(). The column type must match the key: integer({ references: '${c.references}' }), or a random text key on "${c.references}" with id().`);
|
|
7340
7420
|
}
|
|
7341
7421
|
}
|
|
7342
7422
|
const tableByName = new Map(tables.map((t) => [t.name, t]));
|
|
@@ -7387,7 +7467,7 @@ function extractSchemaTs(source, options = {}) {
|
|
|
7387
7467
|
}
|
|
7388
7468
|
const membershipCols = knownColumns(membershipTable);
|
|
7389
7469
|
if (membershipTable.client && (membershipTable.client.create !== null || membershipTable.client.update !== null || membershipTable.client.delete || membershipTable.client.intake !== void 0)) {
|
|
7390
|
-
errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes
|
|
7470
|
+
errors.push(`Table "${sc.membership}" grants membership access to "${t.name}" and cannot allow client writes; membership changes go through a server function.`);
|
|
7391
7471
|
}
|
|
7392
7472
|
if (!membershipCols.has(sc.memberUser)) {
|
|
7393
7473
|
errors.push(
|