@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.
- package/dist/cjs/src/Dream.js +155 -13
- package/dist/cjs/src/cli/index.js +39 -5
- package/dist/cjs/src/db/DreamDbConnection.js +19 -1
- package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +34 -2
- package/dist/cjs/src/dream/Query.js +178 -14
- package/dist/cjs/src/dream/QueryDriver/Base.js +80 -0
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +35 -29
- package/dist/cjs/src/dream/QueryDriver/Postgres.js +83 -1
- package/dist/cjs/src/dream/internal/findOrCreateBy.js +6 -1
- package/dist/cjs/src/dream/internal/updateOrCreateBy.js +6 -1
- package/dist/cjs/src/encrypt/algorithms/aes-gcm/decryptAESGCM.js +24 -10
- package/dist/cjs/src/errors/CreateOrFindByFailedToCreateAndFind.js +63 -6
- package/dist/cjs/src/errors/CreateOrUpdateByFailedToCreateAndUpdate.js +41 -6
- package/dist/cjs/src/errors/GeneratedFilesAlreadyExist.js +38 -0
- package/dist/cjs/src/errors/schema-builder/ColumnNamesDoNotRoundTrip.js +28 -0
- package/dist/cjs/src/helpers/cli/ASTConnectionBuilder.js +62 -10
- package/dist/cjs/src/helpers/cli/generateDream.js +109 -18
- package/dist/cjs/src/helpers/cli/generateMigration.js +12 -8
- package/dist/cjs/src/helpers/cli/writeGeneratedFile.js +3 -3
- package/dist/cjs/src/helpers/db/normalizeDataForDb.js +17 -2
- package/dist/esm/src/Dream.js +155 -13
- package/dist/esm/src/cli/index.js +39 -5
- package/dist/esm/src/db/DreamDbConnection.js +19 -1
- package/dist/esm/src/dream/DreamClassTransactionBuilder.js +34 -2
- package/dist/esm/src/dream/Query.js +178 -14
- package/dist/esm/src/dream/QueryDriver/Base.js +80 -0
- package/dist/esm/src/dream/QueryDriver/Kysely.js +35 -29
- package/dist/esm/src/dream/QueryDriver/Postgres.js +83 -1
- package/dist/esm/src/dream/internal/findOrCreateBy.js +6 -1
- package/dist/esm/src/dream/internal/updateOrCreateBy.js +6 -1
- package/dist/esm/src/encrypt/algorithms/aes-gcm/decryptAESGCM.js +24 -10
- package/dist/esm/src/errors/CreateOrFindByFailedToCreateAndFind.js +63 -6
- package/dist/esm/src/errors/CreateOrUpdateByFailedToCreateAndUpdate.js +41 -6
- package/dist/esm/src/errors/GeneratedFilesAlreadyExist.js +38 -0
- package/dist/esm/src/errors/schema-builder/ColumnNamesDoNotRoundTrip.js +28 -0
- package/dist/esm/src/helpers/cli/ASTConnectionBuilder.js +62 -10
- package/dist/esm/src/helpers/cli/generateDream.js +109 -18
- package/dist/esm/src/helpers/cli/generateMigration.js +12 -8
- package/dist/esm/src/helpers/cli/writeGeneratedFile.js +3 -3
- package/dist/esm/src/helpers/db/normalizeDataForDb.js +17 -2
- package/dist/types/src/Dream.d.ts +109 -6
- package/dist/types/src/bin/index.d.ts +11 -0
- package/dist/types/src/cli/index.d.ts +27 -0
- package/dist/types/src/db/DreamDbConnection.d.ts +15 -1
- package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +34 -2
- package/dist/types/src/dream/Query.d.ts +101 -6
- package/dist/types/src/dream/QueryDriver/Base.d.ts +59 -0
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +9 -0
- package/dist/types/src/dream/QueryDriver/Postgres.d.ts +29 -1
- package/dist/types/src/dream/internal/executeDatabaseQuery.d.ts +2 -1
- package/dist/types/src/errors/CreateOrFindByFailedToCreateAndFind.d.ts +40 -1
- package/dist/types/src/errors/CreateOrUpdateByFailedToCreateAndUpdate.d.ts +8 -1
- package/dist/types/src/errors/GeneratedFilesAlreadyExist.d.ts +18 -0
- package/dist/types/src/errors/schema-builder/ColumnNamesDoNotRoundTrip.d.ts +12 -0
- package/dist/types/src/helpers/cli/ASTConnectionBuilder.d.ts +23 -1
- package/dist/types/src/helpers/cli/generateDream.d.ts +25 -0
- package/dist/types/src/helpers/cli/generateMigration.d.ts +20 -2
- package/dist/types/src/helpers/cli/syncWithFailureContext.d.ts +7 -2
- package/dist/types/src/helpers/cli/writeGeneratedFile.d.ts +6 -1
- package/dist/types/src/helpers/db/normalizeDataForDb.d.ts +7 -0
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +87 -50
- package/docs/classes/db.PostgresQueryDriver.html +76 -50
- package/docs/classes/db.QueryDriverBase.html +87 -50
- package/docs/classes/errors.CannotSaveMissingDream.html +2 -2
- package/docs/classes/errors.CheckConstraintViolation.html +3 -3
- package/docs/classes/errors.ColumnOverflow.html +3 -3
- package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +6 -3
- package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
- package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
- package/docs/classes/errors.DecryptionError.html +2 -2
- package/docs/classes/errors.DecryptionParseError.html +2 -2
- package/docs/classes/errors.DecryptionRotationError.html +3 -3
- package/docs/classes/errors.GlobalNameNotSet.html +3 -3
- package/docs/classes/errors.InvalidCalendarDate.html +2 -2
- package/docs/classes/errors.InvalidClockTime.html +2 -2
- package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
- package/docs/classes/errors.InvalidDateTime.html +2 -2
- package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
- package/docs/classes/errors.NonLoadedAssociation.html +3 -3
- package/docs/classes/errors.NotNullViolation.html +3 -3
- package/docs/classes/errors.RecordNotFound.html +3 -3
- package/docs/classes/errors.SortableScopeDidNotStabilize.html +3 -3
- package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +3 -3
- package/docs/classes/errors.ValidationError.html +3 -3
- package/docs/classes/index.CalendarDate.html +33 -33
- package/docs/classes/index.ClockTime.html +32 -32
- package/docs/classes/index.ClockTimeTz.html +35 -35
- package/docs/classes/index.DateTime.html +86 -86
- package/docs/classes/index.Decorators.html +19 -19
- package/docs/classes/index.Dream.html +203 -133
- package/docs/classes/index.DreamApp.html +13 -13
- package/docs/classes/index.DreamTransaction.html +3 -3
- package/docs/classes/index.Env.html +5 -5
- package/docs/classes/index.Query.html +147 -100
- package/docs/classes/system.CliFileWriter.html +4 -4
- package/docs/classes/system.DreamBin.html +7 -3
- package/docs/classes/system.DreamCLI.html +7 -7
- package/docs/classes/system.DreamImporter.html +2 -2
- package/docs/classes/system.DreamLogos.html +2 -2
- package/docs/classes/system.DreamSerializerBuilder.html +11 -11
- package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
- package/docs/classes/system.PathHelpers.html +3 -3
- package/docs/classes/utils.Encrypt.html +4 -4
- package/docs/classes/utils.Range.html +2 -2
- package/docs/functions/db.closeAllDbConnections.html +1 -1
- package/docs/functions/db.dreamDbConnections.html +1 -1
- package/docs/functions/db.untypedDb.html +1 -1
- package/docs/functions/db.validateColumn.html +1 -1
- package/docs/functions/db.validateTable.html +1 -1
- package/docs/functions/errors.pgErrorType.html +1 -1
- package/docs/functions/index.DreamSerializer.html +1 -1
- package/docs/functions/index.ObjectSerializer.html +1 -1
- package/docs/functions/index.ReplicaSafe.html +1 -1
- package/docs/functions/index.STI.html +1 -1
- package/docs/functions/index.SoftDelete.html +1 -1
- package/docs/functions/utils.camelize.html +1 -1
- package/docs/functions/utils.capitalize.html +1 -1
- package/docs/functions/utils.cloneDeepSafe.html +1 -1
- package/docs/functions/utils.compact.html +1 -1
- package/docs/functions/utils.groupBy.html +1 -1
- package/docs/functions/utils.hyphenize.html +1 -1
- package/docs/functions/utils.intersection.html +1 -1
- package/docs/functions/utils.isEmpty.html +3 -3
- package/docs/functions/utils.normalizeUnicode.html +1 -1
- package/docs/functions/utils.pascalize.html +1 -1
- package/docs/functions/utils.percent.html +1 -1
- package/docs/functions/utils.range.html +1 -1
- package/docs/functions/utils.round.html +1 -1
- package/docs/functions/utils.sanitizeString.html +1 -1
- package/docs/functions/utils.snakeify.html +1 -1
- package/docs/functions/utils.sort.html +1 -1
- package/docs/functions/utils.sortBy.html +1 -1
- package/docs/functions/utils.sortObjectByKey.html +1 -1
- package/docs/functions/utils.sortObjectByValue.html +1 -1
- package/docs/functions/utils.uncapitalize.html +1 -1
- package/docs/functions/utils.uniq.html +1 -1
- package/docs/hierarchy.html +1 -1
- package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
- package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
- package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
- package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
- package/docs/interfaces/types.BelongsToStatement.html +2 -2
- package/docs/interfaces/types.DecoratorContext.html +2 -2
- package/docs/interfaces/types.DecryptCallbacks.html +2 -2
- package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
- package/docs/interfaces/types.DreamAppOpts.html +2 -2
- package/docs/interfaces/types.DreamDbConfig.html +6 -6
- package/docs/interfaces/types.DurationObject.html +2 -2
- package/docs/interfaces/types.EncryptOptions.html +2 -2
- package/docs/interfaces/types.EncryptionLegacyKeyUsedEvent.html +4 -4
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
- package/docs/interfaces/types.IntrospectedColumn.html +7 -7
- package/docs/interfaces/types.IntrospectedDatabase.html +2 -2
- package/docs/interfaces/types.IntrospectedEnum.html +2 -2
- package/docs/interfaces/types.IntrospectedTable.html +5 -5
- package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
- package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
- package/docs/types/openapi.OpenapiAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiFormats.html +1 -1
- package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaCombinatorFields.html +1 -1
- package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
- package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
- package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaString.html +1 -1
- package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiTypeField.html +1 -1
- package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
- package/docs/types/types.CalendarDateDurationUnit.html +1 -1
- package/docs/types/types.CalendarDateObject.html +1 -1
- package/docs/types/types.Camelized.html +1 -1
- package/docs/types/types.ClockTimeObject.html +1 -1
- package/docs/types/types.DbConnectionType.html +1 -1
- package/docs/types/types.DbTypes.html +1 -1
- package/docs/types/types.DreamAssociationMetadata.html +1 -1
- package/docs/types/types.DreamAttributes.html +1 -1
- package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
- package/docs/types/types.DreamClassColumn.html +1 -1
- package/docs/types/types.DreamColumn.html +1 -1
- package/docs/types/types.DreamColumnNames.html +1 -1
- package/docs/types/types.DreamLogLevel.html +1 -1
- package/docs/types/types.DreamLogger.html +2 -2
- package/docs/types/types.DreamModelSerializerType.html +1 -1
- package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
- package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
- package/docs/types/types.DreamParamSafeAttributes.html +1 -1
- package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
- package/docs/types/types.DreamSerializable.html +1 -1
- package/docs/types/types.DreamSerializableArray.html +1 -1
- package/docs/types/types.DreamSerializerKey.html +1 -1
- package/docs/types/types.DreamSerializers.html +1 -1
- package/docs/types/types.DreamVirtualColumns.html +1 -1
- package/docs/types/types.DurationUnit.html +1 -1
- package/docs/types/types.EncryptAlgorithm.html +1 -1
- package/docs/types/types.HasManyStatement.html +1 -1
- package/docs/types/types.HasOneStatement.html +1 -1
- package/docs/types/types.Hyphenized.html +1 -1
- package/docs/types/types.IntrospectedValueType.html +1 -1
- package/docs/types/types.Pascalized.html +1 -1
- package/docs/types/types.PrimaryKeyType.html +1 -1
- package/docs/types/types.RoundingPrecision.html +1 -1
- package/docs/types/types.SerializerCasing.html +1 -1
- package/docs/types/types.SimpleObjectSerializerType.html +1 -1
- package/docs/types/types.Snakeified.html +1 -1
- package/docs/types/types.StrictInterface.html +1 -1
- package/docs/types/types.UpdateableAssociationProperties.html +1 -1
- package/docs/types/types.UpdateableProperties.html +1 -1
- package/docs/types/types.ValidationType.html +1 -1
- package/docs/types/types.ViewModel.html +2 -2
- package/docs/types/types.ViewModelClass.html +1 -1
- package/docs/types/types.WeekdayName.html +1 -1
- package/docs/types/types.WhereStatementForDream.html +1 -1
- package/docs/types/types.WhereStatementForDreamClass.html +1 -1
- package/docs/variables/index.DreamConst.html +1 -1
- package/docs/variables/index.ops.html +1 -1
- package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
- package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
- package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
- package/docs/variables/system.primaryKeyTypes.html +1 -1
- package/package.json +1 -1
- package/dist/cjs/src/helpers/cli/generateFactory.js +0 -10
- package/dist/cjs/src/helpers/cli/generateSerializer.js +0 -18
- package/dist/cjs/src/helpers/cli/generateUnitSpec.js +0 -10
- package/dist/esm/src/helpers/cli/generateFactory.js +0 -10
- package/dist/esm/src/helpers/cli/generateSerializer.js +0 -18
- package/dist/esm/src/helpers/cli/generateUnitSpec.js +0 -10
- package/dist/types/src/helpers/cli/generateFactory.d.ts +0 -5
- package/dist/types/src/helpers/cli/generateSerializer.d.ts +0 -9
- package/dist/types/src/helpers/cli/generateUnitSpec.d.ts +0 -3
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import findOrCreateLookupStatement from './findOrCreateLookupStatement.js';
|
|
2
2
|
export default async function findOrCreateBy(dreamClass, txn = null, attributes, extraOpts = {}) {
|
|
3
|
-
|
|
3
|
+
// where().first() rather than findBy: a value the database rejects is raised, never read as no match,
|
|
4
|
+
// so it cannot lead to creating a record that already exists
|
|
5
|
+
const existingRecord = await dreamClass
|
|
6
|
+
.txn(txn)
|
|
7
|
+
.where(findOrCreateLookupStatement(dreamClass, attributes))
|
|
8
|
+
.first();
|
|
4
9
|
if (existingRecord)
|
|
5
10
|
return existingRecord;
|
|
6
11
|
const { createWith, skipHooks } = extraOpts;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import findOrCreateLookupStatement from './findOrCreateLookupStatement.js';
|
|
2
2
|
import saveDream from './saveDream.js';
|
|
3
3
|
export default async function updateOrCreateBy(dreamClass, txn = null, attributes, extraOpts = {}) {
|
|
4
|
-
|
|
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
|
|
6
|
+
const existingRecord = await dreamClass
|
|
7
|
+
.txn(txn)
|
|
8
|
+
.where(findOrCreateLookupStatement(dreamClass, attributes))
|
|
9
|
+
.first();
|
|
5
10
|
const { with: attrs, skipHooks } = extraOpts;
|
|
6
11
|
if (existingRecord) {
|
|
7
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
12
|
-
a
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
|
12
|
-
a
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
22
|
+
Here is the error that was raised:
|
|
23
23
|
${error.message}
|
|
24
24
|
`, { cause: error });
|
|
25
25
|
}
|
|
@@ -7,6 +7,21 @@ import ClockTimeTz from '../../utils/datetime/ClockTimeTz.js';
|
|
|
7
7
|
import { DateTime } from '../../utils/datetime/DateTime.js';
|
|
8
8
|
import normalizeUnicode from '../normalizeUnicode.js';
|
|
9
9
|
import { DATE_OR_DATE_ARRAY_COLUMN_CHECK_REGEXP, DATETIME_OR_DATETIME_ARRAY_COLUMN_CHECK_REGEXP, STRING_OR_STRING_ARRAY_COLUMN_CHECK_REGEXP, TIME_WITH_TIMEZONE_COLUMN_CHECK_REGEXP, TIME_WITHOUT_TIMEZONE_COLUMN_CHECK_REGEXP, } from './types/helpers.js';
|
|
10
|
+
const mismatchesRaisedBeforeQuerying = new WeakSet();
|
|
11
|
+
/**
|
|
12
|
+
* Whether `error` is the `DataTypeColumnTypeMismatch` raised here for a date,
|
|
13
|
+
* datetime or time value Dream cannot convert for its column. Values are
|
|
14
|
+
* converted while a statement is built, so such an error means the statement
|
|
15
|
+
* was never sent to the database.
|
|
16
|
+
*/
|
|
17
|
+
export function isMismatchRaisedBeforeQuerying(error) {
|
|
18
|
+
return error instanceof Error && mismatchesRaisedBeforeQuerying.has(error);
|
|
19
|
+
}
|
|
20
|
+
function mismatchRaisedBeforeQuerying(error) {
|
|
21
|
+
const mismatch = new DataTypeColumnTypeMismatch(error);
|
|
22
|
+
mismatchesRaisedBeforeQuerying.add(mismatch);
|
|
23
|
+
return mismatch;
|
|
24
|
+
}
|
|
10
25
|
export function normalizeDataForDb({ val, dreamClass, column, }) {
|
|
11
26
|
const columnType = cachedTypeForAttribute(dreamClass, column);
|
|
12
27
|
if (val instanceof DateTime || DATETIME_OR_DATETIME_ARRAY_COLUMN_CHECK_REGEXP.test(columnType)) {
|
|
@@ -59,7 +74,7 @@ function dateTimeOrDateToSql(dateClass, _val) {
|
|
|
59
74
|
return dateClass.fromISO(val, { zone: 'UTC' }).toSQL();
|
|
60
75
|
}
|
|
61
76
|
catch (error) {
|
|
62
|
-
throw
|
|
77
|
+
throw mismatchRaisedBeforeQuerying(error instanceof Error
|
|
63
78
|
? error
|
|
64
79
|
: new Error(`database column type error: invalid ${dateClass.name} (received ${typeof val}: ${val})`));
|
|
65
80
|
}
|
|
@@ -87,7 +102,7 @@ function clockTimeOrClockTimeTzToSql(clockTimeClass, _val) {
|
|
|
87
102
|
return clockTimeClass.fromISO(val, { zone: 'UTC' }).toSQL();
|
|
88
103
|
}
|
|
89
104
|
catch (error) {
|
|
90
|
-
throw
|
|
105
|
+
throw mismatchRaisedBeforeQuerying(error instanceof Error
|
|
91
106
|
? error
|
|
92
107
|
: new Error(`database column type error: invalid ${clockTimeClass.name} (received ${typeof val}: ${val})`));
|
|
93
108
|
}
|