@rvoh/psychic 3.0.0-alpha.8 → 3.0.1
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 -22
- package/dist/cjs/src/controller/index.js +15 -2
- package/dist/cjs/src/generate/controller.js +3 -3
- package/dist/cjs/src/generate/helpers/generateResourceControllerSpecContent.js +6 -5
- package/dist/cjs/src/openapi-renderer/body-segment.js +0 -1
- package/dist/cjs/src/openapi-renderer/endpoint.js +13 -29
- package/dist/cjs/src/openapi-renderer/helpers/safelyAttachCursorPaginationParamToRequestBodySegment.js +3 -15
- package/dist/cjs/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.js +14 -1
- package/dist/cjs/src/router/helpers.js +12 -28
- package/dist/cjs/src/router/index.js +2 -7
- package/dist/cjs/src/server/params.js +71 -89
- package/dist/esm/src/cli/index.js +4 -22
- package/dist/esm/src/controller/index.js +15 -2
- package/dist/esm/src/generate/controller.js +3 -3
- package/dist/esm/src/generate/helpers/generateResourceControllerSpecContent.js +6 -5
- package/dist/esm/src/openapi-renderer/body-segment.js +0 -1
- package/dist/esm/src/openapi-renderer/endpoint.js +13 -29
- package/dist/esm/src/openapi-renderer/helpers/safelyAttachCursorPaginationParamToRequestBodySegment.js +3 -15
- package/dist/esm/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.js +14 -1
- package/dist/esm/src/router/helpers.js +12 -28
- package/dist/esm/src/router/index.js +2 -7
- package/dist/esm/src/server/params.js +71 -89
- package/dist/types/src/controller/index.d.ts +12 -1
- package/dist/types/src/openapi-renderer/helpers/safelyAttachCursorPaginationParamToRequestBodySegment.d.ts +1 -1
- package/dist/types/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.d.ts +6 -0
- package/dist/types/src/router/helpers.d.ts +3 -0
- package/package.json +30 -20
- package/dist/cjs/src/generate/helpers/zustandBindings/printFinalStepsMessage.js +0 -36
- package/dist/cjs/src/generate/helpers/zustandBindings/promptForOptions.js +0 -55
- package/dist/cjs/src/generate/helpers/zustandBindings/writeApiClientFile.js +0 -50
- package/dist/cjs/src/generate/helpers/zustandBindings/writeInitializer.js +0 -46
- package/dist/cjs/src/generate/openapi/zustandBindings.js +0 -27
- package/dist/esm/src/generate/helpers/zustandBindings/printFinalStepsMessage.js +0 -36
- package/dist/esm/src/generate/helpers/zustandBindings/promptForOptions.js +0 -55
- package/dist/esm/src/generate/helpers/zustandBindings/writeApiClientFile.js +0 -50
- package/dist/esm/src/generate/helpers/zustandBindings/writeInitializer.js +0 -46
- package/dist/esm/src/generate/openapi/zustandBindings.js +0 -27
- package/dist/types/src/generate/helpers/zustandBindings/printFinalStepsMessage.d.ts +0 -2
- package/dist/types/src/generate/helpers/zustandBindings/promptForOptions.d.ts +0 -2
- package/dist/types/src/generate/helpers/zustandBindings/writeApiClientFile.d.ts +0 -5
- package/dist/types/src/generate/helpers/zustandBindings/writeInitializer.d.ts +0 -5
- package/dist/types/src/generate/openapi/zustandBindings.d.ts +0 -21
|
@@ -4,7 +4,6 @@ import generateController from '../generate/controller.js';
|
|
|
4
4
|
import generateSyncEnumsInitializer from '../generate/initializer/syncEnums.js';
|
|
5
5
|
import generateSyncOpenapiTypescriptInitializer from '../generate/initializer/syncOpenapiTypescript.js';
|
|
6
6
|
import generateOpenapiReduxBindings from '../generate/openapi/reduxBindings.js';
|
|
7
|
-
import generateOpenapiZustandBindings from '../generate/openapi/zustandBindings.js';
|
|
8
7
|
import generateResource from '../generate/resource.js';
|
|
9
8
|
import Watcher from '../watcher/Watcher.js';
|
|
10
9
|
const INDENT = ' ';
|
|
@@ -23,6 +22,9 @@ ${INDENT} - citext:
|
|
|
23
22
|
${INDENT} - citext[]:
|
|
24
23
|
${INDENT} case insensitive text (indexes and queries are automatically case insensitive)
|
|
25
24
|
${INDENT}
|
|
25
|
+
${INDENT} - encrypted:
|
|
26
|
+
${INDENT} encrypted text (used in conjunction with the @deco.Encrypted decorator)
|
|
27
|
+
${INDENT}
|
|
26
28
|
${INDENT} - string:
|
|
27
29
|
${INDENT} - string[]:
|
|
28
30
|
${INDENT} varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional
|
|
@@ -84,7 +86,7 @@ export default class PsychicCLI {
|
|
|
84
86
|
- update
|
|
85
87
|
- delete`)
|
|
86
88
|
.option('--sti-base-serializer', 'omits the serializer from the dream model, but does create the serializer so it can be extended by STI children')
|
|
87
|
-
.option('--owning-model <modelName>', 'the model class of the object that `associationQuery`/`createAssociation` will be performed on in the created controller and spec (e.g., "Host", "Guest", "Ticketing/Ticket")
|
|
89
|
+
.option('--owning-model <modelName>', 'the model class of the object that `associationQuery`/`createAssociation` will be performed on in the created controller and spec (e.g., "Host", "Guest", "Ticketing/Ticket"). Defaults to the current user for non-admin/internal namespaced controllers. For admin/internal namespaced controllers, this defaults to null, meaning every admin/internal user can access the model.')
|
|
88
90
|
.option('--connection-name <connectionName>', 'the name of the db connection you would like to use for your model. Defaults to "default"', 'default')
|
|
89
91
|
.option('--model-name <modelName>', 'explicit model class name to use instead of the auto-generated one (e.g. --model-name=Kitchen for Room/Kitchen)')
|
|
90
92
|
.argument('<path>', 'URL path from root domain. Specify nesting resource with `{}`, e.g.: `tickets/{}/comments`')
|
|
@@ -144,26 +146,6 @@ export default class PsychicCLI {
|
|
|
144
146
|
});
|
|
145
147
|
process.exit();
|
|
146
148
|
});
|
|
147
|
-
program
|
|
148
|
-
.command('setup:sync:openapi-zustand')
|
|
149
|
-
.description('Generates openapi zustand bindings using openapi-fetch to connect one of your openapi files to one of your clients.')
|
|
150
|
-
.option('--schema-file <schemaFile>', 'the path from your api root to the openapi file you wish to use to generate your schema, i.e. ./src/openapi/openapi.json')
|
|
151
|
-
.option('--export-name <exportName>', 'the camelCased name to use for your exported api client, i.e. myBackendApi')
|
|
152
|
-
.option('--output-dir <outputDir>', 'the path to the directory where the generated api client and types files will be written, i.e. ../client/src/api')
|
|
153
|
-
.option('--types-file <typesFile>', 'the path to the file that will contain your generated openapi TypeScript type definitions, i.e. ../client/src/api/myBackendApi.types.d.ts')
|
|
154
|
-
.action(async ({ schemaFile, exportName, outputDir, typesFile, }) => {
|
|
155
|
-
await initializePsychicApp({
|
|
156
|
-
bypassDreamIntegrityChecks: true,
|
|
157
|
-
bypassDbConnectionsDuringInit: true,
|
|
158
|
-
});
|
|
159
|
-
await generateOpenapiZustandBindings({
|
|
160
|
-
exportName,
|
|
161
|
-
schemaFile,
|
|
162
|
-
outputDir,
|
|
163
|
-
typesFile,
|
|
164
|
-
});
|
|
165
|
-
process.exit();
|
|
166
|
-
});
|
|
167
149
|
program
|
|
168
150
|
.command('setup:sync:openapi-typescript')
|
|
169
151
|
.description('Generates an initializer in your app for converting one of your openapi files to typescript.')
|
|
@@ -197,7 +197,10 @@ export default class PsychicController {
|
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
|
-
* Gets the HTTP request headers from the
|
|
200
|
+
* Gets the HTTP request headers from the Koa ctx object.
|
|
201
|
+
* We recommend using the #header method instead when looking
|
|
202
|
+
* to retreive the value for a specific header, since that method
|
|
203
|
+
* will be safe with regards to case sensitivity.
|
|
201
204
|
*
|
|
202
205
|
* @returns The request headers as a key-value object where header names are lowercase strings
|
|
203
206
|
* and values can be strings, string arrays, or undefined.
|
|
@@ -216,6 +219,16 @@ export default class PsychicController {
|
|
|
216
219
|
get headers() {
|
|
217
220
|
return this.ctx.request.headers;
|
|
218
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* returns the value for the requested header. This method is case insensitive.
|
|
224
|
+
* If the header requested is not found, a blank string is returned.
|
|
225
|
+
*
|
|
226
|
+
* @param headerName - the name of the header
|
|
227
|
+
* @returns string
|
|
228
|
+
*/
|
|
229
|
+
header(headerName) {
|
|
230
|
+
return this.ctx.request.get(headerName);
|
|
231
|
+
}
|
|
219
232
|
/**
|
|
220
233
|
* Gets the combined parameters from the HTTP request. This includes URL parameters,
|
|
221
234
|
* request body, and query string parameters merged together. The merge order is:
|
|
@@ -796,7 +809,7 @@ export default class PsychicController {
|
|
|
796
809
|
* ```
|
|
797
810
|
*/
|
|
798
811
|
redirect(path) {
|
|
799
|
-
this.
|
|
812
|
+
this.koaRedirect(302, path);
|
|
800
813
|
}
|
|
801
814
|
// begin: http status codes
|
|
802
815
|
/**
|
|
@@ -20,9 +20,9 @@ export default async function generateController({ fullyQualifiedControllerName,
|
|
|
20
20
|
const allControllerNameParts = fullyQualifiedControllerName.split('/');
|
|
21
21
|
const forAdmin = allControllerNameParts[0] === 'Admin';
|
|
22
22
|
const forInternal = allControllerNameParts[0] === 'Internal';
|
|
23
|
-
const controllerNameParts =
|
|
23
|
+
const controllerNameParts = forAdmin || forInternal ? [allControllerNameParts.shift()] : [];
|
|
24
24
|
for (let index = 0; index < allControllerNameParts.length; index++) {
|
|
25
|
-
if (controllerNameParts.length > (
|
|
25
|
+
if (controllerNameParts.length > (forAdmin || forInternal ? 1 : 0)) {
|
|
26
26
|
// Write the ancestor controller
|
|
27
27
|
const [baseAncestorName, baseAncestorImportStatement] = baseAncestorNameAndImport(controllerNameParts, forAdmin, forInternal, { forBaseController: true });
|
|
28
28
|
if (baseAncestorName === undefined)
|
|
@@ -97,7 +97,7 @@ export default async function generateController({ fullyQualifiedControllerName,
|
|
|
97
97
|
function baseAncestorNameAndImport(controllerNameParts, forAdmin, forInternal, { forBaseController }) {
|
|
98
98
|
const maybeAncestorNameForBase = `${controllerNameParts.slice(0, controllerNameParts.length - 1).join('')}BaseController`;
|
|
99
99
|
const dotFiles = forBaseController ? '..' : '.';
|
|
100
|
-
return controllerNameParts.length === (
|
|
100
|
+
return controllerNameParts.length === (forAdmin || forInternal ? 2 : 1)
|
|
101
101
|
? forAdmin
|
|
102
102
|
? [
|
|
103
103
|
`AdminAuthedController`,
|
|
@@ -124,6 +124,7 @@ function processAttributeByType({ attributeType, attributeName, isArray, enumVal
|
|
|
124
124
|
case 'string':
|
|
125
125
|
case 'text':
|
|
126
126
|
case 'citext':
|
|
127
|
+
case 'encrypted':
|
|
127
128
|
processStringAttribute({
|
|
128
129
|
attributeName,
|
|
129
130
|
isArray,
|
|
@@ -267,7 +268,7 @@ function generateIndexActionSpec(options) {
|
|
|
267
268
|
if (options.actionConfig.omitIndex)
|
|
268
269
|
return '';
|
|
269
270
|
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular } = options;
|
|
270
|
-
const subjectFunctionName =
|
|
271
|
+
const subjectFunctionName = 'index';
|
|
271
272
|
return `
|
|
272
273
|
|
|
273
274
|
describe('GET index', () => {
|
|
@@ -304,7 +305,7 @@ function generateShowActionSpec(options) {
|
|
|
304
305
|
if (options.actionConfig.omitShow)
|
|
305
306
|
return '';
|
|
306
307
|
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular, attributeData } = options;
|
|
307
|
-
const subjectFunctionName =
|
|
308
|
+
const subjectFunctionName = 'show';
|
|
308
309
|
const subjectFunction = singular
|
|
309
310
|
? `
|
|
310
311
|
const ${subjectFunctionName} = async <StatusCode extends 200 | 400 | 404>(expectedStatus: StatusCode) => {
|
|
@@ -345,7 +346,7 @@ function generateCreateActionSpec(options) {
|
|
|
345
346
|
if (options.actionConfig.omitCreate)
|
|
346
347
|
return '';
|
|
347
348
|
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular, attributeData } = options;
|
|
348
|
-
const subjectFunctionName =
|
|
349
|
+
const subjectFunctionName = 'create';
|
|
349
350
|
const uuidSetup = attributeData.uuidAttributes
|
|
350
351
|
.map(attrName => {
|
|
351
352
|
const isArray = attributeData.uuidArrayAttributes.includes(attrName);
|
|
@@ -396,7 +397,7 @@ function generateUpdateActionSpec(options) {
|
|
|
396
397
|
if (options.actionConfig.omitUpdate)
|
|
397
398
|
return '';
|
|
398
399
|
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular, attributeData } = options;
|
|
399
|
-
const subjectFunctionName =
|
|
400
|
+
const subjectFunctionName = 'update';
|
|
400
401
|
const uuidSetup = attributeData.uuidAttributes
|
|
401
402
|
.map(attrName => {
|
|
402
403
|
const isArray = attributeData.uuidArrayAttributes.includes(attrName);
|
|
@@ -491,7 +492,7 @@ function generateDestroyActionSpec(options) {
|
|
|
491
492
|
if (options.actionConfig.omitDestroy)
|
|
492
493
|
return '';
|
|
493
494
|
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular } = options;
|
|
494
|
-
const subjectFunctionName =
|
|
495
|
+
const subjectFunctionName = 'destroy';
|
|
495
496
|
const subjectFunction = singular
|
|
496
497
|
? `
|
|
497
498
|
const ${subjectFunctionName} = async <StatusCode extends 204 | 400 | 404>(expectedStatus: StatusCode) => {
|
|
@@ -9,11 +9,9 @@ import PsychicApp from '../psychic-app/index.js';
|
|
|
9
9
|
import openapiParamNamesForDreamClass from '../server/helpers/openapiParamNamesForDreamClass.js';
|
|
10
10
|
import OpenapiSegmentExpander from './body-segment.js';
|
|
11
11
|
import { DEFAULT_OPENAPI_RESPONSES } from './defaults.js';
|
|
12
|
-
import cursorPaginationParamOpenapiProperty from './helpers/cursorPaginationParamOpenapiProperty.js';
|
|
13
12
|
import { dreamColumnOpenapiShape } from './helpers/dreamColumnOpenapiShape.js';
|
|
14
13
|
import openapiOpts from './helpers/openapiOpts.js';
|
|
15
14
|
import openapiRoute from './helpers/openapiRoute.js';
|
|
16
|
-
import paginationPageParamOpenapiProperty from './helpers/paginationPageParamOpenapiProperty.js';
|
|
17
15
|
import safelyAttachCursorPaginationParamToRequestBodySegment from './helpers/safelyAttachCursorPaginationParamToRequestBodySegment.js';
|
|
18
16
|
import safelyAttachPaginationParamToRequestBodySegment from './helpers/safelyAttachPaginationParamsToBodySegment.js';
|
|
19
17
|
import SerializerOpenapiRenderer from './SerializerOpenapiRenderer.js';
|
|
@@ -464,37 +462,23 @@ export default class OpenapiEndpointRenderer {
|
|
|
464
462
|
defaultRequestBody() {
|
|
465
463
|
const bodyPaginationPageParam = this.paginate?.body;
|
|
466
464
|
const bodyCursorPaginationParam = this.cursorPaginate?.body ?? this.scrollPaginate?.body;
|
|
465
|
+
const paramName = bodyPaginationPageParam || bodyCursorPaginationParam;
|
|
466
|
+
if (!paramName)
|
|
467
|
+
return undefined;
|
|
468
|
+
let schema = undefined;
|
|
467
469
|
if (bodyPaginationPageParam) {
|
|
468
|
-
|
|
469
|
-
content: {
|
|
470
|
-
'application/json': {
|
|
471
|
-
schema: {
|
|
472
|
-
type: 'object',
|
|
473
|
-
properties: {
|
|
474
|
-
[bodyPaginationPageParam]: paginationPageParamOpenapiProperty(),
|
|
475
|
-
},
|
|
476
|
-
},
|
|
477
|
-
},
|
|
478
|
-
},
|
|
479
|
-
};
|
|
470
|
+
schema = safelyAttachPaginationParamToRequestBodySegment(bodyPaginationPageParam, schema);
|
|
480
471
|
}
|
|
481
472
|
else if (bodyCursorPaginationParam) {
|
|
482
|
-
|
|
483
|
-
content: {
|
|
484
|
-
'application/json': {
|
|
485
|
-
schema: {
|
|
486
|
-
type: 'object',
|
|
487
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
488
|
-
properties: {
|
|
489
|
-
[bodyCursorPaginationParam]: cursorPaginationParamOpenapiProperty(),
|
|
490
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
491
|
-
},
|
|
492
|
-
},
|
|
493
|
-
},
|
|
494
|
-
},
|
|
495
|
-
};
|
|
473
|
+
schema = safelyAttachCursorPaginationParamToRequestBodySegment(bodyCursorPaginationParam, schema);
|
|
496
474
|
}
|
|
497
|
-
return
|
|
475
|
+
return {
|
|
476
|
+
content: {
|
|
477
|
+
'application/json': {
|
|
478
|
+
schema: schema,
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
};
|
|
498
482
|
}
|
|
499
483
|
/**
|
|
500
484
|
* @internal
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import cursorPaginationParamOpenapiProperty from './cursorPaginationParamOpenapiProperty.js';
|
|
2
|
+
import { safelyAttachParamToRequestBodySegment } from './safelyAttachPaginationParamsToBodySegment.js';
|
|
2
3
|
/**
|
|
3
4
|
* @internal
|
|
4
5
|
*
|
|
5
|
-
* Used to carefully bind implicit pagination params
|
|
6
|
+
* Used to carefully bind implicit cursor pagination params
|
|
6
7
|
* to the requestBody properties. It will not apply
|
|
7
8
|
* the pagination param unless the provided bodySegment
|
|
8
9
|
* is:
|
|
@@ -14,18 +15,5 @@ import cursorPaginationParamOpenapiProperty from './cursorPaginationParamOpenapi
|
|
|
14
15
|
* what was given to it, without any modifications
|
|
15
16
|
*/
|
|
16
17
|
export default function safelyAttachCursorPaginationParamToRequestBodySegment(paramName, bodySegment) {
|
|
17
|
-
|
|
18
|
-
type: 'object',
|
|
19
|
-
properties: {},
|
|
20
|
-
};
|
|
21
|
-
if (bodySegment.type === 'object') {
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
23
|
-
;
|
|
24
|
-
bodySegment.properties = {
|
|
25
|
-
...bodySegment.properties,
|
|
26
|
-
[paramName]: cursorPaginationParamOpenapiProperty(),
|
|
27
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
return bodySegment;
|
|
18
|
+
return safelyAttachParamToRequestBodySegment(paramName, cursorPaginationParamOpenapiProperty(), bodySegment);
|
|
31
19
|
}
|
|
@@ -14,15 +14,28 @@ import paginationPageParamOpenapiProperty from './paginationPageParamOpenapiProp
|
|
|
14
14
|
* what was given to it, without any modifications
|
|
15
15
|
*/
|
|
16
16
|
export default function safelyAttachPaginationParamToRequestBodySegment(paramName, bodySegment) {
|
|
17
|
+
return safelyAttachParamToRequestBodySegment(paramName, paginationPageParamOpenapiProperty(), bodySegment);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* @internal
|
|
21
|
+
*
|
|
22
|
+
* Generic version: attaches any OpenAPI property definition to a body segment.
|
|
23
|
+
*/
|
|
24
|
+
export function safelyAttachParamToRequestBodySegment(paramName,
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
|
+
property, bodySegment) {
|
|
17
27
|
bodySegment ||= {
|
|
18
28
|
type: 'object',
|
|
19
29
|
properties: {},
|
|
20
30
|
};
|
|
21
31
|
if (bodySegment.type === 'object') {
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
22
33
|
;
|
|
23
34
|
bodySegment.properties = {
|
|
24
35
|
...bodySegment.properties,
|
|
25
|
-
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
37
|
+
[paramName]: property,
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
26
39
|
};
|
|
27
40
|
}
|
|
28
41
|
return bodySegment;
|
|
@@ -58,13 +58,14 @@ function inferControllerOrFail(filteredNamespaces, opts) {
|
|
|
58
58
|
});
|
|
59
59
|
return controller;
|
|
60
60
|
}
|
|
61
|
-
export function
|
|
61
|
+
export function applyResourcefulAction(path, action, routingMechanism, options, plural) {
|
|
62
62
|
const controller = options?.controller ||
|
|
63
63
|
lookupControllerOrFail(routingMechanism, {
|
|
64
64
|
path: action,
|
|
65
65
|
httpMethod: httpMethodFromResourcefulAction(action),
|
|
66
66
|
resourceName: path,
|
|
67
67
|
});
|
|
68
|
+
const memberPath = plural ? `${path}/:id` : path;
|
|
68
69
|
switch (action) {
|
|
69
70
|
case 'index':
|
|
70
71
|
routingMechanism.get(path, controller, 'index');
|
|
@@ -73,41 +74,24 @@ export function applyResourcesAction(path, action, routingMechanism, options) {
|
|
|
73
74
|
routingMechanism.post(path, controller, 'create');
|
|
74
75
|
break;
|
|
75
76
|
case 'update':
|
|
76
|
-
routingMechanism.put(
|
|
77
|
-
routingMechanism.patch(
|
|
77
|
+
routingMechanism.put(memberPath, controller, 'update');
|
|
78
|
+
routingMechanism.patch(memberPath, controller, 'update');
|
|
78
79
|
break;
|
|
79
80
|
case 'show':
|
|
80
|
-
routingMechanism.get(
|
|
81
|
+
routingMechanism.get(memberPath, controller, 'show');
|
|
81
82
|
break;
|
|
82
83
|
case 'destroy':
|
|
83
|
-
routingMechanism.delete(
|
|
84
|
+
routingMechanism.delete(memberPath, controller, 'destroy');
|
|
84
85
|
break;
|
|
85
86
|
}
|
|
86
87
|
}
|
|
88
|
+
/** @deprecated Use applyResourcefulAction with plural parameter instead */
|
|
89
|
+
export function applyResourcesAction(path, action, routingMechanism, options) {
|
|
90
|
+
return applyResourcefulAction(path, action, routingMechanism, options, true);
|
|
91
|
+
}
|
|
92
|
+
/** @deprecated Use applyResourcefulAction with plural parameter instead */
|
|
87
93
|
export function applyResourceAction(path, action, routingMechanism, options) {
|
|
88
|
-
|
|
89
|
-
lookupControllerOrFail(routingMechanism, {
|
|
90
|
-
path: action,
|
|
91
|
-
httpMethod: httpMethodFromResourcefulAction(action),
|
|
92
|
-
resourceName: path,
|
|
93
|
-
});
|
|
94
|
-
switch (action) {
|
|
95
|
-
case 'create':
|
|
96
|
-
routingMechanism.post(path, controller, 'create');
|
|
97
|
-
break;
|
|
98
|
-
case 'update':
|
|
99
|
-
routingMechanism.put(path, controller, 'update');
|
|
100
|
-
routingMechanism.patch(path, controller, 'update');
|
|
101
|
-
break;
|
|
102
|
-
case 'show':
|
|
103
|
-
routingMechanism.get(path, controller, 'show');
|
|
104
|
-
break;
|
|
105
|
-
case 'destroy':
|
|
106
|
-
routingMechanism.delete(path, controller, 'destroy');
|
|
107
|
-
break;
|
|
108
|
-
default:
|
|
109
|
-
throw new Error(`unsupported resource method type: ${action}`);
|
|
110
|
-
}
|
|
94
|
+
return applyResourcefulAction(path, action, routingMechanism, options, false);
|
|
111
95
|
}
|
|
112
96
|
/**
|
|
113
97
|
* Converts OpenAPI-style route parameters to Express.js-style parameters
|
|
@@ -10,7 +10,7 @@ import CannotCommitRoutesWithoutKoaApp from '../error/router/cannot-commit-route
|
|
|
10
10
|
import EnvInternal from '../helpers/EnvInternal.js';
|
|
11
11
|
import errorIsRescuableHttpError from '../helpers/error/errorIsRescuableHttpError.js';
|
|
12
12
|
import PsychicApp from '../psychic-app/index.js';
|
|
13
|
-
import {
|
|
13
|
+
import { applyResourcefulAction, convertRouteParams, lookupControllerOrFail, routePath, } from '../router/helpers.js';
|
|
14
14
|
import RouteManager from './route-manager.js';
|
|
15
15
|
import { ResourceMethods, ResourcesMethods, } from './types.js';
|
|
16
16
|
const ERROR_LOGGING_DEPTH = 6;
|
|
@@ -170,12 +170,7 @@ suggested fix: "${convertRouteParams(path)}"
|
|
|
170
170
|
this.runNestedCallbacks(path, nestedRouter, cb, { asMember: plural, resourceful: true });
|
|
171
171
|
this.currentNamespaces = originalCurrentNamespaces;
|
|
172
172
|
resourceMethods.forEach(action => {
|
|
173
|
-
|
|
174
|
-
applyResourcesAction(path, action, nestedRouter, options);
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
applyResourceAction(path, action, nestedRouter, options);
|
|
178
|
-
}
|
|
173
|
+
applyResourcefulAction(path, action, nestedRouter, options, plural);
|
|
179
174
|
});
|
|
180
175
|
}
|
|
181
176
|
runNestedCallbacks(namespace, nestedRouter, cb, { asMember = false, resourceful = false, treatNamespaceAsScope = false, } = {}) {
|
|
@@ -48,97 +48,31 @@ export default class Params {
|
|
|
48
48
|
continue;
|
|
49
49
|
const columnMetadata = columns[columnName];
|
|
50
50
|
try {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
case 'citext[]':
|
|
73
|
-
case 'text[]':
|
|
74
|
-
returnObj[columnName] = this.cast(params, columnName.toString(), 'string[]', {
|
|
51
|
+
const castType = DB_TYPE_TO_CAST_TYPE[columnMetadata?.dbType];
|
|
52
|
+
if (castType) {
|
|
53
|
+
returnObj[columnName] = this.cast(params, columnName.toString(), castType, { allowNull: columnMetadata.allowNull });
|
|
54
|
+
}
|
|
55
|
+
else if (dreamClass.isVirtualColumn(columnName)) {
|
|
56
|
+
returnObj[columnName] = params[columnName];
|
|
57
|
+
}
|
|
58
|
+
else if (columnMetadata?.enumValues) {
|
|
59
|
+
const paramValue = params[columnName];
|
|
60
|
+
if (columnMetadata.isArray) {
|
|
61
|
+
if (!Array.isArray(paramValue))
|
|
62
|
+
returnObj[columnName] = ['expected an array of enum values'];
|
|
63
|
+
returnObj[columnName] = paramValue.map(p => {
|
|
64
|
+
return new this(params).cast(columnName.toString(), p, 'string', {
|
|
65
|
+
allowNull: columnMetadata.allowNull,
|
|
66
|
+
enum: columnMetadata.enumValues,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
returnObj[columnName] = this.cast(params, columnName.toString(), 'string', {
|
|
75
72
|
allowNull: columnMetadata.allowNull,
|
|
73
|
+
enum: columnMetadata.enumValues,
|
|
76
74
|
});
|
|
77
|
-
|
|
78
|
-
case 'timestamp':
|
|
79
|
-
case 'timestamp with time zone':
|
|
80
|
-
case 'timestamp without time zone':
|
|
81
|
-
returnObj[columnName] = this.cast(params, columnName.toString(), 'datetime', { allowNull: columnMetadata.allowNull });
|
|
82
|
-
break;
|
|
83
|
-
case 'timestamp[]':
|
|
84
|
-
case 'timestamp with time zone[]':
|
|
85
|
-
case 'timestamp without time zone[]':
|
|
86
|
-
returnObj[columnName] = this.cast(params, columnName.toString(), 'datetime[]', { allowNull: columnMetadata.allowNull });
|
|
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;
|
|
104
|
-
case 'jsonb':
|
|
105
|
-
returnObj[columnName] = this.cast(params, columnName.toString(), 'json', { allowNull: columnMetadata.allowNull });
|
|
106
|
-
break;
|
|
107
|
-
case 'jsonb[]':
|
|
108
|
-
returnObj[columnName] = this.cast(params, columnName.toString(), 'json[]', { allowNull: columnMetadata.allowNull });
|
|
109
|
-
break;
|
|
110
|
-
case 'numeric':
|
|
111
|
-
returnObj[columnName] = this.cast(params, columnName.toString(), 'number', { allowNull: columnMetadata.allowNull });
|
|
112
|
-
break;
|
|
113
|
-
case 'numeric[]':
|
|
114
|
-
returnObj[columnName] = this.cast(params, columnName.toString(), 'number[]', { allowNull: columnMetadata.allowNull });
|
|
115
|
-
break;
|
|
116
|
-
default:
|
|
117
|
-
if (dreamClass.isVirtualColumn(columnName))
|
|
118
|
-
returnObj[columnName] = params[columnName];
|
|
119
|
-
if (columnMetadata?.enumValues) {
|
|
120
|
-
const paramValue = params[columnName];
|
|
121
|
-
if (columnMetadata.isArray) {
|
|
122
|
-
if (!Array.isArray(paramValue))
|
|
123
|
-
returnObj[columnName] = ['expected an array of enum values'];
|
|
124
|
-
returnObj[columnName] = paramValue.map(p => {
|
|
125
|
-
return new this(params).cast(columnName.toString(), p,
|
|
126
|
-
// casting to allow enum handling at lower level
|
|
127
|
-
'string', {
|
|
128
|
-
allowNull: columnMetadata.allowNull,
|
|
129
|
-
enum: columnMetadata.enumValues,
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
returnObj[columnName] = this.cast(params, columnName.toString(),
|
|
135
|
-
// casting to allow enum handling at lower level
|
|
136
|
-
'string', {
|
|
137
|
-
allowNull: columnMetadata.allowNull,
|
|
138
|
-
enum: columnMetadata.enumValues,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
75
|
+
}
|
|
142
76
|
}
|
|
143
77
|
}
|
|
144
78
|
catch (err) {
|
|
@@ -402,6 +336,54 @@ export default class Params {
|
|
|
402
336
|
throw new ParamValidationError(paramName, [message]);
|
|
403
337
|
}
|
|
404
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Maps PostgreSQL database column types to Psychic cast types.
|
|
341
|
+
* Used by Params.for() to determine how to validate and cast each column value.
|
|
342
|
+
*/
|
|
343
|
+
const DB_TYPE_TO_CAST_TYPE = {
|
|
344
|
+
// identity mappings (db type matches cast type)
|
|
345
|
+
bigint: 'bigint',
|
|
346
|
+
'bigint[]': 'bigint[]',
|
|
347
|
+
boolean: 'boolean',
|
|
348
|
+
'boolean[]': 'boolean[]',
|
|
349
|
+
date: 'date',
|
|
350
|
+
'date[]': 'date[]',
|
|
351
|
+
integer: 'integer',
|
|
352
|
+
'integer[]': 'integer[]',
|
|
353
|
+
uuid: 'uuid',
|
|
354
|
+
'uuid[]': 'uuid[]',
|
|
355
|
+
json: 'json',
|
|
356
|
+
'json[]': 'json[]',
|
|
357
|
+
// text variants → string
|
|
358
|
+
'character varying': 'string',
|
|
359
|
+
citext: 'string',
|
|
360
|
+
text: 'string',
|
|
361
|
+
'character varying[]': 'string[]',
|
|
362
|
+
'citext[]': 'string[]',
|
|
363
|
+
'text[]': 'string[]',
|
|
364
|
+
// timestamp variants → datetime
|
|
365
|
+
timestamp: 'datetime',
|
|
366
|
+
'timestamp with time zone': 'datetime',
|
|
367
|
+
'timestamp without time zone': 'datetime',
|
|
368
|
+
'timestamp[]': 'datetime[]',
|
|
369
|
+
'timestamp with time zone[]': 'datetime[]',
|
|
370
|
+
'timestamp without time zone[]': 'datetime[]',
|
|
371
|
+
// time variants
|
|
372
|
+
time: 'time',
|
|
373
|
+
'time without time zone': 'time',
|
|
374
|
+
'time[]': 'time[]',
|
|
375
|
+
'time without time zone[]': 'time[]',
|
|
376
|
+
timetz: 'timetz',
|
|
377
|
+
'time with time zone': 'timetz',
|
|
378
|
+
'timetz[]': 'timetz[]',
|
|
379
|
+
'time with time zone[]': 'timetz[]',
|
|
380
|
+
// jsonb → json
|
|
381
|
+
jsonb: 'json',
|
|
382
|
+
'jsonb[]': 'json[]',
|
|
383
|
+
// numeric → number
|
|
384
|
+
numeric: 'number',
|
|
385
|
+
'numeric[]': 'number[]',
|
|
386
|
+
};
|
|
405
387
|
const typeToErrorMap = {
|
|
406
388
|
bigint: 'expected bigint',
|
|
407
389
|
boolean: 'expected boolean',
|
|
@@ -4,7 +4,6 @@ import generateController from '../generate/controller.js';
|
|
|
4
4
|
import generateSyncEnumsInitializer from '../generate/initializer/syncEnums.js';
|
|
5
5
|
import generateSyncOpenapiTypescriptInitializer from '../generate/initializer/syncOpenapiTypescript.js';
|
|
6
6
|
import generateOpenapiReduxBindings from '../generate/openapi/reduxBindings.js';
|
|
7
|
-
import generateOpenapiZustandBindings from '../generate/openapi/zustandBindings.js';
|
|
8
7
|
import generateResource from '../generate/resource.js';
|
|
9
8
|
import Watcher from '../watcher/Watcher.js';
|
|
10
9
|
const INDENT = ' ';
|
|
@@ -23,6 +22,9 @@ ${INDENT} - citext:
|
|
|
23
22
|
${INDENT} - citext[]:
|
|
24
23
|
${INDENT} case insensitive text (indexes and queries are automatically case insensitive)
|
|
25
24
|
${INDENT}
|
|
25
|
+
${INDENT} - encrypted:
|
|
26
|
+
${INDENT} encrypted text (used in conjunction with the @deco.Encrypted decorator)
|
|
27
|
+
${INDENT}
|
|
26
28
|
${INDENT} - string:
|
|
27
29
|
${INDENT} - string[]:
|
|
28
30
|
${INDENT} varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional
|
|
@@ -84,7 +86,7 @@ export default class PsychicCLI {
|
|
|
84
86
|
- update
|
|
85
87
|
- delete`)
|
|
86
88
|
.option('--sti-base-serializer', 'omits the serializer from the dream model, but does create the serializer so it can be extended by STI children')
|
|
87
|
-
.option('--owning-model <modelName>', 'the model class of the object that `associationQuery`/`createAssociation` will be performed on in the created controller and spec (e.g., "Host", "Guest", "Ticketing/Ticket")
|
|
89
|
+
.option('--owning-model <modelName>', 'the model class of the object that `associationQuery`/`createAssociation` will be performed on in the created controller and spec (e.g., "Host", "Guest", "Ticketing/Ticket"). Defaults to the current user for non-admin/internal namespaced controllers. For admin/internal namespaced controllers, this defaults to null, meaning every admin/internal user can access the model.')
|
|
88
90
|
.option('--connection-name <connectionName>', 'the name of the db connection you would like to use for your model. Defaults to "default"', 'default')
|
|
89
91
|
.option('--model-name <modelName>', 'explicit model class name to use instead of the auto-generated one (e.g. --model-name=Kitchen for Room/Kitchen)')
|
|
90
92
|
.argument('<path>', 'URL path from root domain. Specify nesting resource with `{}`, e.g.: `tickets/{}/comments`')
|
|
@@ -144,26 +146,6 @@ export default class PsychicCLI {
|
|
|
144
146
|
});
|
|
145
147
|
process.exit();
|
|
146
148
|
});
|
|
147
|
-
program
|
|
148
|
-
.command('setup:sync:openapi-zustand')
|
|
149
|
-
.description('Generates openapi zustand bindings using openapi-fetch to connect one of your openapi files to one of your clients.')
|
|
150
|
-
.option('--schema-file <schemaFile>', 'the path from your api root to the openapi file you wish to use to generate your schema, i.e. ./src/openapi/openapi.json')
|
|
151
|
-
.option('--export-name <exportName>', 'the camelCased name to use for your exported api client, i.e. myBackendApi')
|
|
152
|
-
.option('--output-dir <outputDir>', 'the path to the directory where the generated api client and types files will be written, i.e. ../client/src/api')
|
|
153
|
-
.option('--types-file <typesFile>', 'the path to the file that will contain your generated openapi TypeScript type definitions, i.e. ../client/src/api/myBackendApi.types.d.ts')
|
|
154
|
-
.action(async ({ schemaFile, exportName, outputDir, typesFile, }) => {
|
|
155
|
-
await initializePsychicApp({
|
|
156
|
-
bypassDreamIntegrityChecks: true,
|
|
157
|
-
bypassDbConnectionsDuringInit: true,
|
|
158
|
-
});
|
|
159
|
-
await generateOpenapiZustandBindings({
|
|
160
|
-
exportName,
|
|
161
|
-
schemaFile,
|
|
162
|
-
outputDir,
|
|
163
|
-
typesFile,
|
|
164
|
-
});
|
|
165
|
-
process.exit();
|
|
166
|
-
});
|
|
167
149
|
program
|
|
168
150
|
.command('setup:sync:openapi-typescript')
|
|
169
151
|
.description('Generates an initializer in your app for converting one of your openapi files to typescript.')
|