@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
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import cachedTypeForAttribute from '../../../../helpers/db/cachedTypeForAttribute.js';
|
|
2
|
+
const UTF8 = new TextEncoder();
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*
|
|
6
|
+
* The canonical text form of one sort-scope value — the thing an advisory lock
|
|
7
|
+
* key and a resort group identity are actually built from.
|
|
8
|
+
*
|
|
9
|
+
* Two writers of the same physical sort scope have to derive the same key, and
|
|
10
|
+
* they do not always hold the value in the same JavaScript shape. A save that
|
|
11
|
+
* has not been written yet holds the *pending* value the caller supplied
|
|
12
|
+
* (`'1.00'`, an uppercase UUID, an object whose keys are in source order),
|
|
13
|
+
* while a snapshot read, a create's post-INSERT read and `resort`'s table read
|
|
14
|
+
* hold the value Postgres *hydrated* (`1`, a lowercase UUID, an object whose
|
|
15
|
+
* keys are in jsonb's own order). Postgres considers those pairs one value;
|
|
16
|
+
* `typeof`-only normalization considers them two, and two keys do not exclude
|
|
17
|
+
* each other.
|
|
18
|
+
*
|
|
19
|
+
* So canonicalization is keyed on the column's runtime `dbType`, and the
|
|
20
|
+
* canonical form of each type is **the form the hydrated value already takes**.
|
|
21
|
+
* That anchoring is what makes this safe to ship without a migration or a
|
|
22
|
+
* coordinated deploy: no key derived from a database-hydrated value moves, and
|
|
23
|
+
* the pending-side keys that do move are exactly the ones that disagree with
|
|
24
|
+
* every hydrated-path writer of the same scope today.
|
|
25
|
+
*
|
|
26
|
+
* The classes handled, and what the hydrated value looks like:
|
|
27
|
+
*
|
|
28
|
+
* - `uuid` — Postgres renders UUIDs lowercase, so the canonical form is
|
|
29
|
+
* lowercase.
|
|
30
|
+
* - `numeric` / `real` / `double precision` — hydrated through
|
|
31
|
+
* `parsePostgresDecimal`, i.e. `parseFloat`, so `'1.00'` and `1` both
|
|
32
|
+
* canonicalize to `'1'`.
|
|
33
|
+
* - `jsonb` — hydrated by `JSON.parse` of jsonb's own output, whose object keys
|
|
34
|
+
* are ordered by key length and then bytewise. Objects are re-emitted in that
|
|
35
|
+
* order, recursively, so property insertion order stops mattering.
|
|
36
|
+
* - `boolean` — hydrated as a JavaScript boolean; Postgres's own input literals
|
|
37
|
+
* (`'t'`, `'yes'`, `'0'`, …) canonicalize to `'true'`/`'false'`.
|
|
38
|
+
* - `date` and the four timestamp/time types — hydrated as `CalendarDate`,
|
|
39
|
+
* `DateTime`, `ClockTime` or `ClockTimeTz`, whose JSON form is the quoted ISO
|
|
40
|
+
* string. Strings are quoted the same way so a pending `'2026-02-07'` and a
|
|
41
|
+
* hydrated `CalendarDate` agree.
|
|
42
|
+
*
|
|
43
|
+
* Everything else — `bigint` and `character varying`, which is to say the
|
|
44
|
+
* foreign keys and text columns nearly every sort scope is made of — is left on
|
|
45
|
+
* the untyped normalization below, unchanged.
|
|
46
|
+
*
|
|
47
|
+
* Two residuals are known and accepted rather than fixed:
|
|
48
|
+
*
|
|
49
|
+
* - **`citext`**: Postgres stores the case a row was written with while
|
|
50
|
+
* comparing case-insensitively, so two rows of one physical scope hydrate to
|
|
51
|
+
* values that differ in case and split the key on their own. Lowercasing
|
|
52
|
+
* would fix the split but would *move* hydrated keys, which is the one thing
|
|
53
|
+
* this design refuses to do.
|
|
54
|
+
* - **Temporal values that agree on the instant but not on the text**: a zone
|
|
55
|
+
* offset or a fractional-second precision that differs from the hydrated
|
|
56
|
+
* rendering still yields a different key. Only the string-vs-object quoting
|
|
57
|
+
* mismatch is canonicalized.
|
|
58
|
+
*
|
|
59
|
+
* Both are over-serialization or a missed exclusion between two *pending*-side
|
|
60
|
+
* writers, never a corrupt position under a key two writers do share.
|
|
61
|
+
*
|
|
62
|
+
* @param dream - the record whose scope column is being canonicalized
|
|
63
|
+
* @param column - the physical database column
|
|
64
|
+
* @param value - the pending or hydrated value held for that column
|
|
65
|
+
* @returns the canonical text form, or `null` for an absent value
|
|
66
|
+
*/
|
|
67
|
+
export default function canonicalScopeValue(dream, column, value) {
|
|
68
|
+
if (value === null || value === undefined)
|
|
69
|
+
return null;
|
|
70
|
+
switch (cachedTypeForAttribute(dream.constructor, column)) {
|
|
71
|
+
case 'uuid':
|
|
72
|
+
return canonicalUuid(value);
|
|
73
|
+
case 'numeric':
|
|
74
|
+
case 'real':
|
|
75
|
+
case 'double precision':
|
|
76
|
+
return canonicalNumeric(value);
|
|
77
|
+
case 'jsonb':
|
|
78
|
+
return canonicalJsonb(value);
|
|
79
|
+
case 'boolean':
|
|
80
|
+
return canonicalBoolean(value);
|
|
81
|
+
case 'date':
|
|
82
|
+
case 'timestamp without time zone':
|
|
83
|
+
case 'timestamp with time zone':
|
|
84
|
+
case 'time without time zone':
|
|
85
|
+
case 'time with time zone':
|
|
86
|
+
return canonicalTemporal(value);
|
|
87
|
+
default:
|
|
88
|
+
return untypedScopeValue(value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* @internal
|
|
93
|
+
*
|
|
94
|
+
* The type-blind fallback, and the shape every unrecognized `dbType` keeps:
|
|
95
|
+
* the same foreign key can arrive as a string or a number depending on the path
|
|
96
|
+
* that loaded it, so values are rendered as text and null is kept distinct from
|
|
97
|
+
* every string, including the empty one.
|
|
98
|
+
*/
|
|
99
|
+
export function untypedScopeValue(value) {
|
|
100
|
+
if (value === null || value === undefined)
|
|
101
|
+
return null;
|
|
102
|
+
switch (typeof value) {
|
|
103
|
+
case 'string':
|
|
104
|
+
return value;
|
|
105
|
+
case 'number':
|
|
106
|
+
case 'bigint':
|
|
107
|
+
case 'boolean':
|
|
108
|
+
return value.toString();
|
|
109
|
+
default:
|
|
110
|
+
return JSON.stringify(value) ?? null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function canonicalUuid(value) {
|
|
114
|
+
return typeof value === 'string' ? value.toLowerCase() : untypedScopeValue(value);
|
|
115
|
+
}
|
|
116
|
+
function canonicalNumeric(value) {
|
|
117
|
+
const parsed = typeof value === 'number' ? value : parseFloat(String(value));
|
|
118
|
+
return Number.isFinite(parsed) ? String(parsed) : untypedScopeValue(value);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* @internal
|
|
122
|
+
*
|
|
123
|
+
* Postgres's own boolean input literals, lower-cased and trimmed, which a
|
|
124
|
+
* pending value may hold while the hydrated value is a JavaScript boolean.
|
|
125
|
+
* Exported so the spec drives its cases from the sets themselves rather than
|
|
126
|
+
* from a hand-copied list that can silently drift out of step with them.
|
|
127
|
+
*/
|
|
128
|
+
export const BOOLEAN_TRUE_LITERALS = new Set(['t', 'true', 'y', 'yes', 'on', '1']);
|
|
129
|
+
export const BOOLEAN_FALSE_LITERALS = new Set(['f', 'false', 'n', 'no', 'off', '0']);
|
|
130
|
+
function canonicalBoolean(value) {
|
|
131
|
+
if (typeof value === 'boolean')
|
|
132
|
+
return value ? 'true' : 'false';
|
|
133
|
+
if (typeof value === 'string') {
|
|
134
|
+
const literal = value.trim().toLowerCase();
|
|
135
|
+
if (BOOLEAN_TRUE_LITERALS.has(literal))
|
|
136
|
+
return 'true';
|
|
137
|
+
if (BOOLEAN_FALSE_LITERALS.has(literal))
|
|
138
|
+
return 'false';
|
|
139
|
+
}
|
|
140
|
+
return untypedScopeValue(value);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Temporal columns hydrate as `CalendarDate`/`DateTime`/`ClockTime` instances
|
|
144
|
+
* whose `toJSON` is the ISO string, so JSON-encoding both shapes puts a pending
|
|
145
|
+
* ISO string and its hydrated counterpart on the same quoted text.
|
|
146
|
+
*/
|
|
147
|
+
function canonicalTemporal(value) {
|
|
148
|
+
return JSON.stringify(value) ?? null;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* jsonb neither stores nor renders property insertion order: it orders object
|
|
152
|
+
* keys by length and then bytewise, and a hydrated value therefore always
|
|
153
|
+
* arrives in that order. Re-emitting in the same order makes a pending object
|
|
154
|
+
* literal agree with it, and leaves every hydrated value byte-identical.
|
|
155
|
+
*/
|
|
156
|
+
function canonicalJsonb(value) {
|
|
157
|
+
const parsed = typeof value === 'string' ? parseJson(value) : value;
|
|
158
|
+
return JSON.stringify(inJsonbKeyOrder(parsed)) ?? null;
|
|
159
|
+
}
|
|
160
|
+
function parseJson(value) {
|
|
161
|
+
try {
|
|
162
|
+
return JSON.parse(value);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function inJsonbKeyOrder(value) {
|
|
169
|
+
if (Array.isArray(value))
|
|
170
|
+
return value.map(inJsonbKeyOrder);
|
|
171
|
+
if (value === null || typeof value !== 'object')
|
|
172
|
+
return value;
|
|
173
|
+
return Object.keys(value)
|
|
174
|
+
.sort(compareJsonbKeys)
|
|
175
|
+
.reduce((acc, key) => {
|
|
176
|
+
acc[key] = inJsonbKeyOrder(value[key]);
|
|
177
|
+
return acc;
|
|
178
|
+
}, {});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* jsonb's own key collation: shorter keys first, ties broken by comparing the
|
|
182
|
+
* UTF-8 bytes.
|
|
183
|
+
*/
|
|
184
|
+
function compareJsonbKeys(a, b) {
|
|
185
|
+
const bytesA = UTF8.encode(a);
|
|
186
|
+
const bytesB = UTF8.encode(b);
|
|
187
|
+
if (bytesA.length !== bytesB.length)
|
|
188
|
+
return bytesA.length - bytesB.length;
|
|
189
|
+
for (let i = 0; i < bytesA.length; i++) {
|
|
190
|
+
if (bytesA[i] !== bytesB[i])
|
|
191
|
+
return bytesA[i] - bytesB[i];
|
|
192
|
+
}
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import sortableCacheValuesName from './sortableCacheValuesName.js';
|
|
2
|
+
import { clearSortableSnapshot } from './sortableSnapshot.js';
|
|
2
3
|
export default function clearCachedSortableValues(dream, positionField) {
|
|
3
4
|
const cachedValuesName = sortableCacheValuesName(positionField);
|
|
4
5
|
dream[cachedValuesName] = undefined;
|
|
5
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* @internal
|
|
9
|
+
*
|
|
10
|
+
* Discards both halves of what a save's sortable phase stashes on the
|
|
11
|
+
* instance: the cached values the position work reads, and the snapshot of the
|
|
12
|
+
* row it would have computed its shift against. The position work clears them
|
|
13
|
+
* itself as it consumes them, so this is for the paths where it does not run —
|
|
14
|
+
* the row was already gone when the snapshot read found it, or the save's
|
|
15
|
+
* transaction rolled back — leaving a later save of the same instance to read
|
|
16
|
+
* the row for itself rather than inherit this save's reading of it.
|
|
17
|
+
*/
|
|
18
|
+
export function clearSortableSaveState(dream, configs) {
|
|
19
|
+
for (const { positionField } of configs) {
|
|
20
|
+
clearCachedSortableValues(dream, positionField);
|
|
21
|
+
clearSortableSnapshot(dream, positionField);
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/esm/src/decorators/field/sortable/helpers/decrementScopedRecordsGreaterThanPosition.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ops from '../../../../ops/index.js';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
export default async function decrementPositionForScopedRecordsGreaterThanPosition(position, { dream, positionField, query, scope, }) {
|
|
2
|
+
import filterQueryToScopeValues from './filterQueryToScopeValues.js';
|
|
3
|
+
import { snapshotScopeValue } from './sortableSnapshot.js';
|
|
4
|
+
export default async function decrementPositionForScopedRecordsGreaterThanPosition(position, { dream, positionField, query, scope, snapshot, }) {
|
|
5
5
|
let kyselyQuery = query
|
|
6
6
|
.whereNot({ [dream['_primaryKey']]: dream.primaryKeyValue() })
|
|
7
7
|
.where({
|
|
@@ -13,11 +13,11 @@ export default async function decrementPositionForScopedRecordsGreaterThanPositi
|
|
|
13
13
|
[positionField]: eb(positionField, '-', 1),
|
|
14
14
|
};
|
|
15
15
|
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
// A null scope value has to be matched with `is null`: `column = NULL` is
|
|
17
|
+
// never true, so the compaction would silently shift nothing at all in a
|
|
18
|
+
// null-valued sort scope. `filterQueryToScopeValues` is the one place that
|
|
19
|
+
// branch lives, and it is the same filter every other position-mutating path
|
|
20
|
+
// builds.
|
|
21
|
+
kyselyQuery = filterQueryToScopeValues(dream, kyselyQuery, column => snapshotScopeValue(snapshot, column, () => dream[column]), scope);
|
|
22
22
|
await kyselyQuery.execute();
|
|
23
23
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import sortableScopeColumns from './sortableScopeColumns.js';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* Narrows a Dream `Query` to one sort scope, **reading the scope values off the
|
|
6
|
+
* in-memory instance**. That is only the scope the row physically occupies when
|
|
7
|
+
* the instance is known to agree with the row — inside a transaction that has
|
|
8
|
+
* just written those values, or on a record read under the scope lock.
|
|
9
|
+
*
|
|
10
|
+
* Where the row may have been moved by another writer, the caller wants
|
|
11
|
+
* `filterQueryToScopeValues` instead, which takes each column's value from the
|
|
12
|
+
* caller (the snapshot read under the lock) rather than from the instance.
|
|
13
|
+
*/
|
|
14
|
+
export default function filterQueryToInstanceScope(query, dream, scope) {
|
|
15
|
+
for (const column of sortableScopeColumns(dream, scope)) {
|
|
16
|
+
const value = dream[column];
|
|
17
|
+
query = query.where({ [column]: value ?? null });
|
|
18
|
+
}
|
|
19
|
+
return query;
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import sortableScopeColumns from './sortableScopeColumns.js';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* Narrows a kysely query to one sort scope, taking each scope column's value
|
|
6
|
+
* **from the caller** rather than from the in-memory instance — the snapshot
|
|
7
|
+
* read under the scope lock on the paths that compact a scope a record is
|
|
8
|
+
* leaving, which is not necessarily the scope the instance was loaded from.
|
|
9
|
+
*
|
|
10
|
+
* This is also the one place the null branch lives: a null scope value has to
|
|
11
|
+
* be matched with `is null`, since `column = NULL` is never true and the filter
|
|
12
|
+
* would silently match nothing at all in a null-valued sort scope.
|
|
13
|
+
*
|
|
14
|
+
* The instance-reading sibling is `filterQueryToInstanceScope`.
|
|
15
|
+
*
|
|
16
|
+
* @param dream - the record whose class resolves the scope members to columns
|
|
17
|
+
* @param kyselyQuery - the query to narrow
|
|
18
|
+
* @param whereValueCB - the value to match each scope column against
|
|
19
|
+
* @param scope - the sortable field's scope
|
|
20
|
+
*/
|
|
21
|
+
export default function filterQueryToScopeValues(dream, kyselyQuery, whereValueCB, scope) {
|
|
22
|
+
for (const column of sortableScopeColumns(dream, scope)) {
|
|
23
|
+
const columnValue = whereValueCB(column);
|
|
24
|
+
if (columnValue === null) {
|
|
25
|
+
kyselyQuery = kyselyQuery.where(column, 'is', null);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
kyselyQuery = kyselyQuery.where(column, '=', columnValue);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return kyselyQuery;
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const heldKeysByTransaction = new WeakMap();
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* The sortable scope-lock keys this transaction already holds.
|
|
6
|
+
*
|
|
7
|
+
* Advisory transaction locks are released only when the transaction ends, so a
|
|
8
|
+
* key taken once is held for the rest of it: every later acquisition of that
|
|
9
|
+
* key is a statement whose entire effect is to observe that the transaction
|
|
10
|
+
* already has what it is asking for. Tracking them turns those statements into
|
|
11
|
+
* nothing at all — the per-field acquisition inside the position write, and
|
|
12
|
+
* every per-record acquisition inside a locked batch whose preflight already
|
|
13
|
+
* took the batch's whole key set.
|
|
14
|
+
*
|
|
15
|
+
* Being the transaction's whole ledger is also what lets the locked-batch
|
|
16
|
+
* preflight count its bound cumulatively rather than per batch: how many scope
|
|
17
|
+
* locks one more batch would leave the transaction holding is this set's size
|
|
18
|
+
* plus whatever of the batch's keys is not already in it.
|
|
19
|
+
*
|
|
20
|
+
* Keyed by the transaction object rather than by the connection, which is what
|
|
21
|
+
* makes it correct rather than merely fast: a nested transaction gets its own
|
|
22
|
+
* `DreamTransaction` and therefore its own set, so keys taken inside a
|
|
23
|
+
* subtransaction that rolls back — releasing its locks with it — are never
|
|
24
|
+
* remembered as held by the transaction that outlives it. The entry is
|
|
25
|
+
* discarded with the transaction object.
|
|
26
|
+
*/
|
|
27
|
+
export default function heldSortableScopeLockKeys(txn) {
|
|
28
|
+
const held = heldKeysByTransaction.get(txn);
|
|
29
|
+
if (held)
|
|
30
|
+
return held;
|
|
31
|
+
const newHeld = new Set();
|
|
32
|
+
heldKeysByTransaction.set(txn, newHeld);
|
|
33
|
+
return newHeld;
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { STI_SCOPE_NAME } from '../../../class/STI.js';
|
|
2
|
+
import afterSortableDestroy from '../hooks/afterSortableDestroy.js';
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*
|
|
6
|
+
* The compaction of a sortable destroy: closes the vacancy each sortable
|
|
7
|
+
* field's sort scope is left with, computed from the snapshot the preparation
|
|
8
|
+
* phase (`prepareSortableFieldsForDestroy`) read before the delete. Called
|
|
9
|
+
* directly from `destroyDream` after the delete and before any user
|
|
10
|
+
* `afterDestroy` hook — never registered as a hook among them — so every
|
|
11
|
+
* after-destroy hook observes the compacted scope regardless of where it was
|
|
12
|
+
* declared, exactly as every after-save hook observes the computed position.
|
|
13
|
+
*/
|
|
14
|
+
export default async function performSortableDestroyWork(dream, txn) {
|
|
15
|
+
const dreamClass = dream.constructor;
|
|
16
|
+
const sortableFields = dreamClass['sortableFields'];
|
|
17
|
+
for (const { positionField, scope } of sortableFields) {
|
|
18
|
+
const query = dreamClass.query().removeDefaultScope(STI_SCOPE_NAME).txn(txn);
|
|
19
|
+
await afterSortableDestroy({ dream, positionField, query, scope, txn });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { STI_SCOPE_NAME } from '../../../class/STI.js';
|
|
2
|
+
import afterSortableCreate from '../hooks/afterSortableCreate.js';
|
|
3
|
+
import afterUpdateSortable from '../hooks/afterSortableUpdate.js';
|
|
4
|
+
import acquireSortableScopeLocks from './acquireSortableScopeLocks.js';
|
|
5
|
+
import { sortableCurrentScopeLockKeys } from './sortableScopeLockKeys.js';
|
|
6
|
+
/**
|
|
7
|
+
* @internal
|
|
8
|
+
*
|
|
9
|
+
* The position work of a sortable save: computes each sortable field's real
|
|
10
|
+
* position under the scope lock and writes it. Called directly from
|
|
11
|
+
* `saveDream` after the driver write — never registered as an after-hook — so
|
|
12
|
+
* no user hook code can interleave with it while the scope lock is held, and
|
|
13
|
+
* on a self-opened save it is the last work before COMMIT releases the lock.
|
|
14
|
+
*/
|
|
15
|
+
export default async function performSortablePositionWork(dream, txn, sortableFieldConfigs, { alreadyPersisted }) {
|
|
16
|
+
const dreamClass = dream.constructor;
|
|
17
|
+
// A create acquires its whole advisory key set here — every sortable field,
|
|
18
|
+
// one sorted pass — before any field's position is computed, so a
|
|
19
|
+
// multi-field create takes its keys in the same global order as an update or
|
|
20
|
+
// a destroy and cannot deadlock against them. The per-field acquisition
|
|
21
|
+
// inside `setPosition` then re-acquires keys this transaction already holds,
|
|
22
|
+
// which is a no-op. (An update acquired its keys before the driver write, in
|
|
23
|
+
// `saveDream`.)
|
|
24
|
+
if (!alreadyPersisted) {
|
|
25
|
+
await acquireSortableScopeLocks(dream, txn, sortableCurrentScopeLockKeys(dream, sortableFieldConfigs));
|
|
26
|
+
}
|
|
27
|
+
for (const { positionField, scope } of sortableFieldConfigs) {
|
|
28
|
+
const query = dreamClass.query().removeDefaultScope(STI_SCOPE_NAME).txn(txn);
|
|
29
|
+
if (alreadyPersisted) {
|
|
30
|
+
await afterUpdateSortable({ dream, positionField, query, txn, scope });
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
await afterSortableCreate({ dream, positionField, query, txn, scope });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import acquireStabilizedSortableScopeLocks from './acquireStabilizedSortableScopeLocks.js';
|
|
2
|
+
import clearCachedSortableValues from './clearCachedSortableValues.js';
|
|
3
|
+
import { cacheSortableSnapshots, clearSortableSnapshot } from './sortableSnapshot.js';
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*
|
|
7
|
+
* The sortable preparation phase of a destroy. Called from `destroyDream` after
|
|
8
|
+
* every user `beforeDestroy` hook has run and before the delete — a phase, not
|
|
9
|
+
* a hook among them — so the operation's whole advisory key set, across every
|
|
10
|
+
* sortable field, is taken in one sorted stabilized acquisition, with one
|
|
11
|
+
* snapshot SELECT covering all fields. Per-field acquisition would take
|
|
12
|
+
* multi-field keys in declaration order, which deadlocks against the paths
|
|
13
|
+
* that acquire sorted.
|
|
14
|
+
*
|
|
15
|
+
* Pre-delete is the only seat available for the snapshot read: on a hard
|
|
16
|
+
* destroy the row is gone afterwards, and a soft destroy nulls the position
|
|
17
|
+
* columns in the same UPDATE as `deletedAt`. The lock is taken around the read
|
|
18
|
+
* so the snapshot cannot be invalidated between the read and the compaction;
|
|
19
|
+
* it is transaction-scoped, and `destroyDream` opens a transaction that spans
|
|
20
|
+
* the delete and the after-destroy hooks, so one acquisition covers both.
|
|
21
|
+
*
|
|
22
|
+
* The per-field compactions (`performSortableDestroyWork`) consume the cached
|
|
23
|
+
* snapshots.
|
|
24
|
+
*
|
|
25
|
+
* @returns false when the snapshot read found no row — this destroy has nothing
|
|
26
|
+
* to vacate, and compacting from the instance's remembered position would
|
|
27
|
+
* shift a scope some other writer has already closed. The delete's own
|
|
28
|
+
* affected-row count is the stronger signal (a writer outside the sortable
|
|
29
|
+
* path can remove the row after this read), so the caller checks both.
|
|
30
|
+
*/
|
|
31
|
+
export default async function prepareSortableFieldsForDestroy(dream, txn) {
|
|
32
|
+
const sortableFields = dream.constructor['sortableFields'];
|
|
33
|
+
if (!sortableFields.length)
|
|
34
|
+
return true;
|
|
35
|
+
const { rowExists, snapshots } = await acquireStabilizedSortableScopeLocks(dream, txn, sortableFields);
|
|
36
|
+
cacheSortableSnapshots(dream, snapshots);
|
|
37
|
+
return rowExists;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @internal
|
|
41
|
+
*
|
|
42
|
+
* Discards what the preparation phase stashed on the instance. The per-field
|
|
43
|
+
* compaction clears its own snapshot as it consumes it, so this
|
|
44
|
+
* exists for the path where that compaction never runs: a destroy that removed
|
|
45
|
+
* nothing leaves the snapshot behind, and a later save or destroy of the same
|
|
46
|
+
* instance would compute its shift from a reading of a row this operation
|
|
47
|
+
* already found gone.
|
|
48
|
+
*/
|
|
49
|
+
export function clearSortableFieldsForDestroy(dream) {
|
|
50
|
+
const sortableFields = (dream.constructor['sortableFields'] ??
|
|
51
|
+
[]);
|
|
52
|
+
for (const { positionField } of sortableFields) {
|
|
53
|
+
clearCachedSortableValues(dream, positionField);
|
|
54
|
+
clearSortableSnapshot(dream, positionField);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import positionIsInvalid from './positionIsInvalid.js';
|
|
2
|
+
import sortableCacheValuesName from './sortableCacheValuesName.js';
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*
|
|
6
|
+
* The sortable preparation phase of a save. Called from `saveDream` after every
|
|
7
|
+
* consumer before-hook has run — a phase, not a hook among them — so the
|
|
8
|
+
* decision is made from the final state of the instance: a position or scope
|
|
9
|
+
* change introduced by a `beforeSave` hook registered after Sortable's gets the
|
|
10
|
+
* same handling as one made before `save` was called, and a cache is never
|
|
11
|
+
* carried over from a state an intervening hook has made stale.
|
|
12
|
+
*
|
|
13
|
+
* For each sortable field this caches the values the position work needs and
|
|
14
|
+
* applies the position sentinel, then returns the fields whose position work
|
|
15
|
+
* this save will perform — the fields that make the save need a transaction of
|
|
16
|
+
* its own, so that the write and the position write that follows it commit
|
|
17
|
+
* together under one scope lock.
|
|
18
|
+
*
|
|
19
|
+
* - every create on a model with sortable fields performs position work;
|
|
20
|
+
* - an update performs it only when the save changes the position or one of the
|
|
21
|
+
* sortable scope columns.
|
|
22
|
+
*
|
|
23
|
+
* A save that skips hooks performs no position work at all, so it needs no
|
|
24
|
+
* transaction.
|
|
25
|
+
*/
|
|
26
|
+
export default function prepareSortableFieldsForSave(dream, { alreadyPersisted, skipHooks }) {
|
|
27
|
+
if (skipHooks)
|
|
28
|
+
return [];
|
|
29
|
+
const sortableFields = dream.constructor['sortableFields'];
|
|
30
|
+
if (!sortableFields.length)
|
|
31
|
+
return [];
|
|
32
|
+
sortableFields.forEach(config => prepareSortableFieldForSave(dream, config));
|
|
33
|
+
if (!alreadyPersisted)
|
|
34
|
+
return [...sortableFields];
|
|
35
|
+
return sortableFields.filter(config => dream[sortableCacheValuesName(config.positionField)] !== undefined);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Caches the values one sortable field's position work needs and applies the
|
|
39
|
+
* position sentinel to the instance, so that the driver write never commits a
|
|
40
|
+
* caller-supplied position the scope has not made room for.
|
|
41
|
+
*/
|
|
42
|
+
function prepareSortableFieldForSave(dream, { positionField, scope }) {
|
|
43
|
+
const cachedValuesName = sortableCacheValuesName(positionField);
|
|
44
|
+
const dreamAsAny = dream;
|
|
45
|
+
const changingScope = dream.isPersisted &&
|
|
46
|
+
scope.filter(scopeField => {
|
|
47
|
+
const association = dream['getAssociationMetadata'](scopeField);
|
|
48
|
+
return association
|
|
49
|
+
? dream.willSaveChangeToAttribute(association.foreignKey())
|
|
50
|
+
: dream.willSaveChangeToAttribute(scopeField);
|
|
51
|
+
}).length > 0;
|
|
52
|
+
if (dream.willSaveChangeToAttribute(positionField) || changingScope) {
|
|
53
|
+
const position = dreamAsAny[positionField];
|
|
54
|
+
// store values to be used by the position work later in the save
|
|
55
|
+
const values = {
|
|
56
|
+
wasNewRecord: dream.isNewRecord,
|
|
57
|
+
changingScope,
|
|
58
|
+
position: positionIsInvalid(position) ? undefined : position,
|
|
59
|
+
previousPosition: dream.isPersisted && dream.willSaveChangeToAttribute(positionField)
|
|
60
|
+
? dream.changes()[positionField]?.was
|
|
61
|
+
: changingScope
|
|
62
|
+
? position
|
|
63
|
+
: undefined,
|
|
64
|
+
};
|
|
65
|
+
dreamAsAny[cachedValuesName] = values;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// computed from the final state of the instance, so a cache from an earlier,
|
|
69
|
+
// now-stale state must not survive this decision
|
|
70
|
+
dreamAsAny[cachedValuesName] = undefined;
|
|
71
|
+
}
|
|
72
|
+
if (dream.isNewRecord || changingScope) {
|
|
73
|
+
// if the dream is not saved, or is being moved between scopes, set position to 0
|
|
74
|
+
// to prevent collisions with existing position values.
|
|
75
|
+
// the real position is computed and written under the scope lock later in the save.
|
|
76
|
+
dreamAsAny[positionField] = 0;
|
|
77
|
+
}
|
|
78
|
+
else if (dream.willSaveChangeToAttribute(positionField)) {
|
|
79
|
+
// if the dream is saved and the position is being changed, set the position field to
|
|
80
|
+
// undefined, which will cause the update cycle to ignore the position field. The
|
|
81
|
+
// position work later in the save writes the real value.
|
|
82
|
+
dreamAsAny[positionField] = undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|