Multiple one2one/one2many/many2many relationships will not compile SWIFT
Describe the bug
If you have a singular o2o/o2m/m2m relationship in a table the swift generated files by amplify will compile and run correctly. If there are multiple o2o/o2m/m2m relationships in a table that reference multiple different tables, the files generated will not compile.
Steps To Reproduce
Amplify generation 2
Xcode 16.2
used our code for our application as well as followed the tutorial in the documentation. this reproduced on both scenarios.
Expected behavior
compile correctly.
Amplify Framework Version
6.12.0
Amplify Categories
DataStore
Dependency manager
Swift PM
Swift version
5
CLI version
1.4.6
Xcode version
16.2
Relevant log output
<details>
<summary>Log Messages</summary>
INSERT LOG MESSAGES HERE
```
Is this a regression?
No
Regression additional context
No response
Platforms
iOS
OS Version
iOS 18.2
Device
iPhone 16 pro
Specific to simulators
No response
Additional context
No response
Thanks for filing the issue @bargeboss-chris. Could you please provide the plain-text version the schema that causes the issue? That will help us reproduce and diagnose the issue.
Hi @mattcreaser
Here is the file
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
const schema = a.schema({
customer: a.model({ customerName: a.string().required(), address1: a.string().required(), address2: a.string(), city: a.string().required(), state: a.string().required(), zipCode: a.integer().required(), country: a.string().required(), billingEmail: a.email().required(), billingName: a.string().required(), billingPhone: a.string().required(), creationTimeStamp: a.timestamp(), usr: a.hasMany('usr', 'customerID'), resource: a.hasMany('resource', 'customerID'),//many should be trackerHardware: a.hasMany('trackerHardware', 'customerID'), grp: a.hasMany('grp','customerID'), }) .secondaryIndexes((index) => [index("customerName")]) .authorization(allow => [allow.publicApiKey()]),
usr: a.model({ cognitoUserID: a.string().required(), givenName: a.string().required(), familyName: a.string().required(), phoneNumber: a.phone().required(), permissionLevel: a.integer().required(), enabled: a.boolean().required(), chartsEnabled: a.boolean().required(), lastSignIn: a.timestamp().required(), email: a.email().required(), customerID: a.id(), customer: a.belongsTo('customer', 'customerID'), simCardAssignmentHistoryID: a.id(), simCardAssignmentHistory: a.belongsTo('simCardAssignmentHistory','simCardAssignmentHistoryID'), trackerHardwareServiceID: a.id(), trackerHardwareService: a.belongsTo('trackerHardwareService','trackerHardwareServiceID'), trackerHardwareAssignmentHistoryID: a.id(), trackerHardwareAssignmentHistory: a.belongsTo('trackerHardwareAssignmentHistory','trackerHardwareAssignmentHistoryID'), simCardID: a.id(), simCard: a.belongsTo('simCard','simCardID'), resourceGeoFenceAlarmHistoryID: a.id(), resourceGeoFenceAlarmHistory: a.belongsTo('resourceGeoFenceAlarmHistory','resourceGeoFenceAlarmHistoryID'), resourceGeoFenceActivationHistoryEnableID: a.id(), resourceGeoFenceActivationHistoryEnable: a.belongsTo('resourceGeoFenceActivationHistory','resourceGeoFenceActivationHistoryEnableID'), resourceGeoFenceActivationHistoryDisableID: a.id(), resourceGeoFenceActivationHistoryDisable: a.belongsTo('resourceGeoFenceActivationHistory','resourceGeoFenceActivationHistoryDisableID'), resourceLowBatteryAlarmHistoryID: a.id(), resourceLowBatteryAlarmHistory: a.belongsTo('resourceLowBatteryAlarmHistory','resourceLowBatteryAlarmHistoryID'), resourceHardHitAlarmHistoryID: a.id(), resourceHardHitAlarmHistory: a.belongsTo('resourceHardHitAlarmHistory','resourceHardHitAlarmHistoryID'), grp: a.hasMany('grpUsr','usrID'), }) .secondaryIndexes((index) => [index("cognitoUserID")]) .authorization(allow => [allow.publicApiKey()]),
grp: a.model({ name: a.string().required(), grpDescription: a.string().required(), grpNotes: a.string(), enabled: a.boolean().required(), chartsEnabled: a.boolean().required(), lastSignIn: a.timestamp().required(), customerID: a.id(), customer: a.belongsTo('customer', 'customerID'), resource: a.hasMany('grpResource','grpID'), usr: a.hasMany('grpUsr','grpID'), }) .authorization(allow => [allow.publicApiKey()]),
grpUsr: a.model({ grpID: a.id().required(), grp: a.belongsTo('grp','grpID'), usrID: a.id().required(), usr: a.belongsTo('usr','usrID'), }) .authorization(allow => [allow.publicApiKey()]),
resource: a.model({ name: a.string().required(), hullNumber: a.string(), MMSI: a.string(), magHdgCorrection: a.float(), type: a.string().required(), yearBuilt: a.integer(), beam: a.float(), draft: a.float(), lenth: a.float(), overallHeight: a.float(), distFromPort: a.float(), distFromBow: a.float(), icon: a.string(),//123 iconColor: a.string(), GFAlarmActive: a.boolean(), GFActive: a.boolean(), GFLatCtr: a.float(), GFLonCtr: a.float(), GFRad: a.float(), HHThreshold: a.float(), HHAlarmActive: a.boolean(), HHXg: a.float(), HHYg: a.float(), HHZg: a.float(), mqttRate: a.integer(), BattLowVoltsSP: a.float(), LowBattActive1: a.boolean(), LowBattActive2: a.boolean(), LowBattActive3: a.boolean(), LowBattActive4: a.boolean(), customerID: a.id(), customer: a.belongsTo('customer', 'customerID'), trackerHardwareAssignmentHistoryID: a.id(), trackerHardwareAssignmentHistory: a.belongsTo('trackerHardwareAssignmentHistory','trackerHardwareAssignmentHistoryID'), grp: a.hasMany('grpResource','resourceID'), trackerHardware: a.hasOne('trackerHardware','resourceID'), resourceLocationHistory: a.hasMany('resourceLocationHistory','resourceID'), resourceGeoFenceAlarmHistory: a.hasMany('resourceGeoFenceAlarmHistory','resourceID'), resourceGeoFenceActivationHistory: a.hasMany('resourceGeoFenceActivationHistory','resourceID'), resourceHardHitAlarmHistory: a.hasMany('resourceHardHitAlarmHistory','resourceID'), resourceLowBatteryAlarmHistory: a.hasMany('resourceLowBatteryAlarmHistory','resourceID') }) .authorization(allow => [allow.publicApiKey()]),
grpResource: a.model({ grpID: a.id().required(), grp: a.belongsTo('grp','grpID'), resourceID: a.id().required(), resource: a.belongsTo('resource','resourceID'), }) .authorization(allow => [allow.publicApiKey()]),
trackerHardware: a.model({ SerialNumber: a.string(), shipDate: a.date(), warrantyExpireDate: a.date(), unitType: a.string(), resourceID: a.id(), resource: a.belongsTo('resource','resourceID'), customerID: a.id(), customer: a.belongsTo('customer', 'customerID'), simCardAssignmentHistoryID: a.id(), simCardAssignmentHistory: a.belongsTo('simCardAssignmentHistory','simCardAssignmentHistoryID'), simCard: a.hasOne('simCard','trackerHardwareID'), resourceLocationHistory: a.hasMany('resourceLocationHistory','trackerHardwareID'), trackerHardwareService: a.hasMany('trackerHardwareService','trackerHardwareID'), }) .secondaryIndexes((index) => [index("SerialNumber")]) .authorization(allow => [allow.publicApiKey()]),
trackerHardwareAssignmentHistory: a.model({ resource: a.hasOne('resource','trackerHardwareAssignmentHistoryID'), usr: a.hasOne('usr','trackerHardwareAssignmentHistoryID'), }) .authorization(allow => [allow.publicApiKey()]),
trackerHardwareService: a.model({ dateReceived: a.date(), dateShipped: a.date(), shippingCarrier: a.string(), trackingNumber: a.string(), serviceDate: a.datetime(), serviceNotes: a.string(), trackerHardwareID: a.id(), trackerHardware: a.belongsTo('trackerHardware','trackerHardwareID'), trackerHardwareServiceParts: a.hasMany('trackerHardwareServiceParts','trackerHardwareServiceID'), usr: a.hasOne('usr','trackerHardwareServiceID'), }) .authorization(allow => [allow.publicApiKey()]),
trackerHardwareServiceParts: a.model({ item: a.string().required(), cost: a.float().required(), sellPrice: a.float().required(), vendor: a.string().required(), vendorPartNumber: a.string().required(), notes: a.string(), trackerHardwareServiceID: a.id(), trackerHardwareService: a.belongsTo('trackerHardwareService','trackerHardwareServiceID'), }) .authorization(allow => [allow.publicApiKey()]),
simCard: a.model({ SSID: a.string().required(), starlinkActive: a.boolean(), carrier: a.string().required(), phoneNumber: a.phone().required(), trackerHardwareID: a.id(), trackerHardware: a.belongsTo('trackerHardware','trackerHardwareID'), usr: a.hasOne('usr','simCardID'), }) .secondaryIndexes((index) => [index("SSID")]) .authorization(allow => [allow.publicApiKey()]),
simCardAssignmentHistory: a.model({ trackerHardware: a.hasOne('trackerHardware','simCardAssignmentHistoryID'), usr: a.hasOne('usr','simCardAssignmentHistoryID'), }) .authorization(allow => [allow.publicApiKey()]),
resourceLocationHistory: a.model({ timeStamp: a.timestamp(), Batt1Volts: a.float(), Batt2Volts: a.float(), Batt3Volts: a.float(), Batt4Volts: a.float(), cellSig: a.integer(), distanceFromLastLocation: a.float(), GFDeviation: a.float(), gX: a.float(), gY: a.float(), gZ: a.float(), IMEI: a.float(), insertTimeStamp: a.timestamp(), lat: a.float(), lon: a.float(), magneticHeading: a.float(), mileMarker: a.float(), numSat: a.integer(), riverName: a.string(), speed: a.float(), xM: a.float(), yM: a.float(), zM: a.float(), TTL: a.timestamp(), resourceID: a.id(), resource: a.belongsTo('resource','resourceID'), trackerHardwareID: a.id(), trackerHardware: a.belongsTo('trackerHardware','trackerHardwareID'), }) .secondaryIndexes((index) => [ index("resourceID") .sortKeys(["timeStamp"]), ]) .authorization(allow => [allow.publicApiKey()]),
resourceGeoFenceAlarmHistory: a.model({ enabledTimeStamp: a.timestamp().required(), clearedTimeStamp: a.timestamp().required(), lat: a.float(), lon: a.float(), speed: a.float(), GFDeviation: a.float(), GFLatCtr: a.float(), GFLonCtr: a.float(), GFRad: a.float(), resourceID: a.id(), resource: a.belongsTo('resource','resourceID'), usrCleared: a.hasOne('usr','resourceGeoFenceAlarmHistoryID'), }) .secondaryIndexes((index) => [ index("resourceID") .sortKeys(["enabledTimeStamp"]), ]) .authorization(allow => [allow.publicApiKey()]),
resourceGeoFenceActivationHistory: a.model({ enabledTimeStamp: a.timestamp().required(), disabledTimeStamp: a.timestamp().required(), GFEnableLat: a.float(), GFEnableLon: a.float(), GFDisableLat: a.float(), GFDisableLon: a.float(), GFDisableSpeed: a.float(), GFDisableDeviation: a.float(), GFEnableLatCtr: a.float(), GFEnableLonCtr: a.float(), GFEnableRad: a.float(), buttonEnable: a.boolean(), buttonDisable: a.boolean(), resourceID: a.id(), resource: a.belongsTo('resource','resourceID'), usrEnabled: a.hasOne('usr','resourceGeoFenceActivationHistoryEnableID'), usrDisabled: a.hasOne('usr','resourceGeoFenceActivationHistoryDisableID') }) .secondaryIndexes((index) => [ index("resourceID") .sortKeys(["enabledTimeStamp"]), ]) .authorization(allow => [allow.publicApiKey()]),
resourceHardHitAlarmHistory: a.model({ enabledTimeStamp: a.timestamp().required(), disabledTimeStamp: a.timestamp().required(), gX: a.float(), gY: a.float(), gZ: a.float(), threshold: a.float(), lat: a.float(), lon: a.float(), resourceID: a.id(), resource: a.belongsTo('resource','resourceID'), usrCleared: a.hasOne('usr','resourceHardHitAlarmHistoryID'), }) .secondaryIndexes((index) => [ index("resourceID") .sortKeys(["enabledTimeStamp"]), ]) .authorization(allow => [allow.publicApiKey()]),
resourceLowBatteryAlarmHistory: a.model({ enabledTimeStamp: a.timestamp().required(), disabledTimeStamp: a.timestamp().required(), lat: a.float(), lon: a.float(), Batt1: a.boolean(), Batt1Voltage: a.float(), Batt2: a.boolean(), Batt2Voltage: a.float(), Batt3: a.boolean(), Batt3Voltage: a.float(), Batt4: a.boolean(), Batt4Voltage: a.float(), resourceID: a.id(), resource: a.belongsTo('resource','resourceID'), usrCleared: a.hasOne('usr','resourceLowBatteryAlarmHistoryID'), }) .secondaryIndexes((index) => [ index("resourceID") .sortKeys(["enabledTimeStamp"]), ]) .authorization(allow => [allow.publicApiKey()]),
});
export type Schema = ClientSchema
export const data = defineData({ schema, authorizationModes: { defaultAuthorizationMode: 'apiKey', apiKeyAuthorizationMode: { expiresInDays: 30 } }, });
/== STEP 1 =============================================================== The section below creates a Todo database table with a "content" field. Try adding a new "isDone" field as a boolean. The authorization rule below specifies that any unauthenticated user can "create", "read", "update", and "delete" any "Todo" records. =========================================================================/
/*== STEP 2 =============================================================== Go to your frontend source code. From your client-side code, generate a Data client to make CRUDL requests to your table. (THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.)
Using JavaScript or Next.js React Server Components, Middleware, Server Actions or Pages Router? Review how to generate Data clients for those use cases: https://docs.amplify.aws/gen2/build-a-backend/data/connect-to-API/ =========================================================================*/
/* "use client" import { generateClient } from "aws-amplify/data"; import type { Schema } from "@/amplify/data/resource";
const client = generateClient<Schema>() // use this Data client for CRUDL requests */
/== STEP 3 =============================================================== Fetch records from the database and use them in your frontend component. (THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.) =========================================================================/
/* For example, in a React component, you can use this snippet in your function's RETURN statement */ // const { data: todos } = await client.models.Todo.list()
// return
- {todos.map(todo =>
- {todo.content} )}
@mattcreaser
here is the sample file I was using for testing. the one above is for our production application
import { type ClientSchema, a, defineData } from '@aws-amplify/backend';
/== STEP 1 =============================================================== The section below creates a Todo database table with a "content" field. Try adding a new "isDone" field as a boolean. The authorization rule below specifies that any unauthenticated user can "create", "read", "update", and "delete" any "Todo" records. =========================================================================/ const schema = a.schema({ TodoDB: a .model({ content: a.string(), isDone: a.boolean().required(), tasksDBTodoDB: a.hasMany('TasksDB','todoIdTasksDB'), dateIDTodoDB: a.id(), datedb: a.belongsTo('TasksDB','dateIDTodoDB'), TasksDB2: a.hasMany('TasksDB2','todoIdTasksDB2') }) .authorization(allow => [allow.publicApiKey()]),
TasksDB: a
.model({
content: a.string(),
isDone: a.boolean().required(),
todoIdTasksDB: a.id(),
todoTasksDB: a.belongsTo('TodoDB', 'todoIdTasksDB'),
TodoDBTasksDB: a.hasOne('TodoDB','dateIDTodoDB'),
})
.authorization(allow => [allow.publicApiKey()]),
TasksDB2: a
.model({
content: a.string(),
isDone: a.boolean().required(),
todoIdTasksDB2: a.id(),
todoTasksDB2: a.belongsTo('TodoDB', 'todoIdTasksDB2'),
})
.authorization(allow => [allow.publicApiKey()])
});
export type Schema = ClientSchema
export const data = defineData({ schema, authorizationModes: { defaultAuthorizationMode: 'apiKey', apiKeyAuthorizationMode: { expiresInDays: 30 } }, });
/*== STEP 2 =============================================================== Go to your frontend source code. From your client-side code, generate a Data client to make CRUDL requests to your table. (THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.)
Using JavaScript or Next.js React Server Components, Middleware, Server Actions or Pages Router? Review how to generate Data clients for those use cases: https://docs.amplify.aws/gen2/build-a-backend/data/connect-to-API/ =========================================================================*/
/* "use client" import { generateClient } from "aws-amplify/data"; import type { Schema } from "@/amplify/data/resource";
const client = generateClient<Schema>() // use this Data client for CRUDL requests */
/== STEP 3 =============================================================== Fetch records from the database and use them in your frontend component. (THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.) =========================================================================/
/* For example, in a React component, you can use this snippet in your function's RETURN statement */ // const { data: todos } = await client.models.Todo.list()
// return
- {todos.map(todo =>
- {todo.content} )}
@mattcreaser : Any updates on this issue?
Thank you for the sample data, we will be running some tests today and get back to you.
@bargeboss-chris We figured out the issue you are running into. You have named your variable TasksDB2 to be the same name as the actual model.
Your TodoDB model has this variable
TasksDB2: a.hasMany('TasksDB2','todoIdTasksDB2')
The issue highlighted in TodoDB+Schema.swift is because TasksDB2 is both a variable and a model, and Swift doesn't know which one to use.
public var TasksDB2: ModelPath<TasksDB2> {
TasksDB2.Path(name: "TasksDB2", isCollection: true, parent: self)
}
Ideally you would camel case your variable names and this problem wouldn't be present. This isn't an issue with multiple relationships on a model, but just that a conflict has been created by naming a variable the exact same as the model itself.
The fix is simply
tasksDB2: a.hasMany('TasksDB2','todoIdTasksDB2')