@rvoh/dream 2.14.0 → 2.15.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 +3 -11
- package/dist/cjs/src/decorators/field/association/BelongsTo.js +28 -1
- package/dist/cjs/src/decorators/field/association/shared.js +20 -1
- package/dist/cjs/src/dream/LeftJoinLoadBuilder.js +8 -1
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +6 -5
- package/dist/cjs/src/errors/associations/MissingRequiredBelongsToAssociation.js +84 -0
- package/dist/esm/src/Dream.js +3 -11
- package/dist/esm/src/decorators/field/association/BelongsTo.js +28 -1
- package/dist/esm/src/decorators/field/association/shared.js +20 -1
- package/dist/esm/src/dream/LeftJoinLoadBuilder.js +8 -1
- package/dist/esm/src/dream/QueryDriver/Kysely.js +6 -5
- package/dist/esm/src/errors/associations/MissingRequiredBelongsToAssociation.js +84 -0
- package/dist/types/src/dream/LeftJoinLoadBuilder.d.ts +1 -0
- package/dist/types/src/errors/associations/MissingRequiredBelongsToAssociation.d.ts +49 -0
- package/dist/types/src/types/dream.d.ts +11 -1
- package/dist/types/src/types/dream.ts +39 -2
- package/dist/types/src/types/variadic.d.ts +9 -4
- package/dist/types/src/types/variadic.ts +28 -4
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +34 -34
- package/docs/classes/db.PostgresQueryDriver.html +35 -35
- package/docs/classes/db.QueryDriverBase.html +33 -33
- 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 +118 -118
- 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 +57 -57
- 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
package/dist/cjs/src/Dream.js
CHANGED
|
@@ -36,7 +36,6 @@ import CannotDestroyAssociationOnUnpersistedDream from './errors/associations/Ca
|
|
|
36
36
|
import CannotPassNullOrUndefinedToRequiredBelongsTo from './errors/associations/CannotPassNullOrUndefinedToRequiredBelongsTo.js';
|
|
37
37
|
import CannotUpdateAssociationOnUnpersistedDream from './errors/associations/CannotUpdateAssociationOnUnpersistedDream.js';
|
|
38
38
|
import CanOnlyPassBelongsToModelParam from './errors/associations/CanOnlyPassBelongsToModelParam.js';
|
|
39
|
-
import NonLoadedAssociation from './errors/associations/NonLoadedAssociation.js';
|
|
40
39
|
import CannotCallUndestroyOnANonSoftDeleteModel from './errors/CannotCallUndestroyOnANonSoftDeleteModel.js';
|
|
41
40
|
import ConstructorOnlyForInternalUse from './errors/ConstructorOnlyForInternalUse.js';
|
|
42
41
|
import CreateOrFindByFailedToCreateAndFind from './errors/CreateOrFindByFailedToCreateAndFind.js';
|
|
@@ -3349,17 +3348,10 @@ export default class Dream {
|
|
|
3349
3348
|
* @returns A boolean
|
|
3350
3349
|
*/
|
|
3351
3350
|
loaded(associationName) {
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
;
|
|
3355
|
-
this[associationName];
|
|
3356
|
-
return true;
|
|
3357
|
-
}
|
|
3358
|
-
catch (error) {
|
|
3359
|
-
if (error.constructor !== NonLoadedAssociation)
|
|
3360
|
-
throw error;
|
|
3351
|
+
const association = this['getAssociationMetadata'](associationName);
|
|
3352
|
+
if (association === undefined)
|
|
3361
3353
|
return false;
|
|
3362
|
-
|
|
3354
|
+
return this[associationToGetterSetterProp(association)] !== undefined;
|
|
3363
3355
|
}
|
|
3364
3356
|
/**
|
|
3365
3357
|
* Reloads an instance, refreshing all it's attribute values
|
|
@@ -20,8 +20,35 @@ import { applyGetterAndSetter, associationPrimaryKeyAccessors, blankAssociations
|
|
|
20
20
|
* }
|
|
21
21
|
* ```
|
|
22
22
|
*
|
|
23
|
+
* ## Required (non-optional) BelongsTo and nullability
|
|
24
|
+
*
|
|
25
|
+
* `optional` is the single source of truth for whether a BelongsTo can be
|
|
26
|
+
* absent. A non-optional BelongsTo (the default, `optional: false`) is typed
|
|
27
|
+
* non-null, and that non-null type flows through serializers into the generated
|
|
28
|
+
* OpenAPI spec as a non-nullable field. "Required" therefore means "always
|
|
29
|
+
* present"; if a parent can legitimately be absent, mark the association
|
|
30
|
+
* `optional: true` (which flows to OpenAPI as nullable).
|
|
31
|
+
*
|
|
32
|
+
* Two consequences follow from that guarantee:
|
|
33
|
+
*
|
|
34
|
+
* - **Compile-time:** a non-optional BelongsTo cannot take an explicit
|
|
35
|
+
* load-time constraint. Passing `{ and: {...} }` (or any on-clause) as a
|
|
36
|
+
* trailing argument to `preload`, `load`, `leftJoinPreload`, or `leftJoinLoad`
|
|
37
|
+
* targeting a required BelongsTo is a type error, because such a constraint
|
|
38
|
+
* could filter out a real, existing record and yield null for a value the
|
|
39
|
+
* OpenAPI spec declares non-nullable. Constraints remain allowed on optional
|
|
40
|
+
* BelongsTo, `HasOne`, and `HasMany`; `join` is unaffected.
|
|
41
|
+
*
|
|
42
|
+
* - **Runtime:** if a required BelongsTo is null when accessed — its row was
|
|
43
|
+
* hard-deleted, or filtered out by an internal/default scope such as
|
|
44
|
+
* soft-delete — accessing the getter throws
|
|
45
|
+
* {@link MissingRequiredBelongsToAssociation}, a clear error in place of a
|
|
46
|
+
* cryptic null-deref deep in serialization. An unexpected throw is surfacing a
|
|
47
|
+
* modeling bug, typically a missing `dependent: 'destroy'` on the inverse
|
|
48
|
+
* `HasOne`/`HasMany` (or the association should have been `optional: true`).
|
|
49
|
+
*
|
|
23
50
|
* @param opts.on - A custom column name to use for joining associations on.
|
|
24
|
-
* @param opts.optional - Whether or not this association is optional. Defaults to false.
|
|
51
|
+
* @param opts.optional - Whether or not this association is optional. Defaults to false. When false (the default) the association is required: it is typed non-null, serializes as a non-nullable OpenAPI field, rejects explicit load-time constraints at compile time, and throws {@link MissingRequiredBelongsToAssociation} if null when accessed.
|
|
25
52
|
* @param opts.polymorphic - If true, this association will be treated as a polymorphic association.
|
|
26
53
|
* @param opts.primaryKeyOverride - A custom column name to use for the primary key.
|
|
27
54
|
* @param opts.withoutDefaultScopes - A list of default scopes to bypass when loading this association
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pluralize from 'pluralize-esm';
|
|
2
2
|
import { DreamConst } from '../../../dream/constants.js';
|
|
3
3
|
import { checkForeignKey } from '../../../errors/associations/InvalidComputedForeignKey.js';
|
|
4
|
+
import MissingRequiredBelongsToAssociation from '../../../errors/associations/MissingRequiredBelongsToAssociation.js';
|
|
4
5
|
import NonLoadedAssociation from '../../../errors/associations/NonLoadedAssociation.js';
|
|
5
6
|
import CannotDefineAssociationWithBothDependentAndPassthrough from '../../../errors/CannotDefineAssociationWithBothDependentAndPassthrough.js';
|
|
6
7
|
import CannotDefineAssociationWithBothDependentAndRequiredAndClause from '../../../errors/CannotDefineAssociationWithBothDependentAndRequiredAndClause.js';
|
|
@@ -55,8 +56,26 @@ export function applyGetterAndSetter(target, partialAssociation, { foreignKeyBas
|
|
|
55
56
|
const value = this[associationToGetterSetterProp(partialAssociation)];
|
|
56
57
|
if (value === undefined)
|
|
57
58
|
throw new NonLoadedAssociation({ dreamClass, associationName: partialAssociation.as });
|
|
58
|
-
|
|
59
|
+
// Getter semantics should follow the association metadata. The `isBelongsTo`
|
|
60
|
+
// option below is only for setter FK side effects when assigning models.
|
|
61
|
+
if (partialAssociation.type === 'BelongsTo' && !partialAssociation.optional && value === null) {
|
|
62
|
+
const foreignKey = finalForeignKey(foreignKeyBase, dreamClass, partialAssociation);
|
|
63
|
+
const foreignKeyType = partialAssociation.polymorphic
|
|
64
|
+
? foreignKeyTypeField(foreignKeyBase, dreamClass, partialAssociation)
|
|
65
|
+
: null;
|
|
66
|
+
throw new MissingRequiredBelongsToAssociation({
|
|
67
|
+
dreamClass,
|
|
68
|
+
associationName: partialAssociation.as,
|
|
69
|
+
foreignKey,
|
|
70
|
+
foreignKeyValue: this[foreignKey],
|
|
71
|
+
foreignKeyTypeField: foreignKeyType,
|
|
72
|
+
foreignKeyTypeValue: foreignKeyType ? this[foreignKeyType] : null,
|
|
73
|
+
polymorphic: partialAssociation.polymorphic,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
59
77
|
return value;
|
|
78
|
+
}
|
|
60
79
|
},
|
|
61
80
|
set: function (associatedModel) {
|
|
62
81
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import associationToGetterSetterProp from '../decorators/field/association/associationToGetterSetterProp.js';
|
|
1
2
|
import UnexpectedUndefined from '../errors/UnexpectedUndefined.js';
|
|
2
3
|
import unaliasTableName from './internal/unaliasTableName.js';
|
|
3
4
|
export default class LeftJoinLoadBuilder {
|
|
@@ -69,7 +70,10 @@ export default class LeftJoinLoadBuilder {
|
|
|
69
70
|
throw new UnexpectedUndefined();
|
|
70
71
|
this.query.dbDriverInstance()['hydrateAssociation']([this.dream], associationMetadata, this.associationToPreloadedDreamsAndWhatTheyPointTo({
|
|
71
72
|
pointsToPrimaryKey: this.dream.primaryKeyValue(),
|
|
72
|
-
associatedModels:
|
|
73
|
+
associatedModels: this.leftJoinLoadedAssociationValue({
|
|
74
|
+
dreamWithLoadedAssociations,
|
|
75
|
+
associationMetadata,
|
|
76
|
+
}),
|
|
73
77
|
}));
|
|
74
78
|
});
|
|
75
79
|
return this.dream;
|
|
@@ -77,4 +81,7 @@ export default class LeftJoinLoadBuilder {
|
|
|
77
81
|
associationToPreloadedDreamsAndWhatTheyPointTo({ pointsToPrimaryKey, associatedModels, }) {
|
|
78
82
|
return [associatedModels].flat().map(dream => ({ dream, pointsToPrimaryKey }));
|
|
79
83
|
}
|
|
84
|
+
leftJoinLoadedAssociationValue({ dreamWithLoadedAssociations, associationMetadata, }) {
|
|
85
|
+
return dreamWithLoadedAssociations[associationToGetterSetterProp(associationMetadata)];
|
|
86
|
+
}
|
|
80
87
|
}
|
|
@@ -755,15 +755,16 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
755
755
|
* Used to hydrate dreams with the provided associations
|
|
756
756
|
*/
|
|
757
757
|
hydrateAssociation(dreams, association, preloadedDreamsAndWhatTheyPointTo) {
|
|
758
|
+
const associationBackingProperty = associationToGetterSetterProp(association);
|
|
758
759
|
switch (association.type) {
|
|
759
760
|
case 'HasMany':
|
|
760
761
|
dreams.forEach((dream) => {
|
|
761
|
-
dream[
|
|
762
|
+
dream[associationBackingProperty] = [];
|
|
762
763
|
});
|
|
763
764
|
break;
|
|
764
765
|
default:
|
|
765
766
|
dreams.forEach((dream) => {
|
|
766
|
-
dream[
|
|
767
|
+
dream[associationBackingProperty] = null;
|
|
767
768
|
});
|
|
768
769
|
}
|
|
769
770
|
// dreams is a Rating
|
|
@@ -776,18 +777,18 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
776
777
|
.filter(dream => dream.primaryKeyValue() === preloadedDreamAndWhatItPointsTo.pointsToPrimaryKey)
|
|
777
778
|
.forEach((dream) => {
|
|
778
779
|
if (association.type === 'HasMany') {
|
|
779
|
-
dream[
|
|
780
|
+
dream[associationBackingProperty].push(preloadedDreamAndWhatItPointsTo.dream);
|
|
780
781
|
}
|
|
781
782
|
else {
|
|
782
783
|
// in a HasOne context, order clauses will be applied in advance,
|
|
783
784
|
// prior to hydration. Considering, we only want to set the first
|
|
784
785
|
// result and ignore other results, so we will use ||= to set.
|
|
785
|
-
dream[
|
|
786
|
+
dream[associationBackingProperty] ||= preloadedDreamAndWhatItPointsTo.dream;
|
|
786
787
|
}
|
|
787
788
|
});
|
|
788
789
|
});
|
|
789
790
|
if (association.type === 'HasMany') {
|
|
790
|
-
dreams.forEach((dream) => Object.freeze(dream[
|
|
791
|
+
dreams.forEach((dream) => Object.freeze(dream[associationBackingProperty]));
|
|
791
792
|
}
|
|
792
793
|
}
|
|
793
794
|
/**
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a non-optional (`optional: false`) BelongsTo association is null
|
|
3
|
+
* at the moment its getter is accessed.
|
|
4
|
+
*
|
|
5
|
+
* A non-optional BelongsTo is typed non-null and serializes to a non-nullable
|
|
6
|
+
* field in the generated OpenAPI spec — "required" means "always present". When
|
|
7
|
+
* the loaded value is nevertheless null, this error fails loud in place of a
|
|
8
|
+
* cryptic null-deref deep in serialization. It arises when the associated row is
|
|
9
|
+
* absent for one of these reasons:
|
|
10
|
+
*
|
|
11
|
+
* - The associated row was hard-deleted, leaving a dangling foreign key. This
|
|
12
|
+
* usually signals a missing `dependent: 'destroy'` on the inverse
|
|
13
|
+
* `HasOne`/`HasMany`.
|
|
14
|
+
* - An internal or default scope (such as soft-delete) filtered the associated
|
|
15
|
+
* row out at load time.
|
|
16
|
+
*
|
|
17
|
+
* The fix is one of:
|
|
18
|
+
*
|
|
19
|
+
* - Add `dependent: 'destroy'` to the inverse `HasOne`/`HasMany` so the parent
|
|
20
|
+
* cannot outlive its children, or
|
|
21
|
+
* - Mark the BelongsTo `optional: true` if the parent can legitimately be absent
|
|
22
|
+
* (which makes the field nullable in the OpenAPI spec).
|
|
23
|
+
*
|
|
24
|
+
* Note that an *explicit* load-time constraint on a required BelongsTo (e.g.
|
|
25
|
+
* `preload('parent', { and: {...} })`) is forbidden at compile time rather than
|
|
26
|
+
* surfacing here at runtime; see {@link BelongsTo}.
|
|
27
|
+
*/
|
|
28
|
+
export default class MissingRequiredBelongsToAssociation extends Error {
|
|
29
|
+
dreamClass;
|
|
30
|
+
associationName;
|
|
31
|
+
foreignKey;
|
|
32
|
+
foreignKeyValue;
|
|
33
|
+
foreignKeyTypeField;
|
|
34
|
+
foreignKeyTypeValue;
|
|
35
|
+
polymorphic;
|
|
36
|
+
constructor({ dreamClass, associationName, foreignKey, foreignKeyValue, foreignKeyTypeField = null, foreignKeyTypeValue = null, polymorphic, }) {
|
|
37
|
+
super();
|
|
38
|
+
this.dreamClass = dreamClass;
|
|
39
|
+
this.associationName = associationName;
|
|
40
|
+
this.foreignKey = foreignKey;
|
|
41
|
+
this.foreignKeyValue = foreignKeyValue;
|
|
42
|
+
this.foreignKeyTypeField = foreignKeyTypeField;
|
|
43
|
+
this.foreignKeyTypeValue = foreignKeyTypeValue;
|
|
44
|
+
this.polymorphic = polymorphic;
|
|
45
|
+
}
|
|
46
|
+
get message() {
|
|
47
|
+
return `
|
|
48
|
+
Attempting to access required BelongsTo association \`${this.associationName}\` on an instance of \`${this.dreamClass.sanitizedName}\`,
|
|
49
|
+
but the loaded association is null.
|
|
50
|
+
|
|
51
|
+
${this.foreignKeyMessage}
|
|
52
|
+
|
|
53
|
+
If the associated record was deleted, check whether the inverse HasOne/HasMany association should specify dependent: 'destroy'.
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
get foreignKeyMessage() {
|
|
57
|
+
if (this.foreignKeyValue === null || this.foreignKeyValue === undefined) {
|
|
58
|
+
return `The foreign key \`${this.foreignKey}\` is null, which violates this non-optional BelongsTo association.`;
|
|
59
|
+
}
|
|
60
|
+
return [
|
|
61
|
+
`The foreign key \`${this.foreignKey}\` is set to \`${this.formatValue(this.foreignKeyValue)}\`, but no associated record was loaded.`,
|
|
62
|
+
this.polymorphic && this.foreignKeyTypeField
|
|
63
|
+
? `The polymorphic type field \`${this.foreignKeyTypeField}\` is set to \`${this.formatValue(this.foreignKeyTypeValue)}\`.`
|
|
64
|
+
: null,
|
|
65
|
+
]
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join('\n');
|
|
68
|
+
}
|
|
69
|
+
formatValue(value) {
|
|
70
|
+
if (value === null)
|
|
71
|
+
return 'null';
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
return 'undefined';
|
|
74
|
+
if (typeof value === 'string')
|
|
75
|
+
return value;
|
|
76
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint')
|
|
77
|
+
return value.toString();
|
|
78
|
+
if (typeof value === 'symbol')
|
|
79
|
+
return value.description ?? value.toString();
|
|
80
|
+
if (typeof value === 'function')
|
|
81
|
+
return value.name || '[function]';
|
|
82
|
+
return JSON.stringify(value);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/dist/esm/src/Dream.js
CHANGED
|
@@ -36,7 +36,6 @@ import CannotDestroyAssociationOnUnpersistedDream from './errors/associations/Ca
|
|
|
36
36
|
import CannotPassNullOrUndefinedToRequiredBelongsTo from './errors/associations/CannotPassNullOrUndefinedToRequiredBelongsTo.js';
|
|
37
37
|
import CannotUpdateAssociationOnUnpersistedDream from './errors/associations/CannotUpdateAssociationOnUnpersistedDream.js';
|
|
38
38
|
import CanOnlyPassBelongsToModelParam from './errors/associations/CanOnlyPassBelongsToModelParam.js';
|
|
39
|
-
import NonLoadedAssociation from './errors/associations/NonLoadedAssociation.js';
|
|
40
39
|
import CannotCallUndestroyOnANonSoftDeleteModel from './errors/CannotCallUndestroyOnANonSoftDeleteModel.js';
|
|
41
40
|
import ConstructorOnlyForInternalUse from './errors/ConstructorOnlyForInternalUse.js';
|
|
42
41
|
import CreateOrFindByFailedToCreateAndFind from './errors/CreateOrFindByFailedToCreateAndFind.js';
|
|
@@ -3349,17 +3348,10 @@ export default class Dream {
|
|
|
3349
3348
|
* @returns A boolean
|
|
3350
3349
|
*/
|
|
3351
3350
|
loaded(associationName) {
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
;
|
|
3355
|
-
this[associationName];
|
|
3356
|
-
return true;
|
|
3357
|
-
}
|
|
3358
|
-
catch (error) {
|
|
3359
|
-
if (error.constructor !== NonLoadedAssociation)
|
|
3360
|
-
throw error;
|
|
3351
|
+
const association = this['getAssociationMetadata'](associationName);
|
|
3352
|
+
if (association === undefined)
|
|
3361
3353
|
return false;
|
|
3362
|
-
|
|
3354
|
+
return this[associationToGetterSetterProp(association)] !== undefined;
|
|
3363
3355
|
}
|
|
3364
3356
|
/**
|
|
3365
3357
|
* Reloads an instance, refreshing all it's attribute values
|
|
@@ -20,8 +20,35 @@ import { applyGetterAndSetter, associationPrimaryKeyAccessors, blankAssociations
|
|
|
20
20
|
* }
|
|
21
21
|
* ```
|
|
22
22
|
*
|
|
23
|
+
* ## Required (non-optional) BelongsTo and nullability
|
|
24
|
+
*
|
|
25
|
+
* `optional` is the single source of truth for whether a BelongsTo can be
|
|
26
|
+
* absent. A non-optional BelongsTo (the default, `optional: false`) is typed
|
|
27
|
+
* non-null, and that non-null type flows through serializers into the generated
|
|
28
|
+
* OpenAPI spec as a non-nullable field. "Required" therefore means "always
|
|
29
|
+
* present"; if a parent can legitimately be absent, mark the association
|
|
30
|
+
* `optional: true` (which flows to OpenAPI as nullable).
|
|
31
|
+
*
|
|
32
|
+
* Two consequences follow from that guarantee:
|
|
33
|
+
*
|
|
34
|
+
* - **Compile-time:** a non-optional BelongsTo cannot take an explicit
|
|
35
|
+
* load-time constraint. Passing `{ and: {...} }` (or any on-clause) as a
|
|
36
|
+
* trailing argument to `preload`, `load`, `leftJoinPreload`, or `leftJoinLoad`
|
|
37
|
+
* targeting a required BelongsTo is a type error, because such a constraint
|
|
38
|
+
* could filter out a real, existing record and yield null for a value the
|
|
39
|
+
* OpenAPI spec declares non-nullable. Constraints remain allowed on optional
|
|
40
|
+
* BelongsTo, `HasOne`, and `HasMany`; `join` is unaffected.
|
|
41
|
+
*
|
|
42
|
+
* - **Runtime:** if a required BelongsTo is null when accessed — its row was
|
|
43
|
+
* hard-deleted, or filtered out by an internal/default scope such as
|
|
44
|
+
* soft-delete — accessing the getter throws
|
|
45
|
+
* {@link MissingRequiredBelongsToAssociation}, a clear error in place of a
|
|
46
|
+
* cryptic null-deref deep in serialization. An unexpected throw is surfacing a
|
|
47
|
+
* modeling bug, typically a missing `dependent: 'destroy'` on the inverse
|
|
48
|
+
* `HasOne`/`HasMany` (or the association should have been `optional: true`).
|
|
49
|
+
*
|
|
23
50
|
* @param opts.on - A custom column name to use for joining associations on.
|
|
24
|
-
* @param opts.optional - Whether or not this association is optional. Defaults to false.
|
|
51
|
+
* @param opts.optional - Whether or not this association is optional. Defaults to false. When false (the default) the association is required: it is typed non-null, serializes as a non-nullable OpenAPI field, rejects explicit load-time constraints at compile time, and throws {@link MissingRequiredBelongsToAssociation} if null when accessed.
|
|
25
52
|
* @param opts.polymorphic - If true, this association will be treated as a polymorphic association.
|
|
26
53
|
* @param opts.primaryKeyOverride - A custom column name to use for the primary key.
|
|
27
54
|
* @param opts.withoutDefaultScopes - A list of default scopes to bypass when loading this association
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pluralize from 'pluralize-esm';
|
|
2
2
|
import { DreamConst } from '../../../dream/constants.js';
|
|
3
3
|
import { checkForeignKey } from '../../../errors/associations/InvalidComputedForeignKey.js';
|
|
4
|
+
import MissingRequiredBelongsToAssociation from '../../../errors/associations/MissingRequiredBelongsToAssociation.js';
|
|
4
5
|
import NonLoadedAssociation from '../../../errors/associations/NonLoadedAssociation.js';
|
|
5
6
|
import CannotDefineAssociationWithBothDependentAndPassthrough from '../../../errors/CannotDefineAssociationWithBothDependentAndPassthrough.js';
|
|
6
7
|
import CannotDefineAssociationWithBothDependentAndRequiredAndClause from '../../../errors/CannotDefineAssociationWithBothDependentAndRequiredAndClause.js';
|
|
@@ -55,8 +56,26 @@ export function applyGetterAndSetter(target, partialAssociation, { foreignKeyBas
|
|
|
55
56
|
const value = this[associationToGetterSetterProp(partialAssociation)];
|
|
56
57
|
if (value === undefined)
|
|
57
58
|
throw new NonLoadedAssociation({ dreamClass, associationName: partialAssociation.as });
|
|
58
|
-
|
|
59
|
+
// Getter semantics should follow the association metadata. The `isBelongsTo`
|
|
60
|
+
// option below is only for setter FK side effects when assigning models.
|
|
61
|
+
if (partialAssociation.type === 'BelongsTo' && !partialAssociation.optional && value === null) {
|
|
62
|
+
const foreignKey = finalForeignKey(foreignKeyBase, dreamClass, partialAssociation);
|
|
63
|
+
const foreignKeyType = partialAssociation.polymorphic
|
|
64
|
+
? foreignKeyTypeField(foreignKeyBase, dreamClass, partialAssociation)
|
|
65
|
+
: null;
|
|
66
|
+
throw new MissingRequiredBelongsToAssociation({
|
|
67
|
+
dreamClass,
|
|
68
|
+
associationName: partialAssociation.as,
|
|
69
|
+
foreignKey,
|
|
70
|
+
foreignKeyValue: this[foreignKey],
|
|
71
|
+
foreignKeyTypeField: foreignKeyType,
|
|
72
|
+
foreignKeyTypeValue: foreignKeyType ? this[foreignKeyType] : null,
|
|
73
|
+
polymorphic: partialAssociation.polymorphic,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
59
77
|
return value;
|
|
78
|
+
}
|
|
60
79
|
},
|
|
61
80
|
set: function (associatedModel) {
|
|
62
81
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import associationToGetterSetterProp from '../decorators/field/association/associationToGetterSetterProp.js';
|
|
1
2
|
import UnexpectedUndefined from '../errors/UnexpectedUndefined.js';
|
|
2
3
|
import unaliasTableName from './internal/unaliasTableName.js';
|
|
3
4
|
export default class LeftJoinLoadBuilder {
|
|
@@ -69,7 +70,10 @@ export default class LeftJoinLoadBuilder {
|
|
|
69
70
|
throw new UnexpectedUndefined();
|
|
70
71
|
this.query.dbDriverInstance()['hydrateAssociation']([this.dream], associationMetadata, this.associationToPreloadedDreamsAndWhatTheyPointTo({
|
|
71
72
|
pointsToPrimaryKey: this.dream.primaryKeyValue(),
|
|
72
|
-
associatedModels:
|
|
73
|
+
associatedModels: this.leftJoinLoadedAssociationValue({
|
|
74
|
+
dreamWithLoadedAssociations,
|
|
75
|
+
associationMetadata,
|
|
76
|
+
}),
|
|
73
77
|
}));
|
|
74
78
|
});
|
|
75
79
|
return this.dream;
|
|
@@ -77,4 +81,7 @@ export default class LeftJoinLoadBuilder {
|
|
|
77
81
|
associationToPreloadedDreamsAndWhatTheyPointTo({ pointsToPrimaryKey, associatedModels, }) {
|
|
78
82
|
return [associatedModels].flat().map(dream => ({ dream, pointsToPrimaryKey }));
|
|
79
83
|
}
|
|
84
|
+
leftJoinLoadedAssociationValue({ dreamWithLoadedAssociations, associationMetadata, }) {
|
|
85
|
+
return dreamWithLoadedAssociations[associationToGetterSetterProp(associationMetadata)];
|
|
86
|
+
}
|
|
80
87
|
}
|
|
@@ -755,15 +755,16 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
755
755
|
* Used to hydrate dreams with the provided associations
|
|
756
756
|
*/
|
|
757
757
|
hydrateAssociation(dreams, association, preloadedDreamsAndWhatTheyPointTo) {
|
|
758
|
+
const associationBackingProperty = associationToGetterSetterProp(association);
|
|
758
759
|
switch (association.type) {
|
|
759
760
|
case 'HasMany':
|
|
760
761
|
dreams.forEach((dream) => {
|
|
761
|
-
dream[
|
|
762
|
+
dream[associationBackingProperty] = [];
|
|
762
763
|
});
|
|
763
764
|
break;
|
|
764
765
|
default:
|
|
765
766
|
dreams.forEach((dream) => {
|
|
766
|
-
dream[
|
|
767
|
+
dream[associationBackingProperty] = null;
|
|
767
768
|
});
|
|
768
769
|
}
|
|
769
770
|
// dreams is a Rating
|
|
@@ -776,18 +777,18 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
776
777
|
.filter(dream => dream.primaryKeyValue() === preloadedDreamAndWhatItPointsTo.pointsToPrimaryKey)
|
|
777
778
|
.forEach((dream) => {
|
|
778
779
|
if (association.type === 'HasMany') {
|
|
779
|
-
dream[
|
|
780
|
+
dream[associationBackingProperty].push(preloadedDreamAndWhatItPointsTo.dream);
|
|
780
781
|
}
|
|
781
782
|
else {
|
|
782
783
|
// in a HasOne context, order clauses will be applied in advance,
|
|
783
784
|
// prior to hydration. Considering, we only want to set the first
|
|
784
785
|
// result and ignore other results, so we will use ||= to set.
|
|
785
|
-
dream[
|
|
786
|
+
dream[associationBackingProperty] ||= preloadedDreamAndWhatItPointsTo.dream;
|
|
786
787
|
}
|
|
787
788
|
});
|
|
788
789
|
});
|
|
789
790
|
if (association.type === 'HasMany') {
|
|
790
|
-
dreams.forEach((dream) => Object.freeze(dream[
|
|
791
|
+
dreams.forEach((dream) => Object.freeze(dream[associationBackingProperty]));
|
|
791
792
|
}
|
|
792
793
|
}
|
|
793
794
|
/**
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when a non-optional (`optional: false`) BelongsTo association is null
|
|
3
|
+
* at the moment its getter is accessed.
|
|
4
|
+
*
|
|
5
|
+
* A non-optional BelongsTo is typed non-null and serializes to a non-nullable
|
|
6
|
+
* field in the generated OpenAPI spec — "required" means "always present". When
|
|
7
|
+
* the loaded value is nevertheless null, this error fails loud in place of a
|
|
8
|
+
* cryptic null-deref deep in serialization. It arises when the associated row is
|
|
9
|
+
* absent for one of these reasons:
|
|
10
|
+
*
|
|
11
|
+
* - The associated row was hard-deleted, leaving a dangling foreign key. This
|
|
12
|
+
* usually signals a missing `dependent: 'destroy'` on the inverse
|
|
13
|
+
* `HasOne`/`HasMany`.
|
|
14
|
+
* - An internal or default scope (such as soft-delete) filtered the associated
|
|
15
|
+
* row out at load time.
|
|
16
|
+
*
|
|
17
|
+
* The fix is one of:
|
|
18
|
+
*
|
|
19
|
+
* - Add `dependent: 'destroy'` to the inverse `HasOne`/`HasMany` so the parent
|
|
20
|
+
* cannot outlive its children, or
|
|
21
|
+
* - Mark the BelongsTo `optional: true` if the parent can legitimately be absent
|
|
22
|
+
* (which makes the field nullable in the OpenAPI spec).
|
|
23
|
+
*
|
|
24
|
+
* Note that an *explicit* load-time constraint on a required BelongsTo (e.g.
|
|
25
|
+
* `preload('parent', { and: {...} })`) is forbidden at compile time rather than
|
|
26
|
+
* surfacing here at runtime; see {@link BelongsTo}.
|
|
27
|
+
*/
|
|
28
|
+
export default class MissingRequiredBelongsToAssociation extends Error {
|
|
29
|
+
dreamClass;
|
|
30
|
+
associationName;
|
|
31
|
+
foreignKey;
|
|
32
|
+
foreignKeyValue;
|
|
33
|
+
foreignKeyTypeField;
|
|
34
|
+
foreignKeyTypeValue;
|
|
35
|
+
polymorphic;
|
|
36
|
+
constructor({ dreamClass, associationName, foreignKey, foreignKeyValue, foreignKeyTypeField = null, foreignKeyTypeValue = null, polymorphic, }) {
|
|
37
|
+
super();
|
|
38
|
+
this.dreamClass = dreamClass;
|
|
39
|
+
this.associationName = associationName;
|
|
40
|
+
this.foreignKey = foreignKey;
|
|
41
|
+
this.foreignKeyValue = foreignKeyValue;
|
|
42
|
+
this.foreignKeyTypeField = foreignKeyTypeField;
|
|
43
|
+
this.foreignKeyTypeValue = foreignKeyTypeValue;
|
|
44
|
+
this.polymorphic = polymorphic;
|
|
45
|
+
}
|
|
46
|
+
get message() {
|
|
47
|
+
return `
|
|
48
|
+
Attempting to access required BelongsTo association \`${this.associationName}\` on an instance of \`${this.dreamClass.sanitizedName}\`,
|
|
49
|
+
but the loaded association is null.
|
|
50
|
+
|
|
51
|
+
${this.foreignKeyMessage}
|
|
52
|
+
|
|
53
|
+
If the associated record was deleted, check whether the inverse HasOne/HasMany association should specify dependent: 'destroy'.
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
get foreignKeyMessage() {
|
|
57
|
+
if (this.foreignKeyValue === null || this.foreignKeyValue === undefined) {
|
|
58
|
+
return `The foreign key \`${this.foreignKey}\` is null, which violates this non-optional BelongsTo association.`;
|
|
59
|
+
}
|
|
60
|
+
return [
|
|
61
|
+
`The foreign key \`${this.foreignKey}\` is set to \`${this.formatValue(this.foreignKeyValue)}\`, but no associated record was loaded.`,
|
|
62
|
+
this.polymorphic && this.foreignKeyTypeField
|
|
63
|
+
? `The polymorphic type field \`${this.foreignKeyTypeField}\` is set to \`${this.formatValue(this.foreignKeyTypeValue)}\`.`
|
|
64
|
+
: null,
|
|
65
|
+
]
|
|
66
|
+
.filter(Boolean)
|
|
67
|
+
.join('\n');
|
|
68
|
+
}
|
|
69
|
+
formatValue(value) {
|
|
70
|
+
if (value === null)
|
|
71
|
+
return 'null';
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
return 'undefined';
|
|
74
|
+
if (typeof value === 'string')
|
|
75
|
+
return value;
|
|
76
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint')
|
|
77
|
+
return value.toString();
|
|
78
|
+
if (typeof value === 'symbol')
|
|
79
|
+
return value.description ?? value.toString();
|
|
80
|
+
if (typeof value === 'function')
|
|
81
|
+
return value.name || '[function]';
|
|
82
|
+
return JSON.stringify(value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Dream from '../../Dream.js';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when a non-optional (`optional: false`) BelongsTo association is null
|
|
4
|
+
* at the moment its getter is accessed.
|
|
5
|
+
*
|
|
6
|
+
* A non-optional BelongsTo is typed non-null and serializes to a non-nullable
|
|
7
|
+
* field in the generated OpenAPI spec — "required" means "always present". When
|
|
8
|
+
* the loaded value is nevertheless null, this error fails loud in place of a
|
|
9
|
+
* cryptic null-deref deep in serialization. It arises when the associated row is
|
|
10
|
+
* absent for one of these reasons:
|
|
11
|
+
*
|
|
12
|
+
* - The associated row was hard-deleted, leaving a dangling foreign key. This
|
|
13
|
+
* usually signals a missing `dependent: 'destroy'` on the inverse
|
|
14
|
+
* `HasOne`/`HasMany`.
|
|
15
|
+
* - An internal or default scope (such as soft-delete) filtered the associated
|
|
16
|
+
* row out at load time.
|
|
17
|
+
*
|
|
18
|
+
* The fix is one of:
|
|
19
|
+
*
|
|
20
|
+
* - Add `dependent: 'destroy'` to the inverse `HasOne`/`HasMany` so the parent
|
|
21
|
+
* cannot outlive its children, or
|
|
22
|
+
* - Mark the BelongsTo `optional: true` if the parent can legitimately be absent
|
|
23
|
+
* (which makes the field nullable in the OpenAPI spec).
|
|
24
|
+
*
|
|
25
|
+
* Note that an *explicit* load-time constraint on a required BelongsTo (e.g.
|
|
26
|
+
* `preload('parent', { and: {...} })`) is forbidden at compile time rather than
|
|
27
|
+
* surfacing here at runtime; see {@link BelongsTo}.
|
|
28
|
+
*/
|
|
29
|
+
export default class MissingRequiredBelongsToAssociation extends Error {
|
|
30
|
+
dreamClass: typeof Dream;
|
|
31
|
+
associationName: string;
|
|
32
|
+
foreignKey: string;
|
|
33
|
+
foreignKeyValue: unknown;
|
|
34
|
+
foreignKeyTypeField: string | null;
|
|
35
|
+
foreignKeyTypeValue: unknown;
|
|
36
|
+
polymorphic: boolean;
|
|
37
|
+
constructor({ dreamClass, associationName, foreignKey, foreignKeyValue, foreignKeyTypeField, foreignKeyTypeValue, polymorphic, }: {
|
|
38
|
+
dreamClass: typeof Dream;
|
|
39
|
+
associationName: string;
|
|
40
|
+
foreignKey: string;
|
|
41
|
+
foreignKeyValue: unknown;
|
|
42
|
+
foreignKeyTypeField?: string | null;
|
|
43
|
+
foreignKeyTypeValue?: unknown;
|
|
44
|
+
polymorphic: boolean;
|
|
45
|
+
});
|
|
46
|
+
get message(): string;
|
|
47
|
+
private get foreignKeyMessage();
|
|
48
|
+
private formatValue;
|
|
49
|
+
}
|
|
@@ -42,6 +42,15 @@ export type DreamAssociationNamesWithPassthroughOnClauses<DreamInstance extends
|
|
|
42
42
|
}, PassthroughOnClauses = keyof RejectInterface<SchemaTypeInterface, null> & string> = PassthroughOnClauses;
|
|
43
43
|
export type RequiredOnClauseKeys<Schema, TableName, AssociationName, Associations = TableName extends null ? null : TableName extends keyof Schema & string ? Schema[TableName]['associations' & keyof Schema[TableName]] : null, Association = Associations extends null ? null : AssociationName extends keyof Associations ? Associations[AssociationName] : null, RequiredOnClauses = Association extends null ? null : Association['requiredAndClauses' & keyof Association] & (string[] | null)> = RequiredOnClauses;
|
|
44
44
|
export type PassthroughOnClauseKeys<Schema, TableName, AssociationName, Associations = TableName extends null ? null : TableName extends keyof Schema & string ? Schema[TableName]['associations' & keyof Schema[TableName]] : null, Association = Associations extends null ? null : AssociationName extends keyof Associations ? Associations[AssociationName] : null, PassthroughOnClauses = Association extends null ? null : Association['passthroughAndClauses' & keyof Association] & (string[] | null)> = PassthroughOnClauses;
|
|
45
|
+
/**
|
|
46
|
+
* Resolves to `true` when the named association on the given table is a
|
|
47
|
+
* non-optional (required) BelongsTo, and `false` otherwise. A required
|
|
48
|
+
* BelongsTo getter is typed non-null and that non-null type flows through
|
|
49
|
+
* serializers into the generated OpenAPI spec as a non-nullable field, so a
|
|
50
|
+
* load-time constraint that could filter the parent out (nulling the value)
|
|
51
|
+
* must be forbidden at compile time for the hydrating load variants.
|
|
52
|
+
*/
|
|
53
|
+
export type IsNonOptionalBelongsToAssociation<Schema, TableName, AssociationName, Associations = TableName extends null ? null : TableName extends keyof Schema & string ? Schema[TableName]['associations' & keyof Schema[TableName]] : null, Association = Associations extends null ? null : AssociationName extends keyof Associations ? Associations[AssociationName] : null> = Association extends null ? false : Association['type' & keyof Association] extends 'BelongsTo' ? Association['optional' & keyof Association] extends false ? true : false : false;
|
|
45
54
|
export type DreamAssociationNames<DreamInstance extends Dream, SchemaAssociations = DreamAssociationMetadata<DreamInstance>> = keyof SchemaAssociations;
|
|
46
55
|
export type DreamBelongsToAssociationNames<DreamInstance extends Dream> = keyof DreamBelongsToAssociationMetadata<DreamInstance>;
|
|
47
56
|
export type DreamHasOneAssociationNames<DreamInstance extends Dream> = keyof DreamHasOneAssociationMetadata<DreamInstance>;
|
|
@@ -70,8 +79,9 @@ export type DreamAttributeDbTypes<DreamInstance extends Dream, Schema = DreamIns
|
|
|
70
79
|
-readonly [K in keyof SchemaColumns]: SchemaColumns[K]['dbType' & keyof SchemaColumns[K]];
|
|
71
80
|
};
|
|
72
81
|
export type DreamParamSafeAttributes<DreamInstance extends Dream> = {
|
|
73
|
-
[K in keyof UpdateableProperties<DreamInstance> & DreamParamSafeColumnNames<DreamInstance>]: UpdateableProperties<DreamInstance>[K];
|
|
82
|
+
[K in keyof UpdateableProperties<DreamInstance> & DreamParamSafeColumnNames<DreamInstance>]: IsAny<UpdateableProperties<DreamInstance>[K]> extends true ? K extends keyof DreamInstance ? DreamInstance[K] : UpdateableProperties<DreamInstance>[K] : UpdateableProperties<DreamInstance>[K];
|
|
74
83
|
};
|
|
84
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
75
85
|
export type DreamTableSchema<DreamInstance extends Dream> = Updateable<DreamInstance['DB'][DreamInstance['table']]>;
|
|
76
86
|
export type ModelColumnType<Schema, TableName, Column, TableSchema extends Schema[TableName & keyof Schema] = Schema[TableName & keyof Schema], TableColumns extends TableSchema['columns' & keyof TableSchema] = TableSchema['columns' & keyof TableSchema], TableColumnMetadata extends TableColumns[Column & keyof TableColumns] = TableColumns[Column & keyof TableColumns], ColumnType extends TableColumnMetadata['coercedType' & keyof TableColumnMetadata] = TableColumnMetadata['coercedType' & keyof TableColumnMetadata]> = ColumnType;
|
|
77
87
|
export type TableColumnType<Schema, TableName, Column, TableSchema extends Schema[TableName & keyof Schema] = Schema[TableName & keyof Schema], TableColumns extends TableSchema['columns' & keyof TableSchema] = TableSchema['columns' & keyof TableSchema], TableColumnMetadata extends TableColumns[Column & keyof TableColumns] = TableColumns[Column & keyof TableColumns], ColumnType extends TableColumnMetadata['dbType' & keyof TableColumnMetadata] = TableColumnMetadata['dbType' & keyof TableColumnMetadata]> = ColumnType;
|