@rvoh/dream 2.18.1 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/dist/cjs/src/Dream.js +186 -0
  2. package/dist/cjs/src/db/DreamDbConnection.js +6 -0
  3. package/dist/cjs/src/decorators/class/SoftDelete.js +12 -0
  4. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +113 -0
  5. package/dist/cjs/src/dream/Query.js +148 -0
  6. package/dist/cjs/src/dream/QueryDriver/Base.js +84 -0
  7. package/dist/cjs/src/dream/QueryDriver/Kysely.js +420 -81
  8. package/dist/cjs/src/dream/internal/filterRowToKnownColumns.js +41 -0
  9. package/dist/cjs/src/dream/internal/saveDream.js +6 -1
  10. package/dist/cjs/src/dream/internal/sqlResultToDreamInstance.js +9 -2
  11. package/dist/cjs/src/errors/CannotNamespaceAssociationFilterToAnotherTable.js +27 -0
  12. package/dist/cjs/src/errors/schema-builder/CannotIgnoreAssociationColumn.js +38 -0
  13. package/dist/cjs/src/errors/schema-builder/CannotIgnoreEncryptedColumn.js +20 -0
  14. package/dist/cjs/src/errors/schema-builder/CannotIgnorePrimaryKey.js +18 -0
  15. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.js +21 -0
  16. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.js +20 -0
  17. package/dist/cjs/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.js +24 -0
  18. package/dist/cjs/src/errors/schema-builder/CannotIgnoreStiTypeColumn.js +17 -0
  19. package/dist/cjs/src/errors/schema-builder/ConflictingIgnoredColumns.js +27 -0
  20. package/dist/cjs/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.js +21 -0
  21. package/dist/cjs/src/helpers/cli/ASTConnectionBuilder.js +38 -1
  22. package/dist/cjs/src/helpers/cli/ASTKyselyCodegenEnhancer.js +60 -0
  23. package/dist/cjs/src/helpers/cli/generateMigrationContent.js +21 -13
  24. package/dist/cjs/src/helpers/cli/resolveIgnoredColumns.js +198 -0
  25. package/dist/esm/src/Dream.js +186 -0
  26. package/dist/esm/src/db/DreamDbConnection.js +6 -0
  27. package/dist/esm/src/decorators/class/SoftDelete.js +12 -0
  28. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +113 -0
  29. package/dist/esm/src/dream/Query.js +148 -0
  30. package/dist/esm/src/dream/QueryDriver/Base.js +84 -0
  31. package/dist/esm/src/dream/QueryDriver/Kysely.js +420 -81
  32. package/dist/esm/src/dream/internal/filterRowToKnownColumns.js +41 -0
  33. package/dist/esm/src/dream/internal/saveDream.js +6 -1
  34. package/dist/esm/src/dream/internal/sqlResultToDreamInstance.js +9 -2
  35. package/dist/esm/src/errors/CannotNamespaceAssociationFilterToAnotherTable.js +27 -0
  36. package/dist/esm/src/errors/schema-builder/CannotIgnoreAssociationColumn.js +38 -0
  37. package/dist/esm/src/errors/schema-builder/CannotIgnoreEncryptedColumn.js +20 -0
  38. package/dist/esm/src/errors/schema-builder/CannotIgnorePrimaryKey.js +18 -0
  39. package/dist/esm/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.js +21 -0
  40. package/dist/esm/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.js +20 -0
  41. package/dist/esm/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.js +24 -0
  42. package/dist/esm/src/errors/schema-builder/CannotIgnoreStiTypeColumn.js +17 -0
  43. package/dist/esm/src/errors/schema-builder/ConflictingIgnoredColumns.js +27 -0
  44. package/dist/esm/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.js +21 -0
  45. package/dist/esm/src/helpers/cli/ASTConnectionBuilder.js +38 -1
  46. package/dist/esm/src/helpers/cli/ASTKyselyCodegenEnhancer.js +60 -0
  47. package/dist/esm/src/helpers/cli/generateMigrationContent.js +21 -13
  48. package/dist/esm/src/helpers/cli/resolveIgnoredColumns.js +198 -0
  49. package/dist/types/src/Dream.d.ts +178 -4
  50. package/dist/types/src/decorators/class/SoftDelete.d.ts +12 -0
  51. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +103 -0
  52. package/dist/types/src/dream/Query.d.ts +138 -0
  53. package/dist/types/src/dream/QueryDriver/Base.d.ts +69 -0
  54. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +187 -8
  55. package/dist/types/src/dream/internal/filterRowToKnownColumns.d.ts +30 -0
  56. package/dist/types/src/errors/CannotNamespaceAssociationFilterToAnotherTable.d.ts +8 -0
  57. package/dist/types/src/errors/schema-builder/CannotIgnoreAssociationColumn.d.ts +12 -0
  58. package/dist/types/src/errors/schema-builder/CannotIgnoreEncryptedColumn.d.ts +7 -0
  59. package/dist/types/src/errors/schema-builder/CannotIgnorePrimaryKey.d.ts +6 -0
  60. package/dist/types/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.d.ts +7 -0
  61. package/dist/types/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.d.ts +7 -0
  62. package/dist/types/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.d.ts +8 -0
  63. package/dist/types/src/errors/schema-builder/CannotIgnoreStiTypeColumn.d.ts +6 -0
  64. package/dist/types/src/errors/schema-builder/ConflictingIgnoredColumns.d.ts +7 -0
  65. package/dist/types/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.d.ts +7 -0
  66. package/dist/types/src/helpers/cli/ASTConnectionBuilder.d.ts +20 -0
  67. package/dist/types/src/helpers/cli/ASTKyselyCodegenEnhancer.d.ts +13 -0
  68. package/dist/types/src/helpers/cli/resolveIgnoredColumns.d.ts +41 -0
  69. package/dist/types/src/types/associations/shared.d.ts +4 -1
  70. package/dist/types/src/types/dream.d.ts +17 -0
  71. package/dist/types/src/types/types/associations/shared.ts +10 -1
  72. package/dist/types/src/types/types/dream.ts +53 -0
  73. package/dist/types/src/types/types/variadic.ts +179 -140
  74. package/dist/types/src/types/variadic.d.ts +15 -10
  75. package/docs/assets/hierarchy.js +1 -1
  76. package/docs/assets/search.js +1 -1
  77. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  78. package/docs/classes/db.KyselyQueryDriver.html +96 -34
  79. package/docs/classes/db.PostgresQueryDriver.html +97 -35
  80. package/docs/classes/db.QueryDriverBase.html +77 -33
  81. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  82. package/docs/classes/errors.ColumnOverflow.html +3 -3
  83. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  84. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  85. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  86. package/docs/classes/errors.DecryptionError.html +2 -2
  87. package/docs/classes/errors.DecryptionParseError.html +2 -2
  88. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  89. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  90. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  91. package/docs/classes/errors.InvalidClockTime.html +2 -2
  92. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  93. package/docs/classes/errors.InvalidDateTime.html +2 -2
  94. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  95. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  96. package/docs/classes/errors.NotNullViolation.html +3 -3
  97. package/docs/classes/errors.RecordNotFound.html +3 -3
  98. package/docs/classes/errors.ValidationError.html +3 -3
  99. package/docs/classes/index.CalendarDate.html +33 -33
  100. package/docs/classes/index.ClockTime.html +32 -32
  101. package/docs/classes/index.ClockTimeTz.html +35 -35
  102. package/docs/classes/index.DateTime.html +86 -86
  103. package/docs/classes/index.Decorators.html +19 -19
  104. package/docs/classes/index.Dream.html +247 -119
  105. package/docs/classes/index.DreamApp.html +10 -10
  106. package/docs/classes/index.DreamTransaction.html +2 -2
  107. package/docs/classes/index.Env.html +2 -2
  108. package/docs/classes/index.Query.html +155 -57
  109. package/docs/classes/system.CliFileWriter.html +4 -4
  110. package/docs/classes/system.DreamBin.html +2 -2
  111. package/docs/classes/system.DreamCLI.html +7 -7
  112. package/docs/classes/system.DreamImporter.html +2 -2
  113. package/docs/classes/system.DreamLogos.html +2 -2
  114. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  115. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  116. package/docs/classes/system.PathHelpers.html +3 -3
  117. package/docs/classes/utils.Encrypt.html +3 -3
  118. package/docs/classes/utils.Range.html +2 -2
  119. package/docs/functions/db.closeAllDbConnections.html +1 -1
  120. package/docs/functions/db.dreamDbConnections.html +1 -1
  121. package/docs/functions/db.untypedDb.html +1 -1
  122. package/docs/functions/db.validateColumn.html +1 -1
  123. package/docs/functions/db.validateTable.html +1 -1
  124. package/docs/functions/errors.pgErrorType.html +1 -1
  125. package/docs/functions/index.DreamSerializer.html +1 -1
  126. package/docs/functions/index.ObjectSerializer.html +1 -1
  127. package/docs/functions/index.ReplicaSafe.html +1 -1
  128. package/docs/functions/index.STI.html +1 -1
  129. package/docs/functions/index.SoftDelete.html +12 -1
  130. package/docs/functions/utils.camelize.html +1 -1
  131. package/docs/functions/utils.capitalize.html +1 -1
  132. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  133. package/docs/functions/utils.compact.html +1 -1
  134. package/docs/functions/utils.groupBy.html +1 -1
  135. package/docs/functions/utils.hyphenize.html +1 -1
  136. package/docs/functions/utils.intersection.html +1 -1
  137. package/docs/functions/utils.isEmpty.html +1 -1
  138. package/docs/functions/utils.normalizeUnicode.html +1 -1
  139. package/docs/functions/utils.pascalize.html +1 -1
  140. package/docs/functions/utils.percent.html +1 -1
  141. package/docs/functions/utils.range.html +1 -1
  142. package/docs/functions/utils.round.html +1 -1
  143. package/docs/functions/utils.sanitizeString.html +1 -1
  144. package/docs/functions/utils.snakeify.html +1 -1
  145. package/docs/functions/utils.sort.html +1 -1
  146. package/docs/functions/utils.sortBy.html +1 -1
  147. package/docs/functions/utils.sortObjectByKey.html +1 -1
  148. package/docs/functions/utils.sortObjectByValue.html +1 -1
  149. package/docs/functions/utils.uncapitalize.html +1 -1
  150. package/docs/functions/utils.uniq.html +1 -1
  151. package/docs/hierarchy.html +1 -1
  152. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  153. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  154. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  155. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  156. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  157. package/docs/interfaces/types.DecoratorContext.html +2 -2
  158. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  159. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  160. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  161. package/docs/interfaces/types.DurationObject.html +2 -2
  162. package/docs/interfaces/types.EncryptOptions.html +2 -2
  163. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  164. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  165. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  166. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  167. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  168. package/docs/types/openapi.OpenapiFormats.html +1 -1
  169. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  170. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  171. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  172. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  173. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  174. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  175. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  176. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  177. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  178. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  179. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  180. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  181. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  182. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  183. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  184. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  185. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  186. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  187. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  188. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  189. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  190. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  191. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  192. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  193. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  194. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  195. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  196. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  197. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  198. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  199. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  200. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  201. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  202. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  203. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  204. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  205. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  206. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  207. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  208. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  209. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  210. package/docs/types/types.CalendarDateObject.html +1 -1
  211. package/docs/types/types.Camelized.html +1 -1
  212. package/docs/types/types.ClockTimeObject.html +1 -1
  213. package/docs/types/types.DbConnectionType.html +1 -1
  214. package/docs/types/types.DbTypes.html +1 -1
  215. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  216. package/docs/types/types.DreamAttributes.html +1 -1
  217. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  218. package/docs/types/types.DreamClassColumn.html +1 -1
  219. package/docs/types/types.DreamColumn.html +1 -1
  220. package/docs/types/types.DreamColumnNames.html +1 -1
  221. package/docs/types/types.DreamLogLevel.html +1 -1
  222. package/docs/types/types.DreamLogger.html +2 -2
  223. package/docs/types/types.DreamModelSerializerType.html +1 -1
  224. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  225. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  226. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  227. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  228. package/docs/types/types.DreamSerializable.html +1 -1
  229. package/docs/types/types.DreamSerializableArray.html +1 -1
  230. package/docs/types/types.DreamSerializerKey.html +1 -1
  231. package/docs/types/types.DreamSerializers.html +1 -1
  232. package/docs/types/types.DreamVirtualColumns.html +1 -1
  233. package/docs/types/types.DurationUnit.html +1 -1
  234. package/docs/types/types.EncryptAlgorithm.html +1 -1
  235. package/docs/types/types.HasManyStatement.html +1 -1
  236. package/docs/types/types.HasOneStatement.html +1 -1
  237. package/docs/types/types.Hyphenized.html +1 -1
  238. package/docs/types/types.Pascalized.html +1 -1
  239. package/docs/types/types.PrimaryKeyType.html +1 -1
  240. package/docs/types/types.RoundingPrecision.html +1 -1
  241. package/docs/types/types.SerializerCasing.html +1 -1
  242. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  243. package/docs/types/types.Snakeified.html +1 -1
  244. package/docs/types/types.StrictInterface.html +1 -1
  245. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  246. package/docs/types/types.UpdateableProperties.html +1 -1
  247. package/docs/types/types.ValidationType.html +1 -1
  248. package/docs/types/types.ViewModel.html +2 -2
  249. package/docs/types/types.ViewModelClass.html +1 -1
  250. package/docs/types/types.WeekdayName.html +1 -1
  251. package/docs/types/types.WhereStatementForDream.html +1 -1
  252. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  253. package/docs/variables/index.DreamConst.html +1 -1
  254. package/docs/variables/index.ops.html +1 -1
  255. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  256. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  257. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  258. package/docs/variables/system.primaryKeyTypes.html +1 -1
  259. package/package.json +3 -3
  260. package/dist/cjs/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.js +0 -11
  261. package/dist/cjs/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.js +0 -17
  262. package/dist/esm/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.js +0 -11
  263. package/dist/esm/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.js +0 -17
  264. package/dist/types/src/dream/internal/associations/throughAssociationHasOptionsBesidesThroughAndSource.d.ts +0 -13
  265. package/dist/types/src/errors/associations/ThroughAssociationConditionsIncompatibleWithThroughAssociationSource.d.ts +0 -12
@@ -0,0 +1,41 @@
1
+ import protectAgainstPollutingAssignment from '../../helpers/protectAgainstPollutingAssignment.js';
2
+ /**
3
+ * @internal
4
+ *
5
+ * Returns an object containing only the keys of `row` that are columns
6
+ * the compiled schema knows about (per the provided column set).
7
+ *
8
+ * Under schema/image skew (e.g. a rolling deploy in which a migration adds
9
+ * a column while containers built against the previous schema are still
10
+ * draining, or application code is rolled back after an add-column
11
+ * migration), a `RETURNING *` / `select *` row can include columns this
12
+ * build has never heard of. Passing such keys to `setAttributes` would
13
+ * assign them as plain properties — invoking a same-named user-defined
14
+ * setter, or throwing on a getter-only property — so they must be dropped
15
+ * before hydration.
16
+ *
17
+ * Keys are intersected rather than enumerated from the column set so that
18
+ * a column missing from the row (the dropped-column direction of skew)
19
+ * simply does not appear, rather than appearing with an `undefined` value.
20
+ *
21
+ * Identity fast path: when every key of `row` is a known column (the
22
+ * no-skew steady state), `row` itself is returned unchanged, without
23
+ * allocating a copy; a filtered copy is built only when at least one
24
+ * unknown key is present.
25
+ *
26
+ * @param row - a raw database row
27
+ * @param columns - the compiled column set for the Dream class being hydrated
28
+ * @returns `row` itself when every key is a known column; otherwise a new
29
+ * object containing only the known-column entries of `row`
30
+ */
31
+ export default function filterRowToKnownColumns(row, columns) {
32
+ const keys = Object.keys(row);
33
+ if (keys.every(key => columns.has(key)))
34
+ return row;
35
+ const filtered = {};
36
+ keys.forEach(key => {
37
+ if (columns.has(key))
38
+ filtered[protectAgainstPollutingAssignment(key)] = row[key];
39
+ });
40
+ return filtered;
41
+ }
@@ -2,6 +2,7 @@ import ValidationError from '../../errors/ValidationError.js';
2
2
  import sqlAttributes from '../../helpers/sqlAttributes.js';
3
3
  import { DateTime } from '../../utils/datetime/DateTime.js';
4
4
  import Query from '../Query.js';
5
+ import filterRowToKnownColumns from './filterRowToKnownColumns.js';
5
6
  import runHooksFor from './runHooksFor.js';
6
7
  export default async function saveDream(dream, txn = null, { skipHooks = false } = {}) {
7
8
  const alreadyPersisted = dream.isPersisted;
@@ -30,7 +31,11 @@ export default async function saveDream(dream, txn = null, { skipHooks = false }
30
31
  if (!alreadyPersisted || hasUnsavedData) {
31
32
  const data = await Query.dbDriverClass(dream.connectionName || 'default').saveDream(dream, txn);
32
33
  dream['isPersisted'] = true;
33
- dream.setAttributes(data);
34
+ // the row returned by the driver comes from `RETURNING *` (or a
35
+ // `select *` reload on drivers without RETURNING support), so under
36
+ // schema/image skew it can contain columns this build's compiled
37
+ // schema doesn't know about; those must never reach setAttributes
38
+ dream.setAttributes(filterRowToKnownColumns(data, dream.columns()));
34
39
  }
35
40
  // set frozen attributes to what has already been saved
36
41
  dream['freezeAttributes']();
@@ -1,10 +1,17 @@
1
1
  import STIChildMissing from '../../errors/sti/STIChildMissing.js';
2
+ import filterRowToKnownColumns from './filterRowToKnownColumns.js';
2
3
  export default function sqlResultToDreamInstance(dreamClass, sqlResult) {
4
+ // base-model reads select `*`, so under schema/image skew (or with a
5
+ // column declared in ignoredColumns but not yet dropped) the row can
6
+ // contain columns the compiled schema doesn't know about. Passing such
7
+ // keys into the constructor would assign them as plain properties —
8
+ // invoking a same-named user-defined setter, or throwing on a
9
+ // getter-only property — so they are dropped before hydration.
3
10
  if (dreamClass['isSTIBase']) {
4
11
  const extendingDreamClass = findExtendingDreamClass(dreamClass, sqlResult.type);
5
12
  if (!extendingDreamClass)
6
13
  throw new STIChildMissing(dreamClass, sqlResult.type, sqlResult[dreamClass.primaryKey]);
7
- const dreamModel = new extendingDreamClass(sqlResult, {
14
+ const dreamModel = new extendingDreamClass(filterRowToKnownColumns(sqlResult, extendingDreamClass.columns()), {
8
15
  bypassUserDefinedSetters: true,
9
16
  isPersisted: true,
10
17
  _internalUseOnly: true,
@@ -13,7 +20,7 @@ export default function sqlResultToDreamInstance(dreamClass, sqlResult) {
13
20
  return dreamModel;
14
21
  }
15
22
  else {
16
- const dreamModel = new dreamClass(sqlResult, {
23
+ const dreamModel = new dreamClass(filterRowToKnownColumns(sqlResult, dreamClass.columns()), {
17
24
  bypassUserDefinedSetters: true,
18
25
  isPersisted: true,
19
26
  _internalUseOnly: true,
@@ -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
+ }
@@ -0,0 +1,38 @@
1
+ export default class CannotIgnoreAssociationColumn extends Error {
2
+ tableName;
3
+ columnName;
4
+ modelClass;
5
+ association;
6
+ columnRole;
7
+ constructor(tableName, columnName, modelClass, association, columnRole) {
8
+ super();
9
+ this.tableName = tableName;
10
+ this.columnName = columnName;
11
+ this.modelClass = modelClass;
12
+ this.association = association;
13
+ this.columnRole = columnRole;
14
+ }
15
+ get message() {
16
+ return `
17
+ The models backed by the "${this.tableName}" table declare
18
+ "${this.columnName}" in ignoredColumns, but "${this.columnName}" is the
19
+ ${this.columnRole} of the ${this.association.type} association
20
+ "${this.association.as}" on ${this.modelClass.sanitizedName}.
21
+
22
+ Associations read and write their columns by name, so ignoring
23
+ "${this.columnName}" would silently break "${this.association.as}" at
24
+ runtime: ${this.consequence}. Remove the
25
+ "${this.association.as}" association from ${this.modelClass.sanitizedName}
26
+ (or point it at a different column) before ignoring "${this.columnName}".
27
+ `;
28
+ }
29
+ get consequence() {
30
+ if (this.columnRole === 'primary key override')
31
+ return `associating a record would read "${this.columnName}" from an
32
+ instance that no longer hydrates it, silently persisting an empty foreign
33
+ key, and association loads would join on a column missing from the
34
+ generated schema`;
35
+ return `writes to the column would stop persisting, and association loads
36
+ would reference a column missing from the generated schema`;
37
+ }
38
+ }
@@ -0,0 +1,20 @@
1
+ export default class CannotIgnoreEncryptedColumn extends Error {
2
+ modelClass;
3
+ columnName;
4
+ constructor(modelClass, columnName) {
5
+ super();
6
+ this.modelClass = modelClass;
7
+ this.columnName = columnName;
8
+ }
9
+ get message() {
10
+ return `
11
+ ${this.modelClass.sanitizedName} declares "${this.columnName}" in
12
+ ignoredColumns, but "${this.columnName}" is the backing column of an
13
+ @Encrypted property on ${this.modelClass.sanitizedName}.
14
+
15
+ @Encrypted stores its ciphertext in this column, so it can never be
16
+ ignored. Remove the @Encrypted declaration (and its property) from
17
+ ${this.modelClass.sanitizedName} before ignoring "${this.columnName}".
18
+ `;
19
+ }
20
+ }
@@ -0,0 +1,18 @@
1
+ export default class CannotIgnorePrimaryKey extends Error {
2
+ modelClass;
3
+ constructor(modelClass) {
4
+ super();
5
+ this.modelClass = modelClass;
6
+ }
7
+ get message() {
8
+ return `
9
+ ${this.modelClass.sanitizedName} declares its primary key ("${this.modelClass.primaryKey}")
10
+ in ignoredColumns.
11
+
12
+ The primary key is required to identify rows for hydration, updates, and
13
+ associations, so it can never be ignored. Remove
14
+ "${this.modelClass.primaryKey}" from the ignoredColumns getter on
15
+ ${this.modelClass.sanitizedName}.
16
+ `;
17
+ }
18
+ }
@@ -0,0 +1,21 @@
1
+ export default class CannotIgnoreSoftDeleteColumn extends Error {
2
+ modelClass;
3
+ columnName;
4
+ constructor(modelClass, columnName) {
5
+ super();
6
+ this.modelClass = modelClass;
7
+ this.columnName = columnName;
8
+ }
9
+ get message() {
10
+ return `
11
+ ${this.modelClass.sanitizedName} declares "${this.columnName}" in
12
+ ignoredColumns, but ${this.modelClass.sanitizedName} is a SoftDelete model
13
+ and "${this.columnName}" is its deletedAtField.
14
+
15
+ SoftDelete writes this column on destroy and filters on it in its default
16
+ scope, so it can never be ignored. Remove the @SoftDelete decorator from
17
+ ${this.modelClass.sanitizedName} (or point deletedAtField at a different
18
+ column) before ignoring "${this.columnName}".
19
+ `;
20
+ }
21
+ }
@@ -0,0 +1,20 @@
1
+ export default class CannotIgnoreSortablePositionColumn extends Error {
2
+ modelClass;
3
+ columnName;
4
+ constructor(modelClass, columnName) {
5
+ super();
6
+ this.modelClass = modelClass;
7
+ this.columnName = columnName;
8
+ }
9
+ get message() {
10
+ return `
11
+ ${this.modelClass.sanitizedName} declares "${this.columnName}" in
12
+ ignoredColumns, but "${this.columnName}" is the position field of an
13
+ @Sortable declaration on ${this.modelClass.sanitizedName}.
14
+
15
+ @Sortable reads and rewrites this column to keep positions contiguous, so
16
+ it can never be ignored. Remove the @Sortable decorator (and its property)
17
+ from ${this.modelClass.sanitizedName} before ignoring "${this.columnName}".
18
+ `;
19
+ }
20
+ }
@@ -0,0 +1,24 @@
1
+ export default class CannotIgnoreSortableScopeColumn extends Error {
2
+ modelClass;
3
+ columnName;
4
+ positionField;
5
+ constructor(modelClass, columnName, positionField) {
6
+ super();
7
+ this.modelClass = modelClass;
8
+ this.columnName = columnName;
9
+ this.positionField = positionField;
10
+ }
11
+ get message() {
12
+ return `
13
+ ${this.modelClass.sanitizedName} declares "${this.columnName}" in
14
+ ignoredColumns, but "${this.columnName}" is a scope column of the @Sortable
15
+ declaration on "${this.positionField}" on ${this.modelClass.sanitizedName}.
16
+
17
+ @Sortable reads this column by name on every save and destroy to partition
18
+ position values, so it can never be ignored. Remove "${this.columnName}"
19
+ from the @Sortable scope on "${this.positionField}" (or remove the
20
+ @Sortable decorator and its property) on ${this.modelClass.sanitizedName}
21
+ before ignoring "${this.columnName}".
22
+ `;
23
+ }
24
+ }
@@ -0,0 +1,17 @@
1
+ export default class CannotIgnoreStiTypeColumn extends Error {
2
+ modelClass;
3
+ constructor(modelClass) {
4
+ super();
5
+ this.modelClass = modelClass;
6
+ }
7
+ get message() {
8
+ return `
9
+ ${this.modelClass.sanitizedName} participates in single-table inheritance
10
+ and declares the STI "type" column in ignoredColumns.
11
+
12
+ The "type" column determines which model class each row hydrates into, so
13
+ an STI model can never ignore it. Remove "type" from the ignoredColumns
14
+ getter on ${this.modelClass.sanitizedName}.
15
+ `;
16
+ }
17
+ }
@@ -0,0 +1,27 @@
1
+ export default class ConflictingIgnoredColumns extends Error {
2
+ tableName;
3
+ modelClasses;
4
+ constructor(tableName, modelClasses) {
5
+ super();
6
+ this.tableName = tableName;
7
+ this.modelClasses = modelClasses;
8
+ }
9
+ get message() {
10
+ const declarations = this.modelClasses
11
+ .map(modelClass => ` ${modelClass.sanitizedName}: [${[...modelClass.prototype.ignoredColumns]
12
+ .sort()
13
+ .map(column => `'${column}'`)
14
+ .join(', ')}]`)
15
+ .join('\n');
16
+ return `
17
+ Models sharing the table "${this.tableName}" declare different ignoredColumns:
18
+
19
+ ${declarations}
20
+
21
+ There is only one generated schema per table, so every model backed by a
22
+ table must agree on which of its columns are ignored. Align the
23
+ ignoredColumns getters (STI children inherit the base model's getter, so
24
+ the common fix is to declare ignored columns only on the base model).
25
+ `;
26
+ }
27
+ }
@@ -0,0 +1,21 @@
1
+ import camelize from '../../helpers/camelize.js';
2
+ export default class IgnoredColumnMustBeCamelCase extends Error {
3
+ modelClass;
4
+ columnName;
5
+ constructor(modelClass, columnName) {
6
+ super();
7
+ this.modelClass = modelClass;
8
+ this.columnName = columnName;
9
+ }
10
+ get message() {
11
+ return `
12
+ ${this.modelClass.sanitizedName} declares "${this.columnName}" in
13
+ ignoredColumns, but ignored columns must be declared in camelCase.
14
+
15
+ Dream camelizes database column names when generating types, so a
16
+ declaration that is not camelCase can never match a generated column and
17
+ would be silently inert. Declare "${camelize(this.columnName)}" in the
18
+ ignoredColumns getter on ${this.modelClass.sanitizedName} instead.
19
+ `;
20
+ }
21
+ }
@@ -11,6 +11,7 @@ import intersection from '../intersection.js';
11
11
  import sortBy from '../sortBy.js';
12
12
  import uniq from '../uniq.js';
13
13
  import ASTBuilder from './ASTBuilder.js';
14
+ import resolveIgnoredColumns from './resolveIgnoredColumns.js';
14
15
  /**
15
16
  * @internal
16
17
  *
@@ -265,12 +266,48 @@ may need to update the table getter in the corresponding Dream.
265
266
  default: uniq(models.flatMap(model => model['scopes'].default.map(scopeStatement => scopeStatement.method))),
266
267
  named: uniq(models.flatMap(model => model['scopes'].named.map(scopeStatement => scopeStatement.method))),
267
268
  },
268
- columns: await this.getColumnData(tableName, associationData),
269
+ columns: this.withoutIgnoredColumns(await this.getColumnData(tableName, associationData), tableName),
269
270
  virtualColumns: uniq(models.flatMap(model => model['virtualAttributes'].map(prop => prop.property) || [])),
270
271
  associations: associationData,
271
272
  serializerKeys,
272
273
  };
273
274
  }
275
+ /**
276
+ * @internal
277
+ *
278
+ * resolves the ignored columns declared by the models backed by the
279
+ * given table (validating the declarations; see resolveIgnoredColumns)
280
+ */
281
+ ignoredColumnsForTable(tableName) {
282
+ const dreamApp = DreamApp.getOrFail();
283
+ const allModels = Object.values(dreamApp.models).filter(model => model.prototype?.connectionName === this.connectionName);
284
+ const models = allModels.filter(model => model.table === tableName);
285
+ return resolveIgnoredColumns(models, tableName, allModels);
286
+ }
287
+ /**
288
+ * @internal
289
+ *
290
+ * returns the provided column data without the columns that the table's
291
+ * models declare in ignoredColumns. This is what removes ignored columns
292
+ * from the generated dream schema file: `columns()` reads the generated
293
+ * schema at runtime, so every column enumeration built from `columns()`
294
+ * (preload and join-load select lists, save hydration, attribute
295
+ * definition) inherits this filtering. A column that is ignored but not
296
+ * present in the introspected table (e.g. after the drop migration has
297
+ * run but before the declaration is removed) is a no-op.
298
+ */
299
+ withoutIgnoredColumns(columnData, tableName) {
300
+ const ignoredColumns = this.ignoredColumnsForTable(tableName);
301
+ if (!ignoredColumns.size)
302
+ return columnData;
303
+ return Object.keys(columnData)
304
+ .filter(columnName => !ignoredColumns.has(columnName))
305
+ .reduce((filtered, columnName) => {
306
+ ;
307
+ filtered[columnName] = columnData[columnName];
308
+ return filtered;
309
+ }, {});
310
+ }
274
311
  /**
275
312
  * @internal
276
313
  *
@@ -25,6 +25,7 @@ export default class ASTKyselyCodegenEnhancer extends ASTConnectionBuilder {
25
25
  async enhance() {
26
26
  let dbSourceFile = await this.getDbSourceFile();
27
27
  dbSourceFile = this.camelizeKeys(dbSourceFile);
28
+ dbSourceFile = this.removeIgnoredColumns(dbSourceFile);
28
29
  dbSourceFile = this.replaceTimestampExport(dbSourceFile);
29
30
  dbSourceFile = await this.replaceTimeFieldsInInterfaces(dbSourceFile);
30
31
  dbSourceFile = this.addMissingImports(dbSourceFile);
@@ -283,6 +284,65 @@ ${output}`);
283
284
  const result = ts.transform(dbSourceFile, [transformer]);
284
285
  return result.transformed[0];
285
286
  }
287
+ /**
288
+ * @internal
289
+ *
290
+ * removes columns declared in model `ignoredColumns` getters from the
291
+ * table interfaces generated by kysely-codegen. This is what removes
292
+ * ignored columns from the generated db types file: every column-name
293
+ * level type (`DreamColumnNames`, `UpdateableProperties`, where-clause
294
+ * statements) derives from the Kysely `DB` interface, so filtering here
295
+ * turns any remaining code reference to an ignored column into a type
296
+ * error. Runs after camelizeKeys so that member names align with the
297
+ * camelCase column names models declare in ignoredColumns.
298
+ */
299
+ removeIgnoredColumns(dbSourceFile) {
300
+ const dbInterface = this.findDbExport(dbSourceFile, 'DB');
301
+ if (!dbInterface)
302
+ return dbSourceFile;
303
+ // the DB interface maps table names to table interfaces (e.g.
304
+ // `collars: Collars`), so it provides the table name each table
305
+ // interface corresponds to
306
+ const ignoredColumnsByInterfaceName = {};
307
+ dbInterface.members.forEach(member => {
308
+ if (!ts.isPropertySignature(member))
309
+ return;
310
+ if (!member.type || !ts.isTypeReferenceNode(member.type))
311
+ return;
312
+ if (!ts.isIdentifier(member.type.typeName))
313
+ return;
314
+ const tableName = ts.isIdentifier(member.name)
315
+ ? member.name.text
316
+ : ts.isStringLiteral(member.name)
317
+ ? member.name.text
318
+ : null;
319
+ if (!tableName)
320
+ return;
321
+ const ignoredColumns = this.ignoredColumnsForTable(tableName);
322
+ if (ignoredColumns.size)
323
+ ignoredColumnsByInterfaceName[member.type.typeName.text] = ignoredColumns;
324
+ });
325
+ if (!Object.keys(ignoredColumnsByInterfaceName).length)
326
+ return dbSourceFile;
327
+ // @ts-expect-error cannot lock this type down, though implementation is correct
328
+ const transformer = context => {
329
+ const visit = node => {
330
+ const interfaceNode = this.exportedInterfaceOrNull(node);
331
+ const ignoredColumns = interfaceNode
332
+ ? ignoredColumnsByInterfaceName[interfaceNode.name.text]
333
+ : undefined;
334
+ if (interfaceNode && ignoredColumns) {
335
+ return f.updateInterfaceDeclaration(interfaceNode, interfaceNode.modifiers, interfaceNode.name, interfaceNode.typeParameters, interfaceNode.heritageClauses, interfaceNode.members.filter(member => !(ts.isPropertySignature(member) &&
336
+ (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) &&
337
+ ignoredColumns.has(member.name.text))));
338
+ }
339
+ return ts.visitEachChild(node, visit, context);
340
+ };
341
+ return node => ts.visitNode(node, visit);
342
+ };
343
+ const result = ts.transform(dbSourceFile, [transformer]);
344
+ return result.transformed[0];
345
+ }
286
346
  /**
287
347
  * @internal
288
348
  *
@@ -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');