@rvoh/dream 2.18.0 → 2.19.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 (238) hide show
  1. package/dist/cjs/src/Dream.js +103 -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 +137 -0
  6. package/dist/cjs/src/dream/QueryDriver/Base.js +84 -0
  7. package/dist/cjs/src/dream/QueryDriver/Kysely.js +358 -67
  8. package/dist/cjs/src/errors/CannotNamespaceAssociationFilterToAnotherTable.js +27 -0
  9. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +21 -13
  10. package/dist/esm/src/Dream.js +103 -0
  11. package/dist/esm/src/db/DreamDbConnection.js +6 -0
  12. package/dist/esm/src/decorators/class/SoftDelete.js +12 -0
  13. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +113 -0
  14. package/dist/esm/src/dream/Query.js +137 -0
  15. package/dist/esm/src/dream/QueryDriver/Base.js +84 -0
  16. package/dist/esm/src/dream/QueryDriver/Kysely.js +358 -67
  17. package/dist/esm/src/errors/CannotNamespaceAssociationFilterToAnotherTable.js +27 -0
  18. package/dist/esm/src/helpers/cli/generateMigrationContent.js +21 -13
  19. package/dist/types/src/Dream.d.ts +97 -4
  20. package/dist/types/src/decorators/class/SoftDelete.d.ts +12 -0
  21. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +103 -0
  22. package/dist/types/src/dream/Query.d.ts +127 -0
  23. package/dist/types/src/dream/QueryDriver/Base.d.ts +69 -0
  24. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +166 -8
  25. package/dist/types/src/errors/CannotNamespaceAssociationFilterToAnotherTable.d.ts +8 -0
  26. package/dist/types/src/types/associations/shared.d.ts +4 -1
  27. package/dist/types/src/types/dream.d.ts +17 -0
  28. package/dist/types/src/types/{associations → types/associations}/shared.ts +10 -1
  29. package/dist/types/src/types/{dream.ts → types/dream.ts} +53 -0
  30. package/dist/types/src/types/{variadic.ts → types/variadic.ts} +179 -140
  31. package/dist/types/src/types/variadic.d.ts +15 -10
  32. package/docs/assets/hierarchy.js +1 -1
  33. package/docs/assets/search.js +1 -1
  34. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  35. package/docs/classes/db.KyselyQueryDriver.html +78 -34
  36. package/docs/classes/db.PostgresQueryDriver.html +79 -35
  37. package/docs/classes/db.QueryDriverBase.html +77 -33
  38. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  39. package/docs/classes/errors.ColumnOverflow.html +3 -3
  40. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  41. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  42. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  43. package/docs/classes/errors.DecryptionError.html +2 -2
  44. package/docs/classes/errors.DecryptionParseError.html +2 -2
  45. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  46. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  47. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  48. package/docs/classes/errors.InvalidClockTime.html +2 -2
  49. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  50. package/docs/classes/errors.InvalidDateTime.html +2 -2
  51. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  52. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  53. package/docs/classes/errors.NotNullViolation.html +3 -3
  54. package/docs/classes/errors.RecordNotFound.html +3 -3
  55. package/docs/classes/errors.ValidationError.html +3 -3
  56. package/docs/classes/index.CalendarDate.html +33 -33
  57. package/docs/classes/index.ClockTime.html +32 -32
  58. package/docs/classes/index.ClockTimeTz.html +35 -35
  59. package/docs/classes/index.DateTime.html +86 -86
  60. package/docs/classes/index.Decorators.html +19 -19
  61. package/docs/classes/index.Dream.html +182 -119
  62. package/docs/classes/index.DreamApp.html +10 -10
  63. package/docs/classes/index.DreamTransaction.html +2 -2
  64. package/docs/classes/index.Env.html +2 -2
  65. package/docs/classes/index.Query.html +144 -57
  66. package/docs/classes/system.CliFileWriter.html +4 -4
  67. package/docs/classes/system.DreamBin.html +2 -2
  68. package/docs/classes/system.DreamCLI.html +7 -7
  69. package/docs/classes/system.DreamImporter.html +2 -2
  70. package/docs/classes/system.DreamLogos.html +2 -2
  71. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  72. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  73. package/docs/classes/system.PathHelpers.html +3 -3
  74. package/docs/classes/utils.Encrypt.html +3 -3
  75. package/docs/classes/utils.Range.html +2 -2
  76. package/docs/functions/db.closeAllDbConnections.html +1 -1
  77. package/docs/functions/db.dreamDbConnections.html +1 -1
  78. package/docs/functions/db.untypedDb.html +1 -1
  79. package/docs/functions/db.validateColumn.html +1 -1
  80. package/docs/functions/db.validateTable.html +1 -1
  81. package/docs/functions/errors.pgErrorType.html +1 -1
  82. package/docs/functions/index.DreamSerializer.html +1 -1
  83. package/docs/functions/index.ObjectSerializer.html +1 -1
  84. package/docs/functions/index.ReplicaSafe.html +1 -1
  85. package/docs/functions/index.STI.html +1 -1
  86. package/docs/functions/index.SoftDelete.html +12 -1
  87. package/docs/functions/utils.camelize.html +1 -1
  88. package/docs/functions/utils.capitalize.html +1 -1
  89. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  90. package/docs/functions/utils.compact.html +1 -1
  91. package/docs/functions/utils.groupBy.html +1 -1
  92. package/docs/functions/utils.hyphenize.html +1 -1
  93. package/docs/functions/utils.intersection.html +1 -1
  94. package/docs/functions/utils.isEmpty.html +1 -1
  95. package/docs/functions/utils.normalizeUnicode.html +1 -1
  96. package/docs/functions/utils.pascalize.html +1 -1
  97. package/docs/functions/utils.percent.html +1 -1
  98. package/docs/functions/utils.range.html +1 -1
  99. package/docs/functions/utils.round.html +1 -1
  100. package/docs/functions/utils.sanitizeString.html +1 -1
  101. package/docs/functions/utils.snakeify.html +1 -1
  102. package/docs/functions/utils.sort.html +1 -1
  103. package/docs/functions/utils.sortBy.html +1 -1
  104. package/docs/functions/utils.sortObjectByKey.html +1 -1
  105. package/docs/functions/utils.sortObjectByValue.html +1 -1
  106. package/docs/functions/utils.uncapitalize.html +1 -1
  107. package/docs/functions/utils.uniq.html +1 -1
  108. package/docs/hierarchy.html +1 -1
  109. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  110. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  111. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  112. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  113. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  114. package/docs/interfaces/types.DecoratorContext.html +2 -2
  115. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  116. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  117. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  118. package/docs/interfaces/types.DurationObject.html +2 -2
  119. package/docs/interfaces/types.EncryptOptions.html +2 -2
  120. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  121. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  122. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  123. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  124. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  125. package/docs/types/openapi.OpenapiFormats.html +1 -1
  126. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  127. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  128. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  129. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  130. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  131. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  132. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  133. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  134. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  135. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  136. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  137. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  138. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  139. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  140. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  141. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  142. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  143. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  144. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  145. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  146. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  147. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  148. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  149. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  150. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  151. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  152. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  153. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  154. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  155. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  156. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  157. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  158. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  159. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  160. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  161. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  162. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  163. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  164. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  165. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  166. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  167. package/docs/types/types.CalendarDateObject.html +1 -1
  168. package/docs/types/types.Camelized.html +1 -1
  169. package/docs/types/types.ClockTimeObject.html +1 -1
  170. package/docs/types/types.DbConnectionType.html +1 -1
  171. package/docs/types/types.DbTypes.html +1 -1
  172. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  173. package/docs/types/types.DreamAttributes.html +1 -1
  174. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  175. package/docs/types/types.DreamClassColumn.html +1 -1
  176. package/docs/types/types.DreamColumn.html +1 -1
  177. package/docs/types/types.DreamColumnNames.html +1 -1
  178. package/docs/types/types.DreamLogLevel.html +1 -1
  179. package/docs/types/types.DreamLogger.html +2 -2
  180. package/docs/types/types.DreamModelSerializerType.html +1 -1
  181. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  182. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  183. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  184. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  185. package/docs/types/types.DreamSerializable.html +1 -1
  186. package/docs/types/types.DreamSerializableArray.html +1 -1
  187. package/docs/types/types.DreamSerializerKey.html +1 -1
  188. package/docs/types/types.DreamSerializers.html +1 -1
  189. package/docs/types/types.DreamVirtualColumns.html +1 -1
  190. package/docs/types/types.DurationUnit.html +1 -1
  191. package/docs/types/types.EncryptAlgorithm.html +1 -1
  192. package/docs/types/types.HasManyStatement.html +1 -1
  193. package/docs/types/types.HasOneStatement.html +1 -1
  194. package/docs/types/types.Hyphenized.html +1 -1
  195. package/docs/types/types.Pascalized.html +1 -1
  196. package/docs/types/types.PrimaryKeyType.html +1 -1
  197. package/docs/types/types.RoundingPrecision.html +1 -1
  198. package/docs/types/types.SerializerCasing.html +1 -1
  199. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  200. package/docs/types/types.Snakeified.html +1 -1
  201. package/docs/types/types.StrictInterface.html +1 -1
  202. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  203. package/docs/types/types.UpdateableProperties.html +1 -1
  204. package/docs/types/types.ValidationType.html +1 -1
  205. package/docs/types/types.ViewModel.html +2 -2
  206. package/docs/types/types.ViewModelClass.html +1 -1
  207. package/docs/types/types.WeekdayName.html +1 -1
  208. package/docs/types/types.WhereStatementForDream.html +1 -1
  209. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  210. package/docs/variables/index.DreamConst.html +1 -1
  211. package/docs/variables/index.ops.html +1 -1
  212. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  213. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  214. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  215. package/docs/variables/system.primaryKeyTypes.html +1 -1
  216. package/package.json +3 -3
  217. package/dist/cjs/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.js +0 -11
  218. package/dist/cjs/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.js +0 -17
  219. package/dist/esm/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.js +0 -11
  220. package/dist/esm/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.js +0 -17
  221. package/dist/types/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.d.ts +0 -13
  222. package/dist/types/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.d.ts +0 -12
  223. /package/dist/types/src/types/{associations → types/associations}/belongsTo.ts +0 -0
  224. /package/dist/types/src/types/{associations → types/associations}/hasMany.ts +0 -0
  225. /package/dist/types/src/types/{associations → types/associations}/hasOne.ts +0 -0
  226. /package/dist/types/src/types/{calendardate.ts → types/calendardate.ts} +0 -0
  227. /package/dist/types/src/types/{clocktime.ts → types/clocktime.ts} +0 -0
  228. /package/dist/types/src/types/{datetime.ts → types/datetime.ts} +0 -0
  229. /package/dist/types/src/types/{db.ts → types/db.ts} +0 -0
  230. /package/dist/types/src/types/{lifecycle.ts → types/lifecycle.ts} +0 -0
  231. /package/dist/types/src/types/{logger.ts → types/logger.ts} +0 -0
  232. /package/dist/types/src/types/{moduleDeclarations → types/moduleDeclarations}/luxon.d.ts +0 -0
  233. /package/dist/types/src/types/{openapi.ts → types/openapi.ts} +0 -0
  234. /package/dist/types/src/types/{query.ts → types/query.ts} +0 -0
  235. /package/dist/types/src/types/{recursiveSerialization.ts → types/recursiveSerialization.ts} +0 -0
  236. /package/dist/types/src/types/{serializer.ts → types/serializer.ts} +0 -0
  237. /package/dist/types/src/types/{utils.ts → types/utils.ts} +0 -0
  238. /package/dist/types/src/types/{validation.ts → types/validation.ts} +0 -0
@@ -26,8 +26,8 @@ import MissingRequiredAssociationAndClause from '../../errors/associations/Missi
26
26
  import MissingRequiredPassthroughForAssociationAndClause from '../../errors/associations/MissingRequiredPassthroughForAssociationAndClause.js';
27
27
  import MissingThroughAssociation from '../../errors/associations/MissingThroughAssociation.js';
28
28
  import MissingThroughAssociationSource from '../../errors/associations/MissingThroughAssociationSource.js';
29
- import ThroughAssociationConditionsIncompatibleWithThroughAssociationSource from '../../errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.js';
30
29
  import CannotNegateSimilarityClause from '../../errors/CannotNegateSimilarityClause.js';
30
+ import CannotNamespaceAssociationFilterToAnotherTable from '../../errors/CannotNamespaceAssociationFilterToAnotherTable.js';
31
31
  import CannotPassUndefinedAsAValueToAWhereClause from '../../errors/CannotPassUndefinedAsAValueToAWhereClause.js';
32
32
  import CheckConstraintViolation from '../../errors/db/CheckConstraintViolation.js';
33
33
  import ColumnOverflow from '../../errors/db/ColumnOverflow.js';
@@ -55,7 +55,6 @@ import CurriedOpsStatement from '../../ops/curried-ops-statement.js';
55
55
  import OpsStatement from '../../ops/ops-statement.js';
56
56
  import { DreamConst, primaryKeyTypes } from '../constants.js';
57
57
  import DreamTransaction from '../DreamTransaction.js';
58
- import throughAssociationHasOptionsBesidesThroughAndSource from '../internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.js';
59
58
  import associationStringToNameAndAlias from '../internal/associationStringToNameAndAlias.js';
60
59
  import executeDatabaseQuery from '../internal/executeDatabaseQuery.js';
61
60
  import extractAssignableAssociationAttributes from '../internal/extractAssignableAssociationAttributes.js';
@@ -213,6 +212,10 @@ export default class KyselyQueryDriver extends QueryDriverBase {
213
212
  await DreamCLI.logger.logProgress('[dream] sync failed, reverting file contents...', async () => {
214
213
  await CliFileWriter.revert();
215
214
  });
215
+ // rethrow after reverting so callers (e.g. the `psy sync` and
216
+ // `db:migrate` CLI commands) exit nonzero instead of reporting success
217
+ // with stale generated types
218
+ throw error;
216
219
  }
217
220
  }
218
221
  static get syncDialect() {
@@ -441,6 +444,24 @@ export default class KyselyQueryDriver extends QueryDriverBase {
441
444
  const data = await executeDatabaseQuery(kyselyQuery, 'executeTakeFirstOrThrow');
442
445
  return data.max;
443
446
  }
447
+ /**
448
+ * Retrieves the max value of the specified column within each group,
449
+ * keyed by the value of the provided group column.
450
+ *
451
+ * ```ts
452
+ * await CompositionAsset.query().maxBy('name', 'score')
453
+ * // Map(2) { 'primary' => 9, 'secondary' => 4 }
454
+ * ```
455
+ *
456
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
457
+ * @param aggregatedColumn - the column to take the max of within each group
458
+ * @returns A Map from each present group value to the max of the aggregated column in that group
459
+ */
460
+ async maxBy(groupColumn, aggregatedColumn) {
461
+ // eslint-disable-next-line @typescript-eslint/unbound-method
462
+ const { max } = this.dbFor('select').fn;
463
+ return this.groupedAggregate(groupColumn, max(aggregatedColumn));
464
+ }
444
465
  /**
445
466
  * Retrieves the min value of the specified column
446
467
  * for this Query
@@ -464,6 +485,24 @@ export default class KyselyQueryDriver extends QueryDriverBase {
464
485
  const data = await executeDatabaseQuery(kyselyQuery, 'executeTakeFirstOrThrow');
465
486
  return data.min;
466
487
  }
488
+ /**
489
+ * Retrieves the min value of the specified column within each group,
490
+ * keyed by the value of the provided group column.
491
+ *
492
+ * ```ts
493
+ * await CompositionAsset.query().minBy('name', 'score')
494
+ * // Map(2) { 'primary' => 1, 'secondary' => 4 }
495
+ * ```
496
+ *
497
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
498
+ * @param aggregatedColumn - the column to take the min of within each group
499
+ * @returns A Map from each present group value to the min of the aggregated column in that group
500
+ */
501
+ async minBy(groupColumn, aggregatedColumn) {
502
+ // eslint-disable-next-line @typescript-eslint/unbound-method
503
+ const { min } = this.dbFor('select').fn;
504
+ return this.groupedAggregate(groupColumn, min(aggregatedColumn));
505
+ }
467
506
  /**
468
507
  * Retrieves the sum value of the specified column
469
508
  * for this Query
@@ -487,6 +526,24 @@ export default class KyselyQueryDriver extends QueryDriverBase {
487
526
  const data = await executeDatabaseQuery(kyselyQuery, 'executeTakeFirstOrThrow');
488
527
  return data.sum === null ? null : parseFloat(data.sum);
489
528
  }
529
+ /**
530
+ * Retrieves the sum of the specified column within each group,
531
+ * keyed by the value of the provided group column.
532
+ *
533
+ * ```ts
534
+ * await CompositionAsset.query().sumBy('name', 'score')
535
+ * // Map(2) { 'primary' => 10, 'secondary' => 4 }
536
+ * ```
537
+ *
538
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
539
+ * @param aggregatedColumn - the column to sum within each group
540
+ * @returns A Map from each present group value to the sum of the aggregated column in that group
541
+ */
542
+ async sumBy(groupColumn, aggregatedColumn) {
543
+ // eslint-disable-next-line @typescript-eslint/unbound-method
544
+ const { sum } = this.dbFor('select').fn;
545
+ return this.groupedAggregate(groupColumn, sum(aggregatedColumn), rawSum => rawSum === null ? null : parseFloat(rawSum));
546
+ }
490
547
  /**
491
548
  * Retrieves the average value of the specified column
492
549
  * for this Query
@@ -510,6 +567,24 @@ export default class KyselyQueryDriver extends QueryDriverBase {
510
567
  const data = await executeDatabaseQuery(kyselyQuery, 'executeTakeFirstOrThrow');
511
568
  return data.avg;
512
569
  }
570
+ /**
571
+ * Retrieves the average of the specified column within each group,
572
+ * keyed by the value of the provided group column.
573
+ *
574
+ * ```ts
575
+ * await CompositionAsset.query().avgBy('name', 'score')
576
+ * // Map(2) { 'primary' => 5, 'secondary' => 4 }
577
+ * ```
578
+ *
579
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
580
+ * @param aggregatedColumn - the column to average within each group
581
+ * @returns A Map from each present group value to the average of the aggregated column in that group
582
+ */
583
+ async avgBy(groupColumn, aggregatedColumn) {
584
+ // eslint-disable-next-line @typescript-eslint/unbound-method
585
+ const { avg } = this.dbFor('select').fn;
586
+ return this.groupedAggregate(groupColumn, avg(aggregatedColumn));
587
+ }
513
588
  /**
514
589
  * Retrieves the number of records in the database
515
590
  *
@@ -535,6 +610,59 @@ export default class KyselyQueryDriver extends QueryDriverBase {
535
610
  const data = await executeDatabaseQuery(kyselyQuery, 'executeTakeFirstOrThrow');
536
611
  return parseInt(data.tablecount.toString());
537
612
  }
613
+ /**
614
+ * Retrieves the number of records in each group, keyed by the
615
+ * value of the provided group column.
616
+ *
617
+ * ```ts
618
+ * await User.query().countBy('name')
619
+ * // Map(2) { 'fred' => 2, 'zed' => 1 }
620
+ * ```
621
+ *
622
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
623
+ * @returns A Map from each present group value to the number of records in that group
624
+ */
625
+ async countBy(groupColumn) {
626
+ // eslint-disable-next-line @typescript-eslint/unbound-method
627
+ const { count } = this.dbFor('select').fn;
628
+ const primaryKeyRef = this.namespaceColumn(this.query.dreamInstance['_primaryKey']);
629
+ return this.groupedAggregate(groupColumn, count(primaryKeyRef), rawCount => parseInt(rawCount.toString()));
630
+ }
631
+ /**
632
+ * @internal
633
+ *
634
+ * Shared plumbing for grouped aggregates (`countBy` and the value-aggregate
635
+ * siblings `minBy` / `maxBy` / `sumBy` / `avgBy`). Selects the group column
636
+ * alongside a single aggregate expression, groups by the group column, executes,
637
+ * and folds the resulting rows into a Map keyed by the group value.
638
+ *
639
+ * The group column and the aggregate are selected under stable, underscore-free
640
+ * aliases so they can be read back off each row without being affected by
641
+ * Kysely's CamelCasePlugin (which would otherwise mangle a namespaced alias),
642
+ * mirroring the short-alias strategy used by `pluck`.
643
+ *
644
+ * @param groupColumn - the column to GROUP BY (base or joined-association-namespaced)
645
+ * @param aggregateExpression - the Kysely aggregate expression to select per group (e.g. `count(pk)`)
646
+ * @param coerceValue - maps each raw aggregate value to the value stored in the returned Map
647
+ * @returns A Map from each present group value to its coerced aggregate value
648
+ */
649
+ async groupedAggregate(groupColumn, aggregateExpression, coerceValue) {
650
+ const groupColumnRef = this.namespaceColumn(groupColumn);
651
+ let kyselyQuery = new this.constructor(this.query).buildSelect({
652
+ bypassSelectAll: true,
653
+ bypassOrder: true,
654
+ });
655
+ kyselyQuery = kyselyQuery
656
+ .select(`${groupColumnRef} as groupvalue`)
657
+ .select(aggregateExpression.as('aggregatevalue'))
658
+ .groupBy(groupColumnRef);
659
+ const rows = await executeDatabaseQuery(kyselyQuery, 'execute');
660
+ const result = new Map();
661
+ for (const row of rows) {
662
+ result.set(row.groupvalue, coerceValue ? coerceValue(row.aggregatevalue) : row.aggregatevalue);
663
+ }
664
+ return result;
665
+ }
538
666
  /**
539
667
  * @internal
540
668
  *
@@ -999,6 +1127,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
999
1127
  }, {}), {
1000
1128
  negate,
1001
1129
  disallowSimilarityOperator,
1130
+ expectedAlias: rootTableOrAssociationAlias,
1002
1131
  });
1003
1132
  }
1004
1133
  buildCommon(kyselyQuery) {
@@ -1041,20 +1170,25 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1041
1170
  kyselyQuery = kyselyQuery.where((eb) => eb.and([
1042
1171
  ...this.aliasWhereStatements(query['whereStatements'], query['baseSqlAlias']).map(whereStatement => this.whereStatementToExpressionWrapper(this.dreamClass, eb, whereStatement, {
1043
1172
  disallowSimilarityOperator: false,
1173
+ expectedAlias: query['baseSqlAlias'],
1044
1174
  })),
1045
1175
  ...this.aliasWhereStatements(query['whereNotStatements'], query['baseSqlAlias']).map(whereNotStatement => this.whereStatementToExpressionWrapper(this.dreamClass, eb, whereNotStatement, {
1046
1176
  negate: true,
1177
+ expectedAlias: query['baseSqlAlias'],
1047
1178
  })),
1048
- ...query['whereAnyStatements'].map(whereAnyStatements => eb.or(this.aliasWhereStatements(whereAnyStatements, query['baseSqlAlias']).map(whereAnyStatement => this.whereStatementToExpressionWrapper(this.dreamClass, eb, whereAnyStatement)))),
1179
+ ...query['whereAnyStatements'].map(whereAnyStatements => eb.or(this.aliasWhereStatements(whereAnyStatements, query['baseSqlAlias']).map(whereAnyStatement => this.whereStatementToExpressionWrapper(this.dreamClass, eb, whereAnyStatement, {
1180
+ expectedAlias: query['baseSqlAlias'],
1181
+ })))),
1049
1182
  ]));
1050
1183
  }
1051
1184
  return kyselyQuery;
1052
1185
  }
1053
- whereStatementToExpressionWrapper(dreamClass, eb, whereStatement, { negate = false, disallowSimilarityOperator = true, } = {}) {
1186
+ whereStatementToExpressionWrapper(dreamClass, eb, whereStatement, { negate = false, disallowSimilarityOperator = true, expectedAlias, } = {}) {
1054
1187
  const clauses = compact(Object.keys(whereStatement)
1055
1188
  .filter(key => whereStatement[key] !== DreamConst.required)
1056
1189
  .map(attr => {
1057
1190
  const val = whereStatement[attr];
1191
+ this.validateAssociationFilterAlias(attr, val, expectedAlias);
1058
1192
  if (val?.isOpsStatement &&
1059
1193
  val.shouldBypassWhereStatement) {
1060
1194
  if (disallowSimilarityOperator)
@@ -1063,6 +1197,10 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1063
1197
  // and should be ommited from the where clause directly
1064
1198
  return;
1065
1199
  }
1200
+ if (this.isAssociationInstanceArray(dreamClass, attr, val))
1201
+ return this.associationInstanceArrayToExpressionWrapper(dreamClass, eb, attr, val, {
1202
+ negate,
1203
+ });
1066
1204
  const { a, b, c, a2, b2, c2 } = this.dreamWhereStatementToExpressionBuilderParts(dreamClass, attr, val);
1067
1205
  // postgres is unable to handle WHERE IN statements with blank arrays, such as in
1068
1206
  // "WHERE id IN ()", meaning that:
@@ -1106,7 +1244,13 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1106
1244
  //
1107
1245
  }
1108
1246
  else if (b === '=' && negate) {
1109
- return eb.and([eb(a, '=', c), eb(a, 'is not', null)]);
1247
+ const conditions = [eb(a, '=', c), eb(a, 'is not', null)];
1248
+ // when a second condition is present (e.g. the type field of a polymorphic
1249
+ // association), it must be included so that the caller negates the full
1250
+ // conjunction rather than the first condition alone
1251
+ if (b2)
1252
+ conditions.push(eb(a2, b2, c2), eb(a2, 'is not', null));
1253
+ return eb.and(conditions);
1110
1254
  //
1111
1255
  }
1112
1256
  else if (b === '!=' && c !== null) {
@@ -1147,6 +1291,95 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1147
1291
  return eb.and([eb(a, 'not in', compactedC), isNotNullStatement]);
1148
1292
  return isNotNullStatement;
1149
1293
  }
1294
+ /**
1295
+ * @internal
1296
+ *
1297
+ * A Dream instance (or array of Dream instances) as a where-clause value is
1298
+ * resolved as an association of the dreamClass whose statement is being
1299
+ * compiled, so a key namespaced to a different table (e.g.
1300
+ * `where({ 'c.user': user })` after `innerJoin('composition as c')`) would
1301
+ * silently resolve the association against the wrong class, emitting invalid
1302
+ * SQL or SQL that filters the wrong table. Such keys are already rejected at
1303
+ * the type level; this guard rejects them at runtime. Un-namespaced keys and
1304
+ * keys namespaced to the alias the statement applies to are unaffected, as
1305
+ * are callers that do not declare an expected alias.
1306
+ */
1307
+ validateAssociationFilterAlias(attr, val, expectedAlias) {
1308
+ if (expectedAlias === undefined)
1309
+ return;
1310
+ const isolatedColumn = maybeNamespacedColumnNameToColumnName(attr);
1311
+ if (isolatedColumn === attr)
1312
+ return;
1313
+ const namespace = attr.slice(0, attr.length - isolatedColumn.length - 1);
1314
+ if (namespace === expectedAlias)
1315
+ return;
1316
+ if (val instanceof Dream ||
1317
+ (Array.isArray(val) && val.length > 0 && val.every(element => element instanceof Dream)))
1318
+ throw new CannotNamespaceAssociationFilterToAnotherTable(this.dreamClass, attr, expectedAlias);
1319
+ }
1320
+ /**
1321
+ * @internal
1322
+ *
1323
+ * An array under an association name can only be an array of Dream instances
1324
+ * (association names are not columns). The every-element check simply avoids
1325
+ * changing the handling of invalid runtime input (e.g. an array of ids under
1326
+ * an association name), which the type system already rejects.
1327
+ */
1328
+ isAssociationInstanceArray(dreamClass, attr, val) {
1329
+ return (Array.isArray(val) &&
1330
+ dreamClass.associationNames.includes(maybeNamespacedColumnNameToColumnName(attr)) &&
1331
+ val.every(element => element instanceof Dream));
1332
+ }
1333
+ /**
1334
+ * @internal
1335
+ *
1336
+ * Expands an array of Dream instances under a BelongsTo association name into
1337
+ * foreign key filtering:
1338
+ * - non-polymorphic: `foreignKey IN (...)`
1339
+ * - polymorphic: instances are grouped by their reference type (STI children
1340
+ * collapse into their base class), each group becoming
1341
+ * `(foreignKey IN (...) AND foreignKeyTypeField = 'TheType')`, with multiple
1342
+ * groups ORed together
1343
+ * - empty array: matches nothing (FALSE); when the caller negates, NOT(FALSE)
1344
+ * matches everything, mirroring empty `in`/`not in` array semantics
1345
+ *
1346
+ * When `negate` is set, IS NOT NULL conditions are included so that the
1347
+ * caller's negation also matches records in which the foreign key (or type
1348
+ * field) is null, consistent with negation of single Dream instances and of
1349
+ * `in` arrays.
1350
+ */
1351
+ associationInstanceArrayToExpressionWrapper(dreamClass, eb, attr, instances, { negate }) {
1352
+ if (instances.length === 0)
1353
+ return sql `FALSE`;
1354
+ const isolatedColumn = maybeNamespacedColumnNameToColumnName(attr);
1355
+ // preserve any table alias prefix from the attribute (e.g. `posts.user` -> `posts.`)
1356
+ // on the columns we compare against
1357
+ const namespacePrefix = attr.slice(0, attr.length - isolatedColumn.length);
1358
+ const association = dreamClass['associationMetadataMap']()[isolatedColumn];
1359
+ const foreignKeyColumn = `${namespacePrefix}${association.foreignKey()}`;
1360
+ const primaryKeyValuesFor = (instances) => instances.map(instance => {
1361
+ const primaryKeyValue = association.primaryKeyValue(instance);
1362
+ if (primaryKeyValue === undefined)
1363
+ throw new CannotPassUndefinedAsAValueToAWhereClause(this.dreamClass, foreignKeyColumn);
1364
+ return primaryKeyValue;
1365
+ });
1366
+ if (!association.polymorphic) {
1367
+ const inClause = eb(foreignKeyColumn, 'in', primaryKeyValuesFor(instances));
1368
+ return negate ? eb.and([inClause, eb(foreignKeyColumn, 'is not', null)]) : inClause;
1369
+ }
1370
+ const typeColumn = `${namespacePrefix}${association.foreignKeyTypeField()}`;
1371
+ const instancesByType = groupBy(instances, instance => instance.referenceTypeString);
1372
+ const typeClauses = Object.keys(instancesByType).map(referenceTypeString => {
1373
+ const conditions = [
1374
+ eb(foreignKeyColumn, 'in', primaryKeyValuesFor(instancesByType[referenceTypeString])),
1375
+ eb(typeColumn, '=', referenceTypeString),
1376
+ ];
1377
+ if (negate)
1378
+ conditions.push(eb(foreignKeyColumn, 'is not', null), eb(typeColumn, 'is not', null));
1379
+ return eb.and(conditions);
1380
+ });
1381
+ return eb.or(typeClauses);
1382
+ }
1150
1383
  dreamWhereStatementToExpressionBuilderParts(dreamClass, attr, val) {
1151
1384
  let a;
1152
1385
  let b;
@@ -1260,7 +1493,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1260
1493
  explicitAlias: alias,
1261
1494
  joinAndStatement,
1262
1495
  joinType,
1263
- previousThroughAssociation: undefined,
1496
+ previousThroughAssociations: [],
1264
1497
  dreamClassThroughAssociationWantsToHydrate: undefined,
1265
1498
  });
1266
1499
  query = results.query;
@@ -1563,20 +1796,35 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1563
1796
  * public b
1564
1797
  * ```
1565
1798
  *
1799
+ * Options declared on a through association (`and`/`andAny`/`andNot`/`selfAnd`/
1800
+ * `selfAndNot`/`order`/`distinct`) are applied at the join of the table where the
1801
+ * through association's target model materializes. When a source is itself a
1802
+ * through association, that join is only reached after bridging further through
1803
+ * associations, so each through association is pushed onto the
1804
+ * `previousThroughAssociations` stack (along with the alias `selfAnd`/`selfAndNot`
1805
+ * clauses reference) before the terminal `applyOneJoin` call, and the stack is
1806
+ * threaded through every recursion until `addAssociationJoinStatementToQuery`
1807
+ * reaches a concrete (non-through) association, where every stacked entry is
1808
+ * applied to that join.
1809
+ *
1566
1810
  * Then `MyModel.leftJoinPreload('myB')` is processed as follows:
1567
1811
  * - `applyOneJoin` is called with the `myB` association
1568
1812
  * - `joinsBridgeThroughAssociations` is called with the `myB` association
1569
1813
  * - `joinsBridgeThroughAssociations` is called with the `myA` association
1570
1814
  * - `addAssociationJoinStatementToQuery` is called with the `otherModel` association
1571
- * - `applyOneJoin` is called with the `a` association from OtherModel
1572
- * - `joinsBridgeThroughAssociations` is called with the `a` association from OtherModel
1573
- * // throw ThroughAssociationConditionsIncompatibleWithThroughAssociationSource if
1574
- * // myA in MyModel defines conditions, distinct, or order
1815
+ * - `applyOneJoin` is called with the `a` association from OtherModel, with `myA` pushed
1816
+ * onto the previousThroughAssociations stack
1817
+ * - `joinsBridgeThroughAssociations` is called with the `a` association from OtherModel,
1818
+ * inheriting the stack
1575
1819
  * - `addAssociationJoinStatementToQuery` is called with the `aToOtherModelJoinModel` association
1576
- * - `applyOneJoin` is called with the `a` association from AToOtherModelJoinModel with conditions (if present) on `a` defined on OtherModel
1577
- * - `addAssociationJoinStatementToQuery` is called with the `myA` association
1578
- * - `applyOneJoin` is called with the `b` association from A with conditions (if present) from `myB` defined on MyModel
1579
- * - `addAssociationJoinStatementToQuery` is called with the `myB` association
1820
+ * - `applyOneJoin` is called with the `a` association from AToOtherModelJoinModel, with the
1821
+ * `a` association from OtherModel pushed onto the stack
1822
+ * - `addAssociationJoinStatementToQuery` is called with the `a` association from
1823
+ * AToOtherModelJoinModel, applying the options (if present) of every stacked through
1824
+ * association (`myA` defined on MyModel, `a` defined on OtherModel) to the `through_as` join
1825
+ * - `applyOneJoin` is called with the `b` association from A, with `myB` pushed onto the stack
1826
+ * - `addAssociationJoinStatementToQuery` is called with the `b` association from A, applying
1827
+ * the options (if present) of `myB` defined on MyModel to the `through_bs` join
1580
1828
  */
1581
1829
  joinsBridgeThroughAssociations({ query, dreamClassTheAssociationIsDefinedOn, throughAssociation,
1582
1830
  /**
@@ -1598,7 +1846,13 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1598
1846
  /**
1599
1847
  * previousTableAlias is always set
1600
1848
  */
1601
- previousTableAlias, joinAndStatement, joinType, }) {
1849
+ previousTableAlias, joinAndStatement,
1850
+ /**
1851
+ * Through associations from further out in the association chain whose
1852
+ * options are waiting to be applied at the terminal concrete join of this
1853
+ * chain (see the doc comment above this method).
1854
+ */
1855
+ previousThroughAssociations, joinType, }) {
1602
1856
  /**
1603
1857
  * `through` associations always point to other associations on the same model
1604
1858
  * they are defined on. So when we want to find an association referenced by a
@@ -1632,6 +1886,14 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1632
1886
  explicitAlias: undefined,
1633
1887
  previousTableAlias,
1634
1888
  joinAndStatement: {},
1889
+ /**
1890
+ * The through associations pending in previousThroughAssociations are
1891
+ * applied at the terminal join of _this_ association's chain (their
1892
+ * target model is this through association's target model), not at the
1893
+ * terminal join of the nested through association being bridged here,
1894
+ * so the nested chain starts with an empty stack.
1895
+ */
1896
+ previousThroughAssociations: [],
1635
1897
  joinType,
1636
1898
  });
1637
1899
  //
@@ -1658,7 +1920,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1658
1920
  * The joinAndStatement is reserved for the final association, not intermediary join tables
1659
1921
  */
1660
1922
  joinAndStatement: {},
1661
- previousThroughAssociation: undefined,
1923
+ previousThroughAssociations: [],
1662
1924
  joinType,
1663
1925
  dreamClassThroughAssociationWantsToHydrate: undefined,
1664
1926
  });
@@ -1698,15 +1960,22 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1698
1960
  // recursively and therefore may have added other through associations and their sources prior to reaching this point)
1699
1961
  explicitAlias,
1700
1962
  previousTableAlias: recursiveResult.association.as,
1701
- selfTableAlias: (throughAssociation.selfAnd ?? throughAssociation.selfAndNot)
1702
- ? previousTableAlias
1703
- : recursiveResult.association.as,
1963
+ selfTableAlias: recursiveResult.association.as,
1704
1964
  // since joinsBridgeThroughAssociations passes {} to joinAndStatement recursively, we know this is only set on the
1705
1965
  // first call to joinsBridgeThroughAssociations, which corresponds to the outermoset through association and therefore
1706
1966
  // the last source association to be added to the statement (because joinsBridgeThroughAssociations was called
1707
1967
  // recursively and therefore may have added other through associations and their sources prior to reaching this point)
1708
1968
  joinAndStatement,
1709
- previousThroughAssociation: throughAssociation,
1969
+ previousThroughAssociations: [
1970
+ ...previousThroughAssociations,
1971
+ /**
1972
+ * This through association's options are applied at the terminal
1973
+ * concrete join of this chain. `selfAnd`/`selfAndNot` clauses on a
1974
+ * through association reference the model the association is defined
1975
+ * on, whose table is aliased with this method's previousTableAlias.
1976
+ */
1977
+ { association: throughAssociation, selfTableAlias: previousTableAlias },
1978
+ ],
1710
1979
  joinType,
1711
1980
  dreamClassThroughAssociationWantsToHydrate,
1712
1981
  });
@@ -1731,14 +2000,8 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1731
2000
  /**
1732
2001
  * previousTableAlias is always set
1733
2002
  */
1734
- previousTableAlias, selfTableAlias = previousTableAlias, joinAndStatement = {}, previousThroughAssociation, joinType, dreamClassThroughAssociationWantsToHydrate, }) {
2003
+ previousTableAlias, selfTableAlias = previousTableAlias, joinAndStatement = {}, previousThroughAssociations, joinType, dreamClassThroughAssociationWantsToHydrate, }) {
1735
2004
  if (association.type !== 'BelongsTo' && association.through) {
1736
- if (throughAssociationHasOptionsBesidesThroughAndSource(previousThroughAssociation)) {
1737
- throw new ThroughAssociationConditionsIncompatibleWithThroughAssociationSource({
1738
- dreamClass,
1739
- association,
1740
- });
1741
- }
1742
2005
  return this.joinsBridgeThroughAssociations({
1743
2006
  query,
1744
2007
  dreamClassTheAssociationIsDefinedOn: dreamClass,
@@ -1746,6 +2009,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1746
2009
  explicitAlias,
1747
2010
  previousTableAlias,
1748
2011
  joinAndStatement,
2012
+ previousThroughAssociations,
1749
2013
  joinType,
1750
2014
  });
1751
2015
  }
@@ -1757,7 +2021,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1757
2021
  previousTableAlias,
1758
2022
  selfTableAlias,
1759
2023
  joinAndStatement,
1760
- previousThroughAssociation,
2024
+ previousThroughAssociations,
1761
2025
  joinType,
1762
2026
  dreamClassThroughAssociationWantsToHydrate,
1763
2027
  });
@@ -1782,24 +2046,32 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1782
2046
  /**
1783
2047
  * previousTableAlias is always set
1784
2048
  */
1785
- previousTableAlias, selfTableAlias, joinAndStatement, previousThroughAssociation, joinType, dreamClassThroughAssociationWantsToHydrate, }) {
2049
+ previousTableAlias, selfTableAlias, joinAndStatement, previousThroughAssociations, joinType, dreamClassThroughAssociationWantsToHydrate, }) {
1786
2050
  const currentTableAlias = explicitAlias ?? association.as;
1787
2051
  const _associatedDreamClass = association.modelCB();
1788
2052
  const associatedDreamClass = Array.isArray(_associatedDreamClass)
1789
2053
  ? _associatedDreamClass[0]
1790
2054
  : _associatedDreamClass;
1791
- if (previousThroughAssociation?.type === 'HasMany') {
1792
- if (query?.distinctOn && previousThroughAssociation.distinct) {
2055
+ /**
2056
+ * Stacked order/distinct clauses are applied in join order: the options of
2057
+ * the through association whose bridging joins were added to the query
2058
+ * first are applied first, and the terminal association's own
2059
+ * order/distinct (applied after the join, below) come last.
2060
+ */
2061
+ for (const { association: throughAssociation } of previousThroughAssociations) {
2062
+ if (throughAssociation.type !== 'HasMany')
2063
+ continue;
2064
+ if (query?.distinctOn && throughAssociation.distinct) {
1793
2065
  query = query.distinctOn(this.distinctColumnNameForAssociation({
1794
- association: previousThroughAssociation,
2066
+ association: throughAssociation,
1795
2067
  tableNameOrAlias: currentTableAlias,
1796
- foreignKey: previousThroughAssociation.primaryKey(),
2068
+ foreignKey: throughAssociation.primaryKey(),
1797
2069
  }));
1798
2070
  }
1799
- if (previousThroughAssociation?.order) {
2071
+ if (throughAssociation.order) {
1800
2072
  query = this.applyOrderStatementForAssociation({
1801
2073
  query,
1802
- association: previousThroughAssociation,
2074
+ association: throughAssociation,
1803
2075
  tableNameOrAlias: currentTableAlias,
1804
2076
  });
1805
2077
  }
@@ -1818,21 +2090,18 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1818
2090
  join = join.onRef(this.namespaceColumn(association.foreignKey(), previousTableAlias), '=', this.namespaceColumn(association.primaryKey(undefined, {
1819
2091
  associatedClassOverride: dreamClassThroughAssociationWantsToHydrate,
1820
2092
  }), currentTableAlias));
1821
- if (previousThroughAssociation) {
1822
- if (dreamClassThroughAssociationWantsToHydrate) {
1823
- join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement({
1824
- [association.foreignKeyTypeField()]: dreamClassThroughAssociationWantsToHydrate.sanitizedName,
1825
- }, previousThroughAssociation.through)));
1826
- }
1827
- join = this.applyAssociationAndStatementsToJoinStatement({
1828
- dreamClass,
1829
- join,
1830
- association: previousThroughAssociation,
1831
- currentTableAlias,
1832
- selfTableAlias,
1833
- joinAndStatement,
1834
- });
2093
+ if (dreamClassThroughAssociationWantsToHydrate) {
2094
+ join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement({
2095
+ [association.foreignKeyTypeField()]: dreamClassThroughAssociationWantsToHydrate.sanitizedName,
2096
+ }, previousTableAlias)));
1835
2097
  }
2098
+ join = this.applyPreviousThroughAssociationAndStatementsToJoinStatement({
2099
+ dreamClass,
2100
+ join,
2101
+ previousThroughAssociations,
2102
+ currentTableAlias,
2103
+ joinAndStatement,
2104
+ });
1836
2105
  join = this.conditionallyApplyDefaultScopesDependentOnAssociation({
1837
2106
  dreamClass,
1838
2107
  join,
@@ -1856,16 +2125,13 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1856
2125
  : dreamClass.referenceTypeString,
1857
2126
  }, currentTableAlias)));
1858
2127
  }
1859
- if (previousThroughAssociation) {
1860
- join = this.applyAssociationAndStatementsToJoinStatement({
1861
- dreamClass,
1862
- join,
1863
- association: previousThroughAssociation,
1864
- currentTableAlias,
1865
- selfTableAlias,
1866
- joinAndStatement,
1867
- });
1868
- }
2128
+ join = this.applyPreviousThroughAssociationAndStatementsToJoinStatement({
2129
+ dreamClass,
2130
+ join,
2131
+ previousThroughAssociations,
2132
+ currentTableAlias,
2133
+ joinAndStatement,
2134
+ });
1869
2135
  join = this.applyAssociationAndStatementsToJoinStatement({
1870
2136
  dreamClass,
1871
2137
  join,
@@ -1929,27 +2195,52 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1929
2195
  return this.namespaceColumn(foreignKey, tableNameOrAlias);
1930
2196
  return this.namespaceColumn(association.distinct, tableNameOrAlias);
1931
2197
  }
2198
+ /**
2199
+ * Applies the and-family clauses (`and`/`andAny`/`andNot`/`selfAnd`/`selfAndNot`)
2200
+ * of every pending through association to the terminal concrete join of a
2201
+ * through association chain.
2202
+ *
2203
+ * The first entry in the stack is the association the developer named in the
2204
+ * join/preload/associationQuery statement, so it is the association the
2205
+ * developer-supplied joinAndStatement corresponds to (used to satisfy
2206
+ * `DreamConst.required` clauses); `DreamConst.required` on any other stacked
2207
+ * through association cannot be satisfied and will throw
2208
+ * `MissingRequiredAssociationAndClause`.
2209
+ */
2210
+ applyPreviousThroughAssociationAndStatementsToJoinStatement({ dreamClass, join, previousThroughAssociations, currentTableAlias, joinAndStatement, }) {
2211
+ previousThroughAssociations.forEach(({ association, selfTableAlias }, index) => {
2212
+ join = this.applyAssociationAndStatementsToJoinStatement({
2213
+ dreamClass,
2214
+ join,
2215
+ association,
2216
+ currentTableAlias,
2217
+ selfTableAlias,
2218
+ joinAndStatement: index === 0 ? joinAndStatement : {},
2219
+ });
2220
+ });
2221
+ return join;
2222
+ }
1932
2223
  applyAssociationAndStatementsToJoinStatement({ dreamClass, join, currentTableAlias, selfTableAlias, association, joinAndStatement, }) {
1933
2224
  if (association.and) {
1934
2225
  this.throwUnlessAllRequiredWhereClausesProvided(association, currentTableAlias, joinAndStatement);
1935
- join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(association.and, currentTableAlias), { disallowSimilarityOperator: false }));
2226
+ join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(association.and, currentTableAlias), { disallowSimilarityOperator: false, expectedAlias: currentTableAlias }));
1936
2227
  }
1937
2228
  if (association.andNot) {
1938
- join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(association.andNot, currentTableAlias), { negate: true }));
2229
+ join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(association.andNot, currentTableAlias), { negate: true, expectedAlias: currentTableAlias }));
1939
2230
  }
1940
2231
  if (association.andAny) {
1941
- join = join.on((eb) => eb.or(association.andAny.map(whereAnyStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereAnyStatement, currentTableAlias), { disallowSimilarityOperator: false }))));
2232
+ join = join.on((eb) => eb.or(association.andAny.map(whereAnyStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereAnyStatement, currentTableAlias), { disallowSimilarityOperator: false, expectedAlias: currentTableAlias }))));
1942
2233
  }
1943
2234
  if (association.selfAnd) {
1944
2235
  join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.rawifiedSelfOnClause({
1945
- associationAlias: association.as,
2236
+ associationAlias: currentTableAlias,
1946
2237
  selfAlias: selfTableAlias,
1947
2238
  selfAndClause: association.selfAnd,
1948
2239
  })));
1949
2240
  }
1950
2241
  if (association.selfAndNot) {
1951
2242
  join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.rawifiedSelfOnClause({
1952
- associationAlias: association.as,
2243
+ associationAlias: currentTableAlias,
1953
2244
  selfAlias: selfTableAlias,
1954
2245
  selfAndClause: association.selfAndNot,
1955
2246
  }), { negate: true }));
@@ -1997,9 +2288,9 @@ export default class KyselyQueryDriver extends QueryDriverBase {
1997
2288
  // branches below, such default scopes are silently dropped on association loads, leaking
1998
2289
  // rows the app intended to hide.
1999
2290
  join = join.on((eb) => eb.and([
2000
- ...scopesQuery['whereStatements'].map(whereStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereStatement, tableNameOrAlias), { disallowSimilarityOperator: false })),
2001
- ...scopesQuery['whereNotStatements'].map(whereNotStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereNotStatement, tableNameOrAlias), { negate: true })),
2002
- ...scopesQuery['whereAnyStatements'].map(whereAnyStatements => eb.or(whereAnyStatements.map(whereAnyStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereAnyStatement, tableNameOrAlias))))),
2291
+ ...scopesQuery['whereStatements'].map(whereStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereStatement, tableNameOrAlias), { disallowSimilarityOperator: false, expectedAlias: tableNameOrAlias })),
2292
+ ...scopesQuery['whereNotStatements'].map(whereNotStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereNotStatement, tableNameOrAlias), { negate: true, expectedAlias: tableNameOrAlias })),
2293
+ ...scopesQuery['whereAnyStatements'].map(whereAnyStatements => eb.or(whereAnyStatements.map(whereAnyStatement => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement(whereAnyStatement, tableNameOrAlias), { expectedAlias: tableNameOrAlias })))),
2003
2294
  ]));
2004
2295
  }
2005
2296
  return join;