@brytdesigns/shopify-ajax-client 3.1.0 → 3.3.0
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/main/Request.js +57 -2
- package/dist/main/Request.js.map +1 -1
- package/dist/main/index.d.ts +59 -1
- package/dist/main/index.js +2 -2
- package/dist/main/index.js.map +1 -1
- package/package.json +2 -2
package/dist/main/Request.js
CHANGED
|
@@ -10,11 +10,13 @@ import * as LogLevel from "effect/LogLevel";
|
|
|
10
10
|
import * as Pipeable from "effect/Pipeable";
|
|
11
11
|
import * as Predicate from "effect/Predicate";
|
|
12
12
|
import * as Schema from "effect/Schema";
|
|
13
|
+
import * as SchemaGetter from "effect/SchemaGetter";
|
|
13
14
|
//#region src/Request.ts
|
|
14
15
|
var Request_exports = /* @__PURE__ */ __exportAll({
|
|
15
16
|
execute: () => execute,
|
|
16
17
|
isAjaxRequest: () => isAjaxRequest,
|
|
17
18
|
makeRoute: () => makeRoute,
|
|
19
|
+
renderSections: () => renderSections,
|
|
18
20
|
withHeader: () => withHeader,
|
|
19
21
|
withHeaders: () => withHeaders,
|
|
20
22
|
withInput: () => withInput,
|
|
@@ -27,6 +29,7 @@ var METHOD = {
|
|
|
27
29
|
POST: "post"
|
|
28
30
|
};
|
|
29
31
|
var METHODS_BY_URL = {
|
|
32
|
+
root_url: METHOD.GET,
|
|
30
33
|
products_url: METHOD.GET,
|
|
31
34
|
product_url: METHOD.GET,
|
|
32
35
|
predictive_search_url: METHOD.GET,
|
|
@@ -117,6 +120,58 @@ var withSections = dual(2, (self, sections) => makeRequest({
|
|
|
117
120
|
}
|
|
118
121
|
}));
|
|
119
122
|
/**
|
|
123
|
+
* The standalone Section Rendering API's input: one to five section ids.
|
|
124
|
+
*
|
|
125
|
+
* Its own schema rather than `Ajax.Sections.Input`, for two reasons. There
|
|
126
|
+
* `sections` is optional, because a cart route answers without it; here a
|
|
127
|
+
* request without it asks for the whole page as HTML, which this client
|
|
128
|
+
* cannot decode. And `sections_url` only means something to the bundled form —
|
|
129
|
+
* on a page request, the page already is the rendering context.
|
|
130
|
+
*
|
|
131
|
+
* Five is Shopify's documented limit. Checking it here fails the request
|
|
132
|
+
* before it is sent rather than after a round trip.
|
|
133
|
+
*/
|
|
134
|
+
var RenderSectionsInput = Schema.Struct({ sections: Schema.Array(Schema.String).check(Schema.isMinLength(1), Schema.isMaxLength(5)).pipe(Schema.decodeTo(Schema.String, {
|
|
135
|
+
decode: SchemaGetter.transform((sections) => sections.join(",")),
|
|
136
|
+
encode: SchemaGetter.transform((str) => str.split(","))
|
|
137
|
+
})) });
|
|
138
|
+
/**
|
|
139
|
+
* Renders sections through Shopify's standalone Section Rendering API.
|
|
140
|
+
*
|
|
141
|
+
* The cart routes can bundle sections into their own response, under a
|
|
142
|
+
* `sections` key beside the cart. This is the other form — `?sections=` on any
|
|
143
|
+
* storefront page — and it answers with the sections and nothing else, keyed
|
|
144
|
+
* by id at the top level. So the output schema is the section record itself,
|
|
145
|
+
* not a field added to some route's output.
|
|
146
|
+
*
|
|
147
|
+
* `path` picks the page, and the sections inherit its Liquid context: a
|
|
148
|
+
* product section rendered against `/products/hat?variant=123` renders that
|
|
149
|
+
* variant, and any query parameter the page respects (`q`, `page`) is
|
|
150
|
+
* respected here too. Nothing prefixes a locale, so on a multi-language store
|
|
151
|
+
* build the path from `window.Shopify.routes.root` or `location.pathname`.
|
|
152
|
+
*
|
|
153
|
+
* Sections are fixed when the request is built. They type the result, so the
|
|
154
|
+
* output schema is made from them rather than from whatever is eventually
|
|
155
|
+
* sent: a later {@link withSections} fails at decode, loudly, instead of
|
|
156
|
+
* returning a record whose keys the type does not describe.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* const { header, footer } = yield* pipe(
|
|
161
|
+
* renderSections({ path: "/", sections: ["header", "footer"] }),
|
|
162
|
+
* execute,
|
|
163
|
+
* );
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
var renderSections = (options) => makeRequest({
|
|
167
|
+
routeName: "root_url",
|
|
168
|
+
path: options.path,
|
|
169
|
+
inputSchema: RenderSectionsInput,
|
|
170
|
+
outputSchema: () => Ajax.Sections.makeResponseSchema(options.sections.join(",")),
|
|
171
|
+
input: { sections: options.sections },
|
|
172
|
+
headers: {}
|
|
173
|
+
});
|
|
174
|
+
/**
|
|
120
175
|
* Runs the description.
|
|
121
176
|
*
|
|
122
177
|
* Named rather than untraced: this is the one place in the package worth a
|
|
@@ -135,7 +190,7 @@ var execute = Effect.fn("AjaxClient.execute")(function* (self) {
|
|
|
135
190
|
const routes = yield* Ajax.Window.ShopifyRoutes.Routes;
|
|
136
191
|
const origin = yield* Ajax.Window.StorefrontOrigin.Origin;
|
|
137
192
|
const decodedInput = yield* Schema.decodeUnknownEffect(self.inputSchema)(self.input);
|
|
138
|
-
const route = self.routeName === "product_url" && "handle" in decodedInput ? routes[self.routeName].replaceAll(":handle", encodeURIComponent(String(decodedInput.handle))) : routes[self.routeName];
|
|
193
|
+
const route = self.path !== void 0 ? self.path : self.routeName === "product_url" && "handle" in decodedInput ? routes[self.routeName].replaceAll(":handle", encodeURIComponent(String(decodedInput.handle))) : routes[self.routeName];
|
|
139
194
|
let url;
|
|
140
195
|
if (origin === "") url = route;
|
|
141
196
|
else {
|
|
@@ -195,6 +250,6 @@ var execute = Effect.fn("AjaxClient.execute")(function* (self) {
|
|
|
195
250
|
return output;
|
|
196
251
|
}, Effect.tapCause((cause) => Cause.squash(cause) instanceof AjaxResponseError ? Effect.logDebug("Ajax request returned a non-200", cause) : Effect.logError("Ajax request failed", cause)), Effect.scoped, withNamespacedLogSpan("execute"));
|
|
197
252
|
//#endregion
|
|
198
|
-
export { Request_exports, execute, isAjaxRequest, makeRoute, withHeader, withHeaders, withInput, withSections };
|
|
253
|
+
export { Request_exports, execute, isAjaxRequest, makeRoute, renderSections, withHeader, withHeaders, withInput, withSections };
|
|
199
254
|
|
|
200
255
|
//# sourceMappingURL=Request.js.map
|
package/dist/main/Request.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Request.js","names":[],"sources":["../../src/Request.ts"],"sourcesContent":["import { Ajax } from \"@brytdesigns/shopify-utils/effect\";\nimport * as Cause from \"effect/Cause\";\nimport * as Effect from \"effect/Effect\";\nimport { dual } from \"effect/Function\";\nimport { HttpClient, HttpClientRequest, HttpClientResponse } from \"effect/http\";\nimport * as LogLevel from \"effect/LogLevel\";\nimport * as Pipeable from \"effect/Pipeable\";\nimport * as Predicate from \"effect/Predicate\";\nimport * as Schema from \"effect/Schema\";\nimport {\n\ttype AjaxErrorBody,\n\tAjaxResponseError,\n\tInvalidAjaxMethodError,\n\tUnsafeAjaxRouteError,\n} from \"./errors.js\";\nimport * as LoggerUtils from \"./LoggerUtils.js\";\n\n/**\n * Every route's input extends Shopify's `sections` pair, decoded.\n *\n * Constraining the descriptor on this is what lets {@link execute} read\n * `decodedInput.sections` and {@link withSections} write it without a cast.\n */\nexport type AjaxInput = typeof Ajax.Sections.Input.Type;\n\n/**\n * The same input, encoded — which is what a descriptor actually carries.\n *\n * Worth stating plainly because the two differ in a way that bites:\n * `sections` decodes from `ReadonlyArray<string>` to a comma-joined `string`,\n * so the encoded side is the array. A descriptor holds encoded input, so\n * {@link withSections} takes the array form.\n */\nexport type AjaxInputEncoded = typeof Ajax.Sections.Input.Encoded;\n\nconst BaseOutputSchema = Schema.Struct({});\nconst ProductInputSchema = Ajax.Sections.Input.pipe(\n\tSchema.fieldsAssign({\n\t\thandle: Schema.String,\n\t}),\n);\n\nconst METHOD = {\n\tGET: \"get\",\n\tPOST: \"post\",\n} as const;\n\nconst METHODS_BY_URL = {\n\tproducts_url: METHOD.GET,\n\tproduct_url: METHOD.GET,\n\tpredictive_search_url: METHOD.GET,\n\tproduct_recommendations_url: METHOD.GET,\n\tcart_add_url: METHOD.POST,\n\tcart_update_url: METHOD.POST,\n\tcart_url: METHOD.GET,\n\tcart_change_url: METHOD.POST,\n\tcart_clear_url: METHOD.POST,\n} as const;\n\nexport type ValidAjaxRoutes = keyof typeof METHODS_BY_URL;\n\nconst TypeIdSymbol: unique symbol = Symbol.for(\n\t\"@brytdesigns/shopify-ajax-client/AjaxRequest\",\n);\nexport type TypeId = typeof TypeIdSymbol;\n\n/**\n * A description of a Shopify Ajax request, before it is run.\n *\n * The reason this type exists at all: headers, sections and input are consumed\n * *inside* the request effect, so there is nothing to modify once that effect\n * has been constructed. `pipe(getCart(), withHeaders(...))` can only work if\n * the thing in the pipe is a description rather than an `Effect`. {@link execute}\n * is where the description becomes one.\n */\nexport interface AjaxRequest<\n\tA extends AjaxInput,\n\tI extends AjaxInputEncoded,\n\tRD,\n\tB,\n\tJ,\n\tSD,\n> extends Pipeable.Pipeable {\n\treadonly [TypeIdSymbol]: TypeId;\n\treadonly routeName: ValidAjaxRoutes;\n\treadonly inputSchema: Schema.Codec<A, I, RD>;\n\treadonly outputSchema: (sections?: string) => Schema.Codec<B, J, SD>;\n\treadonly input: I;\n\treadonly headers: Readonly<Record<string, string>>;\n}\n\n/** Narrows an unknown value to an {@link AjaxRequest}. */\nexport const isAjaxRequest = (\n\tu: unknown,\n): u is AjaxRequest<\n\tAjaxInput,\n\tAjaxInputEncoded,\n\tunknown,\n\tunknown,\n\tunknown,\n\tunknown\n> => Predicate.hasProperty(u, TypeIdSymbol);\n\nconst makeRequest = <\n\tA extends AjaxInput,\n\tI extends AjaxInputEncoded,\n\tRD,\n\tB,\n\tJ,\n\tSD,\n>(fields: {\n\treadonly routeName: ValidAjaxRoutes;\n\treadonly inputSchema: Schema.Codec<A, I, RD>;\n\treadonly outputSchema: (sections?: string) => Schema.Codec<B, J, SD>;\n\treadonly input: I;\n\treadonly headers: Readonly<Record<string, string>>;\n}): AjaxRequest<A, I, RD, B, J, SD> => ({\n\t...fields,\n\t[TypeIdSymbol]: TypeIdSymbol,\n\tpipe() {\n\t\t// biome-ignore lint/complexity/noArguments: Pipeable's contract is arguments-based.\n\t\treturn Pipeable.pipeArguments(this, arguments);\n\t},\n});\n\n/**\n * Builds a route constructor.\n *\n * Replaces `makeFactory`. The difference is only in what the returned function\n * gives back: a description that can be piped through the combinators below,\n * rather than an `Effect` that is already closed over its headers.\n *\n * @example\n * ```ts\n * const getCart = Request.makeRoute({\n * routeName: \"cart_url\",\n * inputSchema: CartGetInput,\n * outputSchema: makeCartOutput,\n * });\n *\n * pipe(\n * getCart(),\n * Request.withSections(\"cart-drawer\"),\n * Request.withHeader(\"X-Foo\", \"1\"),\n * Request.execute,\n * );\n * ```\n */\nexport const makeRoute =\n\t<\n\t\tRouteName extends ValidAjaxRoutes,\n\t\tA extends RouteName extends \"product_url\"\n\t\t\t? typeof ProductInputSchema.Type\n\t\t\t: AjaxInput,\n\t\tI extends AjaxInputEncoded,\n\t\tRD,\n\t\tB extends typeof BaseOutputSchema.Type,\n\t\tJ,\n\t\tSD,\n\t>({\n\t\trouteName,\n\t\tinputSchema,\n\t\toutputSchema,\n\t}: {\n\t\trouteName: RouteName;\n\t\tinputSchema: Schema.Codec<A, I, RD>;\n\t\t/**\n\t\t * Built per request from the caller's `sections` input.\n\t\t *\n\t\t * v4's schema extension (`fieldsAssign` / `extendTo`) only applies to a\n\t\t * `Struct`, and output schemas here are transforms. Composing the\n\t\t * sections field is therefore the caller's job - it holds the\n\t\t * pre-transform struct and can extend that instead.\n\t\t */\n\t\toutputSchema: (sections?: string) => Schema.Codec<B, J, SD>;\n\t}) =>\n\t(input?: I): AjaxRequest<A, I, RD, B, J, SD> =>\n\t\tmakeRequest({\n\t\t\trouteName,\n\t\t\tinputSchema,\n\t\t\toutputSchema,\n\t\t\tinput: input ?? ({} as I),\n\t\t\theaders: {},\n\t\t});\n\n/**\n * Merges headers into the request, right-biased.\n *\n * Dual: `withHeaders(request, h)` and `pipe(request, withHeaders(h))` are the\n * same call.\n */\nexport const withHeaders: {\n\t(\n\t\theaders: Readonly<Record<string, string>>,\n\t): <A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t) => AjaxRequest<A, I, RD, B, J, SD>;\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\theaders: Readonly<Record<string, string>>,\n\t): AjaxRequest<A, I, RD, B, J, SD>;\n} = dual(\n\t2,\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\theaders: Readonly<Record<string, string>>,\n\t): AjaxRequest<A, I, RD, B, J, SD> =>\n\t\tmakeRequest({ ...self, headers: { ...self.headers, ...headers } }),\n);\n\n/** Sets a single header. Dual. */\nexport const withHeader: {\n\t(\n\t\tname: string,\n\t\tvalue: string,\n\t): <A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t) => AjaxRequest<A, I, RD, B, J, SD>;\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tname: string,\n\t\tvalue: string,\n\t): AjaxRequest<A, I, RD, B, J, SD>;\n} = dual(\n\t3,\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tname: string,\n\t\tvalue: string,\n\t): AjaxRequest<A, I, RD, B, J, SD> =>\n\t\tmakeRequest({ ...self, headers: { ...self.headers, [name]: value } }),\n);\n\n/** Replaces the request input wholesale. Dual. */\nexport const withInput: {\n\t<I extends AjaxInputEncoded>(\n\t\tinput: I,\n\t): <A extends AjaxInput, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t) => AjaxRequest<A, I, RD, B, J, SD>;\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tinput: I,\n\t): AjaxRequest<A, I, RD, B, J, SD>;\n} = dual(\n\t2,\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tinput: I,\n\t): AjaxRequest<A, I, RD, B, J, SD> => makeRequest({ ...self, input }),\n);\n\n/**\n * Asks Shopify to re-render `sections` alongside the response.\n *\n * Sugar over {@link withInput} for the one input field every route shares, and\n * the field {@link AjaxRequest.outputSchema} is keyed on. Dual.\n */\nexport const withSections: {\n\t(\n\t\tsections: string | ReadonlyArray<string>,\n\t): <A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t) => AjaxRequest<A, I, RD, B, J, SD>;\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tsections: string | ReadonlyArray<string>,\n\t): AjaxRequest<A, I, RD, B, J, SD>;\n} = dual(\n\t2,\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tsections: string | ReadonlyArray<string>,\n\t): AjaxRequest<A, I, RD, B, J, SD> =>\n\t\tmakeRequest({\n\t\t\t...self,\n\t\t\t// `as unknown as I` rather than `as I`: `I` is only known to extend\n\t\t\t// the sections pair, so TypeScript cannot see that adding the field\n\t\t\t// back preserves whatever else the route's input carries. The spread\n\t\t\t// above is what actually preserves it.\n\t\t\tinput: {\n\t\t\t\t...self.input,\n\t\t\t\t// A single section name is the common case. The schema's encoded\n\t\t\t\t// side is the array either way, so normalise instead of\n\t\t\t\t// overloading the combinator.\n\t\t\t\tsections: Predicate.isString(sections) ? [sections] : sections,\n\t\t\t} as unknown as I,\n\t\t}),\n);\n\n/**\n * Runs the description.\n *\n * Named rather than untraced: this is the one place in the package worth a\n * span, and upstream's guidance is to reserve `Effect.fn` for genuine tracing\n * boundaries and prefer `fnUntraced` inside library internals. Transforms are\n * passed as trailing arguments because `Effect.fn` explicitly must not be\n * `.pipe`d.\n *\n * Errors are left as they come. `RequestError`, `ResponseError`, `ParseError`\n * and `HttpBodyError` are already tagged, and flattening them into a plain\n * `Error` — which this package used to do — destroyed the only machine-readable\n * thing about them.\n */\nexport const execute = Effect.fn(\"AjaxClient.execute\")(\n\tfunction* <A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t) {\n\t\tconst client = yield* HttpClient.HttpClient;\n\t\tconst routes = yield* Ajax.Window.ShopifyRoutes.Routes;\n\t\tconst origin = yield* Ajax.Window.StorefrontOrigin.Origin;\n\n\t\tconst decodedInput = yield* Schema.decodeUnknownEffect(self.inputSchema)(\n\t\t\tself.input,\n\t\t);\n\n\t\tconst route =\n\t\t\tself.routeName === \"product_url\" && \"handle\" in decodedInput\n\t\t\t\t? // `encodeURIComponent` rather than raw interpolation, for two\n\t\t\t\t\t// separate reasons. A handle of `../cart/clear.js` would resolve\n\t\t\t\t\t// to a different same-origin endpoint, carrying the shopper's\n\t\t\t\t\t// session cookies to it - and `/cart/clear.js` answers GET. And\n\t\t\t\t\t// `String.prototype.replace` expands `$&`, `` $` `` and `$'` in\n\t\t\t\t\t// the *replacement*, so a handle containing `$` rewrites the path\n\t\t\t\t\t// on its own. Encoding closes both: the output never contains\n\t\t\t\t\t// `/`, `.` is harmless without `/`, and `$` is escaped.\n\t\t\t\t\troutes[self.routeName].replaceAll(\n\t\t\t\t\t\t\":handle\",\n\t\t\t\t\t\tencodeURIComponent(String(decodedInput.handle)),\n\t\t\t\t\t)\n\t\t\t\t: routes[self.routeName];\n\n\t\t// Resolved rather than concatenated, and then checked.\n\t\t//\n\t\t// `new URL(route, origin)` treats the route as a path relative to the\n\t\t// origin, so the userinfo trick that `` `${origin}${route}` `` fell for\n\t\t// (\"@evil.com/collect\") lands harmlessly under the storefront. The\n\t\t// explicit origin comparison is still needed for the case `URL` honours\n\t\t// rather than neutralises: a fully absolute route like\n\t\t// \"https://evil.com/collect\".\n\t\t//\n\t\t// `origin` is \"\" outside a DOM — ajax-client's own tests run without\n\t\t// jsdom — and `new URL(route, \"\")` throws, so that case keeps the old\n\t\t// relative-URL behaviour. There is no origin to escape from.\n\t\tlet url: string;\n\t\tif (origin === \"\") {\n\t\t\turl = route;\n\t\t} else {\n\t\t\tconst resolved = yield* Effect.try({\n\t\t\t\ttry: () => new URL(route, origin),\n\t\t\t\tcatch: () =>\n\t\t\t\t\tnew UnsafeAjaxRouteError({\n\t\t\t\t\t\troute,\n\t\t\t\t\t\trouteName: self.routeName,\n\t\t\t\t\t\torigin,\n\t\t\t\t\t}),\n\t\t\t});\n\t\t\t// Off-origin is allowed, but only when the theme has said so. A\n\t\t\t// storefront fronting its cart through a proxy or an edge worker is\n\t\t\t// a real case; a route that arrives off-origin without the matching\n\t\t\t// entry is either a typo or smuggled, and both should fail.\n\t\t\tconst allowed = yield* Ajax.Window.AllowedRouteOrigins.Origins;\n\t\t\tif (\n\t\t\t\tresolved.origin !== new URL(origin).origin &&\n\t\t\t\t!allowed.includes(resolved.origin)\n\t\t\t) {\n\t\t\t\treturn yield* new UnsafeAjaxRouteError({\n\t\t\t\t\troute,\n\t\t\t\t\trouteName: self.routeName,\n\t\t\t\t\torigin,\n\t\t\t\t});\n\t\t\t}\n\t\t\turl = resolved.toString();\n\t\t}\n\n\t\tconst method = METHODS_BY_URL[self.routeName];\n\t\tconst headers = {\n\t\t\t\"X-SDK-Variant\": \"brytdesigns\",\n\t\t\t\"X-SDK-Variant-Source\": \"shopify-ajax-client\",\n\t\t\t...self.headers,\n\t\t};\n\n\t\tlet request: HttpClientRequest.HttpClientRequest;\n\n\t\t// An explicit three-way rather than POST-or-else-GET: the fallback\n\t\t// branch silently turned any future method into a GET, and made\n\t\t// `InvalidAjaxMethodError` unreachable decoration.\n\t\tif (method === METHOD.POST) {\n\t\t\trequest = yield* HttpClientRequest.post(url, {\n\t\t\t\tacceptJson: true,\n\t\t\t\theaders,\n\t\t\t}).pipe(HttpClientRequest.bodyJson(decodedInput));\n\t\t} else if (method === METHOD.GET) {\n\t\t\trequest = HttpClientRequest.get(url, {\n\t\t\t\tacceptJson: true,\n\t\t\t\theaders,\n\t\t\t}).pipe(HttpClientRequest.setUrlParams(decodedInput));\n\t\t} else {\n\t\t\treturn yield* new InvalidAjaxMethodError();\n\t\t}\n\n\t\t// Annotated before the request goes out rather than after it comes back:\n\t\t// the `tapCause` below is the whole reason this package is debuggable,\n\t\t// and a failure that does not say which route it was is most of the way\n\t\t// to useless.\n\t\t//\n\t\t// `input` rides along only when Debug logging is actually on.\n\t\t//\n\t\t// It is the decoded request body, which for a cart write carries\n\t\t// line-item `properties` (gift messages, engraving text), the cart\n\t\t// `note` and `attributes` — shopper PII. Annotations attach\n\t\t// unconditionally and surface on any log the effect emits, and\n\t\t// `MinimumLogLevel` is Info by default for anyone on the documented\n\t\t// `./effect` path, so annotating it outright logged that payload on an\n\t\t// ordinary non-debug page.\n\t\t//\n\t\t// `isEnabled` rather than a flag of our own, so the gate is the one the\n\t\t// caller already set: the Promise facades pin \"None\" (nothing logs) or\n\t\t// \"All\" with `debug: true` (body included, which is the point of asking\n\t\t// for debug), and the bare Effect path sits at Info, where Debug is off\n\t\t// and the body stays out.\n\t\tconst withBody = yield* LogLevel.isEnabled(\"Debug\");\n\t\tyield* Effect.annotateLogsScoped(\n\t\t\twithBody ? { method, route, input: decodedInput } : { method, route },\n\t\t);\n\n\t\tconst response = yield* client.execute(request);\n\n\t\tif (response.status !== 200) {\n\t\t\t// Shopify's Ajax routes answer with a JSON error body, but the things\n\t\t\t// in front of them do not: a 502 from an edge or a WAF challenge is\n\t\t\t// an HTML page, and `response.json` fails on it. Letting that failure\n\t\t\t// through would replace the error consumers are told to catch with a\n\t\t\t// `ResponseError` carrying neither the status nor the route - which\n\t\t\t// is exactly the case where knowing the route matters most.\n\t\t\tconst body = yield* response.json.pipe(\n\t\t\t\tEffect.catchCause(() => Effect.succeed({} as Partial<AjaxErrorBody>)),\n\t\t\t);\n\t\t\tconst parsed = (body ?? {}) as Partial<AjaxErrorBody>;\n\n\t\t\treturn yield* new AjaxResponseError({\n\t\t\t\t// The body is whatever the server sent. Taking `status` from it\n\t\t\t\t// unchecked lets `{\"status\":\"500\"}` put a string behind a field\n\t\t\t\t// typed `number`, so a consumer's `error.status === 422` silently\n\t\t\t\t// never matches.\n\t\t\t\tstatus: Predicate.isNumber(parsed.status)\n\t\t\t\t\t? parsed.status\n\t\t\t\t\t: response.status,\n\t\t\t\tmessage:\n\t\t\t\t\tparsed.message ?? `Request failed with status ${response.status}`,\n\t\t\t\tdescription: parsed.description,\n\t\t\t\troute,\n\t\t\t});\n\t\t}\n\n\t\tconst output = yield* HttpClientResponse.schemaBodyJson(\n\t\t\tself.outputSchema(decodedInput.sections),\n\t\t)(response);\n\n\t\tyield* Effect.logInfo(\"Request\");\n\n\t\treturn output;\n\t},\n\t// --- Transform order matters here. ---\n\t// `Effect.fn` applies these left to right, so `tapCause` has to come first\n\t// to sit *inside* the scope and the log span. Outside them, the failure\n\t// line carries neither the span nor the `method`/`route` annotations - the\n\t// precise context it exists to record.\n\tEffect.tapCause((cause) =>\n\t\t// A non-200 is an ordinary outcome on a storefront: a sold-out variant\n\t\t// answers 422 on every add-to-cart. Logging that at ERROR would put a\n\t\t// red line in every shopper's console for a case the caller is about to\n\t\t// handle. Anything else - a transport failure, a schema mismatch, a\n\t\t// defect - is genuinely unexpected and keeps ERROR.\n\t\tCause.squash(cause) instanceof AjaxResponseError\n\t\t\t? Effect.logDebug(\"Ajax request returned a non-200\", cause)\n\t\t\t: Effect.logError(\"Ajax request failed\", cause),\n\t),\n\tEffect.scoped,\n\tLoggerUtils.withNamespacedLogSpan(\"execute\"),\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Request.js","names":[],"sources":["../../src/Request.ts"],"sourcesContent":["import { Ajax } from \"@brytdesigns/shopify-utils/effect\";\nimport * as Cause from \"effect/Cause\";\nimport * as Effect from \"effect/Effect\";\nimport { dual } from \"effect/Function\";\nimport { HttpClient, HttpClientRequest, HttpClientResponse } from \"effect/http\";\nimport * as LogLevel from \"effect/LogLevel\";\nimport * as Pipeable from \"effect/Pipeable\";\nimport * as Predicate from \"effect/Predicate\";\nimport * as Schema from \"effect/Schema\";\nimport * as SchemaGetter from \"effect/SchemaGetter\";\nimport {\n\ttype AjaxErrorBody,\n\tAjaxResponseError,\n\tInvalidAjaxMethodError,\n\tUnsafeAjaxRouteError,\n} from \"./errors.js\";\nimport * as LoggerUtils from \"./LoggerUtils.js\";\n\n/**\n * Every route's input extends Shopify's `sections` pair, decoded.\n *\n * Constraining the descriptor on this is what lets {@link execute} read\n * `decodedInput.sections` and {@link withSections} write it without a cast.\n */\nexport type AjaxInput = typeof Ajax.Sections.Input.Type;\n\n/**\n * The same input, encoded — which is what a descriptor actually carries.\n *\n * Worth stating plainly because the two differ in a way that bites:\n * `sections` decodes from `ReadonlyArray<string>` to a comma-joined `string`,\n * so the encoded side is the array. A descriptor holds encoded input, so\n * {@link withSections} takes the array form.\n */\nexport type AjaxInputEncoded = typeof Ajax.Sections.Input.Encoded;\n\nconst BaseOutputSchema = Schema.Struct({});\nconst ProductInputSchema = Ajax.Sections.Input.pipe(\n\tSchema.fieldsAssign({\n\t\thandle: Schema.String,\n\t}),\n);\n\nconst METHOD = {\n\tGET: \"get\",\n\tPOST: \"post\",\n} as const;\n\nconst METHODS_BY_URL = {\n\troot_url: METHOD.GET,\n\tproducts_url: METHOD.GET,\n\tproduct_url: METHOD.GET,\n\tpredictive_search_url: METHOD.GET,\n\tproduct_recommendations_url: METHOD.GET,\n\tcart_add_url: METHOD.POST,\n\tcart_update_url: METHOD.POST,\n\tcart_url: METHOD.GET,\n\tcart_change_url: METHOD.POST,\n\tcart_clear_url: METHOD.POST,\n} as const;\n\nexport type ValidAjaxRoutes = keyof typeof METHODS_BY_URL;\n\nconst TypeIdSymbol: unique symbol = Symbol.for(\n\t\"@brytdesigns/shopify-ajax-client/AjaxRequest\",\n);\nexport type TypeId = typeof TypeIdSymbol;\n\n/**\n * A description of a Shopify Ajax request, before it is run.\n *\n * The reason this type exists at all: headers, sections and input are consumed\n * *inside* the request effect, so there is nothing to modify once that effect\n * has been constructed. `pipe(getCart(), withHeaders(...))` can only work if\n * the thing in the pipe is a description rather than an `Effect`. {@link execute}\n * is where the description becomes one.\n */\nexport interface AjaxRequest<\n\tA extends AjaxInput,\n\tI extends AjaxInputEncoded,\n\tRD,\n\tB,\n\tJ,\n\tSD,\n> extends Pipeable.Pipeable {\n\treadonly [TypeIdSymbol]: TypeId;\n\treadonly routeName: ValidAjaxRoutes;\n\t/**\n\t * A storefront page to send the request to, in place of\n\t * `routes[routeName]`.\n\t *\n\t * Only {@link renderSections} sets it: the Section Rendering API answers on\n\t * any page, and the page is what the sections render against. It goes\n\t * through the same origin check a route does, and it is not part of\n\t * `input`, so it never ends up in the query string.\n\t */\n\treadonly path?: string;\n\treadonly inputSchema: Schema.Codec<A, I, RD>;\n\treadonly outputSchema: (sections?: string) => Schema.Codec<B, J, SD>;\n\treadonly input: I;\n\treadonly headers: Readonly<Record<string, string>>;\n}\n\n/** Narrows an unknown value to an {@link AjaxRequest}. */\nexport const isAjaxRequest = (\n\tu: unknown,\n): u is AjaxRequest<\n\tAjaxInput,\n\tAjaxInputEncoded,\n\tunknown,\n\tunknown,\n\tunknown,\n\tunknown\n> => Predicate.hasProperty(u, TypeIdSymbol);\n\nconst makeRequest = <\n\tA extends AjaxInput,\n\tI extends AjaxInputEncoded,\n\tRD,\n\tB,\n\tJ,\n\tSD,\n>(fields: {\n\treadonly routeName: ValidAjaxRoutes;\n\treadonly path?: string;\n\treadonly inputSchema: Schema.Codec<A, I, RD>;\n\treadonly outputSchema: (sections?: string) => Schema.Codec<B, J, SD>;\n\treadonly input: I;\n\treadonly headers: Readonly<Record<string, string>>;\n}): AjaxRequest<A, I, RD, B, J, SD> => ({\n\t...fields,\n\t[TypeIdSymbol]: TypeIdSymbol,\n\tpipe() {\n\t\t// biome-ignore lint/complexity/noArguments: Pipeable's contract is arguments-based.\n\t\treturn Pipeable.pipeArguments(this, arguments);\n\t},\n});\n\n/**\n * Builds a route constructor.\n *\n * Replaces `makeFactory`. The difference is only in what the returned function\n * gives back: a description that can be piped through the combinators below,\n * rather than an `Effect` that is already closed over its headers.\n *\n * @example\n * ```ts\n * const getCart = Request.makeRoute({\n * routeName: \"cart_url\",\n * inputSchema: CartGetInput,\n * outputSchema: makeCartOutput,\n * });\n *\n * pipe(\n * getCart(),\n * Request.withSections(\"cart-drawer\"),\n * Request.withHeader(\"X-Foo\", \"1\"),\n * Request.execute,\n * );\n * ```\n */\nexport const makeRoute =\n\t<\n\t\tRouteName extends ValidAjaxRoutes,\n\t\tA extends RouteName extends \"product_url\"\n\t\t\t? typeof ProductInputSchema.Type\n\t\t\t: AjaxInput,\n\t\tI extends AjaxInputEncoded,\n\t\tRD,\n\t\tB extends typeof BaseOutputSchema.Type,\n\t\tJ,\n\t\tSD,\n\t>({\n\t\trouteName,\n\t\tinputSchema,\n\t\toutputSchema,\n\t}: {\n\t\trouteName: RouteName;\n\t\tinputSchema: Schema.Codec<A, I, RD>;\n\t\t/**\n\t\t * Built per request from the caller's `sections` input.\n\t\t *\n\t\t * v4's schema extension (`fieldsAssign` / `extendTo`) only applies to a\n\t\t * `Struct`, and output schemas here are transforms. Composing the\n\t\t * sections field is therefore the caller's job - it holds the\n\t\t * pre-transform struct and can extend that instead.\n\t\t */\n\t\toutputSchema: (sections?: string) => Schema.Codec<B, J, SD>;\n\t}) =>\n\t(input?: I): AjaxRequest<A, I, RD, B, J, SD> =>\n\t\tmakeRequest({\n\t\t\trouteName,\n\t\t\tinputSchema,\n\t\t\toutputSchema,\n\t\t\tinput: input ?? ({} as I),\n\t\t\theaders: {},\n\t\t});\n\n/**\n * Merges headers into the request, right-biased.\n *\n * Dual: `withHeaders(request, h)` and `pipe(request, withHeaders(h))` are the\n * same call.\n */\nexport const withHeaders: {\n\t(\n\t\theaders: Readonly<Record<string, string>>,\n\t): <A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t) => AjaxRequest<A, I, RD, B, J, SD>;\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\theaders: Readonly<Record<string, string>>,\n\t): AjaxRequest<A, I, RD, B, J, SD>;\n} = dual(\n\t2,\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\theaders: Readonly<Record<string, string>>,\n\t): AjaxRequest<A, I, RD, B, J, SD> =>\n\t\tmakeRequest({ ...self, headers: { ...self.headers, ...headers } }),\n);\n\n/** Sets a single header. Dual. */\nexport const withHeader: {\n\t(\n\t\tname: string,\n\t\tvalue: string,\n\t): <A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t) => AjaxRequest<A, I, RD, B, J, SD>;\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tname: string,\n\t\tvalue: string,\n\t): AjaxRequest<A, I, RD, B, J, SD>;\n} = dual(\n\t3,\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tname: string,\n\t\tvalue: string,\n\t): AjaxRequest<A, I, RD, B, J, SD> =>\n\t\tmakeRequest({ ...self, headers: { ...self.headers, [name]: value } }),\n);\n\n/** Replaces the request input wholesale. Dual. */\nexport const withInput: {\n\t<I extends AjaxInputEncoded>(\n\t\tinput: I,\n\t): <A extends AjaxInput, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t) => AjaxRequest<A, I, RD, B, J, SD>;\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tinput: I,\n\t): AjaxRequest<A, I, RD, B, J, SD>;\n} = dual(\n\t2,\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tinput: I,\n\t): AjaxRequest<A, I, RD, B, J, SD> => makeRequest({ ...self, input }),\n);\n\n/**\n * Asks Shopify to re-render `sections` alongside the response.\n *\n * Sugar over {@link withInput} for the one input field every route shares, and\n * the field {@link AjaxRequest.outputSchema} is keyed on. Dual.\n */\nexport const withSections: {\n\t(\n\t\tsections: string | ReadonlyArray<string>,\n\t): <A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t) => AjaxRequest<A, I, RD, B, J, SD>;\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tsections: string | ReadonlyArray<string>,\n\t): AjaxRequest<A, I, RD, B, J, SD>;\n} = dual(\n\t2,\n\t<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t\tsections: string | ReadonlyArray<string>,\n\t): AjaxRequest<A, I, RD, B, J, SD> =>\n\t\tmakeRequest({\n\t\t\t...self,\n\t\t\t// `as unknown as I` rather than `as I`: `I` is only known to extend\n\t\t\t// the sections pair, so TypeScript cannot see that adding the field\n\t\t\t// back preserves whatever else the route's input carries. The spread\n\t\t\t// above is what actually preserves it.\n\t\t\tinput: {\n\t\t\t\t...self.input,\n\t\t\t\t// A single section name is the common case. The schema's encoded\n\t\t\t\t// side is the array either way, so normalise instead of\n\t\t\t\t// overloading the combinator.\n\t\t\t\tsections: Predicate.isString(sections) ? [sections] : sections,\n\t\t\t} as unknown as I,\n\t\t}),\n);\n\n/**\n * The standalone Section Rendering API's input: one to five section ids.\n *\n * Its own schema rather than `Ajax.Sections.Input`, for two reasons. There\n * `sections` is optional, because a cart route answers without it; here a\n * request without it asks for the whole page as HTML, which this client\n * cannot decode. And `sections_url` only means something to the bundled form —\n * on a page request, the page already is the rendering context.\n *\n * Five is Shopify's documented limit. Checking it here fails the request\n * before it is sent rather than after a round trip.\n */\nconst RenderSectionsInput = Schema.Struct({\n\tsections: Schema.Array(Schema.String)\n\t\t.check(Schema.isMinLength(1), Schema.isMaxLength(5))\n\t\t.pipe(\n\t\t\tSchema.decodeTo(Schema.String, {\n\t\t\t\tdecode: SchemaGetter.transform((sections: ReadonlyArray<string>) =>\n\t\t\t\t\tsections.join(\",\"),\n\t\t\t\t),\n\t\t\t\tencode: SchemaGetter.transform((str: string) => str.split(\",\")),\n\t\t\t}),\n\t\t),\n});\n\n/**\n * Sections rendered by {@link renderSections}, keyed by the ids asked for.\n *\n * `null` is Shopify's answer for a section that failed to render. The\n * response is still a 200 when that happens, so it is a value to handle\n * rather than an error.\n */\nexport type RenderedSections<S extends string> = {\n\treadonly [K in S]: Element | null;\n};\n\n/**\n * Renders sections through Shopify's standalone Section Rendering API.\n *\n * The cart routes can bundle sections into their own response, under a\n * `sections` key beside the cart. This is the other form — `?sections=` on any\n * storefront page — and it answers with the sections and nothing else, keyed\n * by id at the top level. So the output schema is the section record itself,\n * not a field added to some route's output.\n *\n * `path` picks the page, and the sections inherit its Liquid context: a\n * product section rendered against `/products/hat?variant=123` renders that\n * variant, and any query parameter the page respects (`q`, `page`) is\n * respected here too. Nothing prefixes a locale, so on a multi-language store\n * build the path from `window.Shopify.routes.root` or `location.pathname`.\n *\n * Sections are fixed when the request is built. They type the result, so the\n * output schema is made from them rather than from whatever is eventually\n * sent: a later {@link withSections} fails at decode, loudly, instead of\n * returning a record whose keys the type does not describe.\n *\n * @example\n * ```ts\n * const { header, footer } = yield* pipe(\n * renderSections({ path: \"/\", sections: [\"header\", \"footer\"] }),\n * execute,\n * );\n * ```\n */\nexport const renderSections = <const S extends string>(options: {\n\treadonly path: string;\n\treadonly sections: ReadonlyArray<S>;\n}): AjaxRequest<\n\ttypeof RenderSectionsInput.Type,\n\ttypeof RenderSectionsInput.Encoded,\n\tnever,\n\tRenderedSections<S>,\n\t{ readonly [K in S]: string | null },\n\tnever\n> =>\n\tmakeRequest({\n\t\trouteName: \"root_url\",\n\t\tpath: options.path,\n\t\tinputSchema: RenderSectionsInput,\n\t\t// Built lazily, in `execute`: `makeResponseSchema` reaches for `Element`\n\t\t// and `DOMParser`, so building it here would make merely describing a\n\t\t// request throw outside a DOM.\n\t\t//\n\t\t// The cast narrows the keys only. Given a plain `string`, the schema's\n\t\t// static type is keyed by `string`; at runtime it requires exactly the\n\t\t// ids in `options.sections`, which is what `S` is.\n\t\toutputSchema: () =>\n\t\t\tAjax.Sections.makeResponseSchema(\n\t\t\t\toptions.sections.join(\",\"),\n\t\t\t) as unknown as Schema.Codec<\n\t\t\t\tRenderedSections<S>,\n\t\t\t\t{ readonly [K in S]: string | null }\n\t\t\t>,\n\t\tinput: { sections: options.sections },\n\t\theaders: {},\n\t});\n\n/**\n * Runs the description.\n *\n * Named rather than untraced: this is the one place in the package worth a\n * span, and upstream's guidance is to reserve `Effect.fn` for genuine tracing\n * boundaries and prefer `fnUntraced` inside library internals. Transforms are\n * passed as trailing arguments because `Effect.fn` explicitly must not be\n * `.pipe`d.\n *\n * Errors are left as they come. `RequestError`, `ResponseError`, `ParseError`\n * and `HttpBodyError` are already tagged, and flattening them into a plain\n * `Error` — which this package used to do — destroyed the only machine-readable\n * thing about them.\n */\nexport const execute = Effect.fn(\"AjaxClient.execute\")(\n\tfunction* <A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(\n\t\tself: AjaxRequest<A, I, RD, B, J, SD>,\n\t) {\n\t\tconst client = yield* HttpClient.HttpClient;\n\t\tconst routes = yield* Ajax.Window.ShopifyRoutes.Routes;\n\t\tconst origin = yield* Ajax.Window.StorefrontOrigin.Origin;\n\n\t\tconst decodedInput = yield* Schema.decodeUnknownEffect(self.inputSchema)(\n\t\t\tself.input,\n\t\t);\n\n\t\t// A page path replaces the route table outright, and is then checked\n\t\t// below exactly as a route is.\n\t\tconst route =\n\t\t\tself.path !== undefined\n\t\t\t\t? self.path\n\t\t\t\t: self.routeName === \"product_url\" && \"handle\" in decodedInput\n\t\t\t\t\t? // `encodeURIComponent` rather than raw interpolation, for two\n\t\t\t\t\t\t// separate reasons. A handle of `../cart/clear.js` would resolve\n\t\t\t\t\t\t// to a different same-origin endpoint, carrying the shopper's\n\t\t\t\t\t\t// session cookies to it - and `/cart/clear.js` answers GET. And\n\t\t\t\t\t\t// `String.prototype.replace` expands `$&`, `` $` `` and `$'` in\n\t\t\t\t\t\t// the *replacement*, so a handle containing `$` rewrites the path\n\t\t\t\t\t\t// on its own. Encoding closes both: the output never contains\n\t\t\t\t\t\t// `/`, `.` is harmless without `/`, and `$` is escaped.\n\t\t\t\t\t\troutes[self.routeName].replaceAll(\n\t\t\t\t\t\t\t\":handle\",\n\t\t\t\t\t\t\tencodeURIComponent(String(decodedInput.handle)),\n\t\t\t\t\t\t)\n\t\t\t\t\t: routes[self.routeName];\n\n\t\t// Resolved rather than concatenated, and then checked.\n\t\t//\n\t\t// `new URL(route, origin)` treats the route as a path relative to the\n\t\t// origin, so the userinfo trick that `` `${origin}${route}` `` fell for\n\t\t// (\"@evil.com/collect\") lands harmlessly under the storefront. The\n\t\t// explicit origin comparison is still needed for the case `URL` honours\n\t\t// rather than neutralises: a fully absolute route like\n\t\t// \"https://evil.com/collect\".\n\t\t//\n\t\t// `origin` is \"\" outside a DOM — ajax-client's own tests run without\n\t\t// jsdom — and `new URL(route, \"\")` throws, so that case keeps the old\n\t\t// relative-URL behaviour. There is no origin to escape from.\n\t\tlet url: string;\n\t\tif (origin === \"\") {\n\t\t\turl = route;\n\t\t} else {\n\t\t\tconst resolved = yield* Effect.try({\n\t\t\t\ttry: () => new URL(route, origin),\n\t\t\t\tcatch: () =>\n\t\t\t\t\tnew UnsafeAjaxRouteError({\n\t\t\t\t\t\troute,\n\t\t\t\t\t\trouteName: self.routeName,\n\t\t\t\t\t\torigin,\n\t\t\t\t\t}),\n\t\t\t});\n\t\t\t// Off-origin is allowed, but only when the theme has said so. A\n\t\t\t// storefront fronting its cart through a proxy or an edge worker is\n\t\t\t// a real case; a route that arrives off-origin without the matching\n\t\t\t// entry is either a typo or smuggled, and both should fail.\n\t\t\tconst allowed = yield* Ajax.Window.AllowedRouteOrigins.Origins;\n\t\t\tif (\n\t\t\t\tresolved.origin !== new URL(origin).origin &&\n\t\t\t\t!allowed.includes(resolved.origin)\n\t\t\t) {\n\t\t\t\treturn yield* new UnsafeAjaxRouteError({\n\t\t\t\t\troute,\n\t\t\t\t\trouteName: self.routeName,\n\t\t\t\t\torigin,\n\t\t\t\t});\n\t\t\t}\n\t\t\turl = resolved.toString();\n\t\t}\n\n\t\tconst method = METHODS_BY_URL[self.routeName];\n\t\tconst headers = {\n\t\t\t\"X-SDK-Variant\": \"brytdesigns\",\n\t\t\t\"X-SDK-Variant-Source\": \"shopify-ajax-client\",\n\t\t\t...self.headers,\n\t\t};\n\n\t\tlet request: HttpClientRequest.HttpClientRequest;\n\n\t\t// An explicit three-way rather than POST-or-else-GET: the fallback\n\t\t// branch silently turned any future method into a GET, and made\n\t\t// `InvalidAjaxMethodError` unreachable decoration.\n\t\tif (method === METHOD.POST) {\n\t\t\trequest = yield* HttpClientRequest.post(url, {\n\t\t\t\tacceptJson: true,\n\t\t\t\theaders,\n\t\t\t}).pipe(HttpClientRequest.bodyJson(decodedInput));\n\t\t} else if (method === METHOD.GET) {\n\t\t\trequest = HttpClientRequest.get(url, {\n\t\t\t\tacceptJson: true,\n\t\t\t\theaders,\n\t\t\t}).pipe(HttpClientRequest.setUrlParams(decodedInput));\n\t\t} else {\n\t\t\treturn yield* new InvalidAjaxMethodError();\n\t\t}\n\n\t\t// Annotated before the request goes out rather than after it comes back:\n\t\t// the `tapCause` below is the whole reason this package is debuggable,\n\t\t// and a failure that does not say which route it was is most of the way\n\t\t// to useless.\n\t\t//\n\t\t// `input` rides along only when Debug logging is actually on.\n\t\t//\n\t\t// It is the decoded request body, which for a cart write carries\n\t\t// line-item `properties` (gift messages, engraving text), the cart\n\t\t// `note` and `attributes` — shopper PII. Annotations attach\n\t\t// unconditionally and surface on any log the effect emits, and\n\t\t// `MinimumLogLevel` is Info by default for anyone on the documented\n\t\t// `./effect` path, so annotating it outright logged that payload on an\n\t\t// ordinary non-debug page.\n\t\t//\n\t\t// `isEnabled` rather than a flag of our own, so the gate is the one the\n\t\t// caller already set: the Promise facades pin \"None\" (nothing logs) or\n\t\t// \"All\" with `debug: true` (body included, which is the point of asking\n\t\t// for debug), and the bare Effect path sits at Info, where Debug is off\n\t\t// and the body stays out.\n\t\tconst withBody = yield* LogLevel.isEnabled(\"Debug\");\n\t\tyield* Effect.annotateLogsScoped(\n\t\t\twithBody ? { method, route, input: decodedInput } : { method, route },\n\t\t);\n\n\t\tconst response = yield* client.execute(request);\n\n\t\tif (response.status !== 200) {\n\t\t\t// Shopify's Ajax routes answer with a JSON error body, but the things\n\t\t\t// in front of them do not: a 502 from an edge or a WAF challenge is\n\t\t\t// an HTML page, and `response.json` fails on it. Letting that failure\n\t\t\t// through would replace the error consumers are told to catch with a\n\t\t\t// `ResponseError` carrying neither the status nor the route - which\n\t\t\t// is exactly the case where knowing the route matters most.\n\t\t\tconst body = yield* response.json.pipe(\n\t\t\t\tEffect.catchCause(() => Effect.succeed({} as Partial<AjaxErrorBody>)),\n\t\t\t);\n\t\t\tconst parsed = (body ?? {}) as Partial<AjaxErrorBody>;\n\n\t\t\treturn yield* new AjaxResponseError({\n\t\t\t\t// The body is whatever the server sent. Taking `status` from it\n\t\t\t\t// unchecked lets `{\"status\":\"500\"}` put a string behind a field\n\t\t\t\t// typed `number`, so a consumer's `error.status === 422` silently\n\t\t\t\t// never matches.\n\t\t\t\tstatus: Predicate.isNumber(parsed.status)\n\t\t\t\t\t? parsed.status\n\t\t\t\t\t: response.status,\n\t\t\t\tmessage:\n\t\t\t\t\tparsed.message ?? `Request failed with status ${response.status}`,\n\t\t\t\tdescription: parsed.description,\n\t\t\t\troute,\n\t\t\t});\n\t\t}\n\n\t\tconst output = yield* HttpClientResponse.schemaBodyJson(\n\t\t\tself.outputSchema(decodedInput.sections),\n\t\t)(response);\n\n\t\tyield* Effect.logInfo(\"Request\");\n\n\t\treturn output;\n\t},\n\t// --- Transform order matters here. ---\n\t// `Effect.fn` applies these left to right, so `tapCause` has to come first\n\t// to sit *inside* the scope and the log span. Outside them, the failure\n\t// line carries neither the span nor the `method`/`route` annotations - the\n\t// precise context it exists to record.\n\tEffect.tapCause((cause) =>\n\t\t// A non-200 is an ordinary outcome on a storefront: a sold-out variant\n\t\t// answers 422 on every add-to-cart. Logging that at ERROR would put a\n\t\t// red line in every shopper's console for a case the caller is about to\n\t\t// handle. Anything else - a transport failure, a schema mismatch, a\n\t\t// defect - is genuinely unexpected and keeps ERROR.\n\t\tCause.squash(cause) instanceof AjaxResponseError\n\t\t\t? Effect.logDebug(\"Ajax request returned a non-200\", cause)\n\t\t\t: Effect.logError(\"Ajax request failed\", cause),\n\t),\n\tEffect.scoped,\n\tLoggerUtils.withNamespacedLogSpan(\"execute\"),\n);\n"],"mappings":"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"}
|
package/dist/main/index.d.ts
CHANGED
|
@@ -194,6 +194,7 @@ declare const ProductInputSchema: Schema.Struct<{
|
|
|
194
194
|
readonly handle: Schema.String;
|
|
195
195
|
}>;
|
|
196
196
|
declare const METHODS_BY_URL: {
|
|
197
|
+
readonly root_url: "get";
|
|
197
198
|
readonly products_url: "get";
|
|
198
199
|
readonly product_url: "get";
|
|
199
200
|
readonly predictive_search_url: "get";
|
|
@@ -219,6 +220,16 @@ export type TypeId = typeof TypeIdSymbol;
|
|
|
219
220
|
export interface AjaxRequest<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD> extends Pipeable.Pipeable {
|
|
220
221
|
readonly [TypeIdSymbol]: TypeId;
|
|
221
222
|
readonly routeName: ValidAjaxRoutes;
|
|
223
|
+
/**
|
|
224
|
+
* A storefront page to send the request to, in place of
|
|
225
|
+
* `routes[routeName]`.
|
|
226
|
+
*
|
|
227
|
+
* Only {@link renderSections} sets it: the Section Rendering API answers on
|
|
228
|
+
* any page, and the page is what the sections render against. It goes
|
|
229
|
+
* through the same origin check a route does, and it is not part of
|
|
230
|
+
* `input`, so it never ends up in the query string.
|
|
231
|
+
*/
|
|
232
|
+
readonly path?: string;
|
|
222
233
|
readonly inputSchema: Schema.Codec<A, I, RD>;
|
|
223
234
|
readonly outputSchema: (sections?: string) => Schema.Codec<B, J, SD>;
|
|
224
235
|
readonly input: I;
|
|
@@ -292,6 +303,53 @@ export declare const withSections: {
|
|
|
292
303
|
(sections: string | ReadonlyArray<string>): <A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(self: AjaxRequest<A, I, RD, B, J, SD>) => AjaxRequest<A, I, RD, B, J, SD>;
|
|
293
304
|
<A extends AjaxInput, I extends AjaxInputEncoded, RD, B, J, SD>(self: AjaxRequest<A, I, RD, B, J, SD>, sections: string | ReadonlyArray<string>): AjaxRequest<A, I, RD, B, J, SD>;
|
|
294
305
|
};
|
|
306
|
+
declare const RenderSectionsInput: Schema.Struct<{
|
|
307
|
+
readonly sections: Schema.decodeTo<Schema.String, Schema.$Array<Schema.String>, never, never>;
|
|
308
|
+
}>;
|
|
309
|
+
/**
|
|
310
|
+
* Sections rendered by {@link renderSections}, keyed by the ids asked for.
|
|
311
|
+
*
|
|
312
|
+
* `null` is Shopify's answer for a section that failed to render. The
|
|
313
|
+
* response is still a 200 when that happens, so it is a value to handle
|
|
314
|
+
* rather than an error.
|
|
315
|
+
*/
|
|
316
|
+
export type RenderedSections<S extends string> = {
|
|
317
|
+
readonly [K in S]: Element | null;
|
|
318
|
+
};
|
|
319
|
+
/**
|
|
320
|
+
* Renders sections through Shopify's standalone Section Rendering API.
|
|
321
|
+
*
|
|
322
|
+
* The cart routes can bundle sections into their own response, under a
|
|
323
|
+
* `sections` key beside the cart. This is the other form — `?sections=` on any
|
|
324
|
+
* storefront page — and it answers with the sections and nothing else, keyed
|
|
325
|
+
* by id at the top level. So the output schema is the section record itself,
|
|
326
|
+
* not a field added to some route's output.
|
|
327
|
+
*
|
|
328
|
+
* `path` picks the page, and the sections inherit its Liquid context: a
|
|
329
|
+
* product section rendered against `/products/hat?variant=123` renders that
|
|
330
|
+
* variant, and any query parameter the page respects (`q`, `page`) is
|
|
331
|
+
* respected here too. Nothing prefixes a locale, so on a multi-language store
|
|
332
|
+
* build the path from `window.Shopify.routes.root` or `location.pathname`.
|
|
333
|
+
*
|
|
334
|
+
* Sections are fixed when the request is built. They type the result, so the
|
|
335
|
+
* output schema is made from them rather than from whatever is eventually
|
|
336
|
+
* sent: a later {@link withSections} fails at decode, loudly, instead of
|
|
337
|
+
* returning a record whose keys the type does not describe.
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```ts
|
|
341
|
+
* const { header, footer } = yield* pipe(
|
|
342
|
+
* renderSections({ path: "/", sections: ["header", "footer"] }),
|
|
343
|
+
* execute,
|
|
344
|
+
* );
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
export declare const renderSections: <const S extends string>(options: {
|
|
348
|
+
readonly path: string;
|
|
349
|
+
readonly sections: ReadonlyArray<S>;
|
|
350
|
+
}) => AjaxRequest<typeof RenderSectionsInput.Type, typeof RenderSectionsInput.Encoded, never, RenderedSections<S>, {
|
|
351
|
+
readonly [K in S]: string | null;
|
|
352
|
+
}, never>;
|
|
295
353
|
/**
|
|
296
354
|
* Runs the description.
|
|
297
355
|
*
|
|
@@ -311,7 +369,7 @@ export declare const execute: <A extends AjaxInput, I extends AjaxInputEncoded,
|
|
|
311
369
|
export declare const layer: import("effect/Layer").Layer<import("effect/http/HttpClient").HttpClient, never, never>;
|
|
312
370
|
|
|
313
371
|
declare namespace Request$1 {
|
|
314
|
-
export { AjaxInput, AjaxInputEncoded, AjaxRequest, TypeId, ValidAjaxRoutes, execute, isAjaxRequest, makeRoute, withHeader, withHeaders, withInput, withSections };
|
|
372
|
+
export { AjaxInput, AjaxInputEncoded, AjaxRequest, RenderedSections, TypeId, ValidAjaxRoutes, execute, isAjaxRequest, makeRoute, renderSections, withHeader, withHeaders, withInput, withSections };
|
|
315
373
|
}
|
|
316
374
|
|
|
317
375
|
export {
|
package/dist/main/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { AjaxResponseError, InvalidAjaxMethodError, UnsafeAjaxRouteError } from "./errors.js";
|
|
2
|
-
import { Request_exports, execute, isAjaxRequest, makeRoute, withHeader, withHeaders, withInput, withSections } from "./Request.js";
|
|
2
|
+
import { Request_exports, execute, isAjaxRequest, makeRoute, renderSections, withHeader, withHeaders, withInput, withSections } from "./Request.js";
|
|
3
3
|
import { FetchHttpClient } from "effect/http";
|
|
4
4
|
//#region src/index.ts
|
|
5
5
|
/** Named `layer` per the v4 convention, which replaces v3's `Default`/`Live`. */
|
|
6
6
|
var layer = FetchHttpClient.layer;
|
|
7
7
|
//#endregion
|
|
8
|
-
export { AjaxResponseError, InvalidAjaxMethodError, Request_exports as Request, UnsafeAjaxRouteError, execute, isAjaxRequest, layer, makeRoute, withHeader, withHeaders, withInput, withSections };
|
|
8
|
+
export { AjaxResponseError, InvalidAjaxMethodError, Request_exports as Request, UnsafeAjaxRouteError, execute, isAjaxRequest, layer, makeRoute, renderSections, withHeader, withHeaders, withInput, withSections };
|
|
9
9
|
|
|
10
10
|
//# sourceMappingURL=index.js.map
|
package/dist/main/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["// v4 absorbed @effect/platform into the core package. These sat under\n// effect/unstable/http during the RCs and graduated to effect/http at 4.0.0,\n// so they are covered by semver now rather than exempt from it.\nimport { FetchHttpClient } from \"effect/http\";\n\n/** Named `layer` per the v4 convention, which replaces v3's `Default`/`Live`. */\nexport const layer = FetchHttpClient.layer;\n\nexport {\n\ttype AjaxErrorBody,\n\tAjaxResponseError,\n\tInvalidAjaxMethodError,\n\tUnsafeAjaxRouteError,\n} from \"./errors.js\";\nexport type {\n\tAjaxInput,\n\tAjaxInputEncoded,\n\tAjaxRequest,\n\tValidAjaxRoutes,\n} from \"./Request.js\";\nexport * as Request from \"./Request.js\";\nexport {\n\texecute,\n\tisAjaxRequest,\n\tmakeRoute,\n\twithHeader,\n\twithHeaders,\n\twithInput,\n\twithSections,\n} from \"./Request.js\";\n"],"mappings":";;;;;AAMA,IAAa,QAAQ,gBAAgB"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/index.ts"],"sourcesContent":["// v4 absorbed @effect/platform into the core package. These sat under\n// effect/unstable/http during the RCs and graduated to effect/http at 4.0.0,\n// so they are covered by semver now rather than exempt from it.\nimport { FetchHttpClient } from \"effect/http\";\n\n/** Named `layer` per the v4 convention, which replaces v3's `Default`/`Live`. */\nexport const layer = FetchHttpClient.layer;\n\nexport {\n\ttype AjaxErrorBody,\n\tAjaxResponseError,\n\tInvalidAjaxMethodError,\n\tUnsafeAjaxRouteError,\n} from \"./errors.js\";\nexport type {\n\tAjaxInput,\n\tAjaxInputEncoded,\n\tAjaxRequest,\n\tRenderedSections,\n\tValidAjaxRoutes,\n} from \"./Request.js\";\nexport * as Request from \"./Request.js\";\nexport {\n\texecute,\n\tisAjaxRequest,\n\tmakeRoute,\n\trenderSections,\n\twithHeader,\n\twithHeaders,\n\twithInput,\n\twithSections,\n} from \"./Request.js\";\n"],"mappings":";;;;;AAMA,IAAa,QAAQ,gBAAgB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brytdesigns/shopify-ajax-client",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "An AJAX cart that integrates with ALL shopify ajax apis",
|
|
5
5
|
"main": "./dist/main/index.js",
|
|
6
6
|
"types": "./dist/main/index.d.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"vitest": "^3.2.7"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@brytdesigns/shopify-utils": "3.
|
|
27
|
+
"@brytdesigns/shopify-utils": "3.3.0",
|
|
28
28
|
"effect": "4.0.3",
|
|
29
29
|
"lz-string": "^1.5.0"
|
|
30
30
|
},
|