i18n - docs translations (#22068)

Created by Github action

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22068?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
github-actions[bot]
2026-06-24 13:41:48 +02:00
committed by GitHub
parent 5ca41d55fb
commit 5ff1d997c7
13 changed files with 2412 additions and 0 deletions
+12
View File
@@ -843,6 +843,7 @@
"pages": [
"l/fr/developers/extend/apps/logic/overview",
"l/fr/developers/extend/apps/logic/logic-functions",
"l/fr/developers/extend/apps/logic/key-value-store",
"l/fr/developers/extend/apps/logic/skills-and-agents",
"l/fr/developers/extend/apps/logic/connections"
]
@@ -1276,6 +1277,7 @@
"pages": [
"l/ar/developers/extend/apps/logic/overview",
"l/ar/developers/extend/apps/logic/logic-functions",
"l/ar/developers/extend/apps/logic/key-value-store",
"l/ar/developers/extend/apps/logic/skills-and-agents",
"l/ar/developers/extend/apps/logic/connections"
]
@@ -1709,6 +1711,7 @@
"pages": [
"l/cs/developers/extend/apps/logic/overview",
"l/cs/developers/extend/apps/logic/logic-functions",
"l/cs/developers/extend/apps/logic/key-value-store",
"l/cs/developers/extend/apps/logic/skills-and-agents",
"l/cs/developers/extend/apps/logic/connections"
]
@@ -2142,6 +2145,7 @@
"pages": [
"l/de/developers/extend/apps/logic/overview",
"l/de/developers/extend/apps/logic/logic-functions",
"l/de/developers/extend/apps/logic/key-value-store",
"l/de/developers/extend/apps/logic/skills-and-agents",
"l/de/developers/extend/apps/logic/connections"
]
@@ -2575,6 +2579,7 @@
"pages": [
"l/es/developers/extend/apps/logic/overview",
"l/es/developers/extend/apps/logic/logic-functions",
"l/es/developers/extend/apps/logic/key-value-store",
"l/es/developers/extend/apps/logic/skills-and-agents",
"l/es/developers/extend/apps/logic/connections"
]
@@ -3008,6 +3013,7 @@
"pages": [
"l/it/developers/extend/apps/logic/overview",
"l/it/developers/extend/apps/logic/logic-functions",
"l/it/developers/extend/apps/logic/key-value-store",
"l/it/developers/extend/apps/logic/skills-and-agents",
"l/it/developers/extend/apps/logic/connections"
]
@@ -3769,6 +3775,7 @@
"pages": [
"l/ko/developers/extend/apps/logic/overview",
"l/ko/developers/extend/apps/logic/logic-functions",
"l/ko/developers/extend/apps/logic/key-value-store",
"l/ko/developers/extend/apps/logic/skills-and-agents",
"l/ko/developers/extend/apps/logic/connections"
]
@@ -4202,6 +4209,7 @@
"pages": [
"l/pt/developers/extend/apps/logic/overview",
"l/pt/developers/extend/apps/logic/logic-functions",
"l/pt/developers/extend/apps/logic/key-value-store",
"l/pt/developers/extend/apps/logic/skills-and-agents",
"l/pt/developers/extend/apps/logic/connections"
]
@@ -4635,6 +4643,7 @@
"pages": [
"l/ro/developers/extend/apps/logic/overview",
"l/ro/developers/extend/apps/logic/logic-functions",
"l/ro/developers/extend/apps/logic/key-value-store",
"l/ro/developers/extend/apps/logic/skills-and-agents",
"l/ro/developers/extend/apps/logic/connections"
]
@@ -5068,6 +5077,7 @@
"pages": [
"l/ru/developers/extend/apps/logic/overview",
"l/ru/developers/extend/apps/logic/logic-functions",
"l/ru/developers/extend/apps/logic/key-value-store",
"l/ru/developers/extend/apps/logic/skills-and-agents",
"l/ru/developers/extend/apps/logic/connections"
]
@@ -5501,6 +5511,7 @@
"pages": [
"l/tr/developers/extend/apps/logic/overview",
"l/tr/developers/extend/apps/logic/logic-functions",
"l/tr/developers/extend/apps/logic/key-value-store",
"l/tr/developers/extend/apps/logic/skills-and-agents",
"l/tr/developers/extend/apps/logic/connections"
]
@@ -5934,6 +5945,7 @@
"pages": [
"l/zh/developers/extend/apps/logic/overview",
"l/zh/developers/extend/apps/logic/logic-functions",
"l/zh/developers/extend/apps/logic/key-value-store",
"l/zh/developers/extend/apps/logic/skills-and-agents",
"l/zh/developers/extend/apps/logic/connections"
]
@@ -0,0 +1,200 @@
---
title: مخزن المفاتيح-القيم
description: احتفظ بالنتائج الوسيطة، وخزّن البيانات مؤقتًا، وشارك الحالة عبر تشغيلات دوال المنطق باستخدام كائن بسيط من نوع مفاتيح-قيم.
icon: قاعدة البيانات
---
تعمل دوال المنطق داخل عمليات 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) └──────────────────────────┘
```
## عرّف كائن المخزن
عرِّف كائنًا مخصصًا بحقلين — `key` (حقل `TEXT` فريد) و `value` (حقل `RAW_JSON` حتى تتمكن من تخزين أي حمولة قابلة للتسلسل إلى JSON). راجع [Objects](/l/ar/developers/extend/apps/data/objects) للحصول على مرجع `defineObject` الكامل.
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### فرض تفرد المفتاح
أضف **فهرسًا فريدًا** على `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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
يحمي الفهرس الفريد من التكرارات، لكن عمليتي تشغيل تكتبان **نفس المفتاح الجديد** في اللحظة نفسها يمكن أن تتسابقا بين عملية البحث والإنشاء. اعتبر أن عملية الإنشاء التي تفشل بسبب قيد التفرد تعني "أن طرفًا آخر سبقك" — التقط هذا الخطأ وأعد القراءة، أو أعد المحاولة كعملية تحديث.
</Note>
## استخدمه: خزّن استدعاء مكلفًا في الذاكرة المؤقتة
استخدام شائع هو التخزين المؤقت لاستجابة جهة خارجية بطيئة أو مقيدة بمعدل معيّن حتى تعيد عمليات التشغيل المتكررة استخدامها بدلًا من دفع التكلفة في كل مرة.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## أنماط ونصائح
* **مساحات الأسماء.** أضف بادئة إلى المفاتيح للحفاظ على فصل الاهتمامات المختلفة ولتسهيل عمليات البحث المجمّعة — `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).
* **الرؤية والصلاحيات.** تعيش الصفوف في قاعدة بيانات مساحة العمل مثل أي سجل آخر، لذا يمكن الاستعلام عنها عبر واجهة برمجة التطبيقات وتلتزم [بدور](/l/ar/developers/extend/apps/config/roles) التطبيق لديك. لإبقاء المخزن خارج واجهة المستخدم الرئيسية، اتركه خارج [قائمة التنقل](/l/ar/developers/extend/apps/layout/navigation-menu-items).
* **تحديد النطاق على سجل معيّن.** هل تحتاج إلى حالة لكل سجل بدلًا من مفاتيح عالمية؟ أضف [علاقة](/l/ar/developers/extend/apps/data/relations) من كائن المخزن إلى الكائن المستهدف بدلًا من ترميز المعرّف داخل المفتاح.
<Note>
هذا عُرف وليس ميزة منفصلة — "مخزن المفاتيح-القيم (KV Store)" هو مجرد كائن مخصص عادي تقوم بتعريفه والاستعلام عنه باستخدام واجهة برمجة التطبيقات القياسية. هذا يعني أنه يستفيد من نفس آليات المزامنة والصلاحيات والأدوات مثل باقي بيانات تطبيقك.
</Note>
@@ -0,0 +1,200 @@
---
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.
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.
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.
```text
┌─────────────────┐ set(key, value) ┌──────────────────────────┐
│ Logic function │ ───────────────────▶ │ "KV Store" object │
│ (your handler) │ ◀─────────────────── │ key (unique) │ value │
└─────────────────┘ get(key) └──────────────────────────┘
```
## Definujte objekt úložiště
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).
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### 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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
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.
</Note>
## Použití: kešujte nákladné volání
Typickým použitím je kešování pomalé nebo omezované (rate-limited) odpovědi třetí strany, aby opakované běhy znovu použily výsledek místo placení nákladů při každém spuštění.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## 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).
* **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.
<Note>
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.
</Note>
@@ -0,0 +1,200 @@
---
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.
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.
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.
```text
┌─────────────────┐ set(key, value) ┌──────────────────────────┐
│ Logic function │ ───────────────────▶ │ "KV Store" object │
│ (your handler) │ ◀─────────────────── │ key (unique) │ value │
└─────────────────┘ get(key) └──────────────────────────┘
```
## Das Store-Objekt definieren
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.
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### 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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
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.
</Note>
## Verwenden Sie ihn: einen teuren Aufruf zwischenspeichern
Eine typische Verwendung ist das Zwischenspeichern einer langsamen oder ratelimitierten Antwort eines Drittanbieters, sodass wiederholte Durchläufe sie wiederverwenden, anstatt jedes Mal die Kosten zu tragen.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## 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).
* **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.
<Note>
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.
</Note>
@@ -0,0 +1,200 @@
---
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.
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.
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.
```text
┌─────────────────┐ set(key, value) ┌──────────────────────────┐
│ Logic function │ ───────────────────▶ │ "KV Store" object │
│ (your handler) │ ◀─────────────────── │ key (unique) │ value │
└─────────────────┘ get(key) └──────────────────────────┘
```
## Definir el objeto de almacenamiento
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`.
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### 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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
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.
</Note>
## Úsalo: almacena en caché una llamada costosa
Un uso típico es almacenar en caché una respuesta lenta o limitada por *rate limiting* de un tercero para que ejecuciones repetidas la reutilicen en lugar de pagar el coste cada vez.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## 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).
* **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.
<Note>
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.
</Note>
@@ -0,0 +1,200 @@
---
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 à laide dun simple objet clé-valeur.
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 dAPI coûteuse, stocker un curseur pour des synchronisations incrémentales, appliquer un anti-rebond, ou transférer l’état dune fonction à une autre), conservez-le dans la base de données de lespace de travail.
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.
```text
┌─────────────────┐ set(key, value) ┌──────────────────────────┐
│ Logic function │ ───────────────────▶ │ "KV Store" object │
│ (your handler) │ ◀─────────────────── │ key (unique) │ value │
└─────────────────┘ get(key) └──────────────────────────┘
```
## Définir l'objet de stockage
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`.
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### 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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
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.
</Note>
## Utilisez-le : mettez en cache un appel coûteux
Un cas d'utilisation typique consiste à mettre en cache une réponse tierce lente ou soumise à des limites de débit afin que les exécutions répétées la réutilisent au lieu d'en payer le coût à chaque fois.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## 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).
* **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é.
<Note>
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.
</Note>
@@ -0,0 +1,200 @@
---
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.
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.
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.
```text
┌─────────────────┐ set(key, value) ┌──────────────────────────┐
│ Logic function │ ───────────────────▶ │ "KV Store" object │
│ (your handler) │ ◀─────────────────── │ key (unique) │ value │
└─────────────────┘ get(key) └──────────────────────────┘
```
## Definisci l'oggetto store
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`.
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### 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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
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.
</Note>
## Usalo: metti in cache una chiamata costosa
Un uso tipico consiste nel mettere in cache una risposta lenta o soggetta a limitazione di frequenza da parte di terzi, così che le esecuzioni ripetute la riutilizzino invece di sostenerne il costo ogni volta.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## 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).
* **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.
<Note>
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.
</Note>
@@ -0,0 +1,200 @@
---
title: 키-값 저장소
description: 간단한 키-값 객체를 사용하여 중간 결과를 유지하고, 데이터를 캐시하며, 논리 함수 실행 간 상태를 공유하세요.
icon: database
---
로직 함수는 단기간 실행되는 샌드박스된 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) └──────────────────────────┘
```
## 스토어 객체 정의하기
두 개의 필드가 있는 커스텀 객체를 선언합니다. `key`(고유한 `TEXT`)와 `value`(`RAW_JSON`으로, JSON으로 직렬화할 수 있는 어떤 페이로드든 저장할 수 있음)입니다. 전체 `defineObject` 레퍼런스는 [Objects](/l/ko/developers/extend/apps/data/objects)를 참고하세요.
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### 키 고유성 강제하기
`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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
고유 인덱스는 중복을 방지하지만, 두 번의 실행이 **동일한 새로운 키**를 정확히 같은 시점에 쓰려고 하면 조회와 생성 사이에서 여전히 경쟁 상태가 발생할 수 있습니다. 생성이 고유성 제약 조건으로 실패하면 이를 "다른 누군가가 먼저 썼다"라고 간주하고, 예외를 처리해 다시 읽거나, 업데이트로 재시도하세요.
</Note>
## 사용 예: 비용이 큰 호출 캐시하기
일반적인 사용 예는 느리거나 rate limit이 걸린 서드파티 응답을 캐시해서, 반복 실행 시 매번 비용을 지불하지 않고 재사용하도록 하는 것입니다.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## 패턴 및 팁
* **네임스페이스 구성.** 서로 다른 관심사를 분리하고 대량 조회를 쉽게 하기 위해 키에 접두사를 붙이세요 — `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)을 사용하세요.
* **가시성 및 권한.** 행은 다른 레코드와 마찬가지로 워크스페이스 데이터베이스에 저장되므로, 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를 키에 인코딩하지 마세요.
<Note>
이는 별도의 기능이 아니라 하나의 컨벤션일 뿐입니다. "KV Store"는 여러분이 정의하고 표준 API로 조회하는 일반 커스텀 객체입니다. 즉, 앱의 다른 데이터와 동일한 동기화, 권한, 도구의 이점을 모두 누릴 수 있습니다.
</Note>
@@ -0,0 +1,200 @@
---
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.
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.
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.
```text
┌─────────────────┐ set(key, value) ┌──────────────────────────┐
│ Logic function │ ───────────────────▶ │ "KV Store" object │
│ (your handler) │ ◀─────────────────── │ key (unique) │ value │
└─────────────────┘ get(key) └──────────────────────────┘
```
## Definir o objeto de armazenamento
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`.
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### 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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
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.
</Note>
## Use-o: armazenar em cache uma chamada cara
Um uso típico é armazenar em cache uma resposta lenta ou com limite de taxa de terceiros, para que execuções repetidas a reutilizem em vez de pagar o custo todas as vezes.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## 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).
* **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.
<Note>
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.
</Note>
@@ -0,0 +1,200 @@
---
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.
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.
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.
```text
┌─────────────────┐ set(key, value) ┌──────────────────────────┐
│ Logic function │ ───────────────────▶ │ "KV Store" object │
│ (your handler) │ ◀─────────────────── │ key (unique) │ value │
└─────────────────┘ get(key) └──────────────────────────┘
```
## Definește obiectul de stocare
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`.
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### 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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
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.
</Note>
## Folosește-l: păstrează în cache un apel costisitor
O utilizare tipică este păstrarea în cache a unui răspuns lent sau cu limitare de rată de la un terț, astfel încât rulările repetate să îl refolosească în loc să plătească costul de fiecare dată.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## 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).
* **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.
<Note>
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.
</Note>
@@ -0,0 +1,200 @@
---
title: Хранилище ключ-значение
description: Сохраняйте промежуточные результаты, кэшируйте данные и разделяйте состояние между запусками логических функций с помощью простого объекта ключ-значение.
icon: database
---
Логические функции выполняются в изолированной среде во временных процессах Node.js — после завершения запуска в памяти ничего не сохраняется. Когда вам нужно **что-то запомнить между запусками** (кэшировать дорогой ответ API, сохранить курсор для инкрементальной синхронизации, «задебаунсить» работу или передать состояние от одной функции к другой), сохраняйте это в базе данных рабочего пространства.
Для этого не нужен отдельный примитив хранилища: небольшой **технический объект** с полем `key` и полем `value` даёт вам надёжное key-value-хранилище, ограниченное рабочим пространством и доступное для запросов через тот же [типизированный API-клиент](/l/ru/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) └──────────────────────────┘
```
## Определите объект хранилища
Объявите настраиваемый объект с двумя полями — `key` (уникальный `TEXT`) и `value` (`RAW_JSON`, чтобы можно было сохранять любые JSON-сериализуемые данные). См. [Objects](/l/ru/developers/extend/apps/data/objects) для полной справки по `defineObject`.
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### Обеспечьте уникальность ключей
Добавьте **уникальный индекс** на `key`, чтобы один и тот же ключ никогда не мог иметь две строки. Это рекомендуемый примитив для обеспечения уникальности — см. [Data → Unique indexes](/l/ru/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,
},
],
});
```
## Чтение и запись из логической функции
Спрячьте объект за несколькими небольшими вспомогательными функциями, чтобы остальной код выглядел как key-value API — `get`, `set` и `del`. Они используют [`CoreApiClient`](/l/ru/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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
Уникальный индекс защищает от дубликатов, но два запуска, записывающие **один и тот же новый ключ** в один и тот же момент, всё ещё могут войти в гонку между поиском и созданием. Рассматривайте создание, которое завершилось сбоем по ограничению уникальности, как «кто-то другой успел первым» — перехватите его и перечитайте, или повторите попытку как обновление.
</Note>
## Использование: кэширование дорогого вызова
Типичный сценарий — кэширование медленного или ограниченного по частоте ответа стороннего сервиса, чтобы при повторных запусках переиспользовать его, а не нести затраты каждый раз.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## Шаблоны и советы
* **Пространства имён.** Добавляйте префиксы к ключам, чтобы разделять разные задачи и упростить массовые выборки — `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. Фильтруйте по `key: { like: 'cache:%' }`, чтобы перечислить или очистить целое пространство имён.
* **Срок жизни (TTL).** У хранилища нет встроенного механизма истечения срока действия. Сохраняйте метку времени внутри `value` (как в примере с кэшем) и проверяйте её при чтении, или добавьте поле `DATE_TIME` и периодически очищайте устаревшие строки из [функции, запускаемой по cron](/l/ru/developers/extend/apps/logic/logic-functions).
* **Что хранить.** `RAW_JSON` может содержать любое JSON-сериализуемое значение — числа, строки, массивы, объекты. Держите записи небольшими; это для координации и кэширования, а не для больших блобов или файлов. Для файлов используйте поле `FILES` и [`uploadFile`](/l/ru/developers/extend/apps/logic/logic-functions#uploading-files).
* **Видимость и разрешения.** Строки живут в базе данных рабочего пространства как любые другие записи, поэтому к ним можно обращаться через API, и они подчиняются [ролям](/l/ru/developers/extend/apps/config/roles) вашего приложения. Чтобы скрыть хранилище из основного пользовательского интерфейса, не добавляйте его в [навигационное меню](/l/ru/developers/extend/apps/layout/navigation-menu-items).
* **Привязка к записи.** Нужно состояние на уровне отдельной записи вместо глобальных ключей? Добавьте [relation](/l/ru/developers/extend/apps/data/relations) из объекта хранилища к целевому объекту вместо кодирования идентификатора в ключе.
<Note>
Это соглашение, а не отдельная функция — «KV Store» — это просто обычный настраиваемый объект, который вы определяете и к которому обращаетесь с помощью стандартного API. Это значит, что он получает преимущества той же синхронизации, системы разрешений и инструментов, что и остальные данные вашего приложения.
</Note>
@@ -0,0 +1,200 @@
---
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.
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.
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.
```text
┌─────────────────┐ set(key, value) ┌──────────────────────────┐
│ Logic function │ ───────────────────▶ │ "KV Store" object │
│ (your handler) │ ◀─────────────────── │ key (unique) │ value │
└─────────────────┘ get(key) └──────────────────────────┘
```
## Depo nesnesini tanımlayın
İ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.
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### 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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
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.
</Note>
## Kullanın: maliyetli bir çağrıyı önbelleğe alın
Yaygın bir kullanım, yavaş veya hız sınırına tabi üçüncü taraf yanıtını önbelleğe almak, böylece tekrar eden çalıştırmalar her seferinde maliyet ödemek yerine bunu yeniden kullanır.
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## 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.
* **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.
<Note>
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.
</Note>
@@ -0,0 +1,200 @@
---
title: 键值存储
description: 使用简单的键值对象持久化中间结果、缓存数据,并在逻辑函数运行之间共享状态。
icon: database
---
逻辑函数在短生命周期的 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) └──────────────────────────┘
```
## 定义存储对象
声明一个具有两个字段的自定义对象——`key`(唯一的 `TEXT`)和 `value``RAW_JSON`,因此你可以存储任何可序列化为 JSON 的有效负载)。 完整的 `defineObject` 参考请参见[对象](/l/zh/developers/extend/apps/data/objects)。
```ts src/objects/kv-store.object.ts
import { defineObject, FieldType } from 'twenty-sdk/define';
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';
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',
},
],
});
```
### 强制键唯一性
在 `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 <TValue>(key: string): Promise<TValue | undefined> => {
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<void> => {
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<void> => {
const existing = await findByKey(key);
if (isDefined(existing)) {
await client.mutation({
deleteKvStore: { __args: { id: existing.id }, id: true },
});
}
};
```
<Note>
唯一索引可以防止重复,但在查找和创建之间,两个运行在同一时刻写入**相同的新键**时仍然可能发生竞争。 将因唯一性约束失败的创建视为“被其他人抢先了”——捕获该错误并重新读取,或改为重试执行更新。
</Note>
## 使用示例:缓存一次昂贵的调用
一个典型用例是缓存一次缓慢或受限于速率的第三方响应,这样重复的运行就可以重复使用该响应,而不必每次都付出相同的开销。
```ts src/logic-functions/getExchangeRate.logic-function.ts
import { defineLogicFunction } from 'twenty-sdk/define';
import { get, set } from './handlers/kv-store';
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<CachedRate>(cacheKey);
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
return { rate: cached.rate, cached: true };
}
const response = await fetch(
`https://api.example.com/rate?from=${params.from}&to=${params.to}`,
);
const { rate } = (await response.json()) as { rate: number };
await set(cacheKey, { rate, fetchedAt: Date.now() });
return { rate, cached: false };
};
export default defineLogicFunction({
universalIdentifier: 'd9b2f4e6-1c83-4a07-9e52-6b1d3c8a0f47',
name: 'get-exchange-rate',
timeoutSeconds: 10,
handler,
});
```
## 模式与技巧
* **命名空间划分。** 给键添加前缀,以区分不同的用途,并便于批量查找——`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)。
* **可见性与权限。** 这些行像任何其他记录一样存放在工作区数据库中,因此可以通过 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 编码进键中。
<Note>
这是一种约定,而不是一个单独的功能——“KV Store” 只是你定义并通过标准 API 查询的常规自定义对象。 这意味着它可以像你的应用中其他数据一样,受益于相同的同步机制、权限控制和工具链。
</Note>