@rvoh/dream 2.35.0 → 2.36.1
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 +260 -55
- 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 +35 -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 +77 -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 +260 -55
- 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 +35 -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 +77 -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 +208 -42
- 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 +29 -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 +270 -142
- 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
|
@@ -68,44 +68,56 @@ function buildColumnsWithTypesDescription(belongsToDescription) {
|
|
|
68
68
|
return `${baseColumnsWithTypesDescription}
|
|
69
69
|
${belongsToDescription}`;
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The `[columnsWithTypes...]` argument description for `g:model`. Exported so
|
|
73
|
+
* that spec/unit/cli/generateMigrationContent.spec.ts can run each
|
|
74
|
+
* `:belongs_to` example in this text through the migration generator and check
|
|
75
|
+
* the FK column the example's comment names: an example that drifts from what
|
|
76
|
+
* the generator actually produces then fails the suite instead of shipping.
|
|
77
|
+
*/
|
|
78
|
+
export const columnsWithTypesDescription = buildColumnsWithTypesDescription(`
|
|
72
79
|
${INDENT}
|
|
73
80
|
${INDENT} - belongs_to:
|
|
74
81
|
${INDENT} ALWAYS use this instead of adding a raw uuid column for foreign keys. It creates the FK column, adds a database index,
|
|
75
82
|
${INDENT} AND generates the @deco.BelongsTo association and typed property on the model. A raw uuid column does none of this.
|
|
76
83
|
${INDENT}
|
|
77
|
-
${INDENT} use the fully qualified model name (matching its path under src/app/models/)
|
|
84
|
+
${INDENT} use the fully qualified model name (matching its path under src/app/models/). The last segment of that
|
|
85
|
+
${INDENT} path names the FK column, the typed FK property and the association:
|
|
78
86
|
${INDENT} User:belongs_to # creates user_id column + BelongsTo association
|
|
79
|
-
${INDENT} Health/Coach:belongs_to # creates
|
|
87
|
+
${INDENT} Health/Coach:belongs_to # creates coach_id column, coachId property, coach association
|
|
80
88
|
${INDENT} User:belongs_to:optional # nullable foreign key (for optional associations)
|
|
81
89
|
${INDENT}
|
|
82
|
-
${INDENT} rename the association with Model@alias — the snake_case alias
|
|
83
|
-
${INDENT} @deco.BelongsTo association + typed FK property on the generated model:
|
|
90
|
+
${INDENT} rename the association with Model@alias — the snake_case alias takes the last segment's place, naming the
|
|
91
|
+
${INDENT} FK column <alias>_id AND the @deco.BelongsTo association + typed FK property on the generated model:
|
|
84
92
|
${INDENT} InternalUser@canceled_by:belongs_to:optional # canceled_by_id column, canceledById property, canceledBy association,
|
|
85
93
|
${INDENT} # @deco.BelongsTo('InternalUser', { on: 'canceledById', optional: true })
|
|
86
|
-
${INDENT}
|
|
87
|
-
${INDENT} # (
|
|
88
|
-
${INDENT} # the namespaced model reference intact)
|
|
94
|
+
${INDENT} Health/Coach@health_coach:belongs_to # health_coach_id column, healthCoachId property, healthCoach association,
|
|
95
|
+
${INDENT} # @deco.BelongsTo('Health/Coach', { on: 'healthCoachId' })
|
|
89
96
|
${INDENT} Aliasing also lets you declare multiple FKs to the same model in one generator call without column collisions.`);
|
|
90
97
|
export const columnsWithTypesDescriptionForStiChild = buildColumnsWithTypesDescription(`
|
|
91
98
|
${INDENT}
|
|
92
99
|
${INDENT} - belongs_to:
|
|
93
100
|
${INDENT} NOT supported for STI children. STI children cannot declare associations; declare all BelongsTo,
|
|
94
101
|
${INDENT} HasOne, and HasMany associations on the STI parent model instead.`);
|
|
95
|
-
|
|
102
|
+
/**
|
|
103
|
+
* The `[columnsWithTypes...]` argument description for `g:migration`. Exported
|
|
104
|
+
* for the same `:belongs_to` example check as `columnsWithTypesDescription`.
|
|
105
|
+
*/
|
|
106
|
+
export const columnsWithTypesDescriptionForMigration = buildColumnsWithTypesDescription(`
|
|
96
107
|
${INDENT}
|
|
97
108
|
${INDENT} - belongs_to:
|
|
98
109
|
${INDENT} ALWAYS use this instead of adding a raw uuid column for foreign keys. It creates the FK column with an index.
|
|
99
110
|
${INDENT} Unlike in g:model/g:resource, this does NOT add a BelongsTo association (no model is generated).
|
|
100
111
|
${INDENT}
|
|
101
|
-
${INDENT} use the fully qualified model name (matching its path under src/app/models/)
|
|
112
|
+
${INDENT} use the fully qualified model name (matching its path under src/app/models/). The last segment of that
|
|
113
|
+
${INDENT} path names the FK column:
|
|
102
114
|
${INDENT} User:belongs_to # creates user_id column with index
|
|
103
|
-
${INDENT} Health/Coach:belongs_to # creates
|
|
115
|
+
${INDENT} Health/Coach:belongs_to # creates coach_id column with index
|
|
104
116
|
${INDENT} User:belongs_to:optional # nullable foreign key
|
|
105
117
|
${INDENT}
|
|
106
|
-
${INDENT} rename the FK column with Model@alias (
|
|
118
|
+
${INDENT} rename the FK column with Model@alias (the column is <alias>_id, with the alias snake_cased):
|
|
107
119
|
${INDENT} InternalUser@canceled_by:belongs_to:optional # canceled_by_id column with index
|
|
108
|
-
${INDENT}
|
|
120
|
+
${INDENT} Health/Coach@health_coach:belongs_to # health_coach_id column with index`);
|
|
109
121
|
/**
|
|
110
122
|
* The `<migrationName>` argument description for `g:migration`. Exported so
|
|
111
123
|
* that spec/unit/helpers/cli/generateMigration.spec.ts can read the example
|
|
@@ -217,7 +229,7 @@ ${INDENT} # STI parent model (use with g:sti-child for children)
|
|
|
217
229
|
${INDENT} pnpm psy g:model --sti-base-serializer Room Place:belongs_to type:enum:room_types:Bathroom,Bedroom deleted_at:datetime:optional`)
|
|
218
230
|
.option('--no-serializer', 'skip serializer generation. Useful for internal models that will never be serialized in an API response (e.g., join tables, audit logs)')
|
|
219
231
|
.option('--connection-name <connectionName>', 'the name of the database connection to use for the model. Only needed for multi-database setups; defaults to "default"', 'default')
|
|
220
|
-
.option('--sti-base-serializer', `Creates generically typed base serializers (default and
|
|
232
|
+
.option('--sti-base-serializer', `Creates generically typed base serializers (summary and default, plus each Admin and Internal pair requested with --admin-serializers or --internal-serializers) that accept a \`StiChildClass\` parameter. When a \`type\` column is given, each summary serializer renders \`type\` right after \`id\`, with an OpenAPI enum of just the STI child's class name, and the default serializer inherits it from the summary. This allows consuming applications to determine the response shape based on the STI type discriminator.
|
|
221
233
|
${INDENT}
|
|
222
234
|
${INDENT}Use this when generating the parent model of an STI hierarchy. After generating the parent, use g:sti-child for each child type.
|
|
223
235
|
${INDENT}
|
|
@@ -255,9 +267,9 @@ ${INDENT} Settings/CommunicationPreferences # src/app/models/Settings/Communi
|
|
|
255
267
|
program
|
|
256
268
|
.command('generate:sti-child')
|
|
257
269
|
.alias('g:sti-child')
|
|
258
|
-
.description(`Generates an STI (Single Table Inheritance) child model that extends an existing parent model. The child shares the parent's database table (discriminated by the \`type\` column) and can add child-specific columns.
|
|
270
|
+
.description(`Generates an STI (Single Table Inheritance) child model that extends an existing parent model. The child shares the parent's database table (discriminated by the \`type\` column) and can add child-specific columns. Always generates the child model decorated with @STI(Parent), a factory, and a spec skeleton; unless --no-serializer is passed, also generates child serializers extending the parent's base serializers.
|
|
259
271
|
${INDENT}
|
|
260
|
-
${INDENT}
|
|
272
|
+
${INDENT}A migration is generated only when positional field:type args are passed. It ALTERs the parent's table (it does not create a new one), adds a check constraint requiring a value on this child's rows for each column that is not :optional, an array or a boolean, and goes in the migrations folder of the parent model's database connection, which the child shares. A child with no additional columns needs no schema change, so no migration is generated for it. STI children cannot declare associations — declare them on the parent model instead. The generator rejects belongs_to columns. STI children never receive @SoftDelete() — soft delete is enforced at the parent level only — and the generator does not accept --no-soft-delete.
|
|
261
273
|
${INDENT}
|
|
262
274
|
${INDENT}The parent must already exist (typically generated with g:model --sti-base-serializer or g:resource --sti-base-serializer).
|
|
263
275
|
${INDENT}
|
|
@@ -271,7 +283,6 @@ ${INDENT}
|
|
|
271
283
|
${INDENT} # Child with no additional columns
|
|
272
284
|
${INDENT} pnpm psy g:sti-child --model-name=Kitchen Room/Kitchen extends Room`)
|
|
273
285
|
.option('--no-serializer', 'skip serializer generation. Useful if the child uses the parent serializer directly or serialization is handled elsewhere')
|
|
274
|
-
.option('--connection-name', 'the name of the database connection to use for the model. Only needed for multi-database setups; defaults to "default"', 'default')
|
|
275
286
|
.option('--model-name <modelName>', `Explicit model class name to use instead of the one auto-derived from the model path. Useful when the path segments don't match the desired class name.
|
|
276
287
|
${INDENT}
|
|
277
288
|
${INDENT}Example:
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { sql } from 'kysely';
|
|
2
|
+
import defaultEncryptedColumnName from '../../encrypt/defaultEncryptedColumnName.js';
|
|
2
3
|
import InternalEncrypt from '../../encrypt/InternalEncrypt.js';
|
|
3
4
|
import DropEnumValueRetypeFailed, { quotedRelationName, } from '../../errors/DropEnumValueRetypeFailed.js';
|
|
5
|
+
import snakeify from '../../helpers/snakeify.js';
|
|
4
6
|
export default class DreamMigrationHelpers {
|
|
5
7
|
/**
|
|
6
8
|
* Rename a table and its associated primary key index and sequence.
|
|
@@ -49,8 +51,8 @@ export default class DreamMigrationHelpers {
|
|
|
49
51
|
await this.dropConstraint(db, constraintName, { table });
|
|
50
52
|
await sql `
|
|
51
53
|
ALTER TABLE ${sql.table(table)}
|
|
52
|
-
ADD CONSTRAINT ${sql.
|
|
53
|
-
UNIQUE (${sql.
|
|
54
|
+
ADD CONSTRAINT ${sql.ref(constraintName)}
|
|
55
|
+
UNIQUE (${sql.join(columns.map(column => sql.ref(column)))})
|
|
54
56
|
DEFERRABLE INITIALLY DEFERRED;
|
|
55
57
|
`.execute(db);
|
|
56
58
|
}
|
|
@@ -65,7 +67,7 @@ export default class DreamMigrationHelpers {
|
|
|
65
67
|
* @param __namedParameters.value - The name of the value to add to the enum
|
|
66
68
|
*/
|
|
67
69
|
static async addEnumValue(db, { enumName, value }) {
|
|
68
|
-
await sql `ALTER TYPE ${sql.
|
|
70
|
+
await sql `ALTER TYPE ${sql.ref(enumName)} ADD VALUE IF NOT EXISTS ${sql.lit(value)};`.execute(db);
|
|
69
71
|
}
|
|
70
72
|
/**
|
|
71
73
|
* Create a database extension.
|
|
@@ -104,7 +106,7 @@ export default class DreamMigrationHelpers {
|
|
|
104
106
|
*/
|
|
105
107
|
static async createGinIndex(db, indexName, { table, column }) {
|
|
106
108
|
await sql `
|
|
107
|
-
CREATE INDEX IF NOT EXISTS ${sql.
|
|
109
|
+
CREATE INDEX IF NOT EXISTS ${sql.ref(indexName)} ON ${sql.table(table)} USING GIN (${sql.ref(column)} gin_trgm_ops);
|
|
108
110
|
`.execute(db);
|
|
109
111
|
}
|
|
110
112
|
/**
|
|
@@ -120,7 +122,7 @@ export default class DreamMigrationHelpers {
|
|
|
120
122
|
*/
|
|
121
123
|
static async dropConstraint(db, constraintName, { table }) {
|
|
122
124
|
await sql `
|
|
123
|
-
ALTER TABLE ${sql.table(table)} DROP CONSTRAINT IF EXISTS ${sql.
|
|
125
|
+
ALTER TABLE ${sql.table(table)} DROP CONSTRAINT IF EXISTS ${sql.ref(constraintName)};
|
|
124
126
|
`.execute(db);
|
|
125
127
|
}
|
|
126
128
|
/**
|
|
@@ -262,11 +264,11 @@ export default class DreamMigrationHelpers {
|
|
|
262
264
|
* @param options - Configuration options
|
|
263
265
|
* @param options.table - The name of the table
|
|
264
266
|
* @param options.column - The current (plaintext) column name
|
|
265
|
-
* @param options.encryptedColumnName - The target encrypted column name. Defaults to
|
|
267
|
+
* @param options.encryptedColumnName - The target encrypted column name. Defaults to the column the `@Encrypted` decorator reads for a property named after `column` (`encrypted_phone` for `phone`); pass this when the decorator was given a custom encrypted column name.
|
|
266
268
|
* @param options.primaryKey - The primary key column used to keyset-paginate and target each row's update. Defaults to `id`.
|
|
267
269
|
* @param options.batchSize - How many rows to read per batch. Defaults to `1000`.
|
|
268
270
|
*/
|
|
269
|
-
static async encryptColumn(db, { table, column, encryptedColumnName =
|
|
271
|
+
static async encryptColumn(db, { table, column, encryptedColumnName = snakeify(defaultEncryptedColumnName(column)), primaryKey = 'id', batchSize = 1000, }) {
|
|
270
272
|
await db.schema.alterTable(table).renameColumn(column, encryptedColumnName).execute();
|
|
271
273
|
await db.schema
|
|
272
274
|
.alterTable(table)
|
|
@@ -301,12 +303,12 @@ export default class DreamMigrationHelpers {
|
|
|
301
303
|
* @param options - Configuration options
|
|
302
304
|
* @param options.table - The name of the table
|
|
303
305
|
* @param options.column - The target (plaintext) column name to rename back to
|
|
304
|
-
* @param options.encryptedColumnName - The current encrypted column name. Defaults to `
|
|
306
|
+
* @param options.encryptedColumnName - The current encrypted column name. Defaults to the column the `@Encrypted` decorator reads for a property named after `column` (`encrypted_phone` for `phone`).
|
|
305
307
|
* @param options.primaryKey - The primary key column used to keyset-paginate and target each row's update. Defaults to `id`.
|
|
306
308
|
* @param options.batchSize - How many rows to read per batch. Defaults to `1000`.
|
|
307
309
|
* @param options.columnType - When provided, the restored column is converted to this type. When omitted, the column is left as `text`.
|
|
308
310
|
*/
|
|
309
|
-
static async decryptColumn(db, { table, column, encryptedColumnName =
|
|
311
|
+
static async decryptColumn(db, { table, column, encryptedColumnName = snakeify(defaultEncryptedColumnName(column)), primaryKey = 'id', batchSize = 1000, columnType, }) {
|
|
310
312
|
await this.transformColumnInBatches(db, { table, column: encryptedColumnName, primaryKey, batchSize }, value => InternalEncrypt.decryptColumn(value));
|
|
311
313
|
await db.schema.alterTable(table).renameColumn(encryptedColumnName, column).execute();
|
|
312
314
|
if (columnType !== undefined) {
|
|
@@ -452,7 +454,7 @@ function constraintContextFor(byPosition, position) {
|
|
|
452
454
|
return byPosition[position] ?? { relationResolved: false, checkConstraints: [] };
|
|
453
455
|
}
|
|
454
456
|
async function getEnumValues(db, enumName) {
|
|
455
|
-
const response = await sql `SELECT unnest(enum_range(NULL::${sql.
|
|
457
|
+
const response = await sql `SELECT unnest(enum_range(NULL::${sql.ref(enumName)}))`.execute(db);
|
|
456
458
|
return response.rows.map(row => row.unnest);
|
|
457
459
|
}
|
|
458
460
|
// finds any records in the specified table
|
|
@@ -466,10 +468,10 @@ async function replaceArrayValues(db, value, tableAndColumnToChange) {
|
|
|
466
468
|
.updateTable(table)
|
|
467
469
|
.set({
|
|
468
470
|
[column]: tableAndColumnToChange.behavior === 'remove'
|
|
469
|
-
? sql
|
|
470
|
-
: sql
|
|
471
|
+
? sql `array_remove(${sql.ref(column)}, ${value})`
|
|
472
|
+
: sql `array_replace(${sql.ref(column)}, ${value}, ${tableAndColumnToChange.replaceWith})`,
|
|
471
473
|
})
|
|
472
|
-
.where(sql
|
|
474
|
+
.where(sql `${value} = ANY(${sql.ref(column)})`)
|
|
473
475
|
.execute();
|
|
474
476
|
}
|
|
475
477
|
// finds any records in the specified table
|
|
@@ -489,20 +491,20 @@ async function replaceNonArrayValues(db, value, tableAndColumnToChange) {
|
|
|
489
491
|
async function updateTableColumnToNewEnumArrayType(db, enumName, tableAndColumnToChange) {
|
|
490
492
|
const { table, column } = tableAndColumnToChange;
|
|
491
493
|
await sql `
|
|
492
|
-
ALTER TABLE ${sql.
|
|
493
|
-
ALTER ${sql.
|
|
494
|
-
TYPE ${sql.
|
|
495
|
-
USING ${sql.
|
|
494
|
+
ALTER TABLE ${sql.table(table)}
|
|
495
|
+
ALTER COLUMN ${sql.ref(column)}
|
|
496
|
+
TYPE ${sql.ref(enumName)}[]
|
|
497
|
+
USING ${sql.ref(column)}::${sql.ref(enumName)}[];
|
|
496
498
|
`.execute(db);
|
|
497
499
|
}
|
|
498
500
|
// updates the table's targeted column to the newly-created enum
|
|
499
501
|
async function updateTableColumnToNewEnumType(db, enumName, tableAndColumnToChange) {
|
|
500
502
|
const { table, column } = tableAndColumnToChange;
|
|
501
503
|
await sql `
|
|
502
|
-
ALTER TABLE ${sql.
|
|
503
|
-
ALTER ${sql.
|
|
504
|
-
TYPE ${sql.
|
|
505
|
-
USING ${sql.
|
|
504
|
+
ALTER TABLE ${sql.table(table)}
|
|
505
|
+
ALTER COLUMN ${sql.ref(column)}
|
|
506
|
+
TYPE ${sql.ref(enumName)}
|
|
507
|
+
USING ${sql.ref(column)}::${sql.ref(enumName)};
|
|
506
508
|
`.execute(db);
|
|
507
509
|
}
|
|
508
510
|
function computedTemporaryType(array) {
|
|
@@ -115,16 +115,38 @@ export default class Decorators {
|
|
|
115
115
|
*
|
|
116
116
|
* // automatically sets `myEncryptedPhone` to the encrypted value that
|
|
117
117
|
* // `phone` is set to new/create/update, e.g., `await user.update({ phone })`
|
|
118
|
-
* @deco.Encrypted('myEncryptedPhone)
|
|
118
|
+
* @deco.Encrypted('myEncryptedPhone')
|
|
119
119
|
* public phone: string
|
|
120
|
+
*
|
|
121
|
+
* // automatically sets `myEncryptedMfaSettings` to the encrypted value that
|
|
122
|
+
* // `mfaSettings` is set to in new/create/update, and declares the
|
|
123
|
+
* // value's OpenAPI shape, since it is not a string
|
|
124
|
+
* @deco.Encrypted({
|
|
125
|
+
* column: 'myEncryptedMfaSettings',
|
|
126
|
+
* openapi: { type: ['object', 'null'], properties: { token: 'string' } },
|
|
127
|
+
* })
|
|
128
|
+
* public mfaSettings: { token: string } | null
|
|
120
129
|
* }
|
|
121
130
|
* ```
|
|
122
131
|
*
|
|
123
|
-
*
|
|
132
|
+
* The decorated property may hold any JSON-serializable value. Its OpenAPI
|
|
133
|
+
* shape is what Psychic uses to cast the property in params and to render
|
|
134
|
+
* it in request body and serializer OpenAPI. Without an `openapi` option,
|
|
135
|
+
* that shape is `'string'`, or `['string', 'null']` when the backing column
|
|
136
|
+
* allows null. Pass `openapi` when the property holds a non-string value,
|
|
137
|
+
* such as an object, a number, or an array; it accepts any shape
|
|
138
|
+
* `@deco.Virtual` accepts. The declared shape is used exactly as given and
|
|
139
|
+
* replaces the nullability inferred from the backing column, so when the
|
|
140
|
+
* backing column allows null, include `'null'` in the shape, e.g.
|
|
141
|
+
* `type: ['object', 'null']`.
|
|
142
|
+
*
|
|
143
|
+
* @param columnOrOptions - Either the backing column's name, or an options object:
|
|
144
|
+
* - `column` — the backing column's name; if omitted, then 'encrypted' is prepended to the Pascal cased version of the decorated field
|
|
145
|
+
* - `openapi` — the OpenAPI shape of the decorated property's value; if omitted, the property's shape is a string (see above)
|
|
124
146
|
* @returns An Encrypted decorator
|
|
125
147
|
*/
|
|
126
|
-
Encrypted(
|
|
127
|
-
return Encrypted(
|
|
148
|
+
Encrypted(columnOrOptions) {
|
|
149
|
+
return Encrypted(columnOrOptions);
|
|
128
150
|
}
|
|
129
151
|
/**
|
|
130
152
|
* The Scope decorator decorates a static method that accepts
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import DreamApp from '../../dream-app/index.js';
|
|
2
|
+
import defaultEncryptedColumnName from '../../encrypt/defaultEncryptedColumnName.js';
|
|
1
3
|
import InternalEncrypt from '../../encrypt/InternalEncrypt.js';
|
|
2
4
|
import DoNotSetEncryptedFieldsDirectly from '../../errors/DoNotSetEncryptedFieldsDirectly.js';
|
|
3
5
|
import columnAllowsNull from '../../helpers/db/columnAllowsNull.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
export default function Encrypted(columnOrOptions) {
|
|
7
|
+
const { column, openapi } = typeof columnOrOptions === 'string' ? { column: columnOrOptions } : (columnOrOptions ?? {});
|
|
6
8
|
return function (_, context) {
|
|
7
9
|
const key = context.name;
|
|
8
|
-
const encryptedKey =
|
|
10
|
+
const encryptedKey = column || defaultEncryptedColumnName(key);
|
|
11
|
+
// records whose legacy-key listeners for this property are running; a
|
|
12
|
+
// read of the property on one of them returns its value without
|
|
13
|
+
// dispatching the event again, so a listener that reads the property
|
|
14
|
+
// does not recurse
|
|
15
|
+
const recordsDispatchingLegacyKeyUsed = new WeakSet();
|
|
9
16
|
context.addInitializer(function () {
|
|
10
17
|
const dreamClass = this.constructor;
|
|
11
18
|
if (!dreamClass['globallyInitializingDecorators']) {
|
|
@@ -30,7 +37,7 @@ export default function Encrypted(encryptedColumnName) {
|
|
|
30
37
|
;
|
|
31
38
|
dreamClass['virtualAttributes'].push({
|
|
32
39
|
property: key,
|
|
33
|
-
type: columnAllowsNull(dreamClass, encryptedKey) ? ['string', 'null'] : 'string',
|
|
40
|
+
type: openapi ?? (columnAllowsNull(dreamClass, encryptedKey) ? ['string', 'null'] : 'string'),
|
|
34
41
|
});
|
|
35
42
|
if (!Object.getOwnPropertyDescriptor(dreamClass, 'explicitUnsafeParamColumns')) {
|
|
36
43
|
// This pattern allows `explicitUnsafeParamColumns` on a base STI class and on
|
|
@@ -56,7 +63,22 @@ export default function Encrypted(encryptedColumnName) {
|
|
|
56
63
|
const dreamPrototype = Object.getPrototypeOf(this);
|
|
57
64
|
Object.defineProperty(dreamPrototype, key, {
|
|
58
65
|
get() {
|
|
59
|
-
|
|
66
|
+
const dream = this;
|
|
67
|
+
return InternalEncrypt.decryptColumn(this.getAttribute(encryptedKey), {
|
|
68
|
+
onLegacyKeyUsed: () => {
|
|
69
|
+
if (recordsDispatchingLegacyKeyUsed.has(dream))
|
|
70
|
+
return;
|
|
71
|
+
recordsDispatchingLegacyKeyUsed.add(dream);
|
|
72
|
+
try {
|
|
73
|
+
DreamApp.getOrFail().specialHooks.encryptionLegacyKeyUsed.forEach(fn => {
|
|
74
|
+
fn({ dream, property: key, encryptedColumnName: encryptedKey });
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
recordsDispatchingLegacyKeyUsed.delete(dream);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
});
|
|
60
82
|
},
|
|
61
83
|
set(val) {
|
|
62
84
|
/**
|
|
@@ -74,7 +96,14 @@ export default function Encrypted(encryptedColumnName) {
|
|
|
74
96
|
*/
|
|
75
97
|
if (this.columnSetterGuardActivated)
|
|
76
98
|
return;
|
|
77
|
-
|
|
99
|
+
// On a persisted record, the ciphertext as loaded or last saved is
|
|
100
|
+
// kept when the current key opens it to the assigned value, so
|
|
101
|
+
// assigning a property the plaintext it already holds leaves the
|
|
102
|
+
// record clean; any other value is encrypted fresh. Either way the
|
|
103
|
+
// value is ciphertext Dream produced, so it bypasses setAttribute's
|
|
104
|
+
// check that a backing column receives only decryptable ciphertext
|
|
105
|
+
const dream = this;
|
|
106
|
+
this.setAttributeUnchecked(encryptedKey, InternalEncrypt.reuseOrEncryptColumn(val, dream.isPersisted ? dream['frozenAttributes'][encryptedKey] : undefined));
|
|
78
107
|
},
|
|
79
108
|
configurable: false,
|
|
80
109
|
});
|
|
@@ -498,12 +498,12 @@ export default class DreamClassTransactionBuilder {
|
|
|
498
498
|
* 5. associations loading associations loading associations could result in exponential amounts of data; in those cases, `.preload(...).findEach(...)` avoids instantiating massive amounts of data at once
|
|
499
499
|
*
|
|
500
500
|
* ```ts
|
|
501
|
-
* const user = await User.txn(txn).leftJoinPreload('posts', 'comments', {
|
|
501
|
+
* const user = await User.txn(txn).leftJoinPreload('posts', 'comments', { and: { visibility: 'public' } }, 'replies').first()
|
|
502
502
|
* console.log(user.posts[0].comments[0].replies)
|
|
503
503
|
* // [Reply{id: 1}, Reply{id: 2}]
|
|
504
504
|
* ```
|
|
505
505
|
*
|
|
506
|
-
* @param args - A chain of association names and
|
|
506
|
+
* @param args - A chain of association names and and/andNot/andAny clauses
|
|
507
507
|
* @returns A query for this model with the include statement applied
|
|
508
508
|
*/
|
|
509
509
|
leftJoinPreload(...args) {
|
|
@@ -521,13 +521,13 @@ export default class DreamClassTransactionBuilder {
|
|
|
521
521
|
*
|
|
522
522
|
* ```ts
|
|
523
523
|
* await ApplicationModel.transaction(async txn => {
|
|
524
|
-
* const user = await User.txn(txn).preload('posts', 'comments', {
|
|
524
|
+
* const user = await User.txn(txn).preload('posts', 'comments', { and: { visibility: 'public' } }, 'replies').first()
|
|
525
525
|
* console.log(user.posts[0].comments[0].replies)
|
|
526
526
|
* // [Reply{id: 1}, Reply{id: 2}]
|
|
527
527
|
* })
|
|
528
528
|
* ```
|
|
529
529
|
*
|
|
530
|
-
* @param args - A chain of association names and
|
|
530
|
+
* @param args - A chain of association names and and/andNot/andAny clauses
|
|
531
531
|
* @returns A query for this model with the preload statement applied
|
|
532
532
|
*/
|
|
533
533
|
preload(...args) {
|
|
@@ -591,7 +591,7 @@ export default class DreamClassTransactionBuilder {
|
|
|
591
591
|
* })
|
|
592
592
|
* ```
|
|
593
593
|
*
|
|
594
|
-
* @param args - A chain of association names and
|
|
594
|
+
* @param args - A chain of association names and and/andNot/andAny clauses
|
|
595
595
|
* @returns A Query for this model with the inner join clause applied
|
|
596
596
|
*/
|
|
597
597
|
innerJoin(...args) {
|
|
@@ -607,7 +607,7 @@ export default class DreamClassTransactionBuilder {
|
|
|
607
607
|
* })
|
|
608
608
|
* ```
|
|
609
609
|
*
|
|
610
|
-
* @param args - A chain of association names and
|
|
610
|
+
* @param args - A chain of association names and and/andNot/andAny clauses
|
|
611
611
|
* @returns A Query for this model with the left join clause applied
|
|
612
612
|
*/
|
|
613
613
|
leftJoin(...args) {
|
|
@@ -3,6 +3,7 @@ import CannotCreateAssociationOnUnpersistedDream from '../errors/associations/Ca
|
|
|
3
3
|
import CannotDestroyAssociationOnUnpersistedDream from '../errors/associations/CannotDestroyAssociationOnUnpersistedDream.js';
|
|
4
4
|
import CannotUpdateAssociationOnUnpersistedDream from '../errors/associations/CannotUpdateAssociationOnUnpersistedDream.js';
|
|
5
5
|
import RecordNotFound from '../errors/RecordNotFound.js';
|
|
6
|
+
import assertRecognizedAssociationConditionKeys, { DESTROY_ASSOCIATION_OPTION_KEYS, UPDATE_ASSOCIATION_OPTION_KEYS, } from './internal/associations/assertRecognizedAssociationConditionKeys.js';
|
|
6
7
|
import associationQuery from './internal/associations/associationQuery.js';
|
|
7
8
|
import associationUpdateQuery from './internal/associations/associationUpdateQuery.js';
|
|
8
9
|
import createAssociation from './internal/associations/createAssociation.js';
|
|
@@ -33,26 +34,29 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
33
34
|
/**
|
|
34
35
|
* Loads the requested associations upon execution
|
|
35
36
|
*
|
|
37
|
+
* **IMPORTANT:** `load().execute()` returns a **new clone** of the model with the
|
|
38
|
+
* associations loaded. It does NOT modify the original instance.
|
|
39
|
+
*
|
|
36
40
|
* NOTE: {@link Dream#preload} is often a preferrable way of achieving the
|
|
37
41
|
* same goal.
|
|
38
42
|
*
|
|
39
43
|
* ```ts
|
|
40
44
|
* await ApplicationModel.transaction(async txn => {
|
|
41
|
-
* await user
|
|
45
|
+
* const loadedUser = await user
|
|
42
46
|
* .txn(txn)
|
|
43
|
-
* .load('posts', { body: ops.ilike('%hello world%') }, 'comments', 'replies')
|
|
47
|
+
* .load('posts', { and: { body: ops.ilike('%hello world%') } }, 'comments', 'replies')
|
|
44
48
|
* .load('images')
|
|
45
49
|
* .execute()
|
|
46
50
|
*
|
|
47
|
-
*
|
|
51
|
+
* loadedUser.posts[0].comments[0].replies[0]
|
|
48
52
|
* // Reply{}
|
|
49
53
|
*
|
|
50
|
-
*
|
|
54
|
+
* loadedUser.images[0]
|
|
51
55
|
* // Image{}
|
|
52
56
|
* })
|
|
53
57
|
* ```
|
|
54
58
|
*
|
|
55
|
-
* @param args - A
|
|
59
|
+
* @param args - A chain of association names and and/andNot/andAny clauses
|
|
56
60
|
* @returns A chainable LoadBuilder instance
|
|
57
61
|
*/
|
|
58
62
|
load(...args) {
|
|
@@ -112,32 +116,34 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
112
116
|
* Load each specified association using a single SQL query.
|
|
113
117
|
* See {@link #load} for loading in separate queries.
|
|
114
118
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
119
|
+
* **IMPORTANT:** Like `load()`, `leftJoinLoad().execute()` returns a **new clone** of the
|
|
120
|
+
* model with associations loaded. It does NOT modify the original instance.
|
|
121
|
+
*
|
|
122
|
+
* Note: since leftJoinLoad loads via single query, it has
|
|
123
|
+
* some downsides that may be avoided using {@link #load}:
|
|
117
124
|
* 1. `limit` and `offset` will be automatically removed
|
|
118
125
|
* 2. `through` associations will bring additional namespaces into the query that can conflict with through associations from other associations, creating an invalid query
|
|
119
|
-
* 3. each nested association will result in an additional record which duplicates data from the outer record. E.g., given `.
|
|
126
|
+
* 3. each nested association will result in an additional record which duplicates data from the outer record. E.g., given `.leftJoinLoad('a', 'b', 'c')`, if each `a` has 10 `b` and each `b` has 10 `c`, then for one `a`, 100 records will be returned, each of which has all of the columns of `a`. `.load('a', 'b', 'c')` would perform three separate SQL queries, but the data for a single `a` would only be returned once.
|
|
120
127
|
* 4. the individual query becomes more complex the more associations are included
|
|
121
128
|
* 5. associations loading associations loading associations could result in exponential amounts of data; in those cases, `.load(...).findEach(...)` avoids instantiating massive amounts of data at once
|
|
122
|
-
* Loads the requested associations upon execution
|
|
123
|
-
*
|
|
124
|
-
* NOTE: {@link Dream#leftJoinPreload} is often a preferrable way of achieving the
|
|
125
|
-
* same goal.
|
|
126
129
|
*
|
|
127
130
|
* ```ts
|
|
128
|
-
* await
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
131
|
+
* await ApplicationModel.transaction(async txn => {
|
|
132
|
+
* const loadedUser = await user
|
|
133
|
+
* .txn(txn)
|
|
134
|
+
* .leftJoinLoad('posts', { and: { body: ops.ilike('%hello world%') } }, 'comments', 'replies')
|
|
135
|
+
* .leftJoinLoad('images')
|
|
136
|
+
* .execute()
|
|
132
137
|
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
138
|
+
* loadedUser.posts[0].comments[0].replies[0]
|
|
139
|
+
* // Reply{}
|
|
135
140
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
141
|
+
* loadedUser.images[0]
|
|
142
|
+
* // Image{}
|
|
143
|
+
* })
|
|
138
144
|
* ```
|
|
139
145
|
*
|
|
140
|
-
* @param args - A
|
|
146
|
+
* @param args - A chain of association names and and/andNot/andAny clauses
|
|
141
147
|
* @returns A chainable LeftJoinLoadBuilder instance
|
|
142
148
|
*/
|
|
143
149
|
leftJoinLoad(...args) {
|
|
@@ -153,7 +159,7 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
153
159
|
* })
|
|
154
160
|
* ```
|
|
155
161
|
*
|
|
156
|
-
* @param args - A chain of association names and
|
|
162
|
+
* @param args - A chain of association names and and/andNot/andAny clauses
|
|
157
163
|
* @returns A Query for this model with the inner join clause applied
|
|
158
164
|
*/
|
|
159
165
|
innerJoin(...args) {
|
|
@@ -169,7 +175,7 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
169
175
|
* })
|
|
170
176
|
* ```
|
|
171
177
|
*
|
|
172
|
-
* @param args - A chain of association names and
|
|
178
|
+
* @param args - A chain of association names and and/andNot/andAny clauses
|
|
173
179
|
* @returns A Query for this model with the left join clause applied
|
|
174
180
|
*/
|
|
175
181
|
leftJoin(...args) {
|
|
@@ -252,6 +258,12 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
252
258
|
* See {@link Dream.save | save} for details on
|
|
253
259
|
* the side effects of saving.
|
|
254
260
|
*
|
|
261
|
+
* A column whose value is still `undefined` after setters run is
|
|
262
|
+
* skipped, not written (see {@link Dream.save | save}), with two
|
|
263
|
+
* `@Encrypted` exceptions: `undefined` for an encrypted property (e.g.
|
|
264
|
+
* `secret`) is encrypted to `null`, so it clears the column, and
|
|
265
|
+
* `undefined` for its backing column (e.g. `encryptedSecret`) throws.
|
|
266
|
+
*
|
|
255
267
|
* NOTE:
|
|
256
268
|
* To bypass custom-defined setters, use {@link Dream.updateAttributes | updateAttributes} instead.
|
|
257
269
|
*
|
|
@@ -279,6 +291,18 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
279
291
|
* See {@link Dream.save | save} for details on
|
|
280
292
|
* the side effects of saving.
|
|
281
293
|
*
|
|
294
|
+
* An encrypted property (e.g. `secret`) is still encrypted. Its
|
|
295
|
+
* `@Encrypted` backing column (e.g. `encryptedSecret`) accepts only
|
|
296
|
+
* ciphertext that decrypts with this app's column encryption keys, or
|
|
297
|
+
* `null`; anything else throws before anything is saved (see
|
|
298
|
+
* {@link Dream.setAttribute | setAttribute}).
|
|
299
|
+
*
|
|
300
|
+
* A column whose value is still `undefined` after setters run is
|
|
301
|
+
* skipped, not written (see {@link Dream.save | save}), with two
|
|
302
|
+
* `@Encrypted` exceptions: `undefined` for an encrypted property (e.g.
|
|
303
|
+
* `secret`) is encrypted to `null`, so it clears the column, and
|
|
304
|
+
* `undefined` for its backing column (e.g. `encryptedSecret`) throws.
|
|
305
|
+
*
|
|
282
306
|
* NOTE:
|
|
283
307
|
* To update the values without bypassing any custom-defined
|
|
284
308
|
* setters, use {@link Dream.update | update} instead.
|
|
@@ -329,6 +353,10 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
329
353
|
* Upon updating an instance, the update timestamp (if defined)
|
|
330
354
|
* will be updated on the model.
|
|
331
355
|
*
|
|
356
|
+
* A column whose value is `undefined` is skipped, not written: a new
|
|
357
|
+
* record gets the column's database default, and a persisted record
|
|
358
|
+
* keeps the value stored for it. Set a column to `null` to clear it.
|
|
359
|
+
*
|
|
332
360
|
* ```ts
|
|
333
361
|
* const user = User.new({ email: 'how@yadoin' })
|
|
334
362
|
*
|
|
@@ -386,8 +414,8 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
386
414
|
}
|
|
387
415
|
/**
|
|
388
416
|
* Updates all records matching the association with
|
|
389
|
-
* the provided attributes. If
|
|
390
|
-
*
|
|
417
|
+
* the provided attributes. If an and, andNot or andAny
|
|
418
|
+
* statement is passed, it will be applied to the query
|
|
391
419
|
* before updating.
|
|
392
420
|
*
|
|
393
421
|
* ```ts
|
|
@@ -402,13 +430,16 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
402
430
|
* @param associationName - The name of the association to update
|
|
403
431
|
* @param attributes - The attributes to update on the association
|
|
404
432
|
* @param updateAssociationOptions - Options for updating the association
|
|
405
|
-
* @param updateAssociationOptions.
|
|
433
|
+
* @param updateAssociationOptions.and - Optional and statement to apply to query before updating
|
|
434
|
+
* @param updateAssociationOptions.andNot - Optional andNot statement to apply to query before updating
|
|
435
|
+
* @param updateAssociationOptions.andAny - Optional andAny statement to apply to query before updating
|
|
406
436
|
* @param updateAssociationOptions.bypassAllDefaultScopes - If true, bypasses user-removable default scopes when updating the association; an STI child's reserved discriminator remains enforced, including when associations are loaded. Defaults to false
|
|
407
437
|
* @param updateAssociationOptions.defaultScopesToBypass - An array of default scope names to bypass when updating the association. Defaults to an empty array
|
|
408
438
|
* @param updateAssociationOptions.skipHooks - If true, skips applying model hooks during the update operation. Defaults to false
|
|
409
439
|
* @returns The number of updated records
|
|
410
440
|
*/
|
|
411
441
|
async updateAssociation(associationName, attributes, updateAssociationOptions) {
|
|
442
|
+
assertRecognizedAssociationConditionKeys(updateAssociationOptions, UPDATE_ASSOCIATION_OPTION_KEYS, associationName, 'updateAssociation');
|
|
412
443
|
if (this.dreamInstance.isNewRecord)
|
|
413
444
|
throw new CannotUpdateAssociationOnUnpersistedDream(this.dreamInstance, associationName);
|
|
414
445
|
return await associationUpdateQuery(this.dreamInstance, this.dreamTransaction, associationName, {
|
|
@@ -465,6 +496,7 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
465
496
|
* @returns The number of records deleted
|
|
466
497
|
*/
|
|
467
498
|
async destroyAssociation(associationName, options) {
|
|
499
|
+
assertRecognizedAssociationConditionKeys(options, DESTROY_ASSOCIATION_OPTION_KEYS, associationName, 'destroyAssociation');
|
|
468
500
|
if (this.dreamInstance.isNewRecord)
|
|
469
501
|
throw new CannotDestroyAssociationOnUnpersistedDream(this.dreamInstance, associationName);
|
|
470
502
|
return await destroyAssociation(this.dreamInstance, this.dreamTransaction, associationName, {
|
|
@@ -503,6 +535,7 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
503
535
|
* @returns The number of records deleted
|
|
504
536
|
*/
|
|
505
537
|
async reallyDestroyAssociation(associationName, options) {
|
|
538
|
+
assertRecognizedAssociationConditionKeys(options, DESTROY_ASSOCIATION_OPTION_KEYS, associationName, 'reallyDestroyAssociation');
|
|
506
539
|
if (this.dreamInstance.isNewRecord)
|
|
507
540
|
throw new CannotDestroyAssociationOnUnpersistedDream(this.dreamInstance, associationName);
|
|
508
541
|
return await destroyAssociation(this.dreamInstance, this.dreamTransaction, associationName, {
|
|
@@ -521,7 +554,9 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
521
554
|
* will also be undeleted.
|
|
522
555
|
*
|
|
523
556
|
* ```ts
|
|
524
|
-
* await
|
|
557
|
+
* await ApplicationModel.transaction(async txn => {
|
|
558
|
+
* await user.txn(txn).undestroyAssociation('posts', { and: { body: 'hello world' } })
|
|
559
|
+
* })
|
|
525
560
|
* ```
|
|
526
561
|
*
|
|
527
562
|
* @param associationName - The name of the association to undestroy
|
|
@@ -536,6 +571,7 @@ export default class DreamInstanceTransactionBuilder {
|
|
|
536
571
|
* @returns The number of records undestroyed
|
|
537
572
|
*/
|
|
538
573
|
async undestroyAssociation(associationName, options) {
|
|
574
|
+
assertRecognizedAssociationConditionKeys(options, DESTROY_ASSOCIATION_OPTION_KEYS, associationName, 'undestroyAssociation');
|
|
539
575
|
return await undestroyAssociation(this.dreamInstance, this.dreamTransaction, associationName, {
|
|
540
576
|
...undestroyOptions(options),
|
|
541
577
|
joinAndStatements: {
|
|
@@ -28,13 +28,13 @@ export default class LeftJoinLoadBuilder {
|
|
|
28
28
|
return this;
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Attaches a
|
|
31
|
+
* Attaches a leftJoinLoad statement to the load builder
|
|
32
32
|
*
|
|
33
33
|
* ```ts
|
|
34
34
|
* const user = await User.firstOrFail()
|
|
35
35
|
* await user
|
|
36
|
-
* .
|
|
37
|
-
* .
|
|
36
|
+
* .leftJoinLoad('settings')
|
|
37
|
+
* .leftJoinLoad('posts', 'comments', 'replies', ['image', 'localizedText'])
|
|
38
38
|
* .execute()
|
|
39
39
|
* ```
|
|
40
40
|
*/
|