@cubicweb/client 3.0.0-alpha.9 → 3.0.2

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.
Files changed (122) hide show
  1. package/README.md +58 -34
  2. package/lib/Authenticator.d.ts +8 -0
  3. package/lib/Authenticator.d.ts.map +1 -0
  4. package/lib/Authenticator.js +2 -0
  5. package/lib/Authenticator.js.map +1 -0
  6. package/lib/api/Api.d.ts +56 -23
  7. package/lib/api/Api.d.ts.map +1 -1
  8. package/lib/api/Api.js +152 -149
  9. package/lib/api/Api.js.map +1 -1
  10. package/lib/api/Client.d.ts +26 -10
  11. package/lib/api/Client.d.ts.map +1 -1
  12. package/lib/api/Client.js +41 -58
  13. package/lib/api/Client.js.map +1 -1
  14. package/lib/api/ResultSet.d.ts +52 -0
  15. package/lib/api/ResultSet.d.ts.map +1 -0
  16. package/lib/api/ResultSet.js +64 -0
  17. package/lib/api/ResultSet.js.map +1 -0
  18. package/lib/api/authenticators/CookieAuthenticator.d.ts +10 -0
  19. package/lib/api/authenticators/CookieAuthenticator.d.ts.map +1 -0
  20. package/lib/api/authenticators/CookieAuthenticator.js +11 -0
  21. package/lib/api/authenticators/CookieAuthenticator.js.map +1 -0
  22. package/lib/api/authenticators/SignedRequestAuthenticator.d.ts +32 -0
  23. package/lib/api/authenticators/SignedRequestAuthenticator.d.ts.map +1 -0
  24. package/lib/api/authenticators/SignedRequestAuthenticator.js +89 -0
  25. package/lib/api/authenticators/SignedRequestAuthenticator.js.map +1 -0
  26. package/lib/api/binary/RQLBinary.d.ts +6 -0
  27. package/lib/api/binary/RQLBinary.d.ts.map +1 -0
  28. package/lib/api/binary/RQLBinary.js +10 -0
  29. package/lib/api/binary/RQLBinary.js.map +1 -0
  30. package/lib/api/query/RQLQuery.d.ts +40 -0
  31. package/lib/api/query/RQLQuery.d.ts.map +1 -0
  32. package/lib/api/query/RQLQuery.js +48 -0
  33. package/lib/api/query/RQLQuery.js.map +1 -0
  34. package/lib/api/query/references.d.ts +41 -0
  35. package/lib/api/query/references.d.ts.map +1 -0
  36. package/lib/api/query/references.js +50 -0
  37. package/lib/api/query/references.js.map +1 -0
  38. package/lib/api/transaction/Transaction.d.ts +25 -0
  39. package/lib/api/transaction/Transaction.d.ts.map +1 -0
  40. package/lib/api/transaction/Transaction.js +28 -0
  41. package/lib/api/transaction/Transaction.js.map +1 -0
  42. package/lib/api/transaction/TransactionResult.d.ts +41 -0
  43. package/lib/api/transaction/TransactionResult.d.ts.map +1 -0
  44. package/lib/api/transaction/TransactionResult.js +45 -0
  45. package/lib/api/transaction/TransactionResult.js.map +1 -0
  46. package/lib/encodeFormData.d.ts +5 -0
  47. package/lib/encodeFormData.d.ts.map +1 -0
  48. package/lib/encodeFormData.js +94 -0
  49. package/lib/encodeFormData.js.map +1 -0
  50. package/lib/{Errors.d.ts → errors.d.ts} +7 -4
  51. package/lib/errors.d.ts.map +1 -0
  52. package/lib/errors.js +19 -0
  53. package/lib/errors.js.map +1 -0
  54. package/lib/index.d.ts +19 -13
  55. package/lib/index.d.ts.map +1 -1
  56. package/lib/index.js +17 -34
  57. package/lib/index.js.map +1 -1
  58. package/lib/schema/classes/EntitySchema.d.ts +5 -5
  59. package/lib/schema/classes/EntitySchema.d.ts.map +1 -1
  60. package/lib/schema/classes/EntitySchema.js +3 -5
  61. package/lib/schema/classes/EntitySchema.js.map +1 -1
  62. package/lib/schema/classes/RelationDefinitionSchema.d.ts +6 -6
  63. package/lib/schema/classes/RelationDefinitionSchema.d.ts.map +1 -1
  64. package/lib/schema/classes/RelationDefinitionSchema.js +8 -10
  65. package/lib/schema/classes/RelationDefinitionSchema.js.map +1 -1
  66. package/lib/schema/classes/Schema.d.ts +7 -7
  67. package/lib/schema/classes/Schema.d.ts.map +1 -1
  68. package/lib/schema/classes/Schema.js +8 -11
  69. package/lib/schema/classes/Schema.js.map +1 -1
  70. package/lib/schema/classes/Utils.d.ts +10 -10
  71. package/lib/schema/classes/Utils.d.ts.map +1 -1
  72. package/lib/schema/classes/Utils.js +19 -35
  73. package/lib/schema/classes/Utils.js.map +1 -1
  74. package/lib/schema/raw/Constraints.d.ts +27 -0
  75. package/lib/schema/raw/Constraints.d.ts.map +1 -1
  76. package/lib/schema/raw/Constraints.js +1 -2
  77. package/lib/schema/raw/Entities.js +1 -2
  78. package/lib/schema/raw/RelationDefinitions.d.ts +3 -3
  79. package/lib/schema/raw/RelationDefinitions.d.ts.map +1 -1
  80. package/lib/schema/raw/RelationDefinitions.js +1 -2
  81. package/lib/schema/raw/Schema.d.ts +2 -2
  82. package/lib/schema/raw/Schema.d.ts.map +1 -1
  83. package/lib/schema/raw/Schema.js +1 -2
  84. package/lib/schema/raw/Utils.d.ts +1 -1
  85. package/lib/schema/raw/Utils.d.ts.map +1 -1
  86. package/lib/schema/raw/Utils.js +1 -2
  87. package/lib/utils.d.ts +39 -0
  88. package/lib/utils.d.ts.map +1 -0
  89. package/lib/utils.js +58 -0
  90. package/lib/utils.js.map +1 -0
  91. package/package.json +15 -5
  92. package/src/Authenticator.ts +8 -0
  93. package/src/api/Api.ts +154 -125
  94. package/src/api/Client.ts +31 -18
  95. package/src/api/ResultSet.ts +95 -0
  96. package/src/api/authenticators/CookieAuthenticator.ts +17 -0
  97. package/src/api/authenticators/SignedRequestAuthenticator.ts +133 -0
  98. package/src/api/binary/RQLBinary.ts +9 -0
  99. package/src/api/query/RQLQuery.ts +51 -0
  100. package/src/api/query/references.ts +43 -0
  101. package/src/api/transaction/Transaction.ts +31 -0
  102. package/src/api/transaction/TransactionResult.ts +68 -0
  103. package/src/encodeFormData.ts +126 -0
  104. package/src/{Errors.ts → errors.ts} +6 -7
  105. package/src/index.ts +24 -17
  106. package/src/schema/classes/EntitySchema.ts +8 -5
  107. package/src/schema/classes/RelationDefinitionSchema.ts +7 -7
  108. package/src/schema/classes/Schema.ts +10 -7
  109. package/src/schema/classes/Utils.ts +4 -4
  110. package/src/schema/raw/Constraints.ts +28 -0
  111. package/src/schema/raw/RelationDefinitions.ts +3 -3
  112. package/src/schema/raw/Schema.ts +2 -2
  113. package/src/schema/raw/Utils.ts +1 -1
  114. package/src/utils.ts +91 -0
  115. package/lib/Errors.d.ts.map +0 -1
  116. package/lib/Errors.js +0 -25
  117. package/lib/Errors.js.map +0 -1
  118. package/lib/api/Transaction.d.ts +0 -46
  119. package/lib/api/Transaction.d.ts.map +0 -1
  120. package/lib/api/Transaction.js +0 -75
  121. package/lib/api/Transaction.js.map +0 -1
  122. package/src/api/Transaction.ts +0 -94
package/package.json CHANGED
@@ -2,12 +2,10 @@
2
2
  "name": "@cubicweb/client",
3
3
  "description": "Official JS client for CubicWeb API",
4
4
  "author": "Logilab",
5
- "version": "3.0.0-alpha.9",
5
+ "version": "3.0.2",
6
6
  "license": "LGPL-3.0-or-later",
7
- "main": "lib/index.js",
8
- "types": "lib/index.d.ts",
9
7
  "scripts": {
10
- "build": "run -T tsc --project tsconfig.build.json",
8
+ "build": "run -T tsc --project tsconfig.build.json",
11
9
  "docs": "run -T typedoc"
12
10
  },
13
11
  "repository": {
@@ -23,5 +21,17 @@
23
21
  "lib/**",
24
22
  "src/**",
25
23
  "!**/__tests__/**"
26
- ]
24
+ ],
25
+ "dependencies": {
26
+ "uuid": "^9.0.1"
27
+ },
28
+ "devDependencies": {
29
+ "@types/uuid": "^9.0.8"
30
+ },
31
+ "type": "module",
32
+ "exports": {
33
+ ".": "./lib/index.js",
34
+ "./authenticators/SignedRequestAuthenticator": "./lib/api/authenticators/SignedRequestAuthenticator.js",
35
+ "./authenticators/CookieAuthenticator": "./lib/api/authenticators/CookieAuthenticator.js"
36
+ }
27
37
  }
@@ -0,0 +1,8 @@
1
+ export interface AuthenticatedRequest {
2
+ url: string;
3
+ init: RequestInit;
4
+ }
5
+
6
+ export interface Authenticator {
7
+ authenticate(url: string, init: RequestInit): Promise<AuthenticatedRequest>;
8
+ }
package/src/api/Api.ts CHANGED
@@ -1,46 +1,42 @@
1
- import { ValidationError } from "../Errors";
2
- import { RawSchema } from "../schema/raw/Schema";
1
+ import type { Authenticator } from "../Authenticator.js";
2
+ import { ValidationError } from "../errors.js";
3
+ import { RawSchema } from "../schema/raw/Schema.js";
4
+ import { getBinariesFromTransaction, serializeQueryParams } from "../utils.js";
5
+ import { RQLBinary } from "./binary/RQLBinary.js";
6
+ import { RQLQueryCellRef } from "./query/references.js";
7
+ import { Transaction } from "./transaction/Transaction.js";
3
8
  import {
9
+ RawTransactionResult,
4
10
  TransactionResult,
5
- Transaction,
6
- TransactionQueryScalarRef,
7
- TransactionQueryBinaryRef,
8
- } from "./Transaction";
11
+ } from "./transaction/TransactionResult.js";
12
+
13
+ interface ApiOptions {
14
+ authenticator?: Authenticator;
15
+ }
9
16
 
10
17
  /**
11
- * Type representing an RQL request and its associated parameters.
18
+ * Type representing valid final values to use as parameters in a RQL query.
19
+ *
20
+ * For references to requests/binaries, see {@link RQLQueryCellRef} and {@link RQLQueryBinaryRef}
12
21
  *
13
22
  * @category Api
14
23
  */
15
- export type RQLQuery = [string, RQLParams];
24
+ export type RQLQueryParamValue = string | number | boolean | null;
16
25
 
17
- export type RQLParamValue = string | number | boolean | null;
18
26
  /**
19
27
  * Type representing RQL parameters to send along a request.
20
28
  *
29
+ * Each key must be referenced in the RQL query.
30
+ *
21
31
  * @category Api
22
32
  */
23
- export type RQLParams = Record<
33
+ export type RQLQueryParams = Record<
24
34
  string,
25
- RQLParamValue | TransactionQueryScalarRef | TransactionQueryBinaryRef
35
+ RQLQueryParamValue | RQLQueryCellRef | RQLBinary
26
36
  >;
27
37
 
28
38
  /**
29
- * Type representing a row returned in a result set.
30
- *
31
- * @category Api
32
- */
33
- export type RqlRow = Array<string | number | boolean | null>;
34
-
35
- /**
36
- * Type representing a RQL request result.
37
- *
38
- * @category Api
39
- */
40
- export type ResultSet = Array<RqlRow>;
41
-
42
- /**
43
- * Type representing the currently logged user's data
39
+ * Type representing the currently logged user's data as returned by the `/current-user` route
44
40
  *
45
41
  * @category Api
46
42
  */
@@ -64,6 +60,9 @@ type ApiError = {
64
60
  /**
65
61
  * Type representing the error data sent to clients when retrieving an API error.
66
62
  *
63
+ * It simply adds the HTTP status code in a `status` key to the regular CubicWeb api error,
64
+ * so users can handle HTTP status codes in their code.
65
+ *
67
66
  * @category Api
68
67
  */
69
68
  export type ApiErrorResponse = {
@@ -74,7 +73,7 @@ export type ApiErrorResponse = {
74
73
  * Class used to handle requests to the CubicWeb API.
75
74
  *
76
75
  * You should never have to create this object yourself.
77
- * Instead, retrieve this object from the methods in the `Client` class.
76
+ * Instead, retrieve this object from the methods in the {@link Client} class.
78
77
  *
79
78
  * @internal
80
79
  */
@@ -87,18 +86,19 @@ export default class Api {
87
86
  private _logoutApiUrl: string;
88
87
  private _currentUserApiUrl: string;
89
88
  private _binaryApiUrl: string;
89
+ private _authenticator?: Authenticator;
90
90
 
91
91
  /**
92
92
  * Class used to handle requests to the CubicWeb API.
93
93
  *
94
94
  * You should never have to create this object yourself.
95
- * Instead, retrieve this object from the methods in the `Client` class.
95
+ * Instead, retrieve this object from the methods in the {@link Client} class.
96
96
  *
97
97
  * @param apiUrl The API url. It should not include the API version as it will be automatically added.
98
98
  * Example: if you can retrieve the schema with https://example.com/api/v1/schema,
99
99
  * use https://example.com/api as `apiUrl`.
100
100
  */
101
- constructor(apiUrl: string) {
101
+ constructor(apiUrl: string, options: ApiOptions) {
102
102
  this._apiUrl = apiUrl;
103
103
  this._rqlApiUrl = this.getApiRoute("rql");
104
104
  this._schemaApiUrl = this.getApiRoute("schema");
@@ -106,6 +106,7 @@ export default class Api {
106
106
  this._logoutApiUrl = this.getApiRoute("logout");
107
107
  this._currentUserApiUrl = this.getApiRoute("current-user");
108
108
  this._binaryApiUrl = this.getApiRoute("binary");
109
+ this._authenticator = options.authenticator;
109
110
  }
110
111
 
111
112
  get apiUrl() {
@@ -122,9 +123,8 @@ export default class Api {
122
123
  * @returns The CubicWeb instance's JSON schema.
123
124
  */
124
125
  async schema(): Promise<RawSchema> {
125
- return nonNullFetchApi<RawSchema>(this._schemaApiUrl, {
126
+ return this.nonNullFetchApi<RawSchema>(this._schemaApiUrl, {
126
127
  method: "GET",
127
- credentials: "include",
128
128
  });
129
129
  }
130
130
 
@@ -132,18 +132,17 @@ export default class Api {
132
132
  * Tries to log in the user on the configured CubicWeb instance with the given credentials.
133
133
  *
134
134
  * @param login The user login
135
- * @param password THe user password
135
+ * @param password The user password
136
136
  * @returns A promise returning true if the login was successful, false otherwise
137
137
  */
138
138
  async login(login: string, password: string): Promise<boolean> {
139
139
  try {
140
- await fetchApi<null>(this._loginApiUrl, {
140
+ await this.fetchApi(this._loginApiUrl, {
141
141
  method: "POST",
142
142
  body: JSON.stringify({
143
143
  login,
144
144
  password,
145
145
  }),
146
- credentials: "include",
147
146
  });
148
147
  return true;
149
148
  } catch (_error) {
@@ -157,9 +156,8 @@ export default class Api {
157
156
  * @returns A promise which will resolve when the user is properly logged out
158
157
  */
159
158
  async logout(): Promise<void> {
160
- await fetchApi<null>(this._logoutApiUrl, {
159
+ await this.fetchApi(this._logoutApiUrl, {
161
160
  method: "POST",
162
- credentials: "include",
163
161
  });
164
162
  }
165
163
 
@@ -169,32 +167,52 @@ export default class Api {
169
167
  * @returns A promise resolving to the current user
170
168
  */
171
169
  async currentUser(): Promise<CurrentUser> {
172
- return await nonNullFetchApi<CurrentUser>(this._currentUserApiUrl, {
170
+ return await this.nonNullFetchApi<CurrentUser>(this._currentUserApiUrl, {
173
171
  method: "GET",
174
- credentials: "include",
175
172
  });
176
173
  }
177
174
 
178
175
  /**
179
176
  * Get the binary for the given entity attribute
180
177
  *
181
- * @returns A promise resolving to the binary
178
+ * @param eid Entity eid to fetch the binary from
179
+ * @param attribute Attribute on the entity in which the binary is stored
180
+ * @returns A promise resolving to the binary as a Blob
182
181
  */
183
182
  async binary(eid: number, attribute: string): Promise<Blob> {
184
183
  const params = new URLSearchParams({
185
184
  eid: eid.toString(),
186
185
  attribute,
187
186
  });
188
- return await nonNullFetchApi<Blob>(this._binaryApiUrl + params, {
187
+ const response = await this.fetchApi(`${this._binaryApiUrl}?${params}`, {
189
188
  method: "GET",
190
- credentials: "include",
191
189
  });
190
+ const blob = await response?.blob();
191
+ if (blob == null) {
192
+ const error: ApiErrorResponse = {
193
+ status: 500,
194
+ title: "Unexpected null response",
195
+ message: `No blob found for ${params}`,
196
+ };
197
+ throw error;
198
+ }
199
+ return blob;
192
200
  }
193
201
 
202
+ /**
203
+ * Executes the given transaction.
204
+ *
205
+ * If the {@link Transaction} object contains binaries, the request is automatically
206
+ * sent using `multipart/form-data` to allow uploading binaries.
207
+ *
208
+ * @param transaction Transaction to execute
209
+ * @returns A promise returning the transaction result on success
210
+ */
194
211
  async rql(transaction: Transaction): Promise<TransactionResult> {
212
+ const binaries = getBinariesFromTransaction(transaction);
195
213
  try {
196
- if (Object.keys(transaction.binaries).length > 0) {
197
- return await this.multipartTransaction(transaction);
214
+ if (binaries.length > 0) {
215
+ return await this.multipartTransaction(transaction, binaries);
198
216
  } else {
199
217
  return await this.jsonTransaction(transaction);
200
218
  }
@@ -204,44 +222,42 @@ export default class Api {
204
222
  }
205
223
 
206
224
  private getTransactionParams(transaction: Transaction) {
207
- return transaction.queries.map((transactionQuery) => ({
208
- query: transactionQuery.query,
209
- params: transactionQuery.params,
225
+ return transaction.queries.map((rqlQuery) => ({
226
+ query: rqlQuery.rql,
227
+ params: serializeQueryParams(transaction, rqlQuery.params),
210
228
  }));
211
229
  }
212
230
 
213
- private async multipartTransaction(transaction: Transaction) {
231
+ private async multipartTransaction(
232
+ transaction: Transaction,
233
+ binaries: RQLBinary[]
234
+ ) {
214
235
  const jsonParams = this.getTransactionParams(transaction);
215
236
  const formData = new FormData();
216
237
  formData.append("queries", JSON.stringify(jsonParams));
217
- Object.entries(transaction.binaries).forEach(([key, value]) => {
218
- formData.append(key, value);
238
+ binaries.forEach((binary) => {
239
+ formData.append(binary.uuid, binary.blob);
219
240
  });
220
241
 
221
- const resultSets = await nonNullFetchApi<ReadonlyArray<ResultSet>>(
242
+ const resultSets = await this.nonNullFetchApi<RawTransactionResult>(
222
243
  this._rqlApiUrl,
223
244
  {
224
245
  method: "POST",
225
- credentials: "include",
226
246
  body: formData,
227
- headers: {
228
- "Content-Type": "multipart/form-data",
229
- },
230
247
  }
231
248
  );
232
- return new TransactionResult(resultSets);
249
+ return new TransactionResult(transaction, resultSets);
233
250
  }
234
251
 
235
252
  private async jsonTransaction(transaction: Transaction) {
236
- const resultSets = await nonNullFetchApi<ReadonlyArray<ResultSet>>(
253
+ const resultSets = await this.nonNullFetchApi<RawTransactionResult>(
237
254
  this._rqlApiUrl,
238
255
  {
239
256
  method: "POST",
240
- credentials: "include",
241
257
  body: JSON.stringify(this.getTransactionParams(transaction)),
242
258
  }
243
259
  );
244
- return new TransactionResult(resultSets);
260
+ return new TransactionResult(transaction, resultSets);
245
261
  }
246
262
 
247
263
  private handleUserErrors(error: unknown): never {
@@ -260,79 +276,92 @@ export default class Api {
260
276
  }
261
277
  throw error;
262
278
  }
263
- }
264
279
 
265
- /**
266
- * Behaves the same as `fetchAPI` but forbids null responses.
267
- * If this function detects a null response from the API,
268
- * the promise will be rejected with a 500 error code.
269
- *
270
- * @param input The API route to use
271
- * @param init Additional request parameters
272
- * @returns A promise with the non-null fetched result
273
- */
274
- async function nonNullFetchApi<R = unknown>(
275
- input: RequestInfo,
276
- init?: RequestInit
277
- ) {
278
- const result = await fetchApi<R>(input, init);
279
- if (result == null) {
280
- const error: ApiErrorResponse = {
281
- status: 500,
282
- title: "Unexpected null response",
283
- message: "Response was null but a value was expected",
284
- };
285
- throw error;
280
+ /**
281
+ * Behaves the same as `fetchAPI` but forbids null responses.
282
+ * If this function detects a null response from the API,
283
+ * the promise will be rejected with a 500 error code.
284
+ *
285
+ * @param input The API route to use
286
+ * @param init Additional request parameters
287
+ * @returns A promise with the non-null fetched result
288
+ */
289
+ private async nonNullFetchApi<R = unknown>(url: string, init: RequestInit) {
290
+ const response = await this.fetchApi(url, init);
291
+ const result: R = await response?.json();
292
+ if (result == null) {
293
+ const error: ApiErrorResponse = {
294
+ status: 500,
295
+ title: "Unexpected null response",
296
+ message: "Response was null but a value was expected",
297
+ };
298
+ throw error;
299
+ }
300
+ return result;
286
301
  }
287
- return result;
288
- }
289
302
 
290
- /**
291
- * Sends a request to the given API route with the `Content-Type` header set and waits for a response.
292
- * The returned promise will be rejected if the API returns HTTP error codes.
293
- * The rejected promise will contain an `ApiErrorResponse` object.
294
- *
295
- * @param input The API route to use
296
- * @param init Additional request parameters
297
- * @returns A promise with the fetched result
298
- */
299
- async function fetchApi<R = unknown>(
300
- input: RequestInfo,
301
- init?: RequestInit
302
- ): Promise<R | null> {
303
- let headers = init?.headers;
304
- if (init?.method && init.method !== "GET") {
305
- headers = {
306
- "Content-Type": "application/json;charset=UTF-8",
307
- // CSRF custom header necessary for POST requests
308
- "X-Client-Name": "XMLHttpRequest",
309
- ...init?.headers,
310
- };
311
- }
303
+ /**
304
+ * Sends a request to the given API route with the `Content-Type` header set and waits for a response.
305
+ * The returned promise will be rejected if the API returns HTTP error codes.
306
+ * The rejected promise will contain an `ApiErrorResponse` object.
307
+ *
308
+ * @param input The API route to use
309
+ * @param init Additional request parameters
310
+ * @returns A promise with the response
311
+ */
312
+ private async fetchApi(
313
+ url: string,
314
+ init: RequestInit
315
+ ): Promise<Response | null> {
316
+ const headers = new Headers(init.headers);
312
317
 
313
- const response = await fetch(input, {
314
- ...init,
315
- headers,
316
- });
317
- const status = response.status;
318
- if (status >= 200 && status < 300) {
319
- if (status === 204) {
320
- return null;
318
+ // Add custom header used as CSRF protection
319
+ // note: it is only necessary for non GET requests but can also be useful
320
+ // while debugging.
321
+ if (!headers.has("X-Client-Name")) {
322
+ headers.append("X-Client-Name", "XMLHttpRequest");
321
323
  }
322
- try {
323
- return await response.json();
324
- } catch (e) {
325
- console.error(e);
326
- return null;
324
+
325
+ // Add a "Content-Type" header whenever a body is passed.
326
+ // The header might already have been set (either by the caller of this
327
+ // function or by and encoder)
328
+ if (
329
+ init.body &&
330
+ !headers.has("Content-Type") &&
331
+ !(init.body instanceof FormData)
332
+ ) {
333
+ headers.append("Content-Type", "application/json;charset=UTF-8");
327
334
  }
328
- } else {
329
- let json;
330
- try {
331
- json = await response.json();
332
- } catch (e) {
333
- console.error(e);
334
- throw { status, title: "Error", message: "" };
335
+ init.headers = headers;
336
+ // Authenticate request
337
+ if (this._authenticator) {
338
+ const authenticatedRequest = await this._authenticator.authenticate(
339
+ url,
340
+ init
341
+ );
342
+ url = authenticatedRequest.url;
343
+ init = authenticatedRequest.init;
344
+ }
345
+
346
+ // Execute request
347
+ const response = await fetch(url, init);
348
+
349
+ // Parse response body and handle errors
350
+ const status = response.status;
351
+ if (status >= 200 && status < 300) {
352
+ if (status === 204) {
353
+ return null;
354
+ }
355
+ return response;
356
+ } else {
357
+ let json;
358
+ try {
359
+ json = await response.json();
360
+ } catch (e) {
361
+ console.error(e);
362
+ throw { status, title: "Error", message: "" };
363
+ }
364
+ throw { status, ...json };
335
365
  }
336
- throw { status, ...json };
337
366
  }
338
367
  }
package/src/api/Client.ts CHANGED
@@ -1,6 +1,14 @@
1
- import { Schema } from "../schema/classes/Schema";
2
- import Api, { RQLParams, ResultSet, CurrentUser } from "./Api";
3
- import { BinariesParam, Transaction, TransactionResult } from "./Transaction";
1
+ import { Authenticator } from "../Authenticator.js";
2
+ import { Schema } from "../schema/classes/Schema.js";
3
+ import Api, { RQLQueryParams, CurrentUser } from "./Api.js";
4
+ import { ResultSet } from "./ResultSet.js";
5
+ import { RQLQuery } from "./query/RQLQuery.js";
6
+ import { Transaction } from "./transaction/Transaction.js";
7
+ import { TransactionResult } from "./transaction/TransactionResult.js";
8
+
9
+ export interface ClientOptions {
10
+ authenticator?: Authenticator;
11
+ }
4
12
 
5
13
  /**
6
14
  * Class used to communicate with a CubicWeb instance's API.
@@ -9,7 +17,6 @@ import { BinariesParam, Transaction, TransactionResult } from "./Transaction";
9
17
  */
10
18
  export default class Client {
11
19
  private api: Api;
12
-
13
20
  /**
14
21
  * Class used to communicate with a CubicWeb instance's API.
15
22
  *
@@ -17,8 +24,8 @@ export default class Client {
17
24
  * Example: if you can retrieve the schema with https://example.com/api/v1/schema,
18
25
  * use https://example.com/api as `apiUrl`.
19
26
  */
20
- constructor(apiUrl: string) {
21
- this.api = new Api(apiUrl);
27
+ constructor(apiUrl: string, options?: ClientOptions) {
28
+ this.api = new Api(apiUrl, options ?? {});
22
29
  }
23
30
 
24
31
  get apiUrl() {
@@ -28,20 +35,19 @@ export default class Client {
28
35
  /**
29
36
  * Executes the given query and waits for the response.
30
37
  *
31
- * @param query The RQL query to send
38
+ * If binaries are given, the request is automatically
39
+ * sent using `multipart/form-data` to allow uploading binaries.
40
+ *
41
+ * @param rql The RQL query to send
32
42
  * @param params The additional parameters for the request
33
43
  * @returns A promise with the request's result set
34
44
  */
35
- async execute(
36
- query: string,
37
- params: RQLParams = {},
38
- binaries?: BinariesParam
39
- ): Promise<ResultSet> {
45
+ async execute(rql: string, params: RQLQueryParams = {}): Promise<ResultSet> {
40
46
  const transaction = new Transaction();
41
- const transactionQuery = transaction.push(query, params);
42
- transaction.setBinaries(binaries);
47
+ const query = new RQLQuery(rql, params);
48
+ transaction.push(query);
43
49
  const result = await this.executeTransaction(transaction);
44
- return result.resolveQuery(transactionQuery.ref());
50
+ return result.getResultSet(query);
45
51
  }
46
52
 
47
53
  /**
@@ -58,7 +64,7 @@ export default class Client {
58
64
  * Tries to log in the user on the configured CubicWeb instance with the given credentials.
59
65
  *
60
66
  * @param login The user login
61
- * @param password THe user password
67
+ * @param password The user password
62
68
  * @returns A promise returning true if the login was successful, false otherwise
63
69
  */
64
70
  async login(login: string, password: string) {
@@ -84,8 +90,13 @@ export default class Client {
84
90
  }
85
91
 
86
92
  /**
93
+ * Executes the given transaction.
94
+ *
95
+ * If the {@link Transaction} object contains binaries, the request is automatically
96
+ * sent using `multipart/form-data` to allow uploading binaries.
97
+ *
87
98
  * @param transaction Transaction to execute
88
- * @returns A promise fulfilled when the transaction is completed
99
+ * @returns A promise returning the transaction result on success
89
100
  */
90
101
  async executeTransaction(
91
102
  transaction: Transaction
@@ -96,7 +107,9 @@ export default class Client {
96
107
  /**
97
108
  * Get the binary for the given entity attribute
98
109
  *
99
- * @returns A promise resolving to the binary
110
+ * @param eid Entity eid to fetch the binary from
111
+ * @param attribute Attribute on the entity in which the binary is stored
112
+ * @returns A promise resolving to the binary as a Blob
100
113
  */
101
114
  async getBinary(eid: number, attribute: string) {
102
115
  return await this.api.binary(eid, attribute);
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Interface representing the json returned by the API for a single result set
3
+ *
4
+ * @category Api
5
+ */
6
+ export interface RawResultSet {
7
+ readonly rows: ResultSetCell[][];
8
+ readonly column_names: string[];
9
+ }
10
+
11
+ /**
12
+ * Interface representing the json returned by the API for a single row in a result set
13
+ *
14
+ * @category Api
15
+ */
16
+ export type RawResultSetRow = ResultSetCell[];
17
+
18
+ /**
19
+ * Type representing a final value in a result set.
20
+ *
21
+ * Please not this type may not be accurate if you use entity methods,
22
+ * as those methods may return any data structure.
23
+ *
24
+ * @category Api
25
+ */
26
+ export type ResultSetCell = string | number | boolean | null;
27
+
28
+ /**
29
+ * Class representing a row returned in a result set.
30
+ *
31
+ * @category Api
32
+ */
33
+ export class ResultSetRow implements Iterable<ResultSetCell> {
34
+ constructor(
35
+ public readonly columnNames: string[],
36
+ private readonly cells: RawResultSetRow
37
+ ) {}
38
+
39
+ *[Symbol.iterator]() {
40
+ for (const cell of this.cells) {
41
+ yield cell;
42
+ }
43
+ }
44
+
45
+ get length() {
46
+ return this.cells.length;
47
+ }
48
+
49
+ getCell(column: number | string) {
50
+ if (typeof column === "number") {
51
+ return this.cells[column];
52
+ } else {
53
+ const columnIndex = this.columnNames.indexOf(column);
54
+ return this.cells[columnIndex];
55
+ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Class representing a RQL request result.
61
+ *
62
+ * @category Api
63
+ */
64
+ export class ResultSet {
65
+ public readonly columnNames: string[];
66
+ readonly #rows: RawResultSetRow[];
67
+
68
+ constructor(rawResultSet: RawResultSet) {
69
+ this.columnNames = rawResultSet.column_names;
70
+ this.#rows = rawResultSet.rows;
71
+ }
72
+
73
+ *[Symbol.iterator]() {
74
+ for (const r of this.#rows) {
75
+ yield new ResultSetRow(this.columnNames, r);
76
+ }
77
+ }
78
+
79
+ get length() {
80
+ return this.#rows.length;
81
+ }
82
+
83
+ getRow(row: number): ResultSetRow {
84
+ return new ResultSetRow(this.columnNames, this.#rows[row]);
85
+ }
86
+
87
+ getCell(row: number, column: number | string): ResultSetCell {
88
+ if (typeof column === "number") {
89
+ return this.#rows[row][column];
90
+ } else {
91
+ const columnIndex = this.columnNames.indexOf(column);
92
+ return this.#rows[row][columnIndex];
93
+ }
94
+ }
95
+ }