@rvoh/psychic 2.2.0 → 2.2.1-alpha.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.
@@ -14,6 +14,10 @@ ${INDENT}all properties default to not nullable; null can be allowed by appendin
14
14
  ${INDENT} subtitle:string:optional
15
15
  ${INDENT}
16
16
  ${INDENT}supported types:
17
+ ${INDENT} - uuid:
18
+ ${INDENT} - uuid[]:
19
+ ${INDENT} a column optimized for storing UUIDs
20
+ ${INDENT}
17
21
  ${INDENT} - citext:
18
22
  ${INDENT} - citext[]:
19
23
  ${INDENT} case insensitive text (indexes and queries are automatically case insensitive)
@@ -7,7 +7,7 @@ export default function generateResourceControllerSpecContent(options) {
7
7
  const modelConfig = createModelConfiguration(options);
8
8
  const actionConfig = createActionConfiguration(options);
9
9
  const attributeData = processAttributes(options.columnsWithTypes, modelConfig, options.fullyQualifiedModelName);
10
- const imports = generateImportStatements(modelConfig, attributeData.dreamImports, options.owningModel);
10
+ const imports = generateImportStatements(modelConfig, attributeData.dreamImports, options.owningModel, attributeData.uuidAttributeIncluded);
11
11
  return generateSpecTemplate({
12
12
  ...options,
13
13
  path,
@@ -62,6 +62,8 @@ function processAttributes(columnsWithTypes, modelConfig, fullyQualifiedModelNam
62
62
  originalValueVariableAssignments: [],
63
63
  dateAttributeIncluded: false,
64
64
  datetimeAttributeIncluded: false,
65
+ uuidAttributeIncluded: false,
66
+ uuidArrayAttributes: [],
65
67
  dreamImports: [],
66
68
  };
67
69
  for (const attribute of columnsWithTypes) {
@@ -102,7 +104,8 @@ function parseAttribute(attribute) {
102
104
  return null;
103
105
  const arrayBracketRegexp = /\[\]$/;
104
106
  const isArray = arrayBracketRegexp.test(rawAttributeType);
105
- const attributeType = rawAttributeType.replace(arrayBracketRegexp, '');
107
+ const _attributeType = rawAttributeType.replace(arrayBracketRegexp, '');
108
+ const attributeType = /uuid$/.test(rawAttributeName) ? 'uuid' : _attributeType;
106
109
  return { attributeName, attributeType, isArray, enumValues };
107
110
  }
108
111
  function processAttributeByType({ attributeType, attributeName, isArray, enumValues, dotNotationVariable, fullyQualifiedModelName, attributeData, }) {
@@ -137,6 +140,9 @@ function processAttributeByType({ attributeType, attributeName, isArray, enumVal
137
140
  case 'datetime':
138
141
  processDateTimeAttribute({ attributeName, isArray, dotNotationVariable, attributeData });
139
142
  break;
143
+ case 'uuid':
144
+ processUuidAttribute({ attributeName, isArray, dotNotationVariable, attributeData });
145
+ break;
140
146
  }
141
147
  }
142
148
  function processEnumAttribute({ attributeName, isArray, enumValues, dotNotationVariable, attributeData, }) {
@@ -192,6 +198,21 @@ function processDateTimeAttribute({ attributeName, isArray, dotNotationVariable,
192
198
  attributeData.expectEqualOriginalValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualDateTime(now)`);
193
199
  attributeData.expectEqualUpdatedValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualDateTime(lastHour)`);
194
200
  }
201
+ function processUuidAttribute({ attributeName, isArray, dotNotationVariable, attributeData, }) {
202
+ attributeData.uuidAttributeIncluded = true;
203
+ if (isArray) {
204
+ attributeData.uuidArrayAttributes.push(attributeName);
205
+ }
206
+ const newUuidVariableName = `new${capitalize(attributeName)}`;
207
+ // For arrays, the variable itself is an array, so we use it directly without brackets
208
+ const uuidValue = attributeName;
209
+ const newUuidValue = newUuidVariableName;
210
+ attributeData.attributeCreationKeyValues.push(`${attributeName}: ${uuidValue},`);
211
+ attributeData.attributeUpdateKeyValues.push(`${attributeName}: ${newUuidValue},`);
212
+ attributeData.comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
213
+ attributeData.expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(${attributeName})`);
214
+ attributeData.expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(${newUuidVariableName})`);
215
+ }
195
216
  function addOriginalValueTracking(attributeType, attributeName, isArray, dotNotationVariable, attributeData) {
196
217
  const hardToCompareArray = (attributeType === 'date' || attributeType === 'datetime') && isArray;
197
218
  // Exclude belongs_to relationships from original value tracking
@@ -201,7 +222,7 @@ function addOriginalValueTracking(attributeType, attributeName, isArray, dotNota
201
222
  attributeData.expectEqualOriginalNamedVariable.push(`expect(${dotNotationVariable}).toEqual(${originalAttributeVariableName})`);
202
223
  }
203
224
  }
204
- function generateImportStatements(modelConfig, dreamImports, owningModel) {
225
+ function generateImportStatements(modelConfig, dreamImports, owningModel, uuidAttributeIncluded) {
205
226
  const importStatements = compact([
206
227
  importStatementForModel(modelConfig.fullyQualifiedModelName),
207
228
  importStatementForModel(modelConfig.userModelName),
@@ -210,10 +231,11 @@ function generateImportStatements(modelConfig, dreamImports, owningModel) {
210
231
  importStatementForModelFactory(modelConfig.userModelName),
211
232
  owningModel ? importStatementForModelFactory(owningModel) : undefined,
212
233
  ]);
234
+ const cryptoImportLine = uuidAttributeIncluded ? `import { randomUUID } from 'node:crypto'\n` : '';
213
235
  const dreamImportLine = dreamImports.length
214
236
  ? `import { ${uniq(dreamImports).join(', ')} } from '@rvoh/dream'\n`
215
237
  : '';
216
- return `${dreamImportLine}${uniq(importStatements).join('\n')}
238
+ return `${cryptoImportLine}${dreamImportLine}${uniq(importStatements).join('\n')}
217
239
  import { RequestBody, session, SpecRequestType } from '@spec/unit/helpers/${addImportSuffix('authentication.js')}'`;
218
240
  }
219
241
  function generateSpecTemplate(options) {
@@ -314,13 +336,28 @@ function generateCreateActionSpec(options) {
314
336
  return '';
315
337
  const { path, pathParams, modelConfig, fullyQualifiedModelName, forAdmin, singular, attributeData } = options;
316
338
  const subjectFunctionName = `create${modelConfig.modelClassName}`;
317
- const dateTimeSetup = `${attributeData.dateAttributeIncluded
339
+ const uuidAttributeNames = attributeData.attributeCreationKeyValues
340
+ .map(kv => {
341
+ const match = kv.match(/^(\w+):\s*\1,?$/);
342
+ return match?.[1];
343
+ })
344
+ .filter((name) => Boolean(name));
345
+ const uuidSetup = uuidAttributeNames
346
+ .map(attrName => {
347
+ const isArray = attributeData.uuidArrayAttributes.includes(attrName);
348
+ return isArray ? `const ${attrName} = [randomUUID()]` : `const ${attrName} = randomUUID()`;
349
+ })
350
+ .join('\n ');
351
+ const dateTimeSetup = `${uuidSetup
352
+ ? `
353
+ ${uuidSetup}`
354
+ : ''}${attributeData.dateAttributeIncluded
318
355
  ? `
319
356
  const today = CalendarDate.today()`
320
357
  : ''}${attributeData.datetimeAttributeIncluded
321
358
  ? `
322
359
  const now = DateTime.now()`
323
- : ''}${attributeData.dateAttributeIncluded || attributeData.datetimeAttributeIncluded ? '\n' : ''}`;
360
+ : ''}${uuidSetup || attributeData.dateAttributeIncluded || attributeData.datetimeAttributeIncluded ? '\n' : ''}`;
324
361
  const modelQuery = forAdmin
325
362
  ? `${modelConfig.modelClassName}.firstOrFail()`
326
363
  : `${modelConfig.owningModelVariableName}.associationQuery('${singular ? modelConfig.modelVariableName : pluralize(modelConfig.modelVariableName)}').firstOrFail()`;
@@ -356,7 +393,28 @@ function generateUpdateActionSpec(options) {
356
393
  return '';
357
394
  const { path, pathParams, modelConfig, fullyQualifiedModelName, singular, forAdmin, attributeData } = options;
358
395
  const subjectFunctionName = `update${modelConfig.modelClassName}`;
359
- const dateTimeSetup = `${attributeData.dateAttributeIncluded
396
+ const uuidAttributeNames = attributeData.attributeUpdateKeyValues
397
+ .filter(kv => {
398
+ const match = kv.match(/^(\w+):\s*new([A-Z]\w+),?$/);
399
+ return match && match[1] && camelize(match[1]) === camelize(match[2]);
400
+ })
401
+ .map(kv => {
402
+ const match = kv.match(/^(\w+):/);
403
+ return match?.[1];
404
+ })
405
+ .filter(Boolean);
406
+ const uuidSetup = uuidAttributeNames
407
+ .map(attrName => {
408
+ const isArray = attributeData.uuidArrayAttributes.includes(attrName);
409
+ return isArray
410
+ ? `const new${capitalize(attrName)} = [randomUUID()]`
411
+ : `const new${capitalize(attrName)} = randomUUID()`;
412
+ })
413
+ .join('\n ');
414
+ const dateTimeSetup = `${uuidSetup
415
+ ? `
416
+ ${uuidSetup}`
417
+ : ''}${attributeData.dateAttributeIncluded
360
418
  ? `
361
419
  const yesterday = CalendarDate.yesterday()`
362
420
  : ''}${attributeData.datetimeAttributeIncluded
@@ -400,7 +458,21 @@ function generateUpdateActionSpec(options) {
400
458
  ${attributeData.originalValueVariableAssignments.length ? attributeData.originalValueVariableAssignments.join('\n ') : ''}
401
459
 
402
460
  await ${subjectFunctionName}(${modelConfig.modelVariableName}, {
403
- ${attributeData.attributeUpdateKeyValues.length ? attributeData.attributeUpdateKeyValues.join('\n ') : ''}
461
+ ${attributeData.attributeUpdateKeyValues.length
462
+ ? attributeData.attributeUpdateKeyValues
463
+ .map(kv => {
464
+ const match = kv.match(/^(\w+):\s*new([A-Z]\w+),?$/);
465
+ if (match && match[1]) {
466
+ const attrName = match[1];
467
+ const isArray = attributeData.uuidArrayAttributes.includes(attrName);
468
+ return isArray
469
+ ? kv.replace(/\bnew[A-Z]\w+\b/g, '[randomUUID()]')
470
+ : kv.replace(/\bnew[A-Z]\w+\b/g, 'randomUUID()');
471
+ }
472
+ return kv;
473
+ })
474
+ .join('\n ')
475
+ : ''}
404
476
  }, 404)
405
477
 
406
478
  await ${modelConfig.modelVariableName}.reload()
@@ -14,6 +14,10 @@ ${INDENT}all properties default to not nullable; null can be allowed by appendin
14
14
  ${INDENT} subtitle:string:optional
15
15
  ${INDENT}
16
16
  ${INDENT}supported types:
17
+ ${INDENT} - uuid:
18
+ ${INDENT} - uuid[]:
19
+ ${INDENT} a column optimized for storing UUIDs
20
+ ${INDENT}
17
21
  ${INDENT} - citext:
18
22
  ${INDENT} - citext[]:
19
23
  ${INDENT} case insensitive text (indexes and queries are automatically case insensitive)
@@ -7,7 +7,7 @@ export default function generateResourceControllerSpecContent(options) {
7
7
  const modelConfig = createModelConfiguration(options);
8
8
  const actionConfig = createActionConfiguration(options);
9
9
  const attributeData = processAttributes(options.columnsWithTypes, modelConfig, options.fullyQualifiedModelName);
10
- const imports = generateImportStatements(modelConfig, attributeData.dreamImports, options.owningModel);
10
+ const imports = generateImportStatements(modelConfig, attributeData.dreamImports, options.owningModel, attributeData.uuidAttributeIncluded);
11
11
  return generateSpecTemplate({
12
12
  ...options,
13
13
  path,
@@ -62,6 +62,8 @@ function processAttributes(columnsWithTypes, modelConfig, fullyQualifiedModelNam
62
62
  originalValueVariableAssignments: [],
63
63
  dateAttributeIncluded: false,
64
64
  datetimeAttributeIncluded: false,
65
+ uuidAttributeIncluded: false,
66
+ uuidArrayAttributes: [],
65
67
  dreamImports: [],
66
68
  };
67
69
  for (const attribute of columnsWithTypes) {
@@ -102,7 +104,8 @@ function parseAttribute(attribute) {
102
104
  return null;
103
105
  const arrayBracketRegexp = /\[\]$/;
104
106
  const isArray = arrayBracketRegexp.test(rawAttributeType);
105
- const attributeType = rawAttributeType.replace(arrayBracketRegexp, '');
107
+ const _attributeType = rawAttributeType.replace(arrayBracketRegexp, '');
108
+ const attributeType = /uuid$/.test(rawAttributeName) ? 'uuid' : _attributeType;
106
109
  return { attributeName, attributeType, isArray, enumValues };
107
110
  }
108
111
  function processAttributeByType({ attributeType, attributeName, isArray, enumValues, dotNotationVariable, fullyQualifiedModelName, attributeData, }) {
@@ -137,6 +140,9 @@ function processAttributeByType({ attributeType, attributeName, isArray, enumVal
137
140
  case 'datetime':
138
141
  processDateTimeAttribute({ attributeName, isArray, dotNotationVariable, attributeData });
139
142
  break;
143
+ case 'uuid':
144
+ processUuidAttribute({ attributeName, isArray, dotNotationVariable, attributeData });
145
+ break;
140
146
  }
141
147
  }
142
148
  function processEnumAttribute({ attributeName, isArray, enumValues, dotNotationVariable, attributeData, }) {
@@ -192,6 +198,21 @@ function processDateTimeAttribute({ attributeName, isArray, dotNotationVariable,
192
198
  attributeData.expectEqualOriginalValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualDateTime(now)`);
193
199
  attributeData.expectEqualUpdatedValue.push(`expect(${dotNotationVariable}${isArray ? '[0]' : ''}).toEqualDateTime(lastHour)`);
194
200
  }
201
+ function processUuidAttribute({ attributeName, isArray, dotNotationVariable, attributeData, }) {
202
+ attributeData.uuidAttributeIncluded = true;
203
+ if (isArray) {
204
+ attributeData.uuidArrayAttributes.push(attributeName);
205
+ }
206
+ const newUuidVariableName = `new${capitalize(attributeName)}`;
207
+ // For arrays, the variable itself is an array, so we use it directly without brackets
208
+ const uuidValue = attributeName;
209
+ const newUuidValue = newUuidVariableName;
210
+ attributeData.attributeCreationKeyValues.push(`${attributeName}: ${uuidValue},`);
211
+ attributeData.attributeUpdateKeyValues.push(`${attributeName}: ${newUuidValue},`);
212
+ attributeData.comparableOriginalAttributeKeyValues.push(`${attributeName}: ${dotNotationVariable},`);
213
+ attributeData.expectEqualOriginalValue.push(`expect(${dotNotationVariable}).toEqual(${attributeName})`);
214
+ attributeData.expectEqualUpdatedValue.push(`expect(${dotNotationVariable}).toEqual(${newUuidVariableName})`);
215
+ }
195
216
  function addOriginalValueTracking(attributeType, attributeName, isArray, dotNotationVariable, attributeData) {
196
217
  const hardToCompareArray = (attributeType === 'date' || attributeType === 'datetime') && isArray;
197
218
  // Exclude belongs_to relationships from original value tracking
@@ -201,7 +222,7 @@ function addOriginalValueTracking(attributeType, attributeName, isArray, dotNota
201
222
  attributeData.expectEqualOriginalNamedVariable.push(`expect(${dotNotationVariable}).toEqual(${originalAttributeVariableName})`);
202
223
  }
203
224
  }
204
- function generateImportStatements(modelConfig, dreamImports, owningModel) {
225
+ function generateImportStatements(modelConfig, dreamImports, owningModel, uuidAttributeIncluded) {
205
226
  const importStatements = compact([
206
227
  importStatementForModel(modelConfig.fullyQualifiedModelName),
207
228
  importStatementForModel(modelConfig.userModelName),
@@ -210,10 +231,11 @@ function generateImportStatements(modelConfig, dreamImports, owningModel) {
210
231
  importStatementForModelFactory(modelConfig.userModelName),
211
232
  owningModel ? importStatementForModelFactory(owningModel) : undefined,
212
233
  ]);
234
+ const cryptoImportLine = uuidAttributeIncluded ? `import { randomUUID } from 'node:crypto'\n` : '';
213
235
  const dreamImportLine = dreamImports.length
214
236
  ? `import { ${uniq(dreamImports).join(', ')} } from '@rvoh/dream'\n`
215
237
  : '';
216
- return `${dreamImportLine}${uniq(importStatements).join('\n')}
238
+ return `${cryptoImportLine}${dreamImportLine}${uniq(importStatements).join('\n')}
217
239
  import { RequestBody, session, SpecRequestType } from '@spec/unit/helpers/${addImportSuffix('authentication.js')}'`;
218
240
  }
219
241
  function generateSpecTemplate(options) {
@@ -314,13 +336,28 @@ function generateCreateActionSpec(options) {
314
336
  return '';
315
337
  const { path, pathParams, modelConfig, fullyQualifiedModelName, forAdmin, singular, attributeData } = options;
316
338
  const subjectFunctionName = `create${modelConfig.modelClassName}`;
317
- const dateTimeSetup = `${attributeData.dateAttributeIncluded
339
+ const uuidAttributeNames = attributeData.attributeCreationKeyValues
340
+ .map(kv => {
341
+ const match = kv.match(/^(\w+):\s*\1,?$/);
342
+ return match?.[1];
343
+ })
344
+ .filter((name) => Boolean(name));
345
+ const uuidSetup = uuidAttributeNames
346
+ .map(attrName => {
347
+ const isArray = attributeData.uuidArrayAttributes.includes(attrName);
348
+ return isArray ? `const ${attrName} = [randomUUID()]` : `const ${attrName} = randomUUID()`;
349
+ })
350
+ .join('\n ');
351
+ const dateTimeSetup = `${uuidSetup
352
+ ? `
353
+ ${uuidSetup}`
354
+ : ''}${attributeData.dateAttributeIncluded
318
355
  ? `
319
356
  const today = CalendarDate.today()`
320
357
  : ''}${attributeData.datetimeAttributeIncluded
321
358
  ? `
322
359
  const now = DateTime.now()`
323
- : ''}${attributeData.dateAttributeIncluded || attributeData.datetimeAttributeIncluded ? '\n' : ''}`;
360
+ : ''}${uuidSetup || attributeData.dateAttributeIncluded || attributeData.datetimeAttributeIncluded ? '\n' : ''}`;
324
361
  const modelQuery = forAdmin
325
362
  ? `${modelConfig.modelClassName}.firstOrFail()`
326
363
  : `${modelConfig.owningModelVariableName}.associationQuery('${singular ? modelConfig.modelVariableName : pluralize(modelConfig.modelVariableName)}').firstOrFail()`;
@@ -356,7 +393,28 @@ function generateUpdateActionSpec(options) {
356
393
  return '';
357
394
  const { path, pathParams, modelConfig, fullyQualifiedModelName, singular, forAdmin, attributeData } = options;
358
395
  const subjectFunctionName = `update${modelConfig.modelClassName}`;
359
- const dateTimeSetup = `${attributeData.dateAttributeIncluded
396
+ const uuidAttributeNames = attributeData.attributeUpdateKeyValues
397
+ .filter(kv => {
398
+ const match = kv.match(/^(\w+):\s*new([A-Z]\w+),?$/);
399
+ return match && match[1] && camelize(match[1]) === camelize(match[2]);
400
+ })
401
+ .map(kv => {
402
+ const match = kv.match(/^(\w+):/);
403
+ return match?.[1];
404
+ })
405
+ .filter(Boolean);
406
+ const uuidSetup = uuidAttributeNames
407
+ .map(attrName => {
408
+ const isArray = attributeData.uuidArrayAttributes.includes(attrName);
409
+ return isArray
410
+ ? `const new${capitalize(attrName)} = [randomUUID()]`
411
+ : `const new${capitalize(attrName)} = randomUUID()`;
412
+ })
413
+ .join('\n ');
414
+ const dateTimeSetup = `${uuidSetup
415
+ ? `
416
+ ${uuidSetup}`
417
+ : ''}${attributeData.dateAttributeIncluded
360
418
  ? `
361
419
  const yesterday = CalendarDate.yesterday()`
362
420
  : ''}${attributeData.datetimeAttributeIncluded
@@ -400,7 +458,21 @@ function generateUpdateActionSpec(options) {
400
458
  ${attributeData.originalValueVariableAssignments.length ? attributeData.originalValueVariableAssignments.join('\n ') : ''}
401
459
 
402
460
  await ${subjectFunctionName}(${modelConfig.modelVariableName}, {
403
- ${attributeData.attributeUpdateKeyValues.length ? attributeData.attributeUpdateKeyValues.join('\n ') : ''}
461
+ ${attributeData.attributeUpdateKeyValues.length
462
+ ? attributeData.attributeUpdateKeyValues
463
+ .map(kv => {
464
+ const match = kv.match(/^(\w+):\s*new([A-Z]\w+),?$/);
465
+ if (match && match[1]) {
466
+ const attrName = match[1];
467
+ const isArray = attributeData.uuidArrayAttributes.includes(attrName);
468
+ return isArray
469
+ ? kv.replace(/\bnew[A-Z]\w+\b/g, '[randomUUID()]')
470
+ : kv.replace(/\bnew[A-Z]\w+\b/g, 'randomUUID()');
471
+ }
472
+ return kv;
473
+ })
474
+ .join('\n ')
475
+ : ''}
404
476
  }, 404)
405
477
 
406
478
  await ${modelConfig.modelVariableName}.reload()
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": "2.2.0",
5
+ "version": "2.2.1-alpha.1",
6
6
  "author": "RVOHealth",
7
7
  "repository": {
8
8
  "type": "git",