FabricFabricSDK

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:

MethodDescription
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 tenant

Account-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.