Skip to main content

URN Functions

A URN is EverShop's stable, portable reference to an entity:

urn:evershop:<service>:<type>:<uuid>

for example urn:evershop:catalog:product:7afebbbd-69f6-4e2c-84c5-5b899173b867.

URNs exist because entity URLs are not stable. A widget that stored /women/shoes broke the moment somebody renamed the category. A URN stores identity instead of a URL and resolves to the current URL at request time. They are also the cross-module handle: the CMS module can reference a catalog product without importing catalog code or knowing its table layout.

URNs are derived, not stored

There is no urn column anywhere. URNs are built on demand from an entity's uuid — usually in a GraphQL resolver, or when a page-builder widget saves a link. The entity tables are untouched.

Import

import {
registerUrnSchema,
getUrnSchema,
hasUrnSchema,
listUrnSchemas,
UrnService,
CatalogUrn,
CmsUrn,
OmsUrn,
BlogUrn,
CustomerUrn,
PromotionUrn
} from '@evershop/evershop/lib/urn';
import type { UrnSchema, UrnParts } from '@evershop/evershop/lib/urn';
Importing this module has a side effect

lib/urn registers the core schemas at module-load time. Importing it — even only for the UrnService class — triggers that registration. This is why core types are always available without any bootstrap step.

Types

interface UrnSchema {
service: string;
type: string;
description: string;
}

interface UrnParts {
raw: string;
scheme: string; // always 'urn'
platform: string; // always 'evershop'
service: string;
type: string;
uuid: string;
}

Built-in schemas

ServiceTypeDescription
catalogproductCatalog product
catalogcategoryProduct category
cmswidget_instancePage builder widget instance
cmswidget_placementWidget placement on a route + area
cmspageCMS page
omsorderCustomer order
customercustomerCustomer account
blogpostBlog post
blogcategoryBlog category
blogtagBlog tag
promotionlanding_pagePromotion landing page
There is no catalog:collection schema

Collections are non-navigable groupings — they have no public page — so they are referenced by code in widget settings, not by URN.


registerUrnSchema

registerUrnSchema(schema: UrnSchema): void

Register a (service, type) pair so UrnService.build and UrnService.parse will accept it. Call this from your module's bootstrap.ts.

Parameters

FieldTypeDescription
schema.servicestringThe owning service / module, e.g. 'reviews'.
schema.typestringThe entity type within that service, e.g. 'review'.
schema.descriptionstringHuman-readable label. Required.

Return Value

void.

Throws

URN schema already registered: <service>:<type> when the pair is already in the registry. Registration is not idempotent — guard with hasUrnSchema if two of your entry points might both register.

Example

extensions/reviews/src/bootstrap.ts
import { registerUrnSchema } from '@evershop/evershop/lib/urn';

export default () => {
registerUrnSchema({
service: 'reviews',
type: 'review',
description: 'Product review'
});
};

getUrnSchema

getUrnSchema(service: string, type: string): UrnSchema | undefined

Look up a registered schema. Returns undefined when the pair is unknown — it never throws.

import { getUrnSchema } from '@evershop/evershop/lib/urn';

getUrnSchema('catalog', 'product');
// { service: 'catalog', type: 'product', description: 'Catalog product' }

hasUrnSchema

hasUrnSchema(service: string, type: string): boolean

true when the pair is registered. Use it as the guard before a conditional registerUrnSchema, or to check whether an extension that owns a type is installed.


listUrnSchemas

listUrnSchemas(): UrnSchema[]

Every registered schema, in registration order. Useful for building admin pickers that enumerate linkable entity types.


UrnService

A static class — there is nothing to instantiate.

UrnService.build

UrnService.build(service: string, type: string, uuid: string): string

Compose a URN string. Throws when (service, type) is not registered:

Cannot build URN: (service="x", type="y") is not registered.
Call registerUrnSchema() in your module bootstrap.

UrnService.parse

UrnService.parse(raw: string): UrnParts

Parse a URN into its parts. Throws on:

ConditionMessage
Wrong segment count (not exactly 5 colon-separated parts)Invalid URN "…": expected 5 segments, got N
Scheme is not urnInvalid URN scheme: "…" (expected "urn")
Platform is not evershopInvalid URN platform: "…" (expected "evershop")
Unregistered (service, type)Unknown URN type: "s:t". Not registered in UrnRegistry.

UrnService.isValid

UrnService.isValid(raw: string): boolean

true when raw parses cleanly and its type is registered. This is the standard "is this a URN or a plain URL?" test — a plain /about or https://… returns false.

UrnService.extractUuid

UrnService.extractUuid(raw: string): string

Shorthand for UrnService.parse(raw).uuid. Throws under the same conditions as parse.

Example

import { UrnService } from '@evershop/evershop/lib/urn';

const urn = UrnService.build('catalog', 'product', product.uuid);
// 'urn:evershop:catalog:product:7afebbbd-…'

if (UrnService.isValid(stored)) {
const { service, type, uuid } = UrnService.parse(stored);
}

Built-in URN helpers

Thin typo-proof wrappers over UrnService.build. Prefer them over hand-writing the service/type strings.

HelperMethodProduces
CatalogUrnproduct(uuid)urn:evershop:catalog:product:<uuid>
category(uuid)urn:evershop:catalog:category:<uuid>
CmsUrnwidgetInstance(uuid)urn:evershop:cms:widget_instance:<uuid>
widgetPlacement(uuid)urn:evershop:cms:widget_placement:<uuid>
page(uuid)urn:evershop:cms:page:<uuid>
OmsUrnorder(uuid)urn:evershop:oms:order:<uuid>
BlogUrnpost(uuid)urn:evershop:blog:post:<uuid>
category(uuid)urn:evershop:blog:category:<uuid>
tag(uuid)urn:evershop:blog:tag:<uuid>
CustomerUrncustomer(uuid)urn:evershop:customer:customer:<uuid>
PromotionUrnlandingPage(uuid)urn:evershop:promotion:landing_page:<uuid>
import { CatalogUrn, CmsUrn } from '@evershop/evershop/lib/urn';

// Typically inside a GraphQL resolver — derived, never stored.
const resolvers = {
Product: {
urn: ({ uuid }) => CatalogUrn.product(uuid)
}
};

const pageUrn = CmsUrn.page(page.uuid);

Registering a linkable type end to end

A custom entity that should be selectable in the page-builder link picker needs both a URN schema and a link loader, registered from the same bootstrap:

extensions/reviews/src/bootstrap.ts
import { registerUrnSchema } from '@evershop/evershop/lib/urn';
import {
registerLinkLoader,
linkLoaderFromBatch
} from '@evershop/evershop/lib/widget/linkResolver';
import { select } from '@evershop/evershop/lib/postgres/query';

export default () => {
registerUrnSchema({
service: 'reviews',
type: 'review',
description: 'Product review'
});

registerLinkLoader(
'reviews',
'review',
linkLoaderFromBatch(async (uuids, pool) => {
if (uuids.length === 0) return [];
const rows = await select('uuid', 'slug')
.from('review')
.where('uuid', 'IN', [...uuids])
.execute(pool);
const m = new Map(rows.map((r) => [r.uuid, `/reviews/${r.slug}`]));
return uuids.map((u) => m.get(u) ?? null);
})
);
};

Register the schema first — a link loader for an unregistered (service, type) never fires, because resolveLink rejects the URN at parse time before it reaches any loader.

See Also