Skip to content

Commit e4c5736

Browse files
diegocstnlawmichamanaswi223
authored
feat(datastore): support for custom primary key (#1752)
* feat(datastore): custom pk * chore: update API calls * address comments * address comments, update models * update pk encoding * fix(datastore): create indexes for custom primary key (#1828) * precondition failure in case of identifier field not found * test(datastore): custom pk tests (#1799) * test(datastore): custom PK tests * update models * more SQL statements tests * add CascadeDeleteOperation tests with composite pk * address comments * feat(datastore): custom pk associations (#1811) * test(datastore): custom PK tests * update models * update pk encoding * fix associations * feat(datastore): support for CPK in associations * restore associations test models * rebase on tests * feat(datastore): support for CPK in associations * add more tests * add schema for test models; * rename schema.graphql to avoid build failures * address comments * fix tests when pk field is enum * tests with composite keys * resolve associated model * update test models * feat(datastore): custom primary key - add more tests, fix cascade delete (#1878) * feat(datastore): add more tests, fix cascade delete * fix typo * precondition failure only when associated field is required * add tests for query predicate * fix(datastore): avoid creating index on foreign key fields (#1937) * fix(datastore): resolve PK value in JSONValue models (#1949) * fix quoting on table names in unit test * fix(datastore): Fix ModelSyncedEventEmitter, add lifecycle events logging (#2022) * try to fix ready event emitter * lifecycle event logging * fix(datastore): Add default value for where predicate (#2041) * fix(datastore): Add default value for where predicate * Indentation fixtures for where predicate * Fix-final changes * fix(datastore): String conform to ModelIdentifierProtocol (#2067) Co-authored-by: Michael Law <[email protected]> Co-authored-by: Manaswi Manthena <[email protected]>
1 parent b8fd918 commit e4c5736

File tree

121 files changed

+4850
-417
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+4850
-417
lines changed

Amplify.xcodeproj/project.pbxproj

Lines changed: 196 additions & 48 deletions
Large diffs are not rendered by default.

Amplify/Categories/DataStore/DataStoreCategory+Behavior+Combine.swift

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public extension DataStoreBaseBehavior {
3131
/// - modelType: The type of the model to delete
3232
/// - id: The ID of the model to delete
3333
/// - Returns: A DataStorePublisher with the results of the operation
34+
@available(*, deprecated, message: "Use delete(:withIdentifier:where:)")
3435
func delete<M: Model>(
3536
_ modelType: M.Type,
3637
withId id: String,
@@ -41,6 +42,40 @@ public extension DataStoreBaseBehavior {
4142
}.eraseToAnyPublisher()
4243
}
4344

45+
/// Deletes the model with the specified identifier from the DataStore. If sync is enabled, this will delete the
46+
/// model from the remote store as well.
47+
///
48+
/// - Parameters:
49+
/// - modelType: The type of the model to delete
50+
/// - identifier: The identifier of the model to delete
51+
/// - Returns: A DataStorePublisher with the results of the operation
52+
func delete<M: Model>(
53+
_ modelType: M.Type,
54+
withIdentifier identifier: String,
55+
where predicate: QueryPredicate? = nil
56+
) -> DataStorePublisher<Void> where M: ModelIdentifiable, M.IdentifierFormat == ModelIdentifierFormat.Default {
57+
Future { promise in
58+
self.delete(modelType, withIdentifier: identifier, where: predicate) { promise($0) }
59+
}.eraseToAnyPublisher()
60+
}
61+
62+
/// Deletes the model with the specified identifier from the DataStore. If sync is enabled, this will delete the
63+
/// model from the remote store as well.
64+
///
65+
/// - Parameters:
66+
/// - modelType: The type of the model to delete
67+
/// - identifier: The identifier of the model to delete
68+
/// - Returns: A DataStorePublisher with the results of the operation
69+
func delete<M: Model>(
70+
_ modelType: M.Type,
71+
withIdentifier identifier: ModelIdentifier<M, M.IdentifierFormat>,
72+
where predicate: QueryPredicate? = nil
73+
) -> DataStorePublisher<Void> where M: ModelIdentifiable {
74+
Future { promise in
75+
self.delete(modelType, withIdentifier: identifier, where: predicate) { promise($0) }
76+
}.eraseToAnyPublisher()
77+
}
78+
4479
/// Deletes models matching the `predicate` from the DataStore. If sync is enabled, this will delete the
4580
/// model from the remote store as well.
4681
///
@@ -79,6 +114,7 @@ public extension DataStoreBaseBehavior {
79114
/// - modelType: The type of the model to query
80115
/// - id: The ID of the model to query
81116
/// - Returns: A DataStorePublisher with the results of the operation
117+
@available(*, deprecated, message: "Use query(:byIdentifier:)")
82118
func query<M: Model>(
83119
_ modelType: M.Type,
84120
byId id: String
@@ -88,6 +124,36 @@ public extension DataStoreBaseBehavior {
88124
}.eraseToAnyPublisher()
89125
}
90126

127+
/// Queries for a specific model instance by id
128+
///
129+
/// - Parameters:
130+
/// - modelType: The type of the model to query
131+
/// - id: The ID of the model to query
132+
/// - Returns: A DataStorePublisher with the results of the operation
133+
func query<M: Model>(
134+
_ modelType: M.Type,
135+
byIdentifier identifier: String
136+
) -> DataStorePublisher<M?> where M: ModelIdentifiable, M.IdentifierFormat == ModelIdentifierFormat.Default {
137+
Future { promise in
138+
self.query(modelType, byIdentifier: identifier) { promise($0) }
139+
}.eraseToAnyPublisher()
140+
}
141+
142+
/// Queries for a specific model instance by id
143+
///
144+
/// - Parameters:
145+
/// - modelType: The type of the model to query
146+
/// - id: The ID of the model to query
147+
/// - Returns: A DataStorePublisher with the results of the operation
148+
func query<M: Model>(
149+
_ modelType: M.Type,
150+
byIdentifier identifier: ModelIdentifier<M, M.IdentifierFormat>
151+
) -> DataStorePublisher<M?> where M: ModelIdentifiable {
152+
Future { promise in
153+
self.query(modelType, byIdentifier: identifier) { promise($0) }
154+
}.eraseToAnyPublisher()
155+
}
156+
91157
/// Queries for any model instances that match the specified predicate
92158
///
93159
/// - Parameters:

Amplify/Categories/DataStore/DataStoreCategory+Behavior.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ extension DataStoreCategory: DataStoreBaseBehavior {
1818
plugin.query(modelType, byId: id, completion: completion)
1919
}
2020

21+
public func query<M: Model>(_ modelType: M.Type,
22+
byIdentifier id: String,
23+
completion: (DataStoreResult<M?>) -> Void) where M: ModelIdentifiable,
24+
M.IdentifierFormat == ModelIdentifierFormat.Default {
25+
plugin.query(modelType, byIdentifier: id, completion: completion)
26+
}
27+
28+
public func query<M: Model>(_ modelType: M.Type,
29+
byIdentifier identifier: ModelIdentifier<M, M.IdentifierFormat>,
30+
completion: (DataStoreResult<M?>) -> Void) where M: ModelIdentifiable {
31+
plugin.query(modelType, byIdentifier: identifier, completion: completion)
32+
}
33+
2134
public func query<M: Model>(_ modelType: M.Type,
2235
where predicate: QueryPredicate? = nil,
2336
sort sortInput: QuerySortInput? = nil,
@@ -39,6 +52,22 @@ extension DataStoreCategory: DataStoreBaseBehavior {
3952
plugin.delete(modelType, withId: id, where: predicate, completion: completion)
4053
}
4154

55+
public func delete<M: Model>(_ modelType: M.Type,
56+
withIdentifier id: String,
57+
where predicate: QueryPredicate? = nil,
58+
completion: @escaping DataStoreCallback<Void>) where M: ModelIdentifiable,
59+
M.IdentifierFormat == ModelIdentifierFormat.Default {
60+
61+
plugin.delete(modelType, withIdentifier: id, where: predicate, completion: completion)
62+
}
63+
64+
public func delete<M: Model>(_ modelType: M.Type,
65+
withIdentifier id: ModelIdentifier<M, M.IdentifierFormat>,
66+
where predicate: QueryPredicate? = nil,
67+
completion: @escaping DataStoreCallback<Void>) where M: ModelIdentifiable {
68+
plugin.delete(modelType, withIdentifier: id, where: predicate, completion: completion)
69+
}
70+
4271
public func delete<M: Model>(_ modelType: M.Type,
4372
where predicate: QueryPredicate,
4473
completion: @escaping DataStoreCallback<Void>) {

Amplify/Categories/DataStore/DataStoreCategoryBehavior.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,20 @@ public protocol DataStoreBaseBehavior {
1616
where condition: QueryPredicate?,
1717
completion: @escaping DataStoreCallback<M>)
1818

19+
@available(*, deprecated, message: "Use query(:byIdentifier:completion)")
1920
func query<M: Model>(_ modelType: M.Type,
2021
byId id: String,
2122
completion: DataStoreCallback<M?>)
2223

24+
func query<M: Model>(_ modelType: M.Type,
25+
byIdentifier id: String,
26+
completion: DataStoreCallback<M?>) where M: ModelIdentifiable,
27+
M.IdentifierFormat == ModelIdentifierFormat.Default
28+
29+
func query<M: Model>(_ modelType: M.Type,
30+
byIdentifier id: ModelIdentifier<M, M.IdentifierFormat>,
31+
completion: DataStoreCallback<M?>) where M: ModelIdentifiable
32+
2333
func query<M: Model>(_ modelType: M.Type,
2434
where predicate: QueryPredicate?,
2535
sort sortInput: QuerySortInput?,
@@ -30,14 +40,26 @@ public protocol DataStoreBaseBehavior {
3040
where predicate: QueryPredicate?,
3141
completion: @escaping DataStoreCallback<Void>)
3242

43+
@available(*, deprecated, message: "Use delete(:withIdentifier:where:completion)")
3344
func delete<M: Model>(_ modelType: M.Type,
3445
withId id: String,
3546
where predicate: QueryPredicate?,
3647
completion: @escaping DataStoreCallback<Void>)
3748

3849
func delete<M: Model>(_ modelType: M.Type,
39-
where predicate: QueryPredicate,
40-
completion: @escaping DataStoreCallback<Void>)
50+
withIdentifier id: String,
51+
where predicate: QueryPredicate?,
52+
completion: @escaping DataStoreCallback<Void>) where M: ModelIdentifiable,
53+
M.IdentifierFormat == ModelIdentifierFormat.Default
54+
55+
func delete<M: Model>(_ modelType: M.Type,
56+
withIdentifier id: ModelIdentifier<M, M.IdentifierFormat>,
57+
where predicate: QueryPredicate?,
58+
completion: @escaping DataStoreCallback<Void>) where M: ModelIdentifiable
59+
60+
func delete<M: Model>(_ modelType: M.Type,
61+
where predicate: QueryPredicate,
62+
completion: @escaping DataStoreCallback<Void>)
4163

4264
/**
4365
Synchronization starts automatically whenever you run any DataStore operation (query(), save(), delete())

Amplify/Categories/DataStore/Model/Internal/Schema/ModelField+Association.swift

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,25 +88,38 @@ import Foundation
8888
/// directly by host applications. The behavior of this may change without warning.
8989
public enum ModelAssociation {
9090
case hasMany(associatedFieldName: String?)
91-
case hasOne(associatedFieldName: String?, targetName: String? = nil)
92-
case belongsTo(associatedFieldName: String?, targetName: String?)
91+
case hasOne(associatedFieldName: String?, targetNames: [String])
92+
case belongsTo(associatedFieldName: String?, targetNames: [String])
9393

94-
public static let belongsTo: ModelAssociation = .belongsTo(associatedFieldName: nil, targetName: nil)
94+
public static let belongsTo: ModelAssociation = .belongsTo(associatedFieldName: nil, targetNames: [])
9595

9696
public static func belongsTo(targetName: String? = nil) -> ModelAssociation {
97-
return .belongsTo(associatedFieldName: nil, targetName: nil)
97+
let targetNames = targetName.map { [$0] } ?? []
98+
return .belongsTo(associatedFieldName: nil, targetNames: targetNames)
9899
}
99100

100101
public static func hasMany(associatedWith: CodingKey?) -> ModelAssociation {
101102
return .hasMany(associatedFieldName: associatedWith?.stringValue)
102103
}
103104

105+
@available(*, deprecated, message: "Use hasOne(associatedWith:targetNames:)")
104106
public static func hasOne(associatedWith: CodingKey?, targetName: String? = nil) -> ModelAssociation {
105-
return .hasOne(associatedFieldName: associatedWith?.stringValue, targetName: targetName)
107+
let targetNames = targetName.map { [$0] } ?? []
108+
return .hasOne(associatedWith: associatedWith, targetNames: targetNames)
106109
}
107110

111+
public static func hasOne(associatedWith: CodingKey?, targetNames: [String] = []) -> ModelAssociation {
112+
return .hasOne(associatedFieldName: associatedWith?.stringValue, targetNames: targetNames)
113+
}
114+
115+
@available(*, deprecated, message: "Use belongsTo(associatedWith:targetNames:)")
108116
public static func belongsTo(associatedWith: CodingKey?, targetName: String?) -> ModelAssociation {
109-
return .belongsTo(associatedFieldName: associatedWith?.stringValue, targetName: targetName)
117+
let targetNames = targetName.map { [$0] } ?? []
118+
return .belongsTo(associatedFieldName: associatedWith?.stringValue, targetNames: targetNames)
119+
}
120+
121+
public static func belongsTo(associatedWith: CodingKey?, targetNames: [String] = []) -> ModelAssociation {
122+
return .belongsTo(associatedFieldName: associatedWith?.stringValue, targetNames: targetNames)
110123
}
111124

112125
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
public struct ModelPrimaryKey {
9+
public var fields: [ModelField] = []
10+
private var fieldsLookup: Set<ModelFieldName> = []
11+
12+
public var isCompositeKey: Bool {
13+
fields.count > 1
14+
}
15+
16+
init?(allFields: ModelFields,
17+
attributes: [ModelAttribute],
18+
primaryKeyFieldKeys: [String] = []) {
19+
self.fields = resolvePrimaryKeyFields(allFields: allFields,
20+
attributes: attributes,
21+
primaryKeyFieldKeys: primaryKeyFieldKeys)
22+
23+
if fields.isEmpty {
24+
return nil
25+
}
26+
27+
self.fieldsLookup = Set(fields.map { $0.name })
28+
}
29+
30+
/// Returns the list of fields that make up the primary key for the model.
31+
/// In case of a custom primary key, the model has a `@key` directive
32+
/// without a name and at least 1 field
33+
func primaryFieldsFromIndexes(attributes: [ModelAttribute]) -> [ModelFieldName]? {
34+
attributes.compactMap {
35+
if case let .index(fields, name) = $0, name == nil, fields.count >= 1 {
36+
return fields
37+
}
38+
return nil
39+
}.first
40+
}
41+
42+
/// Resolve the model fields that are part of the primary key.
43+
/// For backward compatibility with different versions of the codegen,
44+
/// the algorithm tries first to resolve them using first the `primaryKeyFields`
45+
/// received from `primaryKey` member set in `ModelSchemaDefinition`,
46+
/// if not available tries to infer the fields using the `.indexes` and it eventually
47+
/// falls back on the `.primaryKey` attribute.
48+
///
49+
/// It returns an array of fields as custom and composite primary keys are supported.
50+
/// - Parameter fields: schema model fields
51+
/// - Returns: an array of model fields
52+
func resolvePrimaryKeyFields(allFields: ModelFields,
53+
attributes: [ModelAttribute],
54+
primaryKeyFieldKeys: [String]) -> [ModelField] {
55+
var primaryKeyFields: [ModelField] = []
56+
57+
if !primaryKeyFieldKeys.isEmpty {
58+
primaryKeyFields = primaryKeyFieldKeys.map {
59+
guard let field = allFields[$0] else {
60+
preconditionFailure("Primary key field named (\($0)) not found in schema fields.")
61+
}
62+
return field
63+
}
64+
65+
/// if indexes aren't defined most likely the model has a default `id` as PK
66+
/// so we have to rely on the `.primaryKey` attribute of each individual field
67+
} else if attributes.indexes.filter({ $0.isPrimaryKeyIndex }).isEmpty {
68+
primaryKeyFields = allFields.values.filter { $0.isPrimaryKey }
69+
70+
/// Use the array of fields with a primary key index
71+
} else if let fieldNames = primaryFieldsFromIndexes(attributes: attributes) {
72+
primaryKeyFields = fieldNames.compactMap {
73+
if let field = allFields[$0] {
74+
return field
75+
}
76+
return nil
77+
}
78+
}
79+
return primaryKeyFields
80+
}
81+
82+
/// Convenience method to verify if a field is part of the primary key
83+
/// - Parameter name: field name
84+
/// - Returns: true if the field is part of the primary key
85+
public func contains(named name: ModelFieldName) -> Bool {
86+
fieldsLookup.contains(name)
87+
}
88+
89+
/// Returns the first index in which a model field name of the collection
90+
/// is equal to the provided `name`
91+
/// Returns `nil` if no element was found.
92+
public func indexOfField(named name: ModelFieldName) -> Int? {
93+
fields.firstIndex(where: { $0.name == name })
94+
}
95+
}

0 commit comments

Comments
 (0)