Repository: xataio/client-ts
Pull request #58 (2022-03-22 12:01:55)

Internally use the ApiClient for the SchemaClient


Patch:
diff --git a/client/src/api/client.ts b/client/src/api/client.ts
index 3a47c1cb..f089b3a2 100644
--- a/client/src/api/client.ts
+++ b/client/src/api/client.ts
@@ -12,7 +12,7 @@ export interface XataApiClientOptions {
 }
 
 export class XataApiClient {
-  private extraProps: FetcherExtraProps;
+  #extraProps: FetcherExtraProps;
 
   constructor(options: XataApiClientOptions) {
     const fetchImpl = typeof fetch !== 'undefined' ? fetch : options.fetch;
@@ -25,7 +25,7 @@ export class XataApiClient {
 
     const provider = options.host ?? 'production';
 
-    this.extraProps = {
+    this.#extraProps = {
       apiUrl: getHostUrl(provider, 'main'),
       workspacesApiUrl: getHostUrl(provider, 'workspaces'),
       fetchImpl,
@@ -34,27 +34,27 @@ export class XataApiClient {
   }
 
   public get user() {
-    return new UserApi(this.extraProps);
+    return new UserApi(this.#extraProps);
   }
 
   public get workspaces() {
-    return new WorkspaceApi(this.extraProps);
+    return new WorkspaceApi(this.#extraProps);
   }
 
   public get databases() {
-    return new DatabaseApi(this.extraProps);
+    return new DatabaseApi(this.#extraProps);
   }
 
   public get branches() {
-    return new BranchApi(this.extraProps);
+    return new BranchApi(this.#extraProps);
   }
 
   public get tables() {
-    return new TableApi(this.extraProps);
+    return new TableApi(this.#extraProps);
   }
 
   public get records() {
-    return new RecordsApi(this.extraProps);
+    return new RecordsApi(this.#extraProps);
   }
 }
 
diff --git a/client/src/api/fetcher.ts b/client/src/api/fetcher.ts
index 7b1b35c2..905c08a4 100644
--- a/client/src/api/fetcher.ts
+++ b/client/src/api/fetcher.ts
@@ -14,9 +14,11 @@ export type FetchImpl = (
   init?: { body?: string; headers?: Record<string, string>; method?: string }
 ) => Promise<{ ok: boolean; status: number; json(): Promise<any> }>;
 
+export type WorkspaceApiUrlBuilder = (path: string, pathParams: Record<string, string>) => string;
+
 export type FetcherExtraProps = {
   apiUrl: string;
-  workspacesApiUrl: string;
+  workspacesApiUrl: string | WorkspaceApiUrlBuilder;
   fetchImpl: FetchImpl;
   apiKey: string;
 };
@@ -32,20 +34,35 @@ export type FetcherOptions<TBody, THeaders, TQueryParams, TPathParams> = {
 
 const fallbackError: SimpleError = { message: 'Network response was not ok' };
 
-function baseURLForWorkspace(workspacesApiUrl: string, workspace: string) {
+function buildBaseUrl({
+  path,
+  workspacesApiUrl,
+  apiUrl,
+  pathParams
+}: {
+  path: string;
+  workspacesApiUrl: string | WorkspaceApiUrlBuilder;
+  apiUrl: string;
+  pathParams?: Record<string, string>;
+}) {
+  if (!pathParams?.workspace) return `${apiUrl}${path}`;
+
+  const url = typeof workspacesApiUrl === 'string' ? `${workspacesApiUrl}${path}` : workspacesApiUrl(path, pathParams);
+
   // Node.js on localhost won't resolve localhost subdomains unless mapped in /etc/hosts
   // So, instead, we use localhost without subdomains, but will add a Host header
-  if (typeof window === 'undefined' && workspacesApiUrl.includes('localhost:')) {
-    return workspacesApiUrl.replace('{workspaceId}.', '');
+  if (typeof window === 'undefined' && url.includes('localhost:')) {
+    return url.replace('{workspaceId}.', '');
   }
-  return workspacesApiUrl.replace('{workspaceId}', workspace);
+
+  return url.replace('{workspaceId}', pathParams.workspace);
 }
 
-function hostHeaderForWorkspace(workspacesApiUrl: string, workspace: string) {
-  const url = baseURLForWorkspace(workspacesApiUrl, workspace);
-  if (!url) return;
-  const [, hostname] = url.split('://');
-  return hostname;
+function hostHeaderForWorkspace(url: string) {
+  const pattern = /.*:\/\/(?<host>[^/]+).*/;
+  const { groups } = pattern.exec(url) ?? {};
+
+  return groups?.host;
 }
 
 export async function fetch<
@@ -55,7 +72,7 @@ export async function fetch<
   TQueryParams extends Record<string, unknown>,
   TPathParams extends Record<string, string>
 >({
-  url,
+  url: path,
   method,
   body,
   headers,
@@ -66,8 +83,10 @@ export async function fetch<
   apiUrl,
   workspacesApiUrl
 }: FetcherOptions<TBody, THeaders, TQueryParams, TPathParams> & FetcherExtraProps): Promise<TData> {
-  const baseURL = pathParams?.workspace ? baseURLForWorkspace(workspacesApiUrl, pathParams.workspace) : apiUrl;
-  const response = await fetchImpl(`${baseURL}${resolveUrl(url, queryParams, pathParams)}`, {
+  const baseUrl = buildBaseUrl({ path, workspacesApiUrl, pathParams, apiUrl });
+  const url = resolveUrl(baseUrl, queryParams, pathParams);
+
+  const response = await fetchImpl(url, {
     method: method.toUpperCase(),
     body: body ? JSON.stringify(body) : undefined,
     headers: {
@@ -76,7 +95,7 @@ export async function fetch<
       Authorization: `Bearer ${apiKey}`,
       // The host header is needed by Node.js on localhost.
       // It is ignored by fetch() in the frontend
-      ...(pathParams?.workspace ? { Host: hostHeaderForWorkspace(workspacesApiUrl, pathParams.workspace) } : {})
+      ...(pathParams?.workspace ? { Host: hostHeaderForWorkspace(url) } : {})
     }
   });
 
diff --git a/client/src/index.ts b/client/src/index.ts
index a32b4669..ee1e6c95 100644
--- a/client/src/index.ts
+++ b/client/src/index.ts
@@ -1,3 +1,5 @@
+import { bulkInsertTableRecords, deleteRecord, getRecord, insertRecord, insertRecordWithID, queryTable } from './api';
+import { FetcherExtraProps, FetchImpl } from './api/fetcher';
 import { buildSortFilter } from './schema/filters';
 import { Page } from './schema/pagination';
 import { Query, QueryOptions } from './schema/query';
@@ -52,50 +54,33 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
     this.#fetch = fetchImpl;
   }
 
-  async request<T>(method: string, path: string, body?: unknown): Promise<T | undefined> {
-    const { databaseURL, apiKey } = this.#client.options;
+  async #getFetchProps(): Promise<FetcherExtraProps> {
     const branch = await this.#client.getBranch();
-    const fetchImpl = this.#fetch;
-
-    const resp: Response = await fetchImpl(`${databaseURL}:${branch}${path}`, {
-      method,
-      headers: {
-        Accept: '*/*',
-        'Content-Type': 'application/json',
-        Authorization: `Bearer ${apiKey}`
-      },
-      body: JSON.stringify(body)
-    });
 
-    if (!resp.ok) {
-      try {
-        const json = await resp.json();
-        const message = json.message;
-        if (typeof message === 'string') {
-          throw new XataError(message, resp.status);
-        }
-      } catch (err) {
-        if (err instanceof XataError) throw err;
-        // Ignore errors for other reasons.
-        // For example if the response's body cannot be parsed as JSON
+    return {
+      fetchImpl: this.#fetch,
+      apiKey: this.#client.options.apiKey,
+      apiUrl: '',
+      // Instead of using workspace and dbBranch, we inject a probably CNAME'd URL
+      workspacesApiUrl: (path, params) => {
+        const baseUrl = this.#client.options.databaseURL ?? '';
+        const hasBranch = params.dbBranchName ?? params.branch;
+        const newPath = path.replace(/^\/db\/[^/]+/, hasBranch ? `:${branch}` : '');
+        return baseUrl + newPath;
       }
-      throw new XataError(resp.statusText, resp.status);
-    }
-
-    if (resp.status === 204) return undefined;
-    return resp.json();
+    };
   }
 
   async create(object: T): Promise<T> {
+    const fetchProps = await this.#getFetchProps();
+
     const record = transformObjectLinks(object);
 
-    const response = await this.request<{
-      id: string;
-      xata: { version: number };
-    }>('POST', `/tables/${this.#table}/data`, record);
-    if (!response) {
-      throw new Error("The server didn't return any data for the query");
-    }
+    const response = await insertRecord({
+      pathParams: { workspace: '{workspaceId}', dbBranchName: '{dbBranch}', tableName: this.#table },
+      body: record,
+      ...fetchProps
+    });
 
     const finalObject = await this.read(response.id);
     if (!finalObject) {
@@ -106,14 +91,15 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
   }
 
   async createMany(objects: T[]): Promise<T[]> {
+    const fetchProps = await this.#getFetchProps();
+
     const records = objects.map((object) => transformObjectLinks(object));
 
-    const response = await this.request<{
-      recordIDs: string[];
-    }>('POST', `/tables/${this.#table}/bulk`, { records });
-    if (!response) {
-      throw new Error("The server didn't return any data for the query");
-    }
+    const response = await bulkInsertTableRecords({
+      pathParams: { workspace: '{workspaceId}', dbBranchName: '{dbBranch}', tableName: this.#table },
+      body: { records },
+      ...fetchProps
+    });
 
     // TODO: Use filer.$any() to get all the records
     const finalObjects = await Promise.all(response.recordIDs.map((id) => this.read(id)));
@@ -124,35 +110,37 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
     return finalObjects as T[];
   }
 
-  async read(id: string): Promise<T | null> {
-    try {
-      const response = await this.request<
-        T & { id: string; xata: { version: number; table?: string; warnings?: string[] } }
-      >('GET', `/tables/${this.#table}/data/${id}`);
-      if (!response) return null;
-
-      return this.#client.initObject(this.#table, response);
-    } catch (err) {
-      if ((err as XataError).status === 404) return null;
-      throw err;
-    }
+  async read(recordId: string): Promise<T | null> {
+    const fetchProps = await this.#getFetchProps();
+
+    const response = await getRecord({
+      pathParams: { workspace: '{workspaceId}', dbBranchName: '{dbBranch}', tableName: this.#table, recordId },
+      ...fetchProps
+    });
+
+    return this.#client.initObject(this.#table, response);
   }
 
-  async update(id: string, object: Partial<T>): Promise<T> {
-    const response = await this.request<{
-      id: string;
-      xata: { version: number };
-    }>('PUT', `/tables/${this.#table}/data/${id}`, object);
-    if (!response) {
-      throw new Error("The server didn't return any data for the query");
-    }
+  async update(recordId: string, object: Partial<T>): Promise<T> {
+    const fetchProps = await this.#getFetchProps();
+
+    const response = await insertRecordWithID({
+      pathParams: { workspace: '{workspaceId}', dbBranchName: '{dbBranch}', tableName: this.#table, recordId },
+      body: object,
+      ...fetchProps
+    });
 
     // TODO: Review this, not sure we are properly initializing the object
     return this.#client.initObject(this.#table, response);
   }
 
-  async delete(id: string) {
-    await this.request('DELETE', `/tables/${this.#table}/data/${id}`);
+  async delete(recordId: string) {
+    const fetchProps = await this.#getFetchProps();
+
+    await deleteRecord({
+      pathParams: { workspace: '{workspaceId}', dbBranchName: '{dbBranch}', tableName: this.#table, recordId },
+      ...fetchProps
+    });
   }
 
   async query<R extends XataRecord, Options extends QueryOptions<T>>(
@@ -170,15 +158,13 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
       columns: options?.columns ?? data.columns
     };
 
-    const response = await this.request<{
-      records: object[];
-      meta: { page: { cursor: string; more: boolean } };
-    }>('POST', `/tables/${this.#table}/query`, body);
-    if (!response) {
-      throw new Error("The server didn't return any data for the query");
-    }
+    const fetchProps = await this.#getFetchProps();
+    const { meta, records: objects } = await queryTable({
+      pathParams: { workspace: '{workspaceId}', dbBranchName: '{dbBranch}', tableName: this.#table },
+      body,
+      ...fetchProps
+    });
 
-    const { meta, records: objects } = response;
     const records = objects.map((record) =>
       this.#client.initObject<
         typeof options['columns'] extends SelectableColumn<T>[] ? Select<T, typeof options['columns'][number]> : R
@@ -206,7 +192,7 @@ type BranchStrategy = BranchStrategyValue | BranchStrategyBuilder;
 type BranchStrategyOption = NonNullable<BranchStrategy | BranchStrategy[]>;
 
 export type XataClientOptions = {
-  fetch?: unknown;
+  fetch?: FetchImpl;
   databaseURL?: string;
   branch: BranchStrategyOption;
   apiKey: string;
@@ -214,9 +200,10 @@ export type XataClientOptions = {
 };
 
 export class BaseClient<D extends Record<string, Repository<any>>> {
+  #links: Links;
+  #branch: BranchStrategyValue;
+
   options: XataClientOptions;
-  private links: Links;
-  private branch: BranchStrategyValue;
   db!: D;
 
   constructor(options: XataClientOptions, links: Links) {
@@ -225,14 +212,14 @@ export class BaseClient<D extends Record<string, Repository<any>>> {
     }
 
     this.options = options;
-    this.links = links;
+    this.#links = links;
   }
 
   public initObject<T>(table: string, object: object) {
     const o: Record<string, unknown> = {};
     Object.assign(o, object);
 
-    const tableLinks = this.links[table] || [];
+    const tableLinks = this.#links[table] || [];
     for (const link of tableLinks) {
       const [field, linkTable] = link;
       const value = o[field];
@@ -274,7 +261,7 @@ export class BaseClient<D extends Record<string, Repository<any>>> {
   }
 
   public async getBranch(): Promise<string> {
-    if (this.branch) return this.branch;
+    if (this.#branch) return this.#branch;
 
     const { branch: param } = this.options;
     const strategies = Array.isArray(param) ? [...param] : [param];
@@ -286,7 +273,7 @@ export class BaseClient<D extends Record<string, Repository<any>>> {
     for await (const strategy of strategies) {
       const branch = await evaluateBranch(strategy);
       if (branch) {
-        this.branch = branch;
+        this.#branch = branch;
         return branch;
       }
     }
diff --git a/client/src/schema/query.ts b/client/src/schema/query.ts
index 51b34b57..68a7ad63 100644
--- a/client/src/schema/query.ts
+++ b/client/src/schema/query.ts
@@ -1,4 +1,5 @@
 import { XataRecord, Repository } from '..';
+import { FilterExpression, SortExpression, PageConfig, ColumnsFilter } from '../api/schemas';
 import { compact } from '../util/lang';
 import { Constraint, DeepConstraint, FilterConstraints, SortDirection, SortFilter } from './filters';
 import { PaginationOptions, Page, Paginable, PaginationQueryMeta } from './pagination';
@@ -6,93 +7,11 @@ import { Selectable, SelectableColumn, Select } from './selection';
 
 export type QueryOptions<T> = {
   page?: PaginationOptions;
-  columns?: Array<keyof Selectable<T>>;
+  columns?: Extract<keyof Selectable<T>, string>[];
   //filter?: FilterConstraints<T>;
   sort?: SortFilter<T> | SortFilter<T>[];
 };
 
-export type FilterExpression = {
-  $exists?: string;
-  $existsNot?: string;
-  $any?: FilterList;
-  $all?: FilterList;
-  $none?: FilterList;
-  $not?: FilterList;
-} & {
-  [key: string]: FilterColumn;
-};
-
-export type FilterList = FilterExpression | FilterExpression[];
-export type FilterColumn = FilterColumnIncludes | FilterPredicate | FilterList;
-
-/**
- * @maxProperties 1
- * @minProperties 1
- */
-export type FilterColumnIncludes = {
-  $includes?: FilterPredicate;
-  $includesAny?: FilterPredicate;
-  $includesAll?: FilterPredicate;
-  $includesNone?: FilterPredicate;
-};
-
-export type FilterPredicate = FilterValue | FilterPredicate[] | FilterPredicateOp | FilterPredicateRangeOp;
-
-/**
- * @maxProperties 1
- * @minProperties 1
- */
-export type FilterPredicateOp = {
-  $any?: FilterPredicate[];
-  $all?: FilterPredicate[];
-  $none?: FilterPredicate | FilterPredicate[];
-  $not?: FilterPredicate | FilterPredicate[];
-  $is?: FilterValue | FilterValue[];
-  $isNot?: FilterValue | FilterValue[];
-  $lt?: FilterRangeValue;
-  $le?: FilterRangeValue;
-  $gt?: FilterRangeValue;
-  $ge?: FilterRangeValue;
-  $contains?: string;
-  $startsWith?: string;
-  $endsWith?: string;
-  $pattern?: string;
-};
-
-export type FilterPredicateRangeOp = {
-  $lt?: FilterRangeValue;
-  $le?: FilterRangeValue;
-  $gt?: FilterRangeValue;
-  $ge?: FilterRangeValue;
-};
-
-export type FilterRangeValue = number | string;
-
-export type FilterValue = number | string | boolean;
-
-export type SortExpression =
-  | string[]
-  | {
-      [key: string]: SortOrder;
-    }
-  | {
-      [key: string]: SortOrder;
-    }[];
-
-export type SortOrder = 'asc' | 'desc';
-
-export type PageConfig = {
-  after?: string;
-  before?: string;
-  first?: string;
-  last?: string;
-  size?: number;
-  offset?: number;
-};
-
-export type ColumnsFilter = string[];
-
-// TODO: Remove all these types with API Schemas PR
 export type QueryTableOptions = {
   filter: FilterExpression;
   sort?: SortExpression;


Test patch:
diff --git a/client/src/index.test.ts b/client/src/index.test.ts
index 5eb0e43c..10e91385 100644
--- a/client/src/index.test.ts
+++ b/client/src/index.test.ts
@@ -39,7 +39,7 @@ describe('client options', () => {
   test('throws if branch cannot be resolved', () => {
     const { users } = buildClient({ branch: () => null });
 
-    expect(users.request('GET', '/foo')).rejects.toThrow('Unable to resolve branch value');
+    expect(users.getOne()).rejects.toThrow('Unable to resolve branch value');
   });
 
   test('provide branch as a string', async () => {
@@ -48,24 +48,27 @@ describe('client options', () => {
     fetch.mockReset().mockImplementation(() => {
       return {
         ok: true,
-        json: async () => ({})
+        json: async () => ({
+          records: [],
+          meta: { page: { cursor: '', more: false } }
+        })
       };
     });
 
-    await users.request('GET', '/foo');
+    await users.getOne();
 
     expect(fetch).toHaveBeenCalledTimes(1);
     expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
       Array [
-        "https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
+        "https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/tables/users/query",
         Object {
-          "body": undefined,
+          "body": "{\\"page\\":{\\"size\\":1},\\"columns\\":[\\"*\\"]}",
           "headers": Object {
-            "Accept": "*/*",
             "Authorization": "Bearer 1234",
             "Content-Type": "application/json",
+            "Host": "my-workspace-5df34do.staging.xatabase.co",
           },
-          "method": "GET",
+          "method": "POST",
         },
       ]
     `);
@@ -79,24 +82,27 @@ describe('client options', () => {
     fetch.mockReset().mockImplementation(() => {
       return {
         ok: true,
-        json: async () => ({})
+        json: async () => ({
+          records: [],
+          meta: { page: { cursor: '', more: false } }
+        })
       };
     });
 
-    await users.request('GET', '/foo');
+    await users.getOne();
 
     expect(fetch).toHaveBeenCalledTimes(1);
     expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
       Array [
-        "https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
+        "https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/tables/users/query",
         Object {
-          "body": undefined,
+          "body": "{\\"page\\":{\\"size\\":1},\\"columns\\":[\\"*\\"]}",
           "headers": Object {
-            "Accept": "*/*",
             "Authorization": "Bearer 1234",
             "Content-Type": "application/json",
+            "Host": "my-workspace-5df34do.staging.xatabase.co",
           },
-          "method": "GET",
+          "method": "POST",
         },
       ]
     `);
@@ -108,24 +114,27 @@ describe('client options', () => {
     fetch.mockReset().mockImplementation(() => {
       return {
         ok: true,
-        json: async () => ({})
+        json: async () => ({
+          records: [],
+          meta: { page: { cursor: '', more: false } }
+        })
       };
     });
 
-    await users.request('GET', '/foo');
+    await users.getOne();
 
     expect(fetch).toHaveBeenCalledTimes(1);
     expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
       Array [
-        "https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/foo",
+        "https://my-workspace-5df34do.staging.xatabase.co/db/xata:branch/tables/users/query",
         Object {
-          "body": undefined,
+          "body": "{\\"page\\":{\\"size\\":1},\\"columns\\":[\\"*\\"]}",
           "headers": Object {
-            "Accept": "*/*",
             "Authorization": "Bearer 1234",
             "Content-Type": "application/json",
+            "Host": "my-workspace-5df34do.staging.xatabase.co",
           },
-          "method": "GET",
+          "method": "POST",
         },
       ]
     `);
@@ -139,12 +148,15 @@ describe('client options', () => {
     fetch.mockReset().mockImplementation(() => {
       return {
         ok: true,
-        json: async () => ({})
+        json: async () => ({
+          records: [],
+          meta: { page: { cursor: '', more: false } }
+        })
       };
     });
 
-    await users.request('GET', '/foo');
-    await users.request('GET', '/foo');
+    await users.getOne();
+    await users.getMany();
 
     expect(branchGetter).toHaveBeenCalledTimes(1);
   });
@@ -157,24 +169,27 @@ describe('request', () => {
     fetch.mockReset().mockImplementation(() => {
       return {
         ok: true,
-        json: async () => ({})
+        json: async () => ({
+          records: [],
+          meta: { page: { cursor: '', more: false } }
+        })
       };
     });
 
-    await users.request('GET', '/foo');
+    await users.getOne();
 
     expect(fetch).toHaveBeenCalledTimes(1);
     expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
       Array [
-        "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/foo",
+        "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/tables/users/query",
         Object {
-          "body": undefined,
+          "body": "{\\"page\\":{\\"size\\":1},\\"columns\\":[\\"*\\"]}",
           "headers": Object {
-            "Accept": "*/*",
             "Authorization": "Bearer 1234",
             "Content-Type": "application/json",
+            "Host": "my-workspace-5df34do.staging.xatabase.co",
           },
-          "method": "GET",
+          "method": "POST",
         },
       ]
     `);
@@ -186,22 +201,25 @@ describe('request', () => {
     fetch.mockReset().mockImplementation(() => {
       return {
         ok: true,
-        json: async () => ({})
+        json: async () => ({
+          records: [],
+          meta: { page: { cursor: '', more: false } }
+        })
       };
     });
 
-    await users.request('POST', '/foo', { a: 1 });
+    await users.getMany({ page: { size: 20 } });
 
     expect(fetch).toHaveBeenCalledTimes(1);
     expect(fetch.mock.calls[0]).toMatchInlineSnapshot(`
       Array [
-        "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/foo",
+        "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/tables/users/query",
         Object {
-          "body": "{\\"a\\":1}",
+          "body": "{\\"page\\":{\\"size\\":20},\\"columns\\":[\\"*\\"]}",
           "headers": Object {
-            "Accept": "*/*",
             "Authorization": "Bearer 1234",
             "Content-Type": "application/json",
+            "Host": "my-workspace-5df34do.staging.xatabase.co",
           },
           "method": "POST",
         },
@@ -212,44 +230,32 @@ describe('request', () => {
   test('throws if the response is not ok', async () => {
     const { fetch, users } = buildClient();
 
-    fetch.mockImplementation(() => {
-      return {
-        ok: false,
-        status: 404,
-        statusText: 'Not Found'
-      };
-    });
-
-    expect(users.request('GET', '/foo')).rejects.toThrow(new XataError('Not Found', 404));
-  });
-
-  test('throws with the error from the server if the response is not ok', async () => {
-    const { fetch, users } = buildClient();
-
-    fetch.mockImplementation(() => {
+    fetch.mockReset().mockImplementation(async () => {
       return {
         ok: false,
         status: 404,
-        statusText: 'Not Found',
-        json: async () => ({ message: 'Resource not found' })
+        json: async () => ({ message: 'Not Found' })
       };
     });
 
-    expect(users.request('GET', '/foo')).rejects.toThrow(new XataError('Resource not found', 404));
+    expect(users.getOne()).rejects.toThrowErrorMatchingInlineSnapshot(`"Not Found"`);
   });
 
   test('returns the json body if the response is ok', async () => {
     const { fetch, users } = buildClient();
 
     const json = { a: 1 };
-    fetch.mockImplementation(() => {
+    fetch.mockReset().mockImplementation(() => {
       return {
         ok: true,
-        json: async () => json
+        json: async () => ({
+          records: [json],
+          meta: { page: { cursor: '', more: false } }
+        })
       };
     });
 
-    const result = await users.request('GET', '/foo');
+    const result = await users.getOne();
     expect(result).toEqual(json);
   });
 });
@@ -261,111 +267,189 @@ type ExpectedRequest = {
 };
 
 async function expectRequest(
-  users: RestRepository<User>,
+  fetch: jest.Mock<any, any>,
   expectedRequest: ExpectedRequest[] | ExpectedRequest,
   callback: () => void,
   response?: any
-) {
-  const request = jest.fn(async () => response);
-  users.request = request;
+): Promise<any[]> {
+  fetch.mockReset().mockImplementation(() => {
+    return {
+      ok: true,
+      json: async () => response
+    };
+  });
 
   await callback();
 
-  const { calls } = request.mock;
+  const { calls } = fetch.mock;
 
   const requests = Array.isArray(expectedRequest) ? expectedRequest : [expectedRequest];
 
   expect(calls.length).toBe(requests.length);
 
-  for (let i = 0; i < calls.length; i++) {
-    const [method, path, body] = calls[i] as any;
-    expect(method).toBe(requests[i].method);
-    expect(path).toBe(requests[i].path);
-    expect(JSON.stringify(body)).toBe(JSON.stringify(requests[i].body));
-  }
+  return calls;
 }
 
 describe('query', () => {
   describe('getMany', () => {
     test('simple query', async () => {
-      const { users } = buildClient();
+      const { fetch, users } = buildClient();
 
-      const expected = { method: 'POST', path: '/tables/users/query', body: { columns: ['*'] } };
-      expectRequest(users, expected, () => users.getMany(), {
+      const expected = { method: 'POST', path: '/tables/users/query', body: {} };
+      const result = await expectRequest(fetch, expected, () => users.getMany(), {
         records: [],
         meta: { page: { cursor: '', more: false } }
       });
+
+      expect(result).toMatchInlineSnapshot(`
+        Array [
+          Array [
+            "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/tables/users/query",
+            Object {
+              "body": "{\\"columns\\":[\\"*\\"]}",
+              "headers": Object {
+                "Authorization": "Bearer 1234",
+                "Content-Type": "application/json",
+                "Host": "my-workspace-5df34do.staging.xatabase.co",
+              },
+              "method": "POST",
+            },
+          ],
+        ]
+      `);
     });
 
     test('query with one filter', async () => {
-      const { users } = buildClient();
+      const { fetch, users } = buildClient();
 
-      const expected = {
-        method: 'POST',
-        path: '/tables/users/query',
-        body: { filter: { $all: [{ name: 'foo' }] }, columns: ['*'] }
-      };
-      expectRequest(users, expected, () => users.filter('name', 'foo').getMany(), {
+      const expected = { method: 'POST', path: '/tables/users/query', body: { filter: { $all: [{ name: 'foo' }] } } };
+      const result = await expectRequest(fetch, expected, () => users.filter('name', 'foo').getMany(), {
         records: [],
         meta: { page: { cursor: '', more: false } }
       });
+
+      expect(result).toMatchInlineSnapshot(`
+        Array [
+          Array [
+            "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/tables/users/query",
+            Object {
+              "body": "{\\"filter\\":{\\"$all\\":[{\\"name\\":\\"foo\\"}]},\\"columns\\":[\\"*\\"]}",
+              "headers": Object {
+                "Authorization": "Bearer 1234",
+                "Content-Type": "application/json",
+                "Host": "my-workspace-5df34do.staging.xatabase.co",
+              },
+              "method": "POST",
+            },
+          ],
+        ]
+      `);
     });
   });
 
   describe('getOne', () => {
     test('returns a single object', async () => {
-      const { users } = buildClient();
+      const { fetch, users } = buildClient();
 
-      const result = { records: [{ id: '1234', name: 'Name' }], meta: { page: { cursor: '', more: false } } };
-      const expected = { method: 'POST', path: '/tables/users/query', body: { page: { size: 1 }, columns: ['name'] } };
-      expectRequest(
-        users,
+      const resultBody = { records: [{ id: '1234' }], meta: { page: { cursor: '', more: false } } };
+      const expected = { method: 'POST', path: '/tables/users/query', body: { page: { size: 1 } } };
+      const result = await expectRequest(
+        fetch,
         expected,
         async () => {
-          const first = await users.select(['name']).getOne();
-          expect(first?.id).toBe(result.records[0].id);
-          expect(first?.name).toBe(result.records[0].name);
+          const first = await users.getOne();
+          expect(first?.id).toBe(resultBody.records[0].id);
         },
-        result
+        resultBody
       );
+
+      expect(result).toMatchInlineSnapshot(`
+        Array [
+          Array [
+            "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/tables/users/query",
+            Object {
+              "body": "{\\"page\\":{\\"size\\":1},\\"columns\\":[\\"*\\"]}",
+              "headers": Object {
+                "Authorization": "Bearer 1234",
+                "Content-Type": "application/json",
+                "Host": "my-workspace-5df34do.staging.xatabase.co",
+              },
+              "method": "POST",
+            },
+          ],
+        ]
+      `);
     });
 
     test('returns null if no objects are returned', async () => {
-      const { users } = buildClient();
+      const { fetch, users } = buildClient();
 
-      const result = { records: [], meta: { page: { cursor: '', more: false } } };
-      const expected = { method: 'POST', path: '/tables/users/query', body: { page: { size: 1 }, columns: ['*'] } };
-      expectRequest(
-        users,
+      const expected = { method: 'POST', path: '/tables/users/query', body: { page: { size: 1 } } };
+      const result = await expectRequest(
+        fetch,
         expected,
         async () => {
           const first = await users.getOne();
           expect(first).toBeNull();
         },
-        result
+        { records: [], meta: { page: { cursor: '', more: false } } }
       );
+
+      expect(result).toMatchInlineSnapshot(`
+        Array [
+          Array [
+            "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/tables/users/query",
+            Object {
+              "body": "{\\"page\\":{\\"size\\":1},\\"columns\\":[\\"*\\"]}",
+              "headers": Object {
+                "Authorization": "Bearer 1234",
+                "Content-Type": "application/json",
+                "Host": "my-workspace-5df34do.staging.xatabase.co",
+              },
+              "method": "POST",
+            },
+          ],
+        ]
+      `);
     });
   });
 });
 
 describe('read', () => {
   test('reads an object by id successfully', async () => {
-    const { users } = buildClient();
+    const { fetch, users } = buildClient();
 
     const id = 'rec_1234';
     const expected = { method: 'GET', path: `/tables/users/data/${id}`, body: undefined };
-    expectRequest(users, expected, () => users.read(id));
+    const result = await expectRequest(fetch, expected, () => users.read(id));
+
+    expect(result).toMatchInlineSnapshot(`
+      Array [
+        Array [
+          "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/tables/users/data/rec_1234",
+          Object {
+            "body": undefined,
+            "headers": Object {
+              "Authorization": "Bearer 1234",
+              "Content-Type": "application/json",
+              "Host": "my-workspace-5df34do.staging.xatabase.co",
+            },
+            "method": "GET",
+          },
+        ],
+      ]
+    `);
   });
 });
 
 describe('Repository.update', () => {
   test('updates and object successfully', async () => {
-    const { users } = buildClient();
+    const { fetch, users } = buildClient();
 
     const object = { id: 'rec_1234', xata: { version: 1 }, name: 'Ada' } as User;
     const expected = { method: 'PUT', path: `/tables/users/data/${object.id}`, body: object };
-    expectRequest(
-      users,
+    const result = await expectRequest(
+      fetch,
       expected,
       async () => {
         const result = await users.update(object.id, object);
@@ -373,25 +457,59 @@ describe('Repository.update', () => {
       },
       { id: object.id }
     );
+
+    expect(result).toMatchInlineSnapshot(`
+      Array [
+        Array [
+          "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/tables/users/data/rec_1234",
+          Object {
+            "body": "{\\"id\\":\\"rec_1234\\",\\"xata\\":{\\"version\\":1},\\"name\\":\\"Ada\\"}",
+            "headers": Object {
+              "Authorization": "Bearer 1234",
+              "Content-Type": "application/json",
+              "Host": "my-workspace-5df34do.staging.xatabase.co",
+            },
+            "method": "PUT",
+          },
+        ],
+      ]
+    `);
   });
 });
 
 describe('Repository.delete', () => {
   test('deletes a record by id successfully', async () => {
-    const { users } = buildClient();
+    const { fetch, users } = buildClient();
 
     const id = 'rec_1234';
     const expected = { method: 'DELETE', path: `/tables/users/data/${id}`, body: undefined };
-    expectRequest(users, expected, async () => {
+    const result = await expectRequest(fetch, expected, async () => {
       const result = await users.delete(id);
       expect(result).toBe(undefined);
     });
+
+    expect(result).toMatchInlineSnapshot(`
+      Array [
+        Array [
+          "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/tables/users/data/rec_1234",
+          Object {
+            "body": undefined,
+            "headers": Object {
+              "Authorization": "Bearer 1234",
+              "Content-Type": "application/json",
+              "Host": "my-workspace-5df34do.staging.xatabase.co",
+            },
+            "method": "DELETE",
+          },
+        ],
+      ]
+    `);
   });
 });
 
 describe('create', () => {
   test('successful', async () => {
-    const { users } = buildClient();
+    const { fetch, users } = buildClient();
 
     const created = { id: 'rec_1234', _version: 0 };
     const object = { name: 'Ada' } as User;
@@ -404,8 +522,8 @@ describe('create', () => {
       }
     ];
 
-    expectRequest(
-      users,
+    const result = await expectRequest(
+      fetch,
       expected,
       async () => {
         const result = await users.create(object);
@@ -413,5 +531,34 @@ describe('create', () => {
       },
       created
     );
+
+    expect(result).toMatchInlineSnapshot(`
+      Array [
+        Array [
+          "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/tables/users/data",
+          Object {
+            "body": "{\\"name\\":\\"Ada\\"}",
+            "headers": Object {
+              "Authorization": "Bearer 1234",
+              "Content-Type": "application/json",
+              "Host": "my-workspace-5df34do.staging.xatabase.co",
+            },
+            "method": "POST",
+          },
+        ],
+        Array [
+          "https://my-workspace-5df34do.staging.xatabase.co/db/xata:main/tables/users/data/rec_1234",
+          Object {
+            "body": undefined,
+            "headers": Object {
+              "Authorization": "Bearer 1234",
+              "Content-Type": "application/json",
+              "Host": "my-workspace-5df34do.staging.xatabase.co",
+            },
+            "method": "GET",
+          },
+        ],
+      ]
+    `);
   });
 });