@rvoh/dream 2.21.1 → 2.23.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 +71 -23
- package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +3 -0
- package/dist/cjs/src/dream/Query.js +242 -12
- package/dist/cjs/src/dream/QueryDriver/Base.js +26 -0
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +46 -4
- package/dist/cjs/src/dream/QueryDriver/Postgres.js +21 -0
- package/dist/cjs/src/dream/internal/buildSerializerPreloadPaths.js +93 -20
- package/dist/cjs/src/dream/internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js +18 -10
- package/dist/cjs/src/dream/internal/destroyAssociatedRecords.js +76 -4
- package/dist/cjs/src/dream/internal/destroyDream.js +6 -7
- package/dist/cjs/src/dream/internal/mergeSerializerAssociationEdges.js +75 -0
- package/dist/cjs/src/dream/internal/printSerializerHierarchyLevel.js +50 -8
- package/dist/cjs/src/dream/internal/resolveSerializerAssociationEdges.js +79 -14
- package/dist/cjs/src/errors/InvalidBatchSize.js +19 -0
- package/dist/cjs/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
- package/dist/cjs/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
- package/dist/cjs/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -0
- package/dist/cjs/src/errors/serializers/NoSerializersResolvedForKey.js +30 -0
- package/dist/cjs/src/ops/index.js +19 -0
- package/dist/cjs/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +22 -0
- package/dist/cjs/src/serializer/helpers/serializerForAssociatedClass.js +13 -0
- package/dist/esm/src/Dream.js +71 -23
- package/dist/esm/src/dream/DreamClassTransactionBuilder.js +3 -0
- package/dist/esm/src/dream/Query.js +242 -12
- package/dist/esm/src/dream/QueryDriver/Base.js +26 -0
- package/dist/esm/src/dream/QueryDriver/Kysely.js +46 -4
- package/dist/esm/src/dream/QueryDriver/Postgres.js +21 -0
- package/dist/esm/src/dream/internal/buildSerializerPreloadPaths.js +93 -20
- package/dist/esm/src/dream/internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js +18 -10
- package/dist/esm/src/dream/internal/destroyAssociatedRecords.js +76 -4
- package/dist/esm/src/dream/internal/destroyDream.js +6 -7
- package/dist/esm/src/dream/internal/mergeSerializerAssociationEdges.js +75 -0
- package/dist/esm/src/dream/internal/printSerializerHierarchyLevel.js +50 -8
- package/dist/esm/src/dream/internal/resolveSerializerAssociationEdges.js +79 -14
- package/dist/esm/src/errors/InvalidBatchSize.js +19 -0
- package/dist/esm/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
- package/dist/esm/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
- package/dist/esm/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -0
- package/dist/esm/src/errors/serializers/NoSerializersResolvedForKey.js +30 -0
- package/dist/esm/src/ops/index.js +19 -0
- package/dist/esm/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +22 -0
- package/dist/esm/src/serializer/helpers/serializerForAssociatedClass.js +13 -0
- package/dist/types/src/Dream.d.ts +12 -0
- package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +3 -0
- package/dist/types/src/dream/Query.d.ts +150 -3
- package/dist/types/src/dream/QueryDriver/Base.d.ts +21 -0
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +1 -0
- package/dist/types/src/dream/QueryDriver/Postgres.d.ts +20 -0
- package/dist/types/src/dream/internal/destroyAssociatedRecords.d.ts +22 -3
- package/dist/types/src/dream/internal/mergeSerializerAssociationEdges.d.ts +69 -0
- package/dist/types/src/dream/internal/printSerializerHierarchyLevel.d.ts +23 -5
- package/dist/types/src/dream/internal/resolveSerializerAssociationEdges.d.ts +27 -1
- package/dist/types/src/errors/InvalidBatchSize.d.ts +5 -0
- package/dist/types/src/errors/RowLockIncompatibleWithDistinct.d.ts +3 -0
- package/dist/types/src/errors/RowLockIncompatibleWithJoinLoad.d.ts +3 -0
- package/dist/types/src/errors/ops/ArrayEqualityRequiresArrayColumn.d.ts +8 -0
- package/dist/types/src/errors/serializers/NoSerializersResolvedForKey.d.ts +18 -0
- package/dist/types/src/ops/index.d.ts +19 -0
- package/dist/types/src/serializer/helpers/inferSerializerFromDreamOrViewModel.d.ts +16 -0
- package/dist/types/src/types/associations/shared.d.ts +2 -2
- package/dist/types/src/types/dream.d.ts +8 -0
- package/dist/types/src/types/types/associations/shared.ts +84 -55
- package/dist/types/src/types/types/dream.ts +30 -0
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +55 -41
- package/docs/classes/db.PostgresQueryDriver.html +55 -42
- package/docs/classes/db.QueryDriverBase.html +54 -40
- 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.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 +127 -125
- package/docs/classes/index.DreamApp.html +10 -10
- package/docs/classes/index.DreamTransaction.html +2 -2
- package/docs/classes/index.Env.html +2 -2
- package/docs/classes/index.Query.html +161 -71
- 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 +1 -1
- package/docs/functions/utils.sortBy.html +1 -1
- package/docs/functions/utils.sortObjectByKey.html +1 -1
- package/docs/functions/utils.sortObjectByValue.html +1 -1
- package/docs/functions/utils.uncapitalize.html +1 -1
- package/docs/functions/utils.uniq.html +1 -1
- package/docs/hierarchy.html +1 -1
- package/docs/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/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 +38 -23
- 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
|
@@ -1,19 +1,78 @@
|
|
|
1
|
-
import MissingSerializersDefinition from '../../errors/serializers/MissingSerializersDefinition.js';
|
|
2
1
|
import compact from '../../helpers/compact.js';
|
|
3
2
|
import { inferSerializersFromDreamClassOrViewModelClass } from '../../serializer/helpers/inferSerializerFromDreamOrViewModel.js';
|
|
3
|
+
/**
|
|
4
|
+
* Keyed on the two arguments. Weak on both, so an entry is collectable once either the Dream class
|
|
5
|
+
* or the serializer is. The cached arrays are handed out by reference and must be treated as
|
|
6
|
+
* read-only by callers.
|
|
7
|
+
*/
|
|
8
|
+
let resolvedEdgesCache = new WeakMap();
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
*
|
|
12
|
+
* Drops every memoized edge set. Only needed when a class object that stays in the process has its
|
|
13
|
+
* `serializers` getter replaced, which the memo cannot detect — specs that install or remove one
|
|
14
|
+
* around an example. Replacing the model classes themselves needs no call, since the memo keys on
|
|
15
|
+
* the class and serializer objects and entries resolved against the previous ones become
|
|
16
|
+
* unreachable.
|
|
17
|
+
*/
|
|
18
|
+
export function clearResolvedSerializerAssociationEdgesCache() {
|
|
19
|
+
resolvedEdgesCache = new WeakMap();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @internal
|
|
23
|
+
*
|
|
24
|
+
* Resolves the rendersOne/rendersMany/delegatedAttribute attributes of `serializer` to the
|
|
25
|
+
* associations of `dreamClass` they preload, along with the classes and serializers each one
|
|
26
|
+
* targets.
|
|
27
|
+
*
|
|
28
|
+
* Memoized on its two arguments, because every call rebuilds a throwaway serializer builder and
|
|
29
|
+
* re-infers the serializers of every association target, and the traversal in
|
|
30
|
+
* buildSerializerPreloadPaths revisits the same (class, serializer) pair many times.
|
|
31
|
+
*
|
|
32
|
+
* The two arguments are not the function's only inputs, though: resolving a target's serializers
|
|
33
|
+
* also reads that target's `serializers` getter, the STI children registered on it, and the
|
|
34
|
+
* DreamApp serializer registry. Loading a fresh set of classes replaces the keys along with that
|
|
35
|
+
* state, so the memo follows it. Mutating a `serializers` getter in place does not, and needs
|
|
36
|
+
* `clearResolvedSerializerAssociationEdgesCache` — see there.
|
|
37
|
+
*/
|
|
4
38
|
export default function resolveSerializerAssociationEdges(dreamClass, serializer) {
|
|
39
|
+
let cachedForDreamClass = resolvedEdgesCache.get(dreamClass);
|
|
40
|
+
if (cachedForDreamClass === undefined) {
|
|
41
|
+
cachedForDreamClass = new WeakMap();
|
|
42
|
+
resolvedEdgesCache.set(dreamClass, cachedForDreamClass);
|
|
43
|
+
}
|
|
44
|
+
const cached = cachedForDreamClass.get(serializer);
|
|
45
|
+
if (cached !== undefined)
|
|
46
|
+
return cached;
|
|
47
|
+
const edges = buildSerializerAssociationEdges(dreamClass, serializer);
|
|
48
|
+
cachedForDreamClass.set(serializer, edges);
|
|
49
|
+
return edges;
|
|
50
|
+
}
|
|
51
|
+
function buildSerializerAssociationEdges(dreamClass, serializer) {
|
|
5
52
|
const serializerBuilder = serializer(undefined, undefined);
|
|
6
53
|
const serializerAssociations = serializerBuilder['attributes'].filter(attribute => ['rendersOne', 'rendersMany', 'delegatedAttribute'].includes(attribute.type));
|
|
7
54
|
return compact(serializerAssociations.map(serializerAssociation => {
|
|
8
55
|
const serializerAssociationName = serializerAssociation.targetName ??
|
|
9
56
|
serializerAssociation.name;
|
|
57
|
+
// `dreamClass` is the class the walk is rooted at, which for an STI base is the base itself
|
|
58
|
+
// even while `serializer` is one child's serializer. That pairing is safe because an STI child
|
|
59
|
+
// cannot declare associations of its own — the `STI` class decorator throws
|
|
60
|
+
// StiChildCannotDefineNewAssociations — so every association a child's serializer can render
|
|
61
|
+
// is declared on the base and resolves here.
|
|
62
|
+
//
|
|
63
|
+
// The one hole: that guard reads decorator metadata and
|
|
64
|
+
// `associationDeclarationNamesFromMetadata` returns [] when the metadata is absent, which is
|
|
65
|
+
// also what happens when the `STI` decorator runs without a `context`. Under a TS config that
|
|
66
|
+
// does not emit decorator metadata the guard silently passes, a child-declared association
|
|
67
|
+
// survives, and it is dropped *here*, silently, by the return below — the symptom is then a
|
|
68
|
+
// missing preload and a NonLoadedAssociation at render, not a decorator error naming the real
|
|
69
|
+
// problem.
|
|
10
70
|
const association = dreamClass['getAssociationMetadata'](serializerAssociationName);
|
|
11
71
|
if (!association)
|
|
12
72
|
return null;
|
|
13
73
|
if (serializerAssociation.type === 'delegatedAttribute') {
|
|
14
74
|
return {
|
|
15
75
|
associationAs: association.as,
|
|
16
|
-
sourceDreamClass: dreamClass,
|
|
17
76
|
type: serializerAssociation.type,
|
|
18
77
|
serializerAssociationName,
|
|
19
78
|
targets: [],
|
|
@@ -26,17 +85,24 @@ export default function resolveSerializerAssociationEdges(dreamClass, serializer
|
|
|
26
85
|
? maybeAssociatedClasses
|
|
27
86
|
: [maybeAssociatedClasses];
|
|
28
87
|
const targets = associatedClasses.flatMap(associatedClass => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
88
|
+
// Deliberately unguarded: every serializer that cannot be resolved while building preload
|
|
89
|
+
// paths throws, and there is no tolerance here by design.
|
|
90
|
+
//
|
|
91
|
+
// Why: an unresolvable serializer is a configuration error, and the first query that walks
|
|
92
|
+
// the serializer graph is where it should be found — no rows, no rendering required.
|
|
93
|
+
// Swallowing it means the only way to discover a broken `serializerKey` (or a missing
|
|
94
|
+
// `serializers` getter) is a spec that creates a row of the affected class *and* renders it.
|
|
95
|
+
//
|
|
96
|
+
// Why the obvious tolerance is also wrong, not merely undesirable:
|
|
97
|
+
// `inferSerializersFromDreamClassOrViewModelClass` below expands STI and throws on the
|
|
98
|
+
// *first* child that fails, so catching here would drop the serializers of *every* child of
|
|
99
|
+
// this target. An edge with no targets is treated as terminal by buildSerializerPreloadPaths,
|
|
100
|
+
// so the association is preloaded but never descended into, and a row of a *well-formed*
|
|
101
|
+
// sibling fails at render with NonLoadedAssociation — an error naming a class that has
|
|
102
|
+
// nothing wrong with it.
|
|
103
|
+
const serializers = serializerAssociation.options.serializer
|
|
104
|
+
? compact([serializerAssociation.options.serializer])
|
|
105
|
+
: compact(inferSerializersFromDreamClassOrViewModelClass(associatedClass, serializerAssociation.options.serializerKey));
|
|
40
106
|
return serializers.map(associatedSerializer => ({
|
|
41
107
|
dreamClass: associatedClass,
|
|
42
108
|
serializer: associatedSerializer,
|
|
@@ -44,7 +110,6 @@ export default function resolveSerializerAssociationEdges(dreamClass, serializer
|
|
|
44
110
|
});
|
|
45
111
|
return {
|
|
46
112
|
associationAs: association.as,
|
|
47
|
-
sourceDreamClass: dreamClass,
|
|
48
113
|
type: serializerAssociation.type,
|
|
49
114
|
serializerAssociationName,
|
|
50
115
|
targets,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default class InvalidBatchSize extends Error {
|
|
2
|
+
batchSize;
|
|
3
|
+
constructor(batchSize) {
|
|
4
|
+
super();
|
|
5
|
+
this.batchSize = batchSize;
|
|
6
|
+
}
|
|
7
|
+
get message() {
|
|
8
|
+
return `
|
|
9
|
+
Invalid batchSize provided.
|
|
10
|
+
|
|
11
|
+
batchSize given:
|
|
12
|
+
${String(this.batchSize)}
|
|
13
|
+
|
|
14
|
+
batchSize must be a positive integer. A batchSize of 0 is especially dangerous,
|
|
15
|
+
since a limit of 0 is treated as "no limit", which would collapse a batched
|
|
16
|
+
operation into a single unbounded pass over the entire matched set.
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default class RowLockIncompatibleWithDistinct extends Error {
|
|
2
|
+
get message() {
|
|
3
|
+
// `SELECT DISTINCT ON (...) ... FOR UPDATE` is rejected by the database
|
|
4
|
+
// ("FOR UPDATE is not allowed with DISTINCT clause"), so fail here, with a
|
|
5
|
+
// message that names the two incompatible parts, rather than surfacing a
|
|
6
|
+
// raw driver error from inside a per-batch transaction.
|
|
7
|
+
return `
|
|
8
|
+
An exclusive row lock was requested on a query that also calls \`distinct\`.
|
|
9
|
+
|
|
10
|
+
A locked read cannot be combined with DISTINCT. Drop the \`distinct\` call, or
|
|
11
|
+
drop the lock.
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default class RowLockIncompatibleWithJoinLoad extends Error {
|
|
2
|
+
get message() {
|
|
3
|
+
// A join load builds its own select statement (one row per joined
|
|
4
|
+
// association row, later collapsed in memory), which cannot carry the
|
|
5
|
+
// Query's row lock. Rather than silently performing an unlocked read and
|
|
6
|
+
// dropping the compare-and-set guarantee the caller asked for, refuse.
|
|
7
|
+
return `
|
|
8
|
+
An exclusive row lock was requested on a query that also uses leftJoinPreload.
|
|
9
|
+
|
|
10
|
+
A join-loaded query cannot be locked. Use \`preload\` instead of
|
|
11
|
+
\`leftJoinPreload\`, or drop the lock.
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export default class ArrayEqualityRequiresArrayColumn extends Error {
|
|
2
|
+
dreamClass;
|
|
3
|
+
column;
|
|
4
|
+
operator;
|
|
5
|
+
constructor(dreamClass, column, operator) {
|
|
6
|
+
super();
|
|
7
|
+
this.dreamClass = dreamClass;
|
|
8
|
+
this.column = column;
|
|
9
|
+
this.operator = operator;
|
|
10
|
+
}
|
|
11
|
+
get message() {
|
|
12
|
+
return `
|
|
13
|
+
Attempting to compare an array for equality against a column that is not an
|
|
14
|
+
array in the database:
|
|
15
|
+
|
|
16
|
+
${this.dreamClass.sanitizedName}#${this.column} (operator: ${this.operator})
|
|
17
|
+
|
|
18
|
+
Whole-array equality (\`=\`, \`!=\`, \`<>\`) is only meaningful for array columns.
|
|
19
|
+
If you meant "is this column one of these values", use \`ops.in([...])\`
|
|
20
|
+
(or a bare array) instead.
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised when resolving a serializer key against a class yields an empty set of serializers.
|
|
3
|
+
*
|
|
4
|
+
* This is an internal-invariant failure, not a misconfiguration: an unresolvable serializer key
|
|
5
|
+
* raises `MissingSerializersDefinitionForKey` (and a class with no `serializers` getter at all
|
|
6
|
+
* raises `MissingSerializersDefinition`) while it is being resolved, and STI expansion always
|
|
7
|
+
* yields at least one class, so there is no supported application shape that reaches this. It is
|
|
8
|
+
* typed rather than a bare `Error` so that it can be asserted on, and so that the two callers that
|
|
9
|
+
* guard the invariant raise the same thing.
|
|
10
|
+
*/
|
|
11
|
+
export default class NoSerializersResolvedForKey extends Error {
|
|
12
|
+
classDef;
|
|
13
|
+
serializerKey;
|
|
14
|
+
constructor(classDef, serializerKey) {
|
|
15
|
+
super();
|
|
16
|
+
this.classDef = classDef;
|
|
17
|
+
this.serializerKey = serializerKey;
|
|
18
|
+
}
|
|
19
|
+
get message() {
|
|
20
|
+
const className = this.classDef === null || this.classDef === undefined
|
|
21
|
+
? String(this.classDef)
|
|
22
|
+
: (this.classDef.globalName ?? this.classDef.name);
|
|
23
|
+
return `
|
|
24
|
+
No serializers resolved for \`${className}\` under serializer key \`${this.serializerKey}\`
|
|
25
|
+
|
|
26
|
+
This should be impossible — a serializer key that cannot be resolved throws while it is being
|
|
27
|
+
resolved, before reaching here — and indicates a bug in Dream rather than a problem with your
|
|
28
|
+
application. Please report it.`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -117,11 +117,27 @@ const ops = {
|
|
|
117
117
|
/**
|
|
118
118
|
* Creates an `OpsStatement` using the `=` operator for strict equality.
|
|
119
119
|
*
|
|
120
|
+
* **Array columns:** passing an array means literal array equality — the
|
|
121
|
+
* whole array is bound as a single parameter, so the column must match
|
|
122
|
+
* element-for-element, in order. This is the opt-in counterpart to a bare
|
|
123
|
+
* array in a where clause (`Post.where({ tags: ['a', 'b'] })`), which means
|
|
124
|
+
* `IN`, and to `ops.any`, which tests for containment of a single element.
|
|
125
|
+
* Element types are constrained by the column, so an enum array column
|
|
126
|
+
* rejects a string that is not a member of its enum.
|
|
127
|
+
*
|
|
128
|
+
* Note that PostgreSQL defines no equality operator for `json` (as opposed
|
|
129
|
+
* to `jsonb`), so `ops.equal` against a `json[]` column raises at the
|
|
130
|
+
* database.
|
|
131
|
+
*
|
|
120
132
|
* @param equal - The value the column must equal.
|
|
121
133
|
* @returns An `OpsStatement` using the `=` operator.
|
|
122
134
|
*
|
|
123
135
|
* @example
|
|
124
136
|
* User.where({ role: ops.equal('admin') })
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* // matches only rows whose tags column is exactly ['a', 'b'], in that order
|
|
140
|
+
* Post.where({ tags: ops.equal(['a', 'b']) })
|
|
125
141
|
*/
|
|
126
142
|
equal: (equal) => new OpsStatement('=', equal),
|
|
127
143
|
/**
|
|
@@ -260,6 +276,9 @@ const ops = {
|
|
|
260
276
|
/**
|
|
261
277
|
* Creates an `OpsStatement` using the `!=` operator for inequality.
|
|
262
278
|
*
|
|
279
|
+
* **Array columns:** as with `ops.equal`, passing an array compares against
|
|
280
|
+
* the whole array rather than against its elements.
|
|
281
|
+
*
|
|
263
282
|
* @param equal - The value the column must not equal.
|
|
264
283
|
* @returns An `OpsStatement` using the `!=` operator.
|
|
265
284
|
*
|
|
@@ -3,6 +3,7 @@ import MissingSerializersDefinition from '../../errors/serializers/MissingSerial
|
|
|
3
3
|
import MissingSerializersDefinitionForKey from '../../errors/serializers/MissingSerializersDefinitionForKey.js';
|
|
4
4
|
import NoGlobalSerializerForSpecifiedKey from '../../errors/serializers/NoGlobalSerializerForSpecifiedKey.js';
|
|
5
5
|
import NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey from '../../errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.js';
|
|
6
|
+
import NoSerializersResolvedForKey from '../../errors/serializers/NoSerializersResolvedForKey.js';
|
|
6
7
|
import compact from '../../helpers/compact.js';
|
|
7
8
|
import expandStiClasses from '../../helpers/sti/expandStiClasses.js';
|
|
8
9
|
import uniq from '../../helpers/uniq.js';
|
|
@@ -35,3 +36,24 @@ export function inferSerializersFromDreamClassOrViewModelClass(classDef, seriali
|
|
|
35
36
|
const serializers = classes.map(classDef => inferSerializerFromDreamOrViewModel(classDef.prototype, serializerKey));
|
|
36
37
|
return uniq(compact(serializers));
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* As `inferSerializersFromDreamClassOrViewModelClass`, but guarantees a non-empty result.
|
|
41
|
+
*
|
|
42
|
+
* The guard is unreachable today, and deliberately not phrased as "no serializer found for key": a
|
|
43
|
+
* serializer key that a class does not register throws `MissingSerializersDefinitionForKey` (and a
|
|
44
|
+
* class with no `serializers` getter at all throws `MissingSerializersDefinition`) from inside the
|
|
45
|
+
* inference above, and `expandStiClasses` always yields at least one class. It exists only so that
|
|
46
|
+
* a future change to any of those cannot turn an unresolvable serializer key into a silently empty
|
|
47
|
+
* serializer set — which, for the callers below, means a silently empty preload set and a
|
|
48
|
+
* `NonLoadedAssociation` at render rather than an error naming the real problem.
|
|
49
|
+
*
|
|
50
|
+
* Used by everything that walks a serializer graph rooted on a class rather than an instance —
|
|
51
|
+
* preload-path building and serialization display — so that they raise one typed error between
|
|
52
|
+
* them.
|
|
53
|
+
*/
|
|
54
|
+
export function inferSerializersFromDreamClassOrViewModelClassOrFail(classDef, serializerKey) {
|
|
55
|
+
const serializers = inferSerializersFromDreamClassOrViewModelClass(classDef, serializerKey);
|
|
56
|
+
if (serializers.length === 0)
|
|
57
|
+
throw new NoSerializersResolvedForKey(classDef, serializerKey);
|
|
58
|
+
return serializers;
|
|
59
|
+
}
|
|
@@ -15,5 +15,18 @@ export function serializerForAssociatedClass(object, associationName, options) {
|
|
|
15
15
|
const associatedClass = association.modelCB();
|
|
16
16
|
if (Array.isArray(associatedClass))
|
|
17
17
|
throw new Error('rendersOne flatten is incompatible with a polymorphic belongs-to association');
|
|
18
|
+
// Taking the first is only a narrowing when `associatedClass` is an STI base, where
|
|
19
|
+
// inferSerializersFromDreamClassOrViewModelClass returns one serializer per child sorted by
|
|
20
|
+
// sanitizedName. It is correct here because there is no associated row: this function is reached
|
|
21
|
+
// only from SerializerRenderer's `flatten: true` branch for a *null* associated object, so no
|
|
22
|
+
// child of the base is the right one — the row that would have decided is absent. (rendersMany
|
|
23
|
+
// never reaches here and does not accept `flatten`.)
|
|
24
|
+
//
|
|
25
|
+
// It is observable, though, so it is not "nothing happens": the picked serializer is invoked and
|
|
26
|
+
// its output spread into the parent's payload, so the parent JSON carries the
|
|
27
|
+
// alphabetically-first child's flattened keys as nulls, and omits keys only a later-sorting child
|
|
28
|
+
// would flatten. Unioning every child's serializer here — which is what buildSerializerPreloadPaths
|
|
29
|
+
// does for preload paths — is meaningless: exactly one set of keys can be flattened into the
|
|
30
|
+
// payload.
|
|
18
31
|
return inferSerializersFromDreamClassOrViewModelClass(associatedClass, options.serializerKey)[0] ?? null;
|
|
19
32
|
}
|
|
@@ -429,6 +429,15 @@ export default class Dream {
|
|
|
429
429
|
private static setGlobalName;
|
|
430
430
|
private static serializationMap;
|
|
431
431
|
private static displaySerialization;
|
|
432
|
+
/**
|
|
433
|
+
* @internal
|
|
434
|
+
*
|
|
435
|
+
* Maps the association tree reachable from `serializers` — every serializer that can render this
|
|
436
|
+
* class at this point in the walk, which is more than one when this class is an STI base. The
|
|
437
|
+
* serializers are merged into one edge set (see mergeSerializerAssociationEdges) rather than
|
|
438
|
+
* walked one at a time, so an association several of them render is visited once and the nested
|
|
439
|
+
* trees of a shared target merge instead of clobbering each other.
|
|
440
|
+
*/
|
|
432
441
|
private static recursiveSerializationMap;
|
|
433
442
|
/**
|
|
434
443
|
* Returns the column names for the given model
|
|
@@ -1011,6 +1020,9 @@ export default class Dream {
|
|
|
1011
1020
|
* // User{email: 'goodbye@world'}
|
|
1012
1021
|
* ```
|
|
1013
1022
|
*
|
|
1023
|
+
* Records are always visited in ascending primary key order; any `order`
|
|
1024
|
+
* applied to the query is discarded. See {@link Query.findEach} for why.
|
|
1025
|
+
*
|
|
1014
1026
|
* @param cb - The callback to call for each found record
|
|
1015
1027
|
* @param opts - Optional parameters for batch processing
|
|
1016
1028
|
* @param opts.batchSize - the batch size you wish to collect records in. If not provided, it will default to 1000
|
|
@@ -384,6 +384,9 @@ export default class DreamClassTransactionBuilder<DreamClass extends typeof Drea
|
|
|
384
384
|
* // User{email: 'goodbye@world'}
|
|
385
385
|
* ```
|
|
386
386
|
*
|
|
387
|
+
* Records are always visited in ascending primary key order; any `order`
|
|
388
|
+
* applied to the query is discarded. See {@link Query.findEach} for why.
|
|
389
|
+
*
|
|
387
390
|
* @param cb - The callback to call for each found record
|
|
388
391
|
* @param options.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000
|
|
389
392
|
* @returns void
|
|
@@ -7,6 +7,16 @@ import { CursorPaginatedDreamQueryOptions, CursorPaginatedDreamQueryResult, Defa
|
|
|
7
7
|
import { JoinedAssociation, JoinedAssociationsTypeFromAssociations, QueryTypeOptions, VariadicJoinsArgs, VariadicLeftJoinLoadArgs, VariadicLoadArgs } from '../types/variadic.js';
|
|
8
8
|
import DreamTransaction from './DreamTransaction.js';
|
|
9
9
|
import QueryDriverBase from './QueryDriver/Base.js';
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
*
|
|
13
|
+
* Drops every memoized preload path. Only needed when a class object that stays in the process has
|
|
14
|
+
* its `serializers` getter replaced, which the cache cannot detect — specs that install or remove
|
|
15
|
+
* one around an example. Replacing the model classes themselves needs no call, since the cache
|
|
16
|
+
* keys on the class object. See `clearResolvedSerializerAssociationEdgesCache`, which has the same
|
|
17
|
+
* contract and is cleared alongside it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function clearSerializerPreloadPathsCache(): void;
|
|
10
20
|
export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Readonly<QueryTypeOptions> = DefaultQueryTypeOptions<DreamInstance>> {
|
|
11
21
|
/**
|
|
12
22
|
* @internal
|
|
@@ -16,6 +26,7 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
|
|
|
16
26
|
*/
|
|
17
27
|
static readonly BATCH_SIZES: {
|
|
18
28
|
FIND_EACH: number;
|
|
29
|
+
LOCKED_DESTROY: number;
|
|
19
30
|
PLUCK_EACH: number;
|
|
20
31
|
PLUCK_EACH_THROUGH: number;
|
|
21
32
|
};
|
|
@@ -282,13 +293,30 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
|
|
|
282
293
|
* records matching the Query have been fetched.
|
|
283
294
|
*
|
|
284
295
|
* ```ts
|
|
285
|
-
* await User.
|
|
296
|
+
* await User.where({ deletedAt: null }).findEach(user => {
|
|
286
297
|
* DreamApp.log(user)
|
|
287
298
|
* })
|
|
288
299
|
* // User{id: 1}
|
|
289
300
|
* // User{id: 2}
|
|
290
301
|
* ```
|
|
291
302
|
*
|
|
303
|
+
* IMPORTANT: `findEach` always iterates in ascending primary key order.
|
|
304
|
+
* Any `order` applied to the Query is discarded; passing one has no effect
|
|
305
|
+
* on the order records are visited in.
|
|
306
|
+
*
|
|
307
|
+
* This is not a limitation that can be lifted. `findEach` guarantees that
|
|
308
|
+
* every matching record is visited exactly once, and it delivers that by
|
|
309
|
+
* paginating on a keyset: each batch selects the next `batchSize` records
|
|
310
|
+
* with a primary key greater than the last record of the previous batch.
|
|
311
|
+
* That guarantee only holds over a stable, unique key. Ordering by a
|
|
312
|
+
* mutable, non-unique column would mean a record whose sort value changed
|
|
313
|
+
* partway through the iteration could be skipped entirely or visited twice.
|
|
314
|
+
* ({@link Query.cursorPaginate} can honor an arbitrary order precisely
|
|
315
|
+
* because a paginated UI tolerates that anomaly; a full traversal cannot.)
|
|
316
|
+
*
|
|
317
|
+
* If you need records processed in a particular order, load them with a
|
|
318
|
+
* regular ordered query instead of `findEach`.
|
|
319
|
+
*
|
|
292
320
|
* @param cb - The callback to call for each found record
|
|
293
321
|
* @param __namedParameters - Options for batch processing
|
|
294
322
|
* @param __namedParameters.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000
|
|
@@ -1209,17 +1237,131 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
|
|
|
1209
1237
|
* // 12
|
|
1210
1238
|
* ```
|
|
1211
1239
|
*
|
|
1240
|
+
* ## Guarded (compare-and-set) destroy
|
|
1241
|
+
*
|
|
1242
|
+
* By default, `destroy` reads the matching records and then destroys each of
|
|
1243
|
+
* them, so a concurrent writer can change a record in between — the record is
|
|
1244
|
+
* destroyed even though it no longer matches the Query that selected it.
|
|
1245
|
+
* Passing `lock: true` closes that window, making `destroy` a compare-and-set
|
|
1246
|
+
* the way `update(..., { skipHooks: true })` already is:
|
|
1247
|
+
*
|
|
1248
|
+
* ```ts
|
|
1249
|
+
* const destroyed = await Order.where({ status: 'pending' }).destroy({ lock: true })
|
|
1250
|
+
* // 0 — someone else moved every one of them out of 'pending' first
|
|
1251
|
+
* ```
|
|
1252
|
+
*
|
|
1253
|
+
* With `lock: true`, records are processed in batches, and each batch runs in
|
|
1254
|
+
* its own transaction which first re-selects that batch's records with an
|
|
1255
|
+
* exclusive row lock and then destroys them before committing. Because the
|
|
1256
|
+
* database re-checks the Query's conditions after acquiring each row lock,
|
|
1257
|
+
* a record a concurrent transaction has already moved out of the Query drops
|
|
1258
|
+
* out of the locked read and is never destroyed. The returned count is the
|
|
1259
|
+
* number of records actually claimed, so a caller can detect that it lost a
|
|
1260
|
+
* race by comparing the count against what it expected.
|
|
1261
|
+
*
|
|
1262
|
+
* Things to know before relying on it:
|
|
1263
|
+
*
|
|
1264
|
+
* - **The guarantee is per batch, not set-wide.** A run spanning more than one
|
|
1265
|
+
* batch can win a race in batch 1 and lose one in batch 2; nothing holds the
|
|
1266
|
+
* whole matched set still for the duration of the run. Sharing one
|
|
1267
|
+
* transaction across every batch — by threading a transaction onto the Query
|
|
1268
|
+
* itself, `Order.query().txn(txn).where({ ... }).destroy({ lock: true })` —
|
|
1269
|
+
* buys two things: the destroys become all-or-nothing (a rollback undoes the
|
|
1270
|
+
* whole run), and the row locks taken by every batch are held until you
|
|
1271
|
+
* commit. It does **not** extend the compare-and-set to the part of the set
|
|
1272
|
+
* the run has not reached yet: under READ COMMITTED each statement takes a
|
|
1273
|
+
* fresh snapshot, so a record moved out of the Query between batch 1 and
|
|
1274
|
+
* batch 2 still drops out of batch 2's locked read. A genuinely set-wide
|
|
1275
|
+
* guarantee needs REPEATABLE READ / SERIALIZABLE, or a single locking
|
|
1276
|
+
* statement over the whole set.
|
|
1277
|
+
* - **Only a transaction carried by the Query is shared.** Merely wrapping the
|
|
1278
|
+
* call in `Model.transaction(...)` does not thread that transaction onto the
|
|
1279
|
+
* Query — `Dream.transaction` establishes no ambient transaction, it only
|
|
1280
|
+
* hands a `txn` to your callback — so each batch would open its own
|
|
1281
|
+
* transaction on a different pooled connection. Worse, if the surrounding
|
|
1282
|
+
* transaction has already written to any of the matched rows, each batch's
|
|
1283
|
+
* locked read blocks on locks the surrounding transaction holds while that
|
|
1284
|
+
* transaction awaits the destroy: a cycle across two connections, which
|
|
1285
|
+
* Postgres's deadlock detector cannot see, so it hangs rather than aborting.
|
|
1286
|
+
* Always pass the transaction with `.txn(txn)`.
|
|
1287
|
+
* - **Behavior above READ COMMITTED differs.** The drop-out-of-the-result
|
|
1288
|
+
* behavior described above is READ COMMITTED (Postgres's default). Under
|
|
1289
|
+
* REPEATABLE READ or SERIALIZABLE, the same race raises a serialization
|
|
1290
|
+
* failure instead of silently returning a lower count. Both are safe; only
|
|
1291
|
+
* one of them is quiet.
|
|
1292
|
+
* - **`batchSize` defaults to 10 here**, not `findEach`'s 1000, and the
|
|
1293
|
+
* default is deliberately small. Every row in a batch stays locked for the
|
|
1294
|
+
* whole batch, including the time spent running each record's hooks and
|
|
1295
|
+
* `dependent: 'destroy'` cascade, so an oversized batch blocks unrelated
|
|
1296
|
+
* writers touching those rows and surfaces as timeouts somewhere else in the
|
|
1297
|
+
* application, which is very hard to trace back to this destroy. Too small a
|
|
1298
|
+
* batch, by contrast, only slows down this call, which you can see and fix.
|
|
1299
|
+
* Lower it further for models with deep `dependent: 'destroy'` cascades or
|
|
1300
|
+
* expensive destroy hooks.
|
|
1301
|
+
* - **A batch waits, without bound, for whoever holds the row.** The locked
|
|
1302
|
+
* read is a plain `FOR UPDATE`: if another transaction already holds a lock
|
|
1303
|
+
* on one of the batch's rows, this call blocks until that transaction ends,
|
|
1304
|
+
* holding its own transaction and its pooled connection open the whole
|
|
1305
|
+
* time. Dream sets no `lock_timeout` or `statement_timeout`, so a
|
|
1306
|
+
* long-running writer on the other side stalls the run indefinitely and, at
|
|
1307
|
+
* scale, ties up connections. If that is a risk in your application, set a
|
|
1308
|
+
* `lock_timeout` on the connection so a stuck batch fails instead of
|
|
1309
|
+
* hanging.
|
|
1310
|
+
* - **If you are destroying a large set and do not need compare-and-set, do
|
|
1311
|
+
* not pass `lock`.** Plain `destroy()` (hooks and cascade, no locking) or
|
|
1312
|
+
* {@link Query.delete | delete} (a single statement, no hooks or cascade)
|
|
1313
|
+
* are the right tools for bulk removal.
|
|
1314
|
+
*
|
|
1212
1315
|
* @param __namedParameters - Options for destroying the instance
|
|
1213
1316
|
* @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
|
|
1214
1317
|
* @param __namedParameters.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
1318
|
+
* @param __namedParameters.lock - If true, each batch is re-selected with an exclusive row lock inside its own transaction before being destroyed, making the destroy a compare-and-set. Defaults to false
|
|
1319
|
+
* @param __namedParameters.batchSize - The number of records to process per batch. Must be a positive integer. Defaults to 10 when `lock` is true, and to 1000 otherwise
|
|
1215
1320
|
* @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
|
|
1216
1321
|
* @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
|
|
1217
1322
|
* @returns The number of records that were removed
|
|
1323
|
+
* @throws InvalidBatchSize if `batchSize` is not a positive integer
|
|
1218
1324
|
*/
|
|
1219
|
-
destroy({ skipHooks, cascade, }?: {
|
|
1325
|
+
destroy({ skipHooks, cascade, lock, batchSize, }?: {
|
|
1220
1326
|
skipHooks?: boolean | undefined;
|
|
1221
1327
|
cascade?: boolean | undefined;
|
|
1328
|
+
lock?: boolean | undefined;
|
|
1329
|
+
batchSize?: number | undefined;
|
|
1222
1330
|
}): Promise<number>;
|
|
1331
|
+
/**
|
|
1332
|
+
* @internal
|
|
1333
|
+
*
|
|
1334
|
+
* The compare-and-set implementation behind `destroy({ lock: true })`.
|
|
1335
|
+
*
|
|
1336
|
+
* Each batch is handled in two steps inside one transaction:
|
|
1337
|
+
*
|
|
1338
|
+
* 1. an unlocked pluck of the next `batchSize` primary keys, in ascending
|
|
1339
|
+
* primary key order, which establishes the batch's window (its keyset
|
|
1340
|
+
* cursor) and tells us how many records were *attempted*. Only the keys are
|
|
1341
|
+
* read: hydrating and preloading these records would duplicate the work
|
|
1342
|
+
* step 2 does over the same rows; then
|
|
1343
|
+
* 2. a locked re-read restricted to exactly those primary keys, which
|
|
1344
|
+
* re-applies every condition on the Query while acquiring an exclusive row
|
|
1345
|
+
* lock on each row it returns. Records a concurrent transaction has moved
|
|
1346
|
+
* out of the Query since step 1 do not come back, and are the records this
|
|
1347
|
+
* run lost the race for.
|
|
1348
|
+
*
|
|
1349
|
+
* The count comes from step 2's result set, which is why it reports records
|
|
1350
|
+
* actually claimed rather than records attempted. (One thing it does not
|
|
1351
|
+
* account for, because it never has: a `beforeDestroy` hook that sets
|
|
1352
|
+
* `_preventDeletion` leaves the record in place but still counts.)
|
|
1353
|
+
*
|
|
1354
|
+
* The two-step shape is what keeps iteration correct. If the locked read both
|
|
1355
|
+
* claimed rows and advanced the cursor, a batch whose records all lost the
|
|
1356
|
+
* race would come back empty and end the run early, silently skipping every
|
|
1357
|
+
* record after it. Advancing the cursor from step 1 instead means losing a
|
|
1358
|
+
* race skips only the records that were lost.
|
|
1359
|
+
*
|
|
1360
|
+
* When the Query already carries a transaction, that transaction is used for
|
|
1361
|
+
* every batch rather than opening one per batch, so the caller's transaction
|
|
1362
|
+
* boundary wins and the locks are held until the caller commits.
|
|
1363
|
+
*/
|
|
1364
|
+
private lockedDestroy;
|
|
1223
1365
|
/**
|
|
1224
1366
|
* Destroys all records matching the Query,
|
|
1225
1367
|
* ignoring the SoftDelete decorator and
|
|
@@ -1241,13 +1383,18 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
|
|
|
1241
1383
|
* @param __namedParameters - Options for destroying the instance
|
|
1242
1384
|
* @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
|
|
1243
1385
|
* @param __namedParameters.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
1386
|
+
* @param __namedParameters.lock - If true, each batch is re-selected with an exclusive row lock inside its own transaction before being destroyed, making the destroy a compare-and-set. See {@link Query.destroy} for the full semantics. Defaults to false
|
|
1387
|
+
* @param __namedParameters.batchSize - The number of records to process per batch. Must be a positive integer. Defaults to 10 when `lock` is true, and to 1000 otherwise
|
|
1244
1388
|
* @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
|
|
1245
1389
|
* @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
|
|
1246
1390
|
* @returns The number of records that were removed
|
|
1391
|
+
* @throws InvalidBatchSize if `batchSize` is not a positive integer
|
|
1247
1392
|
*/
|
|
1248
|
-
reallyDestroy({ skipHooks, cascade, }?: {
|
|
1393
|
+
reallyDestroy({ skipHooks, cascade, lock, batchSize, }?: {
|
|
1249
1394
|
skipHooks?: boolean;
|
|
1250
1395
|
cascade?: boolean;
|
|
1396
|
+
lock?: boolean;
|
|
1397
|
+
batchSize?: number;
|
|
1251
1398
|
}): Promise<number>;
|
|
1252
1399
|
/**
|
|
1253
1400
|
* Undestroys a SoftDelete model, unsetting
|
|
@@ -190,7 +190,28 @@ export default class QueryDriverBase<DreamInstance extends Dream> {
|
|
|
190
190
|
*/
|
|
191
191
|
takeAll(options?: {
|
|
192
192
|
columns?: DreamColumnNames<DreamInstance>[];
|
|
193
|
+
lock?: boolean;
|
|
193
194
|
}): Promise<DreamInstance[]>;
|
|
195
|
+
/**
|
|
196
|
+
* @internal
|
|
197
|
+
*
|
|
198
|
+
* Adapter seam for exclusive row locking. Given the select statement about to
|
|
199
|
+
* be executed, return a statement that additionally acquires an exclusive
|
|
200
|
+
* lock on each row it returns, held until the enclosing transaction commits
|
|
201
|
+
* or rolls back (Postgres: `SELECT ... FOR UPDATE OF <table>`).
|
|
202
|
+
*
|
|
203
|
+
* This is only ever reached when a caller opts into a locked read (today,
|
|
204
|
+
* `Query#destroy({ lock: true })`), and only from inside a transaction. The
|
|
205
|
+
* base implementation throws so that an adapter which has not implemented row
|
|
206
|
+
* locking fails loud rather than silently performing an unlocked read and
|
|
207
|
+
* quietly dropping the compare-and-set guarantee the caller asked for.
|
|
208
|
+
*
|
|
209
|
+
* @param kyselyQuery - the select statement to lock
|
|
210
|
+
* @param tableAlias - the alias of the Query's base table; only rows of that
|
|
211
|
+
* table are locked, so a joined query does not lock unrelated rows and does
|
|
212
|
+
* not fail on the nullable side of an outer join
|
|
213
|
+
*/
|
|
214
|
+
applyRowLock(kyselyQuery: SelectQueryBuilder<any, any, any>, tableAlias: string): SelectQueryBuilder<any, any, any>;
|
|
194
215
|
/**
|
|
195
216
|
* Retrieves the max value of the specified column
|
|
196
217
|
* for this Query
|
|
@@ -142,6 +142,7 @@ export default class KyselyQueryDriver<DreamInstance extends Dream> extends Quer
|
|
|
142
142
|
*/
|
|
143
143
|
takeAll(options?: {
|
|
144
144
|
columns?: DreamColumnNames<DreamInstance>[];
|
|
145
|
+
lock?: boolean;
|
|
145
146
|
}): Promise<DreamInstance[]>;
|
|
146
147
|
/**
|
|
147
148
|
* Retrieves the max value of the specified column
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SelectQueryBuilder } from 'kysely';
|
|
1
2
|
import Dream from '../../Dream.js';
|
|
2
3
|
import { SchemaBuilderAssociationData, SchemaBuilderColumnData } from '../../helpers/cli/ASTBuilder.js';
|
|
3
4
|
import KyselyQueryDriver from './Kysely.js';
|
|
@@ -34,6 +35,25 @@ export default class PostgresQueryDriver<DreamInstance extends Dream> extends Ky
|
|
|
34
35
|
[key: string]: SchemaBuilderColumnData;
|
|
35
36
|
}>;
|
|
36
37
|
static duplicateDatabase(connectionName: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* @internal
|
|
40
|
+
*
|
|
41
|
+
* Postgres implementation of the row-locking seam: appends
|
|
42
|
+
* `FOR UPDATE OF "<tableAlias>"` to the select, so every row the statement
|
|
43
|
+
* returns is exclusively locked until the enclosing transaction ends.
|
|
44
|
+
*
|
|
45
|
+
* Scoping the lock to the Query's own base table (rather than a bare
|
|
46
|
+
* `FOR UPDATE`) means a query that joins other tables neither locks rows the
|
|
47
|
+
* caller never asked about nor raises Postgres's "FOR UPDATE cannot be
|
|
48
|
+
* applied to the nullable side of an outer join".
|
|
49
|
+
*
|
|
50
|
+
* This is a waiting lock: neither `NOWAIT` nor `SKIP LOCKED` is applied, so a
|
|
51
|
+
* statement that collides with another transaction's lock blocks until that
|
|
52
|
+
* transaction ends. Callers that cannot afford an unbounded wait should set a
|
|
53
|
+
* `lock_timeout` on the connection; see the guarded-destroy notes on
|
|
54
|
+
* `Query#destroy`.
|
|
55
|
+
*/
|
|
56
|
+
applyRowLock(kyselyQuery: SelectQueryBuilder<any, any, any>, tableAlias: string): SelectQueryBuilder<any, any, any>;
|
|
37
57
|
static supportsParallelTestDatabases: boolean;
|
|
38
58
|
/**
|
|
39
59
|
* @internal
|