@aotter/mantle 0.2.0-alpha.9 → 0.2.1-alpha.1

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.
Files changed (180) hide show
  1. package/README.md +15 -5
  2. package/dist/auth/adapter.js +1 -28
  3. package/dist/auth/admin.js +2 -5
  4. package/dist/auth/caller.js +0 -27
  5. package/dist/auth/consent.js +100 -0
  6. package/dist/auth/hardening.js +0 -7
  7. package/dist/auth/identity.js +0 -11
  8. package/dist/auth/oauth.js +0 -32
  9. package/dist/auth/resource-seeds.js +0 -2
  10. package/dist/auth/route-http.js +33 -0
  11. package/dist/auth/routes.js +5 -151
  12. package/dist/build/compiler.d.ts +19 -609
  13. package/dist/build/compiler.js +6 -6896
  14. package/dist/build/spec/domain/model/ManifestGrammar.d.ts +39 -1
  15. package/dist/build/spec/domain/model/RuntimePlan.d.ts +16 -3
  16. package/dist/build/spec/domain/model/SqlIr.d.ts +32 -0
  17. package/dist/build/spec/domain/model/UserFields.d.ts +1 -0
  18. package/dist/build/spec/kernel/diagnostic.d.ts +1 -1
  19. package/dist/bun/assets.js +0 -2
  20. package/dist/domain/adminOrder.js +0 -2
  21. package/dist/domain/columns.js +7 -20
  22. package/dist/domain/describe/chunk-P6PP6VVV.js +1 -0
  23. package/dist/domain/describe/index.d.ts +1 -0
  24. package/dist/domain/describe/index.js +1 -115
  25. package/dist/domain/describe/match.d.ts +19 -0
  26. package/dist/domain/describe/target-hash.js +1 -12
  27. package/dist/domain/entry.js +0 -14
  28. package/dist/domain/generated-tools.js +0 -1
  29. package/dist/domain/input.js +0 -2
  30. package/dist/domain/paging.js +0 -17
  31. package/dist/domain/recipes.js +10 -7
  32. package/dist/domain/userReferences.js +0 -1
  33. package/dist/domain/wireScalars.js +0 -6
  34. package/dist/infrastructure/adminEntries.js +27 -8
  35. package/dist/infrastructure/adminPaging.js +55 -57
  36. package/dist/infrastructure/artifacts.js +0 -4
  37. package/dist/infrastructure/authSchema.js +0 -9
  38. package/dist/infrastructure/bundle.js +45 -38
  39. package/dist/infrastructure/cli.js +26 -12
  40. package/dist/infrastructure/generate.js +26 -19
  41. package/dist/infrastructure/generateAuth.js +1 -9
  42. package/dist/infrastructure/help.js +55 -0
  43. package/dist/infrastructure/init.js +50 -0
  44. package/dist/infrastructure/inputs.js +0 -9
  45. package/dist/infrastructure/mounts.js +22 -0
  46. package/dist/infrastructure/wrangler.js +0 -6
  47. package/dist/kernel/authorization.js +0 -1
  48. package/dist/kernel/binds.d.ts +34 -7
  49. package/dist/kernel/binds.js +5 -11
  50. package/dist/kernel/caller-identity.d.ts +0 -3
  51. package/dist/kernel/caller-identity.js +0 -15
  52. package/dist/kernel/caller-mode.d.ts +4 -0
  53. package/dist/kernel/caller-mode.js +14 -0
  54. package/dist/kernel/contracts.d.ts +13 -10
  55. package/dist/kernel/contracts.js +14 -41
  56. package/dist/kernel/cron.js +0 -4
  57. package/dist/kernel/dialect/common.js +0 -3
  58. package/dist/kernel/dialect/postgres.js +0 -2
  59. package/dist/kernel/dialect/sqlite.js +1 -6
  60. package/dist/kernel/drain.js +2 -8
  61. package/dist/kernel/driver/bun-sqlite.js +3 -7
  62. package/dist/kernel/driver/d1.d.ts +1 -1
  63. package/dist/kernel/driver/d1.js +6 -4
  64. package/dist/kernel/driver/postgres.d.ts +2 -1
  65. package/dist/kernel/driver/postgres.js +12 -22
  66. package/dist/kernel/driver/transaction-context.js +0 -1
  67. package/dist/kernel/email.js +0 -4
  68. package/dist/kernel/host-access.js +2 -3
  69. package/dist/kernel/host-caller.d.ts +1 -1
  70. package/dist/kernel/host-caller.js +3 -6
  71. package/dist/kernel/json-body.js +0 -2
  72. package/dist/kernel/mcp-apps.js +0 -5
  73. package/dist/kernel/mcp-call.js +0 -1
  74. package/dist/kernel/mcp-handler.js +4 -27
  75. package/dist/kernel/mcp.js +0 -5
  76. package/dist/kernel/media.js +0 -18
  77. package/dist/kernel/paging.js +1 -9
  78. package/dist/kernel/publishing.js +3 -4
  79. package/dist/kernel/r2-media.js +0 -7
  80. package/dist/kernel/rest-dispatch.js +0 -1
  81. package/dist/kernel/rest-router.js +5 -8
  82. package/dist/kernel/rest.d.ts +2 -1
  83. package/dist/kernel/rest.js +1 -3
  84. package/dist/kernel/route-pattern.d.ts +26 -0
  85. package/dist/kernel/route-pattern.js +34 -0
  86. package/dist/kernel/scalars.js +1 -9
  87. package/dist/kernel/search.js +0 -1
  88. package/dist/kernel/site.js +0 -2
  89. package/dist/kernel/store.d.ts +26 -2
  90. package/dist/kernel/store.js +69 -49
  91. package/dist/kernel/surface-failure.d.ts +10 -0
  92. package/dist/kernel/surface-failure.js +29 -0
  93. package/dist/kernel/surface.js +0 -3
  94. package/dist/kernel/sweep.js +0 -1
  95. package/dist/kernel/translations.js +0 -1
  96. package/dist/kernel/user-caller.js +2 -8
  97. package/dist/kernel/user-fields.d.ts +3 -2
  98. package/dist/kernel/user-fields.js +9 -9
  99. package/dist/kernel/validation.js +0 -8
  100. package/dist/kernel/web-gateway.js +0 -2
  101. package/dist/kernel/web-head.js +0 -1
  102. package/dist/kernel/web-index.js +0 -1
  103. package/dist/kernel/web-response.js +0 -1
  104. package/dist/kernel/web-serve.js +97 -0
  105. package/dist/kernel/web.d.ts +1 -1
  106. package/dist/kernel/web.js +10 -91
  107. package/dist/kernel/where.js +0 -6
  108. package/dist/usecase/adminBindings.js +32 -23
  109. package/dist/usecase/adminClient.js +4 -22
  110. package/dist/usecase/adminDescriptors.js +2 -9
  111. package/dist/usecase/generate.js +14 -17
  112. package/dist/usecase/handlers.js +1 -4
  113. package/dist/usecase/mcp-media.js +0 -1
  114. package/dist/usecase/mcp.js +0 -4
  115. package/dist/usecase/paging.js +1 -3
  116. package/dist/usecase/publishing.js +20 -11
  117. package/dist/usecase/rest.js +5 -6
  118. package/dist/usecase/site.js +0 -1
  119. package/dist/usecase/stores.js +12 -17
  120. package/dist/usecase/tool-version.js +0 -3
  121. package/dist/usecase/translations.js +0 -2
  122. package/dist/usecase/validation.js +0 -20
  123. package/docs/examples/README.md +13 -5
  124. package/docs/examples/app-shell/README.md +91 -12
  125. package/docs/examples/app-shell/accept.mjs +91 -0
  126. package/docs/examples/app-shell/gitignore +7 -0
  127. package/docs/examples/app-shell/package.json +10 -9
  128. package/docs/examples/app-shell/smoke-kit.mjs +29 -15
  129. package/docs/examples/app-shell/src/routes.ts +6 -3
  130. package/docs/examples/app-shell/src/service.ts +2 -1
  131. package/docs/examples/club-events/README.md +7 -2
  132. package/docs/examples/club-events/manifests/club-events.yaml +17 -0
  133. package/docs/examples/club-events/smoke.mjs +7 -1
  134. package/docs/examples/generated-web/seed.sql +5 -4
  135. package/docs/examples/purchasing/README.md +12 -3
  136. package/docs/examples/purchasing/manifests/purchasing.yaml +24 -4
  137. package/docs/examples/purchasing/smoke.mjs +14 -0
  138. package/docs/examples/recipes/README.md +5 -3
  139. package/docs/handbook/cloudflare/media-r2.md +6 -3
  140. package/docs/handbook/cloudflare/service-entry.md +5 -0
  141. package/docs/handbook/concepts/lifecycle-and-locales.md +21 -1
  142. package/docs/handbook/examples/hub.md +1 -1
  143. package/docs/handbook/guides/admin-ui.md +1 -1
  144. package/docs/handbook/guides/agent-setup.md +20 -10
  145. package/docs/handbook/guides/agent-workflow-acceptance.md +225 -105
  146. package/docs/handbook/guides/local-testing.md +85 -0
  147. package/docs/handbook/guides/native-admin.md +242 -16
  148. package/docs/handbook/guides/native-execution.md +10 -8
  149. package/docs/handbook/guides/public-pages.md +44 -4
  150. package/docs/handbook/guides/revisions.md +584 -0
  151. package/docs/handbook/guides/size-and-startup.md +140 -0
  152. package/docs/handbook/guides/sql-patterns.md +312 -0
  153. package/docs/handbook/guides/typed-queries.md +146 -3
  154. package/docs/handbook/navigation.json +20 -0
  155. package/docs/handbook/reference/admin-descriptors.md +4 -1
  156. package/docs/handbook/reference/authorization.md +3 -1
  157. package/docs/handbook/reference/describe.md +42 -0
  158. package/docs/handbook/reference/diagnostics.md +16 -2
  159. package/docs/handbook/reference/procedure.md +6 -6
  160. package/docs/handbook/reference/schema.md +79 -9
  161. package/docs/handbook/reference/sql.md +65 -0
  162. package/docs/handbook/reference/surface.md +8 -4
  163. package/docs/handbook/reference/trigger.md +12 -0
  164. package/docs/handbook/reference/view.md +11 -3
  165. package/docs/handbook/releases/index.md +71 -7
  166. package/docs/handbook/start/agents.md +49 -0
  167. package/docs/handbook/start/project-and-cli.md +10 -0
  168. package/docs/handbook/start/quickstart-worker.md +1 -1
  169. package/docs/skills/develop/SKILL.md +19 -6
  170. package/docs/skills/integrate/SKILL.md +3 -1
  171. package/docs/skills/performance/SKILL.md +169 -0
  172. package/docs/skills/update/SKILL.md +10 -4
  173. package/docs/upgrade-0.1-to-0.2.md +151 -39
  174. package/package.json +31 -11
  175. package/skills/README.md +6 -2
  176. package/skills/mantle/SKILL.md +53 -120
  177. package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
  178. package/skills/mantle/references/local-testing.md +0 -20
  179. package/skills/mantle/references/web.md +0 -41
  180. package/skills/mantle/references/website.md +0 -51
@@ -0,0 +1,97 @@
1
+ import { forwarded, wire, ContractError } from "./contracts.js";
2
+ import { decode } from "./surface.js";
3
+ const NO_STORE = { "cache-control": "no-store" };
4
+ const notFound = () => new Response(null, { status: 404, headers: NO_STORE });
5
+ export function serve(options, views, pages, query) {
6
+ const origin = new URL(options.origin);
7
+ const context = async (request, env, ctx) => ({ db: options.db, caller: forwarded(ctx, options.gateway)
8
+ ? (await import("./host-caller.js")).gatewayCaller(ctx, options.gateway, options.fields)
9
+ : typeof options.caller === "function" ? await options.caller(request, env, { readOnly: ["GET", "HEAD"].includes(request.method) }) : options.caller ?? { kind: "anonymous" } });
10
+ const urlFor = (path, row, view) => origin.origin + path.replace(/\{([^{}]+)\}/g, (_, key) => {
11
+ let value = row[key];
12
+ if (views.get(view).booleans?.includes(key) && (value === 0 || value === 1))
13
+ value = Boolean(value);
14
+ if (value === null || value === undefined)
15
+ throw new TypeError("List View must return every page parameter: " + key);
16
+ const segment = String(value);
17
+ if (segment === "." || segment === "..")
18
+ return "";
19
+ return encodeURIComponent(segment).replace(/[()]/g, c => "%" + c.charCodeAt(0).toString(16).toUpperCase());
20
+ });
21
+ const validUrl = (path, row, view) => { if ([...path.matchAll(/\{([^{}]+)\}/g)].some(([, key]) => row[key] === "." || row[key] === ".."))
22
+ return; return urlFor(path, row, view); };
23
+ const match = (path) => {
24
+ for (const entry of pages) {
25
+ const hit = entry.pattern.exec(path);
26
+ if (!hit)
27
+ continue;
28
+ return { ...entry, params: new URLSearchParams(decode(entry.keys, hit, "Malformed URL parameter.", "web/path")) };
29
+ }
30
+ };
31
+ const read = async (entry, ctx) => (await query(entry.page.view, ctx.db, ctx.caller, entry.params)).rows[0];
32
+ const response = async (request, body, type, caller) => {
33
+ const { webResponse } = await import('./web-response.js');
34
+ return webResponse(request, body, type, caller.kind === "anonymous" ? (options.cacheControl ?? "public, max-age=60") : "private, no-store");
35
+ };
36
+ return {
37
+ async fetch(request, env, execution) {
38
+ let caller;
39
+ try {
40
+ if (request.method !== "GET" && request.method !== "HEAD")
41
+ return;
42
+ const path = new URL(request.url).pathname, index = path === "/llms.txt" || path === "/sitemap.xml";
43
+ const entry = !index && path.endsWith(".md") ? match(path.slice(0, -3)) : undefined, markdown = entry?.page.markdown;
44
+ if (!index && !markdown)
45
+ return;
46
+ const ctx = await context(request, env, execution);
47
+ caller = ctx.caller;
48
+ if (index) {
49
+ const sitemap = path === "/sitemap.xml";
50
+ const body = await (await import("./web-index.js")).webIndex(sitemap, options, pages, (list, cursor) => query(list, ctx.db, ctx.caller, new URLSearchParams(), cursor), validUrl);
51
+ return response(request, body, sitemap ? "application/xml" : "text/plain", ctx.caller);
52
+ }
53
+ const exact = match(path);
54
+ if (exact)
55
+ try {
56
+ if (await read(exact, ctx))
57
+ return;
58
+ }
59
+ catch (error) {
60
+ if (wire(error).status !== 400)
61
+ throw error;
62
+ }
63
+ const row = await read(entry, ctx);
64
+ if (!row)
65
+ return notFound();
66
+ const body = row[markdown];
67
+ if (body == null)
68
+ return notFound();
69
+ if (typeof body !== "string")
70
+ throw new TypeError("Markdown field must return text.");
71
+ return response(request, body, "text/markdown", ctx.caller);
72
+ }
73
+ catch (error) {
74
+ return (await import("./host-caller.js")).hostFailure(error, caller, "web");
75
+ }
76
+ },
77
+ async head(request, env, execution) {
78
+ try {
79
+ const path = new URL(request.url).pathname, entry = match(path);
80
+ if (!entry)
81
+ return "";
82
+ const ctx = await context(request, env, execution), row = await read(entry, ctx);
83
+ if (!row)
84
+ return "";
85
+ const canonical = validUrl(entry.path, row, entry.page.view);
86
+ if (!canonical)
87
+ return "";
88
+ return (await import("./web-head.js")).headTags(canonical, row, !!entry.page.markdown, options.locales);
89
+ }
90
+ catch (error) {
91
+ if (error instanceof ContractError && error.code === "HOST_CONTRACT")
92
+ throw error;
93
+ return "";
94
+ }
95
+ },
96
+ };
97
+ }
@@ -1,5 +1,5 @@
1
1
  /** Web output for a generated public View table: /llms.txt, /sitemap.xml, Markdown pages and head tags. SQL owns visibility. */
2
- import { type Caller, type Gateway, type RequestContext } from "./contracts.js";
2
+ import type { Caller, Gateway, RequestContext } from "./contracts.js";
3
3
  import type { UserFields } from "./user-fields.js";
4
4
  import { type Conversions } from "./surface.js";
5
5
  /** An app's public Views: row type and input keys per View, and the Views a list may name (input that accepts no fields). */
@@ -1,8 +1,4 @@
1
- /** Web output for a generated public View table: /llms.txt, /sitemap.xml, Markdown pages and head tags. SQL owns visibility. */
2
- import { forwarded, wire, ContractError } from "./contracts.js";
3
- import { convert, decode } from "./surface.js";
4
- const NO_STORE = { "cache-control": "no-store" };
5
- const notFound = () => new Response(null, { status: 404, headers: NO_STORE });
1
+ import { convert } from "./surface.js";
6
2
  export function web(options, table) {
7
3
  const views = new Map(table);
8
4
  const query = (name, db, caller, params, cursor) => {
@@ -23,35 +19,6 @@ export function web(options, table) {
23
19
  const pattern = path.split(/(\{[^{}]+\})/).map(part => part.startsWith("{") ? (keys.push(part.slice(1, -1)), "([^/]+)") : part.replace(/[.*+?^$()|[\]\\]/g, "\\$&")).join("");
24
20
  return { path, page, keys, pattern: new RegExp("^" + pattern + "$") };
25
21
  }).sort((a, b) => a.keys.length - b.keys.length);
26
- const context = async (request, env, ctx) => ({ db: options.db, caller: forwarded(ctx, options.gateway)
27
- ? (await import("./host-caller.js")).gatewayCaller(ctx, options.gateway, options.fields)
28
- : typeof options.caller === "function" ? await options.caller(request, env, { readOnly: ["GET", "HEAD"].includes(request.method) }) : options.caller ?? { kind: "anonymous" } });
29
- const urlFor = (path, row, view) => origin.origin + path.replace(/\{([^{}]+)\}/g, (_, key) => {
30
- let value = row[key];
31
- if (views.get(view).booleans?.includes(key) && (value === 0 || value === 1))
32
- value = Boolean(value);
33
- if (value === null || value === undefined)
34
- throw new TypeError("List View must return every page parameter: " + key);
35
- const segment = String(value);
36
- if (segment === "." || segment === "..")
37
- return "";
38
- return encodeURIComponent(segment).replace(/[()]/g, c => "%" + c.charCodeAt(0).toString(16).toUpperCase());
39
- });
40
- const validUrl = (path, row, view) => { if ([...path.matchAll(/\{([^{}]+)\}/g)].some(([, key]) => row[key] === "." || row[key] === ".."))
41
- return; return urlFor(path, row, view); };
42
- const match = (path) => {
43
- for (const entry of pages) {
44
- const hit = entry.pattern.exec(path);
45
- if (!hit)
46
- continue;
47
- return { ...entry, params: new URLSearchParams(decode(entry.keys, hit, "Malformed URL parameter.", "web/path")) };
48
- }
49
- };
50
- const read = async (entry, ctx) => (await query(entry.page.view, ctx.db, ctx.caller, entry.params)).rows[0];
51
- const response = async (request, body, type, caller) => {
52
- const { webResponse } = await import('./web-response.js');
53
- return webResponse(request, body, type, caller.kind === "anonymous" ? (options.cacheControl ?? "public, max-age=60") : "private, no-store");
54
- };
55
22
  if (options.gateway || options.authorize) {
56
23
  let handle;
57
24
  const load = async () => handle ??= (await import("./web-gateway.js")).gatewayWeb(options, table);
@@ -60,65 +27,17 @@ export function web(options, table) {
60
27
  head: async (request, env, ctx) => (await load()).head(request, env, ctx),
61
28
  };
62
29
  }
30
+ let served;
31
+ const serve = async () => served ??= (await import("./web-serve.js")).serve(options, views, pages, query);
63
32
  return {
64
33
  async fetch(request, env, execution) {
65
- let caller;
66
- try {
67
- if (request.method !== "GET" && request.method !== "HEAD")
68
- return;
69
- const path = new URL(request.url).pathname, index = path === "/llms.txt" || path === "/sitemap.xml";
70
- const entry = !index && path.endsWith(".md") ? match(path.slice(0, -3)) : undefined, markdown = entry?.page.markdown;
71
- if (!index && !markdown)
72
- return;
73
- const ctx = await context(request, env, execution);
74
- caller = ctx.caller;
75
- if (index) {
76
- const sitemap = path === "/sitemap.xml";
77
- const body = await (await import("./web-index.js")).webIndex(sitemap, options, pages, (list, cursor) => query(list, ctx.db, ctx.caller, new URLSearchParams(), cursor), validUrl);
78
- return response(request, body, sitemap ? "application/xml" : "text/plain", ctx.caller);
79
- }
80
- const exact = match(path);
81
- if (exact)
82
- try {
83
- if (await read(exact, ctx))
84
- return;
85
- }
86
- catch (error) {
87
- if (wire(error).status !== 400)
88
- throw error;
89
- }
90
- const row = await read(entry, ctx);
91
- if (!row)
92
- return notFound();
93
- const body = row[markdown];
94
- if (body == null)
95
- return notFound();
96
- if (typeof body !== "string")
97
- throw new TypeError("Markdown field must return text.");
98
- return response(request, body, "text/markdown", ctx.caller);
99
- }
100
- catch (error) {
101
- return (await import("./host-caller.js")).hostFailure(error, caller, "web");
102
- }
103
- },
104
- async head(request, env, execution) {
105
- try {
106
- const path = new URL(request.url).pathname, entry = match(path);
107
- if (!entry)
108
- return "";
109
- const ctx = await context(request, env, execution), row = await read(entry, ctx);
110
- if (!row)
111
- return "";
112
- const canonical = validUrl(entry.path, row, entry.page.view);
113
- if (!canonical)
114
- return "";
115
- return (await import("./web-head.js")).headTags(canonical, row, !!entry.page.markdown, options.locales);
116
- }
117
- catch (error) {
118
- if (error instanceof ContractError && error.code === "HOST_CONTRACT")
119
- throw error;
120
- return "";
121
- }
34
+ if (request.method !== "GET" && request.method !== "HEAD")
35
+ return;
36
+ const path = new URL(request.url).pathname;
37
+ if (path !== "/llms.txt" && path !== "/sitemap.xml" && !path.endsWith(".md"))
38
+ return;
39
+ return (served ?? await serve()).fetch(request, env, execution);
122
40
  },
41
+ head: async (request, env, execution) => (served ?? await serve()).head(request, env, execution),
123
42
  };
124
43
  }
@@ -1,10 +1,4 @@
1
1
  const COMPARE = { gt: ">", gte: ">=", lt: "<", lte: "<=" };
2
- /**
3
- * `column` is already quoted. `bind` registers one parameter and returns its placeholder. `insensitive` folds both sides of
4
- * eq/ne/in/notIn with LOWER (text only) and picks the dialect's case-insensitive LIKE. A null eq/ne is IS [NOT] NULL.
5
- * `like` takes a finished pattern (`escapeLike` the literal parts).
6
- * @internal
7
- */
8
2
  export function condition(d, column, operator, value, bind, insensitive = false) {
9
3
  const lhs = insensitive ? `LOWER(${column})` : column;
10
4
  const rhs = (v) => insensitive ? `LOWER(${bind(v)})` : bind(v);
@@ -1,5 +1,4 @@
1
1
  import { userReferences } from "../domain/userReferences.js";
2
- /** Selected application bindings: compiled checks/native SQL; only the app depends on Admin adapters. */
3
2
  import { isPrimaryCollection } from "../build/compiler.js";
4
3
  import { enumOptions } from "../build/compiler.js";
5
4
  import { databaseImport, isSqlite, literal as q } from "../domain/entry.js";
@@ -14,7 +13,6 @@ const wire = (fields, sqlite) => {
14
13
  const geo = fields.filter(([, type]) => type === "geo").map(([field]) => `row[${q(field)}]=row[${q(field + "_lat")}] == null || row[${q(field + "_lng")}] == null ? null : {lat:row[${q(field + "_lat")}],lng:row[${q(field + "_lng")}]};delete row[${q(field + "_lat")}];delete row[${q(field + "_lng")}];`);
15
14
  return `(raw:Row):Row=>{const row=${columns.length ? `wireRows([raw],${q(columns)})[0]!` : geo.length ? "{...raw}" : "raw"};${geo.join("")}return row;}`;
16
15
  };
17
- /** Core's titled string choices are enum checks; constrained or general oneOf remains refused. */
18
16
  function adminProperty(property) {
19
17
  const choices = enumOptions(property);
20
18
  if (!property.oneOf || property.enum || !choices || !property.oneOf.every(branch => Object.entries(branch).every(([key, value]) => key === "const" || key === "title" || key === "type" && value === "string")) || new Set(choices.map(c => c.value)).size !== choices.length)
@@ -30,19 +28,31 @@ const allowed = (requires, path) => {
30
28
  const lines = authLines(requires, path);
31
29
  return lines.length ? `(caller:Caller)=>{try{${lines.join("")}return true;}catch{return false;}}` : "(_caller:Caller)=>true";
32
30
  };
33
- /** Share repeated compiler SQL constants; generated modules evaluate no decoder or kernel at import. */
34
- function sqlConstants(value) {
35
- const counts = new Map();
36
- const visit = (v) => { if (typeof v === "string" && v.length > 80)
37
- counts.set(v, (counts.get(v) ?? 0) + 1);
38
- else if (Array.isArray(v))
39
- v.forEach(visit);
40
- else if (v && typeof v === "object")
41
- Object.values(v).forEach(visit); };
42
- visit(value);
43
- const pool = new Map([...counts].filter(([, count]) => count > 1).map(([text], n) => [text, `sql${n}`]));
44
- const literal = (v) => typeof v === "string" ? pool.get(v) ?? q(v) : Array.isArray(v) ? "[" + v.map(literal).join(",") + "]" : v && typeof v === "object" ? "{" + Object.entries(v).map(([name, child]) => `${name === "__proto__" ? `[${q(name)}]` : q(name)}:${literal(child)}`).join(",") + "}" : q(v);
45
- return { code: literal(value), constants: [...pool].map(([text, name]) => `const ${name}=${q(text)};`) };
31
+ function sqlConstants(values) {
32
+ const counts = new Map(), seen = new Map(), pool = new Map(), constants = [];
33
+ let named = 0;
34
+ const key = (v) => typeof v === "string" ? v.length > 80 ? q(v) : undefined : v && typeof v === "object" ? (text => text.length > 80 ? text : undefined)(q(v)) : undefined;
35
+ const children = (v) => v && typeof v === "object" ? Object.values(v) : [];
36
+ const tally = (v, by = 1) => { const k = key(v); if (k) {
37
+ counts.set(k, (counts.get(k) ?? 0) + by);
38
+ seen.set(k, v);
39
+ } children(v).forEach(child => tally(child, by)); };
40
+ values.forEach(value => children(value).forEach(child => tally(child)));
41
+ for (const [k, v] of [...seen].sort(([a], [b]) => b.length - a.length)) {
42
+ const copies = counts.get(k);
43
+ if (copies > 1) {
44
+ pool.set(k, "");
45
+ children(v).forEach(child => tally(child, 1 - copies));
46
+ }
47
+ }
48
+ const print = (v) => Array.isArray(v) ? "[" + v.map(reference).join(",") + "]" : v && typeof v === "object" ? "{" + Object.entries(v).map(([name, child]) => `${name === "__proto__" ? `[${q(name)}]` : q(name)}:${reference(child)}`).join(",") + "}" : q(v);
49
+ const reference = (v) => { const k = key(v); if (!k || pool.get(k) === undefined)
50
+ return print(v); if (pool.get(k) === "") {
51
+ const name = `sql${named++}`;
52
+ pool.set(k, name);
53
+ constants.push(`const ${name}=${print(v)}${typeof v === "string" ? "" : " as const"};`);
54
+ } return pool.get(k); };
55
+ return { code: values.map(print), constants };
46
56
  }
47
57
  export function emitAdminBindings(input, refuse) {
48
58
  if (!input.admin)
@@ -54,9 +64,7 @@ export function emitAdminBindings(input, refuse) {
54
64
  const schemas = [], views = [], adminViews = [], operations = [], pageSpecs = [];
55
65
  for (const [n, s] of Object.values(plan.schemas).entries()) {
56
66
  const schemaPath = `schemas/${s.name}/admin`, declared = Object.keys(s.fields).filter(f => f !== s.scope), writable = declared.filter(f => scalar(s.fields[f]) || s.fields[f] === "json");
57
- // readOnly properties are server-maintained: values() pins them to their default, and they have no Admin UPDATE.
58
67
  const server = Object.fromEntries(writable.filter(f => serverOwned(s.schema, f)).map(f => [f, Object.entries(s.schema.properties ?? {}).find(([k]) => k.toLowerCase() === f)?.[1]?.default ?? null]));
59
- // Experimental geo collections remain readable; edits require a declared Procedure.
60
68
  const validators = emitValidation({ views: {}, procedures: { full: { input: checks(s, true) }, partial: { input: checks(s, false) } } }, (path, message) => refuse(schemaPath + "/" + path, message));
61
69
  for (const mode of ["full", "partial"]) {
62
70
  const name = `validation/admin/${encodeURIComponent(s.name)}-${mode}.ts`;
@@ -85,9 +93,9 @@ export function emitAdminBindings(input, refuse) {
85
93
  const defaults = Object.fromEntries(Object.entries(checks(s, true).properties ?? {}).flatMap(([key, p]) => p.default !== undefined ? [[key, p.default]] : []));
86
94
  const primary = isPrimaryCollection(s, Object.values(plan.schemas));
87
95
  const schemaCode = `{
88
- name:${q(s.name)},primary:${primary},descriptor:allCollections[${q(s.name)}],publishing:${!!s.publishing},readOnly:${s.schema.readOnly === true || declared.some(f => s.fields[f] === "geo")},fields:names${n},defaults:${q(defaults)},
96
+ name:${q(s.name)},primary:${primary},descriptor:allCollections[${q(s.name)}],publishing:${!!s.publishing},${s.liveEdits ? "liveEdits:true," : ""}readOnly:${s.schema.readOnly === true || declared.some(f => s.fields[f] === "geo")},fields:names${n},defaults:${q(defaults)},
89
97
  binding:${entrySpec},
90
- query:query${n},validate:(data,full)=>{const checked=Object.fromEntries(Object.entries(data).filter(([key,v])=>v!==null||${q(s.schema.required ?? [])}.includes(key)));(full?full${n}:partial${n})(checked);${sqlite ? "" : "nativeText(data);"}${yearZero}return data;},
98
+ query:query${n},validate:(data,full)=>{const checked=Object.fromEntries(Object.entries(data).filter(([key,v])=>v!==null||${s.publishing ? "full&&" : ""}${q(s.schema.required ?? [])}.includes(key)));(full?full${n}:partial${n})(checked);${sqlite ? "" : "nativeText(data);"}${yearZero}return data;},
91
99
  values:values${n},row:${named},
92
100
  ${sqlite ? "" : "native:nativeText,"}
93
101
  ${s.publishing ? `transition:async(db,caller,verb,id,expectedVersion)=>{const run=await ({publish:()=>import("./procedures/${encodeURIComponent(s.name + ".publish")}.js"),unpublish:()=>import("./procedures/${encodeURIComponent(s.name + ".unpublish")}.js"),archive:()=>import("./procedures/${encodeURIComponent(s.name + ".archive")}.js")})[verb]();const version=wireRows([{version:expectedVersion}],[["version","int"]])[0]!.version;return run.default(db,caller,{id,expectedVersion:version} as Parameters<typeof run.default>[2]);},` : ""}
@@ -104,7 +112,8 @@ export function emitAdminBindings(input, refuse) {
104
112
  imports.push(`const viewNames${n}=${q(rename)} as const;`);
105
113
  const pageData = input.adminPages?.[name];
106
114
  if (!pageData) {
107
- refuse(`views/${name}/admin`, "Missing compiler-authorized matching pages.");
115
+ if (!input.adminPages)
116
+ refuse(`views/${name}/admin`, "Admin list was not generated: no compiler-authorized matching pages were supplied for this View.");
108
117
  continue;
109
118
  }
110
119
  const kinds = (v.uiSchema?.list ?? {});
@@ -121,7 +130,7 @@ export function emitAdminBindings(input, refuse) {
121
130
  const filterReads = Object.entries(filters).map(([field, s]) => { const type = [s.type].flat().find(t => t && t !== "null"); return `if(params.has(${q("filter." + field)})&&params.get(${q("filter." + field)})!=="")filters[${q(field)}]=${type === "boolean" ? `params.get(${q("filter." + field)})==="true"?true:params.get(${q("filter." + field)})==="false"?false:invalid("Expected true or false.",${q("views/" + name)})` : ["number", "integer"].includes(type ?? "") ? `Number(params.get(${q("filter." + field)}))` : `params.get(${q("filter." + field)})!`};`; });
122
131
  const cursorKinds = (input.paging?.[name]?.types ?? []).map(type => type === "bool" ? "bool" : type === "integer" || type === "int8" ? "int" : type === "int4" ? "int4" : type.startsWith("numeric(") ? "numeric" : type === "real" || type === "float8" ? "float" : type === "date" ? "date" : type === "timestamptz" ? "timestamp" : "text");
123
132
  const modes = Object.fromEntries(Object.entries({ caller: pageData.caller, trusted: pageData.trusted }).map(([mode, p]) => [mode, { ...p, binds: recipesOf(p, Object.keys(v.input?.properties ?? {}), message => refuse(`views/${name}/admin`, message)) }]));
124
- pageSpecs.push(`const viewPage${n}=${q({ path: "views/" + name, cap: v.stmts[0]?.SelectStmt.limitCount?.A_Const?.ival?.ival ?? 500, cursorKinds, ...modes })} as const;`);
133
+ pageSpecs.push({ path: "views/" + name, cap: v.stmts[0]?.SelectStmt.limitCount?.A_Const?.ival?.ival ?? 500, cursorKinds, ...modes });
125
134
  imports.push(`const allowsView${n}=${allowed(v.requires, `views/${name}`)};`);
126
135
  if (v.surface === "admin")
127
136
  adminViews.push(`view${n}`);
@@ -132,8 +141,8 @@ export function emitAdminBindings(input, refuse) {
132
141
  imports.push(`import {validate as operation${n}} from "./validation/procedures/${encoded}.js";`);
133
142
  operations.push(`{${input.description.targets.find(t => t._meta.id === `procedure:${name}`)?._meta.readOnly ? "readOnly:true," : ""}${user.used || p.trustedOnly ? `check:(caller)=>{${authLines(undefined, `procedures/${name}`, lowered.procedures[name]?.caller?.flatMap(s => s.binds), sqlite, p.trustedOnly).join("")}},` : ""}name:${q(name)},allows:${allowed(p.requires, `procedures/${name}`)},validate:operation${n},${converted ? `convert:${converted},` : ""}run:${ref ? `async(db,caller,input)=>(await import("./generated.js")).store(db,caller,{handlers:options.handlers}).procedures[${q(name)}](input)` : `async(db,caller,input)=>(await import("./procedures/${encoded}.js")).default(db,caller,input)`}}`);
134
143
  }
135
- const entryData = sqlConstants(input.adminEntries);
136
- imports.push(...views.map((view, n) => `const view${n}=${view} satisfies View<Database>;`), ...pageSpecs, ...entryData.constants, `const entrySpecs=${entryData.code} as const;`);
144
+ const { code: [entrySpecs, ...pages], constants } = sqlConstants([input.adminEntries, ...pageSpecs]);
145
+ imports.push(...views.map((view, n) => `const view${n}=${view} satisfies View<Database>;`), ...constants, ...pages.map((page, n) => `const viewPage${n}=${page} as const;`), `const entrySpecs=${entrySpecs} as const;`);
137
146
  imports.push(`export const statisticsView=(db:Database,caller:Caller,collection:keyof typeof entrySpecs,args:{from:number;to:number;bucketMs:number})=>nativeStatistics(entrySpecs,adapter,db,caller,collection,args);`);
138
147
  const metadataGuards = [...Object.entries(plan.views).map(([name, v]) => `case ${q("view:" + name)}: return (${allowed(v.requires, `views/${name}`)})(caller);`), ...Object.entries(plan.procedures).map(([name, p]) => `case ${q("procedure:" + name)}: return (${allowed(p.requires, `procedures/${name}`)})(caller);`), ...Object.values(plan.schemas).filter(s => s.publishing).flatMap(s => ["publish", "unpublish", "archive"].map(verb => `case ${q("procedure:" + s.name + "." + verb)}: return isAdmin(caller);`))];
139
148
  files["admin-bindings.ts"] = `${imports.filter(Boolean).join("\n")}\nexport function adminBindings${refs ? "<const H extends Handlers>(options:{handlers:H & Record<Exclude<keyof H,keyof Handlers>,never>})" : "(_options?:{handlers?:Handlers})"}{\nreturn {metadata:{...bootstrap,accessTargets:()=>import("./access-targets.js").then(m=>m.targets),${plan.access ? `userFields:${q(plan.access.userFields)} as const,` : ""}allows:(target:import("@aotter/mantle/contracts").Target,caller:Caller)=>{switch(target.kind+":"+target.name){${metadataGuards.join("")}default:return target.kind==="schema";}}},adapter,schemas:[${schemas.join(",\n")}] satisfies readonly EntrySchema<Database>[],views:[${adminViews.join(",")}],developerViews:[${views.map((_, n) => `view${n}`).join(",")}],operations:[${operations.join(",\n")}] satisfies readonly Operation<Database,${refs ? "Handlers" : "never"}>[]};\n}\n`;
@@ -1,12 +1,5 @@
1
- /** `admin-client.ts`: the types Admin's data hooks bind to, for the application's browser-side `admin.tsx`. */
2
1
  import { BUILT_IN_NAV_GROUPS, mcpTools } from "../build/compiler.js";
3
2
  import { q } from "../domain/entry.js";
4
- /**
5
- * The compatibility test of the application's Admin MCP App (`node --test generated/admin-mcp-app.check.mjs`): it loads the App
6
- * `mantle bundle` built in the official MCP Apps host and fails on a network request, a console error or an exception. It needs
7
- * `playwright-core` (a devDependency) and Chrome: `npx playwright-core install chromium`, or `MANTLE_TEST_CHROMIUM=<executable>`.
8
- * Node strips the types of `admin-descriptors.ts` (Node 22.18 or newer).
9
- */
10
3
  const MCP_APP_TEST = `import { existsSync } from "node:fs";
11
4
  import { test } from "node:test";
12
5
  import { checkMcpApp } from "@aotter/mantle-admin/testing";
@@ -20,40 +13,29 @@ test("the Admin MCP App works in an MCP host", { timeout: 300_000 }, async () =>
20
13
  await checkMcpApp({ catalogue: mcpAppCatalogues.admin, html });
21
14
  });
22
15
  `;
23
- /** A Schema, View input or Procedure input as a closed object type; one that is only a `$ref` stays open. */
16
+ const nullable = (p) => p.nullable === true || [p.type].flat().includes("null") || (p.enum ?? []).includes(null);
24
17
  const closed = (typeOf, schema) => !schema ? "{}" : schema.properties || schema.type === "object" && !schema["$ref"] ? typeOf({ ...schema, additionalProperties: false }) : "{ readonly [key: string]: unknown }";
25
- /**
26
- * Types are what the routes take and return, not the Store's rows: an entry's `data` keeps declared field names, an input is
27
- * what the route validates, a View row is its wire row under the names the browser route gives its columns (an MCP App's
28
- * `tools/call` returns the SQL names; the hooks rename them with `rowNames`), with `true` when it is an admin MCP tool, else null, which
29
- * types `pages[].mcp.renders` (types only; nothing ships). The file imports `generated/*` as types only, so nothing of the
30
- * Worker enters the browser bundle, and it is refused in the Worker's startup graph (`mantle bundle`).
31
- */
32
18
  export function emitAdminClient(plan, typeOf) {
33
19
  const tools = mcpTools(plan, "admin"), hasTool = (kind, source) => tools.some(t => t.kind === kind && t.source === source);
34
20
  const views = Object.entries(plan.views).filter(([, v]) => v.surface === "admin").sort(([a], [b]) => a < b ? -1 : 1);
35
21
  const operations = Object.entries(plan.procedures).filter(([name]) => hasTool("procedure", name)).sort(([a], [b]) => a < b ? -1 : 1);
36
22
  const schemas = Object.values(plan.schemas).sort((a, b) => a.name < b.name ? -1 : 1);
37
- // the scope column belongs to the caller, so an entry's data has no such field
38
23
  const dataOf = (s) => {
39
- const scope = s.scope ? s.names[s.scope] ?? s.scope : undefined, { [scope ?? ""]: _scope, ...properties } = s.schema.properties ?? {};
40
- // a draft of a publishing collection is only partly validated, so none of its fields is certain to be there
24
+ const scope = s.scope ? s.names[s.scope] ?? s.scope : undefined, { [scope ?? ""]: _scope, ...declared } = s.schema.properties ?? {};
25
+ const certain = (name) => !s.publishing && s.schema.required?.includes(name);
26
+ const properties = Object.fromEntries(Object.entries(declared).map(([name, p]) => [name, certain(name) || nullable(p) ? p : { ...p, nullable: true }]));
41
27
  const required = s.publishing ? [] : s.schema.required?.filter(f => f !== scope);
42
28
  const schema = { ...s.schema, properties, ...(required ? { required } : {}) };
43
29
  return s.translates ? typeOf(schema) : closed(typeOf, schema);
44
30
  };
45
- // the names the browser route gives a View's columns where they differ from the SQL output names, as it applies them
46
31
  const renames = views.map(([, v]) => Object.fromEntries(Object.entries(v.columns ?? {}).flatMap(([key, c]) => {
47
32
  const named = key === c.field ? plan.schemas[c.schema].names[c.field] ?? key : key;
48
33
  return named !== key ? [[key, named]] : [];
49
34
  })));
50
- // every sidebar group an admin Schema or View places an item in; `defineAdmin` must declare the ones Admin does not have
51
- // (the generate-time check of `uiSchema.nav.group` is this type)
52
35
  const placed = (ui) => { const group = ui?.["nav"]?.group; return typeof group === "string" ? [group] : []; };
53
36
  const used = [...new Set([...schemas.flatMap(s => placed(s.uiSchema)), ...views.flatMap(([, v]) => placed(v.uiSchema))])].sort();
54
37
  const custom = used.filter(group => !BUILT_IN_NAV_GROUPS.has(group));
55
38
  const union = (names) => names.length ? names.map(name => q(name)).join(" | ") : "never";
56
- // whether an admin View is an MCP tool, which a page declared for an MCP App (`pages[].mcp.renders`) must have
57
39
  const toolOf = (name) => hasTool("view", name) || null;
58
40
  const rows = views.map(([name], n) => `import type { WireRow as View${n}Wire } from "./views/${encodeURIComponent(name)}.js";`);
59
41
  const sql = operations.some(([, p]) => "sql" in p.handler);
@@ -1,4 +1,3 @@
1
- /** Build-time projections only; generated exports are static, independently tree-shakable data. */
2
1
  import { isPrimaryCollection, mcpTools, mcpToolNameSegment, toolTitle, collectionOf, developerConsole, rowPreconditions, appCatalog, entrySchema } from "../build/compiler.js";
3
2
  import { q } from "../domain/entry.js";
4
3
  import { advertised } from "./validation.js";
@@ -9,7 +8,6 @@ export function emitAdminDescriptors({ plan, admin = false }, diagnostics) {
9
8
  const placed = [...schemaList.map(s => ["schemas/" + s.name, s.uiSchema, s]), ...Object.entries(plan.views).map(([name, v]) => ["views/" + name, v.uiSchema, null])];
10
9
  for (const [path, ui, s] of placed) {
11
10
  const nav = (ui?.["nav"] ?? {}), group = nav["group"];
12
- // a child folded under its parent, or a translation on its parent's language tabs, has no sidebar entry to place
13
11
  if (s && !isPrimaryCollection(s, schemaList))
14
12
  for (const key of ["group", "order", "hidden"])
15
13
  if (nav[key] !== undefined)
@@ -46,7 +44,6 @@ export function emitAdminDescriptors({ plan, admin = false }, diagnostics) {
46
44
  const target = plan.procedures[t.source].target;
47
45
  return target ? [[t.name, { path: "/admin/c/" + encodeURIComponent(plan.schemas[target.schema.toLowerCase()]?.name ?? target.schema), entry: true }]] : [];
48
46
  })) };
49
- // These are the actual generated transitions, not authored ref handlers or SQL IR.
50
47
  for (const schema of schemas.filter(s => s.publishing))
51
48
  for (const [verb] of publishingTransitions) {
52
49
  const name = schema.name + "." + verb, tool = mcpToolNameSegment(verb + "_" + schema.name);
@@ -67,12 +64,9 @@ export function emitAdminDescriptors({ plan, admin = false }, diagnostics) {
67
64
  snapshot.interfaces.callable.push({ kind: "procedure", name: tool, target: name, surface: "admin", audience: "admin", title: null, description, input: publishingInput, output: null, trigger: null });
68
65
  }
69
66
  const data = { allCollections, views, operations, fieldNames: Object.fromEntries(schemas.map(s => [s.name, s.names])), viewNames, authorizations, developerConsole: snapshot, mcpAppCatalogues: catalogues, webmcp };
70
- return { "admin-descriptors.ts": "// Generated static Admin metadata. Import only the exports the application uses.\n" + literals(data) + "\nexport const collections = [" + schemas.filter(s => !s.translates).map(s => "allCollections[" + q(s.name) + "]").join(",") + "] as const;\nexport const bootstrap = {collections, allCollections, operations, views, webmcp, developerConsole} as const;\n" };
67
+ return { "admin-descriptors.ts": "// Generated static Admin metadata. Import only the exports the application uses.\n" + literals(data) + "\nexport const collections = [" + schemas.filter(s => !s.translates).map(s => "allCollections[" + q(s.name) + "]").join(",") + "] as const;\nexport const bootstrap = {collections, allCollections, operations, views, webmcp, developerConsole: () => import(\"./admin-developer-console.js\").then(m => m.developerConsole)} as const;\n",
68
+ "admin-developer-console.ts": "// Generated developer snapshot, loaded by bootstrap.developerConsole.\n" + literals({ developerConsole: snapshot }) + "\n" };
71
69
  }
72
- /**
73
- * Each export as a JSON literal; a subtree of 40 or more bytes that would be printed more than once is printed once, as a
74
- * module-private constant the exports reference (a bundler keeps only those an imported export reaches).
75
- */
76
70
  function literals(exports) {
77
71
  const data = JSON.parse(q(exports)), count = new Map(), hoisted = new Map(), constants = [];
78
72
  const each = (value, visit) => { if (value && typeof value === "object")
@@ -83,7 +77,6 @@ function literals(exports) {
83
77
  count.set(json, (count.get(json) ?? 0) + by); each(value, (json, child) => tally(json, child, by)); };
84
78
  for (const value of Object.values(data))
85
79
  each(value, tally);
86
- // Longest first: once a subtree is printed once, what it contains occurs (copies - 1) times fewer per occurrence in it.
87
80
  for (const json of [...count.keys()].sort((a, b) => b.length - a.length)) {
88
81
  const copies = count.get(json);
89
82
  if (copies > 1) {
@@ -1,6 +1,5 @@
1
1
  import { userReferences } from "../domain/userReferences.js";
2
- /** Canonical lowered programs become ordinary target-specific TypeScript, never a runtime plan. */
3
- import { parseNumeric } from "../build/compiler.js";
2
+ import { parseNumeric, tableOutputs } from "../build/compiler.js";
4
3
  import { boundaryType, nativeType, outputType, wireType } from "../domain/columns.js";
5
4
  import { databaseImport, driverFor, entry, exportName, generatedFiles, isSqlite as sqlite, q } from "../domain/entry.js";
6
5
  import { authLines as auth } from "../domain/input.js";
@@ -62,11 +61,10 @@ export function generate(input) {
62
61
  if (!bind || bind.k !== "input")
63
62
  return `readonly ${q(name)}${schema.required?.includes(name) ? "" : "?"}: ${typeOf(field)};`;
64
63
  const type = String(bind.type);
65
- const driverType = type === "json" || type === "date" ? "string" : type === "timestamptz" ? "string | Date" : isSqlite
64
+ const driverType = type === "json" ? typeOf(field) : type === "date" ? "string" : type === "timestamptz" ? "string | Date" : isSqlite
66
65
  ? type === "bool" || parseNumeric(type) ? "number" : typeOf(field)
67
66
  : parseNumeric(type) ? "number | string" : typeOf(field);
68
67
  const nullable = field.nullable === true || Array.isArray(field.type) && field.type.includes("null") || !schema.required?.includes(name);
69
- // D1 cannot bind undefined. Bound fields are explicit, null represents absence.
70
68
  return `readonly ${q(name)}: ${driverType}${nullable ? " | null" : ""};`;
71
69
  });
72
70
  return `{ ${properties.join(" ")} ${schema.additionalProperties !== false ? "readonly [key:string]: unknown;" : ""} }`;
@@ -77,18 +75,18 @@ export function generate(input) {
77
75
  refuse(path, "Input properties must have unique case-insensitive names. Rename input properties to be unique without regard to case.");
78
76
  return fields;
79
77
  };
80
- // The dialect functions the bind recipes call, so a bundle carries only those (`binds` takes this object).
81
78
  const values = new Set();
82
- /** A statement as a constant: its text and how to bind it. */
83
79
  const step = (s, path, fields, row = false) => {
84
- const recipes = recipesOf(s, fields, message => refuse(path, message));
85
- for (const r of recipes) {
80
+ const recipes = recipesOf(s, fields, message => refuse(path, message)), probe = row && s.probe ? recipesOf(s.probe, fields, message => refuse(path, message)) : [];
81
+ for (const r of [...recipes, ...probe]) {
86
82
  if (r.k === "now" || r.k === "cutoff")
87
83
  values.add(r.k);
88
84
  else if (r.k === "input" && r.type)
89
85
  values.add(r.type === "date" ? "date" : "timestamp");
90
86
  }
91
- return `{ sql: ${q(s.sql)}, binds: ${q(recipes)}${row ? ", row: true" : ""} }`;
87
+ const locks = Object.fromEntries((plan.schemas[s.schema ?? ""]?.locks ?? []).filter(l => l.on.includes(s.verb)).map(l => [l.name, l.message]));
88
+ const lock = row && s.probe ? `, lock: ${q({ sql: s.probe.sql, binds: probe, schema: plan.schemas[s.schema].name, verb: s.verb, locks })}` : "";
89
+ return `{ sql: ${q(s.sql)}, binds: ${q(recipes)}${row ? ", row: true" : s.onNoRows ? ', row: "some"' : ""}${lock} }`;
92
90
  };
93
91
  const declarations = [], views = [], procedures = [], refProcedures = [];
94
92
  const entries = {};
@@ -109,15 +107,14 @@ export function generate(input) {
109
107
  refuse(path, "Dynamic or repeated View outputs need an explicit generated row contract. Give each SELECT output a unique explicit alias.");
110
108
  continue;
111
109
  }
112
- const output = [];
110
+ const output = [], direct = tableOutputs(view, plan.schemas);
113
111
  for (const key of keys) {
114
- const type = outputType(plan, name, key);
115
- // Declared timestamps and dates arrive formatted by the SQL itself (domain/wireScalars).
116
- output.push(`readonly ${q(key)}: ${nativeType(type, isSqlite)};`);
112
+ const type = outputType(plan, name, key) ?? direct[key]?.type;
113
+ output.push(`readonly ${q(key)}: ${nativeType(type, isSqlite, direct[key]?.notNull)};`);
117
114
  }
118
115
  const fieldNames = inputFields(view.input, path + "/input");
119
116
  const callerStep = step(c, path, fieldNames), trustedStep = mode === "caller" ? step(trusted, path, fieldNames) : callerStep;
120
- declarations.push(`export type View${index}Input = ${queryInputType(view.input ?? { type: "object", additionalProperties: false }, [c, ...(trusted ? [trusted] : [])])};`, `export type View${index}Row = { ${output.join(" ")} };`, `export type View${index}WireRow = { ${keys.map(key => `readonly ${q(key)}: ${boundaryType(plan, name, key) ? wireType(boundaryType(plan, name, key)) : `View${index}Row[${q(key)}]`};`).join(" ")} };`, `const ${fn}Caller = ${callerStep} as const;`, ...(trustedStep !== callerStep ? [`const ${fn}Trusted = ${trustedStep} as const;`] : []), `async function ${fn}(db: ${isSqlite ? "Database" : "Unsafe"}, caller: Caller, raw: View${index}Input): Promise<View${index}Row[]> {`, ...auth(view.requires, path), ...(user.used ? [' USER.modeOf(caller);'] : []), ` const m = ${trustedStep !== callerStep ? `${user.used ? 'USER.modeOf(caller) === "trusted"' : 'caller.kind === "system"'} ? ${fn}Trusted : ` : ""}${fn}Caller;`, ` return DRIVER.read(db, ${user.used ? "USER.scoped(m.sql, caller)" : "m.sql"}, binds(D, m.binds, caller, raw));`, "}");
117
+ declarations.push(`export type View${index}Input = ${queryInputType(view.input ?? { type: "object", additionalProperties: false }, [c, ...(trusted ? [trusted] : [])])};`, `export type View${index}Row = { ${output.join(" ")} };`, `export type View${index}WireRow = { ${keys.map(key => `readonly ${q(key)}: ${boundaryType(plan, name, key) ? wireType(boundaryType(plan, name, key), direct[key]?.notNull) : `View${index}Row[${q(key)}]`};`).join(" ")} };`, `const ${fn}Caller = ${callerStep} as const;`, ...(trustedStep !== callerStep ? [`const ${fn}Trusted = ${trustedStep} as const;`] : []), `async function ${fn}(db: ${isSqlite ? "Database" : "Unsafe"}, caller: Caller, raw: View${index}Input): Promise<View${index}Row[]> {`, ...auth(view.requires, path), ...(user.used ? [' USER.modeOf(caller);'] : []), ` const m = ${trustedStep !== callerStep ? `${user.used ? 'USER.modeOf(caller) === "trusted"' : 'caller.kind === "system"'} ? ${fn}Trusted : ` : ""}${fn}Caller;`, ` return DRIVER.read(db, ${user.used ? "USER.scoped(m.sql, caller)" : "m.sql"}, binds(D, m.binds, caller, raw));`, "}");
121
118
  views.push(`[${q(name)}]: ${fn}`);
122
119
  declarations.push(`export { ${fn} as ${exportName("view", name)} };`);
123
120
  entries[`views/${encodeURIComponent(name)}.ts`] = entry("view", name, `View${index}Input as Input, View${index}Row as Row, View${index}WireRow as WireRow`);
@@ -132,7 +129,7 @@ export function generate(input) {
132
129
  continue;
133
130
  }
134
131
  if ([...modes.caller, ...modes.trusted].some(s => s.publish || s.schema && plan.schemas[s.schema]?.publishing))
135
- refuse(path, "Publishing writes are generated: use the Store and the `<schema>.publish` / `.unpublish` / `.archive` transitions.");
132
+ refuse(path, "A SQL Procedure cannot write a publishing Schema: create and edit its rows from a ref handler through the Store (`ctx.store.collection(...)`), and change status with the generated `<schema>.publish` / `.unpublish` / `.archive` transitions.");
136
133
  const fieldNames = inputFields(procedure.input, path + "/input");
137
134
  const steps = (statements) => `[${statements.map(s => step(s, path, fieldNames, s.kind === "row")).join(", ")}] as const`;
138
135
  const same = steps(modes.caller) === steps(modes.trusted);
@@ -142,8 +139,8 @@ export function generate(input) {
142
139
  entries[`procedures/${encodeURIComponent(name)}.ts`] = entry("procedure", name, `Procedure${index}Input as Input`, `export type { Output } from "${base}contracts";\n`);
143
140
  }
144
141
  emitTtl(plan, isSqlite, declarations, procedures, entries, refuse);
145
- emitPublishing(plan, isSqlite, declarations, procedures, entries, values, refuse);
146
- const stores = emitStores(plan.schemas, isSqlite, newId, refuse, hasHandlers);
142
+ emitPublishing(plan, isSqlite, declarations, procedures, entries, values, refuse, input.locks);
143
+ const stores = emitStores(plan.schemas, isSqlite, newId, refuse, hasHandlers, input.locks);
147
144
  const surfaces = { ...emitPaging(plan, lowered, target, refuse, paging), ...emitPaging(plan, input.wireLowered ?? lowered, target, refuse, paging, true), ...emitValidation(plan, refuse), ...emitRest(plan, lowered, target, input.description), ...emitWeb(plan, lowered, target), ...emitMcp(plan, lowered, target, input.description, refuse, !!site), ...emitAdminDescriptors({ plan, admin }, diagnostics), ...emitAdminBindings(input, refuse), ...(admin ? emitAdminClient(plan, typeOf) : {}), ...emitSite(target, site, refuse, Object.keys(plan.schemas)), ...emitTranslations(plan, target) };
148
145
  if (diagnostics.length)
149
146
  return { ok: false, diagnostics };
@@ -2,18 +2,15 @@ import { authLines as auth } from "../domain/input.js";
2
2
  import { q } from "../domain/entry.js";
3
3
  export function emitHandlers(plan, interactive, typeOf, declarations, procedures) {
4
4
  const refs = Object.entries(plan.procedures).sort().map(([name, procedure], index) => ({ name, procedure, index })).filter(({ procedure }) => "ref" in procedure.handler);
5
- // An empty contract keeps app wiring (`handlers: Handlers = {}`) identical whether or not any ref Procedure exists.
6
5
  if (!refs.length) {
7
6
  declarations.push("export interface Handlers {}");
8
7
  return [];
9
8
  }
10
- declarations.push('export type HandlerStore = Pick<ReturnType<typeof makeStore>, "collection" | "views" | "procedures">;',
11
- // D1 has no interactive transactions, so its handlers get no `transaction`: using one there fails typecheck.
9
+ declarations.push('export type HandlerStore = Pick<ReturnType<typeof makeStore>, "collection" | "batch" | "views" | "procedures">;',
12
10
  `export interface HandlerContext { readonly caller: Caller; readonly db: Database; readonly store: HandlerStore;${interactive ? " readonly transaction: <R>(fn: (ctx: { readonly store: HandlerStore; readonly db: Database }) => Promise<R>) => Promise<R>;" : ""} }`, "export interface Handlers {", ...refs.map(({ name, index }) => ` readonly [${q(name)}]: (ctx: HandlerContext, input: Procedure${index}Input) => Promise<Procedure${index}Output>;`), "}");
13
11
  for (const { name, procedure, index } of refs) {
14
12
  const path = `procedures/${name}`, fn = `procedure${index}`;
15
13
  declarations.push(`export type Procedure${index}Input = ${typeOf(procedure.input ?? { type: "object", additionalProperties: false })};`, `export type Procedure${index}Output = ${typeOf(procedure.output)};`, `async function ${fn}(db: Database, caller: Caller, raw: Procedure${index}Input, context: { handlers: Handlers; store: HandlerContext["store"] }): Promise<Procedure${index}Output> {`, ` if ("authenticatedAs" in caller && caller.authenticatedAs === "snapshot") (await import("@aotter/mantle/host-caller")).checkFresh(caller, false, ${q(path)});`, ...auth(procedure.requires, path), " const handlers = context?.handlers;", ` const handler = handlers && Object.hasOwn(handlers, ${q(name)}) ? handlers[${q(name)}] : undefined;`, ` if (!handler) throw new ContractError("HANDLER_NOT_REGISTERED", "Procedure handler is not registered.", ${q(path)});`,
16
- // TODO #1470: REST/MCP use shared generated validators at the boundary; bare calls stay typed.
17
14
  ` return handler({ caller, db, store: context.store${interactive ? ", transaction: handlerTransaction(context.store, db)" : ""} }, raw);`, "}");
18
15
  procedures.push(`[${q(name)}]: ${fn}`);
19
16
  }
@@ -1,7 +1,6 @@
1
1
  import { emitValidation } from "./validation.js";
2
2
  import { mediaDefinitions } from "../domain/generated-tools.js";
3
3
  export { mediaDefinitions };
4
- /** `definitions` are the versioned media tools, computed once by the MCP emitter. */
5
4
  export function emitMcpMedia(definitions, refuse) {
6
5
  const inputs = Object.fromEntries(definitions.map(tool => [tool.name, { input: tool.inputSchema }]));
7
6
  const checks = Object.fromEntries(Object.entries(emitValidation({ views: {}, procedures: inputs }, refuse)).map(([path, code]) => [path.replace("validation/procedures/", "validation/media/"), code]));