@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
@@ -1,17 +1,12 @@
1
- /** Build-time proofs for the compiler's keyset keys. No AST reaches a generated operation. */
2
1
  import { NATIVE_COLUMNS, NATIVE_NOT_NULL, notNullColumns } from "../build/compiler.js";
3
2
  import { declaredType } from "./columns.js";
4
- /** Storage column -> the native field a manifest index names. */
5
3
  const NATIVE_FIELDS = new Map(Object.entries(NATIVE_COLUMNS).map(([field, column]) => [column, field]));
6
4
  const fieldName = (n) => n.ColumnRef?.fields?.at(-1)?.String?.sval;
7
5
  const scalar = (n) => !!(n.ParamRef || n.A_Const || n.TypeCast && scalar(n.TypeCast.arg));
8
- // A user-field term lowers to the dialect's anyOf, `(col) IN (SELECT value FROM json_each((?)))` or `(col) = ANY(CAST(...(?)...))`.
9
6
  const anyOf = (raw) => raw?.length === 8 && raw[1]?.ColumnRef && raw[5]?.ParamRef && /^ (IN \(SELECT value FROM json_each\(|= ANY\()/.test(raw[3]);
10
- // A scope prints `/*s:field*/ kept /*|*/ kept OR 1 = 1 /*;*/`, so `{ all: true }` drops it; any other anyOf is a restrict, which it never lifts.
11
7
  const scopeTerm = (raw) => typeof raw?.[0] === "string" && raw[0].startsWith("/*s:");
12
8
  const restricts = (n) => !!n && typeof n === "object" && (anyOf(n.Raw?.parts) || !scopeTerm(n.Raw?.parts) && Object.values(n).some(restricts));
13
9
  const aggregate = (n) => !!n && !n.SubLink && !n.SelectStmt && (n.FuncCall && !n.FuncCall.over && ["count", "sum", "min", "max", "avg"].includes(n.FuncCall.funcname?.at(-1)?.String?.sval) || Object.values(n).some(v => v && typeof v === "object" && (Array.isArray(v) ? v.some(aggregate) : aggregate(v))));
14
- /** Only complete grouped tuples, base ids and non-fanning singleton joins are proved. */
15
10
  export function provePaging(plan, ast) {
16
11
  let reason;
17
12
  const fail = (message) => { reason = message; return undefined; };
@@ -50,7 +45,6 @@ export function provePaging(plan, ast) {
50
45
  return fail(`Fanning join '${[...aliases.keys()].filter(alias => !before.has(alias)).join(", ")}': join on the joined Schema's id or a single-field unique key, or reduce it to one row.`);
51
46
  if (n.JoinExpr.jointype === "JOIN_FULL" || n.JoinExpr.jointype === "JOIN_RIGHT")
52
47
  return;
53
- // The right side of LEFT JOIN may be absent: its fields cannot become NOT NULL cursor keys.
54
48
  if (n.JoinExpr.jointype === "JOIN_LEFT")
55
49
  for (const [alias, r] of aliases)
56
50
  if (!before.has(alias))
@@ -73,7 +67,6 @@ export function provePaging(plan, ast) {
73
67
  else if (sub)
74
68
  fact = select(sub.subquery.SelectStmt, ctes)?.relation;
75
69
  if (fact) {
76
- // Two aliases of one Schema/CTE are distinct row sources, including a singleton joined to many rows.
77
70
  const sources = new Map();
78
71
  fact = { ...fact, fields: new Map([...fact.fields].map(([name, f]) => {
79
72
  if (!sources.has(f.source))
@@ -117,8 +110,6 @@ export function provePaging(plan, ast) {
117
110
  const fixed = (n) => {
118
111
  if (n?.BoolExpr?.boolop === "AND_EXPR")
119
112
  n.BoolExpr.args.forEach(fixed);
120
- // `col IN auth.user(field)` (a scope or restrict) never fixes a prefix (#1644): the plan is prepared for a set, so
121
- // native ORDER BY after it reads and sorts all the caller's rows. It is only remembered for the refusal's guidance.
122
113
  const raw = n?.Raw?.parts;
123
114
  if (scopeTerm(raw))
124
115
  return fixed(raw[1]);
@@ -151,9 +142,7 @@ export function provePaging(plan, ast) {
151
142
  const same = (a, b) => a && b && a.name === b.name && a.source === b.source;
152
143
  if (sorts.length) {
153
144
  if (group.length) {
154
- // Full GROUP BY keys, not an id from an arbitrary row in the group, make the result unique.
155
145
  if (!group.every(g => {
156
- // GROUP BY prefers an input column over a colliding output alias; ORDER BY does the reverse.
157
146
  const source = g.ColumnRef?.fields?.length === 1 ? [...aliases.values()].filter(r => r.names.has(fieldName(g))) : [];
158
147
  const f = source.length ? source.length === 1 ? source[0].fields.get(fieldName(g)) : undefined : resolve(g);
159
148
  return f?.notNull && keys.some(k => same(k, f));
@@ -175,21 +164,16 @@ export function provePaging(plan, ast) {
175
164
  return order.every((f, n) => index[i + n] === f.name);
176
165
  });
177
166
  if (!serving(base.fixed)) {
178
- // SQL names a column (`created_at`), a manifest index the field (`createdAt`).
179
167
  const index = serving(new Set([...base.fixed, ...base.sets])), named = (c) => schema.names?.[c] ?? c, field = (c) => schema.names?.[c] ?? NATIVE_FIELDS.get(c) ?? c;
180
168
  const columns = order.map(f => named(f.name)).join(", "), fields = order.map(f => field(f.name)).join(", ");
181
169
  if (!index)
182
170
  return fail(`Missing index for ORDER BY (${columns}): declare a Schema index with [${fields}] in this order after any fixed equality prefix.`);
183
171
  const prefix = index.slice(0, index.indexOf(order[0].name)), set = prefix.filter(c => !base.fixed.has(c));
184
- // The suggested order is the index from its first set-bound column: an equality-fixed column between stays in it.
185
172
  const lead = prefix.slice(prefix.indexOf(set[0])), quoted = (cs) => cs.map(c => `'${field(c)}'`).join(", ");
186
- // The scope-first order is offered only when SQL can name every added column and none can be null. The owner
187
- // scope column is NOT NULL in storage, but a View cannot address it.
188
173
  const notNull = notNullColumns(schema), owner = lead.filter(c => c === schema.scope), nullable = lead.filter(c => !NATIVE_NOT_NULL.has(c) && !notNull.has(c) && c !== schema.scope);
189
174
  const dir = (f) => sorts[keys.indexOf(f)].SortBy.sortby_dir === "SORTBY_DESC" ? " DESC" : "";
190
175
  const sorted = [...lead.map(c => named(c) + dir(order[0])), ...order.map(f => named(f.name) + dir(f))].join(", ");
191
176
  const desc = order.some(f => dir(f)) ? ", read backwards; on PostgreSQL confirm with EXPLAIN that no Sort remains" : "";
192
- // `{ all: true }` drops only scope terms: offer it an index on the View's equality-fixed columns, unless a restrict remains.
193
177
  const eq = [...base.fixed].filter(c => c !== "id" && !order.some(f => f.name === c));
194
178
  const ways = [...nullable.length || owner.length ? [] : [`ORDER BY ${sorted} (same index${desc})`], ...restricts(ast) ? [] : [`for { all: true } callers only, index [${[...eq.map(field), fields].join(", ")}, id]`]];
195
179
  const why = [...owner.length ? [`${quoted(owner)} is the owner column, which SQL cannot name`] : [], ...nullable.length ? [`${quoted(nullable)} can be null`] : []];
@@ -205,7 +189,6 @@ export function provePaging(plan, ast) {
205
189
  const result = select(ast.SelectStmt);
206
190
  return result ? { types: result.keys.map(f => f.type), single: result.relation.single } : reason ? { types: [], single: false, reason } : undefined;
207
191
  }
208
- /** Cast only cursor ParamRefs in the outer predicate, never authored literals or bare projections. */
209
192
  export function castCursor(ast, sources, types, postgres) {
210
193
  const typeOf = (source) => source && "cursor" in source ? types[source.cursor] : undefined;
211
194
  const typed = (type) => type && (postgres ? ["timestamptz", "date"] : ["integer", "int4", "int8"]).includes(type);
@@ -1,19 +1,22 @@
1
- const KINDS = new Set(["uid", "admin", "user", "now", "cutoff", "const", "input", "version"]);
2
- /** @internal */
1
+ const KINDS = new Set(["uid", "admin", "user", "now", "cutoff", "const", "input", "version", "scoped"]);
3
2
  export function recipesOf(statement, fields, refuse, values) {
3
+ const field = (bound) => {
4
+ const name = fields.find(field => field.toLowerCase() === String(bound).toLowerCase());
5
+ if (name === undefined)
6
+ refuse(`Input bind '${bound}' has no declared field.`);
7
+ return name ?? String(bound);
8
+ };
4
9
  return statement.binds.map(b => {
5
10
  if (!KINDS.has(b.k))
6
11
  refuse(`Bind recipe '${b.k}' needs an explicit native implementation.`);
7
12
  if (b.k === "now" || b.k === "cutoff")
8
13
  values?.add(b.k);
14
+ if (b.k === "scoped" && b.name !== undefined)
15
+ return { ...b, name: field(b.name) };
9
16
  if (b.k !== "input")
10
17
  return b;
11
18
  if (b.type === "date" || b.type === "timestamptz")
12
19
  values?.add(b.type === "date" ? "date" : "timestamp");
13
- const name = fields.find(field => field.toLowerCase() === String(b.name).toLowerCase());
14
- if (name === undefined)
15
- refuse(`Input bind '${b.name}' has no declared field.`);
16
- // only a date-time or a date is converted at bind time (`encode` in kernel/binds)
17
- return { k: "input", name: name ?? String(b.name), ...(b.type === "timestamptz" || b.type === "date" ? { type: b.type } : {}) };
20
+ return { k: "input", name: field(b.name), ...(b.type === "timestamptz" || b.type === "date" || b.type === "json" ? { type: b.type } : {}) };
18
21
  });
19
22
  }
@@ -1,4 +1,3 @@
1
- /** Conditional capability imports keep applications without user references unchanged. */
2
1
  export function userReferences(plan, sqlite) {
3
2
  const used = !!plan.access;
4
3
  return { used, code: used ? `import * as USER from "@aotter/mantle/user-fields";\nconst USER_VALUES = { scopeInsert: USER.scopeInsert, scopedAll: USER.scopedAll, userRefs: USER.userRefs(${sqlite}, 1) };` : '', values: used ? '...USER_VALUES' : '' };
@@ -1,11 +1,6 @@
1
1
  import { declaredType } from "./columns.js";
2
2
  const raw = (...parts) => ({ Raw: { parts } });
3
- /** Temporal SQL forms, plus selected integer text before driver rounding. */
4
3
  const wire = (type, d, v, boundary = false) => type === "timestamptz" ? raw(...d.wireTimestamp(v)) : type === "date" ? raw(...d.wireDate(v)) : boundary && type && ["integer", "int8", "int4"].includes(type) ? raw("CAST(", v, " AS text)") : undefined;
5
- /**
6
- * Formats declared temporal outputs and selected integers in a View's top-level SELECT, preserving names.
7
- * Returns a refusal when the outputs cannot be matched to SELECT items by position.
8
- */
9
4
  export function wireScalars(ast, keys, types, d, boundary = false) {
10
5
  if (!types.some(type => type === "timestamptz" || type === "date" || boundary && type && ["integer", "int8", "int4"].includes(type)) || !ast.SelectStmt)
11
6
  return ast;
@@ -24,7 +19,6 @@ export function wireScalars(ast, keys, types, d, boundary = false) {
24
19
  const out = select(ast.SelectStmt);
25
20
  return typeof out === "string" ? out : { ...ast, SelectStmt: out };
26
21
  }
27
- /** The same wire form for a write's RETURNING list (Core has already expanded `*`): a column of the written table is wrapped by its declared type. */
28
22
  export function wireReturning(ast, schemas, d, boundary = false) {
29
23
  const [kind, stmt] = Object.entries(ast)[0] ?? [];
30
24
  const list = stmt?.returningClause?.exprs;
@@ -1,4 +1,3 @@
1
- /** Selected Admin queries/procedures: Core checks, rewrites and prints every native fragment at generation. */
2
1
  import { enumOptions, protectedColumns } from "../build/compiler.js";
3
2
  import { parsePgSql } from "../build/compiler.js";
4
3
  import { compileProgram } from "../build/compiler.js";
@@ -6,6 +5,8 @@ import { adminOrder } from "../domain/adminOrder.js";
6
5
  import { declaredType, serverOwned } from "../domain/columns.js";
7
6
  import { wireScalars, wireReturning } from "../domain/wireScalars.js";
8
7
  import { recipesOf } from "../domain/recipes.js";
8
+ import { lockCondition, refChecks } from "../build/compiler.js";
9
+ import { renderTerm } from "../kernel/store.js";
9
10
  const ref = (...names) => ({ ColumnRef: { fields: names.map(sval => ({ String: { sval } })) } });
10
11
  const op = (lexpr, rexpr) => ({ A_Expr: { kind: "AEXPR_OP", name: [{ String: { sval: "=" } }], lexpr, rexpr } });
11
12
  const and = (...args) => args.length === 1 ? args[0] : { BoolExpr: { boolop: "AND_EXPR", args } };
@@ -29,14 +30,22 @@ export async function adminEntries(plan, dialect, wire, refusals) {
29
30
  const columns = [...system(s), ...fields(s)], writable = Object.keys(s.fields).filter(f => f !== s.scope && s.fields[f] !== "geo");
30
31
  const parent = s.translates && plan.schemas[s.translates.parent.toLowerCase()];
31
32
  const types = columns.map(f => declaredType(s.fields, f) ?? (f === "version" ? "integer" : f.endsWith("_lat") || f.endsWith("_lng") ? "real" : "text"));
32
- // Compiler scope names belong only to relations this generated query can reference.
33
33
  const schemas = Object.create(null);
34
34
  let relation = s;
35
35
  while (relation && !Object.hasOwn(schemas, relation.name)) {
36
36
  schemas[relation.name] = relation;
37
37
  relation = relation.translates ? plan.schemas[relation.translates.parent.toLowerCase()] : undefined;
38
38
  }
39
- const ctx = (kind, trusted, inputs) => ({ dialect, schemas, inputs, kind, mode: trusted ? "trusted" : "admin" });
39
+ const lockReads = (n) => { if (!n || typeof n !== "object")
40
+ return; const v = n.RangeVar; if (v && plan.schemas[v.relname])
41
+ schemas[v.relname] ??= plan.schemas[v.relname]; Object.values(n).forEach(lockReads); };
42
+ lockReads(s.locks);
43
+ const ctx = (kind, trusted, inputs) => ({ dialect, schemas, inputs, kind, mode: trusted ? "trusted" : "admin", uncheckedRefs: true });
44
+ const refs = (child) => Object.fromEntries(refChecks(s, plan.schemas).filter(r => writable.includes(r.column)).map(r => {
45
+ const holes = [], hole = (value) => { const token = wire.param(marker + 1 + holes.length); holes.push([token, value]); return token; };
46
+ const sql = `EXISTS (SELECT 1 FROM ${wire.ident(r.table)} _mantle_ref WHERE ${[`_mantle_ref.${wire.ident(r.field)} = ${hole(r.column)}`, ...r.same.map(([parent, column]) => `_mantle_ref.${wire.ident(parent)} = ${child(column, hole)}`), ...r.terms.map(t => renderTerm(wire, t, "_mantle_ref.", hole))].join(" AND ")})`;
47
+ return [r.column, { sql, holes }];
48
+ }));
40
49
  const select = `SELECT ${columns.map(c => `${wire.ident(s.name)}.${wire.ident(c)}`).join(",")} FROM ${wire.ident(s.name)}`;
41
50
  const base = await parse(select);
42
51
  const project = (ast, names, keys) => {
@@ -53,6 +62,17 @@ export async function adminEntries(plan, dialect, wire, refusals) {
53
62
  const usedNames = new Set([...columns, ...(parent ? fields(parent) : [])].map(name => name.toLowerCase()));
54
63
  const cursorAliases = [0, 1].map(i => { let name = "_mantle_admin_key" + i; while (usedNames.has(name.toLowerCase()))
55
64
  name += "_"; usedNames.add(name.toLowerCase()); return { name, sql: wire.ident(name) }; });
65
+ const lockColumns = s.locks?.length ? ["update", "delete"].map(verb => {
66
+ let name = "_mantle_admin_lock_" + verb;
67
+ while (usedNames.has(name.toLowerCase()))
68
+ name += "_";
69
+ usedNames.add(name.toLowerCase());
70
+ const qualify = (n) => { if (!n || typeof n !== "object")
71
+ return; const f = n.ColumnRef?.fields; if (f?.length === 1)
72
+ f.unshift({ String: { sval: s.name } }); Object.values(n).forEach(qualify); };
73
+ const held = s.locks.filter(l => l.on.includes(verb)).map(l => { const when = structuredClone(l.when); qualify(when); return { CaseWhen: { expr: { BooleanTest: { arg: lockCondition(when, plan.schemas, dialect.lowering), booltesttype: "IS_NOT_FALSE" } }, result: { A_Const: { sval: { sval: l.name } } } } }; });
74
+ return { name, sql: (held.length ? expr({ CaseExpr: { args: held } }) : "NULL") + " AS " + wire.ident(name) };
75
+ }) : undefined;
56
76
  const ordered = adminOrder(s), fieldFacts = Object.create(null);
57
77
  for (const column of [...system(s), ...writable]) {
58
78
  const type = declaredType(s.fields, column) ?? (column === "version" ? "integer" : "text");
@@ -83,10 +103,8 @@ export async function adminEntries(plan, dialect, wire, refusals) {
83
103
  const [compiled] = compileProgram(base, ctx("view", trusted, {}));
84
104
  if (!compiled)
85
105
  throw Error("Missing compiled collection View.");
86
- // Keep the compiler's verified policy, but share the physical SELECT template at requests.
87
106
  const baseInner = compiled.ast.SelectStmt.fromClause[0].RangeSubselect.subquery.SelectStmt, policy = baseInner.whereClause;
88
107
  compiled.ast.SelectStmt.fromClause = [baseInner.fromClause[0]];
89
- // The native merge is a scalar-id lookup, never a fanning join. Core owns both sides' visibility.
90
108
  if (parent) {
91
109
  const own = new Set(Object.keys(s.fields).flatMap(f => s.fields[f] === "geo" ? [f + "_lat", f + "_lng"] : [f]));
92
110
  const inherited = fields(parent).filter(f => !own.has(f)), on = s.translates.on.toLowerCase(), alias = "_mantle_merge";
@@ -109,7 +127,6 @@ export async function adminEntries(plan, dialect, wire, refusals) {
109
127
  const names = compiled.ast.SelectStmt.targetList.map((t) => t.ResTarget.name ?? t.ResTarget.val.ColumnRef.fields.at(-1).String.sval);
110
128
  const projection = print(project({ SelectStmt: { targetList: compiled.ast.SelectStmt.targetList, op: "SETOP_NONE", limitOption: "LIMIT_OPTION_DEFAULT" } }, names, [])).replace(/^SELECT /, "");
111
129
  const from = print({ SelectStmt: { ...compiled.ast.SelectStmt, targetList: [{ ResTarget: { val: { A_Const: { ival: { ival: 1 } } } } }] } }).replace(/^SELECT 1/, "");
112
- // Core compiles all writable fields once; request-time work selects only generated fragments.
113
130
  const inputs = { ...Object.fromEntries(writable.map(f => [f, s.fields[f]])), id: "text" };
114
131
  const returning = columns.map(wire.ident).join(",");
115
132
  const insertAst = writable.length ? (await parse(`INSERT INTO ${wire.ident(s.name)} (${writable.map(wire.ident).join(",")}) VALUES (${writable.map(f => `input.${wire.ident(f)}`).join(",")}) RETURNING ${returning}`))[0] : { InsertStmt: { relation: { RangeVar: { relname: s.name, mantle: "table", inh: true, relpersistence: "p" } }.RangeVar, cols: [], selectStmt: { SelectStmt: { valuesLists: [{ List: { items: [] } }] } }, returningClause: { exprs: columns.map(f => ({ ResTarget: { val: ref(f) } })) } } };
@@ -135,14 +152,16 @@ export async function adminEntries(plan, dialect, wire, refusals) {
135
152
  if (v && typeof v === "object")
136
153
  Array.isArray(v) ? v.forEach(rewrite) : rewrite(v); };
137
154
  rewrite(rewritten);
138
- return { ast: rewritten, binds: recipesOf({ binds: fixed }, ["id"], message => { throw Error(message); }) };
155
+ const verb = c.verb === "update" || c.verb === "delete" ? c.verb : undefined, locks = Object.fromEntries((s.locks ?? []).filter(l => verb && l.on.includes(verb)).map(l => [l.name, l.message]));
156
+ return { ast: rewritten, binds: recipesOf({ binds: fixed }, ["id"], message => { throw Error(message); }), ...(c.probe && verb ? { lock: { sql: print(c.probe.ast), binds: recipesOf(c.probe, ["id"], message => { throw Error(message); }), schema: s.name, verb, locks } } : {}) };
139
157
  };
140
158
  const create = write(insertAst), patch = write(updateAst), remove = write(deleteAst), entryFields = Object.create(null);
141
159
  const insert = create.ast.InsertStmt, update = patch.ast.UpdateStmt;
142
160
  for (const [i, field] of writable.entries())
143
161
  entryFields[field] = { column: wire.ident(field), insert: expr(insert.selectStmt.SelectStmt.fromClause[0].RangeSubselect.subquery.SelectStmt.valuesLists[0].List.items[i]), ...(!protectedColumns(s).includes(field) && !serverOwned(s.schema, field) ? { update: wire.ident(field) + " = " + expr(update.targetList[updateFields.indexOf(field)].ResTarget.val) } : {}) };
144
162
  const returnSQL = (ast) => " RETURNING " + ast.returningClause.exprs.map((t) => expr(t.ResTarget.val) + (t.ResTarget.name ? " AS " + wire.ident(t.ResTarget.name) : "")).join(",");
145
- return { projection, from, policy: policy ? expr(policy) : "", binds: recipesOf(compiled, [], message => { throw Error(message); }), fields: entryFields, create: { prefix: print(create.ast).slice(0, print(create.ast).indexOf(" (")) + " (", between: ") VALUES (", suffix: ")", binds: create.binds, columns: insert.cols.slice(writable.length).map((t) => wire.ident(t.ResTarget.name)), values: insert.selectStmt.SelectStmt.targetList.slice(writable.length ? 1 : 0).map((t) => expr(t.ResTarget.val)), returning: returnSQL(insert) }, patch: { prefix: print(patch.ast).slice(0, print(patch.ast).indexOf(" SET ")) + " SET ", binds: patch.binds, sets: update.targetList.slice(updateFields.length).map((t) => wire.ident(t.ResTarget.name) + " = " + expr(t.ResTarget.val)), where: " WHERE " + expr(update.whereClause), returning: returnSQL(update) }, remove: { sql: print(remove.ast), binds: remove.binds, row: true } };
163
+ const created = trusted ? {} : refs((column, hole) => hole(column)), patched = trusted ? {} : refs(column => wire.ident(s.name) + "." + wire.ident(column));
164
+ return { projection, from, policy: policy ? expr(policy) : "", ...(lockColumns && !trusted ? { locks: { projection: lockColumns.map(c => c.sql).join(", "), update: lockColumns[0].name, delete: lockColumns[1].name, messages: Object.fromEntries(s.locks.map(l => [l.name, l.message])) } } : {}), binds: recipesOf(compiled, [], message => { throw Error(message); }), fields: entryFields, create: { ...(Object.keys(created).length ? { refs: created } : {}), prefix: print(create.ast).slice(0, print(create.ast).indexOf(" (")) + " (", between: ") VALUES (", suffix: ")", binds: create.binds, columns: insert.cols.slice(writable.length).map((t) => wire.ident(t.ResTarget.name)), values: insert.selectStmt.SelectStmt.targetList.slice(writable.length ? 1 : 0).map((t) => expr(t.ResTarget.val)), returning: returnSQL(insert) }, patch: { ...(Object.keys(patched).length ? { refs: patched } : {}), ...(patch.lock ? { lock: patch.lock } : {}), prefix: print(patch.ast).slice(0, print(patch.ast).indexOf(" SET ")) + " SET ", binds: patch.binds, sets: update.targetList.slice(updateFields.length).map((t) => wire.ident(t.ResTarget.name) + " = " + expr(t.ResTarget.val)), where: " WHERE " + expr(update.whereClause), returning: returnSQL(update) }, remove: { sql: print(remove.ast), binds: remove.binds, row: true, ...(remove.lock ? { lock: remove.lock } : {}) } };
146
165
  }));
147
166
  result[s.name] = { ...(s.userScopes?.length ? { userScopes: s.userScopes } : {}), cursorAliases, scoped: !!s.scope, path: "schemas/" + s.name + "/admin", marker: markerText, id: wire.ident(s.name) + "." + wire.ident("id"), fields: fieldFacts, orderFields: Object.keys(ordered), search: `(${wire.ident(s.name)}.${wire.ident("id")} = ${markerText}${s.search?.length ? ` OR ${wire.search(s.name, s.search, wire.ident(s.name), markerText)}` : ""})`, statistics: { rowsAlias, column: wire.ident(s.name) + "." + wire.ident("created_at"), time: postgres ? `extract(epoch FROM ${wire.ident(s.name)}.${wire.ident("created_at")}) * 1000000` : wire.ident(s.name) + "." + wire.ident("created_at"), subtype }, caller: modes[0], trusted: modes[1] };
148
167
  }
@@ -4,69 +4,67 @@ import { boundaryType } from "../domain/columns.js";
4
4
  import { viewOutputs, compileProgram } from "../build/compiler.js";
5
5
  import { pagedOf } from "../build/compiler.js";
6
6
  import { castCursor } from "../domain/paging.js";
7
- export function adminPaging(plan, dialect, proofs, project) {
7
+ export function adminPaging(plan, dialect, proofs, project, refuse) {
8
8
  const all = Object.create(null);
9
9
  for (const [name, v] of Object.entries(plan.views).filter(([, v]) => v.surface !== "internal")) {
10
10
  const proof = proofs[name];
11
- if (!proof || proof.reason)
12
- throw Error(proof?.reason ?? `Unproved Admin paging: ${name}`);
13
- const list = (v.uiSchema?.list ?? {});
14
- const modes = v.surface === "public" ? ["public", "public"] : ["caller", "trusted"];
15
- const pages = modes.map(mode => {
16
- const [c] = compileProgram(v.stmts, { dialect, schemas: plan.schemas, inputs: v.inputs, kind: "view", mode, restrict: v.restrict });
17
- if (!c)
18
- throw Error(`No compiled Admin View: ${name}`);
19
- const keys = c.ast.SelectStmt.sortClause?.length ?? 0;
20
- if (keys !== proof.types.length)
21
- throw Error(`Incomplete Admin cursor proof: ${name}`);
22
- const first = pagedOf({ schemas: plan.schemas, dialect }, c, undefined, !keys && (list.filterFields?.length || list.searchFields?.length) ? { eq: [{ column: (list.filterFields ?? list.searchFields)[0], value: "" }] } : undefined), next = keys ? pagedOf({ schemas: plan.schemas, dialect }, c, Array(keys).fill(0), undefined) : undefined;
23
- if (!keys && !first.flat)
24
- first.ast.SelectStmt.whereClause = undefined;
25
- const reserved = JSON.stringify([c.ast, v.source, list, Object.values(plan.schemas).map(s => s.fields)]);
26
- let marker = 32767;
27
- while (reserved.includes(String(marker)))
28
- marker++;
29
- const print = (ast) => dialect.print(ast, plan.schemas);
30
- const read = (p) => { const typed = castCursor(p.ast, p.sources, proof.types, !!dialect.nativeOrder); return { prefix: print(project({ SelectStmt: { ...typed.SelectStmt, whereClause: undefined, sortClause: undefined, limitCount: undefined, limitOption: "LIMIT_OPTION_DEFAULT" } }, p.flat ? viewOutputs(v, plan.schemas).keys ?? [] : p.names, keys, name)) + (typed.SelectStmt.whereClause ? ` WHERE (${expression(typed.SelectStmt.whereClause)})` : ""), extra: p.sources.filter(x => "cursor" in x).map(x => x.cursor) }; };
31
- const suffixAst = { SelectStmt: { targetList: [{ ResTarget: { val: { A_Const: { ival: { ival: 1 } } } } }], sortClause: keys ? first.ast.SelectStmt.sortClause : undefined, limitCount: { ParamRef: { number: marker } }, limitOption: "LIMIT_OPTION_COUNT", op: "SETOP_NONE" } };
32
- const tail = print(suffixAst).replace(/^SELECT 1/, "");
33
- const expression = (ast) => print({ SelectStmt: { targetList: [{ ResTarget: { val: ast } }], limitOption: "LIMIT_OPTION_DEFAULT", op: "SETOP_NONE" } }).replace(/^SELECT /, "");
34
- const fragment = (match, type) => {
35
- const p = pagedOf({ schemas: plan.schemas, dialect }, c, undefined, match);
36
- const ast = structuredClone(p.ast.SelectStmt.whereClause);
37
- const renumber = (node) => { if (node.ParamRef) {
38
- node.ParamRef.number = marker;
39
- if (dialect.nativeOrder && (type === "date" || type === "timestamptz")) {
40
- delete node.ParamRef;
41
- node.Raw = { parts: ["CAST(CAST(", { ParamRef: { number: marker } }, ` AS text) AS ${type})`] };
42
- return;
43
- }
44
- } for (const child of Object.values(node))
45
- if (child && typeof child === "object")
46
- Array.isArray(child) ? child.forEach(renumber) : renumber(child); };
47
- renumber(ast);
48
- return expression(ast);
49
- };
50
- return { marker: dialect.nativeOrder ? `$${marker}` : `?${marker}`, binds: c.binds, keys, first: read(first), ...(next ? { next: read(next) } : {}), tail, eq: Object.fromEntries((list.filterFields ?? []).map(field => { const col = v.columns?.[field] ?? v.columns?.[field.toLowerCase()], type = col && plan.schemas[col.schema]?.fields[col.field]; return [field, fragment({ eq: [{ column: field, value: "" }] }, type)]; })), ...(list.searchFields?.length ? { search: fragment({ search: { columns: list.searchFields, text: "x" } }) } : {}) };
51
- });
52
- all[name] = { caller: pages[0], trusted: pages[1] };
11
+ if (!proof || !proof.types.length && !proof.single)
12
+ continue;
13
+ try {
14
+ const list = (v.uiSchema?.list ?? {});
15
+ const modes = v.surface === "public" ? ["public", "public"] : ["caller", "trusted"];
16
+ const pages = modes.map(mode => {
17
+ const [c] = compileProgram(v.stmts, { dialect, schemas: plan.schemas, inputs: v.inputs, kind: "view", mode, restrict: v.restrict });
18
+ if (!c)
19
+ throw Error(`No compiled Admin View: ${name}`);
20
+ const keys = c.ast.SelectStmt.sortClause?.length ?? 0;
21
+ if (keys !== proof.types.length)
22
+ throw Error(`Incomplete Admin cursor proof: ${name}`);
23
+ const first = pagedOf({ schemas: plan.schemas, dialect }, c, undefined, !keys && (list.filterFields?.length || list.searchFields?.length) ? { eq: [{ column: (list.filterFields ?? list.searchFields)[0], value: "" }] } : undefined), next = keys ? pagedOf({ schemas: plan.schemas, dialect }, c, Array(keys).fill(0), undefined) : undefined;
24
+ if (!keys && !first.flat)
25
+ first.ast.SelectStmt.whereClause = undefined;
26
+ const reserved = JSON.stringify([c.ast, v.source, list, Object.values(plan.schemas).map(s => s.fields)]);
27
+ let marker = 32767;
28
+ while (reserved.includes(String(marker)))
29
+ marker++;
30
+ const print = (ast) => dialect.print(ast, plan.schemas);
31
+ const read = (p) => { const typed = castCursor(p.ast, p.sources, proof.types, !!dialect.nativeOrder); return { prefix: print(project({ SelectStmt: { ...typed.SelectStmt, whereClause: undefined, sortClause: undefined, limitCount: undefined, limitOption: "LIMIT_OPTION_DEFAULT" } }, p.flat ? viewOutputs(v, plan.schemas).keys ?? [] : p.names, keys, name)) + (typed.SelectStmt.whereClause ? ` WHERE (${expression(typed.SelectStmt.whereClause)})` : ""), extra: p.sources.filter(x => "cursor" in x).map(x => x.cursor) }; };
32
+ const suffixAst = { SelectStmt: { targetList: [{ ResTarget: { val: { A_Const: { ival: { ival: 1 } } } } }], sortClause: keys ? first.ast.SelectStmt.sortClause : undefined, limitCount: { ParamRef: { number: marker } }, limitOption: "LIMIT_OPTION_COUNT", op: "SETOP_NONE" } };
33
+ const tail = print(suffixAst).replace(/^SELECT 1/, "");
34
+ const expression = (ast) => print({ SelectStmt: { targetList: [{ ResTarget: { val: ast } }], limitOption: "LIMIT_OPTION_DEFAULT", op: "SETOP_NONE" } }).replace(/^SELECT /, "");
35
+ const fragment = (match, type) => {
36
+ const p = pagedOf({ schemas: plan.schemas, dialect }, c, undefined, match);
37
+ const ast = structuredClone(p.ast.SelectStmt.whereClause);
38
+ const renumber = (node) => { if (node.ParamRef) {
39
+ node.ParamRef.number = marker;
40
+ if (dialect.nativeOrder && (type === "date" || type === "timestamptz")) {
41
+ delete node.ParamRef;
42
+ node.Raw = { parts: ["CAST(CAST(", { ParamRef: { number: marker } }, ` AS text) AS ${type})`] };
43
+ return;
44
+ }
45
+ } for (const child of Object.values(node))
46
+ if (child && typeof child === "object")
47
+ Array.isArray(child) ? child.forEach(renumber) : renumber(child); };
48
+ renumber(ast);
49
+ return expression(ast);
50
+ };
51
+ return { marker: dialect.nativeOrder ? `$${marker}` : `?${marker}`, binds: c.binds, keys, first: read(first), ...(next ? { next: read(next) } : {}), tail, eq: Object.fromEntries((list.filterFields ?? []).map(field => { const col = v.columns?.[field] ?? v.columns?.[field.toLowerCase()], type = col && plan.schemas[col.schema]?.fields[col.field]; return [field, fragment({ eq: [{ column: field, value: "" }] }, type)]; })), ...(list.searchFields?.length ? { search: fragment({ search: { columns: list.searchFields, text: "x" } }) } : {}) };
52
+ });
53
+ all[name] = { caller: pages[0], trusted: pages[1] };
54
+ }
55
+ catch (error) {
56
+ refuse(`views/${name}/admin`, error instanceof Error ? error.message : String(error));
57
+ }
53
58
  }
54
59
  return all;
55
60
  }
56
- /** Only selected build-time Admin work; the ordinary generator remains unchanged. */
57
61
  export async function adminBindingsOf(plan, dialect, paging, wire, refusals) {
58
- let adminPages;
59
- try {
60
- adminPages = adminPaging(plan, dialect, paging, (ast, names, nkeys, name) => {
61
- const keys = [...names, ...Array.from({ length: nkeys }, (_, i) => `_k${i}`)];
62
- const projected = wireScalars(ast, keys, [...names.map(key => boundaryType(plan, name, key)), ...(paging[name]?.types ?? [])], wire, true);
63
- if (typeof projected === "string")
64
- throw Error(projected);
65
- return projected;
66
- });
67
- }
68
- catch (error) {
69
- refusals.push({ code: "GENERATE_UNSUPPORTED", path: "admin/views", message: error instanceof Error ? error.message : String(error) });
70
- }
62
+ const adminPages = adminPaging(plan, dialect, paging, (ast, names, nkeys, name) => {
63
+ const keys = [...names, ...Array.from({ length: nkeys }, (_, i) => `_k${i}`)];
64
+ const projected = wireScalars(ast, keys, [...names.map(key => boundaryType(plan, name, key)), ...(paging[name]?.types ?? [])], wire, true);
65
+ if (typeof projected === "string")
66
+ throw Error(projected);
67
+ return projected;
68
+ }, (path, message) => refusals.push({ code: "GENERATE_UNSUPPORTED", path, message }));
71
69
  return { ok: true, adminPages, adminEntries: await adminEntries(plan, dialect, wire, refusals) };
72
70
  }
@@ -1,7 +1,6 @@
1
1
  import { readFile, readdir, mkdir, rm, writeFile, lstat } from "node:fs/promises";
2
2
  import { dirname, join, resolve, relative, sep, isAbsolute } from "node:path";
3
3
  import { GENERATED } from "../domain/entry.js";
4
- /** Only marked generated files (and pre-header operation entries) can be removed. */
5
4
  async function ownedFiles(directory, prefix = "") {
6
5
  const entries = await readdir(join(directory, prefix), { withFileTypes: true }).catch(error => {
7
6
  if (error.code === "ENOENT")
@@ -22,15 +21,12 @@ async function ownedFiles(directory, prefix = "") {
22
21
  }
23
22
  return files;
24
23
  }
25
- /** Check never writes; normal generation removes obsolete SDK-owned files only. */
26
24
  export async function writeArtifacts(directory, files, check = false) {
27
25
  const root = resolve(directory);
28
26
  const rootStat = await lstat(root).catch(error => { if (error.code === "ENOENT")
29
27
  return undefined; throw error; });
30
28
  if (rootStat?.isSymbolicLink())
31
29
  throw new Error(`Generated output cannot follow an application-owned symlink: ${root}`);
32
- // Preflight all destinations before writing any: a symlink belongs to the app,
33
- // never to generated output (including links to somewhere inside the tree).
34
30
  for (const name of Object.keys(files)) {
35
31
  const child = relative(root, resolve(root, name));
36
32
  if (!child || isAbsolute(child) || child === ".." || child.startsWith(`..${sep}`))
@@ -3,7 +3,6 @@ import { getSchema } from "better-auth/db";
3
3
  import { ident } from "../kernel/dialect/common.js";
4
4
  function columnType(field, dialect) {
5
5
  const pg = dialect === "postgres";
6
- // Anything else, including the arrays the factory stores as JSON text, is text (Bun.SQL would encode that text into jsonb as a JSON string).
7
6
  switch (field.type) {
8
7
  case "number": return field.bigint ? "bigint" : "integer";
9
8
  case "boolean": return pg ? "boolean" : "integer";
@@ -12,12 +11,6 @@ function columnType(field, dialect) {
12
11
  default: return "text";
13
12
  }
14
13
  }
15
- /**
16
- * Better Auth's name for a field-level index (`<table>_<column>_idx`; past PostgreSQL's 63-byte identifier limit the middle is
17
- * cut and an FNV-1a hash of the full name is spliced in). Its helper lives in `@better-auth/core/db/internal`, which is not public
18
- * API, so the same rule is restated here: a database already migrated by Better Auth's CLI keeps matching names, and
19
- * `IF NOT EXISTS` skips the index instead of duplicating it.
20
- */
21
14
  export function fieldIndexName(table, column) {
22
15
  const full = `${table}_${column}_idx`;
23
16
  if (new TextEncoder().encode(full).length <= 63)
@@ -28,7 +21,6 @@ export function fieldIndexName(table, column) {
28
21
  const suffix = `_${(hash >>> 0).toString(16).padStart(8, "0")}_idx`;
29
22
  return full.slice(0, 63 - suffix.length) + suffix;
30
23
  }
31
- /** Better Auth's own `order`, then referenced tables before the tables that reference them. */
32
24
  function orderedTables(options) {
33
25
  const byOrder = Object.entries(getSchema(options)).filter(([, table]) => !table.disableMigrations).sort((a, b) => a[1].order - b[1].order);
34
26
  const ordered = [];
@@ -51,7 +43,6 @@ export function authSchemaSql(options, dialect) {
51
43
  const generateId = options.advanced?.database?.generateId;
52
44
  const idType = generateId === "serial" ? "integer" : generateId === "uuid" && dialect === "postgres" ? "uuid" : "text";
53
45
  const idColumn = generateId === "serial" ? (dialect === "postgres" ? "integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY" : "integer PRIMARY KEY") : `${idType} PRIMARY KEY NOT NULL${idType === "uuid" ? " DEFAULT gen_random_uuid()" : ""}`;
54
- // `references.field` is a logical field name; the column it points at may be renamed (`fields: { email: "email_address" }`).
55
46
  const tables = getAuthTables(options);
56
47
  const logical = Object.values(tables);
57
48
  const referenced = (ref) => logical.find(t => t.modelName === ref.model)?.fields[ref.field]?.fieldName ?? ref.field;