@unifiedcommerce/core 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/config/types.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface EntityConfig {
|
|
|
47
47
|
variants: EntityVariantConfig;
|
|
48
48
|
fulfillment: string;
|
|
49
49
|
hooks?: EntityHooks;
|
|
50
|
+
/** Optional URL alias. Generates ergonomic CRUD routes at `/api/{alias}` that delegate to the catalog service with `type` pre-injected. E.g., `alias: "products"` creates `GET /api/products`, `POST /api/products`, etc. */
|
|
51
|
+
alias?: string;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -17,6 +17,7 @@ import { searchRoutes } from "./routes/search.js";
|
|
|
17
17
|
import { auditRoutes } from "./routes/audit.js";
|
|
18
18
|
import { adminJobRoutes } from "./routes/admin-jobs.js";
|
|
19
19
|
import { customerRoutes } from "./routes/customers.js";
|
|
20
|
+
import { entityAliasRoutes } from "./routes/entity-aliases.js";
|
|
20
21
|
|
|
21
22
|
export function createRestRoutes(kernel: Kernel) {
|
|
22
23
|
const router = new OpenAPIHono<AppEnv>({
|
|
@@ -66,6 +67,38 @@ export function createRestRoutes(kernel: Kernel) {
|
|
|
66
67
|
router.route("/audit", auditRoutes(kernel));
|
|
67
68
|
router.route("/admin", adminJobRoutes(kernel));
|
|
68
69
|
|
|
70
|
+
// ─── Entity alias routes (e.g., /api/products → /api/catalog/entities?type=product) ──
|
|
71
|
+
for (const aliasRouter of entityAliasRoutes(kernel)) {
|
|
72
|
+
const aliasPath = (aliasRouter as unknown as Record<string, string>).__aliasPath;
|
|
73
|
+
if (aliasPath) {
|
|
74
|
+
router.route(`/${aliasPath}`, aliasRouter);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Static shortcut aliases ───────────────────────────────
|
|
79
|
+
// /api/categories → forwards to /api/catalog/categories
|
|
80
|
+
router.all("/categories/*", (c) => {
|
|
81
|
+
const url = new URL(c.req.url);
|
|
82
|
+
url.pathname = url.pathname.replace("/api/categories", "/api/catalog/categories");
|
|
83
|
+
return router.fetch(new Request(url.toString(), c.req.raw));
|
|
84
|
+
});
|
|
85
|
+
router.all("/categories", (c) => {
|
|
86
|
+
const url = new URL(c.req.url);
|
|
87
|
+
url.pathname = "/api/catalog/categories";
|
|
88
|
+
return router.fetch(new Request(url.toString(), c.req.raw));
|
|
89
|
+
});
|
|
90
|
+
// /api/brands → forwards to /api/catalog/brands
|
|
91
|
+
router.all("/brands/*", (c) => {
|
|
92
|
+
const url = new URL(c.req.url);
|
|
93
|
+
url.pathname = url.pathname.replace("/api/brands", "/api/catalog/brands");
|
|
94
|
+
return router.fetch(new Request(url.toString(), c.req.raw));
|
|
95
|
+
});
|
|
96
|
+
router.all("/brands", (c) => {
|
|
97
|
+
const url = new URL(c.req.url);
|
|
98
|
+
url.pathname = "/api/catalog/brands";
|
|
99
|
+
return router.fetch(new Request(url.toString(), c.req.raw));
|
|
100
|
+
});
|
|
101
|
+
|
|
69
102
|
// API Reference (Scalar) — disabled in production unless config.exposeOpenApiSpec is true
|
|
70
103
|
const exposeSpec = kernel.config.exposeOpenApiSpec ?? (process.env.NODE_ENV !== "production");
|
|
71
104
|
if (exposeSpec) {
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
|
+
import { z, createRoute } from "@hono/zod-openapi";
|
|
3
|
+
import type { Kernel } from "../../../runtime/kernel.js";
|
|
4
|
+
import type { CreateEntityInput, UpdateEntityInput } from "../../../modules/catalog/schemas.js";
|
|
5
|
+
import {
|
|
6
|
+
type AppEnv,
|
|
7
|
+
mapErrorToResponse,
|
|
8
|
+
mapErrorToStatus,
|
|
9
|
+
parseInclude,
|
|
10
|
+
parsePagination,
|
|
11
|
+
parseSort,
|
|
12
|
+
} from "../utils.js";
|
|
13
|
+
import { errorResponses } from "../schemas/shared.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate ergonomic alias routes for an entity type.
|
|
17
|
+
*
|
|
18
|
+
* E.g., if config has `entities.product.alias = "products"`, this creates:
|
|
19
|
+
* GET /api/products
|
|
20
|
+
* GET /api/products/:idOrSlug
|
|
21
|
+
* POST /api/products
|
|
22
|
+
* PATCH /api/products/:id
|
|
23
|
+
* DELETE /api/products/:id
|
|
24
|
+
* POST /api/products/:id/publish
|
|
25
|
+
* POST /api/products/:id/archive
|
|
26
|
+
* GET /api/products/:id/variants
|
|
27
|
+
* POST /api/products/:id/variants
|
|
28
|
+
* GET /api/products/:id/options
|
|
29
|
+
* GET /api/products/:id/attributes/:locale
|
|
30
|
+
* PUT /api/products/:id/attributes/:locale
|
|
31
|
+
*
|
|
32
|
+
* All delegate to the same catalog service with `type` pre-injected.
|
|
33
|
+
*/
|
|
34
|
+
export function entityAliasRoutes(kernel: Kernel): OpenAPIHono<AppEnv>[] {
|
|
35
|
+
const routers: OpenAPIHono<AppEnv>[] = [];
|
|
36
|
+
const entities = kernel.config.entities ?? {};
|
|
37
|
+
|
|
38
|
+
for (const [entityType, entityConfig] of Object.entries(entities)) {
|
|
39
|
+
if (!entityConfig.alias) continue;
|
|
40
|
+
|
|
41
|
+
const alias = entityConfig.alias;
|
|
42
|
+
const tag = alias.charAt(0).toUpperCase() + alias.slice(1);
|
|
43
|
+
const router = new OpenAPIHono<AppEnv>();
|
|
44
|
+
|
|
45
|
+
// ─── LIST ─────────────────────────────────────────────────
|
|
46
|
+
const listRoute = createRoute({
|
|
47
|
+
method: "get",
|
|
48
|
+
path: "/",
|
|
49
|
+
tags: [tag],
|
|
50
|
+
summary: `List ${alias}`,
|
|
51
|
+
description: `Alias for GET /api/catalog/entities?type=${entityType}`,
|
|
52
|
+
request: {
|
|
53
|
+
query: z.object({
|
|
54
|
+
status: z.string().optional(),
|
|
55
|
+
category: z.string().optional(),
|
|
56
|
+
brand: z.string().optional(),
|
|
57
|
+
include: z.string().optional(),
|
|
58
|
+
sort: z.string().optional(),
|
|
59
|
+
page: z.string().optional(),
|
|
60
|
+
limit: z.string().optional(),
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
responses: {
|
|
64
|
+
200: {
|
|
65
|
+
content: { "application/json": { schema: z.object({ data: z.array(z.record(z.string(), z.unknown())), meta: z.record(z.string(), z.unknown()) }) } },
|
|
66
|
+
description: `${tag} list`,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// @ts-expect-error -- openapi handler union return type
|
|
72
|
+
router.openapi(listRoute, async (c) => {
|
|
73
|
+
const include = parseInclude(c.req.query("include"));
|
|
74
|
+
const pagination = parsePagination(c.req.query());
|
|
75
|
+
const filter: Record<string, string> = { type: entityType };
|
|
76
|
+
const status = c.req.query("status");
|
|
77
|
+
const category = c.req.query("category");
|
|
78
|
+
const brand = c.req.query("brand");
|
|
79
|
+
if (status) filter.status = status;
|
|
80
|
+
if (category) filter.category = category;
|
|
81
|
+
if (brand) filter.brand = brand;
|
|
82
|
+
|
|
83
|
+
const payload: Record<string, unknown> = { filter, pagination };
|
|
84
|
+
const sort = parseSort(c.req.query("sort"));
|
|
85
|
+
if (sort) payload.sort = sort;
|
|
86
|
+
|
|
87
|
+
const result = await kernel.services.catalog.list(payload, c.get("actor"));
|
|
88
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
89
|
+
|
|
90
|
+
let items = result.value.items;
|
|
91
|
+
if (include.size > 0) {
|
|
92
|
+
const opts = {
|
|
93
|
+
includeAttributes: include.has("attributes"),
|
|
94
|
+
includeVariants: include.has("variants"),
|
|
95
|
+
includeOptionTypes: include.has("optionTypes"),
|
|
96
|
+
includeCategories: include.has("categories"),
|
|
97
|
+
includeBrands: include.has("brands"),
|
|
98
|
+
includeMedia: include.has("media"),
|
|
99
|
+
includeInventory: include.has("inventory"),
|
|
100
|
+
includePricing: include.has("pricing"),
|
|
101
|
+
};
|
|
102
|
+
items = await Promise.all(
|
|
103
|
+
items.map(async (item) => {
|
|
104
|
+
const full = await kernel.services.catalog.getById(item.id, opts);
|
|
105
|
+
return full.ok ? full.value : item;
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return c.json({ data: items, meta: { pagination: result.value.pagination } });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ─── GET BY ID/SLUG ───────────────────────────────────────
|
|
114
|
+
const getRoute = createRoute({
|
|
115
|
+
method: "get",
|
|
116
|
+
path: "/{idOrSlug}",
|
|
117
|
+
tags: [tag],
|
|
118
|
+
summary: `Get ${alias.slice(0, -1)} by ID or slug`,
|
|
119
|
+
request: {
|
|
120
|
+
params: z.object({ idOrSlug: z.string() }),
|
|
121
|
+
query: z.object({ include: z.string().optional() }),
|
|
122
|
+
},
|
|
123
|
+
responses: {
|
|
124
|
+
200: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: `${tag} detail` },
|
|
125
|
+
...errorResponses,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// @ts-expect-error -- openapi handler union return type
|
|
130
|
+
router.openapi(getRoute, async (c) => {
|
|
131
|
+
const idOrSlug = c.req.valid("param").idOrSlug;
|
|
132
|
+
const include = parseInclude(c.req.query("include"));
|
|
133
|
+
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idOrSlug);
|
|
134
|
+
const opts = {
|
|
135
|
+
includeAttributes: include.has("attributes"),
|
|
136
|
+
includeVariants: include.has("variants"),
|
|
137
|
+
includeOptionTypes: include.has("optionTypes"),
|
|
138
|
+
includeCategories: include.has("categories"),
|
|
139
|
+
includeBrands: include.has("brands"),
|
|
140
|
+
includeMedia: include.has("media"),
|
|
141
|
+
includeInventory: include.has("inventory"),
|
|
142
|
+
includePricing: include.has("pricing"),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = isUUID
|
|
146
|
+
? await kernel.services.catalog.getById(idOrSlug, opts)
|
|
147
|
+
: await kernel.services.catalog.getBySlug(idOrSlug, opts);
|
|
148
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
149
|
+
return c.json({ data: result.value });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ─── CREATE ───────────────────────────────────────────────
|
|
153
|
+
const createEntityRoute = createRoute({
|
|
154
|
+
method: "post",
|
|
155
|
+
path: "/",
|
|
156
|
+
tags: [tag],
|
|
157
|
+
summary: `Create ${alias.slice(0, -1)}`,
|
|
158
|
+
request: {
|
|
159
|
+
body: { content: { "application/json": { schema: z.object({ slug: z.string().optional(), title: z.string().optional(), description: z.string().optional(), metadata: z.record(z.string(), z.unknown()).optional() }).openapi(`Create${tag}Request`) } } },
|
|
160
|
+
},
|
|
161
|
+
responses: {
|
|
162
|
+
201: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: `${tag} created` },
|
|
163
|
+
...errorResponses,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// @ts-expect-error -- openapi handler union return type
|
|
168
|
+
router.openapi(createEntityRoute, async (c) => {
|
|
169
|
+
const body = c.req.valid("json");
|
|
170
|
+
const input: CreateEntityInput = { type: entityType, ...body };
|
|
171
|
+
const result = await kernel.services.catalog.create(input, c.get("actor"));
|
|
172
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
173
|
+
return c.json({ data: result.value }, 201);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// ─── UPDATE ───────────────────────────────────────────────
|
|
177
|
+
const updateRoute = createRoute({
|
|
178
|
+
method: "patch",
|
|
179
|
+
path: "/{id}",
|
|
180
|
+
tags: [tag],
|
|
181
|
+
summary: `Update ${alias.slice(0, -1)}`,
|
|
182
|
+
request: {
|
|
183
|
+
params: z.object({ id: z.string().uuid() }),
|
|
184
|
+
body: { content: { "application/json": { schema: z.object({ slug: z.string().optional(), title: z.string().optional(), description: z.string().optional(), metadata: z.record(z.string(), z.unknown()).optional() }).openapi(`Update${tag}Request`) } } },
|
|
185
|
+
},
|
|
186
|
+
responses: {
|
|
187
|
+
200: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: `${tag} updated` },
|
|
188
|
+
...errorResponses,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// @ts-expect-error -- openapi handler union return type
|
|
193
|
+
router.openapi(updateRoute, async (c) => {
|
|
194
|
+
const { id } = c.req.valid("param");
|
|
195
|
+
const body = c.req.valid("json");
|
|
196
|
+
const input: UpdateEntityInput = { ...body };
|
|
197
|
+
const result = await kernel.services.catalog.update(id, input, c.get("actor"));
|
|
198
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
199
|
+
return c.json({ data: result.value });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ─── DELETE ────────────────────────────────────────────────
|
|
203
|
+
const deleteRoute = createRoute({
|
|
204
|
+
method: "delete",
|
|
205
|
+
path: "/{id}",
|
|
206
|
+
tags: [tag],
|
|
207
|
+
summary: `Delete ${alias.slice(0, -1)}`,
|
|
208
|
+
request: { params: z.object({ id: z.string().uuid() }) },
|
|
209
|
+
responses: {
|
|
210
|
+
200: { content: { "application/json": { schema: z.object({ data: z.object({ deleted: z.literal(true) }) }) } }, description: "Deleted" },
|
|
211
|
+
...errorResponses,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// @ts-expect-error -- openapi handler union return type
|
|
216
|
+
router.openapi(deleteRoute, async (c) => {
|
|
217
|
+
const { id } = c.req.valid("param");
|
|
218
|
+
const result = await kernel.services.catalog.delete(id, c.get("actor"));
|
|
219
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
220
|
+
return c.json({ data: { deleted: true as const } });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ─── PUBLISH / ARCHIVE ────────────────────────────────────
|
|
224
|
+
for (const action of ["publish", "archive"] as const) {
|
|
225
|
+
const actionRoute = createRoute({
|
|
226
|
+
method: "post",
|
|
227
|
+
path: `/{id}/${action}`,
|
|
228
|
+
tags: [tag],
|
|
229
|
+
summary: `${action.charAt(0).toUpperCase() + action.slice(1)} ${alias.slice(0, -1)}`,
|
|
230
|
+
request: { params: z.object({ id: z.string().uuid() }) },
|
|
231
|
+
responses: {
|
|
232
|
+
200: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: `${tag} ${action}ed` },
|
|
233
|
+
...errorResponses,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// @ts-expect-error -- openapi handler union return type
|
|
238
|
+
router.openapi(actionRoute, async (c) => {
|
|
239
|
+
const { id } = c.req.valid("param");
|
|
240
|
+
const result = await kernel.services.catalog[action](id, c.get("actor"));
|
|
241
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
242
|
+
return c.json({ data: result.value });
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ─── ATTRIBUTES ───────────────────────────────────────────
|
|
247
|
+
const getAttrsRoute = createRoute({
|
|
248
|
+
method: "get",
|
|
249
|
+
path: "/{id}/attributes/{locale}",
|
|
250
|
+
tags: [tag],
|
|
251
|
+
summary: `Get ${alias.slice(0, -1)} attributes`,
|
|
252
|
+
request: { params: z.object({ id: z.string().uuid(), locale: z.string() }) },
|
|
253
|
+
responses: {
|
|
254
|
+
200: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: "Attributes" },
|
|
255
|
+
...errorResponses,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// @ts-expect-error -- openapi handler union return type
|
|
260
|
+
router.openapi(getAttrsRoute, async (c) => {
|
|
261
|
+
const { id, locale } = c.req.valid("param");
|
|
262
|
+
const result = await kernel.services.catalog.getAttributes(id, locale);
|
|
263
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
264
|
+
return c.json({ data: result.value });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const setAttrsRoute = createRoute({
|
|
268
|
+
method: "put",
|
|
269
|
+
path: "/{id}/attributes/{locale}",
|
|
270
|
+
tags: [tag],
|
|
271
|
+
summary: `Set ${alias.slice(0, -1)} attributes`,
|
|
272
|
+
request: {
|
|
273
|
+
params: z.object({ id: z.string().uuid(), locale: z.string() }),
|
|
274
|
+
body: { content: { "application/json": { schema: z.record(z.string(), z.unknown()).openapi(`Set${tag}AttributesRequest`) } } },
|
|
275
|
+
},
|
|
276
|
+
responses: {
|
|
277
|
+
200: { content: { "application/json": { schema: z.object({ data: z.object({ updated: z.literal(true) }) }) } }, description: "Attributes set" },
|
|
278
|
+
...errorResponses,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// @ts-expect-error -- openapi handler union return type
|
|
283
|
+
router.openapi(setAttrsRoute, async (c) => {
|
|
284
|
+
const { id, locale } = c.req.valid("param");
|
|
285
|
+
const body = c.req.valid("json");
|
|
286
|
+
const result = await kernel.services.catalog.setAttributes(id, locale, body);
|
|
287
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
288
|
+
return c.json({ data: { updated: true as const } });
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ─── VARIANTS ─────────────────────────────────────────────
|
|
292
|
+
if (entityConfig.variants.enabled) {
|
|
293
|
+
const createVariantRoute = createRoute({
|
|
294
|
+
method: "post",
|
|
295
|
+
path: "/{id}/variants",
|
|
296
|
+
tags: [tag],
|
|
297
|
+
summary: `Create variant for ${alias.slice(0, -1)}`,
|
|
298
|
+
request: {
|
|
299
|
+
params: z.object({ id: z.string().uuid() }),
|
|
300
|
+
body: { content: { "application/json": { schema: z.object({ sku: z.string().optional(), options: z.record(z.string(), z.string()).optional(), metadata: z.record(z.string(), z.unknown()).optional() }).openapi(`Create${tag}VariantRequest`) } } },
|
|
301
|
+
},
|
|
302
|
+
responses: {
|
|
303
|
+
201: { content: { "application/json": { schema: z.object({ data: z.record(z.string(), z.unknown()) }) } }, description: "Variant created" },
|
|
304
|
+
...errorResponses,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// @ts-expect-error -- openapi handler union return type
|
|
309
|
+
router.openapi(createVariantRoute, async (c) => {
|
|
310
|
+
const { id } = c.req.valid("param");
|
|
311
|
+
const body = c.req.valid("json");
|
|
312
|
+
const result = await kernel.services.catalog.createVariant({ entityId: id, ...body }, c.get("actor"));
|
|
313
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
314
|
+
return c.json({ data: result.value }, 201);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const listOptionsRoute = createRoute({
|
|
318
|
+
method: "get",
|
|
319
|
+
path: "/{id}/options",
|
|
320
|
+
tags: [tag],
|
|
321
|
+
summary: `List option types for ${alias.slice(0, -1)}`,
|
|
322
|
+
request: { params: z.object({ id: z.string().uuid() }) },
|
|
323
|
+
responses: {
|
|
324
|
+
200: { content: { "application/json": { schema: z.object({ data: z.array(z.record(z.string(), z.unknown())) }) } }, description: "Option types" },
|
|
325
|
+
...errorResponses,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// @ts-expect-error -- openapi handler union return type
|
|
330
|
+
router.openapi(listOptionsRoute, async (c) => {
|
|
331
|
+
const { id } = c.req.valid("param");
|
|
332
|
+
const result = await kernel.services.catalog.getById(id, { includeOptionTypes: true });
|
|
333
|
+
if (!result.ok) return c.json(mapErrorToResponse(result.error), mapErrorToStatus(result.error));
|
|
334
|
+
return c.json({ data: result.value.optionTypes ?? [] });
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Store alias → entityType for the tag group
|
|
339
|
+
(router as unknown as Record<string, string>).__aliasTag = tag;
|
|
340
|
+
(router as unknown as Record<string, string>).__aliasPath = alias;
|
|
341
|
+
|
|
342
|
+
routers.push(router);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return routers;
|
|
346
|
+
}
|
package/src/runtime/server.ts
CHANGED
|
@@ -268,8 +268,15 @@ export async function createServer(config: CommerceConfig) {
|
|
|
268
268
|
description: "Headless commerce engine REST API. Includes core and plugin endpoints.",
|
|
269
269
|
},
|
|
270
270
|
tags: [
|
|
271
|
+
// ── Entity aliases (generated from config.entities[type].alias) ──
|
|
272
|
+
...Object.entries(config.entities ?? {})
|
|
273
|
+
.filter(([, cfg]) => cfg.alias)
|
|
274
|
+
.map(([type, cfg]) => {
|
|
275
|
+
const name = cfg.alias!.charAt(0).toUpperCase() + cfg.alias!.slice(1);
|
|
276
|
+
return { name, description: `Ergonomic alias for ${type} entities — CRUD, variants, attributes` };
|
|
277
|
+
}),
|
|
271
278
|
// ── Storefront ──
|
|
272
|
-
{ name: "Catalog", description: "Products, categories, brands, variants, and option types" },
|
|
279
|
+
{ name: "Catalog", description: "Products, categories, brands, variants, and option types (unified entity API)" },
|
|
273
280
|
{ name: "Search", description: "Full-text search and typeahead suggestions" },
|
|
274
281
|
{ name: "Pricing", description: "Base prices, price modifiers, and customer group pricing" },
|
|
275
282
|
{ name: "Carts", description: "Shopping cart lifecycle — create, add items, update quantities" },
|
|
@@ -289,7 +296,11 @@ export async function createServer(config: CommerceConfig) {
|
|
|
289
296
|
});
|
|
290
297
|
|
|
291
298
|
// Serve enriched spec with x-tagGroups vendor extension (Scalar/Redocly sidebar grouping)
|
|
299
|
+
const aliasTags = Object.values(config.entities ?? {})
|
|
300
|
+
.filter((cfg) => cfg.alias)
|
|
301
|
+
.map((cfg) => cfg.alias!.charAt(0).toUpperCase() + cfg.alias!.slice(1));
|
|
292
302
|
const tagGroups = [
|
|
303
|
+
...(aliasTags.length > 0 ? [{ name: "Quick Access", tags: aliasTags }] : []),
|
|
293
304
|
{ name: "Storefront", tags: ["Catalog", "Search", "Pricing", "Carts", "Checkout", "Promotions"] },
|
|
294
305
|
{ name: "Admin", tags: ["Orders", "Customers", "Inventory", "Media", "Payments"] },
|
|
295
306
|
{ name: "Operations", tags: ["Webhooks", "Audit", "Admin Jobs"] },
|