@rvoh/psychic 2.3.6 → 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.
- package/dist/cjs/src/cli/index.js +4 -0
- package/dist/cjs/src/controller/index.js +4 -0
- package/dist/cjs/src/devtools/helpers/launchDevServer.js +16 -1
- package/dist/cjs/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js +44 -0
- package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +2 -2
- package/dist/cjs/src/openapi-renderer/endpoint.js +3 -3
- package/dist/cjs/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.js → dreamColumnOpenapiShape.js} +19 -6
- package/dist/cjs/src/psychic-app/index.js +7 -0
- package/dist/cjs/src/server/helpers/startPsychicServer.js +3 -1
- package/dist/cjs/src/server/params.js +56 -3
- package/dist/esm/src/cli/index.js +4 -0
- package/dist/esm/src/controller/index.js +4 -0
- package/dist/esm/src/devtools/helpers/launchDevServer.js +16 -1
- package/dist/esm/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.js +44 -0
- package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +2 -2
- package/dist/esm/src/openapi-renderer/endpoint.js +3 -3
- package/dist/esm/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.js → dreamColumnOpenapiShape.js} +19 -6
- package/dist/esm/src/psychic-app/index.js +7 -0
- package/dist/esm/src/server/helpers/startPsychicServer.js +3 -1
- package/dist/esm/src/server/params.js +56 -3
- package/dist/types/src/controller/index.d.ts +1 -1
- package/dist/types/src/devtools/helpers/launchDevServer.d.ts +2 -1
- package/dist/types/src/error/openapi/UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute.d.ts +7 -0
- package/dist/types/src/openapi-renderer/helpers/{dreamAttributeOpenapiShape.d.ts → dreamColumnOpenapiShape.d.ts} +1 -1
- package/dist/types/src/psychic-app/index.d.ts +6 -3
- package/dist/types/src/server/params.d.ts +2 -2
- package/package.json +4 -4
|
@@ -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/
|
|
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/
|
|
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';
|
|
@@ -317,7 +317,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
317
317
|
return (controllerRouteConf.controller === this.controllerClass && controllerRouteConf.action === this.action);
|
|
318
318
|
});
|
|
319
319
|
const route = this.action === 'update'
|
|
320
|
-
? filteredRoutes.find(routeConfig => routeConfig.httpMethod === 'patch')
|
|
320
|
+
? filteredRoutes.find(routeConfig => routeConfig.httpMethod === 'patch') || filteredRoutes.at(0)
|
|
321
321
|
: filteredRoutes.at(0);
|
|
322
322
|
if (!route)
|
|
323
323
|
throw new MissingControllerActionPairingInRoutes(this.controllerClass, this.action);
|
|
@@ -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(
|
|
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(
|
|
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
|
|
149
|
+
throw new UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute(source, column, dreamColumnInfo.dbType);
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
export class UseCustomOpenapiForJson extends Error {
|
|
@@ -246,6 +246,10 @@ Try setting it to something valid, like:
|
|
|
246
246
|
get port() {
|
|
247
247
|
return this._port;
|
|
248
248
|
}
|
|
249
|
+
_httpServerOptions = {};
|
|
250
|
+
get httpServerOptions() {
|
|
251
|
+
return this._httpServerOptions;
|
|
252
|
+
}
|
|
249
253
|
_corsOptions = {};
|
|
250
254
|
get corsOptions() {
|
|
251
255
|
return this._corsOptions;
|
|
@@ -485,6 +489,9 @@ Try setting it to something valid, like:
|
|
|
485
489
|
case 'apiOnly':
|
|
486
490
|
this._apiOnly = value;
|
|
487
491
|
break;
|
|
492
|
+
case 'httpServerOptions':
|
|
493
|
+
this._httpServerOptions = value;
|
|
494
|
+
break;
|
|
488
495
|
case 'apiRoot':
|
|
489
496
|
this._apiRoot = value;
|
|
490
497
|
break;
|
|
@@ -16,16 +16,18 @@ export default async function startPsychicServer({ app, port, sslCredentials, })
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
export function createPsychicHttpInstance(app, sslCredentials) {
|
|
19
|
+
const psychicApp = PsychicApp.getOrFail();
|
|
19
20
|
if (sslCredentials?.key && sslCredentials?.cert) {
|
|
20
21
|
return https.createServer({
|
|
21
22
|
key: fs.readFileSync(sslCredentials.key),
|
|
22
23
|
cert: fs.readFileSync(sslCredentials.cert),
|
|
23
24
|
ca: sslCredentials.ca?.map(filePath => fs.readFileSync(filePath)),
|
|
24
25
|
rejectUnauthorized: sslCredentials?.rejectUnauthorized,
|
|
26
|
+
...psychicApp.httpServerOptions,
|
|
25
27
|
}, app);
|
|
26
28
|
}
|
|
27
29
|
else {
|
|
28
|
-
return http.createServer(app);
|
|
30
|
+
return http.createServer(psychicApp.httpServerOptions, app);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
function welcomeMessage({ port }) {
|
|
@@ -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) {
|
|
@@ -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/
|
|
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/
|
|
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';
|
|
@@ -317,7 +317,7 @@ export default class OpenapiEndpointRenderer {
|
|
|
317
317
|
return (controllerRouteConf.controller === this.controllerClass && controllerRouteConf.action === this.action);
|
|
318
318
|
});
|
|
319
319
|
const route = this.action === 'update'
|
|
320
|
-
? filteredRoutes.find(routeConfig => routeConfig.httpMethod === 'patch')
|
|
320
|
+
? filteredRoutes.find(routeConfig => routeConfig.httpMethod === 'patch') || filteredRoutes.at(0)
|
|
321
321
|
: filteredRoutes.at(0);
|
|
322
322
|
if (!route)
|
|
323
323
|
throw new MissingControllerActionPairingInRoutes(this.controllerClass, this.action);
|
|
@@ -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(
|
|
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(
|
|
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
|
|
149
|
+
throw new UnrecognizedDbTypeFoundWhileComputingOpenapiAttribute(source, column, dreamColumnInfo.dbType);
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
export class UseCustomOpenapiForJson extends Error {
|
|
@@ -246,6 +246,10 @@ Try setting it to something valid, like:
|
|
|
246
246
|
get port() {
|
|
247
247
|
return this._port;
|
|
248
248
|
}
|
|
249
|
+
_httpServerOptions = {};
|
|
250
|
+
get httpServerOptions() {
|
|
251
|
+
return this._httpServerOptions;
|
|
252
|
+
}
|
|
249
253
|
_corsOptions = {};
|
|
250
254
|
get corsOptions() {
|
|
251
255
|
return this._corsOptions;
|
|
@@ -485,6 +489,9 @@ Try setting it to something valid, like:
|
|
|
485
489
|
case 'apiOnly':
|
|
486
490
|
this._apiOnly = value;
|
|
487
491
|
break;
|
|
492
|
+
case 'httpServerOptions':
|
|
493
|
+
this._httpServerOptions = value;
|
|
494
|
+
break;
|
|
488
495
|
case 'apiRoot':
|
|
489
496
|
this._apiRoot = value;
|
|
490
497
|
break;
|
|
@@ -16,16 +16,18 @@ export default async function startPsychicServer({ app, port, sslCredentials, })
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
export function createPsychicHttpInstance(app, sslCredentials) {
|
|
19
|
+
const psychicApp = PsychicApp.getOrFail();
|
|
19
20
|
if (sslCredentials?.key && sslCredentials?.cert) {
|
|
20
21
|
return https.createServer({
|
|
21
22
|
key: fs.readFileSync(sslCredentials.key),
|
|
22
23
|
cert: fs.readFileSync(sslCredentials.cert),
|
|
23
24
|
ca: sslCredentials.ca?.map(filePath => fs.readFileSync(filePath)),
|
|
24
25
|
rejectUnauthorized: sslCredentials?.rejectUnauthorized,
|
|
26
|
+
...psychicApp.httpServerOptions,
|
|
25
27
|
}, app);
|
|
26
28
|
}
|
|
27
29
|
else {
|
|
28
|
-
return http.createServer(app);
|
|
30
|
+
return http.createServer(psychicApp.httpServerOptions, app);
|
|
29
31
|
}
|
|
30
32
|
}
|
|
31
33
|
function welcomeMessage({ port }) {
|
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -7,6 +7,7 @@ import { Command } from 'commander';
|
|
|
7
7
|
import { CorsOptions } from 'cors';
|
|
8
8
|
import { Express, Request, RequestHandler, Response } from 'express';
|
|
9
9
|
import * as http from 'node:http';
|
|
10
|
+
import * as https from 'node:https';
|
|
10
11
|
import { OpenapiValidateTarget } from '../openapi-renderer/defaults.js';
|
|
11
12
|
import { OpenapiContent, OpenapiHeaders, OpenapiResponses, OpenapiSecurity, OpenapiSecuritySchemes, OpenapiServer, OpenapiValidateOption } from '../openapi-renderer/endpoint.js';
|
|
12
13
|
import PsychicRouter from '../router/index.js';
|
|
@@ -34,7 +35,7 @@ export default class PsychicApp {
|
|
|
34
35
|
/**
|
|
35
36
|
* @internal
|
|
36
37
|
*/
|
|
37
|
-
static getPsychicHttpInstance(app: Express, sslCredentials: PsychicSslCredentials | undefined): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> |
|
|
38
|
+
static getPsychicHttpInstance(app: Express, sslCredentials: PsychicSslCredentials | undefined): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> | https.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
38
39
|
/**
|
|
39
40
|
* Builds the routes cache if it does not already
|
|
40
41
|
* exist. This is called during PsychicApp.init,
|
|
@@ -139,6 +140,8 @@ export default class PsychicApp {
|
|
|
139
140
|
get appName(): string;
|
|
140
141
|
private _port;
|
|
141
142
|
get port(): number;
|
|
143
|
+
private _httpServerOptions;
|
|
144
|
+
get httpServerOptions(): http.ServerOptions<typeof http.IncomingMessage, typeof http.ServerResponse> | https.ServerOptions<typeof http.IncomingMessage, typeof http.ServerResponse>;
|
|
142
145
|
private _corsOptions;
|
|
143
146
|
get corsOptions(): CorsOptions;
|
|
144
147
|
private _jsonOptions;
|
|
@@ -222,10 +225,10 @@ export default class PsychicApp {
|
|
|
222
225
|
plugin(cb: (app: PsychicApp) => void | Promise<void>): void;
|
|
223
226
|
on<T extends PsychicHookEventType>(hookEventType: T, cb: T extends 'server:error' ? (err: Error, req: Request, res: Response) => void | Promise<void> : T extends 'server:init:before-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-middleware' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:start' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:shutdown' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'server:init:after-routes' ? (psychicServer: PsychicServer) => void | Promise<void> : T extends 'cli:start' ? (program: Command) => void | Promise<void> : T extends 'cli:sync' ? () => any : (conf: PsychicApp) => void | Promise<void>): void;
|
|
224
227
|
set(option: 'openapi', name: string, value: NamedPsychicOpenapiOptions): void;
|
|
225
|
-
set<Opt extends PsychicAppOption>(option: Opt, value: Opt extends 'appName' ? string : Opt extends 'apiOnly' ? boolean : Opt extends 'defaultResponseHeaders' ? Record<string, string | null> : Opt extends 'encryption' ? PsychicAppEncryptionOptions : Opt extends 'cors' ? CorsOptions : Opt extends 'cookie' ? CustomCookieOptions : Opt extends 'apiRoot' ? string : Opt extends 'importExtension' ? GeneratorImportStyle : Opt extends 'sessionCookieName' ? string : Opt extends 'json' ? bodyParser.Options : Opt extends 'logger' ? PsychicLogger : Opt extends 'ssl' ? PsychicSslCredentials : Opt extends 'openapi' ? DefaultPsychicOpenapiOptions : Opt extends 'paths' ? PsychicPathOptions : Opt extends 'port' ? number : Opt extends 'saltRounds' ? number : Opt extends 'sanitizeResponseJson' ? boolean : Opt extends 'packageManager' ? DreamAppAllowedPackageManagersEnum : Opt extends 'inflections' ? () => void | Promise<void> : Opt extends 'routes' ? (r: PsychicRouter) => void | Promise<void> : never): void;
|
|
228
|
+
set<Opt extends PsychicAppOption>(option: Opt, value: Opt extends 'appName' ? string : Opt extends 'apiOnly' ? boolean : Opt extends 'defaultResponseHeaders' ? Record<string, string | null> : Opt extends 'httpServerOptions' ? http.ServerOptions | https.ServerOptions : Opt extends 'encryption' ? PsychicAppEncryptionOptions : Opt extends 'cors' ? CorsOptions : Opt extends 'cookie' ? CustomCookieOptions : Opt extends 'apiRoot' ? string : Opt extends 'importExtension' ? GeneratorImportStyle : Opt extends 'sessionCookieName' ? string : Opt extends 'json' ? bodyParser.Options : Opt extends 'logger' ? PsychicLogger : Opt extends 'ssl' ? PsychicSslCredentials : Opt extends 'openapi' ? DefaultPsychicOpenapiOptions : Opt extends 'paths' ? PsychicPathOptions : Opt extends 'port' ? number : Opt extends 'saltRounds' ? number : Opt extends 'sanitizeResponseJson' ? boolean : Opt extends 'packageManager' ? DreamAppAllowedPackageManagersEnum : Opt extends 'inflections' ? () => void | Promise<void> : Opt extends 'routes' ? (r: PsychicRouter) => void | Promise<void> : never): void;
|
|
226
229
|
override<Override extends keyof PsychicAppOverrides>(override: Override, value: PsychicAppOverrides[Override]): void;
|
|
227
230
|
}
|
|
228
|
-
export type PsychicAppOption = 'appName' | 'apiOnly' | 'apiRoot' | 'importExtension' | 'encryption' | 'sessionCookieName' | 'cookie' | 'cors' | 'defaultResponseHeaders' | 'inflections' | 'json' | 'logger' | 'openapi' | 'packageManager' | 'paths' | 'port' | 'routes' | 'saltRounds' | 'sanitizeResponseJson' | 'ssl';
|
|
231
|
+
export type PsychicAppOption = 'appName' | 'apiOnly' | 'apiRoot' | 'httpServerOptions' | 'importExtension' | 'encryption' | 'sessionCookieName' | 'cookie' | 'cors' | 'defaultResponseHeaders' | 'inflections' | 'json' | 'logger' | 'openapi' | 'packageManager' | 'paths' | 'port' | 'routes' | 'saltRounds' | 'sanitizeResponseJson' | 'ssl';
|
|
229
232
|
export interface PsychicAppSpecialHooks {
|
|
230
233
|
cliSync: (() => any)[];
|
|
231
234
|
serverInitBeforeMiddleware: ((server: PsychicServer) => void | Promise<void>)[];
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
92
|
-
"@rvoh/dream-spec-helpers": "^2.
|
|
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",
|