diff --git a/packages/common/infra/src/orm/affine/client.ts b/packages/common/infra/src/orm/affine/client.ts deleted file mode 100644 index fc77da83bf..0000000000 --- a/packages/common/infra/src/orm/affine/client.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createORMClientType } from '../core'; -import { AFFiNE_DB_SCHEMA } from './schema'; - -export const ORMClient = createORMClientType(AFFiNE_DB_SCHEMA); diff --git a/packages/common/infra/src/orm/affine/hooks.ts b/packages/common/infra/src/orm/affine/hooks.ts deleted file mode 100644 index 727c3111f5..0000000000 --- a/packages/common/infra/src/orm/affine/hooks.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ORMClient } from './client'; - -// The ORM hooks are used to define the transformers that will be applied on entities when they are loaded from the data providers. -// All transformers are doing in memory, none of the data under the hood will be changed. -// -// for example: -// data in providers: { color: 'red' } -// hook: { color: 'red' } => { color: '#FF0000' } -// -// ORMClient.defineHook( -// 'demo', -// 'deprecate color field and introduce colors filed', -// { -// deserialize(tag) { -// tag.color = stringToHex(tag.color) -// return tag; -// }, -// } -// ); - -export { ORMClient }; diff --git a/packages/common/infra/src/orm/affine/index.ts b/packages/common/infra/src/orm/affine/index.ts deleted file mode 100644 index e0cd954226..0000000000 --- a/packages/common/infra/src/orm/affine/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import './hooks'; - -export { ORMClient } from './client'; diff --git a/packages/common/infra/src/orm/affine/schema.ts b/packages/common/infra/src/orm/affine/schema.ts deleted file mode 100644 index be2e6f903f..0000000000 --- a/packages/common/infra/src/orm/affine/schema.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { DBSchemaBuilder } from '../core'; -// import { f } from './core'; - -export const AFFiNE_DB_SCHEMA = { - // demo: { - // id: f.string().primaryKey().optional().default(nanoid), - // name: f.string(), - // // v1 - // // color: f.string(), - // // v2, without data level breaking change - // /** - // * @deprecated use [colors] - // */ - // color: f.string().optional(), // <= mark as optional since new created record might only have [colors] field - // colors: f.json().optional(), // <= mark as optional since old records might only have [color] field - // }, -} as const satisfies DBSchemaBuilder; diff --git a/packages/common/infra/src/orm/core/__tests__/entity.spec.ts b/packages/common/infra/src/orm/core/__tests__/entity.spec.ts index 9d9d2c778f..88aafbfe29 100644 --- a/packages/common/infra/src/orm/core/__tests__/entity.spec.ts +++ b/packages/common/infra/src/orm/core/__tests__/entity.spec.ts @@ -1,18 +1,12 @@ import { nanoid } from 'nanoid'; -import { - afterEach, - beforeEach, - describe, - expect, - test as t, - type TestAPI, -} from 'vitest'; +import { beforeEach, describe, expect, test as t, type TestAPI } from 'vitest'; import { - createORMClientType, + createORMClient, type DBSchemaBuilder, f, MemoryORMAdapter, + type ORMClient, Table, } from '../'; @@ -24,18 +18,12 @@ const TEST_SCHEMA = { }, } satisfies DBSchemaBuilder; -const Client = createORMClientType(TEST_SCHEMA); type Context = { - client: InstanceType; + client: ORMClient; }; beforeEach(async t => { - t.client = new Client(new MemoryORMAdapter()); - await t.client.connect(); -}); - -afterEach(async t => { - await t.client.disconnect(); + t.client = createORMClient(TEST_SCHEMA, MemoryORMAdapter); }); const test = t as TestAPI; diff --git a/packages/common/infra/src/orm/core/__tests__/hook.spec.ts b/packages/common/infra/src/orm/core/__tests__/hook.spec.ts index ca2618e4e1..4a2b79ee55 100644 --- a/packages/common/infra/src/orm/core/__tests__/hook.spec.ts +++ b/packages/common/infra/src/orm/core/__tests__/hook.spec.ts @@ -1,19 +1,13 @@ import { nanoid } from 'nanoid'; -import { - afterEach, - beforeEach, - describe, - expect, - test as t, - type TestAPI, -} from 'vitest'; +import { beforeEach, describe, expect, test as t, type TestAPI } from 'vitest'; import { - createORMClientType, + createORMClient, type DBSchemaBuilder, type Entity, f, MemoryORMAdapter, + type ORMClient, } from '../'; const TEST_SCHEMA = { @@ -29,30 +23,23 @@ const TEST_SCHEMA = { }, } satisfies DBSchemaBuilder; -const Client = createORMClientType(TEST_SCHEMA); - -// define the hooks -Client.defineHook('tags', 'migrate field `color` to field `colors`', { - deserialize(data) { - if (!data.colors && data.color) { - data.colors = [data.color]; - } - - return data; - }, -}); - type Context = { - client: InstanceType; + client: ORMClient; }; beforeEach(async t => { - t.client = new Client(new MemoryORMAdapter()); - await t.client.connect(); -}); + t.client = createORMClient(TEST_SCHEMA, MemoryORMAdapter); -afterEach(async t => { - await t.client.disconnect(); + // define the hooks + t.client.defineHook('tags', 'migrate field `color` to field `colors`', { + deserialize(data) { + if (!data.colors && data.color) { + data.colors = [data.color]; + } + + return data; + }, + }); }); const test = t as TestAPI; diff --git a/packages/common/infra/src/orm/core/__tests__/schema.spec.ts b/packages/common/infra/src/orm/core/__tests__/schema.spec.ts index 78189c1cb1..6d4ebf5620 100644 --- a/packages/common/infra/src/orm/core/__tests__/schema.spec.ts +++ b/packages/common/infra/src/orm/core/__tests__/schema.spec.ts @@ -1,12 +1,21 @@ import { nanoid } from 'nanoid'; import { describe, expect, test } from 'vitest'; -import { createORMClientType, f, MemoryORMAdapter } from '../'; +import { + createORMClient, + type DBSchemaBuilder, + f, + MemoryORMAdapter, +} from '../'; + +function createClient(schema: Schema) { + return createORMClient(schema, MemoryORMAdapter); +} describe('Schema validations', () => { test('primary key must be set', () => { expect(() => - createORMClientType({ + createClient({ tags: { id: f.string(), name: f.string(), @@ -19,7 +28,7 @@ describe('Schema validations', () => { test('primary key must be unique', () => { expect(() => - createORMClientType({ + createClient({ tags: { id: f.string().primaryKey(), name: f.string().primaryKey(), @@ -32,7 +41,7 @@ describe('Schema validations', () => { test('primary key should not be optional without default value', () => { expect(() => - createORMClientType({ + createClient({ tags: { id: f.string().primaryKey().optional(), name: f.string(), @@ -45,7 +54,7 @@ describe('Schema validations', () => { test('primary key can be optional with default value', async () => { expect(() => - createORMClientType({ + createClient({ tags: { id: f.string().primaryKey().optional().default(nanoid), name: f.string(), @@ -56,20 +65,18 @@ describe('Schema validations', () => { }); describe('Entity validations', () => { - const Client = createORMClientType({ - tags: { - id: f.string().primaryKey().default(nanoid), - name: f.string(), - color: f.string(), - }, - }); - - function createClient() { - return new Client(new MemoryORMAdapter()); + function createTagsClient() { + return createClient({ + tags: { + id: f.string().primaryKey().default(nanoid), + name: f.string(), + color: f.string(), + }, + }); } test('should not update primary key', () => { - const client = createClient(); + const client = createTagsClient(); const tag = client.tags.create({ name: 'tag', @@ -83,7 +90,7 @@ describe('Entity validations', () => { }); test('should throw when trying to create entity with missing required field', () => { - const client = createClient(); + const client = createTagsClient(); // @ts-expect-error test expect(() => client.tags.create({ name: 'test' })).toThrow( @@ -92,7 +99,7 @@ describe('Entity validations', () => { }); test('should throw when trying to create entity with extra field', () => { - const client = createClient(); + const client = createTagsClient(); expect(() => // @ts-expect-error test @@ -101,34 +108,28 @@ describe('Entity validations', () => { }); test('should throw when trying to create entity with unexpected field type', () => { - const client = createClient(); + const client = createTagsClient(); - expect(() => - // @ts-expect-error test - client.tags.create({ name: 'test', color: 123 }) - ).toThrow( + // @ts-expect-error test + expect(() => client.tags.create({ name: 'test', color: 123 })).toThrow( "[Table(tags)]: Field 'color' type mismatch. Expected type 'string' but got 'number'." ); - expect(() => - // @ts-expect-error test - client.tags.create({ name: 'test', color: [123] }) - ).toThrow( + // @ts-expect-error test + expect(() => client.tags.create({ name: 'test', color: [123] })).toThrow( "[Table(tags)]: Field 'color' type mismatch. Expected type 'string' but got 'json'" ); }); test('should be able to assign `null` to json field', () => { expect(() => { - const Client = createORMClientType({ + const client = createClient({ tags: { id: f.string().primaryKey().default(nanoid), info: f.json(), }, }); - const client = new Client(new MemoryORMAdapter()); - const tag = client.tags.create({ info: null }); expect(tag.info).toBe(null); diff --git a/packages/common/infra/src/orm/core/__tests__/sync.spec.ts b/packages/common/infra/src/orm/core/__tests__/sync.spec.ts index 018ba0b898..c87b9d25c4 100644 --- a/packages/common/infra/src/orm/core/__tests__/sync.spec.ts +++ b/packages/common/infra/src/orm/core/__tests__/sync.spec.ts @@ -14,9 +14,10 @@ import { DocEngine } from '../../../sync'; import { MiniSyncServer } from '../../../sync/doc/__tests__/utils'; import { MemoryStorage } from '../../../sync/doc/storage'; import { - createORMClientType, + createORMClient, type DBSchemaBuilder, f, + type ORMClient, YjsDBAdapter, } from '../'; @@ -29,27 +30,14 @@ const TEST_SCHEMA = { }, } satisfies DBSchemaBuilder; -const Client = createORMClientType(TEST_SCHEMA); - -// define the hooks -Client.defineHook('tags', 'migrate field `color` to field `colors`', { - deserialize(data) { - if (!data.colors && data.color) { - data.colors = [data.color]; - } - - return data; - }, -}); - type Context = { server: MiniSyncServer; user1: { - client: InstanceType; + client: ORMClient; engine: DocEngine; }; user2: { - client: InstanceType; + client: ORMClient; engine: DocEngine; }; }; @@ -60,16 +48,25 @@ function createEngine(server: MiniSyncServer) { async function createClient(server: MiniSyncServer, clientId: number) { const engine = createEngine(server); - const client = new Client( - new YjsDBAdapter({ - getDoc(guid: string) { - const doc = new Doc({ guid }); - doc.clientID = clientId; - engine.addDoc(doc); - return doc; - }, - }) - ); + const client = createORMClient(TEST_SCHEMA, YjsDBAdapter, { + getDoc(guid: string) { + const doc = new Doc({ guid }); + doc.clientID = clientId; + engine.addDoc(doc); + return doc; + }, + }); + + // define the hooks + client.defineHook('tags', 'migrate field `color` to field `colors`', { + deserialize(data) { + if (!data.colors && data.color) { + data.colors = [data.color]; + } + + return data; + }, + }); return { engine, @@ -85,14 +82,10 @@ beforeEach(async t => { t.user2 = await createClient(t.server, 2); t.user1.engine.start(); - await t.user1.client.connect(); t.user2.engine.start(); - await t.user2.client.connect(); }); afterEach(async t => { - t.user1.client.disconnect(); - t.user2.client.disconnect(); t.user1.engine.stop(); t.user2.engine.stop(); }); diff --git a/packages/common/infra/src/orm/core/__tests__/yjs.spec.ts b/packages/common/infra/src/orm/core/__tests__/yjs.spec.ts index 3463261a17..387ace83ae 100644 --- a/packages/common/infra/src/orm/core/__tests__/yjs.spec.ts +++ b/packages/common/infra/src/orm/core/__tests__/yjs.spec.ts @@ -1,20 +1,14 @@ import { nanoid } from 'nanoid'; -import { - afterEach, - beforeEach, - describe, - expect, - test as t, - type TestAPI, -} from 'vitest'; +import { beforeEach, describe, expect, test as t, type TestAPI } from 'vitest'; import { Doc } from 'yjs'; import { - createORMClientType, + createORMClient, type DBSchemaBuilder, type DocProvider, type Entity, f, + type ORMClient, Table, YjsDBAdapter, } from '../'; @@ -33,18 +27,12 @@ const docProvider: DocProvider = { }, }; -const Client = createORMClientType(TEST_SCHEMA); type Context = { - client: InstanceType; + client: ORMClient; }; beforeEach(async t => { - t.client = new Client(new YjsDBAdapter(docProvider)); - await t.client.connect(); -}); - -afterEach(async t => { - await t.client.disconnect(); + t.client = createORMClient(TEST_SCHEMA, YjsDBAdapter, docProvider); }); const test = t as TestAPI; @@ -223,15 +211,13 @@ describe('ORM entity CRUD', () => { }); test('can not use reserved keyword as field name', () => { - const Client = createORMClientType({ + const schema = { tags: { $$KEY: f.string().primaryKey().default(nanoid), }, - }); + }; - expect(() => - new Client(new YjsDBAdapter(docProvider)).connect() - ).rejects.toThrow( + expect(() => createORMClient(schema, YjsDBAdapter, docProvider)).toThrow( "[Table(tags)]: Field '$$KEY' is reserved keyword and can't be used" ); }); diff --git a/packages/common/infra/src/orm/core/adapters/memory/db.ts b/packages/common/infra/src/orm/core/adapters/memory/db.ts index 3de9abeb31..99d40e956e 100644 --- a/packages/common/infra/src/orm/core/adapters/memory/db.ts +++ b/packages/common/infra/src/orm/core/adapters/memory/db.ts @@ -1,16 +1,7 @@ -import type { DBSchemaBuilder } from '../../schema'; import type { DBAdapter } from '../types'; import { MemoryTableAdapter } from './table'; export class MemoryORMAdapter implements DBAdapter { - connect(_db: DBSchemaBuilder): Promise { - return Promise.resolve(); - } - - disconnect(_db: DBSchemaBuilder): Promise { - return Promise.resolve(); - } - table(tableName: string) { return new MemoryTableAdapter(tableName); } diff --git a/packages/common/infra/src/orm/core/adapters/types.ts b/packages/common/infra/src/orm/core/adapters/types.ts index 6d274826e3..54832e39ab 100644 --- a/packages/common/infra/src/orm/core/adapters/types.ts +++ b/packages/common/infra/src/orm/core/adapters/types.ts @@ -1,4 +1,4 @@ -import type { DBSchemaBuilder, TableSchemaBuilder } from '../schema'; +import type { TableSchemaBuilder } from '../schema'; export interface Key { toString(): string; @@ -21,8 +21,5 @@ export interface TableAdapter { } export interface DBAdapter { - connect(db: DBSchemaBuilder): Promise; - disconnect(db: DBSchemaBuilder): Promise; - table(tableName: string): TableAdapter; } diff --git a/packages/common/infra/src/orm/core/adapters/yjs/db.ts b/packages/common/infra/src/orm/core/adapters/yjs/db.ts index aad83144f6..f7183e9007 100644 --- a/packages/common/infra/src/orm/core/adapters/yjs/db.ts +++ b/packages/common/infra/src/orm/core/adapters/yjs/db.ts @@ -11,25 +11,16 @@ export interface DocProvider { export class YjsDBAdapter implements DBAdapter { tables: Map = new Map(); - constructor(private readonly provider: DocProvider) {} - - connect(db: DBSchemaBuilder): Promise { + constructor( + db: DBSchemaBuilder, + private readonly provider: DocProvider + ) { for (const [tableName, table] of Object.entries(db)) { validators.validateYjsTableSchema(tableName, table); const doc = this.provider.getDoc(tableName); this.tables.set(tableName, new YjsTableAdapter(tableName, doc)); } - - return Promise.resolve(); - } - - disconnect(_db: DBSchemaBuilder): Promise { - this.tables.forEach(table => { - table.dispose(); - }); - this.tables.clear(); - return Promise.resolve(); } table(tableName: string) { diff --git a/packages/common/infra/src/orm/core/client.ts b/packages/common/infra/src/orm/core/client.ts index afbb2b67df..6b07a9ef12 100644 --- a/packages/common/infra/src/orm/core/client.ts +++ b/packages/common/infra/src/orm/core/client.ts @@ -1,10 +1,10 @@ import { type DBAdapter, type Hook } from './adapters'; import type { DBSchemaBuilder } from './schema'; -import { type CreateEntityInput, Table, type TableMap } from './table'; +import { Table, type TableMap } from './table'; import { validators } from './validators'; -export class ORMClient { - static hooksMap: Map[]> = new Map(); +class RawORMClient { + hooksMap: Map[]> = new Map(); private readonly tables = new Map>(); constructor( protected readonly db: DBSchemaBuilder, @@ -17,7 +17,7 @@ export class ORMClient { if (!table) { table = new Table(this.adapter, tableName, { schema: tableSchema, - hooks: ORMClient.hooksMap.get(tableName), + hooks: this.hooksMap.get(tableName), }); this.tables.set(tableName, table); } @@ -27,7 +27,7 @@ export class ORMClient { }); } - static defineHook(tableName: string, _desc: string, hook: Hook) { + defineHook(tableName: string, _desc: string, hook: Hook) { let hooks = this.hooksMap.get(tableName); if (!hooks) { hooks = []; @@ -36,48 +36,30 @@ export class ORMClient { hooks.push(hook); } - - async connect() { - await this.adapter.connect(this.db); - } - - async disconnect() { - await this.adapter.disconnect(this.db); - } } -export function createORMClientType( - db: Schema -): ORMClientWithTablesClass { +export function createORMClient< + const Schema extends DBSchemaBuilder, + AdapterConstructor extends new (...args: any[]) => DBAdapter, + AdapterConstructorParams extends + any[] = ConstructorParameters extends [ + DBSchemaBuilder, + ...infer Args, + ] + ? Args + : never, +>( + db: Schema, + adapter: AdapterConstructor, + ...args: AdapterConstructorParams +): ORMClient { Object.entries(db).forEach(([tableName, schema]) => { validators.validateTableSchema(tableName, schema); }); - class ORMClientWithTables extends ORMClient { - constructor(adapter: DBAdapter) { - super(db, adapter); - } - } - - return ORMClientWithTables as { - new ( - ...args: ConstructorParameters - ): ORMClient & TableMap; - - defineHook( - tableName: TableName, - desc: string, - hook: Hook> - ): void; - }; + return new RawORMClient(db, new adapter(db, ...args)) as TableMap & + RawORMClient; } -export type ORMClientWithTablesClass = { - new (adapter: DBAdapter): TableMap & ORMClient; - - defineHook( - tableName: TableName, - desc: string, - hook: Hook> - ): void; -}; +export type ORMClient = RawORMClient & + TableMap; diff --git a/packages/common/infra/src/orm/index.ts b/packages/common/infra/src/orm/index.ts deleted file mode 100644 index 8f9dc079c0..0000000000 --- a/packages/common/infra/src/orm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './affine';