@unifiedcommerce/core 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/setup.d.ts.map +1 -1
- package/dist/auth/setup.js +8 -3
- package/dist/config/types.d.ts +3 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/interfaces/mcp/server.d.ts +3 -5
- package/dist/interfaces/mcp/server.d.ts.map +1 -1
- package/dist/interfaces/mcp/server.js +25 -510
- package/dist/interfaces/mcp/tool-builder.d.ts +120 -0
- package/dist/interfaces/mcp/tool-builder.d.ts.map +1 -0
- package/dist/interfaces/mcp/tool-builder.js +224 -0
- package/dist/interfaces/mcp/tools/analytics.d.ts +42 -0
- package/dist/interfaces/mcp/tools/analytics.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/analytics.js +70 -0
- package/dist/interfaces/mcp/tools/cart.d.ts +14 -0
- package/dist/interfaces/mcp/tools/cart.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/cart.js +47 -0
- package/dist/interfaces/mcp/tools/catalog.d.ts +53 -0
- package/dist/interfaces/mcp/tools/catalog.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/catalog.js +284 -0
- package/dist/interfaces/mcp/tools/index.d.ts +3 -0
- package/dist/interfaces/mcp/tools/index.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/index.js +20 -0
- package/dist/interfaces/mcp/tools/inventory.d.ts +27 -0
- package/dist/interfaces/mcp/tools/inventory.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/inventory.js +143 -0
- package/dist/interfaces/mcp/tools/orders.d.ts +18 -0
- package/dist/interfaces/mcp/tools/orders.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/orders.js +82 -0
- package/dist/interfaces/mcp/tools/pricing.d.ts +29 -0
- package/dist/interfaces/mcp/tools/pricing.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/pricing.js +90 -0
- package/dist/interfaces/mcp/tools/promotions.d.ts +44 -0
- package/dist/interfaces/mcp/tools/promotions.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/promotions.js +109 -0
- package/dist/interfaces/mcp/tools/registry.d.ts +32 -0
- package/dist/interfaces/mcp/tools/registry.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/registry.js +55 -0
- package/dist/interfaces/mcp/tools/search.d.ts +14 -0
- package/dist/interfaces/mcp/tools/search.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/search.js +39 -0
- package/dist/interfaces/mcp/tools/webhooks.d.ts +15 -0
- package/dist/interfaces/mcp/tools/webhooks.d.ts.map +1 -0
- package/dist/interfaces/mcp/tools/webhooks.js +48 -0
- package/dist/interfaces/mcp/transport.d.ts +17 -2
- package/dist/interfaces/mcp/transport.d.ts.map +1 -1
- package/dist/interfaces/mcp/transport.js +91 -44
- package/dist/interfaces/rest/router.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/checkout.js +1 -1
- package/dist/interfaces/rest/routes/promotions.d.ts.map +1 -1
- package/dist/interfaces/rest/routes/promotions.js +3 -2
- package/dist/kernel/database/adapter.d.ts +8 -0
- package/dist/kernel/database/adapter.d.ts.map +1 -1
- package/dist/kernel/factory/repository-factory.d.ts.map +1 -1
- package/dist/kernel/factory/repository-factory.js +3 -1
- package/dist/kernel/local-api.d.ts.map +1 -1
- package/dist/kernel/local-api.js +2 -0
- package/dist/kernel/plugin/manifest.d.ts +3 -3
- package/dist/kernel/plugin/manifest.d.ts.map +1 -1
- package/dist/kernel/plugin/manifest.js +36 -7
- package/dist/runtime/kernel.d.ts +1 -2
- package/dist/runtime/kernel.d.ts.map +1 -1
- package/dist/runtime/kernel.js +16 -8
- package/dist/runtime/server.d.ts.map +1 -1
- package/dist/runtime/server.js +8 -3
- package/dist/test-utils/create-pglite-adapter.d.ts.map +1 -1
- package/dist/test-utils/create-pglite-adapter.js +7 -6
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +3 -1
- package/src/adapters/console-email.ts +43 -0
- package/src/auth/access.ts +187 -0
- package/src/auth/auth-schema.ts +139 -0
- package/src/auth/middleware.ts +161 -0
- package/src/auth/org.ts +41 -0
- package/src/auth/permissions.ts +28 -0
- package/src/auth/setup.ts +171 -0
- package/src/auth/system-actor.ts +19 -0
- package/src/auth/types.ts +10 -0
- package/src/config/defaults.ts +82 -0
- package/src/config/define-config.ts +53 -0
- package/src/config/types.ts +301 -0
- package/src/generated/plugin-capabilities.d.ts +20 -0
- package/src/generated/plugin-manifest.ts +23 -0
- package/src/generated/plugin-repositories.d.ts +20 -0
- package/src/hooks/checkout-completion.ts +262 -0
- package/src/hooks/checkout.ts +677 -0
- package/src/hooks/order-emails.ts +62 -0
- package/src/index.ts +215 -0
- package/src/interfaces/mcp/agent-prompt.ts +174 -0
- package/src/interfaces/mcp/context-enrichment.ts +177 -0
- package/src/interfaces/mcp/server.ts +47 -0
- package/src/interfaces/mcp/tool-builder.ts +261 -0
- package/src/interfaces/mcp/tools/analytics.ts +76 -0
- package/src/interfaces/mcp/tools/cart.ts +57 -0
- package/src/interfaces/mcp/tools/catalog.ts +299 -0
- package/src/interfaces/mcp/tools/index.ts +22 -0
- package/src/interfaces/mcp/tools/inventory.ts +161 -0
- package/src/interfaces/mcp/tools/orders.ts +104 -0
- package/src/interfaces/mcp/tools/pricing.ts +94 -0
- package/src/interfaces/mcp/tools/promotions.ts +106 -0
- package/src/interfaces/mcp/tools/registry.ts +101 -0
- package/src/interfaces/mcp/tools/search.ts +42 -0
- package/src/interfaces/mcp/tools/webhooks.ts +48 -0
- package/src/interfaces/mcp/transport.ts +128 -0
- package/src/interfaces/rest/customer-portal.ts +299 -0
- package/src/interfaces/rest/index.ts +74 -0
- package/src/interfaces/rest/router.ts +333 -0
- package/src/interfaces/rest/routes/admin-jobs.ts +58 -0
- package/src/interfaces/rest/routes/audit.ts +50 -0
- package/src/interfaces/rest/routes/carts.ts +89 -0
- package/src/interfaces/rest/routes/catalog.ts +493 -0
- package/src/interfaces/rest/routes/checkout.ts +284 -0
- package/src/interfaces/rest/routes/inventory.ts +70 -0
- package/src/interfaces/rest/routes/media.ts +86 -0
- package/src/interfaces/rest/routes/orders.ts +78 -0
- package/src/interfaces/rest/routes/payments.ts +60 -0
- package/src/interfaces/rest/routes/pricing.ts +57 -0
- package/src/interfaces/rest/routes/promotions.ts +93 -0
- package/src/interfaces/rest/routes/search.ts +71 -0
- package/src/interfaces/rest/routes/webhooks.ts +46 -0
- package/src/interfaces/rest/schemas/admin-jobs.ts +40 -0
- package/src/interfaces/rest/schemas/audit.ts +46 -0
- package/src/interfaces/rest/schemas/carts.ts +125 -0
- package/src/interfaces/rest/schemas/catalog.ts +450 -0
- package/src/interfaces/rest/schemas/checkout.ts +66 -0
- package/src/interfaces/rest/schemas/customer-portal.ts +195 -0
- package/src/interfaces/rest/schemas/inventory.ts +138 -0
- package/src/interfaces/rest/schemas/media.ts +75 -0
- package/src/interfaces/rest/schemas/orders.ts +104 -0
- package/src/interfaces/rest/schemas/pricing.ts +80 -0
- package/src/interfaces/rest/schemas/promotions.ts +110 -0
- package/src/interfaces/rest/schemas/responses.ts +85 -0
- package/src/interfaces/rest/schemas/search.ts +58 -0
- package/src/interfaces/rest/schemas/shared.ts +62 -0
- package/src/interfaces/rest/schemas/webhooks.ts +68 -0
- package/src/interfaces/rest/utils.ts +104 -0
- package/src/interfaces/rest/webhook-router.ts +50 -0
- package/src/kernel/compensation/executor.ts +61 -0
- package/src/kernel/compensation/types.ts +26 -0
- package/src/kernel/database/adapter.ts +21 -0
- package/src/kernel/database/drizzle-db.ts +56 -0
- package/src/kernel/database/migrate.ts +76 -0
- package/src/kernel/database/plugin-types.ts +34 -0
- package/src/kernel/database/schema.ts +49 -0
- package/src/kernel/database/scoped-db.ts +68 -0
- package/src/kernel/database/tx-context.ts +46 -0
- package/src/kernel/error-mapper.ts +15 -0
- package/src/kernel/errors.ts +89 -0
- package/src/kernel/factory/repository-factory.ts +244 -0
- package/src/kernel/hooks/create-context.ts +43 -0
- package/src/kernel/hooks/executor.ts +88 -0
- package/src/kernel/hooks/registry.ts +74 -0
- package/src/kernel/hooks/types.ts +52 -0
- package/src/kernel/http-error.ts +44 -0
- package/src/kernel/jobs/adapter.ts +36 -0
- package/src/kernel/jobs/drizzle-adapter.ts +58 -0
- package/src/kernel/jobs/runner.ts +153 -0
- package/src/kernel/jobs/schema.ts +46 -0
- package/src/kernel/jobs/types.ts +30 -0
- package/src/kernel/local-api.ts +187 -0
- package/src/kernel/plugin/manifest.ts +271 -0
- package/src/kernel/query/executor.ts +184 -0
- package/src/kernel/query/registry.ts +46 -0
- package/src/kernel/result.ts +33 -0
- package/src/kernel/schema/extra-columns.ts +37 -0
- package/src/kernel/service-registry.ts +76 -0
- package/src/kernel/service-timing.ts +89 -0
- package/src/kernel/state-machine/machine.ts +101 -0
- package/src/modules/analytics/drizzle-adapter.ts +426 -0
- package/src/modules/analytics/hooks.ts +11 -0
- package/src/modules/analytics/models.ts +125 -0
- package/src/modules/analytics/repository/index.ts +6 -0
- package/src/modules/analytics/service.ts +245 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/audit/hooks.ts +78 -0
- package/src/modules/audit/schema.ts +33 -0
- package/src/modules/audit/service.ts +151 -0
- package/src/modules/cart/access.ts +27 -0
- package/src/modules/cart/matcher.ts +26 -0
- package/src/modules/cart/repository/index.ts +234 -0
- package/src/modules/cart/schema.ts +42 -0
- package/src/modules/cart/schemas.ts +38 -0
- package/src/modules/cart/service.ts +541 -0
- package/src/modules/catalog/repository/index.ts +772 -0
- package/src/modules/catalog/schema.ts +203 -0
- package/src/modules/catalog/schemas.ts +104 -0
- package/src/modules/catalog/service.ts +1544 -0
- package/src/modules/customers/repository/index.ts +327 -0
- package/src/modules/customers/schema.ts +64 -0
- package/src/modules/customers/service.ts +171 -0
- package/src/modules/fulfillment/repository/index.ts +426 -0
- package/src/modules/fulfillment/schema.ts +101 -0
- package/src/modules/fulfillment/service.ts +555 -0
- package/src/modules/fulfillment/types.ts +59 -0
- package/src/modules/inventory/repository/index.ts +509 -0
- package/src/modules/inventory/schema.ts +94 -0
- package/src/modules/inventory/schemas.ts +38 -0
- package/src/modules/inventory/service.ts +490 -0
- package/src/modules/media/adapter.ts +17 -0
- package/src/modules/media/repository/index.ts +274 -0
- package/src/modules/media/schema.ts +41 -0
- package/src/modules/media/service.ts +151 -0
- package/src/modules/orders/repository/index.ts +287 -0
- package/src/modules/orders/schema.ts +66 -0
- package/src/modules/orders/service.ts +619 -0
- package/src/modules/orders/stale-order-cleanup.ts +76 -0
- package/src/modules/organization/service.ts +191 -0
- package/src/modules/payments/adapter.ts +47 -0
- package/src/modules/payments/repository/index.ts +6 -0
- package/src/modules/payments/service.ts +107 -0
- package/src/modules/pricing/repository/index.ts +291 -0
- package/src/modules/pricing/schema.ts +71 -0
- package/src/modules/pricing/schemas.ts +38 -0
- package/src/modules/pricing/service.ts +494 -0
- package/src/modules/promotions/repository/index.ts +325 -0
- package/src/modules/promotions/schema.ts +62 -0
- package/src/modules/promotions/schemas.ts +38 -0
- package/src/modules/promotions/service.ts +598 -0
- package/src/modules/search/adapter.ts +57 -0
- package/src/modules/search/hooks.ts +12 -0
- package/src/modules/search/repository/index.ts +6 -0
- package/src/modules/search/service.ts +315 -0
- package/src/modules/shipping/calculator.ts +188 -0
- package/src/modules/shipping/repository/index.ts +6 -0
- package/src/modules/shipping/service.ts +51 -0
- package/src/modules/tax/adapter.ts +60 -0
- package/src/modules/tax/repository/index.ts +6 -0
- package/src/modules/tax/service.ts +53 -0
- package/src/modules/webhooks/hook.ts +34 -0
- package/src/modules/webhooks/repository/index.ts +278 -0
- package/src/modules/webhooks/schema.ts +56 -0
- package/src/modules/webhooks/service.ts +117 -0
- package/src/modules/webhooks/signing.ts +6 -0
- package/src/modules/webhooks/ssrf-guard.ts +71 -0
- package/src/modules/webhooks/tasks.ts +52 -0
- package/src/modules/webhooks/worker.ts +134 -0
- package/src/runtime/commerce.ts +145 -0
- package/src/runtime/kernel.ts +426 -0
- package/src/runtime/logger.ts +36 -0
- package/src/runtime/server.ts +355 -0
- package/src/runtime/shutdown.ts +43 -0
- package/src/test-utils/create-pglite-adapter.ts +129 -0
- package/src/test-utils/create-plugin-test-app.ts +128 -0
- package/src/test-utils/create-repository-test-harness.ts +16 -0
- package/src/test-utils/create-test-config.ts +190 -0
- package/src/test-utils/create-test-kernel.ts +7 -0
- package/src/test-utils/create-test-plugin-context.ts +75 -0
- package/src/test-utils/rest-api-test-utils.ts +265 -0
- package/src/test-utils/test-actors.ts +62 -0
- package/src/test-utils/typed-hooks.ts +54 -0
- package/src/types/commerce-types.ts +34 -0
- package/src/utils/id.ts +3 -0
- package/src/utils/logger.ts +18 -0
- package/src/utils/pagination.ts +22 -0
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
];
|
|
@@ -0,0 +1,245 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
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;
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
);
|