@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.
package/dist/index.mjs CHANGED
@@ -1,4 +1,41 @@
1
1
  import { ofetch } from 'ofetch';
2
+ import { createHooks } from 'hookable';
3
+ import defu from 'defu';
4
+
5
+ function createHeaders(init) {
6
+ const _headers = {
7
+ "Content-Type": "application/json"
8
+ };
9
+ const handler = {
10
+ get: (target, prop) => {
11
+ if (prop === "apply") {
12
+ return apply;
13
+ }
14
+ return Reflect.get(target, prop);
15
+ },
16
+ set: (target, prop, value) => {
17
+ if (prop === "apply") {
18
+ throw new Error("Cannot override apply method");
19
+ }
20
+ return Reflect.set(target, prop, value);
21
+ }
22
+ };
23
+ const headersProxy = new Proxy(
24
+ _headers,
25
+ handler
26
+ );
27
+ function apply(headers) {
28
+ for (const [key, value] of Object.entries(headers)) {
29
+ if (value) {
30
+ headersProxy[key] = value;
31
+ } else {
32
+ delete headersProxy[key];
33
+ }
34
+ }
35
+ }
36
+ headersProxy.apply({ ...init });
37
+ return headersProxy;
38
+ }
2
39
 
3
40
  var __defProp = Object.defineProperty;
4
41
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -9,14 +46,23 @@ var __publicField = (obj, key, value) => {
9
46
  class ApiClientError extends Error {
10
47
  constructor(response) {
11
48
  let message = "Failed request";
12
- message += response._data?.errors.reduce((message2, error) => {
49
+ const errorDetails = response._data || {
50
+ errors: [
51
+ {
52
+ title: "Unknown error",
53
+ detail: "API did not return errors, but request failed. Please check the network tab."
54
+ }
55
+ ]
56
+ };
57
+ message += errorDetails.errors?.reduce((message2, error) => {
13
58
  let pointer = "";
14
59
  if (error.source?.pointer) {
15
60
  pointer = `[${error.source.pointer}]`;
16
61
  }
62
+ const details = error.detail ?? "No error details provided.";
17
63
  return `${message2}
18
- - [${error.title}]${pointer} ${error.detail ?? ""}`;
19
- }, "");
64
+ - [${error.title}]${pointer} ${details}`;
65
+ }, "") ?? "";
20
66
  super(message);
21
67
  /**
22
68
  * Flag to indicate if the request was successful.
@@ -43,9 +89,7 @@ class ApiClientError extends Error {
43
89
  */
44
90
  __publicField(this, "headers");
45
91
  this.name = "ApiClientError";
46
- this.details = response._data || {
47
- errors: [{ title: "Unknown error", detail: "" }]
48
- };
92
+ this.details = errorDetails;
49
93
  this.ok = response.ok;
50
94
  this.status = response.status;
51
95
  this.statusText = response.statusText;
@@ -53,188 +97,203 @@ class ApiClientError extends Error {
53
97
  this.headers = response.headers;
54
98
  }
55
99
  }
100
+
56
101
  function errorInterceptor(response) {
57
102
  throw new ApiClientError(response);
58
103
  }
59
104
 
60
- function transformPathToQuery(path, params) {
61
- const [, method, pathDefinition, headerParams] = path.split(" ");
62
- const [requestPath, queryParams] = pathDefinition.split("?");
63
- const pathParams = requestPath.match(/{[^}]+}/g)?.map((param) => param.substring(1, param.length - 1)) || [];
64
- const requestPathWithParams = pathParams.reduce((acc, paramName) => {
65
- return acc.replace(`{${paramName}}`, params[paramName]);
105
+ function createPathWithParams(requestPath, pathParams) {
106
+ return Object.keys(pathParams || {}).reduce((acc, paramName) => {
107
+ return acc.replace(`{${paramName}}`, pathParams[paramName]);
66
108
  }, requestPath);
67
- const queryParamNames = queryParams?.split(",") || [];
68
- const headerParamnames = headerParams?.split(",") || [];
69
- const headers = {};
70
- headerParamnames.forEach((paramName) => {
71
- headers[paramName] = params[paramName];
72
- });
73
- const query = {};
74
- queryParamNames.forEach((paramName) => {
75
- let queryParamName = paramName;
76
- if (Array.isArray(params[paramName]) && !queryParamName.includes("[]")) {
77
- queryParamName += "[]";
78
- }
79
- query[queryParamName] = params[paramName];
80
- });
81
- const returnOptions = {
82
- method: method.toUpperCase(),
83
- headers,
84
- query
85
- };
86
- if (!params) {
87
- return [requestPathWithParams, returnOptions];
88
- }
89
- Object.keys(params).forEach((key) => {
90
- if (!pathParams.includes(key) && !queryParamNames.includes(key) && !headerParamnames.includes(key)) {
91
- returnOptions.body ?? (returnOptions.body = {});
92
- Reflect.set(returnOptions.body, key, params[key]);
93
- }
94
- });
95
- return [requestPathWithParams, returnOptions];
96
109
  }
97
110
 
98
111
  function createAPIClient(params) {
99
- const defaultHeaders = {
100
- "sw-access-key": params.accessToken
101
- };
102
- if (params.contextToken) {
103
- defaultHeaders["sw-context-token"] = params.contextToken;
104
- }
112
+ const defaultHeaders = createHeaders({
113
+ "sw-access-key": params.accessToken,
114
+ Accept: "application/json",
115
+ "sw-context-token": params.contextToken,
116
+ ...params.defaultHeaders
117
+ });
118
+ const apiClientHooks = createHooks();
105
119
  const apiFetch = ofetch.create({
106
120
  baseURL: params.baseURL,
107
121
  // async onRequest({ request, options }) {},
108
122
  // async onRequestError({ request, options, error }) {},
109
123
  async onResponse(context) {
110
- if (defaultHeaders["sw-context-token"] !== context.response.headers.get("sw-context-token")) {
111
- const newContextToken = context.response.headers.get("sw-context-token") || "";
124
+ apiClientHooks.callHook("onSuccessResponse", context.response);
125
+ if (context.response.headers.has("sw-context-token") && defaultHeaders["sw-context-token"] !== context.response.headers.get("sw-context-token")) {
126
+ const newContextToken = context.response.headers.get(
127
+ "sw-context-token"
128
+ );
112
129
  defaultHeaders["sw-context-token"] = newContextToken;
113
- params.onContextChanged?.(newContextToken);
130
+ apiClientHooks.callHook("onContextChanged", newContextToken);
114
131
  }
115
132
  },
116
- async onResponseError({ request, response, options }) {
133
+ async onResponseError({ response }) {
134
+ apiClientHooks.callHook("onResponseError", response);
117
135
  errorInterceptor(response);
118
136
  }
119
137
  });
120
138
  async function invoke(pathParam, ...params2) {
121
- const [requestPath, options] = transformPathToQuery(
122
- pathParam,
123
- params2?.[0]
124
- );
125
- return apiFetch(
139
+ const [, method, requestPath] = pathParam.split(" ");
140
+ const currentParams = params2[0] || {};
141
+ const requestPathWithParams = createPathWithParams(
126
142
  requestPath,
127
- {
128
- ...options,
129
- headers: {
130
- ...defaultHeaders,
131
- ...options.headers
132
- }
133
- }
143
+ currentParams.pathParams
134
144
  );
145
+ const fetchOptions = {
146
+ ...currentParams.fetchOptions || {}
147
+ };
148
+ const resp = await apiFetch.raw(requestPathWithParams, {
149
+ ...fetchOptions,
150
+ method,
151
+ body: currentParams.body,
152
+ headers: defu(defaultHeaders, currentParams.headers),
153
+ query: currentParams.query
154
+ });
155
+ return {
156
+ data: resp._data,
157
+ status: resp.status
158
+ };
135
159
  }
136
160
  return {
137
- invoke
161
+ invoke,
162
+ /**
163
+ * Default headers used in every client request (if not overriden in specific request).
164
+ */
165
+ defaultHeaders,
166
+ hook: apiClientHooks.hook
138
167
  };
139
168
  }
169
+
140
170
  function createAuthorizationHeader(token) {
141
171
  if (!token)
142
- return null;
172
+ return "";
143
173
  if (token.startsWith("Bearer "))
144
174
  return token;
145
175
  return `Bearer ${token}`;
146
176
  }
147
177
  function createAdminAPIClient(params) {
178
+ const isTokenBasedAuth = params.credentials?.grant_type === "client_credentials";
148
179
  const sessionData = {
149
180
  accessToken: params.sessionData?.accessToken || "",
150
181
  refreshToken: params.sessionData?.refreshToken || "",
151
182
  expirationTime: Number(params.sessionData?.expirationTime || 0)
152
183
  };
184
+ const defaultHeaders = createHeaders({
185
+ Authorization: createAuthorizationHeader(sessionData.accessToken),
186
+ Accept: "application/json"
187
+ });
188
+ const apiClientHooks = createHooks();
189
+ function getSessionData() {
190
+ return { ...sessionData };
191
+ }
192
+ function setSessionData(data) {
193
+ sessionData.accessToken = data.accessToken;
194
+ sessionData.refreshToken = data.refreshToken || "";
195
+ sessionData.expirationTime = data.expirationTime;
196
+ return getSessionData();
197
+ }
153
198
  function updateSessionData(responseData) {
154
199
  if (responseData?.access_token) {
155
200
  defaultHeaders.Authorization = createAuthorizationHeader(
156
201
  responseData.access_token
157
202
  );
158
- sessionData.accessToken = responseData.access_token;
159
- sessionData.refreshToken = responseData.refresh_token;
160
- sessionData.expirationTime = Date.now() + responseData.expires_in * 1e3;
161
- params.onAuthChange?.({
162
- ...sessionData
203
+ const dataCopy = setSessionData({
204
+ accessToken: responseData.access_token,
205
+ refreshToken: responseData.refresh_token,
206
+ expirationTime: Date.now() + responseData.expires_in * 1e3
163
207
  });
208
+ apiClientHooks.callHook("onAuthChange", dataCopy);
164
209
  }
165
210
  }
166
- function setSessionData(data) {
167
- sessionData.accessToken = data.accessToken;
168
- sessionData.refreshToken = data.refreshToken;
169
- sessionData.expirationTime = data.expirationTime;
170
- return getSessionData();
171
- }
172
- function getSessionData() {
173
- return { ...sessionData };
174
- }
175
- const defaultHeaders = {
176
- Authorization: createAuthorizationHeader(sessionData.accessToken)
177
- };
178
211
  const apiFetch = ofetch.create({
179
212
  baseURL: params.baseURL,
180
213
  async onRequest({ request, options }) {
181
214
  const isExpired = sessionData.expirationTime <= Date.now();
182
215
  if (isExpired && !request.toString().includes("/oauth/token")) {
216
+ if (!params.credentials && !isTokenBasedAuth && !sessionData.refreshToken) {
217
+ console.warn(
218
+ "[ApiClientWarning] No `credentials` or `sessionData` provided. Provide at least one of them to ensure authentication."
219
+ );
220
+ }
221
+ const body = params.credentials && !sessionData.refreshToken ? params.credentials : {
222
+ grant_type: "refresh_token",
223
+ client_id: "administration",
224
+ refresh_token: sessionData.refreshToken
225
+ };
183
226
  await ofetch("/oauth/token", {
184
227
  baseURL: params.baseURL,
185
228
  method: "POST",
186
- body: {
187
- grant_type: "refresh_token",
188
- client_id: "administration",
189
- refresh_token: sessionData.refreshToken || ""
190
- },
229
+ body,
191
230
  headers: defaultHeaders,
192
231
  onResponseError({ response }) {
193
232
  errorInterceptor(response);
194
233
  },
195
234
  onResponse(context) {
235
+ if (!context.response._data)
236
+ return;
196
237
  updateSessionData(context.response._data);
238
+ options.headers = {
239
+ ...options.headers,
240
+ Authorization: createAuthorizationHeader(
241
+ context.response._data.access_token
242
+ )
243
+ };
197
244
  }
198
245
  });
199
246
  }
200
247
  },
201
248
  async onResponse(context) {
249
+ apiClientHooks.callHook("onSuccessResponse", context.response);
202
250
  updateSessionData(context.response._data);
203
251
  },
204
- async onResponseError({ request, response, options }) {
252
+ async onResponseError({ response }) {
253
+ apiClientHooks.callHook("onResponseError", response);
205
254
  errorInterceptor(response);
206
255
  }
207
256
  });
208
- async function invoke(pathParam, params2) {
209
- const [requestPath, options] = transformPathToQuery(
210
- pathParam,
211
- params2
212
- );
213
- return apiFetch(
257
+ async function invoke(pathParam, ...params2) {
258
+ const [, method, requestPath] = pathParam.split(" ");
259
+ const requestPathWithParams = createPathWithParams(
214
260
  requestPath,
215
- {
216
- ...options,
217
- headers: {
218
- ...defaultHeaders,
219
- ...options.headers
220
- }
221
- }
261
+ params2[0]?.pathParams
222
262
  );
263
+ const fetchOptions = {
264
+ ...params2[0]?.fetchOptions || {}
265
+ };
266
+ const resp = await apiFetch.raw(requestPathWithParams, {
267
+ ...fetchOptions,
268
+ method,
269
+ body: params2[0]?.body,
270
+ headers: defu(defaultHeaders, params2[0]?.headers),
271
+ query: params2[0]?.query
272
+ });
273
+ return {
274
+ data: resp._data,
275
+ status: resp.status
276
+ };
223
277
  }
224
278
  return {
225
- /**
226
- * Invoke API request based on provided path definition.
227
- */
228
279
  invoke,
229
280
  /**
230
281
  * Enables to change session data in runtime. Useful for testing purposes.
231
- * Setting session data with this methis will **not** fire `onAuthChange` hook.
282
+ * Setting session data with this method will **not** fire `onAuthChange` hook.
232
283
  */
233
284
  setSessionData,
234
285
  /**
235
286
  * Returns current session data. Useful for testing purposes, as in most cases you'll want to use `onAuthChange` hook for that.
236
287
  */
237
- getSessionData
288
+ getSessionData,
289
+ /**
290
+ * Default headers used in every client request (if not overriden in specific request).
291
+ */
292
+ defaultHeaders,
293
+ /**
294
+ * Available hooks for the client.
295
+ */
296
+ hook: apiClientHooks.hook
238
297
  };
239
298
  }
240
299
 
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "@shopware/api-client",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "description": "Shopware client for API connection.",
5
5
  "author": "Shopware",
6
+ "type": "module",
6
7
  "repository": {
7
8
  "type": "git",
8
9
  "url": "git+https://github.com/shopware/frontends.git"
@@ -35,31 +36,36 @@
35
36
  "types": "./dist/index.d.mts",
36
37
  "default": "./dist/index.mjs"
37
38
  }
38
- }
39
+ },
40
+ "./api-types": "./api-types/storeApiTypes.d.ts",
41
+ "./store-api-types": "./api-types/storeApiTypes.d.ts",
42
+ "./admin-api-types": "./api-types/adminApiTypes.d.ts"
39
43
  },
40
44
  "devDependencies": {
41
- "@types/prettier": "^3.0.0",
42
- "@vitest/coverage-c8": "^0.33.0",
43
- "prettier": "^3.0.3",
44
- "unbuild": "^2.0.0",
45
- "vitest": "^0.34.6",
46
- "@shopware/api-gen": "0.1.0",
47
- "eslint-config-shopware": "0.0.7",
45
+ "@codspeed/vitest-plugin": "3.1.0",
46
+ "@types/prettier": "3.0.0",
47
+ "@vitest/coverage-v8": "1.6.0",
48
+ "prettier": "3.3.2",
49
+ "unbuild": "2.0.0",
50
+ "vitest": "1.6.0",
51
+ "eslint-config-shopware": "0.0.9",
48
52
  "tsconfig": "0.0.0"
49
53
  },
50
54
  "dependencies": {
51
- "ofetch": "^1.3.3"
55
+ "defu": "6.1.4",
56
+ "hookable": "5.5.3",
57
+ "ofetch": "1.3.4"
52
58
  },
53
59
  "scripts": {
54
60
  "build": "export NODE_ENV=production && unbuild && pnpm build:types",
55
61
  "build:types": "tsc ./src/*.ts --declaration --allowJs --emitDeclarationOnly --outDir ./temp --skipLibCheck",
56
62
  "dev": "export NODE_ENV=development && unbuild --stub",
57
- "generate": "esno ../api-gen/src/cli.ts generate",
58
63
  "lint": "eslint src/**/*.ts* --fix --max-warnings=0 && pnpm run typecheck",
59
64
  "typecheck": "tsc --noEmit",
60
- "test": "vitest run && pnpm run test:types",
61
- "test:types": "vitest typecheck --run",
65
+ "test": "vitest run --typecheck",
62
66
  "test:bench": "vitest bench",
63
- "test:watch": "vitest"
67
+ "test:watch": "vitest --typecheck",
68
+ "generate-types": "esno ../api-gen/src/cli.ts generate --apiType=store",
69
+ "generate-admin-types": "esno ../api-gen/src/cli.ts generate --apiType=admin"
64
70
  }
65
71
  }