@rvoh/psychic 1.10.5 → 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.
@@ -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
@@ -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
  //////////////////
@@ -62,7 +62,7 @@ class PsychicApp {
62
62
  */
63
63
  static async init(cb, dreamCb, opts = {}) {
64
64
  let psychicApp;
65
- await dream_1.DreamApp.init(dreamCb, { bypassModelIntegrityCheck: opts.bypassModelIntegrityCheck }, async (dreamApp) => {
65
+ await dream_1.DreamApp.init(dreamCb, opts, async (dreamApp) => {
66
66
  psychicApp = new PsychicApp();
67
67
  await cb(psychicApp);
68
68
  if (!psychicApp.loadedControllers)
@@ -97,7 +97,7 @@ class PsychicApp {
97
97
  dreamApp.set('logger', psychicApp.logger);
98
98
  dreamApp.set('packageManager', psychicApp.packageManager);
99
99
  (0, cache_js_1.cachePsychicApp)(psychicApp);
100
- if (!opts.bypassModelIntegrityCheck) {
100
+ if (!opts.bypassDreamIntegrityChecks) {
101
101
  // routes _must_ be built before openapi
102
102
  // cache can be processed
103
103
  await psychicApp.buildRoutesCache();
@@ -67,7 +67,7 @@ export default class PsychicCLI {
67
67
  .argument('<modelName>', 'the name of the model to create, e.g. Post or Settings/CommunicationPreferences')
68
68
  .argument('[columnsWithTypes...]', columnsWithTypesDescription)
69
69
  .action(async (route, modelName, columnsWithTypes, options) => {
70
- await initializePsychicApp();
70
+ await initializePsychicApp({ bypassDreamIntegrityChecks: true });
71
71
  await PsychicBin.generateResource(route, modelName, columnsWithTypes, options);
72
72
  process.exit();
73
73
  });
@@ -78,7 +78,7 @@ export default class PsychicCLI {
78
78
  .argument('<controllerName>', 'the name of the controller to create, including namespaces, e.g. Posts or Api/V1/Posts')
79
79
  .argument('[actions...]', 'the names of controller actions to create')
80
80
  .action(async (controllerName, actions) => {
81
- await initializePsychicApp();
81
+ await initializePsychicApp({ bypassDreamIntegrityChecks: true });
82
82
  await PsychicBin.generateController(controllerName, actions);
83
83
  process.exit();
84
84
  });
@@ -135,7 +135,7 @@ export default class PsychicCLI {
135
135
  .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')
136
136
  .option('--schema-only')
137
137
  .action(async (options = {}) => {
138
- await initializePsychicApp();
138
+ await initializePsychicApp({ bypassDreamIntegrityChecks: !!options.schemaOnly });
139
139
  await PsychicBin.sync(options);
140
140
  process.exit();
141
141
  });
@@ -144,7 +144,7 @@ export default class PsychicCLI {
144
144
  .description('watches your app for changes, and re-syncs any time they happen')
145
145
  .argument('[dir]', 'the folder you want to watch, defaults to ./src')
146
146
  .action(async (dir) => {
147
- await initializePsychicApp();
147
+ await initializePsychicApp({ bypassDreamIntegrityChecks: true });
148
148
  Watcher.watch(dir);
149
149
  });
150
150
  program
@@ -1,8 +1,8 @@
1
1
  import { camelize, capitalize, compact, globalClassNameFromFullyQualifiedModelName, standardizeFullyQualifiedModelName, uniq, } from '@rvoh/dream';
2
+ import addImportSuffix from '../../helpers/path/addImportSuffix.js';
2
3
  import relativePsychicPath from '../../helpers/path/relativePsychicPath.js';
3
4
  import updirsFromPath from '../../helpers/path/updirsFromPath.js';
4
5
  import { pluralize } from '../../index.js';
5
- import addImportSuffix from '../../helpers/path/addImportSuffix.js';
6
6
  export default function generateResourceControllerSpecContent({ fullyQualifiedControllerName, route, fullyQualifiedModelName, columnsWithTypes, owningModel, forAdmin, singular, actions, }) {
7
7
  fullyQualifiedModelName = standardizeFullyQualifiedModelName(fullyQualifiedModelName);
8
8
  const modelClassName = globalClassNameFromFullyQualifiedModelName(fullyQualifiedModelName);
@@ -15,7 +15,7 @@ export default function generateResourceControllerSpecContent({ fullyQualifiedCo
15
15
  ? globalClassNameFromFullyQualifiedModelName(owningModel)
16
16
  : userModelName;
17
17
  const owningModelVariableName = owningModelName ? camelize(owningModelName) : userVariableName;
18
- const dreamImports = ['UpdateableProperties'];
18
+ const dreamImports = [];
19
19
  const importStatements = compact([
20
20
  importStatementForModel(fullyQualifiedControllerName, fullyQualifiedModelName),
21
21
  importStatementForModel(fullyQualifiedControllerName, userModelName),
@@ -28,7 +28,6 @@ export default function generateResourceControllerSpecContent({ fullyQualifiedCo
28
28
  const attributeCreationKeyValues = [];
29
29
  const attributeUpdateKeyValues = [];
30
30
  const comparableOriginalAttributeKeyValues = [];
31
- const comparableUpdatedAttributeKeyValues = [];
32
31
  const expectEqualOriginalValue = [];
33
32
  const expectEqualUpdatedValue = [];
34
33
  const expectEqualOriginalNamedVariable = [];
@@ -37,89 +36,88 @@ export default function generateResourceControllerSpecContent({ fullyQualifiedCo
37
36
  let dateAttributeIncluded = false;
38
37
  let datetimeAttributeIncluded = false;
39
38
  for (const attribute of columnsWithTypes) {
40
- const [rawAttributeName, attributeType, , enumValues] = attribute.split(':');
39
+ const [rawAttributeName, rawAttributeType, , enumValues] = attribute.split(':');
41
40
  if (/(_type|_id)$/.test(rawAttributeName ?? ''))
42
41
  continue;
43
42
  const attributeName = camelize(rawAttributeName ?? '');
44
43
  const dotNotationVariable = `${modelVariableName}.${attributeName}`;
44
+ if (!rawAttributeType)
45
+ continue;
46
+ const arrayBracketRegexp = /\[\]$/;
47
+ const isArray = arrayBracketRegexp.test(rawAttributeType);
48
+ const attributeType = rawAttributeType.replace(arrayBracketRegexp, '');
45
49
  if (attributeName === 'deletedAt')
46
50
  continue;
47
51
  switch (attributeType) {
48
52
  case 'enum': {
49
- const originalEnumValue = (enumValues ?? '').split(',').at(0);
50
- const updatedEnumValue = (enumValues ?? '').split(',').at(-1);
51
- attributeCreationKeyValues.push(`${attributeName}: '${originalEnumValue}',`);
52
- attributeUpdateKeyValues.push(`${attributeName}: '${updatedEnumValue}',`);
53
+ const rawOriginalEnumValue = (enumValues ?? '').split(',').at(0);
54
+ const rawUpdatedEnumValue = (enumValues ?? '').split(',').at(-1);
55
+ const originalEnumValue = isArray ? [rawOriginalEnumValue] : rawOriginalEnumValue;
56
+ const updatedEnumValue = isArray ? [rawUpdatedEnumValue] : rawUpdatedEnumValue;
57
+ attributeCreationKeyValues.push(`${attributeName}: ${jsonify(originalEnumValue)},`);
58
+ attributeUpdateKeyValues.push(`${attributeName}: ${jsonify(updatedEnumValue)},`);
53
59
  comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
54
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: '${updatedEnumValue}',`);
55
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual('${originalEnumValue}')`);
56
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual('${updatedEnumValue}')`);
60
+ expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(originalEnumValue)})`);
61
+ expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(updatedEnumValue)})`);
57
62
  break;
58
63
  }
59
64
  case 'string':
60
65
  case 'text':
61
66
  case 'citext': {
62
- const originalStringValue = `The ${fullyQualifiedModelName} ${attributeName}`;
63
- const updatedStringValue = `Updated ${fullyQualifiedModelName} ${attributeName}`;
64
- attributeCreationKeyValues.push(`${attributeName}: '${originalStringValue}',`);
65
- attributeUpdateKeyValues.push(`${attributeName}: '${updatedStringValue}',`);
67
+ const rawOriginalStringValue = `The ${fullyQualifiedModelName} ${attributeName}`;
68
+ const rawUpdatedStringValue = `Updated ${fullyQualifiedModelName} ${attributeName}`;
69
+ const originalStringValue = isArray ? [rawOriginalStringValue] : rawOriginalStringValue;
70
+ const updatedStringValue = isArray ? [rawUpdatedStringValue] : rawUpdatedStringValue;
71
+ attributeCreationKeyValues.push(`${attributeName}: ${jsonify(originalStringValue)},`);
72
+ attributeUpdateKeyValues.push(`${attributeName}: ${jsonify(updatedStringValue)},`);
66
73
  comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
67
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: '${updatedStringValue}',`);
68
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual('${originalStringValue}')`);
69
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual('${updatedStringValue}')`);
74
+ expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(originalStringValue)})`);
75
+ expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(updatedStringValue)})`);
70
76
  break;
71
77
  }
72
78
  case 'integer':
73
- attributeCreationKeyValues.push(`${attributeName}: 1,`);
74
- attributeUpdateKeyValues.push(`${attributeName}: 2,`);
75
- comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
76
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: 2,`);
77
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(1)`);
78
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(2)`);
79
- break;
80
- case 'bigint':
81
- attributeCreationKeyValues.push(`${attributeName}: '11111111111111111',`);
82
- attributeUpdateKeyValues.push(`${attributeName}: '22222222222222222',`);
83
- comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
84
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: '22222222222222222',`);
85
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual('11111111111111111')`);
86
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual('22222222222222222')`);
87
- break;
88
79
  case 'decimal':
89
- attributeCreationKeyValues.push(`${attributeName}: 1.1,`);
90
- attributeUpdateKeyValues.push(`${attributeName}: 2.2,`);
80
+ case 'bigint': {
81
+ const rawOriginalValue = attributeType === 'integer' ? 1 : attributeType === 'decimal' ? 1.1 : '11111111111111111';
82
+ const rawUpdatedValue = attributeType === 'integer' ? 2 : attributeType === 'decimal' ? 2.2 : '22222222222222222';
83
+ const originalValue = isArray ? [rawOriginalValue] : rawOriginalValue;
84
+ const updatedValue = isArray ? [rawUpdatedValue] : rawUpdatedValue;
85
+ attributeCreationKeyValues.push(`${attributeName}: ${jsonify(originalValue)},`);
86
+ attributeUpdateKeyValues.push(`${attributeName}: ${jsonify(updatedValue)},`);
91
87
  comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
92
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: 2.2,`);
93
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(1.1)`);
94
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(2.2)`);
88
+ expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(originalValue)})`);
89
+ expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(${jsonify(updatedValue)})`);
95
90
  break;
96
- case 'date':
91
+ }
92
+ case 'date': {
97
93
  dreamImports.push('CalendarDate');
98
94
  dateAttributeIncluded = true;
99
- attributeCreationKeyValues.push(`${attributeName}: today,`);
100
- attributeUpdateKeyValues.push(`${attributeName}: yesterday,`);
101
- comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable}.toISO(),`);
102
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: yesterday.toISO(),`);
103
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqualCalendarDate(today)`);
104
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqualCalendarDate(yesterday)`);
95
+ attributeCreationKeyValues.push(`${attributeName}: ${isArray ? '[today.toISO()]' : 'today.toISO()'},`);
96
+ attributeUpdateKeyValues.push(`${attributeName}: ${isArray ? '[yesterday.toISO()]' : 'yesterday.toISO()'},`);
97
+ comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable}${isArray ? '.map(date => date.toISO())' : '.toISO()'},`);
98
+ expectEqualOriginalValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualCalendarDate(today)`);
99
+ expectEqualUpdatedValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualCalendarDate(yesterday)`);
105
100
  break;
106
- case 'datetime':
101
+ }
102
+ case 'datetime': {
107
103
  dreamImports.push('DateTime');
108
104
  datetimeAttributeIncluded = true;
109
- attributeCreationKeyValues.push(`${attributeName}: now,`);
110
- attributeUpdateKeyValues.push(`${attributeName}: lastHour,`);
111
- comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable}.toISO(),`);
112
- comparableUpdatedAttributeKeyValues.push(`${attributeName}: lastHour.toISO(),`);
113
- expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqualDateTime(now)`);
114
- expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqualDateTime(lastHour)`);
105
+ attributeCreationKeyValues.push(`${attributeName}: ${isArray ? '[now.toISO()]' : 'now.toISO()'},`);
106
+ attributeUpdateKeyValues.push(`${attributeName}: ${isArray ? '[lastHour.toISO()]' : 'lastHour.toISO()'},`);
107
+ comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable}${isArray ? '.map(datetime => datetime.toISO())' : '.toISO()'},`);
108
+ expectEqualOriginalValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualDateTime(now)`);
109
+ expectEqualUpdatedValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualDateTime(lastHour)`);
115
110
  break;
111
+ }
116
112
  default:
117
113
  continue;
118
114
  }
119
115
  keyWithDotValue.push(`${attributeName}: ${dotNotationVariable},`);
120
- const originalAttributeVariableName = 'original' + capitalize(attributeName);
121
- originalValueVariableAssignments.push(`const ${originalAttributeVariableName} = ${dotNotationVariable}`);
122
- expectEqualOriginalNamedVariable.push(`expect(${dotNotationVariable}).toEqual(${originalAttributeVariableName})`);
116
+ if (!((attributeType === 'date' || attributeType === 'datetime') && isArray)) {
117
+ const originalAttributeVariableName = 'original' + capitalize(attributeName);
118
+ originalValueVariableAssignments.push(`const ${originalAttributeVariableName} = ${dotNotationVariable}`);
119
+ expectEqualOriginalNamedVariable.push(`expect(${dotNotationVariable}).toEqual(${originalAttributeVariableName})`);
120
+ }
123
121
  }
124
122
  const simpleCreationCommand = `const ${modelVariableName} = await create${modelClassName}(${forAdmin ? '' : `{ ${owningModelVariableName} }`})`;
125
123
  const omitIndex = singular || !actions.includes('index');
@@ -128,7 +126,10 @@ export default function generateResourceControllerSpecContent({ fullyQualifiedCo
128
126
  const omitUpdate = !actions.includes('update');
129
127
  const omitDestroy = !actions.includes('destroy');
130
128
  return `\
131
- import { ${uniq(dreamImports).join(', ')} } from '@rvoh/dream'${uniq(importStatements).join('')}
129
+ import { DreamRequestAttributes } from '@rvoh/psychic-spec-helpers'${dreamImports.length
130
+ ? `
131
+ import { ${uniq(dreamImports).join(', ')} } from '@rvoh/dream'`
132
+ : ''}${uniq(importStatements).join('')}
132
133
  import { session, SpecRequestType } from '${specUnitUpdirs}helpers/${addImportSuffix('authentication.js')}'
133
134
 
134
135
  describe('${fullyQualifiedControllerName}', () => {
@@ -213,7 +214,7 @@ describe('${fullyQualifiedControllerName}', () => {
213
214
 
214
215
  describe('POST create', () => {
215
216
  const subject = async <StatusCode extends 201 | 400>(
216
- data: UpdateableProperties<${modelClassName}>,
217
+ data: DreamRequestAttributes<${modelClassName}>,
217
218
  expectedStatus: StatusCode
218
219
  ) => {
219
220
  return request.post('/${route}', expectedStatus, { data })
@@ -247,7 +248,7 @@ describe('${fullyQualifiedControllerName}', () => {
247
248
  describe('PATCH update', () => {${singular
248
249
  ? `
249
250
  const subject = async <StatusCode extends 204 | 400 | 404>(
250
- data: UpdateableProperties<${modelClassName}>,
251
+ data: DreamRequestAttributes<${modelClassName}>,
251
252
  expectedStatus: StatusCode
252
253
  ) => {
253
254
  return request.patch('/${route}', expectedStatus, {
@@ -257,7 +258,7 @@ describe('${fullyQualifiedControllerName}', () => {
257
258
  : `
258
259
  const subject = async <StatusCode extends 204 | 400 | 404>(
259
260
  ${modelVariableName}: ${modelClassName},
260
- data: UpdateableProperties<${modelClassName}>,
261
+ data: DreamRequestAttributes<${modelClassName}>,
261
262
  expectedStatus: StatusCode
262
263
  ) => {
263
264
  return request.patch('/${route}/{id}', expectedStatus, {
@@ -286,7 +287,13 @@ describe('${fullyQualifiedControllerName}', () => {
286
287
  : `
287
288
 
288
289
  context('a ${fullyQualifiedModelName} created by another ${owningModelName}', () => {
289
- it('is not updated', async () => {
290
+ it('is not updated', async () => {${dateAttributeIncluded
291
+ ? `
292
+ const yesterday = CalendarDate.yesterday()`
293
+ : ''}${datetimeAttributeIncluded
294
+ ? `
295
+ const lastHour = DateTime.now().minus({ hour: 1 })`
296
+ : ''}${dateAttributeIncluded || datetimeAttributeIncluded ? '\n' : ''}
290
297
  const ${modelVariableName} = await create${modelClassName}()
291
298
  ${originalValueVariableAssignments.length ? originalValueVariableAssignments.join('\n ') : ''}
292
299
 
@@ -343,3 +350,7 @@ function importStatementForModel(originModelName, destinationModelName = originM
343
350
  function importStatementForModelFactory(originModelName, destinationModelName = originModelName) {
344
351
  return `\nimport create${globalClassNameFromFullyQualifiedModelName(destinationModelName)} from '${relativePsychicPath('controllerSpecs', 'factories', originModelName, destinationModelName)}'`;
345
352
  }
353
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
354
+ function jsonify(val) {
355
+ return JSON.stringify(val).replace(/"/g, "'");
356
+ }
@@ -10,6 +10,8 @@ export default async function generateResource({ route, fullyQualifiedModelName,
10
10
  // which will exhibit bad behavior when provided with
11
11
  // a prefixing slash.
12
12
  route = route.replace(/^\/+/, '');
13
+ if (!options.singular)
14
+ route = pluralize(route);
13
15
  const fullyQualifiedControllerName = standardizeFullyQualifiedModelName(route);
14
16
  const resourcefulActions = options.singular ? [...SINGULAR_RESOURCE_ACTIONS] : [...RESOURCE_ACTIONS];
15
17
  const onlyActions = options.only?.split(',');
@@ -24,7 +26,6 @@ export default async function generateResource({ route, fullyQualifiedModelName,
24
26
  connectionName: options.connectionName,
25
27
  },
26
28
  });
27
- route = pluralize(route);
28
29
  await generateController({
29
30
  fullyQualifiedControllerName,
30
31
  fullyQualifiedModelName,
@@ -67,11 +67,7 @@ export default class SerializerOpenapiRenderer {
67
67
  const requiredProperties = compact(this.serializerBuilder['attributes'].map(attribute => {
68
68
  const attributeType = attribute.type;
69
69
  switch (attributeType) {
70
- case 'attribute': {
71
- if (attribute.options?.required === false)
72
- return null;
73
- return attribute.options?.as ?? attribute.name;
74
- }
70
+ case 'attribute':
75
71
  case 'delegatedAttribute': {
76
72
  if (attribute.options?.required === false)
77
73
  return null;
@@ -114,23 +110,6 @@ export default class SerializerOpenapiRenderer {
114
110
  let newlyReferencedSerializers = [];
115
111
  accumulator = (() => {
116
112
  switch (attributeType) {
117
- ////////////////
118
- // attributes //
119
- ////////////////
120
- case 'attribute': {
121
- const outputAttributeName = this.setCase(attribute.options?.as ?? attribute.name);
122
- const openapi = attribute.options.openapi;
123
- newlyReferencedSerializers = allSerializersFromHandWrittenOpenapi(openapi);
124
- accumulator[outputAttributeName] = DataTypeForOpenapi?.isDream
125
- ? dreamColumnOpenapiShape(DataTypeForOpenapi, attribute.name, openapi, {
126
- suppressResponseEnums: this.suppressResponseEnums,
127
- })
128
- : allSerializersToRefsInOpenapi(openapiShorthandToOpenapi(openapi));
129
- return accumulator;
130
- }
131
- /////////////////////
132
- // end: attributes //
133
- /////////////////////
134
113
  ///////////////////////
135
114
  // custom attributes //
136
115
  ///////////////////////
@@ -149,19 +128,39 @@ export default class SerializerOpenapiRenderer {
149
128
  ////////////////////////////
150
129
  // end: custom attributes //
151
130
  ////////////////////////////
152
- //////////////////////////
153
- // delegated attributes //
154
- //////////////////////////
131
+ //////////////////////////////////////////
132
+ // attributes and delegated attributes //
133
+ //////////////////////////////////////////
134
+ case 'attribute':
155
135
  case 'delegatedAttribute': {
156
136
  const outputAttributeName = this.setCase(attribute.options?.as ?? attribute.name);
157
137
  const openapi = attribute.options.openapi;
158
138
  newlyReferencedSerializers = allSerializersFromHandWrittenOpenapi(openapi);
159
- accumulator[outputAttributeName] = allSerializersToRefsInOpenapi(openapiShorthandToOpenapi(openapi));
139
+ let target;
140
+ if (attributeType === 'delegatedAttribute' && DataTypeForOpenapi?.isDream) {
141
+ const source = DataTypeForOpenapi;
142
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
143
+ const associatedModelOrModels = source['getAssociationMetadata'](attribute.targetName)?.modelCB();
144
+ target = Array.isArray(associatedModelOrModels)
145
+ ? associatedModelOrModels[0]
146
+ : associatedModelOrModels;
147
+ }
148
+ else if (attributeType === 'delegatedAttribute') {
149
+ target = undefined;
150
+ }
151
+ else {
152
+ target = DataTypeForOpenapi;
153
+ }
154
+ accumulator[outputAttributeName] = allSerializersToRefsInOpenapi(target?.isDream
155
+ ? dreamColumnOpenapiShape(target, attribute.name, openapi, {
156
+ suppressResponseEnums: this.suppressResponseEnums,
157
+ })
158
+ : openapiShorthandToOpenapi(openapi));
160
159
  return accumulator;
161
160
  }
162
- ///////////////////////////////
163
- // end: delegated attributes //
164
- ///////////////////////////////
161
+ /////////////////////////////////////////////
162
+ // end:attributes and delegated attributes //
163
+ /////////////////////////////////////////////
165
164
  //////////////////
166
165
  // rendersOnes //
167
166
  //////////////////
@@ -33,7 +33,7 @@ export default class PsychicApp {
33
33
  */
34
34
  static async init(cb, dreamCb, opts = {}) {
35
35
  let psychicApp;
36
- await DreamApp.init(dreamCb, { bypassModelIntegrityCheck: opts.bypassModelIntegrityCheck }, async (dreamApp) => {
36
+ await DreamApp.init(dreamCb, opts, async (dreamApp) => {
37
37
  psychicApp = new PsychicApp();
38
38
  await cb(psychicApp);
39
39
  if (!psychicApp.loadedControllers)
@@ -68,7 +68,7 @@ export default class PsychicApp {
68
68
  dreamApp.set('logger', psychicApp.logger);
69
69
  dreamApp.set('packageManager', psychicApp.packageManager);
70
70
  cachePsychicApp(psychicApp);
71
- if (!opts.bypassModelIntegrityCheck) {
71
+ if (!opts.bypassDreamIntegrityChecks) {
72
72
  // routes _must_ be built before openapi
73
73
  // cache can be processed
74
74
  await psychicApp.buildRoutesCache();
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": "1.10.5",
5
+ "version": "1.11.0-beta.1",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",
@@ -62,9 +62,9 @@
62
62
  "devDependencies": {
63
63
  "@eslint/js": "^9.19.0",
64
64
  "@jest-mock/express": "^3.0.0",
65
- "@rvoh/dream": "^1.9.3",
66
- "@rvoh/dream-spec-helpers": "^1.1.1",
67
- "@rvoh/psychic-spec-helpers": "^1.0.0",
65
+ "@rvoh/dream": "^1.10.0-beta.1",
66
+ "@rvoh/dream-spec-helpers": "^1.2.1",
67
+ "@rvoh/psychic-spec-helpers": "^1.1.3",
68
68
  "@types/express": "^5.0.1",
69
69
  "@types/express-session": "^1.18.2",
70
70
  "@types/node": "^22.17.1",
@@ -77,7 +77,7 @@
77
77
  "express": "^5.1.0",
78
78
  "express-session": "^1.18.2",
79
79
  "jsdom": "^26.1.0",
80
- "kysely": "^0.27.4",
80
+ "kysely": "^0.28.5",
81
81
  "kysely-codegen": "~0.17.0",
82
82
  "luxon-jest-matchers": "^0.1.14",
83
83
  "nodemon": "^3.1.7",
@@ -97,4 +97,4 @@
97
97
  "winston": "^3.14.2"
98
98
  },
99
99
  "packageManager": "yarn@4.7.0"
100
- }
100
+ }
package/CHANGELOG.md DELETED
@@ -1,320 +0,0 @@
1
- ## 1.10.5
2
-
3
- - add "combining" option to requestBody for OpenAPI decorator, enabling you to combine additional openapi fields to the request body, while still leveraging the powerful automatically-generated request body.
4
- - syncing client enums now sync types along with values
5
- - better dev logging
6
-
7
- ## 1.10.4
8
-
9
- Fix issue with rendering incorrect enum descriptions when suppressResponseEnums is set to true and enums are explicitly overridden in the openapi option.
10
-
11
- ## 1.10.3
12
-
13
- - respect `required: false` when generating OpenAPI spec
14
-
15
- ## 1.10.2
16
-
17
- - return 400 instead of throwing error and 500 when there is a column overflow at the database level (let database validation suffice for enforcing data length validation rather than requiring model level validation)
18
-
19
- ## 1.10.1
20
-
21
- - OpenAPI and castParam validation errors are logged only when `NODE_DEBUG=psychic`
22
-
23
- ## 1.10.0
24
-
25
- - remove OpenAPI and Dream validation error response configuration and do not respond with errors (don't introduce such a difference between development and production environments)
26
- - log validation errors in test and dev (not prod to avoid DOS)
27
- - remove distinction between 400 and 422 to block ability of attacker to get feedback on how far into the system their request made it
28
-
29
- ## 1.9.0
30
-
31
- 1. Validate params against OpenAPI at the latest possible of:
32
- a. when the params are accessed
33
- b. when about to render the action
34
- This ensures that we return the proper 401/403 response instead of 400 for authenticated endpoints that fail authentication and prevents unauthenticated requests from gaining information about the API
35
-
36
- 2. Ability to configure whether or not OpenAPI validation errors include detailed information
37
-
38
- ## 1.8.6
39
-
40
- remove dead env variable, now that we are open sourced
41
-
42
- ## 1.8.5
43
-
44
- Do not hard crash when initializing a psychic application when one of the openapi routes is not found for an openapi-decorated controller endpoint. We will continue to raise this exception when building openapi specs, but not when booting up the psychic application, since one can define routes that are i.e. not available in certain environments, and we don't want this to cause hard crashes when our app boots in those environments.
45
-
46
- ## 1.8.4
47
-
48
- - OpenAPI decorator with default 204 status does not throw an exception when passed a Dream model without a `serializers` getter
49
- - OpenAPI decorator that defines an explicit OpenAPI shape for the default status code does not throw an exception when passed a Dream model without a `serializers` getter
50
-
51
- ## 1.8.3
52
-
53
- - don't build openapi when `bypassModelIntegrityCheck: true`
54
-
55
- ## 1.8.2
56
-
57
- - openapi validation properly coerces non-array query params to arrays when validating, since both express and ajv fail to do this under the hood properly. This solves issues where sending up array params with only a single item in them are not treated as arrays.
58
-
59
- ## 1.8.1
60
-
61
- - do not coerce types in ajv when processing request or response bodies during validation. Type coercion will still happen for headers and query params, since they will need to respect the schema type specified in the openapi docuement.
62
-
63
- ## 1.8.0
64
-
65
- - remove unused `clientRoot` config
66
-
67
- ## 1.7.2
68
-
69
- - generate admin routes in routes.admin.ts (requires `routes.admin.ts` next to `routes.ts`)
70
-
71
- ## 1.7.1
72
-
73
- - compute openapi doc during intiialization, rather than problematically reading from a file cache
74
-
75
- ## 1.7.0
76
-
77
- - `sanitizeResponseJson` config to automatically escape `<`, `>`, `&`, `/`, `\`, `'`, and `"` unicode representations when rendering json to satisfy security reviews (e.g., a pentest report recently called this out on one of our applications). For all practical purposes, this doesn't protect against anything (now that we have the `nosniff` header) since `JSON.parse` on the other end restores the original, dangerous string. Modern front end web frameworks already handle safely displaying arbitrary content, so further sanitization generally isn't needed. This version does provide the `sanitizeString` function that could be used to sanitize individual strings, replacing the above characters with string representations of the unicode characters that will survive Psychic converting to json and then parsing that json (i.e.: `<` will end up as the string "\u003c")
78
-
79
- - Fix openapi serializer fallback issue introduced in 1.6.3, where we mistakenly double render data that has already been serialized.
80
-
81
- ## 1.6.4
82
-
83
- Raise an exception if attempting to import an openapi file during PsychicApp.init when in production. We will still swallow the exception in non-prod environments so that one can create a new openapi configuration and run sync without getting an error.
84
-
85
- ## 1.6.3
86
-
87
- - castParam accepts raw openapi shapes as type arguments, correctly casting the result to an interface representing the provided openapi shape.
88
-
89
- ```ts
90
- class MyController extends ApplicationController {
91
- public index() {
92
- const myParam = this.castParam('myParam', {
93
- type: 'array',
94
- items: {
95
- anyOf: [{ type: 'string' }, { type: 'number' }],
96
- },
97
- })
98
- myParam[0] // string | number
99
- }
100
- }
101
- ```
102
-
103
- - simplify the needlessly-robust new psychic router patterns by making expressApp optional, essentially reverting us back to the same psychic router we had prior to the recent openapi validation changes.
104
-
105
- - fallback to serializer specified in openapi decorator before falling back to dream serializer when rendering dreams
106
-
107
- ## 1.6.2
108
-
109
- fix OpenAPI spec generation by DRYing up generation of request and response body
110
-
111
- ## 1.6.1
112
-
113
- fix issue preventing validation fallbacks from properly overriding on OpenAPI decorator calls when explicitly opting out of validation
114
-
115
- ## 1.6.0
116
-
117
- enables validation to be added to both openapi configurations, as well as to `OpenAPI` decorator calls, enabling the developer to granularly control validation logic for their endpoints.
118
-
119
- To leverage global config:
120
-
121
- ```ts
122
- // conf/app.ts
123
- export default async (psy: PsychicApp) => {
124
- ...
125
-
126
- psy.set('openapi', {
127
- // ...
128
- validate: {
129
- headers: true,
130
- requestBody: true,
131
- query: true,
132
- responseBody: AppEnv.isTest,
133
- },
134
- })
135
- }
136
- ```
137
-
138
- To leverage endpoint config:
139
-
140
- ```ts
141
- // controllers/PetsController
142
- export default class PetsController {
143
- @OpenAPI(Pet, {
144
- ...
145
- validate: {
146
- headers: true,
147
- requestBody: true,
148
- query: true,
149
- responseBody: AppEnv.isTest,
150
- }
151
- })
152
- public async index() {
153
- ...
154
- }
155
- }
156
- ```
157
-
158
- This PR additionally formally introduces a new possible error type for 400 status codes, and to help distinguish, it also introduces a `type` field, which can be either `openapi` or `dream` to aid the developer in easily handling the various cases.
159
-
160
- We have made a conscious decision to render openapi errors in the exact format that ajv returns, since it empowers the developer to utilize tools which can already respond to ajv errors.
161
-
162
- For added flexibility, this PR includes the ability to provide configuration overrides for the ajv instance, as well as the ability to provide an initialization function to override ajv behavior, since much of the configuration for ajv is driven by method calls, rather than simple config.
163
-
164
- ```ts
165
- // controllers/PetsController
166
- export default class PetsController {
167
- @OpenAPI(Pet, {
168
- ...
169
- validate: {
170
- ajvOptions: {
171
- // this is off by default, but you will
172
- // always want to keep this off in prod
173
- // to avoid DoS vulnerabilities
174
- allErrors: AppEnv.isTest,
175
-
176
- // provide a custom init function to further
177
- // configure your ajv instance before validating
178
- init: ajv => {
179
- ajv.addFormat('myFormat', {
180
- type: 'string',
181
- validate: data => MY_FORMAT_REGEX.test(data),
182
- })
183
- }
184
- }
185
- }
186
- })
187
- public async index() {
188
- ...
189
- }
190
- }
191
- ```
192
-
193
- ## 1.5.5
194
-
195
- - ensure that openapi-typescript and typescript are not required dependencies when running migrations with --skip-sync flag
196
-
197
- ## 1.5.4
198
-
199
- - fix issue when providing the `including` argument exclusively to an OpenAPI decorator's `requestBody`
200
-
201
- ## 1.5.3
202
-
203
- - add missing peer dependency for openapi-typescript, allow BIGINT type when generating openapi-typescript bigints
204
-
205
- ## 1.5.2
206
-
207
- - ensure that bigints are converted to number | string when generating openapi-typescript type files
208
-
209
- ## 1.5.1
210
-
211
- - fix issue with enum syncing related to multi-db engine support regression
212
-
213
- ## 1.5.0
214
-
215
- - add support for multiple database engines in dream
216
-
217
- ## 1.2.3
218
-
219
- - add support for the connectionName argument when generating a resource
220
-
221
- ## 1.2.2
222
-
223
- - bump supertest and express-session to close dependabot issues [53](https://github.com/rvohealth/psychic/security/dependabot/53), [56](https://github.com/rvohealth/psychic/security/dependabot/56), and [57](https://github.com/rvohealth/psychic/security/dependabot/57)
224
-
225
- ## 1.2.1
226
-
227
- - add ability to set custom import extension, which will be used when generating new files for your application
228
-
229
- ## 1.2.0
230
-
231
- - update for Dream 1.4.0
232
-
233
- ## 1.1.11
234
-
235
- - 400 is more appropriate than 422 for `DataTypeColumnTypeMismatch`
236
-
237
- ## 1.1.10
238
-
239
- - Don't include deletedAt in generated create/update actions in resource specs since deletedAt is for deleting
240
-
241
- - return 422 if Dream throws `NotNullViolation` or `CheckConstraintViolation`
242
-
243
- ## 1.1.9
244
-
245
- - return 422 if dream throws `DataTypeColumnTypeMismatch`, which happens when a dream is saved to the database with data that cannot be inserted into the respective columns, usually because of a type mismatch.
246
-
247
- - castParam will now encase params in an array when being explicitly casted as an array type, bypassing a known bug in express from causing arrays with single items in them to be treated as non-arrays.
248
-
249
- ## 1.1.8
250
-
251
- - Tap into CliFileWriter provided by dream to tap into file reversion for sync files, since the auto-sync function in psychic can fail and leave your file tree in a bad state.
252
-
253
- ## 1.1.7
254
-
255
- - Add support for middleware arrays, enabling express plugins like passport
256
-
257
- ## 1.1.6
258
-
259
- - fix regression caused by missing --schema-only option in psychic cli
260
-
261
- ## 1.1.5
262
-
263
- - pass packageManager through to dream, now that it accepts a packageManager setting.
264
- - update dream shadowing within psychic application initialization to take place after initializers and plugins are processed, so that those initializers and plugins have an opportunity to adjust the settings.
265
-
266
- ## 1.1.4
267
-
268
- - fix regressions to redux bindings caused by default openapi path location changes
269
- - resource generator can handle prefixing slashes
270
-
271
- ## 1.1.3
272
-
273
- - fix more minor issues with redux openapi bindings
274
-
275
- ## 1.1.2
276
-
277
- - Fix various issues with openapi redux bindings
278
- - raise hard exception if accidentally using openapi route params in an expressjs route path
279
-
280
- ## 1.1.1
281
-
282
- Fix route printing regression causing route printouts to show the path instead of the action
283
-
284
- ## v1.1.0
285
-
286
- Provides easier access to express middleware by exposing `PsychicApp#use`, which enables a developer to provide express middleware directly through the psychcic application, without tapping into any hooks.
287
-
288
- ```ts
289
- psy.use((_, res) => {
290
- res.send(
291
- 'this will be run after psychic middleware (i.e. cors and bodyParser) are processed, but before routes are processed',
292
- )
293
- })
294
- ```
295
-
296
- Some middleware needs to be run before other middleware, so we expose an optional first argument which can be provided so explicitly send your middleware into express at various stages of the psychic configuration process. For example, to inject your middleware before cors and bodyParser are configured, provide `before-middleware` as the first argument. To initialize your middleware after the psychic default middleware, but before your routes have been processed, provide `after-middleware` as the first argument (or simply provide a callback function directly, since this is the default). To run after routes have been processed, provide `after-routes` as the first argument.
297
-
298
- ```ts
299
- psy.use('before-middleware', (_, res) => {
300
- res.send('this will be run before psychic has configured any default middleware')
301
- })
302
-
303
- psy.use('after-middleware', (_, res) => {
304
- res.send('this will be run after psychic has configured default middleware')
305
- })
306
-
307
- psy.use('after-routes', (_, res) => {
308
- res.send('this will be run after psychic has processed all the routes in your conf/routes.ts file')
309
- })
310
- ```
311
-
312
- Additionally, a new overload has been added to all CRUD methods on the PsychicRouter class, enabling you to provide RequestHandler middleware directly to psychic, like so:
313
-
314
- ```ts
315
- // conf/routes.ts
316
-
317
- r.get('helloworld', (req, res, next) => {
318
- res.json({ hello: 'world' })
319
- })
320
- ```