@rvoh/dream 2.22.0 → 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 +68 -23
- package/dist/cjs/src/dream/Query.js +39 -8
- 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/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/serializers/NoSerializersResolvedForKey.js +30 -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 +68 -23
- package/dist/esm/src/dream/Query.js +39 -8
- 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/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/serializers/NoSerializersResolvedForKey.js +30 -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 +9 -0
- package/dist/types/src/dream/Query.d.ts +10 -0
- 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/serializers/NoSerializersResolvedForKey.d.ts +18 -0
- package/dist/types/src/serializer/helpers/inferSerializerFromDreamOrViewModel.d.ts +16 -0
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +40 -40
- package/docs/classes/db.PostgresQueryDriver.html +41 -41
- package/docs/classes/db.QueryDriverBase.html +39 -39
- package/docs/classes/errors.CheckConstraintViolation.html +3 -3
- package/docs/classes/errors.ColumnOverflow.html +3 -3
- package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
- package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
- package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
- package/docs/classes/errors.DecryptionError.html +2 -2
- package/docs/classes/errors.DecryptionParseError.html +2 -2
- package/docs/classes/errors.DecryptionRotationError.html +3 -3
- package/docs/classes/errors.GlobalNameNotSet.html +3 -3
- package/docs/classes/errors.InvalidCalendarDate.html +2 -2
- package/docs/classes/errors.InvalidClockTime.html +2 -2
- package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
- package/docs/classes/errors.InvalidDateTime.html +2 -2
- package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
- package/docs/classes/errors.NonLoadedAssociation.html +3 -3
- package/docs/classes/errors.NotNullViolation.html +3 -3
- package/docs/classes/errors.RecordNotFound.html +3 -3
- package/docs/classes/errors.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 +124 -124
- 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 +62 -62
- 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/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 +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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised when resolving a serializer key against a class yields an empty set of serializers.
|
|
3
|
+
*
|
|
4
|
+
* This is an internal-invariant failure, not a misconfiguration: an unresolvable serializer key
|
|
5
|
+
* raises `MissingSerializersDefinitionForKey` (and a class with no `serializers` getter at all
|
|
6
|
+
* raises `MissingSerializersDefinition`) while it is being resolved, and STI expansion always
|
|
7
|
+
* yields at least one class, so there is no supported application shape that reaches this. It is
|
|
8
|
+
* typed rather than a bare `Error` so that it can be asserted on, and so that the two callers that
|
|
9
|
+
* guard the invariant raise the same thing.
|
|
10
|
+
*/
|
|
11
|
+
export default class NoSerializersResolvedForKey extends Error {
|
|
12
|
+
classDef;
|
|
13
|
+
serializerKey;
|
|
14
|
+
constructor(classDef, serializerKey) {
|
|
15
|
+
super();
|
|
16
|
+
this.classDef = classDef;
|
|
17
|
+
this.serializerKey = serializerKey;
|
|
18
|
+
}
|
|
19
|
+
get message() {
|
|
20
|
+
const className = this.classDef === null || this.classDef === undefined
|
|
21
|
+
? String(this.classDef)
|
|
22
|
+
: (this.classDef.globalName ?? this.classDef.name);
|
|
23
|
+
return `
|
|
24
|
+
No serializers resolved for \`${className}\` under serializer key \`${this.serializerKey}\`
|
|
25
|
+
|
|
26
|
+
This should be impossible — a serializer key that cannot be resolved throws while it is being
|
|
27
|
+
resolved, before reaching here — and indicates a bug in Dream rather than a problem with your
|
|
28
|
+
application. Please report it.`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -3,6 +3,7 @@ import MissingSerializersDefinition from '../../errors/serializers/MissingSerial
|
|
|
3
3
|
import MissingSerializersDefinitionForKey from '../../errors/serializers/MissingSerializersDefinitionForKey.js';
|
|
4
4
|
import NoGlobalSerializerForSpecifiedKey from '../../errors/serializers/NoGlobalSerializerForSpecifiedKey.js';
|
|
5
5
|
import NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey from '../../errors/serializers/NonDreamSerializerDerivedFromGlobalSerializerForSpecifiedKey.js';
|
|
6
|
+
import NoSerializersResolvedForKey from '../../errors/serializers/NoSerializersResolvedForKey.js';
|
|
6
7
|
import compact from '../../helpers/compact.js';
|
|
7
8
|
import expandStiClasses from '../../helpers/sti/expandStiClasses.js';
|
|
8
9
|
import uniq from '../../helpers/uniq.js';
|
|
@@ -35,3 +36,24 @@ export function inferSerializersFromDreamClassOrViewModelClass(classDef, seriali
|
|
|
35
36
|
const serializers = classes.map(classDef => inferSerializerFromDreamOrViewModel(classDef.prototype, serializerKey));
|
|
36
37
|
return uniq(compact(serializers));
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* As `inferSerializersFromDreamClassOrViewModelClass`, but guarantees a non-empty result.
|
|
41
|
+
*
|
|
42
|
+
* The guard is unreachable today, and deliberately not phrased as "no serializer found for key": a
|
|
43
|
+
* serializer key that a class does not register throws `MissingSerializersDefinitionForKey` (and a
|
|
44
|
+
* class with no `serializers` getter at all throws `MissingSerializersDefinition`) from inside the
|
|
45
|
+
* inference above, and `expandStiClasses` always yields at least one class. It exists only so that
|
|
46
|
+
* a future change to any of those cannot turn an unresolvable serializer key into a silently empty
|
|
47
|
+
* serializer set — which, for the callers below, means a silently empty preload set and a
|
|
48
|
+
* `NonLoadedAssociation` at render rather than an error naming the real problem.
|
|
49
|
+
*
|
|
50
|
+
* Used by everything that walks a serializer graph rooted on a class rather than an instance —
|
|
51
|
+
* preload-path building and serialization display — so that they raise one typed error between
|
|
52
|
+
* them.
|
|
53
|
+
*/
|
|
54
|
+
export function inferSerializersFromDreamClassOrViewModelClassOrFail(classDef, serializerKey) {
|
|
55
|
+
const serializers = inferSerializersFromDreamClassOrViewModelClass(classDef, serializerKey);
|
|
56
|
+
if (serializers.length === 0)
|
|
57
|
+
throw new NoSerializersResolvedForKey(classDef, serializerKey);
|
|
58
|
+
return serializers;
|
|
59
|
+
}
|
|
@@ -15,5 +15,18 @@ export function serializerForAssociatedClass(object, associationName, options) {
|
|
|
15
15
|
const associatedClass = association.modelCB();
|
|
16
16
|
if (Array.isArray(associatedClass))
|
|
17
17
|
throw new Error('rendersOne flatten is incompatible with a polymorphic belongs-to association');
|
|
18
|
+
// Taking the first is only a narrowing when `associatedClass` is an STI base, where
|
|
19
|
+
// inferSerializersFromDreamClassOrViewModelClass returns one serializer per child sorted by
|
|
20
|
+
// sanitizedName. It is correct here because there is no associated row: this function is reached
|
|
21
|
+
// only from SerializerRenderer's `flatten: true` branch for a *null* associated object, so no
|
|
22
|
+
// child of the base is the right one — the row that would have decided is absent. (rendersMany
|
|
23
|
+
// never reaches here and does not accept `flatten`.)
|
|
24
|
+
//
|
|
25
|
+
// It is observable, though, so it is not "nothing happens": the picked serializer is invoked and
|
|
26
|
+
// its output spread into the parent's payload, so the parent JSON carries the
|
|
27
|
+
// alphabetically-first child's flattened keys as nulls, and omits keys only a later-sorting child
|
|
28
|
+
// would flatten. Unioning every child's serializer here — which is what buildSerializerPreloadPaths
|
|
29
|
+
// does for preload paths — is meaningless: exactly one set of keys can be flattened into the
|
|
30
|
+
// payload.
|
|
18
31
|
return inferSerializersFromDreamClassOrViewModelClass(associatedClass, options.serializerKey)[0] ?? null;
|
|
19
32
|
}
|
package/dist/esm/src/Dream.js
CHANGED
|
@@ -19,9 +19,9 @@ import destroyDream from './dream/internal/destroyDream.js';
|
|
|
19
19
|
import { destroyOptions, reallyDestroyOptions, undestroyOptions, } from './dream/internal/destroyOptions.js';
|
|
20
20
|
import ensureSTITypeFieldIsSet from './dream/internal/ensureSTITypeFieldIsSet.js';
|
|
21
21
|
import findOrCreateBy from './dream/internal/findOrCreateBy.js';
|
|
22
|
+
import mergeSerializerAssociationEdges from './dream/internal/mergeSerializerAssociationEdges.js';
|
|
22
23
|
import printSerializerHierarchyLevel from './dream/internal/printSerializerHierarchyLevel.js';
|
|
23
24
|
import reload from './dream/internal/reload.js';
|
|
24
|
-
import resolveSerializerAssociationEdges from './dream/internal/resolveSerializerAssociationEdges.js';
|
|
25
25
|
import runValidations from './dream/internal/runValidations.js';
|
|
26
26
|
import saveDream from './dream/internal/saveDream.js';
|
|
27
27
|
import { DEFAULT_BYPASS_ALL_DEFAULT_SCOPES, DEFAULT_DEFAULT_SCOPES_TO_BYPASS, DEFAULT_SKIP_HOOKS, } from './dream/internal/scopeHelpers.js';
|
|
@@ -48,7 +48,7 @@ import areEqual from './helpers/areEqual.js';
|
|
|
48
48
|
import cloneDeepSafe from './helpers/cloneDeepSafe.js';
|
|
49
49
|
import isJsonColumn from './helpers/db/types/isJsonColumn.js';
|
|
50
50
|
import notEqual from './helpers/notEqual.js';
|
|
51
|
-
import {
|
|
51
|
+
import { inferSerializersFromDreamClassOrViewModelClassOrFail } from './serializer/helpers/inferSerializerFromDreamOrViewModel.js';
|
|
52
52
|
export default class Dream {
|
|
53
53
|
DB;
|
|
54
54
|
/**
|
|
@@ -542,44 +542,89 @@ export default class Dream {
|
|
|
542
542
|
}
|
|
543
543
|
static serializationMap(serializerKey) {
|
|
544
544
|
const key = serializerKey || 'default';
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
545
|
+
// Deliberately one serializer: this maps a single serializer's view of the class, and taking
|
|
546
|
+
// the first is only a narrowing when `this` is an STI base (where
|
|
547
|
+
// inferSerializersFromDreamClassOrViewModelClass returns one serializer per child, sorted by
|
|
548
|
+
// sanitizedName). That narrowing is deliberate and differs from `buildSerializerPreloadPaths`
|
|
549
|
+
// and `displaySerialization` below, both of which union every child's serializer because they
|
|
550
|
+
// answer "what does a query load / display for this class", not "what does one serializer
|
|
551
|
+
// render". Nothing in src/ calls
|
|
552
|
+
// this method; it exists as the single-serializer entry point onto recursiveSerializationMap and
|
|
553
|
+
// is exercised by spec/unit/dream/serializationMap.spec.ts, which pins the narrowing on an STI
|
|
554
|
+
// base so that the divergence from displaySerialization is executable rather than only
|
|
555
|
+
// described here. A caller that wants the union for an STI base wants displaySerialization's
|
|
556
|
+
// shape, not this one.
|
|
557
|
+
//
|
|
558
|
+
// Note that the narrowing is to one serializer of *this* class only: every association this
|
|
559
|
+
// serializer renders is still walked with the full set of serializers its target resolves to,
|
|
560
|
+
// so an STI base reached as a target below is unioned.
|
|
561
|
+
const serializer = inferSerializersFromDreamClassOrViewModelClassOrFail(this, key)[0];
|
|
562
|
+
return this.recursiveSerializationMap([serializer]);
|
|
549
563
|
}
|
|
550
564
|
static displaySerialization(serializerKey) {
|
|
551
565
|
const key = serializerKey || 'default';
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
566
|
+
/**
|
|
567
|
+
* When `this` is an STI base, this is one serializer per STI child (deduped), since each child
|
|
568
|
+
* may register a different serializer under `key`. Every one is walked, because this method
|
|
569
|
+
* backs `psy i:serialization`, whose purpose is verifying what gets preloaded — and
|
|
570
|
+
* `preloadFor` unions the preload paths of every child's serializer. Walking only one child's
|
|
571
|
+
* serializer would print a strict subset of what the query actually loads.
|
|
572
|
+
*/
|
|
573
|
+
// ...OrFail, rather than the plain inference, so that an empty serializer set raises
|
|
574
|
+
// NoSerializersResolvedForKey instead of silently printing a class name with nothing under it.
|
|
575
|
+
// That guard is unreachable today and is documented where it lives; it is shared with
|
|
576
|
+
// buildSerializerPreloadPaths so that the display and the preload walk raise the same thing.
|
|
577
|
+
const serializers = inferSerializersFromDreamClassOrViewModelClassOrFail(this, key);
|
|
555
578
|
// eslint-disable-next-line no-console
|
|
556
579
|
console.log(yoctocolors.cyan(this.sanitizedName));
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
|
|
580
|
+
// One line per serializer: for an STI base the displayed tree is the union across its children,
|
|
581
|
+
// so naming a single child's serializer under the base's own name would misattribute the tree.
|
|
582
|
+
serializers.forEach(serializer => {
|
|
583
|
+
// eslint-disable-next-line no-console
|
|
584
|
+
console.log(yoctocolors.gray(serializer.globalName));
|
|
585
|
+
});
|
|
586
|
+
return this.recursiveSerializationMap(serializers, {
|
|
560
587
|
forDisplay: true,
|
|
561
588
|
});
|
|
562
589
|
}
|
|
563
|
-
|
|
590
|
+
/**
|
|
591
|
+
* @internal
|
|
592
|
+
*
|
|
593
|
+
* Maps the association tree reachable from `serializers` — every serializer that can render this
|
|
594
|
+
* class at this point in the walk, which is more than one when this class is an STI base. The
|
|
595
|
+
* serializers are merged into one edge set (see mergeSerializerAssociationEdges) rather than
|
|
596
|
+
* walked one at a time, so an association several of them render is visited once and the nested
|
|
597
|
+
* trees of a shared target merge instead of clobbering each other.
|
|
598
|
+
*/
|
|
599
|
+
static recursiveSerializationMap(serializers, { forDisplay = false, forDisplayDepth = 0, repeatedAssociationTracker = {}, } = {}) {
|
|
564
600
|
const depthTrackerId = this.globalName;
|
|
565
601
|
repeatedAssociationTracker[depthTrackerId] ??= 0;
|
|
566
602
|
if (repeatedAssociationTracker[depthTrackerId] + 1 > RECURSIVE_SERIALIZATION_MAX_REPEATS)
|
|
567
603
|
return {};
|
|
568
604
|
repeatedAssociationTracker[depthTrackerId]++;
|
|
569
|
-
const edges =
|
|
605
|
+
const edges = mergeSerializerAssociationEdges(this, serializers);
|
|
570
606
|
const mappedAssociations = edges.reduce((accumulator, edge) => {
|
|
607
|
+
// An edge with no targets terminates: either a delegatedAttribute (never printed, and
|
|
608
|
+
// carrying `targets: []` by construction) or an association resolving to no serializer. Its
|
|
609
|
+
// nestedSerializerInfo is `{}` because this reduce never runs.
|
|
571
610
|
const innerAssociationSerializerInfo = edge.targets.reduce((innerAccumulator, target) => {
|
|
572
|
-
if (forDisplay
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
611
|
+
if (forDisplay) {
|
|
612
|
+
// One heading per (association, target class), naming every serializer that renders that
|
|
613
|
+
// target underneath it, and then — below all of them — the union of their association
|
|
614
|
+
// trees, which is what `preloadFor` loads through this association.
|
|
615
|
+
//
|
|
616
|
+
// The recursion below is a single merged walk of every serializer of the target, so
|
|
617
|
+
// there is one tree, not one per child. Grouping the serializer names under a single
|
|
618
|
+
// heading is what keeps that honest — repeating the heading per serializer would leave
|
|
619
|
+
// the merged tree indented directly beneath the last one's name and attribute every
|
|
620
|
+
// sibling's associations to it.
|
|
621
|
+
printSerializerHierarchyLevel({ targetSerializers: target.serializers, forDisplayDepth });
|
|
579
622
|
}
|
|
580
623
|
return {
|
|
581
624
|
...innerAccumulator,
|
|
582
|
-
|
|
625
|
+
// recursed once per target class with *all* of that class's serializers, so their nested
|
|
626
|
+
// trees merge rather than the last one winning a shallow spread
|
|
627
|
+
...target.dreamClass['recursiveSerializationMap'](target.serializers.map(({ serializer }) => serializer), {
|
|
583
628
|
forDisplay,
|
|
584
629
|
forDisplayDepth: forDisplayDepth + 1,
|
|
585
630
|
repeatedAssociationTracker,
|
|
@@ -588,7 +633,7 @@ export default class Dream {
|
|
|
588
633
|
}, {});
|
|
589
634
|
accumulator[edge.associationAs] = {
|
|
590
635
|
parentDreamClass: this,
|
|
591
|
-
nestedSerializerInfo:
|
|
636
|
+
nestedSerializerInfo: innerAssociationSerializerInfo,
|
|
592
637
|
};
|
|
593
638
|
return accumulator;
|
|
594
639
|
}, {});
|
|
@@ -22,8 +22,26 @@ import ops from '../ops/index.js';
|
|
|
22
22
|
import buildSerializerPreloadPaths from './internal/buildSerializerPreloadPaths.js';
|
|
23
23
|
import computedPaginatePage from './internal/computedPaginatePage.js';
|
|
24
24
|
import convertDreamClassAndAssociationNameTupleArrayToPreloadArgs from './internal/convertDreamClassAndAssociationNameTupleArrayToPreloadArgs.js';
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Cache for serializer preload-path results, keyed on the root class itself and then the serializer
|
|
27
|
+
* key. Keying on the class rather than its global name is what makes the cache self-invalidating:
|
|
28
|
+
* loading a fresh set of model classes into the process yields new class objects, so entries
|
|
29
|
+
* resolved against the previous ones become unreachable and collectable rather than being served
|
|
30
|
+
* under a name that now means something else.
|
|
31
|
+
*/
|
|
32
|
+
let extractedNestedPathsCache = new WeakMap();
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
*
|
|
36
|
+
* Drops every memoized preload path. Only needed when a class object that stays in the process has
|
|
37
|
+
* its `serializers` getter replaced, which the cache cannot detect — specs that install or remove
|
|
38
|
+
* one around an example. Replacing the model classes themselves needs no call, since the cache
|
|
39
|
+
* keys on the class object. See `clearResolvedSerializerAssociationEdgesCache`, which has the same
|
|
40
|
+
* contract and is cleared alongside it.
|
|
41
|
+
*/
|
|
42
|
+
export function clearSerializerPreloadPathsCache() {
|
|
43
|
+
extractedNestedPathsCache = new WeakMap();
|
|
44
|
+
}
|
|
27
45
|
export default class Query {
|
|
28
46
|
/**
|
|
29
47
|
* @internal
|
|
@@ -560,17 +578,30 @@ export default class Query {
|
|
|
560
578
|
* @returns A Query with all serialization associations preloaded
|
|
561
579
|
*/
|
|
562
580
|
preloadFor(serializerKey, modifierFn) {
|
|
563
|
-
const cacheKey =
|
|
564
|
-
let
|
|
581
|
+
const cacheKey = serializerKey ?? 'default';
|
|
582
|
+
let pathsBySerializerKey = extractedNestedPathsCache.get(this.dreamClass);
|
|
583
|
+
if (!pathsBySerializerKey) {
|
|
584
|
+
pathsBySerializerKey = new Map();
|
|
585
|
+
extractedNestedPathsCache.set(this.dreamClass, pathsBySerializerKey);
|
|
586
|
+
}
|
|
587
|
+
let preloadArgs = pathsBySerializerKey.get(cacheKey);
|
|
565
588
|
if (!preloadArgs) {
|
|
566
589
|
preloadArgs = buildSerializerPreloadPaths(this.dreamClass, serializerKey);
|
|
567
|
-
|
|
590
|
+
pathsBySerializerKey.set(cacheKey, preloadArgs);
|
|
568
591
|
}
|
|
569
|
-
|
|
592
|
+
// Rather than calling `preload` once per path (which deep clones the accumulating preload tree
|
|
593
|
+
// on every call, making this quadratic in the number of paths), clone once, flesh out every
|
|
594
|
+
// path against the same accumulators, and clone the Query once at the end. Iteration order is
|
|
595
|
+
// preserved, so a later path's `and` still wins over an earlier path's for a shared prefix.
|
|
596
|
+
const preloadStatements = cloneDeepSafe(this.preloadStatements);
|
|
597
|
+
const preloadOnStatements = cloneDeepSafe(this.preloadOnStatements);
|
|
570
598
|
preloadArgs.forEach(dreamClassAndAssociationNameTupleArray => {
|
|
571
|
-
|
|
599
|
+
const args = convertDreamClassAndAssociationNameTupleArrayToPreloadArgs(dreamClassAndAssociationNameTupleArray, modifierFn);
|
|
600
|
+
// `modifierFn` returning 'omit' prunes the path from the omitted association down, which can
|
|
601
|
+
// leave nothing to preload; `fleshOutJoinStatements` treats an empty arg list as a no-op.
|
|
602
|
+
this.fleshOutJoinStatements([], preloadStatements, preloadOnStatements, null, args);
|
|
572
603
|
});
|
|
573
|
-
return
|
|
604
|
+
return this.clone({ preloadStatements, preloadOnStatements });
|
|
574
605
|
}
|
|
575
606
|
/**
|
|
576
607
|
* Returns a new Query instance, with the provided
|
|
@@ -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/esm/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
|
}
|
|
@@ -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
|
}
|