// // Copyright Amazon.com Inc. or its affiliates. // All Rights Reserved. // // SPDX-License-Identifier: Apache-2.0 // import Foundation /// Defines the association type between two models. The type of association is /// important when defining how to store and query them. Each association have /// its own rules depending on the storage mechanism. /// /// The semantics of a association can be defined as: /// /// **Many-to-One/One-to-Many** /// /// The most common association type. It defines an array/collection on one side and a /// single `Model` reference on the other. The side with the `Model` (marked as `belongsTo`) /// holds a reference to the other side's `id` (aka "foreign key"). /// /// Example: /// /// ``` /// struct Post: Model { /// let id: Model.Identifier /// /// // hasMany(associatedWith: Comment.keys.post) /// let comments: [Comment] /// } /// /// struct Comment: Model { /// let id: Model.Identifier /// /// // belongsTo /// let post: Post /// } /// ``` /// /// **One-to-One** /// /// This type of association is not too common since in these scenarios data can usually /// be normalized and stored under the same `Model`. However, there are use-cases where /// one-to-one can be useful, specially when one side of the association is optional. /// /// Example: /// /// ``` /// struct Person: Model { /// // hasOne(associatedWith: License.keys.person) /// let license: License? /// } /// /// struct License: Model { /// // belongsTo /// let person: Person /// } /// ``` /// /// **Many-to-Many** /// /// These associations mean that an instance of one `Model` can relate to many other /// instances of another `Model` and vice-versa. Many-to-Many is achieved by combining /// `hasMany` and `belongsTo` with an intermediate `Model` that is responsible for /// holding a reference to the keys of both related models. /// /// ``` /// struct Book: Model { /// // hasMany(associatedWith: BookAuthor.keys.book) /// let auhors: [BookAuthor] /// } /// /// struct Author: Model { /// // hasMany(associatedWith: BookAuthor.keys.author) /// let books: [BookAuthor] /// } /// /// struct BookAuthor: Model { /// // belongsTo /// let book: Book /// /// // belongsTo /// let author: Author /// } /// ``` /// /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. public enum ModelAssociation { case hasMany(associatedFieldName: String?, associatedFieldNames: [String] = []) case hasOne(associatedFieldName: String?, targetNames: [String]) case belongsTo(associatedFieldName: String?, targetNames: [String]) public static let belongsTo: ModelAssociation = .belongsTo(associatedFieldName: nil, targetNames: []) public static func belongsTo(targetName: String? = nil) -> ModelAssociation { let targetNames = targetName.map { [$0] } ?? [] return .belongsTo(associatedFieldName: nil, targetNames: targetNames) } public static func hasMany(associatedWith: CodingKey? = nil, associatedFields: [CodingKey] = []) -> ModelAssociation { return .hasMany(associatedFieldName: associatedWith?.stringValue, associatedFieldNames: associatedFields.map { $0.stringValue }) } @available(*, deprecated, message: "Use hasOne(associatedWith:targetNames:)") public static func hasOne(associatedWith: CodingKey?, targetName: String? = nil) -> ModelAssociation { let targetNames = targetName.map { [$0] } ?? [] return .hasOne(associatedWith: associatedWith, targetNames: targetNames) } public static func hasOne(associatedWith: CodingKey?, targetNames: [String] = []) -> ModelAssociation { return .hasOne(associatedFieldName: associatedWith?.stringValue, targetNames: targetNames) } @available(*, deprecated, message: "Use belongsTo(associatedWith:targetNames:)") public static func belongsTo(associatedWith: CodingKey?, targetName: String?) -> ModelAssociation { let targetNames = targetName.map { [$0] } ?? [] return .belongsTo(associatedFieldName: associatedWith?.stringValue, targetNames: targetNames) } public static func belongsTo(associatedWith: CodingKey?, targetNames: [String] = []) -> ModelAssociation { return .belongsTo(associatedFieldName: associatedWith?.stringValue, targetNames: targetNames) } } extension ModelField { /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var hasAssociation: Bool { return association != nil } /// If the field represents an association returns the `Model.Type`. /// - seealso: `ModelFieldType` /// - seealso: `ModelFieldAssociation` /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. @available(*, deprecated, message: """ Use of associated model type is deprecated, use `associatedModelName` instead. """) public var associatedModel: Model.Type? { switch type { case .model(let modelName), .collection(let modelName): return ModelRegistry.modelType(from: modelName) default: return nil } } /// If the field represents an association returns the `ModelName`. /// - seealso: `ModelFieldType` /// - seealso: `ModelFieldAssociation` /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var associatedModelName: ModelName? { switch type { case .model(let modelName), .collection(let modelName): return modelName default: return nil } } /// This calls `associatedModelName` but enforces that the field must represent an association. /// In case the field type is not a `Model` it calls `preconditionFailure`. Consumers /// should fix their models in order to recover from it, since associations are only /// possible between two `Model`. /// /// - Note: as a maintainer, make sure you use this computed property only when context /// allows (i.e. the field is a valid relationship, such as foreign keys). /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. @available(*, deprecated, message: """ Use of requiredAssociatedModel with Model.Type is deprecated, use `requiredAssociatedModelName` that return ModelName instead. """) public var requiredAssociatedModel: Model.Type { guard let modelType = associatedModel else { return Fatal.preconditionFailure(""" Model fields that are foreign keys must be connected to another Model. Check the `ModelSchema` section of your "\(name)+Schema.swift" file. """) } return modelType } /// This calls `associatedModelName` but enforces that the field must represent an association. /// In case the field type is not a `Model` it calls `preconditionFailure`. Consumers /// should fix their models in order to recover from it, since associations are only /// possible between two `Model`. /// /// - Note: as a maintainer, make sure you use this computed property only when context /// allows (i.e. the field is a valid relationship, such as foreign keys). /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var requiredAssociatedModelName: ModelName { guard let modelName = associatedModelName else { return Fatal.preconditionFailure(""" Model fields that are foreign keys must be connected to another Model. Check the `ModelSchema` section of your "\(name)+Schema.swift" file. """) } return modelName } /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var isAssociationOwner: Bool { guard case .belongsTo = association else { return false } return true } /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var _isBelongsToOrHasOne: Bool { switch association { case .belongsTo, .hasOne: return true case .hasMany, .none: return false } } /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var associatedField: ModelField? { if hasAssociation { let associatedModel = requiredAssociatedModelName switch association { case .belongsTo(let associatedKey, _), .hasOne(let associatedKey, _), .hasMany(let associatedKey, _): // TODO handle modelName casing (convert to camelCase) let key = associatedKey ?? associatedModel let schema = ModelRegistry.modelSchema(from: associatedModel) return schema?.field(withName: key) case .none: return nil } } return nil } /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var associatedFieldNames: [String] { switch association { case .hasMany(let associatedKey, let associatedKeys): if associatedKeys.isEmpty, let associatedKey = associatedKey { return [associatedKey] } return associatedKeys case .hasOne, .belongsTo: return ModelRegistry.modelSchema(from: requiredAssociatedModelName)? .primaryKey .fields .map(\.name) ?? [] case .none: return [] } } /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var isOneToOne: Bool { if case .hasOne = association { return true } if case .belongsTo = association, case .hasOne = associatedField?.association { return true } return false } /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var isOneToMany: Bool { if case .hasMany = association, case .belongsTo = associatedField?.association { return true } return false } /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var isManyToOne: Bool { if case .belongsTo = association, case .hasMany = associatedField?.association { return true } return false } /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. @available(*, deprecated, message: """ Use `embeddedType` is deprecated, use `embeddedTypeSchema` instead. """) public var embeddedType: Embeddable.Type? { switch type { case .embedded(let type, _), .embeddedCollection(let type, _): if let embeddedType = type as? Embeddable.Type { return embeddedType } return nil default: return nil } } /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var embeddedTypeSchema: ModelSchema? { switch type { case .embedded(_, let modelSchema), .embeddedCollection(_, let modelSchema): return modelSchema default: return nil } } /// - Warning: Although this has `public` access, it is intended for internal & codegen use and should not be used /// directly by host applications. The behavior of this may change without warning. Though it is not used by host /// application making any change to these `public` types should be backward compatible, otherwise it will be a /// breaking change. public var isEmbeddedType: Bool { switch type { case .embedded, .embeddedCollection: return true default: return false } } }