amplify-swift icon indicating copy to clipboard operation
amplify-swift copied to clipboard

Multiple one2one/one2many/many2many relationships will not compile SWIFT

Open bargeboss-chris opened this issue 11 months ago • 7 comments

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

bargeboss-chris avatar Jan 06 '25 07:01 bargeboss-chris

Screenshot 2025-01-06 at 2 33 52 AM Screenshot 2025-01-06 at 2 33 40 AM Screenshot 2025-01-06 at 2 30 12 AM Screenshot 2025-01-06 at 2 29 50 AM Screenshot 2025-01-06 at 2 29 29 AM

bargeboss-chris avatar Jan 06 '25 07:01 bargeboss-chris

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.

mattcreaser avatar Jan 06 '25 20:01 mattcreaser

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}
  • )}

bargeboss-chris avatar Jan 06 '25 21:01 bargeboss-chris

@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}
  • )}

bargeboss-chris avatar Jan 06 '25 21:01 bargeboss-chris

@mattcreaser : Any updates on this issue?

bargeboss-chris avatar Jan 08 '25 23:01 bargeboss-chris

Thank you for the sample data, we will be running some tests today and get back to you.

tylerjroach avatar Jan 09 '25 14:01 tylerjroach

@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')

tylerjroach avatar Jan 09 '25 16:01 tylerjroach