@narrative.io/marketplace-sdk
v0.3.0
Published
Generated TypeScript SDK for the Narrative Marketplace API
Readme
@narrative.io/marketplace-sdk
A TypeScript client for the Narrative platform API, generated from the platform's OpenAPI spec with Hey API. It covers the derivation rules and the three identity endpoints.
- Typed end to end. Request and response types come from the spec, so a field that changes upstream changes the types here.
- One dependency. The fetch client. Zod, Pinia Colada, and MSW are optional peers, pulled in only if you import their subpath.
- Batteries on separate shelves. Validation schemas, Vue query factories, MSW handlers, and test fixtures each live behind their own import.
- ESM only. Works in Node 18+, Bun, Deno, and browsers — anywhere with a
global
fetch.
Install
npm install @narrative.io/marketplace-sdkpnpm add @narrative.io/marketplace-sdk
yarn add @narrative.io/marketplace-sdk
bun add @narrative.io/marketplace-sdkQuick start
Configure the default client once at startup, then call the SDK functions:
import {
configureNarrativeClient,
getDerivations,
} from "@narrative.io/marketplace-sdk";
configureNarrativeClient({ token: process.env.NARRATIVE_TOKEN! });
const { data } = await getDerivations({
query: { source_attribute_id: [123] },
});To reach more than one environment or company from the same process, build a client per target and pass it to each call:
import {
createNarrativeClient,
getDerivations,
} from "@narrative.io/marketplace-sdk";
const dev = createNarrativeClient({
token: process.env.NARRATIVE_DEV_TOKEN!,
environment: "dev",
});
const { data } = await getDerivations({ client: dev });Authentication
Both factories take a token, either a string or a function returning one. The
Bearer prefix is added for you, so pass the token on its own. A function is
re-read per request, which is what to use for a token that rotates:
configureNarrativeClient({ token: () => vault.currentNarrativeToken() });Environments
environment selects the base URL and defaults to prod:
| environment | Base URL |
| ------------- | ------------------------------ |
| "prod" | https://api.narrative.io |
| "dev" | https://api-dev.narrative.io |
Tokens are environment-specific — a prod token is not accepted by dev, so switch the token alongside the environment.
configureNarrativeClient({
token: process.env.NARRATIVE_DEV_TOKEN!,
environment: "dev",
});NARRATIVE_BASE_URLS exports the mapping. For a local API or a data plane,
pass baseUrl instead; it takes precedence over environment.
Errors
Calls reject on any non-2xx response. To handle the failure as a value instead,
pass throwOnError: false on the call and read error from the result:
const { data, error, response } = await getDerivations({
throwOnError: false,
});
if (error) console.error(response?.status, error);Pass it per call rather than on the client. createNarrativeClient({ token,
throwOnError: false }) does stop the rejection at runtime, but TypeScript still
types the result as the throwing variant — error is missing from the type and
data looks present while being undefined. The option at the call site is
what changes the type.
response is optional on that result, because an error can come from building
the request or from the network, before there is any response.
API
Every operation is an exported function taking one options object and returning
{ data, error, response }.
| Function | Request |
| -------------------------------------------- | --------------------------------------------------- |
| getAttributes | GET /attributes |
| postAttributes | POST /attributes |
| deleteAttributesByAttributeNameOrId | DELETE /attributes/{attribute_name_or_id} |
| getAttributesByAttributeNameOrId | GET /attributes/{attribute_name_or_id} |
| putAttributesByAttributeNameOrId | PUT /attributes/{attribute_name_or_id} |
| getAttributesByAttributeNameOrIdReferences | GET /attributes/{attribute_name_or_id}/references |
| getDerivations | GET /derivations |
| postDerivations | POST /derivations |
| deleteDerivationsByDerivationId | DELETE /derivations/{derivation_id} |
| getDerivationsByDerivationId | GET /derivations/{derivation_id} |
| patchDerivationsByDerivationId | PATCH /derivations/{derivation_id} |
| getWhoami | GET /whoami |
| getCompanyInfoWhoami | GET /company-info/whoami |
| getInstallationsWhoami | GET /installations/whoami |
| getWebhooks | GET /webhooks |
| postWebhooks | POST /webhooks |
| deleteWebhooksByWebhookSubscriptionId | DELETE /webhooks/{webhook_subscription_id} |
| getWebhooksByWebhookSubscriptionId | GET /webhooks/{webhook_subscription_id} |
| getAccessTokensTokens | GET /access-tokens/tokens |
| postAccessTokensTokens | POST /access-tokens/tokens |
| deleteAccessTokensTokensByAccessTokenId | DELETE /access-tokens/tokens/{access_token_id} |
| getAccessTokensTokensByAccessTokenId | GET /access-tokens/tokens/{access_token_id} |
| putAccessTokensTokensByAccessTokenId | PUT /access-tokens/tokens/{access_token_id} |
The names come from the method and path, because the spec's operations carry no
operationId.
Identity
Three calls answer "who is this token", and they answer different questions.
getWhoami is the one to reach for. It returns the user behind the token — id,
name, email, role, the companies they can reach, the company the token is
currently scoped to, their permissions, and when the token expires.
const { data: me } = await getWhoami();
console.log(me.email, me.role, me.current_company_scope?.name);getCompanyInfoWhoami returns only the company id, plus the user id when asked:
const { data } = await getCompanyInfoWhoami({ query: { with_user_id: true } });getInstallationsWhoami is for app tokens. It reports which installation the
token operates under, and 403s on a token that is not installation-bound.
Two things about the response types are easy to trip on. getWhoami returns
WhoamiResponse and getCompanyInfoWhoami returns WhoAmIResponse; the names
differ only in capitalization, because the spec names the schemas that way. And
every field on WhoamiResponse is optional — the spec marks none of them
required, so me.email types as string | undefined even though the API
always sends it.
Entry points
| Import | What it gives you | Needs |
| -------------------------------------------- | -------------------------------------- | ----------------- |
| @narrative.io/marketplace-sdk | The client, the operations, the types | — |
| @narrative.io/marketplace-sdk/zod | A Zod schema per type in the spec | zod |
| @narrative.io/marketplace-sdk/colada | Pinia Colada query and mutation factories | @pinia/colada |
| @narrative.io/marketplace-sdk/msw | An MSW request handler per operation | msw |
| @narrative.io/marketplace-sdk/fixtures | A fixture factory per object schema | — |
zod, @pinia/colada, and msw are optional peer dependencies, so installing
this package pulls in none of them. Add whichever subpath you import:
npm install zod # for /zod
npm install @pinia/colada # for /colada
npm install --save-dev msw # for /mswImporting a subpath without its peer installed fails at resolution, not silently at runtime.
Zod schemas
@narrative.io/marketplace-sdk/zod exports a schema per type in the spec —
zDerivationResponse, zDerivationCreateRequest, zWhoamiResponse,
zInstallationInfo, and so on. They carry the spec's constraints rather than
just its shapes, so a cost of 0 fails against minimum: 1.
import { zDerivationsResponse } from "@narrative.io/marketplace-sdk/zod";
const parsed = zDerivationsResponse.safeParse(payload);
if (!parsed.success) console.error(parsed.error.issues);SDK calls do not validate on their own. Validating a response is something you opt into, at the point you care about it.
Pinia Colada
@narrative.io/marketplace-sdk/colada exports a query factory per read and a
mutation factory per write, for use in Vue components.
import { useQuery } from "@pinia/colada";
import { getDerivationsQuery } from "@narrative.io/marketplace-sdk/colada";
const { data, isLoading } = useQuery(getDerivationsQuery({
query: { source_attribute_id: [123] },
}));The factory names follow the SDK function names: a read gets a Query factory
and a QueryKey factory, and a write gets a Mutation. The QueryKey
factories build the cache keys, for invalidating a query after a mutation.
| Factory | Operation |
| ---------------------------------------------------- | --------------------------------------------------- |
| getAttributesQuery | GET /attributes |
| getAttributesQueryKey | GET /attributes |
| postAttributesMutation | POST /attributes |
| deleteAttributesByAttributeNameOrIdMutation | DELETE /attributes/{attribute_name_or_id} |
| getAttributesByAttributeNameOrIdQuery | GET /attributes/{attribute_name_or_id} |
| getAttributesByAttributeNameOrIdQueryKey | GET /attributes/{attribute_name_or_id} |
| putAttributesByAttributeNameOrIdMutation | PUT /attributes/{attribute_name_or_id} |
| getAttributesByAttributeNameOrIdReferencesQuery | GET /attributes/{attribute_name_or_id}/references |
| getAttributesByAttributeNameOrIdReferencesQueryKey | GET /attributes/{attribute_name_or_id}/references |
| getDerivationsQuery | GET /derivations |
| getDerivationsQueryKey | GET /derivations |
| postDerivationsMutation | POST /derivations |
| deleteDerivationsByDerivationIdMutation | DELETE /derivations/{derivation_id} |
| getDerivationsByDerivationIdQuery | GET /derivations/{derivation_id} |
| getDerivationsByDerivationIdQueryKey | GET /derivations/{derivation_id} |
| patchDerivationsByDerivationIdMutation | PATCH /derivations/{derivation_id} |
| getWhoamiQuery | GET /whoami |
| getWhoamiQueryKey | GET /whoami |
| getCompanyInfoWhoamiQuery | GET /company-info/whoami |
| getCompanyInfoWhoamiQueryKey | GET /company-info/whoami |
| getInstallationsWhoamiQuery | GET /installations/whoami |
| getInstallationsWhoamiQueryKey | GET /installations/whoami |
| getWebhooksQuery | GET /webhooks |
| getWebhooksQueryKey | GET /webhooks |
| postWebhooksMutation | POST /webhooks |
| deleteWebhooksByWebhookSubscriptionIdMutation | DELETE /webhooks/{webhook_subscription_id} |
| getWebhooksByWebhookSubscriptionIdQuery | GET /webhooks/{webhook_subscription_id} |
| getWebhooksByWebhookSubscriptionIdQueryKey | GET /webhooks/{webhook_subscription_id} |
| getAccessTokensTokensQuery | GET /access-tokens/tokens |
| getAccessTokensTokensQueryKey | GET /access-tokens/tokens |
| postAccessTokensTokensMutation | POST /access-tokens/tokens |
| deleteAccessTokensTokensByAccessTokenIdMutation | DELETE /access-tokens/tokens/{access_token_id} |
| getAccessTokensTokensByAccessTokenIdQuery | GET /access-tokens/tokens/{access_token_id} |
| getAccessTokensTokensByAccessTokenIdQueryKey | GET /access-tokens/tokens/{access_token_id} |
| putAccessTokensTokensByAccessTokenIdMutation | PUT /access-tokens/tokens/{access_token_id} |
Testing
MSW handlers
@narrative.io/marketplace-sdk/msw exports a request handler per operation, so
tests can serve responses typed against the spec instead of hand-written
fixtures.
import { setupServer } from "msw/node";
import { handleGetDerivations } from "@narrative.io/marketplace-sdk/msw";
const server = setupServer(
handleGetDerivations({ status: 200, body: { records: [rule] } }),
);createMswHandlers() returns handlers for every operation at once. Passing a
resolver function instead of a response object gives access to the intercepted
request.
| Handler | Operation |
| -------------------------------------------------- | --------------------------------------------------- |
| handleGetAttributes | GET /attributes |
| handlePostAttributes | POST /attributes |
| handleDeleteAttributesByAttributeNameOrId | DELETE /attributes/{attribute_name_or_id} |
| handleGetAttributesByAttributeNameOrId | GET /attributes/{attribute_name_or_id} |
| handlePutAttributesByAttributeNameOrId | PUT /attributes/{attribute_name_or_id} |
| handleGetAttributesByAttributeNameOrIdReferences | GET /attributes/{attribute_name_or_id}/references |
| handleGetDerivations | GET /derivations |
| handlePostDerivations | POST /derivations |
| handleDeleteDerivationsByDerivationId | DELETE /derivations/{derivation_id} |
| handleGetDerivationsByDerivationId | GET /derivations/{derivation_id} |
| handlePatchDerivationsByDerivationId | PATCH /derivations/{derivation_id} |
| handleGetWhoami | GET /whoami |
| handleGetCompanyInfoWhoami | GET /company-info/whoami |
| handleGetInstallationsWhoami | GET /installations/whoami |
| handleGetWebhooks | GET /webhooks |
| handlePostWebhooks | POST /webhooks |
| handleDeleteWebhooksByWebhookSubscriptionId | DELETE /webhooks/{webhook_subscription_id} |
| handleGetWebhooksByWebhookSubscriptionId | GET /webhooks/{webhook_subscription_id} |
| handleGetAccessTokensTokens | GET /access-tokens/tokens |
| handlePostAccessTokensTokens | POST /access-tokens/tokens |
| handleDeleteAccessTokensTokensByAccessTokenId | DELETE /access-tokens/tokens/{access_token_id} |
| handleGetAccessTokensTokensByAccessTokenId | GET /access-tokens/tokens/{access_token_id} |
| handlePutAccessTokensTokensByAccessTokenId | PUT /access-tokens/tokens/{access_token_id} |
Fixture factories
@narrative.io/marketplace-sdk/fixtures exports a factory per object schema in
the spec, for building a response body without writing the object out:
import { createWhoamiResponse } from "@narrative.io/marketplace-sdk/fixtures";
import { handleGetWhoami } from "@narrative.io/marketplace-sdk/msw";
server.use(
handleGetWhoami({ body: createWhoamiResponse({ role: "global_admin" }) }),
);Each takes a Partial of its type and spreads it last, so a caller overrides
only what its assertion is about. The return type is the annotation on the
function, which is what keeps role a literal union rather than string.
Three properties are worth relying on:
- The values are the same on every call and after every regeneration. They come from the spec's examples and, where the spec has none, from the schema itself. Nothing is random, so a test can seed an id and assert on it.
- Optional fields are present rather than absent. A nullable field the spec
gives no example for is an explicit
null. A fixture that omits a key lets a test asserttoBeUndefined(), pass, and be wrong about an API that sends the key with a null in it. - Nested objects come from the factory for their own schema. So
createWhoamiResponse().current_company_scopeequalscreatePublicCompanyResponse(), and a test can seed from one and assert against the other rather than restating a literal.
Two limits are worth knowing before reaching for them:
- There is one factory per schema, not per semantic variant. A per-schema
generator has no way to know which values make a variant, so the variant is an
override at the call site:
createDerivationCollaborators({ use: { type: "all" } }). Annotate the override when it is a const, so a required field added to the schema later fails at the override rather than quietly producing a value the API never sends. - Only object schemas get one.
Partial<T>and a spread mean nothing forInstant, which is a string, and a union likeDerivationMappinghas no single shape to spread into. Both still appear, as the value of a property on a schema that does get a factory.
| Factory | Schema |
| -------------------------------------- | -------------------------------- |
| createPermission | Permission |
| createAccessTokenMetadata | AccessTokenMetadata |
| createUpdateAccessTokenRequest | UpdateAccessTokenRequest |
| createGetAccessTokenResponse | GetAccessTokenResponse |
| createCreatedAccessToken | CreatedAccessToken |
| createCreateAccessTokenRequest | CreateAccessTokenRequest |
| createListAccessTokensResponse | ListAccessTokensResponse |
| createJobTypeFilter | JobTypeFilter |
| createJobStateFilter | JobStateFilter |
| createJobIdFilter | JobIdFilter |
| createWebhookSubscriptionResponse | WebhookSubscriptionResponse |
| createAppWebhookSubscriptionRequest | AppWebhookSubscriptionRequest |
| createJobsWebhookSubscriptionRequest | JobsWebhookSubscriptionRequest |
| createError | Error |
| createInstallationInfo | InstallationInfo |
| createWhoAmIResponse | WhoAmIResponse |
| createResourcePermissions | ResourcePermissions |
| createPublicCompanyResponse | PublicCompanyResponse |
| createWhoamiSchemasAccessToken | WhoamiSchemasAccessToken |
| createWhoamiResponse | WhoamiResponse |
| createRawValueMapping | RawValueMapping |
| createRawObjectMapping | RawObjectMapping |
| createDerivationCollaborators | DerivationCollaborators |
| createDerivationUpdateRequest | DerivationUpdateRequest |
| createDerivationWarning | DerivationWarning |
| createDerivationResponse | DerivationResponse |
| createDerivationCreateRequest | DerivationCreateRequest |
| createDerivationsResponse | DerivationsResponse |
| createVariantType | VariantType |
| createTimestampTzType | TimestampTzType |
| createStringType | StringType |
| createRefType | RefType |
| createLongType | LongType |
| createDoubleType | DoubleType |
| createBooleanType | BooleanType |
| createBinaryType | BinaryType |
| createAttributeReferencesResponse | AttributeReferencesResponse |
| createObjectType | ObjectType |
| createArrayType | ArrayType |
| createAttributeMetadata | AttributeMetadata |
| createAttributeCooccurrence | AttributeCooccurrence |
| createAttributesResponse | AttributesResponse |
Versioning
Semver, with the caveat that this client's surface is generated from a spec that
moves upstream. A removed or renamed operation, a narrowed type, or a new
required parameter is a major; a new operation or optional field is a minor.
Every release is described in CHANGELOG.md, which ships in the package.
Support
Platform documentation is at docs.narrative.io. For anything else — a bug in this client, an endpoint you need covered, a question about a response — email [email protected].
License
ISC © Narrative I/O, Inc.
