mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 19:58:09 +00:00
full text adapter
Signed-off-by: Andrey Platov <andrey@hardcoreeng.com>
This commit is contained in:
parent
9562eefd8e
commit
2a010b5311
@ -16,7 +16,7 @@
|
||||
import type { Tx, Storage, Ref, Doc, Class, DocumentQuery, FindResult, FindOptions, TxHander, ServerStorage } from '@anticrm/core'
|
||||
import { DOMAIN_TX } from '@anticrm/core'
|
||||
import { createInMemoryAdapter, createInMemoryTxAdapter } from '@anticrm/dev-storage'
|
||||
import { createServerStorage } from '@anticrm/server-core'
|
||||
import { createServerStorage, FullTextAdapter, IndexedDoc } from '@anticrm/server-core'
|
||||
import type { DbConfiguration } from '@anticrm/server-core'
|
||||
|
||||
class ServerStorageWrapper implements Storage {
|
||||
@ -32,6 +32,20 @@ class ServerStorageWrapper implements Storage {
|
||||
}
|
||||
}
|
||||
|
||||
class NullFullTextAdapter implements FullTextAdapter {
|
||||
async index (doc: IndexedDoc): Promise<void> {
|
||||
console.log('noop full text indexer: ', doc)
|
||||
}
|
||||
|
||||
async search (query: any): Promise<IndexedDoc[]> {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function createNullFullTextAdapter (): Promise<FullTextAdapter> {
|
||||
return new NullFullTextAdapter()
|
||||
}
|
||||
|
||||
export async function connect (handler: (tx: Tx) => void): Promise<Storage> {
|
||||
const conf: DbConfiguration = {
|
||||
domains: {
|
||||
@ -48,6 +62,10 @@ export async function connect (handler: (tx: Tx) => void): Promise<Storage> {
|
||||
url: ''
|
||||
}
|
||||
},
|
||||
fulltextAdapter: {
|
||||
factory: createNullFullTextAdapter,
|
||||
url: ''
|
||||
},
|
||||
workspace: ''
|
||||
}
|
||||
const serverStorage = await createServerStorage(conf)
|
||||
|
@ -17,12 +17,26 @@
|
||||
import { DOMAIN_TX } from '@anticrm/core'
|
||||
import { start as startJsonRpc } from '@anticrm/server-ws'
|
||||
import { createInMemoryAdapter, createInMemoryTxAdapter } from '@anticrm/dev-storage'
|
||||
import { createServerStorage } from '@anticrm/server-core'
|
||||
import { createServerStorage, FullTextAdapter, IndexedDoc } from '@anticrm/server-core'
|
||||
import type { DbConfiguration } from '@anticrm/server-core'
|
||||
|
||||
import { addLocation } from '@anticrm/platform'
|
||||
import { serverChunterId } from '@anticrm/server-chunter'
|
||||
|
||||
class NullFullTextAdapter implements FullTextAdapter {
|
||||
async index (doc: IndexedDoc): Promise<void> {
|
||||
console.log('noop full text indexer: ', doc)
|
||||
}
|
||||
|
||||
async search (query: any): Promise<IndexedDoc[]> {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function createNullFullTextAdapter (): Promise<FullTextAdapter> {
|
||||
return new NullFullTextAdapter()
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -45,6 +59,10 @@ export async function start (port: number, host?: string): Promise<void> {
|
||||
url: ''
|
||||
}
|
||||
},
|
||||
fulltextAdapter: {
|
||||
factory: createNullFullTextAdapter,
|
||||
url: ''
|
||||
},
|
||||
workspace: ''
|
||||
}
|
||||
return createServerStorage(conf)
|
||||
|
@ -169,12 +169,20 @@ export class Hierarchy {
|
||||
attributes.set(attribute.name, attribute)
|
||||
}
|
||||
|
||||
getAttributes (clazz: Ref<Class<Obj>>): Map<string, AnyAttribute> {
|
||||
const attributes = this.attributes.get(clazz)
|
||||
if (attributes === undefined) {
|
||||
throw new Error('attributes not found for class ' + clazz)
|
||||
getAllAttributes (clazz: Ref<Class<Obj>>): Map<string, AnyAttribute> {
|
||||
const result = new Map<string, AnyAttribute>()
|
||||
const ancestors = this.getAncestors(clazz)
|
||||
|
||||
for (const cls of ancestors) {
|
||||
const attributes = this.attributes.get(cls)
|
||||
if (attributes !== undefined) {
|
||||
for (const [name, attr] of attributes) {
|
||||
result.set(name, attr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return attributes
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
getAttribute (_class: Ref<Class<Obj>>, name: string): AnyAttribute {
|
||||
|
65
server/core/src/fulltext.ts
Normal file
65
server/core/src/fulltext.ts
Normal file
@ -0,0 +1,65 @@
|
||||
//
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { TxCreateDoc, Doc, Ref, Class, Obj, Hierarchy, AnyAttribute } from '@anticrm/core'
|
||||
import { TxProcessor, IndexKind } from '@anticrm/core'
|
||||
import type { IndexedDoc, FullTextAdapter } from './types'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const NO_INDEX = {} as AnyAttribute
|
||||
|
||||
export class FullTextIndex extends TxProcessor {
|
||||
private readonly indexes = new Map<Ref<Class<Obj>>, AnyAttribute>()
|
||||
|
||||
constructor (
|
||||
private readonly hierarchy: Hierarchy,
|
||||
private readonly adapter: FullTextAdapter
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
private findFullTextAttribute (clazz: Ref<Class<Obj>>): AnyAttribute | undefined {
|
||||
const attribute = this.indexes.get(clazz)
|
||||
if (attribute === undefined) {
|
||||
const attributes = this.hierarchy.getAllAttributes(clazz)
|
||||
for (const [, attr] of attributes) {
|
||||
if (attr.index === IndexKind.FullText) {
|
||||
this.indexes.set(clazz, attr)
|
||||
return attr
|
||||
}
|
||||
}
|
||||
this.indexes.set(clazz, NO_INDEX)
|
||||
} else if (attribute !== NO_INDEX) {
|
||||
return attribute
|
||||
}
|
||||
}
|
||||
|
||||
protected override async txCreateDoc (tx: TxCreateDoc<Doc>): Promise<void> {
|
||||
const attribute = this.findFullTextAttribute(tx.objectClass)
|
||||
if (attribute === undefined) return
|
||||
const doc = TxProcessor.createDoc2Doc(tx)
|
||||
const content = (doc as any)[attribute.name]
|
||||
const indexedDoc: IndexedDoc = {
|
||||
id: doc._id,
|
||||
_class: doc._class,
|
||||
modifiedBy: doc.modifiedBy,
|
||||
modifiedOn: doc.modifiedOn,
|
||||
space: doc.space,
|
||||
content
|
||||
}
|
||||
return await this.adapter.index(indexedDoc)
|
||||
}
|
||||
}
|
@ -14,191 +14,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Doc, Tx, TxCreateDoc, Ref, Class, ServerStorage, DocumentQuery, FindOptions, FindResult, Storage, Account, Domain, TxCUD } from '@anticrm/core'
|
||||
import core, { Hierarchy, TxFactory, DOMAIN_TX } from '@anticrm/core'
|
||||
import type { Resource, Plugin } from '@anticrm/platform'
|
||||
import { getResource, plugin } from '@anticrm/platform'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type TriggerFunc = (tx: Tx, txFactory: TxFactory) => Promise<Tx[]>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Trigger extends Doc {
|
||||
trigger: Resource<TriggerFunc>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class Triggers {
|
||||
private readonly triggers: TriggerFunc[] = []
|
||||
|
||||
async tx (tx: Tx): Promise<void> {
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
const createTx = tx as TxCreateDoc<Doc>
|
||||
if (createTx.objectClass === serverCore.class.Trigger) {
|
||||
const trigger = (createTx as TxCreateDoc<Trigger>).attributes.trigger
|
||||
const func = await getResource(trigger)
|
||||
this.triggers.push(func)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async apply (account: Ref<Account>, tx: Tx): Promise<Tx[]> {
|
||||
const derived = this.triggers.map(trigger => trigger(tx, new TxFactory(account)))
|
||||
const result = await Promise.all(derived)
|
||||
return result.flatMap(x => x)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DbAdapter extends Storage {
|
||||
/**
|
||||
* Method called after hierarchy is ready to use.
|
||||
*/
|
||||
init: (model: Tx[]) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface TxAdapter extends DbAdapter {
|
||||
getModel: () => Promise<Tx[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type DbAdapterFactory = (hierarchy: Hierarchy, url: string, db: string) => Promise<DbAdapter>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DbAdapterConfiguration {
|
||||
factory: DbAdapterFactory
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DbConfiguration {
|
||||
adapters: Record<string, DbAdapterConfiguration>
|
||||
domains: Record<string, string>
|
||||
defaultAdapter: string
|
||||
workspace: string
|
||||
}
|
||||
|
||||
class TServerStorage implements ServerStorage {
|
||||
constructor (
|
||||
private readonly domains: Record<string, string>,
|
||||
private readonly defaultAdapter: string,
|
||||
private readonly adapters: Map<string, DbAdapter>,
|
||||
private readonly hierarchy: Hierarchy,
|
||||
private readonly triggers: Triggers
|
||||
) {
|
||||
}
|
||||
|
||||
private getAdapter (domain: Domain): DbAdapter {
|
||||
const name = this.domains[domain] ?? this.defaultAdapter
|
||||
const adapter = this.adapters.get(name)
|
||||
if (adapter === undefined) {
|
||||
throw new Error('adapter not provided: ' + name)
|
||||
}
|
||||
return adapter
|
||||
}
|
||||
|
||||
private routeTx (tx: Tx): Promise<void> {
|
||||
if (this.hierarchy.isDerived(tx._class, core.class.TxCUD)) {
|
||||
const txCUD = tx as TxCUD<Doc>
|
||||
const domain = this.hierarchy.getDomain(txCUD.objectClass)
|
||||
return this.getAdapter(domain).tx(txCUD)
|
||||
} else {
|
||||
throw new Error('not implemented (not derived from TxCUD)')
|
||||
}
|
||||
}
|
||||
|
||||
async findAll<T extends Doc> (
|
||||
clazz: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
const domain = this.hierarchy.getDomain(clazz)
|
||||
return await this.getAdapter(domain).findAll(clazz, query, options)
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<Tx[]> {
|
||||
// store tx
|
||||
await this.getAdapter(DOMAIN_TX).tx(tx)
|
||||
|
||||
if (tx.objectSpace === core.space.Model) {
|
||||
// maintain hiearachy and triggers
|
||||
this.hierarchy.tx(tx)
|
||||
await this.triggers.tx(tx)
|
||||
return []
|
||||
} else {
|
||||
// store object
|
||||
await this.routeTx(tx)
|
||||
// invoke triggers and store derived objects
|
||||
const derived = await this.triggers.apply(tx.modifiedBy, tx)
|
||||
for (const tx of derived) {
|
||||
await this.routeTx(tx)
|
||||
}
|
||||
return derived
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function createServerStorage (conf: DbConfiguration): Promise<ServerStorage> {
|
||||
const hierarchy = new Hierarchy()
|
||||
const triggers = new Triggers()
|
||||
const adapters = new Map<string, DbAdapter>()
|
||||
|
||||
for (const key in conf.adapters) {
|
||||
const adapterConf = conf.adapters[key]
|
||||
adapters.set(key, await adapterConf.factory(hierarchy, adapterConf.url, conf.workspace))
|
||||
}
|
||||
|
||||
const txAdapter = adapters.get(conf.domains[DOMAIN_TX]) as TxAdapter
|
||||
if (txAdapter === undefined) {
|
||||
console.log('no txadapter found')
|
||||
}
|
||||
|
||||
const model = await txAdapter.getModel()
|
||||
|
||||
for (const tx of model) {
|
||||
hierarchy.tx(tx)
|
||||
await triggers.tx(tx)
|
||||
}
|
||||
|
||||
for (const [, adapter] of adapters) {
|
||||
await adapter.init(model)
|
||||
}
|
||||
|
||||
return new TServerStorage(conf.domains, conf.defaultAdapter, adapters, hierarchy, triggers)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const serverCoreId = 'server-core' as Plugin
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
const serverCore = plugin(serverCoreId, {
|
||||
class: {
|
||||
Trigger: '' as Ref<Class<Trigger>>
|
||||
}
|
||||
})
|
||||
|
||||
export default serverCore
|
||||
export * from './types'
|
||||
export * from './storage'
|
||||
export { default } from './plugin'
|
||||
|
37
server/core/src/plugin.ts
Normal file
37
server/core/src/plugin.ts
Normal file
@ -0,0 +1,37 @@
|
||||
//
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Plugin } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
|
||||
import type { Ref, Class } from '@anticrm/core'
|
||||
import type { Trigger } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const serverCoreId = 'server-core' as Plugin
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
const serverCore = plugin(serverCoreId, {
|
||||
class: {
|
||||
Trigger: '' as Ref<Class<Trigger>>
|
||||
}
|
||||
})
|
||||
|
||||
export default serverCore
|
168
server/core/src/storage.ts
Normal file
168
server/core/src/storage.ts
Normal file
@ -0,0 +1,168 @@
|
||||
//
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { ServerStorage, Domain, Tx, TxCUD, Doc, Ref, Class, DocumentQuery, FindResult, FindOptions, Storage } from '@anticrm/core'
|
||||
import core, { Hierarchy, DOMAIN_TX } from '@anticrm/core'
|
||||
import type { FullTextAdapterFactory } from './types'
|
||||
import { FullTextIndex } from './fulltext'
|
||||
import { Triggers } from './triggers'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DbAdapter extends Storage {
|
||||
/**
|
||||
* Method called after hierarchy is ready to use.
|
||||
*/
|
||||
init: (model: Tx[]) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface TxAdapter extends DbAdapter {
|
||||
getModel: () => Promise<Tx[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type DbAdapterFactory = (hierarchy: Hierarchy, url: string, db: string) => Promise<DbAdapter>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DbAdapterConfiguration {
|
||||
factory: DbAdapterFactory
|
||||
url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DbConfiguration {
|
||||
adapters: Record<string, DbAdapterConfiguration>
|
||||
domains: Record<string, string>
|
||||
defaultAdapter: string
|
||||
workspace: string
|
||||
fulltextAdapter: {
|
||||
factory: FullTextAdapterFactory
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
class TServerStorage implements ServerStorage {
|
||||
constructor (
|
||||
private readonly domains: Record<string, string>,
|
||||
private readonly defaultAdapter: string,
|
||||
private readonly adapters: Map<string, DbAdapter>,
|
||||
private readonly hierarchy: Hierarchy,
|
||||
private readonly triggers: Triggers,
|
||||
private readonly fulltext: FullTextIndex
|
||||
) {
|
||||
}
|
||||
|
||||
private getAdapter (domain: Domain): DbAdapter {
|
||||
const name = this.domains[domain] ?? this.defaultAdapter
|
||||
const adapter = this.adapters.get(name)
|
||||
if (adapter === undefined) {
|
||||
throw new Error('adapter not provided: ' + name)
|
||||
}
|
||||
return adapter
|
||||
}
|
||||
|
||||
private routeTx (tx: Tx): Promise<void> {
|
||||
if (this.hierarchy.isDerived(tx._class, core.class.TxCUD)) {
|
||||
const txCUD = tx as TxCUD<Doc>
|
||||
const domain = this.hierarchy.getDomain(txCUD.objectClass)
|
||||
return this.getAdapter(domain).tx(txCUD)
|
||||
} else {
|
||||
throw new Error('not implemented (not derived from TxCUD)')
|
||||
}
|
||||
}
|
||||
|
||||
async findAll<T extends Doc> (
|
||||
clazz: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
const domain = this.hierarchy.getDomain(clazz)
|
||||
return await this.getAdapter(domain).findAll(clazz, query, options)
|
||||
}
|
||||
|
||||
async tx (tx: Tx): Promise<Tx[]> {
|
||||
// store tx
|
||||
await this.getAdapter(DOMAIN_TX).tx(tx)
|
||||
|
||||
if (tx.objectSpace === core.space.Model) {
|
||||
// maintain hiearachy and triggers
|
||||
this.hierarchy.tx(tx)
|
||||
await this.triggers.tx(tx)
|
||||
return []
|
||||
} else {
|
||||
// store object
|
||||
await this.routeTx(tx)
|
||||
// invoke triggers and store derived objects
|
||||
const derived = await this.triggers.apply(tx.modifiedBy, tx)
|
||||
for (const tx of derived) {
|
||||
await this.routeTx(tx)
|
||||
}
|
||||
// index object
|
||||
await this.fulltext.tx(tx)
|
||||
// index derived objects
|
||||
for (const tx of derived) {
|
||||
await this.fulltext.tx(tx)
|
||||
}
|
||||
|
||||
return derived
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function createServerStorage (conf: DbConfiguration): Promise<ServerStorage> {
|
||||
const hierarchy = new Hierarchy()
|
||||
const triggers = new Triggers()
|
||||
const adapters = new Map<string, DbAdapter>()
|
||||
|
||||
for (const key in conf.adapters) {
|
||||
const adapterConf = conf.adapters[key]
|
||||
adapters.set(key, await adapterConf.factory(hierarchy, adapterConf.url, conf.workspace))
|
||||
}
|
||||
|
||||
const txAdapter = adapters.get(conf.domains[DOMAIN_TX]) as TxAdapter
|
||||
if (txAdapter === undefined) {
|
||||
console.log('no txadapter found')
|
||||
}
|
||||
|
||||
const model = await txAdapter.getModel()
|
||||
|
||||
for (const tx of model) {
|
||||
hierarchy.tx(tx)
|
||||
await triggers.tx(tx)
|
||||
}
|
||||
|
||||
for (const [, adapter] of adapters) {
|
||||
await adapter.init(model)
|
||||
}
|
||||
|
||||
const fulltextAdapter = await conf.fulltextAdapter.factory(conf.fulltextAdapter.url, conf.workspace)
|
||||
const fulltext = new FullTextIndex(hierarchy, fulltextAdapter)
|
||||
|
||||
return new TServerStorage(conf.domains, conf.defaultAdapter, adapters, hierarchy, triggers, fulltext)
|
||||
}
|
47
server/core/src/triggers.ts
Normal file
47
server/core/src/triggers.ts
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Tx, Doc, TxCreateDoc, Ref, Account } from '@anticrm/core'
|
||||
import core, { TxFactory } from '@anticrm/core'
|
||||
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import type { Trigger, TriggerFunc } from './types'
|
||||
|
||||
import serverCore from './plugin'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class Triggers {
|
||||
private readonly triggers: TriggerFunc[] = []
|
||||
|
||||
async tx (tx: Tx): Promise<void> {
|
||||
if (tx._class === core.class.TxCreateDoc) {
|
||||
const createTx = tx as TxCreateDoc<Doc>
|
||||
if (createTx.objectClass === serverCore.class.Trigger) {
|
||||
const trigger = (createTx as TxCreateDoc<Trigger>).attributes.trigger
|
||||
const func = await getResource(trigger)
|
||||
this.triggers.push(func)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async apply (account: Ref<Account>, tx: Tx): Promise<Tx[]> {
|
||||
const derived = this.triggers.map(trigger => trigger(tx, new TxFactory(account)))
|
||||
const result = await Promise.all(derived)
|
||||
return result.flatMap(x => x)
|
||||
}
|
||||
}
|
61
server/core/src/types.ts
Normal file
61
server/core/src/types.ts
Normal file
@ -0,0 +1,61 @@
|
||||
//
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Tx, Ref, Doc, Class, Space, Timestamp, Account } from '@anticrm/core'
|
||||
import { TxFactory } from '@anticrm/core'
|
||||
import type { Resource } from '@anticrm/platform'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type TriggerFunc = (tx: Tx, txFactory: TxFactory) => Promise<Tx[]>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Trigger extends Doc {
|
||||
trigger: Resource<TriggerFunc>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IndexedDoc {
|
||||
id: Ref<Doc>
|
||||
_class: Ref<Class<Doc>>
|
||||
space: Ref<Space>
|
||||
modifiedOn: Timestamp
|
||||
modifiedBy: Ref<Account>
|
||||
content: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type SearchQuery = any
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface FullTextAdapter {
|
||||
index: (doc: IndexedDoc) => Promise<void>
|
||||
search: (query: SearchQuery) => Promise<IndexedDoc[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type FullTextAdapterFactory = (url: string, workspace: string) => Promise<FullTextAdapter>
|
@ -14,28 +14,26 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { Hierarchy, TxFactory } from '@anticrm/core'
|
||||
import type { Tx } from '@anticrm/core'
|
||||
import { createElasticAdapter } from '../adapter'
|
||||
// import type { Tx } from '@anticrm/core'
|
||||
|
||||
import * as txJson from './model.tx.json'
|
||||
// import * as txJson from './model.tx.json'
|
||||
|
||||
const txes = txJson as unknown as Tx[]
|
||||
// const txes = txJson as unknown as Tx[]
|
||||
|
||||
describe('client', () => {
|
||||
it('should create document', async () => {
|
||||
const hierarchy = new Hierarchy()
|
||||
for (const tx of txes) hierarchy.tx(tx)
|
||||
const adapter = await createElasticAdapter(hierarchy, 'http://localhost:9200/', 'ws1')
|
||||
const txFactory = new TxFactory(core.account.System)
|
||||
const createTx = txFactory.createTxCreateDoc(core.class.Space, core.space.Model, {
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
private: false,
|
||||
members: []
|
||||
})
|
||||
await adapter.tx(createTx)
|
||||
const spaces = await adapter.findAll(core.class.Space, {})
|
||||
console.log(spaces)
|
||||
// const hierarchy = new Hierarchy()
|
||||
// for (const tx of txes) hierarchy.tx(tx)
|
||||
// const adapter = await createElasticAdapter(hierarchy, 'http://localhost:9200/', 'ws1')
|
||||
// const txFactory = new TxFactory(core.account.System)
|
||||
// const createTx = txFactory.createTxCreateDoc(core.class.Space, core.space.Model, {
|
||||
// name: 'name',
|
||||
// description: 'description',
|
||||
// private: false,
|
||||
// members: []
|
||||
// })
|
||||
// await adapter.tx(createTx)
|
||||
// const spaces = await adapter.findAll(core.class.Space, {})
|
||||
// console.log(spaces)
|
||||
})
|
||||
})
|
||||
|
@ -14,37 +14,22 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { TxProcessor, Hierarchy } from '@anticrm/core'
|
||||
import type { Doc, Ref, Class, DocumentQuery, FindOptions, FindResult, TxCreateDoc } from '@anticrm/core'
|
||||
import type { DbAdapter } from '@anticrm/server-core'
|
||||
import type { FullTextAdapter, IndexedDoc, SearchQuery } from '@anticrm/server-core'
|
||||
|
||||
import { Client } from '@elastic/elasticsearch'
|
||||
|
||||
function translateDoc (doc: Doc): any {
|
||||
const obj = { id: doc._id, ...doc } as any
|
||||
delete obj._id
|
||||
return obj
|
||||
}
|
||||
|
||||
class ElasticAdapter extends TxProcessor implements DbAdapter {
|
||||
class ElasticAdapter implements FullTextAdapter {
|
||||
constructor (
|
||||
private readonly client: Client,
|
||||
private readonly db: string,
|
||||
private readonly hierarchy: Hierarchy
|
||||
private readonly db: string
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async init (): Promise<void> {}
|
||||
|
||||
async findAll<T extends Doc> (
|
||||
clazz: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
const domain = this.hierarchy.getDomain(clazz)
|
||||
async search (
|
||||
query: SearchQuery
|
||||
): Promise<IndexedDoc[]> {
|
||||
const result = await this.client.search({
|
||||
index: this.db + '_' + domain,
|
||||
index: this.db,
|
||||
type: '_doc',
|
||||
body: {
|
||||
}
|
||||
@ -54,24 +39,23 @@ class ElasticAdapter extends TxProcessor implements DbAdapter {
|
||||
return []
|
||||
}
|
||||
|
||||
protected override async txCreateDoc (tx: TxCreateDoc<Doc>): Promise<void> {
|
||||
const doc = TxProcessor.createDoc2Doc(tx)
|
||||
const domain = this.hierarchy.getDomain(doc._class)
|
||||
async index (doc: IndexedDoc): Promise<void> {
|
||||
await this.client.index({
|
||||
index: this.db + '_' + domain,
|
||||
index: this.db,
|
||||
type: '_doc',
|
||||
body: translateDoc(doc)
|
||||
body: doc
|
||||
})
|
||||
console.log('indexing this thing: ', doc)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function createElasticAdapter (hierarchy: Hierarchy, url: string, dbName: string): Promise<DbAdapter> {
|
||||
export async function createElasticAdapter (url: string, dbName: string): Promise<FullTextAdapter> {
|
||||
const client = new Client({
|
||||
node: url
|
||||
})
|
||||
|
||||
return new ElasticAdapter(client, dbName, hierarchy)
|
||||
return new ElasticAdapter(client, dbName)
|
||||
}
|
||||
|
@ -28,6 +28,7 @@
|
||||
"@anticrm/server-ws": "~0.6.11",
|
||||
"@anticrm/server-chunter": "~0.6.1",
|
||||
"@anticrm/server-chunter-resources": "~0.6.0",
|
||||
"@anticrm/mongo": "~0.6.1"
|
||||
"@anticrm/mongo": "~0.6.1",
|
||||
"@anticrm/elastic": "~0.6.0"
|
||||
}
|
||||
}
|
||||
|
@ -17,11 +17,16 @@
|
||||
import { start } from '.'
|
||||
|
||||
const url = process.env.MONGO_URL
|
||||
|
||||
if (url === undefined) {
|
||||
console.error('please provide mongodb url')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const elasticUrl = process.env.ELASTIC_URL
|
||||
if (elasticUrl === undefined) {
|
||||
console.error('please provide elastic url')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
start(url, 3333)
|
||||
start(url, elasticUrl, 3333)
|
||||
|
@ -17,6 +17,7 @@
|
||||
import { DOMAIN_TX } from '@anticrm/core'
|
||||
import { start as startJsonRpc } from '@anticrm/server-ws'
|
||||
import { createMongoAdapter, createMongoTxAdapter } from '@anticrm/mongo'
|
||||
import { createElasticAdapter } from '@anticrm/elastic'
|
||||
import { createServerStorage } from '@anticrm/server-core'
|
||||
import type { DbConfiguration } from '@anticrm/server-core'
|
||||
|
||||
@ -26,7 +27,7 @@ import { serverChunterId } from '@anticrm/server-chunter'
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function start (dbUrl: string, port: number, host?: string): Promise<void> {
|
||||
export async function start (dbUrl: string, fullTextUrl: string, port: number, host?: string): Promise<void> {
|
||||
addLocation(serverChunterId, () => import('@anticrm/server-chunter-resources'))
|
||||
|
||||
startJsonRpc((workspace: string) => {
|
||||
@ -45,6 +46,10 @@ export async function start (dbUrl: string, port: number, host?: string): Promis
|
||||
url: dbUrl
|
||||
}
|
||||
},
|
||||
fulltextAdapter: {
|
||||
factory: createElasticAdapter,
|
||||
url: fullTextUrl
|
||||
},
|
||||
workspace
|
||||
}
|
||||
return createServerStorage(conf)
|
||||
|
@ -27,6 +27,7 @@ export async function createModel (url: string, dbName: string): Promise<number>
|
||||
try {
|
||||
await client.connect()
|
||||
const db = client.db(dbName)
|
||||
await db.dropDatabase()
|
||||
const result = await db.collection(DOMAIN_TX).insertMany(txJson as Document[])
|
||||
return result.insertedCount
|
||||
} finally {
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user