@rvoh/psychic 3.0.0-alpha.8 → 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 (41) hide show
  1. package/dist/cjs/src/cli/index.js +4 -22
  2. package/dist/cjs/src/controller/index.js +1 -1
  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 +1 -1
  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/openapi-renderer/helpers/safelyAttachCursorPaginationParamToRequestBodySegment.d.ts +1 -1
  24. package/dist/types/src/openapi-renderer/helpers/safelyAttachPaginationParamsToBodySegment.d.ts +6 -0
  25. package/dist/types/src/router/helpers.d.ts +3 -0
  26. package/package.json +30 -20
  27. package/dist/cjs/src/generate/helpers/zustandBindings/printFinalStepsMessage.js +0 -36
  28. package/dist/cjs/src/generate/helpers/zustandBindings/promptForOptions.js +0 -55
  29. package/dist/cjs/src/generate/helpers/zustandBindings/writeApiClientFile.js +0 -50
  30. package/dist/cjs/src/generate/helpers/zustandBindings/writeInitializer.js +0 -46
  31. package/dist/cjs/src/generate/openapi/zustandBindings.js +0 -27
  32. package/dist/esm/src/generate/helpers/zustandBindings/printFinalStepsMessage.js +0 -36
  33. package/dist/esm/src/generate/helpers/zustandBindings/promptForOptions.js +0 -55
  34. package/dist/esm/src/generate/helpers/zustandBindings/writeApiClientFile.js +0 -50
  35. package/dist/esm/src/generate/helpers/zustandBindings/writeInitializer.js +0 -46
  36. package/dist/esm/src/generate/openapi/zustandBindings.js +0 -27
  37. package/dist/types/src/generate/helpers/zustandBindings/printFinalStepsMessage.d.ts +0 -2
  38. package/dist/types/src/generate/helpers/zustandBindings/promptForOptions.d.ts +0 -2
  39. package/dist/types/src/generate/helpers/zustandBindings/writeApiClientFile.d.ts +0 -5
  40. package/dist/types/src/generate/helpers/zustandBindings/writeInitializer.d.ts +0 -5
  41. package/dist/types/src/generate/openapi/zustandBindings.d.ts +0 -21
@@ -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',
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @internal
3
3
  *
4
- * Used to carefully bind implicit pagination params
4
+ * Used to carefully bind implicit cursor pagination params
5
5
  * to the requestBody properties. It will not apply
6
6
  * the pagination param unless the provided bodySegment
7
7
  * is:
@@ -13,3 +13,9 @@
13
13
  * what was given to it, without any modifications
14
14
  */
15
15
  export default function safelyAttachPaginationParamToRequestBodySegment<T>(paramName: string, bodySegment: T): T;
16
+ /**
17
+ * @internal
18
+ *
19
+ * Generic version: attaches any OpenAPI property definition to a body segment.
20
+ */
21
+ export declare function safelyAttachParamToRequestBodySegment<T>(paramName: string, property: any, bodySegment: T): T;
@@ -21,7 +21,10 @@ export interface NamespaceConfig {
21
21
  resourceful: boolean;
22
22
  isScope: boolean;
23
23
  }
24
+ export declare function applyResourcefulAction(path: string, action: ResourcesMethodType, routingMechanism: RoutingMechanism, options?: ResourcesOptions, plural?: boolean): void;
25
+ /** @deprecated Use applyResourcefulAction with plural parameter instead */
24
26
  export declare function applyResourcesAction(path: string, action: ResourcesMethodType, routingMechanism: RoutingMechanism, options?: ResourcesOptions): void;
27
+ /** @deprecated Use applyResourcefulAction with plural parameter instead */
25
28
  export declare function applyResourceAction(path: string, action: ResourcesMethodType, routingMechanism: RoutingMechanism, options?: ResourcesOptions): void;
26
29
  /**
27
30
  * Converts OpenAPI-style route parameters to Express.js-style parameters
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "name": "@rvoh/psychic",
4
4
  "description": "Typescript web framework",
5
- "version": "3.0.0-alpha.8",
5
+ "version": "3.0.0",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",
@@ -47,6 +47,24 @@
47
47
  "files": [
48
48
  "dist/**/*"
49
49
  ],
50
+ "scripts": {
51
+ "client": "cd client && pnpm start",
52
+ "client:fspec": "cd client && VITE_PSYCHIC_ENV=test BROWSER=none pnpm start",
53
+ "psy": "NODE_ENV=${NODE_ENV:-test} pnpm psyts",
54
+ "psyjs": "node ./dist/test-app/src/cli/index.js",
55
+ "psyts": "NODE_ENV=${NODE_ENV:-test} tsx ./test-app/src/cli/index.ts",
56
+ "gpsy": "tsx ./global-cli/main.ts",
57
+ "build": "echo \"building cjs...\" && rm -rf dist && tsc -p ./tsconfig.cjs.build.json && echo \"building esm...\" && tsc -p ./tsconfig.esm.build.json",
58
+ "build:test-app": "rm -rf dist && echo \"building test app to esm...\" && tsc -p ./tsconfig.esm.build.test-app.json && echo \"building test app to cjs...\" && tsc -p ./tsconfig.cjs.build.test-app.json",
59
+ "types:esm:trace": "rm -rf dist && tsc -p ./tsconfig.esm.build.json --generateTrace ./typetrace --diagnostics && pnpm analyze-trace ./typetrace --skipMillis 100 --forceMillis 300",
60
+ "dev": "nodemon --quiet --no-stdin",
61
+ "console": "tsx ./test-app/src/conf/repl.ts",
62
+ "uspec": "vitest --config ./spec/unit/vite.config.ts",
63
+ "fspec": "vitest run --config=./spec/features/vite.config.ts",
64
+ "format": "pnpm prettier . --write",
65
+ "lint": "pnpm eslint --no-warn-ignored \"src/**/*.ts\" \"spec/**/*.ts\" \"test-app/**/*.ts\" && pnpm prettier . --check",
66
+ "prepack": "pnpm build"
67
+ },
50
68
  "dependencies": {
51
69
  "ajv": "^8.17.1",
52
70
  "ajv-formats": "^3.0.1",
@@ -74,9 +92,9 @@
74
92
  "@koa/cors": "^5.0.0",
75
93
  "@koa/etag": "^5.0.2",
76
94
  "@koa/router": "^15.3.1",
77
- "@rvoh/dream": "^2.4.0-alpha.1",
95
+ "@rvoh/dream": "^2.4.0",
78
96
  "@rvoh/dream-spec-helpers": "^2.1.1",
79
- "@rvoh/psychic-spec-helpers": "3.0.0-alpha.3",
97
+ "@rvoh/psychic-spec-helpers": "3.0.0",
80
98
  "@types/koa": "^3.0.1",
81
99
  "@types/koa-bodyparser": "^4.3.12",
82
100
  "@types/koa-conditional-get": "^2.0.3",
@@ -120,21 +138,13 @@
120
138
  "vitest": "^4.0.9",
121
139
  "winston": "^3.14.2"
122
140
  },
123
- "scripts": {
124
- "client": "cd client && pnpm start",
125
- "client:fspec": "cd client && VITE_PSYCHIC_ENV=test BROWSER=none pnpm start",
126
- "psy": "NODE_ENV=${NODE_ENV:-test} pnpm psyts",
127
- "psyjs": "node ./dist/test-app/src/cli/index.js",
128
- "psyts": "NODE_ENV=${NODE_ENV:-test} tsx ./test-app/src/cli/index.ts",
129
- "gpsy": "tsx ./global-cli/main.ts",
130
- "build": "echo \"building cjs...\" && rm -rf dist && tsc -p ./tsconfig.cjs.build.json && echo \"building esm...\" && tsc -p ./tsconfig.esm.build.json",
131
- "build:test-app": "rm -rf dist && echo \"building test app to esm...\" && tsc -p ./tsconfig.esm.build.test-app.json && echo \"building test app to cjs...\" && tsc -p ./tsconfig.cjs.build.test-app.json",
132
- "types:esm:trace": "rm -rf dist && tsc -p ./tsconfig.esm.build.json --generateTrace ./typetrace --diagnostics && pnpm analyze-trace ./typetrace --skipMillis 100 --forceMillis 300",
133
- "dev": "nodemon --quiet --no-stdin",
134
- "console": "tsx ./test-app/src/conf/repl.ts",
135
- "uspec": "vitest --config ./spec/unit/vite.config.ts",
136
- "fspec": "vitest run --config=./spec/features/vite.config.ts",
137
- "format": "pnpm prettier . --write",
138
- "lint": "pnpm eslint --no-warn-ignored \"src/**/*.ts\" \"spec/**/*.ts\" \"test-app/**/*.ts\" && pnpm prettier . --check"
141
+ "packageManager": "pnpm@10.26.0+sha512.3b3f6c725ebe712506c0ab1ad4133cf86b1f4b687effce62a9b38b4d72e3954242e643190fc51fa1642949c735f403debd44f5cb0edd657abe63a8b6a7e1e402",
142
+ "pnpm": {
143
+ "overrides": {
144
+ "diff": ">=8.0.3",
145
+ "minimatch@3": "3.1.3",
146
+ "minimatch@5": "5.1.7",
147
+ "minimatch@9": "9.0.6"
148
+ }
139
149
  }
140
- }
150
+ }
@@ -1,36 +0,0 @@
1
- import { DreamCLI } from '@rvoh/dream/system';
2
- import { camelize } from '@rvoh/dream/utils';
3
- import colorize from '../../../cli/helpers/colorize.js';
4
- export default function printFinalStepsMessage(opts) {
5
- const clientFile = `${opts.outputDir}/${camelize(opts.exportName)}.ts`;
6
- const importLine = colorize(`+ import { ${opts.exportName} } from '${clientFile}'`, { color: 'green' });
7
- DreamCLI.logger.log(`
8
- Finished generating zustand + openapi-fetch bindings for your application,
9
- but to wire them into your app, we're going to need your help.
10
-
11
- First, you will need to be sure to sync, so that the new openapi
12
- types are sent over to your client application.
13
-
14
- Next, you can use the generated api client in your zustand stores, i.e.
15
-
16
- ${importLine}
17
-
18
- ${colorize(`+ import { create } from 'zustand'`, { color: 'green' })}
19
-
20
- interface MyState {
21
- items: Item[]
22
- loading: boolean
23
- fetchItems: () => Promise<void>
24
- }
25
-
26
- export const useMyStore = create<MyState>((set) => ({
27
- items: [],
28
- loading: false,
29
- fetchItems: async () => {
30
- set({ loading: true })
31
- const { data } = await ${opts.exportName}.GET('/items')
32
- set({ items: data ?? [], loading: false })
33
- },
34
- }))
35
- `, { logPrefix: '' });
36
- }