@aimform/spaces
v0.3.5
Published
Aimform Spaces — general-purpose content model: Spaces, Entities, Blocks, Properties, Views, Files, Connections, and Conversations.
Maintainers
Readme
@aimform/spaces
spaceType is an extensible string projection. The built-in semantic values are
project, ability-app, ability-skill, ability-tool, and ability-package.
Ability source Spaces are linked by the API's ability_sources projection and
remain ordinary Spaces for Files, Entities, and Git-backed source operations.
General-purpose content model — Spaces, Entities, Blocks, Properties, Collections, Views, Files, Connections, and Conversations.
Install
npm install @aimform/spaces @aimform/network-client @aimform/identityUsage
import { Database, D1Driver } from "@aimform/db";
import { Spaces, Entities, Blocks, Collections, Properties, Views, Files, Connections, Conversations, spacesSchema }
from "@aimform/spaces";
// Service-side relational persistence
const database = new Database({ schema: spacesSchema, driver: new D1Driver(env.DB) });
const spaces = new Spaces({ database });
const blocks = new Blocks({ database });
// Platform API
const aimform = createAimform({ apiKey: "af_xxx" });
const spaces = new Spaces({ client: aimform.client });
const entities = new Entities({ client: aimform.client });For service-side persistence, construct one authorized Database at the
composition root and pass it through the package APIs:
import { Database, D1Driver } from "@aimform/db";
import { DatabaseKnowledgeCollectionAdapter, spacesSchema } from "@aimform/spaces";
const database = new Database({ schema: spacesSchema, driver: new D1Driver(env.DB) });
const knowledge = new DatabaseKnowledgeCollectionAdapter(database);Request contexts may expose that same instance as both ctx.db and
ctx.database. Raw provider bindings are not application database surfaces.
The conversations model includes optional thread metadata fields
(threadKind, surface, parentConversationId, rootConversationId, and
conversationVersion). Existing rows remain valid; services may populate
these fields during the initial conversation insert to avoid a second update.
The server-side DatabaseFilesAdapter accepts an injected StorageAdapter for
direct file operations. It writes uploaded bytes to object storage before
persisting metadata, reads content through the stored object key, and deletes
the object before removing its metadata row.
Knowledge collection reads are non-mutating. DatabaseKnowledgeCollectionAdapter.listMany(scope, collectionIds) reads several knowledge collections from one scoped entity query and preserves the same scope filtering as list.
Spaces
import { Spaces } from "@aimform/spaces";
const spaces = new Spaces({ database: db });
await spaces.list("org_456");
await spaces.listChildren("org_456"); // root-level Spaces and folders
await spaces.listChildren("org_456", "spc_123"); // direct children, like ls
await spaces.listChildren("org_456", "spc_123", "folder"); // folders only
await spaces.get("spc_123");
await spaces.create({ ownerType: "organization", ownerId: "o1", name: "Project", createdByProfileId: "p1" });
await spaces.update("spc_123", { name: "Renamed" });
await spaces.delete("spc_123");
await spaces.getIndex("spc_123");Spaces can be nested by setting parentSpaceId; use spaceType: "folder" for a folder Space. File paths are stored as relative directory metadata, and Git-backed repository snapshots preserve file identity with LFS pointers while edits update the existing file object.
Entities
import { Entities } from "@aimform/spaces";
const entities = new Entities({ database: db });
await entities.list("spc_123", { entityType: "task" });
await entities.count("spc_123", { entityType: "task" }); // cheap COUNT(*) — prefer over list() for badges/overview counts
await entities.get("ent_789");
await entities.create({ spaceId: "spc_123", entityType: "task", properties: { title: "Setup CI" }, createdByProfileId: "p1" });
await entities.update("ent_789", { properties: { status: "done" } });
await entities.delete("ent_789");Public content for SSR
Collections can be explicitly shared for public, read-only rendering using
CollectionShares (authenticated create/revoke) and PublicContent (anonymous
SSR reads). The public endpoint returns only live Collection Entities marked with
properties.status: "published" or properties.published: true, together
with their ordered Blocks. Create a Collection share link through the
authenticated Space route, keep the returned token in server-side config, and
load it with the SDK:
import { AimformClient } from "@aimform/network-client";
import { CollectionShares, PublicContent } from "@aimform/spaces";
const editorClient = new AimformClient({ apiKey: process.env.AIMFORM_API_KEY! });
const share = await new CollectionShares(editorClient).create(orgId, spaceId, "blog_post");
// Store share.token in server-side config; it grants public read-only access.
const publicContent = new PublicContent(new AimformClient({ baseUrl: "https://api.aimform.com" }));
const { collection, items } = await publicContent.getCollection(share.token);The token grants read-only access to published items and is revocable. It does not grant access to drafts or write operations.
Blocks
import { Blocks } from "@aimform/spaces";
const blocks = new Blocks({ database: db });
await blocks.list("ent_789");
await blocks.add("ent_789", { blockType: "paragraph", content: "Hello world" });
await blocks.addSource(block.id, { sourceType: "url", sourceUrl: "https://...", excerpt: "...", confidence: "high" });
await blocks.update(block.id, { content: "Updated" });
await blocks.delete(block.id);
await blocks.removeSource(block.id, source.id);Collections
import { Collections } from "@aimform/spaces";
const collections = new Collections({ database: db });
await collections.list("spc_123");
await collections.upsert({
spaceId: "spc_123", entityType: "task",
renderMode: "record", presentation: "collection",
displayName: "Tasks", pinned: true, position: 0,
});
await collections.delete("spc_123", "task");Properties
import { Properties } from "@aimform/spaces";
const properties = new Properties({ database: db });
await properties.list("spc_123", "task");
await properties.create({ spaceId: "spc_123", key: "priority", label: "Priority", valueType: "select" });
await properties.createMany([
{ spaceId: "spc_123", entityType: "task", key: "status", label: "Status", valueType: "select" },
{ spaceId: "spc_123", entityType: "task", key: "dueAt", label: "Due at", valueType: "date" },
]);
await properties.update("prop_1", { label: "Task Priority" });
await properties.delete("prop_1");Views
import { Views } from "@aimform/spaces";
const views = new Views({ database: db });
await views.list("spc_123");
await views.create({ spaceId: "spc_123", name: "Active Tasks", viewType: "table", entityType: "task" });
await views.update("view_1", { filters: JSON.stringify([{ field: "status", operator: "eq", value: "active" }]) });
await views.delete("view_1");Files
import { Files } from "@aimform/spaces";
const files = new Files({ database: db });
await files.list("org_456");
await files.get("file_1");
await files.put({ orgId: "org_456", r2Key: "reports/q4.pdf", mimeType: "application/pdf", sizeBytes: 1024 });
await files.delete("file_1");
await files.getPresignedUrl("reports/q4.pdf", "application/pdf", 3600);Automations
import { Automations } from "@aimform/spaces";
const automations = new Automations({ client: aimform.client });
const lineage = await automations.getDecisionLineage("org_456", "run_123");
console.log(lineage.coverage, lineage.items);getDecisionLineage is a read-only, organization-authorized audit view over
one durable Workflow run. It preserves references across events, Function
invocations, AI and policy decisions, human approvals, external effects, and
the final result.
Connections
import { Connections } from "@aimform/spaces";
const connections = new Connections({ database: db });
await connections.list("org_456");
await connections.create({ orgId: "org_456", provider: "xero", credentialHandle: "vault://object/xero/connection-1" });
await connections.listProviders();
const result = await connections.resolveCapability("org_456", "financial:transactions:read");
await connections.getCanonicalData("conn_1", "financial:transactions:read", { limit: 50 });
await connections.grantAccess("conn_1", "client_1", ["financial:transactions:read"]);
await connections.revoke("conn_1");The Apps connection surface accepts credentialValue only while establishing
a connection. The API stores that value in Vault and returns only
credential_handle plus credential_status; callers must use explicit
capability grants when assigning a connection to a Function or Workflow.
App onboarding is manifest-driven. Apps.getProviderSetup() exposes the
managed Aimform default, its billing label, optional BYOD credential fields,
and provider manual steps. Apps.validateProviderCredentials() performs the
pre-persistence Form Intake check. Use connectionMode: "byod" only when the
organisation owns the provider credentials; the default managed mode is
billed through Aimform. GoCardless onboarding may provide a validated
returnPath to return to an App's native in-app surface after authorization.
Apps.listChatAppState() returns the provider catalog and connection statuses
in one response for server-rendered chat connection cards.
Connection Functions are reusable and always scoped to one Connection:
const functions = await apps.listConnectionFunctions("org_456", "conn_bank_1");
const accounts = await apps.invokeConnectionFunction("org_456", "conn_bank_1", "getAccounts");
const balances = await apps.invokeConnectionFunction("org_456", "conn_bank_1", "getBalances", {
accountId: "account_123",
});
const transactions = await apps.invokeConnectionFunction("org_456", "conn_bank_1", "listTransactions", {
accountId: "account_123",
});Each bank authorization is an independent Connection. Provider account IDs are verified and stored as safe Connection metadata, while provider tokens remain behind Vault; account reads cannot cross into another bank Connection.
Conversations
import { Conversations } from "@aimform/spaces";
const conversations = new Conversations({ database: db });
await conversations.list("org_456", "prof_123");
await conversations.create({ orgId: "org_456", profileId: "prof_123", contextScope: "space:spc_123" });
await conversations.appendMessage("conv_1", { role: "user", content: "Hello" });
const conv = await conversations.get("conv_1");
await conversations.delete("conv_1");Gmail provider
Gmail connection reads, writes, attachment downloads, and normalized sync records are owned by Spaces. The deployed Sync service composes this provider with Trust-owned Google OAuth credentials.
import { GmailAdapter } from "@aimform/spaces";
const gmail = new GmailAdapter(accessToken);
const messages = await gmail.listMessages("newer_than:14d");Production Gmail composition passes { guard } as the adapter options. Guard
admission happens before Gmail requests and tracks request/fanout usage without
coalescing semantically distinct mailbox events.
