@x12i/memorix-metadata 3.3.0 → 3.4.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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +163 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +561 -25
- package/dist/validate.js.map +1 -1
- package/examples/analytics/review-decisions.pack.json +197 -0
- package/examples/custom-kinds/acme-ui-tabs.pack.json +1 -1
- package/examples/invalid/agent-classification.json +48 -24
- package/examples/invalid/broken-binding.json +48 -24
- package/examples/invalid/completion-mapping-with-exec-policy.json +48 -24
- package/examples/invalid/missing-parent.json +48 -24
- package/examples/invalid/operational-kind-root.json +48 -24
- package/examples/invalid/pipeline-unknown-service.json +48 -24
- package/examples/invalid/relationship-missing-rules.json +48 -24
- package/examples/invalid/relationship-with-storage.json +48 -24
- package/examples/invalid/service-with-secrets.json +48 -24
- package/examples/knowx/normalized/pack.json +1883 -1861
- package/examples/opx/normalized/pack.json +1304 -1281
- package/examples/opx/raw/object-types.json +26 -19
- package/examples/project-opx/normalized/pack.json +38 -19
- package/package.json +1 -1
package/dist/validate.js
CHANGED
|
@@ -3,7 +3,7 @@ import { foldLegacyAbstractBindings } from "./fold-bindings.js";
|
|
|
3
3
|
import { isForbiddenOperationalKindId, METADATA_KIND_IDS, } from "./kinds.js";
|
|
4
4
|
import { validateMappingMap } from "@x12i/memorix-mapping";
|
|
5
5
|
import { isPlatformInterfaceId, platformInterfaceForCategory, } from "./platform-interfaces.js";
|
|
6
|
-
import { parentIdsOf } from "./types.js";
|
|
6
|
+
import { ANALYTICS_PSEUDO_PROPERTIES, ANALYTICS_RESERVED_BUCKETS, parentIdsOf, } from "./types.js";
|
|
7
7
|
function issue(path, code, message) {
|
|
8
8
|
return { path, code, message };
|
|
9
9
|
}
|
|
@@ -110,6 +110,219 @@ function validateAgent(raw, path, issues) {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
function validateContentTypeProfile(raw, path, issues) {
|
|
114
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
115
|
+
issues.push(issue(path, "META-REQUIRED", `${path} is required (declare allowSnapshotBuckets)`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const profile = raw;
|
|
119
|
+
if (typeof profile.allowSnapshotBuckets !== "boolean") {
|
|
120
|
+
issues.push(issue(`${path}.allowSnapshotBuckets`, "META-REQUIRED", `${path}.allowSnapshotBuckets must be a boolean`));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function validateContentTypeDiscriminator(raw, path, issues) {
|
|
124
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
125
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const disc = raw;
|
|
129
|
+
requireNonEmptyString(disc.path, `${path}.path`, issues);
|
|
130
|
+
if (disc.required !== undefined && typeof disc.required !== "boolean") {
|
|
131
|
+
issues.push(issue(`${path}.required`, "META-TYPE", `${path}.required must be a boolean`));
|
|
132
|
+
}
|
|
133
|
+
if (disc.defaultValue !== undefined &&
|
|
134
|
+
(typeof disc.defaultValue !== "string" || !disc.defaultValue.trim())) {
|
|
135
|
+
issues.push(issue(`${path}.defaultValue`, "META-TYPE", `${path}.defaultValue must be a non-empty string`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function validateFieldMappings(raw, path, issues) {
|
|
139
|
+
if (!Array.isArray(raw)) {
|
|
140
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an array`));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
for (let i = 0; i < raw.length; i++) {
|
|
144
|
+
const item = raw[i];
|
|
145
|
+
const itemPath = `${path}[${i}]`;
|
|
146
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
147
|
+
issues.push(issue(itemPath, "META-TYPE", `${itemPath} must be an object`));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const profile = item;
|
|
151
|
+
requireNonEmptyString(profile.id, `${itemPath}.id`, issues);
|
|
152
|
+
requireNonEmptyString(profile.namespace, `${itemPath}.namespace`, issues);
|
|
153
|
+
if (profile.format !== "memorix-field-map/1") {
|
|
154
|
+
issues.push(issue(`${itemPath}.format`, "META-TYPE", `${itemPath}.format must be memorix-field-map/1`));
|
|
155
|
+
}
|
|
156
|
+
if (!profile.fieldMap || typeof profile.fieldMap !== "object" || Array.isArray(profile.fieldMap)) {
|
|
157
|
+
issues.push(issue(`${itemPath}.fieldMap`, "META-TYPE", `${itemPath}.fieldMap must be an object`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const PRESENTATION_TABS = new Set(["always", "whenPresent", "hidden"]);
|
|
162
|
+
const ORIGIN_PRODUCERS = new Set(["model", "code", "mixed"]);
|
|
163
|
+
const ORIGIN_USES = new Set(["always", "conditional"]);
|
|
164
|
+
const FUNCTION_KINDS = new Set(["spine", "threads"]);
|
|
165
|
+
function validateContentTypePresentation(raw, path, issues) {
|
|
166
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
167
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const presentation = raw;
|
|
171
|
+
if (presentation.label !== undefined && typeof presentation.label !== "string") {
|
|
172
|
+
issues.push(issue(`${path}.label`, "META-TYPE", `${path}.label must be a string`));
|
|
173
|
+
}
|
|
174
|
+
if (presentation.order !== undefined && typeof presentation.order !== "number") {
|
|
175
|
+
issues.push(issue(`${path}.order`, "META-TYPE", `${path}.order must be a number`));
|
|
176
|
+
}
|
|
177
|
+
if (presentation.tab !== undefined &&
|
|
178
|
+
(typeof presentation.tab !== "string" || !PRESENTATION_TABS.has(presentation.tab))) {
|
|
179
|
+
issues.push(issue(`${path}.tab`, "META-TYPE", `${path}.tab must be always, whenPresent, or hidden`));
|
|
180
|
+
}
|
|
181
|
+
if (presentation.values === undefined)
|
|
182
|
+
return;
|
|
183
|
+
if (!Array.isArray(presentation.values)) {
|
|
184
|
+
issues.push(issue(`${path}.values`, "META-TYPE", `${path}.values must be an array`));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
for (let i = 0; i < presentation.values.length; i++) {
|
|
188
|
+
const value = presentation.values[i];
|
|
189
|
+
const valuePath = `${path}.values[${i}]`;
|
|
190
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
191
|
+
issues.push(issue(valuePath, "META-TYPE", `${valuePath} must be an object`));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
requireNonEmptyString(value.value, `${valuePath}.value`, issues);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function validateContentTypeOrigin(raw, path, issues) {
|
|
198
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
199
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const origin = raw;
|
|
203
|
+
if (origin.producer !== undefined &&
|
|
204
|
+
(typeof origin.producer !== "string" || !ORIGIN_PRODUCERS.has(origin.producer))) {
|
|
205
|
+
issues.push(issue(`${path}.producer`, "META-TYPE", `${path}.producer must be model, code, or mixed`));
|
|
206
|
+
}
|
|
207
|
+
if (origin.use !== undefined && (typeof origin.use !== "string" || !ORIGIN_USES.has(origin.use))) {
|
|
208
|
+
issues.push(issue(`${path}.use`, "META-TYPE", `${path}.use must be always or conditional`));
|
|
209
|
+
}
|
|
210
|
+
if (origin.record !== undefined && (typeof origin.record !== "string" || !origin.record.trim())) {
|
|
211
|
+
issues.push(issue(`${path}.record`, "META-TYPE", `${path}.record must be a non-empty string`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function validateFunctionRead(raw, path, issues) {
|
|
215
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
216
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const read = raw;
|
|
220
|
+
for (const key of ["objectType", "contentType", "recordId", "template"]) {
|
|
221
|
+
const value = read[key];
|
|
222
|
+
if (value !== undefined && (typeof value !== "string" || !value.trim())) {
|
|
223
|
+
issues.push(issue(`${path}.${key}`, "META-TYPE", `${path}.${key} must be a non-empty string`));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (read.sameSubject !== undefined && typeof read.sameSubject !== "boolean") {
|
|
227
|
+
issues.push(issue(`${path}.sameSubject`, "META-TYPE", `${path}.sameSubject must be a boolean`));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function validateObjectTypeFunctions(raw, path, issues) {
|
|
231
|
+
if (!Array.isArray(raw)) {
|
|
232
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an array`));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
requireUniqueIds(raw, path, issues);
|
|
236
|
+
const templates = new Map();
|
|
237
|
+
for (let i = 0; i < raw.length; i++) {
|
|
238
|
+
const item = raw[i];
|
|
239
|
+
const itemPath = `${path}[${i}]`;
|
|
240
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
241
|
+
issues.push(issue(itemPath, "META-TYPE", `${itemPath} must be an object`));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const fn = item;
|
|
245
|
+
requireNonEmptyString(fn.id, `${itemPath}.id`, issues);
|
|
246
|
+
requireNonEmptyString(fn.question, `${itemPath}.question`, issues);
|
|
247
|
+
requireNonEmptyString(fn.page, `${itemPath}.page`, issues);
|
|
248
|
+
if (typeof fn.composes !== "boolean") {
|
|
249
|
+
issues.push(issue(`${itemPath}.composes`, "META-TYPE", `${itemPath}.composes must be a boolean`));
|
|
250
|
+
}
|
|
251
|
+
if (typeof fn.pageStory !== "boolean") {
|
|
252
|
+
issues.push(issue(`${itemPath}.pageStory`, "META-TYPE", `${itemPath}.pageStory must be a boolean`));
|
|
253
|
+
}
|
|
254
|
+
if (fn.template !== undefined) {
|
|
255
|
+
if (typeof fn.template !== "string" || !fn.template.trim()) {
|
|
256
|
+
issues.push(issue(`${itemPath}.template`, "META-TYPE", `${itemPath}.template must be a non-empty string`));
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
const template = fn.template.trim();
|
|
260
|
+
const previous = templates.get(template);
|
|
261
|
+
if (previous !== undefined) {
|
|
262
|
+
issues.push(issue(`${itemPath}.template`, "META-STORY-TEMPLATE", `template "${template}" is already used by ${path}[${previous}]`));
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
templates.set(template, i);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (fn.kind !== undefined && (typeof fn.kind !== "string" || !FUNCTION_KINDS.has(fn.kind))) {
|
|
270
|
+
issues.push(issue(`${itemPath}.kind`, "META-TYPE", `${itemPath}.kind must be spine or threads`));
|
|
271
|
+
}
|
|
272
|
+
if (!Array.isArray(fn.reads)) {
|
|
273
|
+
issues.push(issue(`${itemPath}.reads`, "META-TYPE", `${itemPath}.reads must be an array`));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
for (let r = 0; r < fn.reads.length; r++) {
|
|
277
|
+
validateFunctionRead(fn.reads[r], `${itemPath}.reads[${r}]`, issues);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function storyPresentationTemplates(contentTypes) {
|
|
283
|
+
if (!Array.isArray(contentTypes))
|
|
284
|
+
return [];
|
|
285
|
+
const templates = [];
|
|
286
|
+
for (const contentType of contentTypes) {
|
|
287
|
+
if (!contentType || typeof contentType !== "object" || Array.isArray(contentType))
|
|
288
|
+
continue;
|
|
289
|
+
const spec = contentType;
|
|
290
|
+
if (spec.id !== "story")
|
|
291
|
+
continue;
|
|
292
|
+
const values = spec.presentation?.values;
|
|
293
|
+
if (!Array.isArray(values))
|
|
294
|
+
continue;
|
|
295
|
+
for (const value of values) {
|
|
296
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
297
|
+
continue;
|
|
298
|
+
const raw = value.value;
|
|
299
|
+
if (typeof raw === "string" && raw.trim())
|
|
300
|
+
templates.push(raw.trim());
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return templates;
|
|
304
|
+
}
|
|
305
|
+
function assertStoryFunctionTemplates(contentTypes, functions, path, issues) {
|
|
306
|
+
const counts = new Map();
|
|
307
|
+
for (const fn of functions) {
|
|
308
|
+
if (!fn || typeof fn !== "object" || Array.isArray(fn))
|
|
309
|
+
continue;
|
|
310
|
+
const template = fn.template;
|
|
311
|
+
if (typeof template !== "string" || !template.trim())
|
|
312
|
+
continue;
|
|
313
|
+
const key = template.trim();
|
|
314
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
315
|
+
}
|
|
316
|
+
const seen = new Set();
|
|
317
|
+
for (const template of storyPresentationTemplates(contentTypes)) {
|
|
318
|
+
if (seen.has(template))
|
|
319
|
+
continue;
|
|
320
|
+
seen.add(template);
|
|
321
|
+
if ((counts.get(template) ?? 0) === 0) {
|
|
322
|
+
issues.push(issue(`${path}.functions`, "META-STORY-TEMPLATE", `story template "${template}" must have exactly one function on this object type`));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
113
326
|
function validateObjectType(raw, path, issues) {
|
|
114
327
|
if (!raw || typeof raw !== "object") {
|
|
115
328
|
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
@@ -134,12 +347,53 @@ function validateObjectType(raw, path, issues) {
|
|
|
134
347
|
}
|
|
135
348
|
}
|
|
136
349
|
}
|
|
350
|
+
let declaredContentTypes = new Set();
|
|
137
351
|
if (!Array.isArray(o.contentTypes) || o.contentTypes.length === 0) {
|
|
138
352
|
issues.push(issue(`${path}.contentTypes`, "META-REQUIRED", `${path}.contentTypes must be a non-empty array`));
|
|
139
353
|
}
|
|
140
354
|
else {
|
|
355
|
+
let leadingCount = 0;
|
|
141
356
|
for (let i = 0; i < o.contentTypes.length; i++) {
|
|
142
|
-
|
|
357
|
+
const ct = o.contentTypes[i];
|
|
358
|
+
const cp = `${path}.contentTypes[${i}]`;
|
|
359
|
+
if (!ct || typeof ct !== "object" || Array.isArray(ct)) {
|
|
360
|
+
issues.push(issue(cp, "META-TYPE", `${cp} must be an object`));
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (requireNonEmptyString(ct.id, `${cp}.id`, issues)) {
|
|
364
|
+
declaredContentTypes.add(ct.id);
|
|
365
|
+
}
|
|
366
|
+
validateContentTypeProfile(ct.profile, `${cp}.profile`, issues);
|
|
367
|
+
if (ct.required !== undefined && typeof ct.required !== "boolean") {
|
|
368
|
+
issues.push(issue(`${cp}.required`, "META-TYPE", `${cp}.required must be a boolean`));
|
|
369
|
+
}
|
|
370
|
+
else if (ct.required === true) {
|
|
371
|
+
requireNonEmptyString(ct.requiredReason, `${cp}.requiredReason`, issues);
|
|
372
|
+
}
|
|
373
|
+
if (ct.leading !== undefined) {
|
|
374
|
+
if (typeof ct.leading !== "boolean") {
|
|
375
|
+
issues.push(issue(`${cp}.leading`, "META-TYPE", `${cp}.leading must be a boolean`));
|
|
376
|
+
}
|
|
377
|
+
else if (ct.leading) {
|
|
378
|
+
leadingCount += 1;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (ct.discriminator !== undefined) {
|
|
382
|
+
validateContentTypeDiscriminator(ct.discriminator, `${cp}.discriminator`, issues);
|
|
383
|
+
}
|
|
384
|
+
if (ct.fieldMappings !== undefined) {
|
|
385
|
+
validateFieldMappings(ct.fieldMappings, `${cp}.fieldMappings`, issues);
|
|
386
|
+
}
|
|
387
|
+
if (ct.presentation !== undefined) {
|
|
388
|
+
validateContentTypePresentation(ct.presentation, `${cp}.presentation`, issues);
|
|
389
|
+
}
|
|
390
|
+
if (ct.origin !== undefined) {
|
|
391
|
+
validateContentTypeOrigin(ct.origin, `${cp}.origin`, issues);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
requireUniqueIds(o.contentTypes, `${path}.contentTypes`, issues);
|
|
395
|
+
if (leadingCount > 1) {
|
|
396
|
+
issues.push(issue(`${path}.contentTypes`, "META-LEADING", `object type "${typeof o.id === "string" ? o.id : ""}" has more than one leading content type`));
|
|
143
397
|
}
|
|
144
398
|
}
|
|
145
399
|
if (o.fields !== undefined) {
|
|
@@ -156,12 +410,26 @@ function validateObjectType(raw, path, issues) {
|
|
|
156
410
|
}
|
|
157
411
|
requireNonEmptyString(f.path, `${fp}.path`, issues);
|
|
158
412
|
requireNonEmptyString(f.valueType, `${fp}.valueType`, issues);
|
|
413
|
+
if (f.contentType !== undefined) {
|
|
414
|
+
if (typeof f.contentType !== "string" || !f.contentType.trim()) {
|
|
415
|
+
issues.push(issue(`${fp}.contentType`, "META-TYPE", `${fp}.contentType must be a non-empty string`));
|
|
416
|
+
}
|
|
417
|
+
else if (!declaredContentTypes.has(f.contentType)) {
|
|
418
|
+
issues.push(issue(`${fp}.contentType`, "META-REF-CONTENT", `content type "${f.contentType}" is not registered on this object type`));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
159
421
|
if (f.enum !== undefined) {
|
|
160
422
|
validateEnumList(f.enum, `${fp}.enum`, issues);
|
|
161
423
|
}
|
|
162
424
|
}
|
|
163
425
|
}
|
|
164
426
|
}
|
|
427
|
+
if (o.functions !== undefined) {
|
|
428
|
+
validateObjectTypeFunctions(o.functions, `${path}.functions`, issues);
|
|
429
|
+
if (Array.isArray(o.functions)) {
|
|
430
|
+
assertStoryFunctionTemplates(o.contentTypes, o.functions, path, issues);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
165
433
|
}
|
|
166
434
|
function validateAbstractType(raw, path, issues) {
|
|
167
435
|
if (!raw || typeof raw !== "object") {
|
|
@@ -373,15 +641,73 @@ function validateEnumList(value, path, issues) {
|
|
|
373
641
|
}
|
|
374
642
|
}
|
|
375
643
|
function requirePredicate(raw, path, issues) {
|
|
376
|
-
if (!raw || typeof raw !== "object") {
|
|
644
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
377
645
|
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
378
646
|
return;
|
|
379
647
|
}
|
|
380
648
|
const p = raw;
|
|
381
649
|
requireNonEmptyString(p.property, `${path}.property`, issues);
|
|
382
|
-
|
|
383
|
-
|
|
650
|
+
const forms = ["equals", "in", "exists"].filter((k) => k in p);
|
|
651
|
+
if (forms.length !== 1) {
|
|
652
|
+
issues.push(issue(path, "META-REQUIRED", `${path} must set exactly one of equals, in, exists`));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (forms[0] === "in" && (!Array.isArray(p.in) || p.in.length === 0)) {
|
|
656
|
+
issues.push(issue(`${path}.in`, "META-TYPE", `${path}.in must be a non-empty array`));
|
|
657
|
+
}
|
|
658
|
+
if (forms[0] === "exists" && typeof p.exists !== "boolean") {
|
|
659
|
+
issues.push(issue(`${path}.exists`, "META-TYPE", `${path}.exists must be a boolean`));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function requireWhere(raw, path, issues) {
|
|
663
|
+
if (Array.isArray(raw)) {
|
|
664
|
+
if (raw.length === 0) {
|
|
665
|
+
issues.push(issue(path, "META-TYPE", `${path} must be a predicate or a non-empty array`));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
raw.forEach((p, i) => requirePredicate(p, `${path}[${i}]`, issues));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
requirePredicate(raw, path, issues);
|
|
672
|
+
}
|
|
673
|
+
function whereList(where) {
|
|
674
|
+
if (!where)
|
|
675
|
+
return [];
|
|
676
|
+
return Array.isArray(where) ? where : [where];
|
|
677
|
+
}
|
|
678
|
+
function rejectReservedBuckets(buckets, path, issues) {
|
|
679
|
+
if (!Array.isArray(buckets))
|
|
680
|
+
return;
|
|
681
|
+
for (let i = 0; i < buckets.length; i++) {
|
|
682
|
+
const b = buckets[i];
|
|
683
|
+
if (typeof b !== "string")
|
|
684
|
+
continue;
|
|
685
|
+
if (ANALYTICS_RESERVED_BUCKETS.includes(b.trim().toLowerCase())) {
|
|
686
|
+
issues.push(issue(`${path}[${i}]`, "META-ANALYTICS-BUCKET", `${path}[${i}] "${b}" is not a vocabulary member; use split.missing for absent values`));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/** A measure, group, or split names an abstract or a stored object type and content type. */
|
|
691
|
+
function requireRecordSelector(node, path, issues) {
|
|
692
|
+
const abs = typeof node?.abstractType === "string" && node.abstractType.trim().length > 0;
|
|
693
|
+
const ot = typeof node?.objectType === "string" && node.objectType.trim().length > 0;
|
|
694
|
+
const ct = typeof node?.contentType === "string" && node.contentType.trim().length > 0;
|
|
695
|
+
if (abs && (ot || ct)) {
|
|
696
|
+
issues.push(issue(path, "META-ANALYTICS", `${path} names abstractType or objectType and contentType, not both`));
|
|
697
|
+
return;
|
|
384
698
|
}
|
|
699
|
+
if (abs)
|
|
700
|
+
return;
|
|
701
|
+
if (ot && ct)
|
|
702
|
+
return;
|
|
703
|
+
if (!ot && !ct) {
|
|
704
|
+
issues.push(issue(path, "META-REQUIRED", `${path} must name abstractType or objectType and contentType`));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (!ot)
|
|
708
|
+
requireNonEmptyString(node?.objectType, `${path}.objectType`, issues);
|
|
709
|
+
if (!ct)
|
|
710
|
+
requireNonEmptyString(node?.contentType, `${path}.contentType`, issues);
|
|
385
711
|
}
|
|
386
712
|
function validateAnalytics(raw, path, issues) {
|
|
387
713
|
if (!raw || typeof raw !== "object") {
|
|
@@ -391,7 +717,10 @@ function validateAnalytics(raw, path, issues) {
|
|
|
391
717
|
const a = raw;
|
|
392
718
|
requireNonEmptyString(a.id, `${path}.id`, issues);
|
|
393
719
|
requireNonEmptyString(a.name, `${path}.name`, issues);
|
|
394
|
-
if (
|
|
720
|
+
if (a.scope === undefined) {
|
|
721
|
+
// organization-wide product
|
|
722
|
+
}
|
|
723
|
+
else if (!Array.isArray(a.scope) || a.scope.length !== 2) {
|
|
395
724
|
issues.push(issue(`${path}.scope`, "META-ANALYTICS-SCOPE", `${path}.scope must be an ordered pair of two properties`));
|
|
396
725
|
}
|
|
397
726
|
else {
|
|
@@ -447,7 +776,7 @@ function validateAnalytics(raw, path, issues) {
|
|
|
447
776
|
const m = a.measures[i];
|
|
448
777
|
const mp = `${path}.measures[${i}]`;
|
|
449
778
|
requireNonEmptyString(m?.key, `${mp}.key`, issues);
|
|
450
|
-
|
|
779
|
+
requireRecordSelector(m, mp, issues);
|
|
451
780
|
if (!m?.kind ||
|
|
452
781
|
!["count", "distinct", "sum", "mean", "max"].includes(m.kind)) {
|
|
453
782
|
issues.push(issue(`${mp}.kind`, "META-ANALYTICS", `${mp}.kind must be count|distinct|sum|mean|max`));
|
|
@@ -456,7 +785,22 @@ function validateAnalytics(raw, path, issues) {
|
|
|
456
785
|
issues.push(issue(`${mp}.property`, "META-REQUIRED", `${mp}.property is required for kind ${m.kind}`));
|
|
457
786
|
}
|
|
458
787
|
if (m?.where !== undefined)
|
|
459
|
-
|
|
788
|
+
requireWhere(m.where, `${mp}.where`, issues);
|
|
789
|
+
if (m?.windows !== undefined) {
|
|
790
|
+
if (m.kind !== "count") {
|
|
791
|
+
issues.push(issue(`${mp}.windows`, "META-ANALYTICS", `${mp}.windows is only valid for kind count`));
|
|
792
|
+
}
|
|
793
|
+
if (!Array.isArray(m.windows) || m.windows.length === 0) {
|
|
794
|
+
issues.push(issue(`${mp}.windows`, "META-TYPE", `${mp}.windows must be a non-empty array`));
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
for (let j = 0; j < m.windows.length; j++) {
|
|
798
|
+
if (!["24h", "7d", "30d"].includes(m.windows[j])) {
|
|
799
|
+
issues.push(issue(`${mp}.windows[${j}]`, "META-ANALYTICS", `${mp}.windows[${j}] must be 24h|7d|30d`));
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
460
804
|
}
|
|
461
805
|
}
|
|
462
806
|
}
|
|
@@ -465,17 +809,78 @@ function validateAnalytics(raw, path, issues) {
|
|
|
465
809
|
issues.push(issue(`${path}.groups`, "META-TYPE", `${path}.groups must be an array`));
|
|
466
810
|
}
|
|
467
811
|
else {
|
|
812
|
+
requireUniqueIds(a.groups.map((g) => ({ id: g?.key })), `${path}.groups`, issues);
|
|
468
813
|
for (let i = 0; i < a.groups.length; i++) {
|
|
469
814
|
const g = a.groups[i];
|
|
470
815
|
const gp = `${path}.groups[${i}]`;
|
|
471
816
|
requireNonEmptyString(g?.key, `${gp}.key`, issues);
|
|
472
|
-
|
|
817
|
+
requireRecordSelector(g, gp, issues);
|
|
473
818
|
requireNonEmptyString(g?.property, `${gp}.property`, issues);
|
|
474
|
-
|
|
475
|
-
|
|
819
|
+
const openBuckets = g?.property === "@datasource" && g.buckets === undefined;
|
|
820
|
+
if (!openBuckets) {
|
|
821
|
+
if (!Array.isArray(g?.buckets) || g.buckets.length === 0) {
|
|
822
|
+
issues.push(issue(`${gp}.buckets`, "META-REQUIRED", `${gp}.buckets must be a non-empty array`));
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
validateEnumList(g.buckets, `${gp}.buckets`, issues);
|
|
826
|
+
rejectReservedBuckets(g.buckets, `${gp}.buckets`, issues);
|
|
827
|
+
}
|
|
476
828
|
}
|
|
477
|
-
|
|
478
|
-
|
|
829
|
+
if (g?.subject !== undefined)
|
|
830
|
+
requireNonEmptyString(g.subject, `${gp}.subject`, issues);
|
|
831
|
+
if (g?.where !== undefined)
|
|
832
|
+
requireWhere(g.where, `${gp}.where`, issues);
|
|
833
|
+
if (g?.split !== undefined) {
|
|
834
|
+
const s = g.split;
|
|
835
|
+
const sp = `${gp}.split`;
|
|
836
|
+
if (!s || typeof s !== "object") {
|
|
837
|
+
issues.push(issue(sp, "META-TYPE", `${sp} must be an object`));
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
requireNonEmptyString(s.key, `${sp}.key`, issues);
|
|
841
|
+
requireNonEmptyString(s.property, `${sp}.property`, issues);
|
|
842
|
+
if (!Array.isArray(s.buckets) || s.buckets.length === 0) {
|
|
843
|
+
issues.push(issue(`${sp}.buckets`, "META-REQUIRED", `${sp}.buckets must be a non-empty array`));
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
validateEnumList(s.buckets, `${sp}.buckets`, issues);
|
|
847
|
+
rejectReservedBuckets(s.buckets, `${sp}.buckets`, issues);
|
|
848
|
+
}
|
|
849
|
+
if (s.missing !== undefined) {
|
|
850
|
+
if (requireNonEmptyString(s.missing, `${sp}.missing`, issues)) {
|
|
851
|
+
if (Array.isArray(s.buckets) && s.buckets.includes(s.missing)) {
|
|
852
|
+
issues.push(issue(`${sp}.missing`, "META-ANALYTICS-BUCKET", `${sp}.missing "${s.missing}" must not also be a declared bucket`));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (a.derived !== undefined) {
|
|
862
|
+
if (!Array.isArray(a.derived)) {
|
|
863
|
+
issues.push(issue(`${path}.derived`, "META-TYPE", `${path}.derived must be an array`));
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
requireUniqueIds(a.derived.map((d) => ({ id: d?.key })), `${path}.derived`, issues);
|
|
867
|
+
for (let i = 0; i < a.derived.length; i++) {
|
|
868
|
+
const d = a.derived[i];
|
|
869
|
+
const dp = `${path}.derived[${i}]`;
|
|
870
|
+
requireNonEmptyString(d?.key, `${dp}.key`, issues);
|
|
871
|
+
if (!d?.op || !["sum", "difference", "ratio"].includes(d.op)) {
|
|
872
|
+
issues.push(issue(`${dp}.op`, "META-ANALYTICS", `${dp}.op must be sum|difference|ratio`));
|
|
873
|
+
}
|
|
874
|
+
if (!Array.isArray(d?.terms) || d.terms.length === 0) {
|
|
875
|
+
issues.push(issue(`${dp}.terms`, "META-REQUIRED", `${dp}.terms must be a non-empty array`));
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
d.terms.forEach((t, j) => requireNonEmptyString(t, `${dp}.terms[${j}]`, issues));
|
|
879
|
+
if (d.op === "ratio" && d.terms.length !== 2) {
|
|
880
|
+
issues.push(issue(`${dp}.terms`, "META-ANALYTICS", `${dp}.terms must have exactly two terms for ratio`));
|
|
881
|
+
}
|
|
882
|
+
if (d.op === "difference" && d.terms.length < 2) {
|
|
883
|
+
issues.push(issue(`${dp}.terms`, "META-ANALYTICS", `${dp}.terms must have at least two terms for difference`));
|
|
479
884
|
}
|
|
480
885
|
}
|
|
481
886
|
}
|
|
@@ -503,7 +908,7 @@ function validateAnalytics(raw, path, issues) {
|
|
|
503
908
|
const s = a.splits[i];
|
|
504
909
|
const sp = `${path}.splits[${i}]`;
|
|
505
910
|
requireNonEmptyString(s?.key, `${sp}.key`, issues);
|
|
506
|
-
|
|
911
|
+
requireRecordSelector(s, sp, issues);
|
|
507
912
|
requireNonEmptyString(s?.property, `${sp}.property`, issues);
|
|
508
913
|
requireNonEmptyString(s?.totalKey, `${sp}.totalKey`, issues);
|
|
509
914
|
requireNonEmptyString(s?.subsetKey, `${sp}.subsetKey`, issues);
|
|
@@ -555,7 +960,21 @@ function validateAnalytics(raw, path, issues) {
|
|
|
555
960
|
requireNonEmptyString(l?.abstractType, `${lp}.abstractType`, issues);
|
|
556
961
|
requireNonEmptyString(l?.idProperty, `${lp}.idProperty`, issues);
|
|
557
962
|
requireNonEmptyString(l?.labelProperty, `${lp}.labelProperty`, issues);
|
|
558
|
-
if (
|
|
963
|
+
if (l?.countOf !== undefined) {
|
|
964
|
+
if (!l.countOf || typeof l.countOf !== "object") {
|
|
965
|
+
issues.push(issue(`${lp}.countOf`, "META-TYPE", `${lp}.countOf must be an object`));
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
requireNonEmptyString(l.countOf.abstractType, `${lp}.countOf.abstractType`, issues);
|
|
969
|
+
requireNonEmptyString(l.countOf.property, `${lp}.countOf.property`, issues);
|
|
970
|
+
if (l.countOf.where !== undefined)
|
|
971
|
+
requireWhere(l.countOf.where, `${lp}.countOf.where`, issues);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (l?.sort === undefined && l?.countOf !== undefined) {
|
|
975
|
+
// ranked by count
|
|
976
|
+
}
|
|
977
|
+
else if (!Array.isArray(l?.sort) || l.sort.length === 0) {
|
|
559
978
|
issues.push(issue(`${lp}.sort`, "META-REQUIRED", `${lp}.sort must be a non-empty array`));
|
|
560
979
|
}
|
|
561
980
|
else {
|
|
@@ -588,7 +1007,7 @@ function validateAnalytics(raw, path, issues) {
|
|
|
588
1007
|
issues.push(issue(`${cp}.expected`, "META-REQUIRED", `${cp}.expected must be a number or { measure }`));
|
|
589
1008
|
}
|
|
590
1009
|
if (c?.where !== undefined)
|
|
591
|
-
|
|
1010
|
+
requireWhere(c.where, `${cp}.where`, issues);
|
|
592
1011
|
}
|
|
593
1012
|
}
|
|
594
1013
|
}
|
|
@@ -1028,6 +1447,30 @@ function validateReferenceIntegrity(pack, issues) {
|
|
|
1028
1447
|
}
|
|
1029
1448
|
absProps.set(abs.id, props);
|
|
1030
1449
|
}
|
|
1450
|
+
const otContent = new Map();
|
|
1451
|
+
const otFields = new Map();
|
|
1452
|
+
const snapshotBucketsByContentType = new Map();
|
|
1453
|
+
for (const ot of pack.objectTypes) {
|
|
1454
|
+
otContent.set(ot.id, new Set((ot.contentTypes ?? []).map((c) => c.id).filter(Boolean)));
|
|
1455
|
+
for (const c of ot.contentTypes ?? []) {
|
|
1456
|
+
const flag = c?.profile?.allowSnapshotBuckets;
|
|
1457
|
+
if (!c?.id || typeof flag !== "boolean")
|
|
1458
|
+
continue;
|
|
1459
|
+
const previous = snapshotBucketsByContentType.get(c.id);
|
|
1460
|
+
if (previous !== undefined && previous !== flag) {
|
|
1461
|
+
issues.push(issue(`objectTypes/${ot.id}.contentTypes`, "META-CT-PROFILE", `content type "${c.id}" must declare allowSnapshotBuckets the same way on every object type`));
|
|
1462
|
+
}
|
|
1463
|
+
else {
|
|
1464
|
+
snapshotBucketsByContentType.set(c.id, flag);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
const fields = new Map();
|
|
1468
|
+
for (const f of ot.fields ?? []) {
|
|
1469
|
+
if (f?.path)
|
|
1470
|
+
fields.set(f.path, { enum: f.enum });
|
|
1471
|
+
}
|
|
1472
|
+
otFields.set(ot.id, fields);
|
|
1473
|
+
}
|
|
1031
1474
|
const measureKeysByAnalytics = new Map();
|
|
1032
1475
|
for (const an of pack.analytics ?? []) {
|
|
1033
1476
|
const measureKeys = new Set((an.measures ?? []).map((m) => m.key).filter(Boolean));
|
|
@@ -1037,6 +1480,8 @@ function validateReferenceIntegrity(pack, issues) {
|
|
|
1037
1480
|
issues.push(issue(path, "META-REF-ABSTRACT", `analytics references unknown abstract "${abstractType}"`));
|
|
1038
1481
|
return null;
|
|
1039
1482
|
}
|
|
1483
|
+
if (ANALYTICS_PSEUDO_PROPERTIES.includes(property))
|
|
1484
|
+
return null;
|
|
1040
1485
|
const props = absProps.get(abstractType);
|
|
1041
1486
|
if (props && props.size > 0 && !props.has(property)) {
|
|
1042
1487
|
issues.push(issue(path, "META-REF-PROP", `analytics references unknown property "${property}" on "${abstractType}"`));
|
|
@@ -1044,6 +1489,26 @@ function validateReferenceIntegrity(pack, issues) {
|
|
|
1044
1489
|
}
|
|
1045
1490
|
return props?.get(property) ?? null;
|
|
1046
1491
|
};
|
|
1492
|
+
const namesContent = (node) => Boolean((node.objectType?.trim() || node.contentType?.trim()) && !node.abstractType?.trim());
|
|
1493
|
+
const checkStored = (node, path) => {
|
|
1494
|
+
const objectType = node.objectType?.trim() ?? "";
|
|
1495
|
+
const contentType = node.contentType?.trim() ?? "";
|
|
1496
|
+
if (!objectTypeIds.has(objectType)) {
|
|
1497
|
+
issues.push(issue(path, "META-REF-OBJECT", `analytics references unknown object type "${objectType}"`));
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (!otContent.get(objectType)?.has(contentType)) {
|
|
1501
|
+
issues.push(issue(path, "META-REF-CONTENT", `content type "${contentType}" is not registered on "${objectType}"`));
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
const storedEnum = (objectType, property) => {
|
|
1505
|
+
if (!objectType || !property)
|
|
1506
|
+
return null;
|
|
1507
|
+
if (ANALYTICS_PSEUDO_PROPERTIES.includes(property))
|
|
1508
|
+
return null;
|
|
1509
|
+
const en = otFields.get(objectType)?.get(property)?.enum;
|
|
1510
|
+
return en?.length ? { enum: en } : null;
|
|
1511
|
+
};
|
|
1047
1512
|
for (const s of an.scope ?? []) {
|
|
1048
1513
|
if (s?.abstractType && s?.property) {
|
|
1049
1514
|
checkAbsProp(s.abstractType, s.property, `analytics/${an.id}.scope`);
|
|
@@ -1056,29 +1521,90 @@ function validateReferenceIntegrity(pack, issues) {
|
|
|
1056
1521
|
issues.push(issue(`analytics/${an.id}.target.objectType`, "META-REF-OBJECT", `analytics target references unknown object type "${an.target.objectType}"`));
|
|
1057
1522
|
}
|
|
1058
1523
|
for (const m of an.measures ?? []) {
|
|
1524
|
+
if (namesContent(m)) {
|
|
1525
|
+
checkStored(m, `analytics/${an.id}.measures/${m.key}`);
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1059
1528
|
if (m.abstractType && !abstractIds.has(m.abstractType)) {
|
|
1060
1529
|
issues.push(issue(`analytics/${an.id}.measures/${m.key}`, "META-REF-ABSTRACT", `analytics references unknown abstract "${m.abstractType}"`));
|
|
1061
1530
|
}
|
|
1062
1531
|
if (m.property && m.abstractType) {
|
|
1063
1532
|
checkAbsProp(m.abstractType, m.property, `analytics/${an.id}.measures/${m.key}.property`);
|
|
1064
1533
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1534
|
+
for (const p of whereList(m.where)) {
|
|
1535
|
+
if (p?.property && m.abstractType) {
|
|
1536
|
+
checkAbsProp(m.abstractType, p.property, `analytics/${an.id}.measures/${m.key}.where`);
|
|
1537
|
+
}
|
|
1067
1538
|
}
|
|
1068
1539
|
}
|
|
1540
|
+
const checkBucketsInEnum = (prop, buckets, path, label) => {
|
|
1541
|
+
if (!prop?.enum || !Array.isArray(buckets))
|
|
1542
|
+
return;
|
|
1543
|
+
for (const b of buckets) {
|
|
1544
|
+
if (!prop.enum.includes(b)) {
|
|
1545
|
+
issues.push(issue(path, "META-ENUM-BUCKET", `bucket "${b}" is not in enum for ${label}`));
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
const groupBuckets = new Map();
|
|
1069
1550
|
for (const g of an.groups ?? []) {
|
|
1551
|
+
if (namesContent(g)) {
|
|
1552
|
+
checkStored(g, `analytics/${an.id}.groups/${g.key}`);
|
|
1553
|
+
checkBucketsInEnum(storedEnum(g.objectType, g.property), g.buckets, `analytics/${an.id}.groups/${g.key}.buckets`, `${g.objectType}.${g.property}`);
|
|
1554
|
+
if (g.key)
|
|
1555
|
+
groupBuckets.set(g.key, Array.isArray(g.buckets) ? new Set(g.buckets) : "open");
|
|
1556
|
+
if (g.split?.property) {
|
|
1557
|
+
checkBucketsInEnum(storedEnum(g.objectType, g.split.property), g.split.buckets, `analytics/${an.id}.groups/${g.key}.split.buckets`, `${g.objectType}.${g.split.property}`);
|
|
1558
|
+
}
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1070
1561
|
const prop = g.abstractType && g.property
|
|
1071
1562
|
? checkAbsProp(g.abstractType, g.property, `analytics/${an.id}.groups/${g.key}`)
|
|
1072
1563
|
: null;
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1564
|
+
checkBucketsInEnum(prop, g.buckets, `analytics/${an.id}.groups/${g.key}.buckets`, `${g.abstractType}.${g.property}`);
|
|
1565
|
+
if (g.key)
|
|
1566
|
+
groupBuckets.set(g.key, Array.isArray(g.buckets) ? new Set(g.buckets) : "open");
|
|
1567
|
+
for (const p of whereList(g.where)) {
|
|
1568
|
+
if (p?.property && g.abstractType) {
|
|
1569
|
+
checkAbsProp(g.abstractType, p.property, `analytics/${an.id}.groups/${g.key}.where`);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
if (g.split?.property && g.abstractType) {
|
|
1573
|
+
const splitProp = checkAbsProp(g.abstractType, g.split.property, `analytics/${an.id}.groups/${g.key}.split`);
|
|
1574
|
+
checkBucketsInEnum(splitProp, g.split.buckets, `analytics/${an.id}.groups/${g.key}.split.buckets`, `${g.abstractType}.${g.split.property}`);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
const ratioKeys = new Set((an.ratios ?? []).map((r) => r.key).filter(Boolean));
|
|
1578
|
+
const derivedSoFar = new Set();
|
|
1579
|
+
for (const d of an.derived ?? []) {
|
|
1580
|
+
for (const term of d.terms ?? []) {
|
|
1581
|
+
if (typeof term !== "string" || !term)
|
|
1582
|
+
continue;
|
|
1583
|
+
let known = measureKeys.has(term) || ratioKeys.has(term) || derivedSoFar.has(term);
|
|
1584
|
+
for (let dot = term.indexOf("."); !known && dot > 0; dot = term.indexOf(".", dot + 1)) {
|
|
1585
|
+
const buckets = groupBuckets.get(term.slice(0, dot));
|
|
1586
|
+
known = buckets === "open" || Boolean(buckets?.has(term.slice(dot + 1)));
|
|
1587
|
+
}
|
|
1588
|
+
if (!known) {
|
|
1589
|
+
issues.push(issue(`analytics/${an.id}.derived/${d.key}`, "META-REF-MEASURE", `derived term "${term}" is not a measure, ratio, earlier derived key, or group.bucket`));
|
|
1078
1590
|
}
|
|
1079
1591
|
}
|
|
1592
|
+
if (d.key)
|
|
1593
|
+
derivedSoFar.add(d.key);
|
|
1080
1594
|
}
|
|
1081
1595
|
for (const s of an.splits ?? []) {
|
|
1596
|
+
if (namesContent(s)) {
|
|
1597
|
+
checkStored(s, `analytics/${an.id}.splits/${s.key}`);
|
|
1598
|
+
const prop = storedEnum(s.objectType, s.property);
|
|
1599
|
+
if (prop?.enum && Array.isArray(s.buckets)) {
|
|
1600
|
+
for (const b of s.buckets) {
|
|
1601
|
+
if (!prop.enum.includes(b)) {
|
|
1602
|
+
issues.push(issue(`analytics/${an.id}.splits/${s.key}.buckets`, "META-ENUM-BUCKET", `bucket "${b}" is not in enum for ${s.objectType}.${s.property}`));
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1082
1608
|
const prop = s.abstractType && s.property
|
|
1083
1609
|
? checkAbsProp(s.abstractType, s.property, `analytics/${an.id}.splits/${s.key}`)
|
|
1084
1610
|
: null;
|
|
@@ -1100,13 +1626,23 @@ function validateReferenceIntegrity(pack, issues) {
|
|
|
1100
1626
|
checkAbsProp(l.abstractType, l.idProperty, `analytics/${an.id}.leaders/${l.key}.idProperty`);
|
|
1101
1627
|
checkAbsProp(l.abstractType, l.labelProperty, `analytics/${an.id}.leaders/${l.key}.labelProperty`);
|
|
1102
1628
|
}
|
|
1629
|
+
if (l.countOf?.abstractType && l.countOf.property) {
|
|
1630
|
+
checkAbsProp(l.countOf.abstractType, l.countOf.property, `analytics/${an.id}.leaders/${l.key}.countOf`);
|
|
1631
|
+
for (const p of whereList(l.countOf.where)) {
|
|
1632
|
+
if (p?.property) {
|
|
1633
|
+
checkAbsProp(l.countOf.abstractType, p.property, `analytics/${an.id}.leaders/${l.key}.countOf.where`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1103
1637
|
}
|
|
1104
1638
|
for (const c of an.coverage ?? []) {
|
|
1105
1639
|
if (c.abstractType && !abstractIds.has(c.abstractType)) {
|
|
1106
1640
|
issues.push(issue(`analytics/${an.id}.coverage/${c.key}`, "META-REF-ABSTRACT", `analytics references unknown abstract "${c.abstractType}"`));
|
|
1107
1641
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1642
|
+
for (const p of whereList(c.where)) {
|
|
1643
|
+
if (p?.property && c.abstractType) {
|
|
1644
|
+
checkAbsProp(c.abstractType, p.property, `analytics/${an.id}.coverage/${c.key}.where`);
|
|
1645
|
+
}
|
|
1110
1646
|
}
|
|
1111
1647
|
if (typeof c.expected === "object" && c.expected?.measure && !measureKeys.has(c.expected.measure)) {
|
|
1112
1648
|
issues.push(issue(`analytics/${an.id}.coverage/${c.key}.expected`, "META-REF-MEASURE", `coverage expected measure "${c.expected.measure}" unknown`));
|