@rvoh/psychic 1.10.4 → 1.11.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/cjs/src/bin/helpers/enumsFileStr.js +1 -0
  2. package/dist/cjs/src/cli/helpers/PsychicLogos.js +38 -0
  3. package/dist/cjs/src/cli/index.js +4 -4
  4. package/dist/cjs/src/controller/helpers/httpMethodColor.js +34 -0
  5. package/dist/cjs/src/controller/helpers/logIfDevelopment.js +28 -0
  6. package/dist/cjs/src/controller/helpers/statusCodeColor.js +22 -0
  7. package/dist/cjs/src/controller/index.js +32 -13
  8. package/dist/cjs/src/generate/helpers/generateResourceControllerSpecContent.js +71 -60
  9. package/dist/cjs/src/generate/resource.js +2 -1
  10. package/dist/cjs/src/openapi-renderer/SerializerOpenapiRenderer.js +28 -29
  11. package/dist/cjs/src/openapi-renderer/endpoint.js +8 -2
  12. package/dist/cjs/src/psychic-app/index.js +2 -2
  13. package/dist/cjs/src/router/index.js +10 -19
  14. package/dist/cjs/src/server/helpers/startPsychicServer.js +17 -4
  15. package/dist/cjs/src/server/index.js +20 -3
  16. package/dist/esm/src/bin/helpers/enumsFileStr.js +1 -0
  17. package/dist/esm/src/cli/helpers/PsychicLogos.js +32 -0
  18. package/dist/esm/src/cli/index.js +4 -4
  19. package/dist/esm/src/controller/helpers/httpMethodColor.js +30 -0
  20. package/dist/esm/src/controller/helpers/logIfDevelopment.js +22 -0
  21. package/dist/esm/src/controller/helpers/statusCodeColor.js +18 -0
  22. package/dist/esm/src/controller/index.js +32 -13
  23. package/dist/esm/src/generate/helpers/generateResourceControllerSpecContent.js +71 -60
  24. package/dist/esm/src/generate/resource.js +2 -1
  25. package/dist/esm/src/openapi-renderer/SerializerOpenapiRenderer.js +28 -29
  26. package/dist/esm/src/openapi-renderer/endpoint.js +8 -2
  27. package/dist/esm/src/psychic-app/index.js +2 -2
  28. package/dist/esm/src/router/index.js +10 -19
  29. package/dist/esm/src/server/helpers/startPsychicServer.js +17 -4
  30. package/dist/esm/src/server/index.js +21 -4
  31. package/dist/types/src/cli/helpers/PsychicLogos.d.ts +3 -0
  32. package/dist/types/src/controller/helpers/httpMethodColor.d.ts +4 -0
  33. package/dist/types/src/controller/helpers/logIfDevelopment.d.ts +7 -0
  34. package/dist/types/src/controller/helpers/statusCodeColor.d.ts +3 -0
  35. package/dist/types/src/controller/index.d.ts +5 -1
  36. package/dist/types/src/helpers/EnvInternal.d.ts +2 -2
  37. package/dist/types/src/openapi-renderer/endpoint.d.ts +39 -1
  38. package/dist/types/src/server/index.d.ts +1 -1
  39. package/dist/types/src/server/params.d.ts +2 -1
  40. package/package.json +8 -7
  41. package/CHANGELOG.md +0 -314
@@ -20,6 +20,7 @@ export const ${exportedTypeName} = [
20
20
  .map(val => `'${val}'`)
21
21
  .join(',\n ')}
22
22
  ] as const
23
+ export type ${exportedTypeName.replace(/Values^/, '')} = (typeof ${exportedTypeName})[number]
23
24
 
24
25
  `;
25
26
  });
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const colorize_js_1 = __importDefault(require("./colorize.js"));
7
+ class PsychicLogos {
8
+ static babyAster() {
9
+ const g = (str) => (0, colorize_js_1.default)(str, { color: 'green' });
10
+ return `
11
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
12
+ ░░░░░░█▀█░█▀▀░█░█░█▀▀░█░█░▀█▀░█▀▀░░░░░░░░░
13
+ ░░░░░░█▀▀░▀▀█░░█░░█░░░█▀█░░█░░█░░░░░░░░░░░
14
+ ░░░░░░▀░░░▀▀▀░░▀░░▀▀▀░▀░▀░▀▀▀░▀▀▀░░░░░░░░░
15
+ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
16
+ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⣿⣿⣿⣿⣿⣿⣿⣿⠟⢉⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
17
+ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⣀⡀⠈⠻⠿⡿⠿⠛⠉⠁⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
18
+ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠀⠀⠹⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
19
+ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
20
+ ⣿⣿⣿⣿⣿⣿⣿⣿⠟⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
21
+ ⣿⠿⠛⠋⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢴⣲⠀⠀⠀⠋⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
22
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
23
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⡀⠀⢀⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
24
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
25
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
26
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
27
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
28
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀
29
+ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿
30
+ ⠀⠀⠀⠀⠀⠀⣀⠀⠀⠀⠀⠀⠀⠀⢸▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓
31
+ ⠀⠀⠀⠀⢠▓⣿⠀⠀⠀⣿ ⠀ ⣸⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿
32
+ ⠀⠀⠀⢠▓⣿▓⣦⠀⠀⣿▓⠀⠀⠋▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓
33
+ ⠀⠀⠀⢸⣿▓⣿▓⡄⠀⠈⠙⣄⣀⣠⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿
34
+ ⠀⠀⠀⠺▓⣿▓⣿▓⣄⣀⣤⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓⣿▓\
35
+ `.replace(/▓/g, g('▓'));
36
+ }
37
+ }
38
+ exports.default = PsychicLogos;
@@ -72,7 +72,7 @@ class PsychicCLI {
72
72
  .argument('<modelName>', 'the name of the model to create, e.g. Post or Settings/CommunicationPreferences')
73
73
  .argument('[columnsWithTypes...]', columnsWithTypesDescription)
74
74
  .action(async (route, modelName, columnsWithTypes, options) => {
75
- await initializePsychicApp();
75
+ await initializePsychicApp({ bypassDreamIntegrityChecks: true });
76
76
  await index_js_1.default.generateResource(route, modelName, columnsWithTypes, options);
77
77
  process.exit();
78
78
  });
@@ -83,7 +83,7 @@ class PsychicCLI {
83
83
  .argument('<controllerName>', 'the name of the controller to create, including namespaces, e.g. Posts or Api/V1/Posts')
84
84
  .argument('[actions...]', 'the names of controller actions to create')
85
85
  .action(async (controllerName, actions) => {
86
- await initializePsychicApp();
86
+ await initializePsychicApp({ bypassDreamIntegrityChecks: true });
87
87
  await index_js_1.default.generateController(controllerName, actions);
88
88
  process.exit();
89
89
  });
@@ -140,7 +140,7 @@ class PsychicCLI {
140
140
  .description('sync introspects your database, updating your schema to reflect, and then syncs the new schema with the installed dream node module, allowing it provide your schema to the underlying kysely integration')
141
141
  .option('--schema-only')
142
142
  .action(async (options = {}) => {
143
- await initializePsychicApp();
143
+ await initializePsychicApp({ bypassDreamIntegrityChecks: !!options.schemaOnly });
144
144
  await index_js_1.default.sync(options);
145
145
  process.exit();
146
146
  });
@@ -149,7 +149,7 @@ class PsychicCLI {
149
149
  .description('watches your app for changes, and re-syncs any time they happen')
150
150
  .argument('[dir]', 'the folder you want to watch, defaults to ./src')
151
151
  .action(async (dir) => {
152
- await initializePsychicApp();
152
+ await initializePsychicApp({ bypassDreamIntegrityChecks: true });
153
153
  Watcher_js_1.default.watch(dir);
154
154
  });
155
155
  program
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = httpMethodColor;
4
+ exports.httpMethodBgColor = httpMethodBgColor;
5
+ function httpMethodColor(httpMethod) {
6
+ switch (httpMethod) {
7
+ case 'delete':
8
+ return 'red';
9
+ case 'options':
10
+ return 'gray';
11
+ case 'patch':
12
+ case 'put':
13
+ return 'magentaBright';
14
+ case 'post':
15
+ return 'greenBright';
16
+ default:
17
+ return 'blue';
18
+ }
19
+ }
20
+ function httpMethodBgColor(httpMethod) {
21
+ switch (httpMethod) {
22
+ case 'delete':
23
+ return 'bgRed';
24
+ case 'options':
25
+ return 'bgWhite';
26
+ case 'patch':
27
+ case 'put':
28
+ return 'bgMagentaBright';
29
+ case 'post':
30
+ return 'bgGreen';
31
+ default:
32
+ return 'bgBlue';
33
+ }
34
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = logIfDevelopment;
7
+ const dream_1 = require("@rvoh/dream");
8
+ const colorize_js_1 = __importDefault(require("../../cli/helpers/colorize.js"));
9
+ const EnvInternal_js_1 = __importDefault(require("../../helpers/EnvInternal.js"));
10
+ const httpMethodColor_js_1 = require("./httpMethodColor.js");
11
+ const statusCodeColor_js_1 = require("./statusCodeColor.js");
12
+ function logIfDevelopment({ req, res, startTime, fallbackStatusCode = 200, }) {
13
+ if (!EnvInternal_js_1.default.isDevelopment)
14
+ return;
15
+ const method = (0, colorize_js_1.default)(` ${req.method.toUpperCase()} `, {
16
+ color: 'black',
17
+ bgColor: (0, httpMethodColor_js_1.httpMethodBgColor)(req.method.toLowerCase()),
18
+ });
19
+ const computedStatus = res.statusCode || fallbackStatusCode;
20
+ const statusBgColor = (0, statusCodeColor_js_1.statusCodeBgColor)(computedStatus);
21
+ const status = (0, colorize_js_1.default)(` ${computedStatus} `, {
22
+ color: 'black',
23
+ bgColor: statusBgColor,
24
+ });
25
+ const url = (0, colorize_js_1.default)(req.url, { color: 'green' });
26
+ const benchmark = (0, colorize_js_1.default)(`${Date.now() - startTime}ms`, { color: 'gray' });
27
+ dream_1.DreamCLI.logger.log(`${method} ${url} ${status} ${benchmark}`, { logPrefix: '' });
28
+ }
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = statusCodeColor;
4
+ exports.statusCodeBgColor = statusCodeBgColor;
5
+ function statusCodeColor(statusCode) {
6
+ if (statusCode >= 200 && statusCode < 300)
7
+ return 'green';
8
+ if (statusCode >= 300 && statusCode < 400)
9
+ return 'yellow';
10
+ if (statusCode >= 400 && statusCode < 500)
11
+ return 'red';
12
+ return 'redBright';
13
+ }
14
+ function statusCodeBgColor(statusCode) {
15
+ if (statusCode >= 200 && statusCode < 300)
16
+ return 'bgGreen';
17
+ if (statusCode >= 300 && statusCode < 400)
18
+ return 'bgYellow';
19
+ if (statusCode >= 400 && statusCode < 500)
20
+ return 'bgRed';
21
+ return 'bgRedBright';
22
+ }
@@ -43,7 +43,9 @@ const index_js_1 = __importDefault(require("../psychic-app/index.js"));
43
43
  const params_js_1 = __importDefault(require("../server/params.js"));
44
44
  const index_js_2 = __importDefault(require("../session/index.js"));
45
45
  const isPaginatedResult_js_1 = __importDefault(require("./helpers/isPaginatedResult.js"));
46
+ const logIfDevelopment_js_1 = __importDefault(require("./helpers/logIfDevelopment.js"));
46
47
  const renderDreamOrViewModel_js_1 = __importDefault(require("./helpers/renderDreamOrViewModel.js"));
48
+ const EnvInternal_js_1 = __importDefault(require("../helpers/EnvInternal.js"));
47
49
  exports.PsychicParamsPrimitiveLiterals = [
48
50
  'bigint',
49
51
  'bigint[]',
@@ -177,7 +179,9 @@ class PsychicController {
177
179
  session;
178
180
  action;
179
181
  renderOpts;
182
+ startTime;
180
183
  constructor(req, res, { action, }) {
184
+ this.startTime = Date.now();
181
185
  this.req = req;
182
186
  this.res = res;
183
187
  this.session = new index_js_2.default(req, res);
@@ -428,7 +432,26 @@ class PsychicController {
428
432
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
429
433
  data) {
430
434
  this.validateOpenapiResponseBody(data);
431
- this.res.type('json').send((0, toJson_js_1.default)(data, index_js_1.default.getOrFail().sanitizeResponseJson));
435
+ this.expressSendJson(data);
436
+ }
437
+ expressSendJson(
438
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
439
+ data, statusCode = this.res.statusCode) {
440
+ this.res.type('json').status(statusCode).send((0, toJson_js_1.default)(data, index_js_1.default.getOrFail().sanitizeResponseJson));
441
+ this.logIfDevelopment();
442
+ }
443
+ expressSendStatus(statusCode) {
444
+ this.res.sendStatus(statusCode);
445
+ this.logIfDevelopment();
446
+ }
447
+ expressRedirect(statusCode, newLocation) {
448
+ this.res.redirect(statusCode, newLocation);
449
+ this.logIfDevelopment();
450
+ }
451
+ logIfDevelopment() {
452
+ if (!EnvInternal_js_1.default.isDevelopment)
453
+ return;
454
+ (0, logIfDevelopment_js_1.default)({ req: this.req, res: this.res, startTime: this.startTime });
432
455
  }
433
456
  defaultSerializerPassthrough = {};
434
457
  serializerPassthrough(passthrough) {
@@ -475,15 +498,15 @@ class PsychicController {
475
498
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
476
499
  nonAuthoritativeInformation(message = undefined) {
477
500
  if (message) {
478
- this.res.status(203).json(message);
501
+ this.expressSendJson(message, 203);
479
502
  }
480
503
  else {
481
- this.res.sendStatus(203);
504
+ this.expressSendStatus(203);
482
505
  }
483
506
  }
484
507
  // 204
485
508
  noContent() {
486
- this.res.sendStatus(204);
509
+ this.expressSendStatus(204);
487
510
  }
488
511
  // 205
489
512
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -499,27 +522,23 @@ class PsychicController {
499
522
  }
500
523
  // 301
501
524
  movedPermanently(newLocation) {
502
- this.res.redirect(301, newLocation);
525
+ this.expressRedirect(301, newLocation);
503
526
  }
504
527
  // 302
505
528
  found(newLocation) {
506
- this.res.redirect(302, newLocation);
529
+ this.expressRedirect(302, newLocation);
507
530
  }
508
531
  // 303
509
532
  seeOther(newLocation) {
510
- this.res.redirect(303, newLocation);
511
- }
512
- // 304
513
- notModified(newLocation) {
514
- this.res.redirect(304, newLocation);
533
+ this.expressRedirect(303, newLocation);
515
534
  }
516
535
  // 307
517
536
  temporaryRedirect(newLocation) {
518
- this.res.redirect(307, newLocation);
537
+ this.expressRedirect(307, newLocation);
519
538
  }
520
539
  // 308
521
540
  permanentRedirect(newLocation) {
522
- this.res.redirect(308, newLocation);
541
+ this.expressRedirect(308, newLocation);
523
542
  }
524
543
  // 400
525
544
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -5,10 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.default = generateResourceControllerSpecContent;
7
7
  const dream_1 = require("@rvoh/dream");
8
+ const addImportSuffix_js_1 = __importDefault(require("../../helpers/path/addImportSuffix.js"));
8
9
  const relativePsychicPath_js_1 = __importDefault(require("../../helpers/path/relativePsychicPath.js"));
9
10
  const updirsFromPath_js_1 = __importDefault(require("../../helpers/path/updirsFromPath.js"));
10
11
  const index_js_1 = require("../../index.js");
11
- const addImportSuffix_js_1 = __importDefault(require("../../helpers/path/addImportSuffix.js"));
12
12
  function generateResourceControllerSpecContent({ fullyQualifiedControllerName, route, fullyQualifiedModelName, columnsWithTypes, owningModel, forAdmin, singular, actions, }) {
13
13
  fullyQualifiedModelName = (0, dream_1.standardizeFullyQualifiedModelName)(fullyQualifiedModelName);
14
14
  const modelClassName = (0, dream_1.globalClassNameFromFullyQualifiedModelName)(fullyQualifiedModelName);
@@ -21,7 +21,7 @@ function generateResourceControllerSpecContent({ fullyQualifiedControllerName, r
21
21
  ? (0, dream_1.globalClassNameFromFullyQualifiedModelName)(owningModel)
22
22
  : userModelName;
23
23
  const owningModelVariableName = owningModelName ? (0, dream_1.camelize)(owningModelName) : userVariableName;
24
- const dreamImports = ['UpdateableProperties'];
24
+ const dreamImports = [];
25
25
  const importStatements = (0, dream_1.compact)([
26
26
  importStatementForModel(fullyQualifiedControllerName, fullyQualifiedModelName),
27
27
  importStatementForModel(fullyQualifiedControllerName, userModelName),
@@ -34,7 +34,6 @@ function generateResourceControllerSpecContent({ fullyQualifiedControllerName, r
34
34
  const attributeCreationKeyValues = [];
35
35
  const attributeUpdateKeyValues = [];
36
36
  const comparableOriginalAttributeKeyValues = [];
37
- const comparableUpdatedAttributeKeyValues = [];
38
37
  const expectEqualOriginalValue = [];
39
38
  const expectEqualUpdatedValue = [];
40
39
  const expectEqualOriginalNamedVariable = [];
@@ -43,89 +42,88 @@ function generateResourceControllerSpecContent({ fullyQualifiedControllerName, r
43
42
  let dateAttributeIncluded = false;
44
43
  let datetimeAttributeIncluded = false;
45
44
  for (const attribute of columnsWithTypes) {
46
- const [rawAttributeName, attributeType, , enumValues] = attribute.split(':');
45
+ const [rawAttributeName, rawAttributeType, , enumValues] = attribute.split(':');
47
46
  if (/(_type|_id)$/.test(rawAttributeName ?? ''))
48
47
  continue;
49
48
  const attributeName = (0, dream_1.camelize)(rawAttributeName ?? '');
50
49
  const dotNotationVariable = `${modelVariableName}.${attributeName}`;
50
+ if (!rawAttributeType)
51
+ continue;
52
+ const arrayBracketRegexp = /\[\]$/;
53
+ const isArray = arrayBracketRegexp.test(rawAttributeType);
54
+ const attributeType = rawAttributeType.replace(arrayBracketRegexp, '');
51
55
  if (attributeName === 'deletedAt')
52
56
  continue;
53
57
  switch (attributeType) {
54
58
  case 'enum': {
55
- const originalEnumValue = (enumValues ?? '').split(',').at(0);
56
- const updatedEnumValue = (enumValues ?? '').split(',').at(-1);
57
- attributeCreationKeyValues.push(`${attributeName}: '${originalEnumValue}',`);
58
- attributeUpdateKeyValues.push(`${attributeName}: '${updatedEnumValue}',`);
59
+ const rawOriginalEnumValue = (enumValues ?? '').split(',').at(0);
60
+ const rawUpdatedEnumValue = (enumValues ?? '').split(',').at(-1);
61
+ const originalEnumValue = isArray ? [rawOriginalEnumValue] : rawOriginalEnumValue;
62
+ const updatedEnumValue = isArray ? [rawUpdatedEnumValue] : rawUpdatedEnumValue;
63
+ attributeCreationKeyValues.push(`${attributeName}: ${jsonify(originalEnumValue)},`);
64
+ attributeUpdateKeyValues.push(`${attributeName}: ${jsonify(updatedEnumValue)},`);
59
65
  comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
60
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: '${updatedEnumValue}',`);
61
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual('${originalEnumValue}')`);
62
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual('${updatedEnumValue}')`);
66
+ expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(originalEnumValue)})`);
67
+ expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(updatedEnumValue)})`);
63
68
  break;
64
69
  }
65
70
  case 'string':
66
71
  case 'text':
67
72
  case 'citext': {
68
- const originalStringValue = `The ${fullyQualifiedModelName} ${attributeName}`;
69
- const updatedStringValue = `Updated ${fullyQualifiedModelName} ${attributeName}`;
70
- attributeCreationKeyValues.push(`${attributeName}: '${originalStringValue}',`);
71
- attributeUpdateKeyValues.push(`${attributeName}: '${updatedStringValue}',`);
73
+ const rawOriginalStringValue = `The ${fullyQualifiedModelName} ${attributeName}`;
74
+ const rawUpdatedStringValue = `Updated ${fullyQualifiedModelName} ${attributeName}`;
75
+ const originalStringValue = isArray ? [rawOriginalStringValue] : rawOriginalStringValue;
76
+ const updatedStringValue = isArray ? [rawUpdatedStringValue] : rawUpdatedStringValue;
77
+ attributeCreationKeyValues.push(`${attributeName}: ${jsonify(originalStringValue)},`);
78
+ attributeUpdateKeyValues.push(`${attributeName}: ${jsonify(updatedStringValue)},`);
72
79
  comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
73
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: '${updatedStringValue}',`);
74
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual('${originalStringValue}')`);
75
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual('${updatedStringValue}')`);
80
+ expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(originalStringValue)})`);
81
+ expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(updatedStringValue)})`);
76
82
  break;
77
83
  }
78
84
  case 'integer':
79
- attributeCreationKeyValues.push(`${attributeName}: 1,`);
80
- attributeUpdateKeyValues.push(`${attributeName}: 2,`);
81
- comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
82
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: 2,`);
83
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(1)`);
84
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(2)`);
85
- break;
86
- case 'bigint':
87
- attributeCreationKeyValues.push(`${attributeName}: '11111111111111111',`);
88
- attributeUpdateKeyValues.push(`${attributeName}: '22222222222222222',`);
89
- comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
90
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: '22222222222222222',`);
91
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual('11111111111111111')`);
92
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual('22222222222222222')`);
93
- break;
94
85
  case 'decimal':
95
- attributeCreationKeyValues.push(`${attributeName}: 1.1,`);
96
- attributeUpdateKeyValues.push(`${attributeName}: 2.2,`);
86
+ case 'bigint': {
87
+ const rawOriginalValue = attributeType === 'integer' ? 1 : attributeType === 'decimal' ? 1.1 : '11111111111111111';
88
+ const rawUpdatedValue = attributeType === 'integer' ? 2 : attributeType === 'decimal' ? 2.2 : '22222222222222222';
89
+ const originalValue = isArray ? [rawOriginalValue] : rawOriginalValue;
90
+ const updatedValue = isArray ? [rawUpdatedValue] : rawUpdatedValue;
91
+ attributeCreationKeyValues.push(`${attributeName}: ${jsonify(originalValue)},`);
92
+ attributeUpdateKeyValues.push(`${attributeName}: ${jsonify(updatedValue)},`);
97
93
  comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
98
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: 2.2,`);
99
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(1.1)`);
100
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(2.2)`);
94
+ expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(originalValue)})`);
95
+ expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(updatedValue)})`);
101
96
  break;
102
- case 'date':
97
+ }
98
+ case 'date': {
103
99
  dreamImports.push('CalendarDate');
104
100
  dateAttributeIncluded = true;
105
- attributeCreationKeyValues.push(`${attributeName}: today,`);
106
- attributeUpdateKeyValues.push(`${attributeName}: yesterday,`);
107
- comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable}.toISO(),`);
108
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: yesterday.toISO(),`);
109
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqualCalendarDate(today)`);
110
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqualCalendarDate(yesterday)`);
101
+ attributeCreationKeyValues.push(`${attributeName}: ${isArray ? '[today.toISO()]' : 'today.toISO()'},`);
102
+ attributeUpdateKeyValues.push(`${attributeName}: ${isArray ? '[yesterday.toISO()]' : 'yesterday.toISO()'},`);
103
+ comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable}${isArray ? '.map(date => date.toISO())' : '.toISO()'},`);
104
+ expectEqualOriginalValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualCalendarDate(today)`);
105
+ expectEqualUpdatedValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualCalendarDate(yesterday)`);
111
106
  break;
112
- case 'datetime':
107
+ }
108
+ case 'datetime': {
113
109
  dreamImports.push('DateTime');
114
110
  datetimeAttributeIncluded = true;
115
- attributeCreationKeyValues.push(`${attributeName}: now,`);
116
- attributeUpdateKeyValues.push(`${attributeName}: lastHour,`);
117
- comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable}.toISO(),`);
118
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: lastHour.toISO(),`);
119
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqualDateTime(now)`);
120
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqualDateTime(lastHour)`);
111
+ attributeCreationKeyValues.push(`${attributeName}: ${isArray ? '[now.toISO()]' : 'now.toISO()'},`);
112
+ attributeUpdateKeyValues.push(`${attributeName}: ${isArray ? '[lastHour.toISO()]' : 'lastHour.toISO()'},`);
113
+ comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable}${isArray ? '.map(datetime => datetime.toISO())' : '.toISO()'},`);
114
+ expectEqualOriginalValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualDateTime(now)`);
115
+ expectEqualUpdatedValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualDateTime(lastHour)`);
121
116
  break;
117
+ }
122
118
  default:
123
119
  continue;
124
120
  }
125
121
  keyWithDotValue.push(`${attributeName}: ${dotNotationVariable},`);
126
- const originalAttributeVariableName = 'original' + (0, dream_1.capitalize)(attributeName);
127
- originalValueVariableAssignments.push(`const ${originalAttributeVariableName} = ${dotNotationVariable}`);
128
- expectEqualOriginalNamedVariable.push(`expect(${dotNotationVariable}).toEqual(${originalAttributeVariableName})`);
122
+ if (!((attributeType === 'date' || attributeType === 'datetime') && isArray)) {
123
+ const originalAttributeVariableName = 'original' + (0, dream_1.capitalize)(attributeName);
124
+ originalValueVariableAssignments.push(`const ${originalAttributeVariableName} = ${dotNotationVariable}`);
125
+ expectEqualOriginalNamedVariable.push(`expect(${dotNotationVariable}).toEqual(${originalAttributeVariableName})`);
126
+ }
129
127
  }
130
128
  const simpleCreationCommand = `const ${modelVariableName} = await create${modelClassName}(${forAdmin ? '' : `{ ${owningModelVariableName} }`})`;
131
129
  const omitIndex = singular || !actions.includes('index');
@@ -134,7 +132,10 @@ function generateResourceControllerSpecContent({ fullyQualifiedControllerName, r
134
132
  const omitUpdate = !actions.includes('update');
135
133
  const omitDestroy = !actions.includes('destroy');
136
134
  return `\
137
- import { ${(0, dream_1.uniq)(dreamImports).join(', ')} } from '@rvoh/dream'${(0, dream_1.uniq)(importStatements).join('')}
135
+ import { DreamRequestAttributes } from '@rvoh/psychic-spec-helpers'${dreamImports.length
136
+ ? `
137
+ import { ${(0, dream_1.uniq)(dreamImports).join(', ')} } from '@rvoh/dream'`
138
+ : ''}${(0, dream_1.uniq)(importStatements).join('')}
138
139
  import { session, SpecRequestType } from '${specUnitUpdirs}helpers/${(0, addImportSuffix_js_1.default)('authentication.js')}'
139
140
 
140
141
  describe('${fullyQualifiedControllerName}', () => {
@@ -219,7 +220,7 @@ describe('${fullyQualifiedControllerName}', () => {
219
220
 
220
221
  describe('POST create', () => {
221
222
  const subject = async <StatusCode extends 201 | 400>(
222
- data: UpdateableProperties<${modelClassName}>,
223
+ data: DreamRequestAttributes<${modelClassName}>,
223
224
  expectedStatus: StatusCode
224
225
  ) => {
225
226
  return request.post('/${route}', expectedStatus, { data })
@@ -253,7 +254,7 @@ describe('${fullyQualifiedControllerName}', () => {
253
254
  describe('PATCH update', () => {${singular
254
255
  ? `
255
256
  const subject = async <StatusCode extends 204 | 400 | 404>(
256
- data: UpdateableProperties<${modelClassName}>,
257
+ data: DreamRequestAttributes<${modelClassName}>,
257
258
  expectedStatus: StatusCode
258
259
  ) => {
259
260
  return request.patch('/${route}', expectedStatus, {
@@ -263,7 +264,7 @@ describe('${fullyQualifiedControllerName}', () => {
263
264
  : `
264
265
  const subject = async <StatusCode extends 204 | 400 | 404>(
265
266
  ${modelVariableName}: ${modelClassName},
266
- data: UpdateableProperties<${modelClassName}>,
267
+ data: DreamRequestAttributes<${modelClassName}>,
267
268
  expectedStatus: StatusCode
268
269
  ) => {
269
270
  return request.patch('/${route}/{id}', expectedStatus, {
@@ -292,7 +293,13 @@ describe('${fullyQualifiedControllerName}', () => {
292
293
  : `
293
294
 
294
295
  context('a ${fullyQualifiedModelName} created by another ${owningModelName}', () => {
295
- it('is not updated', async () => {
296
+ it('is not updated', async () => {${dateAttributeIncluded
297
+ ? `
298
+ const yesterday = CalendarDate.yesterday()`
299
+ : ''}${datetimeAttributeIncluded
300
+ ? `
301
+ const lastHour = DateTime.now().minus({ hour: 1 })`
302
+ : ''}${dateAttributeIncluded || datetimeAttributeIncluded ? '\n' : ''}
296
303
  const ${modelVariableName} = await create${modelClassName}()
297
304
  ${originalValueVariableAssignments.length ? originalValueVariableAssignments.join('\n ') : ''}
298
305
 
@@ -349,3 +356,7 @@ function importStatementForModel(originModelName, destinationModelName = originM
349
356
  function importStatementForModelFactory(originModelName, destinationModelName = originModelName) {
350
357
  return `\nimport create${(0, dream_1.globalClassNameFromFullyQualifiedModelName)(destinationModelName)} from '${(0, relativePsychicPath_js_1.default)('controllerSpecs', 'factories', originModelName, destinationModelName)}'`;
351
358
  }
359
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
360
+ function jsonify(val) {
361
+ return JSON.stringify(val).replace(/"/g, "'");
362
+ }
@@ -17,6 +17,8 @@ async function generateResource({ route, fullyQualifiedModelName, options, colum
17
17
  // which will exhibit bad behavior when provided with
18
18
  // a prefixing slash.
19
19
  route = route.replace(/^\/+/, '');
20
+ if (!options.singular)
21
+ route = (0, pluralize_esm_1.default)(route);
20
22
  const fullyQualifiedControllerName = (0, dream_1.standardizeFullyQualifiedModelName)(route);
21
23
  const resourcefulActions = options.singular ? [...exports.SINGULAR_RESOURCE_ACTIONS] : [...exports.RESOURCE_ACTIONS];
22
24
  const onlyActions = options.only?.split(',');
@@ -31,7 +33,6 @@ async function generateResource({ route, fullyQualifiedModelName, options, colum
31
33
  connectionName: options.connectionName,
32
34
  },
33
35
  });
34
- route = (0, pluralize_esm_1.default)(route);
35
36
  await (0, controller_js_1.default)({
36
37
  fullyQualifiedControllerName,
37
38
  fullyQualifiedModelName,
@@ -72,11 +72,7 @@ class SerializerOpenapiRenderer {
72
72
  const requiredProperties = (0, dream_1.compact)(this.serializerBuilder['attributes'].map(attribute => {
73
73
  const attributeType = attribute.type;
74
74
  switch (attributeType) {
75
- case 'attribute': {
76
- if (attribute.options?.required === false)
77
- return null;
78
- return attribute.options?.as ?? attribute.name;
79
- }
75
+ case 'attribute':
80
76
  case 'delegatedAttribute': {
81
77
  if (attribute.options?.required === false)
82
78
  return null;
@@ -119,23 +115,6 @@ class SerializerOpenapiRenderer {
119
115
  let newlyReferencedSerializers = [];
120
116
  accumulator = (() => {
121
117
  switch (attributeType) {
122
- ////////////////
123
- // attributes //
124
- ////////////////
125
- case 'attribute': {
126
- const outputAttributeName = this.setCase(attribute.options?.as ?? attribute.name);
127
- const openapi = attribute.options.openapi;
128
- newlyReferencedSerializers = (0, allSerializersFromHandWrittenOpenapi_js_1.default)(openapi);
129
- accumulator[outputAttributeName] = DataTypeForOpenapi?.isDream
130
- ? (0, dreamAttributeOpenapiShape_js_1.dreamColumnOpenapiShape)(DataTypeForOpenapi, attribute.name, openapi, {
131
- suppressResponseEnums: this.suppressResponseEnums,
132
- })
133
- : (0, allSerializersToRefsInOpenapi_js_1.default)((0, openapiShorthandToOpenapi_js_1.default)(openapi));
134
- return accumulator;
135
- }
136
- /////////////////////
137
- // end: attributes //
138
- /////////////////////
139
118
  ///////////////////////
140
119
  // custom attributes //
141
120
  ///////////////////////
@@ -154,19 +133,39 @@ class SerializerOpenapiRenderer {
154
133
  ////////////////////////////
155
134
  // end: custom attributes //
156
135
  ////////////////////////////
157
- //////////////////////////
158
- // delegated attributes //
159
- //////////////////////////
136
+ //////////////////////////////////////////
137
+ // attributes and delegated attributes //
138
+ //////////////////////////////////////////
139
+ case 'attribute':
160
140
  case 'delegatedAttribute': {
161
141
  const outputAttributeName = this.setCase(attribute.options?.as ?? attribute.name);
162
142
  const openapi = attribute.options.openapi;
163
143
  newlyReferencedSerializers = (0, allSerializersFromHandWrittenOpenapi_js_1.default)(openapi);
164
- accumulator[outputAttributeName] = (0, allSerializersToRefsInOpenapi_js_1.default)((0, openapiShorthandToOpenapi_js_1.default)(openapi));
144
+ let target;
145
+ if (attributeType === 'delegatedAttribute' && DataTypeForOpenapi?.isDream) {
146
+ const source = DataTypeForOpenapi;
147
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
148
+ const associatedModelOrModels = source['getAssociationMetadata'](attribute.targetName)?.modelCB();
149
+ target = Array.isArray(associatedModelOrModels)
150
+ ? associatedModelOrModels[0]
151
+ : associatedModelOrModels;
152
+ }
153
+ else if (attributeType === 'delegatedAttribute') {
154
+ target = undefined;
155
+ }
156
+ else {
157
+ target = DataTypeForOpenapi;
158
+ }
159
+ accumulator[outputAttributeName] = (0, allSerializersToRefsInOpenapi_js_1.default)(target?.isDream
160
+ ? (0, dreamAttributeOpenapiShape_js_1.dreamColumnOpenapiShape)(target, attribute.name, openapi, {
161
+ suppressResponseEnums: this.suppressResponseEnums,
162
+ })
163
+ : (0, openapiShorthandToOpenapi_js_1.default)(openapi));
165
164
  return accumulator;
166
165
  }
167
- ///////////////////////////////
168
- // end: delegated attributes //
169
- ///////////////////////////////
166
+ /////////////////////////////////////////////
167
+ // end:attributes and delegated attributes //
168
+ /////////////////////////////////////////////
170
169
  //////////////////
171
170
  // rendersOnes //
172
171
  //////////////////
@@ -474,6 +474,8 @@ class OpenapiEndpointRenderer {
474
474
  return true;
475
475
  if (body.including)
476
476
  return true;
477
+ if (body.combining)
478
+ return true;
477
479
  if (body.for)
478
480
  return true;
479
481
  if (body.required && body.type !== 'object')
@@ -494,12 +496,16 @@ class OpenapiEndpointRenderer {
494
496
  const dreamClass = forDreamClass || this.getSingleDreamModelClass();
495
497
  if (!dreamClass)
496
498
  return this.defaultRequestBody();
497
- const { only, including } = (this.requestBody || {});
499
+ const { only, including, combining } = (this.requestBody || {});
498
500
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
499
501
  const paramSafeColumns = (0, paramNamesForDreamClass_js_1.default)(dreamClass, { only, including });
500
502
  const paramsShape = {
501
503
  type: 'object',
502
- properties: {},
504
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
505
+ properties: {
506
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
507
+ ...(combining || {}),
508
+ },
503
509
  };
504
510
  const required = this.requestBody?.required;
505
511
  if (required) {