@aotter/mantle 0.2.0 → 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 (82) hide show
  1. package/README.md +6 -2
  2. package/dist/build/compiler.d.ts +4 -4
  3. package/dist/build/compiler.js +6 -3
  4. package/dist/build/spec/domain/model/ManifestGrammar.d.ts +39 -1
  5. package/dist/build/spec/domain/model/RuntimePlan.d.ts +16 -3
  6. package/dist/build/spec/domain/model/SqlIr.d.ts +30 -0
  7. package/dist/build/spec/kernel/diagnostic.d.ts +1 -1
  8. package/dist/domain/describe/chunk-P6PP6VVV.js +1 -0
  9. package/dist/domain/describe/index.d.ts +1 -0
  10. package/dist/domain/describe/index.js +1 -1
  11. package/dist/domain/describe/match.d.ts +19 -0
  12. package/dist/domain/describe/target-hash.js +1 -1
  13. package/dist/infrastructure/adminEntries.js +19 -3
  14. package/dist/infrastructure/bundle.js +43 -8
  15. package/dist/infrastructure/cli.js +1 -1
  16. package/dist/infrastructure/generate.js +17 -3
  17. package/dist/kernel/binds.d.ts +18 -3
  18. package/dist/kernel/binds.js +1 -1
  19. package/dist/kernel/contracts.d.ts +7 -0
  20. package/dist/kernel/contracts.js +9 -0
  21. package/dist/kernel/driver/bun-sqlite.js +3 -3
  22. package/dist/kernel/driver/d1.d.ts +1 -1
  23. package/dist/kernel/driver/d1.js +6 -3
  24. package/dist/kernel/driver/postgres.d.ts +2 -1
  25. package/dist/kernel/driver/postgres.js +11 -6
  26. package/dist/kernel/mcp-handler.js +2 -1
  27. package/dist/kernel/rest-router.js +3 -5
  28. package/dist/kernel/rest.d.ts +2 -1
  29. package/dist/kernel/rest.js +1 -0
  30. package/dist/kernel/route-pattern.d.ts +26 -0
  31. package/dist/kernel/route-pattern.js +34 -0
  32. package/dist/kernel/store.d.ts +15 -1
  33. package/dist/kernel/store.js +51 -18
  34. package/dist/usecase/adminBindings.js +2 -2
  35. package/dist/usecase/adminClient.js +4 -1
  36. package/dist/usecase/generate.js +7 -5
  37. package/dist/usecase/handlers.js +1 -1
  38. package/dist/usecase/publishing.js +20 -7
  39. package/dist/usecase/rest.js +5 -5
  40. package/dist/usecase/stores.js +8 -3
  41. package/docs/examples/README.md +1 -1
  42. package/docs/examples/app-shell/README.md +58 -10
  43. package/docs/examples/app-shell/package.json +6 -6
  44. package/docs/examples/club-events/manifests/club-events.yaml +2 -0
  45. package/docs/examples/generated-web/seed.sql +5 -4
  46. package/docs/examples/purchasing/manifests/purchasing.yaml +3 -0
  47. package/docs/handbook/cloudflare/media-r2.md +6 -3
  48. package/docs/handbook/cloudflare/service-entry.md +2 -0
  49. package/docs/handbook/concepts/lifecycle-and-locales.md +21 -1
  50. package/docs/handbook/examples/hub.md +1 -1
  51. package/docs/handbook/guides/admin-ui.md +1 -1
  52. package/docs/handbook/guides/agent-setup.md +13 -8
  53. package/docs/handbook/guides/agent-workflow-acceptance.md +129 -19
  54. package/docs/handbook/guides/local-testing.md +50 -5
  55. package/docs/handbook/guides/native-admin.md +218 -11
  56. package/docs/handbook/guides/native-execution.md +5 -4
  57. package/docs/handbook/guides/revisions.md +584 -0
  58. package/docs/handbook/guides/size-and-startup.md +140 -0
  59. package/docs/handbook/guides/sql-patterns.md +47 -1
  60. package/docs/handbook/guides/typed-queries.md +8 -3
  61. package/docs/handbook/navigation.json +4 -0
  62. package/docs/handbook/reference/authorization.md +2 -0
  63. package/docs/handbook/reference/describe.md +42 -0
  64. package/docs/handbook/reference/diagnostics.md +12 -3
  65. package/docs/handbook/reference/procedure.md +4 -4
  66. package/docs/handbook/reference/schema.md +71 -7
  67. package/docs/handbook/reference/sql.md +2 -1
  68. package/docs/handbook/reference/surface.md +3 -2
  69. package/docs/handbook/reference/trigger.md +12 -0
  70. package/docs/handbook/releases/index.md +44 -1
  71. package/docs/handbook/start/agents.md +6 -2
  72. package/docs/handbook/start/project-and-cli.md +4 -0
  73. package/docs/handbook/start/quickstart-worker.md +1 -1
  74. package/docs/skills/develop/SKILL.md +15 -3
  75. package/docs/skills/integrate/SKILL.md +3 -1
  76. package/docs/skills/performance/SKILL.md +169 -0
  77. package/docs/skills/update/SKILL.md +9 -5
  78. package/docs/upgrade-0.1-to-0.2.md +151 -39
  79. package/package.json +9 -9
  80. package/skills/README.md +6 -2
  81. package/skills/mantle/SKILL.md +19 -5
  82. package/dist/domain/describe/chunk-SGMRVDGE.js +0 -1
@@ -1,4 +1,4 @@
1
- import { checkConflict, conflict, ContractError } from "../contracts.js";
1
+ import { checkConflict, conflict, ContractError, lockConflict } from "../contracts.js";
2
2
  import { unique } from "../dialect/sqlite.js";
3
3
  export { definitive } from "../dialect/sqlite.js";
4
4
  import { context, guard } from "./transaction-context.js";
@@ -30,8 +30,8 @@ export async function procedure(db, steps, path) {
30
30
  catch (error) {
31
31
  throw unique(error) ? conflict(path, true, error) : checkConflict(path, error) ?? error;
32
32
  }
33
- if (s.row && changes !== 1)
34
- throw conflict(path, false);
33
+ if (s.row && (changes < 1 || changes > 1 && s.row !== "some"))
34
+ throw s.row === true && s.lock ? lockConflict(path, s.lock, handle.query(s.lock.sql).all(...s.lock.binds)) : conflict(path, false);
35
35
  return rows;
36
36
  });
37
37
  return available(db, () => transactions.has(db) || steps.length === 1 ? execute(db) : db.transaction(execute)(db));
@@ -20,5 +20,5 @@ export interface Handle extends Reader {
20
20
  }[]>;
21
21
  }
22
22
  export declare const read: (db: Reader, sql: string, binds: unknown[]) => Promise<any[]>;
23
- /** The assert table's trigger aborts the whole batch with `CONFLICT op=<k>` when statement k did not change exactly one row. */
23
+ /** The assert table's trigger aborts the whole batch with `CONFLICT op=<k>` when statement k did not change exactly one row (at least one for `row: "some"`). */
24
24
  export declare function procedure(db: Handle, steps: readonly Bound[], path: string): Promise<Row[][]>;
@@ -1,4 +1,4 @@
1
- import { checkConflict, conflict } from "../contracts.js";
1
+ import { checkConflict, conflict, lockConflict } from "../contracts.js";
2
2
  import { unique } from "../dialect/sqlite.js";
3
3
  export { definitive } from "../dialect/sqlite.js";
4
4
  export const read = async (db, sql, binds) => (await db.prepare(sql).bind(...binds).all()).results;
@@ -8,14 +8,17 @@ export async function procedure(db, steps, path) {
8
8
  at.push(batch.length);
9
9
  batch.push(db.prepare(s.sql).bind(...s.binds));
10
10
  if (s.row)
11
- batch.push(db.prepare(`INSERT INTO _mantle_assert (op,ok) SELECT ${i}, changes() = 1`));
11
+ batch.push(db.prepare(`INSERT INTO _mantle_assert (op,ok) SELECT ${i}, changes() ${s.row === "some" ? "> 0" : "= 1"}`));
12
12
  });
13
13
  let out;
14
14
  try {
15
15
  out = await db.batch(batch);
16
16
  }
17
17
  catch (error) {
18
- throw unique(error) ? conflict(path, true, error) : /CONFLICT op=\d/.test(String(error?.message)) ? conflict(path, false, error) : checkConflict(path, error) ?? error;
18
+ const op = /CONFLICT op=(\d+)/.exec(String(error?.message)), step = op ? steps[Number(op[1])] : undefined, probe = step?.row === true ? step.lock : undefined;
19
+ if (probe)
20
+ throw lockConflict(path, probe, await read(db, probe.sql, probe.binds));
21
+ throw unique(error) ? conflict(path, true, error) : op ? conflict(path, false, error) : checkConflict(path, error) ?? error;
19
22
  }
20
23
  return at.map(i => out[i].results);
21
24
  }
@@ -16,7 +16,8 @@ export declare function definitive(error: unknown): boolean;
16
16
  */
17
17
  export declare function postgresOutcome(error: unknown, path: string): unknown;
18
18
  /**
19
- * One statement is atomic on its own (a row statement touches at most one row, so a failed count check changes nothing)
19
+ * One statement is atomic on its own (a row statement touches at most one row, and a `"some"` one fails only when it wrote
20
+ * none, so a failed count check changes nothing)
20
21
  * and runs in autocommit. Several run in `begin(serializable)`: a failure before the callback returns was rolled back;
21
22
  * only a failed COMMIT can leave the outcome unknown.
22
23
  */
@@ -1,4 +1,4 @@
1
- import { conflict, ContractError, schemaCheckFailed } from "../contracts.js";
1
+ import { conflict, ContractError, lockConflict, schemaCheckFailed } from "../contracts.js";
2
2
  import { context, guard } from "./transaction-context.js";
3
3
  const transactions = new WeakSet();
4
4
  const ISOLATION = ["serializable", "repeatable read", "read committed", "read uncommitted"];
@@ -15,18 +15,23 @@ export function definitive(error) {
15
15
  const state = sqlstate(error);
16
16
  return typeof state === "string" && /^[0-9A-Z]{5}$/.test(state) && typeof error?.severity === "string" && !/^(08|28|53|57|58|XX)/.test(state);
17
17
  }
18
+ function serialization(error, path) {
19
+ const state = sqlstate(error);
20
+ if ((state === "40001" || state === "40P01") && definitive(error))
21
+ return new ContractError("CONFLICT", "A concurrent transaction prevented this write. Read again before deciding whether to retry.", path, { cause: error, fields: { kind: "serialization" } });
22
+ }
18
23
  export function postgresOutcome(error, path) {
19
24
  if (definitive(error))
20
- return error;
25
+ return serialization(error, path) ?? error;
21
26
  return new ContractError("OUTCOME_UNKNOWN", "The write outcome is unknown; reconcile before retrying.", path, { cause: error });
22
27
  }
23
28
  export async function procedure(db, steps, path) {
24
29
  const execute = async (handle) => {
25
30
  const results = [];
26
31
  for (const s of steps) {
27
- const { rows, changes } = await run(handle, s.sql, s.binds).catch(error => { throw sqlstate(error) === "23505" ? conflict(path, true, error) : sqlstate(error) === "23514" ? check(path, error) : error; });
28
- if (s.row && changes !== 1)
29
- throw conflict(path, false);
32
+ const { rows, changes } = await run(handle, s.sql, s.binds).catch(error => { throw sqlstate(error) === "23505" ? conflict(path, true, error) : sqlstate(error) === "23514" ? check(path, error) : serialization(error, path) ?? error; });
33
+ if (s.row && (changes < 1 || changes > 1 && s.row !== "some"))
34
+ throw s.row === true && s.lock ? lockConflict(path, s.lock, (await run(handle, s.lock.sql, s.lock.binds)).rows) : conflict(path, false);
30
35
  results.push(rows);
31
36
  }
32
37
  return results;
@@ -75,7 +80,7 @@ export async function transaction(db, fn, options = {}, path = "store/transactio
75
80
  });
76
81
  }
77
82
  catch (error) {
78
- throw error instanceof ContractError || !committing ? error : postgresOutcome(error, path);
83
+ throw serialization(error, path) ?? (error instanceof ContractError || !committing ? error : postgresOutcome(error, path));
79
84
  }
80
85
  finally {
81
86
  state.outer = undefined;
@@ -20,6 +20,7 @@ function observe(listener, event) {
20
20
  }
21
21
  const paging = (schema) => ({ ...schema, properties: { ...schema.properties, limit: { type: "integer", minimum: 1, maximum: 500 }, cursor: { type: "string" } } });
22
22
  const MCP_PAGE_BUDGET = 60_000;
23
+ const MCP_INPUT_ELEMENTS = 10_000;
23
24
  const union = (scopes, extra) => [...new Set([...scopes, ...extra])].join(" ");
24
25
  export function handler(tools, options, apps = {}, reads = []) {
25
26
  const allowed = options.allowedOrigins ?? [];
@@ -207,7 +208,7 @@ export function handler(tools, options, apps = {}, reads = []) {
207
208
  const serverInfo = { name: "aotter.mantle", ...options.serverInfo, version: options.serverInfo?.version ?? version };
208
209
  const handler = createMcpHandler(() => {
209
210
  const schema = (value) => fromJsonSchema(value, { getValidator() { return (input) => ({ valid: true, data: input, errorMessage: undefined }); } });
210
- const server = new McpServer(serverInfo, options.instructions ? { instructions: options.instructions } : {});
211
+ const server = new McpServer(serverInfo, { maxToolInputElements: MCP_INPUT_ELEMENTS, ...(options.instructions ? { instructions: options.instructions } : {}) });
211
212
  server.server.registerCapabilities({ tools: { listChanged: false } });
212
213
  if (host)
213
214
  server.server.onclose = () => access?.finish(isRecord(parsedBody) && parsedBody.method === "initialize" ? undefined : new ContractError("INPUT_VALIDATION_FAILED", "MCP request refused.", "mcp", { status: 400 }));
@@ -2,6 +2,7 @@ import { forwarded, ContractError } from "./contracts.js";
2
2
  import { checkOrigin, surfaceCaller } from "./surface-failure.js";
3
3
  import { decode, invalid } from "./surface.js";
4
4
  import { draining } from "./drain.js";
5
+ import { canonicalPath } from "./route-pattern.js";
5
6
  const isApi = (path) => path === "/api" || path.startsWith("/api/");
6
7
  const fail = message => invalid(message, "rest");
7
8
  export function router(routes, options, send) {
@@ -14,11 +15,8 @@ export function router(routes, options, send) {
14
15
  if (isForwarded)
15
16
  caller = (await import("./host-caller.js")).gatewayCaller(ctx, options.gateway, options.fields);
16
17
  const original = new URL(request.url).pathname;
17
- let path;
18
- try {
19
- path = original.split("/").map(segment => encodeURIComponent(decodeURIComponent(segment))).join("/");
20
- }
21
- catch {
18
+ const path = canonicalPath(original);
19
+ if (path === undefined) {
22
20
  if (!isApi(original))
23
21
  return undefined;
24
22
  fail("Malformed request path.");
@@ -1,6 +1,8 @@
1
1
  /** REST request handling for a generated route table: matching, body and parameter decoding, dispatch and errors. */
2
2
  import type { Caller, Target, Authorize, Gateway, RequestContext, GatewayOptions } from "./contracts.js";
3
3
  import { type Conversions } from "./surface.js";
4
+ /** The routers' path grammar, for Admin's router: its mount, route patterns and the canonical request path. */
5
+ export { ADMIN_API_BASE, canonicalPath, routeSource } from "./route-pattern.js";
4
6
  type Paging = {
5
7
  limit?: number;
6
8
  cursor?: string;
@@ -55,4 +57,3 @@ export declare function rest<Env, Database, Handlers = never>(routes: readonly R
55
57
  export declare function nativeRest<Env, Database, Handlers = never>(routes: readonly Route<Database, Handlers, Env>[], options: Options<Env, Database> & {
56
58
  handlers?: Handlers;
57
59
  }): (request: Request, env: Env, ctx?: RequestContext) => Promise<Response | undefined>;
58
- export {};
@@ -1,5 +1,6 @@
1
1
  import { ContractError } from "./contract-error.js";
2
2
  import { invalid } from "./surface.js";
3
+ export { ADMIN_API_BASE, canonicalPath, routeSource } from "./route-pattern.js";
3
4
  const headers = { "cache-control": "private, no-store" };
4
5
  const fail = message => invalid(message, "rest");
5
6
  export async function body(request, limit = 1_000_000) {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The one path grammar of the generated REST router, the Admin router and describe's `matchTarget`: a route template's
3
+ * pattern, the request path it is tested against and REST precedence. Pure; no database, application or other module.
4
+ */
5
+ /** Where Admin serves its API. Admin route templates and `admin:` describe ids are relative to it. */
6
+ export declare const ADMIN_API_BASE = "/admin/api";
7
+ /**
8
+ * An http Trigger path as a describe template: `{name}` becomes `:name`. The compiler refuses other braces and
9
+ * `:`-prefixed segments in these paths, so `routePath` inverts it exactly.
10
+ */
11
+ export declare const pathTemplate: (path: string) => string;
12
+ /** A describe template as the route path the routers compile: `:name` segments become `{name}`. */
13
+ export declare const routePath: (template: string) => string;
14
+ /**
15
+ * A template's anchored pattern source and capture keys. A `{name}` segment captures one segment; with `suffix`,
16
+ * `{name*}` captures the rest of the path. Other segments, `:name` included, match literally in encoded form. One
17
+ * trailing slash is optional.
18
+ */
19
+ export declare function routeSource(template: string, base?: string, suffix?: boolean): {
20
+ source: string;
21
+ keys: string[];
22
+ };
23
+ /** The path the routers test: each segment decoded and re-encoded, so equivalent escapes match alike. Malformed escapes are undefined. */
24
+ export declare function canonicalPath(pathname: string): string | undefined;
25
+ /** REST precedence over route paths: fewer parameters, then a literal before a parameter from the left, then the path. */
26
+ export declare function routeOrder(a: string, b: string): number;
@@ -0,0 +1,34 @@
1
+ export const ADMIN_API_BASE = "/admin/api";
2
+ export const pathTemplate = (path) => path.replace(/\{([^}]+)\}/g, ":$1");
3
+ export const routePath = (template) => template.split("/").map(segment => segment.startsWith(":") ? `{${segment.slice(1)}}` : segment).join("/");
4
+ export function routeSource(template, base = "", suffix = false) {
5
+ const keys = [];
6
+ const path = template.split("/").map(segment => {
7
+ const rest = suffix ? /^\{([a-zA-Z]\w*)\*\}$/.exec(segment) : null, one = /^\{([^}]+)\}$/.exec(segment);
8
+ if (rest || one) {
9
+ keys.push((rest ?? one)[1]);
10
+ return rest ? "(.*)" : "([^/]+)";
11
+ }
12
+ return encodeURIComponent(segment).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
13
+ }).join("/");
14
+ return { source: `^${base}${path}/?$`, keys };
15
+ }
16
+ export function canonicalPath(pathname) {
17
+ try {
18
+ return pathname.split("/").map(segment => encodeURIComponent(decodeURIComponent(segment))).join("/");
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ }
24
+ const isParameter = (segment) => /^\{[^}]+\}$/.test(segment);
25
+ export function routeOrder(a, b) {
26
+ const x = a.split("/"), y = b.split("/");
27
+ const count = (segments) => segments.filter(isParameter).length;
28
+ if (count(x) !== count(y))
29
+ return count(x) - count(y);
30
+ for (let i = 0; i < Math.min(x.length, y.length); i++)
31
+ if (isParameter(x[i]) !== isParameter(y[i]))
32
+ return isParameter(x[i]) ? 1 : -1;
33
+ return a < b ? -1 : a > b ? 1 : 0;
34
+ }
@@ -30,6 +30,13 @@ export interface Rules {
30
30
  /** Core's new entry id as the dialect prints it (an SQL expression). */
31
31
  readonly newId: string;
32
32
  }
33
+ /** A Schema lock (#1822): while `sql` (Core's `when`, printed for the dialect) holds for the existing row, a caller's write on `on` is refused. */
34
+ export interface Lock {
35
+ readonly name: string;
36
+ readonly on: readonly ("update" | "delete")[];
37
+ readonly sql: string;
38
+ readonly message: string;
39
+ }
33
40
  /** What a Schema adds to the rules. The table is the collection's name. */
34
41
  export interface Fact {
35
42
  readonly u?: readonly Extract<Term, readonly [string, 'in', ...unknown[]]>[];
@@ -39,6 +46,8 @@ export interface Fact {
39
46
  readonly ttl?: readonly [column: string, seconds: number];
40
47
  /** Opt-in publishing lifecycle. */
41
48
  readonly pub?: 1;
49
+ /** The columns `publish` requires non-null, each to its declared field: an update of a published row cannot clear one (#1822). */
50
+ readonly req?: Readonly<Record<string, string>>;
42
51
  /** Declared date-time and date columns (`created_at` and `updated_at` are always date-times). */
43
52
  readonly ts?: readonly string[];
44
53
  readonly d?: readonly string[];
@@ -52,6 +61,8 @@ export interface Fact {
52
61
  readonly same: readonly (readonly [parent: string, child: string])[];
53
62
  readonly terms: readonly Term[];
54
63
  }[];
64
+ /** Schema locks; a trusted caller is not bound by them. */
65
+ readonly lock?: readonly Lock[];
55
66
  /** Generated parent policy and inherited read-only columns; no authored schema is interpreted. */
56
67
  readonly parent?: {
57
68
  readonly on: string;
@@ -66,6 +77,8 @@ export interface Fact {
66
77
  };
67
78
  }
68
79
  export declare const SCOPE = "$scope", TTL = "$ttl";
80
+ /** The probe's answer for an update that would leave a published row unpublishable; no lock name has a dot. */
81
+ export declare const REQUIRED = "publishing.required";
69
82
  /** A Schema's rules: the shared ones with its scope and TTL put in, and a term dropped where it has none. The generator proves this equals Core's for every Schema. */
70
83
  export declare function expand(r: Rules, f: Fact): {
71
84
  caller: Term[];
@@ -148,6 +161,7 @@ export interface TransactionOptions {
148
161
  }
149
162
  export interface Store<T extends Record<string, Table>> {
150
163
  collection<K extends keyof T & string>(name: K): Collection<T[K]>;
164
+ /** Atomic unused Writes from this Store. Inside a transaction, joins it; any rejection makes the outer transaction rollback-only, even if caught. */
151
165
  batch<const W extends readonly Write<unknown>[]>(writes: W): Promise<{
152
166
  -readonly [K in keyof W]: Awaited<W[K]>;
153
167
  }>;
@@ -158,7 +172,7 @@ type BoundOperations<O> = {
158
172
  [N in keyof O[K]]: O[K][N] extends (db: any, caller: Caller, input: infer I, ...args: any[]) => infer R ? (input: I) => R : never;
159
173
  };
160
174
  };
161
- type Tx<T extends Record<string, Table>, O> = Pick<Store<T>, "collection"> & BoundOperations<O>;
175
+ type Tx<T extends Record<string, Table>, O> = Store<T> & BoundOperations<O>;
162
176
  type Interactive<T extends Record<string, Table>, O> = {
163
177
  transaction<R>(fn: (tx: Tx<T, O>) => Promise<R>, options?: TransactionOptions): Promise<R>;
164
178
  };
@@ -6,6 +6,7 @@ import { condition } from "./where.js";
6
6
  import { decodeCursor, encodeCursor } from "./paging.js";
7
7
  export { decodeCursor, encodeCursor };
8
8
  export const SCOPE = "$scope", TTL = "$ttl";
9
+ export const REQUIRED = "publishing.required";
9
10
  const SYSTEM = ["id", "version", "created_at", "updated_at", "author_id", "status", "published_at", "_rid"];
10
11
  export function expand(r, f) {
11
12
  const all = (l) => l.flatMap(([c, ...t]) => (c = c === SCOPE ? f.s : c === TTL ? f.ttl?.[0] : c === "status" && !f.pub ? "" : c) ? [[c, ...t.map(v => v.k === "cutoff" ? { ...v, seconds: f.ttl[1] } : v)]] : []);
@@ -67,8 +68,8 @@ export function createStore(d, drv, rules, facts, operations) {
67
68
  p.policy.set(trusted, preparedPolicy);
68
69
  return preparedPolicy;
69
70
  };
70
- return ((db, caller, options) => make(db, caller, false, options));
71
- function make(db, caller, inTransaction = false, options) {
71
+ return ((db, caller, options) => make(db, caller, undefined, options));
72
+ function make(db, caller, transaction, options) {
72
73
  const pending = new WeakMap();
73
74
  const write = (build, project, path) => {
74
75
  const state = { build: () => { checkFresh(caller, false, path); return build(); }, project };
@@ -94,7 +95,8 @@ export function createStore(d, drv, rules, facts, operations) {
94
95
  return fn && (cache[name] = (input) => fn(db, caller, input, options && { ...options, store }));
95
96
  } })];
96
97
  })),
97
- ...(!inTransaction && { batch: async (writes) => {
98
+ batch: async (writes) => {
99
+ try {
98
100
  const states = writes.map(w => pending.get(w));
99
101
  if (new Set(writes).size !== writes.length || states.some(s => !s || s.claimed))
100
102
  throw new Error("Batch needs unused Writes from this Store.");
@@ -102,14 +104,26 @@ export function createStore(d, drv, rules, facts, operations) {
102
104
  s.claimed = true;
103
105
  const rows = writes.length ? await drv.procedure(db, states.map(s => s.build()), "store/batch") : [];
104
106
  return states.map((s, i) => s.project(rows[i]));
105
- } }),
107
+ }
108
+ catch (error) {
109
+ if (transaction)
110
+ transaction.failure ??= { error };
111
+ throw error;
112
+ }
113
+ },
106
114
  ...(drv.transaction && { transaction: async (fn, txOptions) => {
107
- if (inTransaction)
115
+ if (transaction)
108
116
  throw new Error("Nested transactions are not supported.");
109
- return drv.transaction(db, tx => fn(make(tx, caller, true, options), tx), txOptions);
117
+ const state = {};
118
+ return drv.transaction(db, async (tx) => {
119
+ const result = await fn(make(tx, caller, state, options), tx);
120
+ if (state.failure)
121
+ throw state.failure.error;
122
+ return result;
123
+ }, txOptions);
110
124
  } }),
111
125
  };
112
- if (inTransaction)
126
+ if (transaction)
113
127
  transactionStores.add(store);
114
128
  return store;
115
129
  }
@@ -132,11 +146,18 @@ export function createStore(d, drv, rules, facts, operations) {
132
146
  return out;
133
147
  };
134
148
  const rows = (sql, values) => drv.read(db, scoped(sql, caller), values);
135
- const clean = (list) => { for (const r of list) {
136
- delete r._rid;
137
- if (f.s)
138
- delete r[f.s];
139
- } return list; };
149
+ const clean = (list) => {
150
+ for (const r of list) {
151
+ delete r._rid;
152
+ if (f.s)
153
+ delete r[f.s];
154
+ const v = Number(r.version);
155
+ if (!Number.isSafeInteger(v))
156
+ throw new ContractError("SQL_TYPE", `version ${r.version} is beyond Number.MAX_SAFE_INTEGER.`, p.name);
157
+ r.version = v;
158
+ }
159
+ return list;
160
+ };
140
161
  const select = () => pol.select ??= `SELECT r.*, ${wires(p, "r.")}${pol.projection ? ", " + pol.projection : ""}${pol.from}`;
141
162
  const where = (conds) => { const all = pol.sql ? [pol.sql, ...conds] : conds; return all.length ? ` WHERE ${all.join(" AND ")}` : ""; };
142
163
  const base = () => evaluate(d, pol.recipes, caller);
@@ -189,10 +210,22 @@ export function createStore(d, drv, rules, facts, operations) {
189
210
  return [d.ident(name), mark];
190
211
  });
191
212
  const refs = (child) => trusted ? [] : (f.ref ?? []).flatMap(r => { const v = marks.get(r.column); return v ? [`EXISTS (SELECT 1 FROM ${d.ident(r.table)} _mantle_ref WHERE ${[`_mantle_ref.${d.ident(r.field)} = ${v}`, ...r.same.map(([parent, column]) => `_mantle_ref.${d.ident(parent)} = ${child(column) ?? "NULL"}`), ...r.terms.map(t => renderTerm(d, t, "_mantle_ref.", recipe))].join(" AND ")})`] : []; });
192
- const guard = (id, also = []) => ` WHERE ${[`${ID} = ${push(id)}`, ...[...pol.terms, x.lock].map(t => renderTerm(d, t, t[1] === "parent" ? d.ident(p.name) + "." : "", recipe)), ...also].join(" AND ")}`;
193
- const step = (sql) => ({ sql: scoped(sql, caller), binds: values, row: true });
213
+ const guard = (id, also = [], versioned = true) => ` WHERE ${[`${ID} = ${push(id)}`, ...[...pol.terms, ...versioned ? [x.lock] : []].map(t => renderTerm(d, t, t[1] === "parent" ? d.ident(p.name) + "." : "", recipe)), ...also].join(" AND ")}`;
214
+ const step = (sql, lock) => ({ sql: scoped(sql, caller), binds: values, row: true, ...(lock ? { lock } : {}) });
194
215
  return { given, marks, recipe, refs, guard, step, into, returning: () => ` RETURNING *, ${p.returning ??= wires(p, "")}` };
195
216
  };
217
+ const cleared = (set) => {
218
+ const req = f.req ?? {};
219
+ return [...new Set(Object.entries(set).flatMap(([name, v]) => v === null && Object.hasOwn(req, name.toLowerCase()) ? [req[name.toLowerCase()]] : []))];
220
+ };
221
+ const locked = (id, verb, fields = []) => {
222
+ const locks = trusted ? [] : (f.lock ?? []).filter(l => l.on.includes(verb)), required = fields.length ? `${d.ident("status")} <> 'published'` : undefined;
223
+ if (!locks.length && !required)
224
+ return { terms: [] };
225
+ const w = writer(), where = w.guard(id, [], false), cases = [...locks.map(l => `WHEN (${l.sql}) IS NOT FALSE THEN '${l.name}'`), ...required ? [`WHEN NOT (${required}) THEN '${REQUIRED}'`] : []];
226
+ const { sql, binds } = w.step(`SELECT CASE ${cases.join(" ")} END AS ${d.ident("lock")} FROM ${w.into}${where}`);
227
+ return { terms: [...locks.map(l => `NOT (${l.sql})`), ...required ? [required] : []], lock: { sql, binds, schema: p.name, verb, locks: Object.fromEntries(locks.map(l => [l.name, l.message])), ...required ? { required: `This entry is published: publish requires ${fields.join(", ")}, so an update cannot clear ${fields.length > 1 ? "them" : "it"}.` } : {} } };
228
+ };
196
229
  return {
197
230
  async findById(id) {
198
231
  const values = base();
@@ -229,14 +262,14 @@ export function createStore(d, drv, rules, facts, operations) {
229
262
  const w = writer(lock), assigns = w.given(set).map(([name, mark]) => `${name} = ${mark}`);
230
263
  for (const [column, v] of x.sets)
231
264
  assigns.push(`${d.ident(column)} = ${v === "bump" ? `${d.ident(column)} + 1` : w.recipe(v)}`);
232
- const refs = w.refs(column => `${w.into}.${d.ident(column)}`);
233
- return w.step(`UPDATE ${w.into} SET ${assigns.join(", ")}${w.guard(id, refs)}${w.returning()}`);
265
+ const refs = w.refs(column => `${w.into}.${d.ident(column)}`), held = locked(id, "update", cleared(set));
266
+ return w.step(`UPDATE ${w.into} SET ${assigns.join(", ")}${w.guard(id, [...refs, ...held.terms])}${w.returning()}`, held.lock);
234
267
  }, rows => clean(rows)[0], `${p.name}/update`);
235
268
  },
236
269
  delete(id, { lock }) {
237
270
  return write(() => {
238
- const w = writer(lock);
239
- return w.step(`DELETE FROM ${w.into}${w.guard(id)}`);
271
+ const w = writer(lock), held = locked(id, "delete");
272
+ return w.step(`DELETE FROM ${w.into}${w.guard(id, held.terms)}`, held.lock);
240
273
  }, () => undefined, `${p.name}/delete`);
241
274
  },
242
275
  };
@@ -93,9 +93,9 @@ export function emitAdminBindings(input, refuse) {
93
93
  const defaults = Object.fromEntries(Object.entries(checks(s, true).properties ?? {}).flatMap(([key, p]) => p.default !== undefined ? [[key, p.default]] : []));
94
94
  const primary = isPrimaryCollection(s, Object.values(plan.schemas));
95
95
  const schemaCode = `{
96
- 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)},
97
97
  binding:${entrySpec},
98
- 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;},
99
99
  values:values${n},row:${named},
100
100
  ${sqlite ? "" : "native:nativeText,"}
101
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]);},` : ""}
@@ -13,6 +13,7 @@ test("the Admin MCP App works in an MCP host", { timeout: 300_000 }, async () =>
13
13
  await checkMcpApp({ catalogue: mcpAppCatalogues.admin, html });
14
14
  });
15
15
  `;
16
+ const nullable = (p) => p.nullable === true || [p.type].flat().includes("null") || (p.enum ?? []).includes(null);
16
17
  const closed = (typeOf, schema) => !schema ? "{}" : schema.properties || schema.type === "object" && !schema["$ref"] ? typeOf({ ...schema, additionalProperties: false }) : "{ readonly [key: string]: unknown }";
17
18
  export function emitAdminClient(plan, typeOf) {
18
19
  const tools = mcpTools(plan, "admin"), hasTool = (kind, source) => tools.some(t => t.kind === kind && t.source === source);
@@ -20,7 +21,9 @@ export function emitAdminClient(plan, typeOf) {
20
21
  const operations = Object.entries(plan.procedures).filter(([name]) => hasTool("procedure", name)).sort(([a], [b]) => a < b ? -1 : 1);
21
22
  const schemas = Object.values(plan.schemas).sort((a, b) => a.name < b.name ? -1 : 1);
22
23
  const dataOf = (s) => {
23
- const scope = s.scope ? s.names[s.scope] ?? s.scope : undefined, { [scope ?? ""]: _scope, ...properties } = s.schema.properties ?? {};
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 }]));
24
27
  const required = s.publishing ? [] : s.schema.required?.filter(f => f !== scope);
25
28
  const schema = { ...s.schema, properties, ...(required ? { required } : {}) };
26
29
  return s.translates ? typeOf(schema) : closed(typeOf, schema);
@@ -77,14 +77,16 @@ export function generate(input) {
77
77
  };
78
78
  const values = new Set();
79
79
  const step = (s, path, fields, row = false) => {
80
- const recipes = recipesOf(s, fields, message => refuse(path, message));
81
- 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]) {
82
82
  if (r.k === "now" || r.k === "cutoff")
83
83
  values.add(r.k);
84
84
  else if (r.k === "input" && r.type)
85
85
  values.add(r.type === "date" ? "date" : "timestamp");
86
86
  }
87
- 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} }`;
88
90
  };
89
91
  const declarations = [], views = [], procedures = [], refProcedures = [];
90
92
  const entries = {};
@@ -137,8 +139,8 @@ export function generate(input) {
137
139
  entries[`procedures/${encodeURIComponent(name)}.ts`] = entry("procedure", name, `Procedure${index}Input as Input`, `export type { Output } from "${base}contracts";\n`);
138
140
  }
139
141
  emitTtl(plan, isSqlite, declarations, procedures, entries, refuse);
140
- emitPublishing(plan, isSqlite, declarations, procedures, entries, values, refuse);
141
- 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);
142
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) };
143
145
  if (diagnostics.length)
144
146
  return { ok: false, diagnostics };
@@ -6,7 +6,7 @@ export function emitHandlers(plan, interactive, typeOf, declarations, procedures
6
6
  declarations.push("export interface Handlers {}");
7
7
  return [];
8
8
  }
9
- declarations.push('export type HandlerStore = Pick<ReturnType<typeof makeStore>, "collection" | "views" | "procedures">;',
9
+ declarations.push('export type HandlerStore = Pick<ReturnType<typeof makeStore>, "collection" | "batch" | "views" | "procedures">;',
10
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>;`), "}");
11
11
  for (const { name, procedure, index } of refs) {
12
12
  const path = `procedures/${name}`, fn = `procedure${index}`;
@@ -6,7 +6,9 @@ import { publishedParent } from "./translations.js";
6
6
  import { entry, exportName, q } from "../domain/entry.js";
7
7
  import { publishingAnnotations, publishingInput, publishingTransitions } from "../domain/generated-tools.js";
8
8
  export { publishingAnnotations, publishingInput, publishingTransitions };
9
- export function emitPublishing(plan, isSqlite, declarations, procedures, entries, values, refuse) {
9
+ export const publishRequired = (s) => (s.schema.required ?? []).flatMap(field => Object.keys(s.fields).filter(c => (s.names?.[c] ?? c) === field).slice(0, 1)
10
+ .flatMap(c => s.fields[c] === "geo" ? [[`${c}_lat`, field], [`${c}_lng`, field]] : [[c, field]]));
11
+ export function emitPublishing(plan, isSqlite, declarations, procedures, entries, values, refuse, locks = {}) {
10
12
  const d = isSqlite ? sqlite : postgres;
11
13
  const translations = Object.values(plan.schemas).some(s => s.translates);
12
14
  if (translations)
@@ -35,15 +37,26 @@ export function emitPublishing(plan, isSqlite, declarations, procedures, entries
35
37
  if (term[1] !== "admin")
36
38
  conditions.push(renderTerm(d, term, term[1] === "parent" ? d.ident(s.name) + "." : "", recipe => (recipes.push(recipe), d.param(recipes.length))));
37
39
  if (verb === "publish")
38
- for (const field of s.schema.required ?? []) {
39
- const column = Object.keys(s.fields).find(c => (s.names?.[c] ?? c) === field);
40
- if (column)
41
- conditions.push(`${d.ident(column)} IS NOT NULL`);
42
- }
40
+ for (const [column] of publishRequired(s))
41
+ conditions.push(`${d.ident(column)} IS NOT NULL`);
43
42
  if (verb === "publish" && s.translates)
44
43
  conditions.push(publishedParent(plan, schema, d, recipes, trusted));
45
44
  const clock = d.param(1);
46
- return { sql: `UPDATE ${d.ident(s.name)} SET status = '${to}', published_at = ${verb === "publish" ? clock : "NULL"}, version = version + 1, updated_at = ${clock} WHERE ${conditions.join(" AND ")} RETURNING id, version, status, ${d.wireTimestamp(d.ident("published_at")).join("")} AS published_at`, binds: recipes, row: true };
45
+ const held = trusted ? [] : (locks[s.name.toLowerCase()] ?? []).filter(l => l.on.includes("update"));
46
+ const lock = held.length ? (() => {
47
+ const probe = [], number = new Map(), renumber = (sql) => sql.replace(isSqlite ? /\?(\d+)/g : /\$(\d+)/g, (_, n) => {
48
+ const i = Number(n);
49
+ if (!number.has(i)) {
50
+ probe.push(recipes[i - 1]);
51
+ number.set(i, d.param(probe.length));
52
+ }
53
+ return number.get(i);
54
+ });
55
+ const where = renumber(conditions.filter(c => c !== `version = ${d.param(3)}`).join(" AND "));
56
+ return { sql: `SELECT CASE ${held.map(l => `WHEN (${l.sql}) IS NOT FALSE THEN '${l.name}'`).join(" ")} END AS ${d.ident("lock")} FROM ${d.ident(s.name)} WHERE ${where}`, binds: probe, schema: s.name, verb: "update", locks: Object.fromEntries(held.map(l => [l.name, l.message])) };
57
+ })() : undefined;
58
+ conditions.push(...held.map(l => `NOT (${l.sql})`));
59
+ return { sql: `UPDATE ${d.ident(s.name)} SET status = '${to}', published_at = ${verb === "publish" ? clock : "NULL"}, version = version + 1, updated_at = ${clock} WHERE ${conditions.join(" AND ")} RETURNING id, version, status, ${d.wireTimestamp(d.ident("published_at")).join("")} AS published_at`, binds: recipes, row: true, ...(lock ? { lock } : {}) };
47
60
  };
48
61
  values.add("now");
49
62
  const caller = q([statement(false)]), trusted = q([statement(true)]);
@@ -1,6 +1,7 @@
1
1
  import { databaseImport, q, viewImports } from "../domain/entry.js";
2
2
  import { authLines, conversions } from "../domain/input.js";
3
3
  import { boundaryPage, outputConversions } from "../domain/columns.js";
4
+ import { pathTemplate as template, routeOrder, routeSource } from "../kernel/route-pattern.js";
4
5
  export function emitRest(plan, lowered, target, description) {
5
6
  const guard = (requires, path, binds = [], trustedOnly = false) => {
6
7
  const lines = authLines(requires, path, binds, !target.endsWith("postgres"), trustedOnly);
@@ -15,9 +16,8 @@ export function emitRest(plan, lowered, target, description) {
15
16
  let hasWire = false;
16
17
  const convert = (code) => code ? `, convert: ${code}` : "";
17
18
  const route = (method, path) => {
18
- const keys = [];
19
- const source = path.split("/").map(segment => segment.startsWith("{") && segment.endsWith("}") ? (keys.push(segment.slice(1, -1)), "([^/]+)") : encodeURIComponent(segment).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/");
20
- return `method: ${q(method)}, pattern: new RegExp(${q("^" + source + "/?$")})${keys.length ? `, keys: ${q(keys)}` : ""}`;
19
+ const { source, keys } = routeSource(path);
20
+ return `method: ${q(method)}, pattern: new RegExp(${q(source)})${keys.length ? `, keys: ${q(keys)}` : ""}`;
21
21
  };
22
22
  for (const [name, view] of Object.entries(plan.views)) {
23
23
  if (view.surface !== "public")
@@ -28,7 +28,7 @@ export function emitRest(plan, lowered, target, description) {
28
28
  hasWire ||= wire.length > 0;
29
29
  views.push(`{ ${route("GET", `/api/views/${name}`)}, target: ${q({ kind: "view", name })}${guard(view.requires, `views/${name}`, (lowered.views[name]?.public ?? lowered.views[name]?.caller)?.binds)}, query: query${id}, page: ${boundaryPage(`page${id}`, wire)}${convert(conversions(view.input?.properties, (lowered.views[name]?.public ?? lowered.views[name]?.caller)?.binds ?? [], target))} }`);
30
30
  }
31
- const triggers = Object.values(plan.triggers).filter(t => t.source.kind === "http").sort((a, b) => a.source.kind === "http" && b.source.kind === "http" ? (a.source.path.match(/\{/g)?.length ?? 0) - (b.source.path.match(/\{/g)?.length ?? 0) : 0);
31
+ const triggers = Object.values(plan.triggers).filter(t => t.source.kind === "http").sort((a, b) => a.source.kind === "http" && b.source.kind === "http" ? routeOrder(a.source.path, b.source.path) : 0);
32
32
  for (const trigger of triggers) {
33
33
  if (trigger.source.kind !== "http")
34
34
  continue;
@@ -40,7 +40,7 @@ export function emitRest(plan, lowered, target, description) {
40
40
  const type = [procedure.input?.properties?.[key]?.type].flat().find(t => t && t !== "null");
41
41
  return type === "integer" || type === "number" ? [[key, "number"]] : type === "boolean" ? [[key, "boolean"]] : [];
42
42
  }));
43
- procedures.push(`{ ${route(method, path)}, id: ${q(`rest:${method} ${path.replace(/\{([^}]+)\}/g, ":$1")}`)}, target: ${q({ kind: "procedure", name })}${description.targets.find(t => t._meta.id === `procedure:${name}`)?._meta.readOnly ? ", readOnly: true" : ""}${guard(procedure.requires, `procedures/${name}`, lowered.procedures[name]?.caller?.flatMap(s => s.binds), procedure.trustedOnly)}${Object.keys(params).length ? `, params: ${q(params)}` : ""}, validate: validate${id}${convert(conversions(procedure.input?.properties, lowered.procedures[name]?.caller?.flatMap(s => s.binds) ?? [], target))}, run: ${ref ? `(db, caller, input, handlers) => store(db, caller, { handlers }).procedures[${q(name)}](input)` : `procedure${id}`} }`);
43
+ procedures.push(`{ ${route(method, path)}, id: ${q(`rest:${method} ${template(path)}`)}, target: ${q({ kind: "procedure", name })}${description.targets.find(t => t._meta.id === `procedure:${name}`)?._meta.readOnly ? ", readOnly: true" : ""}${guard(procedure.requires, `procedures/${name}`, lowered.procedures[name]?.caller?.flatMap(s => s.binds), procedure.trustedOnly)}${Object.keys(params).length ? `, params: ${q(params)}` : ""}, validate: validate${id}${convert(conversions(procedure.input?.properties, lowered.procedures[name]?.caller?.flatMap(s => s.binds) ?? [], target))}, run: ${ref ? `(db, caller, input, handlers) => store(db, caller, { handlers }).procedures[${q(name)}](input)` : `procedure${id}`} }`);
44
44
  }
45
45
  if (hasWire)
46
46
  imports.push('import { wireRows } from "@aotter/mantle/contracts";');