Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2024-09-11 15:31:26 +07:00
committed by GitHub
parent 74c27d6dd7
commit bf1de1f436
62 changed files with 4187 additions and 632 deletions
+233
View File
@@ -0,0 +1,233 @@
//
// Copyright © 2024 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 core, {
type Account,
type Arr,
type AttachedDoc,
type Class,
ClassifierKind,
type Data,
type Doc,
DOMAIN_DOC_INDEX_STATE,
DOMAIN_MODEL,
DOMAIN_TX,
type Mixin,
type Obj,
type Ref,
type TxCreateDoc,
type TxCUD,
TxFactory,
AccountRole
} from '@hcengineering/core'
import type { IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
export const txFactory = new TxFactory(core.account.System)
export function createClass (_class: Ref<Class<Obj>>, attributes: Data<Class<Obj>>): TxCreateDoc<Doc> {
return txFactory.createTxCreateDoc(core.class.Class, core.space.Model, attributes, _class)
}
/**
* @public
*/
export function createDoc<T extends Doc> (
_class: Ref<Class<T>>,
attributes: Data<T>,
id?: Ref<T>,
modifiedBy?: Ref<Account>
): TxCreateDoc<Doc> {
const result = txFactory.createTxCreateDoc(_class, core.space.Model, attributes, id)
if (modifiedBy !== undefined) {
result.modifiedBy = modifiedBy
}
return result
}
/**
* @public
*/
export interface TestMixin extends Doc {
arr: Arr<string>
}
/**
* @public
*/
export interface AttachedComment extends AttachedDoc {
message: string
}
/**
* @public
*/
export const test = plugin('test' as Plugin, {
mixin: {
TestMixin: '' as Ref<Mixin<TestMixin>>
},
class: {
TestComment: '' as Ref<Class<AttachedComment>>
}
})
/**
* @public
* Generate minimal model for testing purposes.
* @returns R
*/
export function genMinModel (): TxCUD<Doc>[] {
const txes = []
// Fill Tx'es with basic model classes.
txes.push(createClass(core.class.Obj, { label: 'Obj' as IntlString, kind: ClassifierKind.CLASS }))
txes.push(
createClass(core.class.Doc, { label: 'Doc' as IntlString, extends: core.class.Obj, kind: ClassifierKind.CLASS })
)
txes.push(
createClass(core.class.AttachedDoc, {
label: 'AttachedDoc' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.MIXIN
})
)
txes.push(
createClass(core.class.Class, {
label: 'Class' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_MODEL
})
)
txes.push(
createClass(core.class.Space, {
label: 'Space' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_MODEL
})
)
txes.push(
createClass(core.class.DocIndexState, {
label: 'DocIndexState' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_DOC_INDEX_STATE
})
)
txes.push(
createClass(core.class.Account, {
label: 'Account' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_MODEL
})
)
txes.push(
createClass(core.class.Tx, {
label: 'Tx' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_TX
})
)
txes.push(
createClass(core.class.TxCUD, {
label: 'TxCUD' as IntlString,
extends: core.class.Tx,
kind: ClassifierKind.CLASS,
domain: DOMAIN_TX
})
)
txes.push(
createClass(core.class.TxCreateDoc, {
label: 'TxCreateDoc' as IntlString,
extends: core.class.TxCUD,
kind: ClassifierKind.CLASS
})
)
txes.push(
createClass(core.class.TxUpdateDoc, {
label: 'TxUpdateDoc' as IntlString,
extends: core.class.TxCUD,
kind: ClassifierKind.CLASS
})
)
txes.push(
createClass(core.class.TxRemoveDoc, {
label: 'TxRemoveDoc' as IntlString,
extends: core.class.TxCUD,
kind: ClassifierKind.CLASS
})
)
txes.push(
createClass(core.class.TxCollectionCUD, {
label: 'TxCollectionCUD' as IntlString,
extends: core.class.TxCUD,
kind: ClassifierKind.CLASS
})
)
txes.push(
createClass(test.mixin.TestMixin, {
label: 'TestMixin' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.MIXIN
})
)
txes.push(
createClass(test.class.TestComment, {
label: 'TestComment' as IntlString,
extends: core.class.AttachedDoc,
kind: ClassifierKind.CLASS
})
)
const u1 = 'User1' as Ref<Account>
const u2 = 'User2' as Ref<Account>
txes.push(
createDoc(core.class.Account, { email: 'user1@site.com', role: AccountRole.User }, u1),
createDoc(core.class.Account, { email: 'user2@site.com', role: AccountRole.User }, u2),
createDoc(core.class.Space, {
name: 'Sp1',
description: '',
private: false,
archived: false,
members: [u1, u2]
})
)
txes.push(
createDoc(core.class.Space, {
name: 'Sp2',
description: '',
private: false,
archived: false,
members: [u1]
})
)
txes.push(
createClass(core.class.DomainIndexConfiguration, {
label: 'DomainIndexConfiguration' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_MODEL
})
)
return txes
}
@@ -0,0 +1,328 @@
//
// Copyright © 2024 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 core, {
type Client,
type ClientConnection,
createClient,
type Doc,
type DocChunk,
type Domain,
generateId,
getWorkspaceId,
Hierarchy,
MeasureMetricsContext,
ModelDb,
type Ref,
SortingOrder,
type Space,
TxOperations
} from '@hcengineering/core'
import { type DbAdapter } from '@hcengineering/server-core'
import { createPostgresAdapter, createPostgresTxAdapter } from '..'
import { getDBClient, type PostgresClientReference, shutdown } from '../utils'
import { genMinModel } from './minmodel'
import { createTaskModel, type Task, type TaskComment, taskPlugin } from './tasks'
const txes = genMinModel()
createTaskModel(txes)
describe('postgres operations', () => {
const baseDbUri: string = process.env.DB_URL ?? 'postgresql://postgres:example@localhost:5433'
let dbId: string = 'pg_testdb_' + generateId()
let dbUri: string = baseDbUri + '/' + dbId
const clientRef: PostgresClientReference = getDBClient(baseDbUri)
let hierarchy: Hierarchy
let model: ModelDb
let client: Client
let operations: TxOperations
let serverStorage: DbAdapter
afterAll(async () => {
clientRef.close()
await shutdown()
})
beforeEach(async () => {
try {
dbId = 'pg_testdb_' + generateId()
dbUri = baseDbUri + '/' + dbId
const client = await clientRef.getClient()
await client.query(`CREATE DATABASE ${dbId}`)
} catch (err) {
console.error(err)
}
})
afterEach(async () => {
try {
// await client.close()
// await (await clientRef.getClient()).query(`DROP DATABASE ${dbId}`)
} catch (err) {
console.log(err)
}
await serverStorage?.close()
})
async function initDb (): Promise<void> {
// Remove all stuff from database.
hierarchy = new Hierarchy()
model = new ModelDb(hierarchy)
for (const t of txes) {
hierarchy.tx(t)
}
for (const t of txes) {
await model.tx(t)
}
const mctx = new MeasureMetricsContext('', {})
const txStorage = await createPostgresTxAdapter(mctx, hierarchy, dbUri, getWorkspaceId(dbId), model)
// Put all transactions to Tx
for (const t of txes) {
await txStorage.tx(mctx, t)
}
await txStorage.close()
const ctx = new MeasureMetricsContext('client', {})
const serverStorage = await createPostgresAdapter(ctx, hierarchy, dbUri, getWorkspaceId(dbId), model)
await serverStorage.init?.()
client = await createClient(async (handler) => {
const st: ClientConnection = {
isConnected: () => true,
findAll: async (_class, query, options) => await serverStorage.findAll(ctx, _class, query, options),
tx: async (tx) => await serverStorage.tx(ctx, tx),
searchFulltext: async () => ({ docs: [] }),
close: async () => {},
loadChunk: async (domain): Promise<DocChunk> => await Promise.reject(new Error('unsupported')),
closeChunk: async (idx) => {},
loadDocs: async (domain: Domain, docs: Ref<Doc>[]) => [],
upload: async (domain: Domain, docs: Doc[]) => {},
clean: async (domain: Domain, docs: Ref<Doc>[]) => {},
loadModel: async () => txes,
getAccount: async () => ({}) as any,
sendForceClose: async () => {}
}
return st
})
operations = new TxOperations(client, core.account.System)
}
beforeEach(async () => {
jest.setTimeout(30000)
await initDb()
})
it('check add', async () => {
const times: number[] = []
for (let i = 0; i < 50; i++) {
const t = Date.now()
await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: `my-task-${i}`,
description: `${i * i}`,
rate: 20 + i
})
times.push(Date.now() - t)
}
console.log('createDoc times', times)
const r = await client.findAll<Task>(taskPlugin.class.Task, {})
expect(r.length).toEqual(50)
})
it('check find by criteria', async () => {
jest.setTimeout(20000)
for (let i = 0; i < 50; i++) {
await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: `my-task-${i}`,
description: `${i * i}`,
rate: 20 + i
})
}
const r = await client.findAll<Task>(taskPlugin.class.Task, {})
expect(r.length).toEqual(50)
const first = await client.findAll<Task>(taskPlugin.class.Task, { name: 'my-task-0' })
expect(first.length).toEqual(1)
const second = await client.findAll<Task>(taskPlugin.class.Task, { name: { $like: '%0' } })
expect(second.length).toEqual(5)
const third = await client.findAll<Task>(taskPlugin.class.Task, { rate: { $in: [25, 26, 27, 28] } })
expect(third.length).toEqual(4)
})
it('check update', async () => {
await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: 'my-task',
description: 'some data ',
rate: 20,
arr: []
})
const doc = (await client.findAll<Task>(taskPlugin.class.Task, {}))[0]
await operations.updateDoc(doc._class, doc.space, doc._id, { rate: 30 })
let tasks = await client.findAll<Task>(taskPlugin.class.Task, {})
expect(tasks.length).toEqual(1)
expect(tasks[0].rate).toEqual(30)
await operations.updateDoc(doc._class, doc.space, doc._id, { $inc: { rate: 1 } })
tasks = await client.findAll<Task>(taskPlugin.class.Task, {})
expect(tasks.length).toEqual(1)
expect(tasks[0].rate).toEqual(31)
await operations.updateDoc(doc._class, doc.space, doc._id, { $inc: { rate: -1 } })
tasks = await client.findAll<Task>(taskPlugin.class.Task, {})
expect(tasks.length).toEqual(1)
expect(tasks[0].rate).toEqual(30)
await operations.updateDoc(doc._class, doc.space, doc._id, { $push: { arr: 1 } })
tasks = await client.findAll<Task>(taskPlugin.class.Task, {})
expect(tasks.length).toEqual(1)
expect(tasks[0].arr?.length).toEqual(1)
expect(tasks[0].arr?.[0]).toEqual(1)
await operations.updateDoc(doc._class, doc.space, doc._id, { $push: { arr: 3 } })
tasks = await client.findAll<Task>(taskPlugin.class.Task, {})
expect(tasks.length).toEqual(1)
expect(tasks[0].arr?.length).toEqual(2)
expect(tasks[0].arr?.[0]).toEqual(1)
expect(tasks[0].arr?.[1]).toEqual(3)
})
it('check remove', async () => {
for (let i = 0; i < 10; i++) {
await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: `my-task-${i}`,
description: `${i * i}`,
rate: 20 + i
})
}
let r = await client.findAll<Task>(taskPlugin.class.Task, {})
expect(r.length).toEqual(10)
await operations.removeDoc<Task>(taskPlugin.class.Task, '' as Ref<Space>, r[0]._id)
r = await client.findAll<Task>(taskPlugin.class.Task, {})
expect(r.length).toEqual(9)
})
it('limit and sorting', async () => {
for (let i = 0; i < 5; i++) {
await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: `my-task-${i}`,
description: `${i * i}`,
rate: 20 + i
})
}
const without = await client.findAll(taskPlugin.class.Task, {})
expect(without).toHaveLength(5)
const limit = await client.findAll(taskPlugin.class.Task, {}, { limit: 1 })
expect(limit).toHaveLength(1)
const sortAsc = await client.findAll(taskPlugin.class.Task, {}, { sort: { name: SortingOrder.Ascending } })
expect(sortAsc[0].name).toMatch('my-task-0')
const sortDesc = await client.findAll(taskPlugin.class.Task, {}, { sort: { name: SortingOrder.Descending } })
expect(sortDesc[0].name).toMatch('my-task-4')
})
it('check attached', async () => {
const docId = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: 'my-task',
description: 'Descr',
rate: 20
})
const commentId = await operations.addCollection(
taskPlugin.class.TaskComment,
'' as Ref<Space>,
docId,
taskPlugin.class.Task,
'tasks',
{
message: 'my-msg',
date: new Date()
}
)
await operations.addCollection(
taskPlugin.class.TaskComment,
'' as Ref<Space>,
docId,
taskPlugin.class.Task,
'tasks',
{
message: 'my-msg2',
date: new Date()
}
)
const r2 = await client.findAll<TaskComment>(
taskPlugin.class.TaskComment,
{},
{
lookup: {
attachedTo: taskPlugin.class.Task
}
}
)
expect(r2.length).toEqual(2)
expect((r2[0].$lookup?.attachedTo as Task)?._id).toEqual(docId)
const r3 = await client.findAll<Task>(
taskPlugin.class.Task,
{},
{
lookup: {
_id: { comment: taskPlugin.class.TaskComment }
}
}
)
expect(r3).toHaveLength(1)
expect((r3[0].$lookup as any).comment).toHaveLength(2)
const comment2Id = await operations.addCollection(
taskPlugin.class.TaskComment,
'' as Ref<Space>,
commentId,
taskPlugin.class.TaskComment,
'comments',
{
message: 'my-msg3',
date: new Date()
}
)
const r4 = await client.findAll<TaskComment>(
taskPlugin.class.TaskComment,
{
_id: comment2Id
},
{
lookup: { attachedTo: [taskPlugin.class.TaskComment, { attachedTo: taskPlugin.class.Task } as any] }
}
)
expect((r4[0].$lookup?.attachedTo as TaskComment)?._id).toEqual(commentId)
expect(((r4[0].$lookup?.attachedTo as any)?.$lookup.attachedTo as Task)?._id).toEqual(docId)
})
})
+112
View File
@@ -0,0 +1,112 @@
import {
type Account,
type AttachedDoc,
type Class,
ClassifierKind,
type Data,
type Doc,
type Domain,
type Ref,
type Space,
type Tx
} from '@hcengineering/core'
import { type IntlString, plugin, type Plugin } from '@hcengineering/platform'
import { createClass } from './minmodel'
export interface TaskComment extends AttachedDoc {
message: string
date: Date
}
export enum TaskStatus {
Open,
Close,
Resolved = 100,
InProgress
}
export enum TaskReproduce {
Always = 'always',
Rare = 'rare',
Sometimes = 'sometimes'
}
export interface Task extends Doc {
name: string
description: string
rate?: number
status?: TaskStatus
reproduce?: TaskReproduce
eta?: TaskEstimate | null
arr?: number[]
}
/**
* Define ROM and Estimated Time to arrival
*/
export interface TaskEstimate extends AttachedDoc {
rom: number // in hours
eta: number // in hours
}
export interface TaskMixin extends Task {
textValue?: string
}
export interface TaskWithSecond extends Task {
secondTask: string | null
}
const taskIds = 'taskIds' as Plugin
export const taskPlugin = plugin(taskIds, {
class: {
Task: '' as Ref<Class<Task>>,
TaskEstimate: '' as Ref<Class<TaskEstimate>>,
TaskComment: '' as Ref<Class<TaskComment>>
}
})
/**
* Create a random task with name specified
* @param name
*/
export function createTask (name: string, rate: number, description: string): Data<Task> {
return {
name,
description,
rate
}
}
export const doc1: Task = {
_id: 'd1' as Ref<Task>,
_class: taskPlugin.class.Task,
name: 'my-space',
description: 'some-value',
rate: 20,
modifiedBy: 'user' as Ref<Account>,
modifiedOn: 10,
// createdOn: 10,
space: '' as Ref<Space>
}
export function createTaskModel (txes: Tx[]): void {
txes.push(
createClass(taskPlugin.class.Task, {
kind: ClassifierKind.CLASS,
label: 'Task' as IntlString,
domain: 'test-task' as Domain
}),
createClass(taskPlugin.class.TaskEstimate, {
kind: ClassifierKind.CLASS,
label: 'Estimate' as IntlString,
domain: 'test-task' as Domain
}),
createClass(taskPlugin.class.TaskComment, {
kind: ClassifierKind.CLASS,
label: 'Comment' as IntlString,
domain: 'test-task' as Domain
})
)
}