@shopware/api-client 0.5.0 → 1.0.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.
@@ -0,0 +1,182 @@
1
+ import { components as mainComponents } from "./storeApiTypes";
2
+
3
+ export type components = mainComponents;
4
+ // & {
5
+ // schemas: schemas;
6
+ // };
7
+
8
+ export type Schemas = {
9
+ ProductListingResult: components["schemas"]["EntitySearchResult"] & {
10
+ /** @enum {string} */
11
+ apiAlias: "product_listing";
12
+ /** Contains the available sorting. These can be used to show a sorting select-box in the product listing. */
13
+ availableSortings: {
14
+ /** @enum {string} */
15
+ apiAlias: "product_sorting";
16
+ key: string;
17
+ label: string;
18
+ priority: number;
19
+ translated: {
20
+ apiAlias?: string;
21
+ key?: string;
22
+ label: string;
23
+ };
24
+ }[];
25
+ /** Contains the state of the filters. These can be used to create listing filters. */
26
+ currentFilters: {
27
+ manufacturer: string[];
28
+ navigationId: string;
29
+ price: {
30
+ /** @default 0 */
31
+ max: number;
32
+ /** @default 0 */
33
+ min: number;
34
+ };
35
+ properties: string[];
36
+ rating?: number; // TODO: [OpenAPI][ProductListingResult] - rating should be defined the same as in body of the request
37
+ search: string; // TODO: [OpenAPI][ProductListingResult] - search should be required as is required in body of the request, otherwise everywhere optional
38
+ /** @default false */
39
+ "shipping-free": boolean;
40
+ };
41
+ elements: components["schemas"]["Product"][];
42
+ /** @enum {string} */
43
+ entity?: "product";
44
+ sorting?: string;
45
+ };
46
+ SwagPaypalVaultToken: {
47
+ // TODO: [OpenAPI][SwagPaypalVaultToken] - add SwagPaypalVaultToken definition to schema
48
+ /** Format: date-time */
49
+ createdAt: string;
50
+ customer?: components["schemas"]["Customer"];
51
+ customerId: string;
52
+ id?: string;
53
+ identifier: string;
54
+ mainMapping?: components["schemas"]["SwagPaypalVaultTokenMapping"];
55
+ paymentMethod?: components["schemas"]["PaymentMethod"];
56
+ paymentMethodId: string;
57
+ /** Format: date-time */
58
+ updatedAt?: string;
59
+ };
60
+ };
61
+
62
+ export type operations = {
63
+ "register post /account/register": {
64
+ contentType?: "application/json";
65
+ accept?: "application/json";
66
+ body: {
67
+ /** Flag indicating accepted data protection */
68
+ acceptedDataProtection: boolean;
69
+ /**
70
+ * Account type of the customer which can be either `private` or `business`.
71
+ * @default private
72
+ */
73
+ accountType?: string;
74
+ /** Field can be used to store an affiliate tracking code */
75
+ affiliateCode?: string;
76
+ billingAddress: Omit<
77
+ components["schemas"]["CustomerAddress"],
78
+ "createdAt" | "id" | "customerId" | "firstName" | "lastName"
79
+ >; // TODO: [OpenAPI][register] - omit id, createdAt, customerId, firstName, lastName while creating address (or better to reverse and pick required fields)
80
+ /** Birthday day */
81
+ birthdayDay?: number;
82
+ /** Birthday month */
83
+ birthdayMonth?: number;
84
+ /** Birthday year */
85
+ birthdayYear?: number;
86
+ /** Field can be used to store a campaign tracking code */
87
+ campaignCode?: string;
88
+ /** Email of the customer. Has to be unique, unless `guest` is `true` */
89
+ email: string;
90
+ /** Customer first name. Value will be reused for shipping and billing address if not provided explicitly. */
91
+ firstName: string;
92
+ /**
93
+ * If set, will create a guest customer. Guest customers can re-use an email address and don't need a password.
94
+ * @default false
95
+ */
96
+ guest?: boolean;
97
+ /** Customer last name. Value will be reused for shipping and billing address if not provided explicitly. */
98
+ lastName: string;
99
+ /** Password for the customer. Required, unless `guest` is `true` */
100
+ password: string;
101
+ /** Id of the salutation for the customer account. Fetch options using `salutation` endpoint. */
102
+ salutationId: string;
103
+ shippingAddress?: components["schemas"]["CustomerAddress"];
104
+ /** URL of the storefront for that registration. Used in confirmation emails. Has to be one of the configured domains of the sales channel. */
105
+ storefrontUrl: string;
106
+ /** (Academic) title of the customer */
107
+ title?: string;
108
+ };
109
+ response: components["schemas"]["Customer"];
110
+ responseCode: 200;
111
+ };
112
+ "addLineItem post /checkout/cart/line-item": {
113
+ contentType?: "application/json";
114
+ accept?: "application/json";
115
+ headers?: {
116
+ /** Instructs Shopware to return the response in the given language. */
117
+ "sw-language-id"?: string;
118
+ };
119
+ body: {
120
+ // TODO: [OpenAPI][addLineItem] - add proper request body type with required fields
121
+ items: Array<{
122
+ id?: string; // TODO: [OpenAPI][addLineItem] - check if this is used at all?
123
+ referencedId: string;
124
+ quantity?: number;
125
+ type: "product" | "promotion" | "custom" | "credit"; // TODO: [OpenAPI][addLineItem] - add proper type -> see also #456
126
+ }>;
127
+ };
128
+ response: components["schemas"]["Cart"];
129
+ responseCode: 200;
130
+ };
131
+ "updateLineItem patch /checkout/cart/line-item": {
132
+ contentType?: "application/json";
133
+ accept?: "application/json";
134
+ headers?: {
135
+ /** Instructs Shopware to return the response in the given language. */
136
+ "sw-language-id"?: string;
137
+ };
138
+ body: {
139
+ // TODO: [OpenAPI][updateLineItem] - add proper request body type with required fields
140
+ items: Array<{
141
+ id: string;
142
+ quantity: number;
143
+ }>;
144
+ };
145
+ response: components["schemas"]["Cart"];
146
+ responseCode: 200;
147
+ };
148
+ "readProduct post /product": {
149
+ contentType?: "application/json";
150
+ accept?: "application/json";
151
+ headers?: {
152
+ /** Instructs Shopware to return the response in the given language. */
153
+ "sw-language-id"?: string;
154
+ };
155
+ body?: components["schemas"]["Criteria"];
156
+ response: {
157
+ elements: components["schemas"]["Product"][]; // TODO: [OpenAPI][readProduct]: add elements property as required
158
+ } & components["schemas"]["EntitySearchResult"];
159
+ responseCode: 200;
160
+ };
161
+ "readShippingMethod post /shipping-method": {
162
+ contentType?: "application/json";
163
+ accept?: "application/json";
164
+ headers?: {
165
+ /** Instructs Shopware to return the response in the given language. */
166
+ "sw-language-id"?: string;
167
+ };
168
+ query?: {
169
+ /** List only available shipping methods. This filters shipping methods methods which can not be used in the actual context because of their availability rule. */
170
+ onlyAvailable?: boolean;
171
+ };
172
+ body?: components["schemas"]["Criteria"];
173
+ response: {
174
+ /** aggregation result */
175
+ aggregations?: Record<string, never>;
176
+ elements: components["schemas"]["ShippingMethod"][]; // TODO: [OpenAPI][readShippingMethod]: response should be `EntitySearchResult` and elements should be required
177
+ /** Total amount */
178
+ total?: number;
179
+ };
180
+ responseCode: 200;
181
+ };
182
+ };
package/dist/index.cjs CHANGED
@@ -1,6 +1,47 @@
1
1
  'use strict';
2
2
 
3
3
  const ofetch = require('ofetch');
4
+ const hookable = require('hookable');
5
+ const defu = require('defu');
6
+
7
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
8
+
9
+ const defu__default = /*#__PURE__*/_interopDefaultCompat(defu);
10
+
11
+ function createHeaders(init) {
12
+ const _headers = {
13
+ "Content-Type": "application/json"
14
+ };
15
+ const handler = {
16
+ get: (target, prop) => {
17
+ if (prop === "apply") {
18
+ return apply;
19
+ }
20
+ return Reflect.get(target, prop);
21
+ },
22
+ set: (target, prop, value) => {
23
+ if (prop === "apply") {
24
+ throw new Error("Cannot override apply method");
25
+ }
26
+ return Reflect.set(target, prop, value);
27
+ }
28
+ };
29
+ const headersProxy = new Proxy(
30
+ _headers,
31
+ handler
32
+ );
33
+ function apply(headers) {
34
+ for (const [key, value] of Object.entries(headers)) {
35
+ if (value) {
36
+ headersProxy[key] = value;
37
+ } else {
38
+ delete headersProxy[key];
39
+ }
40
+ }
41
+ }
42
+ headersProxy.apply({ ...init });
43
+ return headersProxy;
44
+ }
4
45
 
5
46
  var __defProp = Object.defineProperty;
6
47
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -11,14 +52,23 @@ var __publicField = (obj, key, value) => {
11
52
  class ApiClientError extends Error {
12
53
  constructor(response) {
13
54
  let message = "Failed request";
14
- message += response._data?.errors.reduce((message2, error) => {
55
+ const errorDetails = response._data || {
56
+ errors: [
57
+ {
58
+ title: "Unknown error",
59
+ detail: "API did not return errors, but request failed. Please check the network tab."
60
+ }
61
+ ]
62
+ };
63
+ message += errorDetails.errors?.reduce((message2, error) => {
15
64
  let pointer = "";
16
65
  if (error.source?.pointer) {
17
66
  pointer = `[${error.source.pointer}]`;
18
67
  }
68
+ const details = error.detail ?? "No error details provided.";
19
69
  return `${message2}
20
- - [${error.title}]${pointer} ${error.detail ?? ""}`;
21
- }, "");
70
+ - [${error.title}]${pointer} ${details}`;
71
+ }, "") ?? "";
22
72
  super(message);
23
73
  /**
24
74
  * Flag to indicate if the request was successful.
@@ -45,9 +95,7 @@ class ApiClientError extends Error {
45
95
  */
46
96
  __publicField(this, "headers");
47
97
  this.name = "ApiClientError";
48
- this.details = response._data || {
49
- errors: [{ title: "Unknown error", detail: "" }]
50
- };
98
+ this.details = errorDetails;
51
99
  this.ok = response.ok;
52
100
  this.status = response.status;
53
101
  this.statusText = response.statusText;
@@ -55,188 +103,203 @@ class ApiClientError extends Error {
55
103
  this.headers = response.headers;
56
104
  }
57
105
  }
106
+
58
107
  function errorInterceptor(response) {
59
108
  throw new ApiClientError(response);
60
109
  }
61
110
 
62
- function transformPathToQuery(path, params) {
63
- const [, method, pathDefinition, headerParams] = path.split(" ");
64
- const [requestPath, queryParams] = pathDefinition.split("?");
65
- const pathParams = requestPath.match(/{[^}]+}/g)?.map((param) => param.substring(1, param.length - 1)) || [];
66
- const requestPathWithParams = pathParams.reduce((acc, paramName) => {
67
- return acc.replace(`{${paramName}}`, params[paramName]);
111
+ function createPathWithParams(requestPath, pathParams) {
112
+ return Object.keys(pathParams || {}).reduce((acc, paramName) => {
113
+ return acc.replace(`{${paramName}}`, pathParams[paramName]);
68
114
  }, requestPath);
69
- const queryParamNames = queryParams?.split(",") || [];
70
- const headerParamnames = headerParams?.split(",") || [];
71
- const headers = {};
72
- headerParamnames.forEach((paramName) => {
73
- headers[paramName] = params[paramName];
74
- });
75
- const query = {};
76
- queryParamNames.forEach((paramName) => {
77
- let queryParamName = paramName;
78
- if (Array.isArray(params[paramName]) && !queryParamName.includes("[]")) {
79
- queryParamName += "[]";
80
- }
81
- query[queryParamName] = params[paramName];
82
- });
83
- const returnOptions = {
84
- method: method.toUpperCase(),
85
- headers,
86
- query
87
- };
88
- if (!params) {
89
- return [requestPathWithParams, returnOptions];
90
- }
91
- Object.keys(params).forEach((key) => {
92
- if (!pathParams.includes(key) && !queryParamNames.includes(key) && !headerParamnames.includes(key)) {
93
- returnOptions.body ?? (returnOptions.body = {});
94
- Reflect.set(returnOptions.body, key, params[key]);
95
- }
96
- });
97
- return [requestPathWithParams, returnOptions];
98
115
  }
99
116
 
100
117
  function createAPIClient(params) {
101
- const defaultHeaders = {
102
- "sw-access-key": params.accessToken
103
- };
104
- if (params.contextToken) {
105
- defaultHeaders["sw-context-token"] = params.contextToken;
106
- }
118
+ const defaultHeaders = createHeaders({
119
+ "sw-access-key": params.accessToken,
120
+ Accept: "application/json",
121
+ "sw-context-token": params.contextToken,
122
+ ...params.defaultHeaders
123
+ });
124
+ const apiClientHooks = hookable.createHooks();
107
125
  const apiFetch = ofetch.ofetch.create({
108
126
  baseURL: params.baseURL,
109
127
  // async onRequest({ request, options }) {},
110
128
  // async onRequestError({ request, options, error }) {},
111
129
  async onResponse(context) {
112
- if (defaultHeaders["sw-context-token"] !== context.response.headers.get("sw-context-token")) {
113
- const newContextToken = context.response.headers.get("sw-context-token") || "";
130
+ apiClientHooks.callHook("onSuccessResponse", context.response);
131
+ if (context.response.headers.has("sw-context-token") && defaultHeaders["sw-context-token"] !== context.response.headers.get("sw-context-token")) {
132
+ const newContextToken = context.response.headers.get(
133
+ "sw-context-token"
134
+ );
114
135
  defaultHeaders["sw-context-token"] = newContextToken;
115
- params.onContextChanged?.(newContextToken);
136
+ apiClientHooks.callHook("onContextChanged", newContextToken);
116
137
  }
117
138
  },
118
- async onResponseError({ request, response, options }) {
139
+ async onResponseError({ response }) {
140
+ apiClientHooks.callHook("onResponseError", response);
119
141
  errorInterceptor(response);
120
142
  }
121
143
  });
122
144
  async function invoke(pathParam, ...params2) {
123
- const [requestPath, options] = transformPathToQuery(
124
- pathParam,
125
- params2?.[0]
126
- );
127
- return apiFetch(
145
+ const [, method, requestPath] = pathParam.split(" ");
146
+ const currentParams = params2[0] || {};
147
+ const requestPathWithParams = createPathWithParams(
128
148
  requestPath,
129
- {
130
- ...options,
131
- headers: {
132
- ...defaultHeaders,
133
- ...options.headers
134
- }
135
- }
149
+ currentParams.pathParams
136
150
  );
151
+ const fetchOptions = {
152
+ ...currentParams.fetchOptions || {}
153
+ };
154
+ const resp = await apiFetch.raw(requestPathWithParams, {
155
+ ...fetchOptions,
156
+ method,
157
+ body: currentParams.body,
158
+ headers: defu__default(defaultHeaders, currentParams.headers),
159
+ query: currentParams.query
160
+ });
161
+ return {
162
+ data: resp._data,
163
+ status: resp.status
164
+ };
137
165
  }
138
166
  return {
139
- invoke
167
+ invoke,
168
+ /**
169
+ * Default headers used in every client request (if not overriden in specific request).
170
+ */
171
+ defaultHeaders,
172
+ hook: apiClientHooks.hook
140
173
  };
141
174
  }
175
+
142
176
  function createAuthorizationHeader(token) {
143
177
  if (!token)
144
- return null;
178
+ return "";
145
179
  if (token.startsWith("Bearer "))
146
180
  return token;
147
181
  return `Bearer ${token}`;
148
182
  }
149
183
  function createAdminAPIClient(params) {
184
+ const isTokenBasedAuth = params.credentials?.grant_type === "client_credentials";
150
185
  const sessionData = {
151
186
  accessToken: params.sessionData?.accessToken || "",
152
187
  refreshToken: params.sessionData?.refreshToken || "",
153
188
  expirationTime: Number(params.sessionData?.expirationTime || 0)
154
189
  };
190
+ const defaultHeaders = createHeaders({
191
+ Authorization: createAuthorizationHeader(sessionData.accessToken),
192
+ Accept: "application/json"
193
+ });
194
+ const apiClientHooks = hookable.createHooks();
195
+ function getSessionData() {
196
+ return { ...sessionData };
197
+ }
198
+ function setSessionData(data) {
199
+ sessionData.accessToken = data.accessToken;
200
+ sessionData.refreshToken = data.refreshToken || "";
201
+ sessionData.expirationTime = data.expirationTime;
202
+ return getSessionData();
203
+ }
155
204
  function updateSessionData(responseData) {
156
205
  if (responseData?.access_token) {
157
206
  defaultHeaders.Authorization = createAuthorizationHeader(
158
207
  responseData.access_token
159
208
  );
160
- sessionData.accessToken = responseData.access_token;
161
- sessionData.refreshToken = responseData.refresh_token;
162
- sessionData.expirationTime = Date.now() + responseData.expires_in * 1e3;
163
- params.onAuthChange?.({
164
- ...sessionData
209
+ const dataCopy = setSessionData({
210
+ accessToken: responseData.access_token,
211
+ refreshToken: responseData.refresh_token,
212
+ expirationTime: Date.now() + responseData.expires_in * 1e3
165
213
  });
214
+ apiClientHooks.callHook("onAuthChange", dataCopy);
166
215
  }
167
216
  }
168
- function setSessionData(data) {
169
- sessionData.accessToken = data.accessToken;
170
- sessionData.refreshToken = data.refreshToken;
171
- sessionData.expirationTime = data.expirationTime;
172
- return getSessionData();
173
- }
174
- function getSessionData() {
175
- return { ...sessionData };
176
- }
177
- const defaultHeaders = {
178
- Authorization: createAuthorizationHeader(sessionData.accessToken)
179
- };
180
217
  const apiFetch = ofetch.ofetch.create({
181
218
  baseURL: params.baseURL,
182
219
  async onRequest({ request, options }) {
183
220
  const isExpired = sessionData.expirationTime <= Date.now();
184
221
  if (isExpired && !request.toString().includes("/oauth/token")) {
222
+ if (!params.credentials && !isTokenBasedAuth && !sessionData.refreshToken) {
223
+ console.warn(
224
+ "[ApiClientWarning] No `credentials` or `sessionData` provided. Provide at least one of them to ensure authentication."
225
+ );
226
+ }
227
+ const body = params.credentials && !sessionData.refreshToken ? params.credentials : {
228
+ grant_type: "refresh_token",
229
+ client_id: "administration",
230
+ refresh_token: sessionData.refreshToken
231
+ };
185
232
  await ofetch.ofetch("/oauth/token", {
186
233
  baseURL: params.baseURL,
187
234
  method: "POST",
188
- body: {
189
- grant_type: "refresh_token",
190
- client_id: "administration",
191
- refresh_token: sessionData.refreshToken || ""
192
- },
235
+ body,
193
236
  headers: defaultHeaders,
194
237
  onResponseError({ response }) {
195
238
  errorInterceptor(response);
196
239
  },
197
240
  onResponse(context) {
241
+ if (!context.response._data)
242
+ return;
198
243
  updateSessionData(context.response._data);
244
+ options.headers = {
245
+ ...options.headers,
246
+ Authorization: createAuthorizationHeader(
247
+ context.response._data.access_token
248
+ )
249
+ };
199
250
  }
200
251
  });
201
252
  }
202
253
  },
203
254
  async onResponse(context) {
255
+ apiClientHooks.callHook("onSuccessResponse", context.response);
204
256
  updateSessionData(context.response._data);
205
257
  },
206
- async onResponseError({ request, response, options }) {
258
+ async onResponseError({ response }) {
259
+ apiClientHooks.callHook("onResponseError", response);
207
260
  errorInterceptor(response);
208
261
  }
209
262
  });
210
- async function invoke(pathParam, params2) {
211
- const [requestPath, options] = transformPathToQuery(
212
- pathParam,
213
- params2
214
- );
215
- return apiFetch(
263
+ async function invoke(pathParam, ...params2) {
264
+ const [, method, requestPath] = pathParam.split(" ");
265
+ const requestPathWithParams = createPathWithParams(
216
266
  requestPath,
217
- {
218
- ...options,
219
- headers: {
220
- ...defaultHeaders,
221
- ...options.headers
222
- }
223
- }
267
+ params2[0]?.pathParams
224
268
  );
269
+ const fetchOptions = {
270
+ ...params2[0]?.fetchOptions || {}
271
+ };
272
+ const resp = await apiFetch.raw(requestPathWithParams, {
273
+ ...fetchOptions,
274
+ method,
275
+ body: params2[0]?.body,
276
+ headers: defu__default(defaultHeaders, params2[0]?.headers),
277
+ query: params2[0]?.query
278
+ });
279
+ return {
280
+ data: resp._data,
281
+ status: resp.status
282
+ };
225
283
  }
226
284
  return {
227
- /**
228
- * Invoke API request based on provided path definition.
229
- */
230
285
  invoke,
231
286
  /**
232
287
  * Enables to change session data in runtime. Useful for testing purposes.
233
- * Setting session data with this methis will **not** fire `onAuthChange` hook.
288
+ * Setting session data with this method will **not** fire `onAuthChange` hook.
234
289
  */
235
290
  setSessionData,
236
291
  /**
237
292
  * Returns current session data. Useful for testing purposes, as in most cases you'll want to use `onAuthChange` hook for that.
238
293
  */
239
- getSessionData
294
+ getSessionData,
295
+ /**
296
+ * Default headers used in every client request (if not overriden in specific request).
297
+ */
298
+ defaultHeaders,
299
+ /**
300
+ * Available hooks for the client.
301
+ */
302
+ hook: apiClientHooks.hook
240
303
  };
241
304
  }
242
305