@rvoh/dream 2.34.0 → 2.36.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/README.md +2 -2
- package/dist/cjs/src/Dream.js +270 -69
- package/dist/cjs/src/bin/index.js +13 -0
- package/dist/cjs/src/cli/index.js +28 -17
- package/dist/cjs/src/db/migration-helpers/DreamMigrationHelpers.js +23 -21
- package/dist/cjs/src/decorators/Decorators.js +26 -4
- package/dist/cjs/src/decorators/field/Encrypted.js +30 -6
- package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +6 -6
- package/dist/cjs/src/dream/DreamInstanceTransactionBuilder.js +63 -27
- package/dist/cjs/src/dream/LeftJoinLoadBuilder.js +3 -3
- package/dist/cjs/src/dream/LoadBuilder.js +1 -1
- package/dist/cjs/src/dream/Query.js +542 -167
- package/dist/cjs/src/dream/QueryDriver/Base.js +72 -0
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +216 -50
- package/dist/cjs/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.js +48 -0
- package/dist/cjs/src/dream/internal/associations/associationQuery.js +4 -0
- package/dist/cjs/src/dream/internal/reload.js +6 -1
- package/dist/cjs/src/dream/internal/saveDream.js +8 -2
- package/dist/cjs/src/dream/internal/sqlResultToDreamInstance.js +2 -0
- package/dist/cjs/src/dream/internal/undestroyDream.js +1 -1
- package/dist/cjs/src/dream-app/index.js +38 -0
- package/dist/cjs/src/encrypt/InternalEncrypt.js +49 -4
- package/dist/cjs/src/encrypt/defaultEncryptedColumnName.js +10 -0
- package/dist/cjs/src/encrypt/index.js +76 -9
- package/dist/cjs/src/errors/DoNotSetEncryptedFieldsDirectly.js +4 -0
- package/dist/cjs/src/errors/associations/UnrecognizedAssociationConditionKeys.js +25 -0
- package/dist/cjs/src/helpers/Env.js +28 -0
- package/dist/cjs/src/helpers/cli/generateDreamContent.js +2 -1
- package/dist/cjs/src/helpers/cli/generateFactoryContent.js +1 -1
- package/dist/cjs/src/helpers/cli/generateMigrationContent.js +9 -9
- package/dist/cjs/src/helpers/cli/generateSerializerContent.js +21 -8
- package/dist/cjs/src/serializer/SerializerRenderer.js +65 -13
- package/dist/cjs/src/serializer/builders/DreamSerializerBuilder.js +54 -38
- package/dist/cjs/src/serializer/builders/ObjectSerializerBuilder.js +34 -14
- package/dist/cjs/src/serializer/helpers/serializersForAssociatedClass.js +43 -0
- package/dist/esm/src/Dream.js +270 -69
- package/dist/esm/src/bin/index.js +13 -0
- package/dist/esm/src/cli/index.js +28 -17
- package/dist/esm/src/db/migration-helpers/DreamMigrationHelpers.js +23 -21
- package/dist/esm/src/decorators/Decorators.js +26 -4
- package/dist/esm/src/decorators/field/Encrypted.js +30 -6
- package/dist/esm/src/dream/DreamClassTransactionBuilder.js +6 -6
- package/dist/esm/src/dream/DreamInstanceTransactionBuilder.js +63 -27
- package/dist/esm/src/dream/LeftJoinLoadBuilder.js +3 -3
- package/dist/esm/src/dream/LoadBuilder.js +1 -1
- package/dist/esm/src/dream/Query.js +542 -167
- package/dist/esm/src/dream/QueryDriver/Base.js +72 -0
- package/dist/esm/src/dream/QueryDriver/Kysely.js +216 -50
- package/dist/esm/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.js +48 -0
- package/dist/esm/src/dream/internal/associations/associationQuery.js +4 -0
- package/dist/esm/src/dream/internal/reload.js +6 -1
- package/dist/esm/src/dream/internal/saveDream.js +8 -2
- package/dist/esm/src/dream/internal/sqlResultToDreamInstance.js +2 -0
- package/dist/esm/src/dream/internal/undestroyDream.js +1 -1
- package/dist/esm/src/dream-app/index.js +38 -0
- package/dist/esm/src/encrypt/InternalEncrypt.js +49 -4
- package/dist/esm/src/encrypt/defaultEncryptedColumnName.js +10 -0
- package/dist/esm/src/encrypt/index.js +76 -9
- package/dist/esm/src/errors/DoNotSetEncryptedFieldsDirectly.js +4 -0
- package/dist/esm/src/errors/associations/UnrecognizedAssociationConditionKeys.js +25 -0
- package/dist/esm/src/helpers/Env.js +28 -0
- package/dist/esm/src/helpers/cli/generateDreamContent.js +2 -1
- package/dist/esm/src/helpers/cli/generateFactoryContent.js +1 -1
- package/dist/esm/src/helpers/cli/generateMigrationContent.js +9 -9
- package/dist/esm/src/helpers/cli/generateSerializerContent.js +21 -8
- package/dist/esm/src/serializer/SerializerRenderer.js +65 -13
- package/dist/esm/src/serializer/builders/DreamSerializerBuilder.js +54 -38
- package/dist/esm/src/serializer/builders/ObjectSerializerBuilder.js +34 -14
- package/dist/esm/src/serializer/helpers/serializersForAssociatedClass.js +43 -0
- package/dist/types/src/Dream.d.ts +217 -55
- package/dist/types/src/bin/index.d.ts +7 -1
- package/dist/types/src/cli/index.d.ts +13 -0
- package/dist/types/src/db/migration-helpers/DreamMigrationHelpers.d.ts +2 -2
- package/dist/types/src/decorators/Decorators.d.ts +26 -3
- package/dist/types/src/decorators/field/Encrypted.d.ts +6 -1
- package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +9 -9
- package/dist/types/src/dream/DreamInstanceTransactionBuilder.d.ts +52 -25
- package/dist/types/src/dream/LeftJoinLoadBuilder.d.ts +4 -4
- package/dist/types/src/dream/LoadBuilder.d.ts +1 -1
- package/dist/types/src/dream/Query.d.ts +272 -33
- package/dist/types/src/dream/QueryDriver/Base.d.ts +63 -1
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +85 -1
- package/dist/types/src/dream/internal/associations/assertRecognizedAssociationConditionKeys.d.ts +29 -0
- package/dist/types/src/dream-app/index.d.ts +56 -2
- package/dist/types/src/encrypt/InternalEncrypt.d.ts +20 -1
- package/dist/types/src/encrypt/defaultEncryptedColumnName.d.ts +7 -0
- package/dist/types/src/encrypt/index.d.ts +75 -6
- package/dist/types/src/errors/associations/UnrecognizedAssociationConditionKeys.d.ts +8 -0
- package/dist/types/src/helpers/Env.d.ts +28 -0
- package/dist/types/src/package-exports/openapi.d.ts +1 -1
- package/dist/types/src/package-exports/types.d.ts +2 -2
- package/dist/types/src/serializer/SerializerRenderer.d.ts +13 -0
- package/dist/types/src/serializer/builders/DreamSerializerBuilder.d.ts +59 -55
- package/dist/types/src/serializer/builders/ObjectSerializerBuilder.d.ts +40 -19
- package/dist/types/src/serializer/helpers/serializersForAssociatedClass.d.ts +17 -0
- package/dist/types/src/types/lifecycle.d.ts +6 -0
- package/dist/types/src/types/openapi.d.ts +52 -29
- package/dist/types/src/types/serializer.d.ts +43 -2
- package/dist/types/src/types/types/lifecycle.ts +6 -0
- package/dist/types/src/types/types/openapi.ts +51 -37
- package/dist/types/src/types/types/serializer.ts +62 -2
- package/dist/types/src/types/types/variadic.ts +99 -43
- package/dist/types/src/types/variadic.d.ts +29 -3
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/highlight.css +4 -11
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +13 -13
- package/docs/classes/db.KyselyQueryDriver.html +85 -46
- package/docs/classes/db.PostgresQueryDriver.html +86 -47
- package/docs/classes/db.QueryDriverBase.html +84 -45
- 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 +3 -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 +37 -23
- package/docs/classes/index.Dream.html +281 -155
- package/docs/classes/index.DreamApp.html +40 -10
- package/docs/classes/index.DreamTransaction.html +3 -3
- package/docs/classes/index.Env.html +16 -2
- package/docs/classes/index.Query.html +331 -129
- package/docs/classes/system.CliFileWriter.html +4 -4
- package/docs/classes/system.DreamBin.html +6 -2
- 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 +52 -34
- package/docs/classes/system.ObjectSerializerBuilder.html +41 -22
- package/docs/classes/system.PathHelpers.html +4 -4
- package/docs/classes/utils.Encrypt.html +54 -10
- 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 +1 -1
- 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/index.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 +6 -0
- 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 +10 -0
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
- package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
- package/docs/modules/openapi.html +1 -1
- package/docs/modules/types.html +1 -1
- 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 +4 -0
- package/docs/types/openapi.OpenapiSchemaCommonFields.html +5 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +1 -2
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +1 -2
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +1 -2
- 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 +4 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +5 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +2 -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 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +1 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +1 -2
- 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.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/serializer/helpers/serializerForAssociatedClass.js +0 -37
- package/dist/esm/src/serializer/helpers/serializerForAssociatedClass.js +0 -37
- package/dist/types/src/serializer/helpers/serializerForAssociatedClass.d.ts +0 -9
- package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +0 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +0 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +0 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +0 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import pascalize from '../helpers/pascalize.js';
|
|
2
|
+
/**
|
|
3
|
+
* The column that holds an `@deco.Encrypted` property's ciphertext when the
|
|
4
|
+
* decorator is not given one: the property's name, pascalized and prefixed
|
|
5
|
+
* with `encrypted`. `secretPhone`, or `secret_phone`, gives
|
|
6
|
+
* `encryptedSecretPhone`, which is the `encrypted_secret_phone` column.
|
|
7
|
+
*/
|
|
8
|
+
export default function defaultEncryptedColumnName(propertyName) {
|
|
9
|
+
return `encrypted${pascalize(propertyName)}`;
|
|
10
|
+
}
|
|
@@ -41,14 +41,30 @@ export default class Encrypt {
|
|
|
41
41
|
*
|
|
42
42
|
* `MissingEncryptionKey` propagates from either form when a key is missing.
|
|
43
43
|
*
|
|
44
|
+
* **`onLegacyKeyUsed`** (optional fourth argument): during a key rotation,
|
|
45
|
+
* tells you which values still need the legacy key. It is called with no
|
|
46
|
+
* arguments, synchronously, before a three-arg call returns a value that
|
|
47
|
+
* the legacy key opened. It is not called when the current key opens the
|
|
48
|
+
* value, when decryption throws, or in the two-arg form. An error it
|
|
49
|
+
* throws propagates from `decrypt` as is.
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* const value = Encrypt.decrypt<string>(
|
|
53
|
+
* encrypted,
|
|
54
|
+
* { algorithm: 'aes-256-gcm', key: newKey },
|
|
55
|
+
* { algorithm: 'aes-256-gcm', key: oldKey },
|
|
56
|
+
* { onLegacyKeyUsed: () => console.warn('this value is still encrypted with the old key') }
|
|
57
|
+
* )
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
44
60
|
* @throws MissingEncryptionKey
|
|
45
61
|
* @throws DecryptionError
|
|
46
62
|
* @throws DecryptionParseError
|
|
47
63
|
* @throws DecryptionRotationError
|
|
48
64
|
*/
|
|
49
|
-
static decrypt(encrypted, { algorithm, key }, legacyOpts) {
|
|
65
|
+
static decrypt(encrypted, { algorithm, key }, legacyOpts, { onLegacyKeyUsed } = {}) {
|
|
50
66
|
if (legacyOpts)
|
|
51
|
-
return this.attemptDecryptionWithLegacyKeys(encrypted, { algorithm, key }, legacyOpts);
|
|
67
|
+
return this.attemptDecryptionWithLegacyKeys(encrypted, { algorithm, key }, legacyOpts, onLegacyKeyUsed);
|
|
52
68
|
if (!key)
|
|
53
69
|
throw new MissingEncryptionKey();
|
|
54
70
|
if ([null, undefined].includes(encrypted))
|
|
@@ -65,7 +81,31 @@ export default class Encrypt {
|
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Opens `encrypted` as {@link Encrypt.decrypt} does, with `currentOpts` and,
|
|
86
|
+
* when they do not open it, `legacyOpts`, and encrypts the value again with
|
|
87
|
+
* `currentOpts`, so that reading it no longer needs the legacy key. Use it
|
|
88
|
+
* during a key rotation for a value stored outside an `@deco.Encrypted`
|
|
89
|
+
* column, such as a cookie; `Model.reencryptEncryptedColumns()` does this
|
|
90
|
+
* for every value of a model's encrypted properties.
|
|
91
|
+
*
|
|
92
|
+
* ```ts
|
|
93
|
+
* const rewritten = Encrypt.reencrypt(
|
|
94
|
+
* storedValue,
|
|
95
|
+
* { algorithm: 'aes-256-gcm', key: newKey },
|
|
96
|
+
* { algorithm: 'aes-256-gcm', key: oldKey }
|
|
97
|
+
* )
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* @throws MissingEncryptionKey
|
|
101
|
+
* @throws DecryptionError
|
|
102
|
+
* @throws DecryptionParseError
|
|
103
|
+
* @throws DecryptionRotationError
|
|
104
|
+
*/
|
|
105
|
+
static reencrypt(encrypted, currentOpts, legacyOpts) {
|
|
106
|
+
return this.encrypt(this.decrypt(encrypted, currentOpts, legacyOpts), currentOpts);
|
|
107
|
+
}
|
|
108
|
+
static attemptDecryptionWithLegacyKeys(encrypted, currentOpts, legacyOpts, onLegacyKeyUsed) {
|
|
69
109
|
let currentKeyError;
|
|
70
110
|
try {
|
|
71
111
|
return this.decrypt(encrypted, currentOpts);
|
|
@@ -75,14 +115,19 @@ export default class Encrypt {
|
|
|
75
115
|
throw err;
|
|
76
116
|
currentKeyError = err;
|
|
77
117
|
}
|
|
118
|
+
let decrypted;
|
|
78
119
|
try {
|
|
79
|
-
|
|
120
|
+
decrypted = this.decrypt(encrypted, legacyOpts);
|
|
80
121
|
}
|
|
81
122
|
catch (err) {
|
|
82
123
|
if (!(err instanceof DecryptionError))
|
|
83
124
|
throw err;
|
|
84
125
|
throw new DecryptionRotationError(currentKeyError, err);
|
|
85
126
|
}
|
|
127
|
+
// called outside the try above, so an error the callback throws is never
|
|
128
|
+
// mistaken for the legacy key failing
|
|
129
|
+
onLegacyKeyUsed?.();
|
|
130
|
+
return decrypted;
|
|
86
131
|
}
|
|
87
132
|
/**
|
|
88
133
|
* Generates a base64-encoded random key suitable for the given algorithm.
|
|
@@ -100,11 +145,26 @@ export default class Encrypt {
|
|
|
100
145
|
* to decrypt via `legacy` fallback.
|
|
101
146
|
* 4. For cookies, wait at least the cookie `maxAge` so all in-flight
|
|
102
147
|
* cookies have either expired or been re-issued under the new key. For
|
|
103
|
-
* `@Encrypted` columns, re-encrypt every existing row under the new
|
|
104
|
-
*
|
|
105
|
-
* `
|
|
148
|
+
* `@Encrypted` columns, re-encrypt every existing row under the new key
|
|
149
|
+
* by calling `reencryptEncryptedColumns()` on each model that has
|
|
150
|
+
* encrypted properties (`await User.reencryptEncryptedColumns()`), in
|
|
151
|
+
* each environment, from a console or a script rather than a migration.
|
|
152
|
+
* Run it only once every server in that environment runs with the new
|
|
153
|
+
* `current`: a server still on the old `current` keeps writing old-key
|
|
154
|
+
* values that its single pass over the table can miss. Writing old-key
|
|
155
|
+
* ciphertext straight into a backing column (`setAttribute`,
|
|
156
|
+
* `setAttributes` or `updateAttributes` given the backing column) after
|
|
157
|
+
* it has run also puts an old-key value back. For any other value
|
|
158
|
+
* encrypted with these keys, {@link Encrypt.reencrypt} rewrites it.
|
|
106
159
|
* 5. Drop `legacy` from config and deploy again.
|
|
107
160
|
*
|
|
161
|
+
* To see which values still need the old key while `legacy` is
|
|
162
|
+
* configured, register `dreamApp.on('encryption:legacy-key-used', ...)`,
|
|
163
|
+
* which fires each time reading an `@Encrypted` property needed `legacy`
|
|
164
|
+
* to open the value, or pass `onLegacyKeyUsed` to {@link Encrypt.decrypt}
|
|
165
|
+
* for values you decrypt yourself. A value nobody reads never fires
|
|
166
|
+
* either, so silence alone does not show that `legacy` is safe to drop.
|
|
167
|
+
*
|
|
108
168
|
* ## When to rotate
|
|
109
169
|
*
|
|
110
170
|
* - On a scheduled cadence (90–180 days is a reasonable policy default).
|
|
@@ -116,8 +176,15 @@ export default class Encrypt {
|
|
|
116
176
|
* - For cookies: at least the cookie `maxAge`, so in-flight sessions are
|
|
117
177
|
* not forced to re-authenticate.
|
|
118
178
|
* - For `@Encrypted` columns: until every existing row has been
|
|
119
|
-
* re-encrypted under the new key.
|
|
120
|
-
* `
|
|
179
|
+
* re-encrypted under the new key. Once `legacy` is dropped, reading the
|
|
180
|
+
* `@Encrypted` property of a row that is still under the old key throws
|
|
181
|
+
* `DecryptionError`, the same error a tampered or corrupted value
|
|
182
|
+
* throws, so the error class alone cannot show that `legacy` was
|
|
183
|
+
* dropped too early. `DecryptionRotationError` is thrown only while
|
|
184
|
+
* `legacy` is configured and neither key opens the value. Neither class
|
|
185
|
+
* extends the other, so code that catches `DecryptionRotationError`
|
|
186
|
+
* during a rotation must also catch `DecryptionError` to handle such a
|
|
187
|
+
* read once `legacy` is dropped.
|
|
121
188
|
*/
|
|
122
189
|
static generateKey(algorithm) {
|
|
123
190
|
switch (algorithm) {
|
|
@@ -13,6 +13,10 @@ export default class DoNotSetEncryptedFieldsDirectly extends Error {
|
|
|
13
13
|
Do not set @Encrypted columns directly. Instead, set their accessors, so that
|
|
14
14
|
those fields can be encrypted by Dream internally.
|
|
15
15
|
|
|
16
|
+
Writes that bypass setters, such as setAttribute, setAttributes and
|
|
17
|
+
updateAttributes, accept a value for an @Encrypted column only when it is
|
|
18
|
+
ciphertext that decrypts with this app's encryption keys, or null.
|
|
19
|
+
|
|
16
20
|
Dream class: ${this.dreamClass.sanitizedName}
|
|
17
21
|
Problematic setter: ${this.encryptedColumnName}
|
|
18
22
|
Setter to be used instead: ${this.encryptedProperty}`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export default class UnrecognizedAssociationConditionKeys extends Error {
|
|
2
|
+
associationName;
|
|
3
|
+
unrecognizedKeys;
|
|
4
|
+
acceptedKeys;
|
|
5
|
+
methodName;
|
|
6
|
+
constructor(associationName, unrecognizedKeys, acceptedKeys, methodName) {
|
|
7
|
+
super();
|
|
8
|
+
this.associationName = associationName;
|
|
9
|
+
this.unrecognizedKeys = unrecognizedKeys;
|
|
10
|
+
this.acceptedKeys = acceptedKeys;
|
|
11
|
+
this.methodName = methodName;
|
|
12
|
+
}
|
|
13
|
+
get message() {
|
|
14
|
+
const source = this.methodName
|
|
15
|
+
? `in the object passed to ${this.methodName}('${this.associationName}')`
|
|
16
|
+
: `in the condition on the "${this.associationName}" association`;
|
|
17
|
+
return `
|
|
18
|
+
Unrecognized key${this.unrecognizedKeys.length === 1 ? '' : 's'} ${source}: ${this.unrecognizedKeys.join(', ')}
|
|
19
|
+
Accepted keys: ${this.acceptedKeys.join(', ')}
|
|
20
|
+
|
|
21
|
+
Where-clauses on an association go under \`and\`, \`andNot\` or \`andAny\`, e.g.:
|
|
22
|
+
{ and: { email: 'how@yadoin' } }
|
|
23
|
+
`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -34,6 +34,22 @@ export default class Env {
|
|
|
34
34
|
const parsedVal = parseInt(val, 10);
|
|
35
35
|
return (parsedVal.toString() === val ? parsedVal : undefined);
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Reads a boolean environment variable. Only the exact string `"1"` is
|
|
39
|
+
* true. Every other value is false, including `"true"`, `"TRUE"`, `"yes"`
|
|
40
|
+
* and `""`, and so is an unset variable.
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* // FEATURE_FLAG=1
|
|
44
|
+
* AppEnv.boolean('FEATURE_FLAG') // true
|
|
45
|
+
*
|
|
46
|
+
* // FEATURE_FLAG=true
|
|
47
|
+
* AppEnv.boolean('FEATURE_FLAG') // false
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @param env - the name of the environment variable
|
|
51
|
+
* @returns `true` when the variable is exactly `"1"`, otherwise `false`
|
|
52
|
+
*/
|
|
37
53
|
boolean(env) {
|
|
38
54
|
return this.optional(env) === '1';
|
|
39
55
|
}
|
|
@@ -51,9 +67,21 @@ export default class Env {
|
|
|
51
67
|
unsetInteger(env) {
|
|
52
68
|
delete process.env[env];
|
|
53
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Sets a boolean environment variable to `"1"`, the only value
|
|
72
|
+
* {@link Env.boolean} reads as true.
|
|
73
|
+
*
|
|
74
|
+
* @param env - the name of the environment variable
|
|
75
|
+
*/
|
|
54
76
|
setBoolean(env) {
|
|
55
77
|
process.env[env] = '1';
|
|
56
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Deletes a boolean environment variable. {@link Env.boolean} reads an
|
|
81
|
+
* unset variable as false, since only the exact string `"1"` is true.
|
|
82
|
+
*
|
|
83
|
+
* @param env - the name of the environment variable
|
|
84
|
+
*/
|
|
57
85
|
unsetBoolean(env) {
|
|
58
86
|
delete process.env[env];
|
|
59
87
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pluralize from 'pluralize-esm';
|
|
2
2
|
import serializerGlobalNameFromFullyQualifiedModelName from '../../serializer/helpers/serializerGlobalNameFromFullyQualifiedModelName.js';
|
|
3
|
+
import defaultEncryptedColumnName from '../../encrypt/defaultEncryptedColumnName.js';
|
|
3
4
|
import camelize from '../camelize.js';
|
|
4
5
|
import globalClassNameFromFullyQualifiedModelName from '../globalClassNameFromFullyQualifiedModelName.js';
|
|
5
6
|
import pascalize from '../pascalize.js';
|
|
@@ -189,7 +190,7 @@ public ${associationForeignKey}: DreamColumn<${modelClassName}, '${associationNa
|
|
|
189
190
|
};
|
|
190
191
|
}
|
|
191
192
|
export function createEncryptedAttribute(attributeName, attribute, modelClassName) {
|
|
192
|
-
const encryptedColumnName =
|
|
193
|
+
const encryptedColumnName = defaultEncryptedColumnName(attributeName);
|
|
193
194
|
const content = `
|
|
194
195
|
@deco.Encrypted()
|
|
195
196
|
public ${camelize(attributeName)}: DreamColumn<${modelClassName}, '${encryptedColumnName}'>`;
|
|
@@ -46,7 +46,7 @@ export default function generateFactoryContent({ fullyQualifiedModelName, column
|
|
|
46
46
|
switch (safeAttributeType) {
|
|
47
47
|
case 'belongsto': {
|
|
48
48
|
// When `Model@alias:belongs_to`, the factory property uses the alias;
|
|
49
|
-
// otherwise it uses the model's last namespace segment
|
|
49
|
+
// otherwise it uses the model's last namespace segment.
|
|
50
50
|
const attributeVariable = aliasName ? camelize(aliasName) : camelize(attributeName.split('/').pop());
|
|
51
51
|
const fullyQualifiedAssociatedModelName = standardizeFullyQualifiedModelName(attributeName);
|
|
52
52
|
const associationModelName = globalClassNameFromFullyQualifiedModelName(fullyQualifiedAssociatedModelName);
|
|
@@ -4,6 +4,7 @@ import Query from '../../dream/Query.js';
|
|
|
4
4
|
import InvalidDecimalFieldPassedToGenerator from '../../errors/InvalidDecimalFieldPassedToGenerator.js';
|
|
5
5
|
import NoColumnsToAlterMigration from '../../errors/NoColumnsToAlterMigration.js';
|
|
6
6
|
import UnparseableMigrationColumn from '../../errors/UnparseableMigrationColumn.js';
|
|
7
|
+
import defaultEncryptedColumnName from '../../encrypt/defaultEncryptedColumnName.js';
|
|
7
8
|
import camelize from '../camelize.js';
|
|
8
9
|
import compact from '../compact.js';
|
|
9
10
|
import globalClassNameFromFullyQualifiedModelName from '../globalClassNameFromFullyQualifiedModelName.js';
|
|
@@ -141,7 +142,7 @@ export default function generateMigrationContent({ connectionName = 'default', t
|
|
|
141
142
|
columnDefs.push(generateBooleanStr(attributeName, { omitInlineNonNull }));
|
|
142
143
|
break;
|
|
143
144
|
case 'encrypted':
|
|
144
|
-
attributeName =
|
|
145
|
+
attributeName = snakeify(defaultEncryptedColumnName(attributeName));
|
|
145
146
|
columnDefs.push(generateColumnStr(attributeName, 'text', descriptors, {
|
|
146
147
|
omitInlineNonNull,
|
|
147
148
|
skipUniqueHeuristic: true,
|
|
@@ -183,13 +184,12 @@ export default function generateMigrationContent({ connectionName = 'default', t
|
|
|
183
184
|
// time rather than generation time. Fail loudly here instead.
|
|
184
185
|
//
|
|
185
186
|
// This guard is scoped to the standalone `g:migration` `-to-`/`-from-` flow
|
|
186
|
-
// only (`stiChildClassName` is unset there)
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// `
|
|
192
|
-
// expected, not a sign of a mistyped/unparseable column list.
|
|
187
|
+
// only (`stiChildClassName` is unset there); STI child migrations skip it.
|
|
188
|
+
// `g:sti-child` writes no migration for a child declared with no columns
|
|
189
|
+
// (see `generateDream`), so an STI child migration reaches this point with
|
|
190
|
+
// an empty `columnDefs` only when none of its declarations produced a
|
|
191
|
+
// column (e.g. only `has_one`/`has_many` ones), and it then emits the bare
|
|
192
|
+
// `.alterTable(...).execute()` described above.
|
|
193
193
|
if (altering &&
|
|
194
194
|
columnDefs.length === 0 &&
|
|
195
195
|
table !== MIGRATION_TABLE_NAME_PLACEHOLDER &&
|
|
@@ -472,7 +472,7 @@ function generateBelongsToStr(connectionName, associationName, { primaryKeyType,
|
|
|
472
472
|
const dataType = dbDriverClass.foreignKeyTypeFromPrimaryKey(primaryKeyType);
|
|
473
473
|
const references = lookupReferencesTable(associationName, originalAssociationName);
|
|
474
474
|
// When the user passed `Model@alias:belongs_to`, the column name comes from
|
|
475
|
-
// the alias; otherwise it's the model's last segment
|
|
475
|
+
// the alias; otherwise it's the model's last segment.
|
|
476
476
|
const columnNameSource = aliasName ? snakeify(aliasName) : associationName.split('/').pop();
|
|
477
477
|
return `.addColumn('${associationNameToForeignKey(columnNameSource)}', '${dataType}', col => col.references('${references}.id').onDelete('restrict')${optional ? '' : '.notNull()'})`;
|
|
478
478
|
}
|
|
@@ -40,6 +40,16 @@ export default function generateSerializerContent({ fullyQualifiedModelName, col
|
|
|
40
40
|
? `import { ${uniq(dreamImports).join(', ')} } from '@rvoh/dream'\n`
|
|
41
41
|
: '';
|
|
42
42
|
const additionalImportsStr = uniq(additionalImports).join('');
|
|
43
|
+
// An STI base renders the `type` discriminator in its summary serializers,
|
|
44
|
+
// so each default serializer, and each STI child serializer, inherits it.
|
|
45
|
+
const rendersStiType = stiBaseSerializer &&
|
|
46
|
+
columnsWithTypes.some(attr => {
|
|
47
|
+
const [name, type] = attr.split(':');
|
|
48
|
+
return name === 'type' && !isAssociationType(type);
|
|
49
|
+
});
|
|
50
|
+
const stiTypeAttribute = rendersStiType
|
|
51
|
+
? `\n .attribute('type', { openapi: { type: 'string', enum: [(StiChildClass ?? ${modelClassName}).sanitizedName] } })`
|
|
52
|
+
: '';
|
|
43
53
|
const buildSerializerPair = (variantSuffix) => {
|
|
44
54
|
const summaryName = variantSerializerClassName(localSerializerBase, variantSuffix, 'summary');
|
|
45
55
|
const defaultName = variantSerializerClassName(localSerializerBase, variantSuffix, 'default');
|
|
@@ -50,15 +60,17 @@ export default function generateSerializerContent({ fullyQualifiedModelName, col
|
|
|
50
60
|
? `${variantSerializerClassName(parentSerializerBase, variantSuffix, 'default')}(${modelClassName}, ${modelSerializerArgs})`
|
|
51
61
|
: `${summaryName}(${stiBaseSerializer ? 'StiChildClass, ' : ''}${modelSerializerArgs})`;
|
|
52
62
|
const summary = `export const ${summaryName} = ${modelSerializerSignature} =>
|
|
53
|
-
${summaryExtends}${isSTI ? '' : `\n .attribute('id')`}`;
|
|
63
|
+
${summaryExtends}${isSTI ? '' : `\n .attribute('id')`}${stiTypeAttribute}`;
|
|
54
64
|
const defaultBody = columnsWithTypes
|
|
55
65
|
.map(attr => {
|
|
56
66
|
const [name, type] = attr.split(':');
|
|
57
67
|
if (name === undefined)
|
|
58
68
|
return '';
|
|
59
|
-
if (
|
|
69
|
+
if (isAssociationType(type))
|
|
60
70
|
return '';
|
|
61
|
-
|
|
71
|
+
if (rendersStiType && name === 'type')
|
|
72
|
+
return '';
|
|
73
|
+
return `\n ${attribute(name, type, attr)}`;
|
|
62
74
|
})
|
|
63
75
|
.join('');
|
|
64
76
|
const def = `export const ${defaultName} = ${modelSerializerSignature} =>
|
|
@@ -75,16 +87,17 @@ function variantSerializerClassName(serializerBase, variantSuffix, kind) {
|
|
|
75
87
|
? `${serializerBase}${variantSuffix}SummarySerializer`
|
|
76
88
|
: `${serializerBase}${variantSuffix}Serializer`;
|
|
77
89
|
}
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
function isAssociationType(type) {
|
|
91
|
+
return ['belongsto', 'hasone', 'hasmany'].includes(camelize(type)?.toLowerCase());
|
|
92
|
+
}
|
|
93
|
+
function attribute(name, type, attr) {
|
|
82
94
|
switch (type) {
|
|
83
95
|
case 'json':
|
|
84
96
|
case 'jsonb':
|
|
97
|
+
return `.attribute('${camelize(name)}', { openapi: { type: 'object', properties: { } } })`;
|
|
85
98
|
case 'json[]':
|
|
86
99
|
case 'jsonb[]':
|
|
87
|
-
return `.attribute('${camelize(name)}', { openapi: { type: 'object', properties: { } } })`;
|
|
100
|
+
return `.attribute('${camelize(name)}', { openapi: { type: 'array', items: { type: 'object', properties: { } } } })`;
|
|
88
101
|
default:
|
|
89
102
|
return `.attribute('${camelize(name)}'${attributeOptionsSpecifier(type, attr)})`;
|
|
90
103
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Dream from '../Dream.js';
|
|
1
2
|
import RendersManyMustReceiveArray from '../errors/serializers/RendersManyMustReceiveArray.js';
|
|
2
3
|
import compact from '../helpers/compact.js';
|
|
3
4
|
import round from '../helpers/round.js';
|
|
@@ -8,7 +9,7 @@ import { DateTime } from '../utils/datetime/DateTime.js';
|
|
|
8
9
|
import DreamSerializerBuilder from './builders/DreamSerializerBuilder.js';
|
|
9
10
|
import ObjectSerializerBuilder from './builders/ObjectSerializerBuilder.js';
|
|
10
11
|
import inferSerializerFromDreamOrViewModel from './helpers/inferSerializerFromDreamOrViewModel.js';
|
|
11
|
-
import {
|
|
12
|
+
import { serializersForAssociatedClass } from './helpers/serializersForAssociatedClass.js';
|
|
12
13
|
export default class SerializerRenderer {
|
|
13
14
|
serializerBuilder;
|
|
14
15
|
passthroughData;
|
|
@@ -95,19 +96,16 @@ export default class SerializerRenderer {
|
|
|
95
96
|
case 'rendersOne': {
|
|
96
97
|
const outputAttributeName = this.setCase(attribute.options.as ?? attribute.name);
|
|
97
98
|
const associatedObject = data[attribute.name];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Only used when flatten: true, and the associated model is null, in which case,
|
|
105
|
-
* we need something to determine the keys that will be flattened into the
|
|
106
|
-
* rendering serializer
|
|
107
|
-
*/
|
|
108
|
-
serializer = serializerForAssociatedClass(data, attribute.name, attribute.options);
|
|
99
|
+
if (!associatedObject && attribute.options.flatten) {
|
|
100
|
+
return {
|
|
101
|
+
...accumulator,
|
|
102
|
+
...this.nullFlattenedAttributes(serializersForAssociatedClass(data instanceof Dream ? data.constructor : null, attribute.name, attribute.options), passthroughData),
|
|
103
|
+
};
|
|
109
104
|
}
|
|
110
|
-
const
|
|
105
|
+
const serializer = associatedObject
|
|
106
|
+
? serializerForAssociatedObject(associatedObject, attribute.options, renderTimeResolutionContext(attribute.options, 'rendersOne', attribute.name))
|
|
107
|
+
: null;
|
|
108
|
+
const serializerBuilder = serializer?.(associatedObject,
|
|
111
109
|
// passthrough data going into the serializer is the argument that gets
|
|
112
110
|
// used in the custom attribute callback function
|
|
113
111
|
passthroughData);
|
|
@@ -178,6 +176,60 @@ export default class SerializerRenderer {
|
|
|
178
176
|
}, renderedAttributes);
|
|
179
177
|
return renderedAttributes;
|
|
180
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* The keys a flattened `rendersOne` adds to the parent's payload when its associated object is
|
|
181
|
+
* null: each key any of `serializers` declares, set to `null`. There are several serializers when
|
|
182
|
+
* the association is a polymorphic BelongsTo, one for each target class, or when the associated
|
|
183
|
+
* class is an STI base, one for each child, and their keys are unioned.
|
|
184
|
+
*
|
|
185
|
+
* Each serializer is built with `undefined` data to read its declarations, as Psychic's OpenAPI
|
|
186
|
+
* generation and `preloadFor` build it, and nothing is rendered: no attribute callback runs and
|
|
187
|
+
* no association of the missing object is read. A flattened `rendersOne` it declares adds its
|
|
188
|
+
* own serializers' keys the same way. A flattened `customAttribute` adds none, since only its
|
|
189
|
+
* callback knows its keys.
|
|
190
|
+
*/
|
|
191
|
+
nullFlattenedAttributes(serializers, passthroughData, serializersBeingFlattened = new Set()) {
|
|
192
|
+
return serializers.reduce((flattenedKeys, serializer) => {
|
|
193
|
+
// a serializer reached again, e.g. one that flattens itself directly or through others, or
|
|
194
|
+
// one that two polymorphic targets or two STI children both flatten, adds no keys the first
|
|
195
|
+
// walk through it did not
|
|
196
|
+
if (serializersBeingFlattened.has(serializer))
|
|
197
|
+
return flattenedKeys;
|
|
198
|
+
serializersBeingFlattened.add(serializer);
|
|
199
|
+
const serializerBuilder = serializer(undefined, passthroughData);
|
|
200
|
+
const dreamClass = serializerBuilder instanceof DreamSerializerBuilder
|
|
201
|
+
? serializerBuilder['$typeForOpenapi']
|
|
202
|
+
: null;
|
|
203
|
+
return serializerBuilder['attributes'].reduce((keys, attribute) => {
|
|
204
|
+
const attributeType = attribute.type;
|
|
205
|
+
switch (attributeType) {
|
|
206
|
+
case 'attribute':
|
|
207
|
+
case 'delegatedAttribute':
|
|
208
|
+
case 'rendersMany':
|
|
209
|
+
keys[this.setCase(attribute.options?.as ?? attribute.name)] = null;
|
|
210
|
+
return keys;
|
|
211
|
+
case 'customAttribute':
|
|
212
|
+
if (!attribute.options.flatten)
|
|
213
|
+
keys[this.setCase(attribute.name)] = null;
|
|
214
|
+
return keys;
|
|
215
|
+
case 'rendersOne':
|
|
216
|
+
if (!attribute.options.flatten) {
|
|
217
|
+
keys[this.setCase(attribute.options.as ?? attribute.name)] = null;
|
|
218
|
+
return keys;
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
...keys,
|
|
222
|
+
...this.nullFlattenedAttributes(serializersForAssociatedClass(dreamClass, attribute.name, attribute.options), passthroughData, serializersBeingFlattened),
|
|
223
|
+
};
|
|
224
|
+
default: {
|
|
225
|
+
// protection so that if a new ValidationType is ever added, this will throw a type error at build time
|
|
226
|
+
const _never = attributeType;
|
|
227
|
+
throw new Error(`Unhandled serializer attribute type: ${_never}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}, flattenedKeys);
|
|
231
|
+
}, {});
|
|
232
|
+
}
|
|
181
233
|
setCase(attr) {
|
|
182
234
|
switch (this.renderOpts.casing) {
|
|
183
235
|
case 'camel':
|
|
@@ -26,9 +26,10 @@ export default class DreamSerializerBuilder {
|
|
|
26
26
|
* 2. `required: false` → omits the key entirely from the rendered output
|
|
27
27
|
* 3. otherwise → renders `null`
|
|
28
28
|
*
|
|
29
|
-
* When the target is a Dream model,
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* When the target is a Dream model, a non-json column's OpenAPI shape is inferred
|
|
30
|
+
* from the column, so its `openapi` option takes only a `description`. json/jsonb
|
|
31
|
+
* columns and non-Dream targets have no shape to infer, so they require a full
|
|
32
|
+
* `openapi` shape.
|
|
32
33
|
*
|
|
33
34
|
* `optional` and `required` are not aliases — they encode different things and can
|
|
34
35
|
* be used together:
|
|
@@ -52,12 +53,14 @@ export default class DreamSerializerBuilder {
|
|
|
52
53
|
* a discriminator string when the association is actually missing produces a response
|
|
53
54
|
* indistinguishable from "association present with that type," which is misleading)
|
|
54
55
|
* - `openapi` - OpenAPI schema definition; required for non-Dream targets and json/jsonb
|
|
55
|
-
* columns
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
56
|
+
* columns. On a non-json Dream column, whose shape is inferred, it takes only a
|
|
57
|
+
* `description` (and, on the STI `type` column, a `type: 'string'` and `enum`)
|
|
58
|
+
* - `optional` - Set to `true` to mark the value as nullable in the OpenAPI schema:
|
|
59
|
+
* Psychic adds `null` to the rendered schema (e.g., `type: 'string'` becomes
|
|
60
|
+
* `type: ['string', 'null']`, and a `$ref` becomes `anyOf: [{ $ref }, { type: 'null' }]`).
|
|
61
|
+
* OpenAPI-only — the key is still rendered (as `null`). For Dream models, this is
|
|
62
|
+
* auto-inferred from optional BelongsTo associations. Use this when delegating
|
|
63
|
+
* through a HasOne or other nullable association.
|
|
61
64
|
* - `precision` - Round decimal values to the specified number of decimal places (0–9)
|
|
62
65
|
* during rendering; does not affect the OpenAPI shape (not available when delegating
|
|
63
66
|
* to a `'type'` STI discriminator column, which is always a string enum)
|
|
@@ -69,27 +72,26 @@ export default class DreamSerializerBuilder {
|
|
|
69
72
|
* @example
|
|
70
73
|
* ```typescript
|
|
71
74
|
* // Delegate to a Dream association's column (type inferred)
|
|
72
|
-
* .delegatedAttribute('currentLocalizedText', 'title'
|
|
75
|
+
* .delegatedAttribute('currentLocalizedText', 'title')
|
|
73
76
|
*
|
|
74
77
|
* // With default value for null target or attribute
|
|
75
|
-
* .delegatedAttribute('user', 'displayName', {
|
|
76
|
-
* openapi: { type: 'string' },
|
|
77
|
-
* default: 'Unknown User'
|
|
78
|
-
* })
|
|
78
|
+
* .delegatedAttribute('user', 'displayName', { default: 'Unknown User' })
|
|
79
79
|
*
|
|
80
80
|
* // Rename the output key
|
|
81
|
-
* .delegatedAttribute('profile', 'avatarUrl', {
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* .delegatedAttribute('profile', 'avatarUrl', { as: 'avatar' })
|
|
82
|
+
*
|
|
83
|
+
* // A json/jsonb column has no shape to infer, so it declares one
|
|
84
|
+
* .delegatedAttribute('user', 'preferences', {
|
|
85
|
+
* openapi: { type: 'object', properties: { theme: 'string' } }
|
|
84
86
|
* })
|
|
85
87
|
* ```
|
|
86
88
|
*/
|
|
87
|
-
delegatedAttribute(targetName, name, options) {
|
|
89
|
+
delegatedAttribute(targetName, name, ...options) {
|
|
88
90
|
this.attributes.push({
|
|
89
91
|
type: 'delegatedAttribute',
|
|
90
92
|
targetName: targetName,
|
|
91
93
|
name: name,
|
|
92
|
-
options: options ?? {},
|
|
94
|
+
options: options[0] ?? {},
|
|
93
95
|
});
|
|
94
96
|
return this;
|
|
95
97
|
}
|
|
@@ -99,17 +101,24 @@ export default class DreamSerializerBuilder {
|
|
|
99
101
|
* Executes a callback function to generate a custom attribute value.
|
|
100
102
|
* The `openapi` option is always required since the return type cannot be inferred.
|
|
101
103
|
*
|
|
104
|
+
* The callback's return value renders as is, so any fallback or rounding belongs in the
|
|
105
|
+
* callback.
|
|
106
|
+
*
|
|
102
107
|
* @param name - The attribute name for the computed value
|
|
103
108
|
* @param fn - Callback function that returns the computed value
|
|
104
109
|
* @param options - Configuration options:
|
|
105
110
|
* - `openapi` - (required) OpenAPI schema definition for the computed value
|
|
106
|
-
* - `as` - Rename the attribute key in the serialized output and OpenAPI shape
|
|
107
|
-
* - `default` - Value to use when the callback returns undefined
|
|
108
111
|
* - `flatten` - When `true`, spreads the returned object's properties directly into the
|
|
109
|
-
* parent serialized output instead of nesting them under `name
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
112
|
+
* parent serialized output instead of nesting them under `name`. The `openapi` option
|
|
113
|
+
* then describes the whole returned object, not each property, because Psychic adds it
|
|
114
|
+
* to the parent's schema as an `allOf` branch: either an object schema with full
|
|
115
|
+
* property bodies (`type: 'object'`, `properties`, and `required` listing the keys that
|
|
116
|
+
* are always present), or `{ $serializer: SomeSerializer }` when the callback returns
|
|
117
|
+
* what that serializer renders. A serializer ref with `many: true` does not type-check
|
|
118
|
+
* here, since a list has no properties to spread. A `null` or `undefined` return adds no
|
|
119
|
+
* keys. Attributes render in declaration order, so a key the returned object shares with
|
|
120
|
+
* another attribute takes the value of whichever is declared later; under the `allOf`,
|
|
121
|
+
* that value must match both schemas
|
|
113
122
|
* - `required` - Set to `false` to mark the attribute as optional in the OpenAPI schema;
|
|
114
123
|
* when omitted, attributes are required by default
|
|
115
124
|
* @returns The serializer builder for method chaining
|
|
@@ -122,20 +131,18 @@ export default class DreamSerializerBuilder {
|
|
|
122
131
|
* { openapi: { type: 'string' } }
|
|
123
132
|
* )
|
|
124
133
|
*
|
|
125
|
-
* // Flattened object properties
|
|
134
|
+
* // Flattened object properties: `openapi` describes the whole returned object
|
|
126
135
|
* .customAttribute('coordinates', () => ({ lat: 40.7, lng: -74.0 }), {
|
|
127
136
|
* flatten: true,
|
|
128
137
|
* openapi: {
|
|
129
|
-
*
|
|
130
|
-
*
|
|
138
|
+
* type: 'object',
|
|
139
|
+
* required: ['lat', 'lng'],
|
|
140
|
+
* properties: {
|
|
141
|
+
* lat: { type: 'number' },
|
|
142
|
+
* lng: { type: 'number' }
|
|
143
|
+
* }
|
|
131
144
|
* }
|
|
132
145
|
* })
|
|
133
|
-
*
|
|
134
|
-
* // With decimal precision
|
|
135
|
-
* .customAttribute('averageRating', () => calculateAverage(ratings), {
|
|
136
|
-
* openapi: 'decimal',
|
|
137
|
-
* precision: 2
|
|
138
|
-
* })
|
|
139
146
|
* ```
|
|
140
147
|
*/
|
|
141
148
|
customAttribute(name, fn, options) {
|
|
@@ -143,7 +150,7 @@ export default class DreamSerializerBuilder {
|
|
|
143
150
|
type: 'customAttribute',
|
|
144
151
|
name,
|
|
145
152
|
fn,
|
|
146
|
-
options
|
|
153
|
+
options,
|
|
147
154
|
});
|
|
148
155
|
return this;
|
|
149
156
|
}
|
|
@@ -157,9 +164,18 @@ export default class DreamSerializerBuilder {
|
|
|
157
164
|
* @param options - Configuration options:
|
|
158
165
|
* - `as` - Rename the association key in the serialized output
|
|
159
166
|
* - `flatten` - When `true`, spreads the rendered association's attributes directly into
|
|
160
|
-
* the parent serialized output instead of nesting them under `name`.
|
|
161
|
-
*
|
|
162
|
-
*
|
|
167
|
+
* the parent serialized output instead of nesting them under `name`. Attributes render in
|
|
168
|
+
* declaration order, so when the parent and the association share a key (e.g., `id`),
|
|
169
|
+
* whichever is declared later wins: the association's value replaces an attribute declared
|
|
170
|
+
* before the `rendersOne`, and an attribute declared after it replaces the association's.
|
|
171
|
+
* When the association is `null`, every key its serializer declares renders as `null`
|
|
172
|
+
* (for a polymorphic `BelongsTo`, every key the serializer of any of its target classes
|
|
173
|
+
* declares, and for an association to an STI base, every key the serializer of any of its
|
|
174
|
+
* STI children declares), in the same order (so an `id` declared before the `rendersOne` becomes
|
|
175
|
+
* `null`); nothing in that serializer is rendered, so its custom attribute callbacks do
|
|
176
|
+
* not run, and a flattened `customAttribute` in it adds no keys. Psychic adds the association's OpenAPI
|
|
177
|
+
* schema to the parent's as an `allOf` branch, so a shared key's value must match both
|
|
178
|
+
* schemas, whichever declaration wins
|
|
163
179
|
* - `optional` - When `true`, allows the association to be null/missing without causing
|
|
164
180
|
* an `OpenapiResponseValidationFailure` during Psychic controller unit specs. By default,
|
|
165
181
|
* `rendersOne` expects the association to be present (mirroring the `optional` option on
|