@rvoh/dream 2.14.1 → 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.
Files changed (200) hide show
  1. package/dist/cjs/src/Dream.js +3 -11
  2. package/dist/cjs/src/decorators/field/association/BelongsTo.js +28 -1
  3. package/dist/cjs/src/decorators/field/association/shared.js +20 -1
  4. package/dist/cjs/src/dream/LeftJoinLoadBuilder.js +8 -1
  5. package/dist/cjs/src/dream/QueryDriver/Kysely.js +6 -5
  6. package/dist/cjs/src/errors/associations/MissingRequiredBelongsToAssociation.js +84 -0
  7. package/dist/esm/src/Dream.js +3 -11
  8. package/dist/esm/src/decorators/field/association/BelongsTo.js +28 -1
  9. package/dist/esm/src/decorators/field/association/shared.js +20 -1
  10. package/dist/esm/src/dream/LeftJoinLoadBuilder.js +8 -1
  11. package/dist/esm/src/dream/QueryDriver/Kysely.js +6 -5
  12. package/dist/esm/src/errors/associations/MissingRequiredBelongsToAssociation.js +84 -0
  13. package/dist/types/src/dream/LeftJoinLoadBuilder.d.ts +1 -0
  14. package/dist/types/src/errors/associations/MissingRequiredBelongsToAssociation.d.ts +49 -0
  15. package/dist/types/src/types/dream.d.ts +9 -0
  16. package/dist/types/src/types/dream.ts +30 -0
  17. package/dist/types/src/types/variadic.d.ts +9 -4
  18. package/dist/types/src/types/variadic.ts +28 -4
  19. package/docs/classes/db.DreamMigrationHelpers.html +11 -11
  20. package/docs/classes/db.KyselyQueryDriver.html +34 -34
  21. package/docs/classes/db.PostgresQueryDriver.html +35 -35
  22. package/docs/classes/db.QueryDriverBase.html +33 -33
  23. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  24. package/docs/classes/errors.ColumnOverflow.html +3 -3
  25. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  26. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  27. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  28. package/docs/classes/errors.DecryptionError.html +2 -2
  29. package/docs/classes/errors.DecryptionParseError.html +2 -2
  30. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  31. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  32. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  33. package/docs/classes/errors.InvalidClockTime.html +2 -2
  34. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  35. package/docs/classes/errors.InvalidDateTime.html +2 -2
  36. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  37. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  38. package/docs/classes/errors.NotNullViolation.html +3 -3
  39. package/docs/classes/errors.RecordNotFound.html +3 -3
  40. package/docs/classes/errors.ValidationError.html +3 -3
  41. package/docs/classes/index.CalendarDate.html +33 -33
  42. package/docs/classes/index.ClockTime.html +32 -32
  43. package/docs/classes/index.ClockTimeTz.html +35 -35
  44. package/docs/classes/index.DateTime.html +86 -86
  45. package/docs/classes/index.Decorators.html +19 -19
  46. package/docs/classes/index.Dream.html +118 -118
  47. package/docs/classes/index.DreamApp.html +10 -10
  48. package/docs/classes/index.DreamTransaction.html +2 -2
  49. package/docs/classes/index.Env.html +2 -2
  50. package/docs/classes/index.Query.html +57 -57
  51. package/docs/classes/system.CliFileWriter.html +4 -4
  52. package/docs/classes/system.DreamBin.html +2 -2
  53. package/docs/classes/system.DreamCLI.html +7 -7
  54. package/docs/classes/system.DreamImporter.html +2 -2
  55. package/docs/classes/system.DreamLogos.html +2 -2
  56. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  57. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  58. package/docs/classes/system.PathHelpers.html +3 -3
  59. package/docs/classes/utils.Encrypt.html +3 -3
  60. package/docs/classes/utils.Range.html +2 -2
  61. package/docs/functions/db.closeAllDbConnections.html +1 -1
  62. package/docs/functions/db.dreamDbConnections.html +1 -1
  63. package/docs/functions/db.untypedDb.html +1 -1
  64. package/docs/functions/db.validateColumn.html +1 -1
  65. package/docs/functions/db.validateTable.html +1 -1
  66. package/docs/functions/errors.pgErrorType.html +1 -1
  67. package/docs/functions/index.DreamSerializer.html +1 -1
  68. package/docs/functions/index.ObjectSerializer.html +1 -1
  69. package/docs/functions/index.ReplicaSafe.html +1 -1
  70. package/docs/functions/index.STI.html +1 -1
  71. package/docs/functions/index.SoftDelete.html +1 -1
  72. package/docs/functions/utils.camelize.html +1 -1
  73. package/docs/functions/utils.capitalize.html +1 -1
  74. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  75. package/docs/functions/utils.compact.html +1 -1
  76. package/docs/functions/utils.groupBy.html +1 -1
  77. package/docs/functions/utils.hyphenize.html +1 -1
  78. package/docs/functions/utils.intersection.html +1 -1
  79. package/docs/functions/utils.isEmpty.html +1 -1
  80. package/docs/functions/utils.normalizeUnicode.html +1 -1
  81. package/docs/functions/utils.pascalize.html +1 -1
  82. package/docs/functions/utils.percent.html +1 -1
  83. package/docs/functions/utils.range.html +1 -1
  84. package/docs/functions/utils.round.html +1 -1
  85. package/docs/functions/utils.sanitizeString.html +1 -1
  86. package/docs/functions/utils.snakeify.html +1 -1
  87. package/docs/functions/utils.sort.html +1 -1
  88. package/docs/functions/utils.sortBy.html +1 -1
  89. package/docs/functions/utils.sortObjectByKey.html +1 -1
  90. package/docs/functions/utils.sortObjectByValue.html +1 -1
  91. package/docs/functions/utils.uncapitalize.html +1 -1
  92. package/docs/functions/utils.uniq.html +1 -1
  93. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  94. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  95. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  96. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  97. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  98. package/docs/interfaces/types.DecoratorContext.html +2 -2
  99. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  100. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  101. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  102. package/docs/interfaces/types.DurationObject.html +2 -2
  103. package/docs/interfaces/types.EncryptOptions.html +2 -2
  104. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  105. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  106. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  107. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  108. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  109. package/docs/types/openapi.OpenapiFormats.html +1 -1
  110. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  111. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  112. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  113. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  114. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  115. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  116. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  117. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  118. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  119. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  120. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  121. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  122. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  123. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  124. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  125. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  126. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  127. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  128. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  129. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  130. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  131. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  132. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  133. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  134. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  135. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  136. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  137. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  138. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  139. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  140. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  141. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  142. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  143. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  144. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  145. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  146. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  147. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  148. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  149. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  150. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  151. package/docs/types/types.CalendarDateObject.html +1 -1
  152. package/docs/types/types.Camelized.html +1 -1
  153. package/docs/types/types.ClockTimeObject.html +1 -1
  154. package/docs/types/types.DbConnectionType.html +1 -1
  155. package/docs/types/types.DbTypes.html +1 -1
  156. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  157. package/docs/types/types.DreamAttributes.html +1 -1
  158. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  159. package/docs/types/types.DreamClassColumn.html +1 -1
  160. package/docs/types/types.DreamColumn.html +1 -1
  161. package/docs/types/types.DreamColumnNames.html +1 -1
  162. package/docs/types/types.DreamLogLevel.html +1 -1
  163. package/docs/types/types.DreamLogger.html +2 -2
  164. package/docs/types/types.DreamModelSerializerType.html +1 -1
  165. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  166. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  167. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  168. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  169. package/docs/types/types.DreamSerializable.html +1 -1
  170. package/docs/types/types.DreamSerializableArray.html +1 -1
  171. package/docs/types/types.DreamSerializerKey.html +1 -1
  172. package/docs/types/types.DreamSerializers.html +1 -1
  173. package/docs/types/types.DreamVirtualColumns.html +1 -1
  174. package/docs/types/types.DurationUnit.html +1 -1
  175. package/docs/types/types.EncryptAlgorithm.html +1 -1
  176. package/docs/types/types.HasManyStatement.html +1 -1
  177. package/docs/types/types.HasOneStatement.html +1 -1
  178. package/docs/types/types.Hyphenized.html +1 -1
  179. package/docs/types/types.Pascalized.html +1 -1
  180. package/docs/types/types.PrimaryKeyType.html +1 -1
  181. package/docs/types/types.RoundingPrecision.html +1 -1
  182. package/docs/types/types.SerializerCasing.html +1 -1
  183. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  184. package/docs/types/types.Snakeified.html +1 -1
  185. package/docs/types/types.StrictInterface.html +1 -1
  186. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  187. package/docs/types/types.UpdateableProperties.html +1 -1
  188. package/docs/types/types.ValidationType.html +1 -1
  189. package/docs/types/types.ViewModel.html +2 -2
  190. package/docs/types/types.ViewModelClass.html +1 -1
  191. package/docs/types/types.WeekdayName.html +1 -1
  192. package/docs/types/types.WhereStatementForDream.html +1 -1
  193. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  194. package/docs/variables/index.DreamConst.html +1 -1
  195. package/docs/variables/index.ops.html +1 -1
  196. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  197. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  198. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  199. package/docs/variables/system.primaryKeyTypes.html +1 -1
  200. package/package.json +1 -1
@@ -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
- try {
3353
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
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
- else
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: dreamWithLoadedAssociations[associationName],
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[association.as] = [];
762
+ dream[associationBackingProperty] = [];
762
763
  });
763
764
  break;
764
765
  default:
765
766
  dreams.forEach((dream) => {
766
- dream[associationToGetterSetterProp(association)] = null;
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[association.as].push(preloadedDreamAndWhatItPointsTo.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[association.as] ||= preloadedDreamAndWhatItPointsTo.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[association.as]));
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
+ }
@@ -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
- try {
3353
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
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
- else
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: dreamWithLoadedAssociations[associationName],
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[association.as] = [];
762
+ dream[associationBackingProperty] = [];
762
763
  });
763
764
  break;
764
765
  default:
765
766
  dreams.forEach((dream) => {
766
- dream[associationToGetterSetterProp(association)] = null;
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[association.as].push(preloadedDreamAndWhatItPointsTo.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[association.as] ||= preloadedDreamAndWhatItPointsTo.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[association.as]));
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
+ }
@@ -52,4 +52,5 @@ export default class LeftJoinLoadBuilder<DreamInstance extends Dream> {
52
52
  */
53
53
  execute(): Promise<DreamInstance>;
54
54
  private associationToPreloadedDreamsAndWhatTheyPointTo;
55
+ private leftJoinLoadedAssociationValue;
55
56
  }
@@ -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>;
@@ -224,6 +224,36 @@ export type PassthroughOnClauseKeys<
224
224
  : Association['passthroughAndClauses' & keyof Association] & (string[] | null),
225
225
  > = PassthroughOnClauses
226
226
 
227
+ /**
228
+ * Resolves to `true` when the named association on the given table is a
229
+ * non-optional (required) BelongsTo, and `false` otherwise. A required
230
+ * BelongsTo getter is typed non-null and that non-null type flows through
231
+ * serializers into the generated OpenAPI spec as a non-nullable field, so a
232
+ * load-time constraint that could filter the parent out (nulling the value)
233
+ * must be forbidden at compile time for the hydrating load variants.
234
+ */
235
+ export type IsNonOptionalBelongsToAssociation<
236
+ Schema,
237
+ TableName,
238
+ AssociationName,
239
+ Associations = TableName extends null
240
+ ? null
241
+ : TableName extends keyof Schema & string
242
+ ? Schema[TableName]['associations' & keyof Schema[TableName]]
243
+ : null,
244
+ Association = Associations extends null
245
+ ? null
246
+ : AssociationName extends keyof Associations
247
+ ? Associations[AssociationName]
248
+ : null,
249
+ > = Association extends null
250
+ ? false
251
+ : Association['type' & keyof Association] extends 'BelongsTo'
252
+ ? Association['optional' & keyof Association] extends false
253
+ ? true
254
+ : false
255
+ : false
256
+
227
257
  export type DreamAssociationNames<
228
258
  DreamInstance extends Dream,
229
259
  SchemaAssociations = DreamAssociationMetadata<DreamInstance>,