@rvoh/dream 2.17.1 → 2.17.2

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 (192) hide show
  1. package/dist/cjs/src/cli/index.js +17 -10
  2. package/dist/cjs/src/helpers/cli/generateDreamContent.js +13 -6
  3. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +18 -12
  4. package/dist/cjs/src/helpers/cli/validateStiChildColumns.js +7 -0
  5. package/dist/esm/src/cli/index.js +17 -10
  6. package/dist/esm/src/helpers/cli/generateDreamContent.js +13 -6
  7. package/dist/esm/src/helpers/cli/generateMigrationContent.js +18 -12
  8. package/dist/esm/src/helpers/cli/validateStiChildColumns.js +7 -0
  9. package/dist/types/src/cli/index.d.ts +2 -1
  10. package/dist/types/src/helpers/cli/validateStiChildColumns.d.ts +1 -0
  11. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  12. package/docs/classes/db.KyselyQueryDriver.html +34 -34
  13. package/docs/classes/db.PostgresQueryDriver.html +35 -35
  14. package/docs/classes/db.QueryDriverBase.html +33 -33
  15. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  16. package/docs/classes/errors.ColumnOverflow.html +3 -3
  17. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  18. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  19. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  20. package/docs/classes/errors.DecryptionError.html +2 -2
  21. package/docs/classes/errors.DecryptionParseError.html +2 -2
  22. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  23. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  24. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  25. package/docs/classes/errors.InvalidClockTime.html +2 -2
  26. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  27. package/docs/classes/errors.InvalidDateTime.html +2 -2
  28. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  29. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  30. package/docs/classes/errors.NotNullViolation.html +3 -3
  31. package/docs/classes/errors.RecordNotFound.html +3 -3
  32. package/docs/classes/errors.ValidationError.html +3 -3
  33. package/docs/classes/index.CalendarDate.html +33 -33
  34. package/docs/classes/index.ClockTime.html +32 -32
  35. package/docs/classes/index.ClockTimeTz.html +35 -35
  36. package/docs/classes/index.DateTime.html +86 -86
  37. package/docs/classes/index.Decorators.html +19 -19
  38. package/docs/classes/index.Dream.html +118 -118
  39. package/docs/classes/index.DreamApp.html +10 -10
  40. package/docs/classes/index.DreamTransaction.html +2 -2
  41. package/docs/classes/index.Env.html +2 -2
  42. package/docs/classes/index.Query.html +57 -57
  43. package/docs/classes/system.CliFileWriter.html +4 -4
  44. package/docs/classes/system.DreamBin.html +2 -2
  45. package/docs/classes/system.DreamCLI.html +7 -7
  46. package/docs/classes/system.DreamImporter.html +2 -2
  47. package/docs/classes/system.DreamLogos.html +2 -2
  48. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  49. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  50. package/docs/classes/system.PathHelpers.html +3 -3
  51. package/docs/classes/utils.Encrypt.html +3 -3
  52. package/docs/classes/utils.Range.html +2 -2
  53. package/docs/functions/db.closeAllDbConnections.html +1 -1
  54. package/docs/functions/db.dreamDbConnections.html +1 -1
  55. package/docs/functions/db.untypedDb.html +1 -1
  56. package/docs/functions/db.validateColumn.html +1 -1
  57. package/docs/functions/db.validateTable.html +1 -1
  58. package/docs/functions/errors.pgErrorType.html +1 -1
  59. package/docs/functions/index.DreamSerializer.html +1 -1
  60. package/docs/functions/index.ObjectSerializer.html +1 -1
  61. package/docs/functions/index.ReplicaSafe.html +1 -1
  62. package/docs/functions/index.STI.html +1 -1
  63. package/docs/functions/index.SoftDelete.html +1 -1
  64. package/docs/functions/utils.camelize.html +1 -1
  65. package/docs/functions/utils.capitalize.html +1 -1
  66. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  67. package/docs/functions/utils.compact.html +1 -1
  68. package/docs/functions/utils.groupBy.html +1 -1
  69. package/docs/functions/utils.hyphenize.html +1 -1
  70. package/docs/functions/utils.intersection.html +1 -1
  71. package/docs/functions/utils.isEmpty.html +1 -1
  72. package/docs/functions/utils.normalizeUnicode.html +1 -1
  73. package/docs/functions/utils.pascalize.html +1 -1
  74. package/docs/functions/utils.percent.html +1 -1
  75. package/docs/functions/utils.range.html +1 -1
  76. package/docs/functions/utils.round.html +1 -1
  77. package/docs/functions/utils.sanitizeString.html +1 -1
  78. package/docs/functions/utils.snakeify.html +1 -1
  79. package/docs/functions/utils.sort.html +1 -1
  80. package/docs/functions/utils.sortBy.html +1 -1
  81. package/docs/functions/utils.sortObjectByKey.html +1 -1
  82. package/docs/functions/utils.sortObjectByValue.html +1 -1
  83. package/docs/functions/utils.uncapitalize.html +1 -1
  84. package/docs/functions/utils.uniq.html +1 -1
  85. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  86. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  87. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  88. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  89. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  90. package/docs/interfaces/types.DecoratorContext.html +2 -2
  91. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  92. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  93. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  94. package/docs/interfaces/types.DurationObject.html +2 -2
  95. package/docs/interfaces/types.EncryptOptions.html +2 -2
  96. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  97. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  98. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  99. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  100. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  101. package/docs/types/openapi.OpenapiFormats.html +1 -1
  102. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  103. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  104. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  105. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  106. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  107. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  108. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  109. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  110. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  111. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  112. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  113. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  114. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  115. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  116. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  117. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  118. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  119. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  120. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  121. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  122. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  123. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  124. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  125. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  126. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  127. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  128. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  129. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  130. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  131. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  132. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  133. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  134. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  135. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  136. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  137. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  138. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  139. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  140. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  141. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  142. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  143. package/docs/types/types.CalendarDateObject.html +1 -1
  144. package/docs/types/types.Camelized.html +1 -1
  145. package/docs/types/types.ClockTimeObject.html +1 -1
  146. package/docs/types/types.DbConnectionType.html +1 -1
  147. package/docs/types/types.DbTypes.html +1 -1
  148. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  149. package/docs/types/types.DreamAttributes.html +1 -1
  150. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  151. package/docs/types/types.DreamClassColumn.html +1 -1
  152. package/docs/types/types.DreamColumn.html +1 -1
  153. package/docs/types/types.DreamColumnNames.html +1 -1
  154. package/docs/types/types.DreamLogLevel.html +1 -1
  155. package/docs/types/types.DreamLogger.html +2 -2
  156. package/docs/types/types.DreamModelSerializerType.html +1 -1
  157. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  158. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  159. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  160. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  161. package/docs/types/types.DreamSerializable.html +1 -1
  162. package/docs/types/types.DreamSerializableArray.html +1 -1
  163. package/docs/types/types.DreamSerializerKey.html +1 -1
  164. package/docs/types/types.DreamSerializers.html +1 -1
  165. package/docs/types/types.DreamVirtualColumns.html +1 -1
  166. package/docs/types/types.DurationUnit.html +1 -1
  167. package/docs/types/types.EncryptAlgorithm.html +1 -1
  168. package/docs/types/types.HasManyStatement.html +1 -1
  169. package/docs/types/types.HasOneStatement.html +1 -1
  170. package/docs/types/types.Hyphenized.html +1 -1
  171. package/docs/types/types.Pascalized.html +1 -1
  172. package/docs/types/types.PrimaryKeyType.html +1 -1
  173. package/docs/types/types.RoundingPrecision.html +1 -1
  174. package/docs/types/types.SerializerCasing.html +1 -1
  175. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  176. package/docs/types/types.Snakeified.html +1 -1
  177. package/docs/types/types.StrictInterface.html +1 -1
  178. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  179. package/docs/types/types.UpdateableProperties.html +1 -1
  180. package/docs/types/types.ValidationType.html +1 -1
  181. package/docs/types/types.ViewModel.html +2 -2
  182. package/docs/types/types.ViewModelClass.html +1 -1
  183. package/docs/types/types.WeekdayName.html +1 -1
  184. package/docs/types/types.WhereStatementForDream.html +1 -1
  185. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  186. package/docs/variables/index.DreamConst.html +1 -1
  187. package/docs/variables/index.ops.html +1 -1
  188. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  189. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  190. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  191. package/docs/variables/system.primaryKeyTypes.html +1 -1
  192. package/package.json +1 -1
@@ -11,8 +11,8 @@ 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, which may take an @alias suffix to rename the FK) properties like this:
15
- ${INDENT} title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal User:belongs_to
14
+ export const baseColumnsWithTypesDescription = `space separated snake-case properties like this:
15
+ ${INDENT} title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal
16
16
  ${INDENT}
17
17
  ${INDENT}all properties default to not nullable; null can be allowed by appending ':optional':
18
18
  ${INDENT} subtitle:string:optional
@@ -61,8 +61,11 @@ ${INDENT} omit the enum values to leverage an existing enum (omits the en
61
61
  ${INDENT} type:enum:room_types or type:enum:room_types:optional
62
62
  ${INDENT}
63
63
  ${INDENT} leveraging arrays, add the "[]" suffix, e.g.: type:enum[]:room_types:bathroom,kitchen,bedroom`;
64
- const columnsWithTypesDescription = baseColumnsWithTypesDescription +
65
- `
64
+ function buildColumnsWithTypesDescription(belongsToDescription) {
65
+ return `${baseColumnsWithTypesDescription}
66
+ ${belongsToDescription}`;
67
+ }
68
+ const columnsWithTypesDescription = buildColumnsWithTypesDescription(`
66
69
  ${INDENT}
67
70
  ${INDENT} - belongs_to:
68
71
  ${INDENT} ALWAYS use this instead of adding a raw uuid column for foreign keys. It creates the FK column, adds a database index,
@@ -80,9 +83,13 @@ ${INDENT} # @deco.
80
83
  ${INDENT} Messaging/Template@template:belongs_to # template_id column, templateId property, template association
81
84
  ${INDENT} # (strips the namespace from the property/association names while keeping
82
85
  ${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.`;
84
- const columnsWithTypesDescriptionForMigration = baseColumnsWithTypesDescription +
85
- `
86
+ ${INDENT} Aliasing also lets you declare multiple FKs to the same model in one generator call without column collisions.`);
87
+ export const columnsWithTypesDescriptionForStiChild = buildColumnsWithTypesDescription(`
88
+ ${INDENT}
89
+ ${INDENT} - belongs_to:
90
+ ${INDENT} NOT supported for STI children. STI children cannot declare associations; declare all BelongsTo,
91
+ ${INDENT} HasOne, and HasMany associations on the STI parent model instead.`);
92
+ const columnsWithTypesDescriptionForMigration = buildColumnsWithTypesDescription(`
86
93
  ${INDENT}
87
94
  ${INDENT} - belongs_to:
88
95
  ${INDENT} ALWAYS use this instead of adding a raw uuid column for foreign keys. It creates the FK column with an index.
@@ -95,7 +102,7 @@ ${INDENT} User:belongs_to:optional # nullable foreign key
95
102
  ${INDENT}
96
103
  ${INDENT} rename the FK column with Model@alias (snake_case alias becomes the column name):
97
104
  ${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)`;
105
+ ${INDENT} Messaging/Template@template:belongs_to # template_id column (strips the namespace from the column name)`);
99
106
  export default class DreamCLI {
100
107
  /**
101
108
  * Starts the Dream console
@@ -222,7 +229,7 @@ ${INDENT} Settings/CommunicationPreferences # src/app/models/Settings/Communi
222
229
  .alias('g:sti-child')
223
230
  .description(`Generates an STI (Single Table Inheritance) child model that extends an existing parent model. The child shares the parent's database table (discriminated by the \`type\` column) and can add child-specific columns. Generates a child model decorated with @STI(Parent), child serializers extending the parent's base serializers, a migration that ALTERs the parent table (not a new table), check constraints, a factory, and spec skeleton.
224
231
  ${INDENT}
225
- ${INDENT}If the child declares no additional columns, only the model file is generated — no migration is created. STI children share the parent's table, so a no-columns child requires no schema change. Add a migration only by passing positional field:type args, in which case the generator emits one with the appropriate check constraint. STI children never receive @SoftDelete() — soft delete is enforced at the parent level only — and the generator does not accept --no-soft-delete.
232
+ ${INDENT}If the child declares no additional columns, only the model file is generated — no migration is created. STI children share the parent's table, so a no-columns child requires no schema change. Add a migration only by passing positional field:type args, in which case the generator emits one with the appropriate check constraint. STI children cannot declare associations — declare them on the parent model instead. The generator rejects belongs_to columns. STI children never receive @SoftDelete() — soft delete is enforced at the parent level only — and the generator does not accept --no-soft-delete.
226
233
  ${INDENT}
227
234
  ${INDENT}The parent must already exist (typically generated with g:model --sti-base-serializer or g:resource --sti-base-serializer).
228
235
  ${INDENT}
@@ -256,7 +263,7 @@ ${INDENT}
256
263
  ${INDENT}Examples:
257
264
  ${INDENT} Room # extends src/app/models/Room.ts
258
265
  ${INDENT} Health/Coach # extends src/app/models/Health/Coach.ts`)
259
- .argument('[columnsWithTypes...]', columnsWithTypesDescription)
266
+ .argument('[columnsWithTypes...]', columnsWithTypesDescriptionForStiChild)
260
267
  .action(async (childModelName, extendsWord, parentModelName, columnsWithTypes, options) => {
261
268
  await initializeDreamApp({ bypassDreamIntegrityChecks: true, bypassDbConnectionsDuringInit: true });
262
269
  if (extendsWord !== 'extends')
@@ -8,6 +8,7 @@ import snakeify from '../snakeify.js';
8
8
  import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
9
9
  import uniq from '../uniq.js';
10
10
  import parseAttribute from './parseAttribute.js';
11
+ import validateStiChildColumns from './validateStiChildColumns.js';
11
12
  /**
12
13
  * Column names that are automatically emitted by the model generator (and
13
14
  * the migration generator). When the user passes any of these explicitly,
@@ -26,6 +27,8 @@ export function filterAutoGeneratedTimestampColumns(columnsWithTypes) {
26
27
  }
27
28
  export default function generateDreamContent(options) {
28
29
  const config = createModelConfig(options);
30
+ if (config.isSTI)
31
+ validateStiChildColumns(options.columnsWithTypes);
29
32
  // SoftDelete is incompatible with STI children, so it's only applied to
30
33
  // non-STI-child models.
31
34
  const includeSoftDelete = !config.isSTI && !!options.softDelete;
@@ -59,15 +62,19 @@ export default function generateDreamContent(options) {
59
62
  includeDeletedAt: includeSoftDelete || hasExplicitDeletedAt,
60
63
  });
61
64
  const decoBlock = decoratorsInUse
62
- ? `const deco = new Decorators<typeof ${config.modelClassName}>()`
63
- : `// Uncomment when adding decorators (@deco.BelongsTo, @deco.Validates, etc.):
65
+ ? `const deco = new Decorators<typeof ${config.modelClassName}>()
66
+
67
+ `
68
+ : config.isSTI
69
+ ? ''
70
+ : `// Uncomment when adding decorators (@deco.BelongsTo, @deco.Validates, etc.):
64
71
  // import { Decorators } from '@rvoh/dream'
65
- // const deco = new Decorators<typeof ${config.modelClassName}>()`;
66
- return `${importSection}
72
+ // const deco = new Decorators<typeof ${config.modelClassName}>()
67
73
 
68
- ${decoBlock}
74
+ `;
75
+ return `${importSection}
69
76
 
70
- ${classDeclaration}
77
+ ${decoBlock}${classDeclaration}
71
78
  ${tableMethod}${serializersMethod}${fieldsSection}
72
79
  }
73
80
  `.replace(/^\s*$/gm, '');
@@ -8,7 +8,8 @@ import globalClassNameFromFullyQualifiedModelName from '../globalClassNameFromFu
8
8
  import snakeify from '../snakeify.js';
9
9
  import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
10
10
  const STI_TYPE_COLUMN_NAME = 'type';
11
- const COLUMNS_TO_INDEX = [STI_TYPE_COLUMN_NAME];
11
+ const DELETED_AT_COLUMN_NAME = 'deleted_at';
12
+ const COLUMNS_TO_INDEX = [STI_TYPE_COLUMN_NAME, DELETED_AT_COLUMN_NAME];
12
13
  export default function generateMigrationContent({ connectionName = 'default', table, columnsWithTypes = [], primaryKeyType = 'bigserial', createOrAlter = 'create', stiChildClassName, softDelete = false, } = {}) {
13
14
  const altering = createOrAlter === 'alter';
14
15
  let requireCitextExtension = false;
@@ -25,8 +26,8 @@ export default function generateMigrationContent({ connectionName = 'default', t
25
26
  return name !== 'created_at' && name !== 'updated_at' && name !== 'deleted_at';
26
27
  });
27
28
  const emitDeletedAtColumn = !altering && (softDelete || userExplicitlyPassedDeletedAt);
28
- const { columnDefs, columnDrops, indexDefs, indexDrops } = processedColumnsWithTypes.reduce((acc, attributeDeclaration) => {
29
- const { columnDefs, columnDrops, indexDefs, indexDrops } = acc;
29
+ const { columnDefs, columnDrops, indexDefs } = processedColumnsWithTypes.reduce((acc, attributeDeclaration) => {
30
+ const { columnDefs, columnDrops, indexDefs } = acc;
30
31
  const [rawSegmentOne, _attributeType, ...descriptors] = attributeDeclaration.split(':');
31
32
  if (!rawSegmentOne)
32
33
  return acc;
@@ -129,13 +130,7 @@ export default function generateMigrationContent({ connectionName = 'default', t
129
130
  }
130
131
  columnDrops.push(`.dropColumn('${attributeName}')`);
131
132
  if (processedAttrType === 'belongsto' || COLUMNS_TO_INDEX.includes(attributeName)) {
132
- const indexName = `${table}_${attributeName}`;
133
- indexDefs.push(`await db.schema
134
- .createIndex('${indexName}')
135
- .on('${table}')
136
- .column('${attributeName}')
137
- .execute()`);
138
- indexDrops.push(`await db.schema.dropIndex('${indexName}').execute()`);
133
+ indexDefs.push(columnIndexStatement(table, attributeName));
139
134
  }
140
135
  if (stiChildClassName && !userWantsThisOptional && !arrayAttribute && !isBooleanColumn) {
141
136
  checkConstraints.push(`
@@ -149,7 +144,10 @@ export default function generateMigrationContent({ connectionName = 'default', t
149
144
  .execute()`);
150
145
  }
151
146
  return acc;
152
- }, { columnDefs: [], columnDrops: [], indexDefs: [], indexDrops: [] });
147
+ }, { columnDefs: [], columnDrops: [], indexDefs: [] });
148
+ if (emitDeletedAtColumn) {
149
+ indexDefs.push(columnIndexStatement(table, DELETED_AT_COLUMN_NAME));
150
+ }
153
151
  if (!table) {
154
152
  return `\
155
153
  import { Kysely, sql } from 'kysely'
@@ -196,7 +194,7 @@ ${citextExtension}${generateEnumStatements(columnsWithTypes)} await db.schema
196
194
 
197
195
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
198
196
  export async function down(db: Kysely<any>): Promise<void> {
199
- ${indexDrops.join(newlineIndent)}${indexDrops.length ? newlineIndent : ''}${altering
197
+ ${altering
200
198
  ? `await db.schema${newlineDoubleIndent}.alterTable('${table}')${columnDropLines}.execute()`
201
199
  : `await db.schema.dropTable('${table}').execute()`}${generateEnumDropStatements(columnsWithTypes)}
202
200
  }\
@@ -393,6 +391,14 @@ function lookupReferencesTable(snakeAssociationName, originalAssociationName) {
393
391
  function associationNameToForeignKey(associationName) {
394
392
  return snakeify(associationName.replace(/\//g, '_').replace(/_id$/, '') + '_id');
395
393
  }
394
+ function columnIndexStatement(table, attributeName) {
395
+ const indexName = `${table}_${attributeName}`;
396
+ return `await db.schema
397
+ .createIndex('${indexName}')
398
+ .on('${table}')
399
+ .column('${attributeName}')
400
+ .execute()`;
401
+ }
396
402
  export function optionalFromDescriptors(descriptors) {
397
403
  const optional = descriptors.at(-1) === 'optional';
398
404
  if (optional)
@@ -0,0 +1,7 @@
1
+ import parseAttribute from './parseAttribute.js';
2
+ export default function validateStiChildColumns(columnsWithTypes) {
3
+ const belongsToColumns = columnsWithTypes.filter(columnWithType => parseAttribute(columnWithType)?.normalizedAttributeType === 'belongsto');
4
+ if (belongsToColumns.length) {
5
+ throw new Error(`STI children cannot declare belongs_to associations. Declare associations on the STI parent instead. Unsupported columns: ${belongsToColumns.join(', ')}`);
6
+ }
7
+ }
@@ -11,8 +11,8 @@ 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, which may take an @alias suffix to rename the FK) properties like this:
15
- ${INDENT} title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal User:belongs_to
14
+ export const baseColumnsWithTypesDescription = `space separated snake-case properties like this:
15
+ ${INDENT} title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal
16
16
  ${INDENT}
17
17
  ${INDENT}all properties default to not nullable; null can be allowed by appending ':optional':
18
18
  ${INDENT} subtitle:string:optional
@@ -61,8 +61,11 @@ ${INDENT} omit the enum values to leverage an existing enum (omits the en
61
61
  ${INDENT} type:enum:room_types or type:enum:room_types:optional
62
62
  ${INDENT}
63
63
  ${INDENT} leveraging arrays, add the "[]" suffix, e.g.: type:enum[]:room_types:bathroom,kitchen,bedroom`;
64
- const columnsWithTypesDescription = baseColumnsWithTypesDescription +
65
- `
64
+ function buildColumnsWithTypesDescription(belongsToDescription) {
65
+ return `${baseColumnsWithTypesDescription}
66
+ ${belongsToDescription}`;
67
+ }
68
+ const columnsWithTypesDescription = buildColumnsWithTypesDescription(`
66
69
  ${INDENT}
67
70
  ${INDENT} - belongs_to:
68
71
  ${INDENT} ALWAYS use this instead of adding a raw uuid column for foreign keys. It creates the FK column, adds a database index,
@@ -80,9 +83,13 @@ ${INDENT} # @deco.
80
83
  ${INDENT} Messaging/Template@template:belongs_to # template_id column, templateId property, template association
81
84
  ${INDENT} # (strips the namespace from the property/association names while keeping
82
85
  ${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.`;
84
- const columnsWithTypesDescriptionForMigration = baseColumnsWithTypesDescription +
85
- `
86
+ ${INDENT} Aliasing also lets you declare multiple FKs to the same model in one generator call without column collisions.`);
87
+ export const columnsWithTypesDescriptionForStiChild = buildColumnsWithTypesDescription(`
88
+ ${INDENT}
89
+ ${INDENT} - belongs_to:
90
+ ${INDENT} NOT supported for STI children. STI children cannot declare associations; declare all BelongsTo,
91
+ ${INDENT} HasOne, and HasMany associations on the STI parent model instead.`);
92
+ const columnsWithTypesDescriptionForMigration = buildColumnsWithTypesDescription(`
86
93
  ${INDENT}
87
94
  ${INDENT} - belongs_to:
88
95
  ${INDENT} ALWAYS use this instead of adding a raw uuid column for foreign keys. It creates the FK column with an index.
@@ -95,7 +102,7 @@ ${INDENT} User:belongs_to:optional # nullable foreign key
95
102
  ${INDENT}
96
103
  ${INDENT} rename the FK column with Model@alias (snake_case alias becomes the column name):
97
104
  ${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)`;
105
+ ${INDENT} Messaging/Template@template:belongs_to # template_id column (strips the namespace from the column name)`);
99
106
  export default class DreamCLI {
100
107
  /**
101
108
  * Starts the Dream console
@@ -222,7 +229,7 @@ ${INDENT} Settings/CommunicationPreferences # src/app/models/Settings/Communi
222
229
  .alias('g:sti-child')
223
230
  .description(`Generates an STI (Single Table Inheritance) child model that extends an existing parent model. The child shares the parent's database table (discriminated by the \`type\` column) and can add child-specific columns. Generates a child model decorated with @STI(Parent), child serializers extending the parent's base serializers, a migration that ALTERs the parent table (not a new table), check constraints, a factory, and spec skeleton.
224
231
  ${INDENT}
225
- ${INDENT}If the child declares no additional columns, only the model file is generated — no migration is created. STI children share the parent's table, so a no-columns child requires no schema change. Add a migration only by passing positional field:type args, in which case the generator emits one with the appropriate check constraint. STI children never receive @SoftDelete() — soft delete is enforced at the parent level only — and the generator does not accept --no-soft-delete.
232
+ ${INDENT}If the child declares no additional columns, only the model file is generated — no migration is created. STI children share the parent's table, so a no-columns child requires no schema change. Add a migration only by passing positional field:type args, in which case the generator emits one with the appropriate check constraint. STI children cannot declare associations — declare them on the parent model instead. The generator rejects belongs_to columns. STI children never receive @SoftDelete() — soft delete is enforced at the parent level only — and the generator does not accept --no-soft-delete.
226
233
  ${INDENT}
227
234
  ${INDENT}The parent must already exist (typically generated with g:model --sti-base-serializer or g:resource --sti-base-serializer).
228
235
  ${INDENT}
@@ -256,7 +263,7 @@ ${INDENT}
256
263
  ${INDENT}Examples:
257
264
  ${INDENT} Room # extends src/app/models/Room.ts
258
265
  ${INDENT} Health/Coach # extends src/app/models/Health/Coach.ts`)
259
- .argument('[columnsWithTypes...]', columnsWithTypesDescription)
266
+ .argument('[columnsWithTypes...]', columnsWithTypesDescriptionForStiChild)
260
267
  .action(async (childModelName, extendsWord, parentModelName, columnsWithTypes, options) => {
261
268
  await initializeDreamApp({ bypassDreamIntegrityChecks: true, bypassDbConnectionsDuringInit: true });
262
269
  if (extendsWord !== 'extends')
@@ -8,6 +8,7 @@ import snakeify from '../snakeify.js';
8
8
  import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
9
9
  import uniq from '../uniq.js';
10
10
  import parseAttribute from './parseAttribute.js';
11
+ import validateStiChildColumns from './validateStiChildColumns.js';
11
12
  /**
12
13
  * Column names that are automatically emitted by the model generator (and
13
14
  * the migration generator). When the user passes any of these explicitly,
@@ -26,6 +27,8 @@ export function filterAutoGeneratedTimestampColumns(columnsWithTypes) {
26
27
  }
27
28
  export default function generateDreamContent(options) {
28
29
  const config = createModelConfig(options);
30
+ if (config.isSTI)
31
+ validateStiChildColumns(options.columnsWithTypes);
29
32
  // SoftDelete is incompatible with STI children, so it's only applied to
30
33
  // non-STI-child models.
31
34
  const includeSoftDelete = !config.isSTI && !!options.softDelete;
@@ -59,15 +62,19 @@ export default function generateDreamContent(options) {
59
62
  includeDeletedAt: includeSoftDelete || hasExplicitDeletedAt,
60
63
  });
61
64
  const decoBlock = decoratorsInUse
62
- ? `const deco = new Decorators<typeof ${config.modelClassName}>()`
63
- : `// Uncomment when adding decorators (@deco.BelongsTo, @deco.Validates, etc.):
65
+ ? `const deco = new Decorators<typeof ${config.modelClassName}>()
66
+
67
+ `
68
+ : config.isSTI
69
+ ? ''
70
+ : `// Uncomment when adding decorators (@deco.BelongsTo, @deco.Validates, etc.):
64
71
  // import { Decorators } from '@rvoh/dream'
65
- // const deco = new Decorators<typeof ${config.modelClassName}>()`;
66
- return `${importSection}
72
+ // const deco = new Decorators<typeof ${config.modelClassName}>()
67
73
 
68
- ${decoBlock}
74
+ `;
75
+ return `${importSection}
69
76
 
70
- ${classDeclaration}
77
+ ${decoBlock}${classDeclaration}
71
78
  ${tableMethod}${serializersMethod}${fieldsSection}
72
79
  }
73
80
  `.replace(/^\s*$/gm, '');
@@ -8,7 +8,8 @@ import globalClassNameFromFullyQualifiedModelName from '../globalClassNameFromFu
8
8
  import snakeify from '../snakeify.js';
9
9
  import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
10
10
  const STI_TYPE_COLUMN_NAME = 'type';
11
- const COLUMNS_TO_INDEX = [STI_TYPE_COLUMN_NAME];
11
+ const DELETED_AT_COLUMN_NAME = 'deleted_at';
12
+ const COLUMNS_TO_INDEX = [STI_TYPE_COLUMN_NAME, DELETED_AT_COLUMN_NAME];
12
13
  export default function generateMigrationContent({ connectionName = 'default', table, columnsWithTypes = [], primaryKeyType = 'bigserial', createOrAlter = 'create', stiChildClassName, softDelete = false, } = {}) {
13
14
  const altering = createOrAlter === 'alter';
14
15
  let requireCitextExtension = false;
@@ -25,8 +26,8 @@ export default function generateMigrationContent({ connectionName = 'default', t
25
26
  return name !== 'created_at' && name !== 'updated_at' && name !== 'deleted_at';
26
27
  });
27
28
  const emitDeletedAtColumn = !altering && (softDelete || userExplicitlyPassedDeletedAt);
28
- const { columnDefs, columnDrops, indexDefs, indexDrops } = processedColumnsWithTypes.reduce((acc, attributeDeclaration) => {
29
- const { columnDefs, columnDrops, indexDefs, indexDrops } = acc;
29
+ const { columnDefs, columnDrops, indexDefs } = processedColumnsWithTypes.reduce((acc, attributeDeclaration) => {
30
+ const { columnDefs, columnDrops, indexDefs } = acc;
30
31
  const [rawSegmentOne, _attributeType, ...descriptors] = attributeDeclaration.split(':');
31
32
  if (!rawSegmentOne)
32
33
  return acc;
@@ -129,13 +130,7 @@ export default function generateMigrationContent({ connectionName = 'default', t
129
130
  }
130
131
  columnDrops.push(`.dropColumn('${attributeName}')`);
131
132
  if (processedAttrType === 'belongsto' || COLUMNS_TO_INDEX.includes(attributeName)) {
132
- const indexName = `${table}_${attributeName}`;
133
- indexDefs.push(`await db.schema
134
- .createIndex('${indexName}')
135
- .on('${table}')
136
- .column('${attributeName}')
137
- .execute()`);
138
- indexDrops.push(`await db.schema.dropIndex('${indexName}').execute()`);
133
+ indexDefs.push(columnIndexStatement(table, attributeName));
139
134
  }
140
135
  if (stiChildClassName && !userWantsThisOptional && !arrayAttribute && !isBooleanColumn) {
141
136
  checkConstraints.push(`
@@ -149,7 +144,10 @@ export default function generateMigrationContent({ connectionName = 'default', t
149
144
  .execute()`);
150
145
  }
151
146
  return acc;
152
- }, { columnDefs: [], columnDrops: [], indexDefs: [], indexDrops: [] });
147
+ }, { columnDefs: [], columnDrops: [], indexDefs: [] });
148
+ if (emitDeletedAtColumn) {
149
+ indexDefs.push(columnIndexStatement(table, DELETED_AT_COLUMN_NAME));
150
+ }
153
151
  if (!table) {
154
152
  return `\
155
153
  import { Kysely, sql } from 'kysely'
@@ -196,7 +194,7 @@ ${citextExtension}${generateEnumStatements(columnsWithTypes)} await db.schema
196
194
 
197
195
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
198
196
  export async function down(db: Kysely<any>): Promise<void> {
199
- ${indexDrops.join(newlineIndent)}${indexDrops.length ? newlineIndent : ''}${altering
197
+ ${altering
200
198
  ? `await db.schema${newlineDoubleIndent}.alterTable('${table}')${columnDropLines}.execute()`
201
199
  : `await db.schema.dropTable('${table}').execute()`}${generateEnumDropStatements(columnsWithTypes)}
202
200
  }\
@@ -393,6 +391,14 @@ function lookupReferencesTable(snakeAssociationName, originalAssociationName) {
393
391
  function associationNameToForeignKey(associationName) {
394
392
  return snakeify(associationName.replace(/\//g, '_').replace(/_id$/, '') + '_id');
395
393
  }
394
+ function columnIndexStatement(table, attributeName) {
395
+ const indexName = `${table}_${attributeName}`;
396
+ return `await db.schema
397
+ .createIndex('${indexName}')
398
+ .on('${table}')
399
+ .column('${attributeName}')
400
+ .execute()`;
401
+ }
396
402
  export function optionalFromDescriptors(descriptors) {
397
403
  const optional = descriptors.at(-1) === 'optional';
398
404
  if (optional)
@@ -0,0 +1,7 @@
1
+ import parseAttribute from './parseAttribute.js';
2
+ export default function validateStiChildColumns(columnsWithTypes) {
3
+ const belongsToColumns = columnsWithTypes.filter(columnWithType => parseAttribute(columnWithType)?.normalizedAttributeType === 'belongsto');
4
+ if (belongsToColumns.length) {
5
+ throw new Error(`STI children cannot declare belongs_to associations. Declare associations on the STI parent instead. Unsupported columns: ${belongsToColumns.join(', ')}`);
6
+ }
7
+ }
@@ -14,7 +14,8 @@ export type SpawnOptions = Omit<NodeSpawnOptions, 'shell'> & {
14
14
  args?: string[];
15
15
  };
16
16
  export declare const CLI_INDENT = " ";
17
- export declare 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:\n title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal User:belongs_to\n \n all properties default to not nullable; null can be allowed by appending ':optional':\n subtitle:string:optional\n \n supported types:\n - uuid:\n - uuid[]:\n a column optimized for storing UUIDs\n \n - citext:\n - citext[]:\n case insensitive text (indexes and queries are automatically case insensitive)\n \n - encrypted:\n encrypted text (used in conjunction with the @deco.Encrypted decorator)\n \n - string:\n - string[]:\n varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional\n \n - text\n - text[]\n - date\n - date[]\n - datetime\n - datetime[]\n - time\n - time[]\n - timetz\n - timetz[]\n - integer\n - integer[]\n \n - decimal:\n - decimal[]:\n precision,scale is required, e.g.: volume:decimal:3,2 or volume:decimal:3,2:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: volume:decimal[]:3,2\n \n - enum:\n - enum[]:\n include the enum name to automatically create the enum:\n type:enum:room_types:bathroom,kitchen,bedroom or type:enum:room_types:bathroom,kitchen,bedroom:optional\n \n omit the enum values to leverage an existing enum (omits the enum type creation):\n type:enum:room_types or type:enum:room_types:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: type:enum[]:room_types:bathroom,kitchen,bedroom";
17
+ export declare const baseColumnsWithTypesDescription = "space separated snake-case properties like this:\n title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal\n \n all properties default to not nullable; null can be allowed by appending ':optional':\n subtitle:string:optional\n \n supported types:\n - uuid:\n - uuid[]:\n a column optimized for storing UUIDs\n \n - citext:\n - citext[]:\n case insensitive text (indexes and queries are automatically case insensitive)\n \n - encrypted:\n encrypted text (used in conjunction with the @deco.Encrypted decorator)\n \n - string:\n - string[]:\n varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional\n \n - text\n - text[]\n - date\n - date[]\n - datetime\n - datetime[]\n - time\n - time[]\n - timetz\n - timetz[]\n - integer\n - integer[]\n \n - decimal:\n - decimal[]:\n precision,scale is required, e.g.: volume:decimal:3,2 or volume:decimal:3,2:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: volume:decimal[]:3,2\n \n - enum:\n - enum[]:\n include the enum name to automatically create the enum:\n type:enum:room_types:bathroom,kitchen,bedroom or type:enum:room_types:bathroom,kitchen,bedroom:optional\n \n omit the enum values to leverage an existing enum (omits the enum type creation):\n type:enum:room_types or type:enum:room_types:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: type:enum[]:room_types:bathroom,kitchen,bedroom";
18
+ export declare const columnsWithTypesDescriptionForStiChild: string;
18
19
  export default class DreamCLI {
19
20
  /**
20
21
  * Starts the Dream console
@@ -0,0 +1 @@
1
+ export default function validateStiChildColumns(columnsWithTypes: string[]): void;