@rvoh/psychic 0.37.9 → 0.37.10
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 +36 -2
- package/dist/cjs/src/generate/controller.js +15 -14
- package/dist/cjs/src/generate/helpers/generateControllerContent.js +36 -26
- package/dist/cjs/src/generate/helpers/generateResourceControllerSpecContent.js +120 -74
- package/dist/cjs/src/generate/resource.js +8 -2
- package/dist/esm/src/cli/index.js +36 -2
- package/dist/esm/src/generate/controller.js +15 -14
- package/dist/esm/src/generate/helpers/generateControllerContent.js +36 -26
- package/dist/esm/src/generate/helpers/generateResourceControllerSpecContent.js +121 -75
- package/dist/esm/src/generate/resource.js +8 -2
- package/dist/types/src/generate/helpers/generateControllerContent.d.ts +2 -1
- package/dist/types/src/generate/helpers/generateResourceControllerSpecContent.d.ts +2 -1
- package/package.json +5 -5
|
@@ -9,6 +9,40 @@ const syncEnums_js_1 = __importDefault(require("../generate/initializer/syncEnum
|
|
|
9
9
|
const syncOpenapiTypescript_js_1 = __importDefault(require("../generate/initializer/syncOpenapiTypescript.js"));
|
|
10
10
|
const reduxBindings_js_1 = __importDefault(require("../generate/openapi/reduxBindings.js"));
|
|
11
11
|
const Watcher_js_1 = __importDefault(require("../watcher/Watcher.js"));
|
|
12
|
+
const INDENT = ' ';
|
|
13
|
+
const columnsWithTypesDescription = `space separated snake-case (except for belongs_to model name) properties like this:
|
|
14
|
+
${INDENT} title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal User:belongs_to
|
|
15
|
+
${INDENT}
|
|
16
|
+
${INDENT}all properties default to not nullable; null can be allowed by appending ':optional':
|
|
17
|
+
${INDENT} subtitle:string:optional
|
|
18
|
+
${INDENT}
|
|
19
|
+
${INDENT}supported types:
|
|
20
|
+
${INDENT} - citext:
|
|
21
|
+
${INDENT} case insensitive text (indexes and queries are automatically case insensitive)
|
|
22
|
+
${INDENT}
|
|
23
|
+
${INDENT} - string:
|
|
24
|
+
${INDENT} varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional
|
|
25
|
+
${INDENT}
|
|
26
|
+
${INDENT} - text
|
|
27
|
+
${INDENT}
|
|
28
|
+
${INDENT} - integer
|
|
29
|
+
${INDENT}
|
|
30
|
+
${INDENT} - decimal:
|
|
31
|
+
${INDENT} scale,precision is required, e.g.: volume:decimal:3,2 or volume:decimal:3,2:optional
|
|
32
|
+
${INDENT}
|
|
33
|
+
${INDENT} - enum:
|
|
34
|
+
${INDENT} include the enum name to automatically create the enum:
|
|
35
|
+
${INDENT} type:enum:room_types:bathroom,kitchen,bedroom or type:enum:room_types:bathroom,kitchen,bedroom:optional
|
|
36
|
+
${INDENT}
|
|
37
|
+
${INDENT} omit the enum values to leverage an existing enum (omits the enum type creation):
|
|
38
|
+
${INDENT} type:enum:room_types or type:enum:room_types:optional
|
|
39
|
+
${INDENT}
|
|
40
|
+
${INDENT} - belongs_to:
|
|
41
|
+
${INDENT} Not only updates the migration but also adds a BelongsTo association to the generated model:
|
|
42
|
+
${INDENT} Place:belongs_to
|
|
43
|
+
${INDENT}
|
|
44
|
+
${INDENT} Include the full Path to the model. E.g., if the Coach model is in src/app/models/Health/Coach:
|
|
45
|
+
${INDENT} Health/Coach:belongs_to`;
|
|
12
46
|
class PsychicCLI {
|
|
13
47
|
static provide(program, { initializePsychicApp, seedDb, }) {
|
|
14
48
|
dream_1.DreamCLI.generateDreamCli(program, {
|
|
@@ -23,10 +57,10 @@ class PsychicCLI {
|
|
|
23
57
|
.alias('g:resource')
|
|
24
58
|
.description('create a Dream model, migration, controller, serializer, and spec placeholders')
|
|
25
59
|
.option('--sti-base-serializer', 'omits the serializer from the dream model, but does create the serializer so it can be extended by STI children')
|
|
26
|
-
.option('--owning-model <modelName>', '
|
|
60
|
+
.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") (simply to save time making changes to the generated code). Defaults to User')
|
|
27
61
|
.argument('<path>', 'URL path from root domain')
|
|
28
62
|
.argument('<modelName>', 'the name of the model to create, e.g. Post or Settings/CommunicationPreferences')
|
|
29
|
-
.argument('[columnsWithTypes...]',
|
|
63
|
+
.argument('[columnsWithTypes...]', columnsWithTypesDescription)
|
|
30
64
|
.action(async (route, modelName, columnsWithTypes, options) => {
|
|
31
65
|
await initializePsychicApp();
|
|
32
66
|
await index_js_1.default.generateResource(route, modelName, columnsWithTypes, options);
|
|
@@ -31,7 +31,6 @@ const dream_1 = require("@rvoh/dream");
|
|
|
31
31
|
const node_fs_1 = require("node:fs");
|
|
32
32
|
const fs = __importStar(require("node:fs/promises"));
|
|
33
33
|
const UnexpectedUndefined_js_1 = __importDefault(require("../error/UnexpectedUndefined.js"));
|
|
34
|
-
const EnvInternal_js_1 = __importDefault(require("../helpers/EnvInternal.js"));
|
|
35
34
|
const psychicFileAndDirPaths_js_1 = __importDefault(require("../helpers/path/psychicFileAndDirPaths.js"));
|
|
36
35
|
const psychicPath_js_1 = __importDefault(require("../helpers/path/psychicPath.js"));
|
|
37
36
|
const generateControllerContent_js_1 = __importDefault(require("./helpers/generateControllerContent.js"));
|
|
@@ -44,12 +43,12 @@ async function generateController({ fullyQualifiedControllerName, fullyQualified
|
|
|
44
43
|
fullyQualifiedControllerName = (0, dream_1.standardizeFullyQualifiedModelName)(`${fullyQualifiedControllerName.replace(/Controller$/, '')}Controller`);
|
|
45
44
|
const route = (0, dream_1.hyphenize)(fullyQualifiedControllerName.replace(/Controller$/, ''));
|
|
46
45
|
const allControllerNameParts = fullyQualifiedControllerName.split('/');
|
|
47
|
-
const
|
|
48
|
-
const controllerNameParts =
|
|
46
|
+
const forAdmin = allControllerNameParts[0] === 'Admin';
|
|
47
|
+
const controllerNameParts = forAdmin ? [allControllerNameParts.shift()] : [];
|
|
49
48
|
for (let index = 0; index < allControllerNameParts.length; index++) {
|
|
50
|
-
if (controllerNameParts.length > (
|
|
49
|
+
if (controllerNameParts.length > (forAdmin ? 1 : 0)) {
|
|
51
50
|
// Write the ancestor controller
|
|
52
|
-
const [baseAncestorName, baseAncestorImportStatement] = baseAncestorNameAndImport(controllerNameParts,
|
|
51
|
+
const [baseAncestorName, baseAncestorImportStatement] = baseAncestorNameAndImport(controllerNameParts, forAdmin, { forBaseController: true });
|
|
53
52
|
if (baseAncestorName === undefined)
|
|
54
53
|
throw new UnexpectedUndefined_js_1.default();
|
|
55
54
|
if (baseAncestorImportStatement === undefined)
|
|
@@ -63,6 +62,7 @@ async function generateController({ fullyQualifiedControllerName, fullyQualified
|
|
|
63
62
|
ancestorName: baseAncestorName,
|
|
64
63
|
fullyQualifiedControllerName: baseControllerName,
|
|
65
64
|
omitOpenApi: true,
|
|
65
|
+
forAdmin,
|
|
66
66
|
}));
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -71,7 +71,7 @@ async function generateController({ fullyQualifiedControllerName, fullyQualified
|
|
|
71
71
|
controllerNameParts.push(namedPart);
|
|
72
72
|
}
|
|
73
73
|
// Write the controller
|
|
74
|
-
const [ancestorName, ancestorImportStatement] = baseAncestorNameAndImport(controllerNameParts,
|
|
74
|
+
const [ancestorName, ancestorImportStatement] = baseAncestorNameAndImport(controllerNameParts, forAdmin, {
|
|
75
75
|
forBaseController: false,
|
|
76
76
|
});
|
|
77
77
|
if (ancestorName === undefined)
|
|
@@ -81,8 +81,7 @@ async function generateController({ fullyQualifiedControllerName, fullyQualified
|
|
|
81
81
|
const { relFilePath, absDirPath, absFilePath } = (0, psychicFileAndDirPaths_js_1.default)((0, psychicPath_js_1.default)('controllers'), fullyQualifiedControllerName + `.ts`);
|
|
82
82
|
await fs.mkdir(absDirPath, { recursive: true });
|
|
83
83
|
try {
|
|
84
|
-
|
|
85
|
-
console.log(`generating controller: ${relFilePath}`);
|
|
84
|
+
console.log(`generating controller: ${relFilePath}`);
|
|
86
85
|
await fs.writeFile(absFilePath, (0, generateControllerContent_js_1.default)({
|
|
87
86
|
ancestorImportStatement,
|
|
88
87
|
ancestorName,
|
|
@@ -90,6 +89,7 @@ async function generateController({ fullyQualifiedControllerName, fullyQualified
|
|
|
90
89
|
fullyQualifiedModelName,
|
|
91
90
|
actions,
|
|
92
91
|
owningModel,
|
|
92
|
+
forAdmin,
|
|
93
93
|
}));
|
|
94
94
|
}
|
|
95
95
|
catch (error) {
|
|
@@ -108,22 +108,22 @@ async function generateController({ fullyQualifiedControllerName, fullyQualified
|
|
|
108
108
|
columnsWithTypes,
|
|
109
109
|
resourceSpecs,
|
|
110
110
|
owningModel,
|
|
111
|
+
forAdmin,
|
|
111
112
|
});
|
|
112
113
|
}
|
|
113
|
-
function baseAncestorNameAndImport(controllerNameParts,
|
|
114
|
+
function baseAncestorNameAndImport(controllerNameParts, forAdmin, { forBaseController }) {
|
|
114
115
|
const maybeAncestorNameForBase = `${controllerNameParts.slice(0, controllerNameParts.length - 1).join('')}BaseController`;
|
|
115
116
|
const dotFiles = forBaseController ? '..' : '.';
|
|
116
|
-
return controllerNameParts.length === (
|
|
117
|
-
?
|
|
117
|
+
return controllerNameParts.length === (forAdmin ? 2 : 1)
|
|
118
|
+
? forAdmin
|
|
118
119
|
? [`AdminAuthedController`, `import AdminAuthedController from '${dotFiles}/AuthedController.js'`]
|
|
119
120
|
: [`AuthedController`, `import AuthedController from '${dotFiles}/AuthedController.js'`]
|
|
120
121
|
: [maybeAncestorNameForBase, `import ${maybeAncestorNameForBase} from '${dotFiles}/BaseController.js'`];
|
|
121
122
|
}
|
|
122
|
-
async function generateControllerSpec({ fullyQualifiedControllerName, route, fullyQualifiedModelName, columnsWithTypes, resourceSpecs, owningModel, }) {
|
|
123
|
+
async function generateControllerSpec({ fullyQualifiedControllerName, route, fullyQualifiedModelName, columnsWithTypes, resourceSpecs, owningModel, forAdmin, }) {
|
|
123
124
|
const { relFilePath, absDirPath, absFilePath } = (0, psychicFileAndDirPaths_js_1.default)((0, psychicPath_js_1.default)('controllerSpecs'), fullyQualifiedControllerName + `.spec.ts`);
|
|
124
125
|
try {
|
|
125
|
-
|
|
126
|
-
console.log(`generating controller: ${relFilePath}`);
|
|
126
|
+
console.log(`generating controller spec: ${relFilePath}`);
|
|
127
127
|
await fs.mkdir(absDirPath, { recursive: true });
|
|
128
128
|
await fs.writeFile(absFilePath, resourceSpecs && fullyQualifiedModelName
|
|
129
129
|
? (0, generateResourceControllerSpecContent_js_1.default)({
|
|
@@ -132,6 +132,7 @@ async function generateControllerSpec({ fullyQualifiedControllerName, route, ful
|
|
|
132
132
|
fullyQualifiedModelName,
|
|
133
133
|
columnsWithTypes,
|
|
134
134
|
owningModel,
|
|
135
|
+
forAdmin,
|
|
135
136
|
})
|
|
136
137
|
: (0, generateControllerSpecContent_js_1.default)(fullyQualifiedControllerName));
|
|
137
138
|
}
|
|
@@ -7,7 +7,7 @@ exports.default = generateControllerContent;
|
|
|
7
7
|
const dream_1 = require("@rvoh/dream");
|
|
8
8
|
const pluralize_esm_1 = __importDefault(require("pluralize-esm"));
|
|
9
9
|
const relativePsychicPath_js_1 = __importDefault(require("../../helpers/path/relativePsychicPath.js"));
|
|
10
|
-
function generateControllerContent({ ancestorName, ancestorImportStatement, fullyQualifiedControllerName, fullyQualifiedModelName, actions = [], omitOpenApi = false, owningModel, }) {
|
|
10
|
+
function generateControllerContent({ ancestorName, ancestorImportStatement, fullyQualifiedControllerName, fullyQualifiedModelName, actions = [], omitOpenApi = false, owningModel, forAdmin, }) {
|
|
11
11
|
fullyQualifiedControllerName = (0, dream_1.standardizeFullyQualifiedModelName)(fullyQualifiedControllerName);
|
|
12
12
|
const additionalImports = [];
|
|
13
13
|
const controllerClassName = (0, dream_1.globalClassNameFromFullyQualifiedModelName)(fullyQualifiedControllerName);
|
|
@@ -25,6 +25,13 @@ function generateControllerContent({ ancestorName, ancestorImportStatement, full
|
|
|
25
25
|
pluralizedModelAttributeName = (0, pluralize_esm_1.default)(modelAttributeName);
|
|
26
26
|
additionalImports.push(importStatementForModel(fullyQualifiedControllerName, fullyQualifiedModelName));
|
|
27
27
|
}
|
|
28
|
+
const defaultOpenapiSerializerKeyProperty = forAdmin
|
|
29
|
+
? `
|
|
30
|
+
serializerKey: 'admin',`
|
|
31
|
+
: '';
|
|
32
|
+
const loadQueryBase = forAdmin
|
|
33
|
+
? (modelClassName ?? 'no-class-name')
|
|
34
|
+
: `this.${owningModelProperty}.associationQuery('${pluralizedModelAttributeName}')`;
|
|
28
35
|
const methodDefs = actions.map(methodName => {
|
|
29
36
|
switch (methodName) {
|
|
30
37
|
case 'create':
|
|
@@ -33,11 +40,12 @@ function generateControllerContent({ ancestorName, ancestorImportStatement, full
|
|
|
33
40
|
@OpenAPI(${modelClassName}, {
|
|
34
41
|
status: 201,
|
|
35
42
|
tags: openApiTags,
|
|
36
|
-
description: 'Create ${aOrAnDreamModelName(modelClassName)}'
|
|
43
|
+
description: 'Create ${aOrAnDreamModelName(modelClassName)}',${defaultOpenapiSerializerKeyProperty}
|
|
37
44
|
})
|
|
38
45
|
public async create() {
|
|
39
|
-
//
|
|
40
|
-
//
|
|
46
|
+
// let ${modelAttributeName} = await ${forAdmin ? `${modelClassName}.create(` : `this.${owningModelProperty}.createAssociation('${pluralizedModelAttributeName}', `}this.paramsFor(${modelClassName}))
|
|
47
|
+
// if (${modelAttributeName}.isPersisted) ${modelAttributeName} = await ${modelAttributeName}.loadFor('${forAdmin ? 'admin' : 'default'}').execute()
|
|
48
|
+
// this.created(${modelAttributeName})
|
|
41
49
|
}`;
|
|
42
50
|
else
|
|
43
51
|
return `\
|
|
@@ -56,11 +64,13 @@ function generateControllerContent({ ancestorName, ancestorImportStatement, full
|
|
|
56
64
|
tags: openApiTags,
|
|
57
65
|
description: 'Fetch multiple ${(0, pluralize_esm_1.default)(modelClassName)}',
|
|
58
66
|
many: true,
|
|
59
|
-
serializerKey: 'summary',
|
|
67
|
+
serializerKey: '${forAdmin ? 'adminSummary' : 'summary'}',
|
|
60
68
|
})
|
|
61
69
|
public async index() {
|
|
62
|
-
//
|
|
63
|
-
//
|
|
70
|
+
// const ${pluralizedModelAttributeName} = await ${loadQueryBase}
|
|
71
|
+
// .preloadFor('${forAdmin ? 'adminSummary' : 'summary'}')
|
|
72
|
+
// .all()
|
|
73
|
+
// this.ok(${pluralizedModelAttributeName})
|
|
64
74
|
}`;
|
|
65
75
|
else
|
|
66
76
|
return `\
|
|
@@ -69,7 +79,7 @@ function generateControllerContent({ ancestorName, ancestorImportStatement, full
|
|
|
69
79
|
// tags: openApiTags,
|
|
70
80
|
// description: '<tbd>',
|
|
71
81
|
// many: true,
|
|
72
|
-
// serializerKey: 'summary',
|
|
82
|
+
// serializerKey: '${forAdmin ? 'adminSummary' : 'summary'}',
|
|
73
83
|
// })
|
|
74
84
|
public async index() {
|
|
75
85
|
}`;
|
|
@@ -79,11 +89,11 @@ function generateControllerContent({ ancestorName, ancestorImportStatement, full
|
|
|
79
89
|
@OpenAPI(${modelClassName}, {
|
|
80
90
|
status: 200,
|
|
81
91
|
tags: openApiTags,
|
|
82
|
-
description: 'Fetch ${aOrAnDreamModelName(modelClassName)}'
|
|
92
|
+
description: 'Fetch ${aOrAnDreamModelName(modelClassName)}',${defaultOpenapiSerializerKeyProperty}
|
|
83
93
|
})
|
|
84
94
|
public async show() {
|
|
85
|
-
//
|
|
86
|
-
//
|
|
95
|
+
// const ${modelAttributeName} = await this.${modelAttributeName}()
|
|
96
|
+
// this.ok(${modelAttributeName})
|
|
87
97
|
}`;
|
|
88
98
|
else
|
|
89
99
|
return `\
|
|
@@ -103,9 +113,9 @@ function generateControllerContent({ ancestorName, ancestorImportStatement, full
|
|
|
103
113
|
description: 'Update ${aOrAnDreamModelName(modelClassName)}',
|
|
104
114
|
})
|
|
105
115
|
public async update() {
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
116
|
+
// const ${modelAttributeName} = await this.${modelAttributeName}()
|
|
117
|
+
// await ${modelAttributeName}.update(this.paramsFor(${modelClassName}))
|
|
118
|
+
// this.noContent()
|
|
109
119
|
}`;
|
|
110
120
|
else
|
|
111
121
|
return `\
|
|
@@ -125,9 +135,9 @@ function generateControllerContent({ ancestorName, ancestorImportStatement, full
|
|
|
125
135
|
description: 'Destroy ${aOrAnDreamModelName(modelClassName)}',
|
|
126
136
|
})
|
|
127
137
|
public async destroy() {
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
138
|
+
// const ${modelAttributeName} = await this.${modelAttributeName}()
|
|
139
|
+
// await ${modelAttributeName}.destroy()
|
|
140
|
+
// this.noContent()
|
|
131
141
|
}`;
|
|
132
142
|
else
|
|
133
143
|
return `\
|
|
@@ -147,8 +157,8 @@ function generateControllerContent({ ancestorName, ancestorImportStatement, full
|
|
|
147
157
|
description: 'Fetch ${aOrAnDreamModelName(modelClassName)}',
|
|
148
158
|
})
|
|
149
159
|
public async ${methodName}() {
|
|
150
|
-
//
|
|
151
|
-
//
|
|
160
|
+
// const ${modelAttributeName} = await this.${modelAttributeName}()
|
|
161
|
+
// this.ok(${modelAttributeName})
|
|
152
162
|
}`;
|
|
153
163
|
else
|
|
154
164
|
return `\
|
|
@@ -167,23 +177,23 @@ function generateControllerContent({ ancestorName, ancestorImportStatement, full
|
|
|
167
177
|
${omitOpenApi ? '' : openApiImport + '\n'}${ancestorImportStatement}${additionalImports.length ? '\n' + additionalImports.join('\n') : ''}${omitOpenApi ? '' : '\n\n' + openApiTags}
|
|
168
178
|
|
|
169
179
|
export default class ${controllerClassName} extends ${ancestorName} {
|
|
170
|
-
${methodDefs.join('\n\n')}${modelClassName ? privateMethods(modelClassName, actions,
|
|
180
|
+
${methodDefs.join('\n\n')}${modelClassName ? privateMethods(forAdmin, modelClassName, actions, loadQueryBase) : ''}
|
|
171
181
|
}
|
|
172
182
|
`;
|
|
173
183
|
}
|
|
174
|
-
function privateMethods(modelClassName, methods,
|
|
184
|
+
function privateMethods(forAdmin, modelClassName, methods, loadQueryBase) {
|
|
175
185
|
const privateMethods = [];
|
|
176
186
|
if (methods.find(methodName => ['show', 'update', 'destroy'].includes(methodName)))
|
|
177
|
-
privateMethods.push(loadModelStatement(modelClassName,
|
|
187
|
+
privateMethods.push(loadModelStatement(forAdmin, modelClassName, loadQueryBase));
|
|
178
188
|
if (!privateMethods.length)
|
|
179
189
|
return '';
|
|
180
190
|
return `\n\n${privateMethods.join('\n\n')}`;
|
|
181
191
|
}
|
|
182
|
-
function loadModelStatement(modelClassName,
|
|
192
|
+
function loadModelStatement(forAdmin, modelClassName, loadQueryBase) {
|
|
183
193
|
return ` private async ${(0, dream_1.camelize)(modelClassName)}() {
|
|
184
|
-
// return await
|
|
185
|
-
//
|
|
186
|
-
// )
|
|
194
|
+
// return await ${loadQueryBase}
|
|
195
|
+
// .preloadFor('${forAdmin ? 'admin' : 'default'}')
|
|
196
|
+
// .findOrFail(this.castParam('id', 'string'))
|
|
187
197
|
}`;
|
|
188
198
|
}
|
|
189
199
|
function importStatementForModel(originControllerName, destinationModelName) {
|
|
@@ -7,75 +7,116 @@ exports.default = generateResourceControllerSpecContent;
|
|
|
7
7
|
const dream_1 = require("@rvoh/dream");
|
|
8
8
|
const relativePsychicPath_js_1 = __importDefault(require("../../helpers/path/relativePsychicPath.js"));
|
|
9
9
|
const updirsFromPath_js_1 = __importDefault(require("../../helpers/path/updirsFromPath.js"));
|
|
10
|
-
|
|
10
|
+
const index_js_1 = require("../../index.js");
|
|
11
|
+
function generateResourceControllerSpecContent({ fullyQualifiedControllerName, route, fullyQualifiedModelName, columnsWithTypes, owningModel, forAdmin, }) {
|
|
11
12
|
fullyQualifiedModelName = (0, dream_1.standardizeFullyQualifiedModelName)(fullyQualifiedModelName);
|
|
12
13
|
const modelClassName = (0, dream_1.globalClassNameFromFullyQualifiedModelName)(fullyQualifiedModelName);
|
|
13
14
|
const modelVariableName = (0, dream_1.camelize)(modelClassName);
|
|
14
15
|
// Always use User for authentication
|
|
15
|
-
const
|
|
16
|
-
const userVariableName = 'user';
|
|
16
|
+
const userModelClassName = forAdmin ? 'AdminUser' : 'User';
|
|
17
|
+
const userVariableName = forAdmin ? 'adminUser' : 'user';
|
|
17
18
|
// Determine attached model settings if provided
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
const owningModelClassName = owningModel
|
|
20
|
+
? (0, dream_1.globalClassNameFromFullyQualifiedModelName)(owningModel)
|
|
21
|
+
: userModelClassName;
|
|
22
|
+
const owningModelVariableName = owningModelClassName ? (0, dream_1.camelize)(owningModelClassName) : userVariableName;
|
|
23
|
+
const importStatements = (0, dream_1.compact)([
|
|
21
24
|
importStatementForModel(fullyQualifiedControllerName, fullyQualifiedModelName),
|
|
22
|
-
importStatementForModel(fullyQualifiedControllerName,
|
|
23
|
-
|
|
25
|
+
importStatementForModel(fullyQualifiedControllerName, userModelClassName),
|
|
26
|
+
owningModel ? importStatementForModel(fullyQualifiedControllerName, owningModel) : undefined,
|
|
24
27
|
importStatementForModelFactory(fullyQualifiedControllerName, fullyQualifiedModelName),
|
|
25
|
-
importStatementForModelFactory(fullyQualifiedControllerName,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (owningModel) {
|
|
29
|
-
importStatements.push(importStatementForModel(fullyQualifiedControllerName, owningModel), importStatementForModelFactory(fullyQualifiedControllerName, owningModel));
|
|
30
|
-
}
|
|
28
|
+
importStatementForModelFactory(fullyQualifiedControllerName, userModelClassName),
|
|
29
|
+
owningModel ? importStatementForModelFactory(fullyQualifiedControllerName, owningModel) : undefined,
|
|
30
|
+
]);
|
|
31
31
|
const specUnitUpdirs = (0, updirsFromPath_js_1.default)(fullyQualifiedControllerName);
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
32
|
+
const attributeCreationKeyValues = [];
|
|
33
|
+
const attributeUpdateKeyValues = [];
|
|
34
|
+
const originalValueAttributeChecks = [];
|
|
35
|
+
const updatedValueAttributeChecks = [];
|
|
36
|
+
const nonUpdatedValueAttributeChecks = [];
|
|
37
|
+
const originalValueVariableAssignments = [];
|
|
38
|
+
const keyWithDotValue = [];
|
|
36
39
|
for (const attribute of columnsWithTypes) {
|
|
37
|
-
const [
|
|
40
|
+
const [rawAttributeName, attributeType, , enumValues] = attribute.split(':');
|
|
41
|
+
if (rawAttributeName === 'type')
|
|
42
|
+
continue;
|
|
43
|
+
if (/(_type|_id)$/.test(rawAttributeName ?? ''))
|
|
44
|
+
continue;
|
|
45
|
+
const attributeName = (0, dream_1.camelize)(rawAttributeName ?? '');
|
|
38
46
|
const originalName = `The ${fullyQualifiedModelName} ${attributeName}`;
|
|
39
47
|
const updatedName = `Updated ${fullyQualifiedModelName} ${attributeName}`;
|
|
48
|
+
const dotNotationVariable = `${modelVariableName}.${attributeName}`;
|
|
40
49
|
switch (attributeType) {
|
|
50
|
+
case 'enum': {
|
|
51
|
+
if (attribute === 'type')
|
|
52
|
+
continue;
|
|
53
|
+
const originalEnumValue = (enumValues ?? '').split(',').at(0);
|
|
54
|
+
const updatedEnumValue = (enumValues ?? '').split(',').at(-1);
|
|
55
|
+
attributeCreationKeyValues.push(`${attributeName}: '${originalEnumValue}',`);
|
|
56
|
+
attributeUpdateKeyValues.push(`${attributeName}: '${updatedEnumValue}',`);
|
|
57
|
+
originalValueAttributeChecks.push(`expect(${dotNotationVariable}).toEqual('${originalEnumValue}')`);
|
|
58
|
+
updatedValueAttributeChecks.push(`expect(${dotNotationVariable}).toEqual('${updatedEnumValue}')`);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
41
61
|
case 'string':
|
|
42
62
|
case 'text':
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
case 'citext':
|
|
64
|
+
attributeCreationKeyValues.push(`${attributeName}: '${originalName}',`);
|
|
65
|
+
attributeUpdateKeyValues.push(`${attributeName}: '${updatedName}',`);
|
|
66
|
+
originalValueAttributeChecks.push(`expect(${dotNotationVariable}).toEqual('${originalName}')`);
|
|
67
|
+
updatedValueAttributeChecks.push(`expect(${dotNotationVariable}).toEqual('${updatedName}')`);
|
|
68
|
+
break;
|
|
69
|
+
case 'integer':
|
|
70
|
+
attributeCreationKeyValues.push(`${attributeName}: 1,`);
|
|
71
|
+
attributeUpdateKeyValues.push(`${attributeName}: 2,`);
|
|
72
|
+
originalValueAttributeChecks.push(`expect(${dotNotationVariable}).toEqual(1)`);
|
|
73
|
+
updatedValueAttributeChecks.push(`expect(${dotNotationVariable}).toEqual(2)`);
|
|
74
|
+
break;
|
|
75
|
+
case 'bigint':
|
|
76
|
+
attributeCreationKeyValues.push(`${attributeName}: '11111111111111111',`);
|
|
77
|
+
attributeUpdateKeyValues.push(`${attributeName}: '22222222222222222',`);
|
|
78
|
+
originalValueAttributeChecks.push(`expect(${dotNotationVariable}).toEqual('11111111111111111')`);
|
|
79
|
+
updatedValueAttributeChecks.push(`expect(${dotNotationVariable}).toEqual('22222222222222222')`);
|
|
80
|
+
break;
|
|
81
|
+
case 'decimal':
|
|
82
|
+
attributeCreationKeyValues.push(`${attributeName}: 1.1,`);
|
|
83
|
+
attributeUpdateKeyValues.push(`${attributeName}: 2.2,`);
|
|
84
|
+
originalValueAttributeChecks.push(`expect(${dotNotationVariable}).toEqual(1.1)`);
|
|
85
|
+
updatedValueAttributeChecks.push(`expect(${dotNotationVariable}).toEqual(2.2)`);
|
|
47
86
|
break;
|
|
48
87
|
default:
|
|
49
|
-
|
|
88
|
+
continue;
|
|
50
89
|
}
|
|
90
|
+
keyWithDotValue.push(`${attributeName}: ${dotNotationVariable},`);
|
|
91
|
+
const originalAttributeVariableName = 'original' + (0, dream_1.capitalize)(attributeName);
|
|
92
|
+
originalValueVariableAssignments.push(`const ${originalAttributeVariableName} = ${dotNotationVariable}`);
|
|
93
|
+
nonUpdatedValueAttributeChecks.push(`expect(${dotNotationVariable}).toEqual(${originalAttributeVariableName})`);
|
|
51
94
|
}
|
|
95
|
+
const simpleCreationCommand = `const ${modelVariableName} = await create${modelClassName}(${forAdmin ? '' : `{ ${owningModelVariableName} }`})`;
|
|
96
|
+
const dotValueAttributes = keyWithDotValue.length
|
|
97
|
+
? '\n ' + keyWithDotValue.join('\n ')
|
|
98
|
+
: '';
|
|
52
99
|
return `\
|
|
53
|
-
import { UpdateableProperties } from '@rvoh/dream'
|
|
54
|
-
import {
|
|
55
|
-
import { OpenapiSpecRequest } from '@rvoh/psychic-spec-helpers'${(0, dream_1.uniq)(importStatements).join('')}
|
|
56
|
-
import addEndUserAuthHeader from '${specUnitUpdirs}helpers/authentication.js'
|
|
57
|
-
|
|
58
|
-
const request = new OpenapiSpecRequest<validationOpenapiPaths>()
|
|
100
|
+
import { UpdateableProperties } from '@rvoh/dream'${(0, dream_1.uniq)(importStatements).join('')}
|
|
101
|
+
import { session, SpecRequestType } from '${specUnitUpdirs}helpers/authentication.js'
|
|
59
102
|
|
|
60
103
|
describe('${fullyQualifiedControllerName}', () => {
|
|
61
|
-
let
|
|
104
|
+
let request: SpecRequestType
|
|
105
|
+
let ${userVariableName}: ${userModelClassName}${owningModel ? `\n let ${owningModelVariableName}: ${owningModelClassName}` : ''}
|
|
62
106
|
|
|
63
107
|
beforeEach(async () => {
|
|
64
|
-
await
|
|
65
|
-
|
|
108
|
+
${userVariableName} = await create${userModelClassName}()${owningModel ? `\n ${owningModelVariableName} = await create${owningModelClassName}({ ${userVariableName} })` : ''}
|
|
109
|
+
request = await session(${userVariableName})
|
|
66
110
|
})
|
|
67
111
|
|
|
68
112
|
describe('GET index', () => {
|
|
69
113
|
const subject = async <StatusCode extends 200 | 400>(expectedStatus: StatusCode) => {
|
|
70
|
-
return request.get('/${route}', expectedStatus
|
|
71
|
-
headers: await addEndUserAuthHeader(request, ${userVariableName}, {}),
|
|
72
|
-
})
|
|
114
|
+
return request.get('/${route}', expectedStatus)
|
|
73
115
|
}
|
|
74
116
|
|
|
75
117
|
it('returns the index of ${fullyQualifiedModelName}s', async () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
})
|
|
118
|
+
${simpleCreationCommand}
|
|
119
|
+
|
|
79
120
|
const { body } = await subject(200)
|
|
80
121
|
|
|
81
122
|
expect(body).toEqual([
|
|
@@ -83,45 +124,49 @@ describe('${fullyQualifiedControllerName}', () => {
|
|
|
83
124
|
id: ${modelVariableName}.id,
|
|
84
125
|
}),
|
|
85
126
|
])
|
|
86
|
-
})
|
|
127
|
+
})${forAdmin
|
|
128
|
+
? ''
|
|
129
|
+
: `
|
|
87
130
|
|
|
88
131
|
context('${modelClassName}s created by another ${owningModelClassName}', () => {
|
|
89
132
|
it('are omitted', async () => {
|
|
90
133
|
await create${modelClassName}()
|
|
134
|
+
|
|
91
135
|
const { body } = await subject(200)
|
|
92
136
|
|
|
93
137
|
expect(body).toEqual([])
|
|
94
138
|
})
|
|
95
|
-
})
|
|
139
|
+
})`}
|
|
96
140
|
})
|
|
97
141
|
|
|
98
142
|
describe('GET show', () => {
|
|
99
143
|
const subject = async <StatusCode extends 200 | 400 | 404>(${modelVariableName}: ${modelClassName}, expectedStatus: StatusCode) => {
|
|
100
144
|
return request.get('/${route}/{id}', expectedStatus, {
|
|
101
145
|
id: ${modelVariableName}.id,
|
|
102
|
-
headers: await addEndUserAuthHeader(request, ${userVariableName}, {}),
|
|
103
146
|
})
|
|
104
147
|
}
|
|
105
148
|
|
|
106
149
|
it('returns the specified ${fullyQualifiedModelName}', async () => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
})
|
|
150
|
+
${simpleCreationCommand}
|
|
151
|
+
|
|
110
152
|
const { body } = await subject(${modelVariableName}, 200)
|
|
111
153
|
|
|
112
154
|
expect(body).toEqual(
|
|
113
155
|
expect.objectContaining({
|
|
114
|
-
id: ${modelVariableName}.id,${
|
|
156
|
+
id: ${modelVariableName}.id,${dotValueAttributes}
|
|
115
157
|
}),
|
|
116
158
|
)
|
|
117
|
-
})
|
|
159
|
+
})${forAdmin
|
|
160
|
+
? ''
|
|
161
|
+
: `
|
|
118
162
|
|
|
119
163
|
context('${fullyQualifiedModelName} created by another ${owningModelClassName}', () => {
|
|
120
164
|
it('is not found', async () => {
|
|
121
165
|
const other${owningModelClassName}${modelClassName} = await create${modelClassName}()
|
|
166
|
+
|
|
122
167
|
await subject(other${owningModelClassName}${modelClassName}, 404)
|
|
123
168
|
})
|
|
124
|
-
})
|
|
169
|
+
})`}
|
|
125
170
|
})
|
|
126
171
|
|
|
127
172
|
describe('POST create', () => {
|
|
@@ -129,21 +174,21 @@ describe('${fullyQualifiedControllerName}', () => {
|
|
|
129
174
|
data: UpdateableProperties<${modelClassName}>,
|
|
130
175
|
expectedStatus: StatusCode
|
|
131
176
|
) => {
|
|
132
|
-
return request.post('/${route}', expectedStatus, {
|
|
133
|
-
data,
|
|
134
|
-
headers: await addEndUserAuthHeader(request, ${userVariableName}, {}),
|
|
135
|
-
})
|
|
177
|
+
return request.post('/${route}', expectedStatus, { data })
|
|
136
178
|
}
|
|
137
179
|
|
|
138
|
-
it('creates a ${fullyQualifiedModelName} for this ${owningModelClassName}', async () => {
|
|
180
|
+
it('creates a ${fullyQualifiedModelName}${forAdmin ? '' : ` for this ${owningModelClassName}`}', async () => {
|
|
139
181
|
const { body } = await subject({
|
|
140
|
-
${
|
|
182
|
+
${attributeCreationKeyValues.join('\n ')}
|
|
141
183
|
}, 201)
|
|
142
|
-
|
|
184
|
+
|
|
185
|
+
const ${modelVariableName} = await ${forAdmin
|
|
186
|
+
? `${modelClassName}.firstOrFail()`
|
|
187
|
+
: `${owningModelVariableName}.associationQuery('${(0, index_js_1.pluralize)(modelVariableName)}').firstOrFail()`}
|
|
143
188
|
|
|
144
189
|
expect(body).toEqual(
|
|
145
190
|
expect.objectContaining({
|
|
146
|
-
id: ${modelVariableName}.id,${
|
|
191
|
+
id: ${modelVariableName}.id,${attributeCreationKeyValues.length ? '\n ' + attributeCreationKeyValues.join('\n ') : ''}
|
|
147
192
|
}),
|
|
148
193
|
)
|
|
149
194
|
})
|
|
@@ -158,58 +203,63 @@ describe('${fullyQualifiedControllerName}', () => {
|
|
|
158
203
|
return request.patch('/${route}/{id}', expectedStatus, {
|
|
159
204
|
id: ${modelVariableName}.id,
|
|
160
205
|
data,
|
|
161
|
-
headers: await addEndUserAuthHeader(request, ${userVariableName}, {}),
|
|
162
206
|
})
|
|
163
207
|
}
|
|
164
208
|
|
|
165
209
|
it('updates the ${fullyQualifiedModelName}', async () => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
})
|
|
210
|
+
${simpleCreationCommand}
|
|
211
|
+
|
|
169
212
|
await subject(${modelVariableName}, {
|
|
170
|
-
${
|
|
213
|
+
${attributeUpdateKeyValues.length ? attributeUpdateKeyValues.join('\n ') : ''}
|
|
171
214
|
}, 204)
|
|
172
215
|
|
|
173
216
|
await ${modelVariableName}.reload()
|
|
174
|
-
${
|
|
175
|
-
})
|
|
217
|
+
${updatedValueAttributeChecks.join('\n ')}
|
|
218
|
+
})${forAdmin
|
|
219
|
+
? ''
|
|
220
|
+
: `
|
|
176
221
|
|
|
177
222
|
context('a ${fullyQualifiedModelName} created by another ${owningModelClassName}', () => {
|
|
178
223
|
it('is not updated', async () => {
|
|
179
224
|
const ${modelVariableName} = await create${modelClassName}()
|
|
225
|
+
${originalValueVariableAssignments.length ? originalValueVariableAssignments.join('\n ') : ''}
|
|
226
|
+
|
|
180
227
|
await subject(${modelVariableName}, {
|
|
181
|
-
${
|
|
228
|
+
${attributeUpdateKeyValues.length ? attributeUpdateKeyValues.join('\n ') : ''}
|
|
182
229
|
}, 404)
|
|
183
230
|
|
|
184
231
|
await ${modelVariableName}.reload()
|
|
185
|
-
${
|
|
232
|
+
${nonUpdatedValueAttributeChecks.join('\n ')}
|
|
186
233
|
})
|
|
187
|
-
})
|
|
234
|
+
})`}
|
|
188
235
|
})
|
|
189
236
|
|
|
190
237
|
describe('DELETE destroy', () => {
|
|
191
238
|
const subject = async <StatusCode extends 204 | 400 | 404>(${modelVariableName}: ${modelClassName}, expectedStatus: StatusCode) => {
|
|
192
239
|
return request.delete('/${route}/{id}', expectedStatus, {
|
|
193
240
|
id: ${modelVariableName}.id,
|
|
194
|
-
headers: await addEndUserAuthHeader(request, ${userVariableName}, {}),
|
|
195
241
|
})
|
|
196
242
|
}
|
|
197
243
|
|
|
198
244
|
it('deletes the ${fullyQualifiedModelName}', async () => {
|
|
199
|
-
|
|
245
|
+
${simpleCreationCommand}
|
|
246
|
+
|
|
200
247
|
await subject(${modelVariableName}, 204)
|
|
201
248
|
|
|
202
249
|
expect(await ${modelClassName}.find(${modelVariableName}.id)).toBeNull()
|
|
203
|
-
})
|
|
250
|
+
})${forAdmin
|
|
251
|
+
? ''
|
|
252
|
+
: `
|
|
204
253
|
|
|
205
254
|
context('a ${fullyQualifiedModelName} created by another ${owningModelClassName}', () => {
|
|
206
255
|
it('is not deleted', async () => {
|
|
207
256
|
const ${modelVariableName} = await create${modelClassName}()
|
|
257
|
+
|
|
208
258
|
await subject(${modelVariableName}, 404)
|
|
209
259
|
|
|
210
260
|
expect(await ${modelClassName}.find(${modelVariableName}.id)).toMatchDreamModel(${modelVariableName})
|
|
211
261
|
})
|
|
212
|
-
})
|
|
262
|
+
})`}
|
|
213
263
|
})
|
|
214
264
|
})
|
|
215
265
|
`;
|
|
@@ -217,10 +267,6 @@ describe('${fullyQualifiedControllerName}', () => {
|
|
|
217
267
|
function importStatementForModel(originModelName, destinationModelName = originModelName) {
|
|
218
268
|
return `\nimport ${(0, dream_1.globalClassNameFromFullyQualifiedModelName)(destinationModelName)} from '${(0, relativePsychicPath_js_1.default)('controllerSpecs', 'models', originModelName, destinationModelName)}'`;
|
|
219
269
|
}
|
|
220
|
-
function importStatementForType(typeFilePath, typeExportName) {
|
|
221
|
-
const importPath = (0, relativePsychicPath_js_1.default)('controllerSpecs', 'types', typeFilePath).toLowerCase();
|
|
222
|
-
return `\nimport { ${typeExportName} } from '${importPath}'`;
|
|
223
|
-
}
|
|
224
270
|
function importStatementForModelFactory(originModelName, destinationModelName = originModelName) {
|
|
225
271
|
return `\nimport create${(0, dream_1.globalClassNameFromFullyQualifiedModelName)(destinationModelName)} from '${(0, relativePsychicPath_js_1.default)('controllerSpecs', 'factories', originModelName, destinationModelName)}'`;
|
|
226
272
|
}
|