Логические функции выполняются в изолированной среде во временных процессах Node.js — после завершения запуска в памяти ничего не сохраняется. Когда вам нужно что-то запомнить между запусками (кэшировать дорогой ответ API, сохранить курсор для инкрементальной синхронизации, «задебаунсить» работу или передать состояние от одной функции к другой), сохраняйте это в базе данных рабочего пространства.
Для этого не нужен отдельный примитив хранилища: небольшой технический объект с полем key и полем value даёт вам надёжное key-value-хранилище, ограниченное рабочим пространством и доступное для запросов через тот же типизированный API-клиент, который вы уже используете для записей.
┌─────────────────┐ set(key, value) ┌──────────────────────────┐
│ Logic function │ ───────────────────▶ │ "KV Store" object │
│ (your handler) │ ◀─────────────────── │ key (unique) │ value │
└─────────────────┘ get(key) └──────────────────────────┘
Определите объект хранилища
Объявите настраиваемый объект с двумя полями — key (уникальный TEXT) и value (RAW_JSON, чтобы можно было сохранять любые JSON-сериализуемые данные). См. Objects для полной справки по defineObject.
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.
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, который генерируется из схемы вашего рабочего пространства и полностью типизирован относительно объекта kvStore.
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 },
});
}
};
Уникальный индекс защищает от дубликатов, но два запуска, записывающие один и тот же новый ключ в один и тот же момент, всё ещё могут войти в гонку между поиском и созданием. Рассматривайте создание, которое завершилось сбоем по ограничению уникальности, как «кто-то другой успел первым» — перехватите его и перечитайте, или повторите попытку как обновление.
Использование: кэширование дорогого вызова
Типичный сценарий — кэширование медленного или ограниченного по частоте ответа стороннего сервиса, чтобы при повторных запусках переиспользовать его, а не нести затраты каждый раз.
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.
- Что хранить.
RAW_JSON может содержать любое JSON-сериализуемое значение — числа, строки, массивы, объекты. Держите записи небольшими; это для координации и кэширования, а не для больших блобов или файлов. Для файлов используйте поле FILES и uploadFile.
- Видимость и разрешения. Строки живут в базе данных рабочего пространства как любые другие записи, поэтому к ним можно обращаться через API, и они подчиняются ролям вашего приложения. Чтобы скрыть хранилище из основного пользовательского интерфейса, не добавляйте его в навигационное меню.
- Привязка к записи. Нужно состояние на уровне отдельной записи вместо глобальных ключей? Добавьте relation из объекта хранилища к целевому объекту вместо кодирования идентификатора в ключе.
Это соглашение, а не отдельная функция — «KV Store» — это просто обычный настраиваемый объект, который вы определяете и к которому обращаетесь с помощью стандартного API. Это значит, что он получает преимущества той же синхронизации, системы разрешений и инструментов, что и остальные данные вашего приложения.