@reactionary/source 0.3.0 → 0.3.2

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 (103) hide show
  1. package/core/src/client/client-builder.ts +3 -7
  2. package/core/src/client/client.ts +2 -3
  3. package/core/src/decorators/reactionary.decorator.ts +2 -2
  4. package/core/src/initialization.ts +11 -3
  5. package/core/src/providers/analytics.provider.ts +75 -0
  6. package/core/src/providers/cart.provider.ts +3 -0
  7. package/core/src/providers/category.provider.ts +1 -0
  8. package/core/src/providers/identity.provider.ts +5 -0
  9. package/core/src/schemas/errors/invalid-input.error.ts +1 -1
  10. package/core/src/schemas/errors/invalid-output.error.ts +1 -1
  11. package/core/src/schemas/models/identifiers.model.ts +3 -0
  12. package/core/src/schemas/models/order.model.ts +2 -2
  13. package/core/src/schemas/mutations/analytics/index.ts +23 -0
  14. package/core/src/schemas/mutations/analytics/product-add-to-cart.mutation.ts +25 -0
  15. package/core/src/schemas/mutations/analytics/product-details-view.mutation.ts +14 -0
  16. package/core/src/schemas/mutations/analytics/product-summary-click.mutation.ts +26 -0
  17. package/core/src/schemas/mutations/analytics/product-summary-view.mutation.ts +25 -0
  18. package/core/src/schemas/mutations/analytics/purchase.mutation.ts +14 -0
  19. package/core/src/schemas/mutations/index.ts +1 -1
  20. package/core/src/schemas/queries/order-search.query.ts +3 -0
  21. package/core/src/schemas/session.schema.ts +21 -9
  22. package/core/src/test/client-builder.spec.ts +60 -0
  23. package/core/src/zod-utils.ts +3 -1
  24. package/documentation/{1-purpose.md → docs/1-purpose.md} +4 -0
  25. package/documentation/docs/8-tracking.md +9 -0
  26. package/documentation/docs/providers/analytics.provider.md +297 -0
  27. package/documentation/docs/providers/base.provider.md +118 -0
  28. package/documentation/docs/providers/cart.provider.md +305 -0
  29. package/documentation/docs/providers/category.provider.md +244 -0
  30. package/documentation/docs/providers/checkout.provider.md +315 -0
  31. package/documentation/docs/providers/identity.provider.md +194 -0
  32. package/documentation/docs/providers/inventory.provider.md +162 -0
  33. package/documentation/docs/providers/order-search.provider.md +155 -0
  34. package/documentation/docs/providers/order.provider.md +160 -0
  35. package/documentation/docs/providers/price.provider.md +197 -0
  36. package/documentation/docs/providers/product-search.provider.md +265 -0
  37. package/documentation/docs/providers/product.provider.md +204 -0
  38. package/documentation/docs/providers/profile.provider.md +283 -0
  39. package/documentation/docs/providers/store.provider.md +146 -0
  40. package/documentation/docs/schemas/schemas.md +1862 -0
  41. package/documentation/docusaurus.config.js +33 -0
  42. package/documentation/scripts/generate.ts +52 -0
  43. package/documentation/sidebars.js +8 -0
  44. package/documentation/src/css/custom.css +3 -0
  45. package/documentation/src/pages/index.js +12 -0
  46. package/eslint.config.mjs +1 -1
  47. package/examples/node/package.json +6 -6
  48. package/examples/node/src/basic/basic-node-provider-model-extension.spec.ts +0 -2
  49. package/examples/node/src/basic/client-creation.spec.ts +2 -2
  50. package/package.json +19 -5
  51. package/providers/algolia/README.md +12 -4
  52. package/providers/algolia/project.json +1 -1
  53. package/providers/algolia/src/core/initialize.ts +7 -2
  54. package/providers/algolia/src/providers/analytics.provider.ts +114 -0
  55. package/providers/algolia/src/providers/index.ts +1 -0
  56. package/providers/algolia/src/providers/product-search.provider.ts +5 -4
  57. package/providers/algolia/src/test/analytics.spec.ts +138 -0
  58. package/providers/commercetools/project.json +1 -1
  59. package/providers/commercetools/src/providers/identity.provider.ts +8 -1
  60. package/providers/commercetools/src/providers/profile.provider.ts +1 -4
  61. package/providers/commercetools/src/test/caching.spec.ts +3 -3
  62. package/providers/commercetools/src/test/identity.spec.ts +2 -2
  63. package/providers/fake/project.json +1 -1
  64. package/providers/fake/src/providers/analytics.provider.ts +5 -0
  65. package/providers/fake/src/providers/checkout.provider.ts +5 -2
  66. package/providers/fake/src/providers/product.provider.ts +18 -8
  67. package/providers/fake/src/test/cart.provider.spec.ts +0 -2
  68. package/providers/fake/src/test/category.provider.spec.ts +3 -3
  69. package/providers/fake/src/test/checkout.provider.spec.ts +3 -7
  70. package/providers/google-analytics/README.md +11 -0
  71. package/providers/google-analytics/eslint.config.mjs +25 -0
  72. package/providers/google-analytics/package.json +12 -0
  73. package/providers/google-analytics/project.json +33 -0
  74. package/providers/google-analytics/src/core/initialize.ts +16 -0
  75. package/providers/google-analytics/src/index.ts +4 -0
  76. package/providers/google-analytics/src/providers/analytics.provider.ts +162 -0
  77. package/providers/google-analytics/src/schema/capabilities.schema.ts +10 -0
  78. package/providers/google-analytics/src/schema/configuration.schema.ts +9 -0
  79. package/providers/google-analytics/src/test/analytics.provider.spec.ts +93 -0
  80. package/providers/google-analytics/tsconfig.json +24 -0
  81. package/providers/google-analytics/tsconfig.lib.json +23 -0
  82. package/providers/google-analytics/tsconfig.spec.json +28 -0
  83. package/providers/google-analytics/vite.config.ts +26 -0
  84. package/providers/google-analytics/vitest.config.mts +21 -0
  85. package/providers/medusa/package.json +3 -10
  86. package/providers/medusa/project.json +1 -1
  87. package/providers/medusa/src/providers/identity.provider.ts +34 -10
  88. package/providers/medusa/src/providers/profile.provider.ts +5 -15
  89. package/providers/medusa/src/test/test-utils.ts +0 -1
  90. package/providers/medusa/tsconfig.json +3 -0
  91. package/providers/medusa/tsconfig.lib.json +16 -1
  92. package/providers/meilisearch/project.json +1 -1
  93. package/providers/posthog/project.json +1 -1
  94. package/tsconfig.base.json +4 -1
  95. package/.claude/settings.local.json +0 -28
  96. package/core/src/schemas/mutations/analytics.mutation.ts +0 -23
  97. package/providers/algolia/src/test/test-utils.ts +0 -31
  98. /package/documentation/{2-getting-started.md → docs/2-getting-started.md} +0 -0
  99. /package/documentation/{3-querying-and-changing-data.md → docs/3-querying-and-changing-data.md} +0 -0
  100. /package/documentation/{4-product-data.md → docs/4-product-data.md} +0 -0
  101. /package/documentation/{5-cart-and-checkout.md → docs/5-cart-and-checkout.md} +0 -0
  102. /package/documentation/{6-product-search.md → docs/6-product-search.md} +0 -0
  103. /package/documentation/{7-marketing.md → docs/7-marketing.md} +0 -0
@@ -1,7 +1,7 @@
1
1
  import type { Cache } from '../cache/cache.interface.js';
2
2
  import { NoOpCache } from '../cache/noop-cache.js';
3
3
  import type { Client } from './client.js';
4
- import type { AnalyticsProvider } from '../providers/analytics.provider.js';
4
+ import { MulticastAnalyticsProvider, type AnalyticsProvider } from '../providers/analytics.provider.js';
5
5
  import {
6
6
  RequestContextSchema,
7
7
  type RequestContext,
@@ -60,18 +60,14 @@ export class ClientBuilder<TClient = Client> {
60
60
  };
61
61
 
62
62
  if (provider.analytics) {
63
- mergedAnalytics.push(...provider.analytics);
63
+ mergedAnalytics.push(provider.analytics);
64
64
  }
65
65
  }
66
66
 
67
- // Add merged analytics if any were collected
68
- if (mergedAnalytics.length > 0) {
69
- (client as Record<string, unknown>)['analytics'] = mergedAnalytics;
70
- }
71
-
72
67
  // Add cache to complete the client
73
68
  const completeClient = {
74
69
  ...client,
70
+ analytics: new MulticastAnalyticsProvider(sharedCache, this.context, mergedAnalytics),
75
71
  cache: sharedCache,
76
72
  } as TClient & { cache: Cache };
77
73
 
@@ -1,4 +1,3 @@
1
- import type { AnalyticsProvider } from "../providers/analytics.provider.js";
2
1
  import type { ProductProvider } from "../providers/product.provider.js";
3
2
  import type { ProductSearchProvider } from "../providers/product-search.provider.js";
4
3
  import type { IdentityProvider } from '../providers/identity.provider.js';
@@ -7,7 +6,7 @@ import type { PriceProvider } from "../providers/price.provider.js";
7
6
  import type { InventoryProvider } from "../providers/inventory.provider.js";
8
7
  import type { Cache } from "../cache/cache.interface.js";
9
8
  import type { CategoryProvider } from "../providers/category.provider.js";
10
- import type { CheckoutProvider, OrderProvider, ProfileProvider, StoreProvider } from "../providers/index.js";
9
+ import type { AnalyticsProvider, CheckoutProvider, OrderProvider, ProfileProvider, StoreProvider } from "../providers/index.js";
11
10
  import type { OrderSearchProvider } from "../providers/order-search.provider.js";
12
11
 
13
12
  export interface Client {
@@ -17,7 +16,7 @@ export interface Client {
17
16
  cache: Cache,
18
17
  cart: CartProvider,
19
18
  checkout: CheckoutProvider,
20
- analytics: Array<AnalyticsProvider>,
19
+ analytics: AnalyticsProvider,
21
20
  price: PriceProvider,
22
21
  inventory: InventoryProvider,
23
22
  category: CategoryProvider,
@@ -193,7 +193,7 @@ export function validateInput<T>(
193
193
  if (!parse.success) {
194
194
  validated = error<InvalidInputError>({
195
195
  type: 'InvalidInput',
196
- error: parse.error,
196
+ error: JSON.stringify(z.flattenError(parse.error)),
197
197
  });
198
198
  }
199
199
 
@@ -218,7 +218,7 @@ export function validateOutput(
218
218
  if (!parse.success) {
219
219
  validated = error<InvalidOutputError>({
220
220
  type: 'InvalidOutput',
221
- error: parse.error,
221
+ error: JSON.stringify(z.flattenError(parse.error)),
222
222
  });
223
223
  }
224
224
  }
@@ -1,4 +1,5 @@
1
- import type { RequestContext } from "./schemas/session.schema.js";
1
+ import type { AnonymousIdentity } from './schemas/index.js';
2
+ import type { RequestContext } from './schemas/session.schema.js';
2
3
 
3
4
  export function createInitialRequestContext(): RequestContext {
4
5
  return {
@@ -15,8 +16,15 @@ export function createInitialRequestContext(): RequestContext {
15
16
  countyCode: '',
16
17
  cityCode: '',
17
18
  },
18
- session: {},
19
-
19
+ session: {
20
+ identityContext: {
21
+ identity: {
22
+ type: 'Anonymous'
23
+ } satisfies AnonymousIdentity,
24
+ lastUpdated: new Date(),
25
+ personalizationKey: crypto.randomUUID(),
26
+ },
27
+ },
20
28
  correlationId: '',
21
29
  isBot: false,
22
30
  clientIp: '',
@@ -1,7 +1,82 @@
1
+ import type { RequestContext } from '../schemas/session.schema.js';
1
2
  import { BaseProvider } from './base.provider.js';
3
+ import type { Cache } from '../cache/cache.interface.js';
4
+ import {
5
+ AnalyticsMutationSchema,
6
+ type AnalyticsMutation,
7
+ type AnalyticsMutationProductAddToCartEvent,
8
+ type AnalyticsMutationProductDetailsViewEvent,
9
+ type AnalyticsMutationProductSummaryClickEvent,
10
+ type AnalyticsMutationProductSummaryViewEvent,
11
+ type AnalyticsMutationPurchaseEvent,
12
+ } from '../schemas/index.js';
13
+ import { Reactionary } from '../decorators/reactionary.decorator.js';
2
14
 
3
15
  export abstract class AnalyticsProvider extends BaseProvider {
4
16
  protected override getResourceName(): string {
5
17
  return 'analytics';
6
18
  }
19
+
20
+ public async track(event: AnalyticsMutation): Promise<void> {
21
+ switch (event.event) {
22
+ case 'product-summary-view':
23
+ await this.processProductSummaryView(event);
24
+ break;
25
+ case 'product-summary-click':
26
+ await this.processProductSummaryClick(event);
27
+ break;
28
+ case 'product-details-view':
29
+ await this.processProductDetailsView(event);
30
+ break;
31
+ case 'product-cart-add':
32
+ await this.processProductAddToCart(event);
33
+ break;
34
+ case 'purchase':
35
+ await this.processPurchase(event)
36
+ break;
37
+ }
38
+ }
39
+
40
+ protected async processProductSummaryView(event: AnalyticsMutationProductSummaryViewEvent) {
41
+ // Default is no-op
42
+ }
43
+
44
+ protected async processProductSummaryClick(event: AnalyticsMutationProductSummaryClickEvent) {
45
+ // Default is no-op
46
+ }
47
+
48
+ protected async processProductDetailsView(event: AnalyticsMutationProductDetailsViewEvent) {
49
+ // Default is no-op
50
+ }
51
+
52
+ protected async processProductAddToCart(event: AnalyticsMutationProductAddToCartEvent) {
53
+ // Default is no-op
54
+ }
55
+
56
+ protected async processPurchase(event: AnalyticsMutationPurchaseEvent) {
57
+ // Default is no-op
58
+ }
59
+ }
60
+
61
+ export class MulticastAnalyticsProvider extends AnalyticsProvider {
62
+ protected providers: Array<AnalyticsProvider>;
63
+
64
+ constructor(
65
+ cache: Cache,
66
+ requestContext: RequestContext,
67
+ providers: Array<AnalyticsProvider>
68
+ ) {
69
+ super(cache, requestContext);
70
+
71
+ this.providers = providers;
72
+ }
73
+
74
+ @Reactionary({
75
+ inputSchema: AnalyticsMutationSchema,
76
+ })
77
+ public override async track(event: AnalyticsMutation) {
78
+ for (const provider of this.providers) {
79
+ provider.track(event);
80
+ }
81
+ }
7
82
  }
@@ -6,6 +6,9 @@ import type { CartQueryById } from "../schemas/queries/cart.query.js";
6
6
  import type { Result } from "../schemas/result.js";
7
7
  import { BaseProvider } from "./base.provider.js";
8
8
 
9
+ /**
10
+ * @group Providers
11
+ */
9
12
  export abstract class CartProvider extends BaseProvider {
10
13
 
11
14
  /**
@@ -11,6 +11,7 @@ import { BaseProvider } from "./base.provider.js";
11
11
  *
12
12
  * We only allow fetching one hierachy level at a time, for now. This is to avoid development patterns of "fetch 5000 categories in one go.."
13
13
  *
14
+ * @group Foo
14
15
  */
15
16
  export abstract class CategoryProvider extends BaseProvider {
16
17
  /**
@@ -13,4 +13,9 @@ export abstract class IdentityProvider extends BaseProvider {
13
13
  protected override getResourceName(): string {
14
14
  return 'identity';
15
15
  }
16
+
17
+ protected updateIdentityContext(identity: Identity) {
18
+ this.context.session.identityContext.lastUpdated = new Date();
19
+ this.context.session.identityContext.identity = identity;
20
+ }
16
21
  }
@@ -3,7 +3,7 @@ import type { InferType } from '../../zod-utils.js';
3
3
 
4
4
  export const InvalidInputErrorSchema = z.looseObject({
5
5
  type: z.literal('InvalidInput'),
6
- error: z.ZodError
6
+ error: z.string(),
7
7
  });
8
8
 
9
9
  export type InvalidInputError = InferType<typeof InvalidInputErrorSchema>;
@@ -3,7 +3,7 @@ import type { InferType } from '../../zod-utils.js';
3
3
 
4
4
  export const InvalidOutputErrorSchema = z.looseObject({
5
5
  type: z.literal('InvalidOutput'),
6
- error: z.ZodError
6
+ error: z.string()
7
7
  });
8
8
 
9
9
  export type InvalidOutputError = InferType<typeof InvalidOutputErrorSchema>;
@@ -127,6 +127,9 @@ export const ProductSearchIdentifierSchema = z.looseObject({
127
127
  categoryFilter: FacetValueIdentifierSchema.optional().describe('An optional category filter applied to the search results.'),
128
128
  });
129
129
 
130
+ /**
131
+ * Bar
132
+ */
130
133
  export const OrderSearchIdentifierSchema = z.looseObject({
131
134
  term: z.string().describe('The search term used to find orders. Not all providers may support term-based search for orders.'),
132
135
  partNumber: z.array(z.string()).optional().describe('An optional list part number to filter orders by specific products. Will be ANDed together.'),
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { CartIdentifierSchema, CartItemIdentifierSchema, IdentityIdentifierSchema, OrderIdentifierSchema, OrderInventoryStatusSchema, OrderItemIdentifierSchema, OrderStatusSchema, ProductVariantIdentifierSchema } from '../models/identifiers.model.js';
2
+ import { CartIdentifierSchema, IdentityIdentifierSchema, OrderIdentifierSchema, OrderInventoryStatusSchema, OrderItemIdentifierSchema, OrderStatusSchema, ProductVariantIdentifierSchema } from '../models/identifiers.model.js';
3
3
  import { BaseModelSchema } from './base.model.js';
4
4
  import { AddressSchema } from './profile.model.js';
5
5
  import { ShippingMethodSchema } from './shipping-method.model.js';
@@ -34,7 +34,7 @@ export const OrderSchema = BaseModelSchema.extend({
34
34
  });
35
35
 
36
36
 
37
- export type OrderStatus = InferType<typeof OrderStatusSchema>;
37
+ export type OrderStatus = z.infer<typeof OrderStatusSchema>;
38
38
  export type OrderInventoryStatus = InferType<typeof OrderInventoryStatusSchema>;
39
39
  export type OrderItem = InferType<typeof OrderItemSchema>;
40
40
  export type Order = InferType<typeof OrderSchema>;
@@ -0,0 +1,23 @@
1
+ import z from 'zod';
2
+ import type { InferType } from '../../../zod-utils.js';
3
+ import { AnalyticsMutationProductAddToCartEventSchema } from './product-add-to-cart.mutation.js';
4
+ import { AnalyticsMutationProductDetailsViewEventSchema } from './product-details-view.mutation.js';
5
+ import { AnalyticsMutationProductSummaryClickEventSchema } from './product-summary-click.mutation.js';
6
+ import { AnalyticsMutationProductSummaryViewEventSchema } from './product-summary-view.mutation.js';
7
+ import { AnalyticsMutationPurchaseEventSchema } from './purchase.mutation.js';
8
+
9
+ export const AnalyticsMutationSchema = z.discriminatedUnion('event', [
10
+ AnalyticsMutationProductSummaryViewEventSchema,
11
+ AnalyticsMutationProductSummaryClickEventSchema,
12
+ AnalyticsMutationProductDetailsViewEventSchema,
13
+ AnalyticsMutationProductAddToCartEventSchema,
14
+ AnalyticsMutationPurchaseEventSchema
15
+ ]);
16
+
17
+ export type AnalyticsMutation = InferType<typeof AnalyticsMutationSchema>;
18
+
19
+ export * from './product-add-to-cart.mutation.js';
20
+ export * from './product-details-view.mutation.js';
21
+ export * from './product-summary-click.mutation.js';
22
+ export * from './product-summary-view.mutation.js';
23
+ export * from './purchase.mutation.js';
@@ -0,0 +1,25 @@
1
+ import z from 'zod';
2
+ import {
3
+ ProductSearchIdentifierSchema,
4
+ ProductIdentifierSchema,
5
+ } from '../../models/identifiers.model.js';
6
+ import { BaseMutationSchema } from '../base.mutation.js';
7
+ import type { InferType } from '../../../zod-utils.js';
8
+
9
+ export const AnalyticsMutationProductAddToCartEventSchema =
10
+ BaseMutationSchema.extend({
11
+ event: z.literal('product-cart-add'),
12
+ source: z
13
+ .discriminatedUnion('type', [
14
+ z.object({
15
+ type: z.literal('search'),
16
+ identifier: ProductSearchIdentifierSchema,
17
+ }),
18
+ ])
19
+ .optional(),
20
+ product: ProductIdentifierSchema,
21
+ });
22
+
23
+ export type AnalyticsMutationProductAddToCartEvent = InferType<
24
+ typeof AnalyticsMutationProductAddToCartEventSchema
25
+ >;
@@ -0,0 +1,14 @@
1
+ import z from 'zod';
2
+ import { ProductIdentifierSchema } from '../../models/identifiers.model.js';
3
+ import { BaseMutationSchema } from '../base.mutation.js';
4
+ import type { InferType } from '../../../zod-utils.js';
5
+
6
+ export const AnalyticsMutationProductDetailsViewEventSchema =
7
+ BaseMutationSchema.extend({
8
+ event: z.literal('product-details-view'),
9
+ product: ProductIdentifierSchema,
10
+ });
11
+
12
+ export type AnalyticsMutationProductDetailsViewEvent = InferType<
13
+ typeof AnalyticsMutationProductDetailsViewEventSchema
14
+ >;
@@ -0,0 +1,26 @@
1
+ import type { InferType } from '../../../zod-utils.js';
2
+ import {
3
+ ProductIdentifierSchema,
4
+ ProductSearchIdentifierSchema,
5
+ } from '../../models/identifiers.model.js';
6
+ import { BaseMutationSchema } from '../base.mutation.js';
7
+ import { z } from 'zod';
8
+
9
+ export const AnalyticsMutationProductSummaryClickEventSchema =
10
+ BaseMutationSchema.extend({
11
+ event: z.literal('product-summary-click'),
12
+ product: ProductIdentifierSchema,
13
+ source: z
14
+ .discriminatedUnion('type', [
15
+ z.object({
16
+ type: z.literal('search'),
17
+ identifier: ProductSearchIdentifierSchema,
18
+ }),
19
+ ])
20
+ .optional(),
21
+ position: z.number().min(0),
22
+ });
23
+
24
+ export type AnalyticsMutationProductSummaryClickEvent = InferType<
25
+ typeof AnalyticsMutationProductSummaryClickEventSchema
26
+ >;
@@ -0,0 +1,25 @@
1
+ import type { InferType } from '../../../zod-utils.js';
2
+ import {
3
+ ProductIdentifierSchema,
4
+ ProductSearchIdentifierSchema,
5
+ } from '../../models/identifiers.model.js';
6
+ import { BaseMutationSchema } from '../base.mutation.js';
7
+ import { z } from 'zod';
8
+
9
+ export const AnalyticsMutationProductSummaryViewEventSchema =
10
+ BaseMutationSchema.extend({
11
+ event: z.literal('product-summary-view'),
12
+ source: z
13
+ .discriminatedUnion('type', [
14
+ z.object({
15
+ type: z.literal('search'),
16
+ identifier: ProductSearchIdentifierSchema,
17
+ }),
18
+ ])
19
+ .optional(),
20
+ products: z.array(ProductIdentifierSchema),
21
+ });
22
+
23
+ export type AnalyticsMutationProductSummaryViewEvent = InferType<
24
+ typeof AnalyticsMutationProductSummaryViewEventSchema
25
+ >;
@@ -0,0 +1,14 @@
1
+ import z from 'zod';
2
+ import { BaseMutationSchema } from '../base.mutation.js';
3
+ import type { InferType } from '../../../zod-utils.js';
4
+ import { OrderSchema } from '../../models/order.model.js';
5
+
6
+ export const AnalyticsMutationPurchaseEventSchema =
7
+ BaseMutationSchema.extend({
8
+ event: z.literal('purchase'),
9
+ order: OrderSchema
10
+ });
11
+
12
+ export type AnalyticsMutationPurchaseEvent = InferType<
13
+ typeof AnalyticsMutationPurchaseEventSchema
14
+ >;
@@ -1,4 +1,4 @@
1
- export * from './analytics.mutation.js';
1
+ export * from './analytics/index.js';
2
2
  export * from './base.mutation.js';
3
3
  export * from './cart.mutation.js';
4
4
  export * from './identity.mutation.js';
@@ -2,6 +2,9 @@ import type { InferType } from "../../zod-utils.js";
2
2
  import { OrderSearchIdentifierSchema } from "../models/identifiers.model.js";
3
3
  import { BaseQuerySchema } from "./base.query.js";
4
4
 
5
+ /**
6
+ * Foo
7
+ */
5
8
  export const OrderSearchQueryByTermSchema = BaseQuerySchema.extend({
6
9
  search: OrderSearchIdentifierSchema
7
10
  });
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
- import { WebStoreIdentifierSchema } from './models/identifiers.model.js';
2
+ import { IdentityIdentifierSchema, WebStoreIdentifierSchema } from './models/identifiers.model.js';
3
3
  import { CurrencySchema } from './models/currency.model.js';
4
+ import { IdentitySchema } from './models/identity.model.js';
4
5
 
5
6
  /**
6
7
  * The language and locale context for the current request.
@@ -8,9 +9,17 @@ import { CurrencySchema } from './models/currency.model.js';
8
9
  export const LanguageContextSchema = z.looseObject( {
9
10
  locale: z.string().default('en-US'),
10
11
  currencyCode: CurrencySchema.default(() => CurrencySchema.parse({})),
11
- })
12
+ });
13
+
14
+ export const IdentityContextSchema = z.looseObject({
15
+ identity: IdentitySchema,
16
+ personalizationKey: z.string(),
17
+ lastUpdated: z.date()
18
+ });
12
19
 
13
- export const SessionSchema = z.record(z.string(), z.any());
20
+ export const SessionSchema = z.record(z.string(), z.any()).and(z.object({
21
+ identityContext: IdentityContextSchema
22
+ }));
14
23
 
15
24
  export const TaxJurisdictionSchema = z.object( {
16
25
  countryCode: z.string().default('US'),
@@ -21,7 +30,6 @@ export const TaxJurisdictionSchema = z.object( {
21
30
 
22
31
  export const RequestContextSchema = z.looseObject( {
23
32
  session: SessionSchema.default(() => SessionSchema.parse({})).describe('Read/Write session storage. Caller is responsible for persisting any changes. Providers will prefix own values'),
24
-
25
33
  languageContext: LanguageContextSchema.default(() => LanguageContextSchema.parse({})).describe('ReadOnly. The language and locale context for the current request.'),
26
34
  storeIdentifier: WebStoreIdentifierSchema.default(() => WebStoreIdentifierSchema.parse({})).describe('ReadOnly. The identifier of the current web store making the request.'),
27
35
  taxJurisdiction: TaxJurisdictionSchema.default(() => TaxJurisdictionSchema.parse({})).describe('ReadOnly. The tax jurisdiction for the current request, typically derived from the store location or carts billing address'),
@@ -32,14 +40,18 @@ export const RequestContextSchema = z.looseObject( {
32
40
  clientIp: z.string().default('').describe('The IP address of the client making the request, if available. Mostly for logging purposes'),
33
41
  userAgent: z.string().default('').describe('The user agent string of the client making the request, if available.'),
34
42
  referrer: z.string().default('').describe('The referrer URL, if available.'),
35
- })
43
+ });
36
44
 
37
45
 
38
46
 
39
47
  // Note, for this ONE type (which is effectively a dictionary), we currently don't want
40
48
  // to strip [key: string]: unknown, hence the manual zod infer over the helper.
41
49
  // Maybe there is a better solution, with a different typing for SessionSchema...
42
- export type Session = z.infer<typeof SessionSchema>;
43
- export type LanguageContext = z.infer<typeof LanguageContextSchema>;
44
- export type RequestContext = z.infer<typeof RequestContextSchema>;
45
- export type TaxJurisdiction = z.infer<typeof TaxJurisdictionSchema>;
50
+ /**
51
+ * @see {@link SessionSchema}
52
+ */
53
+ export type Session = z.infer<typeof SessionSchema> & { _?: never};
54
+ export type LanguageContext = z.infer<typeof LanguageContextSchema> & { _?: never};
55
+ export type RequestContext = z.infer<typeof RequestContextSchema> & { _?: never};
56
+ export type TaxJurisdiction = z.infer<typeof TaxJurisdictionSchema> & { _?: never};
57
+ export type IdentityContext = z.infer<typeof IdentityContextSchema> & { _?: never};
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Capabilities, ClientFromCapabilities } from "../schemas/capabilities.schema.js";
3
+ import type { Cache } from '../cache/cache.interface.js';
4
+ import type { RequestContext } from "../schemas/session.schema.js";
5
+ import { AnalyticsProvider } from "../providers/analytics.provider.js";
6
+ import type { AnalyticsMutation } from "../schemas/index.js";
7
+ import { NoOpCache } from "../cache/noop-cache.js";
8
+ import { createInitialRequestContext } from "../initialization.js";
9
+ import { ClientBuilder } from "../client/client-builder.js";
10
+ import type { Client } from "../client/client.js";
11
+
12
+ export class MockAnalyticsProvider extends AnalyticsProvider {
13
+ public events: Array<AnalyticsMutation> = [];
14
+
15
+ public override async track(event: AnalyticsMutation): Promise<void> {
16
+ this.events.push(event);
17
+ }
18
+ }
19
+
20
+ export interface MockConfiguration {
21
+ mock?: string;
22
+ }
23
+
24
+ export function withMockCapabilities<T extends Partial<Capabilities>>(
25
+ client: Partial<Client>
26
+ ) {
27
+ return (
28
+ cache: Cache,
29
+ context: RequestContext
30
+ ): ClientFromCapabilities<T> => {
31
+ return client as ClientFromCapabilities<T>
32
+ }
33
+ };
34
+
35
+ describe('Client Builder', () => {
36
+ it('should properly multicast analytics events to all analytics providers that register themselves', async () => {
37
+ const cache = new NoOpCache();
38
+ const context = createInitialRequestContext();
39
+ const builder = new ClientBuilder(context);
40
+ const analyticsProvider = new MockAnalyticsProvider(cache, context);
41
+ const secondaryAnalyticsProvider = new MockAnalyticsProvider(cache, context);
42
+ const client = builder
43
+ .withCache(cache)
44
+ .withCapability(withMockCapabilities({ analytics: analyticsProvider }))
45
+ .withCapability(withMockCapabilities({ analytics: secondaryAnalyticsProvider }))
46
+ .build();
47
+
48
+ const track = await client.analytics.track({
49
+ event: 'product-details-view',
50
+ product: {
51
+ key: 'P-1000'
52
+ }
53
+ });
54
+
55
+ expect(analyticsProvider.events.length).toBe(1);
56
+ expect(analyticsProvider.events[0].event).toBe('product-details-view');
57
+ expect(secondaryAnalyticsProvider.events.length).toBe(1);
58
+ expect(secondaryAnalyticsProvider.events[0].event).toBe('product-details-view');
59
+ });
60
+ });
@@ -16,4 +16,6 @@ export type StripIndexSignature<T> =
16
16
  } :
17
17
  T;
18
18
 
19
- export type InferType<T extends z.ZodTypeAny> = StripIndexSignature<z.infer<T>>;
19
+ export type AvoidSimplification<T> = T & { _?: never }
20
+
21
+ export type InferType<T extends z.ZodTypeAny> = AvoidSimplification<StripIndexSignature<z.infer<T>>>;
@@ -1,3 +1,7 @@
1
+ ---
2
+ slug: /
3
+ ---
4
+
1
5
  # Reactionary
2
6
 
3
7
  Reactionary is an oppinionated abstraction layer between UX developer and Backend developer in a Composable Commerce environment.
@@ -0,0 +1,9 @@
1
+ # Tracking customer interactions
2
+ Reactionary takes the approach that tracking customer data should be structured and managed, as opposed to ad-hoc in the browser. The core implication of this is that tracking should happen as a centralized, first-party stream of data to the backend, which may then in turn delegate it to the interested parties. We take this approach for a few reasons:
3
+
4
+ - Performance: the client should not have to load several megabytes of tracking scripts that may in turn bring in other tracking scripts as their first priority upon entering the site. This is treating the customer a product rather than a valued customer. The client should only send their tracking data ONCE, and the performance impact of having to collect it should be a minimal obstruction to their actual goal.
5
+ - Control: the system should enforce mandatory requirements such as Do Not Track in a single place, and ensure that anonymization happens as required.
6
+ - Structure: it should be possible to reason about what is tracked on the site, how it is tracked and when it is tracked without having to visit seven different tag managers.
7
+ - Security: pulling in all the embedded and inline scripts from every tag manager is a security incident waiting to happen.
8
+
9
+ To this end the client exposes a single provider in the form of `client.analytics`. This client is internally responsible for delegating events to all relevant subscribers capabilities used to build the client. This means that a single call to record a pageview or attribution will be enough, even if that data internally needs to be multiplexed to GA4, Algolia and Posthog as an example.