@powerlines/schema 0.11.28 → 0.11.37

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 (82) hide show
  1. package/dist/bundle.cjs +34 -20
  2. package/dist/bundle.d.cts +3 -3
  3. package/dist/bundle.d.cts.map +1 -1
  4. package/dist/bundle.d.mts +3 -3
  5. package/dist/bundle.d.mts.map +1 -1
  6. package/dist/bundle.mjs +35 -21
  7. package/dist/bundle.mjs.map +1 -1
  8. package/dist/codegen.cjs +55 -8
  9. package/dist/codegen.d.cts +12 -6
  10. package/dist/codegen.d.cts.map +1 -1
  11. package/dist/codegen.d.mts +12 -6
  12. package/dist/codegen.d.mts.map +1 -1
  13. package/dist/codegen.mjs +52 -8
  14. package/dist/codegen.mjs.map +1 -1
  15. package/dist/constants.cjs +42 -0
  16. package/dist/constants.d.cts +15 -0
  17. package/dist/constants.d.cts.map +1 -0
  18. package/dist/constants.d.mts +15 -0
  19. package/dist/constants.d.mts.map +1 -0
  20. package/dist/constants.mjs +39 -0
  21. package/dist/constants.mjs.map +1 -0
  22. package/dist/extract.cjs +144 -95
  23. package/dist/extract.d.cts +34 -69
  24. package/dist/extract.d.cts.map +1 -1
  25. package/dist/extract.d.mts +34 -69
  26. package/dist/extract.d.mts.map +1 -1
  27. package/dist/extract.mjs +144 -95
  28. package/dist/extract.mjs.map +1 -1
  29. package/dist/helpers.cjs +87 -0
  30. package/dist/helpers.d.cts +48 -0
  31. package/dist/helpers.d.cts.map +1 -0
  32. package/dist/helpers.d.mts +48 -0
  33. package/dist/helpers.d.mts.map +1 -0
  34. package/dist/helpers.mjs +84 -0
  35. package/dist/helpers.mjs.map +1 -0
  36. package/dist/index.cjs +29 -5
  37. package/dist/index.d.cts +8 -5
  38. package/dist/index.d.mts +8 -5
  39. package/dist/index.mjs +7 -4
  40. package/dist/metadata.cjs +80 -0
  41. package/dist/metadata.d.cts +52 -0
  42. package/dist/metadata.d.cts.map +1 -0
  43. package/dist/metadata.d.mts +52 -0
  44. package/dist/metadata.d.mts.map +1 -0
  45. package/dist/metadata.mjs +76 -0
  46. package/dist/metadata.mjs.map +1 -0
  47. package/dist/persistence.cjs +75 -0
  48. package/dist/persistence.d.cts +47 -0
  49. package/dist/persistence.d.cts.map +1 -0
  50. package/dist/persistence.d.mts +47 -0
  51. package/dist/persistence.d.mts.map +1 -0
  52. package/dist/persistence.mjs +71 -0
  53. package/dist/persistence.mjs.map +1 -0
  54. package/dist/reflection.cjs +292 -299
  55. package/dist/reflection.d.cts +3 -16
  56. package/dist/reflection.d.cts.map +1 -1
  57. package/dist/reflection.d.mts +3 -16
  58. package/dist/reflection.d.mts.map +1 -1
  59. package/dist/reflection.mjs +291 -299
  60. package/dist/reflection.mjs.map +1 -1
  61. package/dist/resolve.cjs +7 -7
  62. package/dist/resolve.mjs +7 -7
  63. package/dist/resolve.mjs.map +1 -1
  64. package/dist/type-checks.cjs +126 -25
  65. package/dist/type-checks.d.cts +45 -23
  66. package/dist/type-checks.d.cts.map +1 -1
  67. package/dist/type-checks.d.mts +45 -23
  68. package/dist/type-checks.d.mts.map +1 -1
  69. package/dist/type-checks.mjs +125 -25
  70. package/dist/type-checks.mjs.map +1 -1
  71. package/dist/types.d.cts +237 -95
  72. package/dist/types.d.cts.map +1 -1
  73. package/dist/types.d.mts +237 -95
  74. package/dist/types.d.mts.map +1 -1
  75. package/package.json +24 -8
  76. package/dist/jtd.cjs +0 -385
  77. package/dist/jtd.d.cts +0 -15
  78. package/dist/jtd.d.cts.map +0 -1
  79. package/dist/jtd.d.mts +0 -15
  80. package/dist/jtd.d.mts.map +0 -1
  81. package/dist/jtd.mjs +0 -384
  82. package/dist/jtd.mjs.map +0 -1
@@ -1,236 +1,259 @@
1
+ import { isJsonSchema, isJsonSchemaObject, isNullOnlyJsonSchema } from "./type-checks.mjs";
2
+ import defu from "defu";
3
+ import { isSetArray, isSetObject, isSetString, isUndefined } from "@stryke/type-checks";
1
4
  import { ReflectionClass, ReflectionKind, TypeNumberBrand } from "@powerlines/deepkit/vendor/type";
2
- import { isSetArray, isSetObject, isSetString } from "@stryke/type-checks";
3
5
 
4
6
  //#region src/reflection.ts
5
7
  /**
6
- * Maps a Deepkit numeric `brand` to the JTD numeric `type` keyword that best preserves the underlying width and signedness.
8
+ * Maps a Deepkit numeric `brand` to JSON Schema `type` and `format`.
7
9
  *
8
- * @param brand - The Deepkit `TypeNumberBrand` of a numeric reflection, or `undefined` when no brand is set.
9
- * @returns The JTD numeric `type` keyword to use.
10
+ * @remarks
11
+ * This function takes a `TypeNumberBrand` (which represents specific numeric types in Deepkit, such as `integer`, `float`, `int8`, etc.) and returns a corresponding JSON Schema fragment that includes the appropriate `type`, `format`, and any relevant keywords (like `multipleOf` for integers). If the brand is not recognized, it defaults to a generic JSON Schema for numbers.
12
+ *
13
+ * @param brand - The Deepkit numeric brand to convert.
14
+ * @return A JSON Schema fragment representing the numeric type corresponding to the provided brand.
10
15
  */
11
- function numberBrandToJtdType(brand) {
16
+ function numberBrandToJsonSchema(brand) {
12
17
  switch (brand) {
13
- case TypeNumberBrand.integer: return "int32";
14
- case TypeNumberBrand.int8: return "int8";
15
- case TypeNumberBrand.uint8: return "uint8";
16
- case TypeNumberBrand.int16: return "int16";
17
- case TypeNumberBrand.uint16: return "uint16";
18
- case TypeNumberBrand.int32: return "int32";
19
- case TypeNumberBrand.uint32: return "uint32";
18
+ case TypeNumberBrand.integer: return {
19
+ type: "integer",
20
+ format: "int32",
21
+ multipleOf: 1
22
+ };
23
+ case TypeNumberBrand.int8: return {
24
+ type: "integer",
25
+ format: "int8",
26
+ multipleOf: 1
27
+ };
28
+ case TypeNumberBrand.uint8: return {
29
+ type: "integer",
30
+ format: "uint8",
31
+ multipleOf: 1
32
+ };
33
+ case TypeNumberBrand.int16: return {
34
+ type: "integer",
35
+ format: "int16",
36
+ multipleOf: 1
37
+ };
38
+ case TypeNumberBrand.uint16: return {
39
+ type: "integer",
40
+ format: "uint16",
41
+ multipleOf: 1
42
+ };
43
+ case TypeNumberBrand.int32: return {
44
+ type: "integer",
45
+ format: "int32",
46
+ multipleOf: 1
47
+ };
48
+ case TypeNumberBrand.uint32: return {
49
+ type: "integer",
50
+ format: "uint32",
51
+ multipleOf: 1
52
+ };
20
53
  case TypeNumberBrand.float:
21
- case TypeNumberBrand.float32: return "float32";
22
- case TypeNumberBrand.float64: return "float64";
54
+ case TypeNumberBrand.float32: return {
55
+ type: "number",
56
+ format: "float"
57
+ };
58
+ case TypeNumberBrand.float64: return {
59
+ type: "number",
60
+ format: "double"
61
+ };
23
62
  case void 0:
24
- default: return "float64";
63
+ default: return { type: "number" };
25
64
  }
26
65
  }
66
+ function withReflectionTags(reflection, schema) {
67
+ if (!isSetObject(reflection?.tags)) return schema;
68
+ const tags = reflection.tags;
69
+ if (isSetString(tags.title)) schema.title = tags.title;
70
+ if (isSetArray(tags.alias)) schema.alias = tags.alias;
71
+ if (!isUndefined(tags.hidden)) schema.hidden = tags.hidden;
72
+ if (!isUndefined(tags.ignore)) schema.ignore = tags.ignore;
73
+ if (!isUndefined(tags.internal)) schema.internal = tags.internal;
74
+ if (!isUndefined(tags.runtime)) schema.runtime = tags.runtime;
75
+ if (!isUndefined(tags.readonly)) schema.readOnly = tags.readonly;
76
+ return schema;
77
+ }
78
+ function withNullable(schema, nullable) {
79
+ if (!nullable) return schema;
80
+ const types = Array.isArray(schema.type) ? [...schema.type] : schema.type ? [schema.type] : [];
81
+ if (!types.includes("null")) types.push("null");
82
+ return {
83
+ ...schema,
84
+ type: types.length === 1 ? types[0] : types
85
+ };
86
+ }
27
87
  /**
28
- * Converts a Deepkit type reflection into a JSON Type Definition (RFC 8927) form suitable for AJV's JTD validator.
29
- *
30
- * @remarks
31
- * Some TypeScript constructs have no direct JTD equivalent and are handled with the closest available form:
32
- *
33
- * - `null` and `undefined` become the empty JTD form with `nullable: true`.
34
- * - Unions of primitives that cannot be expressed as a JTD enum collapse to the empty form (which validates any value).
35
- * - String/number/bigint literal unions are emitted as a JTD enum (non-string members are stringified, as JTD requires string enum members).
36
- * - Tuples are emitted as a JTD elements form whose element schema is the single tuple member type, or the empty schema for mixed tuples.
37
- * - `Date` is emitted as `{ type: "timestamp" }`.
38
- * - Discriminated unions of object literals (a shared string-literal tag property) are emitted as a JTD discriminator form.
39
- *
40
- * @param reflection - The Deepkit type reflection to convert.
41
- * @returns The corresponding JTD form, or `undefined` if the type cannot be represented.
88
+ * Converts a Deepkit type reflection into a JSON Schema (draft-07) fragment.
42
89
  */
43
90
  function reflectionToJsonSchema(reflection) {
44
- return reflectionToJtd(reflection);
91
+ return reflectionToJsonSchemaInner(reflection);
45
92
  }
46
- /**
47
- * Internal worker that performs the recursive Deepkit reflection → JTD conversion.
48
- *
49
- * @param reflection - The Deepkit type reflection to convert.
50
- * @returns The corresponding JTD form, or `undefined` if the type cannot be represented.
51
- */
52
- function reflectionToJtd(reflection) {
53
- const schema = {};
54
- if (isSetObject(reflection?.tags)) {
55
- const tags = reflection.tags;
56
- schema.metadata = schema.metadata ?? {};
57
- if (tags.readonly === true) schema.metadata.isReadonly = true;
58
- if (tags.ignore === true) schema.metadata.isIgnored = true;
59
- if (tags.internal === true) schema.metadata.isInternal = true;
60
- if (tags.runtime === true) schema.metadata.isRuntime = true;
61
- if (tags.hidden === true) schema.metadata.isHidden = true;
62
- if (isSetArray(tags.alias)) schema.metadata.alias = tags.alias;
63
- if (isSetString(tags.title)) schema.metadata.title = tags.title;
64
- }
93
+ function reflectionToJsonSchemaInner(reflection) {
65
94
  switch (reflection.kind) {
66
95
  case ReflectionKind.any:
67
96
  case ReflectionKind.unknown:
68
97
  case ReflectionKind.void:
69
- case ReflectionKind.object: return {};
98
+ case ReflectionKind.object: return withReflectionTags(reflection, { name: reflection.typeName });
70
99
  case ReflectionKind.never: return;
71
100
  case ReflectionKind.undefined:
72
- case ReflectionKind.null: return { nullable: true };
73
- case ReflectionKind.string: return {
74
- ...schema,
75
- type: "string"
76
- };
77
- case ReflectionKind.boolean: return {
78
- ...schema,
79
- type: "boolean"
80
- };
81
- case ReflectionKind.number: return {
82
- ...schema,
83
- type: numberBrandToJtdType(reflection.brand)
84
- };
85
- case ReflectionKind.bigint: return {
86
- ...schema,
87
- type: "float64"
88
- };
89
- case ReflectionKind.regexp: return {
90
- ...schema,
91
- type: "string"
92
- };
101
+ case ReflectionKind.null: return withReflectionTags(reflection, {
102
+ type: "null",
103
+ name: reflection.typeName,
104
+ nullable: true
105
+ });
106
+ case ReflectionKind.string: return withReflectionTags(reflection, {
107
+ type: "string",
108
+ name: reflection.typeName
109
+ });
110
+ case ReflectionKind.boolean: return withReflectionTags(reflection, {
111
+ type: "boolean",
112
+ name: reflection.typeName
113
+ });
114
+ case ReflectionKind.number: return withReflectionTags(reflection, numberBrandToJsonSchema(reflection.brand));
115
+ case ReflectionKind.bigint: return withReflectionTags(reflection, {
116
+ type: "integer",
117
+ name: reflection.typeName,
118
+ format: "int64",
119
+ multipleOf: 1
120
+ });
121
+ case ReflectionKind.regexp: return withReflectionTags(reflection, {
122
+ type: "string",
123
+ name: reflection.typeName,
124
+ format: "regex",
125
+ contentMediaType: "text/regex"
126
+ });
93
127
  case ReflectionKind.literal: {
94
128
  const { literal } = reflection;
95
- if (typeof literal === "string") return {
96
- ...schema,
97
- enum: [literal]
98
- };
99
- if (typeof literal === "number" || typeof literal === "bigint") return {
100
- ...schema,
101
- enum: [String(literal)]
102
- };
103
- if (typeof literal === "boolean") return {
104
- ...schema,
105
- type: "boolean"
106
- };
107
- if (literal instanceof RegExp) return {
108
- ...schema,
109
- type: "string"
110
- };
111
- return schema;
129
+ if (typeof literal === "string" || typeof literal === "number" || typeof literal === "boolean") return withReflectionTags(reflection, {
130
+ type: typeof literal,
131
+ name: reflection.typeName,
132
+ const: literal
133
+ });
134
+ if (typeof literal === "bigint") return withReflectionTags(reflection, {
135
+ type: "integer",
136
+ name: reflection.typeName,
137
+ format: "int64",
138
+ multipleOf: 1,
139
+ const: String(literal)
140
+ });
141
+ if (literal instanceof RegExp) return withReflectionTags(reflection, {
142
+ type: "string",
143
+ name: reflection.typeName,
144
+ format: "regex",
145
+ const: literal.source
146
+ });
147
+ return withReflectionTags(reflection, { name: reflection.typeName });
112
148
  }
113
- case ReflectionKind.templateLiteral: return {
114
- ...schema,
115
- type: "string"
116
- };
149
+ case ReflectionKind.templateLiteral: return withReflectionTags(reflection, { type: "string" });
117
150
  case ReflectionKind.enum: {
118
- const values = reflection.values.filter((value) => typeof value === "string" || typeof value === "number").map((value) => String(value));
119
- const unique = Array.from(new Set(values));
120
- if (unique.length === 0) return schema;
121
- return {
122
- ...schema,
123
- enum: unique
124
- };
151
+ const values = reflection.values.filter((value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean");
152
+ if (values.length === 0) return withReflectionTags(reflection, {
153
+ name: reflection.typeName,
154
+ description: reflection.description
155
+ });
156
+ return withReflectionTags(reflection, {
157
+ name: reflection.typeName,
158
+ description: reflection.description,
159
+ enum: values
160
+ });
125
161
  }
126
162
  case ReflectionKind.array: {
127
- const items = reflectionToJtd(reflection.type);
128
- return {
129
- ...schema,
130
- elements: items ?? {}
131
- };
163
+ const items = reflectionToJsonSchemaInner(reflection.type);
164
+ return withReflectionTags(reflection, {
165
+ type: "array",
166
+ name: reflection.typeName,
167
+ items: items ?? {}
168
+ });
132
169
  }
133
170
  case ReflectionKind.tuple: {
134
- const items = reflection.types.map((member) => reflectionToJtd(member.type)).filter((item) => item !== void 0);
135
- if (items.length === 0) return {
136
- ...schema,
137
- elements: {}
138
- };
139
- if (items.length === 1) return {
140
- ...schema,
141
- elements: items[0]
142
- };
143
- return {
144
- ...schema,
145
- elements: {}
146
- };
171
+ const items = reflection.types.map((member) => reflectionToJsonSchemaInner(member.type)).filter((item) => item !== void 0);
172
+ if (items.length <= 1) return withReflectionTags(reflection, {
173
+ type: "array",
174
+ name: reflection.typeName,
175
+ items: items[0] ?? {}
176
+ });
177
+ return withReflectionTags(reflection, {
178
+ type: "array",
179
+ name: reflection.typeName,
180
+ items,
181
+ minItems: items.length,
182
+ maxItems: items.length
183
+ });
147
184
  }
148
185
  case ReflectionKind.union: {
149
- const branches = reflection.types.map((inner) => reflectionToJtd(inner)).filter((item) => item !== void 0);
186
+ const branches = reflection.types.map((inner) => reflectionToJsonSchemaInner(inner)).filter(isJsonSchema);
150
187
  const nullable = reflection.types.some((inner) => inner.kind === ReflectionKind.null || inner.kind === ReflectionKind.undefined);
151
- const nonNull = branches.filter((b) => !isPureNullable(b));
152
- if (nonNull.length === 0) return {
153
- ...schema,
188
+ const nonNull = branches.filter((branch) => branch.type !== "null" && !isNullOnlyJsonSchema(branch));
189
+ if (nonNull.length === 0) return withReflectionTags(reflection, {
190
+ type: "null",
154
191
  nullable: true
155
- };
156
- if (nonNull.length === 1) {
157
- const only = nonNull[0];
158
- if (nullable) only.nullable = true;
159
- return {
160
- ...schema,
161
- ...only
162
- };
163
- }
164
- if (nonNull.every(isEnumForm)) {
165
- const merged = Array.from(new Set(nonNull.flatMap((b) => b.enum)));
166
- const form = {
167
- ...schema,
168
- enum: merged
169
- };
170
- if (nullable) form.nullable = true;
171
- return {
172
- ...schema,
173
- ...form
174
- };
175
- }
192
+ });
193
+ if (nonNull.length === 1) return withNullable(withReflectionTags(reflection, {
194
+ name: reflection.typeName,
195
+ ...nonNull[0]
196
+ }), nullable);
197
+ const enumValues = nonNull.map((branch) => branch.const).filter((value) => value !== void 0);
198
+ if (enumValues.length === nonNull.length) return withNullable(withReflectionTags(reflection, {
199
+ name: reflection.typeName,
200
+ enum: enumValues
201
+ }), nullable);
176
202
  const discriminator = tryReflectionDiscriminator(reflection.types);
177
- if (discriminator) {
178
- if (nullable) discriminator.nullable = true;
179
- return {
180
- ...schema,
181
- ...discriminator
182
- };
183
- }
184
- const fallback = {};
185
- if (nullable) fallback.nullable = true;
186
- return {
187
- ...schema,
188
- ...fallback
189
- };
203
+ if (discriminator) return withNullable(withReflectionTags(reflection, {
204
+ name: reflection.typeName,
205
+ ...discriminator
206
+ }), nullable);
207
+ return withNullable(withReflectionTags(reflection, {
208
+ name: reflection.typeName,
209
+ anyOf: nonNull
210
+ }), nullable);
190
211
  }
191
212
  case ReflectionKind.intersection: {
192
- const members = reflection.types.map((inner) => reflectionToJtd(inner)).filter((item) => item !== void 0);
213
+ const members = reflection.types.map((inner) => reflectionToJsonSchemaInner(inner)).filter((item) => item !== void 0);
193
214
  if (members.length === 0) return;
194
- if (members.length === 1) return {
195
- ...schema,
196
- ...members[0]
197
- };
198
- if (members.every((member) => member && isPropertiesForm(member))) return mergePropertiesForms(members);
199
- return {
200
- ...schema,
215
+ if (members.length === 1) return withReflectionTags(reflection, {
216
+ name: reflection.typeName,
201
217
  ...members[0]
202
- };
218
+ });
219
+ if (members.every(isJsonSchemaObject)) return withReflectionTags(reflection, {
220
+ name: reflection.typeName,
221
+ ...mergeObjectSchemas(members)
222
+ });
223
+ return withReflectionTags(reflection, {
224
+ name: reflection.typeName,
225
+ allOf: members
226
+ });
203
227
  }
204
- case ReflectionKind.promise: return reflectionToJtd(reflection.type);
205
- case ReflectionKind.objectLiteral: return objectReflectionToJtd(reflection);
228
+ case ReflectionKind.promise: return reflectionToJsonSchemaInner(reflection.type);
229
+ case ReflectionKind.objectLiteral: return objectReflectionToJsonSchema(reflection);
206
230
  case ReflectionKind.class: switch (reflection.classType?.name) {
207
- case "Date": return {
208
- ...schema,
209
- type: "timestamp"
210
- };
211
- case "RegExp": return {
212
- ...schema,
213
- type: "string"
214
- };
215
- case "URL": return {
216
- ...schema,
217
- type: "string"
218
- };
231
+ case "Date": return withReflectionTags(reflection, {
232
+ type: "string",
233
+ format: "date-time"
234
+ });
235
+ case "RegExp": return withReflectionTags(reflection, {
236
+ type: "string",
237
+ format: "regex"
238
+ });
239
+ case "URL": return withReflectionTags(reflection, {
240
+ type: "string",
241
+ format: "uri"
242
+ });
219
243
  case "Set": {
220
244
  const itemType = reflection.arguments?.[0];
221
- const items = itemType ? reflectionToJtd(itemType) : void 0;
222
- return {
223
- ...schema,
224
- elements: items ?? {}
225
- };
245
+ return withReflectionTags(reflection, {
246
+ type: "array",
247
+ items: (itemType ? reflectionToJsonSchemaInner(itemType) : void 0) ?? {},
248
+ uniqueItems: true
249
+ });
226
250
  }
227
251
  case "Map": {
228
252
  const valueType = reflection.arguments?.[1];
229
- const values = valueType ? reflectionToJtd(valueType) : void 0;
230
- return {
231
- ...schema,
232
- values: values ?? {}
233
- };
253
+ return withReflectionTags(reflection, {
254
+ type: "object",
255
+ additionalProperties: (valueType ? reflectionToJsonSchemaInner(valueType) : void 0) ?? true
256
+ });
234
257
  }
235
258
  case "Uint8Array":
236
259
  case "Uint8ClampedArray":
@@ -242,12 +265,17 @@ function reflectionToJtd(reflection) {
242
265
  case "Float32Array":
243
266
  case "Float64Array":
244
267
  case "BigInt64Array":
245
- case "BigUint64Array": return {
246
- ...schema,
247
- type: "string"
248
- };
268
+ case "BigUint64Array": return withReflectionTags(reflection, {
269
+ type: "string",
270
+ format: "byte",
271
+ contentEncoding: "base64"
272
+ });
249
273
  case void 0:
250
- default: return objectReflectionToJtd(reflection);
274
+ default: return withReflectionTags(reflection, {
275
+ name: reflection.typeName,
276
+ description: reflection.description,
277
+ ...objectReflectionToJsonSchema(reflection)
278
+ });
251
279
  }
252
280
  case ReflectionKind.symbol:
253
281
  case ReflectionKind.property:
@@ -266,72 +294,26 @@ function reflectionToJtd(reflection) {
266
294
  default: return;
267
295
  }
268
296
  }
269
- /**
270
- * Tests whether a JTD form is an enum form.
271
- *
272
- * @param form - The JTD form to inspect.
273
- * @returns `true` if the form is a JTD enum form.
274
- */
275
- function isEnumForm(form) {
276
- return Array.isArray(form.enum);
277
- }
278
- /**
279
- * Tests whether a JTD form is a properties form (object).
280
- *
281
- * @param form - The JTD form to inspect.
282
- * @returns `true` if the form is a JTD properties form.
283
- */
284
- function isPropertiesForm(form) {
285
- return "properties" in form || "optionalProperties" in form;
286
- }
287
- /**
288
- * Tests whether a JTD form is the empty `{ nullable: true }` placeholder.
289
- *
290
- * @param form - The JTD form to inspect.
291
- * @returns `true` if the form has no shape constraints beyond `nullable`.
292
- */
293
- function isPureNullable(form) {
294
- return Object.keys(form).filter((k) => k !== "nullable" && k !== "metadata").length === 0 && form.nullable === true;
295
- }
296
- /**
297
- * Shallow-merges two JTD properties forms, unioning their `properties` and `optionalProperties` maps.
298
- *
299
- * @param forms - The JTD properties forms to merge.
300
- * @returns The merged JTD properties form.
301
- */
302
- function mergePropertiesForms(forms) {
297
+ function mergeObjectSchemas(schemas) {
303
298
  const merged = {
299
+ type: "object",
304
300
  properties: {},
305
- optionalProperties: {}
301
+ required: []
306
302
  };
307
- for (const form of forms) {
308
- const p = form.properties;
309
- const o = form.optionalProperties;
310
- if (p) Object.assign(merged.properties, p);
311
- if (o) Object.assign(merged.optionalProperties, o);
312
- if (form.additionalProperties) merged.additionalProperties = true;
303
+ for (const schema of schemas) {
304
+ if (schema.properties) merged.properties = defu(merged.properties, schema.properties);
305
+ if (schema.required) merged.required = Array.from(new Set([...merged.required ?? [], ...schema.required]));
306
+ if (schema.additionalProperties !== void 0) merged.additionalProperties = schema.additionalProperties;
313
307
  }
314
- const hasProperties = Object.keys(merged.properties).length > 0;
315
- const hasOptional = Object.keys(merged.optionalProperties).length > 0;
316
- const result = {};
317
- if (hasProperties) result.properties = merged.properties;
318
- else if (!hasOptional) result.properties = {};
319
- if (hasOptional) result.optionalProperties = merged.optionalProperties;
320
- if (merged.additionalProperties) result.additionalProperties = true;
321
- return result;
308
+ if ((merged.required?.length ?? 0) === 0) delete merged.required;
309
+ return merged;
322
310
  }
323
- /**
324
- * Detects whether a Deepkit union represents a tagged union and, when so, emits the corresponding JTD discriminator form.
325
- *
326
- * @param types - The Deepkit reflection types that make up the union branches.
327
- * @returns A JTD discriminator form if every non-null branch is an object literal that shares a string-literal tag property, otherwise `undefined`.
328
- */
329
311
  function tryReflectionDiscriminator(types) {
330
312
  const nonNullTypes = types.filter((t) => t.kind !== ReflectionKind.null && t.kind !== ReflectionKind.undefined);
331
313
  const objectBranches = nonNullTypes.filter((t) => t.kind === ReflectionKind.objectLiteral || t.kind === ReflectionKind.class);
332
314
  if (objectBranches.length < 2 || objectBranches.length !== nonNullTypes.length) return;
333
315
  let tagKey;
334
- const mapping = {};
316
+ const branches = [];
335
317
  for (const branch of objectBranches) {
336
318
  const literalProps = [];
337
319
  for (const member of branch.types) if ((member.kind === ReflectionKind.property || member.kind === ReflectionKind.propertySignature) && typeof member.name === "string" && member.type.kind === ReflectionKind.literal && typeof member.type.literal === "string") literalProps.push({
@@ -342,67 +324,77 @@ function tryReflectionDiscriminator(types) {
342
324
  const first = literalProps[0];
343
325
  if (!tagKey) tagKey = first.name;
344
326
  else if (tagKey !== first.name) return;
345
- const body = objectReflectionToJtd({
327
+ const body = objectReflectionToJsonSchema({
346
328
  ...branch,
347
329
  types: branch.types.filter((member) => !((member.kind === ReflectionKind.property || member.kind === ReflectionKind.propertySignature) && member.name === tagKey))
348
330
  });
349
- if (!body || !isPropertiesForm(body)) return;
350
- mapping[first.literal] = body;
331
+ if (!body || !isJsonSchemaObject(body)) return;
332
+ branches.push({
333
+ type: "object",
334
+ properties: {
335
+ [tagKey]: { const: first.literal },
336
+ ...body.properties ?? {}
337
+ },
338
+ required: [tagKey, ...body.required ?? []],
339
+ additionalProperties: body.additionalProperties ?? false
340
+ });
351
341
  }
352
342
  if (!tagKey) return;
353
343
  return {
354
- discriminator: tagKey,
355
- mapping
344
+ oneOf: branches,
345
+ discriminator: { propertyName: tagKey }
356
346
  };
357
347
  }
358
- /**
359
- * Internal worker that produces a JTD properties form (or `values` form for index signatures alone) from a Deepkit object-like type.
360
- *
361
- * @param type - The class or object literal type whose members should be serialized.
362
- * @returns A JTD properties or values form describing the type's members.
363
- */
364
- function objectReflectionToJtd(type) {
348
+ function objectReflectionToJsonSchema(type) {
365
349
  const reflection = ReflectionClass.from(type);
366
- const schema = {};
367
- schema.metadata = schema.metadata ?? {};
368
- schema.metadata.isReadonly = reflection.isReadonly();
369
- schema.metadata.isIgnored = reflection.isIgnored();
370
- schema.metadata.isInternal = reflection.isInternal();
371
- schema.metadata.isRuntime = reflection.isRuntime();
372
- schema.metadata.isHidden = reflection.isHidden();
373
- if (isSetString(reflection.databaseSchemaName)) schema.metadata.table = reflection.databaseSchemaName;
374
- if (isSetString(reflection.getDescription())) schema.metadata.description = reflection.getDescription();
375
- if (isSetArray(reflection.getAlias())) schema.metadata.alias = reflection.getAlias();
376
- if (isSetString(reflection.getTitle())) schema.metadata.title = reflection.getTitle();
377
- const properties = {};
378
- const optionalProperties = {};
379
- for (const propertyReflection of reflection.getProperties()) if (propertyReflection.getKind() === ReflectionKind.indexSignature) {
380
- const valueSchema = reflectionToJtd(propertyReflection.type);
381
- if (valueSchema) return {
382
- ...schema,
383
- values: valueSchema,
384
- additionalProperties: true
385
- };
386
- } else {
387
- const property = reflectionToJtd(propertyReflection.type);
350
+ const schema = {
351
+ type: "object",
352
+ name: reflection.getName(),
353
+ description: reflection.getDescription(),
354
+ properties: {},
355
+ required: [],
356
+ readOnly: reflection.isReadonly(),
357
+ ignore: reflection.isIgnored(),
358
+ internal: reflection.isInternal(),
359
+ runtime: reflection.isRuntime(),
360
+ hidden: reflection.isHidden(),
361
+ primaryKey: reflection.getPrimaries().map((primary) => primary.getNameAsString()),
362
+ ...isSetString(reflection.databaseSchemaName) ? { databaseSchemaName: reflection.databaseSchemaName } : {},
363
+ ...isSetString(reflection.getName()) ? { name: reflection.getName() } : {},
364
+ ...isSetString(reflection.getDescription()) ? { description: reflection.getDescription() } : {},
365
+ ...isSetArray(reflection.getAlias()) ? { alias: reflection.getAlias() } : {},
366
+ ...isSetString(reflection.getTitle()) ? { title: reflection.getTitle() } : {}
367
+ };
368
+ for (const propertyReflection of reflection.getProperties()) {
369
+ if (propertyReflection.getKind() === ReflectionKind.indexSignature) {
370
+ schema.additionalProperties = reflectionToJsonSchemaInner(propertyReflection.type) ?? true;
371
+ continue;
372
+ }
373
+ let property = reflectionToJsonSchemaInner(propertyReflection.type);
388
374
  if (!property) continue;
389
- property.metadata = property.metadata ?? {};
390
- property.metadata.isReadonly = propertyReflection.isReadonly();
391
- property.metadata.isIgnored = propertyReflection.isIgnored();
392
- property.metadata.isInternal = propertyReflection.isInternal();
393
- property.metadata.isRuntime = propertyReflection.isRuntime();
394
- property.metadata.isPrimaryKey = propertyReflection.isPrimaryKey();
395
- property.metadata.isHidden = propertyReflection.isHidden();
396
- if (propertyReflection.hasDefault()) property.metadata.default = propertyReflection.getDefaultValue();
397
- if (isSetString(propertyReflection.getDescription())) property.metadata.description = propertyReflection.getDescription();
398
- if (isSetArray(propertyReflection.getAlias())) property.metadata.alias = propertyReflection.getAlias();
399
- if (isSetString(propertyReflection.getTitle())) property.metadata.title = propertyReflection.getTitle();
400
- if (propertyReflection.isOptional()) optionalProperties[propertyReflection.name] = property;
401
- else properties[propertyReflection.name] = property;
375
+ property = {
376
+ ...property,
377
+ name: propertyReflection.getNameAsString(),
378
+ description: propertyReflection.getDescription(),
379
+ readOnly: propertyReflection.isReadonly(),
380
+ ignore: propertyReflection.isIgnored(),
381
+ internal: propertyReflection.isInternal(),
382
+ runtime: propertyReflection.isRuntime(),
383
+ hidden: propertyReflection.isHidden(),
384
+ visibility: propertyReflection.isPublic() ? "public" : propertyReflection.isProtected() ? "protected" : propertyReflection.isPrivate() ? "private" : void 0,
385
+ ...propertyReflection.hasDefault() ? { default: propertyReflection.getDefaultValue() } : {},
386
+ ...isSetArray(propertyReflection.getGroups()) ? { tags: propertyReflection.getGroups() } : {},
387
+ ...isSetArray(propertyReflection.getAlias()) ? { alias: propertyReflection.getAlias() } : {},
388
+ ...isSetString(propertyReflection.getTitle()) ? { title: propertyReflection.getTitle() } : {}
389
+ };
390
+ if (propertyReflection.isNullable()) property = withNullable(property, true);
391
+ schema.properties ??= {};
392
+ schema.properties[propertyReflection.name] = property;
393
+ if (!propertyReflection.isOptional()) {
394
+ schema.required ??= [];
395
+ schema.required.push(propertyReflection.name);
396
+ }
402
397
  }
403
- if (Object.keys(properties).length > 0) schema.properties = properties;
404
- else if (Object.keys(optionalProperties).length > 0) schema.optionalProperties = optionalProperties;
405
- else schema.properties = {};
406
398
  return schema;
407
399
  }
408
400