@rvoh/psychic 0.37.0-beta.3 → 0.37.0-beta.5

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 (62) hide show
  1. package/dist/cjs/src/bin/index.js +2 -2
  2. package/dist/cjs/src/controller/helpers/isPaginatedResult.js +5 -2
  3. package/dist/cjs/src/error/openapi/AttemptedToDeriveDescendentSerializersFromNonSerializer.js +16 -0
  4. package/dist/cjs/src/error/openapi/ExpectedSerializerForRendersOneOrManyOption.js +36 -0
  5. package/dist/cjs/src/error/openapi/NoSerializerFoundForRendersOneAndMany.js +16 -0
  6. package/dist/cjs/src/error/openapi/NonSerializerPassedToSerializerOpenapiRenderer.js +16 -0
  7. package/dist/cjs/src/error/openapi/NonSerializerSerializerOverrideProvided.js +23 -0
  8. package/dist/cjs/src/error/openapi/ObjectSerializerRendersOneAndManyRequireClassType.js +19 -0
  9. package/dist/cjs/src/helpers/isObject.js +12 -0
  10. package/dist/cjs/src/helpers/pathifyNestedObject.js +5 -2
  11. package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +362 -0
  12. package/dist/cjs/src/openapi-renderer/body-segment.js +5 -3
  13. package/dist/cjs/src/openapi-renderer/endpoint.js +6 -5
  14. package/dist/cjs/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.js +34 -0
  15. package/dist/cjs/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.js +52 -0
  16. package/dist/cjs/src/openapi-renderer/helpers/dreamAttributeOpenapiShape.js +145 -0
  17. package/dist/cjs/src/openapi-renderer/helpers/isOpenapiShorthand.js +16 -0
  18. package/dist/cjs/src/openapi-renderer/helpers/maybeNullOpenapiShorthandToOpenapiShorthand.js +17 -0
  19. package/dist/cjs/src/openapi-renderer/helpers/openapiShorthandToOpenapi.js +111 -0
  20. package/dist/cjs/src/openapi-renderer/helpers/primitiveOpenapiStatementToOpenapi.js +5 -2
  21. package/dist/cjs/src/server/params.js +4 -4
  22. package/dist/esm/src/bin/index.js +1 -1
  23. package/dist/esm/src/controller/helpers/isPaginatedResult.js +1 -1
  24. package/dist/esm/src/error/openapi/AttemptedToDeriveDescendentSerializersFromNonSerializer.js +13 -0
  25. package/dist/esm/src/error/openapi/ExpectedSerializerForRendersOneOrManyOption.js +33 -0
  26. package/dist/esm/src/error/openapi/NoSerializerFoundForRendersOneAndMany.js +13 -0
  27. package/dist/esm/src/error/openapi/NonSerializerPassedToSerializerOpenapiRenderer.js +13 -0
  28. package/dist/esm/src/error/openapi/NonSerializerSerializerOverrideProvided.js +20 -0
  29. package/dist/esm/src/error/openapi/ObjectSerializerRendersOneAndManyRequireClassType.js +16 -0
  30. package/dist/esm/src/helpers/isObject.js +9 -0
  31. package/dist/esm/src/helpers/pathifyNestedObject.js +1 -1
  32. package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +356 -0
  33. package/dist/esm/src/openapi-renderer/body-segment.js +3 -1
  34. package/dist/esm/src/openapi-renderer/endpoint.js +2 -1
  35. package/dist/esm/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.js +28 -0
  36. package/dist/esm/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.js +46 -0
  37. package/dist/esm/src/openapi-renderer/helpers/dreamAttributeOpenapiShape.js +136 -0
  38. package/dist/esm/src/openapi-renderer/helpers/isOpenapiShorthand.js +10 -0
  39. package/dist/esm/src/openapi-renderer/helpers/maybeNullOpenapiShorthandToOpenapiShorthand.js +14 -0
  40. package/dist/esm/src/openapi-renderer/helpers/openapiShorthandToOpenapi.js +102 -0
  41. package/dist/esm/src/openapi-renderer/helpers/primitiveOpenapiStatementToOpenapi.js +1 -1
  42. package/dist/esm/src/server/params.js +3 -3
  43. package/dist/types/src/error/openapi/AttemptedToDeriveDescendentSerializersFromNonSerializer.d.ts +5 -0
  44. package/dist/types/src/error/openapi/ExpectedSerializerForRendersOneOrManyOption.d.ts +8 -0
  45. package/dist/types/src/error/openapi/NoSerializerFoundForRendersOneAndMany.d.ts +5 -0
  46. package/dist/types/src/error/openapi/NonSerializerPassedToSerializerOpenapiRenderer.d.ts +5 -0
  47. package/dist/types/src/error/openapi/NonSerializerSerializerOverrideProvided.d.ts +6 -0
  48. package/dist/types/src/error/openapi/ObjectSerializerRendersOneAndManyRequireClassType.d.ts +5 -0
  49. package/dist/types/src/helpers/isObject.d.ts +1 -0
  50. package/dist/types/src/helpers/typeHelpers.d.ts +2 -0
  51. package/dist/types/src/openapi-renderer/SerializerOpenapiRenderer.d.ts +25 -0
  52. package/dist/types/src/openapi-renderer/endpoint.d.ts +2 -1
  53. package/dist/types/src/openapi-renderer/helpers/allSerializersFromHandWrittenOpenapi.d.ts +2 -0
  54. package/dist/types/src/openapi-renderer/helpers/allSerializersToRefsInOpenapi.d.ts +2 -0
  55. package/dist/types/src/openapi-renderer/helpers/dreamAttributeOpenapiShape.d.ts +780 -0
  56. package/dist/types/src/openapi-renderer/helpers/isOpenapiShorthand.d.ts +1 -0
  57. package/dist/types/src/openapi-renderer/helpers/maybeNullOpenapiShorthandToOpenapiShorthand.d.ts +2 -0
  58. package/dist/types/src/openapi-renderer/helpers/openapiShorthandToOpenapi.d.ts +16 -0
  59. package/package.json +2 -2
  60. package/dist/cjs/src/helpers/typechecks.js +0 -20
  61. package/dist/esm/src/helpers/typechecks.js +0 -16
  62. package/dist/types/src/helpers/typechecks.d.ts +0 -2
@@ -0,0 +1,356 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
5
+ /* eslint-disable @typescript-eslint/no-explicit-any */
6
+ import { compact, expandStiClasses, inferSerializersFromDreamClassOrViewModelClass, isDreamSerializer, snakeify, sort, sortBy, sortObjectByKey, uniq, } from '@rvoh/dream';
7
+ import { inspect } from 'node:util';
8
+ import AttemptedToDeriveDescendentSerializersFromNonSerializer from '../error/openapi/AttemptedToDeriveDescendentSerializersFromNonSerializer.js';
9
+ import ExpectedSerializerForRendersOneOrManyOption from '../error/openapi/ExpectedSerializerForRendersOneOrManyOption.js';
10
+ import NonSerializerPassedToSerializerOpenapiRenderer from '../error/openapi/NonSerializerPassedToSerializerOpenapiRenderer.js';
11
+ import NonSerializerSerializerOverrideProvided from '../error/openapi/NonSerializerSerializerOverrideProvided.js';
12
+ import NoSerializerFoundForRendersOneAndMany from '../error/openapi/NoSerializerFoundForRendersOneAndMany.js';
13
+ import ObjectSerializerRendersOneAndManyRequireClassType from '../error/openapi/ObjectSerializerRendersOneAndManyRequireClassType.js';
14
+ import allSerializersFromHandWrittenOpenapi from './helpers/allSerializersFromHandWrittenOpenapi.js';
15
+ import allSerializersToRefsInOpenapi from './helpers/allSerializersToRefsInOpenapi.js';
16
+ import { dreamColumnOpenapiShape } from './helpers/dreamAttributeOpenapiShape.js';
17
+ import openapiShorthandToOpenapi from './helpers/openapiShorthandToOpenapi.js';
18
+ const NULL_OBJECT_OPENAPI = { type: 'null' };
19
+ export default class SerializerOpenapiRenderer {
20
+ serializer;
21
+ casing;
22
+ suppressResponseEnums;
23
+ allOfSiblings = [];
24
+ constructor(serializer, { casing = 'camel', suppressResponseEnums = false, } = {}) {
25
+ this.serializer = serializer;
26
+ if (!isDreamSerializer(this.serializer))
27
+ throw new NonSerializerPassedToSerializerOpenapiRenderer(this.serializer);
28
+ this.casing = casing;
29
+ this.suppressResponseEnums = suppressResponseEnums;
30
+ }
31
+ get globalName() {
32
+ return this.serializer.globalName ?? '--unnamed--';
33
+ }
34
+ get openapiName() {
35
+ return this.serializer.openapiName ?? '--unnamed--';
36
+ }
37
+ get serializerRef() {
38
+ return {
39
+ $ref: `#/components/schemas/${this.openapiName}`,
40
+ };
41
+ }
42
+ _serializerBuilder;
43
+ get serializerBuilder() {
44
+ if (this._serializerBuilder)
45
+ return this._serializerBuilder;
46
+ this._serializerBuilder = this.serializer(undefined, undefined);
47
+ return this._serializerBuilder;
48
+ }
49
+ renderedOpenapi(alreadyExtractedDescendantSerializers = {}) {
50
+ alreadyExtractedDescendantSerializers[this.serializer.globalName] = true;
51
+ const referencedSerializersAndOpenapiSchemaBodyShorthand = this._renderedOpenapi(alreadyExtractedDescendantSerializers);
52
+ if (this.allOfSiblings.length) {
53
+ const openapi = referencedSerializersAndOpenapiSchemaBodyShorthand.openapi;
54
+ return {
55
+ ...referencedSerializersAndOpenapiSchemaBodyShorthand,
56
+ openapi: {
57
+ allOf: [openapi, ...this.allOfSiblings],
58
+ },
59
+ };
60
+ }
61
+ else {
62
+ return referencedSerializersAndOpenapiSchemaBodyShorthand;
63
+ }
64
+ }
65
+ _renderedOpenapi(alreadyExtractedDescendantSerializers) {
66
+ const referencedSerializersAndAttributes = this.renderedOpenapiAttributes(alreadyExtractedDescendantSerializers);
67
+ const requiredProperties = compact(this.serializerBuilder['attributes'].map(attribute => {
68
+ const attributeType = attribute.type;
69
+ switch (attributeType) {
70
+ case 'attribute': {
71
+ return attribute.options?.as ?? attribute.name;
72
+ }
73
+ case 'delegatedAttribute': {
74
+ return attribute.options?.as ?? attribute.name;
75
+ }
76
+ case 'customAttribute': {
77
+ return attribute.options.flatten ? null : attribute.name;
78
+ }
79
+ case 'rendersOne': {
80
+ return attribute.options.flatten ? null : (attribute.options?.as ?? attribute.name);
81
+ }
82
+ case 'rendersMany': {
83
+ return attribute.options?.as ?? attribute.name;
84
+ }
85
+ default: {
86
+ // protection so that if a new ValidationType is ever added, this will throw a type error at build time
87
+ const _never = attributeType;
88
+ throw new Error(`Unhandled serializer attribute type: ${_never}`);
89
+ }
90
+ }
91
+ }));
92
+ return {
93
+ referencedSerializers: referencedSerializersAndAttributes.referencedSerializers,
94
+ openapi: {
95
+ type: 'object',
96
+ required: sort(uniq(requiredProperties.map(property => this.setCase(property)))),
97
+ properties: sortObjectByKey(referencedSerializersAndAttributes.attributes),
98
+ },
99
+ };
100
+ }
101
+ renderedOpenapiAttributes(alreadyExtractedDescendantSerializers = {}) {
102
+ const $typeForOpenapi = this.serializerBuilder['$typeForOpenapi'];
103
+ const DataTypeForOpenapi = $typeForOpenapi;
104
+ let referencedSerializers = [];
105
+ let renderedOpenapi = {};
106
+ const openapiRenderingOpts = {
107
+ casing: this.casing,
108
+ suppressResponseEnums: this.suppressResponseEnums,
109
+ };
110
+ renderedOpenapi = this.serializerBuilder['attributes'].reduce((accumulator, attribute) => {
111
+ const attributeType = attribute.type;
112
+ let newlyReferencedSerializers = [];
113
+ accumulator = (() => {
114
+ switch (attributeType) {
115
+ ////////////////
116
+ // attributes //
117
+ ////////////////
118
+ case 'attribute': {
119
+ const outputAttributeName = this.setCase(attribute.options?.as ?? attribute.name);
120
+ const openapi = attribute.options.openapi;
121
+ newlyReferencedSerializers = allSerializersFromHandWrittenOpenapi(openapi);
122
+ accumulator[outputAttributeName] = DataTypeForOpenapi?.isDream
123
+ ? dreamColumnOpenapiShape(DataTypeForOpenapi, attribute.name, openapi, {
124
+ suppressResponseEnums: this.suppressResponseEnums,
125
+ })
126
+ : allSerializersToRefsInOpenapi(openapiShorthandToOpenapi(openapi));
127
+ return accumulator;
128
+ }
129
+ /////////////////////
130
+ // end: attributes //
131
+ /////////////////////
132
+ ///////////////////////
133
+ // custom attributes //
134
+ ///////////////////////
135
+ case 'customAttribute': {
136
+ const outputAttributeName = this.setCase(attribute.name);
137
+ const openapi = attribute.options.openapi;
138
+ newlyReferencedSerializers = allSerializersFromHandWrittenOpenapi(openapi);
139
+ if (attribute.options.flatten) {
140
+ this.allOfSiblings.push(allSerializersToRefsInOpenapi(openapiShorthandToOpenapi(openapi)));
141
+ }
142
+ else {
143
+ accumulator[outputAttributeName] = allSerializersToRefsInOpenapi(openapiShorthandToOpenapi(openapi));
144
+ }
145
+ return accumulator;
146
+ }
147
+ ////////////////////////////
148
+ // end: custom attributes //
149
+ ////////////////////////////
150
+ //////////////////////////
151
+ // delegated attributes //
152
+ //////////////////////////
153
+ case 'delegatedAttribute': {
154
+ const outputAttributeName = this.setCase(attribute.options?.as ?? attribute.name);
155
+ const openapi = attribute.options.openapi;
156
+ newlyReferencedSerializers = allSerializersFromHandWrittenOpenapi(openapi);
157
+ accumulator[outputAttributeName] = allSerializersToRefsInOpenapi(openapiShorthandToOpenapi(openapi));
158
+ return accumulator;
159
+ }
160
+ ///////////////////////////////
161
+ // end: delegated attributes //
162
+ ///////////////////////////////
163
+ //////////////////
164
+ // rendersOnes //
165
+ //////////////////
166
+ case 'rendersOne': {
167
+ try {
168
+ const outputAttributeName = this.setCase(attribute.options.as ?? attribute.name);
169
+ const referencedSerializersAndOpenapiSchemaBodyShorthand = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers, openapiRenderingOpts);
170
+ newlyReferencedSerializers =
171
+ referencedSerializersAndOpenapiSchemaBodyShorthand.referencedSerializers;
172
+ if (attribute.options.flatten && attribute.options.optional) {
173
+ this.allOfSiblings.push({
174
+ anyOf: [referencedSerializersAndOpenapiSchemaBodyShorthand.openapi, NULL_OBJECT_OPENAPI],
175
+ });
176
+ //
177
+ }
178
+ else if (attribute.options.flatten) {
179
+ this.allOfSiblings.push(referencedSerializersAndOpenapiSchemaBodyShorthand.openapi);
180
+ //
181
+ }
182
+ else if (attribute.options.optional) {
183
+ accumulator[outputAttributeName] = {
184
+ anyOf: [referencedSerializersAndOpenapiSchemaBodyShorthand.openapi, NULL_OBJECT_OPENAPI],
185
+ };
186
+ }
187
+ else {
188
+ accumulator[outputAttributeName] = referencedSerializersAndOpenapiSchemaBodyShorthand.openapi;
189
+ }
190
+ return accumulator;
191
+ }
192
+ catch (error) {
193
+ if (error instanceof CallingSerializersThrewError)
194
+ return accumulator;
195
+ if (error instanceof AttemptedToDeriveDescendentSerializersFromNonSerializer)
196
+ throw new ExpectedSerializerForRendersOneOrManyOption('rendersOne', this.globalName, attribute);
197
+ throw error;
198
+ }
199
+ }
200
+ ///////////////////////
201
+ // end: rendersOnes //
202
+ ///////////////////////
203
+ ///////////////////
204
+ // rendersManys //
205
+ ///////////////////
206
+ case 'rendersMany': {
207
+ try {
208
+ const outputAttributeName = this.setCase(attribute.options.as ?? attribute.name);
209
+ const referencedSerializersAndOpenapiSchemaBodyShorthand = associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers, openapiRenderingOpts);
210
+ newlyReferencedSerializers =
211
+ referencedSerializersAndOpenapiSchemaBodyShorthand.referencedSerializers;
212
+ accumulator[outputAttributeName] = {
213
+ type: 'array',
214
+ items: referencedSerializersAndOpenapiSchemaBodyShorthand.openapi,
215
+ };
216
+ return accumulator;
217
+ }
218
+ catch (error) {
219
+ if (error instanceof CallingSerializersThrewError)
220
+ return accumulator;
221
+ if (error instanceof AttemptedToDeriveDescendentSerializersFromNonSerializer)
222
+ throw new ExpectedSerializerForRendersOneOrManyOption('rendersMany', this.globalName, attribute);
223
+ throw error;
224
+ }
225
+ }
226
+ ////////////////////////
227
+ // end: rendersManys //
228
+ ////////////////////////
229
+ default: {
230
+ // protection so that if a new ValidationType is ever added, this will throw a type error at build time
231
+ const _never = attributeType;
232
+ throw new Error(`Unhandled serializer attribute type: ${_never}`);
233
+ }
234
+ }
235
+ })();
236
+ const recursiveNewlyReferencedSerializers = newlyReferencedSerializers.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers, openapiRenderingOpts));
237
+ referencedSerializers = [
238
+ ...referencedSerializers,
239
+ ...newlyReferencedSerializers,
240
+ ...recursiveNewlyReferencedSerializers,
241
+ ];
242
+ return accumulator;
243
+ }, renderedOpenapi);
244
+ return {
245
+ referencedSerializers: uniq(referencedSerializers, serializer => serializer.globalName),
246
+ attributes: renderedOpenapi,
247
+ };
248
+ }
249
+ setCase(attr) {
250
+ switch (this.casing) {
251
+ case 'camel':
252
+ return attr;
253
+ case 'snake':
254
+ return snakeify(attr);
255
+ default: {
256
+ // protection so that if a new Casing is ever added, this will throw a type error at build time
257
+ const _never = this.casing;
258
+ throw new Error(`Unhandled Casing: ${_never}`);
259
+ }
260
+ }
261
+ }
262
+ }
263
+ function associationOpenapi(attribute, DataTypeForOpenapi, alreadyExtractedDescendantSerializers, opts) {
264
+ const serializerOverride = attribute.options.serializer;
265
+ if (serializerOverride) {
266
+ try {
267
+ return {
268
+ referencedSerializers: [
269
+ serializerOverride,
270
+ ...descendantSerializers(serializerOverride, alreadyExtractedDescendantSerializers, opts),
271
+ ],
272
+ openapi: new SerializerOpenapiRenderer(serializerOverride, opts).serializerRef,
273
+ };
274
+ }
275
+ catch (error) {
276
+ if (error instanceof NonSerializerPassedToSerializerOpenapiRenderer)
277
+ throw new NonSerializerSerializerOverrideProvided(attribute, serializerOverride);
278
+ throw error;
279
+ }
280
+ }
281
+ let associatedClasses;
282
+ const association = DataTypeForOpenapi?.isDream &&
283
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
284
+ DataTypeForOpenapi['getAssociationMetadata'](attribute.name);
285
+ if (association) {
286
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
287
+ associatedClasses = expandStiClasses(association.modelCB());
288
+ //
289
+ }
290
+ else {
291
+ const associatedClass = attribute.options.dreamClass ?? attribute.options.viewModelClass;
292
+ if (associatedClass === undefined) {
293
+ let serializerCheck;
294
+ try {
295
+ ;
296
+ DataTypeForOpenapi?.prototype?.serializers;
297
+ }
298
+ catch {
299
+ throw new CallingSerializersThrewError();
300
+ }
301
+ if (serializerCheck)
302
+ throw new ObjectSerializerRendersOneAndManyRequireClassType(attribute.name);
303
+ throw new ObjectSerializerRendersOneAndManyRequireClassType(attribute.name);
304
+ }
305
+ if (associatedClass?.isDream) {
306
+ associatedClasses = expandStiClasses(associatedClass);
307
+ }
308
+ else {
309
+ associatedClasses = [associatedClass];
310
+ }
311
+ }
312
+ const serializersOpenapi = associatedClasses.flatMap(associatedClass => inferSerializersFromDreamClassOrViewModelClass(associatedClass, attribute.options.serializerKey));
313
+ if (serializersOpenapi.length === 0)
314
+ throw new NoSerializerFoundForRendersOneAndMany(attribute.name);
315
+ if (serializersOpenapi.length === 1) {
316
+ const serializer = serializersOpenapi[0];
317
+ return {
318
+ referencedSerializers: [
319
+ serializer,
320
+ ...descendantSerializers(serializer, alreadyExtractedDescendantSerializers, opts),
321
+ ],
322
+ openapi: new SerializerOpenapiRenderer(serializer, opts).serializerRef,
323
+ };
324
+ }
325
+ return {
326
+ referencedSerializers: [
327
+ ...serializersOpenapi,
328
+ ...serializersOpenapi.flatMap(serializer => descendantSerializers(serializer, alreadyExtractedDescendantSerializers, opts)),
329
+ ],
330
+ openapi: {
331
+ anyOf: sortBy(uniq(serializersOpenapi.map(serializer => new SerializerOpenapiRenderer(serializer, opts).serializerRef), ref => ref['$ref']), ref => (ref['$ref'] ? ref['$ref'] : inspect(ref, { depth: 2 }))),
332
+ },
333
+ };
334
+ }
335
+ function descendantSerializers(serializer, alreadyExtractedDescendantSerializers, opts) {
336
+ // alreadyExtractedDescendantSerializers is used not only to avoid duplicate
337
+ // work (and thereby speed up OpenAPI spec generation), but also to avoid
338
+ // infinite loops (a recursive OpenAPI structure is valid)
339
+ if (alreadyExtractedDescendantSerializers[serializer.globalName])
340
+ return [];
341
+ if (!isDreamSerializer(serializer))
342
+ throw new AttemptedToDeriveDescendentSerializersFromNonSerializer(serializer);
343
+ const immediateDescendantSerializers = new SerializerOpenapiRenderer(serializer, opts).renderedOpenapi(alreadyExtractedDescendantSerializers).referencedSerializers;
344
+ return [
345
+ ...immediateDescendantSerializers,
346
+ ...immediateDescendantSerializers.flatMap(descendantSerializer => descendantSerializers(descendantSerializer, alreadyExtractedDescendantSerializers, opts)),
347
+ ];
348
+ }
349
+ // When attempting to expand STI children, we might call `.serializers` on
350
+ // an instance that throws an error just by calling `.serializers` (so that
351
+ // they can be sure to define serializers on the STI children, but in this
352
+ // case, there might be STI children that are intermediaries to the intended
353
+ // STI children, so they don't have serializers and calling `.serializers`
354
+ // throws an error)
355
+ class CallingSerializersThrewError extends Error {
356
+ }
@@ -1,10 +1,12 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { SerializerOpenapiRenderer, inferSerializerFromDreamOrViewModel, isDreamSerializer, maybeNullOpenapiShorthandToOpenapiShorthand, openapiShorthandPrimitiveTypes, } from '@rvoh/dream';
2
+ import { inferSerializerFromDreamOrViewModel, isDreamSerializer, openapiShorthandPrimitiveTypes, } from '@rvoh/dream';
3
3
  import NonSerializerSuppliedToSerializerBodySegment from '../error/openapi/NonSerializerSuppliedToSerializerBodySegment.js';
4
4
  import isArrayParamName from '../helpers/isArrayParamName.js';
5
5
  import isBlankDescription from './helpers/isBlankDescription.js';
6
+ import maybeNullOpenapiShorthandToOpenapiShorthand from './helpers/maybeNullOpenapiShorthandToOpenapiShorthand.js';
6
7
  import primitiveOpenapiStatementToOpenapi from './helpers/primitiveOpenapiStatementToOpenapi.js';
7
8
  import schemaToRef from './helpers/schemaToRef.js';
9
+ import SerializerOpenapiRenderer from './SerializerOpenapiRenderer.js';
8
10
  export default class OpenapiBodySegmentRenderer {
9
11
  bodySegment;
10
12
  casing;
@@ -1,4 +1,4 @@
1
- import { SerializerOpenapiRenderer, cloneDeepSafe, compact, inferSerializersFromDreamClassOrViewModelClass, isDreamSerializer, sortBy, } from '@rvoh/dream';
1
+ import { cloneDeepSafe, compact, inferSerializersFromDreamClassOrViewModelClass, isDreamSerializer, sortBy, } from '@rvoh/dream';
2
2
  import OpenApiFailedToLookupSerializerForEndpoint from '../error/openapi/FailedToLookupSerializerForEndpoint.js';
3
3
  import NonSerializerDerivedInOpenapiEndpointRenderer from '../error/openapi/NonSerializerDerivedInOpenapiEndpointRenderer.js';
4
4
  import NonSerializerDerivedInToSchemaObjects from '../error/openapi/NonSerializerDerivedInToSchemaObjects.js';
@@ -10,6 +10,7 @@ import openapiRoute from './helpers/openapiRoute.js';
10
10
  import openapiPageParamProperty from './helpers/pageParamOpenapiProperty.js';
11
11
  import primitiveOpenapiStatementToOpenapi from './helpers/primitiveOpenapiStatementToOpenapi.js';
12
12
  import safelyAttachPaginationParamToRequestBodySegment from './helpers/safelyAttachPaginationParamsToBodySegment.js';
13
+ import SerializerOpenapiRenderer from './SerializerOpenapiRenderer.js';
13
14
  export default class OpenapiEndpointRenderer {
14
15
  dreamsOrSerializers;
15
16
  controllerClass;
@@ -0,0 +1,28 @@
1
+ import isObject from '../../helpers/isObject.js';
2
+ export default function allSerializersFromHandWrittenOpenapi(openapi) {
3
+ const serializers = new Set();
4
+ extractSerializers(openapi, serializers);
5
+ return [...serializers];
6
+ }
7
+ function extractSerializers(
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ value, serializers) {
10
+ if (!value)
11
+ return;
12
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
13
+ if (value.$serializer) {
14
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
15
+ serializers.add(value.$serializer);
16
+ //
17
+ }
18
+ else if (isObject(value)) {
19
+ // Recurse into objects
20
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
21
+ Object.values(value).forEach(val => extractSerializers(val, serializers));
22
+ //
23
+ }
24
+ else if (Array.isArray(value)) {
25
+ // Recurse into arrays
26
+ value.forEach(val => extractSerializers(val, serializers));
27
+ }
28
+ }
@@ -0,0 +1,46 @@
1
+ import isObject from '../../helpers/isObject.js';
2
+ import SerializerOpenapiRenderer from '../SerializerOpenapiRenderer.js';
3
+ export default function allSerializersToRefsInOpenapi(openapi) {
4
+ if (!openapi)
5
+ return {};
6
+ return transformValue(openapi);
7
+ }
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ function transformValue(value) {
10
+ if (!value)
11
+ return value;
12
+ // If this is an object with a $serializer property, replace it with $ref
13
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
14
+ if (value.$serializer) {
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
16
+ const { $serializer, ...rest } = value;
17
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
18
+ const openapiRenderer = new SerializerOpenapiRenderer($serializer).serializerRef;
19
+ return {
20
+ ...rest,
21
+ ...openapiRenderer,
22
+ };
23
+ }
24
+ else if (isObject(value)) {
25
+ // Recurse into objects
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ const transformed = {};
28
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
29
+ for (const [key, val] of Object.entries(value)) {
30
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
31
+ transformed[key] = transformValue(val);
32
+ }
33
+ return transformed;
34
+ //
35
+ }
36
+ else if (Array.isArray(value)) {
37
+ // Recurse into arrays
38
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
39
+ return value.map(val => transformValue(val));
40
+ //
41
+ }
42
+ else {
43
+ // Return primitive values as-is
44
+ return value;
45
+ }
46
+ }
@@ -0,0 +1,136 @@
1
+ import openapiShorthandToOpenapi from './openapiShorthandToOpenapi.js';
2
+ export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined, { suppressResponseEnums = false } = {}) {
3
+ const dream = dreamClass.prototype;
4
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
5
+ const dreamColumnInfo = dream.schema[dream.table]?.columns[column];
6
+ if (!dreamColumnInfo) {
7
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
8
+ if (openapi)
9
+ return openapiShorthandToOpenapi(openapi);
10
+ throw new UseCustomOpenapiForVirtualAttributes(dreamClass, column);
11
+ }
12
+ switch (baseDbType(dreamColumnInfo)) {
13
+ case 'json':
14
+ case 'jsonb':
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
16
+ if (openapi)
17
+ return openapiShorthandToOpenapi(openapi);
18
+ throw new UseCustomOpenapiForJson(dreamClass, column);
19
+ }
20
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
21
+ const openapiObject = openapiShorthandToOpenapi((openapi ?? {}));
22
+ const singleType = singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums);
23
+ if (dreamColumnInfo.isArray) {
24
+ return {
25
+ type: dreamColumnInfo.allowNull ? ['array', 'null'] : 'array',
26
+ items: singleType,
27
+ ...openapiObject,
28
+ };
29
+ }
30
+ else {
31
+ const existingType = singleType.type;
32
+ return {
33
+ ...singleType,
34
+ type: dreamColumnInfo.allowNull && !Array.isArray(existingType) ? [existingType, 'null'] : existingType,
35
+ ...openapiObject,
36
+ };
37
+ }
38
+ }
39
+ function baseDbType(dreamColumnInfo) {
40
+ return dreamColumnInfo.dbType.replace('[]', '');
41
+ }
42
+ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums) {
43
+ if (dreamColumnInfo.enumValues) {
44
+ if (suppressResponseEnums) {
45
+ return {
46
+ type: 'string',
47
+ description: `The following values will be allowed:\n ${dreamColumnInfo.enumValues.join(',\n ')}`,
48
+ };
49
+ }
50
+ else {
51
+ return { type: 'string', enum: dreamColumnInfo.enumValues };
52
+ }
53
+ }
54
+ switch (baseDbType(dreamColumnInfo)) {
55
+ case 'boolean':
56
+ return { type: 'boolean' };
57
+ case 'bigint':
58
+ case 'bigserial':
59
+ case 'bytea':
60
+ case 'char':
61
+ case 'character varying':
62
+ case 'character':
63
+ case 'cidr':
64
+ case 'citext':
65
+ case 'inet':
66
+ case 'macaddr':
67
+ case 'money':
68
+ case 'path':
69
+ case 'text':
70
+ case 'uuid':
71
+ case 'varbit':
72
+ case 'varchar':
73
+ case 'xml':
74
+ return { type: 'string' };
75
+ case 'integer':
76
+ case 'serial':
77
+ case 'smallint':
78
+ case 'smallserial':
79
+ return { type: 'integer' };
80
+ case 'numeric':
81
+ case 'decimal':
82
+ return { type: 'number', format: 'decimal' };
83
+ case 'double':
84
+ case 'real':
85
+ return { type: 'number' };
86
+ case 'datetime':
87
+ case 'time':
88
+ case 'time with time zone':
89
+ case 'timestamp':
90
+ case 'timestamp with time zone':
91
+ case 'timestamp without time zone':
92
+ return { type: 'string', format: 'date-time' };
93
+ case 'date':
94
+ return { type: 'string', format: 'date' };
95
+ default:
96
+ throw new Error(`Unrecognized dbType used in serializer OpenAPI type declaration: ${dreamColumnInfo.dbType}`);
97
+ }
98
+ }
99
+ export class UseCustomOpenapiForVirtualAttributes extends Error {
100
+ dreamClass;
101
+ field;
102
+ constructor(dreamClass, field) {
103
+ super();
104
+ this.dreamClass = dreamClass;
105
+ this.field = field;
106
+ }
107
+ get message() {
108
+ return `Use custom OpenAPI declaration (OpenapiSchemaBodyShorthand) to define shape of virtual fields:
109
+ Dream model: ${this.dreamClass.sanitizedName}
110
+ Attribute: ${this.field}`;
111
+ }
112
+ }
113
+ export class UseCustomOpenapiForJson extends Error {
114
+ dreamClass;
115
+ field;
116
+ constructor(dreamClass, field) {
117
+ super();
118
+ this.dreamClass = dreamClass;
119
+ this.field = field;
120
+ }
121
+ get message() {
122
+ return `Use custom OpenAPI declaration (OpenapiSchemaBodyShorthand) to define shape of json and jsonb fields:
123
+ Dream model: ${this.dreamClass.sanitizedName}
124
+ Attribute: ${this.field}
125
+
126
+ For example:
127
+
128
+ export const MySerializer = (data: MyModel) =>
129
+ DreamSerializer(MyModel, data)
130
+ .jsonAttribute('myJson', {
131
+ openapi: {
132
+ type: 'object', properties: { hello: 'string' },
133
+ },
134
+ })`;
135
+ }
136
+ }
@@ -0,0 +1,10 @@
1
+ import { openapiShorthandPrimitiveTypes } from '@rvoh/dream';
2
+ import maybeNullOpenapiShorthandToOpenapiShorthand from './maybeNullOpenapiShorthandToOpenapiShorthand.js';
3
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4
+ export default function isOpenapiShorthand(openapi) {
5
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
6
+ const openapiShorthand = maybeNullOpenapiShorthandToOpenapiShorthand(openapi);
7
+ if (typeof openapiShorthand !== 'string')
8
+ return false;
9
+ return openapiShorthandPrimitiveTypes.includes(openapiShorthand);
10
+ }
@@ -0,0 +1,14 @@
1
+ export default function maybeNullOpenapiShorthandToOpenapiShorthand(openapi) {
2
+ if (openapi === undefined)
3
+ return undefined;
4
+ if (typeof openapi === 'string')
5
+ return openapi;
6
+ if (!Array.isArray(openapi))
7
+ return undefined;
8
+ if (openapi.length !== 2)
9
+ return undefined;
10
+ if (openapi[1] === 'null')
11
+ return openapi[0];
12
+ if (openapi[0] === 'null')
13
+ return openapi[1];
14
+ }