@vxil/config 0.8.0 → 0.8.2
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 +17 -18
- package/dist/index.js +1 -1
- package/package.json +4 -3
- package/src/index.ts +19 -20
- package/src/deepPartial.test.ts +0 -24
- package/src/index.test.ts +0 -52
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,7 +40,7 @@ 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 (
|
|
43
|
+
/** EQUALITY INDEX for an UNSLOTTED field (guide ch. 4 "Indexed equality"):
|
|
44
44
|
* `=` / `$eq` / `$in` filters on it are index-served instead of a bounded
|
|
45
45
|
* scan — results are identical either way. At most 4 per collection (the
|
|
46
46
|
* equality-key budget, separate from the 8 index slots); not together with
|
|
@@ -56,7 +56,7 @@ export declare const CMS_INDEXED_FIELD_BUDGET = 4;
|
|
|
56
56
|
* the server makes (422), so a config fails at `defineConfig` / `collection()`
|
|
57
57
|
* instead of mid-push. Throws on the first violation. */
|
|
58
58
|
export declare function assertIndexedFields(name: string, def: CollectionDef): void;
|
|
59
|
-
/** One config-declared per-record ACTION button (
|
|
59
|
+
/** One config-declared per-record ACTION button (guide ch. 4): exactly ONE
|
|
60
60
|
* human-initiated step — pressing it invokes the deployed tenant function `fn`
|
|
61
61
|
* with `{ collection, item_id, action, actor, item }` and returns its result.
|
|
62
62
|
* No conditions, no chaining, no scheduling. `key` matches
|
|
@@ -71,7 +71,7 @@ export interface CmsActionDef {
|
|
|
71
71
|
* `FieldDef` itself so it can never drift from the authoring type: it is every
|
|
72
72
|
* key of `FieldDef` except `type` (which is required, not an optional carried
|
|
73
73
|
* attribute). This is the compile-time source of truth for the config-carriage
|
|
74
|
-
* completeness ratchet
|
|
74
|
+
* completeness ratchet: the CLI carriage
|
|
75
75
|
* table (`packages/cli/src/cms.ts` FIELD_ATTRS) and the server one
|
|
76
76
|
* (`workers/control-plane/src/handlers/apply.ts` FIELD_ATTR_DELTA) each pin
|
|
77
77
|
* `keyof` coverage against a set that equals this — so a 9th attribute added to
|
|
@@ -98,7 +98,7 @@ export interface CollectionDef {
|
|
|
98
98
|
* server-caller mode. Not a hard-isolation predicate; a single declarative
|
|
99
99
|
* flag the typed worker consults. Omit for shared/reference collections. */
|
|
100
100
|
ownerField?: string;
|
|
101
|
-
/** Public delivery (
|
|
101
|
+
/** Public delivery (guide ch. 4): when true, this collection's PUBLISHED items
|
|
102
102
|
* are servable through the keyless, edge-cached GET /v1/cms/public/:tenantId/
|
|
103
103
|
* :collection lane (no API key). Owner-UNSCOPED (public content, not per-user);
|
|
104
104
|
* the owner_field is stripped from every served row. Default false. Carried by
|
|
@@ -106,22 +106,22 @@ export interface CollectionDef {
|
|
|
106
106
|
* with a regression test on each — an attribute read by no enforcing path ships
|
|
107
107
|
* inert (the validation.unique / owner_field precedent). */
|
|
108
108
|
public?: boolean;
|
|
109
|
-
/** Per-record action buttons (
|
|
109
|
+
/** Per-record action buttons (guide ch. 4): `[{ key, label, fn }]`, stored on
|
|
110
110
|
* the collection (model data, not a config leaf). The dashboard renders one
|
|
111
111
|
* button per action on each record row; `POST /v1/cms/items/:coll/:id/actions/
|
|
112
112
|
* :key` invokes `fn` once. Carried by BOTH push paths (create + reconcile on
|
|
113
113
|
* existing collections — config is the source of truth; `[]`/absent clears). */
|
|
114
114
|
actions?: CmsActionDef[];
|
|
115
115
|
}
|
|
116
|
-
/** The auth lifecycle events an `authHook` binding may subscribe to
|
|
117
|
-
*
|
|
116
|
+
/** The auth lifecycle events an `authHook` binding may subscribe to
|
|
117
|
+
* (2026-09-10) — a CLOSED union; ONE event per binding, one audit
|
|
118
118
|
* event → one function invocation, no branching inside vxil:
|
|
119
119
|
* user.created — auth.user.created { user_id, method, is_anonymous }
|
|
120
120
|
* session.created — auth.session.created { user_id, session_id }
|
|
121
121
|
* session.revoked — auth.session.revoked { user_id, session_id, reason }
|
|
122
122
|
* signin.failure — auth.signin.failure { email_hash | user_id, reason } */
|
|
123
123
|
export type AuthHookEvent = 'user.created' | 'session.created' | 'session.revoked' | 'signin.failure';
|
|
124
|
-
/** The opt-in re-delivery of a platform-delivered trigger (
|
|
124
|
+
/** The opt-in re-delivery of a platform-delivered trigger (guide ch. 8).
|
|
125
125
|
* DEFAULT OFF: the platform acknowledges every delivery with a 200 whatever
|
|
126
126
|
* the handler returned — an application error is observed (`functions.run.failed`),
|
|
127
127
|
* never re-delivered. With `retry` set on a `queue` / `webhook` / `cmsHook` /
|
|
@@ -141,7 +141,7 @@ export interface FunctionTriggerRetry {
|
|
|
141
141
|
/** 1–5 attempts in all (1 = a failed attempt dead-letters at once, no re-delivery) */
|
|
142
142
|
maxAttempts: number;
|
|
143
143
|
}
|
|
144
|
-
/** A function trigger (
|
|
144
|
+
/** A function trigger (guide ch. 8). cmsHook fires on a CMS write;
|
|
145
145
|
* authHook fires on ONE auth lifecycle event (default `user.created` — the
|
|
146
146
|
* post-signup hook; at-least-once, ~1min fanout latency). */
|
|
147
147
|
export type FunctionTrigger = {
|
|
@@ -243,7 +243,7 @@ export interface FunctionDef {
|
|
|
243
243
|
timeoutMs?: number;
|
|
244
244
|
};
|
|
245
245
|
enabled?: boolean;
|
|
246
|
-
/** optional typed contract surfaced by `vxil gen` (Level 1
|
|
246
|
+
/** optional typed contract surfaced by `vxil gen` (Level 1; guide ch. 8). */
|
|
247
247
|
signature?: {
|
|
248
248
|
input?: unknown;
|
|
249
249
|
output?: unknown;
|
|
@@ -278,8 +278,7 @@ export declare const RELEASED_API_VERSIONS: readonly ApiVersion[];
|
|
|
278
278
|
/** Recursive partial: a `vxil.config.ts` may write any SUBSET of a nested bag
|
|
279
279
|
* (`auth: { otp: { enabled: true } }`) — the server applies the defaults for
|
|
280
280
|
* the rest (Value.Default). A shallow `Partial` made `tsc` reject every
|
|
281
|
-
* partially-written nested bag, including the guide's own skeleton
|
|
282
|
-
* (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
|
|
283
282
|
* as-is; only plain object bags recurse. */
|
|
284
283
|
export type DeepPartial<T> = T extends readonly unknown[] ? T : T extends object ? {
|
|
285
284
|
[K in keyof T]?: DeepPartial<T[K]>;
|
|
@@ -318,7 +317,7 @@ export interface VxilConfig {
|
|
|
318
317
|
};
|
|
319
318
|
/** (b) CMS SCHEMA-AS-CODE — collections + fields. `push` reconciles them
|
|
320
319
|
* additively against GET /v1/cms/collections (no destructive drops without
|
|
321
|
-
* --allow-destructive).
|
|
320
|
+
* --allow-destructive). Validation hooks stay in features.cms.hooks (config). */
|
|
322
321
|
cms?: {
|
|
323
322
|
collections?: Record<string, CollectionDef>;
|
|
324
323
|
};
|
|
@@ -334,7 +333,7 @@ export interface VxilConfig {
|
|
|
334
333
|
* from JS callers with no compile gate, or a stale generated value) fails fast
|
|
335
334
|
* here rather than 404-ing every request against a nonexistent major. */
|
|
336
335
|
export declare function defineConfig(c: VxilConfig): VxilConfig;
|
|
337
|
-
/** Authoring helper for split-out cms/<name>.ts collection modules (
|
|
336
|
+
/** Authoring helper for split-out cms/<name>.ts collection modules (guide ch. 5).
|
|
338
337
|
* `field('title', 'string', { required: true, indexSlot: 's1' })`. */
|
|
339
338
|
export declare function field(type: FieldType, opts?: Omit<FieldDef, 'type'>): FieldDef;
|
|
340
339
|
/** Authoring helper for a collection module: `export default collection({...})`. */
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ export function defineConfig(c) {
|
|
|
37
37
|
assertIndexedFields(name, def);
|
|
38
38
|
return c;
|
|
39
39
|
}
|
|
40
|
-
/** Authoring helper for split-out cms/<name>.ts collection modules (
|
|
40
|
+
/** Authoring helper for split-out cms/<name>.ts collection modules (guide ch. 5).
|
|
41
41
|
* `field('title', 'string', { required: true, indexSlot: 's1' })`. */
|
|
42
42
|
export function field(type, opts = {}) {
|
|
43
43
|
return { type, ...opts };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vxil/config",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
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",
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
-
"src"
|
|
22
|
+
"src",
|
|
23
|
+
"!src/**/*.test.ts"
|
|
23
24
|
],
|
|
24
25
|
"dependencies": {
|
|
25
|
-
"@vxil/feature-configs": "0.7.
|
|
26
|
+
"@vxil/feature-configs": "0.7.1"
|
|
26
27
|
},
|
|
27
28
|
"publishConfig": {
|
|
28
29
|
"access": "public"
|
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,7 +59,7 @@ 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 (
|
|
62
|
+
/** EQUALITY INDEX for an UNSLOTTED field (guide ch. 4 "Indexed equality"):
|
|
63
63
|
* `=` / `$eq` / `$in` filters on it are index-served instead of a bounded
|
|
64
64
|
* scan — results are identical either way. At most 4 per collection (the
|
|
65
65
|
* equality-key budget, separate from the 8 index slots); not together with
|
|
@@ -93,7 +93,7 @@ export function assertIndexedFields(name: string, def: CollectionDef): void {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
/** One config-declared per-record ACTION button (
|
|
96
|
+
/** One config-declared per-record ACTION button (guide ch. 4): exactly ONE
|
|
97
97
|
* human-initiated step — pressing it invokes the deployed tenant function `fn`
|
|
98
98
|
* with `{ collection, item_id, action, actor, item }` and returns its result.
|
|
99
99
|
* No conditions, no chaining, no scheduling. `key` matches
|
|
@@ -109,7 +109,7 @@ export interface CmsActionDef {
|
|
|
109
109
|
* `FieldDef` itself so it can never drift from the authoring type: it is every
|
|
110
110
|
* key of `FieldDef` except `type` (which is required, not an optional carried
|
|
111
111
|
* attribute). This is the compile-time source of truth for the config-carriage
|
|
112
|
-
* completeness ratchet
|
|
112
|
+
* completeness ratchet: the CLI carriage
|
|
113
113
|
* table (`packages/cli/src/cms.ts` FIELD_ATTRS) and the server one
|
|
114
114
|
* (`workers/control-plane/src/handlers/apply.ts` FIELD_ATTR_DELTA) each pin
|
|
115
115
|
* `keyof` coverage against a set that equals this — so a 9th attribute added to
|
|
@@ -137,7 +137,7 @@ export interface CollectionDef {
|
|
|
137
137
|
* server-caller mode. Not a hard-isolation predicate; a single declarative
|
|
138
138
|
* flag the typed worker consults. Omit for shared/reference collections. */
|
|
139
139
|
ownerField?: string;
|
|
140
|
-
/** Public delivery (
|
|
140
|
+
/** Public delivery (guide ch. 4): when true, this collection's PUBLISHED items
|
|
141
141
|
* are servable through the keyless, edge-cached GET /v1/cms/public/:tenantId/
|
|
142
142
|
* :collection lane (no API key). Owner-UNSCOPED (public content, not per-user);
|
|
143
143
|
* the owner_field is stripped from every served row. Default false. Carried by
|
|
@@ -145,7 +145,7 @@ export interface CollectionDef {
|
|
|
145
145
|
* with a regression test on each — an attribute read by no enforcing path ships
|
|
146
146
|
* inert (the validation.unique / owner_field precedent). */
|
|
147
147
|
public?: boolean;
|
|
148
|
-
/** Per-record action buttons (
|
|
148
|
+
/** Per-record action buttons (guide ch. 4): `[{ key, label, fn }]`, stored on
|
|
149
149
|
* the collection (model data, not a config leaf). The dashboard renders one
|
|
150
150
|
* button per action on each record row; `POST /v1/cms/items/:coll/:id/actions/
|
|
151
151
|
* :key` invokes `fn` once. Carried by BOTH push paths (create + reconcile on
|
|
@@ -153,8 +153,8 @@ export interface CollectionDef {
|
|
|
153
153
|
actions?: CmsActionDef[];
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
/** The auth lifecycle events an `authHook` binding may subscribe to
|
|
157
|
-
*
|
|
156
|
+
/** The auth lifecycle events an `authHook` binding may subscribe to
|
|
157
|
+
* (2026-09-10) — a CLOSED union; ONE event per binding, one audit
|
|
158
158
|
* event → one function invocation, no branching inside vxil:
|
|
159
159
|
* user.created — auth.user.created { user_id, method, is_anonymous }
|
|
160
160
|
* session.created — auth.session.created { user_id, session_id }
|
|
@@ -162,7 +162,7 @@ export interface CollectionDef {
|
|
|
162
162
|
* signin.failure — auth.signin.failure { email_hash | user_id, reason } */
|
|
163
163
|
export type AuthHookEvent = 'user.created' | 'session.created' | 'session.revoked' | 'signin.failure';
|
|
164
164
|
|
|
165
|
-
/** The opt-in re-delivery of a platform-delivered trigger (
|
|
165
|
+
/** The opt-in re-delivery of a platform-delivered trigger (guide ch. 8).
|
|
166
166
|
* DEFAULT OFF: the platform acknowledges every delivery with a 200 whatever
|
|
167
167
|
* the handler returned — an application error is observed (`functions.run.failed`),
|
|
168
168
|
* never re-delivered. With `retry` set on a `queue` / `webhook` / `cmsHook` /
|
|
@@ -183,7 +183,7 @@ export interface FunctionTriggerRetry {
|
|
|
183
183
|
maxAttempts: number;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
/** A function trigger (
|
|
186
|
+
/** A function trigger (guide ch. 8). cmsHook fires on a CMS write;
|
|
187
187
|
* authHook fires on ONE auth lifecycle event (default `user.created` — the
|
|
188
188
|
* post-signup hook; at-least-once, ~1min fanout latency). */
|
|
189
189
|
export type FunctionTrigger =
|
|
@@ -265,7 +265,7 @@ export interface FunctionDef {
|
|
|
265
265
|
timeoutMs?: number;
|
|
266
266
|
};
|
|
267
267
|
enabled?: boolean;
|
|
268
|
-
/** optional typed contract surfaced by `vxil gen` (Level 1
|
|
268
|
+
/** optional typed contract surfaced by `vxil gen` (Level 1; guide ch. 8). */
|
|
269
269
|
signature?: { input?: unknown; output?: unknown };
|
|
270
270
|
}
|
|
271
271
|
|
|
@@ -296,8 +296,7 @@ export const RELEASED_API_VERSIONS: readonly ApiVersion[] = ['v1'];
|
|
|
296
296
|
/** Recursive partial: a `vxil.config.ts` may write any SUBSET of a nested bag
|
|
297
297
|
* (`auth: { otp: { enabled: true } }`) — the server applies the defaults for
|
|
298
298
|
* the rest (Value.Default). A shallow `Partial` made `tsc` reject every
|
|
299
|
-
* partially-written nested bag, including the guide's own skeleton
|
|
300
|
-
* (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
|
|
301
300
|
* as-is; only plain object bags recurse. */
|
|
302
301
|
export type DeepPartial<T> = T extends readonly unknown[]
|
|
303
302
|
? T
|
|
@@ -342,7 +341,7 @@ export interface VxilConfig {
|
|
|
342
341
|
|
|
343
342
|
/** (b) CMS SCHEMA-AS-CODE — collections + fields. `push` reconciles them
|
|
344
343
|
* additively against GET /v1/cms/collections (no destructive drops without
|
|
345
|
-
* --allow-destructive).
|
|
344
|
+
* --allow-destructive). Validation hooks stay in features.cms.hooks (config). */
|
|
346
345
|
cms?: {
|
|
347
346
|
collections?: Record<string, CollectionDef>;
|
|
348
347
|
};
|
|
@@ -373,7 +372,7 @@ export function defineConfig(c: VxilConfig): VxilConfig {
|
|
|
373
372
|
return c;
|
|
374
373
|
}
|
|
375
374
|
|
|
376
|
-
/** Authoring helper for split-out cms/<name>.ts collection modules (
|
|
375
|
+
/** Authoring helper for split-out cms/<name>.ts collection modules (guide ch. 5).
|
|
377
376
|
* `field('title', 'string', { required: true, indexSlot: 's1' })`. */
|
|
378
377
|
export function field(
|
|
379
378
|
type: FieldType,
|
package/src/deepPartial.test.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// A `vxil.config.ts` may write any SUBSET of a nested feature bag; the server
|
|
2
|
-
// defaults the rest. This file is COMPILED by the package typecheck, so a
|
|
3
|
-
// regression to a shallow Partial fails `tsc`, not just a runtime assertion
|
|
4
|
-
// (the 2026-09-19 cvskit dry-run found the guide's own skeleton red under tsc).
|
|
5
|
-
import { describe, expect, it } from 'vitest';
|
|
6
|
-
import type { DeepPartial, VxilConfig } from './index.js';
|
|
7
|
-
|
|
8
|
-
const partial: VxilConfig = {
|
|
9
|
-
features: {
|
|
10
|
-
auth: { otp: { enabled: true }, anonymous: { enabled: true } },
|
|
11
|
-
files: { ttl: { enabled: true, defaultExpiresInSeconds: 2592000 } },
|
|
12
|
-
ai: { defaults: { model: 'x' } },
|
|
13
|
-
cms: { limits: { maxItemsPerCollection: 5 } },
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
describe('DeepPartial', () => {
|
|
18
|
-
it('accepts partially-written nested bags (compile-time) and leaves arrays alone', () => {
|
|
19
|
-
type T = DeepPartial<{ a: { b: number; c: string }; list: string[] }>;
|
|
20
|
-
const v: T = { a: { b: 1 }, list: ['x'] };
|
|
21
|
-
expect(v.a?.b).toBe(1);
|
|
22
|
-
expect(partial.features?.auth?.otp?.enabled).toBe(true);
|
|
23
|
-
});
|
|
24
|
-
});
|
package/src/index.test.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
assertIndexedFields, CMS_INDEXED_FIELD_BUDGET, collection, defineConfig, field, RELEASED_API_VERSIONS,
|
|
4
|
-
} from './index.js';
|
|
5
|
-
|
|
6
|
-
describe('defineConfig', () => {
|
|
7
|
-
it('is an identity function (returns its argument verbatim)', () => {
|
|
8
|
-
const cfg = { features: { cms: {} } };
|
|
9
|
-
expect(defineConfig(cfg)).toBe(cfg);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('accepts the released apiVersion', () => {
|
|
13
|
-
const cfg = defineConfig({ apiVersion: 'v1', features: {} });
|
|
14
|
-
expect(cfg.apiVersion).toBe('v1');
|
|
15
|
-
expect(RELEASED_API_VERSIONS).toEqual(['v1']);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('accepts a config without apiVersion (optional; defaults to v1 at the CLI)', () => {
|
|
19
|
-
expect(() => defineConfig({ features: {} })).not.toThrow();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('throws on an unreleased apiVersion (fail-fast, actionable message)', () => {
|
|
23
|
-
// A JS caller (no compile gate) or a stale generated value must be rejected
|
|
24
|
-
// at load, not silently 404 every request.
|
|
25
|
-
expect(() => defineConfig({ apiVersion: 'v2' as 'v1', features: {} }))
|
|
26
|
-
.toThrow(/apiVersion 'v2' is not a released API major/);
|
|
27
|
-
expect(() => defineConfig({ apiVersion: 'latest' as 'v1' }))
|
|
28
|
-
.toThrow(/docs\/guide\/12-going-to-production/);
|
|
29
|
-
});
|
|
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
|
-
});
|