@shopware/api-client 0.4.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,185 +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
- Object.keys(params).forEach((key) => {
89
- if (!pathParams.includes(key) && !queryParamNames.includes(key) && !headerParamnames.includes(key)) {
90
- returnOptions.body ?? (returnOptions.body = {});
91
- Reflect.set(returnOptions.body, key, params[key]);
92
- }
93
- });
94
- return [requestPathWithParams, returnOptions];
95
115
  }
96
116
 
97
117
  function createAPIClient(params) {
98
- const defaultHeaders = {
99
- "sw-access-key": params.accessToken
100
- };
101
- if (params.contextToken) {
102
- defaultHeaders["sw-context-token"] = params.contextToken;
103
- }
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();
104
125
  const apiFetch = ofetch.ofetch.create({
105
126
  baseURL: params.baseURL,
106
127
  // async onRequest({ request, options }) {},
107
128
  // async onRequestError({ request, options, error }) {},
108
129
  async onResponse(context) {
109
- if (defaultHeaders["sw-context-token"] !== context.response.headers.get("sw-context-token")) {
110
- 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
+ );
111
135
  defaultHeaders["sw-context-token"] = newContextToken;
112
- params.onContextChanged?.(newContextToken);
136
+ apiClientHooks.callHook("onContextChanged", newContextToken);
113
137
  }
114
138
  },
115
- async onResponseError({ request, response, options }) {
139
+ async onResponseError({ response }) {
140
+ apiClientHooks.callHook("onResponseError", response);
116
141
  errorInterceptor(response);
117
142
  }
118
143
  });
119
- async function invoke(pathParam, params2) {
120
- const [requestPath, options] = transformPathToQuery(
121
- pathParam,
122
- params2
123
- );
124
- return apiFetch(
144
+ async function invoke(pathParam, ...params2) {
145
+ const [, method, requestPath] = pathParam.split(" ");
146
+ const currentParams = params2[0] || {};
147
+ const requestPathWithParams = createPathWithParams(
125
148
  requestPath,
126
- {
127
- ...options,
128
- headers: {
129
- ...defaultHeaders,
130
- ...options.headers
131
- }
132
- }
149
+ currentParams.pathParams
133
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
+ };
134
165
  }
135
166
  return {
136
- 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
137
173
  };
138
174
  }
175
+
139
176
  function createAuthorizationHeader(token) {
140
177
  if (!token)
141
- return null;
178
+ return "";
142
179
  if (token.startsWith("Bearer "))
143
180
  return token;
144
181
  return `Bearer ${token}`;
145
182
  }
146
183
  function createAdminAPIClient(params) {
184
+ const isTokenBasedAuth = params.credentials?.grant_type === "client_credentials";
147
185
  const sessionData = {
148
186
  accessToken: params.sessionData?.accessToken || "",
149
187
  refreshToken: params.sessionData?.refreshToken || "",
150
188
  expirationTime: Number(params.sessionData?.expirationTime || 0)
151
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
+ }
152
204
  function updateSessionData(responseData) {
153
205
  if (responseData?.access_token) {
154
206
  defaultHeaders.Authorization = createAuthorizationHeader(
155
207
  responseData.access_token
156
208
  );
157
- sessionData.accessToken = responseData.access_token;
158
- sessionData.refreshToken = responseData.refresh_token;
159
- sessionData.expirationTime = Date.now() + responseData.expires_in * 1e3;
160
- params.onAuthChange?.({
161
- ...sessionData
209
+ const dataCopy = setSessionData({
210
+ accessToken: responseData.access_token,
211
+ refreshToken: responseData.refresh_token,
212
+ expirationTime: Date.now() + responseData.expires_in * 1e3
162
213
  });
214
+ apiClientHooks.callHook("onAuthChange", dataCopy);
163
215
  }
164
216
  }
165
- function setSessionData(data) {
166
- sessionData.accessToken = data.accessToken;
167
- sessionData.refreshToken = data.refreshToken;
168
- sessionData.expirationTime = data.expirationTime;
169
- return getSessionData();
170
- }
171
- function getSessionData() {
172
- return { ...sessionData };
173
- }
174
- const defaultHeaders = {
175
- Authorization: createAuthorizationHeader(sessionData.accessToken)
176
- };
177
217
  const apiFetch = ofetch.ofetch.create({
178
218
  baseURL: params.baseURL,
179
219
  async onRequest({ request, options }) {
180
220
  const isExpired = sessionData.expirationTime <= Date.now();
181
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
+ };
182
232
  await ofetch.ofetch("/oauth/token", {
183
233
  baseURL: params.baseURL,
184
234
  method: "POST",
185
- body: {
186
- grant_type: "refresh_token",
187
- client_id: "administration",
188
- refresh_token: sessionData.refreshToken || ""
189
- },
235
+ body,
190
236
  headers: defaultHeaders,
191
237
  onResponseError({ response }) {
192
238
  errorInterceptor(response);
193
239
  },
194
240
  onResponse(context) {
241
+ if (!context.response._data)
242
+ return;
195
243
  updateSessionData(context.response._data);
244
+ options.headers = {
245
+ ...options.headers,
246
+ Authorization: createAuthorizationHeader(
247
+ context.response._data.access_token
248
+ )
249
+ };
196
250
  }
197
251
  });
198
252
  }
199
253
  },
200
254
  async onResponse(context) {
255
+ apiClientHooks.callHook("onSuccessResponse", context.response);
201
256
  updateSessionData(context.response._data);
202
257
  },
203
- async onResponseError({ request, response, options }) {
258
+ async onResponseError({ response }) {
259
+ apiClientHooks.callHook("onResponseError", response);
204
260
  errorInterceptor(response);
205
261
  }
206
262
  });
207
- async function invoke(pathParam, params2) {
208
- const [requestPath, options] = transformPathToQuery(
209
- pathParam,
210
- params2
211
- );
212
- return apiFetch(
263
+ async function invoke(pathParam, ...params2) {
264
+ const [, method, requestPath] = pathParam.split(" ");
265
+ const requestPathWithParams = createPathWithParams(
213
266
  requestPath,
214
- {
215
- ...options,
216
- headers: {
217
- ...defaultHeaders,
218
- ...options.headers
219
- }
220
- }
267
+ params2[0]?.pathParams
221
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
+ };
222
283
  }
223
284
  return {
224
- /**
225
- * Invoke API request based on provided path definition.
226
- */
227
285
  invoke,
228
286
  /**
229
287
  * Enables to change session data in runtime. Useful for testing purposes.
230
- * Setting session data with this methis will **not** fire `onAuthChange` hook.
288
+ * Setting session data with this method will **not** fire `onAuthChange` hook.
231
289
  */
232
290
  setSessionData,
233
291
  /**
234
292
  * Returns current session data. Useful for testing purposes, as in most cases you'll want to use `onAuthChange` hook for that.
235
293
  */
236
- 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
237
303
  };
238
304
  }
239
305