@unifiedcommerce/core 0.1.0 → 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 (174) hide show
  1. package/package.json +1 -2
  2. package/src/adapters/console-email.ts +0 -43
  3. package/src/auth/access.ts +0 -187
  4. package/src/auth/auth-schema.ts +0 -139
  5. package/src/auth/middleware.ts +0 -161
  6. package/src/auth/org.ts +0 -41
  7. package/src/auth/permissions.ts +0 -28
  8. package/src/auth/setup.ts +0 -169
  9. package/src/auth/system-actor.ts +0 -19
  10. package/src/auth/types.ts +0 -10
  11. package/src/config/defaults.ts +0 -82
  12. package/src/config/define-config.ts +0 -53
  13. package/src/config/types.ts +0 -299
  14. package/src/generated/plugin-capabilities.d.ts +0 -20
  15. package/src/generated/plugin-manifest.ts +0 -23
  16. package/src/generated/plugin-repositories.d.ts +0 -20
  17. package/src/hooks/checkout-completion.ts +0 -262
  18. package/src/hooks/checkout.ts +0 -677
  19. package/src/hooks/order-emails.ts +0 -62
  20. package/src/index.ts +0 -214
  21. package/src/interfaces/mcp/agent-prompt.ts +0 -174
  22. package/src/interfaces/mcp/context-enrichment.ts +0 -177
  23. package/src/interfaces/mcp/server.ts +0 -617
  24. package/src/interfaces/mcp/transport.ts +0 -68
  25. package/src/interfaces/rest/customer-portal.ts +0 -299
  26. package/src/interfaces/rest/index.ts +0 -74
  27. package/src/interfaces/rest/router.ts +0 -334
  28. package/src/interfaces/rest/routes/admin-jobs.ts +0 -58
  29. package/src/interfaces/rest/routes/audit.ts +0 -50
  30. package/src/interfaces/rest/routes/carts.ts +0 -89
  31. package/src/interfaces/rest/routes/catalog.ts +0 -493
  32. package/src/interfaces/rest/routes/checkout.ts +0 -283
  33. package/src/interfaces/rest/routes/inventory.ts +0 -70
  34. package/src/interfaces/rest/routes/media.ts +0 -86
  35. package/src/interfaces/rest/routes/orders.ts +0 -78
  36. package/src/interfaces/rest/routes/payments.ts +0 -60
  37. package/src/interfaces/rest/routes/pricing.ts +0 -57
  38. package/src/interfaces/rest/routes/promotions.ts +0 -92
  39. package/src/interfaces/rest/routes/search.ts +0 -71
  40. package/src/interfaces/rest/routes/webhooks.ts +0 -46
  41. package/src/interfaces/rest/schemas/admin-jobs.ts +0 -40
  42. package/src/interfaces/rest/schemas/audit.ts +0 -46
  43. package/src/interfaces/rest/schemas/carts.ts +0 -125
  44. package/src/interfaces/rest/schemas/catalog.ts +0 -450
  45. package/src/interfaces/rest/schemas/checkout.ts +0 -66
  46. package/src/interfaces/rest/schemas/customer-portal.ts +0 -195
  47. package/src/interfaces/rest/schemas/inventory.ts +0 -138
  48. package/src/interfaces/rest/schemas/media.ts +0 -75
  49. package/src/interfaces/rest/schemas/orders.ts +0 -104
  50. package/src/interfaces/rest/schemas/pricing.ts +0 -80
  51. package/src/interfaces/rest/schemas/promotions.ts +0 -110
  52. package/src/interfaces/rest/schemas/responses.ts +0 -85
  53. package/src/interfaces/rest/schemas/search.ts +0 -58
  54. package/src/interfaces/rest/schemas/shared.ts +0 -62
  55. package/src/interfaces/rest/schemas/webhooks.ts +0 -68
  56. package/src/interfaces/rest/utils.ts +0 -104
  57. package/src/interfaces/rest/webhook-router.ts +0 -50
  58. package/src/kernel/compensation/executor.ts +0 -61
  59. package/src/kernel/compensation/types.ts +0 -26
  60. package/src/kernel/database/adapter.ts +0 -13
  61. package/src/kernel/database/drizzle-db.ts +0 -56
  62. package/src/kernel/database/migrate.ts +0 -76
  63. package/src/kernel/database/plugin-types.ts +0 -34
  64. package/src/kernel/database/schema.ts +0 -49
  65. package/src/kernel/database/scoped-db.ts +0 -68
  66. package/src/kernel/database/tx-context.ts +0 -46
  67. package/src/kernel/error-mapper.ts +0 -15
  68. package/src/kernel/errors.ts +0 -89
  69. package/src/kernel/factory/repository-factory.ts +0 -242
  70. package/src/kernel/hooks/create-context.ts +0 -43
  71. package/src/kernel/hooks/executor.ts +0 -88
  72. package/src/kernel/hooks/registry.ts +0 -74
  73. package/src/kernel/hooks/types.ts +0 -52
  74. package/src/kernel/http-error.ts +0 -44
  75. package/src/kernel/jobs/adapter.ts +0 -36
  76. package/src/kernel/jobs/drizzle-adapter.ts +0 -58
  77. package/src/kernel/jobs/runner.ts +0 -153
  78. package/src/kernel/jobs/schema.ts +0 -46
  79. package/src/kernel/jobs/types.ts +0 -30
  80. package/src/kernel/local-api.ts +0 -185
  81. package/src/kernel/plugin/manifest.ts +0 -253
  82. package/src/kernel/query/executor.ts +0 -184
  83. package/src/kernel/query/registry.ts +0 -46
  84. package/src/kernel/result.ts +0 -33
  85. package/src/kernel/schema/extra-columns.ts +0 -37
  86. package/src/kernel/service-registry.ts +0 -76
  87. package/src/kernel/service-timing.ts +0 -89
  88. package/src/kernel/state-machine/machine.ts +0 -101
  89. package/src/modules/analytics/drizzle-adapter.ts +0 -426
  90. package/src/modules/analytics/hooks.ts +0 -11
  91. package/src/modules/analytics/models.ts +0 -125
  92. package/src/modules/analytics/repository/index.ts +0 -6
  93. package/src/modules/analytics/service.ts +0 -245
  94. package/src/modules/analytics/types.ts +0 -180
  95. package/src/modules/audit/hooks.ts +0 -78
  96. package/src/modules/audit/schema.ts +0 -33
  97. package/src/modules/audit/service.ts +0 -151
  98. package/src/modules/cart/access.ts +0 -27
  99. package/src/modules/cart/matcher.ts +0 -26
  100. package/src/modules/cart/repository/index.ts +0 -234
  101. package/src/modules/cart/schema.ts +0 -42
  102. package/src/modules/cart/schemas.ts +0 -38
  103. package/src/modules/cart/service.ts +0 -541
  104. package/src/modules/catalog/repository/index.ts +0 -772
  105. package/src/modules/catalog/schema.ts +0 -203
  106. package/src/modules/catalog/schemas.ts +0 -104
  107. package/src/modules/catalog/service.ts +0 -1544
  108. package/src/modules/customers/repository/index.ts +0 -327
  109. package/src/modules/customers/schema.ts +0 -64
  110. package/src/modules/customers/service.ts +0 -171
  111. package/src/modules/fulfillment/repository/index.ts +0 -426
  112. package/src/modules/fulfillment/schema.ts +0 -101
  113. package/src/modules/fulfillment/service.ts +0 -555
  114. package/src/modules/fulfillment/types.ts +0 -59
  115. package/src/modules/inventory/repository/index.ts +0 -509
  116. package/src/modules/inventory/schema.ts +0 -94
  117. package/src/modules/inventory/schemas.ts +0 -38
  118. package/src/modules/inventory/service.ts +0 -490
  119. package/src/modules/media/adapter.ts +0 -17
  120. package/src/modules/media/repository/index.ts +0 -274
  121. package/src/modules/media/schema.ts +0 -41
  122. package/src/modules/media/service.ts +0 -151
  123. package/src/modules/orders/repository/index.ts +0 -287
  124. package/src/modules/orders/schema.ts +0 -66
  125. package/src/modules/orders/service.ts +0 -619
  126. package/src/modules/orders/stale-order-cleanup.ts +0 -76
  127. package/src/modules/organization/service.ts +0 -191
  128. package/src/modules/payments/adapter.ts +0 -47
  129. package/src/modules/payments/repository/index.ts +0 -6
  130. package/src/modules/payments/service.ts +0 -107
  131. package/src/modules/pricing/repository/index.ts +0 -291
  132. package/src/modules/pricing/schema.ts +0 -71
  133. package/src/modules/pricing/schemas.ts +0 -38
  134. package/src/modules/pricing/service.ts +0 -494
  135. package/src/modules/promotions/repository/index.ts +0 -325
  136. package/src/modules/promotions/schema.ts +0 -62
  137. package/src/modules/promotions/schemas.ts +0 -38
  138. package/src/modules/promotions/service.ts +0 -598
  139. package/src/modules/search/adapter.ts +0 -57
  140. package/src/modules/search/hooks.ts +0 -12
  141. package/src/modules/search/repository/index.ts +0 -6
  142. package/src/modules/search/service.ts +0 -315
  143. package/src/modules/shipping/calculator.ts +0 -188
  144. package/src/modules/shipping/repository/index.ts +0 -6
  145. package/src/modules/shipping/service.ts +0 -51
  146. package/src/modules/tax/adapter.ts +0 -60
  147. package/src/modules/tax/repository/index.ts +0 -6
  148. package/src/modules/tax/service.ts +0 -53
  149. package/src/modules/webhooks/hook.ts +0 -34
  150. package/src/modules/webhooks/repository/index.ts +0 -278
  151. package/src/modules/webhooks/schema.ts +0 -56
  152. package/src/modules/webhooks/service.ts +0 -117
  153. package/src/modules/webhooks/signing.ts +0 -6
  154. package/src/modules/webhooks/ssrf-guard.ts +0 -71
  155. package/src/modules/webhooks/tasks.ts +0 -52
  156. package/src/modules/webhooks/worker.ts +0 -134
  157. package/src/runtime/commerce.ts +0 -145
  158. package/src/runtime/kernel.ts +0 -419
  159. package/src/runtime/logger.ts +0 -36
  160. package/src/runtime/server.ts +0 -349
  161. package/src/runtime/shutdown.ts +0 -43
  162. package/src/test-utils/create-pglite-adapter.ts +0 -129
  163. package/src/test-utils/create-plugin-test-app.ts +0 -128
  164. package/src/test-utils/create-repository-test-harness.ts +0 -16
  165. package/src/test-utils/create-test-config.ts +0 -190
  166. package/src/test-utils/create-test-kernel.ts +0 -7
  167. package/src/test-utils/create-test-plugin-context.ts +0 -75
  168. package/src/test-utils/rest-api-test-utils.ts +0 -265
  169. package/src/test-utils/test-actors.ts +0 -62
  170. package/src/test-utils/typed-hooks.ts +0 -54
  171. package/src/types/commerce-types.ts +0 -34
  172. package/src/utils/id.ts +0 -3
  173. package/src/utils/logger.ts +0 -18
  174. 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
- );