@rvoh/dream 2.21.1 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/Dream.js +71 -23
- package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +3 -0
- package/dist/cjs/src/dream/Query.js +242 -12
- package/dist/cjs/src/dream/QueryDriver/Base.js +26 -0
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +46 -4
- package/dist/cjs/src/dream/QueryDriver/Postgres.js +21 -0
- package/dist/cjs/src/dream/internal/buildSerializerPreloadPaths.js +93 -20
- package/dist/cjs/src/dream/internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js +18 -10
- package/dist/cjs/src/dream/internal/destroyAssociatedRecords.js +76 -4
- package/dist/cjs/src/dream/internal/destroyDream.js +6 -7
- package/dist/cjs/src/dream/internal/mergeSerializerAssociationEdges.js +75 -0
- package/dist/cjs/src/dream/internal/printSerializerHierarchyLevel.js +50 -8
- package/dist/cjs/src/dream/internal/resolveSerializerAssociationEdges.js +79 -14
- package/dist/cjs/src/errors/InvalidBatchSize.js +19 -0
- package/dist/cjs/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
- package/dist/cjs/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
- package/dist/cjs/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -0
- package/dist/cjs/src/errors/serializers/NoSerializersResolvedForKey.js +30 -0
- package/dist/cjs/src/ops/index.js +19 -0
- package/dist/cjs/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +22 -0
- package/dist/cjs/src/serializer/helpers/serializerForAssociatedClass.js +13 -0
- package/dist/esm/src/Dream.js +71 -23
- package/dist/esm/src/dream/DreamClassTransactionBuilder.js +3 -0
- package/dist/esm/src/dream/Query.js +242 -12
- package/dist/esm/src/dream/QueryDriver/Base.js +26 -0
- package/dist/esm/src/dream/QueryDriver/Kysely.js +46 -4
- package/dist/esm/src/dream/QueryDriver/Postgres.js +21 -0
- package/dist/esm/src/dream/internal/buildSerializerPreloadPaths.js +93 -20
- package/dist/esm/src/dream/internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js +18 -10
- package/dist/esm/src/dream/internal/destroyAssociatedRecords.js +76 -4
- package/dist/esm/src/dream/internal/destroyDream.js +6 -7
- package/dist/esm/src/dream/internal/mergeSerializerAssociationEdges.js +75 -0
- package/dist/esm/src/dream/internal/printSerializerHierarchyLevel.js +50 -8
- package/dist/esm/src/dream/internal/resolveSerializerAssociationEdges.js +79 -14
- package/dist/esm/src/errors/InvalidBatchSize.js +19 -0
- package/dist/esm/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
- package/dist/esm/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
- package/dist/esm/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -0
- package/dist/esm/src/errors/serializers/NoSerializersResolvedForKey.js +30 -0
- package/dist/esm/src/ops/index.js +19 -0
- package/dist/esm/src/serializer/helpers/inferSerializerFromDreamOrViewModel.js +22 -0
- package/dist/esm/src/serializer/helpers/serializerForAssociatedClass.js +13 -0
- package/dist/types/src/Dream.d.ts +12 -0
- package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +3 -0
- package/dist/types/src/dream/Query.d.ts +150 -3
- package/dist/types/src/dream/QueryDriver/Base.d.ts +21 -0
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +1 -0
- package/dist/types/src/dream/QueryDriver/Postgres.d.ts +20 -0
- package/dist/types/src/dream/internal/destroyAssociatedRecords.d.ts +22 -3
- package/dist/types/src/dream/internal/mergeSerializerAssociationEdges.d.ts +69 -0
- package/dist/types/src/dream/internal/printSerializerHierarchyLevel.d.ts +23 -5
- package/dist/types/src/dream/internal/resolveSerializerAssociationEdges.d.ts +27 -1
- package/dist/types/src/errors/InvalidBatchSize.d.ts +5 -0
- package/dist/types/src/errors/RowLockIncompatibleWithDistinct.d.ts +3 -0
- package/dist/types/src/errors/RowLockIncompatibleWithJoinLoad.d.ts +3 -0
- package/dist/types/src/errors/ops/ArrayEqualityRequiresArrayColumn.d.ts +8 -0
- package/dist/types/src/errors/serializers/NoSerializersResolvedForKey.d.ts +18 -0
- package/dist/types/src/ops/index.d.ts +19 -0
- package/dist/types/src/serializer/helpers/inferSerializerFromDreamOrViewModel.d.ts +16 -0
- package/dist/types/src/types/associations/shared.d.ts +2 -2
- package/dist/types/src/types/dream.d.ts +8 -0
- package/dist/types/src/types/types/associations/shared.ts +84 -55
- package/dist/types/src/types/types/dream.ts +30 -0
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +55 -41
- package/docs/classes/db.PostgresQueryDriver.html +55 -42
- package/docs/classes/db.QueryDriverBase.html +54 -40
- package/docs/classes/errors.CheckConstraintViolation.html +3 -3
- package/docs/classes/errors.ColumnOverflow.html +3 -3
- package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
- package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
- package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
- package/docs/classes/errors.DecryptionError.html +2 -2
- package/docs/classes/errors.DecryptionParseError.html +2 -2
- package/docs/classes/errors.DecryptionRotationError.html +3 -3
- package/docs/classes/errors.GlobalNameNotSet.html +3 -3
- package/docs/classes/errors.InvalidCalendarDate.html +2 -2
- package/docs/classes/errors.InvalidClockTime.html +2 -2
- package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
- package/docs/classes/errors.InvalidDateTime.html +2 -2
- package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
- package/docs/classes/errors.NonLoadedAssociation.html +3 -3
- package/docs/classes/errors.NotNullViolation.html +3 -3
- package/docs/classes/errors.RecordNotFound.html +3 -3
- package/docs/classes/errors.ValidationError.html +3 -3
- package/docs/classes/index.CalendarDate.html +33 -33
- package/docs/classes/index.ClockTime.html +32 -32
- package/docs/classes/index.ClockTimeTz.html +35 -35
- package/docs/classes/index.DateTime.html +86 -86
- package/docs/classes/index.Decorators.html +19 -19
- package/docs/classes/index.Dream.html +127 -125
- package/docs/classes/index.DreamApp.html +10 -10
- package/docs/classes/index.DreamTransaction.html +2 -2
- package/docs/classes/index.Env.html +2 -2
- package/docs/classes/index.Query.html +161 -71
- package/docs/classes/system.CliFileWriter.html +4 -4
- package/docs/classes/system.DreamBin.html +2 -2
- package/docs/classes/system.DreamCLI.html +7 -7
- package/docs/classes/system.DreamImporter.html +2 -2
- package/docs/classes/system.DreamLogos.html +2 -2
- package/docs/classes/system.DreamSerializerBuilder.html +11 -11
- package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
- package/docs/classes/system.PathHelpers.html +3 -3
- package/docs/classes/utils.Encrypt.html +3 -3
- package/docs/classes/utils.Range.html +2 -2
- package/docs/functions/db.closeAllDbConnections.html +1 -1
- package/docs/functions/db.dreamDbConnections.html +1 -1
- package/docs/functions/db.untypedDb.html +1 -1
- package/docs/functions/db.validateColumn.html +1 -1
- package/docs/functions/db.validateTable.html +1 -1
- package/docs/functions/errors.pgErrorType.html +1 -1
- package/docs/functions/index.DreamSerializer.html +1 -1
- package/docs/functions/index.ObjectSerializer.html +1 -1
- package/docs/functions/index.ReplicaSafe.html +1 -1
- package/docs/functions/index.STI.html +1 -1
- package/docs/functions/index.SoftDelete.html +1 -1
- package/docs/functions/utils.camelize.html +1 -1
- package/docs/functions/utils.capitalize.html +1 -1
- package/docs/functions/utils.cloneDeepSafe.html +1 -1
- package/docs/functions/utils.compact.html +1 -1
- package/docs/functions/utils.groupBy.html +1 -1
- package/docs/functions/utils.hyphenize.html +1 -1
- package/docs/functions/utils.intersection.html +1 -1
- package/docs/functions/utils.isEmpty.html +1 -1
- package/docs/functions/utils.normalizeUnicode.html +1 -1
- package/docs/functions/utils.pascalize.html +1 -1
- package/docs/functions/utils.percent.html +1 -1
- package/docs/functions/utils.range.html +1 -1
- package/docs/functions/utils.round.html +1 -1
- package/docs/functions/utils.sanitizeString.html +1 -1
- package/docs/functions/utils.snakeify.html +1 -1
- package/docs/functions/utils.sort.html +1 -1
- package/docs/functions/utils.sortBy.html +1 -1
- package/docs/functions/utils.sortObjectByKey.html +1 -1
- package/docs/functions/utils.sortObjectByValue.html +1 -1
- package/docs/functions/utils.uncapitalize.html +1 -1
- package/docs/functions/utils.uniq.html +1 -1
- package/docs/hierarchy.html +1 -1
- package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
- package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
- package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
- package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
- package/docs/interfaces/types.BelongsToStatement.html +2 -2
- package/docs/interfaces/types.DecoratorContext.html +2 -2
- package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
- package/docs/interfaces/types.DreamAppOpts.html +2 -2
- package/docs/interfaces/types.DreamDbConfig.html +5 -5
- package/docs/interfaces/types.DurationObject.html +2 -2
- package/docs/interfaces/types.EncryptOptions.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
- package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
- package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
- package/docs/types/openapi.OpenapiAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiFormats.html +1 -1
- package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
- package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
- package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaString.html +1 -1
- package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiTypeField.html +1 -1
- package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
- package/docs/types/types.CalendarDateDurationUnit.html +1 -1
- package/docs/types/types.CalendarDateObject.html +1 -1
- package/docs/types/types.Camelized.html +1 -1
- package/docs/types/types.ClockTimeObject.html +1 -1
- package/docs/types/types.DbConnectionType.html +1 -1
- package/docs/types/types.DbTypes.html +1 -1
- package/docs/types/types.DreamAssociationMetadata.html +1 -1
- package/docs/types/types.DreamAttributes.html +1 -1
- package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
- package/docs/types/types.DreamClassColumn.html +1 -1
- package/docs/types/types.DreamColumn.html +1 -1
- package/docs/types/types.DreamColumnNames.html +1 -1
- package/docs/types/types.DreamLogLevel.html +1 -1
- package/docs/types/types.DreamLogger.html +2 -2
- package/docs/types/types.DreamModelSerializerType.html +1 -1
- package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
- package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
- package/docs/types/types.DreamParamSafeAttributes.html +1 -1
- package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
- package/docs/types/types.DreamSerializable.html +1 -1
- package/docs/types/types.DreamSerializableArray.html +1 -1
- package/docs/types/types.DreamSerializerKey.html +1 -1
- package/docs/types/types.DreamSerializers.html +1 -1
- package/docs/types/types.DreamVirtualColumns.html +1 -1
- package/docs/types/types.DurationUnit.html +1 -1
- package/docs/types/types.EncryptAlgorithm.html +1 -1
- package/docs/types/types.HasManyStatement.html +1 -1
- package/docs/types/types.HasOneStatement.html +1 -1
- package/docs/types/types.Hyphenized.html +1 -1
- package/docs/types/types.Pascalized.html +1 -1
- package/docs/types/types.PrimaryKeyType.html +1 -1
- package/docs/types/types.RoundingPrecision.html +1 -1
- package/docs/types/types.SerializerCasing.html +1 -1
- package/docs/types/types.SimpleObjectSerializerType.html +1 -1
- package/docs/types/types.Snakeified.html +1 -1
- package/docs/types/types.StrictInterface.html +1 -1
- package/docs/types/types.UpdateableAssociationProperties.html +1 -1
- package/docs/types/types.UpdateableProperties.html +1 -1
- package/docs/types/types.ValidationType.html +1 -1
- package/docs/types/types.ViewModel.html +2 -2
- package/docs/types/types.ViewModelClass.html +1 -1
- package/docs/types/types.WeekdayName.html +1 -1
- package/docs/types/types.WhereStatementForDream.html +1 -1
- package/docs/types/types.WhereStatementForDreamClass.html +1 -1
- package/docs/variables/index.DreamConst.html +1 -1
- package/docs/variables/index.ops.html +38 -23
- package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
- package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
- package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
- package/docs/variables/system.primaryKeyTypes.html +1 -1
- package/package.json +1 -1
|
@@ -1,18 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { inferSerializersFromDreamClassOrViewModelClassOrFail } from '../../serializer/helpers/inferSerializerFromDreamOrViewModel.js';
|
|
2
2
|
import { RECURSIVE_SERIALIZATION_MAX_REPEATS } from '../constants.js';
|
|
3
3
|
import buildAssociationPaths from './buildAssociationPaths.js';
|
|
4
|
-
import
|
|
4
|
+
import mergeSerializerAssociationEdges from './mergeSerializerAssociationEdges.js';
|
|
5
5
|
export default function buildSerializerPreloadPaths(dreamClass, serializerKey) {
|
|
6
6
|
const key = serializerKey || 'default';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
/**
|
|
8
|
+
* When `dreamClass` is an STI base, this returns one serializer per STI child (deduped), since
|
|
9
|
+
* each child may register a different serializer under `key`. Every child's serializer has to be
|
|
10
|
+
* walked: an association rendered only by, say, the alphabetically-last child would otherwise be
|
|
11
|
+
* missing from the preload set and throw NonLoadedAssociation at render time.
|
|
12
|
+
*/
|
|
13
|
+
const serializers = inferSerializersFromDreamClassOrViewModelClassOrFail(dreamClass, key);
|
|
14
|
+
/**
|
|
15
|
+
* All of the serializers are walked in a *single* traversal, as one node per Dream class carrying
|
|
16
|
+
* the set of serializers that reach it, rather than one traversal per serializer. A path is a
|
|
17
|
+
* sequence of [DreamClass, associationName] tuples — it records no serializer — so walking each
|
|
18
|
+
* serializer separately recomputes the same paths once per route through the graph, which is
|
|
19
|
+
* exponential in the number of sibling serializers when a serializer cycles back to the STI base.
|
|
20
|
+
* Merging is exactly equivalent: an edge is emitted from the merged node when *some* serializer in
|
|
21
|
+
* the set declares it, and every serializer in the set is reachable by a real route with this
|
|
22
|
+
* path's prefix, so the merged walk emits precisely the paths the per-serializer walks emit after
|
|
23
|
+
* deduping — with one exception: where one serializer of a class renders no associations and
|
|
24
|
+
* another renders some, the per-serializer walk also emitted the bare prefix path reached by the
|
|
25
|
+
* first, which is a strict prefix of a path the second contributes and so preloads nothing extra.
|
|
26
|
+
* The recursion budget is keyed on the Dream class and counted along the current path, so it is
|
|
27
|
+
* unaffected by the merge.
|
|
28
|
+
*
|
|
29
|
+
* Every tuple emitted at the root of the traversal carries `dreamClass` (the STI base) rather than
|
|
30
|
+
* the contributing child, both so that identical paths dedupe below and so that a caller's
|
|
31
|
+
* `modifierFn` keeps receiving the class it was queried on. Tuples emitted deeper in a path carry
|
|
32
|
+
* the association's target class, which is what `modifierFn` receives for a nested association.
|
|
33
|
+
*/
|
|
34
|
+
const paths = buildAssociationPaths({ dreamClass, serializers }, {
|
|
14
35
|
getKey: node => node.dreamClass.globalName,
|
|
15
|
-
getEdges:
|
|
36
|
+
getEdges: memoizedSerializerNodeToEdges(),
|
|
16
37
|
maxRepeats: RECURSIVE_SERIALIZATION_MAX_REPEATS,
|
|
17
38
|
});
|
|
18
39
|
const dedupedPaths = new Map();
|
|
@@ -23,16 +44,68 @@ export default function buildSerializerPreloadPaths(dreamClass, serializerKey) {
|
|
|
23
44
|
}
|
|
24
45
|
return [...dedupedPaths.values()];
|
|
25
46
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
/**
|
|
48
|
+
* `serializerNodeToEdges` is a pure function of the node's (Dream class, ordered serializer set), so
|
|
49
|
+
* two nodes carrying the same pair have the same edges no matter which route reached them. The
|
|
50
|
+
* traversal hands `getEdges` a *fresh* node object per edge it descends — the `nextNode`s built
|
|
51
|
+
* below are new objects every time — so memoizing on the object itself would never hit; the key has
|
|
52
|
+
* to be the node's contents. Without this the whole merge (resolve every serializer's edges, then
|
|
53
|
+
* one Map per association and one node object per target class) is rebuilt at every visit, and the
|
|
54
|
+
* visit count is the number of *routes* through the serializer graph rather than the number of
|
|
55
|
+
* distinct nodes in it: an STI base whose children render `n` differently-named self-referential
|
|
56
|
+
* associations visits 1 + n + n² + n³ nodes to reach n⁴ paths, all of them the same one node.
|
|
57
|
+
*
|
|
58
|
+
* The serializer identifiers are assigned per call rather than globally so that the key stays short
|
|
59
|
+
* and so that nothing here holds a serializer alive; identity is all that matters, since the key is
|
|
60
|
+
* only ever compared against others built in the same call. Order-sensitive on purpose: the merge
|
|
61
|
+
* is insertion-ordered, so two nodes carrying the same serializers in a different order do not have
|
|
62
|
+
* the same edges.
|
|
63
|
+
*
|
|
64
|
+
* Scoped to a single `buildSerializerPreloadPaths` call for the same reason: it cannot then serve
|
|
65
|
+
* edges resolved against a stale serializer registry or a not-yet-complete set of STI children.
|
|
66
|
+
*/
|
|
67
|
+
function memoizedSerializerNodeToEdges() {
|
|
68
|
+
const cache = new Map();
|
|
69
|
+
const serializerIds = new Map();
|
|
70
|
+
const serializerId = (serializer) => {
|
|
71
|
+
let id = serializerIds.get(serializer);
|
|
72
|
+
if (id === undefined) {
|
|
73
|
+
id = serializerIds.size;
|
|
74
|
+
serializerIds.set(serializer, id);
|
|
32
75
|
}
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}))
|
|
76
|
+
return id;
|
|
77
|
+
};
|
|
78
|
+
return node => {
|
|
79
|
+
const cacheKey = `${node.dreamClass.globalName}|${node.serializers.map(serializerId).join(',')}`;
|
|
80
|
+
const cached = cache.get(cacheKey);
|
|
81
|
+
if (cached !== undefined)
|
|
82
|
+
return cached;
|
|
83
|
+
const edges = serializerNodeToEdges(node);
|
|
84
|
+
cache.set(cacheKey, edges);
|
|
85
|
+
return edges;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* `mergeSerializerAssociationEdges` — the single union both serializer walks run on, the other being
|
|
90
|
+
* `Dream.recursiveSerializationMap`'s display walk — has already resolved every serializer in the
|
|
91
|
+
* node's set and merged them into one insertion-ordered edge set keyed on (association, target
|
|
92
|
+
* class). All that is left here is to shed what only the display walk needs (the per-serializer edge
|
|
93
|
+
* type and serializer-side association name) and turn each merged edge into path edges: a leaf where
|
|
94
|
+
* some serializer terminates the association, plus one traversal node per target class carrying that
|
|
95
|
+
* class's serializers.
|
|
96
|
+
*/
|
|
97
|
+
function serializerNodeToEdges({ dreamClass, serializers, }) {
|
|
98
|
+
return mergeSerializerAssociationEdges(dreamClass, serializers).flatMap(edge => {
|
|
99
|
+
const tuple = [dreamClass, edge.associationAs];
|
|
100
|
+
return [
|
|
101
|
+
...(edge.terminates ? [{ nextNode: null, tuple }] : []),
|
|
102
|
+
...edge.targets.map(target => ({
|
|
103
|
+
nextNode: {
|
|
104
|
+
dreamClass: target.dreamClass,
|
|
105
|
+
serializers: target.serializers.map(({ serializer }) => serializer),
|
|
106
|
+
},
|
|
107
|
+
tuple,
|
|
108
|
+
})),
|
|
109
|
+
];
|
|
37
110
|
});
|
|
38
111
|
}
|
package/dist/cjs/src/dream/internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import compact from '../../helpers/compact.js';
|
|
2
1
|
export default function convertDreamClassAndAssociationNameTupleArrayToPreloadArgs(dreamClassAndAssociationNameTupleArray, modifierFn, counter) {
|
|
3
|
-
|
|
2
|
+
const preloadArgs = [];
|
|
3
|
+
for (const dreamClassAndAssociationNameTuple of dreamClassAndAssociationNameTupleArray) {
|
|
4
4
|
const associationName = dreamClassAndAssociationNameTuple[1];
|
|
5
|
-
const aliasedAssociationName = counter
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
const aliasedAssociationName = counter ? `${associationName} as drsz${counter.count++}` : associationName;
|
|
6
|
+
if (!modifierFn) {
|
|
7
|
+
preloadArgs.push(aliasedAssociationName);
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
10
|
const modifier = modifierFn(associationName, dreamClassAndAssociationNameTuple[0]);
|
|
11
|
+
// omitting an association prunes the entire subtree beneath it: the path is truncated at the
|
|
12
|
+
// omitted association, keeping the ancestors already accumulated (so the omitted association's
|
|
13
|
+
// parent is still preloaded) and discarding the remainder, since the surviving tail would be
|
|
14
|
+
// re-rooted on the class the query was rooted on and preload a different association than the
|
|
15
|
+
// caller asked for. When the omitted association is the first tuple, this yields an empty list.
|
|
11
16
|
if (modifier === 'omit')
|
|
12
|
-
return
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
return preloadArgs;
|
|
18
|
+
preloadArgs.push(aliasedAssociationName);
|
|
19
|
+
if (modifier !== undefined && modifier !== null)
|
|
20
|
+
preloadArgs.push(modifier);
|
|
21
|
+
}
|
|
22
|
+
return preloadArgs;
|
|
15
23
|
}
|
|
@@ -1,20 +1,69 @@
|
|
|
1
|
+
import loadDependentDestroyTree from './loadDependentDestroyTree.js';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* Marks a Dream instance as one whose `dependent: 'destroy'` associations were
|
|
6
|
+
* hydrated by this cascade's own call to `loadDependentDestroyTree`, using the
|
|
7
|
+
* same destroy options the cascade is running under.
|
|
8
|
+
*
|
|
9
|
+
* Associations a caller happened to preload themselves (e.g.
|
|
10
|
+
* `Post.preload('comments')`) are deliberately NOT trusted: they may have been
|
|
11
|
+
* loaded under different default scopes than the destroy requires — most
|
|
12
|
+
* importantly, `reallyDestroy` strips the soft-delete scope, so a
|
|
13
|
+
* caller-preloaded association could be missing soft-deleted records that the
|
|
14
|
+
* cascade must destroy.
|
|
15
|
+
*/
|
|
16
|
+
const CASCADE_LOADED = Symbol.for('dream:dependentDestroyTreeLoaded');
|
|
1
17
|
/**
|
|
2
18
|
* @internal
|
|
3
19
|
*
|
|
4
20
|
* Destroys all HasOne/HasMany associations on this
|
|
5
21
|
* dream that are marked as `dependent: 'destroy'`.
|
|
6
22
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
23
|
+
* The whole dependent-destroy tree is loaded ONCE, at the root of the destroy
|
|
24
|
+
* operation, by `loadDependentDestroyTree`. Descendants reached by recursing
|
|
25
|
+
* through this function consume those already-hydrated associations rather
|
|
26
|
+
* than re-querying their own subtrees.
|
|
27
|
+
*
|
|
28
|
+
* A load still happens here when an association is not hydrated. That covers
|
|
29
|
+
* two cases:
|
|
30
|
+
*
|
|
31
|
+
* 1. the root of the operation, whose associations have not been loaded yet
|
|
32
|
+
* 2. a node past `RECURSIVE_DESTROY_PRELOAD_MAX_REPEATS` levels of
|
|
33
|
+
* self-reference, which the root's preload could not reach
|
|
34
|
+
*
|
|
35
|
+
* The second case is why the check is per-association-hydration rather than a
|
|
36
|
+
* "descendants are already loaded" flag: such a flag would truncate cascades
|
|
37
|
+
* deeper than the preload horizon.
|
|
38
|
+
*
|
|
39
|
+
* NOTE: reusing a hydrated subtree is only sound because the same `options`
|
|
40
|
+
* are forwarded unchanged all the way down the cascade (see the recursive
|
|
41
|
+
* `destroy`/`reallyDestroy` calls below), so every node's associations were
|
|
42
|
+
* loaded under the scopes this node would have used. Nothing in the types
|
|
43
|
+
* enforces that; if `options` ever start varying by depth, this reuse has to
|
|
44
|
+
* be revisited.
|
|
10
45
|
*/
|
|
11
46
|
export default async function destroyAssociatedRecords(dream, txn, options) {
|
|
12
47
|
const dreamClass = dream.constructor;
|
|
13
48
|
const { reallyDestroy } = options;
|
|
49
|
+
// `loadDependentDestroyTree` hydrates and returns a CLONE rather than
|
|
50
|
+
// mutating the receiver, so the result must be assigned back — otherwise
|
|
51
|
+
// each association in the loop below would trigger its own reload.
|
|
52
|
+
let dreamWithAssociations = dream;
|
|
53
|
+
let alreadyLoadedTree = false;
|
|
14
54
|
for (const associationName of dreamClass['dependentDestroyAssociationNames']()) {
|
|
15
|
-
|
|
55
|
+
if (!alreadyLoadedTree && !cascadeLoaded(dreamWithAssociations, associationName)) {
|
|
56
|
+
dreamWithAssociations = await loadDependentDestroyTree(dreamWithAssociations, txn, {
|
|
57
|
+
reallyDestroy,
|
|
58
|
+
bypassAllDefaultScopes: options.bypassAllDefaultScopes ?? false,
|
|
59
|
+
defaultScopesToBypass: options.defaultScopesToBypass ?? [],
|
|
60
|
+
});
|
|
61
|
+
alreadyLoadedTree = true;
|
|
62
|
+
}
|
|
63
|
+
const loaded = dreamWithAssociations[associationName];
|
|
16
64
|
const records = Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
|
|
17
65
|
for (const record of records) {
|
|
66
|
+
markCascadeLoaded(record);
|
|
18
67
|
if (reallyDestroy) {
|
|
19
68
|
await record.txn(txn).reallyDestroy(options);
|
|
20
69
|
}
|
|
@@ -24,3 +73,26 @@ export default async function destroyAssociatedRecords(dream, txn, options) {
|
|
|
24
73
|
}
|
|
25
74
|
}
|
|
26
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* @internal
|
|
78
|
+
*
|
|
79
|
+
* True when this dream was hydrated by this cascade's own preload AND the
|
|
80
|
+
* given association is hydrated on it.
|
|
81
|
+
*/
|
|
82
|
+
function cascadeLoaded(dream, associationName) {
|
|
83
|
+
if (!dream[CASCADE_LOADED])
|
|
84
|
+
return false;
|
|
85
|
+
return dream.loaded(associationName);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @internal
|
|
89
|
+
*
|
|
90
|
+
* A plain assignment rather than `Object.defineProperty`: the key is a symbol,
|
|
91
|
+
* so it is already excluded from `Object.keys`, `for...in` and
|
|
92
|
+
* `JSON.stringify` without an explicit descriptor, and this runs once per node
|
|
93
|
+
* of a potentially wide cascade tree.
|
|
94
|
+
*/
|
|
95
|
+
function markCascadeLoaded(dream) {
|
|
96
|
+
;
|
|
97
|
+
dream[CASCADE_LOADED] = true;
|
|
98
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import DreamApp from '../../dream-app/index.js';
|
|
2
2
|
import destroyAssociatedRecords from './destroyAssociatedRecords.js';
|
|
3
|
-
import loadDependentDestroyTree from './loadDependentDestroyTree.js';
|
|
4
3
|
import runHooksFor from './runHooksFor.js';
|
|
5
4
|
/**
|
|
6
5
|
* @internal
|
|
@@ -31,12 +30,12 @@ export default async function destroyDream(dream, txn = null, options) {
|
|
|
31
30
|
async function destroyDreamWithTransaction(dream, txn, options) {
|
|
32
31
|
const { cascade, reallyDestroy, skipHooks } = options;
|
|
33
32
|
if (cascade) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
await destroyAssociatedRecords(
|
|
33
|
+
// NOTE: the dependent-destroy tree is loaded lazily, inside
|
|
34
|
+
// destroyAssociatedRecords, so that a cascaded descendant whose
|
|
35
|
+
// associations the root already preloaded does not re-query its own
|
|
36
|
+
// subtree. `dream` itself is intentionally never replaced by the loaded
|
|
37
|
+
// clone: the hooks and the delete below operate on the original.
|
|
38
|
+
await destroyAssociatedRecords(dream, txn, options);
|
|
40
39
|
}
|
|
41
40
|
if (!skipHooks) {
|
|
42
41
|
await runHooksFor('beforeDestroy', dream, true, null, txn);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import resolveSerializerAssociationEdges from './resolveSerializerAssociationEdges.js';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* Resolves the association edges of *every* serializer that can render `dreamClass` at this point
|
|
6
|
+
* in a serialization walk and merges them into one set, so that a walk rooted at an STI base sees
|
|
7
|
+
* the union across its children's serializers rather than one child's view of the class.
|
|
8
|
+
*
|
|
9
|
+
* The merge is keyed on (association, target class), so:
|
|
10
|
+
*
|
|
11
|
+
* - an association several children render identically (all three `stiUnion` serializers render
|
|
12
|
+
* `sandbags`) collapses to a single edge with a single target, rather than repeating per child;
|
|
13
|
+
* - a polymorphic association keeps one target per target class
|
|
14
|
+
* (`Polymorphic/Task#taskable` → `Chore` and `Workout`), which is a real distinction, not a
|
|
15
|
+
* duplicate;
|
|
16
|
+
* - an association only some children render (`heartRatings`, rendered only by Latex's `stiUnion`
|
|
17
|
+
* serializer) is present exactly once.
|
|
18
|
+
*
|
|
19
|
+
* Grouping the serializers *under* the target rather than emitting one target per serializer is
|
|
20
|
+
* what lets a caller recurse once per target class with the full set, so nested trees from
|
|
21
|
+
* different children of the same target merge instead of clobbering one another.
|
|
22
|
+
*
|
|
23
|
+
* Insertion-ordered throughout, so edges come back in the order the serializers declare them.
|
|
24
|
+
*
|
|
25
|
+
* This is the single union both serializer walks run on: `buildSerializerPreloadPaths` turns these
|
|
26
|
+
* edges into preload paths (taking `terminates` and the target classes, ignoring the per-serializer
|
|
27
|
+
* edge type and serializer-side name), and `Dream.recursiveSerializationMap` prints them (taking the
|
|
28
|
+
* per-serializer edge type and name, ignoring `terminates`). Keeping the two walks on one merge is the point: the bug
|
|
29
|
+
* this exists to prevent is `psy i:serialization` and `preloadFor` disagreeing about which
|
|
30
|
+
* associations a serializer key reaches, which a second hand-maintained merge invites back.
|
|
31
|
+
*/
|
|
32
|
+
export default function mergeSerializerAssociationEdges(dreamClass, serializers) {
|
|
33
|
+
/**
|
|
34
|
+
* Each target carries a Set mirroring its `serializers`, so the identity dedupe below is a lookup
|
|
35
|
+
* rather than a scan of the array. Dropped when the merged edges are built at the end.
|
|
36
|
+
*/
|
|
37
|
+
const mergedEdges = new Map();
|
|
38
|
+
for (const serializer of serializers) {
|
|
39
|
+
for (const edge of resolveSerializerAssociationEdges(dreamClass, serializer)) {
|
|
40
|
+
let mergedEdge = mergedEdges.get(edge.associationAs);
|
|
41
|
+
if (mergedEdge === undefined) {
|
|
42
|
+
mergedEdge = { terminates: false, targets: new Map() };
|
|
43
|
+
mergedEdges.set(edge.associationAs, mergedEdge);
|
|
44
|
+
}
|
|
45
|
+
if (edge.targets.length === 0) {
|
|
46
|
+
mergedEdge.terminates = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const target of edge.targets) {
|
|
50
|
+
const targetKey = target.dreamClass.globalName;
|
|
51
|
+
const mergedTarget = mergedEdge.targets.get(targetKey);
|
|
52
|
+
const targetSerializer = {
|
|
53
|
+
serializer: target.serializer,
|
|
54
|
+
type: edge.type,
|
|
55
|
+
serializerAssociationName: edge.serializerAssociationName,
|
|
56
|
+
};
|
|
57
|
+
if (mergedTarget === undefined) {
|
|
58
|
+
mergedEdge.targets.set(targetKey, {
|
|
59
|
+
target: { dreamClass: target.dreamClass, serializers: [targetSerializer] },
|
|
60
|
+
serializerSet: new Set([target.serializer]),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else if (!mergedTarget.serializerSet.has(target.serializer)) {
|
|
64
|
+
mergedTarget.serializerSet.add(target.serializer);
|
|
65
|
+
mergedTarget.target.serializers.push(targetSerializer);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...mergedEdges.entries()].map(([associationAs, { terminates, targets }]) => ({
|
|
71
|
+
associationAs,
|
|
72
|
+
terminates,
|
|
73
|
+
targets: [...targets.values()].map(({ target }) => target),
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
@@ -1,16 +1,58 @@
|
|
|
1
1
|
import yoctocolors from 'yoctocolors';
|
|
2
2
|
import { indent } from '../../helpers/indent.js';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*
|
|
6
|
+
* Prints one level of `psy i:serialization`'s tree: the heading for an association, then the name
|
|
7
|
+
* of every serializer that renders the target of that association at this point in the walk. That
|
|
8
|
+
* is more than one serializer when the target is an STI base, since each child may register a
|
|
9
|
+
* different serializer under the same key.
|
|
10
|
+
*
|
|
11
|
+
* Those serializers are printed under a *single* heading rather than one heading each, because the
|
|
12
|
+
* caller prints the union of their association trees below, indented one level in from the
|
|
13
|
+
* heading. Repeating the heading per serializer would leave that union sitting directly beneath
|
|
14
|
+
* whichever child's serializer happened to print last, reading as though every sibling's
|
|
15
|
+
* associations belonged to that one child. Grouped, the shape matches what `displaySerialization`
|
|
16
|
+
* prints at the root of the tree — one name, then every serializer that can render it, then one
|
|
17
|
+
* merged tree.
|
|
18
|
+
*
|
|
19
|
+
* A target reached under more than one edge type or serializer-side name (two children of an STI
|
|
20
|
+
* base rendering the same class through `rendersOne` and `rendersMany`, say) gets one heading per
|
|
21
|
+
* distinct (type, name), since those are genuinely different lines rather than repetitions of one.
|
|
22
|
+
*/
|
|
23
|
+
export default function printSerializerHierarchyLevel({ targetSerializers, forDisplayDepth, }) {
|
|
4
24
|
const hierarchyLine = '└───';
|
|
5
25
|
const indentation = indent((hierarchyLine.length + 1) * forDisplayDepth, {
|
|
6
26
|
tabWidth: 1,
|
|
7
27
|
});
|
|
8
28
|
const prefix = `${hierarchyLine} `;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
29
|
+
groupByHeading(targetSerializers).forEach(({ type, serializerAssociationName, serializers }) => {
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
console.log(indentation + `${prefix}${type} ${yoctocolors.cyan(serializerAssociationName)}`);
|
|
32
|
+
serializers.forEach(serializer => {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.log(yoctocolors.gray(indentation +
|
|
35
|
+
indent(prefix.length, { tabWidth: 1 }) +
|
|
36
|
+
serializer.globalName));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Collects the serializers rendering a single merged target into one entry per heading they print
|
|
42
|
+
* under — (edge type, serializer-side association name) — preserving the order the serializers
|
|
43
|
+
* were merged in.
|
|
44
|
+
*/
|
|
45
|
+
function groupByHeading(targetSerializers) {
|
|
46
|
+
const headings = new Map();
|
|
47
|
+
for (const { serializer, type, serializerAssociationName } of targetSerializers) {
|
|
48
|
+
const headingKey = `${type}:${serializerAssociationName}`;
|
|
49
|
+
const heading = headings.get(headingKey);
|
|
50
|
+
if (heading === undefined) {
|
|
51
|
+
headings.set(headingKey, { type, serializerAssociationName, serializers: [serializer] });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
heading.serializers.push(serializer);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return [...headings.values()];
|
|
16
58
|
}
|
|
@@ -1,19 +1,78 @@
|
|
|
1
|
-
import MissingSerializersDefinition from '../../errors/serializers/MissingSerializersDefinition.js';
|
|
2
1
|
import compact from '../../helpers/compact.js';
|
|
3
2
|
import { inferSerializersFromDreamClassOrViewModelClass } from '../../serializer/helpers/inferSerializerFromDreamOrViewModel.js';
|
|
3
|
+
/**
|
|
4
|
+
* Keyed on the two arguments. Weak on both, so an entry is collectable once either the Dream class
|
|
5
|
+
* or the serializer is. The cached arrays are handed out by reference and must be treated as
|
|
6
|
+
* read-only by callers.
|
|
7
|
+
*/
|
|
8
|
+
let resolvedEdgesCache = new WeakMap();
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
*
|
|
12
|
+
* Drops every memoized edge set. Only needed when a class object that stays in the process has its
|
|
13
|
+
* `serializers` getter replaced, which the memo cannot detect — specs that install or remove one
|
|
14
|
+
* around an example. Replacing the model classes themselves needs no call, since the memo keys on
|
|
15
|
+
* the class and serializer objects and entries resolved against the previous ones become
|
|
16
|
+
* unreachable.
|
|
17
|
+
*/
|
|
18
|
+
export function clearResolvedSerializerAssociationEdgesCache() {
|
|
19
|
+
resolvedEdgesCache = new WeakMap();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @internal
|
|
23
|
+
*
|
|
24
|
+
* Resolves the rendersOne/rendersMany/delegatedAttribute attributes of `serializer` to the
|
|
25
|
+
* associations of `dreamClass` they preload, along with the classes and serializers each one
|
|
26
|
+
* targets.
|
|
27
|
+
*
|
|
28
|
+
* Memoized on its two arguments, because every call rebuilds a throwaway serializer builder and
|
|
29
|
+
* re-infers the serializers of every association target, and the traversal in
|
|
30
|
+
* buildSerializerPreloadPaths revisits the same (class, serializer) pair many times.
|
|
31
|
+
*
|
|
32
|
+
* The two arguments are not the function's only inputs, though: resolving a target's serializers
|
|
33
|
+
* also reads that target's `serializers` getter, the STI children registered on it, and the
|
|
34
|
+
* DreamApp serializer registry. Loading a fresh set of classes replaces the keys along with that
|
|
35
|
+
* state, so the memo follows it. Mutating a `serializers` getter in place does not, and needs
|
|
36
|
+
* `clearResolvedSerializerAssociationEdgesCache` — see there.
|
|
37
|
+
*/
|
|
4
38
|
export default function resolveSerializerAssociationEdges(dreamClass, serializer) {
|
|
39
|
+
let cachedForDreamClass = resolvedEdgesCache.get(dreamClass);
|
|
40
|
+
if (cachedForDreamClass === undefined) {
|
|
41
|
+
cachedForDreamClass = new WeakMap();
|
|
42
|
+
resolvedEdgesCache.set(dreamClass, cachedForDreamClass);
|
|
43
|
+
}
|
|
44
|
+
const cached = cachedForDreamClass.get(serializer);
|
|
45
|
+
if (cached !== undefined)
|
|
46
|
+
return cached;
|
|
47
|
+
const edges = buildSerializerAssociationEdges(dreamClass, serializer);
|
|
48
|
+
cachedForDreamClass.set(serializer, edges);
|
|
49
|
+
return edges;
|
|
50
|
+
}
|
|
51
|
+
function buildSerializerAssociationEdges(dreamClass, serializer) {
|
|
5
52
|
const serializerBuilder = serializer(undefined, undefined);
|
|
6
53
|
const serializerAssociations = serializerBuilder['attributes'].filter(attribute => ['rendersOne', 'rendersMany', 'delegatedAttribute'].includes(attribute.type));
|
|
7
54
|
return compact(serializerAssociations.map(serializerAssociation => {
|
|
8
55
|
const serializerAssociationName = serializerAssociation.targetName ??
|
|
9
56
|
serializerAssociation.name;
|
|
57
|
+
// `dreamClass` is the class the walk is rooted at, which for an STI base is the base itself
|
|
58
|
+
// even while `serializer` is one child's serializer. That pairing is safe because an STI child
|
|
59
|
+
// cannot declare associations of its own — the `STI` class decorator throws
|
|
60
|
+
// StiChildCannotDefineNewAssociations — so every association a child's serializer can render
|
|
61
|
+
// is declared on the base and resolves here.
|
|
62
|
+
//
|
|
63
|
+
// The one hole: that guard reads decorator metadata and
|
|
64
|
+
// `associationDeclarationNamesFromMetadata` returns [] when the metadata is absent, which is
|
|
65
|
+
// also what happens when the `STI` decorator runs without a `context`. Under a TS config that
|
|
66
|
+
// does not emit decorator metadata the guard silently passes, a child-declared association
|
|
67
|
+
// survives, and it is dropped *here*, silently, by the return below — the symptom is then a
|
|
68
|
+
// missing preload and a NonLoadedAssociation at render, not a decorator error naming the real
|
|
69
|
+
// problem.
|
|
10
70
|
const association = dreamClass['getAssociationMetadata'](serializerAssociationName);
|
|
11
71
|
if (!association)
|
|
12
72
|
return null;
|
|
13
73
|
if (serializerAssociation.type === 'delegatedAttribute') {
|
|
14
74
|
return {
|
|
15
75
|
associationAs: association.as,
|
|
16
|
-
sourceDreamClass: dreamClass,
|
|
17
76
|
type: serializerAssociation.type,
|
|
18
77
|
serializerAssociationName,
|
|
19
78
|
targets: [],
|
|
@@ -26,17 +85,24 @@ export default function resolveSerializerAssociationEdges(dreamClass, serializer
|
|
|
26
85
|
? maybeAssociatedClasses
|
|
27
86
|
: [maybeAssociatedClasses];
|
|
28
87
|
const targets = associatedClasses.flatMap(associatedClass => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
88
|
+
// Deliberately unguarded: every serializer that cannot be resolved while building preload
|
|
89
|
+
// paths throws, and there is no tolerance here by design.
|
|
90
|
+
//
|
|
91
|
+
// Why: an unresolvable serializer is a configuration error, and the first query that walks
|
|
92
|
+
// the serializer graph is where it should be found — no rows, no rendering required.
|
|
93
|
+
// Swallowing it means the only way to discover a broken `serializerKey` (or a missing
|
|
94
|
+
// `serializers` getter) is a spec that creates a row of the affected class *and* renders it.
|
|
95
|
+
//
|
|
96
|
+
// Why the obvious tolerance is also wrong, not merely undesirable:
|
|
97
|
+
// `inferSerializersFromDreamClassOrViewModelClass` below expands STI and throws on the
|
|
98
|
+
// *first* child that fails, so catching here would drop the serializers of *every* child of
|
|
99
|
+
// this target. An edge with no targets is treated as terminal by buildSerializerPreloadPaths,
|
|
100
|
+
// so the association is preloaded but never descended into, and a row of a *well-formed*
|
|
101
|
+
// sibling fails at render with NonLoadedAssociation — an error naming a class that has
|
|
102
|
+
// nothing wrong with it.
|
|
103
|
+
const serializers = serializerAssociation.options.serializer
|
|
104
|
+
? compact([serializerAssociation.options.serializer])
|
|
105
|
+
: compact(inferSerializersFromDreamClassOrViewModelClass(associatedClass, serializerAssociation.options.serializerKey));
|
|
40
106
|
return serializers.map(associatedSerializer => ({
|
|
41
107
|
dreamClass: associatedClass,
|
|
42
108
|
serializer: associatedSerializer,
|
|
@@ -44,7 +110,6 @@ export default function resolveSerializerAssociationEdges(dreamClass, serializer
|
|
|
44
110
|
});
|
|
45
111
|
return {
|
|
46
112
|
associationAs: association.as,
|
|
47
|
-
sourceDreamClass: dreamClass,
|
|
48
113
|
type: serializerAssociation.type,
|
|
49
114
|
serializerAssociationName,
|
|
50
115
|
targets,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default class InvalidBatchSize extends Error {
|
|
2
|
+
batchSize;
|
|
3
|
+
constructor(batchSize) {
|
|
4
|
+
super();
|
|
5
|
+
this.batchSize = batchSize;
|
|
6
|
+
}
|
|
7
|
+
get message() {
|
|
8
|
+
return `
|
|
9
|
+
Invalid batchSize provided.
|
|
10
|
+
|
|
11
|
+
batchSize given:
|
|
12
|
+
${String(this.batchSize)}
|
|
13
|
+
|
|
14
|
+
batchSize must be a positive integer. A batchSize of 0 is especially dangerous,
|
|
15
|
+
since a limit of 0 is treated as "no limit", which would collapse a batched
|
|
16
|
+
operation into a single unbounded pass over the entire matched set.
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default class RowLockIncompatibleWithDistinct extends Error {
|
|
2
|
+
get message() {
|
|
3
|
+
// `SELECT DISTINCT ON (...) ... FOR UPDATE` is rejected by the database
|
|
4
|
+
// ("FOR UPDATE is not allowed with DISTINCT clause"), so fail here, with a
|
|
5
|
+
// message that names the two incompatible parts, rather than surfacing a
|
|
6
|
+
// raw driver error from inside a per-batch transaction.
|
|
7
|
+
return `
|
|
8
|
+
An exclusive row lock was requested on a query that also calls \`distinct\`.
|
|
9
|
+
|
|
10
|
+
A locked read cannot be combined with DISTINCT. Drop the \`distinct\` call, or
|
|
11
|
+
drop the lock.
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default class RowLockIncompatibleWithJoinLoad extends Error {
|
|
2
|
+
get message() {
|
|
3
|
+
// A join load builds its own select statement (one row per joined
|
|
4
|
+
// association row, later collapsed in memory), which cannot carry the
|
|
5
|
+
// Query's row lock. Rather than silently performing an unlocked read and
|
|
6
|
+
// dropping the compare-and-set guarantee the caller asked for, refuse.
|
|
7
|
+
return `
|
|
8
|
+
An exclusive row lock was requested on a query that also uses leftJoinPreload.
|
|
9
|
+
|
|
10
|
+
A join-loaded query cannot be locked. Use \`preload\` instead of
|
|
11
|
+
\`leftJoinPreload\`, or drop the lock.
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export default class ArrayEqualityRequiresArrayColumn extends Error {
|
|
2
|
+
dreamClass;
|
|
3
|
+
column;
|
|
4
|
+
operator;
|
|
5
|
+
constructor(dreamClass, column, operator) {
|
|
6
|
+
super();
|
|
7
|
+
this.dreamClass = dreamClass;
|
|
8
|
+
this.column = column;
|
|
9
|
+
this.operator = operator;
|
|
10
|
+
}
|
|
11
|
+
get message() {
|
|
12
|
+
return `
|
|
13
|
+
Attempting to compare an array for equality against a column that is not an
|
|
14
|
+
array in the database:
|
|
15
|
+
|
|
16
|
+
${this.dreamClass.sanitizedName}#${this.column} (operator: ${this.operator})
|
|
17
|
+
|
|
18
|
+
Whole-array equality (\`=\`, \`!=\`, \`<>\`) is only meaningful for array columns.
|
|
19
|
+
If you meant "is this column one of these values", use \`ops.in([...])\`
|
|
20
|
+
(or a bare array) instead.
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
}
|