@rvoh/dream 2.26.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 -17
- 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/DreamClassTransactionBuilder.js +0 -18
- package/dist/cjs/src/dream/Query.js +64 -32
- package/dist/cjs/src/dream/QueryDriver/Base.js +75 -2
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +19 -12
- 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 -17
- 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/DreamClassTransactionBuilder.js +0 -18
- package/dist/esm/src/dream/Query.js +64 -32
- package/dist/esm/src/dream/QueryDriver/Base.js +75 -2
- package/dist/esm/src/dream/QueryDriver/Kysely.js +19 -12
- 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 +35 -2
- 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/DreamClassTransactionBuilder.d.ts +3 -2
- package/dist/types/src/dream/Query.d.ts +44 -9
- package/dist/types/src/dream/QueryDriver/Base.d.ts +61 -1
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +6 -2
- package/dist/types/src/dream/QueryDriver/Postgres.d.ts +40 -0
- package/dist/types/src/dream/internal/softDeleteDream.d.ts +7 -1
- package/dist/types/src/dream-app/index.d.ts +18 -2
- package/dist/types/src/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/dist/types/src/types/associations/shared.d.ts +1 -1
- package/dist/types/src/types/dream.d.ts +3 -0
- package/dist/types/src/types/query.d.ts +1 -1
- package/dist/types/src/types/types/associations/shared.ts +8 -6
- package/dist/types/src/types/types/dream.ts +6 -0
- package/dist/types/src/types/types/query.ts +1 -1
- 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 +159 -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 +122 -99
- 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,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
|
+
}
|
|
@@ -1,35 +1,192 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import { sql } from 'kysely';
|
|
2
|
+
import Query from '../../../../dream/Query.js';
|
|
3
|
+
import SortableRequiresAdvisoryTransactionLocks from '../../../../errors/SortableRequiresAdvisoryTransactionLocks.js';
|
|
4
|
+
import { STI_SCOPE_NAME } from '../../../class/STI.js';
|
|
5
|
+
import acquireSortableScopeLocks from './acquireSortableScopeLocks.js';
|
|
6
|
+
import canonicalScopeValue from './canonicalScopeValue.js';
|
|
7
|
+
import filterQueryToScopeValues from './filterQueryToScopeValues.js';
|
|
8
|
+
import sortableScopeColumns from './sortableScopeColumns.js';
|
|
9
|
+
import sortableScopeLockKey from './sortableScopeLockKey.js';
|
|
10
|
+
/**
|
|
11
|
+
* The alias and column names the renumbering UPDATE joins its `row_number()`
|
|
12
|
+
* subquery on. They are namespaced so that no real column of the table being
|
|
13
|
+
* resorted can collide with them.
|
|
14
|
+
*/
|
|
15
|
+
const RENUMBER_ALIAS = 'dream_resort_renumber';
|
|
16
|
+
const RENUMBER_PRIMARY_KEY = 'dream_resort_primary_key';
|
|
17
|
+
const RENUMBER_POSITION = 'dream_resort_position';
|
|
18
|
+
/**
|
|
19
|
+
* The aliases the one-row summary of a sort scope's positions comes back under.
|
|
20
|
+
* They are namespaced for the same reason the renumbering aliases are, and are
|
|
21
|
+
* lowercase without underscores so that Kysely's CamelCasePlugin passes them
|
|
22
|
+
* through untouched and each value can be read straight off the result row (the
|
|
23
|
+
* same alias strategy `pluck` and the grouped aggregates use).
|
|
24
|
+
*/
|
|
25
|
+
const SUMMARY_ROW_COUNT = 'dreamresortrowcount';
|
|
26
|
+
const SUMMARY_DISTINCT_POSITIONS = 'dreamresortdistinctpositions';
|
|
27
|
+
const SUMMARY_MIN_POSITION = 'dreamresortminposition';
|
|
28
|
+
const SUMMARY_MAX_POSITION = 'dreamresortmaxposition';
|
|
29
|
+
/**
|
|
30
|
+
* @internal
|
|
31
|
+
*
|
|
32
|
+
* Renumbers every sort scope of a model so that each one runs 1..n with no gaps.
|
|
33
|
+
*
|
|
34
|
+
* Four things make this safe to run against a live table:
|
|
35
|
+
*
|
|
36
|
+
* - **A driver that cannot take the scope lock fails before the table is
|
|
37
|
+
* read**, rather than succeeding for whichever scopes happen to be contiguous
|
|
38
|
+
* and throwing on the first one that is not.
|
|
39
|
+
* - **The STI scope is stripped**, exactly as every sortable hook strips it. An
|
|
40
|
+
* STI hierarchy shares one position space, so renumbering one child's rows in
|
|
41
|
+
* isolation would hand them positions its siblings already hold.
|
|
42
|
+
* - **Groups are identified structurally**, by the scope columns' `[column,
|
|
43
|
+
* value]` tuple rather than by joining the values into one string. Joining
|
|
44
|
+
* merges scopes whose values differ only in where a delimiter falls, and
|
|
45
|
+
* merges a null member with an empty-string one; a merged group is two
|
|
46
|
+
* physical sort scopes renumbered as though they were one, and no single lock
|
|
47
|
+
* key covers it.
|
|
48
|
+
* - **Every discovered scope is renumbered under its own scope lock, from a
|
|
49
|
+
* read taken inside that lock.** The unlocked pass below only discovers
|
|
50
|
+
* *which* sort scopes exist; it decides nothing about their contents, because
|
|
51
|
+
* a concurrent create, destroy or raw update between the two reads is
|
|
52
|
+
* precisely the case this exists to repair. Whether a scope needs renumbering
|
|
53
|
+
* at all is decided under the lock.
|
|
54
|
+
*
|
|
55
|
+
* Each scope's transaction is this function's own: `resort` cannot join a
|
|
56
|
+
* transaction the caller owns, and must not be called from inside one. It would
|
|
57
|
+
* wait on a connection of its own for a scope lock the caller's transaction may
|
|
58
|
+
* already hold — a wait no deadlock detector can see, which ends in
|
|
59
|
+
* `SortableScopeLockWaitTimedOut` rather than in progress.
|
|
60
|
+
*/
|
|
4
61
|
export default async function resortAllRecords(dreamClass, positionField, scope) {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
for (const
|
|
13
|
-
const dreams = dreamArr;
|
|
14
|
-
if (isSortedCorrectly(dreams, positionField))
|
|
15
|
-
continue;
|
|
62
|
+
const queryDriverClass = Query.dbDriverClass(dreamClass.prototype.connectionName || 'default');
|
|
63
|
+
if (!queryDriverClass.supportsAdvisoryTransactionLocks)
|
|
64
|
+
throw new SortableRequiresAdvisoryTransactionLocks(queryDriverClass.name);
|
|
65
|
+
// every scope value, lock key and group identity below is derived from class
|
|
66
|
+
// level metadata — the table, the connection, the scope members' columns and
|
|
67
|
+
// their database types — so one bare instance stands in for every row
|
|
68
|
+
const derivationRecord = dreamClass.new();
|
|
69
|
+
for (const scopeValues of await discoverSortScopes(dreamClass, derivationRecord, scope)) {
|
|
16
70
|
await dreamClass.transaction(async (txn) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.set({
|
|
24
|
-
[positionField]: counter++,
|
|
25
|
-
})
|
|
26
|
-
.execute();
|
|
27
|
-
}
|
|
71
|
+
await acquireSortableScopeLocks(derivationRecord, txn, [
|
|
72
|
+
sortableScopeLockKey(derivationRecord, positionField, scopeValues),
|
|
73
|
+
]);
|
|
74
|
+
if (await scopeIsAlreadyOrdered({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn }))
|
|
75
|
+
return;
|
|
76
|
+
await renumberScope({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn });
|
|
28
77
|
});
|
|
29
78
|
}
|
|
30
79
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Every physical sort scope the table currently holds, as the `[column, value]`
|
|
82
|
+
* tuples a lock key is derived from.
|
|
83
|
+
*
|
|
84
|
+
* One distinct read over the scope columns alone, which both bounds the cost —
|
|
85
|
+
* a single round trip returning one row per sort scope, however many rows the
|
|
86
|
+
* table holds — and makes the answer stable: a walk that windows the table can
|
|
87
|
+
* be shifted past rows by a concurrent delete, and a sort scope whose only rows
|
|
88
|
+
* fall in that gap would never be renumbered at all. Each discovered scope's
|
|
89
|
+
* rows are read again under its own lock before anything is written.
|
|
90
|
+
*
|
|
91
|
+
* A sortable field with no scope has exactly one sort scope, the whole table,
|
|
92
|
+
* so its answer needs no read.
|
|
93
|
+
*/
|
|
94
|
+
async function discoverSortScopes(dreamClass, derivationRecord, scope) {
|
|
95
|
+
const columns = sortableScopeColumns(derivationRecord, scope);
|
|
96
|
+
if (columns.length === 0)
|
|
97
|
+
return [[]];
|
|
98
|
+
const rows = (await stiUnscopedQuery(dreamClass)
|
|
99
|
+
.toKysely('select')
|
|
100
|
+
.clearSelect()
|
|
101
|
+
.distinct()
|
|
102
|
+
.select(columns)
|
|
103
|
+
.execute());
|
|
104
|
+
// the database's `distinct` is by raw value; two raw values the column's type
|
|
105
|
+
// treats as one (a citext scope column matching case-insensitively) are one
|
|
106
|
+
// sort scope under one lock key, and canonicalization is what says so
|
|
107
|
+
const sortScopes = new Map();
|
|
108
|
+
for (const row of rows) {
|
|
109
|
+
const scopeValues = columns.map((column) => [column, row[column]]);
|
|
110
|
+
const identity = scopeIdentity(derivationRecord, scopeValues);
|
|
111
|
+
if (!sortScopes.has(identity))
|
|
112
|
+
sortScopes.set(identity, scopeValues);
|
|
113
|
+
}
|
|
114
|
+
return [...sortScopes.values()];
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether the sort scope already runs 1..n, decided inside the scope's lock by
|
|
118
|
+
* one aggregate row rather than by shipping every position in the scope to the
|
|
119
|
+
* client: a sort scope can hold as many rows as the table does, and this runs
|
|
120
|
+
* with the scope's lock held.
|
|
121
|
+
*
|
|
122
|
+
* A scope of `n` rows is already correct exactly when its positions are `n`
|
|
123
|
+
* distinct non-null values running from 1 to n. `count(distinct position)`
|
|
124
|
+
* drops both duplicates and nulls, so it agrees with `count(*)` only when every
|
|
125
|
+
* row holds a position of its own, and `min`/`max` pin that set to 1..n. An
|
|
126
|
+
* empty scope — one whose last row was destroyed between its discovery and this
|
|
127
|
+
* lock — has nothing to renumber.
|
|
128
|
+
*/
|
|
129
|
+
async function scopeIsAlreadyOrdered({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn, }) {
|
|
130
|
+
const summary = (await scopedSelect({ dreamClass, derivationRecord, scope, scopeValues, txn })
|
|
131
|
+
.clearSelect()
|
|
132
|
+
.select([
|
|
133
|
+
sql `count(*)`.as(SUMMARY_ROW_COUNT),
|
|
134
|
+
sql `count(distinct ${sql.ref(positionField)})`.as(SUMMARY_DISTINCT_POSITIONS),
|
|
135
|
+
sql `min(${sql.ref(positionField)})`.as(SUMMARY_MIN_POSITION),
|
|
136
|
+
sql `max(${sql.ref(positionField)})`.as(SUMMARY_MAX_POSITION),
|
|
137
|
+
])
|
|
138
|
+
.executeTakeFirstOrThrow());
|
|
139
|
+
// counts come back from some drivers as strings, since a count is a bigint
|
|
140
|
+
const rowCount = Number(summary[SUMMARY_ROW_COUNT]);
|
|
141
|
+
if (rowCount === 0)
|
|
142
|
+
return true;
|
|
143
|
+
return (Number(summary[SUMMARY_DISTINCT_POSITIONS]) === rowCount &&
|
|
144
|
+
Number(summary[SUMMARY_MIN_POSITION]) === 1 &&
|
|
145
|
+
Number(summary[SUMMARY_MAX_POSITION]) === rowCount);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Renumbers the whole sort scope in one statement, from a `row_number()` taken
|
|
149
|
+
* over the scope's rows ordered by position, nulls first, ties broken by
|
|
150
|
+
* primary key. A row already holding the position it is owed is left alone, so
|
|
151
|
+
* a scope with a single gap in it writes a single row rather than all of them.
|
|
152
|
+
*/
|
|
153
|
+
async function renumberScope({ dreamClass, derivationRecord, positionField, scope, scopeValues, txn, }) {
|
|
154
|
+
const table = derivationRecord.table;
|
|
155
|
+
const primaryKey = derivationRecord['_primaryKey'];
|
|
156
|
+
const renumbered = scopedSelect({ dreamClass, derivationRecord, scope, scopeValues, txn })
|
|
157
|
+
.clearSelect()
|
|
158
|
+
.select([
|
|
159
|
+
sql.ref(primaryKey).as(RENUMBER_PRIMARY_KEY),
|
|
160
|
+
sql `row_number() over (order by ${sql.ref(positionField)} asc nulls first, ${sql.ref(primaryKey)} asc)`.as(RENUMBER_POSITION),
|
|
161
|
+
]);
|
|
162
|
+
await stiUnscopedQuery(dreamClass)
|
|
163
|
+
.txn(txn)
|
|
164
|
+
.toKysely('update')
|
|
165
|
+
.from(renumbered.as(RENUMBER_ALIAS))
|
|
166
|
+
.set({ [positionField]: sql.ref(`${RENUMBER_ALIAS}.${RENUMBER_POSITION}`) })
|
|
167
|
+
.whereRef(`${table}.${primaryKey}`, '=', `${RENUMBER_ALIAS}.${RENUMBER_PRIMARY_KEY}`)
|
|
168
|
+
.where(sql.ref(`${table}.${positionField}`), 'is distinct from', sql.ref(`${RENUMBER_ALIAS}.${RENUMBER_POSITION}`))
|
|
169
|
+
.execute();
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* A kysely select over the rows of one physical sort scope, carrying the
|
|
173
|
+
* model's default scopes (a soft-deleted row is not part of any sort scope) but
|
|
174
|
+
* not the STI scope, and matching a null scope member with `is null`.
|
|
175
|
+
*/
|
|
176
|
+
function scopedSelect({ dreamClass, derivationRecord, scope, scopeValues, txn, }) {
|
|
177
|
+
const values = new Map(scopeValues);
|
|
178
|
+
return filterQueryToScopeValues(derivationRecord, stiUnscopedQuery(dreamClass).txn(txn).toKysely('select'), column => values.get(column) ?? null, scope);
|
|
179
|
+
}
|
|
180
|
+
function stiUnscopedQuery(dreamClass) {
|
|
181
|
+
return dreamClass.query().removeDefaultScope(STI_SCOPE_NAME);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* A collision-free identity for a physical sort scope. Values are canonicalized
|
|
185
|
+
* against the column's database type by the same function the advisory lock key
|
|
186
|
+
* uses (null distinct from every string, a pending value agreeing with its
|
|
187
|
+
* hydrated counterpart) and encoded structurally, so two distinct tuples can
|
|
188
|
+
* never share an identity and a scope can never disagree with its own lock.
|
|
189
|
+
*/
|
|
190
|
+
function scopeIdentity(derivationRecord, scopeValues) {
|
|
191
|
+
return JSON.stringify(scopeValues.map(([column, value]) => [column, canonicalScopeValue(derivationRecord, column, value)]));
|
|
35
192
|
}
|
|
@@ -1,62 +1,116 @@
|
|
|
1
|
-
import DreamApp from '../../../../dream-app/index.js';
|
|
2
|
-
import PostgresQueryDriver from '../../../../dream/QueryDriver/Postgres.js';
|
|
3
1
|
import range from '../../../../helpers/range.js';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
2
|
+
import ops from '../../../../ops/index.js';
|
|
3
|
+
import acquireSortableScopeLocks from './acquireSortableScopeLocks.js';
|
|
4
|
+
import canonicalScopeValue from './canonicalScopeValue.js';
|
|
5
|
+
import filterQueryToScopeValues from './filterQueryToScopeValues.js';
|
|
6
|
+
import sortableScopeColumns from './sortableScopeColumns.js';
|
|
7
|
+
import { sortableScopeLockKeyForCurrentScope } from './sortableScopeLockKeys.js';
|
|
8
|
+
import { invalidateSortableRowCache } from './sortableRowCache.js';
|
|
9
|
+
import { snapshotScopeValue } from './sortableSnapshot.js';
|
|
6
10
|
import validPosition from './validPosition.js';
|
|
7
11
|
export default async function setPosition(obj) {
|
|
8
12
|
const { dream, txn } = obj;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
await dream.txn(txn).reload();
|
|
12
|
-
}
|
|
13
|
-
else {
|
|
14
|
-
await dream.constructor.transaction(async (txn) => {
|
|
15
|
-
await applyUpdates({ ...obj, txn });
|
|
16
|
-
await dream.txn(txn).reload();
|
|
17
|
-
});
|
|
18
|
-
}
|
|
13
|
+
await applyUpdates(obj);
|
|
14
|
+
await dream.txn(txn).reload();
|
|
19
15
|
}
|
|
20
16
|
async function applyUpdates(obj) {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
const { dream, positionField, scope, txn } = obj;
|
|
18
|
+
// Serialize every writer of this sort scope for the rest of the transaction,
|
|
19
|
+
// before the first position read. The operation-level phase has already
|
|
20
|
+
// acquired this key in its one sorted pass over every sortable field's keys
|
|
21
|
+
// (performSortablePositionWork on create; saveDream, before the driver
|
|
22
|
+
// write, on update), so this asks for a key the transaction holds and issues
|
|
23
|
+
// no statement at all.
|
|
24
|
+
await acquireSortableScopeLocks(dream, txn, [
|
|
25
|
+
sortableScopeLockKeyForCurrentScope(dream, positionField, scope),
|
|
26
|
+
]);
|
|
27
|
+
// A pending scope change that lands on the scope the row already physically
|
|
28
|
+
// occupies moves the record between no scopes at all, and the rest of the
|
|
29
|
+
// write has to see it that way: the scope being vacated *is* the destination,
|
|
30
|
+
// so one source computes a new position from a max the other is about to
|
|
31
|
+
// shift out from under it. The record still counts toward the destination's
|
|
32
|
+
// extent — its row is in the scope, holding the position sentinel in place of
|
|
33
|
+
// the position the snapshot read — so the position write carries that
|
|
34
|
+
// position through to the clamp, and a record that was already last in the
|
|
35
|
+
// scope stays last rather than swapping with the record below it.
|
|
36
|
+
const write = alreadyInDestinationScope(obj)
|
|
37
|
+
? { ...obj, changingScope: false, positionUnderSentinel: obj.previousPosition }
|
|
38
|
+
: obj;
|
|
39
|
+
const position = await validPosition(write);
|
|
40
|
+
await updateConflictingRecords({ ...write, position });
|
|
41
|
+
await updatePositionForRecord({ ...write, position });
|
|
42
|
+
// the shift above moved rows this transaction may be holding preflight reads
|
|
43
|
+
// of — every position a locked batch's later records were read with is now a
|
|
44
|
+
// position they no longer have
|
|
45
|
+
invalidateSortableRowCache(txn);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether a save that means to change sort scope is really a move within one
|
|
49
|
+
* scope: the snapshot read under the lock says the row already sits in the
|
|
50
|
+
* scope the instance is being written into, so nothing joins the destination
|
|
51
|
+
* and nothing is left behind.
|
|
52
|
+
*
|
|
53
|
+
* Values are compared through `canonicalScopeValue`, the same normalization the
|
|
54
|
+
* advisory lock key is derived from, so two shapes of one physical value — the
|
|
55
|
+
* pending `'1.00'` and the hydrated `1` — are one scope here exactly as they
|
|
56
|
+
* are when the key is taken.
|
|
57
|
+
*
|
|
58
|
+
* A row whose snapshot position is null holds no position in that scope, so it
|
|
59
|
+
* is joining the destination's position space and remains a scope change.
|
|
60
|
+
*/
|
|
61
|
+
function alreadyInDestinationScope({ changingScope, dream, previousPosition, scope, snapshot, }) {
|
|
62
|
+
if (!changingScope || snapshot === undefined)
|
|
63
|
+
return false;
|
|
64
|
+
if (previousPosition === undefined || previousPosition === null)
|
|
65
|
+
return false;
|
|
66
|
+
const columns = sortableScopeColumns(dream, scope);
|
|
67
|
+
if (!columns.length)
|
|
68
|
+
return false;
|
|
69
|
+
return columns.every(column => snapshot.scopeValues.has(column) &&
|
|
70
|
+
canonicalScopeValue(dream, column, snapshot.scopeValues.get(column)) ===
|
|
71
|
+
canonicalScopeValue(dream, column, dream[column]));
|
|
24
72
|
}
|
|
25
73
|
async function updateConflictingRecords(obj) {
|
|
26
|
-
const { wasNewRecord, position, previousPosition, dream, positionField, query, scope, txn, changingScope } = obj;
|
|
74
|
+
const { wasNewRecord, position, previousPosition, dream, positionField, query, scope, txn, changingScope, snapshot, } = obj;
|
|
75
|
+
// A scope move spans two unrelated position spaces: `previousPosition` is a
|
|
76
|
+
// position in the scope being left, `position` one in the scope being
|
|
77
|
+
// entered. Only the scope being left is compacted here, and it compacts all
|
|
78
|
+
// the way up — bounding it by a position from the other space closes nothing
|
|
79
|
+
// at all whenever the destination is the smaller of the two, leaving a
|
|
80
|
+
// permanent hole. The destination needs no room made: a scope-changing save
|
|
81
|
+
// always lands at the end of it, because `afterSortableUpdate` passes no
|
|
82
|
+
// position and `validPosition` then returns the destination's max + 1.
|
|
83
|
+
//
|
|
84
|
+
// A record with no position to vacate — a soft-deleted row, whose position
|
|
85
|
+
// column is NULL — leaves the scope it is moving out of untouched.
|
|
86
|
+
if (changingScope && (previousPosition === undefined || previousPosition === null))
|
|
87
|
+
return;
|
|
27
88
|
const increasing = changingScope ||
|
|
28
89
|
position === undefined ||
|
|
29
90
|
(previousPosition !== undefined && previousPosition !== null && previousPosition < position);
|
|
91
|
+
const conflictingPositions = changingScope
|
|
92
|
+
? ops.greaterThanOrEqualTo(previousPosition)
|
|
93
|
+
: increasing
|
|
94
|
+
? range(previousPosition, position)
|
|
95
|
+
: range(position, previousPosition);
|
|
30
96
|
let kyselyQuery = query
|
|
31
97
|
.txn(txn)
|
|
32
98
|
.whereNot({ [dream['_primaryKey']]: dream.primaryKeyValue() })
|
|
33
99
|
.where({
|
|
34
|
-
[positionField]:
|
|
100
|
+
[positionField]: conflictingPositions,
|
|
35
101
|
})
|
|
36
102
|
.toKysely('update')
|
|
37
103
|
.set((eb) => ({
|
|
38
104
|
[positionField]: eb(positionField, increasing ? '-' : '+', 1),
|
|
39
105
|
}));
|
|
40
|
-
|
|
106
|
+
// the range being compacted belongs to the scope the record is *leaving*, so
|
|
107
|
+
// the filter has to name that scope's real column values — the snapshot read
|
|
108
|
+
// under the lock before the write, not what the instance remembers
|
|
109
|
+
kyselyQuery = filterQueryToScopeValues(dream, kyselyQuery, column => snapshotScopeValue(snapshot, column, () => !wasNewRecord && dream.savedChangeToAttribute(column)
|
|
41
110
|
? (dream.changes()[column]?.was ?? dream[column])
|
|
42
|
-
: dream[column], scope);
|
|
111
|
+
: dream[column]), scope);
|
|
43
112
|
await kyselyQuery.execute();
|
|
44
113
|
}
|
|
45
|
-
export function applySortableScopesToQuery(dream, kyselyQuery, whereValueCB, scope) {
|
|
46
|
-
for (const singleScope of scopeArray(scope)) {
|
|
47
|
-
const column = getColumnForSortableScope(dream, singleScope);
|
|
48
|
-
if (column) {
|
|
49
|
-
const columnValue = whereValueCB(column);
|
|
50
|
-
if (columnValue === null) {
|
|
51
|
-
kyselyQuery = kyselyQuery.where(column, 'is', null);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
kyselyQuery = kyselyQuery.where(column, '=', columnValue);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return kyselyQuery;
|
|
59
|
-
}
|
|
60
114
|
async function updatePositionForRecord(obj) {
|
|
61
115
|
const { txn, dream, positionField, position } = obj;
|
|
62
116
|
await txn.kyselyTransaction
|
|
@@ -67,13 +121,3 @@ async function updatePositionForRecord(obj) {
|
|
|
67
121
|
})
|
|
68
122
|
.execute();
|
|
69
123
|
}
|
|
70
|
-
export function getPostgresQueryDriver(connectionName) {
|
|
71
|
-
const queryDriverClass = DreamApp.getOrFail().dbConnectionQueryDriverClass(connectionName);
|
|
72
|
-
if (!(queryDriverClass === PostgresQueryDriver)) {
|
|
73
|
-
throw new Error(`
|
|
74
|
-
${queryDriverClass.name} is not an instance of PostgresQueryDriver.
|
|
75
|
-
You must be using the PostgresQueryDriver when leveraging the Sortable decorator
|
|
76
|
-
`);
|
|
77
|
-
}
|
|
78
|
-
return queryDriverClass;
|
|
79
|
-
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// The cached rows of every live transaction, keyed by the `DreamTransaction`
|
|
2
|
+
// object itself. Module-level state is safe here because the key is an object
|
|
3
|
+
// identity: every transaction — including a nested one — is its own
|
|
4
|
+
// `DreamTransaction` instance, so no lookup can ever see rows stashed by
|
|
5
|
+
// another transaction, whatever request or async flow it belongs to. The keys
|
|
6
|
+
// are held weakly, so an entry becomes collectible with its transaction and
|
|
7
|
+
// nothing accumulates for the life of the process. (The lock ledger in
|
|
8
|
+
// `heldSortableScopeLockKeys.ts` relies on the same properties.)
|
|
9
|
+
const rowsByTransaction = new WeakMap();
|
|
10
|
+
// a JSON tuple rather than a joined string, so that no table name and no
|
|
11
|
+
// primary key value can combine into another pair's key
|
|
12
|
+
function cacheKey(table, primaryKeyValue) {
|
|
13
|
+
return JSON.stringify([table, String(primaryKeyValue)]);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* @internal
|
|
17
|
+
*
|
|
18
|
+
* Stashes rows a locked batch's preflight read under the batch's scope locks,
|
|
19
|
+
* so that the per-record position work that follows does not read them a second
|
|
20
|
+
* time. The preflight reads every candidate's position and sort scope columns
|
|
21
|
+
* to derive the key set; each record's own snapshot read then asks the same
|
|
22
|
+
* question of the same row, in the same transaction, under the same locks.
|
|
23
|
+
*
|
|
24
|
+
* Called once per preflight, with the rows of the pass that converged: an
|
|
25
|
+
* earlier pass read before some of the locks were held, and a preflight that
|
|
26
|
+
* raised holds no lock the rest of the transaction can rely on. The rows are
|
|
27
|
+
* consumed one at a time and the whole entry is dropped by the first position
|
|
28
|
+
* write of the batch, since a compaction moves rows this holds stale positions
|
|
29
|
+
* for — so what it saves is the snapshot reads of the records walked before
|
|
30
|
+
* that first write.
|
|
31
|
+
*
|
|
32
|
+
* @param txn - the batch's transaction
|
|
33
|
+
* @param table - the table the rows came from
|
|
34
|
+
* @param primaryKey - the column the rows are keyed by
|
|
35
|
+
* @param rows - the rows read, carrying at least the position and scope columns
|
|
36
|
+
*/
|
|
37
|
+
export function cacheSortableRowsReadUnderLock(txn, table, primaryKey, rows) {
|
|
38
|
+
rowsByTransaction.set(txn, new Map(rows.map(row => [cacheKey(table, row[primaryKey]), row])));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* @internal
|
|
42
|
+
*
|
|
43
|
+
* The cached row for one record, removed from the cache as it is handed over.
|
|
44
|
+
*
|
|
45
|
+
* Consuming is destructive because the consumer is a position-mutating
|
|
46
|
+
* operation: it is about to compact or renumber the scope the row sits in, and
|
|
47
|
+
* a second read of the same cached row would see the position the row had
|
|
48
|
+
* before that. {@link invalidateSortableRowCache} covers the same hazard for
|
|
49
|
+
* the *other* records a compaction moves.
|
|
50
|
+
*/
|
|
51
|
+
export function takeCachedSortableRow(txn, table, primaryKeyValue) {
|
|
52
|
+
const rows = rowsByTransaction.get(txn);
|
|
53
|
+
if (!rows)
|
|
54
|
+
return undefined;
|
|
55
|
+
const key = cacheKey(table, primaryKeyValue);
|
|
56
|
+
const row = rows.get(key);
|
|
57
|
+
rows.delete(key);
|
|
58
|
+
return row;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* @internal
|
|
62
|
+
*
|
|
63
|
+
* Discards everything cached for this transaction. Called for two reasons: a
|
|
64
|
+
* position write just moved rows the cache cannot identify — a compaction
|
|
65
|
+
* shifts every row in one sort scope above a position, and the cache holds no
|
|
66
|
+
* index by scope — and the batch the rows were read for has finished, after
|
|
67
|
+
* which they describe nothing the rest of a caller-owned transaction should
|
|
68
|
+
* trust. The records still to be walked read their own rows again, which is
|
|
69
|
+
* what they did before the cache existed.
|
|
70
|
+
*/
|
|
71
|
+
export function invalidateSortableRowCache(txn) {
|
|
72
|
+
rowsByTransaction.delete(txn);
|
|
73
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import getColumnForSortableScope from './getColumnForSortableScope.js';
|
|
2
|
+
import scopeArray from './scopeArray.js';
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*
|
|
6
|
+
* The scope members of a sortable field paired with the database columns they
|
|
7
|
+
* resolve to. A member names either the column itself or the BelongsTo
|
|
8
|
+
* association whose foreign key backs it, and a caller matching incoming
|
|
9
|
+
* attributes needs both spellings; a member that resolves to no column is
|
|
10
|
+
* dropped.
|
|
11
|
+
*
|
|
12
|
+
* Every sortable path resolves its scope columns through this one function, so
|
|
13
|
+
* that a lock key, a snapshot read, a group identity and a scope filter can
|
|
14
|
+
* never disagree about which columns a sort scope is made of.
|
|
15
|
+
*/
|
|
16
|
+
export function sortableScopeColumnPairs(dream, scope) {
|
|
17
|
+
return scopeArray(scope).reduce((acc, singleScope) => {
|
|
18
|
+
const column = getColumnForSortableScope(dream, singleScope);
|
|
19
|
+
if (column)
|
|
20
|
+
acc.push([singleScope, column]);
|
|
21
|
+
return acc;
|
|
22
|
+
}, []);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @internal
|
|
26
|
+
*
|
|
27
|
+
* The database columns a sortable field's scope resolves to.
|
|
28
|
+
*/
|
|
29
|
+
export default function sortableScopeColumns(dream, scope) {
|
|
30
|
+
return sortableScopeColumnPairs(dream, scope).map(([, column]) => column);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @internal
|
|
34
|
+
*
|
|
35
|
+
* The sort scope as `[column, value]` pairs — the shape a lock key is derived
|
|
36
|
+
* from and a group is identified by. The value for each column comes from the
|
|
37
|
+
* caller, which decides whether it is reading the in-memory instance or a
|
|
38
|
+
* snapshot of the row.
|
|
39
|
+
*/
|
|
40
|
+
export function sortableScopeValues(dream, scope, valueForColumn) {
|
|
41
|
+
return sortableScopeColumns(dream, scope).map(column => [column, valueForColumn(column)]);
|
|
42
|
+
}
|