@x12i/memorix-metadata 3.4.0 → 3.4.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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/story-schema.d.ts +41 -0
- package/dist/story-schema.d.ts.map +1 -0
- package/dist/story-schema.js +74 -0
- package/dist/story-schema.js.map +1 -0
- package/dist/types.d.ts +118 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +392 -4
- package/dist/validate.js.map +1 -1
- package/examples/analytics/review-decisions.pack.json +3 -3
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isValidCustomKindId, validateAgainstItemSchema, } from "./custom-kinds.js";
|
|
2
2
|
import { foldLegacyAbstractBindings } from "./fold-bindings.js";
|
|
3
|
+
import { attachStorySchemas } from "./story-schema.js";
|
|
3
4
|
import { isForbiddenOperationalKindId, METADATA_KIND_IDS, } from "./kinds.js";
|
|
4
5
|
import { validateMappingMap } from "@x12i/memorix-mapping";
|
|
5
6
|
import { isPlatformInterfaceId, platformInterfaceForCategory, } from "./platform-interfaces.js";
|
|
@@ -110,6 +111,233 @@ function validateAgent(raw, path, issues) {
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
}
|
|
114
|
+
function validateContentTypeProfile(raw, path, issues) {
|
|
115
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
116
|
+
issues.push(issue(path, "META-REQUIRED", `${path} is required (declare allowSnapshotBuckets)`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const profile = raw;
|
|
120
|
+
if (typeof profile.allowSnapshotBuckets !== "boolean") {
|
|
121
|
+
issues.push(issue(`${path}.allowSnapshotBuckets`, "META-REQUIRED", `${path}.allowSnapshotBuckets must be a boolean`));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function validateContentTypeDiscriminator(raw, path, issues) {
|
|
125
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
126
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const disc = raw;
|
|
130
|
+
requireNonEmptyString(disc.path, `${path}.path`, issues);
|
|
131
|
+
if (disc.required !== undefined && typeof disc.required !== "boolean") {
|
|
132
|
+
issues.push(issue(`${path}.required`, "META-TYPE", `${path}.required must be a boolean`));
|
|
133
|
+
}
|
|
134
|
+
if (disc.defaultValue !== undefined &&
|
|
135
|
+
(typeof disc.defaultValue !== "string" || !disc.defaultValue.trim())) {
|
|
136
|
+
issues.push(issue(`${path}.defaultValue`, "META-TYPE", `${path}.defaultValue must be a non-empty string`));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function validateFieldMappings(raw, path, issues) {
|
|
140
|
+
if (!Array.isArray(raw)) {
|
|
141
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an array`));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for (let i = 0; i < raw.length; i++) {
|
|
145
|
+
const item = raw[i];
|
|
146
|
+
const itemPath = `${path}[${i}]`;
|
|
147
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
148
|
+
issues.push(issue(itemPath, "META-TYPE", `${itemPath} must be an object`));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const profile = item;
|
|
152
|
+
requireNonEmptyString(profile.id, `${itemPath}.id`, issues);
|
|
153
|
+
requireNonEmptyString(profile.namespace, `${itemPath}.namespace`, issues);
|
|
154
|
+
if (profile.format !== "memorix-field-map/1") {
|
|
155
|
+
issues.push(issue(`${itemPath}.format`, "META-TYPE", `${itemPath}.format must be memorix-field-map/1`));
|
|
156
|
+
}
|
|
157
|
+
if (!profile.fieldMap || typeof profile.fieldMap !== "object" || Array.isArray(profile.fieldMap)) {
|
|
158
|
+
issues.push(issue(`${itemPath}.fieldMap`, "META-TYPE", `${itemPath}.fieldMap must be an object`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const PRESENTATION_TABS = new Set(["always", "whenPresent", "hidden"]);
|
|
163
|
+
const ORIGIN_PRODUCERS = new Set(["model", "code", "mixed"]);
|
|
164
|
+
const ORIGIN_USES = new Set(["always", "conditional"]);
|
|
165
|
+
const FUNCTION_KINDS = new Set(["spine", "threads"]);
|
|
166
|
+
function validateContentTypePresentation(raw, path, issues) {
|
|
167
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
168
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const presentation = raw;
|
|
172
|
+
if (presentation.label !== undefined && typeof presentation.label !== "string") {
|
|
173
|
+
issues.push(issue(`${path}.label`, "META-TYPE", `${path}.label must be a string`));
|
|
174
|
+
}
|
|
175
|
+
if (presentation.order !== undefined && typeof presentation.order !== "number") {
|
|
176
|
+
issues.push(issue(`${path}.order`, "META-TYPE", `${path}.order must be a number`));
|
|
177
|
+
}
|
|
178
|
+
if (presentation.tab !== undefined &&
|
|
179
|
+
(typeof presentation.tab !== "string" || !PRESENTATION_TABS.has(presentation.tab))) {
|
|
180
|
+
issues.push(issue(`${path}.tab`, "META-TYPE", `${path}.tab must be always, whenPresent, or hidden`));
|
|
181
|
+
}
|
|
182
|
+
if (presentation.values === undefined)
|
|
183
|
+
return;
|
|
184
|
+
if (!Array.isArray(presentation.values)) {
|
|
185
|
+
issues.push(issue(`${path}.values`, "META-TYPE", `${path}.values must be an array`));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
for (let i = 0; i < presentation.values.length; i++) {
|
|
189
|
+
const value = presentation.values[i];
|
|
190
|
+
const valuePath = `${path}.values[${i}]`;
|
|
191
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
192
|
+
issues.push(issue(valuePath, "META-TYPE", `${valuePath} must be an object`));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
requireNonEmptyString(value.value, `${valuePath}.value`, issues);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function validateContentTypeSchema(raw, path, issues) {
|
|
199
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
200
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an object keyed by discriminator value`));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const [key, schema] of Object.entries(raw)) {
|
|
204
|
+
if (!key.trim()) {
|
|
205
|
+
issues.push(issue(`${path}.${key}`, "META-TYPE", `${path} keys must be non-empty discriminator values`));
|
|
206
|
+
}
|
|
207
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
208
|
+
issues.push(issue(`${path}.${key}`, "META-TYPE", `${path}.${key} must be a JSON Schema object`));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function validateContentTypeOrigin(raw, path, issues) {
|
|
213
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
214
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const origin = raw;
|
|
218
|
+
if (origin.producer !== undefined &&
|
|
219
|
+
(typeof origin.producer !== "string" || !ORIGIN_PRODUCERS.has(origin.producer))) {
|
|
220
|
+
issues.push(issue(`${path}.producer`, "META-TYPE", `${path}.producer must be model, code, or mixed`));
|
|
221
|
+
}
|
|
222
|
+
if (origin.use !== undefined && (typeof origin.use !== "string" || !ORIGIN_USES.has(origin.use))) {
|
|
223
|
+
issues.push(issue(`${path}.use`, "META-TYPE", `${path}.use must be always or conditional`));
|
|
224
|
+
}
|
|
225
|
+
if (origin.record !== undefined && (typeof origin.record !== "string" || !origin.record.trim())) {
|
|
226
|
+
issues.push(issue(`${path}.record`, "META-TYPE", `${path}.record must be a non-empty string`));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function validateFunctionRead(raw, path, issues) {
|
|
230
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
231
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const read = raw;
|
|
235
|
+
for (const key of ["objectType", "contentType", "recordId", "template"]) {
|
|
236
|
+
const value = read[key];
|
|
237
|
+
if (value !== undefined && (typeof value !== "string" || !value.trim())) {
|
|
238
|
+
issues.push(issue(`${path}.${key}`, "META-TYPE", `${path}.${key} must be a non-empty string`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (read.sameSubject !== undefined && typeof read.sameSubject !== "boolean") {
|
|
242
|
+
issues.push(issue(`${path}.sameSubject`, "META-TYPE", `${path}.sameSubject must be a boolean`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function validateObjectTypeFunctions(raw, path, issues) {
|
|
246
|
+
if (!Array.isArray(raw)) {
|
|
247
|
+
issues.push(issue(path, "META-TYPE", `${path} must be an array`));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
requireUniqueIds(raw, path, issues);
|
|
251
|
+
const templates = new Map();
|
|
252
|
+
for (let i = 0; i < raw.length; i++) {
|
|
253
|
+
const item = raw[i];
|
|
254
|
+
const itemPath = `${path}[${i}]`;
|
|
255
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
256
|
+
issues.push(issue(itemPath, "META-TYPE", `${itemPath} must be an object`));
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const fn = item;
|
|
260
|
+
requireNonEmptyString(fn.id, `${itemPath}.id`, issues);
|
|
261
|
+
requireNonEmptyString(fn.question, `${itemPath}.question`, issues);
|
|
262
|
+
requireNonEmptyString(fn.page, `${itemPath}.page`, issues);
|
|
263
|
+
if (typeof fn.composes !== "boolean") {
|
|
264
|
+
issues.push(issue(`${itemPath}.composes`, "META-TYPE", `${itemPath}.composes must be a boolean`));
|
|
265
|
+
}
|
|
266
|
+
if (typeof fn.pageStory !== "boolean") {
|
|
267
|
+
issues.push(issue(`${itemPath}.pageStory`, "META-TYPE", `${itemPath}.pageStory must be a boolean`));
|
|
268
|
+
}
|
|
269
|
+
if (fn.template !== undefined) {
|
|
270
|
+
if (typeof fn.template !== "string" || !fn.template.trim()) {
|
|
271
|
+
issues.push(issue(`${itemPath}.template`, "META-TYPE", `${itemPath}.template must be a non-empty string`));
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const template = fn.template.trim();
|
|
275
|
+
const previous = templates.get(template);
|
|
276
|
+
if (previous !== undefined) {
|
|
277
|
+
issues.push(issue(`${itemPath}.template`, "META-STORY-TEMPLATE", `template "${template}" is already used by ${path}[${previous}]`));
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
templates.set(template, i);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (fn.kind !== undefined && (typeof fn.kind !== "string" || !FUNCTION_KINDS.has(fn.kind))) {
|
|
285
|
+
issues.push(issue(`${itemPath}.kind`, "META-TYPE", `${itemPath}.kind must be spine or threads`));
|
|
286
|
+
}
|
|
287
|
+
if (!Array.isArray(fn.reads)) {
|
|
288
|
+
issues.push(issue(`${itemPath}.reads`, "META-TYPE", `${itemPath}.reads must be an array`));
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
for (let r = 0; r < fn.reads.length; r++) {
|
|
292
|
+
validateFunctionRead(fn.reads[r], `${itemPath}.reads[${r}]`, issues);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function storyPresentationTemplates(contentTypes) {
|
|
298
|
+
if (!Array.isArray(contentTypes))
|
|
299
|
+
return [];
|
|
300
|
+
const templates = [];
|
|
301
|
+
for (const contentType of contentTypes) {
|
|
302
|
+
if (!contentType || typeof contentType !== "object" || Array.isArray(contentType))
|
|
303
|
+
continue;
|
|
304
|
+
const spec = contentType;
|
|
305
|
+
if (spec.id !== "story")
|
|
306
|
+
continue;
|
|
307
|
+
const values = spec.presentation?.values;
|
|
308
|
+
if (!Array.isArray(values))
|
|
309
|
+
continue;
|
|
310
|
+
for (const value of values) {
|
|
311
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
312
|
+
continue;
|
|
313
|
+
const raw = value.value;
|
|
314
|
+
if (typeof raw === "string" && raw.trim())
|
|
315
|
+
templates.push(raw.trim());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return templates;
|
|
319
|
+
}
|
|
320
|
+
function assertStoryFunctionTemplates(contentTypes, functions, path, issues) {
|
|
321
|
+
const counts = new Map();
|
|
322
|
+
for (const fn of functions) {
|
|
323
|
+
if (!fn || typeof fn !== "object" || Array.isArray(fn))
|
|
324
|
+
continue;
|
|
325
|
+
const template = fn.template;
|
|
326
|
+
if (typeof template !== "string" || !template.trim())
|
|
327
|
+
continue;
|
|
328
|
+
const key = template.trim();
|
|
329
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
330
|
+
}
|
|
331
|
+
const seen = new Set();
|
|
332
|
+
for (const template of storyPresentationTemplates(contentTypes)) {
|
|
333
|
+
if (seen.has(template))
|
|
334
|
+
continue;
|
|
335
|
+
seen.add(template);
|
|
336
|
+
if ((counts.get(template) ?? 0) === 0) {
|
|
337
|
+
issues.push(issue(`${path}.functions`, "META-STORY-TEMPLATE", `story template "${template}" must have exactly one function on this object type`));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
113
341
|
function validateObjectType(raw, path, issues) {
|
|
114
342
|
if (!raw || typeof raw !== "object") {
|
|
115
343
|
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
@@ -134,12 +362,59 @@ function validateObjectType(raw, path, issues) {
|
|
|
134
362
|
}
|
|
135
363
|
}
|
|
136
364
|
}
|
|
365
|
+
let declaredContentTypes = new Set();
|
|
137
366
|
if (!Array.isArray(o.contentTypes) || o.contentTypes.length === 0) {
|
|
138
367
|
issues.push(issue(`${path}.contentTypes`, "META-REQUIRED", `${path}.contentTypes must be a non-empty array`));
|
|
139
368
|
}
|
|
140
369
|
else {
|
|
370
|
+
let leadingCount = 0;
|
|
141
371
|
for (let i = 0; i < o.contentTypes.length; i++) {
|
|
142
|
-
|
|
372
|
+
const ct = o.contentTypes[i];
|
|
373
|
+
const cp = `${path}.contentTypes[${i}]`;
|
|
374
|
+
if (!ct || typeof ct !== "object" || Array.isArray(ct)) {
|
|
375
|
+
issues.push(issue(cp, "META-TYPE", `${cp} must be an object`));
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (requireNonEmptyString(ct.id, `${cp}.id`, issues)) {
|
|
379
|
+
declaredContentTypes.add(ct.id);
|
|
380
|
+
}
|
|
381
|
+
validateContentTypeProfile(ct.profile, `${cp}.profile`, issues);
|
|
382
|
+
if (ct.required !== undefined && typeof ct.required !== "boolean") {
|
|
383
|
+
issues.push(issue(`${cp}.required`, "META-TYPE", `${cp}.required must be a boolean`));
|
|
384
|
+
}
|
|
385
|
+
else if (ct.required === true) {
|
|
386
|
+
requireNonEmptyString(ct.requiredReason, `${cp}.requiredReason`, issues);
|
|
387
|
+
}
|
|
388
|
+
if (ct.leading !== undefined) {
|
|
389
|
+
if (typeof ct.leading !== "boolean") {
|
|
390
|
+
issues.push(issue(`${cp}.leading`, "META-TYPE", `${cp}.leading must be a boolean`));
|
|
391
|
+
}
|
|
392
|
+
else if (ct.leading) {
|
|
393
|
+
leadingCount += 1;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (ct.discriminator !== undefined) {
|
|
397
|
+
validateContentTypeDiscriminator(ct.discriminator, `${cp}.discriminator`, issues);
|
|
398
|
+
}
|
|
399
|
+
if (ct.fieldMappings !== undefined) {
|
|
400
|
+
validateFieldMappings(ct.fieldMappings, `${cp}.fieldMappings`, issues);
|
|
401
|
+
}
|
|
402
|
+
if (ct.presentation !== undefined) {
|
|
403
|
+
validateContentTypePresentation(ct.presentation, `${cp}.presentation`, issues);
|
|
404
|
+
}
|
|
405
|
+
if (ct.origin !== undefined) {
|
|
406
|
+
validateContentTypeOrigin(ct.origin, `${cp}.origin`, issues);
|
|
407
|
+
}
|
|
408
|
+
if (ct.schema !== undefined) {
|
|
409
|
+
validateContentTypeSchema(ct.schema, `${cp}.schema`, issues);
|
|
410
|
+
}
|
|
411
|
+
if (ct.owner !== undefined && (typeof ct.owner !== "string" || !ct.owner.trim())) {
|
|
412
|
+
issues.push(issue(`${cp}.owner`, "META-TYPE", `${cp}.owner must be a non-empty string`));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
requireUniqueIds(o.contentTypes, `${path}.contentTypes`, issues);
|
|
416
|
+
if (leadingCount > 1) {
|
|
417
|
+
issues.push(issue(`${path}.contentTypes`, "META-LEADING", `object type "${typeof o.id === "string" ? o.id : ""}" has more than one leading content type`));
|
|
143
418
|
}
|
|
144
419
|
}
|
|
145
420
|
if (o.fields !== undefined) {
|
|
@@ -156,12 +431,26 @@ function validateObjectType(raw, path, issues) {
|
|
|
156
431
|
}
|
|
157
432
|
requireNonEmptyString(f.path, `${fp}.path`, issues);
|
|
158
433
|
requireNonEmptyString(f.valueType, `${fp}.valueType`, issues);
|
|
434
|
+
if (f.contentType !== undefined) {
|
|
435
|
+
if (typeof f.contentType !== "string" || !f.contentType.trim()) {
|
|
436
|
+
issues.push(issue(`${fp}.contentType`, "META-TYPE", `${fp}.contentType must be a non-empty string`));
|
|
437
|
+
}
|
|
438
|
+
else if (!declaredContentTypes.has(f.contentType)) {
|
|
439
|
+
issues.push(issue(`${fp}.contentType`, "META-REF-CONTENT", `content type "${f.contentType}" is not registered on this object type`));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
159
442
|
if (f.enum !== undefined) {
|
|
160
443
|
validateEnumList(f.enum, `${fp}.enum`, issues);
|
|
161
444
|
}
|
|
162
445
|
}
|
|
163
446
|
}
|
|
164
447
|
}
|
|
448
|
+
if (o.functions !== undefined) {
|
|
449
|
+
validateObjectTypeFunctions(o.functions, `${path}.functions`, issues);
|
|
450
|
+
if (Array.isArray(o.functions)) {
|
|
451
|
+
assertStoryFunctionTemplates(o.contentTypes, o.functions, path, issues);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
165
454
|
}
|
|
166
455
|
function validateAbstractType(raw, path, issues) {
|
|
167
456
|
if (!raw || typeof raw !== "object") {
|
|
@@ -419,6 +708,28 @@ function rejectReservedBuckets(buckets, path, issues) {
|
|
|
419
708
|
}
|
|
420
709
|
}
|
|
421
710
|
}
|
|
711
|
+
/** A measure, group, or split names an abstract or a stored object type and content type. */
|
|
712
|
+
function requireRecordSelector(node, path, issues) {
|
|
713
|
+
const abs = typeof node?.abstractType === "string" && node.abstractType.trim().length > 0;
|
|
714
|
+
const ot = typeof node?.objectType === "string" && node.objectType.trim().length > 0;
|
|
715
|
+
const ct = typeof node?.contentType === "string" && node.contentType.trim().length > 0;
|
|
716
|
+
if (abs && (ot || ct)) {
|
|
717
|
+
issues.push(issue(path, "META-ANALYTICS", `${path} names abstractType or objectType and contentType, not both`));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (abs)
|
|
721
|
+
return;
|
|
722
|
+
if (ot && ct)
|
|
723
|
+
return;
|
|
724
|
+
if (!ot && !ct) {
|
|
725
|
+
issues.push(issue(path, "META-REQUIRED", `${path} must name abstractType or objectType and contentType`));
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (!ot)
|
|
729
|
+
requireNonEmptyString(node?.objectType, `${path}.objectType`, issues);
|
|
730
|
+
if (!ct)
|
|
731
|
+
requireNonEmptyString(node?.contentType, `${path}.contentType`, issues);
|
|
732
|
+
}
|
|
422
733
|
function validateAnalytics(raw, path, issues) {
|
|
423
734
|
if (!raw || typeof raw !== "object") {
|
|
424
735
|
issues.push(issue(path, "META-TYPE", `${path} must be an object`));
|
|
@@ -486,7 +797,7 @@ function validateAnalytics(raw, path, issues) {
|
|
|
486
797
|
const m = a.measures[i];
|
|
487
798
|
const mp = `${path}.measures[${i}]`;
|
|
488
799
|
requireNonEmptyString(m?.key, `${mp}.key`, issues);
|
|
489
|
-
|
|
800
|
+
requireRecordSelector(m, mp, issues);
|
|
490
801
|
if (!m?.kind ||
|
|
491
802
|
!["count", "distinct", "sum", "mean", "max"].includes(m.kind)) {
|
|
492
803
|
issues.push(issue(`${mp}.kind`, "META-ANALYTICS", `${mp}.kind must be count|distinct|sum|mean|max`));
|
|
@@ -524,7 +835,7 @@ function validateAnalytics(raw, path, issues) {
|
|
|
524
835
|
const g = a.groups[i];
|
|
525
836
|
const gp = `${path}.groups[${i}]`;
|
|
526
837
|
requireNonEmptyString(g?.key, `${gp}.key`, issues);
|
|
527
|
-
|
|
838
|
+
requireRecordSelector(g, gp, issues);
|
|
528
839
|
requireNonEmptyString(g?.property, `${gp}.property`, issues);
|
|
529
840
|
const openBuckets = g?.property === "@datasource" && g.buckets === undefined;
|
|
530
841
|
if (!openBuckets) {
|
|
@@ -618,7 +929,7 @@ function validateAnalytics(raw, path, issues) {
|
|
|
618
929
|
const s = a.splits[i];
|
|
619
930
|
const sp = `${path}.splits[${i}]`;
|
|
620
931
|
requireNonEmptyString(s?.key, `${sp}.key`, issues);
|
|
621
|
-
|
|
932
|
+
requireRecordSelector(s, sp, issues);
|
|
622
933
|
requireNonEmptyString(s?.property, `${sp}.property`, issues);
|
|
623
934
|
requireNonEmptyString(s?.totalKey, `${sp}.totalKey`, issues);
|
|
624
935
|
requireNonEmptyString(s?.subsetKey, `${sp}.subsetKey`, issues);
|
|
@@ -1157,6 +1468,30 @@ function validateReferenceIntegrity(pack, issues) {
|
|
|
1157
1468
|
}
|
|
1158
1469
|
absProps.set(abs.id, props);
|
|
1159
1470
|
}
|
|
1471
|
+
const otContent = new Map();
|
|
1472
|
+
const otFields = new Map();
|
|
1473
|
+
const snapshotBucketsByContentType = new Map();
|
|
1474
|
+
for (const ot of pack.objectTypes) {
|
|
1475
|
+
otContent.set(ot.id, new Set((ot.contentTypes ?? []).map((c) => c.id).filter(Boolean)));
|
|
1476
|
+
for (const c of ot.contentTypes ?? []) {
|
|
1477
|
+
const flag = c?.profile?.allowSnapshotBuckets;
|
|
1478
|
+
if (!c?.id || typeof flag !== "boolean")
|
|
1479
|
+
continue;
|
|
1480
|
+
const previous = snapshotBucketsByContentType.get(c.id);
|
|
1481
|
+
if (previous !== undefined && previous !== flag) {
|
|
1482
|
+
issues.push(issue(`objectTypes/${ot.id}.contentTypes`, "META-CT-PROFILE", `content type "${c.id}" must declare allowSnapshotBuckets the same way on every object type`));
|
|
1483
|
+
}
|
|
1484
|
+
else {
|
|
1485
|
+
snapshotBucketsByContentType.set(c.id, flag);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
const fields = new Map();
|
|
1489
|
+
for (const f of ot.fields ?? []) {
|
|
1490
|
+
if (f?.path)
|
|
1491
|
+
fields.set(f.path, { enum: f.enum });
|
|
1492
|
+
}
|
|
1493
|
+
otFields.set(ot.id, fields);
|
|
1494
|
+
}
|
|
1160
1495
|
const measureKeysByAnalytics = new Map();
|
|
1161
1496
|
for (const an of pack.analytics ?? []) {
|
|
1162
1497
|
const measureKeys = new Set((an.measures ?? []).map((m) => m.key).filter(Boolean));
|
|
@@ -1175,6 +1510,26 @@ function validateReferenceIntegrity(pack, issues) {
|
|
|
1175
1510
|
}
|
|
1176
1511
|
return props?.get(property) ?? null;
|
|
1177
1512
|
};
|
|
1513
|
+
const namesContent = (node) => Boolean((node.objectType?.trim() || node.contentType?.trim()) && !node.abstractType?.trim());
|
|
1514
|
+
const checkStored = (node, path) => {
|
|
1515
|
+
const objectType = node.objectType?.trim() ?? "";
|
|
1516
|
+
const contentType = node.contentType?.trim() ?? "";
|
|
1517
|
+
if (!objectTypeIds.has(objectType)) {
|
|
1518
|
+
issues.push(issue(path, "META-REF-OBJECT", `analytics references unknown object type "${objectType}"`));
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (!otContent.get(objectType)?.has(contentType)) {
|
|
1522
|
+
issues.push(issue(path, "META-REF-CONTENT", `content type "${contentType}" is not registered on "${objectType}"`));
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
const storedEnum = (objectType, property) => {
|
|
1526
|
+
if (!objectType || !property)
|
|
1527
|
+
return null;
|
|
1528
|
+
if (ANALYTICS_PSEUDO_PROPERTIES.includes(property))
|
|
1529
|
+
return null;
|
|
1530
|
+
const en = otFields.get(objectType)?.get(property)?.enum;
|
|
1531
|
+
return en?.length ? { enum: en } : null;
|
|
1532
|
+
};
|
|
1178
1533
|
for (const s of an.scope ?? []) {
|
|
1179
1534
|
if (s?.abstractType && s?.property) {
|
|
1180
1535
|
checkAbsProp(s.abstractType, s.property, `analytics/${an.id}.scope`);
|
|
@@ -1187,6 +1542,10 @@ function validateReferenceIntegrity(pack, issues) {
|
|
|
1187
1542
|
issues.push(issue(`analytics/${an.id}.target.objectType`, "META-REF-OBJECT", `analytics target references unknown object type "${an.target.objectType}"`));
|
|
1188
1543
|
}
|
|
1189
1544
|
for (const m of an.measures ?? []) {
|
|
1545
|
+
if (namesContent(m)) {
|
|
1546
|
+
checkStored(m, `analytics/${an.id}.measures/${m.key}`);
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1190
1549
|
if (m.abstractType && !abstractIds.has(m.abstractType)) {
|
|
1191
1550
|
issues.push(issue(`analytics/${an.id}.measures/${m.key}`, "META-REF-ABSTRACT", `analytics references unknown abstract "${m.abstractType}"`));
|
|
1192
1551
|
}
|
|
@@ -1210,6 +1569,16 @@ function validateReferenceIntegrity(pack, issues) {
|
|
|
1210
1569
|
};
|
|
1211
1570
|
const groupBuckets = new Map();
|
|
1212
1571
|
for (const g of an.groups ?? []) {
|
|
1572
|
+
if (namesContent(g)) {
|
|
1573
|
+
checkStored(g, `analytics/${an.id}.groups/${g.key}`);
|
|
1574
|
+
checkBucketsInEnum(storedEnum(g.objectType, g.property), g.buckets, `analytics/${an.id}.groups/${g.key}.buckets`, `${g.objectType}.${g.property}`);
|
|
1575
|
+
if (g.key)
|
|
1576
|
+
groupBuckets.set(g.key, Array.isArray(g.buckets) ? new Set(g.buckets) : "open");
|
|
1577
|
+
if (g.split?.property) {
|
|
1578
|
+
checkBucketsInEnum(storedEnum(g.objectType, g.split.property), g.split.buckets, `analytics/${an.id}.groups/${g.key}.split.buckets`, `${g.objectType}.${g.split.property}`);
|
|
1579
|
+
}
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1213
1582
|
const prop = g.abstractType && g.property
|
|
1214
1583
|
? checkAbsProp(g.abstractType, g.property, `analytics/${an.id}.groups/${g.key}`)
|
|
1215
1584
|
: null;
|
|
@@ -1245,6 +1614,18 @@ function validateReferenceIntegrity(pack, issues) {
|
|
|
1245
1614
|
derivedSoFar.add(d.key);
|
|
1246
1615
|
}
|
|
1247
1616
|
for (const s of an.splits ?? []) {
|
|
1617
|
+
if (namesContent(s)) {
|
|
1618
|
+
checkStored(s, `analytics/${an.id}.splits/${s.key}`);
|
|
1619
|
+
const prop = storedEnum(s.objectType, s.property);
|
|
1620
|
+
if (prop?.enum && Array.isArray(s.buckets)) {
|
|
1621
|
+
for (const b of s.buckets) {
|
|
1622
|
+
if (!prop.enum.includes(b)) {
|
|
1623
|
+
issues.push(issue(`analytics/${an.id}.splits/${s.key}.buckets`, "META-ENUM-BUCKET", `bucket "${b}" is not in enum for ${s.objectType}.${s.property}`));
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1248
1629
|
const prop = s.abstractType && s.property
|
|
1249
1630
|
? checkAbsProp(s.abstractType, s.property, `analytics/${an.id}.splits/${s.key}`)
|
|
1250
1631
|
: null;
|
|
@@ -1402,6 +1783,13 @@ export function validateMetadataPack(input, opts) {
|
|
|
1402
1783
|
if (Array.isArray(pack.abstractTypes)) {
|
|
1403
1784
|
foldLegacyAbstractBindings(pack);
|
|
1404
1785
|
}
|
|
1786
|
+
// Off unless the caller turns it on. Story writers that still send empty threads get 400 once this runs.
|
|
1787
|
+
if (opts?.generateStorySchemas === true && Array.isArray(pack.objectTypes)) {
|
|
1788
|
+
for (const objectType of pack.objectTypes) {
|
|
1789
|
+
if (objectType && typeof objectType === "object")
|
|
1790
|
+
attachStorySchemas(objectType);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1405
1793
|
if (pack.format !== "memorix-metadata/1") {
|
|
1406
1794
|
issues.push(issue("format", "META-FORMAT", 'format must be "memorix-metadata/1"'));
|
|
1407
1795
|
}
|