@rvoh/dream 2.5.7 → 2.6.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 +37 -66
- package/dist/cjs/src/decorators/class/STI.js +2 -2
- package/dist/cjs/src/dream/LeftJoinLoadBuilder.js +16 -10
- package/dist/cjs/src/dream/LoadBuilder.js +70 -9
- package/dist/cjs/src/dream/Query.js +5 -6
- package/dist/cjs/src/dream/constants.js +2 -0
- package/dist/cjs/src/dream/internal/buildAssociationPaths.js +33 -0
- package/dist/cjs/src/dream/internal/buildDependentDestroyPreloadPaths.js +43 -0
- package/dist/cjs/src/dream/internal/buildSerializerPreloadPaths.js +38 -0
- package/dist/cjs/src/dream/internal/destroyAssociatedRecords.js +14 -6
- package/dist/cjs/src/dream/internal/destroyDream.js +7 -1
- package/dist/cjs/src/dream/internal/loadDependentDestroyTree.js +35 -0
- package/dist/cjs/src/dream/internal/resolveSerializerAssociationEdges.js +53 -0
- package/dist/cjs/src/serializer/SerializerRenderer.js +1 -1
- package/dist/cjs/src/serializer/builders/DreamSerializerBuilder.js +114 -52
- package/dist/cjs/src/serializer/builders/ObjectSerializerBuilder.js +111 -41
- package/dist/cjs/src/types/recursiveSerialization.js +1 -0
- package/dist/esm/src/Dream.js +37 -66
- package/dist/esm/src/decorators/class/STI.js +2 -2
- package/dist/esm/src/dream/LeftJoinLoadBuilder.js +16 -10
- package/dist/esm/src/dream/LoadBuilder.js +70 -9
- package/dist/esm/src/dream/Query.js +5 -6
- package/dist/esm/src/dream/constants.js +2 -0
- package/dist/esm/src/dream/internal/buildAssociationPaths.js +33 -0
- package/dist/esm/src/dream/internal/buildDependentDestroyPreloadPaths.js +43 -0
- package/dist/esm/src/dream/internal/buildSerializerPreloadPaths.js +38 -0
- package/dist/esm/src/dream/internal/destroyAssociatedRecords.js +14 -6
- package/dist/esm/src/dream/internal/destroyDream.js +7 -1
- package/dist/esm/src/dream/internal/loadDependentDestroyTree.js +35 -0
- package/dist/esm/src/dream/internal/resolveSerializerAssociationEdges.js +53 -0
- package/dist/esm/src/serializer/SerializerRenderer.js +1 -1
- package/dist/esm/src/serializer/builders/DreamSerializerBuilder.js +114 -52
- package/dist/esm/src/serializer/builders/ObjectSerializerBuilder.js +111 -41
- package/dist/esm/src/types/recursiveSerialization.js +1 -0
- package/dist/types/src/Dream.d.ts +25 -10
- package/dist/types/src/decorators/Decorators.d.ts +8 -0
- package/dist/types/src/decorators/class/STI.d.ts +1 -3
- package/dist/types/src/decorators/field/sortable/Sortable.d.ts +9 -0
- package/dist/types/src/decorators/field/validation/Validates.d.ts +4 -0
- package/dist/types/src/decorators/static-method/Scope.d.ts +4 -0
- package/dist/types/src/dream/LeftJoinLoadBuilder.d.ts +16 -10
- package/dist/types/src/dream/LoadBuilder.d.ts +63 -10
- package/dist/types/src/dream/Query.d.ts +1 -1
- package/dist/types/src/dream/constants.d.ts +2 -0
- package/dist/types/src/dream/internal/buildAssociationPaths.d.ts +12 -0
- package/dist/types/src/dream/internal/buildDependentDestroyPreloadPaths.d.ts +17 -0
- package/dist/types/src/dream/internal/buildSerializerPreloadPaths.d.ts +3 -0
- package/dist/types/src/dream/internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.d.ts +1 -1
- package/dist/types/src/dream/internal/destroyAssociatedRecords.d.ts +5 -1
- package/dist/types/src/dream/internal/loadDependentDestroyTree.d.ts +17 -0
- package/dist/types/src/dream/internal/resolveSerializerAssociationEdges.d.ts +13 -0
- package/dist/types/src/serializer/builders/DreamSerializerBuilder.d.ts +164 -63
- package/dist/types/src/serializer/builders/ObjectSerializerBuilder.d.ts +126 -43
- package/dist/types/src/types/associations/belongsTo.d.ts +34 -0
- package/dist/types/src/types/associations/belongsTo.ts +41 -0
- package/dist/types/src/types/associations/hasMany.d.ts +18 -0
- package/dist/types/src/types/associations/hasMany.ts +18 -0
- package/dist/types/src/types/associations/shared.d.ts +71 -0
- package/dist/types/src/types/associations/shared.ts +74 -0
- package/dist/types/src/types/dream.d.ts +16 -0
- package/dist/types/src/types/dream.ts +18 -0
- package/dist/types/src/types/lifecycle.d.ts +18 -0
- package/dist/types/src/types/lifecycle.ts +18 -0
- package/dist/types/src/types/query.d.ts +3 -0
- package/dist/types/src/types/query.ts +3 -0
- package/dist/types/src/types/recursiveSerialization.d.ts +8 -0
- package/dist/types/src/types/recursiveSerialization.ts +10 -0
- package/dist/types/src/types/serializer.d.ts +8 -1
- package/dist/types/src/types/serializer.ts +8 -1
- package/docs/classes/db.DreamMigrationHelpers.html +9 -9
- package/docs/classes/db.KyselyQueryDriver.html +32 -32
- package/docs/classes/db.PostgresQueryDriver.html +33 -33
- package/docs/classes/db.QueryDriverBase.html +31 -31
- 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.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 +21 -20
- package/docs/classes/index.Dream.html +133 -126
- package/docs/classes/index.DreamApp.html +5 -5
- package/docs/classes/index.DreamTransaction.html +2 -2
- package/docs/classes/index.Env.html +2 -2
- package/docs/classes/index.Query.html +57 -57
- package/docs/classes/system.CliFileWriter.html +4 -4
- package/docs/classes/system.DreamBin.html +2 -2
- package/docs/classes/system.DreamCLI.html +6 -6
- package/docs/classes/system.DreamImporter.html +2 -2
- package/docs/classes/system.DreamLogos.html +2 -2
- package/docs/classes/system.DreamSerializerBuilder.html +177 -59
- package/docs/classes/system.ObjectSerializerBuilder.html +110 -34
- package/docs/classes/system.PathHelpers.html +3 -3
- package/docs/classes/utils.Encrypt.html +2 -2
- 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/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.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 +1 -1
- package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
- package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
- package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
- package/docs/variables/system.primaryKeyTypes.html +1 -1
- package/package.json +1 -1
- package/dist/cjs/src/dream/internal/extractNestedPaths.js +0 -34
- package/dist/esm/src/dream/internal/extractNestedPaths.js +0 -34
- package/dist/types/src/dream/internal/extractNestedPaths.d.ts +0 -21
package/dist/esm/src/Dream.js
CHANGED
|
@@ -5,6 +5,7 @@ import { blankAssociationsFactory } from './decorators/field/association/shared.
|
|
|
5
5
|
import { blankHooksFactory } from './decorators/field/lifecycle/shared.js';
|
|
6
6
|
import resortAllRecords from './decorators/field/sortable/helpers/resortAllRecords.js';
|
|
7
7
|
import DreamApp from './dream-app/index.js';
|
|
8
|
+
import { RECURSIVE_SERIALIZATION_MAX_REPEATS } from './dream/constants.js';
|
|
8
9
|
import DreamClassTransactionBuilder from './dream/DreamClassTransactionBuilder.js';
|
|
9
10
|
import DreamInstanceTransactionBuilder from './dream/DreamInstanceTransactionBuilder.js';
|
|
10
11
|
import associationQuery from './dream/internal/associations/associationQuery.js';
|
|
@@ -20,6 +21,7 @@ import ensureSTITypeFieldIsSet from './dream/internal/ensureSTITypeFieldIsSet.js
|
|
|
20
21
|
import findOrCreateBy from './dream/internal/findOrCreateBy.js';
|
|
21
22
|
import printSerializerHierarchyLevel from './dream/internal/printSerializerHierarchyLevel.js';
|
|
22
23
|
import reload from './dream/internal/reload.js';
|
|
24
|
+
import resolveSerializerAssociationEdges from './dream/internal/resolveSerializerAssociationEdges.js';
|
|
23
25
|
import runValidations from './dream/internal/runValidations.js';
|
|
24
26
|
import saveDream from './dream/internal/saveDream.js';
|
|
25
27
|
import { DEFAULT_BYPASS_ALL_DEFAULT_SCOPES, DEFAULT_DEFAULT_SCOPES_TO_BYPASS, DEFAULT_SKIP_HOOKS, } from './dream/internal/scopeHelpers.js';
|
|
@@ -43,14 +45,11 @@ import GlobalNameNotSet from './errors/dream-app/GlobalNameNotSet.js';
|
|
|
43
45
|
import DreamMissingRequiredOverride from './errors/DreamMissingRequiredOverride.js';
|
|
44
46
|
import NonExistentScopeProvidedToResort from './errors/NonExistentScopeProvidedToResort.js';
|
|
45
47
|
import RecordNotFound from './errors/RecordNotFound.js';
|
|
46
|
-
import MissingSerializersDefinition from './errors/serializers/MissingSerializersDefinition.js';
|
|
47
48
|
import areEqual from './helpers/areEqual.js';
|
|
48
49
|
import cloneDeepSafe from './helpers/cloneDeepSafe.js';
|
|
49
|
-
import compact from './helpers/compact.js';
|
|
50
50
|
import isJsonColumn from './helpers/db/types/isJsonColumn.js';
|
|
51
51
|
import notEqual from './helpers/notEqual.js';
|
|
52
52
|
import { inferSerializersFromDreamClassOrViewModelClass } from './serializer/helpers/inferSerializerFromDreamOrViewModel.js';
|
|
53
|
-
const RECURSIVE_SERIALIZATION_MAX_REPEATS = 4;
|
|
54
53
|
export default class Dream {
|
|
55
54
|
DB;
|
|
56
55
|
/**
|
|
@@ -568,72 +567,29 @@ export default class Dream {
|
|
|
568
567
|
if (repeatedAssociationTracker[depthTrackerId] + 1 > RECURSIVE_SERIALIZATION_MAX_REPEATS)
|
|
569
568
|
return {};
|
|
570
569
|
repeatedAssociationTracker[depthTrackerId]++;
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
serializerAssociation.name;
|
|
576
|
-
const serializerAssociationType = serializerAssociation.type;
|
|
577
|
-
const association = this['getAssociationMetadata'](serializerAssociationName);
|
|
578
|
-
if (!association)
|
|
579
|
-
return accumulator;
|
|
580
|
-
const maybeAssociatedClasses = association.modelCB();
|
|
581
|
-
if (!maybeAssociatedClasses)
|
|
582
|
-
throw new Error(`No class defined on ${serializerAssociationName} association on ${this.sanitizedName}`);
|
|
583
|
-
const associatedClasses = Array.isArray(maybeAssociatedClasses)
|
|
584
|
-
? maybeAssociatedClasses
|
|
585
|
-
: [maybeAssociatedClasses];
|
|
586
|
-
/////////////////////////////////////////////////
|
|
587
|
-
// map associated classes to their serializers //
|
|
588
|
-
/////////////////////////////////////////////////
|
|
589
|
-
const associatedClassSerializerTuples = associatedClasses.flatMap(associatedClass => {
|
|
590
|
-
/**
|
|
591
|
-
* `serializers` is an array because `associatedClass` may be an STI
|
|
592
|
-
* base, with each of its STI children having its own serializer
|
|
593
|
-
*/
|
|
594
|
-
let serializers = [];
|
|
595
|
-
try {
|
|
596
|
-
serializers = serializerAssociation.options.serializer
|
|
597
|
-
? compact([serializerAssociation.options.serializer])
|
|
598
|
-
: compact(inferSerializersFromDreamClassOrViewModelClass(associatedClass, serializerAssociation.options.serializerKey));
|
|
599
|
-
}
|
|
600
|
-
catch (error) {
|
|
601
|
-
if (!(error instanceof MissingSerializersDefinition))
|
|
602
|
-
throw error;
|
|
603
|
-
serializers = [];
|
|
604
|
-
}
|
|
605
|
-
return serializers.map(serializer => [associatedClass, serializer]);
|
|
606
|
-
});
|
|
607
|
-
/////////////////////////////////////////////////////
|
|
608
|
-
// end:map associated classes to their serializers //
|
|
609
|
-
/////////////////////////////////////////////////////
|
|
610
|
-
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
611
|
-
// reduce over all associated serializers, recursively building out their associated serializers //
|
|
612
|
-
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
613
|
-
const innerAssociationSerializerInfo = associatedClassSerializerTuples.reduce((innerAccumulator, [associatedClass, associatedSerializer]) => {
|
|
614
|
-
if (forDisplay && serializerAssociationType !== 'delegatedAttribute') {
|
|
570
|
+
const edges = resolveSerializerAssociationEdges(this, serializer);
|
|
571
|
+
const mappedAssociations = edges.reduce((accumulator, edge) => {
|
|
572
|
+
const innerAssociationSerializerInfo = edge.targets.reduce((innerAccumulator, target) => {
|
|
573
|
+
if (forDisplay && edge.type !== 'delegatedAttribute') {
|
|
615
574
|
printSerializerHierarchyLevel({
|
|
616
|
-
serializerAssociationType,
|
|
617
|
-
serializerAssociationName,
|
|
618
|
-
associationSerializer:
|
|
575
|
+
serializerAssociationType: edge.type,
|
|
576
|
+
serializerAssociationName: edge.serializerAssociationName,
|
|
577
|
+
associationSerializer: target.serializer,
|
|
619
578
|
forDisplayDepth,
|
|
620
579
|
});
|
|
621
580
|
}
|
|
622
581
|
return {
|
|
623
582
|
...innerAccumulator,
|
|
624
|
-
...
|
|
583
|
+
...target.dreamClass['recursiveSerializationMap'](target.serializer, {
|
|
625
584
|
forDisplay,
|
|
626
585
|
forDisplayDepth: forDisplayDepth + 1,
|
|
627
586
|
repeatedAssociationTracker,
|
|
628
587
|
}),
|
|
629
588
|
};
|
|
630
589
|
}, {});
|
|
631
|
-
|
|
632
|
-
// end:reduce over all associated serializers, recursively building out their associated serializers //
|
|
633
|
-
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
634
|
-
accumulator[association.as] = {
|
|
590
|
+
accumulator[edge.associationAs] = {
|
|
635
591
|
parentDreamClass: this,
|
|
636
|
-
nestedSerializerInfo:
|
|
592
|
+
nestedSerializerInfo: edge.type === 'delegatedAttribute' ? {} : innerAssociationSerializerInfo,
|
|
637
593
|
};
|
|
638
594
|
return accumulator;
|
|
639
595
|
}, {});
|
|
@@ -3238,13 +3194,28 @@ export default class Dream {
|
|
|
3238
3194
|
return new LoadBuilder(this).passthrough(passthroughWhereStatement);
|
|
3239
3195
|
}
|
|
3240
3196
|
/**
|
|
3241
|
-
* Loads the requested associations upon execution
|
|
3197
|
+
* Loads the requested associations upon execution.
|
|
3198
|
+
*
|
|
3199
|
+
* **IMPORTANT:** `load().execute()` returns a **new clone** of the model with the
|
|
3200
|
+
* associations loaded. It does NOT modify the original instance. You must use the
|
|
3201
|
+
* returned value:
|
|
3202
|
+
*
|
|
3203
|
+
* ```ts
|
|
3204
|
+
* // CORRECT — use the returned clone:
|
|
3205
|
+
* const loaded = await user.load('posts').execute()
|
|
3206
|
+
* loaded.posts // works
|
|
3207
|
+
*
|
|
3208
|
+
* // WRONG — original is not modified:
|
|
3209
|
+
* await user.load('posts').execute()
|
|
3210
|
+
* user.posts // association not loaded!
|
|
3211
|
+
* ```
|
|
3242
3212
|
*
|
|
3243
3213
|
* NOTE: {@link Dream.preload} is often a preferrable way of achieving the
|
|
3244
|
-
* same goal.
|
|
3214
|
+
* same goal. Alternatively, `await model.association('posts')` loads only
|
|
3215
|
+
* if the association is not already loaded.
|
|
3245
3216
|
*
|
|
3246
3217
|
* ```ts
|
|
3247
|
-
* await user
|
|
3218
|
+
* const user = await user
|
|
3248
3219
|
* .load('posts', { body: ops.ilike('%hello world%') }, 'comments', 'replies')
|
|
3249
3220
|
* .load('images')
|
|
3250
3221
|
* .execute()
|
|
@@ -3257,7 +3228,7 @@ export default class Dream {
|
|
|
3257
3228
|
* ```
|
|
3258
3229
|
*
|
|
3259
3230
|
* @param args - A list of associations (and optional where clauses) to load
|
|
3260
|
-
* @returns A chainable LoadBuilder instance
|
|
3231
|
+
* @returns A chainable LoadBuilder instance. Call `.execute()` to get the cloned model with associations loaded.
|
|
3261
3232
|
*/
|
|
3262
3233
|
load(...args) {
|
|
3263
3234
|
return new LoadBuilder(this).load(...args);
|
|
@@ -3326,19 +3297,19 @@ export default class Dream {
|
|
|
3326
3297
|
* Load each specified association using a single SQL query.
|
|
3327
3298
|
* See {@link Dream.load} for loading in separate queries.
|
|
3328
3299
|
*
|
|
3300
|
+
* **IMPORTANT:** Like `load()`, `leftJoinLoad().execute()` returns a **new clone** of the
|
|
3301
|
+
* model with associations loaded. It does NOT modify the original instance.
|
|
3302
|
+
*
|
|
3329
3303
|
* Note: since leftJoinLoad loads via single query, it has
|
|
3330
|
-
* some downsides
|
|
3304
|
+
* some downsides that may be avoided using {@link Dream.load}:
|
|
3331
3305
|
* 1. `limit` and `offset` will be automatically removed
|
|
3332
3306
|
* 2. `through` associations will bring additional namespaces into the query that can conflict with through associations from other associations, creating an invalid query
|
|
3333
3307
|
* 3. each nested association will result in an additional record which duplicates data from the outer record. E.g., given `.leftJoinLoad('a', 'b', 'c')`, if each `a` has 10 `b` and each `b` has 10 `c`, then for one `a`, 100 records will be returned, each of which has all of the columns of `a`. `.load('a', 'b', 'c')` would perform three separate SQL queries, but the data for a single `a` would only be returned once.
|
|
3334
3308
|
* 4. the individual query becomes more complex the more associations are included
|
|
3335
3309
|
* 5. associations loading associations loading associations could result in exponential amounts of data; in those cases, `.load(...).findEach(...)` avoids instantiating massive amounts of data at once
|
|
3336
3310
|
*
|
|
3337
|
-
* Note: Left join loading loads all data in a single SQL query but has trade-offs compared
|
|
3338
|
-
* to regular preloading. See {@link Dream.leftJoinPreload} for details about limitations.
|
|
3339
|
-
*
|
|
3340
3311
|
* ```ts
|
|
3341
|
-
* await user
|
|
3312
|
+
* const user = await user
|
|
3342
3313
|
* .leftJoinLoad('posts', { body: ops.ilike('%hello world%') }, 'comments', 'replies')
|
|
3343
3314
|
* .leftJoinLoad('images')
|
|
3344
3315
|
* .execute()
|
|
@@ -3351,7 +3322,7 @@ export default class Dream {
|
|
|
3351
3322
|
* ```
|
|
3352
3323
|
*
|
|
3353
3324
|
* @param args - A list of associations (and optional where clauses) to load
|
|
3354
|
-
* @returns A chainable LeftJoinLoadBuilder instance
|
|
3325
|
+
* @returns A chainable LeftJoinLoadBuilder instance. Call `.execute()` to get the cloned model with associations loaded.
|
|
3355
3326
|
*/
|
|
3356
3327
|
leftJoinLoad(...args) {
|
|
3357
3328
|
return new LeftJoinLoadBuilder(this).leftJoinLoad(...args);
|
|
@@ -3,7 +3,7 @@ import StiChildIncompatibleWithReplicaSafeDecorator from '../../errors/sti/StiCh
|
|
|
3
3
|
import StiChildIncompatibleWithSoftDeleteDecorator from '../../errors/sti/StiChildIncompatibleWithSoftDeleteDecorator.js';
|
|
4
4
|
import { scopeImplementation } from '../static-method/Scope.js';
|
|
5
5
|
export const STI_SCOPE_NAME = 'dream:STI';
|
|
6
|
-
export default function STI(dreamClass
|
|
6
|
+
export default function STI(dreamClass) {
|
|
7
7
|
return function (target) {
|
|
8
8
|
const stiChildClass = target;
|
|
9
9
|
const baseClass = dreamClass['sti'].baseClass || dreamClass;
|
|
@@ -21,7 +21,7 @@ export default function STI(dreamClass, { value } = {}) {
|
|
|
21
21
|
stiChildClass['sti'] = {
|
|
22
22
|
active: true,
|
|
23
23
|
baseClass,
|
|
24
|
-
value:
|
|
24
|
+
value: stiChildClass.sanitizedName,
|
|
25
25
|
};
|
|
26
26
|
stiChildClass[STI_SCOPE_NAME] = function (query) {
|
|
27
27
|
return query.where({ type: stiChildClass['sti'].value });
|
|
@@ -5,13 +5,16 @@ export default class LeftJoinLoadBuilder {
|
|
|
5
5
|
dream;
|
|
6
6
|
query;
|
|
7
7
|
/**
|
|
8
|
-
* An intermediate
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* An intermediate builder for loading associations onto a Dream model
|
|
9
|
+
* via a single left-join SQL query. Accessed via `#leftJoinLoad`.
|
|
10
|
+
*
|
|
11
|
+
* **Note:** The LeftJoinLoadBuilder works on a clone of the original model.
|
|
12
|
+
* Call `.execute()` to get the clone with associations loaded.
|
|
11
13
|
*
|
|
12
14
|
* ```ts
|
|
13
15
|
* const user = await User.firstOrFail()
|
|
14
|
-
* await user.
|
|
16
|
+
* const loaded = await user.leftJoinLoad('settings').execute()
|
|
17
|
+
* loaded.settings // association is loaded on the returned clone
|
|
15
18
|
* ```
|
|
16
19
|
*/
|
|
17
20
|
constructor(dream, dreamTransaction) {
|
|
@@ -39,17 +42,20 @@ export default class LeftJoinLoadBuilder {
|
|
|
39
42
|
return this;
|
|
40
43
|
}
|
|
41
44
|
/**
|
|
42
|
-
*
|
|
43
|
-
* all associations
|
|
44
|
-
* instances.
|
|
45
|
+
* Executes the left-join load query, returning a **clone** of the original model
|
|
46
|
+
* with all requested associations loaded. The original instance is not modified.
|
|
45
47
|
*
|
|
46
48
|
* ```ts
|
|
47
49
|
* const user = await User.firstOrFail()
|
|
48
|
-
* await user
|
|
49
|
-
* .
|
|
50
|
-
* .
|
|
50
|
+
* const loaded = await user
|
|
51
|
+
* .leftJoinLoad('settings')
|
|
52
|
+
* .leftJoinLoad('posts', 'comments', 'replies')
|
|
51
53
|
* .execute()
|
|
54
|
+
*
|
|
55
|
+
* loaded.settings // works — associations are on the returned clone
|
|
52
56
|
* ```
|
|
57
|
+
*
|
|
58
|
+
* @returns A clone of the Dream instance with all requested associations loaded
|
|
53
59
|
*/
|
|
54
60
|
async execute() {
|
|
55
61
|
if (this.dreamTransaction) {
|
|
@@ -3,13 +3,16 @@ export default class LoadBuilder {
|
|
|
3
3
|
dream;
|
|
4
4
|
query;
|
|
5
5
|
/**
|
|
6
|
-
* An intermediate
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* An intermediate builder for loading associations onto a Dream model.
|
|
7
|
+
* Accessed via the `#load` method on a Dream instance.
|
|
8
|
+
*
|
|
9
|
+
* **Note:** The LoadBuilder works on a clone of the original model.
|
|
10
|
+
* Call `.execute()` to get the clone with associations loaded.
|
|
9
11
|
*
|
|
10
12
|
* ```ts
|
|
11
13
|
* const user = await User.firstOrFail()
|
|
12
|
-
* await user.load('settings').execute()
|
|
14
|
+
* const loaded = await user.load('settings').execute()
|
|
15
|
+
* loaded.settings // association is loaded on the returned clone
|
|
13
16
|
* ```
|
|
14
17
|
*/
|
|
15
18
|
constructor(dream, dreamTransaction) {
|
|
@@ -21,6 +24,61 @@ export default class LoadBuilder {
|
|
|
21
24
|
this.query = this.query.passthrough(passthroughWhereStatement);
|
|
22
25
|
return this;
|
|
23
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Prevents a specific default scope from applying when
|
|
29
|
+
* loading associations
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* const user = await User.firstOrFail()
|
|
33
|
+
* const loaded = await user
|
|
34
|
+
* .load('posts')
|
|
35
|
+
* .removeDefaultScope('dream:SoftDelete')
|
|
36
|
+
* .execute()
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @param scopeName - The default scope to bypass
|
|
40
|
+
* @returns The LoadBuilder instance for chaining
|
|
41
|
+
*/
|
|
42
|
+
removeDefaultScope(scopeName) {
|
|
43
|
+
this.query = this.query.removeDefaultScope(scopeName);
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Prevents all default scopes from applying when
|
|
48
|
+
* loading associations
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* const user = await User.firstOrFail()
|
|
52
|
+
* const loaded = await user
|
|
53
|
+
* .load('posts')
|
|
54
|
+
* .removeAllDefaultScopes()
|
|
55
|
+
* .execute()
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @returns The LoadBuilder instance for chaining
|
|
59
|
+
*/
|
|
60
|
+
removeAllDefaultScopes() {
|
|
61
|
+
this.query = this.query.removeAllDefaultScopes();
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Sets the database connection to use when loading associations
|
|
66
|
+
*
|
|
67
|
+
* ```ts
|
|
68
|
+
* const user = await User.firstOrFail()
|
|
69
|
+
* const loaded = await user
|
|
70
|
+
* .load('posts')
|
|
71
|
+
* .connection('replica')
|
|
72
|
+
* .execute()
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @param connection - The connection type ('primary' or 'replica')
|
|
76
|
+
* @returns The LoadBuilder instance for chaining
|
|
77
|
+
*/
|
|
78
|
+
connection(connection) {
|
|
79
|
+
this.query = this.query.connection(connection);
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
24
82
|
/**
|
|
25
83
|
* Attaches a load statement to the load builder
|
|
26
84
|
*
|
|
@@ -89,19 +147,22 @@ export default class LoadBuilder {
|
|
|
89
147
|
return this;
|
|
90
148
|
}
|
|
91
149
|
/**
|
|
92
|
-
* Executes
|
|
93
|
-
*
|
|
94
|
-
*
|
|
150
|
+
* Executes the load query, binding all associations to a **clone** of the
|
|
151
|
+
* original model instance. The original instance is not modified.
|
|
152
|
+
*
|
|
153
|
+
* You must use the returned value to access the loaded associations:
|
|
95
154
|
*
|
|
96
155
|
* ```ts
|
|
97
156
|
* const user = await User.firstOrFail()
|
|
98
|
-
* await user
|
|
157
|
+
* const loaded = await user
|
|
99
158
|
* .load('settings')
|
|
100
159
|
* .load('posts', 'comments', 'replies', ['image', 'localizedText'])
|
|
101
160
|
* .execute()
|
|
161
|
+
*
|
|
162
|
+
* loaded.settings // works — associations are on the returned clone
|
|
102
163
|
* ```
|
|
103
164
|
*
|
|
104
|
-
* @returns
|
|
165
|
+
* @returns A clone of the Dream instance with all requested associations loaded
|
|
105
166
|
*/
|
|
106
167
|
async execute() {
|
|
107
168
|
if (this.dreamTransaction) {
|
|
@@ -17,10 +17,10 @@ import namespaceColumn from '../helpers/namespaceColumn.js';
|
|
|
17
17
|
import protectAgainstPollutingAssignment from '../helpers/protectAgainstPollutingAssignment.js';
|
|
18
18
|
import { toSafeObject } from '../helpers/toSafeObject.js';
|
|
19
19
|
import ops from '../ops/index.js';
|
|
20
|
+
import buildSerializerPreloadPaths from './internal/buildSerializerPreloadPaths.js';
|
|
20
21
|
import computedPaginatePage from './internal/computedPaginatePage.js';
|
|
21
22
|
import convertDreamClassAndAssociationNameTupleArrayToPreloadArgs from './internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js';
|
|
22
|
-
|
|
23
|
-
// Cache for extractNestedPaths results, keyed by "${sanitizedName}:${serializerKey}"
|
|
23
|
+
// Cache for serializer preload-path results, keyed by "${globalName}:${serializerKey}"
|
|
24
24
|
const extractedNestedPathsCache = new Map();
|
|
25
25
|
export default class Query {
|
|
26
26
|
/**
|
|
@@ -526,11 +526,10 @@ export default class Query {
|
|
|
526
526
|
* @returns A Query with all serialization associations preloaded
|
|
527
527
|
*/
|
|
528
528
|
preloadFor(serializerKey, modifierFn) {
|
|
529
|
-
const cacheKey = `${this.dreamClass.
|
|
529
|
+
const cacheKey = `${this.dreamClass.globalName}:${serializerKey ?? 'default'}`;
|
|
530
530
|
let preloadArgs = extractedNestedPathsCache.get(cacheKey);
|
|
531
531
|
if (!preloadArgs) {
|
|
532
|
-
|
|
533
|
-
preloadArgs = extractNestedPaths(this.dreamClass['serializationMap'](serializerKey));
|
|
532
|
+
preloadArgs = buildSerializerPreloadPaths(this.dreamClass, serializerKey);
|
|
534
533
|
extractedNestedPathsCache.set(cacheKey, preloadArgs);
|
|
535
534
|
}
|
|
536
535
|
let query = this;
|
|
@@ -1698,7 +1697,7 @@ export default class Query {
|
|
|
1698
1697
|
* Updates all records matching the Query
|
|
1699
1698
|
*
|
|
1700
1699
|
* ```ts
|
|
1701
|
-
* await User.where({ email: ops.ilike('%burpcollaborator%') }).
|
|
1700
|
+
* await User.where({ email: ops.ilike('%burpcollaborator%') }).update({ email: null })
|
|
1702
1701
|
* // 12
|
|
1703
1702
|
* ```
|
|
1704
1703
|
* @param attributes - The attributes used to update the records
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export const primaryKeyTypes = ['uuid7', 'uuid4', 'bigserial', 'bigint', 'integer'];
|
|
2
2
|
export const TRIGRAM_OPERATORS = ['%', '<%', '<<%'];
|
|
3
|
+
export const RECURSIVE_SERIALIZATION_MAX_REPEATS = 4;
|
|
4
|
+
export const RECURSIVE_DESTROY_PRELOAD_MAX_REPEATS = RECURSIVE_SERIALIZATION_MAX_REPEATS;
|
|
3
5
|
class RequiredAttribute {
|
|
4
6
|
constructor() { }
|
|
5
7
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export default function buildAssociationPaths(rootNode, opts) {
|
|
2
|
+
const paths = [];
|
|
3
|
+
traverseAssociationPaths(rootNode, [], {}, paths, opts);
|
|
4
|
+
return paths;
|
|
5
|
+
}
|
|
6
|
+
function traverseAssociationPaths(node, currentPath, depthTracker, paths, opts) {
|
|
7
|
+
const trackerId = opts.getKey(node);
|
|
8
|
+
depthTracker[trackerId] ??= 0;
|
|
9
|
+
if (depthTracker[trackerId] + 1 > opts.maxRepeats) {
|
|
10
|
+
if (currentPath.length > 0) {
|
|
11
|
+
paths.push([...currentPath]);
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
depthTracker[trackerId]++;
|
|
16
|
+
const edges = opts.getEdges(node);
|
|
17
|
+
if (edges.length === 0) {
|
|
18
|
+
if (currentPath.length > 0) {
|
|
19
|
+
paths.push([...currentPath]);
|
|
20
|
+
}
|
|
21
|
+
depthTracker[trackerId]--;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
for (const edge of edges) {
|
|
25
|
+
const newPath = [...currentPath, edge.tuple];
|
|
26
|
+
if (!edge.nextNode) {
|
|
27
|
+
paths.push(newPath);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
traverseAssociationPaths(edge.nextNode, newPath, depthTracker, paths, opts);
|
|
31
|
+
}
|
|
32
|
+
depthTracker[trackerId]--;
|
|
33
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { RECURSIVE_DESTROY_PRELOAD_MAX_REPEATS } from '../constants.js';
|
|
2
|
+
import buildAssociationPaths from './buildAssociationPaths.js';
|
|
3
|
+
const dependentDestroyPreloadPathsCache = new Map();
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*
|
|
7
|
+
* Recursively walks `dependent: 'destroy'` associations starting from the given
|
|
8
|
+
* Dream class, producing an array of preload paths. Each path is an array of
|
|
9
|
+
* [DreamClass, associationName] tuples representing a chain from root to leaf.
|
|
10
|
+
*
|
|
11
|
+
* Allows the same Dream class to appear up to MAX_REPEATS times in a single
|
|
12
|
+
* path to support tree structures (e.g. a Category with `dependent: 'destroy'`
|
|
13
|
+
* on its children, which are also Categories).
|
|
14
|
+
*
|
|
15
|
+
* Used to build a preload tree so that all records in the cascade can be loaded
|
|
16
|
+
* upfront, eliminating N+1 queries during destroy.
|
|
17
|
+
*/
|
|
18
|
+
export default function buildDependentDestroyPreloadPaths(dreamClass) {
|
|
19
|
+
const cacheKey = dreamClass.globalName;
|
|
20
|
+
const cached = dependentDestroyPreloadPathsCache.get(cacheKey);
|
|
21
|
+
if (cached)
|
|
22
|
+
return cached;
|
|
23
|
+
const paths = [];
|
|
24
|
+
paths.push(...buildAssociationPaths(dreamClass, {
|
|
25
|
+
getKey: currentDreamClass => currentDreamClass.globalName,
|
|
26
|
+
getEdges: currentDreamClass => {
|
|
27
|
+
const associationMap = currentDreamClass['associationMetadataMap']();
|
|
28
|
+
const dependentDestroyNames = Object.keys(associationMap).filter(key => associationMap[key].dependent === 'destroy');
|
|
29
|
+
return dependentDestroyNames.flatMap(associationName => {
|
|
30
|
+
const association = associationMap[associationName];
|
|
31
|
+
const modelCBResult = association.modelCB();
|
|
32
|
+
const targetClasses = Array.isArray(modelCBResult) ? modelCBResult : [modelCBResult];
|
|
33
|
+
return targetClasses.map(targetClass => ({
|
|
34
|
+
nextNode: targetClass,
|
|
35
|
+
tuple: [currentDreamClass, associationName],
|
|
36
|
+
}));
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
maxRepeats: RECURSIVE_DESTROY_PRELOAD_MAX_REPEATS,
|
|
40
|
+
}));
|
|
41
|
+
dependentDestroyPreloadPathsCache.set(cacheKey, paths);
|
|
42
|
+
return paths;
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { inferSerializersFromDreamClassOrViewModelClass } from '../../serializer/helpers/inferSerializerFromDreamOrViewModel.js';
|
|
2
|
+
import { RECURSIVE_SERIALIZATION_MAX_REPEATS } from '../constants.js';
|
|
3
|
+
import buildAssociationPaths from './buildAssociationPaths.js';
|
|
4
|
+
import resolveSerializerAssociationEdges from './resolveSerializerAssociationEdges.js';
|
|
5
|
+
export default function buildSerializerPreloadPaths(dreamClass, serializerKey) {
|
|
6
|
+
const key = serializerKey || 'default';
|
|
7
|
+
const serializer = inferSerializersFromDreamClassOrViewModelClass(dreamClass, key)[0] ?? null;
|
|
8
|
+
if (!serializer)
|
|
9
|
+
throw new Error(`unable to find serializer with key: ${key}`);
|
|
10
|
+
const paths = buildAssociationPaths({
|
|
11
|
+
dreamClass,
|
|
12
|
+
serializer,
|
|
13
|
+
}, {
|
|
14
|
+
getKey: node => node.dreamClass.globalName,
|
|
15
|
+
getEdges: serializerNodeToEdges,
|
|
16
|
+
maxRepeats: RECURSIVE_SERIALIZATION_MAX_REPEATS,
|
|
17
|
+
});
|
|
18
|
+
const dedupedPaths = new Map();
|
|
19
|
+
for (const path of paths) {
|
|
20
|
+
dedupedPaths.set(path
|
|
21
|
+
.map(([pathDreamClass, associationName]) => `${pathDreamClass.globalName}:${associationName}`)
|
|
22
|
+
.join('|'), path);
|
|
23
|
+
}
|
|
24
|
+
return [...dedupedPaths.values()];
|
|
25
|
+
}
|
|
26
|
+
function serializerNodeToEdges({ dreamClass, serializer, }) {
|
|
27
|
+
const edges = resolveSerializerAssociationEdges(dreamClass, serializer);
|
|
28
|
+
return edges.flatMap(edge => {
|
|
29
|
+
const tuple = [dreamClass, edge.associationAs];
|
|
30
|
+
if (edge.type === 'delegatedAttribute' || edge.targets.length === 0) {
|
|
31
|
+
return [{ nextNode: null, tuple }];
|
|
32
|
+
}
|
|
33
|
+
return edge.targets.map(target => ({
|
|
34
|
+
nextNode: target,
|
|
35
|
+
tuple,
|
|
36
|
+
}));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -2,17 +2,25 @@
|
|
|
2
2
|
* @internal
|
|
3
3
|
*
|
|
4
4
|
* Destroys all HasOne/HasMany associations on this
|
|
5
|
-
* dream that are marked as `dependent: 'destroy'
|
|
5
|
+
* dream that are marked as `dependent: 'destroy'`.
|
|
6
|
+
*
|
|
7
|
+
* Expects associations to be preloaded onto the dream instance
|
|
8
|
+
* via `loadDependentDestroyTree`. Iterates loaded associations
|
|
9
|
+
* directly instead of querying the database.
|
|
6
10
|
*/
|
|
7
11
|
export default async function destroyAssociatedRecords(dream, txn, options) {
|
|
8
12
|
const dreamClass = dream.constructor;
|
|
9
13
|
const { reallyDestroy } = options;
|
|
10
14
|
for (const associationName of dreamClass['dependentDestroyAssociationNames']()) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
const loaded = dream[associationName];
|
|
16
|
+
const records = Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
|
|
17
|
+
for (const record of records) {
|
|
18
|
+
if (reallyDestroy) {
|
|
19
|
+
await record.txn(txn).reallyDestroy(options);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
await record.txn(txn).destroy(options);
|
|
23
|
+
}
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
26
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import DreamApp from '../../dream-app/index.js';
|
|
2
2
|
import destroyAssociatedRecords from './destroyAssociatedRecords.js';
|
|
3
|
+
import loadDependentDestroyTree from './loadDependentDestroyTree.js';
|
|
3
4
|
import runHooksFor from './runHooksFor.js';
|
|
4
5
|
/**
|
|
5
6
|
* @internal
|
|
@@ -30,7 +31,12 @@ export default async function destroyDream(dream, txn = null, options) {
|
|
|
30
31
|
async function destroyDreamWithTransaction(dream, txn, options) {
|
|
31
32
|
const { cascade, reallyDestroy, skipHooks } = options;
|
|
32
33
|
if (cascade) {
|
|
33
|
-
await
|
|
34
|
+
const dreamWithAssociations = await loadDependentDestroyTree(dream, txn, {
|
|
35
|
+
reallyDestroy,
|
|
36
|
+
bypassAllDefaultScopes: options.bypassAllDefaultScopes ?? false,
|
|
37
|
+
defaultScopesToBypass: options.defaultScopesToBypass ?? [],
|
|
38
|
+
});
|
|
39
|
+
await destroyAssociatedRecords(dreamWithAssociations, txn, options);
|
|
34
40
|
}
|
|
35
41
|
if (!skipHooks) {
|
|
36
42
|
await runHooksFor('beforeDestroy', dream, true, null, txn);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import LoadBuilder from '../LoadBuilder.js';
|
|
2
|
+
import buildDependentDestroyPreloadPaths from './buildDependentDestroyPreloadPaths.js';
|
|
3
|
+
import convertDreamClassAndAssociationNameTupleArrayToPreloadArgs from './convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js';
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
*
|
|
7
|
+
* Loads all `dependent: 'destroy'` associations onto the dream instance
|
|
8
|
+
* upfront, eliminating N+1 queries during cascade destruction.
|
|
9
|
+
*
|
|
10
|
+
* Returns a clone of the dream with all associations loaded.
|
|
11
|
+
* If there are no dependent-destroy associations, returns the original dream.
|
|
12
|
+
*/
|
|
13
|
+
export default async function loadDependentDestroyTree(dream, txn, { reallyDestroy, bypassAllDefaultScopes, defaultScopesToBypass, }) {
|
|
14
|
+
const dreamClass = dream.constructor;
|
|
15
|
+
const paths = buildDependentDestroyPreloadPaths(dreamClass);
|
|
16
|
+
if (paths.length === 0)
|
|
17
|
+
return dream;
|
|
18
|
+
let loadBuilder = new LoadBuilder(dream, txn);
|
|
19
|
+
for (const path of paths) {
|
|
20
|
+
const args = convertDreamClassAndAssociationNameTupleArrayToPreloadArgs(path);
|
|
21
|
+
loadBuilder = loadBuilder.load(...args);
|
|
22
|
+
}
|
|
23
|
+
if (bypassAllDefaultScopes) {
|
|
24
|
+
loadBuilder = loadBuilder.removeAllDefaultScopes();
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
for (const scopeName of defaultScopesToBypass) {
|
|
28
|
+
loadBuilder = loadBuilder.removeDefaultScope(scopeName);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (reallyDestroy) {
|
|
32
|
+
loadBuilder = loadBuilder.removeDefaultScope('dream:SoftDelete');
|
|
33
|
+
}
|
|
34
|
+
return await loadBuilder.execute();
|
|
35
|
+
}
|