@dataverse-kit/portal-api-service
v0.2.0
Published
Power Pages portal (/_api/) adapters for the workspace's Dataverse seams — PortalApiService (canonical IReadWriteApiService: CRUD + FetchXML + @odata.bind + $ref) and PortalSeamService (the create-app 6-method seam), sharing one hardened transport (anti-f
Maintainers
Readme
@dataverse-kit/portal-api-service
IReadWriteApiService implementation for the Power Pages portals Web API (/_api/). This is
the D2 portal adapter: it gives portal SPA code the same injectable read/write seam that Dynamics
code gets from @dataverse-kit/api-service,
so a <Component apiService={svc} /> that only reads and writes records is host-portable across
Dynamics and a Power Pages portal — moving hosts becomes a one-line provider swap.
Zero React deps. Framework-agnostic. Browser + Node compatible (Node 18+, ES2020 build).
Install
npm install --save @dataverse-kit/portal-api-serviceWhy a separate interface (IReadWriteApiService, not IApiService)
The portal Web API serves record CRUD + FetchXML + $ref association and nothing else — no
$batch, no metadata endpoints, no view management, no unbound actions/functions. Rather than
implement the full 18-method IApiService and throw "unsupported" on 11 of them at runtime,
PortalApiService implements the 7-method IReadWriteApiService subset (defined in
@dataverse-kit/api-service, which IApiService extends). The type system then stops a portal
service being passed where batch/metadata/view/action operations are called — a compile-time error
rather than a runtime throw.
Usage
import { PortalApiService, bindLookup } from '@dataverse-kit/portal-api-service';
const svc = new PortalApiService(); // defaults: basePath '/_api', globalThis.fetch
const { value } = await svc.retrieveMultipleRecords<Account>('accounts', '$select=name&$top=25');
const { id } = await svc.createRecord(
'accounts',
bindLookup({ name: 'Contoso' }, 'primarycontactid', 'contacts', contactId), // @odata.bind lookup
);
await svc.updateRecord('accounts', id, { telephone1: '555-0100' });
await svc.retrieveMultipleFetchXml('accounts', '<fetch>…</fetch>'); // portals support ?fetchXml=Because the component depends only on the IReadWriteApiService type, the same code runs on a
Dynamics host by injecting any @dataverse-kit/api-service implementation instead:
import { ServiceFactory } from '@dataverse-kit/api-service';
const svc = ServiceFactory.create(); // Xrm / Fetch / Mock — same seam, no component changeInterface surface (7 methods)
retrieveRecord<T>(entitySetName, id, options?)retrieveMultipleRecords<T>(entitySetName, options?)— OData query;@odata.nextLinksurfaced in the enveloperetrieveMultipleFetchXml<T>(entitySetName, fetchXml)— portals support?fetchXml=createRecord(entitySetName, data)— returns the new id (from theentityidheader)updateRecord(entitySetName, id, data)deleteRecord(entitySetName, id)associateRecord(entitySetName, id, relationshipName, related)—$reffor collection-valued nav props
Single-valued lookups are written with bindLookup(...) (@odata.bind) on create/update.
What it adds over a hand-rolled portal client
Microsoft's /integrate-webapi Power Pages plugin emits per-table free functions over a
powerPagesFetch transport. This package keeps that transport's proven behaviours (anti-forgery
token, 403-90040107 invalidate+retry, 429/5xx backoff, @odata.nextLink in the read envelope) and
adds — each verified needed on a live portal:
- The injectable interface (
IReadWriteApiService) the plugin's free functions lack. - FetchXML support (the plugin generates none).
- Identifier/GUID validators on every spliced
entitySetName/id(the plugin interpolatedentitySetraw into/_api/${entitySet}). - In-flight anti-forgery memoization — concurrent first writes share one
/_layout/tokenhtmlrequest (the plugin's cache-check didn't memoize the pending promise). - Idempotency-aware retry — a POST that returns 5xx is not auto-retried, because CDX portals were observed returning 503 on writes that nonetheless committed; retrying would double-create.
Second adapter: the create-app 6-method seam (PortalSeamService)
The create-dynamics-app templates ship their own data-access seam — a 6-method IApiService
(updateRecord, createRecord, deleteRecord, retrieveMultipleRecords(entity, **fetchXml**)→{ entities },
executeRequest, associateRecord) — distinct from the canonical IReadWriteApiService above.
PortalSeamService is the portal implementation of that shape, so a create-app project can run its
components on a Power Pages portal by swapping in one service:
import { PortalSeamService } from '@dataverse-kit/portal-api-service';
const svc = new PortalSeamService(); // or ({ apiService: existingPortalApiService })
const { entities } = await svc.retrieveMultipleRecords('accounts', '<fetch>…</fetch>');
const { id } = await svc.createRecord('accounts', { name: 'Contoso' });It is a thin wrapper over PortalApiService — every CRUD/associate call delegates, reusing the same
hardened transport (nothing re-implemented). Two shape differences are bridged:
- reads are FetchXML-only and return
{ entities }(the portal{ value }envelope is mapped over); executeRequestrejects — the portal Web API has no unbound actions/functions/custom APIs, so it fails loudly rather than silently no-op'ing. Move server-side logic to Power Pages server logic or a cloud flow.
ICreateAppSeam is declared with unknown/Record<string, unknown> (this package stays any-free) but
is structurally assignable to the create-app any-typed IApiService, so no cast is needed.
Notes
- Site gating (per table) is still required:
Webapi/<table>/enabled,Webapi/<table>/fields(mandatory — list the_<lookup>_valueread form too), and a table permission for the web role. - A lookup write (
bindLookup/associateRecord) needs both Append AND AppendTo on both the source and target permissions — the textbook Dataverse split alone yieldsEntityPermissionAppendToIsMissingDuringAssociationChangeon portals. - Fluent UI v9 is broken on Power Pages — use v8. (Unrelated to this package, which ships no UI.)
Testing
npm test # vitest — transport (anti-forgery memoization, idempotency-aware retry) + service
npm run typecheck