@vxil/config 0.7.0 → 0.8.0
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 +15 -0
- package/dist/index.js +25 -0
- package/package.json +1 -1
- package/src/index.test.ts +25 -1
- package/src/index.ts +34 -0
package/dist/index.d.ts
CHANGED
|
@@ -40,7 +40,22 @@ 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 (cms.md §3 "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
|
}
|
|
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;
|
|
44
59
|
/** One config-declared per-record ACTION button (cms.md §17): 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.
|
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,6 +33,8 @@ 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
40
|
/** Authoring helper for split-out cms/<name>.ts collection modules (§6a).
|
|
@@ -20,5 +44,6 @@ export function field(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.0",
|
|
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",
|
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
|
@@ -59,6 +59,38 @@ 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 (cms.md §3 "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;
|
|
71
|
+
}
|
|
72
|
+
|
|
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
|
+
}
|
|
62
94
|
}
|
|
63
95
|
|
|
64
96
|
/** One config-declared per-record ACTION button (cms.md §17): exactly ONE
|
|
@@ -337,6 +369,7 @@ export function defineConfig(c: VxilConfig): VxilConfig {
|
|
|
337
369
|
'A new major ships as a new path with its own deprecation window — see https://vxil.com/docs/guide/12-going-to-production.',
|
|
338
370
|
);
|
|
339
371
|
}
|
|
372
|
+
for (const [name, def] of Object.entries(c.cms?.collections ?? {})) assertIndexedFields(name, def);
|
|
340
373
|
return c;
|
|
341
374
|
}
|
|
342
375
|
|
|
@@ -351,5 +384,6 @@ export function field(
|
|
|
351
384
|
|
|
352
385
|
/** Authoring helper for a collection module: `export default collection({...})`. */
|
|
353
386
|
export function collection(def: CollectionDef): CollectionDef {
|
|
387
|
+
assertIndexedFields(def.singular ?? 'collection', def);
|
|
354
388
|
return def;
|
|
355
389
|
}
|