@vxil/config 0.7.0 → 0.8.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/dist/index.d.ts +31 -17
- package/dist/index.js +26 -1
- package/package.json +2 -2
- package/src/index.test.ts +25 -1
- package/src/index.ts +52 -19
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface FieldDef {
|
|
|
14
14
|
relationTo?: string;
|
|
15
15
|
computed?: boolean;
|
|
16
16
|
compute?: unknown;
|
|
17
|
-
/** Claims-table-enforced value uniqueness across live items (
|
|
17
|
+
/** Claims-table-enforced value uniqueness across live items (guide ch. 4):
|
|
18
18
|
* a duplicate write is a clean 409 unique_violation. Scalar types only
|
|
19
19
|
* (string/int/float/datetime/relation/file). Carried on collection/field
|
|
20
20
|
* CREATE by `vxil push` and server apply; since 2026-07-17 `vxil push` also
|
|
@@ -22,12 +22,12 @@ export interface FieldDef {
|
|
|
22
22
|
* field re-add — packages/cli/src/cms.ts FIELD_ATTRS); attributes push
|
|
23
23
|
* cannot reconcile surface as a visible plan warning, never a silent no-op. */
|
|
24
24
|
unique?: boolean;
|
|
25
|
-
/** Per-relation-field delete behavior (
|
|
26
|
-
* when the referenced item is deleted — or `restrict
|
|
25
|
+
/** Per-relation-field delete behavior (guide ch. 4): bounded, atomic fan-out
|
|
26
|
+
* when the referenced item is deleted — or `restrict`, which REFUSES
|
|
27
27
|
* the delete with 409 `referenced` while a live reference exists. Same
|
|
28
28
|
* carriage + reconcile behavior as `unique`. */
|
|
29
29
|
onDelete?: 'cascade' | 'set_null' | 'restrict';
|
|
30
|
-
/** FIELD-LEVEL READ SECURITY (
|
|
30
|
+
/** FIELD-LEVEL READ SECURITY (guide ch. 4): the end-user ORG ROLE slugs allowed
|
|
31
31
|
* to READ this field. Omitted/`[]` = ungated (every caller sees it — today's
|
|
32
32
|
* behavior). A non-empty list is FAIL-SAFE: in VERIFIED end-user mode the
|
|
33
33
|
* field is OMITTED from every read (get / list / query / `$expand` at any
|
|
@@ -40,8 +40,23 @@ export interface FieldDef {
|
|
|
40
40
|
* `orgs` role alphabet). The gate
|
|
41
41
|
* never affects WRITES. Carried by BOTH push paths and alterable in place. */
|
|
42
42
|
readRoles?: string[];
|
|
43
|
+
/** EQUALITY INDEX for an UNSLOTTED field (guide ch. 4 "Indexed equality"):
|
|
44
|
+
* `=` / `$eq` / `$in` filters on it are index-served instead of a bounded
|
|
45
|
+
* scan — results are identical either way. At most 4 per collection (the
|
|
46
|
+
* equality-key budget, separate from the 8 index slots); not together with
|
|
47
|
+
* `indexSlot` (a slot already indexes equality) and not on a computed field
|
|
48
|
+
* (both checked here at define time and again by the server). Carried by
|
|
49
|
+
* BOTH push paths and alterable in place; on a collection over 2,000 live
|
|
50
|
+
* items `vxil push` also runs the re-index that arms it. */
|
|
51
|
+
indexed?: boolean;
|
|
43
52
|
}
|
|
44
|
-
/**
|
|
53
|
+
/** The per-collection budget of `indexed: true` fields (cms/0158: e1…e4). */
|
|
54
|
+
export declare const CMS_INDEXED_FIELD_BUDGET = 4;
|
|
55
|
+
/** Define-time checks of a collection's `indexed` fields — the same refusals
|
|
56
|
+
* the server makes (422), so a config fails at `defineConfig` / `collection()`
|
|
57
|
+
* instead of mid-push. Throws on the first violation. */
|
|
58
|
+
export declare function assertIndexedFields(name: string, def: CollectionDef): void;
|
|
59
|
+
/** One config-declared per-record ACTION button (guide ch. 4): exactly ONE
|
|
45
60
|
* human-initiated step — pressing it invokes the deployed tenant function `fn`
|
|
46
61
|
* with `{ collection, item_id, action, actor, item }` and returns its result.
|
|
47
62
|
* No conditions, no chaining, no scheduling. `key` matches
|
|
@@ -56,7 +71,7 @@ export interface CmsActionDef {
|
|
|
56
71
|
* `FieldDef` itself so it can never drift from the authoring type: it is every
|
|
57
72
|
* key of `FieldDef` except `type` (which is required, not an optional carried
|
|
58
73
|
* attribute). This is the compile-time source of truth for the config-carriage
|
|
59
|
-
* completeness ratchet
|
|
74
|
+
* completeness ratchet: the CLI carriage
|
|
60
75
|
* table (`packages/cli/src/cms.ts` FIELD_ATTRS) and the server one
|
|
61
76
|
* (`workers/control-plane/src/handlers/apply.ts` FIELD_ATTR_DELTA) each pin
|
|
62
77
|
* `keyof` coverage against a set that equals this — so a 9th attribute added to
|
|
@@ -83,7 +98,7 @@ export interface CollectionDef {
|
|
|
83
98
|
* server-caller mode. Not a hard-isolation predicate; a single declarative
|
|
84
99
|
* flag the typed worker consults. Omit for shared/reference collections. */
|
|
85
100
|
ownerField?: string;
|
|
86
|
-
/** Public delivery (
|
|
101
|
+
/** Public delivery (guide ch. 4): when true, this collection's PUBLISHED items
|
|
87
102
|
* are servable through the keyless, edge-cached GET /v1/cms/public/:tenantId/
|
|
88
103
|
* :collection lane (no API key). Owner-UNSCOPED (public content, not per-user);
|
|
89
104
|
* the owner_field is stripped from every served row. Default false. Carried by
|
|
@@ -91,22 +106,22 @@ export interface CollectionDef {
|
|
|
91
106
|
* with a regression test on each — an attribute read by no enforcing path ships
|
|
92
107
|
* inert (the validation.unique / owner_field precedent). */
|
|
93
108
|
public?: boolean;
|
|
94
|
-
/** Per-record action buttons (
|
|
109
|
+
/** Per-record action buttons (guide ch. 4): `[{ key, label, fn }]`, stored on
|
|
95
110
|
* the collection (model data, not a config leaf). The dashboard renders one
|
|
96
111
|
* button per action on each record row; `POST /v1/cms/items/:coll/:id/actions/
|
|
97
112
|
* :key` invokes `fn` once. Carried by BOTH push paths (create + reconcile on
|
|
98
113
|
* existing collections — config is the source of truth; `[]`/absent clears). */
|
|
99
114
|
actions?: CmsActionDef[];
|
|
100
115
|
}
|
|
101
|
-
/** The auth lifecycle events an `authHook` binding may subscribe to
|
|
102
|
-
*
|
|
116
|
+
/** The auth lifecycle events an `authHook` binding may subscribe to
|
|
117
|
+
* (2026-09-10) — a CLOSED union; ONE event per binding, one audit
|
|
103
118
|
* event → one function invocation, no branching inside vxil:
|
|
104
119
|
* user.created — auth.user.created { user_id, method, is_anonymous }
|
|
105
120
|
* session.created — auth.session.created { user_id, session_id }
|
|
106
121
|
* session.revoked — auth.session.revoked { user_id, session_id, reason }
|
|
107
122
|
* signin.failure — auth.signin.failure { email_hash | user_id, reason } */
|
|
108
123
|
export type AuthHookEvent = 'user.created' | 'session.created' | 'session.revoked' | 'signin.failure';
|
|
109
|
-
/** The opt-in re-delivery of a platform-delivered trigger (
|
|
124
|
+
/** The opt-in re-delivery of a platform-delivered trigger (guide ch. 8).
|
|
110
125
|
* DEFAULT OFF: the platform acknowledges every delivery with a 200 whatever
|
|
111
126
|
* the handler returned — an application error is observed (`functions.run.failed`),
|
|
112
127
|
* never re-delivered. With `retry` set on a `queue` / `webhook` / `cmsHook` /
|
|
@@ -126,7 +141,7 @@ export interface FunctionTriggerRetry {
|
|
|
126
141
|
/** 1–5 attempts in all (1 = a failed attempt dead-letters at once, no re-delivery) */
|
|
127
142
|
maxAttempts: number;
|
|
128
143
|
}
|
|
129
|
-
/** A function trigger (
|
|
144
|
+
/** A function trigger (guide ch. 8). cmsHook fires on a CMS write;
|
|
130
145
|
* authHook fires on ONE auth lifecycle event (default `user.created` — the
|
|
131
146
|
* post-signup hook; at-least-once, ~1min fanout latency). */
|
|
132
147
|
export type FunctionTrigger = {
|
|
@@ -228,7 +243,7 @@ export interface FunctionDef {
|
|
|
228
243
|
timeoutMs?: number;
|
|
229
244
|
};
|
|
230
245
|
enabled?: boolean;
|
|
231
|
-
/** optional typed contract surfaced by `vxil gen` (Level 1
|
|
246
|
+
/** optional typed contract surfaced by `vxil gen` (Level 1; guide ch. 8). */
|
|
232
247
|
signature?: {
|
|
233
248
|
input?: unknown;
|
|
234
249
|
output?: unknown;
|
|
@@ -263,8 +278,7 @@ export declare const RELEASED_API_VERSIONS: readonly ApiVersion[];
|
|
|
263
278
|
/** Recursive partial: a `vxil.config.ts` may write any SUBSET of a nested bag
|
|
264
279
|
* (`auth: { otp: { enabled: true } }`) — the server applies the defaults for
|
|
265
280
|
* the rest (Value.Default). A shallow `Partial` made `tsc` reject every
|
|
266
|
-
* partially-written nested bag, including the guide's own skeleton
|
|
267
|
-
* (found by the 2026-09-19 cvskit dry-run). Arrays and primitives are kept
|
|
281
|
+
* partially-written nested bag, including the guide's own skeleton. Arrays and primitives are kept
|
|
268
282
|
* as-is; only plain object bags recurse. */
|
|
269
283
|
export type DeepPartial<T> = T extends readonly unknown[] ? T : T extends object ? {
|
|
270
284
|
[K in keyof T]?: DeepPartial<T[K]>;
|
|
@@ -303,7 +317,7 @@ export interface VxilConfig {
|
|
|
303
317
|
};
|
|
304
318
|
/** (b) CMS SCHEMA-AS-CODE — collections + fields. `push` reconciles them
|
|
305
319
|
* additively against GET /v1/cms/collections (no destructive drops without
|
|
306
|
-
* --allow-destructive).
|
|
320
|
+
* --allow-destructive). Validation hooks stay in features.cms.hooks (config). */
|
|
307
321
|
cms?: {
|
|
308
322
|
collections?: Record<string, CollectionDef>;
|
|
309
323
|
};
|
|
@@ -319,7 +333,7 @@ export interface VxilConfig {
|
|
|
319
333
|
* from JS callers with no compile gate, or a stale generated value) fails fast
|
|
320
334
|
* here rather than 404-ing every request against a nonexistent major. */
|
|
321
335
|
export declare function defineConfig(c: VxilConfig): VxilConfig;
|
|
322
|
-
/** Authoring helper for split-out cms/<name>.ts collection modules (
|
|
336
|
+
/** Authoring helper for split-out cms/<name>.ts collection modules (guide ch. 5).
|
|
323
337
|
* `field('title', 'string', { required: true, indexSlot: 's1' })`. */
|
|
324
338
|
export declare function field(type: FieldType, opts?: Omit<FieldDef, 'type'>): FieldDef;
|
|
325
339
|
/** Authoring helper for a collection module: `export default collection({...})`. */
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/** The per-collection budget of `indexed: true` fields (cms/0158: e1…e4). */
|
|
2
|
+
export const CMS_INDEXED_FIELD_BUDGET = 4;
|
|
3
|
+
/** Define-time checks of a collection's `indexed` fields — the same refusals
|
|
4
|
+
* the server makes (422), so a config fails at `defineConfig` / `collection()`
|
|
5
|
+
* instead of mid-push. Throws on the first violation. */
|
|
6
|
+
export function assertIndexedFields(name, def) {
|
|
7
|
+
let n = 0;
|
|
8
|
+
for (const [fname, f] of Object.entries(def.fields ?? {})) {
|
|
9
|
+
if (f.indexed !== true)
|
|
10
|
+
continue;
|
|
11
|
+
n += 1;
|
|
12
|
+
if (f.indexSlot) {
|
|
13
|
+
throw new Error(`vxil.config: cms.${name}.${fname} sets both indexed and indexSlot '${f.indexSlot}' — a slot already indexes equality; use one`);
|
|
14
|
+
}
|
|
15
|
+
if (f.computed) {
|
|
16
|
+
throw new Error(`vxil.config: cms.${name}.${fname} is computed — a computed field cannot be indexed (bind it to an indexSlot instead)`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (n > CMS_INDEXED_FIELD_BUDGET) {
|
|
20
|
+
throw new Error(`vxil.config: cms.${name} has ${n} indexed fields — at most ${CMS_INDEXED_FIELD_BUDGET} per collection`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
1
23
|
/** The released majors `defineConfig` validates `apiVersion` against. Widened
|
|
2
24
|
* only by a real v2. */
|
|
3
25
|
export const RELEASED_API_VERSIONS = ['v1'];
|
|
@@ -11,14 +33,17 @@ export function defineConfig(c) {
|
|
|
11
33
|
`(released: ${RELEASED_API_VERSIONS.map((v) => `'${v}'`).join(', ')}). ` +
|
|
12
34
|
'A new major ships as a new path with its own deprecation window — see https://vxil.com/docs/guide/12-going-to-production.');
|
|
13
35
|
}
|
|
36
|
+
for (const [name, def] of Object.entries(c.cms?.collections ?? {}))
|
|
37
|
+
assertIndexedFields(name, def);
|
|
14
38
|
return c;
|
|
15
39
|
}
|
|
16
|
-
/** Authoring helper for split-out cms/<name>.ts collection modules (
|
|
40
|
+
/** Authoring helper for split-out cms/<name>.ts collection modules (guide ch. 5).
|
|
17
41
|
* `field('title', 'string', { required: true, indexSlot: 's1' })`. */
|
|
18
42
|
export function field(type, opts = {}) {
|
|
19
43
|
return { type, ...opts };
|
|
20
44
|
}
|
|
21
45
|
/** Authoring helper for a collection module: `export default collection({...})`. */
|
|
22
46
|
export function collection(def) {
|
|
47
|
+
assertIndexedFields(def.singular ?? 'collection', def);
|
|
23
48
|
return def;
|
|
24
49
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vxil/config",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Typed vxil.config.ts authoring for the vxil backend platform — defineConfig, feature and cms field definitions (published for @vxil/cli).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://vxil.com",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"src"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@vxil/feature-configs": "0.7.
|
|
25
|
+
"@vxil/feature-configs": "0.7.1"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|
|
28
28
|
"access": "public"
|
package/src/index.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
assertIndexedFields, CMS_INDEXED_FIELD_BUDGET, collection, defineConfig, field, RELEASED_API_VERSIONS,
|
|
4
|
+
} from './index.js';
|
|
3
5
|
|
|
4
6
|
describe('defineConfig', () => {
|
|
5
7
|
it('is an identity function (returns its argument verbatim)', () => {
|
|
@@ -26,3 +28,25 @@ describe('defineConfig', () => {
|
|
|
26
28
|
.toThrow(/docs\/guide\/12-going-to-production/);
|
|
27
29
|
});
|
|
28
30
|
});
|
|
31
|
+
|
|
32
|
+
describe('indexed fields (PERF-26b) — define-time checks', () => {
|
|
33
|
+
it('accepts up to four unslotted indexed fields', () => {
|
|
34
|
+
const c = collection({ fields: {
|
|
35
|
+
a: field('string', { indexed: true }), b: field('int', { indexed: true }),
|
|
36
|
+
c: field('bool', { indexed: true }), d: field('string', { indexed: true }),
|
|
37
|
+
e: field('string', { indexSlot: 's1' }),
|
|
38
|
+
} });
|
|
39
|
+
expect(Object.keys(c.fields)).toHaveLength(5);
|
|
40
|
+
expect(CMS_INDEXED_FIELD_BUDGET).toBe(4);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('refuses a fifth, indexed + indexSlot, and a computed indexed field', () => {
|
|
44
|
+
expect(() => defineConfig({ cms: { collections: { posts: { fields: Object.fromEntries(
|
|
45
|
+
['a', 'b', 'c', 'd', 'e'].map((k) => [k, field('string', { indexed: true })]),
|
|
46
|
+
) } } } })).toThrow(/at most 4 per collection/);
|
|
47
|
+
expect(() => collection({ fields: { a: field('string', { indexed: true, indexSlot: 's1' }) } }))
|
|
48
|
+
.toThrow(/already indexes equality/);
|
|
49
|
+
expect(() => assertIndexedFields('posts', { fields: { a: field('string', { indexed: true, computed: true }) } }))
|
|
50
|
+
.toThrow(/computed/);
|
|
51
|
+
});
|
|
52
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// so `vxil push` can compile it to a JSON manifest. Type-checking happens in the
|
|
5
5
|
// tenant's editor (real autocomplete from the @vxil/feature-configs Static<>
|
|
6
6
|
// types); the control plane never executes this file. This keeps config-as-code
|
|
7
|
-
//
|
|
8
|
-
// is (a) the
|
|
7
|
+
// consistent — config is DATA; the only tenant CODE that crosses into vxil
|
|
8
|
+
// is (a) the hook expressions (a closed sandbox) and (b) the functions/
|
|
9
9
|
// sources (the opt-in, plan-limited, egress-guarded crossing). Everything here is
|
|
10
10
|
// declarative. See https://vxil.com/docs/guide/05-typed-sdk-and-cli.
|
|
11
11
|
import type {
|
|
@@ -33,7 +33,7 @@ export interface FieldDef {
|
|
|
33
33
|
relationTo?: string;
|
|
34
34
|
computed?: boolean;
|
|
35
35
|
compute?: unknown;
|
|
36
|
-
/** Claims-table-enforced value uniqueness across live items (
|
|
36
|
+
/** Claims-table-enforced value uniqueness across live items (guide ch. 4):
|
|
37
37
|
* a duplicate write is a clean 409 unique_violation. Scalar types only
|
|
38
38
|
* (string/int/float/datetime/relation/file). Carried on collection/field
|
|
39
39
|
* CREATE by `vxil push` and server apply; since 2026-07-17 `vxil push` also
|
|
@@ -41,12 +41,12 @@ export interface FieldDef {
|
|
|
41
41
|
* field re-add — packages/cli/src/cms.ts FIELD_ATTRS); attributes push
|
|
42
42
|
* cannot reconcile surface as a visible plan warning, never a silent no-op. */
|
|
43
43
|
unique?: boolean;
|
|
44
|
-
/** Per-relation-field delete behavior (
|
|
45
|
-
* when the referenced item is deleted — or `restrict
|
|
44
|
+
/** Per-relation-field delete behavior (guide ch. 4): bounded, atomic fan-out
|
|
45
|
+
* when the referenced item is deleted — or `restrict`, which REFUSES
|
|
46
46
|
* the delete with 409 `referenced` while a live reference exists. Same
|
|
47
47
|
* carriage + reconcile behavior as `unique`. */
|
|
48
48
|
onDelete?: 'cascade' | 'set_null' | 'restrict';
|
|
49
|
-
/** FIELD-LEVEL READ SECURITY (
|
|
49
|
+
/** FIELD-LEVEL READ SECURITY (guide ch. 4): the end-user ORG ROLE slugs allowed
|
|
50
50
|
* to READ this field. Omitted/`[]` = ungated (every caller sees it — today's
|
|
51
51
|
* behavior). A non-empty list is FAIL-SAFE: in VERIFIED end-user mode the
|
|
52
52
|
* field is OMITTED from every read (get / list / query / `$expand` at any
|
|
@@ -59,9 +59,41 @@ export interface FieldDef {
|
|
|
59
59
|
* `orgs` role alphabet). The gate
|
|
60
60
|
* never affects WRITES. Carried by BOTH push paths and alterable in place. */
|
|
61
61
|
readRoles?: string[];
|
|
62
|
+
/** EQUALITY INDEX for an UNSLOTTED field (guide ch. 4 "Indexed equality"):
|
|
63
|
+
* `=` / `$eq` / `$in` filters on it are index-served instead of a bounded
|
|
64
|
+
* scan — results are identical either way. At most 4 per collection (the
|
|
65
|
+
* equality-key budget, separate from the 8 index slots); not together with
|
|
66
|
+
* `indexSlot` (a slot already indexes equality) and not on a computed field
|
|
67
|
+
* (both checked here at define time and again by the server). Carried by
|
|
68
|
+
* BOTH push paths and alterable in place; on a collection over 2,000 live
|
|
69
|
+
* items `vxil push` also runs the re-index that arms it. */
|
|
70
|
+
indexed?: boolean;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
/**
|
|
73
|
+
/** The per-collection budget of `indexed: true` fields (cms/0158: e1…e4). */
|
|
74
|
+
export const CMS_INDEXED_FIELD_BUDGET = 4;
|
|
75
|
+
|
|
76
|
+
/** Define-time checks of a collection's `indexed` fields — the same refusals
|
|
77
|
+
* the server makes (422), so a config fails at `defineConfig` / `collection()`
|
|
78
|
+
* instead of mid-push. Throws on the first violation. */
|
|
79
|
+
export function assertIndexedFields(name: string, def: CollectionDef): void {
|
|
80
|
+
let n = 0;
|
|
81
|
+
for (const [fname, f] of Object.entries(def.fields ?? {})) {
|
|
82
|
+
if (f.indexed !== true) continue;
|
|
83
|
+
n += 1;
|
|
84
|
+
if (f.indexSlot) {
|
|
85
|
+
throw new Error(`vxil.config: cms.${name}.${fname} sets both indexed and indexSlot '${f.indexSlot}' — a slot already indexes equality; use one`);
|
|
86
|
+
}
|
|
87
|
+
if (f.computed) {
|
|
88
|
+
throw new Error(`vxil.config: cms.${name}.${fname} is computed — a computed field cannot be indexed (bind it to an indexSlot instead)`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (n > CMS_INDEXED_FIELD_BUDGET) {
|
|
92
|
+
throw new Error(`vxil.config: cms.${name} has ${n} indexed fields — at most ${CMS_INDEXED_FIELD_BUDGET} per collection`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One config-declared per-record ACTION button (guide ch. 4): exactly ONE
|
|
65
97
|
* human-initiated step — pressing it invokes the deployed tenant function `fn`
|
|
66
98
|
* with `{ collection, item_id, action, actor, item }` and returns its result.
|
|
67
99
|
* No conditions, no chaining, no scheduling. `key` matches
|
|
@@ -77,7 +109,7 @@ export interface CmsActionDef {
|
|
|
77
109
|
* `FieldDef` itself so it can never drift from the authoring type: it is every
|
|
78
110
|
* key of `FieldDef` except `type` (which is required, not an optional carried
|
|
79
111
|
* attribute). This is the compile-time source of truth for the config-carriage
|
|
80
|
-
* completeness ratchet
|
|
112
|
+
* completeness ratchet: the CLI carriage
|
|
81
113
|
* table (`packages/cli/src/cms.ts` FIELD_ATTRS) and the server one
|
|
82
114
|
* (`workers/control-plane/src/handlers/apply.ts` FIELD_ATTR_DELTA) each pin
|
|
83
115
|
* `keyof` coverage against a set that equals this — so a 9th attribute added to
|
|
@@ -105,7 +137,7 @@ export interface CollectionDef {
|
|
|
105
137
|
* server-caller mode. Not a hard-isolation predicate; a single declarative
|
|
106
138
|
* flag the typed worker consults. Omit for shared/reference collections. */
|
|
107
139
|
ownerField?: string;
|
|
108
|
-
/** Public delivery (
|
|
140
|
+
/** Public delivery (guide ch. 4): when true, this collection's PUBLISHED items
|
|
109
141
|
* are servable through the keyless, edge-cached GET /v1/cms/public/:tenantId/
|
|
110
142
|
* :collection lane (no API key). Owner-UNSCOPED (public content, not per-user);
|
|
111
143
|
* the owner_field is stripped from every served row. Default false. Carried by
|
|
@@ -113,7 +145,7 @@ export interface CollectionDef {
|
|
|
113
145
|
* with a regression test on each — an attribute read by no enforcing path ships
|
|
114
146
|
* inert (the validation.unique / owner_field precedent). */
|
|
115
147
|
public?: boolean;
|
|
116
|
-
/** Per-record action buttons (
|
|
148
|
+
/** Per-record action buttons (guide ch. 4): `[{ key, label, fn }]`, stored on
|
|
117
149
|
* the collection (model data, not a config leaf). The dashboard renders one
|
|
118
150
|
* button per action on each record row; `POST /v1/cms/items/:coll/:id/actions/
|
|
119
151
|
* :key` invokes `fn` once. Carried by BOTH push paths (create + reconcile on
|
|
@@ -121,8 +153,8 @@ export interface CollectionDef {
|
|
|
121
153
|
actions?: CmsActionDef[];
|
|
122
154
|
}
|
|
123
155
|
|
|
124
|
-
/** The auth lifecycle events an `authHook` binding may subscribe to
|
|
125
|
-
*
|
|
156
|
+
/** The auth lifecycle events an `authHook` binding may subscribe to
|
|
157
|
+
* (2026-09-10) — a CLOSED union; ONE event per binding, one audit
|
|
126
158
|
* event → one function invocation, no branching inside vxil:
|
|
127
159
|
* user.created — auth.user.created { user_id, method, is_anonymous }
|
|
128
160
|
* session.created — auth.session.created { user_id, session_id }
|
|
@@ -130,7 +162,7 @@ export interface CollectionDef {
|
|
|
130
162
|
* signin.failure — auth.signin.failure { email_hash | user_id, reason } */
|
|
131
163
|
export type AuthHookEvent = 'user.created' | 'session.created' | 'session.revoked' | 'signin.failure';
|
|
132
164
|
|
|
133
|
-
/** The opt-in re-delivery of a platform-delivered trigger (
|
|
165
|
+
/** The opt-in re-delivery of a platform-delivered trigger (guide ch. 8).
|
|
134
166
|
* DEFAULT OFF: the platform acknowledges every delivery with a 200 whatever
|
|
135
167
|
* the handler returned — an application error is observed (`functions.run.failed`),
|
|
136
168
|
* never re-delivered. With `retry` set on a `queue` / `webhook` / `cmsHook` /
|
|
@@ -151,7 +183,7 @@ export interface FunctionTriggerRetry {
|
|
|
151
183
|
maxAttempts: number;
|
|
152
184
|
}
|
|
153
185
|
|
|
154
|
-
/** A function trigger (
|
|
186
|
+
/** A function trigger (guide ch. 8). cmsHook fires on a CMS write;
|
|
155
187
|
* authHook fires on ONE auth lifecycle event (default `user.created` — the
|
|
156
188
|
* post-signup hook; at-least-once, ~1min fanout latency). */
|
|
157
189
|
export type FunctionTrigger =
|
|
@@ -233,7 +265,7 @@ export interface FunctionDef {
|
|
|
233
265
|
timeoutMs?: number;
|
|
234
266
|
};
|
|
235
267
|
enabled?: boolean;
|
|
236
|
-
/** optional typed contract surfaced by `vxil gen` (Level 1
|
|
268
|
+
/** optional typed contract surfaced by `vxil gen` (Level 1; guide ch. 8). */
|
|
237
269
|
signature?: { input?: unknown; output?: unknown };
|
|
238
270
|
}
|
|
239
271
|
|
|
@@ -264,8 +296,7 @@ export const RELEASED_API_VERSIONS: readonly ApiVersion[] = ['v1'];
|
|
|
264
296
|
/** Recursive partial: a `vxil.config.ts` may write any SUBSET of a nested bag
|
|
265
297
|
* (`auth: { otp: { enabled: true } }`) — the server applies the defaults for
|
|
266
298
|
* the rest (Value.Default). A shallow `Partial` made `tsc` reject every
|
|
267
|
-
* partially-written nested bag, including the guide's own skeleton
|
|
268
|
-
* (found by the 2026-09-19 cvskit dry-run). Arrays and primitives are kept
|
|
299
|
+
* partially-written nested bag, including the guide's own skeleton. Arrays and primitives are kept
|
|
269
300
|
* as-is; only plain object bags recurse. */
|
|
270
301
|
export type DeepPartial<T> = T extends readonly unknown[]
|
|
271
302
|
? T
|
|
@@ -310,7 +341,7 @@ export interface VxilConfig {
|
|
|
310
341
|
|
|
311
342
|
/** (b) CMS SCHEMA-AS-CODE — collections + fields. `push` reconciles them
|
|
312
343
|
* additively against GET /v1/cms/collections (no destructive drops without
|
|
313
|
-
* --allow-destructive).
|
|
344
|
+
* --allow-destructive). Validation hooks stay in features.cms.hooks (config). */
|
|
314
345
|
cms?: {
|
|
315
346
|
collections?: Record<string, CollectionDef>;
|
|
316
347
|
};
|
|
@@ -337,10 +368,11 @@ export function defineConfig(c: VxilConfig): VxilConfig {
|
|
|
337
368
|
'A new major ships as a new path with its own deprecation window — see https://vxil.com/docs/guide/12-going-to-production.',
|
|
338
369
|
);
|
|
339
370
|
}
|
|
371
|
+
for (const [name, def] of Object.entries(c.cms?.collections ?? {})) assertIndexedFields(name, def);
|
|
340
372
|
return c;
|
|
341
373
|
}
|
|
342
374
|
|
|
343
|
-
/** Authoring helper for split-out cms/<name>.ts collection modules (
|
|
375
|
+
/** Authoring helper for split-out cms/<name>.ts collection modules (guide ch. 5).
|
|
344
376
|
* `field('title', 'string', { required: true, indexSlot: 's1' })`. */
|
|
345
377
|
export function field(
|
|
346
378
|
type: FieldType,
|
|
@@ -351,5 +383,6 @@ export function field(
|
|
|
351
383
|
|
|
352
384
|
/** Authoring helper for a collection module: `export default collection({...})`. */
|
|
353
385
|
export function collection(def: CollectionDef): CollectionDef {
|
|
386
|
+
assertIndexedFields(def.singular ?? 'collection', def);
|
|
354
387
|
return def;
|
|
355
388
|
}
|