From 41c32a04e40852981fa61918e2458f1e2146c7bb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:17:44 +0200 Subject: [PATCH] i18n - docs translations (#23162) Created by Github action Co-authored-by: github-actions --- .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + .../extend/apps/logic/key-value-store.mdx | 194 +++++------------- .../developers/extend/apps/logic/overview.mdx | 3 + .../extend/apps/operations/publishing.mdx | 11 + 36 files changed, 792 insertions(+), 1704 deletions(-) diff --git a/packages/twenty-docs/l/ar/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/ar/developers/extend/apps/logic/key-value-store.mdx index 5c9ff114aa..9f8cf6500a 100644 --- a/packages/twenty-docs/l/ar/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/ar/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: مخزن المفاتيح-القيم -description: احتفظ بالنتائج الوسيطة، وخزّن البيانات مؤقتًا، وشارك الحالة عبر تشغيلات دوال المنطق باستخدام كائن بسيط من نوع مفاتيح-قيم. +description: احتفظ بالنتائج الوسيطة، وخزّن البيانات مؤقتًا، وشارك الحالة عبر تشغيلات دوال المنطق باستخدام مخزن مفاتيح-قيم مدمج على مستوى التطبيق. icon: database --- -تعمل دوال المنطق داخل عمليات Node.js معزولة وقصيرة العمر — بمجرد انتهاء التشغيل، لا يظل أي شيء محفوظًا في الذاكرة. عندما تحتاج إلى **تذكر شيء ما بين عمليات التشغيل** (تخزين مؤقت لاستجابة واجهة برمجة تطبيقات مكلفة، أو تخزين مؤشر لمزامنات تزايدية، أو تقليل التكرار في العمل، أو تمرير الحالة من دالة إلى أخرى)، احتفظ به في قاعدة بيانات مساحة العمل. +تعمل دوال المنطق داخل عمليات Node.js معزولة وقصيرة العمر — بمجرد انتهاء التشغيل، لا يظل أي شيء محفوظًا في الذاكرة. عندما تحتاج إلى تذكّر شيءٍ ما بين عمليات التشغيل (تخزين مؤقت لاستجابة واجهة برمجة تطبيقات مكلفة، أو تخزين مؤشر لمزامنات تزايدية، أو تقليل تكرار العمل، أو تمرير الحالة من دالة إلى أخرى)، احتفظ به في مخزن مفاتيح-قيم مدمج. -لا تحتاج إلى بُنية تخزين أولية مخصصة لهذا: كائن تقني صغير يحتوي على حقل `key` وحقل `value` يمنحك مخزن مفاتيح-قيم دائم، محدود النطاق في مساحة العمل، ويمكن الاستعلام عنه من خلال نفس [عميل واجهة برمجة التطبيقات المُنمَّط](/l/ar/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) الذي تستخدمه بالفعل للسجلات. +يحصل كل تطبيق على مساحة أسماء معزولة خاصة به: تتم فهرسة الإدخالات بحسب التطبيق المصادق عليه، لذا لا يمكن أن تتصادم مفاتيحك مع مفاتيح تطبيق آخر — ولا يمكن لتطبيق آخر قراءتها. ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## عرّف كائن المخزن +## جلب، تعيين، حذف -عرِّف كائنًا مخصصًا بحقلين — `key` (حقل `TEXT` فريد) و `value` (حقل `RAW_JSON` حتى تتمكن من تخزين أي حمولة قابلة للتسلسل إلى JSON). راجع [Objects](/l/ar/developers/extend/apps/data/objects) للحصول على مرجع `defineObject` الكامل. +استورد `kv` من `twenty-sdk/logic-function`. يمكن أن تكون القيم أي حمولة قابلة للتسلسل إلى JSON. -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## النطاقات + +لكل إدخال نطاق (scope)، يتم تمريره كخيار في كل استدعاء. القيمة الافتراضية هي `WORKSPACE`. + +* **`WORKSPACE`** (الافتراضي) — يكون الإدخال خاصًا بتثبيت مساحة العمل الحالية لتطبيقك. كل مساحة عمل تقوم بتثبيت التطبيق تحصل على مجموعة مستقلة خاصة بها من المفاتيح. هذا هو الخيار المناسب لبيانات التخزين المؤقت، والمؤشرات (cursors)، والحالة الخاصة بكل مساحة عمل. +* **`SERVER`** — تتم مشاركة الإدخال عبر **كل عملية تثبيت** لتطبيقك على الخادم. تتصرف إدخالات الخادم مثل **مطالبات (claims)**: تكون القيمة المخزّنة دائمًا هي workspaceId لمساحة العمل التي طالبت بالمفتاح (احذف `value` في `set` للمطالبة بالمفتاح لصالح مساحة العمل الحالية)، ولا يمكن سوى لتلك المساحة أن تعيد الكتابة فوقه أو تحذفه. يمكن لأي تثبيت قراءة الإدخال. + +توجد مطالبات الخادم من أجل التوجيه عبر مساحات العمل (cross-workspace routing). يعمل [محلّل مسار الخادم](/l/ar/developers/extend/apps/logic/logic-functions#server-route-trigger) في مساحة عمل مالك تسجيل التطبيق، ولكن عادةً لا يحمل خطاف الويب الوارد سوى معرّف حساب خارجي — وليس معرّف مساحة العمل في Twenty. اجعل كل مساحة عمل تطالب بالمعرّف الخارجي الخاص بها في وقت الاتصال، ثم قم بحلّه في المسار (route): + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### فرض تفرد المفتاح - -أضف **فهرسًا فريدًا** على `key` حتى لا يمكن أبدًا أن يكون لنفس المفتاح صفّان. هذه هي البنية الأولية الموصى بها لفرض التفرد — راجع [Data → Unique indexes](/l/ar/developers/extend/apps/data/overview#unique-indexes). - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## القراءة والكتابة من دالة منطقية - -قم بتغليف الكائن بعدد قليل من الأدوات المساعدة الصغيرة بحيث يَظهر باقي الشفرة وكأنه واجهة برمجة تطبيقات لمخزن مفاتيح-قيم — `get` و `set` و `del`. تستخدم هذه الأدوات [`CoreApiClient`](/l/ar/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk)، الذي يتم توليده من مخطط مساحة العمل الخاصة بك ويكون مُنمَّطًا بالكامل مقابل كائن `kvStore`. - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -يحمي الفهرس الفريد من التكرارات، لكن عمليتي تشغيل تكتبان **نفس المفتاح الجديد** في اللحظة نفسها يمكن أن تتسابقا بين عملية البحث والإنشاء. اعتبر أن عملية الإنشاء التي تفشل بسبب قيد التفرد تعني "أن طرفًا آخر سبقك" — التقط هذا الخطأ وأعد القراءة، أو أعد المحاولة كعملية تحديث. - +نظرًا إلى أنّه لا يمكن المطالبة بمفتاح خادم إلا لصالح مساحة عمل المتصل نفسه ولا يمكن أبدًا الكتابة فوقه من قِبَل مساحة أخرى، فلن تتمكّن أي مساحة عمل من اختطاف تعيين (mapping) يخص مساحة عمل أخرى. يصدر `kv.set` استثناءً (throws) عندما يكون المفتاح مُطالَبًا به مسبقًا من قِبَل مساحة عمل أخرى. ## استخدمه: خزّن استدعاء مكلفًا في الذاكرة المؤقتة @@ -155,15 +59,15 @@ export const del = async (key: string): Promise => { ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## أنماط ونصائح -* **مساحات الأسماء.** أضف بادئة إلى المفاتيح للحفاظ على فصل الاهتمامات المختلفة ولتسهيل عمليات البحث المجمّعة — `sync-cursor:linear` و `cache:exchange-rate:USD:EUR` و `lock:nightly-report`. رشِّح باستخدام `key: { like: 'cache:%' }` لسرد أو مسح مساحة اسم كاملة. -* **انتهاء الصلاحية (TTL).** لا يحتوي المخزن على آلية انتهاء صلاحية مدمجة. قم بتخزين طابع زمني داخل `value` (كما في مثال التخزين المؤقت) وتحقق منه عند القراءة، أو أضف حقل `DATE_TIME` وقم دوريًا بمسح الصفوف القديمة من [دالة يتم تشغيلها بواسطة cron](/l/ar/developers/extend/apps/logic/logic-functions). -* **ما الذي يتم تخزينه.** يمكن لـ `RAW_JSON` أن يحتوي على أي قيمة قابلة للتسلسل إلى JSON — أرقام، سلاسل نصية، مصفوفات، كائنات. حافظ على صِغر الإدخالات؛ فهذا مخصّص للتنسيق والتخزين المؤقت، وليس للملفات الكبيرة أو الكتل الثنائية الضخمة. بالنسبة للملفات، استخدم حقل `FILES` ودالة [`uploadFile`](/l/ar/developers/extend/apps/logic/logic-functions#uploading-files). +* **مساحات الأسماء.** أضف بادئة إلى المفاتيح للحفاظ على فصل الاهتمامات المختلفة — `sync-cursor:linear` و `cache:exchange-rate:USD:EUR` و `lock:nightly-report`. +* **انتهاء الصلاحية (TTL).** لا يحتوي المخزن على آلية انتهاء صلاحية مدمجة. قم بتخزين طابع زمني داخل القيمة (كما في مثال التخزين المؤقت) وتحقق منه عند القراءة، أو امسح دوريًا المفاتيح القديمة من خلال [دالة يتم تشغيلها بواسطة cron](/l/ar/developers/extend/apps/logic/logic-functions). +* **ما الذي يتم تخزينه.** أي قيمة قابلة للتسلسل إلى JSON — أرقام، سلاسل نصية، مصفوفات، كائنات. حافظ على صِغر الإدخالات؛ فهذا مخصّص للتنسيق والتخزين المؤقت، وليس للملفات الكبيرة أو الكتل الثنائية الضخمة. بالنسبة للملفات، استخدم حقل `FILES` ودالة [`uploadFile`](/l/ar/developers/extend/apps/logic/logic-functions#uploading-files). +* **إمكانية الرؤية.** تعيش الإدخالات في قاعدة بيانات المثيل (instance)، وليس كسجلات مساحة عمل — فهي لا تظهر مطلقًا في واجهة مستخدم مساحة العمل، وليست جزءًا من نموذج بيانات تطبيقك، ولا تحتاج إلى أذونات أدوار أو كائنات. + +## بديل: كائن مخزن قابل للاستعلام + +المخزن المدمج غامض عمدًا: الإدخالات ليست سجلات، لذلك لا يمكنك استعراضها في واجهة المستخدم، أو ربطها بكائنات أخرى، أو تصفيتها باستعلامات السجلات. عندما تحتاج إلى أيٍّ من ذلك — مثل سجل مزامنة مرئي، أو حالة لكل سجل — عرِّف بدلًا من ذلك كائنًا تقنيًا صغيرًا يحتوي على حقل `key` فريد وحقل `value` من نوع `RAW_JSON`، واستعلم عنه عبر عميل واجهة برمجة التطبيقات محدد الأنواع (typed API client). اطلع على [Objects](/l/ar/developers/extend/apps/data/objects) للرجوع إلى `defineObject` وعلى [Data → Unique indexes](/l/ar/developers/extend/apps/data/overview#unique-indexes) لفرض تفرّد المفاتيح. + +* **تحديد النطاق إلى سجل.** أضف [علاقة](/l/ar/developers/extend/apps/data/relations) من كائن المخزن إلى الكائن المستهدف بدلًا من ترميز المعرّف داخل المفتاح. * **الرؤية والصلاحيات.** تعيش الصفوف في قاعدة بيانات مساحة العمل مثل أي سجل آخر، لذا يمكن الاستعلام عنها عبر واجهة برمجة التطبيقات وتلتزم [بدور](/l/ar/developers/extend/apps/config/roles) التطبيق لديك. لإبقاء المخزن خارج واجهة المستخدم الرئيسية، اتركه خارج [قائمة التنقل](/l/ar/developers/extend/apps/layout/navigation-menu-items). -* **تحديد النطاق على سجل معيّن.** هل تحتاج إلى حالة لكل سجل بدلًا من مفاتيح عالمية؟ أضف [علاقة](/l/ar/developers/extend/apps/data/relations) من كائن المخزن إلى الكائن المستهدف بدلًا من ترميز المعرّف داخل المفتاح. -هذا عُرف وليس ميزة منفصلة — "مخزن المفاتيح-القيم (KV Store)" هو مجرد كائن مخصص عادي تقوم بتعريفه والاستعلام عنه باستخدام واجهة برمجة التطبيقات القياسية. هذا يعني أنه يستفيد من نفس آليات المزامنة والصلاحيات والأدوات مثل باقي بيانات تطبيقك. +على عكس المخزن المدمج، يكون الكائن المخصص محدَّد النطاق دائمًا بمساحة عمل واحدة — لا يمكنه مشاركة الإدخالات عبر التثبيتات بالطريقة التي تفعلها مفاتيح `SERVER`. diff --git a/packages/twenty-docs/l/ar/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/ar/developers/extend/apps/logic/overview.mdx index 767cef1f05..f62f92f085 100644 --- a/packages/twenty-docs/l/ar/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/ar/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ icon: bolt بيانات اعتماد OAuth التي يحتفظ بها تطبيقك للخدمات الخارجية — مثل Linear وGitHub وSlack وغيرها. + + الاحتفاظ بالحالة بين تشغيلات دالة المنطق — ذاكرات التخزين المؤقت، والمؤشرات، والمطالبات عبر مساحات العمل. + ## لمحة عن أنواع المشغلات diff --git a/packages/twenty-docs/l/ar/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/ar/developers/extend/apps/operations/publishing.mdx index d6a3451ebb..ddbd4d8722 100644 --- a/packages/twenty-docs/l/ar/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/ar/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ yarn twenty app:publish --private على npmjs.com افتح الحزمة الخاصة بك > **Settings → Trusted Publisher** وسجّل هذا المستودع مع سير عمل `publish.yml` (راجع [وثائق النشر الموثوق من npm](https://docs.npmjs.com/trusted-publishers)). يُصادِق النشر مع بيانات provenance على مستودع GitHub الذي بنى الحزمة، وهو أيضًا ما يتيح لك المطالبة بملكية تطبيقك في سوق Twenty. + +npm يقبل إثبات المصدر (provenance) فقط من مستودعات المصدر **العامّة**. إذا قمت بالنشر من مستودع خاص، يرفض npm حزمة إثبات المصدر عبر OIDC برسالة `E422 ... خطأ Unsupported GitHub Actions source repository visibility: "private"`. لنشر الحزمة (publish) من مستودع خاص، يمكنك إيقاف استخدام إثبات المصدر (provenance) عبر تعيين `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` في متغيّر `env` لخطوة النشر (publish step) (تلميح مضاف كتعليق موجود في ملف `publish.yml` المُنشأ تلقائيًا): + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### تثبيت الإجراءات القابلة لإعادة الاستخدام يشير سيرَا العمل `ci.yml` و`cd.yml` إلى إجراءات قابلة لإعادة الاستخدام عند `@main`، لذا تُلتقط تحديثات الإجراءات في مستودع `twentyhq/twenty` تلقائيًا. إذا كنت تريد بناءات حتمية، فاستبدِل `@main` بقيمة SHA لالتزام أو بوسم إصدار في كل سطر `uses:`. diff --git a/packages/twenty-docs/l/cs/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/cs/developers/extend/apps/logic/key-value-store.mdx index cf5afa9c74..ec244221c4 100644 --- a/packages/twenty-docs/l/cs/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/cs/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: Úložiště typu klíč–hodnota -description: Ukládejte průběžné výsledky, kešujte data a sdílejte stav mezi spuštěními logických funkcí pomocí jednoduchého objektu typu klíč–hodnota. +description: Ukládejte průběžné výsledky, kešujte data a sdílejte stav mezi spuštěními logických funkcí pomocí vestavěného aplikačního úložiště typu klíč–hodnota. icon: database --- -Logické funkce běží v izolovaných, krátce žijících procesech Node.js — jakmile běh skončí, nic, co bylo v paměti, nepřežije. Když potřebujete **něco zapamatovat mezi běhy** (kešovat nákladnou odpověď z API, uložit kurzor pro inkrementální synchronizace, odložit práci nebo předat stav z jedné funkce do druhé), uložte to do databáze pracovního prostoru. +Logické funkce běží v izolovaných, krátce žijících procesech Node.js — jakmile běh skončí, nic, co bylo v paměti, nepřežije. Když potřebujete **něco zapamatovat mezi běhy** (kešovat nákladnou odpověď z API, uložit kurzor pro inkrementální synchronizace, odložit práci nebo předat stav z jedné funkce do druhé), uložte to do vestavěného úložiště typu klíč–hodnota. -Na to nepotřebujete speciální úložiště: malý **technický objekt** s polem `key` a polem `value` vám poskytne trvalé key-value úložiště, omezené na pracovní prostor, které lze dotazovat přes stejný [typovaný klient API](/l/cs/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk), který už používáte pro záznamy. +Každá aplikace má svůj vlastní izolovaný jmenný prostor: položky jsou svázané s ověřenou aplikací, takže vaše klíče nikdy nemohou kolidovat s klíči jiné aplikace ani je jiná aplikace nemůže číst. ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## Definujte objekt úložiště +## Get, set, delete -Deklarujte vlastní objekt se dvěma poli — `key` (jedinečný `TEXT`) a `value` (`RAW_JSON`, takže můžete ukládat libovolná JSON-serializovatelná data). Úplnou referenci `defineObject` najdete v [Objects](/l/cs/developers/extend/apps/data/objects). +Importujte `kv` z `twenty-sdk/logic-function`. Hodnoty mohou být libovolná JSON-serializovatelná data. -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## Rozsahy + +Každá položka má rozsah, který se předává jako volba při každém volání. Výchozí hodnota je `WORKSPACE`. + +* **`WORKSPACE`** (výchozí) — položka je soukromá pro aktuální instalaci vaší aplikace v pracovním prostoru. Každý pracovní prostor, který aplikaci nainstaluje, získá vlastní nezávislou sadu klíčů. To je to, co chcete pro keše, kurzory a stav na úrovni pracovního prostoru. +* **`SERVER`** — položka je sdílena napříč **všemi instalacemi** vaší aplikace na serveru. Serverové položky se chovají jako **nároky (claims)**: uloženou hodnotou je vždy workspaceId, které klíč nárokuje (vynechte `value` při `set`, abyste klíč nárokovali pro aktuální pracovní prostor) a pouze tento pracovní prostor ji může přepsat nebo smazat. Každá instalace může položku číst. + +Serverové nároky existují pro směrování napříč pracovními prostory. [Server-route resolver](/l/cs/developers/extend/apps/logic/logic-functions#server-route-trigger) běží v pracovním prostoru vlastníka registrace aplikace, ale příchozí webhook obvykle nese pouze externí id účtu — nikoli Twenty workspaceId. Nechte každý pracovní prostor, aby si při připojení nárokoval své externí id, a poté ho v routě rozřešte: + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### Vynucení jedinečnosti klíče - -Přidejte na `key` **jedinečný index**, aby stejný klíč nikdy nemohl mít dva řádky. Toto je doporučené primitivum pro jedinečnost — viz [Data → Unique indexes](/l/cs/developers/extend/apps/data/overview#unique-indexes). - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## Čtení a zápis z logické funkce - -Zabalte objekt do několika malých helperů, aby zbytek vašeho kódu vypadal jako key-value API — `get`, `set` a `del`. Používají [`CoreApiClient`](/l/cs/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk), který je generovaný ze schématu vašeho pracovního prostoru a je plně typovaný proti objektu `kvStore`. - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -Jedinečný index chrání před duplikáty, ale dva běhy, které ve stejný okamžik zapisují **stejný nový klíč**, se stále mohou předhánět mezi vyhledáním a vytvořením. Považujte vytvoření, které selže na omezení jedinečnosti, za „někdo jiný vyhrál“ — zachyťte ho a znovu přečtěte, nebo to zkuste znovu jako aktualizaci. - +Protože serverový klíč může být nárokován pouze pro vlastní pracovní prostor volajícího a nikdy nemůže být přepsán jiným, pracovní prostor nemůže převzít mapování, které patří někomu jinému. `kv.set` vyvolá výjimku, když je klíč už nárokován jiným pracovním prostorem. ## Použití: kešujte nákladné volání @@ -155,15 +59,15 @@ Typickým použitím je kešování pomalé nebo omezované (rate-limited) odpov ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## Vzorové postupy a tipy -* **Jmenné prostory.** Přidávejte prefixy ke klíčům, abyste oddělili různé oblasti a usnadnili hromadná vyhledávání — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. Filtrováním `key: { like: 'cache:%' }` můžete vypsat nebo vyčistit celý jmenný prostor. -* **Expirace (TTL).** Úložiště nemá vestavěnou expiraci. Uložte časové razítko dovnitř `value` (jako v příkladu s keší) a při čtení ho kontrolujte, nebo přidejte pole `DATE_TIME` a pravidelně čistěte zastaralé řádky z [funkce spouštěné cronem](/l/cs/developers/extend/apps/logic/logic-functions). -* **Co ukládat.** `RAW_JSON` obsahuje libovolnou JSON-serializovatelnou hodnotu — čísla, řetězce, pole, objekty. Držte záznamy malé; toto je určeno pro koordinaci a kešování, ne pro velké objekty blob nebo soubory. Pro soubory použijte pole `FILES` a [`uploadFile`](/l/cs/developers/extend/apps/logic/logic-functions#uploading-files). +* **Jmenné prostory.** Přidávejte prefixy ke klíčům, abyste oddělili různé oblasti — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. +* **Expirace (TTL).** Úložiště nemá vestavěnou expiraci. Uložte časové razítko dovnitř hodnoty (jako v příkladu s keší) a při čtení ho kontrolujte, nebo čistěte zastaralé klíče z [funkce spouštěné cronem](/l/cs/developers/extend/apps/logic/logic-functions). +* **Co ukládat.** Jakákoli JSON-serializovatelná hodnota — čísla, řetězce, pole, objekty. Držte záznamy malé; toto je určeno pro koordinaci a kešování, ne pro velké objekty blob nebo soubory. Pro soubory použijte pole `FILES` a [`uploadFile`](/l/cs/developers/extend/apps/logic/logic-functions#uploading-files). +* **Viditelnost.** Položky žijí v databázi instance, ne jako záznamy pracovního prostoru — nikdy se neobjevují v uživatelském rozhraní pracovního prostoru, nejsou součástí datového modelu vaší aplikace a nevyžadují žádná oprávnění k rolím ani objektům. + +## Alternativa: dotazovatelný objekt úložiště + +Vestavěné úložiště je záměrně neprůhledné: položky nejsou záznamy, takže je nemůžete procházet v UI, propojovat s jinými objekty nebo filtrovat pomocí dotazů na záznamy. Kdykoli něco z toho potřebujete — například viditelný log synchronizace nebo stav na úrovni záznamu — definujte místo toho malý **technický objekt** s jedinečným polem `key` a polem `value` typu `RAW_JSON` a dotazujte ho přes [typovaný API klient](/l/cs/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Viz [Objects](/l/cs/developers/extend/apps/data/objects) pro referenci k `defineObject` a [Data → Unique indexes](/l/cs/developers/extend/apps/data/overview#unique-indexes) pro vynucení jedinečnosti klíče. + +* **Omezení na záznam.** Přidejte [relaci](/l/cs/developers/extend/apps/data/relations) z objektu úložiště na cílový objekt namísto zakódování id do klíče. * **Viditelnost a oprávnění.** Řádky žijí v databázi pracovního prostoru jako jakýkoli jiný záznam, takže je lze dotazovat přes API a respektují [role](/l/cs/developers/extend/apps/config/roles) vaší aplikace. Aby se úložiště neobjevovalo v hlavním rozhraní, neuvádějte ho v [navigačním menu](/l/cs/developers/extend/apps/layout/navigation-menu-items). -* **Vazba na záznam.** Potřebujete stav na úrovni jednotlivých záznamů místo globálních klíčů? Přidejte [relaci](/l/cs/developers/extend/apps/data/relations) z objektu úložiště na cílový objekt namísto zakódování id do klíče. -Jde o konvenci, ne o samostatnou funkci — „KV Store“ je pouze běžný vlastní objekt, který definujete a dotazujete přes standardní API. To znamená, že těží ze stejné synchronizace, oprávnění a nástrojů jako ostatní data vaší aplikace. +Na rozdíl od vestavěného úložiště je vlastní objekt vždy omezený na jeden pracovní prostor — nemůže sdílet položky napříč instalacemi tak, jako to dělají klíče `SERVER`. diff --git a/packages/twenty-docs/l/cs/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/cs/developers/extend/apps/logic/overview.mdx index 6a58244f47..de4f9bef69 100644 --- a/packages/twenty-docs/l/cs/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/cs/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ icon: bolt Přihlašovací údaje OAuth, které vaše aplikace uchovává pro služby třetích stran — Linear, GitHub, Slack a další. + + Udržujte stav mezi spuštěními logických funkcí – cache, kurzory a nároky napříč pracovními prostory. + ## Přehled typů spouštěčů diff --git a/packages/twenty-docs/l/cs/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/cs/developers/extend/apps/operations/publishing.mdx index 3a1bcbaacb..74af5d4d19 100644 --- a/packages/twenty-docs/l/cs/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/cs/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ Publikuje vaši aplikaci na npm s doloženým původem, když odešlete tag verz Na npmjs.com otevřete svůj balíček > **Settings → Trusted Publisher** a zaregistrujte tento repozitář s workflowem `publish.yml` (viz [dokumentaci k důvěryhodnému publikování na npm](https://docs.npmjs.com/trusted-publishers)). Publikování s provenance potvrzuje, který repozitář na GitHubu balíček sestavil, a zároveň tak uplatňujete vlastnictví své aplikace na tržišti Twenty. + +npm přijímá údaje o původu pouze z **veřejných** zdrojových repozitářů. Pokud publikujete ze soukromého repozitáře, npm odmítne balíček údajů o původu OIDC s chybou `E422 ... Unsupported GitHub Actions source repository visibility: "private"` error. Chcete-li publikovat ze soukromého repozitáře, vypněte prokazování původu nastavením `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` v `env` publikačního kroku (ve vygenerovaném `publish.yml` je uveden zakomentovaný tip): + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### Připnutí verzí znovupoužitelných akcí Workflowy `ci.yml` a `cd.yml` odkazují na znovupoužitelné akce na `@main`, takže aktualizace akcí v repozitáři `twentyhq/twenty` se přeberou automaticky. Pokud chcete deterministická sestavení, nahraďte `@main` v každém řádku `uses:` za commit SHA nebo tag vydání. diff --git a/packages/twenty-docs/l/de/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/de/developers/extend/apps/logic/key-value-store.mdx index e3dcff8432..9887a9a9bc 100644 --- a/packages/twenty-docs/l/de/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/de/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: Key-Value-Store -description: Bewahren Sie Zwischenergebnisse auf, zwischenspeichern Sie Daten und teilen Sie den Zustand zwischen Ausführungen von Logikfunktionen mit einem einfachen Key-Value-Objekt. +description: Bewahren Sie Zwischenergebnisse auf, zwischenspeichern Sie Daten und teilen Sie den Zustand zwischen Ausführungen von Logikfunktionen mit dem integrierten Key-Value-Speicher der Anwendung. icon: database --- -Logikfunktionen laufen isoliert in kurzlebigen Node.js-Prozessen – sobald ein Durchlauf abgeschlossen ist, überlebt nichts, was im Speicher gehalten wurde. Wenn Sie sich **zwischen Durchläufen etwas merken müssen** (eine teure API-Antwort zwischenspeichern, einen Cursor für inkrementelle Synchronisierungen speichern, Arbeit entprellen oder Zustand von einer Funktion an eine andere übergeben), speichern Sie es in der Workspace-Datenbank. +Logikfunktionen laufen isoliert in kurzlebigen Node.js-Prozessen – sobald ein Durchlauf abgeschlossen ist, überlebt nichts, was im Speicher gehalten wurde. Wenn Sie sich **zwischen Durchläufen etwas merken müssen** (eine teure API-Antwort zwischenspeichern, einen Cursor für inkrementelle Synchronisierungen speichern, Arbeit entprellen oder Zustand von einer Funktion an eine andere übergeben), speichern Sie es im integrierten Key-Value-Speicher. -Dafür benötigen Sie kein spezielles Speicher-Primitiv: Ein kleines **technisches Objekt** mit einem `key`-Feld und einem `value`-Feld gibt Ihnen einen dauerhaften Key-Value-Store, der auf den Workspace begrenzt ist und über denselben [typisierten API-Client](/l/de/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) abfragbar ist, den Sie bereits für Datensätze verwenden. +Jede Anwendung erhält ihren eigenen isolierten Namespace: Einträge werden durch die authentifizierte App indiziert, sodass Ihre Schlüssel niemals mit denen einer anderen Anwendung kollidieren können – oder von ihr gelesen werden können. ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## Das Store-Objekt definieren +## Abrufen, Setzen, Löschen -Deklarieren Sie ein benutzerdefiniertes Objekt mit zwei Feldern – `key` (ein eindeutiger `TEXT`) und `value` (ein `RAW_JSON`, damit Sie jede JSON-serialisierbare Nutzlast speichern können). Siehe [Objekte](/l/de/developers/extend/apps/data/objects) für die vollständige `defineObject`-Referenz. +Importieren Sie `kv` aus `twenty-sdk/logic-function`. Werte können beliebige JSON-serialisierbare Werte sein. -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## Geltungsbereiche + +Jeder Eintrag hat einen Geltungsbereich, der bei jedem Aufruf als Option übergeben wird. Der Standardwert ist `WORKSPACE`. + +* **`WORKSPACE`** (Standard) – der Eintrag ist nur für die aktuelle Workspace-Installation Ihrer App sichtbar. Jeder Workspace, der die App installiert, erhält seinen eigenen unabhängigen Satz von Schlüsseln. Das ist genau das, was Sie für Caches, Cursor und workspacespezifischen Zustand benötigen. +* **`SERVER`** – der Eintrag wird über **alle Installationen** Ihrer App auf dem Server hinweg geteilt. Server-Einträge verhalten sich wie **Claims**: Der gespeicherte Wert ist immer die workspaceId, die den Schlüssel beansprucht hat (lassen Sie `value` bei `set` weg, um den Schlüssel für den aktuellen Workspace zu beanspruchen), und nur dieser Workspace kann ihn überschreiben oder löschen. Jede Installation kann den Eintrag lesen. + +Server-Claims existieren für Cross-Workspace-Routing. Ein [Server-Route-Resolver](/l/de/developers/extend/apps/logic/logic-functions#server-route-trigger) läuft im Workspace des Anwendungsregistrierungs-Inhabers, aber ein eingehender Webhook übermittelt in der Regel nur eine externe Konto-ID – nicht eine Twenty workspaceId. Lassen Sie jeden Workspace seine externe ID zum Verbindungszeitpunkt beanspruchen und lösen Sie sie dann in der Route auf: + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### Eindeutigkeit des Schlüssels erzwingen - -Fügen Sie einen **eindeutigen Index** auf `key` hinzu, damit derselbe Schlüssel niemals zwei Zeilen haben kann. Dies ist das empfohlene Primitiv für Eindeutigkeit – siehe [Daten → Eindeutige Indizes](/l/de/developers/extend/apps/data/overview#unique-indexes). - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## Aus einer Logikfunktion lesen und schreiben - -Kapseln Sie das Objekt hinter ein paar kleinen Hilfsfunktionen, sodass der Rest Ihres Codes wie eine Key-Value-API aussieht – `get`, `set` und `del`. Sie verwenden [`CoreApiClient`](/l/de/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk), der aus Ihrem Workspace-Schema generiert wird und vollständig gegen das `kvStore`-Objekt typisiert ist. - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -Der eindeutige Index schützt vor Duplikaten, aber zwei Durchläufe, die **denselben neuen Schlüssel** im gleichen Moment schreiben, können zwischen der Abfrage und dem Erstellen dennoch in eine Race-Condition geraten. Behandeln Sie einen Erstellvorgang, der an der Eindeutigkeitsbeschränkung scheitert, als „jemand anderes war schneller“ – fangen Sie den Fehler ab und lesen Sie erneut, oder versuchen Sie es als Aktualisierung noch einmal. - +Da ein Server-Schlüssel nur für den eigenen Workspace des Aufrufers beansprucht und niemals von einem anderen überschrieben werden kann, kann ein Workspace kein Mapping kapern, das jemand anderem gehört. `kv.set` löst eine Exception aus, wenn der Schlüssel bereits von einem anderen Workspace beansprucht wurde. ## Verwenden Sie ihn: einen teuren Aufruf zwischenspeichern @@ -155,15 +59,15 @@ Eine typische Verwendung ist das Zwischenspeichern einer langsamen oder ratelimi ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## Muster & Tipps -* **Namespacing.** Präfixieren Sie Schlüssel, um unterschiedliche Belange getrennt zu halten und Bulk-Abfragen zu erleichtern – `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. Filtern Sie mit `key: { like: 'cache:%' }`, um einen gesamten Namespace aufzulisten oder zu leeren. -* **Ablauf (TTL).** Der Store hat keine eingebaute Ablaufzeit. Speichern Sie einen Zeitstempel innerhalb des `value` (wie im Cache-Beispiel) und prüfen Sie ihn beim Lesen, oder fügen Sie ein `DATE_TIME`-Feld hinzu und löschen Sie regelmäßig veraltete Zeilen aus einer [cron-getriggerten Funktion](/l/de/developers/extend/apps/logic/logic-functions). -* **Was gespeichert wird.** `RAW_JSON` enthält jeden JSON-serialisierbaren Wert – Zahlen, Zeichenketten, Arrays, Objekte. Halten Sie Einträge klein; dies ist für Koordination und Caching gedacht, nicht für große Blobs oder Dateien. Für Dateien verwenden Sie ein `FILES`-Feld und [`uploadFile`](/l/de/developers/extend/apps/logic/logic-functions#uploading-files). +* **Namespacing.** Präfixieren Sie Schlüssel, um unterschiedliche Belange getrennt zu halten – `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. +* **Ablauf (TTL).** Der Store hat keine eingebaute Ablaufzeit. Speichern Sie einen Zeitstempel im Wert (wie im Cache-Beispiel) und prüfen Sie ihn beim Lesen, oder löschen Sie veraltete Schlüssel aus einer [cron-getriggerten Funktion](/l/de/developers/extend/apps/logic/logic-functions). +* **Was gespeichert wird.** Jeder JSON-serialisierbare Wert – Zahlen, Zeichenketten, Arrays, Objekte. Halten Sie Einträge klein; dies ist für Koordination und Caching gedacht, nicht für große Blobs oder Dateien. Für Dateien verwenden Sie ein `FILES`-Feld und [`uploadFile`](/l/de/developers/extend/apps/logic/logic-functions#uploading-files). +* **Sichtbarkeit.** Einträge liegen in der Instanzdatenbank, nicht als Workspace-Datensätze – sie erscheinen nie in der Workspace-UI, sind kein Teil des Datenmodells Ihrer App und benötigen keine Rollen- oder Objektberechtigungen. + +## Alternative: ein abfragbares Store-Objekt + +Der integrierte Store ist bewusst intransparent: Einträge sind keine Datensätze, daher können Sie sie nicht in der UI durchsuchen, nicht mit anderen Objekten verknüpfen oder mit Record-Abfragen filtern. Wenn Sie irgendetwas davon benötigen – etwa ein sichtbares Sync-Log oder pro-Datensatz-Zustand – definieren Sie stattdessen ein kleines **technisches Objekt** mit einem eindeutigen `key`-Feld und einem `RAW_JSON`-`value`-Feld und fragen Sie es über den [typisierten API-Client](/l/de/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) ab. Siehe [Objekte](/l/de/developers/extend/apps/data/objects) für die Referenz zu `defineObject` und [Daten → Eindeutige Indizes](/l/de/developers/extend/apps/data/overview#unique-indexes) zur Durchsetzung der Schlüssel-Eindeutigkeit. + +* **Geltungsbereich für einen Datensatz.** Fügen Sie vom Store-Objekt eine [Relation](/l/de/developers/extend/apps/data/relations) zum Zielobjekt hinzu, anstatt die ID in den Schlüssel zu kodieren. * **Sichtbarkeit & Berechtigungen.** Zeilen befinden sich wie jeder andere Datensatz in der Workspace-Datenbank, sind also über die API abfragbar und respektieren die [Rolle](/l/de/developers/extend/apps/config/roles) Ihrer App. Um den Store aus dem Haupt-UI herauszuhalten, führen Sie ihn nicht in Ihrem [Navigationsmenü](/l/de/developers/extend/apps/layout/navigation-menu-items) auf. -* **Auf einen Datensatz begrenzen.** Sie benötigen zustandsspezifische Daten pro Datensatz statt globaler Schlüssel? Fügen Sie vom Store-Objekt eine [Relation](/l/de/developers/extend/apps/data/relations) zum Zielobjekt hinzu, anstatt die ID in den Schlüssel zu kodieren. -Dies ist eine Konvention, keine separate Funktion – der „KV Store“ ist einfach ein reguläres benutzerdefiniertes Objekt, das Sie definieren und mit der Standard-API abfragen. Das bedeutet, er profitiert von derselben Synchronisierung, denselben Berechtigungen und denselben Tools wie die übrigen Daten Ihrer App. +Im Gegensatz zum integrierten Store ist ein benutzerdefiniertes Objekt immer auf einen Workspace begrenzt – es kann keine Einträge über Installationen hinweg teilen, so wie es `SERVER`-Schlüssel tun. diff --git a/packages/twenty-docs/l/de/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/de/developers/extend/apps/logic/overview.mdx index bf8d8287d6..2c23072fbd 100644 --- a/packages/twenty-docs/l/de/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/de/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ Die **Logikschicht** einer Twenty-App ist der Code, der *ausgeführt wird* – s OAuth-Anmeldedaten, die Ihre App für Dienste von Drittanbietern verwaltet – Linear, GitHub, Slack und mehr. + + Zustand zwischen Ausführungen von Logikfunktionen beibehalten — Caches, Cursor und arbeitsbereichsübergreifende Claims. + ## Auslösertypen im Überblick diff --git a/packages/twenty-docs/l/de/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/de/developers/extend/apps/operations/publishing.mdx index c749843d65..f923396838 100644 --- a/packages/twenty-docs/l/de/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/de/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ Veröffentlicht Ihre App mit Herkunftsnachweis auf npm, wenn Sie ein Versions-Ta Öffnen Sie auf npmjs.com Ihr Paket > **Settings → Trusted Publisher** und registrieren Sie dieses Repository mit dem `publish.yml`-Workflow (siehe die [npm trusted publishing docs](https://docs.npmjs.com/trusted-publishers)). Das Veröffentlichen mit Provenance bestätigt, welches GitHub-Repository das Paket gebaut hat; zugleich beanspruchen Sie damit die Inhaberschaft Ihrer App in einem Twenty-Marktplatz. + +npm akzeptiert Herkunftsnachweise nur aus **öffentlichen** Quellcode-Repositories. Wenn du aus einem privaten Repository veröffentlichst, weist npm das OIDC-Provenance-Bundle mit einem `E422 Unsupported GitHub Actions source repository visibility: "private"`-Fehler. Um aus einem privaten Repository zu veröffentlichen, deaktiviere Herkunftsnachweise, indem du `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` im `env`-Block des Veröffentlichungs-Schritts setzt (ein auskommentierter Hinweis ist in der generierten `publish.yml` enthalten): + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### Fixieren der wiederverwendbaren Actions Die Workflows `ci.yml` und `cd.yml` verweisen auf wiederverwendbare Actions mit `@main`, sodass Aktualisierungen der Actions im Repository `twentyhq/twenty` automatisch übernommen werden. Wenn Sie deterministische Builds möchten, ersetzen Sie `@main` in jeder `uses:`-Zeile durch eine Commit-SHA oder einen Release-Tag. diff --git a/packages/twenty-docs/l/es/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/es/developers/extend/apps/logic/key-value-store.mdx index 31822c24d6..d1192b2ac6 100644 --- a/packages/twenty-docs/l/es/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/es/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: Almacén de clave-valor -description: Conserva resultados intermedios, almacena en caché datos y comparte estado entre ejecuciones de funciones lógicas con un sencillo objeto de clave-valor. +description: Conserva resultados intermedios, almacena en caché datos y comparte estado entre ejecuciones de funciones lógicas con el almacén de clave-valor integrado de la aplicación. icon: database --- -Las funciones de lógica se ejecutan aisladas en procesos de Node.js de corta duración; una vez que una ejecución finaliza, nada de lo que se mantuvo en memoria sobrevive. Cuando necesitas **recordar algo entre ejecuciones** (almacenar en caché una respuesta de API costosa, guardar un cursor para sincronizaciones incrementales, aplicar *debounce* al trabajo o traspasar estado de una función a otra), persístelo en la base de datos del espacio de trabajo. +Las funciones de lógica se ejecutan aisladas en procesos de Node.js de corta duración; una vez que una ejecución finaliza, nada de lo que se mantuvo en memoria sobrevive. Cuando necesitas **recordar algo entre ejecuciones** (almacenar en caché una respuesta de API costosa, guardar un cursor para sincronizaciones incrementales, aplicar *debounce* al trabajo o traspasar estado de una función a otra), persiste esos datos en el almacén de clave-valor integrado. -No necesitas una primitiva de almacenamiento dedicada para esto: un pequeño **objeto técnico** con un campo `key` y un campo `value` te ofrece un almacén de clave-valor duradero, con alcance al espacio de trabajo, consultable a través del mismo [cliente de API tipado](/l/es/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) que ya utilizas para los registros. +Cada aplicación obtiene su propio espacio de nombres aislado: las entradas se asocian con la aplicación autenticada, por lo que tus claves nunca pueden entrar en conflicto con — ni ser leídas por — otra aplicación. ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## Definir el objeto de almacenamiento +## Obtener, establecer, eliminar -Declara un objeto personalizado con dos campos: `key` (un `TEXT` único) y `value` (un `RAW_JSON` para que puedas almacenar cualquier carga útil serializable en JSON). Consulta [Objetos](/l/es/developers/extend/apps/data/objects) para ver la referencia completa de `defineObject`. +Importa `kv` desde `twenty-sdk/logic-function`. Los valores pueden ser cualquier carga útil serializable en JSON. -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## Ámbitos + +Cada entrada tiene un ámbito (*scope*), que se pasa como una opción en cada llamada. El valor predeterminado es `WORKSPACE`. + +* **`WORKSPACE`** (predeterminado): la entrada es privada para la instalación actual de tu aplicación en el espacio de trabajo. Cada espacio de trabajo que instala la aplicación obtiene su propio conjunto independiente de claves. Esto es lo que quieres para cachés, cursores y estado por espacio de trabajo. +* **`SERVER`**: la entrada se comparte entre **todas las instalaciones** de tu aplicación en el servidor. Las entradas de servidor se comportan como reclamaciones: el valor almacenado es siempre el workspaceId que reclamó la clave (omite `value` en `set` para reclamar la clave para el espacio de trabajo actual), y solo ese espacio de trabajo puede sobrescribirla o eliminarla. Cualquier instalación puede leer la entrada. + +Las reclamaciones de servidor existen para el enrutamiento entre espacios de trabajo. Un [server-route resolver](/l/es/developers/extend/apps/logic/logic-functions#server-route-trigger) se ejecuta en el espacio de trabajo propietario del registro de la aplicación, pero un webhook entrante normalmente solo lleva un id de cuenta externa, no un Twenty workspaceId. Haz que cada espacio de trabajo reclame su id externo en el momento de la conexión y luego resuélvelo en la ruta: + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### Aplicar la unicidad de la clave - -Añade un **índice único** en `key` para que la misma clave nunca pueda tener dos filas. Esta es la primitiva recomendada para la unicidad; consulta [Datos → Índices únicos](/l/es/developers/extend/apps/data/overview#unique-indexes). - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## Leer y escribir desde una función de lógica - -Envuelve el objeto tras unos pequeños *helpers* para que el resto de tu código se lea como una API de clave-valor: `get`, `set` y `del`. Utilizan [`CoreApiClient`](/l/es/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk), que se genera a partir del esquema de tu espacio de trabajo y está completamente tipado contra el objeto `kvStore`. - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -El índice único protege contra duplicados, pero dos ejecuciones que escriben la **misma clave nueva** en el mismo instante aún pueden competir entre la búsqueda y la creación. Trata una creación que falle por la restricción de unicidad como "alguien más ganó": captúrala y vuelve a leer, o inténtalo de nuevo como una actualización. - +Como una clave de servidor solo puede ser reclamada para el propio espacio de trabajo de quien la llama y nunca puede ser sobrescrita por otro, un espacio de trabajo no puede secuestrar un mapeo que pertenezca a otra persona. `kv.set` produce una excepción cuando la clave ya ha sido reclamada por otro espacio de trabajo. ## Úsalo: almacena en caché una llamada costosa @@ -155,15 +59,15 @@ Un uso típico es almacenar en caché una respuesta lenta o limitada por *rate l ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## Patrones y consejos -* **Espacios de nombres.** Añade un prefijo a las claves para mantener separadas las distintas responsabilidades y facilitar las búsquedas masivas: `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. Filtra con `key: { like: 'cache:%' }` para listar o limpiar todo un espacio de nombres. -* **Caducidad (TTL).** El almacén no tiene caducidad integrada. Almacena una marca de tiempo dentro de `value` (como en el ejemplo de caché) y revísala al leer, o añade un campo `DATE_TIME` y borra periódicamente las filas obsoletas desde una [función activada por cron](/l/es/developers/extend/apps/logic/logic-functions). -* **Qué almacenar.** `RAW_JSON` contiene cualquier valor serializable en JSON: números, cadenas, arreglos, objetos. Mantén las entradas pequeñas; esto es para coordinación y almacenamiento en caché, no para *blobs* grandes o archivos. Para archivos, utiliza un campo `FILES` y [`uploadFile`](/l/es/developers/extend/apps/logic/logic-functions#uploading-files). +* **Espacios de nombres.** Añade un prefijo a las claves para mantener separadas las distintas responsabilidades: `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. +* **Caducidad (TTL).** El almacén no tiene caducidad integrada. Almacena una marca de tiempo dentro del valor (como en el ejemplo de caché) y revísala al leer, o borra las claves obsoletas desde una [función activada por cron](/l/es/developers/extend/apps/logic/logic-functions). +* **Qué almacenar.** Cualquier valor serializable en JSON: números, cadenas, arreglos, objetos. Mantén las entradas pequeñas; esto es para coordinación y almacenamiento en caché, no para *blobs* grandes o archivos. Para archivos, utiliza un campo `FILES` y [`uploadFile`](/l/es/developers/extend/apps/logic/logic-functions#uploading-files). +* **Visibilidad.** Las entradas residen en la base de datos de la instancia, no como registros del espacio de trabajo: nunca aparecen en la interfaz de usuario del espacio de trabajo, no forman parte del modelo de datos de tu aplicación y no necesitan permisos de rol ni de objeto. + +## Alternativa: un objeto de almacenamiento consultable + +El almacén integrado es deliberadamente opaco: las entradas no son registros, por lo que no puedes explorarlas en la interfaz de usuario, relacionarlas con otros objetos ni filtrarlas con consultas de registros. Cuando necesites algo de eso — por ejemplo, un registro de sincronización visible o estado por registro — define en su lugar un pequeño **objeto técnico** con un campo `key` único y un campo `value` de tipo `RAW_JSON`, y consúltalo mediante el [cliente de API tipado](/l/es/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Consulta [Objects](/l/es/developers/extend/apps/data/objects) para la referencia de `defineObject` y [Data → Unique indexes](/l/es/developers/extend/apps/data/overview#unique-indexes) para aplicar la unicidad de la clave. + +* **Limitando el ámbito a un registro.** Añade una [relación](/l/es/developers/extend/apps/data/relations) desde el objeto de almacenamiento al objeto de destino en lugar de codificar el id en la clave. * **Visibilidad y permisos.** Las filas residen en la base de datos del espacio de trabajo como cualquier otro registro, por lo que se pueden consultar a través de la API y respetan el [rol](/l/es/developers/extend/apps/config/roles) de tu aplicación. Para mantener el almacén fuera de la interfaz principal, déjalo fuera de tu [menú de navegación](/l/es/developers/extend/apps/layout/navigation-menu-items). -* **Ámbito por registro.** ¿Necesitas estado por registro en lugar de claves globales? Añade una [relación](/l/es/developers/extend/apps/data/relations) desde el objeto de almacenamiento al objeto de destino en lugar de codificar el id en la clave. -Esto es una convención, no una característica independiente: el "KV Store" es solo un objeto personalizado normal que defines y consultas con la API estándar. Eso significa que se beneficia de la misma sincronización, permisos y herramientas que el resto de los datos de tu aplicación. +A diferencia del almacén integrado, un objeto personalizado siempre está limitado a un solo espacio de trabajo: no puede compartir entradas entre instalaciones como lo hacen las claves `SERVER`. diff --git a/packages/twenty-docs/l/es/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/es/developers/extend/apps/logic/overview.mdx index a7475eedf6..6a7dc0b5c4 100644 --- a/packages/twenty-docs/l/es/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/es/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ La **capa de lógica** de una app de Twenty es el código que *se ejecuta*: cont Credenciales OAuth que tu app mantiene para servicios de terceros — Linear, GitHub, Slack y más. + + Conserva el estado entre ejecuciones de funciones lógicas — cachés, cursores y declaraciones entre espacios de trabajo. + ## Tipos de disparadores de un vistazo diff --git a/packages/twenty-docs/l/es/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/es/developers/extend/apps/operations/publishing.mdx index 3a78be5314..0d195705e0 100644 --- a/packages/twenty-docs/l/es/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/es/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ Publica tu aplicación en npm con procedencia cuando haces push de una etiqueta En npmjs.com abre tu paquete > **Settings → Trusted Publisher** y registra este repositorio con el flujo de trabajo `publish.yml` (consulta la [documentación de publicación de confianza de npm](https://docs.npmjs.com/trusted-publishers)). Publicar con provenance certifica qué repositorio de GitHub compiló el paquete, lo cual también es cómo reclamas la propiedad de tu aplicación en un marketplace de Twenty. + +npm solo acepta procedencia de repositorios de código fuente **públicos**. Si publicas desde un repositorio privado, npm rechaza el paquete de procedencia OIDC con un `E422 ... Visibilidad del repositorio de origen de GitHub Actions no compatible: "private"` error. Para publicar desde un repositorio privado, excluya la procedencia configurando `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` en el `env` del paso de publicación (se incluye una sugerencia comentada en el archivo `publish.yml` generado automáticamente): + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### Fijar las acciones reutilizables Los flujos de trabajo `ci.yml` y `cd.yml` hacen referencia a acciones reutilizables en `@main`, por lo que las actualizaciones de acciones en el repositorio `twentyhq/twenty` se aplican automáticamente. Si quieres compilaciones deterministas, reemplaza `@main` por un SHA de commit o una etiqueta de versión en cada línea `uses:`. diff --git a/packages/twenty-docs/l/fr/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/fr/developers/extend/apps/logic/key-value-store.mdx index c02691d64f..b61360cb7c 100644 --- a/packages/twenty-docs/l/fr/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/fr/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: Stockage clé-valeur -description: Conservez des résultats intermédiaires, mettez les données en cache et partagez l’état entre les exécutions de fonctions logiques à l’aide d’un simple objet clé-valeur. +description: Conservez des résultats intermédiaires, mettez les données en cache et partagez l’état entre les exécutions de fonctions logiques avec le magasin clé-valeur intégré à l’application. icon: database --- -Les fonctions logiques s'exécutent dans des processus Node.js isolés et de courte durée — une fois une exécution terminée, rien de ce qui était conservé en mémoire ne survit. Lorsque vous avez besoin de **vous souvenir de quelque chose entre les exécutions** (mettre en cache une réponse d’API coûteuse, stocker un curseur pour des synchronisations incrémentales, appliquer un anti-rebond, ou transférer l’état d’une fonction à une autre), conservez-le dans la base de données de l’espace de travail. +Les fonctions logiques s'exécutent dans des processus Node.js isolés et de courte durée — une fois une exécution terminée, rien de ce qui était conservé en mémoire ne survit. Lorsque vous avez besoin de **vous souvenir de quelque chose entre les exécutions** (mettre en cache une réponse d’API coûteuse, stocker un curseur pour des synchronisations incrémentales, appliquer un anti-rebond, ou transférer l’état d’une fonction à une autre), conservez-le dans le magasin clé-valeur intégré. -Vous n'avez pas besoin d'un mécanisme de stockage dédié pour cela : un petit **objet technique** avec un champ `key` et un champ `value` vous fournit un stockage clé-valeur durable, limité à l'espace de travail, interrogeable via le même [client d'API typé](/l/fr/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) que vous utilisez déjà pour les enregistrements. +Chaque application dispose de son propre espace de noms isolé : les entrées sont associées à l’application authentifiée, de sorte que vos clés ne peuvent jamais entrer en collision avec celles d’une autre application, ni être lues par celle-ci. ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## Définir l'objet de stockage +## Obtenir, définir, supprimer -Déclarez un objet personnalisé avec deux champs — `key` (un `TEXT` unique) et `value` (un `RAW_JSON` afin que vous puissiez stocker n'importe quelle charge utile sérialisable en JSON). Voir [Objets](/l/fr/developers/extend/apps/data/objects) pour la référence complète de `defineObject`. +Importez `kv` depuis `twenty-sdk/logic-function`. Les valeurs peuvent être n’importe quelle charge utile sérialisable en JSON. -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## Périmètres + +Chaque entrée possède une portée, passée comme option à chaque appel. La valeur par défaut est `WORKSPACE`. + +* **`WORKSPACE`** (par défaut) — l’entrée est réservée à l’installation actuelle de votre application dans l’espace de travail. Chaque espace de travail qui installe l’application obtient son propre ensemble indépendant de clés. C’est ce qu’il vous faut pour les caches, les curseurs et l’état par espace de travail. +* **`SERVER`** — l’entrée est partagée entre **toutes les installations** de votre application sur le serveur. Les entrées serveur se comportent comme des **revendications** : la valeur stockée est toujours le workspaceId qui a revendiqué la clé (omettre `value` lors de `set` pour revendiquer la clé pour l’espace de travail actuel), et seul cet espace de travail peut la remplacer ou la supprimer. N’importe quelle installation peut lire l’entrée. + +Les revendications serveur existent pour le routage inter-espaces de travail. Un [résolveur de route serveur](/l/fr/developers/extend/apps/logic/logic-functions#server-route-trigger) s’exécute dans l’espace de travail propriétaire de l’enregistrement de l’application, mais un webhook entrant ne transporte généralement qu’un identifiant de compte externe — pas un workspaceId Twenty. Faites en sorte que chaque espace de travail revendique son identifiant externe au moment de la connexion, puis résolvez-le dans la route : + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### Appliquer l'unicité de la clé - -Ajoutez un **index unique** sur `key` pour que la même clé ne puisse jamais avoir deux lignes. Ceci est le mécanisme recommandé pour l'unicité — voir [Données → Index uniques](/l/fr/developers/extend/apps/data/overview#unique-indexes). - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## Lire et écrire depuis une fonction logique - -Placez l'objet derrière quelques petits utilitaires afin que le reste de votre code se lise comme une API clé-valeur — `get`, `set` et `del`. Ils utilisent [`CoreApiClient`](/l/fr/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk), qui est généré à partir du schéma de votre espace de travail et entièrement typé par rapport à l'objet `kvStore`. - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -L'index unique protège contre les doublons, mais deux exécutions qui écrivent **la même nouvelle clé** au même instant peuvent toujours entrer en concurrence entre la recherche et la création. Considérez une création qui échoue sur la contrainte d'unicité comme « quelqu'un d'autre a gagné » — interceptez-la et relisez, ou réessayez sous forme de mise à jour. - +Comme une clé serveur ne peut être revendiquée que pour l’espace de travail de l’appelant et jamais écrasée par un autre, un espace de travail ne peut pas détourner un mappage appartenant à quelqu’un d’autre. `kv.set` lève une exception lorsque la clé est déjà revendiquée par un autre espace de travail. ## Utilisez-le : mettez en cache un appel coûteux @@ -155,15 +59,15 @@ Un cas d'utilisation typique consiste à mettre en cache une réponse tierce len ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## Modèles et conseils -* **Espaces de noms.** Préfixez les clés pour séparer les différentes préoccupations et faciliter les recherches en masse — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. Filtrez avec `key: { like: 'cache:%' }` pour lister ou vider un espace de noms entier. -* **Expiration (TTL).** Le store n'a pas d'expiration intégrée. Stockez un horodatage dans le `value` (comme dans l'exemple de cache) et vérifiez-le à la lecture, ou ajoutez un champ `DATE_TIME` et effacez périodiquement les lignes obsolètes à partir d'une [fonction déclenchée par cron](/l/fr/developers/extend/apps/logic/logic-functions). -* **Que stocker.** `RAW_JSON` contient toute valeur sérialisable en JSON — nombres, chaînes de caractères, tableaux, objets. Gardez les entrées petites ; ceci sert à la coordination et à la mise en cache, pas aux blobs volumineux ni aux fichiers. Pour les fichiers, utilisez un champ `FILES` et [`uploadFile`](/l/fr/developers/extend/apps/logic/logic-functions#uploading-files). +* **Espaces de noms.** Préfixez les clés pour séparer les différentes préoccupations — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. +* **Expiration (TTL).** Le store n'a pas d'expiration intégrée. Stockez un horodatage dans la valeur (comme dans l’exemple de cache) et vérifiez-le à la lecture, ou effacez les clés obsolètes à partir d’une [fonction déclenchée par cron](/l/fr/developers/extend/apps/logic/logic-functions). +* **Que stocker.** Toute valeur sérialisable en JSON — nombres, chaînes de caractères, tableaux, objets. Gardez les entrées petites ; ceci sert à la coordination et à la mise en cache, pas aux blobs volumineux ni aux fichiers. Pour les fichiers, utilisez un champ `FILES` et [`uploadFile`](/l/fr/developers/extend/apps/logic/logic-functions#uploading-files). +* **Visibilité.** Les entrées résident dans la base de données de l’instance, et non comme enregistrements d’espace de travail — elles n’apparaissent jamais dans l’interface utilisateur de l’espace de travail, ne font pas partie du modèle de données de votre application et ne nécessitent aucun droit ni aucune permission d’objet. + +## Alternative : un objet de stockage interrogeable + +Le magasin intégré est délibérément opaque : les entrées ne sont pas des enregistrements, vous ne pouvez donc pas les parcourir dans l’interface utilisateur, les relier à d’autres objets, ni les filtrer avec des requêtes sur les enregistrements. Lorsque vous avez besoin de tout cela — par exemple un journal de synchronisation visible, ou un état par enregistrement — définissez plutôt un petit **objet technique** avec un champ `key` unique et un champ `RAW_JSON` `value`, puis interrogez-le via le [client d’API typé](/l/fr/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Consultez [Objets](/l/fr/developers/extend/apps/data/objects) pour la référence `defineObject` et [Données → Index uniques](/l/fr/developers/extend/apps/data/overview#unique-indexes) pour faire appliquer l’unicité des clés. + +* **Délimiter la portée à un enregistrement.** Ajoutez une [relation](/l/fr/developers/extend/apps/data/relations) de l’objet de stockage vers l’objet cible plutôt que de coder l’identifiant dans la clé. * **Visibilité et autorisations.** Les lignes résident dans la base de données de l'espace de travail comme n'importe quel autre enregistrement, elles sont donc interrogeables via l'API et respectent le [rôle](/l/fr/developers/extend/apps/config/roles) de votre application. Pour garder le store hors de l'interface principale, ne l'ajoutez pas à votre [menu de navigation](/l/fr/developers/extend/apps/layout/navigation-menu-items). -* **Portée à un enregistrement.** Besoin d'un état par enregistrement plutôt que de clés globales ? Ajoutez une [relation](/l/fr/developers/extend/apps/data/relations) de l'objet de stockage vers l'objet cible plutôt que de coder l'identifiant dans la clé. -Il s'agit d'une convention, pas d'une fonctionnalité distincte — le « KV Store » est simplement un objet personnalisé classique que vous définissez et interrogez avec l'API standard. Cela signifie qu'il bénéficie de la même synchronisation, des mêmes autorisations et des mêmes outils que le reste des données de votre application. +Contrairement au magasin intégré, un objet personnalisé est toujours limité à un seul espace de travail — il ne peut pas partager des entrées entre installations comme le font les clés `SERVER`. diff --git a/packages/twenty-docs/l/fr/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/fr/developers/extend/apps/logic/overview.mdx index 04b10d6f93..dfc9354220 100644 --- a/packages/twenty-docs/l/fr/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/fr/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ La **couche logique** d’une application Twenty est le code qui *s’exécute* Identifiants OAuth détenus par votre application pour des services tiers — Linear, GitHub, Slack, et plus encore. + + Conservez l’état entre les exécutions de fonctions logiques — caches, curseurs et revendications inter-espaces de travail. + ## Aperçu des types de déclencheurs diff --git a/packages/twenty-docs/l/fr/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/fr/developers/extend/apps/operations/publishing.mdx index 8117addbe3..33106deadf 100644 --- a/packages/twenty-docs/l/fr/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/fr/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ Publie votre application sur npm avec provenance lorsque vous poussez une étiqu Sur npmjs.com, ouvrez votre package > **Settings → Trusted Publisher** et enregistrez ce dépôt avec le workflow `publish.yml` (voir la [documentation npm sur la publication de confiance](https://docs.npmjs.com/trusted-publishers)). La publication avec provenance certifie quel dépôt GitHub a construit le package, ce qui est aussi la façon de revendiquer la propriété de votre application dans une marketplace Twenty. + +npm n’accepte la provenance que depuis des dépôts sources **publics**. Si vous publiez depuis un dépôt privé, npm rejette le bundle de provenance OIDC avec une erreur `E422 ... Erreur « Unsupported GitHub Actions source repository visibility: "private"` ». Pour publier depuis un dépôt privé, désactivez la provenance en définissant `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` dans la variable `env` de l’étape de publication (un indice commenté est inclus dans le fichier `publish.yml` généré automatiquement) : + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### Épingler les actions réutilisables Les workflows `ci.yml` et `cd.yml` référencent des actions réutilisables sur `@main`, de sorte que les mises à jour des actions dans le dépôt `twentyhq/twenty` sont récupérées automatiquement. Si vous souhaitez des builds déterministes, remplacez `@main` par un SHA de commit ou un tag de version sur chaque ligne `uses:`. diff --git a/packages/twenty-docs/l/it/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/it/developers/extend/apps/logic/key-value-store.mdx index a3be3c87b6..b7903a9d8b 100644 --- a/packages/twenty-docs/l/it/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/it/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: Archivio chiave-valore -description: Rendi persistenti i risultati intermedi, memorizza nella cache i dati e condividi lo stato tra le esecuzioni delle funzioni di logica con un semplice oggetto chiave-valore. +description: Rendi persistenti i risultati intermedi, memorizza nella cache i dati e condividi lo stato tra le esecuzioni delle funzioni di logica con l'archivio chiave-valore integrato dell'applicazione. icon: database --- -Le funzioni di logica vengono eseguite in sandbox in processi Node.js di breve durata — una volta terminata un'esecuzione, nulla di ciò che era in memoria sopravvive. Quando devi **ricordare qualcosa tra un'esecuzione e l'altra** (memorizzare nella cache una costosa risposta di un'API, archiviare un cursore per sincronizzazioni incrementali, applicare un debounce al lavoro o passare lo stato da una funzione all'altra), conservalo nel database dello spazio di lavoro. +Le funzioni di logica vengono eseguite in sandbox in processi Node.js di breve durata — una volta terminata un'esecuzione, nulla di ciò che era in memoria sopravvive. Quando devi **ricordare qualcosa tra un'esecuzione e l'altra** (memorizzare nella cache una costosa risposta di un'API, archiviare un cursore per sincronizzazioni incrementali, applicare un debounce al lavoro o passare lo stato da una funzione all'altra), conservalo nell'archivio chiave-valore integrato. -Non ti serve un apposito costrutto di archiviazione per questo: un piccolo **oggetto tecnico** con un campo `key` e un campo `value` ti offre un archivio di tipo key-value durevole, con ambito limitato allo spazio di lavoro, interrogabile tramite lo stesso [client API tipizzato](/l/it/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) che usi già per i record. +Ogni applicazione dispone del proprio namespace isolato: le voci sono associate all'app autenticata, quindi le tue chiavi non potranno mai entrare in conflitto con — o essere lette da — un'altra applicazione. ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## Definisci l'oggetto store +## Get, set, delete -Dichiara un oggetto personalizzato con due campi — `key` (un `TEXT` univoco) e `value` (un `RAW_JSON` così puoi archiviare qualsiasi payload serializzabile in JSON). Consulta [Objects](/l/it/developers/extend/apps/data/objects) per la documentazione completa di `defineObject`. +Importa `kv` da `twenty-sdk/logic-function`. I valori possono essere qualsiasi payload serializzabile in JSON. -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## Ambiti + +Ogni voce ha un ambito, passato come opzione a ogni chiamata. L'impostazione predefinita è `WORKSPACE`. + +* **`WORKSPACE`** (predefinito) — la voce è privata per l'installazione corrente della tua app nel workspace. Ogni workspace che installa l'app ottiene il proprio set indipendente di chiavi. Questo è ciò che ti serve per cache, cursori e stato per workspace. +* **`SERVER`** — la voce è condivisa tra **ogni installazione** della tua app sul server. Le voci del server si comportano come **claim**: il valore memorizzato corrisponde sempre al workspaceId che ha richiesto la chiave (ometti `value` su `set` per richiedere la chiave per il workspace corrente) e solo quel workspace può sovrascriverla o eliminarla. Qualsiasi installazione può leggere la voce. + +I claim del server esistono per l'instradamento tra workspace. Un [server-route resolver](/l/it/developers/extend/apps/logic/logic-functions#server-route-trigger) viene eseguito nel workspace del proprietario della registrazione dell'applicazione, ma un webhook in ingresso di solito contiene solo un id account esterno — non un Twenty workspaceId. Fai in modo che ogni workspace richieda il proprio id esterno al momento della connessione, quindi risolvilo nella route: + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### Imponi l'univocità della chiave - -Aggiungi un **indice univoco** su `key` in modo che la stessa chiave non possa mai avere due righe. Questo è il costrutto consigliato per l'univocità — vedi [Data → Unique indexes](/l/it/developers/extend/apps/data/overview#unique-indexes). - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## Leggi e scrivi da una funzione di logica - -Incapsula l'oggetto dietro a pochi piccoli helper in modo che il resto del tuo codice assomigli a un'API key-value — `get`, `set` e `del`. Usano [`CoreApiClient`](/l/it/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk), che è generato dallo schema del tuo spazio di lavoro ed è completamente tipizzato rispetto all'oggetto `kvStore`. - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -L'indice univoco protegge dai duplicati, ma due esecuzioni che scrivono **la stessa nuova chiave** nello stesso istante possono comunque entrare in race condition tra la ricerca e la creazione. Considera una creazione che non riesce a causa del vincolo di univocità come "qualcun altro ha vinto" — intercettala e rileggi, oppure riprova come aggiornamento. - +Poiché una chiave server può essere richiesta solo per il workspace del chiamante e non può mai essere sovrascritta da un altro, un workspace non può dirottare una mappatura che appartiene a qualcun altro. `kv.set` genera un'eccezione quando la chiave è già stata richiesta da un altro workspace. ## Usalo: metti in cache una chiamata costosa @@ -155,15 +59,15 @@ Un uso tipico consiste nel mettere in cache una risposta lenta o soggetta a limi ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## Pattern e suggerimenti -* **Namespace.** Anteponi prefissi alle chiavi per tenere separate le varie aree di interesse e per rendere semplici le ricerche in blocco — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. Filtra con `key: { like: 'cache:%' }` per elencare o cancellare un intero namespace. -* **Scadenza (TTL).** Lo store non ha una scadenza integrata. Archivia un timestamp all'interno del `value` (come nell'esempio di cache) e verificalo in lettura, oppure aggiungi un campo `DATE_TIME` e cancella periodicamente le righe obsolete da una [funzione attivata da cron](/l/it/developers/extend/apps/logic/logic-functions). -* **Cosa archiviare.** `RAW_JSON` contiene qualsiasi valore serializzabile in JSON — numeri, stringhe, array, oggetti. Mantieni ridotte le dimensioni delle voci; questo meccanismo serve per coordinamento e caching, non per grandi blob o file. Per i file, usa un campo `FILES` e [`uploadFile`](/l/it/developers/extend/apps/logic/logic-functions#uploading-files). +* **Namespace.** Anteponi prefissi alle chiavi per tenere separate le varie aree di interesse — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. +* **Scadenza (TTL).** Lo store non ha una scadenza integrata. Archivia un timestamp all'interno del valore (come nell'esempio di cache) e verificalo in lettura oppure cancella le chiavi obsolete da una [funzione attivata da cron](/l/it/developers/extend/apps/logic/logic-functions). +* **Cosa archiviare.** Qualsiasi valore serializzabile in JSON — numeri, stringhe, array, oggetti. Mantieni ridotte le dimensioni delle voci; questo meccanismo serve per coordinamento e caching, non per grandi blob o file. Per i file, usa un campo `FILES` e [`uploadFile`](/l/it/developers/extend/apps/logic/logic-functions#uploading-files). +* **Visibilità.** Le voci risiedono nel database dell'istanza, non come record di workspace: non vengono mai visualizzate nell'interfaccia utente del workspace, non fanno parte del modello dati della tua app e non richiedono ruoli o autorizzazioni sugli oggetti. + +## Alternativa: un oggetto store interrogabile + +L'archivio integrato è volutamente opaco: le voci non sono record, quindi non puoi sfogliarle nell'interfaccia utente, collegarle ad altri oggetti o filtrarle con query sui record. Quando ti serve una di queste funzionalità — ad esempio un log di sincronizzazione visibile o uno stato per record — definisci invece un piccolo **oggetto tecnico** con un campo `key` univoco e un campo `value` `RAW_JSON`, e interrogalo tramite il [client API tipizzato](/l/it/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Vedi [Objects](/l/it/developers/extend/apps/data/objects) per la reference di `defineObject` e [Data → Unique indexes](/l/it/developers/extend/apps/data/overview#unique-indexes) per applicare l'univocità delle chiavi. + +* **Definire l'ambito rispetto a un record.** Aggiungi una [relation](/l/it/developers/extend/apps/data/relations) dall'oggetto store all'oggetto di destinazione invece di codificare l'id nella chiave. * **Visibilità e autorizzazioni.** Le righe risiedono nel database dello spazio di lavoro come qualsiasi altro record, quindi sono interrogabili tramite l'API e rispettano il [ruolo](/l/it/developers/extend/apps/config/roles) della tua app. Per tenere lo store fuori dall'interfaccia principale, escludilo dal tuo [navigation menu](/l/it/developers/extend/apps/layout/navigation-menu-items). -* **Limitare l'ambito a un record.** Hai bisogno di uno stato per record invece che di chiavi globali? Aggiungi una [relation](/l/it/developers/extend/apps/data/relations) dall'oggetto store all'oggetto di destinazione invece di codificare l'id nella chiave. -Questa è una convenzione, non una funzionalità separata — il "KV Store" è semplicemente un normale oggetto personalizzato che definisci e interroghi con le API standard. Questo significa che beneficia dello stesso meccanismo di sincronizzazione, delle stesse autorizzazioni e degli stessi strumenti del resto dei dati della tua app. +A differenza dell'archivio integrato, un oggetto personalizzato è sempre limitato a un solo workspace — non può condividere voci tra installazioni come fanno le chiavi `SERVER`. diff --git a/packages/twenty-docs/l/it/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/it/developers/extend/apps/logic/overview.mdx index 2e98239c92..64c38304e0 100644 --- a/packages/twenty-docs/l/it/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/it/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ Il **livello logico** di un'app Twenty è il codice che *viene eseguito* — han Credenziali OAuth che la tua app detiene per servizi di terze parti — Linear, GitHub, Slack e altri. + + Mantieni lo stato tra le esecuzioni delle funzioni di logica — cache, cursori e asserzioni tra spazi di lavoro. + ## Tipi di trigger in sintesi diff --git a/packages/twenty-docs/l/it/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/it/developers/extend/apps/operations/publishing.mdx index 35ac04974c..cd94d787cf 100644 --- a/packages/twenty-docs/l/it/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/it/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ Pubblica la tua app su npm con attestazione di provenienza quando esegui il push Su npmjs.com apri il tuo pacchetto > **Settings → Trusted Publisher** e registra questo repository con il workflow `publish.yml` (consulta la [documentazione su npm trusted publishing](https://docs.npmjs.com/trusted-publishers)). La pubblicazione con provenance certifica quale repository GitHub ha creato il pacchetto ed è anche il modo in cui dichiari la proprietà della tua app in un marketplace Twenty. + +npm accetta la provenienza solo da repository di origine **pubblici**. Se esegui la pubblicazione da un repository privato, npm rifiuta il bundle di provenienza OIDC con un `E422 ... Errore "Unsupported GitHub Actions source repository visibility: \"private\"`." Per pubblicare da un repository privato, escludi la provenienza impostando `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` nella `env` dello step di pubblicazione (nel `publish.yml` generato è incluso un suggerimento commentato): + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### Bloccare le azioni riutilizzabili I workflow `ci.yml` e `cd.yml` fanno riferimento ad azioni riutilizzabili a `@main`, quindi gli aggiornamenti delle azioni nel repository `twentyhq/twenty` vengono recepiti automaticamente. Se desideri build deterministiche, sostituisci `@main` con uno SHA di commit o un tag di release in ciascuna riga `uses:`. diff --git a/packages/twenty-docs/l/ja/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/ja/developers/extend/apps/logic/key-value-store.mdx index 2a668290ed..34eadac6c5 100644 --- a/packages/twenty-docs/l/ja/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/ja/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: キーバリューストア -description: シンプルなキーと値のオブジェクトを使って、中間結果を永続化し、データをキャッシュし、ロジック関数の実行間で状態を共有します。 +description: 組み込みのアプリケーション用キー・バリュー ストアを使って、中間結果を永続化し、データをキャッシュし、ロジック関数の実行間で状態を共有します。 icon: database --- -ロジック関数は短命な Node.js プロセス内でサンドボックス実行されます。1 回の実行が終了すると、メモリ上に保持されていたものは何も残りません。 実行間で**何かを記憶しておく必要がある**場合(高コストな API レスポンスのキャッシュ、増分同期のためのカーソルの保存、処理のデバウンス、ある関数から別の関数への状態の受け渡しなど)、ワークスペースのデータベースに永続化します。 +ロジック関数は短命な Node.js プロセス内でサンドボックス実行されます。1 回の実行が終了すると、メモリ上に保持されていたものは何も残りません。 実行間で**何かを記憶しておく必要がある**場合(高コストな API レスポンスのキャッシュ、増分同期のためのカーソルの保存、処理のデバウンス、ある関数から別の関数への状態の受け渡しなど)、組み込みのキー・バリュー ストアに永続化します。 -これには専用のストレージプリミティブは必要ありません。`key` フィールドと `value` フィールドを持つ小さな**技術的なオブジェクト**があれば、ワークスペースをスコープとした永続的なキーバリューストアになり、既にレコード用に使用しているのと同じ [型付き API クライアント](/l/ja/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk)からクエリできます。 +各アプリケーションには独自の分離された名前空間が割り当てられます。エントリは認証済みアプリ単位でキー付けされるため、別のアプリケーションとキーが衝突したり、読み取られたりすることは決してありません。 ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## ストアオブジェクトを定義する +## 取得、設定、削除 -2 つのフィールドを持つカスタムオブジェクトを宣言します — `key`(一意な `TEXT`)と `value`(任意の JSON シリアライズ可能なペイロードを保存できるようにするための `RAW_JSON`)。 完全な `defineObject` のリファレンスについては [Objects](/l/ja/developers/extend/apps/data/objects) を参照してください。 +`kv` を `twenty-sdk/logic-function` からインポートします。 値には、任意の JSON シリアライズ可能なペイロードを指定できます。 -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## スコープ + +各エントリにはスコープがあり、すべての呼び出しでオプションとして渡されます。 デフォルトは `WORKSPACE` です。 + +* **`WORKSPACE`**(デフォルト) — エントリは、アプリの現在のワークスペース インストールに対してのみプライベートです。 アプリをインストールした各ワークスペースは、独立したキーセットをそれぞれ取得します。 これは、キャッシュやカーソル、ワークスペース単位の状態に適したスコープです。 +* **`SERVER`** — エントリは、サーバー上の**すべてのインストール**間で共有されます。 サーバーエントリは **クレーム** のように動作します。保存される値は常にキーをクレームした workspaceId であり(現在のワークスペースでキーをクレームするには、`set` 時に `value` を省略します)、そのワークスペースだけが上書きや削除を行えます。 どのインストールからでも、そのエントリを読み取ることができます。 + +サーバークレームは、ワークスペース間ルーティングのために存在します。 [server-route resolver](/l/ja/developers/extend/apps/logic/logic-functions#server-route-trigger) はアプリケーション登録オーナーのワークスペース内で実行されますが、受信 Webhook は通常、外部アカウント ID のみを保持しており、Twenty の workspaceId は持っていません。 各ワークスペースが接続時に自分の外部 ID をクレームし、その後ルート内で解決します。 + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### キーの一意性を保証する - -`key` に対して**一意インデックス**を追加し、同じキーに 2 行が割り当てられないようにします。 これは一意性のために推奨されるプリミティブです。詳細は [Data → Unique indexes](/l/ja/developers/extend/apps/data/overview#unique-indexes) を参照してください。 - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## ロジック関数から読み書きする - -オブジェクトをいくつかの小さなヘルパーでラップし、残りのコードを `get`、`set`、`del` を持つキーバリュー API のように記述できるようにします。 これらは [`CoreApiClient`](/l/ja/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) を使用します。これはワークスペースのスキーマから生成され、`kvStore` オブジェクトに対して完全に型付けされています。 - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -一意インデックスは重複を防ぎますが、**同じ新しいキー**を 2 つの実行がまったく同時に書き込もうとした場合、ルックアップと作成の間で競合状態が発生する可能性があります。 作成が一意制約で失敗した場合は「他の誰かが勝った」と見なし、それをキャッチして再読み込みするか、更新としてリトライします。 - +サーバーキーは呼び出し元自身のワークスペースに対してのみクレームでき、他のワークスペースによって上書きされることはないため、あるワークスペースが他人のマッピングを乗っ取ることはできません。 キーがすでに別のワークスペースによってクレームされている場合、`kv.set` は例外をスローします。 ## 使ってみる:高コストな呼び出しをキャッシュする @@ -155,15 +59,15 @@ export const del = async (key: string): Promise => { ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## パターン & ヒント -* **ネームスペース化。** さまざまな用途を分離し、まとめてルックアップしやすくするために、キーにプレフィックスを付けます — `sync-cursor:linear`、`cache:exchange-rate:USD:EUR`、`lock:nightly-report`。 `key: { like: 'cache:%' }` でフィルタリングして、ネームスペース全体を一覧表示またはクリアします。 -* **期限 (TTL)。** ストアには組み込みの有効期限はありません。 `value` 内にタイムスタンプを保存して(キャッシュの例のように)読み取り時にチェックするか、`DATE_TIME` フィールドを追加し、[cron-triggered function](/l/ja/developers/extend/apps/logic/logic-functions) から定期的に古い行をクリアします。 -* **何を保存するか。** `RAW_JSON` には、数値、文字列、配列、オブジェクトなど、任意の JSON シリアライズ可能な値を格納できます。 エントリは小さく保ってください。これは調整やキャッシュのためのものであり、大きな BLOB やファイル用ではありません。 ファイルには `FILES` フィールドと [`uploadFile`](/l/ja/developers/extend/apps/logic/logic-functions#uploading-files) を使用してください。 +* **ネームスペース化。** さまざまな用途を分離するため、また `sync-cursor:linear`、`cache:exchange-rate:USD:EUR`、`lock:nightly-report` のように、キーにプレフィックスを付けておくとまとめてルックアップしやすくなります。 +* **期限 (TTL)。** ストアには組み込みの有効期限はありません。 値の中にタイムスタンプを保存して(キャッシュの例のように)読み取り時にチェックするか、[cron-triggered function](/l/ja/developers/extend/apps/logic/logic-functions) から古くなったキーを定期的にクリアします。 +* **何を保存するか。** 数値、文字列、配列、オブジェクトなど、任意の JSON シリアライズ可能な値を保存できます。 エントリは小さく保ってください。これは調整やキャッシュのためのものであり、大きな BLOB やファイル用ではありません。 ファイルには `FILES` フィールドと [`uploadFile`](/l/ja/developers/extend/apps/logic/logic-functions#uploading-files) を使用してください。 +* **可視性。** エントリはインスタンス データベースに保存され、ワークスペースレコードとしては存在しません。そのためワークスペースの UI に表示されることはなく、アプリのデータモデルの一部にもならず、ロールやオブジェクトの権限設定も不要です。 + +## 代替案:クエリ可能なストアオブジェクト + +組み込みストアは、あえて中身が見えないように設計されています。エントリはレコードではないため、UI で閲覧したり、他のオブジェクトと関連付けたり、レコードクエリでフィルタしたりすることはできません。 そうしたものが必要な場合、たとえば可視化された同期ログやレコード単位の状態などには、代わりに一意な `key` フィールドと `RAW_JSON` の `value` フィールドを持つ小さな**テクニカルオブジェクト**を定義し、[typed API client](/l/ja/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) を通じてクエリします。 `defineObject` のリファレンスについては [Objects](/l/ja/developers/extend/apps/data/objects) を、キーの一意性を強制する方法については [Data → Unique indexes](/l/ja/developers/extend/apps/data/overview#unique-indexes) を参照してください。 + +* **レコードへのスコープ設定。** id をキーにエンコードするのではなく、ストアオブジェクトから対象オブジェクトへの [relation](/l/ja/developers/extend/apps/data/relations) を追加して関連付けます。 * **可視性と権限。** 行は他のレコードと同様にワークスペースのデータベース内に存在するため、API を通じてクエリでき、アプリの [role](/l/ja/developers/extend/apps/config/roles) に従った権限が適用されます。 ストアをメイン UI から隠しておきたい場合は、[navigation menu](/l/ja/developers/extend/apps/layout/navigation-menu-items) に追加しないでください。 -* **レコードへのスコープ。** グローバルなキーではなく、レコード単位の状態が必要ですか? ストアオブジェクトから対象オブジェクトへの [relation](/l/ja/developers/extend/apps/data/relations) を追加し、id をキーにエンコードするのではなく関連付けを使います。 -これは約束事であり、別個の機能ではありません。「KV Store」は、標準の API で定義およびクエリする、通常のカスタムオブジェクトにすぎません。 つまり、アプリの他のデータと同様に、同じ同期、権限、ツール群の恩恵を受けられます。 +組み込みストアとは異なり、カスタムオブジェクトは常に 1 つのワークスペースにスコープされます。`SERVER` キーのように、インストール間でエントリを共有することはできません。 diff --git a/packages/twenty-docs/l/ja/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/ja/developers/extend/apps/logic/overview.mdx index 11b4f2c6da..479a38f5de 100644 --- a/packages/twenty-docs/l/ja/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/ja/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ Twenty アプリの **ロジックレイヤー** は、*実行される* コー Linear、GitHub、Slack など、サードパーティサービス向けにアプリが保持する OAuth 資格情報。 + + ロジック関数の実行間で状態を永続化します ― キャッシュ、カーソル、ワークスペース間のクレーム。 + ## トリガータイプの概要 diff --git a/packages/twenty-docs/l/ja/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/ja/developers/extend/apps/operations/publishing.mdx index 537cb90237..5cac9e5601 100644 --- a/packages/twenty-docs/l/ja/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/ja/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ tarball アプリは公開マーケットプレイスには掲載されないた npmjs.com で自分のパッケージを開き、**Settings → Trusted Publisher** から、このリポジトリを `publish.yml` ワークフローで登録します(詳しくは [npm trusted publishing docs](https://docs.npmjs.com/trusted-publishers) を参照してください)。 provenance 付きで公開すると、どの GitHub リポジトリがそのパッケージをビルドしたかが証明されます。これは、Twenty マーケットプレイスでアプリの所有権を主張する方法にもなります。 + +npm は **public** なソースリポジトリからの provenance しか受け付けません。 private リポジトリから publish しようとすると、npm は OIDC provenance バンドルを `E422 ...` エラーで拒否します。 「Unsupported GitHub Actions source repository visibility: "private"」というエラー。 private リポジトリから publish するには、publish ステップの `env` で `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` を設定して provenance を無効化します(コメントアウトされたヒントが scaffold された `publish.yml` に含まれています)。 + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### 再利用可能なアクションのバージョン固定 `ci.yml` と `cd.yml` のワークフローは、`@main` の再利用可能なアクションを参照しているため、`twentyhq/twenty` リポジトリでのアクションの更新が自動的に取り込まれます。 ビルドを決定的にしたい場合は、各 `uses:` 行の `@main` をコミット SHA またはリリースタグに置き換えてください。 diff --git a/packages/twenty-docs/l/ko/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/ko/developers/extend/apps/logic/key-value-store.mdx index 7db57f1e1c..858c58f583 100644 --- a/packages/twenty-docs/l/ko/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/ko/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: 키-값 저장소 -description: 간단한 키-값 객체를 사용하여 중간 결과를 유지하고, 데이터를 캐시하며, 논리 함수 실행 간 상태를 공유하세요. +description: 기본 제공 애플리케이션 키-값 스토어를 사용해 중간 결과를 유지하고, 데이터를 캐시하며, 논리 함수 실행 간 상태를 공유하세요. icon: database --- -로직 함수는 단기간 실행되는 샌드박스된 Node.js 프로세스에서 동작하며, 한 번 실행이 끝나면 메모리에 남아 있는 것은 아무것도 유지되지 않습니다. 실행 사이에서 **어떤 값을 기억해야 할 때**(비용이 큰 API 응답을 캐시하거나, 증분 동기화를 위한 커서를 저장하거나, 작업을 디바운스하거나, 한 함수에서 다른 함수로 상태를 넘길 때)는 워크스페이스 데이터베이스에 상태를 저장하세요. +로직 함수는 단기간 실행되는 샌드박스된 Node.js 프로세스에서 동작하며, 한 번 실행이 끝나면 메모리에 남아 있는 것은 아무것도 유지되지 않습니다. 실행 사이에서 **어떤 값을 기억해야 할 때**(비용이 큰 API 응답을 캐시하거나, 증분 동기화를 위한 커서를 저장하거나, 작업을 디바운스하거나, 한 함수에서 다른 함수로 상태를 넘길 때)는 기본 제공 키-값 스토어에 상태를 저장하세요. -이를 위해 별도의 저장용 프리미티브가 필요하지는 않습니다. `key` 필드와 `value` 필드가 있는 작은 **기술용 객체**만 있으면, 워크스페이스 범위에서 내구성이 있는 키-값 저장소를 만들 수 있으며, 이미 레코드에 사용 중인 동일한 [typed API client](/l/ko/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk)를 통해 조회할 수 있습니다. +각 애플리케이션은 자체적으로 격리된 네임스페이스를 갖습니다. 항목은 인증된 앱을 기준으로 키가 지정되므로, 키가 다른 애플리케이션과 충돌하거나 다른 애플리케이션에 의해 읽히는 일은 없습니다. ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## 스토어 객체 정의하기 +## 가져오기, 설정, 삭제 -두 개의 필드가 있는 커스텀 객체를 선언합니다. `key`(고유한 `TEXT`)와 `value`(`RAW_JSON`으로, JSON으로 직렬화할 수 있는 어떤 페이로드든 저장할 수 있음)입니다. 전체 `defineObject` 레퍼런스는 [Objects](/l/ko/developers/extend/apps/data/objects)를 참고하세요. +`twenty-sdk/logic-function`에서 `kv`를 임포트하세요. 값은 JSON으로 직렬화할 수 있는 모든 페이로드가 될 수 있습니다. -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## 범위 + +각 항목에는 스코프가 있으며, 모든 호출에서 옵션으로 전달됩니다. 기본값은 `WORKSPACE`입니다. + +* **`WORKSPACE`** (기본값) — 항목은 현재 워크스페이스에 설치된 앱 인스턴스에만 비공개입니다. 앱을 설치한 각 워크스페이스는 자체적인 독립 키 집합을 가집니다. 캐시, 커서, 워크스페이스별 상태에는 이 스코프를 사용하는 것이 적합합니다. +* **`SERVER`** — 항목은 서버에서 실행 중인 앱의 모든 설치 간에 공유됩니다. 서버 항목은 \*\*클레임(claim)\*\*처럼 동작합니다. 저장된 값은 항상 해당 키를 클레임한 workspaceId이며(`set`에서 `value`를 생략하면 현재 워크스페이스에 대해 키를 클레임합니다), 그 워크스페이스만 해당 값을 덮어쓰거나 삭제할 수 있습니다. 어떤 설치에서든 해당 항목을 읽을 수 있습니다. + +서버 클레임은 워크스페이스 간 라우팅을 위해 존재합니다. [server-route resolver](/l/ko/developers/extend/apps/logic/logic-functions#server-route-trigger)는 애플리케이션 등록 소유자 워크스페이스에서 실행되지만, 인바운드 웹후크는 일반적으로 Twenty workspaceId가 아닌 외부 계정 ID만을 포함합니다. 각 워크스페이스가 연결 시점에 자신의 외부 ID를 클레임하게 한 다음, 라우트에서 이를 해결하세요: + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### 키 고유성 강제하기 - -`key`에 **고유 인덱스**를 추가해서 동일한 키에 두 개의 행이 존재하지 못하도록 합니다. 이는 고유성을 위한 권장 프리미티브입니다. 자세한 내용은 [Data → Unique indexes](/l/ko/developers/extend/apps/data/overview#unique-indexes)를 참고하세요. - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## 로직 함수에서 읽기 및 쓰기 - -객체를 몇 개의 작은 헬퍼 뒤에 래핑해서, 나머지 코드가 `get`, `set`, `del` 같은 키-값 API를 사용하는 것처럼 보이도록 만드세요. 이들은 워크스페이스 스키마에서 생성되며 `kvStore` 객체에 대해 완전히 타입이 지정된 [`CoreApiClient`](/l/ko/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk)를 사용합니다. - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -고유 인덱스는 중복을 방지하지만, 두 번의 실행이 **동일한 새로운 키**를 정확히 같은 시점에 쓰려고 하면 조회와 생성 사이에서 여전히 경쟁 상태가 발생할 수 있습니다. 생성이 고유성 제약 조건으로 실패하면 이를 "다른 누군가가 먼저 썼다"라고 간주하고, 예외를 처리해 다시 읽거나, 업데이트로 재시도하세요. - +서버 키는 호출자의 워크스페이스에 대해서만 클레임할 수 있고 다른 워크스페이스가 덮어쓸 수 없기 때문에, 한 워크스페이스가 다른 워크스페이스에 속한 매핑을 가로채는 일은 일어날 수 없습니다. 키가 이미 다른 워크스페이스에 의해 클레임된 경우 `kv.set`은 예외를 발생시킵니다. ## 사용 예: 비용이 큰 호출 캐시하기 @@ -155,15 +59,15 @@ export const del = async (key: string): Promise => { ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## 패턴 및 팁 -* **네임스페이스 구성.** 서로 다른 관심사를 분리하고 대량 조회를 쉽게 하기 위해 키에 접두사를 붙이세요 — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. 전체 네임스페이스를 나열하거나 비우려면 `key: { like: 'cache:%' }`으로 필터링하세요. -* **만료(TTL).** 이 스토어에는 내장된 만료 기능이 없습니다. 읽을 때 확인할 수 있도록 `value` 안에 타임스탬프를 저장(캐시 예제처럼)하거나, `DATE_TIME` 필드를 추가하고 [cron-triggered function](/l/ko/developers/extend/apps/logic/logic-functions)을 통해 주기적으로 오래된 행을 정리하세요. -* **무엇을 저장할지.** `RAW_JSON`은 숫자, 문자열, 배열, 객체 등 JSON으로 직렬화 가능한 어떤 값이든 저장할 수 있습니다. 엔트리는 작게 유지하세요. 이 스토어는 대용량 blob이나 파일이 아니라, 조정 및 캐싱용입니다. 파일의 경우 `FILES` 필드와 [`uploadFile`](/l/ko/developers/extend/apps/logic/logic-functions#uploading-files)을 사용하세요. +* **네임스페이스 구성.** 서로 다른 관심사를 분리하기 위해 키에 접두사를 붙이세요 — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. +* **만료(TTL).** 이 스토어에는 내장된 만료 기능이 없습니다. 값 안에 타임스탬프를 저장해(캐시 예제처럼) 읽을 때 확인하거나, [cron-triggered function](/l/ko/developers/extend/apps/logic/logic-functions)을 통해 오래된 키를 주기적으로 정리하세요. +* **무엇을 저장할지.** 숫자, 문자열, 배열, 객체 등 JSON으로 직렬화 가능한 어떤 값이든 저장할 수 있습니다. 엔트리는 작게 유지하세요. 이 스토어는 대용량 blob이나 파일이 아니라, 조정 및 캐싱용입니다. 파일의 경우 `FILES` 필드와 [`uploadFile`](/l/ko/developers/extend/apps/logic/logic-functions#uploading-files)을 사용하세요. +* **가시성.** 항목은 워크스페이스 레코드가 아니라 인스턴스 데이터베이스에 저장됩니다. 따라서 워크스페이스 UI에 나타나지 않고, 앱의 데이터 모델의 일부도 아니며, 역할이나 객체 권한도 필요하지 않습니다. + +## 대안: 조회 가능한 스토어 객체 + +기본 제공 스토어는 의도적으로 불투명합니다. 항목이 레코드가 아니기 때문에 UI에서 탐색하거나, 다른 객체와 관계를 맺거나, 레코드 쿼리로 필터링할 수 없습니다. 이러한 기능이 필요할 때(예: 눈에 보이는 동기화 로그나 레코드별 상태 등)에는 고유한 `key` 필드와 `RAW_JSON` `value` 필드를 가진 작은 **technical object**를 정의하고, [typed API client](/l/ko/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk)를 통해 이를 쿼리하세요. `defineObject` 레퍼런스는 [Objects](/l/ko/developers/extend/apps/data/objects)를, 키의 고유성을 강제하려면 [Data → Unique indexes](/l/ko/developers/extend/apps/data/overview#unique-indexes)를 참고하세요. + +* **레코드에 스코프 지정하기.** ID를 키에 인코딩하지 말고, 스토어 객체에서 대상 객체로 [relation](/l/ko/developers/extend/apps/data/relations)을 추가하세요. * **가시성 및 권한.** 행은 다른 레코드와 마찬가지로 워크스페이스 데이터베이스에 저장되므로, API를 통해 조회할 수 있고 앱의 [role](/l/ko/developers/extend/apps/config/roles)을 그대로 따릅니다. 스토어를 기본 UI에서 숨기고 싶다면, [navigation menu](/l/ko/developers/extend/apps/layout/navigation-menu-items)에 추가하지 마세요. -* **레코드 단위 스코핑.** 전역 키 대신 레코드별 상태가 필요하신가요? 스토어 객체에서 대상 객체로 [relation](/l/ko/developers/extend/apps/data/relations)을 추가하고, id를 키에 인코딩하지 마세요. -이는 별도의 기능이 아니라 하나의 컨벤션일 뿐입니다. "KV Store"는 여러분이 정의하고 표준 API로 조회하는 일반 커스텀 객체입니다. 즉, 앱의 다른 데이터와 동일한 동기화, 권한, 도구의 이점을 모두 누릴 수 있습니다. +기본 제공 스토어와 달리 커스텀 객체는 항상 하나의 워크스페이스에만 스코프가 지정되며, `SERVER` 키처럼 설치 간에 항목을 공유할 수 없습니다. diff --git a/packages/twenty-docs/l/ko/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/ko/developers/extend/apps/logic/overview.mdx index ff03921d96..bb2faacc06 100644 --- a/packages/twenty-docs/l/ko/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/ko/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ Twenty 앱의 **로직 계층**은 *실행되는* 코드로, HTTP 요청, 크론 Linear, GitHub, Slack 등 서드파티 서비스를 위해 앱이 보유한 OAuth 자격 증명입니다. + + 로직 함수 실행 사이에 상태를 유지합니다 — 캐시, 커서, 그리고 워크스페이스 간 클레임. + ## 한눈에 보는 트리거 유형 diff --git a/packages/twenty-docs/l/ko/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/ko/developers/extend/apps/operations/publishing.mdx index f09519e00b..7c29f6ac43 100644 --- a/packages/twenty-docs/l/ko/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/ko/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ yarn twenty app:publish --private npmjs.com에서 패키지를 연 다음 **Settings → Trusted Publisher**로 이동하여 이 저장소를 `publish.yml` 워크플로에 등록하세요([npm trusted publishing 문서](https://docs.npmjs.com/trusted-publishers)를 참고하세요). provenance와 함께 게시하면 어떤 GitHub 저장소가 패키지를 빌드했는지 증명되며, 이는 Twenty 마켓플레이스에서 앱 소유권을 주장하는 방법이기도 합니다. + +npm은 **공개(public)** 소스 저장소의 provenance만 허용합니다. 비공개 저장소에서 publish를 수행하면, npm은 OIDC provenance 번들을 `E422 ...` 오류와 함께 거부합니다. 지원되지 않는 GitHub Actions 소스 저장소 가시성: "private"`오류입니다. 비공개 저장소에서 publish하려면, publish 단계의`env`에서 `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'`를 설정하여 provenance를 사용하지 않도록(opt out) 해야 합니다(스캐폴딩된 `publish.yml\`에 주석으로 된 힌트가 포함되어 있습니다): + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### 재사용 가능한 액션 고정하기 `ci.yml` 및 `cd.yml` 워크플로는 `@main`의 재사용 가능한 액션을 참조하므로, `twentyhq/twenty` 저장소의 액션 업데이트가 자동으로 반영됩니다. 결정적 빌드를 원한다면, 각 `uses:` 줄에서 `@main`을 커밋 SHA 또는 릴리스 태그로 바꾸세요. diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/logic/key-value-store.mdx index 7ec54c381b..a46ba528a5 100644 --- a/packages/twenty-docs/l/pt/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/pt/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: Armazenamento de chave-valor -description: Mantenha resultados intermediários, armazene em cache dados e compartilhe estado entre execuções de funções lógicas com um simples objeto de chave-valor. +description: Persista resultados intermediários, armazene em cache dados e compartilhe estado entre execuções de funções lógicas com o armazenamento interno de chave-valor do aplicativo. icon: database --- -Funções de lógica são executadas de forma isolada em processos Node.js de curta duração — assim que uma execução termina, nada que foi mantido na memória sobrevive. Quando você precisa **lembrar de algo entre execuções** (armazenar em cache uma resposta cara de uma API, guardar um cursor para sincronizações incrementais, aplicar debounce ao trabalho ou passar estado de uma função para outra), persista isso no banco de dados do workspace. +Funções de lógica são executadas de forma isolada em processos Node.js de curta duração — assim que uma execução termina, nada que foi mantido na memória sobrevive. Quando você precisa **lembrar de algo entre execuções** (armazenar em cache uma resposta cara de uma API, guardar um cursor para sincronizações incrementais, aplicar debounce ao trabalho ou passar estado de uma função para outra), persista isso no armazenamento interno de chave-valor. -Você não precisa de uma primitiva de armazenamento dedicada para isso: um pequeno **objeto técnico** com um campo `key` e um campo `value` oferece um armazenamento durável de chave-valor, com escopo para o workspace, consultável por meio do mesmo [cliente de API tipado](/l/pt/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) que você já usa para registros. +Cada aplicativo recebe seu próprio namespace isolado: os registros são indexados pelo app autenticado, então suas chaves nunca podem colidir com — nem ser lidas por — outro aplicativo. ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## Definir o objeto de armazenamento +## Obter, definir, excluir -Declare um objeto personalizado com dois campos — `key` (um `TEXT` único) e `value` (um `RAW_JSON`, para que você possa armazenar qualquer payload serializável em JSON). Consulte [Objects](/l/pt/developers/extend/apps/data/objects) para a referência completa de `defineObject`. +Importe `kv` de `twenty-sdk/logic-function`. Os valores podem ser quaisquer valores serializáveis em JSON. -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## Escopos + +Cada registro tem um escopo, passado como uma opção em cada chamada. O padrão é `WORKSPACE`. + +* **`WORKSPACE`** (padrão) — o registro é privado para a instalação atual do seu app no workspace. Cada workspace que instala o app recebe seu próprio conjunto independente de chaves. É isso que você quer para caches, cursores e estado por workspace. +* **`SERVER`** — o registro é compartilhado entre **todas as instalações** do seu app no servidor. Registros de servidor se comportam como **claims**: o valor armazenado é sempre o workspaceId que reivindicou a chave (omita `value` em `set` para reivindicar a chave para o workspace atual), e somente esse workspace pode sobrescrevê-la ou excluí-la. Qualquer instalação pode ler o registro. + +Claims de servidor existem para roteamento entre workspaces. Um [server-route resolver](/l/pt/developers/extend/apps/logic/logic-functions#server-route-trigger) é executado no workspace proprietário do registro do aplicativo, mas um webhook de entrada normalmente só carrega um id de conta externa — não um workspaceId do Twenty. Faça com que cada workspace reivindique seu id externo no momento da conexão e, em seguida, resolva-o na rota: + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### Aplicar unicidade à chave - -Adicione um **índice exclusivo** em `key` para que a mesma chave nunca possa ter duas linhas. Esta é a primitiva recomendada para unicidade — consulte [Data → Unique indexes](/l/pt/developers/extend/apps/data/overview#unique-indexes). - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## Ler e gravar a partir de uma função de lógica - -Encapsule o objeto por trás de alguns pequenos helpers para que o restante do seu código pareça uma API de chave-valor — `get`, `set` e `del`. Eles usam [`CoreApiClient`](/l/pt/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk), que é gerado a partir do schema do seu workspace e totalmente tipado em relação ao objeto `kvStore`. - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -O índice exclusivo protege contra duplicatas, mas duas execuções gravando a **mesma nova chave** no mesmo instante ainda podem competir entre a busca e a criação. Trate uma criação que falhar na restrição de unicidade como "alguém mais venceu" — capture o erro e leia novamente, ou tente novamente como uma atualização. - +Como uma chave de servidor só pode ser reivindicada para o próprio workspace de quem chama e nunca sobrescrita por outro, um workspace não pode sequestrar um mapeamento que pertence a outra pessoa. `kv.set` lança uma exceção quando a chave já foi reivindicada por outro workspace. ## Use-o: armazenar em cache uma chamada cara @@ -155,15 +59,15 @@ Um uso típico é armazenar em cache uma resposta lenta ou com limite de taxa de ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## Padrões e dicas -* **Namespacing.** Prefixe chaves para manter diferentes responsabilidades separadas e facilitar buscas em lote — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. Filtre com `key: { like: 'cache:%' }` para listar ou limpar todo um namespace. -* **Expiração (TTL).** O armazenamento não possui expiração integrada. Armazene um carimbo de data/hora dentro de `value` (como no exemplo de cache) e verifique-o na leitura, ou adicione um campo `DATE_TIME` e limpe periodicamente linhas obsoletas a partir de uma [função acionada por cron](/l/pt/developers/extend/apps/logic/logic-functions). -* **O que armazenar.** `RAW_JSON` comporta qualquer valor serializável em JSON — números, strings, arrays, objetos. Mantenha as entradas pequenas; isto é para coordenação e cache, não para blobs grandes ou arquivos. Para arquivos, use um campo `FILES` e [`uploadFile`](/l/pt/developers/extend/apps/logic/logic-functions#uploading-files). +* **Namespacing.** Prefixe chaves para manter diferentes responsabilidades separadas — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. +* **Expiração (TTL).** O armazenamento não possui expiração integrada. Armazene um carimbo de data/hora dentro do valor (como no exemplo de cache) e verifique-o na leitura, ou limpe chaves obsoletas a partir de uma [função acionada por cron](/l/pt/developers/extend/apps/logic/logic-functions). +* **O que armazenar.** Qualquer valor serializável em JSON — números, strings, arrays, objetos. Mantenha as entradas pequenas; isto é para coordenação e cache, não para blobs grandes ou arquivos. Para arquivos, use um campo `FILES` e [`uploadFile`](/l/pt/developers/extend/apps/logic/logic-functions#uploading-files). +* **Visibilidade.** Os registros ficam no banco de dados da instância, não como registros de workspace — eles nunca aparecem na interface do workspace, não fazem parte do modelo de dados do seu app e não precisam de permissões de função ou de objeto. + +## Alternativa: um objeto de armazenamento consultável + +O armazenamento interno é deliberadamente opaco: as entradas não são registros, então você não pode navegá-las na interface, relacioná-las a outros objetos ou filtrá-las com consultas de registros. Quando você precisar de qualquer uma dessas coisas — por exemplo, um log de sincronização visível ou estado por registro —, em vez disso defina um pequeno **objeto técnico** com um campo `key` exclusivo e um campo `value` `RAW_JSON`, e faça consultas por meio do [cliente de API tipado](/l/pt/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Veja [Objects](/l/pt/developers/extend/apps/data/objects) para a referência de `defineObject` e [Data → Unique indexes](/l/pt/developers/extend/apps/data/overview#unique-indexes) para impor exclusividade de chaves. + +* **Definindo o escopo para um registro.** Adicione uma [relation](/l/pt/developers/extend/apps/data/relations) do objeto de armazenamento para o objeto de destino em vez de codificar o id na chave. * **Visibilidade e permissões.** As linhas vivem no banco de dados do workspace como qualquer outro registro, portanto podem ser consultadas pela API e respeitam a [role](/l/pt/developers/extend/apps/config/roles) do seu app. Para manter o armazenamento fora da interface principal, deixe-o de fora do seu [menu de navegação](/l/pt/developers/extend/apps/layout/navigation-menu-items). -* **Escopo para um registro.** Precisa de estado por registro em vez de chaves globais? Adicione uma [relation](/l/pt/developers/extend/apps/data/relations) do objeto de armazenamento para o objeto de destino em vez de codificar o id na chave. -Isto é uma convenção, não um recurso separado — o "KV Store" é apenas um objeto personalizado comum que você define e consulta com a API padrão. Isso significa que ele se beneficia da mesma sincronização, permissões e ferramentas que o restante dos dados do seu app. +Ao contrário do armazenamento interno, um objeto personalizado está sempre com escopo para um único workspace — ele não consegue compartilhar registros entre instalações como as chaves `SERVER` fazem. diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/logic/overview.mdx index cacdedbf33..073219dc4a 100644 --- a/packages/twenty-docs/l/pt/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/pt/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ A **camada de lógica** de um app do Twenty é o código que *é executado* — Credenciais OAuth que seu app mantém para serviços de terceiros — Linear, GitHub, Slack e outros. + + Persistir o estado entre execuções de funções de lógica — caches, cursores e declarações entre espaços de trabalho. + ## Tipos de gatilho em resumo diff --git a/packages/twenty-docs/l/pt/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/pt/developers/extend/apps/operations/publishing.mdx index 0a363dab31..f8e6610f8a 100644 --- a/packages/twenty-docs/l/pt/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/pt/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ Publica seu app no npm com procedência quando você envia uma tag de versão (p Em npmjs.com, abra o seu pacote > **Settings → Trusted Publisher** e registre este repositório com o fluxo de trabalho `publish.yml` (consulte a [documentação de publicação confiável do npm](https://docs.npmjs.com/trusted-publishers)). Publicar com proveniência certifica qual repositório do GitHub compilou o pacote, o que também é como você reivindica a propriedade do seu app em um marketplace da Twenty. + +npm só aceita proveniência de repositórios de código-fonte **públicos**. Se você publicar a partir de um repositório privado, o npm rejeita o pacote de proveniência OIDC com um `E422 ... Erro: `Unsupported GitHub Actions source repository visibility: "private"`. Para publicar a partir de um repositório privado, desative a proveniência definindo `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'`no`env`da etapa de publicação (uma dica comentada está incluída no`publish.yml\` gerado pelo scaffold): + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### Fixando as ações reutilizáveis Os fluxos de trabalho `ci.yml` e `cd.yml` fazem referência a ações reutilizáveis em `@main`, portanto as atualizações de ações no repositório `twentyhq/twenty` são aplicadas automaticamente. Se você quiser builds determinísticos, substitua `@main` por um SHA de commit ou uma tag de release em cada linha `uses:`. diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/logic/key-value-store.mdx index 2275975bf0..e955771ea3 100644 --- a/packages/twenty-docs/l/ro/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/ro/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: Stocare cheie-valoare -description: Păstrați rezultatele intermediare, puneți datele în cache și partajați starea între rulări ale funcțiilor logice cu un obiect simplu cheie-valoare. +description: Păstrați rezultatele intermediare, puneți datele în cache și partajați starea între rulările funcțiilor logice cu spațiul de stocare cheie-valoare integrat al aplicației. icon: database --- -Funcțiile logice rulează în sandbox în procese Node.js de scurtă durată — odată ce o rulare se termină, nimic din ce a fost păstrat în memorie nu supraviețuiește. Când ai nevoie să îți amintești ceva între rulări (să păstrezi în cache un răspuns API costisitor, să stochezi un cursor pentru sincronizări incrementale, să aplici debounce sau să transmiți starea de la o funcție la alta), salvează-l în baza de date a workspace-ului. +Funcțiile logice rulează în sandbox în procese Node.js de scurtă durată — odată ce o rulare se termină, nimic din ce a fost păstrat în memorie nu supraviețuiește. Când ai nevoie să **îți amintești ceva între rulări** (să pui în cache un răspuns API costisitor, să stochezi un cursor pentru sincronizări incrementale, să aplici debounce sau să transmiți starea de la o funcție la alta), salvează-l în spațiul de stocare cheie-valoare integrat. -Nu ai nevoie de o primitivă de stocare dedicată pentru asta: un mic obiect tehnic cu un câmp `key` și un câmp `value` îți oferă o stocare cheie-valoare durabilă, limitată la workspace, care poate fi interogată prin același [client API tipizat](/l/ro/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) pe care îl folosești deja pentru înregistrări. +Fiecare aplicație primește propriul spațiu de nume izolat: intrările sunt asociate cu aplicația autentificată, astfel încât cheile tale nu pot intra niciodată în coliziune cu — sau fi citite de — o altă aplicație. ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## Definește obiectul de stocare +## Get, set, delete -Declară un obiect personalizat cu două câmpuri — `key` (un `TEXT` unic) și `value` (un `RAW_JSON` astfel încât să poți stoca orice payload serializabil JSON). Vezi [Objects](/l/ro/developers/extend/apps/data/objects) pentru referința completă `defineObject`. +Importați `kv` din `twenty-sdk/logic-function`. Valorile pot fi orice payload serializabil JSON. -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## Domenii de aplicare + +Fiecare intrare are un domeniu de aplicare, transmis ca opțiune la fiecare apel. Valoarea implicită este `WORKSPACE`. + +* **`WORKSPACE`** (implicit) — intrarea este privată pentru instalarea curentă a workspace-ului aplicației tale. Fiecare workspace care instalează aplicația primește propriul set independent de chei. Aceasta este opțiunea potrivită pentru cache-uri, cursoare și stare specifică fiecărui workspace. +* **`SERVER`** — intrarea este partajată între **fiecare instalare** a aplicației tale de pe server. Intrările de tip server se comportă ca niște **claim-uri**: valoarea stocată este întotdeauna workspaceId-ul care a revendicat cheia (omite `value` la `set` pentru a revendica cheia pentru workspace-ul curent), iar doar acel workspace o poate suprascrie sau șterge. Orice instalare poate citi intrarea. + +Revendicările de server există pentru rutare între workspace-uri. Un [server-route resolver](/l/ro/developers/extend/apps/logic/logic-functions#server-route-trigger) rulează în workspace-ul proprietar al înregistrării aplicației, dar un webhook de intrare, de obicei, conține doar un id de cont extern — nu un Twenty workspaceId. Configurați fiecare workspace să își revendice ID-ul extern la momentul conectării, apoi rezolvați-l în rută: + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### Aplică unicitatea cheii - -Adaugă un **index unic** pe `key` astfel încât aceeași cheie să nu poată avea niciodată două rânduri. Acesta este primitiva recomandată pentru unicitate — vezi [Data → Unique indexes](/l/ro/developers/extend/apps/data/overview#unique-indexes). - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## Citește și scrie dintr-o funcție logică - -Înfășoară obiectul în câțiva helperi mici astfel încât restul codului tău să se comporte ca un API de tip cheie-valoare — `get`, `set` și `del`. Aceștia folosesc [`CoreApiClient`](/l/ro/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk), care este generat din schema workspace-ului tău și este complet tipizat față de obiectul `kvStore`. - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -Indexul unic protejează împotriva duplicatelor, dar două rulări care scriu **aceeași cheie nouă** în același moment pot totuși să intre în cursă între căutare și creare. Tratează o creare care eșuează pe constrângerea de unicitate ca pe „altcineva a câștigat” — intercepteaz-o și recitește, sau reîncearcă sub formă de actualizare. - +Deoarece o cheie de tip server poate fi revendicată doar pentru propriul workspace al apelantului și nu poate fi niciodată suprascrisă de un altul, un workspace nu poate deturna o mapare care aparține altcuiva. `kv.set` aruncă o eroare atunci când cheia este deja revendicată de un alt workspace. ## Folosește-l: păstrează în cache un apel costisitor @@ -155,15 +59,15 @@ O utilizare tipică este păstrarea în cache a unui răspuns lent sau cu limita ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## Tipare și sfaturi -* **Spații de nume.** Prefixează cheile pentru a separa domeniile diferite și pentru a face ușoare căutările în masă — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. Filtrează cu `key: { like: 'cache:%' }` pentru a lista sau a curăța un întreg namespace. -* **Expiry (TTL).** Store-ul nu are expirare integrată. Stochează un timestamp în interiorul câmpului `value` (ca în exemplul de cache) și verifică-l la citire, sau adaugă un câmp `DATE_TIME` și curăță periodic rândurile vechi dintr-o [funcție declanșată de cron](/l/ro/developers/extend/apps/logic/logic-functions). -* **Ce să stochezi.** `RAW_JSON` poate conține orice valoare serializabilă JSON — numere, stringuri, array-uri, obiecte. Păstrează intrările mici; acesta este pentru coordonare și caching, nu pentru blob-uri mari sau fișiere. Pentru fișiere, folosește un câmp `FILES` și [`uploadFile`](/l/ro/developers/extend/apps/logic/logic-functions#uploading-files). +* **Spații de nume.** Prefixează cheile pentru a păstra domeniile diferite separate — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. +* **Expiry (TTL).** Store-ul nu are expirare integrată. Stochează un timestamp în interiorul valorii (ca în exemplul de cache) și verifică-l la citire sau curăță cheile învechite dintr-o [funcție declanșată de cron](/l/ro/developers/extend/apps/logic/logic-functions). +* **Ce să stochezi.** Orice valoare serializabilă JSON — numere, stringuri, array-uri, obiecte. Păstrează intrările mici; acesta este pentru coordonare și caching, nu pentru blob-uri mari sau fișiere. Pentru fișiere, folosește un câmp `FILES` și [`uploadFile`](/l/ro/developers/extend/apps/logic/logic-functions#uploading-files). +* **Vizibilitate.** Intrările există în baza de date a instanței, nu ca înregistrări ale workspace-ului — nu apar niciodată în interfața workspace-ului, nu fac parte din modelul de date al aplicației tale și nu necesită permisiuni de rol sau de obiect. + +## Alternativă: un obiect de stocare interogabil + +Spațiul de stocare integrat este, în mod deliberat, opac: intrările nu sunt înregistrări, astfel că nu le poți răsfoi în UI, nu le poți corela cu alte obiecte și nu le poți filtra cu interogări de înregistrări. Când ai nevoie de oricare dintre acestea — de exemplu un jurnal de sincronizare vizibil sau o stare per-înregistrare — definește în schimb un mic **obiect tehnic** cu un câmp `key` unic și un câmp `RAW_JSON` `value`, și interoghează-l prin [clientul API tipizat](/l/ro/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk). Vezi [Objects](/l/ro/developers/extend/apps/data/objects) pentru referința `defineObject` și [Data → Unique indexes](/l/ro/developers/extend/apps/data/overview#unique-indexes) pentru aplicarea unicității cheilor. + +* **Limitarea la o înregistrare.** Adaugă o [relație](/l/ro/developers/extend/apps/data/relations) de la obiectul de stocare la obiectul țintă, în loc să codifici id-ul în cheie. * **Vizibilitate și permisiuni.** Rândurile trăiesc în baza de date a workspace-ului ca orice altă înregistrare, astfel încât pot fi interogate prin API și respectă [rolul](/l/ro/developers/extend/apps/config/roles) aplicației tale. Pentru a ține store-ul în afara UI-ului principal, nu îl include în [navigation menu](/l/ro/developers/extend/apps/layout/navigation-menu-items). -* **Limitare la o înregistrare.** Ai nevoie de stare per înregistrare în loc de chei globale? Adaugă o [relație](/l/ro/developers/extend/apps/data/relations) de la obiectul de stocare la obiectul țintă, în loc să codifici ID-ul în cheie. -Aceasta este o convenție, nu o funcționalitate separată — „KV Store-ul” este doar un obiect personalizat obișnuit pe care îl definești și îl interoghezi cu API-ul standard. Asta înseamnă că beneficiază de același mecanism de sincronizare, aceleași permisiuni și același tooling ca restul datelor aplicației tale. +Spre deosebire de spațiul de stocare integrat, un obiect personalizat este întotdeauna limitat la un singur workspace — nu poate partaja intrări între instalări, așa cum fac cheile `SERVER`. diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/logic/overview.mdx index a02b7c1889..431f78f684 100644 --- a/packages/twenty-docs/l/ro/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/ro/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ icon: bolt Credențiale OAuth pe care aplicația ta le deține pentru servicii terțe — Linear, GitHub, Slack și altele. + + Păstrează starea între execuțiile funcțiilor de logică — cache-uri, cursoare și revendicări între spații de lucru. + ## Tipuri de declanșatoare, dintr-o privire diff --git a/packages/twenty-docs/l/ro/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/ro/developers/extend/apps/operations/publishing.mdx index 69d8530906..7099752046 100644 --- a/packages/twenty-docs/l/ro/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/ro/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ Publică aplicația pe npm, cu proveniență, când împingi un tag de versiune Pe npmjs.com deschide pachetul tău > **Settings → Trusted Publisher** și înregistrează acest repozitoriu cu fluxul de lucru `publish.yml` (vezi [documentația npm trusted publishing](https://docs.npmjs.com/trusted-publishers)). Publicarea cu provenance certifică ce repozitoriu GitHub a construit pachetul, ceea ce este, de asemenea, modul în care îți revendici proprietatea asupra aplicației tale într-un marketplace Twenty. + +npm acceptă doar proveniență din depozite sursă **publice**. Dacă publici dintr-un depozit privat, npm respinge pachetul de proveniență OIDC cu un `E422 ... Eroare „Unsupported GitHub Actions source repository visibility: "private"`.” Pentru a publica dintr-un depozit privat, renunță la proveniență setând `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` în `env` al etapei de publicare (un indiciu comentat este inclus în fișierul generat `publish.yml`): + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### Fixarea acțiunilor reutilizabile Fluxurile de lucru `ci.yml` și `cd.yml` fac referire la acțiuni reutilizabile la `@main`, astfel încât actualizările acțiunilor din repo-ul `twentyhq/twenty` sunt preluate automat. Dacă dorești builduri deterministe, înlocuiește `@main` cu un SHA de commit sau cu un tag de release pe fiecare linie `uses:`. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/logic/key-value-store.mdx index e2477eb9f3..744d1b4e60 100644 --- a/packages/twenty-docs/l/tr/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/tr/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: Anahtar-Değer Deposu -description: Basit bir anahtar-değer nesnesiyle ara sonuçları kalıcı hale getirin, verileri önbelleğe alın ve mantık işlevi çalıştırmaları arasında durumu paylaşın. +description: Yerleşik uygulama anahtar-değer deposuyla ara sonuçları kalıcı hale getirin, verileri önbelleğe alın ve mantık işlevi çalıştırmaları arasında durumu paylaşın. icon: database --- -Mantık işlevleri, kısa ömürlü, izole Node.js süreçlerinde çalışır — bir çalıştırma tamamlandıktan sonra, bellekte tutulan hiçbir şey kalıcı olmaz. Çalıştırmalar arasında **bir şeyi hatırlamanız** gerektiğinde (maliyetli bir API yanıtını önbelleğe almak, artımlı eşitlemeler için bir imleç saklamak, işleri ertelemek ya da durumu bir işlevden diğerine aktarmak için), bunu çalışma alanı veritabanında kalıcı hale getirin. +Mantık işlevleri, kısa ömürlü, izole Node.js süreçlerinde çalışır — bir çalıştırma tamamlandıktan sonra, bellekte tutulan hiçbir şey kalıcı olmaz. Çalıştırmalar arasında **bir şeyi hatırlamanız** gerektiğinde (maliyetli bir API yanıtını önbelleğe almak, artımlı eşitlemeler için bir imleç saklamak, işleri ertelemek ya da durumu bir işlevden diğerine aktarmak için), bunu yerleşik anahtar-değer deposunda kalıcı hale getirin. -Bunun için özel bir depolama ilkeline ihtiyacınız yok: `key` alanı ve `value` alanı olan küçük bir **teknik nesne**, çalışma alanıyla sınırlı, kalıcı bir anahtar-değer deposu sağlar ve kayıtlar için zaten kullandığınız aynı [typed API client](/l/tr/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) üzerinden sorgulanabilir. +Her uygulamanın kendi yalıtılmış ad alanı vardır: girişler, kimliği doğrulanmış uygulamaya göre anahtarlanır, böylece anahtarlarınız başka bir uygulamayla asla çakışamaz veya başka bir uygulama tarafından okunamaz. ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## Depo nesnesini tanımlayın +## Getir, ayarla, sil -İki alanı olan özel bir nesne tanımlayın — `key` (benzersiz bir `TEXT`) ve `value` (her türlü JSON-serileştirilebilir yükü saklayabilmeniz için bir `RAW_JSON`). Tam `defineObject` referansı için [Objects](/l/tr/developers/extend/apps/data/objects) bölümüne bakın. +`kv` öğesini `twenty-sdk/logic-function` içinden içe aktarın. Değerler, JSON olarak serileştirilebilir herhangi bir veri yükü olabilir. -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## Kapsamlar + +Her girişin, her çağrıda bir seçenek olarak iletilen bir kapsamı vardır. Varsayılan `WORKSPACE` değeridir. + +* **`WORKSPACE`** (varsayılan) — giriş, uygulamanızın geçerli çalışma alanı kurulumuna özeldir. Uygulamayı kuran her çalışma alanı kendi bağımsız anahtar setini alır. Önbellekler, imleçler ve çalışma alanı başına durum için istediğiniz budur. +* **`SERVER`** — giriş, sunucudaki uygulamanızın **her kurulumuyla** paylaşılır. Sunucu girişleri **claim** gibi davranır: saklanan değer, anahtarı talep eden çalışma alanı kimliği (geçerli çalışma alanı için anahtarı talep etmek üzere `set` çağrısında `value` belirtmeyin) olur ve yalnızca o çalışma alanı bu değeri üzerine yazabilir veya silebilir. Her kurulum girişi okuyabilir. + +Sunucu claim'leri, çalışma alanları arası yönlendirme için vardır. [server-route resolver](/l/tr/developers/extend/apps/logic/logic-functions#server-route-trigger), uygulama kaydı sahibinin çalışma alanında çalışır, ancak gelen bir web kancası genellikle yalnızca harici bir hesap kimliği taşır — bir Twenty çalışma alanı kimliği taşımaz. Her çalışma alanının, bağlanma anında kendi harici kimliğini talep etmesini sağlayın, ardından bunu rotada çözümleyin: + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### Anahtar benzersizliğini zorunlu kılın - -Aynı anahtarın asla iki satıra sahip olmaması için `key` üzerinde **benzersiz bir indeks** ekleyin. Bu, benzersizlik için önerilen ilkeldir — bkz. [Data → Unique indexes](/l/tr/developers/extend/apps/data/overview#unique-indexes). - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## Bir mantık işlevinden okuma ve yazma - -Nesneyi, geri kalan kodunuzun `get`, `set` ve `del` ile bir anahtar-değer API'si gibi okunmasını sağlamak için birkaç küçük yardımcı işlevin arkasına alın. Bunlar, çalışma alanı şemanızdan üretilen ve tamamen `kvStore` nesnesine göre türlendirilmiş [`CoreApiClient`](/l/tr/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) kullanır. - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -Benzersiz indeks çoğaltmalara karşı koruma sağlar, ancak aynı anda **aynı yeni anahtarı** yazan iki çalıştırma, yine de arama ve oluşturma arasında yarışabilir. Benzersizlik kısıtlamasında başarısız olan bir oluşturma işlemini "başka biri kazandı" olarak ele alın — hatayı yakalayın ve yeniden okuyun ya da bir güncelleme olarak tekrar deneyin. - +Bir sunucu anahtarı yalnızca çağıranın kendi çalışma alanı için talep edilebildiğinden ve başka bir çalışma alanı tarafından asla üzerine yazılamadığından, bir çalışma alanı başkasına ait bir eşlemeyi ele geçiremez. Anahtar zaten başka bir çalışma alanı tarafından talep edilmişse `kv.set` hata fırlatır. ## Kullanın: maliyetli bir çağrıyı önbelleğe alın @@ -155,15 +59,15 @@ Yaygın bir kullanım, yavaş veya hız sınırına tabi üçüncü taraf yanıt ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## Kalıplar ve ipuçları -* **Ad alanları.** Farklı konuları birbirinden ayırmak ve toplu aramaları kolaylaştırmak için anahtarlara ön ek ekleyin — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. Bir ad alanının tamamını listelemek veya temizlemek için `key: { like: 'cache:%' }` ile filtreleyin. -* **Sona erme (TTL).** Depoda yerleşik bir sona erme özelliği yoktur. Okuma sırasında kontrol etmek için `value` içinde bir zaman damgası saklayın (önbellek örneğinde olduğu gibi) veya bir `DATE_TIME` alanı ekleyip, düzenli aralıklarla [cron-triggered function](/l/tr/developers/extend/apps/logic/logic-functions) içinden eski satırları temizleyin. -* **Ne saklanmalı.** `RAW_JSON`, sayılar, dizeler, diziler ve nesneler gibi JSON-serileştirilebilir herhangi bir değeri tutar. Kayıtları küçük tutun; bu, koordinasyon ve önbelleğe alma içindir, büyük blob'lar veya dosyalar için değil. Dosyalar için bir `FILES` alanı ve [`uploadFile`](/l/tr/developers/extend/apps/logic/logic-functions#uploading-files) kullanın. +* **Ad alanları.** Farklı konuları birbirinden ayırmak için anahtarlara ön ek ekleyin — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. +* **Sona erme (TTL).** Depoda yerleşik bir sona erme özelliği yoktur. Okuma sırasında kontrol etmek için değerin içinde (önbellek örneğinde olduğu gibi) bir zaman damgası saklayın veya eski anahtarları bir [cron-triggered function](/l/tr/developers/extend/apps/logic/logic-functions) içinden temizleyin. +* **Ne saklanmalı.** Sayılar, dizeler, diziler, nesneler gibi JSON olarak serileştirilebilir herhangi bir değer. Kayıtları küçük tutun; bu, koordinasyon ve önbelleğe alma içindir, büyük blob'lar veya dosyalar için değil. Dosyalar için bir `FILES` alanı ve [`uploadFile`](/l/tr/developers/extend/apps/logic/logic-functions#uploading-files) kullanın. +* **Görünürlük.** Girişler, çalışma alanı kayıtları olarak değil, instance veritabanında tutulur — çalışma alanı arayüzünde asla görünmezler, uygulamanızın veri modelinin parçası değildirler ve herhangi bir rol veya nesne iznine ihtiyaç duymazlar. + +## Alternatif: sorgulanabilir bir depo nesnesi + +Yerleşik depo kasıtlı olarak opaktır: girişler kayıt değildir, bu nedenle onları arayüzde gezemez, diğer nesnelerle ilişkilendiremez veya kayıt sorgularıyla filtreleyemezsiniz. Bunlardan herhangi birine ihtiyaç duyduğunuzda — örneğin görünür bir eşitleme günlüğü veya kayıt başına durum — bunun yerine benzersiz bir `key` alanına ve bir `RAW_JSON` `value` alanına sahip küçük bir **teknik nesne** tanımlayın ve onu [typed API client](/l/tr/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) aracılığıyla sorgulayın. `defineObject` başvurusu için [Objects](/l/tr/developers/extend/apps/data/objects) bölümüne ve anahtar benzersizliğini zorlamak için [Data → Unique indexes](/l/tr/developers/extend/apps/data/overview#unique-indexes) bölümüne bakın. + +* **Bir kayda kapsam belirleme.** Kimliği anahtarın içine kodlamak yerine, depo nesnesinden hedef nesneye bir [relation](/l/tr/developers/extend/apps/data/relations) ekleyin. * **Görünürlük ve izinler.** Satırlar, diğer kayıtlar gibi çalışma alanı veritabanında yaşar; bu nedenle API üzerinden sorgulanabilirler ve uygulamanızın [role](/l/tr/developers/extend/apps/config/roles) yapılandırmasına uyarlar. Depoyu ana arayüzün dışında tutmak için, onu [navigation menu](/l/tr/developers/extend/apps/layout/navigation-menu-items) dışında bırakın. -* **Bir kayda kapsamlamak.** Genel anahtarlar yerine kayıt başına duruma mı ihtiyacınız var? Kimliği anahtarın içine kodlamak yerine, depo nesnesinden hedef nesneye bir [relation](/l/tr/developers/extend/apps/data/relations) ekleyin. -Bu bir kuraldır, ayrı bir özellik değildir — "KV Store", sizin tanımladığınız ve standart API ile sorguladığınız normal bir özel nesnedir. Bu da, uygulamanızın verilerinin geri kalanıyla aynı eşitleme, izinler ve araç setinden yararlanması anlamına gelir. +Yerleşik depodan farklı olarak, özel bir nesne her zaman tek bir çalışma alanına kapsamlanır — `SERVER` anahtarlarının yaptığı gibi kurulumlar arasında girişleri paylaşamaz. diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/logic/overview.mdx index 04d855b7c0..143667b8f5 100644 --- a/packages/twenty-docs/l/tr/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/tr/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ Bir Twenty uygulamasının **mantık katmanı**, *çalışan* koddur — HTTP is Uygulamanızın üçüncü taraf servisler — Linear, GitHub, Slack ve daha fazlası — için tuttuğu OAuth kimlik bilgileri. + + Mantık fonksiyonu çalıştırmaları arasında durumu kalıcı hale getirin — önbellekler, imleçler ve çalışma alanları arası beyanlar. + ## Tetikleyici türlerine genel bakış diff --git a/packages/twenty-docs/l/tr/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/tr/developers/extend/apps/operations/publishing.mdx index 402e2645b5..db1f5b768d 100644 --- a/packages/twenty-docs/l/tr/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/tr/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ Bir sürüm etiketi (örn. `v1.0.0`) gönderdiğinizde veya Actions sekmesinden npmjs.com’da paketinizin sayfasını açın > **Settings → Trusted Publisher** bölümüne gidin ve bu depoyu `publish.yml` iş akışıyla kaydedin ([npm trusted publishing belgelerine](https://docs.npmjs.com/trusted-publishers) bakın). Provenance ile yayımlama, paketi hangi GitHub deposunun oluşturduğunu doğrular; ayrıca Twenty pazaryerinde uygulamanızın sahipliğini bu şekilde talep edersiniz. + +npm yalnızca **herkese açık** kaynak depolarından gelen provenansı kabul eder. Özel bir depodan yayınlarsanız, npm OIDC provenans paketini `E422 ... Desteklenmeyen GitHub Actions kaynak deposu görünürlüğü: "private"` hatası. Özel bir depodan yayınlamak için, yayın adımının `env` bölümünde `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` ayarını yaparak provenansı devre dışı bırakın (hazırlanmış `publish.yml` içinde yorum satırı olarak eklenmiş bir ipucu bulunur): + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### Yeniden kullanılabilir eylemleri sabitleme `ci.yml` ve `cd.yml` iş akışları, `@main` üzerindeki yeniden kullanılabilir eylemlere başvurur; bu nedenle `twentyhq/twenty` deposundaki eylem güncellemeleri otomatik olarak alınır. Deterministik derlemeler istiyorsanız, her `uses:` satırında `@main` ifadesini bir commit SHA'sı veya sürüm etiketiyle değiştirin. diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/logic/key-value-store.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/logic/key-value-store.mdx index 358a32127a..a37868a210 100644 --- a/packages/twenty-docs/l/zh/developers/extend/apps/logic/key-value-store.mdx +++ b/packages/twenty-docs/l/zh/developers/extend/apps/logic/key-value-store.mdx @@ -1,153 +1,57 @@ --- title: 键值存储 -description: 使用简单的键值对象持久化中间结果、缓存数据,并在逻辑函数运行之间共享状态。 +description: 使用内置的应用键值存储持久化中间结果、缓存数据,并在多次逻辑函数运行之间共享状态。 icon: database --- -逻辑函数在短生命周期的 Node.js 进程中以沙盒方式运行——一旦一次运行结束,内存中不会保留任何内容。 当你需要在**多次运行之间记住一些东西**时(缓存一次昂贵的 API 响应、存储增量同步的游标、对工作进行防抖处理,或在函数之间传递状态),请将其持久化到工作区数据库中。 +逻辑函数在短生命周期的 Node.js 进程中以沙盒方式运行——一旦一次运行结束,内存中不会保留任何内容。 当你需要在**多次运行之间记住一些东西**时(缓存一次昂贵的 API 响应、存储增量同步的游标、对工作进行防抖处理,或在函数之间传递状态),请将其持久化到内置键值存储中。 -你不需要专门的存储原语来实现这一点:一个带有 `key` 字段和 `value` 字段的小型**技术对象**就可以为你提供一个持久的键值存储,它以工作区为作用域,并且可以通过你已经用于记录的同一个[类型化 API 客户端](/l/zh/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk)进行查询。 +每个应用都会获得自己隔离的命名空间:条目根据已认证的应用进行键控,因此你的键永远不会与其他应用发生冲突,也不会被其他应用读取。 ```text - ┌─────────────────┐ set(key, value) ┌──────────────────────────┐ - │ Logic function │ ───────────────────▶ │ "KV Store" object │ - │ (your handler) │ ◀─────────────────── │ key (unique) │ value │ - └─────────────────┘ get(key) └──────────────────────────┘ + ┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐ + │ Logic function │ ─────────────────────▶ │ Application KV store │ + │ (your handler) │ ◀───────────────────── │ key (unique) │ value │ + └─────────────────┘ kv.get(key) └──────────────────────────┘ ``` -## 定义存储对象 +## 获取、设置、删除 -声明一个具有两个字段的自定义对象——`key`(唯一的 `TEXT`)和 `value`(`RAW_JSON`,因此你可以存储任何可序列化为 JSON 的有效负载)。 完整的 `defineObject` 参考请参见[对象](/l/zh/developers/extend/apps/data/objects)。 +从 `twenty-sdk/logic-function` 导入 `kv`。 值可以是任何可序列化为 JSON 的负载。 -```ts src/objects/kv-store.object.ts -import { defineObject, FieldType } from 'twenty-sdk/define'; +```ts src/logic-functions/sync-linear-issues.ts +import { kv } from 'twenty-sdk/logic-function'; -export const KV_STORE_UNIVERSAL_IDENTIFIER = - '2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33'; -export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER = - '4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21'; -export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER = - '8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58'; +// Read a value. Returns null when the key is missing. +const cursor = await kv.get('sync-cursor:linear'); -export default defineObject({ - universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - nameSingular: 'kvStore', - namePlural: 'kvStores', - labelSingular: 'KV Store', - labelPlural: 'KV Store', - description: 'Key-value storage for logic functions', - icon: 'IconDatabase', - fields: [ - { - universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - name: 'key', - type: FieldType.TEXT, - label: 'Key', - description: 'Unique lookup key', - icon: 'IconKey', - }, - { - universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER, - name: 'value', - type: FieldType.RAW_JSON, - label: 'Value', - description: 'Stored JSON payload', - icon: 'IconJson', - }, - ], +// Write a value. Creates the entry on first write, updates it afterwards. +await kv.set('sync-cursor:linear', newCursor); + +// Delete an entry. Returns true when an entry was removed. +await kv.delete('sync-cursor:linear'); +``` + +## 范围 + +每个条目都有一个作用域,在每次调用时作为选项传入。 默认值为 `WORKSPACE`。 + +* **`WORKSPACE`**(默认)— 条目对当前工作区中安装的应用是私有的。 每个安装该应用的工作区都会获得自己独立的一组键。 这正是缓存、游标和按工作区存储状态时所需要的。 +* **`SERVER`** — 条目在服务器上应用的**每一次安装**之间共享。 服务器条目的行为类似**声明(claim)**:存储的值始终是声明该键的 workspaceId(在 `set` 时省略 `value` 即可为当前工作区声明该键),并且只有该工作区可以覆盖或删除它。 任何一次安装都可以读取该条目。 + +服务器声明用于跨工作区路由。 [服务器路由解析器](/l/zh/developers/extend/apps/logic/logic-functions#server-route-trigger)在应用注册所有者的工作区中运行,但入站 Webhook 通常只携带外部账户 ID——而不是 Twenty 的 workspaceId。 让每个工作区在连接时声明其外部 id,然后在路由中解析它: + +```ts +// In the connected workspace, when the external account is linked: +await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' }); + +// In the server-route resolver (owner workspace), on each webhook: +const workspaceId = await kv.get(`slack:team:${teamId}`, { + scope: 'SERVER', }); ``` -### 强制键唯一性 - -在 `key` 上添加一个**唯一索引**,这样同一个键就永远不会有两行。 这是实现唯一性的推荐原语——参见[数据 → 唯一索引](/l/zh/developers/extend/apps/data/overview#unique-indexes)。 - -```ts src/indexes/kv-store-key.index.ts -import { defineIndex } from 'twenty-sdk/define'; -import { - KV_STORE_UNIVERSAL_IDENTIFIER, - KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, -} from '../objects/kv-store.object'; - -export default defineIndex({ - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14', - objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER, - isUnique: true, - fields: [ - { - universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15', - fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER, - }, - ], -}); -``` - -## 从逻辑函数中读写 - -用几个小型的辅助函数来封装该对象,这样其余代码用起来就像键值 API 一样——`get`、`set` 和 `del`。 它们使用 [`CoreApiClient`](/l/zh/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk),该客户端由你的工作区模式生成,并针对 `kvStore` 对象提供完整的类型。 - -```ts src/logic-functions/handlers/kv-store.ts -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { isDefined } from 'twenty-sdk/utils'; - -const client = new CoreApiClient(); - -// Look up a single row by its key. -const findByKey = async (key: string) => { - const { kvStores } = await client.query({ - kvStores: { - __args: { filter: { key: { eq: key } }, first: 1 }, - edges: { node: { id: true, value: true } }, - }, - }); - - return kvStores.edges[0]?.node; -}; - -// Read a value. Returns undefined when the key is missing. -export const get = async (key: string): Promise => { - const row = await findByKey(key); - - return isDefined(row) ? (row.value as TValue) : undefined; -}; - -// Write a value. Creates the row on first write, updates it afterwards (upsert). -export const set = async (key: string, value: unknown): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - updateKvStore: { - __args: { id: existing.id, data: { value } }, - id: true, - }, - }); - return; - } - - await client.mutation({ - createKvStore: { - __args: { data: { key, value } }, - id: true, - }, - }); -}; - -// Delete a value. No-op when the key is missing. -export const del = async (key: string): Promise => { - const existing = await findByKey(key); - - if (isDefined(existing)) { - await client.mutation({ - deleteKvStore: { __args: { id: existing.id }, id: true }, - }); - } -}; -``` - - -唯一索引可以防止重复,但在查找和创建之间,两个运行在同一时刻写入**相同的新键**时仍然可能发生竞争。 将因唯一性约束失败的创建视为“被其他人抢先了”——捕获该错误并重新读取,或改为重试执行更新。 - +因为服务器键只能为调用方自己的工作区声明,且永远不会被其他工作区覆盖,所以某个工作区无法劫持属于其他工作区的映射。 当某个键已经被其他工作区声明时,`kv.set` 会抛出异常。 ## 使用示例:缓存一次昂贵的调用 @@ -155,15 +59,15 @@ export const del = async (key: string): Promise => { ```ts src/logic-functions/getExchangeRate.logic-function.ts import { defineLogicFunction } from 'twenty-sdk/define'; -import { get, set } from './handlers/kv-store'; +import { kv } from 'twenty-sdk/logic-function'; const ONE_HOUR_MS = 60 * 60 * 1000; type CachedRate = { rate: number; fetchedAt: number }; const handler = async (params: { from: string; to: string }) => { - const cacheKey = `exchange-rate:${params.from}:${params.to}`; - const cached = await get(cacheKey); + const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`; + const cached = await kv.get(cacheKey); if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) { return { rate: cached.rate, cached: true }; @@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => { ); const { rate } = (await response.json()) as { rate: number }; - await set(cacheKey, { rate, fetchedAt: Date.now() }); + await kv.set(cacheKey, { rate, fetchedAt: Date.now() }); return { rate, cached: false }; }; @@ -189,12 +93,18 @@ export default defineLogicFunction({ ## 模式与技巧 -* **命名空间划分。** 给键添加前缀,以区分不同的用途,并便于批量查找——`sync-cursor:linear`、`cache:exchange-rate:USD:EUR`、`lock:nightly-report`。 使用 `key: { like: 'cache:%' }` 进行过滤,以列出或清理整个命名空间。 -* **过期时间(TTL)。** 该存储本身不带有过期机制。 在 `value` 中存储时间戳(如缓存示例中所示)并在读取时检查,或者添加一个 `DATE_TIME` 字段,并通过[定时任务触发的函数](/l/zh/developers/extend/apps/logic/logic-functions)定期清理陈旧的行。 -* **存什么。** `RAW_JSON` 可以存储任何可序列化为 JSON 的值——数字、字符串、数组、对象。 保持条目足够小;此存储用于协调和缓存,而不是用于存放大型二进制对象或文件。 对于文件,请使用 `FILES` 字段和 [`uploadFile`](/l/zh/developers/extend/apps/logic/logic-functions#uploading-files)。 +* **命名空间划分。** 给键添加前缀,以区分不同的用途——`sync-cursor:linear`、`cache:exchange-rate:USD:EUR`、`lock:nightly-report`。 +* **过期时间(TTL)。** 该存储本身不带有过期机制。 在值中存储时间戳(如缓存示例中所示)并在读取时检查,或者通过[定时任务触发的函数](/l/zh/developers/extend/apps/logic/logic-functions)清理陈旧的键。 +* **存什么。** 任何可序列化为 JSON 的值——数字、字符串、数组、对象。 保持条目足够小;此存储用于协调和缓存,而不是用于存放大型二进制对象或文件。 对于文件,请使用 `FILES` 字段和 [`uploadFile`](/l/zh/developers/extend/apps/logic/logic-functions#uploading-files)。 +* **可见性。** 条目存在于实例数据库中,而不是作为工作区记录存在——它们不会出现在工作区 UI 中,不属于你应用的数据模型,也不需要角色或对象权限。 + +## 可选方案:可查询的存储对象 + +内置存储是刻意设计为不透明的:条目不是记录,因此你无法在 UI 中浏览它们、将它们与其他对象关联,或通过记录查询对它们进行筛选。 当你需要这些能力时——比如可见的同步日志或逐条记录的状态——请定义一个小型的**技术对象**,其中包含唯一的 `key` 字段和一个 `RAW_JSON` 类型的 `value` 字段,并通过[类型化 API 客户端](/l/zh/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk)对其进行查询。 `defineObject` 的参考请参见 [Objects](/l/zh/developers/extend/apps/data/objects),关于强制键唯一性请参见 [Data → Unique indexes](/l/zh/developers/extend/apps/data/overview#unique-indexes)。 + +* **将作用域限定到记录。** 从存储对象到目标对象添加一个[关系](/l/zh/developers/extend/apps/data/relations),而不是把 id 编码进键中。 * **可见性与权限。** 这些行像任何其他记录一样存放在工作区数据库中,因此可以通过 API 查询,并遵循你的应用[角色](/l/zh/developers/extend/apps/config/roles)设置。 要将存储从主 UI 中隐藏,只需不要把它加入到你的[导航菜单](/l/zh/developers/extend/apps/layout/navigation-menu-items)中。 -* **作用域到记录。** 需要针对每条记录的状态,而不是全局键吗? 从存储对象到目标对象添加一个[关系](/l/zh/developers/extend/apps/data/relations),而不是把 id 编码进键中。 -这是一种约定,而不是一个单独的功能——“KV Store” 只是你定义并通过标准 API 查询的常规自定义对象。 这意味着它可以像你的应用中其他数据一样,受益于相同的同步机制、权限控制和工具链。 +与内置存储不同,自定义对象始终作用于单个工作区——它无法像 `SERVER` 键那样在安装之间共享条目。 diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/logic/overview.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/logic/overview.mdx index 6148bc6292..ce779aaa68 100644 --- a/packages/twenty-docs/l/zh/developers/extend/apps/logic/overview.mdx +++ b/packages/twenty-docs/l/zh/developers/extend/apps/logic/overview.mdx @@ -34,6 +34,9 @@ Twenty 应用的 **逻辑层** 是实际*运行*的代码——用于响应 HTTP 你的应用为第三方服务(如 Linear、GitHub、Slack 等)持有的 OAuth 凭证。 + + 在逻辑函数的多次运行之间持久化状态——缓存、游标和跨工作区声明。 + ## 触发器类型一览 diff --git a/packages/twenty-docs/l/zh/developers/extend/apps/operations/publishing.mdx b/packages/twenty-docs/l/zh/developers/extend/apps/operations/publishing.mdx index 651a549741..c17f3b3269 100644 --- a/packages/twenty-docs/l/zh/developers/extend/apps/operations/publishing.mdx +++ b/packages/twenty-docs/l/zh/developers/extend/apps/operations/publishing.mdx @@ -170,6 +170,17 @@ yarn twenty app:publish --private 在 npmjs.com 上打开你的包 > **Settings → Trusted Publisher**,并使用 `publish.yml` 工作流为此仓库注册(参见 [npm trusted publishing 文档](https://docs.npmjs.com/trusted-publishers))。 带有 provenance 的发布会证明是哪个 GitHub 仓库构建了该包,这也同样是你在 Twenty 市场中声明应用所有权的方式。 + +npm 仅接受来自**公共**源代码仓库的 provenance。 如果你从私有仓库发布,npm 会以 `E422 ...` 错误拒绝 OIDC provenance 包。 Unsupported GitHub Actions source repository visibility: "private"`错误。 要从私有仓库进行发布,请在发布步骤的`env`中将`TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'`设置为退出 provenance(在脚手架生成的`publish.yml\` 中包含了一条已注释的提示): + +```yaml filename=".github/workflows/publish.yml" + - name: Publish to npm + env: + TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true' + run: yarn twenty app:publish +``` + + ### 固定可复用的 actions `ci.yml` 和 `cd.yml` 工作流引用了 `@main` 上的可复用 actions,因此会自动获取 `twentyhq/twenty` 仓库中的 action 更新。 如果你希望构建具有确定性,请在每个 `uses:` 行中将 `@main` 替换为某个提交的 SHA 或发行标签。