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

Sign up to get free protection for your applications and to get access to all the features.
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
  });