@rvoh/dream 2.41.0 → 2.42.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 (261) hide show
  1. package/dist/cjs/src/Dream.js +155 -13
  2. package/dist/cjs/src/cli/index.js +39 -5
  3. package/dist/cjs/src/db/DreamDbConnection.js +19 -1
  4. package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +34 -2
  5. package/dist/cjs/src/dream/Query.js +178 -14
  6. package/dist/cjs/src/dream/QueryDriver/Base.js +80 -0
  7. package/dist/cjs/src/dream/QueryDriver/Kysely.js +35 -29
  8. package/dist/cjs/src/dream/QueryDriver/Postgres.js +83 -1
  9. package/dist/cjs/src/dream/internal/findOrCreateBy.js +6 -1
  10. package/dist/cjs/src/dream/internal/updateOrCreateBy.js +6 -1
  11. package/dist/cjs/src/encrypt/algorithms/aes-gcm/decryptAESGCM.js +24 -10
  12. package/dist/cjs/src/errors/CreateOrFindByFailedToCreateAndFind.js +63 -6
  13. package/dist/cjs/src/errors/CreateOrUpdateByFailedToCreateAndUpdate.js +41 -6
  14. package/dist/cjs/src/errors/GeneratedFilesAlreadyExist.js +38 -0
  15. package/dist/cjs/src/errors/schema-builder/ColumnNamesDoNotRoundTrip.js +28 -0
  16. package/dist/cjs/src/helpers/cli/ASTConnectionBuilder.js +62 -10
  17. package/dist/cjs/src/helpers/cli/generateDream.js +109 -18
  18. package/dist/cjs/src/helpers/cli/generateMigration.js +12 -8
  19. package/dist/cjs/src/helpers/cli/writeGeneratedFile.js +3 -3
  20. package/dist/cjs/src/helpers/db/normalizeDataForDb.js +17 -2
  21. package/dist/esm/src/Dream.js +155 -13
  22. package/dist/esm/src/cli/index.js +39 -5
  23. package/dist/esm/src/db/DreamDbConnection.js +19 -1
  24. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +34 -2
  25. package/dist/esm/src/dream/Query.js +178 -14
  26. package/dist/esm/src/dream/QueryDriver/Base.js +80 -0
  27. package/dist/esm/src/dream/QueryDriver/Kysely.js +35 -29
  28. package/dist/esm/src/dream/QueryDriver/Postgres.js +83 -1
  29. package/dist/esm/src/dream/internal/findOrCreateBy.js +6 -1
  30. package/dist/esm/src/dream/internal/updateOrCreateBy.js +6 -1
  31. package/dist/esm/src/encrypt/algorithms/aes-gcm/decryptAESGCM.js +24 -10
  32. package/dist/esm/src/errors/CreateOrFindByFailedToCreateAndFind.js +63 -6
  33. package/dist/esm/src/errors/CreateOrUpdateByFailedToCreateAndUpdate.js +41 -6
  34. package/dist/esm/src/errors/GeneratedFilesAlreadyExist.js +38 -0
  35. package/dist/esm/src/errors/schema-builder/ColumnNamesDoNotRoundTrip.js +28 -0
  36. package/dist/esm/src/helpers/cli/ASTConnectionBuilder.js +62 -10
  37. package/dist/esm/src/helpers/cli/generateDream.js +109 -18
  38. package/dist/esm/src/helpers/cli/generateMigration.js +12 -8
  39. package/dist/esm/src/helpers/cli/writeGeneratedFile.js +3 -3
  40. package/dist/esm/src/helpers/db/normalizeDataForDb.js +17 -2
  41. package/dist/types/src/Dream.d.ts +109 -6
  42. package/dist/types/src/bin/index.d.ts +11 -0
  43. package/dist/types/src/cli/index.d.ts +27 -0
  44. package/dist/types/src/db/DreamDbConnection.d.ts +15 -1
  45. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +34 -2
  46. package/dist/types/src/dream/Query.d.ts +101 -6
  47. package/dist/types/src/dream/QueryDriver/Base.d.ts +59 -0
  48. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +9 -0
  49. package/dist/types/src/dream/QueryDriver/Postgres.d.ts +29 -1
  50. package/dist/types/src/dream/internal/executeDatabaseQuery.d.ts +2 -1
  51. package/dist/types/src/errors/CreateOrFindByFailedToCreateAndFind.d.ts +40 -1
  52. package/dist/types/src/errors/CreateOrUpdateByFailedToCreateAndUpdate.d.ts +8 -1
  53. package/dist/types/src/errors/GeneratedFilesAlreadyExist.d.ts +18 -0
  54. package/dist/types/src/errors/schema-builder/ColumnNamesDoNotRoundTrip.d.ts +12 -0
  55. package/dist/types/src/helpers/cli/ASTConnectionBuilder.d.ts +23 -1
  56. package/dist/types/src/helpers/cli/generateDream.d.ts +25 -0
  57. package/dist/types/src/helpers/cli/generateMigration.d.ts +20 -2
  58. package/dist/types/src/helpers/cli/syncWithFailureContext.d.ts +7 -2
  59. package/dist/types/src/helpers/cli/writeGeneratedFile.d.ts +6 -1
  60. package/dist/types/src/helpers/db/normalizeDataForDb.d.ts +7 -0
  61. package/docs/assets/hierarchy.js +1 -1
  62. package/docs/assets/search.js +1 -1
  63. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  64. package/docs/classes/db.KyselyQueryDriver.html +87 -50
  65. package/docs/classes/db.PostgresQueryDriver.html +76 -50
  66. package/docs/classes/db.QueryDriverBase.html +87 -50
  67. package/docs/classes/errors.CannotSaveMissingDream.html +2 -2
  68. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  69. package/docs/classes/errors.ColumnOverflow.html +3 -3
  70. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +6 -3
  71. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  72. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  73. package/docs/classes/errors.DecryptionError.html +2 -2
  74. package/docs/classes/errors.DecryptionParseError.html +2 -2
  75. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  76. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  77. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  78. package/docs/classes/errors.InvalidClockTime.html +2 -2
  79. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  80. package/docs/classes/errors.InvalidDateTime.html +2 -2
  81. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  82. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  83. package/docs/classes/errors.NotNullViolation.html +3 -3
  84. package/docs/classes/errors.RecordNotFound.html +3 -3
  85. package/docs/classes/errors.SortableScopeDidNotStabilize.html +3 -3
  86. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +3 -3
  87. package/docs/classes/errors.ValidationError.html +3 -3
  88. package/docs/classes/index.CalendarDate.html +33 -33
  89. package/docs/classes/index.ClockTime.html +32 -32
  90. package/docs/classes/index.ClockTimeTz.html +35 -35
  91. package/docs/classes/index.DateTime.html +86 -86
  92. package/docs/classes/index.Decorators.html +19 -19
  93. package/docs/classes/index.Dream.html +203 -133
  94. package/docs/classes/index.DreamApp.html +13 -13
  95. package/docs/classes/index.DreamTransaction.html +3 -3
  96. package/docs/classes/index.Env.html +5 -5
  97. package/docs/classes/index.Query.html +147 -100
  98. package/docs/classes/system.CliFileWriter.html +4 -4
  99. package/docs/classes/system.DreamBin.html +7 -3
  100. package/docs/classes/system.DreamCLI.html +7 -7
  101. package/docs/classes/system.DreamImporter.html +2 -2
  102. package/docs/classes/system.DreamLogos.html +2 -2
  103. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  104. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  105. package/docs/classes/system.PathHelpers.html +3 -3
  106. package/docs/classes/utils.Encrypt.html +4 -4
  107. package/docs/classes/utils.Range.html +2 -2
  108. package/docs/functions/db.closeAllDbConnections.html +1 -1
  109. package/docs/functions/db.dreamDbConnections.html +1 -1
  110. package/docs/functions/db.untypedDb.html +1 -1
  111. package/docs/functions/db.validateColumn.html +1 -1
  112. package/docs/functions/db.validateTable.html +1 -1
  113. package/docs/functions/errors.pgErrorType.html +1 -1
  114. package/docs/functions/index.DreamSerializer.html +1 -1
  115. package/docs/functions/index.ObjectSerializer.html +1 -1
  116. package/docs/functions/index.ReplicaSafe.html +1 -1
  117. package/docs/functions/index.STI.html +1 -1
  118. package/docs/functions/index.SoftDelete.html +1 -1
  119. package/docs/functions/utils.camelize.html +1 -1
  120. package/docs/functions/utils.capitalize.html +1 -1
  121. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  122. package/docs/functions/utils.compact.html +1 -1
  123. package/docs/functions/utils.groupBy.html +1 -1
  124. package/docs/functions/utils.hyphenize.html +1 -1
  125. package/docs/functions/utils.intersection.html +1 -1
  126. package/docs/functions/utils.isEmpty.html +3 -3
  127. package/docs/functions/utils.normalizeUnicode.html +1 -1
  128. package/docs/functions/utils.pascalize.html +1 -1
  129. package/docs/functions/utils.percent.html +1 -1
  130. package/docs/functions/utils.range.html +1 -1
  131. package/docs/functions/utils.round.html +1 -1
  132. package/docs/functions/utils.sanitizeString.html +1 -1
  133. package/docs/functions/utils.snakeify.html +1 -1
  134. package/docs/functions/utils.sort.html +1 -1
  135. package/docs/functions/utils.sortBy.html +1 -1
  136. package/docs/functions/utils.sortObjectByKey.html +1 -1
  137. package/docs/functions/utils.sortObjectByValue.html +1 -1
  138. package/docs/functions/utils.uncapitalize.html +1 -1
  139. package/docs/functions/utils.uniq.html +1 -1
  140. package/docs/hierarchy.html +1 -1
  141. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  142. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  143. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  144. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  145. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  146. package/docs/interfaces/types.DecoratorContext.html +2 -2
  147. package/docs/interfaces/types.DecryptCallbacks.html +2 -2
  148. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  149. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  150. package/docs/interfaces/types.DreamDbConfig.html +6 -6
  151. package/docs/interfaces/types.DurationObject.html +2 -2
  152. package/docs/interfaces/types.EncryptOptions.html +2 -2
  153. package/docs/interfaces/types.EncryptionLegacyKeyUsedEvent.html +4 -4
  154. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  155. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  156. package/docs/interfaces/types.IntrospectedColumn.html +7 -7
  157. package/docs/interfaces/types.IntrospectedDatabase.html +2 -2
  158. package/docs/interfaces/types.IntrospectedEnum.html +2 -2
  159. package/docs/interfaces/types.IntrospectedTable.html +5 -5
  160. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  161. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  162. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  163. package/docs/types/openapi.OpenapiFormats.html +1 -1
  164. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  165. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  166. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  167. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  168. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  169. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  170. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  171. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  172. package/docs/types/openapi.OpenapiSchemaCombinatorFields.html +1 -1
  173. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  174. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +1 -1
  175. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +1 -1
  176. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +1 -1
  177. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  178. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  179. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  180. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  181. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  182. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  183. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  184. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  185. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  186. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  187. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  188. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  189. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +1 -1
  190. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +1 -1
  191. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +1 -1
  192. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  193. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  194. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  195. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  196. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  197. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  198. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  199. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  200. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  201. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  202. package/docs/types/types.CalendarDateObject.html +1 -1
  203. package/docs/types/types.Camelized.html +1 -1
  204. package/docs/types/types.ClockTimeObject.html +1 -1
  205. package/docs/types/types.DbConnectionType.html +1 -1
  206. package/docs/types/types.DbTypes.html +1 -1
  207. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  208. package/docs/types/types.DreamAttributes.html +1 -1
  209. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  210. package/docs/types/types.DreamClassColumn.html +1 -1
  211. package/docs/types/types.DreamColumn.html +1 -1
  212. package/docs/types/types.DreamColumnNames.html +1 -1
  213. package/docs/types/types.DreamLogLevel.html +1 -1
  214. package/docs/types/types.DreamLogger.html +2 -2
  215. package/docs/types/types.DreamModelSerializerType.html +1 -1
  216. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  217. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  218. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  219. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  220. package/docs/types/types.DreamSerializable.html +1 -1
  221. package/docs/types/types.DreamSerializableArray.html +1 -1
  222. package/docs/types/types.DreamSerializerKey.html +1 -1
  223. package/docs/types/types.DreamSerializers.html +1 -1
  224. package/docs/types/types.DreamVirtualColumns.html +1 -1
  225. package/docs/types/types.DurationUnit.html +1 -1
  226. package/docs/types/types.EncryptAlgorithm.html +1 -1
  227. package/docs/types/types.HasManyStatement.html +1 -1
  228. package/docs/types/types.HasOneStatement.html +1 -1
  229. package/docs/types/types.Hyphenized.html +1 -1
  230. package/docs/types/types.IntrospectedValueType.html +1 -1
  231. package/docs/types/types.Pascalized.html +1 -1
  232. package/docs/types/types.PrimaryKeyType.html +1 -1
  233. package/docs/types/types.RoundingPrecision.html +1 -1
  234. package/docs/types/types.SerializerCasing.html +1 -1
  235. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  236. package/docs/types/types.Snakeified.html +1 -1
  237. package/docs/types/types.StrictInterface.html +1 -1
  238. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  239. package/docs/types/types.UpdateableProperties.html +1 -1
  240. package/docs/types/types.ValidationType.html +1 -1
  241. package/docs/types/types.ViewModel.html +2 -2
  242. package/docs/types/types.ViewModelClass.html +1 -1
  243. package/docs/types/types.WeekdayName.html +1 -1
  244. package/docs/types/types.WhereStatementForDream.html +1 -1
  245. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  246. package/docs/variables/index.DreamConst.html +1 -1
  247. package/docs/variables/index.ops.html +1 -1
  248. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  249. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  250. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  251. package/docs/variables/system.primaryKeyTypes.html +1 -1
  252. package/package.json +1 -1
  253. package/dist/cjs/src/helpers/cli/generateFactory.js +0 -10
  254. package/dist/cjs/src/helpers/cli/generateSerializer.js +0 -18
  255. package/dist/cjs/src/helpers/cli/generateUnitSpec.js +0 -10
  256. package/dist/esm/src/helpers/cli/generateFactory.js +0 -10
  257. package/dist/esm/src/helpers/cli/generateSerializer.js +0 -18
  258. package/dist/esm/src/helpers/cli/generateUnitSpec.js +0 -10
  259. package/dist/types/src/helpers/cli/generateFactory.d.ts +0 -5
  260. package/dist/types/src/helpers/cli/generateSerializer.d.ts +0 -9
  261. package/dist/types/src/helpers/cli/generateUnitSpec.d.ts +0 -3
@@ -1,5 +1,5 @@
1
1
  import yoctocolors from 'yoctocolors';
2
- import { pgErrorType, UNIQUE_VIOLATION } from './db/errors.js';
2
+ import { STI_SCOPE_NAME } from './decorators/class/STI.js';
3
3
  import associationToGetterSetterProp from './decorators/field/association/associationToGetterSetterProp.js';
4
4
  import { blankAssociationsFactory } from './decorators/field/association/shared.js';
5
5
  import { blankHooksFactory } from './decorators/field/lifecycle/shared.js';
@@ -49,6 +49,7 @@ import DreamMissingRequiredOverride from './errors/DreamMissingRequiredOverride.
49
49
  import NonExistentScopeProvidedToResort from './errors/NonExistentScopeProvidedToResort.js';
50
50
  import RecordNotFound from './errors/RecordNotFound.js';
51
51
  import areEqual from './helpers/areEqual.js';
52
+ import camelize from './helpers/camelize.js';
52
53
  import cloneDeepSafe from './helpers/cloneDeepSafe.js';
53
54
  import isJsonColumn from './helpers/db/types/isJsonColumn.js';
54
55
  import notEqual from './helpers/notEqual.js';
@@ -1267,6 +1268,18 @@ export default class Dream {
1267
1268
  * IMPORTANT: A unique index/uniqueness constraint must exist on
1268
1269
  * at least one of the provided attributes
1269
1270
  *
1271
+ * The lookup applies the model's default scopes, so a conflicting
1272
+ * record they hide (for example, a soft-deleted one under a plain
1273
+ * unique index) is never returned. When the lookup finds nothing,
1274
+ * `CreateOrFindByFailedToCreateAndFind` is thrown. When the database
1275
+ * identifies the violated unique index (Postgres does, unless it is an
1276
+ * expression or partial index), its message names the `createWith`
1277
+ * attributes that index covers as the likely cause, unless the index
1278
+ * also covers every find attribute. When the index covers only find
1279
+ * attributes, or every find attribute, and the record is found with the
1280
+ * model's default scopes removed, the message names the default scope
1281
+ * likely hiding it. Otherwise it lists the possible causes.
1282
+ *
1270
1283
  * ```ts
1271
1284
  * const logEntry = await LogEntry.createOrFindBy({ externalId }, { createWith: params })
1272
1285
  * ```
@@ -1288,10 +1301,12 @@ export default class Dream {
1288
1301
  return dreamModel;
1289
1302
  }
1290
1303
  catch (err) {
1291
- if (pgErrorType(err) === UNIQUE_VIOLATION) {
1292
- const dreamModel = await this.findBy(findOrCreateLookupStatement(this, attributes));
1304
+ if (Query.dbDriverClass(this.prototype.connectionName).isUniqueConstraintViolation(err)) {
1305
+ const lookup = findOrCreateLookupStatement(this, attributes);
1306
+ // where().first() rather than findBy: a value the database rejects is raised, never read as no match
1307
+ const dreamModel = await this.where(lookup).first();
1293
1308
  if (!dreamModel)
1294
- throw new CreateOrFindByFailedToCreateAndFind(this);
1309
+ throw new CreateOrFindByFailedToCreateAndFind(this, await this.createOrFindFailureCause(err, lookup, extraOpts.createWith));
1295
1310
  return dreamModel;
1296
1311
  }
1297
1312
  throw err;
@@ -1327,6 +1342,18 @@ export default class Dream {
1327
1342
  * IMPORTANT: A unique index/uniqueness constraint must exist on
1328
1343
  * at least one of the provided attributes
1329
1344
  *
1345
+ * The lookup applies the model's default scopes, so a conflicting
1346
+ * record they hide (for example, a soft-deleted one under a plain
1347
+ * unique index) is never updated. When the lookup finds nothing,
1348
+ * `CreateOrUpdateByFailedToCreateAndUpdate` is thrown. When the database
1349
+ * identifies the violated unique index (Postgres does, unless it is an
1350
+ * expression or partial index), its message names the `with`
1351
+ * attributes that index covers as the likely cause, unless the index
1352
+ * also covers every find attribute. When the index covers only find
1353
+ * attributes, or every find attribute, and the record is found with the
1354
+ * model's default scopes removed, the message names the default scope
1355
+ * likely hiding it. Otherwise it lists the possible causes.
1356
+ *
1330
1357
  * @param attributes - The base attributes for finding which record to update, also used when creating
1331
1358
  * @param extraOpts - Additional options
1332
1359
  * @param extraOpts.with - additional attributes to persist when updating and creating, but not used for finding
@@ -1342,10 +1369,12 @@ export default class Dream {
1342
1369
  }, skipHooks ? { skipHooks } : undefined);
1343
1370
  }
1344
1371
  catch (err) {
1345
- if (pgErrorType(err) === UNIQUE_VIOLATION) {
1346
- const existingRecord = await this.findBy(findOrCreateLookupStatement(this, attributes));
1372
+ if (Query.dbDriverClass(this.prototype.connectionName).isUniqueConstraintViolation(err)) {
1373
+ const lookup = findOrCreateLookupStatement(this, attributes);
1374
+ // where().first() rather than findBy: a value the database rejects is raised, never read as no match
1375
+ const existingRecord = await this.where(lookup).first();
1347
1376
  if (!existingRecord)
1348
- throw new CreateOrUpdateByFailedToCreateAndUpdate(this);
1377
+ throw new CreateOrUpdateByFailedToCreateAndUpdate(this, await this.createOrFindFailureCause(err, lookup, extraOpts.with));
1349
1378
  const { with: attrs } = extraOpts;
1350
1379
  if (existingRecord) {
1351
1380
  existingRecord.assignAttributes(attrs ?? {});
@@ -1355,6 +1384,64 @@ export default class Dream {
1355
1384
  throw err;
1356
1385
  }
1357
1386
  }
1387
+ /**
1388
+ * @internal
1389
+ *
1390
+ * Used by createOrFindBy and createOrUpdateBy after their default-scoped
1391
+ * lookup finds nothing, to tell why. The driver names the columns of the
1392
+ * unique index the insert violated, when it can. When that index covers a
1393
+ * `createWith`/`with` attribute but not every find attribute, one of those
1394
+ * values likely conflicted with another record. When it covers only find
1395
+ * attributes, or every find attribute, a record matching them that the
1396
+ * default scopes hide is the record the insert conflicted with. Otherwise
1397
+ * the cause is undetermined, and a hidden record matching the find
1398
+ * attributes is only a candidate.
1399
+ */
1400
+ static async createOrFindFailureCause(error, lookup, extraAttributes) {
1401
+ const connectionName = this.prototype.connectionName;
1402
+ const indexColumns = await Query.dbDriverClass(connectionName).violatedUniqueIndexColumns(connectionName, error, this.table);
1403
+ const indexAttributes = indexColumns?.map(column => camelize(column)) ?? null;
1404
+ const extraAttributeNames = Object.keys(extraAttributes ? this.extractAttributesFromUpdateableProperties(extraAttributes) : {});
1405
+ // the conflicting record holds the inserted value in every column of the index, so when the
1406
+ // index covers every find attribute, that record matches them and is not a different record
1407
+ const indexCoversEveryFindAttribute = !!indexAttributes && Object.keys(lookup).every(attribute => indexAttributes.includes(attribute));
1408
+ if (!indexCoversEveryFindAttribute) {
1409
+ const conflictingAttributes = (indexAttributes ?? []).filter(attribute => extraAttributeNames.includes(attribute));
1410
+ if (conflictingAttributes.length)
1411
+ return { kind: 'conflictingAttributes', attributes: conflictingAttributes };
1412
+ }
1413
+ const hidingDefaultScopes = await this.defaultScopesHidingLookup(lookup);
1414
+ const indexCoversOnlyFindAttributes = !!indexAttributes?.every(attribute => attribute in lookup);
1415
+ if (hidingDefaultScopes.length && (indexCoversOnlyFindAttributes || indexCoversEveryFindAttribute))
1416
+ return { kind: 'hiddenByDefaultScope', defaultScopes: hidingDefaultScopes };
1417
+ return {
1418
+ kind: 'undetermined',
1419
+ hidingDefaultScopes,
1420
+ extraAttributesMayConflict: indexAttributes === null && extraAttributeNames.length > 0,
1421
+ };
1422
+ }
1423
+ /**
1424
+ * @internal
1425
+ *
1426
+ * Used by createOrFindFailureCause. Returns the model's removable default scopes when a
1427
+ * record matching `lookup` exists once they are all removed, and an empty
1428
+ * array otherwise. The STI type discriminator is never removed. One probe
1429
+ * with every removable scope removed cannot tell which of several scopes
1430
+ * hid the record, so all of them are returned as candidates.
1431
+ */
1432
+ static async defaultScopesHidingLookup(lookup) {
1433
+ const removableDefaultScopes = this['scopes'].default
1434
+ .map(scope => scope.method)
1435
+ .filter(scopeName => scopeName !== STI_SCOPE_NAME);
1436
+ if (!removableDefaultScopes.length)
1437
+ return [];
1438
+ let query = this.query();
1439
+ for (const scopeName of removableDefaultScopes) {
1440
+ query = query.removeDefaultScope(scopeName);
1441
+ }
1442
+ // where() rather than findBy: a value the database rejects is raised, never read as no match
1443
+ return (await query.where(lookup).exists()) ? removableDefaultScopes : [];
1444
+ }
1358
1445
  /**
1359
1446
  * Returns a new query instance with the distinct query applied.
1360
1447
  * If no columnName is provided, then distinct will apply to the
@@ -1372,12 +1459,25 @@ export default class Dream {
1372
1459
  }
1373
1460
  /**
1374
1461
  * Finds a record for the corresponding model with the
1375
- * specified primary key. Returns null if not found.
1462
+ * specified primary key. Returns null if not found,
1463
+ * including when the primary key is a value its column
1464
+ * cannot hold (for example, `'abc'` for an integer
1465
+ * primary key).
1466
+ *
1467
+ * When the database rejects a value elsewhere in the query (for
1468
+ * example, in a `whereNot`, `whereAny` or `leftJoinPreload`
1469
+ * clause), that rejection is raised as `DataTypeColumnTypeMismatch`
1470
+ * or `ColumnOverflow`, since a record may still match. Inside a
1471
+ * transaction, a primary key the database rejects is raised that
1472
+ * way too, because the database has then aborted the transaction.
1376
1473
  *
1377
1474
  * ```ts
1378
1475
  * await User.find(123)
1379
1476
  * // User{id: 123}
1380
1477
  *
1478
+ * await User.find('abc')
1479
+ * // null
1480
+ *
1381
1481
  * await User.find(null)
1382
1482
  * // null
1383
1483
  *
@@ -1393,15 +1493,26 @@ export default class Dream {
1393
1493
  }
1394
1494
  /**
1395
1495
  * Finds a record for the corresponding model with the
1396
- * specified primary key. If not found, an exception is raised.
1496
+ * specified primary key. If not found, `RecordNotFound` is
1497
+ * raised, including when the primary key is a value its
1498
+ * column cannot hold (for example, `'abc'` for an integer
1499
+ * primary key).
1500
+ *
1501
+ * When the database rejects a value elsewhere in the query (for
1502
+ * example, in a `whereNot`, `whereAny` or `leftJoinPreload`
1503
+ * clause), that rejection is raised as `DataTypeColumnTypeMismatch`
1504
+ * or `ColumnOverflow`, since a record may still match. Inside a
1505
+ * transaction, a primary key the database rejects is raised that
1506
+ * way too, because the database has then aborted the transaction.
1397
1507
  *
1398
1508
  * ```ts
1399
- * await User.query().findOrFail(123)
1509
+ * await User.findOrFail(123)
1400
1510
  * // User{id: 123}
1401
1511
  * ```
1402
1512
  *
1403
1513
  * @param primaryKey - The primaryKey of the record to look up
1404
- * @returns Either the found record, or else null
1514
+ * @returns The found record
1515
+ * @throws RecordNotFound if no record is found
1405
1516
  */
1406
1517
  static async findOrFail(primaryKey) {
1407
1518
  return await this.query().findOrFail(primaryKey);
@@ -1541,7 +1652,22 @@ export default class Dream {
1541
1652
  /**
1542
1653
  * Finds the first record—ordered by primary key—matching
1543
1654
  * the corresponding model and the specified where statement.
1544
- * If not found, null is returned.
1655
+ * If not found, null is returned, including when a value the where
1656
+ * statement compares for equality (a plain value or
1657
+ * `ops.equal`) is one its column cannot hold: for example,
1658
+ * `'abc'` for a uuid column, or a date string Dream cannot
1659
+ * parse for a date column.
1660
+ *
1661
+ * A value the database rejects anywhere else (in a list of values,
1662
+ * under another operator such as `ops.not.equal` or `ops.lessThan`,
1663
+ * or elsewhere in the query, such as a `whereNot` or `whereAny`
1664
+ * clause) is raised as `DataTypeColumnTypeMismatch` or
1665
+ * `ColumnOverflow`, since a record may still match; so is a date,
1666
+ * datetime or time string there that Dream cannot parse. Inside a
1667
+ * transaction, an equality value the database rejects is raised that
1668
+ * way too, because the database has then aborted the transaction; a
1669
+ * date, datetime or time string Dream cannot parse never reaches the
1670
+ * database, so it is treated as not found there as well.
1545
1671
  *
1546
1672
  * ```ts
1547
1673
  * await User.findBy({ email: 'how@yadoin' })
@@ -1557,7 +1683,22 @@ export default class Dream {
1557
1683
  /**
1558
1684
  * Finds the first record—ordered by primary key—matching
1559
1685
  * the corresponding model and the specified where statement.
1560
- * If not found, an exception is raised.
1686
+ * If not found, `RecordNotFound` is raised, including when
1687
+ * a value the where statement compares for equality (a plain
1688
+ * value or `ops.equal`) is one its column cannot hold: for
1689
+ * example, `'abc'` for a uuid column, or a date string Dream
1690
+ * cannot parse for a date column.
1691
+ *
1692
+ * A value the database rejects anywhere else (in a list of values,
1693
+ * under another operator such as `ops.not.equal` or `ops.lessThan`,
1694
+ * or elsewhere in the query, such as a `whereNot` or `whereAny`
1695
+ * clause) is raised as `DataTypeColumnTypeMismatch` or
1696
+ * `ColumnOverflow`, since a record may still match; so is a date,
1697
+ * datetime or time string there that Dream cannot parse. Inside a
1698
+ * transaction, an equality value the database rejects is raised that
1699
+ * way too, because the database has then aborted the transaction; a
1700
+ * date, datetime or time string Dream cannot parse never reaches the
1701
+ * database, so it is treated as not found there as well.
1561
1702
  *
1562
1703
  * ```ts
1563
1704
  * await User.findOrFailBy({ email: 'how@yadoin' })
@@ -1566,6 +1707,7 @@ export default class Dream {
1566
1707
  *
1567
1708
  * @param whereStatement - The where statement used to locate the record
1568
1709
  * @returns The first model found matching the whereStatement
1710
+ * @throws RecordNotFound if no record is found
1569
1711
  */
1570
1712
  static async findOrFailBy(whereStatement) {
1571
1713
  return await this.query().findOrFailBy(whereStatement);
@@ -252,6 +252,10 @@ ${INDENT} # model is named GroupDanceLesson instead of LessonDanceGroup`)
252
252
  .option('--admin-serializers', 'also generate AdminSerializer and AdminSummarySerializer variants for admin-facing API endpoints that may expose additional fields', false)
253
253
  .option('--internal-serializers', 'also generate InternalSerializer and InternalSummarySerializer variants for internal API endpoints that may expose additional fields', false)
254
254
  .option('--no-soft-delete', `skip generating the @SoftDelete() decorator and the corresponding nullable \`deleted_at\` column. By default, generated models use soft-delete semantics (rows are marked deleted via \`deleted_at\` instead of being removed from the database). Pass this flag when you want records to be hard-deleted.`)
255
+ .option('--overwrite', 'replace the model, spec, factory and serializer files when they already exist. Without it or --skip-existing, the generator writes nothing and lists the existing files when any of them, or the migration it would write, already exists. An existing migration is kept even with --overwrite, never rewritten or duplicated, since it may already have run; edit it by hand if the columns changed', false)
256
+ .addOption(new Option('--skip-existing', 'keep the model, spec, factory, serializer and migration files that already exist, write only the missing ones, and list each file kept. No migration is written when the model file already exists, even if no migration has the name the generator would use, since the model may already have one under another name. Cannot be combined with --overwrite')
257
+ .default(false)
258
+ .conflicts('overwrite'))
255
259
  .argument('<modelName>', `The fully qualified model name, using / for namespacing. This determines the model class name (may be overridden with \`--model-name\`), table name, and file path under src/app/models/.
256
260
  ${INDENT}
257
261
  ${INDENT}Examples:
@@ -261,7 +265,11 @@ ${INDENT} Settings/CommunicationPreferences # src/app/models/Settings/Communi
261
265
  .argument('[columnsWithTypes...]', columnsWithTypesDescription)
262
266
  .action(async (modelName, columnsWithTypes, options) => {
263
267
  await initializeDreamApp({ bypassDreamIntegrityChecks: true, bypassDbConnectionsDuringInit: true });
264
- await DreamBin.generateDream(modelName, columnsWithTypes, options);
268
+ const { overwrite, skipExisting, ...generatorOptions } = options;
269
+ await DreamBin.generateDream(modelName, columnsWithTypes, {
270
+ ...generatorOptions,
271
+ existingFiles: existingFilesMode({ overwrite, skipExisting }),
272
+ });
265
273
  process.exit();
266
274
  });
267
275
  program
@@ -290,6 +298,10 @@ ${INDENT} pnpm psy g:sti-child --model-name=GroupDanceLesson Lesson/Dance/Group
290
298
  ${INDENT} # model is named GroupDanceLesson instead of LessonDanceGroup`)
291
299
  .option('--admin-serializers', 'also generate AdminSerializer and AdminSummarySerializer variants for admin-facing API endpoints that may expose additional fields', false)
292
300
  .option('--internal-serializers', 'also generate InternalSerializer and InternalSummarySerializer variants for internal API endpoints that may expose additional fields', false)
301
+ .option('--overwrite', 'replace the model, spec, factory and serializer files when they already exist. Without it or --skip-existing, the generator writes nothing and lists the existing files when any of them, or the migration it would write, already exists. An existing migration is kept even with --overwrite, never rewritten or duplicated, since it may already have run; edit it by hand if the columns changed', false)
302
+ .addOption(new Option('--skip-existing', 'keep the model, spec, factory, serializer and migration files that already exist, write only the missing ones, and list each file kept. No migration is written when the model file already exists, even if no migration has the name the generator would use, since the model may already have one under another name. Cannot be combined with --overwrite')
303
+ .default(false)
304
+ .conflicts('overwrite'))
293
305
  .argument('<childModelName>', `The namespaced child model path. By convention, children are nested under the parent name.
294
306
  ${INDENT}
295
307
  ${INDENT}Examples:
@@ -307,7 +319,11 @@ ${INDENT} Health/Coach # extends src/app/models/Health/Coach.ts`)
307
319
  await initializeDreamApp({ bypassDreamIntegrityChecks: true, bypassDbConnectionsDuringInit: true });
308
320
  if (extendsWord !== 'extends')
309
321
  throw new Error('Expecting: `<child-name> extends <parent-name> <columns-and-types>');
310
- await DreamBin.generateStiChild(childModelName, parentModelName, columnsWithTypes, options);
322
+ const { overwrite, skipExisting, ...generatorOptions } = options;
323
+ await DreamBin.generateStiChild(childModelName, parentModelName, columnsWithTypes, {
324
+ ...generatorOptions,
325
+ existingFiles: existingFilesMode({ overwrite, skipExisting }),
326
+ });
311
327
  process.exit();
312
328
  });
313
329
  program
@@ -364,7 +380,12 @@ ${INDENT} pnpm psy db:migrate`)
364
380
  await syncWithFailureContext({
365
381
  commandName: 'db:migrate',
366
382
  completedWork: 'Every pending migration ran successfully',
367
- recoveryAdvice: 'run `sync` on its own, or re-run `db:migrate` — the migrations it already applied are not re-applied, so it goes straight back to the sync',
383
+ // never `sync` on its own here: the failed sync reverted the
384
+ // generated types to their pre-migration content, and a Psychic
385
+ // app's `psy sync` boots with the integrity checks on, so it
386
+ // fails against them as soon as a model or serializer uses what
387
+ // the migration added. db:migrate boots with them bypassed
388
+ recoveryAdvice: 're-run `db:migrate` — the migrations it already applied are not re-applied, so it goes straight back to the sync',
368
389
  }, async () => await DreamBin.sync(onSync));
369
390
  }
370
391
  process.exit();
@@ -444,8 +465,10 @@ ${INDENT}Warning: all existing data will be lost. The seed file (db/seed.ts) wil
444
465
  // hook directly (below), while the `db:seed` *command* refuses to
445
466
  // seed under NODE_ENV=test unless DREAM_SEED_DB_IN_TEST=1 — and
446
467
  // NODE_ENV=test is the only environment DreamBin.sync does any
447
- // work in, so it is the only environment this advice is printed in
448
- recoveryAdvice: 're-run `db:reset`, which seeds directly and so picks up the skipped seeding step, or run `sync` on its own followed by `DREAM_SEED_DB_IN_TEST=1 db:seed` — this sync only runs under `NODE_ENV=test`, and a bare `db:seed` does not seed there',
468
+ // work in, so it is the only environment this advice is printed in.
469
+ // The second route re-runs db:migrate rather than `sync` for the
470
+ // reason given at db:migrate's own recovery advice
471
+ recoveryAdvice: 're-run `db:reset`, which seeds directly and so picks up the skipped seeding step, or re-run `db:migrate` followed by `DREAM_SEED_DB_IN_TEST=1 db:seed` — this sync only runs under `NODE_ENV=test`, and a bare `db:seed` does not seed there',
449
472
  }, async () => await DreamBin.sync(onSync));
450
473
  DreamCLI.logger.log(arrows, { logPrefix: ' ' });
451
474
  DreamCLI.logger.log(colorize('db:seed', { color: 'green' }), {
@@ -577,6 +600,17 @@ ${INDENT}Examples: User, Place, Room/Bedroom, Settings/CommunicationPreferences`
577
600
  }
578
601
  static _logger = undefined;
579
602
  }
603
+ /**
604
+ * commander rejects --overwrite together with --skip-existing before an
605
+ * action runs, so at most one of them is set here.
606
+ */
607
+ function existingFilesMode({ overwrite, skipExisting, }) {
608
+ if (overwrite)
609
+ return 'overwrite';
610
+ if (skipExisting)
611
+ return 'skip';
612
+ return 'refuse';
613
+ }
580
614
  function myParseInt(value) {
581
615
  const parsedValue = parseInt(value, 10);
582
616
  if (isNaN(parsedValue)) {
@@ -11,6 +11,24 @@ import DreamApp from '../dream-app/index.js';
11
11
  import { claimedTestDatabaseIndexOrNull } from './testDatabasePool.js';
12
12
  import protectAgainstPollutingAssignment from '../helpers/protectAgainstPollutingAssignment.js';
13
13
  import { installDbConnectionLeakDiagnosticsIfEnabled, reportLeakedDbConnections, } from './dbConnectionLeakDiagnostics.js';
14
+ /**
15
+ * @internal
16
+ *
17
+ * the CamelCasePlugin every Dream connection carries: it turns each
18
+ * attribute name a query names into the column name it queries, and each
19
+ * column name a result row carries into a camelized key
20
+ */
21
+ export class DreamCamelCasePlugin extends CamelCasePlugin {
22
+ constructor() {
23
+ super({ underscoreBetweenUppercaseLetters: true });
24
+ }
25
+ /**
26
+ * the column name a query naming the given attribute targets
27
+ */
28
+ columnNameForAttribute(attributeName) {
29
+ return this.snakeCase(attributeName);
30
+ }
31
+ }
14
32
  let connections = {};
15
33
  export default class DreamDbConnection {
16
34
  static getConnection(connectionName, connectionType, dialectProvider) {
@@ -32,7 +50,7 @@ export default class DreamDbConnection {
32
50
  });
33
51
  },
34
52
  dialect: dialectProvider(connectionConf),
35
- plugins: [new CamelCasePlugin({ underscoreBetweenUppercaseLetters: true })],
53
+ plugins: [new DreamCamelCasePlugin()],
36
54
  });
37
55
  const protectedName = protectAgainstPollutingAssignment(connectionName);
38
56
  connections[protectedName] ||= {};
@@ -294,6 +294,12 @@ export default class DreamClassTransactionBuilder {
294
294
  * Finds a record for the corresponding model with the
295
295
  * specified primary key. If not found, null is returned.
296
296
  *
297
+ * A value the database rejects as one its column cannot hold,
298
+ * whether the primary key (for example, `'abc'` for an integer
299
+ * primary key) or a value elsewhere in the query, raises
300
+ * `DataTypeColumnTypeMismatch` or `ColumnOverflow`, because the
301
+ * database has then aborted the transaction.
302
+ *
297
303
  * ```ts
298
304
  * await ApplicationModel.transaction(async txn => {
299
305
  * await User.txn(txn).find(123)
@@ -317,6 +323,12 @@ export default class DreamClassTransactionBuilder {
317
323
  * Finds a record for the corresponding model with the
318
324
  * specified primary key. If not found, an exception is raised.
319
325
  *
326
+ * A value the database rejects as one its column cannot hold,
327
+ * whether the primary key (for example, `'abc'` for an integer
328
+ * primary key) or a value elsewhere in the query, raises
329
+ * `DataTypeColumnTypeMismatch` or `ColumnOverflow`, because the
330
+ * database has then aborted the transaction.
331
+ *
320
332
  * ```ts
321
333
  * await ApplicationModel.transaction(async txn => {
322
334
  * await User.txn(txn).findOrFail(123)
@@ -333,7 +345,17 @@ export default class DreamClassTransactionBuilder {
333
345
  /**
334
346
  * Finds the first record—ordered by primary key—matching
335
347
  * the corresponding model and the specified where statement.
336
- * If not found, null is returned.
348
+ * If not found, null is returned, including when a value the where
349
+ * statement compares for equality (a plain value or `ops.equal`)
350
+ * is a date, datetime or time string Dream cannot parse for its
351
+ * column, which never reaches the database.
352
+ *
353
+ * A value the database rejects as one its column cannot hold
354
+ * (for example, `'abc'` for a uuid column) raises
355
+ * `DataTypeColumnTypeMismatch` or `ColumnOverflow`, because the
356
+ * database has then aborted the transaction. A date, datetime or
357
+ * time string Dream cannot parse anywhere else in the query raises
358
+ * `DataTypeColumnTypeMismatch`.
337
359
  *
338
360
  * ```ts
339
361
  * await ApplicationModel.transaction(async txn => {
@@ -351,7 +373,17 @@ export default class DreamClassTransactionBuilder {
351
373
  /**
352
374
  * Finds the first record—ordered by primary key—matching
353
375
  * the corresponding model and the specified where statement.
354
- * If not found, an exception is raised.
376
+ * If not found, `RecordNotFound` is raised, including when a
377
+ * value the where statement compares for equality (a plain value
378
+ * or `ops.equal`) is a date, datetime or time string Dream cannot
379
+ * parse for its column, which never reaches the database.
380
+ *
381
+ * A value the database rejects as one its column cannot hold
382
+ * (for example, `'abc'` for a uuid column) raises
383
+ * `DataTypeColumnTypeMismatch` or `ColumnOverflow`, because the
384
+ * database has then aborted the transaction. A date, datetime or
385
+ * time string Dream cannot parse anywhere else in the query raises
386
+ * `DataTypeColumnTypeMismatch`.
355
387
  *
356
388
  * ```ts
357
389
  * await ApplicationModel.transaction(async txn => {