From 2dc6034a6ca9ec3c6eafe4fc6671e1e177cc1efa Mon Sep 17 00:00:00 2001 From: martmull Date: Mon, 22 Jun 2026 18:47:30 +0200 Subject: [PATCH] Add twenty meeting bot to internal application ci (#21974) - remove useless github-connector (validated with @charlesBochet) - add twenty-meeting-bot to internal apps --- .../community/github-connector/.gitignore | 32 - .../community/github-connector/.nvmrc | 1 - .../community/github-connector/.oxlintrc.json | 37 - .../community/github-connector/.yarnrc.yml | 1 - .../community/github-connector/README.md | 254 -- .../community/github-connector/package.json | 35 - .../public/screenshots/app-listing.png | Bin 44831 -> 0 bytes .../public/screenshots/contributor-stats.png | Bin 50313 -> 0 bytes .../public/screenshots/github-dashboard.png | Bin 72904 -> 0 bytes .../public/screenshots/pull-requests-view.png | Bin 93238 -> 0 bytes .../src/__tests__/global-setup.ts | 54 - .../src/__tests__/helpers/metadata.ts | 128 - .../logic-functions.integration-test.ts | 73 - .../src/__tests__/schema.integration-test.ts | 125 - .../webhook-signature.integration-test.ts | 150 - .../src/application-config.ts | 66 - .../github-connector/src/default-role.ts | 16 - .../src/modules/github/connector/auth.ts | 122 - .../src/modules/github/connector/config.ts | 55 - .../github/connector/count-across-repos.ts | 45 - .../modules/github/connector/github-client.ts | 157 - .../modules/github/connector/github-user.ts | 5 - .../github/connector/webhook-payload.ts | 15 - .../github/connector/webhook-signature.ts | 58 - .../contributor-stats.command-menu-item.ts | 11 - .../fetch-contributors.command-menu-item.ts | 11 - .../contributor/components/leaderboard.tsx | 282 -- .../github/contributor/components/theme.ts | 23 - ...gned-project-items-on-contributor.field.ts | 24 - .../authored-issues-on-contributor.field.ts | 22 - .../authored-prs-on-contributor.field.ts | 23 - ...rged-pull-requests-on-contributor.field.ts | 24 - .../review-events-on-contributor.field.ts | 23 - .../fields/reviews-on-contributor.field.ts | 23 - .../contributor-stats.front-component.tsx | 681 ---- .../fetch-contributors.front-component.tsx | 97 - .../top-pr-authors.front-component.tsx | 66 - .../top-reviewers.front-component.tsx | 66 - .../graphql/github/count-contributors.ts | 20 - .../graphql/github/fetch-contributors.ts | 52 - .../graphql/mutations/batch-upsert.ts | 21 - .../graphql/queries/find-by-gh-login.ts | 27 - .../graphql/queries/search-contributors.ts | 52 - .../contributor-stats.logic-function.ts | 389 --- .../count-contributors.logic-function.ts | 25 - .../fetch-contributors.logic-function.ts | 75 - .../search-contributors.logic-function.ts | 44 - .../top-contributors.logic-function.ts | 266 -- .../modules/github/contributor/normalizers.ts | 47 - .../contributor/objects/contributor.object.ts | 72 - .../contributor/types/contributor-row.ts | 10 - .../github/contributor/utils/is-bot-login.ts | 30 - .../views/all-contributors.view.ts | 51 - .../fetch-issues.command-menu-item.ts | 11 - .../issue/fields/author-on-issue.field.ts | 25 - .../fields/project-items-on-issue.field.ts | 23 - .../fetch-issues.front-component.tsx | 100 - .../issue/graphql/github/count-issues.ts | 26 - .../issue/graphql/github/fetch-issues.ts | 66 - .../issue/graphql/mutations/batch-upsert.ts | 27 - .../queries/find-by-number-and-repo.ts | 40 - .../count-issues.logic-function.ts | 25 - .../fetch-issues.logic-function.ts | 93 - .../issues.navigation-menu-item.ts | 11 - .../src/modules/github/issue/normalizers.ts | 63 - .../github/issue/objects/issue.object.ts | 134 - .../github/issue/types/github-issue.ts | 13 - .../modules/github/issue/types/issue-row.ts | 15 - .../github/issue/views/all-issues.view.ts | 87 - .../handle-webhook.logic-function.ts | 266 -- .../contributors.navigation-menu-item.ts | 14 - .../github-dashboard.navigation-menu-item.ts | 17 - .../github-folder.navigation-menu-item.ts | 13 - .../github-dashboard.page-layout.ts | 281 -- .../fetch-project-items.command-menu-item.ts | 11 - .../linked-issue-on-project-item.field.ts | 24 - .../fields/linked-pr-on-project-item.field.ts | 25 - .../main-assignee-on-project-item.field.ts | 25 - .../fetch-project-items.front-component.tsx | 99 - .../graphql/github/count-project-items.ts | 41 - .../github/fetch-project-item-by-node-id.ts | 31 - .../graphql/github/fetch-project-items.ts | 78 - .../project-item/graphql/github/fragments.ts | 46 - .../graphql/mutations/batch-upsert.ts | 29 - .../count-project-items.logic-function.ts | 48 - .../fetch-project-items.logic-function.ts | 58 - .../project-items.navigation-menu-item.ts | 11 - .../github/project-item/normalizers.ts | 106 - .../objects/project-item.object.ts | 181 -- .../types/github-project-v2-item.ts | 7 - .../project-item/types/project-item-row.ts | 16 - .../project-item/types/project-v2-item.ts | 42 - .../project-item/utils/extract-field-value.ts | 34 - .../views/all-project-items.view.ts | 123 - .../pull-request-on-review-event.field.ts | 30 - .../fields/review-on-review-event.field.ts | 30 - .../fields/reviewer-on-review-event.field.ts | 30 - .../graphql/mutations/batch-upsert.ts | 25 - .../graphql/queries/find-events-for-pair.ts | 61 - ...uest-review-events.navigation-menu-item.ts | 15 - .../pull-request-review-event/normalizers.ts | 48 - .../pull-request-review-event.object.ts | 97 - .../types/github-review.ts | 9 - .../types/pull-request-review-event-row.ts | 10 - .../all-pull-request-review-events.view.ts | 78 - .../fields/events-on-review.field.ts | 23 - .../fields/pull-request-on-review.field.ts | 30 - .../fields/reviewer-on-review.field.ts | 30 - .../graphql/mutations/batch-upsert.ts | 20 - ...ute-pull-request-reviews.logic-function.ts | 173 - ...ll-request-reviews.navigation-menu-item.ts | 14 - .../objects/pull-request-review.object.ts | 133 - .../types/pull-request-review-row.ts | 12 - .../utils/build-consolidated-row.ts | 70 - .../consolidate-reviews.integration-test.ts | 211 -- .../utils/consolidate-reviews.ts | 54 - .../views/all-pull-request-reviews.view.ts | 95 - .../fetch-prs.command-menu-item.ts | 11 - .../fields/author-on-pull-request.field.ts | 25 - .../fields/merger-on-pull-request.field.ts | 25 - .../project-items-on-pull-request.field.ts | 23 - .../review-events-on-pull-request.field.ts | 23 - .../fields/reviews-on-pull-request.field.ts | 23 - .../fetch-prs.front-component.tsx | 103 - .../graphql/github/count-pull-requests.ts | 26 - .../graphql/github/fetch-pull-requests.ts | 82 - .../graphql/mutations/batch-upsert.ts | 26 - .../graphql/mutations/update-pull-request.ts | 14 - .../graphql/queries/find-by-github-number.ts | 55 - .../count-prs.logic-function.ts | 25 - .../fetch-prs.logic-function.ts | 246 -- .../pull-requests.navigation-menu-item.ts | 11 - .../github/pull-request/normalizers.ts | 64 - .../objects/pull-request.object.ts | 133 - .../pull-request/types/github-pull-request.ts | 14 - .../pull-request/types/pull-request-row.ts | 15 - .../views/all-pull-requests.view.ts | 84 - .../src/modules/shared/call-app-route.ts | 50 - .../src/modules/shared/fixtures.ts | 3 - .../src/modules/shared/retry.ts | 49 - .../src/modules/shared/timing.ts | 17 - .../src/modules/shared/twenty-client.ts | 48 - .../src/modules/shared/types.ts | 9 - .../modules/shared/universal-identifiers.ts | 30 - .../community/github-connector/tsconfig.json | 42 - .../github-connector/tsconfig.spec.json | 9 - .../github-connector/vitest.config.ts | 30 - .../community/github-connector/yarn.lock | 2816 ----------------- .../internal/twenty-meeting-bot/package.json | 2 + .../complete-call-recording-ingestion.util.ts | 2 +- .../data/create-call-recording.util.ts | 6 +- .../fetch-calendar-events-by-filter.util.ts | 2 +- .../data/fetch-calendar-events-by-ids.util.ts | 2 +- ...alendar-events-by-starts-at-values.util.ts | 2 +- ...l-recordings-by-calendar-event-ids.util.ts | 2 +- .../find-call-recordings-by-filter.util.ts | 2 +- .../data/find-call-recordings-by-ids.util.ts | 2 +- ...ind-open-scheduled-call-recordings.util.ts | 2 +- .../data/update-call-recording.util.ts | 2 +- .../cancel-call-recording-request.util.ts | 2 +- ...complete-and-charge-call-recording.util.ts | 2 +- .../converge-diverged-call-recordings.util.ts | 2 +- .../flows/ensure-meeting-bot.util.ts | 2 +- .../flows/handle-recall-webhook.util.ts | 2 +- .../heal-call-recordings-missing-bot.util.ts | 2 +- .../persist-call-recording-progress.util.ts | 2 +- .../flows/reap-orphaned-meeting-bots.util.ts | 2 +- .../flows/reconcile-meeting-bot.util.ts | 2 +- .../reschedule-call-recording-bot.util.ts | 2 +- .../internal/twenty-meeting-bot/tsconfig.json | 2 +- .../twenty-meeting-bot/vitest.config.ts | 16 +- .../internal/twenty-meeting-bot/yarn.lock | 82 + 172 files changed, 113 insertions(+), 11833 deletions(-) delete mode 100644 packages/twenty-apps/community/github-connector/.gitignore delete mode 100644 packages/twenty-apps/community/github-connector/.nvmrc delete mode 100644 packages/twenty-apps/community/github-connector/.oxlintrc.json delete mode 100644 packages/twenty-apps/community/github-connector/.yarnrc.yml delete mode 100644 packages/twenty-apps/community/github-connector/README.md delete mode 100644 packages/twenty-apps/community/github-connector/package.json delete mode 100644 packages/twenty-apps/community/github-connector/public/screenshots/app-listing.png delete mode 100644 packages/twenty-apps/community/github-connector/public/screenshots/contributor-stats.png delete mode 100644 packages/twenty-apps/community/github-connector/public/screenshots/github-dashboard.png delete mode 100644 packages/twenty-apps/community/github-connector/public/screenshots/pull-requests-view.png delete mode 100644 packages/twenty-apps/community/github-connector/src/__tests__/global-setup.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/__tests__/helpers/metadata.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/__tests__/logic-functions.integration-test.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/__tests__/schema.integration-test.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/__tests__/webhook-signature.integration-test.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/application-config.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/default-role.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/connector/auth.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/connector/config.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/connector/count-across-repos.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/connector/github-client.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/connector/github-user.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/connector/webhook-payload.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/connector/webhook-signature.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/command-menu-items/contributor-stats.command-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/command-menu-items/fetch-contributors.command-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/components/leaderboard.tsx delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/components/theme.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/assigned-project-items-on-contributor.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/authored-issues-on-contributor.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/authored-prs-on-contributor.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/merged-pull-requests-on-contributor.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/review-events-on-contributor.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/reviews-on-contributor.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/contributor-stats.front-component.tsx delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/fetch-contributors.front-component.tsx delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/top-pr-authors.front-component.tsx delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/top-reviewers.front-component.tsx delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/github/count-contributors.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/github/fetch-contributors.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/mutations/batch-upsert.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/queries/find-by-gh-login.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/queries/search-contributors.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/contributor-stats.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/count-contributors.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/fetch-contributors.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/search-contributors.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/top-contributors.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/normalizers.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/objects/contributor.object.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/types/contributor-row.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/utils/is-bot-login.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/contributor/views/all-contributors.view.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/command-menu-items/fetch-issues.command-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/fields/author-on-issue.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/fields/project-items-on-issue.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/front-components/fetch-issues.front-component.tsx delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/github/count-issues.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/github/fetch-issues.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/mutations/batch-upsert.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/queries/find-by-number-and-repo.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/logic-functions/count-issues.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/logic-functions/fetch-issues.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/navigation-menu-items/issues.navigation-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/normalizers.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/objects/issue.object.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/types/github-issue.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/types/issue-row.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/issue/views/all-issues.view.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/logic-functions/handle-webhook.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/contributors.navigation-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/github-dashboard.navigation-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/github-folder.navigation-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/page-layouts/github-dashboard.page-layout.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/command-menu-items/fetch-project-items.command-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/linked-issue-on-project-item.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/linked-pr-on-project-item.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/main-assignee-on-project-item.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/front-components/fetch-project-items.front-component.tsx delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/count-project-items.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fetch-project-item-by-node-id.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fetch-project-items.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fragments.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/mutations/batch-upsert.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/logic-functions/count-project-items.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/logic-functions/fetch-project-items.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/navigation-menu-items/project-items.navigation-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/normalizers.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/objects/project-item.object.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/github-project-v2-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/project-item-row.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/project-v2-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/utils/extract-field-value.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/project-item/views/all-project-items.view.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/pull-request-on-review-event.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/review-on-review-event.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/reviewer-on-review-event.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/graphql/mutations/batch-upsert.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/graphql/queries/find-events-for-pair.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/navigation-menu-items/pull-request-review-events.navigation-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/normalizers.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/objects/pull-request-review-event.object.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/types/github-review.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/types/pull-request-review-event-row.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/views/all-pull-request-review-events.view.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/events-on-review.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/pull-request-on-review.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/reviewer-on-review.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/graphql/mutations/batch-upsert.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/logic-functions/recompute-pull-request-reviews.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/navigation-menu-items/pull-request-reviews.navigation-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/objects/pull-request-review.object.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/types/pull-request-review-row.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/build-consolidated-row.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/consolidate-reviews.integration-test.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/consolidate-reviews.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/views/all-pull-request-reviews.view.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/command-menu-items/fetch-prs.command-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/author-on-pull-request.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/merger-on-pull-request.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/project-items-on-pull-request.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/review-events-on-pull-request.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/reviews-on-pull-request.field.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/front-components/fetch-prs.front-component.tsx delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/github/count-pull-requests.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/github/fetch-pull-requests.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/mutations/batch-upsert.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/mutations/update-pull-request.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/queries/find-by-github-number.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/logic-functions/count-prs.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/logic-functions/fetch-prs.logic-function.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/navigation-menu-items/pull-requests.navigation-menu-item.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/normalizers.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/objects/pull-request.object.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/types/github-pull-request.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/types/pull-request-row.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/github/pull-request/views/all-pull-requests.view.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/shared/call-app-route.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/shared/fixtures.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/shared/retry.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/shared/timing.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/shared/twenty-client.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/shared/types.ts delete mode 100644 packages/twenty-apps/community/github-connector/src/modules/shared/universal-identifiers.ts delete mode 100644 packages/twenty-apps/community/github-connector/tsconfig.json delete mode 100644 packages/twenty-apps/community/github-connector/tsconfig.spec.json delete mode 100644 packages/twenty-apps/community/github-connector/vitest.config.ts delete mode 100644 packages/twenty-apps/community/github-connector/yarn.lock diff --git a/packages/twenty-apps/community/github-connector/.gitignore b/packages/twenty-apps/community/github-connector/.gitignore deleted file mode 100644 index 5fc90b42ab..0000000000 --- a/packages/twenty-apps/community/github-connector/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn - -# codegen -generated - -# dev -/dist/ -.twenty - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files -.env* - -# typescript -*.tsbuildinfo -*.d.ts diff --git a/packages/twenty-apps/community/github-connector/.nvmrc b/packages/twenty-apps/community/github-connector/.nvmrc deleted file mode 100644 index 341cb50613..0000000000 --- a/packages/twenty-apps/community/github-connector/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -24.5.0 diff --git a/packages/twenty-apps/community/github-connector/.oxlintrc.json b/packages/twenty-apps/community/github-connector/.oxlintrc.json deleted file mode 100644 index 369b9cfd76..0000000000 --- a/packages/twenty-apps/community/github-connector/.oxlintrc.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "plugins": ["typescript"], - "categories": { - "correctness": "off" - }, - "ignorePatterns": ["node_modules", "dist"], - "rules": { - "no-unused-vars": "off", - - "typescript/no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "^_" - } - ], - "typescript/no-explicit-any": "off" - }, - "overrides": [ - { - "files": ["**/*.logic-function.ts", "**/logic-functions/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { - "patterns": [ - { - "group": ["twenty-shared", "twenty-shared/*"], - "message": "Logic functions must not import from twenty-shared directly. Import runtime types and helpers from `twenty-sdk/logic-function` instead so the logic-function bundle stays minimal." - } - ] - } - ] - } - } - ] -} diff --git a/packages/twenty-apps/community/github-connector/.yarnrc.yml b/packages/twenty-apps/community/github-connector/.yarnrc.yml deleted file mode 100644 index 3186f3f079..0000000000 --- a/packages/twenty-apps/community/github-connector/.yarnrc.yml +++ /dev/null @@ -1 +0,0 @@ -nodeLinker: node-modules diff --git a/packages/twenty-apps/community/github-connector/README.md b/packages/twenty-apps/community/github-connector/README.md deleted file mode 100644 index 0b2e68c854..0000000000 --- a/packages/twenty-apps/community/github-connector/README.md +++ /dev/null @@ -1,254 +0,0 @@ -# GitHub Connector - -Sync pull requests, issues, contributors and project items from GitHub into -Twenty, and react to GitHub webhook events in real time. - -This app showcases how to build a non-trivial third-party connector with the -Twenty SDK: custom objects with rich relationships, navigation menu items, -table views, a dashboard page layout, logic functions for periodic syncs, an -HTTP webhook handler, and authenticated GraphQL/REST calls against an -external provider. - -![GitHub Connector marketplace listing in Settings → Apps with About / Content / Permissions / Settings tabs](public/screenshots/app-listing.png) - -![GitHub Dashboard with PR / review counters, weekly histograms, and top-contributor leaderboards](public/screenshots/github-dashboard.png) - -![Pull Requests view with the Fetch Pull Requests command](public/screenshots/pull-requests-view.png) - -![Contributor detail page with the Contributor Stats panel showing PRs authored, merged and reviewed over time](public/screenshots/contributor-stats.png) - -## What it adds to your workspace - -### Custom objects - -Six custom objects, each with fields, relationships and table views: - -- `pullRequest` -- `pullRequestReview` -- `pullRequestReviewEvent` -- `issue` -- `projectItem` -- `contributor` - -### Navigation - -A top-level **GitHub** folder in the left sidebar with: - -- Pull Requests -- Issues -- Project Items -- Contributors -- Pull Request Reviews -- Pull Request Review Events -- GitHub Dashboard (a page layout that aggregates PR activity over time and - surfaces top contributors) - -### Logic functions - -| Function | Trigger | -| --------------------------------- | ------------------------------------------------ | -| `count-prs` | HTTP `POST /github/count-prs` | -| `fetch-prs` | HTTP `POST /github/fetch-prs` | -| `count-issues` | HTTP `POST /github/count-issues` | -| `fetch-issues` | HTTP `POST /github/fetch-issues` | -| `count-contributors` | HTTP `POST /github/count-contributors` | -| `fetch-contributors` | HTTP `POST /github/fetch-contributors` | -| `count-project-items` | HTTP `POST /github/count-project-items` | -| `fetch-project-items` | HTTP `POST /github/fetch-project-items` | -| `handle-github-webhook` | HTTP `POST /github/webhook` (no auth, signed) | -| `search-contributors` | HTTP `POST /contributors/search` | -| `contributor-stats` | HTTP `POST /contributors/stats` | -| `top-contributors` | HTTP `POST /contributors/top` | -| `recompute-pull-request-reviews` | HTTP `POST /pull-request-reviews/recompute` | - -### Front components - -Seven front components surface the connector inside the Twenty UI: - -- **Fetch Pull Requests** — command on the Pull Request object -- **Fetch Issues** — command on the Issue object -- **Fetch Contributors** — command on the Contributor object -- **Fetch Project Items** — command on the Project Item object -- **Contributor Stats** — panel on the Contributor object that renders a - bar chart of PRs authored / merged / reviewed over the selected period -- **Top PR Authors** — dashboard widget that ranks the top 20 PR authors - over the last 90 days -- **Top Reviewers** — dashboard widget that ranks the top 20 PR reviewers - over the last 90 days - -## Install - -You have two options. Use **dev mode** for a tight edit/test loop while -iterating on the app, or **install** for a one-shot deploy. - -### Option A — Live development (`yarn twenty dev`) - -Use this when you want every code change to be re-synced into your local -Twenty server automatically. - -```bash -cd packages/twenty-apps/community/github-connector -yarn install - -# Register your local Twenty server as a remote (interactive prompt). -# When asked for the URL use http://localhost:2021 and paste an API key -# from Settings -> Developers in the Twenty UI. -yarn twenty remote:add - -# Build, install, and watch for changes. -yarn twenty dev -``` - -The first `yarn twenty dev` run installs the app on the remote and starts -watching `src/`. Edit any file and the change is re-synced within seconds. - -### Option B — One-shot install - -```bash -cd packages/twenty-apps/community/github-connector -yarn install -yarn twenty remote:add # same prompts as above -yarn twenty app:install # builds and installs once -``` - -## Configure authentication - -Once the app is installed, open the Twenty UI and go to -**Settings → Apps → GitHub Connector**. You only need one of the two auth -methods. - -### Option 1 — Personal Access Token (recommended for trying it out) - -| Variable | Required | Notes | -| --------------- | -------- | -------------------------------------------------------------------- | -| `GITHUB_TOKEN` | yes | Fine-grained PAT (`github_pat_…`). See permissions below. | - -Create a fine-grained PAT at -: - -1. **Resource owner**: the org (or user) that owns the repos in - `GITHUB_REPOS` and the projects in `GITHUB_PROJECTS`. Org-owned tokens - must be approved by an org admin before they can read org resources. -2. **Repository access**: pick the specific repos (or "All repositories"). -3. **Repository permissions** — set to **Read-only**: - - `Contents` - - `Issues` - - `Pull requests` - - `Metadata` (selected automatically) -4. **Organization permissions** — only if you want to sync GitHub Projects - (v2): set `Projects` to **Read-only**. -5. Generate, then copy the `github_pat_…` value. - -Classic PATs are intentionally not supported — fine-grained tokens are -scoped per-repo/per-org and avoid the all-or-nothing `repo` scope. - -When `GITHUB_TOKEN` is set, it always wins regardless of any GitHub App -config below. - -### Option 2 — GitHub App (recommended for production / org-wide installs) - -| Variable | Required | Notes | -| ---------------------------- | -------- | ---------------------------------------------------------------------- | -| `GITHUB_APP_ID` | yes | Numeric App ID from the GitHub App settings page. | -| `GITHUB_APP_PRIVATE_KEY` | yes | PEM private key (BEGIN/END PRIVATE KEY block). Newlines are tolerant. | -| `GITHUB_APP_INSTALLATION_ID` | yes | The installation id of the App on your org/user. | - -To create one: - -1. (or - `https://github.com/organizations//settings/apps/new`). -2. Grant the App these **repository permissions**: `Contents: Read`, - `Issues: Read`, `Pull Requests: Read`, `Metadata: Read`. For Projects v2 - add `Organization → Projects: Read`. -3. Generate a private key (downloads a `.pem`). -4. Install the App on your org/user — the URL bar of the post-install page - contains the installation id, e.g. `.../installations/12345678`. -5. Paste the App ID, the PEM contents, and the installation ID into the - variables above. - -The connector exchanges the App credentials for a short-lived installation -token (cached in-memory until shortly before expiry) and uses it for all -GitHub calls. - -### Common variables - -| Variable | Required | Notes | -| ------------------------- | -------- | --------------------------------------------------------------------------------------- | -| `GITHUB_REPOS` | yes | Comma-separated `owner/repo` list, e.g. `octocat/hello-world,octo-org/octo-repo`. | -| `GITHUB_PROJECTS` | no | Comma-separated GitHub Projects (v2). See format below. | -| `GITHUB_WEBHOOK_SECRET` | no | Shared secret to verify `X-Hub-Signature-256`. When unset, signatures are not verified. | - -`GITHUB_PROJECTS` accepts entries in either of these forms: - -- `owner/number` — e.g. `twentyhq/24,octo/3`. Owner can be an org or a user. -- Full project URL — e.g. `https://github.com/orgs/twentyhq/projects/24` or - `https://github.com/users/octocat/projects/3`. - -## Running a sync - -In the Twenty UI, open any of the GitHub objects (e.g. **Pull Requests**) and -trigger the matching command from the command palette (`Cmd/Ctrl+K`): - -| View | Command | Reads from | -| ----------------- | --------------------- | ----------------- | -| Pull Requests | Fetch Pull Requests | `GITHUB_REPOS` | -| Issues | Fetch Issues | `GITHUB_REPOS` | -| Contributors | Fetch Contributors | `GITHUB_REPOS` | -| Project Items | Fetch Project Items | `GITHUB_PROJECTS` | - -Each command iterates over every entry in the relevant variable and shows a -progress bar. - -## Webhooks - -Point a GitHub repo or App webhook at the public URL of your Twenty server, -path `POST /github/webhook`. Recommended event subscriptions: - -- Pull requests -- Pull request reviews -- Issues -- Project (v2) items - -Set the same value as `GITHUB_WEBHOOK_SECRET` on both sides to enable HMAC -verification. For local testing, expose your dev server with -[smee.io](https://smee.io/) or `ngrok` and use that URL as the webhook URL on -GitHub. - -> **Heads up — raw body required for signatures.** HMAC verification needs -> the original request body bytes. The Twenty SDK currently parses JSON -> requests before handing them to logic functions, so when the runtime -> delivers an already-parsed body the connector logs a warning and rejects -> the delivery instead of silently accepting it. Until the SDK exposes the -> raw bytes for HTTP routes, either leave `GITHUB_WEBHOOK_SECRET` unset (and -> rely on a hard-to-guess `/github/webhook` URL plus IP allow-listing), or -> terminate signature verification at a reverse proxy in front of Twenty. - -## How auth resolution works - -`src/modules/github/connector/auth.ts` returns a token using the following -order: - -1. `GITHUB_TOKEN` (fine-grained PAT, `github_pat_…`) if present. Classic - PATs are rejected at startup. -2. Cached installation token, if still valid. -3. Fresh installation token minted from the GitHub App credentials. - -This makes the example easy to try in 30 seconds with a PAT, while still -demonstrating the production-grade GitHub App flow. - -## Tests - -The app ships with a small integration test suite that runs against a local -`twenty-app-dev-test` container. - -```bash -docker run -d --name twenty-app-dev-test \ - -p 2021:2021 twentycrm/twenty-app-dev:v2.0.3 - -cd packages/twenty-apps/community/github-connector -yarn test -``` - -The suite installs the app into the container, then asserts that every -object/field/logic-function is wired up and that webhook signature -verification behaves correctly. diff --git a/packages/twenty-apps/community/github-connector/package.json b/packages/twenty-apps/community/github-connector/package.json deleted file mode 100644 index 131fcfc984..0000000000 --- a/packages/twenty-apps/community/github-connector/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "github-connector", - "version": "0.2.0", - "license": "MIT", - "engines": { - "node": "^24.5.0", - "npm": "please-use-yarn", - "yarn": ">=4.0.2" - }, - "keywords": [ - "twenty-app" - ], - "packageManager": "yarn@4.13.0", - "scripts": { - "twenty": "twenty", - "lint": "oxlint -c .oxlintrc.json .", - "lint:fix": "oxlint --fix -c .oxlintrc.json .", - "test": "vitest run", - "test:watch": "vitest" - }, - "dependencies": { - "twenty-client-sdk": "2.13.0", - "twenty-sdk": "2.13.0" - }, - "devDependencies": { - "@types/node": "^24.7.2", - "@types/react": "^19.0.0", - "oxlint": "^0.16.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "typescript": "^5.9.3", - "vite-tsconfig-paths": "^4.2.1", - "vitest": "^4.0.0" - } -} diff --git a/packages/twenty-apps/community/github-connector/public/screenshots/app-listing.png b/packages/twenty-apps/community/github-connector/public/screenshots/app-listing.png deleted file mode 100644 index 1b3b88a238e1e13959b41d4605e342ff5f66ab27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44831 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ي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 diff --git a/packages/twenty-apps/community/github-connector/public/screenshots/github-dashboard.png b/packages/twenty-apps/community/github-connector/public/screenshots/github-dashboard.png deleted file mode 100644 index f33c31d23ad006a6be70694dd2e93175ad53bf48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72904 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%0A9zxj2Kt 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 Vt>SA%Cxq!jEzo;8A)$XP{~tSUhI;@2 diff --git a/packages/twenty-apps/community/github-connector/public/screenshots/pull-requests-view.png b/packages/twenty-apps/community/github-connector/public/screenshots/pull-requests-view.png deleted file mode 100644 index c8ad0ccb9ce5f4d0a81a8edc08ab1bf9dc440018..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93238 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 z 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 diff --git a/packages/twenty-apps/community/github-connector/src/__tests__/global-setup.ts b/packages/twenty-apps/community/github-connector/src/__tests__/global-setup.ts deleted file mode 100644 index c3f48da953..0000000000 --- a/packages/twenty-apps/community/github-connector/src/__tests__/global-setup.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; - -import { appDevOnce, appUninstall } from 'twenty-sdk/cli'; - -const APP_PATH = process.cwd(); -const CONFIG_DIR = path.join(os.homedir(), '.twenty'); - -function writeConfig(apiUrl: string, apiKey: string) { - const payload = JSON.stringify( - { - remotes: { - local: { apiUrl, apiKey, accessToken: apiKey }, - }, - defaultRemote: 'local', - }, - null, - 2, - ); - - fs.mkdirSync(CONFIG_DIR, { recursive: true }); - fs.writeFileSync(path.join(CONFIG_DIR, 'config.test.json'), payload); -} - -export async function setup() { - const apiUrl = process.env.TWENTY_API_URL!; - const apiKey = process.env.TWENTY_API_KEY!; - - writeConfig(apiUrl, apiKey); - - await appUninstall({ appPath: APP_PATH }).catch(() => {}); - - const result = await appDevOnce({ - appPath: APP_PATH, - onProgress: (message: string) => console.log(`[dev] ${message}`), - }); - - if (!result.success) { - throw new Error( - `Dev sync failed: ${result.error?.message ?? 'Unknown error'}`, - ); - } -} - -export async function teardown() { - const uninstallResult = await appUninstall({ appPath: APP_PATH }); - - if (!uninstallResult.success) { - console.warn( - `App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`, - ); - } -} diff --git a/packages/twenty-apps/community/github-connector/src/__tests__/helpers/metadata.ts b/packages/twenty-apps/community/github-connector/src/__tests__/helpers/metadata.ts deleted file mode 100644 index 8d17ff9668..0000000000 --- a/packages/twenty-apps/community/github-connector/src/__tests__/helpers/metadata.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { MetadataApiClient } from 'twenty-client-sdk/metadata'; - -const metadata = () => - new MetadataApiClient({ - headers: { - Authorization: `Bearer ${process.env.TWENTY_API_KEY}`, - }, - }); - -export async function findObjectByName(name: string) { - const client = metadata(); - const result = await client.query({ - objects: { - __args: { - filter: { isCustom: { is: true } }, - paging: { first: 50 }, - }, - edges: { - node: { - nameSingular: true, - fields: { - __args: { paging: { first: 500 } }, - edges: { node: { name: true, type: true } }, - }, - }, - }, - }, - }); - - return result.objects?.edges?.find((e) => e.node.nameSingular === name)?.node; -} - -type ExecutionResult = { - data: unknown; - status: string; - duration: number; - error: unknown; -}; - -export async function findLogicFunctionId( - universalIdentifier: string, -): Promise { - const client = metadata(); - const result = await client.query({ - findManyLogicFunctions: { - id: true, - universalIdentifier: true, - }, - }); - - const fn = result.findManyLogicFunctions?.find( - (f) => f.universalIdentifier === universalIdentifier, - ); - - if (!fn) { - throw new Error(`Logic function ${universalIdentifier} not found`); - } - - return fn.id; -} - -export async function executeLogicFunction( - id: string, - payload: Record, -): Promise { - const client = metadata(); - const result = await client.mutation({ - executeOneLogicFunction: { - __args: { input: { id, payload } }, - data: true, - status: true, - duration: true, - error: true, - }, - }); - - return result.executeOneLogicFunction as ExecutionResult; -} - -const BASE_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2021'; - -export async function callRoute( - path: string, - body: Record | string, - options: { - method?: string; - auth?: boolean; - headers?: Record; - } = {}, -): Promise<{ status: number; data: unknown }> { - const headers: Record = { - 'Content-Type': 'application/json', - ...(options.headers ?? {}), - }; - - if (options.auth) { - headers['Authorization'] = `Bearer ${process.env.TWENTY_API_KEY}`; - } - - const res = await fetch(`${BASE_URL}/s${path}`, { - method: options.method ?? 'POST', - headers, - body: typeof body === 'string' ? body : JSON.stringify(body), - }); - - let data: unknown = null; - try { - data = await res.json(); - } catch { - data = await res.text().catch(() => null); - } - return { status: res.status, data }; -} - -export async function findInstalledApp(universalIdentifier: string) { - const client = metadata(); - const result = await client.query({ - findManyApplications: { - id: true, - name: true, - universalIdentifier: true, - }, - }); - - return result.findManyApplications?.find( - (app) => app.universalIdentifier === universalIdentifier, - ); -} diff --git a/packages/twenty-apps/community/github-connector/src/__tests__/logic-functions.integration-test.ts b/packages/twenty-apps/community/github-connector/src/__tests__/logic-functions.integration-test.ts deleted file mode 100644 index 91f22016c0..0000000000 --- a/packages/twenty-apps/community/github-connector/src/__tests__/logic-functions.integration-test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { beforeAll, describe, expect, it } from 'vitest'; - -import { - executeLogicFunction, - findLogicFunctionId, -} from './helpers/metadata'; - -const COUNT_PRS_FN_UI = '082227ae-2acc-4320-8d31-62ad6c443da6'; -const COUNT_ISSUES_FN_UI = 'd8cc32bf-6be9-44fc-920a-8bba510f045f'; -const COUNT_PROJECT_ITEMS_FN_UI = 'f7a3e1b2-5c4d-4e6f-8a9b-0d1c2e3f4a5b'; -const COUNT_CONTRIBUTORS_FN_UI = 'fe0a6f00-0d63-4cb9-9b3c-1d8186181830'; -const HANDLE_WEBHOOK_FN_UI = '22b199b3-2851-4a4f-99fd-4e79c188fe7d'; - -const fnIds: Record = {}; - -beforeAll(async () => { - fnIds.prs = await findLogicFunctionId(COUNT_PRS_FN_UI); - fnIds.issues = await findLogicFunctionId(COUNT_ISSUES_FN_UI); - fnIds.projectItems = await findLogicFunctionId(COUNT_PROJECT_ITEMS_FN_UI); - fnIds.contributors = await findLogicFunctionId(COUNT_CONTRIBUTORS_FN_UI); - fnIds.webhook = await findLogicFunctionId(HANDLE_WEBHOOK_FN_UI); -}); - -describe('logic functions are wired up', () => { - it('count-prs is reachable and returns the expected payload shape', async () => { - const result = await executeLogicFunction(fnIds.prs, { - body: { repos: ['fixture-org/fixture-repo'] }, - }); - expect(['SUCCESS', 'ERROR']).toContain(result.status); - if (result.status === 'SUCCESS') { - const data = result.data as { - totalPages: number; - repos: Array<{ owner: string; repo: string; pages: number }>; - }; - expect(typeof data.totalPages).toBe('number'); - expect(Array.isArray(data.repos)).toBe(true); - } - }); - - it('count-issues is reachable', async () => { - const result = await executeLogicFunction(fnIds.issues, { - body: { repos: ['fixture-org/fixture-repo'] }, - }); - expect(['SUCCESS', 'ERROR']).toContain(result.status); - }); - - it('count-project-items is reachable', async () => { - const result = await executeLogicFunction(fnIds.projectItems, { - body: { projects: [{ owner: 'fixture-org', number: 9999999 }] }, - }); - expect(['SUCCESS', 'ERROR']).toContain(result.status); - }); - - it('count-contributors iterates configured repos and returns the per-repo split', async () => { - const result = await executeLogicFunction(fnIds.contributors, { - body: { repos: ['fixture-org/fixture-repo'] }, - }); - expect(['SUCCESS', 'ERROR']).toContain(result.status); - if (result.status === 'SUCCESS') { - const data = result.data as { - totalPages: number; - repos: Array<{ owner: string; repo: string; pages: number }>; - }; - expect(typeof data.totalPages).toBe('number'); - expect(Array.isArray(data.repos)).toBe(true); - expect('orgMembers' in (data as Record)).toBe(false); - } - }); - - it('handle-github-webhook is registered (but rejects unsigned requests gracefully)', async () => { - expect(fnIds.webhook).toBeDefined(); - }); -}); diff --git a/packages/twenty-apps/community/github-connector/src/__tests__/schema.integration-test.ts b/packages/twenty-apps/community/github-connector/src/__tests__/schema.integration-test.ts deleted file mode 100644 index f2246f1ee7..0000000000 --- a/packages/twenty-apps/community/github-connector/src/__tests__/schema.integration-test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/modules/shared/universal-identifiers'; -import { describe, expect, it } from 'vitest'; - -import { findInstalledApp, findObjectByName } from './helpers/metadata'; - -describe('App installation', () => { - it('finds the installed GitHub Connector app', async () => { - const app = await findInstalledApp(APPLICATION_UNIVERSAL_IDENTIFIER); - expect(app).toBeDefined(); - expect(app?.name).toMatch(/github/i); - }); -}); - -describe('Contributor object', () => { - it('exists with the expected GitHub-only fields', async () => { - const obj = await findObjectByName('contributor'); - expect(obj).toBeDefined(); - - const names = obj!.fields.edges.map((e) => e.node.name); - expect(names).toContain('name'); - expect(names).toContain('ghLogin'); - expect(names).toContain('githubId'); - expect(names).toContain('avatarUrl'); - expect(names).toContain('contributions'); - - expect(names).not.toContain('isCoreTeam'); - expect(names).not.toContain('discordId'); - }); -}); - -describe('PullRequest object', () => { - it('exists with the expected fields and relations', async () => { - const obj = await findObjectByName('pullRequest'); - expect(obj).toBeDefined(); - - const names = obj!.fields.edges.map((e) => e.node.name); - expect(names).toContain('name'); - expect(names).toContain('githubNumber'); - expect(names).toContain('uniqueIdentifier'); - expect(names).toContain('url'); - expect(names).toContain('state'); - expect(names).toContain('mergedAt'); - expect(names).toContain('closedAt'); - expect(names).toContain('githubCreatedAt'); - expect(names).toContain('author'); - expect(names).toContain('merger'); - expect(names).toContain('reviews'); - expect(names).toContain('projectItems'); - }); -}); - -describe('PullRequestReviewEvent object', () => { - it('exists with the expected fields and relations', async () => { - const obj = await findObjectByName('pullRequestReviewEvent'); - expect(obj).toBeDefined(); - - const names = obj!.fields.edges.map((e) => e.node.name); - expect(names).toContain('title'); - expect(names).toContain('githubReviewId'); - expect(names).toContain('state'); - expect(names).toContain('submittedAt'); - expect(names).toContain('reviewer'); - expect(names).toContain('pullRequest'); - expect(names).toContain('review'); - }); -}); - -describe('PullRequestReview object', () => { - it('exists with the expected fields and relations', async () => { - const obj = await findObjectByName('pullRequestReview'); - expect(obj).toBeDefined(); - - const names = obj!.fields.edges.map((e) => e.node.name); - expect(names).toContain('title'); - expect(names).toContain('reviewKey'); - expect(names).toContain('state'); - expect(names).toContain('firstSubmittedAt'); - expect(names).toContain('lastSubmittedAt'); - expect(names).toContain('eventCount'); - expect(names).toContain('isSelfReview'); - expect(names).toContain('reviewer'); - expect(names).toContain('pullRequest'); - expect(names).toContain('reviewEvents'); - }); -}); - -describe('Issue object', () => { - it('exists with the expected fields and relations', async () => { - const obj = await findObjectByName('issue'); - expect(obj).toBeDefined(); - - const names = obj!.fields.edges.map((e) => e.node.name); - expect(names).toContain('title'); - expect(names).toContain('githubNumber'); - expect(names).toContain('uniqueIdentifier'); - expect(names).toContain('githubUrl'); - expect(names).toContain('state'); - expect(names).toContain('labels'); - expect(names).toContain('githubCreatedAt'); - expect(names).toContain('closedAt'); - expect(names).toContain('repo'); - expect(names).toContain('author'); - expect(names).toContain('projectItems'); - }); -}); - -describe('ProjectItem object', () => { - it('exists with the expected fields and relations', async () => { - const obj = await findObjectByName('projectItem'); - expect(obj).toBeDefined(); - - const names = obj!.fields.edges.map((e) => e.node.name); - expect(names).toContain('name'); - expect(names).toContain('githubProjectItemId'); - expect(names).toContain('status'); - expect(names).toContain('sprint'); - expect(names).toContain('assignees'); - expect(names).toContain('priority'); - expect(names).toContain('mainAssignee'); - expect(names).toContain('linkedIssue'); - expect(names).toContain('linkedPullRequest'); - expect(names).toContain('githubUrl'); - expect(names).toContain('repo'); - }); -}); diff --git a/packages/twenty-apps/community/github-connector/src/__tests__/webhook-signature.integration-test.ts b/packages/twenty-apps/community/github-connector/src/__tests__/webhook-signature.integration-test.ts deleted file mode 100644 index 714786048b..0000000000 --- a/packages/twenty-apps/community/github-connector/src/__tests__/webhook-signature.integration-test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { createHmac } from 'crypto'; - -import { - getRawBodyForSignature, - verifyGitHubSignature, -} from 'src/modules/github/connector/webhook-signature'; - -const SECRET = 'super-secret-shared-string'; - -function sign(body: string): string { - return `sha256=${createHmac('sha256', SECRET).update(body).digest('hex')}`; -} - -describe('verifyGitHubSignature', () => { - it('accepts a valid signature', () => { - const body = '{"action":"opened","number":42}'; - const result = verifyGitHubSignature({ - rawBody: body, - signatureHeader: sign(body), - secret: SECRET, - }); - expect(result.ok).toBe(true); - }); - - it('rejects a tampered body', () => { - const body = '{"action":"opened","number":42}'; - const signature = sign(body); - const result = verifyGitHubSignature({ - rawBody: body.replace('42', '43'), - signatureHeader: signature, - secret: SECRET, - }); - expect(result.ok).toBe(false); - }); - - it('rejects a wrong secret', () => { - const body = '{"action":"opened","number":42}'; - const result = verifyGitHubSignature({ - rawBody: body, - signatureHeader: sign(body), - secret: 'other-secret', - }); - expect(result.ok).toBe(false); - }); - - it('rejects a missing header', () => { - const result = verifyGitHubSignature({ - rawBody: 'anything', - signatureHeader: undefined, - secret: SECRET, - }); - expect(result).toMatchObject({ - ok: false, - reason: 'missing X-Hub-Signature-256 header', - }); - }); - - it('rejects a header without sha256= prefix', () => { - const result = verifyGitHubSignature({ - rawBody: 'anything', - signatureHeader: 'sha1=deadbeef', - secret: SECRET, - }); - expect(result).toMatchObject({ - ok: false, - reason: 'malformed signature header', - }); - }); - - it('rejects when length differs', () => { - const result = verifyGitHubSignature({ - rawBody: 'anything', - signatureHeader: 'sha256=tooshort', - secret: SECRET, - }); - expect(result).toMatchObject({ - ok: false, - reason: 'signature length mismatch', - }); - }); -}); - -describe('getRawBodyForSignature', () => { - it('prefers event.rawBody when the runtime forwarded it', () => { - const original = '{ "action": "opened", "number": 42 }'; - expect( - getRawBodyForSignature({ - body: { action: 'opened', number: 42 }, - rawBody: original, - }), - ).toBe(original); - }); - - it('falls back to string body when rawBody is not provided', () => { - expect( - getRawBodyForSignature({ body: '{"a":1}', isBase64Encoded: false }), - ).toBe('{"a":1}'); - }); - - it('decodes base64 bodies', () => { - const original = '{"a":1}'; - const b64 = Buffer.from(original, 'utf8').toString('base64'); - expect(getRawBodyForSignature({ body: b64, isBase64Encoded: true })).toBe( - original, - ); - }); - - it('returns null for parsed object bodies (raw bytes lost)', () => { - expect(getRawBodyForSignature({ body: { a: 1 } })).toBeNull(); - }); - - it('returns empty string for null/undefined', () => { - expect(getRawBodyForSignature({ body: null })).toBe(''); - }); -}); - -describe('verifyGitHubSignature with parsed body', () => { - it('rejects with a clear reason when the runtime parsed the JSON', () => { - const result = verifyGitHubSignature({ - rawBody: null, - signatureHeader: 'sha256=deadbeef', - secret: SECRET, - }); - expect(result).toMatchObject({ - ok: false, - reason: - 'raw request body is unavailable (the runtime parsed it as JSON); HMAC cannot be verified', - }); - }); -}); - -describe('end-to-end: server forwards rawBody, signature verifies', () => { - it('verifies a signature when rawBody is present alongside parsed body', () => { - const original = '{ "action": "opened", "number": 42 }'; - const event = { - body: { action: 'opened', number: 42 }, - rawBody: original, - isBase64Encoded: false, - }; - - const rawBody = getRawBodyForSignature(event); - const result = verifyGitHubSignature({ - rawBody, - signatureHeader: sign(original), - secret: SECRET, - }); - expect(result.ok).toBe(true); - }); -}); diff --git a/packages/twenty-apps/community/github-connector/src/application-config.ts b/packages/twenty-apps/community/github-connector/src/application-config.ts deleted file mode 100644 index 673c442388..0000000000 --- a/packages/twenty-apps/community/github-connector/src/application-config.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { defineApplication } from 'twenty-sdk/define'; - -import { - APP_ABOUT_DESCRIPTION, - APP_DESCRIPTION, - APP_DISPLAY_NAME, - APPLICATION_UNIVERSAL_IDENTIFIER, - DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, -} from 'src/modules/shared/universal-identifiers'; - -export default defineApplication({ - universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, - displayName: APP_DISPLAY_NAME, - description: APP_DESCRIPTION, - aboutDescription: APP_ABOUT_DESCRIPTION, - icon: 'IconBrandGithub', - defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, - screenshots: [ - 'public/screenshots/app-listing.png', - 'public/screenshots/github-dashboard.png', - 'public/screenshots/pull-requests-view.png', - 'public/screenshots/contributor-stats.png', - ], - applicationVariables: { - GITHUB_TOKEN: { - universalIdentifier: 'fb1d2e91-3a75-4c89-9d6b-1e2f7a4c5d8e', - description: - 'Fine-grained Personal Access Token (github_pat_…) from https://github.com/settings/personal-access-tokens — needs Read-only access to Contents, Issues, Pull requests and Metadata (and Organization → Projects for Projects v2). Classic PATs are not supported. When set, takes precedence over the GitHub App credentials below.', - isSecret: true, - }, - GITHUB_APP_ID: { - universalIdentifier: '2a196d91-4c1b-4f8d-bc34-6693fcdaa771', - description: - 'GitHub App ID. Used together with GITHUB_APP_PRIVATE_KEY and GITHUB_APP_INSTALLATION_ID when GITHUB_TOKEN is unset.', - isSecret: false, - }, - GITHUB_APP_PRIVATE_KEY: { - universalIdentifier: 'c5591dd4-f653-4b92-bec9-970ddc8e10cc', - description: 'GitHub App PEM private key (BEGIN/END PRIVATE KEY block).', - isSecret: true, - }, - GITHUB_APP_INSTALLATION_ID: { - universalIdentifier: '0c39a59a-ee2e-49a9-88c7-3fbe6cb04ad0', - description: 'GitHub App installation ID for your org.', - isSecret: false, - }, - GITHUB_WEBHOOK_SECRET: { - universalIdentifier: 'b9f3c2d8-1e7a-4d56-9c8b-3a2f1e5d7c9a', - description: - 'Shared secret used to verify the X-Hub-Signature-256 HMAC on incoming GitHub webhooks. When unset, signature verification is skipped (use only in dev/test).', - isSecret: true, - }, - GITHUB_REPOS: { - universalIdentifier: '7d1e9c84-2f63-4a58-9b0d-5e8a3c1f7b29', - description: - 'Comma-separated list of `owner/repo` to sync (e.g. `twentyhq/twenty,octo/hello`). Used by the manual fetch routes.', - isSecret: false, - }, - GITHUB_PROJECTS: { - universalIdentifier: 'e3a8c7d2-4b95-4e1f-8a6c-9d2b5f7e1c84', - description: - 'Comma-separated list of GitHub Projects (v2) to sync. Each entry is `owner/number` (e.g. `twentyhq/24,octo/3`). Owner can be an organization or a user. Full URLs like `https://github.com/orgs/twentyhq/projects/24` or `https://github.com/users/octo/projects/3` are also accepted.', - isSecret: false, - }, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/default-role.ts b/packages/twenty-apps/community/github-connector/src/default-role.ts deleted file mode 100644 index 8d3e930cc1..0000000000 --- a/packages/twenty-apps/community/github-connector/src/default-role.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defineRole } from 'twenty-sdk/define'; - -import { - APP_DISPLAY_NAME, - DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, -} from 'src/modules/shared/universal-identifiers'; - -export default defineRole({ - universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, - label: `${APP_DISPLAY_NAME} default function role`, - description: `${APP_DISPLAY_NAME} default function role`, - canReadAllObjectRecords: true, - canUpdateAllObjectRecords: true, - canSoftDeleteAllObjectRecords: true, - canDestroyAllObjectRecords: false, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/connector/auth.ts b/packages/twenty-apps/community/github-connector/src/modules/github/connector/auth.ts deleted file mode 100644 index 5cb01a98ba..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/connector/auth.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { createSign } from 'crypto'; - -export function normalizePem(raw: string): string { - let pem = raw.replace(/\\n/g, '\n').trim(); - - if (!pem.includes('\n')) { - const beginMatch = pem.match(/^(-----BEGIN [A-Z ]+-----)/); - const endMatch = pem.match(/(-----END [A-Z ]+-----)$/); - - if (beginMatch && endMatch) { - const header = beginMatch[1]; - const footer = endMatch[1]; - const body = pem.slice(header.length, pem.length - footer.length); - const lines = body.match(/.{1,64}/g) ?? []; - pem = [header, ...lines, footer].join('\n'); - } - } - - return pem; -} - -function base64url(input: Buffer | string): string { - const buf = typeof input === 'string' ? Buffer.from(input) : input; - return buf.toString('base64url'); -} - -function signJwt(appId: string, privateKey: string): string { - const now = Math.floor(Date.now() / 1000); - const header = base64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' })); - const payload = base64url( - JSON.stringify({ iat: now - 60, exp: now + 600, iss: appId }), - ); - - const signature = createSign('RSA-SHA256') - .update(`${header}.${payload}`) - .sign(privateKey, 'base64url'); - - return `${header}.${payload}.${signature}`; -} - -type CachedToken = { token: string; expiresAt: number }; -let cached: CachedToken | null = null; - -const TOKEN_MARGIN_MS = 5 * 60 * 1000; - -async function requestInstallationToken( - appId: string, - privateKey: string, - installationId: string, -): Promise { - const jwt = signJwt(appId, privateKey); - - const res = await fetch( - `https://api.github.com/app/installations/${installationId}/access_tokens`, - { - method: 'POST', - headers: { - Authorization: `Bearer ${jwt}`, - Accept: 'application/vnd.github+json', - 'X-GitHub-Api-Version': '2022-11-28', - }, - }, - ); - - if (!res.ok) { - const body = await res.text(); - throw new Error( - `GitHub App token exchange failed (${res.status}): ${body}`, - ); - } - - const data = (await res.json()) as { - token: string; - expires_at: string; - }; - - return { - token: data.token, - expiresAt: new Date(data.expires_at).getTime(), - }; -} - -function assertFineGrainedPat(token: string): void { - if (token.startsWith('github_pat_')) return; - throw new Error( - 'GITHUB_TOKEN must be a fine-grained Personal Access Token (starts with `github_pat_`). Classic PATs are not supported — create one at https://github.com/settings/personal-access-tokens.', - ); -} - -export async function getGitHubToken(): Promise { - const pat = process.env.GITHUB_TOKEN?.trim(); - if (pat) { - assertFineGrainedPat(pat); - return pat; - } - - if (cached && Date.now() < cached.expiresAt - TOKEN_MARGIN_MS) { - return cached.token; - } - - const appId = process.env.GITHUB_APP_ID; - const rawKey = process.env.GITHUB_APP_PRIVATE_KEY; - const installationId = process.env.GITHUB_APP_INSTALLATION_ID; - - if (!appId || !rawKey || !installationId) { - const missing = [ - !appId && 'GITHUB_APP_ID', - !rawKey && 'GITHUB_APP_PRIVATE_KEY', - !installationId && 'GITHUB_APP_INSTALLATION_ID', - ] - .filter(Boolean) - .join(', '); - throw new Error( - `Missing GitHub credentials. Set GITHUB_TOKEN (fine-grained PAT) or all of: ${missing}.`, - ); - } - - const privateKey = normalizePem(rawKey); - cached = await requestInstallationToken(appId, privateKey, installationId); - - return cached.token; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/connector/config.ts b/packages/twenty-apps/community/github-connector/src/modules/github/connector/config.ts deleted file mode 100644 index c74eb46d31..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/connector/config.ts +++ /dev/null @@ -1,55 +0,0 @@ -export type GithubRepo = { owner: string; repo: string }; - -export function parseGithubRepo(entry: string): GithubRepo | null { - const trimmed = entry.trim(); - if (!trimmed) return null; - const match = trimmed.match(/^([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+)$/); - if (!match) return null; - return { owner: match[1], repo: match[2] }; -} - -export function getGithubRepos(): string[] { - const raw = process.env.GITHUB_REPOS ?? ''; - return raw - .split(',') - .map(parseGithubRepo) - .filter((r): r is GithubRepo => r !== null) - .map((r) => `${r.owner}/${r.repo}`); -} - -export type GithubProject = { owner: string; number: number }; - -export function parseGithubProject(entry: string): GithubProject | null { - const trimmed = entry.trim(); - if (!trimmed) return null; - - const urlMatch = trimmed.match( - /github\.com\/(?:orgs|users)\/([^/]+)\/projects\/(\d+)/i, - ); - if (urlMatch) { - const owner = urlMatch[1]; - const number = Number.parseInt(urlMatch[2], 10); - if (owner && Number.isFinite(number) && number > 0) { - return { owner, number }; - } - } - - const shortMatch = trimmed.match(/^([^/\s]+)\/(\d+)$/); - if (shortMatch) { - const owner = shortMatch[1]; - const number = Number.parseInt(shortMatch[2], 10); - if (owner && Number.isFinite(number) && number > 0) { - return { owner, number }; - } - } - - return null; -} - -export function getGithubProjects(): GithubProject[] { - const raw = process.env.GITHUB_PROJECTS ?? ''; - return raw - .split(',') - .map(parseGithubProject) - .filter((p): p is GithubProject => p !== null); -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/connector/count-across-repos.ts b/packages/twenty-apps/community/github-connector/src/modules/github/connector/count-across-repos.ts deleted file mode 100644 index 414218a25e..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/connector/count-across-repos.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - getGithubRepos, - parseGithubRepo, -} from 'src/modules/github/connector/config'; - -export type RepoCount = { - owner: string; - repo: string; - totalCount: number; - pages: number; -}; - -export type CountAcrossReposResult = { - totalPages: number; - repos: RepoCount[]; -}; - -const PAGE_SIZE = 100; - -export async function countAcrossRepos( - bodyRepos: string[] | undefined, - count: (owner: string, repo: string) => Promise, - logTag: string, -): Promise { - const repos = - bodyRepos && bodyRepos.length > 0 ? bodyRepos : getGithubRepos(); - - const results: RepoCount[] = []; - let totalPages = 0; - - for (const fullRepo of repos) { - const parsed = parseGithubRepo(fullRepo); - if (!parsed) { - console.warn(`[${logTag}] Skipping malformed repo entry: ${fullRepo}`); - continue; - } - const { owner, repo } = parsed; - const totalCount = await count(owner, repo); - const pages = Math.max(Math.ceil(totalCount / PAGE_SIZE), 1); - results.push({ owner, repo, totalCount, pages }); - totalPages += pages; - } - - return { totalPages, repos: results }; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/connector/github-client.ts b/packages/twenty-apps/community/github-connector/src/modules/github/connector/github-client.ts deleted file mode 100644 index 46051e6d71..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/connector/github-client.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { getGitHubToken } from 'src/modules/github/connector/auth'; - -const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql'; -const LOW_REMAINING_THRESHOLD = 200; - -export class GitHubRateLimitError extends Error { - constructor( - message: string, - public readonly kind: 'primary' | 'secondary', - public readonly remaining: number | null, - public readonly limit: number | null, - public readonly resetAt: Date | null, - public readonly retryAfterSeconds: number | null, - ) { - super(message); - this.name = 'GitHubRateLimitError'; - } -} - -function readRateLimitHeaders(res: Response) { - const num = (h: string) => { - const v = res.headers.get(h); - return v === null ? null : Number(v); - }; - const reset = num('x-ratelimit-reset'); - return { - limit: num('x-ratelimit-limit'), - remaining: num('x-ratelimit-remaining'), - used: num('x-ratelimit-used'), - resource: res.headers.get('x-ratelimit-resource'), - resetAt: - reset === null || Number.isNaN(reset) ? null : new Date(reset * 1000), - retryAfter: num('retry-after'), - }; -} - -export async function githubGraphql( - query: string, - variables: Record, -): Promise { - const queryName = query.match(/query\s*\(/) - ? query.slice(0, 60).replace(/\s+/g, ' ').trim() - : 'mutation'; - console.log( - `[github-gql] Executing: ${queryName} vars=${JSON.stringify(Object.keys(variables))}`, - ); - const token = await getGitHubToken(); - const res = await fetch(GITHUB_GRAPHQL_URL, { - method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ query, variables }), - }); - - const rl = readRateLimitHeaders(res); - if (rl.remaining !== null && rl.limit !== null) { - const resetIn = rl.resetAt - ? Math.max(0, Math.round((rl.resetAt.getTime() - Date.now()) / 1000)) - : null; - const isLow = - rl.remaining <= LOW_REMAINING_THRESHOLD || - rl.remaining / rl.limit < 0.1; - const tag = isLow - ? '[github-gql][rate-limit-low]' - : '[github-gql][rate-limit]'; - console.log( - `${tag} resource=${rl.resource ?? 'graphql'} remaining=${rl.remaining}/${rl.limit} used=${rl.used ?? '?'} resetIn=${resetIn ?? '?'}s`, - ); - } - - if (!res.ok) { - const body = await res.text(); - const lower = body.toLowerCase(); - const isSecondary = - res.status === 403 && - (lower.includes('secondary rate limit') || - lower.includes('abuse detection')); - const isPrimary = - (res.status === 403 || res.status === 429) && - lower.includes('rate limit') && - !isSecondary; - - if (isPrimary || isSecondary) { - throw new GitHubRateLimitError( - `GitHub ${isSecondary ? 'secondary' : 'primary'} rate limit hit (${res.status}): ${body.slice(0, 200)}`, - isSecondary ? 'secondary' : 'primary', - rl.remaining, - rl.limit, - rl.resetAt, - rl.retryAfter, - ); - } - - throw new Error( - `GitHub GraphQL ${res.status} ${res.statusText}: ${body.slice(0, 500)}`, - ); - } - - const json = (await res.json()) as { - data: T; - errors?: Array<{ type?: string; message: string }>; - }; - - if (json.errors?.length) { - const isRateLimited = json.errors.some( - (e) => e.type === 'RATE_LIMITED' || /rate limit/i.test(e.message), - ); - if (isRateLimited) { - throw new GitHubRateLimitError( - `GitHub GraphQL RATE_LIMITED: ${json.errors.map((e) => e.message).join(', ')}`, - 'primary', - rl.remaining, - rl.limit, - rl.resetAt, - rl.retryAfter, - ); - } - throw new Error( - `GraphQL errors: ${json.errors.map((e) => e.message).join(', ')}`, - ); - } - - return json.data; -} - -export async function githubGraphqlOptional( - query: string, - variables: Record, -): Promise { - try { - return await githubGraphql(query, variables); - } catch (err) { - const msg = err instanceof Error ? err.message : ''; - if (/Could not resolve to a (User|Organization)/i.test(msg)) return null; - throw err; - } -} - -export type GithubPage = { - totalCount: number; - pageInfo: { hasNextPage: boolean; endCursor: string | null }; - nodes: T[]; -}; - -export type Paged> = TItems & { - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}; - -export const EMPTY_PAGE = { - totalCount: 0, - hasMore: false, - endCursor: null, -} as const; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/connector/github-user.ts b/packages/twenty-apps/community/github-connector/src/modules/github/connector/github-user.ts deleted file mode 100644 index 297596ed90..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/connector/github-user.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type GitHubUser = { - login: string; - id: number; - avatar_url?: string | null; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/connector/webhook-payload.ts b/packages/twenty-apps/community/github-connector/src/modules/github/connector/webhook-payload.ts deleted file mode 100644 index c71c26c5b9..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/connector/webhook-payload.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { GitHubUser } from 'src/modules/github/connector/github-user'; -import type { GitHubPullRequest } from 'src/modules/github/pull-request/types/github-pull-request'; -import type { GitHubReview } from 'src/modules/github/pull-request-review-event/types/github-review'; -import type { GitHubIssue } from 'src/modules/github/issue/types/github-issue'; -import type { GitHubProjectV2Item } from 'src/modules/github/project-item/types/github-project-v2-item'; - -export type GitHubWebhookPayload = { - action: string; - pull_request?: GitHubPullRequest; - review?: GitHubReview; - issue?: GitHubIssue; - projects_v2_item?: GitHubProjectV2Item; - sender: GitHubUser; - repository?: { full_name: string }; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/connector/webhook-signature.ts b/packages/twenty-apps/community/github-connector/src/modules/github/connector/webhook-signature.ts deleted file mode 100644 index 51b4fd5241..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/connector/webhook-signature.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { createHmac, timingSafeEqual } from 'crypto'; - -export type SignatureVerificationResult = - | { ok: true } - | { ok: false; reason: string }; - -export function getRawBodyForSignature(event: { - body: unknown; - isBase64Encoded?: boolean; - rawBody?: string; -}): string | null { - if (typeof event.rawBody === 'string') { - return event.rawBody; - } - const raw = event.body; - if (raw == null) return ''; - if (typeof raw === 'string') { - return event.isBase64Encoded - ? Buffer.from(raw, 'base64').toString('utf8') - : raw; - } - return null; -} - -export function verifyGitHubSignature({ - rawBody, - signatureHeader, - secret, -}: { - rawBody: string | null; - signatureHeader: string | undefined; - secret: string; -}): SignatureVerificationResult { - if (rawBody === null) { - return { - ok: false, - reason: - 'raw request body is unavailable (the runtime parsed it as JSON); HMAC cannot be verified', - }; - } - if (!signatureHeader) { - return { ok: false, reason: 'missing X-Hub-Signature-256 header' }; - } - const prefix = 'sha256='; - if (!signatureHeader.startsWith(prefix)) { - return { ok: false, reason: 'malformed signature header' }; - } - const provided = signatureHeader.slice(prefix.length); - const expected = createHmac('sha256', secret).update(rawBody).digest('hex'); - if (provided.length !== expected.length) { - return { ok: false, reason: 'signature length mismatch' }; - } - const ok = timingSafeEqual( - Buffer.from(provided, 'utf8'), - Buffer.from(expected, 'utf8'), - ); - return ok ? { ok: true } : { ok: false, reason: 'signature mismatch' }; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/command-menu-items/contributor-stats.command-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/command-menu-items/contributor-stats.command-menu-item.ts deleted file mode 100644 index ebc49ffdc2..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/command-menu-items/contributor-stats.command-menu-item.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define'; - -export default defineCommandMenuItem({ - universalIdentifier: '7d3f2b9e-5c0a-4e8b-ad6e-2f9c3b4d5e6a', - frontComponentUniversalIdentifier: '6c2f1a8d-4b9e-4f7a-9c5d-1e8b2a3d4c5f', - label: 'Contributor Stats', - icon: 'IconChartBar', - isPinned: false, - conditionalAvailabilityExpression: - objectMetadataItem.nameSingular === 'contributor', -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/command-menu-items/fetch-contributors.command-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/command-menu-items/fetch-contributors.command-menu-item.ts deleted file mode 100644 index 72d5a7a30a..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/command-menu-items/fetch-contributors.command-menu-item.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define'; - -export default defineCommandMenuItem({ - universalIdentifier: '4640992f-c2c9-4bba-b5df-9c8f05dc9e80', - frontComponentUniversalIdentifier: '08f40f82-24ed-4f3e-8c99-695151e90e38', - label: 'Fetch Contributors', - icon: 'IconUsers', - isPinned: false, - conditionalAvailabilityExpression: - objectMetadataItem.nameSingular === 'contributor', -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/components/leaderboard.tsx b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/components/leaderboard.tsx deleted file mode 100644 index 688f7981e9..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/components/leaderboard.tsx +++ /dev/null @@ -1,282 +0,0 @@ -import { type ReactNode } from 'react'; - -import { THEME } from 'src/modules/github/contributor/components/theme'; - -export type LeaderboardEntry = { - id: string; - name: string | null; - ghLogin: string | null; - avatarUrl: string | null; - count: number; -}; - -const ROW_HEIGHT = 32; -const CELL_PADDING_X = 8; -const RANK_COL_WIDTH = 36; -const COUNT_COL_WIDTH = 72; - -const styles = { - root: { - display: 'flex', - flexDirection: 'column', - width: '100%', - height: '100%', - minHeight: 0, - fontFamily: THEME.fontFamily, - color: THEME.fontPrimary, - background: THEME.bgPrimary, - boxSizing: 'border-box', - overflow: 'hidden', - } as const, - table: { - display: 'flex', - flexDirection: 'column', - flex: 1, - minHeight: 0, - overflowY: 'auto', - overflowX: 'hidden', - width: '100%', - } as const, - headerRow: { - display: 'flex', - alignItems: 'stretch', - height: ROW_HEIGHT, - flexShrink: 0, - borderBottom: `1px solid ${THEME.borderLight}`, - background: THEME.bgPrimary, - } as const, - headerCell: { - display: 'flex', - alignItems: 'center', - height: '100%', - padding: `0 ${CELL_PADDING_X}px`, - borderRight: `1px solid ${THEME.borderLight}`, - color: THEME.fontTertiary, - fontSize: 12, - fontWeight: 500, - boxSizing: 'border-box', - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - userSelect: 'none', - } as const, - headerCellLast: { - borderRight: 'none', - } as const, - row: { - display: 'flex', - alignItems: 'stretch', - height: ROW_HEIGHT, - flexShrink: 0, - width: '100%', - background: THEME.bgPrimary, - color: THEME.fontPrimary, - textDecoration: 'none', - boxSizing: 'border-box', - } as const, - cell: { - display: 'flex', - alignItems: 'center', - height: '100%', - padding: `0 ${CELL_PADDING_X}px`, - borderBottom: `1px solid ${THEME.borderLight}`, - borderRight: `1px solid ${THEME.borderLight}`, - fontSize: 13, - boxSizing: 'border-box', - minWidth: 0, - overflow: 'hidden', - transition: 'background-color 80ms ease', - } as const, - cellLast: { - borderRight: 'none', - } as const, - rankCell: { - width: RANK_COL_WIDTH, - flexShrink: 0, - justifyContent: 'flex-end', - color: THEME.fontTertiary, - fontSize: 12, - fontVariantNumeric: 'tabular-nums', - } as const, - contributorCell: { - flex: 1, - minWidth: 0, - gap: 6, - } as const, - countCell: { - width: COUNT_COL_WIDTH, - flexShrink: 0, - justifyContent: 'flex-end', - color: THEME.fontSecondary, - fontVariantNumeric: 'tabular-nums', - fontWeight: 500, - } as const, - avatar: { - width: 16, - height: 16, - borderRadius: '50%', - objectFit: 'cover', - background: THEME.bgTertiary, - flexShrink: 0, - } as const, - nameWrapper: { - display: 'flex', - alignItems: 'baseline', - gap: 6, - minWidth: 0, - flex: 1, - lineHeight: 1.2, - } as const, - name: { - fontSize: 13, - fontWeight: 500, - color: THEME.fontPrimary, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - } as const, - login: { - fontSize: 12, - fontWeight: 400, - color: THEME.fontTertiary, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - } as const, - empty: { - flex: 1, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - color: THEME.fontTertiary, - fontSize: 13, - padding: 16, - textAlign: 'center', - } as const, -}; - -type LeaderboardProps = { - contributorColumnLabel: string; - countColumnLabel: string; - entries: LeaderboardEntry[]; - loading: boolean; - error: string | null; - emptyMessage?: string; -}; - -const renderState = (content: ReactNode) => ( -

{content}
-); - -const Row = ({ - rank, - entry, - countLabel, -}: { - rank: number; - entry: LeaderboardEntry; - countLabel: string; -}) => { - const label = entry.name ?? entry.ghLogin ?? 'Unknown'; - const profileHref = entry.ghLogin - ? `https://github.com/${entry.ghLogin}` - : undefined; - - const onEnter = (e: React.MouseEvent) => { - e.currentTarget.style.backgroundColor = THEME.bgSecondary; - }; - const onLeave = (e: React.MouseEvent) => { - e.currentTarget.style.backgroundColor = THEME.bgPrimary; - }; - - const cells = ( - <> -
{rank}
-
- {entry.avatarUrl ? ( - - ) : ( -
- )} -
- {label} - {entry.ghLogin && entry.ghLogin !== label && ( - @{entry.ghLogin} - )} -
-
-
- {entry.count} -
- - ); - - if (profileHref) { - return ( -
- {cells} - - ); - } - - return ( -
- {cells} -
- ); -}; - -export const Leaderboard = ({ - contributorColumnLabel, - countColumnLabel, - entries, - loading, - error, - emptyMessage = 'No activity in the last 90 days', -}: LeaderboardProps) => ( -
-
-
#
-
- {contributorColumnLabel} -
-
- {countColumnLabel} -
-
- {error ? ( - renderState(error) - ) : loading && entries.length === 0 ? ( - renderState('Loading...') - ) : entries.length === 0 ? ( - renderState(emptyMessage) - ) : ( -
- {entries.map((entry, i) => ( - - ))} -
- )} -
-); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/components/theme.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/components/theme.ts deleted file mode 100644 index d55b5f74f4..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/components/theme.ts +++ /dev/null @@ -1,23 +0,0 @@ -export const THEME = { - fontFamily: - 'var(--t-font-family, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif)', - borderRadius: 'var(--t-border-radius-sm, 4px)', - - fontPrimary: 'var(--t-font-color-primary)', - fontSecondary: 'var(--t-font-color-secondary)', - fontTertiary: 'var(--t-font-color-tertiary)', - fontLight: 'var(--t-font-color-light)', - - borderLight: 'var(--t-border-color-light)', - borderMedium: 'var(--t-border-color-medium)', - - bgPrimary: 'var(--t-background-primary)', - bgSecondary: 'var(--t-background-secondary)', - bgTertiary: 'var(--t-background-tertiary)', - bgTransparentLight: 'var(--t-background-transparent-light)', - bgTransparentLighter: 'var(--t-background-transparent-lighter)', - - chartAuthored: 'var(--t-color-purple)', - chartMerged: 'var(--t-color-green)', - chartReviewed: 'var(--t-color-blue)', -} as const; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/assigned-project-items-on-contributor.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/assigned-project-items-on-contributor.field.ts deleted file mode 100644 index 0199c89905..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/assigned-project-items-on-contributor.field.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { PROJECT_ITEM_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/objects/project-item.object'; -import { MAIN_ASSIGNEE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/fields/main-assignee-on-project-item.field'; - -export const ASSIGNED_PROJECT_ITEMS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER = - 'b35aed82-2c01-4f37-b3f9-cf67dc269d2c'; - -export default defineField({ - universalIdentifier: - ASSIGNED_PROJECT_ITEMS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'assignedProjectItems', - label: 'Assigned Items', - icon: 'IconLayoutKanban', - relationTargetObjectMetadataUniversalIdentifier: - PROJECT_ITEM_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - MAIN_ASSIGNEE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/authored-issues-on-contributor.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/authored-issues-on-contributor.field.ts deleted file mode 100644 index 5d6b2c1375..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/authored-issues-on-contributor.field.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { ISSUE_UNIVERSAL_IDENTIFIER } from 'src/modules/github/issue/objects/issue.object'; -import { ISSUE_AUTHOR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/issue/fields/author-on-issue.field'; - -export const AUTHORED_ISSUES_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER = - '31f185c0-d2a2-47e2-9132-8cbac05f174a'; - -export default defineField({ - universalIdentifier: AUTHORED_ISSUES_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'authoredIssues', - label: 'Authored Issues', - icon: 'IconBug', - relationTargetObjectMetadataUniversalIdentifier: ISSUE_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - ISSUE_AUTHOR_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/authored-prs-on-contributor.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/authored-prs-on-contributor.field.ts deleted file mode 100644 index 4f72b653b9..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/authored-prs-on-contributor.field.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { PULL_REQUEST_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/objects/pull-request.object'; -import { AUTHOR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/fields/author-on-pull-request.field'; - -export const AUTHORED_PRS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER = - 'c3d4e5f6-2b3c-4d4e-9f6a-7b8c9d0e1f2a'; - -export default defineField({ - universalIdentifier: AUTHORED_PRS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'authoredPrs', - label: 'Authored PRs', - icon: 'IconGitPullRequest', - relationTargetObjectMetadataUniversalIdentifier: - PULL_REQUEST_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - AUTHOR_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/merged-pull-requests-on-contributor.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/merged-pull-requests-on-contributor.field.ts deleted file mode 100644 index 9dde443d05..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/merged-pull-requests-on-contributor.field.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { PULL_REQUEST_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/objects/pull-request.object'; -import { MERGER_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/fields/merger-on-pull-request.field'; - -export const MERGED_PULL_REQUESTS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER = - '02ec0165-fb5a-46f9-b7c7-a0fd605befa2'; - -export default defineField({ - universalIdentifier: - MERGED_PULL_REQUESTS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'mergedPullRequests', - label: 'Merged Pull Requests', - icon: 'IconGitMerge', - relationTargetObjectMetadataUniversalIdentifier: - PULL_REQUEST_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - MERGER_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/review-events-on-contributor.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/review-events-on-contributor.field.ts deleted file mode 100644 index 2081d4d0d2..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/review-events-on-contributor.field.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/objects/pull-request-review-event.object'; -import { REVIEWER_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/fields/reviewer-on-review-event.field'; - -export const REVIEW_EVENTS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER = - 'e4f5a6b7-8c9d-4e0f-a1b2-c3d4e5f6a7b8'; - -export default defineField({ - universalIdentifier: REVIEW_EVENTS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'reviewEvents', - label: 'Review Events', - icon: 'IconEye', - relationTargetObjectMetadataUniversalIdentifier: - PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - REVIEWER_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/reviews-on-contributor.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/reviews-on-contributor.field.ts deleted file mode 100644 index 3d99635a41..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/fields/reviews-on-contributor.field.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/objects/pull-request-review.object'; -import { REVIEWER_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/fields/reviewer-on-review.field'; - -export const REVIEWS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER = - '564e7f97-74dc-4e9b-a4bc-ed4215ab9ac7'; - -export default defineField({ - universalIdentifier: REVIEWS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'reviews', - label: 'Reviews', - icon: 'IconEye', - relationTargetObjectMetadataUniversalIdentifier: - PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - REVIEWER_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/contributor-stats.front-component.tsx b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/contributor-stats.front-component.tsx deleted file mode 100644 index e2aee6d49d..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/contributor-stats.front-component.tsx +++ /dev/null @@ -1,681 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { defineFrontComponent } from 'twenty-sdk/define'; -import { enqueueSnackbar, useRecordId } from 'twenty-sdk/front-component'; - -import { THEME } from 'src/modules/github/contributor/components/theme'; -import { callAppRoute } from 'src/modules/shared/call-app-route'; - -type Period = 'week' | 'month' | '3months' | 'year'; - -type Bucket = { - key: string; - label: string; - start: string; - end: string; - prAuthored: number; - prMerged: number; - prReviewed: number; -}; - -type ContributorInfo = { - id: string; - name: string | null; - ghLogin: string | null; - avatarUrl: string | null; -}; - -type StatsResponse = { - contributor: ContributorInfo; - period: Period; - granularity: 'day' | 'week' | 'month'; - buckets: Bucket[]; - totals: { prAuthored: number; prMerged: number; prReviewed: number }; - truncated: { - prAuthored: boolean; - prMerged: boolean; - prReviewed: boolean; - }; - error?: string; -}; - -type SearchResponse = { - contributors: ContributorInfo[]; -}; - -const readSerializedValue = ( - e: React.SyntheticEvent, -): string | undefined => { - const obj = e as { detail?: { value?: string }; value?: string }; - if (typeof obj.detail?.value === 'string') return obj.detail.value; - if (typeof obj.value === 'string') return obj.value; - return undefined; -}; - -const onValueChange = - (fn: (value: string) => void) => - (e: React.SyntheticEvent) => { - const v = readSerializedValue(e); - if (typeof v === 'string') fn(v); - }; - -const PERIOD_OPTIONS: Array<{ value: Period; label: string }> = [ - { value: 'week', label: 'Last week' }, - { value: 'month', label: 'Last month' }, - { value: '3months', label: 'Last 3 months' }, - { value: 'year', label: 'Last year' }, -]; - -const WIDGET_HEADER_HEIGHT = 24; - -const styles = { - root: { - display: 'flex', - flexDirection: 'column', - width: '100%', - height: '100%', - minHeight: 0, - boxSizing: 'border-box', - padding: 12, - gap: 12, - fontFamily: THEME.fontFamily, - color: THEME.fontPrimary, - background: THEME.bgSecondary, - overflow: 'hidden', - } as const, - header: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - gap: 8, - flexWrap: 'wrap', - padding: '8px 12px', - background: THEME.bgPrimary, - border: `1px solid ${THEME.borderLight}`, - borderRadius: THEME.borderRadius, - boxSizing: 'border-box', - flexShrink: 0, - } as const, - contributor: { - display: 'flex', - alignItems: 'center', - gap: 8, - minWidth: 0, - } as const, - avatar: { - width: 24, - height: 24, - borderRadius: '50%', - objectFit: 'cover', - background: THEME.bgTertiary, - flexShrink: 0, - } as const, - smallAvatar: { - width: 16, - height: 16, - borderRadius: '50%', - objectFit: 'cover', - background: THEME.bgTertiary, - flexShrink: 0, - } as const, - nameCol: { - display: 'flex', - alignItems: 'baseline', - minWidth: 0, - gap: 6, - lineHeight: 1.2, - } as const, - name: { - fontSize: 14, - fontWeight: 500, - color: THEME.fontPrimary, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - } as const, - login: { - fontSize: 12, - fontWeight: 400, - color: THEME.fontTertiary, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - } as const, - controls: { - display: 'flex', - alignItems: 'center', - gap: 6, - flexWrap: 'wrap', - } as const, - select: { - fontFamily: THEME.fontFamily, - fontSize: 13, - height: 28, - lineHeight: '26px', - padding: '0 24px 0 10px', - border: `1px solid ${THEME.borderMedium}`, - borderRadius: THEME.borderRadius, - background: `${THEME.bgPrimary} url("data:image/svg+xml;utf8,") no-repeat right 8px center`, - color: THEME.fontPrimary, - cursor: 'pointer', - outline: 'none', - appearance: 'none', - WebkitAppearance: 'none', - MozAppearance: 'none', - } as const, - buttonNeutral: { - fontFamily: THEME.fontFamily, - fontSize: 13, - fontWeight: 500, - height: 28, - lineHeight: '26px', - padding: '0 10px', - border: `1px solid ${THEME.borderMedium}`, - borderRadius: THEME.borderRadius, - background: THEME.bgPrimary, - color: THEME.fontPrimary, - cursor: 'pointer', - outline: 'none', - } as const, - body: { - display: 'flex', - flexDirection: 'column', - flex: 1, - minHeight: 0, - gap: 12, - } as const, - chartCard: { - display: 'flex', - flexDirection: 'column', - flex: 1, - minHeight: 0, - border: `1px solid ${THEME.borderLight}`, - borderRadius: THEME.borderRadius, - padding: 8, - background: THEME.bgPrimary, - boxSizing: 'border-box', - } as const, - chartHeader: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - height: WIDGET_HEADER_HEIGHT, - padding: '0 4px', - flexShrink: 0, - } as const, - chartTitle: { - fontSize: 13, - fontWeight: 500, - color: THEME.fontPrimary, - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - } as const, - chartTotal: { - fontSize: 13, - fontWeight: 500, - color: THEME.fontTertiary, - fontVariantNumeric: 'tabular-nums', - flexShrink: 0, - } as const, - chartContainer: { - flex: '1 1 0%', - minHeight: 90, - width: '100%', - position: 'relative', - marginTop: 4, - } as const, - centered: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - flex: 1, - color: THEME.fontTertiary, - fontSize: 13, - textAlign: 'center', - padding: 16, - } as const, - searchWrapper: { - display: 'flex', - flexDirection: 'column', - gap: 8, - flex: 1, - minHeight: 0, - } as const, - searchInput: { - fontFamily: THEME.fontFamily, - fontSize: 13, - height: 28, - padding: '0 10px', - border: `1px solid ${THEME.borderMedium}`, - borderRadius: THEME.borderRadius, - outline: 'none', - background: THEME.bgPrimary, - color: THEME.fontPrimary, - } as const, - searchList: { - display: 'flex', - flexDirection: 'column', - flex: 1, - minHeight: 0, - overflowY: 'auto', - border: `1px solid ${THEME.borderLight}`, - borderRadius: THEME.borderRadius, - background: THEME.bgPrimary, - } as const, - searchItem: { - display: 'flex', - alignItems: 'center', - gap: 8, - padding: '6px 10px', - cursor: 'pointer', - borderBottom: `1px solid ${THEME.borderLight}`, - fontSize: 13, - color: THEME.fontPrimary, - } as const, - truncatedHint: { - fontSize: 11, - color: THEME.fontTertiary, - marginTop: 4, - paddingLeft: 4, - } as const, -}; - -type BarChartProps = { - buckets: Bucket[]; - valueKey: 'prAuthored' | 'prMerged' | 'prReviewed'; - color: string; -}; - -const CHART_PADDING = { top: 6, right: 4, bottom: 16, left: 22 }; - -const BarChart = ({ buckets, valueKey, color }: BarChartProps) => { - const yMax = useMemo(() => { - const m = buckets.reduce((acc, b) => Math.max(acc, b[valueKey]), 0); - if (m === 0) return 1; - return m <= 5 ? m : Math.ceil(m / 5) * 5; - }, [buckets, valueKey]); - - const yTicks = useMemo(() => [0, Math.round(yMax / 2), yMax], [yMax]); - - const n = buckets.length; - const labelEvery = (() => { - if (n <= 6) return 1; - if (n <= 14) return 2; - return Math.ceil(n / 6); - })(); - - return ( -
-
- {yTicks.map((t) => { - const bottomPct = (t / yMax) * 100; - return ( -
- {t} -
- ); - })} -
- -
- {yTicks.map((t, i) => { - const bottomPct = (t / yMax) * 100; - return ( -
- ); - })} - -
- {buckets.map((b, i) => { - const v = b[valueKey]; - const heightPct = (v / yMax) * 100; - const showLabel = i % labelEvery === 0 || i === n - 1; - return ( -
-
0 ? 2 : 0, - background: color, - borderRadius: '2px 2px 0 0', - }} - /> - {showLabel && ( -
- {b.label} -
- )} -
- ); - })} -
-
-
- ); -}; - -const ContributorStats = () => { - const recordId = useRecordId(); - const [period, setPeriod] = useState('month'); - const [selectedId, setSelectedId] = useState(recordId); - const [stats, setStats] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const [searchQuery, setSearchQuery] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [searchLoading, setSearchLoading] = useState(false); - - useEffect(() => { - setSelectedId(recordId); - }, [recordId]); - - useEffect(() => { - if (selectedId) return; - let cancelled = false; - setSearchLoading(true); - const handle = setTimeout(async () => { - try { - const res = (await callAppRoute('/contributors/search', { - query: searchQuery, - limit: 20, - })) as SearchResponse; - if (!cancelled) setSearchResults(res.contributors ?? []); - } catch (err) { - if (!cancelled) { - enqueueSnackbar({ - message: - err instanceof Error ? err.message : 'Failed to search contributors', - variant: 'error', - }); - } - } finally { - if (!cancelled) setSearchLoading(false); - } - }, 200); - return () => { - cancelled = true; - clearTimeout(handle); - }; - }, [selectedId, searchQuery]); - - useEffect(() => { - if (!selectedId) { - setStats(null); - return; - } - let cancelled = false; - setLoading(true); - setError(null); - (async () => { - try { - const res = (await callAppRoute('/contributors/stats', { - contributorId: selectedId, - period, - })) as StatsResponse; - if (cancelled) return; - if (res.error) { - setError(res.error); - setStats(null); - } else { - setStats(res); - } - } catch (err) { - if (!cancelled) { - setError(err instanceof Error ? err.message : 'Failed to load stats'); - setStats(null); - } - } finally { - if (!cancelled) setLoading(false); - } - })(); - return () => { - cancelled = true; - }; - }, [selectedId, period]); - - const renderHeader = (contributor: ContributorInfo | null) => ( -
-
- {contributor?.avatarUrl ? ( - - ) : ( -
- )} -
- - {contributor?.name ?? contributor?.ghLogin ?? 'Contributor stats'} - - {contributor?.ghLogin && - contributor.ghLogin !== contributor.name && ( - @{contributor.ghLogin} - )} -
-
-
- {selectedId && !recordId && ( - - )} - -
-
- ); - - if (!selectedId) { - return ( -
- {renderHeader(null)} -
- -
- {searchLoading && searchResults.length === 0 && ( -
- Searching... -
- )} - {!searchLoading && searchResults.length === 0 && ( -
- No contributors found. -
- )} - {searchResults.map((eng) => ( -
setSelectedId(eng.id)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') setSelectedId(eng.id); - }} - style={styles.searchItem} - > - {eng.avatarUrl ? ( - - ) : ( -
- )} -
- - {eng.name ?? eng.ghLogin ?? 'Unknown'} - - {eng.ghLogin && eng.ghLogin !== eng.name && ( - @{eng.ghLogin} - )} -
-
- ))} -
-
-
- ); - } - - return ( -
- {renderHeader(stats?.contributor ?? null)} -
- {loading && !stats && ( -
Loading contributor stats...
- )} - {error &&
{error}
} - {stats && ( - <> -
-
- PRs authored (merged) - {stats.totals.prAuthored} -
- - {stats.truncated.prAuthored && ( - - Showing partial data (results truncated). - - )} -
- -
-
- PRs merged - {stats.totals.prMerged} -
- - {stats.truncated.prMerged && ( - - Showing partial data (results truncated). - - )} -
- -
-
- PRs reviewed - {stats.totals.prReviewed} -
- - {stats.truncated.prReviewed && ( - - Showing partial data (results truncated). - - )} -
- - )} -
-
- ); -}; - -export default defineFrontComponent({ - universalIdentifier: '6c2f1a8d-4b9e-4f7a-9c5d-1e8b2a3d4c5f', - name: 'Contributor Stats', - description: - 'Displays time-bucketed charts of PRs authored (merged only), merged and reviewed by a contributor over the last week, month, 3 months or year.', - component: ContributorStats, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/fetch-contributors.front-component.tsx b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/fetch-contributors.front-component.tsx deleted file mode 100644 index a3d70ae079..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/fetch-contributors.front-component.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useEffect, useState } from 'react'; -import { defineFrontComponent } from 'twenty-sdk/define'; -import { - enqueueSnackbar, - unmountFrontComponent, - updateProgress, -} from 'twenty-sdk/front-component'; -import { callAppRoute } from 'src/modules/shared/call-app-route'; - -type CountResponse = { - totalPages: number; - repos: Array<{ owner: string; repo: string; totalCount: number; pages: number }>; -}; - -type FetchPageResponse = { - contributorCount: number; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}; - -type SyncStatus = 'syncing' | 'done' | 'error'; - -const FetchContributors = () => { - const [status, setStatus] = useState('syncing'); - - useEffect(() => { - const run = async () => { - try { - const counts = (await callAppRoute( - '/github/count-contributors', - {}, - )) as CountResponse; - - if (counts.repos.length === 0) { - throw new Error( - 'No repos resolved. Set GITHUB_REPOS in the application variables.', - ); - } - - const totalPages = Math.max(counts.totalPages, 1); - let pagesProcessed = 0; - let totalSynced = 0; - - for (const { owner, repo } of counts.repos) { - let cursor: string | null = null; - let hasMore = true; - - while (hasMore) { - const data = (await callAppRoute('/github/fetch-contributors', { - owner, - repo, - cursor, - })) as FetchPageResponse; - - totalSynced += data.contributorCount; - hasMore = data.hasMore && data.contributorCount > 0; - cursor = data.endCursor; - pagesProcessed++; - - updateProgress( - Math.min(Math.round((pagesProcessed / totalPages) * 100), 99), - ); - } - } - - updateProgress(100); - enqueueSnackbar({ - message: `Synced ${totalSynced} contributors`, - variant: 'success', - }); - setStatus('done'); - } catch (err) { - const message = - err instanceof Error ? err.message : 'Failed to fetch contributors'; - enqueueSnackbar({ message, variant: 'error' }); - setStatus('error'); - } finally { - unmountFrontComponent(); - } - }; - run(); - }, []); - - if (status === 'syncing') return
Fetching contributors...
; - if (status === 'error') return
Failed to fetch contributors.
; - return
Done
; -}; - -export default defineFrontComponent({ - universalIdentifier: '08f40f82-24ed-4f3e-8c99-695151e90e38', - name: 'Fetch Contributors', - description: - 'Fetches contributors from every configured GitHub repo (GITHUB_REPOS) and upserts them as Contributor records.', - isHeadless: true, - component: FetchContributors, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/top-pr-authors.front-component.tsx b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/top-pr-authors.front-component.tsx deleted file mode 100644 index 9818b1e28b..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/top-pr-authors.front-component.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useEffect, useState } from 'react'; -import { defineFrontComponent } from 'twenty-sdk/define'; -import { enqueueSnackbar } from 'twenty-sdk/front-component'; -import { - Leaderboard, - type LeaderboardEntry, -} from 'src/modules/github/contributor/components/leaderboard'; -import { callAppRoute } from 'src/modules/shared/call-app-route'; - -type Response = { - topAuthors: LeaderboardEntry[]; -}; - -const TopPRAuthors = () => { - const [entries, setEntries] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - let cancelled = false; - setLoading(true); - setError(null); - (async () => { - try { - const res = (await callAppRoute('/contributors/top', { - days: 90, - limit: 20, - kind: 'authors', - })) as Response; - if (!cancelled) setEntries(res.topAuthors ?? []); - } catch (err) { - if (!cancelled) { - const message = - err instanceof Error ? err.message : 'Failed to load top authors'; - setError(message); - enqueueSnackbar({ message, variant: 'error' }); - } - } finally { - if (!cancelled) setLoading(false); - } - })(); - return () => { - cancelled = true; - }; - }, []); - - return ( - - ); -}; - -export const TOP_PR_AUTHORS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER = - 'a1d4f7e2-9b3c-4e8a-bf21-5d6c8a9b2e3f'; - -export default defineFrontComponent({ - universalIdentifier: TOP_PR_AUTHORS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, - name: 'Top PR Authors', - description: 'Leaderboard of the top 20 pull-request authors over the last 90 days.', - component: TopPRAuthors, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/top-reviewers.front-component.tsx b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/top-reviewers.front-component.tsx deleted file mode 100644 index 457a682b24..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/front-components/top-reviewers.front-component.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useEffect, useState } from 'react'; -import { defineFrontComponent } from 'twenty-sdk/define'; -import { enqueueSnackbar } from 'twenty-sdk/front-component'; -import { - Leaderboard, - type LeaderboardEntry, -} from 'src/modules/github/contributor/components/leaderboard'; -import { callAppRoute } from 'src/modules/shared/call-app-route'; - -type Response = { - topReviewers: LeaderboardEntry[]; -}; - -const TopReviewers = () => { - const [entries, setEntries] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - let cancelled = false; - setLoading(true); - setError(null); - (async () => { - try { - const res = (await callAppRoute('/contributors/top', { - days: 90, - limit: 20, - kind: 'reviewers', - })) as Response; - if (!cancelled) setEntries(res.topReviewers ?? []); - } catch (err) { - if (!cancelled) { - const message = - err instanceof Error ? err.message : 'Failed to load top reviewers'; - setError(message); - enqueueSnackbar({ message, variant: 'error' }); - } - } finally { - if (!cancelled) setLoading(false); - } - })(); - return () => { - cancelled = true; - }; - }, []); - - return ( - - ); -}; - -export const TOP_REVIEWERS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER = - 'b2e5f8a3-ac4d-4f9b-8032-6e7d9bac3f4a'; - -export default defineFrontComponent({ - universalIdentifier: TOP_REVIEWERS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, - name: 'Top Reviewers', - description: 'Leaderboard of the top 20 pull-request reviewers over the last 90 days.', - component: TopReviewers, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/github/count-contributors.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/github/count-contributors.ts deleted file mode 100644 index 343c1f0b83..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/github/count-contributors.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { githubGraphql } from 'src/modules/github/connector/github-client'; - -const QUERY = ` -query($owner: String!, $name: String!) { - repository(owner: $owner, name: $name) { - mentionableUsers { totalCount } - } -}`; - -type Response = { - repository: { mentionableUsers: { totalCount: number } } | null; -}; - -export async function countContributors( - owner: string, - name: string, -): Promise { - const data = await githubGraphql(QUERY, { owner, name }); - return data.repository?.mentionableUsers.totalCount ?? 0; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/github/fetch-contributors.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/github/fetch-contributors.ts deleted file mode 100644 index 78c489eb92..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/github/fetch-contributors.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - EMPTY_PAGE, - type GithubPage, - githubGraphql, -} from 'src/modules/github/connector/github-client'; - -const QUERY = ` -query($owner: String!, $name: String!, $cursor: String) { - repository(owner: $owner, name: $name) { - mentionableUsers(first: 100, after: $cursor) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - login - databaseId - avatarUrl - } - } - } -}`; - -export type GqlContributor = { - login: string; - databaseId: number; - avatarUrl: string; -}; - -type Response = { - repository: { mentionableUsers: GithubPage } | null; -}; - -export async function fetchContributors( - owner: string, - name: string, - cursor: string | null = null, -): Promise<{ - contributors: GqlContributor[]; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}> { - const data = await githubGraphql(QUERY, { owner, name, cursor }); - const conn = data.repository?.mentionableUsers; - if (!conn) return { contributors: [], ...EMPTY_PAGE }; - - return { - contributors: conn.nodes, - totalCount: conn.totalCount, - hasMore: conn.pageInfo.hasNextPage, - endCursor: conn.pageInfo.endCursor, - }; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/mutations/batch-upsert.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/mutations/batch-upsert.ts deleted file mode 100644 index b6f3f7e867..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/mutations/batch-upsert.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ContributorRow } from 'src/modules/github/contributor/types/contributor-row'; -import { chunkedBatchCreate } from 'src/modules/shared/twenty-client'; - -export async function batchUpsertContributors( - items: Array<{ - ghLogin: string; - name: string; - githubId: number; - avatarUrl?: { primaryLinkLabel: string; primaryLinkUrl: string; secondaryLinks: null } | null; - contributions?: number; - }>, -): Promise { - return chunkedBatchCreate('createContributors', items, { - id: true, - ghLogin: true, - name: true, - githubId: true, - avatarUrl: true, - contributions: true, - }) as Promise; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/queries/find-by-gh-login.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/queries/find-by-gh-login.ts deleted file mode 100644 index 9ee5b6a64b..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/queries/find-by-gh-login.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { ContributorRow } from 'src/modules/github/contributor/types/contributor-row'; -import { getClient } from 'src/modules/shared/twenty-client'; - -export async function findContributorByGhLogin( - ghLogin: string, -): Promise { - const client = getClient(); - const res = await client.query({ - contributors: { - __args: { - filter: { ghLogin: { eq: ghLogin } }, - first: 1, - }, - edges: { - node: { - id: true, - ghLogin: true, - name: true, - githubId: true, - }, - }, - }, - }); - - const edges = res.contributors?.edges; - return (edges?.[0]?.node as ContributorRow | undefined) ?? null; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/queries/search-contributors.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/queries/search-contributors.ts deleted file mode 100644 index 5c45ba376c..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/graphql/queries/search-contributors.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getClient } from 'src/modules/shared/twenty-client'; - -export type ContributorSearchResult = { - id: string; - name: string | null; - ghLogin: string | null; - avatarUrl: string | null; -}; - -export async function searchContributors( - query: string, - limit: number, -): Promise { - const client = getClient(); - - const filter = - query.length === 0 - ? undefined - : { - or: [ - { name: { ilike: `%${query}%` } }, - { ghLogin: { ilike: `%${query}%` } }, - ], - }; - - const res = await client.query({ - contributors: { - __args: { - ...(filter ? { filter } : {}), - orderBy: [{ name: 'AscNullsLast' }], - first: limit, - }, - edges: { - node: { - id: true, - name: true, - ghLogin: true, - avatarUrl: { primaryLinkUrl: true }, - }, - }, - }, - }); - - const edges = res.contributors?.edges ?? []; - - return edges.map((e) => ({ - id: e.node.id, - name: e.node.name ?? null, - ghLogin: e.node.ghLogin ?? null, - avatarUrl: e.node.avatarUrl?.primaryLinkUrl ?? null, - })); -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/contributor-stats.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/contributor-stats.logic-function.ts deleted file mode 100644 index 4ebb1dfab4..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/contributor-stats.logic-function.ts +++ /dev/null @@ -1,389 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { getClient } from 'src/modules/shared/twenty-client'; - -export type StatsPeriod = 'week' | 'month' | '3months' | 'year'; - -type Granularity = 'day' | 'week' | 'month'; - -type Bucket = { - key: string; - label: string; - start: string; - end: string; - prAuthored: number; - prMerged: number; - prReviewed: number; -}; - -type Totals = { - prAuthored: number; - prMerged: number; - prReviewed: number; -}; - -type ContributorInfo = { - id: string; - name: string | null; - ghLogin: string | null; - avatarUrl: string | null; -}; - -type ContributorStatsPayload = { - contributorId?: string; - period?: StatsPeriod; -}; - -type ContributorStatsResponse = - | { - contributor: ContributorInfo; - period: StatsPeriod; - granularity: Granularity; - buckets: Bucket[]; - totals: Totals; - truncated: { - prAuthored: boolean; - prMerged: boolean; - prReviewed: boolean; - }; - } - | { error: string }; - -const PAGE_SIZE = 100; -const MAX_PAGES = 30; - -const PERIOD_CONFIG: Record< - StatsPeriod, - { granularity: Granularity; rangeMs: number; bucketCount: number } -> = { - week: { granularity: 'day', rangeMs: 7 * 24 * 3600 * 1000, bucketCount: 7 }, - month: { - granularity: 'day', - rangeMs: 30 * 24 * 3600 * 1000, - bucketCount: 30, - }, - '3months': { - granularity: 'week', - rangeMs: 13 * 7 * 24 * 3600 * 1000, - bucketCount: 13, - }, - year: { - granularity: 'month', - rangeMs: 365 * 24 * 3600 * 1000, - bucketCount: 12, - }, -}; - -const startOfUtcDay = (d: Date): Date => - new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())); - -const startOfUtcWeek = (d: Date): Date => { - const day = startOfUtcDay(d); - const dow = day.getUTCDay(); - const diff = (dow + 6) % 7; - day.setUTCDate(day.getUTCDate() - diff); - return day; -}; - -const startOfUtcMonth = (d: Date): Date => - new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1)); - -const addBucket = (start: Date, granularity: Granularity, n: number): Date => { - const out = new Date(start); - if (granularity === 'day') out.setUTCDate(out.getUTCDate() + n); - else if (granularity === 'week') out.setUTCDate(out.getUTCDate() + 7 * n); - else out.setUTCMonth(out.getUTCMonth() + n); - return out; -}; - -const bucketStartFor = (d: Date, granularity: Granularity): Date => { - if (granularity === 'day') return startOfUtcDay(d); - if (granularity === 'week') return startOfUtcWeek(d); - return startOfUtcMonth(d); -}; - -const formatBucketLabel = (start: Date, granularity: Granularity): string => { - const month = start.toLocaleString('en-US', { - month: 'short', - timeZone: 'UTC', - }); - if (granularity === 'month') { - return `${month} ${String(start.getUTCFullYear()).slice(2)}`; - } - return `${month} ${start.getUTCDate()}`; -}; - -const buildBuckets = ( - now: Date, - period: StatsPeriod, -): { buckets: Bucket[]; rangeStart: Date; granularity: Granularity } => { - const { granularity, bucketCount } = PERIOD_CONFIG[period]; - const lastBucketStart = bucketStartFor(now, granularity); - const firstBucketStart = addBucket( - lastBucketStart, - granularity, - -(bucketCount - 1), - ); - - const buckets: Bucket[] = []; - for (let i = 0; i < bucketCount; i++) { - const start = addBucket(firstBucketStart, granularity, i); - const end = addBucket(start, granularity, 1); - buckets.push({ - key: start.toISOString(), - label: formatBucketLabel(start, granularity), - start: start.toISOString(), - end: end.toISOString(), - prAuthored: 0, - prMerged: 0, - prReviewed: 0, - }); - } - return { buckets, rangeStart: firstBucketStart, granularity }; -}; - -type Edge = { node: T }; -type Connection = { - edges: Edge[]; - pageInfo: { hasNextPage: boolean; endCursor: string | null }; -}; - -async function paginateUntil( - fetchPage: (cursor: string | null) => Promise>, - isOlderThanRange: (item: T) => boolean, -): Promise<{ items: T[]; truncated: boolean }> { - const items: T[] = []; - let cursor: string | null = null; - for (let page = 0; page < MAX_PAGES; page++) { - const conn = await fetchPage(cursor); - let stop = false; - for (const edge of conn.edges) { - if (isOlderThanRange(edge.node)) { - stop = true; - break; - } - items.push(edge.node); - } - if (stop) return { items, truncated: false }; - if (!conn.pageInfo.hasNextPage || !conn.pageInfo.endCursor) { - return { items, truncated: false }; - } - cursor = conn.pageInfo.endCursor; - } - return { items, truncated: true }; -} - -type MergedPrNode = { mergedAt: string | null }; -type ReviewNode = { firstSubmittedAt: string | null }; - -const fetchContributorInfo = async ( - contributorId: string, -): Promise => { - const client = getClient(); - const res = await client.query({ - contributors: { - __args: { filter: { id: { eq: contributorId } }, first: 1 }, - edges: { - node: { - id: true, - name: true, - ghLogin: true, - avatarUrl: { primaryLinkUrl: true }, - }, - }, - }, - }); - const node = res.contributors?.edges?.[0]?.node; - if (!node) return null; - return { - id: node.id, - name: node.name ?? null, - ghLogin: node.ghLogin ?? null, - avatarUrl: node.avatarUrl?.primaryLinkUrl ?? null, - }; -}; - -const handler = async ( - event: RoutePayload, -): Promise => { - const contributorId = event.body?.contributorId; - const period: StatsPeriod = event.body?.period ?? 'month'; - - if (!contributorId) { - return { error: 'contributorId is required' }; - } - if (!(period in PERIOD_CONFIG)) { - return { error: `Unsupported period: ${period}` }; - } - - const contributor = await fetchContributorInfo(contributorId); - if (!contributor) { - return { error: 'Contributor not found' }; - } - - const now = new Date(); - const { buckets, rangeStart, granularity } = buildBuckets(now, period); - const rangeStartMs = rangeStart.getTime(); - const bucketIndex = new Map(); - buckets.forEach((b, i) => bucketIndex.set(b.key, i)); - - const client = getClient(); - - const mergedResult = await paginateUntil( - async (cursor) => { - const res = await client.query({ - pullRequests: { - __args: { - filter: { - and: [ - { mergerId: { eq: contributorId } }, - { state: { eq: 'MERGED' } }, - ], - }, - orderBy: [{ mergedAt: 'DescNullsLast' }], - first: PAGE_SIZE, - after: cursor, - }, - edges: { node: { mergedAt: true } }, - pageInfo: { hasNextPage: true, endCursor: true }, - }, - }); - return ( - (res.pullRequests as Connection) ?? { - edges: [], - pageInfo: { hasNextPage: false, endCursor: null }, - } - ); - }, - (n) => { - if (!n.mergedAt) return false; - return new Date(n.mergedAt).getTime() < rangeStartMs; - }, - ); - - const authoredResult = await paginateUntil( - async (cursor) => { - const res = await client.query({ - pullRequests: { - __args: { - filter: { - and: [ - { authorId: { eq: contributorId } }, - { state: { eq: 'MERGED' } }, - ], - }, - orderBy: [{ mergedAt: 'DescNullsLast' }], - first: PAGE_SIZE, - after: cursor, - }, - edges: { node: { mergedAt: true } }, - pageInfo: { hasNextPage: true, endCursor: true }, - }, - }); - return ( - (res.pullRequests as Connection) ?? { - edges: [], - pageInfo: { hasNextPage: false, endCursor: null }, - } - ); - }, - (n) => { - if (!n.mergedAt) return false; - return new Date(n.mergedAt).getTime() < rangeStartMs; - }, - ); - - const reviewedResult = await paginateUntil( - async (cursor) => { - const res = await client.query({ - pullRequestReviews: { - __args: { - filter: { - and: [ - { reviewerId: { eq: contributorId } }, - { isSelfReview: { eq: false } }, - ], - }, - orderBy: [{ firstSubmittedAt: 'DescNullsLast' }], - first: PAGE_SIZE, - after: cursor, - }, - edges: { node: { firstSubmittedAt: true } }, - pageInfo: { hasNextPage: true, endCursor: true }, - }, - }); - return ( - (res.pullRequestReviews as Connection) ?? { - edges: [], - pageInfo: { hasNextPage: false, endCursor: null }, - } - ); - }, - (n) => { - if (!n.firstSubmittedAt) return false; - return new Date(n.firstSubmittedAt).getTime() < rangeStartMs; - }, - ); - - const totals: Totals = { prAuthored: 0, prMerged: 0, prReviewed: 0 }; - - for (const pr of authoredResult.items) { - if (!pr.mergedAt) continue; - const d = new Date(pr.mergedAt); - if (d.getTime() < rangeStartMs) continue; - const key = bucketStartFor(d, granularity).toISOString(); - const idx = bucketIndex.get(key); - if (idx === undefined) continue; - buckets[idx].prAuthored++; - totals.prAuthored++; - } - - for (const pr of mergedResult.items) { - if (!pr.mergedAt) continue; - const d = new Date(pr.mergedAt); - if (d.getTime() < rangeStartMs) continue; - const key = bucketStartFor(d, granularity).toISOString(); - const idx = bucketIndex.get(key); - if (idx === undefined) continue; - buckets[idx].prMerged++; - totals.prMerged++; - } - - for (const r of reviewedResult.items) { - if (!r.firstSubmittedAt) continue; - const d = new Date(r.firstSubmittedAt); - if (d.getTime() < rangeStartMs) continue; - const key = bucketStartFor(d, granularity).toISOString(); - const idx = bucketIndex.get(key); - if (idx === undefined) continue; - buckets[idx].prReviewed++; - totals.prReviewed++; - } - - return { - contributor, - period, - granularity, - buckets, - totals, - truncated: { - prAuthored: authoredResult.truncated, - prMerged: mergedResult.truncated, - prReviewed: reviewedResult.truncated, - }, - }; -}; - -export default defineLogicFunction({ - universalIdentifier: 'a3c9e1b6-2f47-4d8a-9b0f-7e6d1a2c3b4f', - name: 'contributor-stats', - description: - 'Returns time-bucketed counts of PRs authored (merged only), merged and reviewed by a contributor over the selected period.', - timeoutSeconds: 30, - handler, - httpRouteTriggerSettings: { - path: '/contributors/stats', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/count-contributors.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/count-contributors.logic-function.ts deleted file mode 100644 index 5704f40049..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/count-contributors.logic-function.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { countAcrossRepos } from 'src/modules/github/connector/count-across-repos'; -import { countContributors } from 'src/modules/github/contributor/graphql/github/count-contributors'; - -type CountContributorsPayload = { - repos?: string[]; -}; - -const handler = async (event: RoutePayload) => - countAcrossRepos(event.body?.repos, countContributors, 'count-contributors'); - -export default defineLogicFunction({ - universalIdentifier: 'fe0a6f00-0d63-4cb9-9b3c-1d8186181830', - name: 'count-contributors', - description: - 'Counts contributors across configured repos and returns the per-repo page split.', - timeoutSeconds: 30, - handler, - httpRouteTriggerSettings: { - path: '/github/count-contributors', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/fetch-contributors.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/fetch-contributors.logic-function.ts deleted file mode 100644 index 0a5128aa52..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/fetch-contributors.logic-function.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { - fetchContributors, - type GqlContributor, -} from 'src/modules/github/contributor/graphql/github/fetch-contributors'; -import { batchUpsertContributors } from 'src/modules/github/contributor/graphql/mutations/batch-upsert'; -import { isFixtureAllowed } from 'src/modules/shared/fixtures'; - -export type FetchContributorsFixturePage = { - contributors: GqlContributor[]; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}; - -type FetchContributorsPayload = { - owner: string; - repo: string; - cursor?: string | null; - fixturePage?: FetchContributorsFixturePage; -}; - -const handler = async (event: RoutePayload) => { - const { owner, repo, cursor = null, fixturePage } = event.body ?? {}; - if (!owner || !repo) { - return { error: 'owner and repo are required' }; - } - - const result = - fixturePage && isFixtureAllowed() - ? fixturePage - : await fetchContributors(owner, repo, cursor); - const { contributors, totalCount, hasMore, endCursor } = result; - - if (contributors.length === 0) { - return { contributorCount: 0, totalCount, hasMore: false, endCursor: null }; - } - - const contributorData = contributors.map((c: GqlContributor) => ({ - ghLogin: c.login, - name: c.login, - githubId: c.databaseId ?? 0, - avatarUrl: c.avatarUrl - ? { - primaryLinkLabel: c.login, - primaryLinkUrl: c.avatarUrl, - secondaryLinks: null, - } - : null, - })); - - await batchUpsertContributors(contributorData); - - return { - contributorCount: contributors.length, - totalCount, - hasMore, - endCursor, - }; -}; - -export default defineLogicFunction({ - universalIdentifier: 'ed9ba981-4172-4924-a18f-51fc6dcbcb23', - name: 'fetch-contributors', - description: - 'Fetches one page of contributors via GitHub GraphQL API and batch upserts them into the workspace.', - timeoutSeconds: 300, - handler, - httpRouteTriggerSettings: { - path: '/github/fetch-contributors', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/search-contributors.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/search-contributors.logic-function.ts deleted file mode 100644 index 362fc64c04..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/search-contributors.logic-function.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { - searchContributors, - type ContributorSearchResult, -} from 'src/modules/github/contributor/graphql/queries/search-contributors'; - -type SearchContributorsPayload = { - query?: string; - limit?: number; -}; - -const DEFAULT_LIMIT = 20; -const MAX_LIMIT = 50; - -const handler = async ( - event: RoutePayload, -): Promise<{ contributors: ContributorSearchResult[] }> => { - const queryInput = event.body?.query; - const rawQuery = typeof queryInput === 'string' ? queryInput.trim() : ''; - const limitInput = event.body?.limit; - const limitNumber = - typeof limitInput === 'number' && Number.isFinite(limitInput) - ? limitInput - : DEFAULT_LIMIT; - const limit = Math.min(Math.max(Math.floor(limitNumber), 1), MAX_LIMIT); - - const contributors = await searchContributors(rawQuery, limit); - return { contributors }; -}; - -export default defineLogicFunction({ - universalIdentifier: 'b4d8f2a7-3e58-4f9b-ac1d-8f7e2b3c4d5e', - name: 'search-contributors', - description: - 'Searches contributors by name or GitHub login for use in front-end pickers.', - timeoutSeconds: 15, - handler, - httpRouteTriggerSettings: { - path: '/contributors/search', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/top-contributors.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/top-contributors.logic-function.ts deleted file mode 100644 index 5323e896cc..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/logic-functions/top-contributors.logic-function.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { isBotLogin } from 'src/modules/github/contributor/utils/is-bot-login'; -import { getClient } from 'src/modules/shared/twenty-client'; - -type ContributorRef = { - id: string; - name: string | null; - ghLogin: string | null; - avatarUrl: string | null; -}; - -type LeaderboardEntry = ContributorRef & { - count: number; -}; - -type TopContributorsKind = 'authors' | 'reviewers' | 'both'; - -type TopContributorsPayload = { - days?: number; - limit?: number; - kind?: TopContributorsKind; -}; - -type TopContributorsResponse = { - days: number; - limit: number; - topAuthors: LeaderboardEntry[]; - topReviewers: LeaderboardEntry[]; - truncated: { authors: boolean; reviewers: boolean }; -}; - -const PAGE_SIZE = 100; -const MAX_PAGES = 50; -const DEFAULT_DAYS = 90; -const DEFAULT_LIMIT = 10; -const MAX_LIMIT = 50; -const MAX_DAYS = 365; - -type Edge = { node: T }; -type Connection = { - edges: Edge[]; - pageInfo: { hasNextPage: boolean; endCursor: string | null }; -}; - -async function paginateUntil( - fetchPage: (cursor: string | null) => Promise>, - isOlderThanRange: (item: T) => boolean, -): Promise<{ items: T[]; truncated: boolean }> { - const items: T[] = []; - let cursor: string | null = null; - for (let page = 0; page < MAX_PAGES; page++) { - const conn = await fetchPage(cursor); - let stop = false; - for (const edge of conn.edges) { - if (isOlderThanRange(edge.node)) { - stop = true; - break; - } - items.push(edge.node); - } - if (stop) return { items, truncated: false }; - if (!conn.pageInfo.hasNextPage || !conn.pageInfo.endCursor) { - return { items, truncated: false }; - } - cursor = conn.pageInfo.endCursor; - } - return { items, truncated: true }; -} - -type AuthorInfo = { - id: string; - name: string | null; - ghLogin: string | null; - avatarUrl: { primaryLinkUrl: string | null } | null; -}; - -type PrNode = { - githubCreatedAt: string | null; - author: AuthorInfo | null; -}; - -type ReviewNode = { - firstSubmittedAt: string | null; - reviewer: AuthorInfo | null; -}; - -const tally = ( - items: { contributor: AuthorInfo | null }[], - limit: number, -): LeaderboardEntry[] => { - const counts = new Map(); - for (const item of items) { - const c = item.contributor; - if (!c) continue; - if (isBotLogin(c.ghLogin)) continue; - const existing = counts.get(c.id); - if (existing) { - existing.count += 1; - } else { - counts.set(c.id, { - id: c.id, - name: c.name ?? null, - ghLogin: c.ghLogin ?? null, - avatarUrl: c.avatarUrl?.primaryLinkUrl ?? null, - count: 1, - }); - } - } - return Array.from(counts.values()) - .sort( - (a, b) => - b.count - a.count || (a.ghLogin ?? '').localeCompare(b.ghLogin ?? ''), - ) - .slice(0, limit); -}; - -const handler = async ( - event: RoutePayload, -): Promise => { - const daysInput = event.body?.days; - const limitInput = event.body?.limit; - - const days = Math.min( - Math.max( - Math.floor( - typeof daysInput === 'number' && Number.isFinite(daysInput) - ? daysInput - : DEFAULT_DAYS, - ), - 1, - ), - MAX_DAYS, - ); - const limit = Math.min( - Math.max( - Math.floor( - typeof limitInput === 'number' && Number.isFinite(limitInput) - ? limitInput - : DEFAULT_LIMIT, - ), - 1, - ), - MAX_LIMIT, - ); - - const kindInput = event.body?.kind; - const kind: TopContributorsKind = - kindInput === 'authors' || kindInput === 'reviewers' ? kindInput : 'both'; - - const sinceMs = Date.now() - days * 24 * 3600 * 1000; - const client = getClient(); - - const authoredResult: { items: PrNode[]; truncated: boolean } = - kind === 'reviewers' - ? { items: [], truncated: false } - : await paginateUntil( - async (cursor) => { - const res = await client.query({ - pullRequests: { - __args: { - orderBy: [{ githubCreatedAt: 'DescNullsLast' }], - first: PAGE_SIZE, - after: cursor, - }, - edges: { - node: { - githubCreatedAt: true, - author: { - id: true, - name: true, - ghLogin: true, - avatarUrl: { primaryLinkUrl: true }, - }, - }, - }, - pageInfo: { hasNextPage: true, endCursor: true }, - }, - }); - return ( - (res.pullRequests as Connection) ?? { - edges: [], - pageInfo: { hasNextPage: false, endCursor: null }, - } - ); - }, - (n) => { - if (!n.githubCreatedAt) return false; - return new Date(n.githubCreatedAt).getTime() < sinceMs; - }, - ); - - const reviewedResult: { items: ReviewNode[]; truncated: boolean } = - kind === 'authors' - ? { items: [], truncated: false } - : await paginateUntil( - async (cursor) => { - const res = await client.query({ - pullRequestReviews: { - __args: { - filter: { isSelfReview: { eq: false } }, - orderBy: [{ firstSubmittedAt: 'DescNullsLast' }], - first: PAGE_SIZE, - after: cursor, - }, - edges: { - node: { - firstSubmittedAt: true, - reviewer: { - id: true, - name: true, - ghLogin: true, - avatarUrl: { primaryLinkUrl: true }, - }, - }, - }, - pageInfo: { hasNextPage: true, endCursor: true }, - }, - }); - return ( - (res.pullRequestReviews as Connection) ?? { - edges: [], - pageInfo: { hasNextPage: false, endCursor: null }, - } - ); - }, - (n) => { - if (!n.firstSubmittedAt) return false; - return new Date(n.firstSubmittedAt).getTime() < sinceMs; - }, - ); - - const topAuthors = tally( - authoredResult.items.map((pr) => ({ contributor: pr.author })), - limit, - ); - const topReviewers = tally( - reviewedResult.items.map((r) => ({ contributor: r.reviewer })), - limit, - ); - - return { - days, - limit, - topAuthors, - topReviewers, - truncated: { - authors: authoredResult.truncated, - reviewers: reviewedResult.truncated, - }, - }; -}; - -export default defineLogicFunction({ - universalIdentifier: 'd5b9c4a2-7e3f-4a1b-9c8d-2f4e6a7b8c9d', - name: 'top-contributors', - description: - 'Returns the top contributors by pull-request authorship and review counts over a configurable time window.', - timeoutSeconds: 30, - handler, - httpRouteTriggerSettings: { - path: '/contributors/top', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/normalizers.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/normalizers.ts deleted file mode 100644 index 8462783aea..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/normalizers.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { type LinksFieldValue, toLinksField } from 'src/modules/shared/types'; - -export type GhUser = { - login: string; - id?: number; - databaseId?: number; - avatarUrl?: string | null; - avatar_url?: string | null; -}; - -export type ContributorCanonical = { - ghLogin: string; - name: string; - githubId: number; - avatarUrl: LinksFieldValue | null; -}; - -export function contributorFromGhUser(user: GhUser): ContributorCanonical { - const avatarUrl = user.avatarUrl ?? user.avatar_url ?? null; - return { - ghLogin: user.login, - name: user.login, - githubId: user.id ?? user.databaseId ?? 0, - avatarUrl: avatarUrl ? toLinksField(avatarUrl, user.login) : null, - }; -} - -export function dedupeContributors( - users: Array, -): ContributorCanonical[] { - const seen = new Map(); - for (const u of users) { - if (!u || !u.login) continue; - const existing = seen.get(u.login); - if (!existing) { - seen.set(u.login, contributorFromGhUser(u)); - continue; - } - if (!existing.avatarUrl) { - const avatarUrl = u.avatarUrl ?? u.avatar_url ?? null; - if (avatarUrl) { - existing.avatarUrl = toLinksField(avatarUrl, u.login); - } - } - } - return [...seen.values()]; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/objects/contributor.object.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/objects/contributor.object.ts deleted file mode 100644 index c0de548677..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/objects/contributor.object.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { defineObject, FieldType } from 'twenty-sdk/define'; - -export const CONTRIBUTOR_UNIVERSAL_IDENTIFIER = - '8e9464f0-fdc9-487a-9963-c7accac0d4bb'; - -export const CONTRIBUTOR_NAME_FIELD_UNIVERSAL_IDENTIFIER = - 'e2e77e0e-5300-44ce-ab1c-9ad0593b5d1a'; - -export const CONTRIBUTOR_GH_LOGIN_FIELD_UNIVERSAL_IDENTIFIER = - 'a1d3c7b2-4e5f-4a8b-9c6d-2e1f0b3a4c5d'; - -export const CONTRIBUTOR_GITHUB_ID_FIELD_UNIVERSAL_IDENTIFIER = - 'f7a8b9c0-1d2e-4f3a-8b5c-6d7e8f9a0b1c'; - -export const CONTRIBUTOR_AVATAR_URL_FIELD_UNIVERSAL_IDENTIFIER = - '450a612c-f7a3-4bba-aa36-e22339eb0720'; - -export const CONTRIBUTOR_CONTRIBUTIONS_FIELD_UNIVERSAL_IDENTIFIER = - '66f73e49-6edb-48fa-812b-c3e684ef6340'; - -export const CONTRIBUTOR_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER = - 'bb8d2a87-b41c-578b-98de-2e37dba12a14'; - -export default defineObject({ - universalIdentifier: CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - nameSingular: 'contributor', - namePlural: 'contributors', - labelSingular: 'Contributor', - labelPlural: 'Contributors', - icon: 'IconUsers', - labelIdentifierFieldMetadataUniversalIdentifier: - CONTRIBUTOR_NAME_FIELD_UNIVERSAL_IDENTIFIER, - fields: [ - { - universalIdentifier: CONTRIBUTOR_NAME_FIELD_UNIVERSAL_IDENTIFIER, - name: 'name', - type: FieldType.TEXT, - label: 'Name', - icon: 'IconUser', - }, - { - universalIdentifier: CONTRIBUTOR_GH_LOGIN_FIELD_UNIVERSAL_IDENTIFIER, - name: 'ghLogin', - type: FieldType.TEXT, - label: 'GitHub Login', - icon: 'IconBrandGithub', - isUnique: true, - }, - { - universalIdentifier: CONTRIBUTOR_GITHUB_ID_FIELD_UNIVERSAL_IDENTIFIER, - name: 'githubId', - type: FieldType.NUMBER, - label: 'GitHub ID', - icon: 'IconHash', - }, - { - universalIdentifier: CONTRIBUTOR_AVATAR_URL_FIELD_UNIVERSAL_IDENTIFIER, - name: 'avatarUrl', - type: FieldType.LINKS, - label: 'Avatar', - icon: 'IconPhoto', - }, - { - universalIdentifier: CONTRIBUTOR_CONTRIBUTIONS_FIELD_UNIVERSAL_IDENTIFIER, - name: 'contributions', - type: FieldType.NUMBER, - label: 'Contributions', - icon: 'IconTrendingUp', - defaultValue: 0, - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/types/contributor-row.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/types/contributor-row.ts deleted file mode 100644 index 0f4fbce38b..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/types/contributor-row.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { LinksFieldValue } from 'src/modules/shared/types'; - -export type ContributorRow = { - id: string; - ghLogin?: string | null; - name?: string | null; - githubId?: number | null; - avatarUrl?: LinksFieldValue | null; - contributions?: number | null; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/utils/is-bot-login.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/utils/is-bot-login.ts deleted file mode 100644 index e5e031f3b1..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/utils/is-bot-login.ts +++ /dev/null @@ -1,30 +0,0 @@ -const BOT_LOGIN_SUFFIX = '[bot]'; - -const KNOWN_BOT_LOGINS = new Set([ - 'github-actions', - 'dependabot', - 'dependabot-preview', - 'renovate', - 'renovate-bot', - 'cubic-dev-ai', - 'greptile-apps', - 'sentry', - 'sentry-io', - 'codecov', - 'codecov-commenter', - 'snyk-bot', - 'mergify', - 'allcontributors', - 'imgbot', - 'pre-commit-ci', - 'semantic-release-bot', - 'stale', - 'web-flow', -]); - -export function isBotLogin(login: string | null | undefined): boolean { - if (!login) return false; - const lower = login.toLowerCase(); - if (lower.endsWith(BOT_LOGIN_SUFFIX)) return true; - return KNOWN_BOT_LOGINS.has(lower); -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/views/all-contributors.view.ts b/packages/twenty-apps/community/github-connector/src/modules/github/contributor/views/all-contributors.view.ts deleted file mode 100644 index 6346c1adad..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/contributor/views/all-contributors.view.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - defineView, - ViewKey, - ViewSortDirection, - ViewType, -} from 'twenty-sdk/define'; -import { - CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - CONTRIBUTOR_NAME_FIELD_UNIVERSAL_IDENTIFIER, - CONTRIBUTOR_GH_LOGIN_FIELD_UNIVERSAL_IDENTIFIER, - CONTRIBUTOR_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, -} from 'src/modules/github/contributor/objects/contributor.object'; - -export const ALL_CONTRIBUTORS_VIEW_UNIVERSAL_IDENTIFIER = - 'e2b51f8e-97ea-49e0-b38d-a69d2191236f'; - -export default defineView({ - universalIdentifier: ALL_CONTRIBUTORS_VIEW_UNIVERSAL_IDENTIFIER, - name: 'All Contributors', - objectUniversalIdentifier: CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - type: ViewType.TABLE, - icon: 'IconUsers', - key: ViewKey.INDEX, - position: 0, - fields: [ - { - universalIdentifier: 'b53cb9dd-0e2e-415e-81af-80bedbed89bc', - fieldMetadataUniversalIdentifier: - CONTRIBUTOR_NAME_FIELD_UNIVERSAL_IDENTIFIER, - position: 0, - isVisible: true, - size: 200, - }, - { - universalIdentifier: '604a2f1d-ef20-4821-8742-fe3b5b83ffcf', - fieldMetadataUniversalIdentifier: - CONTRIBUTOR_GH_LOGIN_FIELD_UNIVERSAL_IDENTIFIER, - position: 1, - isVisible: true, - size: 150, - }, - ], - sorts: [ - { - universalIdentifier: 'c0c54264-8de9-418b-b91a-cc4a2b94c539', - fieldMetadataUniversalIdentifier: - CONTRIBUTOR_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - direction: ViewSortDirection.DESC, - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/command-menu-items/fetch-issues.command-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/command-menu-items/fetch-issues.command-menu-item.ts deleted file mode 100644 index 2eb31aa011..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/command-menu-items/fetch-issues.command-menu-item.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define'; - -export default defineCommandMenuItem({ - universalIdentifier: 'c34f56aa-ff65-43a4-9db2-774945dbcc53', - frontComponentUniversalIdentifier: '9430e4fc-9ecb-428d-9bde-2babeb1f452f', - label: 'Fetch Issues', - icon: 'IconBug', - isPinned: false, - conditionalAvailabilityExpression: - objectMetadataItem.nameSingular === 'issue', -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/fields/author-on-issue.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/fields/author-on-issue.field.ts deleted file mode 100644 index 9df18c1ba1..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/fields/author-on-issue.field.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; -import { ISSUE_UNIVERSAL_IDENTIFIER } from 'src/modules/github/issue/objects/issue.object'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { AUTHORED_ISSUES_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/fields/authored-issues-on-contributor.field'; - -export const ISSUE_AUTHOR_FIELD_UNIVERSAL_IDENTIFIER = - '5ff7ab7f-bcfa-4e7a-9a33-0860ec692736'; - -export default defineField({ - universalIdentifier: ISSUE_AUTHOR_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: ISSUE_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'author', - label: 'Author', - icon: 'IconUser', - relationTargetObjectMetadataUniversalIdentifier: - CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - AUTHORED_ISSUES_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: OnDeleteAction.SET_NULL, - joinColumnName: 'authorId', - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/fields/project-items-on-issue.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/fields/project-items-on-issue.field.ts deleted file mode 100644 index b7713df535..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/fields/project-items-on-issue.field.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; -import { ISSUE_UNIVERSAL_IDENTIFIER } from 'src/modules/github/issue/objects/issue.object'; -import { PROJECT_ITEM_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/objects/project-item.object'; -import { LINKED_ISSUE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/fields/linked-issue-on-project-item.field'; - -export const PROJECT_ITEMS_ON_ISSUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e'; - -export default defineField({ - universalIdentifier: PROJECT_ITEMS_ON_ISSUE_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: ISSUE_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'projectItems', - label: 'Project Items', - icon: 'IconLayoutKanban', - relationTargetObjectMetadataUniversalIdentifier: - PROJECT_ITEM_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - LINKED_ISSUE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/front-components/fetch-issues.front-component.tsx b/packages/twenty-apps/community/github-connector/src/modules/github/issue/front-components/fetch-issues.front-component.tsx deleted file mode 100644 index afab8218fe..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/front-components/fetch-issues.front-component.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { useEffect, useState } from 'react'; -import { defineFrontComponent } from 'twenty-sdk/define'; -import { - enqueueSnackbar, - unmountFrontComponent, - updateProgress, -} from 'twenty-sdk/front-component'; -import { callAppRoute } from 'src/modules/shared/call-app-route'; -import { retry } from 'src/modules/shared/retry'; - -type CountResponse = { - totalPages: number; - repos: Array<{ owner: string; repo: string; totalCount: number; pages: number }>; -}; - -type FetchPageResponse = { - issueCount: number; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}; - -type SyncStatus = 'syncing' | 'done' | 'error'; - -const FetchIssues = () => { - const [status, setStatus] = useState('syncing'); - - useEffect(() => { - const run = async () => { - try { - const counts = (await callAppRoute( - '/github/count-issues', - {}, - )) as CountResponse; - - if (counts.repos.length === 0) { - throw new Error( - 'No repos resolved. Set GITHUB_REPOS in the application variables.', - ); - } - - const totalPages = Math.max(counts.totalPages, 1); - let pagesProcessed = 0; - let totalIssues = 0; - - for (const { owner, repo } of counts.repos) { - let cursor: string | null = null; - let hasMore = true; - - while (hasMore) { - const cursorTag = cursor ? `@${cursor.slice(0, 8)}` : ''; - const data = (await retry( - `fetch-issues ${owner}/${repo}${cursorTag}`, - () => - callAppRoute('/github/fetch-issues', { - owner, - repo, - cursor, - }), - )) as FetchPageResponse; - - totalIssues += data.issueCount; - hasMore = data.hasMore && data.issueCount > 0; - cursor = data.endCursor; - pagesProcessed++; - - updateProgress(Math.min(Math.round((pagesProcessed / totalPages) * 100), 99)); - } - } - - updateProgress(100); - enqueueSnackbar({ - message: `Fetched ${totalIssues} issues`, - variant: 'success', - }); - setStatus('done'); - } catch (err) { - const message = - err instanceof Error ? err.message : 'Failed to fetch issues'; - enqueueSnackbar({ message, variant: 'error' }); - setStatus('error'); - } finally { - unmountFrontComponent(); - } - }; - run(); - }, []); - - if (status === 'syncing') return
Fetching issues...
; - if (status === 'error') return
Failed to fetch issues.
; - return
Done
; -}; - -export default defineFrontComponent({ - universalIdentifier: '9430e4fc-9ecb-428d-9bde-2babeb1f452f', - name: 'Fetch Issues', - description: 'Fetches issues from GitHub repos', - isHeadless: true, - component: FetchIssues, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/github/count-issues.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/github/count-issues.ts deleted file mode 100644 index b78777210c..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/github/count-issues.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { githubGraphql } from 'src/modules/github/connector/github-client'; - -const QUERY = ` -query($owner: String!, $name: String!) { - repository(owner: $owner, name: $name) { - issues(states: [OPEN, CLOSED]) { - totalCount - } - } -}`; - -type Response = { - repository: { issues: { totalCount: number } } | null; -}; - -export async function countIssues( - owner: string, - name: string, -): Promise { - try { - const data = await githubGraphql(QUERY, { owner, name }); - return data.repository?.issues.totalCount ?? 0; - } catch { - return 0; - } -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/github/fetch-issues.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/github/fetch-issues.ts deleted file mode 100644 index 2b9df95a78..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/github/fetch-issues.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - EMPTY_PAGE, - type GithubPage, - githubGraphql, -} from 'src/modules/github/connector/github-client'; - -const QUERY = ` -query($owner: String!, $name: String!, $cursor: String) { - repository(owner: $owner, name: $name) { - issues(first: 100, states: [OPEN, CLOSED], orderBy: {field: CREATED_AT, direction: DESC}, after: $cursor) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - number - title - url - state - createdAt - closedAt - author { login avatarUrl ... on User { databaseId } } - labels(first: 50) { nodes { name } } - } - } - } -}`; - -export type GqlIssue = { - number: number; - title: string; - url: string; - state: 'OPEN' | 'CLOSED'; - createdAt: string; - closedAt: string | null; - author: { login: string; avatarUrl?: string | null; databaseId?: number } | null; - labels: { nodes: Array<{ name: string }> }; -}; - -type Response = { - repository: { issues: GithubPage } | null; -}; - -export async function fetchIssues( - owner: string, - name: string, - cursor: string | null = null, -): Promise<{ - issues: GqlIssue[]; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}> { - try { - const data = await githubGraphql(QUERY, { owner, name, cursor }); - const conn = data.repository?.issues; - if (!conn) return { issues: [], ...EMPTY_PAGE }; - - return { - issues: conn.nodes, - totalCount: conn.totalCount, - hasMore: conn.pageInfo.hasNextPage, - endCursor: conn.pageInfo.endCursor, - }; - } catch { - return { issues: [], ...EMPTY_PAGE }; - } -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/mutations/batch-upsert.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/mutations/batch-upsert.ts deleted file mode 100644 index 2a2995e0f2..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/mutations/batch-upsert.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { chunkedBatchCreate } from 'src/modules/shared/twenty-client'; -import type { LinksFieldValue } from 'src/modules/shared/types'; -import type { IssueRow } from 'src/modules/github/issue/types/issue-row'; - -export async function batchUpsertIssues( - items: Array<{ - title: string; - githubNumber: number; - uniqueIdentifier: string; - githubUrl: LinksFieldValue; - state: string; - labels: string[]; - githubCreatedAt: string | null; - closedAt: string | null; - repo: string; - authorId: string | null; - }>, -): Promise { - return chunkedBatchCreate('createIssues', items, { - id: true, - githubNumber: true, - uniqueIdentifier: true, - title: true, - state: true, - repo: true, - }) as Promise; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/queries/find-by-number-and-repo.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/queries/find-by-number-and-repo.ts deleted file mode 100644 index 5f7d7681b9..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/graphql/queries/find-by-number-and-repo.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getClient } from 'src/modules/shared/twenty-client'; -import type { IssueRow } from 'src/modules/github/issue/types/issue-row'; - -export async function findIssueByNumberAndRepo( - githubNumber: number, - repo: string, -): Promise { - const client = getClient(); - const res = await client.query({ - issues: { - __args: { - filter: { - and: [ - { githubNumber: { eq: githubNumber } }, - { repo: { eq: repo } }, - ], - }, - first: 1, - }, - edges: { - node: { - id: true, - title: true, - githubNumber: true, - uniqueIdentifier: true, - githubUrl: { primaryLinkLabel: true, primaryLinkUrl: true }, - state: true, - labels: true, - githubCreatedAt: true, - closedAt: true, - repo: true, - authorId: true, - }, - }, - }, - }); - - const node = res.issues?.edges?.[0]?.node; - return (node as IssueRow | undefined) ?? null; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/logic-functions/count-issues.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/logic-functions/count-issues.logic-function.ts deleted file mode 100644 index 90d264aae2..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/logic-functions/count-issues.logic-function.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { countAcrossRepos } from 'src/modules/github/connector/count-across-repos'; -import { countIssues } from 'src/modules/github/issue/graphql/github/count-issues'; - -type CountIssuesPayload = { - repos?: string[]; -}; - -const handler = async (event: RoutePayload) => - countAcrossRepos(event.body?.repos, countIssues, 'count-issues'); - -export default defineLogicFunction({ - universalIdentifier: 'd8cc32bf-6be9-44fc-920a-8bba510f045f', - name: 'count-issues', - description: - 'Counts total issue pages across configured repos using GraphQL totalCount', - timeoutSeconds: 30, - handler, - httpRouteTriggerSettings: { - path: '/github/count-issues', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/logic-functions/fetch-issues.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/logic-functions/fetch-issues.logic-function.ts deleted file mode 100644 index 306cee67ea..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/logic-functions/fetch-issues.logic-function.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { - fetchIssues, - type GqlIssue, -} from 'src/modules/github/issue/graphql/github/fetch-issues'; -import { batchUpsertContributors } from 'src/modules/github/contributor/graphql/mutations/batch-upsert'; -import { batchUpsertIssues } from 'src/modules/github/issue/graphql/mutations/batch-upsert'; -import { dedupeContributors } from 'src/modules/github/contributor/normalizers'; -import { issueFromGraphql } from 'src/modules/github/issue/normalizers'; -import { timed } from 'src/modules/shared/timing'; -import { isFixtureAllowed } from 'src/modules/shared/fixtures'; - -export type FetchIssuesFixturePage = { - issues: GqlIssue[]; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}; - -type FetchIssuesPayload = { - owner: string; - repo: string; - cursor?: string | null; - fixturePage?: FetchIssuesFixturePage; -}; - -const handler = async (event: RoutePayload) => { - const handlerStart = Date.now(); - const { owner, repo, cursor = null, fixturePage } = event.body ?? {}; - if (!owner || !repo) { - return { error: 'owner and repo are required' }; - } - - const tag = `${owner}/${repo}${cursor ? `@${cursor.slice(0, 8)}` : ''}`; - console.log(`[fetch-issues] start ${tag}${fixturePage ? ' (fixture)' : ''}`); - - const result = - fixturePage && isFixtureAllowed() - ? fixturePage - : await timed(`fetch-issues:github ${tag}`, () => - fetchIssues(owner, repo, cursor), - ); - const { issues, totalCount, hasMore, endCursor } = result; - - if (issues.length === 0) { - console.log(`[fetch-issues] empty page for ${tag}`); - return { issueCount: 0, totalCount, hasMore: false, endCursor: null }; - } - - const fullRepo = `${owner}/${repo}`; - - const contributorInputs = dedupeContributors(issues.map((i) => i.author)); - const contributors = await timed( - `fetch-issues:upsertContributors ${tag} (${contributorInputs.length})`, - () => batchUpsertContributors(contributorInputs), - ); - - const idByLogin = new Map(); - for (const c of contributors) { - if (c.ghLogin) idByLogin.set(c.ghLogin, c.id); - } - - const issueData = issues.map((issue) => ({ - ...issueFromGraphql(issue, fullRepo), - authorId: issue.author ? (idByLogin.get(issue.author.login) ?? null) : null, - })); - - await timed(`fetch-issues:upsertIssues ${tag} (${issueData.length})`, () => - batchUpsertIssues(issueData), - ); - - const totalMs = Date.now() - handlerStart; - console.log( - `[fetch-issues] done ${tag} in ${totalMs}ms (issues=${issues.length}, hasMore=${hasMore})`, - ); - - return { issueCount: issues.length, totalCount, hasMore, endCursor }; -}; - -export default defineLogicFunction({ - universalIdentifier: 'b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e', - name: 'fetch-issues', - description: - 'Fetches one page of issues via GitHub GraphQL API and batch upserts them', - timeoutSeconds: 300, - handler, - httpRouteTriggerSettings: { - path: '/github/fetch-issues', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/navigation-menu-items/issues.navigation-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/navigation-menu-items/issues.navigation-menu-item.ts deleted file mode 100644 index ed4c89df6c..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/navigation-menu-items/issues.navigation-menu-item.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define'; -import { ISSUE_UNIVERSAL_IDENTIFIER } from 'src/modules/github/issue/objects/issue.object'; -import { GITHUB_FOLDER_UNIVERSAL_IDENTIFIER } from 'src/modules/github/navigation-menu-items/github-folder.navigation-menu-item'; - -export default defineNavigationMenuItem({ - universalIdentifier: '7c4f8e1a-3b9d-4f2e-8a6c-1d5b7e9f3a2c', - position: 1, - type: NavigationMenuItemType.OBJECT, - targetObjectUniversalIdentifier: ISSUE_UNIVERSAL_IDENTIFIER, - folderUniversalIdentifier: GITHUB_FOLDER_UNIVERSAL_IDENTIFIER, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/normalizers.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/normalizers.ts deleted file mode 100644 index b4045d9b16..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/normalizers.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { LinksFieldValue } from 'src/modules/shared/types'; -import { toLinksField } from 'src/modules/shared/types'; -import type { GitHubIssue } from 'src/modules/github/issue/types/github-issue'; -import type { GqlIssue } from 'src/modules/github/issue/graphql/github/fetch-issues'; - -export type IssueState = 'OPEN' | 'CLOSED'; - -export type IssueCanonical = { - title: string; - githubNumber: number; - uniqueIdentifier: string; - githubUrl: LinksFieldValue; - state: IssueState; - labels: string[]; - githubCreatedAt: string | null; - closedAt: string | null; - repo: string; -}; - -export function deriveIssueState(state: string): IssueState { - return state.toUpperCase() === 'CLOSED' ? 'CLOSED' : 'OPEN'; -} - -export function buildIssueUniqueIdentifier( - repoFullName: string, - number: number, -): string { - return `${repoFullName}#${number}`; -} - -export function issueFromWebhook( - issue: GitHubIssue, - repoFullName: string, -): IssueCanonical { - return { - title: issue.title, - githubNumber: issue.number, - uniqueIdentifier: buildIssueUniqueIdentifier(repoFullName, issue.number), - githubUrl: toLinksField(issue.html_url, issue.title), - state: deriveIssueState(issue.state), - labels: issue.labels.map((l) => l.name), - githubCreatedAt: issue.created_at, - closedAt: issue.closed_at, - repo: repoFullName, - }; -} - -export function issueFromGraphql( - issue: GqlIssue, - repoFullName: string, -): IssueCanonical { - return { - title: issue.title, - githubNumber: issue.number, - uniqueIdentifier: buildIssueUniqueIdentifier(repoFullName, issue.number), - githubUrl: toLinksField(issue.url, issue.title), - state: deriveIssueState(issue.state), - labels: issue.labels.nodes.map((l) => l.name), - githubCreatedAt: issue.createdAt, - closedAt: issue.closedAt, - repo: repoFullName, - }; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/objects/issue.object.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/objects/issue.object.ts deleted file mode 100644 index 64fa230202..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/objects/issue.object.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { defineObject, FieldType } from 'twenty-sdk/define'; - -export const ISSUE_UNIVERSAL_IDENTIFIER = - '3a4b5c6d-7e8f-4a9b-8c0d-1e2f3a4b5c6d'; - -export const ISSUE_TITLE_FIELD_UNIVERSAL_IDENTIFIER = - '4b5c6d7e-8f9a-4b0c-9d1e-2f3a4b5c6d7e'; - -export const ISSUE_GITHUB_NUMBER_FIELD_UNIVERSAL_IDENTIFIER = - '5c6d7e8f-9a0b-4c1d-ae2f-3a4b5c6d7e8f'; - -export const ISSUE_GITHUB_URL_FIELD_UNIVERSAL_IDENTIFIER = - '6d7e8f9a-0b1c-4d2e-bf3a-4b5c6d7e8f9a'; - -export const ISSUE_LABELS_FIELD_UNIVERSAL_IDENTIFIER = - 'dfd35d92-f4a3-44c5-b758-494c0882fcc8'; - -export const ISSUE_GITHUB_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER = - '76364df9-6f5c-477d-bd99-97b6738df3bf'; - -export const ISSUE_CLOSED_AT_FIELD_UNIVERSAL_IDENTIFIER = - '48ac555b-73b7-4ce9-a264-251c8e06b4e9'; - -export const ISSUE_REPO_FIELD_UNIVERSAL_IDENTIFIER = - '79c41cdc-912a-4e4d-b9de-3f2421bcb2fb'; - -export const ISSUE_UNIQUE_IDENTIFIER_FIELD_UNIVERSAL_IDENTIFIER = - '0ca04207-4d74-4298-8238-59cdf685862b'; - -enum IssueState { - OPEN = 'OPEN', - CLOSED = 'CLOSED', -} - -export const ISSUE_STATE_FIELD_UNIVERSAL_IDENTIFIER = - '1c2d3e4f-5a6b-4c7d-a48e-9f0a1b2c3d4e'; - -export const ISSUE_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER = - '440e9ba8-20d1-5843-91ef-3b7b699a59f4'; - -export default defineObject({ - universalIdentifier: ISSUE_UNIVERSAL_IDENTIFIER, - nameSingular: 'issue', - namePlural: 'issues', - labelSingular: 'Issue', - labelPlural: 'Issues', - icon: 'IconBug', - labelIdentifierFieldMetadataUniversalIdentifier: - ISSUE_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - fields: [ - { - universalIdentifier: ISSUE_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'title', - type: FieldType.TEXT, - label: 'Title', - icon: 'IconTextCaption', - }, - { - universalIdentifier: ISSUE_GITHUB_NUMBER_FIELD_UNIVERSAL_IDENTIFIER, - name: 'githubNumber', - type: FieldType.NUMBER, - label: 'Issue Number', - icon: 'IconHash', - }, - { - universalIdentifier: ISSUE_GITHUB_URL_FIELD_UNIVERSAL_IDENTIFIER, - name: 'githubUrl', - type: FieldType.LINKS, - label: 'GitHub URL', - icon: 'IconLink', - }, - { - universalIdentifier: ISSUE_STATE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'state', - type: FieldType.SELECT, - label: 'State', - icon: 'IconCircleDot', - options: [ - { - value: IssueState.OPEN, - label: 'Open', - position: 0, - color: 'green', - }, - { - value: IssueState.CLOSED, - label: 'Closed', - position: 1, - color: 'red', - }, - ], - }, - { - universalIdentifier: ISSUE_LABELS_FIELD_UNIVERSAL_IDENTIFIER, - name: 'labels', - type: FieldType.ARRAY, - label: 'Labels', - icon: 'IconTag', - }, - { - universalIdentifier: ISSUE_GITHUB_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - name: 'githubCreatedAt', - type: FieldType.DATE_TIME, - label: 'GitHub Created At', - icon: 'IconCalendarPlus', - isNullable: true, - defaultValue: null, - }, - { - universalIdentifier: ISSUE_CLOSED_AT_FIELD_UNIVERSAL_IDENTIFIER, - name: 'closedAt', - type: FieldType.DATE_TIME, - label: 'Closed At', - icon: 'IconCalendarOff', - isNullable: true, - defaultValue: null, - }, - { - universalIdentifier: ISSUE_REPO_FIELD_UNIVERSAL_IDENTIFIER, - name: 'repo', - type: FieldType.TEXT, - label: 'Repository', - icon: 'IconFolder', - }, - { - universalIdentifier: ISSUE_UNIQUE_IDENTIFIER_FIELD_UNIVERSAL_IDENTIFIER, - name: 'uniqueIdentifier', - type: FieldType.TEXT, - label: 'Unique Identifier', - icon: 'IconFingerprint', - isUnique: true, - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/types/github-issue.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/types/github-issue.ts deleted file mode 100644 index 57619c0cbb..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/types/github-issue.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { GitHubUser } from 'src/modules/github/connector/github-user'; - -export type GitHubIssue = { - number: number; - title: string; - html_url: string; - state: 'open' | 'closed'; - labels: Array<{ name: string }>; - created_at: string; - closed_at: string | null; - user: GitHubUser; - pull_request?: unknown; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/types/issue-row.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/types/issue-row.ts deleted file mode 100644 index 32df42ca55..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/types/issue-row.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { LinksFieldValue } from 'src/modules/shared/types'; - -export type IssueRow = { - id: string; - title?: string | null; - githubNumber?: number | null; - uniqueIdentifier?: string | null; - githubUrl?: LinksFieldValue | null; - state?: string | null; - labels?: string[] | null; - githubCreatedAt?: string | null; - closedAt?: string | null; - repo?: string | null; - authorId?: string | null; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/issue/views/all-issues.view.ts b/packages/twenty-apps/community/github-connector/src/modules/github/issue/views/all-issues.view.ts deleted file mode 100644 index ea429bda16..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/issue/views/all-issues.view.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - defineView, - ViewKey, - ViewSortDirection, - ViewType, -} from 'twenty-sdk/define'; -import { - ISSUE_UNIVERSAL_IDENTIFIER, - ISSUE_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - ISSUE_GITHUB_NUMBER_FIELD_UNIVERSAL_IDENTIFIER, - ISSUE_STATE_FIELD_UNIVERSAL_IDENTIFIER, - ISSUE_LABELS_FIELD_UNIVERSAL_IDENTIFIER, - ISSUE_REPO_FIELD_UNIVERSAL_IDENTIFIER, - ISSUE_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, -} from 'src/modules/github/issue/objects/issue.object'; -import { ISSUE_AUTHOR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/issue/fields/author-on-issue.field'; - -export const ALL_ISSUES_VIEW_UNIVERSAL_IDENTIFIER = - 'b2c3d4e5-1111-4f6a-b7c8-d9e0f1a2b3c4'; - -export default defineView({ - universalIdentifier: ALL_ISSUES_VIEW_UNIVERSAL_IDENTIFIER, - name: 'All Issues', - objectUniversalIdentifier: ISSUE_UNIVERSAL_IDENTIFIER, - type: ViewType.TABLE, - icon: 'IconBug', - key: ViewKey.INDEX, - position: 0, - fields: [ - { - universalIdentifier: '5b115bd1-489c-44e5-ac1b-7d508e06f165', - fieldMetadataUniversalIdentifier: - ISSUE_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - position: 0, - isVisible: true, - size: 300, - }, - { - universalIdentifier: '77ac1882-3a13-43e3-822e-c9f5f7d83a67', - fieldMetadataUniversalIdentifier: - ISSUE_GITHUB_NUMBER_FIELD_UNIVERSAL_IDENTIFIER, - position: 1, - isVisible: true, - size: 100, - }, - { - universalIdentifier: '4f0d17f2-c3a6-4431-a928-3812273592e2', - fieldMetadataUniversalIdentifier: - ISSUE_STATE_FIELD_UNIVERSAL_IDENTIFIER, - position: 2, - isVisible: true, - size: 100, - }, - { - universalIdentifier: '01673163-ccfa-4370-a1c3-48c33b6c4aa3', - fieldMetadataUniversalIdentifier: - ISSUE_LABELS_FIELD_UNIVERSAL_IDENTIFIER, - position: 3, - isVisible: true, - size: 200, - }, - { - universalIdentifier: '6d932c97-8f92-4d93-968f-29ca34b27ac0', - fieldMetadataUniversalIdentifier: - ISSUE_REPO_FIELD_UNIVERSAL_IDENTIFIER, - position: 4, - isVisible: true, - size: 180, - }, - { - universalIdentifier: 'dc293fd6-747c-4291-9426-5d7b53234936', - fieldMetadataUniversalIdentifier: - ISSUE_AUTHOR_FIELD_UNIVERSAL_IDENTIFIER, - position: 5, - isVisible: true, - size: 150, - }, - ], - sorts: [ - { - universalIdentifier: '8e972333-b919-4c1b-ad7b-4fab8f018253', - fieldMetadataUniversalIdentifier: - ISSUE_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - direction: ViewSortDirection.DESC, - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/logic-functions/handle-webhook.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/logic-functions/handle-webhook.logic-function.ts deleted file mode 100644 index fa2701169e..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/logic-functions/handle-webhook.logic-function.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import type { GitHubWebhookPayload } from 'src/modules/github/connector/webhook-payload'; -import type { ProjectV2Item } from 'src/modules/github/project-item/types/project-v2-item'; -import { fetchProjectItemByNodeId } from 'src/modules/github/project-item/graphql/github/fetch-project-item-by-node-id'; -import { - getRawBodyForSignature, - verifyGitHubSignature, -} from 'src/modules/github/connector/webhook-signature'; -import { batchUpsertContributors } from 'src/modules/github/contributor/graphql/mutations/batch-upsert'; -import { batchUpsertPullRequests } from 'src/modules/github/pull-request/graphql/mutations/batch-upsert'; -import { batchUpsertReviewEvents } from 'src/modules/github/pull-request-review-event/graphql/mutations/batch-upsert'; -import { batchUpsertConsolidatedReviews } from 'src/modules/github/pull-request-review/graphql/mutations/batch-upsert'; -import { findReviewEventsForPair } from 'src/modules/github/pull-request-review-event/graphql/queries/find-events-for-pair'; -import { buildConsolidatedRow } from 'src/modules/github/pull-request-review/utils/build-consolidated-row'; -import type { ReviewEventState } from 'src/modules/github/pull-request-review/utils/consolidate-reviews'; -import { batchUpsertIssues } from 'src/modules/github/issue/graphql/mutations/batch-upsert'; -import { batchUpsertProjectItems } from 'src/modules/github/project-item/graphql/mutations/batch-upsert'; -import { dedupeContributors } from 'src/modules/github/contributor/normalizers'; -import { pullRequestFromWebhook } from 'src/modules/github/pull-request/normalizers'; -import { reviewEventFromWebhook } from 'src/modules/github/pull-request-review-event/normalizers'; -import { issueFromWebhook } from 'src/modules/github/issue/normalizers'; -import { projectItemFromGraphql } from 'src/modules/github/project-item/normalizers'; - -async function upsertContributorsByLogin( - users: Array<{ login: string; id?: number } | null | undefined>, -): Promise> { - const inputs = dedupeContributors(users); - if (inputs.length === 0) return new Map(); - const rows = await batchUpsertContributors(inputs); - const map = new Map(); - for (const r of rows) { - if (r.ghLogin) map.set(r.ghLogin, r.id); - } - return map; -} - -async function handlePullRequestEvent(payload: GitHubWebhookPayload) { - const pr = payload.pull_request; - const repository = payload.repository; - if (!pr || !repository) { - return { skipped: true, reason: 'missing pull_request or repository' }; - } - - const idByLogin = await upsertContributorsByLogin([pr.user, pr.merged_by]); - const authorId = idByLogin.get(pr.user.login) ?? null; - const mergerId = pr.merged_by - ? (idByLogin.get(pr.merged_by.login) ?? null) - : null; - - const canonical = pullRequestFromWebhook(pr, repository.full_name); - - const [record] = await batchUpsertPullRequests([ - { ...canonical, authorId, mergerId }, - ]); - - return { - processed: true, - pullRequestId: record?.id, - action: payload.action, - state: canonical.state, - }; -} - -async function handlePullRequestReviewEvent(payload: GitHubWebhookPayload) { - const review = payload.review; - const pr = payload.pull_request; - const repository = payload.repository; - if (!review || !pr || !repository) { - return { - skipped: true, - reason: 'missing review, pull_request, or repository', - }; - } - - const idByLogin = await upsertContributorsByLogin([ - pr.user, - pr.merged_by, - review.user, - ]); - - const prCanonical = pullRequestFromWebhook(pr, repository.full_name); - const [prRecord] = await batchUpsertPullRequests([ - { - ...prCanonical, - authorId: idByLogin.get(pr.user.login) ?? null, - mergerId: pr.merged_by - ? (idByLogin.get(pr.merged_by.login) ?? null) - : null, - }, - ]); - - const reviewCanonical = reviewEventFromWebhook(review); - const reviewerId = idByLogin.get(review.user.login) ?? null; - const pullRequestId = prRecord?.id ?? ''; - - const [eventRecord] = await batchUpsertReviewEvents([ - { - ...reviewCanonical, - reviewerId, - pullRequestId, - }, - ]); - - let consolidatedId: string | undefined; - if (pullRequestId) { - const events = await findReviewEventsForPair(pullRequestId, reviewerId); - if (events.length > 0) { - const consolidatedRow = buildConsolidatedRow({ - pullRequestId, - reviewerId, - prNumber: pr.number ?? null, - reviewerLogin: review.user.login, - events: events.map((e) => ({ - state: e.state as ReviewEventState, - submittedAt: e.submittedAt, - })), - }); - const [consolidatedRecord] = await batchUpsertConsolidatedReviews([ - consolidatedRow, - ]); - consolidatedId = consolidatedRecord?.id; - - if (consolidatedId && eventRecord?.id) { - await batchUpsertReviewEvents([ - { - ...reviewCanonical, - reviewerId, - pullRequestId, - reviewId: consolidatedId, - }, - ]); - } - } - } - - return { - processed: true, - reviewEventId: eventRecord?.id, - reviewId: consolidatedId, - pullRequestId: prRecord?.id, - action: payload.action, - }; -} - -async function handleIssueEvent(payload: GitHubWebhookPayload) { - const issue = payload.issue; - const repository = payload.repository; - if (!issue || !repository) { - return { skipped: true, reason: 'missing issue or repository' }; - } - - const idByLogin = await upsertContributorsByLogin([issue.user]); - const canonical = issueFromWebhook(issue, repository.full_name); - - const [record] = await batchUpsertIssues([ - { ...canonical, authorId: idByLogin.get(issue.user.login) ?? null }, - ]); - - return { - processed: true, - issueId: record?.id, - action: payload.action, - }; -} - -async function handleProjectV2ItemEvent( - payload: GitHubWebhookPayload, - testProjectItem?: ProjectV2Item, -) { - const item = payload.projects_v2_item; - if (!item?.node_id) { - return { skipped: true, reason: 'missing projects_v2_item.node_id' }; - } - - if (payload.action === 'deleted') { - console.log( - `[handle-webhook] projects_v2_item delete skipped (nodeId=${item.node_id})`, - ); - return { skipped: true, reason: 'delete not implemented' }; - } - - const node = - testProjectItem ?? (await fetchProjectItemByNodeId(item.node_id)); - if (!node) { - return { skipped: true, reason: 'project item not found on GitHub' }; - } - - const canonical = await projectItemFromGraphql(node); - const [record] = await batchUpsertProjectItems([canonical]); - - return { - processed: true, - projectItemId: record?.id, - action: payload.action, - }; -} - -const handler = async ( - event: RoutePayload< - GitHubWebhookPayload & { __test_projectItem?: ProjectV2Item } - >, -) => { - const secret = process.env.GITHUB_WEBHOOK_SECRET; - if (secret) { - const signatureHeader = - event.headers?.['x-hub-signature-256'] ?? - event.headers?.['X-Hub-Signature-256']; - const rawBody = getRawBodyForSignature(event); - const verification = verifyGitHubSignature({ - rawBody, - signatureHeader, - secret, - }); - if (!verification.ok) { - const delivery = - event.headers?.['x-github-delivery'] ?? - event.headers?.['X-GitHub-Delivery']; - console.warn( - `[handle-webhook] signature verification failed (${verification.reason}) delivery=${delivery ?? 'unknown'}`, - ); - return { error: 'invalid signature', reason: verification.reason }; - } - } else { - console.warn( - '[handle-webhook] GITHUB_WEBHOOK_SECRET is not set; skipping signature verification (insecure)', - ); - } - - const payload = event.body; - if (!payload) return { error: 'empty body' }; - - if (payload.pull_request && payload.review) { - return handlePullRequestReviewEvent(payload); - } - if (payload.pull_request) { - return handlePullRequestEvent(payload); - } - if (payload.issue) { - return handleIssueEvent(payload); - } - if (payload.projects_v2_item) { - return handleProjectV2ItemEvent(payload, payload.__test_projectItem); - } - - return { skipped: true, reason: 'unhandled event' }; -}; - -export default defineLogicFunction({ - universalIdentifier: '22b199b3-2851-4a4f-99fd-4e79c188fe7d', - name: 'handle-github-webhook', - description: - 'Receives GitHub webhook events for PRs, reviews, issues, and project items; idempotent upserts via shared batch helpers', - timeoutSeconds: 30, - handler, - httpRouteTriggerSettings: { - path: '/github/webhook', - httpMethod: 'POST', - isAuthRequired: false, - forwardedRequestHeaders: [ - 'x-hub-signature-256', - 'x-github-event', - 'x-github-delivery', - ], - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/contributors.navigation-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/contributors.navigation-menu-item.ts deleted file mode 100644 index 437638a82c..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/contributors.navigation-menu-item.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { - defineNavigationMenuItem, - NavigationMenuItemType, -} from 'twenty-sdk/define'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { GITHUB_FOLDER_UNIVERSAL_IDENTIFIER } from 'src/modules/github/navigation-menu-items/github-folder.navigation-menu-item'; - -export default defineNavigationMenuItem({ - universalIdentifier: 'd1c2b3a4-9e8f-4d3c-b2a1-0f9e8d7c6b5a', - position: 3, - type: NavigationMenuItemType.OBJECT, - targetObjectUniversalIdentifier: CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - folderUniversalIdentifier: GITHUB_FOLDER_UNIVERSAL_IDENTIFIER, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/github-dashboard.navigation-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/github-dashboard.navigation-menu-item.ts deleted file mode 100644 index 9a2db40645..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/github-dashboard.navigation-menu-item.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - defineNavigationMenuItem, - NavigationMenuItemType, -} from 'twenty-sdk/define'; -import { GITHUB_DASHBOARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER } from 'src/modules/github/page-layouts/github-dashboard.page-layout'; -import { GITHUB_FOLDER_UNIVERSAL_IDENTIFIER } from 'src/modules/github/navigation-menu-items/github-folder.navigation-menu-item'; - -export default defineNavigationMenuItem({ - universalIdentifier: 'e8f9a1b2-c3d4-4567-89ab-cdef01234567', - name: 'GitHub Dashboard', - icon: 'IconBrandGithub', - position: 6, - type: NavigationMenuItemType.PAGE_LAYOUT, - pageLayoutUniversalIdentifier: - GITHUB_DASHBOARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER, - folderUniversalIdentifier: GITHUB_FOLDER_UNIVERSAL_IDENTIFIER, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/github-folder.navigation-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/github-folder.navigation-menu-item.ts deleted file mode 100644 index 16658a3eec..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/navigation-menu-items/github-folder.navigation-menu-item.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define'; - -export const GITHUB_FOLDER_UNIVERSAL_IDENTIFIER = - 'a4f7d8b1-2c93-4e6f-8b1a-9d3e5c7f2a48'; - -export default defineNavigationMenuItem({ - universalIdentifier: GITHUB_FOLDER_UNIVERSAL_IDENTIFIER, - name: 'GitHub', - icon: 'IconBrandGithub', - color: 'gray', - position: 0, - type: NavigationMenuItemType.FOLDER, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/page-layouts/github-dashboard.page-layout.ts b/packages/twenty-apps/community/github-connector/src/modules/github/page-layouts/github-dashboard.page-layout.ts deleted file mode 100644 index 70831ca6e5..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/page-layouts/github-dashboard.page-layout.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { - AggregateOperations, - definePageLayout, - ObjectRecordGroupByDateGranularity, - PageLayoutTabLayoutMode, -} from 'twenty-sdk/define'; -import { TOP_PR_AUTHORS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/front-components/top-pr-authors.front-component'; -import { TOP_REVIEWERS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/front-components/top-reviewers.front-component'; -import { - ISSUE_GITHUB_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - ISSUE_STATE_FIELD_UNIVERSAL_IDENTIFIER, - ISSUE_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - ISSUE_UNIVERSAL_IDENTIFIER, -} from 'src/modules/github/issue/objects/issue.object'; -import { - MERGED_AT_FIELD_UNIVERSAL_IDENTIFIER, - PR_GITHUB_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - PR_STATE_FIELD_UNIVERSAL_IDENTIFIER, - PULL_REQUEST_NAME_FIELD_UNIVERSAL_IDENTIFIER, - PULL_REQUEST_UNIVERSAL_IDENTIFIER, -} from 'src/modules/github/pull-request/objects/pull-request.object'; -import { - PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - REVIEW_FIRST_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - REVIEW_TITLE_FIELD_UNIVERSAL_IDENTIFIER, -} from 'src/modules/github/pull-request-review/objects/pull-request-review.object'; - -export const GITHUB_DASHBOARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = - 'a8d2f1c4-7b69-4e3a-8c5d-9f6b1a3e7c2d'; - -const THIS_WEEK_RELATIVE = 'THIS_1_WEEK;;UTC;;SUNDAY;;'; - -const COMMON = { - timezone: 'UTC', - firstDayOfTheWeek: 0, -}; - -const COL_1 = { column: 0, columnSpan: 3 } as const; -const COL_2 = { column: 3, columnSpan: 5 } as const; -const COL_3 = { column: 8, columnSpan: 4 } as const; - -export default definePageLayout({ - universalIdentifier: GITHUB_DASHBOARD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER, - name: 'GitHub Dashboard', - type: 'STANDALONE_PAGE', - tabs: [ - { - universalIdentifier: 'b3e9c2a1-4d68-4f7b-9c25-1e3a5b7c9d11', - title: 'GitHub Dashboard', - position: 0, - icon: 'IconBrandGithub', - layoutMode: PageLayoutTabLayoutMode.GRID, - widgets: [ - { - universalIdentifier: 'f1a2b3c4-d5e6-4789-a0b1-c2d3e4f5a6b1', - title: 'PRs Merged This Week', - type: 'GRAPH', - objectUniversalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - gridPosition: { row: 0, ...COL_1, rowSpan: 3 }, - configuration: { - configurationType: 'AGGREGATE_CHART', - aggregateFieldMetadataUniversalIdentifier: - PULL_REQUEST_NAME_FIELD_UNIVERSAL_IDENTIFIER, - aggregateOperation: AggregateOperations.COUNT, - label: 'PRs Merged', - color: 'green', - displayDataLabel: false, - ...COMMON, - filter: { - recordFilters: [ - { - fieldMetadataUniversalIdentifier: - MERGED_AT_FIELD_UNIVERSAL_IDENTIFIER, - operand: 'IS_RELATIVE', - value: THIS_WEEK_RELATIVE, - type: 'DATE_TIME', - }, - ], - }, - }, - }, - { - universalIdentifier: 'f1a2b3c4-d5e6-4789-a0b1-c2d3e4f5a6b2', - title: 'PR Reviews This Week', - type: 'GRAPH', - objectUniversalIdentifier: PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - gridPosition: { row: 3, ...COL_1, rowSpan: 3 }, - configuration: { - configurationType: 'AGGREGATE_CHART', - aggregateFieldMetadataUniversalIdentifier: - REVIEW_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - aggregateOperation: AggregateOperations.COUNT, - label: 'Reviews', - color: 'blue', - displayDataLabel: false, - ...COMMON, - filter: { - recordFilters: [ - { - fieldMetadataUniversalIdentifier: - REVIEW_FIRST_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - operand: 'IS_RELATIVE', - value: THIS_WEEK_RELATIVE, - type: 'DATE_TIME', - }, - ], - }, - }, - }, - { - universalIdentifier: '8d4e9d09-29b0-4925-8fcc-aedf5bcecb37', - title: 'PRs Opened This Week', - type: 'GRAPH', - objectUniversalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - gridPosition: { row: 6, ...COL_1, rowSpan: 3 }, - configuration: { - configurationType: 'AGGREGATE_CHART', - aggregateFieldMetadataUniversalIdentifier: - PULL_REQUEST_NAME_FIELD_UNIVERSAL_IDENTIFIER, - aggregateOperation: AggregateOperations.COUNT, - label: 'PRs Opened', - color: 'purple', - displayDataLabel: false, - ...COMMON, - filter: { - recordFilters: [ - { - fieldMetadataUniversalIdentifier: - PR_GITHUB_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - operand: 'IS_RELATIVE', - value: THIS_WEEK_RELATIVE, - type: 'DATE_TIME', - }, - ], - }, - }, - }, - { - universalIdentifier: '3c321867-0227-4535-81db-c64a6f673d86', - title: 'Issues Opened This Week', - type: 'GRAPH', - objectUniversalIdentifier: ISSUE_UNIVERSAL_IDENTIFIER, - gridPosition: { row: 9, ...COL_1, rowSpan: 3 }, - configuration: { - configurationType: 'AGGREGATE_CHART', - aggregateFieldMetadataUniversalIdentifier: - ISSUE_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - aggregateOperation: AggregateOperations.COUNT, - label: 'Issues Opened', - color: 'orange', - displayDataLabel: false, - ...COMMON, - filter: { - recordFilters: [ - { - fieldMetadataUniversalIdentifier: - ISSUE_GITHUB_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - operand: 'IS_RELATIVE', - value: THIS_WEEK_RELATIVE, - type: 'DATE_TIME', - }, - ], - }, - }, - }, - { - universalIdentifier: '704209be-9174-43a8-ae51-6b11fff20e22', - title: 'PRs by State', - type: 'GRAPH', - objectUniversalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - gridPosition: { row: 12, ...COL_1, rowSpan: 6 }, - configuration: { - configurationType: 'PIE_CHART', - aggregateFieldMetadataUniversalIdentifier: - PULL_REQUEST_NAME_FIELD_UNIVERSAL_IDENTIFIER, - aggregateOperation: AggregateOperations.COUNT, - groupByFieldMetadataUniversalIdentifier: - PR_STATE_FIELD_UNIVERSAL_IDENTIFIER, - displayLegend: true, - showCenterMetric: true, - hideEmptyCategory: true, - ...COMMON, - }, - }, - { - universalIdentifier: 'c1f2a3b4-d5e6-4789-a0b1-c2d3e4f5a6b7', - title: 'PRs Merged per Week', - type: 'GRAPH', - objectUniversalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - gridPosition: { row: 0, ...COL_2, rowSpan: 6 }, - configuration: { - configurationType: 'BAR_CHART', - aggregateFieldMetadataUniversalIdentifier: - PULL_REQUEST_NAME_FIELD_UNIVERSAL_IDENTIFIER, - aggregateOperation: AggregateOperations.COUNT, - primaryAxisGroupByFieldMetadataUniversalIdentifier: - MERGED_AT_FIELD_UNIVERSAL_IDENTIFIER, - primaryAxisDateGranularity: - ObjectRecordGroupByDateGranularity.WEEK, - displayDataLabel: false, - displayLegend: false, - color: 'green', - layout: 'VERTICAL', - omitNullValues: true, - ...COMMON, - }, - }, - { - universalIdentifier: 'd2e3f4a5-b6c7-4890-a1b2-c3d4e5f6a7b8', - title: 'PR Reviews per Week', - type: 'GRAPH', - objectUniversalIdentifier: PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - gridPosition: { row: 6, ...COL_2, rowSpan: 6 }, - configuration: { - configurationType: 'BAR_CHART', - aggregateFieldMetadataUniversalIdentifier: - REVIEW_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - aggregateOperation: AggregateOperations.COUNT, - primaryAxisGroupByFieldMetadataUniversalIdentifier: - REVIEW_FIRST_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - primaryAxisDateGranularity: - ObjectRecordGroupByDateGranularity.WEEK, - displayDataLabel: false, - displayLegend: false, - color: 'blue', - layout: 'VERTICAL', - omitNullValues: true, - ...COMMON, - }, - }, - { - universalIdentifier: '6d03cbd1-ee03-4bf4-a3ac-143d03fa7fd9', - title: 'Issues per Week', - type: 'GRAPH', - objectUniversalIdentifier: ISSUE_UNIVERSAL_IDENTIFIER, - gridPosition: { row: 12, ...COL_2, rowSpan: 6 }, - configuration: { - configurationType: 'BAR_CHART', - aggregateFieldMetadataUniversalIdentifier: - ISSUE_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - aggregateOperation: AggregateOperations.COUNT, - primaryAxisGroupByFieldMetadataUniversalIdentifier: - ISSUE_GITHUB_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - primaryAxisDateGranularity: - ObjectRecordGroupByDateGranularity.WEEK, - secondaryAxisGroupByFieldMetadataUniversalIdentifier: - ISSUE_STATE_FIELD_UNIVERSAL_IDENTIFIER, - displayDataLabel: false, - displayLegend: true, - color: 'orange', - layout: 'VERTICAL', - omitNullValues: true, - ...COMMON, - }, - }, - { - universalIdentifier: '7b3e9c4a-1d52-4f8b-ac76-3e5b8d2f1a9c', - title: 'Top PR Authors', - type: 'FRONT_COMPONENT', - gridPosition: { row: 0, ...COL_3, rowSpan: 9 }, - configuration: { - configurationType: 'FRONT_COMPONENT', - frontComponentUniversalIdentifier: - TOP_PR_AUTHORS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, - }, - }, - { - universalIdentifier: '5e4a8c1d-7f93-4b2e-9d6c-3a8f1b5e7d4c', - title: 'Top Reviewers', - type: 'FRONT_COMPONENT', - gridPosition: { row: 9, ...COL_3, rowSpan: 9 }, - configuration: { - configurationType: 'FRONT_COMPONENT', - frontComponentUniversalIdentifier: - TOP_REVIEWERS_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, - }, - }, - ], - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/command-menu-items/fetch-project-items.command-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/command-menu-items/fetch-project-items.command-menu-item.ts deleted file mode 100644 index 52fbaedbb6..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/command-menu-items/fetch-project-items.command-menu-item.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define'; - -export default defineCommandMenuItem({ - universalIdentifier: '719cfe1c-d570-4c8c-89e6-88671c6ba1ea', - frontComponentUniversalIdentifier: '7c397b0c-8b19-4fac-924a-8f6aa1dece78', - label: 'Fetch Project Items', - icon: 'IconLayoutKanban', - isPinned: false, - conditionalAvailabilityExpression: - objectMetadataItem.nameSingular === 'projectItem', -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/linked-issue-on-project-item.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/linked-issue-on-project-item.field.ts deleted file mode 100644 index f03e3ec74b..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/linked-issue-on-project-item.field.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; -import { PROJECT_ITEM_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/objects/project-item.object'; -import { ISSUE_UNIVERSAL_IDENTIFIER } from 'src/modules/github/issue/objects/issue.object'; -import { PROJECT_ITEMS_ON_ISSUE_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/issue/fields/project-items-on-issue.field'; - -export const LINKED_ISSUE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER = - '7a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d'; - -export default defineField({ - universalIdentifier: LINKED_ISSUE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PROJECT_ITEM_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'linkedIssue', - label: 'Linked Issue', - icon: 'IconBug', - relationTargetObjectMetadataUniversalIdentifier: ISSUE_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - PROJECT_ITEMS_ON_ISSUE_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: OnDeleteAction.SET_NULL, - joinColumnName: 'linkedIssueId', - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/linked-pr-on-project-item.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/linked-pr-on-project-item.field.ts deleted file mode 100644 index 85fcea65c8..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/linked-pr-on-project-item.field.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; -import { PROJECT_ITEM_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/objects/project-item.object'; -import { PULL_REQUEST_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/objects/pull-request.object'; -import { PROJECT_ITEMS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/fields/project-items-on-pull-request.field'; - -export const LINKED_PR_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER = - '9c4d5e6f-7a8b-4c9d-ae0f-1a2b3c4d5e6f'; - -export default defineField({ - universalIdentifier: LINKED_PR_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PROJECT_ITEM_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'linkedPullRequest', - label: 'Linked PR', - icon: 'IconGitPullRequest', - relationTargetObjectMetadataUniversalIdentifier: - PULL_REQUEST_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - PROJECT_ITEMS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: OnDeleteAction.SET_NULL, - joinColumnName: 'linkedPullRequestId', - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/main-assignee-on-project-item.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/main-assignee-on-project-item.field.ts deleted file mode 100644 index 03ddeb0ef4..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/fields/main-assignee-on-project-item.field.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; -import { PROJECT_ITEM_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/objects/project-item.object'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { ASSIGNED_PROJECT_ITEMS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/fields/assigned-project-items-on-contributor.field'; - -export const MAIN_ASSIGNEE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER = - '5bc9d7b3-ca5a-4006-bd74-0420d1f3df85'; - -export default defineField({ - universalIdentifier: MAIN_ASSIGNEE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PROJECT_ITEM_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'mainAssignee', - label: 'Main Assignee', - icon: 'IconUser', - relationTargetObjectMetadataUniversalIdentifier: - CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - ASSIGNED_PROJECT_ITEMS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: OnDeleteAction.SET_NULL, - joinColumnName: 'mainAssigneeId', - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/front-components/fetch-project-items.front-component.tsx b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/front-components/fetch-project-items.front-component.tsx deleted file mode 100644 index bf5b7a64cc..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/front-components/fetch-project-items.front-component.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { useEffect, useState } from 'react'; -import { defineFrontComponent } from 'twenty-sdk/define'; -import { - enqueueSnackbar, - unmountFrontComponent, - updateProgress, -} from 'twenty-sdk/front-component'; -import { callAppRoute } from 'src/modules/shared/call-app-route'; - -type CountResponse = { - totalPages: number; - projects: Array<{ - owner: string; - number: number; - totalCount: number; - pages: number; - }>; -}; - -type FetchPageResponse = { - itemCount: number; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}; - -type SyncStatus = 'syncing' | 'done' | 'error'; - -const FetchProjectItems = () => { - const [status, setStatus] = useState('syncing'); - - useEffect(() => { - const run = async () => { - try { - const counts = (await callAppRoute( - '/github/count-project-items', - {}, - )) as CountResponse; - - if (counts.projects.length === 0) { - throw new Error( - 'No projects resolved. Set GITHUB_PROJECTS in the application variables (e.g. `twentyhq/24`).', - ); - } - - const totalPages = Math.max(counts.totalPages, 1); - let pagesProcessed = 0; - let totalItems = 0; - - for (const { owner, number } of counts.projects) { - let cursor: string | null = null; - let hasMore = true; - - while (hasMore) { - const data = (await callAppRoute('/github/fetch-project-items', { - owner, - number, - cursor, - })) as FetchPageResponse; - - totalItems += data.itemCount; - hasMore = data.hasMore && data.itemCount > 0; - cursor = data.endCursor; - pagesProcessed++; - - updateProgress(Math.min(Math.round((pagesProcessed / totalPages) * 100), 99)); - } - } - - updateProgress(100); - enqueueSnackbar({ - message: `Fetched ${totalItems} project items`, - variant: 'success', - }); - setStatus('done'); - } catch (err) { - const message = - err instanceof Error ? err.message : 'Failed to fetch project items'; - enqueueSnackbar({ message, variant: 'error' }); - setStatus('error'); - } finally { - unmountFrontComponent(); - } - }; - run(); - }, []); - - if (status === 'syncing') return
Fetching project items...
; - if (status === 'error') return
Failed to fetch project items.
; - return
Done
; -}; - -export default defineFrontComponent({ - universalIdentifier: '7c397b0c-8b19-4fac-924a-8f6aa1dece78', - name: 'Fetch Project Items', - description: 'Fetches project items from GitHub Projects V2', - isHeadless: true, - component: FetchProjectItems, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/count-project-items.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/count-project-items.ts deleted file mode 100644 index ae8e24c29c..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/count-project-items.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { githubGraphqlOptional } from 'src/modules/github/connector/github-client'; - -const ORG_QUERY = ` -query($owner: String!, $number: Int!) { - organization(login: $owner) { - projectV2(number: $number) { - items { totalCount } - } - } -}`; - -const USER_QUERY = ORG_QUERY.replace( - 'organization(login: $owner)', - 'user(login: $owner)', -); - -type OrgResponse = { - organization: { projectV2: { items: { totalCount: number } } | null } | null; -}; - -type UserResponse = { - user: { projectV2: { items: { totalCount: number } } | null } | null; -}; - -export async function countProjectItems( - owner: string, - projectNumber: number, -): Promise { - const orgData = await githubGraphqlOptional(ORG_QUERY, { - owner, - number: projectNumber, - }); - const orgCount = orgData?.organization?.projectV2?.items.totalCount; - if (typeof orgCount === 'number') return orgCount; - - const userData = await githubGraphqlOptional(USER_QUERY, { - owner, - number: projectNumber, - }); - return userData?.user?.projectV2?.items.totalCount ?? 0; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fetch-project-item-by-node-id.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fetch-project-item-by-node-id.ts deleted file mode 100644 index caccd64cb6..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fetch-project-item-by-node-id.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { githubGraphql } from 'src/modules/github/connector/github-client'; -import type { ProjectV2Item } from 'src/modules/github/project-item/types/project-v2-item'; -import { PROJECT_V2_ITEM_FRAGMENT } from 'src/modules/github/project-item/graphql/github/fragments'; - -const QUERY = ` -query($id: ID!) { - node(id: $id) { - ... on ProjectV2Item { - ${PROJECT_V2_ITEM_FRAGMENT} - } - } -}`; - -type Response = { - node: ProjectV2Item | null; -}; - -export async function fetchProjectItemByNodeId( - nodeId: string, -): Promise { - try { - const data = await githubGraphql(QUERY, { id: nodeId }); - return data.node; - } catch (err) { - const msg = err instanceof Error ? err.message : ''; - if (msg.includes('Could not resolve') || msg.includes('global id')) { - return null; - } - throw err; - } -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fetch-project-items.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fetch-project-items.ts deleted file mode 100644 index 068af361e8..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fetch-project-items.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - EMPTY_PAGE, - type GithubPage, - githubGraphqlOptional, -} from 'src/modules/github/connector/github-client'; -import type { ProjectV2Item } from 'src/modules/github/project-item/types/project-v2-item'; -import { PROJECT_V2_ITEM_FRAGMENT } from 'src/modules/github/project-item/graphql/github/fragments'; - -const ORG_QUERY = ` -query($owner: String!, $number: Int!, $cursor: String) { - organization(login: $owner) { - projectV2(number: $number) { - items(first: 100, after: $cursor) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { ${PROJECT_V2_ITEM_FRAGMENT} } - } - } - } -}`; - -const USER_QUERY = ORG_QUERY.replace( - 'organization(login: $owner)', - 'user(login: $owner)', -); - -type ItemsConnection = GithubPage; - -type OrgResponse = { - organization: { projectV2: { items: ItemsConnection } | null } | null; -}; - -type UserResponse = { - user: { projectV2: { items: ItemsConnection } | null } | null; -}; - -export async function fetchProjectItems( - owner: string, - projectNumber: number, - cursor: string | null = null, -): Promise<{ - items: ProjectV2Item[]; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}> { - const orgData = await githubGraphqlOptional(ORG_QUERY, { - owner, - number: projectNumber, - cursor, - }); - - let conn: ItemsConnection | undefined = - orgData?.organization?.projectV2?.items; - - if (!conn) { - const userData = await githubGraphqlOptional(USER_QUERY, { - owner, - number: projectNumber, - cursor, - }); - conn = userData?.user?.projectV2?.items; - } - - if (!conn) { - console.warn( - `[github-gql] project ${owner}/${projectNumber} returned no items (project does not exist or the fine-grained PAT lacks Organization → Projects: Read for "${owner}", and may also need to be approved by an org admin).`, - ); - return { items: [], ...EMPTY_PAGE }; - } - - return { - items: conn.nodes, - totalCount: conn.totalCount, - hasMore: conn.pageInfo.hasNextPage, - endCursor: conn.pageInfo.endCursor, - }; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fragments.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fragments.ts deleted file mode 100644 index 6d6c35832a..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/github/fragments.ts +++ /dev/null @@ -1,46 +0,0 @@ -export const PROJECT_V2_ITEM_FRAGMENT = ` - id - content { - __typename - ... on Issue { - title - number - url - repository { nameWithOwner } - } - ... on PullRequest { - title - number - url - repository { nameWithOwner } - } - ... on DraftIssue { - title - } - } - fieldValues(first: 20) { - nodes { - __typename - ... on ProjectV2ItemFieldSingleSelectValue { - name - field { ... on ProjectV2SingleSelectField { name } } - } - ... on ProjectV2ItemFieldIterationValue { - title - field { ... on ProjectV2IterationField { name } } - } - ... on ProjectV2ItemFieldTextValue { - text - field { ... on ProjectV2Field { name } } - } - ... on ProjectV2ItemFieldNumberValue { - number - field { ... on ProjectV2Field { name } } - } - ... on ProjectV2ItemFieldUserValue { - users(first: 10) { nodes { login } } - field { ... on ProjectV2Field { name } } - } - } - } -`; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/mutations/batch-upsert.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/mutations/batch-upsert.ts deleted file mode 100644 index 0d2511effb..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/graphql/mutations/batch-upsert.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { chunkedBatchCreate } from 'src/modules/shared/twenty-client'; -import type { ProjectItemRow } from 'src/modules/github/project-item/types/project-item-row'; - -export async function batchUpsertProjectItems( - items: Array<{ - name: string; - githubProjectItemId: string; - status: string; - sprint: string; - assignees: string; - priority: string | null; - mainAssigneeId: string | null; - linkedIssueId: string | null; - linkedPullRequestId: string | null; - githubUrl: { primaryLinkLabel: string; primaryLinkUrl: string; secondaryLinks: null } | null; - repo: string; - }>, -): Promise { - return chunkedBatchCreate('createProjectItems', items, { - id: true, - githubProjectItemId: true, - name: true, - status: true, - mainAssigneeId: true, - linkedIssueId: true, - linkedPullRequestId: true, - githubUrl: { primaryLinkLabel: true, primaryLinkUrl: true }, - }) as Promise; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/logic-functions/count-project-items.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/logic-functions/count-project-items.logic-function.ts deleted file mode 100644 index 2b96b8ce5f..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/logic-functions/count-project-items.logic-function.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { - getGithubProjects, - type GithubProject, -} from 'src/modules/github/connector/config'; -import { countProjectItems } from 'src/modules/github/project-item/graphql/github/count-project-items'; - -const PAGE_SIZE = 100; - -type CountProjectItemsPayload = { - projects?: GithubProject[]; -}; - -const handler = async (event: RoutePayload) => { - const bodyProjects = event.body?.projects; - const projects = - bodyProjects && bodyProjects.length > 0 - ? bodyProjects - : getGithubProjects(); - - const results: Array = - []; - let totalPages = 0; - - for (const { owner, number } of projects) { - const totalCount = await countProjectItems(owner, number); - const pages = Math.max(Math.ceil(totalCount / PAGE_SIZE), 1); - results.push({ owner, number, totalCount, pages }); - totalPages += pages; - } - - return { totalPages, projects: results }; -}; - -export default defineLogicFunction({ - universalIdentifier: 'f7a3e1b2-5c4d-4e6f-8a9b-0d1c2e3f4a5b', - name: 'count-project-items', - description: - 'Counts total project item pages across configured projects using GraphQL totalCount', - timeoutSeconds: 30, - handler, - httpRouteTriggerSettings: { - path: '/github/count-project-items', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/logic-functions/fetch-project-items.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/logic-functions/fetch-project-items.logic-function.ts deleted file mode 100644 index 6222569935..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/logic-functions/fetch-project-items.logic-function.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { fetchProjectItems } from 'src/modules/github/project-item/graphql/github/fetch-project-items'; -import type { ProjectV2Item } from 'src/modules/github/project-item/types/project-v2-item'; -import { batchUpsertProjectItems } from 'src/modules/github/project-item/graphql/mutations/batch-upsert'; -import { projectItemFromGraphql } from 'src/modules/github/project-item/normalizers'; -import { isFixtureAllowed } from 'src/modules/shared/fixtures'; - -export type FetchProjectItemsFixturePage = { - items: ProjectV2Item[]; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}; - -type FetchProjectItemsPayload = { - owner: string; - number: number; - cursor?: string | null; - fixturePage?: FetchProjectItemsFixturePage; -}; - -const handler = async (event: RoutePayload) => { - const { owner, number, cursor = null, fixturePage } = event.body ?? {}; - if (!owner || !number) { - return { error: 'owner and number are required' }; - } - - const result = - fixturePage && isFixtureAllowed() - ? fixturePage - : await fetchProjectItems(owner, number, cursor); - const { items, totalCount, hasMore, endCursor } = result; - - if (items.length === 0) { - return { itemCount: 0, totalCount, hasMore: false, endCursor: null }; - } - - const itemData = await Promise.all(items.map(projectItemFromGraphql)); - - await batchUpsertProjectItems(itemData); - - return { itemCount: items.length, totalCount, hasMore, endCursor }; -}; - -export default defineLogicFunction({ - universalIdentifier: 'acb300d4-d4ec-491c-b314-3d4db90a49c5', - name: 'fetch-project-items', - description: - 'Fetches one page of project items from GitHub Projects V2 GraphQL API and batch upserts them', - timeoutSeconds: 300, - handler, - httpRouteTriggerSettings: { - path: '/github/fetch-project-items', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/navigation-menu-items/project-items.navigation-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/navigation-menu-items/project-items.navigation-menu-item.ts deleted file mode 100644 index 6b9f762b5e..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/navigation-menu-items/project-items.navigation-menu-item.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define'; -import { PROJECT_ITEM_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/objects/project-item.object'; -import { GITHUB_FOLDER_UNIVERSAL_IDENTIFIER } from 'src/modules/github/navigation-menu-items/github-folder.navigation-menu-item'; - -export default defineNavigationMenuItem({ - universalIdentifier: '25d3a916-9a70-478f-8d83-ef336e582fbc', - position: 2, - type: NavigationMenuItemType.OBJECT, - targetObjectUniversalIdentifier: PROJECT_ITEM_UNIVERSAL_IDENTIFIER, - folderUniversalIdentifier: GITHUB_FOLDER_UNIVERSAL_IDENTIFIER, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/normalizers.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/normalizers.ts deleted file mode 100644 index e0bac19f10..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/normalizers.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { LinksFieldValue } from 'src/modules/shared/types'; -import { toLinksField } from 'src/modules/shared/types'; -import type { ProjectV2Item } from 'src/modules/github/project-item/types/project-v2-item'; -import { - extractFieldValue, - extractAssigneeLogins, -} from 'src/modules/github/project-item/utils/extract-field-value'; -import { findIssueByNumberAndRepo } from 'src/modules/github/issue/graphql/queries/find-by-number-and-repo'; -import { - findPullRequestByGithubNumber, - findPullRequestByRepoAndNumber, -} from 'src/modules/github/pull-request/graphql/queries/find-by-github-number'; -import { findContributorByGhLogin } from 'src/modules/github/contributor/graphql/queries/find-by-gh-login'; - -const STATUS_MAP: Record = { - 'No Status': 'NO_STATUS', - Backlog: 'BACKLOG', - Todo: 'TODO', - 'In Progress': 'IN_PROGRESS', - 'In Review': 'IN_REVIEW', - Done: 'DONE', -}; - -const PRIORITY_MAP: Record = { - Low: 'LOW', - Medium: 'MEDIUM', - High: 'HIGH', - Critical: 'CRITICAL', -}; - -export type ProjectItemUpsertInput = { - name: string; - githubProjectItemId: string; - status: string; - sprint: string; - assignees: string; - priority: string | null; - mainAssigneeId: string | null; - linkedIssueId: string | null; - linkedPullRequestId: string | null; - githubUrl: LinksFieldValue | null; - repo: string; -}; - -export async function projectItemFromGraphql( - item: ProjectV2Item, -): Promise { - const title = - item.content?.title ?? - (extractFieldValue(item, 'Title') || 'Untitled'); - const rawStatus = extractFieldValue(item, 'Status'); - const status = STATUS_MAP[rawStatus] ?? 'NO_STATUS'; - const sprint = - extractFieldValue(item, 'Sprint') || - extractFieldValue(item, 'Iteration'); - const assigneeLogins = extractAssigneeLogins(item); - const assignees = assigneeLogins.join(', '); - const rawPriority = extractFieldValue(item, 'Priority'); - const priority = PRIORITY_MAP[rawPriority] ?? null; - - let mainAssigneeId: string | null = null; - if (assigneeLogins.length > 0) { - const contributor = await findContributorByGhLogin(assigneeLogins[0]); - mainAssigneeId = contributor?.id ?? null; - } - - let linkedIssueId: string | null = null; - let linkedPullRequestId: string | null = null; - let repo = ''; - let githubUrl: LinksFieldValue | null = null; - - if (item.content) { - const contentType = item.content.__typename; - repo = item.content.repository?.nameWithOwner ?? ''; - - if (contentType === 'Issue' && item.content.number) { - const issue = await findIssueByNumberAndRepo(item.content.number, repo); - linkedIssueId = issue?.id ?? null; - if (item.content.url) { - githubUrl = toLinksField(item.content.url, `#${item.content.number}`); - } - } else if (contentType === 'PullRequest' && item.content.number) { - const pr = repo - ? await findPullRequestByRepoAndNumber(repo, item.content.number) - : await findPullRequestByGithubNumber(item.content.number); - linkedPullRequestId = pr?.id ?? null; - if (item.content.url) { - githubUrl = toLinksField(item.content.url, `#${item.content.number}`); - } - } - } - - return { - name: title, - githubProjectItemId: item.id, - status, - sprint, - assignees, - priority, - mainAssigneeId, - linkedIssueId, - linkedPullRequestId, - githubUrl, - repo, - }; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/objects/project-item.object.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/objects/project-item.object.ts deleted file mode 100644 index b339f48913..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/objects/project-item.object.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { defineObject, FieldType } from 'twenty-sdk/define'; - -export const PROJECT_ITEM_UNIVERSAL_IDENTIFIER = - '1c4c36a5-586c-4ead-9f3e-5e9718ba6231'; - -export const PROJECT_ITEM_NAME_FIELD_UNIVERSAL_IDENTIFIER = - '215cdbbd-8606-4c13-9025-3ebe912eaa32'; - -export const PROJECT_ITEM_GITHUB_ID_FIELD_UNIVERSAL_IDENTIFIER = - '2d3e4f5a-6b7c-4d8e-af9b-0c1d2e3f4a5b'; - -export const PROJECT_ITEM_SPRINT_FIELD_UNIVERSAL_IDENTIFIER = - 'e3708df4-6b6b-45f6-83fb-9d47c5bc5a25'; - -export const PROJECT_ITEM_ASSIGNEES_FIELD_UNIVERSAL_IDENTIFIER = - '3e4f5a6b-7c8d-4e9f-b0ac-1d2e3f4a5b6c'; - -export const PROJECT_ITEM_GITHUB_URL_FIELD_UNIVERSAL_IDENTIFIER = - '2db040a6-f2af-4e7d-9b70-d453f3cd99b7'; - -export const PROJECT_ITEM_REPO_FIELD_UNIVERSAL_IDENTIFIER = - '589d6aeb-8384-4d52-9d71-d63b7fd94ec7'; - -enum ProjectItemStatus { - NO_STATUS = 'NO_STATUS', - BACKLOG = 'BACKLOG', - TODO = 'TODO', - IN_PROGRESS = 'IN_PROGRESS', - IN_REVIEW = 'IN_REVIEW', - DONE = 'DONE', -} - -export const PROJECT_ITEM_STATUS_FIELD_UNIVERSAL_IDENTIFIER = - '54034cfc-83d9-478d-849c-98c91a819342'; - -enum ProjectItemPriority { - LOW = 'LOW', - MEDIUM = 'MEDIUM', - HIGH = 'HIGH', - CRITICAL = 'CRITICAL', -} - -export const PROJECT_ITEM_PRIORITY_FIELD_UNIVERSAL_IDENTIFIER = - '952a601c-b2da-4b84-9c7b-a9b42fcb7d95'; - -export const PROJECT_ITEM_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER = - '597b4141-487f-5ad3-87c2-49f0a1679856'; - -export default defineObject({ - universalIdentifier: PROJECT_ITEM_UNIVERSAL_IDENTIFIER, - nameSingular: 'projectItem', - namePlural: 'projectItems', - labelSingular: 'Project Item', - labelPlural: 'Project Items', - icon: 'IconLayoutKanban', - labelIdentifierFieldMetadataUniversalIdentifier: - PROJECT_ITEM_NAME_FIELD_UNIVERSAL_IDENTIFIER, - fields: [ - { - universalIdentifier: PROJECT_ITEM_NAME_FIELD_UNIVERSAL_IDENTIFIER, - name: 'name', - type: FieldType.TEXT, - label: 'Name', - icon: 'IconTextCaption', - }, - { - universalIdentifier: PROJECT_ITEM_GITHUB_ID_FIELD_UNIVERSAL_IDENTIFIER, - name: 'githubProjectItemId', - type: FieldType.TEXT, - label: 'GitHub Item ID', - icon: 'IconHash', - isUnique: true, - }, - { - universalIdentifier: PROJECT_ITEM_STATUS_FIELD_UNIVERSAL_IDENTIFIER, - name: 'status', - type: FieldType.SELECT, - label: 'Status', - icon: 'IconCircleDot', - options: [ - { - value: ProjectItemStatus.NO_STATUS, - label: 'No Status', - position: 0, - color: 'gray', - }, - { - value: ProjectItemStatus.BACKLOG, - label: 'Backlog', - position: 1, - color: 'gray', - }, - { - value: ProjectItemStatus.TODO, - label: 'Todo', - position: 2, - color: 'blue', - }, - { - value: ProjectItemStatus.IN_PROGRESS, - label: 'In Progress', - position: 3, - color: 'yellow', - }, - { - value: ProjectItemStatus.IN_REVIEW, - label: 'In Review', - position: 4, - color: 'turquoise', - }, - { - value: ProjectItemStatus.DONE, - label: 'Done', - position: 5, - color: 'green', - }, - ], - }, - { - universalIdentifier: PROJECT_ITEM_SPRINT_FIELD_UNIVERSAL_IDENTIFIER, - name: 'sprint', - type: FieldType.TEXT, - label: 'Sprint', - icon: 'IconRun', - }, - { - universalIdentifier: PROJECT_ITEM_ASSIGNEES_FIELD_UNIVERSAL_IDENTIFIER, - name: 'assignees', - type: FieldType.TEXT, - label: 'Assignees', - icon: 'IconUsers', - }, - { - universalIdentifier: PROJECT_ITEM_PRIORITY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'priority', - type: FieldType.SELECT, - label: 'Priority', - icon: 'IconFlag', - options: [ - { - value: ProjectItemPriority.LOW, - label: 'Low', - position: 0, - color: 'green', - }, - { - value: ProjectItemPriority.MEDIUM, - label: 'Medium', - position: 1, - color: 'turquoise', - }, - { - value: ProjectItemPriority.HIGH, - label: 'High', - position: 2, - color: 'red', - }, - { - value: ProjectItemPriority.CRITICAL, - label: 'Critical', - position: 3, - color: 'red', - }, - ], - }, - { - universalIdentifier: PROJECT_ITEM_GITHUB_URL_FIELD_UNIVERSAL_IDENTIFIER, - name: 'githubUrl', - type: FieldType.LINKS, - label: 'GitHub URL', - icon: 'IconLink', - }, - { - universalIdentifier: PROJECT_ITEM_REPO_FIELD_UNIVERSAL_IDENTIFIER, - name: 'repo', - type: FieldType.TEXT, - label: 'Repository', - icon: 'IconFolder', - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/github-project-v2-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/github-project-v2-item.ts deleted file mode 100644 index c91adc6216..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/github-project-v2-item.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type GitHubProjectV2Item = { - id: number; - node_id: string; - project_node_id?: string; - content_node_id?: string; - content_type?: 'Issue' | 'PullRequest' | 'DraftIssue'; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/project-item-row.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/project-item-row.ts deleted file mode 100644 index 8445526743..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/project-item-row.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { LinksFieldValue } from 'src/modules/shared/types'; - -export type ProjectItemRow = { - id: string; - name?: string | null; - githubProjectItemId?: string | null; - status?: string | null; - sprint?: string | null; - assignees?: string | null; - priority?: string | null; - mainAssigneeId?: string | null; - linkedIssueId?: string | null; - linkedPullRequestId?: string | null; - githubUrl?: LinksFieldValue | null; - repo?: string | null; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/project-v2-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/project-v2-item.ts deleted file mode 100644 index 91fb168578..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/types/project-v2-item.ts +++ /dev/null @@ -1,42 +0,0 @@ -type FieldRef = { name: string }; - -export type ProjectV2FieldValue = - | { - __typename: 'ProjectV2ItemFieldSingleSelectValue'; - name: string; - field: FieldRef; - } - | { - __typename: 'ProjectV2ItemFieldIterationValue'; - title: string; - field: FieldRef; - } - | { - __typename: 'ProjectV2ItemFieldTextValue'; - text: string; - field: FieldRef; - } - | { - __typename: 'ProjectV2ItemFieldNumberValue'; - number: number; - field: FieldRef; - } - | { - __typename: 'ProjectV2ItemFieldUserValue'; - users: { nodes: Array<{ login: string }> }; - field: FieldRef; - }; - -export type ProjectV2Item = { - id: string; - content: { - __typename: string; - title?: string; - number?: number; - url?: string; - repository?: { nameWithOwner: string }; - } | null; - fieldValues: { - nodes: Array; - }; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/utils/extract-field-value.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/utils/extract-field-value.ts deleted file mode 100644 index c09a393472..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/utils/extract-field-value.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { ProjectV2Item } from 'src/modules/github/project-item/types/project-v2-item'; - -export function extractAssigneeLogins(item: ProjectV2Item): string[] { - for (const fv of item.fieldValues.nodes) { - if (fv.field?.name !== 'Assignees') continue; - if (fv.__typename === 'ProjectV2ItemFieldUserValue') { - return fv.users.nodes.map((u) => u.login); - } - } - return []; -} - -export function extractFieldValue( - item: ProjectV2Item, - fieldName: string, -): string { - for (const fv of item.fieldValues.nodes) { - if (fv.field?.name !== fieldName) continue; - - switch (fv.__typename) { - case 'ProjectV2ItemFieldSingleSelectValue': - return fv.name; - case 'ProjectV2ItemFieldIterationValue': - return fv.title; - case 'ProjectV2ItemFieldTextValue': - return fv.text; - case 'ProjectV2ItemFieldNumberValue': - return String(fv.number); - case 'ProjectV2ItemFieldUserValue': - return fv.users.nodes.map((u) => u.login).join(', '); - } - } - return ''; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/views/all-project-items.view.ts b/packages/twenty-apps/community/github-connector/src/modules/github/project-item/views/all-project-items.view.ts deleted file mode 100644 index 1b8a8971e2..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/project-item/views/all-project-items.view.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - defineView, - ViewKey, - ViewSortDirection, - ViewType, -} from 'twenty-sdk/define'; -import { - PROJECT_ITEM_UNIVERSAL_IDENTIFIER, - PROJECT_ITEM_NAME_FIELD_UNIVERSAL_IDENTIFIER, - PROJECT_ITEM_STATUS_FIELD_UNIVERSAL_IDENTIFIER, - PROJECT_ITEM_SPRINT_FIELD_UNIVERSAL_IDENTIFIER, - PROJECT_ITEM_ASSIGNEES_FIELD_UNIVERSAL_IDENTIFIER, - PROJECT_ITEM_PRIORITY_FIELD_UNIVERSAL_IDENTIFIER, - PROJECT_ITEM_GITHUB_URL_FIELD_UNIVERSAL_IDENTIFIER, - PROJECT_ITEM_REPO_FIELD_UNIVERSAL_IDENTIFIER, - PROJECT_ITEM_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, -} from 'src/modules/github/project-item/objects/project-item.object'; -import { MAIN_ASSIGNEE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/fields/main-assignee-on-project-item.field'; -import { LINKED_ISSUE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/fields/linked-issue-on-project-item.field'; -import { LINKED_PR_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/fields/linked-pr-on-project-item.field'; - -export const ALL_PROJECT_ITEMS_VIEW_UNIVERSAL_IDENTIFIER = - '9bb6d69e-9411-4195-9042-8df2e4b72a11'; - -export default defineView({ - universalIdentifier: ALL_PROJECT_ITEMS_VIEW_UNIVERSAL_IDENTIFIER, - name: 'All Project Items', - objectUniversalIdentifier: PROJECT_ITEM_UNIVERSAL_IDENTIFIER, - type: ViewType.TABLE, - icon: 'IconLayoutKanban', - key: ViewKey.INDEX, - position: 0, - fields: [ - { - universalIdentifier: '19064693-70e0-4653-b6f6-572c3adfcc78', - fieldMetadataUniversalIdentifier: - PROJECT_ITEM_NAME_FIELD_UNIVERSAL_IDENTIFIER, - position: 0, - isVisible: true, - size: 300, - }, - { - universalIdentifier: 'f7a1de3d-3989-4cb9-8e8e-0edd1b4d15e7', - fieldMetadataUniversalIdentifier: - PROJECT_ITEM_STATUS_FIELD_UNIVERSAL_IDENTIFIER, - position: 1, - isVisible: true, - size: 130, - }, - { - universalIdentifier: 'f4fae1f9-8480-4cfd-b493-9dbac84c6643', - fieldMetadataUniversalIdentifier: - PROJECT_ITEM_PRIORITY_FIELD_UNIVERSAL_IDENTIFIER, - position: 2, - isVisible: true, - size: 130, - }, - { - universalIdentifier: '5969adcf-1e89-4561-a052-0296058623f3', - fieldMetadataUniversalIdentifier: - MAIN_ASSIGNEE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER, - position: 3, - isVisible: true, - size: 180, - }, - { - universalIdentifier: '21868cca-dae2-45e7-aa92-e667703e8cfe', - fieldMetadataUniversalIdentifier: - PROJECT_ITEM_SPRINT_FIELD_UNIVERSAL_IDENTIFIER, - position: 4, - isVisible: true, - size: 150, - }, - { - universalIdentifier: 'bf877bd3-a788-4db5-89cd-db6fb74fe49d', - fieldMetadataUniversalIdentifier: - PROJECT_ITEM_ASSIGNEES_FIELD_UNIVERSAL_IDENTIFIER, - position: 5, - isVisible: true, - size: 200, - }, - { - universalIdentifier: '4c063477-ffe8-4799-bc50-bb9b2483cbab', - fieldMetadataUniversalIdentifier: - LINKED_ISSUE_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER, - position: 6, - isVisible: true, - size: 180, - }, - { - universalIdentifier: 'acba5c9a-fd97-42b6-b0f5-e8e3fb7d49c5', - fieldMetadataUniversalIdentifier: - LINKED_PR_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER, - position: 7, - isVisible: true, - size: 180, - }, - { - universalIdentifier: '931fac89-e99d-46dd-b316-85ff04356811', - fieldMetadataUniversalIdentifier: - PROJECT_ITEM_GITHUB_URL_FIELD_UNIVERSAL_IDENTIFIER, - position: 8, - isVisible: true, - size: 200, - }, - { - universalIdentifier: '11c446af-470a-40d4-96e8-9e91879accf4', - fieldMetadataUniversalIdentifier: - PROJECT_ITEM_REPO_FIELD_UNIVERSAL_IDENTIFIER, - position: 9, - isVisible: true, - size: 180, - }, - ], - sorts: [ - { - universalIdentifier: 'a7c181f1-2961-45b0-a700-dd9489b3420c', - fieldMetadataUniversalIdentifier: - PROJECT_ITEM_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - direction: ViewSortDirection.DESC, - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/pull-request-on-review-event.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/pull-request-on-review-event.field.ts deleted file mode 100644 index 60f0ba5915..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/pull-request-on-review-event.field.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - defineField, - FieldType, - RelationType, - OnDeleteAction, -} from 'twenty-sdk/define'; -import { PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/objects/pull-request-review-event.object'; -import { PULL_REQUEST_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/objects/pull-request.object'; -import { REVIEW_EVENTS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/fields/review-events-on-pull-request.field'; - -export const PULL_REQUEST_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER = - '8bd96f9d-fa6b-431a-9264-000097b580f8'; - -export default defineField({ - universalIdentifier: PULL_REQUEST_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'pullRequest', - label: 'Pull Request', - icon: 'IconGitPullRequest', - relationTargetObjectMetadataUniversalIdentifier: - PULL_REQUEST_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - REVIEW_EVENTS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: OnDeleteAction.CASCADE, - joinColumnName: 'pullRequestId', - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/review-on-review-event.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/review-on-review-event.field.ts deleted file mode 100644 index 2c1916498c..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/review-on-review-event.field.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - defineField, - FieldType, - RelationType, - OnDeleteAction, -} from 'twenty-sdk/define'; -import { PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/objects/pull-request-review-event.object'; -import { PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/objects/pull-request-review.object'; -import { EVENTS_ON_REVIEW_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/fields/events-on-review.field'; - -export const REVIEW_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER = - '1bc4170d-e998-4aac-bfea-4a6145d7d707'; - -export default defineField({ - universalIdentifier: REVIEW_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'review', - label: 'Review', - icon: 'IconEye', - relationTargetObjectMetadataUniversalIdentifier: - PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - EVENTS_ON_REVIEW_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: OnDeleteAction.SET_NULL, - joinColumnName: 'reviewId', - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/reviewer-on-review-event.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/reviewer-on-review-event.field.ts deleted file mode 100644 index 1ee48f85b2..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/fields/reviewer-on-review-event.field.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - defineField, - FieldType, - RelationType, - OnDeleteAction, -} from 'twenty-sdk/define'; -import { PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/objects/pull-request-review-event.object'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { REVIEW_EVENTS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/fields/review-events-on-contributor.field'; - -export const REVIEWER_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER = - 'f5a6b7c8-9d0e-4f1a-b2c3-d4e5f6a7b8c9'; - -export default defineField({ - universalIdentifier: REVIEWER_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'reviewer', - label: 'Reviewer', - icon: 'IconUser', - relationTargetObjectMetadataUniversalIdentifier: - CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - REVIEW_EVENTS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: OnDeleteAction.SET_NULL, - joinColumnName: 'reviewerId', - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/graphql/mutations/batch-upsert.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/graphql/mutations/batch-upsert.ts deleted file mode 100644 index 0276fb8f2f..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/graphql/mutations/batch-upsert.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { chunkedBatchCreate } from 'src/modules/shared/twenty-client'; -import type { PullRequestReviewEventRow } from 'src/modules/github/pull-request-review-event/types/pull-request-review-event-row'; - -export async function batchUpsertReviewEvents( - items: Array<{ - githubReviewId: number; - title: string; - state: string; - submittedAt: string | null; - reviewerId: string | null; - pullRequestId: string; - reviewId?: string | null; - }>, -): Promise { - return chunkedBatchCreate('createPullRequestReviewEvents', items, { - id: true, - githubReviewId: true, - title: true, - state: true, - submittedAt: true, - reviewerId: true, - pullRequestId: true, - reviewId: true, - }) as Promise; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/graphql/queries/find-events-for-pair.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/graphql/queries/find-events-for-pair.ts deleted file mode 100644 index 48074fc7cc..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/graphql/queries/find-events-for-pair.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { getClient } from 'src/modules/shared/twenty-client'; - -export type ReviewEventForPair = { - id: string; - state: string; - submittedAt: string | null; -}; - -type Edge = { node: T }; -type Connection = { - edges: Edge[]; - pageInfo: { hasNextPage: boolean; endCursor: string | null }; -}; - -const PAGE_SIZE = 100; - -export async function findReviewEventsForPair( - pullRequestId: string, - reviewerId: string | null, -): Promise { - const client = getClient(); - const out: ReviewEventForPair[] = []; - let cursor: string | null = null; - - const reviewerFilter = - reviewerId === null - ? { reviewerId: { is: 'NULL' } } - : { reviewerId: { eq: reviewerId } }; - - for (;;) { - const res = await client.query({ - pullRequestReviewEvents: { - __args: { - filter: { - and: [{ pullRequestId: { eq: pullRequestId } }, reviewerFilter], - }, - orderBy: [{ submittedAt: 'AscNullsLast' }], - first: PAGE_SIZE, - after: cursor, - }, - edges: { - node: { id: true, state: true, submittedAt: true }, - }, - pageInfo: { hasNextPage: true, endCursor: true }, - }, - }); - - const conn = - (res.pullRequestReviewEvents as Connection) ?? { - edges: [], - pageInfo: { hasNextPage: false, endCursor: null }, - }; - - for (const edge of conn.edges) out.push(edge.node); - - if (!conn.pageInfo.hasNextPage || !conn.pageInfo.endCursor) break; - cursor = conn.pageInfo.endCursor; - } - - return out; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/navigation-menu-items/pull-request-review-events.navigation-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/navigation-menu-items/pull-request-review-events.navigation-menu-item.ts deleted file mode 100644 index 72be66c037..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/navigation-menu-items/pull-request-review-events.navigation-menu-item.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - defineNavigationMenuItem, - NavigationMenuItemType, -} from 'twenty-sdk/define'; -import { GITHUB_FOLDER_UNIVERSAL_IDENTIFIER } from 'src/modules/github/navigation-menu-items/github-folder.navigation-menu-item'; -import { PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/objects/pull-request-review-event.object'; - -export default defineNavigationMenuItem({ - universalIdentifier: '2a3b4c5d-6e7f-4890-9abc-def012345679', - position: 5, - type: NavigationMenuItemType.OBJECT, - targetObjectUniversalIdentifier: - PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER, - folderUniversalIdentifier: GITHUB_FOLDER_UNIVERSAL_IDENTIFIER, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/normalizers.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/normalizers.ts deleted file mode 100644 index 4e7c94e908..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/normalizers.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { GitHubReview } from 'src/modules/github/pull-request-review-event/types/github-review'; - -export type ReviewEventState = - | 'APPROVED' - | 'CHANGES_REQUESTED' - | 'COMMENTED' - | 'DISMISSED'; - -export type ReviewEventCanonical = { - githubReviewId: number; - title: string; - state: ReviewEventState; - submittedAt: string | null; -}; - -export type GqlReviewLike = { - databaseId: number; - state: ReviewEventState; - submittedAt: string | null; - author: { login: string } | null; -}; - -export function buildReviewEventTitle(login: string, state: string): string { - return `${login} \u2014 ${state.toLowerCase()}`; -} - -export function reviewEventFromWebhook( - review: GitHubReview, -): ReviewEventCanonical { - return { - githubReviewId: review.id, - title: buildReviewEventTitle(review.user.login, review.state), - state: review.state, - submittedAt: review.submitted_at, - }; -} - -export function reviewEventFromGraphql( - review: GqlReviewLike, -): ReviewEventCanonical { - const login = review.author?.login ?? 'unknown'; - return { - githubReviewId: review.databaseId, - title: buildReviewEventTitle(login, review.state), - state: review.state, - submittedAt: review.submittedAt, - }; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/objects/pull-request-review-event.object.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/objects/pull-request-review-event.object.ts deleted file mode 100644 index a137486947..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/objects/pull-request-review-event.object.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { defineObject, FieldType } from 'twenty-sdk/define'; - -export const PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER = - '7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d'; - -export const REVIEW_EVENT_TITLE_FIELD_UNIVERSAL_IDENTIFIER = - '8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e'; - -export const REVIEW_EVENT_GITHUB_REVIEW_ID_FIELD_UNIVERSAL_IDENTIFIER = - '9c0d1e2f-3a4b-4c5d-ae6f-7a8b9c0d1e2f'; - -export const REVIEW_EVENT_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER = - '0d1e2f3a-4b5c-4d6e-bf7a-8b9c0d1e2f3a'; - -enum ReviewEventState { - APPROVED = 'APPROVED', - CHANGES_REQUESTED = 'CHANGES_REQUESTED', - COMMENTED = 'COMMENTED', - DISMISSED = 'DISMISSED', -} - -export const REVIEW_EVENT_STATE_FIELD_UNIVERSAL_IDENTIFIER = - 'e5eeeaab-4ea9-4f9f-873d-9c1c1473ddb3'; - -export const PULL_REQUEST_REVIEW_EVENT_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER = - 'fc577b22-c927-5b0e-9d6d-532dd41cf6d2'; - -export default defineObject({ - universalIdentifier: PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER, - nameSingular: 'pullRequestReviewEvent', - namePlural: 'pullRequestReviewEvents', - labelSingular: 'Pull Request Review Event', - labelPlural: 'Pull Request Review Events', - icon: 'IconEye', - labelIdentifierFieldMetadataUniversalIdentifier: - REVIEW_EVENT_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - fields: [ - { - universalIdentifier: REVIEW_EVENT_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'title', - type: FieldType.TEXT, - label: 'Title', - icon: 'IconTextCaption', - }, - { - universalIdentifier: - REVIEW_EVENT_GITHUB_REVIEW_ID_FIELD_UNIVERSAL_IDENTIFIER, - name: 'githubReviewId', - type: FieldType.NUMBER, - label: 'GitHub Review ID', - icon: 'IconHash', - isUnique: true, - }, - { - universalIdentifier: REVIEW_EVENT_STATE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'state', - type: FieldType.SELECT, - label: 'State', - icon: 'IconCircleDot', - options: [ - { - value: ReviewEventState.APPROVED, - label: 'Approved', - position: 0, - color: 'green', - }, - { - value: ReviewEventState.CHANGES_REQUESTED, - label: 'Changes Requested', - position: 1, - color: 'red', - }, - { - value: ReviewEventState.COMMENTED, - label: 'Commented', - position: 2, - color: 'blue', - }, - { - value: ReviewEventState.DISMISSED, - label: 'Dismissed', - position: 3, - color: 'gray', - }, - ], - }, - { - universalIdentifier: REVIEW_EVENT_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - name: 'submittedAt', - type: FieldType.DATE_TIME, - label: 'Submitted At', - icon: 'IconCalendar', - isNullable: true, - defaultValue: null, - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/types/github-review.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/types/github-review.ts deleted file mode 100644 index ef65ca870c..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/types/github-review.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { GitHubUser } from 'src/modules/github/connector/github-user'; - -export type GitHubReview = { - id: number; - user: GitHubUser; - state: 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'DISMISSED'; - submitted_at: string; - body: string | null; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/types/pull-request-review-event-row.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/types/pull-request-review-event-row.ts deleted file mode 100644 index bf1aabb656..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/types/pull-request-review-event-row.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type PullRequestReviewEventRow = { - id: string; - title?: string | null; - githubReviewId?: number | null; - state?: string | null; - submittedAt?: string | null; - reviewerId?: string | null; - pullRequestId?: string | null; - reviewId?: string | null; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/views/all-pull-request-review-events.view.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/views/all-pull-request-review-events.view.ts deleted file mode 100644 index e25d0900b1..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review-event/views/all-pull-request-review-events.view.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - defineView, - ViewKey, - ViewSortDirection, - ViewType, -} from 'twenty-sdk/define'; -import { - PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER, - REVIEW_EVENT_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - REVIEW_EVENT_STATE_FIELD_UNIVERSAL_IDENTIFIER, - REVIEW_EVENT_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - PULL_REQUEST_REVIEW_EVENT_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, -} from 'src/modules/github/pull-request-review-event/objects/pull-request-review-event.object'; -import { REVIEWER_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/fields/reviewer-on-review-event.field'; -import { PULL_REQUEST_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/fields/pull-request-on-review-event.field'; - -export const ALL_REVIEW_EVENTS_VIEW_UNIVERSAL_IDENTIFIER = - 'a1b2c3d4-0000-4e5f-a6b7-c8d9e0f1a2b3'; - -export default defineView({ - universalIdentifier: ALL_REVIEW_EVENTS_VIEW_UNIVERSAL_IDENTIFIER, - name: 'All Review Events', - objectUniversalIdentifier: PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER, - type: ViewType.TABLE, - icon: 'IconEye', - key: ViewKey.INDEX, - position: 0, - fields: [ - { - universalIdentifier: '69e69714-05a5-48cd-98b9-13e79ba602c5', - fieldMetadataUniversalIdentifier: - REVIEW_EVENT_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - position: 0, - isVisible: true, - size: 250, - }, - { - universalIdentifier: 'ca794df5-74b0-4fc7-a2a7-05a72380e080', - fieldMetadataUniversalIdentifier: - REVIEW_EVENT_STATE_FIELD_UNIVERSAL_IDENTIFIER, - position: 1, - isVisible: true, - size: 150, - }, - { - universalIdentifier: '624b139e-46de-416d-97b2-527c07b3a738', - fieldMetadataUniversalIdentifier: - REVIEWER_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER, - position: 2, - isVisible: true, - size: 160, - }, - { - universalIdentifier: 'bcf9d3ac-d68c-410b-99f4-7e36f992f767', - fieldMetadataUniversalIdentifier: - PULL_REQUEST_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER, - position: 3, - isVisible: true, - size: 200, - }, - { - universalIdentifier: '798ec4a2-9e72-4d48-8010-fe5dcaa7fbed', - fieldMetadataUniversalIdentifier: - REVIEW_EVENT_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - position: 4, - isVisible: true, - size: 180, - }, - ], - sorts: [ - { - universalIdentifier: '0c8f9d6f-1a2d-4b14-a35a-8ec65a0683e3', - fieldMetadataUniversalIdentifier: - PULL_REQUEST_REVIEW_EVENT_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - direction: ViewSortDirection.DESC, - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/events-on-review.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/events-on-review.field.ts deleted file mode 100644 index e09fadfa0a..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/events-on-review.field.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; -import { PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/objects/pull-request-review.object'; -import { PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/objects/pull-request-review-event.object'; -import { REVIEW_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/fields/review-on-review-event.field'; - -export const EVENTS_ON_REVIEW_FIELD_UNIVERSAL_IDENTIFIER = - '846f70d2-5a2b-41e1-b10c-628f874837f2'; - -export default defineField({ - universalIdentifier: EVENTS_ON_REVIEW_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'reviewEvents', - label: 'Review Events', - icon: 'IconHistory', - relationTargetObjectMetadataUniversalIdentifier: - PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - REVIEW_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/pull-request-on-review.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/pull-request-on-review.field.ts deleted file mode 100644 index 687b40ef2d..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/pull-request-on-review.field.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - defineField, - FieldType, - RelationType, - OnDeleteAction, -} from 'twenty-sdk/define'; -import { PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/objects/pull-request-review.object'; -import { PULL_REQUEST_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/objects/pull-request.object'; -import { REVIEWS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/fields/reviews-on-pull-request.field'; - -export const PULL_REQUEST_ON_REVIEW_FIELD_UNIVERSAL_IDENTIFIER = - '5cc6fd4f-400f-421b-8c24-ffbf08077dd8'; - -export default defineField({ - universalIdentifier: PULL_REQUEST_ON_REVIEW_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'pullRequest', - label: 'Pull Request', - icon: 'IconGitPullRequest', - relationTargetObjectMetadataUniversalIdentifier: - PULL_REQUEST_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - REVIEWS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: OnDeleteAction.CASCADE, - joinColumnName: 'pullRequestId', - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/reviewer-on-review.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/reviewer-on-review.field.ts deleted file mode 100644 index e3225ccf1a..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/fields/reviewer-on-review.field.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - defineField, - FieldType, - RelationType, - OnDeleteAction, -} from 'twenty-sdk/define'; -import { PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/objects/pull-request-review.object'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { REVIEWS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/fields/reviews-on-contributor.field'; - -export const REVIEWER_FIELD_UNIVERSAL_IDENTIFIER = - '2272ecc4-90e1-4c66-96bd-a7ac20280943'; - -export default defineField({ - universalIdentifier: REVIEWER_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'reviewer', - label: 'Reviewer', - icon: 'IconUser', - relationTargetObjectMetadataUniversalIdentifier: - CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - REVIEWS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: OnDeleteAction.SET_NULL, - joinColumnName: 'reviewerId', - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/graphql/mutations/batch-upsert.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/graphql/mutations/batch-upsert.ts deleted file mode 100644 index f27fde9374..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/graphql/mutations/batch-upsert.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { chunkedBatchCreate } from 'src/modules/shared/twenty-client'; -import type { PullRequestReviewRow } from 'src/modules/github/pull-request-review/types/pull-request-review-row'; -import type { ConsolidatedReviewUpsertInput } from 'src/modules/github/pull-request-review/utils/build-consolidated-row'; - -export async function batchUpsertConsolidatedReviews( - items: ConsolidatedReviewUpsertInput[], -): Promise { - return chunkedBatchCreate('createPullRequestReviews', items, { - id: true, - reviewKey: true, - title: true, - state: true, - firstSubmittedAt: true, - lastSubmittedAt: true, - eventCount: true, - reviewerId: true, - pullRequestId: true, - isSelfReview: true, - }) as Promise; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/logic-functions/recompute-pull-request-reviews.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/logic-functions/recompute-pull-request-reviews.logic-function.ts deleted file mode 100644 index feb9e4d03f..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/logic-functions/recompute-pull-request-reviews.logic-function.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { getClient } from 'src/modules/shared/twenty-client'; -import { timed } from 'src/modules/shared/timing'; -import { batchUpsertConsolidatedReviews } from 'src/modules/github/pull-request-review/graphql/mutations/batch-upsert'; -import { - buildConsolidatedRow, - type ConsolidatedReviewUpsertInput, -} from 'src/modules/github/pull-request-review/utils/build-consolidated-row'; -import type { ReviewEventState } from 'src/modules/github/pull-request-review/utils/consolidate-reviews'; - -const PAGE_SIZE = 100; - -type Edge = { node: T }; -type Connection = { - edges: Edge[]; - pageInfo: { hasNextPage: boolean; endCursor: string | null }; -}; - -type ReviewEventNode = { - state: ReviewEventState; - submittedAt: string | null; - reviewerId: string | null; - pullRequestId: string | null; - reviewer: { ghLogin: string | null } | null; - pullRequest: { - githubNumber: number | null; - authorId: string | null; - } | null; -}; - -type GroupContext = { - pullRequestId: string; - reviewerId: string | null; - prNumber: number | null; - reviewerLogin: string | null; - prAuthorId: string | null; - events: { state: ReviewEventState; submittedAt: string | null }[]; -}; - -async function* paginate( - fetchPage: (cursor: string | null) => Promise>, -): AsyncGenerator { - let cursor: string | null = null; - let hasMore = true; - while (hasMore) { - const conn = await fetchPage(cursor); - yield conn.edges.map((e) => e.node); - hasMore = conn.pageInfo.hasNextPage; - cursor = conn.pageInfo.endCursor; - if (!cursor) break; - } -} - -const handler = async (_event: RoutePayload) => { - const handlerStart = Date.now(); - const client = getClient(); - - const groups = new Map(); - let eventCount = 0; - let skippedNoPr = 0; - - await timed('recompute-reviews:scan-events', async () => { - for await (const batch of paginate(async (cursor) => { - const res = await client.query({ - pullRequestReviewEvents: { - __args: { first: PAGE_SIZE, after: cursor }, - edges: { - node: { - state: true, - submittedAt: true, - reviewerId: true, - pullRequestId: true, - reviewer: { ghLogin: true }, - pullRequest: { githubNumber: true, authorId: true }, - }, - }, - pageInfo: { hasNextPage: true, endCursor: true }, - }, - }); - return ( - (res.pullRequestReviewEvents as Connection) ?? { - edges: [], - pageInfo: { hasNextPage: false, endCursor: null }, - } - ); - })) { - for (const node of batch) { - eventCount++; - if (!node.pullRequestId) { - skippedNoPr++; - continue; - } - const key = `${node.pullRequestId}:${node.reviewerId ?? 'unknown'}`; - let group = groups.get(key); - if (!group) { - group = { - pullRequestId: node.pullRequestId, - reviewerId: node.reviewerId, - prNumber: node.pullRequest?.githubNumber ?? null, - reviewerLogin: node.reviewer?.ghLogin ?? null, - prAuthorId: node.pullRequest?.authorId ?? null, - events: [], - }; - groups.set(key, group); - } - if (!group.reviewerLogin && node.reviewer?.ghLogin) { - group.reviewerLogin = node.reviewer.ghLogin; - } - if (group.prNumber === null && node.pullRequest?.githubNumber != null) { - group.prNumber = node.pullRequest.githubNumber; - } - if (group.prAuthorId === null && node.pullRequest?.authorId) { - group.prAuthorId = node.pullRequest.authorId; - } - group.events.push({ - state: node.state, - submittedAt: node.submittedAt, - }); - } - } - }); - - const rows: ConsolidatedReviewUpsertInput[] = []; - for (const group of groups.values()) { - if (group.events.length === 0) continue; - rows.push( - buildConsolidatedRow({ - pullRequestId: group.pullRequestId, - reviewerId: group.reviewerId, - prNumber: group.prNumber, - reviewerLogin: group.reviewerLogin, - events: group.events, - prAuthorId: group.prAuthorId, - }), - ); - } - - let upsertedCount = 0; - if (rows.length > 0) { - const recs = await timed(`recompute-reviews:upsert (${rows.length})`, () => - batchUpsertConsolidatedReviews(rows), - ); - upsertedCount = recs.length; - } - - const totalMs = Date.now() - handlerStart; - console.log( - `[recompute-reviews] done in ${totalMs}ms (events=${eventCount}, groups=${groups.size}, upserted=${upsertedCount}, skippedNoPr=${skippedNoPr})`, - ); - - return { - eventCount, - groupCount: groups.size, - upsertedCount, - skippedNoPr, - durationMs: totalMs, - }; -}; - -export default defineLogicFunction({ - universalIdentifier: 'fd2cbfa6-89a2-40f4-9164-0f658525cf07', - name: 'recompute-pull-request-reviews', - description: - 'Backfill/repeat-safe: scans every PullRequestReviewEvent, groups by (pullRequestId, reviewerId), and upserts the consolidated PullRequestReview row using the "first substantive wins" rule.', - timeoutSeconds: 300, - handler, - httpRouteTriggerSettings: { - path: '/pull-request-reviews/recompute', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/navigation-menu-items/pull-request-reviews.navigation-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/navigation-menu-items/pull-request-reviews.navigation-menu-item.ts deleted file mode 100644 index ef843310a2..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/navigation-menu-items/pull-request-reviews.navigation-menu-item.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { - defineNavigationMenuItem, - NavigationMenuItemType, -} from 'twenty-sdk/define'; -import { GITHUB_FOLDER_UNIVERSAL_IDENTIFIER } from 'src/modules/github/navigation-menu-items/github-folder.navigation-menu-item'; -import { PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/objects/pull-request-review.object'; - -export default defineNavigationMenuItem({ - universalIdentifier: '1f2e3d4c-5b6a-4798-8abc-def012345678', - position: 4, - type: NavigationMenuItemType.OBJECT, - targetObjectUniversalIdentifier: PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - folderUniversalIdentifier: GITHUB_FOLDER_UNIVERSAL_IDENTIFIER, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/objects/pull-request-review.object.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/objects/pull-request-review.object.ts deleted file mode 100644 index f4f0242cfc..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/objects/pull-request-review.object.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { defineObject, FieldType } from 'twenty-sdk/define'; - -export const PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER = - '3c9b62b5-88a6-4a05-8e62-7637987cc8bb'; - -export const REVIEW_TITLE_FIELD_UNIVERSAL_IDENTIFIER = - '0d0f01f1-bdb2-4dc9-a915-a2eacecdedce'; - -export const REVIEW_KEY_FIELD_UNIVERSAL_IDENTIFIER = - 'f96c80a8-d42f-414e-8eab-5c5684c0b948'; - -export const REVIEW_STATE_FIELD_UNIVERSAL_IDENTIFIER = - 'b93fabcc-67ea-4532-847c-2b015fe90ee5'; - -export const REVIEW_FIRST_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER = - 'd7157b19-b173-41ca-96a5-98b7cc4f379e'; - -export const REVIEW_LAST_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER = - 'ea73e23e-6010-4cb4-b9e0-9dfe572e4b69'; - -export const REVIEW_EVENT_COUNT_FIELD_UNIVERSAL_IDENTIFIER = - '5bca3165-8033-41da-96bb-bf381e6db45f'; - -export const PULL_REQUEST_REVIEW_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER = - 'b4d61187-1b78-5d6e-9752-79f994ff6d55'; - -export const REVIEW_IS_SELF_REVIEW_FIELD_UNIVERSAL_IDENTIFIER = - 'c1f3e8a2-9b5d-4e7f-8a6c-2d4b6f8e1a93'; - -enum ReviewState { - APPROVED = 'APPROVED', - CHANGES_REQUESTED = 'CHANGES_REQUESTED', - COMMENTED = 'COMMENTED', - DISMISSED = 'DISMISSED', -} - -export default defineObject({ - universalIdentifier: PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - nameSingular: 'pullRequestReview', - namePlural: 'pullRequestReviews', - labelSingular: 'Pull Request Review', - labelPlural: 'Pull Request Reviews', - icon: 'IconEye', - labelIdentifierFieldMetadataUniversalIdentifier: - REVIEW_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - fields: [ - { - universalIdentifier: REVIEW_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'title', - type: FieldType.TEXT, - label: 'Title', - icon: 'IconTextCaption', - }, - { - universalIdentifier: REVIEW_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'reviewKey', - type: FieldType.TEXT, - label: 'Review Key', - icon: 'IconKey', - isUnique: true, - }, - { - universalIdentifier: REVIEW_STATE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'state', - type: FieldType.SELECT, - label: 'State', - icon: 'IconCircleDot', - options: [ - { - value: ReviewState.APPROVED, - label: 'Approved', - position: 0, - color: 'green', - }, - { - value: ReviewState.CHANGES_REQUESTED, - label: 'Changes Requested', - position: 1, - color: 'red', - }, - { - value: ReviewState.COMMENTED, - label: 'Commented', - position: 2, - color: 'blue', - }, - { - value: ReviewState.DISMISSED, - label: 'Dismissed', - position: 3, - color: 'gray', - }, - ], - }, - { - universalIdentifier: - REVIEW_FIRST_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - name: 'firstSubmittedAt', - type: FieldType.DATE_TIME, - label: 'First Submitted At', - icon: 'IconCalendar', - isNullable: true, - defaultValue: null, - }, - { - universalIdentifier: REVIEW_LAST_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - name: 'lastSubmittedAt', - type: FieldType.DATE_TIME, - label: 'Last Submitted At', - icon: 'IconCalendarTime', - isNullable: true, - defaultValue: null, - }, - { - universalIdentifier: REVIEW_EVENT_COUNT_FIELD_UNIVERSAL_IDENTIFIER, - name: 'eventCount', - type: FieldType.NUMBER, - label: 'Event Count', - icon: 'IconHash', - defaultValue: 0, - }, - { - universalIdentifier: REVIEW_IS_SELF_REVIEW_FIELD_UNIVERSAL_IDENTIFIER, - name: 'isSelfReview', - type: FieldType.BOOLEAN, - label: 'Self review', - description: - 'True when the reviewer is the same contributor as the PR author. Self reviews are excluded from review-count aggregations (top reviewers, contributor stats, etc.) so contributors are credited only for reviews on other people\u2019s PRs.', - icon: 'IconUserCheck', - defaultValue: false, - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/types/pull-request-review-row.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/types/pull-request-review-row.ts deleted file mode 100644 index 7493fb8cb4..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/types/pull-request-review-row.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type PullRequestReviewRow = { - id: string; - title?: string | null; - reviewKey?: string | null; - state?: string | null; - firstSubmittedAt?: string | null; - lastSubmittedAt?: string | null; - eventCount?: number | null; - reviewerId?: string | null; - pullRequestId?: string | null; - isSelfReview?: boolean | null; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/build-consolidated-row.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/build-consolidated-row.ts deleted file mode 100644 index 30331aad3c..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/build-consolidated-row.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - consolidateReviews, - type ReviewEventForConsolidation, -} from 'src/modules/github/pull-request-review/utils/consolidate-reviews'; - -export type ConsolidatedReviewUpsertInput = { - reviewKey: string; - title: string; - state: string; - firstSubmittedAt: string | null; - lastSubmittedAt: string | null; - eventCount: number; - reviewerId: string | null; - pullRequestId: string; - isSelfReview: boolean; -}; - -export type BuildConsolidatedRowParams = { - pullRequestId: string; - reviewerId: string | null; - prNumber: number | null; - reviewerLogin: string | null; - events: ReviewEventForConsolidation[]; - /** - * Author of the PR being reviewed. When non-null and equal to `reviewerId` - * the consolidated row is flagged as a self-review so downstream - * aggregations (top reviewers, contributor stats, etc.) can exclude it. - */ - prAuthorId?: string | null; -}; - -export const isSelfReview = ( - reviewerId: string | null, - prAuthorId: string | null | undefined, -): boolean => - reviewerId !== null && - prAuthorId !== null && - prAuthorId !== undefined && - reviewerId === prAuthorId; - -export const buildReviewKey = ( - pullRequestId: string, - reviewerId: string | null, -): string => `${pullRequestId}:${reviewerId ?? 'unknown'}`; - -export const buildConsolidatedTitle = ( - reviewerLogin: string | null, - prNumber: number | null, -): string => { - const login = reviewerLogin ?? 'unknown'; - const number = typeof prNumber === 'number' ? prNumber : '?'; - return `${login} on PR #${number}`; -}; - -export const buildConsolidatedRow = ( - params: BuildConsolidatedRowParams, -): ConsolidatedReviewUpsertInput => { - const verdict = consolidateReviews(params.events); - return { - reviewKey: buildReviewKey(params.pullRequestId, params.reviewerId), - title: buildConsolidatedTitle(params.reviewerLogin, params.prNumber), - state: verdict.state, - firstSubmittedAt: verdict.firstSubmittedAt, - lastSubmittedAt: verdict.lastSubmittedAt, - eventCount: verdict.eventCount, - reviewerId: params.reviewerId, - pullRequestId: params.pullRequestId, - isSelfReview: isSelfReview(params.reviewerId, params.prAuthorId ?? null), - }; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/consolidate-reviews.integration-test.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/consolidate-reviews.integration-test.ts deleted file mode 100644 index e200d2039a..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/consolidate-reviews.integration-test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { - consolidateReviews, - type ReviewEventForConsolidation, -} from 'src/modules/github/pull-request-review/utils/consolidate-reviews'; -import { - buildConsolidatedRow, - buildReviewKey, - buildConsolidatedTitle, - isSelfReview, -} from 'src/modules/github/pull-request-review/utils/build-consolidated-row'; - -const evt = ( - state: ReviewEventForConsolidation['state'], - submittedAt: string | null, -): ReviewEventForConsolidation => ({ state, submittedAt }); - -describe('consolidateReviews', () => { - it('throws on empty input', () => { - expect(() => consolidateReviews([])).toThrow(); - }); - - it('returns a verdict for a single COMMENTED event', () => { - const verdict = consolidateReviews([evt('COMMENTED', '2025-04-01T10:00:00Z')]); - expect(verdict).toEqual({ - state: 'COMMENTED', - firstSubmittedAt: '2025-04-01T10:00:00Z', - lastSubmittedAt: '2025-04-01T10:00:00Z', - eventCount: 1, - }); - }); - - it('picks the earliest substantive event when COMMENTED → APPROVED → COMMENTED', () => { - const verdict = consolidateReviews([ - evt('COMMENTED', '2025-04-01T10:00:00Z'), - evt('APPROVED', '2025-04-02T10:00:00Z'), - evt('COMMENTED', '2025-04-03T10:00:00Z'), - ]); - expect(verdict).toEqual({ - state: 'APPROVED', - firstSubmittedAt: '2025-04-02T10:00:00Z', - lastSubmittedAt: '2025-04-03T10:00:00Z', - eventCount: 3, - }); - }); - - it('keeps CHANGES_REQUESTED when it precedes APPROVED', () => { - const verdict = consolidateReviews([ - evt('CHANGES_REQUESTED', '2025-04-01T08:00:00Z'), - evt('APPROVED', '2025-04-05T08:00:00Z'), - ]); - expect(verdict.state).toBe('CHANGES_REQUESTED'); - expect(verdict.firstSubmittedAt).toBe('2025-04-01T08:00:00Z'); - expect(verdict.lastSubmittedAt).toBe('2025-04-05T08:00:00Z'); - expect(verdict.eventCount).toBe(2); - }); - - it('falls back to first event when only DISMISSED entries exist', () => { - const verdict = consolidateReviews([ - evt('DISMISSED', '2025-04-02T08:00:00Z'), - evt('DISMISSED', '2025-04-03T08:00:00Z'), - ]); - expect(verdict.state).toBe('DISMISSED'); - expect(verdict.firstSubmittedAt).toBe('2025-04-02T08:00:00Z'); - expect(verdict.lastSubmittedAt).toBe('2025-04-03T08:00:00Z'); - expect(verdict.eventCount).toBe(2); - }); - - it('handles only COMMENTED entries', () => { - const verdict = consolidateReviews([ - evt('COMMENTED', '2025-04-01T10:00:00Z'), - evt('COMMENTED', '2025-04-02T10:00:00Z'), - ]); - expect(verdict.state).toBe('COMMENTED'); - expect(verdict.firstSubmittedAt).toBe('2025-04-01T10:00:00Z'); - expect(verdict.lastSubmittedAt).toBe('2025-04-02T10:00:00Z'); - }); - - it('is order-independent', () => { - const a = consolidateReviews([ - evt('COMMENTED', '2025-04-01T10:00:00Z'), - evt('APPROVED', '2025-04-02T10:00:00Z'), - ]); - const b = consolidateReviews([ - evt('APPROVED', '2025-04-02T10:00:00Z'), - evt('COMMENTED', '2025-04-01T10:00:00Z'), - ]); - expect(a).toEqual(b); - }); - - it('handles null submittedAt by treating it as the earliest', () => { - const verdict = consolidateReviews([ - evt('APPROVED', '2025-04-01T10:00:00Z'), - evt('COMMENTED', null), - ]); - expect(verdict.eventCount).toBe(2); - expect(verdict.state).toBe('APPROVED'); - }); -}); - -describe('buildReviewKey', () => { - it('joins pullRequestId and reviewerId with a colon', () => { - expect(buildReviewKey('pr-1', 'rev-2')).toBe('pr-1:rev-2'); - }); - - it('uses "unknown" when reviewerId is null', () => { - expect(buildReviewKey('pr-1', null)).toBe('pr-1:unknown'); - }); -}); - -describe('buildConsolidatedTitle', () => { - it('formats reviewer login + PR number', () => { - expect(buildConsolidatedTitle('alice', 42)).toBe('alice on PR #42'); - }); - - it('falls back gracefully on missing data', () => { - expect(buildConsolidatedTitle(null, null)).toBe('unknown on PR #?'); - }); -}); - -describe('buildConsolidatedRow', () => { - it('produces the expected upsert payload', () => { - const row = buildConsolidatedRow({ - pullRequestId: 'pr-1', - reviewerId: 'rev-2', - prNumber: 7, - reviewerLogin: 'alice', - events: [ - evt('COMMENTED', '2025-04-01T10:00:00Z'), - evt('APPROVED', '2025-04-02T10:00:00Z'), - evt('COMMENTED', '2025-04-03T10:00:00Z'), - ], - }); - expect(row).toEqual({ - reviewKey: 'pr-1:rev-2', - title: 'alice on PR #7', - state: 'APPROVED', - firstSubmittedAt: '2025-04-02T10:00:00Z', - lastSubmittedAt: '2025-04-03T10:00:00Z', - eventCount: 3, - reviewerId: 'rev-2', - pullRequestId: 'pr-1', - isSelfReview: false, - }); - }); - - it('flags isSelfReview when prAuthorId equals reviewerId', () => { - const row = buildConsolidatedRow({ - pullRequestId: 'pr-1', - reviewerId: 'alice', - prNumber: 7, - reviewerLogin: 'alice', - prAuthorId: 'alice', - events: [evt('COMMENTED', '2025-04-01T10:00:00Z')], - }); - expect(row.isSelfReview).toBe(true); - }); - - it('does not flag isSelfReview when prAuthorId differs from reviewerId', () => { - const row = buildConsolidatedRow({ - pullRequestId: 'pr-1', - reviewerId: 'alice', - prNumber: 7, - reviewerLogin: 'alice', - prAuthorId: 'bob', - events: [evt('APPROVED', '2025-04-01T10:00:00Z')], - }); - expect(row.isSelfReview).toBe(false); - }); - - it('does not flag isSelfReview when prAuthorId is missing', () => { - const row = buildConsolidatedRow({ - pullRequestId: 'pr-1', - reviewerId: 'alice', - prNumber: 7, - reviewerLogin: 'alice', - events: [evt('APPROVED', '2025-04-01T10:00:00Z')], - }); - expect(row.isSelfReview).toBe(false); - }); - - it('does not flag isSelfReview when reviewerId is null (ghost reviewer)', () => { - const row = buildConsolidatedRow({ - pullRequestId: 'pr-1', - reviewerId: null, - prNumber: 7, - reviewerLogin: null, - prAuthorId: null, - events: [evt('COMMENTED', '2025-04-01T10:00:00Z')], - }); - expect(row.isSelfReview).toBe(false); - }); -}); - -describe('isSelfReview', () => { - it('returns true only when both ids are present and equal', () => { - expect(isSelfReview('a', 'a')).toBe(true); - }); - - it('returns false when ids differ', () => { - expect(isSelfReview('a', 'b')).toBe(false); - }); - - it('returns false when either side is null/undefined', () => { - expect(isSelfReview(null, 'a')).toBe(false); - expect(isSelfReview('a', null)).toBe(false); - expect(isSelfReview('a', undefined)).toBe(false); - expect(isSelfReview(null, null)).toBe(false); - }); -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/consolidate-reviews.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/consolidate-reviews.ts deleted file mode 100644 index c27f8ce421..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/utils/consolidate-reviews.ts +++ /dev/null @@ -1,54 +0,0 @@ -export type ReviewEventState = - | 'APPROVED' - | 'CHANGES_REQUESTED' - | 'COMMENTED' - | 'DISMISSED'; - -export type ReviewEventForConsolidation = { - state: ReviewEventState; - submittedAt: string | null; -}; - -export type ConsolidatedVerdict = { - state: ReviewEventState; - firstSubmittedAt: string | null; - lastSubmittedAt: string | null; - eventCount: number; -}; - -const SUBSTANTIVE_STATES: ReadonlyArray = [ - 'APPROVED', - 'CHANGES_REQUESTED', -]; - -const isSubstantive = (state: ReviewEventState): boolean => - SUBSTANTIVE_STATES.includes(state); - -const compareSubmittedAt = ( - a: ReviewEventForConsolidation, - b: ReviewEventForConsolidation, -): number => { - const av = a.submittedAt ?? ''; - const bv = b.submittedAt ?? ''; - if (av === bv) return 0; - return av < bv ? -1 : 1; -}; - -export const consolidateReviews = ( - events: ReviewEventForConsolidation[], -): ConsolidatedVerdict => { - if (events.length === 0) { - throw new Error('consolidateReviews called with empty events'); - } - - const sorted = [...events].sort(compareSubmittedAt); - const substantive = sorted.find((e) => isSubstantive(e.state)); - const chosen = substantive ?? sorted[0]; - - return { - state: chosen.state, - firstSubmittedAt: chosen.submittedAt, - lastSubmittedAt: sorted[sorted.length - 1].submittedAt, - eventCount: sorted.length, - }; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/views/all-pull-request-reviews.view.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/views/all-pull-request-reviews.view.ts deleted file mode 100644 index 71a05c4f0e..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request-review/views/all-pull-request-reviews.view.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - defineView, - ViewKey, - ViewSortDirection, - ViewType, -} from 'twenty-sdk/define'; -import { - PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - REVIEW_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - REVIEW_STATE_FIELD_UNIVERSAL_IDENTIFIER, - REVIEW_FIRST_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - REVIEW_LAST_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - REVIEW_EVENT_COUNT_FIELD_UNIVERSAL_IDENTIFIER, - PULL_REQUEST_REVIEW_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, -} from 'src/modules/github/pull-request-review/objects/pull-request-review.object'; -import { REVIEWER_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/fields/reviewer-on-review.field'; -import { PULL_REQUEST_ON_REVIEW_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/fields/pull-request-on-review.field'; - -export const ALL_REVIEWS_VIEW_UNIVERSAL_IDENTIFIER = - '3559d020-bdf4-424b-9b6a-bc3cbb3d4502'; - -export default defineView({ - universalIdentifier: ALL_REVIEWS_VIEW_UNIVERSAL_IDENTIFIER, - name: 'All Reviews', - objectUniversalIdentifier: PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - type: ViewType.TABLE, - icon: 'IconEye', - key: ViewKey.INDEX, - position: 0, - fields: [ - { - universalIdentifier: '63010877-3fd3-43f0-a105-66168a0c9782', - fieldMetadataUniversalIdentifier: - REVIEW_TITLE_FIELD_UNIVERSAL_IDENTIFIER, - position: 0, - isVisible: true, - size: 240, - }, - { - universalIdentifier: 'c7f3ad93-e4bc-47d6-a3c3-664d9975bb60', - fieldMetadataUniversalIdentifier: - REVIEW_STATE_FIELD_UNIVERSAL_IDENTIFIER, - position: 1, - isVisible: true, - size: 150, - }, - { - universalIdentifier: 'ab0cd954-95e6-4e29-8a73-2ad614dc6b1f', - fieldMetadataUniversalIdentifier: REVIEWER_FIELD_UNIVERSAL_IDENTIFIER, - position: 2, - isVisible: true, - size: 160, - }, - { - universalIdentifier: '43440edf-ef04-413a-a39c-3ae70b839166', - fieldMetadataUniversalIdentifier: - PULL_REQUEST_ON_REVIEW_FIELD_UNIVERSAL_IDENTIFIER, - position: 3, - isVisible: true, - size: 200, - }, - { - universalIdentifier: '20ddfe2f-0310-4c01-af85-b7c75d801e26', - fieldMetadataUniversalIdentifier: - REVIEW_FIRST_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - position: 4, - isVisible: true, - size: 170, - }, - { - universalIdentifier: '4e274558-49e9-465a-98c3-44177c6138c3', - fieldMetadataUniversalIdentifier: - REVIEW_LAST_SUBMITTED_AT_FIELD_UNIVERSAL_IDENTIFIER, - position: 5, - isVisible: true, - size: 170, - }, - { - universalIdentifier: 'd2964ab2-7e05-4ee0-9219-b88cfef78230', - fieldMetadataUniversalIdentifier: - REVIEW_EVENT_COUNT_FIELD_UNIVERSAL_IDENTIFIER, - position: 6, - isVisible: true, - size: 110, - }, - ], - sorts: [ - { - universalIdentifier: 'add26365-5060-411e-b0a7-1ce32155d4f1', - fieldMetadataUniversalIdentifier: - PULL_REQUEST_REVIEW_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - direction: ViewSortDirection.DESC, - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/command-menu-items/fetch-prs.command-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/command-menu-items/fetch-prs.command-menu-item.ts deleted file mode 100644 index 06d7110b72..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/command-menu-items/fetch-prs.command-menu-item.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineCommandMenuItem, objectMetadataItem } from 'twenty-sdk/define'; - -export default defineCommandMenuItem({ - universalIdentifier: '0b24e6d6-da0c-4c0e-8d88-5bfd7f3cd75a', - frontComponentUniversalIdentifier: '5e2ba8df-1db5-4964-94d3-44cccfd791a0', - label: 'Fetch Pull Requests', - icon: 'IconGitPullRequest', - isPinned: false, - conditionalAvailabilityExpression: - objectMetadataItem.nameSingular === 'pullRequest', -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/author-on-pull-request.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/author-on-pull-request.field.ts deleted file mode 100644 index 7b9c97c9e3..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/author-on-pull-request.field.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; -import { PULL_REQUEST_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/objects/pull-request.object'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { AUTHORED_PRS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/fields/authored-prs-on-contributor.field'; - -export const AUTHOR_FIELD_UNIVERSAL_IDENTIFIER = - 'b2c3d4e5-1a2b-4c3d-8e5f-6a7b8c9d0e1f'; - -export default defineField({ - universalIdentifier: AUTHOR_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'author', - label: 'Author', - icon: 'IconUser', - relationTargetObjectMetadataUniversalIdentifier: - CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - AUTHORED_PRS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: OnDeleteAction.SET_NULL, - joinColumnName: 'authorId', - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/merger-on-pull-request.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/merger-on-pull-request.field.ts deleted file mode 100644 index 0bc08d9232..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/merger-on-pull-request.field.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineField, FieldType, RelationType, OnDeleteAction } from 'twenty-sdk/define'; -import { PULL_REQUEST_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/objects/pull-request.object'; -import { CONTRIBUTOR_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/objects/contributor.object'; -import { MERGED_PULL_REQUESTS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/contributor/fields/merged-pull-requests-on-contributor.field'; - -export const MERGER_FIELD_UNIVERSAL_IDENTIFIER = - '3a4a6af0-a6a7-4cb6-a91d-273a2842449e'; - -export default defineField({ - universalIdentifier: MERGER_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'merger', - label: 'Merger', - icon: 'IconUser', - relationTargetObjectMetadataUniversalIdentifier: - CONTRIBUTOR_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - MERGED_PULL_REQUESTS_ON_CONTRIBUTOR_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.MANY_TO_ONE, - onDelete: OnDeleteAction.SET_NULL, - joinColumnName: 'mergerId', - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/project-items-on-pull-request.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/project-items-on-pull-request.field.ts deleted file mode 100644 index 01af097177..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/project-items-on-pull-request.field.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; -import { PULL_REQUEST_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/objects/pull-request.object'; -import { PROJECT_ITEM_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/objects/project-item.object'; -import { LINKED_PR_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/project-item/fields/linked-pr-on-project-item.field'; - -export const PROJECT_ITEMS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER = - 'ad5e6f7a-8b9c-4d0e-bf1a-2b3c4d5e6f7a'; - -export default defineField({ - universalIdentifier: PROJECT_ITEMS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'projectItems', - label: 'Project Items', - icon: 'IconLayoutKanban', - relationTargetObjectMetadataUniversalIdentifier: - PROJECT_ITEM_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - LINKED_PR_ON_PROJECT_ITEM_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/review-events-on-pull-request.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/review-events-on-pull-request.field.ts deleted file mode 100644 index 2517afd737..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/review-events-on-pull-request.field.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; -import { PULL_REQUEST_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/objects/pull-request.object'; -import { PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/objects/pull-request-review-event.object'; -import { PULL_REQUEST_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review-event/fields/pull-request-on-review-event.field'; - -export const REVIEW_EVENTS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER = - '299b3ef8-cba0-4f16-a785-81728df09c46'; - -export default defineField({ - universalIdentifier: REVIEW_EVENTS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'reviewEvents', - label: 'Review Events', - icon: 'IconEye', - relationTargetObjectMetadataUniversalIdentifier: - PULL_REQUEST_REVIEW_EVENT_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - PULL_REQUEST_ON_REVIEW_EVENT_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/reviews-on-pull-request.field.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/reviews-on-pull-request.field.ts deleted file mode 100644 index c38cee3654..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/fields/reviews-on-pull-request.field.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineField, FieldType, RelationType } from 'twenty-sdk/define'; -import { PULL_REQUEST_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/objects/pull-request.object'; -import { PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/objects/pull-request-review.object'; -import { PULL_REQUEST_ON_REVIEW_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request-review/fields/pull-request-on-review.field'; - -export const REVIEWS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER = - '9a089f03-76fe-40ba-9a4d-39b84ba11d03'; - -export default defineField({ - universalIdentifier: REVIEWS_ON_PR_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - type: FieldType.RELATION, - name: 'reviews', - label: 'Reviews', - icon: 'IconEye', - relationTargetObjectMetadataUniversalIdentifier: - PULL_REQUEST_REVIEW_UNIVERSAL_IDENTIFIER, - relationTargetFieldMetadataUniversalIdentifier: - PULL_REQUEST_ON_REVIEW_FIELD_UNIVERSAL_IDENTIFIER, - universalSettings: { - relationType: RelationType.ONE_TO_MANY, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/front-components/fetch-prs.front-component.tsx b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/front-components/fetch-prs.front-component.tsx deleted file mode 100644 index d19ec28d72..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/front-components/fetch-prs.front-component.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useEffect, useState } from 'react'; -import { defineFrontComponent } from 'twenty-sdk/define'; -import { - enqueueSnackbar, - unmountFrontComponent, - updateProgress, -} from 'twenty-sdk/front-component'; -import { callAppRoute } from 'src/modules/shared/call-app-route'; -import { retry } from 'src/modules/shared/retry'; - -type CountResponse = { - totalPages: number; - repos: Array<{ owner: string; repo: string; totalCount: number; pages: number }>; -}; - -type FetchPageResponse = { - prCount: number; - reviewCount: number; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}; - -type SyncStatus = 'syncing' | 'done' | 'error'; - -const FetchPrs = () => { - const [status, setStatus] = useState('syncing'); - - useEffect(() => { - const run = async () => { - try { - const counts = (await callAppRoute( - '/github/count-prs', - {}, - )) as CountResponse; - - if (counts.repos.length === 0) { - throw new Error( - 'No repos resolved. Set GITHUB_REPOS in the application variables.', - ); - } - - const totalPages = Math.max(counts.totalPages, 1); - let pagesProcessed = 0; - let totalPrs = 0; - let totalReviews = 0; - - for (const { owner, repo } of counts.repos) { - let cursor: string | null = null; - let hasMore = true; - - while (hasMore) { - const cursorTag = cursor ? `@${cursor.slice(0, 8)}` : ''; - const data = (await retry( - `fetch-prs ${owner}/${repo}${cursorTag}`, - () => - callAppRoute('/github/fetch-prs', { - owner, - repo, - cursor, - }), - )) as FetchPageResponse; - - totalPrs += data.prCount; - totalReviews += data.reviewCount; - hasMore = data.hasMore && data.prCount > 0; - cursor = data.endCursor; - pagesProcessed++; - - updateProgress(Math.min(Math.round((pagesProcessed / totalPages) * 100), 99)); - } - } - - updateProgress(100); - enqueueSnackbar({ - message: `Fetched ${totalPrs} PRs and ${totalReviews} reviews`, - variant: 'success', - }); - setStatus('done'); - } catch (err) { - const message = - err instanceof Error ? err.message : 'Failed to fetch pull requests'; - enqueueSnackbar({ message, variant: 'error' }); - setStatus('error'); - } finally { - unmountFrontComponent(); - } - }; - run(); - }, []); - - if (status === 'syncing') return
Fetching pull requests...
; - if (status === 'error') return
Failed to fetch pull requests.
; - return
Done
; -}; - -export default defineFrontComponent({ - universalIdentifier: '5e2ba8df-1db5-4964-94d3-44cccfd791a0', - name: 'Fetch Pull Requests', - description: 'Fetches pull requests and reviews from GitHub repos', - isHeadless: true, - component: FetchPrs, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/github/count-pull-requests.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/github/count-pull-requests.ts deleted file mode 100644 index a167242ef3..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/github/count-pull-requests.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { githubGraphql } from 'src/modules/github/connector/github-client'; - -const QUERY = ` -query($owner: String!, $name: String!) { - repository(owner: $owner, name: $name) { - pullRequests(states: [OPEN, CLOSED, MERGED]) { - totalCount - } - } -}`; - -type Response = { - repository: { pullRequests: { totalCount: number } } | null; -}; - -export async function countPullRequests( - owner: string, - name: string, -): Promise { - try { - const data = await githubGraphql(QUERY, { owner, name }); - return data.repository?.pullRequests.totalCount ?? 0; - } catch { - return 0; - } -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/github/fetch-pull-requests.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/github/fetch-pull-requests.ts deleted file mode 100644 index 622fd74f4f..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/github/fetch-pull-requests.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { - EMPTY_PAGE, - type GithubPage, - githubGraphql, -} from 'src/modules/github/connector/github-client'; - -const QUERY = ` -query($owner: String!, $name: String!, $cursor: String) { - repository(owner: $owner, name: $name) { - pullRequests(first: 100, states: [OPEN, CLOSED, MERGED], orderBy: {field: CREATED_AT, direction: DESC}, after: $cursor) { - totalCount - pageInfo { hasNextPage endCursor } - nodes { - number - title - url - state - merged - mergedAt - closedAt - createdAt - author { login avatarUrl ... on User { databaseId } } - mergedBy { login avatarUrl ... on User { databaseId } } - reviews(first: 100) { - nodes { - databaseId - state - submittedAt - author { login avatarUrl ... on User { databaseId } } - } - } - } - } - } -}`; - -export type GqlPullRequest = { - number: number; - title: string; - url: string; - state: 'OPEN' | 'CLOSED' | 'MERGED'; - merged: boolean; - mergedAt: string | null; - closedAt: string | null; - createdAt: string; - author: { login: string; avatarUrl?: string | null; databaseId?: number } | null; - mergedBy: { login: string; avatarUrl?: string | null; databaseId?: number } | null; - reviews: { - nodes: Array<{ - databaseId: number; - state: 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'DISMISSED'; - submittedAt: string | null; - author: { login: string; avatarUrl?: string | null; databaseId?: number } | null; - }>; - }; -}; - -type Response = { - repository: { pullRequests: GithubPage } | null; -}; - -export async function fetchPullRequests( - owner: string, - name: string, - cursor: string | null = null, -): Promise<{ - prs: GqlPullRequest[]; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}> { - const data = await githubGraphql(QUERY, { owner, name, cursor }); - const conn = data.repository?.pullRequests; - if (!conn) return { prs: [], ...EMPTY_PAGE }; - - return { - prs: conn.nodes, - totalCount: conn.totalCount, - hasMore: conn.pageInfo.hasNextPage, - endCursor: conn.pageInfo.endCursor, - }; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/mutations/batch-upsert.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/mutations/batch-upsert.ts deleted file mode 100644 index 92d4241c4f..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/mutations/batch-upsert.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { chunkedBatchCreate } from 'src/modules/shared/twenty-client'; -import type { LinksFieldValue } from 'src/modules/shared/types'; -import type { PullRequestRow } from 'src/modules/github/pull-request/types/pull-request-row'; - -export async function batchUpsertPullRequests( - items: Array<{ - githubNumber: number; - uniqueIdentifier: string; - name: string; - url: LinksFieldValue; - state: string; - mergedAt: string | null; - closedAt: string | null; - githubCreatedAt: string | null; - authorId: string | null; - mergerId: string | null; - }>, -): Promise { - return chunkedBatchCreate('createPullRequests', items, { - id: true, - githubNumber: true, - uniqueIdentifier: true, - name: true, - state: true, - }) as Promise; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/mutations/update-pull-request.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/mutations/update-pull-request.ts deleted file mode 100644 index d9db8d7304..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/mutations/update-pull-request.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getClient } from 'src/modules/shared/twenty-client'; - -export async function updatePullRequest( - id: string, - data: Record, -): Promise { - const client = getClient(); - await client.mutation({ - updatePullRequest: { - __args: { id, data }, - id: true, - }, - }); -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/queries/find-by-github-number.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/queries/find-by-github-number.ts deleted file mode 100644 index f58fedf1b5..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/graphql/queries/find-by-github-number.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getClient } from 'src/modules/shared/twenty-client'; -import type { PullRequestRow } from 'src/modules/github/pull-request/types/pull-request-row'; -import { buildPrUniqueIdentifier } from 'src/modules/github/pull-request/normalizers'; - -const PR_NODE_SELECTION = { - id: true, - name: true, - githubNumber: true, - uniqueIdentifier: true, - url: { primaryLinkLabel: true, primaryLinkUrl: true }, - state: true, - mergedAt: true, - closedAt: true, - githubCreatedAt: true, - authorId: true, - mergerId: true, -} as const; - -export async function findPullRequestByRepoAndNumber( - repo: string, - githubNumber: number, -): Promise { - const client = getClient(); - const uniqueIdentifier = buildPrUniqueIdentifier(repo, githubNumber); - const res = await client.query({ - pullRequests: { - __args: { - filter: { uniqueIdentifier: { eq: uniqueIdentifier } }, - first: 1, - }, - edges: { node: PR_NODE_SELECTION }, - }, - }); - - const node = res.pullRequests?.edges?.[0]?.node; - return (node as PullRequestRow | undefined) ?? null; -} - -export async function findPullRequestByGithubNumber( - githubNumber: number, -): Promise { - const client = getClient(); - const res = await client.query({ - pullRequests: { - __args: { - filter: { githubNumber: { eq: githubNumber } }, - first: 1, - }, - edges: { node: PR_NODE_SELECTION }, - }, - }); - - const node = res.pullRequests?.edges?.[0]?.node; - return (node as PullRequestRow | undefined) ?? null; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/logic-functions/count-prs.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/logic-functions/count-prs.logic-function.ts deleted file mode 100644 index 86b0080855..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/logic-functions/count-prs.logic-function.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { countAcrossRepos } from 'src/modules/github/connector/count-across-repos'; -import { countPullRequests } from 'src/modules/github/pull-request/graphql/github/count-pull-requests'; - -type CountPrsPayload = { - repos?: string[]; -}; - -const handler = async (event: RoutePayload) => - countAcrossRepos(event.body?.repos, countPullRequests, 'count-prs'); - -export default defineLogicFunction({ - universalIdentifier: '082227ae-2acc-4320-8d31-62ad6c443da6', - name: 'count-prs', - description: - 'Counts total PR pages across configured repos using GraphQL totalCount', - timeoutSeconds: 30, - handler, - httpRouteTriggerSettings: { - path: '/github/count-prs', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/logic-functions/fetch-prs.logic-function.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/logic-functions/fetch-prs.logic-function.ts deleted file mode 100644 index 9a9af41978..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/logic-functions/fetch-prs.logic-function.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { defineLogicFunction } from 'twenty-sdk/define'; -import { type RoutePayload } from 'twenty-sdk/logic-function'; -import { - fetchPullRequests, - type GqlPullRequest, -} from 'src/modules/github/pull-request/graphql/github/fetch-pull-requests'; -import { batchUpsertContributors } from 'src/modules/github/contributor/graphql/mutations/batch-upsert'; -import { batchUpsertPullRequests } from 'src/modules/github/pull-request/graphql/mutations/batch-upsert'; -import { batchUpsertReviewEvents } from 'src/modules/github/pull-request-review-event/graphql/mutations/batch-upsert'; -import { batchUpsertConsolidatedReviews } from 'src/modules/github/pull-request-review/graphql/mutations/batch-upsert'; -import { buildConsolidatedRow } from 'src/modules/github/pull-request-review/utils/build-consolidated-row'; -import { dedupeContributors } from 'src/modules/github/contributor/normalizers'; -import { pullRequestFromGraphql } from 'src/modules/github/pull-request/normalizers'; -import { - reviewEventFromGraphql, - type ReviewEventState, -} from 'src/modules/github/pull-request-review-event/normalizers'; -import { timed } from 'src/modules/shared/timing'; -import { isFixtureAllowed } from 'src/modules/shared/fixtures'; - -export type FetchPrsFixturePage = { - prs: GqlPullRequest[]; - totalCount: number; - hasMore: boolean; - endCursor: string | null; -}; - -type FetchPrsPayload = { - owner: string; - repo: string; - cursor?: string | null; - fixturePage?: FetchPrsFixturePage; -}; - -const handler = async (event: RoutePayload) => { - const handlerStart = Date.now(); - const { owner, repo, cursor = null, fixturePage } = event.body ?? {}; - if (!owner || !repo) { - return { error: 'owner and repo are required' }; - } - - const tag = `${owner}/${repo}${cursor ? `@${cursor.slice(0, 8)}` : ''}`; - console.log(`[fetch-prs] start ${tag}${fixturePage ? ' (fixture)' : ''}`); - - const result = - fixturePage && isFixtureAllowed() - ? fixturePage - : await timed(`fetch-prs:github ${tag}`, () => - fetchPullRequests(owner, repo, cursor), - ); - const { prs, totalCount, hasMore, endCursor } = result; - - if (prs.length === 0) { - console.log(`[fetch-prs] empty page for ${tag}`); - return { - prCount: 0, - reviewCount: 0, - totalCount, - hasMore: false, - endCursor: null, - }; - } - - const allUsers = prs.flatMap((pr) => [ - pr.author, - pr.mergedBy, - ...pr.reviews.nodes.map((r) => r.author), - ]); - const contributorInputs = dedupeContributors(allUsers); - const contributors = await timed( - `fetch-prs:upsertContributors ${tag} (${contributorInputs.length})`, - () => batchUpsertContributors(contributorInputs), - ); - - const idByLogin = new Map(); - for (const c of contributors) { - if (c.ghLogin) idByLogin.set(c.ghLogin, c.id); - } - - const fullRepo = `${owner}/${repo}`; - - const prData = prs.map((pr) => ({ - ...pullRequestFromGraphql(pr, fullRepo), - authorId: pr.author ? (idByLogin.get(pr.author.login) ?? null) : null, - mergerId: pr.mergedBy ? (idByLogin.get(pr.mergedBy.login) ?? null) : null, - })); - - const prRecords = await timed( - `fetch-prs:upsertPullRequests ${tag} (${prData.length})`, - () => batchUpsertPullRequests(prData), - ); - - const prIdByNumber = new Map(); - for (const rec of prRecords) { - if (rec.githubNumber) prIdByNumber.set(rec.githubNumber, rec.id); - } - - type ReviewEventInput = { - githubReviewId: number; - title: string; - state: string; - submittedAt: string | null; - reviewerId: string | null; - pullRequestId: string; - }; - - type GroupKey = string; - type GroupContext = { - pullRequestId: string; - reviewerId: string | null; - prNumber: number; - reviewerLogin: string | null; - prAuthorId: string | null; - events: { state: ReviewEventState; submittedAt: string | null }[]; - }; - - const authorIdByPullRequestId = new Map(); - for (const pr of prData) { - const id = prIdByNumber.get(pr.githubNumber); - if (id) authorIdByPullRequestId.set(id, pr.authorId); - } - - let skippedReviews = 0; - const reviewEventData: ReviewEventInput[] = []; - const groups = new Map(); - - for (const pr of prs) { - const pullRequestId = prIdByNumber.get(pr.number); - if (!pullRequestId) { - skippedReviews += pr.reviews.nodes.length; - continue; - } - for (const review of pr.reviews.nodes) { - const reviewerId = review.author - ? (idByLogin.get(review.author.login) ?? null) - : null; - const canonical = reviewEventFromGraphql(review); - reviewEventData.push({ - ...canonical, - reviewerId, - pullRequestId, - }); - const key = `${pullRequestId}:${reviewerId ?? 'unknown'}`; - let group = groups.get(key); - if (!group) { - group = { - pullRequestId, - reviewerId, - prNumber: pr.number, - reviewerLogin: review.author?.login ?? null, - prAuthorId: authorIdByPullRequestId.get(pullRequestId) ?? null, - events: [], - }; - groups.set(key, group); - } - if (!group.reviewerLogin && review.author?.login) { - group.reviewerLogin = review.author.login; - } - group.events.push({ - state: canonical.state, - submittedAt: canonical.submittedAt, - }); - } - } - - if (skippedReviews > 0) { - console.log( - `[fetch-prs] skipped ${skippedReviews} reviews with no matching PR for ${tag}`, - ); - } - - if (reviewEventData.length > 0) { - await timed( - `fetch-prs:upsertReviewEvents ${tag} (${reviewEventData.length})`, - () => batchUpsertReviewEvents(reviewEventData), - ); - } - - let consolidatedCount = 0; - if (groups.size > 0) { - const consolidatedRows = Array.from(groups.values()).map((group) => - buildConsolidatedRow({ - pullRequestId: group.pullRequestId, - reviewerId: group.reviewerId, - prNumber: group.prNumber, - reviewerLogin: group.reviewerLogin, - events: group.events, - prAuthorId: group.prAuthorId, - }), - ); - const consolidatedRecords = await timed( - `fetch-prs:upsertConsolidatedReviews ${tag} (${consolidatedRows.length})`, - () => batchUpsertConsolidatedReviews(consolidatedRows), - ); - consolidatedCount = consolidatedRecords.length; - - const reviewIdByKey = new Map(); - for (const rec of consolidatedRecords) { - if (rec.reviewKey && rec.id) reviewIdByKey.set(rec.reviewKey, rec.id); - } - - const eventBackfill: ReviewEventInput[] = reviewEventData - .map((evt) => { - const key = `${evt.pullRequestId}:${evt.reviewerId ?? 'unknown'}`; - const reviewId = reviewIdByKey.get(key); - if (!reviewId) return null; - return { ...evt, reviewId }; - }) - .filter((x): x is ReviewEventInput & { reviewId: string } => x !== null); - - if (eventBackfill.length > 0) { - await timed( - `fetch-prs:backfillReviewIds ${tag} (${eventBackfill.length})`, - () => batchUpsertReviewEvents(eventBackfill), - ); - } - } - - const totalMs = Date.now() - handlerStart; - console.log( - `[fetch-prs] done ${tag} in ${totalMs}ms (prs=${prs.length}, reviewEvents=${reviewEventData.length}, consolidatedReviews=${consolidatedCount}, hasMore=${hasMore})`, - ); - - return { - prCount: prs.length, - reviewCount: reviewEventData.length, - consolidatedReviewCount: consolidatedCount, - totalCount, - hasMore, - endCursor, - }; -}; - -export default defineLogicFunction({ - universalIdentifier: 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d', - name: 'fetch-prs', - description: - 'Fetches one page of pull requests with reviews via GitHub GraphQL API and batch upserts them', - timeoutSeconds: 300, - handler, - httpRouteTriggerSettings: { - path: '/github/fetch-prs', - httpMethod: 'POST', - isAuthRequired: true, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/navigation-menu-items/pull-requests.navigation-menu-item.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/navigation-menu-items/pull-requests.navigation-menu-item.ts deleted file mode 100644 index 3c83dbf892..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/navigation-menu-items/pull-requests.navigation-menu-item.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineNavigationMenuItem, NavigationMenuItemType } from 'twenty-sdk/define'; -import { PULL_REQUEST_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/objects/pull-request.object'; -import { GITHUB_FOLDER_UNIVERSAL_IDENTIFIER } from 'src/modules/github/navigation-menu-items/github-folder.navigation-menu-item'; - -export default defineNavigationMenuItem({ - universalIdentifier: 'bafa56a5-fd74-4c6f-ab44-d933877edb93', - position: 0, - type: NavigationMenuItemType.OBJECT, - targetObjectUniversalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - folderUniversalIdentifier: GITHUB_FOLDER_UNIVERSAL_IDENTIFIER, -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/normalizers.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/normalizers.ts deleted file mode 100644 index 9c7a20c50d..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/normalizers.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { LinksFieldValue } from 'src/modules/shared/types'; -import { toLinksField } from 'src/modules/shared/types'; -import type { GitHubPullRequest } from 'src/modules/github/pull-request/types/github-pull-request'; -import type { GqlPullRequest } from 'src/modules/github/pull-request/graphql/github/fetch-pull-requests'; - -export type PullRequestState = 'OPEN' | 'CLOSED' | 'MERGED'; - -export type PullRequestCanonical = { - githubNumber: number; - uniqueIdentifier: string; - name: string; - url: LinksFieldValue; - state: PullRequestState; - mergedAt: string | null; - closedAt: string | null; - githubCreatedAt: string | null; -}; - -export function derivePrState( - merged: boolean | null | undefined, - state: string, -): PullRequestState { - if (merged) return 'MERGED'; - return state.toUpperCase() === 'CLOSED' ? 'CLOSED' : 'OPEN'; -} - -export function buildPrUniqueIdentifier( - repoFullName: string, - number: number, -): string { - return `${repoFullName}#${number}`; -} - -export function pullRequestFromWebhook( - pr: GitHubPullRequest, - repoFullName: string, -): PullRequestCanonical { - return { - githubNumber: pr.number, - uniqueIdentifier: buildPrUniqueIdentifier(repoFullName, pr.number), - name: pr.title, - url: toLinksField(pr.html_url, pr.title), - state: derivePrState(pr.merged, pr.state), - mergedAt: pr.merged_at, - closedAt: pr.closed_at, - githubCreatedAt: pr.created_at, - }; -} - -export function pullRequestFromGraphql( - pr: GqlPullRequest, - repoFullName: string, -): PullRequestCanonical { - return { - githubNumber: pr.number, - uniqueIdentifier: buildPrUniqueIdentifier(repoFullName, pr.number), - name: pr.title, - url: toLinksField(pr.url, pr.title), - state: derivePrState(pr.merged, pr.state), - mergedAt: pr.mergedAt, - closedAt: pr.closedAt, - githubCreatedAt: pr.createdAt, - }; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/objects/pull-request.object.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/objects/pull-request.object.ts deleted file mode 100644 index 452b36d2f1..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/objects/pull-request.object.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { defineObject, FieldType } from 'twenty-sdk/define'; - -export const PULL_REQUEST_UNIVERSAL_IDENTIFIER = - 'bf539871-f118-4f35-b953-fa2ba528aea3'; - -export const PULL_REQUEST_NAME_FIELD_UNIVERSAL_IDENTIFIER = - '5b394c0a-14db-4582-9513-6671661d64a5'; - -export const MERGED_AT_FIELD_UNIVERSAL_IDENTIFIER = - '064b38ec-ae2e-4d1b-bae9-d8d18329c8cd'; - -export const GITHUB_NUMBER_FIELD_UNIVERSAL_IDENTIFIER = - 'a4b5c6d7-8e9f-4a0b-b1c2-d3e4f5a6b7c8'; - -export const URL_FIELD_UNIVERSAL_IDENTIFIER = - 'd4c3b2a1-6f5e-4d3c-9b8a-7f6e5d4c3b2a'; - -export const PR_UNIQUE_IDENTIFIER_FIELD_UNIVERSAL_IDENTIFIER = - '23b446f8-199a-4be4-881a-a475570b9590'; - -enum PullRequestState { - OPEN = 'OPEN', - CLOSED = 'CLOSED', - MERGED = 'MERGED', -} - -export const PR_STATE_FIELD_UNIVERSAL_IDENTIFIER = - 'e4f5a6b7-0c1d-4e2f-a3b4-c5d6e7f8a9b0'; - -export const PR_CLOSED_AT_FIELD_UNIVERSAL_IDENTIFIER = - 'f5a6b7c8-1d2e-4f3a-b4c5-d6e7f8a9b0c1'; - -export const PR_GITHUB_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER = - 'e38deee8-c57f-4b40-8c35-2b5d1fee759e'; - -export const PULL_REQUEST_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER = - '78d142cf-7033-565d-b802-860bde5b0078'; - -export default defineObject({ - universalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - nameSingular: 'pullRequest', - namePlural: 'pullRequests', - labelSingular: 'Pull Request', - labelPlural: 'Pull Requests', - icon: 'IconTank', - labelIdentifierFieldMetadataUniversalIdentifier: - PULL_REQUEST_NAME_FIELD_UNIVERSAL_IDENTIFIER, - fields: [ - { - universalIdentifier: PULL_REQUEST_NAME_FIELD_UNIVERSAL_IDENTIFIER, - name: 'name', - type: FieldType.TEXT, - label: 'Name', - icon: 'IconGitPullRequest', - }, - { - universalIdentifier: GITHUB_NUMBER_FIELD_UNIVERSAL_IDENTIFIER, - name: 'githubNumber', - type: FieldType.NUMBER, - label: 'PR Number', - icon: 'IconHash', - }, - { - universalIdentifier: URL_FIELD_UNIVERSAL_IDENTIFIER, - name: 'url', - type: FieldType.LINKS, - label: 'URL', - icon: 'IconLink', - }, - { - universalIdentifier: PR_STATE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'state', - type: FieldType.SELECT, - label: 'State', - icon: 'IconCircleDot', - options: [ - { - value: PullRequestState.OPEN, - label: 'Open', - position: 0, - color: 'green', - }, - { - value: PullRequestState.CLOSED, - label: 'Closed', - position: 1, - color: 'red', - }, - { - value: PullRequestState.MERGED, - label: 'Merged', - position: 2, - color: 'purple', - }, - ], - }, - { - universalIdentifier: MERGED_AT_FIELD_UNIVERSAL_IDENTIFIER, - name: 'mergedAt', - type: FieldType.DATE_TIME, - label: 'Merged At', - icon: 'IconCalendarCheck', - isNullable: true, - defaultValue: null, - }, - { - universalIdentifier: PR_CLOSED_AT_FIELD_UNIVERSAL_IDENTIFIER, - name: 'closedAt', - type: FieldType.DATE_TIME, - label: 'Closed At', - icon: 'IconCalendarOff', - isNullable: true, - defaultValue: null, - }, - { - universalIdentifier: PR_GITHUB_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - name: 'githubCreatedAt', - type: FieldType.DATE_TIME, - label: 'GitHub Created At', - icon: 'IconCalendarPlus', - isNullable: true, - defaultValue: null, - }, - { - universalIdentifier: PR_UNIQUE_IDENTIFIER_FIELD_UNIVERSAL_IDENTIFIER, - name: 'uniqueIdentifier', - type: FieldType.TEXT, - label: 'Unique Identifier', - icon: 'IconFingerprint', - isUnique: true, - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/types/github-pull-request.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/types/github-pull-request.ts deleted file mode 100644 index 2a8e84dcd7..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/types/github-pull-request.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { GitHubUser } from 'src/modules/github/connector/github-user'; - -export type GitHubPullRequest = { - number: number; - title: string; - html_url: string; - state: 'open' | 'closed'; - merged: boolean | null; - merged_at: string | null; - closed_at: string | null; - created_at: string; - user: GitHubUser; - merged_by: GitHubUser | null; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/types/pull-request-row.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/types/pull-request-row.ts deleted file mode 100644 index 446481e0f7..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/types/pull-request-row.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { LinksFieldValue } from 'src/modules/shared/types'; - -export type PullRequestRow = { - id: string; - name?: string | null; - githubNumber?: number | null; - uniqueIdentifier?: string | null; - url?: LinksFieldValue | null; - state?: string | null; - mergedAt?: string | null; - closedAt?: string | null; - githubCreatedAt?: string | null; - authorId?: string | null; - mergerId?: string | null; -}; diff --git a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/views/all-pull-requests.view.ts b/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/views/all-pull-requests.view.ts deleted file mode 100644 index a37ae5dfee..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/github/pull-request/views/all-pull-requests.view.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - defineView, - ViewKey, - ViewSortDirection, - ViewType, -} from 'twenty-sdk/define'; -import { - PULL_REQUEST_UNIVERSAL_IDENTIFIER, - PULL_REQUEST_NAME_FIELD_UNIVERSAL_IDENTIFIER, - GITHUB_NUMBER_FIELD_UNIVERSAL_IDENTIFIER, - PR_STATE_FIELD_UNIVERSAL_IDENTIFIER, - MERGED_AT_FIELD_UNIVERSAL_IDENTIFIER, - PULL_REQUEST_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, -} from 'src/modules/github/pull-request/objects/pull-request.object'; -import { AUTHOR_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/fields/author-on-pull-request.field'; -import { MERGER_FIELD_UNIVERSAL_IDENTIFIER } from 'src/modules/github/pull-request/fields/merger-on-pull-request.field'; - -export const ALL_PULL_REQUESTS_VIEW_UNIVERSAL_IDENTIFIER = - 'e6a0bd60-08be-4973-81de-0e15ce8f9bf8'; - -export default defineView({ - universalIdentifier: ALL_PULL_REQUESTS_VIEW_UNIVERSAL_IDENTIFIER, - name: 'All Pull Requests', - objectUniversalIdentifier: PULL_REQUEST_UNIVERSAL_IDENTIFIER, - type: ViewType.TABLE, - icon: 'IconTank', - key: ViewKey.INDEX, - position: 0, - fields: [ - { - universalIdentifier: 'fd4fc905-f099-4553-8043-f677d9fbe0e0', - fieldMetadataUniversalIdentifier: - PULL_REQUEST_NAME_FIELD_UNIVERSAL_IDENTIFIER, - position: 0, - isVisible: true, - size: 250, - }, - { - universalIdentifier: '8848460a-5cb3-455d-91a3-d1916293d5f0', - fieldMetadataUniversalIdentifier: - GITHUB_NUMBER_FIELD_UNIVERSAL_IDENTIFIER, - position: 1, - isVisible: true, - size: 100, - }, - { - universalIdentifier: 'c05e00b7-a18a-44fb-970e-27b0b6ecdc7f', - fieldMetadataUniversalIdentifier: - PR_STATE_FIELD_UNIVERSAL_IDENTIFIER, - position: 2, - isVisible: true, - size: 120, - }, - { - universalIdentifier: 'f2834200-06c5-4702-b44d-ac04a292444c', - fieldMetadataUniversalIdentifier: AUTHOR_FIELD_UNIVERSAL_IDENTIFIER, - position: 3, - isVisible: true, - size: 160, - }, - { - universalIdentifier: '93d10703-194f-4e38-bdeb-813097f19d50', - fieldMetadataUniversalIdentifier: MERGER_FIELD_UNIVERSAL_IDENTIFIER, - position: 4, - isVisible: true, - size: 160, - }, - { - universalIdentifier: 'f90a284f-034a-42e8-9a08-38d22f5da983', - fieldMetadataUniversalIdentifier: MERGED_AT_FIELD_UNIVERSAL_IDENTIFIER, - position: 5, - isVisible: true, - size: 180, - }, - ], - sorts: [ - { - universalIdentifier: '7e7497fd-5bcd-43f8-bece-36e82f9f8f3f', - fieldMetadataUniversalIdentifier: - PULL_REQUEST_CREATED_AT_FIELD_UNIVERSAL_IDENTIFIER, - direction: ViewSortDirection.DESC, - }, - ], -}); diff --git a/packages/twenty-apps/community/github-connector/src/modules/shared/call-app-route.ts b/packages/twenty-apps/community/github-connector/src/modules/shared/call-app-route.ts deleted file mode 100644 index 814b05074e..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/shared/call-app-route.ts +++ /dev/null @@ -1,50 +0,0 @@ -const getEnv = (key: string): string | undefined => { - if (typeof process === 'undefined') return undefined; - return process.env?.[key]; -}; - -type ErrorResponse = { - messages?: string[]; - message?: string; - error?: string; - statusCode?: number; -}; - -function extractErrorMessage(text: string, status: number): string { - try { - const json = JSON.parse(text) as ErrorResponse; - if (json.messages?.length) return json.messages[0]; - return json.message ?? json.error ?? `Server error (${status})`; - } catch { - return text || `Server error (${status})`; - } -} - -export async function callAppRoute( - path: string, - body: Record, -): Promise { - const apiUrl = getEnv('TWENTY_API_URL') ?? ''; - const token = getEnv('TWENTY_APP_ACCESS_TOKEN'); - - const headers: Record = { - 'Content-Type': 'application/json', - }; - - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - - const res = await fetch(`${apiUrl}/s${path}`, { - method: 'POST', - headers, - body: JSON.stringify(body), - }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(extractErrorMessage(text, res.status)); - } - - return res.json(); -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/shared/fixtures.ts b/packages/twenty-apps/community/github-connector/src/modules/shared/fixtures.ts deleted file mode 100644 index a848b3f491..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/shared/fixtures.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function isFixtureAllowed(): boolean { - return process.env.NODE_ENV !== 'production'; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/shared/retry.ts b/packages/twenty-apps/community/github-connector/src/modules/shared/retry.ts deleted file mode 100644 index c3fa2e58d8..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/shared/retry.ts +++ /dev/null @@ -1,49 +0,0 @@ -export type RetryOptions = { - retries?: number; - baseDelayMs?: number; - maxDelayMs?: number; - shouldRetry?: (err: unknown) => boolean; -}; - -const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); - -export async function retry( - label: string, - fn: () => Promise, - options: RetryOptions = {}, -): Promise { - const requestedRetries = options.retries ?? 3; - const retries = Math.max( - Math.floor(Number.isFinite(requestedRetries) ? requestedRetries : 1), - 1, - ); - const baseDelayMs = options.baseDelayMs ?? 500; - const maxDelayMs = options.maxDelayMs ?? 5_000; - const shouldRetry = options.shouldRetry ?? (() => true); - - let lastErr: unknown; - for (let attempt = 1; attempt <= retries; attempt++) { - try { - return await fn(); - } catch (err) { - lastErr = err; - const msg = err instanceof Error ? err.message : String(err); - - if (attempt === retries || !shouldRetry(err)) { - console.log( - `[retry] ${label} attempt ${attempt}/${retries} failed (giving up): ${msg}`, - ); - throw err; - } - - const delay = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1)); - const jittered = delay + Math.floor(Math.random() * baseDelayMs); - console.log( - `[retry] ${label} attempt ${attempt}/${retries} failed, retrying in ${jittered}ms: ${msg}`, - ); - await sleep(jittered); - } - } - - throw lastErr; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/shared/timing.ts b/packages/twenty-apps/community/github-connector/src/modules/shared/timing.ts deleted file mode 100644 index f24b3c9d5c..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/shared/timing.ts +++ /dev/null @@ -1,17 +0,0 @@ -export async function timed( - label: string, - fn: () => Promise, -): Promise { - const start = Date.now(); - try { - const result = await fn(); - const ms = Date.now() - start; - console.log(`[timing] ${label} ok in ${ms}ms`); - return result; - } catch (err) { - const ms = Date.now() - start; - const msg = err instanceof Error ? err.message : String(err); - console.log(`[timing] ${label} FAILED in ${ms}ms: ${msg}`); - throw err; - } -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/shared/twenty-client.ts b/packages/twenty-apps/community/github-connector/src/modules/shared/twenty-client.ts deleted file mode 100644 index 20c4309a81..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/shared/twenty-client.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; - -export const getClient = () => new CoreApiClient(); - -const BATCH_CHUNK_SIZE = 50; - -export async function chunkedBatchCreate>( - mutationName: string, - items: T[], - returnFields: Record>, -): Promise[]> { - if (items.length === 0) return []; - - const client = getClient(); - const results: Record[] = []; - const totalChunks = Math.ceil(items.length / BATCH_CHUNK_SIZE); - const overallStart = Date.now(); - - for (let i = 0; i < items.length; i += BATCH_CHUNK_SIZE) { - const chunk = items.slice(i, i + BATCH_CHUNK_SIZE); - const chunkIndex = Math.floor(i / BATCH_CHUNK_SIZE) + 1; - const chunkStart = Date.now(); - - const res = await client.mutation({ - [mutationName]: { - __args: { data: chunk, upsert: true }, - ...returnFields, - }, - }); - - const chunkMs = Date.now() - chunkStart; - console.log( - `[timing] ${mutationName} chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows) in ${chunkMs}ms`, - ); - - const rows = res[mutationName]; - if (Array.isArray(rows)) { - results.push(...(rows as Record[])); - } - } - - const totalMs = Date.now() - overallStart; - console.log( - `[timing] ${mutationName} total ${items.length} rows in ${totalMs}ms (${totalChunks} chunk(s))`, - ); - - return results; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/shared/types.ts b/packages/twenty-apps/community/github-connector/src/modules/shared/types.ts deleted file mode 100644 index 46ed72233a..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/shared/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type LinksFieldValue = { - primaryLinkLabel: string; - primaryLinkUrl: string; - secondaryLinks: null; -}; - -export function toLinksField(url: string, label = ''): LinksFieldValue { - return { primaryLinkLabel: label, primaryLinkUrl: url, secondaryLinks: null }; -} diff --git a/packages/twenty-apps/community/github-connector/src/modules/shared/universal-identifiers.ts b/packages/twenty-apps/community/github-connector/src/modules/shared/universal-identifiers.ts deleted file mode 100644 index cf4da82f3f..0000000000 --- a/packages/twenty-apps/community/github-connector/src/modules/shared/universal-identifiers.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const APP_DISPLAY_NAME = 'GitHub Connector'; -export const APP_DESCRIPTION = - 'Sync pull requests, reviews, issues, contributors and Projects (v2) items from GitHub into Twenty.'; -export const APP_ABOUT_DESCRIPTION = [ - 'Bring your GitHub activity into Twenty as native records you can view, filter, group and report on alongside the rest of your CRM data.', - '', - '**What gets synced**', - '- Pull requests, pull-request reviews and review events', - '- Issues', - '- Contributors (with avatars, profile links and bot detection)', - '- Projects (v2) items, including custom field values', - '', - '**How it stays in sync**', - '- Real-time updates via GitHub webhooks', - '- On-demand backfills via one-click commands on every record and view', - '- Works with any number of repositories and organizations', - '', - '**Authentication**', - '- GitHub App with private-key JWT (recommended for orgs)', - '- Fine-grained Personal Access Token (great for quick setups)', - '', - '**Out of the box**', - '- A GitHub Dashboard with PR / review counters, weekly histograms and top-contributor leaderboards', - '- A Contributor Stats panel on every contributor record (PRs authored, merged and reviewed over time)', - '- Pre-built views for pull requests, reviews, issues and project items', -].join('\n'); -export const APPLICATION_UNIVERSAL_IDENTIFIER = - '0c5b6bf2-2b41-4d5e-9f8a-3c4e6d7a8b9c'; -export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = - '4e7d8a5f-1b32-4f6a-bc91-2d9c8b7a6e54'; diff --git a/packages/twenty-apps/community/github-connector/tsconfig.json b/packages/twenty-apps/community/github-connector/tsconfig.json deleted file mode 100644 index d574c8c810..0000000000 --- a/packages/twenty-apps/community/github-connector/tsconfig.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "compileOnSave": false, - "compilerOptions": { - "sourceMap": true, - "declaration": true, - "outDir": "./dist", - "rootDir": ".", - "jsx": "react-jsx", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "importHelpers": true, - "allowUnreachableCode": false, - "strict": true, - "alwaysStrict": true, - "noImplicitAny": true, - "strictBindCallApply": false, - "target": "es2018", - "module": "esnext", - "lib": ["es2020", "dom"], - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "resolveJsonModule": true, - "paths": { - "src/*": ["./src/*"], - "~/*": ["./*"] - } - }, - "exclude": [ - "node_modules", - "dist", - "**/*.test.ts", - "**/*.spec.ts", - "**/*.integration-test.ts" - ], - "references": [ - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/packages/twenty-apps/community/github-connector/tsconfig.spec.json b/packages/twenty-apps/community/github-connector/tsconfig.spec.json deleted file mode 100644 index ea69a6c010..0000000000 --- a/packages/twenty-apps/community/github-connector/tsconfig.spec.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "composite": true, - "types": ["vitest/globals", "node"] - }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/twenty-apps/community/github-connector/vitest.config.ts b/packages/twenty-apps/community/github-connector/vitest.config.ts deleted file mode 100644 index ae8883e69f..0000000000 --- a/packages/twenty-apps/community/github-connector/vitest.config.ts +++ /dev/null @@ -1,30 +0,0 @@ -import tsconfigPaths from 'vite-tsconfig-paths'; -import { defineConfig } from 'vitest/config'; - -const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2021'; -const TWENTY_API_KEY = - process.env.TWENTY_API_KEY ?? - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc'; - -process.env.TWENTY_API_URL = TWENTY_API_URL; -process.env.TWENTY_API_KEY = TWENTY_API_KEY; - -export default defineConfig({ - plugins: [ - tsconfigPaths({ - projects: ['tsconfig.spec.json'], - ignoreConfigErrors: true, - }), - ], - test: { - testTimeout: 120_000, - hookTimeout: 120_000, - fileParallelism: false, - include: ['src/**/*.integration-test.ts'], - globalSetup: ['src/__tests__/global-setup.ts'], - env: { - TWENTY_API_URL, - TWENTY_API_KEY, - }, - }, -}); diff --git a/packages/twenty-apps/community/github-connector/yarn.lock b/packages/twenty-apps/community/github-connector/yarn.lock deleted file mode 100644 index fd892ceba2..0000000000 --- a/packages/twenty-apps/community/github-connector/yarn.lock +++ /dev/null @@ -1,2816 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 8 - cacheKey: 10c0 - -"@alcalzone/ansi-tokenize@npm:^0.2.4": - version: 0.2.5 - resolution: "@alcalzone/ansi-tokenize@npm:0.2.5" - dependencies: - ansi-styles: "npm:^6.2.1" - is-fullwidth-code-point: "npm:^5.0.0" - checksum: 10c0/dd8622288426b5b7dbf8b68d51d4b930cea591d0e3b9dbc3f523131464d78ac922c165fcb7ba5307f133d35dbcaa0e6648a1c3fb6a0dc2725546d6f867b70af4 - languageName: node - linkType: hard - -"@emnapi/core@npm:1.10.0": - version: 1.10.0 - resolution: "@emnapi/core@npm:1.10.0" - dependencies: - "@emnapi/wasi-threads": "npm:1.2.1" - tslib: "npm:^2.4.0" - checksum: 10c0/f51d08227857b60632de7714d708124f0e100a1462dde6df8221760939aa3204a73193830371830fac0716f3ccd2129f2cac1b17cd7d7958bc4da9018a296edb - languageName: node - linkType: hard - -"@emnapi/runtime@npm:1.10.0": - version: 1.10.0 - resolution: "@emnapi/runtime@npm:1.10.0" - dependencies: - tslib: "npm:^2.4.0" - checksum: 10c0/953f14991d1aefb92ee6f8eb27dea725e484791a53a0cb5f47d9e0087b9a2c929ff2e92adf95af15d6ad456db6300c6b761ebf72b50a875b874a83520b3ba093 - languageName: node - linkType: hard - -"@emnapi/wasi-threads@npm:1.2.1": - version: 1.2.1 - resolution: "@emnapi/wasi-threads@npm:1.2.1" - dependencies: - tslib: "npm:^2.4.0" - checksum: 10c0/32fcfa81ab396533b2ec1f4082b1ff779a05d9c836bbbd3f4398405b0e6814c0d9503b7993130e37bc6941dbc1ded49f55e9700ae9ca4e803bab2b5bc5deb331 - languageName: node - linkType: hard - -"@esbuild/aix-ppc64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/aix-ppc64@npm:0.28.1" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - -"@esbuild/android-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/android-arm64@npm:0.28.1" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/android-arm@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/android-arm@npm:0.28.1" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - -"@esbuild/android-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/android-x64@npm:0.28.1" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/darwin-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/darwin-arm64@npm:0.28.1" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/darwin-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/darwin-x64@npm:0.28.1" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/freebsd-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/freebsd-arm64@npm:0.28.1" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/freebsd-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/freebsd-x64@npm:0.28.1" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/linux-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-arm64@npm:0.28.1" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/linux-arm@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-arm@npm:0.28.1" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@esbuild/linux-ia32@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-ia32@npm:0.28.1" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - -"@esbuild/linux-loong64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-loong64@npm:0.28.1" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - -"@esbuild/linux-mips64el@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-mips64el@npm:0.28.1" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - -"@esbuild/linux-ppc64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-ppc64@npm:0.28.1" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - -"@esbuild/linux-riscv64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-riscv64@npm:0.28.1" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - -"@esbuild/linux-s390x@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-s390x@npm:0.28.1" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - -"@esbuild/linux-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-x64@npm:0.28.1" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/netbsd-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/netbsd-arm64@npm:0.28.1" - conditions: os=netbsd & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/netbsd-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/netbsd-x64@npm:0.28.1" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/openbsd-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/openbsd-arm64@npm:0.28.1" - conditions: os=openbsd & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/openbsd-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/openbsd-x64@npm:0.28.1" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/openharmony-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/openharmony-arm64@npm:0.28.1" - conditions: os=openharmony & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/sunos-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/sunos-x64@npm:0.28.1" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/win32-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/win32-arm64@npm:0.28.1" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/win32-ia32@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/win32-ia32@npm:0.28.1" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"@esbuild/win32-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/win32-x64@npm:0.28.1" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@genql/runtime@npm:^2.10.0": - version: 2.10.0 - resolution: "@genql/runtime@npm:2.10.0" - dependencies: - "@types/qs": "npm:^6.9.0" - "@types/ws": "npm:^6.0.1" - graphql-query-batcher: "npm:^1.0.1" - isomorphic-unfetch: "npm:^3.0.0" - lodash: "npm:^4.17.20" - subscriptions-transport-ws: "npm:^0.9.16" - tslib: "npm:^2.0.0" - utility-types: "npm:^3.10.0" - ws: "npm:^6.1.4" - zen-observable-ts: "npm:^0.8.21" - peerDependencies: - graphql: "*" - checksum: 10c0/e2a886c2469c933681e2b0ddd6a5b7f4cb12932251ba460e3cf2db4246817da79313ea4ba9769ec7cbe53ab9c1cb81ad8fcce6a969cd241185b79398d2a4f3c6 - languageName: node - linkType: hard - -"@inquirer/ansi@npm:^2.0.7": - version: 2.0.7 - resolution: "@inquirer/ansi@npm:2.0.7" - checksum: 10c0/a574f97a899f0d9346fa26b528b3f4a9ba6dcb9172288efb6b4314d8486470ed53d2f538200f66a25b843c6e0cbf83688c6d5174a8dc6eca853b291b09609c5a - languageName: node - linkType: hard - -"@inquirer/checkbox@npm:^5.2.1": - version: 5.2.1 - resolution: "@inquirer/checkbox@npm:5.2.1" - dependencies: - "@inquirer/ansi": "npm:^2.0.7" - "@inquirer/core": "npm:^11.2.1" - "@inquirer/figures": "npm:^2.0.7" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/e3a845156c718fbbec228a0e7fd2a4f10775185615eac1f405b3a2bb2ae607dda6baf178fbd6487db0e12e5e33014536e81acefe65de57cd476a7aeaea6fb35d - languageName: node - linkType: hard - -"@inquirer/confirm@npm:^6.1.1": - version: 6.1.1 - resolution: "@inquirer/confirm@npm:6.1.1" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/4684406161c09327df830b4026f3165b31e13831276d215051586408ed434423263b15686393ce95a4b55058c1b7f9b08aa4b66f5ac930b47523fff75051d36f - languageName: node - linkType: hard - -"@inquirer/core@npm:^11.2.1": - version: 11.2.1 - resolution: "@inquirer/core@npm:11.2.1" - dependencies: - "@inquirer/ansi": "npm:^2.0.7" - "@inquirer/figures": "npm:^2.0.7" - "@inquirer/type": "npm:^4.0.7" - cli-width: "npm:^4.1.0" - fast-wrap-ansi: "npm:^0.2.0" - mute-stream: "npm:^3.0.0" - signal-exit: "npm:^4.1.0" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/b5be386cecd9e441ac2f9d3417a6ae1c4658b3ee6cdf5dae791211400f4de158851f81fca2245e2062833716f95366b9e1717770828cb7365e756c16e822f0d2 - languageName: node - linkType: hard - -"@inquirer/editor@npm:^5.2.2": - version: 5.2.2 - resolution: "@inquirer/editor@npm:5.2.2" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/external-editor": "npm:^3.0.3" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/a75e7012aad3ccc3ec84893463ed689924515a6cbaee8e2a475769fb27c12bcf359fcdd5f62e92107fc4be88fd69444c15adf13071982efc140c7015a6604d9a - languageName: node - linkType: hard - -"@inquirer/expand@npm:^5.1.1": - version: 5.1.1 - resolution: "@inquirer/expand@npm:5.1.1" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/4de661a042147759ce351971a4f92010ab66cb76450424e7d8aec5de79b449d14e8751f54f557d0b047180bca2b76707626f4835ee46603c6200139c31b59652 - languageName: node - linkType: hard - -"@inquirer/external-editor@npm:^3.0.3": - version: 3.0.3 - resolution: "@inquirer/external-editor@npm:3.0.3" - dependencies: - chardet: "npm:^2.1.1" - iconv-lite: "npm:^0.7.2" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/fc24ddbff15f0874db6498c16d2fe153bb19a670d83605f480486d6ff0ea872ae2f32a548fd555797989db09850fa019a6b5e49a8d40cfee0d7deacad17edc1e - languageName: node - linkType: hard - -"@inquirer/figures@npm:^2.0.7": - version: 2.0.7 - resolution: "@inquirer/figures@npm:2.0.7" - checksum: 10c0/e0573dc9ad25fa3628d5164745e52852d8cd832a9918605b7716df2e37a0005a0aaf40b6d81cef2ca09cb708b200e61b82d1dcd17003f572577e233c19a9ec7b - languageName: node - linkType: hard - -"@inquirer/input@npm:^5.1.2": - version: 5.1.2 - resolution: "@inquirer/input@npm:5.1.2" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/9c3614f8d79fc2bec07a088858a58967a162046c9292b0c6f0c6f63396cf391181cfb54bb636f5b3dce8d23924c24ee4a0310183a493c94190e4750218f3744d - languageName: node - linkType: hard - -"@inquirer/number@npm:^4.1.1": - version: 4.1.1 - resolution: "@inquirer/number@npm:4.1.1" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/06210dd70bf89d35242af43009b5e3f76a5f07d6275a9d842bbed61536032f5f349ecba1c20012975d7b0362deb89746d5903e90f21516da10bfd8c2055829a9 - languageName: node - linkType: hard - -"@inquirer/password@npm:^5.1.1": - version: 5.1.1 - resolution: "@inquirer/password@npm:5.1.1" - dependencies: - "@inquirer/ansi": "npm:^2.0.7" - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/e4cb3a53b56743808f83958a8fe85738d721599690a4bc7640eaa07fb8f4765bc6f174e40c16efcc8a5958a7169667e240714a706a36e831f9c7a6822cbe8b55 - languageName: node - linkType: hard - -"@inquirer/prompts@npm:^8.5.2": - version: 8.5.2 - resolution: "@inquirer/prompts@npm:8.5.2" - dependencies: - "@inquirer/checkbox": "npm:^5.2.1" - "@inquirer/confirm": "npm:^6.1.1" - "@inquirer/editor": "npm:^5.2.2" - "@inquirer/expand": "npm:^5.1.1" - "@inquirer/input": "npm:^5.1.2" - "@inquirer/number": "npm:^4.1.1" - "@inquirer/password": "npm:^5.1.1" - "@inquirer/rawlist": "npm:^5.3.1" - "@inquirer/search": "npm:^4.2.1" - "@inquirer/select": "npm:^5.2.1" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/253b92e31c6a1f8f00a778eda8196bb53fc931723f7db4a03937f38ccd4d07c987766d4f60b9250b5d90bfe30b04747dfa73927a9f6fb886bd48091ccb202535 - languageName: node - linkType: hard - -"@inquirer/rawlist@npm:^5.3.1": - version: 5.3.1 - resolution: "@inquirer/rawlist@npm:5.3.1" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/a05eb6a16633bd7b32b3b6b2c1f645e6e28d955475b21ba7222e7e66806b1be15be9f91235b2933e8d27e04fdad81ebfb2d64fc1fc0d4b8d82dcd2718817b2b9 - languageName: node - linkType: hard - -"@inquirer/search@npm:^4.2.1": - version: 4.2.1 - resolution: "@inquirer/search@npm:4.2.1" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/figures": "npm:^2.0.7" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/eae9b2d524aae58266f96987696be22ad62f608661d28763c413f2560094d571a39d72a40630d2470f503ad5e6e50d8c574a653cc028bf79b51104fa91f59a67 - languageName: node - linkType: hard - -"@inquirer/select@npm:^5.2.1": - version: 5.2.1 - resolution: "@inquirer/select@npm:5.2.1" - dependencies: - "@inquirer/ansi": "npm:^2.0.7" - "@inquirer/core": "npm:^11.2.1" - "@inquirer/figures": "npm:^2.0.7" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/3c6d0151b5a29111254a58eaf8b93bb4c476e68b0751526d8bff61a4bea457bdbe782890ae33977c5d2015f436596b5d32a8bb0677bfdf610f5f5998e65f5a92 - languageName: node - linkType: hard - -"@inquirer/type@npm:^4.0.7": - version: 4.0.7 - resolution: "@inquirer/type@npm:4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/80678ac1c6e19ce309909e4a54a69adc95697ea3abc2cb92f17b1bc52f4caadbcb4003ae7339fb5a70c0d36d3bde975e1bb4450069662f41c953a0d28695bb70 - languageName: node - linkType: hard - -"@isaacs/fs-minipass@npm:^4.0.0": - version: 4.0.1 - resolution: "@isaacs/fs-minipass@npm:4.0.1" - dependencies: - minipass: "npm:^7.0.4" - checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 - languageName: node - linkType: hard - -"@jridgewell/sourcemap-codec@npm:^1.5.5": - version: 1.5.5 - resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" - checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 - languageName: node - linkType: hard - -"@napi-rs/wasm-runtime@npm:^1.1.4": - version: 1.1.5 - resolution: "@napi-rs/wasm-runtime@npm:1.1.5" - dependencies: - "@tybys/wasm-util": "npm:^0.10.2" - peerDependencies: - "@emnapi/core": ^1.7.1 - "@emnapi/runtime": ^1.7.1 - checksum: 10c0/727f2b6ae0e68bbe5d39aeb68aa6f183314e9f03dc50bb55a962849535b2db53ecc3fbf1554d8656a54488a608df5a2634670595cf5874dc4af2ee59f817c65d - languageName: node - linkType: hard - -"@oxc-project/types@npm:=0.133.0": - version: 0.133.0 - resolution: "@oxc-project/types@npm:0.133.0" - checksum: 10c0/70c57ba58644f7ec217b670c301801f4d06995f4ccdba6b2bd106ea3e5ee49d616573e6ef8d55530b87571a960696543687f3850e87ad173d3f88965c30cdd63 - languageName: node - linkType: hard - -"@oxlint/darwin-arm64@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/darwin-arm64@npm:0.16.12" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@oxlint/darwin-x64@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/darwin-x64@npm:0.16.12" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@oxlint/linux-arm64-gnu@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/linux-arm64-gnu@npm:0.16.12" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@oxlint/linux-arm64-musl@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/linux-arm64-musl@npm:0.16.12" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@oxlint/linux-x64-gnu@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/linux-x64-gnu@npm:0.16.12" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@oxlint/linux-x64-musl@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/linux-x64-musl@npm:0.16.12" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@oxlint/win32-arm64@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/win32-arm64@npm:0.16.12" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@oxlint/win32-x64@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/win32-x64@npm:0.16.12" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@rolldown/binding-android-arm64@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-android-arm64@npm:1.0.3" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-darwin-arm64@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.3" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-darwin-x64@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.3" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@rolldown/binding-freebsd-x64@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.3" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.3" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@rolldown/binding-linux-arm64-gnu@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.3" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-arm64-musl@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.3" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.3" - conditions: os=linux & cpu=ppc64 & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-s390x-gnu@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.3" - conditions: os=linux & cpu=s390x & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-x64-gnu@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.3" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-x64-musl@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.3" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@rolldown/binding-openharmony-arm64@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.3" - conditions: os=openharmony & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-wasm32-wasi@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.3" - dependencies: - "@emnapi/core": "npm:1.10.0" - "@emnapi/runtime": "npm:1.10.0" - "@napi-rs/wasm-runtime": "npm:^1.1.4" - conditions: cpu=wasm32 - languageName: node - linkType: hard - -"@rolldown/binding-win32-arm64-msvc@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.3" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-win32-x64-msvc@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.3" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@rolldown/pluginutils@npm:^1.0.0": - version: 1.0.1 - resolution: "@rolldown/pluginutils@npm:1.0.1" - checksum: 10c0/99d9b06d90196823e4d8c841f258db7a16e5dbba5824a2962b05d907b79f1ba929d56f22dd744fd530936e568c865ee56a719dc31e57e13bc0a8eb4764a8d8dd - languageName: node - linkType: hard - -"@sniptt/guards@npm:^0.2.0": - version: 0.2.0 - resolution: "@sniptt/guards@npm:0.2.0" - checksum: 10c0/749bb0f550d1ddd4abdb23dc1076cba26e977659922b73d000bceeb253c09270aaced0d18e92f09d4ce2fdaae33e55f60f2142f5359bc90ba505422af0873526 - languageName: node - linkType: hard - -"@standard-schema/spec@npm:^1.1.0": - version: 1.1.0 - resolution: "@standard-schema/spec@npm:1.1.0" - checksum: 10c0/d90f55acde4b2deb983529c87e8025fa693de1a5e8b49ecc6eb84d1fd96328add0e03d7d551442156c7432fd78165b2c26ff561b970a9a881f046abb78d6a526 - languageName: node - linkType: hard - -"@tybys/wasm-util@npm:^0.10.2": - version: 0.10.2 - resolution: "@tybys/wasm-util@npm:0.10.2" - dependencies: - tslib: "npm:^2.4.0" - checksum: 10c0/26165bcd1fd7269f42d7fbe3de318f854a8968de8397e89fc9a423bb3e2da35a52150f382e6323b3367595beb16d9800a6f35971a5599daf76da1742ec3afc25 - languageName: node - linkType: hard - -"@types/chai@npm:^5.2.2": - version: 5.2.3 - resolution: "@types/chai@npm:5.2.3" - dependencies: - "@types/deep-eql": "npm:*" - assertion-error: "npm:^2.0.1" - checksum: 10c0/e0ef1de3b6f8045a5e473e867c8565788c444271409d155588504840ad1a53611011f85072188c2833941189400228c1745d78323dac13fcede9c2b28bacfb2f - languageName: node - linkType: hard - -"@types/deep-eql@npm:*": - version: 4.0.2 - resolution: "@types/deep-eql@npm:4.0.2" - checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 - languageName: node - linkType: hard - -"@types/estree@npm:^1.0.0": - version: 1.0.8 - resolution: "@types/estree@npm:1.0.8" - checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5 - languageName: node - linkType: hard - -"@types/node@npm:*": - version: 25.6.0 - resolution: "@types/node@npm:25.6.0" - dependencies: - undici-types: "npm:~7.19.0" - checksum: 10c0/d2d2015630ff098a201407f55f5077a20270ae4f465c739b40865cd9933b91b9c5d2b85568eadaf3db0801b91e267333ca7eb39f007428b173d1cdab4b339ac5 - languageName: node - linkType: hard - -"@types/node@npm:^24.7.2": - version: 24.12.2 - resolution: "@types/node@npm:24.12.2" - dependencies: - undici-types: "npm:~7.16.0" - checksum: 10c0/710050c42f89075c4479e4e1e4c2532486b0c41b1e2a8a13ad88641c88b88cdaea87414e19224f30028719737bd70e327edcaa184d50e86b9418941edd7eb02b - languageName: node - linkType: hard - -"@types/qs@npm:^6.9.0": - version: 6.15.0 - resolution: "@types/qs@npm:6.15.0" - checksum: 10c0/1b104cac50e655fc41d7fc1de2c2aba2908c4cf833a555b6808fb4c96752662b439238f2392a15d2590a7a6ca75dbd40e42d9378ac2be0d548ee484954363688 - languageName: node - linkType: hard - -"@types/react@npm:^19.0.0": - version: 19.2.14 - resolution: "@types/react@npm:19.2.14" - dependencies: - csstype: "npm:^3.2.2" - checksum: 10c0/7d25bf41b57719452d86d2ac0570b659210402707313a36ee612666bf11275a1c69824f8c3ee1fdca077ccfe15452f6da8f1224529b917050eb2d861e52b59b7 - languageName: node - linkType: hard - -"@types/ws@npm:^6.0.1": - version: 6.0.4 - resolution: "@types/ws@npm:6.0.4" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/fa958e64596ca9487c3ed6012834de70b47f25d971f1950cfb8e6a99cb77ff340ae82ac7627744e01b58010674ef8ede07d5a2ac29ca9ad0d67a430fcc69ae14 - languageName: node - linkType: hard - -"@vitest/expect@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/expect@npm:4.1.8" - dependencies: - "@standard-schema/spec": "npm:^1.1.0" - "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.1.8" - "@vitest/utils": "npm:4.1.8" - chai: "npm:^6.2.2" - tinyrainbow: "npm:^3.1.0" - checksum: 10c0/f7bf6c720d2427c3bd0b35472ebd84d963be7d09ecf52a0fb05e8c4d5d0c9ee164a8c28eee6360947be1b245b47faefab54560cb98e5cb678c1c1074260b9149 - languageName: node - linkType: hard - -"@vitest/mocker@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/mocker@npm:4.1.8" - dependencies: - "@vitest/spy": "npm:4.1.8" - estree-walker: "npm:^3.0.3" - magic-string: "npm:^0.30.21" - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - checksum: 10c0/f8cb2b8b55dc2cba0b2399aeee528b0187042f22cbc2d50a4fd6141f5aa246ebc41700f45dd1d73eca44ddfb57dcde48b2eb317bfbb1198f5ab2cc4fd04b2ea0 - languageName: node - linkType: hard - -"@vitest/pretty-format@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/pretty-format@npm:4.1.8" - dependencies: - tinyrainbow: "npm:^3.1.0" - checksum: 10c0/553c456692a4b9ae13cd116c234c74b4495e0f1a0d5c51ffc3fab8ea085e3550769967e29db79bdac0cf127b1bf88b7f70bfba3dcc72be6bddf834433e30cc91 - languageName: node - linkType: hard - -"@vitest/runner@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/runner@npm:4.1.8" - dependencies: - "@vitest/utils": "npm:4.1.8" - pathe: "npm:^2.0.3" - checksum: 10c0/706808a4b7b95ea9a9268fc152dd39e15a9a754f37c7990aea167486a9094caa913dae454771ae02c18dccfabd667f8cc38eed33a1307a79d32a89878b5bcce1 - languageName: node - linkType: hard - -"@vitest/snapshot@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/snapshot@npm:4.1.8" - dependencies: - "@vitest/pretty-format": "npm:4.1.8" - "@vitest/utils": "npm:4.1.8" - magic-string: "npm:^0.30.21" - pathe: "npm:^2.0.3" - checksum: 10c0/ba4c32112491d42d24986f921c50ede5edbdb4b7eafa16c72cf8d2c9ecc44121fdb3d9365236747a9841f0d6776affc6457470fcbb082df9dbc28c24792a0c6d - languageName: node - linkType: hard - -"@vitest/spy@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/spy@npm:4.1.8" - checksum: 10c0/3c10c0325a09d16bc0e77c0be96c47c15416186e33332880c0d1dd0a51d51a866091067b81f2a2ef6fb422a7760e6cf15c04d91a0eca4d59f62e8c8401fa53fc - languageName: node - linkType: hard - -"@vitest/utils@npm:4.1.8": - version: 4.1.8 - resolution: "@vitest/utils@npm:4.1.8" - dependencies: - "@vitest/pretty-format": "npm:4.1.8" - convert-source-map: "npm:^2.0.0" - tinyrainbow: "npm:^3.1.0" - checksum: 10c0/acda9d3d640c1ebc81afb358ac30589d7d7d583af81e2d09419f0af9cbe41f3ce0b90527326943bf0da51614be5fc31afcd32259f6beb32b3417999d6ef380f3 - languageName: node - linkType: hard - -"abbrev@npm:^4.0.0": - version: 4.0.0 - resolution: "abbrev@npm:4.0.0" - checksum: 10c0/b4cc16935235e80702fc90192e349e32f8ef0ed151ef506aa78c81a7c455ec18375c4125414b99f84b2e055199d66383e787675f0bcd87da7a4dbd59f9eac1d5 - languageName: node - linkType: hard - -"agent-base@npm:6": - version: 6.0.2 - resolution: "agent-base@npm:6.0.2" - dependencies: - debug: "npm:4" - checksum: 10c0/dc4f757e40b5f3e3d674bc9beb4f1048f4ee83af189bae39be99f57bf1f48dde166a8b0a5342a84b5944ee8e6ed1e5a9d801858f4ad44764e84957122fe46261 - languageName: node - linkType: hard - -"ansi-escapes@npm:^7.3.0": - version: 7.3.0 - resolution: "ansi-escapes@npm:7.3.0" - dependencies: - environment: "npm:^1.0.0" - checksum: 10c0/068961d99f0ef28b661a4a9f84a5d645df93ccf3b9b93816cc7d46bbe1913321d4cdf156bb842a4e1e4583b7375c631fa963efb43001c4eb7ff9ab8f78fc0679 - languageName: node - linkType: hard - -"ansi-regex@npm:^6.2.2": - version: 6.2.2 - resolution: "ansi-regex@npm:6.2.2" - checksum: 10c0/05d4acb1d2f59ab2cf4b794339c7b168890d44dda4bf0ce01152a8da0213aca207802f930442ce8cd22d7a92f44907664aac6508904e75e038fa944d2601b30f - languageName: node - linkType: hard - -"ansi-styles@npm:^6.2.1, ansi-styles@npm:^6.2.3": - version: 6.2.3 - resolution: "ansi-styles@npm:6.2.3" - checksum: 10c0/23b8a4ce14e18fb854693b95351e286b771d23d8844057ed2e7d083cd3e708376c3323707ec6a24365f7d7eda3ca00327fe04092e29e551499ec4c8b7bfac868 - languageName: node - linkType: hard - -"assertion-error@npm:^2.0.1": - version: 2.0.1 - resolution: "assertion-error@npm:2.0.1" - checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 - languageName: node - linkType: hard - -"async-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-function@npm:1.0.0" - checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73 - languageName: node - linkType: hard - -"async-generator-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-generator-function@npm:1.0.0" - checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186 - languageName: node - linkType: hard - -"async-limiter@npm:~1.0.0": - version: 1.0.1 - resolution: "async-limiter@npm:1.0.1" - checksum: 10c0/0693d378cfe86842a70d4c849595a0bb50dc44c11649640ca982fa90cbfc74e3cc4753b5a0847e51933f2e9c65ce8e05576e75e5e1fd963a086e673735b35969 - languageName: node - linkType: hard - -"asynckit@npm:^0.4.0": - version: 0.4.0 - resolution: "asynckit@npm:0.4.0" - checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d - languageName: node - linkType: hard - -"auto-bind@npm:^5.0.1": - version: 5.0.1 - resolution: "auto-bind@npm:5.0.1" - checksum: 10c0/a703375350ea7b6e92405d8e6bcc6dbfb84b0d7c7172b33e5788a7593929a18227999ff9aa9c32436741d06d021e6672457b1cec73287efe3fab95cff6627eaf - languageName: node - linkType: hard - -"axios@npm:^1.16.0": - version: 1.17.0 - resolution: "axios@npm:1.17.0" - dependencies: - follow-redirects: "npm:^1.16.0" - form-data: "npm:^4.0.5" - https-proxy-agent: "npm:^5.0.1" - proxy-from-env: "npm:^2.1.0" - checksum: 10c0/c4fa19ff3a3a63bde48beec03ad816b133b9a6385cccffffe172577ab18c6a70e299280d57f12c80c867fe25df41f92cb91d3a8258708a6d2be3e9e085f92650 - languageName: node - linkType: hard - -"backo2@npm:^1.0.2": - version: 1.0.2 - resolution: "backo2@npm:1.0.2" - checksum: 10c0/a9e825a6a38a6d1c4a94476eabc13d6127dfaafb0967baf104affbb67806ae26abbb58dab8d572d2cd21ef06634ff57c3ad48dff14b904e18de1474cc2f22bf3 - languageName: node - linkType: hard - -"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": - version: 1.0.2 - resolution: "call-bind-apply-helpers@npm:1.0.2" - dependencies: - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938 - languageName: node - linkType: hard - -"chai@npm:^6.2.2": - version: 6.2.2 - resolution: "chai@npm:6.2.2" - checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 - languageName: node - linkType: hard - -"chalk@npm:^5.3.0, chalk@npm:^5.6.0": - version: 5.6.2 - resolution: "chalk@npm:5.6.2" - checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976 - languageName: node - linkType: hard - -"chardet@npm:^2.1.1": - version: 2.1.1 - resolution: "chardet@npm:2.1.1" - checksum: 10c0/d8391dd412338442b3de0d3a488aa9327f8bcf74b62b8723d6bd0b85c4084d50b731320e0a7c710edb1d44de75969995d2784b80e4c13b004a6c7a0db4c6e793 - languageName: node - linkType: hard - -"chokidar@npm:^4.0.0": - version: 4.0.3 - resolution: "chokidar@npm:4.0.3" - dependencies: - readdirp: "npm:^4.0.1" - checksum: 10c0/a58b9df05bb452f7d105d9e7229ac82fa873741c0c40ddcc7bb82f8a909fbe3f7814c9ebe9bc9a2bef9b737c0ec6e2d699d179048ef06ad3ec46315df0ebe6ad - languageName: node - linkType: hard - -"chownr@npm:^3.0.0": - version: 3.0.0 - resolution: "chownr@npm:3.0.0" - checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 - languageName: node - linkType: hard - -"cli-boxes@npm:^3.0.0": - version: 3.0.0 - resolution: "cli-boxes@npm:3.0.0" - checksum: 10c0/4db3e8fbfaf1aac4fb3a6cbe5a2d3fa048bee741a45371b906439b9ffc821c6e626b0f108bdcd3ddf126a4a319409aedcf39a0730573ff050fdd7b6731e99fb9 - languageName: node - linkType: hard - -"cli-cursor@npm:^4.0.0": - version: 4.0.0 - resolution: "cli-cursor@npm:4.0.0" - dependencies: - restore-cursor: "npm:^4.0.0" - checksum: 10c0/e776e8c3c6727300d0539b0d25160b2bb56aed1a63942753ba1826b012f337a6f4b7ace3548402e4f2f13b5e16bfd751be672c44b203205e7eca8be94afec42c - languageName: node - linkType: hard - -"cli-truncate@npm:^5.1.1": - version: 5.2.0 - resolution: "cli-truncate@npm:5.2.0" - dependencies: - slice-ansi: "npm:^8.0.0" - string-width: "npm:^8.2.0" - checksum: 10c0/0d4ec94702ca85b64522ac93633837fb5ea7db17b79b1322a60f6045e6ae2b8cd7bd4c1d19ac7d1f9e10e3bbda1112e172e439b68c02b785ee00da8d6a5c5471 - languageName: node - linkType: hard - -"cli-width@npm:^4.1.0": - version: 4.1.0 - resolution: "cli-width@npm:4.1.0" - checksum: 10c0/1fbd56413578f6117abcaf858903ba1f4ad78370a4032f916745fa2c7e390183a9d9029cf837df320b0fdce8137668e522f60a30a5f3d6529ff3872d265a955f - languageName: node - linkType: hard - -"code-excerpt@npm:^4.0.0": - version: 4.0.0 - resolution: "code-excerpt@npm:4.0.0" - dependencies: - convert-to-spaces: "npm:^2.0.1" - checksum: 10c0/b6c5a06e039cecd2ab6a0e10ee0831de8362107d1f298ca3558b5f9004cb8e0260b02dd6c07f57b9a0e346c76864d2873311ee1989809fdeb05bd5fbbadde773 - languageName: node - linkType: hard - -"combined-stream@npm:^1.0.8": - version: 1.0.8 - resolution: "combined-stream@npm:1.0.8" - dependencies: - delayed-stream: "npm:~1.0.0" - checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 - languageName: node - linkType: hard - -"commander@npm:^12.0.0": - version: 12.1.0 - resolution: "commander@npm:12.1.0" - checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 - languageName: node - linkType: hard - -"convert-source-map@npm:^2.0.0": - version: 2.0.0 - resolution: "convert-source-map@npm:2.0.0" - checksum: 10c0/8f2f7a27a1a011cc6cc88cc4da2d7d0cfa5ee0369508baae3d98c260bb3ac520691464e5bbe4ae7cdf09860c1d69ecc6f70c63c6e7c7f7e3f18ec08484dc7d9b - languageName: node - linkType: hard - -"convert-to-spaces@npm:^2.0.1": - version: 2.0.1 - resolution: "convert-to-spaces@npm:2.0.1" - checksum: 10c0/d90aa0e3b6a27f9d5265a8d32def3c5c855b3e823a9db1f26d772f8146d6b91020a2fdfd905ce8048a73fad3aaf836fef8188c67602c374405e2ae8396c4ac46 - languageName: node - linkType: hard - -"csstype@npm:^3.2.2": - version: 3.2.3 - resolution: "csstype@npm:3.2.3" - checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce - languageName: node - linkType: hard - -"debug@npm:4, debug@npm:^4.1.1": - version: 4.4.3 - resolution: "debug@npm:4.4.3" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 - languageName: node - linkType: hard - -"delayed-stream@npm:~1.0.0": - version: 1.0.0 - resolution: "delayed-stream@npm:1.0.0" - checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 - languageName: node - linkType: hard - -"detect-libc@npm:^2.0.3": - version: 2.1.2 - resolution: "detect-libc@npm:2.1.2" - checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 - languageName: node - linkType: hard - -"dunder-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "dunder-proto@npm:1.0.1" - dependencies: - call-bind-apply-helpers: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - gopd: "npm:^1.2.0" - checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031 - languageName: node - linkType: hard - -"emoji-regex@npm:^10.3.0": - version: 10.6.0 - resolution: "emoji-regex@npm:10.6.0" - checksum: 10c0/1e4aa097bb007301c3b4b1913879ae27327fdc48e93eeefefe3b87e495eb33c5af155300be951b4349ff6ac084f4403dc9eff970acba7c1c572d89396a9a32d7 - languageName: node - linkType: hard - -"env-paths@npm:^2.2.0": - version: 2.2.1 - resolution: "env-paths@npm:2.2.1" - checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 - languageName: node - linkType: hard - -"environment@npm:^1.0.0": - version: 1.1.0 - resolution: "environment@npm:1.1.0" - checksum: 10c0/fb26434b0b581ab397039e51ff3c92b34924a98b2039dcb47e41b7bca577b9dbf134a8eadb364415c74464b682e2d3afe1a4c0eb9873dc44ea814c5d3103331d - languageName: node - linkType: hard - -"es-define-property@npm:^1.0.1": - version: 1.0.1 - resolution: "es-define-property@npm:1.0.1" - checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c - languageName: node - linkType: hard - -"es-errors@npm:^1.3.0": - version: 1.3.0 - resolution: "es-errors@npm:1.3.0" - checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 - languageName: node - linkType: hard - -"es-module-lexer@npm:^2.0.0": - version: 2.1.0 - resolution: "es-module-lexer@npm:2.1.0" - checksum: 10c0/93bcf2454fa72d67fe3ccd0abef8ce7933f5840a319513418a643dd8e9c6aa8f49709cecfae02ded722805dd327232d30723a807cc52e6809d6ac697c62c29fb - languageName: node - linkType: hard - -"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": - version: 1.1.1 - resolution: "es-object-atoms@npm:1.1.1" - dependencies: - es-errors: "npm:^1.3.0" - checksum: 10c0/65364812ca4daf48eb76e2a3b7a89b3f6a2e62a1c420766ce9f692665a29d94fe41fe88b65f24106f449859549711e4b40d9fb8002d862dfd7eb1c512d10be0c - languageName: node - linkType: hard - -"es-set-tostringtag@npm:^2.1.0": - version: 2.1.0 - resolution: "es-set-tostringtag@npm:2.1.0" - dependencies: - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.6" - has-tostringtag: "npm:^1.0.2" - hasown: "npm:^2.0.2" - checksum: 10c0/ef2ca9ce49afe3931cb32e35da4dcb6d86ab02592cfc2ce3e49ced199d9d0bb5085fc7e73e06312213765f5efa47cc1df553a6a5154584b21448e9fb8355b1af - languageName: node - linkType: hard - -"es-toolkit@npm:^1.39.10": - version: 1.46.0 - resolution: "es-toolkit@npm:1.46.0" - dependenciesMeta: - "@trivago/prettier-plugin-sort-imports@4.3.0": - unplugged: true - prettier-plugin-sort-re-exports@0.0.1: - unplugged: true - checksum: 10c0/d52d3602e89621d8cf8e0f74d6b574e7d5296d97ff34b57ae5331735a4296e7c88c8c28fe6e43388c3c6df84fa06179705a3125767ebcc8b76f672bd06dccfb7 - languageName: node - linkType: hard - -"esbuild@npm:^0.28.1": - version: 0.28.1 - resolution: "esbuild@npm:0.28.1" - dependencies: - "@esbuild/aix-ppc64": "npm:0.28.1" - "@esbuild/android-arm": "npm:0.28.1" - "@esbuild/android-arm64": "npm:0.28.1" - "@esbuild/android-x64": "npm:0.28.1" - "@esbuild/darwin-arm64": "npm:0.28.1" - "@esbuild/darwin-x64": "npm:0.28.1" - "@esbuild/freebsd-arm64": "npm:0.28.1" - "@esbuild/freebsd-x64": "npm:0.28.1" - "@esbuild/linux-arm": "npm:0.28.1" - "@esbuild/linux-arm64": "npm:0.28.1" - "@esbuild/linux-ia32": "npm:0.28.1" - "@esbuild/linux-loong64": "npm:0.28.1" - "@esbuild/linux-mips64el": "npm:0.28.1" - "@esbuild/linux-ppc64": "npm:0.28.1" - "@esbuild/linux-riscv64": "npm:0.28.1" - "@esbuild/linux-s390x": "npm:0.28.1" - "@esbuild/linux-x64": "npm:0.28.1" - "@esbuild/netbsd-arm64": "npm:0.28.1" - "@esbuild/netbsd-x64": "npm:0.28.1" - "@esbuild/openbsd-arm64": "npm:0.28.1" - "@esbuild/openbsd-x64": "npm:0.28.1" - "@esbuild/openharmony-arm64": "npm:0.28.1" - "@esbuild/sunos-x64": "npm:0.28.1" - "@esbuild/win32-arm64": "npm:0.28.1" - "@esbuild/win32-ia32": "npm:0.28.1" - "@esbuild/win32-x64": "npm:0.28.1" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-arm64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-arm64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/openharmony-arm64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10c0/29cd456a79ce35ac2c7e05fe871330416b2c395c045d849653f843e51378d6e0d6e774d6dcd01b35f4e83238a29bf8decd04fcd34b3780c589a250b21e5f92bb - languageName: node - linkType: hard - -"escape-string-regexp@npm:^2.0.0": - version: 2.0.0 - resolution: "escape-string-regexp@npm:2.0.0" - checksum: 10c0/2530479fe8db57eace5e8646c9c2a9c80fa279614986d16dcc6bcaceb63ae77f05a851ba6c43756d816c61d7f4534baf56e3c705e3e0d884818a46808811c507 - languageName: node - linkType: hard - -"estree-walker@npm:^3.0.3": - version: 3.0.3 - resolution: "estree-walker@npm:3.0.3" - dependencies: - "@types/estree": "npm:^1.0.0" - checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d - languageName: node - linkType: hard - -"eventemitter3@npm:^3.1.0": - version: 3.1.2 - resolution: "eventemitter3@npm:3.1.2" - checksum: 10c0/c67262eccbf85848b7cc6d4abb6c6e34155e15686db2a01c57669fd0d44441a574a19d44d25948b442929e065774cbe5003d8e77eed47674fbf876ac77887793 - languageName: node - linkType: hard - -"expect-type@npm:^1.3.0": - version: 1.3.0 - resolution: "expect-type@npm:1.3.0" - checksum: 10c0/8412b3fe4f392c420ab41dae220b09700e4e47c639a29ba7ba2e83cc6cffd2b4926f7ac9e47d7e277e8f4f02acda76fd6931cb81fd2b382fa9477ef9ada953fd - languageName: node - linkType: hard - -"exponential-backoff@npm:^3.1.1": - version: 3.1.3 - resolution: "exponential-backoff@npm:3.1.3" - checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267 - languageName: node - linkType: hard - -"fast-string-truncated-width@npm:^3.0.2": - version: 3.0.3 - resolution: "fast-string-truncated-width@npm:3.0.3" - checksum: 10c0/043b8663397d14a3880ce4f3407bcda60b40db9bbeafe62863a35d1f9c69ea17c8da3fcd72de235553e6c9cd053128cde9e24ca0d4a7463208f48db3cd23d981 - languageName: node - linkType: hard - -"fast-string-width@npm:^3.0.2": - version: 3.0.2 - resolution: "fast-string-width@npm:3.0.2" - dependencies: - fast-string-truncated-width: "npm:^3.0.2" - checksum: 10c0/c8822d175315bb353ebe782b65214ac53b13e3bf704e03b132ea7bdfa8de6a636375b3ab7a4097545393d109381c37c4f387c72a462c90b61412dbc4632f39a7 - languageName: node - linkType: hard - -"fast-wrap-ansi@npm:^0.2.0": - version: 0.2.2 - resolution: "fast-wrap-ansi@npm:0.2.2" - dependencies: - fast-string-width: "npm:^3.0.2" - checksum: 10c0/1aa7be4f7cb86f4bdb14691cb6bcc0b8df8b3b89df142ade3ae1602332dcf6f990cd750a923cd581ca0847808cb4ec1aa5afaafa7a72f849e87a2a62c98fa370 - languageName: node - linkType: hard - -"fdir@npm:^6.5.0": - version: 6.5.0 - resolution: "fdir@npm:6.5.0" - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f - languageName: node - linkType: hard - -"follow-redirects@npm:^1.16.0": - version: 1.16.0 - resolution: "follow-redirects@npm:1.16.0" - peerDependenciesMeta: - debug: - optional: true - checksum: 10c0/a1e2900163e6f1b4d1ed5c221b607f41decbab65534c63fe7e287e40a5d552a6496e7d9d7d976fa4ba77b4c51c11e5e9f683f10b43011ea11e442ff128d0e181 - languageName: node - linkType: hard - -"form-data@npm:^4.0.5": - version: 4.0.6 - resolution: "form-data@npm:4.0.6" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.4" - mime-types: "npm:^2.1.35" - checksum: 10c0/43947a77bf0ff45c6ceed789778982d47a3f3e720a74b71721174ebf3310a5f1a8be1d6b38a3ee3688e8a18a2c4273073ec0844cd37efda3eaf46d41c9c318ff - languageName: node - linkType: hard - -"fsevents@npm:~2.3.3": - version: 2.3.3 - resolution: "fsevents@npm:2.3.3" - dependencies: - node-gyp: "npm:latest" - checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 - conditions: os=darwin - languageName: node - linkType: hard - -"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": - version: 2.3.3 - resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" - dependencies: - node-gyp: "npm:latest" - conditions: os=darwin - languageName: node - linkType: hard - -"function-bind@npm:^1.1.2": - version: 1.1.2 - resolution: "function-bind@npm:1.1.2" - checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 - languageName: node - linkType: hard - -"generator-function@npm:^2.0.0": - version: 2.0.1 - resolution: "generator-function@npm:2.0.1" - checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8 - languageName: node - linkType: hard - -"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.1, get-east-asian-width@npm:^1.5.0": - version: 1.5.0 - resolution: "get-east-asian-width@npm:1.5.0" - checksum: 10c0/bff8bbc8d81790b9477f7aa55b1806b9f082a8dc1359fff7bd8b96939622c86b729685afc2bfeb22def1fc6ef1e5228e4d87dd4e6da60bc43a5edfb03c4ee167 - languageName: node - linkType: hard - -"get-intrinsic@npm:^1.2.6": - version: 1.3.1 - resolution: "get-intrinsic@npm:1.3.1" - dependencies: - async-function: "npm:^1.0.0" - async-generator-function: "npm:^1.0.0" - call-bind-apply-helpers: "npm:^1.0.2" - es-define-property: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.1.1" - function-bind: "npm:^1.1.2" - generator-function: "npm:^2.0.0" - get-proto: "npm:^1.0.1" - gopd: "npm:^1.2.0" - has-symbols: "npm:^1.1.0" - hasown: "npm:^2.0.2" - math-intrinsics: "npm:^1.1.0" - checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d - languageName: node - linkType: hard - -"get-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "get-proto@npm:1.0.1" - dependencies: - dunder-proto: "npm:^1.0.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c - languageName: node - linkType: hard - -"github-connector@workspace:.": - version: 0.0.0-use.local - resolution: "github-connector@workspace:." - dependencies: - "@types/node": "npm:^24.7.2" - "@types/react": "npm:^19.0.0" - oxlint: "npm:^0.16.0" - react: "npm:^19.0.0" - react-dom: "npm:^19.0.0" - twenty-client-sdk: "npm:2.13.0" - twenty-sdk: "npm:2.13.0" - typescript: "npm:^5.9.3" - vite-tsconfig-paths: "npm:^4.2.1" - vitest: "npm:^4.0.0" - languageName: unknown - linkType: soft - -"globrex@npm:^0.1.2": - version: 0.1.2 - resolution: "globrex@npm:0.1.2" - checksum: 10c0/a54c029520cf58bda1d8884f72bd49b4cd74e977883268d931fd83bcbd1a9eb96d57c7dbd4ad80148fb9247467ebfb9b215630b2ed7563b2a8de02e1ff7f89d1 - languageName: node - linkType: hard - -"gopd@npm:^1.2.0": - version: 1.2.0 - resolution: "gopd@npm:1.2.0" - checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead - languageName: node - linkType: hard - -"graceful-fs@npm:^4.2.6": - version: 4.2.11 - resolution: "graceful-fs@npm:4.2.11" - checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 - languageName: node - linkType: hard - -"graphql-query-batcher@npm:^1.0.1": - version: 1.0.1 - resolution: "graphql-query-batcher@npm:1.0.1" - checksum: 10c0/804d0f4064721a2116a16b9eac422e9233e85f4ab5b250cb8f83662725658ffde35779a8ae8211037f3dd2f9717de8cb63b394ad8057ce22171a83db2471196a - languageName: node - linkType: hard - -"graphql-sse@npm:^2.5.4": - version: 2.6.0 - resolution: "graphql-sse@npm:2.6.0" - peerDependencies: - graphql: ">=0.11 <=16" - checksum: 10c0/e05f0b5c8539d61e5ce34af8e0bb418c02bf922d6a7f9232a9abd53c77df5654684ca14f674ac771645c8fba9c37ce666c5d06f46eef54ea07e63653468065b0 - languageName: node - linkType: hard - -"graphql@npm:^16.8.1": - version: 16.13.2 - resolution: "graphql@npm:16.13.2" - checksum: 10c0/64e822a0a0e4398781e4bc9765b88d370c08261498b517add4b878038ef7be2005b6b394a79a5102b9379d57052f60bc7f23fec8f39808d101984a74772ebd9d - languageName: node - linkType: hard - -"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": - version: 1.1.0 - resolution: "has-symbols@npm:1.1.0" - checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e - languageName: node - linkType: hard - -"has-tostringtag@npm:^1.0.2": - version: 1.0.2 - resolution: "has-tostringtag@npm:1.0.2" - dependencies: - has-symbols: "npm:^1.0.3" - checksum: 10c0/a8b166462192bafe3d9b6e420a1d581d93dd867adb61be223a17a8d6dad147aa77a8be32c961bb2f27b3ef893cae8d36f564ab651f5e9b7938ae86f74027c48c - languageName: node - linkType: hard - -"hasown@npm:^2.0.2": - version: 2.0.3 - resolution: "hasown@npm:2.0.3" - dependencies: - function-bind: "npm:^1.1.2" - checksum: 10c0/f5eb28c3fd0d3e4facd821c1eeee3836c37b70ab0b0fc532e8a39976e18fef43652415dadc52f8c7a5ff6d5ac93b7bef128789aa6f90f4e9b9a9083dce74ab38 - languageName: node - linkType: hard - -"hasown@npm:^2.0.4": - version: 2.0.4 - resolution: "hasown@npm:2.0.4" - dependencies: - function-bind: "npm:^1.1.2" - checksum: 10c0/2d8de939e270b70618f8cebb69746620db10617dbb495bc66ddad326955ea24d3ca4af133aff3eb7c1853e0218f867bc2b050ec26fe02e3aea58f880ffc5e506 - languageName: node - linkType: hard - -"https-proxy-agent@npm:^5.0.1": - version: 5.0.1 - resolution: "https-proxy-agent@npm:5.0.1" - dependencies: - agent-base: "npm:6" - debug: "npm:4" - checksum: 10c0/6dd639f03434003577c62b27cafdb864784ef19b2de430d8ae2a1d45e31c4fd60719e5637b44db1a88a046934307da7089e03d6089ec3ddacc1189d8de8897d1 - languageName: node - linkType: hard - -"iconv-lite@npm:^0.7.2": - version: 0.7.2 - resolution: "iconv-lite@npm:0.7.2" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 10c0/3c228920f3bd307f56bf8363706a776f4a060eb042f131cd23855ceca962951b264d0997ab38a1ad340e1c5df8499ed26e1f4f0db6b2a2ad9befaff22f14b722 - languageName: node - linkType: hard - -"indent-string@npm:^5.0.0": - version: 5.0.0 - resolution: "indent-string@npm:5.0.0" - checksum: 10c0/8ee77b57d92e71745e133f6f444d6fa3ed503ad0e1bcd7e80c8da08b42375c07117128d670589725ed07b1978065803fa86318c309ba45415b7fe13e7f170220 - languageName: node - linkType: hard - -"ink@npm:^6.8.0": - version: 6.8.0 - resolution: "ink@npm:6.8.0" - dependencies: - "@alcalzone/ansi-tokenize": "npm:^0.2.4" - ansi-escapes: "npm:^7.3.0" - ansi-styles: "npm:^6.2.1" - auto-bind: "npm:^5.0.1" - chalk: "npm:^5.6.0" - cli-boxes: "npm:^3.0.0" - cli-cursor: "npm:^4.0.0" - cli-truncate: "npm:^5.1.1" - code-excerpt: "npm:^4.0.0" - es-toolkit: "npm:^1.39.10" - indent-string: "npm:^5.0.0" - is-in-ci: "npm:^2.0.0" - patch-console: "npm:^2.0.0" - react-reconciler: "npm:^0.33.0" - scheduler: "npm:^0.27.0" - signal-exit: "npm:^3.0.7" - slice-ansi: "npm:^8.0.0" - stack-utils: "npm:^2.0.6" - string-width: "npm:^8.1.1" - terminal-size: "npm:^4.0.1" - type-fest: "npm:^5.4.1" - widest-line: "npm:^6.0.0" - wrap-ansi: "npm:^9.0.0" - ws: "npm:^8.18.0" - yoga-layout: "npm:~3.2.1" - peerDependencies: - "@types/react": ">=19.0.0" - react: ">=19.0.0" - react-devtools-core: ">=6.1.2" - peerDependenciesMeta: - "@types/react": - optional: true - react-devtools-core: - optional: true - checksum: 10c0/50500e547fdf6a1f1d836d6befbd4770e3ab649ef0be1884500a6da411fb68a90e22dd7dcc9c404911d30e9f87506b3b9d8e997c6c6ceac85ee054b4dadefaff - languageName: node - linkType: hard - -"inquirer@npm:^14.0.0": - version: 14.0.2 - resolution: "inquirer@npm:14.0.2" - dependencies: - "@inquirer/ansi": "npm:^2.0.7" - "@inquirer/core": "npm:^11.2.1" - "@inquirer/prompts": "npm:^8.5.2" - "@inquirer/type": "npm:^4.0.7" - mute-stream: "npm:^3.0.0" - run-async: "npm:^4.0.6" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/31ec80b7d599dcb19568540d694865b1da116f701b75503a23ed38189d96fca70eb1fb6ccb1dbd994f6f79d407d51dc1a94d06dcef5370644ce736792414781b - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^5.0.0, is-fullwidth-code-point@npm:^5.1.0": - version: 5.1.0 - resolution: "is-fullwidth-code-point@npm:5.1.0" - dependencies: - get-east-asian-width: "npm:^1.3.1" - checksum: 10c0/c1172c2e417fb73470c56c431851681591f6a17233603a9e6f94b7ba870b2e8a5266506490573b607fb1081318589372034aa436aec07b465c2029c0bc7f07a4 - languageName: node - linkType: hard - -"is-in-ci@npm:^2.0.0": - version: 2.0.0 - resolution: "is-in-ci@npm:2.0.0" - bin: - is-in-ci: cli.js - checksum: 10c0/1e1d1056939a681e8206035de5ad84e0404556eaa7622bb55f0f1868b9788bff3df427bc0b1ed5a172623154a90fcb1e759a230817cd73d09435543ae3c71feb - languageName: node - linkType: hard - -"isexe@npm:^4.0.0": - version: 4.0.0 - resolution: "isexe@npm:4.0.0" - checksum: 10c0/5884815115bceac452877659a9c7726382531592f43dc29e5d48b7c4100661aed54018cb90bd36cb2eaeba521092570769167acbb95c18d39afdccbcca06c5ce - languageName: node - linkType: hard - -"isomorphic-unfetch@npm:^3.0.0": - version: 3.1.0 - resolution: "isomorphic-unfetch@npm:3.1.0" - dependencies: - node-fetch: "npm:^2.6.1" - unfetch: "npm:^4.2.0" - checksum: 10c0/d3b61fca06304db692b7f76bdfd3a00f410e42cfa7403c3b250546bf71589d18cf2f355922f57198e4cc4a9872d3647b20397a5c3edf1a347c90d57c83cf2a89 - languageName: node - linkType: hard - -"iterall@npm:^1.2.1": - version: 1.3.0 - resolution: "iterall@npm:1.3.0" - checksum: 10c0/40de624e5fe937c4c0e511981b91caea9ff2142bfc0316cccc8506eaa03aa253820cc17c5bc5f0a98706c7268a373e5ebee9af9a0c8a359730cf7c05938b57b5 - languageName: node - linkType: hard - -"jsonc-parser@npm:^3.2.0": - version: 3.3.1 - resolution: "jsonc-parser@npm:3.3.1" - checksum: 10c0/269c3ae0a0e4f907a914bf334306c384aabb9929bd8c99f909275ebd5c2d3bc70b9bcd119ad794f339dec9f24b6a4ee9cd5a8ab2e6435e730ad4075388fc2ab6 - languageName: node - linkType: hard - -"lightningcss-android-arm64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-android-arm64@npm:1.32.0" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"lightningcss-darwin-arm64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-darwin-arm64@npm:1.32.0" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"lightningcss-darwin-x64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-darwin-x64@npm:1.32.0" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"lightningcss-freebsd-x64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-freebsd-x64@npm:1.32.0" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"lightningcss-linux-arm-gnueabihf@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm-gnueabihf@npm:1.32.0" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"lightningcss-linux-arm64-gnu@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm64-gnu@npm:1.32.0" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"lightningcss-linux-arm64-musl@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm64-musl@npm:1.32.0" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"lightningcss-linux-x64-gnu@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-x64-gnu@npm:1.32.0" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"lightningcss-linux-x64-musl@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-x64-musl@npm:1.32.0" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"lightningcss-win32-arm64-msvc@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-win32-arm64-msvc@npm:1.32.0" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"lightningcss-win32-x64-msvc@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-win32-x64-msvc@npm:1.32.0" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"lightningcss@npm:^1.32.0": - version: 1.32.0 - resolution: "lightningcss@npm:1.32.0" - dependencies: - detect-libc: "npm:^2.0.3" - lightningcss-android-arm64: "npm:1.32.0" - lightningcss-darwin-arm64: "npm:1.32.0" - lightningcss-darwin-x64: "npm:1.32.0" - lightningcss-freebsd-x64: "npm:1.32.0" - lightningcss-linux-arm-gnueabihf: "npm:1.32.0" - lightningcss-linux-arm64-gnu: "npm:1.32.0" - lightningcss-linux-arm64-musl: "npm:1.32.0" - lightningcss-linux-x64-gnu: "npm:1.32.0" - lightningcss-linux-x64-musl: "npm:1.32.0" - lightningcss-win32-arm64-msvc: "npm:1.32.0" - lightningcss-win32-x64-msvc: "npm:1.32.0" - dependenciesMeta: - lightningcss-android-arm64: - optional: true - lightningcss-darwin-arm64: - optional: true - lightningcss-darwin-x64: - optional: true - lightningcss-freebsd-x64: - optional: true - lightningcss-linux-arm-gnueabihf: - optional: true - lightningcss-linux-arm64-gnu: - optional: true - lightningcss-linux-arm64-musl: - optional: true - lightningcss-linux-x64-gnu: - optional: true - lightningcss-linux-x64-musl: - optional: true - lightningcss-win32-arm64-msvc: - optional: true - lightningcss-win32-x64-msvc: - optional: true - checksum: 10c0/70945bd55097af46fc9fab7f5ed09cd5869d85940a2acab7ee06d0117004a1d68155708a2d462531cea2fc3c67aefc9333a7068c80b0b78dd404c16838809e03 - languageName: node - linkType: hard - -"lodash@npm:^4.17.20, lodash@npm:^4.17.21": - version: 4.18.1 - resolution: "lodash@npm:4.18.1" - checksum: 10c0/757228fc68805c59789e82185135cf85f05d0b2d3d54631d680ca79ec21944ec8314d4533639a14b8bcfbd97a517e78960933041a5af17ecb693ec6eecb99a27 - languageName: node - linkType: hard - -"magic-string@npm:^0.30.21": - version: 0.30.21 - resolution: "magic-string@npm:0.30.21" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.5.5" - checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a - languageName: node - linkType: hard - -"math-intrinsics@npm:^1.1.0": - version: 1.1.0 - resolution: "math-intrinsics@npm:1.1.0" - checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f - languageName: node - linkType: hard - -"mime-db@npm:1.52.0": - version: 1.52.0 - resolution: "mime-db@npm:1.52.0" - checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa - languageName: node - linkType: hard - -"mime-types@npm:^2.1.35": - version: 2.1.35 - resolution: "mime-types@npm:2.1.35" - dependencies: - mime-db: "npm:1.52.0" - checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 - languageName: node - linkType: hard - -"mimic-fn@npm:^2.1.0": - version: 2.1.0 - resolution: "mimic-fn@npm:2.1.0" - checksum: 10c0/b26f5479d7ec6cc2bce275a08f146cf78f5e7b661b18114e2506dd91ec7ec47e7a25bf4360e5438094db0560bcc868079fb3b1fb3892b833c1ecbf63f80c95a4 - languageName: node - linkType: hard - -"minipass@npm:^7.0.4, minipass@npm:^7.1.2": - version: 7.1.3 - resolution: "minipass@npm:7.1.3" - checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb - languageName: node - linkType: hard - -"minizlib@npm:^3.1.0": - version: 3.1.0 - resolution: "minizlib@npm:3.1.0" - dependencies: - minipass: "npm:^7.1.2" - checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec - languageName: node - linkType: hard - -"ms@npm:^2.1.3": - version: 2.1.3 - resolution: "ms@npm:2.1.3" - checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 - languageName: node - linkType: hard - -"mute-stream@npm:^3.0.0": - version: 3.0.0 - resolution: "mute-stream@npm:3.0.0" - checksum: 10c0/12cdb36a101694c7a6b296632e6d93a30b74401873cf7507c88861441a090c71c77a58f213acadad03bc0c8fa186639dec99d68a14497773a8744320c136e701 - languageName: node - linkType: hard - -"nanoid@npm:^3.3.12": - version: 3.3.12 - resolution: "nanoid@npm:3.3.12" - bin: - nanoid: bin/nanoid.cjs - checksum: 10c0/ba142b7b39e11e80c16dd74b0365d407880c87c1cf7e1480956981ae940ee36060fa5b6f092cd1e315184dd19244c657bd017d03327bd3c62247d691c5e8edfb - languageName: node - linkType: hard - -"node-fetch@npm:^2.6.1": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" - dependencies: - whatwg-url: "npm:^5.0.0" - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 - languageName: node - linkType: hard - -"node-gyp@npm:latest": - version: 12.3.0 - resolution: "node-gyp@npm:12.3.0" - dependencies: - env-paths: "npm:^2.2.0" - exponential-backoff: "npm:^3.1.1" - graceful-fs: "npm:^4.2.6" - nopt: "npm:^9.0.0" - proc-log: "npm:^6.0.0" - semver: "npm:^7.3.5" - tar: "npm:^7.5.4" - tinyglobby: "npm:^0.2.12" - undici: "npm:^6.25.0" - which: "npm:^6.0.0" - bin: - node-gyp: bin/node-gyp.js - checksum: 10c0/9d9032b405cbe42f72a105259d9eb679376470c102df4a2dbaa51e07d59bf741dcffb85897087ea9d8318b9cabb824a8978af51508ae142f0239ae1e6a3c2329 - languageName: node - linkType: hard - -"nopt@npm:^9.0.0": - version: 9.0.0 - resolution: "nopt@npm:9.0.0" - dependencies: - abbrev: "npm:^4.0.0" - bin: - nopt: bin/nopt.js - checksum: 10c0/1822eb6f9b020ef6f7a7516d7b64a8036e09666ea55ac40416c36e4b2b343122c3cff0e2f085675f53de1d2db99a2a89a60ccea1d120bcd6a5347bf6ceb4a7fd - languageName: node - linkType: hard - -"obug@npm:^2.1.1": - version: 2.1.2 - resolution: "obug@npm:2.1.2" - checksum: 10c0/e857080ef23c018bd4ad8553238cd97008cd4ab8472b4b83eafe75c19e9b6f84ac8fe53ef7f50b515a1dbf83e6372dd0b198aece33bc716edfa70366f6f6ed5e - languageName: node - linkType: hard - -"onetime@npm:^5.1.0": - version: 5.1.2 - resolution: "onetime@npm:5.1.2" - dependencies: - mimic-fn: "npm:^2.1.0" - checksum: 10c0/ffcef6fbb2692c3c40749f31ea2e22677a876daea92959b8a80b521d95cca7a668c884d8b2045d1d8ee7d56796aa405c405462af112a1477594cc63531baeb8f - languageName: node - linkType: hard - -"oxlint@npm:^0.16.0": - version: 0.16.12 - resolution: "oxlint@npm:0.16.12" - dependencies: - "@oxlint/darwin-arm64": "npm:0.16.12" - "@oxlint/darwin-x64": "npm:0.16.12" - "@oxlint/linux-arm64-gnu": "npm:0.16.12" - "@oxlint/linux-arm64-musl": "npm:0.16.12" - "@oxlint/linux-x64-gnu": "npm:0.16.12" - "@oxlint/linux-x64-musl": "npm:0.16.12" - "@oxlint/win32-arm64": "npm:0.16.12" - "@oxlint/win32-x64": "npm:0.16.12" - dependenciesMeta: - "@oxlint/darwin-arm64": - optional: true - "@oxlint/darwin-x64": - optional: true - "@oxlint/linux-arm64-gnu": - optional: true - "@oxlint/linux-arm64-musl": - optional: true - "@oxlint/linux-x64-gnu": - optional: true - "@oxlint/linux-x64-musl": - optional: true - "@oxlint/win32-arm64": - optional: true - "@oxlint/win32-x64": - optional: true - bin: - oxc_language_server: bin/oxc_language_server - oxlint: bin/oxlint - checksum: 10c0/209c3484039c4f1fdd340689e81be93a70fdf74e8c111731e70d81580d223b0029439107c95fa4118cec3c9b03ba6c56624d3dcb8cdc79af222da3561122b3e7 - languageName: node - linkType: hard - -"patch-console@npm:^2.0.0": - version: 2.0.0 - resolution: "patch-console@npm:2.0.0" - checksum: 10c0/486602591a0af7af8d4c76d8eea42cad32b6de7200488819c6383c75e43733ca7bdc80e30f2e68ce05f06a1607cce1683a1706c6672ca27dada1921b366e8f1c - languageName: node - linkType: hard - -"pathe@npm:^2.0.3": - version: 2.0.3 - resolution: "pathe@npm:2.0.3" - checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1 - languageName: node - linkType: hard - -"picocolors@npm:^1.1.1": - version: 1.1.1 - resolution: "picocolors@npm:1.1.1" - checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 - languageName: node - linkType: hard - -"picomatch@npm:^4.0.3, picomatch@npm:^4.0.4": - version: 4.0.4 - resolution: "picomatch@npm:4.0.4" - checksum: 10c0/e2c6023372cc7b5764719a5ffb9da0f8e781212fa7ca4bd0562db929df8e117460f00dff3cb7509dacfc06b86de924b247f504d0ce1806a37fac4633081466b0 - languageName: node - linkType: hard - -"postcss@npm:^8.5.15": - version: 8.5.15 - resolution: "postcss@npm:8.5.15" - dependencies: - nanoid: "npm:^3.3.12" - picocolors: "npm:^1.1.1" - source-map-js: "npm:^1.2.1" - checksum: 10c0/7f2e63ae22fbe43aace1bf652bd99da4e90737c64194d49e51ddc9cd0f9e51ff2861a7d734379b494deffa03a880a5c65eec70bc29ee9ebaa7136dde3eee8f31 - languageName: node - linkType: hard - -"preact@npm:^10.28.3": - version: 10.29.1 - resolution: "preact@npm:10.29.1" - checksum: 10c0/55da3994d0d9ebcc26476552c6571f8be1150cccdafe842aad40eff9abc84c867555f6d01db386fc63ac4cbe9f9da245002468bb24df0642365162d8f1b545e2 - languageName: node - linkType: hard - -"prettier@npm:^3.8.3": - version: 3.8.4 - resolution: "prettier@npm:3.8.4" - bin: - prettier: bin/prettier.cjs - checksum: 10c0/b90a0cbe75b88ac0af9c13fe0f359bd19926fabccd88483227b21f71f0c1cc42da056fc1ac3a361e665577c568371d5ccfb2c62c31c8a1186f8d1bd531a063e9 - languageName: node - linkType: hard - -"proc-log@npm:^6.0.0": - version: 6.1.0 - resolution: "proc-log@npm:6.1.0" - checksum: 10c0/4f178d4062733ead9d71a9b1ab24ebcecdfe2250916a5b1555f04fe2eda972a0ec76fbaa8df1ad9c02707add6749219d118a4fc46dc56bdfe4dde4b47d80bb82 - languageName: node - linkType: hard - -"proxy-from-env@npm:^2.1.0": - version: 2.1.0 - resolution: "proxy-from-env@npm:2.1.0" - checksum: 10c0/ed01729fd4d094eab619cd7e17ce3698b3413b31eb102c4904f9875e677cd207392795d5b4adee9cec359dfd31c44d5ad7595a3a3ad51c40250e141512281c58 - languageName: node - linkType: hard - -"react-dom@npm:^19.0.0": - version: 19.2.5 - resolution: "react-dom@npm:19.2.5" - dependencies: - scheduler: "npm:^0.27.0" - peerDependencies: - react: ^19.2.5 - checksum: 10c0/8067606e9f58e4c2e8cb5f09570217dbc71c4843ebcaa20ae2085912d3e3a351f17d8f7c1713313cdda7f272840c8c34ff6c860fcb840862071bceea218e0c63 - languageName: node - linkType: hard - -"react-dom@npm:^19.2.0": - version: 19.2.7 - resolution: "react-dom@npm:19.2.7" - dependencies: - scheduler: "npm:^0.27.0" - peerDependencies: - react: ^19.2.7 - checksum: 10c0/970ff600f6e80d47d39e2f226f12f226173b3cba3382efc97c5f0cd663de9af38c7a4c11c213fb936094faeac83060d660247accaa96b752180d5b951b9cfecb - languageName: node - linkType: hard - -"react-reconciler@npm:^0.33.0": - version: 0.33.0 - resolution: "react-reconciler@npm:0.33.0" - dependencies: - scheduler: "npm:^0.27.0" - peerDependencies: - react: ^19.2.0 - checksum: 10c0/3f7b27ea8d0ff4c8bf0e402a285e1af9b7d0e6f4c1a70a28f4384938bc1130bc82a90a31df0b79ef5e380e2e55e2598bd90b4dbf802b1203d735ba0355817d3a - languageName: node - linkType: hard - -"react@npm:^19.0.0": - version: 19.2.5 - resolution: "react@npm:19.2.5" - checksum: 10c0/4b5f231dbef92886f602533c9ce3bde04d99f0e71dfb5d794c43e02726efaad0421c08688f75fc98a6d6e1dc017372e1af7abbfecdc86a79968f461675931a7a - languageName: node - linkType: hard - -"react@npm:^19.2.0": - version: 19.2.7 - resolution: "react@npm:19.2.7" - checksum: 10c0/0bd0e2f1bbd4ba97561c6597bf8a5fec05e6476fe61e165c1065598d16668efc6715205599c94d3ddd49d36cb0f21cbf1b9bcc18ee840b805ce222c3e8d558ac - languageName: node - linkType: hard - -"readdirp@npm:^4.0.1": - version: 4.1.2 - resolution: "readdirp@npm:4.1.2" - checksum: 10c0/60a14f7619dec48c9c850255cd523e2717001b0e179dc7037cfa0895da7b9e9ab07532d324bfb118d73a710887d1e35f79c495fa91582784493e085d18c72c62 - languageName: node - linkType: hard - -"restore-cursor@npm:^4.0.0": - version: 4.0.0 - resolution: "restore-cursor@npm:4.0.0" - dependencies: - onetime: "npm:^5.1.0" - signal-exit: "npm:^3.0.2" - checksum: 10c0/6f7da8c5e422ac26aa38354870b1afac09963572cf2879443540449068cb43476e9cbccf6f8de3e0171e0d6f7f533c2bc1a0a008003c9a525bbc098e89041318 - languageName: node - linkType: hard - -"rolldown@npm:1.0.3": - version: 1.0.3 - resolution: "rolldown@npm:1.0.3" - dependencies: - "@oxc-project/types": "npm:=0.133.0" - "@rolldown/binding-android-arm64": "npm:1.0.3" - "@rolldown/binding-darwin-arm64": "npm:1.0.3" - "@rolldown/binding-darwin-x64": "npm:1.0.3" - "@rolldown/binding-freebsd-x64": "npm:1.0.3" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.3" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.3" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.3" - "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.3" - "@rolldown/binding-linux-s390x-gnu": "npm:1.0.3" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.3" - "@rolldown/binding-linux-x64-musl": "npm:1.0.3" - "@rolldown/binding-openharmony-arm64": "npm:1.0.3" - "@rolldown/binding-wasm32-wasi": "npm:1.0.3" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.3" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.3" - "@rolldown/pluginutils": "npm:^1.0.0" - dependenciesMeta: - "@rolldown/binding-android-arm64": - optional: true - "@rolldown/binding-darwin-arm64": - optional: true - "@rolldown/binding-darwin-x64": - optional: true - "@rolldown/binding-freebsd-x64": - optional: true - "@rolldown/binding-linux-arm-gnueabihf": - optional: true - "@rolldown/binding-linux-arm64-gnu": - optional: true - "@rolldown/binding-linux-arm64-musl": - optional: true - "@rolldown/binding-linux-ppc64-gnu": - optional: true - "@rolldown/binding-linux-s390x-gnu": - optional: true - "@rolldown/binding-linux-x64-gnu": - optional: true - "@rolldown/binding-linux-x64-musl": - optional: true - "@rolldown/binding-openharmony-arm64": - optional: true - "@rolldown/binding-wasm32-wasi": - optional: true - "@rolldown/binding-win32-arm64-msvc": - optional: true - "@rolldown/binding-win32-x64-msvc": - optional: true - bin: - rolldown: ./bin/cli.mjs - checksum: 10c0/5f9dd47b7abf203b16bc600db68542f245e974c800e59ff50b76157d1dada1403657690435b036fabca88e93d13a67c31abe5cfaa6f61ce33717f61720204cdf - languageName: node - linkType: hard - -"run-async@npm:^4.0.6": - version: 4.0.6 - resolution: "run-async@npm:4.0.6" - checksum: 10c0/3e512c689d356238a06a59839deddeb09aec23bc66f780fe970fcf12b64bfc00c6880e9530ea22b8cf88a927145561f5a43343d8be87166e849ec0daaa3d4cf4 - languageName: node - linkType: hard - -"safer-buffer@npm:>= 2.1.2 < 3.0.0": - version: 2.1.2 - resolution: "safer-buffer@npm:2.1.2" - checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 - languageName: node - linkType: hard - -"scheduler@npm:^0.27.0": - version: 0.27.0 - resolution: "scheduler@npm:0.27.0" - checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452 - languageName: node - linkType: hard - -"semver@npm:^7.3.5": - version: 7.7.4 - resolution: "semver@npm:7.7.4" - bin: - semver: bin/semver.js - checksum: 10c0/5215ad0234e2845d4ea5bb9d836d42b03499546ddafb12075566899fc617f68794bb6f146076b6881d755de17d6c6cc73372555879ec7dce2c2feee947866ad2 - languageName: node - linkType: hard - -"siginfo@npm:^2.0.0": - version: 2.0.0 - resolution: "siginfo@npm:2.0.0" - checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 - languageName: node - linkType: hard - -"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.7": - version: 3.0.7 - resolution: "signal-exit@npm:3.0.7" - checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 - languageName: node - linkType: hard - -"signal-exit@npm:^4.1.0": - version: 4.1.0 - resolution: "signal-exit@npm:4.1.0" - checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 - languageName: node - linkType: hard - -"slice-ansi@npm:^8.0.0": - version: 8.0.0 - resolution: "slice-ansi@npm:8.0.0" - dependencies: - ansi-styles: "npm:^6.2.3" - is-fullwidth-code-point: "npm:^5.1.0" - checksum: 10c0/0ce4aa91febb7cea4a00c2c27bb820fa53b6d2862ce0f80f7120134719f7914fc416b0ed966cf35250a3169e152916392f35917a2d7cad0fcc5d8b841010fa9a - languageName: node - linkType: hard - -"source-map-js@npm:^1.2.1": - version: 1.2.1 - resolution: "source-map-js@npm:1.2.1" - checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf - languageName: node - linkType: hard - -"stack-utils@npm:^2.0.6": - version: 2.0.6 - resolution: "stack-utils@npm:2.0.6" - dependencies: - escape-string-regexp: "npm:^2.0.0" - checksum: 10c0/651c9f87667e077584bbe848acaecc6049bc71979f1e9a46c7b920cad4431c388df0f51b8ad7cfd6eed3db97a2878d0fc8b3122979439ea8bac29c61c95eec8a - languageName: node - linkType: hard - -"stackback@npm:0.0.2": - version: 0.0.2 - resolution: "stackback@npm:0.0.2" - checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 - languageName: node - linkType: hard - -"std-env@npm:^4.0.0-rc.1": - version: 4.1.0 - resolution: "std-env@npm:4.1.0" - checksum: 10c0/2e14b6b490db34cb969a48d9cf7c35bca4a47653914aac2814221baae7b867a5b15940d133625c391621971f98cd2266a5dc7036669960e883f1081db2a56558 - languageName: node - linkType: hard - -"string-width@npm:^7.0.0": - version: 7.2.0 - resolution: "string-width@npm:7.2.0" - dependencies: - emoji-regex: "npm:^10.3.0" - get-east-asian-width: "npm:^1.0.0" - strip-ansi: "npm:^7.1.0" - checksum: 10c0/eb0430dd43f3199c7a46dcbf7a0b34539c76fe3aa62763d0b0655acdcbdf360b3f66f3d58ca25ba0205f42ea3491fa00f09426d3b7d3040e506878fc7664c9b9 - languageName: node - linkType: hard - -"string-width@npm:^8.1.0, string-width@npm:^8.1.1, string-width@npm:^8.2.0": - version: 8.2.0 - resolution: "string-width@npm:8.2.0" - dependencies: - get-east-asian-width: "npm:^1.5.0" - strip-ansi: "npm:^7.1.2" - checksum: 10c0/d8915428b43519b0f494da6590dbe4491857d8a12e40250e50fc01fbb616ffd8400a436bbe25712255ee129511fe0414c49d3b6b9627e2bc3a33dcec1d2eda02 - languageName: node - linkType: hard - -"strip-ansi@npm:^7.1.0, strip-ansi@npm:^7.1.2": - version: 7.2.0 - resolution: "strip-ansi@npm:7.2.0" - dependencies: - ansi-regex: "npm:^6.2.2" - checksum: 10c0/544d13b7582f8254811ea97db202f519e189e59d35740c46095897e254e4f1aa9fe1524a83ad6bc5ad67d4dd6c0281d2e0219ed62b880a6238a16a17d375f221 - languageName: node - linkType: hard - -"subscriptions-transport-ws@npm:^0.9.16": - version: 0.9.19 - resolution: "subscriptions-transport-ws@npm:0.9.19" - dependencies: - backo2: "npm:^1.0.2" - eventemitter3: "npm:^3.1.0" - iterall: "npm:^1.2.1" - symbol-observable: "npm:^1.0.4" - ws: "npm:^5.2.0 || ^6.0.0 || ^7.0.0" - peerDependencies: - graphql: ">=0.10.0" - checksum: 10c0/6f2ade56865f0ba291d3ff82c79781b051c2374873bac853286fedfdbc05001b8c4018ab7cba44af667ead7f573e48d18892d58a8f9ca8d90dfb4bff5c125045 - languageName: node - linkType: hard - -"symbol-observable@npm:^1.0.4": - version: 1.2.0 - resolution: "symbol-observable@npm:1.2.0" - checksum: 10c0/009fee50798ef80ed4b8195048288f108b03de162db07493f2e1fd993b33fafa72d659e832b584da5a2427daa78e5a738fb2a9ab027ee9454252e0bedbcd1fdc - languageName: node - linkType: hard - -"tagged-tag@npm:^1.0.0": - version: 1.0.0 - resolution: "tagged-tag@npm:1.0.0" - checksum: 10c0/91d25c9ffb86a91f20522cefb2cbec9b64caa1febe27ad0df52f08993ff60888022d771e868e6416cf2e72dab68449d2139e8709ba009b74c6c7ecd4000048d1 - languageName: node - linkType: hard - -"tar@npm:^7.5.4": - version: 7.5.16 - resolution: "tar@npm:7.5.16" - dependencies: - "@isaacs/fs-minipass": "npm:^4.0.0" - chownr: "npm:^3.0.0" - minipass: "npm:^7.1.2" - minizlib: "npm:^3.1.0" - yallist: "npm:^5.0.0" - checksum: 10c0/4f37f3c4bd2ca2755fd736a5df1d573c1a868ec1b1e893346aeafa95ac510f9e2fd1469420bd866cc7904799e5bd4ac62b5d4f03fe27747d6e1e373b44505c5c - languageName: node - linkType: hard - -"terminal-size@npm:^4.0.1": - version: 4.0.1 - resolution: "terminal-size@npm:4.0.1" - checksum: 10c0/89afd9d816dd9dbfe4499da9aeea70491bbde4ff4592226a9c8ac71074a7580afead6a78e95ecc35f6d42e09087b55ffcb1019302cd55e0cc957b6ce5c4847e8 - languageName: node - linkType: hard - -"tinybench@npm:^2.9.0": - version: 2.9.0 - resolution: "tinybench@npm:2.9.0" - checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c - languageName: node - linkType: hard - -"tinyexec@npm:^1.0.2": - version: 1.2.4 - resolution: "tinyexec@npm:1.2.4" - checksum: 10c0/153b8db6b080194b558ff145b9cffc36b80a6e07babd644dcfbe49c807eee668c876049d28bdee90b96304476f883352f2dad91b3f86bc23832532f4363e66ff - languageName: node - linkType: hard - -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": - version: 0.2.16 - resolution: "tinyglobby@npm:0.2.16" - dependencies: - fdir: "npm:^6.5.0" - picomatch: "npm:^4.0.4" - checksum: 10c0/f2e09fd93dd95c41e522113b686ff6f7c13020962f8698a864a257f3d7737599afc47722b7ab726e12f8a813f779906187911ff8ee6701ede65072671a7e934b - languageName: node - linkType: hard - -"tinyglobby@npm:^0.2.17": - version: 0.2.17 - resolution: "tinyglobby@npm:0.2.17" - dependencies: - fdir: "npm:^6.5.0" - picomatch: "npm:^4.0.4" - checksum: 10c0/7f7bb0f197c88bc4b20c231e0deca4240ca3bf313a88f5a7fee93a872b84966a4d50220947c0455ad07a60b3b360961c5b7fd979222aeb716a9f99b412002e4c - languageName: node - linkType: hard - -"tinyrainbow@npm:^3.1.0": - version: 3.1.0 - resolution: "tinyrainbow@npm:3.1.0" - checksum: 10c0/f11cf387a26c5c9255bec141a90ac511b26172981b10c3e50053bc6700ea7d2336edcc4a3a21dbb8412fe7c013477d2ba4d7e4877800f3f8107be5105aad6511 - languageName: node - linkType: hard - -"tr46@npm:~0.0.3": - version: 0.0.3 - resolution: "tr46@npm:0.0.3" - checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11 - languageName: node - linkType: hard - -"tsconfck@npm:^3.0.3": - version: 3.1.6 - resolution: "tsconfck@npm:3.1.6" - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - bin: - tsconfck: bin/tsconfck.js - checksum: 10c0/269c3c513540be44844117bb9b9258fe6f8aeab026d32aeebf458d5299125f330711429dbb556dbf125a0bc25f4a81e6c24ac96de2740badd295c3fb400f66c4 - languageName: node - linkType: hard - -"tslib@npm:^1.9.3": - version: 1.14.1 - resolution: "tslib@npm:1.14.1" - checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 - languageName: node - linkType: hard - -"tslib@npm:^2.0.0, tslib@npm:^2.4.0": - version: 2.8.1 - resolution: "tslib@npm:2.8.1" - checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 - languageName: node - linkType: hard - -"twenty-client-sdk@npm:2.13.0": - version: 2.13.0 - resolution: "twenty-client-sdk@npm:2.13.0" - dependencies: - "@genql/runtime": "npm:^2.10.0" - esbuild: "npm:^0.28.1" - graphql: "npm:^16.8.1" - lodash: "npm:^4.17.21" - prettier: "npm:^3.8.3" - checksum: 10c0/740464acec94c1d4cc5fa50ed6b190a7f1d1681963f61437a6c704835c05ffe0f5a13e254e57601a99b07bbe0f68483dee19211731853aafbcc15ec79f8039ce - languageName: node - linkType: hard - -"twenty-sdk@npm:2.13.0": - version: 2.13.0 - resolution: "twenty-sdk@npm:2.13.0" - dependencies: - "@sniptt/guards": "npm:^0.2.0" - axios: "npm:^1.16.0" - chalk: "npm:^5.3.0" - chokidar: "npm:^4.0.0" - commander: "npm:^12.0.0" - esbuild: "npm:^0.28.1" - graphql: "npm:^16.8.1" - graphql-sse: "npm:^2.5.4" - ink: "npm:^6.8.0" - inquirer: "npm:^14.0.0" - jsonc-parser: "npm:^3.2.0" - preact: "npm:^10.28.3" - react: "npm:^19.2.0" - react-dom: "npm:^19.2.0" - tinyglobby: "npm:^0.2.15" - twenty-client-sdk: "npm:2.13.0" - typescript: "npm:^5.9.3" - uuid: "npm:^13.0.2" - bin: - twenty: dist/cli.cjs - checksum: 10c0/51c350abe5d344fcad3c961a77632fd3cb2d91e357d0562b3eb02f05f66dbc41221c5463d0f91c022316a16af03a05ad43f038d9534c0ae31c060008d0e48672 - languageName: node - linkType: hard - -"type-fest@npm:^5.4.1": - version: 5.6.0 - resolution: "type-fest@npm:5.6.0" - dependencies: - tagged-tag: "npm:^1.0.0" - checksum: 10c0/5468a8ffda7f3904e6f7bbd8069eb8b6dd4bd9156e206df7a01d09a73e28cd1afedf74ead9d0fc12841c8c90074194859feca240511c50800962fde1bd9ddcbc - languageName: node - linkType: hard - -"typescript@npm:^5.9.3": - version: 5.9.3 - resolution: "typescript@npm:5.9.3" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5 - languageName: node - linkType: hard - -"typescript@patch:typescript@npm%3A^5.9.3#optional!builtin": - version: 5.9.3 - resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430 - languageName: node - linkType: hard - -"undici-types@npm:~7.16.0": - version: 7.16.0 - resolution: "undici-types@npm:7.16.0" - checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a - languageName: node - linkType: hard - -"undici-types@npm:~7.19.0": - version: 7.19.2 - resolution: "undici-types@npm:7.19.2" - checksum: 10c0/7159f10546f9f6c47d36776bb1bbf8671e87c1e587a6fee84ae1f111ae8de4f914efa8ca0dfcd224f4f4a9dfc3f6028f627ccb5ddaccf82d7fd54671b89fac3e - languageName: node - linkType: hard - -"undici@npm:^6.25.0": - version: 6.27.0 - resolution: "undici@npm:6.27.0" - checksum: 10c0/f88c3dae3957dbf9d93cb481440aced317bd3c4941b5914fea5efba516d51138988cdb5c76006f0bb1337e41d56c3443351055d492e73af2428521c37ba2a76f - languageName: node - linkType: hard - -"unfetch@npm:^4.2.0": - version: 4.2.0 - resolution: "unfetch@npm:4.2.0" - checksum: 10c0/a5c0a896a6f09f278b868075aea65652ad185db30e827cb7df45826fe5ab850124bf9c44c4dafca4bf0c55a0844b17031e8243467fcc38dd7a7d435007151f1b - languageName: node - linkType: hard - -"utility-types@npm:^3.10.0": - version: 3.11.0 - resolution: "utility-types@npm:3.11.0" - checksum: 10c0/2f1580137b0c3e6cf5405f37aaa8f5249961a76d26f1ca8efc0ff49a2fc0e0b2db56de8e521a174d075758e0c7eb3e590edec0832eb44478b958f09914920f19 - languageName: node - linkType: hard - -"uuid@npm:^13.0.2": - version: 13.0.2 - resolution: "uuid@npm:13.0.2" - bin: - uuid: dist-node/bin/uuid - checksum: 10c0/32c7ee84fa7c7966cc09b3a1514a752a8e2f609f15c00033fbaedc0255d577d1877b839f11ee537f37e587bbc620bbb98c3b3a1482931b8ed47d79497cd7a7cd - languageName: node - linkType: hard - -"vite-tsconfig-paths@npm:^4.2.1": - version: 4.3.2 - resolution: "vite-tsconfig-paths@npm:4.3.2" - dependencies: - debug: "npm:^4.1.1" - globrex: "npm:^0.1.2" - tsconfck: "npm:^3.0.3" - peerDependencies: - vite: "*" - peerDependenciesMeta: - vite: - optional: true - checksum: 10c0/f390ac1d1c3992fc5ac50f9274c1090f8b55ab34a89ea88893db9a6924a3b26c9f64bc1163615150ad100749db73b6b2cf1d57f6cd60df6e762ceb5b8ad30024 - languageName: node - linkType: hard - -"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": - version: 8.0.16 - resolution: "vite@npm:8.0.16" - dependencies: - fsevents: "npm:~2.3.3" - lightningcss: "npm:^1.32.0" - picomatch: "npm:^4.0.4" - postcss: "npm:^8.5.15" - rolldown: "npm:1.0.3" - tinyglobby: "npm:^0.2.17" - peerDependencies: - "@types/node": ^20.19.0 || >=22.12.0 - "@vitejs/devtools": ^0.1.18 - esbuild: ^0.27.0 || ^0.28.0 - jiti: ">=1.21.0" - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: ">=0.54.8" - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - dependenciesMeta: - fsevents: - optional: true - peerDependenciesMeta: - "@types/node": - optional: true - "@vitejs/devtools": - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - bin: - vite: bin/vite.js - checksum: 10c0/d75be3fbe2f63e6a8145325970338afaf0dd4d96ba9175c13f9a286fd5f95afc489401b693e4fa6c0899a4dd0e137be91cdf9401a40a635563911ad5036e3467 - languageName: node - linkType: hard - -"vitest@npm:^4.0.0": - version: 4.1.8 - resolution: "vitest@npm:4.1.8" - dependencies: - "@vitest/expect": "npm:4.1.8" - "@vitest/mocker": "npm:4.1.8" - "@vitest/pretty-format": "npm:4.1.8" - "@vitest/runner": "npm:4.1.8" - "@vitest/snapshot": "npm:4.1.8" - "@vitest/spy": "npm:4.1.8" - "@vitest/utils": "npm:4.1.8" - es-module-lexer: "npm:^2.0.0" - expect-type: "npm:^1.3.0" - magic-string: "npm:^0.30.21" - obug: "npm:^2.1.1" - pathe: "npm:^2.0.3" - picomatch: "npm:^4.0.3" - std-env: "npm:^4.0.0-rc.1" - tinybench: "npm:^2.9.0" - tinyexec: "npm:^1.0.2" - tinyglobby: "npm:^0.2.15" - tinyrainbow: "npm:^3.1.0" - vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0" - why-is-node-running: "npm:^2.3.0" - peerDependencies: - "@edge-runtime/vm": "*" - "@opentelemetry/api": ^1.9.0 - "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.1.8 - "@vitest/browser-preview": 4.1.8 - "@vitest/browser-webdriverio": 4.1.8 - "@vitest/coverage-istanbul": 4.1.8 - "@vitest/coverage-v8": 4.1.8 - "@vitest/ui": 4.1.8 - happy-dom: "*" - jsdom: "*" - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - "@edge-runtime/vm": - optional: true - "@opentelemetry/api": - optional: true - "@types/node": - optional: true - "@vitest/browser-playwright": - optional: true - "@vitest/browser-preview": - optional: true - "@vitest/browser-webdriverio": - optional: true - "@vitest/coverage-istanbul": - optional: true - "@vitest/coverage-v8": - optional: true - "@vitest/ui": - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vite: - optional: false - bin: - vitest: vitest.mjs - checksum: 10c0/f459c500f8818c7a2318cd23228b4e5c0b5efb25bf8e5cb7f116c6d26e51482b2f800a8bb19837c0b5f0d05c51519edbf502bc8ceb5bd86868e8facf1d2c498e - languageName: node - linkType: hard - -"webidl-conversions@npm:^3.0.0": - version: 3.0.1 - resolution: "webidl-conversions@npm:3.0.1" - checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db - languageName: node - linkType: hard - -"whatwg-url@npm:^5.0.0": - version: 5.0.0 - resolution: "whatwg-url@npm:5.0.0" - dependencies: - tr46: "npm:~0.0.3" - webidl-conversions: "npm:^3.0.0" - checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5 - languageName: node - linkType: hard - -"which@npm:^6.0.0": - version: 6.0.1 - resolution: "which@npm:6.0.1" - dependencies: - isexe: "npm:^4.0.0" - bin: - node-which: bin/which.js - checksum: 10c0/7e710e54ea36d2d6183bee2f9caa27a3b47b9baf8dee55a199b736fcf85eab3b9df7556fca3d02b50af7f3dfba5ea3a45644189836df06267df457e354da66d5 - languageName: node - linkType: hard - -"why-is-node-running@npm:^2.3.0": - version: 2.3.0 - resolution: "why-is-node-running@npm:2.3.0" - dependencies: - siginfo: "npm:^2.0.0" - stackback: "npm:0.0.2" - bin: - why-is-node-running: cli.js - checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 - languageName: node - linkType: hard - -"widest-line@npm:^6.0.0": - version: 6.0.0 - resolution: "widest-line@npm:6.0.0" - dependencies: - string-width: "npm:^8.1.0" - checksum: 10c0/735f1fdcd97fe765a07bb8b5e73c020bed8e53ab34e83ce0ef01693ba3c914d9e7977fe5f5facf0d0b670297a82dd5e376d3efa0896860dfcdaf7cd6924c0fb7 - languageName: node - linkType: hard - -"wrap-ansi@npm:^9.0.0": - version: 9.0.2 - resolution: "wrap-ansi@npm:9.0.2" - dependencies: - ansi-styles: "npm:^6.2.1" - string-width: "npm:^7.0.0" - strip-ansi: "npm:^7.1.0" - checksum: 10c0/3305839b9a0d6fb930cb63a52f34d3936013d8b0682ff3ec133c9826512620f213800ffa19ea22904876d5b7e9a3c1f40682f03597d986a4ca881fa7b033688c - languageName: node - linkType: hard - -"ws@npm:^5.2.0 || ^6.0.0 || ^7.0.0": - version: 7.5.11 - resolution: "ws@npm:7.5.11" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10c0/7972670b676fb1ccba73b0899ca3c2e04e8c2075629c2614cced7f556536f96a672bbf4619fc5a06c8b8720bb839a47ca88c69c95dc14c9c61a99fbecba1c866 - languageName: node - linkType: hard - -"ws@npm:^6.1.4": - version: 6.2.4 - resolution: "ws@npm:6.2.4" - dependencies: - async-limiter: "npm:~1.0.0" - checksum: 10c0/5c2b9474164f9cb68c7776a1d10b0461c186f3a69bffb1028fca33eba5ab7206a09173fb0b311d6c5a81c8cf148406f8deb0b7d899542ab8ca67407d99717dad - languageName: node - linkType: hard - -"ws@npm:^8.18.0": - version: 8.21.0 - resolution: "ws@npm:8.21.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10c0/ef4a243476283fc49bc7550966c4af4aa0eef56273837211e700de3b664e08604a760cdddcb5ba43c049140e74ccfec5b0ee0bb439e08c2adf9138902fdde5f9 - languageName: node - linkType: hard - -"yallist@npm:^5.0.0": - version: 5.0.0 - resolution: "yallist@npm:5.0.0" - checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 - languageName: node - linkType: hard - -"yoga-layout@npm:~3.2.1": - version: 3.2.1 - resolution: "yoga-layout@npm:3.2.1" - checksum: 10c0/9001e51be993c85e03757e5a04a2b61b8b30c9e5a7865d0156ca87a6431a3b717d51eb4990bfe588189fcfeac688dd9c3de707bbd50d1c344a84e63974cc54a8 - languageName: node - linkType: hard - -"zen-observable-ts@npm:^0.8.21": - version: 0.8.21 - resolution: "zen-observable-ts@npm:0.8.21" - dependencies: - tslib: "npm:^1.9.3" - zen-observable: "npm:^0.8.0" - checksum: 10c0/fe4a02f862b5f7e8ae0f86230c37b84c7d5611f5c206981afb4043e732d04cf7067a6cbe1ba82d20f18b735a3387937195a12542158a631d308ae3959a1d93c4 - languageName: node - linkType: hard - -"zen-observable@npm:^0.8.0": - version: 0.8.15 - resolution: "zen-observable@npm:0.8.15" - checksum: 10c0/71cc2f2bbb537300c3f569e25693d37b3bc91f225cefce251a71c30bc6bb3e7f8e9420ca0eb57f2ac9e492b085b8dfa075fd1e8195c40b83c951dd59c6e4fbf8 - languageName: node - linkType: hard diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/package.json b/packages/twenty-apps/internal/twenty-meeting-bot/package.json index fdcf2acab1..b8cd1e9e3d 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/package.json +++ b/packages/twenty-apps/internal/twenty-meeting-bot/package.json @@ -13,6 +13,7 @@ "twenty": "twenty", "lint": "oxlint -c .oxlintrc.json .", "lint:fix": "oxlint --fix -c .oxlintrc.json .", + "typecheck": "tsgo --noEmit -p tsconfig.spec.json", "test": "vitest run", "test:unit": "vitest run --config vitest.unit.config.ts", "test:unit:watch": "vitest --config vitest.unit.config.ts", @@ -26,6 +27,7 @@ "@emotion/styled": "^11.14.0", "@types/node": "^24.7.2", "@types/react": "^19.0.0", + "@typescript/native-preview": "^7.0.0-dev.20260116.1", "oxlint": "^0.16.0", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/complete-call-recording-ingestion.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/complete-call-recording-ingestion.util.ts index 4a48a91e47..22d3da8299 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/complete-call-recording-ingestion.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/complete-call-recording-ingestion.util.ts @@ -1,4 +1,4 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/create-call-recording.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/create-call-recording.util.ts index 8b653bc104..c8ed9a95f5 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/create-call-recording.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/create-call-recording.util.ts @@ -1,8 +1,8 @@ import { isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; -import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; +import { type CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; +import { type CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; export type ScheduledCallRecordingFields = { title: string | null; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts index 18cfb33172..f7d6b7ec8f 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts @@ -1,5 +1,5 @@ import { isString, isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { TWENTY_PAGE_SIZE } from 'src/logic-functions/constants/twenty-page-size'; import { type CalendarEventRecord } from 'src/logic-functions/types/calendar-event-record.type'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts index a6144f712b..d703198a5f 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts @@ -1,4 +1,4 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { type CalendarEventRecord } from 'src/logic-functions/types/calendar-event-record.type'; import { fetchCalendarEventsByFilter } from 'src/logic-functions/data/fetch-calendar-events-by-filter.util'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts index a5a70b9c46..aa5609cf8c 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts @@ -1,4 +1,4 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { type CalendarEventRecord } from 'src/logic-functions/types/calendar-event-record.type'; import { fetchCalendarEventsByFilter } from 'src/logic-functions/data/fetch-calendar-events-by-filter.util'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts index bfbbe14bbf..b07ed738e7 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts @@ -1,4 +1,4 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type'; import { findCallRecordingsByFilter } from 'src/logic-functions/data/find-call-recordings-by-filter.util'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-filter.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-filter.util.ts index c17c2f1a7c..98cc6994c0 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-filter.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-filter.util.ts @@ -1,5 +1,5 @@ import { isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; import { TWENTY_PAGE_SIZE } from 'src/logic-functions/constants/twenty-page-size'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-ids.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-ids.util.ts index 8559795859..5ef2493faf 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-ids.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-ids.util.ts @@ -1,4 +1,4 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type'; import { findCallRecordingsByFilter } from 'src/logic-functions/data/find-call-recordings-by-filter.util'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts index ef8571624a..5bd855c6f4 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts @@ -1,4 +1,4 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/update-call-recording.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/update-call-recording.util.ts index bbe8bd0012..6519c5a15f 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/update-call-recording.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/data/update-call-recording.util.ts @@ -1,4 +1,4 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { type CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; import { type CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/cancel-call-recording-request.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/cancel-call-recording-request.util.ts index 700421d9f0..8184955a53 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/cancel-call-recording-request.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/cancel-call-recording-request.util.ts @@ -1,5 +1,5 @@ import { isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/complete-and-charge-call-recording.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/complete-and-charge-call-recording.util.ts index ebe3a7c6a1..1c1dff964b 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/complete-and-charge-call-recording.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/complete-and-charge-call-recording.util.ts @@ -1,4 +1,4 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { completeCallRecordingIngestion } from 'src/logic-functions/data/complete-call-recording-ingestion.util'; import { chargeCompletedCallRecording } from 'src/logic-functions/flows/charge-completed-call-recording.util'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/converge-diverged-call-recordings.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/converge-diverged-call-recordings.util.ts index 5f0576fd9c..f1082f9f42 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/converge-diverged-call-recordings.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/converge-diverged-call-recordings.util.ts @@ -1,5 +1,5 @@ import { isNonEmptyArray, isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/ensure-meeting-bot.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/ensure-meeting-bot.util.ts index 286d1aad64..c9b9fc0e2e 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/ensure-meeting-bot.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/ensure-meeting-bot.util.ts @@ -1,5 +1,5 @@ import { isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/handle-recall-webhook.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/handle-recall-webhook.util.ts index b75d0c6023..4b8719893b 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/handle-recall-webhook.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/handle-recall-webhook.util.ts @@ -1,5 +1,5 @@ import { isNonEmptyArray, isNull, isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; import { type FilesFieldValue } from 'src/logic-functions/types/files-field-value.type'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts index 6dbbe8af75..a445a00a2a 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts @@ -1,5 +1,5 @@ import { isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { type CalendarEventRecord } from 'src/logic-functions/types/calendar-event-record.type'; import { ensureMeetingBot } from 'src/logic-functions/flows/ensure-meeting-bot.util'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/persist-call-recording-progress.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/persist-call-recording-progress.util.ts index 6a1a620786..336c1d9226 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/persist-call-recording-progress.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/persist-call-recording-progress.util.ts @@ -1,4 +1,4 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { type FilesFieldValue } from 'src/logic-functions/types/files-field-value.type'; import { completeAndChargeCallRecording } from 'src/logic-functions/flows/complete-and-charge-call-recording.util'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reap-orphaned-meeting-bots.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reap-orphaned-meeting-bots.util.ts index a49a0ec150..099a5fc6c4 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reap-orphaned-meeting-bots.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reap-orphaned-meeting-bots.util.ts @@ -1,5 +1,5 @@ import { isNull, isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { APPLICATION_ID_ENV_VAR_NAME } from 'src/logic-functions/constants/application-id-env-var-name'; import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reconcile-meeting-bot.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reconcile-meeting-bot.util.ts index 363cdd931a..615c12a014 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reconcile-meeting-bot.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reconcile-meeting-bot.util.ts @@ -1,5 +1,5 @@ import { isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reschedule-call-recording-bot.util.ts b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reschedule-call-recording-bot.util.ts index 693670ece9..c6a564cc69 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reschedule-call-recording-bot.util.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/src/logic-functions/flows/reschedule-call-recording-bot.util.ts @@ -1,5 +1,5 @@ import { isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; +import { type CoreApiClient } from 'twenty-client-sdk/core'; import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type'; import { buildRecallBotMetadata } from 'src/logic-functions/domain/build-recall-bot-metadata.util'; diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/tsconfig.json b/packages/twenty-apps/internal/twenty-meeting-bot/tsconfig.json index d574c8c810..f71645f97a 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/tsconfig.json +++ b/packages/twenty-apps/internal/twenty-meeting-bot/tsconfig.json @@ -6,7 +6,7 @@ "outDir": "./dist", "rootDir": ".", "jsx": "react-jsx", - "moduleResolution": "node", + "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/vitest.config.ts b/packages/twenty-apps/internal/twenty-meeting-bot/vitest.config.ts index 8f359b3659..055af77dbe 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/vitest.config.ts +++ b/packages/twenty-apps/internal/twenty-meeting-bot/vitest.config.ts @@ -1,18 +1,14 @@ -import { loadEnv } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; -const fileEnv = loadEnv('test', process.cwd(), 'TWENTY_'); - -const TWENTY_API_URL = - process.env.TWENTY_API_URL ?? fileEnv.TWENTY_API_URL ?? 'http://localhost:2020'; -const TWENTY_API_KEY = process.env.TWENTY_API_KEY ?? fileEnv.TWENTY_API_KEY; +const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; +const TWENTY_API_KEY = + process.env.TWENTY_API_KEY ?? + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc'; // Make env vars available to globalSetup (test.env only applies to workers) process.env.TWENTY_API_URL = TWENTY_API_URL; -if (TWENTY_API_KEY) { - process.env.TWENTY_API_KEY = TWENTY_API_KEY; -} +process.env.TWENTY_API_KEY = TWENTY_API_KEY; export default defineConfig({ plugins: [ @@ -29,7 +25,7 @@ export default defineConfig({ globalSetup: ['src/__tests__/global-setup.ts'], env: { TWENTY_API_URL, - ...(TWENTY_API_KEY ? { TWENTY_API_KEY } : {}), + TWENTY_API_KEY, }, }, }); diff --git a/packages/twenty-apps/internal/twenty-meeting-bot/yarn.lock b/packages/twenty-apps/internal/twenty-meeting-bot/yarn.lock index 2dc1d33658..c5ce1d0aca 100644 --- a/packages/twenty-apps/internal/twenty-meeting-bot/yarn.lock +++ b/packages/twenty-apps/internal/twenty-meeting-bot/yarn.lock @@ -1250,6 +1250,87 @@ __metadata: languageName: node linkType: hard +"@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260619.1": + version: 7.0.0-dev.20260619.1 + resolution: "@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260619.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260619.1": + version: 7.0.0-dev.20260619.1 + resolution: "@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260619.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260619.1": + version: 7.0.0-dev.20260619.1 + resolution: "@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260619.1" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260619.1": + version: 7.0.0-dev.20260619.1 + resolution: "@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260619.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260619.1": + version: 7.0.0-dev.20260619.1 + resolution: "@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260619.1" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260619.1": + version: 7.0.0-dev.20260619.1 + resolution: "@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260619.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260619.1": + version: 7.0.0-dev.20260619.1 + resolution: "@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260619.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@typescript/native-preview@npm:^7.0.0-dev.20260116.1": + version: 7.0.0-dev.20260619.1 + resolution: "@typescript/native-preview@npm:7.0.0-dev.20260619.1" + dependencies: + "@typescript/native-preview-darwin-arm64": "npm:7.0.0-dev.20260619.1" + "@typescript/native-preview-darwin-x64": "npm:7.0.0-dev.20260619.1" + "@typescript/native-preview-linux-arm": "npm:7.0.0-dev.20260619.1" + "@typescript/native-preview-linux-arm64": "npm:7.0.0-dev.20260619.1" + "@typescript/native-preview-linux-x64": "npm:7.0.0-dev.20260619.1" + "@typescript/native-preview-win32-arm64": "npm:7.0.0-dev.20260619.1" + "@typescript/native-preview-win32-x64": "npm:7.0.0-dev.20260619.1" + dependenciesMeta: + "@typescript/native-preview-darwin-arm64": + optional: true + "@typescript/native-preview-darwin-x64": + optional: true + "@typescript/native-preview-linux-arm": + optional: true + "@typescript/native-preview-linux-arm64": + optional: true + "@typescript/native-preview-linux-x64": + optional: true + "@typescript/native-preview-win32-arm64": + optional: true + "@typescript/native-preview-win32-x64": + optional: true + bin: + tsgo: bin/tsgo.js + checksum: 10c0/e8bc6cf171ee29131934f662c9b3577374d7e872247831da36a5723abe175998f1318a134ed8eb0ee5213887dbb1f1209fac5555cdf5b6b19d0d7da044d0cdc9 + languageName: node + linkType: hard + "@vitest/expect@npm:3.2.6": version: 3.2.6 resolution: "@vitest/expect@npm:3.2.6" @@ -3143,6 +3224,7 @@ __metadata: "@sniptt/guards": "npm:^0.2.0" "@types/node": "npm:^24.7.2" "@types/react": "npm:^19.0.0" + "@typescript/native-preview": "npm:^7.0.0-dev.20260116.1" oxlint: "npm:^0.16.0" react: "npm:^19.0.0" react-dom: "npm:^19.0.0"