@aotter/mantle 0.2.0-alpha.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/auth/adapter.js +1 -28
- package/dist/auth/admin.js +2 -5
- package/dist/auth/caller.js +0 -27
- package/dist/auth/consent.js +100 -0
- package/dist/auth/hardening.js +0 -7
- package/dist/auth/identity.js +0 -11
- package/dist/auth/oauth.js +0 -32
- package/dist/auth/resource-seeds.js +0 -2
- package/dist/auth/route-http.js +33 -0
- package/dist/auth/routes.js +5 -151
- package/dist/build/compiler.d.ts +19 -609
- package/dist/build/compiler.js +3 -6896
- package/dist/build/spec/domain/model/SqlIr.d.ts +2 -0
- package/dist/build/spec/domain/model/UserFields.d.ts +1 -0
- package/dist/bun/assets.js +0 -2
- package/dist/domain/adminOrder.js +0 -2
- package/dist/domain/columns.js +7 -20
- package/dist/domain/describe/chunk-SGMRVDGE.js +1 -0
- package/dist/domain/describe/index.js +1 -115
- package/dist/domain/describe/target-hash.js +1 -12
- package/dist/domain/entry.js +0 -14
- package/dist/domain/generated-tools.js +0 -1
- package/dist/domain/input.js +0 -2
- package/dist/domain/paging.js +0 -17
- package/dist/domain/recipes.js +10 -7
- package/dist/domain/userReferences.js +0 -1
- package/dist/domain/wireScalars.js +0 -6
- package/dist/infrastructure/adminEntries.js +10 -7
- package/dist/infrastructure/adminPaging.js +55 -57
- package/dist/infrastructure/artifacts.js +0 -4
- package/dist/infrastructure/authSchema.js +0 -9
- package/dist/infrastructure/bundle.js +2 -30
- package/dist/infrastructure/cli.js +25 -11
- package/dist/infrastructure/generate.js +9 -16
- package/dist/infrastructure/generateAuth.js +1 -9
- package/dist/infrastructure/help.js +55 -0
- package/dist/infrastructure/init.js +50 -0
- package/dist/infrastructure/inputs.js +0 -9
- package/dist/infrastructure/mounts.js +22 -0
- package/dist/infrastructure/wrangler.js +0 -6
- package/dist/kernel/authorization.js +0 -1
- package/dist/kernel/binds.d.ts +16 -4
- package/dist/kernel/binds.js +4 -10
- package/dist/kernel/caller-identity.d.ts +0 -3
- package/dist/kernel/caller-identity.js +0 -15
- package/dist/kernel/caller-mode.d.ts +4 -0
- package/dist/kernel/caller-mode.js +14 -0
- package/dist/kernel/contracts.d.ts +6 -10
- package/dist/kernel/contracts.js +5 -41
- package/dist/kernel/cron.js +0 -4
- package/dist/kernel/dialect/common.js +0 -3
- package/dist/kernel/dialect/postgres.js +0 -2
- package/dist/kernel/dialect/sqlite.js +1 -6
- package/dist/kernel/drain.js +2 -8
- package/dist/kernel/driver/bun-sqlite.js +0 -4
- package/dist/kernel/driver/d1.js +0 -1
- package/dist/kernel/driver/postgres.js +2 -17
- package/dist/kernel/driver/transaction-context.js +0 -1
- package/dist/kernel/email.js +0 -4
- package/dist/kernel/host-access.js +2 -3
- package/dist/kernel/host-caller.d.ts +1 -1
- package/dist/kernel/host-caller.js +3 -6
- package/dist/kernel/json-body.js +0 -2
- package/dist/kernel/mcp-apps.js +0 -5
- package/dist/kernel/mcp-call.js +0 -1
- package/dist/kernel/mcp-handler.js +2 -26
- package/dist/kernel/mcp.js +0 -5
- package/dist/kernel/media.js +0 -18
- package/dist/kernel/paging.js +1 -9
- package/dist/kernel/publishing.js +3 -4
- package/dist/kernel/r2-media.js +0 -7
- package/dist/kernel/rest-dispatch.js +0 -1
- package/dist/kernel/rest-router.js +2 -3
- package/dist/kernel/rest.js +0 -3
- package/dist/kernel/scalars.js +1 -9
- package/dist/kernel/search.js +0 -1
- package/dist/kernel/site.js +0 -2
- package/dist/kernel/store.d.ts +11 -1
- package/dist/kernel/store.js +21 -34
- package/dist/kernel/surface-failure.d.ts +10 -0
- package/dist/kernel/surface-failure.js +29 -0
- package/dist/kernel/surface.js +0 -3
- package/dist/kernel/sweep.js +0 -1
- package/dist/kernel/translations.js +0 -1
- package/dist/kernel/user-caller.js +2 -8
- package/dist/kernel/user-fields.d.ts +3 -2
- package/dist/kernel/user-fields.js +9 -9
- package/dist/kernel/validation.js +0 -8
- package/dist/kernel/web-gateway.js +0 -2
- package/dist/kernel/web-head.js +0 -1
- package/dist/kernel/web-index.js +0 -1
- package/dist/kernel/web-response.js +0 -1
- package/dist/kernel/web-serve.js +97 -0
- package/dist/kernel/web.d.ts +1 -1
- package/dist/kernel/web.js +10 -91
- package/dist/kernel/where.js +0 -6
- package/dist/usecase/adminBindings.js +30 -21
- package/dist/usecase/adminClient.js +0 -21
- package/dist/usecase/adminDescriptors.js +2 -9
- package/dist/usecase/generate.js +7 -12
- package/dist/usecase/handlers.js +0 -3
- package/dist/usecase/mcp-media.js +0 -1
- package/dist/usecase/mcp.js +0 -4
- package/dist/usecase/paging.js +1 -3
- package/dist/usecase/publishing.js +0 -4
- package/dist/usecase/rest.js +0 -1
- package/dist/usecase/site.js +0 -1
- package/dist/usecase/stores.js +4 -14
- package/dist/usecase/tool-version.js +0 -3
- package/dist/usecase/translations.js +0 -2
- package/dist/usecase/validation.js +0 -20
- package/docs/examples/README.md +13 -5
- package/docs/examples/app-shell/README.md +38 -7
- package/docs/examples/app-shell/accept.mjs +91 -0
- package/docs/examples/app-shell/gitignore +7 -0
- package/docs/examples/app-shell/package.json +7 -6
- package/docs/examples/app-shell/smoke-kit.mjs +29 -15
- package/docs/examples/app-shell/src/routes.ts +6 -3
- package/docs/examples/app-shell/src/service.ts +2 -1
- package/docs/examples/club-events/README.md +7 -2
- package/docs/examples/club-events/manifests/club-events.yaml +15 -0
- package/docs/examples/club-events/smoke.mjs +7 -1
- package/docs/examples/purchasing/README.md +12 -3
- package/docs/examples/purchasing/manifests/purchasing.yaml +21 -4
- package/docs/examples/purchasing/smoke.mjs +14 -0
- package/docs/examples/recipes/README.md +5 -3
- package/docs/handbook/cloudflare/service-entry.md +3 -0
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/agent-setup.md +15 -10
- package/docs/handbook/guides/agent-workflow-acceptance.md +113 -103
- package/docs/handbook/guides/local-testing.md +40 -0
- package/docs/handbook/guides/native-admin.md +25 -6
- package/docs/handbook/guides/native-execution.md +5 -4
- package/docs/handbook/guides/public-pages.md +44 -4
- package/docs/handbook/guides/sql-patterns.md +266 -0
- package/docs/handbook/guides/typed-queries.md +138 -0
- package/docs/handbook/navigation.json +16 -0
- package/docs/handbook/reference/admin-descriptors.md +4 -1
- package/docs/handbook/reference/authorization.md +1 -1
- package/docs/handbook/reference/diagnostics.md +6 -1
- package/docs/handbook/reference/procedure.md +5 -5
- package/docs/handbook/reference/schema.md +10 -4
- package/docs/handbook/reference/sql.md +64 -0
- package/docs/handbook/reference/surface.md +5 -2
- package/docs/handbook/reference/view.md +11 -3
- package/docs/handbook/releases/index.md +27 -6
- package/docs/handbook/start/agents.md +45 -0
- package/docs/handbook/start/project-and-cli.md +6 -0
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/develop/SKILL.md +6 -5
- package/docs/skills/update/SKILL.md +2 -0
- package/package.json +23 -3
- package/skills/README.md +1 -1
- package/skills/mantle/SKILL.md +39 -120
- package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
- package/skills/mantle/references/local-testing.md +0 -20
- package/skills/mantle/references/web.md +0 -41
- package/skills/mantle/references/website.md +0 -51
|
@@ -101,6 +101,8 @@ export declare function storageColumns(s: {
|
|
|
101
101
|
readonly scope?: string;
|
|
102
102
|
readonly publishing?: boolean;
|
|
103
103
|
}): Map<string, string>;
|
|
104
|
+
/** The aggregate functions either dialect's SQL may call: outside a window and without GROUP BY, one makes a select return one row even over none. */
|
|
105
|
+
export declare const AGGREGATES: ReadonlySet<string>;
|
|
104
106
|
/** The native columns storage declares NOT NULL (`status` only on a publishing Schema, the only one that has it); `author_id` is nullable. */
|
|
105
107
|
export declare const NATIVE_NOT_NULL: ReadonlySet<string>;
|
|
106
108
|
/** Each native field a manifest names (in an index, say) -> its storage column, which SQL names. */
|
package/dist/bun/assets.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
/** Bun owns files; no SQLite, runtime or compiler import enters this optional host reader. */
|
|
2
1
|
import { realpath, stat } from "node:fs/promises";
|
|
3
2
|
import { resolve, sep } from "node:path";
|
|
4
|
-
/** Native file response, or null for missing or invalid paths. */
|
|
5
3
|
export function bunAdminAssets(root) {
|
|
6
4
|
const base = resolve(root);
|
|
7
5
|
return async (path) => {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Selected Admin orders mirror the existing UI's indexed, required fields and native controls. */
|
|
2
1
|
import { adminIndexColumns, NATIVE_NOT_NULL, notNullColumns } from "../build/compiler.js";
|
|
3
2
|
import { dialect as sqlite } from "../kernel/dialect/sqlite.js";
|
|
4
3
|
import { dialect as postgres } from "../kernel/dialect/postgres.js";
|
|
@@ -12,7 +11,6 @@ export function adminOrder(schema) {
|
|
|
12
11
|
return kind ? [[f, [kind, !NATIVE_NOT_NULL.has(f) && !notNull.has(f)]]] : [];
|
|
13
12
|
}));
|
|
14
13
|
}
|
|
15
|
-
/** Deployment-only indexes; no request attempts to converge storage. */
|
|
16
14
|
export function adminIndexes(schemas, isSqlite) {
|
|
17
15
|
const d = isSqlite ? sqlite : postgres;
|
|
18
16
|
return Object.values(schemas).flatMap(s => ["created_at", "updated_at", ...(s.publishing ? ["status"] : [])].flatMap(field => [[], ...(s.scope ? [[s.scope]] : [])].flatMap(prefix => {
|
package/dist/domain/columns.js
CHANGED
|
@@ -1,55 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
import { viewOutputs } from "../build/compiler.js";
|
|
3
|
-
/** A column's declared type, including the system version and timestamps. @internal */
|
|
4
|
-
/** A JSON Schema `readOnly` property is server-maintained (counters, totals): Admin's generic writes never set it. */
|
|
1
|
+
import { jsonOutputs, viewOutputs } from "../build/compiler.js";
|
|
5
2
|
export const serverOwned = (schema, column) => Object.entries(schema.properties ?? {}).some(([name, property]) => name.toLowerCase() === column && property?.readOnly === true);
|
|
6
3
|
export const declaredType = (fields, column) => fields[column] ?? (column === "version" ? "integer" : column === "created_at" || column === "updated_at" || column === "published_at" ? "timestamptz" : undefined);
|
|
7
|
-
|
|
4
|
+
const computedJson = (plan, view, key) => jsonOutputs(plan.views[view]).has(key);
|
|
8
5
|
export function outputType(plan, view, key) {
|
|
9
6
|
const columns = plan.views[view]?.columns;
|
|
10
7
|
const field = columns && Object.hasOwn(columns, key) ? columns[key] : undefined;
|
|
11
8
|
const schema = field && plan.schemas[field.schema];
|
|
12
|
-
return schema
|
|
9
|
+
return schema ? declaredType(schema.fields, field.field) : computedJson(plan, view, key) ? "json" : undefined;
|
|
13
10
|
}
|
|
14
|
-
/** Transport lineage excludes expressions/aggregates; existing temporal SQL formatting is retained. */
|
|
15
11
|
export function boundaryType(plan, view, key) {
|
|
16
12
|
const field = viewOutputs(plan.views[view], plan.schemas, new Map(), false, false).columns[key];
|
|
17
13
|
const type = outputType(plan, view, key);
|
|
18
|
-
return field || type === "date" || type === "timestamptz" ? type : undefined;
|
|
14
|
+
return field || type === "date" || type === "timestamptz" || computedJson(plan, view, key) ? type : undefined;
|
|
19
15
|
}
|
|
20
|
-
/** Declared output lineage only; computed columns retain their native values. */
|
|
21
16
|
export function outputConversions(plan, view, target) {
|
|
22
17
|
const postgres = target.endsWith("postgres");
|
|
23
18
|
const columns = viewOutputs(plan.views[view], plan.schemas, new Map(), false, false).columns;
|
|
24
|
-
return Object.keys(columns).flatMap(key => {
|
|
19
|
+
return [...Object.keys(columns), ...jsonOutputs(plan.views[view])].flatMap(key => {
|
|
25
20
|
const type = outputType(plan, view, key);
|
|
26
21
|
return conversion(key, type, !postgres);
|
|
27
22
|
});
|
|
28
23
|
}
|
|
29
|
-
/** Shared static conversion recipe for Views and Admin collections. */
|
|
30
24
|
export function conversion(key, type, sqlite) {
|
|
31
25
|
return type && ["integer", "int8", "int4"].includes(type) ? [[key, "int"]]
|
|
32
26
|
: sqlite && (type === "json" || type === "bool") ? [[key, type]] : [];
|
|
33
27
|
}
|
|
34
|
-
|
|
35
|
-
export function wireType(type) {
|
|
28
|
+
export function wireType(type, notNull = false) {
|
|
36
29
|
const value = !type ? "unknown" : ["integer", "int8", "int4"].includes(type) ? "number | string"
|
|
37
30
|
: scalarType(type);
|
|
38
|
-
return value === "unknown" ? value : `${value} | null`;
|
|
31
|
+
return value === "unknown" || notNull ? value : `${value} | null`;
|
|
39
32
|
}
|
|
40
33
|
const scalarType = (type) => type === "bool" ? "boolean" : ["integer", "int4", "int8", "real", "float8"].includes(type) ? "number" : type === "json" ? "unknown" : type === "geo" ? "{lat:number;lng:number}" : "string";
|
|
41
|
-
/**
|
|
42
|
-
* The driver-native value a column of this type comes back as, `| null` unless `notNull`; declared timestamps and dates arrive as the
|
|
43
|
-
* SQL's wire-form text. A column with no declared type (a computed output) is `unknown`, which already includes null. @internal
|
|
44
|
-
*/
|
|
45
34
|
export function nativeType(type, sqlite, notNull = false) {
|
|
46
35
|
const native = !type ? "unknown" : ["date", "timestamptz"].includes(type) ? "string" : sqlite
|
|
47
36
|
? type === "bool" ? "number" : type === "json" ? "string" : scalarType(type)
|
|
48
37
|
: ["integer", "int8"].includes(type) ? "number | string | bigint" : scalarType(type);
|
|
49
38
|
return notNull || native === "unknown" ? native : `${native} | null`;
|
|
50
39
|
}
|
|
51
|
-
/** The value a caller of the typed Store passes for a column of this type. JSON is an object or array (the Store tells it by its JS type, so a bare string would be bound as text). @internal */
|
|
52
40
|
export const inputType = (type) => type === "timestamptz" ? "string | Date" : type === "json" ? "object" : scalarType(type);
|
|
53
|
-
/** Wrap only a selected transport page. */
|
|
54
41
|
export const boundaryPage = (page, fields) => fields.length
|
|
55
42
|
? `(db, caller, input, paging) => ${page}(db, caller, input, paging).then(page => ({ ...page, rows: wireRows(page.rows, ${JSON.stringify(fields)}) }))` : page;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function resolveLocalizedText(value,preferred,canonical2){if(value==null)return null;if(typeof value=="string")return value;if(Object.prototype.hasOwnProperty.call(value,preferred))return value[preferred];if(canonical2&&Object.prototype.hasOwnProperty.call(value,canonical2))return value[canonical2];let firstKey=Object.keys(value)[0];return firstKey!==void 0?value[firstKey]:null}var INPUT_RULE_KEYWORDS=["dependentRequired","anyOf","if","then","else"];function lookupLocalRef(root,ref){if(typeof ref!="string"||!ref.startsWith("#/"))return;let value=root;try{for(let key of decodeURIComponent(ref.slice(2)).split("/").map(key2=>key2.replace(/~1/g,"/").replace(/~0/g,"~")))value=value&&typeof value=="object"&&Object.hasOwn(value,key)?value[key]:void 0}catch{return}return value&&typeof value=="object"?value:void 0}function rootRefChain(input){let chain=[];for(let schema=input;schema!==void 0;schema=lookupLocalRef(input,schema.$ref)){if(chain.includes(schema))return;if(chain.push(schema),schema.$ref===void 0)return chain}}function mcpToolNameSegment(name){return name.toLowerCase().replace(/-/g,"_")}function toolTitle(name){let words=name.replace(/([a-z0-9])([A-Z])/g,"$1 $2").replace(/[-_.\s]+/g," ").trim().toLowerCase();return words.charAt(0).toUpperCase()+words.slice(1)}var viewAnnotations={readOnlyHint:!0,destructiveHint:!1};function procedureAnnotations(p){let readOnlyHint=p.mcp?.readOnlyHint??!1;return{readOnlyHint,destructiveHint:!readOnlyHint&&("ref"in p.handler||overwrites(p.handler.sql.stmts)),...p.mcp}}var overwrites=node=>!!node&&typeof node=="object"&&("DeleteStmt"in node||"UpdateStmt"in node||node.onConflictClause?.action==="ONCONFLICT_UPDATE"||Object.values(node).some(overwrites));function mcpTools(plan,surface,locale="en"){let text=t=>resolveLocalizedText(t,locale,"en")??void 0,tools=new Map,add=tool=>{let prior=tools.get(tool.name);if(prior&&(prior.kind!==tool.kind||prior.source!==tool.source))throw new TypeError(`MCP tool '${tool.name}' comes from both '${prior.source}' and '${tool.source}'.`);tools.set(tool.name,tool)};for(let t of Object.values(plan.triggers)){if(t.source.kind!=="mcp"||t.source.surface!==surface)continue;let p=plan.procedures[t.procedure],title=text(p.title);add({name:mcpToolNameSegment(t.procedure),kind:"procedure",source:t.procedure,title:title??toolTitle(t.procedure),description:text(p.description)??title??t.procedure,inputSchema:p.input,...p.output.type==="object"?{outputSchema:p.output}:{},annotations:procedureAnnotations(p),requires:p.requires})}for(let[name,v]of Object.entries(plan.views)){if(v.surface!==surface)continue;let chain=(v.input?rootRefChain(v.input)??[v.input]:[]).reverse(),props=Object.assign({},...chain.map(s=>s.properties??{})),required=[...new Set(chain.flatMap(s=>s.required??[]))];if("limit"in props||"cursor"in props)throw new TypeError(`View '${name}' declares an input named limit or cursor; the MCP tool reserves both for paging.`);let rules=chain.map(s=>Object.fromEntries(INPUT_RULE_KEYWORDS.flatMap(k=>s[k]===void 0?[]:[[k,s[k]]]))).filter(r=>Object.keys(r).length),defs=v.input?referencedDefs(v.input,props):{},title=text(v.title);add({name:mcpToolNameSegment(name),kind:"view",source:name,title:title??toolTitle(name),description:text(v.description)??title??name,inputSchema:{type:"object",properties:{...props,limit:{type:"integer",minimum:1,maximum:500},cursor:{type:"string"}},...required.length?{required}:{},...rules.length>1?{allOf:rules}:rules[0],...Object.keys(defs).length?{$defs:defs}:{}},annotations:viewAnnotations,requires:v.requires})}return[...tools.values()]}function referencedDefs(root,node,found={}){if(!node||typeof node!="object")return found;let ref=node.$ref,[head,segment]=typeof ref=="string"&&ref.startsWith("#/")?decoded(ref.slice(2)).split("/"):[],key=head==="$defs"?segment?.replace(/~1/g,"/").replace(/~0/g,"~"):void 0;key!==void 0&&!Object.hasOwn(found,key)&&root.$defs&&Object.hasOwn(root.$defs,key)&&(found[key]=root.$defs[key],referencedDefs(root,found[key],found));for(let child of Object.values(node))referencedDefs(root,child,found);return found}var decoded=pointer=>{try{return decodeURIComponent(pointer)}catch{return""}};function canonical(v){return Array.isArray(v)?`[${v.map(canonical).join(",")}]`:v&&typeof v=="object"?`{${Object.keys(v).sort().filter(k=>v[k]!==void 0).map(k=>`${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`:JSON.stringify(v)??"null"}var publishingTransitions=[["publish",["draft"],"published"],["unpublish",["published","archived"],"draft"],["archive",["draft","published"],"archived"]];var publishingInput={type:"object",properties:{id:{type:"string",minLength:1},expectedVersion:{type:"integer",minimum:1}},required:["id","expectedVersion"],additionalProperties:!1};function mediaDefinitions(){let define=(name,description,inputSchema,action,readOnlyHint=!1)=>({name,description,inputSchema,action,annotations:{readOnlyHint,destructiveHint:action==="delete",openWorldHint:!0}}),string={type:"string"},texts={alt:{type:"string",maxLength:1e3},caption:{type:"string",maxLength:1e3}},input=(properties,required=[])=>({type:"object",properties,required,additionalProperties:!1});return[define("get_media_upload_policy","Read declared image purpose MIME slots and byte caps before preparing variants or requesting an upload. Optimization runs locally, never in the host.",input({}),"policy",!0),define("create_media_upload","Create short-lived PUT capabilities for image variants. Read site media purpose policy first; optimize images locally. Obtain attachment bytes yourself, then PUT each variant with its requiredHeaders and call commit_media_upload. Never store upload URLs as published images or ask the user to run terminal commands.",input({filename:{type:"string",maxLength:255},purpose:string,variants:{type:"array",minItems:1,items:input({mimeType:string,byteSize:{type:"integer",minimum:1},role:{type:"string",enum:["primary","alternate","fallback"]}},["mimeType","byteSize","role"])},...texts},["filename","purpose","variants"]),"create"),define("commit_media_upload","Verify all uploaded variants and commit one permanent media asset. Store the returned asset id in fields referencing media_assets.id, never a temporary upload URL. Unknown or expired upload groups require a new create_media_upload.",input({uploadGroupId:string,...texts},["uploadGroupId"]),"commit"),define("list_media_assets","List committed image assets, newest first, with their permanent variants and alt/caption. Search matches id, alt or caption; reuse only a returned cursor.",input({limit:{type:"integer",minimum:1,maximum:500},cursor:string,search:string}),"list",!0),define("get_media_asset","Read one committed media asset and its permanent variant URLs.",input({id:string},["id"]),"get",!0),define("update_media_asset","Update committed asset alt or caption; omitted fields remain unchanged and empty strings clear text.",input({id:string,...texts},["id"]),"update"),define("delete_media_asset","Delete an asset's objects and library record. Check application references first. A partial failure can be retried using the same id.",input({id:string},["id"]),"delete")]}function inspectSql(value){let schemas=new Set,readSchemas=new Set,writes=[],readOnly=!0,walk=v=>{if(!v||typeof v!="object")return;let n=v;n.RangeVar?.mantle==="table"&&(schemas.add(n.RangeVar.relname.toLowerCase()),readSchemas.add(n.RangeVar.relname.toLowerCase()));for(let kind of["InsertStmt","UpdateStmt","DeleteStmt"])n[kind]&&(readOnly=!1,n[kind].relation?.relname&&writes.push({schema:n[kind].relation.relname.toLowerCase(),operation:kind==="InsertStmt"?"create":kind==="UpdateStmt"?"update":"delete"}),n[kind].relation?.relname&&(schemas.add(n[kind].relation.relname.toLowerCase()),kind!=="InsertStmt"&&readSchemas.add(n[kind].relation.relname.toLowerCase())));Object.values(n).forEach(walk)};return walk(value),{schemas:[...schemas].sort(),readSchemas:[...readSchemas].sort(),readOnly,writes}}function mergeRestrict(policies,conjunctive){return Object.fromEntries([...new Set(policies.flatMap(p=>Object.keys(p)))].sort().map(schema=>{let rows=policies.map(p=>p[schema]??{terms:[]}),terms=[...new Map(rows.flatMap(p=>p.terms).map(t=>[canonical(t),t])).values()].filter(t=>conjunctive||rows.every(p=>p.terms.some(v=>canonical(v)===canonical(t)))),owner=rows.find(p=>p.owner)?.owner;return[schema,{...owner&&(conjunctive||rows.every(p=>p.owner===owner))?{owner}:{},terms}]}))}function inspectBehavior(plan,ir,opaque,requires,restrict,writes){let inspected=inspectSql(ir);writes&&(inspected.writes=[...writes],inspected.schemas.push(...writes.map(w=>w.schema)),inspected.readSchemas.push(...writes.filter(w=>w.operation!=="create").map(w=>w.schema)));let dependencies=[],policies=[restrict??{}],procedures=new Set,hooks=new Set,visit=name=>{if(procedures.has(name))return;procedures.add(name);let p=plan.procedures[name];if(!p)throw new TypeError(`Unknown dependency '${name}'.`);dependencies.push({procedure:name,declaration:p}),policies.push(p.restrict??{}),opaque||="ref"in p.handler;let g=inspectSql("sql"in p.handler?p.handler.sql.stmts:void 0);inspected.schemas.push(...g.schemas),inspected.readSchemas.push(...g.readSchemas),inspected.writes.push(...g.writes),inspected.readOnly&&=g.readOnly,p.requires?.guard&&visit(p.requires.guard.procedure)};for(requires?.guard&&visit(requires.guard.procedure);;){let before=hooks.size;for(let[id,t]of Object.entries(plan.triggers).sort(([a],[b])=>a<b?-1:a>b?1:0)){if(t.source.kind!=="lifecycle"||hooks.has(id))continue;let source=t.source;!opaque&&!inspected.writes.some(w=>w.schema===source.schema.toLowerCase()&&source.on.some(on=>on.endsWith(`_${w.operation}`)))||(hooks.add(id),dependencies.push({trigger:id,source}),visit(t.procedure))}if(hooks.size===before)break}let related=names=>{let all=new Set(opaque?Object.keys(plan.schemas):names);for(let name of all){let parent=plan.schemas[name]?.translates?.parent.toLowerCase();parent&&all.add(parent)}return[...all].sort()};return{...inspected,schemas:related(inspected.schemas),readSchemas:related(inspected.readSchemas),opaque,restrict:mergeRestrict(policies,!0),dependencies}}var objectInput={type:"object",properties:{}},pathTemplate=path=>path.replace(/\{([^}]+)\}/g,":$1"),adminRoutes=["GET /me","GET /site","GET /collections","GET /views-manifest","GET /operations","GET /webmcp","POST /webmcp/:tool","GET /bootstrap","GET /views/:name/export","GET /views/:name","GET /view-batch","GET /developer/views/:name","POST /operations/:name","GET /developer-console","GET /collections/:name/statistics","GET /entries","GET /entries/export","GET /entries/:id","POST /entries","PATCH /entries/:id","POST /entries/:id/publish","POST /entries/:id/unpublish","POST /entries/:id/archive","DELETE /entries/:id","GET /site-settings","PATCH /site-settings","POST /media/uploads","POST /media/uploads/:groupId/commit","GET /media","GET /media/:id","PATCH /media/:id","DELETE /media/:id","GET /admin","GET /members","PATCH /admin/:id/role","POST /admin/invitations","DELETE /admin/invitations/:id"];function targetDefinitions(plan){let out=[],tools=[...mcpTools(plan,"public"),...mcpTools(plan,"admin")];for(let[kind,entries]of[["view",plan.views],["procedure",plan.procedures]])for(let[name,p]of Object.entries(entries).sort(([a],[b])=>a<b?-1:a>b?1:0)){let view=kind==="view"?plan.views[name]:void 0,proc=kind==="procedure"?plan.procedures[name]:void 0,ir=view?.stmts??(proc&&"sql"in proc.handler?proc.handler.sql.stmts:void 0),inspected=inspectBehavior(plan,ir,!!proc&&"ref"in proc.handler,p.requires,p.restrict),tool=tools.find(t=>t.kind===kind&&t.source===name),title=resolveLocalizedText(p.title,"en","en")??name,triggers=Object.entries(plan.triggers).filter(([,t])=>kind==="procedure"&&t.procedure===name).sort(([a],[b])=>a<b?-1:a>b?1:0).map(([id,t])=>({id,source:t.source})),surfaces=[...new Set([...view?[view.surface,"admin",...view.surface==="public"?["rest"]:[]]:[],...triggers.map(t=>t.source.kind==="mcp"?t.source.surface:t.source.kind==="http"?"rest":t.source.kind)])].sort();surfaces.length||surfaces.push("internal"),out.push({id:`${kind}:${name}`,name:tool?.name??mcpToolNameSegment(name),kind,surface:view?.surface??(surfaces.includes("public")||surfaces.includes("rest")?"public":surfaces.includes("admin")?"admin":"internal"),surfaces,triggers,title,description:resolveLocalizedText(p.description,"en","en")??title,inputSchema:tool?.inputSchema??view?.input??proc?.input??objectInput,annotations:tool?.annotations??{},requires:p.requires,restrict:inspected.restrict,schemas:inspected.schemas,readSchemas:inspected.readSchemas,readOnly:!inspected.opaque&&inspected.readOnly,opaque:inspected.opaque,trustedOnly:!!proc?.trustedOnly,variants:[],behavior:{declaration:p,dependencies:inspected.dependencies}})}let addRoute=(id,dependencies,inputSchema=objectInput)=>{let admin=id.startsWith("admin:");out.push({id,name:id.replace(/[^a-zA-Z0-9_-]/g,"_"),title:id,description:id,kind:admin?"admin":"rest",surface:admin?"admin":"public",surfaces:[admin?"admin":"rest"],triggers:dependencies.flatMap(d=>d.triggers),inputSchema,annotations:{},requires:admin?{auth:{all:["ctx.admin"]}}:dependencies[0]?.requires,restrict:mergeRestrict(dependencies.map(d=>d.restrict),!1),schemas:[...new Set(dependencies.flatMap(d=>d.schemas))].sort(),readSchemas:[...new Set(dependencies.flatMap(d=>d.readSchemas))].sort(),readOnly:dependencies.length>0&&dependencies.every(d=>d.readOnly)&&(!admin||id.startsWith("admin:GET ")||id==="admin:POST /operations/:name"||id==="admin:POST /webmcp/:tool"),opaque:dependencies.some(d=>d.opaque),trustedOnly:dependencies.some(d=>d.trustedOnly),variants:dependencies,behavior:dependencies.map(d=>({id:d.id,behavior:d.behavior,surfaces:d.surfaces,triggers:d.triggers,input:d.inputSchema,requires:d.requires,restrict:d.restrict,trustedOnly:d.trustedOnly}))})},declarations=[...out],adminOperations=new Set(mcpTools(plan,"admin").filter(t=>t.kind==="procedure").map(t=>`procedure:${t.source}`));for(let d of declarations)d.kind==="view"&&d.surfaces.includes("public")&&addRoute(`rest:GET /api/views/${d.id.slice(5)}`,[d],d.inputSchema);for(let[,t]of Object.entries(plan.triggers).sort(([a],[b])=>a<b?-1:a>b?1:0))if(t.source.kind==="http"){let d=declarations.find(d2=>d2.id===`procedure:${t.procedure}`);addRoute(`rest:${t.source.method} ${pathTemplate(t.source.path)}`,[d],d.inputSchema)}let generated=[];for(let[schema,s]of Object.entries(plan.schemas).sort(([a],[b])=>a<b?-1:a>b?1:0))if(s.publishing)for(let[verb,from,to]of publishingTransitions){addRoute(`procedure:${s.name}.${verb}`,[],publishingInput);let d=out.at(-1),inspected=inspectBehavior(plan,void 0,!1,void 0,void 0,[{schema,operation:verb==="publish"?"publish":"update"}]);Object.assign(d,{kind:"procedure",name:mcpToolNameSegment(`${verb}_${s.name}`),surface:"admin",surfaces:["admin"],requires:{auth:{all:["ctx.admin"]}},schemas:[...new Set([schema,...inspected.schemas])].sort(),readSchemas:[...new Set([schema,...inspected.readSchemas])].sort(),opaque:inspected.opaque,behavior:{schema,verb,from,to,dependencies:inspected.dependencies}}),generated.push(d)}for(let tool of mediaDefinitions()){addRoute(`admin:tool:${tool.name}`,[],tool.inputSchema);let d=out.at(-1);Object.assign(d,{name:tool.name,readOnly:tool.annotations.readOnlyHint,opaque:!tool.annotations.readOnlyHint,annotations:tool.annotations,behavior:{tool,schemas:plan.schemas}}),generated.push(d)}for(let route of adminRoutes){let deps=route.includes("/views/:")||route==="GET /view-batch"?declarations.filter(d2=>d2.kind==="view"&&(route.includes("/developer/")||plan.views[d2.id.slice(5)]?.surface==="admin")):route.includes("/operations/:")?declarations.filter(d2=>adminOperations.has(d2.id)):route==="POST /webmcp/:tool"?declarations.filter(d2=>mcpTools(plan,"admin").some(t=>`${t.kind}:${t.source}`===d2.id)).concat(generated):[],generic=route.includes("/entries")||route.includes("/statistics")||route==="GET /bootstrap";addRoute(`admin:${route}`,deps,deps.length&&route!=="GET /view-batch"?{...objectInput,anyOf:deps.map(d2=>({type:"object",properties:{[route.includes("/webmcp/")?"tool":"name"]:{type:"string",const:route.includes("/webmcp/")?d2.name:d2.id.slice(d2.kind==="view"?5:10)}}}))}:objectInput);let d=out.at(-1);if(generic){let operation=route==="POST /entries"?"create":route==="DELETE /entries/:id"?"delete":route.startsWith("GET ")?void 0:route.endsWith("/publish")?"publish":"update",inspected=inspectBehavior(plan,void 0,!1,void 0,void 0,operation?Object.keys(plan.schemas).map(schema=>({schema,operation})):[]);d.schemas=Object.keys(plan.schemas).sort(),d.readSchemas=d.schemas,d.opaque=inspected.opaque,d.behavior={schemas:plan.schemas,dependencies:inspected.dependencies}}}return[...new Map(out.map(d=>[d.id,d])).values()].sort((a,b)=>a.id<b.id?-1:a.id>b.id?1:0)}async function hash(value){let digest=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(canonical(value)));return[...new Uint8Array(digest)].map(b=>b.toString(16).padStart(2,"0")).join("")}function hashDefinition(plan,d){return hash({id:d.id,surfaces:d.surfaces,triggers:d.triggers,behavior:d.behavior,input:d.inputSchema,requires:d.requires,restrict:d.restrict,dialect:plan.dialect,schemas:Object.fromEntries(d.schemas.map(name=>[name,plan.schemas[name]])),userFields:plan.access?.userFields,readOnly:d.readOnly,opaque:d.opaque,trustedOnly:d.trustedOnly})}async function targetHash(plan,targetId){let target=targetDefinitions(plan).find(d=>d.id===targetId);if(!target)throw new TypeError(`Unknown target '${targetId}'.`);return hashDefinition(plan,target)}export{canonical,targetDefinitions,hash,hashDefinition,targetHash};
|
|
@@ -1,115 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
canonical,
|
|
3
|
-
hashDefinition,
|
|
4
|
-
targetDefinitions,
|
|
5
|
-
targetHash
|
|
6
|
-
} from "./chunk-ZVXBGP7Y.js";
|
|
7
|
-
|
|
8
|
-
// src/domain/describe/index.ts
|
|
9
|
-
function requirements(d, surface) {
|
|
10
|
-
const all = d.requires?.auth?.all ?? [];
|
|
11
|
-
return {
|
|
12
|
-
all: surface === "admin" && !all.includes("ctx.admin") ? ["ctx.admin", ...all] : all,
|
|
13
|
-
...d.requires?.guard ? { guard: d.requires.guard.procedure } : {}
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
async function describeApp(plan) {
|
|
17
|
-
const targets = await Promise.all(targetDefinitions(plan).map(async (d) => {
|
|
18
|
-
const scope = Object.fromEntries(d.schemas.map((name) => {
|
|
19
|
-
const s = plan.schemas[name];
|
|
20
|
-
return [name, { ...s.scope ? { owner: s.scope } : {}, terms: s.userScopes ?? [] }];
|
|
21
|
-
}));
|
|
22
|
-
return { name: d.name, title: d.title, description: d.description, inputSchema: d.inputSchema, annotations: { ...d.annotations, readOnlyHint: d.readOnly }, _meta: {
|
|
23
|
-
surfaces: d.surfaces,
|
|
24
|
-
triggers: d.triggers,
|
|
25
|
-
id: d.id,
|
|
26
|
-
targetHash: await hashDefinition(plan, d),
|
|
27
|
-
readOnly: d.readOnly,
|
|
28
|
-
scope,
|
|
29
|
-
restrict: d.restrict,
|
|
30
|
-
opaque: d.opaque,
|
|
31
|
-
unscopedReads: d.readSchemas.some((name) => !plan.schemas[name]?.scope && !plan.schemas[name]?.userScopes?.length),
|
|
32
|
-
trustedOnly: d.trustedOnly,
|
|
33
|
-
variants: d.variants.map((v) => ({
|
|
34
|
-
id: v.id,
|
|
35
|
-
restrict: v.restrict,
|
|
36
|
-
requires: requirements(v, d.surface),
|
|
37
|
-
inputSchema: v.inputSchema,
|
|
38
|
-
readOnly: v.readOnly,
|
|
39
|
-
trustedOnly: v.trustedOnly,
|
|
40
|
-
opaque: v.opaque,
|
|
41
|
-
unscopedReads: v.readSchemas.some((name) => !plan.schemas[name]?.scope && !plan.schemas[name]?.userScopes?.length),
|
|
42
|
-
scope: Object.fromEntries(v.schemas.map((name) => {
|
|
43
|
-
const s = plan.schemas[name];
|
|
44
|
-
return [name, { ...s.scope ? { owner: s.scope } : {}, terms: s.userScopes ?? [] }];
|
|
45
|
-
}))
|
|
46
|
-
})),
|
|
47
|
-
requires: requirements(d, d.surface)
|
|
48
|
-
} };
|
|
49
|
-
}));
|
|
50
|
-
return JSON.parse(canonical({ schemaVersion: 1, access: plan.access?.userFields ?? {}, targets }));
|
|
51
|
-
}
|
|
52
|
-
var securityFlags = ["readOnly", "trustedOnly", "opaque", "unscopedReads"];
|
|
53
|
-
var flagDirection = (field, value) => (field === "readOnly" || field === "trustedOnly" ? !value : value) ? "widened" : "narrowed";
|
|
54
|
-
var inclusion = (before, after) => before.some((t) => !after.includes(t)) ? "widened" : after.some((t) => !before.includes(t)) ? "narrowed" : "changed";
|
|
55
|
-
var exposureDirection = (before, after) => after.some((t) => !before.includes(t) && t !== canonical("internal")) ? "widened" : before.some((t) => !after.includes(t) && t !== canonical("internal")) ? "narrowed" : "changed";
|
|
56
|
-
var combine = (directions) => directions.includes("widened") ? "widened" : directions.length && directions.every((d) => d === "narrowed") ? "narrowed" : "changed";
|
|
57
|
-
var terms = (p) => [...p.owner ? [canonical({ owner: p.owner })] : [], ...p.terms.map(canonical)];
|
|
58
|
-
function policyDirection(before, after, scope) {
|
|
59
|
-
const directions = [];
|
|
60
|
-
for (const schema of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
61
|
-
const x = before[schema], y = after[schema];
|
|
62
|
-
if (canonical(x) === canonical(y)) continue;
|
|
63
|
-
directions.push(scope && (!x || !y) ? x ? "narrowed" : "widened" : inclusion(x ? terms(x) : [], y ? terms(y) : []));
|
|
64
|
-
}
|
|
65
|
-
return combine(directions);
|
|
66
|
-
}
|
|
67
|
-
function requiresDirection(x, y) {
|
|
68
|
-
const directions = [];
|
|
69
|
-
if (canonical(x.all) !== canonical(y.all)) directions.push(inclusion(x.all.map(canonical), y.all.map(canonical)));
|
|
70
|
-
if (x.guard !== y.guard) directions.push(!y.guard ? "widened" : !x.guard ? "narrowed" : "changed");
|
|
71
|
-
return combine(directions);
|
|
72
|
-
}
|
|
73
|
-
function diffApp(a, b) {
|
|
74
|
-
if (a.schemaVersion !== 1 || b.schemaVersion !== 1) throw new TypeError("Unsupported describe schemaVersion.");
|
|
75
|
-
const changes = [], old = new Map(a.targets.map((t) => [t._meta.id, t])), next = new Map(b.targets.map((t) => [t._meta.id, t]));
|
|
76
|
-
for (const id of [.../* @__PURE__ */ new Set([...old.keys(), ...next.keys()])].sort()) {
|
|
77
|
-
const before = old.get(id), after = next.get(id);
|
|
78
|
-
if (!before || !after) {
|
|
79
|
-
changes.push({ id, field: "target", direction: before ? "narrowed" : "widened" });
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
const x = before._meta, y = after._meta, start = changes.length;
|
|
83
|
-
const record = (field, left, right, direction) => {
|
|
84
|
-
if (canonical(left) !== canonical(right)) changes.push({ id, field, direction, before: left, after: right });
|
|
85
|
-
};
|
|
86
|
-
record("scope", x.scope, y.scope, policyDirection(x.scope, y.scope, true));
|
|
87
|
-
record("restrict", x.restrict, y.restrict, policyDirection(x.restrict, y.restrict, false));
|
|
88
|
-
record("requires", x.requires, y.requires, requiresDirection(x.requires, y.requires));
|
|
89
|
-
for (const field of ["surfaces", "triggers"]) record(field, x[field], y[field], exposureDirection(x[field].map(canonical), y[field].map(canonical)));
|
|
90
|
-
for (const field of securityFlags) record(field, x[field], y[field], flagDirection(field, y[field]));
|
|
91
|
-
record("inputSchema", before.inputSchema, after.inputSchema, "changed");
|
|
92
|
-
const directions = [];
|
|
93
|
-
const variants = new Map(x.variants.map((v) => [v.id, v])), added = new Map(y.variants.map((v) => [v.id, v]));
|
|
94
|
-
for (const name of /* @__PURE__ */ new Set([...variants.keys(), ...added.keys()])) {
|
|
95
|
-
const left = variants.get(name), right = added.get(name);
|
|
96
|
-
if (canonical(left) === canonical(right)) continue;
|
|
97
|
-
if (!left || !right) directions.push(left ? "narrowed" : "widened");
|
|
98
|
-
else {
|
|
99
|
-
if (canonical(left.scope) !== canonical(right.scope)) directions.push(policyDirection(left.scope, right.scope, true));
|
|
100
|
-
if (canonical(left.restrict) !== canonical(right.restrict)) directions.push(policyDirection(left.restrict, right.restrict, false));
|
|
101
|
-
if (canonical(left.requires) !== canonical(right.requires)) directions.push(requiresDirection(left.requires, right.requires));
|
|
102
|
-
if (canonical(left.inputSchema) !== canonical(right.inputSchema)) directions.push("changed");
|
|
103
|
-
for (const field of securityFlags) if (left[field] !== right[field]) directions.push(flagDirection(field, right[field]));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
record("variants", x.variants, y.variants, combine(directions));
|
|
107
|
-
if (changes.length === start && x.targetHash !== y.targetHash) changes.push({ id, field: "behavior", direction: "changed" });
|
|
108
|
-
}
|
|
109
|
-
return changes;
|
|
110
|
-
}
|
|
111
|
-
export {
|
|
112
|
-
describeApp,
|
|
113
|
-
diffApp,
|
|
114
|
-
targetHash
|
|
115
|
-
};
|
|
1
|
+
import{canonical,hashDefinition,targetDefinitions,targetHash}from"./chunk-SGMRVDGE.js";function requirements(d,surface){let all=d.requires?.auth?.all??[];return{all:surface==="admin"&&!all.includes("ctx.admin")?["ctx.admin",...all]:all,...d.requires?.guard?{guard:d.requires.guard.procedure}:{}}}async function describeApp(plan){let targets=await Promise.all(targetDefinitions(plan).map(async d=>{let scope=Object.fromEntries(d.schemas.map(name=>{let s=plan.schemas[name];return[name,{...s.scope?{owner:s.scope}:{},terms:s.userScopes??[]}]}));return{name:d.name,title:d.title,description:d.description,inputSchema:d.inputSchema,annotations:{...d.annotations,readOnlyHint:d.readOnly},_meta:{surfaces:d.surfaces,triggers:d.triggers,id:d.id,targetHash:await hashDefinition(plan,d),readOnly:d.readOnly,scope,restrict:d.restrict,opaque:d.opaque,unscopedReads:d.readSchemas.some(name=>!plan.schemas[name]?.scope&&!plan.schemas[name]?.userScopes?.length),trustedOnly:d.trustedOnly,variants:d.variants.map(v=>({id:v.id,restrict:v.restrict,requires:requirements(v,d.surface),inputSchema:v.inputSchema,readOnly:v.readOnly,trustedOnly:v.trustedOnly,opaque:v.opaque,unscopedReads:v.readSchemas.some(name=>!plan.schemas[name]?.scope&&!plan.schemas[name]?.userScopes?.length),scope:Object.fromEntries(v.schemas.map(name=>{let s=plan.schemas[name];return[name,{...s.scope?{owner:s.scope}:{},terms:s.userScopes??[]}]}))})),requires:requirements(d,d.surface)}}}));return JSON.parse(canonical({schemaVersion:1,access:plan.access?.userFields??{},targets}))}var securityFlags=["readOnly","trustedOnly","opaque","unscopedReads"],flagDirection=(field,value)=>(field==="readOnly"||field==="trustedOnly"?!value:value)?"widened":"narrowed",inclusion=(before,after)=>before.some(t=>!after.includes(t))?"widened":after.some(t=>!before.includes(t))?"narrowed":"changed",exposureDirection=(before,after)=>after.some(t=>!before.includes(t)&&t!==canonical("internal"))?"widened":before.some(t=>!after.includes(t)&&t!==canonical("internal"))?"narrowed":"changed",combine=directions=>directions.includes("widened")?"widened":directions.length&&directions.every(d=>d==="narrowed")?"narrowed":"changed",terms=p=>[...p.owner?[canonical({owner:p.owner})]:[],...p.terms.map(canonical)];function policyDirection(before,after,scope){let directions=[];for(let schema of new Set([...Object.keys(before),...Object.keys(after)])){let x=before[schema],y=after[schema];canonical(x)!==canonical(y)&&directions.push(scope&&(!x||!y)?x?"narrowed":"widened":inclusion(x?terms(x):[],y?terms(y):[]))}return combine(directions)}function requiresDirection(x,y){let directions=[];return canonical(x.all)!==canonical(y.all)&&directions.push(inclusion(x.all.map(canonical),y.all.map(canonical))),x.guard!==y.guard&&directions.push(y.guard?x.guard?"changed":"narrowed":"widened"),combine(directions)}function diffApp(a,b){if(a.schemaVersion!==1||b.schemaVersion!==1)throw new TypeError("Unsupported describe schemaVersion.");let changes=[],old=new Map(a.targets.map(t=>[t._meta.id,t])),next=new Map(b.targets.map(t=>[t._meta.id,t]));for(let id of[...new Set([...old.keys(),...next.keys()])].sort()){let before=old.get(id),after=next.get(id);if(!before||!after){changes.push({id,field:"target",direction:before?"narrowed":"widened"});continue}let x=before._meta,y=after._meta,start=changes.length,record=(field,left,right,direction)=>{canonical(left)!==canonical(right)&&changes.push({id,field,direction,before:left,after:right})};record("scope",x.scope,y.scope,policyDirection(x.scope,y.scope,!0)),record("restrict",x.restrict,y.restrict,policyDirection(x.restrict,y.restrict,!1)),record("requires",x.requires,y.requires,requiresDirection(x.requires,y.requires));for(let field of["surfaces","triggers"])record(field,x[field],y[field],exposureDirection(x[field].map(canonical),y[field].map(canonical)));for(let field of securityFlags)record(field,x[field],y[field],flagDirection(field,y[field]));record("inputSchema",before.inputSchema,after.inputSchema,"changed");let directions=[],variants=new Map(x.variants.map(v=>[v.id,v])),added=new Map(y.variants.map(v=>[v.id,v]));for(let name of new Set([...variants.keys(),...added.keys()])){let left=variants.get(name),right=added.get(name);if(canonical(left)!==canonical(right))if(!left||!right)directions.push(left?"narrowed":"widened");else{canonical(left.scope)!==canonical(right.scope)&&directions.push(policyDirection(left.scope,right.scope,!0)),canonical(left.restrict)!==canonical(right.restrict)&&directions.push(policyDirection(left.restrict,right.restrict,!1)),canonical(left.requires)!==canonical(right.requires)&&directions.push(requiresDirection(left.requires,right.requires)),canonical(left.inputSchema)!==canonical(right.inputSchema)&&directions.push("changed");for(let field of securityFlags)left[field]!==right[field]&&directions.push(flagDirection(field,right[field]))}}record("variants",x.variants,y.variants,combine(directions)),changes.length===start&&x.targetHash!==y.targetHash&&changes.push({id,field:"behavior",direction:"changed"})}return changes}export{describeApp,diffApp,targetHash};
|
|
@@ -1,12 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
canonical,
|
|
3
|
-
hash,
|
|
4
|
-
hashDefinition,
|
|
5
|
-
targetHash
|
|
6
|
-
} from "./chunk-ZVXBGP7Y.js";
|
|
7
|
-
export {
|
|
8
|
-
canonical,
|
|
9
|
-
hash,
|
|
10
|
-
hashDefinition,
|
|
11
|
-
targetHash
|
|
12
|
-
};
|
|
1
|
+
import{canonical,hash,hashDefinition,targetHash}from"./chunk-SGMRVDGE.js";export{canonical,hash,hashDefinition,targetHash};
|
package/dist/domain/entry.js
CHANGED
|
@@ -1,29 +1,15 @@
|
|
|
1
|
-
/** Direct entry files: one View or Procedure per module, so a request bundle excludes the others. */
|
|
2
|
-
/**
|
|
3
|
-
* Stable, injective build-only export names, so a stale entry file cannot bind a new ordinal: every byte outside [A-Za-z0-9]
|
|
4
|
-
* (including `$` and `_`) is `$` and two hex digits.
|
|
5
|
-
* @internal
|
|
6
|
-
*/
|
|
7
1
|
export const exportName = (kind, name) => kind + "_" + name.replace(/[^A-Za-z0-9]+/g, run => Array.from(new TextEncoder().encode(run), b => "$" + b.toString(16).padStart(2, "0")).join(""));
|
|
8
|
-
/** A direct entry: `import` then `export`, because Bun 1.3 drops a bare `export { x as default } from` binding under "sideEffects": false. `extra` is any further re-export line. */
|
|
9
|
-
/** @internal */
|
|
10
2
|
export const entry = (kind, name, types, extra = "") => `import { ${exportName(kind, name)} as operation } from "../generated.js";\nexport default operation;\nexport type { ${types} } from "../generated.js";\n${extra}`;
|
|
11
|
-
/** Source text of a JSON value. @internal */
|
|
12
3
|
export const q = JSON.stringify;
|
|
13
|
-
/** Whether a target's database is SQLite (bun:sqlite or D1). @internal */
|
|
14
4
|
export const isSqlite = (target) => target === "bun-sqlite" || target === "cloudflare-d1";
|
|
15
|
-
/** A View's lazy paged boundary and validated query reader, bound as `page<index>` and `<query><index>`. @internal */
|
|
16
5
|
export const viewImports = (name, index, query = "query") => [`const page${index}: typeof import("./paged/views/${encodeURIComponent(name)}.js").default = async (...args) => (await import("./paged/views/${encodeURIComponent(name)}.js")).default(...args);`, `import { query as ${query}${index} } from "./validation/views/${encodeURIComponent(name)}.js";`];
|
|
17
|
-
/** A generated host handle import, shared by the optional boundaries. @internal */
|
|
18
6
|
export const driverFor = (target) => target === "bun-sqlite" ? "bun-sqlite" : target === "cloudflare-d1" ? "d1" : "postgres";
|
|
19
7
|
export const databaseImport = (target) => driverFor(target) === "postgres" ? 'import type { Postgres as Database } from "@aotter/mantle/contracts";' : `import type { Handle as Database } from "@aotter/mantle/driver/${driverFor(target)}";`;
|
|
20
|
-
/** Shared source ownership marker; only newly emitted files are wrapped. */
|
|
21
8
|
export const GENERATED = "Generated by mantle generate — do not edit.";
|
|
22
9
|
export function generatedFiles(files) {
|
|
23
10
|
return Object.fromEntries(Object.entries(files).map(([name, source]) => [name,
|
|
24
11
|
`${name.endsWith(".sql") ? "--" : "//"} ${GENERATED}\n${source}`]));
|
|
25
12
|
}
|
|
26
|
-
/** JavaScript data literals preserve own __proto__ keys instead of mutating the object's prototype. */
|
|
27
13
|
export function literal(value) {
|
|
28
14
|
if (Array.isArray(value))
|
|
29
15
|
return `[${value.map(literal).join(",")}]`;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export const publishingTransitions = [["publish", ["draft"], "published"], ["unpublish", ["published", "archived"], "draft"], ["archive", ["draft", "published"], "archived"]];
|
|
2
|
-
/** Only archive withdraws an entry from both drafts and the public. */
|
|
3
2
|
export const publishingAnnotations = (verb) => ({ readOnlyHint: false, destructiveHint: verb === "archive" });
|
|
4
3
|
export const publishingInput = { type: "object", properties: { id: { type: "string", minLength: 1 }, expectedVersion: { type: "integer", minimum: 1 } }, required: ["id", "expectedVersion"], additionalProperties: false };
|
|
5
4
|
export function mediaDefinitions() {
|
package/dist/domain/input.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { q } from "./entry.js";
|
|
2
|
-
/** The kernel's `Conversions` for the fields an input bind reads (see kernel/surface), as a literal; empty when none. */
|
|
3
2
|
export function conversions(properties, binds, target) {
|
|
4
3
|
const fields = Object.keys(properties ?? {}).flatMap(field => {
|
|
5
4
|
const bind = binds.find(b => b.k === "input" && String(b.name).toLowerCase() === field.toLowerCase());
|
|
@@ -10,7 +9,6 @@ export function conversions(properties, binds, target) {
|
|
|
10
9
|
});
|
|
11
10
|
return fields.length ? q(fields) : "";
|
|
12
11
|
}
|
|
13
|
-
/** Only build-time authorization assembly; generated queries retain their existing predicates. @internal */
|
|
14
12
|
export const authLines = (requires, path, binds = [], sqlite = true, trustedOnly = false) => [...(trustedOnly ? [` deny(caller, caller.kind === "system", ${q(path)});`] : []), ...(requires?.auth?.all ?? []).map((p, index) => {
|
|
15
13
|
const allowed = typeof p === "string" ? p === "ctx.admin" ? 'isAdmin(caller)' : 'isUser(caller)' : `caller.kind === "user" && caller.scopes.includes(${q(p["ctx.auth.scope"])})`;
|
|
16
14
|
return ` deny(caller, ${allowed}, ${q(`${path}/requires/auth/all/${index}`)});`;
|
package/dist/domain/paging.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
/** Build-time proofs for the compiler's keyset keys. No AST reaches a generated operation. */
|
|
2
1
|
import { NATIVE_COLUMNS, NATIVE_NOT_NULL, notNullColumns } from "../build/compiler.js";
|
|
3
2
|
import { declaredType } from "./columns.js";
|
|
4
|
-
/** Storage column -> the native field a manifest index names. */
|
|
5
3
|
const NATIVE_FIELDS = new Map(Object.entries(NATIVE_COLUMNS).map(([field, column]) => [column, field]));
|
|
6
4
|
const fieldName = (n) => n.ColumnRef?.fields?.at(-1)?.String?.sval;
|
|
7
5
|
const scalar = (n) => !!(n.ParamRef || n.A_Const || n.TypeCast && scalar(n.TypeCast.arg));
|
|
8
|
-
// A user-field term lowers to the dialect's anyOf, `(col) IN (SELECT value FROM json_each((?)))` or `(col) = ANY(CAST(...(?)...))`.
|
|
9
6
|
const anyOf = (raw) => raw?.length === 8 && raw[1]?.ColumnRef && raw[5]?.ParamRef && /^ (IN \(SELECT value FROM json_each\(|= ANY\()/.test(raw[3]);
|
|
10
|
-
// A scope prints `/*s:field*/ kept /*|*/ kept OR 1 = 1 /*;*/`, so `{ all: true }` drops it; any other anyOf is a restrict, which it never lifts.
|
|
11
7
|
const scopeTerm = (raw) => typeof raw?.[0] === "string" && raw[0].startsWith("/*s:");
|
|
12
8
|
const restricts = (n) => !!n && typeof n === "object" && (anyOf(n.Raw?.parts) || !scopeTerm(n.Raw?.parts) && Object.values(n).some(restricts));
|
|
13
9
|
const aggregate = (n) => !!n && !n.SubLink && !n.SelectStmt && (n.FuncCall && !n.FuncCall.over && ["count", "sum", "min", "max", "avg"].includes(n.FuncCall.funcname?.at(-1)?.String?.sval) || Object.values(n).some(v => v && typeof v === "object" && (Array.isArray(v) ? v.some(aggregate) : aggregate(v))));
|
|
14
|
-
/** Only complete grouped tuples, base ids and non-fanning singleton joins are proved. */
|
|
15
10
|
export function provePaging(plan, ast) {
|
|
16
11
|
let reason;
|
|
17
12
|
const fail = (message) => { reason = message; return undefined; };
|
|
@@ -50,7 +45,6 @@ export function provePaging(plan, ast) {
|
|
|
50
45
|
return fail(`Fanning join '${[...aliases.keys()].filter(alias => !before.has(alias)).join(", ")}': join on the joined Schema's id or a single-field unique key, or reduce it to one row.`);
|
|
51
46
|
if (n.JoinExpr.jointype === "JOIN_FULL" || n.JoinExpr.jointype === "JOIN_RIGHT")
|
|
52
47
|
return;
|
|
53
|
-
// The right side of LEFT JOIN may be absent: its fields cannot become NOT NULL cursor keys.
|
|
54
48
|
if (n.JoinExpr.jointype === "JOIN_LEFT")
|
|
55
49
|
for (const [alias, r] of aliases)
|
|
56
50
|
if (!before.has(alias))
|
|
@@ -73,7 +67,6 @@ export function provePaging(plan, ast) {
|
|
|
73
67
|
else if (sub)
|
|
74
68
|
fact = select(sub.subquery.SelectStmt, ctes)?.relation;
|
|
75
69
|
if (fact) {
|
|
76
|
-
// Two aliases of one Schema/CTE are distinct row sources, including a singleton joined to many rows.
|
|
77
70
|
const sources = new Map();
|
|
78
71
|
fact = { ...fact, fields: new Map([...fact.fields].map(([name, f]) => {
|
|
79
72
|
if (!sources.has(f.source))
|
|
@@ -117,8 +110,6 @@ export function provePaging(plan, ast) {
|
|
|
117
110
|
const fixed = (n) => {
|
|
118
111
|
if (n?.BoolExpr?.boolop === "AND_EXPR")
|
|
119
112
|
n.BoolExpr.args.forEach(fixed);
|
|
120
|
-
// `col IN auth.user(field)` (a scope or restrict) never fixes a prefix (#1644): the plan is prepared for a set, so
|
|
121
|
-
// native ORDER BY after it reads and sorts all the caller's rows. It is only remembered for the refusal's guidance.
|
|
122
113
|
const raw = n?.Raw?.parts;
|
|
123
114
|
if (scopeTerm(raw))
|
|
124
115
|
return fixed(raw[1]);
|
|
@@ -151,9 +142,7 @@ export function provePaging(plan, ast) {
|
|
|
151
142
|
const same = (a, b) => a && b && a.name === b.name && a.source === b.source;
|
|
152
143
|
if (sorts.length) {
|
|
153
144
|
if (group.length) {
|
|
154
|
-
// Full GROUP BY keys, not an id from an arbitrary row in the group, make the result unique.
|
|
155
145
|
if (!group.every(g => {
|
|
156
|
-
// GROUP BY prefers an input column over a colliding output alias; ORDER BY does the reverse.
|
|
157
146
|
const source = g.ColumnRef?.fields?.length === 1 ? [...aliases.values()].filter(r => r.names.has(fieldName(g))) : [];
|
|
158
147
|
const f = source.length ? source.length === 1 ? source[0].fields.get(fieldName(g)) : undefined : resolve(g);
|
|
159
148
|
return f?.notNull && keys.some(k => same(k, f));
|
|
@@ -175,21 +164,16 @@ export function provePaging(plan, ast) {
|
|
|
175
164
|
return order.every((f, n) => index[i + n] === f.name);
|
|
176
165
|
});
|
|
177
166
|
if (!serving(base.fixed)) {
|
|
178
|
-
// SQL names a column (`created_at`), a manifest index the field (`createdAt`).
|
|
179
167
|
const index = serving(new Set([...base.fixed, ...base.sets])), named = (c) => schema.names?.[c] ?? c, field = (c) => schema.names?.[c] ?? NATIVE_FIELDS.get(c) ?? c;
|
|
180
168
|
const columns = order.map(f => named(f.name)).join(", "), fields = order.map(f => field(f.name)).join(", ");
|
|
181
169
|
if (!index)
|
|
182
170
|
return fail(`Missing index for ORDER BY (${columns}): declare a Schema index with [${fields}] in this order after any fixed equality prefix.`);
|
|
183
171
|
const prefix = index.slice(0, index.indexOf(order[0].name)), set = prefix.filter(c => !base.fixed.has(c));
|
|
184
|
-
// The suggested order is the index from its first set-bound column: an equality-fixed column between stays in it.
|
|
185
172
|
const lead = prefix.slice(prefix.indexOf(set[0])), quoted = (cs) => cs.map(c => `'${field(c)}'`).join(", ");
|
|
186
|
-
// The scope-first order is offered only when SQL can name every added column and none can be null. The owner
|
|
187
|
-
// scope column is NOT NULL in storage, but a View cannot address it.
|
|
188
173
|
const notNull = notNullColumns(schema), owner = lead.filter(c => c === schema.scope), nullable = lead.filter(c => !NATIVE_NOT_NULL.has(c) && !notNull.has(c) && c !== schema.scope);
|
|
189
174
|
const dir = (f) => sorts[keys.indexOf(f)].SortBy.sortby_dir === "SORTBY_DESC" ? " DESC" : "";
|
|
190
175
|
const sorted = [...lead.map(c => named(c) + dir(order[0])), ...order.map(f => named(f.name) + dir(f))].join(", ");
|
|
191
176
|
const desc = order.some(f => dir(f)) ? ", read backwards; on PostgreSQL confirm with EXPLAIN that no Sort remains" : "";
|
|
192
|
-
// `{ all: true }` drops only scope terms: offer it an index on the View's equality-fixed columns, unless a restrict remains.
|
|
193
177
|
const eq = [...base.fixed].filter(c => c !== "id" && !order.some(f => f.name === c));
|
|
194
178
|
const ways = [...nullable.length || owner.length ? [] : [`ORDER BY ${sorted} (same index${desc})`], ...restricts(ast) ? [] : [`for { all: true } callers only, index [${[...eq.map(field), fields].join(", ")}, id]`]];
|
|
195
179
|
const why = [...owner.length ? [`${quoted(owner)} is the owner column, which SQL cannot name`] : [], ...nullable.length ? [`${quoted(nullable)} can be null`] : []];
|
|
@@ -205,7 +189,6 @@ export function provePaging(plan, ast) {
|
|
|
205
189
|
const result = select(ast.SelectStmt);
|
|
206
190
|
return result ? { types: result.keys.map(f => f.type), single: result.relation.single } : reason ? { types: [], single: false, reason } : undefined;
|
|
207
191
|
}
|
|
208
|
-
/** Cast only cursor ParamRefs in the outer predicate, never authored literals or bare projections. */
|
|
209
192
|
export function castCursor(ast, sources, types, postgres) {
|
|
210
193
|
const typeOf = (source) => source && "cursor" in source ? types[source.cursor] : undefined;
|
|
211
194
|
const typed = (type) => type && (postgres ? ["timestamptz", "date"] : ["integer", "int4", "int8"]).includes(type);
|
package/dist/domain/recipes.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
const KINDS = new Set(["uid", "admin", "user", "now", "cutoff", "const", "input", "version"]);
|
|
2
|
-
/** @internal */
|
|
1
|
+
const KINDS = new Set(["uid", "admin", "user", "now", "cutoff", "const", "input", "version", "scoped"]);
|
|
3
2
|
export function recipesOf(statement, fields, refuse, values) {
|
|
3
|
+
const field = (bound) => {
|
|
4
|
+
const name = fields.find(field => field.toLowerCase() === String(bound).toLowerCase());
|
|
5
|
+
if (name === undefined)
|
|
6
|
+
refuse(`Input bind '${bound}' has no declared field.`);
|
|
7
|
+
return name ?? String(bound);
|
|
8
|
+
};
|
|
4
9
|
return statement.binds.map(b => {
|
|
5
10
|
if (!KINDS.has(b.k))
|
|
6
11
|
refuse(`Bind recipe '${b.k}' needs an explicit native implementation.`);
|
|
7
12
|
if (b.k === "now" || b.k === "cutoff")
|
|
8
13
|
values?.add(b.k);
|
|
14
|
+
if (b.k === "scoped" && b.name !== undefined)
|
|
15
|
+
return { ...b, name: field(b.name) };
|
|
9
16
|
if (b.k !== "input")
|
|
10
17
|
return b;
|
|
11
18
|
if (b.type === "date" || b.type === "timestamptz")
|
|
12
19
|
values?.add(b.type === "date" ? "date" : "timestamp");
|
|
13
|
-
|
|
14
|
-
if (name === undefined)
|
|
15
|
-
refuse(`Input bind '${b.name}' has no declared field.`);
|
|
16
|
-
// only a date-time or a date is converted at bind time (`encode` in kernel/binds)
|
|
17
|
-
return { k: "input", name: name ?? String(b.name), ...(b.type === "timestamptz" || b.type === "date" ? { type: b.type } : {}) };
|
|
20
|
+
return { k: "input", name: field(b.name), ...(b.type === "timestamptz" || b.type === "date" || b.type === "json" ? { type: b.type } : {}) };
|
|
18
21
|
});
|
|
19
22
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Conditional capability imports keep applications without user references unchanged. */
|
|
2
1
|
export function userReferences(plan, sqlite) {
|
|
3
2
|
const used = !!plan.access;
|
|
4
3
|
return { used, code: used ? `import * as USER from "@aotter/mantle/user-fields";\nconst USER_VALUES = { scopeInsert: USER.scopeInsert, scopedAll: USER.scopedAll, userRefs: USER.userRefs(${sqlite}, 1) };` : '', values: used ? '...USER_VALUES' : '' };
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { declaredType } from "./columns.js";
|
|
2
2
|
const raw = (...parts) => ({ Raw: { parts } });
|
|
3
|
-
/** Temporal SQL forms, plus selected integer text before driver rounding. */
|
|
4
3
|
const wire = (type, d, v, boundary = false) => type === "timestamptz" ? raw(...d.wireTimestamp(v)) : type === "date" ? raw(...d.wireDate(v)) : boundary && type && ["integer", "int8", "int4"].includes(type) ? raw("CAST(", v, " AS text)") : undefined;
|
|
5
|
-
/**
|
|
6
|
-
* Formats declared temporal outputs and selected integers in a View's top-level SELECT, preserving names.
|
|
7
|
-
* Returns a refusal when the outputs cannot be matched to SELECT items by position.
|
|
8
|
-
*/
|
|
9
4
|
export function wireScalars(ast, keys, types, d, boundary = false) {
|
|
10
5
|
if (!types.some(type => type === "timestamptz" || type === "date" || boundary && type && ["integer", "int8", "int4"].includes(type)) || !ast.SelectStmt)
|
|
11
6
|
return ast;
|
|
@@ -24,7 +19,6 @@ export function wireScalars(ast, keys, types, d, boundary = false) {
|
|
|
24
19
|
const out = select(ast.SelectStmt);
|
|
25
20
|
return typeof out === "string" ? out : { ...ast, SelectStmt: out };
|
|
26
21
|
}
|
|
27
|
-
/** The same wire form for a write's RETURNING list (Core has already expanded `*`): a column of the written table is wrapped by its declared type. */
|
|
28
22
|
export function wireReturning(ast, schemas, d, boundary = false) {
|
|
29
23
|
const [kind, stmt] = Object.entries(ast)[0] ?? [];
|
|
30
24
|
const list = stmt?.returningClause?.exprs;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Selected Admin queries/procedures: Core checks, rewrites and prints every native fragment at generation. */
|
|
2
1
|
import { enumOptions, protectedColumns } from "../build/compiler.js";
|
|
3
2
|
import { parsePgSql } from "../build/compiler.js";
|
|
4
3
|
import { compileProgram } from "../build/compiler.js";
|
|
@@ -6,6 +5,8 @@ import { adminOrder } from "../domain/adminOrder.js";
|
|
|
6
5
|
import { declaredType, serverOwned } from "../domain/columns.js";
|
|
7
6
|
import { wireScalars, wireReturning } from "../domain/wireScalars.js";
|
|
8
7
|
import { recipesOf } from "../domain/recipes.js";
|
|
8
|
+
import { refChecks } from "../build/compiler.js";
|
|
9
|
+
import { renderTerm } from "../kernel/store.js";
|
|
9
10
|
const ref = (...names) => ({ ColumnRef: { fields: names.map(sval => ({ String: { sval } })) } });
|
|
10
11
|
const op = (lexpr, rexpr) => ({ A_Expr: { kind: "AEXPR_OP", name: [{ String: { sval: "=" } }], lexpr, rexpr } });
|
|
11
12
|
const and = (...args) => args.length === 1 ? args[0] : { BoolExpr: { boolop: "AND_EXPR", args } };
|
|
@@ -29,14 +30,18 @@ export async function adminEntries(plan, dialect, wire, refusals) {
|
|
|
29
30
|
const columns = [...system(s), ...fields(s)], writable = Object.keys(s.fields).filter(f => f !== s.scope && s.fields[f] !== "geo");
|
|
30
31
|
const parent = s.translates && plan.schemas[s.translates.parent.toLowerCase()];
|
|
31
32
|
const types = columns.map(f => declaredType(s.fields, f) ?? (f === "version" ? "integer" : f.endsWith("_lat") || f.endsWith("_lng") ? "real" : "text"));
|
|
32
|
-
// Compiler scope names belong only to relations this generated query can reference.
|
|
33
33
|
const schemas = Object.create(null);
|
|
34
34
|
let relation = s;
|
|
35
35
|
while (relation && !Object.hasOwn(schemas, relation.name)) {
|
|
36
36
|
schemas[relation.name] = relation;
|
|
37
37
|
relation = relation.translates ? plan.schemas[relation.translates.parent.toLowerCase()] : undefined;
|
|
38
38
|
}
|
|
39
|
-
const ctx = (kind, trusted, inputs) => ({ dialect, schemas, inputs, kind, mode: trusted ? "trusted" : "admin" });
|
|
39
|
+
const ctx = (kind, trusted, inputs) => ({ dialect, schemas, inputs, kind, mode: trusted ? "trusted" : "admin", uncheckedRefs: true });
|
|
40
|
+
const refs = (child) => Object.fromEntries(refChecks(s, plan.schemas).filter(r => writable.includes(r.column)).map(r => {
|
|
41
|
+
const holes = [], hole = (value) => { const token = wire.param(marker + 1 + holes.length); holes.push([token, value]); return token; };
|
|
42
|
+
const sql = `EXISTS (SELECT 1 FROM ${wire.ident(r.table)} _mantle_ref WHERE ${[`_mantle_ref.${wire.ident(r.field)} = ${hole(r.column)}`, ...r.same.map(([parent, column]) => `_mantle_ref.${wire.ident(parent)} = ${child(column, hole)}`), ...r.terms.map(t => renderTerm(wire, t, "_mantle_ref.", hole))].join(" AND ")})`;
|
|
43
|
+
return [r.column, { sql, holes }];
|
|
44
|
+
}));
|
|
40
45
|
const select = `SELECT ${columns.map(c => `${wire.ident(s.name)}.${wire.ident(c)}`).join(",")} FROM ${wire.ident(s.name)}`;
|
|
41
46
|
const base = await parse(select);
|
|
42
47
|
const project = (ast, names, keys) => {
|
|
@@ -83,10 +88,8 @@ export async function adminEntries(plan, dialect, wire, refusals) {
|
|
|
83
88
|
const [compiled] = compileProgram(base, ctx("view", trusted, {}));
|
|
84
89
|
if (!compiled)
|
|
85
90
|
throw Error("Missing compiled collection View.");
|
|
86
|
-
// Keep the compiler's verified policy, but share the physical SELECT template at requests.
|
|
87
91
|
const baseInner = compiled.ast.SelectStmt.fromClause[0].RangeSubselect.subquery.SelectStmt, policy = baseInner.whereClause;
|
|
88
92
|
compiled.ast.SelectStmt.fromClause = [baseInner.fromClause[0]];
|
|
89
|
-
// The native merge is a scalar-id lookup, never a fanning join. Core owns both sides' visibility.
|
|
90
93
|
if (parent) {
|
|
91
94
|
const own = new Set(Object.keys(s.fields).flatMap(f => s.fields[f] === "geo" ? [f + "_lat", f + "_lng"] : [f]));
|
|
92
95
|
const inherited = fields(parent).filter(f => !own.has(f)), on = s.translates.on.toLowerCase(), alias = "_mantle_merge";
|
|
@@ -109,7 +112,6 @@ export async function adminEntries(plan, dialect, wire, refusals) {
|
|
|
109
112
|
const names = compiled.ast.SelectStmt.targetList.map((t) => t.ResTarget.name ?? t.ResTarget.val.ColumnRef.fields.at(-1).String.sval);
|
|
110
113
|
const projection = print(project({ SelectStmt: { targetList: compiled.ast.SelectStmt.targetList, op: "SETOP_NONE", limitOption: "LIMIT_OPTION_DEFAULT" } }, names, [])).replace(/^SELECT /, "");
|
|
111
114
|
const from = print({ SelectStmt: { ...compiled.ast.SelectStmt, targetList: [{ ResTarget: { val: { A_Const: { ival: { ival: 1 } } } } }] } }).replace(/^SELECT 1/, "");
|
|
112
|
-
// Core compiles all writable fields once; request-time work selects only generated fragments.
|
|
113
115
|
const inputs = { ...Object.fromEntries(writable.map(f => [f, s.fields[f]])), id: "text" };
|
|
114
116
|
const returning = columns.map(wire.ident).join(",");
|
|
115
117
|
const insertAst = writable.length ? (await parse(`INSERT INTO ${wire.ident(s.name)} (${writable.map(wire.ident).join(",")}) VALUES (${writable.map(f => `input.${wire.ident(f)}`).join(",")}) RETURNING ${returning}`))[0] : { InsertStmt: { relation: { RangeVar: { relname: s.name, mantle: "table", inh: true, relpersistence: "p" } }.RangeVar, cols: [], selectStmt: { SelectStmt: { valuesLists: [{ List: { items: [] } }] } }, returningClause: { exprs: columns.map(f => ({ ResTarget: { val: ref(f) } })) } } };
|
|
@@ -142,7 +144,8 @@ export async function adminEntries(plan, dialect, wire, refusals) {
|
|
|
142
144
|
for (const [i, field] of writable.entries())
|
|
143
145
|
entryFields[field] = { column: wire.ident(field), insert: expr(insert.selectStmt.SelectStmt.fromClause[0].RangeSubselect.subquery.SelectStmt.valuesLists[0].List.items[i]), ...(!protectedColumns(s).includes(field) && !serverOwned(s.schema, field) ? { update: wire.ident(field) + " = " + expr(update.targetList[updateFields.indexOf(field)].ResTarget.val) } : {}) };
|
|
144
146
|
const returnSQL = (ast) => " RETURNING " + ast.returningClause.exprs.map((t) => expr(t.ResTarget.val) + (t.ResTarget.name ? " AS " + wire.ident(t.ResTarget.name) : "")).join(",");
|
|
145
|
-
|
|
147
|
+
const created = trusted ? {} : refs((column, hole) => hole(column)), patched = trusted ? {} : refs(column => wire.ident(s.name) + "." + wire.ident(column));
|
|
148
|
+
return { projection, from, policy: policy ? expr(policy) : "", binds: recipesOf(compiled, [], message => { throw Error(message); }), fields: entryFields, create: { ...(Object.keys(created).length ? { refs: created } : {}), prefix: print(create.ast).slice(0, print(create.ast).indexOf(" (")) + " (", between: ") VALUES (", suffix: ")", binds: create.binds, columns: insert.cols.slice(writable.length).map((t) => wire.ident(t.ResTarget.name)), values: insert.selectStmt.SelectStmt.targetList.slice(writable.length ? 1 : 0).map((t) => expr(t.ResTarget.val)), returning: returnSQL(insert) }, patch: { ...(Object.keys(patched).length ? { refs: patched } : {}), prefix: print(patch.ast).slice(0, print(patch.ast).indexOf(" SET ")) + " SET ", binds: patch.binds, sets: update.targetList.slice(updateFields.length).map((t) => wire.ident(t.ResTarget.name) + " = " + expr(t.ResTarget.val)), where: " WHERE " + expr(update.whereClause), returning: returnSQL(update) }, remove: { sql: print(remove.ast), binds: remove.binds, row: true } };
|
|
146
149
|
}));
|
|
147
150
|
result[s.name] = { ...(s.userScopes?.length ? { userScopes: s.userScopes } : {}), cursorAliases, scoped: !!s.scope, path: "schemas/" + s.name + "/admin", marker: markerText, id: wire.ident(s.name) + "." + wire.ident("id"), fields: fieldFacts, orderFields: Object.keys(ordered), search: `(${wire.ident(s.name)}.${wire.ident("id")} = ${markerText}${s.search?.length ? ` OR ${wire.search(s.name, s.search, wire.ident(s.name), markerText)}` : ""})`, statistics: { rowsAlias, column: wire.ident(s.name) + "." + wire.ident("created_at"), time: postgres ? `extract(epoch FROM ${wire.ident(s.name)}.${wire.ident("created_at")}) * 1000000` : wire.ident(s.name) + "." + wire.ident("created_at"), subtype }, caller: modes[0], trusted: modes[1] };
|
|
148
151
|
}
|