@xata.io/client 0.0.0-beta.123dd7a → 0.0.0-beta.249d55a

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.d.ts CHANGED
@@ -54,15 +54,51 @@ export declare const includesAll: (value: string) => Constraint<string>;
54
54
  declare type FilterConstraints<T> = {
55
55
  [key in keyof T]?: T[key] extends Record<string, any> ? FilterConstraints<T[key]> : T[key] | DeepConstraint<T[key]>;
56
56
  };
57
+ declare type CursorNavigationOptions = {
58
+ first?: string;
59
+ } | {
60
+ last?: string;
61
+ } | {
62
+ after?: string;
63
+ before?: string;
64
+ };
65
+ declare type OffsetNavigationOptions = {
66
+ size?: number;
67
+ offset?: number;
68
+ };
69
+ declare type PaginationOptions = CursorNavigationOptions & OffsetNavigationOptions;
57
70
  declare type BulkQueryOptions<T> = {
58
- filter?: FilterConstraints<T>;
59
- sort?: {
60
- column: keyof T;
61
- direction?: SortDirection;
62
- } | keyof T;
71
+ page?: PaginationOptions;
63
72
  };
64
73
  declare type QueryOrConstraint<T, R> = Query<T, R> | Constraint<T>;
65
- export declare class Query<T, R = T> {
74
+ declare type QueryMeta = {
75
+ page: {
76
+ cursor: string;
77
+ more: boolean;
78
+ };
79
+ };
80
+ interface BasePage<T, R> {
81
+ query: Query<T, R>;
82
+ meta: QueryMeta;
83
+ records: R[];
84
+ nextPage(size?: number, offset?: number): Promise<Page<T, R>>;
85
+ previousPage(size?: number, offset?: number): Promise<Page<T, R>>;
86
+ firstPage(size?: number, offset?: number): Promise<Page<T, R>>;
87
+ lastPage(size?: number, offset?: number): Promise<Page<T, R>>;
88
+ hasNextPage(): boolean;
89
+ }
90
+ declare class Page<T, R> implements BasePage<T, R> {
91
+ readonly query: Query<T, R>;
92
+ readonly meta: QueryMeta;
93
+ readonly records: R[];
94
+ constructor(query: Query<T, R>, meta: QueryMeta, records?: R[]);
95
+ nextPage(size?: number, offset?: number): Promise<Page<T, R>>;
96
+ previousPage(size?: number, offset?: number): Promise<Page<T, R>>;
97
+ firstPage(size?: number, offset?: number): Promise<Page<T, R>>;
98
+ lastPage(size?: number, offset?: number): Promise<Page<T, R>>;
99
+ hasNextPage(): boolean;
100
+ }
101
+ export declare class Query<T, R = T> implements BasePage<T, R> {
66
102
  table: string;
67
103
  repository: Repository<T>;
68
104
  readonly $any?: QueryOrConstraint<T, R>[];
@@ -70,6 +106,9 @@ export declare class Query<T, R = T> {
70
106
  readonly $not?: QueryOrConstraint<T, R>[];
71
107
  readonly $none?: QueryOrConstraint<T, R>[];
72
108
  readonly $sort?: Record<string, SortDirection>;
109
+ readonly query: Query<T, R>;
110
+ readonly meta: QueryMeta;
111
+ readonly records: R[];
73
112
  constructor(repository: Repository<T> | null, table: string, data: Partial<Query<T, R>>, parent?: Query<T, R>);
74
113
  any(...queries: Query<T, R>[]): Query<T, R>;
75
114
  all(...queries: Query<T, R>[]): Query<T, R>;
@@ -78,30 +117,40 @@ export declare class Query<T, R = T> {
78
117
  filter(constraints: FilterConstraints<T>): Query<T, R>;
79
118
  filter<F extends keyof T>(column: F, value: FilterConstraints<T[F]> | DeepConstraint<T[F]>): Query<T, R>;
80
119
  sort<F extends keyof T>(column: F, direction: SortDirection): Query<T, R>;
120
+ getPaginated(options?: BulkQueryOptions<T>): Promise<Page<T, R>>;
121
+ [Symbol.asyncIterator](): AsyncIterableIterator<R>;
122
+ getIterator(chunk: number, options?: Omit<BulkQueryOptions<T>, 'page'>): AsyncGenerator<R[]>;
81
123
  getMany(options?: BulkQueryOptions<T>): Promise<R[]>;
82
- getOne(options?: BulkQueryOptions<T>): Promise<R | null>;
124
+ getOne(options?: Omit<BulkQueryOptions<T>, 'page'>): Promise<R | null>;
83
125
  deleteAll(): Promise<number>;
84
126
  include(columns: Include<T>): this;
127
+ nextPage(size?: number, offset?: number): Promise<Page<T, R>>;
128
+ previousPage(size?: number, offset?: number): Promise<Page<T, R>>;
129
+ firstPage(size?: number, offset?: number): Promise<Page<T, R>>;
130
+ lastPage(size?: number, offset?: number): Promise<Page<T, R>>;
131
+ hasNextPage(): boolean;
85
132
  }
86
133
  export declare abstract class Repository<T> extends Query<T, Selectable<T>> {
87
134
  select<K extends keyof Selectable<T>>(...columns: K[]): Query<T, Select<T, K>>;
88
135
  abstract create(object: Selectable<T>): Promise<T>;
136
+ abstract createMany(objects: Selectable<T>[]): Promise<T[]>;
89
137
  abstract read(id: string): Promise<T | null>;
90
138
  abstract update(id: string, object: Partial<T>): Promise<T>;
91
139
  abstract delete(id: string): void;
92
- abstract query<R>(query: Query<T, R>): Promise<R[]>;
140
+ abstract _runQuery<R>(query: Query<T, R>, options?: BulkQueryOptions<T>): Promise<Page<T, R>>;
93
141
  }
94
142
  export declare class RestRepository<T> extends Repository<T> {
95
143
  client: BaseClient<any>;
96
144
  fetch: any;
97
145
  constructor(client: BaseClient<any>, table: string);
98
- request(method: string, path: string, body?: unknown): Promise<any>;
146
+ request<T>(method: string, path: string, body?: unknown): Promise<T | undefined>;
99
147
  select<K extends keyof T>(...columns: K[]): Query<T, Select<T, K>>;
100
148
  create(object: T): Promise<T>;
149
+ createMany(objects: T[]): Promise<T[]>;
101
150
  read(id: string): Promise<T | null>;
102
151
  update(id: string, object: Partial<T>): Promise<T>;
103
152
  delete(id: string): Promise<void>;
104
- query<R>(query: Query<T, R>): Promise<R[]>;
153
+ _runQuery<R>(query: Query<T, R>, options?: BulkQueryOptions<T>): Promise<Page<T, R>>;
105
154
  }
106
155
  interface RepositoryFactory {
107
156
  createRepository<T>(client: BaseClient<any>, table: string): Repository<T>;
@@ -115,7 +164,7 @@ declare type BranchStrategy = BranchStrategyValue | BranchStrategyBuilder;
115
164
  declare type BranchStrategyOption = NonNullable<BranchStrategy | BranchStrategy[]>;
116
165
  export declare type XataClientOptions = {
117
166
  fetch?: unknown;
118
- databaseURL: string;
167
+ databaseURL?: string;
119
168
  branch: BranchStrategyOption;
120
169
  apiKey: string;
121
170
  repositoryFactory?: RepositoryFactory;
package/dist/index.js CHANGED
@@ -15,8 +15,21 @@ var __asyncValues = (this && this.__asyncValues) || function (o) {
15
15
  function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
16
16
  function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
17
17
  };
18
+ var __await = (this && this.__await) || function (v) { return this instanceof __await ? (this.v = v, this) : new __await(v); }
19
+ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _arguments, generator) {
20
+ if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
21
+ var g = generator.apply(thisArg, _arguments || []), i, q = [];
22
+ return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i;
23
+ function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }
24
+ function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
25
+ function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
26
+ function fulfill(value) { resume("next", value); }
27
+ function reject(value) { resume("throw", value); }
28
+ function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
29
+ };
18
30
  Object.defineProperty(exports, "__esModule", { value: true });
19
31
  exports.XataError = exports.BaseClient = exports.RestRespositoryFactory = exports.RestRepository = exports.Repository = exports.Query = exports.includesAll = exports.includesPattern = exports.includesSubstring = exports.includes = exports.contains = exports.isNot = exports.is = exports.pattern = exports.endsWith = exports.startsWith = exports.notExists = exports.exists = exports.le = exports.lte = exports.lt = exports.gte = exports.ge = exports.gt = void 0;
32
+ const errors_1 = require("./util/errors");
20
33
  const gt = (value) => ({ $gt: value });
21
34
  exports.gt = gt;
22
35
  const ge = (value) => ({ $ge: value });
@@ -54,8 +67,43 @@ const includesPattern = (value) => ({ $includesPattern: value });
54
67
  exports.includesPattern = includesPattern;
55
68
  const includesAll = (value) => ({ $includesAll: value });
56
69
  exports.includesAll = includesAll;
70
+ class Page {
71
+ constructor(query, meta, records = []) {
72
+ this.query = query;
73
+ this.meta = meta;
74
+ this.records = records;
75
+ }
76
+ nextPage(size, offset) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ return this.query.getPaginated({ page: { size, offset, after: this.meta.page.cursor } });
79
+ });
80
+ }
81
+ previousPage(size, offset) {
82
+ return __awaiter(this, void 0, void 0, function* () {
83
+ return this.query.getPaginated({ page: { size, offset, before: this.meta.page.cursor } });
84
+ });
85
+ }
86
+ firstPage(size, offset) {
87
+ return __awaiter(this, void 0, void 0, function* () {
88
+ return this.query.getPaginated({ page: { size, offset, first: this.meta.page.cursor } });
89
+ });
90
+ }
91
+ lastPage(size, offset) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ return this.query.getPaginated({ page: { size, offset, last: this.meta.page.cursor } });
94
+ });
95
+ }
96
+ // TODO: We need to add something on the backend if we want a hasPreviousPage
97
+ hasNextPage() {
98
+ return this.meta.page.more;
99
+ }
100
+ }
57
101
  class Query {
58
102
  constructor(repository, table, data, parent) {
103
+ // Cursor pagination
104
+ this.query = this;
105
+ this.meta = { page: { cursor: 'start', more: true } };
106
+ this.records = [];
59
107
  if (repository) {
60
108
  this.repository = repository;
61
109
  }
@@ -128,24 +176,56 @@ class Query {
128
176
  }, this);
129
177
  return q;
130
178
  }
131
- // TODO: pagination. Maybe implement different methods for different type of paginations
132
- // and one to simply get the first records returned by the query with no pagination.
179
+ getPaginated(options) {
180
+ return __awaiter(this, void 0, void 0, function* () {
181
+ return this.repository._runQuery(this, options);
182
+ });
183
+ }
184
+ [Symbol.asyncIterator]() {
185
+ return __asyncGenerator(this, arguments, function* _a() {
186
+ var e_1, _b;
187
+ try {
188
+ for (var _c = __asyncValues(this.getIterator(1)), _d; _d = yield __await(_c.next()), !_d.done;) {
189
+ const [record] = _d.value;
190
+ yield yield __await(record);
191
+ }
192
+ }
193
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
194
+ finally {
195
+ try {
196
+ if (_d && !_d.done && (_b = _c.return)) yield __await(_b.call(_c));
197
+ }
198
+ finally { if (e_1) throw e_1.error; }
199
+ }
200
+ });
201
+ }
202
+ getIterator(chunk, options = {}) {
203
+ return __asyncGenerator(this, arguments, function* getIterator_1() {
204
+ let offset = 0;
205
+ let end = false;
206
+ while (!end) {
207
+ const { records, meta } = yield __await(this.getPaginated(Object.assign(Object.assign({}, options), { page: { size: chunk, offset } })));
208
+ yield yield __await(records);
209
+ offset += chunk;
210
+ end = !meta.page.more;
211
+ }
212
+ });
213
+ }
133
214
  getMany(options) {
134
215
  return __awaiter(this, void 0, void 0, function* () {
135
- // TODO: use options
136
- return this.repository.query(this);
216
+ const { records } = yield this.getPaginated(options);
217
+ return records;
137
218
  });
138
219
  }
139
- getOne(options) {
220
+ getOne(options = {}) {
140
221
  return __awaiter(this, void 0, void 0, function* () {
141
- // TODO: use options
142
- const arr = yield this.getMany(); // TODO, limit to 1
143
- return arr[0] || null;
222
+ const records = yield this.getMany(Object.assign(Object.assign({}, options), { page: { size: 1 } }));
223
+ return records[0] || null;
144
224
  });
145
225
  }
146
226
  deleteAll() {
147
227
  return __awaiter(this, void 0, void 0, function* () {
148
- // Return number of affected rows
228
+ // TODO: Return number of affected rows
149
229
  return 0;
150
230
  });
151
231
  }
@@ -153,6 +233,29 @@ class Query {
153
233
  // TODO
154
234
  return this;
155
235
  }
236
+ nextPage(size, offset) {
237
+ return __awaiter(this, void 0, void 0, function* () {
238
+ return this.firstPage(size, offset);
239
+ });
240
+ }
241
+ previousPage(size, offset) {
242
+ return __awaiter(this, void 0, void 0, function* () {
243
+ return this.firstPage(size, offset);
244
+ });
245
+ }
246
+ firstPage(size, offset) {
247
+ return __awaiter(this, void 0, void 0, function* () {
248
+ return this.getPaginated({ page: { size, offset } });
249
+ });
250
+ }
251
+ lastPage(size, offset) {
252
+ return __awaiter(this, void 0, void 0, function* () {
253
+ return this.getPaginated({ page: { size, offset, before: 'end' } });
254
+ });
255
+ }
256
+ hasNextPage() {
257
+ return this.meta.page.more;
258
+ }
156
259
  }
157
260
  exports.Query = Query;
158
261
  class Repository extends Query {
@@ -165,25 +268,16 @@ class RestRepository extends Repository {
165
268
  constructor(client, table) {
166
269
  super(null, table, {});
167
270
  this.client = client;
168
- const { fetch } = client.options;
169
- if (fetch) {
271
+ const doWeHaveFetch = typeof fetch !== 'undefined';
272
+ const isInjectedFetchProblematic = !this.client.options.fetch;
273
+ if (doWeHaveFetch) {
170
274
  this.fetch = fetch;
171
275
  }
172
- else if (typeof window === 'object') {
173
- this.fetch = window.fetch;
276
+ else if (isInjectedFetchProblematic) {
277
+ throw new Error(errors_1.errors.falsyFetchImplementation);
174
278
  }
175
- else if (typeof require === 'function') {
176
- try {
177
- this.fetch = require('node-fetch');
178
- }
179
- catch (err) {
180
- try {
181
- this.fetch = require('cross-fetch');
182
- }
183
- catch (err) {
184
- throw new Error('No fetch implementation found. Please provide one in the constructor');
185
- }
186
- }
279
+ else {
280
+ this.fetch = this.client.options.fetch;
187
281
  }
188
282
  Object.defineProperty(this, 'client', { enumerable: false });
189
283
  Object.defineProperty(this, 'fetch', { enumerable: false });
@@ -219,7 +313,7 @@ class RestRepository extends Repository {
219
313
  throw new XataError(resp.statusText, resp.status);
220
314
  }
221
315
  if (resp.status === 204)
222
- return;
316
+ return undefined;
223
317
  return resp.json();
224
318
  });
225
319
  }
@@ -228,22 +322,40 @@ class RestRepository extends Repository {
228
322
  }
229
323
  create(object) {
230
324
  return __awaiter(this, void 0, void 0, function* () {
231
- const body = Object.assign({}, object);
232
- for (const key of Object.keys(body)) {
233
- const value = body[key];
234
- if (value && typeof value === 'object' && typeof value.id === 'string') {
235
- body[key] = value.id;
236
- }
325
+ const record = transformObjectLinks(object);
326
+ const response = yield this.request('POST', `/tables/${this.table}/data`, record);
327
+ if (!response) {
328
+ throw new Error("The server didn't return any data for the query");
329
+ }
330
+ const finalObject = yield this.read(response.id);
331
+ if (!finalObject) {
332
+ throw new Error('The server failed to save the record');
237
333
  }
238
- const obj = yield this.request('POST', `/tables/${this.table}/data`, body);
239
- return this.client.initObject(this.table, obj);
334
+ return finalObject;
335
+ });
336
+ }
337
+ createMany(objects) {
338
+ return __awaiter(this, void 0, void 0, function* () {
339
+ const records = objects.map((object) => transformObjectLinks(object));
340
+ const response = yield this.request('POST', `/tables/${this.table}/bulk`, { records });
341
+ if (!response) {
342
+ throw new Error("The server didn't return any data for the query");
343
+ }
344
+ // TODO: Use filer.$any() to get all the records
345
+ const finalObjects = yield Promise.all(response.recordIDs.map((id) => this.read(id)));
346
+ if (finalObjects.some((object) => !object)) {
347
+ throw new Error('The server failed to save the record');
348
+ }
349
+ return finalObjects;
240
350
  });
241
351
  }
242
352
  read(id) {
243
353
  return __awaiter(this, void 0, void 0, function* () {
244
354
  try {
245
- const obj = yield this.request('GET', `/tables/${this.table}/data/${id}`);
246
- return this.client.initObject(this.table, obj);
355
+ const response = yield this.request('GET', `/tables/${this.table}/data/${id}`);
356
+ if (!response)
357
+ return null;
358
+ return this.client.initObject(this.table, response);
247
359
  }
248
360
  catch (err) {
249
361
  if (err.status === 404)
@@ -254,8 +366,12 @@ class RestRepository extends Repository {
254
366
  }
255
367
  update(id, object) {
256
368
  return __awaiter(this, void 0, void 0, function* () {
257
- const obj = yield this.request('PUT', `/tables/${this.table}/data/${id}`, object);
258
- return this.client.initObject(this.table, obj);
369
+ const response = yield this.request('PUT', `/tables/${this.table}/data/${id}`, object);
370
+ if (!response) {
371
+ throw new Error("The server didn't return any data for the query");
372
+ }
373
+ // TODO: Review this, not sure we are properly initializing the object
374
+ return this.client.initObject(this.table, response);
259
375
  });
260
376
  }
261
377
  delete(id) {
@@ -263,7 +379,7 @@ class RestRepository extends Repository {
263
379
  yield this.request('DELETE', `/tables/${this.table}/data/${id}`);
264
380
  });
265
381
  }
266
- query(query) {
382
+ _runQuery(query, options) {
267
383
  return __awaiter(this, void 0, void 0, function* () {
268
384
  const filter = {
269
385
  $any: query.$any,
@@ -273,10 +389,16 @@ class RestRepository extends Repository {
273
389
  };
274
390
  const body = {
275
391
  filter: Object.values(filter).some(Boolean) ? filter : undefined,
276
- sort: query.$sort
392
+ sort: query.$sort,
393
+ page: options === null || options === void 0 ? void 0 : options.page
277
394
  };
278
- const result = yield this.request('POST', `/tables/${this.table}/query`, body);
279
- return result.records.map((record) => this.client.initObject(this.table, record));
395
+ const response = yield this.request('POST', `/tables/${this.table}/query`, body);
396
+ if (!response) {
397
+ throw new Error("The server didn't return any data for the query");
398
+ }
399
+ const { meta, records: objects } = response;
400
+ const records = objects.map((record) => this.client.initObject(this.table, record));
401
+ return new Page(query, meta, records);
280
402
  });
281
403
  }
282
404
  }
@@ -335,7 +457,7 @@ class BaseClient {
335
457
  return o;
336
458
  }
337
459
  getBranch() {
338
- var e_1, _a;
460
+ var e_2, _a;
339
461
  return __awaiter(this, void 0, void 0, function* () {
340
462
  if (this.branch)
341
463
  return this.branch;
@@ -354,12 +476,12 @@ class BaseClient {
354
476
  }
355
477
  }
356
478
  }
357
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
479
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
358
480
  finally {
359
481
  try {
360
482
  if (strategies_1_1 && !strategies_1_1.done && (_a = strategies_1.return)) yield _a.call(strategies_1);
361
483
  }
362
- finally { if (e_1) throw e_1.error; }
484
+ finally { if (e_2) throw e_2.error; }
363
485
  }
364
486
  throw new Error('Unable to resolve branch value');
365
487
  });
@@ -376,3 +498,12 @@ exports.XataError = XataError;
376
498
  const isBranchStrategyBuilder = (strategy) => {
377
499
  return typeof strategy === 'function';
378
500
  };
501
+ // TODO: We can find a better implementation for links
502
+ const transformObjectLinks = (object) => {
503
+ return Object.entries(object).reduce((acc, [key, value]) => {
504
+ if (value && typeof value === 'object' && typeof value.id === 'string') {
505
+ return Object.assign(Object.assign({}, acc), { [key]: value.id });
506
+ }
507
+ return Object.assign(Object.assign({}, acc), { [key]: value });
508
+ }, {});
509
+ };
@@ -0,0 +1,3 @@
1
+ export declare const errors: {
2
+ falsyFetchImplementation: string; /** @todo add a link after docs exist */
3
+ };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.errors = void 0;
4
+ exports.errors = {
5
+ falsyFetchImplementation: `The \`fetch\` option passed to the Xata client is resolving to a falsy value and may not be correctly imported.
6
+
7
+ More in the docs:
8
+ ` /** @todo add a link after docs exist */
9
+ };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@xata.io/client",
3
- "version": "0.0.0-beta.123dd7a",
3
+ "version": "0.0.0-beta.249d55a",
4
4
  "description": "Xata.io SDK for TypeScript and JavaScript",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "scripts": {
8
8
  "test": "echo \"Error: no test specified\" && exit 1",
9
- "build": "tsc",
9
+ "build": "tsc -p tsconfig.build.json",
10
10
  "prepack": "npm run build"
11
11
  },
12
12
  "repository": {
@@ -20,5 +20,5 @@
20
20
  "url": "https://github.com/xataio/client-ts/issues"
21
21
  },
22
22
  "homepage": "https://github.com/xataio/client-ts/blob/main/client/README.md",
23
- "gitHead": "123dd7a5eb1a79e75fd18921d67310c4bf277237"
23
+ "gitHead": "249d55a52f761852ee3c13730adfe154e0fd310a"
24
24
  }
package/src/index.test.ts CHANGED
@@ -262,9 +262,9 @@ type ExpectedRequest = {
262
262
 
263
263
  async function expectRequest(
264
264
  users: RestRepository<User>,
265
- expectedRequest: ExpectedRequest,
265
+ expectedRequest: ExpectedRequest[] | ExpectedRequest,
266
266
  callback: () => void,
267
- response?: unknown
267
+ response?: any
268
268
  ) {
269
269
  const request = jest.fn(async () => response);
270
270
  users.request = request;
@@ -272,11 +272,17 @@ async function expectRequest(
272
272
  await callback();
273
273
 
274
274
  const { calls } = request.mock;
275
- expect(calls.length).toBe(1);
276
- const [method, path, body] = calls[0] as any;
277
- expect(method).toBe(expectedRequest.method);
278
- expect(path).toBe(expectedRequest.path);
279
- expect(JSON.stringify(body)).toBe(JSON.stringify(expectedRequest.body));
275
+
276
+ const requests = Array.isArray(expectedRequest) ? expectedRequest : [expectedRequest];
277
+
278
+ expect(calls.length).toBe(requests.length);
279
+
280
+ for (let i = 0; i < calls.length; i++) {
281
+ const [method, path, body] = calls[i] as any;
282
+ expect(method).toBe(requests[i].method);
283
+ expect(path).toBe(requests[i].path);
284
+ expect(JSON.stringify(body)).toBe(JSON.stringify(requests[i].body));
285
+ }
280
286
  }
281
287
 
282
288
  describe('query', () => {
@@ -285,14 +291,20 @@ describe('query', () => {
285
291
  const { users } = buildClient();
286
292
 
287
293
  const expected = { method: 'POST', path: '/tables/users/query', body: {} };
288
- expectRequest(users, expected, () => users.getMany(), { records: [] });
294
+ expectRequest(users, expected, () => users.getMany(), {
295
+ records: [],
296
+ meta: { page: { cursor: '', more: false } }
297
+ });
289
298
  });
290
299
 
291
300
  test('query with one filter', async () => {
292
301
  const { users } = buildClient();
293
302
 
294
303
  const expected = { method: 'POST', path: '/tables/users/query', body: { filter: { $all: [{ name: 'foo' }] } } };
295
- expectRequest(users, expected, () => users.filter('name', 'foo').getMany(), { records: [] });
304
+ expectRequest(users, expected, () => users.filter('name', 'foo').getMany(), {
305
+ records: [],
306
+ meta: { page: { cursor: '', more: false } }
307
+ });
296
308
  });
297
309
  });
298
310
 
@@ -300,8 +312,8 @@ describe('query', () => {
300
312
  test('returns a single object', async () => {
301
313
  const { users } = buildClient();
302
314
 
303
- const result = { records: [{ id: '1234' }] };
304
- const expected = { method: 'POST', path: '/tables/users/query', body: {} };
315
+ const result = { records: [{ id: '1234' }], meta: { page: { cursor: '', more: false } } };
316
+ const expected = { method: 'POST', path: '/tables/users/query', body: { page: { size: 1 } } };
305
317
  expectRequest(
306
318
  users,
307
319
  expected,
@@ -316,8 +328,8 @@ describe('query', () => {
316
328
  test('returns null if no objects are returned', async () => {
317
329
  const { users } = buildClient();
318
330
 
319
- const result = { records: [] };
320
- const expected = { method: 'POST', path: '/tables/users/query', body: {} };
331
+ const result = { records: [], meta: { page: { cursor: '', more: false } } };
332
+ const expected = { method: 'POST', path: '/tables/users/query', body: { page: { size: 1 } } };
321
333
  expectRequest(
322
334
  users,
323
335
  expected,
@@ -378,7 +390,15 @@ describe('create', () => {
378
390
 
379
391
  const created = { id: 'rec_1234', _version: 0 };
380
392
  const object = { name: 'Ada' } as User;
381
- const expected = { method: 'POST', path: '/tables/users/data', body: object };
393
+ const expected = [
394
+ { method: 'POST', path: '/tables/users/data', body: object },
395
+ {
396
+ method: 'GET',
397
+ path: '/tables/users/data/rec_1234',
398
+ body: undefined
399
+ }
400
+ ];
401
+
382
402
  expectRequest(
383
403
  users,
384
404
  expected,
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { errors } from './util/errors';
2
+
1
3
  export interface XataRecord {
2
4
  id: string;
3
5
  xata: {
@@ -90,7 +92,13 @@ type FilterConstraints<T> = {
90
92
  [key in keyof T]?: T[key] extends Record<string, any> ? FilterConstraints<T[key]> : T[key] | DeepConstraint<T[key]>;
91
93
  };
92
94
 
95
+ type CursorNavigationOptions = { first?: string } | { last?: string } | { after?: string; before?: string };
96
+ type OffsetNavigationOptions = { size?: number; offset?: number };
97
+ type PaginationOptions = CursorNavigationOptions & OffsetNavigationOptions;
98
+
93
99
  type BulkQueryOptions<T> = {
100
+ page?: PaginationOptions;
101
+ /** TODO: Not implemented yet
94
102
  filter?: FilterConstraints<T>;
95
103
  sort?:
96
104
  | {
@@ -98,11 +106,60 @@ type BulkQueryOptions<T> = {
98
106
  direction?: SortDirection;
99
107
  }
100
108
  | keyof T;
109
+ **/
101
110
  };
102
111
 
103
112
  type QueryOrConstraint<T, R> = Query<T, R> | Constraint<T>;
104
113
 
105
- export class Query<T, R = T> {
114
+ type QueryMeta = { page: { cursor: string; more: boolean } };
115
+
116
+ interface BasePage<T, R> {
117
+ query: Query<T, R>;
118
+ meta: QueryMeta;
119
+ records: R[];
120
+
121
+ nextPage(size?: number, offset?: number): Promise<Page<T, R>>;
122
+ previousPage(size?: number, offset?: number): Promise<Page<T, R>>;
123
+ firstPage(size?: number, offset?: number): Promise<Page<T, R>>;
124
+ lastPage(size?: number, offset?: number): Promise<Page<T, R>>;
125
+
126
+ hasNextPage(): boolean;
127
+ }
128
+
129
+ class Page<T, R> implements BasePage<T, R> {
130
+ readonly query: Query<T, R>;
131
+ readonly meta: QueryMeta;
132
+ readonly records: R[];
133
+
134
+ constructor(query: Query<T, R>, meta: QueryMeta, records: R[] = []) {
135
+ this.query = query;
136
+ this.meta = meta;
137
+ this.records = records;
138
+ }
139
+
140
+ async nextPage(size?: number, offset?: number): Promise<Page<T, R>> {
141
+ return this.query.getPaginated({ page: { size, offset, after: this.meta.page.cursor } });
142
+ }
143
+
144
+ async previousPage(size?: number, offset?: number): Promise<Page<T, R>> {
145
+ return this.query.getPaginated({ page: { size, offset, before: this.meta.page.cursor } });
146
+ }
147
+
148
+ async firstPage(size?: number, offset?: number): Promise<Page<T, R>> {
149
+ return this.query.getPaginated({ page: { size, offset, first: this.meta.page.cursor } });
150
+ }
151
+
152
+ async lastPage(size?: number, offset?: number): Promise<Page<T, R>> {
153
+ return this.query.getPaginated({ page: { size, offset, last: this.meta.page.cursor } });
154
+ }
155
+
156
+ // TODO: We need to add something on the backend if we want a hasPreviousPage
157
+ hasNextPage(): boolean {
158
+ return this.meta.page.more;
159
+ }
160
+ }
161
+
162
+ export class Query<T, R = T> implements BasePage<T, R> {
106
163
  table: string;
107
164
  repository: Repository<T>;
108
165
 
@@ -112,6 +169,11 @@ export class Query<T, R = T> {
112
169
  readonly $none?: QueryOrConstraint<T, R>[];
113
170
  readonly $sort?: Record<string, SortDirection>;
114
171
 
172
+ // Cursor pagination
173
+ readonly query: Query<T, R> = this;
174
+ readonly meta: QueryMeta = { page: { cursor: 'start', more: true } };
175
+ readonly records: R[] = [];
176
+
115
177
  constructor(repository: Repository<T> | null, table: string, data: Partial<Query<T, R>>, parent?: Query<T, R>) {
116
178
  if (repository) {
117
179
  this.repository = repository;
@@ -231,21 +293,41 @@ export class Query<T, R = T> {
231
293
  return q;
232
294
  }
233
295
 
234
- // TODO: pagination. Maybe implement different methods for different type of paginations
235
- // and one to simply get the first records returned by the query with no pagination.
296
+ async getPaginated(options?: BulkQueryOptions<T>): Promise<Page<T, R>> {
297
+ return this.repository._runQuery(this, options);
298
+ }
299
+
300
+ async *[Symbol.asyncIterator](): AsyncIterableIterator<R> {
301
+ for await (const [record] of this.getIterator(1)) {
302
+ yield record;
303
+ }
304
+ }
305
+
306
+ async *getIterator(chunk: number, options: Omit<BulkQueryOptions<T>, 'page'> = {}): AsyncGenerator<R[]> {
307
+ let offset = 0;
308
+ let end = false;
309
+
310
+ while (!end) {
311
+ const { records, meta } = await this.getPaginated({ ...options, page: { size: chunk, offset } });
312
+ yield records;
313
+
314
+ offset += chunk;
315
+ end = !meta.page.more;
316
+ }
317
+ }
318
+
236
319
  async getMany(options?: BulkQueryOptions<T>): Promise<R[]> {
237
- // TODO: use options
238
- return this.repository.query(this);
320
+ const { records } = await this.getPaginated(options);
321
+ return records;
239
322
  }
240
323
 
241
- async getOne(options?: BulkQueryOptions<T>): Promise<R | null> {
242
- // TODO: use options
243
- const arr = await this.getMany(); // TODO, limit to 1
244
- return arr[0] || null;
324
+ async getOne(options: Omit<BulkQueryOptions<T>, 'page'> = {}): Promise<R | null> {
325
+ const records = await this.getMany({ ...options, page: { size: 1 } });
326
+ return records[0] || null;
245
327
  }
246
328
 
247
329
  async deleteAll(): Promise<number> {
248
- // Return number of affected rows
330
+ // TODO: Return number of affected rows
249
331
  return 0;
250
332
  }
251
333
 
@@ -253,6 +335,26 @@ export class Query<T, R = T> {
253
335
  // TODO
254
336
  return this;
255
337
  }
338
+
339
+ async nextPage(size?: number, offset?: number): Promise<Page<T, R>> {
340
+ return this.firstPage(size, offset);
341
+ }
342
+
343
+ async previousPage(size?: number, offset?: number): Promise<Page<T, R>> {
344
+ return this.firstPage(size, offset);
345
+ }
346
+
347
+ async firstPage(size?: number, offset?: number): Promise<Page<T, R>> {
348
+ return this.getPaginated({ page: { size, offset } });
349
+ }
350
+
351
+ async lastPage(size?: number, offset?: number): Promise<Page<T, R>> {
352
+ return this.getPaginated({ page: { size, offset, before: 'end' } });
353
+ }
354
+
355
+ hasNextPage(): boolean {
356
+ return this.meta.page.more;
357
+ }
256
358
  }
257
359
 
258
360
  export abstract class Repository<T> extends Query<T, Selectable<T>> {
@@ -262,6 +364,8 @@ export abstract class Repository<T> extends Query<T, Selectable<T>> {
262
364
 
263
365
  abstract create(object: Selectable<T>): Promise<T>;
264
366
 
367
+ abstract createMany(objects: Selectable<T>[]): Promise<T[]>;
368
+
265
369
  abstract read(id: string): Promise<T | null>;
266
370
 
267
371
  abstract update(id: string, object: Partial<T>): Promise<T>;
@@ -269,7 +373,7 @@ export abstract class Repository<T> extends Query<T, Selectable<T>> {
269
373
  abstract delete(id: string): void;
270
374
 
271
375
  // Used by the Query object internally
272
- abstract query<R>(query: Query<T, R>): Promise<R[]>;
376
+ abstract _runQuery<R>(query: Query<T, R>, options?: BulkQueryOptions<T>): Promise<Page<T, R>>;
273
377
  }
274
378
 
275
379
  export class RestRepository<T> extends Repository<T> {
@@ -280,22 +384,15 @@ export class RestRepository<T> extends Repository<T> {
280
384
  super(null, table, {});
281
385
  this.client = client;
282
386
 
283
- const { fetch } = client.options;
387
+ const doWeHaveFetch = typeof fetch !== 'undefined';
388
+ const isInjectedFetchProblematic = !this.client.options.fetch;
284
389
 
285
- if (fetch) {
390
+ if (doWeHaveFetch) {
286
391
  this.fetch = fetch;
287
- } else if (typeof window === 'object') {
288
- this.fetch = window.fetch;
289
- } else if (typeof require === 'function') {
290
- try {
291
- this.fetch = require('node-fetch');
292
- } catch (err) {
293
- try {
294
- this.fetch = require('cross-fetch');
295
- } catch (err) {
296
- throw new Error('No fetch implementation found. Please provide one in the constructor');
297
- }
298
- }
392
+ } else if (isInjectedFetchProblematic) {
393
+ throw new Error(errors.falsyFetchImplementation);
394
+ } else {
395
+ this.fetch = this.client.options.fetch;
299
396
  }
300
397
 
301
398
  Object.defineProperty(this, 'client', { enumerable: false });
@@ -303,7 +400,7 @@ export class RestRepository<T> extends Repository<T> {
303
400
  Object.defineProperty(this, 'hostname', { enumerable: false });
304
401
  }
305
402
 
306
- async request(method: string, path: string, body?: unknown) {
403
+ async request<T>(method: string, path: string, body?: unknown): Promise<T | undefined> {
307
404
  const { databaseURL, apiKey } = this.client.options;
308
405
  const branch = await this.client.getBranch();
309
406
 
@@ -316,6 +413,7 @@ export class RestRepository<T> extends Repository<T> {
316
413
  },
317
414
  body: JSON.stringify(body)
318
415
  });
416
+
319
417
  if (!resp.ok) {
320
418
  try {
321
419
  const json = await resp.json();
@@ -330,7 +428,8 @@ export class RestRepository<T> extends Repository<T> {
330
428
  }
331
429
  throw new XataError(resp.statusText, resp.status);
332
430
  }
333
- if (resp.status === 204) return;
431
+
432
+ if (resp.status === 204) return undefined;
334
433
  return resp.json();
335
434
  }
336
435
 
@@ -339,21 +438,51 @@ export class RestRepository<T> extends Repository<T> {
339
438
  }
340
439
 
341
440
  async create(object: T): Promise<T> {
342
- const body = { ...object } as Record<string, unknown>;
343
- for (const key of Object.keys(body)) {
344
- const value = body[key];
345
- if (value && typeof value === 'object' && typeof (value as Record<string, unknown>).id === 'string') {
346
- body[key] = (value as XataRecord).id;
347
- }
441
+ const record = transformObjectLinks(object);
442
+
443
+ const response = await this.request<{
444
+ id: string;
445
+ xata: { version: number };
446
+ }>('POST', `/tables/${this.table}/data`, record);
447
+ if (!response) {
448
+ throw new Error("The server didn't return any data for the query");
449
+ }
450
+
451
+ const finalObject = await this.read(response.id);
452
+ if (!finalObject) {
453
+ throw new Error('The server failed to save the record');
454
+ }
455
+
456
+ return finalObject;
457
+ }
458
+
459
+ async createMany(objects: T[]): Promise<T[]> {
460
+ const records = objects.map((object) => transformObjectLinks(object));
461
+
462
+ const response = await this.request<{
463
+ recordIDs: string[];
464
+ }>('POST', `/tables/${this.table}/bulk`, { records });
465
+ if (!response) {
466
+ throw new Error("The server didn't return any data for the query");
467
+ }
468
+
469
+ // TODO: Use filer.$any() to get all the records
470
+ const finalObjects = await Promise.all(response.recordIDs.map((id) => this.read(id)));
471
+ if (finalObjects.some((object) => !object)) {
472
+ throw new Error('The server failed to save the record');
348
473
  }
349
- const obj = await this.request('POST', `/tables/${this.table}/data`, body);
350
- return this.client.initObject(this.table, obj);
474
+
475
+ return finalObjects as T[];
351
476
  }
352
477
 
353
478
  async read(id: string): Promise<T | null> {
354
479
  try {
355
- const obj = await this.request('GET', `/tables/${this.table}/data/${id}`);
356
- return this.client.initObject(this.table, obj);
480
+ const response = await this.request<
481
+ T & { id: string; xata: { version: number; table?: string; warnings?: string[] } }
482
+ >('GET', `/tables/${this.table}/data/${id}`);
483
+ if (!response) return null;
484
+
485
+ return this.client.initObject(this.table, response);
357
486
  } catch (err) {
358
487
  if ((err as XataError).status === 404) return null;
359
488
  throw err;
@@ -361,27 +490,48 @@ export class RestRepository<T> extends Repository<T> {
361
490
  }
362
491
 
363
492
  async update(id: string, object: Partial<T>): Promise<T> {
364
- const obj = await this.request('PUT', `/tables/${this.table}/data/${id}`, object);
365
- return this.client.initObject(this.table, obj);
493
+ const response = await this.request<{
494
+ id: string;
495
+ xata: { version: number };
496
+ }>('PUT', `/tables/${this.table}/data/${id}`, object);
497
+ if (!response) {
498
+ throw new Error("The server didn't return any data for the query");
499
+ }
500
+
501
+ // TODO: Review this, not sure we are properly initializing the object
502
+ return this.client.initObject(this.table, response);
366
503
  }
367
504
 
368
505
  async delete(id: string) {
369
506
  await this.request('DELETE', `/tables/${this.table}/data/${id}`);
370
507
  }
371
508
 
372
- async query<R>(query: Query<T, R>): Promise<R[]> {
509
+ async _runQuery<R>(query: Query<T, R>, options?: BulkQueryOptions<T>): Promise<Page<T, R>> {
373
510
  const filter = {
374
511
  $any: query.$any,
375
512
  $all: query.$all,
376
513
  $not: query.$not,
377
514
  $none: query.$none
378
515
  };
516
+
379
517
  const body = {
380
518
  filter: Object.values(filter).some(Boolean) ? filter : undefined,
381
- sort: query.$sort
519
+ sort: query.$sort,
520
+ page: options?.page
382
521
  };
383
- const result = await this.request('POST', `/tables/${this.table}/query`, body);
384
- return result.records.map((record: object) => this.client.initObject(this.table, record));
522
+
523
+ const response = await this.request<{
524
+ records: object[];
525
+ meta: { page: { cursor: string; more: boolean } };
526
+ }>('POST', `/tables/${this.table}/query`, body);
527
+ if (!response) {
528
+ throw new Error("The server didn't return any data for the query");
529
+ }
530
+
531
+ const { meta, records: objects } = response;
532
+ const records = objects.map((record) => this.client.initObject<R>(this.table, record));
533
+
534
+ return new Page(query, meta, records);
385
535
  }
386
536
  }
387
537
 
@@ -402,7 +552,7 @@ type BranchStrategyOption = NonNullable<BranchStrategy | BranchStrategy[]>;
402
552
 
403
553
  export type XataClientOptions = {
404
554
  fetch?: unknown;
405
- databaseURL: string;
555
+ databaseURL?: string;
406
556
  branch: BranchStrategyOption;
407
557
  apiKey: string;
408
558
  repositoryFactory?: RepositoryFactory;
@@ -504,3 +654,14 @@ export type Links = Record<string, Array<string[]>>;
504
654
  const isBranchStrategyBuilder = (strategy: BranchStrategy): strategy is BranchStrategyBuilder => {
505
655
  return typeof strategy === 'function';
506
656
  };
657
+
658
+ // TODO: We can find a better implementation for links
659
+ const transformObjectLinks = (object: any) => {
660
+ return Object.entries(object).reduce((acc, [key, value]) => {
661
+ if (value && typeof value === 'object' && typeof (value as Record<string, unknown>).id === 'string') {
662
+ return { ...acc, [key]: (value as XataRecord).id };
663
+ }
664
+
665
+ return { ...acc, [key]: value };
666
+ }, {});
667
+ };
@@ -0,0 +1,6 @@
1
+ export const errors = {
2
+ falsyFetchImplementation: `The \`fetch\` option passed to the Xata client is resolving to a falsy value and may not be correctly imported.
3
+
4
+ More in the docs:
5
+ ` /** @todo add a link after docs exist */
6
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig",
3
+ "exclude": ["**/*.test.ts", "**/*.mock.ts"]
4
+ }
@@ -1 +0,0 @@
1
- export {};
@@ -1,304 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- const _1 = require("./");
13
- const buildClient = (options = {}) => {
14
- const { apiKey = '1234', databaseURL = 'https://my-workspace-5df34do.staging.xatabase.co/db/xata', branch = 'main' } = options;
15
- const fetch = jest.fn();
16
- const client = new _1.BaseClient({ fetch, apiKey, databaseURL, branch }, {});
17
- const users = new _1.RestRepository(client, 'users');
18
- return { fetch, client, users };
19
- };
20
- describe('client options', () => {
21
- test('option parameters are set', () => {
22
- const { client } = buildClient({ apiKey: 'apiKey', databaseURL: 'url' });
23
- expect(client.options.apiKey).toBe('apiKey');
24
- expect(client.options.databaseURL).toBe('url');
25
- });
26
- test('throws if mandatory options are missing', () => {
27
- // @ts-expect-error Options are mandatory in TypeScript
28
- expect(() => buildClient({ apiKey: null }, {})).toThrow('Options databaseURL, apiKey and branch are required');
29
- // @ts-expect-error Options are mandatory in TypeScript
30
- expect(() => buildClient({ databaseURL: null }, {})).toThrow('Options databaseURL, apiKey and branch are required');
31
- // @ts-expect-error Options are mandatory in TypeScript
32
- expect(() => buildClient({ branch: null }, {})).toThrow('Options databaseURL, apiKey and branch are required');
33
- });
34
- test('throws if branch cannot be resolved', () => {
35
- const { users } = buildClient({ branch: () => null });
36
- expect(users.request('GET', '/foo')).rejects.toThrow('Unable to resolve branch value');
37
- });
38
- test('provide branch as a string', () => __awaiter(void 0, void 0, void 0, function* () {
39
- const { fetch, users } = buildClient({ branch: 'branch' });
40
- fetch.mockReset().mockImplementation(() => {
41
- return {
42
- ok: true,
43
- json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
44
- };
45
- });
46
- yield users.request('GET', '/foo');
47
- expect(fetch).toHaveBeenCalledTimes(1);
48
- expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
49
- Array [
50
- "https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
51
- Object {
52
- "body": undefined,
53
- "headers": Object {
54
- "Accept": "*/*",
55
- "Authorization": "Bearer 1234",
56
- "Content-Type": "application/json",
57
- },
58
- "method": "GET",
59
- },
60
- ]
61
- `);
62
- }));
63
- test('provide branch as an array', () => __awaiter(void 0, void 0, void 0, function* () {
64
- const { fetch, users } = buildClient({
65
- branch: [process.env.NOT_DEFINED_VARIABLE, () => __awaiter(void 0, void 0, void 0, function* () { return null; }), 'branch', 'main']
66
- });
67
- fetch.mockReset().mockImplementation(() => {
68
- return {
69
- ok: true,
70
- json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
71
- };
72
- });
73
- yield users.request('GET', '/foo');
74
- expect(fetch).toHaveBeenCalledTimes(1);
75
- expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
76
- Array [
77
- "https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
78
- Object {
79
- "body": undefined,
80
- "headers": Object {
81
- "Accept": "*/*",
82
- "Authorization": "Bearer 1234",
83
- "Content-Type": "application/json",
84
- },
85
- "method": "GET",
86
- },
87
- ]
88
- `);
89
- }));
90
- test('provide branch as a function', () => __awaiter(void 0, void 0, void 0, function* () {
91
- const { fetch, users } = buildClient({ branch: () => 'branch' });
92
- fetch.mockReset().mockImplementation(() => {
93
- return {
94
- ok: true,
95
- json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
96
- };
97
- });
98
- yield users.request('GET', '/foo');
99
- expect(fetch).toHaveBeenCalledTimes(1);
100
- expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
101
- Array [
102
- "https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
103
- Object {
104
- "body": undefined,
105
- "headers": Object {
106
- "Accept": "*/*",
107
- "Authorization": "Bearer 1234",
108
- "Content-Type": "application/json",
109
- },
110
- "method": "GET",
111
- },
112
- ]
113
- `);
114
- }));
115
- test('ensure branch resolution is memoized', () => __awaiter(void 0, void 0, void 0, function* () {
116
- const branchGetter = jest.fn(() => 'branch');
117
- const { fetch, users } = buildClient({ branch: branchGetter });
118
- fetch.mockReset().mockImplementation(() => {
119
- return {
120
- ok: true,
121
- json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
122
- };
123
- });
124
- yield users.request('GET', '/foo');
125
- yield users.request('GET', '/foo');
126
- expect(branchGetter).toHaveBeenCalledTimes(1);
127
- }));
128
- });
129
- describe('request', () => {
130
- test('builds the right arguments for a GET request', () => __awaiter(void 0, void 0, void 0, function* () {
131
- const { fetch, users } = buildClient();
132
- fetch.mockReset().mockImplementation(() => {
133
- return {
134
- ok: true,
135
- json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
136
- };
137
- });
138
- yield users.request('GET', '/foo');
139
- expect(fetch).toHaveBeenCalledTimes(1);
140
- expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
141
- Array [
142
- "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/foo",
143
- Object {
144
- "body": undefined,
145
- "headers": Object {
146
- "Accept": "*/*",
147
- "Authorization": "Bearer 1234",
148
- "Content-Type": "application/json",
149
- },
150
- "method": "GET",
151
- },
152
- ]
153
- `);
154
- }));
155
- test('builds the right arguments for a POST request', () => __awaiter(void 0, void 0, void 0, function* () {
156
- const { fetch, users } = buildClient();
157
- fetch.mockReset().mockImplementation(() => {
158
- return {
159
- ok: true,
160
- json: () => __awaiter(void 0, void 0, void 0, function* () { return ({}); })
161
- };
162
- });
163
- yield users.request('POST', '/foo', { a: 1 });
164
- expect(fetch).toHaveBeenCalledTimes(1);
165
- expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
166
- Array [
167
- "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/foo",
168
- Object {
169
- "body": "{\\"a\\":1}",
170
- "headers": Object {
171
- "Accept": "*/*",
172
- "Authorization": "Bearer 1234",
173
- "Content-Type": "application/json",
174
- },
175
- "method": "POST",
176
- },
177
- ]
178
- `);
179
- }));
180
- test('throws if the response is not ok', () => __awaiter(void 0, void 0, void 0, function* () {
181
- const { fetch, users } = buildClient();
182
- fetch.mockImplementation(() => {
183
- return {
184
- ok: false,
185
- status: 404,
186
- statusText: 'Not Found'
187
- };
188
- });
189
- expect(users.request('GET', '/foo')).rejects.toThrow(new _1.XataError('Not Found', 404));
190
- }));
191
- test('throws with the error from the server if the response is not ok', () => __awaiter(void 0, void 0, void 0, function* () {
192
- const { fetch, users } = buildClient();
193
- fetch.mockImplementation(() => {
194
- return {
195
- ok: false,
196
- status: 404,
197
- statusText: 'Not Found',
198
- json: () => __awaiter(void 0, void 0, void 0, function* () { return ({ message: 'Resource not found' }); })
199
- };
200
- });
201
- expect(users.request('GET', '/foo')).rejects.toThrow(new _1.XataError('Resource not found', 404));
202
- }));
203
- test('returns the json body if the response is ok', () => __awaiter(void 0, void 0, void 0, function* () {
204
- const { fetch, users } = buildClient();
205
- const json = { a: 1 };
206
- fetch.mockImplementation(() => {
207
- return {
208
- ok: true,
209
- json: () => __awaiter(void 0, void 0, void 0, function* () { return json; })
210
- };
211
- });
212
- const result = yield users.request('GET', '/foo');
213
- expect(result).toEqual(json);
214
- }));
215
- });
216
- function expectRequest(users, expectedRequest, callback, response) {
217
- return __awaiter(this, void 0, void 0, function* () {
218
- const request = jest.fn(() => __awaiter(this, void 0, void 0, function* () { return response; }));
219
- users.request = request;
220
- yield callback();
221
- const { calls } = request.mock;
222
- expect(calls.length).toBe(1);
223
- const [method, path, body] = calls[0];
224
- expect(method).toBe(expectedRequest.method);
225
- expect(path).toBe(expectedRequest.path);
226
- expect(JSON.stringify(body)).toBe(JSON.stringify(expectedRequest.body));
227
- });
228
- }
229
- describe('query', () => {
230
- describe('getMany', () => {
231
- test('simple query', () => __awaiter(void 0, void 0, void 0, function* () {
232
- const { users } = buildClient();
233
- const expected = { method: 'POST', path: '/tables/users/query', body: {} };
234
- expectRequest(users, expected, () => users.getMany(), { records: [] });
235
- }));
236
- test('query with one filter', () => __awaiter(void 0, void 0, void 0, function* () {
237
- const { users } = buildClient();
238
- const expected = { method: 'POST', path: '/tables/users/query', body: { filter: { $all: [{ name: 'foo' }] } } };
239
- expectRequest(users, expected, () => users.filter('name', 'foo').getMany(), { records: [] });
240
- }));
241
- });
242
- describe('getOne', () => {
243
- test('returns a single object', () => __awaiter(void 0, void 0, void 0, function* () {
244
- const { users } = buildClient();
245
- const result = { records: [{ id: '1234' }] };
246
- const expected = { method: 'POST', path: '/tables/users/query', body: {} };
247
- expectRequest(users, expected, () => __awaiter(void 0, void 0, void 0, function* () {
248
- const first = yield users.select().getOne();
249
- expect(first === null || first === void 0 ? void 0 : first.id).toBe(result.records[0].id);
250
- }), result);
251
- }));
252
- test('returns null if no objects are returned', () => __awaiter(void 0, void 0, void 0, function* () {
253
- const { users } = buildClient();
254
- const result = { records: [] };
255
- const expected = { method: 'POST', path: '/tables/users/query', body: {} };
256
- expectRequest(users, expected, () => __awaiter(void 0, void 0, void 0, function* () {
257
- const first = yield users.getOne();
258
- expect(first).toBeNull();
259
- }), result);
260
- }));
261
- });
262
- });
263
- describe('read', () => {
264
- test('reads an object by id successfully', () => __awaiter(void 0, void 0, void 0, function* () {
265
- const { users } = buildClient();
266
- const id = 'rec_1234';
267
- const expected = { method: 'GET', path: `/tables/users/data/${id}`, body: undefined };
268
- expectRequest(users, expected, () => users.read(id));
269
- }));
270
- });
271
- describe('Repository.update', () => {
272
- test('updates and object successfully', () => __awaiter(void 0, void 0, void 0, function* () {
273
- const { users } = buildClient();
274
- const object = { id: 'rec_1234', xata: { version: 1 }, name: 'Ada' };
275
- const expected = { method: 'PUT', path: `/tables/users/data/${object.id}`, body: object };
276
- expectRequest(users, expected, () => __awaiter(void 0, void 0, void 0, function* () {
277
- const result = yield users.update(object.id, object);
278
- expect(result.id).toBe(object.id);
279
- }), { id: object.id });
280
- }));
281
- });
282
- describe('Repository.delete', () => {
283
- test('deletes a record by id successfully', () => __awaiter(void 0, void 0, void 0, function* () {
284
- const { users } = buildClient();
285
- const id = 'rec_1234';
286
- const expected = { method: 'DELETE', path: `/tables/users/data/${id}`, body: undefined };
287
- expectRequest(users, expected, () => __awaiter(void 0, void 0, void 0, function* () {
288
- const result = yield users.delete(id);
289
- expect(result).toBe(undefined);
290
- }));
291
- }));
292
- });
293
- describe('create', () => {
294
- test('successful', () => __awaiter(void 0, void 0, void 0, function* () {
295
- const { users } = buildClient();
296
- const created = { id: 'rec_1234', _version: 0 };
297
- const object = { name: 'Ada' };
298
- const expected = { method: 'POST', path: '/tables/users/data', body: object };
299
- expectRequest(users, expected, () => __awaiter(void 0, void 0, void 0, function* () {
300
- const result = yield users.create(object);
301
- expect(result.id).toBe(created.id);
302
- }), created);
303
- }));
304
- });