@rvoh/dream 2.10.0 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/cjs/src/cli/index.js +16 -3
  2. package/dist/cjs/src/encrypt/index.js +5 -5
  3. package/dist/cjs/src/errors/encrypt/{DecryptionWithRotationError.js → DecryptionRotationError.js} +1 -1
  4. package/dist/cjs/src/helpers/cli/generateDreamContent.js +43 -17
  5. package/dist/cjs/src/helpers/cli/generateFactoryContent.js +34 -7
  6. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +30 -4
  7. package/dist/cjs/src/helpers/cli/parseAttribute.js +61 -0
  8. package/dist/cjs/src/package-exports/errors.js +3 -0
  9. package/dist/esm/src/cli/index.js +16 -3
  10. package/dist/esm/src/encrypt/index.js +5 -5
  11. package/dist/esm/src/errors/encrypt/{DecryptionWithRotationError.js → DecryptionRotationError.js} +1 -1
  12. package/dist/esm/src/helpers/cli/generateDreamContent.js +43 -17
  13. package/dist/esm/src/helpers/cli/generateFactoryContent.js +34 -7
  14. package/dist/esm/src/helpers/cli/generateMigrationContent.js +30 -4
  15. package/dist/esm/src/helpers/cli/parseAttribute.js +61 -0
  16. package/dist/esm/src/package-exports/errors.js +3 -0
  17. package/dist/types/src/cli/index.d.ts +1 -1
  18. package/dist/types/src/encrypt/index.d.ts +3 -3
  19. package/dist/types/src/errors/encrypt/{DecryptionWithRotationError.d.ts → DecryptionRotationError.d.ts} +1 -1
  20. package/dist/types/src/helpers/cli/generateDreamContent.d.ts +6 -2
  21. package/dist/types/src/helpers/cli/parseAttribute.d.ts +64 -0
  22. package/dist/types/src/package-exports/errors.d.ts +3 -0
  23. package/docs/assets/navigation.js +1 -1
  24. package/docs/assets/search.js +1 -1
  25. package/docs/classes/db.DreamMigrationHelpers.html +9 -9
  26. package/docs/classes/db.KyselyQueryDriver.html +32 -32
  27. package/docs/classes/db.PostgresQueryDriver.html +33 -33
  28. package/docs/classes/db.QueryDriverBase.html +31 -31
  29. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  30. package/docs/classes/errors.ColumnOverflow.html +3 -3
  31. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  32. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  33. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  34. package/docs/classes/errors.DecryptionError.html +33 -0
  35. package/docs/classes/errors.DecryptionParseError.html +33 -0
  36. package/docs/classes/errors.DecryptionRotationError.html +35 -0
  37. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  38. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  39. package/docs/classes/errors.InvalidClockTime.html +2 -2
  40. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  41. package/docs/classes/errors.InvalidDateTime.html +2 -2
  42. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  43. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  44. package/docs/classes/errors.NotNullViolation.html +3 -3
  45. package/docs/classes/errors.RecordNotFound.html +3 -3
  46. package/docs/classes/errors.ValidationError.html +3 -3
  47. package/docs/classes/index.CalendarDate.html +33 -33
  48. package/docs/classes/index.ClockTime.html +32 -32
  49. package/docs/classes/index.ClockTimeTz.html +35 -35
  50. package/docs/classes/index.DateTime.html +86 -86
  51. package/docs/classes/index.Decorators.html +19 -19
  52. package/docs/classes/index.Dream.html +118 -118
  53. package/docs/classes/index.DreamApp.html +5 -5
  54. package/docs/classes/index.DreamTransaction.html +2 -2
  55. package/docs/classes/index.Env.html +2 -2
  56. package/docs/classes/index.Query.html +56 -56
  57. package/docs/classes/system.CliFileWriter.html +4 -4
  58. package/docs/classes/system.DreamBin.html +2 -2
  59. package/docs/classes/system.DreamCLI.html +7 -7
  60. package/docs/classes/system.DreamImporter.html +2 -2
  61. package/docs/classes/system.DreamLogos.html +2 -2
  62. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  63. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  64. package/docs/classes/system.PathHelpers.html +3 -3
  65. package/docs/classes/utils.Encrypt.html +6 -6
  66. package/docs/classes/utils.Range.html +2 -2
  67. package/docs/functions/db.closeAllDbConnections.html +1 -1
  68. package/docs/functions/db.dreamDbConnections.html +1 -1
  69. package/docs/functions/db.untypedDb.html +1 -1
  70. package/docs/functions/db.validateColumn.html +1 -1
  71. package/docs/functions/db.validateTable.html +1 -1
  72. package/docs/functions/errors.pgErrorType.html +1 -1
  73. package/docs/functions/index.DreamSerializer.html +1 -1
  74. package/docs/functions/index.ObjectSerializer.html +1 -1
  75. package/docs/functions/index.ReplicaSafe.html +1 -1
  76. package/docs/functions/index.STI.html +1 -1
  77. package/docs/functions/index.SoftDelete.html +1 -1
  78. package/docs/functions/utils.camelize.html +1 -1
  79. package/docs/functions/utils.capitalize.html +1 -1
  80. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  81. package/docs/functions/utils.compact.html +1 -1
  82. package/docs/functions/utils.groupBy.html +1 -1
  83. package/docs/functions/utils.hyphenize.html +1 -1
  84. package/docs/functions/utils.intersection.html +1 -1
  85. package/docs/functions/utils.isEmpty.html +1 -1
  86. package/docs/functions/utils.normalizeUnicode.html +1 -1
  87. package/docs/functions/utils.pascalize.html +1 -1
  88. package/docs/functions/utils.percent.html +1 -1
  89. package/docs/functions/utils.range.html +1 -1
  90. package/docs/functions/utils.round.html +1 -1
  91. package/docs/functions/utils.sanitizeString.html +1 -1
  92. package/docs/functions/utils.snakeify.html +1 -1
  93. package/docs/functions/utils.sort.html +1 -1
  94. package/docs/functions/utils.sortBy.html +1 -1
  95. package/docs/functions/utils.sortObjectByKey.html +1 -1
  96. package/docs/functions/utils.sortObjectByValue.html +1 -1
  97. package/docs/functions/utils.uncapitalize.html +1 -1
  98. package/docs/functions/utils.uniq.html +1 -1
  99. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  100. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  101. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  102. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  103. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  104. package/docs/interfaces/types.DecoratorContext.html +2 -2
  105. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  106. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  107. package/docs/interfaces/types.DurationObject.html +2 -2
  108. package/docs/interfaces/types.EncryptOptions.html +2 -2
  109. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  110. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  111. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  112. package/docs/modules/errors.html +1 -1
  113. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  114. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  115. package/docs/types/openapi.OpenapiFormats.html +1 -1
  116. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  117. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  118. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  119. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  120. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  121. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  122. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  123. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  124. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  125. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  126. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  127. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  128. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  129. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  130. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  131. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  132. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  133. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  134. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  135. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  136. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  137. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  138. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  139. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  140. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  141. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  142. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  143. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  144. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  145. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  146. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  147. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  148. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  149. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  150. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  151. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  152. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  153. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  154. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  155. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  156. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  157. package/docs/types/types.CalendarDateObject.html +1 -1
  158. package/docs/types/types.Camelized.html +1 -1
  159. package/docs/types/types.ClockTimeObject.html +1 -1
  160. package/docs/types/types.DbConnectionType.html +1 -1
  161. package/docs/types/types.DbTypes.html +1 -1
  162. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  163. package/docs/types/types.DreamAttributes.html +1 -1
  164. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  165. package/docs/types/types.DreamClassColumn.html +1 -1
  166. package/docs/types/types.DreamColumn.html +1 -1
  167. package/docs/types/types.DreamColumnNames.html +1 -1
  168. package/docs/types/types.DreamLogLevel.html +1 -1
  169. package/docs/types/types.DreamLogger.html +2 -2
  170. package/docs/types/types.DreamModelSerializerType.html +1 -1
  171. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  172. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  173. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  174. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  175. package/docs/types/types.DreamSerializable.html +1 -1
  176. package/docs/types/types.DreamSerializableArray.html +1 -1
  177. package/docs/types/types.DreamSerializerKey.html +1 -1
  178. package/docs/types/types.DreamSerializers.html +1 -1
  179. package/docs/types/types.DreamVirtualColumns.html +1 -1
  180. package/docs/types/types.DurationUnit.html +1 -1
  181. package/docs/types/types.EncryptAlgorithm.html +1 -1
  182. package/docs/types/types.HasManyStatement.html +1 -1
  183. package/docs/types/types.HasOneStatement.html +1 -1
  184. package/docs/types/types.Hyphenized.html +1 -1
  185. package/docs/types/types.Pascalized.html +1 -1
  186. package/docs/types/types.PrimaryKeyType.html +1 -1
  187. package/docs/types/types.RoundingPrecision.html +1 -1
  188. package/docs/types/types.SerializerCasing.html +1 -1
  189. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  190. package/docs/types/types.Snakeified.html +1 -1
  191. package/docs/types/types.StrictInterface.html +1 -1
  192. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  193. package/docs/types/types.UpdateableProperties.html +1 -1
  194. package/docs/types/types.ValidationType.html +1 -1
  195. package/docs/types/types.ViewModel.html +2 -2
  196. package/docs/types/types.ViewModelClass.html +1 -1
  197. package/docs/types/types.WeekdayName.html +1 -1
  198. package/docs/types/types.WhereStatementForDream.html +1 -1
  199. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  200. package/docs/variables/index.DreamConst.html +1 -1
  201. package/docs/variables/index.ops.html +1 -1
  202. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  203. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  204. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  205. package/docs/variables/system.primaryKeyTypes.html +1 -1
  206. package/package.json +1 -1
@@ -11,7 +11,7 @@ import DreamCliLogger from './logger/DreamCliLogger.js';
11
11
  import colorize from './logger/loggable/colorize.js';
12
12
  export const CLI_INDENT = ' ';
13
13
  const INDENT = CLI_INDENT;
14
- export const baseColumnsWithTypesDescription = `space separated snake-case (except for belongs_to model name) properties like this:
14
+ export const baseColumnsWithTypesDescription = `space separated snake-case (except for belongs_to model name, which may take an @alias suffix to rename the FK) properties like this:
15
15
  ${INDENT} title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal User:belongs_to
16
16
  ${INDENT}
17
17
  ${INDENT}all properties default to not nullable; null can be allowed by appending ':optional':
@@ -71,7 +71,16 @@ ${INDENT}
71
71
  ${INDENT} use the fully qualified model name (matching its path under src/app/models/):
72
72
  ${INDENT} User:belongs_to # creates user_id column + BelongsTo association
73
73
  ${INDENT} Health/Coach:belongs_to # creates health_coach_id column + BelongsTo association
74
- ${INDENT} User:belongs_to:optional # nullable foreign key (for optional associations)`;
74
+ ${INDENT} User:belongs_to:optional # nullable foreign key (for optional associations)
75
+ ${INDENT}
76
+ ${INDENT} rename the association with Model@alias — the snake_case alias drives the FK column name AND the
77
+ ${INDENT} @deco.BelongsTo association + typed FK property on the generated model:
78
+ ${INDENT} InternalUser@canceled_by:belongs_to:optional # canceled_by_id column, canceledById property, canceledBy association,
79
+ ${INDENT} # @deco.BelongsTo('InternalUser', { on: 'canceledById', optional: true })
80
+ ${INDENT} Messaging/Template@template:belongs_to # template_id column, templateId property, template association
81
+ ${INDENT} # (strips the namespace from the property/association names while keeping
82
+ ${INDENT} # the namespaced model reference intact)
83
+ ${INDENT} Aliasing also lets you declare multiple FKs to the same model in one generator call without column collisions.`;
75
84
  const columnsWithTypesDescriptionForMigration = baseColumnsWithTypesDescription +
76
85
  `
77
86
  ${INDENT}
@@ -82,7 +91,11 @@ ${INDENT}
82
91
  ${INDENT} use the fully qualified model name (matching its path under src/app/models/):
83
92
  ${INDENT} User:belongs_to # creates user_id column with index
84
93
  ${INDENT} Health/Coach:belongs_to # creates health_coach_id column with index
85
- ${INDENT} User:belongs_to:optional # nullable foreign key`;
94
+ ${INDENT} User:belongs_to:optional # nullable foreign key
95
+ ${INDENT}
96
+ ${INDENT} rename the FK column with Model@alias (snake_case alias becomes the column name):
97
+ ${INDENT} InternalUser@canceled_by:belongs_to:optional # canceled_by_id column with index
98
+ ${INDENT} Messaging/Template@template:belongs_to # template_id column (strips the namespace from the column name)`;
86
99
  export default class DreamCLI {
87
100
  /**
88
101
  * Starts the Dream console
@@ -1,5 +1,5 @@
1
1
  import DecryptionError from '../errors/encrypt/DecryptionError.js';
2
- import DecryptionWithRotationError from '../errors/encrypt/DecryptionWithRotationError.js';
2
+ import DecryptionRotationError from '../errors/encrypt/DecryptionRotationError.js';
3
3
  import MissingEncryptionKey from '../errors/encrypt/MissingEncryptionKey.js';
4
4
  import decryptAESGCM from './algorithms/aes-gcm/decryptAESGCM.js';
5
5
  import encryptAESGCM from './algorithms/aes-gcm/encryptAESGCM.js';
@@ -34,7 +34,7 @@ export default class Encrypt {
34
34
  *
35
35
  * **Three-arg form** (rotation): tries the current key first; on
36
36
  * `DecryptionError` falls back to the legacy key. If both fail, throws
37
- * `DecryptionWithRotationError` carrying both per-key errors. A
37
+ * `DecryptionRotationError` carrying both per-key errors. A
38
38
  * `DecryptionParseError` from the current key is **not** retried — the
39
39
  * cipher already matched, so a parse failure means the encrypted format
40
40
  * is wrong (an app bug), not a wrong key.
@@ -44,7 +44,7 @@ export default class Encrypt {
44
44
  * @throws MissingEncryptionKey
45
45
  * @throws DecryptionError
46
46
  * @throws DecryptionParseError
47
- * @throws DecryptionWithRotationError
47
+ * @throws DecryptionRotationError
48
48
  */
49
49
  static decrypt(encrypted, { algorithm, key }, legacyOpts) {
50
50
  if (legacyOpts)
@@ -81,7 +81,7 @@ export default class Encrypt {
81
81
  catch (err) {
82
82
  if (!(err instanceof DecryptionError))
83
83
  throw err;
84
- throw new DecryptionWithRotationError(currentKeyError, err);
84
+ throw new DecryptionRotationError(currentKeyError, err);
85
85
  }
86
86
  }
87
87
  /**
@@ -117,7 +117,7 @@ export default class Encrypt {
117
117
  * not forced to re-authenticate.
118
118
  * - For `@Encrypted` columns: until every existing row has been
119
119
  * re-encrypted under the new key. Dropping `legacy` early will cause
120
- * `DecryptionWithRotationError` on any not-yet-rewritten row.
120
+ * `DecryptionRotationError` on any not-yet-rewritten row.
121
121
  */
122
122
  static generateKey(algorithm) {
123
123
  switch (algorithm) {
@@ -1,4 +1,4 @@
1
- export default class DecryptionWithRotationError extends Error {
1
+ export default class DecryptionRotationError extends Error {
2
2
  currentKeyError;
3
3
  legacyKeyError;
4
4
  constructor(currentKeyError, legacyKeyError) {
@@ -7,6 +7,7 @@ import absoluteDreamPath from '../path/absoluteDreamPath.js';
7
7
  import snakeify from '../snakeify.js';
8
8
  import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
9
9
  import uniq from '../uniq.js';
10
+ import parseAttribute from './parseAttribute.js';
10
11
  /**
11
12
  * Column names that are automatically emitted by the model generator (and
12
13
  * the migration generator). When the user passes any of these explicitly,
@@ -35,8 +36,17 @@ export default function generateDreamContent(options) {
35
36
  const columnsForAttributes = config.isSTI
36
37
  ? options.columnsWithTypes
37
38
  : filterAutoGeneratedTimestampColumns(options.columnsWithTypes);
38
- const baseImports = createImportConfig(config, options, { includeSoftDelete });
39
39
  const attributesResult = processAttributes(columnsForAttributes, config.modelClassName);
40
+ // Decorators is only referenced via `@deco.*` annotations on fields (e.g.,
41
+ // BelongsTo, Encrypted). If the generated body emits none of those, the
42
+ // import + declaration would trigger lint errors for unused identifiers,
43
+ // so emit them commented out instead — keeps the boilerplate present for
44
+ // the developer to uncomment when they add their first decorator.
45
+ const decoratorsInUse = attributesResult.formattedDecorators.length > 0;
46
+ const baseImports = createImportConfig(config, options, {
47
+ includeSoftDelete,
48
+ includeDecorators: decoratorsInUse,
49
+ });
40
50
  const allImports = {
41
51
  ...baseImports,
42
52
  modelImportStatements: [...baseImports.modelImportStatements, ...attributesResult.imports],
@@ -48,9 +58,14 @@ export default function generateDreamContent(options) {
48
58
  const fieldsSection = buildFieldsSection(config, attributesResult, {
49
59
  includeDeletedAt: includeSoftDelete || hasExplicitDeletedAt,
50
60
  });
61
+ const decoBlock = decoratorsInUse
62
+ ? `const deco = new Decorators<typeof ${config.modelClassName}>()`
63
+ : `// Uncomment when adding decorators (@deco.BelongsTo, @deco.Validates, etc.):
64
+ // import { Decorators } from '@rvoh/dream'
65
+ // const deco = new Decorators<typeof ${config.modelClassName}>()`;
51
66
  return `${importSection}
52
67
 
53
- const deco = new Decorators<typeof ${config.modelClassName}>()
68
+ ${decoBlock}
54
69
 
55
70
  ${classDeclaration}
56
71
  ${tableMethod}${serializersMethod}${fieldsSection}
@@ -78,9 +93,11 @@ export function createModelConfig(options) {
78
93
  tableName,
79
94
  };
80
95
  }
81
- export function createImportConfig(config, options, { includeSoftDelete }) {
96
+ export function createImportConfig(config, options, { includeSoftDelete, includeDecorators = true }) {
82
97
  const dreamTypeImports = ['DreamColumn'];
83
- const dreamImports = ['Decorators'];
98
+ const dreamImports = [];
99
+ if (includeDecorators)
100
+ dreamImports.push('Decorators');
84
101
  if (options.serializer) {
85
102
  dreamTypeImports.push('DreamSerializers');
86
103
  }
@@ -119,32 +136,41 @@ export function processAttributes(columnsWithTypes, modelClassName) {
119
136
  };
120
137
  }
121
138
  export function processAttribute(attribute, modelClassName) {
122
- const [attributeName, attributeType, ...descriptors] = attribute.split(':');
123
- if (attributeName === undefined)
139
+ const [rawName, rawType] = attribute.split(':');
140
+ if (rawName === undefined)
124
141
  return { content: '', imports: [] };
125
- if (!attributeType) {
126
- throw new Error(`must pass a column type for ${attributeName} (i.e. ${attributeName}:string)`);
142
+ if (!rawType) {
143
+ throw new Error(`must pass a column type for ${rawName} (i.e. ${rawName}:string)`);
127
144
  }
128
- const processedAttrType = camelize(attributeType).toLowerCase();
129
- switch (processedAttrType) {
145
+ const parsed = parseAttribute(attribute);
146
+ // Malformed-but-parseable shapes (e.g., `Model@:belongs_to` with an empty
147
+ // alias) yield no content rather than throwing — the call site has already
148
+ // validated the basic name/type segments above.
149
+ if (!parsed)
150
+ return { content: '', imports: [] };
151
+ switch (parsed.normalizedAttributeType) {
130
152
  case 'belongsto':
131
- return createBelongsToAttribute(attributeName, descriptors, modelClassName);
153
+ return createBelongsToAttribute(parsed.rawAttributeName, modelClassName, {
154
+ aliasName: parsed.aliasName,
155
+ isOptional: parsed.isOptional,
156
+ });
132
157
  case 'hasone':
133
158
  case 'hasmany':
134
159
  return { content: '', imports: [] };
135
160
  case 'encrypted':
136
- return createEncryptedAttribute(attributeName, attribute, modelClassName);
161
+ return createEncryptedAttribute(parsed.rawAttributeName, attribute, modelClassName);
137
162
  default:
138
- return createRegularAttribute(attributeName, attribute, modelClassName);
163
+ return createRegularAttribute(parsed.rawAttributeName, attribute, modelClassName);
139
164
  }
140
165
  }
141
- export function createBelongsToAttribute(attributeName, descriptors, modelClassName) {
142
- const fullyQualifiedAssociatedModelName = standardizeFullyQualifiedModelName(attributeName);
166
+ export function createBelongsToAttribute(fullyQualifiedModelInput, modelClassName, { aliasName, isOptional = false, } = {}) {
167
+ const fullyQualifiedAssociatedModelName = standardizeFullyQualifiedModelName(fullyQualifiedModelInput);
143
168
  const associationModelName = globalClassNameFromFullyQualifiedModelName(fullyQualifiedAssociatedModelName);
144
169
  const associationImportStatement = importStatementForModel(fullyQualifiedAssociatedModelName);
145
- const associationName = camelize(fullyQualifiedAssociatedModelName.split('/').pop());
170
+ const associationName = aliasName
171
+ ? camelize(aliasName)
172
+ : camelize(fullyQualifiedAssociatedModelName.split('/').pop());
146
173
  const associationForeignKey = `${associationName}Id`;
147
- const isOptional = descriptors.includes('optional');
148
174
  const content = `
149
175
  @deco.BelongsTo('${fullyQualifiedAssociatedModelName}', { on: '${associationForeignKey}'${isOptional ? ', optional: true' : ''} })
150
176
  public ${associationName}: ${associationModelName}${isOptional ? ' | null' : ''}
@@ -17,14 +17,23 @@ export default function generateFactoryContent({ fullyQualifiedModelName, column
17
17
  const attributeDefaults = [];
18
18
  let counterVariableIncremented = false;
19
19
  for (const attribute of columnsWithTypes) {
20
- const [attributeName, _attributeType, ...descriptors] = attribute.split(':');
21
- if (attributeName === undefined)
20
+ const [rawSegmentOne, _attributeType, ...descriptors] = attribute.split(':');
21
+ if (rawSegmentOne === undefined)
22
22
  continue;
23
23
  if (_attributeType === undefined)
24
24
  continue;
25
25
  const optional = optionalFromDescriptors(descriptors);
26
26
  if (optional)
27
27
  continue;
28
+ // Extract optional `@alias` from segment-1 (Model@alias:belongs_to form).
29
+ // Non-association tokens never contain `@`, so this is a no-op for scalars.
30
+ const atIdx = rawSegmentOne.indexOf('@');
31
+ const aliasName = atIdx !== -1 ? rawSegmentOne.slice(atIdx + 1) : undefined;
32
+ const attributeName = atIdx !== -1 ? rawSegmentOne.slice(0, atIdx) : rawSegmentOne;
33
+ if (!attributeName)
34
+ continue;
35
+ if (atIdx !== -1 && !aliasName)
36
+ continue;
28
37
  const attributeVariable = camelize(attributeName.replace(/\//g, ''));
29
38
  if (/^type$/.test(attributeName))
30
39
  continue;
@@ -36,7 +45,9 @@ export default function generateFactoryContent({ fullyQualifiedModelName, column
36
45
  const safeAttributeType = camelize(attributeType)?.toLowerCase();
37
46
  switch (safeAttributeType) {
38
47
  case 'belongsto': {
39
- const attributeVariable = camelize(attributeName.split('/').pop());
48
+ // When `Model@alias:belongs_to`, the factory property uses the alias;
49
+ // otherwise it uses the model's last namespace segment (legacy form).
50
+ const attributeVariable = aliasName ? camelize(aliasName) : camelize(attributeName.split('/').pop());
40
51
  const fullyQualifiedAssociatedModelName = standardizeFullyQualifiedModelName(attributeName);
41
52
  const associationModelName = globalClassNameFromFullyQualifiedModelName(fullyQualifiedAssociatedModelName);
42
53
  const associationFactoryImportStatement = `import create${associationModelName} from '${absoluteDreamPath('factories', fullyQualifiedAssociatedModelName)}'`;
@@ -66,11 +77,27 @@ export default function generateFactoryContent({ fullyQualifiedModelName, column
66
77
  counterVariableIncremented = true;
67
78
  break;
68
79
  case 'enum':
69
- attributeDefaults.push(`${attributeVariable}: '${(descriptors.at(-1) || '<tbd>').split(',')[0]}',`);
70
- break;
71
- case 'enum[]':
72
- attributeDefaults.push(`${attributeVariable}: ['${(descriptors.at(-1) || '<tbd>').split(',')[0]}'],`);
80
+ case 'enum[]': {
81
+ // When the user passed `name:enum:enum_type_name` (reuse form, no
82
+ // inline values), descriptors has exactly one element — the enum
83
+ // type name. The factory cannot see the enum's values, so emit a
84
+ // TS-rejecting placeholder rather than the type name itself (the
85
+ // previous behavior emitted `'enum_type_name'` as the literal value,
86
+ // which compiles in the factory but fails at runtime).
87
+ const isReuseWithoutValues = descriptors.length === 1;
88
+ const isArrayEnum = safeAttributeType === 'enum[]';
89
+ if (isReuseWithoutValues) {
90
+ const enumTypeName = descriptors[0];
91
+ const placeholder = isArrayEnum ? `['TODO']` : `'TODO'`;
92
+ attributeDefaults.push(`// TODO: replace with a value from the \`${enumTypeName}\` enum\n ${attributeVariable}: ${placeholder},`);
93
+ }
94
+ else {
95
+ const firstValue = (descriptors.at(-1) || '<tbd>').split(',')[0];
96
+ const literal = isArrayEnum ? `['${firstValue}']` : `'${firstValue}'`;
97
+ attributeDefaults.push(`${attributeVariable}: ${literal},`);
98
+ }
73
99
  break;
100
+ }
74
101
  case 'integer':
75
102
  attributeDefaults.push(`${attributeVariable}: 1,`);
76
103
  break;
@@ -27,9 +27,19 @@ export default function generateMigrationContent({ connectionName = 'default', t
27
27
  const emitDeletedAtColumn = !altering && (softDelete || userExplicitlyPassedDeletedAt);
28
28
  const { columnDefs, columnDrops, indexDefs, indexDrops } = processedColumnsWithTypes.reduce((acc, attributeDeclaration) => {
29
29
  const { columnDefs, columnDrops, indexDefs, indexDrops } = acc;
30
- const [nonStandardAttributeName, _attributeType, ...descriptors] = attributeDeclaration.split(':');
30
+ const [rawSegmentOne, _attributeType, ...descriptors] = attributeDeclaration.split(':');
31
+ if (!rawSegmentOne)
32
+ return acc;
33
+ // Extract optional `@alias` from segment-1 (Model@alias:belongs_to form).
34
+ // The model name (without alias) is what gets standardized / referenced
35
+ // by table lookup; the alias drives the column / index naming when present.
36
+ const atIdx = rawSegmentOne.indexOf('@');
37
+ const aliasName = atIdx !== -1 ? rawSegmentOne.slice(atIdx + 1) : undefined;
38
+ const nonStandardAttributeName = atIdx !== -1 ? rawSegmentOne.slice(0, atIdx) : rawSegmentOne;
31
39
  if (!nonStandardAttributeName)
32
40
  return acc;
41
+ if (atIdx !== -1 && !aliasName)
42
+ return acc;
33
43
  /**
34
44
  * Automatically set email columns to citext since different casings of
35
45
  * email address are the same email address
@@ -64,8 +74,14 @@ export default function generateMigrationContent({ connectionName = 'default', t
64
74
  primaryKeyType,
65
75
  omitInlineNonNull,
66
76
  originalAssociationName: nonStandardAttributeName,
77
+ aliasName,
67
78
  }));
68
- attributeName = snakeify(nonStandardAttributeName.split('/').pop());
79
+ // Resolve the actual column name used for index + drop emission.
80
+ // When an alias is present (Model@alias:belongs_to), the column is
81
+ // `${alias}_id`; otherwise it's derived from the model's last segment.
82
+ attributeName = aliasName
83
+ ? snakeify(aliasName)
84
+ : snakeify(nonStandardAttributeName.split('/').pop());
69
85
  attributeName = associationNameToForeignKey(attributeName);
70
86
  break;
71
87
  case 'enum':
@@ -268,6 +284,7 @@ function generateColumnStr(attributeName, attributeType, descriptors, { omitInli
268
284
  const isUnique = /(email|token|uuid)$/.test(attributeName);
269
285
  const hasExtraValues = providedDefault || notNull || isUnique;
270
286
  const isArray = /\[\]$/.test(attributeType);
287
+ const needsJsonDefault = notNull && !isArray && (attributeType === 'jsonb' || attributeType === 'json');
271
288
  if (hasExtraValues)
272
289
  returnStr += ', col => col';
273
290
  if (notNull)
@@ -278,6 +295,12 @@ function generateColumnStr(attributeName, attributeType, descriptors, { omitInli
278
295
  returnStr += `.defaultTo('${providedDefault}')`;
279
296
  else if (isArray)
280
297
  returnStr += `.defaultTo('{}')`;
298
+ // jsonb / json columns get an empty-object default so calling create() on a
299
+ // model without explicitly setting the column doesn't trip NOT NULL. Mirrors
300
+ // the existing boolean → false and array → '{}' auto-defaults. Optional
301
+ // jsonb columns skip the default since null is the intended initial state.
302
+ else if (needsJsonDefault)
303
+ returnStr += `.defaultTo(sql\`'{}'::${attributeType}\`)`;
281
304
  returnStr = `${returnStr})`;
282
305
  if (attributeName === STI_TYPE_COLUMN_NAME)
283
306
  returnStr = `// CONSIDER: when using type for STI, always use an enum
@@ -306,11 +329,14 @@ function attributeTypeString(attributeType) {
306
329
  }
307
330
  }
308
331
  }
309
- function generateBelongsToStr(connectionName, associationName, { primaryKeyType, omitInlineNonNull: optional = false, originalAssociationName, }) {
332
+ function generateBelongsToStr(connectionName, associationName, { primaryKeyType, omitInlineNonNull: optional = false, originalAssociationName, aliasName, }) {
310
333
  const dbDriverClass = Query.dbDriverClass(connectionName);
311
334
  const dataType = dbDriverClass.foreignKeyTypeFromPrimaryKey(primaryKeyType);
312
335
  const references = lookupReferencesTable(associationName, originalAssociationName);
313
- return `.addColumn('${associationNameToForeignKey(associationName.split('/').pop())}', '${dataType}', col => col.references('${references}.id').onDelete('restrict')${optional ? '' : '.notNull()'})`;
336
+ // When the user passed `Model@alias:belongs_to`, the column name comes from
337
+ // the alias; otherwise it's the model's last segment (existing behavior).
338
+ const columnNameSource = aliasName ? snakeify(aliasName) : associationName.split('/').pop();
339
+ return `.addColumn('${associationNameToForeignKey(columnNameSource)}', '${dataType}', col => col.references('${references}.id').onDelete('restrict')${optional ? '' : '.notNull()'})`;
314
340
  }
315
341
  function generateIdStr({ primaryKeyType }) {
316
342
  switch (primaryKeyType) {
@@ -0,0 +1,61 @@
1
+ import camelize from '../camelize.js';
2
+ /**
3
+ * Parse a single `columnsWithTypes` CLI token into its structural pieces.
4
+ *
5
+ * Centralizes the splitting + normalization logic shared across the model
6
+ * generator (`generateDreamContent`), the migration generator
7
+ * (`generateMigrationContent`), the factory generator
8
+ * (`generateFactoryContent`), and Psychic's resource/controller generators.
9
+ * Keeps the shared layer thin: consumers handle their own coercions (e.g.,
10
+ * migration's `email$ → citext`) and filters (e.g., Psychic's `_type`/`_id`
11
+ * exclusion).
12
+ *
13
+ * Returns `null` for malformed tokens (missing name or type).
14
+ *
15
+ * Supported forms (segment-1 only — see `rawAttributeType` for the full
16
+ * vocabulary of types/associations recognized by individual generators):
17
+ *
18
+ * - `name:type` → standard column
19
+ * - `name:type:optional` → nullable column
20
+ * - `Model:belongs_to[:optional]` → association with model-derived alias
21
+ * - `Model@alias:belongs_to[:optional]` → association with explicit alias
22
+ */
23
+ export default function parseAttribute(attribute) {
24
+ const segments = attribute.split(':');
25
+ const rawSegmentOne = segments[0];
26
+ const rawAttributeType = segments[1];
27
+ const descriptors = segments.slice(2);
28
+ if (!rawSegmentOne || !rawAttributeType)
29
+ return null;
30
+ // Split segment-1 on `@` to extract an optional alias. Empty alias after `@`
31
+ // (e.g., `Model@:belongs_to`) is treated as malformed.
32
+ let rawAttributeName = rawSegmentOne;
33
+ let aliasName;
34
+ const atIndex = rawSegmentOne.indexOf('@');
35
+ if (atIndex !== -1) {
36
+ rawAttributeName = rawSegmentOne.slice(0, atIndex);
37
+ const rawAlias = rawSegmentOne.slice(atIndex + 1);
38
+ if (!rawAttributeName || !rawAlias)
39
+ return null;
40
+ aliasName = rawAlias;
41
+ }
42
+ // Pop trailing `optional` keyword off the descriptors list. Mirrors
43
+ // `optionalFromDescriptors` in generateMigrationContent.ts so the keyword
44
+ // behaves identically regardless of which consumer parses the token.
45
+ let isOptional = false;
46
+ if (descriptors[descriptors.length - 1] === 'optional') {
47
+ descriptors.pop();
48
+ isOptional = true;
49
+ }
50
+ const normalizedAttributeType = camelize(rawAttributeType).toLowerCase();
51
+ const isArray = /\[\]$/.test(rawAttributeType);
52
+ return {
53
+ rawAttributeName,
54
+ aliasName,
55
+ rawAttributeType,
56
+ normalizedAttributeType,
57
+ descriptors,
58
+ isOptional,
59
+ isArray,
60
+ };
61
+ }
@@ -7,6 +7,9 @@ export { default as DataIncompatibleWithDatabaseField } from '../errors/db/DataI
7
7
  export { default as DataTypeColumnTypeMismatch } from '../errors/db/DataTypeColumnTypeMismatch.js';
8
8
  export { default as NotNullViolation } from '../errors/db/NotNullViolation.js';
9
9
  export { default as GlobalNameNotSet } from '../errors/dream-app/GlobalNameNotSet.js';
10
+ export { default as DecryptionError } from '../errors/encrypt/DecryptionError.js';
11
+ export { default as DecryptionParseError } from '../errors/encrypt/DecryptionParseError.js';
12
+ export { default as DecryptionRotationError } from '../errors/encrypt/DecryptionRotationError.js';
10
13
  export { default as RecordNotFound } from '../errors/RecordNotFound.js';
11
14
  export { default as MissingSerializersDefinition } from '../errors/serializers/MissingSerializersDefinition.js';
12
15
  export { default as ValidationError } from '../errors/ValidationError.js';
@@ -11,7 +11,7 @@ import DreamCliLogger from './logger/DreamCliLogger.js';
11
11
  import colorize from './logger/loggable/colorize.js';
12
12
  export const CLI_INDENT = ' ';
13
13
  const INDENT = CLI_INDENT;
14
- export const baseColumnsWithTypesDescription = `space separated snake-case (except for belongs_to model name) properties like this:
14
+ export const baseColumnsWithTypesDescription = `space separated snake-case (except for belongs_to model name, which may take an @alias suffix to rename the FK) properties like this:
15
15
  ${INDENT} title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal User:belongs_to
16
16
  ${INDENT}
17
17
  ${INDENT}all properties default to not nullable; null can be allowed by appending ':optional':
@@ -71,7 +71,16 @@ ${INDENT}
71
71
  ${INDENT} use the fully qualified model name (matching its path under src/app/models/):
72
72
  ${INDENT} User:belongs_to # creates user_id column + BelongsTo association
73
73
  ${INDENT} Health/Coach:belongs_to # creates health_coach_id column + BelongsTo association
74
- ${INDENT} User:belongs_to:optional # nullable foreign key (for optional associations)`;
74
+ ${INDENT} User:belongs_to:optional # nullable foreign key (for optional associations)
75
+ ${INDENT}
76
+ ${INDENT} rename the association with Model@alias — the snake_case alias drives the FK column name AND the
77
+ ${INDENT} @deco.BelongsTo association + typed FK property on the generated model:
78
+ ${INDENT} InternalUser@canceled_by:belongs_to:optional # canceled_by_id column, canceledById property, canceledBy association,
79
+ ${INDENT} # @deco.BelongsTo('InternalUser', { on: 'canceledById', optional: true })
80
+ ${INDENT} Messaging/Template@template:belongs_to # template_id column, templateId property, template association
81
+ ${INDENT} # (strips the namespace from the property/association names while keeping
82
+ ${INDENT} # the namespaced model reference intact)
83
+ ${INDENT} Aliasing also lets you declare multiple FKs to the same model in one generator call without column collisions.`;
75
84
  const columnsWithTypesDescriptionForMigration = baseColumnsWithTypesDescription +
76
85
  `
77
86
  ${INDENT}
@@ -82,7 +91,11 @@ ${INDENT}
82
91
  ${INDENT} use the fully qualified model name (matching its path under src/app/models/):
83
92
  ${INDENT} User:belongs_to # creates user_id column with index
84
93
  ${INDENT} Health/Coach:belongs_to # creates health_coach_id column with index
85
- ${INDENT} User:belongs_to:optional # nullable foreign key`;
94
+ ${INDENT} User:belongs_to:optional # nullable foreign key
95
+ ${INDENT}
96
+ ${INDENT} rename the FK column with Model@alias (snake_case alias becomes the column name):
97
+ ${INDENT} InternalUser@canceled_by:belongs_to:optional # canceled_by_id column with index
98
+ ${INDENT} Messaging/Template@template:belongs_to # template_id column (strips the namespace from the column name)`;
86
99
  export default class DreamCLI {
87
100
  /**
88
101
  * Starts the Dream console
@@ -1,5 +1,5 @@
1
1
  import DecryptionError from '../errors/encrypt/DecryptionError.js';
2
- import DecryptionWithRotationError from '../errors/encrypt/DecryptionWithRotationError.js';
2
+ import DecryptionRotationError from '../errors/encrypt/DecryptionRotationError.js';
3
3
  import MissingEncryptionKey from '../errors/encrypt/MissingEncryptionKey.js';
4
4
  import decryptAESGCM from './algorithms/aes-gcm/decryptAESGCM.js';
5
5
  import encryptAESGCM from './algorithms/aes-gcm/encryptAESGCM.js';
@@ -34,7 +34,7 @@ export default class Encrypt {
34
34
  *
35
35
  * **Three-arg form** (rotation): tries the current key first; on
36
36
  * `DecryptionError` falls back to the legacy key. If both fail, throws
37
- * `DecryptionWithRotationError` carrying both per-key errors. A
37
+ * `DecryptionRotationError` carrying both per-key errors. A
38
38
  * `DecryptionParseError` from the current key is **not** retried — the
39
39
  * cipher already matched, so a parse failure means the encrypted format
40
40
  * is wrong (an app bug), not a wrong key.
@@ -44,7 +44,7 @@ export default class Encrypt {
44
44
  * @throws MissingEncryptionKey
45
45
  * @throws DecryptionError
46
46
  * @throws DecryptionParseError
47
- * @throws DecryptionWithRotationError
47
+ * @throws DecryptionRotationError
48
48
  */
49
49
  static decrypt(encrypted, { algorithm, key }, legacyOpts) {
50
50
  if (legacyOpts)
@@ -81,7 +81,7 @@ export default class Encrypt {
81
81
  catch (err) {
82
82
  if (!(err instanceof DecryptionError))
83
83
  throw err;
84
- throw new DecryptionWithRotationError(currentKeyError, err);
84
+ throw new DecryptionRotationError(currentKeyError, err);
85
85
  }
86
86
  }
87
87
  /**
@@ -117,7 +117,7 @@ export default class Encrypt {
117
117
  * not forced to re-authenticate.
118
118
  * - For `@Encrypted` columns: until every existing row has been
119
119
  * re-encrypted under the new key. Dropping `legacy` early will cause
120
- * `DecryptionWithRotationError` on any not-yet-rewritten row.
120
+ * `DecryptionRotationError` on any not-yet-rewritten row.
121
121
  */
122
122
  static generateKey(algorithm) {
123
123
  switch (algorithm) {
@@ -1,4 +1,4 @@
1
- export default class DecryptionWithRotationError extends Error {
1
+ export default class DecryptionRotationError extends Error {
2
2
  currentKeyError;
3
3
  legacyKeyError;
4
4
  constructor(currentKeyError, legacyKeyError) {
@@ -7,6 +7,7 @@ import absoluteDreamPath from '../path/absoluteDreamPath.js';
7
7
  import snakeify from '../snakeify.js';
8
8
  import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
9
9
  import uniq from '../uniq.js';
10
+ import parseAttribute from './parseAttribute.js';
10
11
  /**
11
12
  * Column names that are automatically emitted by the model generator (and
12
13
  * the migration generator). When the user passes any of these explicitly,
@@ -35,8 +36,17 @@ export default function generateDreamContent(options) {
35
36
  const columnsForAttributes = config.isSTI
36
37
  ? options.columnsWithTypes
37
38
  : filterAutoGeneratedTimestampColumns(options.columnsWithTypes);
38
- const baseImports = createImportConfig(config, options, { includeSoftDelete });
39
39
  const attributesResult = processAttributes(columnsForAttributes, config.modelClassName);
40
+ // Decorators is only referenced via `@deco.*` annotations on fields (e.g.,
41
+ // BelongsTo, Encrypted). If the generated body emits none of those, the
42
+ // import + declaration would trigger lint errors for unused identifiers,
43
+ // so emit them commented out instead — keeps the boilerplate present for
44
+ // the developer to uncomment when they add their first decorator.
45
+ const decoratorsInUse = attributesResult.formattedDecorators.length > 0;
46
+ const baseImports = createImportConfig(config, options, {
47
+ includeSoftDelete,
48
+ includeDecorators: decoratorsInUse,
49
+ });
40
50
  const allImports = {
41
51
  ...baseImports,
42
52
  modelImportStatements: [...baseImports.modelImportStatements, ...attributesResult.imports],
@@ -48,9 +58,14 @@ export default function generateDreamContent(options) {
48
58
  const fieldsSection = buildFieldsSection(config, attributesResult, {
49
59
  includeDeletedAt: includeSoftDelete || hasExplicitDeletedAt,
50
60
  });
61
+ const decoBlock = decoratorsInUse
62
+ ? `const deco = new Decorators<typeof ${config.modelClassName}>()`
63
+ : `// Uncomment when adding decorators (@deco.BelongsTo, @deco.Validates, etc.):
64
+ // import { Decorators } from '@rvoh/dream'
65
+ // const deco = new Decorators<typeof ${config.modelClassName}>()`;
51
66
  return `${importSection}
52
67
 
53
- const deco = new Decorators<typeof ${config.modelClassName}>()
68
+ ${decoBlock}
54
69
 
55
70
  ${classDeclaration}
56
71
  ${tableMethod}${serializersMethod}${fieldsSection}
@@ -78,9 +93,11 @@ export function createModelConfig(options) {
78
93
  tableName,
79
94
  };
80
95
  }
81
- export function createImportConfig(config, options, { includeSoftDelete }) {
96
+ export function createImportConfig(config, options, { includeSoftDelete, includeDecorators = true }) {
82
97
  const dreamTypeImports = ['DreamColumn'];
83
- const dreamImports = ['Decorators'];
98
+ const dreamImports = [];
99
+ if (includeDecorators)
100
+ dreamImports.push('Decorators');
84
101
  if (options.serializer) {
85
102
  dreamTypeImports.push('DreamSerializers');
86
103
  }
@@ -119,32 +136,41 @@ export function processAttributes(columnsWithTypes, modelClassName) {
119
136
  };
120
137
  }
121
138
  export function processAttribute(attribute, modelClassName) {
122
- const [attributeName, attributeType, ...descriptors] = attribute.split(':');
123
- if (attributeName === undefined)
139
+ const [rawName, rawType] = attribute.split(':');
140
+ if (rawName === undefined)
124
141
  return { content: '', imports: [] };
125
- if (!attributeType) {
126
- throw new Error(`must pass a column type for ${attributeName} (i.e. ${attributeName}:string)`);
142
+ if (!rawType) {
143
+ throw new Error(`must pass a column type for ${rawName} (i.e. ${rawName}:string)`);
127
144
  }
128
- const processedAttrType = camelize(attributeType).toLowerCase();
129
- switch (processedAttrType) {
145
+ const parsed = parseAttribute(attribute);
146
+ // Malformed-but-parseable shapes (e.g., `Model@:belongs_to` with an empty
147
+ // alias) yield no content rather than throwing — the call site has already
148
+ // validated the basic name/type segments above.
149
+ if (!parsed)
150
+ return { content: '', imports: [] };
151
+ switch (parsed.normalizedAttributeType) {
130
152
  case 'belongsto':
131
- return createBelongsToAttribute(attributeName, descriptors, modelClassName);
153
+ return createBelongsToAttribute(parsed.rawAttributeName, modelClassName, {
154
+ aliasName: parsed.aliasName,
155
+ isOptional: parsed.isOptional,
156
+ });
132
157
  case 'hasone':
133
158
  case 'hasmany':
134
159
  return { content: '', imports: [] };
135
160
  case 'encrypted':
136
- return createEncryptedAttribute(attributeName, attribute, modelClassName);
161
+ return createEncryptedAttribute(parsed.rawAttributeName, attribute, modelClassName);
137
162
  default:
138
- return createRegularAttribute(attributeName, attribute, modelClassName);
163
+ return createRegularAttribute(parsed.rawAttributeName, attribute, modelClassName);
139
164
  }
140
165
  }
141
- export function createBelongsToAttribute(attributeName, descriptors, modelClassName) {
142
- const fullyQualifiedAssociatedModelName = standardizeFullyQualifiedModelName(attributeName);
166
+ export function createBelongsToAttribute(fullyQualifiedModelInput, modelClassName, { aliasName, isOptional = false, } = {}) {
167
+ const fullyQualifiedAssociatedModelName = standardizeFullyQualifiedModelName(fullyQualifiedModelInput);
143
168
  const associationModelName = globalClassNameFromFullyQualifiedModelName(fullyQualifiedAssociatedModelName);
144
169
  const associationImportStatement = importStatementForModel(fullyQualifiedAssociatedModelName);
145
- const associationName = camelize(fullyQualifiedAssociatedModelName.split('/').pop());
170
+ const associationName = aliasName
171
+ ? camelize(aliasName)
172
+ : camelize(fullyQualifiedAssociatedModelName.split('/').pop());
146
173
  const associationForeignKey = `${associationName}Id`;
147
- const isOptional = descriptors.includes('optional');
148
174
  const content = `
149
175
  @deco.BelongsTo('${fullyQualifiedAssociatedModelName}', { on: '${associationForeignKey}'${isOptional ? ', optional: true' : ''} })
150
176
  public ${associationName}: ${associationModelName}${isOptional ? ' | null' : ''}