@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.
Files changed (247) hide show
  1. package/dist/development/index.cjs +11 -0
  2. package/dist/development/index.cjs.map +1 -1
  3. package/dist/development/index.js +11 -0
  4. package/dist/development/index.js.map +1 -1
  5. package/dist/development/src/acl/PredicateDefinitionProcessor.cjs.map +1 -1
  6. package/dist/development/src/acl/PredicateDefinitionProcessor.js.map +1 -1
  7. package/dist/development/src/dataUtils.cjs +1 -0
  8. package/dist/development/src/dataUtils.cjs.map +1 -1
  9. package/dist/development/src/dataUtils.js +1 -0
  10. package/dist/development/src/dataUtils.js.map +1 -1
  11. package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.cjs.map +1 -1
  12. package/dist/development/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -1
  13. package/dist/development/src/definition-generator/DefinitionCodeGenerator.cjs +37 -2
  14. package/dist/development/src/definition-generator/DefinitionCodeGenerator.cjs.map +1 -1
  15. package/dist/development/src/definition-generator/DefinitionCodeGenerator.js +37 -2
  16. package/dist/development/src/definition-generator/DefinitionCodeGenerator.js.map +1 -1
  17. package/dist/development/src/definition-generator/TsDefinitionGenerator.cjs.map +1 -1
  18. package/dist/development/src/definition-generator/TsDefinitionGenerator.js.map +1 -1
  19. package/dist/development/src/json-schema/collectUnsupportedKeywords.cjs +45 -0
  20. package/dist/development/src/json-schema/collectUnsupportedKeywords.cjs.map +1 -0
  21. package/dist/development/src/json-schema/collectUnsupportedKeywords.js +45 -0
  22. package/dist/development/src/json-schema/collectUnsupportedKeywords.js.map +1 -0
  23. package/dist/development/src/json-schema/jsonSchemaToTsType.cjs +138 -0
  24. package/dist/development/src/json-schema/jsonSchemaToTsType.cjs.map +1 -0
  25. package/dist/development/src/json-schema/jsonSchemaToTsType.js +138 -0
  26. package/dist/development/src/json-schema/jsonSchemaToTsType.js.map +1 -0
  27. package/dist/development/src/json-schema/keywords.cjs +57 -0
  28. package/dist/development/src/json-schema/keywords.cjs.map +1 -0
  29. package/dist/development/src/json-schema/keywords.js +57 -0
  30. package/dist/development/src/json-schema/keywords.js.map +1 -0
  31. package/dist/development/src/lax/LaxSchemaConverter.cjs.map +1 -1
  32. package/dist/development/src/lax/LaxSchemaConverter.js.map +1 -1
  33. package/dist/development/src/model/modelUtils.cjs.map +1 -1
  34. package/dist/development/src/model/modelUtils.js.map +1 -1
  35. package/dist/development/src/model/resolveDefaultColumnType.cjs +2 -0
  36. package/dist/development/src/model/resolveDefaultColumnType.cjs.map +1 -1
  37. package/dist/development/src/model/resolveDefaultColumnType.js +2 -0
  38. package/dist/development/src/model/resolveDefaultColumnType.js.map +1 -1
  39. package/dist/development/src/schemaNormalizer.cjs.map +1 -1
  40. package/dist/development/src/schemaNormalizer.js.map +1 -1
  41. package/dist/development/src/type-schema/acl.cjs.map +1 -1
  42. package/dist/development/src/type-schema/acl.js.map +1 -1
  43. package/dist/development/src/type-schema/condition.cjs +6 -1
  44. package/dist/development/src/type-schema/condition.cjs.map +1 -1
  45. package/dist/development/src/type-schema/condition.js +6 -1
  46. package/dist/development/src/type-schema/condition.js.map +1 -1
  47. package/dist/development/src/type-schema/model.cjs +14 -2
  48. package/dist/development/src/type-schema/model.cjs.map +1 -1
  49. package/dist/development/src/type-schema/model.js +14 -2
  50. package/dist/development/src/type-schema/model.js.map +1 -1
  51. package/dist/development/src/type-schema/settings.cjs +1 -0
  52. package/dist/development/src/type-schema/settings.cjs.map +1 -1
  53. package/dist/development/src/type-schema/settings.js +1 -0
  54. package/dist/development/src/type-schema/settings.js.map +1 -1
  55. package/dist/development/src/type-schema/where.cjs.map +1 -1
  56. package/dist/development/src/type-schema/where.js.map +1 -1
  57. package/dist/development/src/validation/AclValidator.cjs.map +1 -1
  58. package/dist/development/src/validation/AclValidator.js.map +1 -1
  59. package/dist/development/src/validation/ActionsValidator.cjs.map +1 -1
  60. package/dist/development/src/validation/ActionsValidator.js.map +1 -1
  61. package/dist/development/src/validation/MembershipResolver.cjs.map +1 -1
  62. package/dist/development/src/validation/MembershipResolver.js.map +1 -1
  63. package/dist/development/src/validation/ModelValidator.cjs +66 -0
  64. package/dist/development/src/validation/ModelValidator.cjs.map +1 -1
  65. package/dist/development/src/validation/ModelValidator.js +66 -0
  66. package/dist/development/src/validation/ModelValidator.js.map +1 -1
  67. package/dist/development/src/validation/SchemaValidator.cjs.map +1 -1
  68. package/dist/development/src/validation/SchemaValidator.js.map +1 -1
  69. package/dist/development/src/validation/ValidationValidator.cjs.map +1 -1
  70. package/dist/development/src/validation/ValidationValidator.js.map +1 -1
  71. package/dist/development/src/validation/errors.cjs.map +1 -1
  72. package/dist/development/src/validation/errors.js.map +1 -1
  73. package/dist/production/index.cjs +11 -0
  74. package/dist/production/index.cjs.map +1 -1
  75. package/dist/production/index.js +11 -0
  76. package/dist/production/index.js.map +1 -1
  77. package/dist/production/src/acl/PredicateDefinitionProcessor.cjs.map +1 -1
  78. package/dist/production/src/acl/PredicateDefinitionProcessor.js.map +1 -1
  79. package/dist/production/src/dataUtils.cjs +1 -0
  80. package/dist/production/src/dataUtils.cjs.map +1 -1
  81. package/dist/production/src/dataUtils.js +1 -0
  82. package/dist/production/src/dataUtils.js.map +1 -1
  83. package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.cjs.map +1 -1
  84. package/dist/production/src/definition-generator/AclDefinitionCodeGenerator.js.map +1 -1
  85. package/dist/production/src/definition-generator/DefinitionCodeGenerator.cjs +37 -2
  86. package/dist/production/src/definition-generator/DefinitionCodeGenerator.cjs.map +1 -1
  87. package/dist/production/src/definition-generator/DefinitionCodeGenerator.js +37 -2
  88. package/dist/production/src/definition-generator/DefinitionCodeGenerator.js.map +1 -1
  89. package/dist/production/src/definition-generator/TsDefinitionGenerator.cjs.map +1 -1
  90. package/dist/production/src/definition-generator/TsDefinitionGenerator.js.map +1 -1
  91. package/dist/production/src/json-schema/collectUnsupportedKeywords.cjs +45 -0
  92. package/dist/production/src/json-schema/collectUnsupportedKeywords.cjs.map +1 -0
  93. package/dist/production/src/json-schema/collectUnsupportedKeywords.js +45 -0
  94. package/dist/production/src/json-schema/collectUnsupportedKeywords.js.map +1 -0
  95. package/dist/production/src/json-schema/jsonSchemaToTsType.cjs +138 -0
  96. package/dist/production/src/json-schema/jsonSchemaToTsType.cjs.map +1 -0
  97. package/dist/production/src/json-schema/jsonSchemaToTsType.js +138 -0
  98. package/dist/production/src/json-schema/jsonSchemaToTsType.js.map +1 -0
  99. package/dist/production/src/json-schema/keywords.cjs +57 -0
  100. package/dist/production/src/json-schema/keywords.cjs.map +1 -0
  101. package/dist/production/src/json-schema/keywords.js +57 -0
  102. package/dist/production/src/json-schema/keywords.js.map +1 -0
  103. package/dist/production/src/lax/LaxSchemaConverter.cjs.map +1 -1
  104. package/dist/production/src/lax/LaxSchemaConverter.js.map +1 -1
  105. package/dist/production/src/model/modelUtils.cjs.map +1 -1
  106. package/dist/production/src/model/modelUtils.js.map +1 -1
  107. package/dist/production/src/model/resolveDefaultColumnType.cjs +2 -0
  108. package/dist/production/src/model/resolveDefaultColumnType.cjs.map +1 -1
  109. package/dist/production/src/model/resolveDefaultColumnType.js +2 -0
  110. package/dist/production/src/model/resolveDefaultColumnType.js.map +1 -1
  111. package/dist/production/src/schemaNormalizer.cjs.map +1 -1
  112. package/dist/production/src/schemaNormalizer.js.map +1 -1
  113. package/dist/production/src/type-schema/acl.cjs.map +1 -1
  114. package/dist/production/src/type-schema/acl.js.map +1 -1
  115. package/dist/production/src/type-schema/condition.cjs +6 -1
  116. package/dist/production/src/type-schema/condition.cjs.map +1 -1
  117. package/dist/production/src/type-schema/condition.js +6 -1
  118. package/dist/production/src/type-schema/condition.js.map +1 -1
  119. package/dist/production/src/type-schema/model.cjs +14 -2
  120. package/dist/production/src/type-schema/model.cjs.map +1 -1
  121. package/dist/production/src/type-schema/model.js +14 -2
  122. package/dist/production/src/type-schema/model.js.map +1 -1
  123. package/dist/production/src/type-schema/settings.cjs +1 -0
  124. package/dist/production/src/type-schema/settings.cjs.map +1 -1
  125. package/dist/production/src/type-schema/settings.js +1 -0
  126. package/dist/production/src/type-schema/settings.js.map +1 -1
  127. package/dist/production/src/type-schema/where.cjs.map +1 -1
  128. package/dist/production/src/type-schema/where.js.map +1 -1
  129. package/dist/production/src/validation/AclValidator.cjs.map +1 -1
  130. package/dist/production/src/validation/AclValidator.js.map +1 -1
  131. package/dist/production/src/validation/ActionsValidator.cjs.map +1 -1
  132. package/dist/production/src/validation/ActionsValidator.js.map +1 -1
  133. package/dist/production/src/validation/MembershipResolver.cjs.map +1 -1
  134. package/dist/production/src/validation/MembershipResolver.js.map +1 -1
  135. package/dist/production/src/validation/ModelValidator.cjs +66 -0
  136. package/dist/production/src/validation/ModelValidator.cjs.map +1 -1
  137. package/dist/production/src/validation/ModelValidator.js +66 -0
  138. package/dist/production/src/validation/ModelValidator.js.map +1 -1
  139. package/dist/production/src/validation/SchemaValidator.cjs.map +1 -1
  140. package/dist/production/src/validation/SchemaValidator.js.map +1 -1
  141. package/dist/production/src/validation/ValidationValidator.cjs.map +1 -1
  142. package/dist/production/src/validation/ValidationValidator.js.map +1 -1
  143. package/dist/production/src/validation/errors.cjs.map +1 -1
  144. package/dist/production/src/validation/errors.js.map +1 -1
  145. package/dist/tests/cases/unit/jsonSchemaKeywords.test.d.ts +2 -0
  146. package/dist/tests/cases/unit/jsonSchemaKeywords.test.d.ts.map +1 -0
  147. package/dist/tests/cases/unit/jsonSchemaToTsType.test.d.ts +2 -0
  148. package/dist/tests/cases/unit/jsonSchemaToTsType.test.d.ts.map +1 -0
  149. package/dist/tests/cases/unit/numericColumnCodeGenerator.test.d.ts +2 -0
  150. package/dist/tests/cases/unit/numericColumnCodeGenerator.test.d.ts.map +1 -0
  151. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  152. package/dist/types/acl/index.d.ts +3 -3
  153. package/dist/types/acl/index.d.ts.map +1 -1
  154. package/dist/types/dataUtils.d.ts.map +1 -1
  155. package/dist/types/definition-generator/AclDefinitionCodeGenerator.d.ts +1 -1
  156. package/dist/types/definition-generator/AclDefinitionCodeGenerator.d.ts.map +1 -1
  157. package/dist/types/definition-generator/DefinitionCodeGenerator.d.ts +3 -3
  158. package/dist/types/definition-generator/DefinitionCodeGenerator.d.ts.map +1 -1
  159. package/dist/types/definition-generator/TsDefinitionGenerator.d.ts +1 -1
  160. package/dist/types/definition-generator/TsDefinitionGenerator.d.ts.map +1 -1
  161. package/dist/types/definition-generator/index.d.ts +4 -4
  162. package/dist/types/definition-generator/index.d.ts.map +1 -1
  163. package/dist/types/index.d.ts +11 -10
  164. package/dist/types/index.d.ts.map +1 -1
  165. package/dist/types/json-schema/collectUnsupportedKeywords.d.ts +20 -0
  166. package/dist/types/json-schema/collectUnsupportedKeywords.d.ts.map +1 -0
  167. package/dist/types/json-schema/index.d.ts +4 -0
  168. package/dist/types/json-schema/index.d.ts.map +1 -0
  169. package/dist/types/json-schema/jsonSchemaToTsType.d.ts +24 -0
  170. package/dist/types/json-schema/jsonSchemaToTsType.d.ts.map +1 -0
  171. package/dist/types/json-schema/keywords.d.ts +28 -0
  172. package/dist/types/json-schema/keywords.d.ts.map +1 -0
  173. package/dist/types/lax/LaxSchemaConverter.d.ts +2 -2
  174. package/dist/types/lax/LaxSchemaConverter.d.ts.map +1 -1
  175. package/dist/types/lax/index.d.ts +2 -2
  176. package/dist/types/lax/index.d.ts.map +1 -1
  177. package/dist/types/model/index.d.ts +3 -3
  178. package/dist/types/model/index.d.ts.map +1 -1
  179. package/dist/types/model/resolveDefaultColumnType.d.ts.map +1 -1
  180. package/dist/types/tsconfig.tsbuildinfo +1 -1
  181. package/dist/types/type-schema/index.d.ts +5 -5
  182. package/dist/types/type-schema/index.d.ts.map +1 -1
  183. package/dist/types/type-schema/model.d.ts.map +1 -1
  184. package/dist/types/type-schema/settings.d.ts +3 -0
  185. package/dist/types/type-schema/settings.d.ts.map +1 -1
  186. package/dist/types/utils/index.d.ts +3 -3
  187. package/dist/types/utils/index.d.ts.map +1 -1
  188. package/dist/types/validation/AclValidator.d.ts +1 -1
  189. package/dist/types/validation/AclValidator.d.ts.map +1 -1
  190. package/dist/types/validation/ActionsValidator.d.ts +1 -1
  191. package/dist/types/validation/ActionsValidator.d.ts.map +1 -1
  192. package/dist/types/validation/ModelValidator.d.ts +3 -1
  193. package/dist/types/validation/ModelValidator.d.ts.map +1 -1
  194. package/dist/types/validation/SchemaValidator.d.ts +1 -1
  195. package/dist/types/validation/SchemaValidator.d.ts.map +1 -1
  196. package/dist/types/validation/ValidationValidator.d.ts +1 -1
  197. package/dist/types/validation/ValidationValidator.d.ts.map +1 -1
  198. package/dist/types/validation/errors.d.ts +1 -1
  199. package/dist/types/validation/errors.d.ts.map +1 -1
  200. package/dist/types/validation/index.d.ts +6 -6
  201. package/dist/types/validation/index.d.ts.map +1 -1
  202. package/package.json +4 -4
  203. package/src/acl/PredicateDefinitionProcessor.ts +1 -1
  204. package/src/acl/index.ts +3 -3
  205. package/src/dataUtils.ts +1 -0
  206. package/src/definition-generator/AclDefinitionCodeGenerator.ts +3 -3
  207. package/src/definition-generator/DefinitionCodeGenerator.ts +44 -7
  208. package/src/definition-generator/TsDefinitionGenerator.ts +2 -2
  209. package/src/definition-generator/index.ts +4 -4
  210. package/src/index.ts +14 -13
  211. package/src/json-schema/collectUnsupportedKeywords.ts +74 -0
  212. package/src/json-schema/index.ts +3 -0
  213. package/src/json-schema/jsonSchemaToTsType.ts +188 -0
  214. package/src/json-schema/keywords.ts +79 -0
  215. package/src/lax/LaxSchemaConverter.ts +3 -3
  216. package/src/lax/index.ts +2 -2
  217. package/src/model/index.ts +3 -3
  218. package/src/model/modelUtils.ts +1 -1
  219. package/src/model/resolveDefaultColumnType.ts +3 -1
  220. package/src/schemaNormalizer.ts +1 -1
  221. package/src/type-schema/acl.ts +1 -1
  222. package/src/type-schema/condition.ts +5 -0
  223. package/src/type-schema/index.ts +5 -5
  224. package/src/type-schema/model.ts +12 -0
  225. package/src/type-schema/settings.ts +1 -0
  226. package/src/type-schema/where.ts +2 -2
  227. package/src/utils/index.ts +3 -3
  228. package/src/validation/AclValidator.ts +4 -4
  229. package/src/validation/ActionsValidator.ts +4 -4
  230. package/src/validation/MembershipResolver.ts +3 -3
  231. package/src/validation/ModelValidator.ts +80 -2
  232. package/src/validation/SchemaValidator.ts +5 -5
  233. package/src/validation/ValidationValidator.ts +2 -2
  234. package/src/validation/errors.ts +2 -0
  235. package/src/validation/index.ts +6 -6
  236. package/tests/cases/unit/conditionSchema.test.ts +1 -1
  237. package/tests/cases/unit/deepCompare.test.ts +1 -1
  238. package/tests/cases/unit/jsonSchemaKeywords.test.ts +91 -0
  239. package/tests/cases/unit/jsonSchemaToTsType.test.ts +55 -0
  240. package/tests/cases/unit/laxSchemaConverter.test.ts +1 -1
  241. package/tests/cases/unit/membershipResolver.test.ts +1 -1
  242. package/tests/cases/unit/modelSchema.test.ts +1 -1
  243. package/tests/cases/unit/modelValidator.test.ts +246 -1
  244. package/tests/cases/unit/numericColumnCodeGenerator.test.ts +69 -0
  245. package/tests/cases/unit/printJsvalue.test.ts +1 -1
  246. package/tests/cases/unit/tsDefinitionGenerator.test.ts +46 -9
  247. 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'
@@ -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'
@@ -1,4 +1,4 @@
1
- import { assertNever, isIt } from '../utils'
1
+ import { assertNever, isIt } from '../utils/index.js'
2
2
  import { Model } from '@contember/schema'
3
3
 
4
4
  export enum ModelErrorCode {
@@ -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:
@@ -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] ?? {}
@@ -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:
@@ -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'
@@ -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
  }),
@@ -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 }: {
@@ -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) {}
@@ -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'
@@ -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'
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { conditionSchema } from '../../../src/type-schema'
2
+ import { conditionSchema } from '../../../src/type-schema/index.js'
3
3
  import { Model } from '@contember/schema'
4
4
 
5
5
  describe('condition schema', () => {
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { compareArraysIgnoreOrder } from '../../../src'
2
+ import { compareArraysIgnoreOrder } from '../../../src/index.js'
3
3
 
4
4
  describe('deep compare', () => {
5
5
  test('compare array ignore order - success', () => {