@squiz/dx-json-schema-lib 1.12.0-alpha.8 → 1.12.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.npm/_logs/2023-02-27T04_50_27_819Z-debug-0.log +39 -0
  2. package/jest.config.ts +0 -6
  3. package/lib/JsonValidationService.d.ts +35 -0
  4. package/lib/JsonValidationService.js +127 -1
  5. package/lib/JsonValidationService.js.map +1 -1
  6. package/lib/JsonValidationService.spec.js +288 -0
  7. package/lib/JsonValidationService.spec.js.map +1 -1
  8. package/lib/errors/JsonResolutionError.d.ts +5 -0
  9. package/lib/errors/JsonResolutionError.js +12 -0
  10. package/lib/errors/JsonResolutionError.js.map +1 -0
  11. package/lib/index.d.ts +2 -0
  12. package/lib/index.js +2 -0
  13. package/lib/index.js.map +1 -1
  14. package/lib/jsonTypeResolution/arbitraryTypeResolution.d.ts +61 -0
  15. package/lib/jsonTypeResolution/arbitraryTypeResolution.js +61 -0
  16. package/lib/jsonTypeResolution/arbitraryTypeResolution.js.map +1 -0
  17. package/lib/jsonTypeResolution/arbitraryTypeResolution.spec.d.ts +1 -0
  18. package/lib/jsonTypeResolution/arbitraryTypeResolution.spec.js +100 -0
  19. package/lib/jsonTypeResolution/arbitraryTypeResolution.spec.js.map +1 -0
  20. package/lib/jsonTypeResolution/index.d.ts +76 -0
  21. package/lib/jsonTypeResolution/index.js +35 -0
  22. package/lib/jsonTypeResolution/index.js.map +1 -0
  23. package/lib/jsonTypeResolution/primitiveTypes.d.ts +10 -0
  24. package/lib/jsonTypeResolution/primitiveTypes.js +27 -0
  25. package/lib/jsonTypeResolution/primitiveTypes.js.map +1 -0
  26. package/lib/jsonTypeResolution/resolvableTypes.d.ts +12 -0
  27. package/lib/jsonTypeResolution/resolvableTypes.js +30 -0
  28. package/lib/jsonTypeResolution/resolvableTypes.js.map +1 -0
  29. package/lib/manifest/v1/v1.json +4 -3
  30. package/lib/manifest/v1/v1.spec.js +58 -0
  31. package/lib/manifest/v1/v1.spec.js.map +1 -1
  32. package/package.json +5 -4
  33. package/src/JsonValidationService.spec.ts +417 -1
  34. package/src/JsonValidationService.ts +154 -1
  35. package/src/errors/JsonResolutionError.ts +5 -0
  36. package/src/index.ts +3 -0
  37. package/src/jsonTypeResolution/arbitraryTypeResolution.spec.ts +134 -0
  38. package/src/jsonTypeResolution/arbitraryTypeResolution.ts +100 -0
  39. package/src/jsonTypeResolution/index.ts +16 -0
  40. package/src/jsonTypeResolution/primitiveTypes.ts +32 -0
  41. package/src/jsonTypeResolution/resolvableTypes.ts +37 -0
  42. package/src/manifest/v1/v1.json +4 -3
  43. package/src/manifest/v1/v1.spec.ts +96 -0
  44. package/tsconfig.tsbuildinfo +1 -1
@@ -1,3 +1,5 @@
1
+ import JSONQuery, { Input } from '@sagold/json-query';
2
+
1
3
  import DxComponentInputSchema from './manifest/v1/DxComponentInputSchema.json';
2
4
  import DxComponentIcons from './manifest/v1/DxComponentIcons.json';
3
5
  import DxContentMetaSchema from './manifest/v1/DxContentMetaSchema.json';
@@ -7,10 +9,12 @@ import FormattedText from './formatted-text/v1/formattedText.json';
7
9
 
8
10
  import v1 from './manifest/v1/v1.json';
9
11
  import { SchemaValidationError } from './errors/SchemaValidationError';
10
- import { Draft07, JSONError, JSONSchema, Draft, DraftConfig } from 'json-schema-library';
12
+ import { Draft07, JSONError, JSONSchema, Draft, DraftConfig, isJSONError } from 'json-schema-library';
11
13
 
12
14
  import { draft07Config } from 'json-schema-library';
13
15
  import { MANIFEST_MODELS } from '.';
16
+ import { AnyPrimitiveType, AnyResolvableType, TypeResolver } from './jsonTypeResolution/arbitraryTypeResolution';
17
+ import { JsonResolutionError } from './errors/JsonResolutionError';
14
18
 
15
19
  const defaultConfig: DraftConfig = {
16
20
  ...draft07Config,
@@ -71,6 +75,7 @@ export const ComponentInputSchema = new Draft(
71
75
  if (!resolvedSchema) {
72
76
  return resolvedSchema;
73
77
  }
78
+
74
79
  if (resolvedSchema.type === 'FormattedText') {
75
80
  return FTSchema.rootSchema;
76
81
  } else if (Array.isArray(resolvedSchema.type) && resolvedSchema.type.includes('FormattedText')) {
@@ -88,6 +93,27 @@ export const ComponentInputSchema = new Draft(
88
93
  );
89
94
  ComponentInputSchema.addRemoteSchema('DxComponentInputSchema.json/DxContentMetaSchema.json', DxContentMetaSchema);
90
95
 
96
+ export const RenderInputSchema = new Draft({
97
+ ...defaultConfig,
98
+ resolveRef(schema, rootSchema) {
99
+ const resolvedSchema = draft07Config.resolveRef(schema, rootSchema) as MANIFEST_MODELS.v1.CoreSchemaMetaSchema;
100
+ if (!resolvedSchema) {
101
+ return resolvedSchema;
102
+ }
103
+
104
+ if (resolvedSchema.type === 'FormattedText') {
105
+ return { type: 'string' };
106
+ } else if (Array.isArray(resolvedSchema.type) && resolvedSchema.type.includes('FormattedText')) {
107
+ return {
108
+ ...schema,
109
+ type: resolvedSchema.type.filter((t) => t !== 'FormattedText').concat('string'),
110
+ };
111
+ } else {
112
+ return resolvedSchema;
113
+ }
114
+ },
115
+ });
116
+
91
117
  const v1Schema = new Draft07(v1, defaultConfig);
92
118
 
93
119
  v1Schema.addRemoteSchema('DxComponentInputSchema.json/DxContentMetaSchema.json', DxContentMetaSchema);
@@ -96,6 +122,127 @@ v1Schema.addRemoteSchema('/DxComponentIcons.json', DxComponentIcons);
96
122
  v1Schema.addRemoteSchema('http://json-schema.org/draft-07/schema', Draft07Schema);
97
123
  v1Schema.addRemoteSchema('http://json-schema.org/draft-07/schema#', Draft07Schema);
98
124
 
125
+ export const ComponentInputMetaSchema: MetaSchemaInput = {
126
+ root: DxComponentInputSchema,
127
+ remotes: {
128
+ 'DxComponentInputSchema.json/DxContentMetaSchema.json': DxContentMetaSchema,
129
+ },
130
+ };
131
+
132
+ export const RenderInputMetaSchema: MetaSchemaInput = {
133
+ root: Draft07Schema,
134
+ };
135
+
136
+ export const ManifestV1MetaSchema: MetaSchemaInput = {
137
+ root: v1,
138
+ remotes: {
139
+ 'DxComponentInputSchema.json/DxContentMetaSchema.json': DxContentMetaSchema,
140
+ '/DxComponentInputSchema.json': DxComponentInputSchema,
141
+ '/DxComponentIcons.json': DxComponentIcons,
142
+ 'http://json-schema.org/draft-07/schema': Draft07Schema,
143
+ 'http://json-schema.org/draft-07/schema#': Draft07Schema,
144
+ },
145
+ };
146
+
147
+ interface MetaSchemaInput {
148
+ root: JSONSchema;
149
+ remotes?: Record<string, JSONSchema>;
150
+ }
151
+ /**
152
+ * A service that can be used to validate and resolve JSON against a schema.
153
+ */
154
+ export class JSONSchemaService<P extends AnyPrimitiveType, R extends AnyResolvableType> {
155
+ schema: Draft;
156
+ constructor(private typeResolver: TypeResolver<P, R>, metaSchema: MetaSchemaInput) {
157
+ this.schema = new Draft(
158
+ {
159
+ ...defaultConfig,
160
+ resolveRef: (schema, rootSchema) => this.doResolveRef(schema, rootSchema),
161
+ },
162
+ metaSchema.root,
163
+ );
164
+
165
+ for (const [key, value] of Object.entries(metaSchema.remotes || {})) {
166
+ this.schema.addRemoteSchema(key, value);
167
+ }
168
+ }
169
+
170
+ private doResolveRef(schema: JSONSchema, rootSchema: JSONSchema): JSONSchema {
171
+ const initialRef = draft07Config.resolveRef(schema, rootSchema);
172
+
173
+ if (!initialRef) return initialRef;
174
+ if (!this.typeResolver.isPrimitiveType(initialRef.type)) return initialRef;
175
+
176
+ return this.typeResolver.getValidationSchemaForPrimitive(initialRef.type);
177
+ }
178
+
179
+ /**
180
+ * Validate an input value against a specified schema
181
+ * @throws {SchemaValidationError} if the input is invalid
182
+ * @returns true if the input is valid
183
+ */
184
+ public validateInput(input: unknown, inputSchema: JSONSchema = this.schema.rootSchema): true | never {
185
+ inputSchema = this.schema.compileSchema(inputSchema);
186
+ const errors = this.schema.validate(input, inputSchema);
187
+ return this.processValidationResult(errors);
188
+ }
189
+
190
+ private processValidationResult(errors: JSONError[]): true {
191
+ if (errors.length > 0) {
192
+ throw new SchemaValidationError(errors.map((a) => a.message).join(',\n'));
193
+ }
194
+
195
+ return true;
196
+ }
197
+
198
+ /**
199
+ * Resolve an input object by replacing all resolvable shapes with their resolved values
200
+ * @param input any input object which matches the input schema
201
+ * @param inputSchema a JSONSchema which provides type information about the input object
202
+ * @returns the input object with all resolvable shapes resolved
203
+ */
204
+ public async resolveInput(input: Input, inputSchema: JSONSchema) {
205
+ const setters: Array<Promise<(input: Input) => Input>> = [];
206
+ this.schema.each(
207
+ input,
208
+ (schema, value, pointer) => {
209
+ // First we check for if value is a resolvable shape
210
+ if (!this.typeResolver.isResolvableSchema(schema)) return;
211
+ // If its a resolvable schema, it should exist in a oneOf array with other schemas
212
+ // Including a primitive schema
213
+ const allPossibleSchemas: Array<JSONSchema> = schema.oneOfSchema.oneOf;
214
+ if (isJSONError(allPossibleSchemas)) return;
215
+
216
+ const primitiveSchema = allPossibleSchemas.find((schema): schema is P =>
217
+ this.typeResolver.isPrimitiveSchema(schema),
218
+ );
219
+ if (!primitiveSchema) return;
220
+
221
+ const resolver = this.typeResolver.tryGetResolver(primitiveSchema, schema);
222
+ if (!resolver) return;
223
+ const setResolvedData = Promise.resolve()
224
+ .then(() => resolver(value))
225
+ .then((resolvedData) => (item: typeof input) => JSONQuery.set(item, pointer, resolvedData))
226
+ .catch((e) => Promise.reject(new JsonResolutionError(e, pointer, value)));
227
+ setters.push(setResolvedData);
228
+ },
229
+ inputSchema,
230
+ );
231
+
232
+ const potentialResolutionErrors = [];
233
+ for (const resolveResult of await Promise.allSettled(setters)) {
234
+ if (resolveResult.status === 'rejected') {
235
+ potentialResolutionErrors.push(resolveResult.reason);
236
+ continue;
237
+ }
238
+
239
+ input = resolveResult.value(input);
240
+ }
241
+
242
+ return input;
243
+ }
244
+ }
245
+
99
246
  export class JsonValidationService {
100
247
  validateManifest(manifest: unknown, version: 'v1') {
101
248
  switch (version) {
@@ -119,6 +266,12 @@ export class JsonValidationService {
119
266
  return this.processValidationResult(errors);
120
267
  }
121
268
 
269
+ validateRenderInput(functionInputSchema: JSONSchema, inputValue: unknown) {
270
+ const inputSchema = RenderInputSchema.compileSchema(functionInputSchema);
271
+ const errors = RenderInputSchema.validate(inputValue, inputSchema);
272
+ return this.processValidationResult(errors);
273
+ }
274
+
122
275
  private processValidationResult(errors: JSONError[]): true {
123
276
  if (errors.length > 0) {
124
277
  throw new SchemaValidationError(errors.map((a) => a.message).join(',\n'));
@@ -0,0 +1,5 @@
1
+ export class JsonResolutionError extends Error {
2
+ constructor(error: Error, public pointer: string, public value: any) {
3
+ super(`Error resolving JSON at ${pointer}: ${error.message}`);
4
+ }
5
+ }
package/src/index.ts CHANGED
@@ -9,3 +9,6 @@ export * as SUB_SCHEMAS from './manifest/v1/subSchemas';
9
9
 
10
10
  export * from './JsonValidationService';
11
11
  export * from './errors/SchemaValidationError';
12
+ export * from './errors/JsonResolutionError';
13
+
14
+ export * from './jsonTypeResolution';
@@ -0,0 +1,134 @@
1
+ import { JSONSchema } from 'json-schema-library';
2
+ import { PrimitiveType, ResolvableType, TypeResolver } from './arbitraryTypeResolution';
3
+
4
+ const defaultSchema: JSONSchema = {
5
+ type: 'object',
6
+ properties: {
7
+ myProperty: {
8
+ type: 'string',
9
+ },
10
+ },
11
+ required: ['myProperty'],
12
+ };
13
+ function primitiveTypeFixture<T extends string>(title: T, schema: JSONSchema = defaultSchema) {
14
+ return PrimitiveType({
15
+ ...schema,
16
+ title,
17
+ });
18
+ }
19
+
20
+ function resolvableTypeFixture<T extends string>(title: T, schema: JSONSchema = defaultSchema) {
21
+ return ResolvableType({
22
+ ...schema,
23
+ title,
24
+ });
25
+ }
26
+
27
+ describe('getValidationSchemaForPrimitive', () => {
28
+ it('should return only the primitive schema when no resolvers are defined', () => {
29
+ const primitiveType = primitiveTypeFixture('MyPrimitive');
30
+ const resolvableType = resolvableTypeFixture('MyResolvable');
31
+ const resolver = new TypeResolver(
32
+ {
33
+ MyPrimitive: primitiveType,
34
+ },
35
+ {
36
+ MyResolvable: resolvableType,
37
+ },
38
+ {},
39
+ );
40
+
41
+ expect(resolver.getValidationSchemaForPrimitive('MyPrimitive')).toEqual({
42
+ oneOf: [primitiveType],
43
+ });
44
+ });
45
+
46
+ it('should return the primitive schema and the resolvable schema when a resolver is defined', () => {
47
+ const primitiveType = primitiveTypeFixture('MyPrimitive');
48
+ const resolvableType = resolvableTypeFixture('MyResolvable');
49
+ const resolver = new TypeResolver(
50
+ {
51
+ MyPrimitive: primitiveType,
52
+ },
53
+ {
54
+ MyResolvable: resolvableType,
55
+ },
56
+ {
57
+ MyPrimitive: {
58
+ MyResolvable: () => null,
59
+ },
60
+ },
61
+ );
62
+
63
+ expect(resolver.getValidationSchemaForPrimitive('MyPrimitive')).toEqual({
64
+ oneOf: [primitiveType, resolvableType],
65
+ });
66
+ });
67
+
68
+ it('should return the primitive schema and the resolvable schema when a resolver is defined for a different primitive', () => {
69
+ const primitiveType = primitiveTypeFixture('MyPrimitive');
70
+ const resolvableType = resolvableTypeFixture('MyResolvable');
71
+ const resolver = new TypeResolver(
72
+ {
73
+ MyPrimitive: primitiveType,
74
+ MyOtherPrimitive: primitiveTypeFixture('MyOtherPrimitive'),
75
+ },
76
+ {
77
+ MyResolvable: resolvableType,
78
+ },
79
+ {
80
+ MyOtherPrimitive: {
81
+ MyResolvable: () => null,
82
+ },
83
+ },
84
+ );
85
+
86
+ expect(resolver.getValidationSchemaForPrimitive('MyPrimitive')).toEqual({
87
+ oneOf: [primitiveType],
88
+ });
89
+ });
90
+
91
+ it('should error when resolver map contains a key not listed in resolver schemas', () => {
92
+ const primitiveType = primitiveTypeFixture('MyPrimitive');
93
+ const resolvableType = resolvableTypeFixture('MyResolvable');
94
+ expect(
95
+ () =>
96
+ new TypeResolver<typeof primitiveType, typeof resolvableType>(
97
+ {
98
+ MyPrimitive: primitiveType,
99
+ },
100
+ {
101
+ MyResolvable: resolvableType,
102
+ },
103
+ {
104
+ MyPrimitive: {
105
+ // @ts-expect-error - this is not a valid resolvable type
106
+ MyOtherResolvable: () => null,
107
+ },
108
+ },
109
+ ),
110
+ ).toThrowError();
111
+ });
112
+
113
+ it('should error when resolver map contains a key not listed in primitive schemas', () => {
114
+ const primitiveType = primitiveTypeFixture('MyPrimitive');
115
+ const resolvableType = resolvableTypeFixture('MyResolvable');
116
+ expect(
117
+ () =>
118
+ new TypeResolver<typeof primitiveType, typeof resolvableType>(
119
+ {
120
+ MyPrimitive: primitiveType,
121
+ },
122
+ {
123
+ MyResolvable: resolvableType,
124
+ },
125
+ {
126
+ // @ts-expect-error - this is not a valid primitive type
127
+ MyOtherPrimitive: {
128
+ MyResolvable: () => null,
129
+ },
130
+ },
131
+ ),
132
+ ).toThrowError();
133
+ });
134
+ });
@@ -0,0 +1,100 @@
1
+ import type { JSONSchema } from 'json-schema-library';
2
+ import * as t from 'ts-brand';
3
+
4
+ type MaybePromise<T> = T | Promise<T>;
5
+
6
+ type JsonResolutionSchema<TITLE extends string> = JSONSchema & { title: TITLE };
7
+ /**
8
+ * This type allows the TypeScript type to be encoded onto the JSON schema object
9
+ */
10
+ type SchemaWithShape<TITLE extends string, SHAPE> = JsonResolutionSchema<TITLE> & { __shape__: SHAPE };
11
+
12
+ /**
13
+ * A JSON schema which represents a primitive type which can be a resolve target
14
+ *
15
+ * The brand ensures that TypeScript can differentiate between a primitive type and a resolvable type
16
+ */
17
+ export type PrimitiveType<TITLE extends string, SHAPE> = t.Brand<SchemaWithShape<TITLE, SHAPE>, 'primitive'>;
18
+ export function PrimitiveType<SHAPE, TITLE extends string>(
19
+ jsonSchema: JsonResolutionSchema<TITLE>,
20
+ ): PrimitiveType<TITLE, SHAPE> {
21
+ return jsonSchema as PrimitiveType<TITLE, SHAPE>;
22
+ }
23
+ export type AnyPrimitiveType = PrimitiveType<string, any>;
24
+
25
+ /**
26
+ * A JSON schema which represents a type which can be resolved into a primitive type
27
+ *
28
+ * The brand ensures that TypeScript can differentiate between a primitive type and a resolvable type
29
+ */
30
+ export type ResolvableType<TITLE extends string, SHAPE> = t.Brand<SchemaWithShape<TITLE, SHAPE>, 'resolvable'>;
31
+ export function ResolvableType<SHAPE, TITLE extends string>(
32
+ jsonSchema: JsonResolutionSchema<TITLE>,
33
+ ): ResolvableType<TITLE, SHAPE> {
34
+ return jsonSchema as ResolvableType<TITLE, SHAPE>;
35
+ }
36
+ export type AnyResolvableType = ResolvableType<string, any>;
37
+
38
+ type Resolver<INPUT, OUTPUT> = (input: INPUT) => MaybePromise<OUTPUT>;
39
+
40
+ /**
41
+ * A JSON Type Resolver class which stores the primitive and resolvable JSON Schema types and their resolvers
42
+ *
43
+ * No serious logic is required here. The class should only provide data access methods and type safety
44
+ */
45
+ export class TypeResolver<P extends AnyPrimitiveType, R extends AnyResolvableType> {
46
+ constructor(
47
+ private primitives: { [K in P as P['title']]: K },
48
+ private resolvables: { [K in R as R['title']]: K },
49
+ public resolvers: {
50
+ [PT in P as PT['title']]?: {
51
+ [RT in R as RT['title']]?: Resolver<RT['__shape__'], PT['__shape__']>;
52
+ };
53
+ },
54
+ ) {
55
+ for (const [primitiveKey, primitiveResolvers] of Object.entries(resolvers) as [string, Record<string, any>][]) {
56
+ if (!(primitiveKey in primitives)) {
57
+ throw new Error('Resolver keys must match a primitive schema');
58
+ }
59
+ if (!Object.keys(primitiveResolvers).every((k) => k in resolvables)) {
60
+ throw new Error('Primitive resolvers keys must match a resolvable schema');
61
+ }
62
+ }
63
+ }
64
+
65
+ isPrimitiveType(type: string): type is P['title'] {
66
+ return type in this.primitives;
67
+ }
68
+
69
+ isPrimitiveSchema(schema: JSONSchema): schema is P {
70
+ return this.isPrimitiveType(schema.title);
71
+ }
72
+
73
+ isResolvableSchema(schema: JSONSchema): schema is R {
74
+ return schema.title in this.resolvables;
75
+ }
76
+
77
+ getValidationSchemaForPrimitive(type: keyof typeof this.primitives) {
78
+ const primitiveSchema = this.primitives[type];
79
+ const validSchemas = [primitiveSchema, ...this.fetchResolvableSchemasForPrimitive(type)];
80
+
81
+ return {
82
+ oneOf: validSchemas,
83
+ };
84
+ }
85
+
86
+ private *fetchResolvableSchemasForPrimitive(type: keyof typeof this.primitives) {
87
+ for (const resolverKey in this.resolvers[type]) {
88
+ yield this.resolvables[resolverKey];
89
+ }
90
+ }
91
+
92
+ tryGetResolver<PS extends P, RS extends R>(
93
+ primitiveSchema: PS,
94
+ resolvableSchema: RS,
95
+ ): Resolver<RS['__shape__'], PS['__shape__']> | undefined {
96
+ if (!(primitiveSchema.title in this.resolvers)) return;
97
+ // Sometimes typescript can be insanely annoying
98
+ return (this.resolvers[primitiveSchema.title as keyof typeof this.resolvers] as any)?.[resolvableSchema.title];
99
+ }
100
+ }
@@ -0,0 +1,16 @@
1
+ import * as PRIMITIVES from './primitiveTypes';
2
+ import * as RESOLVABLES from './resolvableTypes';
3
+
4
+ export type AllPrimitiveTypes = (typeof PRIMITIVES)[keyof typeof PRIMITIVES];
5
+ export const PrimitiveSchemas = Object.fromEntries(
6
+ Object.entries(PRIMITIVES).map(([_key, typeSchema]) => [typeSchema.title, typeSchema]),
7
+ ) as { [P in AllPrimitiveTypes as P['title']]: P };
8
+ export { PRIMITIVES };
9
+
10
+ export type AllResolvableTypes = (typeof RESOLVABLES)[keyof typeof RESOLVABLES];
11
+ export const ResolvableSchemas = Object.fromEntries(
12
+ Object.entries(RESOLVABLES).map(([_key, typeSchema]) => [typeSchema.title, typeSchema]),
13
+ ) as { [P in AllResolvableTypes as P['title']]: P };
14
+ export { RESOLVABLES };
15
+
16
+ export { TypeResolver as JsonTypeResolver } from './arbitraryTypeResolution';
@@ -0,0 +1,32 @@
1
+ /* eslint-disable @typescript-eslint/no-namespace */
2
+ import { FORMATTED_TEXT_SCHEMAS } from '..';
3
+ import { BaseFormattedNodes } from '../formatted-text/v1/formattedText';
4
+ import { PrimitiveType } from './arbitraryTypeResolution';
5
+
6
+ export interface SquizImageShape {
7
+ src: string;
8
+ alt: string;
9
+ }
10
+ export const SquizImageType = PrimitiveType<SquizImageShape, 'SquizImage'>({
11
+ title: 'SquizImage',
12
+ type: 'object',
13
+ properties: {
14
+ src: {
15
+ type: 'string',
16
+ },
17
+ alt: {
18
+ type: 'string',
19
+ },
20
+ },
21
+ required: ['src', 'alt'],
22
+ });
23
+ export type SquizImageType = typeof SquizImageType;
24
+
25
+ export const PrimitiveFormattedTextType = PrimitiveType<BaseFormattedNodes[], 'FormattedText'>({
26
+ ...FORMATTED_TEXT_SCHEMAS.v1,
27
+ items: {
28
+ $ref: '#/definitions/BaseFormattedNodes',
29
+ },
30
+ title: 'FormattedText',
31
+ });
32
+ export type PrimitiveFormattedTextType = typeof PrimitiveFormattedTextType;
@@ -0,0 +1,37 @@
1
+ import { FORMATTED_TEXT_SCHEMAS } from '..';
2
+ import { BaseFormattedNodes, FormattedText } from '../formatted-text/v1/formattedText';
3
+ import { ResolvableType } from './arbitraryTypeResolution';
4
+
5
+ export interface MatrixImageShape {
6
+ assetId: string;
7
+ matrixIdentifier: string;
8
+ }
9
+ export const MatrixImageType = ResolvableType<MatrixImageShape, 'MatrixImage'>({
10
+ title: 'MatrixImage',
11
+ type: 'object',
12
+ properties: {
13
+ assetId: {
14
+ type: 'string',
15
+ },
16
+ matrixIdentifier: {
17
+ type: 'string',
18
+ },
19
+ },
20
+ required: ['assetId', 'matrixIdentifier'],
21
+ });
22
+ export type MatrixImageType = typeof MatrixImageType;
23
+
24
+ export const HigherOrderFormattedTextType = ResolvableType<FormattedText, 'HigherOrderFormattedText'>({
25
+ ...FORMATTED_TEXT_SCHEMAS.v1,
26
+ title: 'HigherOrderFormattedText',
27
+ });
28
+ export type HigherOrderFormattedTextType = typeof HigherOrderFormattedTextType;
29
+
30
+ export const LowerOrderFormattedTextType = ResolvableType<BaseFormattedNodes[], 'LowerOrderFormattedText'>({
31
+ ...FORMATTED_TEXT_SCHEMAS.v1,
32
+ items: {
33
+ $ref: '#/definitions/BaseFormattedNodes',
34
+ },
35
+ title: 'LowerOrderFormattedText',
36
+ });
37
+ export type LowerOrderFormattedTextType = typeof LowerOrderFormattedTextType;
@@ -35,7 +35,7 @@
35
35
  "properties": {
36
36
  "name": {
37
37
  "type": "string",
38
- "$ref": "#/definitions/name-pattern",
38
+ "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
39
39
  "description": "Environmental variable name"
40
40
  },
41
41
  "required": {
@@ -279,7 +279,8 @@
279
279
  "type": "object",
280
280
  "description": "A map of previews which provide configuration to preview the component in isolation",
281
281
  "propertyNames": {
282
- "$ref": "#/definitions/name-pattern"
282
+ "type": "string",
283
+ "pattern": "^[a-zA-Z0-9_\\-]+$"
283
284
  },
284
285
  "minProperties": 1,
285
286
  "additionalProperties": {
@@ -373,7 +374,7 @@
373
374
  "definitions": {
374
375
  "name-pattern": {
375
376
  "type": "string",
376
- "pattern": "^[a-zA-Z0-9_\\-]+$"
377
+ "pattern": "^[a-z][a-z0-9_\\-]+$"
377
378
  }
378
379
  }
379
380
  }
@@ -3,6 +3,8 @@ import { resolve } from 'path';
3
3
  import { SchemaValidationError } from '../../errors/SchemaValidationError';
4
4
  import { JsonValidationService } from '../../JsonValidationService';
5
5
 
6
+ const NAME_PATTERN = '^[a-z][a-z0-9_\\-]+$';
7
+
6
8
  async function fetchTestManifest(filename: string) {
7
9
  const contents = await readFile(resolve(__dirname, '__test__', 'schemas', filename), {
8
10
  encoding: 'utf-8',
@@ -70,4 +72,98 @@ describe('manifest/v1', () => {
70
72
  'failed validation: Expected value at `#/functions/0/input/type` to be `object`, but value given is `string`',
71
73
  );
72
74
  });
75
+
76
+ describe.each(['_my-name', '-my-name', 'MyName', 'myName', '0my-name'])(
77
+ 'fails name-pattern validation for %s',
78
+ (propertyValue) => {
79
+ it.each(['namespace', 'name'])(`fails validation for manifests with %s of %s`, async (propertyName) => {
80
+ const manifest = await fetchTestManifest('validComponent.json');
81
+
82
+ expectToThrowErrorMatchingTypeAndMessage(
83
+ () => {
84
+ validationService.validateManifest(
85
+ {
86
+ ...manifest,
87
+ [propertyName]: propertyValue,
88
+ },
89
+ 'v1',
90
+ );
91
+ },
92
+ SchemaValidationError,
93
+ `failed validation: Value in \`#/${propertyName}\` should match \`${NAME_PATTERN}\`, but received \`${propertyValue}\``,
94
+ );
95
+ });
96
+
97
+ it('fails validation for manifests with function names of %s', async () => {
98
+ const manifest = await fetchTestManifest('validComponent.json');
99
+
100
+ expectToThrowErrorMatchingTypeAndMessage(
101
+ () => {
102
+ validationService.validateManifest(
103
+ {
104
+ ...manifest,
105
+ functions: [
106
+ {
107
+ name: propertyValue,
108
+ entry: 'main.js',
109
+ input: {},
110
+ output: {
111
+ responseType: 'html',
112
+ },
113
+ },
114
+ ],
115
+ },
116
+ 'v1',
117
+ );
118
+ },
119
+ SchemaValidationError,
120
+ `failed validation: Value in \`#/functions/0/name\` should match \`${NAME_PATTERN}\`, but received \`${propertyValue}\``,
121
+ );
122
+ });
123
+ },
124
+ );
125
+
126
+ it('should allow uppercase letters in property names withe previews', async () => {
127
+ const manifest = await fetchTestManifest('validComponent.json');
128
+
129
+ expect(
130
+ validationService.validateManifest(
131
+ {
132
+ ...manifest,
133
+ previews: {
134
+ ValidPreview: {
135
+ functionData: {
136
+ main: {},
137
+ },
138
+ },
139
+ },
140
+ },
141
+ 'v1',
142
+ ),
143
+ ).toEqual(true);
144
+ });
145
+
146
+ it('errors for non-alphanumeric characters in preview keys', async () => {
147
+ const manifest = await fetchTestManifest('validComponent.json');
148
+
149
+ expectToThrowErrorMatchingTypeAndMessage(
150
+ () => {
151
+ validationService.validateManifest(
152
+ {
153
+ ...manifest,
154
+ previews: {
155
+ 'Bad-@@@Preview': {
156
+ functionData: {
157
+ main: {},
158
+ },
159
+ },
160
+ },
161
+ },
162
+ 'v1',
163
+ );
164
+ },
165
+ SchemaValidationError,
166
+ 'failed validation: Invalid property name `Bad-@@@Preview` at `#/previews`',
167
+ );
168
+ });
73
169
  });