@takeshape/schema 9.32.3 → 9.33.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/es/validate.js CHANGED
@@ -12,16 +12,16 @@ import { CURRENT_SCHEMA_VERSION, LEGACY_SCHEMA_VERSION, LEGACY_API_VERSION } fro
12
12
  import { defaultWorkflow } from './workflows';
13
13
  import { allProjectSchemas } from './schemas';
14
14
  import authSchemas from './schemas/auth-schemas.json';
15
- import { getAllNamespaceShapes, getAllRefsInShapes } from './schema-util';
15
+ import { getAllRefs } from './schema-util';
16
16
  import { builtInShapes } from './builtin-schema';
17
17
  import { isValidTemplate } from './template-shapes';
18
18
  import { isBasicResolver, isComposeResolver, isExtendsSchema, isObjectSchema } from './types/utils';
19
- import { refItemToAtRef, refItemToShapeName, parseReturnShape, getRefShapeName, parsePropertyRef, propertyRefItemToResolverPath, createGetNamespace } from './refs';
19
+ import { refItemToShapeName, getRefShapeName, parsePropertyRef, propertyRefItemToResolverPath, createGetNamespace, propertyRefItemToPath } from './refs';
20
20
  import { scalars } from './scalars';
21
21
  import { isUnionSchema } from './unions';
22
22
  import { isEnumLikeSchema } from './enum';
23
23
  import metaSchemaV3_9_0 from './schemas/project-schema/meta-schema-v3.9.0.json';
24
- import { ensureArray, isIntegerLike, isRecord } from '@takeshape/util';
24
+ import { ensureArray, isIntegerLike, isRecord, value } from '@takeshape/util';
25
25
  import forOwn from 'lodash/forOwn';
26
26
  import uniqBy from 'lodash/uniqBy';
27
27
  import initial from 'lodash/initial';
@@ -32,6 +32,9 @@ import gte from 'semver/functions/gte';
32
32
  import size from 'lodash/size';
33
33
  import isEqual from 'lodash/isEqual';
34
34
  import pick from 'lodash/pick';
35
+ import pMap from 'p-map';
36
+ import { flatten } from 'lodash';
37
+ const builtInShapeNames = new Set([...Object.keys(builtInShapes), ...scalars, 'object']);
35
38
 
36
39
  function findDuplicates(items) {
37
40
  const seen = {};
@@ -166,7 +169,24 @@ function getSemver(schemaVersion) {
166
169
  return coerce(schemaVersion) ?? LEGACY_SCHEMA_VERSION;
167
170
  }
168
171
 
169
- function validateResolver(projectSchema, basePath, resolver) {
172
+ function enumerateBasicResolvers(resolver, path) {
173
+ const results = [];
174
+
175
+ const visit = (resolver, path) => {
176
+ if (isComposeResolver(resolver)) {
177
+ resolver.compose.forEach((resolver, i) => {
178
+ visit(resolver, [...path, i]);
179
+ });
180
+ } else {
181
+ results.push([resolver, path]);
182
+ }
183
+ };
184
+
185
+ visit(resolver, path);
186
+ return results;
187
+ }
188
+
189
+ function validateResolver(projectSchema, basePath, baseResolver) {
170
190
  const errors = [];
171
191
  /**
172
192
  * V3.9 Resolver name enums are the set used for earlier v3 validations
@@ -210,14 +230,14 @@ function validateResolver(projectSchema, basePath, resolver) {
210
230
  const isLessThanV3_9_0 = lt(getSemver(projectSchema.schemaVersion), '3.9.0');
211
231
  const getNamespace = createGetNamespace(projectSchema);
212
232
 
213
- const validateBasicResolver = resolver => {
233
+ for (const [resolver, path] of enumerateBasicResolvers(baseResolver, basePath)) {
214
234
  if (isBasicResolver(resolver)) {
215
235
  var _projectSchema$servic;
216
236
 
217
237
  if ('service' in resolver && resolver.service !== 'takeshape:local' && !((_projectSchema$servic = projectSchema.services) !== null && _projectSchema$servic !== void 0 && _projectSchema$servic[resolver.service])) {
218
238
  errors.push({
219
239
  type: 'notFound',
220
- path: basePath.concat(['service']),
240
+ path: path.concat(['service']),
221
241
  message: `Invalid service "${resolver.service}"`
222
242
  });
223
243
  }
@@ -225,7 +245,7 @@ function validateResolver(projectSchema, basePath, resolver) {
225
245
  if (isLessThanV3_9_0 && !isValidResolverNameV3_9_0(resolver.name)) {
226
246
  errors.push({
227
247
  type: 'notFound',
228
- path: basePath.concat(['name']),
248
+ path: path.concat(['name']),
229
249
  message: `Invalid resolver name "${resolver.name}"`
230
250
  });
231
251
  }
@@ -241,7 +261,7 @@ function validateResolver(projectSchema, basePath, resolver) {
241
261
  if (!isValidShapeName(shapeName)) {
242
262
  errors.push({
243
263
  type: 'notFound',
244
- path: basePath.concat(['options', 'model']),
264
+ path: path.concat(['options', 'model']),
245
265
  message: `Invalid Model Shape "${shapeName ?? ''}"`
246
266
  });
247
267
  }
@@ -256,7 +276,7 @@ function validateResolver(projectSchema, basePath, resolver) {
256
276
  if (!isValidShapeName(shapeName)) {
257
277
  errors.push({
258
278
  type: 'notFound',
259
- path: basePath.concat(['shapeName']),
279
+ path: path.concat(['shapeName']),
260
280
  message: `Invalid Model Shape "${shapeName ?? ''}"`
261
281
  });
262
282
  }
@@ -274,7 +294,7 @@ function validateResolver(projectSchema, basePath, resolver) {
274
294
  } else {
275
295
  errors.push({
276
296
  type: 'conflict',
277
- path: basePath.concat('to'),
297
+ path: path.concat('to'),
278
298
  message: `Unable to parse property ref "${resolver.to}"`
279
299
  });
280
300
  }
@@ -282,62 +302,115 @@ function validateResolver(projectSchema, basePath, resolver) {
282
302
  } else {
283
303
  errors.push({
284
304
  type: 'notFound',
285
- path: basePath,
305
+ path,
286
306
  message: `Invalid resolver`
287
307
  });
288
308
  }
289
- };
290
-
291
- if (isComposeResolver(resolver)) {
292
- resolver.compose.forEach(validateBasicResolver);
293
- } else {
294
- validateBasicResolver(resolver);
295
309
  }
296
310
 
297
311
  return errors;
298
- } // eslint-disable-next-line max-params
299
-
312
+ }
300
313
 
301
- function validateQueryConfig(projectSchema, validReturnShapes, query, operation, name) {
314
+ function validateLocalQueryConfig(projectSchema, query, operation, name) {
302
315
  const location = operation === 'query' ? 'queries' : 'mutations';
303
- const errors = validateResolver(projectSchema, [location, name, 'resolver'], query.resolver);
304
- const {
305
- template,
306
- shapeName
307
- } = parseReturnShape(projectSchema, query.shape);
316
+ return validateResolver(projectSchema, [location, name, 'resolver'], query.resolver);
317
+ }
308
318
 
309
- if (template && !isValidTemplate(template)) {
310
- errors.push({
311
- type: 'notFound',
312
- path: [location, name, 'shape'],
313
- message: `Invalid template "${template}" for ${operation} "${name}"`
314
- });
319
+ const operationProps = [['query', 'queries'], ['mutation', 'mutations']];
320
+
321
+ function validateLocalQueryConfigs(projectSchema) {
322
+ const errors = [];
323
+
324
+ for (const [operation, prop] of operationProps) {
325
+ for (const name of Object.keys(projectSchema[prop])) {
326
+ errors.push(...validateLocalQueryConfig(projectSchema, projectSchema[prop][name], operation, name));
327
+ }
315
328
  }
316
329
 
317
- if (!validReturnShapes.has(shapeName)) {
318
- errors.push({
319
- type: 'notFound',
320
- path: [location, name, 'shape'],
321
- message: `Invalid shape "${shapeName}" for ${operation} "${name}"`
322
- });
330
+ return errors;
331
+ }
332
+
333
+ function allowDisconnected(context, status) {
334
+ return Boolean(context.allowDisconnectedLayers) && status !== 'notAvailable';
335
+ }
336
+
337
+ function isValidShapeReference(context, layerState, shapeName) {
338
+ if (layerState.status !== 'ok') {
339
+ return allowDisconnected(context, layerState.status);
323
340
  }
324
341
 
342
+ return Boolean(layerState.schema.shapes[shapeName]);
343
+ }
344
+
345
+ async function validateResolverReferences(context, projectSchema, basePath, baseResolver) {
346
+ const errors = [];
347
+ await pMap(enumerateBasicResolvers(baseResolver, basePath), async ([resolver, path]) => {
348
+ if (resolver.name === 'graphql:query' || resolver.name === 'graphql:mutation') {
349
+ const {
350
+ service,
351
+ fieldName
352
+ } = resolver;
353
+ const prop = resolver.name === 'graphql:query' ? 'queries' : 'mutations';
354
+ const layerState = await context.resolveLayer(service);
355
+ const valid = layerState.status === 'ok' ? Boolean(layerState.schema[prop][fieldName]) : allowDisconnected(context, layerState.status);
356
+
357
+ if (!valid) {
358
+ const operation = resolver.name === 'graphql:query' ? 'query' : 'mutation';
359
+ errors.push({
360
+ type: 'notFound',
361
+ path: path.concat('fieldName'),
362
+ message: `Missing ${operation} "${resolver.fieldName}" in service layer "${service}"`
363
+ });
364
+ }
365
+ } else if (resolver.name === 'delegate') {
366
+ const ref = parsePropertyRef(resolver.to);
367
+
368
+ if (ref && ref.serviceId !== 'local') {
369
+ const layerState = await context.resolveLayer(ref.serviceId);
370
+ const valid = layerState.status === 'ok' ? Boolean(get(layerState.schema, propertyRefItemToPath(value(''), ref))) : allowDisconnected(context, layerState.status);
371
+
372
+ if (!valid) {
373
+ errors.push({
374
+ type: 'notFound',
375
+ path: path.concat('to'),
376
+ message: `Missing resolver config at property ref "${resolver.to}"`
377
+ });
378
+ }
379
+ }
380
+ }
381
+ });
325
382
  return errors;
326
383
  }
327
384
 
328
- function validateQueryConfigs(projectSchema, additionalShapeNames = builtInShapeNames) {
329
- const validReturnShapes = new Set(Object.keys(projectSchema.shapes).concat(additionalShapeNames));
330
- let errors = [];
385
+ async function validateQueryConfig(context, projectSchema, {
386
+ query,
387
+ name,
388
+ operation
389
+ }) {
390
+ const location = operation === 'query' ? 'queries' : 'mutations';
391
+ return validateResolverReferences(context, projectSchema, [location, name, 'resolver'], query.resolver);
392
+ }
393
+
394
+ async function validateQueryConfigs(context, projectSchema) {
395
+ const isLessThanV3_9_0 = lt(getSemver(projectSchema.schemaVersion), '3.9.0');
331
396
 
332
- for (const name of Object.keys(projectSchema.queries)) {
333
- errors = errors.concat(validateQueryConfig(projectSchema, validReturnShapes, projectSchema.queries[name], 'query', name));
397
+ if (isLessThanV3_9_0) {
398
+ return [];
334
399
  }
335
400
 
336
- for (const name of Object.keys(projectSchema.mutations)) {
337
- errors = errors.concat(validateQueryConfig(projectSchema, validReturnShapes, projectSchema.mutations[name], 'mutation', name));
401
+ const promises = [];
402
+
403
+ for (const [operation, prop] of operationProps) {
404
+ for (const name of Object.keys(projectSchema[prop])) {
405
+ promises.push(validateQueryConfig(context, projectSchema, {
406
+ query: projectSchema[prop][name],
407
+ operation,
408
+ name
409
+ }));
410
+ }
338
411
  }
339
412
 
340
- return errors;
413
+ return flatten(await Promise.all(promises));
341
414
  }
342
415
 
343
416
  function validateIndexedShapeConfig(projectSchema, shapeName, config) {
@@ -419,8 +492,6 @@ function validateIndexedShapes(projectSchema) {
419
492
  return errors;
420
493
  }
421
494
 
422
- const builtInShapeNames = [...Object.keys(builtInShapes), ...scalars, 'object'];
423
-
424
495
  function getModelShapeIds(shapes) {
425
496
  return Object.values(shapes).filter(shape => shape.model).map(shape => shape.id);
426
497
  }
@@ -435,44 +506,94 @@ function isAllOfPath(path) {
435
506
  return index !== -1 && isIntegerLike(path[index + 1]);
436
507
  }
437
508
 
438
- function validateRefs(projectSchema, additionalShapeNames = builtInShapeNames) {
509
+ function validateLocalRefs(projectSchema) {
439
510
  const errors = [];
440
- const shapeNames = new Set([...additionalShapeNames, ...Object.keys(projectSchema.shapes)]);
441
- const refs = getAllRefsInShapes(projectSchema);
511
+ const shapeNames = new Set([...builtInShapeNames, ...Object.keys(projectSchema.shapes)]);
512
+ const refs = getAllRefs(projectSchema).filter(item => !item.isForeign);
513
+
514
+ for (const item of refs) {
515
+ if (item.template && !isValidTemplate(item.template)) {
516
+ errors.push({
517
+ type: 'notFound',
518
+ path: item.path,
519
+ message: `Invalid template "${item.template}"`
520
+ });
521
+ }
442
522
 
443
- if (refs.length > 0) {
444
- refs.forEach(item => {
445
- const shapeName = refItemToShapeName(item);
523
+ const shapeName = refItemToShapeName(item);
446
524
 
447
- if (!shapeNames.has(shapeName)) {
448
- errors.push({
449
- type: 'notFound',
450
- path: item.path,
451
- message: `Invalid ref "${refItemToAtRef(item)}"`
452
- });
453
- } // Make sure refs inside allOf don't refer to their own Shape
525
+ if (!shapeNames.has(shapeName)) {
526
+ errors.push({
527
+ type: 'notFound',
528
+ path: item.path,
529
+ message: `Invalid ref "${get(projectSchema, item.path)}"`
530
+ });
531
+ } // Make sure refs inside allOf don't refer to their own Shape
454
532
 
455
533
 
456
- if (item.path[1] === shapeName && isAllOfPath(item.path)) {
457
- errors.push({
458
- type: 'conflict',
459
- path: item.path,
460
- message: `allOf cannot be self-referential`
461
- });
462
- }
534
+ if (item.path[1] === shapeName && isAllOfPath(item.path)) {
535
+ errors.push({
536
+ type: 'conflict',
537
+ path: item.path,
538
+ message: `allOf cannot be self-referential`
539
+ });
540
+ }
463
541
 
464
- const parentPath = initial(item.path);
465
- const parentSchema = get(projectSchema, parentPath);
466
- const propName = last(item.path);
542
+ const parentPath = initial(item.path);
543
+ const parentSchema = get(projectSchema, parentPath);
544
+ const propName = last(item.path);
467
545
 
468
- if (propName === '@ref' && parentSchema.$ref) {
469
- errors.push({
470
- type: 'conflict',
471
- path: parentPath,
472
- message: `Ref cannot have both @ref and $ref`
473
- });
474
- }
475
- });
546
+ if (propName === '@ref' && parentSchema.$ref) {
547
+ errors.push({
548
+ type: 'conflict',
549
+ path: parentPath,
550
+ message: `Ref cannot have both @ref and $ref`
551
+ });
552
+ }
553
+ }
554
+
555
+ return errors;
556
+ }
557
+
558
+ async function validateRefs(context, projectSchema) {
559
+ const {
560
+ resolveLayer
561
+ } = context;
562
+ const errors = [];
563
+ const refs = getAllRefs(projectSchema);
564
+ const layerIds = new Set();
565
+
566
+ for (const item of refs) {
567
+ if (item.serviceKey !== 'local') {
568
+ layerIds.add(item.serviceKey);
569
+ }
570
+ }
571
+
572
+ const layersById = Object.fromEntries(await pMap(layerIds, async layerId => [layerId, await resolveLayer(layerId)]));
573
+
574
+ for (const item of refs) {
575
+ if (item.template && !isValidTemplate(item.template)) {
576
+ errors.push({
577
+ type: 'notFound',
578
+ path: item.path,
579
+ message: `Invalid template "${item.path}"`
580
+ });
581
+ }
582
+
583
+ const {
584
+ serviceKey: layerId
585
+ } = item;
586
+ const shapeName = refItemToShapeName(item);
587
+ const localShapeExists = Boolean(projectSchema.shapes[shapeName]) || builtInShapeNames.has(shapeName);
588
+ const valid = layerId === 'local' ? localShapeExists : localShapeExists || isValidShapeReference(context, layersById[layerId], shapeName);
589
+
590
+ if (!valid) {
591
+ errors.push({
592
+ type: 'notFound',
593
+ path: item.path,
594
+ message: `Invalid ref "${get(projectSchema, item.path)}"`
595
+ });
596
+ }
476
597
  }
477
598
 
478
599
  return errors;
@@ -739,9 +860,14 @@ export function formatError(error) {
739
860
  path
740
861
  };
741
862
  }
863
+
864
+ function isValidateReferencesContext(context) {
865
+ return Boolean(context.resolveLayer);
866
+ }
867
+
742
868
  const ajv = createAjv();
743
869
 
744
- function validateStructure(schemaVersion, schema, ref, options) {
870
+ function validateStructure(schemaVersion, context, schema, ref) {
745
871
  var _coerce, _ajv$errors;
746
872
 
747
873
  const versionStr = (_coerce = coerce(schemaVersion)) === null || _coerce === void 0 ? void 0 : _coerce.format();
@@ -755,7 +881,9 @@ function validateStructure(schemaVersion, schema, ref, options) {
755
881
 
756
882
  ajv.validate(`https://schema.takeshape.io/project-schema/v${versionStr}#${ref ?? ''}`, schema);
757
883
  let errors = ((_ajv$errors = ajv.errors) === null || _ajv$errors === void 0 ? void 0 : _ajv$errors.map(formatError)) ?? [];
758
- const suppressErrorPaths = options === null || options === void 0 ? void 0 : options.suppressErrorPaths;
884
+ const {
885
+ suppressErrorPaths
886
+ } = context;
759
887
 
760
888
  if (errors.length && suppressErrorPaths) {
761
889
  errors = errors.filter(error => {
@@ -778,20 +906,10 @@ function validateStructure(schemaVersion, schema, ref, options) {
778
906
  };
779
907
  }
780
908
 
781
- function validateV3X(version, obj, options) {
782
- const structuralValidation = validateStructure(version, obj, undefined, options);
783
-
784
- if (!structuralValidation.valid) {
785
- return structuralValidation;
786
- }
787
-
788
- const schema = obj;
789
- let errors = [];
790
- const namespaceShapes = getAllNamespaceShapes(schema);
791
- const additionalShapeNames = (options !== null && options !== void 0 && options.additionalShapeNames ? builtInShapeNames.concat(options.additionalShapeNames) : builtInShapeNames).concat(namespaceShapes);
792
- const additionalModelShapeIds = options !== null && options !== void 0 && options.additionalModelShapeIds ? builtInModelShapeIds.concat(options.additionalModelShapeIds) : builtInModelShapeIds;
793
- errors = errors.concat(checkShapeNames(schema.shapes)).concat(checkShapeIds(schema.shapes)).concat(validateWorkflowsV3(schema)).concat(validateQueryConfigs(schema, additionalShapeNames)).concat(validateRefs(schema, additionalShapeNames)).concat(validateDirectives(schema, additionalModelShapeIds)).concat(validateLocales(schema)).concat(checkWorkflowStepNames(schema.workflows)).concat(checkWorkflowStepKeys(schema.workflows)).concat(validateOneOfs(schema)).concat(validateIndexedShapes(schema)).concat(validateInterfaces(schema)).concat(validateInterfaceImplementations(schema));
794
- const suppressErrorPaths = options === null || options === void 0 ? void 0 : options.suppressErrorPaths;
909
+ function formatValidationResult(context, errors, schema) {
910
+ const {
911
+ suppressErrorPaths
912
+ } = context;
795
913
 
796
914
  if (suppressErrorPaths) {
797
915
  errors = errors.filter(error => {
@@ -810,132 +928,140 @@ function validateV3X(version, obj, options) {
810
928
  };
811
929
  }
812
930
 
813
- function validateV4X(version, obj, options) {
814
- const structuralValidation = validateStructure(version, obj);
815
-
816
- if (!structuralValidation.valid) {
817
- return structuralValidation;
818
- }
819
-
820
- const schema = obj;
821
-
822
- for (const [index, layerConfig] of Object.entries(schema.layers)) {
823
- var _options$resolveLayer;
824
-
825
- const layerId = typeof layerConfig === 'string' ? layerConfig : layerConfig.id;
826
- const layer = options === null || options === void 0 ? void 0 : (_options$resolveLayer = options.resolveLayer) === null || _options$resolveLayer === void 0 ? void 0 : _options$resolveLayer.call(options, layerId);
827
-
828
- if (layer) {
829
- const results = validateStructure(version, layer, '/definitions/layerSchema');
830
-
831
- if (!results.valid) {
832
- return results;
833
- }
834
- } else {
835
- return {
836
- valid: false,
837
- schema: undefined,
838
- errors: [{
839
- path: ['layers', index],
840
- type: 'undefined',
841
- message: `Layer with id is undefined`
842
- }]
843
- };
844
- }
845
- }
846
-
847
- return {
848
- valid: true,
849
- schema,
850
- errors: undefined
851
- };
852
- }
853
-
854
931
  const validators = [{
855
932
  range: '^1.0.0',
856
933
 
857
- validate(schemaVersion, obj) {
858
- const structuralValidation = validateStructure(schemaVersion, obj);
859
-
860
- if (!structuralValidation.valid) {
861
- return structuralValidation;
862
- }
863
-
934
+ validateSyntax(schemaVersion, context, obj) {
864
935
  const schemaV1 = obj;
865
936
  let errors = [];
866
937
  errors = errors.concat(checkContentTypeNames(schemaV1.contentTypes)).concat(validateWorkflowsV1(schemaV1)).concat(checkWorkflowStepNames(schemaV1.workflows)).concat(checkWorkflowStepKeys(schemaV1.workflows));
867
- return errors.length ? {
868
- valid: false,
869
- schema: undefined,
870
- errors
871
- } : {
872
- valid: true,
873
- schema: schemaV1,
874
- errors: undefined
875
- };
938
+ return formatValidationResult(context, errors, schemaV1);
876
939
  }
877
940
 
878
941
  }, {
879
942
  range: '^3.0.0',
880
- validate: validateV3X
881
- }, {
882
- range: '^4.0.0',
883
- validate: validateV4X
943
+
944
+ validateSyntax(schemaVersion, context, obj) {
945
+ const schema = obj;
946
+ let errors = [];
947
+ errors = errors.concat(checkShapeNames(schema.shapes)).concat(checkShapeIds(schema.shapes)).concat(validateWorkflowsV3(schema)).concat(validateLocalQueryConfigs(schema)).concat(validateLocalRefs(schema)).concat(validateDirectives(schema)).concat(validateLocales(schema)).concat(checkWorkflowStepNames(schema.workflows)).concat(checkWorkflowStepKeys(schema.workflows)).concat(validateOneOfs(schema)).concat(validateIndexedShapes(schema)).concat(validateInterfaces(schema)).concat(validateInterfaceImplementations(schema));
948
+ return formatValidationResult(context, errors, schema);
949
+ },
950
+
951
+ async validateReferences(schemaVersion, context, obj) {
952
+ const schema = obj;
953
+ const errors = flatten(await Promise.all([validateRefs(context, schema), validateQueryConfigs(context, schema)]));
954
+ return formatValidationResult(context, errors, schema);
955
+ }
956
+
884
957
  }];
885
958
 
886
959
  function findValidator(schemaVersion) {
887
- const normalizedSchemaVersion = schemaVersion ?? '1';
888
- const schemaSemVer = coerce(normalizedSchemaVersion);
960
+ const schemaSemVer = coerce(schemaVersion);
889
961
 
890
962
  if (schemaSemVer) {
891
963
  const validator = validators.find(v => satisfies(schemaSemVer, v.range));
892
964
 
893
965
  if (validator) {
894
- return validator.validate.bind(null, normalizedSchemaVersion);
966
+ const {
967
+ validateSyntax,
968
+ validateReferences
969
+ } = validator;
970
+ return {
971
+ validateSyntax: validateSyntax.bind(null, schemaVersion),
972
+ ...(validateReferences ? {
973
+ validateReferences: validateReferences.bind(null, schemaVersion)
974
+ } : {})
975
+ };
895
976
  }
896
977
  }
897
978
  }
979
+
980
+ const schemaUndefinedResult = {
981
+ valid: false,
982
+ schema: undefined,
983
+ errors: [{
984
+ path: [],
985
+ type: 'undefined',
986
+ message: 'Schema is undefined'
987
+ }]
988
+ };
989
+ const invalidVersionResult = {
990
+ valid: false,
991
+ schema: undefined,
992
+ errors: [{
993
+ path: [],
994
+ type: 'invalidVersion',
995
+ message: 'Unknown schema version'
996
+ }]
997
+ };
998
+
999
+ function normalizeSchemaVersion(schema) {
1000
+ return schema.schemaVersion ?? '1';
1001
+ }
1002
+
1003
+ export function validateSchemaSyntax(obj, options = {}) {
1004
+ if (isUndefined(obj)) {
1005
+ return schemaUndefinedResult;
1006
+ }
1007
+
1008
+ const schema = obj;
1009
+ const schemaVersion = normalizeSchemaVersion(schema);
1010
+ const validator = findValidator(schemaVersion);
1011
+
1012
+ if (!validator) {
1013
+ return invalidVersionResult;
1014
+ }
1015
+
1016
+ const structuralValidation = validateStructure(schemaVersion, options, obj);
1017
+
1018
+ if (!structuralValidation.valid) {
1019
+ return structuralValidation;
1020
+ }
1021
+
1022
+ return validator.validateSyntax(options, schema);
1023
+ }
898
1024
  /**
899
1025
  * Validates a schema using a matching validation based on the `schemaVersion` property.
900
1026
  */
901
1027
 
902
-
903
- export function validateSchema(obj, options) {
1028
+ export async function validateSchema(context, obj) {
904
1029
  if (isUndefined(obj)) {
905
- return {
906
- valid: false,
907
- schema: undefined,
908
- errors: [{
909
- path: [],
910
- type: 'undefined',
911
- message: 'Schema is undefined'
912
- }]
913
- };
1030
+ return schemaUndefinedResult;
914
1031
  }
915
1032
 
1033
+ const contextWithDefaults = {
1034
+ allowDisconnectedLayers: true,
1035
+ ...context
1036
+ };
916
1037
  const schema = obj;
917
- const validator = findValidator(schema.schemaVersion);
1038
+ const schemaVersion = normalizeSchemaVersion(schema);
1039
+ const validator = findValidator(schemaVersion);
918
1040
 
919
1041
  if (!validator) {
920
- return {
921
- valid: false,
922
- schema: undefined,
923
- errors: [{
924
- path: [],
925
- type: 'invalidVersion',
926
- message: 'Unknown schema version'
927
- }]
928
- };
1042
+ return invalidVersionResult;
1043
+ }
1044
+
1045
+ const structuralValidation = validateStructure(schemaVersion, contextWithDefaults, obj);
1046
+
1047
+ if (!structuralValidation.valid) {
1048
+ return structuralValidation;
1049
+ }
1050
+
1051
+ const syntaxValidation = validator.validateSyntax(contextWithDefaults, schema);
1052
+
1053
+ if (!syntaxValidation.valid || !validator.validateReferences || !isValidateReferencesContext(contextWithDefaults)) {
1054
+ return syntaxValidation;
929
1055
  }
930
1056
 
931
- return validator(schema, options);
1057
+ return validator.validateReferences(contextWithDefaults, obj);
932
1058
  }
933
- export function ensureValidLatestSchema(obj) {
1059
+ export function ensureValidLatestSchemaSyntax(obj) {
934
1060
  const {
935
1061
  valid,
936
1062
  schema,
937
1063
  errors
938
- } = validateSchema(obj);
1064
+ } = validateSchemaSyntax(obj);
939
1065
 
940
1066
  if (!valid && errors) {
941
1067
  throw new Error(`Invalid Schema "${errors[0].path.join(',')}": "${errors[0].message}"`);
@@ -1027,7 +1153,7 @@ export function ensureValidRoleImport(maybeRoles) {
1027
1153
  * Only use when validating an imported schema! ignore fields optional when importing
1028
1154
  */
1029
1155
  export function validateProjectSchemaImport(maybeSchema) {
1030
- return validateSchema(maybeSchema, {
1156
+ return validateSchemaSyntax(maybeSchema, {
1031
1157
  suppressErrorPaths: [...projectSchemaImportOptionalProps, ...legacyProjectSchemaImportOptionalProps]
1032
1158
  });
1033
1159
  }