Skip to content

Commit

Permalink
feat(infra): improve orm
Browse files Browse the repository at this point in the history
  • Loading branch information
forehalo committed Jul 29, 2024
1 parent 97a0271 commit 4717805
Show file tree
Hide file tree
Showing 23 changed files with 436 additions and 388 deletions.
20 changes: 20 additions & 0 deletions packages/common/infra/src/modules/db/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,23 @@ export const AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA = {
} as const satisfies DBSchemaBuilder;
export type AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA =
typeof AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA;

export const AFFiNE_WORKSPACE_DOC_DB_SCHEMA = {
snapshots: {
docId: f.string().primaryKey(),
blob: f.binary(),
state: f.binary(),
createdAt: f.timestamp(),
updatedAt: f.timestamp(),
lastSeenAt: f.timestamp(),
},
updates: {
id: f.string().primaryKey().autoincremental(),
docId: f.string().index(),
blob: f.binary(),
createdAt: f.timestamp(),
},
};

export type AFFiNE_WORKSPACE_DOC_DB_SCHEMA =
typeof AFFiNE_WORKSPACE_DOC_DB_SCHEMA;
4 changes: 2 additions & 2 deletions packages/common/infra/src/modules/db/services/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class WorkspaceDBService extends Service {
constructor(private readonly workspaceService: WorkspaceService) {
super();
this.db = new WorkspaceDBClient(
new YjsDBAdapter(AFFiNE_WORKSPACE_DB_SCHEMA, {
new YjsDBAdapter({
getDoc: guid => {
const ydoc = new YDoc({
// guid format: db${workspaceId}${guid}
Expand All @@ -47,7 +47,7 @@ export class WorkspaceDBService extends Service {
}

const newDB = new WorkspaceUserdataDBClient(
new YjsDBAdapter(AFFiNE_WORKSPACE_USERDATA_DB_SCHEMA, {
new YjsDBAdapter({
getDoc: guid => {
const ydoc = new YDoc({
// guid format: userdata${userId}${workspaceId}${guid}
Expand Down
130 changes: 0 additions & 130 deletions packages/common/infra/src/orm/core/__tests__/hook.spec.ts

This file was deleted.

14 changes: 1 addition & 13 deletions packages/common/infra/src/orm/core/__tests__/sync.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const TEST_SCHEMA = {
id: f.string().primaryKey().default(nanoid),
name: f.string(),
color: f.string().optional(),
colors: f.json<string[]>().optional(),
},
} satisfies DBSchemaBuilder;

Expand All @@ -46,19 +45,8 @@ async function createClient(server: MiniSyncServer, clientId: number) {
const engine = createEngine(server);
const Client = createORMClient(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;
},
});

const client = new Client(
new YjsDBAdapter(TEST_SCHEMA, {
new YjsDBAdapter({
getDoc(guid: string) {
const doc = new Doc({ guid });
doc.clientID = clientId;
Expand Down
21 changes: 9 additions & 12 deletions packages/common/infra/src/orm/core/__tests__/yjs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type Context = {
};

beforeEach<Context>(async t => {
t.client = new Client(new YjsDBAdapter(TEST_SCHEMA, docProvider));
t.client = new Client(new YjsDBAdapter(docProvider));
});

const test = t as TestAPI<Context>;
Expand Down Expand Up @@ -312,18 +312,15 @@ describe('ORM entity CRUD', () => {
});

test('can not use reserved keyword as field name', () => {
expect(
() =>
new YjsDBAdapter(
{
tags: {
$$DELETED: f.string().primaryKey().default(nanoid),
},
},
docProvider
)
expect(() =>
new YjsDBAdapter(docProvider).setup({
tags: {
// @ts-expect-error
id: f.number().autoincremental(),
},
})
).toThrow(
"[Table(tags)]: Field '$$DELETED' is reserved keyword and can't be used"
"[Table(tags)]: Primary key can't be autoincremental in Yjs table adapter."
);
});

Expand Down
107 changes: 107 additions & 0 deletions packages/common/infra/src/orm/core/adapters/indexeddb/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { IDBPDatabase } from 'idb';
import { openDB } from 'idb';

import type { DBSchema } from '../../schema';
import type {
AsyncDBAdapter,
AsyncTableAdapter,
TableAdapterOptions,
} from '../types';
import { IndexedDBTableAdapter } from './table';

interface IndexedDBORMAdapterOptions {
db: string;
version: number;
}

export class IndexedDBORMAdapter implements AsyncDBAdapter {
private db: IDBPDatabase | null = null;

private schema!: DBSchema;

private readonly tables = new Map<string, AsyncTableAdapter>();

constructor(private readonly opts: IndexedDBORMAdapterOptions) {}

async setup(schema: DBSchema) {
this.schema = schema;
}

async connect() {
this.db = await openDB(this.opts.db, 1, {
upgrade: db => {
this.upgradeDB(db);
},
});
}

disconnect() {
this.db = null;
return Promise.resolve();
}

table(opts: TableAdapterOptions) {
let table = this.tables.get(opts.schema.name);

if (!table) {
table = new IndexedDBTableAdapter(opts);
this.tables.set(opts.schema.name, table);
}

return table;
}

private upgradeDB(db: IDBPDatabase) {
const createdStores = new Set(db.objectStoreNames);
const storesInNewVersion = new Set(Object.keys(this.schema));

// create
for (const tableName of storesInNewVersion) {
const tableSchema = this.schema[tableName];

if (!createdStores.has(tableName)) {
const store = db.createObjectStore(tableName, {
keyPath: tableSchema.primaryKey,
autoIncrement: tableSchema.autoincremental,
});

const createdIndexes = Array.from(store.indexNames);
const idxName = (prefix: string, keys: string[]) =>
`${prefix}_${keys.join('_')}`;
const indexesInNewVersion = tableSchema.indexes.map(
({ keys, unique }) => idxName(unique ? 'unique' : 'index', keys)
);

// create index
for (const { keys, unique } of tableSchema.indexes) {
const name = idxName(unique ? 'unique' : 'index', keys);
const existsName = createdIndexes.find(index =>
index.endsWith(idxName('', keys))
);

// index type has changed
// TODO(@forehalo): updating from index to unique is not tested
if (existsName && existsName !== name) {
store.deleteIndex(existsName);
}

store.createIndex(name, keys, { unique });
}

//delete index
for (const index of store.indexNames) {
if (!indexesInNewVersion.includes(index)) {
store.deleteIndex(index);
}
}
}
}

// delete
for (const tableName of createdStores) {
if (!storesInNewVersion.has(tableName)) {
db.deleteObjectStore(tableName);
}
}
}
}
34 changes: 34 additions & 0 deletions packages/common/infra/src/orm/core/adapters/indexeddb/table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type {
DeleteQuery,
FindQuery,
InsertQuery,
ObserveQuery,
UpdateQuery,
} from '../types';
import { AsyncTableAdapter } from '../types';

export class IndexedDBTableAdapter extends AsyncTableAdapter {
toObject(record: any): Record<string, any> {
throw new Error('Method not implemented.');
}

async insert(query: InsertQuery) {
throw new Error('Method not implemented.');
}

async update(query: UpdateQuery): Promise<any[]> {
throw new Error('Method not implemented.');
}

async delete(query: DeleteQuery): Promise<void> {
throw new Error('Method not implemented.');
}

async find(query: FindQuery): Promise<any[]> {
throw new Error('Method not implemented.');
}

observe(query: ObserveQuery): () => void {
throw new Error('Method not implemented.');
}
}
7 changes: 4 additions & 3 deletions packages/common/infra/src/orm/core/adapters/memory/db.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { DBAdapter } from '../types';
import type { DBAdapter, TableAdapterOptions } from '../types';
import { MemoryTableAdapter } from './table';

export class MemoryORMAdapter implements DBAdapter {
table(tableName: string) {
return new MemoryTableAdapter(tableName);
setup(): void {}
table(opts: TableAdapterOptions) {
return new MemoryTableAdapter(opts);
}
}
Loading

0 comments on commit 4717805

Please sign in to comment.