Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 150 additions & 1 deletion foundations/core/packages/core/src/__tests__/memdb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//

import { type Client, type DomainParams, type DomainRequestOptions, type DomainResult } from '..'
import type { Class, Doc, Obj, OperationDomain, Ref } from '../classes'
import type { Class, Doc, Obj, OperationDomain, Ref, Space } from '../classes'
import core from '../component'
import { Hierarchy } from '../hierarchy'
import { ModelDb, TxDb } from '../memdb'
Expand Down Expand Up @@ -293,6 +293,155 @@ describe('memdb', () => {
expect(result2).toHaveLength(1)
})

it('check associations', async () => {
const { model } = await createModel()
const operations = new TxOperations(model, core.account.System)
const association = await operations.findOne(core.class.Association, {})
if (association == null) {
throw new Error('Association not found')
}

const spaces = await operations.findAll(core.class.Space, {})
expect(spaces).toHaveLength(2)

const first = await operations.addCollection(
test.class.TestComment,
core.space.Model,
spaces[0]._id,
spaces[0]._class,
'comments',
{
message: 'msg'
}
)

const second = await operations.addCollection(
test.class.TestComment,
core.space.Model,
first,
test.class.TestComment,
'comments',
{
message: 'msg2'
}
)

await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
docA: first,
docB: second,
association: association._id
})

const r = await operations.findAll(
test.class.TestComment,
{ _id: first },
{
associations: [[association._id, 1]]
}
)
expect(r.length).toEqual(1)
expect((r[0].$associations?.[association._id + '_b'][0] as any)?._id).toEqual(second)
})

it('check deep associations', async () => {
const { model } = await createModel()
const operations = new TxOperations(model, core.account.System)
const association = await operations.findOne(core.class.Association, {})
if (association == null) {
throw new Error('Association not found')
}

const spaces = await operations.findAll(core.class.Space, {})
expect(spaces).toHaveLength(2)

const zero = await operations.addCollection(
test.class.TestComment,
core.space.Model,
spaces[0]._id,
spaces[0]._class,
'comments',
{
message: 'msg'
}
)

const first = await operations.addCollection(
test.class.TestComment,
core.space.Model,
spaces[0]._id,
spaces[0]._class,
'comments',
{
message: 'msg'
}
)

const second = await operations.addCollection(
test.class.TestComment,
core.space.Model,
first,
test.class.TestComment,
'comments',
{
message: 'msg2'
}
)

const second2 = await operations.addCollection(
test.class.TestComment,
core.space.Model,
first,
test.class.TestComment,
'comments',
{
message: 'msg2'
}
)

const third = await operations.addCollection(
test.class.TestComment,
core.space.Model,
spaces[0]._id,
spaces[0]._class,
'comments',
{
message: 'msg3'
}
)
await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
docA: first,
docB: second,
association: association._id
})

await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
docA: first,
docB: second2,
association: association._id
})

await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
docA: second,
docB: third,
association: association._id
})

const r = await operations.findAll(
test.class.TestComment,
{ _id: { $in: [zero, first] } },
{
associations: [[association._id, 1, [[association._id, 1]]]]
}
)
expect(r.length).toEqual(2)
expect(r[1].$associations?.[`${association._id}_b`]).toHaveLength(2)
expect((r[1].$associations?.[`${association._id}_b`][0] as any)?._id).toEqual(second)
expect(r[1].$associations?.[`${association._id}_b`][1]?.$associations?.[`${association._id}_b`]).toHaveLength(0)
expect(
(r[1].$associations?.[`${association._id}_b`][0]?.$associations?.[`${association._id}_b`][0] as any)?._id
).toEqual(third)
})

it('lookups', async () => {
const { model } = await createModel()

Expand Down
30 changes: 29 additions & 1 deletion foundations/core/packages/core/src/__tests__/minmodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import type { IntlString, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import type { Arr, AttachedDoc, Class, Data, Doc, Interface, Mixin, Obj, Ref, Space } from '../classes'
import { ClassifierKind, DOMAIN_MODEL } from '../classes'
import { ClassifierKind, DOMAIN_MODEL, DOMAIN_RELATION } from '../classes'
import core from '../component'
import type { DocumentUpdate, TxCUD, TxCreateDoc, TxRemoveDoc, TxUpdateDoc } from '../tx'
import { DOMAIN_TX, TxFactory } from '../tx'
Expand Down Expand Up @@ -252,6 +252,34 @@ export function genMinModel (): TxCUD<Doc>[] {
})
)

txes.push(
createClass(core.class.Relation, {
label: 'Relation' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_RELATION
})
)

txes.push(
createClass(core.class.Association, {
label: 'Association' as IntlString,
extends: core.class.Doc,
kind: ClassifierKind.CLASS,
domain: DOMAIN_MODEL
})
)

txes.push(
createDoc(core.class.Association, {
nameA: 'my-assoc',
nameB: 'my-assoc',
classA: test.class.TestComment,
classB: test.class.TestComment,
type: '1:1'
})
)

txes.push(
createDoc(core.class.Space, {
name: 'Sp1',
Expand Down
9 changes: 6 additions & 3 deletions foundations/core/packages/core/src/memdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export abstract class MemDb extends TxProcessor implements Storage {
private async getAssociationValue<T extends Doc>(
doc: T,
associations: AssociationQuery[]
): Promise<Record<string, Doc[]>> {
): Promise<Record<string, WithLookup<Doc>[]>> {
const result: Record<string, Doc[]> = {}
for (const association of associations) {
const _id = association[0]
Expand All @@ -170,8 +170,11 @@ export abstract class MemDb extends TxProcessor implements Storage {
const key2 = !isReverse ? 'docB' : 'docA'
const _class = !isReverse ? assoc.classB : assoc.classA
const relations = await this.findAll(core.class.Relation, { association: _id, [key]: doc._id })
const objects = await this.findAll(_class, { _id: { $in: relations.map((r) => r[key2]) } })
result[_id] = objects
let objects = await this.findAll(_class, { _id: { $in: relations.map((r) => r[key2]) } })
if (association[2] !== undefined) {
objects = toFindResult(await this.fillAssociations(objects, association[2]), objects.length)
}
result[`${_id}_${!isReverse ? 'b' : 'a'}`] = objects
}
return result
}
Expand Down
4 changes: 2 additions & 2 deletions foundations/core/packages/core/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export type Projection<T extends Doc> = {
[P in keyof T]?: 0 | 1
}

export type AssociationQuery = [Ref<Association>, 1 | -1]
export type AssociationQuery = [Ref<Association>, 1 | -1] | [Ref<Association>, 1 | -1, AssociationQuery[]]

/**
* @public
Expand Down Expand Up @@ -205,7 +205,7 @@ export type LookupData<T extends Doc> = Partial<RefsAsDocs<T>>
*/
export type WithLookup<T extends Doc> = T & {
$lookup?: LookupData<T>
$associations?: Record<string, Doc[]>
$associations?: Record<string, WithLookup<Doc>[]>
$source?: {
$score: number // Score for document result
[key: string]: any
Expand Down
11 changes: 7 additions & 4 deletions foundations/core/packages/query/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,9 @@ export class LiveQuery implements WithTx, Client {
}
const docs = q.result.getDocs()
for (const doc of docs) {
const docToUpdate = doc.$associations?.[association._id]?.find((it) => it._id === tx.objectId)
const docToUpdate =
doc.$associations?.[`${association._id}_a`]?.find((it) => it._id === tx.objectId) ??
doc.$associations?.[`${association._id}_b`]?.find((it) => it._id === tx.objectId)
if (docToUpdate !== undefined) {
if (tx._class === core.class.TxMixin) {
TxProcessor.updateMixin4Doc(docToUpdate, tx as TxMixin<Doc, Doc>)
Expand Down Expand Up @@ -1128,12 +1130,13 @@ export class LiveQuery implements WithTx, Client {
_id: direct ? relation.docB : relation.docA
})
if (docToPush === undefined) return
const arr = res?.$associations?.[relation.association] ?? []
arr.push(docToPush)
if (res?.$associations === undefined) {
res.$associations = {}
}
res.$associations[relation.association] = arr
const key = direct ? 'b' : 'a'
const arr = res?.$associations?.[`${relation.association}_${key}`] ?? []
arr.push(docToPush)
res.$associations[`${relation.association}_${key}`] = arr
q.result.updateDoc(res, false)
this.queriesToUpdate.set(q.id, q)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ export function runSharedIntegrationTests (adapterName: string, getContext: () =
}
)
expect(r.length).toEqual(1)
expect(r[0].$associations?.[association._id][0]?._id).toEqual(secondTask)
expect(r[0].$associations?.[`${association._id}_b`][0]?._id).toEqual(secondTask)
})
})

Expand Down
73 changes: 72 additions & 1 deletion foundations/server/packages/mongo/src/__tests__/storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,78 @@ describe('mongo operations', () => {
}
)
expect(r.length).toEqual(1)
expect((r[0].$associations?.[association._id][0] as unknown as Task)?._id).toEqual(secondTask)
expect((r[0].$associations?.[association._id + '_b'][0] as unknown as Task)?._id).toEqual(secondTask)
})

it('check deep associations', async () => {
const association = await operations.findOne(core.class.Association, {})
if (association == null) {
throw new Error('Association not found')
}

const zeroTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: 'my-task',
description: 'Descr',
rate: 20
})

const firstTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: 'my-task',
description: 'Descr',
rate: 20
})

const secondTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: 'my-task2',
description: 'Descr',
rate: 20
})

const secondATask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: 'my-task22',
description: 'Descr',
rate: 20
})

const thirdTask = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
name: 'my-task3',
description: 'Descr',
rate: 20
})

await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
docA: firstTask,
docB: secondTask,
association: association._id
})

await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
docA: firstTask,
docB: secondATask,
association: association._id
})

await operations.createDoc(core.class.Relation, '' as Ref<Space>, {
docA: secondTask,
docB: thirdTask,
association: association._id
})

const r = await client.findAll(
taskPlugin.class.Task,
{ _id: { $in: [zeroTask, firstTask] } },
{
associations: [[association._id, 1, [[association._id, 1]]]]
}
)
expect(r.length).toEqual(2)
expect(r[1].$associations?.[`${association._id}_b`]).toHaveLength(2)
expect((r[1].$associations?.[`${association._id}_b`][0] as unknown as Task)?._id).toEqual(secondTask)
expect(r[1].$associations?.[`${association._id}_b`][1]?.$associations?.[`${association._id}_b`]).toHaveLength(0)
expect(
(r[1].$associations?.[`${association._id}_b`][0]?.$associations?.[`${association._id}_b`][0] as unknown as Task)
?._id
).toEqual(thirdTask)
})

// Run shared integration tests
Expand Down
Loading
Loading