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.
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';
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
| Service | Type | Description |
|---|---|---|
catalog | product | Catalog product |
catalog | category | Product category |
cms | widget_instance | Page builder widget instance |
cms | widget_placement | Widget placement on a route + area |
cms | page | CMS page |
oms | order | Customer order |
customer | customer | Customer account |
blog | post | Blog post |
blog | category | Blog category |
blog | tag | Blog tag |
promotion | landing_page | Promotion landing page |
catalog:collection schemaCollections 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
| Field | Type | Description |
|---|---|---|
schema.service | string | The owning service / module, e.g. 'reviews'. |
schema.type | string | The entity type within that service, e.g. 'review'. |
schema.description | string | Human-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
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:
| Condition | Message |
|---|---|
| Wrong segment count (not exactly 5 colon-separated parts) | Invalid URN "…": expected 5 segments, got N |
Scheme is not urn | Invalid URN scheme: "…" (expected "urn") |
Platform is not evershop | Invalid 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.
| Helper | Method | Produces |
|---|---|---|
CatalogUrn | product(uuid) | urn:evershop:catalog:product:<uuid> |
category(uuid) | urn:evershop:catalog:category:<uuid> | |
CmsUrn | widgetInstance(uuid) | urn:evershop:cms:widget_instance:<uuid> |
widgetPlacement(uuid) | urn:evershop:cms:widget_placement:<uuid> | |
page(uuid) | urn:evershop:cms:page:<uuid> | |
OmsUrn | order(uuid) | urn:evershop:oms:order:<uuid> |
BlogUrn | post(uuid) | urn:evershop:blog:post:<uuid> |
category(uuid) | urn:evershop:blog:category:<uuid> | |
tag(uuid) | urn:evershop:blog:tag:<uuid> | |
CustomerUrn | customer(uuid) | urn:evershop:customer:customer:<uuid> |
PromotionUrn | landingPage(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:
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
- Link Resolver — Turning URNs into current URLs at request time
- Widget Link Fields — The page-builder link picker that produces URNs
- Widget Development — Building widgets that store links