@wip/client
@wip/client
What it is. @wip/client is the TypeScript client library for WIP (World In a Pie) services. It is framework-agnostic, has zero runtime dependencies, and talks to WIP over native fetch — it runs in Node.js 18+ and any modern browser. An app dev calls createWipClient() once to get a WipClient, then reaches every store through one property each: client.defStore, client.templates, client.documents, client.files, client.registry, client.reporting. Each property is a thin typed wrapper — one method per REST endpoint — over that store's HTTP API, sharing a single FetchTransport (one baseUrl, one auth provider, one retry/timeout policy) underneath. The library encodes WIP's cross-cutting conventions once so callers don't have to: bulk-first writes, identity-hash-driven natural-upsert, per-item vs. bulk error handling, and namespace scoping under multi-namespace keys.
Install & import. The package is @wip/client, currently at version 0.32.0 (package.json). It is "private": true — not published to a registry — so a consumer depends on it as a local package, either a file: path reference or a packed tarball:
{
"dependencies": {
"@wip/client": "file:../../libs/wip-client"
}
}
It ships a dual ESM/CJS build (tsup, target es2020) with bundled .d.ts: main/require resolves dist/index.cjs + dist/index.d.cts, module/import resolves dist/index.js + dist/index.d.ts. The primary import:
import { createWipClient } from '@wip/client'
Core API
createWipClient(config: WipClientConfig): WipClient
- Purpose. The library's one entry point. Builds a single
FetchTransportfromconfigand wires it into one instance of each of the six service classes, returned together as aWipClient. - Parameters.
WipClientConfig:Field Type Required Notes baseUrlstringyes Base URL of the WIP instance. Empty string or a /-leading path resolves againstwindow.location.originin a browser; in Node.js it must be an absolute URL.authAuthProvider | {type:'api-key', key} | {type:'oidc', getToken}no Either hand-roll an AuthProvider(anything withgetHeaders()) or use one of the two shorthand forms —createWipClientbuilds the matching provider for you.timeoutnumberno Per-request timeout in ms. Default 30000.retryRetryConfigno { maxRetries, baseDelayMs?, maxDelayMs? }. Default{ maxRetries: 2, baseDelayMs: 500, maxDelayMs: 5000 }. Applies to GET only — see Gotchas.onAuthError() => voidno Called once per request on a 401/403 response (e.g. redirect to login). - Returns.
WipClient— synchronous, not a promise:{ defStore, templates, documents, files, registry, reporting, setAuth(auth: AuthProvider): void }. - Throws / errors. Synchronously, a plain
Error(not aWipError) ifbaseUrlis empty or relative and nowindowis present — a Node.js script must pass an absolute URL. - Example.
const client = createWipClient({ baseUrl: '/wip', auth: { type: 'api-key', key: 'dev_master_key_for_testing' }, }) const terminologies = await client.defStore.listTerminologies({ status: 'active' })
Auth providers — AuthProvider, ApiKeyAuthProvider, OidcAuthProvider
- Purpose. Pluggable request-header suppliers.
FetchTransportcalls the active provider once per request (subject to caching, see below) and merges the returned headers into the fetch call. - Parameters.
AuthProvider(interface):getHeaders(): Record<string,string> | Promise<Record<string,string>>; optionalcacheable?: boolean.ApiKeyAuthProvider(apiKey: string)— setscacheable = true(readonly);getHeaders()returns{ 'X-API-Key': apiKey }; hassetApiKey(key: string): voidfor in-place key rotation.OidcAuthProvider(getToken: () => string | Promise<string>)—cacheableis not set (falsy);getHeaders()awaitsgetToken()and returns{ Authorization: 'Bearer ' + token }. Ships with zero OIDC library dependencies — the caller supplies the token callback.
- Returns. Both classes implement
AuthProvider;client.setAuth(provider)swaps the transport's active provider and clears any cached headers. - Throws / errors. Neither provider itself throws; a rejected
getToken()promise propagates out of the request that triggered it. - Example.
client.setAuth(new ApiKeyAuthProvider('new-key')) client.setAuth(new OidcAuthProvider(() => oidcManager.getAccessToken()))
Error hierarchy — WipError and its subclasses
- Purpose. Every thrown error from this library is a
WipError(or a plainErroronly in the onecreateWipClient/FetchTransportconstruction-time case above).FetchTransport.mapResponseErrormaps each non-2xx HTTP status to one of these. - Parameters / shape. All extend
Errorand carrymessage,statusCode?: number,detail?: unknown.Class statusCodeWhen WipErroras constructed Base class; also the fallback for a status the switch doesn't special-case. WipValidationErroralways 422Server returned 400 or 422 — the constructor hardcodes 422regardless of which triggered it, soerr.statusCodenever reflects a raw 400.WipNotFoundError404Entity doesn't exist. WipConflictError409Conflict (most write conflicts surface per-item in BulkResponseinstead).WipAuthError401or403Bad/missing credentials or insufficient permissions; also invalidates the cached auth header and fires onAuthError.WipServerErrorthe 5xx status Server-side error. WipNetworkErrorundefinedTimeout ( AbortError) or afetchfailure below the HTTP layer; carries the originalcause?: Error.WipBulkItemErrorundefinedThrown by single-item convenience methods when the wrapped bulk call's results[0].status === 'error'; carriesindex,itemStatus,errorCode?,details?. - Returns. N/A (these are thrown, not returned).
- Throws / errors. See table.
- Example.
import { WipNotFoundError, WipAuthError, WipBulkItemError, WipError } from '@wip/client' try { await client.defStore.getTerminology('nonexistent') } catch (err) { if (err instanceof WipNotFoundError) console.log('not found:', err.message) else if (err instanceof WipAuthError) console.log('auth failed:', err.statusCode) else if (err instanceof WipBulkItemError) console.log(err.index, err.itemStatus) else if (err instanceof WipError) console.log(err.statusCode, err.detail) }
FetchTransport (advanced usage)
- Purpose. The low-level HTTP transport every service class calls through
BaseService'sget/post/put/patch/del/getBlob/postFormData/streamhelpers. Exported directly for callers who need to hit an endpoint no service class wraps yet. - Parameters. Constructor takes
FetchTransportConfig(same shape asWipClientConfigminus theauthshorthand — takes a resolvedAuthProviderdirectly). Public methods:request<T>(method, path, options?: {body?, params?, headers?, responseType?: 'json'|'blob'|'text', timeout?}): Promise<T>stream(method, path, options?: {params?, headers?, signal?}): Promise<Response>— opens an SSE-style request and returns the rawResponseso the caller readsresponse.bodyitself; never retries.setAuth(auth: AuthProvider | undefined): void
- Returns.
requestresolves the parsed body (undefinedon a 204 or an empty 200 body);streamresolves the rawResponse. - Throws / errors. The
Wip*Errortaxonomy above for non-2xx responses (checked before the caller starts reading a stream's body too). - Example. Not typically constructed directly —
createWipClientbuilds one and shares it across all six services. Advanced callers can stillimport { FetchTransport } from '@wip/client'to drive it standalone.
client.defStore — DefStoreService
-
Purpose. Terminologies, terms, ontology relations, import/export, validation, and audit log, against the def-store (base path
/api/def-store). -
Parameters — methods.
Method Verb + path Notes listTerminologies(params?)GET /terminologiespage,page_size,status,value,namespace.getTerminology(id)GET /terminologies/{id}createTerminology(data)POST /terminologiessingle-item convenience, throws createTerminologies(data[])POST /terminologiesbulk updateTerminology(id, data)PUT /terminologiesthrows deleteTerminology(id, {force?, hardDelete?})DELETE /terminologiesthrows listTerms(terminologyId, params?)GET /terminologies/{id}/termssearchdoes a substring match on value/label/aliasesgetTerm(termId)GET /terms/{id}createTerm(terminologyId, data, {namespace})POST /terminologies/{id}/termssingle-item, throws createTerms(terminologyId, terms[], {namespace, batch_size?, registry_batch_size?})POST /terminologies/{id}/termsbulk updateTerm(termId, data)PUT /termsthrows deprecateTerm(termId, {reason, replaced_by_term_id?})POST /terms/deprecatethrows deleteTerm(termId, {hardDelete?})DELETE /termsthrows importTerminology(data)POST /import-export/importreturns {terminology, terms_result, relationships_result?}exportTerminology(id, {format?, includeInactive?, includeRelationships?, includeMetadata?, languages?})GET /import-export/export/{id}format:'csv'resolves to astringbody instead of JSONimportOntology(data, options)POST /import-export/import-ontologybulk-imports an OBO Graph JSON document (GO/HPO/MONDO-shaped); returns term + relationship counts and elapsed_secondsvalidateValue(data)POST /validatesingle value vs. one terminology bulkValidate({items})POST /validate/bulklistTermRelations({term_id, direction?, relation_type?, ...})GET /ontology/term-relationslistAllTermRelations(params?)GET /ontology/term-relations/allcreateTermRelations(items[], namespace)POST /ontology/term-relationsbulk; namespaceis a required second arg, not on each itemdeleteTermRelations(items[], namespace)DELETE /ontology/term-relationsbulk getAncestors(termId, {relation_type?, max_depth?})GET /ontology/terms/{id}/ancestorsgetDescendants(termId, {relation_type?, max_depth?})GET /ontology/terms/{id}/descendantsgetParents(termId, {relation_type?})GET /ontology/terms/{id}/parentsdepth-1 only, no max_depth;relation_typedefaults tois_aserver-sidegetChildren(termId, {relation_type?})GET /ontology/terms/{id}/childrendepth-1 only getTerminologyAuditLog(terminologyId, params?)GET /audit/terminologies/{id}getTermAuditLog(termId, params?)GET /audit/terms/{id}getRecentAuditLog(params?)GET /audit/Term-ontology edges are relations (
relation_type, e.g.is_a,part_of); document-to-document edges are relationships — the two method families are deliberately named apart. -
Returns. Per-method: see table; list endpoints return a
PaginatedResponse<T>shape (items,total,page,page_size,pages); write endpoints returnBulkResultItem(single, throwing) orBulkResponse(bulk, non-throwing). -
Throws / errors. Single-item convenience methods (
createTerminology,updateTerminology,deleteTerminology,createTerm,updateTerm,deprecateTerm,deleteTerm) throwWipBulkItemErrorwhen the item'sstatus === 'error'. All methods can throw the transport'sWip*Errortaxonomy for HTTP-level failures. -
Example.
const bulk = await client.defStore.createTerms('COUNTRY', [ { value: 'US', label: 'United States' }, { value: 'CA', label: 'Canada' }, ], { namespace: 'wip' }) // bulk.succeeded, bulk.failed, bulk.results[i].status
client.templates — TemplateStoreService
- Purpose. Document schemas: CRUD, versioning, inheritance, draft-activation, and edge-type (relationship-template) endpoint management, against the template-store (base path
/api/template-store). - Parameters — methods.
Method Verb + path Notes listTemplates(params?)GET /templateslatest_only,status,extends,value,namespacegetTemplate(id, version?)GET /templates/{id}latest version if versionomittedgetTemplateRaw(id, version?)GET /templates/{id}/rawunresolved inheritance getTemplateByValue(value, {namespace?})GET /templates/by-value/{value}a valueis unique only within a namespace — omittingnamespaceunder a cross-namespace key returns the latest match across all namespacesgetTemplateByValueRaw(value, namespace)GET /templates/by-value/{value}/rawnamespacerequiredgetTemplateVersions(value, {namespace?})GET /templates/by-value/{value}/versionssame cross-namespace caveat as getTemplateByValuegetTemplateByValueAndVersion(value, version, {namespace?})GET /templates/by-value/{value}/versions/{version}same caveat getTemplateVersionsById(templateId)GET /templates/{id}/versionstemplate_idis globally unique — no namespace neededcreateTemplate(data, {onConflict?: 'error'|'validate'})POST /templatessingle, throws. onConflict:'validate'makes creation idempotent: identical schema →status:'unchanged'; a compatible superset (added optional fields only) → new version; incompatible → throws witherrorCode:'incompatible_schema'and a diff indetailscreateTemplates(data[], {onConflict?})POST /templatesbulk updateTemplate(id, data)PUT /templatesthrows; creates a new version, old version stays active deleteTemplate(id, {version?, force?, hardDelete?, updatedBy?})DELETE /templatesthrows; soft-delete by default validateTemplate(id, request?)POST /templates/{id}/validategetChildren(id)GET /templates/{id}/childrendirect inheritance children getDescendants(id)GET /templates/{id}/descendantsfull inheritance subtree activateTemplate(id, {namespace, dry_run?})POST /templates/{id}/activatedraft → active; dry_runpreviews without writingreactivateTemplate(id, version, {namespace})POST /templates/{id}/reactivaterestores a soft-deleted specific version to active; versionis required (no "latest" default); idempotent on an already-active version, rejected on a draft versioncascadeTemplate(id)POST /templates/{id}/cascadepropagates a parent template's change down to its inheritance descendants addEdgeTypeEndpoints(id, {namespace, addSourceTemplates?, addTargetTemplates?})POST /templates/{id}/endpointsadditively widens a relationship template's allowed source/target templates in place, preserving existing edges. Append-only — endpoint removal is not supported. Idempotent on already-allowed endpoints - Returns. See table; write endpoints follow the same
BulkResultItem/BulkResponsesplit asdefStore. - Throws / errors.
createTemplate,updateTemplate,deleteTemplatethrowWipBulkItemErroron item failure.Wip*Errortaxonomy otherwise. - Example.
await client.templates.createTemplate({ value: 'LAB_RESULT', label: 'Lab Result', namespace: 'wip', identity_fields: ['patient_email', 'test_date'], fields: [ { name: 'patient_email', label: 'Patient Email', type: 'string', mandatory: true, semantic_type: 'email', metadata: {} }, { name: 'test_date', label: 'Test Date', type: 'date', mandatory: true, metadata: {} }, ], })
client.documents — DocumentStoreService
- Purpose. Documents: CRUD, versions, table view/CSV export, identity-hash lookup, filtered/sorted query, relationship-graph traversal, template-version migration, spreadsheet import, replay, and namespace backup/restore — against the document-store (base path
/api/document-store). - Parameters — methods.
Method Verb + path Notes listDocuments(params?)GET /documentstemplate_id,template_value,status,page,page_size,cursor,namespacegetDocument(id, version?, namespace?)GET /documents/{id}idmay be any Registry-resolvable synonym/valuecreateDocument(data)POST /documentssingle, throws; upserts via identity hash createDocuments(data[])POST /documentsbulk updateDocument(documentId, patch, {ifMatch?})PATCH /documentssingle, throws. RFC 7396 JSON Merge Patch: objects deep-merge, arrays replace, nulldeletes the key. Cannot change identity fields — usecreateDocumentfor that.ifMatchenforces optimistic concurrency (concurrency_conflicton mismatch)updateDocuments(items[])PATCH /documentsbulk merge-patch deleteDocument(id, {updatedBy?, hardDelete?, version?})DELETE /documentssingle, throws; soft-delete by default deleteDocuments(ids[], {hardDelete?})DELETE /documentsbulk archiveDocument(id, archivedBy?)POST /documents/archivesingle, throws; no matching restoreDocumentmethod exists on this clientvalidateDocument(data)POST /validation/validateside-effect-free validateDocuments({template_id, namespace, template_version?, items})POST /validation/validate-bulkmany payloads against one template, no writes getVersions(id)GET /documents/{id}/versionsgetVersion(id, version)GET /documents/{id}/versions/{version}getTableView(templateId, params?)GET /table/{id}denormalized/spreadsheet-shaped projection exportTableCsv(templateId, params?)GET /table/{id}/csvreturns a BlobgetLatestDocument(id)GET /documents/{id}/latestgetDocumentByIdentity(identityHash, includeInactive?, namespace?)GET /documents/by-identity/{hash}queryDocuments(body, namespace?)POST /documents/queryfiltered/sorted query; namespacerides the?namespace=query param — putting it in the body is rejected (extra_forbidden)getDocumentRelationships(documentId, {direction?, template?, namespace?, active_only?, page?, page_size?, include?})GET /documents/{id}/relationshipslists relationship documents pointing at/from documentId;include:'peers'embeds aPeerProjectionper itemtraverseDocuments(documentId, {depth?, types?, direction?, namespace?})GET /documents/{id}/traverseBFS over relationship documents; capped at depth=10/max_nodes=1000, response setstruncated:truewhen a cap firesmigrateDocuments(request, namespace?)POST /documents/migratesee Usage patterns previewImport(file, filename)POST /import/previewmultipart; parses a spreadsheet, returns headers + sample rows importDocuments(file, filename, {template_id, column_mapping, namespace, skip_errors?})POST /importmultipart; maps spreadsheet columns to template fields startReplay({filter?, throttle_ms?, batch_size?})POST /replay/startrepublishes stored documents as events for downstream consumers getReplayStatus(sessionId)GET /replay/{id}pauseReplay(sessionId)/resumeReplay(sessionId)POST /replay/{id}/pause//resumecancelReplay(sessionId)DELETE /replay/{id}startBackup(namespace, request?)POST /backup/namespaces/{ns}/backupreturns a job snapshot immediately (server runs the export in a worker thread) startRestore(namespace, archive, options?, filename?)POST /backup/namespaces/{ns}/restore(multipart)streams the archive to disk server-side — safe for multi-GB uploads getBackupJob(jobId)GET /backup/jobs/{id}latest persisted snapshot listBackupJobs(params?)GET /backup/jobsdownloadBackupArchive(jobId)GET /backup/jobs/{id}/downloadreturns a BlobdeleteBackupJob(jobId)DELETE /backup/jobs/{id}streamBackupJobEvents(jobId, signal?)GET /backup/jobs/{id}/events(SSE)async generator, see below - Returns. See table.
DocumentMigrateResponseextendsBulkResponsewithdry_run,template_id,from_version,to_version. - Throws / errors.
createDocument,updateDocument,deleteDocument,archiveDocumentthrowWipBulkItemErroron item failure.downloadBackupArchivethrowsWipConflictError(409, job not yet complete),WipNotFoundError(404, unknown job), or aWipErrorwith status 410 (archive already cleaned up from disk).streamBackupJobEventsthrows a plainError('SSE response has no body')if the response has no readable body. - Example — async iterator over backup progress:
const job = await client.documents.startBackup('wip', {}) const ctrl = new AbortController() for await (const evt of client.documents.streamBackupJobEvents(job.job_id, ctrl.signal)) { console.log(evt.phase, evt.percent, evt.message) if (evt.status === 'complete' || evt.status === 'failed') break }
client.files — FileStoreService
- Purpose. Binary file upload/download/metadata and integrity maintenance. Files live in MinIO and are tracked in the document-store's MongoDB — this service is reached through the document-store's port, at base path
/api/document-store/files. - Parameters — methods.
Method Verb + path Notes uploadFile(file, filename?, metadata?, namespace?)POST `` (multipart) accepts FileorBloblistFiles(params?)GET `` status,content_type,category,tags,uploaded_by,page,page_sizegetFile(fileId)GET /{id}getDownloadUrl(fileId, expiresIn?)GET /{id}/downloadreturns a time-limited download_urldownloadFileContent(fileId)GET /{id}/contentreturns a BlobdirectlyupdateMetadata(fileId, data)PATCH `` single, throws deleteFile(fileId)DELETE `` single, throws; soft-delete deleteFiles(fileIds[])DELETE `` bulk hardDeleteFile(fileId)DELETE /{id}/hardpermanently removes the object from MinIO listOrphans({older_than_hours?, limit?})GET /orphans/listfiles with no document reference findByChecksum(checksum)GET /by-checksum/{sha256}checkIntegrity()GET /health/integritystatus: 'healthy'|'warning'|'error';issues[]typed'orphan_file'|'missing_storage'|'broken_reference'getFileDocuments(fileId, page?, pageSize?)GET /{id}/documentswhich documents reference this file - Returns. See table;
FileEntityfor single-file responses,PaginatedResponse<FileEntity>forlistFiles. - Throws / errors.
updateMetadata,deleteFilethrowWipBulkItemErroron item failure.Wip*Errortaxonomy otherwise. - Example.
const file = await client.files.uploadFile(myFile, 'report.pdf', { description: 'Monthly report', tags: ['report', 'monthly'], allowed_templates: ['REPORT'], }) const integrity = await client.files.checkIntegrity()
client.registry — RegistryService
- Purpose. Namespace lifecycle, entry lookup/search, synonyms, entry merge, namespace export/import, grants, and API-key management — WIP's central ID authority (base path
/api/registry). - Parameters — methods.
Method Verb + path Notes listNamespaces(includeArchived?)GET /namespacesdefault falsegetNamespace(prefix)GET /namespaces/{prefix}getNamespaceStats(prefix)GET /namespaces/{prefix}/statscreateNamespace(data)POST /namespacesupdateNamespace(prefix, data)PUT /namespaces/{prefix}upsertNamespace(prefix, data)PUT /namespaces/{prefix}same call as updateNamespace; named separately for self-healing bootstrap callers — succeeds whether the namespace exists or notarchiveNamespace(prefix, archivedBy?)POST /namespaces/{prefix}/archiverestoreNamespace(prefix, restoredBy?)POST /namespaces/{prefix}/restoredeleteNamespace(prefix, deletedBy?)DELETE /namespaces/{prefix}client always sends confirm: trueinitializeWipNamespace()POST /namespaces/initialize-wipone-time default-namespace bootstrap listEntries(params?)GET /entrieslookupEntry(entryId)POST /entries/lookup/by-idwraps a bulk call, unwraps results[0]searchEntries(term, {namespaces?, entityTypes?, includeInactive?, limit?})POST /search/by-termreturns {hits, total}—totalis the full server-side match count even whenlimitboundshitsunifiedSearch(params)GET /entries/searchseparate endpoint and response shape ( RegistrySearchResponse) fromsearchEntriesgetEntry(entryId)GET /entries/{id}addSynonym(request)POST /synonyms/addmaps multiple composite keys to one entity removeSynonym(request)POST /synonyms/removemergeEntries({preferred_id, deprecated_id})POST /synonyms/mergekeeps preferred_id, deprecatesdeprecated_iddeactivateEntry(entryId, updatedBy?)DELETE /entriesexportNamespace(prefix, {include_files?})POST /namespaces/{prefix}/exportdownloadExport(exportId)GET /namespaces/exports/{id}returns a BlobimportNamespace(file, {target_prefix?, mode?, imported_by?})POST /namespaces/import(multipart)mode: 'create'|'merge'|'replace'listGrants(prefix)GET /namespaces/{prefix}/grantscreateGrants(prefix, grants[])POST /namespaces/{prefix}/grantsbulk revokeGrants(prefix, grants[])DELETE /namespaces/{prefix}/grantsbulk listAPIKeys(params?)GET /api-keysreturns a paginated envelope ( .items), not a bare arraycreateAPIKey(request)POST /api-keysresponse extends APIKeyInfowith a one-timeplaintext_keygetAPIKey(name)GET /api-keys/{name}updateAPIKey(name, request)PATCH /api-keys/{name}revokeAPIKey(name)DELETE /api-keys/{name} - Returns. See table; most mutation endpoints unwrap a single-item bulk response inline (
lookupEntry,addSynonym,removeSynonym,mergeEntries,deactivateEntry) rather than exposing the raw envelope. - Throws / errors. None of
RegistryService's methods are single-item convenience wrappers overbulkWriteOne, so none throwWipBulkItemError; failures surface as the transport'sWip*Errortaxonomy (e.g. a 422 on an unknown query param). - Example.
const { hits, total } = await client.registry.searchEntries('patient record', { namespaces: ['wip'], entityTypes: ['documents'], limit: 20, })
client.reporting — ReportingSyncService
- Purpose. Cross-service search, ad-hoc SQL against the PostgreSQL mirror, sync status/batch-sync control, table introspection, integrity checks, and reference tracking (base path
/api/reporting-sync). Reporting-Sync mirrors MongoDB data into PostgreSQL via NATS events; this service is the client for that mirror, not for the source-of-truth stores. - Parameters — methods.
Method Verb + path Notes healthCheck()GET /health(root, not under/api/reporting-sync)never throws — catches and returns falsegetSyncStatus()GET /statusrunQuery(sql, params?, {timeout_seconds?, max_rows?, namespace?})POST /queryread-only SQL against the per-namespace PostgreSQL schema ( "<ns>"."doc_<value>"); passnamespaceto resolve unqualified table names, or schema-qualify tables yourself for cross-namespace queriestriggerBatchSyncAll({force?, page_size?})POST /sync/batchone job per sync_enabledtemplate; async, poll for progresstriggerBatchSync(templateValue, {force?, page_size?})POST /sync/batch/{value}single template, async triggerTerminologySync(namespace, pageSize?)POST /sync/batch/terminologiessynchronous, inline result triggerTermSync(namespace, pageSize?)POST /sync/batch/termssynchronous triggerTermRelationSync(namespace, pageSize?)POST /sync/batch/term_relationssynchronous listBatchJobs()GET /sync/batch/jobsin-memory — lost on reporting-sync restart getBatchJob(jobId)GET /sync/batch/jobs/{id}404 if unknown cancelBatchJob(jobId)DELETE /sync/batch/jobs/{id}clearCompletedJobs()DELETE /sync/batch/jobsawaitSync({query?, params?, timeout?, interval?})polls runQueryorgetSyncStatussee Usage patterns; throws a WipErroron timeoutlistTables(tableName?)GET /tablesgetTableSchema(templateValue)GET /schema/{value}getIntegrityCheck(params?)GET /health/integritystatus: 'healthy'|'warning'|'error'|'partial'search(params)POST /searchper-type paginated buckets keyed by entity type on results;mode:'auto'|'fts'|'substring'picks the search strategy;include_inactivedefaultsfalsegetRecentActivity({types?, limit?})GET /activity/recentgetTermDocuments(termId, limit?)GET /references/term/{id}/documentsgetEntityReferences(entityType, entityId)GET /entity/{type}/{id}/referencesoutgoing references from one entity getReferencedBy(entityType, entityId, limit?)GET /entity/{type}/{id}/referenced-byincoming references to one entity - Returns. See table;
awaitSyncresolvesvoid. - Throws / errors.
awaitSyncthrows aWipError('Sync timeout: ...') if its deadline passes before the expected row/event appears. Other methods follow the standardWip*Errortaxonomy;healthCheckis the one method in the whole library that swallows all errors and returns a boolean instead. - Example.
const result = await client.reporting.runQuery( 'SELECT status, count(*) FROM doc_patient_record GROUP BY status', [], { namespace: 'wip' }, ) // result.columns, result.rows, result.row_count, result.truncated
templateToFormSchema(template: Template): FormField[]
- Purpose. Converts a WIP
Templateinto an array of framework-agnostic form field descriptors — oneFormFieldpertemplate.fields[]entry. - Parameters.
template: Template(typically fromclient.templates.getTemplateByValue(...)). - Returns.
FormField[], each withname,label,inputType,required,defaultValue?,isIdentity(true when the field is intemplate.identity_fields), plus type-specific extras:terminologyCode(term fields),referenceType/targetTemplates/targetTerminologies(reference fields),fileConfig(file fields),arrayItemType/arrayTerminologyCode(array fields),children(object fields — not recursively populated by this function's own code path beyond the top-level map),validation,semanticType. Field-type → input-type mapping:string→text,number→number,integer→integer,boolean→checkbox,date→date,datetime→datetime,term→select,reference→search,file→file,object→group,array→list. - Throws / errors. None — pure, synchronous data transform.
- Example.
const template = await client.templates.getTemplateByValue('PATIENT_RECORD') const formFields = templateToFormSchema(template)
bulkImport<T>(items: T[], writeFn: (batch: T[]) => Promise<BulkResponse>, options?: BulkImportOptions): Promise<BulkImportProgress>
- Purpose. Batches a large item array into chunks and calls
writeFnper chunk, with progress reporting and optional concurrency. - Parameters.
items: T[];writeFn: (batch: T[]) => Promise<BulkResponse>(typically a bulk create/update method, pre-bound to its fixed args);options?: { batchSize?: number (default 100), concurrency?: number (default 1), continueOnError?: boolean (default true), onProgress?: (p: BulkImportProgress) => void }. - Returns.
Promise<BulkImportProgress>—{ processed, total, succeeded, failed }, the cumulative totals after all batches (or after the batch that stopped it, ifcontinueOnError:falsehit a failure). - Throws / errors. Propagates whatever
writeFnthrows (e.g. aWipErrorfrom a bulk endpoint's own request failure — as distinct from a per-itemstatus:'error'inside a successfulBulkResponse, whichbulkImporttreats as normal accounting, not a throw). - Example.
const result = await bulkImport( largeTermList, (batch) => client.defStore.createTerms('COUNTRY', batch, { namespace: 'wip' }), { batchSize: 500, concurrency: 1, onProgress: ({ processed, total }) => console.log(`${processed}/${total}`) }, )
resolveReference(client: WipClient, templateId: string, searchTerm: string, limit?: number): Promise<ResolvedReference[]>
- Purpose. Autocomplete/typeahead helper for a reference field: finds documents of
templateIdwhose data containssearchTerm. - Parameters.
client: WipClient;templateId: string;searchTerm: string;limit: number = 10. - Returns.
Promise<ResolvedReference[]>—{ documentId, displayValue, identityFields }[]. - Throws / errors. Whatever
client.documents.queryDocumentsthrows (theWip*Errortaxonomy). - Example.
Fetches the 100 most recent active documents forconst matches = await resolveReference(client, 'TPL-PATIENT', 'jane', 10)templateId(page_size: 100, sort_by: 'created_at', sort_order: 'desc') and filters client-side by substring match across string-typeddatavalues — see Gotchas.
buildQueryString(params: Record<string, unknown>): string
- Purpose. Builds a URL query string from a params object, handling
undefined/null(dropped), arrays (repeated key), and booleans ('true'/'false'strings). Used internally byFetchTransport; exported for callers building their own requests againstFetchTransport/rawfetch. - Parameters.
params: Record<string, unknown>. - Returns.
string— either''or a string starting with?. - Throws / errors. None.
- Example.
buildQueryString({ page: 1, tags: ['a', 'b'], archived: false })→'?page=1&tags=a&tags=b&archived=false'.
Usage patterns
1. Bootstrap a client and read.
import { createWipClient } from '@wip/client'
const client = createWipClient({
baseUrl: '/wip', // browser behind a Vite proxy; use '' for direct Caddy, or an absolute URL in Node.js
auth: { type: 'api-key', key: 'dev_master_key_for_testing' },
})
const terminologies = await client.defStore.listTerminologies({ status: 'active' })
2. Identity-preserving document upsert. Creating a document twice with the same identity_fields values versions the same document instead of duplicating it — this is the platform's identity-hash-driven natural-upsert, surfaced through the single-item convenience method:
// First call: creates version 1
await client.documents.createDocument({
template_id: 'PATIENT_RECORD', namespace: 'wip',
data: { email: 'jane@example.com', name: 'Jane' },
})
// Second call, same identity field (email): creates version 2 of the SAME document
await client.documents.createDocument({
template_id: 'PATIENT_RECORD', namespace: 'wip',
data: { email: 'jane@example.com', name: 'Jane Doe' },
})
3. Batch-import with progress, via bulkImport. bulkImport chunks a large array and calls a bulk write method per chunk — the pattern the library's own tests exercise:
import { bulkImport } from '@wip/client'
const result = await bulkImport(
tenThousandTerms,
(batch) => client.defStore.createTerms('ICD10', batch, { namespace: 'wip' }),
{ batchSize: 500, concurrency: 1, continueOnError: true,
onProgress: ({ processed, total, failed }) => console.log(`${processed}/${total} (${failed} errors)`) },
)
// result: { processed, total, succeeded, failed }
4. Dry-run then apply a template-version migration. A validated, identity-preserving bulk re-pin of every active document from one template version to another (the two versions must declare the same identity_fields):
const preview = await client.documents.migrateDocuments({
template_id: 'PATIENT_RECORD', from_version: 1, to_version: 2, // dry_run defaults true
})
if (preview.failed === 0) {
await client.documents.migrateDocuments({
template_id: 'PATIENT_RECORD', from_version: 1, to_version: 2, dry_run: false,
})
}
Gotchas
- Bulk-first HTTP 200. Every write endpoint accepts an array and always returns HTTP 200, even when items fail — errors are per-item in
results[]. Single-item convenience methods (createDocument,createTerminology, ...) wrap the bulk call and throwWipBulkItemErroron a failed item; the plural bulk methods (createDocuments,createTerminologies, ...) never throw for item-level failures — checkresults[i].statusandresponse.failedyourself. WipValidationError.statusCodeis always422, even when the server responded 400 — the constructor hardcodes it. Don't branch onerr.statusCode === 400.- Namespace scoping under a multi-namespace (admin) key. A value-form identifier (
template_value, or a document/templatevalueused asid) has no namespace context to resolve against under a key scoped to multiple namespaces.queryDocuments,getDocument,getDocumentByIdentity,getTemplateByValue,getTemplateVersions,getTemplateByValueAndVersionall take an explicitnamespaceargument (or option) for this reason — a single-namespace key derives it automatically and can omit it, but a multi-namespace key without it either 422s (queryDocuments) or silently resolves against the wrong/first match (the by-value template lookups).queryDocuments'snamespacerides the?namespace=query string specifically — putting it in the request body is rejected (extra_forbidden). - Auth header caching is opt-in per provider.
ApiKeyAuthProvidersetscacheable = true, so its (synchronous) header is computed once and reused until a 401/403 orsetAuth()clears it.OidcAuthProviderdoes not setcacheable, sogetHeaders()— and therefore the consumer's token callback — runs fresh on every single request; a customAuthProviderinherits whichever behavior it sets (or doesn't set) oncacheable. - Retry is GET-only. GET requests retry automatically on 502/503/504 with exponential backoff (default: 2 retries,
baseDelayMs: 500, capped atmaxDelayMs: 5000). POST/PUT/PATCH/DELETE never retry on any status — a failed mutation fails immediately, so a caller doesn't risk a double-create from a retried write. - Restore's
modedefault is a trap.startRestoreomitsmodeby default, and the server default is'restore'— writing back into the archive's source namespace, nottarget_namespace, unlesstarget_namespaceis on a single-namespace archive being redirected explicitly. A multi-namespace archive restores each namespace to itself and rejects a target override entirely.'fresh'is accepted by the type but rejected with a 400 server-side — there is currently no restore mode that lands an archive in a brand-new namespace with new IDs. Passmode: 'restore'explicitly whenever the destination namespace matters. startBackup'sinclude_files: truebuffers in memory. The default isfalse; flipping it against a namespace with non-trivial file content makes the server-side archive writer buffer all blobs in memory before it can stream them out.resolveReferenceis a client-side filter over 100 recent documents, not a server-side search — it fetches the 100 most recently created active documents for the target template and filters them by substring match in the browser/Node process. For large datasets this misses older matches; usequeryDocumentswith real filters instead.- Soft delete is the default everywhere.
deleteDocument,deleteTemplate,deleteTerminology,deleteTerm,deleteFileall setstatus: 'inactive'by default; the exceptions arefiles.hardDeleteFile()(always permanent) and thehardDeleteoption on the others (still subject to the namespace'sdeletion_mode, and for terms, the terminology'smutableflag). identity_fieldsis a design decision, not a formality. Zero identity fields makes everycreateDocumentcall append a new document (no update path via identity). Too many identity fields — including anything that varies per submission, like a timestamp — turns every legitimate correction into an accidental new document instead of a new version.
See also
- Document-Store API, Registry API, Template-Store API, Def-Store API, Reporting & Analytics (REST + SQL layer) — the REST APIs this library wraps, one service class each.
