@rvoh/psychic 3.0.0-alpha.7 → 3.0.0
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 +5 -1
- package/dist/cjs/src/controller/index.js +1 -1
- package/dist/cjs/src/generate/controller.js +21 -11
- package/dist/cjs/src/generate/helpers/addResourceToRoutes.js +6 -0
- package/dist/cjs/src/generate/helpers/generateControllerContent.js +17 -13
- package/dist/cjs/src/generate/helpers/generateResourceControllerSpecContent.js +22 -19
- package/dist/cjs/src/generate/resource.js +3 -0
- package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +1 -0
- package/dist/cjs/src/openapi-renderer/body-segment.js +4 -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 +5 -1
- package/dist/esm/src/controller/index.js +1 -1
- package/dist/esm/src/generate/controller.js +21 -11
- package/dist/esm/src/generate/helpers/addResourceToRoutes.js +6 -0
- package/dist/esm/src/generate/helpers/generateControllerContent.js +17 -13
- package/dist/esm/src/generate/helpers/generateResourceControllerSpecContent.js +22 -19
- package/dist/esm/src/generate/resource.js +3 -0
- package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +1 -0
- package/dist/esm/src/openapi-renderer/body-segment.js +4 -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/bin/index.d.ts +1 -0
- package/dist/types/src/cli/index.d.ts +1 -0
- package/dist/types/src/generate/helpers/generateControllerContent.d.ts +2 -1
- package/dist/types/src/generate/helpers/generateResourceControllerSpecContent.d.ts +1 -0
- package/dist/types/src/generate/resource.d.ts +1 -0
- 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 +14 -8
|
@@ -22,6 +22,9 @@ ${INDENT} - citext:
|
|
|
22
22
|
${INDENT} - citext[]:
|
|
23
23
|
${INDENT} case insensitive text (indexes and queries are automatically case insensitive)
|
|
24
24
|
${INDENT}
|
|
25
|
+
${INDENT} - encrypted:
|
|
26
|
+
${INDENT} encrypted text (used in conjunction with the @deco.Encrypted decorator)
|
|
27
|
+
${INDENT}
|
|
25
28
|
${INDENT} - string:
|
|
26
29
|
${INDENT} - string[]:
|
|
27
30
|
${INDENT} varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional
|
|
@@ -83,8 +86,9 @@ export default class PsychicCLI {
|
|
|
83
86
|
- update
|
|
84
87
|
- delete`)
|
|
85
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')
|
|
86
|
-
.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.')
|
|
87
90
|
.option('--connection-name <connectionName>', 'the name of the db connection you would like to use for your model. Defaults to "default"', 'default')
|
|
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)')
|
|
88
92
|
.argument('<path>', 'URL path from root domain. Specify nesting resource with `{}`, e.g.: `tickets/{}/comments`')
|
|
89
93
|
.argument('<modelName>', 'the name of the model to create, e.g. Post or Settings/CommunicationPreferences')
|
|
90
94
|
.argument('[columnsWithTypes...]', columnsWithTypesDescription)
|
|
@@ -19,11 +19,12 @@ export default async function generateController({ fullyQualifiedControllerName,
|
|
|
19
19
|
fullyQualifiedControllerName = fullyQualifiedControllerName.replace(pathParamRegexp, '/');
|
|
20
20
|
const allControllerNameParts = fullyQualifiedControllerName.split('/');
|
|
21
21
|
const forAdmin = allControllerNameParts[0] === 'Admin';
|
|
22
|
-
const
|
|
22
|
+
const forInternal = allControllerNameParts[0] === 'Internal';
|
|
23
|
+
const controllerNameParts = forAdmin || forInternal ? [allControllerNameParts.shift()] : [];
|
|
23
24
|
for (let index = 0; index < allControllerNameParts.length; index++) {
|
|
24
|
-
if (controllerNameParts.length > (forAdmin ? 1 : 0)) {
|
|
25
|
+
if (controllerNameParts.length > (forAdmin || forInternal ? 1 : 0)) {
|
|
25
26
|
// Write the ancestor controller
|
|
26
|
-
const [baseAncestorName, baseAncestorImportStatement] = baseAncestorNameAndImport(controllerNameParts, forAdmin, { forBaseController: true });
|
|
27
|
+
const [baseAncestorName, baseAncestorImportStatement] = baseAncestorNameAndImport(controllerNameParts, forAdmin, forInternal, { forBaseController: true });
|
|
27
28
|
if (baseAncestorName === undefined)
|
|
28
29
|
throw new UnexpectedUndefined();
|
|
29
30
|
if (baseAncestorImportStatement === undefined)
|
|
@@ -38,6 +39,7 @@ export default async function generateController({ fullyQualifiedControllerName,
|
|
|
38
39
|
fullyQualifiedControllerName: baseControllerName,
|
|
39
40
|
omitOpenApi: true,
|
|
40
41
|
forAdmin,
|
|
42
|
+
forInternal,
|
|
41
43
|
singular,
|
|
42
44
|
}));
|
|
43
45
|
}
|
|
@@ -47,7 +49,7 @@ export default async function generateController({ fullyQualifiedControllerName,
|
|
|
47
49
|
controllerNameParts.push(namedPart);
|
|
48
50
|
}
|
|
49
51
|
// Write the controller
|
|
50
|
-
const [ancestorName, ancestorImportStatement] = baseAncestorNameAndImport(controllerNameParts, forAdmin, {
|
|
52
|
+
const [ancestorName, ancestorImportStatement] = baseAncestorNameAndImport(controllerNameParts, forAdmin, forInternal, {
|
|
51
53
|
forBaseController: false,
|
|
52
54
|
});
|
|
53
55
|
if (ancestorName === undefined)
|
|
@@ -66,6 +68,7 @@ export default async function generateController({ fullyQualifiedControllerName,
|
|
|
66
68
|
actions,
|
|
67
69
|
owningModel,
|
|
68
70
|
forAdmin,
|
|
71
|
+
forInternal,
|
|
69
72
|
singular,
|
|
70
73
|
}));
|
|
71
74
|
}
|
|
@@ -86,29 +89,35 @@ export default async function generateController({ fullyQualifiedControllerName,
|
|
|
86
89
|
resourceSpecs,
|
|
87
90
|
owningModel,
|
|
88
91
|
forAdmin,
|
|
92
|
+
forInternal,
|
|
89
93
|
singular,
|
|
90
94
|
actions,
|
|
91
95
|
});
|
|
92
96
|
}
|
|
93
|
-
function baseAncestorNameAndImport(controllerNameParts, forAdmin, { forBaseController }) {
|
|
97
|
+
function baseAncestorNameAndImport(controllerNameParts, forAdmin, forInternal, { forBaseController }) {
|
|
94
98
|
const maybeAncestorNameForBase = `${controllerNameParts.slice(0, controllerNameParts.length - 1).join('')}BaseController`;
|
|
95
99
|
const dotFiles = forBaseController ? '..' : '.';
|
|
96
|
-
return controllerNameParts.length === (forAdmin ? 2 : 1)
|
|
100
|
+
return controllerNameParts.length === (forAdmin || forInternal ? 2 : 1)
|
|
97
101
|
? forAdmin
|
|
98
102
|
? [
|
|
99
103
|
`AdminAuthedController`,
|
|
100
104
|
`import AdminAuthedController from '${dotFiles}/${addImportSuffix('AuthedController.js')}'`,
|
|
101
105
|
]
|
|
102
|
-
:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
: forInternal
|
|
107
|
+
? [
|
|
108
|
+
`InternalAuthedController`,
|
|
109
|
+
`import InternalAuthedController from '${dotFiles}/${addImportSuffix('AuthedController.js')}'`,
|
|
110
|
+
]
|
|
111
|
+
: [
|
|
112
|
+
`AuthedController`,
|
|
113
|
+
`import AuthedController from '${dotFiles}/${addImportSuffix('AuthedController.js')}'`,
|
|
114
|
+
]
|
|
106
115
|
: [
|
|
107
116
|
maybeAncestorNameForBase,
|
|
108
117
|
`import ${maybeAncestorNameForBase} from '${dotFiles}/${addImportSuffix('BaseController.js')}'`,
|
|
109
118
|
];
|
|
110
119
|
}
|
|
111
|
-
async function generateControllerSpec({ fullyQualifiedControllerName, route, fullyQualifiedModelName, columnsWithTypes, resourceSpecs, owningModel, forAdmin, singular, actions, }) {
|
|
120
|
+
async function generateControllerSpec({ fullyQualifiedControllerName, route, fullyQualifiedModelName, columnsWithTypes, resourceSpecs, owningModel, forAdmin, forInternal, singular, actions, }) {
|
|
112
121
|
const { relFilePath, absDirPath, absFilePath } = psychicFileAndDirPaths(psychicPath('controllerSpecs'), fullyQualifiedControllerName + `.spec.ts`);
|
|
113
122
|
try {
|
|
114
123
|
console.log(`generating controller spec: ${relFilePath}`);
|
|
@@ -121,6 +130,7 @@ async function generateControllerSpec({ fullyQualifiedControllerName, route, ful
|
|
|
121
130
|
columnsWithTypes,
|
|
122
131
|
owningModel,
|
|
123
132
|
forAdmin,
|
|
133
|
+
forInternal,
|
|
124
134
|
singular,
|
|
125
135
|
actions,
|
|
126
136
|
})
|
|
@@ -13,6 +13,12 @@ export default async function addResourceToRoutes(route, options) {
|
|
|
13
13
|
if (fsSync.existsSync(adminRoutesFilePath))
|
|
14
14
|
routesFilePath = adminRoutesFilePath;
|
|
15
15
|
}
|
|
16
|
+
const internalRouteRegexp = /^\/?internal/;
|
|
17
|
+
if (internalRouteRegexp.test(route)) {
|
|
18
|
+
const internalRoutesFilePath = routesFilePath.replace(/\.ts$/, '.internal.ts');
|
|
19
|
+
if (fsSync.existsSync(internalRoutesFilePath))
|
|
20
|
+
routesFilePath = internalRoutesFilePath;
|
|
21
|
+
}
|
|
16
22
|
let routes = (await fs.readFile(routesFilePath)).toString();
|
|
17
23
|
const results = addResourceToRoutes_routeToRegexAndReplacements(routes, route, options);
|
|
18
24
|
routes = results.routes;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DreamApp } from '@rvoh/dream';
|
|
2
2
|
import { camelize, hyphenize } from '@rvoh/dream/utils';
|
|
3
3
|
import pluralize from 'pluralize-esm';
|
|
4
|
-
export default function generateControllerContent({ ancestorName, ancestorImportStatement, fullyQualifiedControllerName, fullyQualifiedModelName, actions = [], omitOpenApi = false, owningModel, forAdmin, singular, }) {
|
|
4
|
+
export default function generateControllerContent({ ancestorName, ancestorImportStatement, fullyQualifiedControllerName, fullyQualifiedModelName, actions = [], omitOpenApi = false, owningModel, forAdmin, forInternal = false, singular, }) {
|
|
5
5
|
fullyQualifiedControllerName = DreamApp.system.standardizeFullyQualifiedModelName(fullyQualifiedControllerName);
|
|
6
6
|
const additionalImports = [];
|
|
7
7
|
const controllerClassName = DreamApp.system.globalClassNameFromFullyQualifiedModelName(fullyQualifiedControllerName);
|
|
@@ -22,8 +22,12 @@ export default function generateControllerContent({ ancestorName, ancestorImport
|
|
|
22
22
|
const defaultOpenapiSerializerKeyProperty = forAdmin
|
|
23
23
|
? `
|
|
24
24
|
serializerKey: 'admin',`
|
|
25
|
-
:
|
|
26
|
-
|
|
25
|
+
: forInternal
|
|
26
|
+
? `
|
|
27
|
+
serializerKey: 'internal',`
|
|
28
|
+
: '';
|
|
29
|
+
const useDirectModelAccess = (forAdmin || forInternal) && !owningModel;
|
|
30
|
+
const loadQueryBase = useDirectModelAccess
|
|
27
31
|
? (modelClassName ?? 'no-class-name')
|
|
28
32
|
: `this.${owningModelProperty}.associationQuery('${pluralizedModelAttributeName}')`;
|
|
29
33
|
const methodDefs = actions.map(methodName => {
|
|
@@ -38,8 +42,8 @@ export default function generateControllerContent({ ancestorName, ancestorImport
|
|
|
38
42
|
fastJsonStringify: true,
|
|
39
43
|
})
|
|
40
44
|
public async create() {
|
|
41
|
-
// let ${modelAttributeName} = await ${
|
|
42
|
-
// if (${modelAttributeName}.isPersisted) ${modelAttributeName} = await ${modelAttributeName}.loadFor('${forAdmin ? 'admin' : 'default'}').execute()
|
|
45
|
+
// let ${modelAttributeName} = await ${useDirectModelAccess ? `${modelClassName}.create(` : `this.${owningModelProperty}.createAssociation('${pluralizedModelAttributeName}', `}this.paramsFor(${modelClassName}))
|
|
46
|
+
// if (${modelAttributeName}.isPersisted) ${modelAttributeName} = await ${modelAttributeName}.loadFor('${forAdmin ? 'admin' : forInternal ? 'internal' : 'default'}').execute()
|
|
43
47
|
// this.created(${modelAttributeName})
|
|
44
48
|
}`;
|
|
45
49
|
else
|
|
@@ -60,12 +64,12 @@ export default function generateControllerContent({ ancestorName, ancestorImport
|
|
|
60
64
|
tags: openApiTags,
|
|
61
65
|
description: 'Paginated index of ${pluralize(modelClassName)}',
|
|
62
66
|
cursorPaginate: true,
|
|
63
|
-
serializerKey: '${forAdmin ? 'adminSummary' : 'summary'}',
|
|
67
|
+
serializerKey: '${forAdmin ? 'adminSummary' : forInternal ? 'internalSummary' : 'summary'}',
|
|
64
68
|
fastJsonStringify: true,
|
|
65
69
|
})
|
|
66
70
|
public async index() {
|
|
67
71
|
// const ${pluralizedModelAttributeName} = await ${loadQueryBase}
|
|
68
|
-
// .preloadFor('${forAdmin ? 'adminSummary' : 'summary'}')
|
|
72
|
+
// .preloadFor('${forAdmin ? 'adminSummary' : forInternal ? 'internalSummary' : 'summary'}')
|
|
69
73
|
// .cursorPaginate({ cursor: this.castParam('cursor', 'string', { allowNull: true }) })
|
|
70
74
|
// this.ok(${pluralizedModelAttributeName})
|
|
71
75
|
}`;
|
|
@@ -76,7 +80,7 @@ export default function generateControllerContent({ ancestorName, ancestorImport
|
|
|
76
80
|
// tags: openApiTags,
|
|
77
81
|
// description: '<tbd>',
|
|
78
82
|
// many: true,
|
|
79
|
-
// serializerKey: '${forAdmin ? 'adminSummary' : 'summary'}',
|
|
83
|
+
// serializerKey: '${forAdmin ? 'adminSummary' : forInternal ? 'internalSummary' : 'summary'}',
|
|
80
84
|
// fastJsonStringify: true,
|
|
81
85
|
// })
|
|
82
86
|
public async index() {
|
|
@@ -183,22 +187,22 @@ export default function generateControllerContent({ ancestorName, ancestorImport
|
|
|
183
187
|
${omitOpenApi ? '' : openApiImport + '\n'}${ancestorImportStatement}${additionalImports.length ? '\n' + additionalImports.join('\n') : ''}${omitOpenApi ? '' : '\n\n' + openApiTags}
|
|
184
188
|
|
|
185
189
|
export default class ${controllerClassName} extends ${ancestorName} {
|
|
186
|
-
${methodDefs.join('\n\n')}${modelClassName ? privateMethods(forAdmin, modelClassName, actions, loadQueryBase, singular) : ''}
|
|
190
|
+
${methodDefs.join('\n\n')}${modelClassName ? privateMethods(forAdmin, forInternal, modelClassName, actions, loadQueryBase, singular) : ''}
|
|
187
191
|
}
|
|
188
192
|
`;
|
|
189
193
|
}
|
|
190
|
-
function privateMethods(forAdmin, modelClassName, methods, loadQueryBase, singular) {
|
|
194
|
+
function privateMethods(forAdmin, forInternal, modelClassName, methods, loadQueryBase, singular) {
|
|
191
195
|
const privateMethods = [];
|
|
192
196
|
if (methods.find(methodName => ['show', 'update', 'destroy'].includes(methodName)))
|
|
193
|
-
privateMethods.push(loadModelStatement(forAdmin, modelClassName, loadQueryBase, singular));
|
|
197
|
+
privateMethods.push(loadModelStatement(forAdmin, forInternal, modelClassName, loadQueryBase, singular));
|
|
194
198
|
if (!privateMethods.length)
|
|
195
199
|
return '';
|
|
196
200
|
return `\n\n${privateMethods.join('\n\n')}`;
|
|
197
201
|
}
|
|
198
|
-
function loadModelStatement(forAdmin, modelClassName, loadQueryBase, singular) {
|
|
202
|
+
function loadModelStatement(forAdmin, forInternal, modelClassName, loadQueryBase, singular) {
|
|
199
203
|
return ` private async ${camelize(modelClassName)}() {
|
|
200
204
|
// return await ${loadQueryBase}
|
|
201
|
-
// .preloadFor('${forAdmin ? 'admin' : 'default'}')
|
|
205
|
+
// .preloadFor('${forAdmin ? 'admin' : forInternal ? 'internal' : 'default'}')
|
|
202
206
|
// ${singular ? '.firstOrFail()' : ".findOrFail(this.castParam('id', 'string'))"}
|
|
203
207
|
}`;
|
|
204
208
|
}
|
|
@@ -22,15 +22,16 @@ function createModelConfiguration(options) {
|
|
|
22
22
|
const fullyQualifiedModelName = DreamApp.system.standardizeFullyQualifiedModelName(options.fullyQualifiedModelName);
|
|
23
23
|
const modelClassName = DreamApp.system.globalClassNameFromFullyQualifiedModelName(fullyQualifiedModelName);
|
|
24
24
|
const modelVariableName = camelize(modelClassName);
|
|
25
|
-
const userModelName = options.forAdmin ? 'AdminUser' : 'User';
|
|
26
|
-
const userVariableName = options.forAdmin ? 'adminUser' : 'user';
|
|
25
|
+
const userModelName = options.forInternal ? 'InternalUser' : options.forAdmin ? 'AdminUser' : 'User';
|
|
26
|
+
const userVariableName = options.forInternal ? 'internalUser' : options.forAdmin ? 'adminUser' : 'user';
|
|
27
27
|
const owningModelName = options.owningModel
|
|
28
28
|
? DreamApp.system.globalClassNameFromFullyQualifiedModelName(options.owningModel)
|
|
29
29
|
: userModelName;
|
|
30
30
|
const owningModelVariableName = options.owningModel
|
|
31
31
|
? camelize(DreamApp.system.standardizeFullyQualifiedModelName(options.owningModel).split('/').pop())
|
|
32
32
|
: userVariableName;
|
|
33
|
-
const
|
|
33
|
+
const useDirectModelAccess = (options.forAdmin || options.forInternal) && !options.owningModel;
|
|
34
|
+
const simpleCreationCommand = `const ${modelVariableName} = await create${modelClassName}(${useDirectModelAccess ? '' : `{ ${owningModelVariableName} }`})`;
|
|
34
35
|
return {
|
|
35
36
|
fullyQualifiedModelName,
|
|
36
37
|
modelClassName,
|
|
@@ -40,6 +41,7 @@ function createModelConfiguration(options) {
|
|
|
40
41
|
owningModelName,
|
|
41
42
|
owningModelVariableName,
|
|
42
43
|
simpleCreationCommand,
|
|
44
|
+
useDirectModelAccess,
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
function createActionConfiguration(options) {
|
|
@@ -122,6 +124,7 @@ function processAttributeByType({ attributeType, attributeName, isArray, enumVal
|
|
|
122
124
|
case 'string':
|
|
123
125
|
case 'text':
|
|
124
126
|
case 'citext':
|
|
127
|
+
case 'encrypted':
|
|
125
128
|
processStringAttribute({
|
|
126
129
|
attributeName,
|
|
127
130
|
isArray,
|
|
@@ -264,8 +267,8 @@ describe('${fullyQualifiedControllerName}', () => {
|
|
|
264
267
|
function generateIndexActionSpec(options) {
|
|
265
268
|
if (options.actionConfig.omitIndex)
|
|
266
269
|
return '';
|
|
267
|
-
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular
|
|
268
|
-
const subjectFunctionName =
|
|
270
|
+
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular } = options;
|
|
271
|
+
const subjectFunctionName = 'index';
|
|
269
272
|
return `
|
|
270
273
|
|
|
271
274
|
describe('GET index', () => {
|
|
@@ -283,7 +286,7 @@ function generateIndexActionSpec(options) {
|
|
|
283
286
|
id: ${modelConfig.modelVariableName}.id,
|
|
284
287
|
}),
|
|
285
288
|
])
|
|
286
|
-
})${singular ||
|
|
289
|
+
})${singular || modelConfig.useDirectModelAccess
|
|
287
290
|
? ''
|
|
288
291
|
: `
|
|
289
292
|
|
|
@@ -301,8 +304,8 @@ function generateIndexActionSpec(options) {
|
|
|
301
304
|
function generateShowActionSpec(options) {
|
|
302
305
|
if (options.actionConfig.omitShow)
|
|
303
306
|
return '';
|
|
304
|
-
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular,
|
|
305
|
-
const subjectFunctionName =
|
|
307
|
+
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular, attributeData } = options;
|
|
308
|
+
const subjectFunctionName = 'show';
|
|
306
309
|
const subjectFunction = singular
|
|
307
310
|
? `
|
|
308
311
|
const ${subjectFunctionName} = async <StatusCode extends 200 | 400 | 404>(expectedStatus: StatusCode) => {
|
|
@@ -326,7 +329,7 @@ function generateShowActionSpec(options) {
|
|
|
326
329
|
id: ${modelConfig.modelVariableName}.id,${attributeData.comparableOriginalAttributeKeyValues.length ? '\n ' + attributeData.comparableOriginalAttributeKeyValues.join('\n ') : ''}
|
|
327
330
|
}),
|
|
328
331
|
)
|
|
329
|
-
})${singular ||
|
|
332
|
+
})${singular || modelConfig.useDirectModelAccess
|
|
330
333
|
? ''
|
|
331
334
|
: `
|
|
332
335
|
|
|
@@ -342,8 +345,8 @@ function generateShowActionSpec(options) {
|
|
|
342
345
|
function generateCreateActionSpec(options) {
|
|
343
346
|
if (options.actionConfig.omitCreate)
|
|
344
347
|
return '';
|
|
345
|
-
const { path, pathParams, modelConfig, fullyQualifiedModelName,
|
|
346
|
-
const subjectFunctionName =
|
|
348
|
+
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular, attributeData } = options;
|
|
349
|
+
const subjectFunctionName = 'create';
|
|
347
350
|
const uuidSetup = attributeData.uuidAttributes
|
|
348
351
|
.map(attrName => {
|
|
349
352
|
const isArray = attributeData.uuidArrayAttributes.includes(attrName);
|
|
@@ -360,7 +363,7 @@ function generateCreateActionSpec(options) {
|
|
|
360
363
|
? `
|
|
361
364
|
const now = DateTime.now()`
|
|
362
365
|
: ''}${uuidSetup || attributeData.dateAttributeIncluded || attributeData.datetimeAttributeIncluded ? '\n' : ''}`;
|
|
363
|
-
const modelQuery =
|
|
366
|
+
const modelQuery = modelConfig.useDirectModelAccess
|
|
364
367
|
? `${modelConfig.modelClassName}.firstOrFail()`
|
|
365
368
|
: `${modelConfig.owningModelVariableName}.associationQuery('${singular ? modelConfig.modelVariableName : pluralize(modelConfig.modelVariableName)}').firstOrFail()`;
|
|
366
369
|
return `
|
|
@@ -375,7 +378,7 @@ function generateCreateActionSpec(options) {
|
|
|
375
378
|
})
|
|
376
379
|
}
|
|
377
380
|
|
|
378
|
-
it('creates a ${fullyQualifiedModelName}${
|
|
381
|
+
it('creates a ${fullyQualifiedModelName}${modelConfig.useDirectModelAccess ? '' : ` for this ${modelConfig.owningModelName}`}', async () => {${dateTimeSetup}
|
|
379
382
|
const { body } = await ${subjectFunctionName}({
|
|
380
383
|
${attributeData.attributeCreationKeyValues.join('\n ')}
|
|
381
384
|
}, 201)
|
|
@@ -393,8 +396,8 @@ function generateCreateActionSpec(options) {
|
|
|
393
396
|
function generateUpdateActionSpec(options) {
|
|
394
397
|
if (options.actionConfig.omitUpdate)
|
|
395
398
|
return '';
|
|
396
|
-
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular,
|
|
397
|
-
const subjectFunctionName =
|
|
399
|
+
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular, attributeData } = options;
|
|
400
|
+
const subjectFunctionName = 'update';
|
|
398
401
|
const uuidSetup = attributeData.uuidAttributes
|
|
399
402
|
.map(attrName => {
|
|
400
403
|
const isArray = attributeData.uuidArrayAttributes.includes(attrName);
|
|
@@ -434,7 +437,7 @@ function generateUpdateActionSpec(options) {
|
|
|
434
437
|
data,
|
|
435
438
|
})
|
|
436
439
|
}`;
|
|
437
|
-
const updateContextSpec = singular ||
|
|
440
|
+
const updateContextSpec = singular || modelConfig.useDirectModelAccess
|
|
438
441
|
? ''
|
|
439
442
|
: `
|
|
440
443
|
|
|
@@ -488,8 +491,8 @@ function generateUpdateActionSpec(options) {
|
|
|
488
491
|
function generateDestroyActionSpec(options) {
|
|
489
492
|
if (options.actionConfig.omitDestroy)
|
|
490
493
|
return '';
|
|
491
|
-
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular
|
|
492
|
-
const subjectFunctionName =
|
|
494
|
+
const { path, pathParams, modelConfig, fullyQualifiedModelName, singular } = options;
|
|
495
|
+
const subjectFunctionName = 'destroy';
|
|
493
496
|
const subjectFunction = singular
|
|
494
497
|
? `
|
|
495
498
|
const ${subjectFunctionName} = async <StatusCode extends 204 | 400 | 404>(expectedStatus: StatusCode) => {
|
|
@@ -499,7 +502,7 @@ function generateDestroyActionSpec(options) {
|
|
|
499
502
|
const ${subjectFunctionName} = async <StatusCode extends 204 | 400 | 404>(${modelConfig.modelVariableName}: ${modelConfig.modelClassName}, expectedStatus: StatusCode) => {
|
|
500
503
|
return request.delete('/${path}/{id}', expectedStatus, ${formatPathParamsWithId(pathParams, modelConfig.modelVariableName)})
|
|
501
504
|
}`;
|
|
502
|
-
const destroyContextSpec = singular ||
|
|
505
|
+
const destroyContextSpec = singular || modelConfig.useDirectModelAccess
|
|
503
506
|
? ''
|
|
504
507
|
: `
|
|
505
508
|
|
|
@@ -17,6 +17,7 @@ export default async function generateResource({ route, fullyQualifiedModelName,
|
|
|
17
17
|
const resourcefulActions = options.singular ? [...SINGULAR_RESOURCE_ACTIONS] : [...RESOURCE_ACTIONS];
|
|
18
18
|
const onlyActions = options.only?.split(',');
|
|
19
19
|
const forAdmin = /^Admin\//.test(fullyQualifiedControllerName);
|
|
20
|
+
const forInternal = /^Internal\//.test(fullyQualifiedControllerName);
|
|
20
21
|
await DreamCLI.generateDream({
|
|
21
22
|
fullyQualifiedModelName,
|
|
22
23
|
columnsWithTypes,
|
|
@@ -24,7 +25,9 @@ export default async function generateResource({ route, fullyQualifiedModelName,
|
|
|
24
25
|
serializer: true,
|
|
25
26
|
stiBaseSerializer: options.stiBaseSerializer,
|
|
26
27
|
includeAdminSerializers: forAdmin,
|
|
28
|
+
includeInternalSerializers: forInternal,
|
|
27
29
|
connectionName: options.connectionName,
|
|
30
|
+
modelName: options.modelName,
|
|
28
31
|
},
|
|
29
32
|
});
|
|
30
33
|
await generateController({
|
|
@@ -98,6 +98,7 @@ export default class SerializerOpenapiRenderer {
|
|
|
98
98
|
type: 'object',
|
|
99
99
|
required: sort(uniq(requiredProperties.map(property => this.setCase(property)))),
|
|
100
100
|
properties: sortObjectByKey(referencedSerializersAndAttributes.attributes),
|
|
101
|
+
additionalProperties: false,
|
|
101
102
|
},
|
|
102
103
|
};
|
|
103
104
|
}
|
|
@@ -309,7 +309,10 @@ export default class OpenapiSegmentExpander {
|
|
|
309
309
|
data.minProperties = objectBodySegment.minProperties;
|
|
310
310
|
}
|
|
311
311
|
let referencedSerializers = [];
|
|
312
|
-
if (objectBodySegment.additionalProperties) {
|
|
312
|
+
if (objectBodySegment.additionalProperties === false) {
|
|
313
|
+
data.additionalProperties = false;
|
|
314
|
+
}
|
|
315
|
+
else if (objectBodySegment.additionalProperties) {
|
|
313
316
|
const results = this.recursivelyParseBody(objectBodySegment.additionalProperties);
|
|
314
317
|
referencedSerializers = [...referencedSerializers, ...results.referencedSerializers];
|
|
315
318
|
data.additionalProperties = results.openapi;
|
|
@@ -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, } = {}) {
|