Compare commits

..

7 Commits

Author SHA1 Message Date
neo773 b0553677c7 wip 2026-05-07 15:25:04 +05:30
neo773 0fe1eca010 dump expiremental code 2026-05-07 15:24:57 +05:30
twenty-pr[bot] f720186122 chore: bump version to 2.4.0 (#20345)
## Summary

- Moves current version to previous versions array
- Sets TWENTY_CURRENT_VERSION to the new version
- Updates TWENTY_NEXT_VERSIONS with the next minor version

## Checklist

- [x] Verify version constants are correct

---------

Co-authored-by: Github Action Deploy <github-action-deploy@twenty.com>
Co-authored-by: prastoin <paul@twenty.com>
2026-05-07 09:41:47 +00:00
github-actions[bot] 9f930aa366 i18n - translations (#20347)
Created by Github action

---------

Co-authored-by: github-actions <github-actions@twenty.com>
2026-05-07 11:19:50 +02:00
Etienne e49673df79 Fix plan-required modal issue (#20346)
Commit ee6c0ef904 (Replace sign-in mocked metadata with hardcoded
BackgroundMock) removed the mocked metadata loading path in
MinimalMetadataLoadEffect. Before this change, users with an access
token but an inactive workspace (plan-required state) would get mocked
metadata loaded, which satisfied IsMinimalMetadataReadyEffect's
areObjectsLoaded check. After the change, shouldLoadRealMetadata =
hasAccessTokenPair && isActiveWorkspace is false for plan-required
users, so nothing is loaded, isMinimalMetadataReady stays false, and
MinimalMetadataGater renders the loading skeleton forever instead of the
actual auth modal content.

Fix: Add AppPath.PlanRequired and AppPath.PlanRequiredSuccess to
isOnExcludedPath in MinimalMetadataGater, mirroring how AppPath.Invite
is already excluded — both are pages where the user may have a token but
the workspace isn't fully active, so they don't need metadata to render.
2026-05-07 11:12:50 +02:00
martmull 900f70fb9e Add description to oAuth_only app created (#20336)
## Before

<img width="1000" height="555" alt="image"
src="https://github.com/user-attachments/assets/bda317c7-93e7-448e-8e65-a9cc1be6df95"
/>

## After
<img width="1010" height="504" alt="image"
src="https://github.com/user-attachments/assets/5cbc5c6c-5cc3-40d8-9545-ba0f3d2675b4"
/>
<img width="980" height="574" alt="image"
src="https://github.com/user-attachments/assets/4386fe56-0614-4a4e-82a0-c9f46af69c85"
/>
2026-05-07 09:02:04 +00:00
Charles Bochet 027331c893 chore(front): move mocked-metadata helpers under src/testing (#20341)
## Summary

Follow-up to #20308. After that PR, production code no longer loads
mocked metadata when the user is unauthenticated. The remaining
mocked-metadata helpers (`useLoadMockedMetadata`,
`preloadMockedMetadata`) are now only consumed by Storybook decorators,
so move them next to the other testing-only utilities to make their
scope explicit and prevent accidental re-introduction of a runtime
dependency on mocks.

- `src/modules/metadata-store/hooks/useLoadMockedMetadata.ts`
  → `src/testing/hooks/useLoadMockedMetadata.ts`
- `src/modules/metadata-store/utils/preloadMockedMetadata.ts`
  → `src/testing/utils/preloadMockedMetadata.ts`

The three Storybook decorator imports
(`MockedMetadataLoadEffect`, `ObjectMetadataItemsDecorator`,
`WorkflowStepDecorator`) are updated to the new `~/testing/...` paths.

No runtime behavior change.

## Test plan

- [x] `npx oxlint --type-aware -c .oxlintrc.json` on touched files —
clean
- [x] `npx prettier --check` on touched files — clean
- [ ] CI: storybook, unit tests, e2e tests
2026-05-07 08:32:41 +00:00
110 changed files with 998 additions and 507 deletions
@@ -76,28 +76,6 @@ yarn twenty logs
```
</Note>
#### enqueueLogicFunctionExecution
Inside your logic function handler — regardless of trigger (HTTP route, cron, database event, tool, workflow action, or install hook) — you can enqueue **another** function that belongs to the **same application** on the worker queue. Import it from `twenty-sdk/logic-function`:
```ts
import {
enqueueLogicFunctionExecution,
type RoutePayload,
} from 'twenty-sdk/logic-function';
const handler = async (event: RoutePayload) => {
const { jobId, status } = await enqueueLogicFunctionExecution({
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
payload: { fromWebhook: true },
});
return { accepted: true, jobId, status };
};
```
Pass **exactly one** of `name` or `universalIdentifier`.
#### Route trigger payload
When a route trigger invokes your logic function, it receives a `RoutePayload` object that follows the
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Sien rekords op alle voorwerpe"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Twee-faktor magtiging-setup suksesvol voltooi!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "عرض السجلات في جميع العناصر"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "تم إكمال إعداد المصادقة الثنائية بنجاح!
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Veure registres de tots els objectes"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Configuració de l'autenticació de dos factors completada amb èxit!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Zobrazit záznamy na všech objektech"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Nastavení dvoufaktorového ověřování bylo úspěšně dokončeno!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Se poster på alle objekter"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15688,6 +15693,7 @@ msgstr "Opsætning af to-faktor godkendelse blev gennemført med succes!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Datensätze auf allen Objekten anzeigen"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Einrichtung der Zwei-Faktor-Authentifizierung erfolgreich abgeschlossen!
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Δείτε εγγραφές σε όλα τα αντικείμενα"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15690,6 +15695,7 @@ msgstr "Η εγκατάσταση δύο παραγοντικής ταυτοπο
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
+6
View File
@@ -13517,6 +13517,11 @@ msgstr "See records"
msgid "See Records on All Objects"
msgstr "See Records on All Objects"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr "Seeded"
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15736,6 +15741,7 @@ msgstr "Two-factor authentication setup completed successfully!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Ver registros en todos los objetos"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15688,6 +15693,7 @@ msgstr "¡La configuración de la autenticación de dos factores se completó co
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Näytä tietueet kaikista objekteista"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Kaksivaiheisen todennuksen asennus on valmis!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Voir les enregistrements sur tous les objets"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15688,6 +15693,7 @@ msgstr "Configuration de l'authentification à deux facteurs terminée avec succ
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "צפייה ברשומות מכל הישויות"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "הגדרת האימות הדו-שלבי הושלמה בהצלחה!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Rekordok megtekintése minden objektumon"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Kétfaktoros hitelesítési beállítás sikeresen befejezve!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Visualizzare record su tutti gli oggetti"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15688,6 +15693,7 @@ msgstr "Configurazione dell'autenticazione a due fattori completata con successo
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "全オブジェクトでレコードを表示する"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "二要素認証のセットアップが正常に完了しました!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "모든 객체의 기록 보기"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "이중 인증 설정이 성공적으로 완료되었습니다!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Bekijk gegevens op alle objecten"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15688,6 +15693,7 @@ msgstr "Two-factor authenticatie setup met succes voltooid!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Se oppføringer på alle objekter"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Tofaktorautentiseringsoppsett fullført!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Zobacz rekordy na wszystkich obiektach"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Konfiguracja uwierzytelniania dwuskładnikowego zakończona pomyślnie!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13494,6 +13494,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr ""
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15681,6 +15686,7 @@ msgstr ""
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Visualizar Registros em Todos os Objetos"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Configuração de autenticação em duas etapas concluída com sucesso!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Ver registros em todos os objetos"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Configuração da autenticação em duas etapas concluída com sucesso!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Vezi înregistrări pentru toate obiectele"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Configurarea autentificării în doi pași a fost finalizată cu succes!
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
Binary file not shown.
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Погледај записе на свим објектима"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Подешавање двофакторске аутентификаци
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13503,6 +13503,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Visa poster i alla objekt"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15694,6 +15699,7 @@ msgstr "Tvåfaktorsautentisering uppsättning genomförd lyckades!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Tüm Nesnelerdeki Kayıtları Görüntüle"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "İki faktörlü doğrulama kurulumu başarıyla tamamlandı!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Переглянути записи на всіх об'єктах"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15688,6 +15693,7 @@ msgstr "Налаштування двофакторної аутентифіка
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "Xem bản ghi trên tất cả các đối tượng"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "Thiết lập xác thực hai yếu tố thành công!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "查看所有对象上的记录"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "双因素身份验证设置成功完成!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -13499,6 +13499,11 @@ msgstr ""
msgid "See Records on All Objects"
msgstr "查看所有物件的記錄"
#. js-lingui-id: Hhnssf
#: src/pages/settings/applications/components/SettingsApplicationTableRow.tsx
msgid "Seeded"
msgstr ""
#. js-lingui-id: rG3WVm
#: src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx
#: src/modules/spreadsheet-import/steps/components/SelectHeaderStep/components/SelectColumn.tsx
@@ -15686,6 +15691,7 @@ msgstr "雙因素身份驗證設定成功完成!"
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/layout/SettingsLayoutPageLayoutDetail.tsx
#: src/pages/settings/data-model/SettingsObjectIndexTable.tsx
#: src/pages/settings/applications/components/SettingsApplicationsTable.tsx
#: src/pages/settings/ai/constants/SettingsSkillTableMetadata.ts
#: src/pages/settings/ai/constants/SettingsAiAgentTableMetadata.ts
#: src/pages/settings/ai/components/SettingsToolParameterTable.tsx
@@ -21,7 +21,9 @@ export const MinimalMetadataGater = ({ children }: React.PropsWithChildren) => {
isMatchingLocation(location, AppPath.SignInUp) ||
isMatchingLocation(location, AppPath.Invite) ||
isMatchingLocation(location, AppPath.ResetPassword) ||
isMatchingLocation(location, AppPath.CreateWorkspace);
isMatchingLocation(location, AppPath.CreateWorkspace) ||
isMatchingLocation(location, AppPath.PlanRequired) ||
isMatchingLocation(location, AppPath.PlanRequiredSuccess);
const shouldShowLoader = !isMinimalMetadataReady && !isOnExcludedPath;
@@ -11,6 +11,7 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
import { getApplicationDescriptionSummary } from '~/pages/settings/applications/utils/getApplicationDescriptionSummary';
import { type ApplicationDisplayData } from '@/applications/types/applicationDisplayData.type';
import { StyledNameTableCell } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
import { ApplicationRegistrationSourceType } from '~/generated-metadata/graphql';
export type SettingsApplicationTableRowProps = {
action: ReactNode;
@@ -19,16 +20,25 @@ export type SettingsApplicationTableRowProps = {
};
hasUpdate?: boolean;
link?: string;
sourceType?: ApplicationRegistrationSourceType;
};
export const APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS =
'164px minmax(0, 1fr) 36px';
'164px 80px minmax(0, 1fr) 36px';
const SOURCE_TYPE_LABELS: Record<ApplicationRegistrationSourceType, string> = {
[ApplicationRegistrationSourceType.LOCAL]: 'Local',
[ApplicationRegistrationSourceType.NPM]: 'NPM',
[ApplicationRegistrationSourceType.OAUTH_ONLY]: 'OAuth',
[ApplicationRegistrationSourceType.TARBALL]: 'Tarball',
};
export const SettingsApplicationTableRow = ({
action,
application,
hasUpdate,
link,
sourceType,
}: SettingsApplicationTableRowProps) => {
const resolvedDescription = useResolvedApplicationDescription(application);
const descriptionSummary =
@@ -43,6 +53,9 @@ export const SettingsApplicationTableRow = ({
<StyledNameTableCell minWidth="0" overflow="hidden">
<ApplicationDisplay application={application} />
</StyledNameTableCell>
<TableCell color={themeCssVariables.font.color.tertiary}>
{sourceType ? SOURCE_TYPE_LABELS[sourceType] : t`Seeded`}
</TableCell>
<TableCell gap={themeCssVariables.spacing[2]} minWidth="0">
<OverflowingTextWithTooltip text={descriptionSummary} />
{hasUpdate === true && (
@@ -38,13 +38,26 @@ export const SettingsApplicationsTable = ({
const [searchTerm, setSearchTerm] = useState('');
const filteredApplications = applications.filter(
(application) =>
application.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(application.description ?? '')
.toLowerCase()
.includes(searchTerm.toLowerCase()),
);
const filteredApplications = applications
.filter(
(application) =>
application.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(application.description ?? '')
.toLowerCase()
.includes(searchTerm.toLowerCase()),
)
.sort((a, b) => {
const aIsOAuth =
a.applicationRegistration?.sourceType ===
ApplicationRegistrationSourceType.OAUTH_ONLY;
const bIsOAuth =
b.applicationRegistration?.sourceType ===
ApplicationRegistrationSourceType.OAUTH_ONLY;
if (aIsOAuth === bIsOAuth) return 0;
return aIsOAuth ? 1 : -1;
});
return (
<Section>
@@ -64,6 +77,7 @@ export const SettingsApplicationsTable = ({
gridTemplateColumns={APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
>
<TableHeader> {t`Name`}</TableHeader>
<TableHeader> {t`Type`}</TableHeader>
<TableHeader> {t`Description`}</TableHeader>
<TableHeader> {''}</TableHeader>
<TableHeader />
@@ -88,6 +102,7 @@ export const SettingsApplicationsTable = ({
key={application.id}
application={application}
hasUpdate={hasUpdate}
sourceType={application.applicationRegistration?.sourceType}
action={
<IconChevronRight
size={theme.icon.size.md}
@@ -1,4 +1,4 @@
import { useLoadMockedMetadata } from '@/metadata-store/hooks/useLoadMockedMetadata';
import { useLoadMockedMetadata } from '~/testing/hooks/useLoadMockedMetadata';
import { isMinimalMetadataReadyState } from '@/metadata-store/states/isMinimalMetadataReadyState';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
@@ -7,7 +7,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
import { useLoadMockedMetadata } from '@/metadata-store/hooks/useLoadMockedMetadata';
import { useLoadMockedMetadata } from '~/testing/hooks/useLoadMockedMetadata';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
@@ -1,6 +1,6 @@
import { sidePanelWorkflowIdComponentState } from '@/side-panel/pages/workflow/states/sidePanelWorkflowIdComponentState';
import { SidePanelPageComponentInstanceContext } from '@/side-panel/states/contexts/SidePanelPageComponentInstanceContext';
import { useLoadMockedMetadata } from '@/metadata-store/hooks/useLoadMockedMetadata';
import { useLoadMockedMetadata } from '~/testing/hooks/useLoadMockedMetadata';
import { flowComponentState } from '@/workflow/states/flowComponentState';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { workflowVisualizerWorkflowRunIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowRunIdComponentState';
@@ -1,11 +1,11 @@
import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft';
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import {
preloadMockedMetadata,
type PreloadedMockedMetadata,
} from '@/metadata-store/utils/preloadMockedMetadata';
import { useStore } from 'jotai';
import { useCallback } from 'react';
} from '~/testing/utils/preloadMockedMetadata';
const MOCKED_COLLECTION_HASH = 'mocked';
@@ -1,111 +0,0 @@
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi,
type MockInstance,
} from 'vitest';
import { enqueueLogicFunctionExecution } from '@/sdk/logic-function/enqueue-logic-function-execution';
describe('enqueueLogicFunctionExecution', () => {
let fetchSpy: MockInstance<typeof fetch>;
beforeEach(() => {
process.env.TWENTY_API_URL = 'https://api.test';
process.env.TWENTY_APP_ACCESS_TOKEN = 'app-token';
fetchSpy = vi.spyOn(globalThis, 'fetch');
});
afterEach(() => {
delete process.env.TWENTY_API_URL;
delete process.env.TWENTY_APP_ACCESS_TOKEN;
fetchSpy.mockRestore();
});
it('POSTs to /app/logic-functions/enqueue with universalIdentifier', async () => {
fetchSpy.mockResolvedValue(
new Response(JSON.stringify({ jobId: 'job-1', status: 'queued' }), {
status: 202,
}),
);
const result = await enqueueLogicFunctionExecution({
universalIdentifier: '550e8400-e29b-41d4-a716-446655440000',
payload: { foo: 'bar' },
});
expect(result).toEqual({ jobId: 'job-1', status: 'queued' });
expect(fetchSpy).toHaveBeenCalledWith(
'https://api.test/app/logic-functions/enqueue',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
universalIdentifier: '550e8400-e29b-41d4-a716-446655440000',
payload: { foo: 'bar' },
}),
headers: expect.objectContaining({
Authorization: 'Bearer app-token',
}),
}),
);
});
it('POSTs with name when provided', async () => {
fetchSpy.mockResolvedValue(
new Response(JSON.stringify({ jobId: 'job-2', status: 'queued' }), {
status: 202,
}),
);
await enqueueLogicFunctionExecution({
name: 'my-fn',
payload: {},
});
expect(fetchSpy).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: JSON.stringify({ name: 'my-fn', payload: {} }),
}),
);
});
it('throws when env vars are missing', async () => {
delete process.env.TWENTY_API_URL;
await expect(
enqueueLogicFunctionExecution({
name: 'x',
payload: {},
}),
).rejects.toThrow(/TWENTY_API_URL/);
});
it('throws when neither name nor universalIdentifier is provided', async () => {
await expect(
enqueueLogicFunctionExecution({ payload: {} }),
).rejects.toThrow(/exactly one of name or universalIdentifier/);
});
it('throws when both name and universalIdentifier are provided', async () => {
await expect(
enqueueLogicFunctionExecution({
name: 'a',
universalIdentifier: '550e8400-e29b-41d4-a716-446655440000',
payload: {},
}),
).rejects.toThrow(/exactly one of name or universalIdentifier/);
});
it('treats empty universalIdentifier as missing', async () => {
await expect(
enqueueLogicFunctionExecution({
universalIdentifier: '',
payload: {},
}),
).rejects.toThrow(/exactly one of name or universalIdentifier/);
});
});
@@ -1,69 +0,0 @@
import { isNonEmptyString } from '@sniptt/guards';
import {
DEFAULT_API_URL_NAME,
DEFAULT_APP_ACCESS_TOKEN_NAME,
} from 'twenty-shared/application';
const ENQUEUE_LOGIC_FUNCTION_TIMEOUT_MS = 10_000;
export type EnqueueLogicFunctionExecutionParams = {
name?: string;
universalIdentifier?: string;
payload: Record<string, unknown>;
};
export type EnqueueLogicFunctionExecutionResult = {
jobId: string;
status: 'queued';
};
export const enqueueLogicFunctionExecution = async (
params: EnqueueLogicFunctionExecutionParams,
): Promise<EnqueueLogicFunctionExecutionResult> => {
const apiUrl = process.env[DEFAULT_API_URL_NAME];
const token = process.env[DEFAULT_APP_ACCESS_TOKEN_NAME];
if (!apiUrl || !token) {
throw new Error(
'enqueueLogicFunctionExecution requires TWENTY_API_URL and TWENTY_APP_ACCESS_TOKEN',
);
}
const hasUniversalIdentifier = isNonEmptyString(params.universalIdentifier);
const hasName = isNonEmptyString(params.name);
if (hasUniversalIdentifier === hasName) {
throw new Error(
'enqueueLogicFunctionExecution: provide exactly one of name or universalIdentifier',
);
}
const response = await fetch(
`${apiUrl.replace(/\/$/, '')}/app/logic-functions/enqueue`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
...(hasName ? { name: params.name } : {}),
...(hasUniversalIdentifier
? { universalIdentifier: params.universalIdentifier }
: {}),
payload: params.payload,
}),
signal: AbortSignal.timeout(ENQUEUE_LOGIC_FUNCTION_TIMEOUT_MS),
},
);
if (!response.ok) {
const body = await response.text().catch(() => '');
throw new Error(
`enqueueLogicFunctionExecution failed: ${response.status} ${response.statusText} ${body}`,
);
}
return response.json() as Promise<EnqueueLogicFunctionExecutionResult>;
};
@@ -38,15 +38,9 @@ export type { RoutePayload } from '@/sdk/define/logic-functions/triggers/route-p
export type { InputJsonSchema } from 'twenty-shared/logic-function';
export {
enqueueLogicFunctionExecution,
type EnqueueLogicFunctionExecutionParams,
type EnqueueLogicFunctionExecutionResult,
} from '@/sdk/logic-function/enqueue-logic-function-execution';
export { AppConnectionAuthFailedError } from '@/sdk/logic-function/connections/errors/app-connection-auth-failed.error';
export { findConnectionForRequest } from '@/sdk/logic-function/connections/find-connection-for-request';
export { getConnection } from '@/sdk/logic-function/connections/get-connection';
export { listConnections } from '@/sdk/logic-function/connections/list-connections';
export type { ListConnectionsFilter } from '@/sdk/logic-function/connections/list-connections';
export { findConnectionForRequest } from '@/sdk/logic-function/connections/find-connection-for-request';
export { AppConnectionAuthFailedError } from '@/sdk/logic-function/connections/errors/app-connection-auth-failed.error';
export type { AppConnection } from '@/sdk/logic-function/connections/types/app-connection.type';
@@ -1,15 +1,15 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InstanceCommandGenerationService should encode version correctly in file and class names 1`] = `
{
"className": "TestFastInstanceCommand",
"fileName": "1-21-instance-command-fast-1775000000000-test.ts",
"fileName": "30-40-instance-command-fast-1775000000000-test.ts",
"fileTemplate": "import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('1.21.0', 1775000000000)
@RegisteredInstanceCommand('30.40.0', 1775000000000)
export class TestFastInstanceCommand implements FastInstanceCommand {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('SELECT 1');
@@ -26,13 +26,13 @@ export class TestFastInstanceCommand implements FastInstanceCommand {
exports[`InstanceCommandGenerationService should escape backslashes in SQL queries 1`] = `
{
"className": "UpdatePathFastInstanceCommand",
"fileName": "2-3-instance-command-fast-1775000000000-update-path.ts",
"fileName": "10-20-instance-command-fast-1775000000000-update-path.ts",
"fileTemplate": "import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.3.0', 1775000000000)
@RegisteredInstanceCommand('10.20.0', 1775000000000)
export class UpdatePathFastInstanceCommand implements FastInstanceCommand {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('UPDATE "core"."config" SET "value" = E\\'path\\\\\\\\to\\\\\\\\file\\'');
@@ -49,13 +49,13 @@ export class UpdatePathFastInstanceCommand implements FastInstanceCommand {
exports[`InstanceCommandGenerationService should escape single quotes in SQL queries 1`] = `
{
"className": "UpdateConfigFastInstanceCommand",
"fileName": "2-3-instance-command-fast-1775000000000-update-config.ts",
"fileName": "10-20-instance-command-fast-1775000000000-update-config.ts",
"fileTemplate": "import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.3.0', 1775000000000)
@RegisteredInstanceCommand('10.20.0', 1775000000000)
export class UpdateConfigFastInstanceCommand implements FastInstanceCommand {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('UPDATE "core"."config" SET "value" = \\'it\\'\\'s done\\'');
@@ -72,13 +72,13 @@ export class UpdateConfigFastInstanceCommand implements FastInstanceCommand {
exports[`InstanceCommandGenerationService should generate a migration with a single up/down query 1`] = `
{
"className": "AddFooColumnFastInstanceCommand",
"fileName": "2-3-instance-command-fast-1775000000000-add-foo-column.ts",
"fileName": "10-20-instance-command-fast-1775000000000-add-foo-column.ts",
"fileTemplate": "import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.3.0', 1775000000000)
@RegisteredInstanceCommand('10.20.0', 1775000000000)
export class AddFooColumnFastInstanceCommand implements FastInstanceCommand {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "core"."user" ADD "foo" varchar');
@@ -95,13 +95,13 @@ export class AddFooColumnFastInstanceCommand implements FastInstanceCommand {
exports[`InstanceCommandGenerationService should generate a migration with multiple queries 1`] = `
{
"className": "CreateTaskTableFastInstanceCommand",
"fileName": "2-3-instance-command-fast-1775000000000-create-task-table.ts",
"fileName": "10-20-instance-command-fast-1775000000000-create-task-table.ts",
"fileTemplate": "import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.3.0', 1775000000000)
@RegisteredInstanceCommand('10.20.0', 1775000000000)
export class CreateTaskTableFastInstanceCommand implements FastInstanceCommand {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('CREATE TABLE "core"."task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" varchar NOT NULL)');
@@ -120,13 +120,13 @@ export class CreateTaskTableFastInstanceCommand implements FastInstanceCommand {
exports[`InstanceCommandGenerationService should generate a migration with query parameters 1`] = `
{
"className": "SeedSettingFastInstanceCommand",
"fileName": "2-3-instance-command-fast-1775000000000-seed-setting.ts",
"fileName": "10-20-instance-command-fast-1775000000000-seed-setting.ts",
"fileTemplate": "import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.3.0', 1775000000000)
@RegisteredInstanceCommand('10.20.0', 1775000000000)
export class SeedSettingFastInstanceCommand implements FastInstanceCommand {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('INSERT INTO "core"."setting" ("key", "value") VALUES ($1, $2)', ["theme","dark"]);
@@ -143,13 +143,13 @@ export class SeedSettingFastInstanceCommand implements FastInstanceCommand {
exports[`InstanceCommandGenerationService should generate a slow instance command with populated up/down 1`] = `
{
"className": "MakeColumnNotNullableSlowInstanceCommand",
"fileName": "2-3-instance-command-slow-1775000000000-make-column-not-nullable.ts",
"fileName": "10-20-instance-command-slow-1775000000000-make-column-not-nullable.ts",
"fileTemplate": "import { DataSource, QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { SlowInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/slow-instance-command.interface';
@RegisteredInstanceCommand('2.3.0', 1775000000000, { type: 'slow' })
@RegisteredInstanceCommand('10.20.0', 1775000000000, { type: 'slow' })
export class MakeColumnNotNullableSlowInstanceCommand implements SlowInstanceCommand {
async runDataMigration(dataSource: DataSource): Promise<void> {
// TODO: implement data backfill before the DDL migration
@@ -170,13 +170,13 @@ export class MakeColumnNotNullableSlowInstanceCommand implements SlowInstanceCom
exports[`InstanceCommandGenerationService should use default migration name in class and file names 1`] = `
{
"className": "AutoGeneratedFastInstanceCommand",
"fileName": "2-3-instance-command-fast-1775000000000-auto-generated.ts",
"fileName": "10-20-instance-command-fast-1775000000000-auto-generated.ts",
"fileTemplate": "import { QueryRunner } from 'typeorm';
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
@RegisteredInstanceCommand('2.3.0', 1775000000000)
@RegisteredInstanceCommand('10.20.0', 1775000000000)
export class AutoGeneratedFastInstanceCommand implements FastInstanceCommand {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('ALTER TABLE "core"."user" ADD "bar" integer');
@@ -2,11 +2,10 @@ import { Test } from '@nestjs/testing';
import { getDataSourceToken } from '@nestjs/typeorm';
import { InstanceCommandGenerationService } from 'src/database/commands/instance-command-generation.service';
import { TWENTY_CURRENT_VERSION } from 'src/engine/core-modules/upgrade/constants/twenty-current-version.constant';
import { TWENTY_PREVIOUS_VERSIONS } from 'src/engine/core-modules/upgrade/constants/twenty-previous-versions.constant';
import { type TwentyAllVersion } from 'src/engine/core-modules/upgrade/constants/twenty-all-versions.constant';
const VERSION_A = TWENTY_CURRENT_VERSION;
const VERSION_B = TWENTY_PREVIOUS_VERSIONS[0];
const VERSION_A = '10.20.0' as TwentyAllVersion;
const VERSION_B = '30.40.0' as TwentyAllVersion;
const FIXED_TIMESTAMP = 1775000000000;
@@ -0,0 +1,74 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class AddInboundWebhookSubscription1778500000000
implements MigrationInterface
{
name = 'AddInboundWebhookSubscription1778500000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "core"."inboundWebhookSubscription" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"workspaceId" uuid NOT NULL,
"source" varchar NOT NULL,
"connectedAccountId" uuid,
"externalSubscriptionId" varchar,
"externalResourceId" varchar,
"secret" varchar NOT NULL,
"expiresAt" TIMESTAMP WITH TIME ZONE,
"lastNotificationAt" TIMESTAMP WITH TIME ZONE,
"metadata" jsonb,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_inboundWebhookSubscription_id" PRIMARY KEY ("id")
)`,
);
await queryRunner.query(
`CREATE INDEX "IDX_INBOUND_WEBHOOK_SUBSCRIPTION_WORKSPACE_ID" ON "core"."inboundWebhookSubscription" ("workspaceId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_INBOUND_WEBHOOK_SUBSCRIPTION_SOURCE" ON "core"."inboundWebhookSubscription" ("source")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_INBOUND_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID" ON "core"."inboundWebhookSubscription" ("externalSubscriptionId")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_INBOUND_WEBHOOK_SUBSCRIPTION_EXPIRES_AT" ON "core"."inboundWebhookSubscription" ("expiresAt")`,
);
await queryRunner.query(
`ALTER TABLE "core"."inboundWebhookSubscription"
ADD CONSTRAINT "FK_inboundWebhookSubscription_workspaceId"
FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id")
ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."inboundWebhookSubscription" DROP CONSTRAINT "FK_inboundWebhookSubscription_workspaceId"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_INBOUND_WEBHOOK_SUBSCRIPTION_EXPIRES_AT"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_INBOUND_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_INBOUND_WEBHOOK_SUBSCRIPTION_SOURCE"`,
);
await queryRunner.query(
`DROP INDEX "core"."IDX_INBOUND_WEBHOOK_SUBSCRIPTION_WORKSPACE_ID"`,
);
await queryRunner.query(`DROP TABLE "core"."inboundWebhookSubscription"`);
}
}
@@ -586,7 +586,8 @@ export class OAuthService {
return this.applicationService.create({
universalIdentifier: applicationRegistration.universalIdentifier,
name: applicationRegistration.name,
version: '0.0.0',
description: `OAuth application registered as "${applicationRegistration.name}"`,
version: applicationRegistration.latestAvailableVersion ?? '1.0.0',
sourcePath: 'oauth-install',
applicationRegistrationId: applicationRegistration.id,
workspaceId,
@@ -40,6 +40,7 @@ import { GeoMapModule } from 'src/engine/core-modules/geo-map/geo-map-module';
import { HealthModule } from 'src/engine/core-modules/health/health.module';
import { ImapSmtpCaldavModule } from 'src/engine/core-modules/imap-smtp-caldav-connection/imap-smtp-caldav-connection.module';
import { ImpersonationModule } from 'src/engine/core-modules/impersonation/impersonation.module';
import { InboundWebhookModule } from 'src/engine/core-modules/inbound-webhook/inbound-webhook.module';
import { LabModule } from 'src/engine/core-modules/lab/lab.module';
import { LoggerModule } from 'src/engine/core-modules/logger/logger.module';
import { loggerModuleFactory } from 'src/engine/core-modules/logger/logger.module-factory';
@@ -89,6 +90,7 @@ import { FileModule } from './file/file.module';
AuthModule,
BillingModule,
BillingWebhookModule,
InboundWebhookModule,
UsageModule,
ClientConfigModule,
FeatureFlagModule,
@@ -0,0 +1,89 @@
import {
Controller,
Logger,
Param,
Post,
type RawBodyRequest,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import { type Request, type Response } from 'express';
import {
InboundWebhookException,
InboundWebhookExceptionCode,
} from 'src/engine/core-modules/inbound-webhook/inbound-webhook.exception';
import { InboundWebhookDispatcherService } from 'src/engine/core-modules/inbound-webhook/services/inbound-webhook-dispatcher.service';
import { InboundWebhookIdempotencyService } from 'src/engine/core-modules/inbound-webhook/services/inbound-webhook-idempotency.service';
import { ProcessInboundWebhookJob } from 'src/engine/core-modules/inbound-webhook/jobs/process-inbound-webhook.job';
import { type InboundWebhookJobData } from 'src/engine/core-modules/inbound-webhook/dtos/inbound-webhook-job-data.type';
import { type InboundWebhookSource } from 'src/engine/core-modules/inbound-webhook/types/inbound-webhook-source.type';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
@Controller()
export class InboundWebhookController {
private readonly logger = new Logger(InboundWebhookController.name);
constructor(
private readonly dispatcher: InboundWebhookDispatcherService,
private readonly idempotencyService: InboundWebhookIdempotencyService,
@InjectMessageQueue(MessageQueue.inboundWebhookQueue)
private readonly inboundWebhookQueue: MessageQueueService,
) {}
@Post(['inbound-webhooks/:source'])
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
async receive(
@Param('source') sourceParam: string,
@Req() request: RawBodyRequest<Request>,
@Res() response: Response,
): Promise<void> {
if (request.rawBody === undefined) {
throw new InboundWebhookException(
'Inbound webhook missing raw body',
InboundWebhookExceptionCode.MISSING_RAW_BODY,
);
}
const source: InboundWebhookSource = sourceParam;
const handler = this.dispatcher.resolve(source);
if (!(await handler.verify(request))) {
throw new InboundWebhookException(
`Inbound webhook signature verification failed for ${source}`,
InboundWebhookExceptionCode.INVALID_SIGNATURE,
);
}
const envelope = await handler.buildEnvelope(request);
const claimed = await this.idempotencyService.claim({
source,
externalEventId: envelope.externalEventId,
});
if (!claimed) {
this.logger.log(
`Dropping duplicate inbound webhook ${source}:${envelope.externalEventId}`,
);
response.status(200).json({ ok: true, duplicate: true });
return;
}
const jobData: InboundWebhookJobData = { envelope };
await this.inboundWebhookQueue.add<InboundWebhookJobData>(
ProcessInboundWebhookJob.name,
jobData,
);
response.status(200).json({ ok: true });
}
}
@@ -0,0 +1,32 @@
import { Command, CommandRunner } from 'nest-commander';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { INBOUND_WEBHOOK_RENEWAL_CRON_PATTERN } from 'src/engine/core-modules/inbound-webhook/inbound-webhook.constants';
import { InboundWebhookRenewalCronJob } from 'src/engine/core-modules/inbound-webhook/crons/jobs/inbound-webhook-renewal.cron.job';
@Command({
name: 'cron:inbound-webhook:renewal',
description:
'Starts a cron job to renew inbound webhook subscriptions before they expire',
})
export class InboundWebhookRenewalCronCommand extends CommandRunner {
constructor(
@InjectMessageQueue(MessageQueue.cronQueue)
private readonly messageQueueService: MessageQueueService,
) {
super();
}
async run(): Promise<void> {
await this.messageQueueService.addCron<undefined>({
jobName: InboundWebhookRenewalCronJob.name,
data: undefined,
options: {
repeat: { pattern: INBOUND_WEBHOOK_RENEWAL_CRON_PATTERN },
},
});
}
}
@@ -0,0 +1,43 @@
import { Logger } from '@nestjs/common';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { INBOUND_WEBHOOK_RENEWAL_BUFFER_MS } from 'src/engine/core-modules/inbound-webhook/inbound-webhook.constants';
import { InboundWebhookDispatcherService } from 'src/engine/core-modules/inbound-webhook/services/inbound-webhook-dispatcher.service';
import { InboundWebhookSubscriptionService } from 'src/engine/core-modules/inbound-webhook/services/inbound-webhook-subscription.service';
@Processor(MessageQueue.cronQueue)
export class InboundWebhookRenewalCronJob {
private readonly logger = new Logger(InboundWebhookRenewalCronJob.name);
constructor(
private readonly subscriptionService: InboundWebhookSubscriptionService,
private readonly dispatcher: InboundWebhookDispatcherService,
) {}
@Process(InboundWebhookRenewalCronJob.name)
async handle(): Promise<void> {
const threshold = new Date(Date.now() + INBOUND_WEBHOOK_RENEWAL_BUFFER_MS);
const expiring =
await this.subscriptionService.findExpiringBefore(threshold);
for (const subscription of expiring) {
const handler = this.dispatcher.resolveSubscribable(subscription.source);
if (handler === null) {
continue;
}
try {
await handler.renewSubscription(subscription);
} catch (error) {
this.logger.error(
`Failed to renew inbound webhook subscription ${subscription.id}`,
error instanceof Error ? error.stack : error,
);
}
}
}
}
@@ -0,0 +1,5 @@
import { type InboundWebhookEnvelope } from 'src/engine/core-modules/inbound-webhook/types/inbound-webhook-context.type';
export type InboundWebhookJobData = {
envelope: InboundWebhookEnvelope;
};
@@ -0,0 +1,55 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { type InboundWebhookSource } from 'src/engine/core-modules/inbound-webhook/types/inbound-webhook-source.type';
@Index('IDX_INBOUND_WEBHOOK_SUBSCRIPTION_WORKSPACE_ID', ['workspaceId'])
@Index('IDX_INBOUND_WEBHOOK_SUBSCRIPTION_SOURCE', ['source'])
@Index('IDX_INBOUND_WEBHOOK_SUBSCRIPTION_EXTERNAL_ID', [
'externalSubscriptionId',
])
@Index('IDX_INBOUND_WEBHOOK_SUBSCRIPTION_EXPIRES_AT', ['expiresAt'])
@Entity({ name: 'inboundWebhookSubscription', schema: 'core' })
export class InboundWebhookSubscriptionEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'uuid' })
workspaceId: string;
@Column({ type: 'varchar' })
source: InboundWebhookSource;
@Column({ type: 'uuid', nullable: true })
connectedAccountId: string | null;
@Column({ type: 'varchar', nullable: true })
externalSubscriptionId: string | null;
@Column({ type: 'varchar', nullable: true })
externalResourceId: string | null;
@Column({ type: 'varchar' })
secret: string;
@Column({ type: 'timestamptz', nullable: true })
expiresAt: Date | null;
@Column({ type: 'timestamptz', nullable: true })
lastNotificationAt: Date | null;
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, unknown> | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -0,0 +1,14 @@
// Default replay window for HMAC-signed sources. Inbound timestamps older
// than this are rejected to defeat replay attacks.
export const INBOUND_WEBHOOK_REPLAY_WINDOW_MS = 5 * 60 * 1000;
// Idempotency lock TTL — duplicate inbound events within this window are
// dropped. 7d covers retry policies of all current providers.
export const INBOUND_WEBHOOK_IDEMPOTENCY_TTL_MS = 7 * 24 * 60 * 60 * 1000;
// Renewal cron pattern. Runs hourly — well below shortest provider expiry
// (Microsoft Graph: 3 days; Gmail watch: 7 days).
export const INBOUND_WEBHOOK_RENEWAL_CRON_PATTERN = '0 * * * *';
// Renewal buffer — refresh subscriptions that expire within this window.
export const INBOUND_WEBHOOK_RENEWAL_BUFFER_MS = 6 * 60 * 60 * 1000;
@@ -0,0 +1,49 @@
import { type MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { assertUnreachable } from 'twenty-shared/utils';
import { CustomException } from 'src/utils/custom-exception';
export enum InboundWebhookExceptionCode {
SOURCE_NOT_SUPPORTED = 'SOURCE_NOT_SUPPORTED',
INVALID_SIGNATURE = 'INVALID_SIGNATURE',
MISSING_RAW_BODY = 'MISSING_RAW_BODY',
SUBSCRIPTION_NOT_FOUND = 'SUBSCRIPTION_NOT_FOUND',
REPLAY_WINDOW_EXCEEDED = 'REPLAY_WINDOW_EXCEEDED',
DUPLICATE_EVENT = 'DUPLICATE_EVENT',
}
const getInboundWebhookExceptionUserFriendlyMessage = (
code: InboundWebhookExceptionCode,
): MessageDescriptor => {
switch (code) {
case InboundWebhookExceptionCode.SOURCE_NOT_SUPPORTED:
return msg`Inbound webhook source is not supported.`;
case InboundWebhookExceptionCode.INVALID_SIGNATURE:
return msg`Inbound webhook signature is invalid.`;
case InboundWebhookExceptionCode.MISSING_RAW_BODY:
return msg`Inbound webhook request is missing a raw body.`;
case InboundWebhookExceptionCode.SUBSCRIPTION_NOT_FOUND:
return msg`Inbound webhook subscription was not found.`;
case InboundWebhookExceptionCode.REPLAY_WINDOW_EXCEEDED:
return msg`Inbound webhook timestamp is outside the accepted replay window.`;
case InboundWebhookExceptionCode.DUPLICATE_EVENT:
return msg`Inbound webhook event was already processed.`;
default:
assertUnreachable(code);
}
};
export class InboundWebhookException extends CustomException<InboundWebhookExceptionCode> {
constructor(
message: string,
code: InboundWebhookExceptionCode,
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
) {
super(message, code, {
userFriendlyMessage:
userFriendlyMessage ??
getInboundWebhookExceptionUserFriendlyMessage(code),
});
}
}
@@ -0,0 +1,34 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { InboundWebhookRenewalCronCommand } from 'src/engine/core-modules/inbound-webhook/crons/commands/inbound-webhook-renewal.cron.command';
import { InboundWebhookRenewalCronJob } from 'src/engine/core-modules/inbound-webhook/crons/jobs/inbound-webhook-renewal.cron.job';
import { InboundWebhookController } from 'src/engine/core-modules/inbound-webhook/controllers/inbound-webhook.controller';
import { InboundWebhookSubscriptionEntity } from 'src/engine/core-modules/inbound-webhook/entities/inbound-webhook-subscription.entity';
import { ProcessInboundWebhookJob } from 'src/engine/core-modules/inbound-webhook/jobs/process-inbound-webhook.job';
import { InboundWebhookDispatcherService } from 'src/engine/core-modules/inbound-webhook/services/inbound-webhook-dispatcher.service';
import { InboundWebhookIdempotencyService } from 'src/engine/core-modules/inbound-webhook/services/inbound-webhook-idempotency.service';
import { InboundWebhookSubscriptionService } from 'src/engine/core-modules/inbound-webhook/services/inbound-webhook-subscription.service';
import { InboundWebhookVerifyService } from 'src/engine/core-modules/inbound-webhook/services/inbound-webhook-verify.service';
@Module({
imports: [
TypeOrmModule.forFeature([InboundWebhookSubscriptionEntity], 'core'),
],
controllers: [InboundWebhookController],
providers: [
InboundWebhookDispatcherService,
InboundWebhookIdempotencyService,
InboundWebhookSubscriptionService,
InboundWebhookVerifyService,
ProcessInboundWebhookJob,
InboundWebhookRenewalCronJob,
InboundWebhookRenewalCronCommand,
],
exports: [
InboundWebhookDispatcherService,
InboundWebhookSubscriptionService,
InboundWebhookVerifyService,
],
})
export class InboundWebhookModule {}
@@ -0,0 +1,32 @@
import { Logger } from '@nestjs/common';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { InboundWebhookDispatcherService } from 'src/engine/core-modules/inbound-webhook/services/inbound-webhook-dispatcher.service';
import { type InboundWebhookJobData } from 'src/engine/core-modules/inbound-webhook/dtos/inbound-webhook-job-data.type';
@Processor(MessageQueue.inboundWebhookQueue)
export class ProcessInboundWebhookJob {
private readonly logger = new Logger(ProcessInboundWebhookJob.name);
constructor(private readonly dispatcher: InboundWebhookDispatcherService) {}
@Process(ProcessInboundWebhookJob.name)
async handle(data: InboundWebhookJobData): Promise<void> {
const { envelope } = data;
const handler = this.dispatcher.resolve(envelope.source);
try {
await handler.handle(envelope);
} catch (error) {
this.logger.error(
`Failed to process inbound webhook ${envelope.source}:${envelope.externalEventId}`,
error instanceof Error ? error.stack : error,
);
throw error;
}
}
}
@@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
import {
InboundWebhookException,
InboundWebhookExceptionCode,
} from 'src/engine/core-modules/inbound-webhook/inbound-webhook.exception';
import {
type InboundWebhookHandler,
type SubscribableInboundWebhookHandler,
isSubscribableInboundWebhookHandler,
} from 'src/engine/core-modules/inbound-webhook/types/inbound-webhook-handler.type';
import { type InboundWebhookSource } from 'src/engine/core-modules/inbound-webhook/types/inbound-webhook-source.type';
// Switch dispatcher (mirrors MessagingGetMessageListService and the Stripe
// webhook handler in billing-webhook). Concrete sources are added by each
// consumer PR — driver migration, inbound email, etc. — by injecting their
// handler class and adding a case below.
@Injectable()
export class InboundWebhookDispatcherService {
resolve(source: InboundWebhookSource): InboundWebhookHandler {
switch (source) {
// Per-source handlers registered in follow-up PRs.
default:
throw new InboundWebhookException(
`Inbound webhook source ${source} is not supported`,
InboundWebhookExceptionCode.SOURCE_NOT_SUPPORTED,
);
}
}
resolveSubscribable(
source: InboundWebhookSource,
): SubscribableInboundWebhookHandler | null {
const handler = this.resolve(source);
return isSubscribableInboundWebhookHandler(handler) ? handler : null;
}
}
@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { INBOUND_WEBHOOK_IDEMPOTENCY_TTL_MS } from 'src/engine/core-modules/inbound-webhook/inbound-webhook.constants';
import { type InboundWebhookSource } from 'src/engine/core-modules/inbound-webhook/types/inbound-webhook-source.type';
@Injectable()
export class InboundWebhookIdempotencyService {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineLock)
private readonly cacheStorageService: CacheStorageService,
) {}
// Returns true if the event was claimed (first time seen) — caller should
// process. Returns false if a prior call already claimed it within the
// TTL — caller should drop as duplicate.
async claim({
source,
externalEventId,
}: {
source: InboundWebhookSource;
externalEventId: string;
}): Promise<boolean> {
return this.cacheStorageService.acquireLock(
this.buildKey({ source, externalEventId }),
INBOUND_WEBHOOK_IDEMPOTENCY_TTL_MS,
);
}
private buildKey({
source,
externalEventId,
}: {
source: InboundWebhookSource;
externalEventId: string;
}): string {
return `inbound-webhook:idempotency:${source}:${externalEventId}`;
}
}
@@ -0,0 +1,63 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { LessThan, Repository } from 'typeorm';
import { InboundWebhookSubscriptionEntity } from 'src/engine/core-modules/inbound-webhook/entities/inbound-webhook-subscription.entity';
import { type InboundWebhookSource } from 'src/engine/core-modules/inbound-webhook/types/inbound-webhook-source.type';
@Injectable()
export class InboundWebhookSubscriptionService {
constructor(
@InjectRepository(InboundWebhookSubscriptionEntity, 'core')
private readonly subscriptionRepository: Repository<InboundWebhookSubscriptionEntity>,
) {}
async findById(id: string): Promise<InboundWebhookSubscriptionEntity | null> {
return this.subscriptionRepository.findOne({ where: { id } });
}
async findByExternalSubscriptionId({
source,
externalSubscriptionId,
}: {
source: InboundWebhookSource;
externalSubscriptionId: string;
}): Promise<InboundWebhookSubscriptionEntity | null> {
return this.subscriptionRepository.findOne({
where: { source, externalSubscriptionId },
});
}
async findExpiringBefore(
threshold: Date,
): Promise<InboundWebhookSubscriptionEntity[]> {
return this.subscriptionRepository.find({
where: { expiresAt: LessThan(threshold) },
});
}
async markNotified(id: string): Promise<void> {
await this.subscriptionRepository.update(id, {
lastNotificationAt: new Date(),
});
}
async updateExpiry({
id,
expiresAt,
externalSubscriptionId,
externalResourceId,
}: {
id: string;
expiresAt: Date | null;
externalSubscriptionId?: string | null;
externalResourceId?: string | null;
}): Promise<void> {
await this.subscriptionRepository.update(id, {
expiresAt,
...(externalSubscriptionId !== undefined && { externalSubscriptionId }),
...(externalResourceId !== undefined && { externalResourceId }),
});
}
}
@@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import {
InboundWebhookException,
InboundWebhookExceptionCode,
} from 'src/engine/core-modules/inbound-webhook/inbound-webhook.exception';
import { INBOUND_WEBHOOK_REPLAY_WINDOW_MS } from 'src/engine/core-modules/inbound-webhook/inbound-webhook.constants';
import {
type HmacAlgorithm,
verifyHmacSignature,
} from 'src/engine/core-modules/inbound-webhook/utils/hmac-signature.util';
@Injectable()
export class InboundWebhookVerifyService {
verifyHmac({
algorithm,
secret,
payload,
expectedSignature,
}: {
algorithm: HmacAlgorithm;
secret: string;
payload: string | Buffer;
expectedSignature: string;
}): boolean {
return verifyHmacSignature({
algorithm,
secret,
payload,
expectedSignature,
});
}
assertWithinReplayWindow(timestampMs: number): void {
const drift = Math.abs(Date.now() - timestampMs);
if (drift > INBOUND_WEBHOOK_REPLAY_WINDOW_MS) {
throw new InboundWebhookException(
`Inbound webhook timestamp drift ${drift}ms exceeds replay window`,
InboundWebhookExceptionCode.REPLAY_WINDOW_EXCEEDED,
);
}
}
}
@@ -0,0 +1,16 @@
import { type RawBodyRequest } from '@nestjs/common';
import { type Request } from 'express';
import { type InboundWebhookSource } from 'src/engine/core-modules/inbound-webhook/types/inbound-webhook-source.type';
export type InboundWebhookRequest = RawBodyRequest<Request>;
export type InboundWebhookEnvelope = {
source: InboundWebhookSource;
externalEventId: string;
workspaceId: string | null;
subscriptionId: string | null;
payload: unknown;
headers: Record<string, string | string[] | undefined>;
};
@@ -0,0 +1,31 @@
import {
type InboundWebhookEnvelope,
type InboundWebhookRequest,
} from 'src/engine/core-modules/inbound-webhook/types/inbound-webhook-context.type';
import { type InboundWebhookSubscriptionEntity } from 'src/engine/core-modules/inbound-webhook/entities/inbound-webhook-subscription.entity';
// Per-source contract. verify + buildEnvelope run on the API pod (must stay
// fast — only HMAC + lookups). handle runs on the worker pod after the job
// is dequeued and is where heavy work (EML parsing, fetch job dispatch)
// belongs.
export type InboundWebhookHandler = {
verify(request: InboundWebhookRequest): Promise<boolean>;
buildEnvelope(
request: InboundWebhookRequest,
): Promise<InboundWebhookEnvelope>;
handle(envelope: InboundWebhookEnvelope): Promise<void>;
};
// Optional capability for sources backed by an external push subscription
// (Google watch, Microsoft Graph subscriptions). Drives the renewal cron.
export type SubscribableInboundWebhookHandler = InboundWebhookHandler & {
renewSubscription(
subscription: InboundWebhookSubscriptionEntity,
): Promise<void>;
};
export const isSubscribableInboundWebhookHandler = (
handler: InboundWebhookHandler,
): handler is SubscribableInboundWebhookHandler =>
typeof (handler as SubscribableInboundWebhookHandler).renewSubscription ===
'function';
@@ -0,0 +1,4 @@
// Sources are identified by an opaque string supplied as a URL path segment.
// Concrete values are added by each consumer PR (driver migration, inbound
// email, etc.) when they register an InboundWebhookHandler.
export type InboundWebhookSource = string;
@@ -0,0 +1,41 @@
import * as crypto from 'node:crypto';
export type HmacAlgorithm = 'sha1' | 'sha256';
export const computeHmac = ({
algorithm,
secret,
payload,
}: {
algorithm: HmacAlgorithm;
secret: string;
payload: string | Buffer;
}): string =>
crypto.createHmac(algorithm, secret).update(payload).digest('hex');
export const timingSafeStringEqual = (a: string, b: string): boolean => {
const bufferA = Buffer.from(a);
const bufferB = Buffer.from(b);
if (bufferA.length !== bufferB.length) {
return false;
}
return crypto.timingSafeEqual(bufferA, bufferB);
};
export const verifyHmacSignature = ({
algorithm,
secret,
payload,
expectedSignature,
}: {
algorithm: HmacAlgorithm;
secret: string;
payload: string | Buffer;
expectedSignature: string;
}): boolean => {
const computed = computeHmac({ algorithm, secret, payload });
return timingSafeStringEqual(computed, expectedSignature);
};
@@ -1,49 +0,0 @@
import {
Body,
Controller,
ForbiddenException,
HttpCode,
HttpStatus,
Post,
Req,
UseGuards,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { Request } from 'express';
import { isDefined } from 'twenty-shared/utils';
import { AppLogicFunctionService } from 'src/engine/core-modules/logic-function/app-logic-function/app-logic-function.service';
import { EnqueueLogicFunctionExecutionDto } from 'src/engine/core-modules/logic-function/app-logic-function/dtos/enqueue-logic-function-execution.dto';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('app/logic-functions')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard, NoPermissionGuard)
export class AppLogicFunctionController {
constructor(
private readonly appLogicFunctionService: AppLogicFunctionService,
) {}
@Post('enqueue')
@HttpCode(HttpStatus.ACCEPTED)
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
async enqueue(
@Req() request: Request,
@Body() body: EnqueueLogicFunctionExecutionDto,
) {
if (!isDefined(request.application) || !isDefined(request.workspace)) {
throw new ForbiddenException(
'Logic function enqueue requires an APPLICATION_ACCESS token.',
);
}
return this.appLogicFunctionService.enqueueExecution({
workspaceId: request.workspace.id,
applicationId: request.application.id,
dto: body,
});
}
}
@@ -1,14 +0,0 @@
import { Module } from '@nestjs/common';
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
import { AppLogicFunctionController } from 'src/engine/core-modules/logic-function/app-logic-function/app-logic-function.controller';
import { AppLogicFunctionService } from 'src/engine/core-modules/logic-function/app-logic-function/app-logic-function.service';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
@Module({
imports: [TokenModule, WorkspaceCacheModule, WorkspaceCacheStorageModule],
controllers: [AppLogicFunctionController],
providers: [AppLogicFunctionService],
})
export class AppLogicFunctionModule {}
@@ -1,134 +0,0 @@
import {
BadRequestException,
ConflictException,
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { type EnqueueLogicFunctionExecutionDto } from 'src/engine/core-modules/logic-function/app-logic-function/dtos/enqueue-logic-function-execution.dto';
import {
LogicFunctionTriggerJob,
type LogicFunctionTriggerJobData,
} from 'src/engine/core-modules/logic-function/logic-function-trigger/jobs/logic-function-trigger.job';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
export type EnqueueLogicFunctionExecutionResult = {
jobId: string;
status: 'queued';
};
@Injectable()
export class AppLogicFunctionService {
constructor(
private readonly workspaceCacheService: WorkspaceCacheService,
@InjectMessageQueue(MessageQueue.logicFunctionQueue)
private readonly logicFunctionMessageQueueService: MessageQueueService,
) {}
async enqueueExecution(params: {
workspaceId: string;
applicationId: string;
dto: EnqueueLogicFunctionExecutionDto;
}): Promise<EnqueueLogicFunctionExecutionResult> {
const { workspaceId, applicationId, dto } = params;
const { name, universalIdentifier, payload } = dto;
const hasUniversalIdentifier = isDefined(universalIdentifier);
const hasName = isNonEmptyString(name);
if (hasUniversalIdentifier === hasName) {
throw new BadRequestException(
'Provide exactly one of name or universalIdentifier',
);
}
const logicFunction = await this.findLogicFunctionForApplicationOrThrow({
workspaceId,
applicationId,
name: hasName ? name : undefined,
universalIdentifier: hasUniversalIdentifier
? universalIdentifier
: undefined,
});
const jobId = await this.logicFunctionMessageQueueService.add<
LogicFunctionTriggerJobData[]
>(
LogicFunctionTriggerJob.name,
[
{
logicFunctionId: logicFunction.id,
workspaceId,
payload,
},
],
{ retryLimit: 3 },
);
if (!isDefined(jobId)) {
throw new InternalServerErrorException(
'Failed to enqueue logic function execution',
);
}
return { jobId, status: 'queued' };
}
private async findLogicFunctionForApplicationOrThrow(params: {
workspaceId: string;
applicationId: string;
name?: string;
universalIdentifier?: string;
}): Promise<FlatLogicFunction> {
const { workspaceId, applicationId, name, universalIdentifier } = params;
const { flatLogicFunctionMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatLogicFunctionMaps',
]);
if (isDefined(universalIdentifier)) {
const logicFunction =
flatLogicFunctionMaps.byUniversalIdentifier[universalIdentifier];
if (
!isDefined(logicFunction) ||
logicFunction.applicationId !== applicationId ||
isDefined(logicFunction.deletedAt)
) {
throw new NotFoundException('Logic function not found');
}
return logicFunction;
}
const matches = Object.values(flatLogicFunctionMaps.byUniversalIdentifier)
.filter(isDefined)
.filter(
(logicFunction) =>
logicFunction.applicationId === applicationId &&
!isDefined(logicFunction.deletedAt) &&
logicFunction.name === name,
);
if (matches.length === 0) {
throw new NotFoundException('Logic function not found');
}
if (matches.length > 1) {
throw new ConflictException(
'Multiple logic functions match this name; use universalIdentifier',
);
}
return matches[0];
}
}
@@ -1,14 +0,0 @@
import { IsObject, IsOptional, IsString, IsUUID } from 'class-validator';
export class EnqueueLogicFunctionExecutionDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsUUID()
universalIdentifier?: string;
@IsObject()
payload!: Record<string, unknown>;
}

Some files were not shown because too many files have changed in this diff Show More