Repository: xataio/client-ts
Pull request #117 (2022-04-25 11:09:34)

Refactor repository to accept record data
<!-- Add a nice description here -->

<!-- Don't forget the `Closed #{issue_number}` -->

Continues #115



Patch:
diff --git a/.changeset/six-squids-count.md b/.changeset/six-squids-count.md
new file mode 100644
index 00000000..2d6335ed
--- /dev/null
+++ b/.changeset/six-squids-count.md
@@ -0,0 +1,6 @@
+---
+'@xata.io/client': patch
+'@xata.io/codegen': patch
+---
+
+Fix in Selectable type
diff --git a/client/src/schema/pagination.ts b/client/src/schema/pagination.ts
index bbcd88eb..e0c22f01 100644
--- a/client/src/schema/pagination.ts
+++ b/client/src/schema/pagination.ts
@@ -1,5 +1,5 @@
-import { XataRecord } from '..';
 import { Query } from './query';
+import { XataRecord } from './record';
 
 export type PaginationQueryMeta = { page: { cursor: string; more: boolean } };
 
diff --git a/client/src/schema/query.ts b/client/src/schema/query.ts
index df270c05..ead978b4 100644
--- a/client/src/schema/query.ts
+++ b/client/src/schema/query.ts
@@ -1,8 +1,9 @@
-import { Repository, XataRecord } from '..';
 import { ColumnsFilter, FilterExpression, PageConfig, SortExpression } from '../api/schemas';
 import { compact } from '../util/lang';
 import { DeepConstraint, FilterConstraints, SortDirection, SortFilter } from './filters';
 import { Page, Paginable, PaginationOptions, PaginationQueryMeta, PAGINATION_MAX_SIZE } from './pagination';
+import { XataRecord } from './record';
+import { Repository } from './repository';
 import { Select, Selectable, SelectableColumn } from './selection';
 
 export type QueryOptions<T extends XataRecord> = {
diff --git a/client/src/schema/record.ts b/client/src/schema/record.ts
index 305599da..5868a370 100644
--- a/client/src/schema/record.ts
+++ b/client/src/schema/record.ts
@@ -10,6 +10,10 @@ export interface Identifiable {
   id: string;
 }
 
+export interface BaseData {
+  [key: string]: any;
+}
+
 /**
  * Represents a persisted record from the database.
  */
diff --git a/client/src/schema/repository.ts b/client/src/schema/repository.ts
index 0c689b8e..063774a5 100644
--- a/client/src/schema/repository.ts
+++ b/client/src/schema/repository.ts
@@ -12,35 +12,38 @@ import { FetcherExtraProps, FetchImpl } from '../api/fetcher';
 import { buildSortFilter } from './filters';
 import { Page } from './pagination';
 import { Query, QueryOptions } from './query';
-import { XataRecord } from './record';
-import { Select, Selectable, SelectableColumn } from './selection';
+import { BaseData, XataRecord } from './record';
+import { Select, SelectableColumn } from './selection';
 
 export type Links = Record<string, Array<string[]>>;
 
 /**
  * Common interface for performing operations on a table.
  */
-export abstract class Repository<T extends XataRecord> extends Query<T> {
-  /**
+export abstract class Repository<
+  Data extends BaseData,
+  Record extends XataRecord = Data & XataRecord
+> extends Query<Record> {
+  /*
    * Creates a record in the table.
    * @param object Object containing the column names with their values to be stored in the table.
    * @returns The full persisted record.
    */
-  abstract create(object: Selectable<T>): Promise<T>;
+  abstract create(object: Data): Promise<Record>;
 
   /**
    * Creates multiple records in the table.
    * @param objects Array of objects with the column names and the values to be stored in the table.
    * @returns Array of the persisted records.
    */
-  abstract createMany(objects: Selectable<T>[]): Promise<T[]>;
+  abstract createMany(objects: Data[]): Promise<Record[]>;
 
   /**
    * Queries a single record from the table given its unique id.
    * @param id The unique id.
    * @returns The persisted record for the given id or null if the record could not be found.
    */
-  abstract read(id: string): Promise<T | null>;
+  abstract read(id: string): Promise<Record | null>;
 
   /**
    * Insert a single record with a unique id.
@@ -48,7 +51,7 @@ export abstract class Repository<T extends XataRecord> extends Query<T> {
    * @param object Object containing the column names with their values to be stored in the table.
    * @returns The full persisted record.
    */
-  abstract insert(id: string, object: Selectable<T>): Promise<T>;
+  abstract insert(id: string, object: Data): Promise<Record>;
 
   /**
    * Partially update a single record given its unique id.
@@ -56,7 +59,7 @@ export abstract class Repository<T extends XataRecord> extends Query<T> {
    * @param object The column names and their values that have to be updatd.
    * @returns The full persisted record.
    */
-  abstract update(id: string, object: Partial<Selectable<T>>): Promise<T>;
+  abstract update(id: string, object: Partial<Data>): Promise<Record>;
 
   /**
    * Updates or inserts a single record. If a record exists with the given id,
@@ -65,7 +68,7 @@ export abstract class Repository<T extends XataRecord> extends Query<T> {
    * @param object The column names and the values to be persisted.
    * @returns The full persisted record.
    */
-  abstract updateOrInsert(id: string, object: Selectable<T>): Promise<T>;
+  abstract updateOrInsert(id: string, object: Data): Promise<Record>;
 
   /**
    * Deletes a record given its unique id.
@@ -74,18 +77,23 @@ export abstract class Repository<T extends XataRecord> extends Query<T> {
    */
   abstract delete(id: string): void;
 
-  abstract query<R extends XataRecord, Options extends QueryOptions<T>>(
-    query: Query<T, R>,
+  abstract query<Result extends XataRecord, Options extends QueryOptions<Record>>(
+    query: Query<Record, Result>,
     options: Options
   ): Promise<
     Page<
-      T,
-      typeof options extends { columns: SelectableColumn<T>[] } ? Select<T, typeof options['columns'][number]> : R
+      Record,
+      typeof options extends { columns: SelectableColumn<Data>[] }
+        ? Select<Data, typeof options['columns'][number]>
+        : Result
     >
   >;
 }
 
-export class RestRepository<T extends XataRecord> extends Repository<T> {
+export class RestRepository<Data extends BaseData, Record extends XataRecord = Data & XataRecord> extends Repository<
+  Data,
+  Record
+> {
   #client: BaseClient<any>;
   #fetch: any;
   #table: string;
@@ -122,7 +130,7 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
     };
   }
 
-  async create(object: Selectable<T>): Promise<T> {
+  async create(object: Data): Promise<Record> {
     const fetchProps = await this.#getFetchProps();
 
     const record = transformObjectLinks(object);
@@ -145,7 +153,7 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
     return finalObject;
   }
 
-  async createMany(objects: T[]): Promise<T[]> {
+  async createMany(objects: Data[]): Promise<Record[]> {
     const fetchProps = await this.#getFetchProps();
 
     const records = objects.map((object) => transformObjectLinks(object));
@@ -164,7 +172,7 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
     return finalObjects;
   }
 
-  async read(recordId: string): Promise<T | null> {
+  async read(recordId: string): Promise<Record | null> {
     const fetchProps = await this.#getFetchProps();
 
     const response = await getRecord({
@@ -175,7 +183,7 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
     return this.#client.initObject(this.#table, response);
   }
 
-  async update(recordId: string, object: Partial<Selectable<T>>): Promise<T> {
+  async update(recordId: string, object: Partial<Data>): Promise<Record> {
     const fetchProps = await this.#getFetchProps();
 
     const response = await updateRecordWithID({
@@ -190,7 +198,7 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
     return item;
   }
 
-  async insert(recordId: string, object: Selectable<T>): Promise<T> {
+  async insert(recordId: string, object: Data): Promise<Record> {
     const fetchProps = await this.#getFetchProps();
 
     const record = transformObjectLinks(object);
@@ -214,7 +222,7 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
     return finalObject;
   }
 
-  async updateOrInsert(recordId: string, object: Selectable<T>): Promise<T> {
+  async updateOrInsert(recordId: string, object: Data): Promise<Record> {
     const fetchProps = await this.#getFetchProps();
 
     const response = await upsertRecordWithID({
@@ -229,7 +237,7 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
     return item;
   }
 
-  async delete(recordId: string) {
+  async delete(recordId: string): Promise<void> {
     const fetchProps = await this.#getFetchProps();
 
     await deleteRecord({
@@ -238,13 +246,15 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
     });
   }
 
-  async query<R extends XataRecord, Options extends QueryOptions<T>>(
-    query: Query<T, R>,
+  async query<Result extends XataRecord, Options extends QueryOptions<Record>>(
+    query: Query<Record, Result>,
     options: Options = {} as Options
   ): Promise<
     Page<
-      T,
-      typeof options extends { columns: SelectableColumn<T>[] } ? Select<T, typeof options['columns'][number]> : R
+      Record,
+      typeof options extends { columns: SelectableColumn<Data>[] }
+        ? Select<Data, typeof options['columns'][number]>
+        : Result
     >
   > {
     const data = query.getQueryOptions();
@@ -265,22 +275,24 @@ export class RestRepository<T extends XataRecord> extends Repository<T> {
 
     const records = objects.map((record) =>
       this.#client.initObject<
-        typeof options['columns'] extends SelectableColumn<T>[] ? Select<T, typeof options['columns'][number]> : R
+        typeof options['columns'] extends SelectableColumn<Data>[]
+          ? Select<Data, typeof options['columns'][number]>
+          : Result
       >(this.#table, record)
     );
 
     // TODO: We should properly type this any
-    return new Page<T, any>(query, meta, records);
+    return new Page<Record, any>(query, meta, records);
   }
 }
 
 interface RepositoryFactory {
-  createRepository<T extends XataRecord>(client: BaseClient<any>, table: string): Repository<T>;
+  createRepository<Data extends BaseData>(client: BaseClient<any>, table: string): Repository<Data>;
 }
 
 export class RestRespositoryFactory implements RepositoryFactory {
-  createRepository<T extends XataRecord>(client: BaseClient<any>, table: string): Repository<T> {
-    return new RestRepository<T>(client, table);
+  createRepository<Data extends BaseData>(client: BaseClient<any>, table: string): Repository<Data> {
+    return new RestRepository<Data>(client, table);
   }
 }
 
diff --git a/client/src/schema/selection.ts b/client/src/schema/selection.ts
index 1b7d5a0f..71ded135 100644
--- a/client/src/schema/selection.ts
+++ b/client/src/schema/selection.ts
@@ -1,27 +1,12 @@
-import { XataRecord } from '..';
 import { StringKeys, UnionToIntersection, Values } from '../util/types';
 import { Query } from './query';
-import { Identifiable } from './record';
+import { BaseData, Identifiable, XataRecord } from './record';
 
 type Queries<T> = {
-  [key in keyof T as T[key] extends Query<any> ? key : never]: T[key];
+  [key in keyof T as T[key] extends Query<any, any> ? key : never]: T[key];
 };
 
-type OmitQueries<T> = {
-  [key in keyof T as T[key] extends Query<any> ? never : key]: T[key];
-};
-
-type OmitLinks<T> = {
-  [key in keyof T as T[key] extends XataRecord ? never : key]: T[key];
-};
-
-type OmitMethods<T> = {
-  // eslint-disable-next-line @typescript-eslint/ban-types
-  [key in keyof T as T[key] extends Function ? never : key]: T[key];
-};
-
-type InternalProperties = keyof XataRecord;
-export type Selectable<T extends XataRecord> = Omit<T, InternalProperties> & Identifiable;
+export type Selectable<T extends BaseData> = T & Partial<Identifiable>;
 
 export type SelectableColumn<O> =
   | '*'
diff --git a/codegen/example/xata.ts b/codegen/example/xata.ts
index a80de185..71d249be 100644
--- a/codegen/example/xata.ts
+++ b/codegen/example/xata.ts
@@ -1,13 +1,12 @@
 import {
   BaseClient,
   Repository,
-  Identifiable,
   RestRespositoryFactory,
   XataClientOptions,
   XataRecord,
 } from "../../client/src";
 
-export interface Team extends Identifiable {
+export interface Team {
   name?: string | null;
   labels?: string[] | null;
   owner?: User | null;
@@ -15,7 +14,7 @@ export interface Team extends Identifiable {
 
 export type TeamRecord = Team & XataRecord;
 
-export interface User extends Identifiable {
+export interface User {
   email?: string | null;
   full_name?: string | null;
   address?: { street?: string | null; zipcode?: number | null } | null;
@@ -27,8 +26,8 @@ export type UserRecord = User & XataRecord;
 const links = { teams: [["owner", "users"]], users: [["team", "teams"]] };
 
 export class XataClient extends BaseClient<{
-  teams: Repository<TeamRecord>;
-  users: Repository<UserRecord>;
+  teams: Repository<Team>;
+  users: Repository<User>;
 }> {
   constructor(options: XataClientOptions) {
     super(
diff --git a/codegen/src/codegen.ts b/codegen/src/codegen.ts
index 0fc8e789..ff53f9d0 100644
--- a/codegen/src/codegen.ts
+++ b/codegen/src/codegen.ts
@@ -21,7 +21,7 @@ function getTypeName(tableName: string) {
 function generateTableType(table: Table) {
   const { columns } = table;
   const revLinks: { table: string }[] = []; // table.rev_links || [];
-  return `export interface ${getTypeName(table.name)} extends Identifiable {
+  return `export interface ${getTypeName(table.name)} {
     ${columns.map((column) => generateColumnType(column)).join('\n')}
     ${revLinks.map((link) => `${link.table}: Query<${getTypeName(link.table)}>`).join('\n')}
   };
@@ -96,7 +96,6 @@ export async function generate({ schema, config, language, javascriptTarget }: G
     import {
       BaseClient,
       Repository,
-      Identifiable,
       RestRespositoryFactory,
       XataClientOptions,
       XataRecord
@@ -109,7 +108,7 @@ export async function generate({ schema, config, language, javascriptTarget }: G
     const links = ${JSON.stringify(links)};
 
     export class XataClient extends BaseClient<{
-      ${tables.map((table) => `"${table.name}": Repository<${getTypeName(table.name)}Record>;`).join('\n')}
+      ${tables.map((table) => `"${table.name}": Repository<${getTypeName(table.name)}>;`).join('\n')}
     }> {
       constructor(options: XataClientOptions) {
         super({ databaseURL: "https://${config.workspaceID}.xata.sh/db/${config.dbName}", ...options}, links);


Test patch:
diff --git a/test/__snapshots__/codegen.test.ts.snap b/test/__snapshots__/codegen.test.ts.snap
index eca597ce..1e0b5493 100644
--- a/test/__snapshots__/codegen.test.ts.snap
+++ b/test/__snapshots__/codegen.test.ts.snap
@@ -74,13 +74,12 @@ exports[`codegen should generate correct TypeScript 1`] = `
 "import {
   BaseClient,
   Repository,
-  Identifiable,
   RestRespositoryFactory,
   XataClientOptions,
   XataRecord,
 } from \\"@xata.io/client\\";
 
-export interface Author extends Identifiable {
+export interface Author {
   name?: string | null;
   email?: string | null;
   photoUrl?: string | null;
@@ -90,7 +89,7 @@ export interface Author extends Identifiable {
 
 export type AuthorRecord = Author & XataRecord;
 
-export interface Post extends Identifiable {
+export interface Post {
   title?: string | null;
   summary?: string | null;
   content?: string | null;
@@ -104,7 +103,7 @@ export interface Post extends Identifiable {
 
 export type PostRecord = Post & XataRecord;
 
-export interface Person extends Identifiable {
+export interface Person {
   slug?: string | null;
   name?: string | null;
   title?: string | null;
@@ -116,9 +115,9 @@ export type PersonRecord = Person & XataRecord;
 const links = { authors: [], posts: [[\\"author\\", \\"authors\\"]], people: [] };
 
 export class XataClient extends BaseClient<{
-  authors: Repository<AuthorRecord>;
-  posts: Repository<PostRecord>;
-  people: Repository<PersonRecord>;
+  authors: Repository<Author>;
+  posts: Repository<Post>;
+  people: Repository<Person>;
 }> {
   constructor(options: XataClientOptions) {
     super(
diff --git a/test/integration.test.ts b/test/integration.test.ts
index f88b346c..675d8202 100644
--- a/test/integration.test.ts
+++ b/test/integration.test.ts
@@ -8,7 +8,7 @@ import {
   PAGINATION_MAX_OFFSET,
   PAGINATION_MAX_SIZE
 } from '../client/src/schema/pagination';
-import { User, XataClient } from '../codegen/example/xata';
+import { User, UserRecord, XataClient } from '../codegen/example/xata';
 import { mockUsers, teamColumns, userColumns } from './mock_data';
 
 // Get environment variables before reading them
@@ -287,7 +287,7 @@ describe('integration tests', () => {
   });
 
   test('repository implements paginable', async () => {
-    async function foo(page: Paginable<User>): Promise<User[]> {
+    async function foo(page: Paginable<UserRecord>): Promise<UserRecord[]> {
       const nextPage = page.hasNextPage() ? await foo(await page.nextPage()) : [];
       return [...page.records, ...nextPage];
     }