@squiz/dx-json-schema-lib 1.12.0-alpha.9 → 1.12.1-alpha.0
Sign up to get free protection for your applications and to get access to all the features.
- package/.npm/_logs/2023-02-27T04_50_27_819Z-debug-0.log +39 -0
- package/jest.config.ts +0 -6
- package/lib/JsonValidationService.d.ts +35 -0
- package/lib/JsonValidationService.js +127 -1
- package/lib/JsonValidationService.js.map +1 -1
- package/lib/JsonValidationService.spec.js +288 -0
- package/lib/JsonValidationService.spec.js.map +1 -1
- package/lib/errors/JsonResolutionError.d.ts +5 -0
- package/lib/errors/JsonResolutionError.js +12 -0
- package/lib/errors/JsonResolutionError.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/jsonTypeResolution/arbitraryTypeResolution.d.ts +61 -0
- package/lib/jsonTypeResolution/arbitraryTypeResolution.js +61 -0
- package/lib/jsonTypeResolution/arbitraryTypeResolution.js.map +1 -0
- package/lib/jsonTypeResolution/arbitraryTypeResolution.spec.d.ts +1 -0
- package/lib/jsonTypeResolution/arbitraryTypeResolution.spec.js +100 -0
- package/lib/jsonTypeResolution/arbitraryTypeResolution.spec.js.map +1 -0
- package/lib/jsonTypeResolution/index.d.ts +76 -0
- package/lib/jsonTypeResolution/index.js +35 -0
- package/lib/jsonTypeResolution/index.js.map +1 -0
- package/lib/jsonTypeResolution/primitiveTypes.d.ts +10 -0
- package/lib/jsonTypeResolution/primitiveTypes.js +27 -0
- package/lib/jsonTypeResolution/primitiveTypes.js.map +1 -0
- package/lib/jsonTypeResolution/resolvableTypes.d.ts +12 -0
- package/lib/jsonTypeResolution/resolvableTypes.js +30 -0
- package/lib/jsonTypeResolution/resolvableTypes.js.map +1 -0
- package/lib/manifest/v1/v1.json +4 -3
- package/lib/manifest/v1/v1.spec.js +58 -0
- package/lib/manifest/v1/v1.spec.js.map +1 -1
- package/package.json +5 -4
- package/src/JsonValidationService.spec.ts +417 -1
- package/src/JsonValidationService.ts +154 -1
- package/src/errors/JsonResolutionError.ts +5 -0
- package/src/index.ts +3 -0
- package/src/jsonTypeResolution/arbitraryTypeResolution.spec.ts +134 -0
- package/src/jsonTypeResolution/arbitraryTypeResolution.ts +100 -0
- package/src/jsonTypeResolution/index.ts +16 -0
- package/src/jsonTypeResolution/primitiveTypes.ts +32 -0
- package/src/jsonTypeResolution/resolvableTypes.ts +37 -0
- package/src/manifest/v1/v1.json +4 -3
- package/src/manifest/v1/v1.spec.ts +96 -0
- 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'));
|
package/src/index.ts
CHANGED
@@ -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;
|
package/src/manifest/v1/v1.json
CHANGED
@@ -35,7 +35,7 @@
|
|
35
35
|
"properties": {
|
36
36
|
"name": {
|
37
37
|
"type": "string",
|
38
|
-
"
|
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
|
-
"
|
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-
|
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
|
});
|