@unifiedcommerce/core 0.1.1 → 0.2.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.
Files changed (257) hide show
  1. package/dist/auth/setup.d.ts.map +1 -1
  2. package/dist/auth/setup.js +8 -3
  3. package/dist/config/types.d.ts +3 -1
  4. package/dist/config/types.d.ts.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/interfaces/mcp/server.d.ts +3 -5
  9. package/dist/interfaces/mcp/server.d.ts.map +1 -1
  10. package/dist/interfaces/mcp/server.js +25 -510
  11. package/dist/interfaces/mcp/tool-builder.d.ts +120 -0
  12. package/dist/interfaces/mcp/tool-builder.d.ts.map +1 -0
  13. package/dist/interfaces/mcp/tool-builder.js +224 -0
  14. package/dist/interfaces/mcp/tools/analytics.d.ts +42 -0
  15. package/dist/interfaces/mcp/tools/analytics.d.ts.map +1 -0
  16. package/dist/interfaces/mcp/tools/analytics.js +70 -0
  17. package/dist/interfaces/mcp/tools/cart.d.ts +14 -0
  18. package/dist/interfaces/mcp/tools/cart.d.ts.map +1 -0
  19. package/dist/interfaces/mcp/tools/cart.js +47 -0
  20. package/dist/interfaces/mcp/tools/catalog.d.ts +53 -0
  21. package/dist/interfaces/mcp/tools/catalog.d.ts.map +1 -0
  22. package/dist/interfaces/mcp/tools/catalog.js +284 -0
  23. package/dist/interfaces/mcp/tools/index.d.ts +3 -0
  24. package/dist/interfaces/mcp/tools/index.d.ts.map +1 -0
  25. package/dist/interfaces/mcp/tools/index.js +20 -0
  26. package/dist/interfaces/mcp/tools/inventory.d.ts +27 -0
  27. package/dist/interfaces/mcp/tools/inventory.d.ts.map +1 -0
  28. package/dist/interfaces/mcp/tools/inventory.js +143 -0
  29. package/dist/interfaces/mcp/tools/orders.d.ts +18 -0
  30. package/dist/interfaces/mcp/tools/orders.d.ts.map +1 -0
  31. package/dist/interfaces/mcp/tools/orders.js +82 -0
  32. package/dist/interfaces/mcp/tools/pricing.d.ts +29 -0
  33. package/dist/interfaces/mcp/tools/pricing.d.ts.map +1 -0
  34. package/dist/interfaces/mcp/tools/pricing.js +90 -0
  35. package/dist/interfaces/mcp/tools/promotions.d.ts +44 -0
  36. package/dist/interfaces/mcp/tools/promotions.d.ts.map +1 -0
  37. package/dist/interfaces/mcp/tools/promotions.js +109 -0
  38. package/dist/interfaces/mcp/tools/registry.d.ts +32 -0
  39. package/dist/interfaces/mcp/tools/registry.d.ts.map +1 -0
  40. package/dist/interfaces/mcp/tools/registry.js +55 -0
  41. package/dist/interfaces/mcp/tools/search.d.ts +14 -0
  42. package/dist/interfaces/mcp/tools/search.d.ts.map +1 -0
  43. package/dist/interfaces/mcp/tools/search.js +39 -0
  44. package/dist/interfaces/mcp/tools/webhooks.d.ts +15 -0
  45. package/dist/interfaces/mcp/tools/webhooks.d.ts.map +1 -0
  46. package/dist/interfaces/mcp/tools/webhooks.js +48 -0
  47. package/dist/interfaces/mcp/transport.d.ts +17 -2
  48. package/dist/interfaces/mcp/transport.d.ts.map +1 -1
  49. package/dist/interfaces/mcp/transport.js +91 -44
  50. package/dist/interfaces/rest/router.d.ts.map +1 -1
  51. package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
  52. package/dist/interfaces/rest/routes/checkout.js +1 -1
  53. package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
  54. package/dist/interfaces/rest/routes/promotions.js +3 -2
  55. package/dist/kernel/database/adapter.d.ts +8 -0
  56. package/dist/kernel/database/adapter.d.ts.map +1 -1
  57. package/dist/kernel/factory/repository-factory.d.ts.map +1 -1
  58. package/dist/kernel/factory/repository-factory.js +3 -1
  59. package/dist/kernel/local-api.d.ts.map +1 -1
  60. package/dist/kernel/local-api.js +2 -0
  61. package/dist/kernel/plugin/manifest.d.ts +3 -3
  62. package/dist/kernel/plugin/manifest.d.ts.map +1 -1
  63. package/dist/kernel/plugin/manifest.js +36 -7
  64. package/dist/runtime/kernel.d.ts +1 -2
  65. package/dist/runtime/kernel.d.ts.map +1 -1
  66. package/dist/runtime/kernel.js +16 -8
  67. package/dist/runtime/server.d.ts.map +1 -1
  68. package/dist/runtime/server.js +8 -3
  69. package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
  70. package/dist/test-utils/create-pglite-adapter.js +7 -6
  71. package/dist/tsconfig.tsbuildinfo +1 -0
  72. package/package.json +3 -1
  73. package/src/adapters/console-email.ts +43 -0
  74. package/src/auth/access.ts +187 -0
  75. package/src/auth/auth-schema.ts +139 -0
  76. package/src/auth/middleware.ts +161 -0
  77. package/src/auth/org.ts +41 -0
  78. package/src/auth/permissions.ts +28 -0
  79. package/src/auth/setup.ts +171 -0
  80. package/src/auth/system-actor.ts +19 -0
  81. package/src/auth/types.ts +10 -0
  82. package/src/config/defaults.ts +82 -0
  83. package/src/config/define-config.ts +53 -0
  84. package/src/config/types.ts +301 -0
  85. package/src/generated/plugin-capabilities.d.ts +20 -0
  86. package/src/generated/plugin-manifest.ts +23 -0
  87. package/src/generated/plugin-repositories.d.ts +20 -0
  88. package/src/hooks/checkout-completion.ts +262 -0
  89. package/src/hooks/checkout.ts +677 -0
  90. package/src/hooks/order-emails.ts +62 -0
  91. package/src/index.ts +215 -0
  92. package/src/interfaces/mcp/agent-prompt.ts +174 -0
  93. package/src/interfaces/mcp/context-enrichment.ts +177 -0
  94. package/src/interfaces/mcp/server.ts +47 -0
  95. package/src/interfaces/mcp/tool-builder.ts +261 -0
  96. package/src/interfaces/mcp/tools/analytics.ts +76 -0
  97. package/src/interfaces/mcp/tools/cart.ts +57 -0
  98. package/src/interfaces/mcp/tools/catalog.ts +299 -0
  99. package/src/interfaces/mcp/tools/index.ts +22 -0
  100. package/src/interfaces/mcp/tools/inventory.ts +161 -0
  101. package/src/interfaces/mcp/tools/orders.ts +104 -0
  102. package/src/interfaces/mcp/tools/pricing.ts +94 -0
  103. package/src/interfaces/mcp/tools/promotions.ts +106 -0
  104. package/src/interfaces/mcp/tools/registry.ts +101 -0
  105. package/src/interfaces/mcp/tools/search.ts +42 -0
  106. package/src/interfaces/mcp/tools/webhooks.ts +48 -0
  107. package/src/interfaces/mcp/transport.ts +128 -0
  108. package/src/interfaces/rest/customer-portal.ts +299 -0
  109. package/src/interfaces/rest/index.ts +74 -0
  110. package/src/interfaces/rest/router.ts +333 -0
  111. package/src/interfaces/rest/routes/admin-jobs.ts +58 -0
  112. package/src/interfaces/rest/routes/audit.ts +50 -0
  113. package/src/interfaces/rest/routes/carts.ts +89 -0
  114. package/src/interfaces/rest/routes/catalog.ts +493 -0
  115. package/src/interfaces/rest/routes/checkout.ts +284 -0
  116. package/src/interfaces/rest/routes/inventory.ts +70 -0
  117. package/src/interfaces/rest/routes/media.ts +86 -0
  118. package/src/interfaces/rest/routes/orders.ts +78 -0
  119. package/src/interfaces/rest/routes/payments.ts +60 -0
  120. package/src/interfaces/rest/routes/pricing.ts +57 -0
  121. package/src/interfaces/rest/routes/promotions.ts +93 -0
  122. package/src/interfaces/rest/routes/search.ts +71 -0
  123. package/src/interfaces/rest/routes/webhooks.ts +46 -0
  124. package/src/interfaces/rest/schemas/admin-jobs.ts +40 -0
  125. package/src/interfaces/rest/schemas/audit.ts +46 -0
  126. package/src/interfaces/rest/schemas/carts.ts +125 -0
  127. package/src/interfaces/rest/schemas/catalog.ts +450 -0
  128. package/src/interfaces/rest/schemas/checkout.ts +66 -0
  129. package/src/interfaces/rest/schemas/customer-portal.ts +195 -0
  130. package/src/interfaces/rest/schemas/inventory.ts +138 -0
  131. package/src/interfaces/rest/schemas/media.ts +75 -0
  132. package/src/interfaces/rest/schemas/orders.ts +104 -0
  133. package/src/interfaces/rest/schemas/pricing.ts +80 -0
  134. package/src/interfaces/rest/schemas/promotions.ts +110 -0
  135. package/src/interfaces/rest/schemas/responses.ts +85 -0
  136. package/src/interfaces/rest/schemas/search.ts +58 -0
  137. package/src/interfaces/rest/schemas/shared.ts +62 -0
  138. package/src/interfaces/rest/schemas/webhooks.ts +68 -0
  139. package/src/interfaces/rest/utils.ts +104 -0
  140. package/src/interfaces/rest/webhook-router.ts +50 -0
  141. package/src/kernel/compensation/executor.ts +61 -0
  142. package/src/kernel/compensation/types.ts +26 -0
  143. package/src/kernel/database/adapter.ts +21 -0
  144. package/src/kernel/database/drizzle-db.ts +56 -0
  145. package/src/kernel/database/migrate.ts +76 -0
  146. package/src/kernel/database/plugin-types.ts +34 -0
  147. package/src/kernel/database/schema.ts +49 -0
  148. package/src/kernel/database/scoped-db.ts +68 -0
  149. package/src/kernel/database/tx-context.ts +46 -0
  150. package/src/kernel/error-mapper.ts +15 -0
  151. package/src/kernel/errors.ts +89 -0
  152. package/src/kernel/factory/repository-factory.ts +244 -0
  153. package/src/kernel/hooks/create-context.ts +43 -0
  154. package/src/kernel/hooks/executor.ts +88 -0
  155. package/src/kernel/hooks/registry.ts +74 -0
  156. package/src/kernel/hooks/types.ts +52 -0
  157. package/src/kernel/http-error.ts +44 -0
  158. package/src/kernel/jobs/adapter.ts +36 -0
  159. package/src/kernel/jobs/drizzle-adapter.ts +58 -0
  160. package/src/kernel/jobs/runner.ts +153 -0
  161. package/src/kernel/jobs/schema.ts +46 -0
  162. package/src/kernel/jobs/types.ts +30 -0
  163. package/src/kernel/local-api.ts +187 -0
  164. package/src/kernel/plugin/manifest.ts +271 -0
  165. package/src/kernel/query/executor.ts +184 -0
  166. package/src/kernel/query/registry.ts +46 -0
  167. package/src/kernel/result.ts +33 -0
  168. package/src/kernel/schema/extra-columns.ts +37 -0
  169. package/src/kernel/service-registry.ts +76 -0
  170. package/src/kernel/service-timing.ts +89 -0
  171. package/src/kernel/state-machine/machine.ts +101 -0
  172. package/src/modules/analytics/drizzle-adapter.ts +426 -0
  173. package/src/modules/analytics/hooks.ts +11 -0
  174. package/src/modules/analytics/models.ts +125 -0
  175. package/src/modules/analytics/repository/index.ts +6 -0
  176. package/src/modules/analytics/service.ts +245 -0
  177. package/src/modules/analytics/types.ts +180 -0
  178. package/src/modules/audit/hooks.ts +78 -0
  179. package/src/modules/audit/schema.ts +33 -0
  180. package/src/modules/audit/service.ts +151 -0
  181. package/src/modules/cart/access.ts +27 -0
  182. package/src/modules/cart/matcher.ts +26 -0
  183. package/src/modules/cart/repository/index.ts +234 -0
  184. package/src/modules/cart/schema.ts +42 -0
  185. package/src/modules/cart/schemas.ts +38 -0
  186. package/src/modules/cart/service.ts +541 -0
  187. package/src/modules/catalog/repository/index.ts +772 -0
  188. package/src/modules/catalog/schema.ts +203 -0
  189. package/src/modules/catalog/schemas.ts +104 -0
  190. package/src/modules/catalog/service.ts +1544 -0
  191. package/src/modules/customers/repository/index.ts +327 -0
  192. package/src/modules/customers/schema.ts +64 -0
  193. package/src/modules/customers/service.ts +171 -0
  194. package/src/modules/fulfillment/repository/index.ts +426 -0
  195. package/src/modules/fulfillment/schema.ts +101 -0
  196. package/src/modules/fulfillment/service.ts +555 -0
  197. package/src/modules/fulfillment/types.ts +59 -0
  198. package/src/modules/inventory/repository/index.ts +509 -0
  199. package/src/modules/inventory/schema.ts +94 -0
  200. package/src/modules/inventory/schemas.ts +38 -0
  201. package/src/modules/inventory/service.ts +490 -0
  202. package/src/modules/media/adapter.ts +17 -0
  203. package/src/modules/media/repository/index.ts +274 -0
  204. package/src/modules/media/schema.ts +41 -0
  205. package/src/modules/media/service.ts +151 -0
  206. package/src/modules/orders/repository/index.ts +287 -0
  207. package/src/modules/orders/schema.ts +66 -0
  208. package/src/modules/orders/service.ts +619 -0
  209. package/src/modules/orders/stale-order-cleanup.ts +76 -0
  210. package/src/modules/organization/service.ts +191 -0
  211. package/src/modules/payments/adapter.ts +47 -0
  212. package/src/modules/payments/repository/index.ts +6 -0
  213. package/src/modules/payments/service.ts +107 -0
  214. package/src/modules/pricing/repository/index.ts +291 -0
  215. package/src/modules/pricing/schema.ts +71 -0
  216. package/src/modules/pricing/schemas.ts +38 -0
  217. package/src/modules/pricing/service.ts +494 -0
  218. package/src/modules/promotions/repository/index.ts +325 -0
  219. package/src/modules/promotions/schema.ts +62 -0
  220. package/src/modules/promotions/schemas.ts +38 -0
  221. package/src/modules/promotions/service.ts +598 -0
  222. package/src/modules/search/adapter.ts +57 -0
  223. package/src/modules/search/hooks.ts +12 -0
  224. package/src/modules/search/repository/index.ts +6 -0
  225. package/src/modules/search/service.ts +315 -0
  226. package/src/modules/shipping/calculator.ts +188 -0
  227. package/src/modules/shipping/repository/index.ts +6 -0
  228. package/src/modules/shipping/service.ts +51 -0
  229. package/src/modules/tax/adapter.ts +60 -0
  230. package/src/modules/tax/repository/index.ts +6 -0
  231. package/src/modules/tax/service.ts +53 -0
  232. package/src/modules/webhooks/hook.ts +34 -0
  233. package/src/modules/webhooks/repository/index.ts +278 -0
  234. package/src/modules/webhooks/schema.ts +56 -0
  235. package/src/modules/webhooks/service.ts +117 -0
  236. package/src/modules/webhooks/signing.ts +6 -0
  237. package/src/modules/webhooks/ssrf-guard.ts +71 -0
  238. package/src/modules/webhooks/tasks.ts +52 -0
  239. package/src/modules/webhooks/worker.ts +134 -0
  240. package/src/runtime/commerce.ts +145 -0
  241. package/src/runtime/kernel.ts +426 -0
  242. package/src/runtime/logger.ts +36 -0
  243. package/src/runtime/server.ts +355 -0
  244. package/src/runtime/shutdown.ts +43 -0
  245. package/src/test-utils/create-pglite-adapter.ts +129 -0
  246. package/src/test-utils/create-plugin-test-app.ts +128 -0
  247. package/src/test-utils/create-repository-test-harness.ts +16 -0
  248. package/src/test-utils/create-test-config.ts +190 -0
  249. package/src/test-utils/create-test-kernel.ts +7 -0
  250. package/src/test-utils/create-test-plugin-context.ts +75 -0
  251. package/src/test-utils/rest-api-test-utils.ts +265 -0
  252. package/src/test-utils/test-actors.ts +62 -0
  253. package/src/test-utils/typed-hooks.ts +54 -0
  254. package/src/types/commerce-types.ts +34 -0
  255. package/src/utils/id.ts +3 -0
  256. package/src/utils/logger.ts +18 -0
  257. package/src/utils/pagination.ts +22 -0
@@ -0,0 +1,271 @@
1
+ import type { Hono } from "hono";
2
+ import type { OpenAPIHono, RouteConfig } from "@hono/zod-openapi";
3
+ import type { PluginDb } from "../database/plugin-types.js";
4
+ import type { DatabaseAdapter } from "../database/adapter.js";
5
+ import type {
6
+ CommerceConfig,
7
+ CommercePlugin,
8
+ MCPTool,
9
+ } from "../../config/types.js";
10
+
11
+ // ─── Plugin Logger ────────────────────────────────────────────────────
12
+
13
+ export interface PluginLogger {
14
+ info(message: string, data?: unknown): void;
15
+ warn(message: string, data?: unknown): void;
16
+ error(message: string, data?: unknown): void;
17
+ }
18
+
19
+ // ─── Plugin Registration Types ────────────────────────────────────────
20
+
21
+ /**
22
+ * Plugin route registration. Supports two modes:
23
+ *
24
+ * 1. Legacy: { method, path, handler } — works, but invisible to OpenAPI spec.
25
+ * 2. OpenAPI: { openapi, handler } — validated by Zod, appears in /api/doc.
26
+ *
27
+ * Use mode 2 for any route that accepts a request body or returns structured data.
28
+ * Mode 1 is acceptable for simple routes (health checks, redirects, file serving).
29
+ */
30
+ export type PluginRouteRegistration =
31
+ | { method: string; path: string; handler: (...args: unknown[]) => unknown }
32
+ | { openapi: RouteConfig; handler: (...args: unknown[]) => unknown };
33
+
34
+ export interface PluginHookRegistration {
35
+ key: string;
36
+ handler: (...args: unknown[]) => unknown;
37
+ }
38
+
39
+ // ─── Plugin Context (available to routes/mcpTools at boot time) ───────
40
+
41
+ export interface PluginContext {
42
+ config: CommerceConfig;
43
+ services: Record<string, unknown>;
44
+ database: {
45
+ /** Drizzle database instance — use for queries, inserts, updates, deletes */
46
+ db: PluginDb;
47
+ transaction<T>(fn: (tx: PluginDb) => Promise<T>): Promise<T>;
48
+ };
49
+ logger: PluginLogger;
50
+ }
51
+
52
+ // ─── Plugin Manifest (input to defineCommercePlugin) ──────────────────
53
+
54
+ /**
55
+ * Permission scope declared by a plugin.
56
+ * Collected at boot time and available via GET /api/admin/permissions.
57
+ */
58
+ export interface PluginPermission {
59
+ /** Permission scope string, e.g., "wishlist:write", "marketplace:admin" */
60
+ scope: string;
61
+ /** Human-readable description for admin UIs */
62
+ description: string;
63
+ }
64
+
65
+ export interface CommercePluginManifest {
66
+ id: string;
67
+ version: string;
68
+ /**
69
+ * IDs of plugins that must be registered before this one.
70
+ * If any required plugin is missing at registration time,
71
+ * defineCommercePlugin will throw with a clear message.
72
+ */
73
+ requires?: string[];
74
+ /**
75
+ * Permission scopes this plugin introduces.
76
+ * Used by admin UIs to build role editors, and validated at boot time
77
+ * against .permission() calls in routes.
78
+ */
79
+ permissions?: PluginPermission[];
80
+ /**
81
+ * Returns Drizzle `pgTable` objects that this plugin needs.
82
+ * These are collected into `config.customSchemas[]` and merged with core
83
+ * schema by `buildSchema(config)`. Each key becomes a named export in the
84
+ * merged schema; names must not collide with core table exports.
85
+ */
86
+ schema?: () => Record<string, unknown>;
87
+ hooks?: () => PluginHookRegistration[];
88
+ routes?: (ctx: PluginContext) => PluginRouteRegistration[];
89
+ mcpTools?: (ctx: PluginContext) => MCPTool[];
90
+ analyticsModels?: () => unknown[];
91
+ }
92
+
93
+ // ─── Plugin Dependency Tracking ──────────────────────────────────────
94
+ // Accumulates plugin IDs as they register during defineConfig().
95
+ // Reset at the start of each defineConfig() call.
96
+ export const _registeredPlugins = new Set<string>();
97
+
98
+ /** @internal — called by defineConfig() before applying plugins */
99
+ export function _resetRegisteredPlugins(): void {
100
+ _registeredPlugins.clear();
101
+ }
102
+
103
+ /**
104
+ * Converts a plugin manifest into a config transform function.
105
+ *
106
+ * - `schema` → pushed into `config.customSchemas`
107
+ * - `hooks` → merged into `config.hooks` flat map
108
+ * - `routes` → chained onto `config.routes` (evaluated at boot with kernel)
109
+ * - `mcpTools` → chained onto `config.mcpTools` (evaluated at boot with kernel)
110
+ * - `analyticsModels` → pushed into `config.analytics.models`
111
+ */
112
+ export function defineCommercePlugin(
113
+ manifest: CommercePluginManifest,
114
+ ): CommercePlugin {
115
+ return (config: CommerceConfig): CommerceConfig => {
116
+ // ── Dependency check ───────────────────────────────────────────
117
+ // In test/development, warn instead of throwing so plugins can be
118
+ // tested in isolation via createPluginTestApp() without installing
119
+ // all dependencies. In production, this is a hard error.
120
+ if (manifest.requires) {
121
+ for (const dep of manifest.requires) {
122
+ if (!_registeredPlugins.has(dep)) {
123
+ const msg = `Plugin "${manifest.id}" requires "${dep}" to be installed before it. Add ${dep}Plugin() before ${manifest.id}Plugin() in your config.plugins array.`;
124
+ if (process.env.NODE_ENV === "production") {
125
+ throw new Error(msg);
126
+ }
127
+ // Non-production: log warning but continue (allows isolated testing)
128
+ console.warn(`[plugin:${manifest.id}] WARNING: ${msg}`);
129
+ }
130
+ }
131
+ }
132
+ _registeredPlugins.add(manifest.id);
133
+ let result = { ...config };
134
+
135
+ // 1. Schema — push into customSchemas for kernel to register
136
+ if (manifest.schema) {
137
+ const schemas = manifest.schema();
138
+ result = {
139
+ ...result,
140
+ customSchemas: [...(result.customSchemas ?? []), schemas],
141
+ };
142
+ }
143
+
144
+ // 2. Hooks — merge into flat hooks map (kernel registers at boot)
145
+ if (manifest.hooks) {
146
+ const registrations = manifest.hooks();
147
+ const hookMap: Record<string, Array<(...args: unknown[]) => unknown>> = {
148
+ ...(result.hooks ?? {}),
149
+ };
150
+ for (const reg of registrations) {
151
+ hookMap[reg.key] = [...(hookMap[reg.key] ?? []), reg.handler];
152
+ }
153
+ result = { ...result, hooks: hookMap };
154
+ }
155
+
156
+ // 3. Routes — chain onto config.routes (deferred: needs kernel at boot)
157
+ if (manifest.routes) {
158
+ const existingRoutes = result.routes;
159
+ const pluginRoutes = manifest.routes;
160
+ result = {
161
+ ...result,
162
+ routes: (app: Hono, kernel: unknown) => {
163
+ existingRoutes?.(app, kernel);
164
+ const k = kernel as {
165
+ config: CommerceConfig;
166
+ services: Record<string, unknown>;
167
+ database: DatabaseAdapter;
168
+ logger: PluginLogger;
169
+ };
170
+ // Narrow DatabaseAdapter<unknown> → PluginContext.database once here.
171
+ // All plugin code receives typed PluginDb — no casts downstream.
172
+ const regs = pluginRoutes({
173
+ config: k.config,
174
+ services: k.services,
175
+ database: {
176
+ db: k.database.db as PluginDb,
177
+ transaction: k.database.transaction as PluginContext["database"]["transaction"],
178
+ },
179
+ logger: k.logger,
180
+ });
181
+ for (const route of regs) {
182
+ // ── Error boundary: wrap handler with plugin context ──
183
+ const originalHandler = route.handler;
184
+ const wrappedHandler = async (...args: unknown[]) => {
185
+ try {
186
+ return await originalHandler(...args);
187
+ } catch (err) {
188
+ // Try to extract logger from Hono context
189
+ const c = args[0] as Record<string, unknown>;
190
+ const logger = (c?.get as Function)?.("logger") as { error: Function } | undefined;
191
+ logger?.error?.(
192
+ { err, plugin: manifest.id },
193
+ `[plugin:${manifest.id}] route handler error`,
194
+ );
195
+ throw err; // re-throw so global handler still catches
196
+ }
197
+ };
198
+
199
+ if ("openapi" in route) {
200
+ // OpenAPI route: validated by Zod, appears in /api/doc
201
+ // Hono type interop — OpenAPIHono.openapi() expects strict RouteConfig+Handler
202
+ // generics that can't be statically resolved for dynamic plugin routes.
203
+ // @ts-expect-error -- dynamic plugin routes cannot satisfy Hono's strict handler generics
204
+ (app as OpenAPIHono).openapi(route.openapi, wrappedHandler);
205
+ } else {
206
+ // Legacy route: raw handler, invisible to OpenAPI spec.
207
+ // Explicit dispatch avoids casting Hono to a Record.
208
+ // Handler cast is a single `as any` (Hono's overloaded method
209
+ // signatures can't unify with our generic handler shape).
210
+ const h = wrappedHandler as any;
211
+ switch (route.method.toLowerCase()) {
212
+ case "get": app.get(route.path, h); break;
213
+ case "post": app.post(route.path, h); break;
214
+ case "put": app.put(route.path, h); break;
215
+ case "patch": app.patch(route.path, h); break;
216
+ case "delete": app.delete(route.path, h); break;
217
+ case "options": app.options(route.path, h); break;
218
+ default:
219
+ console.warn(`[plugin:${manifest.id}] unsupported HTTP method "${route.method}" for ${route.path}`);
220
+ }
221
+ }
222
+ }
223
+ },
224
+ };
225
+ }
226
+
227
+ // 4. MCP Tools — chain onto config.mcpTools (deferred: needs kernel)
228
+ if (manifest.mcpTools) {
229
+ const existingMcpTools = result.mcpTools;
230
+ const pluginMcpTools = manifest.mcpTools;
231
+ result = {
232
+ ...result,
233
+ mcpTools: (kernel: unknown) => {
234
+ const existing = existingMcpTools?.(kernel) ?? [];
235
+ const k = kernel as {
236
+ config: CommerceConfig;
237
+ services: Record<string, unknown>;
238
+ database: DatabaseAdapter;
239
+ logger: PluginLogger;
240
+ };
241
+ return [
242
+ ...existing,
243
+ ...pluginMcpTools({
244
+ config: k.config,
245
+ services: k.services,
246
+ database: {
247
+ db: k.database.db as PluginDb,
248
+ transaction: k.database.transaction as PluginContext["database"]["transaction"],
249
+ },
250
+ logger: k.logger,
251
+ }),
252
+ ];
253
+ },
254
+ };
255
+ }
256
+
257
+ // 5. Analytics models — push into config.analytics.models
258
+ if (manifest.analyticsModels) {
259
+ const models = manifest.analyticsModels();
260
+ result = {
261
+ ...result,
262
+ analytics: {
263
+ ...result.analytics,
264
+ models: [...(result.analytics?.models ?? []), ...models],
265
+ },
266
+ };
267
+ }
268
+
269
+ return result;
270
+ };
271
+ }
@@ -0,0 +1,184 @@
1
+ import { CommerceNotFoundError } from "../errors.js";
2
+ import type {
3
+ QueryRegistry,
4
+ EntityDefinition,
5
+ RelationDefinition,
6
+ } from "./registry.js";
7
+
8
+ export interface QueryInput {
9
+ entity: string;
10
+ id?: string;
11
+ filters?: Record<string, unknown>;
12
+ include?: string[];
13
+ pagination?: { limit?: number; offset?: number };
14
+ }
15
+
16
+ export interface QueryResult<T = Record<string, unknown>> {
17
+ data: T[];
18
+ total?: number | undefined;
19
+ }
20
+
21
+ /**
22
+ * Executes a query against the registry, resolving includes via
23
+ * batched dataloader-style fetches (one WHERE IN per relation).
24
+ */
25
+ export async function executeQuery<T = Record<string, unknown>>(
26
+ registry: QueryRegistry,
27
+ services: Record<string, unknown>,
28
+ input: QueryInput,
29
+ ): Promise<QueryResult<T>> {
30
+ const definition = registry.get(input.entity);
31
+ if (!definition) {
32
+ throw new CommerceNotFoundError(
33
+ `No entity registered with name "${input.entity}".`,
34
+ );
35
+ }
36
+
37
+ const service = services[definition.service] as Record<
38
+ string,
39
+ (...args: unknown[]) => Promise<unknown>
40
+ >;
41
+
42
+ // 1. Fetch primary records
43
+ let rows: Record<string, unknown>[];
44
+ let total: number | undefined;
45
+
46
+ if (input.id) {
47
+ const result = (await service[definition.getByIdMethod]!(
48
+ input.id,
49
+ )) as { ok?: boolean; value?: unknown };
50
+ const value = result?.value ?? result;
51
+ rows = value != null ? [value as Record<string, unknown>] : [];
52
+ } else {
53
+ const result = (await service[definition.listMethod]!(
54
+ input.filters ?? {},
55
+ input.pagination,
56
+ )) as {
57
+ ok?: boolean;
58
+ value?: { items?: unknown[]; total?: number };
59
+ };
60
+ const resolved = result?.value ?? result;
61
+ if (resolved && typeof resolved === "object" && "items" in resolved) {
62
+ rows = (resolved.items ?? []) as Record<string, unknown>[];
63
+ total = resolved.total;
64
+ } else if (Array.isArray(resolved)) {
65
+ rows = resolved as Record<string, unknown>[];
66
+ } else {
67
+ rows = [];
68
+ }
69
+ }
70
+
71
+ // 2. Resolve includes
72
+ if (input.include?.length) {
73
+ await resolveIncludes(rows, input.include, definition, services, registry);
74
+ }
75
+
76
+ return { data: rows as T[], total };
77
+ }
78
+
79
+ async function resolveIncludes(
80
+ rows: Record<string, unknown>[],
81
+ includes: string[],
82
+ definition: EntityDefinition,
83
+ services: Record<string, unknown>,
84
+ registry: QueryRegistry,
85
+ ): Promise<void> {
86
+ // Group includes by top-level segment
87
+ const topLevel = new Map<string, string[]>();
88
+ for (const path of includes) {
89
+ const dot = path.indexOf(".");
90
+ if (dot === -1) {
91
+ if (!topLevel.has(path)) topLevel.set(path, []);
92
+ } else {
93
+ const parent = path.substring(0, dot);
94
+ const child = path.substring(dot + 1);
95
+ const existing = topLevel.get(parent) ?? [];
96
+ existing.push(child);
97
+ topLevel.set(parent, existing);
98
+ }
99
+ }
100
+
101
+ for (const [relationName, nestedIncludes] of topLevel) {
102
+ const relation = definition.relations[relationName];
103
+ if (!relation) continue;
104
+
105
+ const targetService = services[relation.targetService] as
106
+ | Record<string, (...args: unknown[]) => Promise<unknown>>
107
+ | undefined;
108
+ if (!targetService) continue;
109
+
110
+ // Collect foreign key values (deduplicated)
111
+ const ids = [
112
+ ...new Set(
113
+ rows
114
+ .map((r) => r[relation.foreignKey])
115
+ .filter((v): v is string => v != null && typeof v === "string"),
116
+ ),
117
+ ];
118
+ if (ids.length === 0) continue;
119
+
120
+ // One batched query
121
+ const batchFn = targetService[relation.batchMethod];
122
+ if (!batchFn) continue;
123
+
124
+ const relatedResult = await batchFn(ids);
125
+ const relatedRows = extractRows(relatedResult);
126
+
127
+ // Build lookup
128
+ const map = new Map<string, unknown>();
129
+ for (const related of relatedRows) {
130
+ const rec = related as Record<string, unknown>;
131
+ if (relation.isList) {
132
+ const key = rec[relation.foreignKey] as string;
133
+ if (!map.has(key)) map.set(key, []);
134
+ (map.get(key) as unknown[]).push(rec);
135
+ } else {
136
+ map.set(rec["id"] as string, rec);
137
+ }
138
+ }
139
+
140
+ // Attach to parent rows
141
+ for (const row of rows) {
142
+ const fkValue = row[relation.foreignKey] as string | undefined;
143
+ if (!fkValue) continue;
144
+ row[relation.attachAs] =
145
+ map.get(fkValue) ?? (relation.isList ? [] : null);
146
+ }
147
+
148
+ // Resolve nested includes
149
+ if (nestedIncludes.length > 0) {
150
+ const targetDef = registry.get(relation.targetService);
151
+ if (targetDef) {
152
+ const nestedRows = relation.isList
153
+ ? rows.flatMap(
154
+ (r) =>
155
+ (r[relation.attachAs] as Record<string, unknown>[]) ?? [],
156
+ )
157
+ : rows
158
+ .map((r) => r[relation.attachAs] as Record<string, unknown>)
159
+ .filter(Boolean);
160
+ await resolveIncludes(
161
+ nestedRows,
162
+ nestedIncludes,
163
+ targetDef,
164
+ services,
165
+ registry,
166
+ );
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ function extractRows(result: unknown): unknown[] {
173
+ if (Array.isArray(result)) return result;
174
+ if (result && typeof result === "object") {
175
+ const r = result as { ok?: boolean; value?: unknown };
176
+ if (r.value != null) {
177
+ if (Array.isArray(r.value)) return r.value;
178
+ if (typeof r.value === "object" && "items" in r.value) {
179
+ return (r.value as { items: unknown[] }).items;
180
+ }
181
+ }
182
+ }
183
+ return [];
184
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Defines a relation from a parent entity to a related entity.
3
+ * Used by the query executor to batch-load related records.
4
+ */
5
+ export interface RelationDefinition {
6
+ foreignKey: string;
7
+ targetService: string;
8
+ batchMethod: string;
9
+ attachAs: string;
10
+ isList?: boolean;
11
+ }
12
+
13
+ /**
14
+ * Defines an entity that can be queried via kernel.query().
15
+ */
16
+ export interface EntityDefinition {
17
+ service: string;
18
+ getByIdMethod: string;
19
+ listMethod: string;
20
+ relations: Record<string, RelationDefinition>;
21
+ }
22
+
23
+ /**
24
+ * Registry of queryable entities and their relations.
25
+ * Modules register their entities at kernel boot.
26
+ * Plugins can register additional entities.
27
+ */
28
+ export class QueryRegistry {
29
+ private entities = new Map<string, EntityDefinition>();
30
+
31
+ register(name: string, definition: EntityDefinition): void {
32
+ this.entities.set(name, definition);
33
+ }
34
+
35
+ get(name: string): EntityDefinition | undefined {
36
+ return this.entities.get(name);
37
+ }
38
+
39
+ has(name: string): boolean {
40
+ return this.entities.has(name);
41
+ }
42
+
43
+ listEntities(): string[] {
44
+ return [...this.entities.keys()];
45
+ }
46
+ }
@@ -0,0 +1,33 @@
1
+ import type { CommerceError } from "./errors.js";
2
+
3
+ export type Result<T, E = CommerceError> =
4
+ | { ok: true; value: T; meta?: Record<string, unknown> }
5
+ | { ok: false; error: E };
6
+
7
+ /**
8
+ * Simplified Result types for plugin services.
9
+ * Plugin services return `PluginResult<T>` instead of `Result<T, CommerceError>`.
10
+ * This matches the `{ ok: true; value: T } | { ok: false; error: string }` pattern
11
+ * that every plugin has been copy-pasting.
12
+ */
13
+ export type PluginResult<T> = { ok: true; value: T } | { ok: false; error: string; code?: string };
14
+ export type PluginResultErr = { ok: false; error: string; code?: string };
15
+
16
+ export function Ok<T>(value: T, meta?: Record<string, unknown>): Result<T, never> {
17
+ if (meta !== undefined) {
18
+ return { ok: true, value, meta };
19
+ }
20
+ return { ok: true, value };
21
+ }
22
+
23
+ export function Err<E>(error: E): Result<never, E> {
24
+ return { ok: false, error };
25
+ }
26
+
27
+ /**
28
+ * String-based Err for plugin services.
29
+ * Usage: `return PluginErr("Not found", "NOT_FOUND")`
30
+ */
31
+ export function PluginErr(error: string, code?: string): PluginResultErr {
32
+ return code ? { ok: false, error, code } : { ok: false, error };
33
+ }
@@ -0,0 +1,37 @@
1
+ import type { PgColumnBuilderBase } from "drizzle-orm/pg-core";
2
+
3
+ /**
4
+ * Options for modules that support schema extension via extra columns.
5
+ *
6
+ * Usage:
7
+ * ```typescript
8
+ * const catalogModule = createCatalogModule({
9
+ * extraColumns: (base) => ({
10
+ * supplierCode: text("supplier_code"),
11
+ * gtin: text("gtin").unique(),
12
+ * }),
13
+ * });
14
+ * ```
15
+ */
16
+ export interface ExtraColumnsOption<TBaseColumns extends Record<string, unknown>> {
17
+ extraColumns?: (
18
+ baseColumns: TBaseColumns,
19
+ ) => Record<string, PgColumnBuilderBase>;
20
+ }
21
+
22
+ /**
23
+ * Merges base columns with optional extra columns from configuration.
24
+ * Returns the combined column definitions for use in `pgTable()`.
25
+ */
26
+ export function mergeExtraColumns<
27
+ TBase extends Record<string, unknown>,
28
+ >(
29
+ baseColumns: TBase,
30
+ extraColumnsFn?: (base: TBase) => Record<string, PgColumnBuilderBase>,
31
+ ): TBase & Record<string, PgColumnBuilderBase> {
32
+ if (!extraColumnsFn) return baseColumns as TBase & Record<string, PgColumnBuilderBase>;
33
+ const extra = extraColumnsFn(baseColumns);
34
+ return { ...baseColumns, ...extra };
35
+ }
36
+
37
+
@@ -0,0 +1,76 @@
1
+ /**
2
+ * ServiceRegistry -- typed surface of the kernel service container
3
+ * for plugin-to-plugin communication.
4
+ *
5
+ * Plugin services accept this as an optional constructor parameter
6
+ * to call core services without resorting to raw SQL.
7
+ *
8
+ * Core services are loosely typed here (method signatures use `unknown`)
9
+ * to avoid plugin packages depending on core's internal service types.
10
+ * Plugin authors cast the return values at the call site.
11
+ *
12
+ * Usage in a plugin service:
13
+ *
14
+ * import type { ServiceRegistry } from "@unifiedcommerce/core";
15
+ *
16
+ * class MyService {
17
+ * constructor(private db: PluginDb, private services?: ServiceRegistry) {}
18
+ *
19
+ * async doWork() {
20
+ * const result = await this.services?.inventory.adjust({
21
+ * entityId: "...", adjustment: -5, reason: "recipe deduction"
22
+ * }, actor);
23
+ * }
24
+ * }
25
+ */
26
+
27
+ export interface ServiceRegistry {
28
+ inventory: {
29
+ adjust(
30
+ input: {
31
+ entityId: string;
32
+ variantId?: string;
33
+ warehouseId?: string;
34
+ adjustment: number;
35
+ reason?: string;
36
+ },
37
+ actor?: unknown,
38
+ ): Promise<{ ok: boolean; value?: unknown; error?: unknown }>;
39
+ createWarehouse(
40
+ input: { name: string; code: string },
41
+ actor?: unknown,
42
+ ): Promise<{ ok: boolean; value?: unknown; error?: unknown }>;
43
+ reserve(input: unknown, actor?: unknown): Promise<unknown>;
44
+ release(input: unknown, actor?: unknown): Promise<unknown>;
45
+ getAvailable(input: unknown, actor?: unknown): Promise<unknown>;
46
+ [method: string]: unknown;
47
+ };
48
+ catalog: {
49
+ create(input: unknown, actor?: unknown): Promise<{ ok: boolean; value?: unknown; error?: unknown }>;
50
+ getById(id: string, options?: unknown): Promise<{ ok: boolean; value?: unknown; error?: unknown }>;
51
+ publish(id: string, actor?: unknown): Promise<unknown>;
52
+ list(input?: unknown): Promise<unknown>;
53
+ [method: string]: unknown;
54
+ };
55
+ customers: {
56
+ getByUserId(userId: string, actor?: unknown): Promise<{ ok: boolean; value?: unknown; error?: unknown }>;
57
+ getById(id: string, actor?: unknown): Promise<{ ok: boolean; value?: unknown; error?: unknown }>;
58
+ [method: string]: unknown;
59
+ };
60
+ orders: {
61
+ create(input: unknown, actor?: unknown): Promise<{ ok: boolean; value?: unknown; error?: unknown }>;
62
+ changeStatus(input: unknown, actor?: unknown): Promise<unknown>;
63
+ [method: string]: unknown;
64
+ };
65
+ cart: {
66
+ create(input: unknown, actor?: unknown): Promise<{ ok: boolean; value?: unknown; error?: unknown }>;
67
+ [method: string]: unknown;
68
+ };
69
+ organization: {
70
+ create(input: unknown): Promise<{ ok: boolean; value?: unknown; error?: unknown }>;
71
+ getById(id: string): Promise<unknown>;
72
+ [method: string]: unknown;
73
+ };
74
+ /** Access plugin-registered services by plugin ID or service name */
75
+ [key: string]: unknown;
76
+ }