@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
|
@@ -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
|
}
|
package/dist/esm/src/Dream.js
CHANGED
|
@@ -19,9 +19,9 @@ import destroyDream from './dream/internal/destroyDream.js';
|
|
|
19
19
|
import { destroyOptions, reallyDestroyOptions, undestroyOptions, } from './dream/internal/destroyOptions.js';
|
|
20
20
|
import ensureSTITypeFieldIsSet from './dream/internal/ensureSTITypeFieldIsSet.js';
|
|
21
21
|
import findOrCreateBy from './dream/internal/findOrCreateBy.js';
|
|
22
|
+
import mergeSerializerAssociationEdges from './dream/internal/mergeSerializerAssociationEdges.js';
|
|
22
23
|
import printSerializerHierarchyLevel from './dream/internal/printSerializerHierarchyLevel.js';
|
|
23
24
|
import reload from './dream/internal/reload.js';
|
|
24
|
-
import resolveSerializerAssociationEdges from './dream/internal/resolveSerializerAssociationEdges.js';
|
|
25
25
|
import runValidations from './dream/internal/runValidations.js';
|
|
26
26
|
import saveDream from './dream/internal/saveDream.js';
|
|
27
27
|
import { DEFAULT_BYPASS_ALL_DEFAULT_SCOPES, DEFAULT_DEFAULT_SCOPES_TO_BYPASS, DEFAULT_SKIP_HOOKS, } from './dream/internal/scopeHelpers.js';
|
|
@@ -48,7 +48,7 @@ import areEqual from './helpers/areEqual.js';
|
|
|
48
48
|
import cloneDeepSafe from './helpers/cloneDeepSafe.js';
|
|
49
49
|
import isJsonColumn from './helpers/db/types/isJsonColumn.js';
|
|
50
50
|
import notEqual from './helpers/notEqual.js';
|
|
51
|
-
import {
|
|
51
|
+
import { inferSerializersFromDreamClassOrViewModelClassOrFail } from './serializer/helpers/inferSerializerFromDreamOrViewModel.js';
|
|
52
52
|
export default class Dream {
|
|
53
53
|
DB;
|
|
54
54
|
/**
|
|
@@ -542,44 +542,89 @@ export default class Dream {
|
|
|
542
542
|
}
|
|
543
543
|
static serializationMap(serializerKey) {
|
|
544
544
|
const key = serializerKey || 'default';
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
545
|
+
// Deliberately one serializer: this maps a single serializer's view of the class, and taking
|
|
546
|
+
// the first is only a narrowing when `this` is an STI base (where
|
|
547
|
+
// inferSerializersFromDreamClassOrViewModelClass returns one serializer per child, sorted by
|
|
548
|
+
// sanitizedName). That narrowing is deliberate and differs from `buildSerializerPreloadPaths`
|
|
549
|
+
// and `displaySerialization` below, both of which union every child's serializer because they
|
|
550
|
+
// answer "what does a query load / display for this class", not "what does one serializer
|
|
551
|
+
// render". Nothing in src/ calls
|
|
552
|
+
// this method; it exists as the single-serializer entry point onto recursiveSerializationMap and
|
|
553
|
+
// is exercised by spec/unit/dream/serializationMap.spec.ts, which pins the narrowing on an STI
|
|
554
|
+
// base so that the divergence from displaySerialization is executable rather than only
|
|
555
|
+
// described here. A caller that wants the union for an STI base wants displaySerialization's
|
|
556
|
+
// shape, not this one.
|
|
557
|
+
//
|
|
558
|
+
// Note that the narrowing is to one serializer of *this* class only: every association this
|
|
559
|
+
// serializer renders is still walked with the full set of serializers its target resolves to,
|
|
560
|
+
// so an STI base reached as a target below is unioned.
|
|
561
|
+
const serializer = inferSerializersFromDreamClassOrViewModelClassOrFail(this, key)[0];
|
|
562
|
+
return this.recursiveSerializationMap([serializer]);
|
|
549
563
|
}
|
|
550
564
|
static displaySerialization(serializerKey) {
|
|
551
565
|
const key = serializerKey || 'default';
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
566
|
+
/**
|
|
567
|
+
* When `this` is an STI base, this is one serializer per STI child (deduped), since each child
|
|
568
|
+
* may register a different serializer under `key`. Every one is walked, because this method
|
|
569
|
+
* backs `psy i:serialization`, whose purpose is verifying what gets preloaded — and
|
|
570
|
+
* `preloadFor` unions the preload paths of every child's serializer. Walking only one child's
|
|
571
|
+
* serializer would print a strict subset of what the query actually loads.
|
|
572
|
+
*/
|
|
573
|
+
// ...OrFail, rather than the plain inference, so that an empty serializer set raises
|
|
574
|
+
// NoSerializersResolvedForKey instead of silently printing a class name with nothing under it.
|
|
575
|
+
// That guard is unreachable today and is documented where it lives; it is shared with
|
|
576
|
+
// buildSerializerPreloadPaths so that the display and the preload walk raise the same thing.
|
|
577
|
+
const serializers = inferSerializersFromDreamClassOrViewModelClassOrFail(this, key);
|
|
555
578
|
// eslint-disable-next-line no-console
|
|
556
579
|
console.log(yoctocolors.cyan(this.sanitizedName));
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
|
|
580
|
+
// One line per serializer: for an STI base the displayed tree is the union across its children,
|
|
581
|
+
// so naming a single child's serializer under the base's own name would misattribute the tree.
|
|
582
|
+
serializers.forEach(serializer => {
|
|
583
|
+
// eslint-disable-next-line no-console
|
|
584
|
+
console.log(yoctocolors.gray(serializer.globalName));
|
|
585
|
+
});
|
|
586
|
+
return this.recursiveSerializationMap(serializers, {
|
|
560
587
|
forDisplay: true,
|
|
561
588
|
});
|
|
562
589
|
}
|
|
563
|
-
|
|
590
|
+
/**
|
|
591
|
+
* @internal
|
|
592
|
+
*
|
|
593
|
+
* Maps the association tree reachable from `serializers` — every serializer that can render this
|
|
594
|
+
* class at this point in the walk, which is more than one when this class is an STI base. The
|
|
595
|
+
* serializers are merged into one edge set (see mergeSerializerAssociationEdges) rather than
|
|
596
|
+
* walked one at a time, so an association several of them render is visited once and the nested
|
|
597
|
+
* trees of a shared target merge instead of clobbering each other.
|
|
598
|
+
*/
|
|
599
|
+
static recursiveSerializationMap(serializers, { forDisplay = false, forDisplayDepth = 0, repeatedAssociationTracker = {}, } = {}) {
|
|
564
600
|
const depthTrackerId = this.globalName;
|
|
565
601
|
repeatedAssociationTracker[depthTrackerId] ??= 0;
|
|
566
602
|
if (repeatedAssociationTracker[depthTrackerId] + 1 > RECURSIVE_SERIALIZATION_MAX_REPEATS)
|
|
567
603
|
return {};
|
|
568
604
|
repeatedAssociationTracker[depthTrackerId]++;
|
|
569
|
-
const edges =
|
|
605
|
+
const edges = mergeSerializerAssociationEdges(this, serializers);
|
|
570
606
|
const mappedAssociations = edges.reduce((accumulator, edge) => {
|
|
607
|
+
// An edge with no targets terminates: either a delegatedAttribute (never printed, and
|
|
608
|
+
// carrying `targets: []` by construction) or an association resolving to no serializer. Its
|
|
609
|
+
// nestedSerializerInfo is `{}` because this reduce never runs.
|
|
571
610
|
const innerAssociationSerializerInfo = edge.targets.reduce((innerAccumulator, target) => {
|
|
572
|
-
if (forDisplay
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
611
|
+
if (forDisplay) {
|
|
612
|
+
// One heading per (association, target class), naming every serializer that renders that
|
|
613
|
+
// target underneath it, and then — below all of them — the union of their association
|
|
614
|
+
// trees, which is what `preloadFor` loads through this association.
|
|
615
|
+
//
|
|
616
|
+
// The recursion below is a single merged walk of every serializer of the target, so
|
|
617
|
+
// there is one tree, not one per child. Grouping the serializer names under a single
|
|
618
|
+
// heading is what keeps that honest — repeating the heading per serializer would leave
|
|
619
|
+
// the merged tree indented directly beneath the last one's name and attribute every
|
|
620
|
+
// sibling's associations to it.
|
|
621
|
+
printSerializerHierarchyLevel({ targetSerializers: target.serializers, forDisplayDepth });
|
|
579
622
|
}
|
|
580
623
|
return {
|
|
581
624
|
...innerAccumulator,
|
|
582
|
-
|
|
625
|
+
// recursed once per target class with *all* of that class's serializers, so their nested
|
|
626
|
+
// trees merge rather than the last one winning a shallow spread
|
|
627
|
+
...target.dreamClass['recursiveSerializationMap'](target.serializers.map(({ serializer }) => serializer), {
|
|
583
628
|
forDisplay,
|
|
584
629
|
forDisplayDepth: forDisplayDepth + 1,
|
|
585
630
|
repeatedAssociationTracker,
|
|
@@ -588,7 +633,7 @@ export default class Dream {
|
|
|
588
633
|
}, {});
|
|
589
634
|
accumulator[edge.associationAs] = {
|
|
590
635
|
parentDreamClass: this,
|
|
591
|
-
nestedSerializerInfo:
|
|
636
|
+
nestedSerializerInfo: innerAssociationSerializerInfo,
|
|
592
637
|
};
|
|
593
638
|
return accumulator;
|
|
594
639
|
}, {});
|
|
@@ -1330,6 +1375,9 @@ export default class Dream {
|
|
|
1330
1375
|
* // User{email: 'goodbye@world'}
|
|
1331
1376
|
* ```
|
|
1332
1377
|
*
|
|
1378
|
+
* Records are always visited in ascending primary key order; any `order`
|
|
1379
|
+
* applied to the query is discarded. See {@link Query.findEach} for why.
|
|
1380
|
+
*
|
|
1333
1381
|
* @param cb - The callback to call for each found record
|
|
1334
1382
|
* @param opts - Optional parameters for batch processing
|
|
1335
1383
|
* @param opts.batchSize - the batch size you wish to collect records in. If not provided, it will default to 1000
|
|
@@ -421,6 +421,9 @@ export default class DreamClassTransactionBuilder {
|
|
|
421
421
|
* // User{email: 'goodbye@world'}
|
|
422
422
|
* ```
|
|
423
423
|
*
|
|
424
|
+
* Records are always visited in ascending primary key order; any `order`
|
|
425
|
+
* applied to the query is discarded. See {@link Query.findEach} for why.
|
|
426
|
+
*
|
|
424
427
|
* @param cb - The callback to call for each found record
|
|
425
428
|
* @param options.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000
|
|
426
429
|
* @returns void
|
|
@@ -3,7 +3,9 @@ import DreamApp from '../dream-app/index.js';
|
|
|
3
3
|
import AssociationDeclaredWithoutAssociatedDreamClass from '../errors/associations/AssociationDeclaredWithoutAssociatedDreamClass.js';
|
|
4
4
|
import CannotCallUndestroyOnANonSoftDeleteModel from '../errors/CannotCallUndestroyOnANonSoftDeleteModel.js';
|
|
5
5
|
import CannotPassAdditionalFieldsToPluckEachAfterCallback from '../errors/CannotPassAdditionalFieldsToPluckEachAfterCallback.js';
|
|
6
|
+
import InvalidBatchSize from '../errors/InvalidBatchSize.js';
|
|
6
7
|
import LeftJoinPreloadIncompatibleWithFindEach from '../errors/LeftJoinPreloadIncompatibleWithFindEach.js';
|
|
8
|
+
import RowLockIncompatibleWithDistinct from '../errors/RowLockIncompatibleWithDistinct.js';
|
|
7
9
|
import MissingRequiredCallbackFunctionToPluckEach from '../errors/MissingRequiredCallbackFunctionToPluckEach.js';
|
|
8
10
|
import NoUpdateAllOnJoins from '../errors/NoUpdateAllOnJoins.js';
|
|
9
11
|
import NoUpdateOnAssociationQuery from '../errors/NoUpdateOnAssociationQuery.js';
|
|
@@ -20,8 +22,26 @@ import ops from '../ops/index.js';
|
|
|
20
22
|
import buildSerializerPreloadPaths from './internal/buildSerializerPreloadPaths.js';
|
|
21
23
|
import computedPaginatePage from './internal/computedPaginatePage.js';
|
|
22
24
|
import convertDreamClassAndAssociationNameTupleArrayToPreloadArgs from './internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Cache for serializer preload-path results, keyed on the root class itself and then the serializer
|
|
27
|
+
* key. Keying on the class rather than its global name is what makes the cache self-invalidating:
|
|
28
|
+
* loading a fresh set of model classes into the process yields new class objects, so entries
|
|
29
|
+
* resolved against the previous ones become unreachable and collectable rather than being served
|
|
30
|
+
* under a name that now means something else.
|
|
31
|
+
*/
|
|
32
|
+
let extractedNestedPathsCache = new WeakMap();
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
*
|
|
36
|
+
* Drops every memoized preload path. Only needed when a class object that stays in the process has
|
|
37
|
+
* its `serializers` getter replaced, which the cache cannot detect — specs that install or remove
|
|
38
|
+
* one around an example. Replacing the model classes themselves needs no call, since the cache
|
|
39
|
+
* keys on the class object. See `clearResolvedSerializerAssociationEdgesCache`, which has the same
|
|
40
|
+
* contract and is cleared alongside it.
|
|
41
|
+
*/
|
|
42
|
+
export function clearSerializerPreloadPathsCache() {
|
|
43
|
+
extractedNestedPathsCache = new WeakMap();
|
|
44
|
+
}
|
|
25
45
|
export default class Query {
|
|
26
46
|
/**
|
|
27
47
|
* @internal
|
|
@@ -31,6 +51,10 @@ export default class Query {
|
|
|
31
51
|
*/
|
|
32
52
|
static BATCH_SIZES = {
|
|
33
53
|
FIND_EACH: 1000,
|
|
54
|
+
// deliberately small: every record in a locked destroy batch stays
|
|
55
|
+
// row-locked for the whole batch, so an oversized batch blocks unrelated
|
|
56
|
+
// writers. See Query#destroy's `lock` option.
|
|
57
|
+
LOCKED_DESTROY: 10,
|
|
34
58
|
PLUCK_EACH: 10000,
|
|
35
59
|
PLUCK_EACH_THROUGH: 1000,
|
|
36
60
|
};
|
|
@@ -398,13 +422,30 @@ export default class Query {
|
|
|
398
422
|
* records matching the Query have been fetched.
|
|
399
423
|
*
|
|
400
424
|
* ```ts
|
|
401
|
-
* await User.
|
|
425
|
+
* await User.where({ deletedAt: null }).findEach(user => {
|
|
402
426
|
* DreamApp.log(user)
|
|
403
427
|
* })
|
|
404
428
|
* // User{id: 1}
|
|
405
429
|
* // User{id: 2}
|
|
406
430
|
* ```
|
|
407
431
|
*
|
|
432
|
+
* IMPORTANT: `findEach` always iterates in ascending primary key order.
|
|
433
|
+
* Any `order` applied to the Query is discarded; passing one has no effect
|
|
434
|
+
* on the order records are visited in.
|
|
435
|
+
*
|
|
436
|
+
* This is not a limitation that can be lifted. `findEach` guarantees that
|
|
437
|
+
* every matching record is visited exactly once, and it delivers that by
|
|
438
|
+
* paginating on a keyset: each batch selects the next `batchSize` records
|
|
439
|
+
* with a primary key greater than the last record of the previous batch.
|
|
440
|
+
* That guarantee only holds over a stable, unique key. Ordering by a
|
|
441
|
+
* mutable, non-unique column would mean a record whose sort value changed
|
|
442
|
+
* partway through the iteration could be skipped entirely or visited twice.
|
|
443
|
+
* ({@link Query.cursorPaginate} can honor an arbitrary order precisely
|
|
444
|
+
* because a paginated UI tolerates that anomaly; a full traversal cannot.)
|
|
445
|
+
*
|
|
446
|
+
* If you need records processed in a particular order, load them with a
|
|
447
|
+
* regular ordered query instead of `findEach`.
|
|
448
|
+
*
|
|
408
449
|
* @param cb - The callback to call for each found record
|
|
409
450
|
* @param __namedParameters - Options for batch processing
|
|
410
451
|
* @param __namedParameters.batchSize - The batch size you wish to collect records in. If not provided, it will default to 1000
|
|
@@ -537,17 +578,30 @@ export default class Query {
|
|
|
537
578
|
* @returns A Query with all serialization associations preloaded
|
|
538
579
|
*/
|
|
539
580
|
preloadFor(serializerKey, modifierFn) {
|
|
540
|
-
const cacheKey =
|
|
541
|
-
let
|
|
581
|
+
const cacheKey = serializerKey ?? 'default';
|
|
582
|
+
let pathsBySerializerKey = extractedNestedPathsCache.get(this.dreamClass);
|
|
583
|
+
if (!pathsBySerializerKey) {
|
|
584
|
+
pathsBySerializerKey = new Map();
|
|
585
|
+
extractedNestedPathsCache.set(this.dreamClass, pathsBySerializerKey);
|
|
586
|
+
}
|
|
587
|
+
let preloadArgs = pathsBySerializerKey.get(cacheKey);
|
|
542
588
|
if (!preloadArgs) {
|
|
543
589
|
preloadArgs = buildSerializerPreloadPaths(this.dreamClass, serializerKey);
|
|
544
|
-
|
|
590
|
+
pathsBySerializerKey.set(cacheKey, preloadArgs);
|
|
545
591
|
}
|
|
546
|
-
|
|
592
|
+
// Rather than calling `preload` once per path (which deep clones the accumulating preload tree
|
|
593
|
+
// on every call, making this quadratic in the number of paths), clone once, flesh out every
|
|
594
|
+
// path against the same accumulators, and clone the Query once at the end. Iteration order is
|
|
595
|
+
// preserved, so a later path's `and` still wins over an earlier path's for a shared prefix.
|
|
596
|
+
const preloadStatements = cloneDeepSafe(this.preloadStatements);
|
|
597
|
+
const preloadOnStatements = cloneDeepSafe(this.preloadOnStatements);
|
|
547
598
|
preloadArgs.forEach(dreamClassAndAssociationNameTupleArray => {
|
|
548
|
-
|
|
599
|
+
const args = convertDreamClassAndAssociationNameTupleArrayToPreloadArgs(dreamClassAndAssociationNameTupleArray, modifierFn);
|
|
600
|
+
// `modifierFn` returning 'omit' prunes the path from the omitted association down, which can
|
|
601
|
+
// leave nothing to preload; `fleshOutJoinStatements` treats an empty arg list as a no-op.
|
|
602
|
+
this.fleshOutJoinStatements([], preloadStatements, preloadOnStatements, null, args);
|
|
549
603
|
});
|
|
550
|
-
return
|
|
604
|
+
return this.clone({ preloadStatements, preloadOnStatements });
|
|
551
605
|
}
|
|
552
606
|
/**
|
|
553
607
|
* Returns a new Query instance, with the provided
|
|
@@ -1741,21 +1795,109 @@ export default class Query {
|
|
|
1741
1795
|
* // 12
|
|
1742
1796
|
* ```
|
|
1743
1797
|
*
|
|
1798
|
+
* ## Guarded (compare-and-set) destroy
|
|
1799
|
+
*
|
|
1800
|
+
* By default, `destroy` reads the matching records and then destroys each of
|
|
1801
|
+
* them, so a concurrent writer can change a record in between — the record is
|
|
1802
|
+
* destroyed even though it no longer matches the Query that selected it.
|
|
1803
|
+
* Passing `lock: true` closes that window, making `destroy` a compare-and-set
|
|
1804
|
+
* the way `update(..., { skipHooks: true })` already is:
|
|
1805
|
+
*
|
|
1806
|
+
* ```ts
|
|
1807
|
+
* const destroyed = await Order.where({ status: 'pending' }).destroy({ lock: true })
|
|
1808
|
+
* // 0 — someone else moved every one of them out of 'pending' first
|
|
1809
|
+
* ```
|
|
1810
|
+
*
|
|
1811
|
+
* With `lock: true`, records are processed in batches, and each batch runs in
|
|
1812
|
+
* its own transaction which first re-selects that batch's records with an
|
|
1813
|
+
* exclusive row lock and then destroys them before committing. Because the
|
|
1814
|
+
* database re-checks the Query's conditions after acquiring each row lock,
|
|
1815
|
+
* a record a concurrent transaction has already moved out of the Query drops
|
|
1816
|
+
* out of the locked read and is never destroyed. The returned count is the
|
|
1817
|
+
* number of records actually claimed, so a caller can detect that it lost a
|
|
1818
|
+
* race by comparing the count against what it expected.
|
|
1819
|
+
*
|
|
1820
|
+
* Things to know before relying on it:
|
|
1821
|
+
*
|
|
1822
|
+
* - **The guarantee is per batch, not set-wide.** A run spanning more than one
|
|
1823
|
+
* batch can win a race in batch 1 and lose one in batch 2; nothing holds the
|
|
1824
|
+
* whole matched set still for the duration of the run. Sharing one
|
|
1825
|
+
* transaction across every batch — by threading a transaction onto the Query
|
|
1826
|
+
* itself, `Order.query().txn(txn).where({ ... }).destroy({ lock: true })` —
|
|
1827
|
+
* buys two things: the destroys become all-or-nothing (a rollback undoes the
|
|
1828
|
+
* whole run), and the row locks taken by every batch are held until you
|
|
1829
|
+
* commit. It does **not** extend the compare-and-set to the part of the set
|
|
1830
|
+
* the run has not reached yet: under READ COMMITTED each statement takes a
|
|
1831
|
+
* fresh snapshot, so a record moved out of the Query between batch 1 and
|
|
1832
|
+
* batch 2 still drops out of batch 2's locked read. A genuinely set-wide
|
|
1833
|
+
* guarantee needs REPEATABLE READ / SERIALIZABLE, or a single locking
|
|
1834
|
+
* statement over the whole set.
|
|
1835
|
+
* - **Only a transaction carried by the Query is shared.** Merely wrapping the
|
|
1836
|
+
* call in `Model.transaction(...)` does not thread that transaction onto the
|
|
1837
|
+
* Query — `Dream.transaction` establishes no ambient transaction, it only
|
|
1838
|
+
* hands a `txn` to your callback — so each batch would open its own
|
|
1839
|
+
* transaction on a different pooled connection. Worse, if the surrounding
|
|
1840
|
+
* transaction has already written to any of the matched rows, each batch's
|
|
1841
|
+
* locked read blocks on locks the surrounding transaction holds while that
|
|
1842
|
+
* transaction awaits the destroy: a cycle across two connections, which
|
|
1843
|
+
* Postgres's deadlock detector cannot see, so it hangs rather than aborting.
|
|
1844
|
+
* Always pass the transaction with `.txn(txn)`.
|
|
1845
|
+
* - **Behavior above READ COMMITTED differs.** The drop-out-of-the-result
|
|
1846
|
+
* behavior described above is READ COMMITTED (Postgres's default). Under
|
|
1847
|
+
* REPEATABLE READ or SERIALIZABLE, the same race raises a serialization
|
|
1848
|
+
* failure instead of silently returning a lower count. Both are safe; only
|
|
1849
|
+
* one of them is quiet.
|
|
1850
|
+
* - **`batchSize` defaults to 10 here**, not `findEach`'s 1000, and the
|
|
1851
|
+
* default is deliberately small. Every row in a batch stays locked for the
|
|
1852
|
+
* whole batch, including the time spent running each record's hooks and
|
|
1853
|
+
* `dependent: 'destroy'` cascade, so an oversized batch blocks unrelated
|
|
1854
|
+
* writers touching those rows and surfaces as timeouts somewhere else in the
|
|
1855
|
+
* application, which is very hard to trace back to this destroy. Too small a
|
|
1856
|
+
* batch, by contrast, only slows down this call, which you can see and fix.
|
|
1857
|
+
* Lower it further for models with deep `dependent: 'destroy'` cascades or
|
|
1858
|
+
* expensive destroy hooks.
|
|
1859
|
+
* - **A batch waits, without bound, for whoever holds the row.** The locked
|
|
1860
|
+
* read is a plain `FOR UPDATE`: if another transaction already holds a lock
|
|
1861
|
+
* on one of the batch's rows, this call blocks until that transaction ends,
|
|
1862
|
+
* holding its own transaction and its pooled connection open the whole
|
|
1863
|
+
* time. Dream sets no `lock_timeout` or `statement_timeout`, so a
|
|
1864
|
+
* long-running writer on the other side stalls the run indefinitely and, at
|
|
1865
|
+
* scale, ties up connections. If that is a risk in your application, set a
|
|
1866
|
+
* `lock_timeout` on the connection so a stuck batch fails instead of
|
|
1867
|
+
* hanging.
|
|
1868
|
+
* - **If you are destroying a large set and do not need compare-and-set, do
|
|
1869
|
+
* not pass `lock`.** Plain `destroy()` (hooks and cascade, no locking) or
|
|
1870
|
+
* {@link Query.delete | delete} (a single statement, no hooks or cascade)
|
|
1871
|
+
* are the right tools for bulk removal.
|
|
1872
|
+
*
|
|
1744
1873
|
* @param __namedParameters - Options for destroying the instance
|
|
1745
1874
|
* @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
|
|
1746
1875
|
* @param __namedParameters.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
1876
|
+
* @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
|
|
1877
|
+
* @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
|
|
1747
1878
|
* @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
|
|
1748
1879
|
* @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
|
|
1749
1880
|
* @returns The number of records that were removed
|
|
1881
|
+
* @throws InvalidBatchSize if `batchSize` is not a positive integer
|
|
1750
1882
|
*/
|
|
1751
|
-
async destroy({ skipHooks, cascade, } = {}) {
|
|
1883
|
+
async destroy({ skipHooks, cascade, lock, batchSize, } = {}) {
|
|
1752
1884
|
let counter = 0;
|
|
1885
|
+
// resolve the default before validating: `??` would let an explicit 0
|
|
1886
|
+
// through, and `limit(0)` means "no limit", which would turn a batched
|
|
1887
|
+
// destroy into a single unbounded pass (and, with `lock`, a lock over the
|
|
1888
|
+
// entire matched set)
|
|
1889
|
+
const resolvedBatchSize = batchSize ?? (lock ? Query.BATCH_SIZES.LOCKED_DESTROY : Query.BATCH_SIZES.FIND_EACH);
|
|
1890
|
+
if (!Number.isInteger(resolvedBatchSize) || resolvedBatchSize < 1)
|
|
1891
|
+
throw new InvalidBatchSize(batchSize);
|
|
1753
1892
|
const options = {
|
|
1754
1893
|
bypassAllDefaultScopes: this.bypassAllDefaultScopes,
|
|
1755
1894
|
defaultScopesToBypass: this.defaultScopesToBypass,
|
|
1756
1895
|
skipHooks,
|
|
1757
1896
|
cascade,
|
|
1758
1897
|
};
|
|
1898
|
+
if (lock) {
|
|
1899
|
+
return await this.lockedDestroy(options, resolvedBatchSize);
|
|
1900
|
+
}
|
|
1759
1901
|
await this.findEach(async (result) => {
|
|
1760
1902
|
const subquery = this.dreamTransaction
|
|
1761
1903
|
? result.txn(this.dreamTransaction)
|
|
@@ -1767,9 +1909,94 @@ export default class Query {
|
|
|
1767
1909
|
await subquery.destroy(options);
|
|
1768
1910
|
}
|
|
1769
1911
|
counter++;
|
|
1912
|
+
}, {
|
|
1913
|
+
batchSize: resolvedBatchSize,
|
|
1770
1914
|
});
|
|
1771
1915
|
return counter;
|
|
1772
1916
|
}
|
|
1917
|
+
/**
|
|
1918
|
+
* @internal
|
|
1919
|
+
*
|
|
1920
|
+
* The compare-and-set implementation behind `destroy({ lock: true })`.
|
|
1921
|
+
*
|
|
1922
|
+
* Each batch is handled in two steps inside one transaction:
|
|
1923
|
+
*
|
|
1924
|
+
* 1. an unlocked pluck of the next `batchSize` primary keys, in ascending
|
|
1925
|
+
* primary key order, which establishes the batch's window (its keyset
|
|
1926
|
+
* cursor) and tells us how many records were *attempted*. Only the keys are
|
|
1927
|
+
* read: hydrating and preloading these records would duplicate the work
|
|
1928
|
+
* step 2 does over the same rows; then
|
|
1929
|
+
* 2. a locked re-read restricted to exactly those primary keys, which
|
|
1930
|
+
* re-applies every condition on the Query while acquiring an exclusive row
|
|
1931
|
+
* lock on each row it returns. Records a concurrent transaction has moved
|
|
1932
|
+
* out of the Query since step 1 do not come back, and are the records this
|
|
1933
|
+
* run lost the race for.
|
|
1934
|
+
*
|
|
1935
|
+
* The count comes from step 2's result set, which is why it reports records
|
|
1936
|
+
* actually claimed rather than records attempted. (One thing it does not
|
|
1937
|
+
* account for, because it never has: a `beforeDestroy` hook that sets
|
|
1938
|
+
* `_preventDeletion` leaves the record in place but still counts.)
|
|
1939
|
+
*
|
|
1940
|
+
* The two-step shape is what keeps iteration correct. If the locked read both
|
|
1941
|
+
* claimed rows and advanced the cursor, a batch whose records all lost the
|
|
1942
|
+
* race would come back empty and end the run early, silently skipping every
|
|
1943
|
+
* record after it. Advancing the cursor from step 1 instead means losing a
|
|
1944
|
+
* race skips only the records that were lost.
|
|
1945
|
+
*
|
|
1946
|
+
* When the Query already carries a transaction, that transaction is used for
|
|
1947
|
+
* every batch rather than opening one per batch, so the caller's transaction
|
|
1948
|
+
* boundary wins and the locks are held until the caller commits.
|
|
1949
|
+
*/
|
|
1950
|
+
async lockedDestroy(options, batchSize) {
|
|
1951
|
+
if (this.joinLoadActivated)
|
|
1952
|
+
throw new LeftJoinPreloadIncompatibleWithFindEach();
|
|
1953
|
+
// the driver seam refuses this combination too, but catching it here means
|
|
1954
|
+
// failing before any transaction is opened or any row is touched
|
|
1955
|
+
if (this.distinctColumn)
|
|
1956
|
+
throw new RowLockIncompatibleWithDistinct();
|
|
1957
|
+
const primaryKey = this.dreamInstance['_primaryKey'];
|
|
1958
|
+
const orderedQuery = this.order(null)
|
|
1959
|
+
.order(this.namespacedPrimaryKey)
|
|
1960
|
+
.limit(batchSize);
|
|
1961
|
+
let counter = 0;
|
|
1962
|
+
// the cursor is compared against a sentinel rather than tested for
|
|
1963
|
+
// truthiness, since a primary key of 0 is a legitimate cursor value that
|
|
1964
|
+
// would otherwise reset the window to the start of the set
|
|
1965
|
+
let lastId = undefined;
|
|
1966
|
+
let attemptedCount = 0;
|
|
1967
|
+
do {
|
|
1968
|
+
const batchQuery = lastId === undefined
|
|
1969
|
+
? orderedQuery
|
|
1970
|
+
: orderedQuery.where({ [primaryKey]: ops.greaterThan(lastId) });
|
|
1971
|
+
const destroyBatch = async (txn) => {
|
|
1972
|
+
const candidateIds = await batchQuery.txn(txn).pluck(this.namespacedPrimaryKey);
|
|
1973
|
+
if (!candidateIds.length)
|
|
1974
|
+
return { attempted: 0, claimed: 0, lastId };
|
|
1975
|
+
const claimed = await this.dbDriverInstance(batchQuery.txn(txn).where({ [primaryKey]: candidateIds })).takeAll({ lock: true });
|
|
1976
|
+
for (const record of claimed) {
|
|
1977
|
+
const inTransaction = record.txn(txn);
|
|
1978
|
+
if (this.shouldReallyDestroy) {
|
|
1979
|
+
await inTransaction.reallyDestroy(options);
|
|
1980
|
+
}
|
|
1981
|
+
else {
|
|
1982
|
+
await inTransaction.destroy(options);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
return {
|
|
1986
|
+
attempted: candidateIds.length,
|
|
1987
|
+
claimed: claimed.length,
|
|
1988
|
+
lastId: candidateIds.at(-1),
|
|
1989
|
+
};
|
|
1990
|
+
};
|
|
1991
|
+
const results = this.dreamTransaction
|
|
1992
|
+
? await destroyBatch(this.dreamTransaction)
|
|
1993
|
+
: await this.dreamClass.transaction(async (txn) => await destroyBatch(txn));
|
|
1994
|
+
counter += results.claimed;
|
|
1995
|
+
attemptedCount = results.attempted;
|
|
1996
|
+
lastId = results.lastId;
|
|
1997
|
+
} while (attemptedCount > 0 && attemptedCount === batchSize);
|
|
1998
|
+
return counter;
|
|
1999
|
+
}
|
|
1773
2000
|
/**
|
|
1774
2001
|
* Destroys all records matching the Query,
|
|
1775
2002
|
* ignoring the SoftDelete decorator and
|
|
@@ -1791,12 +2018,15 @@ export default class Query {
|
|
|
1791
2018
|
* @param __namedParameters - Options for destroying the instance
|
|
1792
2019
|
* @param __namedParameters.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
|
|
1793
2020
|
* @param __namedParameters.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
2021
|
+
* @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
|
|
2022
|
+
* @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
|
|
1794
2023
|
* @param __namedParameters.bypassAllDefaultScopes - If true, bypasses all default scopes when cascade destroying. Defaults to false
|
|
1795
2024
|
* @param __namedParameters.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
|
|
1796
2025
|
* @returns The number of records that were removed
|
|
2026
|
+
* @throws InvalidBatchSize if `batchSize` is not a positive integer
|
|
1797
2027
|
*/
|
|
1798
|
-
async reallyDestroy({ skipHooks, cascade, } = {}) {
|
|
1799
|
-
return await this.clone({ shouldReallyDestroy: true }).destroy({ skipHooks, cascade });
|
|
2028
|
+
async reallyDestroy({ skipHooks, cascade, lock, batchSize, } = {}) {
|
|
2029
|
+
return await this.clone({ shouldReallyDestroy: true }).destroy({ skipHooks, cascade, lock, batchSize });
|
|
1800
2030
|
}
|
|
1801
2031
|
/**
|
|
1802
2032
|
* Undestroys a SoftDelete model, unsetting
|