diff --git a/README.md b/README.md index 2519ae6..3570739 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Although primarily in-memory, SwiftletModel’s data model is Codable, allowing * [Model Definitions](#model-definitions) - [How to Save Entities](#how-to-save-entities) - [How to Delete Entities](#how-to-delete-entities) - * [How to cascade delete](#how-to-cascade-delete) + * [Relationship DeleteRule](#relationship-deleterule) - [How to Query Entities](#how-to-query-entities) * [Query with nested models](#query-with-nested-models) * [Related models query](#related-models-query) @@ -219,22 +219,22 @@ chat.delete(from: &context) Calling `delete(...)` will  - remove the current instance from the context -- it will nullify all relations +- it will nullify all relations or cascade delete depending on `DeleteRule` attribute - call `willDelete(...)` and `didDelete(...)` callbacks when needed. -### How to cascade delete +### Relationship DeleteRule -There is a `willDelete(...)` callback that can be utilized for cascade deletion implementation: +DeleteRule allows to specify how the related entities would be treated when current entity is deleted: +- nullify (the default option) +- cascade ```swift -extension Message { - func willDelete(from context: inout Context) throws { - try delete(\.$attachment, inverse: \.$message, from: &context) - } -} + +@Relationship(deleteRule: .cascade, inverse: \.message) +var attachment: Attachment? + ``` -The method is throwing to be able to perform some additional checks before deletion  -and throw an error if something has gone wrong. + ## How to Query Entities diff --git a/Sources/SwiftletModel/Relationship/RelationTypes.swift b/Sources/SwiftletModel/Relationship/RelationTypes.swift index 9b8ae81..7c12784 100644 --- a/Sources/SwiftletModel/Relationship/RelationTypes.swift +++ b/Sources/SwiftletModel/Relationship/RelationTypes.swift @@ -40,6 +40,13 @@ public typealias ToManyRelation { - init(inverse: KeyPath) { + init(deleteRule: Relations.DeleteRule = .nullify, + inverse: KeyPath) { self.init(relation: .none) } } @@ -48,6 +49,7 @@ public extension Relationship where Directionality == Relations.Mutual, Cardinality == Relations.ToOne { init(_ constraint: Constraint, + deleteRule: Relations.DeleteRule = .nullify, inverse: KeyPath) { self.init(relation: .none) } @@ -57,7 +59,8 @@ public extension Relationship where Directionality == Relations.Mutual, Constraints == Relations.Required, Cardinality == Relations.ToMany { - init(inverse: KeyPath) { + init(deleteRule: Relations.DeleteRule = .nullify, + inverse: KeyPath) { self.init(relation: .none) } } @@ -77,7 +80,8 @@ public extension Relationship where Directionality == Relations.OneWay, public extension Relationship where Directionality == Relations.OneWay, Cardinality == Relations.ToOne { - init(_ constraint: Constraint = .optional) { + init(_ constraint: Constraint = .optional, + deleteRule: Relations.DeleteRule = .nullify) { self.init(relation: .none) } } @@ -86,7 +90,7 @@ public extension Relationship where Directionality == Relations.OneWay, Cardinality == Relations.ToMany, Constraints == Relations.Required { - init() { + init(deleteRule: Relations.DeleteRule = .nullify) { self.init(relation: .none) } diff --git a/Sources/SwiftletModelMacros/EntityModelMacro.swift b/Sources/SwiftletModelMacros/EntityModelMacro.swift index 531472b..df50ef5 100644 --- a/Sources/SwiftletModelMacros/EntityModelMacro.swift +++ b/Sources/SwiftletModelMacros/EntityModelMacro.swift @@ -44,7 +44,7 @@ public extension EntityModelMacro { relationshipAttributes: relationshipAttributes, optionalProperties: optionalProperties ) - + return [syntax] } } @@ -105,7 +105,14 @@ extension FunctionDeclSyntax { try willDelete(from: &context) context.remove(Self.self, id: id) \(raw: attributes - .map { "detach(\($0.keyPathAttributes.attribute), in: &context)" } + .map { + switch $0.deleteRule { + case .nullify: + "detach(\($0.keyPathAttributes.attribute), in: &context)" + case .cascade: + "try delete(\($0.keyPathAttributes.attribute), from: &context)" + } + } .joined(separator: "\n") ) try didDelete(from: &context) @@ -199,7 +206,8 @@ private extension VariableDeclSyntax { propertyName: property, keyPathAttributes: RelationshipAttributes.KeyPathAttributes( propertyIdentifier: property - ) + ), + deleteRule: .nullify ) } @@ -209,12 +217,13 @@ private extension VariableDeclSyntax { keyPathAttributes: RelationshipAttributes.KeyPathAttributes( propertyIdentifier: property, labeledExprListSyntax: keyPathsExprList - ) + ), + deleteRule: RelationshipAttributes.DeleteRuleAttribute(labeledExprListSyntax: keyPathsExprList) ) } return nil } - + func optionalPropertiesAttributes() -> PropertyAttributes? { for attribute in attributes { diff --git a/Sources/SwiftletModelMacros/RelationshipAttributes.swift b/Sources/SwiftletModelMacros/RelationshipAttributes.swift index 662ba8d..6494776 100644 --- a/Sources/SwiftletModelMacros/RelationshipAttributes.swift +++ b/Sources/SwiftletModelMacros/RelationshipAttributes.swift @@ -19,21 +19,43 @@ struct RelationshipAttributes { let relationWrapperType: WrapperType let propertyName: String let keyPathAttributes: KeyPathAttributes + let deleteRule: DeleteRuleAttribute } extension RelationshipAttributes { + enum WrapperType: String, CaseIterable { case relationship = "Relationship" var title: String { rawValue } - - static let allCasesTitleSet: Set = { - Set(Self.allCases.map { $0.title }) - }() } + enum DeleteRuleAttribute: String, CaseIterable { + static let deleteRule = "deleteRule" + + case cascade + case nullify + + init?(_ expressionString: String) { + let value = Self.allCases.first { expressionString.contains($0.rawValue) } + guard let value else { + return nil + } + + self = value + } + + init(labeledExprListSyntax: LabeledExprListSyntax) { + self = labeledExprListSyntax + .filter { $0.labelString?.contains(DeleteRuleAttribute.deleteRule) ?? false } + .compactMap { DeleteRuleAttribute($0.expressionString) } + .first ?? .nullify + } + } + + enum KeyPathAttributes { case labeledExpressionList(String) case propertyIdentifier(String) diff --git a/Tests/SwiftletModelTests/Models/Message.swift b/Tests/SwiftletModelTests/Models/Message.swift index 31354a4..1c07182 100644 --- a/Tests/SwiftletModelTests/Models/Message.swift +++ b/Tests/SwiftletModelTests/Models/Message.swift @@ -19,7 +19,7 @@ struct Message: Codable, Sendable { @Relationship(inverse: \.messages) var chat: Chat? - @Relationship(inverse: \.message) + @Relationship(deleteRule: .cascade, inverse: \.message) var attachment: Attachment? @Relationship(inverse: \.replyTo) @@ -30,10 +30,6 @@ struct Message: Codable, Sendable { @Relationship var viewedBy: [User]? = nil - - func willDelete(from context: inout Context) throws { - try delete(\.$attachment, inverse: \.$message, from: &context) - } } extension Query where Entity == Message {