twentyhq / twenty

Building a modern alternative to Salesforce, powered by the community.
https://twenty.com
GNU Affero General Public License v3.0
17k stars 1.99k forks source link

InternalServerError: GraphQL errors on favorite: {"message":"cannot pass more than 100 arguments to a function"} #6941

Closed codearranger closed 1 month ago

codearranger commented 1 month ago

Bug Description

After creating a new object with 53 new custom fields the UI fails to load and the logs are filled with messages like this:

twenty-server-1   | Exception Captured
twenty-server-1   |   {
twenty-server-1   |     operation: { name: 'CombinedFindManyRecords', type: 'query' },
twenty-server-1   |     document: 'query CombinedFindManyRecords($filterView: ViewFilterInput, $filterFavorite: FavoriteFilterInput, $orderByView: [ViewOrderByInput], $orderByFavorite: [FavoriteOrderByInput], $lastCursorView: String, $lastCursorFavorite: String, $limitView: Int, $limitFavorite: Int) {\n' +
twenty-server-1   |       '  views(\n' +
twenty-server-1   |       '    filter: $filterView\n' +
twenty-server-1   |       '    orderBy: $orderByView\n' +
twenty-server-1   |       '    first: $limitView\n' +
twenty-server-1   |       '    after: $lastCursorView\n' +
twenty-server-1   |       '  ) {\n' +
twenty-server-1   |       '    edges {\n' +
twenty-server-1   |       '      node {\n' +
twenty-server-1   |       '        __typename\n' +
twenty-server-1   |       '        isCompact\n' +
twenty-server-1   |       '        objectMetadataId\n' +
twenty-server-1   |       '        type\n' +
twenty-server-1   |       '        position\n' +
twenty-server-1   |       '        createdAt\n' +
twenty-server-1   |       '        key\n' +
twenty-server-1   |       '        updatedAt\n' +
twenty-server-1   |       '        viewFields {\n' +
twenty-server-1   |       '          edges {\n' +
twenty-server-1   |       '            node {\n' +
twenty-server-1   |       '              __typename\n' +
twenty-server-1   |       '              id\n' +
twenty-server-1   |       '              createdAt\n' +
twenty-server-1   |       '              updatedAt\n' +
twenty-server-1   |       '              position\n' +
twenty-server-1   |       '              fieldMetadataId\n' +
twenty-server-1   |       '              size\n' +
twenty-server-1   |       '              isVisible\n' +
twenty-server-1   |       '              viewId\n' +
twenty-server-1   |       '            }\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |   [
twenty-server-1   |     InternalServerError: GraphQL errors on favorite: {"message":"cannot pass more than 100 arguments to a function"}
twenty-server-1   |         at new BaseGraphQLError (/app/packages/twenty-server/dist/src/engine/core-modules/graphql/utils/graphql-errors.util.js:82:9)
twenty-server-1   |         at new InternalServerError (/app/packages/twenty-server/dist/src/engine/core-modules/graphql/utils/graphql-errors.util.js:198:9)
twenty-server-1   |         at workspaceQueryRunnerGraphqlApiExceptionHandler (/app/packages/twenty-server/dist/src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util.js:29:23)
twenty-server-1   |         at Object.favorites (/app/packages/twenty-server/dist/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.js:36:120)
twenty-server-1   |         at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
twenty-server-1   |         at async field.resolve (/app/node_modules/@envelop/on-resolve/cjs/index.js:36:42)
twenty-server-1   |         at async /app/node_modules/@graphql-tools/executor/cjs/execution/promiseForObject.js:18:35
twenty-server-1   |         at async Promise.all (index 1) {
twenty-server-1   |       path: undefined,
twenty-server-1   |       locations: undefined,
twenty-server-1   |       extensions: { code: 'INTERNAL_SERVER_ERROR' }
twenty-server-1   |     }
twenty-server-1   |   ]
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          __typename\n' +
twenty-server-1   |       '        }\n' +
twenty-server-1   |       '        viewFilters {\n' +
twenty-server-1   |       '          edges {\n' +
twenty-server-1   |       '            node {\n' +
twenty-server-1   |       '              __typename\n' +
twenty-server-1   |       '              displayValue\n' +
twenty-server-1   |       '              viewId\n' +
twenty-server-1   |       '              fieldMetadataId\n' +
twenty-server-1   |       '              value\n' +
twenty-server-1   |       '              updatedAt\n' +
twenty-server-1   |       '              createdAt\n' +
twenty-server-1   |       '              id\n' +
twenty-server-1   |       '              operand\n' +
twenty-server-1   |       '            }\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          __typename\n' +
twenty-server-1   |       '        }\n' +
twenty-server-1   |       '        viewSorts {\n' +
twenty-server-1   |       '          edges {\n' +
twenty-server-1   |       '            node {\n' +
twenty-server-1   |       '              __typename\n' +
twenty-server-1   |       '              viewId\n' +
twenty-server-1   |       '              updatedAt\n' +
twenty-server-1   |       '              id\n' +
twenty-server-1   |       '              direction\n' +
twenty-server-1   |       '              fieldMetadataId\n' +
twenty-server-1   |       '              createdAt\n' +
twenty-server-1   |       '            }\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          __typename\n' +
twenty-server-1   |       '        }\n' +
twenty-server-1   |       '        icon\n' +
twenty-server-1   |       '        name\n' +
twenty-server-1   |       '        kanbanFieldMetadataId\n' +
twenty-server-1   |       '        id\n' +
twenty-server-1   |       '      }\n' +
twenty-server-1   |       '      cursor\n' +
twenty-server-1   |       '      __typename\n' +
twenty-server-1   |       '    }\n' +
twenty-server-1   |       '    pageInfo {\n' +
twenty-server-1   |       '      hasNextPage\n' +
twenty-server-1   |       '      hasPreviousPage\n' +
twenty-server-1   |       '      startCursor\n' +
twenty-server-1   |       '      endCursor\n' +
twenty-server-1   |       '      __typename\n' +
twenty-server-1   |       '    }\n' +
twenty-server-1   |       '    totalCount\n' +
twenty-server-1   |       '    __typename\n' +
twenty-server-1   |       '  }\n' +
twenty-server-1   |       '  favorites(\n' +
twenty-server-1   |       '    filter: $filterFavorite\n' +
twenty-server-1   |       '    orderBy: $orderByFavorite\n' +
twenty-server-1   |       '    first: $limitFavorite\n' +
twenty-server-1   |       '    after: $lastCursorFavorite\n' +
twenty-server-1   |       '  ) {\n' +
twenty-server-1   |       '    edges {\n' +
twenty-server-1   |       '      node {\n' +
twenty-server-1   |       '        __typename\n' +
twenty-server-1   |       '        opportunityId\n' +
twenty-server-1   |       '        note {\n' +
twenty-server-1   |       '          __typename\n' +
twenty-server-1   |       '          position\n' +
twenty-server-1   |       '          updatedAt\n' +
twenty-server-1   |       '          title\n' +
twenty-server-1   |       '          createdBy {\n' +
twenty-server-1   |       '            source\n' +
twenty-server-1   |       '            workspaceMemberId\n' +
twenty-server-1   |       '            name\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          deletedAt\n' +
twenty-server-1   |       '          body\n' +
twenty-server-1   |       '          id\n' +
twenty-server-1   |       '          createdAt\n' +
twenty-server-1   |       '        }\n' +
twenty-server-1   |       '        property {\n' +
twenty-server-1   |       '          __typename\n' +
twenty-server-1   |       '          spotcrimelink {\n' +
twenty-server-1   |       '            primaryLinkUrl\n' +
twenty-server-1   |       '            primaryLinkLabel\n' +
twenty-server-1   |       '            secondaryLinks\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          compcount\n' +
twenty-server-1   |       '          mortgagebalance {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          ownerrent {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          zipcode\n' +
twenty-server-1   |       '          county\n' +
twenty-server-1   |       '          bedrooms\n' +
twenty-server-1   |       '          pricedifference {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          offercashflow {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          equity {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          deletedAt\n' +
twenty-server-1   |       '          offertotalpayments {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          dateposted\n' +
twenty-server-1   |       '          gy30price {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          name\n' +
twenty-server-1   |       '          bathrooms\n' +
twenty-server-1   |       '          monthlyhoa {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          offerballoonpayment {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          propstreamlink {\n' +
twenty-server-1   |       '            primaryLinkUrl\n' +
twenty-server-1   |       '            primaryLinkLabel\n' +
twenty-server-1   |       '            secondaryLinks\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          gy23price {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          mortgagepayment {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          offerannualrate\n' +
twenty-server-1   |       '          femafloodmap {\n' +
twenty-server-1   |       '            primaryLinkUrl\n' +
twenty-server-1   |       '            primaryLinkLabel\n' +
twenty-server-1   |       '            secondaryLinks\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          state\n' +
twenty-server-1   |       '          grossyield\n' +
twenty-server-1   |       '          offerprice {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          rent {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          piti {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          streetaddress\n' +
twenty-server-1   |       '          livingarea\n' +
twenty-server-1   |       '          dealchecklink {\n' +
twenty-server-1   |       '            primaryLinkUrl\n' +
twenty-server-1   |       '            primaryLinkLabel\n' +
twenty-server-1   |       '            secondaryLinks\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          createdAt\n' +
twenty-server-1   |       '          propertytype\n' +
twenty-server-1   |       '          monthlytaxes {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          monthlyinsurance {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          offerballoonyears\n' +
twenty-server-1   |       '          realtorlink {\n' +
twenty-server-1   |       '            primaryLinkUrl\n' +
twenty-server-1   |       '            primaryLinkLabel\n' +
twenty-server-1   |       '            secondaryLinks\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          createdBy {\n' +
twenty-server-1   |       '            source\n' +
twenty-server-1   |       '            workspaceMemberId\n' +
twenty-server-1   |       '            name\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          cashflow {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          zillowlink {\n' +
twenty-server-1   |       '            primaryLinkUrl\n' +
twenty-server-1   |       '            primaryLinkLabel\n' +
twenty-server-1   |       '            secondaryLinks\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          listprice {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          interestrate\n' +
twenty-server-1   |       '          updatedAt\n' +
twenty-server-1   |       '          saledate\n' +
twenty-server-1   |       '          offermonthly {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          compvalue {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          offerdownpayment {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          offertermyears\n' +
twenty-server-1   |       '          homestatus\n' +
twenty-server-1   |       '          fmrrent {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          offertotaltoseller {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          position\n' +
twenty-server-1   |       '          offerinterestpaid {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          yearbuilt\n' +
twenty-server-1   |       '          offertotalprincipal {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          equitypct\n' +
twenty-server-1   |       '          id\n' +
twenty-server-1   |       '        }\n' +
twenty-server-1   |       '        personId\n' +
twenty-server-1   |       '        id\n' +
twenty-server-1   |       '        opportunity {\n' +
twenty-server-1   |       '          __typename\n' +
twenty-server-1   |       '          updatedAt\n' +
twenty-server-1   |       '          pointOfContactId\n' +
twenty-server-1   |       '          position\n' +
twenty-server-1   |       '          createdBy {\n' +
twenty-server-1   |       '            source\n' +
twenty-server-1   |       '            workspaceMemberId\n' +
twenty-server-1   |       '            name\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          closeDate\n' +
twenty-server-1   |       '          createdAt\n' +
twenty-server-1   |       '          companyId\n' +
twenty-server-1   |       '          name\n' +
twenty-server-1   |       '          id\n' +
twenty-server-1   |       '          opportunityId\n' +
twenty-server-1   |       '          amount {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          deletedAt\n' +
twenty-server-1   |       '          stage\n' +
twenty-server-1   |       '        }\n' +
twenty-server-1   |       '        task {\n' +
twenty-server-1   |       '          __typename\n' +
twenty-server-1   |       '          createdAt\n' +
twenty-server-1   |       '          title\n' +
twenty-server-1   |       '          assigneeId\n' +
twenty-server-1   |       '          body\n' +
twenty-server-1   |       '          position\n' +
twenty-server-1   |       '          id\n' +
twenty-server-1   |       '          updatedAt\n' +
twenty-server-1   |       '          status\n' +
twenty-server-1   |       '          dueAt\n' +
twenty-server-1   |       '          createdBy {\n' +
twenty-server-1   |       '            source\n' +
twenty-server-1   |       '            workspaceMemberId\n' +
twenty-server-1   |       '            name\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          deletedAt\n' +
twenty-server-1   |       '        }\n' +
twenty-server-1   |       '        updatedAt\n' +
twenty-server-1   |       '        position\n' +
twenty-server-1   |       '        person {\n' +
twenty-server-1   |       '          __typename\n' +
twenty-server-1   |       '          jobTitle\n' +
twenty-server-1   |       '          phone\n' +
twenty-server-1   |       '          deletedAt\n' +
twenty-server-1   |       '          name {\n' +
twenty-server-1   |       '            firstName\n' +
twenty-server-1   |       '            lastName\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          createdBy {\n' +
twenty-server-1   |       '            source\n' +
twenty-server-1   |       '            workspaceMemberId\n' +
twenty-server-1   |       '            name\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          updatedAt\n' +
twenty-server-1   |       '          linkedinLink {\n' +
twenty-server-1   |       '            primaryLinkUrl\n' +
twenty-server-1   |       '            primaryLinkLabel\n' +
twenty-server-1   |       '            secondaryLinks\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          id\n' +
twenty-server-1   |       '          position\n' +
twenty-server-1   |       '          createdAt\n' +
twenty-server-1   |       '          avatarUrl\n' +
twenty-server-1   |       '          email\n' +
twenty-server-1   |       '          city\n' +
twenty-server-1   |       '          companyId\n' +
twenty-server-1   |       '          xLink {\n' +
twenty-server-1   |       '            primaryLinkUrl\n' +
twenty-server-1   |       '            primaryLinkLabel\n' +
twenty-server-1   |       '            secondaryLinks\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '        }\n' +
twenty-server-1   |       '        noteId\n' +
twenty-server-1   |       '        propertyId\n' +
twenty-server-1   |       '        taskId\n' +
twenty-server-1   |       '        companyId\n' +
twenty-server-1   |       '        workspaceMember {\n' +
twenty-server-1   |       '          __typename\n' +
twenty-server-1   |       '          avatarUrl\n' +
twenty-server-1   |       '          userEmail\n' +
twenty-server-1   |       '          userId\n' +
twenty-server-1   |       '          timeFormat\n' +
twenty-server-1   |       '          timeZone\n' +
twenty-server-1   |       '          id\n' +
twenty-server-1   |       '          updatedAt\n' +
twenty-server-1   |       '          createdAt\n' +
twenty-server-1   |       '          name {\n' +
twenty-server-1   |       '            firstName\n' +
twenty-server-1   |       '            lastName\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          locale\n' +
twenty-server-1   |       '          colorScheme\n' +
twenty-server-1   |       '          dateFormat\n' +
twenty-server-1   |       '        }\n' +
twenty-server-1   |       '        workspaceMemberId\n' +
twenty-server-1   |       '        company {\n' +
twenty-server-1   |       '          __typename\n' +
twenty-server-1   |       '          address {\n' +
twenty-server-1   |       '            addressStreet1\n' +
twenty-server-1   |       '            addressStreet2\n' +
twenty-server-1   |       '            addressCity\n' +
twenty-server-1   |       '            addressState\n' +
twenty-server-1   |       '            addressCountry\n' +
twenty-server-1   |       '            addressPostcode\n' +
twenty-server-1   |       '            addressLat\n' +
twenty-server-1   |       '            addressLng\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          xLink {\n' +
twenty-server-1   |       '            primaryLinkUrl\n' +
twenty-server-1   |       '            primaryLinkLabel\n' +
twenty-server-1   |       '            secondaryLinks\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          position\n' +
twenty-server-1   |       '          updatedAt\n' +
twenty-server-1   |       '          linkedinLink {\n' +
twenty-server-1   |       '            primaryLinkUrl\n' +
twenty-server-1   |       '            primaryLinkLabel\n' +
twenty-server-1   |       '            secondaryLinks\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          idealCustomerProfile\n' +
twenty-server-1   |       '          annualRecurringRevenue {\n' +
twenty-server-1   |       '            amountMicros\n' +
twenty-server-1   |       '            currencyCode\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          domainName {\n' +
twenty-server-1   |       '            primaryLinkUrl\n' +
twenty-server-1   |       '            primaryLinkLabel\n' +
twenty-server-1   |       '            secondaryLinks\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          employees\n' +
twenty-server-1   |       '          name\n' +
twenty-server-1   |       '          createdAt\n' +
twenty-server-1   |       '          id\n' +
twenty-server-1   |       '          createdBy {\n' +
twenty-server-1   |       '            source\n' +
twenty-server-1   |       '            workspaceMemberId\n' +
twenty-server-1   |       '            name\n' +
twenty-server-1   |       '            __typename\n' +
twenty-server-1   |       '          }\n' +
twenty-server-1   |       '          deletedAt\n' +
twenty-server-1   |       '          accountOwnerId\n' +
twenty-server-1   |       '        }\n' +
twenty-server-1   |       '        createdAt\n' +
twenty-server-1   |       '      }\n' +
twenty-server-1   |       '      cursor\n' +
twenty-server-1   |       '      __typename\n' +
twenty-server-1   |       '    }\n' +
twenty-server-1   |       '    pageInfo {\n' +
twenty-server-1   |       '      hasNextPage\n' +
twenty-server-1   |       '      hasPreviousPage\n' +
twenty-server-1   |       '      startCursor\n' +
twenty-server-1   |       '      endCursor\n' +
twenty-server-1   |       '      __type'... 46 more characters,
twenty-server-1   |     user: User {
twenty-server-1   |       id: 'b754a9cd-6ffe-421f-a5f1-86ff6e50aeb8',
twenty-server-1   |       firstName: 'fistname',
twenty-server-1   |       lastName: 'lastname',
twenty-server-1   |       email: 'foo@gmail.com',
twenty-server-1   |       defaultAvatarUrl: 'profile-picture/original/09c5e25a-6342-4a35-82bd-6d395499eade.jpg',
twenty-server-1   |       emailVerified: false,
twenty-server-1   |       disabled: false,
twenty-server-1   |       passwordHash: null,
twenty-server-1   |       canImpersonate: false,
twenty-server-1   |       createdAt: 2024-08-24T02:36:59.907Z,
twenty-server-1   |       updatedAt: 2024-08-24T02:36:59.907Z,
twenty-server-1   |       deletedAt: null,
twenty-server-1   |       defaultWorkspaceId: '078a3600-de50-40cf-a680-100a4e45c309',
twenty-server-1   |       defaultWorkspace: Workspace {
twenty-server-1   |         id: '078a3600-de50-40cf-a680-100a4e45c309',
twenty-server-1   |         domainName: '',
twenty-server-1   |         displayName: 'Jason',
twenty-server-1   |         logo: null,
twenty-server-1   |         inviteHash: 'a39c5e5a-ebd2-4990-99af-faf7c6ef9073',
twenty-server-1   |         deletedAt: null,
twenty-server-1   |         createdAt: 2024-08-24T02:36:59.824Z,
twenty-server-1   |         updatedAt: 2024-09-08T20:57:29.758Z,
twenty-server-1   |         allowImpersonation: true,
twenty-server-1   |         activationStatus: 'ACTIVE',
twenty-server-1   |         metadataVersion: 227,
twenty-server-1   |         databaseUrl: '',
twenty-server-1   |         databaseSchema: ''
twenty-server-1   |       }
twenty-server-1   |     }
twenty-server-1   |   }
codearranger commented 1 month ago

Here's my python code I used to create the fields

twenty_api.py

import requests
import logging

logger = logging.getLogger(__name__)

class TwentyAPI:
    def __init__(self, base_url, api_key):
        self.base_url = base_url
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }

    def _request(self, method, endpoint, json=None, params=None):
        url = f"{self.base_url}{endpoint}"
        logger.debug(f"Sending {method} request to: {url}")
        logger.debug(f"Parameters: {params}")
        logger.debug(f"JSON data: {json}")
        response = requests.request(method, url, headers=self.headers, json=json, params=params)
        logger.debug(f"Response status code: {response.status_code}")
        logger.debug(f"Response content: {response.text}")
        try:
            response.raise_for_status()
        except requests.exceptions.HTTPError as e:
            logger.error(f"HTTP Error: {e}")
            logger.error(f"Response headers: {response.headers}")
            raise
        return response.json()

    def _paginated_request(self, endpoint, params=None):
        all_data = []
        params = params or {}

        while True:
            response = self._request("GET", endpoint, params=params)
            data = response.get('data', {})

            if endpoint.strip('/') in data:
                all_data.extend(data[endpoint.strip('/')])
            else:
                break  # No more data to fetch

            page_info = response.get('pageInfo', {})
            if not page_info.get('hasNextPage'):
                break

            params['starting_after'] = page_info.get('endCursor')

        return all_data

    # API Keys
    def create_api_key(self, data):
        return self._request("POST", "/apiKeys", data=data)

    def get_api_key(self, id, depth=None):
        params = {"depth": depth} if depth else None
        return self._request("GET", f"/apiKeys/{id}", params=params)

    def update_api_key(self, id, data):
        return self._request("PATCH", f"/apiKeys/{id}", data=data)

    def delete_api_key(self, id):
        return self._request("DELETE", f"/apiKeys/{id}")

    # Favorites
    def create_favorite(self, data):
        return self._request("POST", "/favorites", data=data)

    def get_favorite(self, id, depth=None):
        params = {"depth": depth} if depth else None
        return self._request("GET", f"/favorites/{id}", params=params)

    def update_favorite(self, id, data):
        return self._request("PATCH", f"/favorites/{id}", data=data)

    def delete_favorite(self, id):
        return self._request("DELETE", f"/favorites/{id}")

    def find_favorite_duplicates(self, data, depth=None):
        params = {"depth": depth} if depth else None
        return self._request("POST", "/favorites/duplicates", data=data, params=params)

    # Persons
    def create_person(self, data):
        return self._request("POST", "/persons", data=data)

    def get_person(self, id, depth=None):
        params = {"depth": depth} if depth else None
        return self._request("GET", f"/persons/{id}", params=params)

    def update_person(self, id, data):
        return self._request("PATCH", f"/persons/{id}", data=data)

    def delete_person(self, id):
        return self._request("DELETE", f"/persons/{id}")

    # Notes
    def create_note(self, data):
        return self._request("POST", "/notes", data=data)

    def get_note(self, id, depth=None):
        params = {"depth": depth} if depth else None
        return self._request("GET", f"/notes/{id}", params=params)

    def update_note(self, id, data):
        return self._request("PATCH", f"/notes/{id}", data=data)

    def delete_note(self, id):
        return self._request("DELETE", f"/notes/{id}")

    # Add more methods for other endpoints as needed

    def get_all_people(self, order_by=None, limit=None):
        params = {}
        if order_by:
            params['order_by'] = f"{order_by}[AscNullsFirst]"
        if limit:
            params['first'] = limit
        return self._paginated_request("/people", params=params)

    def get_all_companies(self, order_by=None, filter=None, depth=None, limit=None):
        params = {
            "order_by": order_by,
            "filter": filter,
            "depth": depth,
            "limit": limit
        }
        return self._paginated_request("/companies", params={k: v for k, v in params.items() if v is not None})

    def get_all_opportunities(self, order_by=None, filter=None, depth=None, limit=None):
        params = {
            "order_by": order_by,
            "filter": filter,
            "depth": depth,
            "limit": limit
        }
        return self._paginated_request("/opportunities", params={k: v for k, v in params.items() if v is not None})

    # Metadata API calls

    # Objects
    def get_objects(self, limit=None, starting_after=None, ending_before=None):
        params = {
            "limit": limit,
            "starting_after": starting_after,
            "ending_before": ending_before
        }
        return self._request("GET", "/metadata/objects", params={k: v for k, v in params.items() if v is not None})

    def create_object(self, data):
        return self._request("POST", "/metadata/objects", json=data)

    def get_object(self, id):
        return self._request("GET", f"/metadata/objects/{id}")

    def update_object(self, id, data):
        return self._request("PATCH", f"/metadata/objects/{id}", json=data)

    def delete_object(self, id):
        return self._request("DELETE", f"/metadata/objects/{id}")

    # Fields
    def get_fields(self, limit=None):
        all_fields = []
        params = {"limit": limit} if limit else {}

        while True:
            response = self._request("GET", "/metadata/fields", params=params)
            data = response.get('data', {})

            if 'fields' in data:
                all_fields.extend(data['fields'])
            else:
                break  # No more data to fetch

            page_info = response.get('pageInfo', {})
            if not page_info.get('hasNextPage'):
                break

            params['starting_after'] = page_info.get('endCursor')

        logger.info(f"Retrieved {len(all_fields)} fields")
        return all_fields

    def create_field(self, data):
        response = self._request("POST", "/metadata/fields", json=data)
        created_field = response['data']['createOneField']
        logger.info(f"Created field: {created_field['name']}")
        return created_field
        # Types can be found here: https://github.com/twentyhq/twenty/blob/main/packages/twenty-zapier/src/utils/data.types.ts

    def get_field(self, id):
        return self._request("GET", f"/metadata/fields/{id}")

    def update_field(self, id, data):
        return self._request("PATCH", f"/metadata/fields/{id}", json=data)

    def delete_field(self, id):
        return self._request("DELETE", f"/metadata/fields/{id}")

    # Relations
    def get_relations(self, limit=None, starting_after=None, ending_before=None):
        params = {
            "limit": limit,
            "starting_after": starting_after,
            "ending_before": ending_before
        }
        return self._request("GET", "/metadata/relations", params={k: v for k, v in params.items() if v is not None})

    def create_relation(self, data):
        return self._request("POST", "/metadata/relations", json=data)

    def get_relation(self, id):
        return self._request("GET", f"/metadata/relations/{id}")

    def delete_relation(self, id):
        return self._request("DELETE", f"/metadata/relations/{id}")

    # Open API Schema
    def get_open_api_schema(self):
        return self._request("GET", "/metadata/open-api")

create_objects.py

import logging
from twenty_api import TwentyAPI
import os
from dotenv import load_dotenv
import uuid

# Set up logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# Load environment variables
load_dotenv()

# Initialize the API client
api = TwentyAPI(
    base_url=os.getenv("TWENTY_BASE_URL"),
    api_key=os.getenv("TWENTY_API_KEY")
)

def ensure_custom_object_exists(name_singular, name_plural, label_singular, label_plural):
    # Check if the object already exists
    objects = api.get_objects()
    existing_object = next((obj for obj in objects['data']['objects'] if obj['nameSingular'] == name_singular), None)

    if existing_object:
        logger.info(f"Custom object '{name_singular}' already exists.")
        logger.debug(f"Existing object: {existing_object}")
        return existing_object['id']
    else:
        # Create the custom object
        new_object = api.create_object({
            "nameSingular": name_singular,
            "namePlural": name_plural,
            "labelSingular": label_singular,
            "labelPlural": label_plural,
            "description": f"Custom object for {label_plural}",
            "icon": "IconBuildingSkyscraper"
        })
        logger.info(f"Created custom object '{name_singular}'.")
        return new_object['data']['createOneObject']['id']

def ensure_custom_field_exists(api, object_id, field_name, field_type, field_label, field_description, field_icon, options=None):
    fields = api.get_fields()

    existing_field = next((field for field in fields if field['name'] == field_name), None)

    if existing_field:
        logger.info(f"Field '{field_name}' already exists for object {object_id}")
        return existing_field

    new_field_data = {
        "objectMetadataId": object_id,
        "name": field_name,
        "type": field_type,
        "label": field_label,
        "description": field_description,
        "icon": field_icon,
        "isCustom": True
    }

    if field_type == "SELECT" and options:
        new_field_data["options"] = options

    logger.debug(f"Creating new field with data: {new_field_data}")
    created_field = api.create_field(new_field_data)
    logger.info(f"Created new field '{field_name}' for object {object_id}")
    return created_field

def ensure_custom_fields_exist(api, object_id, fields_to_create):
    # Fetch all fields once
    all_fields = api.get_fields()

    for field_data in fields_to_create:
        logger.debug(f"Ensuring field exists: {field_data[0]}")
        field_name, field_type, field_label, field_description, field_icon, *options = field_data

        existing_field = next((field for field in all_fields if field['name'] == field_name), None)

        if existing_field:
            logger.info(f"Field '{field_name}' already exists for object {object_id}")
            continue

        new_field_data = {
            "objectMetadataId": object_id,
            "name": field_name,
            "type": field_type,
            "label": field_label,
            "description": field_description,
            "icon": field_icon,
            "isCustom": True
        }

        if field_type == "SELECT" and options:
            new_field_data["options"] = options[0]

        logger.debug(f"Creating new field with data: {new_field_data}")
        created_field = api.create_field(new_field_data)
        logger.info(f"Created new field '{field_name}' for object {object_id}")

def get_object_id_by_name(api, name_singular):
    objects = api.get_objects()
    object_data = next((obj for obj in objects['data']['objects'] if obj['nameSingular'] == name_singular), None)
    if object_data:
        return object_data['id']
    return None

def main():
    property_object_id = ensure_custom_object_exists(
        name_singular="property",
        name_plural="properties",
        label_singular="Property",
        label_plural="Properties"
    )

    fields_to_create = [
        ("rent", "CURRENCY", "Rent", "Rent amount", "IconCurrencyDollar"),
        ("fmrrent", "CURRENCY", "HUD Fair Market Rent", "HUD Fair Market Rent amount", "IconCurrencyDollar"),
        ("piti", "CURRENCY", "PITI", "Principal, Interest, Taxes, and Insurance", "IconCurrencyDollar"),
        ("cashflow", "CURRENCY", "Comp Cash Flow", "(Avg Comp Rent - PITI) X .75", "IconCurrencyDollar"),
        ("mortgagepayment", "CURRENCY", "Mortgage Payment", "Monthly mortgage payment", "IconCurrencyDollar"),
        ("listprice", "CURRENCY", "ListPrice", "Listing price of the property", "IconCurrencyDollar"),
        ("mortgagebalance", "CURRENCY", "Mortgage Balance", "Remaining mortgage balance", "IconCurrencyDollar"),
        ("equity", "CURRENCY", "Equity", "Property equity", "IconCurrencyDollar"),
        ("compvalue", "CURRENCY", "Comp Value", "Comparable property value", "IconCurrencyDollar"),
        ("pricedifference", "CURRENCY", "Price Difference", "Comp value - listprice", "IconCurrencyDollar"),
        ("monthlytaxes", "CURRENCY", "Monthly Taxes", "Monthly property taxes", "IconCurrencyDollar"),
        ("monthlyhoa", "CURRENCY", "Monthly HOA", "Monthly HOA fees", "IconCurrencyDollar"),
        ("monthlyinsurance", "CURRENCY", "Monthly Insurance", "Monthly insurance cost", "IconCurrencyDollar"),
        ("ownerrent", "CURRENCY", "Owner Rent", "Rent amount set by owner", "IconCurrencyDollar"),
        ("offermonthly", "CURRENCY", "Offer Monthly", "Monthly offer amount", "IconCurrencyDollar"),
        ("offercashflow", "CURRENCY", "Offer Cash Flow", "Cash flow from offer", "IconCurrencyDollar"),
        ("offertotalpayments", "CURRENCY", "Offer Total Payments", "Total payments for offer", "IconCurrencyDollar"),
        ("offertotalprincipal", "CURRENCY", "Offer Total Principal Paid", "Total principal paid for offer", "IconCurrencyDollar"),
        ("offertotaltoseller", "CURRENCY", "Offer Total Paid to Seller", "Total amount paid to seller", "IconCurrencyDollar"),
        ("offerdownpayment", "CURRENCY", "Offer Down Payment", "Down payment for offer", "IconCurrencyDollar"),
        ("offerballoonpayment", "CURRENCY", "Offer Balloon Payment", "Balloon payment for offer", "IconCurrencyDollar"),
        ("offerinterestpaid", "CURRENCY", "Offer Interest Paid", "Total interest paid for offer", "IconCurrencyDollar"),
        ("offerprice", "CURRENCY", "Offer Price", "Price of the offer", "IconCurrencyDollar"),
        ("gy23price", "CURRENCY", "Gross Yield @ 23% Price", "Gross yield at 23% price", "IconCurrencyDollar"),
        ("gy30price", "CURRENCY", "Gross Yield @ 30% Price", "Gross yield at 30% price", "IconCurrencyDollar"),
        ("equitypct", "NUMBER", "Equity Percent", "Equity percentage", "IconPercentage"),
        ("interestrate", "NUMBER", "Interest Rate", "Interest rate percentage", "IconPercentage"),
        ("offerannualrate", "NUMBER", "Offer Annual Rate", "Annual rate for offer", "IconPercentage"),
        ("grossyield", "NUMBER", "Gross Yield", "Gross yield percentage", "IconPercentage"),
        ("homestatus", "SELECT", "Home Status", "Current status of the home", "IconHome", [
            {"color": "green", "id": str(uuid.uuid4()), "label": "FOR_SALE", "position": 0, "value": "FOR_SALE"},
            {"color": "turquoise", "id": str(uuid.uuid4()), "label": "SOLD", "position": 1, "value": "SOLD"}
        ]),
        ("propertytype", "TEXT", "Property Type", "Type of property", "IconBuilding"),
        ("zillowlink", "LINKS", "Zillow Link", "Link to Zillow listing", "IconLink"),
        ("propstreamlink", "LINKS", "Propstream Link", "Link to Propstream", "IconLink"),
        ("dealchecklink", "LINKS", "Dealcheck Link", "Link to Dealcheck", "IconLink"),
        ("spotcrimelink", "LINKS", "Spotcrime Link", "Link to Spotcrime", "IconLink"),
        ("realtorlink", "LINKS", "Realtor Link", "Link to Realtor listing", "IconLink"),
        ("femafloodmap", "LINKS", "FEMA Flood Map", "Link to FEMA flood map", "IconMap"),
        ("streetaddress", "TEXT", "Street Address", "Property street address", "IconMapPin"),
        ("city", "TEXT", "City", "Property city", "IconBuilding"),
        ("state", "TEXT", "State", "Property state", "IconMap"),
        ("zipcode", "TEXT", "Zipcode", "Property zipcode", "IconMapPin"),
        ("county", "TEXT", "County", "Property county", "IconMap"),
        ("compcount", "NUMBER", "Comp Count", "Number of comparable properties", "IconCalculator"),
        ("yearbuilt", "NUMBER", "Year Built", "Year the property was built", "IconCalendar"),
        ("bedrooms", "NUMBER", "Bedrooms", "Number of bedrooms", "IconBed"),
        ("offerballoonyears", "NUMBER", "Offer Balloon Years", "Number of years for balloon payment", "IconCalendar"),
        ("offertermyears", "NUMBER", "Offer Term Years", "Number of years for offer term", "IconCalendar"),
        ("dateposted", "DATE", "Orig Date Posted", "Original date the property was posted", "IconCalendar"),
        ("saledate", "DATE", "Sale Date", "Date of sale", "IconCalendar"),
        ("livingarea", "NUMBER", "Sq ft", "Living area in square feet", "IconRuler"),
        ("bathrooms", "NUMBER", "Bathrooms", "Number of bathrooms", "IconBath")
    ]

    ensure_custom_fields_exist(api, property_object_id, fields_to_create)

    # Look up the Opportunities object ID
    opportunities_object_id = get_object_id_by_name(api, "opportunity")
    if not opportunities_object_id:
        logger.error("Opportunities object not found")
        return

    # Add relation to Opportunities object
    relation_data = {
        "fromDescription": "Linked Properties",
        "fromIcon": "IconUsers",
        "fromLabel": "Properties",
        "fromName": "properties",
        "fromObjectMetadataId": property_object_id,
        "relationType": "ONE_TO_MANY",
        "toDescription": None,
        "toIcon": "IconBuildingWarehouse",
        "toLabel": "Opportunity",
        "toName": "opportunity",
        "toObjectMetadataId": opportunities_object_id
    }

    try:
        created_relation = api.create_relation(relation_data)
        logger.info(f"Created relation between Property and Opportunities objects")
    except Exception as e:
        logger.error(f"Failed to create relation: {str(e)}")

    logger.info("Custom object, fields, and relation have been created or verified.")

if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        logger.exception("An error occurred during execution:")
codearranger commented 1 month ago

It seems to break once I add about 20 new fields

FelixMalfait commented 1 month ago

@joecryptotoo this is a limitation due to pg_graphql an extension we rely on. Some fields are "composite" (e.g. address create address street, address city, address postcode, etc. ; currency creates 2 field for value + currency ; Links creates 2 columns also). You're probably quite close to the limit. 20 fields should be okay but it seems you have more.

@Weiko is working on removing our dependency to pg_graphql, this will be completed in the next couple of weeks. So probably by end of September this should be fixed

FelixMalfait commented 1 month ago

Closing as this will be solved by end of month