@contember/schema-utils 2.1.0-beta.2 → 2.2.0-alpha.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/dist/development/index.cjs +11 -0
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +11 -0
- package/dist/development/index.js.map +1 -1
- package/dist/development/src/acl/PredicateDefinitionProcessor.cjs.map +1 -1
- package/dist/development/src/acl/PredicateDefinitionProcessor.js.map +1 -1
- package/dist/development/src/dataUtils.cjs +1 -0
- package/dist/development/src/dataUtils.cjs.map +1 -1
- package/dist/development/src/dataUtils.js +1 -0
- package/dist/development/src/dataUtils.js.map +1 -1
- package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.cjs.map +1 -1
- package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -1
- package/dist/development/src/definition-generator/DefinitionCodeGenerator.cjs +37 -2
- package/dist/development/src/definition-generator/DefinitionCodeGenerator.cjs.map +1 -1
- package/dist/development/src/definition-generator/DefinitionCodeGenerator.js +37 -2
- package/dist/development/src/definition-generator/DefinitionCodeGenerator.js.map +1 -1
- package/dist/development/src/definition-generator/TsDefinitionGenerator.cjs.map +1 -1
- package/dist/development/src/definition-generator/TsDefinitionGenerator.js.map +1 -1
- package/dist/development/src/json-schema/collectUnsupportedKeywords.cjs +45 -0
- package/dist/development/src/json-schema/collectUnsupportedKeywords.cjs.map +1 -0
- package/dist/development/src/json-schema/collectUnsupportedKeywords.js +45 -0
- package/dist/development/src/json-schema/collectUnsupportedKeywords.js.map +1 -0
- package/dist/development/src/json-schema/jsonSchemaToTsType.cjs +138 -0
- package/dist/development/src/json-schema/jsonSchemaToTsType.cjs.map +1 -0
- package/dist/development/src/json-schema/jsonSchemaToTsType.js +138 -0
- package/dist/development/src/json-schema/jsonSchemaToTsType.js.map +1 -0
- package/dist/development/src/json-schema/keywords.cjs +57 -0
- package/dist/development/src/json-schema/keywords.cjs.map +1 -0
- package/dist/development/src/json-schema/keywords.js +57 -0
- package/dist/development/src/json-schema/keywords.js.map +1 -0
- package/dist/development/src/lax/LaxSchemaConverter.cjs.map +1 -1
- package/dist/development/src/lax/LaxSchemaConverter.js.map +1 -1
- package/dist/development/src/model/modelUtils.cjs.map +1 -1
- package/dist/development/src/model/modelUtils.js.map +1 -1
- package/dist/development/src/model/resolveDefaultColumnType.cjs +2 -0
- package/dist/development/src/model/resolveDefaultColumnType.cjs.map +1 -1
- package/dist/development/src/model/resolveDefaultColumnType.js +2 -0
- package/dist/development/src/model/resolveDefaultColumnType.js.map +1 -1
- package/dist/development/src/schemaNormalizer.cjs.map +1 -1
- package/dist/development/src/schemaNormalizer.js.map +1 -1
- package/dist/development/src/type-schema/acl.cjs.map +1 -1
- package/dist/development/src/type-schema/acl.js.map +1 -1
- package/dist/development/src/type-schema/condition.cjs +6 -1
- package/dist/development/src/type-schema/condition.cjs.map +1 -1
- package/dist/development/src/type-schema/condition.js +6 -1
- package/dist/development/src/type-schema/condition.js.map +1 -1
- package/dist/development/src/type-schema/model.cjs +14 -2
- package/dist/development/src/type-schema/model.cjs.map +1 -1
- package/dist/development/src/type-schema/model.js +14 -2
- package/dist/development/src/type-schema/model.js.map +1 -1
- package/dist/development/src/type-schema/settings.cjs +1 -0
- package/dist/development/src/type-schema/settings.cjs.map +1 -1
- package/dist/development/src/type-schema/settings.js +1 -0
- package/dist/development/src/type-schema/settings.js.map +1 -1
- package/dist/development/src/type-schema/where.cjs.map +1 -1
- package/dist/development/src/type-schema/where.js.map +1 -1
- package/dist/development/src/validation/AclValidator.cjs.map +1 -1
- package/dist/development/src/validation/AclValidator.js.map +1 -1
- package/dist/development/src/validation/ActionsValidator.cjs.map +1 -1
- package/dist/development/src/validation/ActionsValidator.js.map +1 -1
- package/dist/development/src/validation/MembershipResolver.cjs.map +1 -1
- package/dist/development/src/validation/MembershipResolver.js.map +1 -1
- package/dist/development/src/validation/ModelValidator.cjs +66 -0
- package/dist/development/src/validation/ModelValidator.cjs.map +1 -1
- package/dist/development/src/validation/ModelValidator.js +66 -0
- package/dist/development/src/validation/ModelValidator.js.map +1 -1
- package/dist/development/src/validation/SchemaValidator.cjs.map +1 -1
- package/dist/development/src/validation/SchemaValidator.js.map +1 -1
- package/dist/development/src/validation/ValidationValidator.cjs.map +1 -1
- package/dist/development/src/validation/ValidationValidator.js.map +1 -1
- package/dist/development/src/validation/errors.cjs.map +1 -1
- package/dist/development/src/validation/errors.js.map +1 -1
- package/dist/production/index.cjs +11 -0
- package/dist/production/index.cjs.map +1 -1
- package/dist/production/index.js +11 -0
- package/dist/production/index.js.map +1 -1
- package/dist/production/src/acl/PredicateDefinitionProcessor.cjs.map +1 -1
- package/dist/production/src/acl/PredicateDefinitionProcessor.js.map +1 -1
- package/dist/production/src/dataUtils.cjs +1 -0
- package/dist/production/src/dataUtils.cjs.map +1 -1
- package/dist/production/src/dataUtils.js +1 -0
- package/dist/production/src/dataUtils.js.map +1 -1
- package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.cjs.map +1 -1
- package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -1
- package/dist/production/src/definition-generator/DefinitionCodeGenerator.cjs +37 -2
- package/dist/production/src/definition-generator/DefinitionCodeGenerator.cjs.map +1 -1
- package/dist/production/src/definition-generator/DefinitionCodeGenerator.js +37 -2
- package/dist/production/src/definition-generator/DefinitionCodeGenerator.js.map +1 -1
- package/dist/production/src/definition-generator/TsDefinitionGenerator.cjs.map +1 -1
- package/dist/production/src/definition-generator/TsDefinitionGenerator.js.map +1 -1
- package/dist/production/src/json-schema/collectUnsupportedKeywords.cjs +45 -0
- package/dist/production/src/json-schema/collectUnsupportedKeywords.cjs.map +1 -0
- package/dist/production/src/json-schema/collectUnsupportedKeywords.js +45 -0
- package/dist/production/src/json-schema/collectUnsupportedKeywords.js.map +1 -0
- package/dist/production/src/json-schema/jsonSchemaToTsType.cjs +138 -0
- package/dist/production/src/json-schema/jsonSchemaToTsType.cjs.map +1 -0
- package/dist/production/src/json-schema/jsonSchemaToTsType.js +138 -0
- package/dist/production/src/json-schema/jsonSchemaToTsType.js.map +1 -0
- package/dist/production/src/json-schema/keywords.cjs +57 -0
- package/dist/production/src/json-schema/keywords.cjs.map +1 -0
- package/dist/production/src/json-schema/keywords.js +57 -0
- package/dist/production/src/json-schema/keywords.js.map +1 -0
- package/dist/production/src/lax/LaxSchemaConverter.cjs.map +1 -1
- package/dist/production/src/lax/LaxSchemaConverter.js.map +1 -1
- package/dist/production/src/model/modelUtils.cjs.map +1 -1
- package/dist/production/src/model/modelUtils.js.map +1 -1
- package/dist/production/src/model/resolveDefaultColumnType.cjs +2 -0
- package/dist/production/src/model/resolveDefaultColumnType.cjs.map +1 -1
- package/dist/production/src/model/resolveDefaultColumnType.js +2 -0
- package/dist/production/src/model/resolveDefaultColumnType.js.map +1 -1
- package/dist/production/src/schemaNormalizer.cjs.map +1 -1
- package/dist/production/src/schemaNormalizer.js.map +1 -1
- package/dist/production/src/type-schema/acl.cjs.map +1 -1
- package/dist/production/src/type-schema/acl.js.map +1 -1
- package/dist/production/src/type-schema/condition.cjs +6 -1
- package/dist/production/src/type-schema/condition.cjs.map +1 -1
- package/dist/production/src/type-schema/condition.js +6 -1
- package/dist/production/src/type-schema/condition.js.map +1 -1
- package/dist/production/src/type-schema/model.cjs +14 -2
- package/dist/production/src/type-schema/model.cjs.map +1 -1
- package/dist/production/src/type-schema/model.js +14 -2
- package/dist/production/src/type-schema/model.js.map +1 -1
- package/dist/production/src/type-schema/settings.cjs +1 -0
- package/dist/production/src/type-schema/settings.cjs.map +1 -1
- package/dist/production/src/type-schema/settings.js +1 -0
- package/dist/production/src/type-schema/settings.js.map +1 -1
- package/dist/production/src/type-schema/where.cjs.map +1 -1
- package/dist/production/src/type-schema/where.js.map +1 -1
- package/dist/production/src/validation/AclValidator.cjs.map +1 -1
- package/dist/production/src/validation/AclValidator.js.map +1 -1
- package/dist/production/src/validation/ActionsValidator.cjs.map +1 -1
- package/dist/production/src/validation/ActionsValidator.js.map +1 -1
- package/dist/production/src/validation/MembershipResolver.cjs.map +1 -1
- package/dist/production/src/validation/MembershipResolver.js.map +1 -1
- package/dist/production/src/validation/ModelValidator.cjs +66 -0
- package/dist/production/src/validation/ModelValidator.cjs.map +1 -1
- package/dist/production/src/validation/ModelValidator.js +66 -0
- package/dist/production/src/validation/ModelValidator.js.map +1 -1
- package/dist/production/src/validation/SchemaValidator.cjs.map +1 -1
- package/dist/production/src/validation/SchemaValidator.js.map +1 -1
- package/dist/production/src/validation/ValidationValidator.cjs.map +1 -1
- package/dist/production/src/validation/ValidationValidator.js.map +1 -1
- package/dist/production/src/validation/errors.cjs.map +1 -1
- package/dist/production/src/validation/errors.js.map +1 -1
- package/dist/tests/cases/unit/jsonSchemaKeywords.test.d.ts +2 -0
- package/dist/tests/cases/unit/jsonSchemaKeywords.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/jsonSchemaToTsType.test.d.ts +2 -0
- package/dist/tests/cases/unit/jsonSchemaToTsType.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/numericColumnCodeGenerator.test.d.ts +2 -0
- package/dist/tests/cases/unit/numericColumnCodeGenerator.test.d.ts.map +1 -0
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/dist/types/acl/index.d.ts +3 -3
- package/dist/types/acl/index.d.ts.map +1 -1
- package/dist/types/dataUtils.d.ts.map +1 -1
- package/dist/types/definition-generator/AclDefinitionCodeGenerator.d.ts +1 -1
- package/dist/types/definition-generator/AclDefinitionCodeGenerator.d.ts.map +1 -1
- package/dist/types/definition-generator/DefinitionCodeGenerator.d.ts +3 -3
- package/dist/types/definition-generator/DefinitionCodeGenerator.d.ts.map +1 -1
- package/dist/types/definition-generator/TsDefinitionGenerator.d.ts +1 -1
- package/dist/types/definition-generator/TsDefinitionGenerator.d.ts.map +1 -1
- package/dist/types/definition-generator/index.d.ts +4 -4
- package/dist/types/definition-generator/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +11 -10
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/json-schema/collectUnsupportedKeywords.d.ts +20 -0
- package/dist/types/json-schema/collectUnsupportedKeywords.d.ts.map +1 -0
- package/dist/types/json-schema/index.d.ts +4 -0
- package/dist/types/json-schema/index.d.ts.map +1 -0
- package/dist/types/json-schema/jsonSchemaToTsType.d.ts +24 -0
- package/dist/types/json-schema/jsonSchemaToTsType.d.ts.map +1 -0
- package/dist/types/json-schema/keywords.d.ts +28 -0
- package/dist/types/json-schema/keywords.d.ts.map +1 -0
- package/dist/types/lax/LaxSchemaConverter.d.ts +2 -2
- package/dist/types/lax/LaxSchemaConverter.d.ts.map +1 -1
- package/dist/types/lax/index.d.ts +2 -2
- package/dist/types/lax/index.d.ts.map +1 -1
- package/dist/types/model/index.d.ts +3 -3
- package/dist/types/model/index.d.ts.map +1 -1
- package/dist/types/model/resolveDefaultColumnType.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/type-schema/index.d.ts +5 -5
- package/dist/types/type-schema/index.d.ts.map +1 -1
- package/dist/types/type-schema/model.d.ts.map +1 -1
- package/dist/types/type-schema/settings.d.ts +3 -0
- package/dist/types/type-schema/settings.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +3 -3
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/validation/AclValidator.d.ts +1 -1
- package/dist/types/validation/AclValidator.d.ts.map +1 -1
- package/dist/types/validation/ActionsValidator.d.ts +1 -1
- package/dist/types/validation/ActionsValidator.d.ts.map +1 -1
- package/dist/types/validation/ModelValidator.d.ts +3 -1
- package/dist/types/validation/ModelValidator.d.ts.map +1 -1
- package/dist/types/validation/SchemaValidator.d.ts +1 -1
- package/dist/types/validation/SchemaValidator.d.ts.map +1 -1
- package/dist/types/validation/ValidationValidator.d.ts +1 -1
- package/dist/types/validation/ValidationValidator.d.ts.map +1 -1
- package/dist/types/validation/errors.d.ts +1 -1
- package/dist/types/validation/errors.d.ts.map +1 -1
- package/dist/types/validation/index.d.ts +6 -6
- package/dist/types/validation/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/acl/PredicateDefinitionProcessor.ts +1 -1
- package/src/acl/index.ts +3 -3
- package/src/dataUtils.ts +1 -0
- package/src/definition-generator/AclDefinitionCodeGenerator.ts +3 -3
- package/src/definition-generator/DefinitionCodeGenerator.ts +44 -7
- package/src/definition-generator/TsDefinitionGenerator.ts +2 -2
- package/src/definition-generator/index.ts +4 -4
- package/src/index.ts +14 -13
- package/src/json-schema/collectUnsupportedKeywords.ts +74 -0
- package/src/json-schema/index.ts +3 -0
- package/src/json-schema/jsonSchemaToTsType.ts +188 -0
- package/src/json-schema/keywords.ts +79 -0
- package/src/lax/LaxSchemaConverter.ts +3 -3
- package/src/lax/index.ts +2 -2
- package/src/model/index.ts +3 -3
- package/src/model/modelUtils.ts +1 -1
- package/src/model/resolveDefaultColumnType.ts +3 -1
- package/src/schemaNormalizer.ts +1 -1
- package/src/type-schema/acl.ts +1 -1
- package/src/type-schema/condition.ts +5 -0
- package/src/type-schema/index.ts +5 -5
- package/src/type-schema/model.ts +12 -0
- package/src/type-schema/settings.ts +1 -0
- package/src/type-schema/where.ts +2 -2
- package/src/utils/index.ts +3 -3
- package/src/validation/AclValidator.ts +4 -4
- package/src/validation/ActionsValidator.ts +4 -4
- package/src/validation/MembershipResolver.ts +3 -3
- package/src/validation/ModelValidator.ts +80 -2
- package/src/validation/SchemaValidator.ts +5 -5
- package/src/validation/ValidationValidator.ts +2 -2
- package/src/validation/errors.ts +2 -0
- package/src/validation/index.ts +6 -6
- package/tests/cases/unit/conditionSchema.test.ts +1 -1
- package/tests/cases/unit/deepCompare.test.ts +1 -1
- package/tests/cases/unit/jsonSchemaKeywords.test.ts +91 -0
- package/tests/cases/unit/jsonSchemaToTsType.test.ts +55 -0
- package/tests/cases/unit/laxSchemaConverter.test.ts +1 -1
- package/tests/cases/unit/membershipResolver.test.ts +1 -1
- package/tests/cases/unit/modelSchema.test.ts +1 -1
- package/tests/cases/unit/modelValidator.test.ts +246 -1
- package/tests/cases/unit/numericColumnCodeGenerator.test.ts +69 -0
- package/tests/cases/unit/printJsvalue.test.ts +1 -1
- package/tests/cases/unit/tsDefinitionGenerator.test.ts +46 -9
- package/tests/cases/unit/whereSchema.test.ts +2 -2
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { JSONValue } from '@contember/schema'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The TypeScript type emitted when a JSON Schema (or a subschema) cannot be faithfully expressed.
|
|
5
|
+
* It is the universal JSON supertype, so falling back to it is always sound — never looser than the
|
|
6
|
+
* actual data could be, just less precise.
|
|
7
|
+
*/
|
|
8
|
+
export const JSON_VALUE_TYPE = 'JSONValue'
|
|
9
|
+
|
|
10
|
+
const isPlainObject = (value: JSONValue): value is { readonly [key: string]: JSONValue } =>
|
|
11
|
+
typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
12
|
+
|
|
13
|
+
const isPrimitive = (value: JSONValue): value is string | number | boolean | null =>
|
|
14
|
+
value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
15
|
+
|
|
16
|
+
const literal = (value: string | number | boolean | null): string => {
|
|
17
|
+
if (value === null) {
|
|
18
|
+
return 'null'
|
|
19
|
+
}
|
|
20
|
+
if (typeof value === 'string') {
|
|
21
|
+
return JSON.stringify(value)
|
|
22
|
+
}
|
|
23
|
+
return String(value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const VALID_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
27
|
+
|
|
28
|
+
const propertyKey = (key: string): string => (VALID_IDENTIFIER.test(key) ? key : JSON.stringify(key))
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Derives a TypeScript type from the {@link SUPPORTED_JSON_SCHEMA_KEYWORDS supported subset} of JSON
|
|
32
|
+
* Schema. For any construct it cannot faithfully and soundly express it returns {@link JSON_VALUE_TYPE},
|
|
33
|
+
* never a type that is wider than the data could actually be.
|
|
34
|
+
*
|
|
35
|
+
* Mapping:
|
|
36
|
+
* - `const` (primitive) / `enum` (primitives) → literal / union of literals
|
|
37
|
+
* - `type` scalar: string → `string`, number|integer → `number`, boolean → `boolean`, null → `null`
|
|
38
|
+
* - `type: 'object'` (+ `properties`/`required`) → object type with optional vs required props
|
|
39
|
+
* - `type: 'array'` (+ `items`) → `T[]`
|
|
40
|
+
* - `type` array → union of the per-type results
|
|
41
|
+
* - `allOf` → intersection, `anyOf`/`oneOf` → union
|
|
42
|
+
*
|
|
43
|
+
* A boolean schema, an empty schema, or anything not covered → `JSONValue`.
|
|
44
|
+
*/
|
|
45
|
+
export const jsonSchemaToTsType = (schema: JSONValue): string => {
|
|
46
|
+
const result = derive(schema)
|
|
47
|
+
return result ?? JSON_VALUE_TYPE
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Returns the derived type, or `null` if it must fall back to {@link JSON_VALUE_TYPE}. */
|
|
51
|
+
const derive = (schema: JSONValue): string | null => {
|
|
52
|
+
if (!isPlainObject(schema)) {
|
|
53
|
+
// boolean schema (`true`/`false`) – not faithfully expressible / not a constraint we model
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// const – only primitives are faithfully expressible as a literal
|
|
58
|
+
if ('const' in schema && schema.const !== undefined) {
|
|
59
|
+
return isPrimitive(schema.const) ? literal(schema.const) : null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// enum – union of primitive literals
|
|
63
|
+
if (Array.isArray(schema.enum)) {
|
|
64
|
+
if (schema.enum.length === 0 || !schema.enum.every(isPrimitive)) {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
return union(schema.enum.map(it => literal(it as string | number | boolean | null)))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// combinators (independent of `type`)
|
|
71
|
+
if (Array.isArray(schema.allOf)) {
|
|
72
|
+
const parts = mapAll(schema.allOf)
|
|
73
|
+
return parts && parts.length > 0 ? intersection(parts) : null
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(schema.anyOf)) {
|
|
76
|
+
const parts = mapAll(schema.anyOf)
|
|
77
|
+
return parts && parts.length > 0 ? union(parts) : null
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(schema.oneOf)) {
|
|
80
|
+
const parts = mapAll(schema.oneOf)
|
|
81
|
+
return parts && parts.length > 0 ? union(parts) : null
|
|
82
|
+
}
|
|
83
|
+
// `not` is not expressible as a positive TS type
|
|
84
|
+
if ('not' in schema) {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (schema.type !== undefined) {
|
|
89
|
+
return deriveFromType(schema)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// no recognized type-narrowing keyword → cannot tighten below JSONValue
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const deriveFromType = (schema: { readonly [key: string]: JSONValue }): string | null => {
|
|
97
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type]
|
|
98
|
+
const parts: string[] = []
|
|
99
|
+
for (const type of types) {
|
|
100
|
+
const part = deriveScalarOrContainer(type, schema)
|
|
101
|
+
if (part === null) {
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
parts.push(part)
|
|
105
|
+
}
|
|
106
|
+
return parts.length > 0 ? union(parts) : null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const deriveScalarOrContainer = (type: JSONValue, schema: { readonly [key: string]: JSONValue }): string | null => {
|
|
110
|
+
switch (type) {
|
|
111
|
+
case 'string':
|
|
112
|
+
return 'string'
|
|
113
|
+
case 'number':
|
|
114
|
+
case 'integer':
|
|
115
|
+
return 'number'
|
|
116
|
+
case 'boolean':
|
|
117
|
+
return 'boolean'
|
|
118
|
+
case 'null':
|
|
119
|
+
return 'null'
|
|
120
|
+
case 'array':
|
|
121
|
+
return deriveArray(schema)
|
|
122
|
+
case 'object':
|
|
123
|
+
return deriveObject(schema)
|
|
124
|
+
default:
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const deriveArray = (schema: { readonly [key: string]: JSONValue }): string | null => {
|
|
130
|
+
const items = schema.items
|
|
131
|
+
if (items === undefined) {
|
|
132
|
+
// array of unknown element type
|
|
133
|
+
return `readonly ${JSON_VALUE_TYPE}[]`
|
|
134
|
+
}
|
|
135
|
+
if (Array.isArray(items)) {
|
|
136
|
+
// tuple form (`items` as array) is not part of the supported subset
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
const itemType = derive(items) ?? JSON_VALUE_TYPE
|
|
140
|
+
return `readonly (${itemType})[]`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const deriveObject = (schema: { readonly [key: string]: JSONValue }): string | null => {
|
|
144
|
+
const properties = schema.properties
|
|
145
|
+
if (!isPlainObject(properties) || Object.keys(properties).length === 0) {
|
|
146
|
+
// closed/open object with no declared properties – stay sound, do not emit `{}`
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
const requiredList = Array.isArray(schema.required) ? schema.required.filter((it): it is string => typeof it === 'string') : []
|
|
150
|
+
const required = new Set<string>(requiredList)
|
|
151
|
+
const members: string[] = []
|
|
152
|
+
for (const key of Object.keys(properties)) {
|
|
153
|
+
const propType = derive(properties[key]) ?? JSON_VALUE_TYPE
|
|
154
|
+
const optional = required.has(key) ? '' : '?'
|
|
155
|
+
members.push(`${propertyKey(key)}${optional}: ${propType}`)
|
|
156
|
+
}
|
|
157
|
+
return `{ ${members.join('; ')} }`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const mapAll = (schemas: readonly JSONValue[]): string[] | null => {
|
|
161
|
+
const parts: string[] = []
|
|
162
|
+
for (const sub of schemas) {
|
|
163
|
+
const part = derive(sub)
|
|
164
|
+
if (part === null) {
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
parts.push(part)
|
|
168
|
+
}
|
|
169
|
+
return parts
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const union = (parts: readonly string[]): string => {
|
|
173
|
+
const unique = [...new Set(parts)]
|
|
174
|
+
return unique.length === 1 ? unique[0] : unique.map(wrapForUnion).join(' | ')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const intersection = (parts: readonly string[]): string => {
|
|
178
|
+
const unique = [...new Set(parts)]
|
|
179
|
+
return unique.length === 1 ? unique[0] : unique.map(wrapForUnion).join(' & ')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Wrap composite types in parentheses so unions/intersections compose unambiguously. */
|
|
183
|
+
const wrapForUnion = (type: string): string => {
|
|
184
|
+
if (type.includes(' | ') || type.includes(' & ')) {
|
|
185
|
+
return `(${type})`
|
|
186
|
+
}
|
|
187
|
+
return type
|
|
188
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single source of truth for the JSON Schema subset Contember supports on a
|
|
3
|
+
* {@link Model.ColumnType.Json} column.
|
|
4
|
+
*
|
|
5
|
+
* Three independent consumers MUST agree on this set:
|
|
6
|
+
* - the runtime input validator (`@contember/engine-content-api` `validateJsonSchema`),
|
|
7
|
+
* - the schema-build keyword check ({@link collectUnsupportedJsonSchemaKeywords}, wired into
|
|
8
|
+
* `ModelValidator`),
|
|
9
|
+
* - the generated-client type derivation (`@contember/client-content-generator` `jsonSchemaToTsType`).
|
|
10
|
+
*
|
|
11
|
+
* Keep this list in sync with the keywords actually honored by the validator. Adding a keyword here
|
|
12
|
+
* without also implementing it in the validator would make the schema-build check accept a schema the
|
|
13
|
+
* runtime silently ignores — exactly the bug this set exists to prevent.
|
|
14
|
+
*/
|
|
15
|
+
export const SUPPORTED_JSON_SCHEMA_KEYWORDS = [
|
|
16
|
+
// generic
|
|
17
|
+
'type',
|
|
18
|
+
'enum',
|
|
19
|
+
'const',
|
|
20
|
+
// object
|
|
21
|
+
'properties',
|
|
22
|
+
'required',
|
|
23
|
+
'additionalProperties',
|
|
24
|
+
'minProperties',
|
|
25
|
+
'maxProperties',
|
|
26
|
+
// array (draft-07 style `items` applied to all elements)
|
|
27
|
+
'items',
|
|
28
|
+
'minItems',
|
|
29
|
+
'maxItems',
|
|
30
|
+
'uniqueItems',
|
|
31
|
+
// string
|
|
32
|
+
'minLength',
|
|
33
|
+
'maxLength',
|
|
34
|
+
'pattern',
|
|
35
|
+
// number
|
|
36
|
+
'minimum',
|
|
37
|
+
'maximum',
|
|
38
|
+
'exclusiveMinimum',
|
|
39
|
+
'exclusiveMaximum',
|
|
40
|
+
'multipleOf',
|
|
41
|
+
// combinators
|
|
42
|
+
'allOf',
|
|
43
|
+
'anyOf',
|
|
44
|
+
'oneOf',
|
|
45
|
+
'not',
|
|
46
|
+
] as const
|
|
47
|
+
|
|
48
|
+
export type SupportedJsonSchemaKeyword = (typeof SUPPORTED_JSON_SCHEMA_KEYWORDS)[number]
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Non-validating annotation/metadata keywords. They have no effect on validation but are explicitly
|
|
52
|
+
* allowed in a supplied schema so that documentation, defaults and authoring hints do not trigger a
|
|
53
|
+
* "unsupported keyword" schema error.
|
|
54
|
+
*/
|
|
55
|
+
export const ALLOWED_JSON_SCHEMA_ANNOTATION_KEYWORDS = [
|
|
56
|
+
'title',
|
|
57
|
+
'description',
|
|
58
|
+
'$schema',
|
|
59
|
+
'$id',
|
|
60
|
+
'$comment',
|
|
61
|
+
'examples',
|
|
62
|
+
'default',
|
|
63
|
+
'readOnly',
|
|
64
|
+
'writeOnly',
|
|
65
|
+
'deprecated',
|
|
66
|
+
] as const
|
|
67
|
+
|
|
68
|
+
export type AllowedJsonSchemaAnnotationKeyword = (typeof ALLOWED_JSON_SCHEMA_ANNOTATION_KEYWORDS)[number]
|
|
69
|
+
|
|
70
|
+
const supportedKeywordSet: ReadonlySet<string> = new Set<string>(SUPPORTED_JSON_SCHEMA_KEYWORDS)
|
|
71
|
+
const annotationKeywordSet: ReadonlySet<string> = new Set<string>(ALLOWED_JSON_SCHEMA_ANNOTATION_KEYWORDS)
|
|
72
|
+
|
|
73
|
+
export const isSupportedJsonSchemaKeyword = (keyword: string): keyword is SupportedJsonSchemaKeyword => supportedKeywordSet.has(keyword)
|
|
74
|
+
|
|
75
|
+
export const isAllowedJsonSchemaAnnotationKeyword = (keyword: string): keyword is AllowedJsonSchemaAnnotationKeyword =>
|
|
76
|
+
annotationKeywordSet.has(keyword)
|
|
77
|
+
|
|
78
|
+
/** A keyword is recognized if it is either a supported validation keyword or an allowed annotation. */
|
|
79
|
+
export const isRecognizedJsonSchemaKeyword = (keyword: string): boolean => supportedKeywordSet.has(keyword) || annotationKeywordSet.has(keyword)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Model, Schema, Writable } from '@contember/schema'
|
|
2
|
-
import { LaxSchema, LaxSchemaColumn, LaxSchemaEnum, LaxSchemaField, LaxSchemaRelation } from './schema'
|
|
3
|
-
import { DefaultNamingConventions, NamingConventions, resolveDefaultColumnType } from '../model'
|
|
4
|
-
import { emptySchema } from '../index'
|
|
2
|
+
import { LaxSchema, LaxSchemaColumn, LaxSchemaEnum, LaxSchemaField, LaxSchemaRelation } from './schema.js'
|
|
3
|
+
import { DefaultNamingConventions, NamingConventions, resolveDefaultColumnType } from '../model/index.js'
|
|
4
|
+
import { emptySchema } from '../index.js'
|
|
5
5
|
|
|
6
6
|
const simpleColumnTypes: { [K in LaxSchemaColumn['type']]: { type: Model.ColumnType; columnType: string } } = {
|
|
7
7
|
boolean: { type: Model.ColumnType.Bool, columnType: 'boolean' },
|
package/src/lax/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './schema'
|
|
2
|
-
export { LaxSchemaConverter } from './LaxSchemaConverter'
|
|
1
|
+
export * from './schema.js'
|
|
2
|
+
export { LaxSchemaConverter } from './LaxSchemaConverter.js'
|
package/src/model/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from './modelUtils'
|
|
2
|
-
export * from './resolveDefaultColumnType'
|
|
3
|
-
export * from './NamingConventions'
|
|
1
|
+
export * from './modelUtils.js'
|
|
2
|
+
export * from './resolveDefaultColumnType.js'
|
|
3
|
+
export * from './NamingConventions.js'
|
package/src/model/modelUtils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Model } from '@contember/schema'
|
|
2
|
-
import { assertNever } from '../utils'
|
|
2
|
+
import { assertNever } from '../utils/index.js'
|
|
3
3
|
|
|
4
4
|
export const resolveDefaultColumnType = (type: Exclude<Model.ColumnType, Model.ColumnType.Enum>): string => {
|
|
5
5
|
switch (type) {
|
|
@@ -7,6 +7,8 @@ export const resolveDefaultColumnType = (type: Exclude<Model.ColumnType, Model.C
|
|
|
7
7
|
return 'integer'
|
|
8
8
|
case Model.ColumnType.Double:
|
|
9
9
|
return 'double precision'
|
|
10
|
+
case Model.ColumnType.Numeric:
|
|
11
|
+
return 'numeric'
|
|
10
12
|
case Model.ColumnType.String:
|
|
11
13
|
return 'text'
|
|
12
14
|
case Model.ColumnType.Uuid:
|
package/src/schemaNormalizer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Acl, ProjectRole, Schema } from '@contember/schema'
|
|
2
|
-
import { AllowAllPermissionFactory } from './acl'
|
|
2
|
+
import { AllowAllPermissionFactory } from './acl/index.js'
|
|
3
3
|
|
|
4
4
|
export const normalizeSchema = <S extends Schema>(schema: S): S => {
|
|
5
5
|
const adminRoleDefinition: Partial<Acl.RolePermissions> = schema.acl.roles?.[ProjectRole.ADMIN] ?? {}
|
package/src/type-schema/acl.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Acl, Model } from '@contember/schema'
|
|
2
2
|
import * as Typesafe from '@contember/typesafe'
|
|
3
|
-
import { conditionSchema } from './condition'
|
|
3
|
+
import { conditionSchema } from './condition.js'
|
|
4
4
|
|
|
5
5
|
const membershipMatchRuleSchema = Typesafe.union(
|
|
6
6
|
Typesafe.boolean,
|
|
@@ -48,6 +48,8 @@ const conditionSchemaInner = <T extends Json>(metadata: ResolvedColumnMetadata<T
|
|
|
48
48
|
containsCI: Typesafe.string,
|
|
49
49
|
startsWithCI: Typesafe.string,
|
|
50
50
|
endsWithCI: Typesafe.string,
|
|
51
|
+
similar: Typesafe.string,
|
|
52
|
+
wordSimilar: Typesafe.string,
|
|
51
53
|
}
|
|
52
54
|
: {}),
|
|
53
55
|
}))
|
|
@@ -77,6 +79,9 @@ const resolveColumnTypeSchema = (type: Model.ColumnType) => {
|
|
|
77
79
|
case Model.ColumnType.Enum:
|
|
78
80
|
case Model.ColumnType.String:
|
|
79
81
|
return Typesafe.string
|
|
82
|
+
// Numeric is transported as a string to avoid JS float precision loss.
|
|
83
|
+
case Model.ColumnType.Numeric:
|
|
84
|
+
return Typesafe.string
|
|
80
85
|
case Model.ColumnType.Double:
|
|
81
86
|
return Typesafe.number
|
|
82
87
|
case Model.ColumnType.Int:
|
package/src/type-schema/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export * from './acl'
|
|
2
|
-
export * from './condition'
|
|
3
|
-
export * from './model'
|
|
4
|
-
export * from './settings'
|
|
5
|
-
export * from './validation'
|
|
1
|
+
export * from './acl.js'
|
|
2
|
+
export * from './condition.js'
|
|
3
|
+
export * from './model.js'
|
|
4
|
+
export * from './settings.js'
|
|
5
|
+
export * from './validation.js'
|
package/src/type-schema/model.ts
CHANGED
|
@@ -138,6 +138,7 @@ const columnSchema = Typesafe.intersection(
|
|
|
138
138
|
default: Typesafe.anyJson,
|
|
139
139
|
sequence: intersectionSchema as Typesafe.Type<Model.AnyColumn['sequence']>,
|
|
140
140
|
collation: Typesafe.string as Typesafe.Type<Model.AnyColumn['collation']>,
|
|
141
|
+
schema: Typesafe.anyJson,
|
|
141
142
|
}),
|
|
142
143
|
)
|
|
143
144
|
const columnSchemaCheck: Typesafe.Equals<Model.AnyColumn, ReturnType<typeof columnSchema>> = true
|
|
@@ -173,6 +174,17 @@ const indexSchemaBase = Typesafe.intersection(
|
|
|
173
174
|
Typesafe.partial({
|
|
174
175
|
name: Typesafe.string,
|
|
175
176
|
method: Typesafe.enumeration('btree', 'gin', 'gist', 'hash', 'brin', 'spgist'),
|
|
177
|
+
opClass: Typesafe.string,
|
|
178
|
+
where: Typesafe.string,
|
|
179
|
+
include: Typesafe.array(Typesafe.string),
|
|
180
|
+
columnOptions: Typesafe.record(
|
|
181
|
+
Typesafe.string,
|
|
182
|
+
Typesafe.partial({
|
|
183
|
+
order: Typesafe.enumeration('asc', 'desc'),
|
|
184
|
+
nulls: Typesafe.enumeration('first', 'last'),
|
|
185
|
+
opClass: Typesafe.string,
|
|
186
|
+
}),
|
|
187
|
+
),
|
|
176
188
|
}),
|
|
177
189
|
)
|
|
178
190
|
const indexCheck: Typesafe.Equals<Model.Index, ReturnType<typeof indexSchemaBase>> = true
|
|
@@ -10,6 +10,7 @@ export const settingsSchema = Typesafe.partial({
|
|
|
10
10
|
content: Typesafe.partial({
|
|
11
11
|
useExistsInHasManyFilter: Typesafe.boolean,
|
|
12
12
|
fullDateTimeResponse: Typesafe.boolean,
|
|
13
|
+
dateTimeResponseFormat: Typesafe.union(Typesafe.literal('legacy'), Typesafe.literal('iso8601')),
|
|
13
14
|
shortDateResponse: Typesafe.boolean,
|
|
14
15
|
uuidVersion: Typesafe.union(Typesafe.literal(4), Typesafe.literal(7)),
|
|
15
16
|
}),
|
package/src/type-schema/where.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Input, Model } from '@contember/schema'
|
|
2
|
-
import { acceptFieldVisitor } from '../model'
|
|
3
|
-
import { conditionSchema } from './condition'
|
|
2
|
+
import { acceptFieldVisitor } from '../model/index.js'
|
|
3
|
+
import { conditionSchema } from './condition.js'
|
|
4
4
|
import * as Typesafe from '@contember/typesafe'
|
|
5
5
|
|
|
6
6
|
export const whereSchema = ({ schema, entity }: {
|
package/src/utils/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export * from './assertNever'
|
|
2
|
-
export * from './isIt'
|
|
3
|
-
export * from './deepCompare'
|
|
1
|
+
export * from './assertNever.js'
|
|
2
|
+
export * from './isIt.js'
|
|
3
|
+
export * from './deepCompare.js'
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Acl, Model } from '@contember/schema'
|
|
2
|
-
import { PredicateDefinitionProcessor } from '../acl'
|
|
3
|
-
import { getEntity } from '../model'
|
|
4
|
-
import { ErrorBuilder, ValidationError } from './errors'
|
|
5
|
-
import { conditionSchema } from '../type-schema'
|
|
2
|
+
import { PredicateDefinitionProcessor } from '../acl/index.js'
|
|
3
|
+
import { getEntity } from '../model/index.js'
|
|
4
|
+
import { ErrorBuilder, ValidationError } from './errors.js'
|
|
5
|
+
import { conditionSchema } from '../type-schema/index.js'
|
|
6
6
|
|
|
7
7
|
export class AclValidator {
|
|
8
8
|
constructor(private readonly model: Model.Schema) {}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Actions, Model } from '@contember/schema'
|
|
2
|
-
import { acceptFieldVisitor } from '../model'
|
|
3
|
-
import { ErrorBuilder, ValidationError } from './errors'
|
|
4
|
-
import { assertNever } from '../utils'
|
|
2
|
+
import { acceptFieldVisitor } from '../model/index.js'
|
|
3
|
+
import { ErrorBuilder, ValidationError } from './errors.js'
|
|
4
|
+
import { assertNever } from '../utils/index.js'
|
|
5
5
|
import * as Typesafe from '@contember/typesafe'
|
|
6
|
-
import { whereSchema } from '../type-schema/where'
|
|
6
|
+
import { whereSchema } from '../type-schema/where.js'
|
|
7
7
|
|
|
8
8
|
const manyArgsSchema = (args: { schema: Model.Schema; entity: Model.Entity }) =>
|
|
9
9
|
Typesafe.partial({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Acl, Input } from '@contember/schema'
|
|
2
|
-
import { getRoleVariables } from '../acl'
|
|
3
|
-
import { conditionSchema } from '../type-schema'
|
|
4
|
-
import { assertNever } from '../utils'
|
|
2
|
+
import { getRoleVariables } from '../acl/index.js'
|
|
3
|
+
import { conditionSchema } from '../type-schema/index.js'
|
|
4
|
+
import { assertNever } from '../utils/index.js'
|
|
5
5
|
|
|
6
6
|
export class MembershipResolver {
|
|
7
7
|
public static UnknownIdentity = { identityId: '00000000-0000-0000-0000-000000000000' }
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Model } from '@contember/schema'
|
|
2
|
-
import { ErrorBuilder, ValidationError } from './errors'
|
|
3
|
-
import { acceptEveryFieldVisitor, getTargetEntity, isColumn, isInverseRelation, isOwningRelation } from '../model'
|
|
2
|
+
import { ErrorBuilder, ValidationError } from './errors.js'
|
|
3
|
+
import { acceptEveryFieldVisitor, getTargetEntity, isColumn, isInverseRelation, isOwningRelation } from '../model/index.js'
|
|
4
|
+
import { collectUnsupportedJsonSchemaKeywords, SUPPORTED_JSON_SCHEMA_KEYWORDS } from '../json-schema/index.js'
|
|
4
5
|
|
|
5
6
|
const IDENTIFIER_PATTERN = /^[_a-zA-Z][_a-zA-Z0-9]*$/
|
|
7
|
+
// An index operator class is an identifier, optionally schema-qualified (e.g. "public.gin_trgm_ops").
|
|
8
|
+
const INDEX_OPCLASS_PATTERN = /^[_a-zA-Z][_a-zA-Z0-9$]*(\.[_a-zA-Z][_a-zA-Z0-9$]*)?$/
|
|
6
9
|
const RESERVED_WORDS = ['and', 'or', 'not']
|
|
7
10
|
|
|
8
11
|
export class ModelValidator {
|
|
@@ -54,9 +57,69 @@ export class ModelValidator {
|
|
|
54
57
|
entity,
|
|
55
58
|
errors.for('unique'),
|
|
56
59
|
)
|
|
60
|
+
this.validateIndexes(
|
|
61
|
+
entity,
|
|
62
|
+
errors.for('indexes'),
|
|
63
|
+
)
|
|
57
64
|
this.validateColumnNamesCollision(entity, errors)
|
|
58
65
|
}
|
|
59
66
|
|
|
67
|
+
private validateIndexes(entity: Model.Entity, errors: ErrorBuilder): void {
|
|
68
|
+
for (const index of entity.indexes) {
|
|
69
|
+
for (const field of [...index.fields, ...index.include ?? []]) {
|
|
70
|
+
if (!entity.fields[field]) {
|
|
71
|
+
errors.add('MODEL_UNDEFINED_FIELD', `Referenced field ${field} in an index does not exists`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Postgres rejects an INCLUDE column that is already a key column.
|
|
75
|
+
for (const field of index.include ?? []) {
|
|
76
|
+
if (index.fields.includes(field)) {
|
|
77
|
+
errors.add('MODEL_INVALID_INDEX', `Field ${field} cannot be both an index key and an included (covering) column`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Per-column options (order/nulls/opClass) only apply to key columns.
|
|
81
|
+
for (const field of Object.keys(index.columnOptions ?? {})) {
|
|
82
|
+
if (!index.fields.includes(field)) {
|
|
83
|
+
errors.add('MODEL_INVALID_INDEX', `Column options reference field ${field}, which is not a key column of the index`)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// columnOptions is keyed by field name, so a key column appearing twice cannot carry distinct
|
|
87
|
+
// per-position options (the duplicate collapses to one entry) — reject the ambiguous shape.
|
|
88
|
+
const seenFields = new Set<string>()
|
|
89
|
+
for (const field of index.fields) {
|
|
90
|
+
if (seenFields.has(field)) {
|
|
91
|
+
errors.add('MODEL_INVALID_INDEX', `Field ${field} is listed more than once as an index key column`)
|
|
92
|
+
}
|
|
93
|
+
seenFields.add(field)
|
|
94
|
+
}
|
|
95
|
+
// Sort order and NULLS placement are btree-only; Postgres rejects them on other methods at apply time.
|
|
96
|
+
if (index.method !== undefined && index.method !== 'btree') {
|
|
97
|
+
for (const [field, options] of Object.entries(index.columnOptions ?? {})) {
|
|
98
|
+
if (options.order !== undefined || options.nulls !== undefined) {
|
|
99
|
+
errors.add('MODEL_INVALID_INDEX', `Column options (order/nulls) on field ${field} are only supported for btree indexes, not "${index.method}"`)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// opClass is emitted verbatim into CREATE INDEX, so constrain it to an identifier (optionally
|
|
104
|
+
// schema-qualified) — both to block SQL injection and to catch typos early with a clear message.
|
|
105
|
+
for (const opClass of [index.opClass, ...Object.values(index.columnOptions ?? {}).map(it => it.opClass)]) {
|
|
106
|
+
if (opClass !== undefined && !INDEX_OPCLASS_PATTERN.test(opClass)) {
|
|
107
|
+
errors.add(
|
|
108
|
+
'MODEL_INVALID_INDEX',
|
|
109
|
+
`Invalid index operator class "${opClass}": expected an identifier, optionally schema-qualified (e.g. "public.gin_trgm_ops")`,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// `where` is a raw SQL predicate embedded verbatim into CREATE INDEX. We don't parse SQL,
|
|
114
|
+
// but a couple of basic checks catch the common footguns early with a clear message:
|
|
115
|
+
if (index.where !== undefined && index.where.trim() === '') {
|
|
116
|
+
errors.add('MODEL_INVALID_INDEX', 'Index predicate (where) must not be empty.')
|
|
117
|
+
} else if (index.where?.includes(';')) {
|
|
118
|
+
errors.add('MODEL_INVALID_INDEX', 'Index predicate (where) must not contain ";".')
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
60
123
|
private validateUniqueConstraints(entity: Model.Entity, errors: ErrorBuilder): void {
|
|
61
124
|
for (const constraint of entity.unique) {
|
|
62
125
|
if (entity.view?.materialized && !constraint.index) {
|
|
@@ -109,6 +172,21 @@ export class ModelValidator {
|
|
|
109
172
|
if (field.sequence && field.nullable) {
|
|
110
173
|
errors.add('MODEL_INVALID_COLUMN_DEFINITION', 'Column with sequence cannot be nullable.')
|
|
111
174
|
}
|
|
175
|
+
this.validateColumnJsonSchema(field, errors)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private validateColumnJsonSchema(column: Model.AnyColumn, errors: ErrorBuilder): void {
|
|
180
|
+
if (column.type !== Model.ColumnType.Json || column.schema === undefined) {
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
for (const { keyword, path } of collectUnsupportedJsonSchemaKeywords(column.schema)) {
|
|
184
|
+
const location = path === '' ? 'at the schema root' : `at "${path}"`
|
|
185
|
+
errors.add(
|
|
186
|
+
'MODEL_INVALID_JSON_SCHEMA',
|
|
187
|
+
`JSON Schema on column "${column.name}" uses unsupported keyword "${keyword}" ${location}. `
|
|
188
|
+
+ `Only the following keywords are supported: ${SUPPORTED_JSON_SCHEMA_KEYWORDS.join(', ')}.`,
|
|
189
|
+
)
|
|
112
190
|
}
|
|
113
191
|
}
|
|
114
192
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Schema } from '@contember/schema'
|
|
2
|
-
import { ValidationError, ValidationErrorCode } from './errors'
|
|
3
|
-
import { AclValidator } from './AclValidator'
|
|
4
|
-
import { ModelValidator } from './ModelValidator'
|
|
5
|
-
import { ValidationValidator } from './ValidationValidator'
|
|
6
|
-
import { ActionsValidator } from './ActionsValidator'
|
|
2
|
+
import { ValidationError, ValidationErrorCode } from './errors.js'
|
|
3
|
+
import { AclValidator } from './AclValidator.js'
|
|
4
|
+
import { ModelValidator } from './ModelValidator.js'
|
|
5
|
+
import { ValidationValidator } from './ValidationValidator.js'
|
|
6
|
+
import { ActionsValidator } from './ActionsValidator.js'
|
|
7
7
|
|
|
8
8
|
export interface SchemaValidatorSkippedErrors {
|
|
9
9
|
readonly code: ValidationErrorCode
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Model, Validation } from '@contember/schema'
|
|
2
|
-
import { ErrorBuilder, ValidationError } from './errors'
|
|
3
|
-
import { acceptFieldVisitor } from '../model'
|
|
2
|
+
import { ErrorBuilder, ValidationError } from './errors.js'
|
|
3
|
+
import { acceptFieldVisitor } from '../model/index.js'
|
|
4
4
|
|
|
5
5
|
export class ValidationValidator {
|
|
6
6
|
constructor(private readonly model: Model.Schema) {}
|
package/src/validation/errors.ts
CHANGED
|
@@ -6,10 +6,12 @@ export type ValidationErrorCode =
|
|
|
6
6
|
| 'MODEL_RELATION_REQUIRED'
|
|
7
7
|
| 'MODEL_INVALID_RELATION_DEFINITION'
|
|
8
8
|
| 'MODEL_INVALID_COLUMN_DEFINITION'
|
|
9
|
+
| 'MODEL_INVALID_JSON_SCHEMA'
|
|
9
10
|
| 'MODEL_INVALID_VIEW_USAGE'
|
|
10
11
|
| 'MODEL_INVALID_IDENTIFIER'
|
|
11
12
|
| 'MODEL_INVALID_ENTITY_NAME'
|
|
12
13
|
| 'MODEL_INVALID_CONSTRAINT'
|
|
14
|
+
| 'MODEL_INVALID_INDEX'
|
|
13
15
|
| 'MODEL_NAME_COLLISION'
|
|
14
16
|
| 'ACL_INVALID_CONDITION'
|
|
15
17
|
| 'ACL_UNDEFINED_VARIABLE'
|
package/src/validation/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export * from './AclValidator'
|
|
2
|
-
export * from './ModelValidator'
|
|
3
|
-
export * from './ValidationValidator'
|
|
4
|
-
export * from './errors'
|
|
5
|
-
export * from './SchemaValidator'
|
|
6
|
-
export * from './MembershipResolver'
|
|
1
|
+
export * from './AclValidator.js'
|
|
2
|
+
export * from './ModelValidator.js'
|
|
3
|
+
export * from './ValidationValidator.js'
|
|
4
|
+
export * from './errors.js'
|
|
5
|
+
export * from './SchemaValidator.js'
|
|
6
|
+
export * from './MembershipResolver.js'
|