mirror of
https://github.com/toeverything/AFFiNE.git
synced 2025-01-02 16:57:07 +03:00
feat(core): adjust orm api (#7392)
Removed the `connect` and `disconnect` functions on the orm `DBAdapter`, making the ORM completely non-asynchronous.
This commit is contained in:
parent
d72dbe682c
commit
39acb51d87
@ -1,4 +0,0 @@
|
||||
import { createORMClientType } from '../core';
|
||||
import { AFFiNE_DB_SCHEMA } from './schema';
|
||||
|
||||
export const ORMClient = createORMClientType(AFFiNE_DB_SCHEMA);
|
@ -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 };
|
@ -1,3 +0,0 @@
|
||||
import './hooks';
|
||||
|
||||
export { ORMClient } from './client';
|
@ -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<string[]>().optional(), // <= mark as optional since old records might only have [color] field
|
||||
// },
|
||||
} as const satisfies DBSchemaBuilder;
|
@ -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<typeof Client>;
|
||||
client: ORMClient<typeof TEST_SCHEMA>;
|
||||
};
|
||||
|
||||
beforeEach<Context>(async t => {
|
||||
t.client = new Client(new MemoryORMAdapter());
|
||||
await t.client.connect();
|
||||
});
|
||||
|
||||
afterEach<Context>(async t => {
|
||||
await t.client.disconnect();
|
||||
t.client = createORMClient(TEST_SCHEMA, MemoryORMAdapter);
|
||||
});
|
||||
|
||||
const test = t as TestAPI<Context>;
|
||||
|
@ -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<typeof Client>;
|
||||
client: ORMClient<typeof TEST_SCHEMA>;
|
||||
};
|
||||
|
||||
beforeEach<Context>(async t => {
|
||||
t.client = new Client(new MemoryORMAdapter());
|
||||
await t.client.connect();
|
||||
});
|
||||
t.client = createORMClient(TEST_SCHEMA, MemoryORMAdapter);
|
||||
|
||||
afterEach<Context>(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<Context>;
|
||||
|
@ -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 extends DBSchemaBuilder>(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);
|
||||
|
@ -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<typeof Client>;
|
||||
client: ORMClient<typeof TEST_SCHEMA>;
|
||||
engine: DocEngine;
|
||||
};
|
||||
user2: {
|
||||
client: InstanceType<typeof Client>;
|
||||
client: ORMClient<typeof TEST_SCHEMA>;
|
||||
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<Context>(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<Context>(async t => {
|
||||
t.user1.client.disconnect();
|
||||
t.user2.client.disconnect();
|
||||
t.user1.engine.stop();
|
||||
t.user2.engine.stop();
|
||||
});
|
||||
|
@ -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<typeof Client>;
|
||||
client: ORMClient<typeof TEST_SCHEMA>;
|
||||
};
|
||||
|
||||
beforeEach<Context>(async t => {
|
||||
t.client = new Client(new YjsDBAdapter(docProvider));
|
||||
await t.client.connect();
|
||||
});
|
||||
|
||||
afterEach<Context>(async t => {
|
||||
await t.client.disconnect();
|
||||
t.client = createORMClient(TEST_SCHEMA, YjsDBAdapter, docProvider);
|
||||
});
|
||||
|
||||
const test = t as TestAPI<Context>;
|
||||
@ -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"
|
||||
);
|
||||
});
|
||||
|
@ -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<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
disconnect(_db: DBSchemaBuilder): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
table(tableName: string) {
|
||||
return new MemoryTableAdapter(tableName);
|
||||
}
|
||||
|
@ -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<K extends Key = any, T = unknown> {
|
||||
}
|
||||
|
||||
export interface DBAdapter {
|
||||
connect(db: DBSchemaBuilder): Promise<void>;
|
||||
disconnect(db: DBSchemaBuilder): Promise<void>;
|
||||
|
||||
table(tableName: string): TableAdapter;
|
||||
}
|
||||
|
@ -11,25 +11,16 @@ export interface DocProvider {
|
||||
|
||||
export class YjsDBAdapter implements DBAdapter {
|
||||
tables: Map<string, TableAdapter> = new Map();
|
||||
constructor(private readonly provider: DocProvider) {}
|
||||
|
||||
connect(db: DBSchemaBuilder): Promise<void> {
|
||||
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<void> {
|
||||
this.tables.forEach(table => {
|
||||
table.dispose();
|
||||
});
|
||||
this.tables.clear();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
table(tableName: string) {
|
||||
|
@ -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<string, Hook<any>[]> = new Map();
|
||||
class RawORMClient {
|
||||
hooksMap: Map<string, Hook<any>[]> = new Map();
|
||||
private readonly tables = new Map<string, Table<any>>();
|
||||
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<any>) {
|
||||
defineHook(tableName: string, _desc: string, hook: Hook<any>) {
|
||||
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<Schema extends DBSchemaBuilder>(
|
||||
db: Schema
|
||||
): ORMClientWithTablesClass<Schema> {
|
||||
export function createORMClient<
|
||||
const Schema extends DBSchemaBuilder,
|
||||
AdapterConstructor extends new (...args: any[]) => DBAdapter,
|
||||
AdapterConstructorParams extends
|
||||
any[] = ConstructorParameters<AdapterConstructor> extends [
|
||||
DBSchemaBuilder,
|
||||
...infer Args,
|
||||
]
|
||||
? Args
|
||||
: never,
|
||||
>(
|
||||
db: Schema,
|
||||
adapter: AdapterConstructor,
|
||||
...args: AdapterConstructorParams
|
||||
): ORMClient<Schema> {
|
||||
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<typeof ORMClientWithTables>
|
||||
): ORMClient & TableMap<Schema>;
|
||||
|
||||
defineHook<TableName extends keyof Schema>(
|
||||
tableName: TableName,
|
||||
desc: string,
|
||||
hook: Hook<CreateEntityInput<Schema[TableName]>>
|
||||
): void;
|
||||
};
|
||||
return new RawORMClient(db, new adapter(db, ...args)) as TableMap<Schema> &
|
||||
RawORMClient;
|
||||
}
|
||||
|
||||
export type ORMClientWithTablesClass<Schema extends DBSchemaBuilder> = {
|
||||
new (adapter: DBAdapter): TableMap<Schema> & ORMClient;
|
||||
|
||||
defineHook<TableName extends keyof Schema>(
|
||||
tableName: TableName,
|
||||
desc: string,
|
||||
hook: Hook<CreateEntityInput<Schema[TableName]>>
|
||||
): void;
|
||||
};
|
||||
export type ORMClient<Schema extends DBSchemaBuilder> = RawORMClient &
|
||||
TableMap<Schema>;
|
||||
|
@ -1 +0,0 @@
|
||||
export * from './affine';
|
Loading…
Reference in New Issue
Block a user