@rvoh/dream 2.18.1 → 2.20.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 (265) hide show
  1. package/dist/cjs/src/Dream.js +186 -0
  2. package/dist/cjs/src/db/DreamDbConnection.js +6 -0
  3. package/dist/cjs/src/decorators/class/SoftDelete.js +12 -0
  4. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +113 -0
  5. package/dist/cjs/src/dream/Query.js +148 -0
  6. package/dist/cjs/src/dream/QueryDriver/Base.js +84 -0
  7. package/dist/cjs/src/dream/QueryDriver/Kysely.js +420 -81
  8. package/dist/cjs/src/dream/internal/filterRowToKnownColumns.js +41 -0
  9. package/dist/cjs/src/dream/internal/saveDream.js +6 -1
  10. package/dist/cjs/src/dream/internal/sqlResultToDreamInstance.js +9 -2
  11. package/dist/cjs/src/errors/CannotNamespaceAssociationFilterToAnotherTable.js +27 -0
  12. package/dist/cjs/src/errors/schema-builder/CannotIgnoreAssociationColumn.js +38 -0
  13. package/dist/cjs/src/errors/schema-builder/CannotIgnoreEncryptedColumn.js +20 -0
  14. package/dist/cjs/src/errors/schema-builder/CannotIgnorePrimaryKey.js +18 -0
  15. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.js +21 -0
  16. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.js +20 -0
  17. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.js +24 -0
  18. package/dist/cjs/src/errors/schema-builder/CannotIgnoreStiTypeColumn.js +17 -0
  19. package/dist/cjs/src/errors/schema-builder/ConflictingIgnoredColumns.js +27 -0
  20. package/dist/cjs/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.js +21 -0
  21. package/dist/cjs/src/helpers/cli/ASTConnectionBuilder.js +38 -1
  22. package/dist/cjs/src/helpers/cli/ASTKyselyCodegenEnhancer.js +60 -0
  23. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +21 -13
  24. package/dist/cjs/src/helpers/cli/resolveIgnoredColumns.js +198 -0
  25. package/dist/esm/src/Dream.js +186 -0
  26. package/dist/esm/src/db/DreamDbConnection.js +6 -0
  27. package/dist/esm/src/decorators/class/SoftDelete.js +12 -0
  28. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +113 -0
  29. package/dist/esm/src/dream/Query.js +148 -0
  30. package/dist/esm/src/dream/QueryDriver/Base.js +84 -0
  31. package/dist/esm/src/dream/QueryDriver/Kysely.js +420 -81
  32. package/dist/esm/src/dream/internal/filterRowToKnownColumns.js +41 -0
  33. package/dist/esm/src/dream/internal/saveDream.js +6 -1
  34. package/dist/esm/src/dream/internal/sqlResultToDreamInstance.js +9 -2
  35. package/dist/esm/src/errors/CannotNamespaceAssociationFilterToAnotherTable.js +27 -0
  36. package/dist/esm/src/errors/schema-builder/CannotIgnoreAssociationColumn.js +38 -0
  37. package/dist/esm/src/errors/schema-builder/CannotIgnoreEncryptedColumn.js +20 -0
  38. package/dist/esm/src/errors/schema-builder/CannotIgnorePrimaryKey.js +18 -0
  39. package/dist/esm/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.js +21 -0
  40. package/dist/esm/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.js +20 -0
  41. package/dist/esm/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.js +24 -0
  42. package/dist/esm/src/errors/schema-builder/CannotIgnoreStiTypeColumn.js +17 -0
  43. package/dist/esm/src/errors/schema-builder/ConflictingIgnoredColumns.js +27 -0
  44. package/dist/esm/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.js +21 -0
  45. package/dist/esm/src/helpers/cli/ASTConnectionBuilder.js +38 -1
  46. package/dist/esm/src/helpers/cli/ASTKyselyCodegenEnhancer.js +60 -0
  47. package/dist/esm/src/helpers/cli/generateMigrationContent.js +21 -13
  48. package/dist/esm/src/helpers/cli/resolveIgnoredColumns.js +198 -0
  49. package/dist/types/src/Dream.d.ts +178 -4
  50. package/dist/types/src/decorators/class/SoftDelete.d.ts +12 -0
  51. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +103 -0
  52. package/dist/types/src/dream/Query.d.ts +138 -0
  53. package/dist/types/src/dream/QueryDriver/Base.d.ts +69 -0
  54. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +187 -8
  55. package/dist/types/src/dream/internal/filterRowToKnownColumns.d.ts +30 -0
  56. package/dist/types/src/errors/CannotNamespaceAssociationFilterToAnotherTable.d.ts +8 -0
  57. package/dist/types/src/errors/schema-builder/CannotIgnoreAssociationColumn.d.ts +12 -0
  58. package/dist/types/src/errors/schema-builder/CannotIgnoreEncryptedColumn.d.ts +7 -0
  59. package/dist/types/src/errors/schema-builder/CannotIgnorePrimaryKey.d.ts +6 -0
  60. package/dist/types/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.d.ts +7 -0
  61. package/dist/types/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.d.ts +7 -0
  62. package/dist/types/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.d.ts +8 -0
  63. package/dist/types/src/errors/schema-builder/CannotIgnoreStiTypeColumn.d.ts +6 -0
  64. package/dist/types/src/errors/schema-builder/ConflictingIgnoredColumns.d.ts +7 -0
  65. package/dist/types/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.d.ts +7 -0
  66. package/dist/types/src/helpers/cli/ASTConnectionBuilder.d.ts +20 -0
  67. package/dist/types/src/helpers/cli/ASTKyselyCodegenEnhancer.d.ts +13 -0
  68. package/dist/types/src/helpers/cli/resolveIgnoredColumns.d.ts +41 -0
  69. package/dist/types/src/types/associations/shared.d.ts +4 -1
  70. package/dist/types/src/types/dream.d.ts +17 -0
  71. package/dist/types/src/types/types/associations/shared.ts +10 -1
  72. package/dist/types/src/types/types/dream.ts +53 -0
  73. package/dist/types/src/types/types/variadic.ts +179 -140
  74. package/dist/types/src/types/variadic.d.ts +15 -10
  75. package/docs/assets/hierarchy.js +1 -1
  76. package/docs/assets/search.js +1 -1
  77. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  78. package/docs/classes/db.KyselyQueryDriver.html +96 -34
  79. package/docs/classes/db.PostgresQueryDriver.html +97 -35
  80. package/docs/classes/db.QueryDriverBase.html +77 -33
  81. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  82. package/docs/classes/errors.ColumnOverflow.html +3 -3
  83. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  84. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  85. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  86. package/docs/classes/errors.DecryptionError.html +2 -2
  87. package/docs/classes/errors.DecryptionParseError.html +2 -2
  88. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  89. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  90. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  91. package/docs/classes/errors.InvalidClockTime.html +2 -2
  92. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  93. package/docs/classes/errors.InvalidDateTime.html +2 -2
  94. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  95. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  96. package/docs/classes/errors.NotNullViolation.html +3 -3
  97. package/docs/classes/errors.RecordNotFound.html +3 -3
  98. package/docs/classes/errors.ValidationError.html +3 -3
  99. package/docs/classes/index.CalendarDate.html +33 -33
  100. package/docs/classes/index.ClockTime.html +32 -32
  101. package/docs/classes/index.ClockTimeTz.html +35 -35
  102. package/docs/classes/index.DateTime.html +86 -86
  103. package/docs/classes/index.Decorators.html +19 -19
  104. package/docs/classes/index.Dream.html +247 -119
  105. package/docs/classes/index.DreamApp.html +10 -10
  106. package/docs/classes/index.DreamTransaction.html +2 -2
  107. package/docs/classes/index.Env.html +2 -2
  108. package/docs/classes/index.Query.html +155 -57
  109. package/docs/classes/system.CliFileWriter.html +4 -4
  110. package/docs/classes/system.DreamBin.html +2 -2
  111. package/docs/classes/system.DreamCLI.html +7 -7
  112. package/docs/classes/system.DreamImporter.html +2 -2
  113. package/docs/classes/system.DreamLogos.html +2 -2
  114. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  115. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  116. package/docs/classes/system.PathHelpers.html +3 -3
  117. package/docs/classes/utils.Encrypt.html +3 -3
  118. package/docs/classes/utils.Range.html +2 -2
  119. package/docs/functions/db.closeAllDbConnections.html +1 -1
  120. package/docs/functions/db.dreamDbConnections.html +1 -1
  121. package/docs/functions/db.untypedDb.html +1 -1
  122. package/docs/functions/db.validateColumn.html +1 -1
  123. package/docs/functions/db.validateTable.html +1 -1
  124. package/docs/functions/errors.pgErrorType.html +1 -1
  125. package/docs/functions/index.DreamSerializer.html +1 -1
  126. package/docs/functions/index.ObjectSerializer.html +1 -1
  127. package/docs/functions/index.ReplicaSafe.html +1 -1
  128. package/docs/functions/index.STI.html +1 -1
  129. package/docs/functions/index.SoftDelete.html +12 -1
  130. package/docs/functions/utils.camelize.html +1 -1
  131. package/docs/functions/utils.capitalize.html +1 -1
  132. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  133. package/docs/functions/utils.compact.html +1 -1
  134. package/docs/functions/utils.groupBy.html +1 -1
  135. package/docs/functions/utils.hyphenize.html +1 -1
  136. package/docs/functions/utils.intersection.html +1 -1
  137. package/docs/functions/utils.isEmpty.html +1 -1
  138. package/docs/functions/utils.normalizeUnicode.html +1 -1
  139. package/docs/functions/utils.pascalize.html +1 -1
  140. package/docs/functions/utils.percent.html +1 -1
  141. package/docs/functions/utils.range.html +1 -1
  142. package/docs/functions/utils.round.html +1 -1
  143. package/docs/functions/utils.sanitizeString.html +1 -1
  144. package/docs/functions/utils.snakeify.html +1 -1
  145. package/docs/functions/utils.sort.html +1 -1
  146. package/docs/functions/utils.sortBy.html +1 -1
  147. package/docs/functions/utils.sortObjectByKey.html +1 -1
  148. package/docs/functions/utils.sortObjectByValue.html +1 -1
  149. package/docs/functions/utils.uncapitalize.html +1 -1
  150. package/docs/functions/utils.uniq.html +1 -1
  151. package/docs/hierarchy.html +1 -1
  152. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  153. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  154. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  155. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  156. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  157. package/docs/interfaces/types.DecoratorContext.html +2 -2
  158. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  159. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  160. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  161. package/docs/interfaces/types.DurationObject.html +2 -2
  162. package/docs/interfaces/types.EncryptOptions.html +2 -2
  163. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  164. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  165. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  166. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  167. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  168. package/docs/types/openapi.OpenapiFormats.html +1 -1
  169. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  170. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  171. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  172. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  173. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  174. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  175. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  176. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  177. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  178. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  179. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  180. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  181. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  182. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  183. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  184. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  185. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  186. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  187. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  188. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  189. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  190. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  191. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  192. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  193. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  194. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  195. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  196. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  197. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  198. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  199. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  200. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  201. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  202. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  203. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  204. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  205. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  206. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  207. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  208. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  209. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  210. package/docs/types/types.CalendarDateObject.html +1 -1
  211. package/docs/types/types.Camelized.html +1 -1
  212. package/docs/types/types.ClockTimeObject.html +1 -1
  213. package/docs/types/types.DbConnectionType.html +1 -1
  214. package/docs/types/types.DbTypes.html +1 -1
  215. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  216. package/docs/types/types.DreamAttributes.html +1 -1
  217. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  218. package/docs/types/types.DreamClassColumn.html +1 -1
  219. package/docs/types/types.DreamColumn.html +1 -1
  220. package/docs/types/types.DreamColumnNames.html +1 -1
  221. package/docs/types/types.DreamLogLevel.html +1 -1
  222. package/docs/types/types.DreamLogger.html +2 -2
  223. package/docs/types/types.DreamModelSerializerType.html +1 -1
  224. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  225. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  226. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  227. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  228. package/docs/types/types.DreamSerializable.html +1 -1
  229. package/docs/types/types.DreamSerializableArray.html +1 -1
  230. package/docs/types/types.DreamSerializerKey.html +1 -1
  231. package/docs/types/types.DreamSerializers.html +1 -1
  232. package/docs/types/types.DreamVirtualColumns.html +1 -1
  233. package/docs/types/types.DurationUnit.html +1 -1
  234. package/docs/types/types.EncryptAlgorithm.html +1 -1
  235. package/docs/types/types.HasManyStatement.html +1 -1
  236. package/docs/types/types.HasOneStatement.html +1 -1
  237. package/docs/types/types.Hyphenized.html +1 -1
  238. package/docs/types/types.Pascalized.html +1 -1
  239. package/docs/types/types.PrimaryKeyType.html +1 -1
  240. package/docs/types/types.RoundingPrecision.html +1 -1
  241. package/docs/types/types.SerializerCasing.html +1 -1
  242. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  243. package/docs/types/types.Snakeified.html +1 -1
  244. package/docs/types/types.StrictInterface.html +1 -1
  245. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  246. package/docs/types/types.UpdateableProperties.html +1 -1
  247. package/docs/types/types.ValidationType.html +1 -1
  248. package/docs/types/types.ViewModel.html +2 -2
  249. package/docs/types/types.ViewModelClass.html +1 -1
  250. package/docs/types/types.WeekdayName.html +1 -1
  251. package/docs/types/types.WhereStatementForDream.html +1 -1
  252. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  253. package/docs/variables/index.DreamConst.html +1 -1
  254. package/docs/variables/index.ops.html +1 -1
  255. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  256. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  257. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  258. package/docs/variables/system.primaryKeyTypes.html +1 -1
  259. package/package.json +3 -3
  260. package/dist/cjs/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.js +0 -11
  261. package/dist/cjs/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.js +0 -17
  262. package/dist/esm/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.js +0 -11
  263. package/dist/esm/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.js +0 -17
  264. package/dist/types/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.d.ts +0 -13
  265. package/dist/types/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.d.ts +0 -12
@@ -0,0 +1,198 @@
1
+ import scopeArray from '../../decorators/field/sortable/helpers/scopeArray.js';
2
+ import CannotIgnoreAssociationColumn from '../../errors/schema-builder/CannotIgnoreAssociationColumn.js';
3
+ import CannotIgnoreEncryptedColumn from '../../errors/schema-builder/CannotIgnoreEncryptedColumn.js';
4
+ import CannotIgnorePrimaryKey from '../../errors/schema-builder/CannotIgnorePrimaryKey.js';
5
+ import CannotIgnoreSoftDeleteColumn from '../../errors/schema-builder/CannotIgnoreSoftDeleteColumn.js';
6
+ import CannotIgnoreSortablePositionColumn from '../../errors/schema-builder/CannotIgnoreSortablePositionColumn.js';
7
+ import CannotIgnoreSortableScopeColumn from '../../errors/schema-builder/CannotIgnoreSortableScopeColumn.js';
8
+ import CannotIgnoreStiTypeColumn from '../../errors/schema-builder/CannotIgnoreStiTypeColumn.js';
9
+ import ConflictingIgnoredColumns from '../../errors/schema-builder/ConflictingIgnoredColumns.js';
10
+ import IgnoredColumnMustBeCamelCase from '../../errors/schema-builder/IgnoredColumnMustBeCamelCase.js';
11
+ import camelize from '../camelize.js';
12
+ import uniq from '../uniq.js';
13
+ /**
14
+ * @internal
15
+ *
16
+ * Resolves the set of ignored columns for a table from the `ignoredColumns`
17
+ * declarations of every model backed by that table, validating the
18
+ * declarations along the way. Called while `sync` builds the generated types
19
+ * files (the declarations have no runtime behavior; see the `ignoredColumns`
20
+ * getter on Dream), so each of these guards fails the sync command loudly
21
+ * rather than surfacing as broken behavior at runtime:
22
+ *
23
+ * - ignored columns must be declared in camelCase, since generated column
24
+ * names are camelized, so any other shape can never match a generated
25
+ * column and would be silently inert
26
+ * - a model may never ignore its primary key
27
+ * - an STI model may never ignore the STI "type" column
28
+ * - a model may never ignore an @Sortable position field or a plain-column
29
+ * @Sortable scope, the backing column of an @Encrypted property, or (on a
30
+ * SoftDelete model) its deletedAtField — the framework reads and writes
31
+ * those columns by name
32
+ * - models sharing a table must agree on their ignored columns, since there
33
+ * is only one generated schema per table (STI children inherit the base
34
+ * model's getter, so agreement is automatic unless a child overrides it)
35
+ * - no association anywhere in the app may name an ignored column as the
36
+ * foreign key (or polymorphic type field) it reads and writes on this
37
+ * table — a BelongsTo on a model backed by this table names a foreign key
38
+ * on this table, and so does a HasMany/HasOne on any other model that
39
+ * points at a model backed by this table
40
+ * - no association anywhere in the app may name an ignored column as its
41
+ * primaryKeyOverride — the column the association's foreign key points
42
+ * at, which lives on the associated model's table for a BelongsTo and on
43
+ * the declaring model's own table for a HasMany/HasOne
44
+ *
45
+ * @param models - every model backed by the table
46
+ * @param tableName - the table whose ignored columns are being resolved
47
+ * @param allModels - every model in the app (on the table's connection);
48
+ * required because associations declared on other models can name foreign
49
+ * keys on this table
50
+ * @returns the set of column names to omit from the table's generated types
51
+ */
52
+ export default function resolveIgnoredColumns(models, tableName, allModels) {
53
+ models.forEach(modelClass => {
54
+ const ignoredColumns = modelClass.prototype.ignoredColumns;
55
+ ignoredColumns.forEach(columnName => {
56
+ if (camelize(columnName) !== columnName)
57
+ throw new IgnoredColumnMustBeCamelCase(modelClass, columnName);
58
+ });
59
+ if (ignoredColumns.includes(modelClass.primaryKey))
60
+ throw new CannotIgnorePrimaryKey(modelClass);
61
+ if (ignoredColumns.includes('type') && (modelClass['isSTIBase'] || modelClass['isSTIChild']))
62
+ throw new CannotIgnoreStiTypeColumn(modelClass);
63
+ modelClass['sortableFields'].forEach(sortableFieldConfig => {
64
+ if (ignoredColumns.includes(sortableFieldConfig.positionField))
65
+ throw new CannotIgnoreSortablePositionColumn(modelClass, sortableFieldConfig.positionField);
66
+ // a Sortable scope entry names either a column or a BelongsTo
67
+ // association on the model (getColumnForSortableScope resolves columns
68
+ // first, then falls back to association metadata), so an ignored scope
69
+ // entry only breaks @Sortable when no association answers to the name
70
+ scopeArray(sortableFieldConfig.scope).forEach(scopeEntry => {
71
+ if (ignoredColumns.includes(scopeEntry) &&
72
+ modelClass['associationMetadataMap']()[scopeEntry] === undefined)
73
+ throw new CannotIgnoreSortableScopeColumn(modelClass, scopeEntry, sortableFieldConfig.positionField);
74
+ });
75
+ });
76
+ // the Encrypted decorator records each backing column it manages in
77
+ // explicitUnsafeParamColumns (which nothing else populates)
78
+ modelClass['explicitUnsafeParamColumns'].forEach(columnName => {
79
+ if (ignoredColumns.includes(columnName))
80
+ throw new CannotIgnoreEncryptedColumn(modelClass, columnName);
81
+ });
82
+ if (modelClass['softDelete']) {
83
+ const deletedAtField = modelClass.prototype['_deletedAtField'];
84
+ if (ignoredColumns.includes(deletedAtField))
85
+ throw new CannotIgnoreSoftDeleteColumn(modelClass, deletedAtField);
86
+ }
87
+ });
88
+ const distinctDeclarations = uniq(models.map(modelClass => JSON.stringify(uniq([...modelClass.prototype.ignoredColumns]).sort())));
89
+ if (distinctDeclarations.length > 1)
90
+ throw new ConflictingIgnoredColumns(tableName, models);
91
+ const ignoredColumns = new Set(models.flatMap(modelClass => [...modelClass.prototype.ignoredColumns]));
92
+ if (ignoredColumns.size)
93
+ guardAssociationColumns(ignoredColumns, tableName, allModels);
94
+ return ignoredColumns;
95
+ }
96
+ /**
97
+ * @internal
98
+ *
99
+ * fails the sync when an ignored column of the table is the foreign key (or
100
+ * polymorphic type field) that any association in the app reads and writes
101
+ * on this table, or the primaryKeyOverride column that any association's
102
+ * foreign key points at on this table. A BelongsTo association names a
103
+ * foreign key on the declaring model's own table and a primaryKeyOverride
104
+ * on the associated model's table; a HasMany/HasOne association names a
105
+ * foreign key on the associated model's table and a primaryKeyOverride on
106
+ * the declaring model's own table — so the load-bearing association may be
107
+ * declared on any model in the app, not just the models backed by this
108
+ * table.
109
+ */
110
+ function guardAssociationColumns(ignoredColumns, tableName, allModels) {
111
+ for (const modelClass of allModels) {
112
+ for (const associationName of modelClass.associationNames) {
113
+ const associationMetaData = modelClass['associationMetadataMap']()[associationName];
114
+ if (associationMetaData === undefined)
115
+ continue;
116
+ // a through association names no foreign key of its own; the source
117
+ // association it travels through is guarded directly
118
+ if (associationMetaData.through)
119
+ continue;
120
+ // unlike foreignKey(), primaryKeyOverride is a plain string on the
121
+ // association statement, so it can be guarded without consulting the
122
+ // generated schema — before the foreignKey-specific short-circuits below
123
+ const primaryKeyOverride = associationMetaData.primaryKeyOverride;
124
+ if (primaryKeyOverride &&
125
+ ignoredColumns.has(primaryKeyOverride) &&
126
+ primaryKeyOverrideTableForAssociationMatches(associationMetaData, modelClass, tableName))
127
+ throw new CannotIgnoreAssociationColumn(tableName, primaryKeyOverride, modelClass, associationMetaData, 'primary key override');
128
+ if (!foreignKeyTableForAssociationMatches(associationMetaData, modelClass, tableName))
129
+ continue;
130
+ // NOTE
131
+ // this try-catch mirrors getAssociationData in ASTConnectionBuilder:
132
+ // computing foreignKey() introspects columns via the generated schema
133
+ // file, so it may throw — on the first sync pass because the schema
134
+ // file has not been regenerated yet, and on the second pass because
135
+ // the regenerated schema omits the ignored column. So in orderings
136
+ // where foreignKey() throws on the first pass, it throws on the second
137
+ // pass too, and these guards never see the association's foreign key;
138
+ // the InvalidComputedForeignKey / ExplicitForeignKeyRequired errors
139
+ // swallowed here resurface at runtime as the backstop.
140
+ let foreignKey = null;
141
+ let foreignKeyTypeColumn = null;
142
+ try {
143
+ foreignKey = associationMetaData.foreignKey();
144
+ foreignKeyTypeColumn = associationMetaData.polymorphic
145
+ ? associationMetaData.foreignKeyTypeField()
146
+ : null;
147
+ }
148
+ catch {
149
+ continue;
150
+ }
151
+ if (ignoredColumns.has(foreignKey))
152
+ throw new CannotIgnoreAssociationColumn(tableName, foreignKey, modelClass, associationMetaData, 'foreign key');
153
+ if (foreignKeyTypeColumn && ignoredColumns.has(foreignKeyTypeColumn))
154
+ throw new CannotIgnoreAssociationColumn(tableName, foreignKeyTypeColumn, modelClass, associationMetaData, 'polymorphic type field');
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * @internal
160
+ *
161
+ * returns whether the given association's foreign key physically lives on
162
+ * the given table: on the declaring model's own table for a BelongsTo, and
163
+ * on the associated model's table for a HasMany/HasOne
164
+ */
165
+ function foreignKeyTableForAssociationMatches(associationMetaData, modelClass, tableName) {
166
+ if (associationMetaData.type === 'BelongsTo')
167
+ return modelClass.table === tableName;
168
+ const dreamClassOrClasses = associationMetaData.modelCB();
169
+ // a missing associated class raises FailedToIdentifyAssociation while the
170
+ // builder gathers association data; it is not this guard's concern
171
+ if (!dreamClassOrClasses)
172
+ return false;
173
+ return Array.isArray(dreamClassOrClasses)
174
+ ? dreamClassOrClasses.some(dreamClass => dreamClass.table === tableName)
175
+ : dreamClassOrClasses.table === tableName;
176
+ }
177
+ /**
178
+ * @internal
179
+ *
180
+ * returns whether the given association's primaryKeyOverride column
181
+ * physically lives on the given table — the opposite side from the foreign
182
+ * key (see associationPrimaryKeyAccessors in
183
+ * decorators/field/association/shared.ts): on the associated model's table
184
+ * for a BelongsTo, and on the declaring model's own table for a
185
+ * HasMany/HasOne
186
+ */
187
+ function primaryKeyOverrideTableForAssociationMatches(associationMetaData, modelClass, tableName) {
188
+ if (associationMetaData.type !== 'BelongsTo')
189
+ return modelClass.table === tableName;
190
+ const dreamClassOrClasses = associationMetaData.modelCB();
191
+ // a missing associated class raises FailedToIdentifyAssociation while the
192
+ // builder gathers association data; it is not this guard's concern
193
+ if (!dreamClassOrClasses)
194
+ return false;
195
+ return Array.isArray(dreamClassOrClasses)
196
+ ? dreamClassOrClasses.some(dreamClass => dreamClass.table === tableName)
197
+ : dreamClassOrClasses.table === tableName;
198
+ }
@@ -976,6 +976,25 @@ export default class Dream {
976
976
  static async count() {
977
977
  return await this.query().count();
978
978
  }
979
+ /**
980
+ * Retrieves the number of records in each group, keyed by the
981
+ * value of the provided group column (a SQL `GROUP BY`).
982
+ *
983
+ * ```ts
984
+ * await User.countBy('name')
985
+ * // Map(2) { 'fred' => 2, 'zed' => 1 }
986
+ * ```
987
+ *
988
+ * Only groups with at least one matching row appear in the Map; seed absent
989
+ * groups yourself with `map.get(key) ?? 0`. When the group column is nullable,
990
+ * records with a `null` value are grouped under a real `null` key.
991
+ *
992
+ * @param groupColumn - the column to group by
993
+ * @returns A Map from each present group value to the number of records in that group
994
+ */
995
+ static async countBy(groupColumn) {
996
+ return await this.query().countBy(groupColumn);
997
+ }
979
998
  /**
980
999
  * Retrieves the max value of the specified column
981
1000
  * for this model's records.
@@ -991,6 +1010,27 @@ export default class Dream {
991
1010
  static async max(columnName) {
992
1011
  return await this.query().max(columnName);
993
1012
  }
1013
+ /**
1014
+ * Retrieves the max value of the specified column within each group, keyed by
1015
+ * the value of the provided group column (a SQL `GROUP BY`).
1016
+ *
1017
+ * ```ts
1018
+ * await CompositionAsset.maxBy('name', 'score')
1019
+ * // Map(2) { 'primary' => 9, 'secondary' => 4 }
1020
+ * ```
1021
+ *
1022
+ * Only groups with at least one matching row appear in the Map. When the group
1023
+ * column is nullable, records with a `null` value are grouped under a real
1024
+ * `null` key; a group whose aggregated values are all `null` yields a `null`
1025
+ * value.
1026
+ *
1027
+ * @param groupColumn - the column to group by
1028
+ * @param aggregatedColumn - the column to take the max of within each group
1029
+ * @returns A Map from each present group value to the max of the aggregated column in that group
1030
+ */
1031
+ static async maxBy(groupColumn, aggregatedColumn) {
1032
+ return await this.query().maxBy(groupColumn, aggregatedColumn);
1033
+ }
994
1034
  /**
995
1035
  * Retrieves the min value of the specified column
996
1036
  * for this model's records.
@@ -1007,6 +1047,27 @@ export default class Dream {
1007
1047
  static async min(columnName) {
1008
1048
  return await this.query().min(columnName);
1009
1049
  }
1050
+ /**
1051
+ * Retrieves the min value of the specified column within each group, keyed by
1052
+ * the value of the provided group column (a SQL `GROUP BY`).
1053
+ *
1054
+ * ```ts
1055
+ * await CompositionAsset.minBy('name', 'score')
1056
+ * // Map(2) { 'primary' => 1, 'secondary' => 4 }
1057
+ * ```
1058
+ *
1059
+ * Only groups with at least one matching row appear in the Map. When the group
1060
+ * column is nullable, records with a `null` value are grouped under a real
1061
+ * `null` key; a group whose aggregated values are all `null` yields a `null`
1062
+ * value.
1063
+ *
1064
+ * @param groupColumn - the column to group by
1065
+ * @param aggregatedColumn - the column to take the min of within each group
1066
+ * @returns A Map from each present group value to the min of the aggregated column in that group
1067
+ */
1068
+ static async minBy(groupColumn, aggregatedColumn) {
1069
+ return await this.query().minBy(groupColumn, aggregatedColumn);
1070
+ }
1010
1071
  /**
1011
1072
  * Retrieves the sum for all values of the specified column
1012
1073
  * for this model's records.
@@ -1023,6 +1084,27 @@ export default class Dream {
1023
1084
  static async sum(columnName) {
1024
1085
  return await this.query().sum(columnName);
1025
1086
  }
1087
+ /**
1088
+ * Retrieves the sum of the specified column within each group, keyed by
1089
+ * the value of the provided group column (a SQL `GROUP BY`).
1090
+ *
1091
+ * ```ts
1092
+ * await CompositionAsset.sumBy('name', 'score')
1093
+ * // Map(2) { 'primary' => 10, 'secondary' => 4 }
1094
+ * ```
1095
+ *
1096
+ * Only groups with at least one matching row appear in the Map. When the group
1097
+ * column is nullable, records with a `null` value are grouped under a real
1098
+ * `null` key; a group whose aggregated values are all `null` yields a `null`
1099
+ * value.
1100
+ *
1101
+ * @param groupColumn - the column to group by
1102
+ * @param aggregatedColumn - the column to sum within each group
1103
+ * @returns A Map from each present group value to the sum of the aggregated column in that group
1104
+ */
1105
+ static async sumBy(groupColumn, aggregatedColumn) {
1106
+ return await this.query().sumBy(groupColumn, aggregatedColumn);
1107
+ }
1026
1108
  /**
1027
1109
  * Retrieves the average for all values of the specified column
1028
1110
  * for this model's records.
@@ -1039,6 +1121,27 @@ export default class Dream {
1039
1121
  static async avg(columnName) {
1040
1122
  return await this.query().avg(columnName);
1041
1123
  }
1124
+ /**
1125
+ * Retrieves the average of the specified column within each group, keyed by
1126
+ * the value of the provided group column (a SQL `GROUP BY`).
1127
+ *
1128
+ * ```ts
1129
+ * await CompositionAsset.avgBy('name', 'score')
1130
+ * // Map(2) { 'primary' => 5, 'secondary' => 4 }
1131
+ * ```
1132
+ *
1133
+ * Only groups with at least one matching row appear in the Map. When the group
1134
+ * column is nullable, records with a `null` value are grouped under a real
1135
+ * `null` key; a group whose aggregated values are all `null` yields a `null`
1136
+ * value.
1137
+ *
1138
+ * @param groupColumn - the column to group by
1139
+ * @param aggregatedColumn - the column to average within each group
1140
+ * @returns A Map from each present group value to the average of the aggregated column in that group
1141
+ */
1142
+ static async avgBy(groupColumn, aggregatedColumn) {
1143
+ return await this.query().avgBy(groupColumn, aggregatedColumn);
1144
+ }
1042
1145
  /**
1043
1146
  * Persists a new record, setting the provided attributes.
1044
1147
  * Automatically sets createdAt and updatedAt timestamps.
@@ -1358,6 +1461,12 @@ export default class Dream {
1358
1461
  * 3. each nested association will result in an additional record which duplicates data from the outer record. E.g., given `.leftJoinPreload('a', 'b', 'c')`, if each `a` has 10 `b` and each `b` has 10 `c`, then for one `a`, 100 records will be returned, each of which has all of the columns of `a`. `.preload('a', 'b', 'c')` would perform three separate SQL queries, but the data for a single `a` would only be returned once.
1359
1462
  * 4. the individual query becomes more complex the more associations are included
1360
1463
  * 5. associations loading associations loading associations could result in exponential amounts of data; in those cases, `.preload(...).findEach(...)` avoids instantiating massive amounts of data at once
1464
+ * 6. leftJoinPreload must enumerate every compiled column of every joined
1465
+ * model, so unlike base-model reads, `preload`/`load`, and saves, it does
1466
+ * not tolerate schema/image skew from an unplanned column drop during a
1467
+ * rolling deploy; see {@link Query.leftJoinPreload} and the
1468
+ * `ignoredColumns` getter for the two-deploy process that makes a
1469
+ * planned drop safe.
1361
1470
  *
1362
1471
  * ```ts
1363
1472
  * const user = await User.leftJoinPreload('posts', 'comments', { visibilty: 'public' }, 'replies').first()
@@ -2237,6 +2346,83 @@ export default class Dream {
2237
2346
  get table() {
2238
2347
  throw new DreamMissingRequiredOverride(this.constructor, 'table');
2239
2348
  }
2349
+ /**
2350
+ * Columns that Dream should behave as though they do not exist.
2351
+ *
2352
+ * Declaring a column ignored removes it from the generated types the next
2353
+ * time `sync` runs: it is omitted from both the db types file (the Kysely
2354
+ * `DB` interface) and the dream schema file, so it disappears from
2355
+ * `columns()` and from every place that flows from `columns()` — select
2356
+ * lists built for `preload`/`load` and `leftJoinPreload`, save hydration,
2357
+ * attribute definition, and param safety. References to the column in
2358
+ * application code become type errors, which is the point: they must be
2359
+ * removed before the column can be dropped.
2360
+ *
2361
+ * This enables safely dropping a column under rolling deploys — the same
2362
+ * problem Rails solves with `ignored_columns`. The safety requirement is
2363
+ * that no image that can run against the post-drop schema names the
2364
+ * column in any SQL it generates. To satisfy it: remove all application
2365
+ * code that uses the column, declare it here, and run `sync`; then let
2366
+ * the drop migration run only once no image lacking the declaration can
2367
+ * run against the database — whether the migration ships in a later
2368
+ * deploy of its own, or together with the declaration in a pipeline that
2369
+ * runs migrations only after the new images have rolled out. Once the
2370
+ * column is dropped, remove this declaration and resync.
2371
+ *
2372
+ * Precondition: as soon as an image built with this declaration runs,
2373
+ * the column is never placed in INSERT column lists, so before that
2374
+ * image can run, the column must be nullable or carry a database
2375
+ * default — a live `NOT NULL` column without a default fails every
2376
+ * create against the table.
2377
+ *
2378
+ * Dropping the column while an image that names it can still run leaves
2379
+ * a window during which those containers (including a rolled-back image)
2380
+ * fail with `42703 column does not exist` — `leftJoinPreload` in
2381
+ * particular has no runtime tolerance for this, since it must enumerate
2382
+ * aliased columns.
2383
+ *
2384
+ * This is a mechanism for the drop window, not for permanently hiding
2385
+ * wide columns: the ignored column is still transferred from the
2386
+ * database on every `RETURNING *` / `select *` until it is actually
2387
+ * dropped.
2388
+ *
2389
+ * The declaration is read only while `sync` generates the types files; it
2390
+ * has no runtime behavior of its own. A declared-but-not-synced model is
2391
+ * therefore not yet protected — CI should verify that `sync` produces no
2392
+ * diff. `sync` will fail loudly if a declared name is not camelCase
2393
+ * (generated column names are camelized, so any other shape could never
2394
+ * match and would be silently inert), if models sharing a table declare
2395
+ * different ignored columns, or if a model attempts to ignore a column
2396
+ * the framework itself reads and writes by name: its primary key, an STI
2397
+ * model's `type` column, any association's foreign key, polymorphic type
2398
+ * field, or `primaryKeyOverride` column, an `@Sortable` position field or
2399
+ * plain-column `@Sortable` scope, an `@Encrypted` backing column, or a
2400
+ * SoftDelete model's `deletedAt` column.
2401
+ *
2402
+ * Because an ignored column vanishes from `columns()`, runtime access via
2403
+ * type escape hatches behaves exactly like any unknown attribute: reads
2404
+ * return `undefined`, and writes assign a plain instance property that is
2405
+ * never persisted.
2406
+ *
2407
+ * ```ts
2408
+ * class User extends ApplicationModel {
2409
+ * public override get ignoredColumns() {
2410
+ * return ['legacyEmail'] as const
2411
+ * }
2412
+ * }
2413
+ * ```
2414
+ *
2415
+ * NOTE: this getter is intentionally typed as `readonly string[]` rather
2416
+ * than as a union of known column names: during the deploy that declares
2417
+ * a column ignored, the regenerated types no longer contain the column,
2418
+ * so a column-name-derived type would reject the very declaration that
2419
+ * removed it.
2420
+ *
2421
+ * @returns The list of column names this model should ignore
2422
+ */
2423
+ get ignoredColumns() {
2424
+ return [];
2425
+ }
2240
2426
  /**
2241
2427
  * @internal
2242
2428
  *
@@ -103,6 +103,12 @@ async function destroyConnectionWithinTimeout(conn, label) {
103
103
  reportLeakedDbConnections(label);
104
104
  }
105
105
  }
106
+ catch (error) {
107
+ // teardown is best-effort (the registry entry has already been removed,
108
+ // so a subsequent getConnection() builds a fresh pool), but a rejected
109
+ // destroy() must not vanish silently
110
+ DreamApp.logWithLevel('error', `[dream] failed to close db connection "${label}"`, error);
111
+ }
106
112
  finally {
107
113
  if (timer)
108
114
  clearTimeout(timer);
@@ -35,6 +35,18 @@ export const SOFT_DELETE_SCOPE_NAME = 'dream:SoftDelete';
35
35
  * return 'customDatetimeField' as const
36
36
  * }
37
37
  * }
38
+ *
39
+ * Note on indexing: Dream deliberately does not index `deleted_at`.
40
+ * The default scope's `WHERE deleted_at IS NULL` matches nearly every
41
+ * row on a healthy table, so a plain b-tree on the column is rarely
42
+ * chosen by the planner while still costing index size and write
43
+ * amplification, and Dream itself never issues a query such an index
44
+ * could serve. If your app needs one, add it yourself based on your
45
+ * own access patterns — on Postgres, the two useful shapes are a
46
+ * composite partial index on your hot lookup columns with
47
+ * `WHERE deleted_at IS NULL` (fast scoped reads), or a partial index
48
+ * `WHERE deleted_at IS NOT NULL` (purge/GC sweeps over soft-deleted
49
+ * rows). Both are Postgres-specific syntax.
38
50
  */
39
51
  export default function SoftDelete() {
40
52
  return function (target) {
@@ -58,6 +58,27 @@ export default class DreamClassTransactionBuilder {
58
58
  async count() {
59
59
  return this.queryInstance().count();
60
60
  }
61
+ /**
62
+ * Retrieves the number of records in each group, keyed by the
63
+ * value of the provided group column (a SQL `GROUP BY`).
64
+ *
65
+ * ```ts
66
+ * await ApplicationModel.transaction(async txn => {
67
+ * await User.txn(txn).countBy('name')
68
+ * // Map(2) { 'fred' => 2, 'zed' => 1 }
69
+ * })
70
+ * ```
71
+ *
72
+ * Only groups with at least one matching row appear in the Map; seed absent
73
+ * groups yourself with `map.get(key) ?? 0`. When the group column is nullable,
74
+ * records with a `null` value are grouped under a real `null` key.
75
+ *
76
+ * @param groupColumn - the column to group by
77
+ * @returns A Map from each present group value to the number of records in that group
78
+ */
79
+ async countBy(groupColumn) {
80
+ return this.queryInstance().countBy(groupColumn);
81
+ }
61
82
  /**
62
83
  * Returns a new Query instance, specifying a limit
63
84
  *
@@ -107,6 +128,29 @@ export default class DreamClassTransactionBuilder {
107
128
  async max(columnName) {
108
129
  return this.queryInstance().max(columnName);
109
130
  }
131
+ /**
132
+ * Retrieves the max value of the specified column within each group, keyed by
133
+ * the value of the provided group column (a SQL `GROUP BY`).
134
+ *
135
+ * ```ts
136
+ * await ApplicationModel.transaction(async txn => {
137
+ * await CompositionAsset.txn(txn).maxBy('name', 'score')
138
+ * // Map(2) { 'primary' => 9, 'secondary' => 4 }
139
+ * })
140
+ * ```
141
+ *
142
+ * Only groups with at least one matching row appear in the Map. When the group
143
+ * column is nullable, records with a `null` value are grouped under a real
144
+ * `null` key; a group whose aggregated values are all `null` yields a `null`
145
+ * value.
146
+ *
147
+ * @param groupColumn - the column to group by
148
+ * @param aggregatedColumn - the column to take the max of within each group
149
+ * @returns A Map from each present group value to the max of the aggregated column in that group
150
+ */
151
+ async maxBy(groupColumn, aggregatedColumn) {
152
+ return this.queryInstance().maxBy(groupColumn, aggregatedColumn);
153
+ }
110
154
  /**
111
155
  * Retrieves the min value of the specified column
112
156
  * for this model's records.
@@ -124,6 +168,29 @@ export default class DreamClassTransactionBuilder {
124
168
  async min(columnName) {
125
169
  return this.queryInstance().min(columnName);
126
170
  }
171
+ /**
172
+ * Retrieves the min value of the specified column within each group, keyed by
173
+ * the value of the provided group column (a SQL `GROUP BY`).
174
+ *
175
+ * ```ts
176
+ * await ApplicationModel.transaction(async txn => {
177
+ * await CompositionAsset.txn(txn).minBy('name', 'score')
178
+ * // Map(2) { 'primary' => 1, 'secondary' => 4 }
179
+ * })
180
+ * ```
181
+ *
182
+ * Only groups with at least one matching row appear in the Map. When the group
183
+ * column is nullable, records with a `null` value are grouped under a real
184
+ * `null` key; a group whose aggregated values are all `null` yields a `null`
185
+ * value.
186
+ *
187
+ * @param groupColumn - the column to group by
188
+ * @param aggregatedColumn - the column to take the min of within each group
189
+ * @returns A Map from each present group value to the min of the aggregated column in that group
190
+ */
191
+ async minBy(groupColumn, aggregatedColumn) {
192
+ return this.queryInstance().minBy(groupColumn, aggregatedColumn);
193
+ }
127
194
  /**
128
195
  * Retrieves the sum value of the specified column
129
196
  * for this Query
@@ -139,6 +206,29 @@ export default class DreamClassTransactionBuilder {
139
206
  async sum(columnName) {
140
207
  return this.queryInstance().sum(columnName);
141
208
  }
209
+ /**
210
+ * Retrieves the sum of the specified column within each group, keyed by
211
+ * the value of the provided group column (a SQL `GROUP BY`).
212
+ *
213
+ * ```ts
214
+ * await ApplicationModel.transaction(async txn => {
215
+ * await CompositionAsset.txn(txn).sumBy('name', 'score')
216
+ * // Map(2) { 'primary' => 10, 'secondary' => 4 }
217
+ * })
218
+ * ```
219
+ *
220
+ * Only groups with at least one matching row appear in the Map. When the group
221
+ * column is nullable, records with a `null` value are grouped under a real
222
+ * `null` key; a group whose aggregated values are all `null` yields a `null`
223
+ * value.
224
+ *
225
+ * @param groupColumn - the column to group by
226
+ * @param aggregatedColumn - the column to sum within each group
227
+ * @returns A Map from each present group value to the sum of the aggregated column in that group
228
+ */
229
+ async sumBy(groupColumn, aggregatedColumn) {
230
+ return this.queryInstance().sumBy(groupColumn, aggregatedColumn);
231
+ }
142
232
  /**
143
233
  * Retrieves the average value of the specified column
144
234
  * for this Query
@@ -154,6 +244,29 @@ export default class DreamClassTransactionBuilder {
154
244
  async avg(columnName) {
155
245
  return this.queryInstance().avg(columnName);
156
246
  }
247
+ /**
248
+ * Retrieves the average of the specified column within each group, keyed by
249
+ * the value of the provided group column (a SQL `GROUP BY`).
250
+ *
251
+ * ```ts
252
+ * await ApplicationModel.transaction(async txn => {
253
+ * await CompositionAsset.txn(txn).avgBy('name', 'score')
254
+ * // Map(2) { 'primary' => 5, 'secondary' => 4 }
255
+ * })
256
+ * ```
257
+ *
258
+ * Only groups with at least one matching row appear in the Map. When the group
259
+ * column is nullable, records with a `null` value are grouped under a real
260
+ * `null` key; a group whose aggregated values are all `null` yields a `null`
261
+ * value.
262
+ *
263
+ * @param groupColumn - the column to group by
264
+ * @param aggregatedColumn - the column to average within each group
265
+ * @returns A Map from each present group value to the average of the aggregated column in that group
266
+ */
267
+ async avgBy(groupColumn, aggregatedColumn) {
268
+ return this.queryInstance().avgBy(groupColumn, aggregatedColumn);
269
+ }
157
270
  /**
158
271
  * Persists a new record, setting the provided attributes.
159
272
  * Automatically sets createdAt and updatedAt timestamps.