@cubicweb/client 3.0.0-alpha.8 → 3.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.
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 +141 -140
  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 +148 -122
  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.8",
5
+ "version": "3.0.0",
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<null>(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<null>(this._logoutApiUrl, {
161
160
  method: "POST",
162
- credentials: "include",
163
161
  });
164
162
  }
165
163
 
@@ -169,32 +167,42 @@ 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
+ return await this.nonNullFetchApi<Blob>(this._binaryApiUrl + params, {
189
188
  method: "GET",
190
- credentials: "include",
191
189
  });
192
190
  }
193
191
 
192
+ /**
193
+ * Executes the given transaction.
194
+ *
195
+ * If the {@link Transaction} object contains binaries, the request is automatically
196
+ * sent using `multipart/form-data` to allow uploading binaries.
197
+ *
198
+ * @param transaction Transaction to execute
199
+ * @returns A promise returning the transaction result on success
200
+ */
194
201
  async rql(transaction: Transaction): Promise<TransactionResult> {
202
+ const binaries = getBinariesFromTransaction(transaction);
195
203
  try {
196
- if (Object.keys(transaction.binaries).length > 0) {
197
- return await this.multipartTransaction(transaction);
204
+ if (binaries.length > 0) {
205
+ return await this.multipartTransaction(transaction, binaries);
198
206
  } else {
199
207
  return await this.jsonTransaction(transaction);
200
208
  }
@@ -204,41 +212,42 @@ export default class Api {
204
212
  }
205
213
 
206
214
  private getTransactionParams(transaction: Transaction) {
207
- return transaction.queries.map((transactionQuery) => ({
208
- query: transactionQuery.query,
209
- params: transactionQuery.params,
215
+ return transaction.queries.map((rqlQuery) => ({
216
+ query: rqlQuery.rql,
217
+ params: serializeQueryParams(transaction, rqlQuery.params),
210
218
  }));
211
219
  }
212
220
 
213
- private async multipartTransaction(transaction: Transaction) {
221
+ private async multipartTransaction(
222
+ transaction: Transaction,
223
+ binaries: RQLBinary[]
224
+ ) {
214
225
  const jsonParams = this.getTransactionParams(transaction);
215
226
  const formData = new FormData();
216
227
  formData.append("queries", JSON.stringify(jsonParams));
217
- Object.entries(transaction.binaries).forEach(([key, value]) => {
218
- formData.append(key, value);
228
+ binaries.forEach((binary) => {
229
+ formData.append(binary.uuid, binary.blob);
219
230
  });
220
231
 
221
- const resultSets = await nonNullFetchApi<ReadonlyArray<ResultSet>>(
232
+ const resultSets = await this.nonNullFetchApi<RawTransactionResult>(
222
233
  this._rqlApiUrl,
223
234
  {
224
235
  method: "POST",
225
- credentials: "include",
226
236
  body: formData,
227
237
  }
228
238
  );
229
- return new TransactionResult(resultSets);
239
+ return new TransactionResult(transaction, resultSets);
230
240
  }
231
241
 
232
242
  private async jsonTransaction(transaction: Transaction) {
233
- const resultSets = await nonNullFetchApi<ReadonlyArray<ResultSet>>(
243
+ const resultSets = await this.nonNullFetchApi<RawTransactionResult>(
234
244
  this._rqlApiUrl,
235
245
  {
236
246
  method: "POST",
237
- credentials: "include",
238
247
  body: JSON.stringify(this.getTransactionParams(transaction)),
239
248
  }
240
249
  );
241
- return new TransactionResult(resultSets);
250
+ return new TransactionResult(transaction, resultSets);
242
251
  }
243
252
 
244
253
  private handleUserErrors(error: unknown): never {
@@ -257,79 +266,96 @@ export default class Api {
257
266
  }
258
267
  throw error;
259
268
  }
260
- }
261
269
 
262
- /**
263
- * Behaves the same as `fetchAPI` but forbids null responses.
264
- * If this function detects a null response from the API,
265
- * the promise will be rejected with a 500 error code.
266
- *
267
- * @param input The API route to use
268
- * @param init Additional request parameters
269
- * @returns A promise with the non-null fetched result
270
- */
271
- async function nonNullFetchApi<R = unknown>(
272
- input: RequestInfo,
273
- init?: RequestInit
274
- ) {
275
- const result = await fetchApi<R>(input, init);
276
- if (result == null) {
277
- const error: ApiErrorResponse = {
278
- status: 500,
279
- title: "Unexpected null response",
280
- message: "Response was null but a value was expected",
281
- };
282
- throw error;
270
+ /**
271
+ * Behaves the same as `fetchAPI` but forbids null responses.
272
+ * If this function detects a null response from the API,
273
+ * the promise will be rejected with a 500 error code.
274
+ *
275
+ * @param input The API route to use
276
+ * @param init Additional request parameters
277
+ * @returns A promise with the non-null fetched result
278
+ */
279
+ private async nonNullFetchApi<R = unknown>(url: string, init: RequestInit) {
280
+ const result = await this.fetchApi<R>(url, init);
281
+ if (result == null) {
282
+ const error: ApiErrorResponse = {
283
+ status: 500,
284
+ title: "Unexpected null response",
285
+ message: "Response was null but a value was expected",
286
+ };
287
+ throw error;
288
+ }
289
+ return result;
283
290
  }
284
- return result;
285
- }
286
291
 
287
- /**
288
- * Sends a request to the given API route with the `Content-Type` header set and waits for a response.
289
- * The returned promise will be rejected if the API returns HTTP error codes.
290
- * The rejected promise will contain an `ApiErrorResponse` object.
291
- *
292
- * @param input The API route to use
293
- * @param init Additional request parameters
294
- * @returns A promise with the fetched result
295
- */
296
- async function fetchApi<R = unknown>(
297
- input: RequestInfo,
298
- init?: RequestInit
299
- ): Promise<R | null> {
300
- let headers = init?.headers;
301
- if (init?.method && init.method !== "GET") {
302
- headers = {
303
- "Content-Type": "application/json;charset=UTF-8",
304
- // CSRF custom header necessary for POST requests
305
- "X-Client-Name": "XMLHttpRequest",
306
- ...init?.headers,
307
- };
308
- }
292
+ /**
293
+ * Sends a request to the given API route with the `Content-Type` header set and waits for a response.
294
+ * The returned promise will be rejected if the API returns HTTP error codes.
295
+ * The rejected promise will contain an `ApiErrorResponse` object.
296
+ *
297
+ * @param input The API route to use
298
+ * @param init Additional request parameters
299
+ * @returns A promise with the fetched result
300
+ */
301
+ private async fetchApi<R = unknown>(
302
+ url: string,
303
+ init: RequestInit
304
+ ): Promise<R | null> {
305
+ const headers = new Headers(init.headers);
309
306
 
310
- const response = await fetch(input, {
311
- ...init,
312
- headers,
313
- });
314
- const status = response.status;
315
- if (status >= 200 && status < 300) {
316
- if (status === 204) {
317
- return null;
307
+ // Add custom header used as CSRF protection
308
+ // note: it is only necessary for non GET requests but can also be useful
309
+ // while debugging.
310
+ if (!headers.has("X-Client-Name")) {
311
+ headers.append("X-Client-Name", "XMLHttpRequest");
318
312
  }
319
- try {
320
- return await response.json();
321
- } catch (e) {
322
- console.error(e);
323
- return null;
313
+
314
+ // Add a "Content-Type" header whenever a body is passed.
315
+ // The header might already have been set (either by the caller of this
316
+ // function or by and encoder)
317
+ if (
318
+ init.body &&
319
+ !headers.has("Content-Type") &&
320
+ !(init.body instanceof FormData)
321
+ ) {
322
+ headers.append("Content-Type", "application/json;charset=UTF-8");
324
323
  }
325
- } else {
326
- let json;
327
- try {
328
- json = await response.json();
329
- } catch (e) {
330
- console.error(e);
331
- throw { status, title: "Error", message: "" };
324
+ init.headers = headers;
325
+ // Authenticate request
326
+ if (this._authenticator) {
327
+ const authenticatedRequest = await this._authenticator.authenticate(
328
+ url,
329
+ init
330
+ );
331
+ url = authenticatedRequest.url;
332
+ init = authenticatedRequest.init;
333
+ }
334
+
335
+ // Execute request
336
+ const response = await fetch(url, init);
337
+
338
+ // Parse response body and handle errors
339
+ const status = response.status;
340
+ if (status >= 200 && status < 300) {
341
+ if (status === 204) {
342
+ return null;
343
+ }
344
+ try {
345
+ return await response.json();
346
+ } catch (e) {
347
+ console.error(e);
348
+ return null;
349
+ }
350
+ } else {
351
+ let json;
352
+ try {
353
+ json = await response.json();
354
+ } catch (e) {
355
+ console.error(e);
356
+ throw { status, title: "Error", message: "" };
357
+ }
358
+ throw { status, ...json };
332
359
  }
333
- throw { status, ...json };
334
360
  }
335
361
  }
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
+ }
@@ -0,0 +1,17 @@
1
+ import { AuthenticatedRequest, Authenticator } from "../../Authenticator.js";
2
+
3
+ export interface CookieAuthenticatorOptions {
4
+ credentials?: RequestCredentials;
5
+ }
6
+
7
+ export class CookieAuthenticator implements Authenticator {
8
+ constructor(private options?: CookieAuthenticatorOptions) {}
9
+
10
+ async authenticate(
11
+ url: string,
12
+ init: RequestInit
13
+ ): Promise<AuthenticatedRequest> {
14
+ const credentials = this.options?.credentials;
15
+ return { url, init: { ...init, credentials } };
16
+ }
17
+ }