@rvoh/dream 2.5.3 → 2.5.5

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 (198) hide show
  1. package/dist/cjs/src/Dream.js +4 -1
  2. package/dist/cjs/src/cli/index.js +155 -46
  3. package/dist/cjs/src/decorators/field/sortable/hooks/beforeSortableSave.js +4 -4
  4. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +1 -0
  5. package/dist/cjs/src/dream/internal/findOrCreateBy.js +3 -2
  6. package/dist/cjs/src/dream/internal/reload.js +1 -0
  7. package/dist/cjs/src/dream/internal/similarity/SimilarityBuilder.js +4 -1
  8. package/dist/cjs/src/utils/datetime/DateTime.js +3 -0
  9. package/dist/esm/src/Dream.js +4 -1
  10. package/dist/esm/src/cli/index.js +155 -46
  11. package/dist/esm/src/decorators/field/sortable/hooks/beforeSortableSave.js +4 -4
  12. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +1 -0
  13. package/dist/esm/src/dream/internal/findOrCreateBy.js +3 -2
  14. package/dist/esm/src/dream/internal/reload.js +1 -0
  15. package/dist/esm/src/dream/internal/similarity/SimilarityBuilder.js +4 -1
  16. package/dist/esm/src/utils/datetime/DateTime.js +3 -0
  17. package/dist/types/src/Dream.d.ts +2 -0
  18. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +1 -0
  19. package/dist/types/src/types/dream.d.ts +1 -0
  20. package/dist/types/src/types/dream.ts +1 -0
  21. package/docs/classes/db.DreamMigrationHelpers.html +9 -9
  22. package/docs/classes/db.KyselyQueryDriver.html +32 -32
  23. package/docs/classes/db.PostgresQueryDriver.html +33 -33
  24. package/docs/classes/db.QueryDriverBase.html +31 -31
  25. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  26. package/docs/classes/errors.ColumnOverflow.html +3 -3
  27. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  28. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  29. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  30. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  31. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  32. package/docs/classes/errors.InvalidClockTime.html +2 -2
  33. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  34. package/docs/classes/errors.InvalidDateTime.html +2 -2
  35. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  36. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  37. package/docs/classes/errors.NotNullViolation.html +3 -3
  38. package/docs/classes/errors.RecordNotFound.html +3 -3
  39. package/docs/classes/errors.ValidationError.html +3 -3
  40. package/docs/classes/index.CalendarDate.html +33 -33
  41. package/docs/classes/index.ClockTime.html +32 -32
  42. package/docs/classes/index.ClockTimeTz.html +35 -35
  43. package/docs/classes/index.DateTime.html +86 -86
  44. package/docs/classes/index.Decorators.html +19 -19
  45. package/docs/classes/index.Dream.html +120 -118
  46. package/docs/classes/index.DreamApp.html +5 -5
  47. package/docs/classes/index.DreamTransaction.html +2 -2
  48. package/docs/classes/index.Env.html +2 -2
  49. package/docs/classes/index.Query.html +56 -56
  50. package/docs/classes/system.CliFileWriter.html +4 -4
  51. package/docs/classes/system.DreamBin.html +2 -2
  52. package/docs/classes/system.DreamCLI.html +6 -6
  53. package/docs/classes/system.DreamImporter.html +2 -2
  54. package/docs/classes/system.DreamLogos.html +2 -2
  55. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  56. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  57. package/docs/classes/system.PathHelpers.html +3 -3
  58. package/docs/classes/utils.Encrypt.html +2 -2
  59. package/docs/classes/utils.Range.html +2 -2
  60. package/docs/functions/db.closeAllDbConnections.html +1 -1
  61. package/docs/functions/db.dreamDbConnections.html +1 -1
  62. package/docs/functions/db.untypedDb.html +1 -1
  63. package/docs/functions/db.validateColumn.html +1 -1
  64. package/docs/functions/db.validateTable.html +1 -1
  65. package/docs/functions/errors.pgErrorType.html +1 -1
  66. package/docs/functions/index.DreamSerializer.html +1 -1
  67. package/docs/functions/index.ObjectSerializer.html +1 -1
  68. package/docs/functions/index.ReplicaSafe.html +1 -1
  69. package/docs/functions/index.STI.html +1 -1
  70. package/docs/functions/index.SoftDelete.html +1 -1
  71. package/docs/functions/utils.camelize.html +1 -1
  72. package/docs/functions/utils.capitalize.html +1 -1
  73. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  74. package/docs/functions/utils.compact.html +1 -1
  75. package/docs/functions/utils.groupBy.html +1 -1
  76. package/docs/functions/utils.hyphenize.html +1 -1
  77. package/docs/functions/utils.intersection.html +1 -1
  78. package/docs/functions/utils.isEmpty.html +1 -1
  79. package/docs/functions/utils.normalizeUnicode.html +1 -1
  80. package/docs/functions/utils.pascalize.html +1 -1
  81. package/docs/functions/utils.percent.html +1 -1
  82. package/docs/functions/utils.range.html +1 -1
  83. package/docs/functions/utils.round.html +1 -1
  84. package/docs/functions/utils.sanitizeString.html +1 -1
  85. package/docs/functions/utils.snakeify.html +1 -1
  86. package/docs/functions/utils.sort.html +1 -1
  87. package/docs/functions/utils.sortBy.html +1 -1
  88. package/docs/functions/utils.sortObjectByKey.html +1 -1
  89. package/docs/functions/utils.sortObjectByValue.html +1 -1
  90. package/docs/functions/utils.uncapitalize.html +1 -1
  91. package/docs/functions/utils.uniq.html +1 -1
  92. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  93. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  94. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  95. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  96. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  97. package/docs/interfaces/types.DecoratorContext.html +2 -2
  98. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  99. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  100. package/docs/interfaces/types.DurationObject.html +2 -2
  101. package/docs/interfaces/types.EncryptOptions.html +2 -2
  102. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  103. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  104. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  105. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  106. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  107. package/docs/types/openapi.OpenapiFormats.html +1 -1
  108. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  109. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  110. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  111. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  112. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  113. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  114. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  115. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  116. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  117. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  118. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  119. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  120. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  121. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  122. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  123. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  124. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  125. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  126. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  127. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  128. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  129. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  130. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  131. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  132. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  133. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  134. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  135. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  136. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  137. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  138. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  139. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  140. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  141. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  142. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  143. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  144. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  145. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  146. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  147. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  148. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  149. package/docs/types/types.CalendarDateObject.html +1 -1
  150. package/docs/types/types.Camelized.html +1 -1
  151. package/docs/types/types.ClockTimeObject.html +1 -1
  152. package/docs/types/types.DbConnectionType.html +1 -1
  153. package/docs/types/types.DbTypes.html +1 -1
  154. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  155. package/docs/types/types.DreamAttributes.html +1 -1
  156. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  157. package/docs/types/types.DreamClassColumn.html +1 -1
  158. package/docs/types/types.DreamColumn.html +1 -1
  159. package/docs/types/types.DreamColumnNames.html +1 -1
  160. package/docs/types/types.DreamLogLevel.html +1 -1
  161. package/docs/types/types.DreamLogger.html +2 -2
  162. package/docs/types/types.DreamModelSerializerType.html +1 -1
  163. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  164. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  165. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  166. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  167. package/docs/types/types.DreamSerializable.html +1 -1
  168. package/docs/types/types.DreamSerializableArray.html +1 -1
  169. package/docs/types/types.DreamSerializerKey.html +1 -1
  170. package/docs/types/types.DreamSerializers.html +1 -1
  171. package/docs/types/types.DreamVirtualColumns.html +1 -1
  172. package/docs/types/types.DurationUnit.html +1 -1
  173. package/docs/types/types.EncryptAlgorithm.html +1 -1
  174. package/docs/types/types.HasManyStatement.html +1 -1
  175. package/docs/types/types.HasOneStatement.html +1 -1
  176. package/docs/types/types.Hyphenized.html +1 -1
  177. package/docs/types/types.Pascalized.html +1 -1
  178. package/docs/types/types.PrimaryKeyType.html +1 -1
  179. package/docs/types/types.RoundingPrecision.html +1 -1
  180. package/docs/types/types.SerializerCasing.html +1 -1
  181. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  182. package/docs/types/types.Snakeified.html +1 -1
  183. package/docs/types/types.StrictInterface.html +1 -1
  184. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  185. package/docs/types/types.UpdateableProperties.html +1 -1
  186. package/docs/types/types.ValidationType.html +1 -1
  187. package/docs/types/types.ViewModel.html +2 -2
  188. package/docs/types/types.ViewModelClass.html +1 -1
  189. package/docs/types/types.WeekdayName.html +1 -1
  190. package/docs/types/types.WhereStatementForDream.html +1 -1
  191. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  192. package/docs/variables/index.DreamConst.html +1 -1
  193. package/docs/variables/index.ops.html +1 -1
  194. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  195. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  196. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  197. package/docs/variables/system.primaryKeyTypes.html +1 -1
  198. package/package.json +1 -1
@@ -64,18 +64,24 @@ const columnsWithTypesDescription = baseColumnsWithTypesDescription +
64
64
  `
65
65
  ${INDENT}
66
66
  ${INDENT} - belongs_to:
67
- ${INDENT} not only adds a foreign key to the migration, but also adds a BelongsTo association to the generated model:
67
+ ${INDENT} ALWAYS use this instead of adding a raw uuid column for foreign keys. It creates the FK column, adds a database index,
68
+ ${INDENT} AND generates the @deco.BelongsTo association and typed property on the model. A raw uuid column does none of this.
68
69
  ${INDENT}
69
- ${INDENT} include the fully qualified model name, e.g., if the Coach model is in src/app/models/Health/Coach:
70
- ${INDENT} Health/Coach:belongs_to`;
70
+ ${INDENT} use the fully qualified model name (matching its path under src/app/models/):
71
+ ${INDENT} User:belongs_to # creates user_id column + BelongsTo association
72
+ ${INDENT} Health/Coach:belongs_to # creates health_coach_id column + BelongsTo association
73
+ ${INDENT} User:belongs_to:optional # nullable foreign key (for optional associations)`;
71
74
  const columnsWithTypesDescriptionForMigration = baseColumnsWithTypesDescription +
72
75
  `
73
76
  ${INDENT}
74
77
  ${INDENT} - belongs_to:
75
- ${INDENT} adds a foreign key to migration
78
+ ${INDENT} ALWAYS use this instead of adding a raw uuid column for foreign keys. It creates the FK column with an index.
79
+ ${INDENT} Unlike in g:model/g:resource, this does NOT add a BelongsTo association (no model is generated).
76
80
  ${INDENT}
77
- ${INDENT} include the fully qualified model name, e.g., if the Coach model is in src/app/models/Health/Coach:
78
- ${INDENT} Health/Coach:belongs_to`;
81
+ ${INDENT} use the fully qualified model name (matching its path under src/app/models/):
82
+ ${INDENT} User:belongs_to # creates user_id column with index
83
+ ${INDENT} Health/Coach:belongs_to # creates health_coach_id column with index
84
+ ${INDENT} User:belongs_to:optional # nullable foreign key`;
79
85
  export default class DreamCLI {
80
86
  /**
81
87
  * Starts the Dream console
@@ -90,8 +96,8 @@ export default class DreamCLI {
90
96
  static provide(program, { initializeDreamApp, seedDb, }) {
91
97
  program
92
98
  .command('sync')
93
- .description('Generates types from the current state of the database.')
94
- .option('--schema-only', 'sync database schema types only', false)
99
+ .description('Regenerates TypeScript types (types/db.ts, types/dream.ts) from the current database schema. Run this after changing associations, serializers, or enum types.')
100
+ .option('--schema-only', 'only regenerate database schema types, skipping any custom sync actions', false)
95
101
  .action(async (options) => {
96
102
  await initializeDreamApp({ bypassDreamIntegrityChecks: true });
97
103
  await DreamBin.sync(() => { }, options);
@@ -119,9 +125,25 @@ export default class DreamCLI {
119
125
  program
120
126
  .command('generate:migration')
121
127
  .alias('g:migration')
122
- .description('Generates a new migration file (prefer g:resource or g:model if creating a table for a new model).')
123
- .argument('<migrationName>', 'end with -to-table-name or -from-table-name to prepopulate with an alterTable command')
124
- .option('--connection-name <connectionName>', 'the connection name you wish to use for your migration')
128
+ .description(`Generates a new Kysely migration file for schema changes. Use this for altering existing tables (adding/removing columns, indexes, constraints). Prefer g:resource or g:model when creating a new model, since they generate the migration along with the model, serializer, and spec files.
129
+ ${INDENT}
130
+ ${INDENT}Examples:
131
+ ${INDENT} # Add columns to an existing table (suffix with -to-<table_name> for auto alterTable scaffolding)
132
+ ${INDENT} pnpm psy g:migration add-timezone-to-users timezone:string
133
+ ${INDENT} pnpm psy g:migration add-bio-to-users bio:text:optional avatar_url:string:optional
134
+ ${INDENT}
135
+ ${INDENT} # Remove columns (suffix with -from-<table_name>)
136
+ ${INDENT} pnpm psy g:migration remove-legacy-fields-from-posts
137
+ ${INDENT}
138
+ ${INDENT} # General schema change (no table suffix — generates empty up/down methods)
139
+ ${INDENT} pnpm psy g:migration create-unique-index-on-invitations`)
140
+ .argument('<migrationName>', `Kebab-case name describing the change. End with -to-<table_name> or -from-<table_name> to auto-generate an alterTable scaffold for that table.
141
+ ${INDENT}
142
+ ${INDENT}Examples:
143
+ ${INDENT} add-phone-to-users # scaffolds alterTable('users', ...)
144
+ ${INDENT} remove-status-from-posts # scaffolds alterTable('posts', ...)
145
+ ${INDENT} create-join-table-host-places # empty migration (no table suffix match)`)
146
+ .option('--connection-name <connectionName>', 'the database connection to use for this migration. Only needed for multi-database setups; defaults to "default"')
125
147
  .argument('[columnsWithTypes...]', columnsWithTypesDescriptionForMigration)
126
148
  .action(async (migrationName, columnsWithTypes, options) => {
127
149
  await initializeDreamApp({ bypassDreamIntegrityChecks: true, bypassDbConnectionsDuringInit: true });
@@ -133,15 +155,47 @@ export default class DreamCLI {
133
155
  .alias('g:model')
134
156
  .alias('generate:dream')
135
157
  .alias('g:dream')
136
- .description('Generates a Dream model with corresponding spec factory, serializer, and migration.')
137
- .option('--no-serializer')
138
- .option('--connection-name <connectionName>', 'the db connection you want this attached to (defaults to the default db connection)', 'default')
139
- .option('--sti-base-serializer', 'creates a generically typed base serializer that includes the child type in the output so consuming applications can determine shape based on type', false)
140
- .option('--table-name <tableName>', 'explicit table name to use instead of the auto-generated one (useful when model namespaces produce long names)')
141
- .option('--model-name <modelName>', 'explicit model class name to use instead of the auto-generated one (e.g. --model-name=Kitchen for Room/Kitchen)')
142
- .option('--admin-serializers', 'generate admin serializer variants (AdminSerializer and AdminSummarySerializer) in addition to the default serializers', false)
143
- .option('--internal-serializers', 'generate internal serializer variants (InternalSerializer and InternalSummarySerializer) in addition to the default serializers', false)
144
- .argument('<modelName>', 'the name of the model to create, e.g. Post or Settings/CommunicationPreferences')
158
+ .description(`Generates a Dream model with corresponding spec factory, serializer, and migration. Use this when the model will NOT be accessible via HTTP requests (e.g., internal join tables, data models with no API). For HTTP-accessible models, prefer g:resource which also generates a controller and specs.
159
+ ${INDENT}
160
+ ${INDENT}Examples:
161
+ ${INDENT} # Simple model
162
+ ${INDENT} pnpm psy g:model Tag value:citext
163
+ ${INDENT}
164
+ ${INDENT} # Join table model
165
+ ${INDENT} pnpm psy g:model HostPlace Host:belongs_to Place:belongs_to
166
+ ${INDENT}
167
+ ${INDENT} # STI parent model (use with g:sti-child for children)
168
+ ${INDENT} pnpm psy g:model --sti-base-serializer Room Place:belongs_to type:enum:room_types:Bathroom,Bedroom deleted_at:datetime:optional`)
169
+ .option('--no-serializer', 'skip serializer generation. Useful for internal models that will never be serialized in an API response (e.g., join tables, audit logs)')
170
+ .option('--connection-name <connectionName>', 'the name of the database connection to use for the model. Only needed for multi-database setups; defaults to "default"', 'default')
171
+ .option('--sti-base-serializer', `Creates generically typed base serializers (default and summary) that accept a \`StiChildClass\` parameter and include the \`type\` attribute with a per-child enum constraint. This allows consuming applications to determine the response shape based on the STI type discriminator.
172
+ ${INDENT}
173
+ ${INDENT}Use this when generating the parent model of an STI hierarchy. After generating the parent, use g:sti-child for each child type.
174
+ ${INDENT}
175
+ ${INDENT}Example:
176
+ ${INDENT} # CRITICAL: the type enums must exactly match the class names of the STI children
177
+ ${INDENT} pnpm psy g:model --sti-base-serializer Rental type:enum:place_types:Apartment,House,Condo
178
+ ${INDENT} # STI children subsequently generated using the g:sti-child generator (note the use of \`--model-name\` to generate class names that match the \`type\` column, e.g., "Apartment" instead of the "RentalApartment" default):
179
+ ${INDENT} pnpm psy g:sti-child --model-name=Apartment Rental/Apartment extends Rental
180
+ ${INDENT} pnpm psy g:sti-child --model-name=House Rental/House extends Rental
181
+ ${INDENT} pnpm psy g:sti-child --model-name=Condo Rental/Condo extends Rental`, false)
182
+ .option('--table-name <tableName>', `Explicit table name to use instead of the auto-generated one. Useful when model namespaces produce long or awkward table names.
183
+ ${INDENT}
184
+ ${INDENT}Example:
185
+ ${INDENT} pnpm psy g:model --table-name=notif_prefs Settings/NotificationPreferences User:belongs_to`)
186
+ .option('--model-name <modelName>', `Explicit model class name to use instead of the one auto-derived from the model path. Useful when the path segments don't match the desired class name.
187
+ ${INDENT}
188
+ ${INDENT}Example:
189
+ ${INDENT} pnpm psy g:model --model-name=GroupDanceLesson Lesson/Dance/Group
190
+ ${INDENT} # model is named GroupDanceLesson instead of LessonDanceGroup`)
191
+ .option('--admin-serializers', 'also generate AdminSerializer and AdminSummarySerializer variants for admin-facing API endpoints that may expose additional fields', false)
192
+ .option('--internal-serializers', 'also generate InternalSerializer and InternalSummarySerializer variants for internal API endpoints that may expose additional fields', false)
193
+ .argument('<modelName>', `The fully qualified model name, using / for namespacing. This determines the model class name (may be overridden with \`--model-name\`), table name, and file path under src/app/models/.
194
+ ${INDENT}
195
+ ${INDENT}Examples:
196
+ ${INDENT} Post # src/app/models/Post.ts, table: posts
197
+ ${INDENT} HostPlace # src/app/models/HostPlace.ts, table: host_places
198
+ ${INDENT} Settings/CommunicationPreferences # src/app/models/Settings/CommunicationPreferences.ts`)
145
199
  .argument('[columnsWithTypes...]', columnsWithTypesDescription)
146
200
  .action(async (modelName, columnsWithTypes, options) => {
147
201
  await initializeDreamApp({ bypassDreamIntegrityChecks: true, bypassDbConnectionsDuringInit: true });
@@ -151,17 +205,40 @@ export default class DreamCLI {
151
205
  program
152
206
  .command('generate:sti-child')
153
207
  .alias('g:sti-child')
154
- .description('Generates a Dream model that extends another Dream model, leveraging STI (single table inheritance), with corresponding spec factory, serializer, and migration.')
155
- .option('--no-serializer')
156
- .option('--connection-name', 'the db connection you want this model attached to (defaults to the default connection)', 'default')
157
- .option('--model-name <modelName>', 'explicit model class name to use instead of the auto-generated one (e.g. --model-name=Kitchen for Room/Kitchen)')
158
- .option('--admin-serializers', 'generate admin serializer variants (AdminSerializer and AdminSummarySerializer) in addition to the default serializers', false)
159
- .option('--internal-serializers', 'generate internal serializer variants (InternalSerializer and InternalSummarySerializer) in addition to the default serializers', false)
160
- .argument('<childModelName>', 'the name of the model to create, e.g. Post or Settings/CommunicationPreferences')
161
- .argument('<extends>', 'just the word "extends"')
162
- .argument('<parentModelName>', `fully qualified name of the parent model, e.g.:
163
- ${INDENT} to extend the Room model in src/app/models/Room: Room
164
- ${INDENT} to extend the Coach model in src/app/models/Health/Coach: Health/Coach`)
208
+ .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.
209
+ ${INDENT}
210
+ ${INDENT}The parent must already exist (typically generated with g:model --sti-base-serializer or g:resource --sti-base-serializer).
211
+ ${INDENT}
212
+ ${INDENT}Examples:
213
+ ${INDENT} # Child with an enum column
214
+ ${INDENT} pnpm psy g:sti-child --model-name=Bathroom Room/Bathroom extends Room bath_or_shower_style:enum:bath_or_shower_styles:bath,shower,none
215
+ ${INDENT}
216
+ ${INDENT} # Child with an enum array column
217
+ ${INDENT} pnpm psy g:sti-child --model-name=Bedroom Room/Bedroom extends Room bed_types:enum[]:bed_types:twin,queen,king
218
+ ${INDENT}
219
+ ${INDENT} # Child with no additional columns
220
+ ${INDENT} pnpm psy g:sti-child --model-name=Kitchen Room/Kitchen extends Room`)
221
+ .option('--no-serializer', 'skip serializer generation. Useful if the child uses the parent serializer directly or serialization is handled elsewhere')
222
+ .option('--connection-name', 'the name of the database connection to use for the model. Only needed for multi-database setups; defaults to "default"', 'default')
223
+ .option('--model-name <modelName>', `Explicit model class name to use instead of the one auto-derived from the model path. Useful when the path segments don't match the desired class name.
224
+ ${INDENT}
225
+ ${INDENT}Example:
226
+ ${INDENT} pnpm psy g:sti-child --model-name=GroupDanceLesson Lesson/Dance/Group extends Lesson
227
+ ${INDENT} # model is named GroupDanceLesson instead of LessonDanceGroup`)
228
+ .option('--admin-serializers', 'also generate AdminSerializer and AdminSummarySerializer variants for admin-facing API endpoints that may expose additional fields', false)
229
+ .option('--internal-serializers', 'also generate InternalSerializer and InternalSummarySerializer variants for internal API endpoints that may expose additional fields', false)
230
+ .argument('<childModelName>', `The namespaced child model path. By convention, children are nested under the parent name.
231
+ ${INDENT}
232
+ ${INDENT}Examples:
233
+ ${INDENT} Room/Bathroom # src/app/models/Room/Bathroom.ts
234
+ ${INDENT} Room/Bedroom # src/app/models/Room/Bedroom.ts
235
+ ${INDENT} Vehicle/Truck # src/app/models/Vehicle/Truck.ts`)
236
+ .argument('<extends>', 'the literal word "extends" (required syntax)')
237
+ .argument('<parentModelName>', `Fully qualified name of the parent STI model to extend. Must match the parent's path under src/app/models/.
238
+ ${INDENT}
239
+ ${INDENT}Examples:
240
+ ${INDENT} Room # extends src/app/models/Room.ts
241
+ ${INDENT} Health/Coach # extends src/app/models/Health/Coach.ts`)
165
242
  .argument('[columnsWithTypes...]', columnsWithTypesDescription)
166
243
  .action(async (childModelName, extendsWord, parentModelName, columnsWithTypes, options) => {
167
244
  await initializeDreamApp({ bypassDreamIntegrityChecks: true, bypassDbConnectionsDuringInit: true });
@@ -173,8 +250,18 @@ ${INDENT} to extend the Coach model in src/app/models/Health/Coach: Health/Co
173
250
  program
174
251
  .command('generate:encryption-key')
175
252
  .alias('g:encryption-key')
176
- .description('Generates a new encryption key for any of the encryption use cases in Dream or Psychic (encrypted column, encrypting/decrypting cookies, generalized use of the Encrypt library).')
177
- .addOption(new Option('--algorithm <algorithm>', 'the encryption algorithm to generate a key for')
253
+ .description(`Generates a cryptographically secure encryption key and prints it to stdout. Use this to create keys for any Dream/Psychic encryption use case:
254
+ ${INDENT}
255
+ ${INDENT} - @deco.Encrypted() model columns (e.g., phone numbers, SSNs)
256
+ ${INDENT} - Cookie encryption/decryption in Psychic sessions
257
+ ${INDENT} - General-purpose use of the Dream Encrypt library
258
+ ${INDENT}
259
+ ${INDENT}Store the generated key in your environment variables (e.g., ENCRYPTION_KEY). Never commit keys to source control.
260
+ ${INDENT}
261
+ ${INDENT}Example:
262
+ ${INDENT} pnpm psy g:encryption-key # generates aes-256-gcm key (default)
263
+ ${INDENT} pnpm psy g:encryption-key --algorithm=aes-128-gcm`)
264
+ .addOption(new Option('--algorithm <algorithm>', 'the encryption algorithm to generate a key for. aes-256-gcm (default) is recommended for most use cases')
178
265
  .choices(['aes-256-gcm', 'aes-192-gcm', 'aes-128-gcm'])
179
266
  .default('aes-256-gcm'))
180
267
  .action((options) => {
@@ -184,7 +271,7 @@ ${INDENT} to extend the Coach model in src/app/models/Health/Coach: Health/Co
184
271
  });
185
272
  program
186
273
  .command('db:create')
187
- .description('Creates a new database.')
274
+ .description('Creates the database defined in your Dream configuration. Run this once when setting up a new development environment, or after db:drop. Safe to run if the database already exists.')
188
275
  .action(async () => {
189
276
  await initializeDreamApp({ bypassDreamIntegrityChecks: true, bypassDbConnectionsDuringInit: true });
190
277
  await DreamBin.dbCreate();
@@ -192,7 +279,7 @@ ${INDENT} to extend the Coach model in src/app/models/Health/Coach: Health/Co
192
279
  });
193
280
  program
194
281
  .command('db:integrity-check')
195
- .description('Fails if migrations need to be run.')
282
+ .description('Checks that all migrations have been run and exits with code 1 if any are pending. Useful as a CI check or deploy gate to ensure the database schema is up to date before starting the application.')
196
283
  .action(async () => {
197
284
  await initializeDreamApp({ bypassDreamIntegrityChecks: true });
198
285
  await DreamBin.dbEnsureAllMigrationsHaveBeenRun();
@@ -200,8 +287,13 @@ ${INDENT} to extend the Coach model in src/app/models/Health/Coach: Health/Co
200
287
  });
201
288
  program
202
289
  .command('db:migrate')
203
- .description('Runs any outstanding database migrations.')
204
- .option('--skip-sync', 'skips syncing local schema after running migrations', false)
290
+ .description(`Runs all pending database migrations in order, then automatically syncs types (in development/test). This is the primary command for applying schema changes after generating or editing a migration.
291
+ ${INDENT}
292
+ ${INDENT}Example workflow:
293
+ ${INDENT} pnpm psy g:migration add-phone-to-users phone:string:optional
294
+ ${INDENT} # edit the migration if needed (e.g., add unique constraints)
295
+ ${INDENT} pnpm psy db:migrate`)
296
+ .option('--skip-sync', 'skip the automatic sync after migrating. Useful when running migrations in production or when you plan to sync manually afterward', false)
205
297
  .action(async ({ skipSync }) => {
206
298
  await initializeDreamApp({ bypassDreamIntegrityChecks: true });
207
299
  await DreamBin.dbMigrate();
@@ -212,9 +304,13 @@ ${INDENT} to extend the Coach model in src/app/models/Health/Coach: Health/Co
212
304
  });
213
305
  program
214
306
  .command('db:rollback')
215
- .description('Rolls back the specified number of migration steps (defaults to 1)')
216
- .option('--steps <number>', 'number of steps back to travel', myParseInt, 1)
217
- .option('--skip-sync', 'skips syncing local schema after running migrations', false)
307
+ .description(`Rolls back the most recent migration(s), then automatically syncs types (in development/test). Use this to undo a migration so you can edit and re-run it.
308
+ ${INDENT}
309
+ ${INDENT}Examples:
310
+ ${INDENT} pnpm psy db:rollback # rolls back the last migration
311
+ ${INDENT} pnpm psy db:rollback --steps=3 # rolls back the last 3 migrations`)
312
+ .option('--steps <number>', 'number of migration steps to roll back (default: 1)', myParseInt, 1)
313
+ .option('--skip-sync', 'skip the automatic sync after rolling back. Useful when you plan to immediately re-migrate or sync manually', false)
218
314
  .action(async ({ steps, skipSync }) => {
219
315
  await initializeDreamApp({ bypassDreamIntegrityChecks: true });
220
316
  await DreamBin.dbRollback({ steps });
@@ -225,7 +321,7 @@ ${INDENT} to extend the Coach model in src/app/models/Health/Coach: Health/Co
225
321
  });
226
322
  program
227
323
  .command('db:drop')
228
- .description('Drops the database')
324
+ .description('Drops the database. This is a destructive operation — all data will be lost. Primarily used as part of db:reset or when you need a clean slate during development.')
229
325
  .action(async () => {
230
326
  await initializeDreamApp({ bypassDreamIntegrityChecks: true, bypassDbConnectionsDuringInit: true });
231
327
  await DreamBin.dbDrop();
@@ -233,7 +329,13 @@ ${INDENT} to extend the Coach model in src/app/models/Health/Coach: Health/Co
233
329
  });
234
330
  program
235
331
  .command('db:reset')
236
- .description('Runs db:drop (safely), db:create, db:migrate, and db:seed')
332
+ .description(`Completely resets the database by running db:drop, db:create, db:migrate, sync, and db:seed in sequence. Use this when:
333
+ ${INDENT}
334
+ ${INDENT} - Switching between branches with incompatible migrations ("corrupted migrations" error)
335
+ ${INDENT} - Starting fresh after a schema has diverged significantly
336
+ ${INDENT} - Setting up a clean development environment
337
+ ${INDENT}
338
+ ${INDENT}Warning: all existing data will be lost. The seed file (db/seed.ts) will be run to repopulate initial data.`)
237
339
  .action(async () => {
238
340
  await initializeDreamApp({ bypassDreamIntegrityChecks: true, bypassDbConnectionsDuringInit: true });
239
341
  const arrows = colorize('⭣⭣⭣', { color: 'green' }) + '\n';
@@ -273,7 +375,7 @@ ${INDENT} to extend the Coach model in src/app/models/Health/Coach: Health/Co
273
375
  });
274
376
  program
275
377
  .command('db:seed')
276
- .description('Seeds the database using the file located in db/seed.ts.')
378
+ .description(`Seeds the database by running the seed function defined in db/seed.ts. Skipped automatically in test environments unless DREAM_SEED_DB_IN_TEST=1 is set. Runs automatically as the last step of db:reset.`)
277
379
  .action(async () => {
278
380
  if (process.env.NODE_ENV === 'test' && process.env.DREAM_SEED_DB_IN_TEST !== '1') {
279
381
  DreamApp.log('skipping db seed for test env. To really seed for test, add DREAM_SEED_DB_IN_TEST=1');
@@ -286,9 +388,16 @@ ${INDENT} to extend the Coach model in src/app/models/Health/Coach: Health/Co
286
388
  program
287
389
  .command('inspect:serialization')
288
390
  .alias('i:serialization')
289
- .description('Displays a serialization map to help understand the rendering logic for a particular model.')
290
- .argument('<globalName>', 'the global name of the model you want to look up')
291
- .argument('[serializerKey]', 'the serializer key you wish to use')
391
+ .description(`Displays a detailed serialization map for a model, showing all attributes, associations, custom attributes, and their types. Useful for debugging serializer output, understanding what a model's API response will look like, and verifying that associations are preloaded correctly.
392
+ ${INDENT}
393
+ ${INDENT}Examples:
394
+ ${INDENT} pnpm psy i:serialization Place # shows the default serializer for Place
395
+ ${INDENT} pnpm psy i:serialization Place summary # shows the summary serializer for Place
396
+ ${INDENT} pnpm psy i:serialization Room/Bedroom # shows serializer for an STI child`)
397
+ .argument('<globalName>', `The global name of the model as registered in Dream (typically matches the model class name or its fully qualified path).
398
+ ${INDENT}
399
+ ${INDENT}Examples: User, Place, Room/Bedroom, Settings/CommunicationPreferences`)
400
+ .argument('[serializerKey]', 'the serializer variant to display (e.g., "summary", "admin", "internal"). Defaults to "default" if omitted')
292
401
  .action(async (globalName, serializerKey) => {
293
402
  await initializeDreamApp();
294
403
  const dreamApp = DreamApp.getOrFail();
@@ -32,10 +32,10 @@ export default function beforeSortableSave({ positionField, dream, scope, }) {
32
32
  // it will be updated in an AfterCreateCommit hook to the correct value after saving.
33
33
  dreamAsAny[positionField] = 0;
34
34
  }
35
- else {
36
- // if the dream is saved, set the position field to undefined, which will cause
37
- // the update cycle to ignore the position field. We will proceed to update it in an
38
- // AfterUpdateCommit hook
35
+ else if (dream.willSaveChangeToAttribute(positionField)) {
36
+ // if the dream is saved and the position is being changed, set the position field to
37
+ // undefined, which will cause the update cycle to ignore the position field. We will
38
+ // proceed to update it in an AfterUpdateCommit hook
39
39
  dreamAsAny[positionField] = undefined;
40
40
  }
41
41
  }
@@ -266,6 +266,7 @@ export default class DreamClassTransactionBuilder {
266
266
  * @param attributes - The base attributes for finding, but also the attributes to use when creating
267
267
  * @param extraOpts - Additional options
268
268
  * @param extraOpts.createWith - additional attributes to persist when creating, but not used for finding
269
+ * @param extraOpts.skipHooks - if true, will skip applying model hooks. Defaults to false
269
270
  * @returns A dream instance
270
271
  */
271
272
  async findOrCreateBy(attributes, extraOpts = {}) {
@@ -4,10 +4,11 @@ export default async function findOrCreateBy(dreamClass, txn = null, attributes,
4
4
  .findBy(dreamClass['extractAttributesFromUpdateableProperties'](attributes));
5
5
  if (existingRecord)
6
6
  return existingRecord;
7
+ const { createWith, skipHooks } = extraOpts;
7
8
  const dreamModel = dreamClass.new({
8
9
  ...attributes,
9
- ...extraOpts?.createWith,
10
+ ...createWith,
10
11
  });
11
- await dreamModel.txn(txn).save();
12
+ await dreamModel.txn(txn).save(skipHooks ? { skipHooks } : undefined);
12
13
  return dreamModel;
13
14
  }
@@ -14,5 +14,6 @@ export default async function reload(dream, txn = null) {
14
14
  const reloadedRecord = await query.firstOrFail();
15
15
  dream.setAttributes(reloadedRecord.getAttributes());
16
16
  dream['freezeAttributes']();
17
+ dream['originalAttributes'] = dream.getAttributes();
17
18
  return dream;
18
19
  }
@@ -321,7 +321,10 @@ function removeJoinAndFromObjectHierarchy(obj) {
321
321
  const result = {};
322
322
  for (const key in obj) {
323
323
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
324
- if (isObject(obj[key])) {
324
+ // Only recurse into plain data objects. Class instances (Dream models, DateTime, etc.)
325
+ // may contain circular loaded-association references, which would cause infinite recursion.
326
+ const proto = Object.getPrototypeOf(obj[key]);
327
+ if (isObject(obj[key]) && (proto === Object.prototype || proto === null)) {
325
328
  result[key] = removeJoinAndFromObjectHierarchy(obj[key].and || obj[key]);
326
329
  }
327
330
  else {
@@ -114,6 +114,9 @@ export class DateTime {
114
114
  constructor(luxonDatetime, microseconds = 0) {
115
115
  this.luxonDatetime = luxonDatetime;
116
116
  this._microseconds = microseconds;
117
+ // Make lazy cache properties non-enumerable so they don't interfere with deep equality checks
118
+ Object.defineProperty(this, '_toSQL', { enumerable: false, writable: true, configurable: true });
119
+ Object.defineProperty(this, '_valueOf', { enumerable: false, writable: true, configurable: true });
117
120
  }
118
121
  /**
119
122
  * Returns the underlying Luxon DateTime instance.
@@ -809,6 +809,7 @@ export default class Dream {
809
809
  * @param attributes - The base attributes to persist, but also the attributes to use to find when create fails
810
810
  * @param extraOpts - Additional options
811
811
  * @param extraOpts.createWith - additional attributes to persist when creating, but not used for finding
812
+ * @param extraOpts.skipHooks - if true, will skip applying model hooks. Defaults to false
812
813
  * @returns A dream instance
813
814
  */
814
815
  static createOrFindBy<T extends typeof Dream>(this: T, attributes: UpdateablePropertiesForClass<T>, extraOpts?: CreateOrFindByExtraOpts<T>): Promise<InstanceType<T>>;
@@ -994,6 +995,7 @@ export default class Dream {
994
995
  * @param attributes - The base attributes for finding, but also the attributes to use when creating
995
996
  * @param extraOpts - Additional options
996
997
  * @param extraOpts.createWith - additional attributes to persist when creating, but not used for finding
998
+ * @param extraOpts.skipHooks - if true, will skip applying model hooks. Defaults to false
997
999
  * @returns A dream instance
998
1000
  */
999
1001
  static findOrCreateBy<T extends typeof Dream>(this: T, attributes: UpdateablePropertiesForClass<T>, extraOpts?: CreateOrFindByExtraOpts<T>): Promise<InstanceType<T>>;
@@ -243,6 +243,7 @@ export default class DreamClassTransactionBuilder<DreamClass extends typeof Drea
243
243
  * @param attributes - The base attributes for finding, but also the attributes to use when creating
244
244
  * @param extraOpts - Additional options
245
245
  * @param extraOpts.createWith - additional attributes to persist when creating, but not used for finding
246
+ * @param extraOpts.skipHooks - if true, will skip applying model hooks. Defaults to false
246
247
  * @returns A dream instance
247
248
  */
248
249
  findOrCreateBy<I extends DreamClassTransactionBuilder<DreamClass, DreamInstance>>(this: I, attributes: UpdateablePropertiesForClass<DreamClass>, extraOpts?: CreateOrFindByExtraOpts<DreamClass>): Promise<InstanceType<DreamClass>>;
@@ -130,6 +130,7 @@ export type AttributeKeys<I extends Dream, TableName extends AssociationTableNam
130
130
  export type UpdateableProperties<I extends Dream, TableName extends AssociationTableNames<I['DB'], I['schema']> & I['table'] = I['table'], VirtualColumns = DreamVirtualColumns<I>> = Partial<Updateable<I['DB'][TableName]> & (VirtualColumns extends readonly any[] ? Record<VirtualColumns[number], any> : object) & (AssociatedModelParam<I> extends never ? object : AssociatedModelParam<I>)>;
131
131
  export interface CreateOrFindByExtraOpts<T extends typeof Dream> {
132
132
  createWith?: UpdateablePropertiesForClass<T>;
133
+ skipHooks?: boolean;
133
134
  }
134
135
  export interface UpdateOrCreateByExtraOpts<T extends typeof Dream> {
135
136
  with?: UpdateablePropertiesForClass<T>;
@@ -522,6 +522,7 @@ export type UpdateableProperties<
522
522
 
523
523
  export interface CreateOrFindByExtraOpts<T extends typeof Dream> {
524
524
  createWith?: UpdateablePropertiesForClass<T>
525
+ skipHooks?: boolean
525
526
  }
526
527
 
527
528
  export interface UpdateOrCreateByExtraOpts<T extends typeof Dream> {