Domain types
These are the only shapes that cross from a plugin into the app. They contain no provider names and no remote DTOs.
// Provider-neutral domain types. No provider names may appear here.export type IntegrationId = stringexport type IntegrationType = stringexport type IntegrationInstanceId = string
export type RemoteId = stringexport type ContainerId = stringexport type InventoryItemId = string
export interface InventoryContainer { id: ContainerId integrationInstanceId: IntegrationInstanceId remoteId: RemoteId name: string description?: string parentId?: ContainerId /** Names from the root down to (and including) this container. */ path: string[] itemCount?: number imageUrl?: string sourceUrl?: string metadata: Record<string, unknown> updatedAt?: string syncedAt: string}
export interface InventoryItem { id: InventoryItemId integrationInstanceId: IntegrationInstanceId remoteId: RemoteId name: string description?: string quantity?: number quantityUnit?: string containerId?: ContainerId imageUrl?: string sourceUrl?: string sku?: string serialNumber?: string barcode?: string tags?: string[] metadata: Record<string, unknown> updatedAt?: string syncedAt: string}
export interface InventorySearchQuery { text: string integrationInstanceIds?: IntegrationInstanceId[] containerIds?: ContainerId[] limit?: number includeContainers?: boolean includeItems?: boolean}
interface SearchResultBase { id: string integrationInstanceId: IntegrationInstanceId score: number searchableText: string matchedFields?: string[] canLocatePhysically: boolean}
export interface InventoryItemSearchResult extends SearchResultBase { kind: 'item' item: InventoryItem container?: InventoryContainer}
export interface InventoryContainerSearchResult extends SearchResultBase { kind: 'container' container: InventoryContainer}
export type InventorySearchResult = InventoryItemSearchResult | InventoryContainerSearchResult
export type IntegrationConnectionStatus = | { state: 'connected'; serverVersion?: string; apiVersion?: string } | { state: 'disconnected' } | { state: 'error'; error: import('./errors').IntegrationError }
export interface IntegrationInstance { id: IntegrationInstanceId integrationType: IntegrationType displayName: string baseUrl?: string secureCredentialKey?: string status: IntegrationConnectionStatus pluginVersion?: string serverVersion?: string apiVersion?: string lastSuccessfulSyncAt?: string metadata: Record<string, unknown>}
/** What the local (non-secret) database persists about an instance. */export type StoredIntegrationInstance = Pick< IntegrationInstance, | 'id' | 'integrationType' | 'displayName' | 'secureCredentialKey' | 'pluginVersion' | 'serverVersion' | 'lastSuccessfulSyncAt'>Rules the contract suite checks
Section titled “Rules the contract suite checks”- Ids.
idis unique across all sources;remoteIdis the source’s own id. A good convention is${instance.id}:c:${remoteId}for containers and…:i:…for items, which also keeps two sources from colliding. - Paths. A container’s
pathruns from the root down to and including itself, sopath.at(-1) === name. If it has aparentId, the parent’spathis the same list without the last element. - Items point at containers. Every item returned for a container has
containerIdequal to that container’sid. - Only known fields. A container or item with a field outside the type fails the suite, so provider-specific extras cannot leak.
- Metadata is plain data.
metadatamust surviveJSON.parse(JSON.stringify(x))unchanged: no class instances, functions orDateobjects. Use it for extra data you want to keep; keep it small. - Plain objects. Returned values must be plain objects (
Object.getPrototypeOf(x) === Object.prototype). - Timestamps.
syncedAtis an ISO-8601 string; take it from the context’s clock, notDate.now(), so tests are deterministic.
Search results
Section titled “Search results”A search result is either an item or a container. Both carry a score (higher is better), the searchableText that matched, and canLocatePhysically. An item result that can be located must carry its container, because that is what lets the app say where to point the camera.