Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b0553677c7 | |||
| 0fe1eca010 | |||
| f720186122 | |||
| 9f930aa366 | |||
| e49673df79 | |||
| 900f70fb9e | |||
| 027331c893 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-1
@@ -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;
|
||||
|
||||
|
||||
+14
-1
@@ -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 && (
|
||||
|
||||
+22
-7
@@ -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';
|
||||
|
||||
+3
-3
@@ -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';
|
||||
|
||||
-111
@@ -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';
|
||||
|
||||
+17
-17
@@ -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');
|
||||
|
||||
+3
-4
@@ -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;
|
||||
|
||||
|
||||
+74
@@ -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"`);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+89
@@ -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 });
|
||||
}
|
||||
}
|
||||
+32
@@ -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 },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
+43
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { type InboundWebhookEnvelope } from 'src/engine/core-modules/inbound-webhook/types/inbound-webhook-context.type';
|
||||
|
||||
export type InboundWebhookJobData = {
|
||||
envelope: InboundWebhookEnvelope;
|
||||
};
|
||||
+55
@@ -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;
|
||||
}
|
||||
+14
@@ -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;
|
||||
+49
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
+34
@@ -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 {}
|
||||
+32
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -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;
|
||||
}
|
||||
}
|
||||
+41
@@ -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}`;
|
||||
}
|
||||
}
|
||||
+63
@@ -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 }),
|
||||
});
|
||||
}
|
||||
}
|
||||
+44
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -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>;
|
||||
};
|
||||
+31
@@ -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';
|
||||
+4
@@ -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;
|
||||
+41
@@ -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);
|
||||
};
|
||||
-49
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
-14
@@ -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 {}
|
||||
-134
@@ -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];
|
||||
}
|
||||
}
|
||||
-14
@@ -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
Reference in New Issue
Block a user