@unifiedcommerce/core 0.1.0 → 0.2.0

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 (245) 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 +2 -2
  73. package/src/adapters/console-email.ts +0 -43
  74. package/src/auth/access.ts +0 -187
  75. package/src/auth/auth-schema.ts +0 -139
  76. package/src/auth/middleware.ts +0 -161
  77. package/src/auth/org.ts +0 -41
  78. package/src/auth/permissions.ts +0 -28
  79. package/src/auth/setup.ts +0 -169
  80. package/src/auth/system-actor.ts +0 -19
  81. package/src/auth/types.ts +0 -10
  82. package/src/config/defaults.ts +0 -82
  83. package/src/config/define-config.ts +0 -53
  84. package/src/config/types.ts +0 -299
  85. package/src/generated/plugin-capabilities.d.ts +0 -20
  86. package/src/generated/plugin-manifest.ts +0 -23
  87. package/src/generated/plugin-repositories.d.ts +0 -20
  88. package/src/hooks/checkout-completion.ts +0 -262
  89. package/src/hooks/checkout.ts +0 -677
  90. package/src/hooks/order-emails.ts +0 -62
  91. package/src/index.ts +0 -214
  92. package/src/interfaces/mcp/agent-prompt.ts +0 -174
  93. package/src/interfaces/mcp/context-enrichment.ts +0 -177
  94. package/src/interfaces/mcp/server.ts +0 -617
  95. package/src/interfaces/mcp/transport.ts +0 -68
  96. package/src/interfaces/rest/customer-portal.ts +0 -299
  97. package/src/interfaces/rest/index.ts +0 -74
  98. package/src/interfaces/rest/router.ts +0 -334
  99. package/src/interfaces/rest/routes/admin-jobs.ts +0 -58
  100. package/src/interfaces/rest/routes/audit.ts +0 -50
  101. package/src/interfaces/rest/routes/carts.ts +0 -89
  102. package/src/interfaces/rest/routes/catalog.ts +0 -493
  103. package/src/interfaces/rest/routes/checkout.ts +0 -283
  104. package/src/interfaces/rest/routes/inventory.ts +0 -70
  105. package/src/interfaces/rest/routes/media.ts +0 -86
  106. package/src/interfaces/rest/routes/orders.ts +0 -78
  107. package/src/interfaces/rest/routes/payments.ts +0 -60
  108. package/src/interfaces/rest/routes/pricing.ts +0 -57
  109. package/src/interfaces/rest/routes/promotions.ts +0 -92
  110. package/src/interfaces/rest/routes/search.ts +0 -71
  111. package/src/interfaces/rest/routes/webhooks.ts +0 -46
  112. package/src/interfaces/rest/schemas/admin-jobs.ts +0 -40
  113. package/src/interfaces/rest/schemas/audit.ts +0 -46
  114. package/src/interfaces/rest/schemas/carts.ts +0 -125
  115. package/src/interfaces/rest/schemas/catalog.ts +0 -450
  116. package/src/interfaces/rest/schemas/checkout.ts +0 -66
  117. package/src/interfaces/rest/schemas/customer-portal.ts +0 -195
  118. package/src/interfaces/rest/schemas/inventory.ts +0 -138
  119. package/src/interfaces/rest/schemas/media.ts +0 -75
  120. package/src/interfaces/rest/schemas/orders.ts +0 -104
  121. package/src/interfaces/rest/schemas/pricing.ts +0 -80
  122. package/src/interfaces/rest/schemas/promotions.ts +0 -110
  123. package/src/interfaces/rest/schemas/responses.ts +0 -85
  124. package/src/interfaces/rest/schemas/search.ts +0 -58
  125. package/src/interfaces/rest/schemas/shared.ts +0 -62
  126. package/src/interfaces/rest/schemas/webhooks.ts +0 -68
  127. package/src/interfaces/rest/utils.ts +0 -104
  128. package/src/interfaces/rest/webhook-router.ts +0 -50
  129. package/src/kernel/compensation/executor.ts +0 -61
  130. package/src/kernel/compensation/types.ts +0 -26
  131. package/src/kernel/database/adapter.ts +0 -13
  132. package/src/kernel/database/drizzle-db.ts +0 -56
  133. package/src/kernel/database/migrate.ts +0 -76
  134. package/src/kernel/database/plugin-types.ts +0 -34
  135. package/src/kernel/database/schema.ts +0 -49
  136. package/src/kernel/database/scoped-db.ts +0 -68
  137. package/src/kernel/database/tx-context.ts +0 -46
  138. package/src/kernel/error-mapper.ts +0 -15
  139. package/src/kernel/errors.ts +0 -89
  140. package/src/kernel/factory/repository-factory.ts +0 -242
  141. package/src/kernel/hooks/create-context.ts +0 -43
  142. package/src/kernel/hooks/executor.ts +0 -88
  143. package/src/kernel/hooks/registry.ts +0 -74
  144. package/src/kernel/hooks/types.ts +0 -52
  145. package/src/kernel/http-error.ts +0 -44
  146. package/src/kernel/jobs/adapter.ts +0 -36
  147. package/src/kernel/jobs/drizzle-adapter.ts +0 -58
  148. package/src/kernel/jobs/runner.ts +0 -153
  149. package/src/kernel/jobs/schema.ts +0 -46
  150. package/src/kernel/jobs/types.ts +0 -30
  151. package/src/kernel/local-api.ts +0 -185
  152. package/src/kernel/plugin/manifest.ts +0 -253
  153. package/src/kernel/query/executor.ts +0 -184
  154. package/src/kernel/query/registry.ts +0 -46
  155. package/src/kernel/result.ts +0 -33
  156. package/src/kernel/schema/extra-columns.ts +0 -37
  157. package/src/kernel/service-registry.ts +0 -76
  158. package/src/kernel/service-timing.ts +0 -89
  159. package/src/kernel/state-machine/machine.ts +0 -101
  160. package/src/modules/analytics/drizzle-adapter.ts +0 -426
  161. package/src/modules/analytics/hooks.ts +0 -11
  162. package/src/modules/analytics/models.ts +0 -125
  163. package/src/modules/analytics/repository/index.ts +0 -6
  164. package/src/modules/analytics/service.ts +0 -245
  165. package/src/modules/analytics/types.ts +0 -180
  166. package/src/modules/audit/hooks.ts +0 -78
  167. package/src/modules/audit/schema.ts +0 -33
  168. package/src/modules/audit/service.ts +0 -151
  169. package/src/modules/cart/access.ts +0 -27
  170. package/src/modules/cart/matcher.ts +0 -26
  171. package/src/modules/cart/repository/index.ts +0 -234
  172. package/src/modules/cart/schema.ts +0 -42
  173. package/src/modules/cart/schemas.ts +0 -38
  174. package/src/modules/cart/service.ts +0 -541
  175. package/src/modules/catalog/repository/index.ts +0 -772
  176. package/src/modules/catalog/schema.ts +0 -203
  177. package/src/modules/catalog/schemas.ts +0 -104
  178. package/src/modules/catalog/service.ts +0 -1544
  179. package/src/modules/customers/repository/index.ts +0 -327
  180. package/src/modules/customers/schema.ts +0 -64
  181. package/src/modules/customers/service.ts +0 -171
  182. package/src/modules/fulfillment/repository/index.ts +0 -426
  183. package/src/modules/fulfillment/schema.ts +0 -101
  184. package/src/modules/fulfillment/service.ts +0 -555
  185. package/src/modules/fulfillment/types.ts +0 -59
  186. package/src/modules/inventory/repository/index.ts +0 -509
  187. package/src/modules/inventory/schema.ts +0 -94
  188. package/src/modules/inventory/schemas.ts +0 -38
  189. package/src/modules/inventory/service.ts +0 -490
  190. package/src/modules/media/adapter.ts +0 -17
  191. package/src/modules/media/repository/index.ts +0 -274
  192. package/src/modules/media/schema.ts +0 -41
  193. package/src/modules/media/service.ts +0 -151
  194. package/src/modules/orders/repository/index.ts +0 -287
  195. package/src/modules/orders/schema.ts +0 -66
  196. package/src/modules/orders/service.ts +0 -619
  197. package/src/modules/orders/stale-order-cleanup.ts +0 -76
  198. package/src/modules/organization/service.ts +0 -191
  199. package/src/modules/payments/adapter.ts +0 -47
  200. package/src/modules/payments/repository/index.ts +0 -6
  201. package/src/modules/payments/service.ts +0 -107
  202. package/src/modules/pricing/repository/index.ts +0 -291
  203. package/src/modules/pricing/schema.ts +0 -71
  204. package/src/modules/pricing/schemas.ts +0 -38
  205. package/src/modules/pricing/service.ts +0 -494
  206. package/src/modules/promotions/repository/index.ts +0 -325
  207. package/src/modules/promotions/schema.ts +0 -62
  208. package/src/modules/promotions/schemas.ts +0 -38
  209. package/src/modules/promotions/service.ts +0 -598
  210. package/src/modules/search/adapter.ts +0 -57
  211. package/src/modules/search/hooks.ts +0 -12
  212. package/src/modules/search/repository/index.ts +0 -6
  213. package/src/modules/search/service.ts +0 -315
  214. package/src/modules/shipping/calculator.ts +0 -188
  215. package/src/modules/shipping/repository/index.ts +0 -6
  216. package/src/modules/shipping/service.ts +0 -51
  217. package/src/modules/tax/adapter.ts +0 -60
  218. package/src/modules/tax/repository/index.ts +0 -6
  219. package/src/modules/tax/service.ts +0 -53
  220. package/src/modules/webhooks/hook.ts +0 -34
  221. package/src/modules/webhooks/repository/index.ts +0 -278
  222. package/src/modules/webhooks/schema.ts +0 -56
  223. package/src/modules/webhooks/service.ts +0 -117
  224. package/src/modules/webhooks/signing.ts +0 -6
  225. package/src/modules/webhooks/ssrf-guard.ts +0 -71
  226. package/src/modules/webhooks/tasks.ts +0 -52
  227. package/src/modules/webhooks/worker.ts +0 -134
  228. package/src/runtime/commerce.ts +0 -145
  229. package/src/runtime/kernel.ts +0 -419
  230. package/src/runtime/logger.ts +0 -36
  231. package/src/runtime/server.ts +0 -349
  232. package/src/runtime/shutdown.ts +0 -43
  233. package/src/test-utils/create-pglite-adapter.ts +0 -129
  234. package/src/test-utils/create-plugin-test-app.ts +0 -128
  235. package/src/test-utils/create-repository-test-harness.ts +0 -16
  236. package/src/test-utils/create-test-config.ts +0 -190
  237. package/src/test-utils/create-test-kernel.ts +0 -7
  238. package/src/test-utils/create-test-plugin-context.ts +0 -75
  239. package/src/test-utils/rest-api-test-utils.ts +0 -265
  240. package/src/test-utils/test-actors.ts +0 -62
  241. package/src/test-utils/typed-hooks.ts +0 -54
  242. package/src/types/commerce-types.ts +0 -34
  243. package/src/utils/id.ts +0 -3
  244. package/src/utils/logger.ts +0 -18
  245. package/src/utils/pagination.ts +0 -22
@@ -1,89 +0,0 @@
1
- /**
2
- * Service method observability via ES Proxy.
3
- *
4
- * Wraps a service object so that every async method call is timed.
5
- * Slow calls (above threshold) are logged with service name, method name,
6
- * and duration. Failed calls always log with the error.
7
- *
8
- * Usage at kernel boot:
9
- * services.inventory = withTiming(inventoryService, "inventory", logger);
10
- * services.catalog = withTiming(catalogService, "catalog", logger);
11
- *
12
- * Produces log entries like:
13
- * { service: "inventory", method: "adjust", durationMs: 245 } "slow service call"
14
- * { service: "catalog", method: "create", durationMs: 12, err: ... } "service call failed"
15
- *
16
- * Only wraps own methods (not inherited). Synchronous property access
17
- * (e.g., reading a field) passes through unchanged with zero overhead.
18
- */
19
-
20
- interface TimingLogger {
21
- info(obj: Record<string, unknown>, msg: string): void;
22
- error(obj: Record<string, unknown>, msg: string): void;
23
- }
24
-
25
- export function withTiming<T extends object>(
26
- service: T,
27
- serviceName: string,
28
- logger: TimingLogger,
29
- slowThresholdMs = 100,
30
- ): T {
31
- return new Proxy(service, {
32
- get(target, prop, receiver) {
33
- const value = Reflect.get(target, prop, receiver);
34
-
35
- // Only wrap functions, skip symbols and non-function props
36
- if (typeof value !== "function" || typeof prop === "symbol") {
37
- return value;
38
- }
39
-
40
- // Skip internal/private-looking methods
41
- const methodName = String(prop);
42
- if (methodName.startsWith("_")) return value;
43
-
44
- return function proxiedMethod(this: unknown, ...args: unknown[]) {
45
- const start = performance.now();
46
-
47
- // Call the original method
48
- let result: unknown;
49
- try {
50
- result = value.apply(target, args);
51
- } catch (err) {
52
- // Synchronous throw
53
- const durationMs = Math.round(performance.now() - start);
54
- logger.error(
55
- { service: serviceName, method: methodName, durationMs, err },
56
- `${serviceName}.${methodName} failed (${durationMs}ms)`,
57
- );
58
- throw err;
59
- }
60
-
61
- // If result is a promise, attach timing to its resolution
62
- if (result && typeof (result as Promise<unknown>).then === "function") {
63
- return (result as Promise<unknown>).then(
64
- (resolved) => {
65
- const durationMs = Math.round(performance.now() - start);
66
- if (durationMs > slowThresholdMs) {
67
- logger.info(
68
- { service: serviceName, method: methodName, durationMs },
69
- `${serviceName}.${methodName} slow (${durationMs}ms)`,
70
- );
71
- }
72
- return resolved;
73
- },
74
- (err) => {
75
- const durationMs = Math.round(performance.now() - start);
76
- logger.error(
77
- { service: serviceName, method: methodName, durationMs, err },
78
- `${serviceName}.${methodName} failed (${durationMs}ms)`,
79
- );
80
- throw err;
81
- },
82
- );
83
- }
84
-
85
- return result;
86
- };
87
- },
88
- });
89
- }
@@ -1,101 +0,0 @@
1
- import { CommerceInvalidTransitionError } from "../errors.js";
2
-
3
- export interface StateDefinition<TState extends string> {
4
- states: readonly TState[];
5
- initial: TState;
6
- transitions: Record<TState, readonly TState[]>;
7
- terminal: readonly TState[];
8
- }
9
-
10
- export type OrderState = string;
11
-
12
- const DEFAULT_TRANSITIONS: Record<string, readonly string[]> = {
13
- pending: ["confirmed", "cancelled"],
14
- confirmed: ["processing", "cancelled"],
15
- processing: ["partially_fulfilled", "fulfilled", "cancelled"],
16
- partially_fulfilled: ["fulfilled", "cancelled"],
17
- fulfilled: ["refunded"],
18
- cancelled: [],
19
- refunded: [],
20
- };
21
-
22
- const DEFAULT_STATES: readonly string[] = [
23
- "pending", "confirmed", "processing", "partially_fulfilled",
24
- "fulfilled", "cancelled", "refunded",
25
- ];
26
-
27
- const DEFAULT_TERMINAL: readonly string[] = ["cancelled", "refunded"];
28
-
29
- export const orderStateMachine: StateDefinition<string> = {
30
- states: DEFAULT_STATES,
31
- initial: "pending",
32
- transitions: DEFAULT_TRANSITIONS,
33
- terminal: DEFAULT_TERMINAL,
34
- };
35
-
36
- /**
37
- * Extend the order state machine with custom transitions.
38
- * New states are added automatically. Existing state transition arrays
39
- * are merged (union, not replaced) with the custom ones.
40
- *
41
- * Usage:
42
- * const extended = extendOrderStateMachine({
43
- * pending: ["payment_initiated"],
44
- * payment_initiated: ["payment_authorized", "payment_failed"],
45
- * payment_authorized: ["processing"],
46
- * });
47
- */
48
- export function extendOrderStateMachine(
49
- customTransitions: Record<string, string[]>,
50
- ): StateDefinition<string> {
51
- const merged: Record<string, string[]> = {};
52
-
53
- // Copy defaults
54
- for (const [state, targets] of Object.entries(DEFAULT_TRANSITIONS)) {
55
- merged[state] = [...targets];
56
- }
57
-
58
- // Merge custom
59
- for (const [state, targets] of Object.entries(customTransitions)) {
60
- if (!merged[state]) merged[state] = [];
61
- for (const t of targets) {
62
- if (!merged[state].includes(t)) merged[state].push(t);
63
- }
64
- // Ensure target states also exist in the map
65
- for (const t of targets) {
66
- if (!merged[t]) merged[t] = [];
67
- }
68
- }
69
-
70
- const allStates = Object.keys(merged);
71
- const terminal = allStates.filter((s) => merged[s]!.length === 0);
72
-
73
- return {
74
- states: allStates,
75
- initial: "pending",
76
- transitions: merged,
77
- terminal,
78
- };
79
- }
80
-
81
- export function canTransition<TState extends string>(
82
- machine: StateDefinition<TState>,
83
- from: TState,
84
- to: TState,
85
- ): boolean {
86
- return machine.transitions[from].includes(to);
87
- }
88
-
89
- export function assertTransition<TState extends string>(
90
- machine: StateDefinition<TState>,
91
- from: TState,
92
- to: TState,
93
- ): void {
94
- if (!canTransition(machine, from, to)) {
95
- throw new CommerceInvalidTransitionError(
96
- `Cannot transition from "${from}" to "${to}". Allowed transitions from "${from}": [${machine.transitions[
97
- from
98
- ].join(", ")}]`,
99
- );
100
- }
101
- }
@@ -1,426 +0,0 @@
1
- import { sql, type SQL } from "drizzle-orm";
2
- import type { DrizzleDatabase } from "../../kernel/database/drizzle-db.js";
3
- import { CommerceValidationError } from "../../kernel/errors.js";
4
- import { Err, Ok, type Result } from "../../kernel/result.js";
5
- import type {
6
- AnalyticsAdapter,
7
- AnalyticsFilter,
8
- AnalyticsMeta,
9
- AnalyticsModel,
10
- AnalyticsModelDefinition,
11
- AnalyticsQueryParams,
12
- AnalyticsQueryResult,
13
- AnalyticsScope,
14
- AnalyticsTimeDimension,
15
- } from "./types.js";
16
-
17
- // ─── Date Range Parsing ──────────────────────────────────────────────────────
18
-
19
- function parseDateRange(range: AnalyticsTimeDimension["dateRange"]): [Date, Date] | null {
20
- if (Array.isArray(range)) {
21
- const start = new Date(range[0]!);
22
- const end = new Date(range[1]!);
23
- if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return null;
24
- return [start, end];
25
- }
26
- if (typeof range !== "string") return null;
27
-
28
- const now = new Date();
29
- const lower = range.toLowerCase();
30
-
31
- if (lower === "last month") {
32
- return [
33
- new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1)),
34
- new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 0, 23, 59, 59, 999)),
35
- ];
36
- }
37
- if (lower === "this month") {
38
- return [
39
- new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)),
40
- new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0, 23, 59, 59, 999)),
41
- ];
42
- }
43
-
44
- const qMatch = range.match(/^Q([1-4])\s+(\d{4})$/i);
45
- if (qMatch) {
46
- const quarter = Number(qMatch[1]);
47
- const year = Number(qMatch[2]);
48
- const monthStart = (quarter - 1) * 3;
49
- return [
50
- new Date(Date.UTC(year, monthStart, 1)),
51
- new Date(Date.UTC(year, monthStart + 3, 0, 23, 59, 59, 999)),
52
- ];
53
- }
54
-
55
- return null;
56
- }
57
-
58
- function pickCube(params: AnalyticsQueryParams): string {
59
- const first = [
60
- ...(params.measures ?? []),
61
- ...(params.dimensions ?? []),
62
- ...(params.timeDimensions ?? []).map((td) => td.dimension),
63
- ...(params.filters ?? []).map((f) => f.member),
64
- ][0];
65
- if (!first) return "Orders";
66
- return first.split(".")[0] ?? "Orders";
67
- }
68
-
69
- // ─── Granularity to TO_CHAR format ───────────────────────────────────────────
70
-
71
- const GRANULARITY_FORMAT: Record<string, string> = {
72
- day: "YYYY-MM-DD",
73
- week: "IYYY-\"W\"IW",
74
- month: "YYYY-MM",
75
- year: "YYYY",
76
- };
77
-
78
- // ─── SQL Compilation Helpers ─────────────────────────────────────────────────
79
-
80
- function compileMeasure(cube: AnalyticsModel, measureName: string): SQL {
81
- const shortName = measureName.split(".")[1]!;
82
- const def = cube.measures[shortName];
83
- if (!def) return sql`0`;
84
-
85
- switch (def.type) {
86
- case "count":
87
- if (def.filter) {
88
- return sql.raw(`COUNT(CASE WHEN ${def.filter} THEN 1 END)`);
89
- }
90
- return sql`COUNT(*)`;
91
- case "sum":
92
- return sql.raw(`COALESCE(SUM(${def.sql!}), 0)`);
93
- case "avg":
94
- return sql.raw(`COALESCE(ROUND(AVG(${def.sql!})), 0)`);
95
- case "min":
96
- return sql.raw(`MIN(${def.sql!})`);
97
- case "max":
98
- return sql.raw(`MAX(${def.sql!})`);
99
- case "countDistinct":
100
- return sql.raw(`COUNT(DISTINCT ${def.sql!})`);
101
- }
102
- }
103
-
104
- function compileTimeDimensionSelect(cube: AnalyticsModel, td: AnalyticsTimeDimension): SQL {
105
- const shortName = td.dimension.split(".")[1]!;
106
- const dimDef = cube.dimensions[shortName];
107
- if (!dimDef) return sql`NULL`;
108
-
109
- const gran = td.granularity ?? "day";
110
- // Validate granularity against whitelist to prevent SQL injection
111
- if (!(gran in GRANULARITY_FORMAT)) {
112
- return sql`NULL`;
113
- }
114
- const format = GRANULARITY_FORMAT[gran]!;
115
- return sql.raw(`TO_CHAR(DATE_TRUNC('${gran}', ${dimDef.sql}), '${format}')`);
116
- }
117
-
118
- function compileFilter(cube: AnalyticsModel, filter: AnalyticsFilter): SQL | null {
119
- const shortName = filter.member.split(".")[1]!;
120
- const dimDef = cube.dimensions[shortName];
121
- if (!dimDef) return null;
122
-
123
- const col = dimDef.sql;
124
- const values = filter.values ?? [];
125
- if (values.length === 0) return null;
126
-
127
- switch (filter.operator) {
128
- case "equals":
129
- return sql`${sql.raw(col)} = ${values[0]!}`;
130
- case "notEquals":
131
- return sql`${sql.raw(col)} != ${values[0]!}`;
132
- case "contains":
133
- return sql`${sql.raw(col)} ILIKE ${"%" + values[0]! + "%"}`;
134
- case "in":
135
- return sql`${sql.raw(col)} IN (${sql.join(values.map((v) => sql`${v}`), sql`, `)})`;
136
- case "notIn":
137
- return sql`${sql.raw(col)} NOT IN (${sql.join(values.map((v) => sql`${v}`), sql`, `)})`;
138
- case "gt":
139
- return sql`${sql.raw(col)} > ${Number(values[0]!)}`;
140
- case "gte":
141
- return sql`${sql.raw(col)} >= ${Number(values[0]!)}`;
142
- case "lt":
143
- return sql`${sql.raw(col)} < ${Number(values[0]!)}`;
144
- case "lte":
145
- return sql`${sql.raw(col)} <= ${Number(values[0]!)}`;
146
- case "beforeDate":
147
- return sql`${sql.raw(col)} <= ${values[0]!}::timestamptz`;
148
- case "afterDate":
149
- return sql`${sql.raw(col)} >= ${values[0]!}::timestamptz`;
150
- case "inDateRange":
151
- if (values.length >= 2) {
152
- return sql`${sql.raw(col)} BETWEEN ${values[0]!}::timestamptz AND ${values[1]!}::timestamptz`;
153
- }
154
- return null;
155
- }
156
- }
157
-
158
- // ─── DrizzleAnalyticsAdapter ─────────────────────────────────────────────────
159
-
160
- export class DrizzleAnalyticsAdapter implements AnalyticsAdapter {
161
- private models = new Map<string, AnalyticsModel>();
162
-
163
- constructor(private db: DrizzleDatabase) {}
164
-
165
- registerModel(model: AnalyticsModel): void {
166
- this.models.set(model.name, model);
167
- }
168
-
169
- async query(params: AnalyticsQueryParams, scope: AnalyticsScope): Promise<Result<AnalyticsQueryResult>> {
170
- if (!params.measures || params.measures.length === 0) {
171
- return Err(new CommerceValidationError("analytics.query requires at least one measure."));
172
- }
173
-
174
- const cubeName = pickCube(params);
175
-
176
- // Validate all members are from the same cube
177
- const allMembers = [
178
- ...params.measures,
179
- ...(params.dimensions ?? []),
180
- ...(params.timeDimensions ?? []).map((td) => td.dimension),
181
- ];
182
- const wrongCube = allMembers.find((m) => m.split(".")[0] !== cubeName);
183
- if (wrongCube) {
184
- return Err(new CommerceValidationError(
185
- `analytics.query currently requires measures from a single cube. Found "${wrongCube}" in "${cubeName}" query.`,
186
- ));
187
- }
188
-
189
- const cube = this.models.get(cubeName);
190
- if (!cube) {
191
- const available = [...this.models.keys()].join(", ");
192
- return Err(new CommerceValidationError(
193
- `Unknown analytics cube: "${cubeName}". Available cubes: ${available}`,
194
- ));
195
- }
196
-
197
- // Validate that requested measures exist in the cube
198
- for (const measure of params.measures) {
199
- const shortName = measure.split(".")[1];
200
- if (shortName && !cube.measures[shortName]) {
201
- const available = Object.keys(cube.measures).map((m) => `${cubeName}.${m}`).join(", ");
202
- return Err(new CommerceValidationError(
203
- `Unknown measure: "${measure}". Available measures for ${cubeName}: ${available}`,
204
- ));
205
- }
206
- }
207
-
208
- // Validate that requested dimensions exist in the cube
209
- for (const dim of params.dimensions ?? []) {
210
- const shortName = dim.split(".")[1];
211
- if (shortName && !cube.dimensions[shortName]) {
212
- const available = Object.keys(cube.dimensions).map((d) => `${cubeName}.${d}`).join(", ");
213
- return Err(new CommerceValidationError(
214
- `Unknown dimension: "${dim}". Available dimensions for ${cubeName}: ${available}`,
215
- ));
216
- }
217
- }
218
-
219
- try {
220
- const rows = await this.executeQuery(cube, params, scope);
221
- return Ok({
222
- query: params,
223
- rows,
224
- source: cubeName,
225
- });
226
- } catch (error) {
227
- return Err(new CommerceValidationError(
228
- `Analytics query failed: ${error instanceof Error ? error.message : String(error)}`,
229
- ));
230
- }
231
- }
232
-
233
- async getMeta(_scope: AnalyticsScope): Promise<Result<AnalyticsMeta>> {
234
- const models: AnalyticsModelDefinition[] = [];
235
-
236
- for (const cube of this.models.values()) {
237
- models.push({
238
- name: cube.name,
239
- source: "builtin",
240
- measures: Object.keys(cube.measures).map((m) => `${cube.name}.${m}`),
241
- dimensions: Object.keys(cube.dimensions).map((d) => `${cube.name}.${d}`),
242
- segments: cube.segments
243
- ? Object.keys(cube.segments).map((s) => `${cube.name}.${s}`)
244
- : [],
245
- });
246
- }
247
-
248
- return Ok({
249
- models,
250
- measures: models.flatMap((m) => m.measures),
251
- dimensions: models.flatMap((m) => m.dimensions),
252
- segments: models.flatMap((m) => m.segments ?? []),
253
- });
254
- }
255
-
256
- // ─── SQL Query Builder ───────────────────────────────────────────────────
257
-
258
- private async executeQuery(
259
- cube: AnalyticsModel,
260
- params: AnalyticsQueryParams,
261
- scope: AnalyticsScope,
262
- ): Promise<Record<string, unknown>[]> {
263
- const selectParts: SQL[] = [];
264
- const groupByParts: SQL[] = [];
265
- const whereParts: SQL[] = [];
266
-
267
- // ── Scope-based filtering (always applied) ───────────────────────
268
- //
269
- // Security model (hardened by default):
270
- // admin/staff → no filter (full access)
271
- // vendor/customer → MUST have a matching scopeRule, or blocked
272
- // public → always blocked
273
- //
274
- // Scope is REQUIRED — there is no unscoped code path.
275
- {
276
- if (scope.role === "public") {
277
- whereParts.push(sql.raw("1 = 0"));
278
- } else if (scope.role !== "admin" && scope.role !== "staff") {
279
- // Vendor or customer: look for a matching scope rule
280
- let scopeApplied = false;
281
-
282
- if (cube.scopeRules) {
283
- for (const rule of cube.scopeRules) {
284
- if (rule.role === scope.role) {
285
- // Use parameterized SQL for scope values to prevent injection.
286
- // The filter template uses :vendorId / :customerId placeholders.
287
- // We split the filter on placeholders and build a parameterized
288
- // sql`` template instead of using sql.raw() with string interpolation.
289
- let filterSql = rule.filter;
290
- if (scope.vendorId && filterSql.includes(":vendorId")) {
291
- // Replace placeholder with parameterized value
292
- const parts = filterSql.split(":vendorId");
293
- const fragments = parts.map((part, i) =>
294
- i < parts.length - 1
295
- ? sql`${sql.raw(part)}${scope.vendorId!}`
296
- : sql.raw(part),
297
- );
298
- whereParts.push(sql.join(fragments, sql``));
299
- scopeApplied = true;
300
- continue;
301
- }
302
- if (scope.customerId && filterSql.includes(":customerId")) {
303
- const parts = filterSql.split(":customerId");
304
- const fragments = parts.map((part, i) =>
305
- i < parts.length - 1
306
- ? sql`${sql.raw(part)}${scope.customerId!}`
307
- : sql.raw(part),
308
- );
309
- whereParts.push(sql.join(fragments, sql``));
310
- scopeApplied = true;
311
- continue;
312
- }
313
- // No placeholder found — use raw (trusted scope rule SQL)
314
- whereParts.push(sql.raw(filterSql));
315
- scopeApplied = true;
316
- }
317
- }
318
- }
319
-
320
- // Deny-by-default: if no scope rule matched, block access
321
- if (!scopeApplied) {
322
- whereParts.push(sql.raw("1 = 0"));
323
- }
324
- }
325
- // admin/staff: no filter applied — full access
326
- }
327
-
328
- // Dimensions → SELECT + GROUP BY
329
- for (const dim of params.dimensions ?? []) {
330
- const shortName = dim.split(".")[1]!;
331
- const dimDef = cube.dimensions[shortName];
332
- if (!dimDef) continue;
333
- selectParts.push(sql.raw(`${dimDef.sql} AS "${dim}"`));
334
- groupByParts.push(sql.raw(dimDef.sql));
335
- }
336
-
337
- // Time dimensions → SELECT + GROUP BY + WHERE (dateRange)
338
- for (const td of params.timeDimensions ?? []) {
339
- const selectExpr = compileTimeDimensionSelect(cube, td);
340
- selectParts.push(sql`${selectExpr} AS ${sql.raw(`"${td.dimension}"`)}`);
341
- groupByParts.push(selectExpr);
342
-
343
- // Date range filter
344
- if (td.dateRange) {
345
- const range = parseDateRange(td.dateRange);
346
- if (range) {
347
- const shortName = td.dimension.split(".")[1]!;
348
- const dimDef = cube.dimensions[shortName];
349
- if (dimDef) {
350
- whereParts.push(
351
- sql`${sql.raw(dimDef.sql)} >= ${range[0].toISOString()}::timestamptz AND ${sql.raw(dimDef.sql)} < ${range[1].toISOString()}::timestamptz`,
352
- );
353
- }
354
- }
355
- }
356
- }
357
-
358
- // Measures → SELECT
359
- for (const measure of params.measures) {
360
- selectParts.push(sql`${compileMeasure(cube, measure)} AS ${sql.raw(`"${measure}"`)}`);
361
- }
362
-
363
- // If no select parts (shouldn't happen with validation), bail
364
- if (selectParts.length === 0) {
365
- return [];
366
- }
367
-
368
- // FROM + JOINs
369
- let fromFragment = sql.raw(cube.table);
370
- for (const join of cube.joins ?? []) {
371
- const joinType = join.type === "inner" ? "INNER JOIN" : "LEFT JOIN";
372
- fromFragment = sql`${fromFragment} ${sql.raw(joinType)} ${sql.raw(join.table)} ON ${sql.raw(join.on)}`;
373
- }
374
-
375
- // Filters → WHERE
376
- for (const filter of params.filters ?? []) {
377
- const compiled = compileFilter(cube, filter);
378
- if (compiled) whereParts.push(compiled);
379
- }
380
-
381
- // ORDER BY — validate member names against the cube's registered measures/dimensions
382
- const validMeasures = new Set(Object.keys(cube.measures).map((m) => `${cube.name}.${m}`));
383
- const validDimensions = new Set(Object.keys(cube.dimensions).map((d) => `${cube.name}.${d}`));
384
- const orderParts: SQL[] = [];
385
- for (const [member, dir] of Object.entries(params.order ?? {})) {
386
- if (!validMeasures.has(member) && !validDimensions.has(member)) {
387
- continue; // skip unknown members to prevent SQL injection via sql.raw()
388
- }
389
- const normalizedDir = dir.toUpperCase() === "DESC" ? "DESC" : "ASC";
390
- orderParts.push(sql.raw(`"${member}" ${normalizedDir}`));
391
- }
392
-
393
- // LIMIT
394
- const limit = Math.max(1, Math.min(params.limit ?? 100, 1000));
395
-
396
- // Assemble the full query
397
- const selectClause = sql.join(selectParts, sql`, `);
398
- const whereClause = whereParts.length > 0
399
- ? sql`WHERE ${sql.join(whereParts, sql` AND `)}`
400
- : sql``;
401
- const groupByClause = groupByParts.length > 0
402
- ? sql`GROUP BY ${sql.join(groupByParts, sql`, `)}`
403
- : sql``;
404
- const orderClause = orderParts.length > 0
405
- ? sql`ORDER BY ${sql.join(orderParts, sql`, `)}`
406
- : sql``;
407
-
408
- const fullQuery = sql`SELECT ${selectClause} FROM ${fromFragment} ${whereClause} ${groupByClause} ${orderClause} LIMIT ${limit}`;
409
-
410
- const result = await this.db.execute(fullQuery);
411
-
412
- // Map results: convert bigint to number, preserve column aliases
413
- // db.execute() returns different shapes per driver — normalize to array of rows
414
- const rawRows: Record<string, unknown>[] = Array.isArray(result)
415
- ? result
416
- : (result as { rows?: Record<string, unknown>[] }).rows ?? [];
417
-
418
- return rawRows.map((row: Record<string, unknown>) => {
419
- const mapped: Record<string, unknown> = {};
420
- for (const [key, value] of Object.entries(row)) {
421
- mapped[key] = typeof value === "bigint" ? Number(value) : value;
422
- }
423
- return mapped;
424
- });
425
- }
426
- }
@@ -1,11 +0,0 @@
1
- /**
2
- * Analytics hooks — RFC-006
3
- *
4
- * The in-memory event recording hooks (recordOrderAnalyticsEvent,
5
- * recordInventoryAnalyticsEvent) have been removed. The source tables
6
- * (orders, inventory_levels, customers) ARE the source of truth.
7
- * The DrizzleAnalyticsAdapter queries them directly via SQL.
8
- *
9
- * These empty exports preserve backwards compatibility for any code
10
- * that imports from this module.
11
- */