@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.
@@ -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>', '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')
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...]', 'properties of the model property1:text/string/enum/etc. property2:text/string/enum/etc. ... propertyN:text/string/enum/etc.')
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 isAdmin = allControllerNameParts[0] === 'Admin';
48
- const controllerNameParts = isAdmin ? [allControllerNameParts.shift()] : [];
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 > (isAdmin ? 1 : 0)) {
49
+ if (controllerNameParts.length > (forAdmin ? 1 : 0)) {
51
50
  // Write the ancestor controller
52
- const [baseAncestorName, baseAncestorImportStatement] = baseAncestorNameAndImport(controllerNameParts, isAdmin, { forBaseController: true });
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, isAdmin, {
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
- if (!EnvInternal_js_1.default.isTest)
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, isAdmin, { forBaseController }) {
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 === (isAdmin ? 2 : 1)
117
- ? isAdmin
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
- if (!EnvInternal_js_1.default.isTest)
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
- // const ${modelAttributeName} = await this.${owningModelProperty}.createAssociation('${pluralizedModelAttributeName}', this.paramsFor(${modelClassName}))
40
- // this.created(${modelAttributeName})
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
- // const ${pluralizedModelAttributeName} = await this.${owningModelProperty}.associationQuery('${pluralizedModelAttributeName}').all()
63
- // this.ok(${pluralizedModelAttributeName})
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
- // const ${modelAttributeName} = await this.${modelAttributeName}()
86
- // this.ok(${modelAttributeName})
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
- // const ${modelAttributeName} = await this.${modelAttributeName}()
107
- // await ${modelAttributeName}.update(this.paramsFor(${modelClassName}))
108
- // this.noContent()
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
- // const ${modelAttributeName} = await this.${modelAttributeName}()
129
- // await ${modelAttributeName}.destroy()
130
- // this.noContent()
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
- // const ${modelAttributeName} = await this.${modelAttributeName}()
151
- // this.ok(${modelAttributeName})
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, owningModelProperty) : ''}
180
+ ${methodDefs.join('\n\n')}${modelClassName ? privateMethods(forAdmin, modelClassName, actions, loadQueryBase) : ''}
171
181
  }
172
182
  `;
173
183
  }
174
- function privateMethods(modelClassName, methods, owningModelProperty) {
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, owningModelProperty));
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, owningModelProperty) {
192
+ function loadModelStatement(forAdmin, modelClassName, loadQueryBase) {
183
193
  return ` private async ${(0, dream_1.camelize)(modelClassName)}() {
184
- // return await this.${owningModelProperty}.associationQuery('${(0, pluralize_esm_1.default)((0, dream_1.camelize)(modelClassName))}').findOrFail(
185
- // this.castParam('id', 'string')
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
- function generateResourceControllerSpecContent({ fullyQualifiedControllerName, route, fullyQualifiedModelName, columnsWithTypes, owningModel, }) {
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 owningModelClassName = 'User';
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 attachedModelClassName = owningModel ? (0, dream_1.globalClassNameFromFullyQualifiedModelName)(owningModel) : null;
19
- const attachedModelVariableName = attachedModelClassName ? (0, dream_1.camelize)(attachedModelClassName) : null;
20
- const importStatements = [
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, 'User'),
23
- importStatementForType('openapi/validation.openapi', 'validationOpenapiPaths'),
25
+ importStatementForModel(fullyQualifiedControllerName, userModelClassName),
26
+ owningModel ? importStatementForModel(fullyQualifiedControllerName, owningModel) : undefined,
24
27
  importStatementForModelFactory(fullyQualifiedControllerName, fullyQualifiedModelName),
25
- importStatementForModelFactory(fullyQualifiedControllerName, 'User'),
26
- ];
27
- // Add attached model imports if specified
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 originalStringKeyValues = [];
33
- const updatedStringKeyValues = [];
34
- const originalStringAttributeChecks = [];
35
- const updatedStringAttributeChecks = [];
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 [attributeName, attributeType] = attribute.split(':');
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
- originalStringKeyValues.push(`${attributeName}: '${originalName}',`);
44
- updatedStringKeyValues.push(`${attributeName}: '${updatedName}',`);
45
- originalStringAttributeChecks.push(`expect(${modelVariableName}.${attributeName}).toEqual('${originalName}')`);
46
- updatedStringAttributeChecks.push(`expect(${modelVariableName}.${attributeName}).toEqual('${updatedName}')`);
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
- // noop
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 { PsychicServer } from '@rvoh/psychic'
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 ${userVariableName}: ${owningModelClassName}${attachedModelVariableName ? `\n let ${attachedModelVariableName}: ${attachedModelClassName}` : ''}
104
+ let request: SpecRequestType
105
+ let ${userVariableName}: ${userModelClassName}${owningModel ? `\n let ${owningModelVariableName}: ${owningModelClassName}` : ''}
62
106
 
63
107
  beforeEach(async () => {
64
- await request.init(PsychicServer)
65
- ${userVariableName} = await createUser()${attachedModelVariableName ? `\n ${attachedModelVariableName} = await create${attachedModelClassName}({ ${userVariableName} })` : ''}
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
- const ${modelVariableName} = await create${modelClassName}({
77
- ${attachedModelVariableName || userVariableName}${originalStringKeyValues.length ? ',\n ' + originalStringKeyValues.join('\n ') : ''}
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
- const ${modelVariableName} = await create${modelClassName}({
108
- ${attachedModelVariableName || userVariableName}${originalStringKeyValues.length ? ',\n ' + originalStringKeyValues.join('\n ') : ''}
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,${originalStringKeyValues.length ? '\n ' + originalStringKeyValues.join('\n ') : ''}
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
- ${originalStringKeyValues.length ? originalStringKeyValues.join('\n ') : ''}
182
+ ${attributeCreationKeyValues.join('\n ')}
141
183
  }, 201)
142
- const ${modelVariableName} = await ${modelClassName}.findOrFailBy({ ${userVariableName}Id: ${userVariableName}.id })
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,${originalStringKeyValues.length ? '\n ' + originalStringKeyValues.join('\n ') : ''}
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
- const ${modelVariableName} = await create${modelClassName}({
167
- ${attachedModelVariableName || userVariableName}${originalStringKeyValues.length ? ',\n ' + originalStringKeyValues.join('\n ') : ''}
168
- })
210
+ ${simpleCreationCommand}
211
+
169
212
  await subject(${modelVariableName}, {
170
- ${updatedStringKeyValues.length ? updatedStringKeyValues.join('\n ') : ''}
213
+ ${attributeUpdateKeyValues.length ? attributeUpdateKeyValues.join('\n ') : ''}
171
214
  }, 204)
172
215
 
173
216
  await ${modelVariableName}.reload()
174
- ${updatedStringAttributeChecks.join('\n ')}
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
- ${updatedStringKeyValues.length ? updatedStringKeyValues.join('\n ') : ''}
228
+ ${attributeUpdateKeyValues.length ? attributeUpdateKeyValues.join('\n ') : ''}
182
229
  }, 404)
183
230
 
184
231
  await ${modelVariableName}.reload()
185
- ${originalStringAttributeChecks.join('\n ')}
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
- const ${modelVariableName} = await create${modelClassName}({ ${attachedModelVariableName || userVariableName} })
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
  }