Skip to content

Commit cd4874e

Browse files
authored
feat(datastore): support for readonly fields (#1133)
* feat(datastore): support createdAt and updatedAt model fields * feat(datastore): readonly fields tests * feat(datastore): fix typo in docs * feat(datastore): support createdAt and updatedAt model fields * update tests * hasOne and delete mutation tests * hasOne and update mutation tests * feat(datastore): use bool instead of enum for readonly fields * query tests * add comment, rename method name to make it more explicit
1 parent 58b8f46 commit cd4874e

File tree

14 files changed

+390
-8
lines changed

14 files changed

+390
-8
lines changed

Amplify.xcodeproj/project.pbxproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,10 @@
228228
6BEE081D2533CCFA00133961 /* OGCScenarioBPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BEE081B2533CCFA00133961 /* OGCScenarioBPost.swift */; };
229229
6BEE08242533D30800133961 /* OGCScenarioBMGroupPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BEE08222533D30800133961 /* OGCScenarioBMGroupPost.swift */; };
230230
6BEE08252533D30800133961 /* OGCScenarioBMGroupPost+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BEE08232533D30800133961 /* OGCScenarioBMGroupPost+Schema.swift */; };
231+
762167CC261542F70033FCD2 /* Record.swift in Sources */ = {isa = PBXBuildFile; fileRef = 762167CB261542F70033FCD2 /* Record.swift */; };
232+
762167D52615435C0033FCD2 /* Record+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 762167D42615435C0033FCD2 /* Record+Schema.swift */; };
233+
762C978526210F6400798FA3 /* RecordCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 762C978426210F6400798FA3 /* RecordCover.swift */; };
234+
762C978E26210FF100798FA3 /* RecordCover+Schema.swift in Sources */ = {isa = PBXBuildFile; fileRef = 762C978D26210FF100798FA3 /* RecordCover+Schema.swift */; };
231235
7678B38426017D5300B4917F /* AppSyncErrorTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7678B38326017D5300B4917F /* AppSyncErrorTypeTests.swift */; };
232236
7678B38526017D5300B4917F /* AppSyncErrorTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7678B38326017D5300B4917F /* AppSyncErrorTypeTests.swift */; };
233237
7D5ED6C78E25246DDAF2F2EC /* Pods_Amplify.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84F3A76FB68CEFA45F4BB1BB /* Pods_Amplify.framework */; platformFilter = ios; };
@@ -1091,6 +1095,10 @@
10911095
71B6F506E5F3F00B0743A9BF /* Pods-Amplify-AWSPluginsCore-AWSPluginsTestConfigs-AWSPluginsTestCommon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Amplify-AWSPluginsCore-AWSPluginsTestConfigs-AWSPluginsTestCommon.debug.xcconfig"; path = "Target Support Files/Pods-Amplify-AWSPluginsCore-AWSPluginsTestConfigs-AWSPluginsTestCommon/Pods-Amplify-AWSPluginsCore-AWSPluginsTestConfigs-AWSPluginsTestCommon.debug.xcconfig"; sourceTree = "<group>"; };
10921096
73C2E5FA55C85539AD9E39EE /* Pods_Amplify_AWSPluginsCore_CoreMLPredictionsPlugin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Amplify_AWSPluginsCore_CoreMLPredictionsPlugin.framework; sourceTree = BUILT_PRODUCTS_DIR; };
10931097
7503342A8C13588E2B0DDBDE /* Pods-AmplifyFunctionalTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AmplifyFunctionalTests.debug.xcconfig"; path = "Target Support Files/Pods-AmplifyFunctionalTests/Pods-AmplifyFunctionalTests.debug.xcconfig"; sourceTree = "<group>"; };
1098+
762167CB261542F70033FCD2 /* Record.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Record.swift; sourceTree = "<group>"; };
1099+
762167D42615435C0033FCD2 /* Record+Schema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Record+Schema.swift"; sourceTree = "<group>"; };
1100+
762C978426210F6400798FA3 /* RecordCover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordCover.swift; sourceTree = "<group>"; };
1101+
762C978D26210FF100798FA3 /* RecordCover+Schema.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RecordCover+Schema.swift"; sourceTree = "<group>"; };
10941102
7678B38326017D5300B4917F /* AppSyncErrorTypeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSyncErrorTypeTests.swift; sourceTree = "<group>"; };
10951103
77A2D125114EA0FFA11A7EFD /* Pods-AmplifyTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AmplifyTests.debug.xcconfig"; path = "Target Support Files/Pods-AmplifyTests/Pods-AmplifyTests.debug.xcconfig"; sourceTree = "<group>"; };
10961104
7A238096BAB328655B5E388F /* Pods-Amplify-AWSPluginsCore-CoreMLPredictionsPlugin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Amplify-AWSPluginsCore-CoreMLPredictionsPlugin.debug.xcconfig"; path = "Target Support Files/Pods-Amplify-AWSPluginsCore-CoreMLPredictionsPlugin/Pods-Amplify-AWSPluginsCore-CoreMLPredictionsPlugin.debug.xcconfig"; sourceTree = "<group>"; };
@@ -2888,6 +2896,10 @@
28882896
6B7743D625906F7E001469F5 /* Restaurant */,
28892897
6B9F7C542526864800F1F71C /* ScenarioATest6Post.swift */,
28902898
6B9F7C532526864800F1F71C /* ScenarioATest6Post+Schema.swift */,
2899+
762167CB261542F70033FCD2 /* Record.swift */,
2900+
762167D42615435C0033FCD2 /* Record+Schema.swift */,
2901+
762C978426210F6400798FA3 /* RecordCover.swift */,
2902+
762C978D26210FF100798FA3 /* RecordCover+Schema.swift */,
28912903
B952182F237E21B900F53237 /* schema.graphql */,
28922904
6BE9D6E725A6620B00AB5C9A /* TeamProject */,
28932905
214F49742486D8A200DA616C /* User.swift */,
@@ -5238,6 +5250,8 @@
52385250
buildActionMask = 2147483647;
52395251
files = (
52405252
214F49CD24898E8500DA616C /* Article.swift in Sources */,
5253+
762167CC261542F70033FCD2 /* Record.swift in Sources */,
5254+
762167D52615435C0033FCD2 /* Record+Schema.swift in Sources */,
52415255
B9FAA116238799D3009414B4 /* Author.swift in Sources */,
52425256
B9521833237E21BA00F53237 /* Comment+Schema.swift in Sources */,
52435257
FA176ED7238503C200C5C5F9 /* HubListenerTestUtilities.swift in Sources */,
@@ -5294,6 +5308,7 @@
52945308
214F49772486D8A200DA616C /* UserFollowers+Schema.swift in Sources */,
52955309
B9FAA11423878CEA009414B4 /* UserProfile+Schema.swift in Sources */,
52965310
6BE9D6EF25A6622000AB5C9A /* Team+Schema.swift in Sources */,
5311+
762C978E26210FF100798FA3 /* RecordCover+Schema.swift in Sources */,
52975312
217D5EBC2577F9DF009F0639 /* User5.swift in Sources */,
52985313
6B7743DF25906FD3001469F5 /* Dish.swift in Sources */,
52995314
217D5EB82577F9DF009F0639 /* Project2+Schema.swift in Sources */,
@@ -5340,6 +5355,7 @@
53405355
FA1846EE23998E44009B9D01 /* MockAPIResponders.swift in Sources */,
53415356
6B7743E225906FD3001469F5 /* Dish+Schema.swift in Sources */,
53425357
217D5EC32577F9DF009F0639 /* PostEditor5.swift in Sources */,
5358+
762C978526210F6400798FA3 /* RecordCover.swift in Sources */,
53435359
216E45ED248E914F0035E3CE /* Category.swift in Sources */,
53445360
6BE9D6ED25A6622000AB5C9A /* Project+Schema.swift in Sources */,
53455361
6BE9D6EC25A6622000AB5C9A /* Project.swift in Sources */,

Amplify/Categories/DataStore/Model/Internal/Schema/ModelSchema+Definition.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,19 +167,22 @@ public enum ModelFieldDefinition {
167167
case field(name: String,
168168
type: ModelFieldType,
169169
nullability: ModelFieldNullability,
170+
isReadOnly: Bool,
170171
association: ModelAssociation?,
171172
attributes: [ModelFieldAttribute],
172173
authRules: AuthRules)
173174

174175
public static func field(_ key: CodingKey,
175176
is nullability: ModelFieldNullability = .required,
177+
isReadOnly: Bool = false,
176178
ofType type: ModelFieldType = .string,
177179
attributes: [ModelFieldAttribute] = [],
178180
association: ModelAssociation? = nil,
179181
authRules: AuthRules = []) -> ModelFieldDefinition {
180182
return .field(name: key.stringValue,
181183
type: type,
182184
nullability: nullability,
185+
isReadOnly: isReadOnly,
183186
association: association,
184187
attributes: attributes,
185188
authRules: authRules)
@@ -193,39 +196,46 @@ public enum ModelFieldDefinition {
193196
return .field(name: name,
194197
type: .string,
195198
nullability: .required,
199+
isReadOnly: false,
196200
association: nil,
197201
attributes: [.primaryKey],
198202
authRules: [])
199203
}
200204

201205
public static func hasMany(_ key: CodingKey,
202206
is nullability: ModelFieldNullability = .required,
207+
isReadOnly: Bool = false,
203208
ofType type: Model.Type,
204209
associatedWith associatedKey: CodingKey) -> ModelFieldDefinition {
205210
return .field(key,
206211
is: nullability,
212+
isReadOnly: isReadOnly,
207213
ofType: .collection(of: type),
208214
association: .hasMany(associatedWith: associatedKey))
209215
}
210216

211217
public static func hasOne(_ key: CodingKey,
212218
is nullability: ModelFieldNullability = .required,
219+
isReadOnly: Bool = false,
213220
ofType type: Model.Type,
214221
associatedWith associatedKey: CodingKey,
215222
targetName: String? = nil) -> ModelFieldDefinition {
216223
return .field(key,
217224
is: nullability,
225+
isReadOnly: isReadOnly,
218226
ofType: .model(type: type),
219227
association: .hasOne(associatedWith: associatedKey, targetName: targetName))
220228
}
221229

222230
public static func belongsTo(_ key: CodingKey,
223231
is nullability: ModelFieldNullability = .required,
232+
isReadOnly: Bool = false,
224233
ofType type: Model.Type,
225234
associatedWith associatedKey: CodingKey? = nil,
226235
targetName: String? = nil) -> ModelFieldDefinition {
227236
return .field(key,
228237
is: nullability,
238+
isReadOnly: isReadOnly,
229239
ofType: .model(type: type),
230240
association: .belongsTo(associatedWith: associatedKey, targetName: targetName))
231241
}
@@ -234,6 +244,7 @@ public enum ModelFieldDefinition {
234244
guard case let .field(name,
235245
type,
236246
nullability,
247+
isReadOnly,
237248
association,
238249
attributes,
239250
authRules) = self else {
@@ -242,6 +253,7 @@ public enum ModelFieldDefinition {
242253
return ModelField(name: name,
243254
type: type,
244255
isRequired: nullability.isRequired,
256+
isReadOnly: isReadOnly,
245257
isArray: type.isArray,
246258
attributes: attributes,
247259
association: association,

Amplify/Categories/DataStore/Model/Internal/Schema/ModelSchema.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public struct ModelField {
2929
public let name: String
3030
public let type: ModelFieldType
3131
public let isRequired: Bool
32+
public let isReadOnly: Bool
3233
public let isArray: Bool
3334
public let attributes: [ModelFieldAttribute]
3435
public let association: ModelAssociation?
@@ -41,13 +42,15 @@ public struct ModelField {
4142
public init(name: String,
4243
type: ModelFieldType,
4344
isRequired: Bool = false,
45+
isReadOnly: Bool = false,
4446
isArray: Bool = false,
4547
attributes: [ModelFieldAttribute] = [],
4648
association: ModelAssociation? = nil,
4749
authRules: AuthRules = []) {
4850
self.name = name
4951
self.type = type
5052
self.isRequired = isRequired
53+
self.isReadOnly = isReadOnly
5154
self.isArray = isArray
5255
self.attributes = attributes
5356
self.association = association

AmplifyPlugins/Core/AWSPluginsCore/Model/Decorator/ModelDecorator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public struct ModelDecorator: ModelBasedGraphQLDocumentDecorator {
2727
public func decorate(_ document: SingleDirectiveGraphQLDocument,
2828
modelSchema: ModelSchema) -> SingleDirectiveGraphQLDocument {
2929
var inputs = document.inputs
30-
var graphQLInput = model.graphQLInput(modelSchema)
30+
var graphQLInput = model.graphQLInputForMutation(modelSchema)
3131

3232
if !modelSchema.authRules.isEmpty {
3333
modelSchema.authRules.forEach { authRule in

AmplifyPlugins/Core/AWSPluginsCore/Model/Support/Model+GraphQL.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ extension Model {
1515

1616
/// Get the `Model` values as a `Dictionary` of `String` to `Any?` that can be
1717
/// used as the `input` of GraphQL related operations.
18-
func graphQLInput(_ modelSchema: ModelSchema) -> GraphQLInput {
18+
func graphQLInputForMutation(_ modelSchema: ModelSchema) -> GraphQLInput {
1919
var input: GraphQLInput = [:]
2020
modelSchema.fields.forEach {
2121
let modelField = $0.value
2222

23+
// When the field is read-only don't add it to the GraphQL input object
24+
if modelField.isReadOnly {
25+
return
26+
}
27+
2328
// TODO how to handle associations of type "many" (i.e. cascade save)?
2429
// This is not supported right now and might be added as a future feature
2530
if case .collection = modelField.type {

AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLDocument/GraphQLCreateMutationTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,4 +235,47 @@ class GraphQLCreateMutationTests: XCTestCase {
235235
}
236236
XCTAssertEqual(input["commentPostId"] as? String, post.id)
237237
}
238+
239+
func testCreateGraphQLMutationFromModelWithReadonlyFields() {
240+
let recordCover = RecordCover(artist: "artist")
241+
let record = Record(name: "name", description: "description", cover: recordCover)
242+
var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: Record.schema,
243+
operationType: .mutation)
244+
documentBuilder.add(decorator: DirectiveNameDecorator(type: .create))
245+
documentBuilder.add(decorator: ModelDecorator(model: record))
246+
documentBuilder.add(decorator: ConflictResolutionDecorator())
247+
let document = documentBuilder.build()
248+
let expectedQueryDocument = """
249+
mutation CreateRecord($input: CreateRecordInput!) {
250+
createRecord(input: $input) {
251+
id
252+
coverId
253+
createdAt
254+
description
255+
name
256+
updatedAt
257+
__typename
258+
_version
259+
_deleted
260+
_lastChangedAt
261+
}
262+
}
263+
"""
264+
XCTAssertEqual(document.name, "createRecord")
265+
XCTAssertEqual(document.stringValue, expectedQueryDocument)
266+
guard let variables = document.variables else {
267+
XCTFail("The document doesn't contain variables")
268+
return
269+
}
270+
guard let input = variables["input"] as? GraphQLInput else {
271+
XCTFail("Variables should contain a valid input")
272+
return
273+
}
274+
XCTAssertEqual(input["id"] as? String, record.id)
275+
XCTAssertEqual(input["name"] as? String, record.name)
276+
XCTAssertEqual(input["description"] as? String, record.description)
277+
XCTAssertNil(input["createdAt"] as? Temporal.DateTime)
278+
XCTAssertNil(input["updatedAt"] as? Temporal.DateTime)
279+
XCTAssertNil(input["cover"] as? RecordCover)
280+
}
238281
}

AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLDocument/GraphQLDeleteMutationTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,47 @@ class GraphQLDeleteMutationTests: XCTestCase {
118118
XCTAssert(input["id"] as? String == post.id)
119119
XCTAssert(input["_version"] as? Int == 5)
120120
}
121+
122+
func testDeleteGraphQLMutationModelWithReadOnlyFields() {
123+
let recordCover = RecordCover(artist: "artist")
124+
let record = Record(name: "name", description: "description", cover: recordCover)
125+
var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: Record.schema,
126+
operationType: .mutation)
127+
documentBuilder.add(decorator: DirectiveNameDecorator(type: .delete))
128+
documentBuilder.add(decorator: ModelDecorator(model: record))
129+
documentBuilder.add(decorator: ConflictResolutionDecorator())
130+
let document = documentBuilder.build()
131+
let expectedQueryDocument = """
132+
mutation DeleteRecord($input: DeleteRecordInput!) {
133+
deleteRecord(input: $input) {
134+
id
135+
coverId
136+
createdAt
137+
description
138+
name
139+
updatedAt
140+
__typename
141+
_version
142+
_deleted
143+
_lastChangedAt
144+
}
145+
}
146+
"""
147+
XCTAssertEqual(document.name, "deleteRecord")
148+
XCTAssertEqual(document.stringValue, expectedQueryDocument)
149+
guard let variables = document.variables else {
150+
XCTFail("The document doesn't contain variables")
151+
return
152+
}
153+
guard let input = variables["input"] as? GraphQLInput else {
154+
XCTFail("Variables should contain a valid input")
155+
return
156+
}
157+
XCTAssertEqual(input["id"] as? String, record.id)
158+
XCTAssertEqual(input["name"] as? String, record.name)
159+
XCTAssertEqual(input["description"] as? String, record.description)
160+
XCTAssertNil(input["createdAt"] as? Temporal.DateTime)
161+
XCTAssertNil(input["updatedAt"] as? Temporal.DateTime)
162+
XCTAssertNil(input["cover"] as? RecordCover)
163+
}
121164
}

AmplifyPlugins/Core/AWSPluginsCoreTests/Model/GraphQLDocument/GraphQLGetQueryTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,4 +177,47 @@ class GraphQLGetQueryTests: XCTestCase {
177177
}
178178
XCTAssertEqual(variables["id"] as? String, "id")
179179
}
180+
181+
/// - Given: a `model` type
182+
/// - When:
183+
/// - the model has read-only fields
184+
/// - Then:
185+
/// - the generated query contains read-only fields if requested in selection set
186+
///
187+
func testGetGraphQLQueryModelWithReadOnlyFields() {
188+
var documentBuilder = ModelBasedGraphQLDocumentBuilder(modelSchema: Record.schema, operationType: .query)
189+
documentBuilder.add(decorator: DirectiveNameDecorator(type: .get))
190+
documentBuilder.add(decorator: ModelIdDecorator(id: "id"))
191+
let document = documentBuilder.build()
192+
let expectedQueryDocument = """
193+
query GetRecord($id: ID!) {
194+
getRecord(id: $id) {
195+
id
196+
coverId
197+
createdAt
198+
description
199+
name
200+
updatedAt
201+
__typename
202+
}
203+
}
204+
"""
205+
XCTAssertEqual(document.name, "getRecord")
206+
XCTAssertEqual(document.stringValue, expectedQueryDocument)
207+
guard let variables = document.variables else {
208+
XCTFail("The document doesn't contain variables")
209+
return
210+
}
211+
XCTAssertEqual(variables["id"] as? String, "id")
212+
XCTAssertNil(variables["createdAt"] as? Temporal.DateTime)
213+
XCTAssertNil(variables["updatedAt"] as? Temporal.DateTime)
214+
215+
guard let selectionSet = document.selectionSet else {
216+
XCTFail("The document doesn't contain a selection set")
217+
return
218+
}
219+
let fields = selectionSet.children.map { $0.value.name! }
220+
XCTAssertTrue(fields.contains("createdAt"))
221+
XCTAssertTrue(fields.contains("updatedAt"))
222+
}
180223
}

0 commit comments

Comments
 (0)