@rvoh/dream 2.27.0 → 2.28.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/dist/cjs/src/Dream.js +63 -1
- package/dist/cjs/src/db/errors.js +4 -0
- package/dist/cjs/src/decorators/field/Encrypted.js +12 -0
- package/dist/cjs/src/decorators/field/sortable/Sortable.js +43 -118
- package/dist/cjs/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
- package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
- package/dist/cjs/src/decorators/field/sortable/helpers/setPosition.js +90 -46
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
- package/dist/cjs/src/decorators/field/sortable/helpers/validPosition.js +21 -4
- package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
- package/dist/cjs/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
- package/dist/cjs/src/dream/Query.js +73 -11
- package/dist/cjs/src/dream/QueryDriver/Base.js +75 -2
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +11 -4
- package/dist/cjs/src/dream/QueryDriver/Postgres.js +74 -0
- package/dist/cjs/src/dream/internal/destroyDream.js +60 -6
- package/dist/cjs/src/dream/internal/saveDream.js +109 -7
- package/dist/cjs/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
- package/dist/cjs/src/dream/internal/softDeleteDream.js +13 -1
- package/dist/cjs/src/dream/internal/undestroyDream.js +56 -8
- package/dist/cjs/src/dream-app/index.js +21 -0
- package/dist/cjs/src/encrypt/assertNoEncryptedColumnWrites.js +62 -0
- package/dist/cjs/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
- package/dist/cjs/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
- package/dist/cjs/src/errors/SortableScopeDidNotStabilize.js +39 -0
- package/dist/cjs/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
- package/dist/cjs/src/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.js +31 -0
- package/dist/cjs/src/package-exports/errors.js +3 -0
- package/dist/esm/src/Dream.js +63 -1
- package/dist/esm/src/db/errors.js +4 -0
- package/dist/esm/src/decorators/field/Encrypted.js +12 -0
- package/dist/esm/src/decorators/field/sortable/Sortable.js +43 -118
- package/dist/esm/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.js +38 -0
- package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js +183 -0
- package/dist/esm/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js +40 -0
- package/dist/esm/src/decorators/field/sortable/helpers/canonicalScopeValue.js +194 -0
- package/dist/esm/src/decorators/field/sortable/helpers/clearCachedSortableValues.js +18 -0
- package/dist/esm/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js +9 -9
- package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.js +20 -0
- package/dist/esm/src/decorators/field/sortable/helpers/filterQueryToScopeValues.js +32 -0
- package/dist/esm/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.js +34 -0
- package/dist/esm/src/decorators/field/sortable/helpers/performSortableDestroyWork.js +21 -0
- package/dist/esm/src/decorators/field/sortable/helpers/performSortablePositionWork.js +36 -0
- package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js +56 -0
- package/dist/esm/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.js +84 -0
- package/dist/esm/src/decorators/field/sortable/helpers/resortAllRecords.js +186 -29
- package/dist/esm/src/decorators/field/sortable/helpers/setPosition.js +90 -46
- package/dist/esm/src/decorators/field/sortable/helpers/sortableRowCache.js +73 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeColumns.js +42 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKey.js +99 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableScopeLockKeys.js +72 -0
- package/dist/esm/src/decorators/field/sortable/helpers/sortableSnapshot.js +101 -0
- package/dist/esm/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.js +86 -0
- package/dist/esm/src/decorators/field/sortable/helpers/validPosition.js +21 -4
- package/dist/esm/src/decorators/field/sortable/hooks/afterSortableDestroy.js +24 -7
- package/dist/esm/src/decorators/field/sortable/hooks/afterSortableUpdate.js +10 -0
- package/dist/esm/src/dream/Query.js +73 -11
- package/dist/esm/src/dream/QueryDriver/Base.js +75 -2
- package/dist/esm/src/dream/QueryDriver/Kysely.js +11 -4
- package/dist/esm/src/dream/QueryDriver/Postgres.js +74 -0
- package/dist/esm/src/dream/internal/destroyDream.js +60 -6
- package/dist/esm/src/dream/internal/saveDream.js +109 -7
- package/dist/esm/src/dream/internal/similarity/SimilarityBuilder.js +19 -4
- package/dist/esm/src/dream/internal/softDeleteDream.js +13 -1
- package/dist/esm/src/dream/internal/undestroyDream.js +56 -8
- package/dist/esm/src/dream-app/index.js +21 -0
- package/dist/esm/src/encrypt/assertNoEncryptedColumnWrites.js +62 -0
- package/dist/esm/src/errors/SortableBatchRequiresTooManyScopeLocks.js +57 -0
- package/dist/esm/src/errors/SortableRequiresAdvisoryTransactionLocks.js +26 -0
- package/dist/esm/src/errors/SortableScopeDidNotStabilize.js +39 -0
- package/dist/esm/src/errors/SortableScopeLockWaitTimedOut.js +50 -0
- package/dist/esm/src/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.js +31 -0
- package/dist/esm/src/package-exports/errors.js +3 -0
- package/dist/types/src/Dream.d.ts +56 -0
- package/dist/types/src/db/errors.d.ts +2 -0
- package/dist/types/src/decorators/field/sortable/Sortable.d.ts +33 -0
- package/dist/types/src/decorators/field/sortable/helpers/acquireSortableScopeLocks.d.ts +25 -0
- package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.d.ts +91 -0
- package/dist/types/src/decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.d.ts +31 -0
- package/dist/types/src/decorators/field/sortable/helpers/canonicalScopeValue.d.ts +85 -0
- package/dist/types/src/decorators/field/sortable/helpers/clearCachedSortableValues.d.ts +13 -0
- package/dist/types/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.d.ts +8 -1
- package/dist/types/src/decorators/field/sortable/helpers/filterQueryToInstanceScope.d.ts +24 -0
- package/dist/types/src/decorators/field/sortable/helpers/filterQueryToScopeValues.d.ts +22 -0
- package/dist/types/src/decorators/field/sortable/helpers/heldSortableScopeLockKeys.d.ts +27 -0
- package/dist/types/src/decorators/field/sortable/helpers/performSortableDestroyWork.d.ts +14 -0
- package/dist/types/src/decorators/field/sortable/helpers/performSortablePositionWork.d.ts +15 -0
- package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.d.ts +41 -0
- package/dist/types/src/decorators/field/sortable/helpers/prepareSortableFieldsForSave.d.ts +35 -0
- package/dist/types/src/decorators/field/sortable/helpers/resortAllRecords.d.ts +32 -0
- package/dist/types/src/decorators/field/sortable/helpers/setPosition.d.ts +27 -11
- package/dist/types/src/decorators/field/sortable/helpers/sortableRowCache.d.ts +48 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableScopeColumns.d.ts +30 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKey.d.ts +44 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableScopeLockKeys.d.ts +51 -0
- package/dist/types/src/decorators/field/sortable/helpers/sortableSnapshot.d.ts +92 -0
- package/dist/types/src/decorators/field/sortable/helpers/stabilizeSortableScopeLocks.d.ts +59 -0
- package/dist/types/src/decorators/field/sortable/helpers/validPosition.d.ts +14 -2
- package/dist/types/src/decorators/field/sortable/hooks/afterSortableCreate.d.ts +1 -1
- package/dist/types/src/decorators/field/sortable/hooks/afterSortableDestroy.d.ts +3 -1
- package/dist/types/src/decorators/field/sortable/hooks/afterSortableUpdate.d.ts +1 -1
- package/dist/types/src/dream/Query.d.ts +47 -9
- package/dist/types/src/dream/QueryDriver/Base.d.ts +61 -1
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +6 -2
- package/dist/types/src/dream/QueryDriver/Postgres.d.ts +40 -0
- package/dist/types/src/dream/internal/softDeleteDream.d.ts +7 -1
- package/dist/types/src/dream-app/index.d.ts +18 -2
- package/dist/types/src/encrypt/assertNoEncryptedColumnWrites.d.ts +27 -0
- package/dist/types/src/errors/SortableBatchRequiresTooManyScopeLocks.d.ts +37 -0
- package/dist/types/src/errors/SortableRequiresAdvisoryTransactionLocks.d.ts +13 -0
- package/dist/types/src/errors/SortableScopeDidNotStabilize.d.ts +23 -0
- package/dist/types/src/errors/SortableScopeLockWaitTimedOut.d.ts +34 -0
- package/dist/types/src/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.d.ts +8 -0
- package/dist/types/src/package-exports/errors.d.ts +3 -0
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +88 -41
- package/docs/classes/db.PostgresQueryDriver.html +82 -42
- package/docs/classes/db.QueryDriverBase.html +86 -39
- 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.SortableBatchRequiresTooManyScopeLocks.html +61 -0
- package/docs/classes/errors.SortableScopeDidNotStabilize.html +48 -0
- package/docs/classes/errors.SortableScopeLockWaitTimedOut.html +58 -0
- package/docs/classes/errors.ValidationError.html +3 -3
- package/docs/classes/index.CalendarDate.html +33 -33
- package/docs/classes/index.ClockTime.html +32 -32
- package/docs/classes/index.ClockTimeTz.html +35 -35
- package/docs/classes/index.DateTime.html +86 -86
- package/docs/classes/index.Decorators.html +19 -19
- package/docs/classes/index.Dream.html +151 -124
- package/docs/classes/index.DreamApp.html +11 -10
- package/docs/classes/index.DreamTransaction.html +2 -2
- package/docs/classes/index.Env.html +2 -2
- package/docs/classes/index.Query.html +124 -98
- package/docs/classes/system.CliFileWriter.html +4 -4
- package/docs/classes/system.DreamBin.html +2 -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 +11 -11
- package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
- package/docs/classes/system.PathHelpers.html +3 -3
- package/docs/classes/utils.Encrypt.html +3 -3
- 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 +2 -2
- package/docs/functions/utils.sortBy.html +1 -1
- package/docs/functions/utils.sortObjectByKey.html +1 -1
- package/docs/functions/utils.sortObjectByValue.html +1 -1
- package/docs/functions/utils.uncapitalize.html +1 -1
- package/docs/functions/utils.uniq.html +1 -1
- package/docs/hierarchy.html +1 -1
- package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
- package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
- package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
- package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
- package/docs/interfaces/types.BelongsToStatement.html +2 -2
- package/docs/interfaces/types.DecoratorContext.html +2 -2
- package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
- package/docs/interfaces/types.DreamAppOpts.html +2 -2
- package/docs/interfaces/types.DreamDbConfig.html +5 -5
- package/docs/interfaces/types.DurationObject.html +2 -2
- package/docs/interfaces/types.EncryptOptions.html +2 -2
- 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/errors.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.OpenapiSchemaCommonFields.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -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 +2 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.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 +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -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/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
- package/dist/cjs/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
- package/dist/cjs/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
- package/dist/esm/src/decorators/field/sortable/helpers/applySortableScopeToQuery.js +0 -13
- package/dist/esm/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.js +0 -7
- package/dist/esm/src/decorators/field/sortable/hooks/beforeSortableSave.js +0 -41
- package/dist/types/src/decorators/field/sortable/helpers/applySortableScopeToQuery.d.ts +0 -12
- package/dist/types/src/decorators/field/sortable/helpers/sortableQueryExcludingDream.d.ts +0 -12
- package/dist/types/src/decorators/field/sortable/hooks/beforeSortableSave.d.ts +0 -12
|
@@ -193,6 +193,69 @@ export default class QueryDriverBase {
|
|
|
193
193
|
connectionName) {
|
|
194
194
|
throw new Error('override openTestDatabaseLockSession in child class');
|
|
195
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* @internal
|
|
198
|
+
*
|
|
199
|
+
* Whether this driver implements the transaction-scoped advisory lock seam
|
|
200
|
+
* ({@link acquireAdvisoryTransactionLocks}). Adapters that can hold a
|
|
201
|
+
* key-addressed lock for the life of a transaction override this to `true`.
|
|
202
|
+
* It defaults to `false` so that a driver without the primitive fails loud
|
|
203
|
+
* for features that depend on it (today, the `Sortable` decorator) rather
|
|
204
|
+
* than silently racing on position values.
|
|
205
|
+
*/
|
|
206
|
+
static supportsAdvisoryTransactionLocks = false;
|
|
207
|
+
/**
|
|
208
|
+
* @internal
|
|
209
|
+
*
|
|
210
|
+
* Acquire exclusive, transaction-scoped locks on arbitrary keys, waiting
|
|
211
|
+
* until any other transaction holding one of them finishes. The locks are
|
|
212
|
+
* released when the enclosing transaction commits or rolls back — never by an
|
|
213
|
+
* explicit unlock — so callers only ever have to acquire (Postgres:
|
|
214
|
+
* `pg_advisory_xact_lock(bigint)`).
|
|
215
|
+
*
|
|
216
|
+
* Unlike {@link applyRowLock}, this does not depend on rows existing: it is
|
|
217
|
+
* the only way to serialize writers that are about to insert into the same
|
|
218
|
+
* logical group. The `Sortable` decorator uses it to make each sort scope's
|
|
219
|
+
* position computation exclusive, keyed on the physical database, table,
|
|
220
|
+
* position field and scope values.
|
|
221
|
+
*
|
|
222
|
+
* The seam takes the whole key set rather than one key at a time so that a
|
|
223
|
+
* driver able to express it can take them in a single round trip — the
|
|
224
|
+
* batched, row-locking query APIs can need one key per candidate record per
|
|
225
|
+
* sortable field. **Keys must be acquired in the order given**: callers sort
|
|
226
|
+
* them, and that shared order is what keeps two operations over the same
|
|
227
|
+
* scopes from deadlocking. A driver that has no bulk form implements this by
|
|
228
|
+
* looping over the keys in order.
|
|
229
|
+
*
|
|
230
|
+
* **The wait must be bounded.** A caller that finds a key held waits for the
|
|
231
|
+
* holder's transaction, and an implementation that waits forever turns an
|
|
232
|
+
* ordinary mistake — a sortable operation issued without the enclosing
|
|
233
|
+
* transaction, so that it waits on a key that transaction itself holds — into
|
|
234
|
+
* a permanently wedged connection that no deadlock detector can see, because
|
|
235
|
+
* only one side is waiting. So an implementation applies the
|
|
236
|
+
* `sortableScopeLockTimeout` DreamApp option (milliseconds) to the
|
|
237
|
+
* acquisition and raises `SortableScopeLockWaitTimedOut` when it elapses. A
|
|
238
|
+
* configured `0` means no bound, and is applied just as literally: the
|
|
239
|
+
* acquisition waits without bound even where the connection carries a
|
|
240
|
+
* wait bound of its own. The bound belongs to
|
|
241
|
+
* the acquisition alone: whatever lock-wait behavior the connection was
|
|
242
|
+
* configured with has to be in force again by the time this returns, so that
|
|
243
|
+
* the row locks {@link applyRowLock} takes are unaffected.
|
|
244
|
+
*
|
|
245
|
+
* Only ever called when {@link supportsAdvisoryTransactionLocks} is `true`;
|
|
246
|
+
* the base default throws.
|
|
247
|
+
*
|
|
248
|
+
* @param txn - the transaction the locks are scoped to
|
|
249
|
+
* @param keys - the 64-bit signed lock keys, in acquisition order
|
|
250
|
+
*/
|
|
251
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
252
|
+
static async acquireAdvisoryTransactionLocks(
|
|
253
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
254
|
+
txn,
|
|
255
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
256
|
+
keys) {
|
|
257
|
+
throw new Error('override acquireAdvisoryTransactionLocks in child class');
|
|
258
|
+
}
|
|
196
259
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
197
260
|
static async getColumnData(
|
|
198
261
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -507,9 +570,19 @@ export default class QueryDriverBase {
|
|
|
507
570
|
* @param dream - the dream instance you wish to destroy
|
|
508
571
|
* @param txn - a transaction to encapsulate, consistently provided by underlying dream mechanisms
|
|
509
572
|
* @param reallyDestroy - whether or not to reallyDestroy. If false, soft delete will be attempted when relevant
|
|
573
|
+
* @returns the number of rows this call removed (or soft deleted). Zero means
|
|
574
|
+
* the row was already gone, or that a `preventDeletion` veto stopped the
|
|
575
|
+
* write — callers distinguish the two by the veto flag, and use the count to
|
|
576
|
+
* decide whether there is anything for the after-destroy hooks to act on.
|
|
510
577
|
*/
|
|
511
|
-
// eslint-disable-next-line @typescript-eslint/require-await
|
|
512
|
-
static async destroyDream(
|
|
578
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
579
|
+
static async destroyDream(
|
|
580
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
581
|
+
dream,
|
|
582
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
583
|
+
txn,
|
|
584
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
585
|
+
reallyDestroy) {
|
|
513
586
|
throw new Error('implement destroyDream in child class');
|
|
514
587
|
}
|
|
515
588
|
static get syncDialect() {
|
|
@@ -265,21 +265,28 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
265
265
|
}
|
|
266
266
|
/**
|
|
267
267
|
* destroys a dream, possibly implementing soft delete if reallyDestroy is false
|
|
268
|
-
* and the record being deleted implements soft delete.
|
|
268
|
+
* and the record being deleted implements soft delete. Does nothing at all when a
|
|
269
|
+
* beforeDestroy hook called `preventDeletion`.
|
|
269
270
|
*
|
|
270
271
|
* @param dream - the dream instance you wish to destroy
|
|
271
272
|
* @param txn - a transaction to encapsulate, consistently provided by underlying dream mechanisms
|
|
272
273
|
* @param reallyDestroy - whether or not to reallyDestroy. If false, soft delete will be attempted when relevant
|
|
274
|
+
* @returns the number of rows removed — zero when a `preventDeletion` veto
|
|
275
|
+
* stopped the write, and zero when the row was already gone, which on the
|
|
276
|
+
* soft delete path includes a row that is already soft deleted
|
|
273
277
|
*/
|
|
274
278
|
static async destroyDream(dream, txn, reallyDestroy) {
|
|
279
|
+
if (dream['_preventDeletion'])
|
|
280
|
+
return 0;
|
|
275
281
|
if (shouldSoftDelete(dream, reallyDestroy)) {
|
|
276
|
-
await softDeleteDream(dream, txn);
|
|
282
|
+
return await softDeleteDream(dream, txn);
|
|
277
283
|
}
|
|
278
|
-
else
|
|
279
|
-
await txn.kyselyTransaction
|
|
284
|
+
else {
|
|
285
|
+
const results = await txn.kyselyTransaction
|
|
280
286
|
.deleteFrom(dream.table)
|
|
281
287
|
.where(dream['_primaryKey'], '=', dream.primaryKeyValue())
|
|
282
288
|
.execute();
|
|
289
|
+
return Number(results[0]?.numDeletedRows ?? 0);
|
|
283
290
|
}
|
|
284
291
|
}
|
|
285
292
|
/**
|
|
@@ -10,7 +10,9 @@ import pg from 'pg';
|
|
|
10
10
|
import { sql } from 'kysely';
|
|
11
11
|
import DreamCLI from '../../cli/index.js';
|
|
12
12
|
import { isPrimitiveDataType } from '../../db/dataTypes.js';
|
|
13
|
+
import { LOCK_NOT_AVAILABLE, pgErrorType } from '../../db/errors.js';
|
|
13
14
|
import DreamApp from '../../dream-app/index.js';
|
|
15
|
+
import SortableScopeLockWaitTimedOut from '../../errors/SortableScopeLockWaitTimedOut.js';
|
|
14
16
|
import camelize from '../../helpers/camelize.js';
|
|
15
17
|
import { findCitextArrayOid, findCorrespondingArrayOid, findEnumArrayOids, parsePostgresBigint, parsePostgresDate, parsePostgresDatetime, parsePostgresDatetimeTz, parsePostgresDecimal, parsePostgresTime, parsePostgresTimeTz, } from '../../helpers/customPgParsers.js';
|
|
16
18
|
import { testDatabasePoolSize } from '../../db/testDatabasePool.js';
|
|
@@ -219,6 +221,78 @@ export default class PostgresQueryDriver extends KyselyQueryDriver {
|
|
|
219
221
|
applyRowLock(kyselyQuery, tableAlias) {
|
|
220
222
|
return kyselyQuery.forUpdate(tableAlias);
|
|
221
223
|
}
|
|
224
|
+
static supportsAdvisoryTransactionLocks = true;
|
|
225
|
+
/**
|
|
226
|
+
* @internal
|
|
227
|
+
*
|
|
228
|
+
* Postgres implementation of the transaction-scoped advisory lock seam.
|
|
229
|
+
*
|
|
230
|
+
* Uses the single-`bigint` form `pg_advisory_xact_lock(bigint)`, whose lock
|
|
231
|
+
* space is distinct from the two-`int4` form the test-database pool claims
|
|
232
|
+
* with (see {@link openTestDatabaseLockSession}), so an application lock can
|
|
233
|
+
* never collide with a pool claim.
|
|
234
|
+
*
|
|
235
|
+
* Every key is taken in one statement, over `unnest` of the key array: a
|
|
236
|
+
* function scan yields its rows in array order, so the caller's sorted
|
|
237
|
+
* acquisition order — the thing that keeps two operations over the same
|
|
238
|
+
* scopes from deadlocking — is preserved, while a batch that needs one key
|
|
239
|
+
* per candidate record costs one round trip rather than N.
|
|
240
|
+
*
|
|
241
|
+
* The locks are held until the enclosing transaction ends, and are waiting
|
|
242
|
+
* (no `NOWAIT`): a second transaction asking for the same key blocks at that
|
|
243
|
+
* key's row until the holder commits or rolls back. Because they are
|
|
244
|
+
* transaction-scoped there is nothing to release, and re-acquiring a key
|
|
245
|
+
* within one transaction is a no-op rather than an error.
|
|
246
|
+
*
|
|
247
|
+
* The wait is bounded by `lock_timeout`, taken from
|
|
248
|
+
* {@link DreamApp.sortableScopeLockTimeout} and applied to the acquisition
|
|
249
|
+
* statement alone: the prior value is read in the same statement that sets
|
|
250
|
+
* the bound and restored in the next one, so the transaction's row-lock waits
|
|
251
|
+
* keep whatever the application configured. Postgres cancels the statement
|
|
252
|
+
* with `55P03` when the bound elapses, and that is translated here into
|
|
253
|
+
* {@link SortableScopeLockWaitTimedOut} so no Postgres error code escapes the
|
|
254
|
+
* driver.
|
|
255
|
+
*
|
|
256
|
+
* A timeout of `0` means "no bound", which is what `lock_timeout` itself
|
|
257
|
+
* means by it, and is sent as the bound rather than skipped: an application
|
|
258
|
+
* whose connection carries a `lock_timeout` of its own would otherwise have
|
|
259
|
+
* its acquisition cancelled by that bound, which is neither the unbounded
|
|
260
|
+
* wait `0` asks for nor a cancellation this driver could translate.
|
|
261
|
+
*/
|
|
262
|
+
static async acquireAdvisoryTransactionLocks(txn, keys) {
|
|
263
|
+
if (!keys.length)
|
|
264
|
+
return;
|
|
265
|
+
// the keys are passed as text and cast, since a JS bigint is not a value
|
|
266
|
+
// the pg driver can bind directly
|
|
267
|
+
const lockKeys = keys.map(key => key.toString());
|
|
268
|
+
const timeoutMs = DreamApp.getOrFail().sortableScopeLockTimeout;
|
|
269
|
+
let priorLockTimeout;
|
|
270
|
+
try {
|
|
271
|
+
// one statement, three things in a forced order: `bounded` selects from
|
|
272
|
+
// `prior_setting`, so the prior value is read before the bound replaces
|
|
273
|
+
// it, and both are `from` items, so both are scanned before the target
|
|
274
|
+
// list takes a single lock. Merging them costs nothing where separate
|
|
275
|
+
// statements would cost round trips inside the lock window.
|
|
276
|
+
//
|
|
277
|
+
// The returned column is aliased to a single word deliberately: result
|
|
278
|
+
// keys come back camelized, and a one-word alias is the same either way
|
|
279
|
+
const result = await sql `
|
|
280
|
+
with prior_setting as (select current_setting('lock_timeout') as value),
|
|
281
|
+
bounded as (select set_config('lock_timeout', ${timeoutMs.toString()}, true) as value from prior_setting)
|
|
282
|
+
select prior_setting.value as prior, pg_advisory_xact_lock(sortable_lock_keys.lock_key)
|
|
283
|
+
from prior_setting, bounded, unnest(${lockKeys}::bigint[]) as sortable_lock_keys(lock_key)
|
|
284
|
+
`.execute(txn.kyselyTransaction);
|
|
285
|
+
priorLockTimeout = result.rows[0].prior;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (pgErrorType(error) === LOCK_NOT_AVAILABLE)
|
|
289
|
+
throw new SortableScopeLockWaitTimedOut(timeoutMs);
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
// `current_setting` returns the value with its unit ('0', '5s', '250ms'),
|
|
293
|
+
// which is a form `set_config` accepts back unchanged
|
|
294
|
+
await sql `select set_config('lock_timeout', ${priorLockTimeout}, true)`.execute(txn.kyselyTransaction);
|
|
295
|
+
}
|
|
222
296
|
static supportsParallelTestDatabases = true;
|
|
223
297
|
/**
|
|
224
298
|
* @internal
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import performSortableDestroyWork from '../../decorators/field/sortable/helpers/performSortableDestroyWork.js';
|
|
2
|
+
import prepareSortableFieldsForDestroy, { clearSortableFieldsForDestroy, } from '../../decorators/field/sortable/helpers/prepareSortableFieldsForDestroy.js';
|
|
1
3
|
import DreamApp from '../../dream-app/index.js';
|
|
2
4
|
import destroyAssociatedRecords from './destroyAssociatedRecords.js';
|
|
3
5
|
import runHooksFor from './runHooksFor.js';
|
|
@@ -29,6 +31,17 @@ export default async function destroyDream(dream, txn = null, options) {
|
|
|
29
31
|
*/
|
|
30
32
|
async function destroyDreamWithTransaction(dream, txn, options) {
|
|
31
33
|
const { cascade, reallyDestroy, skipHooks } = options;
|
|
34
|
+
if (!skipHooks)
|
|
35
|
+
await runHooksFor('beforeDestroy', dream, true, null, txn);
|
|
36
|
+
// `preventDeletion` is fail-closed: a veto returns before the
|
|
37
|
+
// dependent-destroy cascade, so neither this record nor any of its
|
|
38
|
+
// `dependent: 'destroy'` associations are touched. The check sits outside the
|
|
39
|
+
// hook guard because the flag can also be set by the caller ahead of a
|
|
40
|
+
// `skipHooks` destroy, which runs no hooks but must not cascade either.
|
|
41
|
+
// Recursing through the cascade means a vetoing descendant stops its own
|
|
42
|
+
// subtree while the destroy that reached it carries on.
|
|
43
|
+
if (dream['_preventDeletion'])
|
|
44
|
+
return dream;
|
|
32
45
|
if (cascade) {
|
|
33
46
|
// NOTE: the dependent-destroy tree is loaded lazily, inside
|
|
34
47
|
// destroyAssociatedRecords, so that a cascaded descendant whose
|
|
@@ -37,15 +50,56 @@ async function destroyDreamWithTransaction(dream, txn, options) {
|
|
|
37
50
|
// clone: the hooks and the delete below operate on the original.
|
|
38
51
|
await destroyAssociatedRecords(dream, txn, options);
|
|
39
52
|
}
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
// Sortable preparation runs as a phase after every user beforeDestroy hook,
|
|
54
|
+
// never as a hook among them: the destroy's whole advisory key set — every
|
|
55
|
+
// sortable field — is acquired in one sorted pass, and the row's real
|
|
56
|
+
// position and scope values are read in one snapshot SELECT while the row
|
|
57
|
+
// still exists. It sits after the cascade so this destroy does not hold the
|
|
58
|
+
// scope lock across every descendant's destroy — true of the destroy itself,
|
|
59
|
+
// not of the transaction it runs in: under `Query#destroy({ lock: true })`
|
|
60
|
+
// the batch's whole key set is already held, taken before any row was
|
|
61
|
+
// claimed. A destroy that skips hooks performs no compaction, and one a
|
|
62
|
+
// cascaded descendant's hook has vetoed will not delete, so neither needs
|
|
63
|
+
// locks or a snapshot.
|
|
64
|
+
let rowFoundBeforeDelete = true;
|
|
65
|
+
if (!skipHooks && !dream['_preventDeletion']) {
|
|
66
|
+
rowFoundBeforeDelete = await prepareSortableFieldsForDestroy(dream, txn);
|
|
42
67
|
}
|
|
43
|
-
await maybeDestroyDream(dream, txn, reallyDestroy);
|
|
44
|
-
|
|
68
|
+
const rowsRemoved = await maybeDestroyDream(dream, txn, reallyDestroy);
|
|
69
|
+
// `preventDeletion` can still arrive after the veto check above has passed:
|
|
70
|
+
// a cascaded descendant's hook can reach back to this record and call it. The
|
|
71
|
+
// record is then still present, so neither the after-destroy hooks nor the
|
|
72
|
+
// reload of a soft deleted row have anything to run against, and the driver
|
|
73
|
+
// leaves the row alone. A veto that late cannot be made fail-closed against
|
|
74
|
+
// the cascade — the descendants are already destroyed by the time it lands.
|
|
75
|
+
const deletionPrevented = dream['_preventDeletion'];
|
|
76
|
+
// A destroy of a row that is already gone — a second `destroy()` on the same
|
|
77
|
+
// instance, a job retry, two workers racing — removes nothing, so there is
|
|
78
|
+
// nothing for the after-destroy hooks to react to and, for a sortable model,
|
|
79
|
+
// no vacancy to close. Compacting anyway would shift the scope a second time
|
|
80
|
+
// from the instance's remembered position, moving survivors onto occupied
|
|
81
|
+
// positions. A veto also removes nothing, and keeps its own path: it is
|
|
82
|
+
// recognized by the flag above, never by the count.
|
|
83
|
+
//
|
|
84
|
+
// Both signals are read because they catch opposite races. The count is the
|
|
85
|
+
// stronger of the two — a writer outside the sortable path can remove the row
|
|
86
|
+
// after the snapshot read, and only the count sees that. The snapshot's
|
|
87
|
+
// existence signal catches the mirror shape: the row was already gone when
|
|
88
|
+
// the snapshot read ran, so this destroy holds no position to vacate, and a
|
|
89
|
+
// row that reappeared under the same primary key is what the delete removed.
|
|
90
|
+
const removedNothing = !deletionPrevented && (rowsRemoved === 0 || !rowFoundBeforeDelete);
|
|
91
|
+
if (removedNothing)
|
|
92
|
+
clearSortableFieldsForDestroy(dream);
|
|
93
|
+
if (!skipHooks && !deletionPrevented && !removedNothing) {
|
|
94
|
+
// The compaction is a phase before the after-destroy hooks, never a hook
|
|
95
|
+
// among them, so every user afterDestroy hook observes the compacted scope
|
|
96
|
+
// regardless of where it was declared — the destroy-side counterpart of
|
|
97
|
+
// performSortablePositionWork running before the after-save hooks.
|
|
98
|
+
await performSortableDestroyWork(dream, txn);
|
|
45
99
|
await runHooksFor('afterDestroy', dream, true, null, txn);
|
|
46
100
|
await runHooksFor('afterDestroyCommit', dream, true, null, txn);
|
|
47
101
|
}
|
|
48
|
-
if (shouldSoftDelete(dream, reallyDestroy)) {
|
|
102
|
+
if (shouldSoftDelete(dream, reallyDestroy) && !deletionPrevented) {
|
|
49
103
|
await dream.txn(txn).reload();
|
|
50
104
|
}
|
|
51
105
|
return dream;
|
|
@@ -62,5 +116,5 @@ function shouldSoftDelete(dream, reallyDestroy) {
|
|
|
62
116
|
*/
|
|
63
117
|
async function maybeDestroyDream(dream, txn, reallyDestroy) {
|
|
64
118
|
const dbDriverClass = DreamApp.getOrFail().dbConnectionQueryDriverClass(dream.connectionName);
|
|
65
|
-
await dbDriverClass.destroyDream(dream, txn, reallyDestroy);
|
|
119
|
+
return await dbDriverClass.destroyDream(dream, txn, reallyDestroy);
|
|
66
120
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import acquireStabilizedSortableScopeLocks from '../../decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js';
|
|
2
|
+
import { clearSortableSaveState } from '../../decorators/field/sortable/helpers/clearCachedSortableValues.js';
|
|
3
|
+
import performSortablePositionWork from '../../decorators/field/sortable/helpers/performSortablePositionWork.js';
|
|
4
|
+
import prepareSortableFieldsForSave from '../../decorators/field/sortable/helpers/prepareSortableFieldsForSave.js';
|
|
5
|
+
import { cacheSortableSnapshots } from '../../decorators/field/sortable/helpers/sortableSnapshot.js';
|
|
1
6
|
import ValidationError from '../../errors/ValidationError.js';
|
|
2
7
|
import sqlAttributes from '../../helpers/sqlAttributes.js';
|
|
3
8
|
import { DateTime } from '../../utils/datetime/DateTime.js';
|
|
@@ -13,6 +18,27 @@ export default async function saveDream(dream, txn = null, { skipHooks = false }
|
|
|
13
18
|
await runHooksFor('beforeCreate', dream, alreadyPersisted, null, txn);
|
|
14
19
|
await runHooksFor('beforeSave', dream, alreadyPersisted, null, txn);
|
|
15
20
|
}
|
|
21
|
+
// Where this save opens a transaction of its own (below), a failure inside it
|
|
22
|
+
// rolls the driver write back, so the instance has to be able to go back to
|
|
23
|
+
// what it was when `save` was called: an instance left holding the primary
|
|
24
|
+
// key of a rolled-back INSERT — and left clean, so that the retry the failure
|
|
25
|
+
// invites returns at the `isDirty` check below without writing anything — is
|
|
26
|
+
// a record whose row does not exist. Captured before the sortable phase
|
|
27
|
+
// applies its position sentinel, so the retry sees the caller's own position.
|
|
28
|
+
// Only a model declaring sortable fields can reach that transaction, so a save
|
|
29
|
+
// of any other model copies nothing.
|
|
30
|
+
const preSaveState = txn || !dream.constructor['sortableFields'].length
|
|
31
|
+
? null
|
|
32
|
+
: capturePersistenceState(dream);
|
|
33
|
+
// Sortable computes each record's position under an advisory lock on its sort
|
|
34
|
+
// scope, and that lock is transaction-scoped, so the write and the position
|
|
35
|
+
// write that follows it have to commit together. Preparation runs here as a
|
|
36
|
+
// called phase — after every consumer before-hook, never as a hook among
|
|
37
|
+
// them — so that a position or scope change a user's own `beforeSave` hook
|
|
38
|
+
// introduced is inside the boundary regardless of where the hook was
|
|
39
|
+
// registered. Seated before the change capture, so the position sentinel
|
|
40
|
+
// lands in `beforeSaveChanges` exactly as it always has.
|
|
41
|
+
const sortableFieldConfigs = prepareSortableFieldsForSave(dream, { alreadyPersisted, skipHooks });
|
|
16
42
|
const beforeSaveChanges = dream.changes();
|
|
17
43
|
// need to check validations after running before hooks, or else
|
|
18
44
|
// model hooks that might make a model valid cannot run
|
|
@@ -20,6 +46,69 @@ export default async function saveDream(dream, txn = null, { skipHooks = false }
|
|
|
20
46
|
throw new ValidationError(dream['sanitizedConstructorName'], dream.errors);
|
|
21
47
|
if (alreadyPersisted && !dream.isDirty)
|
|
22
48
|
return dream;
|
|
49
|
+
if (sortableFieldConfigs.length && !txn) {
|
|
50
|
+
// The self-opened transaction holds only Dream's own work: the scope
|
|
51
|
+
// locks, the driver write, and the position write, which is its last
|
|
52
|
+
// statement. COMMIT releases the scope locks before any user after-hook
|
|
53
|
+
// runs; the after-hook and `*Commit` families then run with no
|
|
54
|
+
// transaction, exactly as on a save of a non-sortable model, so a hook
|
|
55
|
+
// that throws cannot roll back the committed row and a slow hook blocks
|
|
56
|
+
// no other writer of the scope.
|
|
57
|
+
try {
|
|
58
|
+
await dream.constructor.transaction(async (sortableTxn) => await writeDream(dream, sortableTxn, { alreadyPersisted, sortableFieldConfigs }));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
restorePersistenceState(dream, preSaveState);
|
|
62
|
+
clearSortableSaveState(dream, sortableFieldConfigs);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
await runAfterSaveHooks(dream, null, { skipHooks, alreadyPersisted, beforeSaveChanges });
|
|
66
|
+
return dream;
|
|
67
|
+
}
|
|
68
|
+
await writeDream(dream, txn, { alreadyPersisted, sortableFieldConfigs });
|
|
69
|
+
await runAfterSaveHooks(dream, txn, { skipHooks, alreadyPersisted, beforeSaveChanges });
|
|
70
|
+
return dream;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Everything a write changes about how the instance answers "am I saved, and
|
|
74
|
+
* what has changed since?" — the persisted flag, the attributes the driver's
|
|
75
|
+
* `RETURNING` wrote (the primary key among them), and the three attribute
|
|
76
|
+
* snapshots the dirty-checking reads.
|
|
77
|
+
*/
|
|
78
|
+
function capturePersistenceState(dream) {
|
|
79
|
+
return {
|
|
80
|
+
isPersisted: dream.isPersisted,
|
|
81
|
+
frozenAttributes: { ...dream['frozenAttributes'] },
|
|
82
|
+
originalAttributes: { ...dream['originalAttributes'] },
|
|
83
|
+
attributesFromBeforeLastSave: { ...dream['attributesFromBeforeLastSave'] },
|
|
84
|
+
currentAttributes: { ...dream['currentAttributes'] },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function restorePersistenceState(dream, state) {
|
|
88
|
+
dream['isPersisted'] = state.isPersisted;
|
|
89
|
+
dream['frozenAttributes'] = state.frozenAttributes;
|
|
90
|
+
dream['originalAttributes'] = state.originalAttributes;
|
|
91
|
+
dream['attributesFromBeforeLastSave'] = state.attributesFromBeforeLastSave;
|
|
92
|
+
dream['currentAttributes'] = state.currentAttributes;
|
|
93
|
+
}
|
|
94
|
+
async function writeDream(dream, txn, { alreadyPersisted, sortableFieldConfigs, }) {
|
|
95
|
+
let rowFoundBeforeWrite = true;
|
|
96
|
+
// A position- or scope-changing update takes its scope locks here, before the
|
|
97
|
+
// write, because the write overwrites the scope column: after it, neither the
|
|
98
|
+
// scope the record is moving out of nor its real position is readable from the
|
|
99
|
+
// row. The locks are therefore held across the update and the position write
|
|
100
|
+
// that follows it — one transaction, one acquisition — and the snapshot the
|
|
101
|
+
// position work is computed from is read here, under them.
|
|
102
|
+
//
|
|
103
|
+
// A create locks later, around its own position computation: it has no scope
|
|
104
|
+
// to move out of and no prior row to snapshot, and the deferrable unique
|
|
105
|
+
// constraint sortable tables carry tolerates two uncommitted rows at the
|
|
106
|
+
// pre-write position sentinel.
|
|
107
|
+
if (txn && alreadyPersisted && sortableFieldConfigs.length) {
|
|
108
|
+
const { rowExists, snapshots } = await acquireStabilizedSortableScopeLocks(dream, txn, sortableFieldConfigs);
|
|
109
|
+
rowFoundBeforeWrite = rowExists;
|
|
110
|
+
cacheSortableSnapshots(dream, snapshots);
|
|
111
|
+
}
|
|
23
112
|
const now = DateTime.now();
|
|
24
113
|
if (!alreadyPersisted && !dream.createdAt && dream.columns().has('createdAt'))
|
|
25
114
|
dream.createdAt = now;
|
|
@@ -41,14 +130,27 @@ export default async function saveDream(dream, txn = null, { skipHooks = false }
|
|
|
41
130
|
dream['freezeAttributes']();
|
|
42
131
|
dream['attributesFromBeforeLastSave'] = dream['originalAttributes'];
|
|
43
132
|
dream['originalAttributes'] = dream.getAttributes();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
133
|
+
// An update whose row the snapshot read positively found gone holds no
|
|
134
|
+
// position to move: the vacancy it remembers was closed by whoever removed
|
|
135
|
+
// the row, and shifting the scope from the instance's remembered position
|
|
136
|
+
// would move survivors onto occupied ones. The cached values and the empty
|
|
137
|
+
// snapshot go with it, so a later save of this instance reads the row itself.
|
|
138
|
+
if (txn && sortableFieldConfigs.length) {
|
|
139
|
+
if (rowFoundBeforeWrite)
|
|
140
|
+
await performSortablePositionWork(dream, txn, sortableFieldConfigs, { alreadyPersisted });
|
|
48
141
|
else
|
|
49
|
-
|
|
50
|
-
await runHooksFor(alreadyPersisted ? 'afterUpdateCommit' : 'afterCreateCommit', dream, alreadyPersisted, beforeSaveChanges, txn);
|
|
51
|
-
await runHooksFor('afterSaveCommit', dream, alreadyPersisted, beforeSaveChanges, txn);
|
|
142
|
+
clearSortableSaveState(dream, sortableFieldConfigs);
|
|
52
143
|
}
|
|
53
144
|
return dream;
|
|
54
145
|
}
|
|
146
|
+
async function runAfterSaveHooks(dream, txn, { skipHooks, alreadyPersisted, beforeSaveChanges, }) {
|
|
147
|
+
if (skipHooks)
|
|
148
|
+
return;
|
|
149
|
+
await runHooksFor('afterSave', dream, alreadyPersisted, beforeSaveChanges, txn);
|
|
150
|
+
if (alreadyPersisted)
|
|
151
|
+
await runHooksFor('afterUpdate', dream, alreadyPersisted, beforeSaveChanges, txn);
|
|
152
|
+
else
|
|
153
|
+
await runHooksFor('afterCreate', dream, alreadyPersisted, beforeSaveChanges, txn);
|
|
154
|
+
await runHooksFor(alreadyPersisted ? 'afterUpdateCommit' : 'afterCreateCommit', dream, alreadyPersisted, beforeSaveChanges, txn);
|
|
155
|
+
await runHooksFor('afterSaveCommit', dream, alreadyPersisted, beforeSaveChanges, txn);
|
|
156
|
+
}
|
|
@@ -8,7 +8,8 @@ import namespaceColumn from '../../../helpers/namespaceColumn.js';
|
|
|
8
8
|
import { TRIGRAM_OPERATORS } from '../../constants.js';
|
|
9
9
|
import similaritySelectSql from './similaritySelectSql.js';
|
|
10
10
|
import similarityWhereSql from './similarityWhereSql.js';
|
|
11
|
-
import
|
|
11
|
+
import DreamApp from '../../../dream-app/index.js';
|
|
12
|
+
import PostgresQueryDriver from '../../QueryDriver/Postgres.js';
|
|
12
13
|
export default class SimilarityBuilder extends ConnectedToDB {
|
|
13
14
|
whereStatement;
|
|
14
15
|
whereNotStatement;
|
|
@@ -215,7 +216,7 @@ export default class SimilarityBuilder extends ConnectedToDB {
|
|
|
215
216
|
const schema = this.dreamClass.prototype.schema;
|
|
216
217
|
const primaryKeyName = this.dreamClass.primaryKey;
|
|
217
218
|
const { tableName, tableAlias, columnName } = similarityStatement;
|
|
218
|
-
const queryDriverClass =
|
|
219
|
+
const queryDriverClass = postgresQueryDriverForSimilaritySearch(this.dreamInstance.connectionName);
|
|
219
220
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
220
221
|
const { ref } = queryDriverClass.dbFor(this.dreamInstance.connectionName, this.dbConnectionType('select')).dynamic;
|
|
221
222
|
const validatedTableAlias = validateTableAlias(tableAlias);
|
|
@@ -247,7 +248,7 @@ export default class SimilarityBuilder extends ConnectedToDB {
|
|
|
247
248
|
statementType,
|
|
248
249
|
});
|
|
249
250
|
const trigramSearchAlias = this.similaritySearchId(tableAlias, columnName);
|
|
250
|
-
const queryDriverClass =
|
|
251
|
+
const queryDriverClass = postgresQueryDriverForSimilaritySearch(this.dreamInstance.connectionName);
|
|
251
252
|
const selectQuery = queryDriverClass
|
|
252
253
|
.dbFor(this.dreamInstance.connectionName, this.dbConnectionType('select'))
|
|
253
254
|
.selectFrom(validatedTableAlias)
|
|
@@ -261,7 +262,7 @@ export default class SimilarityBuilder extends ConnectedToDB {
|
|
|
261
262
|
const { columnName, opsStatement, tableName } = similarityStatement;
|
|
262
263
|
const validatedTable = validateTable(schema, tableName);
|
|
263
264
|
const validatedPrimaryKey = validateColumn(schema, tableName, primaryKeyName);
|
|
264
|
-
const queryDriverClass =
|
|
265
|
+
const queryDriverClass = postgresQueryDriverForSimilaritySearch(this.dreamInstance.connectionName);
|
|
265
266
|
let nestedQuery = queryDriverClass
|
|
266
267
|
.dbFor(this.dreamInstance.connectionName, this.dbConnectionType('select'))
|
|
267
268
|
.selectFrom(tableName)
|
|
@@ -314,6 +315,20 @@ export default class SimilarityBuilder extends ConnectedToDB {
|
|
|
314
315
|
return `${statementType}_rank_${statementIndex + 1}`;
|
|
315
316
|
}
|
|
316
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Trigram similarity search is built on Postgres's `pg_trgm` operators and on
|
|
320
|
+
* kysely builders taken straight off the Postgres driver, so it resolves the
|
|
321
|
+
* connection's query driver to that driver — accepting a consumer subclass of
|
|
322
|
+
* it, which inherits the same operators — and otherwise fails loud rather than
|
|
323
|
+
* emitting SQL another engine cannot run.
|
|
324
|
+
*/
|
|
325
|
+
function postgresQueryDriverForSimilaritySearch(connectionName) {
|
|
326
|
+
const queryDriverClass = DreamApp.getOrFail().dbConnectionQueryDriverClass(connectionName);
|
|
327
|
+
if (queryDriverClass !== PostgresQueryDriver &&
|
|
328
|
+
!(queryDriverClass.prototype instanceof PostgresQueryDriver))
|
|
329
|
+
throw new Error(`${queryDriverClass.name} is not the PostgresQueryDriver, nor a subclass of it. Similarity search (similarity, wordSimilarity, strictWordSimilarity) requires the PostgresQueryDriver.`);
|
|
330
|
+
return queryDriverClass;
|
|
331
|
+
}
|
|
317
332
|
export const SIMILARITY_TYPES = ['where', 'join_and'];
|
|
318
333
|
function removeJoinAndFromObjectHierarchy(obj) {
|
|
319
334
|
if (obj?.isOpsStatement)
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import MissingDeletedAtFieldForSoftDelete from '../../errors/MissingDeletedAtFieldForSoftDelete.js';
|
|
2
2
|
import isDatetimeOrDatetimeArrayColumn from '../../helpers/db/types/isDatetimeOrDatetimeArrayColumn.js';
|
|
3
3
|
import { DateTime } from '../../utils/datetime/DateTime.js';
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*
|
|
7
|
+
* @returns the number of rows soft deleted — zero when the row is already gone,
|
|
8
|
+
* and zero when it is already soft deleted
|
|
9
|
+
*/
|
|
4
10
|
export default async function softDeleteDream(dream, txn) {
|
|
5
11
|
const deletedAtField = dream['_deletedAtField'];
|
|
6
12
|
const dreamClass = dream.constructor;
|
|
7
13
|
if (!isDatetimeOrDatetimeArrayColumn(dreamClass, deletedAtField)) {
|
|
8
14
|
throw new MissingDeletedAtFieldForSoftDelete(dream.constructor);
|
|
9
15
|
}
|
|
16
|
+
// An already soft deleted row is logically gone, and this UPDATE is what
|
|
17
|
+
// reports the deletion to the caller: matching such a row again would rewrite
|
|
18
|
+
// its `deletedAt` and count a deletion this destroy did not perform, running
|
|
19
|
+
// the after-destroy hooks a second time on a record nothing happened to.
|
|
10
20
|
let query = txn.kyselyTransaction
|
|
11
21
|
.updateTable(dream.table)
|
|
12
22
|
.where(dream['_primaryKey'], '=', dream.primaryKeyValue())
|
|
23
|
+
.where(deletedAtField, 'is', null)
|
|
13
24
|
.set(dream['_deletedAtField'], DateTime.now());
|
|
14
25
|
dreamClass['sortableFields']?.forEach(sortableFieldMetadata => {
|
|
15
26
|
const positionColumn = sortableFieldMetadata.positionField;
|
|
16
27
|
query = query.set(positionColumn, null);
|
|
17
28
|
});
|
|
18
|
-
await query.execute();
|
|
29
|
+
const results = await query.execute();
|
|
30
|
+
return Number(results[0]?.numUpdatedRows ?? 0);
|
|
19
31
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import acquireStabilizedSortableScopeLocks from '../../decorators/field/sortable/helpers/acquireStabilizedSortableScopeLocks.js';
|
|
2
|
+
import filterQueryToScopeValues from '../../decorators/field/sortable/helpers/filterQueryToScopeValues.js';
|
|
3
|
+
import { snapshotScopeValue } from '../../decorators/field/sortable/helpers/sortableSnapshot.js';
|
|
2
4
|
import runHooksFor from './runHooksFor.js';
|
|
3
5
|
/**
|
|
4
6
|
* @internal
|
|
@@ -31,11 +33,23 @@ async function undestroyDreamWithTransaction(dream, txn, options) {
|
|
|
31
33
|
if (!skipHooks) {
|
|
32
34
|
await runHooksFor('beforeUpdate', dream, true, null, txn);
|
|
33
35
|
}
|
|
34
|
-
|
|
36
|
+
// The cascade runs ahead of the restore, matching `destroyDream`: this
|
|
37
|
+
// record's own scope lock is taken inside `doUndestroyDream`, so a cascade
|
|
38
|
+
// seated after it would hold that lock across every descendant's undestroy,
|
|
39
|
+
// and a parent and its children would take their advisory keys in the
|
|
40
|
+
// opposite order to the one the destroy side takes them in. The nested select
|
|
41
|
+
// the cascade builds bypasses the soft-delete scope on this record's own
|
|
42
|
+
// class (`undestroyOptions`), so it reaches the children while this record is
|
|
43
|
+
// still deleted.
|
|
35
44
|
if (cascade) {
|
|
36
45
|
await undestroyAssociatedRecords(dream, txn, options);
|
|
37
46
|
}
|
|
38
|
-
|
|
47
|
+
const restoredRowCount = await doUndestroyDream(dream, txn);
|
|
48
|
+
// A restore of a row that is not deleted — a second `undestroy()`, a job
|
|
49
|
+
// retry, or a record that was never destroyed, which `Query#undestroy` reaches
|
|
50
|
+
// routinely since it lifts the soft-delete scope and walks every match —
|
|
51
|
+
// matches no row, so there is nothing for the after-update hooks to react to.
|
|
52
|
+
if (!skipHooks && restoredRowCount > 0) {
|
|
39
53
|
await runHooksFor('afterUpdate', dream, true, null, txn);
|
|
40
54
|
await runHooksFor('afterUpdateCommit', dream, true, null, txn);
|
|
41
55
|
}
|
|
@@ -45,22 +59,56 @@ async function undestroyDreamWithTransaction(dream, txn, options) {
|
|
|
45
59
|
/**
|
|
46
60
|
* @internal
|
|
47
61
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
62
|
+
* Clears the record's `deletedAt` and, for a sortable model, gives it a
|
|
63
|
+
* position at the end of the sort scope its row physically occupies.
|
|
64
|
+
*
|
|
65
|
+
* Restoring is conditional on the row still being deleted, and the count of
|
|
66
|
+
* rows the statement matched is what the caller reads that off. A row that is
|
|
67
|
+
* already back would otherwise be inside the `max(position)` its new position
|
|
68
|
+
* is computed from, and would move one past itself on every repeat — a
|
|
69
|
+
* permanent gap in the scope.
|
|
70
|
+
*
|
|
71
|
+
* @returns the number of rows restored: 1, or 0 when the row was not deleted
|
|
50
72
|
*/
|
|
51
73
|
async function doUndestroyDream(dream, txn) {
|
|
52
74
|
const updateStatement = txn.kyselyTransaction.updateTable(dream.table);
|
|
53
75
|
let query = updateStatement
|
|
54
76
|
.where(dream['_primaryKey'], '=', dream.primaryKeyValue())
|
|
77
|
+
.where(dream['_deletedAtField'], 'is not', null)
|
|
55
78
|
.set({ [dream['_deletedAtField']]: null });
|
|
56
79
|
const dreamClass = dream.constructor;
|
|
57
|
-
dreamClass['sortableFields']
|
|
80
|
+
const sortableFields = (dreamClass['sortableFields'] ?? []);
|
|
81
|
+
// Undestroy has no sortable hook of its own — the position is recomputed
|
|
82
|
+
// inline, below, before `afterUpdate` ever runs — so the scope lock is taken
|
|
83
|
+
// here, around that recomputation. Without it the `max(position) + 1`
|
|
84
|
+
// subquery races every other writer of the scope the record is being restored
|
|
85
|
+
// into.
|
|
86
|
+
//
|
|
87
|
+
// The scope this record is being restored into is the one its row physically
|
|
88
|
+
// carries, not the one the instance was loaded with: another writer may have
|
|
89
|
+
// moved it between scopes while it was soft deleted, which is what the
|
|
90
|
+
// stabilized acquisition converges on.
|
|
91
|
+
const { snapshots } = await acquireStabilizedSortableScopeLocks(dream, txn, sortableFields);
|
|
92
|
+
sortableFields.forEach(sortableFieldMetadata => {
|
|
58
93
|
const positionColumn = sortableFieldMetadata.positionField;
|
|
94
|
+
const snapshot = snapshots.get(positionColumn);
|
|
59
95
|
query = query.set(eb => ({
|
|
60
|
-
[positionColumn]: eb(
|
|
96
|
+
[positionColumn]: eb(filterQueryToScopeValues(dream, txn.kyselyTransaction.selectFrom(dream.table), column => snapshotScopeValue(snapshot, column, () => dream[column]), sortableFieldMetadata.scope).select(eb =>
|
|
97
|
+
// `max` over a scope with no positioned rows left — an empty
|
|
98
|
+
// scope, or one whose every remaining row is soft deleted and so
|
|
99
|
+
// carries a NULL position — is NULL, and NULL + 1 is NULL, which
|
|
100
|
+
// would restore the record with no position at all. Coalescing
|
|
101
|
+
// before the increment restores it at position 1.
|
|
102
|
+
eb.fn.coalesce(eb.fn.max(positionColumn), eb.lit(0)).as(positionColumn + '_max')), '+', 1),
|
|
61
103
|
}));
|
|
62
104
|
});
|
|
63
|
-
|
|
105
|
+
const [result] = await query.execute();
|
|
106
|
+
// No sortable row cache invalidation here: the restored record took a position
|
|
107
|
+
// at the end of the scope it was restored into and nothing else moved, so no
|
|
108
|
+
// other record's cached preflight row went stale, and this record's own cached
|
|
109
|
+
// row was consumed — destructively — by the snapshot read every sortable field
|
|
110
|
+
// above goes through.
|
|
111
|
+
return Number(result?.numUpdatedRows ?? 0);
|
|
64
112
|
}
|
|
65
113
|
/**
|
|
66
114
|
* @internal
|