@rvoh/dream 2.33.0 → 2.35.0

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 (218) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/src/Dream.js +14 -3
  3. package/dist/cjs/src/decorators/Decorators.js +20 -0
  4. package/dist/cjs/src/decorators/field/lifecycle/AfterCreateCommit.js +4 -0
  5. package/dist/cjs/src/decorators/field/lifecycle/AfterDestroyCommit.js +4 -0
  6. package/dist/cjs/src/decorators/field/lifecycle/AfterSaveCommit.js +4 -0
  7. package/dist/cjs/src/decorators/field/lifecycle/AfterUpdateCommit.js +4 -0
  8. package/dist/cjs/src/dream/DreamTransaction.js +14 -3
  9. package/dist/cjs/src/dream/QueryDriver/Kysely.js +1 -1
  10. package/dist/cjs/src/dream/internal/runHooksFor.js +16 -5
  11. package/dist/cjs/src/dream/internal/withConcurrentWriterRetry.js +6 -1
  12. package/dist/esm/src/Dream.js +14 -3
  13. package/dist/esm/src/decorators/Decorators.js +20 -0
  14. package/dist/esm/src/decorators/field/lifecycle/AfterCreateCommit.js +4 -0
  15. package/dist/esm/src/decorators/field/lifecycle/AfterDestroyCommit.js +4 -0
  16. package/dist/esm/src/decorators/field/lifecycle/AfterSaveCommit.js +4 -0
  17. package/dist/esm/src/decorators/field/lifecycle/AfterUpdateCommit.js +4 -0
  18. package/dist/esm/src/dream/DreamTransaction.js +14 -3
  19. package/dist/esm/src/dream/QueryDriver/Kysely.js +1 -1
  20. package/dist/esm/src/dream/internal/runHooksFor.js +16 -5
  21. package/dist/esm/src/dream/internal/withConcurrentWriterRetry.js +6 -1
  22. package/dist/types/src/Dream.d.ts +13 -2
  23. package/dist/types/src/decorators/Decorators.d.ts +24 -4
  24. package/dist/types/src/decorators/field/lifecycle/AfterCreateCommit.d.ts +6 -2
  25. package/dist/types/src/decorators/field/lifecycle/AfterDestroyCommit.d.ts +6 -1
  26. package/dist/types/src/decorators/field/lifecycle/AfterSaveCommit.d.ts +6 -2
  27. package/dist/types/src/decorators/field/lifecycle/AfterUpdateCommit.d.ts +6 -2
  28. package/dist/types/src/dream/DreamTransaction.d.ts +8 -1
  29. package/dist/types/src/dream/internal/runHooksFor.d.ts +5 -0
  30. package/dist/types/src/dream/internal/withConcurrentWriterRetry.d.ts +2 -1
  31. package/dist/types/src/types/lifecycle.d.ts +29 -0
  32. package/dist/types/src/types/types/lifecycle.ts +46 -0
  33. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  34. package/docs/classes/db.KyselyQueryDriver.html +46 -46
  35. package/docs/classes/db.PostgresQueryDriver.html +47 -47
  36. package/docs/classes/db.QueryDriverBase.html +45 -45
  37. package/docs/classes/errors.CannotSaveMissingDream.html +5 -5
  38. package/docs/classes/errors.CheckConstraintViolation.html +5 -5
  39. package/docs/classes/errors.ColumnOverflow.html +5 -5
  40. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +5 -5
  41. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +5 -5
  42. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +5 -5
  43. package/docs/classes/errors.DecryptionError.html +4 -4
  44. package/docs/classes/errors.DecryptionParseError.html +4 -4
  45. package/docs/classes/errors.DecryptionRotationError.html +5 -5
  46. package/docs/classes/errors.GlobalNameNotSet.html +5 -5
  47. package/docs/classes/errors.InvalidCalendarDate.html +5 -5
  48. package/docs/classes/errors.InvalidClockTime.html +5 -5
  49. package/docs/classes/errors.InvalidClockTimeTz.html +5 -5
  50. package/docs/classes/errors.InvalidDateTime.html +5 -5
  51. package/docs/classes/errors.MissingSerializersDefinition.html +5 -5
  52. package/docs/classes/errors.NonLoadedAssociation.html +5 -5
  53. package/docs/classes/errors.NotNullViolation.html +5 -5
  54. package/docs/classes/errors.RecordNotFound.html +5 -5
  55. package/docs/classes/errors.SortableScopeDidNotStabilize.html +5 -5
  56. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +5 -5
  57. package/docs/classes/errors.ValidationError.html +5 -5
  58. package/docs/classes/index.CalendarDate.html +33 -33
  59. package/docs/classes/index.ClockTime.html +32 -32
  60. package/docs/classes/index.ClockTimeTz.html +35 -35
  61. package/docs/classes/index.DateTime.html +86 -86
  62. package/docs/classes/index.Decorators.html +39 -23
  63. package/docs/classes/index.Dream.html +134 -126
  64. package/docs/classes/index.DreamApp.html +10 -10
  65. package/docs/classes/index.DreamTransaction.html +6 -2
  66. package/docs/classes/index.Env.html +2 -2
  67. package/docs/classes/index.Query.html +65 -65
  68. package/docs/classes/system.CliFileWriter.html +4 -4
  69. package/docs/classes/system.DreamBin.html +2 -2
  70. package/docs/classes/system.DreamCLI.html +7 -7
  71. package/docs/classes/system.DreamImporter.html +2 -2
  72. package/docs/classes/system.DreamLogos.html +2 -2
  73. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  74. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  75. package/docs/classes/system.PathHelpers.html +3 -3
  76. package/docs/classes/utils.Encrypt.html +3 -3
  77. package/docs/classes/utils.Range.html +2 -2
  78. package/docs/functions/db.closeAllDbConnections.html +1 -1
  79. package/docs/functions/db.dreamDbConnections.html +1 -1
  80. package/docs/functions/db.untypedDb.html +1 -1
  81. package/docs/functions/db.validateColumn.html +1 -1
  82. package/docs/functions/db.validateTable.html +1 -1
  83. package/docs/functions/errors.pgErrorType.html +1 -1
  84. package/docs/functions/index.DreamSerializer.html +1 -1
  85. package/docs/functions/index.ObjectSerializer.html +1 -1
  86. package/docs/functions/index.ReplicaSafe.html +1 -1
  87. package/docs/functions/index.STI.html +1 -1
  88. package/docs/functions/index.SoftDelete.html +1 -1
  89. package/docs/functions/utils.camelize.html +1 -1
  90. package/docs/functions/utils.capitalize.html +1 -1
  91. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  92. package/docs/functions/utils.compact.html +1 -1
  93. package/docs/functions/utils.groupBy.html +1 -1
  94. package/docs/functions/utils.hyphenize.html +1 -1
  95. package/docs/functions/utils.intersection.html +1 -1
  96. package/docs/functions/utils.isEmpty.html +3 -3
  97. package/docs/functions/utils.normalizeUnicode.html +1 -1
  98. package/docs/functions/utils.pascalize.html +1 -1
  99. package/docs/functions/utils.percent.html +1 -1
  100. package/docs/functions/utils.range.html +1 -1
  101. package/docs/functions/utils.round.html +1 -1
  102. package/docs/functions/utils.sanitizeString.html +1 -1
  103. package/docs/functions/utils.snakeify.html +1 -1
  104. package/docs/functions/utils.sort.html +1 -1
  105. package/docs/functions/utils.sortBy.html +1 -1
  106. package/docs/functions/utils.sortObjectByKey.html +1 -1
  107. package/docs/functions/utils.sortObjectByValue.html +1 -1
  108. package/docs/functions/utils.uncapitalize.html +1 -1
  109. package/docs/functions/utils.uniq.html +1 -1
  110. package/docs/index.html +1 -1
  111. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  112. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  113. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  114. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  115. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  116. package/docs/interfaces/types.DecoratorContext.html +2 -2
  117. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  118. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  119. package/docs/interfaces/types.DreamDbConfig.html +6 -6
  120. package/docs/interfaces/types.DurationObject.html +2 -2
  121. package/docs/interfaces/types.EncryptOptions.html +2 -2
  122. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  123. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  124. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  125. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  126. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  127. package/docs/types/openapi.OpenapiFormats.html +1 -1
  128. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  129. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  130. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  131. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  132. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  133. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  134. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  135. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  136. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  137. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  138. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  139. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  140. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  141. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  142. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  143. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  144. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  145. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  146. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  147. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  148. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  149. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  150. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  151. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  152. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  153. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  154. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  155. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  156. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  157. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  158. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  159. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  160. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  161. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  162. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  163. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  164. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  165. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  166. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  167. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  168. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  169. package/docs/types/types.CalendarDateObject.html +1 -1
  170. package/docs/types/types.Camelized.html +1 -1
  171. package/docs/types/types.ClockTimeObject.html +1 -1
  172. package/docs/types/types.DbConnectionType.html +1 -1
  173. package/docs/types/types.DbTypes.html +1 -1
  174. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  175. package/docs/types/types.DreamAttributes.html +1 -1
  176. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  177. package/docs/types/types.DreamClassColumn.html +1 -1
  178. package/docs/types/types.DreamColumn.html +1 -1
  179. package/docs/types/types.DreamColumnNames.html +1 -1
  180. package/docs/types/types.DreamLogLevel.html +1 -1
  181. package/docs/types/types.DreamLogger.html +2 -2
  182. package/docs/types/types.DreamModelSerializerType.html +1 -1
  183. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  184. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  185. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  186. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  187. package/docs/types/types.DreamSerializable.html +1 -1
  188. package/docs/types/types.DreamSerializableArray.html +1 -1
  189. package/docs/types/types.DreamSerializerKey.html +1 -1
  190. package/docs/types/types.DreamSerializers.html +1 -1
  191. package/docs/types/types.DreamVirtualColumns.html +1 -1
  192. package/docs/types/types.DurationUnit.html +1 -1
  193. package/docs/types/types.EncryptAlgorithm.html +1 -1
  194. package/docs/types/types.HasManyStatement.html +1 -1
  195. package/docs/types/types.HasOneStatement.html +1 -1
  196. package/docs/types/types.Hyphenized.html +1 -1
  197. package/docs/types/types.Pascalized.html +1 -1
  198. package/docs/types/types.PrimaryKeyType.html +1 -1
  199. package/docs/types/types.RoundingPrecision.html +1 -1
  200. package/docs/types/types.SerializerCasing.html +1 -1
  201. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  202. package/docs/types/types.Snakeified.html +1 -1
  203. package/docs/types/types.StrictInterface.html +1 -1
  204. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  205. package/docs/types/types.UpdateableProperties.html +1 -1
  206. package/docs/types/types.ValidationType.html +1 -1
  207. package/docs/types/types.ViewModel.html +2 -2
  208. package/docs/types/types.ViewModelClass.html +1 -1
  209. package/docs/types/types.WeekdayName.html +1 -1
  210. package/docs/types/types.WhereStatementForDream.html +1 -1
  211. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  212. package/docs/variables/index.DreamConst.html +1 -1
  213. package/docs/variables/index.ops.html +1 -1
  214. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  215. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  216. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  217. package/docs/variables/system.primaryKeyTypes.html +1 -1
  218. package/package.json +5 -2
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- Dream is a typescript-driven, esm-first ORM built on top of [kysely](http://kysely.dev). It is built to operate within the [Psychic web framework](https://psychicframework.com/docs/intro), but can be brought into other projects and used without the encapsulating framework (though this is theoretical, and we do not encourage it at this time). It is actively being developed to support production-grade applications in use within the [RVOHealth organization](https://www.rvohealth.com), who has kindly sponsored the continued development of this ORM, as well as the Psychic web framework as a whole.
1
+ Dream is a typescript-driven, esm-first ORM built on top of [kysely](http://kysely.dev). It supports Node.js 24 and newer, with Node.js 26 as the primary development and CI runtime. It is built to operate within the [Psychic web framework](https://psychicframework.com/docs/intro), but can be brought into other projects and used without the encapsulating framework (though this is theoretical, and we do not encourage it at this time). It is actively being developed to support production-grade applications in use within the [RVOHealth organization](https://www.rvohealth.com), who has kindly sponsored the continued development of this ORM, as well as the Psychic web framework as a whole.
2
2
 
3
3
  Documentation: [https://psychicframework.com/docs/intro](https://psychicframework.com/docs/intro)
4
4
  Psychic web framework github: [https://github.com/rvohealth/psychic](https://github.com/rvohealth/psychic)
@@ -2302,8 +2302,8 @@ export default class Dream {
2302
2302
  /**
2303
2303
  * Returns true if any of the attributes on the instance
2304
2304
  * have changed since it was last pulled from the database.
2305
- * It will also return true if the instance is not yet
2306
- * persisted.
2305
+ * On an unpersisted instance, it returns true when at least
2306
+ * one column has a value other than `undefined`.
2307
2307
  *
2308
2308
  * ```ts
2309
2309
  * const post = Post.new({ body: 'howyadoin' })
@@ -2946,6 +2946,9 @@ export default class Dream {
2946
2946
  * being persisted to the database, with the values
2947
2947
  * that were last persisted to the database.
2948
2948
  *
2949
+ * On a record that has not been persisted, only columns
2950
+ * with values other than `undefined` count as changed.
2951
+ *
2949
2952
  * ```ts
2950
2953
  * const user = User.new({ email: 'original@email', password: 'howyadoin' })
2951
2954
  * await user.save()
@@ -3077,6 +3080,10 @@ export default class Dream {
3077
3080
  * Returns true if the columnName provided has
3078
3081
  * changes that have not yet been persisted.
3079
3082
  *
3083
+ * On a record that has not been persisted, this returns
3084
+ * true only for a column with a value other than `undefined`.
3085
+ * An explicit `null` counts as a change.
3086
+ *
3080
3087
  * @param attribute - the column name to check
3081
3088
  * @returns A boolean
3082
3089
  */
@@ -3096,6 +3103,10 @@ export default class Dream {
3096
3103
  * of columns that have changed since last persist,
3097
3104
  * and their current values.
3098
3105
  *
3106
+ * On a record that has not been persisted, the object
3107
+ * contains only columns with values other than `undefined`.
3108
+ * An explicit `null` is included.
3109
+ *
3099
3110
  * ```ts
3100
3111
  * const user = User.new({ email: 'hello@world' })
3101
3112
  * user.dirtyAttributes()
@@ -3133,7 +3144,7 @@ export default class Dream {
3133
3144
  */
3134
3145
  attributeIsDirty(attribute) {
3135
3146
  if (this.isNewRecord)
3136
- return true;
3147
+ return this.getAttribute(attribute) !== undefined;
3137
3148
  return !areEqual(this.frozenAttributes[attribute], this.getAttribute(attribute));
3138
3149
  }
3139
3150
  /**
@@ -491,6 +491,11 @@ export default class Decorators {
491
491
  * }
492
492
  * ```
493
493
  *
494
+ * The decorated method is called with no argument, once the
495
+ * transaction has committed, so declaring a parameter on it is a
496
+ * compile error. A subclass override of the method that is not itself
497
+ * decorated is not checked; it is still called with no argument.
498
+ *
494
499
  * @returns The AfterCreateCommit decorator
495
500
  */
496
501
  AfterCreateCommit(opts) {
@@ -526,6 +531,11 @@ export default class Decorators {
526
531
  * }
527
532
  * ```
528
533
  *
534
+ * The decorated method is called with no argument, once the
535
+ * transaction has committed, so declaring a parameter on it is a
536
+ * compile error. A subclass override of the method that is not itself
537
+ * decorated is not checked; it is still called with no argument.
538
+ *
529
539
  * @returns The AfterSaveCommit decorator
530
540
  *
531
541
  */
@@ -562,6 +572,11 @@ export default class Decorators {
562
572
  * }
563
573
  * ```
564
574
  *
575
+ * The decorated method is called with no argument, once the
576
+ * transaction has committed, so declaring a parameter on it is a
577
+ * compile error. A subclass override of the method that is not itself
578
+ * decorated is not checked; it is still called with no argument.
579
+ *
565
580
  * @returns The AfterUpdateCommit decorator
566
581
  *
567
582
  */
@@ -598,6 +613,11 @@ export default class Decorators {
598
613
  * }
599
614
  * ```
600
615
  *
616
+ * The decorated method is called with no argument, once the
617
+ * transaction has committed, so declaring a parameter on it is a
618
+ * compile error. A subclass override of the method that is not itself
619
+ * decorated is not checked; it is still called with no argument.
620
+ *
601
621
  * @returns The AfterDestroyCommit decorator
602
622
  *
603
623
  */
@@ -7,6 +7,10 @@ import { blankHooksFactory } from './shared.js';
7
7
  * a transaction, the method will be run after the save
8
8
  * is complete.
9
9
  *
10
+ * The method is called with no argument: the transaction has already
11
+ * committed, so there is none to hand it. Decorating a method that
12
+ * declares a parameter is a compile error (see {@link CommitHookDecorator}).
13
+ *
10
14
  * class User extends ApplicationModel {
11
15
  * @deco.AfterCreateCommit()
12
16
  * public doSomething() {
@@ -7,6 +7,10 @@ import { blankHooksFactory } from './shared.js';
7
7
  * a transaction, the method will be run after the save
8
8
  * is complete.
9
9
  *
10
+ * The method is called with no argument: the transaction has already
11
+ * committed, so there is none to hand it. Decorating a method that
12
+ * declares a parameter is a compile error (see {@link CommitHookDecorator}).
13
+ *
10
14
  * class User extends ApplicationModel {
11
15
  * @deco.AfterDestroyCommit()
12
16
  * public doSomething() {
@@ -7,6 +7,10 @@ import { blankHooksFactory } from './shared.js';
7
7
  * a transaction, the method will be run after the save
8
8
  * is complete.
9
9
  *
10
+ * The method is called with no argument: the transaction has already
11
+ * committed, so there is none to hand it. Decorating a method that
12
+ * declares a parameter is a compile error (see {@link CommitHookDecorator}).
13
+ *
10
14
  * class User extends ApplicationModel {
11
15
  * @deco.AfterSaveCommit()
12
16
  * public doSomething() {
@@ -7,6 +7,10 @@ import { blankHooksFactory } from './shared.js';
7
7
  * a transaction, the method will be run after the save
8
8
  * is complete.
9
9
  *
10
+ * The method is called with no argument: the transaction has already
11
+ * committed, so there is none to hand it. Decorating a method that
12
+ * declares a parameter is a compile error (see {@link CommitHookDecorator}).
13
+ *
10
14
  * class User extends ApplicationModel {
11
15
  * @deco.AfterUpdateCommit()
12
16
  * public doSomething() {
@@ -1,4 +1,4 @@
1
- import { runHook } from './internal/runHooksFor.js';
1
+ import { runCommitHook } from './internal/runHooksFor.js';
2
2
  // though this class is called `DreamTransaction`, it is not itself
3
3
  // a transaction class, as much as a collector for various callbacks
4
4
  // that must be run after the underlying transaction is commited (i.e.
@@ -6,6 +6,10 @@ import { runHook } from './internal/runHooksFor.js';
6
6
  export default class DreamTransaction {
7
7
  _kyselyTransaction;
8
8
  commitHooks = [];
9
+ // Set when the query driver starts the commit hooks, which it does only once
10
+ // the transaction has committed. `withConcurrentWriterRetry` reads it to
11
+ // never run a committed operation again.
12
+ committed = false;
9
13
  get kyselyTransaction() {
10
14
  return this._kyselyTransaction;
11
15
  }
@@ -15,9 +19,16 @@ export default class DreamTransaction {
15
19
  addCommitHook(hookStatement, dreamInstance) {
16
20
  this.commitHooks.push({ dreamInstance, hookStatement });
17
21
  }
18
- async runAfterCommitHooks(txn) {
22
+ /**
23
+ * Runs the commit hooks queued during the transaction, in the order they
24
+ * were queued. A query driver calls this once the transaction has
25
+ * committed. Each hook is called with no argument, since the transaction
26
+ * it would otherwise receive can no longer run queries.
27
+ */
28
+ async runAfterCommitHooks() {
29
+ this.committed = true;
19
30
  for (const hook of this.commitHooks) {
20
- await runHook(hook.hookStatement, hook.dreamInstance, txn);
31
+ await runCommitHook(hook.hookStatement, hook.dreamInstance);
21
32
  }
22
33
  }
23
34
  }
@@ -351,7 +351,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
351
351
  dreamTransaction.kyselyTransaction = kyselyTransaction;
352
352
  callbackResponse = (await callback(dreamTransaction));
353
353
  });
354
- await dreamTransaction.runAfterCommitHooks(dreamTransaction);
354
+ await dreamTransaction.runAfterCommitHooks();
355
355
  return callbackResponse;
356
356
  }
357
357
  /**
@@ -35,7 +35,7 @@ export default async function runHooksFor(key, dream, alreadyPersisted, beforeSa
35
35
  if (txn)
36
36
  txn.addCommitHook(hook, dream);
37
37
  else
38
- await runHook(hook, dream, null);
38
+ await runCommitHook(hook, dream);
39
39
  }
40
40
  break;
41
41
  case 'afterSave':
@@ -50,7 +50,7 @@ export default async function runHooksFor(key, dream, alreadyPersisted, beforeSa
50
50
  if (txn)
51
51
  txn.addCommitHook(hook, dream);
52
52
  else
53
- await runHook(hook, dream, null);
53
+ await runCommitHook(hook, dream);
54
54
  }
55
55
  break;
56
56
  case 'afterUpdate':
@@ -63,7 +63,7 @@ export default async function runHooksFor(key, dream, alreadyPersisted, beforeSa
63
63
  if (txn)
64
64
  txn.addCommitHook(hook, dream);
65
65
  else
66
- await runHook(hook, dream, null);
66
+ await runCommitHook(hook, dream);
67
67
  }
68
68
  break;
69
69
  default:
@@ -79,7 +79,7 @@ export default async function runHooksFor(key, dream, alreadyPersisted, beforeSa
79
79
  if (txn)
80
80
  txn.addCommitHook(hook, dream);
81
81
  else
82
- await runHook(hook, dream, null);
82
+ await runCommitHook(hook, dream);
83
83
  break;
84
84
  default:
85
85
  await runHook(hook, dream, txn);
@@ -88,6 +88,18 @@ export default async function runHooksFor(key, dream, alreadyPersisted, beforeSa
88
88
  }
89
89
  }
90
90
  export async function runHook(statement, dream, txn) {
91
+ assertHookMethodExists(statement, dream);
92
+ await dream[statement.method](txn);
93
+ }
94
+ /**
95
+ * Commit hooks run after the transaction has committed, so they are called
96
+ * with no argument.
97
+ */
98
+ export async function runCommitHook(statement, dream) {
99
+ assertHookMethodExists(statement, dream);
100
+ await dream[statement.method]();
101
+ }
102
+ function assertHookMethodExists(statement, dream) {
91
103
  if (typeof dream[statement.method] !== 'function') {
92
104
  throw new Error(`
93
105
  Attempting to run ${statement.method} as part of the ${statement.type}
@@ -96,7 +108,6 @@ Dream model hook sequence, but we encountered a method that does not exist.
96
108
  Please make sure "${statement.method}" is defined on ${dream['sanitizedConstructorName']}
97
109
  `);
98
110
  }
99
- await dream[statement.method](txn);
100
111
  }
101
112
  function shouldRunBeforeCreateHook(dream, statement) {
102
113
  let shouldRun = false;
@@ -22,7 +22,8 @@ export const MAX_CONCURRENT_WRITER_ATTEMPTS = 3;
22
22
  * rows mid-statement as well. Either way the database has undone the whole
23
23
  * transaction, nothing has committed and no commit hook has run, so running
24
24
  * the operation again reads the rows as the other writer left them. The last
25
- * attempt's error is the adapter's own.
25
+ * attempt's error is the adapter's own. An error raised after COMMIT, such as a
26
+ * commit hook's, is never retried.
26
27
  *
27
28
  * A unique violation raised by a statement inside the callback is not a
28
29
  * collision but a write that violates a constraint outright, and is not
@@ -46,15 +47,19 @@ export const MAX_CONCURRENT_WRITER_ATTEMPTS = 3;
46
47
  export default async function withConcurrentWriterRetry(dreamClass, callback) {
47
48
  const queryDriverClass = DreamApp.getOrFail().dbConnectionQueryDriverClass(dreamClass.prototype.connectionName);
48
49
  for (let attempt = 1;; attempt++) {
50
+ let attemptTransaction = null;
49
51
  let callbackResolved = false;
50
52
  try {
51
53
  return await dreamClass.transaction(async (txn) => {
54
+ attemptTransaction = txn;
52
55
  const result = await callback(txn);
53
56
  callbackResolved = true;
54
57
  return result;
55
58
  });
56
59
  }
57
60
  catch (error) {
61
+ if (attemptTransaction?.['committed'])
62
+ throw error;
58
63
  const undoneByConcurrentWriter = queryDriverClass.isDeadlock(error) ||
59
64
  (callbackResolved && queryDriverClass.isUniqueConstraintViolation(error));
60
65
  if (!undoneByConcurrentWriter || attempt === MAX_CONCURRENT_WRITER_ATTEMPTS)
@@ -2302,8 +2302,8 @@ export default class Dream {
2302
2302
  /**
2303
2303
  * Returns true if any of the attributes on the instance
2304
2304
  * have changed since it was last pulled from the database.
2305
- * It will also return true if the instance is not yet
2306
- * persisted.
2305
+ * On an unpersisted instance, it returns true when at least
2306
+ * one column has a value other than `undefined`.
2307
2307
  *
2308
2308
  * ```ts
2309
2309
  * const post = Post.new({ body: 'howyadoin' })
@@ -2946,6 +2946,9 @@ export default class Dream {
2946
2946
  * being persisted to the database, with the values
2947
2947
  * that were last persisted to the database.
2948
2948
  *
2949
+ * On a record that has not been persisted, only columns
2950
+ * with values other than `undefined` count as changed.
2951
+ *
2949
2952
  * ```ts
2950
2953
  * const user = User.new({ email: 'original@email', password: 'howyadoin' })
2951
2954
  * await user.save()
@@ -3077,6 +3080,10 @@ export default class Dream {
3077
3080
  * Returns true if the columnName provided has
3078
3081
  * changes that have not yet been persisted.
3079
3082
  *
3083
+ * On a record that has not been persisted, this returns
3084
+ * true only for a column with a value other than `undefined`.
3085
+ * An explicit `null` counts as a change.
3086
+ *
3080
3087
  * @param attribute - the column name to check
3081
3088
  * @returns A boolean
3082
3089
  */
@@ -3096,6 +3103,10 @@ export default class Dream {
3096
3103
  * of columns that have changed since last persist,
3097
3104
  * and their current values.
3098
3105
  *
3106
+ * On a record that has not been persisted, the object
3107
+ * contains only columns with values other than `undefined`.
3108
+ * An explicit `null` is included.
3109
+ *
3099
3110
  * ```ts
3100
3111
  * const user = User.new({ email: 'hello@world' })
3101
3112
  * user.dirtyAttributes()
@@ -3133,7 +3144,7 @@ export default class Dream {
3133
3144
  */
3134
3145
  attributeIsDirty(attribute) {
3135
3146
  if (this.isNewRecord)
3136
- return true;
3147
+ return this.getAttribute(attribute) !== undefined;
3137
3148
  return !areEqual(this.frozenAttributes[attribute], this.getAttribute(attribute));
3138
3149
  }
3139
3150
  /**
@@ -491,6 +491,11 @@ export default class Decorators {
491
491
  * }
492
492
  * ```
493
493
  *
494
+ * The decorated method is called with no argument, once the
495
+ * transaction has committed, so declaring a parameter on it is a
496
+ * compile error. A subclass override of the method that is not itself
497
+ * decorated is not checked; it is still called with no argument.
498
+ *
494
499
  * @returns The AfterCreateCommit decorator
495
500
  */
496
501
  AfterCreateCommit(opts) {
@@ -526,6 +531,11 @@ export default class Decorators {
526
531
  * }
527
532
  * ```
528
533
  *
534
+ * The decorated method is called with no argument, once the
535
+ * transaction has committed, so declaring a parameter on it is a
536
+ * compile error. A subclass override of the method that is not itself
537
+ * decorated is not checked; it is still called with no argument.
538
+ *
529
539
  * @returns The AfterSaveCommit decorator
530
540
  *
531
541
  */
@@ -562,6 +572,11 @@ export default class Decorators {
562
572
  * }
563
573
  * ```
564
574
  *
575
+ * The decorated method is called with no argument, once the
576
+ * transaction has committed, so declaring a parameter on it is a
577
+ * compile error. A subclass override of the method that is not itself
578
+ * decorated is not checked; it is still called with no argument.
579
+ *
565
580
  * @returns The AfterUpdateCommit decorator
566
581
  *
567
582
  */
@@ -598,6 +613,11 @@ export default class Decorators {
598
613
  * }
599
614
  * ```
600
615
  *
616
+ * The decorated method is called with no argument, once the
617
+ * transaction has committed, so declaring a parameter on it is a
618
+ * compile error. A subclass override of the method that is not itself
619
+ * decorated is not checked; it is still called with no argument.
620
+ *
601
621
  * @returns The AfterDestroyCommit decorator
602
622
  *
603
623
  */
@@ -7,6 +7,10 @@ import { blankHooksFactory } from './shared.js';
7
7
  * a transaction, the method will be run after the save
8
8
  * is complete.
9
9
  *
10
+ * The method is called with no argument: the transaction has already
11
+ * committed, so there is none to hand it. Decorating a method that
12
+ * declares a parameter is a compile error (see {@link CommitHookDecorator}).
13
+ *
10
14
  * class User extends ApplicationModel {
11
15
  * @deco.AfterCreateCommit()
12
16
  * public doSomething() {
@@ -7,6 +7,10 @@ import { blankHooksFactory } from './shared.js';
7
7
  * a transaction, the method will be run after the save
8
8
  * is complete.
9
9
  *
10
+ * The method is called with no argument: the transaction has already
11
+ * committed, so there is none to hand it. Decorating a method that
12
+ * declares a parameter is a compile error (see {@link CommitHookDecorator}).
13
+ *
10
14
  * class User extends ApplicationModel {
11
15
  * @deco.AfterDestroyCommit()
12
16
  * public doSomething() {
@@ -7,6 +7,10 @@ import { blankHooksFactory } from './shared.js';
7
7
  * a transaction, the method will be run after the save
8
8
  * is complete.
9
9
  *
10
+ * The method is called with no argument: the transaction has already
11
+ * committed, so there is none to hand it. Decorating a method that
12
+ * declares a parameter is a compile error (see {@link CommitHookDecorator}).
13
+ *
10
14
  * class User extends ApplicationModel {
11
15
  * @deco.AfterSaveCommit()
12
16
  * public doSomething() {
@@ -7,6 +7,10 @@ import { blankHooksFactory } from './shared.js';
7
7
  * a transaction, the method will be run after the save
8
8
  * is complete.
9
9
  *
10
+ * The method is called with no argument: the transaction has already
11
+ * committed, so there is none to hand it. Decorating a method that
12
+ * declares a parameter is a compile error (see {@link CommitHookDecorator}).
13
+ *
10
14
  * class User extends ApplicationModel {
11
15
  * @deco.AfterUpdateCommit()
12
16
  * public doSomething() {
@@ -1,4 +1,4 @@
1
- import { runHook } from './internal/runHooksFor.js';
1
+ import { runCommitHook } from './internal/runHooksFor.js';
2
2
  // though this class is called `DreamTransaction`, it is not itself
3
3
  // a transaction class, as much as a collector for various callbacks
4
4
  // that must be run after the underlying transaction is commited (i.e.
@@ -6,6 +6,10 @@ import { runHook } from './internal/runHooksFor.js';
6
6
  export default class DreamTransaction {
7
7
  _kyselyTransaction;
8
8
  commitHooks = [];
9
+ // Set when the query driver starts the commit hooks, which it does only once
10
+ // the transaction has committed. `withConcurrentWriterRetry` reads it to
11
+ // never run a committed operation again.
12
+ committed = false;
9
13
  get kyselyTransaction() {
10
14
  return this._kyselyTransaction;
11
15
  }
@@ -15,9 +19,16 @@ export default class DreamTransaction {
15
19
  addCommitHook(hookStatement, dreamInstance) {
16
20
  this.commitHooks.push({ dreamInstance, hookStatement });
17
21
  }
18
- async runAfterCommitHooks(txn) {
22
+ /**
23
+ * Runs the commit hooks queued during the transaction, in the order they
24
+ * were queued. A query driver calls this once the transaction has
25
+ * committed. Each hook is called with no argument, since the transaction
26
+ * it would otherwise receive can no longer run queries.
27
+ */
28
+ async runAfterCommitHooks() {
29
+ this.committed = true;
19
30
  for (const hook of this.commitHooks) {
20
- await runHook(hook.hookStatement, hook.dreamInstance, txn);
31
+ await runCommitHook(hook.hookStatement, hook.dreamInstance);
21
32
  }
22
33
  }
23
34
  }
@@ -351,7 +351,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
351
351
  dreamTransaction.kyselyTransaction = kyselyTransaction;
352
352
  callbackResponse = (await callback(dreamTransaction));
353
353
  });
354
- await dreamTransaction.runAfterCommitHooks(dreamTransaction);
354
+ await dreamTransaction.runAfterCommitHooks();
355
355
  return callbackResponse;
356
356
  }
357
357
  /**
@@ -35,7 +35,7 @@ export default async function runHooksFor(key, dream, alreadyPersisted, beforeSa
35
35
  if (txn)
36
36
  txn.addCommitHook(hook, dream);
37
37
  else
38
- await runHook(hook, dream, null);
38
+ await runCommitHook(hook, dream);
39
39
  }
40
40
  break;
41
41
  case 'afterSave':
@@ -50,7 +50,7 @@ export default async function runHooksFor(key, dream, alreadyPersisted, beforeSa
50
50
  if (txn)
51
51
  txn.addCommitHook(hook, dream);
52
52
  else
53
- await runHook(hook, dream, null);
53
+ await runCommitHook(hook, dream);
54
54
  }
55
55
  break;
56
56
  case 'afterUpdate':
@@ -63,7 +63,7 @@ export default async function runHooksFor(key, dream, alreadyPersisted, beforeSa
63
63
  if (txn)
64
64
  txn.addCommitHook(hook, dream);
65
65
  else
66
- await runHook(hook, dream, null);
66
+ await runCommitHook(hook, dream);
67
67
  }
68
68
  break;
69
69
  default:
@@ -79,7 +79,7 @@ export default async function runHooksFor(key, dream, alreadyPersisted, beforeSa
79
79
  if (txn)
80
80
  txn.addCommitHook(hook, dream);
81
81
  else
82
- await runHook(hook, dream, null);
82
+ await runCommitHook(hook, dream);
83
83
  break;
84
84
  default:
85
85
  await runHook(hook, dream, txn);
@@ -88,6 +88,18 @@ export default async function runHooksFor(key, dream, alreadyPersisted, beforeSa
88
88
  }
89
89
  }
90
90
  export async function runHook(statement, dream, txn) {
91
+ assertHookMethodExists(statement, dream);
92
+ await dream[statement.method](txn);
93
+ }
94
+ /**
95
+ * Commit hooks run after the transaction has committed, so they are called
96
+ * with no argument.
97
+ */
98
+ export async function runCommitHook(statement, dream) {
99
+ assertHookMethodExists(statement, dream);
100
+ await dream[statement.method]();
101
+ }
102
+ function assertHookMethodExists(statement, dream) {
91
103
  if (typeof dream[statement.method] !== 'function') {
92
104
  throw new Error(`
93
105
  Attempting to run ${statement.method} as part of the ${statement.type}
@@ -96,7 +108,6 @@ Dream model hook sequence, but we encountered a method that does not exist.
96
108
  Please make sure "${statement.method}" is defined on ${dream['sanitizedConstructorName']}
97
109
  `);
98
110
  }
99
- await dream[statement.method](txn);
100
111
  }
101
112
  function shouldRunBeforeCreateHook(dream, statement) {
102
113
  let shouldRun = false;
@@ -22,7 +22,8 @@ export const MAX_CONCURRENT_WRITER_ATTEMPTS = 3;
22
22
  * rows mid-statement as well. Either way the database has undone the whole
23
23
  * transaction, nothing has committed and no commit hook has run, so running
24
24
  * the operation again reads the rows as the other writer left them. The last
25
- * attempt's error is the adapter's own.
25
+ * attempt's error is the adapter's own. An error raised after COMMIT, such as a
26
+ * commit hook's, is never retried.
26
27
  *
27
28
  * A unique violation raised by a statement inside the callback is not a
28
29
  * collision but a write that violates a constraint outright, and is not
@@ -46,15 +47,19 @@ export const MAX_CONCURRENT_WRITER_ATTEMPTS = 3;
46
47
  export default async function withConcurrentWriterRetry(dreamClass, callback) {
47
48
  const queryDriverClass = DreamApp.getOrFail().dbConnectionQueryDriverClass(dreamClass.prototype.connectionName);
48
49
  for (let attempt = 1;; attempt++) {
50
+ let attemptTransaction = null;
49
51
  let callbackResolved = false;
50
52
  try {
51
53
  return await dreamClass.transaction(async (txn) => {
54
+ attemptTransaction = txn;
52
55
  const result = await callback(txn);
53
56
  callbackResolved = true;
54
57
  return result;
55
58
  });
56
59
  }
57
60
  catch (error) {
61
+ if (attemptTransaction?.['committed'])
62
+ throw error;
58
63
  const undoneByConcurrentWriter = queryDriverClass.isDeadlock(error) ||
59
64
  (callbackResolved && queryDriverClass.isUniqueConstraintViolation(error));
60
65
  if (!undoneByConcurrentWriter || attempt === MAX_CONCURRENT_WRITER_ATTEMPTS)
@@ -1900,8 +1900,8 @@ export default class Dream {
1900
1900
  /**
1901
1901
  * Returns true if any of the attributes on the instance
1902
1902
  * have changed since it was last pulled from the database.
1903
- * It will also return true if the instance is not yet
1904
- * persisted.
1903
+ * On an unpersisted instance, it returns true when at least
1904
+ * one column has a value other than `undefined`.
1905
1905
  *
1906
1906
  * ```ts
1907
1907
  * const post = Post.new({ body: 'howyadoin' })
@@ -2312,6 +2312,9 @@ export default class Dream {
2312
2312
  * being persisted to the database, with the values
2313
2313
  * that were last persisted to the database.
2314
2314
  *
2315
+ * On a record that has not been persisted, only columns
2316
+ * with values other than `undefined` count as changed.
2317
+ *
2315
2318
  * ```ts
2316
2319
  * const user = User.new({ email: 'original@email', password: 'howyadoin' })
2317
2320
  * await user.save()
@@ -2421,6 +2424,10 @@ export default class Dream {
2421
2424
  * Returns true if the columnName provided has
2422
2425
  * changes that have not yet been persisted.
2423
2426
  *
2427
+ * On a record that has not been persisted, this returns
2428
+ * true only for a column with a value other than `undefined`.
2429
+ * An explicit `null` counts as a change.
2430
+ *
2424
2431
  * @param attribute - the column name to check
2425
2432
  * @returns A boolean
2426
2433
  */
@@ -2436,6 +2443,10 @@ export default class Dream {
2436
2443
  * of columns that have changed since last persist,
2437
2444
  * and their current values.
2438
2445
  *
2446
+ * On a record that has not been persisted, the object
2447
+ * contains only columns with values other than `undefined`.
2448
+ * An explicit `null` is included.
2449
+ *
2439
2450
  * ```ts
2440
2451
  * const user = User.new({ email: 'hello@world' })
2441
2452
  * user.dirtyAttributes()