mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-03 06:03:21 +03:00
feat(infra): improve orm (#7475)
This commit is contained in:
parent
5dd7382693
commit
024e5500f6
@ -6,7 +6,6 @@ import {
|
||||
type DBSchemaBuilder,
|
||||
f,
|
||||
MemoryORMAdapter,
|
||||
type ORMClient,
|
||||
Table,
|
||||
} from '../';
|
||||
|
||||
@ -18,12 +17,14 @@ const TEST_SCHEMA = {
|
||||
},
|
||||
} satisfies DBSchemaBuilder;
|
||||
|
||||
const ORMClient = createORMClient(TEST_SCHEMA);
|
||||
|
||||
type Context = {
|
||||
client: ORMClient<typeof TEST_SCHEMA>;
|
||||
client: InstanceType<typeof ORMClient>;
|
||||
};
|
||||
|
||||
beforeEach<Context>(async t => {
|
||||
t.client = createORMClient(TEST_SCHEMA, MemoryORMAdapter);
|
||||
t.client = new ORMClient(new MemoryORMAdapter());
|
||||
});
|
||||
|
||||
const test = t as TestAPI<Context>;
|
||||
@ -94,7 +95,7 @@ describe('ORM entity CRUD', () => {
|
||||
});
|
||||
|
||||
// old tag should not be updated
|
||||
expect(tag.name).not.toBe(tag2.name);
|
||||
expect(tag.name).not.toBe(tag2!.name);
|
||||
});
|
||||
|
||||
test('should be able to delete entity', async t => {
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
type Entity,
|
||||
f,
|
||||
MemoryORMAdapter,
|
||||
type ORMClient,
|
||||
} from '../';
|
||||
|
||||
const TEST_SCHEMA = {
|
||||
@ -23,23 +22,25 @@ const TEST_SCHEMA = {
|
||||
},
|
||||
} satisfies DBSchemaBuilder;
|
||||
|
||||
const ORMClient = createORMClient(TEST_SCHEMA);
|
||||
|
||||
// define the hooks
|
||||
ORMClient.defineHook('tags', 'migrate field `color` to field `colors`', {
|
||||
deserialize(data) {
|
||||
if (!data.colors && data.color) {
|
||||
data.colors = [data.color];
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
type Context = {
|
||||
client: ORMClient<typeof TEST_SCHEMA>;
|
||||
client: InstanceType<typeof ORMClient>;
|
||||
};
|
||||
|
||||
beforeEach<Context>(async t => {
|
||||
t.client = createORMClient(TEST_SCHEMA, MemoryORMAdapter);
|
||||
|
||||
// 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;
|
||||
},
|
||||
});
|
||||
t.client = new ORMClient(new MemoryORMAdapter());
|
||||
});
|
||||
|
||||
const test = t as TestAPI<Context>;
|
||||
@ -65,7 +66,7 @@ describe('ORM hook mixin', () => {
|
||||
});
|
||||
|
||||
const tag2 = client.tags.get(tag.id);
|
||||
expect(tag2.colors).toStrictEqual(['red']);
|
||||
expect(tag2!.colors).toStrictEqual(['red']);
|
||||
});
|
||||
|
||||
test('update entity', t => {
|
||||
@ -77,7 +78,7 @@ describe('ORM hook mixin', () => {
|
||||
});
|
||||
|
||||
const tag2 = client.tags.update(tag.id, { color: 'blue' });
|
||||
expect(tag2.colors).toStrictEqual(['blue']);
|
||||
expect(tag2!.colors).toStrictEqual(['blue']);
|
||||
});
|
||||
|
||||
test('subscribe entity', t => {
|
||||
|
@ -1,21 +1,12 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
createORMClient,
|
||||
type DBSchemaBuilder,
|
||||
f,
|
||||
MemoryORMAdapter,
|
||||
} from '../';
|
||||
|
||||
function createClient<Schema extends DBSchemaBuilder>(schema: Schema) {
|
||||
return createORMClient(schema, MemoryORMAdapter);
|
||||
}
|
||||
import { createORMClient, f, MemoryORMAdapter } from '../';
|
||||
|
||||
describe('Schema validations', () => {
|
||||
test('primary key must be set', () => {
|
||||
expect(() =>
|
||||
createClient({
|
||||
createORMClient({
|
||||
tags: {
|
||||
id: f.string(),
|
||||
name: f.string(),
|
||||
@ -28,7 +19,7 @@ describe('Schema validations', () => {
|
||||
|
||||
test('primary key must be unique', () => {
|
||||
expect(() =>
|
||||
createClient({
|
||||
createORMClient({
|
||||
tags: {
|
||||
id: f.string().primaryKey(),
|
||||
name: f.string().primaryKey(),
|
||||
@ -41,7 +32,7 @@ describe('Schema validations', () => {
|
||||
|
||||
test('primary key should not be optional without default value', () => {
|
||||
expect(() =>
|
||||
createClient({
|
||||
createORMClient({
|
||||
tags: {
|
||||
id: f.string().primaryKey().optional(),
|
||||
name: f.string(),
|
||||
@ -54,7 +45,7 @@ describe('Schema validations', () => {
|
||||
|
||||
test('primary key can be optional with default value', async () => {
|
||||
expect(() =>
|
||||
createClient({
|
||||
createORMClient({
|
||||
tags: {
|
||||
id: f.string().primaryKey().optional().default(nanoid),
|
||||
name: f.string(),
|
||||
@ -65,14 +56,16 @@ describe('Schema validations', () => {
|
||||
});
|
||||
|
||||
describe('Entity validations', () => {
|
||||
const Client = createORMClient({
|
||||
tags: {
|
||||
id: f.string().primaryKey().default(nanoid),
|
||||
name: f.string(),
|
||||
color: f.string(),
|
||||
},
|
||||
});
|
||||
|
||||
function createTagsClient() {
|
||||
return createClient({
|
||||
tags: {
|
||||
id: f.string().primaryKey().default(nanoid),
|
||||
name: f.string(),
|
||||
color: f.string(),
|
||||
},
|
||||
});
|
||||
return new Client(new MemoryORMAdapter());
|
||||
}
|
||||
|
||||
test('should not update primary key', () => {
|
||||
@ -123,13 +116,15 @@ describe('Entity validations', () => {
|
||||
|
||||
test('should be able to assign `null` to json field', () => {
|
||||
expect(() => {
|
||||
const client = createClient({
|
||||
const Client = createORMClient({
|
||||
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);
|
||||
|
@ -13,13 +13,7 @@ import { Doc } from 'yjs';
|
||||
import { DocEngine } from '../../../sync';
|
||||
import { MiniSyncServer } from '../../../sync/doc/__tests__/utils';
|
||||
import { MemoryStorage } from '../../../sync/doc/storage';
|
||||
import {
|
||||
createORMClient,
|
||||
type DBSchemaBuilder,
|
||||
f,
|
||||
type ORMClient,
|
||||
YjsDBAdapter,
|
||||
} from '../';
|
||||
import { createORMClient, type DBSchemaBuilder, f, YjsDBAdapter } from '../';
|
||||
|
||||
const TEST_SCHEMA = {
|
||||
tags: {
|
||||
@ -30,14 +24,16 @@ const TEST_SCHEMA = {
|
||||
},
|
||||
} satisfies DBSchemaBuilder;
|
||||
|
||||
const ORMClient = createORMClient(TEST_SCHEMA);
|
||||
|
||||
type Context = {
|
||||
server: MiniSyncServer;
|
||||
user1: {
|
||||
client: ORMClient<typeof TEST_SCHEMA>;
|
||||
client: InstanceType<typeof ORMClient>;
|
||||
engine: DocEngine;
|
||||
};
|
||||
user2: {
|
||||
client: ORMClient<typeof TEST_SCHEMA>;
|
||||
client: InstanceType<typeof ORMClient>;
|
||||
engine: DocEngine;
|
||||
};
|
||||
};
|
||||
@ -48,17 +44,10 @@ function createEngine(server: MiniSyncServer) {
|
||||
|
||||
async function createClient(server: MiniSyncServer, clientId: number) {
|
||||
const engine = createEngine(server);
|
||||
const client = createORMClient(TEST_SCHEMA, YjsDBAdapter, {
|
||||
getDoc(guid: string) {
|
||||
const doc = new Doc({ guid });
|
||||
doc.clientID = clientId;
|
||||
engine.addDoc(doc);
|
||||
return doc;
|
||||
},
|
||||
});
|
||||
const Client = createORMClient(TEST_SCHEMA);
|
||||
|
||||
// define the hooks
|
||||
client.defineHook('tags', 'migrate field `color` to field `colors`', {
|
||||
Client.defineHook('tags', 'migrate field `color` to field `colors`', {
|
||||
deserialize(data) {
|
||||
if (!data.colors && data.color) {
|
||||
data.colors = [data.color];
|
||||
@ -68,6 +57,17 @@ async function createClient(server: MiniSyncServer, clientId: number) {
|
||||
},
|
||||
});
|
||||
|
||||
const client = new Client(
|
||||
new YjsDBAdapter(TEST_SCHEMA, {
|
||||
getDoc(guid: string) {
|
||||
const doc = new Doc({ guid });
|
||||
doc.clientID = clientId;
|
||||
engine.addDoc(doc);
|
||||
return doc;
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
engine,
|
||||
client,
|
||||
|
@ -8,17 +8,25 @@ import {
|
||||
type DocProvider,
|
||||
type Entity,
|
||||
f,
|
||||
type ORMClient,
|
||||
Table,
|
||||
YjsDBAdapter,
|
||||
} from '../';
|
||||
|
||||
function incremental() {
|
||||
let i = 0;
|
||||
return () => i++;
|
||||
}
|
||||
|
||||
const TEST_SCHEMA = {
|
||||
tags: {
|
||||
id: f.string().primaryKey().default(nanoid),
|
||||
name: f.string(),
|
||||
color: f.string(),
|
||||
},
|
||||
users: {
|
||||
id: f.number().primaryKey().default(incremental()),
|
||||
name: f.string(),
|
||||
},
|
||||
} satisfies DBSchemaBuilder;
|
||||
|
||||
const docProvider: DocProvider = {
|
||||
@ -27,12 +35,13 @@ const docProvider: DocProvider = {
|
||||
},
|
||||
};
|
||||
|
||||
const Client = createORMClient(TEST_SCHEMA);
|
||||
type Context = {
|
||||
client: ORMClient<typeof TEST_SCHEMA>;
|
||||
client: InstanceType<typeof Client>;
|
||||
};
|
||||
|
||||
beforeEach<Context>(async t => {
|
||||
t.client = createORMClient(TEST_SCHEMA, YjsDBAdapter, docProvider);
|
||||
t.client = new Client(new YjsDBAdapter(TEST_SCHEMA, docProvider));
|
||||
});
|
||||
|
||||
const test = t as TestAPI<Context>;
|
||||
@ -55,6 +64,13 @@ describe('ORM entity CRUD', () => {
|
||||
expect(tag.id).toBeDefined();
|
||||
expect(tag.name).toBe('test');
|
||||
expect(tag.color).toBe('red');
|
||||
|
||||
const user = client.users.create({
|
||||
name: 'user1',
|
||||
});
|
||||
|
||||
expect(typeof user.id).toBe('number');
|
||||
expect(user.name).toBe('user1');
|
||||
});
|
||||
|
||||
test('should be able to read entity', t => {
|
||||
@ -67,6 +83,12 @@ describe('ORM entity CRUD', () => {
|
||||
|
||||
const tag2 = client.tags.get(tag.id);
|
||||
expect(tag2).toEqual(tag);
|
||||
|
||||
const user = client.users.create({
|
||||
name: 'user1',
|
||||
});
|
||||
const user2 = client.users.get(user.id);
|
||||
expect(user2).toEqual(user);
|
||||
});
|
||||
|
||||
test('should be able to update entity', t => {
|
||||
@ -89,7 +111,7 @@ describe('ORM entity CRUD', () => {
|
||||
});
|
||||
|
||||
// old tag should not be updated
|
||||
expect(tag.name).not.toBe(tag2.name);
|
||||
expect(tag.name).not.toBe(tag2!.name);
|
||||
});
|
||||
|
||||
test('should be able to delete entity', t => {
|
||||
@ -149,6 +171,7 @@ describe('ORM entity CRUD', () => {
|
||||
const { client } = t;
|
||||
|
||||
let tag: Entity<(typeof TEST_SCHEMA)['tags']> | null = null;
|
||||
|
||||
const subscription1 = client.tags.get$('test').subscribe(data => {
|
||||
tag = data;
|
||||
});
|
||||
@ -210,15 +233,73 @@ describe('ORM entity CRUD', () => {
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
test('can not use reserved keyword as field name', () => {
|
||||
const schema = {
|
||||
tags: {
|
||||
$$KEY: f.string().primaryKey().default(nanoid),
|
||||
},
|
||||
};
|
||||
test('should be able to subscribe to filtered entity changes', t => {
|
||||
const { client } = t;
|
||||
|
||||
expect(() => createORMClient(schema, YjsDBAdapter, docProvider)).toThrow(
|
||||
"[Table(tags)]: Field '$$KEY' is reserved keyword and can't be used"
|
||||
let entities: any[] = [];
|
||||
const subscription = client.tags.find$({ name: 'test' }).subscribe(data => {
|
||||
entities = data;
|
||||
});
|
||||
|
||||
const tag1 = client.tags.create({
|
||||
id: '1',
|
||||
name: 'test',
|
||||
color: 'red',
|
||||
});
|
||||
|
||||
expect(entities).toStrictEqual([tag1]);
|
||||
|
||||
const tag2 = client.tags.create({
|
||||
id: '2',
|
||||
name: 'test',
|
||||
color: 'blue',
|
||||
});
|
||||
|
||||
expect(entities).toStrictEqual([tag1, tag2]);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
test('should be able to subscription to any entity changes', t => {
|
||||
const { client } = t;
|
||||
|
||||
let entities: any[] = [];
|
||||
const subscription = client.tags.find$({}).subscribe(data => {
|
||||
entities = data;
|
||||
});
|
||||
|
||||
const tag1 = client.tags.create({
|
||||
id: '1',
|
||||
name: 'tag1',
|
||||
color: 'red',
|
||||
});
|
||||
|
||||
expect(entities).toStrictEqual([tag1]);
|
||||
|
||||
const tag2 = client.tags.create({
|
||||
id: '2',
|
||||
name: 'tag2',
|
||||
color: 'blue',
|
||||
});
|
||||
|
||||
expect(entities).toStrictEqual([tag1, tag2]);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
test('can not use reserved keyword as field name', () => {
|
||||
expect(
|
||||
() =>
|
||||
new YjsDBAdapter(
|
||||
{
|
||||
tags: {
|
||||
$$DELETED: f.string().primaryKey().default(nanoid),
|
||||
},
|
||||
},
|
||||
docProvider
|
||||
)
|
||||
).toThrow(
|
||||
"[Table(tags)]: Field '$$DELETED' is reserved keyword and can't be used"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1,19 +1,36 @@
|
||||
import { merge } from 'lodash-es';
|
||||
import { merge, pick } from 'lodash-es';
|
||||
|
||||
import { HookAdapter } from '../mixins';
|
||||
import type { Key, TableAdapter, TableOptions } from '../types';
|
||||
import type {
|
||||
DeleteQuery,
|
||||
FindQuery,
|
||||
InsertQuery,
|
||||
ObserveQuery,
|
||||
Select,
|
||||
TableAdapter,
|
||||
TableAdapterOptions,
|
||||
UpdateQuery,
|
||||
WhereCondition,
|
||||
} from '../types';
|
||||
|
||||
@HookAdapter()
|
||||
export class MemoryTableAdapter implements TableAdapter {
|
||||
data = new Map<Key, any>();
|
||||
subscriptions = new Map<Key, Array<(data: any) => void>>();
|
||||
private readonly data = new Map<string, any>();
|
||||
private keyField = 'key';
|
||||
private readonly subscriptions = new Set<(key: string, data: any) => void>();
|
||||
|
||||
constructor(private readonly tableName: string) {}
|
||||
|
||||
setup(_opts: TableOptions) {}
|
||||
setup(opts: TableAdapterOptions) {
|
||||
this.keyField = opts.keyField;
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
|
||||
create(key: Key, data: any) {
|
||||
insert(query: InsertQuery) {
|
||||
const { data, select } = query;
|
||||
const key = String(data[this.keyField]);
|
||||
|
||||
if (this.data.has(key)) {
|
||||
throw new Error(
|
||||
`Record with key ${key} already exists in table ${this.tableName}`
|
||||
@ -22,79 +39,125 @@ export class MemoryTableAdapter implements TableAdapter {
|
||||
|
||||
this.data.set(key, data);
|
||||
this.dispatch(key, data);
|
||||
this.dispatch('$$KEYS', this.keys());
|
||||
return data;
|
||||
return this.value(data, select);
|
||||
}
|
||||
|
||||
get(key: Key) {
|
||||
return this.data.get(key) || null;
|
||||
}
|
||||
find(query: FindQuery) {
|
||||
const { where, select } = query;
|
||||
const result = [];
|
||||
|
||||
subscribe(key: Key, callback: (data: any) => void): () => void {
|
||||
const sKey = key.toString();
|
||||
let subs = this.subscriptions.get(sKey.toString());
|
||||
|
||||
if (!subs) {
|
||||
subs = [];
|
||||
this.subscriptions.set(sKey, subs);
|
||||
for (const record of this.iterate(where)) {
|
||||
result.push(this.value(record, select));
|
||||
}
|
||||
|
||||
subs.push(callback);
|
||||
callback(this.data.get(key) || null);
|
||||
return result;
|
||||
}
|
||||
|
||||
observe(query: ObserveQuery): () => void {
|
||||
const { where, select, callback } = query;
|
||||
|
||||
let listeningOnAll = false;
|
||||
const obKeys = new Set<string>();
|
||||
const results = [];
|
||||
|
||||
if (!where) {
|
||||
listeningOnAll = true;
|
||||
} else if ('byKey' in where) {
|
||||
obKeys.add(where.byKey.toString());
|
||||
}
|
||||
|
||||
for (const record of this.iterate(where)) {
|
||||
const key = String(record[this.keyField]);
|
||||
if (!listeningOnAll) {
|
||||
obKeys.add(key);
|
||||
}
|
||||
results.push(this.value(record, select));
|
||||
}
|
||||
|
||||
callback(results);
|
||||
|
||||
const ob = (key: string, data: any) => {
|
||||
if (
|
||||
listeningOnAll ||
|
||||
obKeys.has(key) ||
|
||||
(where && this.match(data, where))
|
||||
) {
|
||||
callback(this.find({ where, select }));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
this.subscriptions.add(ob);
|
||||
|
||||
return () => {
|
||||
this.subscriptions.set(
|
||||
sKey,
|
||||
subs.filter(s => s !== callback)
|
||||
);
|
||||
this.subscriptions.delete(ob);
|
||||
};
|
||||
}
|
||||
|
||||
keys(): Key[] {
|
||||
return Array.from(this.data.keys());
|
||||
}
|
||||
update(query: UpdateQuery) {
|
||||
const { where, data, select } = query;
|
||||
const result = [];
|
||||
|
||||
subscribeKeys(callback: (keys: Key[]) => void): () => void {
|
||||
const sKey = `$$KEYS`;
|
||||
let subs = this.subscriptions.get(sKey);
|
||||
|
||||
if (!subs) {
|
||||
subs = [];
|
||||
this.subscriptions.set(sKey, subs);
|
||||
}
|
||||
subs.push(callback);
|
||||
callback(this.keys());
|
||||
|
||||
return () => {
|
||||
this.subscriptions.set(
|
||||
sKey,
|
||||
subs.filter(s => s !== callback)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
update(key: Key, data: any) {
|
||||
let record = this.data.get(key);
|
||||
|
||||
if (!record) {
|
||||
throw new Error(
|
||||
`Record with key ${key} does not exist in table ${this.tableName}`
|
||||
);
|
||||
for (let record of this.iterate(where)) {
|
||||
record = merge({}, record, data);
|
||||
const key = String(record[this.keyField]);
|
||||
this.data.set(key, record);
|
||||
this.dispatch(key, record);
|
||||
result.push(this.value(this.value(record, select)));
|
||||
}
|
||||
|
||||
record = merge({}, record, data);
|
||||
this.data.set(key, record);
|
||||
this.dispatch(key, record);
|
||||
return result;
|
||||
}
|
||||
|
||||
delete(query: DeleteQuery) {
|
||||
const { where } = query;
|
||||
|
||||
for (const record of this.iterate(where)) {
|
||||
const key = String(record[this.keyField]);
|
||||
this.data.delete(key);
|
||||
this.dispatch(key, null);
|
||||
}
|
||||
}
|
||||
|
||||
toObject(record: any): Record<string, any> {
|
||||
return record;
|
||||
}
|
||||
|
||||
delete(key: Key) {
|
||||
this.data.delete(key);
|
||||
this.dispatch(key, null);
|
||||
this.dispatch('$$KEYS', this.keys());
|
||||
value(data: any, select: Select = '*') {
|
||||
if (select === 'key') {
|
||||
return data[this.keyField];
|
||||
}
|
||||
|
||||
if (select === '*') {
|
||||
return this.toObject(data);
|
||||
}
|
||||
|
||||
return pick(this.toObject(data), select);
|
||||
}
|
||||
|
||||
dispatch(key: Key, data: any) {
|
||||
this.subscriptions.get(key)?.forEach(callback => callback(data));
|
||||
private *iterate(where: WhereCondition = []) {
|
||||
if (Array.isArray(where)) {
|
||||
for (const value of this.data.values()) {
|
||||
if (this.match(value, where)) {
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const key = where.byKey;
|
||||
const record = this.data.get(key.toString());
|
||||
if (record) {
|
||||
yield record;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private match(record: any, where: WhereCondition) {
|
||||
return Array.isArray(where)
|
||||
? where.every(c => record[c.field] === c.value)
|
||||
: where.byKey === record[this.keyField];
|
||||
}
|
||||
|
||||
private dispatch(key: string, data: any) {
|
||||
this.subscriptions.forEach(callback => callback(key, data));
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import type { Key, TableAdapter, TableOptions } from '../types';
|
||||
|
||||
declare module '../types' {
|
||||
import type { TableAdapter, TableAdapterOptions } from '../types';
|
||||
declare module '../../types' {
|
||||
interface TableOptions {
|
||||
hooks?: Hook<unknown>[];
|
||||
}
|
||||
@ -15,12 +14,17 @@ export interface TableAdapterWithHook<T = unknown> extends Hook<T> {}
|
||||
export function HookAdapter(): ClassDecorator {
|
||||
// @ts-expect-error allow
|
||||
return (Class: { new (...args: any[]): TableAdapter }) => {
|
||||
return class TableAdapterImpl
|
||||
return class TableAdapterExtensions
|
||||
extends Class
|
||||
implements TableAdapterWithHook
|
||||
{
|
||||
hooks: Hook<unknown>[] = [];
|
||||
|
||||
override setup(opts: TableAdapterOptions): void {
|
||||
super.setup(opts);
|
||||
this.hooks = opts.hooks ?? [];
|
||||
}
|
||||
|
||||
deserialize(data: unknown) {
|
||||
if (!this.hooks.length) {
|
||||
return data;
|
||||
@ -32,28 +36,8 @@ export function HookAdapter(): ClassDecorator {
|
||||
);
|
||||
}
|
||||
|
||||
override setup(opts: TableOptions) {
|
||||
this.hooks = opts.hooks || [];
|
||||
super.setup(opts);
|
||||
}
|
||||
|
||||
override create(key: Key, data: any) {
|
||||
return this.deserialize(super.create(key, data));
|
||||
}
|
||||
|
||||
override get(key: Key) {
|
||||
return this.deserialize(super.get(key));
|
||||
}
|
||||
|
||||
override update(key: Key, data: any) {
|
||||
return this.deserialize(super.update(key, data));
|
||||
}
|
||||
|
||||
override subscribe(
|
||||
key: Key,
|
||||
callback: (data: unknown) => void
|
||||
): () => void {
|
||||
return super.subscribe(key, data => callback(this.deserialize(data)));
|
||||
override toObject(data: any): Record<string, any> {
|
||||
return this.deserialize(super.toObject(data));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,23 +1,66 @@
|
||||
import type { TableSchemaBuilder } from '../schema';
|
||||
import type { Key, TableOptions } from '../types';
|
||||
|
||||
export interface Key {
|
||||
toString(): string;
|
||||
export interface TableAdapterOptions extends TableOptions {
|
||||
keyField: string;
|
||||
}
|
||||
|
||||
export interface TableOptions {
|
||||
schema: TableSchemaBuilder;
|
||||
}
|
||||
type WhereEqCondition = {
|
||||
field: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export interface TableAdapter<K extends Key = any, T = unknown> {
|
||||
setup(opts: TableOptions): void;
|
||||
type WhereByKeyCondition = {
|
||||
byKey: Key;
|
||||
};
|
||||
|
||||
// currently only support eq condition
|
||||
// TODO(@forehalo): on the way [gt, gte, lt, lte, in, notIn, like, notLike, isNull, isNotNull, And, Or]
|
||||
export type WhereCondition = WhereEqCondition[] | WhereByKeyCondition;
|
||||
export type Select = '*' | 'key' | string[];
|
||||
|
||||
export type InsertQuery = {
|
||||
data: any;
|
||||
select?: Select;
|
||||
};
|
||||
|
||||
export type DeleteQuery = {
|
||||
where?: WhereCondition;
|
||||
};
|
||||
|
||||
export type UpdateQuery = {
|
||||
where?: WhereCondition;
|
||||
data: any;
|
||||
select?: Select;
|
||||
};
|
||||
|
||||
export type FindQuery = {
|
||||
where?: WhereCondition;
|
||||
select?: Select;
|
||||
};
|
||||
|
||||
export type ObserveQuery = {
|
||||
where?: WhereCondition;
|
||||
select?: Select;
|
||||
callback: (data: any[]) => void;
|
||||
};
|
||||
|
||||
export type Query =
|
||||
| InsertQuery
|
||||
| DeleteQuery
|
||||
| UpdateQuery
|
||||
| FindQuery
|
||||
| ObserveQuery;
|
||||
|
||||
export interface TableAdapter {
|
||||
setup(opts: TableAdapterOptions): void;
|
||||
dispose(): void;
|
||||
create(key: K, data: Partial<T>): T;
|
||||
get(key: K): T;
|
||||
subscribe(key: K, callback: (data: T) => void): () => void;
|
||||
keys(): K[];
|
||||
subscribeKeys(callback: (keys: K[]) => void): () => void;
|
||||
update(key: K, data: Partial<T>): T;
|
||||
delete(key: K): void;
|
||||
|
||||
toObject(record: any): Record<string, any>;
|
||||
insert(query: InsertQuery): any;
|
||||
update(query: UpdateQuery): any[];
|
||||
delete(query: DeleteQuery): void;
|
||||
find(query: FindQuery): any[];
|
||||
observe(query: ObserveQuery): () => void;
|
||||
}
|
||||
|
||||
export interface DBAdapter {
|
||||
|
@ -1,9 +1,24 @@
|
||||
import { omit } from 'lodash-es';
|
||||
import type { Doc, Map as YMap, Transaction, YMapEvent } from 'yjs';
|
||||
import { pick } from 'lodash-es';
|
||||
import {
|
||||
type AbstractType,
|
||||
type Doc,
|
||||
Map as YMap,
|
||||
type Transaction,
|
||||
} from 'yjs';
|
||||
|
||||
import { validators } from '../../validators';
|
||||
import { HookAdapter } from '../mixins';
|
||||
import type { Key, TableAdapter, TableOptions } from '../types';
|
||||
import type {
|
||||
DeleteQuery,
|
||||
FindQuery,
|
||||
InsertQuery,
|
||||
ObserveQuery,
|
||||
Select,
|
||||
TableAdapter,
|
||||
TableAdapterOptions,
|
||||
UpdateQuery,
|
||||
WhereCondition,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* Yjs Adapter for AFFiNE ORM
|
||||
@ -22,33 +37,29 @@ import type { Key, TableAdapter, TableOptions } from '../types';
|
||||
@HookAdapter()
|
||||
export class YjsTableAdapter implements TableAdapter {
|
||||
private readonly deleteFlagKey = '$$DELETED';
|
||||
private readonly keyFlagKey = '$$KEY';
|
||||
private readonly hiddenFields = [this.deleteFlagKey, this.keyFlagKey];
|
||||
private keyField: string = 'key';
|
||||
private fields: string[] = [];
|
||||
|
||||
private readonly origin = 'YjsTableAdapter';
|
||||
|
||||
keysCache: Set<Key> | null = null;
|
||||
cacheStaled = true;
|
||||
|
||||
constructor(
|
||||
private readonly tableName: string,
|
||||
private readonly doc: Doc
|
||||
) {}
|
||||
|
||||
setup(_opts: TableOptions): void {
|
||||
this.doc.on('update', (_, origin) => {
|
||||
if (origin !== this.origin) {
|
||||
this.markCacheStaled();
|
||||
}
|
||||
});
|
||||
setup(opts: TableAdapterOptions): void {
|
||||
this.keyField = opts.keyField;
|
||||
this.fields = Object.keys(opts.schema);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.doc.destroy();
|
||||
}
|
||||
|
||||
create(key: Key, data: any) {
|
||||
insert(query: InsertQuery) {
|
||||
const { data, select } = query;
|
||||
validators.validateYjsEntityData(this.tableName, data);
|
||||
const key = data[this.keyField];
|
||||
const record = this.doc.getMap(key.toString());
|
||||
|
||||
this.doc.transact(() => {
|
||||
@ -56,139 +67,174 @@ export class YjsTableAdapter implements TableAdapter {
|
||||
record.set(key, data[key]);
|
||||
}
|
||||
|
||||
this.keyBy(record, key);
|
||||
record.set(this.deleteFlagKey, false);
|
||||
record.delete(this.deleteFlagKey);
|
||||
}, this.origin);
|
||||
|
||||
this.markCacheStaled();
|
||||
return this.value(record);
|
||||
return this.value(record, select);
|
||||
}
|
||||
|
||||
update(key: Key, data: any) {
|
||||
update(query: UpdateQuery) {
|
||||
const { data, select, where } = query;
|
||||
validators.validateYjsEntityData(this.tableName, data);
|
||||
const record = this.record(key);
|
||||
|
||||
if (this.isDeleted(record)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const results: any[] = [];
|
||||
this.doc.transact(() => {
|
||||
for (const key in data) {
|
||||
record.set(key, data[key]);
|
||||
}
|
||||
}, this.origin);
|
||||
|
||||
return this.value(record);
|
||||
}
|
||||
|
||||
get(key: Key) {
|
||||
const record = this.record(key);
|
||||
return this.value(record);
|
||||
}
|
||||
|
||||
subscribe(key: Key, callback: (data: any) => void) {
|
||||
const record: YMap<any> = this.record(key);
|
||||
// init callback
|
||||
callback(this.value(record));
|
||||
|
||||
const ob = (event: YMapEvent<any>) => {
|
||||
callback(this.value(event.target));
|
||||
};
|
||||
record.observe(ob);
|
||||
|
||||
return () => {
|
||||
record.unobserve(ob);
|
||||
};
|
||||
}
|
||||
|
||||
keys() {
|
||||
const keysCache = this.buildKeysCache();
|
||||
return Array.from(keysCache);
|
||||
}
|
||||
|
||||
subscribeKeys(callback: (keys: Key[]) => void) {
|
||||
const keysCache = this.buildKeysCache();
|
||||
// init callback
|
||||
callback(Array.from(keysCache));
|
||||
|
||||
const ob = (tx: Transaction) => {
|
||||
const keysCache = this.buildKeysCache();
|
||||
|
||||
for (const [type] of tx.changed) {
|
||||
const data = type as unknown as YMap<any>;
|
||||
const key = this.keyof(data);
|
||||
if (this.isDeleted(data)) {
|
||||
keysCache.delete(key);
|
||||
} else {
|
||||
keysCache.add(key);
|
||||
for (const record of this.iterate(where)) {
|
||||
results.push(this.value(record, select));
|
||||
for (const key in data) {
|
||||
this.setField(record, key, data[key]);
|
||||
}
|
||||
}
|
||||
}, this.origin);
|
||||
|
||||
callback(Array.from(keysCache));
|
||||
return results;
|
||||
}
|
||||
|
||||
find(query: FindQuery) {
|
||||
const { where, select } = query;
|
||||
const records: any[] = [];
|
||||
for (const record of this.iterate(where)) {
|
||||
records.push(this.value(record, select));
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
observe(query: ObserveQuery) {
|
||||
const { where, select, callback } = query;
|
||||
|
||||
let listeningOnAll = false;
|
||||
const obKeys = new Set<any>();
|
||||
const results = [];
|
||||
|
||||
if (!where) {
|
||||
listeningOnAll = true;
|
||||
} else if ('byKey' in where) {
|
||||
obKeys.add(where.byKey.toString());
|
||||
}
|
||||
|
||||
for (const record of this.iterate(where)) {
|
||||
if (!listeningOnAll) {
|
||||
obKeys.add(this.keyof(record));
|
||||
}
|
||||
results.push(this.value(record, select));
|
||||
}
|
||||
|
||||
callback(results);
|
||||
|
||||
const ob = (tx: Transaction) => {
|
||||
for (const [ty] of tx.changed) {
|
||||
const record = ty as unknown as AbstractType<any>;
|
||||
if (
|
||||
listeningOnAll ||
|
||||
obKeys.has(this.keyof(record)) ||
|
||||
(where && this.match(record, where))
|
||||
) {
|
||||
callback(this.find({ where, select }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.doc.on('afterTransaction', ob);
|
||||
|
||||
return () => {
|
||||
this.doc.off('afterTransaction', ob);
|
||||
};
|
||||
}
|
||||
|
||||
delete(key: Key) {
|
||||
const record = this.record(key);
|
||||
delete(query: DeleteQuery) {
|
||||
const { where } = query;
|
||||
|
||||
this.doc.transact(() => {
|
||||
for (const key of record.keys()) {
|
||||
if (!this.hiddenFields.includes(key)) {
|
||||
record.delete(key);
|
||||
for (const record of this.iterate(where)) {
|
||||
this.deleteTy(record);
|
||||
}
|
||||
}, this.origin);
|
||||
}
|
||||
|
||||
toObject(ty: AbstractType<any>): Record<string, any> {
|
||||
return YMap.prototype.toJSON.call(ty);
|
||||
}
|
||||
|
||||
private recordByKey(key: string): AbstractType<any> | null {
|
||||
// detect if the record is there otherwise yjs will create an empty Map.
|
||||
if (this.doc.share.has(key)) {
|
||||
return this.doc.getMap(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private *iterate(where: WhereCondition = []) {
|
||||
// fast pass for key lookup without iterating the whole table
|
||||
if ('byKey' in where) {
|
||||
const record = this.recordByKey(where.byKey.toString());
|
||||
if (record) {
|
||||
yield record;
|
||||
}
|
||||
} else if (Array.isArray(where)) {
|
||||
for (const map of this.doc.share.values()) {
|
||||
if (this.match(map, where)) {
|
||||
yield map;
|
||||
}
|
||||
}
|
||||
record.set(this.deleteFlagKey, true);
|
||||
}, this.origin);
|
||||
this.markCacheStaled();
|
||||
}
|
||||
}
|
||||
|
||||
private isDeleted(record: YMap<any>) {
|
||||
return record.get(this.deleteFlagKey) === true;
|
||||
}
|
||||
|
||||
private record(key: Key) {
|
||||
return this.doc.getMap(key.toString());
|
||||
}
|
||||
|
||||
private value(record: YMap<any>) {
|
||||
if (this.isDeleted(record) || !record.size) {
|
||||
private value(record: AbstractType<any>, select: Select = '*') {
|
||||
if (this.isDeleted(record) || this.isEmpty(record)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return omit(record.toJSON(), this.hiddenFields);
|
||||
}
|
||||
|
||||
private buildKeysCache() {
|
||||
if (!this.keysCache || this.cacheStaled) {
|
||||
this.keysCache = new Set();
|
||||
|
||||
for (const key of this.doc.share.keys()) {
|
||||
const record = this.doc.getMap(key);
|
||||
if (!this.isDeleted(record)) {
|
||||
this.keysCache.add(this.keyof(record));
|
||||
}
|
||||
}
|
||||
this.cacheStaled = false;
|
||||
let selectedFields: string[];
|
||||
if (select === 'key') {
|
||||
return this.keyof(record);
|
||||
} else if (select === '*') {
|
||||
selectedFields = this.fields;
|
||||
} else {
|
||||
selectedFields = select;
|
||||
}
|
||||
|
||||
return this.keysCache;
|
||||
return pick(this.toObject(record), selectedFields);
|
||||
}
|
||||
|
||||
private markCacheStaled() {
|
||||
this.cacheStaled = true;
|
||||
private match(record: AbstractType<any>, where: WhereCondition) {
|
||||
return (
|
||||
!this.isDeleted(record) &&
|
||||
(Array.isArray(where)
|
||||
? where.every(c => this.field(record, c.field) === c.value)
|
||||
: where.byKey === this.keyof(record))
|
||||
);
|
||||
}
|
||||
|
||||
private keyof(record: YMap<any>) {
|
||||
return record.get(this.keyFlagKey);
|
||||
private isDeleted(record: AbstractType<any>) {
|
||||
return (
|
||||
this.field(record, this.deleteFlagKey) === true || this.isEmpty(record)
|
||||
);
|
||||
}
|
||||
|
||||
private keyBy(record: YMap<any>, key: Key) {
|
||||
record.set(this.keyFlagKey, key);
|
||||
private keyof(record: AbstractType<any>) {
|
||||
return this.field(record, this.keyField);
|
||||
}
|
||||
|
||||
private field(ty: AbstractType<any>, field: string) {
|
||||
return YMap.prototype.get.call(ty, field);
|
||||
}
|
||||
|
||||
private setField(ty: AbstractType<any>, field: string, value: any) {
|
||||
YMap.prototype.set.call(ty, field, value);
|
||||
}
|
||||
|
||||
private isEmpty(ty: AbstractType<any>) {
|
||||
return ty._map.size === 0;
|
||||
}
|
||||
|
||||
private deleteTy(ty: AbstractType<any>) {
|
||||
this.fields.forEach(field => {
|
||||
if (field !== this.keyField) {
|
||||
YMap.prototype.delete.call(ty, field);
|
||||
}
|
||||
});
|
||||
YMap.prototype.set.call(ty, this.deleteFlagKey, true);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { type DBAdapter, type Hook } from './adapters';
|
||||
import type { DBSchemaBuilder } from './schema';
|
||||
import { Table, type TableMap } from './table';
|
||||
import { type CreateEntityInput, Table, type TableMap } from './table';
|
||||
import { validators } from './validators';
|
||||
|
||||
class RawORMClient {
|
||||
hooksMap: Map<string, Hook<any>[]> = new Map();
|
||||
export class ORMClient {
|
||||
static hooksMap: Map<string, Hook<any>[]> = new Map();
|
||||
private readonly tables = new Map<string, Table<any>>();
|
||||
constructor(
|
||||
protected readonly db: DBSchemaBuilder,
|
||||
@ -17,7 +17,7 @@ class RawORMClient {
|
||||
if (!table) {
|
||||
table = new Table(this.adapter, tableName, {
|
||||
schema: tableSchema,
|
||||
hooks: this.hooksMap.get(tableName),
|
||||
hooks: ORMClient.hooksMap.get(tableName),
|
||||
});
|
||||
this.tables.set(tableName, table);
|
||||
}
|
||||
@ -27,7 +27,7 @@ class RawORMClient {
|
||||
});
|
||||
}
|
||||
|
||||
defineHook(tableName: string, _desc: string, hook: Hook<any>) {
|
||||
static defineHook(tableName: string, _desc: string, hook: Hook<any>) {
|
||||
let hooks = this.hooksMap.get(tableName);
|
||||
if (!hooks) {
|
||||
hooks = [];
|
||||
@ -38,28 +38,28 @@ class RawORMClient {
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
export function createORMClient<Schema extends DBSchemaBuilder>(
|
||||
db: Schema
|
||||
): ORMClientWithTablesClass<Schema> {
|
||||
Object.entries(db).forEach(([tableName, schema]) => {
|
||||
validators.validateTableSchema(tableName, schema);
|
||||
});
|
||||
|
||||
return new RawORMClient(db, new adapter(db, ...args)) as TableMap<Schema> &
|
||||
RawORMClient;
|
||||
class ORMClientWithTables extends ORMClient {
|
||||
constructor(adapter: DBAdapter) {
|
||||
super(db, adapter);
|
||||
}
|
||||
}
|
||||
|
||||
return ORMClientWithTables as any;
|
||||
}
|
||||
|
||||
export type ORMClient<Schema extends DBSchemaBuilder> = RawORMClient &
|
||||
TableMap<Schema>;
|
||||
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;
|
||||
};
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { isUndefined, omitBy } from 'lodash-es';
|
||||
import { Observable, shareReplay } from 'rxjs';
|
||||
|
||||
import type { DBAdapter, Key, TableAdapter, TableOptions } from './adapters';
|
||||
import type { DBAdapter, TableAdapter } from './adapters';
|
||||
import type {
|
||||
DBSchemaBuilder,
|
||||
FieldSchemaBuilder,
|
||||
TableSchema,
|
||||
TableSchemaBuilder,
|
||||
} from './schema';
|
||||
import type { Key, TableOptions } from './types';
|
||||
import { validators } from './validators';
|
||||
|
||||
type Pretty<T> = T extends any
|
||||
@ -74,10 +75,16 @@ export type UpdateEntityInput<T extends TableSchemaBuilder> = Pretty<{
|
||||
: never;
|
||||
}>;
|
||||
|
||||
export type FindEntityInput<T extends TableSchemaBuilder> = Pretty<{
|
||||
[key in keyof T]?: T[key] extends FieldSchemaBuilder<infer Type>
|
||||
? Type
|
||||
: never;
|
||||
}>;
|
||||
|
||||
export class Table<T extends TableSchemaBuilder> {
|
||||
readonly schema: TableSchema;
|
||||
readonly keyField: string = '';
|
||||
private readonly adapter: TableAdapter<PrimaryKeyFieldType<T>, Entity<T>>;
|
||||
private readonly adapter: TableAdapter;
|
||||
|
||||
private readonly subscribedKeys: Map<Key, Observable<any>> = new Map();
|
||||
|
||||
@ -87,7 +94,6 @@ export class Table<T extends TableSchemaBuilder> {
|
||||
private readonly opts: TableOptions
|
||||
) {
|
||||
this.adapter = db.table(name) as any;
|
||||
this.adapter.setup(opts);
|
||||
this.schema = Object.entries(this.opts.schema).reduce(
|
||||
(acc, [fieldName, fieldBuilder]) => {
|
||||
acc[fieldName] = fieldBuilder.schema;
|
||||
@ -99,6 +105,7 @@ export class Table<T extends TableSchemaBuilder> {
|
||||
},
|
||||
{} as TableSchema
|
||||
);
|
||||
this.adapter.setup({ ...opts, keyField: this.keyField });
|
||||
}
|
||||
|
||||
create(input: CreateEntityInput<T>): Entity<T> {
|
||||
@ -123,16 +130,35 @@ export class Table<T extends TableSchemaBuilder> {
|
||||
|
||||
validators.validateCreateEntityData(this, data);
|
||||
|
||||
return this.adapter.create(data[this.keyField], data);
|
||||
return this.adapter.insert({
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
update(key: PrimaryKeyFieldType<T>, input: UpdateEntityInput<T>): Entity<T> {
|
||||
update(
|
||||
key: PrimaryKeyFieldType<T>,
|
||||
input: UpdateEntityInput<T>
|
||||
): Entity<T> | null {
|
||||
validators.validateUpdateEntityData(this, input);
|
||||
return this.adapter.update(key, omitBy(input, isUndefined) as any);
|
||||
|
||||
const [record] = this.adapter.update({
|
||||
where: {
|
||||
byKey: key,
|
||||
},
|
||||
data: input,
|
||||
});
|
||||
|
||||
return record || null;
|
||||
}
|
||||
|
||||
get(key: PrimaryKeyFieldType<T>): Entity<T> {
|
||||
return this.adapter.get(key);
|
||||
get(key: PrimaryKeyFieldType<T>): Entity<T> | null {
|
||||
const [record] = this.adapter.find({
|
||||
where: {
|
||||
byKey: key,
|
||||
},
|
||||
});
|
||||
|
||||
return record || null;
|
||||
}
|
||||
|
||||
get$(key: PrimaryKeyFieldType<T>): Observable<Entity<T>> {
|
||||
@ -140,8 +166,13 @@ export class Table<T extends TableSchemaBuilder> {
|
||||
|
||||
if (!ob$) {
|
||||
ob$ = new Observable<Entity<T>>(subscriber => {
|
||||
const unsubscribe = this.adapter.subscribe(key, data => {
|
||||
subscriber.next(data);
|
||||
const unsubscribe = this.adapter.observe({
|
||||
where: {
|
||||
byKey: key,
|
||||
},
|
||||
callback: ([data]) => {
|
||||
subscriber.next(data || null);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
@ -161,8 +192,35 @@ export class Table<T extends TableSchemaBuilder> {
|
||||
return ob$;
|
||||
}
|
||||
|
||||
find(where: FindEntityInput<T>): Entity<T>[] {
|
||||
return this.adapter.find({
|
||||
where: Object.entries(where).map(([field, value]) => ({
|
||||
field,
|
||||
value,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
find$(where: FindEntityInput<T>): Observable<Entity<T>[]> {
|
||||
return new Observable<Entity<T>[]>(subscriber => {
|
||||
const unsubscribe = this.adapter.observe({
|
||||
where: Object.entries(where).map(([field, value]) => ({
|
||||
field,
|
||||
value,
|
||||
})),
|
||||
callback: data => {
|
||||
subscriber.next(data);
|
||||
},
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
});
|
||||
}
|
||||
|
||||
keys(): PrimaryKeyFieldType<T>[] {
|
||||
return this.adapter.keys();
|
||||
return this.adapter.find({
|
||||
select: 'key',
|
||||
});
|
||||
}
|
||||
|
||||
keys$(): Observable<PrimaryKeyFieldType<T>[]> {
|
||||
@ -170,8 +228,11 @@ export class Table<T extends TableSchemaBuilder> {
|
||||
|
||||
if (!ob$) {
|
||||
ob$ = new Observable<PrimaryKeyFieldType<T>[]>(subscriber => {
|
||||
const unsubscribe = this.adapter.subscribeKeys(keys => {
|
||||
subscriber.next(keys);
|
||||
const unsubscribe = this.adapter.observe({
|
||||
select: 'key',
|
||||
callback: (keys: PrimaryKeyFieldType<T>[]) => {
|
||||
subscriber.next(keys);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
@ -192,7 +253,11 @@ export class Table<T extends TableSchemaBuilder> {
|
||||
}
|
||||
|
||||
delete(key: PrimaryKeyFieldType<T>) {
|
||||
return this.adapter.delete(key);
|
||||
this.adapter.delete({
|
||||
where: {
|
||||
byKey: key,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
9
packages/common/infra/src/orm/core/types.ts
Normal file
9
packages/common/infra/src/orm/core/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { TableSchemaBuilder } from './schema';
|
||||
|
||||
export interface Key {
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export interface TableOptions {
|
||||
schema: TableSchemaBuilder;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import type { TableSchemaValidator } from './types';
|
||||
|
||||
const PRESERVED_FIELDS = ['$$KEY', '$$DELETED'];
|
||||
const PRESERVED_FIELDS = ['$$DELETED'];
|
||||
|
||||
interface DataValidator {
|
||||
validate(tableName: string, data: any): void;
|
||||
|
Loading…
Reference in New Issue
Block a user