@unifiedcommerce/core 0.0.4 → 0.1.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 (179) hide show
  1. package/dist/auth/auth-schema.d.ts +92 -0
  2. package/dist/auth/auth-schema.d.ts.map +1 -1
  3. package/dist/auth/auth-schema.js +7 -0
  4. package/dist/auth/setup.d.ts.map +1 -1
  5. package/dist/auth/setup.js +3 -1
  6. package/package.json +1 -2
  7. package/src/adapters/console-email.ts +0 -43
  8. package/src/auth/access.ts +0 -187
  9. package/src/auth/auth-schema.ts +0 -131
  10. package/src/auth/middleware.ts +0 -161
  11. package/src/auth/org.ts +0 -41
  12. package/src/auth/permissions.ts +0 -28
  13. package/src/auth/setup.ts +0 -165
  14. package/src/auth/system-actor.ts +0 -19
  15. package/src/auth/types.ts +0 -10
  16. package/src/config/defaults.ts +0 -82
  17. package/src/config/define-config.ts +0 -53
  18. package/src/config/types.ts +0 -299
  19. package/src/generated/plugin-capabilities.d.ts +0 -20
  20. package/src/generated/plugin-manifest.ts +0 -23
  21. package/src/generated/plugin-repositories.d.ts +0 -20
  22. package/src/hooks/checkout-completion.ts +0 -262
  23. package/src/hooks/checkout.ts +0 -677
  24. package/src/hooks/order-emails.ts +0 -62
  25. package/src/index.ts +0 -214
  26. package/src/interfaces/mcp/agent-prompt.ts +0 -174
  27. package/src/interfaces/mcp/context-enrichment.ts +0 -177
  28. package/src/interfaces/mcp/server.ts +0 -617
  29. package/src/interfaces/mcp/transport.ts +0 -68
  30. package/src/interfaces/rest/customer-portal.ts +0 -299
  31. package/src/interfaces/rest/index.ts +0 -74
  32. package/src/interfaces/rest/router.ts +0 -334
  33. package/src/interfaces/rest/routes/admin-jobs.ts +0 -58
  34. package/src/interfaces/rest/routes/audit.ts +0 -50
  35. package/src/interfaces/rest/routes/carts.ts +0 -89
  36. package/src/interfaces/rest/routes/catalog.ts +0 -493
  37. package/src/interfaces/rest/routes/checkout.ts +0 -283
  38. package/src/interfaces/rest/routes/inventory.ts +0 -70
  39. package/src/interfaces/rest/routes/media.ts +0 -86
  40. package/src/interfaces/rest/routes/orders.ts +0 -78
  41. package/src/interfaces/rest/routes/payments.ts +0 -60
  42. package/src/interfaces/rest/routes/pricing.ts +0 -57
  43. package/src/interfaces/rest/routes/promotions.ts +0 -92
  44. package/src/interfaces/rest/routes/search.ts +0 -71
  45. package/src/interfaces/rest/routes/webhooks.ts +0 -46
  46. package/src/interfaces/rest/schemas/admin-jobs.ts +0 -40
  47. package/src/interfaces/rest/schemas/audit.ts +0 -46
  48. package/src/interfaces/rest/schemas/carts.ts +0 -125
  49. package/src/interfaces/rest/schemas/catalog.ts +0 -450
  50. package/src/interfaces/rest/schemas/checkout.ts +0 -66
  51. package/src/interfaces/rest/schemas/customer-portal.ts +0 -195
  52. package/src/interfaces/rest/schemas/inventory.ts +0 -138
  53. package/src/interfaces/rest/schemas/media.ts +0 -75
  54. package/src/interfaces/rest/schemas/orders.ts +0 -104
  55. package/src/interfaces/rest/schemas/pricing.ts +0 -80
  56. package/src/interfaces/rest/schemas/promotions.ts +0 -110
  57. package/src/interfaces/rest/schemas/responses.ts +0 -85
  58. package/src/interfaces/rest/schemas/search.ts +0 -58
  59. package/src/interfaces/rest/schemas/shared.ts +0 -62
  60. package/src/interfaces/rest/schemas/webhooks.ts +0 -68
  61. package/src/interfaces/rest/utils.ts +0 -104
  62. package/src/interfaces/rest/webhook-router.ts +0 -50
  63. package/src/kernel/compensation/executor.ts +0 -61
  64. package/src/kernel/compensation/types.ts +0 -26
  65. package/src/kernel/database/adapter.ts +0 -13
  66. package/src/kernel/database/drizzle-db.ts +0 -56
  67. package/src/kernel/database/migrate.ts +0 -76
  68. package/src/kernel/database/plugin-types.ts +0 -34
  69. package/src/kernel/database/schema.ts +0 -49
  70. package/src/kernel/database/scoped-db.ts +0 -68
  71. package/src/kernel/database/tx-context.ts +0 -46
  72. package/src/kernel/error-mapper.ts +0 -15
  73. package/src/kernel/errors.ts +0 -89
  74. package/src/kernel/factory/repository-factory.ts +0 -242
  75. package/src/kernel/hooks/create-context.ts +0 -43
  76. package/src/kernel/hooks/executor.ts +0 -88
  77. package/src/kernel/hooks/registry.ts +0 -74
  78. package/src/kernel/hooks/types.ts +0 -52
  79. package/src/kernel/http-error.ts +0 -44
  80. package/src/kernel/jobs/adapter.ts +0 -36
  81. package/src/kernel/jobs/drizzle-adapter.ts +0 -58
  82. package/src/kernel/jobs/runner.ts +0 -153
  83. package/src/kernel/jobs/schema.ts +0 -46
  84. package/src/kernel/jobs/types.ts +0 -30
  85. package/src/kernel/local-api.ts +0 -185
  86. package/src/kernel/plugin/manifest.ts +0 -253
  87. package/src/kernel/query/executor.ts +0 -184
  88. package/src/kernel/query/registry.ts +0 -46
  89. package/src/kernel/result.ts +0 -33
  90. package/src/kernel/schema/extra-columns.ts +0 -37
  91. package/src/kernel/service-registry.ts +0 -76
  92. package/src/kernel/service-timing.ts +0 -89
  93. package/src/kernel/state-machine/machine.ts +0 -101
  94. package/src/modules/analytics/drizzle-adapter.ts +0 -426
  95. package/src/modules/analytics/hooks.ts +0 -11
  96. package/src/modules/analytics/models.ts +0 -125
  97. package/src/modules/analytics/repository/index.ts +0 -6
  98. package/src/modules/analytics/service.ts +0 -245
  99. package/src/modules/analytics/types.ts +0 -180
  100. package/src/modules/audit/hooks.ts +0 -78
  101. package/src/modules/audit/schema.ts +0 -33
  102. package/src/modules/audit/service.ts +0 -151
  103. package/src/modules/cart/access.ts +0 -27
  104. package/src/modules/cart/matcher.ts +0 -26
  105. package/src/modules/cart/repository/index.ts +0 -234
  106. package/src/modules/cart/schema.ts +0 -42
  107. package/src/modules/cart/schemas.ts +0 -38
  108. package/src/modules/cart/service.ts +0 -541
  109. package/src/modules/catalog/repository/index.ts +0 -772
  110. package/src/modules/catalog/schema.ts +0 -203
  111. package/src/modules/catalog/schemas.ts +0 -104
  112. package/src/modules/catalog/service.ts +0 -1544
  113. package/src/modules/customers/repository/index.ts +0 -327
  114. package/src/modules/customers/schema.ts +0 -64
  115. package/src/modules/customers/service.ts +0 -171
  116. package/src/modules/fulfillment/repository/index.ts +0 -426
  117. package/src/modules/fulfillment/schema.ts +0 -101
  118. package/src/modules/fulfillment/service.ts +0 -555
  119. package/src/modules/fulfillment/types.ts +0 -59
  120. package/src/modules/inventory/repository/index.ts +0 -509
  121. package/src/modules/inventory/schema.ts +0 -94
  122. package/src/modules/inventory/schemas.ts +0 -38
  123. package/src/modules/inventory/service.ts +0 -490
  124. package/src/modules/media/adapter.ts +0 -17
  125. package/src/modules/media/repository/index.ts +0 -274
  126. package/src/modules/media/schema.ts +0 -41
  127. package/src/modules/media/service.ts +0 -151
  128. package/src/modules/orders/repository/index.ts +0 -287
  129. package/src/modules/orders/schema.ts +0 -66
  130. package/src/modules/orders/service.ts +0 -619
  131. package/src/modules/orders/stale-order-cleanup.ts +0 -76
  132. package/src/modules/organization/service.ts +0 -191
  133. package/src/modules/payments/adapter.ts +0 -47
  134. package/src/modules/payments/repository/index.ts +0 -6
  135. package/src/modules/payments/service.ts +0 -107
  136. package/src/modules/pricing/repository/index.ts +0 -291
  137. package/src/modules/pricing/schema.ts +0 -71
  138. package/src/modules/pricing/schemas.ts +0 -38
  139. package/src/modules/pricing/service.ts +0 -494
  140. package/src/modules/promotions/repository/index.ts +0 -325
  141. package/src/modules/promotions/schema.ts +0 -62
  142. package/src/modules/promotions/schemas.ts +0 -38
  143. package/src/modules/promotions/service.ts +0 -598
  144. package/src/modules/search/adapter.ts +0 -57
  145. package/src/modules/search/hooks.ts +0 -12
  146. package/src/modules/search/repository/index.ts +0 -6
  147. package/src/modules/search/service.ts +0 -315
  148. package/src/modules/shipping/calculator.ts +0 -188
  149. package/src/modules/shipping/repository/index.ts +0 -6
  150. package/src/modules/shipping/service.ts +0 -51
  151. package/src/modules/tax/adapter.ts +0 -60
  152. package/src/modules/tax/repository/index.ts +0 -6
  153. package/src/modules/tax/service.ts +0 -53
  154. package/src/modules/webhooks/hook.ts +0 -34
  155. package/src/modules/webhooks/repository/index.ts +0 -278
  156. package/src/modules/webhooks/schema.ts +0 -56
  157. package/src/modules/webhooks/service.ts +0 -117
  158. package/src/modules/webhooks/signing.ts +0 -6
  159. package/src/modules/webhooks/ssrf-guard.ts +0 -71
  160. package/src/modules/webhooks/tasks.ts +0 -52
  161. package/src/modules/webhooks/worker.ts +0 -134
  162. package/src/runtime/commerce.ts +0 -145
  163. package/src/runtime/kernel.ts +0 -419
  164. package/src/runtime/logger.ts +0 -36
  165. package/src/runtime/server.ts +0 -349
  166. package/src/runtime/shutdown.ts +0 -43
  167. package/src/test-utils/create-pglite-adapter.ts +0 -129
  168. package/src/test-utils/create-plugin-test-app.ts +0 -128
  169. package/src/test-utils/create-repository-test-harness.ts +0 -16
  170. package/src/test-utils/create-test-config.ts +0 -190
  171. package/src/test-utils/create-test-kernel.ts +0 -7
  172. package/src/test-utils/create-test-plugin-context.ts +0 -75
  173. package/src/test-utils/rest-api-test-utils.ts +0 -265
  174. package/src/test-utils/test-actors.ts +0 -62
  175. package/src/test-utils/typed-hooks.ts +0 -54
  176. package/src/types/commerce-types.ts +0 -34
  177. package/src/utils/id.ts +0 -3
  178. package/src/utils/logger.ts +0 -18
  179. package/src/utils/pagination.ts +0 -22
@@ -1,125 +0,0 @@
1
- import type { AnalyticsModel } from "./types.js";
2
-
3
- /**
4
- * Built-in analytics model definitions for the 4 core tables.
5
- *
6
- * Each model maps semantic names (e.g., "Orders.revenue") to PostgreSQL
7
- * columns and aggregations. The DrizzleAnalyticsAdapter compiles these
8
- * into parameterized SQL queries at runtime.
9
- */
10
-
11
- export const ORDERS_MODEL: AnalyticsModel = {
12
- name: "Orders",
13
- table: "orders",
14
- scopeRules: [
15
- { role: "vendor", filter: "id IN (SELECT order_id FROM marketplace_vendor_sub_orders WHERE vendor_id = :vendorId)" },
16
- { role: "customer", filter: "customer_id = :customerId" },
17
- ],
18
- measures: {
19
- count: { type: "count" },
20
- revenue: { sql: "grand_total", type: "sum" },
21
- averageOrderValue: { sql: "grand_total", type: "avg" },
22
- subtotalRevenue: { sql: "subtotal", type: "sum" },
23
- taxCollected: { sql: "tax_total", type: "sum" },
24
- shippingRevenue: { sql: "shipping_total", type: "sum" },
25
- discountsGiven: { sql: "discount_total", type: "sum" },
26
- uniqueCustomers: { sql: "customer_id", type: "countDistinct" },
27
- },
28
- dimensions: {
29
- id: { sql: "id", type: "string" },
30
- orderNumber: { sql: "order_number", type: "string" },
31
- status: { sql: "status", type: "string" },
32
- currency: { sql: "currency", type: "string" },
33
- placedAt: { sql: "placed_at", type: "time" },
34
- },
35
- };
36
-
37
- export const ORDER_LINE_ITEMS_MODEL: AnalyticsModel = {
38
- name: "OrderLineItems",
39
- table: "order_line_items",
40
- scopeRules: [
41
- { role: "vendor", filter: "order_id IN (SELECT order_id FROM marketplace_vendor_sub_orders WHERE vendor_id = :vendorId)" },
42
- { role: "customer", filter: "order_id IN (SELECT id FROM orders WHERE customer_id = :customerId)" },
43
- ],
44
- joins: [
45
- {
46
- table: "orders",
47
- type: "left",
48
- on: "order_line_items.order_id = orders.id",
49
- },
50
- ],
51
- measures: {
52
- count: { type: "count" },
53
- itemsSold: { sql: "order_line_items.quantity", type: "sum" },
54
- lineItemRevenue: { sql: "order_line_items.total_price", type: "sum" },
55
- averageUnitPrice: { sql: "order_line_items.unit_price", type: "avg" },
56
- },
57
- dimensions: {
58
- id: { sql: "order_line_items.id", type: "string" },
59
- entityType: { sql: "order_line_items.entity_type", type: "string" },
60
- sku: { sql: "order_line_items.sku", type: "string" },
61
- title: { sql: "order_line_items.title", type: "string" },
62
- fulfillmentStatus: { sql: "order_line_items.fulfillment_status", type: "string" },
63
- },
64
- };
65
-
66
- export const INVENTORY_MODEL: AnalyticsModel = {
67
- name: "Inventory",
68
- table: "inventory_levels",
69
- scopeRules: [
70
- { role: "vendor", filter: "entity_id IN (SELECT entity_id FROM marketplace_vendor_entities WHERE vendor_id = :vendorId)" },
71
- ],
72
- measures: {
73
- totalOnHand: { sql: "quantity_on_hand", type: "sum" },
74
- totalReserved: { sql: "quantity_reserved", type: "sum" },
75
- totalAvailable: { sql: "(quantity_on_hand - quantity_reserved)", type: "sum" },
76
- inventoryValue: { sql: "(quantity_on_hand * COALESCE(unit_cost, 0))", type: "sum" },
77
- lowStockCount: {
78
- type: "count",
79
- filter: "reorder_threshold IS NOT NULL AND (quantity_on_hand - quantity_reserved) <= reorder_threshold",
80
- },
81
- },
82
- dimensions: {
83
- entityId: { sql: "entity_id", type: "string" },
84
- warehouseId: { sql: "warehouse_id", type: "string" },
85
- lastRestockedAt: { sql: "last_restocked_at", type: "time" },
86
- },
87
- segments: {
88
- lowStock: {
89
- sql: "reorder_threshold IS NOT NULL AND (quantity_on_hand - quantity_reserved) <= reorder_threshold",
90
- },
91
- },
92
- };
93
-
94
- export const CUSTOMERS_MODEL: AnalyticsModel = {
95
- name: "Customers",
96
- table: "customers",
97
- scopeRules: [
98
- { role: "vendor", filter: "id IN (SELECT DISTINCT customer_id FROM orders WHERE id IN (SELECT order_id FROM marketplace_vendor_sub_orders WHERE vendor_id = :vendorId) AND customer_id IS NOT NULL)" },
99
- { role: "customer", filter: "id = :customerId" },
100
- ],
101
- measures: {
102
- customerCount: { type: "count" },
103
- newCustomers: { type: "count" },
104
- returningCustomers: {
105
- type: "count",
106
- filter: "(SELECT COUNT(*) FROM orders WHERE orders.customer_id = customers.id) > 1",
107
- },
108
- },
109
- dimensions: {
110
- createdAt: { sql: "created_at", type: "time" },
111
- customerGroup: { sql: "COALESCE(metadata->>'customerGroup', 'default')", type: "string" },
112
- },
113
- segments: {
114
- returning: {
115
- sql: "(SELECT COUNT(*) FROM orders WHERE orders.customer_id = customers.id) > 1",
116
- },
117
- },
118
- };
119
-
120
- export const BUILTIN_ANALYTICS_MODELS: AnalyticsModel[] = [
121
- ORDERS_MODEL,
122
- ORDER_LINE_ITEMS_MODEL,
123
- INVENTORY_MODEL,
124
- CUSTOMERS_MODEL,
125
- ];
@@ -1,6 +0,0 @@
1
- import type { TxContext } from "../../../kernel/database/tx-context.js";
2
-
3
- export interface AnalyticsRepository {
4
- // RFC-002 scaffold: module repositories become the only persistence boundary.
5
- ping(ctx: TxContext): Promise<void>;
6
- }
@@ -1,245 +0,0 @@
1
- /**
2
- * AnalyticsService — thin delegation layer over AnalyticsAdapter.
3
- *
4
- * The service manages plugin model registration, custom schema loading,
5
- * and delegates all query execution to the configured adapter
6
- * (DrizzleAnalyticsAdapter — always on, built into core).
7
- */
8
-
9
- import type { CommerceConfig } from "../../config/types.js";
10
- import { CommerceValidationError } from "../../kernel/errors.js";
11
- import { Err, Ok, type Result } from "../../kernel/result.js";
12
- import type {
13
- AnalyticsAdapter,
14
- AnalyticsMeta,
15
- AnalyticsModel,
16
- AnalyticsModelDefinition,
17
- AnalyticsQueryParams,
18
- AnalyticsQueryResult,
19
- AnalyticsScope,
20
- } from "./types.js";
21
-
22
- // Re-export types for backwards compatibility
23
- export type {
24
- AnalyticsTimeDimension,
25
- AnalyticsFilter,
26
- AnalyticsQueryParams,
27
- AnalyticsModelDefinition,
28
- AnalyticsMeta,
29
- } from "./types.js";
30
-
31
- export interface AnalyticsServiceDeps {
32
- adapter: AnalyticsAdapter;
33
- config: CommerceConfig;
34
- }
35
-
36
- export class AnalyticsService {
37
- private pluginModels: AnalyticsModelDefinition[] = [];
38
- private customSchemaModels: AnalyticsModelDefinition[] = [];
39
- private customModelsLoaded = false;
40
-
41
- constructor(private deps: AnalyticsServiceDeps) {}
42
-
43
- /**
44
- * Register a plugin-contributed analytics model.
45
- *
46
- * If the model includes a `table` field and structured measures/dimensions,
47
- * it is also registered as an AnalyticsModel on the adapter for SQL queries.
48
- * Otherwise, it appears in getMeta() but queries return zero-value rows.
49
- */
50
- registerModel(model: unknown): void {
51
- if (!model || typeof model !== "object") return;
52
-
53
- const raw = model as Record<string, unknown>;
54
- const name =
55
- typeof raw.name === "string"
56
- ? raw.name
57
- : `PluginModel_${this.pluginModels.length + 1}`;
58
- const measures = Array.isArray(raw.measures)
59
- ? raw.measures.filter((v): v is string => typeof v === "string")
60
- : [];
61
- const dimensions = Array.isArray(raw.dimensions)
62
- ? raw.dimensions.filter((v): v is string => typeof v === "string")
63
- : [];
64
- const segments = Array.isArray(raw.segments)
65
- ? raw.segments.filter((v): v is string => typeof v === "string")
66
- : [];
67
-
68
- this.pluginModels.push({
69
- name,
70
- source: "plugin",
71
- measures,
72
- dimensions,
73
- segments,
74
- raw: model,
75
- });
76
-
77
- // If the model provides a table + structured definitions, register on the adapter
78
- if (typeof raw.table === "string" && Array.isArray(raw.measures)) {
79
- this.tryRegisterModel(name, raw);
80
- }
81
- }
82
-
83
- /**
84
- * Query analytics with scope-based filtering.
85
- * Scope is REQUIRED — use buildAnalyticsScope(actor) to construct it.
86
- */
87
- async query(params: AnalyticsQueryParams, scope: AnalyticsScope): Promise<Result<AnalyticsQueryResult>> {
88
- return this.deps.adapter.query(params, scope);
89
- }
90
-
91
- async getDashboard(name: string, scope: AnalyticsScope): Promise<Result<AnalyticsQueryResult>> {
92
- const normalized = name.trim().toLowerCase();
93
-
94
- if (normalized === "revenue" || normalized === "revenue-overview") {
95
- return this.query({
96
- measures: ["Orders.revenue", "Orders.count"],
97
- timeDimensions: [{
98
- dimension: "Orders.placedAt",
99
- granularity: "month",
100
- dateRange: "this month",
101
- }],
102
- order: { "Orders.placedAt": "asc" },
103
- }, scope);
104
- }
105
-
106
- if (normalized === "inventory" || normalized === "inventory-health") {
107
- return this.query({
108
- measures: ["Inventory.totalAvailable", "Inventory.lowStockCount"],
109
- dimensions: ["Inventory.warehouseId"],
110
- order: { "Inventory.totalAvailable": "desc" },
111
- }, scope);
112
- }
113
-
114
- return Err(
115
- new CommerceValidationError(`Unknown analytics dashboard: ${name}`),
116
- );
117
- }
118
-
119
- async getMeta(): Promise<Result<AnalyticsMeta>> {
120
- await this.ensureCustomSchemaModelsLoaded();
121
-
122
- // Meta returns model definitions, not data — always use admin scope
123
- const adapterMeta = await this.deps.adapter.getMeta({ role: "admin" });
124
- if (!adapterMeta.ok) return adapterMeta;
125
-
126
- // Merge with plugin and custom schema models
127
- const allModels = [
128
- ...adapterMeta.value.models,
129
- ...this.pluginModels,
130
- ...this.customSchemaModels,
131
- ];
132
-
133
- return Ok({
134
- models: allModels,
135
- measures: allModels.flatMap((m) => m.measures),
136
- dimensions: allModels.flatMap((m) => m.dimensions),
137
- segments: allModels.flatMap((m) => m.segments ?? []),
138
- });
139
- }
140
-
141
- async meta(): Promise<Result<{ measures: string[]; dimensions: string[]; segments: string[] }>> {
142
- const meta = await this.getMeta();
143
- if (!meta.ok) return meta;
144
- return Ok({
145
- measures: meta.value.measures,
146
- dimensions: meta.value.dimensions,
147
- segments: meta.value.segments,
148
- });
149
- }
150
-
151
- // ─── Private ─────────────────────────────────────────────────────────────
152
-
153
- private tryRegisterModel(name: string, raw: Record<string, unknown>): void {
154
- try {
155
- const table = raw.table as string;
156
- const measuresArray = raw.measures as Array<Record<string, unknown>>;
157
- const dimensionsArray = (raw.dimensions ?? []) as Array<Record<string, unknown>>;
158
-
159
- const modelDef: AnalyticsModel = {
160
- name,
161
- table,
162
- measures: {},
163
- dimensions: {},
164
- };
165
-
166
- for (const m of measuresArray) {
167
- if (typeof m === "string") continue;
168
- if (typeof m.name === "string" && typeof m.type === "string") {
169
- modelDef.measures[m.name] = {
170
- type: m.type as AnalyticsModel["measures"][string]["type"],
171
- sql: typeof m.sql === "string" ? m.sql : undefined,
172
- filter: typeof m.filter === "string" ? m.filter : undefined,
173
- };
174
- }
175
- }
176
-
177
- for (const d of dimensionsArray) {
178
- if (typeof d === "string") continue;
179
- if (typeof d.name === "string" && typeof d.sql === "string") {
180
- modelDef.dimensions[d.name] = {
181
- sql: d.sql,
182
- type: (typeof d.type === "string" ? d.type : "string") as AnalyticsModel["dimensions"][string]["type"],
183
- };
184
- }
185
- }
186
-
187
- if (Object.keys(modelDef.measures).length > 0) {
188
- this.deps.adapter.registerModel(modelDef);
189
- }
190
- } catch {
191
- // Silently skip malformed plugin models
192
- }
193
- }
194
-
195
- private async ensureCustomSchemaModelsLoaded(): Promise<void> {
196
- if (this.customModelsLoaded) return;
197
- this.customModelsLoaded = true;
198
-
199
- const customSchemaPath = this.deps.config.analytics?.customSchemaPath;
200
- if (!customSchemaPath) return;
201
-
202
- try {
203
- const fs = await import("node:fs/promises");
204
- const path = await import("node:path");
205
- const entries = await fs.readdir(customSchemaPath);
206
-
207
- for (const entry of entries) {
208
- if (!entry.endsWith(".js")) continue;
209
- const filePath = path.join(customSchemaPath, entry);
210
- const content = await fs.readFile(filePath, "utf8");
211
-
212
- const nameMatch = content.match(/cube\s*\(\s*["'`]([\w-]+)["'`]/);
213
- const name = nameMatch?.[1] ?? entry.replace(/\.js$/, "");
214
-
215
- const measures = [
216
- ...content.matchAll(/measures\s*:\s*{([\s\S]*?)}\s*,/g),
217
- ].flatMap((match) => {
218
- const block = match[1] ?? "";
219
- return [...block.matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*:\s*{/g)].map(
220
- (x) => `${name}.${x[1]}`,
221
- );
222
- });
223
-
224
- const dimensions = [
225
- ...content.matchAll(/dimensions\s*:\s*{([\s\S]*?)}\s*,/g),
226
- ].flatMap((match) => {
227
- const block = match[1] ?? "";
228
- return [...block.matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*:\s*{/g)].map(
229
- (x) => `${name}.${x[1]}`,
230
- );
231
- });
232
-
233
- this.customSchemaModels.push({
234
- name,
235
- source: "custom-schema",
236
- measures,
237
- dimensions,
238
- raw: { path: filePath },
239
- });
240
- }
241
- } catch {
242
- // Optional extension path: ignore file-system issues
243
- }
244
- }
245
- }
@@ -1,180 +0,0 @@
1
- import type { Result } from "../../kernel/result.js";
2
-
3
- // ─── Query Params (unchanged from existing API) ─────────────────────────────
4
-
5
- export interface AnalyticsTimeDimension {
6
- dimension: string;
7
- granularity?: "day" | "week" | "month" | "year";
8
- dateRange?: [string, string] | string;
9
- }
10
-
11
- export interface AnalyticsFilter {
12
- member: string;
13
- operator:
14
- | "equals"
15
- | "notEquals"
16
- | "contains"
17
- | "in"
18
- | "notIn"
19
- | "gt"
20
- | "gte"
21
- | "lt"
22
- | "lte"
23
- | "beforeDate"
24
- | "afterDate"
25
- | "inDateRange";
26
- values?: string[];
27
- }
28
-
29
- export interface AnalyticsQueryParams {
30
- measures: string[];
31
- dimensions?: string[];
32
- timeDimensions?: AnalyticsTimeDimension[];
33
- filters?: AnalyticsFilter[];
34
- order?: Record<string, "asc" | "desc">;
35
- limit?: number;
36
- }
37
-
38
- // ─── Query Result ────────────────────────────────────────────────────────────
39
-
40
- export interface AnalyticsQueryResult {
41
- query: AnalyticsQueryParams;
42
- rows: Record<string, unknown>[];
43
- source: string;
44
- }
45
-
46
- // ─── Model / Meta ────────────────────────────────────────────────────────────
47
-
48
- export interface AnalyticsModelDefinition {
49
- name: string;
50
- measures: string[];
51
- dimensions: string[];
52
- segments?: string[];
53
- source: "builtin" | "plugin" | "custom-schema";
54
- raw?: unknown;
55
- }
56
-
57
- export interface AnalyticsMeta {
58
- models: AnalyticsModelDefinition[];
59
- measures: string[];
60
- dimensions: string[];
61
- segments: string[];
62
- }
63
-
64
- // ─── Analytics Model (declarative SQL mapping) ──────────────────────────────
65
-
66
- export type MeasureType = "count" | "sum" | "avg" | "min" | "max" | "countDistinct";
67
-
68
- export interface AnalyticsMeasure {
69
- type: MeasureType;
70
- /** SQL column or expression (e.g., "grand_total" or "quantity_on_hand * COALESCE(unit_cost, 0)") */
71
- sql?: string | undefined;
72
- /** SQL filter expression that must be true for this measure to count a row */
73
- filter?: string | undefined;
74
- }
75
-
76
- export type DimensionType = "string" | "number" | "time" | "boolean";
77
-
78
- export interface AnalyticsDimension {
79
- /** SQL column or expression */
80
- sql: string;
81
- type: DimensionType;
82
- }
83
-
84
- export interface AnalyticsJoin {
85
- table: string;
86
- type: "left" | "inner";
87
- on: string;
88
- }
89
-
90
- export interface AnalyticsModel {
91
- name: string;
92
- table: string;
93
- joins?: AnalyticsJoin[];
94
- measures: Record<string, AnalyticsMeasure>;
95
- dimensions: Record<string, AnalyticsDimension>;
96
- segments?: Record<string, { sql: string }>;
97
- /**
98
- * Scope rules define how this model is filtered by role.
99
- * The filter SQL uses :vendorId or :customerId as placeholders.
100
- *
101
- * Example: { role: "vendor", filter: "vendor_id = :vendorId" }
102
- */
103
- scopeRules?: AnalyticsScopeRule[];
104
- }
105
-
106
- // ─── Scope (Role-Based Query Filtering) ──────────────────────────────────────
107
-
108
- export interface AnalyticsScope {
109
- role: "admin" | "staff" | "vendor" | "customer" | "public";
110
- vendorId?: string | undefined;
111
- customerId?: string | undefined;
112
- }
113
-
114
- /**
115
- * Scope rule: defines how an analytics model is filtered for a given role.
116
- * Registered alongside model definitions.
117
- */
118
- export interface AnalyticsScopeRule {
119
- /** Which role this rule applies to */
120
- role: "vendor" | "customer";
121
- /** SQL WHERE clause fragment. Use :vendorId or :customerId as placeholders. */
122
- filter: string;
123
- }
124
-
125
- // ─── Scope Builder ───────────────────────────────────────────────────────────
126
-
127
- /**
128
- * Build an AnalyticsScope from an actor (or null for public).
129
- *
130
- * This is the ONLY way scopes should be created. Every call site that
131
- * invokes analytics.query() MUST use this function — never construct
132
- * a scope manually. This ensures the scope always reflects the
133
- * authenticated actor's actual role and identity.
134
- */
135
- export function buildAnalyticsScope(actor: {
136
- role?: string;
137
- vendorId?: string | null;
138
- userId?: string;
139
- } | null): AnalyticsScope {
140
- if (!actor) return { role: "public" };
141
-
142
- const role = actor.role ?? "public";
143
-
144
- if (role === "admin" || role === "owner" || role === "staff" || role === "ai_agent") {
145
- return { role: "admin" };
146
- }
147
-
148
- if (actor.vendorId) {
149
- return { role: "vendor", vendorId: actor.vendorId };
150
- }
151
-
152
- if (role === "customer" && actor.userId) {
153
- return { role: "customer", customerId: actor.userId };
154
- }
155
-
156
- // Unknown role — deny access
157
- return { role: "public" };
158
- }
159
-
160
- // ─── Adapter Interface ───────────────────────────────────────────────────────
161
-
162
- export interface AnalyticsAdapter {
163
- /** Scope is REQUIRED. Use buildAnalyticsScope(actor) to construct it. */
164
- query(params: AnalyticsQueryParams, scope: AnalyticsScope): Promise<Result<AnalyticsQueryResult>>;
165
- getMeta(scope: AnalyticsScope): Promise<Result<AnalyticsMeta>>;
166
- registerModel(model: AnalyticsModel): void;
167
- }
168
-
169
- // ─── Deprecated aliases (remove in next major version) ──────────────────────
170
-
171
- /** @deprecated Use AnalyticsModel instead */
172
- export type CubeDefinition = AnalyticsModel;
173
- /** @deprecated Use AnalyticsScopeRule instead */
174
- export type CubeScopeRule = AnalyticsScopeRule;
175
- /** @deprecated Use AnalyticsMeasure instead */
176
- export type MeasureDefinition = AnalyticsMeasure;
177
- /** @deprecated Use AnalyticsDimension instead */
178
- export type DimensionDefinition = AnalyticsDimension;
179
- /** @deprecated Use AnalyticsJoin instead */
180
- export type JoinDefinition = AnalyticsJoin;
@@ -1,78 +0,0 @@
1
- import type { AfterHook } from "../../kernel/hooks/types.js";
2
- import type { HookHandler } from "../../kernel/hooks/registry.js";
3
- import type { AuditService } from "./service.js";
4
-
5
- /**
6
- * Creates an after-hook that records an audit entry for the operation.
7
- *
8
- * The audit entry is written using the same transaction context as the
9
- * business operation (via ctx.tx). If the operation rolls back, the
10
- * audit entry rolls back too.
11
- */
12
- function createAuditAfterHook(entityType: string, event: string): AfterHook<Record<string, unknown>> {
13
- return async ({ result, context }) => {
14
- const audit = context.services.audit as AuditService | undefined;
15
- if (!audit?.record) return;
16
-
17
- const entityId = (result as { id?: string })?.id ?? "unknown";
18
-
19
- await audit.record({
20
- entityType,
21
- entityId,
22
- event,
23
- payload: safePayload(result),
24
- ctx: context,
25
- });
26
- };
27
- }
28
-
29
- /**
30
- * Strips the result to a safe subset for audit logging.
31
- * Avoids storing sensitive fields or excessively large payloads.
32
- */
33
- function safePayload(result: unknown): Record<string, unknown> {
34
- if (result == null || typeof result !== "object") return {};
35
- const obj = result as Record<string, unknown>;
36
-
37
- // Shallow copy, exclude fields that could be huge
38
- const safe: Record<string, unknown> = {};
39
- for (const [key, value] of Object.entries(obj)) {
40
- // Skip binary data, nested arrays > 10 items, and known sensitive fields
41
- if (key === "password" || key === "secret" || key === "bankAccount") continue;
42
- if (Array.isArray(value) && value.length > 10) {
43
- safe[key] = `[${value.length} items]`;
44
- continue;
45
- }
46
- safe[key] = value;
47
- }
48
- return safe;
49
- }
50
-
51
- /**
52
- * All audit hooks, keyed by hook registration key.
53
- * Registered in kernel boot via hooks.append().
54
- */
55
- export const auditHooks: Record<string, HookHandler> = {
56
- // Catalog
57
- "catalog.afterCreate": createAuditAfterHook("catalog_entity", "created") as HookHandler,
58
- "catalog.afterUpdate": createAuditAfterHook("catalog_entity", "updated") as HookHandler,
59
-
60
- // Orders
61
- "orders.afterCreate": createAuditAfterHook("order", "created") as HookHandler,
62
- "orders.afterStatusChange": createAuditAfterHook("order", "status_changed") as HookHandler,
63
-
64
- // Inventory
65
- "inventory.afterAdjust": createAuditAfterHook("inventory", "adjusted") as HookHandler,
66
-
67
- // Customers
68
- "customers.afterCreate": createAuditAfterHook("customer", "created") as HookHandler,
69
- "customers.afterUpdate": createAuditAfterHook("customer", "updated") as HookHandler,
70
-
71
- // Pricing
72
- "pricing.afterCreate": createAuditAfterHook("price", "created") as HookHandler,
73
- "pricing.afterUpdate": createAuditAfterHook("price", "updated") as HookHandler,
74
-
75
- // Promotions
76
- "promotions.afterCreate": createAuditAfterHook("promotion", "created") as HookHandler,
77
- "promotions.afterUpdate": createAuditAfterHook("promotion", "updated") as HookHandler,
78
- };
@@ -1,33 +0,0 @@
1
- import {
2
- pgTable,
3
- uuid,
4
- text,
5
- jsonb,
6
- timestamp,
7
- index,
8
- } from "drizzle-orm/pg-core";
9
- import { organization } from "../../auth/auth-schema.js";
10
-
11
- export const auditLog = pgTable(
12
- "commerce_audit_log",
13
- {
14
- id: uuid("id").primaryKey().defaultRandom(),
15
- organizationId: text("organization_id")
16
- .notNull()
17
- .references(() => organization.id, { onDelete: "cascade" }),
18
- entityType: text("entity_type").notNull(),
19
- entityId: text("entity_id").notNull(),
20
- event: text("event").notNull(),
21
- payload: jsonb("payload").notNull().default("{}"),
22
- actorId: text("actor_id"),
23
- actorType: text("actor_type"),
24
- requestId: text("request_id"),
25
- createdAt: timestamp("created_at", { withTimezone: true })
26
- .notNull()
27
- .defaultNow(),
28
- },
29
- (table) => ({
30
- entityIdx: index("idx_audit_entity").on(table.entityType, table.entityId),
31
- orgIdx: index("idx_audit_org").on(table.organizationId),
32
- }),
33
- );