@rvoh/psychic 3.0.0-alpha.8 → 3.0.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 (42) hide show
  1. package/dist/cjs/src/cli/index.js +4 -22
  2. package/dist/cjs/src/controller/index.js +15 -2
  3. package/dist/cjs/src/generate/controller.js +3 -3
  4. package/dist/cjs/src/generate/helpers/generateResourceControllerSpecContent.js +6 -5
  5. package/dist/cjs/src/openapi-renderer/body-segment.js +0 -1
  6. package/dist/cjs/src/openapi-renderer/endpoint.js +13 -29
  7. package/dist/cjs/src/openapi-renderer/helpers/safelyAttachCursorPaginationParamToRequestBodySegment.js +3 -15
  8. package/dist/cjs/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.js +14 -1
  9. package/dist/cjs/src/router/helpers.js +12 -28
  10. package/dist/cjs/src/router/index.js +2 -7
  11. package/dist/cjs/src/server/params.js +71 -89
  12. package/dist/esm/src/cli/index.js +4 -22
  13. package/dist/esm/src/controller/index.js +15 -2
  14. package/dist/esm/src/generate/controller.js +3 -3
  15. package/dist/esm/src/generate/helpers/generateResourceControllerSpecContent.js +6 -5
  16. package/dist/esm/src/openapi-renderer/body-segment.js +0 -1
  17. package/dist/esm/src/openapi-renderer/endpoint.js +13 -29
  18. package/dist/esm/src/openapi-renderer/helpers/safelyAttachCursorPaginationParamToRequestBodySegment.js +3 -15
  19. package/dist/esm/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.js +14 -1
  20. package/dist/esm/src/router/helpers.js +12 -28
  21. package/dist/esm/src/router/index.js +2 -7
  22. package/dist/esm/src/server/params.js +71 -89
  23. package/dist/types/src/controller/index.d.ts +12 -1
  24. package/dist/types/src/openapi-renderer/helpers/safelyAttachCursorPaginationParamToRequestBodySegment.d.ts +1 -1
  25. package/dist/types/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.d.ts +6 -0
  26. package/dist/types/src/router/helpers.d.ts +3 -0
  27. package/package.json +30 -20
  28. package/dist/cjs/src/generate/helpers/zustandBindings/printFinalStepsMessage.js +0 -36
  29. package/dist/cjs/src/generate/helpers/zustandBindings/promptForOptions.js +0 -55
  30. package/dist/cjs/src/generate/helpers/zustandBindings/writeApiClientFile.js +0 -50
  31. package/dist/cjs/src/generate/helpers/zustandBindings/writeInitializer.js +0 -46
  32. package/dist/cjs/src/generate/openapi/zustandBindings.js +0 -27
  33. package/dist/esm/src/generate/helpers/zustandBindings/printFinalStepsMessage.js +0 -36
  34. package/dist/esm/src/generate/helpers/zustandBindings/promptForOptions.js +0 -55
  35. package/dist/esm/src/generate/helpers/zustandBindings/writeApiClientFile.js +0 -50
  36. package/dist/esm/src/generate/helpers/zustandBindings/writeInitializer.js +0 -46
  37. package/dist/esm/src/generate/openapi/zustandBindings.js +0 -27
  38. package/dist/types/src/generate/helpers/zustandBindings/printFinalStepsMessage.d.ts +0 -2
  39. package/dist/types/src/generate/helpers/zustandBindings/promptForOptions.d.ts +0 -2
  40. package/dist/types/src/generate/helpers/zustandBindings/writeApiClientFile.d.ts +0 -5
  41. package/dist/types/src/generate/helpers/zustandBindings/writeInitializer.d.ts +0 -5
  42. package/dist/types/src/generate/openapi/zustandBindings.d.ts +0 -21
@@ -4,7 +4,6 @@ import generateController from '../generate/controller.js';
4
4
  import generateSyncEnumsInitializer from '../generate/initializer/syncEnums.js';
5
5
  import generateSyncOpenapiTypescriptInitializer from '../generate/initializer/syncOpenapiTypescript.js';
6
6
  import generateOpenapiReduxBindings from '../generate/openapi/reduxBindings.js';
7
- import generateOpenapiZustandBindings from '../generate/openapi/zustandBindings.js';
8
7
  import generateResource from '../generate/resource.js';
9
8
  import Watcher from '../watcher/Watcher.js';
10
9
  const INDENT = ' ';
@@ -23,6 +22,9 @@ ${INDENT} - citext:
23
22
  ${INDENT} - citext[]:
24
23
  ${INDENT} case insensitive text (indexes and queries are automatically case insensitive)
25
24
  ${INDENT}
25
+ ${INDENT} - encrypted:
26
+ ${INDENT} encrypted text (used in conjunction with the @deco.Encrypted decorator)
27
+ ${INDENT}
26
28
  ${INDENT} - string:
27
29
  ${INDENT} - string[]:
28
30
  ${INDENT} varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional
@@ -84,7 +86,7 @@ export default class PsychicCLI {
84
86
  - update
85
87
  - delete`)
86
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')
87
- .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.')
88
90
  .option('--connection-name <connectionName>', 'the name of the db connection you would like to use for your model. Defaults to "default"', 'default')
89
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)')
90
92
  .argument('<path>', 'URL path from root domain. Specify nesting resource with `{}`, e.g.: `tickets/{}/comments`')
@@ -144,26 +146,6 @@ export default class PsychicCLI {
144
146
  });
145
147
  process.exit();
146
148
  });
147
- program
148
- .command('setup:sync:openapi-zustand')
149
- .description('Generates openapi zustand bindings using openapi-fetch to connect one of your openapi files to one of your clients.')
150
- .option('--schema-file <schemaFile>', 'the path from your api root to the openapi file you wish to use to generate your schema, i.e. ./src/openapi/openapi.json')
151
- .option('--export-name <exportName>', 'the camelCased name to use for your exported api client, i.e. myBackendApi')
152
- .option('--output-dir <outputDir>', 'the path to the directory where the generated api client and types files will be written, i.e. ../client/src/api')
153
- .option('--types-file <typesFile>', 'the path to the file that will contain your generated openapi TypeScript type definitions, i.e. ../client/src/api/myBackendApi.types.d.ts')
154
- .action(async ({ schemaFile, exportName, outputDir, typesFile, }) => {
155
- await initializePsychicApp({
156
- bypassDreamIntegrityChecks: true,
157
- bypassDbConnectionsDuringInit: true,
158
- });
159
- await generateOpenapiZustandBindings({
160
- exportName,
161
- schemaFile,
162
- outputDir,
163
- typesFile,
164
- });
165
- process.exit();
166
- });
167
149
  program
168
150
  .command('setup:sync:openapi-typescript')
169
151
  .description('Generates an initializer in your app for converting one of your openapi files to typescript.')
@@ -197,7 +197,10 @@ export default class PsychicController {
197
197
  };
198
198
  }
199
199
  /**
200
- * Gets the HTTP request headers from the Express request object.
200
+ * Gets the HTTP request headers from the Koa ctx object.
201
+ * We recommend using the #header method instead when looking
202
+ * to retreive the value for a specific header, since that method
203
+ * will be safe with regards to case sensitivity.
201
204
  *
202
205
  * @returns The request headers as a key-value object where header names are lowercase strings
203
206
  * and values can be strings, string arrays, or undefined.
@@ -216,6 +219,16 @@ export default class PsychicController {
216
219
  get headers() {
217
220
  return this.ctx.request.headers;
218
221
  }
222
+ /**
223
+ * returns the value for the requested header. This method is case insensitive.
224
+ * If the header requested is not found, a blank string is returned.
225
+ *
226
+ * @param headerName - the name of the header
227
+ * @returns string
228
+ */
229
+ header(headerName) {
230
+ return this.ctx.request.get(headerName);
231
+ }
219
232
  /**
220
233
  * Gets the combined parameters from the HTTP request. This includes URL parameters,
221
234
  * request body, and query string parameters merged together. The merge order is:
@@ -796,7 +809,7 @@ export default class PsychicController {
796
809
  * ```
797
810
  */
798
811
  redirect(path) {
799
- this.ctx.redirect(path);
812
+ this.koaRedirect(302, path);
800
813
  }
801
814
  // begin: http status codes
802
815
  /**
@@ -20,9 +20,9 @@ export default async function generateController({ fullyQualifiedControllerName,
20
20
  const allControllerNameParts = fullyQualifiedControllerName.split('/');
21
21
  const forAdmin = allControllerNameParts[0] === 'Admin';
22
22
  const forInternal = allControllerNameParts[0] === 'Internal';
23
- const controllerNameParts = (forAdmin || forInternal) ? [allControllerNameParts.shift()] : [];
23
+ const controllerNameParts = forAdmin || forInternal ? [allControllerNameParts.shift()] : [];
24
24
  for (let index = 0; index < allControllerNameParts.length; index++) {
25
- if (controllerNameParts.length > ((forAdmin || forInternal) ? 1 : 0)) {
25
+ if (controllerNameParts.length > (forAdmin || forInternal ? 1 : 0)) {
26
26
  // Write the ancestor controller
27
27
  const [baseAncestorName, baseAncestorImportStatement] = baseAncestorNameAndImport(controllerNameParts, forAdmin, forInternal, { forBaseController: true });
28
28
  if (baseAncestorName === undefined)
@@ -97,7 +97,7 @@ export default async function generateController({ fullyQualifiedControllerName,
97
97
  function baseAncestorNameAndImport(controllerNameParts, forAdmin, forInternal, { forBaseController }) {
98
98
  const maybeAncestorNameForBase = `${controllerNameParts.slice(0, controllerNameParts.length - 1).join('')}BaseController`;
99
99
  const dotFiles = forBaseController ? '..' : '.';
100
- return controllerNameParts.length === ((forAdmin || forInternal) ? 2 : 1)
100
+ return controllerNameParts.length === (forAdmin || forInternal ? 2 : 1)
101
101
  ? forAdmin
102
102
  ? [
103
103
  `AdminAuthedController`,
@@ -124,6 +124,7 @@ function processAttributeByType({ attributeType, attributeName, isArray, enumVal
124
124
  case 'string':
125
125
  case 'text':
126
126
  case 'citext':
127
+ case 'encrypted':
127
128
  processStringAttribute({
128
129
  attributeName,
129
130
  isArray,
@@ -267,7 +268,7 @@ function generateIndexActionSpec(options) {
267
268
  if (options.actionConfig.omitIndex)
268
269
  return '';
269
270
  const { path, pathParams, modelConfig, fullyQualifiedModelName, singular } = options;
270
- const subjectFunctionName = `index${pluralize(modelConfig.modelClassName)}`;
271
+ const subjectFunctionName = 'index';
271
272
  return `
272
273
 
273
274
  describe('GET index', () => {
@@ -304,7 +305,7 @@ function generateShowActionSpec(options) {
304
305
  if (options.actionConfig.omitShow)
305
306
  return '';
306
307
  const { path, pathParams, modelConfig, fullyQualifiedModelName, singular, attributeData } = options;
307
- const subjectFunctionName = `show${modelConfig.modelClassName}`;
308
+ const subjectFunctionName = 'show';
308
309
  const subjectFunction = singular
309
310
  ? `
310
311
  const ${subjectFunctionName} = async <StatusCode extends 200 | 400 | 404>(expectedStatus: StatusCode) => {
@@ -345,7 +346,7 @@ function generateCreateActionSpec(options) {
345
346
  if (options.actionConfig.omitCreate)
346
347
  return '';
347
348
  const { path, pathParams, modelConfig, fullyQualifiedModelName, singular, attributeData } = options;
348
- const subjectFunctionName = `create${modelConfig.modelClassName}`;
349
+ const subjectFunctionName = 'create';
349
350
  const uuidSetup = attributeData.uuidAttributes
350
351
  .map(attrName => {
351
352
  const isArray = attributeData.uuidArrayAttributes.includes(attrName);
@@ -396,7 +397,7 @@ function generateUpdateActionSpec(options) {
396
397
  if (options.actionConfig.omitUpdate)
397
398
  return '';
398
399
  const { path, pathParams, modelConfig, fullyQualifiedModelName, singular, attributeData } = options;
399
- const subjectFunctionName = `update${modelConfig.modelClassName}`;
400
+ const subjectFunctionName = 'update';
400
401
  const uuidSetup = attributeData.uuidAttributes
401
402
  .map(attrName => {
402
403
  const isArray = attributeData.uuidArrayAttributes.includes(attrName);
@@ -491,7 +492,7 @@ function generateDestroyActionSpec(options) {
491
492
  if (options.actionConfig.omitDestroy)
492
493
  return '';
493
494
  const { path, pathParams, modelConfig, fullyQualifiedModelName, singular } = options;
494
- const subjectFunctionName = `destroy${modelConfig.modelClassName}`;
495
+ const subjectFunctionName = 'destroy';
495
496
  const subjectFunction = singular
496
497
  ? `
497
498
  const ${subjectFunctionName} = async <StatusCode extends 204 | 400 | 404>(expectedStatus: StatusCode) => {
@@ -310,7 +310,6 @@ export default class OpenapiSegmentExpander {
310
310
  }
311
311
  let referencedSerializers = [];
312
312
  if (objectBodySegment.additionalProperties === false) {
313
- ;
314
313
  data.additionalProperties = false;
315
314
  }
316
315
  else if (objectBodySegment.additionalProperties) {
@@ -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, } = {}) {
@@ -48,97 +48,31 @@ export default class Params {
48
48
  continue;
49
49
  const columnMetadata = columns[columnName];
50
50
  try {
51
- switch (columnMetadata?.dbType) {
52
- case 'bigint':
53
- case 'bigint[]':
54
- case 'boolean':
55
- case 'boolean[]':
56
- case 'date':
57
- case 'date[]':
58
- case 'integer':
59
- case 'integer[]':
60
- case 'uuid':
61
- case 'uuid[]':
62
- case 'json':
63
- case 'json[]':
64
- returnObj[columnName] = this.cast(params, columnName.toString(), columnMetadata.dbType, { allowNull: columnMetadata.allowNull });
65
- break;
66
- case 'character varying':
67
- case 'citext':
68
- case 'text':
69
- returnObj[columnName] = this.cast(params, columnName.toString(), 'string', { allowNull: columnMetadata.allowNull });
70
- break;
71
- case 'character varying[]':
72
- case 'citext[]':
73
- case 'text[]':
74
- returnObj[columnName] = this.cast(params, columnName.toString(), 'string[]', {
51
+ const castType = DB_TYPE_TO_CAST_TYPE[columnMetadata?.dbType];
52
+ if (castType) {
53
+ returnObj[columnName] = this.cast(params, columnName.toString(), castType, { allowNull: columnMetadata.allowNull });
54
+ }
55
+ else if (dreamClass.isVirtualColumn(columnName)) {
56
+ returnObj[columnName] = params[columnName];
57
+ }
58
+ else if (columnMetadata?.enumValues) {
59
+ const paramValue = params[columnName];
60
+ if (columnMetadata.isArray) {
61
+ if (!Array.isArray(paramValue))
62
+ returnObj[columnName] = ['expected an array of enum values'];
63
+ returnObj[columnName] = paramValue.map(p => {
64
+ return new this(params).cast(columnName.toString(), p, 'string', {
65
+ allowNull: columnMetadata.allowNull,
66
+ enum: columnMetadata.enumValues,
67
+ });
68
+ });
69
+ }
70
+ else {
71
+ returnObj[columnName] = this.cast(params, columnName.toString(), 'string', {
75
72
  allowNull: columnMetadata.allowNull,
73
+ enum: columnMetadata.enumValues,
76
74
  });
77
- break;
78
- case 'timestamp':
79
- case 'timestamp with time zone':
80
- case 'timestamp without time zone':
81
- returnObj[columnName] = this.cast(params, columnName.toString(), 'datetime', { allowNull: columnMetadata.allowNull });
82
- break;
83
- case 'timestamp[]':
84
- case 'timestamp with time zone[]':
85
- case 'timestamp without time zone[]':
86
- returnObj[columnName] = this.cast(params, columnName.toString(), 'datetime[]', { allowNull: columnMetadata.allowNull });
87
- break;
88
- case 'time':
89
- case 'time without time zone':
90
- returnObj[columnName] = this.cast(params, columnName.toString(), 'time', { allowNull: columnMetadata.allowNull });
91
- break;
92
- case 'time[]':
93
- case 'time without time zone[]':
94
- returnObj[columnName] = this.cast(params, columnName.toString(), 'time[]', { allowNull: columnMetadata.allowNull });
95
- break;
96
- case 'timetz':
97
- case 'time with time zone':
98
- returnObj[columnName] = this.cast(params, columnName.toString(), 'timetz', { allowNull: columnMetadata.allowNull });
99
- break;
100
- case 'timetz[]':
101
- case 'time with time zone[]':
102
- returnObj[columnName] = this.cast(params, columnName.toString(), 'timetz[]', { allowNull: columnMetadata.allowNull });
103
- break;
104
- case 'jsonb':
105
- returnObj[columnName] = this.cast(params, columnName.toString(), 'json', { allowNull: columnMetadata.allowNull });
106
- break;
107
- case 'jsonb[]':
108
- returnObj[columnName] = this.cast(params, columnName.toString(), 'json[]', { allowNull: columnMetadata.allowNull });
109
- break;
110
- case 'numeric':
111
- returnObj[columnName] = this.cast(params, columnName.toString(), 'number', { allowNull: columnMetadata.allowNull });
112
- break;
113
- case 'numeric[]':
114
- returnObj[columnName] = this.cast(params, columnName.toString(), 'number[]', { allowNull: columnMetadata.allowNull });
115
- break;
116
- default:
117
- if (dreamClass.isVirtualColumn(columnName))
118
- returnObj[columnName] = params[columnName];
119
- if (columnMetadata?.enumValues) {
120
- const paramValue = params[columnName];
121
- if (columnMetadata.isArray) {
122
- if (!Array.isArray(paramValue))
123
- returnObj[columnName] = ['expected an array of enum values'];
124
- returnObj[columnName] = paramValue.map(p => {
125
- return new this(params).cast(columnName.toString(), p,
126
- // casting to allow enum handling at lower level
127
- 'string', {
128
- allowNull: columnMetadata.allowNull,
129
- enum: columnMetadata.enumValues,
130
- });
131
- });
132
- }
133
- else {
134
- returnObj[columnName] = this.cast(params, columnName.toString(),
135
- // casting to allow enum handling at lower level
136
- 'string', {
137
- allowNull: columnMetadata.allowNull,
138
- enum: columnMetadata.enumValues,
139
- });
140
- }
141
- }
75
+ }
142
76
  }
143
77
  }
144
78
  catch (err) {
@@ -402,6 +336,54 @@ export default class Params {
402
336
  throw new ParamValidationError(paramName, [message]);
403
337
  }
404
338
  }
339
+ /**
340
+ * Maps PostgreSQL database column types to Psychic cast types.
341
+ * Used by Params.for() to determine how to validate and cast each column value.
342
+ */
343
+ const DB_TYPE_TO_CAST_TYPE = {
344
+ // identity mappings (db type matches cast type)
345
+ bigint: 'bigint',
346
+ 'bigint[]': 'bigint[]',
347
+ boolean: 'boolean',
348
+ 'boolean[]': 'boolean[]',
349
+ date: 'date',
350
+ 'date[]': 'date[]',
351
+ integer: 'integer',
352
+ 'integer[]': 'integer[]',
353
+ uuid: 'uuid',
354
+ 'uuid[]': 'uuid[]',
355
+ json: 'json',
356
+ 'json[]': 'json[]',
357
+ // text variants → string
358
+ 'character varying': 'string',
359
+ citext: 'string',
360
+ text: 'string',
361
+ 'character varying[]': 'string[]',
362
+ 'citext[]': 'string[]',
363
+ 'text[]': 'string[]',
364
+ // timestamp variants → datetime
365
+ timestamp: 'datetime',
366
+ 'timestamp with time zone': 'datetime',
367
+ 'timestamp without time zone': 'datetime',
368
+ 'timestamp[]': 'datetime[]',
369
+ 'timestamp with time zone[]': 'datetime[]',
370
+ 'timestamp without time zone[]': 'datetime[]',
371
+ // time variants
372
+ time: 'time',
373
+ 'time without time zone': 'time',
374
+ 'time[]': 'time[]',
375
+ 'time without time zone[]': 'time[]',
376
+ timetz: 'timetz',
377
+ 'time with time zone': 'timetz',
378
+ 'timetz[]': 'timetz[]',
379
+ 'time with time zone[]': 'timetz[]',
380
+ // jsonb → json
381
+ jsonb: 'json',
382
+ 'jsonb[]': 'json[]',
383
+ // numeric → number
384
+ numeric: 'number',
385
+ 'numeric[]': 'number[]',
386
+ };
405
387
  const typeToErrorMap = {
406
388
  bigint: 'expected bigint',
407
389
  boolean: 'expected boolean',
@@ -4,7 +4,6 @@ import generateController from '../generate/controller.js';
4
4
  import generateSyncEnumsInitializer from '../generate/initializer/syncEnums.js';
5
5
  import generateSyncOpenapiTypescriptInitializer from '../generate/initializer/syncOpenapiTypescript.js';
6
6
  import generateOpenapiReduxBindings from '../generate/openapi/reduxBindings.js';
7
- import generateOpenapiZustandBindings from '../generate/openapi/zustandBindings.js';
8
7
  import generateResource from '../generate/resource.js';
9
8
  import Watcher from '../watcher/Watcher.js';
10
9
  const INDENT = ' ';
@@ -23,6 +22,9 @@ ${INDENT} - citext:
23
22
  ${INDENT} - citext[]:
24
23
  ${INDENT} case insensitive text (indexes and queries are automatically case insensitive)
25
24
  ${INDENT}
25
+ ${INDENT} - encrypted:
26
+ ${INDENT} encrypted text (used in conjunction with the @deco.Encrypted decorator)
27
+ ${INDENT}
26
28
  ${INDENT} - string:
27
29
  ${INDENT} - string[]:
28
30
  ${INDENT} varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional
@@ -84,7 +86,7 @@ export default class PsychicCLI {
84
86
  - update
85
87
  - delete`)
86
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')
87
- .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.')
88
90
  .option('--connection-name <connectionName>', 'the name of the db connection you would like to use for your model. Defaults to "default"', 'default')
89
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)')
90
92
  .argument('<path>', 'URL path from root domain. Specify nesting resource with `{}`, e.g.: `tickets/{}/comments`')
@@ -144,26 +146,6 @@ export default class PsychicCLI {
144
146
  });
145
147
  process.exit();
146
148
  });
147
- program
148
- .command('setup:sync:openapi-zustand')
149
- .description('Generates openapi zustand bindings using openapi-fetch to connect one of your openapi files to one of your clients.')
150
- .option('--schema-file <schemaFile>', 'the path from your api root to the openapi file you wish to use to generate your schema, i.e. ./src/openapi/openapi.json')
151
- .option('--export-name <exportName>', 'the camelCased name to use for your exported api client, i.e. myBackendApi')
152
- .option('--output-dir <outputDir>', 'the path to the directory where the generated api client and types files will be written, i.e. ../client/src/api')
153
- .option('--types-file <typesFile>', 'the path to the file that will contain your generated openapi TypeScript type definitions, i.e. ../client/src/api/myBackendApi.types.d.ts')
154
- .action(async ({ schemaFile, exportName, outputDir, typesFile, }) => {
155
- await initializePsychicApp({
156
- bypassDreamIntegrityChecks: true,
157
- bypassDbConnectionsDuringInit: true,
158
- });
159
- await generateOpenapiZustandBindings({
160
- exportName,
161
- schemaFile,
162
- outputDir,
163
- typesFile,
164
- });
165
- process.exit();
166
- });
167
149
  program
168
150
  .command('setup:sync:openapi-typescript')
169
151
  .description('Generates an initializer in your app for converting one of your openapi files to typescript.')