@rvoh/dream 2.27.0 → 2.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/Dream.js +32 -0
- package/dist/cjs/src/db/errors.js +4 -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 +64 -9
- 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/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/package-exports/errors.js +3 -0
- package/dist/esm/src/Dream.js +32 -0
- package/dist/esm/src/db/errors.js +4 -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 +64 -9
- 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/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/package-exports/errors.js +3 -0
- package/dist/types/src/Dream.d.ts +32 -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 +41 -7
- 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/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/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 +94 -84
- 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
|
@@ -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
|
|
@@ -212,6 +212,24 @@ export default class DreamApp {
|
|
|
212
212
|
get paginationMaxPageSize() {
|
|
213
213
|
return this._paginationMaxPageSize;
|
|
214
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* How long, in milliseconds, a position-mutating operation on a
|
|
217
|
+
* `@deco.Sortable` model will wait for another transaction to release the
|
|
218
|
+
* sort scope it needs before giving up with
|
|
219
|
+
* `SortableScopeLockWaitTimedOut`. Applied to the lock acquisition statement
|
|
220
|
+
* alone — the transaction's other lock waits keep whatever `lock_timeout`
|
|
221
|
+
* the application has configured.
|
|
222
|
+
*
|
|
223
|
+
* `0` waits without bound, which is what Postgres's `lock_timeout` means by
|
|
224
|
+
* it, and holds even on a connection configured with a `lock_timeout` of its
|
|
225
|
+
* own: the acquisition disables the bound for the duration of the statement.
|
|
226
|
+
*
|
|
227
|
+
* Set it with `dreamApp.set('sortableScopeLockTimeout', milliseconds)`.
|
|
228
|
+
*/
|
|
229
|
+
_sortableScopeLockTimeout = 5000;
|
|
230
|
+
get sortableScopeLockTimeout() {
|
|
231
|
+
return this._sortableScopeLockTimeout;
|
|
232
|
+
}
|
|
215
233
|
_primaryKeyType = 'bigint';
|
|
216
234
|
get primaryKeyType() {
|
|
217
235
|
return this._primaryKeyType;
|
|
@@ -492,6 +510,9 @@ export default class DreamApp {
|
|
|
492
510
|
case 'paginationMaxPageSize':
|
|
493
511
|
this._paginationMaxPageSize = options;
|
|
494
512
|
break;
|
|
513
|
+
case 'sortableScopeLockTimeout':
|
|
514
|
+
this._sortableScopeLockTimeout = options;
|
|
515
|
+
break;
|
|
495
516
|
default: {
|
|
496
517
|
// protection so that if a new ApplyOpt is ever added, this will throw a type error at build time
|
|
497
518
|
const _never = applyOption;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised when a batch of a locked `Query#destroy` or `Query#update` on a
|
|
3
|
+
* sortable model would carry its transaction past the number of advisory
|
|
4
|
+
* sort-scope locks Dream is willing to hold at once.
|
|
5
|
+
*
|
|
6
|
+
* The batched, row-locking query APIs take every advisory scope key the batch
|
|
7
|
+
* needs before they claim a single row — one key per sort scope the batch's
|
|
8
|
+
* candidate records occupy, per sortable field the model declares. Advisory
|
|
9
|
+
* locks live in Postgres's cluster-wide shared lock table, whose size is fixed
|
|
10
|
+
* at startup (`max_locks_per_transaction` × `max_connections`), and they are
|
|
11
|
+
* held until the transaction ends. A batch over a table whose sort scope has
|
|
12
|
+
* very high cardinality can therefore consume a meaningful fraction of a
|
|
13
|
+
* resource every other connection in the cluster shares, and exhausting it
|
|
14
|
+
* fails unrelated transactions rather than only this one.
|
|
15
|
+
*
|
|
16
|
+
* So the preflight counts first and refuses, before claiming any row. The count
|
|
17
|
+
* is the whole transaction's: the keys it already holds plus the keys the
|
|
18
|
+
* refused batch would add, since advisory locks are released only when the
|
|
19
|
+
* transaction ends. Inside a transaction Dream opens for the batch that is the
|
|
20
|
+
* batch's own key set and nothing has been locked when this is raised; inside a
|
|
21
|
+
* transaction you own, it also counts every key the run's earlier batches took,
|
|
22
|
+
* and those stay held.
|
|
23
|
+
*
|
|
24
|
+
* The fix is almost always a smaller `batchSize`: the keys one batch needs are
|
|
25
|
+
* bounded by how many distinct sort scopes its records span, so halving the
|
|
26
|
+
* batch size halves the worst case. If the run itself is what accumulates them
|
|
27
|
+
* — a long multi-batch run inside a transaction you opened — either let Dream
|
|
28
|
+
* open a transaction per batch (do not pass a transaction of your own) or
|
|
29
|
+
* commit between runs, which is what releases the locks as it goes.
|
|
30
|
+
*/
|
|
31
|
+
export default class SortableBatchRequiresTooManyScopeLocks extends Error {
|
|
32
|
+
dreamClassName;
|
|
33
|
+
keyCount;
|
|
34
|
+
maxKeyCount;
|
|
35
|
+
constructor(dreamClassName, keyCount, maxKeyCount) {
|
|
36
|
+
super();
|
|
37
|
+
this.dreamClassName = dreamClassName;
|
|
38
|
+
this.keyCount = keyCount;
|
|
39
|
+
this.maxKeyCount = maxKeyCount;
|
|
40
|
+
}
|
|
41
|
+
get message() {
|
|
42
|
+
return `\
|
|
43
|
+
A locked batch over ${this.dreamClassName} would leave its transaction holding ${this.keyCount} sort scope locks, and the limit is ${this.maxKeyCount}.
|
|
44
|
+
|
|
45
|
+
Sortable takes one advisory lock per sort scope a batch touches, per sortable field, before the
|
|
46
|
+
batch claims any row, and holds them until the transaction ends. Advisory locks come out of a
|
|
47
|
+
lock table the whole Postgres cluster shares, so a batch that would carry its transaction past
|
|
48
|
+
this limit is refused rather than risking every other connection's transactions.
|
|
49
|
+
|
|
50
|
+
Use a smaller batchSize. The keys one batch needs are bounded by the number of distinct sort
|
|
51
|
+
scopes its records span, so a smaller batch takes proportionally fewer. If this operation runs
|
|
52
|
+
inside a transaction you opened, note that the locks of earlier batches are still held while
|
|
53
|
+
later batches preflight, and are counted here; letting Dream open a transaction per batch
|
|
54
|
+
releases them as it goes.
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised when a model declaring sortable fields is used on a query driver that
|
|
3
|
+
* cannot take transaction-scoped advisory locks.
|
|
4
|
+
*
|
|
5
|
+
* Sortable computes position values under a lock on the sort scope, without
|
|
6
|
+
* which concurrent writers collide on the same position. A driver that cannot
|
|
7
|
+
* provide that lock fails loudly here rather than racing silently.
|
|
8
|
+
*/
|
|
9
|
+
export default class SortableRequiresAdvisoryTransactionLocks extends Error {
|
|
10
|
+
queryDriverClassName;
|
|
11
|
+
constructor(queryDriverClassName) {
|
|
12
|
+
super();
|
|
13
|
+
this.queryDriverClassName = queryDriverClassName;
|
|
14
|
+
}
|
|
15
|
+
get message() {
|
|
16
|
+
return `\
|
|
17
|
+
${this.queryDriverClassName} cannot be used with the Sortable decorator.
|
|
18
|
+
|
|
19
|
+
Sortable computes position values under a transaction-scoped advisory lock on the sort
|
|
20
|
+
scope, without which concurrent writers collide on the same position. Implement
|
|
21
|
+
\`acquireAdvisoryTransactionLocks\` on ${this.queryDriverClassName} and set
|
|
22
|
+
\`supportsAdvisoryTransactionLocks = true\`, or use the PostgresQueryDriver for models
|
|
23
|
+
that declare sortable fields.
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
26
|
+
}
|