@rvoh/psychic 2.3.8 → 2.3.9

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 (22) hide show
  1. package/dist/cjs/src/cli/index.js +4 -0
  2. package/dist/cjs/src/controller/index.js +4 -0
  3. package/dist/cjs/src/devtools/helpers/launchDevServer.js +16 -1
  4. package/dist/cjs/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js +44 -0
  5. package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +2 -2
  6. package/dist/cjs/src/openapi-renderer/endpoint.js +2 -2
  7. package/dist/cjs/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.js → dreamColumnOpenapiShape.js} +19 -6
  8. package/dist/cjs/src/server/params.js +56 -3
  9. package/dist/esm/src/cli/index.js +4 -0
  10. package/dist/esm/src/controller/index.js +4 -0
  11. package/dist/esm/src/devtools/helpers/launchDevServer.js +16 -1
  12. package/dist/esm/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js +44 -0
  13. package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +2 -2
  14. package/dist/esm/src/openapi-renderer/endpoint.js +2 -2
  15. package/dist/esm/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.js → dreamColumnOpenapiShape.js} +19 -6
  16. package/dist/esm/src/server/params.js +56 -3
  17. package/dist/types/src/controller/index.d.ts +1 -1
  18. package/dist/types/src/devtools/helpers/launchDevServer.d.ts +2 -1
  19. package/dist/types/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.d.ts +7 -0
  20. package/dist/types/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.d.ts → dreamColumnOpenapiShape.d.ts} +1 -1
  21. package/dist/types/src/server/params.d.ts +2 -2
  22. package/package.json +4 -4
@@ -32,6 +32,10 @@ ${INDENT} - date
32
32
  ${INDENT} - date[]
33
33
  ${INDENT} - datetime
34
34
  ${INDENT} - datetime[]
35
+ ${INDENT} - time
36
+ ${INDENT} - time[]
37
+ ${INDENT} - timetz
38
+ ${INDENT} - timetz[]
35
39
  ${INDENT} - integer
36
40
  ${INDENT} - integer[]
37
41
  ${INDENT}
@@ -60,6 +60,10 @@ export const PsychicParamsPrimitiveLiterals = [
60
60
  'number[]',
61
61
  'string',
62
62
  'string[]',
63
+ 'time',
64
+ 'time[]',
65
+ 'timetz',
66
+ 'timetz[]',
63
67
  'uuid',
64
68
  'uuid[]',
65
69
  ];
@@ -6,7 +6,7 @@ import UnexpectedUndefined from '../../error/UnexpectedUndefined.js';
6
6
  import PsychicApp from '../../psychic-app/index.js';
7
7
  const devServerProcesses = {};
8
8
  const debugEnabled = debuglog('psychic').enabled;
9
- export async function launchDevServer(key, { port = 3000, cmd = 'pnpm client', timeout = 5000 } = {}) {
9
+ export async function launchDevServer(key, { port = 3000, cmd = 'pnpm client', timeout = 5000, onStdOut } = {}) {
10
10
  if (devServerProcesses[key])
11
11
  return;
12
12
  if (debugEnabled)
@@ -20,6 +20,21 @@ export async function launchDevServer(key, { port = 3000, cmd = 'pnpm client', t
20
20
  ...process.env,
21
21
  },
22
22
  });
23
+ // NOTE: adding this stdout spy so that
24
+ // when this cli utility runs node commands,
25
+ // it can properly hijack the stdout from the command
26
+ proc.stdout?.on('data', chunk => {
27
+ const txt = chunk?.toString()?.trim();
28
+ if (typeof txt !== 'string' || !txt)
29
+ return;
30
+ if (onStdOut) {
31
+ onStdOut(txt);
32
+ }
33
+ else {
34
+ // eslint-disable-next-line no-console
35
+ console.log(txt);
36
+ }
37
+ });
23
38
  devServerProcesses[key] = proc;
24
39
  await waitForPort(key, port, timeout);
25
40
  proc.on('error', err => {
@@ -0,0 +1,44 @@
1
+ export default class UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute extends Error {
2
+ source;
3
+ attributeName;
4
+ dbType;
5
+ constructor(source, attributeName, dbType) {
6
+ super();
7
+ this.source = source;
8
+ this.attributeName = attributeName;
9
+ this.dbType = dbType;
10
+ }
11
+ get message() {
12
+ return `
13
+ While trying to compute the openapi shape for either a serializer or a controller's
14
+ request body, we ran into a db type that we didn't know how to automatically infer the
15
+ openapi shape for. In these cases, we recommend that you provide an explicit openapi
16
+ shape for these attributes, so that the openapi shape can be properly generated.
17
+
18
+ source: ${this.source}
19
+ attribute: ${this.attributeName}
20
+ unexpected db type: ${this.dbType}
21
+
22
+ If the culprit is a serializer attribute, you should provide an explicit openapi definition
23
+ to the attribute causing your problems, like so:
24
+
25
+ .attribute('${this.attributeName}', { openapi: { type: 'string' }})
26
+
27
+ If, instead, it is a controller's request body causing your problems, identify the controller
28
+ method responsible for this exception, and ensure that the openapi request body shape is explicitly
29
+ defined, so that you do not force psychic to autocompute the openapi body shape for this endpoint.
30
+
31
+ @OpenAPI(MyModel, {
32
+ requestBody: {
33
+ type: 'object',
34
+ properties: {
35
+ ${this.attributeName}: {
36
+ type: 'string',
37
+ }
38
+ }
39
+ }
40
+ })
41
+
42
+ `;
43
+ }
44
+ }
@@ -14,7 +14,7 @@ import NoSerializerFoundForRendersOneAndMany from '../error/openapi/NoSerializer
14
14
  import ObjectSerializerRendersOneAndManyRequireClassType from '../error/openapi/ObjectSerializerRendersOneAndManyRequireClassType.js';
15
15
  import allSerializersFromHandWrittenOpenapi from './helpers/allSerializersFromHandWrittenOpenapi.js';
16
16
  import allSerializersToRefsInOpenapi from './helpers/allSerializersToRefsInOpenapi.js';
17
- import { dreamColumnOpenapiShape } from './helpers/dreamAttributeOpenapiShape.js';
17
+ import { dreamColumnOpenapiShape } from './helpers/dreamColumnOpenapiShape.js';
18
18
  import openapiShorthandToOpenapi from './helpers/openapiShorthandToOpenapi.js';
19
19
  const NULL_OBJECT_OPENAPI = { type: 'null' };
20
20
  export default class SerializerOpenapiRenderer {
@@ -153,7 +153,7 @@ export default class SerializerOpenapiRenderer {
153
153
  target = DataTypeForOpenapi;
154
154
  }
155
155
  accumulator[outputAttributeName] = allSerializersToRefsInOpenapi(target?.isDream
156
- ? dreamColumnOpenapiShape(target, attribute.name, openapi, {
156
+ ? dreamColumnOpenapiShape(this.serializer.globalName, target, attribute.name, openapi, {
157
157
  suppressResponseEnums: this.suppressResponseEnums,
158
158
  })
159
159
  : openapiShorthandToOpenapi(openapi));
@@ -10,7 +10,7 @@ import openapiParamNamesForDreamClass from '../server/helpers/openapiParamNamesF
10
10
  import OpenapiSegmentExpander from './body-segment.js';
11
11
  import { DEFAULT_OPENAPI_RESPONSES } from './defaults.js';
12
12
  import cursorPaginationParamOpenapiProperty from './helpers/cursorPaginationParamOpenapiProperty.js';
13
- import { dreamColumnOpenapiShape } from './helpers/dreamAttributeOpenapiShape.js';
13
+ import { dreamColumnOpenapiShape } from './helpers/dreamColumnOpenapiShape.js';
14
14
  import openapiOpts from './helpers/openapiOpts.js';
15
15
  import openapiRoute from './helpers/openapiRoute.js';
16
16
  import paginationPageParamOpenapiProperty from './helpers/paginationPageParamOpenapiProperty.js';
@@ -545,7 +545,7 @@ export default class OpenapiEndpointRenderer {
545
545
  paramsShape.required = required;
546
546
  }
547
547
  paramsShape.properties = paramSafeColumns.reduce((acc, columnName) => {
548
- acc[columnName] = dreamColumnOpenapiShape(dreamClass, columnName, undefined, {
548
+ acc[columnName] = dreamColumnOpenapiShape(this.controllerClass.controllerActionPath(this.action), dreamClass, columnName, undefined, {
549
549
  allowGenericJson: true,
550
550
  });
551
551
  return acc;
@@ -1,7 +1,13 @@
1
1
  import { SerializingPlainPropertyWithoutOpenapiShape } from '../../error/openapi/SerializingPlainPropertyWithoutOpenapiShape.js';
2
+ import UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute from '../../error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js';
2
3
  import OpenapiSegmentExpander from '../body-segment.js';
3
4
  import openapiShorthandToOpenapi from './openapiShorthandToOpenapi.js';
4
- export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined, { suppressResponseEnums = false, allowGenericJson = false, } = {}) {
5
+ export function dreamColumnOpenapiShape(
6
+ // this is the global name of the serializer or controller calling down
7
+ // to get this information. If an unrecognized db type is provided, the
8
+ // source will be rendered in the exception that is returned, enabling
9
+ // the dev to identify the source of the issue and fix it
10
+ source, dreamClass, column, openapi = undefined, { suppressResponseEnums = false, allowGenericJson = false, } = {}) {
5
11
  if (dreamClass.isVirtualColumn(column)) {
6
12
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
7
13
  const openapiObject = openapiShorthandToOpenapi((openapi ?? {}));
@@ -46,7 +52,7 @@ export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined,
46
52
  }
47
53
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
48
54
  const openapiObject = openapiShorthandToOpenapi((openapi ?? {}));
49
- const singleType = singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, openapiObject);
55
+ const singleType = singularAttributeOpenapiShape(source, column, dreamColumnInfo, suppressResponseEnums, openapiObject);
50
56
  if (dreamColumnInfo.isArray) {
51
57
  return {
52
58
  type: dreamColumnInfo.allowNull ? ['array', 'null'] : 'array',
@@ -71,7 +77,12 @@ export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined,
71
77
  function baseDbType(dreamColumnInfo) {
72
78
  return dreamColumnInfo.dbType.replace('[]', '');
73
79
  }
74
- function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, openapiSchema) {
80
+ function singularAttributeOpenapiShape(
81
+ // this is the global name of the serializer or controller calling down
82
+ // to get this information. If an unrecognized db type is provided, the
83
+ // source will be rendered in the exception that is returned, enabling
84
+ // the dev to identify the source of the issue and fix it
85
+ source, column, dreamColumnInfo, suppressResponseEnums, openapiSchema) {
75
86
  if (dreamColumnInfo.enumValues) {
76
87
  const enumOverrides = openapiSchema.enum || dreamColumnInfo.enumValues;
77
88
  if (suppressResponseEnums) {
@@ -102,6 +113,10 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
102
113
  case 'money':
103
114
  case 'path':
104
115
  case 'text':
116
+ case 'time':
117
+ case 'time without time zone':
118
+ case 'timetz':
119
+ case 'time with time zone':
105
120
  case 'uuid':
106
121
  case 'varbit':
107
122
  case 'varchar':
@@ -121,8 +136,6 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
121
136
  case 'real':
122
137
  return { type: 'number' };
123
138
  case 'datetime':
124
- case 'time':
125
- case 'time with time zone':
126
139
  case 'timestamp':
127
140
  case 'timestamp with time zone':
128
141
  case 'timestamp without time zone':
@@ -133,7 +146,7 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
133
146
  case 'jsonb':
134
147
  return { type: 'object' };
135
148
  default:
136
- throw new Error(`Unrecognized dbType used in serializer OpenAPI type declaration: ${dreamColumnInfo.dbType}`);
149
+ throw new UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute(source, column, dreamColumnInfo.dbType);
137
150
  }
138
151
  }
139
152
  export class UseCustomOpenapiForJson extends Error {
@@ -1,4 +1,4 @@
1
- import { CalendarDate, DateTime } from '@rvoh/dream';
1
+ import { CalendarDate, ClockTime, ClockTimeTz, DateTime } from '@rvoh/dream';
2
2
  import { camelize, compact, snakeify } from '@rvoh/dream/utils';
3
3
  import ParamValidationError from '../error/controller/ParamValidationError.js';
4
4
  import ParamValidationErrors from '../error/controller/ParamValidationErrors.js';
@@ -85,6 +85,22 @@ export default class Params {
85
85
  case 'timestamp without time zone[]':
86
86
  returnObj[columnName] = this.cast(params, columnName.toString(), 'datetime[]', { allowNull: columnMetadata.allowNull });
87
87
  break;
88
+ case 'time':
89
+ case 'time without time zone':
90
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'time', { allowNull: columnMetadata.allowNull });
91
+ break;
92
+ case 'time[]':
93
+ case 'time without time zone[]':
94
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'time[]', { allowNull: columnMetadata.allowNull });
95
+ break;
96
+ case 'timetz':
97
+ case 'time with time zone':
98
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'timetz', { allowNull: columnMetadata.allowNull });
99
+ break;
100
+ case 'timetz[]':
101
+ case 'time with time zone[]':
102
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'timetz[]', { allowNull: columnMetadata.allowNull });
103
+ break;
88
104
  case 'jsonb':
89
105
  returnObj[columnName] = this.cast(params, columnName.toString(), 'json', { allowNull: columnMetadata.allowNull });
90
106
  break;
@@ -203,7 +219,6 @@ export default class Params {
203
219
  }
204
220
  return paramValue;
205
221
  }
206
- let dateClass;
207
222
  const integerRegexp = /^-?\d+$/;
208
223
  switch (expectedType) {
209
224
  case 'string':
@@ -227,7 +242,8 @@ export default class Params {
227
242
  return false;
228
243
  throw new ParamValidationError(paramName, [typeToError(expectedType)]);
229
244
  case 'datetime':
230
- case 'date':
245
+ case 'date': {
246
+ let dateClass;
231
247
  switch (expectedType) {
232
248
  case 'datetime':
233
249
  dateClass = DateTime;
@@ -252,6 +268,35 @@ export default class Params {
252
268
  }
253
269
  }
254
270
  throw new ParamValidationError(paramName, [typeToError(expectedType)]);
271
+ }
272
+ case 'time':
273
+ case 'timetz': {
274
+ let timeClass;
275
+ switch (expectedType) {
276
+ case 'time':
277
+ timeClass = ClockTime;
278
+ break;
279
+ case 'timetz':
280
+ timeClass = ClockTimeTz;
281
+ break;
282
+ default:
283
+ if (typeof expectedType === 'string')
284
+ throw Error(`${expectedType} must be "time" or "timetz"`);
285
+ else
286
+ throw Error(`expectedType is not a string`);
287
+ }
288
+ if (paramValue instanceof ClockTime || paramValue instanceof ClockTimeTz)
289
+ return paramValue;
290
+ if (typeof paramValue === 'string') {
291
+ try {
292
+ return timeClass.fromISO(paramValue);
293
+ }
294
+ catch {
295
+ throw new ParamValidationError(paramName, [typeToError(expectedType)]);
296
+ }
297
+ }
298
+ throw new ParamValidationError(paramName, [typeToError(expectedType)]);
299
+ }
255
300
  case 'integer':
256
301
  if (typeof paramValue !== 'string' && typeof paramValue !== 'number')
257
302
  throw new ParamValidationError(paramName, [typeToError(expectedType)]);
@@ -287,6 +332,8 @@ export default class Params {
287
332
  case 'json[]':
288
333
  case 'number[]':
289
334
  case 'string[]':
335
+ case 'time[]':
336
+ case 'timetz[]':
290
337
  case 'uuid[]':
291
338
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
292
339
  if (!Array.isArray(paramValue))
@@ -365,6 +412,8 @@ const typeToErrorMap = {
365
412
  null: 'expecting null',
366
413
  number: 'expected number or string number',
367
414
  string: 'expected string',
415
+ time: 'expected ISO time string',
416
+ timetz: 'expected ISO timetz string',
368
417
  uuid: 'expected uuid',
369
418
  'bigint[]': 'expected bigint array',
370
419
  'boolean[]': 'expected boolean array',
@@ -375,6 +424,8 @@ const typeToErrorMap = {
375
424
  'null[]': 'expecting null array',
376
425
  'number[]': 'expected number or string number array',
377
426
  'string[]': 'expected string array',
427
+ 'time[]': 'expected ISO time string array',
428
+ 'timetz[]': 'expected ISO timetz string array',
378
429
  'uuid[]': 'expected uuid array',
379
430
  };
380
431
  function typeToError(param) {
@@ -393,6 +444,8 @@ const arrayTypeToNonArrayTypeMap = {
393
444
  'null[]': 'null',
394
445
  'number[]': 'number',
395
446
  'string[]': 'string',
447
+ 'time[]': 'time',
448
+ 'timetz[]': 'timetz',
396
449
  'uuid[]': 'uuid',
397
450
  };
398
451
  function arrayTypeToNonArrayType(param) {
@@ -32,6 +32,10 @@ ${INDENT} - date
32
32
  ${INDENT} - date[]
33
33
  ${INDENT} - datetime
34
34
  ${INDENT} - datetime[]
35
+ ${INDENT} - time
36
+ ${INDENT} - time[]
37
+ ${INDENT} - timetz
38
+ ${INDENT} - timetz[]
35
39
  ${INDENT} - integer
36
40
  ${INDENT} - integer[]
37
41
  ${INDENT}
@@ -60,6 +60,10 @@ export const PsychicParamsPrimitiveLiterals = [
60
60
  'number[]',
61
61
  'string',
62
62
  'string[]',
63
+ 'time',
64
+ 'time[]',
65
+ 'timetz',
66
+ 'timetz[]',
63
67
  'uuid',
64
68
  'uuid[]',
65
69
  ];
@@ -6,7 +6,7 @@ import UnexpectedUndefined from '../../error/UnexpectedUndefined.js';
6
6
  import PsychicApp from '../../psychic-app/index.js';
7
7
  const devServerProcesses = {};
8
8
  const debugEnabled = debuglog('psychic').enabled;
9
- export async function launchDevServer(key, { port = 3000, cmd = 'pnpm client', timeout = 5000 } = {}) {
9
+ export async function launchDevServer(key, { port = 3000, cmd = 'pnpm client', timeout = 5000, onStdOut } = {}) {
10
10
  if (devServerProcesses[key])
11
11
  return;
12
12
  if (debugEnabled)
@@ -20,6 +20,21 @@ export async function launchDevServer(key, { port = 3000, cmd = 'pnpm client', t
20
20
  ...process.env,
21
21
  },
22
22
  });
23
+ // NOTE: adding this stdout spy so that
24
+ // when this cli utility runs node commands,
25
+ // it can properly hijack the stdout from the command
26
+ proc.stdout?.on('data', chunk => {
27
+ const txt = chunk?.toString()?.trim();
28
+ if (typeof txt !== 'string' || !txt)
29
+ return;
30
+ if (onStdOut) {
31
+ onStdOut(txt);
32
+ }
33
+ else {
34
+ // eslint-disable-next-line no-console
35
+ console.log(txt);
36
+ }
37
+ });
23
38
  devServerProcesses[key] = proc;
24
39
  await waitForPort(key, port, timeout);
25
40
  proc.on('error', err => {
@@ -0,0 +1,44 @@
1
+ export default class UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute extends Error {
2
+ source;
3
+ attributeName;
4
+ dbType;
5
+ constructor(source, attributeName, dbType) {
6
+ super();
7
+ this.source = source;
8
+ this.attributeName = attributeName;
9
+ this.dbType = dbType;
10
+ }
11
+ get message() {
12
+ return `
13
+ While trying to compute the openapi shape for either a serializer or a controller's
14
+ request body, we ran into a db type that we didn't know how to automatically infer the
15
+ openapi shape for. In these cases, we recommend that you provide an explicit openapi
16
+ shape for these attributes, so that the openapi shape can be properly generated.
17
+
18
+ source: ${this.source}
19
+ attribute: ${this.attributeName}
20
+ unexpected db type: ${this.dbType}
21
+
22
+ If the culprit is a serializer attribute, you should provide an explicit openapi definition
23
+ to the attribute causing your problems, like so:
24
+
25
+ .attribute('${this.attributeName}', { openapi: { type: 'string' }})
26
+
27
+ If, instead, it is a controller's request body causing your problems, identify the controller
28
+ method responsible for this exception, and ensure that the openapi request body shape is explicitly
29
+ defined, so that you do not force psychic to autocompute the openapi body shape for this endpoint.
30
+
31
+ @OpenAPI(MyModel, {
32
+ requestBody: {
33
+ type: 'object',
34
+ properties: {
35
+ ${this.attributeName}: {
36
+ type: 'string',
37
+ }
38
+ }
39
+ }
40
+ })
41
+
42
+ `;
43
+ }
44
+ }
@@ -14,7 +14,7 @@ import NoSerializerFoundForRendersOneAndMany from '../error/openapi/NoSerializer
14
14
  import ObjectSerializerRendersOneAndManyRequireClassType from '../error/openapi/ObjectSerializerRendersOneAndManyRequireClassType.js';
15
15
  import allSerializersFromHandWrittenOpenapi from './helpers/allSerializersFromHandWrittenOpenapi.js';
16
16
  import allSerializersToRefsInOpenapi from './helpers/allSerializersToRefsInOpenapi.js';
17
- import { dreamColumnOpenapiShape } from './helpers/dreamAttributeOpenapiShape.js';
17
+ import { dreamColumnOpenapiShape } from './helpers/dreamColumnOpenapiShape.js';
18
18
  import openapiShorthandToOpenapi from './helpers/openapiShorthandToOpenapi.js';
19
19
  const NULL_OBJECT_OPENAPI = { type: 'null' };
20
20
  export default class SerializerOpenapiRenderer {
@@ -153,7 +153,7 @@ export default class SerializerOpenapiRenderer {
153
153
  target = DataTypeForOpenapi;
154
154
  }
155
155
  accumulator[outputAttributeName] = allSerializersToRefsInOpenapi(target?.isDream
156
- ? dreamColumnOpenapiShape(target, attribute.name, openapi, {
156
+ ? dreamColumnOpenapiShape(this.serializer.globalName, target, attribute.name, openapi, {
157
157
  suppressResponseEnums: this.suppressResponseEnums,
158
158
  })
159
159
  : openapiShorthandToOpenapi(openapi));
@@ -10,7 +10,7 @@ import openapiParamNamesForDreamClass from '../server/helpers/openapiParamNamesF
10
10
  import OpenapiSegmentExpander from './body-segment.js';
11
11
  import { DEFAULT_OPENAPI_RESPONSES } from './defaults.js';
12
12
  import cursorPaginationParamOpenapiProperty from './helpers/cursorPaginationParamOpenapiProperty.js';
13
- import { dreamColumnOpenapiShape } from './helpers/dreamAttributeOpenapiShape.js';
13
+ import { dreamColumnOpenapiShape } from './helpers/dreamColumnOpenapiShape.js';
14
14
  import openapiOpts from './helpers/openapiOpts.js';
15
15
  import openapiRoute from './helpers/openapiRoute.js';
16
16
  import paginationPageParamOpenapiProperty from './helpers/paginationPageParamOpenapiProperty.js';
@@ -545,7 +545,7 @@ export default class OpenapiEndpointRenderer {
545
545
  paramsShape.required = required;
546
546
  }
547
547
  paramsShape.properties = paramSafeColumns.reduce((acc, columnName) => {
548
- acc[columnName] = dreamColumnOpenapiShape(dreamClass, columnName, undefined, {
548
+ acc[columnName] = dreamColumnOpenapiShape(this.controllerClass.controllerActionPath(this.action), dreamClass, columnName, undefined, {
549
549
  allowGenericJson: true,
550
550
  });
551
551
  return acc;
@@ -1,7 +1,13 @@
1
1
  import { SerializingPlainPropertyWithoutOpenapiShape } from '../../error/openapi/SerializingPlainPropertyWithoutOpenapiShape.js';
2
+ import UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute from '../../error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js';
2
3
  import OpenapiSegmentExpander from '../body-segment.js';
3
4
  import openapiShorthandToOpenapi from './openapiShorthandToOpenapi.js';
4
- export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined, { suppressResponseEnums = false, allowGenericJson = false, } = {}) {
5
+ export function dreamColumnOpenapiShape(
6
+ // this is the global name of the serializer or controller calling down
7
+ // to get this information. If an unrecognized db type is provided, the
8
+ // source will be rendered in the exception that is returned, enabling
9
+ // the dev to identify the source of the issue and fix it
10
+ source, dreamClass, column, openapi = undefined, { suppressResponseEnums = false, allowGenericJson = false, } = {}) {
5
11
  if (dreamClass.isVirtualColumn(column)) {
6
12
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
7
13
  const openapiObject = openapiShorthandToOpenapi((openapi ?? {}));
@@ -46,7 +52,7 @@ export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined,
46
52
  }
47
53
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
48
54
  const openapiObject = openapiShorthandToOpenapi((openapi ?? {}));
49
- const singleType = singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, openapiObject);
55
+ const singleType = singularAttributeOpenapiShape(source, column, dreamColumnInfo, suppressResponseEnums, openapiObject);
50
56
  if (dreamColumnInfo.isArray) {
51
57
  return {
52
58
  type: dreamColumnInfo.allowNull ? ['array', 'null'] : 'array',
@@ -71,7 +77,12 @@ export function dreamColumnOpenapiShape(dreamClass, column, openapi = undefined,
71
77
  function baseDbType(dreamColumnInfo) {
72
78
  return dreamColumnInfo.dbType.replace('[]', '');
73
79
  }
74
- function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, openapiSchema) {
80
+ function singularAttributeOpenapiShape(
81
+ // this is the global name of the serializer or controller calling down
82
+ // to get this information. If an unrecognized db type is provided, the
83
+ // source will be rendered in the exception that is returned, enabling
84
+ // the dev to identify the source of the issue and fix it
85
+ source, column, dreamColumnInfo, suppressResponseEnums, openapiSchema) {
75
86
  if (dreamColumnInfo.enumValues) {
76
87
  const enumOverrides = openapiSchema.enum || dreamColumnInfo.enumValues;
77
88
  if (suppressResponseEnums) {
@@ -102,6 +113,10 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
102
113
  case 'money':
103
114
  case 'path':
104
115
  case 'text':
116
+ case 'time':
117
+ case 'time without time zone':
118
+ case 'timetz':
119
+ case 'time with time zone':
105
120
  case 'uuid':
106
121
  case 'varbit':
107
122
  case 'varchar':
@@ -121,8 +136,6 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
121
136
  case 'real':
122
137
  return { type: 'number' };
123
138
  case 'datetime':
124
- case 'time':
125
- case 'time with time zone':
126
139
  case 'timestamp':
127
140
  case 'timestamp with time zone':
128
141
  case 'timestamp without time zone':
@@ -133,7 +146,7 @@ function singularAttributeOpenapiShape(dreamColumnInfo, suppressResponseEnums, o
133
146
  case 'jsonb':
134
147
  return { type: 'object' };
135
148
  default:
136
- throw new Error(`Unrecognized dbType used in serializer OpenAPI type declaration: ${dreamColumnInfo.dbType}`);
149
+ throw new UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute(source, column, dreamColumnInfo.dbType);
137
150
  }
138
151
  }
139
152
  export class UseCustomOpenapiForJson extends Error {
@@ -1,4 +1,4 @@
1
- import { CalendarDate, DateTime } from '@rvoh/dream';
1
+ import { CalendarDate, ClockTime, ClockTimeTz, DateTime } from '@rvoh/dream';
2
2
  import { camelize, compact, snakeify } from '@rvoh/dream/utils';
3
3
  import ParamValidationError from '../error/controller/ParamValidationError.js';
4
4
  import ParamValidationErrors from '../error/controller/ParamValidationErrors.js';
@@ -85,6 +85,22 @@ export default class Params {
85
85
  case 'timestamp without time zone[]':
86
86
  returnObj[columnName] = this.cast(params, columnName.toString(), 'datetime[]', { allowNull: columnMetadata.allowNull });
87
87
  break;
88
+ case 'time':
89
+ case 'time without time zone':
90
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'time', { allowNull: columnMetadata.allowNull });
91
+ break;
92
+ case 'time[]':
93
+ case 'time without time zone[]':
94
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'time[]', { allowNull: columnMetadata.allowNull });
95
+ break;
96
+ case 'timetz':
97
+ case 'time with time zone':
98
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'timetz', { allowNull: columnMetadata.allowNull });
99
+ break;
100
+ case 'timetz[]':
101
+ case 'time with time zone[]':
102
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'timetz[]', { allowNull: columnMetadata.allowNull });
103
+ break;
88
104
  case 'jsonb':
89
105
  returnObj[columnName] = this.cast(params, columnName.toString(), 'json', { allowNull: columnMetadata.allowNull });
90
106
  break;
@@ -203,7 +219,6 @@ export default class Params {
203
219
  }
204
220
  return paramValue;
205
221
  }
206
- let dateClass;
207
222
  const integerRegexp = /^-?\d+$/;
208
223
  switch (expectedType) {
209
224
  case 'string':
@@ -227,7 +242,8 @@ export default class Params {
227
242
  return false;
228
243
  throw new ParamValidationError(paramName, [typeToError(expectedType)]);
229
244
  case 'datetime':
230
- case 'date':
245
+ case 'date': {
246
+ let dateClass;
231
247
  switch (expectedType) {
232
248
  case 'datetime':
233
249
  dateClass = DateTime;
@@ -252,6 +268,35 @@ export default class Params {
252
268
  }
253
269
  }
254
270
  throw new ParamValidationError(paramName, [typeToError(expectedType)]);
271
+ }
272
+ case 'time':
273
+ case 'timetz': {
274
+ let timeClass;
275
+ switch (expectedType) {
276
+ case 'time':
277
+ timeClass = ClockTime;
278
+ break;
279
+ case 'timetz':
280
+ timeClass = ClockTimeTz;
281
+ break;
282
+ default:
283
+ if (typeof expectedType === 'string')
284
+ throw Error(`${expectedType} must be "time" or "timetz"`);
285
+ else
286
+ throw Error(`expectedType is not a string`);
287
+ }
288
+ if (paramValue instanceof ClockTime || paramValue instanceof ClockTimeTz)
289
+ return paramValue;
290
+ if (typeof paramValue === 'string') {
291
+ try {
292
+ return timeClass.fromISO(paramValue);
293
+ }
294
+ catch {
295
+ throw new ParamValidationError(paramName, [typeToError(expectedType)]);
296
+ }
297
+ }
298
+ throw new ParamValidationError(paramName, [typeToError(expectedType)]);
299
+ }
255
300
  case 'integer':
256
301
  if (typeof paramValue !== 'string' && typeof paramValue !== 'number')
257
302
  throw new ParamValidationError(paramName, [typeToError(expectedType)]);
@@ -287,6 +332,8 @@ export default class Params {
287
332
  case 'json[]':
288
333
  case 'number[]':
289
334
  case 'string[]':
335
+ case 'time[]':
336
+ case 'timetz[]':
290
337
  case 'uuid[]':
291
338
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
292
339
  if (!Array.isArray(paramValue))
@@ -365,6 +412,8 @@ const typeToErrorMap = {
365
412
  null: 'expecting null',
366
413
  number: 'expected number or string number',
367
414
  string: 'expected string',
415
+ time: 'expected ISO time string',
416
+ timetz: 'expected ISO timetz string',
368
417
  uuid: 'expected uuid',
369
418
  'bigint[]': 'expected bigint array',
370
419
  'boolean[]': 'expected boolean array',
@@ -375,6 +424,8 @@ const typeToErrorMap = {
375
424
  'null[]': 'expecting null array',
376
425
  'number[]': 'expected number or string number array',
377
426
  'string[]': 'expected string array',
427
+ 'time[]': 'expected ISO time string array',
428
+ 'timetz[]': 'expected ISO timetz string array',
378
429
  'uuid[]': 'expected uuid array',
379
430
  };
380
431
  function typeToError(param) {
@@ -393,6 +444,8 @@ const arrayTypeToNonArrayTypeMap = {
393
444
  'null[]': 'null',
394
445
  'number[]': 'number',
395
446
  'string[]': 'string',
447
+ 'time[]': 'time',
448
+ 'timetz[]': 'timetz',
396
449
  'uuid[]': 'uuid',
397
450
  };
398
451
  function arrayTypeToNonArrayType(param) {
@@ -14,7 +14,7 @@ export type ControllerActionMetadata = Record<string, {
14
14
  serializerKey?: string;
15
15
  }>;
16
16
  export type PsychicParamsPrimitive = string | number | boolean | null | undefined | PsychicParamsPrimitive[];
17
- export declare const PsychicParamsPrimitiveLiterals: readonly ["bigint", "bigint[]", "boolean", "boolean[]", "date", "date[]", "datetime", "datetime[]", "integer", "integer[]", "json", "json[]", "null", "null[]", "number", "number[]", "string", "string[]", "uuid", "uuid[]"];
17
+ export declare const PsychicParamsPrimitiveLiterals: readonly ["bigint", "bigint[]", "boolean", "boolean[]", "date", "date[]", "datetime", "datetime[]", "integer", "integer[]", "json", "json[]", "null", "null[]", "number", "number[]", "string", "string[]", "time", "time[]", "timetz", "timetz[]", "uuid", "uuid[]"];
18
18
  export type PsychicParamsPrimitiveLiteral = (typeof PsychicParamsPrimitiveLiterals)[number];
19
19
  export interface PsychicParamsDictionary {
20
20
  [key: string]: PsychicParamsPrimitive | PsychicParamsDictionary | PsychicParamsDictionary[];
@@ -1,8 +1,9 @@
1
- export declare function launchDevServer(key: string, { port, cmd, timeout }?: LaunchDevServerOpts): Promise<void>;
1
+ export declare function launchDevServer(key: string, { port, cmd, timeout, onStdOut }?: LaunchDevServerOpts): Promise<void>;
2
2
  export declare function stopDevServer(key: string): void;
3
3
  export declare function stopDevServers(): void;
4
4
  export interface LaunchDevServerOpts {
5
5
  port?: number;
6
6
  cmd?: string;
7
7
  timeout?: number;
8
+ onStdOut?: (message: string) => void;
8
9
  }
@@ -0,0 +1,7 @@
1
+ export default class UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute extends Error {
2
+ private source;
3
+ private attributeName;
4
+ private dbType;
5
+ constructor(source: string, attributeName: string, dbType: string);
6
+ get message(): string;
7
+ }
@@ -5,7 +5,7 @@ export interface VirtualAttributeStatement {
5
5
  type: OpenapiShorthandPrimitiveTypes | OpenapiSchemaBodyShorthand | undefined;
6
6
  }
7
7
  type DreamClassColumnNames<DreamClass extends typeof Dream, DreamInstance extends InstanceType<DreamClass> = InstanceType<DreamClass>, DB = DreamInstance['DB'], TableName extends keyof DB = DreamInstance['table'] & keyof DB, Table extends DB[keyof DB] = DB[TableName]> = keyof Table & string;
8
- export declare function dreamColumnOpenapiShape<DreamClass extends typeof Dream>(dreamClass: DreamClass, column: DreamClassColumnNames<DreamClass>, openapi?: OpenapiDescription | OpenapiSchemaBodyShorthand | OpenapiShorthandPrimitiveTypes | undefined, { suppressResponseEnums, allowGenericJson, }?: {
8
+ export declare function dreamColumnOpenapiShape<DreamClass extends typeof Dream>(source: string, dreamClass: DreamClass, column: DreamClassColumnNames<DreamClass>, openapi?: OpenapiDescription | OpenapiSchemaBodyShorthand | OpenapiShorthandPrimitiveTypes | undefined, { suppressResponseEnums, allowGenericJson, }?: {
9
9
  suppressResponseEnums?: boolean;
10
10
  allowGenericJson?: boolean;
11
11
  }): OpenapiSchemaBody;
@@ -1,4 +1,4 @@
1
- import { CalendarDate, DateTime, Dream } from '@rvoh/dream';
1
+ import { CalendarDate, ClockTime, ClockTimeTz, DateTime, Dream } from '@rvoh/dream';
2
2
  import { OpenapiSchemaArray, OpenapiSchemaBody, OpenapiSchemaInteger, OpenapiSchemaNumber, OpenapiSchemaObjectBase, OpenapiSchemaPrimitiveGeneric, OpenapiSchemaPropertiesShorthand, OpenapiSchemaString } from '@rvoh/dream/openapi';
3
3
  import { DreamParamSafeAttributes, DreamParamSafeColumnNames, StrictInterface } from '@rvoh/dream/types';
4
4
  import { PsychicParamsDictionary, PsychicParamsPrimitive, PsychicParamsPrimitiveLiteral } from '../controller/index.js';
@@ -62,7 +62,7 @@ export default class Params {
62
62
  }
63
63
  export type ValidatedReturnType<ExpectedType, OptsType> = ExpectedType extends RegExp ? string : ExpectedType extends 'string' ? OptsType extends {
64
64
  enum: infer EnumValue;
65
- } ? EnumValue extends readonly string[] ? EnumValue[number] : never : string : ExpectedType extends 'number' ? number : ExpectedType extends 'datetime' ? DateTime : ExpectedType extends 'date' ? CalendarDate : ExpectedType extends 'bigint' ? string : ExpectedType extends 'integer' ? number : ExpectedType extends 'json' ? object : ExpectedType extends 'boolean' ? boolean : ExpectedType extends 'null' ? null : ExpectedType extends 'uuid' ? string : ExpectedType extends 'datetime[]' ? DateTime[] : ExpectedType extends 'date[]' ? CalendarDate[] : ExpectedType extends 'string[]' ? OptsType extends {
65
+ } ? EnumValue extends readonly string[] ? EnumValue[number] : never : string : ExpectedType extends 'number' ? number : ExpectedType extends 'datetime' ? DateTime : ExpectedType extends 'date' ? CalendarDate : ExpectedType extends 'time' ? ClockTime : ExpectedType extends 'timetz' ? ClockTimeTz : ExpectedType extends 'bigint' ? string : ExpectedType extends 'integer' ? number : ExpectedType extends 'json' ? object : ExpectedType extends 'boolean' ? boolean : ExpectedType extends 'null' ? null : ExpectedType extends 'uuid' ? string : ExpectedType extends 'datetime[]' ? DateTime[] : ExpectedType extends 'date[]' ? CalendarDate[] : ExpectedType extends 'time[]' ? ClockTime[] : ExpectedType extends 'timetz[]' ? ClockTimeTz[] : ExpectedType extends 'string[]' ? OptsType extends {
66
66
  enum: infer EnumValue;
67
67
  } ? EnumValue extends readonly string[] ? EnumValue[number][] : never : string[] : ExpectedType extends 'bigint[]' ? string[] : ExpectedType extends 'number[]' ? number[] : ExpectedType extends 'integer[]' ? number[] : ExpectedType extends 'boolean[]' ? boolean : ExpectedType extends 'null[]' ? null[] : ExpectedType extends 'uuid[]' ? string[] : OpenapiShapeToInterface<ExpectedType, 0>;
68
68
  type OpenapiShapeToInterface<T, Depth extends number> = Depth extends 30 ? never : T extends OpenapiSchemaObjectBase ? {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "name": "@rvoh/psychic",
4
4
  "description": "Typescript web framework",
5
- "version": "2.3.8",
5
+ "version": "2.3.9",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",
@@ -79,7 +79,7 @@
79
79
  "yoctocolors": "^2.1.1"
80
80
  },
81
81
  "peerDependencies": {
82
- "@rvoh/dream": "^2.2.0",
82
+ "@rvoh/dream": "^2.3.0",
83
83
  "@types/express": "^5.0.1",
84
84
  "commander": "^12.1.0",
85
85
  "express": "^5.2.1",
@@ -88,8 +88,8 @@
88
88
  "devDependencies": {
89
89
  "@eslint/js": "^9.39.1",
90
90
  "@jest-mock/express": "^3.0.0",
91
- "@rvoh/dream": "^2.2.3",
92
- "@rvoh/dream-spec-helpers": "^2.0.0",
91
+ "@rvoh/dream": "^2.3.0",
92
+ "@rvoh/dream-spec-helpers": "^2.1.1",
93
93
  "@rvoh/psychic-spec-helpers": "^2.0.0",
94
94
  "@types/body-parser": "^1.19.6",
95
95
  "@types/express": "^5.0.6",