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

Gen 2 + RDS postgres - How to do tenant isolation with the list command

Open Spockinnator opened this issue 6 months ago • 5 comments

Environment information

{
  "name": "nubiops-react-application",
  "private": true,
  "version": "0.7.2",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build:prod": "tsc vite build",
    "build": "tsc && vite build",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview",
    "lint:fix": "eslint --fix ./src",
    "typecheck": "tsc --noEmit",
    "format:write": "prettier --write \"**/*.{js,jsx,mjs,ts,tsx,mdx}\" --cache",
    "format:check": "prettier --check \"**/*.{js,jsx,mjs,ts,tsx,mdx}\" --cache",
    "test": "jest --watch"
  },
  "dependencies": {
    "@auth0/auth0-react": "2.3.0",
    "@aws-amplify/api-graphql": "^4.7.15",
    "@aws-amplify/auth": "^6.12.4",
    "@aws-amplify/cli": "^13.0.1",
    "@aws-amplify/codegen-ui-react": "^2.20.3",
    "@aws-amplify/core": "^6.11.4",
    "@aws-amplify/data-schema": "^1.20.5",
    "@aws-amplify/ui": "^6.10.2",
    "@aws-amplify/ui-react": "^6.11.1",
    "@aws-amplify/ui-react-storage": "3.10.2",
    "@aws-lambda-powertools/idempotency": "2.19.1",
    "@aws-sdk/client-cost-explorer": "^3.804.0",
    "@aws-sdk/client-dynamodb": "3.804.0",
    "@aws-sdk/client-secrets-manager": "^3.804.0",
    "@aws-sdk/client-ses": "3.804.0",
    "@aws-sdk/client-sso-oidc": "3.804.0",
    "@aws-sdk/client-sts": "3.804.0",
    "@aws-sdk/lib-dynamodb": "3.804.0",
    "@babel/eslint-parser": "7.27.1",
    "@dnd-kit/core": "^6.3.1",
    "@dnd-kit/sortable": "^10.0.0",
    "@dnd-kit/utilities": "^3.2.2",
    "@elgorditosalsero/react-gtm-hook": "2.7.2",
    "@emotion/cache": "11.14.0",
    "@emotion/react": "11.14.0",
    "@emotion/server": "11.11.0",
    "@emotion/styled": "11.14.0",
    "@fontsource/inter": "^5.2.5",
    "@fontsource/plus-jakarta-sans": "5.2.5",
    "@fontsource/roboto-mono": "5.2.5",
    "@fullcalendar/core": "6.1.17",
    "@fullcalendar/daygrid": "6.1.17",
    "@fullcalendar/interaction": "6.1.17",
    "@fullcalendar/list": "6.1.17",
    "@fullcalendar/react": "6.1.17",
    "@fullcalendar/timegrid": "6.1.17",
    "@fullcalendar/timeline": "6.1.17",
    "@hookform/resolvers": "5.0.1",
    "@mui/icons-material": "7.1.0",
    "@mui/lab": "7.0.0-beta.12",
    "@mui/material": "7.1.0",
    "@mui/styled-engine-sc": "7.1.0",
    "@mui/system": "7.1.0",
    "@mui/utils": "7.1.0",
    "@mui/x-data-grid": "7.28.3",
    "@mui/x-data-grid-pro": "7.28.3",
    "@mui/x-date-pickers": "7.28.3",
    "@mui/x-date-pickers-pro": "7.28.3",
    "@mui/x-license": "7.28.0",
    "@phosphor-icons/react": "2.1.7",
    "@react-pdf/renderer": "4.3.0",
    "@supabase/supabase-js": "2.49.4",
    "@tiptap/extension-link": "2.12.0",
    "@tiptap/extension-placeholder": "2.12.0",
    "@tiptap/react": "2.12.0",
    "@tiptap/starter-kit": "2.12.0",
    "amplify": "^0.0.11",
    "aws-amplify": "^6.14.4",
    "axios": "^1.9.0",
    "core-js": "3.42.0",
    "dayjs": "1.11.13",
    "embla-carousel": "8.6.0",
    "embla-carousel-react": "8.6.0",
    "firebase": "11.6.1",
    "i18next": "25.1.1",
    "install": "^0.13.0",
    "jsforce": "^3.8.1",
    "lz-string": "^1.5.0",
    "mapbox-gl": "3.11.1",
    "notistack": "^3.0.2",
    "pg": "^8.15.6",
    "pkce-challenge": "^5.0.0",
    "react": "19.1.0",
    "react-dom": "19.1.0",
    "react-dropzone": "14.3.8",
    "react-helmet-async": "2.0.5",
    "react-hook-form": "7.56.2",
    "react-i18next": "15.5.1",
    "react-icons": "5.5.0",
    "react-map-gl": "8.0.4",
    "react-markdown": "10.1.0",
    "react-modal": "3.16.3",
    "react-router": "^7.5.3",
    "react-router-dom": "7.5.3",
    "react-simple-maps": "3.0.0",
    "react-syntax-highlighter": "15.6.1",
    "react-to-print": "3.1.0",
    "react-tooltip": "^5.28.1",
    "recharts": "2.15.3",
    "rehype-katex": "^7.0.1",
    "remark-gfm": "4.0.1",
    "remark-math": "^6.0.0",
    "sonner": "2.0.3",
    "styled-components": "6.1.17",
    "stylis": "4.3.6",
    "stylis-plugin-rtl": "2.1.1",
    "uuid": "11.1.0",
    "vite": "6.3.5",
    "zod": "3.24.4"
  },
  "devDependencies": {
    "@auth0/auth0-react": "2.3.9",
    "@aws-amplify/backend": "1.16.1",
    "@aws-amplify/backend-cli": "1.7.1",
    "@aws-lambda-powertools/logger": "2.19.1",
    "@aws-sdk/client-cognito-identity-provider": "^3.804.0",
    "@babel/plugin-transform-class-properties": "7.27.1",
    "@babel/plugin-transform-object-rest-spread": "7.27.2",
    "@eslint/config-array": "0.20.0",
    "@eslint/object-schema": "^2.1.6",
    "@ianvs/prettier-plugin-sort-imports": "4.4.1",
    "@testing-library/dom": "10.4.0",
    "@testing-library/jest-dom": "6.6.3",
    "@testing-library/react": "16.3.0",
    "@types/aws-lambda": "^8.10.149",
    "@types/jest": "29.5.14",
    "@types/mapbox-gl": "3.4.1",
    "@types/node": "22.15.15",
    "@types/pg": "^8.15.0",
    "@types/react": "19.1.3",
    "@types/react-beautiful-dnd": "^13.1.8",
    "@types/react-dom": "19.1.3",
    "@types/react-map-gl": "^6.1.7",
    "@types/react-simple-maps": "3.0.6",
    "@types/react-syntax-highlighter": "15.5.13",
    "@types/uuid": "^10.0.0",
    "@typescript-eslint/eslint-plugin": "8.32.0",
    "@typescript-eslint/parser": "8.32.0",
    "@vercel/style-guide": "6.0.0",
    "@vitejs/plugin-react": "4.4.1",
    "aws-cdk": "2.1013.0",
    "aws-cdk-lib": "2.194.0",
    "constructs": "10.4.2",
    "esbuild": "0.25.4",
    "eslint": "9.26.0",
    "eslint-config-prettier": "10.1.3",
    "eslint-plugin-react-hooks": "5.2.0",
    "eslint-plugin-react-refresh": "0.4.20",
    "jest": "29.7.0",
    "jest-environment-jsdom": "29.7.0",
    "prettier": "3.5.3",
    "rollup-plugin-visualizer": "5.14.0",
    "tsx": "4.19.4",
    "typescript": "5.8.3",
    "vite": "^5.4.10"
  }
}

Describe the bug

Hello, I'm having some difficulty with getting my React Gen 2 App working with RDS postgres v2 database working with tenant isolation.

Background: I have an amplify gen 2 React app working with DynamoDB and I have effectively implemented the tenant isolation there.

I've followed the example in the Gen 2 Documentation: https://docs.amplify.aws/react/build-a-backend/data/connect-to-existing-data-sources/connect-postgres-mysql-database/

The App if pretty far along on Dynamodb. I've generated the 'schema.sql.ts' with the command: npx ampx generate schema-from-database --connection-uri-secret SQL_CONNECTION_STRING --out amplify/data/schema.sql.ts

I've been adding RDS postgres and it currently can do CRUD operations on the database. This command will bring back all of the rows of the database regardless of tenant field value. const contractSQLModelListData = await client.models.contractsql.list();

I've tried to limit the access similar to the way I've done it in the dynamodb as you can see from the attached data/resource.ts file, but it is not limiting the client call. Clearly, I need to filter from the front end, but that's not sufficient, I need to limit this on the backend.

As you can see, I'm also using the sql files to do advanced querying of the table.

How do I modify this configuration to limit these queries based on tenant id?

BTW, I'm opening this as a bug per request from the team on the Amplify Office hours.

I've been working on this for days, and would greatly appreciate it if we can jump on a Teams meeting. Please email me [email protected]

Reproduction steps

This is how I'm creating the resource.ts. note that i cut out a bunch of models that are not relevant, but included some that have the group access permissions included.

// amplify/data/resource.ts

import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
import { postConfirmation } from "../auth/post-confirmation/resource";
import { writeGlobalLogFunction } from "../functions/write-global-log/resource";
import { manageDealApprovalFunction } from "../functions/manage-deal-approval/resource";
import { manageInviteUserFunction } from "../functions/manage-invite-user/resource";
import { selectAPlanFunction } from "../functions/select-a-plan/resource";
import { doInitializationsFunction } from "../functions/do-initializations/resource";
import { manageContactEmailFunction } from "../functions/manage-contact-email/resource";
import { getIPAddressFunction } from "../functions/getPAddressFunction/resource";
import { manageIntegrationsFunction } from '../functions/integration-manager/resource';
import { vendorCostImporterFunction } from '../functions/vendor-cost-importer/resource';
import { CRMImporterFunction } from '../functions/crm-importer/resource';
import { CRMSyncDealFunction } from '../functions/crm-sync-deal/resource';
import { OAuth2TokenFunction } from '../functions/OAuth2TokenFunction/resource';
import { sqlCRUDfunction } from '../functions/sql-crud/resource';
import { CRMCallbackFunction } from '../functions/crm-callback/resource';


import { getDBCountsByResellerIDFunction } from '../functions/getDBCountsByResellerID/resource';
import {
    globalAVGAdminGroupName,  
    tenantOwnerGroup,   
    globalAllNonAVGAdminGroups,
    // ContractInterface,
} from '../../src/globals';


import { schema as generatedSqlSchema } from './schema.sql';


// Add a global authorization rule

const sqlSchema = generatedSqlSchema.setAuthorization((models) => [
    
    models.contractsql.authorization(allow => [
        allow.owner().to(['read', 'create', 'update', 'delete']),
        allow.groupDefinedIn('tenant').to(['read']),
        allow.groupDefinedIn('tenanteditgroup').to(['read', 'create', 'update', 'delete']),
        allow.groupDefinedIn('resellerid').to(['read']),
        allow.group(globalAVGAdminGroupName).to(['read', 'create', 'update', 'delete']),
    ]),
    
    models.globallogsql.authorization(allow => [
        allow.owner().to(['read', 'create', 'update', 'delete']),
        allow.groupDefinedIn('tenant').to(['read']),
        allow.groupDefinedIn('resellerid').to(['read']),
        allow.group(globalAVGAdminGroupName).to(['read', 'create', 'update', 'delete']),
    ]),
    models.dbversionsql.authorization(allow => [
        allow.owner().to(['read', 'create', 'update', 'delete']),
        allow.groupDefinedIn('resellerid').to(['read']),
        allow.authenticated("userPools"),
    ]),

    // examples of field level security 
    // models.GlobalLogSQL.fields.tenant.authorization(allow => [
    //     allow.owner().to(['read', 'create', 'update', 'delete']),
    //     allow.groupDefinedIn('tenant').to(['read']),
    //     allow.groupDefinedIn('resellerId').to(['read']),
    //     allow.group(globalAVGAdminGroupName).to(['read', 'create', 'update', 'delete']),
    // ]),
    // models.dbVersionSQL.fields.resellerId.authorization(allow => [
    //     allow.owner().to(['read', 'create', 'update', 'delete']),
    //     allow.groupDefinedIn('resellerId').to(['read']),
    //     allow.authenticated("userPools"),
    // ]),
    
  ]

).addToSchema({
    createContractSQLModel: a.mutation()
      .arguments({
        id: a.string().required(),
        tenant: a.string().required(),
        resellerid: a.string(),
        contractnumber: a.string().required(),
        tenanteditgroup: a.string(),
        version: a.string(),
        name: a.string(),
        updatedyyyymmdd: a.string(),
        createdat: a.string(),
        updatedat: a.string(),
        createdby: a.string(),
        updatedby: a.string(),
        approvedbyid: a.string(),
        approvedbyname: a.string(),
        updatedbyname: a.string(),
        changehistoryjson: a.string(),
        salespersonid: a.string(),
        salespersonname: a.string(),
        salespersonemail: a.string(),
        supportcontactname: a.string(),
        supportcontactemail: a.string(),
        teamnamesjson: a.string(),
        territoryid: a.string(),
        territoryname: a.string(),
        divisionid: a.string(),
        divisionname: a.string(),
        regionid: a.string(),
        regionname: a.string(),
        geographyid: a.string(),
        geographyname: a.string(),
        customerid: a.string(),
        customercompanyname: a.string(),
        customerdepartment: a.string(),
        customercontactname: a.string(),
        customercontactemail: a.string(),
        customercontactphone: a.string(),
        customeraddress: a.string(),
        productshortlistjson: a.string(),
        lineitemdetailsjson: a.string(),
        quoteddate: a.string(),
        approveddate: a.string(),
        trialperiodstart: a.string(),
        trialperiodend: a.string(),
        contractperiodstart: a.string(),
        contractperiodend: a.string(),
        billingmethod: a.string(),
        billingfrequency: a.string(),
        dealnotes: a.string(),
        approvalnotes: a.string(),
        dealsubtotalbeforediscount: a.float(),
        requesteddiscount: a.float(),
        approveddiscount: a.float(),
        dealdiscountamount: a.float(),
        dealsubtotal: a.float(),
        salescommission: a.float(),
        deallevelgrossmargin: a.float(),
        dealcost: a.float(),
        applicabletaxrate: a.float(),
        taxamount: a.float(),
        dealtotal: a.float(),
        currency: a.string(),
        exchangerate: a.float(),
        customerdistributorid: a.string(),
        customerdistributorname: a.string(),
        distributordiscount: a.float(),
        customerresellerid: a.string(),
        customerresellername: a.string(),
        resellerdiscount: a.float(),
        requesterid: a.string(),
        requestername: a.string(),
        requesteremail: a.string(),
        approverid: a.string(),
        approvername: a.string(),
        approveremail: a.string(),
        contracttype: a.string(),
        trialdays: a.float(),
        contractdays: a.float(),
        isapproved: a.boolean(),
        approvalstatus: a.string(),
        approvaldate: a.string(),
        approvalcomments: a.string(),
        enabled: a.boolean(),
        deleted: a.boolean(),
        metadatajson: a.string(),
        contractpdf: a.string(),
        relatedfiles: a.string(),
        imagename: a.string(),
        base64image: a.string(),
        notes: a.string(),
      })
      .returns(a.json())
      .authorization(allow => allow.authenticated())
      .handler(a.handler.sqlReference('./sqlFiles/createContract.sql')),
      
      updateContractSQLModel: a.mutation()
        .arguments({
        id: a.string().required(),
        tenant: a.string().required(),
        resellerid: a.string(),
        contractnumber: a.string().required(),
        tenanteditgroup: a.string(),
        version: a.string(),
        name: a.string(),
        updatedyyyymmdd: a.string(),
        createdat: a.string(),
        updatedat: a.string(),
        createdby: a.string(),
        updatedby: a.string(),
        approvedbyid: a.string(),
        approvedbyname: a.string(),
        updatedbyname: a.string(),
        changehistoryjson: a.string(),
        salespersonid: a.string(),
        salespersonname: a.string(),
        salespersonemail: a.string(),
        supportcontactname: a.string(),
        supportcontactemail: a.string(),
        teamnamesjson: a.string(),
        territoryid: a.string(),
        territoryname: a.string(),
        divisionid: a.string(),
        divisionname: a.string(),
        regionid: a.string(),
        regionname: a.string(),
        geographyid: a.string(),
        geographyname: a.string(),
        customerid: a.string(),
        customercompanyname: a.string(),
        customerdepartment: a.string(),
        customercontactname: a.string(),
        customercontactemail: a.string(),
        customercontactphone: a.string(),
        customeraddress: a.string(),
        productshortlistjson: a.string(),
        lineitemdetailsjson: a.string(),
        quoteddate: a.string(),
        approveddate: a.string(),
        trialperiodstart: a.string(),
        trialperiodend: a.string(),
        contractperiodstart: a.string(),
        contractperiodend: a.string(),
        billingmethod: a.string(),
        billingfrequency: a.string(),
        dealnotes: a.string(),
        approvalnotes: a.string(),
        dealsubtotalbeforediscount: a.float(),
        requesteddiscount: a.float(),
        approveddiscount: a.float(),
        dealdiscountamount: a.float(),
        dealsubtotal: a.float(),
        salescommission: a.float(),
        deallevelgrossmargin: a.float(),
        dealcost: a.float(),
        applicabletaxrate: a.float(),
        taxamount: a.float(),
        dealtotal: a.float(),
        currency: a.string(),
        exchangerate: a.float(),
        customerdistributorid: a.string(),
        customerdistributorname: a.string(),
        distributordiscount: a.float(),
        customerresellerid: a.string(),
        customerresellername: a.string(),
        resellerdiscount: a.float(),
        requesterid: a.string(),
        requestername: a.string(),
        requesteremail: a.string(),
        approverid: a.string(),
        approvername: a.string(),
        approveremail: a.string(),
        contracttype: a.string(),
        trialdays: a.float(),
        contractdays: a.float(),
        isapproved: a.boolean(),
        approvalstatus: a.string(),
        approvaldate: a.string(),
        approvalcomments: a.string(),
        enabled: a.boolean(),
        deleted: a.boolean(),
        metadatajson: a.string(),
        contractpdf: a.string(),
        relatedfiles: a.string(),
        imagename: a.string(),
        base64image: a.string(),
        owner: a.string(),
        notes: a.string(),
        })
        .returns(a.json())
        .authorization(allow => allow.authenticated())
        .handler(a.handler.sqlReference('./sqlFiles/updateContract.sql')),

        listAllContractSQLModel: a.query()
        .arguments({
            tenant: a.string().required()
        })
        .returns(a.json().array())
        .authorization(allow => allow.authenticated())
        .handler(a.handler.sqlReference('./sqlFiles/listAllContract.sql')), 

        listAllContractSQLByTenantModel: a.query()
        .arguments({
            tenant: a.string().required()
        })
        .returns(a.json().array())
        .authorization(allow => allow.authenticated())
        .handler(a.handler.sqlReference('./sqlFiles/listAllContractByTenant.sql')),

        listAllContractSQLByTenantAndGeographyIdModel: a.query()
        .arguments({
            tenant: a.string().required(),
            geographyid: a.string().required()
        })
        .returns(a.json().array())
        .authorization(allow => allow.authenticated())
        .handler(a.handler.sqlReference('./sqlFiles/listAllContractByTenantAndGeographyId.sql')),
        
        listAllContractSQLByTenantAndRegionIdModel: a.query()
        .arguments({
            tenant: a.string().required(),
            regionid: a.string().required()
        })
        .returns(a.json().array())
        .authorization(allow => allow.authenticated())
        .handler(a.handler.sqlReference('./sqlFiles/listAllContractByTenantAndRegionId.sql')),

        listAllContractSQLByTenantAndTerritoryIdModel: a.query()
        .arguments({
            tenant: a.string().required(),
            territoryid: a.string().required()
        })
        .returns(a.json().array())
        .authorization(allow => allow.authenticated())
        .handler(a.handler.sqlReference('./sqlFiles/listAllContractByTenantAndTerritoryId.sql')),

        listAllContractSQLByTenantAndUserModel: a.query()
        .arguments({
            tenant: a.string().required(),
            owner: a.string().required()
        })
        .returns(a.json().array())
        .authorization(allow => allow.authenticated())
        .handler(a.handler.sqlReference('./sqlFiles/listAllContractByTenantAndOwner.sql')),
    });
  
const schema = a.schema({
    // #region APIs
    // note the 'manageInviteUserModel' mutation doesn't actually hit a real table 
    // it is an API only mutation which hits a backend lambda function 'manageInviteUserFunction'
    // #region manageInviteUserModel
    manageInviteUserModel: a.mutation()
      .arguments({
            id: a.id().required(),
            resellerId: a.string().required(),
            tenant: a.string().required(),
            tenantEditGroup: a.string().required(),
            accountNumber: a.string().required(),
            requesterEmail: a.string().required(),
            requesterUserId: a.string().required(),
            requesterName: a.string().required(),
            newUserName: a.string().required(),
            newUserEmail: a.string().required(),
            newUserRole: a.string().required(),
            isApprover: a.boolean().required(),
            companyName: a.string().required(),
            territoryId:    a.string().required(),
            territoryName:  a.string().required(),
            divisionId:     a.string().required(),
            divisionName:   a.string().required(),
            regionId:       a.string().required(),
            regionName:     a.string().required(),
            geographyId:    a.string().required(),
            geographyName:  a.string().required(),
            timeZone: a.string().required(),
            action: a.string().required(),
      })
      .returns(a.string())
      .authorization(allow => [allow.authenticated("userPools")])
      .handler(a.handler.function(manageInviteUserFunction)),
      // #endregion manageInviteUserModel

    // #region InvitedTeamMember
    InvitedTeamMember: a.model({
            id: a.id(),
            tenant: a.id(),
            tenantEditGroup: a.id(),
            resellerId: a.id(),
            accountId: a.id(),
            accountNumber: a.string(),
            requesterUserId: a.string().required(),
            requesterName: a.string().required(),
            requesterEmail: a.string().required(),
            newUserName: a.string().required(),
            newUserEmail: a.string().required(),
            newUserRole: a.string().required(),
            isApprover: a.boolean(),
            companyName: a.string().required(),
            territoryId:    a.string().required(),
            territoryName:  a.string().required(),
            divisionId:     a.string().required(),
            divisionName:   a.string().required(),
            regionId:       a.string().required(),
            regionName:     a.string().required(),
            geographyId:    a.string().required(),
            geographyName:  a.string().required(),
            timeZone: a.string(),
            groups: a.string(),
            updatedYYYYMMDD: a.date(),
            createdAt: a.string(),
            updatedAt: a.string(),
            createdBy: a.id(),
            updatedBy: a.id(),
            enabled: a.boolean(),
            created: a.boolean(),
            owner: a.string(),
            version: a.string(),
            notes:    a.string(),
    })
      .secondaryIndexes((index) => [
          index("resellerId"),
          index("tenant"),
          index("tenant").sortKeys([ "updatedYYYYMMDD"]), 
      ])
      .authorization(allow => [
        
          allow.groupDefinedIn('tenant').to(['read', 'create', 'update']),
          allow.groupDefinedIn('tenantEditGroup').to(['read', 'create', 'update']),
          allow.groupDefinedIn('resellerId').to(['read', 'create', 'update']),
          allow.group(tenantOwnerGroup).to(['read', 'create', 'update']),
          allow.group(globalAVGAdminGroupName).to(['read', 'create', 'update', 'delete']),
          
    ]),

    // #endregion InvitedTeamMember

    // #region Contract
    Contract: a.model({
        id: a.id(),
        tenant: a.id(),
        resellerId: a.id(),
        tenantEditGroup: a.id(),
        contractNumber: a.string(),
        version: a.string(),
        name: a.string(),
        status: a.string(), // draft, sent, approved, etc.
        updatedYYYYMMDD: a.date(),
        createdAt: a.string(),
        updatedAt: a.string(),
        createdBy: a.id(),
        updatedBy: a.id(),

        // Approval and Status Tracking
        isApproved: a.boolean(),
        approvalStatus: a.string(),
        approvalDate: a.string(),

        approvedById: a.id(),
        approvedByName: a.string(),
        updatedByName: a.string(),
        changeHistoryJSON: a.string(),
        
        //sales team information
        salesPersonId: a.id(),
        salesPersonName: a.string(),
        salesPersonEmail: a.string(),
        supportContactName: a.string(),
        supportContactEmail: a.string(),
        teamNamesJSON: a.string(),
        territoryId: a.id(),
        territoryName: a.string(),
        divisionId: a.id(),
        divisionName: a.string(),
        regionId: a.id(),
        regionName: a.string(),
        geographyId: a.id(),
        geographyName: a.string(),
    

        // Customer Information
        customerId: a.id(),
        customer: a.belongsTo('CustomerAccount', 'customerId'),
        customerCompanyName: a.string(),
        customerDepartment: a.string(),
        customerContactName: a.string(),
        customerContactEmail: a.string(),
        customerContactPhone: a.string(),
        customerAddress: a.string(),

        productShortListJSON: a.string(),
        lineItemDetailsJSON: a.string(),
    
        // Deal Information
        quotedDate: a.string(),
        approvedDate: a.string(),
        trialPeriodStart: a.string(),
        trialPeriodEnd: a.string(),
        contractPeriodStart: a.string(),
        contractPeriodEnd: a.string(),
        billingMethod: a.string(),
        billingFrequency: a.string(),
        dealNotes: a.string(),
        approvalNotes: a.string(),
    
        // Deal Financials
        dealSubTotalBeforeDiscount: a.float(),
        requestedDiscount: a.float(),
        approvedDiscount: a.float(),
        dealDiscountAmount: a.float(),
        dealSubtotal: a.float(),
        salesCommission: a.float(),
        dealLevelGrossMargin: a.float(),
        dealCost: a.float(),
        applicableTaxRate: a.float(),
        taxAmount: a.float(),
        dealTotal: a.float(),
        currency: a.string(),
        exchangeRate: a.float(),
        customerDistributorId:      a.id(),
        customerDistributorName:    a.string(),
        distributorDiscount:        a.float(),
        customerResellerId:         a.id(),
        customerResellerName:       a.string(),
        resellerDiscount: a.float(),
        
        requesterId: a.id(),
        requesterName: a.string(),
        requesterEmail: a.string(),

        approverId: a.id(),
        approverName: a.string(),
        approverEmail: a.string(),

        contractType: a.string(), // e.g. trial, paid, etc.
        trialDays: a.float(),
        contractDays: a.float(),
        
        approvalComments: a.string(),
        enabled: a.boolean(),
        deleted: a.boolean(),
    
        // Contract Item Details
        metaDataJSON: a.string(),
        
        // Contract Metadata and Files
        contractPDF: a.string(),
        relatedFiles: a.string(),
        imageName: a.string(),
        base64Image: a.string(),
        owner: a.string(),
        notes: a.string(),
    })
    .secondaryIndexes(index => [
        index("resellerId"),
        index("tenant"), 
        index("tenant").sortKeys([ "updatedYYYYMMDD"]), 
        index("tenant").sortKeys(["customerId"]),
        index("territoryId"),
        index("divisionId"),
        index("regionId"),
        index("geographyId"),
        index("salesPersonId"),
        index("customerId"),
        

    ])
    .authorization(allow => [
        allow.owner().to(['read', 'create', 'update', 'delete']),
        allow.groupDefinedIn('tenant').to(['read']),
        allow.groupDefinedIn('tenantEditGroup').to(['read', 'create', 'update', 'delete']),
        allow.groupDefinedIn('resellerId').to(['read']),
        allow.group(globalAVGAdminGroupName).to(['read', 'create', 'update', 'delete']),
    ]),
    // #endregion Contract
    
// .....

    // #region lambda Authorizations
}).authorization((allow) => [
    allow.resource(postConfirmation), 
    allow.resource(manageInviteUserFunction),
    allow.resource(writeGlobalLogFunction),
    allow.resource(selectAPlanFunction),
    allow.resource(manageDealApprovalFunction),
    allow.resource(manageContactEmailFunction),
    allow.resource(manageIntegrationsFunction),
    allow.resource(vendorCostImporterFunction),
    allow.resource(OAuth2TokenFunction),
    allow.resource(CRMCallbackFunction),
    allow.resource(CRMImporterFunction),
    allow.resource(CRMSyncDealFunction),
    allow.resource(sqlCRUDfunction),
    allow.resource(getDBCountsByResellerIDFunction),
]);

const combinedSchema = a.combine([schema, sqlSchema]);

export type sqlSchema = ClientSchema<typeof sqlSchema>;
export type Schema = ClientSchema<typeof combinedSchema>;


export const data = defineData({
    schema: combinedSchema,
    authorizationModes: {
        defaultAuthorizationMode: 'userPool',
        apiKeyAuthorizationMode: {
            expiresInDays: 365, // API Key expiration in days
            // expires: 365, // API Key expiration in days
        },
    },
});

Spockinnator avatar May 15 '25 18:05 Spockinnator