i18n - docs translations (#23244)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23244?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
committed by
GitHub
parent
7f1e3d3541
commit
d1b556f4a8
@@ -59,7 +59,7 @@ export default defineLogicFunction({
|
||||
* **cron**: يشغّل وظيفتك على جدول باستخدام تعبير CRON.
|
||||
* **databaseEvent**: يعمل على أحداث دورة حياة كائنات مساحة العمل. عندما تكون عملية الحدث هي `updated`، يمكن تحديد الحقول المحددة المراد الاستماع إليها في مصفوفة `updatedFields`. إذا تُركت غير معرّفة أو فارغة، فسيؤدي أي تحديث إلى تشغيل الدالة.
|
||||
> مثال: `person.updated`، `*.created`، `company.*`
|
||||
* **serverRoute**: يوفّر مسار HTTP واحدًا بنطاق التسجيل. تعمل دالة **resolver** (المُعلَنة باستخدام `serverRouteTriggerSettings`) في مساحة عمل المالك وتُرجِع مساحة العمل المستهدفة ودالة المنطق المستهدفة التي يجب التوجيه إليها؛ يؤكّد النظام الأساسي تلقّي الطلب برمز `202` ويُشغِّل تلك الدالة **المستهدفة** في طابور العامل (worker queue). راجع [مشغّل مسار الخادم](#server-route-trigger).
|
||||
* **serverRoute**: يوفّر مسار HTTP واحدًا بنطاق التسجيل. تعمل دالة **resolver** (المُعلَنة باستخدام `serverRouteTriggerSettings`) في مساحة عمل المالك وتُرجِع إمّا كائن `Response` متزامنًا أو كلاً من مساحة العمل المستهدفة ودالة المنطق المطلوب إدراجها في قائمة الانتظار؛ في مسار الإدراج في قائمة الانتظار يؤكّد النظام الأساسي تلقّي الطلب برمز `202` ويُشغِّل ذلك **الهدف** في طابور العامل (worker queue). راجع [مشغّل مسار الخادم](#server-route-trigger).
|
||||
|
||||
<Note>
|
||||
يمكنك أيضًا تنفيذ دالة يدويًا باستخدام CLI:
|
||||
@@ -177,13 +177,18 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
يتكوّن المشغّل من جزأين:
|
||||
|
||||
1. دالة منطق **resolver** — يتم التصريح عنها باستخدام `serverRouteTriggerSettings` — تعمل في **مساحة العمل المالكة** (مساحة العمل التي تمتلك تسجيل التطبيق). تتفحّص الطلب الوارد وتُرجِع `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }`، لتحديد *كلٍ من* مساحة العمل المستهدفة والدالة المستهدفة. يُعَدّ الـ resolver نقطة التفويض الوحيدة — فعنوان URL يحمل فقط معرّف الـ resolver. **هذا هو المكان المفضّل للتحقق من تواقيع الطلبات**: يعمل الـ resolver قبل أي تأثير جانبي، ولديه إمكانية الوصول إلى `rawBody` الأصلي والرؤوس المُمرَّرة، ويمكنه رفض الطلب دون لمس الهدف مطلقًا.
|
||||
2. دالة منطق **target** — دالة منطق عادية لكل مساحة عمل — تعمل بعد ذلك في مساحة العمل التي تم حلّها باستخدام الحمولة التي أعادها الـ resolver (أو حمولة الطلب الأصلية إذا لم يقم الـ resolver بتحويلها). تصبح القيمة التي تعيدها هي استجابة HTTP.
|
||||
1. دالة منطق **resolver** — يتم التصريح عنها باستخدام `serverRouteTriggerSettings` — تعمل في **مساحة العمل المالكة** (مساحة العمل التي تمتلك تسجيل التطبيق). تفحص الطلب الوارد وتُرجِع إمّا:
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — يضع النظام الأساسي ذلك الهدف في قائمة الانتظار في مساحة العمل المُحدَّدة ويؤكّد تلقّي الطلب برمز `202 { queued: true }`، أو
|
||||
* `Response` من `twenty-sdk/logic-function` — تُعيد المنصّة إرسال تلك الاستجابة عبر HTTP بشكل متزامن ولا تُدرِج هدفًا في قائمة الانتظار (استخدم هذا لمصافحات التحدّي مثل Slack `url_verification`).
|
||||
|
||||
يُعَدّ الـ resolver نقطة التفويض الوحيدة — فعنوان URL يحمل فقط معرّف الـ resolver. **هذا هو المكان المفضّل للتحقق من تواقيع الطلبات**: يعمل الـ resolver قبل أي تأثير جانبي، ولديه إمكانية الوصول إلى `rawBody` الأصلي والرؤوس المُمرَّرة، ويمكنه رفض الطلب دون لمس الهدف مطلقًا.
|
||||
2. دالة منطق **target** — دالة منطق عادية لكل مساحة عمل — تعمل بعد ذلك في مساحة العمل التي تم حلّها باستخدام الحمولة التي أعادها الـ resolver (أو حمولة الطلب الأصلية إذا لم يقم الـ resolver بتحويلها). قيمة الإرجاع الخاصة به **لا** يطّلع عليها مستدعي HTTP عندما يختار الـ resolver مسار الإضافة إلى الطابور (enqueue path).
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -210,12 +215,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -264,7 +282,7 @@ POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniver
|
||||
**يجب أن يتم المطالبة بالتطبيق وتثبيته في مساحة عمل المالك الخاصة به.** نظرًا لأن محلِّل الاستدعاء يعمل في **مساحة عمل المالك** (مساحة العمل التي تمتلك تسجيل التطبيق)، فإن مشغّل مسار الخادم يعمل فقط بمجرد أن يكون قد تم *المطالبة* بالتطبيق — أي أصبح لديه مساحة عمل مالكة — **و** تم **تثبيت هذا التطبيق في مساحة عمل المالك**. إلى أن يتحقق الشرطان معًا، فلن يكون لدى محلِّل الاستدعاء مكان يعمل فيه، وبالتالي لا يمكن إرسال المسار. لذلك لا يمكن إدراج أي تطبيق يعرِّض دالة منطقية `serverRouteTriggerSettings` في السوق حتى تتم المطالبة به وتثبيته في مساحة عمل المالك الخاصة به.
|
||||
</Note>
|
||||
|
||||
**عقد الـ Resolver.** يفرض نوع `LogicFunctionConfig` في حزمة SDK هذا في وقت الترجمة: بمجرد تعيينك لـ `serverRouteTriggerSettings`، يُقيَّد الـ handler الخاص بك بأن يُرجِع `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (أو `Promise` من هذا الكائن). يجب أن يكون `workspaceId` لمساحة عمل تكون الدالة المستهدفة مثبّتة فيها، وإلا فسيتم رفض الطلب مع `404`.
|
||||
**عقد الـ Resolver.** يفرض نوع `LogicFunctionConfig` في حزمة SDK هذا في وقت الترجمة: بمجرد تعيينك لـ `serverRouteTriggerSettings`، يُقيَّد الـ handler الخاص بك بأن يُرجِع إما `Response`، أو `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (أو `Promise` لأيٍّ منهما). على مسار الإرسال (dispatch path)، يجب أن يكون `workspaceId` لمساحة عمل تكون الدالة المستهدفة مثبّتة فيها، وإلا فسيتم رفض الطلب مع `404`. أي نتيجة لا تطابق أياً من البنيتين — بما في ذلك تلك التي لا تكون معرّفاتها UUIDs — تُرفَض مع رمز الحالة `502`.
|
||||
|
||||
| الحقل | النوع | الملاحظات |
|
||||
| ---------------------------------------- | ------------------ | ---------------------------------------------------------------------- |
|
||||
@@ -289,7 +307,9 @@ POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniver
|
||||
يُظهِر مثال الـ resolver أعلاه بالفعل تدفّق GitHub HMAC-SHA256 — عدِّل اسم الرأس وترميز الملخّص وسلسلة الحمولة الموقّعة بحسب المزوّد الذي تدمجه.
|
||||
|
||||
<Note>
|
||||
يستجيب المسار بـ `202 { queued: true }` مباشرة بعد أن تُرجِع دالة resolver نتيجتها وتعمل الدالة المستهدفة على طابور العامل (worker queue) — المتصل لا يطّلع أبدًا على زمن استجابة الدالة المستهدفة أو نتيجتها أو حالات الفشل الخاصة بها (تُسجَّل هذه في سجلات التنفيذ). هذا يمنع عمليات إعادة الإرسال من جهة المرسِل من تضخيم تباطؤ المعالجة، وهو ما تريده عند استيعاب خطافات الويب. بالنسبة لنقاط النهاية التي يجب على المتصل قراءة محتوى الاستجابة لها (مثل challenge handshakes وSlack commands)، استخدِم مسارًا من نوع `httpRouteTriggerSettings` بدلاً من ذلك. احرص على أن تكون دالة resolver سريعة — بعض المزوّدين (مثل Slack) تنتهي مهلة طلباتهم خلال بضع ثوانٍ. نظرًا لأن الـ resolver يمكن الوصول إليه كنقطة نهاية عامة، قم بحمايته من خلال تحديد المعدّل (rate limiting) على الحافة لديك.
|
||||
عندما يُرجِع الـ resolver كائن إرسال (dispatch object)، يستجيب المسار بـ `202 { queued: true }` وتعمل الدالة المستهدفة على طابور العامل (worker queue) — المتصل لا يطّلع أبدًا على زمن استجابة الدالة المستهدفة أو نتيجتها أو حالات الفشل الخاصة بها (تُسجَّل هذه في سجلات التنفيذ). هذا يمنع عمليات إعادة الإرسال من جهة المرسِل من تضخيم تباطؤ المعالجة، وهو ما تريده عند استيعاب خطافات الويب.
|
||||
|
||||
عندما يجب على المتصل قراءة جسم الاستجابة في نفس الطلب (مثل challenge handshakes أو interactive acknowledgements)، أرجِع `Response` من **الـ resolver** بدلًا من ذلك. تعيد المنصّة إرجاعها بشكل متزامن وتتجاوز الطابور؛ تمر ترويساتها (headers) عبر نفس قائمة السماح (allow-list) الخاصة باستجابات مسارات HTTP. احرص على أن تكون دالة resolver سريعة — بعض المزوّدين (مثل Slack) تنتهي مهلة طلباتهم خلال بضع ثوانٍ. نظرًا لأن الـ resolver يمكن الوصول إليه كنقطة نهاية عامة، قم بحمايته من خلال تحديد المعدّل (rate limiting) على الحافة لديك.
|
||||
</Note>
|
||||
|
||||
#### حمولة مُحفِّز حدث قاعدة البيانات
|
||||
|
||||
@@ -59,7 +59,7 @@ Chcete-li vyvolat logickou funkci spuštěnou trasou z (bezhlavé) front-endové
|
||||
* **cron**: Spouští vaši funkci podle plánu pomocí výrazu CRON.
|
||||
* **databaseEvent**: Spouští se při událostech životního cyklu objektů v pracovním prostoru. Když je operace události `updated`, lze konkrétní sledovaná pole určit v poli `updatedFields`. Pokud zůstane nedefinované nebo prázdné, spustí funkci jakákoli aktualizace.
|
||||
> např. `person.updated`, `*.created`, `company.*`
|
||||
* **serverRoute**: Zpřístupňuje jednu registrací omezenou trasu HTTP. Funkce **resolver** (deklarovaná pomocí `serverRouteTriggerSettings`) běží ve vlastnickém workspace a vrací cílový workspace i cílovou logickou funkci, na kterou se má směrovat; platforma potvrdí přijetí s `202` a spustí tuto **cílovou** funkci ve frontě workeru. Viz [spouštěč serverové trasy](#server-route-trigger).
|
||||
* **serverRoute**: Zpřístupňuje jednu registrací omezenou trasu HTTP. Funkce **resolver** (deklarovaná pomocí `serverRouteTriggerSettings`) běží ve vlastnickém workspace a buď vrátí synchronní `Response`, nebo cílový workspace a logickou funkci, která se má zařadit do fronty; v případě zařazení platforma potvrdí přijetí kódem `202` a spustí tuto **cílovou** funkci ve frontě workeru. Viz [spouštěč serverové trasy](#server-route-trigger).
|
||||
|
||||
<Note>
|
||||
Funkci můžete také spustit ručně pomocí CLI:
|
||||
@@ -178,13 +178,18 @@ Stavový kód musí být platný stavový kód HTTP (mezi 100 a 599). Názvy hla
|
||||
|
||||
Spouštěč má dvě části:
|
||||
|
||||
1. Logická funkce **resolveru** — deklarovaná pomocí `serverRouteTriggerSettings` — běží ve vašem **vlastnickém workspace** (workspace, který je vlastníkem registrace aplikace). Prohlédne si příchozí request a vrátí `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }`, čímž zvolí *obě* — cílový workspace i cílovou funkci. Resolver je jediným místem autorizace — URL nese pouze identifikátor resolveru. **Toto je preferované místo pro ověřování podpisů requestů**: resolver běží před jakýmikoli vedlejšími efekty, má přístup k původnímu `rawBody` a předaným hlavičkám a může request odmítnout, aniž by se vůbec dotkl cíle.
|
||||
2. Cílová (**target**) logická funkce — běžná per-workspace logická funkce — pak běží v určeném workspace s payloadem vráceným resolverem (nebo s původním payloadem requestu, pokud jej resolver neupravil). Její návratová hodnota se stává HTTP odpovědí.
|
||||
1. Logická funkce **resolveru** — deklarovaná pomocí `serverRouteTriggerSettings` — běží ve vašem **vlastnickém workspace** (workspace, který je vlastníkem registrace aplikace). Prozkoumá příchozí požadavek a vrátí buď:
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — platforma zařadí tento cíl do fronty v určeném workspace a potvrdí přijetí s `202 { queued: true }`, nebo
|
||||
* `Response` z `twenty-sdk/logic-function` — platforma tento HTTP response vrátí **synchronně** a cíl do fronty **ne**zařadí (použijte pro ověřovací handshake, například Slack `url_verification`).
|
||||
|
||||
Resolver je jediným místem autorizace — URL nese pouze identifikátor resolveru. **Toto je preferované místo pro ověřování podpisů requestů**: resolver běží před jakýmikoli vedlejšími efekty, má přístup k původnímu `rawBody` a předaným hlavičkám a může request odmítnout, aniž by se vůbec dotkl cíle.
|
||||
2. Cílová (**target**) logická funkce — běžná per-workspace logická funkce — pak běží v určeném workspace s payloadem vráceným resolverem (nebo s původním payloadem requestu, pokud jej resolver neupravil). Návratovou hodnotu volající HTTP **nevidí**, když resolver zvolí cestu zařazení do fronty.
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -211,12 +216,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -265,7 +283,7 @@ Identifikátor je `universalIdentifier` resolveru z vašeho manifestu. Zaregistr
|
||||
**Aplikace musí být převzata do vlastnictví a nainstalována v pracovním prostoru vlastníka.** Protože resolver běží v **pracovním prostoru vlastníka** (pracovní prostor, který vlastní registraci aplikace), spouštěč serverové trasy funguje pouze tehdy, když byla aplikace *převzata do vlastnictví* — tj. má pracovní prostor vlastníka — **a** tato aplikace je **nainstalována v pracovním prostoru vlastníka**. Dokud nejsou obě podmínky splněny, resolver nemá kde běžet, takže trasu nelze zpracovat. Aplikace, která zpřístupňuje logickou funkci `serverRouteTriggerSettings`, proto nemůže být uvedena na Marketplace, dokud není převzata do vlastnictví a nainstalována v pracovním prostoru vlastníka.
|
||||
</Note>
|
||||
|
||||
**Smlouva resolveru.** Typ `LogicFunctionConfig` v SDK toto vynucuje v době kompilace: jakmile nastavíte `serverRouteTriggerSettings`, váš handler je omezen tak, aby vracel `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (nebo `Promise` této hodnoty). `workspaceId` musí být workspace, ve kterém je cílová funkce nainstalována, jinak je request odmítnut s chybou `404`.
|
||||
**Smlouva resolveru.** Typ `LogicFunctionConfig` v SDK toto vynucuje v době kompilace: jakmile nastavíte `serverRouteTriggerSettings`, váš handler je omezen tak, aby vracel buď `Response`, nebo `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (nebo `Promise` jedné z těchto možností). Na dispatch cestě musí být `workspaceId` workspace, ve kterém je cílová funkce nainstalována, jinak je request odmítnut s chybou `404`. Výsledek, který neodpovídá ani jedné z těchto struktur — včetně takového, jehož identifikátory nejsou UUID — je odmítnut s chybou `502`.
|
||||
|
||||
| Pole | Typ | Poznámky |
|
||||
| ---------------------------------------- | -------------------- | ------------------------------------------------------------------------------ |
|
||||
@@ -290,7 +308,9 @@ U podpisů requestů většina poskytovatelů podepisuje pomocí HMAC-SHA256; č
|
||||
Příklad resolveru výše už ukazuje GitHub HMAC-SHA256 flow — přizpůsobte název hlavičky, kódování digestu a podepsaný řetězec payloadu podle poskytovatele, se kterým se integrujete.
|
||||
|
||||
<Note>
|
||||
Route odpoví `202 { queued: true }` hned poté, co resolver vrátí výsledek, a cíl běží ve frontě workeru — volající nikdy nevidí latenci, výsledek ani chyby cíle (ty jsou zaznamenané v logách běhu). Tím se zabrání tomu, aby opakované doručování na straně odesílatele znásobovalo zpomalení zpracování, což je přesně to, co chcete pro příjem webhooků. Pro endpointy, u kterých volající musí přečíst tělo odpovědi (challenge handshaky, příkazy Slacku), místo toho použijte route s `httpRouteTriggerSettings`. Udržujte resolver rychlý — některým poskytovatelům (např. Slack) vyprší časový limit během několika sekund. Protože je resolver dostupný jako veřejný endpoint, chraňte ho omezením rychlosti (rate limiting) na své edge vrstvě.
|
||||
Když resolver vrátí dispatch objekt, route odpoví `202 { queued: true }` a cíl běží ve frontě workeru — volající nikdy nevidí latenci cíle, výsledek ani chyby (ty jsou zaznamenané v logách běhu). Tím se zabrání tomu, aby opakované doručování na straně odesílatele znásobovalo zpomalení zpracování, což je přesně to, co chcete pro příjem webhooků.
|
||||
|
||||
Když volající musí v rámci stejného requestu přečíst tělo odpovědi (challenge handshaky, interaktivní potvrzení), vraťte místo toho z **resolveru** `Response`. Platforma jej synchronně zopakuje a přeskočí frontu; jeho hlavičky procházejí stejným seznamem povolených položek jako odpovědi HTTP rout. Udržujte resolver rychlý — některým poskytovatelům (např. Slack) vyprší časový limit během několika sekund. Protože je resolver dostupný jako veřejný endpoint, chraňte ho omezením rychlosti (rate limiting) na své edge vrstvě.
|
||||
</Note>
|
||||
|
||||
#### Payload spouštěče databázové události
|
||||
|
||||
@@ -59,7 +59,7 @@ Um eine routenausgelöste Logikfunktion von einer (headless) Front-Komponente au
|
||||
* **cron**: Führt Ihre Funktion nach Zeitplan mithilfe eines CRON-Ausdrucks aus.
|
||||
* **databaseEvent**: Wird bei Lebenszyklusereignissen von Workspace-Objekten ausgeführt. Wenn die Ereignisoperation `updated` ist, können bestimmte zu überwachende Felder im Array `updatedFields` angegeben werden. Wenn das Array undefiniert oder leer ist, löst jede Aktualisierung die Funktion aus.
|
||||
> z. B. `person.updated`, `*.created`, `company.*`
|
||||
* **serverRoute**: Stellt eine einzelne, registrierungsbezogene HTTP-Route bereit. Eine **Resolver**-Funktion (deklariert mit `serverRouteTriggerSettings`) läuft im Owner-Workspace und gibt den Ziel-Workspace UND die Ziel-Logikfunktion zurück, an die weitergeleitet werden soll; die Plattform bestätigt mit `202` und führt diese **Ziel**-Funktion in der Worker-Queue aus. Siehe [Server-Route-Trigger](#server-route-trigger).
|
||||
* **serverRoute**: Stellt eine einzelne, registrierungsbezogene HTTP-Route bereit. Eine **Resolver**-Funktion (deklariert mit `serverRouteTriggerSettings`) läuft im Owner-Workspace und gibt entweder eine synchrone `Response` zurück oder den Ziel-Workspace UND die Ziel-Logikfunktion, die in die Queue eingereiht werden soll; auf dem Enqueue-Pfad bestätigt die Plattform mit `202` und führt dieses **Ziel** in der Worker-Queue aus. Siehe [Server-Route-Trigger](#server-route-trigger).
|
||||
|
||||
<Note>
|
||||
Sie können eine Funktion auch manuell über die CLI ausführen:
|
||||
@@ -177,13 +177,18 @@ Der Statuscode muss ein gültiger HTTP-Statuscode sein (zwischen 100 und 599). A
|
||||
|
||||
Der Trigger besteht aus zwei Teilen:
|
||||
|
||||
1. Eine **Resolver**-Logikfunktion – deklariert mit `serverRouteTriggerSettings` – läuft in deinem **Owner-Workspace** (dem Workspace, dem die Anwendungsregistrierung gehört). Sie untersucht die eingehende Anfrage und gibt `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` zurück und wählt dabei *sowohl* den Ziel-Workspace als auch die Zielfunktion aus. Der Resolver ist die einzige Autorisierungsstelle – die URL enthält nur den Bezeichner des Resolvers. **Dies ist der bevorzugte Ort, um Anfragesignaturen zu verifizieren**: Der Resolver läuft vor jeglicher Nebenwirkung, hat Zugriff auf den ursprünglichen `rawBody` und weitergeleitete Header und kann ablehnen, ohne jemals das Ziel zu berühren.
|
||||
2. Eine **Target**-Logikfunktion – eine reguläre, Workspace-spezifische Logikfunktion – läuft dann im aufgelösten Workspace mit dem Payload, der vom Resolver zurückgegeben wurde (oder dem ursprünglichen Anfrage-Payload, falls der Resolver ihn nicht transformiert hat). Ihr Rückgabewert wird zur HTTP-Antwort.
|
||||
1. Eine **Resolver**-Logikfunktion – deklariert mit `serverRouteTriggerSettings` – läuft in deinem **Owner-Workspace** (dem Workspace, dem die Anwendungsregistrierung gehört). Sie inspiziert die eingehende Anfrage und gibt entweder Folgendes zurück:
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — die Plattform reiht dieses Ziel im aufgelösten Workspace in die Queue ein und bestätigt mit `202 { queued: true }`, oder
|
||||
* eine `Response` von `twenty-sdk/logic-function` — die Plattform gibt diese HTTP-Response **synchron** zurück und reiht **kein** Ziel in die Queue ein (verwende dies für Challenge-Handshakes wie Slack `url_verification`).
|
||||
|
||||
Der Resolver ist die einzige Autorisierungsstelle – die URL enthält nur den Bezeichner des Resolvers. **Dies ist der bevorzugte Ort, um Anfragesignaturen zu verifizieren**: Der Resolver läuft vor jeglicher Nebenwirkung, hat Zugriff auf den ursprünglichen `rawBody` und weitergeleitete Header und kann ablehnen, ohne jemals das Ziel zu berühren.
|
||||
2. Eine **Target**-Logikfunktion – eine reguläre, Workspace-spezifische Logikfunktion – läuft dann im aufgelösten Workspace mit dem Payload, der vom Resolver zurückgegeben wurde (oder dem ursprünglichen Anfrage-Payload, falls der Resolver ihn nicht transformiert hat). Sein Rückgabewert wird vom HTTP-Aufrufer **nicht** beobachtet, wenn der Resolver den Enqueue-Pfad gewählt hat.
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -210,12 +215,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -264,7 +282,7 @@ Der Bezeichner ist der `universalIdentifier` des Resolvers aus deinem Manifest.
|
||||
**Die Anwendung muss beansprucht und im Arbeitsbereich ihres Besitzers installiert werden.** Da der Resolver im **Owner-Arbeitsbereich** ausgeführt wird (dem Arbeitsbereich, der die Anwendungsregistrierung besitzt), funktioniert ein Server-Route-Trigger nur, wenn die Anwendung *beansprucht* wurde – d. h. sie einen Owner-Arbeitsbereich hat – **und** diese Anwendung **im Owner-Arbeitsbereich installiert ist**. Solange beides nicht zutrifft, hat der Resolver keinen Ausführungsort, sodass die Route nicht ausgeführt werden kann. Eine Anwendung, die eine `serverRouteTriggerSettings`-Logikfunktion bereitstellt, kann daher nicht im Marketplace aufgeführt werden, bevor sie beansprucht und im Owner-Arbeitsbereich installiert wurde.
|
||||
</Note>
|
||||
|
||||
**Resolver-Vertrag.** Der `LogicFunctionConfig`-Typ des SDK erzwingt dies zur Compile-Zeit: Sobald du `serverRouteTriggerSettings` setzt, ist dein Handler darauf beschränkt, `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (oder ein `Promise` davon) zurückzugeben. Die `workspaceId` muss auf einen Workspace verweisen, in dem die Zielfunktion installiert ist, andernfalls wird die Anfrage mit `404` abgelehnt.
|
||||
**Resolver-Vertrag.** Der `LogicFunctionConfig`-Typ des SDK erzwingt dies zur Compile-Zeit: Sobald du `serverRouteTriggerSettings` setzt, ist dein Handler darauf beschränkt, entweder eine `Response` oder `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` zurückzugeben (oder ein `Promise` von einem der beiden). Auf dem Dispatch-Pfad muss die `workspaceId` auf einen Workspace verweisen, in dem die Zielfunktion installiert ist, andernfalls wird die Anfrage mit `404` abgelehnt. Ein Ergebnis, das keiner der beiden Formen entspricht – einschließlich eines Ergebnisses, dessen Bezeichner keine UUIDs sind – wird mit `502` abgelehnt.
|
||||
|
||||
| Feld | Typ | Notizen |
|
||||
| ---------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------- |
|
||||
@@ -289,7 +307,9 @@ Für Anfragesignaturen signieren die meisten Provider mit HMAC-SHA256; die Teile
|
||||
Das obige Resolver-Beispiel zeigt bereits den GitHub-HMAC-SHA256-Flow – passe den Header-Namen, die Digest-Codierung und den signierten Payload-String entsprechend dem Provider an, den du integrierst.
|
||||
|
||||
<Note>
|
||||
Die Route antwortet mit `202 { queued: true }`, sobald der Resolver zurückkehrt und das Ziel in der Worker-Queue ausgeführt wird – der Aufrufer bekommt weder die Latenz, noch das Ergebnis oder Fehler des Ziels mit (diese werden in den Ausführungsprotokollen aufgezeichnet). Dadurch wird verhindert, dass erneute Zustellungen des Senders Verarbeitungsverzögerungen verstärken – genau das ist bei der Erfassung von Webhooks erwünscht. Für Endpunkte, bei denen der Aufrufer den Response-Body lesen muss (Challenge-Handshakes, Slack-Befehle), verwenden Sie stattdessen eine `httpRouteTriggerSettings`-Route. Halten Sie den Resolver schnell – einige Provider (z. B. Slack) laufen nach wenigen Sekunden in ein Timeout. Da der Resolver als öffentlicher Endpoint erreichbar ist, schütze ihn mit Rate-Limiting an deinem Edge.
|
||||
Wenn der Resolver ein Dispatch-Objekt zurückgibt, antwortet die Route mit `202 { queued: true }` und das Ziel wird in der Worker-Queue ausgeführt – der Aufrufer bekommt weder die Latenz, noch das Ergebnis oder Fehler des Ziels mit (diese werden in den Ausführungsprotokollen aufgezeichnet). Dadurch wird verhindert, dass erneute Zustellungen des Senders Verarbeitungsverzögerungen verstärken – genau das ist bei der Erfassung von Webhooks erwünscht.
|
||||
|
||||
Wenn der Aufrufer den Response-Body in derselben Anfrage lesen muss (Challenge-Handshakes, interaktive Bestätigungen), gib stattdessen eine `Response` aus dem **Resolver** zurück. Die Plattform gibt sie synchron zurück und überspringt die Queue; ihre Header durchlaufen dieselbe Allow-Liste wie HTTP-Routen-Antworten. Halten Sie den Resolver schnell – einige Provider (z. B. Slack) laufen nach wenigen Sekunden in ein Timeout. Da der Resolver als öffentlicher Endpoint erreichbar ist, schütze ihn mit Rate-Limiting an deinem Edge.
|
||||
</Note>
|
||||
|
||||
#### Datenbank-Event-Trigger-Payload
|
||||
|
||||
@@ -59,7 +59,7 @@ Para invocar una función de lógica activada por una ruta desde un componente d
|
||||
* **cron**: Ejecuta tu función en un horario usando una expresión CRON.
|
||||
* **databaseEvent**: Se ejecuta en eventos del ciclo de vida de objetos del espacio de trabajo. Cuando la operación del evento es `updated`, se pueden especificar campos específicos que se deben escuchar en la matriz `updatedFields`. Si se deja sin definir o vacío, cualquier actualización activará la función.
|
||||
> p. ej. `person.updated`, `*.created`, `company.*`
|
||||
* **serverRoute**: expone una única ruta HTTP con ámbito de registro. Una función de **resolver** (declarada con `serverRouteTriggerSettings`) se ejecuta en el espacio de trabajo propietario y devuelve el espacio de trabajo de destino Y la función de lógica de destino a la que se debe enviar; la plataforma confirma con `202` y ejecuta esa función de **destino** en la cola de workers. Consulta [Disparador de ruta de servidor](#server-route-trigger).
|
||||
* **serverRoute**: expone una única ruta HTTP con ámbito de registro. Una función de **resolver** (declarada con `serverRouteTriggerSettings`) se ejecuta en el espacio de trabajo propietario y devuelve una `Response` síncrona o el espacio de trabajo de destino Y la función de lógica de destino para poner en cola; en la ruta de encolado, la plataforma confirma con `202` y ejecuta ese **destino** en la cola de workers. Consulta [Disparador de ruta de servidor](#server-route-trigger).
|
||||
|
||||
<Note>
|
||||
También puedes ejecutar manualmente una función usando la CLI:
|
||||
@@ -178,13 +178,18 @@ El código de estado debe ser un código de estado HTTP válido (entre 100 y 599
|
||||
|
||||
El disparador tiene dos partes:
|
||||
|
||||
1. Una función de lógica de **resolver** — declarada con `serverRouteTriggerSettings` — se ejecuta en tu **espacio de trabajo propietario** (el espacio de trabajo que es propietario del registro de la aplicación). Inspecciona la solicitud entrante y devuelve `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }`, eligiendo *tanto* el espacio de trabajo de destino como la función de destino. El resolver es el único punto de autorización: la URL solo lleva el identificador del resolver. **Este es el lugar preferido para verificar las firmas de las solicitudes**: el resolver se ejecuta antes de cualquier efecto secundario, tiene acceso al `rawBody` original y a los encabezados reenviados, y puede rechazar sin tocar nunca el destino.
|
||||
2. Luego, una función de lógica de **destino** — una función de lógica normal por espacio de trabajo — se ejecuta en el espacio de trabajo resuelto con el payload devuelto por el resolver (o el payload original de la solicitud si el resolver no lo transformó). Su valor de retorno se convierte en la respuesta HTTP.
|
||||
1. Una función de lógica de **resolver** — declarada con `serverRouteTriggerSettings` — se ejecuta en tu **espacio de trabajo propietario** (el espacio de trabajo que es propietario del registro de la aplicación). Inspecciona la solicitud entrante y devuelve una de las siguientes opciones:
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — la plataforma pone en cola ese destino en el espacio de trabajo resuelto y confirma con `202 { queued: true }`, o
|
||||
* un `Response` de `twenty-sdk/logic-function` — la plataforma replica esa respuesta HTTP de forma **sincrónica** y **no** pone en cola un destino (usa esto para desafíos de verificación como `url_verification` de Slack).
|
||||
|
||||
El resolver es el único punto de autorización: la URL solo lleva el identificador del resolver. **Este es el lugar preferido para verificar las firmas de las solicitudes**: el resolver se ejecuta antes de cualquier efecto secundario, tiene acceso al `rawBody` original y a los encabezados reenviados, y puede rechazar sin tocar nunca el destino.
|
||||
2. Luego, una función de lógica de **destino** — una función de lógica normal por espacio de trabajo — se ejecuta en el espacio de trabajo resuelto con el payload devuelto por el resolver (o el payload original de la solicitud si el resolver no lo transformó). Su valor de retorno **no** es observado por el solicitante HTTP cuando el resolver eligió la ruta de encolado.
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -211,12 +216,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -265,7 +283,7 @@ El identificador es el `universalIdentifier` del resolver de tu manifiesto. Regi
|
||||
**La aplicación debe reclamarse e instalarse en el espacio de trabajo propietario.** Dado que el resolver se ejecuta en el **espacio de trabajo propietario** (el espacio de trabajo que es propietario del registro de la aplicación), un desencadenador de ruta de servidor solo funciona una vez que la aplicación ha sido *reclamada*, es decir, tiene un espacio de trabajo propietario, **y** esa aplicación está **instalada en el espacio de trabajo propietario**. Hasta que ambas condiciones se cumplan, el resolver no tiene dónde ejecutarse, por lo que la ruta no puede despacharse. Por lo tanto, una aplicación que expone una función lógica `serverRouteTriggerSettings` no puede figurar en el marketplace hasta que haya sido reclamada e instalada en su espacio de trabajo propietario.
|
||||
</Note>
|
||||
|
||||
**Contrato del resolver.** El tipo `LogicFunctionConfig` del SDK aplica esto en tiempo de compilación: tan pronto como configuras `serverRouteTriggerSettings`, tu handler se ve obligado a devolver `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (o un `Promise` de esto). El `workspaceId` debe ser un espacio de trabajo donde la función de destino esté instalada; de lo contrario, la solicitud se rechaza con `404`.
|
||||
**Contrato del resolver.** El tipo `LogicFunctionConfig` del SDK aplica esto en tiempo de compilación: tan pronto como configuras `serverRouteTriggerSettings`, tu handler se ve obligado a devolver un `Response`, o `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (o un `Promise` de cualquiera de los dos). En la ruta de envío, el `workspaceId` debe ser un espacio de trabajo donde la función de destino esté instalada; de lo contrario, la solicitud se rechaza con `404`. Un resultado que no coincide con ninguna de las dos formas —incluido uno cuyos identificadores no sean UUID— se rechaza con `502`.
|
||||
|
||||
| Campo | Tipo | Notas |
|
||||
| ---------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------- |
|
||||
@@ -290,7 +308,9 @@ Para las firmas de solicitudes, la mayoría de los proveedores firman con HMAC-S
|
||||
El ejemplo de resolver anterior ya muestra el flujo HMAC-SHA256 de GitHub; adapta el nombre del encabezado, la codificación del digest y la cadena firmada del payload según el proveedor con el que te estés integrando.
|
||||
|
||||
<Note>
|
||||
La ruta responde `202 { queued: true }` justo después de que el resolver devuelve y el destino se ejecuta en la cola de workers — el solicitante nunca observa la latencia, el resultado ni los fallos del destino (estos se registran en los registros de ejecución). Esto evita que los reintentos del remitente amplifiquen las ralentizaciones del procesamiento, que es lo que se desea para la ingesta de webhooks. Para los endpoints cuyo solicitante debe leer el cuerpo de la respuesta (handshakes de verificación, comandos de Slack), usa en su lugar una ruta con `httpRouteTriggerSettings`. Mantén el resolver rápido: algunos proveedores (p. ej. Slack) agotan el tiempo de espera en pocos segundos. Como el resolver es accesible como un endpoint público, protégelo con limitación de tasa en el edge.
|
||||
Cuando el resolver devuelve un objeto de envío, la ruta responde `202 { queued: true }` y el destino se ejecuta en la cola de workers: el solicitante nunca observa la latencia, el resultado ni los fallos del destino (estos se registran en los registros de ejecución). Esto evita que los reintentos del remitente amplifiquen las ralentizaciones del procesamiento, que es lo que se desea para la ingesta de webhooks.
|
||||
|
||||
Cuando el solicitante deba leer el cuerpo de la respuesta en la misma solicitud (desafíos de verificación, acuses de recibo interactivos), en su lugar devuelve un `Response` desde el **resolver**. La plataforma lo replica de forma síncrona y omite la cola; sus cabeceras pasan por la misma lista de permitidos que las respuestas de rutas HTTP. Mantén el resolver rápido: algunos proveedores (p. ej. Slack) agotan el tiempo de espera en pocos segundos. Como el resolver es accesible como un endpoint público, protégelo con limitación de tasa en el edge.
|
||||
</Note>
|
||||
|
||||
#### Payload del disparador de evento de base de datos
|
||||
|
||||
@@ -59,7 +59,7 @@ Pour appeler une fonction logique déclenchée par une route depuis un composant
|
||||
* **cron** : Exécute votre fonction selon une planification à l’aide d’une expression CRON.
|
||||
* **databaseEvent**: S'exécute lors des événements du cycle de vie des objets de l'espace de travail. Lorsque l'opération de l'événement est `updated`, des champs spécifiques à surveiller peuvent être spécifiés dans le tableau `updatedFields`. S'il est laissé indéfini ou vide, toute mise à jour déclenchera la fonction.
|
||||
> p. ex. `person.updated`, `*.created`, `company.*`
|
||||
* **serverRoute** : expose une seule route HTTP à portée d’enregistrement. Une fonction de **résolution** (déclarée avec `serverRouteTriggerSettings`) s’exécute dans l’espace de travail propriétaire et renvoie l’espace de travail cible ET la fonction logique cible vers laquelle acheminer la requête ; la plateforme accuse réception avec `202` et exécute cette fonction **cible** dans la file d’attente du worker. Voir [déclencheur de route serveur](#server-route-trigger).
|
||||
* **serverRoute** : expose une seule route HTTP à portée d’enregistrement. Une fonction de **résolution** (déclarée avec `serverRouteTriggerSettings`) s’exécute dans l’espace de travail propriétaire et renvoie soit une `Response` synchrone, soit l’espace de travail cible ET la fonction logique cible à mettre en file d’attente ; dans le cas de la mise en file d’attente, la plateforme accuse réception avec `202` et exécute cette **cible** dans la file d’attente du worker. Voir [déclencheur de route serveur](#server-route-trigger).
|
||||
|
||||
<Note>
|
||||
Vous pouvez également exécuter manuellement une fonction à l'aide de la CLI :
|
||||
@@ -178,13 +178,18 @@ Le code d’état doit être un code d’état HTTP valide (compris entre 100 et
|
||||
|
||||
Le déclencheur comporte deux parties :
|
||||
|
||||
1. Une fonction de logique de **résolution** — déclarée avec `serverRouteTriggerSettings` — s’exécute dans votre **espace de travail propriétaire** (l’espace de travail qui possède l’enregistrement de l’application). Elle inspecte la requête entrante et retourne `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }`, en choisissant *à la fois* l’espace de travail cible et la fonction cible. Le résolveur est le point d’autorisation unique — l’URL transporte uniquement l’identifiant du résolveur. **C’est l’endroit privilégié pour vérifier les signatures des requêtes** : le résolveur s’exécute avant tout effet de bord, a accès au `rawBody` original et aux en-têtes transmis, et peut rejeter la requête sans jamais toucher la cible.
|
||||
2. Une fonction de logique **cible** — une fonction de logique classique par espace de travail — s’exécute ensuite dans l’espace de travail résolu avec la charge utile renvoyée par le résolveur (ou la charge utile originale de la requête si le résolveur ne l’a pas transformée). Sa valeur de retour devient la réponse HTTP.
|
||||
1. Une fonction de logique de **résolution** — déclarée avec `serverRouteTriggerSettings` — s’exécute dans votre **espace de travail propriétaire** (l’espace de travail qui possède l’enregistrement de l’application). Elle inspecte la requête entrante et renvoie soit :
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — la plateforme met cette cible en file d’attente dans l’espace de travail résolu et accuse réception avec `202 { queued: true }`, ou
|
||||
* une `Response` de `twenty-sdk/logic-function` — la plateforme renvoie cette réponse HTTP **de manière synchrone** et ne met **pas** de cible en file d’attente (utilisez ceci pour les échanges de vérification, comme la `url_verification` de Slack).
|
||||
|
||||
Le résolveur est le point d’autorisation unique — l’URL transporte uniquement l’identifiant du résolveur. **C’est l’endroit privilégié pour vérifier les signatures des requêtes** : le résolveur s’exécute avant tout effet de bord, a accès au `rawBody` original et aux en-têtes transmis, et peut rejeter la requête sans jamais toucher la cible.
|
||||
2. Une fonction de logique **cible** — une fonction de logique classique par espace de travail — s’exécute ensuite dans l’espace de travail résolu avec la charge utile renvoyée par le résolveur (ou la charge utile originale de la requête si le résolveur ne l’a pas transformée). Sa valeur de retour **n’est pas** observée par l’appelant HTTP lorsque le résolveur a choisi le chemin de mise en file d’attente.
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -211,12 +216,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -265,7 +283,7 @@ L’identifiant est le `universalIdentifier` du résolveur issu de votre manifes
|
||||
**L’application doit être revendiquée et installée sur son espace de travail propriétaire.** Comme le résolveur s’exécute dans l’**espace de travail propriétaire** (l’espace de travail qui détient l’enregistrement de l’application), un déclencheur de route serveur ne fonctionne que lorsque l’application a été *revendiquée* — c’est‑à‑dire qu’elle possède un espace de travail propriétaire — **et** que cette application est **installée sur l’espace de travail propriétaire**. Tant que ces deux conditions ne sont pas remplies, le résolveur n’a nulle part où s’exécuter, donc la route ne peut pas être envoyée. Une application qui expose une fonction logique `serverRouteTriggerSettings` ne peut donc pas être répertoriée sur la place de marché tant qu’elle n’a pas été revendiquée et installée sur son espace de travail propriétaire.
|
||||
</Note>
|
||||
|
||||
**Contrat du résolveur.** Le type `LogicFunctionConfig` du SDK impose cela à la compilation : dès que vous définissez `serverRouteTriggerSettings`, votre gestionnaire est contraint de retourner `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (ou une `Promise` de cette valeur). Le `workspaceId` doit être celui d’un espace de travail où la fonction cible est installée, sinon la requête est rejetée avec un `404`.
|
||||
**Contrat du résolveur.** Le type `LogicFunctionConfig` du SDK impose cela à la compilation : dès que vous définissez `serverRouteTriggerSettings`, votre gestionnaire est contraint de retourner soit un `Response`, soit `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (ou une `Promise` de l’un ou l’autre). Sur le chemin de dispatch, le `workspaceId` doit être celui d’un espace de travail où la fonction cible est installée, sinon la requête est rejetée avec un `404`. Un résultat qui ne correspond à aucune de ces formes — y compris un résultat dont les identifiants ne sont pas des UUID — est rejeté avec un `502`.
|
||||
|
||||
| Champ | Type | Notes |
|
||||
| ---------------------------------------- | --------------------- | -------------------------------------------------------------------------------------- |
|
||||
@@ -290,7 +308,9 @@ Pour les signatures de requêtes, la plupart des fournisseurs signent avec HMAC-
|
||||
L’exemple de résolveur ci-dessus montre déjà le flux GitHub HMAC-SHA256 — adaptez le nom de l’en-tête, l’encodage de l’empreinte et la chaîne de la charge utile signée en fonction du fournisseur avec lequel vous vous intégrez.
|
||||
|
||||
<Note>
|
||||
La route répond `202 { queued: true }` juste après le retour du résolveur et la cible s’exécute dans la file d’attente du worker — l’appelant n’observe jamais la latence, le résultat ou les échecs de la cible (ceux-ci sont enregistrés dans les journaux d’exécution). Cela évite que les nouvelles tentatives d’envoi de l’émetteur n’amplifient les ralentissements de traitement, ce qui est souhaitable pour l’ingestion de webhook. Pour les endpoints dont l’appelant doit lire le corps de la réponse (handshakes de vérification, commandes Slack), utilisez plutôt une route `httpRouteTriggerSettings`. Gardez le résolveur rapide — certains fournisseurs (par ex. Slack) ont un délai d’attente de seulement quelques secondes. Comme le résolveur est accessible en tant que point de terminaison public, protégez-le avec une limitation de débit à votre périphérie.
|
||||
Lorsque le résolveur retourne un objet de dispatch, la route répond `202 { queued: true }` et la cible s’exécute dans la file d’attente du worker — l’appelant n’observe jamais la latence, le résultat ou les échecs de la cible (ceux-ci sont enregistrés dans les journaux d’exécution). Cela évite que les nouvelles tentatives d’envoi de l’émetteur n’amplifient les ralentissements de traitement, ce qui est souhaitable pour l’ingestion de webhook.
|
||||
|
||||
Lorsque l’appelant doit lire le corps de la réponse sur la même requête (handshakes de challenge, accusés de réception interactifs), retournez plutôt un `Response` depuis le **résolveur**. La plateforme le renvoie en écho de manière synchrone et ignore la file d’attente ; ses en-têtes passent par la même liste d’autorisation que les réponses de routes HTTP. Gardez le résolveur rapide — certains fournisseurs (par ex. Slack) ont un délai d’attente de seulement quelques secondes. Comme le résolveur est accessible en tant que point de terminaison public, protégez-le avec une limitation de débit à votre périphérie.
|
||||
</Note>
|
||||
|
||||
#### Charge utile du déclencheur d'événement de base de données
|
||||
|
||||
@@ -59,7 +59,7 @@ Per richiamare, da un componente front-end (headless), una funzione logica attiv
|
||||
* **cron**: Esegue la tua funzione secondo una pianificazione utilizzando un'espressione CRON.
|
||||
* **databaseEvent**: Viene eseguito sugli eventi del ciclo di vita degli oggetti dello spazio di lavoro. Quando l'operazione dell'evento è `updated`, è possibile specificare campi specifici da monitorare nell'array `updatedFields`. Se lasciato non definito o vuoto, qualsiasi aggiornamento attiverà la funzione.
|
||||
> ad es. `person.updated`, `*.created`, `company.*`
|
||||
* **serverRoute**: espone una singola route HTTP con ambito di registrazione. Una funzione di **resolver** (dichiarata con `serverRouteTriggerSettings`) viene eseguita nel workspace proprietario e restituisce sia il workspace di destinazione SIA la funzione logica di destinazione a cui indirizzare la richiesta; la piattaforma conferma con `202` ed esegue tale funzione di **destinazione** nella coda dei worker. Vedi [Trigger route del server](#server-route-trigger).
|
||||
* **serverRoute**: espone una singola route HTTP con ambito di registrazione. Una funzione di **resolver** (dichiarata con `serverRouteTriggerSettings`) viene eseguita nel workspace proprietario e restituisce una `Response` sincrona, oppure il workspace di destinazione e la funzione logica di destinazione da mettere in coda; nel percorso di messa in coda la piattaforma conferma con `202` ed esegue tale **destinazione** nella coda dei worker. Vedi [Trigger route del server](#server-route-trigger).
|
||||
|
||||
<Note>
|
||||
Puoi anche eseguire manualmente una funzione utilizzando la CLI:
|
||||
@@ -177,13 +177,18 @@ Il codice di stato deve essere un codice di stato HTTP valido (compreso tra 100
|
||||
|
||||
Il trigger ha due parti:
|
||||
|
||||
1. Una funzione logica di **resolver** — dichiarata con `serverRouteTriggerSettings` — viene eseguita nel tuo **workspace proprietario** (il workspace che possiede la registrazione dell'applicazione). Ispeziona la richiesta in ingresso e restituisce `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }`, scegliendo *sia* il workspace di destinazione che la funzione di destinazione. Il resolver è l'unico punto di autorizzazione: l'URL contiene solo l'identificatore del resolver. **Questo è il punto preferenziale per verificare le firme delle richieste**: il resolver viene eseguito prima di qualsiasi effetto collaterale, ha accesso al `rawBody` originale e agli header inoltrati, e può rifiutare senza toccare la destinazione.
|
||||
2. Una funzione logica di **destinazione** — una normale funzione logica per-workspace — viene quindi eseguita nel workspace risolto con il payload restituito dal resolver (o il payload originale della richiesta se il resolver non lo ha trasformato). Il suo valore di ritorno diventa la risposta HTTP.
|
||||
1. Una funzione logica di **resolver** — dichiarata con `serverRouteTriggerSettings` — viene eseguita nel tuo **workspace proprietario** (il workspace che possiede la registrazione dell'applicazione). Analizza la richiesta in ingresso e restituisce:
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — la piattaforma mette in coda tale destinazione nel workspace risolto e conferma con `202 { queued: true }`, oppure
|
||||
* una `Response` da `twenty-sdk/logic-function` — la piattaforma restituisce tale risposta HTTP **sincronicamente** e **non** mette in coda alcuna destinazione (usa questo per challenge handshake come Slack `url_verification`).
|
||||
|
||||
Il resolver è l'unico punto di autorizzazione: l'URL contiene solo l'identificatore del resolver. **Questo è il punto preferenziale per verificare le firme delle richieste**: il resolver viene eseguito prima di qualsiasi effetto collaterale, ha accesso al `rawBody` originale e agli header inoltrati, e può rifiutare senza toccare la destinazione.
|
||||
2. Una funzione logica di **destinazione** — una normale funzione logica per-workspace — viene quindi eseguita nel workspace risolto con il payload restituito dal resolver (o il payload originale della richiesta se il resolver non lo ha trasformato). Il suo valore di ritorno **non** viene osservato dal chiamante HTTP quando il resolver ha scelto il percorso di messa in coda.
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -210,12 +215,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -264,7 +282,7 @@ L'identificatore è il `universalIdentifier` del resolver dal tuo manifest. Regi
|
||||
**L'applicazione deve essere rivendicata e installata nel workspace del proprietario.** Poiché il resolver viene eseguito nel **workspace del proprietario** (il workspace che possiede la registrazione dell'applicazione), un server route trigger funziona solo quando l'applicazione è stata *rivendicata*, cioè ha un workspace del proprietario, **e** quell'applicazione è **installata nel workspace del proprietario**. Finché entrambe non sono vere, il resolver non ha dove essere eseguito, quindi la route non può essere gestita. Un'applicazione che espone una funzione logica `serverRouteTriggerSettings` quindi non può essere elencata nel marketplace finché non è stata rivendicata e installata nel workspace del proprietario.
|
||||
</Note>
|
||||
|
||||
**Contratto del resolver.** Il tipo `LogicFunctionConfig` dell'SDK impone questo a tempo di compilazione: non appena imposti `serverRouteTriggerSettings`, il tuo handler è vincolato a restituire `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (o una `Promise` di questo valore). Il `workspaceId` deve essere un workspace in cui la funzione di destinazione è installata, altrimenti la richiesta viene rifiutata con `404`.
|
||||
**Contratto del resolver.** Il tipo `LogicFunctionConfig` dell'SDK impone questo a tempo di compilazione: non appena imposti `serverRouteTriggerSettings`, il tuo handler è vincolato a restituire una `Response`, oppure `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (o una `Promise` di uno dei due). Nel percorso di dispatch, il `workspaceId` deve essere un workspace in cui la funzione di destinazione è installata, altrimenti la richiesta viene rifiutata con `404`. Un risultato che non corrisponde a nessuna delle due forme — incluso uno i cui identificatori non sono UUID — viene rifiutato con `502`.
|
||||
|
||||
| Campo | Tipo | Note |
|
||||
| ---------------------------------------- | -------------------- | ---------------------------------------------------------------------------- |
|
||||
@@ -289,7 +307,9 @@ Per le firme delle richieste, la maggior parte dei provider firma con HMAC-SHA25
|
||||
L'esempio di resolver sopra mostra già il flusso HMAC-SHA256 di GitHub — adatta il nome dell'header, la codifica del digest e la stringa del payload firmato in base al provider con cui ti stai integrando.
|
||||
|
||||
<Note>
|
||||
La route risponde con `202 { queued: true }` subito dopo che il resolver restituisce il risultato e la funzione di destinazione viene eseguita nella coda dei worker — il chiamante non osserva mai la latenza, il risultato o gli errori della funzione di destinazione (che vengono registrati nei log di esecuzione). Questo impedisce che le nuove consegne da parte del mittente amplifichino i rallentamenti nell'elaborazione, che è esattamente ciò che si desidera per l'acquisizione dei webhook. Per gli endpoint per i quali il chiamante deve leggere il corpo della risposta (challenge handshake, comandi Slack), utilizza invece una route con `httpRouteTriggerSettings`. Mantieni il resolver veloce — alcuni provider (ad es. Slack) vanno in timeout in pochi secondi. Poiché il resolver è raggiungibile come endpoint pubblico, proteggilo con rate limiting al tuo edge.
|
||||
Quando il resolver restituisce un oggetto di dispatch, la route risponde con `202 { queued: true }` e la funzione di destinazione viene eseguita nella coda dei worker — il chiamante non osserva mai la latenza, il risultato o gli errori della funzione di destinazione (che vengono registrati nei log di esecuzione). Questo impedisce che le nuove consegne da parte del mittente amplifichino i rallentamenti nell'elaborazione, che è esattamente ciò che si desidera per l'acquisizione dei webhook.
|
||||
|
||||
Quando il chiamante deve leggere il corpo della risposta sulla stessa richiesta (challenge handshake, acknowledgement interattivi), restituisci invece una `Response` dal **resolver**. La piattaforma lo riecheggia in modo sincrono e salta la coda; i suoi header passano attraverso la stessa allow-list delle risposte delle route HTTP. Mantieni il resolver veloce — alcuni provider (ad es. Slack) vanno in timeout in pochi secondi. Poiché il resolver è raggiungibile come endpoint pubblico, proteggilo con rate limiting al tuo edge.
|
||||
</Note>
|
||||
|
||||
#### Payload del trigger di evento del database
|
||||
|
||||
@@ -59,7 +59,7 @@ export default defineLogicFunction({
|
||||
* **cron**: CRON 式を使用してスケジュールで関数を実行します。
|
||||
* **databaseEvent**: ワークスペースのオブジェクトのライフサイクルイベントで実行されます。 イベント操作が `updated` の場合、監視する特定のフィールドを `updatedFields` 配列で指定できます。 未定義または空のままにすると、任意の更新でも関数がトリガーされます。
|
||||
> 例:`person.updated`、`*.created`、`company.*`
|
||||
* **serverRoute**: 1 つの登録スコープの HTTP ルートを公開します。 **resolver** 関数(`serverRouteTriggerSettings` で宣言)は、オーナーワークスペースで実行され、ディスパッチ先のターゲットワークスペースとターゲットロジック関数の両方を返します。プラットフォームは `202` で応答し、その **target** 関数をワーカーキュー上で実行します。 [サーバールートトリガー](#server-route-trigger) を参照してください。
|
||||
* **serverRoute**: 1 つの登録スコープの HTTP ルートを公開します。 **resolver** 関数(`serverRouteTriggerSettings` で宣言)は、オーナーワークスペースで実行され、同期的な `Response` を返すか、ターゲットワークスペースとエンキューする対象のロジック関数を返します。エンキュールートでは、プラットフォームは `202` で ACK し、その **target** をワーカーキュー上で実行します。 [サーバールートトリガー](#server-route-trigger) を参照してください。
|
||||
|
||||
<Note>
|
||||
CLI を使用して、関数を手動で実行することもできます:
|
||||
@@ -178,13 +178,18 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
トリガーには 2 つの構成要素があります:
|
||||
|
||||
1. **resolver** ロジック関数 — `serverRouteTriggerSettings` で宣言される — は、**オーナーワークスペース**(アプリケーション登録を所有するワークスペース)で実行されます。 受信リクエストを検査し、`{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` を返し、ターゲットのワークスペースと関数の *両方* を選択します。 resolver は単一の認可ポイントであり、URL には resolver の識別子のみが含まれます。 **ここがリクエスト署名を検証するための推奨箇所です**。resolver は副作用が発生する前に実行され、元の `rawBody` と転送されたヘッダーにアクセスでき、ターゲットに一切触れずにリクエストを拒否できます。
|
||||
2. **target** ロジック関数 — 通常のワークスペース単位のロジック関数 — は、その後、resolver によって解決されたワークスペースで、resolver が返したペイロード(resolver が変換しなかった場合は元のリクエストペイロード)を使って実行されます。 その戻り値が HTTP レスポンスになります。
|
||||
1. **resolver** ロジック関数 — `serverRouteTriggerSettings` で宣言される — は、**オーナーワークスペース**(アプリケーション登録を所有するワークスペース)で実行されます。 この関数は受信リクエストを検査し、次のいずれかを返します。
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — プラットフォームは解決済みワークスペースでその target をキューに入れ、`202 { queued: true }` で ACK します、または
|
||||
* `twenty-sdk/logic-function` からの `Response` — プラットフォームはその HTTP レスポンスを**同期的に**エコーし、ターゲットをキューに入れることは**ありません**(Slack の `url_verification` のようなチャレンジハンドシェイクに使用します)。
|
||||
|
||||
resolver は単一の認可ポイントであり、URL には resolver の識別子のみが含まれます。 **ここがリクエスト署名を検証するための推奨箇所です**。resolver は副作用が発生する前に実行され、元の `rawBody` と転送されたヘッダーにアクセスでき、ターゲットに一切触れずにリクエストを拒否できます。
|
||||
2. **target** ロジック関数 — 通常のワークスペース単位のロジック関数 — は、その後、resolver によって解決されたワークスペースで、resolver が返したペイロード(resolver が変換しなかった場合は元のリクエストペイロード)を使って実行されます。 resolver が enqueue パスを選択した場合、その戻り値は HTTP 呼び出し元からは**観測されません**。
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -211,12 +216,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -265,7 +283,7 @@ POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniver
|
||||
**アプリケーションは所有者ワークスペースでクレームされ、インストールされている必要があります。** リゾルバーは **所有者ワークスペース**(アプリケーション登録を所有しているワークスペース)上で実行されるため、サーバールートトリガーが動作するのは、アプリケーションが*クレーム*されている、つまり所有者ワークスペースを持っていること **かつ** そのアプリケーションが **所有者ワークスペースにインストールされている** 場合のみです。 この2つの条件がどちらも満たされるまでは、リゾルバーを実行する場所が存在しないため、ルートをディスパッチできません。 したがって、`serverRouteTriggerSettings` ロジック関数を公開するアプリケーションは、所有者ワークスペースでクレームされインストールされるまで、マーケットプレイスに掲載することはできません。
|
||||
</Note>
|
||||
|
||||
**Resolver の契約**。SDK の `LogicFunctionConfig` 型は、コンパイル時にこれを強制します。`serverRouteTriggerSettings` を設定するとすぐに、ハンドラーは `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }`(またはその `Promise`)を返すように制約されます。 `workspaceId` は、ターゲット関数がインストールされているワークスペースである必要があります。そうでない場合、リクエストは `404` で拒否されます。
|
||||
**Resolver の契約。** SDK の `LogicFunctionConfig` 型はコンパイル時にこれを強制します。`serverRouteTriggerSettings` を設定するとすぐに、ハンドラーは `Response` か、`{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }`(またはそのいずれかの `Promise`)を返すように制約されます。 dispatch パスでは、`workspaceId` は target 関数がインストールされているワークスペースである必要があります。そうでない場合、リクエストは `404` で拒否されます。 どちらの形にも一致しない結果(識別子が UUID でないものを含む)は、`502` で拒否されます。
|
||||
|
||||
| フィールド | タイプ | ノート |
|
||||
| ---------------------------------------- | --------------- | -------------------------------------------- |
|
||||
@@ -290,7 +308,9 @@ POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniver
|
||||
上記の resolver の例では、すでに GitHub の HMAC-SHA256 フローを示しています。連携するプロバイダーに合わせて、ヘッダー名、ダイジェストのエンコーディング、および署名対象となるペイロード文字列を調整してください。
|
||||
|
||||
<Note>
|
||||
ルートは、resolver が戻った直後に `202 { queued: true }` を返し、target はワーカーキュー上で実行されます。呼び出し元は target のレイテンシ、結果、失敗を一切観測しません(それらは実行ログに記録されます)。 これにより、送信側の再配信によって処理の遅延が増幅されるのを防げます。これは、Webhook 取り込みにおいて望ましい動作です。 呼び出し元がレスポンスボディを読み取る必要があるエンドポイント(チャレンジハンドシェイク、Slack コマンドなど)の場合は、代わりに `httpRouteTriggerSettings` ルートを使用してください。 resolver は高速に保ってください。Slack など一部のプロバイダーは数秒でタイムアウトします。 resolver はパブリックエンドポイントとして到達可能であるため、エッジでレート制限をかけて保護してください。
|
||||
resolver が dispatch オブジェクトを返すと、ルートは `202 { queued: true }` を返し、target はワーカーキュー上で実行されます。呼び出し元は target のレイテンシ、結果、失敗を一切観測しません(それらは実行ログに記録されます)。 これにより、送信側の再配信によって処理の遅延が増幅されるのを防げます。これは、Webhook 取り込みにおいて望ましい動作です。
|
||||
|
||||
呼び出し元が同じリクエストでレスポンスボディを読み取る必要がある場合(チャレンジハンドシェイク、対話的な受領確認など)、代わりに **resolver** から `Response` を返してください。 プラットフォームはそれを同期的にエコーし、キューをスキップします。そのヘッダーは HTTP ルートレスポンスと同じ許可リストを通過します。 resolver は高速に保ってください。Slack など一部のプロバイダーは数秒でタイムアウトします。 resolver はパブリックエンドポイントとして到達可能であるため、エッジでレート制限をかけて保護してください。
|
||||
</Note>
|
||||
|
||||
#### データベースイベントトリガーのペイロード
|
||||
|
||||
@@ -59,7 +59,7 @@ export default defineLogicFunction({
|
||||
* **cron**: CRON 식을 사용하여 예약된 일정으로 함수를 실행합니다.
|
||||
* **databaseEvent**: 워크스페이스 객체 라이프사이클 이벤트에서 실행됩니다. 이벤트 작업이 `updated`인 경우, 수신할 특정 필드를 `updatedFields` 배열에 지정할 수 있습니다. 정의하지 않거나 비워두면, 어떤 업데이트든 함수가 트리거됩니다.
|
||||
> 예: `person.updated`, `*.created`, `company.*`
|
||||
* **serverRoute**: 단일 등록 범위 HTTP 라우트를 노출합니다. `serverRouteTriggerSettings`로 선언된 **resolver** 함수는 소유자 워크스페이스에서 실행되며, 디스패치할 대상 워크스페이스와 대상 로직 함수를 반환합니다. 플랫폼은 `202`로 응답을 확인(ack)하고, 워커 큐에서 해당 **target** 함수를 실행합니다. [서버 라우트 트리거](#server-route-trigger)를 참고하세요.
|
||||
* **serverRoute**: 단일 등록 범위 HTTP 라우트를 노출합니다. `serverRouteTriggerSettings`로 선언된 **resolver** 함수는 소유자 워크스페이스에서 실행되며, 동기식 `Response`를 반환하거나, 대상 워크스페이스와 큐에 넣을 대상 로직 함수를 반환합니다. 큐에 넣는 경로의 경우 플랫폼은 `202`로 응답을 확인(ack)하고 워커 큐에서 해당 **target**을 실행합니다. [서버 라우트 트리거](#server-route-trigger)를 참고하세요.
|
||||
|
||||
<Note>
|
||||
CLI를 사용해 함수를 수동으로 실행할 수도 있습니다:
|
||||
@@ -177,13 +177,18 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
트리거는 두 부분으로 구성됩니다:
|
||||
|
||||
1. **resolver** 로직 함수 — `serverRouteTriggerSettings`로 선언되는 — 는 **소유자 워크스페이스**(애플리케이션 등록을 소유한 워크스페이스)에서 실행됩니다. 이 함수는 들어오는 요청을 검사하고 `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }`를 반환하여, 대상 워크스페이스와 대상 함수를 *둘 다* 선택합니다. resolver는 단일 인가 지점입니다 — URL에는 resolver의 식별자만 포함됩니다. **요청 서명을 검증하기에 가장 적합한 위치입니다**. resolver는 어떤 부수 효과가 발생하기 전에 실행되며, 원본 `rawBody`와 전달된 헤더에 접근할 수 있고, 대상에 전혀 접근하지 않고도 요청을 거부할 수 있습니다.
|
||||
2. 그 다음 **target** 로직 함수 — 일반적인 워크스페이스별 로직 함수 — 가 resolver가 반환한 payload(또는 resolver가 변환하지 않았다면 원본 요청 payload)를 가지고 결정된 워크스페이스에서 실행됩니다. 해당 함수의 반환 값이 HTTP 응답이 됩니다.
|
||||
1. **resolver** 로직 함수 — `serverRouteTriggerSettings`로 선언되는 — 는 **소유자 워크스페이스**(애플리케이션 등록을 소유한 워크스페이스)에서 실행됩니다. 이 함수는 들어오는 요청을 검사한 뒤 다음 중 하나를 반환합니다:
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — 플랫폼은 지정된 워크스페이스에서 해당 대상 함수를 큐에 넣고 `202 { queued: true }`로 응답을 확인(ack)합니다, 또는
|
||||
* `twenty-sdk/logic-function`의 `Response` — 플랫폼은 해당 HTTP 응답을 **동기적**으로 그대로 반환하고, target을 큐에 넣지는 **않습니다**(Slack `url_verification`과 같은 챌린지 핸드셰이크에 사용하십시오).
|
||||
|
||||
resolver는 단일 인가 지점입니다 — URL에는 resolver의 식별자만 포함됩니다. **요청 서명을 검증하기에 가장 적합한 위치입니다**. resolver는 어떤 부수 효과가 발생하기 전에 실행되며, 원본 `rawBody`와 전달된 헤더에 접근할 수 있고, 대상에 전혀 접근하지 않고도 요청을 거부할 수 있습니다.
|
||||
2. 그 다음 **target** 로직 함수 — 일반적인 워크스페이스별 로직 함수 — 가 resolver가 반환한 payload(또는 resolver가 변환하지 않았다면 원본 요청 payload)를 가지고 결정된 워크스페이스에서 실행됩니다. resolver가 enqueue 경로를 선택한 경우, 해당 반환 값은 HTTP 호출자가 **전혀** 관찰하지 못합니다.
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -210,12 +215,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -264,7 +282,7 @@ POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniver
|
||||
**애플리케이션은 소유자 워크스페이스가 지정되어 해당 워크스페이스에 설치되어 있어야 합니다.** 리졸버는 **소유자 워크스페이스**(애플리케이션 등록을 소유한 워크스페이스)에서 실행되므로, 서버 라우트 트리거는 애플리케이션에 소유자 워크스페이스가 *지정되고* — 즉, 소유자 워크스페이스가 있고 — **그리고** 그 애플리케이션이 **소유자 워크스페이스에 설치된** 때에만 동작합니다. 이 두 조건이 모두 충족되기 전에는 리졸버가 실행될 위치가 없으므로, 라우트를 디스패치할 수 없습니다. 따라서 `serverRouteTriggerSettings` 로직 함수를 노출하는 애플리케이션은 소유자 워크스페이스가 지정되어 그 워크스페이스에 설치되기 전까지는 마켓플레이스에 등록될 수 없습니다.
|
||||
</Note>
|
||||
|
||||
**Resolver 계약.** SDK의 `LogicFunctionConfig` 타입은 컴파일 타임에 이를 강제합니다. `serverRouteTriggerSettings`를 설정하는 즉시, 핸들러는 `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }`(또는 이 값을 반환하는 `Promise`)를 반환하도록 제한됩니다. `workspaceId`는 대상 함수가 설치된 워크스페이스여야 하며, 그렇지 않으면 요청은 `404`로 거부됩니다.
|
||||
**Resolver 계약.** SDK의 `LogicFunctionConfig` 타입은 컴파일 타임에 이를 강제합니다. `serverRouteTriggerSettings`를 설정하는 즉시, 핸들러는 `Response` 또는 `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }`(또는 이 둘 중 하나를 반환하는 `Promise`)을 반환하도록 제한됩니다. dispatch 경로에서 `workspaceId`는 대상 함수가 설치된 워크스페이스여야 하며, 그렇지 않으면 요청은 `404`로 거부됩니다. 두 가지 형태 중 어느 것에도 일치하지 않는 결과 — 식별자가 UUID가 아닌 경우를 포함해 —는 `502`로 거부됩니다.
|
||||
|
||||
| 필드 | 유형 | 노트 |
|
||||
| ---------------------------------------- | ---------------- | ------------------------------------------------ |
|
||||
@@ -289,7 +307,9 @@ POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniver
|
||||
위의 resolver 예제는 이미 GitHub HMAC-SHA256 플로우를 보여 줍니다. 통합하려는 공급자에 따라 헤더 이름, 다이제스트 인코딩, 그리고 서명 대상 페이로드 문자열을 조정하세요.
|
||||
|
||||
<Note>
|
||||
resolver가 반환된 직후 라우트는 `202 { queued: true }`로 응답하고, 대상 함수는 워커 큐에서 실행됩니다. 호출자는 대상 함수의 지연 시간, 결과, 실패를 전혀 관찰하지 못하며(이러한 정보는 실행 로그에 기록됩니다). 이렇게 하면 발신자 재전송으로 인해 처리 지연이 더 커지는 것을 막을 수 있으며, 이는 웹훅 수신에서 바람직한 동작입니다. 호출자가 반드시 응답 본문을 읽어야 하는 엔드포인트(Challenge 핸드셰이크, Slack 명령 등)에는 대신 `httpRouteTriggerSettings` 라우트를 사용하세요. resolver는 빠르게 유지하세요. 일부 공급자(예: Slack)는 몇 초 안에 타임아웃됩니다. resolver는 public endpoint로 접근 가능하므로, 엣지에서 rate limiting으로 보호하세요.
|
||||
resolver가 dispatch 객체를 반환하면, 라우트는 `202 { queued: true }`로 응답하고 대상 함수는 워커 큐에서 실행됩니다. 호출자는 대상 함수의 지연 시간, 결과, 실패를 전혀 관찰하지 못하며(이러한 정보는 실행 로그에 기록됩니다). 이렇게 하면 발신자 재전송으로 인해 처리 지연이 더 커지는 것을 막을 수 있으며, 이는 웹훅 수신에서 바람직한 동작입니다.
|
||||
|
||||
호출자가 동일한 요청에서 응답 본문을 반드시 읽어야 하는 경우(챌린지 핸드셰이크, 대화형 확인 등)에는, 대신 **resolver**에서 `Response`를 반환하십시오. 플랫폼은 이를 동기적으로 그대로 반환하고 큐는 건너뜁니다. 이때 헤더는 HTTP 라우트 응답과 동일한 allow-list를 통과합니다. resolver는 빠르게 유지하세요. 일부 공급자(예: Slack)는 몇 초 안에 타임아웃됩니다. resolver는 public endpoint로 접근 가능하므로, 엣지에서 rate limiting으로 보호하세요.
|
||||
</Note>
|
||||
|
||||
#### 데이터베이스 이벤트 트리거 페이로드
|
||||
|
||||
@@ -59,7 +59,7 @@ Para invocar uma função de lógica acionada por rota a partir de um componente
|
||||
* **cron**: Executa sua função em um agendamento usando uma expressão CRON.
|
||||
* **databaseEvent**: Executa em eventos do ciclo de vida de objetos do espaço de trabalho. Quando a operação do evento é `updated`, campos específicos a serem observados podem ser especificados no array `updatedFields`. Se deixar indefinido ou vazio, qualquer atualização acionará a função.
|
||||
> por exemplo, `person.updated`, `*.created`, `company.*`
|
||||
* **serverRoute**: expõe uma única rota HTTP com escopo de registro. Uma função **resolver** (declarada com `serverRouteTriggerSettings`) é executada no workspace proprietário e retorna o workspace de destino E a função de lógica de destino para a qual despachar; a plataforma confirma com `202` e executa essa função de **destino** na fila de workers. Veja [gatilho de rota de servidor](#server-route-trigger).
|
||||
* **serverRoute**: expõe uma única rota HTTP com escopo de registro. Uma função **resolver** (declarada com `serverRouteTriggerSettings`) é executada no workspace proprietário e retorna uma `Response` síncrona ou o workspace de destino E a função de lógica de destino para enfileirar; no caminho de enfileiramento, a plataforma confirma com `202` e executa esse **destino** na fila de workers. Veja [gatilho de rota de servidor](#server-route-trigger).
|
||||
|
||||
<Note>
|
||||
Você também pode executar manualmente uma função usando a CLI:
|
||||
@@ -177,13 +177,18 @@ O código de status deve ser um código de status HTTP válido (entre 100 e 599)
|
||||
|
||||
Nesse caso, o gatilho tem duas partes:
|
||||
|
||||
1. Uma função de lógica de **resolver** — declarada com `serverRouteTriggerSettings` — é executada no seu **workspace proprietário** (o workspace que é proprietário do registro da aplicação). Ela inspeciona a requisição recebida e retorna `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }`, escolhendo *tanto* o workspace de destino quanto a função de destino. O resolver é o ponto único de autorização — a URL carrega apenas o identificador do resolver. **Este é o local preferencial para verificar assinaturas de requisição**: o resolver é executado antes de qualquer efeito colateral, tem acesso ao `rawBody` original e aos headers encaminhados e pode rejeitar sem nunca tocar no alvo.
|
||||
2. Uma função de lógica de **target** — uma função de lógica regular por workspace — então é executada no workspace resolvido com o payload retornado pelo resolver (ou o payload original da requisição, se o resolver não o tiver transformado). Seu valor de retorno se torna a resposta HTTP.
|
||||
1. Uma função de lógica de **resolver** — declarada com `serverRouteTriggerSettings` — é executada no seu **workspace proprietário** (o workspace que é proprietário do registro da aplicação). Ela inspeciona a requisição recebida e retorna um dos seguintes:
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — a plataforma coloca esse destino em fila no workspace resolvido e confirma com `202 { queued: true }`, ou
|
||||
* uma `Response` de `twenty-sdk/logic-function` — a plataforma repete essa resposta HTTP **sincronamente** e **não** coloca nenhum destino em fila (use isso para handshakes de desafio, como o `url_verification` do Slack).
|
||||
|
||||
O resolver é o ponto único de autorização — a URL carrega apenas o identificador do resolver. **Este é o local preferencial para verificar assinaturas de requisição**: o resolver é executado antes de qualquer efeito colateral, tem acesso ao `rawBody` original e aos headers encaminhados e pode rejeitar sem nunca tocar no alvo.
|
||||
2. Uma função de lógica de **target** — uma função de lógica regular por workspace — então é executada no workspace resolvido com o payload retornado pelo resolver (ou o payload original da requisição, se o resolver não o tiver transformado). Seu valor de retorno **não** é observado pelo chamador HTTP quando o resolver escolheu o caminho de enfileiramento.
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -210,12 +215,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -264,7 +282,7 @@ O identificador é o `universalIdentifier` do resolver, vindo do seu manifest. R
|
||||
**O aplicativo deve ser reivindicado e instalado em seu workspace proprietário.** Como o resolvedor é executado no **workspace proprietário** (o workspace que é proprietário do registro do aplicativo), um acionador de rota de servidor só funciona depois que o aplicativo tiver sido *reivindicado* — ou seja, tiver um workspace proprietário — **e** esse aplicativo estiver **instalado no workspace proprietário**. Até que ambas as condições sejam verdadeiras, o resolvedor não tem onde ser executado, portanto a rota não pode ser despachada. Um aplicativo que expõe uma função lógica `serverRouteTriggerSettings`, portanto, não pode ser listado no marketplace até que seja reivindicado e instalado em seu workspace proprietário.
|
||||
</Note>
|
||||
|
||||
**Contrato do resolver.** O tipo `LogicFunctionConfig` do SDK aplica isso em tempo de compilação: assim que você define `serverRouteTriggerSettings`, o seu handler fica limitado a retornar `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (ou uma `Promise` disso). O `workspaceId` deve ser um workspace onde a função de destino esteja instalada, caso contrário a requisição é rejeitada com `404`.
|
||||
**Contrato do resolver.** O tipo `LogicFunctionConfig` do SDK aplica isso em tempo de compilação: assim que você define `serverRouteTriggerSettings`, o seu handler fica limitado a retornar uma `Response` ou `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (ou uma `Promise` de qualquer um deles). No caminho de dispatch, o `workspaceId` deve ser um workspace onde a função de destino esteja instalada, caso contrário a requisição é rejeitada com `404`. Um resultado que não corresponda a nenhum dos formatos — incluindo um cujos identificadores não sejam UUIDs — é rejeitado com `502`.
|
||||
|
||||
| Campo | Tipo | Notas |
|
||||
| ---------------------------------------- | ------------------- | --------------------------------------------------------------------------- |
|
||||
@@ -289,7 +307,9 @@ Para assinaturas de requisição, a maioria dos provedores assina com HMAC-SHA25
|
||||
O exemplo de resolver acima já mostra o fluxo de HMAC-SHA256 do GitHub — adapte o nome do header, a codificação do digest e a string de payload assinada de acordo com o provedor com o qual você está integrando.
|
||||
|
||||
<Note>
|
||||
A rota responde com `202 { queued: true }` logo após o retorno do resolver e o destino é executado na fila de workers — quem faz a chamada nunca observa a latência, o resultado ou as falhas do destino (esses são registrados nos logs de execução). Isso evita que reentregas do remetente amplifiquem a lentidão do processamento, que é o que você quer para a ingestão de webhooks. Para endpoints cujo chamador precisa ler o corpo da resposta (handshakes de verificação (challenge), comandos do Slack), use em vez disso uma rota `httpRouteTriggerSettings`. Mantenha o resolver rápido — alguns provedores (por exemplo, Slack) atingem timeout em poucos segundos. Como o resolver fica acessível como um endpoint público, proteja-o com rate limiting na sua borda.
|
||||
Quando o resolver retorna um objeto de dispatch, a rota responde com `202 { queued: true }` e o destino é executado na fila de workers — quem faz a chamada nunca observa a latência, o resultado ou as falhas do destino (essas são registradas nos logs de execução). Isso evita que reentregas do remetente amplifiquem a lentidão do processamento, que é o que você quer para a ingestão de webhooks.
|
||||
|
||||
Quando o chamador precisa ler o corpo da resposta na mesma requisição (handshakes de desafio, acknowledgements interativos), retorne, em vez disso, uma `Response` a partir do **resolver**. A plataforma o replica de forma síncrona e ignora a fila; seus headers passam pela mesma allow-list que as respostas de rotas HTTP. Mantenha o resolver rápido — alguns provedores (por exemplo, Slack) atingem timeout em poucos segundos. Como o resolver fica acessível como um endpoint público, proteja-o com rate limiting na sua borda.
|
||||
</Note>
|
||||
|
||||
#### Payload do gatilho de evento do banco de dados
|
||||
|
||||
@@ -59,7 +59,7 @@ Pentru a apela o funcție logică declanșată de o rută dintr-o componentă fr
|
||||
* **cron**: Rulează funcția pe un program folosind o expresie CRON.
|
||||
* **databaseEvent**: Rulează la evenimentele ciclului de viață ale obiectelor din spațiul de lucru. Când operațiunea evenimentului este `updated`, câmpurile specifice de urmărit pot fi specificate în array-ul `updatedFields`. Dacă este lăsat nedefinit sau gol, orice actualizare va declanșa funcția.
|
||||
> de ex. `person.updated`, `*.created`, `company.*`
|
||||
* **serverRoute**: Expune o singură rută HTTP la nivelul înregistrării. O funcție de tip **resolver** (declarată cu `serverRouteTriggerSettings`) rulează în workspace-ul proprietar și returnează atât workspace-ul țintă, cât și funcția logică țintă către care se face trimiterea; platforma confirmă cu `202` și rulează acea funcție **țintă** în coada de worker. Consultați [declanșatorul de rută de server](#server-route-trigger).
|
||||
* **serverRoute**: Expune o singură rută HTTP la nivelul înregistrării. O funcție de tip **resolver** (declarată cu `serverRouteTriggerSettings`) rulează în workspace-ul proprietar și fie returnează un `Response` sincron, fie workspace-ul țintă ȘI funcția logică de pus în coadă; pe ramura de punere în coadă, platforma confirmă cu `202` și rulează acea **țintă** în coada worker-ului. Consultați [declanșatorul de rută de server](#server-route-trigger).
|
||||
|
||||
<Note>
|
||||
Puteți, de asemenea, să executați manual o funcție folosind CLI:
|
||||
@@ -178,13 +178,18 @@ Codul de stare trebuie să fie un cod de stare HTTP valid (între 100 și 599).
|
||||
|
||||
Declanșatorul are două părți:
|
||||
|
||||
1. O funcție logică de **resolver** — declarată cu `serverRouteTriggerSettings` — rulează în **workspace-ul deținător** (workspace-ul care deține înregistrarea aplicației). Aceasta inspectează cererea de intrare și returnează `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }`, alegând *atât* workspace-ul țintă, cât și funcția țintă. Resolver-ul este singurul punct de autorizare — URL-ul conține doar identificatorul resolver-ului. **Acesta este locul preferat pentru a verifica semnăturile cererilor**: resolver-ul rulează înaintea oricărui efect secundar, are acces la `rawBody` original și la headerele redirecționate și poate respinge fără a atinge vreodată ținta.
|
||||
2. O funcție logică **țintă** — o funcție logică obișnuită per-workspace — rulează apoi în workspace-ul rezolvat cu payload-ul returnat de resolver (sau payload-ul original al cererii dacă resolver-ul nu l-a transformat). Valoarea returnată devine răspunsul HTTP.
|
||||
1. O funcție logică de **resolver** — declarată cu `serverRouteTriggerSettings` — rulează în **workspace-ul deținător** (workspace-ul care deține înregistrarea aplicației). Inspectează cererea primită și returnează fie:
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — platforma pune în coadă acea țintă în workspace-ul rezolvat și confirmă cu `202 { queued: true }`, sau
|
||||
* un `Response` de la `twenty-sdk/logic-function` — platforma transmite mai departe acel răspuns HTTP **sincron** și **nu** pune în coadă nicio țintă (folosiți acest lucru pentru handshake-uri de tip challenge, cum ar fi Slack `url_verification`).
|
||||
|
||||
Resolver-ul este singurul punct de autorizare — URL-ul conține doar identificatorul resolver-ului. **Acesta este locul preferat pentru a verifica semnăturile cererilor**: resolver-ul rulează înaintea oricărui efect secundar, are acces la `rawBody` original și la headerele redirecționate și poate respinge fără a atinge vreodată ținta.
|
||||
2. O funcție logică **țintă** — o funcție logică obișnuită per-workspace — rulează apoi în workspace-ul rezolvat cu payload-ul returnat de resolver (sau payload-ul original al cererii dacă resolver-ul nu l-a transformat). Valoarea de returnare **nu** este observată de apelantul HTTP atunci când resolver-ul a ales calea de punere în coadă.
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -211,12 +216,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -265,7 +283,7 @@ Identificatorul este `universalIdentifier` al resolver-ului din manifestul dvs.
|
||||
**Aplicația trebuie revendicată și instalată în spațiul de lucru al proprietarului.** Deoarece resolverul rulează în **spațiul de lucru al proprietarului** (spațiul de lucru care deține înregistrarea aplicației), un declanșator de rută de server funcționează doar după ce aplicația a fost *revendicată* — adică are un spațiu de lucru al proprietarului — **și** acea aplicație este **instalată în spațiul de lucru al proprietarului**. Până când ambele condiții sunt adevărate, resolverul nu are unde să ruleze, astfel ruta nu poate fi apelată. O aplicație care expune o funcție logică `serverRouteTriggerSettings` nu poate fi, așadar, listată în marketplace până când nu este revendicată și instalată în spațiul de lucru al proprietarului.
|
||||
</Note>
|
||||
|
||||
**Contractul resolver-ului.** Tipul `LogicFunctionConfig` din SDK impune acest lucru la compilare: de îndată ce setați `serverRouteTriggerSettings`, handler-ul este constrâns să returneze `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (sau un `Promise` al acestuia). `workspaceId` trebuie să fie un workspace în care funcția țintă este instalată, altfel cererea este respinsă cu `404`.
|
||||
**Contractul resolver-ului.** Tipul `LogicFunctionConfig` din SDK impune acest lucru la compilare: de îndată ce setați `serverRouteTriggerSettings`, handler-ul este constrâns să returneze fie un `Response`, fie `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (sau un `Promise` al uneia dintre acestea). Pe calea de trimitere, `workspaceId` trebuie să fie un workspace în care funcția țintă este instalată, altfel cererea este respinsă cu `404`. Un rezultat care nu corespunde niciuneia dintre forme — inclusiv unul ale cărui identificatoare nu sunt UUID-uri — este respins cu `502`.
|
||||
|
||||
| Câmp | Tip | Notițe |
|
||||
| ---------------------------------------- | ------------------- | --------------------------------------------------------------------------------- |
|
||||
@@ -290,7 +308,9 @@ Pentru semnăturile cererilor, majoritatea furnizorilor semnează cu HMAC-SHA256
|
||||
Exemplul de resolver de mai sus arată deja fluxul GitHub HMAC-SHA256 — adaptați numele headerului, codificarea digestului și șirul de payload semnat în funcție de furnizorul cu care vă integrați.
|
||||
|
||||
<Note>
|
||||
Ruta răspunde cu `202 { queued: true }` imediat după ce resolver-ul își încheie execuția, iar ținta rulează în coada worker-ului — apelantul nu observă niciodată latența, rezultatul sau erorile țintei (acestea sunt înregistrate în jurnalele de execuție). Acest lucru împiedică retrimiterile expeditorului să amplifice încetinirile procesării, ceea ce este de dorit pentru ingestia de webhook-uri. Pentru endpoint-urile al căror apelant trebuie să citească corpul răspunsului (challenge handshakes, comenzi Slack), folosiți în schimb o rută `httpRouteTriggerSettings`. Mențineți resolver-ul rapid — unii furnizori (de ex. Slack) expiră după câteva secunde. Deoarece resolver-ul este accesibil ca endpoint public, protejați-l cu limitare de rată la marginea infrastructurii dvs.
|
||||
Când resolver-ul returnează un obiect de dispatch, ruta răspunde cu `202 { queued: true }`, iar ținta rulează în coada worker-ului — apelantul nu observă niciodată latența, rezultatul sau erorile țintei (acestea sunt înregistrate în jurnalele de execuție). Acest lucru împiedică retrimiterile expeditorului să amplifice încetinirile procesării, ceea ce este de dorit pentru ingestia de webhook-uri.
|
||||
|
||||
Atunci când apelantul trebuie să citească corpul răspunsului în cadrul aceleiași cereri (challenge handshakes, confirmări interactive), returnați în schimb un `Response` din **resolver**. Platforma îl reflectă sincron și omite coada; header-ele acestuia trec prin aceeași listă de permisiuni ca răspunsurile rutelor HTTP. Mențineți resolver-ul rapid — unii furnizori (de ex. Slack) expiră după câteva secunde. Deoarece resolver-ul este accesibil ca endpoint public, protejați-l cu limitare de rată la marginea infrastructurii dvs.
|
||||
</Note>
|
||||
|
||||
#### Payload-ul declanșatorului de eveniment al bazei de date
|
||||
|
||||
@@ -59,7 +59,7 @@ Arayüzsüz bir ön uç bileşeninden rota tarafından tetiklenen mantık fonksi
|
||||
* **cron**: Bir CRON ifadesi kullanarak işlevinizi bir zamanlamayla çalıştırır.
|
||||
* **databaseEvent**: Çalışma alanı nesnesi yaşam döngüsü olaylarında çalışır. Olay işlemi `updated` olduğunda, dinlenecek belirli alanlar `updatedFields` dizisinde belirtilebilir. Tanımsız veya boş bırakılırsa, herhangi bir güncelleme işlevi tetikler.
|
||||
> örn. `person.updated`, `*.created`, `company.*`
|
||||
* **serverRoute**: Kayıt kapsamına sahip tek bir HTTP rotasını erişime açar. Bir **resolver** fonksiyonu (`serverRouteTriggerSettings` ile tanımlanır) sahip çalışma alanında çalışır ve hedef çalışma alanını VE yönlendirilecek hedef mantık fonksiyonunu döndürür; platform `202` ile onay verir ve bu **hedef** fonksiyonu worker kuyruğunda çalıştırır. [Sunucu rota tetikleyicisine](#server-route-trigger) bakın.
|
||||
* **serverRoute**: Kayıt kapsamına sahip tek bir HTTP rotasını erişime açar. Bir **resolver** fonksiyonu (`serverRouteTriggerSettings` ile tanımlanır) sahip çalışma alanında çalışır ve ya senkron bir `Response` ya da kuyruğa eklenecek hedef çalışma alanını VE mantık fonksiyonunu döndürür; kuyruğa ekleme yolunda platform `202` ile onay verir ve bu **hedefi** worker kuyruğunda çalıştırır. [Sunucu rota tetikleyicisine](#server-route-trigger) bakın.
|
||||
|
||||
<Note>
|
||||
Bir işlevi CLI kullanarak manuel olarak da çalıştırabilirsiniz:
|
||||
@@ -178,13 +178,18 @@ Durum kodu geçerli bir HTTP durum kodu olmalıdır (100 ile 599 arasında). Yan
|
||||
|
||||
Tetikleyicinin iki parçası vardır:
|
||||
|
||||
1. Bir **resolver** mantık fonksiyonu — `serverRouteTriggerSettings` ile tanımlanır — **sahip çalışma alanınızda** (uygulama kaydına sahip olan çalışma alanı) çalışır. Gelen isteği inceler ve `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` döndürerek hem hedef çalışma alanını hem de hedef fonksiyonu seçer. Resolver, yetkilendirmenin tek noktasıdır — URL yalnızca resolver'ın tanımlayıcısını taşır. **İstek imzalarını doğrulamak için tercih edilen yer burasıdır**: resolver, herhangi bir yan etkiden önce çalışır, orijinal `rawBody` ve iletilen başlıklara erişebilir ve hedefe hiç dokunmadan isteği reddedebilir.
|
||||
2. Ardından bir **hedef** mantık fonksiyonu — her çalışma alanı için normal bir mantık fonksiyonu — resolver tarafından döndürülen payload ile (veya resolver onu dönüştürmediyse orijinal istek payload'ı ile) çözümlenen çalışma alanında çalışır. Döndürdüğü değer HTTP yanıtı olur.
|
||||
1. Bir **resolver** mantık fonksiyonu — `serverRouteTriggerSettings` ile tanımlanır — **sahip çalışma alanınızda** (uygulama kaydına sahip olan çalışma alanı) çalışır. Gelen isteği inceler ve şu ikisinden birini döndürür:
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — platform bu hedefi çözümlenen çalışma alanında kuyruğa ekler ve `202 { queued: true }` ile onay verir, veya
|
||||
* `twenty-sdk/logic-function` içinden bir `Response` — platform bu HTTP yanıtını **senkron** şekilde yansıtır ve **hiçbir** hedefi kuyruğa eklemez (bunu Slack `url_verification` gibi doğrulama el sıkışmaları için kullanın).
|
||||
|
||||
Resolver, yetkilendirmenin tek noktasıdır — URL yalnızca resolver'ın tanımlayıcısını taşır. **İstek imzalarını doğrulamak için tercih edilen yer burasıdır**: resolver, herhangi bir yan etkiden önce çalışır, orijinal `rawBody` ve iletilen başlıklara erişebilir ve hedefe hiç dokunmadan isteği reddedebilir.
|
||||
2. Ardından bir **hedef** mantık fonksiyonu — her çalışma alanı için normal bir mantık fonksiyonu — resolver tarafından döndürülen payload ile (veya resolver onu dönüştürmediyse orijinal istek payload'ı ile) çözümlenen çalışma alanında çalışır. Çözümleyici kuyruğa alma yolunu seçtiğinde, döndürdüğü değer HTTP çağrıcısı tarafından **gözlemlenmez**.
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -211,12 +216,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -265,7 +283,7 @@ Tanımlayıcı, manifest'inizdeki resolver'ın `universalIdentifier` değeridir.
|
||||
**Uygulama, sahip çalışma alanında sahiplenilip kurulmalıdır.** Çözücü **sahip çalışma alanında** (uygulama kaydına sahip olan çalışma alanı) çalıştığı için, bir sunucu rota tetikleyicisi yalnızca uygulama *sahiplenildikten* — yani bir sahip çalışma alanına sahiptir — **ve** o uygulama **sahip çalışma alanına kurulduğunda** çalışır. Her ikisi de doğru olana kadar çözücünün çalışacağı bir yer yoktur, bu yüzden rota çalıştırılamaz. Bu nedenle, `serverRouteTriggerSettings` mantık işlevini sunan bir uygulama, sahip çalışma alanında sahiplenilip kurulana kadar pazaryerinde listelenemez.
|
||||
</Note>
|
||||
|
||||
**Resolver sözleşmesi.** SDK'nin `LogicFunctionConfig` türü bunu derleme zamanında zorunlu kılar: `serverRouteTriggerSettings`'i ayarladığınız anda, işleyicinizin `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (veya bunun bir `Promise`'i) döndürmesi gerekir. `workspaceId`, hedef fonksiyonun kurulu olduğu bir çalışma alanı olmalıdır, aksi takdirde istek `404` ile reddedilir.
|
||||
**Resolver sözleşmesi.** SDK'nin `LogicFunctionConfig` türü bunu derleme zamanında zorunlu kılar: `serverRouteTriggerSettings`'i ayarladığınız anda, işleyicinizin ya bir `Response` ya da `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }` (veya bunlardan birinin `Promise`'i) döndürmesi gerekir. Dispatch yolunda, `workspaceId` hedef fonksiyonun kurulu olduğu bir çalışma alanı olmalıdır, aksi takdirde istek `404` ile reddedilir. Her iki şekille de eşleşmeyen bir sonuç — tanımlayıcıları UUID olmayanlar da dahil — `502` ile reddedilir.
|
||||
|
||||
| Alan | Tür | Notlar |
|
||||
| ---------------------------------------- | ----------------------- | -------------------------------------------------------------------------------- |
|
||||
@@ -290,7 +308,9 @@ Tanımlayıcı, manifest'inizdeki resolver'ın `universalIdentifier` değeridir.
|
||||
Yukarıdaki resolver örneği GitHub HMAC-SHA256 akışını zaten göstermektedir — tümleştirdiğiniz sağlayıcıya göre başlık adını, özet kodlamasını ve imzalanan payload dizesini uyarlayın.
|
||||
|
||||
<Note>
|
||||
Yol, resolver döner dönmez `202 { queued: true }` yanıtını verir ve hedef worker kuyruğunda çalıştırılır — çağıran taraf hedefin gecikmesini, sonucunu veya hatalarını asla gözlemlemez (bunlar yürütme günlüklerinde kaydedilir). Bu, göndericinin yeniden gönderimlerinin işlem yavaşlamalarını artırmasını engeller; bu da webhook alımı için istediğiniz şeydir. Çağıranın yanıt gövdesini okuması gereken uç noktalar için (challenge el sıkışmaları, Slack komutları) bunun yerine bir `httpRouteTriggerSettings` yolu kullanın. Resolver’ı hızlı tutun — bazı sağlayıcılar (örn. Slack) birkaç saniye içinde zaman aşımına uğrar. Resolver herkese açık bir uç nokta olarak erişilebilir olduğundan, onu edge'inizde hız sınırlama ile koruyun.
|
||||
Resolver bir dispatch nesnesi döndürdüğünde, yol `202 { queued: true }` yanıtını verir ve hedef worker kuyruğunda çalıştırılır — çağıran taraf hedefin gecikmesini, sonucunu veya hatalarını asla gözlemlemez (bunlar yürütme günlüklerinde kaydedilir). Bu, göndericinin yeniden gönderimlerinin işlem yavaşlamalarını artırmasını engeller; bu da webhook alımı için istediğiniz şeydir.
|
||||
|
||||
Çağıran tarafın yanıt gövdesini aynı istek üzerinde okuması gerektiğinde (challenge el sıkışmaları, etkileşimli onaylar), bunun yerine **resolver**'dan bir `Response` döndürün. Platform bunu eşzamanlı olarak geri yansıtır ve kuyruğu atlar; başlıkları, HTTP route yanıtlarıyla aynı izin listesi üzerinden geçirilir. Resolver’ı hızlı tutun — bazı sağlayıcılar (örn. Slack) birkaç saniye içinde zaman aşımına uğrar. Resolver herkese açık bir uç nokta olarak erişilebilir olduğundan, onu edge'inizde hız sınırlama ile koruyun.
|
||||
</Note>
|
||||
|
||||
#### Veritabanı olay tetikleyicisi yükü
|
||||
|
||||
@@ -59,7 +59,7 @@ export default defineLogicFunction({
|
||||
* **cron**:使用 CRON 表达式按计划运行你的函数。
|
||||
* **databaseEvent**:在工作区对象生命周期事件上运行。 当事件操作为 `updated` 时,可以在 `updatedFields` 数组中指定要监听的特定字段。 如果未定义或为空,任何更新都会触发该函数。
|
||||
> 例如 `person.updated`、`*.created`、`company.*`
|
||||
* **serverRoute**:公开一个注册作用域的单一 HTTP 路由。 一个在所有者工作区中运行的 **resolver** 函数(使用 `serverRouteTriggerSettings` 声明)会返回目标工作区以及要分发到的目标逻辑函数;平台会发送 `202` 确认并在工作队列上运行该**目标**函数。 参见 [服务端路由触发器](#server-route-trigger)。
|
||||
* **serverRoute**:公开一个注册作用域的单一 HTTP 路由。 一个在所有者工作区中运行的 **resolver** 函数(使用 `serverRouteTriggerSettings` 声明)要么返回一个同步的 `Response`,要么返回目标工作区以及要入队的逻辑函数;在入队路径下,平台会发送 `202` 确认并在工作队列上运行该**目标**。 参见 [服务端路由触发器](#server-route-trigger)。
|
||||
|
||||
<Note>
|
||||
你也可以使用 CLI 手动执行函数:
|
||||
@@ -178,13 +178,18 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
触发器由两部分组成:
|
||||
|
||||
1. 一个在 **所有者工作区**(拥有应用注册的工作区)中运行的 **resolver** 逻辑函数——使用 `serverRouteTriggerSettings` 声明。 它检查传入请求并返回 `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }`,同时选择目标工作区和目标函数。 resolver 是唯一的授权点——URL 只携带 resolver 的标识符。 **这里是验证请求签名的首选位置**:resolver 在任何副作用之前运行,可以访问原始的 `rawBody` 和转发的请求头,并且可以在不触及目标的情况下直接拒绝请求。
|
||||
2. 一个 **target** 逻辑函数——一个常规的、按工作区划分的逻辑函数——随后在解析得到的工作区中运行,并使用 resolver 返回的负载(如果 resolver 未对其进行转换,则使用原始请求负载)。 它的返回值将成为 HTTP 响应。
|
||||
1. 一个在 **所有者工作区**(拥有应用注册的工作区)中运行的 **resolver** 逻辑函数——使用 `serverRouteTriggerSettings` 声明。 它会检查传入请求并返回以下两者之一:
|
||||
|
||||
* `{ workspaceId, targetLogicFunctionUniversalIdentifier, payload? }` — 平台会在解析得到的工作区中将该目标入队,并返回 `202 { queued: true }` 确认,或者
|
||||
* 来自 `twenty-sdk/logic-function` 的 `Response` — 平台会**同步**回显该 HTTP 响应,并且**不会**入队目标(将其用于诸如 Slack `url_verification` 之类的挑战握手)。
|
||||
|
||||
resolver 是唯一的授权点——URL 只携带 resolver 的标识符。 **这里是验证请求签名的首选位置**:resolver 在任何副作用之前运行,可以访问原始的 `rawBody` 和转发的请求头,并且可以在不触及目标的情况下直接拒绝请求。
|
||||
2. 一个 **target** 逻辑函数——一个常规的、按工作区划分的逻辑函数——随后在解析得到的工作区中运行,并使用 resolver 返回的负载(如果 resolver 未对其进行转换,则使用原始请求负载)。 当 resolver 选择入队(enqueue)路径时,其返回值 **不会** 被 HTTP 调用方观察到。
|
||||
|
||||
```ts src/logic-functions/resolve-server-route.logic-function.ts
|
||||
import { createHmac, timingSafeEqual } from 'crypto';
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import type { RoutePayload } from 'twenty-sdk/logic-function';
|
||||
import { Response, type RoutePayload } from 'twenty-sdk/logic-function';
|
||||
|
||||
// Runs in the owner workspace. Verifies the request signature, picks
|
||||
// which target function should handle the event, and returns the
|
||||
@@ -211,12 +216,25 @@ const handler = async (event: RoutePayload) => {
|
||||
}
|
||||
|
||||
const body = (event.body ?? {}) as {
|
||||
challenge?: string;
|
||||
metadata?: { twentyWorkspaceId?: string };
|
||||
type?: string;
|
||||
};
|
||||
|
||||
// Handshakes must be answered on this same response, so reply from the
|
||||
// resolver instead of returning a dispatch target.
|
||||
if (body.type === 'url_verification') {
|
||||
return new Response({ challenge: body.challenge });
|
||||
}
|
||||
|
||||
const workspaceId = body.metadata?.twentyWorkspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new Error('event is not linked to a workspace');
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceId: body.metadata?.twentyWorkspaceId ?? '',
|
||||
workspaceId,
|
||||
// Route different event types to different target functions.
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
body.type === 'invoice.paid'
|
||||
@@ -265,7 +283,7 @@ POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniver
|
||||
**应用程序必须在其所属工作区中被认领并安装。** 由于 resolver 在**所属工作区**中运行(即拥有应用程序注册的工作区),服务器路由触发器只有在应用程序已被*认领*——也就是说它已有一个所属工作区——**并且**该应用程序**已安装在所属工作区**之后才会生效。 在这两个条件都满足之前,resolver 无处可运行,因此无法分发该路由。 因此,暴露 `serverRouteTriggerSettings` 逻辑函数的应用程序在被认领并安装到其所属工作区之前,不能在 marketplace 中列出。
|
||||
</Note>
|
||||
|
||||
**Resolver 合约。** SDK 的 `LogicFunctionConfig` 类型在编译时强制执行这一点:一旦你设置了 `serverRouteTriggerSettings`,你的处理程序就被限制为返回 `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }`(或其 `Promise`)。 `workspaceId` 必须是已安装目标函数的工作区,否则请求会以 `404` 被拒绝。
|
||||
**Resolver 合约。** SDK 的 `LogicFunctionConfig` 类型在编译时强制执行这一点:一旦你设置了 `serverRouteTriggerSettings`,你的处理程序就被限制为返回 `Response`,或 `{ workspaceId: string; targetLogicFunctionUniversalIdentifier: string; payload?: object }`(或它们之一的 `Promise`)。 在分发(dispatch)路径上,`workspaceId` 必须是已安装目标函数的工作区,否则请求会以 `404` 被拒绝。 如果结果不符合这两种结构中的任何一种——包括标识符不是 UUID 的情况——将会以 `502` 被拒绝。
|
||||
|
||||
| 字段 | 类型 | 备注 |
|
||||
| ---------------------------------------- | ------------ | -------------------------------------- |
|
||||
@@ -290,7 +308,9 @@ POST https://your-twenty-server.com/webhooks/server/:resolverLogicFunctionUniver
|
||||
上面的 resolver 示例已经展示了 GitHub 的 HMAC-SHA256 流程——请根据你要集成的服务商,调整请求头名称、摘要编码方式以及被签名的负载字符串。
|
||||
|
||||
<Note>
|
||||
在 resolver 返回之后,该路由会立即响应 `202 { queued: true }`,并在工作队列上运行目标函数——调用方不会感知目标函数的延迟、结果或失败(这些都会记录在执行日志中)。 这可以防止发送方的重新投递放大处理变慢的问题,而这正是你在处理 webhook 摄取时所需要的。 对于调用方必须读取响应正文的端点(挑战握手、Slack 命令),请改用 `httpRouteTriggerSettings` 路由。 保持 resolver 足够快速——某些服务商(例如 Slack)会在几秒内超时。 由于 resolver 可以作为公共端点访问,请在边缘(edge)对其进行速率限制保护。
|
||||
当 resolver 返回一个分发对象时,该路由会响应 `202 { queued: true }`,并在工作队列上运行目标函数——调用方不会感知目标函数的延迟、结果或失败(这些都会记录在执行日志中)。 这可以防止发送方的重新投递放大处理变慢的问题,而这正是你在处理 webhook 摄取时所需要的。
|
||||
|
||||
当调用方必须在同一个请求中读取响应体(挑战握手、交互式确认)时,请改为从 **resolver** 返回一个 `Response`。 平台会同步回显该响应并跳过队列;其响应头会通过与 HTTP 路由响应相同的允许列表进行过滤。 保持 resolver 足够快速——某些服务商(例如 Slack)会在几秒内超时。 由于 resolver 可以作为公共端点访问,请在边缘(edge)对其进行速率限制保护。
|
||||
</Note>
|
||||
|
||||
#### 数据库事件触发器有效负载
|
||||
|
||||
Reference in New Issue
Block a user