@reactionary/medusa 0.10.51 → 0.10.53

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.
@@ -31,6 +31,8 @@ import {
31
31
  import { FetchError } from "@medusajs/js-sdk";
32
32
  import {} from "@medusajs/types";
33
33
  import createDebug from "debug";
34
+ import { MedusaAdminAPI } from "../core/client.js";
35
+ import { MedusaCachedPluginListSchema } from "../schema/medusa.schema.js";
34
36
  import {
35
37
  handleProviderError
36
38
  } from "../utils/medusa-helpers.js";
@@ -52,7 +54,87 @@ class MedusaCartCapability extends CartCapability {
52
54
  * example: this.includedFields = [includedFields, '+discounts.*'].join(',');
53
55
  */
54
56
  includedFields = ["+items.*", "+items.adjustments.*", "+shipping_methods.adjustments.*"].join(",");
57
+ /**
58
+ * Fields requested when listing owned carts, both via the cart-ownership
59
+ * endpoint and via the session-tracked fallback. Every entry must be in
60
+ * the allowed field set of both routes; `customer.id` stands in for
61
+ * `customer_id`, which is not an allowed field (see normalizeOwnedCart).
62
+ */
63
+ ownedCartListFields = ["id", "updated_at", "metadata", "items.id", "customer.id"].join(",");
64
+ /** Sort order for the server-side owned-carts listing. */
65
+ ownedCartListOrder = "-updated_at";
66
+ /**
67
+ * Package name of the optional backend plugin that serves
68
+ * `GET /store/customers/me/carts` (a logged-in customer's open carts).
69
+ */
70
+ cartOwnershipPluginName = "@solteq-excom/medusa-cart-ownership";
71
+ /**
72
+ * How long a backend's plugin list stays cached. Bounds how late a newly
73
+ * (un)installed cart-ownership plugin is noticed.
74
+ */
75
+ pluginDetectionTtlSeconds = 900;
55
76
  async listCarts(payload) {
77
+ let data = null;
78
+ if (await this.shouldUseOwnershipEndpoint(payload)) {
79
+ data = await this.listCartsViaOwnershipEndpoint(payload);
80
+ }
81
+ data ??= await this.listCartsFromSession(payload);
82
+ return success(this.factory.parseCartPaginatedSearchResult(this.context, data, payload));
83
+ }
84
+ /**
85
+ * The server-side owned-carts listing only applies to a logged-in
86
+ * customer's own carts: a company bucket and anonymous/guest carts are
87
+ * only known to the session, and the backend only serves the route when
88
+ * the cart-ownership plugin is installed.
89
+ */
90
+ async shouldUseOwnershipEndpoint(payload) {
91
+ if (payload.search.company) {
92
+ return false;
93
+ }
94
+ if (this.context.session.identityContext.identity.type !== "Registered") {
95
+ return false;
96
+ }
97
+ return await this.isCartOwnershipPluginEnabled();
98
+ }
99
+ /**
100
+ * Lists the customer's open carts through the cart-ownership plugin's
101
+ * `GET /store/customers/me/carts`. Returns null when the backend turns
102
+ * out not to serve the route after all (404: plugin configured but not
103
+ * built; 401: the token is no longer valid), so the caller falls back to
104
+ * the session-tracked list. Other errors are real backend faults and
105
+ * propagate.
106
+ */
107
+ async listCartsViaOwnershipEndpoint(payload) {
108
+ const client = await this.getClient();
109
+ const { pageNumber, pageSize } = payload.search.paginationOptions;
110
+ try {
111
+ const response = await client.client.fetch("/store/customers/me/carts", {
112
+ method: "GET",
113
+ query: {
114
+ fields: this.ownedCartListFields,
115
+ limit: pageSize,
116
+ offset: (pageNumber - 1) * pageSize,
117
+ order: this.ownedCartListOrder
118
+ }
119
+ });
120
+ return {
121
+ items: response.carts.map((cart) => this.normalizeOwnedCart(cart)),
122
+ totalCount: response.count
123
+ };
124
+ } catch (err) {
125
+ if (this.shouldFallBackToSessionList(err)) {
126
+ debug("Owned-carts endpoint unavailable, falling back to session-tracked carts:", err);
127
+ return null;
128
+ }
129
+ throw err;
130
+ }
131
+ }
132
+ /**
133
+ * Lists carts from the ids tracked in the session: the behavior from
134
+ * before the cart-ownership plugin existed, and still the only source for
135
+ * anonymous/guest carts and company buckets.
136
+ */
137
+ async listCartsFromSession(payload) {
56
138
  const client = await this.getClient();
57
139
  const sessionData = this.medusaApi.getSessionData();
58
140
  let cartCollection = payload.search.company ? sessionData.allOwnedCarts?.[payload.search.company.taxIdentifier] : sessionData.allOwnedCarts?.["_me"];
@@ -62,18 +144,58 @@ class MedusaCartCapability extends CartCapability {
62
144
  } else {
63
145
  cartCollection = [];
64
146
  }
65
- const shortFields = ["id", "customerId", "updated_at", "metadata", "items.id"].join(",");
66
- const allPromises = cartCollection.map((cartIdentifier) => client.store.cart.retrieve(cartIdentifier.key, { fields: shortFields }));
147
+ const allPromises = cartCollection.map((cartIdentifier) => client.store.cart.retrieve(cartIdentifier.key, { fields: this.ownedCartListFields }));
67
148
  const responses = await Promise.all(allPromises);
68
149
  const carts = responses.map((response) => response.cart).filter((cart) => !!cart);
69
- return success(this.factory.parseCartPaginatedSearchResult(
70
- this.context,
71
- {
72
- items: carts,
73
- totalCount
74
- },
75
- payload
76
- ));
150
+ return {
151
+ items: carts.map((cart) => this.normalizeOwnedCart(cart)),
152
+ totalCount
153
+ };
154
+ }
155
+ /**
156
+ * The factory reads `customer_id`, but only the `customer.id` relation is
157
+ * an allowed list field, so copy it over when `customer_id` itself wasn't
158
+ * returned.
159
+ */
160
+ normalizeOwnedCart(cart) {
161
+ return { ...cart, customer_id: cart.customer_id ?? cart.customer?.id };
162
+ }
163
+ /** A 404 (route not served) or 401 (token no longer valid) degrades to the session list. */
164
+ shouldFallBackToSessionList(err) {
165
+ return err instanceof FetchError && (err.status === 404 || err.status === 401);
166
+ }
167
+ /**
168
+ * Checks whether the backend has the cart-ownership plugin, by its name in
169
+ * core's `GET /admin/plugins`. Detection must never break cart listing, so
170
+ * any failure just reports the plugin as absent.
171
+ */
172
+ async isCartOwnershipPluginEnabled() {
173
+ const plugins = await this.fetchEnabledPlugins();
174
+ return plugins.includes(this.cartOwnershipPluginName);
175
+ }
176
+ /**
177
+ * The backend's configured plugin list, kept in the reactionary cache
178
+ * (shared across sessions) so not every request re-asks the admin API.
179
+ * Failures are not cached: a plugin-less answer from here only costs the
180
+ * fallback behavior, while caching a transient failure would hide the
181
+ * plugin for a whole TTL.
182
+ */
183
+ async fetchEnabledPlugins() {
184
+ const cacheKey = `medusa:plugins:${this.config.apiUrl}`;
185
+ const cached = await this.cache.get(cacheKey, MedusaCachedPluginListSchema);
186
+ if (cached) {
187
+ return cached.plugins;
188
+ }
189
+ try {
190
+ const adminClient = await new MedusaAdminAPI(this.config, this.context).getClient();
191
+ const response = await adminClient.client.fetch("/admin/plugins", { method: "GET" });
192
+ const plugins = response.plugins.map((plugin) => plugin.name);
193
+ await this.cache.put(cacheKey, { plugins }, { ttlSeconds: this.pluginDetectionTtlSeconds, dependencyIds: [] });
194
+ return plugins;
195
+ } catch (err) {
196
+ debug("Failed to list backend plugins:", err);
197
+ return [];
198
+ }
77
199
  }
78
200
  async getById(payload) {
79
201
  try {
@@ -219,6 +341,21 @@ class MedusaCartCapability extends CartCapability {
219
341
  }
220
342
  };
221
343
  }
344
+ /**
345
+ * Session tracking is skipped for a logged-in customer's own carts when
346
+ * the backend has the cart-ownership plugin: the backend attributes the
347
+ * cart to the customer at creation and lists it server-side. Anonymous
348
+ * carts and company buckets are still only known to the session.
349
+ */
350
+ async shouldTrackOwnedCartInSession(payload) {
351
+ if (payload.company) {
352
+ return true;
353
+ }
354
+ if (this.context.session.identityContext.identity.type !== "Registered") {
355
+ return true;
356
+ }
357
+ return !await this.isCartOwnershipPluginEnabled();
358
+ }
222
359
  addCartToOwnedList(cartIdentifier, companyId) {
223
360
  const sessionData = this.medusaApi.getSessionData();
224
361
  const companyIdToUse = companyId ? companyId.taxIdentifier : "_me";
@@ -278,7 +415,9 @@ class MedusaCartCapability extends CartCapability {
278
415
  }
279
416
  );
280
417
  if (response.cart) {
281
- this.addCartToOwnedList(this.factory.parseCartIdentifier(this.context, response.cart), payload.company);
418
+ if (await this.shouldTrackOwnedCartInSession(payload)) {
419
+ this.addCartToOwnedList(this.factory.parseCartIdentifier(this.context, response.cart), payload.company);
420
+ }
282
421
  this.medusaApi.setSessionData({
283
422
  activeCartId: this.factory.parseCartIdentifier(this.context, response.cart)
284
423
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reactionary/medusa",
3
- "version": "0.10.51",
3
+ "version": "0.10.53",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "types": "./src/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@medusajs/js-sdk": "^2.13.3",
13
- "@reactionary/core": "0.10.51",
13
+ "@reactionary/core": "0.10.53",
14
14
  "debug": "^4.4.3",
15
15
  "@medusajs/types": "^2.13.3",
16
16
  "zod": "4.1.9"
@@ -11,6 +11,9 @@ const MedusaRegionSchema = z.looseObject({
11
11
  name: z.string(),
12
12
  currency_code: z.string()
13
13
  });
14
+ const MedusaCachedPluginListSchema = z.looseObject({
15
+ plugins: z.array(z.string())
16
+ });
14
17
  const MedusaSessionSchema = z.looseObject({
15
18
  activeCartId: MedusaCartIdentifierSchema.optional(),
16
19
  allOwnedCarts: z.record(z.string(), z.array(MedusaCartIdentifierSchema)).optional(),
@@ -20,6 +23,7 @@ const MedusaSessionSchema = z.looseObject({
20
23
  selectedRegion: MedusaRegionSchema.optional()
21
24
  });
22
25
  export {
26
+ MedusaCachedPluginListSchema,
23
27
  MedusaCartIdentifierSchema,
24
28
  MedusaOrderIdentifierSchema,
25
29
  MedusaRegionSchema,
@@ -1,10 +1,20 @@
1
1
  import type { Cache, CartFactory, CartFactoryCartOutput, CartFactoryIdentifierOutput, CartFactoryWithOutput, CartMutationApplyCoupon, CartMutationChangeCurrency, CartMutationCreateCart, CartMutationDeleteCart, CartMutationItemAdd, CartMutationItemQuantityChange, CartMutationItemRemove, CartMutationRemoveCoupon, CartMutationRenameCart, CartPaginatedSearchResult, CartQueryById, CartQueryList, CompanyIdentifier, NotFoundError, RequestContext, Result } from '@reactionary/core';
2
2
  import { CartCapability } from '@reactionary/core';
3
+ import type { StoreCart } from '@medusajs/types';
3
4
  import { type StoreAddCartLineItem, type StoreCartAddPromotion, type StoreCartRemovePromotion, type StoreCreateCart, type StoreUpdateCart, type StoreUpdateCartLineItem } from '@medusajs/types';
4
- import type { MedusaAPI } from '../core/client.js';
5
- import type { MedusaCartFactory } from '../factories/cart/cart.factory.js';
5
+ import { type MedusaAPI } from '../core/client.js';
6
+ import type { MedusaCartFactory, ParseMedusaCartPaginatedSearchResultInput } from '../factories/cart/cart.factory.js';
6
7
  import type { MedusaConfiguration } from '../schema/configuration.schema.js';
7
- import type { MedusaCartIdentifier } from '../schema/medusa.schema.js';
8
+ import { type MedusaCartIdentifier } from '../schema/medusa.schema.js';
9
+ /**
10
+ * A cart as returned when the `customer.id` relation field is requested;
11
+ * `StoreCart` itself has no `customer` property.
12
+ */
13
+ interface MedusaOwnedCart extends StoreCart {
14
+ customer?: {
15
+ id: string;
16
+ } | null;
17
+ }
8
18
  export declare class MedusaCartCapability<TFactory extends CartFactory = MedusaCartFactory> extends CartCapability<CartFactoryCartOutput<TFactory>, CartFactoryIdentifierOutput<TFactory>> {
9
19
  medusaApi: MedusaAPI;
10
20
  protected config: MedusaConfiguration;
@@ -16,8 +26,71 @@ export declare class MedusaCartCapability<TFactory extends CartFactory = MedusaC
16
26
  * example: this.includedFields = [includedFields, '+discounts.*'].join(',');
17
27
  */
18
28
  protected includedFields: string;
29
+ /**
30
+ * Fields requested when listing owned carts, both via the cart-ownership
31
+ * endpoint and via the session-tracked fallback. Every entry must be in
32
+ * the allowed field set of both routes; `customer.id` stands in for
33
+ * `customer_id`, which is not an allowed field (see normalizeOwnedCart).
34
+ */
35
+ protected ownedCartListFields: string;
36
+ /** Sort order for the server-side owned-carts listing. */
37
+ protected ownedCartListOrder: string;
38
+ /**
39
+ * Package name of the optional backend plugin that serves
40
+ * `GET /store/customers/me/carts` (a logged-in customer's open carts).
41
+ */
42
+ protected cartOwnershipPluginName: string;
43
+ /**
44
+ * How long a backend's plugin list stays cached. Bounds how late a newly
45
+ * (un)installed cart-ownership plugin is noticed.
46
+ */
47
+ protected pluginDetectionTtlSeconds: number;
19
48
  constructor(config: MedusaConfiguration, cache: Cache, context: RequestContext, medusaApi: MedusaAPI, factory: CartFactoryWithOutput<TFactory>);
20
49
  listCarts(payload: CartQueryList): Promise<Result<CartPaginatedSearchResult>>;
50
+ /**
51
+ * The server-side owned-carts listing only applies to a logged-in
52
+ * customer's own carts: a company bucket and anonymous/guest carts are
53
+ * only known to the session, and the backend only serves the route when
54
+ * the cart-ownership plugin is installed.
55
+ */
56
+ protected shouldUseOwnershipEndpoint(payload: CartQueryList): Promise<boolean>;
57
+ /**
58
+ * Lists the customer's open carts through the cart-ownership plugin's
59
+ * `GET /store/customers/me/carts`. Returns null when the backend turns
60
+ * out not to serve the route after all (404: plugin configured but not
61
+ * built; 401: the token is no longer valid), so the caller falls back to
62
+ * the session-tracked list. Other errors are real backend faults and
63
+ * propagate.
64
+ */
65
+ protected listCartsViaOwnershipEndpoint(payload: CartQueryList): Promise<ParseMedusaCartPaginatedSearchResultInput | null>;
66
+ /**
67
+ * Lists carts from the ids tracked in the session: the behavior from
68
+ * before the cart-ownership plugin existed, and still the only source for
69
+ * anonymous/guest carts and company buckets.
70
+ */
71
+ protected listCartsFromSession(payload: CartQueryList): Promise<ParseMedusaCartPaginatedSearchResultInput>;
72
+ /**
73
+ * The factory reads `customer_id`, but only the `customer.id` relation is
74
+ * an allowed list field, so copy it over when `customer_id` itself wasn't
75
+ * returned.
76
+ */
77
+ protected normalizeOwnedCart(cart: MedusaOwnedCart): StoreCart;
78
+ /** A 404 (route not served) or 401 (token no longer valid) degrades to the session list. */
79
+ protected shouldFallBackToSessionList(err: unknown): boolean;
80
+ /**
81
+ * Checks whether the backend has the cart-ownership plugin, by its name in
82
+ * core's `GET /admin/plugins`. Detection must never break cart listing, so
83
+ * any failure just reports the plugin as absent.
84
+ */
85
+ protected isCartOwnershipPluginEnabled(): Promise<boolean>;
86
+ /**
87
+ * The backend's configured plugin list, kept in the reactionary cache
88
+ * (shared across sessions) so not every request re-asks the admin API.
89
+ * Failures are not cached: a plugin-less answer from here only costs the
90
+ * fallback behavior, while caching a transient failure would hide the
91
+ * plugin for a whole TTL.
92
+ */
93
+ protected fetchEnabledPlugins(): Promise<string[]>;
21
94
  getById(payload: CartQueryById): Promise<Result<CartFactoryCartOutput<TFactory>, NotFoundError>>;
22
95
  /**
23
96
  * Extension point for the `add` operation to control the payload sent to Medusa when adding an item to the cart. By default, it only includes the variant ID and quantity, but you can override it to include more fields as needed.
@@ -35,6 +108,13 @@ export declare class MedusaCartCapability<TFactory extends CartFactory = MedusaC
35
108
  * @returns
36
109
  */
37
110
  protected createCartPayload(payload: CartMutationCreateCart): Promise<StoreCreateCart>;
111
+ /**
112
+ * Session tracking is skipped for a logged-in customer's own carts when
113
+ * the backend has the cart-ownership plugin: the backend attributes the
114
+ * cart to the customer at creation and lists it server-side. Anonymous
115
+ * carts and company buckets are still only known to the session.
116
+ */
117
+ protected shouldTrackOwnedCartInSession(payload: CartMutationCreateCart): Promise<boolean>;
38
118
  protected addCartToOwnedList(cartIdentifier: MedusaCartIdentifier, companyId?: CompanyIdentifier): void;
39
119
  /**
40
120
  * Removes every trace of a cart from the session: the active cart marker
@@ -84,3 +164,4 @@ export declare class MedusaCartCapability<TFactory extends CartFactory = MedusaC
84
164
  auth: import("@medusajs/js-sdk").Auth;
85
165
  }>;
86
166
  }
167
+ export {};
@@ -13,6 +13,13 @@ export declare const MedusaRegionSchema: z.ZodObject<{
13
13
  name: z.ZodString;
14
14
  currency_code: z.ZodString;
15
15
  }, z.core.$loose>;
16
+ /**
17
+ * Cache entry shape for a backend's configured plugin list, as reported by
18
+ * core's `GET /admin/plugins`.
19
+ */
20
+ export declare const MedusaCachedPluginListSchema: z.ZodObject<{
21
+ plugins: z.ZodArray<z.ZodString>;
22
+ }, z.core.$loose>;
16
23
  export declare const MedusaSessionSchema: z.ZodObject<{
17
24
  activeCartId: z.ZodOptional<z.ZodObject<{
18
25
  key: z.ZodString;
@@ -37,5 +44,6 @@ export declare const MedusaSessionSchema: z.ZodObject<{
37
44
  }, z.core.$loose>;
38
45
  export type MedusaCartIdentifier = InferType<typeof MedusaCartIdentifierSchema>;
39
46
  export type MedusaOrderIdentifier = z.infer<typeof MedusaOrderIdentifierSchema>;
47
+ export type MedusaCachedPluginList = z.infer<typeof MedusaCachedPluginListSchema>;
40
48
  export type MedusaSession = z.infer<typeof MedusaSessionSchema>;
41
49
  export type MedusaRegion = z.infer<typeof MedusaRegionSchema>;