@rvoh/dream 2.40.2 → 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 (269) hide show
  1. package/dist/cjs/src/Dream.js +156 -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 +5 -1
  10. package/dist/cjs/src/dream/internal/findOrCreateLookupStatement.js +19 -0
  11. package/dist/cjs/src/dream/internal/updateOrCreateBy.js +5 -1
  12. package/dist/cjs/src/encrypt/algorithms/aes-gcm/decryptAESGCM.js +24 -10
  13. package/dist/cjs/src/errors/CreateOrFindByFailedToCreateAndFind.js +63 -6
  14. package/dist/cjs/src/errors/CreateOrUpdateByFailedToCreateAndUpdate.js +41 -6
  15. package/dist/cjs/src/errors/GeneratedFilesAlreadyExist.js +38 -0
  16. package/dist/cjs/src/errors/schema-builder/ColumnNamesDoNotRoundTrip.js +28 -0
  17. package/dist/cjs/src/helpers/cli/ASTConnectionBuilder.js +62 -10
  18. package/dist/cjs/src/helpers/cli/generateDream.js +109 -18
  19. package/dist/cjs/src/helpers/cli/generateMigration.js +12 -8
  20. package/dist/cjs/src/helpers/cli/writeGeneratedFile.js +3 -3
  21. package/dist/cjs/src/helpers/db/normalizeDataForDb.js +33 -4
  22. package/dist/cjs/src/ops/index.js +7 -3
  23. package/dist/esm/src/Dream.js +156 -13
  24. package/dist/esm/src/cli/index.js +39 -5
  25. package/dist/esm/src/db/DreamDbConnection.js +19 -1
  26. package/dist/esm/src/dream/DreamClassTransactionBuilder.js +34 -2
  27. package/dist/esm/src/dream/Query.js +178 -14
  28. package/dist/esm/src/dream/QueryDriver/Base.js +80 -0
  29. package/dist/esm/src/dream/QueryDriver/Kysely.js +35 -29
  30. package/dist/esm/src/dream/QueryDriver/Postgres.js +83 -1
  31. package/dist/esm/src/dream/internal/findOrCreateBy.js +5 -1
  32. package/dist/esm/src/dream/internal/findOrCreateLookupStatement.js +19 -0
  33. package/dist/esm/src/dream/internal/updateOrCreateBy.js +5 -1
  34. package/dist/esm/src/encrypt/algorithms/aes-gcm/decryptAESGCM.js +24 -10
  35. package/dist/esm/src/errors/CreateOrFindByFailedToCreateAndFind.js +63 -6
  36. package/dist/esm/src/errors/CreateOrUpdateByFailedToCreateAndUpdate.js +41 -6
  37. package/dist/esm/src/errors/GeneratedFilesAlreadyExist.js +38 -0
  38. package/dist/esm/src/errors/schema-builder/ColumnNamesDoNotRoundTrip.js +28 -0
  39. package/dist/esm/src/helpers/cli/ASTConnectionBuilder.js +62 -10
  40. package/dist/esm/src/helpers/cli/generateDream.js +109 -18
  41. package/dist/esm/src/helpers/cli/generateMigration.js +12 -8
  42. package/dist/esm/src/helpers/cli/writeGeneratedFile.js +3 -3
  43. package/dist/esm/src/helpers/db/normalizeDataForDb.js +33 -4
  44. package/dist/esm/src/ops/index.js +7 -3
  45. package/dist/types/src/Dream.d.ts +109 -6
  46. package/dist/types/src/bin/index.d.ts +11 -0
  47. package/dist/types/src/cli/index.d.ts +27 -0
  48. package/dist/types/src/db/DreamDbConnection.d.ts +15 -1
  49. package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +34 -2
  50. package/dist/types/src/dream/Query.d.ts +101 -6
  51. package/dist/types/src/dream/QueryDriver/Base.d.ts +59 -0
  52. package/dist/types/src/dream/QueryDriver/Kysely.d.ts +9 -0
  53. package/dist/types/src/dream/QueryDriver/Postgres.d.ts +29 -1
  54. package/dist/types/src/dream/internal/executeDatabaseQuery.d.ts +2 -1
  55. package/dist/types/src/dream/internal/findOrCreateLookupStatement.d.ts +14 -0
  56. package/dist/types/src/errors/CreateOrFindByFailedToCreateAndFind.d.ts +40 -1
  57. package/dist/types/src/errors/CreateOrUpdateByFailedToCreateAndUpdate.d.ts +8 -1
  58. package/dist/types/src/errors/GeneratedFilesAlreadyExist.d.ts +18 -0
  59. package/dist/types/src/errors/schema-builder/ColumnNamesDoNotRoundTrip.d.ts +12 -0
  60. package/dist/types/src/helpers/cli/ASTConnectionBuilder.d.ts +23 -1
  61. package/dist/types/src/helpers/cli/generateDream.d.ts +25 -0
  62. package/dist/types/src/helpers/cli/generateMigration.d.ts +20 -2
  63. package/dist/types/src/helpers/cli/syncWithFailureContext.d.ts +7 -2
  64. package/dist/types/src/helpers/cli/writeGeneratedFile.d.ts +6 -1
  65. package/dist/types/src/helpers/db/normalizeDataForDb.d.ts +7 -0
  66. package/dist/types/src/ops/index.d.ts +7 -3
  67. package/dist/types/src/types/associations/shared.d.ts +7 -5
  68. package/dist/types/src/types/types/associations/shared.ts +27 -5
  69. package/docs/assets/hierarchy.js +1 -1
  70. package/docs/assets/search.js +1 -1
  71. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  72. package/docs/classes/db.KyselyQueryDriver.html +87 -50
  73. package/docs/classes/db.PostgresQueryDriver.html +76 -50
  74. package/docs/classes/db.QueryDriverBase.html +87 -50
  75. package/docs/classes/errors.CannotSaveMissingDream.html +2 -2
  76. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  77. package/docs/classes/errors.ColumnOverflow.html +3 -3
  78. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +6 -3
  79. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  80. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  81. package/docs/classes/errors.DecryptionError.html +2 -2
  82. package/docs/classes/errors.DecryptionParseError.html +2 -2
  83. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  84. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  85. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  86. package/docs/classes/errors.InvalidClockTime.html +2 -2
  87. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  88. package/docs/classes/errors.InvalidDateTime.html +2 -2
  89. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  90. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  91. package/docs/classes/errors.NotNullViolation.html +3 -3
  92. package/docs/classes/errors.RecordNotFound.html +3 -3
  93. package/docs/classes/errors.SortableScopeDidNotStabilize.html +3 -3
  94. package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +3 -3
  95. package/docs/classes/errors.ValidationError.html +3 -3
  96. package/docs/classes/index.CalendarDate.html +33 -33
  97. package/docs/classes/index.ClockTime.html +32 -32
  98. package/docs/classes/index.ClockTimeTz.html +35 -35
  99. package/docs/classes/index.DateTime.html +86 -86
  100. package/docs/classes/index.Decorators.html +19 -19
  101. package/docs/classes/index.Dream.html +204 -134
  102. package/docs/classes/index.DreamApp.html +13 -13
  103. package/docs/classes/index.DreamTransaction.html +3 -3
  104. package/docs/classes/index.Env.html +5 -5
  105. package/docs/classes/index.Query.html +147 -100
  106. package/docs/classes/system.CliFileWriter.html +4 -4
  107. package/docs/classes/system.DreamBin.html +7 -3
  108. package/docs/classes/system.DreamCLI.html +7 -7
  109. package/docs/classes/system.DreamImporter.html +2 -2
  110. package/docs/classes/system.DreamLogos.html +2 -2
  111. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  112. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  113. package/docs/classes/system.PathHelpers.html +3 -3
  114. package/docs/classes/utils.Encrypt.html +4 -4
  115. package/docs/classes/utils.Range.html +2 -2
  116. package/docs/functions/db.closeAllDbConnections.html +1 -1
  117. package/docs/functions/db.dreamDbConnections.html +1 -1
  118. package/docs/functions/db.untypedDb.html +1 -1
  119. package/docs/functions/db.validateColumn.html +1 -1
  120. package/docs/functions/db.validateTable.html +1 -1
  121. package/docs/functions/errors.pgErrorType.html +1 -1
  122. package/docs/functions/index.DreamSerializer.html +1 -1
  123. package/docs/functions/index.ObjectSerializer.html +1 -1
  124. package/docs/functions/index.ReplicaSafe.html +1 -1
  125. package/docs/functions/index.STI.html +1 -1
  126. package/docs/functions/index.SoftDelete.html +1 -1
  127. package/docs/functions/utils.camelize.html +1 -1
  128. package/docs/functions/utils.capitalize.html +1 -1
  129. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  130. package/docs/functions/utils.compact.html +1 -1
  131. package/docs/functions/utils.groupBy.html +1 -1
  132. package/docs/functions/utils.hyphenize.html +1 -1
  133. package/docs/functions/utils.intersection.html +1 -1
  134. package/docs/functions/utils.isEmpty.html +3 -3
  135. package/docs/functions/utils.normalizeUnicode.html +1 -1
  136. package/docs/functions/utils.pascalize.html +1 -1
  137. package/docs/functions/utils.percent.html +1 -1
  138. package/docs/functions/utils.range.html +1 -1
  139. package/docs/functions/utils.round.html +1 -1
  140. package/docs/functions/utils.sanitizeString.html +1 -1
  141. package/docs/functions/utils.snakeify.html +1 -1
  142. package/docs/functions/utils.sort.html +1 -1
  143. package/docs/functions/utils.sortBy.html +1 -1
  144. package/docs/functions/utils.sortObjectByKey.html +1 -1
  145. package/docs/functions/utils.sortObjectByValue.html +1 -1
  146. package/docs/functions/utils.uncapitalize.html +1 -1
  147. package/docs/functions/utils.uniq.html +1 -1
  148. package/docs/hierarchy.html +1 -1
  149. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  150. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  151. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  152. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  153. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  154. package/docs/interfaces/types.DecoratorContext.html +2 -2
  155. package/docs/interfaces/types.DecryptCallbacks.html +2 -2
  156. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  157. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  158. package/docs/interfaces/types.DreamDbConfig.html +6 -6
  159. package/docs/interfaces/types.DurationObject.html +2 -2
  160. package/docs/interfaces/types.EncryptOptions.html +2 -2
  161. package/docs/interfaces/types.EncryptionLegacyKeyUsedEvent.html +4 -4
  162. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  163. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  164. package/docs/interfaces/types.IntrospectedColumn.html +7 -7
  165. package/docs/interfaces/types.IntrospectedDatabase.html +2 -2
  166. package/docs/interfaces/types.IntrospectedEnum.html +2 -2
  167. package/docs/interfaces/types.IntrospectedTable.html +5 -5
  168. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  169. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  170. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  171. package/docs/types/openapi.OpenapiFormats.html +1 -1
  172. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  173. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  174. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  175. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  176. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  177. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  178. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  179. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  180. package/docs/types/openapi.OpenapiSchemaCombinatorFields.html +1 -1
  181. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  182. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +1 -1
  183. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +1 -1
  184. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +1 -1
  185. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  186. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  187. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  188. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  189. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  190. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  191. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  192. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  193. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  194. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.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 +1 -1
  198. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +1 -1
  199. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +1 -1
  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.IntrospectedValueType.html +1 -1
  239. package/docs/types/types.Pascalized.html +1 -1
  240. package/docs/types/types.PrimaryKeyType.html +1 -1
  241. package/docs/types/types.RoundingPrecision.html +1 -1
  242. package/docs/types/types.SerializerCasing.html +1 -1
  243. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  244. package/docs/types/types.Snakeified.html +1 -1
  245. package/docs/types/types.StrictInterface.html +1 -1
  246. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  247. package/docs/types/types.UpdateableProperties.html +1 -1
  248. package/docs/types/types.ValidationType.html +1 -1
  249. package/docs/types/types.ViewModel.html +2 -2
  250. package/docs/types/types.ViewModelClass.html +1 -1
  251. package/docs/types/types.WeekdayName.html +1 -1
  252. package/docs/types/types.WhereStatementForDream.html +1 -1
  253. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  254. package/docs/variables/index.DreamConst.html +1 -1
  255. package/docs/variables/index.ops.html +8 -4
  256. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  257. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  258. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  259. package/docs/variables/system.primaryKeyTypes.html +1 -1
  260. package/package.json +1 -1
  261. package/dist/cjs/src/helpers/cli/generateFactory.js +0 -10
  262. package/dist/cjs/src/helpers/cli/generateSerializer.js +0 -18
  263. package/dist/cjs/src/helpers/cli/generateUnitSpec.js +0 -10
  264. package/dist/esm/src/helpers/cli/generateFactory.js +0 -10
  265. package/dist/esm/src/helpers/cli/generateSerializer.js +0 -18
  266. package/dist/esm/src/helpers/cli/generateUnitSpec.js +0 -10
  267. package/dist/types/src/helpers/cli/generateFactory.d.ts +0 -5
  268. package/dist/types/src/helpers/cli/generateSerializer.d.ts +0 -9
  269. package/dist/types/src/helpers/cli/generateUnitSpec.d.ts +0 -3
@@ -0,0 +1,19 @@
1
+ import isDatabaseArrayColumn from '../../helpers/db/types/isDatabaseArrayColumn.js';
2
+ import ops from '../../ops/index.js';
3
+ /**
4
+ * Builds the `findBy` statement that findOrCreateBy, createOrFindBy,
5
+ * updateOrCreateBy and createOrUpdateBy use to look up an existing record.
6
+ * Their attributes are both the lookup and the values to persist, so an array
7
+ * given for an array column is the whole array to store: the lookup compares
8
+ * it with `ops.equal`, matching only a record whose array equals it
9
+ * element-for-element, in order, instead of the `IN` a plain list means in a
10
+ * where clause. PostgreSQL defines no equality operator for `json`, so on a
11
+ * `json[]` column this lookup raises at the database, as `ops.equal` does.
12
+ */
13
+ export default function findOrCreateLookupStatement(dreamClass, attributes) {
14
+ const lookup = dreamClass['extractAttributesFromUpdateableProperties'](attributes);
15
+ return Object.fromEntries(Object.entries(lookup).map(([column, value]) => [
16
+ column,
17
+ Array.isArray(value) && isDatabaseArrayColumn(dreamClass, column) ? ops.equal(value) : value,
18
+ ]));
19
+ }
@@ -1,8 +1,12 @@
1
+ import findOrCreateLookupStatement from './findOrCreateLookupStatement.js';
1
2
  import saveDream from './saveDream.js';
2
3
  export default async function updateOrCreateBy(dreamClass, txn = null, attributes, extraOpts = {}) {
4
+ // where().first() rather than findBy: a value the database rejects is raised, never read as no match,
5
+ // so it cannot lead to creating a record that already exists
3
6
  const existingRecord = await dreamClass
4
7
  .txn(txn)
5
- .findBy(dreamClass['extractAttributesFromUpdateableProperties'](attributes));
8
+ .where(findOrCreateLookupStatement(dreamClass, attributes))
9
+ .first();
6
10
  const { with: attrs, skipHooks } = extraOpts;
7
11
  if (existingRecord) {
8
12
  existingRecord.assignAttributes(attrs ?? {});
@@ -2,9 +2,9 @@ import * as crypto from 'crypto';
2
2
  import DecryptionError from '../../../errors/encrypt/DecryptionError.js';
3
3
  import DecryptionParseError from '../../../errors/encrypt/DecryptionParseError.js';
4
4
  export default function decryptAESGCM(algorithm, encrypted, key) {
5
+ const { ciphertext, tag, iv } = unpackPayloadOrFail(encrypted);
5
6
  let plaintext;
6
7
  try {
7
- const { ciphertext, tag, iv } = unpackPayloadOrFail(encrypted);
8
8
  const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'));
9
9
  decipher.setAuthTag(Buffer.from(tag, 'base64'));
10
10
  // Decrypt to a complete plaintext Buffer and decode utf8 once, mirroring the
@@ -18,6 +18,9 @@ export default function decryptAESGCM(algorithm, encrypted, key) {
18
18
  ]).toString('utf8');
19
19
  }
20
20
  catch (cause) {
21
+ // The cipher's error is kept as `cause`: its messages carry no input bytes, and
22
+ // callers read its `code` (e.g. ERR_CRYPTO_INVALID_KEYLEN to tell a misconfigured
23
+ // key from an undecryptable value).
21
24
  throw Object.assign(new DecryptionError(), { cause });
22
25
  }
23
26
  try {
@@ -32,17 +35,28 @@ export default function decryptAESGCM(algorithm, encrypted, key) {
32
35
  throw new DecryptionParseError();
33
36
  }
34
37
  }
38
+ // Every way the envelope can be malformed throws DecryptionError with no `cause`. The
39
+ // encrypted value is caller input (a cookie, a token, a column that was never
40
+ // encrypted), and the errors that unpacking it raises quote it: JSON.parse's
41
+ // SyntaxError embeds a snippet of the decoded text, and Buffer.from's TypeError on a
42
+ // non-string field echoes the field's value. Checking every field is a string here
43
+ // means the cipher operations in decryptAESGCM only ever see strings.
35
44
  function unpackPayloadOrFail(payload) {
36
- const unpackedPayload = (typeof payload === 'string' ? JSON.parse(Buffer.from(payload, 'base64').toString()) : payload);
37
- if (typeof unpackedPayload !== 'object') {
38
- throw new Error('Failed to unpack encrypted object. Must be an object with a ciphertext and tag property');
45
+ let unpackedPayload;
46
+ try {
47
+ unpackedPayload =
48
+ typeof payload === 'string' ? JSON.parse(Buffer.from(payload, 'base64').toString()) : payload;
49
+ }
50
+ catch {
51
+ throw new DecryptionError();
39
52
  }
53
+ if (typeof unpackedPayload !== 'object' || unpackedPayload === null)
54
+ throw new DecryptionError();
40
55
  const { ciphertext, tag, iv } = unpackedPayload;
41
- if (!ciphertext)
42
- throw new Error('invalid ciphertext found');
43
- if (!tag)
44
- throw new Error('invalid tag found');
45
- if (!iv)
46
- throw new Error('missing iv');
56
+ if (!isNonEmptyString(ciphertext) || !isNonEmptyString(tag) || !isNonEmptyString(iv))
57
+ throw new DecryptionError();
47
58
  return { ciphertext, tag, iv };
48
59
  }
60
+ function isNonEmptyString(value) {
61
+ return typeof value === 'string' && value.length > 0;
62
+ }
@@ -1,15 +1,72 @@
1
+ export const UNDETERMINED_CREATE_OR_FIND_FAILURE = {
2
+ kind: 'undetermined',
3
+ hidingDefaultScopes: [],
4
+ extraAttributesMayConflict: true,
5
+ };
1
6
  export default class CreateOrFindByFailedToCreateAndFind extends Error {
2
7
  dreamClass;
3
- constructor(dreamClass) {
8
+ failureCause;
9
+ /**
10
+ * @param dreamClass - the model `createOrFindBy` was called on
11
+ * @param failureCause - what could be told about why the record that
12
+ * blocked the insert was not found
13
+ */
14
+ constructor(dreamClass, failureCause = UNDETERMINED_CREATE_OR_FIND_FAILURE) {
4
15
  super();
5
16
  this.dreamClass = dreamClass;
17
+ this.failureCause = failureCause;
6
18
  }
7
19
  get message() {
8
- return `
9
- Failed to create instance of ${this.dreamClass.sanitizedName} and no matching model exists.
20
+ const name = this.dreamClass.sanitizedName;
21
+ const failureCause = this.failureCause;
22
+ switch (failureCause.kind) {
23
+ case 'conflictingAttributes':
24
+ return `
25
+ Failed to create instance of ${name}: the insert violated a uniqueness constraint,
26
+ and no ${name} matching the find attributes was found.
10
27
 
11
- The likely cause is that one of the \`createWith\` fields violates
12
- a uniqueness constraint.
13
- `;
28
+ The violated unique index covers the \`createWith\` ${describeAttributes(failureCause.attributes)},
29
+ so a \`createWith\` value likely conflicts with a different record. createOrFindBy
30
+ looks the existing record up by the find attributes only; to find a record by
31
+ ${failureCause.attributes.join(', ')}, pass it as a find attribute instead.
32
+ `;
33
+ case 'hiddenByDefaultScope':
34
+ return `
35
+ Failed to create instance of ${name}: the insert violated a uniqueness constraint,
36
+ and the ${name} matching the find attributes was not found because it is
37
+ likely hidden by ${describeDefaultScopes(failureCause.defaultScopes)}.
38
+
39
+ createOrFindBy only finds records the model's default scopes return, so it
40
+ cannot return a hidden record. Restore the hidden record, or narrow the unique
41
+ index so that it covers only the records the default scopes return (for
42
+ example, a partial unique index).
43
+ `;
44
+ case 'undetermined':
45
+ return `
46
+ Failed to create instance of ${name}: the insert violated a uniqueness constraint,
47
+ and no ${name} matching the find attributes was found.
48
+
49
+ Possible causes:
50
+ ${describePossibleCauses(failureCause, '`createWith`')}
51
+ `;
52
+ }
14
53
  }
15
54
  }
55
+ export function describeDefaultScopes(scopeNames) {
56
+ const quoted = scopeNames.map(scopeName => `"${scopeName}"`).join(', ');
57
+ return scopeNames.length === 1 ? `the default scope ${quoted}` : `one of the default scopes ${quoted}`;
58
+ }
59
+ export function describeAttributes(attributes) {
60
+ return `${attributes.length === 1 ? 'attribute' : 'attributes'} ${attributes.join(', ')}`;
61
+ }
62
+ export function describePossibleCauses(failureCause, extraAttributesOption) {
63
+ const causes = [];
64
+ if (failureCause.hidingDefaultScopes.length)
65
+ causes.push(`a record matching the find attributes exists but is hidden by ${describeDefaultScopes(failureCause.hidingDefaultScopes)}`);
66
+ if (failureCause.extraAttributesMayConflict)
67
+ causes.push(`a ${extraAttributesOption} value conflicts with a different record's unique column`);
68
+ causes.push(`a find attribute outside the violated unique index differs from the
69
+ conflicting record's value`, `a hook changed a find attribute before the insert, so the lookup used
70
+ the original value`, 'the conflicting record was deleted between the failed insert and the lookup');
71
+ return causes.map(cause => ` - ${cause}`).join('\n');
72
+ }
@@ -1,15 +1,50 @@
1
+ import { describeAttributes, describeDefaultScopes, describePossibleCauses, UNDETERMINED_CREATE_OR_FIND_FAILURE, } from './CreateOrFindByFailedToCreateAndFind.js';
1
2
  export default class CreateOrUpdateByFailedToCreateAndUpdate extends Error {
2
3
  dreamClass;
3
- constructor(dreamClass) {
4
+ failureCause;
5
+ /**
6
+ * @param dreamClass - the model `createOrUpdateBy` was called on
7
+ * @param failureCause - what could be told about why the record that
8
+ * blocked the insert was not found
9
+ */
10
+ constructor(dreamClass, failureCause = UNDETERMINED_CREATE_OR_FIND_FAILURE) {
4
11
  super();
5
12
  this.dreamClass = dreamClass;
13
+ this.failureCause = failureCause;
6
14
  }
7
15
  get message() {
8
- return `
9
- Failed to create instance of ${this.dreamClass.sanitizedName} and no matching model exists to update.
16
+ const name = this.dreamClass.sanitizedName;
17
+ const failureCause = this.failureCause;
18
+ switch (failureCause.kind) {
19
+ case 'conflictingAttributes':
20
+ return `
21
+ Failed to create instance of ${name}: the insert violated a uniqueness constraint,
22
+ and no ${name} matching the find attributes exists to update.
10
23
 
11
- The likely cause is that one of the \`with\` fields violates
12
- a uniqueness constraint.
13
- `;
24
+ The violated unique index covers the \`with\` ${describeAttributes(failureCause.attributes)},
25
+ so a \`with\` value likely conflicts with a different record. createOrUpdateBy
26
+ looks the record to update up by the find attributes only; to find a record by
27
+ ${failureCause.attributes.join(', ')}, pass it as a find attribute instead.
28
+ `;
29
+ case 'hiddenByDefaultScope':
30
+ return `
31
+ Failed to create instance of ${name}: the insert violated a uniqueness constraint,
32
+ and the ${name} to update was not found because it is
33
+ likely hidden by ${describeDefaultScopes(failureCause.defaultScopes)}.
34
+
35
+ createOrUpdateBy only finds records the model's default scopes return, so it
36
+ cannot update a hidden record. Restore the hidden record, or narrow the unique
37
+ index so that it covers only the records the default scopes return (for
38
+ example, a partial unique index).
39
+ `;
40
+ case 'undetermined':
41
+ return `
42
+ Failed to create instance of ${name}: the insert violated a uniqueness constraint,
43
+ and no ${name} matching the find attributes exists to update.
44
+
45
+ Possible causes:
46
+ ${describePossibleCauses(failureCause, '`with`')}
47
+ `;
48
+ }
14
49
  }
15
50
  }
@@ -0,0 +1,38 @@
1
+ export default class GeneratedFilesAlreadyExist extends Error {
2
+ modelName;
3
+ existingFiles;
4
+ existingMigrations;
5
+ flags;
6
+ /**
7
+ * @param modelName - the fully qualified model name the generator was invoked with
8
+ * @param existingFiles - project-relative paths of the model, spec, factory and serializer files that already exist
9
+ * @param existingMigrations - project-relative paths of existing migrations with the name the generator would give the model's migration
10
+ * @param flags - the caller's flags for overwriting and for skipping existing files; when omitted, the message names Dream's `--overwrite` and `--skip-existing` and the commands that accept them
11
+ */
12
+ constructor(modelName, existingFiles, existingMigrations, flags) {
13
+ super();
14
+ this.modelName = modelName;
15
+ this.existingFiles = existingFiles;
16
+ this.existingMigrations = existingMigrations;
17
+ this.flags = flags;
18
+ }
19
+ get message() {
20
+ const { overwrite, skip } = this.flags ?? { overwrite: '--overwrite', skip: '--skip-existing' };
21
+ const commandsNote = this.flags ? '' : ' g:model and g:sti-child accept both.';
22
+ const listing = [...this.existingFiles, ...this.existingMigrations].map(file => ` ${file}`).join('\n');
23
+ const migrationNote = this.existingMigrations.length
24
+ ? `
25
+ The existing migration is kept even with ${overwrite}, and no second one is
26
+ written, since it may already have run. Edit it by hand if the columns changed.
27
+ `
28
+ : '';
29
+ return `
30
+ Refusing to generate ${this.modelName}, because these files already exist:
31
+ ${listing}
32
+
33
+ Nothing was written. To replace the model, spec, factory and serializer files,
34
+ re-run with ${overwrite}. To keep the existing files and write only the missing
35
+ ones, re-run with ${skip}.${commandsNote}
36
+ ${migrationNote}`;
37
+ }
38
+ }
@@ -0,0 +1,28 @@
1
+ export default class ColumnNamesDoNotRoundTrip extends Error {
2
+ columns;
3
+ constructor(columns) {
4
+ super();
5
+ this.columns = columns;
6
+ }
7
+ get message() {
8
+ const columnLines = this.columns.map(column => ` ${column.tableName}.${column.columnName} (${column.modelName}): its attribute ${column.attributeName} maps back to ${column.mappedColumnName}`);
9
+ const remedyLines = this.columns.map(column => ` rename ${column.tableName}.${column.columnName} to ${column.mappedColumnName}, or add "${column.attributeName}" to the ignoredColumns getter on ${column.modelName}`);
10
+ return `
11
+ The model attribute of each of these columns maps back to a column that
12
+ does not exist:
13
+
14
+ ${columnLines.join('\n')}
15
+
16
+ Dream names a column's attribute by camelizing the column's name, and each
17
+ query naming the attribute targets the attribute's snake_cased name. For
18
+ these columns, that is a column that does not exist, so a save or update
19
+ that sets the attribute, a where clause on it, and every leftJoinPreload
20
+ that loads the model fail.
21
+
22
+ For each column, either rename it in a migration, or, if it cannot be
23
+ renamed, ignore it:
24
+
25
+ ${remedyLines.join('\n')}
26
+ `;
27
+ }
28
+ }
@@ -1,9 +1,11 @@
1
1
  import * as path from 'node:path';
2
+ import { DreamCamelCasePlugin } from '../../db/DreamDbConnection.js';
2
3
  import dbTypesFilenameForConnection from '../../db/helpers/dbTypesFilenameForConnection.js';
3
4
  import dreamSchemaTypesFilenameForConnection from '../../db/helpers/dreamSchemaTypesFilenameForConnection.js';
4
5
  import DreamApp from '../../dream-app/index.js';
5
6
  import Query from '../../dream/Query.js';
6
7
  import { ExplicitForeignKeyRequired, InvalidComputedForeignKey, } from '../../errors/associations/InvalidComputedForeignKey.js';
8
+ import ColumnNamesDoNotRoundTrip from '../../errors/schema-builder/ColumnNamesDoNotRoundTrip.js';
7
9
  import FailedToIdentifyAssociation from '../../errors/schema-builder/FailedToIdentifyAssociation.js';
8
10
  import camelize from '../camelize.js';
9
11
  import intersection from '../intersection.js';
@@ -47,7 +49,9 @@ export default class ASTConnectionBuilder extends ASTBuilder {
47
49
  * the connection's tables, sorted by their key in the DB interface, and its
48
50
  * enums, read through the query driver once, by this builder or by the one
49
51
  * it takes its introspection from. The tables that `tableIncludePattern`
50
- * leaves out, or `tableExcludePattern` names, are left out.
52
+ * leaves out, or `tableExcludePattern` names, are left out. Fails, before
53
+ * either type file is written, when a column of a model's table does not
54
+ * round-trip through its attribute name (see guardColumnNamesRoundTrip).
51
55
  */
52
56
  async introspectedDatabase() {
53
57
  this.introspection ||=
@@ -58,16 +62,65 @@ export default class ASTConnectionBuilder extends ASTBuilder {
58
62
  const credentials = DreamApp.getOrFail().dbCredentialsFor(this.connectionName);
59
63
  const includePattern = credentials?.tableIncludePattern;
60
64
  const excludePattern = credentials?.tableExcludePattern;
61
- return {
62
- ...database,
63
- tables: database.tables
64
- .filter(table => (!includePattern || tableMatchesPattern(table, includePattern)) &&
65
- !(excludePattern && tableMatchesPattern(table, excludePattern)))
66
- .sort((a, b) => this.tableKey(a).localeCompare(this.tableKey(b))),
67
- };
65
+ const tables = database.tables
66
+ .filter(table => (!includePattern || tableMatchesPattern(table, includePattern)) &&
67
+ !(excludePattern && tableMatchesPattern(table, excludePattern)))
68
+ .sort((a, b) => this.tableKey(a).localeCompare(this.tableKey(b)));
69
+ this.guardColumnNamesRoundTrip(tables);
70
+ return { ...database, tables };
68
71
  })();
69
72
  return await this.introspection;
70
73
  }
74
+ /**
75
+ * @internal
76
+ *
77
+ * fails the sync when a column of a table that one of the connection's
78
+ * models is backed by is not the column its attribute maps back to: Dream
79
+ * names the attribute by camelizing the column name, and each query naming
80
+ * the attribute targets the column the connection's CamelCasePlugin derives
81
+ * from it, so `address_line_1` (attribute `addressLine1`, which maps back to
82
+ * `address_line1`) could only be written, filtered on or join-loaded as a
83
+ * column that does not exist. A column the table's models ignore is
84
+ * skipped, since nothing queries it. A table no model is backed by is left
85
+ * to tableData, which fails the sync on it.
86
+ */
87
+ guardColumnNamesRoundTrip(tables) {
88
+ const columnNameMapper = new DreamCamelCasePlugin();
89
+ const columnsThatDoNotRoundTrip = [];
90
+ for (const table of tables) {
91
+ const tableName = this.tableKey(table);
92
+ const models = this.connectionModels().filter(model => model.table === tableName);
93
+ if (!models.length)
94
+ continue;
95
+ const ignoredColumns = this.ignoredColumnsForTable(tableName);
96
+ const modelName = uniq(models.map(model => model['stiBaseClassOrOwnClass'].sanitizedName)).join(', ');
97
+ for (const column of table.columns) {
98
+ const attributeName = camelize(column.name);
99
+ if (ignoredColumns.has(attributeName))
100
+ continue;
101
+ const mappedColumnName = columnNameMapper.columnNameForAttribute(attributeName);
102
+ if (mappedColumnName === column.name)
103
+ continue;
104
+ columnsThatDoNotRoundTrip.push({
105
+ tableName,
106
+ columnName: column.name,
107
+ attributeName,
108
+ mappedColumnName,
109
+ modelName,
110
+ });
111
+ }
112
+ }
113
+ if (columnsThatDoNotRoundTrip.length)
114
+ throw new ColumnNamesDoNotRoundTrip(columnsThatDoNotRoundTrip);
115
+ }
116
+ /**
117
+ * @internal
118
+ *
119
+ * the models on the builder's connection
120
+ */
121
+ connectionModels() {
122
+ return Object.values(DreamApp.getOrFail().models).filter(model => model.prototype?.connectionName === this.connectionName);
123
+ }
71
124
  /**
72
125
  * @internal
73
126
  *
@@ -284,8 +337,7 @@ may need to update the table getter in the corresponding Dream.
284
337
  * given table (validating the declarations; see resolveIgnoredColumns)
285
338
  */
286
339
  ignoredColumnsForTable(tableName) {
287
- const dreamApp = DreamApp.getOrFail();
288
- const allModels = Object.values(dreamApp.models).filter(model => model.prototype?.connectionName === this.connectionName);
340
+ const allModels = this.connectionModels();
289
341
  const models = allModels.filter(model => model.table === tableName);
290
342
  return resolveIgnoredColumns(models, tableName, allModels);
291
343
  }
@@ -1,9 +1,16 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import DreamCLI from '../../cli/index.js';
4
+ import DreamApp from '../../dream-app/index.js';
5
+ import GeneratedFilesAlreadyExist from '../../errors/GeneratedFilesAlreadyExist.js';
6
+ import dreamFileAndDirPaths from '../path/dreamFileAndDirPaths.js';
7
+ import dreamPath from '../path/dreamPath.js';
1
8
  import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
2
9
  import generateDreamContent from './generateDreamContent.js';
3
- import generateFactory from './generateFactory.js';
4
- import generateMigration from './generateMigration.js';
5
- import generateSerializer from './generateSerializer.js';
6
- import generateUnitSpec from './generateUnitSpec.js';
10
+ import generateFactoryContent from './generateFactoryContent.js';
11
+ import { buildMigration } from './generateMigration.js';
12
+ import generateSerializerContent from './generateSerializerContent.js';
13
+ import generateUnitSpecContent from './generateUnitSpecContent.js';
7
14
  import modelClassNameFrom from './modelClassNameFrom.js';
8
15
  import writeGeneratedFile from './writeGeneratedFile.js';
9
16
  export default async function generateDream({ fullyQualifiedModelName, columnsWithTypes, options, fullyQualifiedParentName, }) {
@@ -14,7 +21,7 @@ export default async function generateDream({ fullyQualifiedModelName, columnsWi
14
21
  // the soft-delete scaffold when generating an STI child regardless of the
15
22
  // caller-provided value.
16
23
  const softDelete = !isSTI && !!options.softDelete;
17
- await writeGeneratedFile({
24
+ const modelFile = {
18
25
  dreamPathKey: 'models',
19
26
  fileName: `${fullyQualifiedModelName}.ts`,
20
27
  content: generateDreamContent({
@@ -30,21 +37,39 @@ export default async function generateDream({ fullyQualifiedModelName, columnsWi
30
37
  softDelete,
31
38
  }),
32
39
  logLabel: 'dream',
33
- });
34
- await generateUnitSpec({ fullyQualifiedModelName });
35
- await generateFactory({ fullyQualifiedModelName, columnsWithTypes, modelClassName });
40
+ };
41
+ const files = [
42
+ modelFile,
43
+ {
44
+ dreamPathKey: 'modelSpecs',
45
+ fileName: `${fullyQualifiedModelName}.spec.ts`,
46
+ content: generateUnitSpecContent({ fullyQualifiedModelName }),
47
+ logLabel: 'spec',
48
+ },
49
+ {
50
+ dreamPathKey: 'factories',
51
+ fileName: `${fullyQualifiedModelName}Factory.ts`,
52
+ content: generateFactoryContent({ fullyQualifiedModelName, columnsWithTypes, modelClassName }),
53
+ logLabel: 'factory',
54
+ },
55
+ ];
36
56
  if (options.serializer)
37
- await generateSerializer({
38
- fullyQualifiedModelName,
39
- columnsWithTypes,
40
- fullyQualifiedParentName,
41
- stiBaseSerializer: options.stiBaseSerializer,
42
- includeAdminSerializers: options.includeAdminSerializers,
43
- includeInternalSerializers: options.includeInternalSerializers ?? false,
44
- modelClassName,
57
+ files.push({
58
+ dreamPathKey: 'serializers',
59
+ fileName: `${fullyQualifiedModelName}Serializer.ts`,
60
+ content: generateSerializerContent({
61
+ fullyQualifiedModelName,
62
+ columnsWithTypes,
63
+ fullyQualifiedParentName,
64
+ stiBaseSerializer: options.stiBaseSerializer,
65
+ includeAdminSerializers: options.includeAdminSerializers,
66
+ includeInternalSerializers: options.includeInternalSerializers ?? false,
67
+ modelClassName,
68
+ }),
69
+ logLabel: 'serializer',
45
70
  });
46
- if (columnsWithTypes.length || !isSTI) {
47
- await generateMigration({
71
+ const migration = columnsWithTypes.length || !isSTI
72
+ ? buildMigration({
48
73
  connectionName: options.connectionName,
49
74
  migrationName: `Create${fullyQualifiedModelName}`,
50
75
  columnsWithTypes,
@@ -53,6 +78,72 @@ export default async function generateDream({ fullyQualifiedModelName, columnsWi
53
78
  tableName: options.tableName,
54
79
  modelClassName,
55
80
  softDelete,
81
+ })
82
+ : undefined;
83
+ const existingFilesMode = options.existingFiles ?? 'refuse';
84
+ const targets = await Promise.all(files.map(async (file) => {
85
+ const { relFilePath, absFilePath } = dreamFileAndDirPaths(dreamPath(file.dreamPathKey), file.fileName);
86
+ return { file, relFilePath, exists: await fileExists(absFilePath) };
87
+ }));
88
+ const existingTargets = targets.filter(target => target.exists);
89
+ const existingMigrations = migration ? await migrationsWithStem(migration) : [];
90
+ if (existingFilesMode === 'refuse' && (existingTargets.length || existingMigrations.length))
91
+ throw new GeneratedFilesAlreadyExist(fullyQualifiedModelName, existingTargets.map(target => target.relFilePath), existingMigrations, options.existingFilesFlags);
92
+ for (const target of targets) {
93
+ if (target.exists && existingFilesMode === 'skip') {
94
+ DreamCLI.logger.log(`[dream] kept existing ${target.file.logLabel}: ${target.relFilePath}`);
95
+ }
96
+ else {
97
+ await writeGeneratedFile({ ...target.file, overwrite: existingFilesMode === 'overwrite' });
98
+ }
99
+ }
100
+ const existingModel = existingTargets.find(target => target.file === modelFile);
101
+ if (existingMigrations.length) {
102
+ for (const existingMigration of existingMigrations)
103
+ DreamCLI.logger.log(`[dream] kept existing migration: ${existingMigration} (no new migration written; edit it by hand if the columns changed)`);
104
+ }
105
+ else if (migration && existingFilesMode === 'skip' && existingModel) {
106
+ DreamCLI.logger.log(`[dream] skipped migration: ${existingModel.relFilePath} already exists, so no migration was written (its schema may come from a migration with another name); generate one with g:migration if the schema needs to change`);
107
+ }
108
+ else if (migration) {
109
+ await writeGeneratedFile({
110
+ basePath: migration.basePath,
111
+ fileName: migration.fileName,
112
+ content: migration.content,
113
+ logLabel: 'migration',
56
114
  });
57
115
  }
58
116
  }
117
+ async function fileExists(absFilePath) {
118
+ try {
119
+ await fs.lstat(absFilePath);
120
+ return true;
121
+ }
122
+ catch (error) {
123
+ if (error.code === 'ENOENT')
124
+ return false;
125
+ throw error;
126
+ }
127
+ }
128
+ /**
129
+ * The project-relative paths of the migrations in `migration`'s folder whose
130
+ * file name differs from its own only in the version prefix. This reads the
131
+ * folder, never the database: the generator cannot tell whether such a
132
+ * migration has run, and a second one duplicates it either way.
133
+ */
134
+ async function migrationsWithStem(migration) {
135
+ const suffix = `-${migration.fileStem}.ts`;
136
+ let fileNames;
137
+ try {
138
+ fileNames = await fs.readdir(path.join(DreamApp.getOrFail().projectRoot, migration.basePath));
139
+ }
140
+ catch (error) {
141
+ if (error.code === 'ENOENT')
142
+ return [];
143
+ throw error;
144
+ }
145
+ return fileNames
146
+ .filter(fileName => fileName.endsWith(suffix) && /^\d+$/.test(fileName.slice(0, -suffix.length)))
147
+ .sort()
148
+ .map(fileName => path.join(migration.basePath, fileName));
149
+ }
@@ -46,11 +46,16 @@ function validTableNameOrThrow(migrationName, table) {
46
46
  throw new InvalidMigrationTableName(migrationName, table);
47
47
  return table;
48
48
  }
49
- export default async function generateMigration({ migrationName, columnsWithTypes, connectionName, fullyQualifiedModelName, fullyQualifiedParentName, tableName: explicitTableName, modelClassName, softDelete = false, }) {
49
+ /**
50
+ * Builds and validates a migration without writing it, so a caller writing
51
+ * several files can build them all before writing any.
52
+ */
53
+ export function buildMigration({ migrationName, columnsWithTypes, connectionName, fullyQualifiedModelName, fullyQualifiedParentName, tableName: explicitTableName, modelClassName, softDelete = false, }) {
50
54
  const migrationsBasePath = connectionName === 'default'
51
55
  ? path.join(dreamPath('db'), 'migrations')
52
56
  : path.join(dreamPath('db'), 'migrations', connectionName);
53
- const fileName = `${migrationVersion()}-${hyphenize(migrationName).replace(/\//g, '-')}.ts`;
57
+ const fileStem = hyphenize(migrationName).replace(/\//g, '-');
58
+ const fileName = `${migrationVersion()}-${fileStem}.ts`;
54
59
  const isSTI = !!fullyQualifiedParentName;
55
60
  let content;
56
61
  if (isSTI) {
@@ -88,10 +93,9 @@ export default async function generateMigration({ migrationName, columnsWithType
88
93
  alterMarker: toTableName ? '-to-' : fromTableName ? '-from-' : undefined,
89
94
  });
90
95
  }
91
- await writeGeneratedFile({
92
- basePath: migrationsBasePath,
93
- fileName,
94
- content,
95
- logLabel: 'migration',
96
- });
96
+ return { basePath: migrationsBasePath, fileName, fileStem, content };
97
+ }
98
+ export default async function generateMigration(args) {
99
+ const { basePath, fileName, content } = buildMigration(args);
100
+ await writeGeneratedFile({ basePath, fileName, content, logLabel: 'migration' });
97
101
  }
@@ -6,20 +6,20 @@ import dreamPath from '../path/dreamPath.js';
6
6
  * Common file-writing helper used by all Dream generators.
7
7
  * Handles directory creation, file writing, logging, and error wrapping.
8
8
  */
9
- export default async function writeGeneratedFile({ dreamPathKey, basePath, fileName, content, logLabel, }) {
9
+ export default async function writeGeneratedFile({ dreamPathKey, basePath, fileName, content, logLabel, overwrite = false, }) {
10
10
  const resolvedBasePath = basePath ?? dreamPath(dreamPathKey);
11
11
  const { relFilePath, absDirPath, absFilePath } = dreamFileAndDirPaths(resolvedBasePath, fileName);
12
12
  try {
13
13
  DreamCLI.logger.log(`[dream] generating ${logLabel}: ${relFilePath}`);
14
14
  await fs.mkdir(absDirPath, { recursive: true });
15
- await fs.writeFile(absFilePath, content);
15
+ await fs.writeFile(absFilePath, content, { flag: overwrite ? 'w' : 'wx' });
16
16
  }
17
17
  catch (error) {
18
18
  throw new Error(`
19
19
  Something happened while trying to create the ${logLabel} file:
20
20
  ${relFilePath}
21
21
 
22
- Does this file already exist? Here is the error that was raised:
22
+ Here is the error that was raised:
23
23
  ${error.message}
24
24
  `, { cause: error });
25
25
  }