@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.
Files changed (39) hide show
  1. package/dist/cjs/src/cli/index.js +5 -1
  2. package/dist/cjs/src/controller/index.js +1 -1
  3. package/dist/cjs/src/generate/controller.js +21 -11
  4. package/dist/cjs/src/generate/helpers/addResourceToRoutes.js +6 -0
  5. package/dist/cjs/src/generate/helpers/generateControllerContent.js +17 -13
  6. package/dist/cjs/src/generate/helpers/generateResourceControllerSpecContent.js +22 -19
  7. package/dist/cjs/src/generate/resource.js +3 -0
  8. package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +1 -0
  9. package/dist/cjs/src/openapi-renderer/body-segment.js +4 -1
  10. package/dist/cjs/src/openapi-renderer/endpoint.js +13 -29
  11. package/dist/cjs/src/openapi-renderer/helpers/safelyAttachCursorPaginationParamToRequestBodySegment.js +3 -15
  12. package/dist/cjs/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.js +14 -1
  13. package/dist/cjs/src/router/helpers.js +12 -28
  14. package/dist/cjs/src/router/index.js +2 -7
  15. package/dist/cjs/src/server/params.js +71 -89
  16. package/dist/esm/src/cli/index.js +5 -1
  17. package/dist/esm/src/controller/index.js +1 -1
  18. package/dist/esm/src/generate/controller.js +21 -11
  19. package/dist/esm/src/generate/helpers/addResourceToRoutes.js +6 -0
  20. package/dist/esm/src/generate/helpers/generateControllerContent.js +17 -13
  21. package/dist/esm/src/generate/helpers/generateResourceControllerSpecContent.js +22 -19
  22. package/dist/esm/src/generate/resource.js +3 -0
  23. package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +1 -0
  24. package/dist/esm/src/openapi-renderer/body-segment.js +4 -1
  25. package/dist/esm/src/openapi-renderer/endpoint.js +13 -29
  26. package/dist/esm/src/openapi-renderer/helpers/safelyAttachCursorPaginationParamToRequestBodySegment.js +3 -15
  27. package/dist/esm/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.js +14 -1
  28. package/dist/esm/src/router/helpers.js +12 -28
  29. package/dist/esm/src/router/index.js +2 -7
  30. package/dist/esm/src/server/params.js +71 -89
  31. package/dist/types/src/bin/index.d.ts +1 -0
  32. package/dist/types/src/cli/index.d.ts +1 -0
  33. package/dist/types/src/generate/helpers/generateControllerContent.d.ts +2 -1
  34. package/dist/types/src/generate/helpers/generateResourceControllerSpecContent.d.ts +1 -0
  35. package/dist/types/src/generate/resource.d.ts +1 -0
  36. package/dist/types/src/openapi-renderer/helpers/safelyAttachCursorPaginationParamToRequestBodySegment.d.ts +1 -1
  37. package/dist/types/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.d.ts +6 -0
  38. package/dist/types/src/router/helpers.d.ts +3 -0
  39. 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") (simply to save time making changes to the generated code). Defaults to User')
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)
@@ -796,7 +796,7 @@ export default class PsychicController {
796
796
  * ```
797
797
  */
798
798
  redirect(path) {
799
- this.ctx.redirect(path);
799
+ this.koaRedirect(302, path);
800
800
  }
801
801
  // begin: http status codes
802
802
  /**
@@ -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 controllerNameParts = forAdmin ? [allControllerNameParts.shift()] : [];
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
- `AuthedController`,
104
- `import AuthedController from '${dotFiles}/${addImportSuffix('AuthedController.js')}'`,
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
- const loadQueryBase = forAdmin
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 ${forAdmin ? `${modelClassName}.create(` : `this.${owningModelProperty}.createAssociation('${pluralizedModelAttributeName}', `}this.paramsFor(${modelClassName}))
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 simpleCreationCommand = `const ${modelVariableName} = await create${modelClassName}(${options.forAdmin ? '' : `{ ${owningModelVariableName} }`})`;
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, forAdmin } = options;
268
- const subjectFunctionName = `index${pluralize(modelConfig.modelClassName)}`;
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 || forAdmin
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, forAdmin, attributeData } = options;
305
- const subjectFunctionName = `show${modelConfig.modelClassName}`;
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 || forAdmin
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, forAdmin, singular, attributeData } = options;
346
- const subjectFunctionName = `create${modelConfig.modelClassName}`;
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 = forAdmin
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}${forAdmin ? '' : ` for this ${modelConfig.owningModelName}`}', async () => {${dateTimeSetup}
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, forAdmin, attributeData } = options;
397
- const subjectFunctionName = `update${modelConfig.modelClassName}`;
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 || forAdmin
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, forAdmin } = options;
492
- const subjectFunctionName = `destroy${modelConfig.modelClassName}`;
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 || forAdmin
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
- return {
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
- return {
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 undefined;
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
- bodySegment ||= {
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
- [paramName]: paginationPageParamOpenapiProperty(),
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 applyResourcesAction(path, action, routingMechanism, options) {
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(`${path}/:id`, controller, 'update');
77
- routingMechanism.patch(`${path}/:id`, controller, 'update');
77
+ routingMechanism.put(memberPath, controller, 'update');
78
+ routingMechanism.patch(memberPath, controller, 'update');
78
79
  break;
79
80
  case 'show':
80
- routingMechanism.get(`${path}/:id`, controller, 'show');
81
+ routingMechanism.get(memberPath, controller, 'show');
81
82
  break;
82
83
  case 'destroy':
83
- routingMechanism.delete(`${path}/:id`, controller, 'destroy');
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
- const controller = options?.controller ||
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 { applyResourceAction, applyResourcesAction, convertRouteParams, lookupControllerOrFail, routePath, } from '../router/helpers.js';
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
- if (plural) {
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, } = {}) {