@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.
- package/README.md +15 -5
- package/dist/auth/adapter.js +1 -28
- package/dist/auth/admin.js +2 -5
- package/dist/auth/caller.js +0 -27
- package/dist/auth/consent.js +100 -0
- package/dist/auth/hardening.js +0 -7
- package/dist/auth/identity.js +0 -11
- package/dist/auth/oauth.js +0 -32
- package/dist/auth/resource-seeds.js +0 -2
- package/dist/auth/route-http.js +33 -0
- package/dist/auth/routes.js +5 -151
- package/dist/build/compiler.d.ts +19 -609
- package/dist/build/compiler.js +6 -6896
- package/dist/build/spec/domain/model/ManifestGrammar.d.ts +39 -1
- package/dist/build/spec/domain/model/RuntimePlan.d.ts +16 -3
- package/dist/build/spec/domain/model/SqlIr.d.ts +32 -0
- package/dist/build/spec/domain/model/UserFields.d.ts +1 -0
- package/dist/build/spec/kernel/diagnostic.d.ts +1 -1
- package/dist/bun/assets.js +0 -2
- package/dist/domain/adminOrder.js +0 -2
- package/dist/domain/columns.js +7 -20
- package/dist/domain/describe/chunk-P6PP6VVV.js +1 -0
- package/dist/domain/describe/index.d.ts +1 -0
- package/dist/domain/describe/index.js +1 -115
- package/dist/domain/describe/match.d.ts +19 -0
- package/dist/domain/describe/target-hash.js +1 -12
- package/dist/domain/entry.js +0 -14
- package/dist/domain/generated-tools.js +0 -1
- package/dist/domain/input.js +0 -2
- package/dist/domain/paging.js +0 -17
- package/dist/domain/recipes.js +10 -7
- package/dist/domain/userReferences.js +0 -1
- package/dist/domain/wireScalars.js +0 -6
- package/dist/infrastructure/adminEntries.js +27 -8
- package/dist/infrastructure/adminPaging.js +55 -57
- package/dist/infrastructure/artifacts.js +0 -4
- package/dist/infrastructure/authSchema.js +0 -9
- package/dist/infrastructure/bundle.js +45 -38
- package/dist/infrastructure/cli.js +26 -12
- package/dist/infrastructure/generate.js +26 -19
- package/dist/infrastructure/generateAuth.js +1 -9
- package/dist/infrastructure/help.js +55 -0
- package/dist/infrastructure/init.js +50 -0
- package/dist/infrastructure/inputs.js +0 -9
- package/dist/infrastructure/mounts.js +22 -0
- package/dist/infrastructure/wrangler.js +0 -6
- package/dist/kernel/authorization.js +0 -1
- package/dist/kernel/binds.d.ts +34 -7
- package/dist/kernel/binds.js +5 -11
- package/dist/kernel/caller-identity.d.ts +0 -3
- package/dist/kernel/caller-identity.js +0 -15
- package/dist/kernel/caller-mode.d.ts +4 -0
- package/dist/kernel/caller-mode.js +14 -0
- package/dist/kernel/contracts.d.ts +13 -10
- package/dist/kernel/contracts.js +14 -41
- package/dist/kernel/cron.js +0 -4
- package/dist/kernel/dialect/common.js +0 -3
- package/dist/kernel/dialect/postgres.js +0 -2
- package/dist/kernel/dialect/sqlite.js +1 -6
- package/dist/kernel/drain.js +2 -8
- package/dist/kernel/driver/bun-sqlite.js +3 -7
- package/dist/kernel/driver/d1.d.ts +1 -1
- package/dist/kernel/driver/d1.js +6 -4
- package/dist/kernel/driver/postgres.d.ts +2 -1
- package/dist/kernel/driver/postgres.js +12 -22
- package/dist/kernel/driver/transaction-context.js +0 -1
- package/dist/kernel/email.js +0 -4
- package/dist/kernel/host-access.js +2 -3
- package/dist/kernel/host-caller.d.ts +1 -1
- package/dist/kernel/host-caller.js +3 -6
- package/dist/kernel/json-body.js +0 -2
- package/dist/kernel/mcp-apps.js +0 -5
- package/dist/kernel/mcp-call.js +0 -1
- package/dist/kernel/mcp-handler.js +4 -27
- package/dist/kernel/mcp.js +0 -5
- package/dist/kernel/media.js +0 -18
- package/dist/kernel/paging.js +1 -9
- package/dist/kernel/publishing.js +3 -4
- package/dist/kernel/r2-media.js +0 -7
- package/dist/kernel/rest-dispatch.js +0 -1
- package/dist/kernel/rest-router.js +5 -8
- package/dist/kernel/rest.d.ts +2 -1
- package/dist/kernel/rest.js +1 -3
- package/dist/kernel/route-pattern.d.ts +26 -0
- package/dist/kernel/route-pattern.js +34 -0
- package/dist/kernel/scalars.js +1 -9
- package/dist/kernel/search.js +0 -1
- package/dist/kernel/site.js +0 -2
- package/dist/kernel/store.d.ts +26 -2
- package/dist/kernel/store.js +69 -49
- package/dist/kernel/surface-failure.d.ts +10 -0
- package/dist/kernel/surface-failure.js +29 -0
- package/dist/kernel/surface.js +0 -3
- package/dist/kernel/sweep.js +0 -1
- package/dist/kernel/translations.js +0 -1
- package/dist/kernel/user-caller.js +2 -8
- package/dist/kernel/user-fields.d.ts +3 -2
- package/dist/kernel/user-fields.js +9 -9
- package/dist/kernel/validation.js +0 -8
- package/dist/kernel/web-gateway.js +0 -2
- package/dist/kernel/web-head.js +0 -1
- package/dist/kernel/web-index.js +0 -1
- package/dist/kernel/web-response.js +0 -1
- package/dist/kernel/web-serve.js +97 -0
- package/dist/kernel/web.d.ts +1 -1
- package/dist/kernel/web.js +10 -91
- package/dist/kernel/where.js +0 -6
- package/dist/usecase/adminBindings.js +32 -23
- package/dist/usecase/adminClient.js +4 -22
- package/dist/usecase/adminDescriptors.js +2 -9
- package/dist/usecase/generate.js +14 -17
- package/dist/usecase/handlers.js +1 -4
- package/dist/usecase/mcp-media.js +0 -1
- package/dist/usecase/mcp.js +0 -4
- package/dist/usecase/paging.js +1 -3
- package/dist/usecase/publishing.js +20 -11
- package/dist/usecase/rest.js +5 -6
- package/dist/usecase/site.js +0 -1
- package/dist/usecase/stores.js +12 -17
- package/dist/usecase/tool-version.js +0 -3
- package/dist/usecase/translations.js +0 -2
- package/dist/usecase/validation.js +0 -20
- package/docs/examples/README.md +13 -5
- package/docs/examples/app-shell/README.md +91 -12
- package/docs/examples/app-shell/accept.mjs +91 -0
- package/docs/examples/app-shell/gitignore +7 -0
- package/docs/examples/app-shell/package.json +10 -9
- package/docs/examples/app-shell/smoke-kit.mjs +29 -15
- package/docs/examples/app-shell/src/routes.ts +6 -3
- package/docs/examples/app-shell/src/service.ts +2 -1
- package/docs/examples/club-events/README.md +7 -2
- package/docs/examples/club-events/manifests/club-events.yaml +17 -0
- package/docs/examples/club-events/smoke.mjs +7 -1
- package/docs/examples/generated-web/seed.sql +5 -4
- package/docs/examples/purchasing/README.md +12 -3
- package/docs/examples/purchasing/manifests/purchasing.yaml +24 -4
- package/docs/examples/purchasing/smoke.mjs +14 -0
- package/docs/examples/recipes/README.md +5 -3
- package/docs/handbook/cloudflare/media-r2.md +6 -3
- package/docs/handbook/cloudflare/service-entry.md +5 -0
- package/docs/handbook/concepts/lifecycle-and-locales.md +21 -1
- package/docs/handbook/examples/hub.md +1 -1
- package/docs/handbook/guides/admin-ui.md +1 -1
- package/docs/handbook/guides/agent-setup.md +20 -10
- package/docs/handbook/guides/agent-workflow-acceptance.md +225 -105
- package/docs/handbook/guides/local-testing.md +85 -0
- package/docs/handbook/guides/native-admin.md +242 -16
- package/docs/handbook/guides/native-execution.md +10 -8
- package/docs/handbook/guides/public-pages.md +44 -4
- package/docs/handbook/guides/revisions.md +584 -0
- package/docs/handbook/guides/size-and-startup.md +140 -0
- package/docs/handbook/guides/sql-patterns.md +312 -0
- package/docs/handbook/guides/typed-queries.md +146 -3
- package/docs/handbook/navigation.json +20 -0
- package/docs/handbook/reference/admin-descriptors.md +4 -1
- package/docs/handbook/reference/authorization.md +3 -1
- package/docs/handbook/reference/describe.md +42 -0
- package/docs/handbook/reference/diagnostics.md +16 -2
- package/docs/handbook/reference/procedure.md +6 -6
- package/docs/handbook/reference/schema.md +79 -9
- package/docs/handbook/reference/sql.md +65 -0
- package/docs/handbook/reference/surface.md +8 -4
- package/docs/handbook/reference/trigger.md +12 -0
- package/docs/handbook/reference/view.md +11 -3
- package/docs/handbook/releases/index.md +71 -7
- package/docs/handbook/start/agents.md +49 -0
- package/docs/handbook/start/project-and-cli.md +10 -0
- package/docs/handbook/start/quickstart-worker.md +1 -1
- package/docs/skills/develop/SKILL.md +19 -6
- package/docs/skills/integrate/SKILL.md +3 -1
- package/docs/skills/performance/SKILL.md +169 -0
- package/docs/skills/update/SKILL.md +10 -4
- package/docs/upgrade-0.1-to-0.2.md +151 -39
- package/package.json +31 -11
- package/skills/README.md +6 -2
- package/skills/mantle/SKILL.md +53 -120
- package/dist/domain/describe/chunk-ZVXBGP7Y.js +0 -424
- package/skills/mantle/references/local-testing.md +0 -20
- package/skills/mantle/references/web.md +0 -41
- package/skills/mantle/references/website.md +0 -51
package/dist/domain/paging.js
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
|
-
/** Build-time proofs for the compiler's keyset keys. No AST reaches a generated operation. */
|
|
2
1
|
import { NATIVE_COLUMNS, NATIVE_NOT_NULL, notNullColumns } from "../build/compiler.js";
|
|
3
2
|
import { declaredType } from "./columns.js";
|
|
4
|
-
/** Storage column -> the native field a manifest index names. */
|
|
5
3
|
const NATIVE_FIELDS = new Map(Object.entries(NATIVE_COLUMNS).map(([field, column]) => [column, field]));
|
|
6
4
|
const fieldName = (n) => n.ColumnRef?.fields?.at(-1)?.String?.sval;
|
|
7
5
|
const scalar = (n) => !!(n.ParamRef || n.A_Const || n.TypeCast && scalar(n.TypeCast.arg));
|
|
8
|
-
// A user-field term lowers to the dialect's anyOf, `(col) IN (SELECT value FROM json_each((?)))` or `(col) = ANY(CAST(...(?)...))`.
|
|
9
6
|
const anyOf = (raw) => raw?.length === 8 && raw[1]?.ColumnRef && raw[5]?.ParamRef && /^ (IN \(SELECT value FROM json_each\(|= ANY\()/.test(raw[3]);
|
|
10
|
-
// A scope prints `/*s:field*/ kept /*|*/ kept OR 1 = 1 /*;*/`, so `{ all: true }` drops it; any other anyOf is a restrict, which it never lifts.
|
|
11
7
|
const scopeTerm = (raw) => typeof raw?.[0] === "string" && raw[0].startsWith("/*s:");
|
|
12
8
|
const restricts = (n) => !!n && typeof n === "object" && (anyOf(n.Raw?.parts) || !scopeTerm(n.Raw?.parts) && Object.values(n).some(restricts));
|
|
13
9
|
const aggregate = (n) => !!n && !n.SubLink && !n.SelectStmt && (n.FuncCall && !n.FuncCall.over && ["count", "sum", "min", "max", "avg"].includes(n.FuncCall.funcname?.at(-1)?.String?.sval) || Object.values(n).some(v => v && typeof v === "object" && (Array.isArray(v) ? v.some(aggregate) : aggregate(v))));
|
|
14
|
-
/** Only complete grouped tuples, base ids and non-fanning singleton joins are proved. */
|
|
15
10
|
export function provePaging(plan, ast) {
|
|
16
11
|
let reason;
|
|
17
12
|
const fail = (message) => { reason = message; return undefined; };
|
|
@@ -50,7 +45,6 @@ export function provePaging(plan, ast) {
|
|
|
50
45
|
return fail(`Fanning join '${[...aliases.keys()].filter(alias => !before.has(alias)).join(", ")}': join on the joined Schema's id or a single-field unique key, or reduce it to one row.`);
|
|
51
46
|
if (n.JoinExpr.jointype === "JOIN_FULL" || n.JoinExpr.jointype === "JOIN_RIGHT")
|
|
52
47
|
return;
|
|
53
|
-
// The right side of LEFT JOIN may be absent: its fields cannot become NOT NULL cursor keys.
|
|
54
48
|
if (n.JoinExpr.jointype === "JOIN_LEFT")
|
|
55
49
|
for (const [alias, r] of aliases)
|
|
56
50
|
if (!before.has(alias))
|
|
@@ -73,7 +67,6 @@ export function provePaging(plan, ast) {
|
|
|
73
67
|
else if (sub)
|
|
74
68
|
fact = select(sub.subquery.SelectStmt, ctes)?.relation;
|
|
75
69
|
if (fact) {
|
|
76
|
-
// Two aliases of one Schema/CTE are distinct row sources, including a singleton joined to many rows.
|
|
77
70
|
const sources = new Map();
|
|
78
71
|
fact = { ...fact, fields: new Map([...fact.fields].map(([name, f]) => {
|
|
79
72
|
if (!sources.has(f.source))
|
|
@@ -117,8 +110,6 @@ export function provePaging(plan, ast) {
|
|
|
117
110
|
const fixed = (n) => {
|
|
118
111
|
if (n?.BoolExpr?.boolop === "AND_EXPR")
|
|
119
112
|
n.BoolExpr.args.forEach(fixed);
|
|
120
|
-
// `col IN auth.user(field)` (a scope or restrict) never fixes a prefix (#1644): the plan is prepared for a set, so
|
|
121
|
-
// native ORDER BY after it reads and sorts all the caller's rows. It is only remembered for the refusal's guidance.
|
|
122
113
|
const raw = n?.Raw?.parts;
|
|
123
114
|
if (scopeTerm(raw))
|
|
124
115
|
return fixed(raw[1]);
|
|
@@ -151,9 +142,7 @@ export function provePaging(plan, ast) {
|
|
|
151
142
|
const same = (a, b) => a && b && a.name === b.name && a.source === b.source;
|
|
152
143
|
if (sorts.length) {
|
|
153
144
|
if (group.length) {
|
|
154
|
-
// Full GROUP BY keys, not an id from an arbitrary row in the group, make the result unique.
|
|
155
145
|
if (!group.every(g => {
|
|
156
|
-
// GROUP BY prefers an input column over a colliding output alias; ORDER BY does the reverse.
|
|
157
146
|
const source = g.ColumnRef?.fields?.length === 1 ? [...aliases.values()].filter(r => r.names.has(fieldName(g))) : [];
|
|
158
147
|
const f = source.length ? source.length === 1 ? source[0].fields.get(fieldName(g)) : undefined : resolve(g);
|
|
159
148
|
return f?.notNull && keys.some(k => same(k, f));
|
|
@@ -175,21 +164,16 @@ export function provePaging(plan, ast) {
|
|
|
175
164
|
return order.every((f, n) => index[i + n] === f.name);
|
|
176
165
|
});
|
|
177
166
|
if (!serving(base.fixed)) {
|
|
178
|
-
// SQL names a column (`created_at`), a manifest index the field (`createdAt`).
|
|
179
167
|
const index = serving(new Set([...base.fixed, ...base.sets])), named = (c) => schema.names?.[c] ?? c, field = (c) => schema.names?.[c] ?? NATIVE_FIELDS.get(c) ?? c;
|
|
180
168
|
const columns = order.map(f => named(f.name)).join(", "), fields = order.map(f => field(f.name)).join(", ");
|
|
181
169
|
if (!index)
|
|
182
170
|
return fail(`Missing index for ORDER BY (${columns}): declare a Schema index with [${fields}] in this order after any fixed equality prefix.`);
|
|
183
171
|
const prefix = index.slice(0, index.indexOf(order[0].name)), set = prefix.filter(c => !base.fixed.has(c));
|
|
184
|
-
// The suggested order is the index from its first set-bound column: an equality-fixed column between stays in it.
|
|
185
172
|
const lead = prefix.slice(prefix.indexOf(set[0])), quoted = (cs) => cs.map(c => `'${field(c)}'`).join(", ");
|
|
186
|
-
// The scope-first order is offered only when SQL can name every added column and none can be null. The owner
|
|
187
|
-
// scope column is NOT NULL in storage, but a View cannot address it.
|
|
188
173
|
const notNull = notNullColumns(schema), owner = lead.filter(c => c === schema.scope), nullable = lead.filter(c => !NATIVE_NOT_NULL.has(c) && !notNull.has(c) && c !== schema.scope);
|
|
189
174
|
const dir = (f) => sorts[keys.indexOf(f)].SortBy.sortby_dir === "SORTBY_DESC" ? " DESC" : "";
|
|
190
175
|
const sorted = [...lead.map(c => named(c) + dir(order[0])), ...order.map(f => named(f.name) + dir(f))].join(", ");
|
|
191
176
|
const desc = order.some(f => dir(f)) ? ", read backwards; on PostgreSQL confirm with EXPLAIN that no Sort remains" : "";
|
|
192
|
-
// `{ all: true }` drops only scope terms: offer it an index on the View's equality-fixed columns, unless a restrict remains.
|
|
193
177
|
const eq = [...base.fixed].filter(c => c !== "id" && !order.some(f => f.name === c));
|
|
194
178
|
const ways = [...nullable.length || owner.length ? [] : [`ORDER BY ${sorted} (same index${desc})`], ...restricts(ast) ? [] : [`for { all: true } callers only, index [${[...eq.map(field), fields].join(", ")}, id]`]];
|
|
195
179
|
const why = [...owner.length ? [`${quoted(owner)} is the owner column, which SQL cannot name`] : [], ...nullable.length ? [`${quoted(nullable)} can be null`] : []];
|
|
@@ -205,7 +189,6 @@ export function provePaging(plan, ast) {
|
|
|
205
189
|
const result = select(ast.SelectStmt);
|
|
206
190
|
return result ? { types: result.keys.map(f => f.type), single: result.relation.single } : reason ? { types: [], single: false, reason } : undefined;
|
|
207
191
|
}
|
|
208
|
-
/** Cast only cursor ParamRefs in the outer predicate, never authored literals or bare projections. */
|
|
209
192
|
export function castCursor(ast, sources, types, postgres) {
|
|
210
193
|
const typeOf = (source) => source && "cursor" in source ? types[source.cursor] : undefined;
|
|
211
194
|
const typed = (type) => type && (postgres ? ["timestamptz", "date"] : ["integer", "int4", "int8"]).includes(type);
|
package/dist/domain/recipes.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
const KINDS = new Set(["uid", "admin", "user", "now", "cutoff", "const", "input", "version"]);
|
|
2
|
-
/** @internal */
|
|
1
|
+
const KINDS = new Set(["uid", "admin", "user", "now", "cutoff", "const", "input", "version", "scoped"]);
|
|
3
2
|
export function recipesOf(statement, fields, refuse, values) {
|
|
3
|
+
const field = (bound) => {
|
|
4
|
+
const name = fields.find(field => field.toLowerCase() === String(bound).toLowerCase());
|
|
5
|
+
if (name === undefined)
|
|
6
|
+
refuse(`Input bind '${bound}' has no declared field.`);
|
|
7
|
+
return name ?? String(bound);
|
|
8
|
+
};
|
|
4
9
|
return statement.binds.map(b => {
|
|
5
10
|
if (!KINDS.has(b.k))
|
|
6
11
|
refuse(`Bind recipe '${b.k}' needs an explicit native implementation.`);
|
|
7
12
|
if (b.k === "now" || b.k === "cutoff")
|
|
8
13
|
values?.add(b.k);
|
|
14
|
+
if (b.k === "scoped" && b.name !== undefined)
|
|
15
|
+
return { ...b, name: field(b.name) };
|
|
9
16
|
if (b.k !== "input")
|
|
10
17
|
return b;
|
|
11
18
|
if (b.type === "date" || b.type === "timestamptz")
|
|
12
19
|
values?.add(b.type === "date" ? "date" : "timestamp");
|
|
13
|
-
|
|
14
|
-
if (name === undefined)
|
|
15
|
-
refuse(`Input bind '${b.name}' has no declared field.`);
|
|
16
|
-
// only a date-time or a date is converted at bind time (`encode` in kernel/binds)
|
|
17
|
-
return { k: "input", name: name ?? String(b.name), ...(b.type === "timestamptz" || b.type === "date" ? { type: b.type } : {}) };
|
|
20
|
+
return { k: "input", name: field(b.name), ...(b.type === "timestamptz" || b.type === "date" || b.type === "json" ? { type: b.type } : {}) };
|
|
18
21
|
});
|
|
19
22
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Conditional capability imports keep applications without user references unchanged. */
|
|
2
1
|
export function userReferences(plan, sqlite) {
|
|
3
2
|
const used = !!plan.access;
|
|
4
3
|
return { used, code: used ? `import * as USER from "@aotter/mantle/user-fields";\nconst USER_VALUES = { scopeInsert: USER.scopeInsert, scopedAll: USER.scopedAll, userRefs: USER.userRefs(${sqlite}, 1) };` : '', values: used ? '...USER_VALUES' : '' };
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { declaredType } from "./columns.js";
|
|
2
2
|
const raw = (...parts) => ({ Raw: { parts } });
|
|
3
|
-
/** Temporal SQL forms, plus selected integer text before driver rounding. */
|
|
4
3
|
const wire = (type, d, v, boundary = false) => type === "timestamptz" ? raw(...d.wireTimestamp(v)) : type === "date" ? raw(...d.wireDate(v)) : boundary && type && ["integer", "int8", "int4"].includes(type) ? raw("CAST(", v, " AS text)") : undefined;
|
|
5
|
-
/**
|
|
6
|
-
* Formats declared temporal outputs and selected integers in a View's top-level SELECT, preserving names.
|
|
7
|
-
* Returns a refusal when the outputs cannot be matched to SELECT items by position.
|
|
8
|
-
*/
|
|
9
4
|
export function wireScalars(ast, keys, types, d, boundary = false) {
|
|
10
5
|
if (!types.some(type => type === "timestamptz" || type === "date" || boundary && type && ["integer", "int8", "int4"].includes(type)) || !ast.SelectStmt)
|
|
11
6
|
return ast;
|
|
@@ -24,7 +19,6 @@ export function wireScalars(ast, keys, types, d, boundary = false) {
|
|
|
24
19
|
const out = select(ast.SelectStmt);
|
|
25
20
|
return typeof out === "string" ? out : { ...ast, SelectStmt: out };
|
|
26
21
|
}
|
|
27
|
-
/** The same wire form for a write's RETURNING list (Core has already expanded `*`): a column of the written table is wrapped by its declared type. */
|
|
28
22
|
export function wireReturning(ast, schemas, d, boundary = false) {
|
|
29
23
|
const [kind, stmt] = Object.entries(ast)[0] ?? [];
|
|
30
24
|
const list = stmt?.returningClause?.exprs;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Selected Admin queries/procedures: Core checks, rewrites and prints every native fragment at generation. */
|
|
2
1
|
import { enumOptions, protectedColumns } from "../build/compiler.js";
|
|
3
2
|
import { parsePgSql } from "../build/compiler.js";
|
|
4
3
|
import { compileProgram } from "../build/compiler.js";
|
|
@@ -6,6 +5,8 @@ import { adminOrder } from "../domain/adminOrder.js";
|
|
|
6
5
|
import { declaredType, serverOwned } from "../domain/columns.js";
|
|
7
6
|
import { wireScalars, wireReturning } from "../domain/wireScalars.js";
|
|
8
7
|
import { recipesOf } from "../domain/recipes.js";
|
|
8
|
+
import { 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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
first.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
marker
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
node.ParamRef
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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;
|