@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
@@ -0,0 +1,27 @@
1
+ export default class CannotNamespaceAssociationFilterToAnotherTable extends Error {
2
+ dreamClass;
3
+ key;
4
+ expectedAlias;
5
+ constructor(dreamClass, key, expectedAlias) {
6
+ super();
7
+ this.dreamClass = dreamClass;
8
+ this.key = key;
9
+ this.expectedAlias = expectedAlias;
10
+ }
11
+ get message() {
12
+ return `
13
+ Cannot filter on an association via a where-clause key namespaced to another table.
14
+
15
+ dream class: ${this.dreamClass.sanitizedName}
16
+ where-clause key: ${this.key}
17
+ table alias this where clause applies to: ${this.expectedAlias}
18
+
19
+ A Dream instance (or array of Dream instances) as a where-clause value resolves
20
+ an association on the model the where clause applies to, so the key may not be
21
+ namespaced to a different table. To filter a joined table by one of its
22
+ associations, pass the filter in the join's on-clause instead, e.g.:
23
+
24
+ .innerJoin('association', { and: { myBelongsToAssociation: instances } })
25
+ `;
26
+ }
27
+ }
@@ -8,8 +8,13 @@ import globalClassNameFromFullyQualifiedModelName from '../globalClassNameFromFu
8
8
  import snakeify from '../snakeify.js';
9
9
  import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
10
10
  const STI_TYPE_COLUMN_NAME = 'type';
11
- const DELETED_AT_COLUMN_NAME = 'deleted_at';
12
- const COLUMNS_TO_INDEX = [STI_TYPE_COLUMN_NAME, DELETED_AT_COLUMN_NAME];
11
+ // deleted_at is deliberately NOT in this list: the SoftDelete default scope's
12
+ // `WHERE deleted_at IS NULL` is unselective on healthy tables, Dream internals
13
+ // never issue a query a deleted_at index could serve, and the useful partial
14
+ // index alternatives are dialect-specific SQL the generator must not emit by
15
+ // default. See spec/unit/cli/generateMigrationContent.spec.ts
16
+ // ("deleted_at is deliberately NOT indexed") and the CHANGELOG.
17
+ const COLUMNS_TO_INDEX = [STI_TYPE_COLUMN_NAME];
13
18
  export default function generateMigrationContent({ connectionName = 'default', table, columnsWithTypes = [], primaryKeyType = 'bigserial', createOrAlter = 'create', stiChildClassName, softDelete = false, } = {}) {
14
19
  const altering = createOrAlter === 'alter';
15
20
  let requireCitextExtension = false;
@@ -43,13 +48,18 @@ export default function generateMigrationContent({ connectionName = 'default', t
43
48
  return acc;
44
49
  /**
45
50
  * Automatically set email columns to citext since different casings of
46
- * email address are the same email address
51
+ * email address are the same email address. Skip this when the user
52
+ * explicitly asked for an encrypted column, since encrypted columns
53
+ * are always stored as encrypted text and must not be overridden by
54
+ * the name-based heuristic.
47
55
  */
48
- const attributeType = /email$/.test(nonStandardAttributeName)
49
- ? 'citext'
50
- : /uuid$/.test(nonStandardAttributeName)
51
- ? 'uuid'
52
- : _attributeType;
56
+ const attributeType = _attributeType === 'encrypted' || _attributeType === 'encrypted[]'
57
+ ? _attributeType
58
+ : /email$/.test(nonStandardAttributeName)
59
+ ? 'citext'
60
+ : /uuid$/.test(nonStandardAttributeName)
61
+ ? 'uuid'
62
+ : _attributeType;
53
63
  const processedAttrType = camelize(attributeType)?.toLowerCase();
54
64
  const userWantsThisOptional = optionalFromDescriptors(descriptors);
55
65
  // when creating a migration for an STI child, we don't want to include notNull;
@@ -115,6 +125,7 @@ export default function generateMigrationContent({ connectionName = 'default', t
115
125
  attributeName = `encrypted_${attributeName}`;
116
126
  columnDefs.push(generateColumnStr(attributeName, 'text', descriptors, {
117
127
  omitInlineNonNull,
128
+ skipUniqueHeuristic: true,
118
129
  }));
119
130
  break;
120
131
  // TODO: determine if we need to support encrypted[] in the future
@@ -145,9 +156,6 @@ export default function generateMigrationContent({ connectionName = 'default', t
145
156
  }
146
157
  return acc;
147
158
  }, { columnDefs: [], columnDrops: [], indexDefs: [] });
148
- if (emitDeletedAtColumn) {
149
- indexDefs.push(columnIndexStatement(table, DELETED_AT_COLUMN_NAME));
150
- }
151
159
  if (!table) {
152
160
  return `\
153
161
  import { Kysely, sql } from 'kysely'
@@ -274,12 +282,12 @@ function generateDecimalStr(attributeName, { descriptors, omitInlineNonNull: opt
274
282
  : `'decimal(${scale}, ${precision})'`;
275
283
  return `.addColumn('${attributeName}', ${decimalStatement}${optional ? '' : `, col => ${columnModifiers}`})`;
276
284
  }
277
- function generateColumnStr(attributeName, attributeType, descriptors, { omitInlineNonNull: optional }) {
285
+ function generateColumnStr(attributeName, attributeType, descriptors, { omitInlineNonNull: optional, skipUniqueHeuristic = false, }) {
278
286
  let returnStr = `.addColumn('${attributeName}', ${attributeTypeString(attributeType)}`;
279
287
  const providedDefaultArg = descriptors.find(d => /^default\(/.test(d));
280
288
  const providedDefault = providedDefaultArg?.replace(/^default\(/, '')?.replace(/\)$/, '');
281
289
  const notNull = !optional;
282
- const isUnique = /(email|token|uuid)$/.test(attributeName);
290
+ const isUnique = !skipUniqueHeuristic && /(email|token|uuid)$/.test(attributeName);
283
291
  const hasExtraValues = providedDefault || notNull || isUnique;
284
292
  const isArray = /\[\]$/.test(attributeType);
285
293
  const needsJsonDefault = notNull && !isArray && (attributeType === 'jsonb' || attributeType === 'json');
@@ -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.
@@ -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.
@@ -980,6 +980,39 @@ export default class Query {
980
980
  async count() {
981
981
  return await this.dbDriverInstance().count();
982
982
  }
983
+ /**
984
+ * Retrieves the number of records in each group, keyed by the
985
+ * value of the provided group column (a SQL `GROUP BY`).
986
+ *
987
+ * ```ts
988
+ * await User.query().countBy('name')
989
+ * // Map(2) { 'fred' => 2, 'zed' => 1 }
990
+ *
991
+ * await User.where({ email: ops.ilike('%gmail.com') }).countBy('name')
992
+ * // Map(1) { 'fred' => 3 }
993
+ * ```
994
+ *
995
+ * Any `where` clauses, base scopes (soft-delete / STI), and joined-association
996
+ * `and` clauses on the Query carry through automatically. A joined-association
997
+ * column may be grouped on by passing its namespaced name (e.g.
998
+ * `'compositionAssets.name'`).
999
+ *
1000
+ * Only groups with at least one matching row appear in the Map (inherent to
1001
+ * `GROUP BY`); seed absent groups yourself with `map.get(key) ?? 0`. When the
1002
+ * group column is nullable, records with a `null` value are grouped under a real
1003
+ * `null` key.
1004
+ *
1005
+ * ```ts
1006
+ * await Composition.query().innerJoin('compositionAssets').countBy('compositionAssets.name')
1007
+ * // Map(2) { 'primary' => 3, null => 1 }
1008
+ * ```
1009
+ *
1010
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
1011
+ * @returns A Map from each present group value to the number of records in that group
1012
+ */
1013
+ async countBy(groupColumn) {
1014
+ return await this.dbDriverInstance().countBy(groupColumn);
1015
+ }
983
1016
  /**
984
1017
  * Returns new Query with distinct clause applied.
985
1018
  * If no column is specified, applies distinct to the primary key.
@@ -1038,6 +1071,32 @@ export default class Query {
1038
1071
  async max(columnName) {
1039
1072
  return await this.dbDriverInstance().max(columnName);
1040
1073
  }
1074
+ /**
1075
+ * Retrieves the max value of the specified column within each group, keyed by
1076
+ * the value of the provided group column (a SQL `GROUP BY`).
1077
+ *
1078
+ * ```ts
1079
+ * await CompositionAsset.query().maxBy('name', 'score')
1080
+ * // Map(2) { 'primary' => 9, 'secondary' => 4 }
1081
+ * ```
1082
+ *
1083
+ * Any `where` clauses, base scopes (soft-delete / STI), and joined-association
1084
+ * `and` clauses on the Query carry through automatically. A joined-association
1085
+ * column may be grouped on or aggregated by passing its namespaced name (e.g.
1086
+ * `'compositionAssets.name'`).
1087
+ *
1088
+ * Only groups with at least one matching row appear in the Map (inherent to
1089
+ * `GROUP BY`). When the group column is nullable, records with a `null` value
1090
+ * are grouped under a real `null` key; a group whose aggregated values are all
1091
+ * `null` yields a `null` value.
1092
+ *
1093
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
1094
+ * @param aggregatedColumn - the column to take the max of within each group
1095
+ * @returns A Map from each present group value to the max of the aggregated column in that group
1096
+ */
1097
+ async maxBy(groupColumn, aggregatedColumn) {
1098
+ return await this.dbDriverInstance().maxBy(groupColumn, aggregatedColumn);
1099
+ }
1041
1100
  /**
1042
1101
  * Retrieves the min value of the specified column
1043
1102
  * for this Query
@@ -1053,6 +1112,32 @@ export default class Query {
1053
1112
  async min(columnName) {
1054
1113
  return await this.dbDriverInstance().min(columnName);
1055
1114
  }
1115
+ /**
1116
+ * Retrieves the min value of the specified column within each group, keyed by
1117
+ * the value of the provided group column (a SQL `GROUP BY`).
1118
+ *
1119
+ * ```ts
1120
+ * await CompositionAsset.query().minBy('name', 'score')
1121
+ * // Map(2) { 'primary' => 1, 'secondary' => 4 }
1122
+ * ```
1123
+ *
1124
+ * Any `where` clauses, base scopes (soft-delete / STI), and joined-association
1125
+ * `and` clauses on the Query carry through automatically. A joined-association
1126
+ * column may be grouped on or aggregated by passing its namespaced name (e.g.
1127
+ * `'compositionAssets.name'`).
1128
+ *
1129
+ * Only groups with at least one matching row appear in the Map (inherent to
1130
+ * `GROUP BY`). When the group column is nullable, records with a `null` value
1131
+ * are grouped under a real `null` key; a group whose aggregated values are all
1132
+ * `null` yields a `null` value.
1133
+ *
1134
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
1135
+ * @param aggregatedColumn - the column to take the min of within each group
1136
+ * @returns A Map from each present group value to the min of the aggregated column in that group
1137
+ */
1138
+ async minBy(groupColumn, aggregatedColumn) {
1139
+ return await this.dbDriverInstance().minBy(groupColumn, aggregatedColumn);
1140
+ }
1056
1141
  /**
1057
1142
  * Retrieves the sum value of the specified column
1058
1143
  * for this Query
@@ -1068,6 +1153,32 @@ export default class Query {
1068
1153
  async sum(columnName) {
1069
1154
  return await this.dbDriverInstance().sum(columnName);
1070
1155
  }
1156
+ /**
1157
+ * Retrieves the sum of the specified column within each group, keyed by
1158
+ * the value of the provided group column (a SQL `GROUP BY`).
1159
+ *
1160
+ * ```ts
1161
+ * await CompositionAsset.query().sumBy('name', 'score')
1162
+ * // Map(2) { 'primary' => 10, 'secondary' => 4 }
1163
+ * ```
1164
+ *
1165
+ * Any `where` clauses, base scopes (soft-delete / STI), and joined-association
1166
+ * `and` clauses on the Query carry through automatically. A joined-association
1167
+ * column may be grouped on or aggregated by passing its namespaced name (e.g.
1168
+ * `'compositionAssets.name'`).
1169
+ *
1170
+ * Only groups with at least one matching row appear in the Map (inherent to
1171
+ * `GROUP BY`). When the group column is nullable, records with a `null` value
1172
+ * are grouped under a real `null` key; a group whose aggregated values are all
1173
+ * `null` yields a `null` value.
1174
+ *
1175
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
1176
+ * @param aggregatedColumn - the column to sum within each group
1177
+ * @returns A Map from each present group value to the sum of the aggregated column in that group
1178
+ */
1179
+ async sumBy(groupColumn, aggregatedColumn) {
1180
+ return await this.dbDriverInstance().sumBy(groupColumn, aggregatedColumn);
1181
+ }
1071
1182
  /**
1072
1183
  * Retrieves the average value of the specified column
1073
1184
  * for this Query
@@ -1083,6 +1194,32 @@ export default class Query {
1083
1194
  async avg(columnName) {
1084
1195
  return await this.dbDriverInstance().avg(columnName);
1085
1196
  }
1197
+ /**
1198
+ * Retrieves the average of the specified column within each group, keyed by
1199
+ * the value of the provided group column (a SQL `GROUP BY`).
1200
+ *
1201
+ * ```ts
1202
+ * await CompositionAsset.query().avgBy('name', 'score')
1203
+ * // Map(2) { 'primary' => 5, 'secondary' => 4 }
1204
+ * ```
1205
+ *
1206
+ * Any `where` clauses, base scopes (soft-delete / STI), and joined-association
1207
+ * `and` clauses on the Query carry through automatically. A joined-association
1208
+ * column may be grouped on or aggregated by passing its namespaced name (e.g.
1209
+ * `'compositionAssets.name'`).
1210
+ *
1211
+ * Only groups with at least one matching row appear in the Map (inherent to
1212
+ * `GROUP BY`). When the group column is nullable, records with a `null` value
1213
+ * are grouped under a real `null` key; a group whose aggregated values are all
1214
+ * `null` yields a `null` value.
1215
+ *
1216
+ * @param groupColumn - the column to group by (base or joined-association-namespaced)
1217
+ * @param aggregatedColumn - the column to average within each group
1218
+ * @returns A Map from each present group value to the average of the aggregated column in that group
1219
+ */
1220
+ async avgBy(groupColumn, aggregatedColumn) {
1221
+ return await this.dbDriverInstance().avgBy(groupColumn, aggregatedColumn);
1222
+ }
1086
1223
  /**
1087
1224
  * Plucks the provided fields from the given dream class table
1088
1225
  *