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