@x12i/memorix-metadata 3.4.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/validate.js CHANGED
@@ -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
- requireNonEmptyString(o.contentTypes[i]?.id, `${path}.contentTypes[${i}].id`, issues);
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") {
@@ -419,6 +687,28 @@ function rejectReservedBuckets(buckets, path, issues) {
419
687
  }
420
688
  }
421
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;
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);
711
+ }
422
712
  function validateAnalytics(raw, path, issues) {
423
713
  if (!raw || typeof raw !== "object") {
424
714
  issues.push(issue(path, "META-TYPE", `${path} must be an object`));
@@ -486,7 +776,7 @@ function validateAnalytics(raw, path, issues) {
486
776
  const m = a.measures[i];
487
777
  const mp = `${path}.measures[${i}]`;
488
778
  requireNonEmptyString(m?.key, `${mp}.key`, issues);
489
- requireNonEmptyString(m?.abstractType, `${mp}.abstractType`, issues);
779
+ requireRecordSelector(m, mp, issues);
490
780
  if (!m?.kind ||
491
781
  !["count", "distinct", "sum", "mean", "max"].includes(m.kind)) {
492
782
  issues.push(issue(`${mp}.kind`, "META-ANALYTICS", `${mp}.kind must be count|distinct|sum|mean|max`));
@@ -524,7 +814,7 @@ function validateAnalytics(raw, path, issues) {
524
814
  const g = a.groups[i];
525
815
  const gp = `${path}.groups[${i}]`;
526
816
  requireNonEmptyString(g?.key, `${gp}.key`, issues);
527
- requireNonEmptyString(g?.abstractType, `${gp}.abstractType`, issues);
817
+ requireRecordSelector(g, gp, issues);
528
818
  requireNonEmptyString(g?.property, `${gp}.property`, issues);
529
819
  const openBuckets = g?.property === "@datasource" && g.buckets === undefined;
530
820
  if (!openBuckets) {
@@ -618,7 +908,7 @@ function validateAnalytics(raw, path, issues) {
618
908
  const s = a.splits[i];
619
909
  const sp = `${path}.splits[${i}]`;
620
910
  requireNonEmptyString(s?.key, `${sp}.key`, issues);
621
- requireNonEmptyString(s?.abstractType, `${sp}.abstractType`, issues);
911
+ requireRecordSelector(s, sp, issues);
622
912
  requireNonEmptyString(s?.property, `${sp}.property`, issues);
623
913
  requireNonEmptyString(s?.totalKey, `${sp}.totalKey`, issues);
624
914
  requireNonEmptyString(s?.subsetKey, `${sp}.subsetKey`, issues);
@@ -1157,6 +1447,30 @@ function validateReferenceIntegrity(pack, issues) {
1157
1447
  }
1158
1448
  absProps.set(abs.id, props);
1159
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
+ }
1160
1474
  const measureKeysByAnalytics = new Map();
1161
1475
  for (const an of pack.analytics ?? []) {
1162
1476
  const measureKeys = new Set((an.measures ?? []).map((m) => m.key).filter(Boolean));
@@ -1175,6 +1489,26 @@ function validateReferenceIntegrity(pack, issues) {
1175
1489
  }
1176
1490
  return props?.get(property) ?? null;
1177
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
+ };
1178
1512
  for (const s of an.scope ?? []) {
1179
1513
  if (s?.abstractType && s?.property) {
1180
1514
  checkAbsProp(s.abstractType, s.property, `analytics/${an.id}.scope`);
@@ -1187,6 +1521,10 @@ function validateReferenceIntegrity(pack, issues) {
1187
1521
  issues.push(issue(`analytics/${an.id}.target.objectType`, "META-REF-OBJECT", `analytics target references unknown object type "${an.target.objectType}"`));
1188
1522
  }
1189
1523
  for (const m of an.measures ?? []) {
1524
+ if (namesContent(m)) {
1525
+ checkStored(m, `analytics/${an.id}.measures/${m.key}`);
1526
+ continue;
1527
+ }
1190
1528
  if (m.abstractType && !abstractIds.has(m.abstractType)) {
1191
1529
  issues.push(issue(`analytics/${an.id}.measures/${m.key}`, "META-REF-ABSTRACT", `analytics references unknown abstract "${m.abstractType}"`));
1192
1530
  }
@@ -1210,6 +1548,16 @@ function validateReferenceIntegrity(pack, issues) {
1210
1548
  };
1211
1549
  const groupBuckets = new Map();
1212
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
+ }
1213
1561
  const prop = g.abstractType && g.property
1214
1562
  ? checkAbsProp(g.abstractType, g.property, `analytics/${an.id}.groups/${g.key}`)
1215
1563
  : null;
@@ -1245,6 +1593,18 @@ function validateReferenceIntegrity(pack, issues) {
1245
1593
  derivedSoFar.add(d.key);
1246
1594
  }
1247
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
+ }
1248
1608
  const prop = s.abstractType && s.property
1249
1609
  ? checkAbsProp(s.abstractType, s.property, `analytics/${an.id}.splits/${s.key}`)
1250
1610
  : null;