ORM (typed entity CRUD)
createPluginOrm + createTenantScopedOrm — typed CRUD over the EntityStore.
Subpath: @fabricorg/integrations/orm.
The runtime persists upstream vendor state via the EntityStore contract: rows are (accountId, entityType, entityId) tuples with arbitrary JSON payloads. The ORM facade wraps that contract with strongly-typed get/list/upsert/delete/count methods per entity type.
Quick example
import { z } from 'zod';
import { InMemoryEntityStore } from '@fabricorg/integrations';
import { createPluginOrm } from '@fabricorg/integrations/orm';
const store = new InMemoryEntityStore();
const slackOrm = createPluginOrm({
store,
pluginId: 'slack',
schemas: {
channels: z.object({
name: z.string(),
is_private: z.boolean(),
num_members: z.number().int().min(0),
}),
users: z.object({
email: z.string(),
name: z.string(),
}),
},
tenantId: 'org:acme',
});
await slackOrm.entities.channels.upsert({
entityId: 'C0123',
version: '1',
data: { name: 'general', is_private: false, num_members: 42 },
});
const all = await slackOrm.entities.channels.list();
const c = await slackOrm.entities.channels.get({ entityId: 'C0123' });API
createPluginOrm(options)
function createPluginOrm<S extends EntitySchemaMap>({
store: EntityStore,
pluginId: string,
schemas: S, // Record<entityType, ZodTypeAny>
tenantId?: string, // default tenant for un-scoped calls
validateWrites?: boolean, // default true; runs schema.parse on upsert.data
}): PluginOrm<S>Returns:
{
pluginId: string;
tenantId: string | null;
entities: {
[entityType]: EntityClient<EntityPayload>;
};
withTenant(tenantId: string): PluginOrm<S>;
}Each EntityClient exposes:
| Method | Description |
|---|---|
get({ entityId }) | Fetch by entity id within the current tenant |
findById({ id }) | Fetch by the store's internal row id |
findMany({ entityIds }) | Bulk fetch |
list(options?) | List with optional { limit, cursor } |
search({ data, limit, cursor }) | Filter by payload fields |
upsert({ entityId, version, data }) | Insert or update — Zod-validated when validateWrites is true |
delete({ entityId }) | Remove a row |
count() | Count rows for the (tenant, entity-type) |
createTenantScopedOrm({ tenantId, plugins })
Compose multiple per-plugin ORMs into a single tenant-scoped facade.
import { createTenantScopedOrm } from '@fabricorg/integrations/orm';
const tenantOrm = createTenantScopedOrm({
tenantId: 'org:acme',
plugins: { slack: slackOrm, hubspot: hubspotOrm },
});
await tenantOrm.slack.entities.channels.list();
await tenantOrm.hubspot.entities.contacts.upsert({ /* … */ });
const globex = tenantOrm.withTenant('org:globex');
await globex.slack.entities.channels.list(); // same shape, different tenantAccount-id convention
The ORM keys its underlying store rows as <tenantId>::<pluginId>. Bring-your-own EntityStore implementations must use this convention for the ORM to find rows.
Validation
validateWrites: true (the default) runs schema.parse(data) on every upsert. Reads are trusted to match the schema — if you change the schema after writing, old rows are surfaced as-is. Set validateWrites: false for hot-path migrations where validation overhead is unacceptable.