Skip to content

Commit

Permalink
Delete rule for related entities (#59)
Browse files Browse the repository at this point in the history
* added delete rule attribute to Relationship

* updated macro to support delete rule attributes

* updated test models

* updated readme
  • Loading branch information
KazaiMazai authored Aug 16, 2024
1 parent 4c26623 commit 600fc72
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 29 deletions.
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions Sources/SwiftletModel/Relationship/RelationTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ public typealias ToManyRelation<T: EntityModelProtocol,

public enum Relations { }

public extension Relations {
enum DeleteRule {
case cascade
case nullify
}
}

//MARK: - Directionality

public protocol DirectionalityProtocol { }
Expand Down
12 changes: 8 additions & 4 deletions Sources/SwiftletModel/Relationship/Relationship.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public extension Relationship where Directionality == Relations.Mutual,
Constraints == Relations.Optional,
Cardinality == Relations.ToOne<Entity> {

init<EnclosingType>(inverse: KeyPath<Entity, EnclosingType?>) {
init<EnclosingType>(deleteRule: Relations.DeleteRule = .nullify,
inverse: KeyPath<Entity, EnclosingType?>) {
self.init(relation: .none)
}
}
Expand All @@ -48,6 +49,7 @@ public extension Relationship where Directionality == Relations.Mutual,
Cardinality == Relations.ToOne<Entity> {

init<EnclosingType>(_ constraint: Constraint<Constraints>,
deleteRule: Relations.DeleteRule = .nullify,
inverse: KeyPath<Entity, EnclosingType?>) {
self.init(relation: .none)
}
Expand All @@ -57,7 +59,8 @@ public extension Relationship where Directionality == Relations.Mutual,
Constraints == Relations.Required,
Cardinality == Relations.ToMany<Entity> {

init<EnclosingType>(inverse: KeyPath<Entity, EnclosingType?>) {
init<EnclosingType>(deleteRule: Relations.DeleteRule = .nullify,
inverse: KeyPath<Entity, EnclosingType?>) {
self.init(relation: .none)
}
}
Expand All @@ -77,7 +80,8 @@ public extension Relationship where Directionality == Relations.OneWay,
public extension Relationship where Directionality == Relations.OneWay,
Cardinality == Relations.ToOne<Entity> {

init(_ constraint: Constraint<Constraints> = .optional) {
init(_ constraint: Constraint<Constraints> = .optional,
deleteRule: Relations.DeleteRule = .nullify) {
self.init(relation: .none)
}
}
Expand All @@ -86,7 +90,7 @@ public extension Relationship where Directionality == Relations.OneWay,
Cardinality == Relations.ToMany<Entity>,
Constraints == Relations.Required {

init() {
init(deleteRule: Relations.DeleteRule = .nullify) {
self.init(relation: .none)
}

Expand Down
19 changes: 14 additions & 5 deletions Sources/SwiftletModelMacros/EntityModelMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public extension EntityModelMacro {
relationshipAttributes: relationshipAttributes,
optionalProperties: optionalProperties
)

return [syntax]
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -199,7 +206,8 @@ private extension VariableDeclSyntax {
propertyName: property,
keyPathAttributes: RelationshipAttributes.KeyPathAttributes(
propertyIdentifier: property
)
),
deleteRule: .nullify
)
}

Expand All @@ -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 {

Expand Down
30 changes: 26 additions & 4 deletions Sources/SwiftletModelMacros/RelationshipAttributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = {
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)
Expand Down
6 changes: 1 addition & 5 deletions Tests/SwiftletModelTests/Models/Message.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down

0 comments on commit 600fc72

Please sign in to comment.