i18n - docs translations (#23162)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com>
This commit is contained in:
committed by
GitHub
parent
25f28ee299
commit
41c32a04e4
@@ -1,153 +1,57 @@
|
||||
---
|
||||
title: Key-Value-Store
|
||||
description: Bewahren Sie Zwischenergebnisse auf, zwischenspeichern Sie Daten und teilen Sie den Zustand zwischen Ausführungen von Logikfunktionen mit einem einfachen Key-Value-Objekt.
|
||||
description: Bewahren Sie Zwischenergebnisse auf, zwischenspeichern Sie Daten und teilen Sie den Zustand zwischen Ausführungen von Logikfunktionen mit dem integrierten Key-Value-Speicher der Anwendung.
|
||||
icon: database
|
||||
---
|
||||
|
||||
Logikfunktionen laufen isoliert in kurzlebigen Node.js-Prozessen – sobald ein Durchlauf abgeschlossen ist, überlebt nichts, was im Speicher gehalten wurde. Wenn Sie sich **zwischen Durchläufen etwas merken müssen** (eine teure API-Antwort zwischenspeichern, einen Cursor für inkrementelle Synchronisierungen speichern, Arbeit entprellen oder Zustand von einer Funktion an eine andere übergeben), speichern Sie es in der Workspace-Datenbank.
|
||||
Logikfunktionen laufen isoliert in kurzlebigen Node.js-Prozessen – sobald ein Durchlauf abgeschlossen ist, überlebt nichts, was im Speicher gehalten wurde. Wenn Sie sich **zwischen Durchläufen etwas merken müssen** (eine teure API-Antwort zwischenspeichern, einen Cursor für inkrementelle Synchronisierungen speichern, Arbeit entprellen oder Zustand von einer Funktion an eine andere übergeben), speichern Sie es im integrierten Key-Value-Speicher.
|
||||
|
||||
Dafür benötigen Sie kein spezielles Speicher-Primitiv: Ein kleines **technisches Objekt** mit einem `key`-Feld und einem `value`-Feld gibt Ihnen einen dauerhaften Key-Value-Store, der auf den Workspace begrenzt ist und über denselben [typisierten API-Client](/l/de/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) abfragbar ist, den Sie bereits für Datensätze verwenden.
|
||||
Jede Anwendung erhält ihren eigenen isolierten Namespace: Einträge werden durch die authentifizierte App indiziert, sodass Ihre Schlüssel niemals mit denen einer anderen Anwendung kollidieren können – oder von ihr gelesen werden können.
|
||||
|
||||
```text
|
||||
┌─────────────────┐ set(key, value) ┌──────────────────────────┐
|
||||
│ Logic function │ ───────────────────▶ │ "KV Store" object │
|
||||
│ (your handler) │ ◀─────────────────── │ key (unique) │ value │
|
||||
└─────────────────┘ get(key) └──────────────────────────┘
|
||||
┌─────────────────┐ kv.set(key, value) ┌──────────────────────────┐
|
||||
│ Logic function │ ─────────────────────▶ │ Application KV store │
|
||||
│ (your handler) │ ◀───────────────────── │ key (unique) │ value │
|
||||
└─────────────────┘ kv.get(key) └──────────────────────────┘
|
||||
```
|
||||
|
||||
## Das Store-Objekt definieren
|
||||
## Abrufen, Setzen, Löschen
|
||||
|
||||
Deklarieren Sie ein benutzerdefiniertes Objekt mit zwei Feldern – `key` (ein eindeutiger `TEXT`) und `value` (ein `RAW_JSON`, damit Sie jede JSON-serialisierbare Nutzlast speichern können). Siehe [Objekte](/l/de/developers/extend/apps/data/objects) für die vollständige `defineObject`-Referenz.
|
||||
Importieren Sie `kv` aus `twenty-sdk/logic-function`. Werte können beliebige JSON-serialisierbare Werte sein.
|
||||
|
||||
```ts src/objects/kv-store.object.ts
|
||||
import { defineObject, FieldType } from 'twenty-sdk/define';
|
||||
```ts src/logic-functions/sync-linear-issues.ts
|
||||
import { kv } from 'twenty-sdk/logic-function';
|
||||
|
||||
export const KV_STORE_UNIVERSAL_IDENTIFIER =
|
||||
'2f1c8a90-3b6d-4e2a-9c47-7d0e5a1b9f33';
|
||||
export const KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'4a7e2d11-9c83-4f60-b5a2-1e6c8d0f4b21';
|
||||
export const KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'8b3f6c02-5d19-47ae-9f31-2c4a7e0b6d58';
|
||||
// Read a value. Returns null when the key is missing.
|
||||
const cursor = await kv.get<string>('sync-cursor:linear');
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'kvStore',
|
||||
namePlural: 'kvStores',
|
||||
labelSingular: 'KV Store',
|
||||
labelPlural: 'KV Store',
|
||||
description: 'Key-value storage for logic functions',
|
||||
icon: 'IconDatabase',
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
name: 'key',
|
||||
type: FieldType.TEXT,
|
||||
label: 'Key',
|
||||
description: 'Unique lookup key',
|
||||
icon: 'IconKey',
|
||||
},
|
||||
{
|
||||
universalIdentifier: KV_STORE_VALUE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
name: 'value',
|
||||
type: FieldType.RAW_JSON,
|
||||
label: 'Value',
|
||||
description: 'Stored JSON payload',
|
||||
icon: 'IconJson',
|
||||
},
|
||||
],
|
||||
// Write a value. Creates the entry on first write, updates it afterwards.
|
||||
await kv.set('sync-cursor:linear', newCursor);
|
||||
|
||||
// Delete an entry. Returns true when an entry was removed.
|
||||
await kv.delete('sync-cursor:linear');
|
||||
```
|
||||
|
||||
## Geltungsbereiche
|
||||
|
||||
Jeder Eintrag hat einen Geltungsbereich, der bei jedem Aufruf als Option übergeben wird. Der Standardwert ist `WORKSPACE`.
|
||||
|
||||
* **`WORKSPACE`** (Standard) – der Eintrag ist nur für die aktuelle Workspace-Installation Ihrer App sichtbar. Jeder Workspace, der die App installiert, erhält seinen eigenen unabhängigen Satz von Schlüsseln. Das ist genau das, was Sie für Caches, Cursor und workspacespezifischen Zustand benötigen.
|
||||
* **`SERVER`** – der Eintrag wird über **alle Installationen** Ihrer App auf dem Server hinweg geteilt. Server-Einträge verhalten sich wie **Claims**: Der gespeicherte Wert ist immer die workspaceId, die den Schlüssel beansprucht hat (lassen Sie `value` bei `set` weg, um den Schlüssel für den aktuellen Workspace zu beanspruchen), und nur dieser Workspace kann ihn überschreiben oder löschen. Jede Installation kann den Eintrag lesen.
|
||||
|
||||
Server-Claims existieren für Cross-Workspace-Routing. Ein [Server-Route-Resolver](/l/de/developers/extend/apps/logic/logic-functions#server-route-trigger) läuft im Workspace des Anwendungsregistrierungs-Inhabers, aber ein eingehender Webhook übermittelt in der Regel nur eine externe Konto-ID – nicht eine Twenty workspaceId. Lassen Sie jeden Workspace seine externe ID zum Verbindungszeitpunkt beanspruchen und lösen Sie sie dann in der Route auf:
|
||||
|
||||
```ts
|
||||
// In the connected workspace, when the external account is linked:
|
||||
await kv.set(`slack:team:${teamId}`, undefined, { scope: 'SERVER' });
|
||||
|
||||
// In the server-route resolver (owner workspace), on each webhook:
|
||||
const workspaceId = await kv.get<string>(`slack:team:${teamId}`, {
|
||||
scope: 'SERVER',
|
||||
});
|
||||
```
|
||||
|
||||
### Eindeutigkeit des Schlüssels erzwingen
|
||||
|
||||
Fügen Sie einen **eindeutigen Index** auf `key` hinzu, damit derselbe Schlüssel niemals zwei Zeilen haben kann. Dies ist das empfohlene Primitiv für Eindeutigkeit – siehe [Daten → Eindeutige Indizes](/l/de/developers/extend/apps/data/overview#unique-indexes).
|
||||
|
||||
```ts src/indexes/kv-store-key.index.ts
|
||||
import { defineIndex } from 'twenty-sdk/define';
|
||||
import {
|
||||
KV_STORE_UNIVERSAL_IDENTIFIER,
|
||||
KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from '../objects/kv-store.object';
|
||||
|
||||
export default defineIndex({
|
||||
universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e14',
|
||||
objectUniversalIdentifier: KV_STORE_UNIVERSAL_IDENTIFIER,
|
||||
isUnique: true,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: 'c0d4e8f2-6a1b-4c93-8e57-3f9a2d0b7e15',
|
||||
fieldUniversalIdentifier: KV_STORE_KEY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Aus einer Logikfunktion lesen und schreiben
|
||||
|
||||
Kapseln Sie das Objekt hinter ein paar kleinen Hilfsfunktionen, sodass der Rest Ihres Codes wie eine Key-Value-API aussieht – `get`, `set` und `del`. Sie verwenden [`CoreApiClient`](/l/de/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk), der aus Ihrem Workspace-Schema generiert wird und vollständig gegen das `kvStore`-Objekt typisiert ist.
|
||||
|
||||
```ts src/logic-functions/handlers/kv-store.ts
|
||||
import { CoreApiClient } from 'twenty-client-sdk/core';
|
||||
import { isDefined } from 'twenty-sdk/utils';
|
||||
|
||||
const client = new CoreApiClient();
|
||||
|
||||
// Look up a single row by its key.
|
||||
const findByKey = async (key: string) => {
|
||||
const { kvStores } = await client.query({
|
||||
kvStores: {
|
||||
__args: { filter: { key: { eq: key } }, first: 1 },
|
||||
edges: { node: { id: true, value: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return kvStores.edges[0]?.node;
|
||||
};
|
||||
|
||||
// Read a value. Returns undefined when the key is missing.
|
||||
export const get = async <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>
|
||||
Da ein Server-Schlüssel nur für den eigenen Workspace des Aufrufers beansprucht und niemals von einem anderen überschrieben werden kann, kann ein Workspace kein Mapping kapern, das jemand anderem gehört. `kv.set` löst eine Exception aus, wenn der Schlüssel bereits von einem anderen Workspace beansprucht wurde.
|
||||
|
||||
## Verwenden Sie ihn: einen teuren Aufruf zwischenspeichern
|
||||
|
||||
@@ -155,15 +59,15 @@ Eine typische Verwendung ist das Zwischenspeichern einer langsamen oder ratelimi
|
||||
|
||||
```ts src/logic-functions/getExchangeRate.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
import { get, set } from './handlers/kv-store';
|
||||
import { kv } from 'twenty-sdk/logic-function';
|
||||
|
||||
const ONE_HOUR_MS = 60 * 60 * 1000;
|
||||
|
||||
type CachedRate = { rate: number; fetchedAt: number };
|
||||
|
||||
const handler = async (params: { from: string; to: string }) => {
|
||||
const cacheKey = `exchange-rate:${params.from}:${params.to}`;
|
||||
const cached = await get<CachedRate>(cacheKey);
|
||||
const cacheKey = `cache:exchange-rate:${params.from}:${params.to}`;
|
||||
const cached = await kv.get<CachedRate>(cacheKey);
|
||||
|
||||
if (cached && Date.now() - cached.fetchedAt < ONE_HOUR_MS) {
|
||||
return { rate: cached.rate, cached: true };
|
||||
@@ -174,7 +78,7 @@ const handler = async (params: { from: string; to: string }) => {
|
||||
);
|
||||
const { rate } = (await response.json()) as { rate: number };
|
||||
|
||||
await set(cacheKey, { rate, fetchedAt: Date.now() });
|
||||
await kv.set(cacheKey, { rate, fetchedAt: Date.now() });
|
||||
|
||||
return { rate, cached: false };
|
||||
};
|
||||
@@ -189,12 +93,18 @@ export default defineLogicFunction({
|
||||
|
||||
## Muster & Tipps
|
||||
|
||||
* **Namespacing.** Präfixieren Sie Schlüssel, um unterschiedliche Belange getrennt zu halten und Bulk-Abfragen zu erleichtern – `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`. Filtern Sie mit `key: { like: 'cache:%' }`, um einen gesamten Namespace aufzulisten oder zu leeren.
|
||||
* **Ablauf (TTL).** Der Store hat keine eingebaute Ablaufzeit. Speichern Sie einen Zeitstempel innerhalb des `value` (wie im Cache-Beispiel) und prüfen Sie ihn beim Lesen, oder fügen Sie ein `DATE_TIME`-Feld hinzu und löschen Sie regelmäßig veraltete Zeilen aus einer [cron-getriggerten Funktion](/l/de/developers/extend/apps/logic/logic-functions).
|
||||
* **Was gespeichert wird.** `RAW_JSON` enthält jeden JSON-serialisierbaren Wert – Zahlen, Zeichenketten, Arrays, Objekte. Halten Sie Einträge klein; dies ist für Koordination und Caching gedacht, nicht für große Blobs oder Dateien. Für Dateien verwenden Sie ein `FILES`-Feld und [`uploadFile`](/l/de/developers/extend/apps/logic/logic-functions#uploading-files).
|
||||
* **Namespacing.** Präfixieren Sie Schlüssel, um unterschiedliche Belange getrennt zu halten – `sync-cursor:linear`, `cache:exchange-rate:USD:EUR`, `lock:nightly-report`.
|
||||
* **Ablauf (TTL).** Der Store hat keine eingebaute Ablaufzeit. Speichern Sie einen Zeitstempel im Wert (wie im Cache-Beispiel) und prüfen Sie ihn beim Lesen, oder löschen Sie veraltete Schlüssel aus einer [cron-getriggerten Funktion](/l/de/developers/extend/apps/logic/logic-functions).
|
||||
* **Was gespeichert wird.** Jeder JSON-serialisierbare Wert – Zahlen, Zeichenketten, Arrays, Objekte. Halten Sie Einträge klein; dies ist für Koordination und Caching gedacht, nicht für große Blobs oder Dateien. Für Dateien verwenden Sie ein `FILES`-Feld und [`uploadFile`](/l/de/developers/extend/apps/logic/logic-functions#uploading-files).
|
||||
* **Sichtbarkeit.** Einträge liegen in der Instanzdatenbank, nicht als Workspace-Datensätze – sie erscheinen nie in der Workspace-UI, sind kein Teil des Datenmodells Ihrer App und benötigen keine Rollen- oder Objektberechtigungen.
|
||||
|
||||
## Alternative: ein abfragbares Store-Objekt
|
||||
|
||||
Der integrierte Store ist bewusst intransparent: Einträge sind keine Datensätze, daher können Sie sie nicht in der UI durchsuchen, nicht mit anderen Objekten verknüpfen oder mit Record-Abfragen filtern. Wenn Sie irgendetwas davon benötigen – etwa ein sichtbares Sync-Log oder pro-Datensatz-Zustand – definieren Sie stattdessen ein kleines **technisches Objekt** mit einem eindeutigen `key`-Feld und einem `RAW_JSON`-`value`-Feld und fragen Sie es über den [typisierten API-Client](/l/de/developers/extend/apps/logic/logic-functions#typed-api-clients-twenty-client-sdk) ab. Siehe [Objekte](/l/de/developers/extend/apps/data/objects) für die Referenz zu `defineObject` und [Daten → Eindeutige Indizes](/l/de/developers/extend/apps/data/overview#unique-indexes) zur Durchsetzung der Schlüssel-Eindeutigkeit.
|
||||
|
||||
* **Geltungsbereich für einen Datensatz.** Fügen Sie vom Store-Objekt eine [Relation](/l/de/developers/extend/apps/data/relations) zum Zielobjekt hinzu, anstatt die ID in den Schlüssel zu kodieren.
|
||||
* **Sichtbarkeit & Berechtigungen.** Zeilen befinden sich wie jeder andere Datensatz in der Workspace-Datenbank, sind also über die API abfragbar und respektieren die [Rolle](/l/de/developers/extend/apps/config/roles) Ihrer App. Um den Store aus dem Haupt-UI herauszuhalten, führen Sie ihn nicht in Ihrem [Navigationsmenü](/l/de/developers/extend/apps/layout/navigation-menu-items) auf.
|
||||
* **Auf einen Datensatz begrenzen.** Sie benötigen zustandsspezifische Daten pro Datensatz statt globaler Schlüssel? Fügen Sie vom Store-Objekt eine [Relation](/l/de/developers/extend/apps/data/relations) zum Zielobjekt hinzu, anstatt die ID in den Schlüssel zu kodieren.
|
||||
|
||||
<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.
|
||||
Im Gegensatz zum integrierten Store ist ein benutzerdefiniertes Objekt immer auf einen Workspace begrenzt – es kann keine Einträge über Installationen hinweg teilen, so wie es `SERVER`-Schlüssel tun.
|
||||
</Note>
|
||||
|
||||
@@ -34,6 +34,9 @@ Die **Logikschicht** einer Twenty-App ist der Code, der *ausgeführt wird* – s
|
||||
<Card title="Verbindungen" icon="plug" href="/l/de/developers/extend/apps/logic/connections">
|
||||
OAuth-Anmeldedaten, die Ihre App für Dienste von Drittanbietern verwaltet – Linear, GitHub, Slack und mehr.
|
||||
</Card>
|
||||
<Card title="Key-Value-Store" icon="database" href="/l/de/developers/extend/apps/logic/key-value-store">
|
||||
Zustand zwischen Ausführungen von Logikfunktionen beibehalten — Caches, Cursor und arbeitsbereichsübergreifende Claims.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Auslösertypen im Überblick
|
||||
|
||||
@@ -170,6 +170,17 @@ Veröffentlicht Ihre App mit Herkunftsnachweis auf npm, wenn Sie ein Versions-Ta
|
||||
|
||||
Öffnen Sie auf npmjs.com Ihr Paket > **Settings → Trusted Publisher** und registrieren Sie dieses Repository mit dem `publish.yml`-Workflow (siehe die [npm trusted publishing docs](https://docs.npmjs.com/trusted-publishers)). Das Veröffentlichen mit Provenance bestätigt, welches GitHub-Repository das Paket gebaut hat; zugleich beanspruchen Sie damit die Inhaberschaft Ihrer App in einem Twenty-Marktplatz.
|
||||
|
||||
<Note>
|
||||
npm akzeptiert Herkunftsnachweise nur aus **öffentlichen** Quellcode-Repositories. Wenn du aus einem privaten Repository veröffentlichst, weist npm das OIDC-Provenance-Bundle mit einem `E422 Unsupported GitHub Actions source repository visibility: "private"`-Fehler. Um aus einem privaten Repository zu veröffentlichen, deaktiviere Herkunftsnachweise, indem du `TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'` im `env`-Block des Veröffentlichungs-Schritts setzt (ein auskommentierter Hinweis ist in der generierten `publish.yml` enthalten):
|
||||
|
||||
```yaml filename=".github/workflows/publish.yml"
|
||||
- name: Publish to npm
|
||||
env:
|
||||
TWENTY_APP_PUBLISH_DISABLE_PROVENANCE: 'true'
|
||||
run: yarn twenty app:publish
|
||||
```
|
||||
</Note>
|
||||
|
||||
### Fixieren der wiederverwendbaren Actions
|
||||
|
||||
Die Workflows `ci.yml` und `cd.yml` verweisen auf wiederverwendbare Actions mit `@main`, sodass Aktualisierungen der Actions im Repository `twentyhq/twenty` automatisch übernommen werden. Wenn Sie deterministische Builds möchten, ersetzen Sie `@main` in jeder `uses:`-Zeile durch eine Commit-SHA oder einen Release-Tag.
|
||||
|
||||
Reference in New Issue
Block a user