mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-24 12:06:57 +03:00
Lookup live query (#883)
Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
parent
60a3793ae7
commit
a7c3999515
@ -11983,7 +11983,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/front.tgz:
|
||||
resolution: {integrity: sha512-RXsa4jlZB6UdPjSIAHmf07BEcWlH6N26QnAVFQ3QL5VdqLi73ohsPQV9seKz36c5jGsA//Z0BS9QYVCETuHdgA==, tarball: file:projects/front.tgz}
|
||||
resolution: {integrity: sha512-Nto3Qer5qe5YaIELZhEaJugu6x/1SbThjaKd0Yyc5BCo6UfjeyvQyKz0iHmTtIVfbBfzQSqj+MMBGL0k6zW3dg==, tarball: file:projects/front.tgz}
|
||||
name: '@rush-temp/front'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
@ -11998,6 +11998,7 @@ packages:
|
||||
'@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237
|
||||
'@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4
|
||||
cors: 2.8.5
|
||||
cross-env: 7.0.3
|
||||
esbuild: 0.12.29
|
||||
eslint: 7.32.0
|
||||
eslint-config-standard-with-typescript: 21.0.1_ce2fa0c4dfa1c256100cababd749a13a
|
||||
@ -13383,7 +13384,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/server.tgz:
|
||||
resolution: {integrity: sha512-5U67EYHkOoUqolvKMKSN1++52yfIzKPOIG2pmnbra2Ffcg9O/7UCat9eBIUx8U+n1998DnVm+c9H3yHadlOdsw==, tarball: file:projects/server.tgz}
|
||||
resolution: {integrity: sha512-b5qcL1erYSk29hFzWoJhG7GbtLdoTbEUHbScHKs7lLsQvftiiZmQRmuVcP8TVjiR3yTuMRJDt4ghH2d2UF9KIw==, tarball: file:projects/server.tgz}
|
||||
name: '@rush-temp/server'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
@ -13394,6 +13395,7 @@ packages:
|
||||
'@types/ws': 7.4.7
|
||||
'@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237
|
||||
'@typescript-eslint/parser': 5.7.0_eslint@7.32.0+typescript@4.5.4
|
||||
cross-env: 7.0.3
|
||||
elastic-apm-node: 3.26.0
|
||||
esbuild: 0.12.29
|
||||
eslint: 7.32.0
|
||||
|
@ -1,4 +1,4 @@
|
||||
import contact, { Employee, EmployeeAccount, Person } from '@anticrm/contact'
|
||||
import contact, { Channel, Employee, EmployeeAccount, Person } from '@anticrm/contact'
|
||||
import core, {
|
||||
AttachedData,
|
||||
Data,
|
||||
@ -225,7 +225,6 @@ async function genCandidate (
|
||||
const candidate: Data<Person> = {
|
||||
name: fName + ',' + lName,
|
||||
city: faker.address.city(),
|
||||
channels: [{ provider: contact.channelProvider.Email, value: faker.internet.email(fName, lName) }],
|
||||
avatar: imgId
|
||||
}
|
||||
|
||||
@ -238,10 +237,19 @@ async function genCandidate (
|
||||
|
||||
const candidateId = (options.random ? `candidate-${generateId()}-${i}` : `candidate-genid-${i}`) as Ref<Candidate>
|
||||
candidates.push(candidateId)
|
||||
const channelId = (options.random ? `channel-${generateId()}-${i}` : `channel-genid-${i}`) as Ref<Channel>
|
||||
|
||||
// Update or create candidate
|
||||
await ctx.with('find-update', {}, async () => {
|
||||
await findOrUpdate(ctx, client, recruit.space.CandidatesPublic, contact.class.Person, candidateId, candidate)
|
||||
await findOrUpdateAttached(ctx, client, recruit.space.CandidatesPublic, contact.class.Channel, channelId, {
|
||||
provider: contact.channelProvider.Email,
|
||||
value: faker.internet.email(fName, lName)
|
||||
}, {
|
||||
attachedTo: candidateId,
|
||||
attachedClass: contact.class.Person,
|
||||
collection: 'channels'
|
||||
})
|
||||
await client.updateMixin(candidateId, contact.class.Person, recruit.space.CandidatesPublic, recruit.mixin.Candidate, candidateMixin)
|
||||
})
|
||||
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import attachment, { Attachment } from '@anticrm/attachment'
|
||||
import chunter, { Comment } from '@anticrm/chunter'
|
||||
import contact, { ChannelProvider, EmployeeAccount, Person } from '@anticrm/contact'
|
||||
import contact, { Channel, ChannelProvider, EmployeeAccount, Person } from '@anticrm/contact'
|
||||
import core, { AttachedData, AttachedDoc, Class, Data, Doc, DocumentUpdate, Ref, SortingOrder, Space, TxOperations, TxResult, MixinData } from '@anticrm/core'
|
||||
import recruit from '@anticrm/model-recruit'
|
||||
import { Applicant, Candidate, Vacancy } from '@anticrm/recruit'
|
||||
@ -261,8 +261,7 @@ async function createCandidate (_name: string, pos: number, len: number, c: any,
|
||||
|
||||
const data: Data<Person> = {
|
||||
name: names.slice(1).join(' ') + ',' + names[0],
|
||||
city: get(c, _.city) ?? '',
|
||||
channels: []
|
||||
city: get(c, _.city) ?? ''
|
||||
}
|
||||
|
||||
const candidateData: MixinData<Person, Candidate> = {
|
||||
@ -277,8 +276,10 @@ async function createCandidate (_name: string, pos: number, len: number, c: any,
|
||||
].filter(p => p !== undefined && p.trim().length > 0).filter(onlyUniq).join('/')
|
||||
}
|
||||
|
||||
pushChannel(c, data, _.email, contact.channelProvider.Email)
|
||||
pushChannel(c, data, _.phone, contact.channelProvider.Phone)
|
||||
const channels: AttachedData<Channel>[] = []
|
||||
|
||||
pushChannel(c, channels, _.email, contact.channelProvider.Email)
|
||||
pushChannel(c, channels, _.phone, contact.channelProvider.Phone)
|
||||
|
||||
const commentData: string[] = []
|
||||
|
||||
@ -291,17 +292,23 @@ async function createCandidate (_name: string, pos: number, len: number, c: any,
|
||||
addComment(commentData, c, _.comment)
|
||||
|
||||
if (telegram !== undefined) {
|
||||
data.channels.push({ provider: contact.channelProvider.Telegram, value: telegram })
|
||||
channels.push({ provider: contact.channelProvider.Telegram, value: telegram })
|
||||
}
|
||||
if (linkedin !== undefined) {
|
||||
data.channels.push({ provider: contact.channelProvider.LinkedIn, value: linkedin })
|
||||
channels.push({ provider: contact.channelProvider.LinkedIn, value: linkedin })
|
||||
}
|
||||
if (github !== undefined) {
|
||||
data.channels.push({ provider: contact.channelProvider.GitHub, value: github })
|
||||
channels.push({ provider: contact.channelProvider.GitHub, value: github })
|
||||
}
|
||||
await findOrUpdate(client, recruit.space.CandidatesPublic, contact.class.Person, candId, data)
|
||||
await client.updateMixin(candId, contact.class.Person, recruit.space.CandidatesPublic, recruit.mixin.Candidate, candidateData)
|
||||
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
const element = channels[i]
|
||||
const channelId = (candId + '.channel.' + i.toString()) as Ref<Channel>
|
||||
await findOrUpdateAttached(client, recruit.space.CandidatesPublic, contact.class.Channel, channelId,
|
||||
element, { attachedTo: candId, attachedClass: recruit.mixin.Candidate, collection: 'channels' })
|
||||
}
|
||||
const commentId = (candId + '.description.comment') as Ref<Comment>
|
||||
|
||||
if (commentData.length > 0) {
|
||||
@ -349,10 +356,10 @@ function parseSocials (c: any): { sourceFields: string[], telegram: string | und
|
||||
return { sourceFields, telegram, linkedin, github }
|
||||
}
|
||||
|
||||
function pushChannel (c: any, data: Data<Candidate>, key: string, provider: Ref<ChannelProvider>): void {
|
||||
function pushChannel (c: any, channels: AttachedData<Channel>[], key: string, provider: Ref<ChannelProvider>): void {
|
||||
const value = get(c, key)
|
||||
if (value !== undefined) {
|
||||
data.channels.push({ provider, value })
|
||||
channels.push({ provider, value })
|
||||
}
|
||||
}
|
||||
export async function findOrUpdate<T extends Doc> (client: TxOperations, space: Ref<Space>, _class: Ref<Class<T>>, objectId: Ref<T>, data: Data<T>): Promise<void> {
|
||||
|
@ -22,6 +22,7 @@ import { attachmentOperation } from '@anticrm/model-attachment'
|
||||
import { leadOperation } from '@anticrm/model-lead'
|
||||
import { recruitOperation } from '@anticrm/model-recruit'
|
||||
import { viewOperation } from '@anticrm/model-view'
|
||||
import { contactOperation } from '@anticrm/model-contact'
|
||||
|
||||
export const migrateOperations: MigrateOperation[] = [
|
||||
coreOperation,
|
||||
@ -29,5 +30,6 @@ export const migrateOperations: MigrateOperation[] = [
|
||||
attachmentOperation,
|
||||
leadOperation,
|
||||
recruitOperation,
|
||||
viewOperation
|
||||
viewOperation,
|
||||
contactOperation
|
||||
]
|
||||
|
@ -13,28 +13,23 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Domain, Type, Ref } from '@anticrm/core'
|
||||
import { DOMAIN_MODEL, IndexKind } from '@anticrm/core'
|
||||
import { Builder, Model, Prop, TypeString, UX, Index, Collection, ArrOf } from '@anticrm/model'
|
||||
import type { IntlString, Asset } from '@anticrm/platform'
|
||||
import chunter from '@anticrm/model-chunter'
|
||||
import core, { TAccount, TDoc, TSpace, TType } from '@anticrm/model-core'
|
||||
import type {
|
||||
Contact,
|
||||
Person,
|
||||
Persons,
|
||||
Organization,
|
||||
Organizations,
|
||||
Employee,
|
||||
Channel,
|
||||
ChannelProvider,
|
||||
EmployeeAccount
|
||||
ChannelProvider, Contact, Employee, EmployeeAccount, Organization,
|
||||
Organizations, Person,
|
||||
Persons
|
||||
} from '@anticrm/contact'
|
||||
import workbench from '@anticrm/model-workbench'
|
||||
import view from '@anticrm/model-view'
|
||||
import type { Domain, Ref } from '@anticrm/core'
|
||||
import { DOMAIN_MODEL, IndexKind } from '@anticrm/core'
|
||||
import { Builder, Collection, Index, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
|
||||
import attachment from '@anticrm/model-attachment'
|
||||
import { ids as contact } from './plugin'
|
||||
import chunter from '@anticrm/model-chunter'
|
||||
import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@anticrm/model-core'
|
||||
import presentation from '@anticrm/model-presentation'
|
||||
import view from '@anticrm/model-view'
|
||||
import workbench from '@anticrm/model-workbench'
|
||||
import type { Asset, IntlString } from '@anticrm/platform'
|
||||
import { ids as contact } from './plugin'
|
||||
|
||||
export const DOMAIN_CONTACT = 'contact' as Domain
|
||||
|
||||
@ -45,16 +40,6 @@ export class TChannelProvider extends TDoc implements ChannelProvider {
|
||||
placeholder!: IntlString
|
||||
}
|
||||
|
||||
@Model(contact.class.TypeChannel, core.class.Type)
|
||||
export class TTypeChannels extends TType {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function TypeChannel (): Type<Channel> {
|
||||
return { _class: contact.class.TypeChannel, label: 'Channel' as IntlString }
|
||||
}
|
||||
|
||||
@Model(contact.class.Contact, core.class.Doc, DOMAIN_CONTACT)
|
||||
@UX('Contact' as IntlString, contact.icon.Person, undefined, 'name')
|
||||
export class TContact extends TDoc implements Contact {
|
||||
@ -64,8 +49,8 @@ export class TContact extends TDoc implements Contact {
|
||||
|
||||
avatar?: string
|
||||
|
||||
@Prop(ArrOf(TypeChannel()), 'Contact Info' as IntlString)
|
||||
channels!: Channel[]
|
||||
@Prop(Collection(contact.class.Channel), 'Contact Info' as IntlString)
|
||||
channels?: number
|
||||
|
||||
@Prop(Collection(attachment.class.Attachment), 'Attachments' as IntlString)
|
||||
attachments?: number
|
||||
@ -77,6 +62,16 @@ export class TContact extends TDoc implements Contact {
|
||||
city!: string
|
||||
}
|
||||
|
||||
@Model(contact.class.Channel, core.class.AttachedDoc, DOMAIN_CONTACT)
|
||||
@UX('Channel' as IntlString, contact.icon.Person)
|
||||
export class TChannel extends TAttachedDoc implements Channel {
|
||||
@Prop(TypeRef(contact.class.ChannelProvider), 'Channel provider' as IntlString)
|
||||
provider!: Ref<ChannelProvider>
|
||||
|
||||
@Prop(TypeString(), 'Value' as IntlString)
|
||||
value!: string
|
||||
}
|
||||
|
||||
@Model(contact.class.Person, contact.class.Contact)
|
||||
@UX('Person' as IntlString, contact.icon.Person, undefined, 'name')
|
||||
export class TPerson extends TContact implements Person {}
|
||||
@ -106,14 +101,14 @@ export class TPersons extends TSpace implements Persons {}
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(
|
||||
TChannelProvider,
|
||||
TTypeChannels,
|
||||
TContact,
|
||||
TPerson,
|
||||
TPersons,
|
||||
TOrganization,
|
||||
TOrganizations,
|
||||
TEmployee,
|
||||
TEmployeeAccount
|
||||
TEmployeeAccount,
|
||||
TChannel
|
||||
)
|
||||
|
||||
builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectFactory, {
|
||||
@ -140,14 +135,16 @@ export function createModel (builder: Builder): void {
|
||||
attachTo: contact.class.Contact,
|
||||
descriptor: view.viewlet.Table,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {},
|
||||
options: {
|
||||
lookup: { _id: { channels: contact.class.Channel } }
|
||||
},
|
||||
config: [
|
||||
'',
|
||||
'city',
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
|
||||
'modifiedOn',
|
||||
{ presenter: view.component.RolePresenter, label: 'Role' },
|
||||
'channels'
|
||||
'$lookup.channels'
|
||||
]
|
||||
})
|
||||
|
||||
@ -163,7 +160,7 @@ export function createModel (builder: Builder): void {
|
||||
editor: contact.component.EditOrganization
|
||||
})
|
||||
|
||||
builder.mixin(contact.class.TypeChannel, core.class.Class, view.mixin.AttributePresenter, {
|
||||
builder.mixin(contact.class.Channel, core.class.Class, view.mixin.AttributePresenter, {
|
||||
presenter: contact.component.ChannelsPresenter
|
||||
})
|
||||
|
||||
@ -255,4 +252,5 @@ export function createModel (builder: Builder): void {
|
||||
}, contact.completion.OrganizationCategory)
|
||||
}
|
||||
|
||||
export { contactOperation } from './migration'
|
||||
export { contact as default }
|
||||
|
214
models/contact/src/migration.ts
Normal file
214
models/contact/src/migration.ts
Normal file
@ -0,0 +1,214 @@
|
||||
//
|
||||
// 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 { Channel, ChannelProvider, Contact } from '@anticrm/contact'
|
||||
import { Class, DOMAIN_TX, generateId, Ref, SortingOrder, TxCreateDoc, TxCUD, TxRemoveDoc, TxUpdateDoc } from '@anticrm/core'
|
||||
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
|
||||
import core from '@anticrm/model-core'
|
||||
import contact, { DOMAIN_CONTACT } from './index'
|
||||
|
||||
function createChannel (tx: TxCUD<Contact>, channel: any): Channel {
|
||||
const doc: Channel = {
|
||||
_class: contact.class.Channel,
|
||||
_id: generateId(),
|
||||
attachedToClass: tx.objectClass,
|
||||
attachedTo: tx.objectId,
|
||||
space: tx.objectSpace,
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn,
|
||||
collection: 'channels',
|
||||
value: channel.value,
|
||||
provider: channel.provider
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
async function createTx (client: MigrationClient, tx: TxCUD<Contact>, doc: Channel): Promise<void> {
|
||||
await client.create<TxCreateDoc<Channel>>(DOMAIN_TX, {
|
||||
_class: core.class.TxCreateDoc,
|
||||
_id: generateId(),
|
||||
objectId: doc._id,
|
||||
objectSpace: doc.space,
|
||||
objectClass: doc._class,
|
||||
space: tx.space,
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn,
|
||||
attributes: {
|
||||
collection: doc.collection,
|
||||
attachedToClass: doc.attachedToClass,
|
||||
attachedTo: doc.attachedTo,
|
||||
value: doc.value,
|
||||
provider: doc.provider
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function removeTx (client: MigrationClient, tx: TxCUD<Contact>, doc: Channel): Promise<void> {
|
||||
await client.create<TxRemoveDoc<Channel>>(DOMAIN_TX, {
|
||||
_class: core.class.TxRemoveDoc,
|
||||
_id: generateId(),
|
||||
objectId: doc._id,
|
||||
objectSpace: doc.space,
|
||||
objectClass: doc._class,
|
||||
space: tx.space,
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn
|
||||
})
|
||||
}
|
||||
|
||||
async function processCreateTxes (client: MigrationClient, createTxes: TxCreateDoc<Contact>[]): Promise<Map<Ref<Contact>, Map<Ref<ChannelProvider>, Channel>>> {
|
||||
const result: Map<Ref<Contact>, Map<Ref<ChannelProvider>, Channel>> = new Map<Ref<Contact>, Map<Ref<ChannelProvider>, Channel>>()
|
||||
for (const tx of createTxes) {
|
||||
if (tx.attributes.channels == null) continue
|
||||
const { channels, ...attributes } = tx.attributes
|
||||
const current = result.get(tx.objectId)
|
||||
for (const channel of (channels as any) ?? []) {
|
||||
const doc = createChannel(tx, channel)
|
||||
if (current !== undefined) {
|
||||
current.set(channel.provider, doc)
|
||||
} else {
|
||||
const map = new Map<Ref<ChannelProvider>, Channel>()
|
||||
map.set(channel.provider, doc)
|
||||
result.set(tx.objectId, map)
|
||||
}
|
||||
await createTx(client, tx, doc)
|
||||
}
|
||||
|
||||
await client.update<TxCreateDoc<Contact>>(DOMAIN_TX, { _id: tx._id }, {
|
||||
attributes: attributes
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function processRemoveTxes (client: MigrationClient, txes: TxRemoveDoc<Contact>[], result: Map<Ref<Contact>, Map<Ref<ChannelProvider>, Channel>>): Promise<Map<Ref<Contact>, Map<Ref<ChannelProvider>, Channel>>> {
|
||||
for (const tx of txes) {
|
||||
const current = result.get(tx.objectId)
|
||||
if (current != null) {
|
||||
for (const provider of current.keys()) {
|
||||
const doc = current.get(provider)
|
||||
if (doc !== undefined) {
|
||||
await removeTx(client, tx, doc)
|
||||
current.delete(provider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export async function migrateContactChannels (client: MigrationClient, classes: Ref<Class<Contact>>[]): Promise<void> {
|
||||
const objectIds: Ref<Contact>[] = []
|
||||
const contacts = await client.find<Contact>(DOMAIN_CONTACT, { _class: { $in: classes } })
|
||||
for (const doc of contacts) {
|
||||
const obj = doc as any
|
||||
if (obj.channels != null && Array.isArray(obj.channels)) {
|
||||
objectIds.push(doc._id)
|
||||
}
|
||||
}
|
||||
|
||||
const createTxes = await client.find<TxCreateDoc<Contact>>(DOMAIN_TX, {
|
||||
_class: core.class.TxCreateDoc,
|
||||
objectId: { $in: objectIds }
|
||||
})
|
||||
const objectChannels = await processCreateTxes(client, createTxes)
|
||||
|
||||
const updateTxes = await client.find<TxUpdateDoc<Contact>>(DOMAIN_TX, {
|
||||
_class: core.class.TxUpdateDoc,
|
||||
objectId: { $in: objectIds }
|
||||
}, { sort: { modifiedOn: SortingOrder.Ascending } })
|
||||
for (const tx of updateTxes) {
|
||||
if (tx.operations.channels === undefined) continue
|
||||
const { channels, ...operations } = tx.operations
|
||||
const current = objectChannels.get(tx.objectId)
|
||||
if (current !== undefined) {
|
||||
const providers = new Set<Ref<ChannelProvider>>(current.keys())
|
||||
for (const channel of (channels as any) ?? []) {
|
||||
const doc = current.get(channel.provider)
|
||||
if (doc !== undefined) {
|
||||
providers.delete(doc.provider)
|
||||
doc.value = channel.value
|
||||
await client.create<TxUpdateDoc<Channel>>(DOMAIN_TX, {
|
||||
_class: core.class.TxUpdateDoc,
|
||||
_id: generateId(),
|
||||
objectId: doc._id,
|
||||
objectSpace: doc.space,
|
||||
objectClass: doc._class,
|
||||
space: tx.space,
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn,
|
||||
operations: {
|
||||
value: doc.value
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const doc = createChannel(tx, channel)
|
||||
current.set(channel.provider, doc)
|
||||
await createTx(client, tx, doc)
|
||||
}
|
||||
}
|
||||
|
||||
for (const provider of providers.keys()) {
|
||||
const doc = current.get(provider)
|
||||
if (doc !== undefined) {
|
||||
await removeTx(client, tx, doc)
|
||||
current.delete(provider)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const channel of (channels as any) ?? []) {
|
||||
const doc = createChannel(tx, channel)
|
||||
const map = new Map<Ref<ChannelProvider>, Channel>()
|
||||
map.set(channel.provider, doc)
|
||||
objectChannels.set(tx.objectId, map)
|
||||
}
|
||||
}
|
||||
if (Object.keys(operations).length > 0) {
|
||||
await client.update<TxUpdateDoc<Contact>>(DOMAIN_TX, { _id: tx._id }, {
|
||||
operations: operations
|
||||
})
|
||||
} else {
|
||||
await client.delete<TxUpdateDoc<Contact>>(DOMAIN_TX, tx._id)
|
||||
}
|
||||
}
|
||||
|
||||
const removeTxes = await client.find<TxRemoveDoc<Contact>>(DOMAIN_TX, {
|
||||
_class: core.class.TxRemoveDoc,
|
||||
objectId: { $in: objectIds }
|
||||
})
|
||||
|
||||
const result = await processRemoveTxes(client, removeTxes, objectChannels)
|
||||
for (const contact of result.values()) {
|
||||
for (const channel of contact.values()) {
|
||||
await client.create(DOMAIN_CONTACT, channel)
|
||||
}
|
||||
}
|
||||
for (const id of objectIds) {
|
||||
const channels = result.get(id)?.size ?? 0
|
||||
await client.update(DOMAIN_CONTACT, { _id: id }, {
|
||||
channels: channels
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const contactOperation: MigrateOperation = {
|
||||
async migrate (client: MigrationClient): Promise<void> {
|
||||
const classes = [contact.class.Contact, contact.class.Person, contact.class.Employee, contact.class.Organization]
|
||||
await migrateContactChannels(client, classes)
|
||||
},
|
||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {
|
||||
}
|
||||
}
|
@ -14,9 +14,8 @@
|
||||
//
|
||||
|
||||
import { IntlString, mergeIds, Resource } from '@anticrm/platform'
|
||||
import type { Ref, Class, Type } from '@anticrm/core'
|
||||
import type { Ref } from '@anticrm/core'
|
||||
import contact, { contactId } from '@anticrm/contact'
|
||||
import type { Channel } from '@anticrm/contact'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
import {} from '@anticrm/core'
|
||||
import { Application } from '@anticrm/workbench'
|
||||
@ -51,9 +50,6 @@ export const ids = mergeIds(contactId, contact, {
|
||||
SearchPerson: '' as IntlString,
|
||||
SearchOrganization: '' as IntlString
|
||||
},
|
||||
class: {
|
||||
TypeChannel: '' as Ref<Class<Type<Channel>>>
|
||||
},
|
||||
completion: {
|
||||
PersonQuery: '' as Resource<ObjectSearchFactory>,
|
||||
EmployeeQuery: '' as Resource<ObjectSearchFactory>,
|
||||
|
@ -33,8 +33,7 @@ export async function createDeps (client: Client): Promise<void> {
|
||||
contact.space.Employee,
|
||||
{
|
||||
name: 'Chen,Rosamund',
|
||||
city: 'Mountain View',
|
||||
channels: []
|
||||
city: 'Mountain View'
|
||||
},
|
||||
account.employee
|
||||
)
|
||||
@ -44,15 +43,13 @@ export async function createDeps (client: Client): Promise<void> {
|
||||
recruit.space.CandidatesPublic,
|
||||
{
|
||||
name: 'P.,Andrey',
|
||||
city: 'Monte Carlo',
|
||||
channels: [
|
||||
{
|
||||
provider: contact.channelProvider.Email,
|
||||
value: 'andrey@hc.engineering'
|
||||
}
|
||||
]
|
||||
city: 'Monte Carlo'
|
||||
}
|
||||
)
|
||||
await tx.addCollection(contact.class.Channel, recruit.space.CandidatesPublic, u1, contact.class.Person, 'channels', {
|
||||
provider: contact.channelProvider.Email,
|
||||
value: 'andrey@hc.engineering'
|
||||
})
|
||||
|
||||
await tx.createMixin(u1, contact.class.Person, recruit.space.CandidatesPublic, recruit.mixin.Candidate, {
|
||||
title: 'Chief Architect'
|
||||
@ -63,15 +60,13 @@ export async function createDeps (client: Client): Promise<void> {
|
||||
recruit.space.CandidatesPublic,
|
||||
{
|
||||
name: 'M.,Marina',
|
||||
city: 'Los Angeles',
|
||||
channels: [
|
||||
{
|
||||
provider: contact.channelProvider.Email,
|
||||
value: 'marina@hc.engineering'
|
||||
}
|
||||
]
|
||||
city: 'Los Angeles'
|
||||
}
|
||||
)
|
||||
await tx.addCollection(contact.class.Channel, recruit.space.CandidatesPublic, u2, contact.class.Person, 'channels', {
|
||||
provider: contact.channelProvider.Email,
|
||||
value: 'marina@hc.engineering'
|
||||
})
|
||||
await tx.createMixin(u2, contact.class.Person, recruit.space.CandidatesPublic, recruit.mixin.Candidate, {
|
||||
title: 'Chief Designer'
|
||||
})
|
||||
@ -81,15 +76,13 @@ export async function createDeps (client: Client): Promise<void> {
|
||||
recruit.space.CandidatesPublic,
|
||||
{
|
||||
name: 'P.,Alex',
|
||||
city: 'Krasnodar, Russia',
|
||||
channels: [
|
||||
{
|
||||
provider: contact.channelProvider.Email,
|
||||
value: 'alex@hc.engineering'
|
||||
}
|
||||
]
|
||||
city: 'Krasnodar, Russia'
|
||||
}
|
||||
)
|
||||
await tx.addCollection(contact.class.Channel, recruit.space.CandidatesPublic, u3, contact.class.Person, 'channels', {
|
||||
provider: contact.channelProvider.Email,
|
||||
value: 'alex@hc.engineering'
|
||||
})
|
||||
await tx.createMixin(u3, contact.class.Person, recruit.space.CandidatesPublic, recruit.mixin.Candidate, {
|
||||
title: 'Frontend Engineer'
|
||||
})
|
||||
|
@ -14,17 +14,16 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// To help typescript locate view plugin properly
|
||||
import type { Category, Product, Variant } from '@anticrm/inventory'
|
||||
import { Doc, Domain, FindOptions, Ref } from '@anticrm/core'
|
||||
import type { Category, Product, Variant } from '@anticrm/inventory'
|
||||
import { Builder, Collection, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
|
||||
import core, { TAttachedDoc } from '@anticrm/model-core'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import type {} from '@anticrm/view'
|
||||
import inventory from './plugin'
|
||||
import workbench from '@anticrm/model-workbench'
|
||||
import view from '@anticrm/view'
|
||||
import attachment from '@anticrm/model-attachment'
|
||||
import core, { TAttachedDoc } from '@anticrm/model-core'
|
||||
import workbench from '@anticrm/model-workbench'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import type { } from '@anticrm/view'
|
||||
import view from '@anticrm/view'
|
||||
import inventory from './plugin'
|
||||
|
||||
export const DOMAIN_INVENTORY = 'inventory' as Domain
|
||||
@Model(inventory.class.Category, core.class.AttachedDoc, DOMAIN_INVENTORY)
|
||||
@ -96,9 +95,7 @@ export function createModel (builder: Builder): void {
|
||||
descriptor: view.viewlet.Table,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: {
|
||||
attachedTo: inventory.class.Category
|
||||
}
|
||||
lookup: { attachedTo: inventory.class.Category }
|
||||
} as FindOptions<Doc>,
|
||||
config: ['', '$lookup.attachedTo', 'modifiedOn']
|
||||
})
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
// To help typescript locate view plugin properly
|
||||
import type { Employee } from '@anticrm/contact'
|
||||
import type { Doc, FindOptions, Ref } from '@anticrm/core'
|
||||
import type { Doc, FindOptions, Lookup, Ref } from '@anticrm/core'
|
||||
import type { Customer, Funnel, Lead } from '@anticrm/lead'
|
||||
import { Builder, Collection, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
|
||||
import attachment from '@anticrm/model-attachment'
|
||||
@ -115,15 +115,33 @@ export function createModel (builder: Builder): void {
|
||||
lead.space.DefaultFunnel
|
||||
)
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: lead.mixin.Customer,
|
||||
descriptor: view.viewlet.Table,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: { _id: { channels: contact.class.Channel } } as any
|
||||
} as FindOptions<Doc>, // TODO: fix
|
||||
config: [
|
||||
'',
|
||||
{ key: 'leads', presenter: lead.component.LeadsPresenter, label: lead.string.Leads },
|
||||
'modifiedOn',
|
||||
'$lookup.channels'
|
||||
]
|
||||
})
|
||||
|
||||
const leadLookup: Lookup<Lead> =
|
||||
{
|
||||
attachedTo: [contact.class.Contact, { _id: { channels: contact.class.Channel } }],
|
||||
state: task.class.State
|
||||
}
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: lead.class.Lead,
|
||||
descriptor: view.viewlet.Table,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: {
|
||||
attachedTo: contact.class.Contact,
|
||||
state: task.class.State
|
||||
}
|
||||
lookup: leadLookup
|
||||
} as FindOptions<Doc>, // TODO: fix
|
||||
config: [
|
||||
'',
|
||||
@ -132,7 +150,7 @@ export function createModel (builder: Builder): void {
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments', sortingKey: 'comments' },
|
||||
'modifiedOn',
|
||||
'$lookup.attachedTo.channels'
|
||||
'$lookup.attachedTo.$lookup.channels'
|
||||
]
|
||||
})
|
||||
|
||||
@ -141,10 +159,7 @@ export function createModel (builder: Builder): void {
|
||||
descriptor: task.viewlet.Kanban,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: {
|
||||
customer: contact.class.Contact,
|
||||
state: task.class.State
|
||||
}
|
||||
lookup: leadLookup
|
||||
} as FindOptions<Doc>, // TODO: fix
|
||||
config: ['$lookup.customer', '$lookup.state']
|
||||
})
|
||||
|
@ -14,7 +14,7 @@
|
||||
//
|
||||
|
||||
import type { Employee } from '@anticrm/contact'
|
||||
import { Doc, FindOptions, Ref, Timestamp } from '@anticrm/core'
|
||||
import { Doc, FindOptions, Lookup, Ref, Timestamp } from '@anticrm/core'
|
||||
import { Builder, Collection, Mixin, Model, Prop, TypeBoolean, TypeDate, TypeRef, TypeString, UX } from '@anticrm/model'
|
||||
import attachment from '@anticrm/model-attachment'
|
||||
import chunter from '@anticrm/model-chunter'
|
||||
@ -153,9 +153,7 @@ export function createModel (builder: Builder): void {
|
||||
descriptor: view.viewlet.Table,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
// lookup: {
|
||||
// resume: chunter.class.Attachment
|
||||
// }
|
||||
lookup: { _id: { channels: contact.class.Channel } }
|
||||
} as FindOptions<Doc>, // TODO: fix
|
||||
config: [
|
||||
'',
|
||||
@ -165,21 +163,24 @@ export function createModel (builder: Builder): void {
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments', sortingKey: 'comments' },
|
||||
'modifiedOn',
|
||||
'channels'
|
||||
'$lookup.channels'
|
||||
]
|
||||
})
|
||||
|
||||
const applicantTableLookup: Lookup<Applicant> =
|
||||
{
|
||||
attachedTo: [recruit.mixin.Candidate, { _id: { channels: contact.class.Channel } }],
|
||||
state: task.class.State,
|
||||
assignee: contact.class.Employee,
|
||||
doneState: task.class.DoneState
|
||||
}
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: recruit.class.Applicant,
|
||||
descriptor: view.viewlet.Table,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: {
|
||||
attachedTo: recruit.mixin.Candidate,
|
||||
state: task.class.State,
|
||||
assignee: contact.class.Employee,
|
||||
doneState: task.class.DoneState
|
||||
}
|
||||
lookup: applicantTableLookup
|
||||
} as FindOptions<Doc>, // TODO: fix
|
||||
config: [
|
||||
'',
|
||||
@ -190,21 +191,24 @@ export function createModel (builder: Builder): void {
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments', sortingKey: 'comments' },
|
||||
'modifiedOn',
|
||||
'$lookup.attachedTo.channels'
|
||||
'$lookup.attachedTo.$lookup.channels'
|
||||
]
|
||||
})
|
||||
|
||||
const applicantKanbanLookup: Lookup<Applicant> =
|
||||
{
|
||||
attachedTo: [recruit.mixin.Candidate, { _id: { channels: contact.class.Channel } }],
|
||||
state: task.class.State
|
||||
}
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: recruit.class.Applicant,
|
||||
descriptor: task.viewlet.Kanban,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: {
|
||||
attachedTo: recruit.mixin.Candidate,
|
||||
state: task.class.State
|
||||
}
|
||||
lookup: applicantKanbanLookup
|
||||
} as FindOptions<Doc>, // TODO: fix
|
||||
config: ['$lookup.attachedTo', '$lookup.state', '$lookup.attachedTo.city', '$lookup.attachedTo.channels']
|
||||
config: ['$lookup.attachedTo', '$lookup.state', '$lookup.attachedTo.city', '$lookup.attachedTo.$lookup.channels']
|
||||
})
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
@ -212,12 +216,7 @@ export function createModel (builder: Builder): void {
|
||||
descriptor: task.viewlet.StatusTable,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: {
|
||||
attachedTo: recruit.mixin.Candidate,
|
||||
state: task.class.State,
|
||||
assignee: contact.class.Employee,
|
||||
doneState: task.class.DoneState
|
||||
}
|
||||
lookup: applicantTableLookup
|
||||
} as FindOptions<Doc>, // TODO: fix
|
||||
config: [
|
||||
'',
|
||||
@ -228,7 +227,7 @@ export function createModel (builder: Builder): void {
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments', sortingKey: 'comments' },
|
||||
'modifiedOn',
|
||||
'$lookup.attachedTo.channels'
|
||||
'$lookup.attachedTo.$lookup.channels'
|
||||
]
|
||||
})
|
||||
|
||||
|
@ -13,7 +13,6 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// To help typescript locate view plugin properly
|
||||
import type { Employee } from '@anticrm/contact'
|
||||
import contact from '@anticrm/contact'
|
||||
import { Arr, Class, Doc, Domain, DOMAIN_MODEL, FindOptions, Ref, Space, Timestamp } from '@anticrm/core'
|
||||
@ -281,9 +280,7 @@ export function createModel (builder: Builder): void {
|
||||
descriptor: view.viewlet.Table,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: {
|
||||
assignee: contact.class.Employee
|
||||
}
|
||||
lookup: { assignee: contact.class.Employee }
|
||||
} as FindOptions<Doc>,
|
||||
config: [
|
||||
'',
|
||||
@ -313,9 +310,8 @@ export function createModel (builder: Builder): void {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: {
|
||||
assignee: contact.class.Employee,
|
||||
state: task.class.State
|
||||
// attachedTo: core.class.Doc
|
||||
state: task.class.State,
|
||||
assignee: contact.class.Employee
|
||||
}
|
||||
} as FindOptions<Doc>, // TODO: fix
|
||||
config: [
|
||||
|
@ -252,4 +252,33 @@ describe('memdb', () => {
|
||||
const result2 = await client.findAll(test.class.TestComment, {})
|
||||
expect(result2).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('lookups', async () => {
|
||||
const { model } = await createModel()
|
||||
|
||||
const client = new TxOperations(model, core.account.System)
|
||||
const spaces = await client.findAll(core.class.Space, {})
|
||||
expect(spaces).toHaveLength(2)
|
||||
|
||||
const first = await client.addCollection(test.class.TestComment, core.space.Model, spaces[0]._id, spaces[0]._class, 'comments', {
|
||||
message: 'msg'
|
||||
})
|
||||
|
||||
const second = await client.addCollection(test.class.TestComment, core.space.Model, first, test.class.TestComment, 'comments', {
|
||||
message: 'msg2'
|
||||
})
|
||||
|
||||
await client.addCollection(test.class.TestComment, core.space.Model, spaces[0]._id, spaces[0]._class, 'comments', {
|
||||
message: 'msg3'
|
||||
})
|
||||
|
||||
const simple = await client.findAll(test.class.TestComment, { _id: first }, { lookup: { attachedTo: spaces[0]._class } })
|
||||
expect(simple[0].$lookup?.attachedTo).toEqual(spaces[0])
|
||||
|
||||
const nested = await client.findAll(test.class.TestComment, { _id: second }, { lookup: { attachedTo: [test.class.TestComment, { attachedTo: spaces[0]._class } as any] } })
|
||||
expect((nested[0].$lookup?.attachedTo as any).$lookup?.attachedTo).toEqual(spaces[0])
|
||||
|
||||
const reverse = await client.findAll(spaces[0]._class, { _id: spaces[0]._id }, { lookup: { _id: { comments: test.class.TestComment } } })
|
||||
expect((reverse[0].$lookup as any).comments).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
@ -21,6 +21,7 @@ export * from './hierarchy'
|
||||
export * from './memdb'
|
||||
export * from './client'
|
||||
export * from './operator'
|
||||
export * from './objvalue'
|
||||
export * from './query'
|
||||
export * from './server'
|
||||
export * from './measurements'
|
||||
|
@ -15,11 +15,12 @@
|
||||
|
||||
import { PlatformError, Severity, Status } from '@anticrm/platform'
|
||||
import clone from 'just-clone'
|
||||
import { Lookup, ReverseLookups } from '.'
|
||||
import type { Class, Doc, Ref } from './classes'
|
||||
import core from './component'
|
||||
import { Hierarchy } from './hierarchy'
|
||||
import { matchQuery, resultSort } from './query'
|
||||
import type { DocumentQuery, FindOptions, FindResult, LookupData, Refs, Storage, TxResult, WithLookup } from './storage'
|
||||
import type { DocumentQuery, FindOptions, FindResult, LookupData, Storage, TxResult, WithLookup } from './storage'
|
||||
import type { Tx, TxCreateDoc, TxMixin, TxPutBag, TxRemoveDoc, TxUpdateDoc } from './tx'
|
||||
import { TxProcessor } from './tx'
|
||||
|
||||
@ -76,16 +77,43 @@ export abstract class MemDb extends TxProcessor {
|
||||
return doc as T
|
||||
}
|
||||
|
||||
private lookup<T extends Doc>(docs: T[], lookup: Refs<T>): WithLookup<T>[] {
|
||||
private async getLookupValue<T extends Doc> (doc: T, lookup: Lookup<T>, result: LookupData<T>): Promise<void> {
|
||||
for (const key in lookup) {
|
||||
if (key === '_id') {
|
||||
await this.getReverseLookupValue(doc, lookup, result)
|
||||
continue
|
||||
}
|
||||
const value = (lookup as any)[key]
|
||||
if (Array.isArray(value)) {
|
||||
const [_class, nested] = value
|
||||
const objects = await this.findAll(_class, { _id: (doc as any)[key] })
|
||||
;(result as any)[key] = objects[0]
|
||||
const nestedResult = {}
|
||||
const parent = (result as any)[key]
|
||||
await this.getLookupValue(parent, nested, nestedResult)
|
||||
Object.assign(parent, {
|
||||
$lookup: nestedResult
|
||||
})
|
||||
} else {
|
||||
const objects = await this.findAll(value, { _id: (doc as any)[key] })
|
||||
;(result as any)[key] = objects[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getReverseLookupValue<T extends Doc> (doc: T, lookup: ReverseLookups, result: LookupData<T>): Promise<void> {
|
||||
for (const key in lookup._id) {
|
||||
const value = lookup._id[key]
|
||||
const objects = await this.findAll(value, { attachedTo: doc._id })
|
||||
;(result as any)[key] = objects
|
||||
}
|
||||
}
|
||||
|
||||
private async lookup<T extends Doc>(docs: T[], lookup: Lookup<T>): Promise<WithLookup<T>[]> {
|
||||
const withLookup: WithLookup<T>[] = []
|
||||
for (const doc of docs) {
|
||||
const result: LookupData<T> = {}
|
||||
for (const key in lookup) {
|
||||
const id = (doc as any)[key] as Ref<Doc>
|
||||
if (id != null) {
|
||||
(result as any)[key] = this.getObject(id)
|
||||
}
|
||||
}
|
||||
await this.getLookupValue(doc, lookup, result)
|
||||
withLookup.push(Object.assign({}, doc, { $lookup: result }))
|
||||
}
|
||||
return withLookup
|
||||
@ -114,7 +142,7 @@ export abstract class MemDb extends TxProcessor {
|
||||
result = result.filter(r => (r as any)[_class] !== undefined)
|
||||
}
|
||||
|
||||
if (options?.lookup !== undefined) result = this.lookup(result as T[], options.lookup)
|
||||
if (options?.lookup !== undefined) result = await this.lookup(result as T[], options.lookup)
|
||||
|
||||
if (options?.sort !== undefined) resultSort(result, options?.sort)
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
//
|
||||
|
||||
import type { KeysByType } from 'simplytyped'
|
||||
import type { Class, Doc, Ref } from './classes'
|
||||
import type { AttachedDoc, Class, Doc, Ref } from './classes'
|
||||
import type { Tx } from './tx'
|
||||
|
||||
/**
|
||||
@ -49,13 +49,42 @@ export type DocumentQuery<T extends Doc> = {
|
||||
* @public
|
||||
*/
|
||||
export type ToClassRef<T extends object> = {
|
||||
[P in keyof T]?: T[P] extends Ref<infer X> ? Ref<Class<X>> : never
|
||||
[P in keyof T]?: T[P] extends Ref<infer X> | null ? Ref<Class<X>> | [Ref<Class<X>>, Lookup<X>] : never
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type Refs<T extends Doc> = ToClassRef<Pick<T, KeysByType<T, Ref<Doc>>>>
|
||||
export type RefKeys<T extends Doc> = Pick<T, KeysByType<T, NullableRef>>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type NullableRef = Ref<Doc> | null
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type Refs<T extends Doc> = ToClassRef<RefKeys<T>>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ReverseLookups {
|
||||
_id?: ReverseLookup
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ReverseLookup {
|
||||
[key: string]: Ref<Class<AttachedDoc>>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type Lookup<T extends Doc> = Refs<T> | ReverseLookups | (Refs<T> & ReverseLookups)
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -64,7 +93,7 @@ export type Refs<T extends Doc> = ToClassRef<Pick<T, KeysByType<T, Ref<Doc>>>>
|
||||
export type FindOptions<T extends Doc> = {
|
||||
limit?: number
|
||||
sort?: SortingQuery<T>
|
||||
lookup?: Refs<T>
|
||||
lookup?: Lookup<T>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,8 +114,8 @@ export enum SortingOrder {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type RefsAsDocs<T extends Doc> = {
|
||||
[P in keyof T]: T[P] extends Ref<infer X> ? X : never
|
||||
export type RefsAsDocs<T> = {
|
||||
[P in keyof T]: T[P] extends Ref<infer X> ? (T extends X ? X : X | WithLookup<X>) : never
|
||||
}
|
||||
|
||||
/**
|
||||
@ -97,7 +126,9 @@ export type RemoveNever<T extends object> = Omit<T, KeysByType<T, never>>
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type LookupData<T extends Doc> = Partial<RemoveNever<RefsAsDocs<T>>>
|
||||
export type LookupData<T extends Doc> = Partial<RemoveNever<RefsAsDocs<T>>> | RemoveNever<{
|
||||
[key: string]: Doc[]
|
||||
}>
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Account, Arr, Class, Data, Doc, Mixin, Obj, Ref, TxCreateDoc, TxCUD } from '@anticrm/core'
|
||||
import type { Account, Arr, Class, Data, Doc, Domain, Mixin, Obj, Ref, TxCreateDoc, TxCUD } from '@anticrm/core'
|
||||
import core, { AttachedDoc, ClassifierKind, DOMAIN_MODEL, DOMAIN_TX, TxFactory } from '@anticrm/core'
|
||||
import type { IntlString, Plugin } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
@ -62,6 +62,8 @@ export const test = plugin('test' as Plugin, {
|
||||
}
|
||||
})
|
||||
|
||||
const DOMAIN_TEST: Domain = 'test' as Domain
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Generate minimal model for testing purposes.
|
||||
@ -86,7 +88,7 @@ export function genMinModel (): TxCUD<Doc>[] {
|
||||
|
||||
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 }))
|
||||
txes.push(createClass(test.class.TestComment, { label: 'TestComment' as IntlString, extends: core.class.AttachedDoc, kind: ClassifierKind.CLASS, domain: DOMAIN_TEST }))
|
||||
|
||||
const u1 = 'User1' as Ref<Account>
|
||||
const u2 = 'User2' as Ref<Account>
|
||||
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import core, { createClient, Doc, SortingOrder, Space, Tx, TxCreateDoc, TxOperations } from '@anticrm/core'
|
||||
import { genMinModel } from './minmodel'
|
||||
import core, { createClient, Doc, generateId, Ref, SortingOrder, Space, Tx, TxCreateDoc, TxOperations, WithLookup } from '@anticrm/core'
|
||||
import { AttachedComment, test, genMinModel } from './minmodel'
|
||||
import { LiveQuery } from '..'
|
||||
import { connect } from './connection'
|
||||
|
||||
@ -71,7 +71,6 @@ describe('query', () => {
|
||||
let attempt = 0
|
||||
const pp = new Promise((resolve) => {
|
||||
liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
|
||||
console.log('query result attempt', result, attempt)
|
||||
expect(result).toHaveLength(expectedLength + attempt)
|
||||
if (attempt > 0) {
|
||||
expect((result[expectedLength + attempt - 1] as any).x).toBe(attempt)
|
||||
@ -213,20 +212,20 @@ describe('query', () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
|
||||
const limit = 1
|
||||
let attempt = -1
|
||||
let doneCount = 0
|
||||
let attempt = 0
|
||||
let descAttempt = 0
|
||||
|
||||
const pp1 = new Promise((resolve) => {
|
||||
liveQuery.query<Space>(
|
||||
core.class.Space,
|
||||
{ private: true },
|
||||
(result) => {
|
||||
if (attempt === 0 && result.length > 0) {
|
||||
if (result.length > 0) {
|
||||
expect(result.length).toEqual(limit)
|
||||
expect(result[0].name).toMatch('0')
|
||||
attempt++
|
||||
}
|
||||
if (attempt === 0) doneCount++
|
||||
if (doneCount === 2) resolve(null)
|
||||
if (attempt === 1) resolve(null)
|
||||
},
|
||||
{ limit: limit, sort: { name: SortingOrder.Ascending } }
|
||||
)
|
||||
@ -237,19 +236,18 @@ describe('query', () => {
|
||||
core.class.Space,
|
||||
{ private: true },
|
||||
(result) => {
|
||||
if (attempt > 0 && result.length > 0) {
|
||||
if (result.length > 0) {
|
||||
expect(result.length).toEqual(limit)
|
||||
expect(result[0].name).toMatch(attempt.toString())
|
||||
expect(result[0].name).toMatch(descAttempt.toString())
|
||||
descAttempt++
|
||||
}
|
||||
if (attempt === 9) doneCount++
|
||||
if (doneCount === 2) resolve(null)
|
||||
if (descAttempt === 10) resolve(null)
|
||||
},
|
||||
{ limit: limit, sort: { name: SortingOrder.Descending } }
|
||||
)
|
||||
})
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
attempt = i
|
||||
await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
private: true,
|
||||
name: i.toString(),
|
||||
@ -362,6 +360,364 @@ describe('query', () => {
|
||||
await pp
|
||||
})
|
||||
|
||||
it('lookup query add doc', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
const futureSpace: Space = {
|
||||
_id: generateId(),
|
||||
_class: core.class.Space,
|
||||
private: false,
|
||||
members: [],
|
||||
space: core.space.Model,
|
||||
name: 'new space',
|
||||
description: '',
|
||||
archived: false,
|
||||
modifiedBy: core.account.System,
|
||||
modifiedOn: 0
|
||||
}
|
||||
const comment = await factory.addCollection(test.class.TestComment, futureSpace._id, futureSpace._id, core.class.Space, 'comments', {
|
||||
message: 'test'
|
||||
})
|
||||
let attempt = 0
|
||||
const pp = new Promise((resolve) => {
|
||||
liveQuery.query<AttachedComment>(
|
||||
test.class.TestComment,
|
||||
{ _id: comment },
|
||||
(result) => {
|
||||
const comment = result[0]
|
||||
if (comment !== undefined) {
|
||||
if (attempt > 0) {
|
||||
expect((comment as WithLookup<AttachedComment>).$lookup?.space).toEqual(futureSpace)
|
||||
resolve(null)
|
||||
} else {
|
||||
expect((comment as WithLookup<AttachedComment>).$lookup?.space).toBeUndefined()
|
||||
attempt++
|
||||
}
|
||||
}
|
||||
},
|
||||
{ lookup: { space: core.class.Space } }
|
||||
)
|
||||
})
|
||||
|
||||
await factory.createDoc(core.class.Space, futureSpace.space, {
|
||||
...futureSpace
|
||||
}, futureSpace._id)
|
||||
await pp
|
||||
})
|
||||
|
||||
it('lookup nested query add doc', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
const futureSpace: Space = {
|
||||
_id: generateId(),
|
||||
_class: core.class.Space,
|
||||
private: false,
|
||||
members: [],
|
||||
space: core.space.Model,
|
||||
name: 'new space',
|
||||
description: '',
|
||||
archived: false,
|
||||
modifiedBy: core.account.System,
|
||||
modifiedOn: 0
|
||||
}
|
||||
const comment = await factory.addCollection(test.class.TestComment, futureSpace._id, futureSpace._id, core.class.Space, 'comments', {
|
||||
message: 'test'
|
||||
})
|
||||
const childComment = await factory.addCollection(test.class.TestComment, futureSpace._id, comment, test.class.TestComment, 'comments', {
|
||||
message: 'child'
|
||||
})
|
||||
let attempt = 0
|
||||
const pp = new Promise((resolve) => {
|
||||
liveQuery.query<AttachedComment>(
|
||||
test.class.TestComment,
|
||||
{ _id: childComment },
|
||||
(result) => {
|
||||
const comment = result[0]
|
||||
if (comment !== undefined) {
|
||||
if (attempt > 0) {
|
||||
expect(((comment as WithLookup<AttachedComment>).$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toEqual(futureSpace)
|
||||
resolve(null)
|
||||
} else {
|
||||
expect(((comment as WithLookup<AttachedComment>).$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
|
||||
attempt++
|
||||
}
|
||||
}
|
||||
},
|
||||
{ lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } }
|
||||
)
|
||||
})
|
||||
|
||||
await factory.createDoc(core.class.Space, futureSpace.space, {
|
||||
...futureSpace
|
||||
}, futureSpace._id)
|
||||
await pp
|
||||
})
|
||||
|
||||
it('lookup reverse query add doc', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
const spaces = await liveQuery.findAll(core.class.Space, {})
|
||||
const parentComment = await factory.addCollection(test.class.TestComment, spaces[0]._id, spaces[0]._id, spaces[0]._class, 'comments', {
|
||||
message: 'test'
|
||||
})
|
||||
let attempt = 0
|
||||
const childLength = 3
|
||||
const pp = new Promise((resolve) => {
|
||||
liveQuery.query<AttachedComment>(
|
||||
test.class.TestComment,
|
||||
{ _id: parentComment },
|
||||
(result) => {
|
||||
const comment = result[0]
|
||||
if (comment !== undefined) {
|
||||
expect(((comment as WithLookup<AttachedComment>).$lookup as any)?.comments).toHaveLength(attempt++)
|
||||
}
|
||||
if (attempt === childLength) {
|
||||
resolve(null)
|
||||
}
|
||||
},
|
||||
{ lookup: { _id: { comments: test.class.TestComment } } }
|
||||
)
|
||||
})
|
||||
|
||||
for (let index = 0; index < childLength; index++) {
|
||||
await factory.addCollection(test.class.TestComment, spaces[0]._id, parentComment, test.class.TestComment, 'comments', {
|
||||
message: index.toString()
|
||||
})
|
||||
}
|
||||
await pp
|
||||
})
|
||||
|
||||
it('lookup query remove doc', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'new space',
|
||||
description: '',
|
||||
archived: false,
|
||||
private: false,
|
||||
members: []
|
||||
})
|
||||
const comment = await factory.addCollection(test.class.TestComment, futureSpace, futureSpace, core.class.Space, 'comments', {
|
||||
message: 'test'
|
||||
})
|
||||
let attempt = 0
|
||||
const pp = new Promise((resolve) => {
|
||||
liveQuery.query<AttachedComment>(
|
||||
test.class.TestComment,
|
||||
{ _id: comment },
|
||||
(result) => {
|
||||
const comment = result[0]
|
||||
if (comment !== undefined) {
|
||||
if (attempt > 0) {
|
||||
expect((comment as WithLookup<AttachedComment>).$lookup?.space).toBeUndefined()
|
||||
resolve(null)
|
||||
} else {
|
||||
expect(((comment as WithLookup<AttachedComment>).$lookup?.space as Doc)?._id).toEqual(futureSpace)
|
||||
attempt++
|
||||
}
|
||||
}
|
||||
},
|
||||
{ lookup: { space: core.class.Space } }
|
||||
)
|
||||
})
|
||||
|
||||
await factory.removeDoc(core.class.Space, core.space.Model, futureSpace)
|
||||
|
||||
await pp
|
||||
})
|
||||
|
||||
it('lookup nested query remove doc', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: 'new space',
|
||||
description: '',
|
||||
archived: false,
|
||||
private: false,
|
||||
members: []
|
||||
})
|
||||
const comment = await factory.addCollection(test.class.TestComment, futureSpace, futureSpace, core.class.Space, 'comments', {
|
||||
message: 'test'
|
||||
})
|
||||
const childComment = await factory.addCollection(test.class.TestComment, futureSpace, comment, test.class.TestComment, 'comments', {
|
||||
message: 'child'
|
||||
})
|
||||
let attempt = 0
|
||||
const pp = new Promise((resolve) => {
|
||||
liveQuery.query<AttachedComment>(
|
||||
test.class.TestComment,
|
||||
{ _id: childComment },
|
||||
(result) => {
|
||||
const comment = result[0]
|
||||
if (comment !== undefined) {
|
||||
if (attempt > 0) {
|
||||
expect(((comment as WithLookup<AttachedComment>).$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space).toBeUndefined()
|
||||
resolve(null)
|
||||
} else {
|
||||
expect((((comment as WithLookup<AttachedComment>).$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space as Doc)?._id).toEqual(futureSpace)
|
||||
attempt++
|
||||
}
|
||||
}
|
||||
},
|
||||
{ lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } }
|
||||
)
|
||||
})
|
||||
|
||||
await factory.removeDoc(core.class.Space, core.space.Model, futureSpace)
|
||||
|
||||
await pp
|
||||
})
|
||||
|
||||
it('lookup reverse query remove doc', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
const spaces = await liveQuery.findAll(core.class.Space, {})
|
||||
const comments = await liveQuery.findAll(test.class.TestComment, {})
|
||||
expect(comments).toHaveLength(0)
|
||||
const parentComment = await factory.addCollection(test.class.TestComment, spaces[0]._id, spaces[0]._id, spaces[0]._class, 'comments', {
|
||||
message: 'test'
|
||||
})
|
||||
let attempt = 0
|
||||
const childLength = 3
|
||||
const childs: Ref<AttachedComment>[] = []
|
||||
for (let index = 0; index < childLength; index++) {
|
||||
childs.push(await factory.addCollection(test.class.TestComment, spaces[0]._id, parentComment, test.class.TestComment, 'comments', {
|
||||
message: index.toString()
|
||||
}))
|
||||
}
|
||||
const pp = new Promise((resolve) => {
|
||||
liveQuery.query<AttachedComment>(
|
||||
test.class.TestComment,
|
||||
{ _id: parentComment },
|
||||
(result) => {
|
||||
const comment = result[0]
|
||||
if (comment !== undefined) {
|
||||
expect(((comment as WithLookup<AttachedComment>).$lookup as any)?.comments).toHaveLength(childLength - attempt)
|
||||
attempt++
|
||||
}
|
||||
if (attempt === childLength) {
|
||||
resolve(null)
|
||||
}
|
||||
},
|
||||
{ lookup: { _id: { comments: test.class.TestComment } } }
|
||||
)
|
||||
})
|
||||
|
||||
for (const child of childs) {
|
||||
await factory.removeCollection(test.class.TestComment, spaces[0]._id, child, parentComment, test.class.TestComment, 'comments')
|
||||
}
|
||||
await pp
|
||||
})
|
||||
|
||||
it('lookup query update doc', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
let attempt = 0
|
||||
const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: '0',
|
||||
description: '',
|
||||
archived: false,
|
||||
private: false,
|
||||
members: []
|
||||
})
|
||||
|
||||
const comment = await factory.addCollection(test.class.TestComment, futureSpace, futureSpace, core.class.Space, 'comments', {
|
||||
message: 'test'
|
||||
})
|
||||
const pp = new Promise((resolve) => {
|
||||
liveQuery.query<AttachedComment>(
|
||||
test.class.TestComment,
|
||||
{ _id: comment },
|
||||
(result) => {
|
||||
const comment = result[0]
|
||||
if (comment !== undefined) {
|
||||
expect(((comment as WithLookup<AttachedComment>).$lookup?.space as Space).name).toEqual(attempt.toString())
|
||||
}
|
||||
if (attempt > 0) {
|
||||
resolve(null)
|
||||
} else {
|
||||
attempt++
|
||||
}
|
||||
},
|
||||
{ lookup: { space: core.class.Space } }
|
||||
)
|
||||
})
|
||||
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, futureSpace, {
|
||||
name: '1'
|
||||
})
|
||||
await pp
|
||||
})
|
||||
|
||||
it('lookup nested query update doc', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
let attempt = 0
|
||||
const futureSpace = await factory.createDoc(core.class.Space, core.space.Model, {
|
||||
name: '0',
|
||||
description: '',
|
||||
archived: false,
|
||||
private: false,
|
||||
members: []
|
||||
})
|
||||
const comment = await factory.addCollection(test.class.TestComment, futureSpace, futureSpace, core.class.Space, 'comments', {
|
||||
message: 'test'
|
||||
})
|
||||
const childComment = await factory.addCollection(test.class.TestComment, futureSpace, comment, test.class.TestComment, 'comments', {
|
||||
message: 'child'
|
||||
})
|
||||
const pp = new Promise((resolve) => {
|
||||
liveQuery.query<AttachedComment>(
|
||||
test.class.TestComment,
|
||||
{ _id: childComment },
|
||||
(result) => {
|
||||
const comment = result[0]
|
||||
if (comment !== undefined) {
|
||||
expect((((comment as WithLookup<AttachedComment>).$lookup?.attachedTo as WithLookup<AttachedComment>)?.$lookup?.space as Space).name).toEqual(attempt.toString())
|
||||
}
|
||||
if (attempt > 0) {
|
||||
resolve(null)
|
||||
} else {
|
||||
attempt++
|
||||
}
|
||||
},
|
||||
{ lookup: { attachedTo: [test.class.TestComment, { space: core.class.Space }] } }
|
||||
)
|
||||
})
|
||||
|
||||
await factory.updateDoc(core.class.Space, core.space.Model, futureSpace, {
|
||||
name: '1'
|
||||
})
|
||||
await pp
|
||||
})
|
||||
|
||||
it('lookup reverse query update doc', async () => {
|
||||
const { liveQuery, factory } = await getClient()
|
||||
const spaces = await liveQuery.findAll(core.class.Space, {})
|
||||
const parentComment = await factory.addCollection(test.class.TestComment, spaces[0]._id, spaces[0]._id, spaces[0]._class, 'comments', {
|
||||
message: 'test'
|
||||
})
|
||||
let attempt = 0
|
||||
const childComment = await factory.addCollection(test.class.TestComment, spaces[0]._id, parentComment, test.class.TestComment, 'comments', {
|
||||
message: '0'
|
||||
})
|
||||
const pp = new Promise((resolve) => {
|
||||
liveQuery.query<AttachedComment>(
|
||||
test.class.TestComment,
|
||||
{ _id: parentComment },
|
||||
(result) => {
|
||||
const comment = result[0]
|
||||
if (comment !== undefined) {
|
||||
expect((((comment as WithLookup<AttachedComment>).$lookup as any)?.comments[0] as AttachedComment).message).toEqual(attempt.toString())
|
||||
}
|
||||
if (attempt > 0) {
|
||||
resolve(null)
|
||||
} else {
|
||||
attempt++
|
||||
}
|
||||
},
|
||||
{ lookup: { _id: { comments: test.class.TestComment } } }
|
||||
)
|
||||
})
|
||||
|
||||
await factory.updateCollection(test.class.TestComment, spaces[0]._id, childComment, parentComment, test.class.TestComment, 'comments', {
|
||||
message: '1'
|
||||
})
|
||||
await pp
|
||||
})
|
||||
|
||||
// it('update with over limit', async () => {
|
||||
// const { liveQuery, factory } = await getClient()
|
||||
|
||||
|
@ -22,12 +22,10 @@ import core, {
|
||||
FindOptions,
|
||||
findProperty,
|
||||
FindResult,
|
||||
Hierarchy,
|
||||
getObjectValue,
|
||||
Hierarchy, Lookup,
|
||||
LookupData,
|
||||
ModelDb,
|
||||
Ref,
|
||||
Refs,
|
||||
resultSort,
|
||||
ModelDb, Ref, resultSort, ReverseLookups,
|
||||
SortingQuery,
|
||||
Tx,
|
||||
TxBulkWrite,
|
||||
@ -86,7 +84,6 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
const query = q.query
|
||||
for (const key in query) {
|
||||
if (key === '$search') continue
|
||||
if (key === '_id' && ((query._id as any)?.$like === undefined || query._id === undefined)) continue
|
||||
const value = (query as any)[key]
|
||||
const result = findProperty([doc], key, value)
|
||||
if (result.length === 0) {
|
||||
@ -123,7 +120,7 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
_class,
|
||||
query,
|
||||
result,
|
||||
options,
|
||||
options: options as FindOptions<Doc>,
|
||||
callback: callback as (result: Doc[]) => void
|
||||
}
|
||||
this.queries.push(q)
|
||||
@ -241,8 +238,49 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
this.sort(q, tx)
|
||||
await this.callback(updatedDoc, q)
|
||||
} else if (this.matchQuery(q, tx)) {
|
||||
await this.refresh(q)
|
||||
return await this.refresh(q)
|
||||
}
|
||||
await this.handleDocUpdateLookup(q, tx)
|
||||
}
|
||||
|
||||
private async handleDocUpdateLookup (q: Query, tx: TxUpdateDoc<Doc>): Promise<void> {
|
||||
if (q.options?.lookup === undefined) return
|
||||
const lookup = q.options.lookup
|
||||
if (q.result instanceof Promise) {
|
||||
q.result = await q.result
|
||||
}
|
||||
let needCallback = false
|
||||
needCallback = this.proccesLookupUpdateDoc(q.result, lookup, tx)
|
||||
|
||||
if (needCallback) {
|
||||
q.callback(this.clone(q.result))
|
||||
}
|
||||
}
|
||||
|
||||
private proccesLookupUpdateDoc (docs: Doc[], lookup: Lookup<Doc>, tx: TxUpdateDoc<Doc>): boolean {
|
||||
let needCallback = false
|
||||
const lookupWays = this.getLookupWays(lookup, tx.objectClass)
|
||||
for (const lookupWay of lookupWays) {
|
||||
const [objWay, key] = lookupWay
|
||||
for (const resDoc of docs) {
|
||||
const obj = getObjectValue(objWay, resDoc)
|
||||
if (obj === undefined) continue
|
||||
const value = getObjectValue('$lookup.' + key, obj)
|
||||
if (Array.isArray(value)) {
|
||||
const index = value.findIndex((p) => p._id === tx.objectId)
|
||||
if (index !== -1) {
|
||||
TxProcessor.updateDoc2Doc(value[index], tx)
|
||||
needCallback = true
|
||||
}
|
||||
} else {
|
||||
if (obj[key] === tx.objectId) {
|
||||
TxProcessor.updateDoc2Doc(obj.$lookup[key], tx)
|
||||
needCallback = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return needCallback
|
||||
}
|
||||
|
||||
/**
|
||||
@ -280,13 +318,41 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
return false
|
||||
}
|
||||
|
||||
private async lookup (doc: Doc, lookup: Refs<Doc>): Promise<void> {
|
||||
const result: LookupData<Doc> = {}
|
||||
private async getLookupValue<T extends Doc> (doc: T, lookup: Lookup<T>, result: LookupData<T>): Promise<void> {
|
||||
for (const key in lookup) {
|
||||
const _class = (lookup as any)[key] as Ref<Class<Doc>>
|
||||
const _id = (doc as any)[key] as Ref<Doc>
|
||||
;(result as any)[key] = await this.client.findOne(_class, { _id })
|
||||
if (key === '_id') {
|
||||
await this.getReverseLookupValue(doc, lookup, result)
|
||||
continue
|
||||
}
|
||||
const value = (lookup as any)[key]
|
||||
if (Array.isArray(value)) {
|
||||
const [_class, nested] = value
|
||||
const objects = await this.findAll(_class, { _id: (doc as any)[key] })
|
||||
;(result as any)[key] = objects[0]
|
||||
const nestedResult = {}
|
||||
const parent = (result as any)[key]
|
||||
await this.getLookupValue(parent, nested, nestedResult)
|
||||
Object.assign(parent, {
|
||||
$lookup: nestedResult
|
||||
})
|
||||
} else {
|
||||
const objects = await this.findAll(value, { _id: (doc as any)[key] })
|
||||
;(result as any)[key] = objects[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getReverseLookupValue<T extends Doc> (doc: T, lookup: ReverseLookups, result: LookupData<T>): Promise<void> {
|
||||
for (const key in lookup._id) {
|
||||
const value = lookup._id[key]
|
||||
const objects = await this.findAll(value, { attachedTo: doc._id })
|
||||
;(result as any)[key] = objects
|
||||
}
|
||||
}
|
||||
|
||||
private async lookup<T extends Doc> (doc: T, lookup: Lookup<T>): Promise<void> {
|
||||
const result: LookupData<Doc> = {}
|
||||
await this.getLookupValue(doc, lookup, result)
|
||||
;(doc as WithLookup<Doc>).$lookup = result
|
||||
}
|
||||
|
||||
@ -328,6 +394,49 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
q.callback(this.clone(q.result))
|
||||
}
|
||||
}
|
||||
|
||||
await this.handleDocAddLookup(q, doc)
|
||||
}
|
||||
|
||||
private async handleDocAddLookup (q: Query, doc: Doc): Promise<void> {
|
||||
if (q.options?.lookup === undefined) return
|
||||
const lookup = q.options.lookup
|
||||
if (q.result instanceof Promise) {
|
||||
q.result = await q.result
|
||||
}
|
||||
let needCallback = false
|
||||
needCallback = this.proccesLookupAddDoc(q.result, lookup, doc)
|
||||
|
||||
if (needCallback) {
|
||||
q.callback(this.clone(q.result))
|
||||
}
|
||||
}
|
||||
|
||||
private proccesLookupAddDoc (docs: Doc[], lookup: Lookup<Doc>, doc: Doc): boolean {
|
||||
let needCallback = false
|
||||
const lookupWays = this.getLookupWays(lookup, doc._class)
|
||||
for (const lookupWay of lookupWays) {
|
||||
const [objWay, key] = lookupWay
|
||||
for (const resDoc of docs) {
|
||||
const obj = getObjectValue(objWay, resDoc)
|
||||
if (obj === undefined) continue
|
||||
const value = getObjectValue('$lookup.' + key, obj)
|
||||
if (Array.isArray(value)) {
|
||||
if (this.client.getHierarchy().isDerived(doc._class, core.class.AttachedDoc)) {
|
||||
if ((doc as AttachedDoc).attachedTo === obj._id) {
|
||||
value.push(doc)
|
||||
needCallback = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (obj[key] === doc._id) {
|
||||
obj.$lookup[key] = doc
|
||||
needCallback = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return needCallback
|
||||
}
|
||||
|
||||
protected async txRemoveDoc (tx: TxRemoveDoc<Doc>): Promise<TxResult> {
|
||||
@ -346,7 +455,6 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
if (q.result instanceof Promise) {
|
||||
q.result = await q.result
|
||||
}
|
||||
const index = q.result.findIndex((p) => p._id === tx.objectId)
|
||||
if (
|
||||
q.options?.limit !== undefined &&
|
||||
q.options.limit === q.result.length &&
|
||||
@ -354,10 +462,83 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
) {
|
||||
return await this.refresh(q)
|
||||
}
|
||||
const index = q.result.findIndex((p) => p._id === tx.objectId)
|
||||
if (index > -1) {
|
||||
q.result.splice(index, 1)
|
||||
q.callback(this.clone(q.result))
|
||||
}
|
||||
await this.handleDocRemoveLookup(q, tx)
|
||||
}
|
||||
|
||||
private async handleDocRemoveLookup (q: Query, tx: TxRemoveDoc<Doc>): Promise<void> {
|
||||
if (q.options?.lookup === undefined) return
|
||||
let needCallback = false
|
||||
const lookupWays = this.getLookupWays(q.options.lookup, tx.objectClass)
|
||||
if (lookupWays.length === 0) return
|
||||
if (q.result instanceof Promise) {
|
||||
q.result = await q.result
|
||||
}
|
||||
for (const lookupWay of lookupWays) {
|
||||
const [objWay, key] = lookupWay
|
||||
const docs = q.result
|
||||
for (const doc of docs) {
|
||||
const obj = getObjectValue(objWay, doc)
|
||||
if (obj === undefined) continue
|
||||
const value = getObjectValue('$lookup.' + key, obj)
|
||||
if (value === undefined) continue
|
||||
if (Array.isArray(value)) {
|
||||
const index = value.findIndex((p) => p._id === tx.objectId)
|
||||
if (index !== -1) {
|
||||
value.splice(index, 1)
|
||||
needCallback = true
|
||||
}
|
||||
} else {
|
||||
if (value._id === tx.objectId) {
|
||||
obj.$lookup[key] = undefined
|
||||
needCallback = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needCallback) {
|
||||
q.callback(this.clone(q.result))
|
||||
}
|
||||
}
|
||||
|
||||
private getLookupWays (lookup: Lookup<Doc>, _class: Ref<Class<Doc>>, parent: string = ''): [string, string][] {
|
||||
const result: [string, string][] = []
|
||||
const hierarchy = this.client.getHierarchy()
|
||||
if (lookup._id !== undefined) {
|
||||
for (const key in lookup._id) {
|
||||
const value = (lookup._id as any)[key]
|
||||
const clazz = hierarchy.isMixin(value) ? hierarchy.getBaseClass(value) : value
|
||||
if (hierarchy.isDerived(_class, clazz)) {
|
||||
result.push([parent, key])
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key in lookup) {
|
||||
if (key === '_id') continue
|
||||
const value = (lookup as any)[key]
|
||||
if (Array.isArray(value)) {
|
||||
const clazz = hierarchy.isMixin(value[0]) ? hierarchy.getBaseClass(value[0]) : value[0]
|
||||
if (hierarchy.isDerived(_class, clazz)) {
|
||||
result.push([parent, key])
|
||||
}
|
||||
const lookupKey = '$lookup.' + key
|
||||
const newParent = parent.length > 0 ? parent + '.' + lookupKey : lookupKey
|
||||
const nested = this.getLookupWays(value[1], _class, newParent)
|
||||
if (nested.length > 0) {
|
||||
result.push(...nested)
|
||||
}
|
||||
} else {
|
||||
const clazz = hierarchy.isMixin(value) ? hierarchy.getBaseClass(value) : value
|
||||
if (hierarchy.isDerived(_class, clazz)) {
|
||||
result.push([parent, key])
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
protected override async txBulkWrite (tx: TxBulkWrite): Promise<TxResult> {
|
||||
|
61
plugins/contact-resources/src/components/Channels.svelte
Normal file
61
plugins/contact-resources/src/components/Channels.svelte
Normal file
@ -0,0 +1,61 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Channel } from '@anticrm/contact'
|
||||
import type { Doc,Ref } from '@anticrm/core'
|
||||
import presentation,{ Channels } from '@anticrm/presentation'
|
||||
import { CircleButton,IconAdd,Label,showPopup } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import contact from '../plugin'
|
||||
|
||||
export let integrations: Set<Ref<Doc>> | undefined = undefined
|
||||
|
||||
export let channels: Channel[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
{#if !channels.length}
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
selected
|
||||
on:click={(ev) =>
|
||||
showPopup(contact.component.SocialEditor, { values: channels }, ev.target, (result) => {
|
||||
dispatch('change', result)
|
||||
})}
|
||||
/>
|
||||
<span><Label label={presentation.string.AddSocialLinks} /></span>
|
||||
{:else}
|
||||
<Channels value={channels} size={'small'} {integrations} on:click />
|
||||
<div class="ml-1">
|
||||
<CircleButton
|
||||
icon={contact.icon.Edit}
|
||||
size={'small'}
|
||||
selected
|
||||
on:click={(ev) =>
|
||||
showPopup(contact.component.SocialEditor, { values: channels }, ev.target, (result) => {
|
||||
dispatch('change', result)
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,74 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021, 2022 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.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { AttachedData, Class, Doc, Ref } from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
|
||||
import { ChannelProvider, Channel } from '@anticrm/contact'
|
||||
import contact from '../plugin'
|
||||
import Channels from './Channels.svelte'
|
||||
|
||||
export let attachedTo: Ref<Doc>
|
||||
export let attachedClass: Ref<Class<Doc>>
|
||||
export let integrations: Set<Ref<Doc>> | undefined = undefined
|
||||
|
||||
let channels: Channel[] = []
|
||||
|
||||
function findValue (provider: Ref<ChannelProvider>): Channel | undefined {
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
if (channels[i].provider === provider) return channels[i]
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const query = createQuery()
|
||||
query.query(contact.class.Channel, {
|
||||
attachedTo: attachedTo
|
||||
}, (res) => {
|
||||
channels = res
|
||||
})
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function save (newValues: AttachedData<Channel>[]): Promise<void> {
|
||||
const currentProviders = new Set(channels.map((p) => p.provider))
|
||||
const promises = []
|
||||
for (const value of newValues) {
|
||||
const oldChannel = findValue(value.provider)
|
||||
if (oldChannel === undefined) {
|
||||
if (value.value.length === 0) continue
|
||||
promises.push(client.addCollection(contact.class.Channel, contact.space.Contacts, attachedTo, attachedClass, 'channels', {
|
||||
value: value.value,
|
||||
provider: value.provider
|
||||
}))
|
||||
} else {
|
||||
currentProviders.delete(value.provider)
|
||||
if (value.value === oldChannel.value) continue
|
||||
promises.push(client.updateCollection(oldChannel._class, oldChannel.space, oldChannel._id, oldChannel.attachedTo, oldChannel.attachedToClass, oldChannel.collection, {
|
||||
value: value.value
|
||||
}))
|
||||
}
|
||||
}
|
||||
for (const value of currentProviders) {
|
||||
const oldChannel = findValue(value)
|
||||
if (oldChannel === undefined) continue
|
||||
promises.push(client.removeCollection(oldChannel._class, oldChannel.space, oldChannel._id, oldChannel.attachedTo, oldChannel.attachedToClass, oldChannel.collection))
|
||||
}
|
||||
Promise.all(promises)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Channels {channels} {integrations} on:change={(e) => { save(e.detail) }} />
|
@ -16,19 +16,22 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import presentation, { getClient, Card, Channels } from '@anticrm/presentation'
|
||||
import { getClient, Card } from '@anticrm/presentation'
|
||||
|
||||
import { EditBox, showPopup, CircleButton, IconEdit, IconAdd, Label } from '@anticrm/ui'
|
||||
import SocialEditor from './SocialEditor.svelte'
|
||||
import { EditBox } from '@anticrm/ui'
|
||||
|
||||
import { Organization } from '@anticrm/contact'
|
||||
import { Channel, Organization } from '@anticrm/contact'
|
||||
import contact from '../plugin'
|
||||
import Company from './icons/Company.svelte'
|
||||
import { generateId } from '@anticrm/core'
|
||||
import Channels from './Channels.svelte'
|
||||
|
||||
export function canClose (): boolean {
|
||||
return object.name === ''
|
||||
}
|
||||
|
||||
const id = generateId()
|
||||
|
||||
const object: Organization = {
|
||||
name: ''
|
||||
} as Organization
|
||||
@ -37,10 +40,18 @@
|
||||
const client = getClient()
|
||||
|
||||
async function createOrganization () {
|
||||
await client.createDoc(contact.class.Organization, contact.space.Contacts, object)
|
||||
await client.createDoc(contact.class.Organization, contact.space.Contacts, object, id)
|
||||
for (const channel of channels) {
|
||||
await client.addCollection(contact.class.Channel, contact.space.Contacts, id, contact.class.Organization, 'channels', {
|
||||
value: channel.value,
|
||||
provider: channel.provider
|
||||
})
|
||||
}
|
||||
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
let channels: Channel[] = []
|
||||
</script>
|
||||
|
||||
<Card
|
||||
@ -64,31 +75,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-row-center channels">
|
||||
{#if !object.channels || object.channels.length === 0}
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
transparent
|
||||
on:click={(ev) =>
|
||||
showPopup(SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => {
|
||||
object.channels = result
|
||||
})}
|
||||
/>
|
||||
<span><Label label={presentation.string.AddSocialLinks} /></span>
|
||||
{:else}
|
||||
<Channels value={object.channels} size={'small'} />
|
||||
<div class="ml-1">
|
||||
<CircleButton
|
||||
icon={IconEdit}
|
||||
size={'small'}
|
||||
transparent
|
||||
on:click={(ev) =>
|
||||
showPopup(SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => {
|
||||
object.channels = result
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Channels bind:channels={channels} on:change={(e) => { channels = e.detail }} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -102,8 +89,5 @@
|
||||
}
|
||||
.channels {
|
||||
margin-top: 1.25rem;
|
||||
span {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -15,21 +15,23 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { Data } from '@anticrm/core'
|
||||
import { getResource } from '@anticrm/platform';
|
||||
import { Data, generateId } from '@anticrm/core'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
|
||||
import presentation, { getClient, Card, Channels, EditableAvatar } from '@anticrm/presentation'
|
||||
import { getClient, Card, EditableAvatar } from '@anticrm/presentation'
|
||||
|
||||
import attachment from '@anticrm/attachment'
|
||||
import { EditBox, showPopup, CircleButton, IconEdit, IconAdd, Label } from '@anticrm/ui'
|
||||
import SocialEditor from './SocialEditor.svelte'
|
||||
import { EditBox } from '@anticrm/ui'
|
||||
|
||||
import { combineName, Person } from '@anticrm/contact'
|
||||
import { Channel, combineName, Person } from '@anticrm/contact'
|
||||
import contact from '../plugin'
|
||||
import Channels from './Channels.svelte'
|
||||
|
||||
let firstName = ''
|
||||
let lastName = ''
|
||||
|
||||
const id = generateId()
|
||||
|
||||
export function canClose (): boolean {
|
||||
return firstName === '' && lastName === ''
|
||||
}
|
||||
@ -60,10 +62,18 @@
|
||||
...avatarProp
|
||||
}
|
||||
|
||||
await client.createDoc(contact.class.Person, contact.space.Contacts, person)
|
||||
await client.createDoc(contact.class.Person, contact.space.Contacts, person, id)
|
||||
|
||||
for (const channel of channels) {
|
||||
await client.addCollection(contact.class.Channel, contact.space.Contacts, id, contact.class.Person, 'channels', {
|
||||
value: channel.value,
|
||||
provider: channel.provider
|
||||
})
|
||||
}
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
let channels: Channel[] = []
|
||||
</script>
|
||||
|
||||
<Card
|
||||
@ -87,39 +97,12 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-row-center channels">
|
||||
{#if !object.channels || object.channels.length === 0}
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
transparent
|
||||
on:click={(ev) =>
|
||||
showPopup(SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => {
|
||||
object.channels = result
|
||||
})}
|
||||
/>
|
||||
<span><Label label={presentation.string.AddSocialLinks} /></span>
|
||||
{:else}
|
||||
<Channels value={object.channels} size={'small'} />
|
||||
<div class="ml-1">
|
||||
<CircleButton
|
||||
icon={IconEdit}
|
||||
size={'small'}
|
||||
transparent
|
||||
on:click={(ev) =>
|
||||
showPopup(SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => {
|
||||
object.channels = result
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<Channels bind:channels={channels} on:change={(e) => { channels = e.detail }} />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.channels {
|
||||
margin-top: 1.25rem;
|
||||
span {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -16,13 +16,14 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { getCurrentAccount, Ref, Space } from '@anticrm/core'
|
||||
import { CircleButton, EditBox, showPopup, IconAdd, Label, IconActivity } from '@anticrm/ui'
|
||||
import presentation, { getClient, createQuery, Channels } from '@anticrm/presentation'
|
||||
import { CircleButton, EditBox, IconActivity } from '@anticrm/ui'
|
||||
import { getClient, createQuery } from '@anticrm/presentation'
|
||||
import setting from '@anticrm/setting'
|
||||
import { IntegrationType } from '@anticrm/setting'
|
||||
import contact from '../plugin'
|
||||
import { Organization } from '@anticrm/contact'
|
||||
import Company from './icons/Company.svelte'
|
||||
import ChannelsEditor from './ChannelsEditor.svelte'
|
||||
|
||||
export let object: Organization
|
||||
|
||||
@ -30,13 +31,6 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function saveChannels (result: any) {
|
||||
if (result !== undefined) {
|
||||
object.channels = result
|
||||
client.updateDoc(object._class, object.space, object._id, { channels: result })
|
||||
}
|
||||
}
|
||||
|
||||
function nameChange () {
|
||||
client.updateDoc(object._class, object.space, object._id, { name: object.name })
|
||||
}
|
||||
@ -64,31 +58,7 @@
|
||||
</div>
|
||||
<div class="flex-between channels">
|
||||
<div class="flex-row-center">
|
||||
{#if !object.channels || object.channels.length === 0}
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
selected
|
||||
on:click={(ev) =>
|
||||
showPopup(contact.component.SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => {
|
||||
saveChannels(result)
|
||||
})}
|
||||
/>
|
||||
<span><Label label={presentation.string.AddSocialLinks} /></span>
|
||||
{:else}
|
||||
<Channels value={object.channels} size={'small'} {integrations} on:click />
|
||||
<div class="ml-1">
|
||||
<CircleButton
|
||||
icon={contact.icon.Edit}
|
||||
size={'small'}
|
||||
selected
|
||||
on:click={(ev) =>
|
||||
showPopup(contact.component.SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => {
|
||||
saveChannels(result)
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<ChannelsEditor attachedTo={object._id} attachedClass={object._class} {integrations} />
|
||||
</div>
|
||||
|
||||
<div class="flex-row-center">
|
||||
@ -117,8 +87,5 @@
|
||||
}
|
||||
.channels {
|
||||
margin-top: 0.75rem;
|
||||
span {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -16,14 +16,15 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, afterUpdate } from 'svelte'
|
||||
import { getCurrentAccount, Ref, Space } from '@anticrm/core'
|
||||
import { CircleButton, EditBox, showPopup, IconAdd, Label, IconActivity } from '@anticrm/ui'
|
||||
import presentation, { getClient, createQuery, Channels, EditableAvatar, AttributeEditor } from '@anticrm/presentation'
|
||||
import { CircleButton, EditBox, IconActivity } from '@anticrm/ui'
|
||||
import { getClient, createQuery, EditableAvatar, AttributeEditor } from '@anticrm/presentation'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import attachment from '@anticrm/attachment'
|
||||
import setting from '@anticrm/setting'
|
||||
import { IntegrationType } from '@anticrm/setting'
|
||||
import contact from '../plugin'
|
||||
import { combineName, getFirstName, getLastName, Person } from '@anticrm/contact'
|
||||
import ChannelsEditor from './ChannelsEditor.svelte'
|
||||
|
||||
export let object: Person
|
||||
|
||||
@ -34,13 +35,6 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function saveChannels (result: any) {
|
||||
if (result !== undefined) {
|
||||
object.channels = result
|
||||
client.updateDoc(object._class, object.space, object._id, { channels: result })
|
||||
}
|
||||
}
|
||||
|
||||
function firstNameChange () {
|
||||
client.updateDoc(object._class, object.space, object._id, {
|
||||
name: combineName(firstName, getLastName(object.name))
|
||||
@ -101,31 +95,7 @@
|
||||
|
||||
<div class="flex-between channels">
|
||||
<div class="flex-row-center">
|
||||
{#if !object.channels || object.channels.length === 0}
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
selected
|
||||
on:click={(ev) =>
|
||||
showPopup(contact.component.SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => {
|
||||
saveChannels(result)
|
||||
})}
|
||||
/>
|
||||
<span><Label label={presentation.string.AddSocialLinks} /></span>
|
||||
{:else}
|
||||
<Channels value={object.channels} size={'small'} {integrations} on:click />
|
||||
<div class="ml-1">
|
||||
<CircleButton
|
||||
icon={contact.icon.Edit}
|
||||
size={'small'}
|
||||
selected
|
||||
on:click={(ev) =>
|
||||
showPopup(contact.component.SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => {
|
||||
saveChannels(result)
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<ChannelsEditor attachedTo={object._id} attachedClass={object._class} {integrations} />
|
||||
</div>
|
||||
|
||||
<div class="flex-row-center">
|
||||
@ -147,9 +117,6 @@
|
||||
}
|
||||
.channels {
|
||||
margin-top: 0.75rem;
|
||||
span {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
.location {
|
||||
margin-top: 0.25rem;
|
||||
|
@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Ref } from '@anticrm/core'
|
||||
import type { AttachedData, Ref } from '@anticrm/core'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { EditBox, Button, ScrollBox } from '@anticrm/ui'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
@ -23,7 +23,7 @@
|
||||
import contact from '../plugin'
|
||||
|
||||
export let values: Channel[]
|
||||
const newValues: Channel[] = []
|
||||
const newValues: AttachedData<Channel>[] = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
function filterUndefined (channels: Channel[]): Channel[] {
|
||||
function filterUndefined (channels: AttachedData<Channel>[]): AttachedData<Channel>[] {
|
||||
return channels.filter((channel) => channel.value !== undefined && channel.value.length > 0)
|
||||
}
|
||||
</script>
|
||||
|
@ -18,6 +18,8 @@ import { Contact, formatName } from '@anticrm/contact'
|
||||
import { Class, Client, Ref } from '@anticrm/core'
|
||||
import { Resources } from '@anticrm/platform'
|
||||
import { Avatar, ObjectSearchResult, UserInfo } from '@anticrm/presentation'
|
||||
import ChannelsEditor from './components/ChannelsEditor.svelte'
|
||||
import Channels from './components/Channels.svelte'
|
||||
import ChannelsPresenter from './components/ChannelsPresenter.svelte'
|
||||
import ContactPresenter from './components/ContactPresenter.svelte'
|
||||
import Contacts from './components/Contacts.svelte'
|
||||
@ -32,7 +34,7 @@ import PersonPresenter from './components/PersonPresenter.svelte'
|
||||
import SocialEditor from './components/SocialEditor.svelte'
|
||||
import contact from './plugin'
|
||||
|
||||
export { ContactPresenter }
|
||||
export { Channels, ChannelsEditor, ContactPresenter }
|
||||
|
||||
async function queryContact (_class: Ref<Class<Contact>>, client: Client, search: string): Promise<ObjectSearchResult[]> {
|
||||
return (await client.findAll(_class, { name: { $like: `%${search}%` } }, { limit: 200 })).map(e => ({
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
import { IntlString, plugin } from '@anticrm/platform'
|
||||
import type { Plugin, Asset } from '@anticrm/platform'
|
||||
import type { Doc, Ref, Class, UXObject, Space, Account } from '@anticrm/core'
|
||||
import type { Doc, Ref, Class, UXObject, Space, Account, AttachedDoc } from '@anticrm/core'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
|
||||
/**
|
||||
@ -40,7 +40,7 @@ export interface ChannelProvider extends Doc, UXObject {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface Channel {
|
||||
export interface Channel extends AttachedDoc {
|
||||
provider: Ref<ChannelProvider>
|
||||
value: string
|
||||
}
|
||||
@ -53,7 +53,7 @@ export interface Contact extends Doc {
|
||||
avatar?: string
|
||||
attachments?: number
|
||||
comments?: number
|
||||
channels: Channel[]
|
||||
channels?: number
|
||||
city: string
|
||||
}
|
||||
|
||||
@ -121,6 +121,7 @@ export const contactId = 'contact' as Plugin
|
||||
export default plugin(contactId, {
|
||||
class: {
|
||||
ChannelProvider: '' as Ref<Class<ChannelProvider>>,
|
||||
Channel: '' as Ref<Class<Channel>>,
|
||||
Contact: '' as Ref<Class<Contact>>,
|
||||
Person: '' as Ref<Class<Person>>,
|
||||
Persons: '' as Ref<Class<Persons>>,
|
||||
|
@ -64,7 +64,7 @@ class ModelClient implements Client {
|
||||
async findOne <T extends Doc>(_class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T>): Promise<WithLookup<T> | undefined> {
|
||||
const result = await this.client.findOne(_class, query, options)
|
||||
console.info('devmodel# findOne=>', _class, query, options, 'result => ', result, ' =>model', this.client.getModel(), getMetadata(devmodel.metadata.DevModel))
|
||||
queries.push({ _class, query, options, result: result !== undefined ? [result] : [], findOne: true })
|
||||
queries.push({ _class, query, options: options as FindOptions<Doc>, result: result !== undefined ? [result] : [], findOne: true })
|
||||
if (queries.length > 100) {
|
||||
queries.shift()
|
||||
}
|
||||
@ -74,7 +74,7 @@ class ModelClient implements Client {
|
||||
async findAll<T extends Doc>(_class: Ref<Class<T>>, query: DocumentQuery<T>, options?: FindOptions<T>): Promise<FindResult<T>> {
|
||||
const result = await this.client.findAll(_class, query, options)
|
||||
console.info('devmodel# findAll=>', _class, query, options, 'result => ', result, ' =>model', this.client.getModel(), getMetadata(devmodel.metadata.DevModel))
|
||||
queries.push({ _class, query, options, result, findOne: false })
|
||||
queries.push({ _class, query, options: options as FindOptions<Doc>, result, findOne: false })
|
||||
if (queries.length > 100) {
|
||||
queries.shift()
|
||||
}
|
||||
|
@ -14,16 +14,24 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Contact } from '@anticrm/contact'
|
||||
import contact, { Channel, Contact } from '@anticrm/contact'
|
||||
import { SharedMessage } from '@anticrm/gmail'
|
||||
import NewMessage from './NewMessage.svelte'
|
||||
import FullMessage from './FullMessage.svelte'
|
||||
import Chats from './Chats.svelte'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
|
||||
export let object: Contact
|
||||
let newMessage: boolean = false
|
||||
let currentMessage: SharedMessage | undefined = undefined
|
||||
$: contactString = object.channels.find((p) => p.provider === contact.channelProvider.Email)
|
||||
let channelValue: string | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
|
||||
client.findOne(contact.class.Channel, {
|
||||
attachedTo: object._id,
|
||||
provider: contact.channelProvider.Email
|
||||
}).then((res) => channelValue = res?.value)
|
||||
|
||||
function back () {
|
||||
if (newMessage) {
|
||||
@ -37,12 +45,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if contactString}
|
||||
{#if channelValue}
|
||||
{#if newMessage}
|
||||
<NewMessage {object} contact={contactString.value} {currentMessage} on:close={back} />
|
||||
<NewMessage {object} contact={channelValue} {currentMessage} on:close={back} />
|
||||
{:else if currentMessage}
|
||||
<FullMessage {currentMessage} bind:newMessage on:close={back} />
|
||||
{:else}
|
||||
<Chats {object} contactString={contactString.value} bind:newMessage on:select={selectHandler} />
|
||||
<Chats {object} contactString={channelValue} bind:newMessage on:select={selectHandler} />
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -16,13 +16,18 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Doc, DocumentQuery } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Icon, Label, ScrollBox, SearchEdit } from '@anticrm/ui'
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
import view, { Viewlet } from '@anticrm/view'
|
||||
import lead from '../plugin'
|
||||
|
||||
let search = ''
|
||||
let resultQuery: DocumentQuery<Doc> = {}
|
||||
|
||||
const client = getClient()
|
||||
const tableDescriptor = client.findOne<Viewlet>(view.class.Viewlet, { attachTo: lead.mixin.Customer, descriptor: view.viewlet.Table })
|
||||
|
||||
function updateResultQuery (search: string): void {
|
||||
resultQuery = (search === '') ? { } : { $search: search }
|
||||
}
|
||||
@ -35,7 +40,7 @@
|
||||
<span class="label"><Label label={lead.string.Customers}/></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<SearchEdit bind:value={search} on:change={() => {
|
||||
updateResultQuery(search)
|
||||
}}/>
|
||||
@ -44,19 +49,17 @@
|
||||
<div class="container">
|
||||
<div class="panel-component">
|
||||
<ScrollBox vertical stretch noShift>
|
||||
|
||||
<Table
|
||||
_class={lead.mixin.Customer}
|
||||
config={[
|
||||
'',
|
||||
{ key: 'leads', presenter: lead.component.LeadsPresenter, label: lead.string.Leads },
|
||||
'modifiedOn',
|
||||
'channels'
|
||||
]}
|
||||
options={ {} }
|
||||
query={ resultQuery }
|
||||
enableChecking
|
||||
/>
|
||||
{#await tableDescriptor then descr}
|
||||
{#if descr}
|
||||
<Table
|
||||
_class={lead.mixin.Customer}
|
||||
config={descr.config}
|
||||
options={descr.options}
|
||||
query={ resultQuery }
|
||||
enableChecking
|
||||
/>
|
||||
{/if}
|
||||
{/await}
|
||||
</ScrollBox>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,14 +15,15 @@
|
||||
|
||||
<script lang="ts">
|
||||
import attachment from '@anticrm/attachment'
|
||||
import contact, { combineName, Person } from '@anticrm/contact'
|
||||
import contact, { Channel, combineName, Person } from '@anticrm/contact'
|
||||
import type { Data, MixinData, Ref } from '@anticrm/core'
|
||||
import { generateId } from '@anticrm/core'
|
||||
import { getResource, setPlatformStatus, unknownError } from '@anticrm/platform'
|
||||
import presentation, { EditableAvatar, Card, Channels, getClient, PDFViewer } from '@anticrm/presentation'
|
||||
import { EditableAvatar, Card, getClient, PDFViewer } from '@anticrm/presentation'
|
||||
import type { Candidate } from '@anticrm/recruit'
|
||||
import { CircleButton, EditBox, IconAdd, IconFile as FileIcon, Label, Link, showPopup, Spinner } from '@anticrm/ui'
|
||||
import { EditBox, IconFile as FileIcon, Label, Link, showPopup, Spinner } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { Channels } from '@anticrm/contact-resources'
|
||||
import recruit from '../plugin'
|
||||
import FileUpload from './icons/FileUpload.svelte'
|
||||
import YesNo from './YesNo.svelte'
|
||||
@ -79,6 +80,12 @@
|
||||
lastModified: resume.lastModified
|
||||
})
|
||||
}
|
||||
for (const channel of channels) {
|
||||
await client.addCollection(contact.class.Channel, contact.space.Contacts, candidateId, contact.class.Person, 'channels', {
|
||||
value: channel.value,
|
||||
provider: channel.provider
|
||||
})
|
||||
}
|
||||
|
||||
dispatch('close')
|
||||
}
|
||||
@ -129,6 +136,8 @@
|
||||
avatar = file
|
||||
}
|
||||
|
||||
let channels: Channel[] = []
|
||||
|
||||
</script>
|
||||
|
||||
<!-- <DialogHeader {space} {object} {newValue} {resume} create={true} on:save={createCandidate}/> -->
|
||||
@ -153,15 +162,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-row-center channels">
|
||||
{#if !object.channels || object.channels.length === 0}
|
||||
<CircleButton icon={IconAdd} size={'small'} transparent on:click={(ev) => showPopup(contact.component.SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => { object.channels = result })} />
|
||||
<span><Label label={presentation.string.AddSocialLinks} /></span>
|
||||
{:else}
|
||||
<Channels value={object.channels} size={'small'} />
|
||||
<div class="ml-1">
|
||||
<CircleButton icon={contact.icon.Edit} size={'small'} transparent on:click={(ev) => showPopup(contact.component.SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => { object.channels = result })} />
|
||||
</div>
|
||||
{/if}
|
||||
<Channels bind:channels={channels} on:change={(e) => { channels = e.detail }} />
|
||||
</div>
|
||||
|
||||
<div class="flex-center resume" class:solid={dragover || resume.uuid}
|
||||
@ -191,7 +192,6 @@
|
||||
<style lang="scss">
|
||||
.channels {
|
||||
margin-top: 1.25rem;
|
||||
span { margin-left: .5rem; }
|
||||
}
|
||||
|
||||
.locations {
|
||||
|
@ -13,22 +13,22 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import presentation, {
|
||||
import {
|
||||
AttributeEditor,
|
||||
Channels,
|
||||
createQuery,
|
||||
EditableAvatar,
|
||||
getClient
|
||||
} from '@anticrm/presentation'
|
||||
|
||||
import setting from '@anticrm/setting'
|
||||
import { CircleButton, EditBox, Icon, IconAdd, Label, showPopup } from '@anticrm/ui'
|
||||
import { EditBox, Icon, Label } from '@anticrm/ui'
|
||||
import contact, { Employee, EmployeeAccount, getFirstName, getLastName } from '@anticrm/contact'
|
||||
import contactRes from '@anticrm/contact-resources/src/plugin'
|
||||
import { getCurrentAccount, Ref } from '@anticrm/core'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import attachment from '@anticrm/attachment'
|
||||
import { changeName } from '@anticrm/login-resources'
|
||||
import { ChannelsEditor } from '@anticrm/contact-resources'
|
||||
const client = getClient()
|
||||
|
||||
let account: EmployeeAccount | undefined
|
||||
@ -129,41 +129,7 @@
|
||||
|
||||
<div class="flex-between channels">
|
||||
<div class="flex-row-center">
|
||||
{#if !employee.channels || employee.channels.length === 0}
|
||||
<CircleButton
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
selected
|
||||
on:click={(ev) =>
|
||||
showPopup(
|
||||
contact.component.SocialEditor,
|
||||
{ values: employee?.channels ?? [] },
|
||||
ev.target,
|
||||
(result) => {
|
||||
saveChannels(result)
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<span><Label label={presentation.string.AddSocialLinks} /></span>
|
||||
{:else}
|
||||
<Channels value={employee.channels} size={'small'} />
|
||||
<div class="ml-1">
|
||||
<CircleButton
|
||||
icon={contact.icon.Edit}
|
||||
size={'small'}
|
||||
selected
|
||||
on:click={(ev) =>
|
||||
showPopup(
|
||||
contact.component.SocialEditor,
|
||||
{ values: employee?.channels ?? [] },
|
||||
ev.target,
|
||||
(result) => {
|
||||
saveChannels(result)
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<ChannelsEditor attachedTo={employee._id} attachedClass={employee._class} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -52,7 +52,7 @@
|
||||
<CircleButton icon={IconAdd} size={'small'} on:click={createApp} />
|
||||
</div>
|
||||
{#if tasks.length > 0}
|
||||
<Table
|
||||
<Table
|
||||
_class={task.class.Issue}
|
||||
config={['', '$lookup.space.name', '$lookup.state']}
|
||||
{options}
|
||||
|
@ -20,14 +20,17 @@ import core, {
|
||||
Client,
|
||||
Collection,
|
||||
Doc,
|
||||
FindOptions,
|
||||
FindResult, Hierarchy, matchQuery, Obj,
|
||||
FindResult,
|
||||
Hierarchy,
|
||||
Lookup,
|
||||
matchQuery,
|
||||
Obj,
|
||||
Ref,
|
||||
TxOperations
|
||||
} from '@anticrm/core'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import { getAttributePresenterClass } from '@anticrm/presentation'
|
||||
import { Channels, getAttributePresenterClass } from '@anticrm/presentation'
|
||||
import { ErrorPresenter, getPlatformColorForText } from '@anticrm/ui'
|
||||
import type { Action, ActionTarget, BuildModelOptions, ObjectDDParticipant } from '@anticrm/view'
|
||||
import view, { AttributeModel, BuildModelKey } from '@anticrm/view'
|
||||
@ -102,12 +105,12 @@ async function getAttributePresenter (
|
||||
}
|
||||
}
|
||||
|
||||
async function getPresenter (
|
||||
async function getPresenter<T extends Doc> (
|
||||
client: Client,
|
||||
_class: Ref<Class<Obj>>,
|
||||
_class: Ref<Class<T>>,
|
||||
key: BuildModelKey,
|
||||
preserveKey: BuildModelKey,
|
||||
options?: FindOptions<Doc>
|
||||
lookup?: Lookup<T>
|
||||
): Promise<AttributeModel> {
|
||||
if (key.presenter !== undefined) {
|
||||
const { presenter, label, sortingKey } = key
|
||||
@ -122,22 +125,11 @@ async function getPresenter (
|
||||
if (key.key.length === 0) {
|
||||
return await getObjectPresenter(client, _class, preserveKey)
|
||||
} else {
|
||||
const split = key.key.split('.')
|
||||
if (split[0] === '$lookup') {
|
||||
const lookupClass = (options?.lookup as any)[split[1]] as Ref<Class<Obj>>
|
||||
if (lookupClass === undefined) {
|
||||
throw new Error('lookup class does not provided for ' + split[1])
|
||||
if (key.key.startsWith('$lookup')) {
|
||||
if (lookup === undefined) {
|
||||
throw new Error('lookup class does not provided for ' + key)
|
||||
}
|
||||
const lookupKey = { ...key, key: split[2] ?? '' }
|
||||
const model = await getPresenter(client, lookupClass, lookupKey, preserveKey)
|
||||
if (lookupKey.key === '') {
|
||||
const attribute = client.getHierarchy().getAttribute(_class, split[1])
|
||||
model.label = attribute.label
|
||||
} else {
|
||||
const attribute = client.getHierarchy().getAttribute(lookupClass, lookupKey.key)
|
||||
model.label = attribute.label
|
||||
}
|
||||
return model
|
||||
return await getLookupPresenter(client, _class, key, preserveKey, lookup)
|
||||
}
|
||||
return await getAttributePresenter(client, _class, key.key, preserveKey)
|
||||
}
|
||||
@ -150,7 +142,7 @@ export async function buildModel (options: BuildModelOptions): Promise<Attribute
|
||||
.map((key) => (typeof key === 'string' ? { key: key } : key))
|
||||
.map(async (key) => {
|
||||
try {
|
||||
return await getPresenter(options.client, options._class, key, key, options.options)
|
||||
return await getPresenter(options.client, options._class, key, key, options.options?.lookup)
|
||||
} catch (err: any) {
|
||||
if (options.ignoreMissing ?? false) {
|
||||
return undefined
|
||||
@ -250,3 +242,65 @@ export function getMixinStyle (id: Ref<Class<Doc>>, selected: boolean): string {
|
||||
border: 1px solid ${color + (selected ? '0f' : '66')};
|
||||
`
|
||||
}
|
||||
|
||||
async function getLookupPresenter<T extends Doc> (client: Client, _class: Ref<Class<T>>, key: BuildModelKey, preserveKey: BuildModelKey, lookup: Lookup<T>): Promise<AttributeModel> {
|
||||
const lookupClass = getLookupClass(key.key, lookup, _class)
|
||||
const lookupProperty = getLookupProperty(key.key)
|
||||
const lookupKey = { ...key, key: lookupProperty[0] }
|
||||
const model = await getPresenter(client, lookupClass[0], lookupKey, preserveKey)
|
||||
model.label = getLookupLabel(client, lookupClass[1], lookupClass[0], lookupKey, lookupProperty[1])
|
||||
return model
|
||||
}
|
||||
|
||||
function getLookupLabel<T extends Doc> (client: Client, _class: Ref<Class<T>>, lookupClass: Ref<Class<Doc>>, key: BuildModelKey, attrib: string): IntlString {
|
||||
if (key.label !== undefined) return key.label
|
||||
if (key.key === '') {
|
||||
try {
|
||||
const attribute = client.getHierarchy().getAttribute(_class, attrib)
|
||||
return attribute.label
|
||||
} catch {}
|
||||
const clazz = client.getHierarchy().getClass(lookupClass)
|
||||
return clazz.label
|
||||
} else {
|
||||
const attribute = client.getHierarchy().getAttribute(lookupClass, key.key)
|
||||
return attribute.label
|
||||
}
|
||||
}
|
||||
|
||||
function getLookupClass<T extends Doc> (key: string, lookup: Lookup<T>, parent: Ref<Class<T>>): [Ref<Class<Doc>>, Ref<Class<Doc>>] {
|
||||
const _class = getLookup(key, lookup, parent)
|
||||
if (_class === undefined) {
|
||||
throw new Error('lookup class does not provided for ' + key)
|
||||
}
|
||||
return _class
|
||||
}
|
||||
|
||||
function getLookupProperty (key: string): [string, string] {
|
||||
const parts = key.split('$lookup')
|
||||
const lastPart = parts[parts.length - 1]
|
||||
const split = lastPart.split('.').filter((p) => p.length > 0)
|
||||
const prev = split.shift() ?? ''
|
||||
const result = split.join('.')
|
||||
return [result, prev]
|
||||
}
|
||||
|
||||
function getLookup (key: string, lookup: Lookup<any>, parent: Ref<Class<Doc>>): [Ref<Class<Doc>>, Ref<Class<Doc>>] | undefined {
|
||||
const parts = key.split('$lookup.').filter((p) => p.length > 0)
|
||||
const currentKey = parts[0].split('.').filter((p) => p.length > 0)[0]
|
||||
const current = (lookup as any)[currentKey]
|
||||
const nestedKey = parts.slice(1).join('$lookup.')
|
||||
if (nestedKey) {
|
||||
if (!Array.isArray(current)) {
|
||||
return
|
||||
}
|
||||
return getLookup(nestedKey, current[1], current[0])
|
||||
}
|
||||
if (Array.isArray(current)) {
|
||||
return [current[0], parent]
|
||||
}
|
||||
if (current === undefined && lookup._id !== undefined) {
|
||||
const reverse = (lookup._id as any)[currentKey]
|
||||
return reverse !== undefined ? [reverse, parent] : undefined
|
||||
}
|
||||
return current !== undefined ? [current, parent] : undefined
|
||||
}
|
||||
|
@ -406,8 +406,7 @@ async function createEmployeeAccount (account: Account, workspace: string): Prom
|
||||
if (existingAccount === undefined) {
|
||||
const employee = await ops.createDoc(contact.class.Employee, contact.space.Employee, {
|
||||
name,
|
||||
city: '',
|
||||
channels: []
|
||||
city: ''
|
||||
})
|
||||
|
||||
await ops.createDoc(contact.class.EmployeeAccount, core.space.Model, {
|
||||
@ -421,8 +420,7 @@ async function createEmployeeAccount (account: Account, workspace: string): Prom
|
||||
// Employee was deleted, let's restore it.
|
||||
const employeeId = await ops.createDoc(contact.class.Employee, contact.space.Employee, {
|
||||
name,
|
||||
city: '',
|
||||
channels: []
|
||||
city: ''
|
||||
})
|
||||
await ops.updateDoc(contact.class.EmployeeAccount, existingAccount.space, existingAccount._id, { employee: employeeId })
|
||||
}
|
||||
|
@ -25,13 +25,12 @@ import core, {
|
||||
FindOptions,
|
||||
FindResult,
|
||||
generateId,
|
||||
Hierarchy, ModelDb, Ref,
|
||||
Hierarchy, MeasureMetricsContext, ModelDb, Ref,
|
||||
SortingOrder,
|
||||
Space,
|
||||
Tx,
|
||||
TxOperations,
|
||||
TxResult,
|
||||
MeasureMetricsContext
|
||||
TxResult
|
||||
} from '@anticrm/core'
|
||||
import { createServerStorage, DbAdapter, DbConfiguration, FullTextAdapter, IndexedDoc } from '@anticrm/server-core'
|
||||
import { MongoClient } from 'mongodb'
|
||||
@ -277,7 +276,7 @@ describe('mongo operations', () => {
|
||||
rate: 20
|
||||
})
|
||||
|
||||
await operations.addCollection(taskPlugin.class.TaskComment, '' as Ref<Space>, docId, taskPlugin.class.Task, 'tasks', {
|
||||
const commentId = await operations.addCollection(taskPlugin.class.TaskComment, '' as Ref<Space>, docId, taskPlugin.class.Task, 'tasks', {
|
||||
message: 'my-msg',
|
||||
date: new Date()
|
||||
})
|
||||
@ -287,16 +286,34 @@ describe('mongo operations', () => {
|
||||
date: new Date()
|
||||
})
|
||||
|
||||
const r = await client.findAll<Task>(taskPlugin.class.TaskComment, {})
|
||||
expect(r.length).toEqual(2)
|
||||
|
||||
const r2 = await client.findAll<TaskComment>(taskPlugin.class.TaskComment, {}, {
|
||||
lookup: {
|
||||
attachedTo: taskPlugin.class.Task
|
||||
}
|
||||
})
|
||||
expect(r2.length).toEqual(2)
|
||||
console.log(JSON.stringify(r2, undefined, 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)
|
||||
})
|
||||
})
|
||||
|
@ -16,8 +16,7 @@
|
||||
import core, {
|
||||
Class,
|
||||
Doc,
|
||||
DocumentQuery, DOMAIN_MODEL, DOMAIN_TX, FindOptions,
|
||||
FindResult, Hierarchy, isOperator, Mixin, ModelDb, Ref, SortingOrder, Tx,
|
||||
DocumentQuery, DOMAIN_MODEL, DOMAIN_TX, FindOptions, FindResult, Hierarchy, isOperator, Lookup, Mixin, ModelDb, Ref, ReverseLookups, SortingOrder, Tx,
|
||||
TxCreateDoc,
|
||||
TxMixin, TxProcessor, TxPutBag,
|
||||
TxRemoveDoc,
|
||||
@ -32,6 +31,13 @@ function translateDoc (doc: Doc): Document {
|
||||
return doc as Document
|
||||
}
|
||||
|
||||
interface LookupStep {
|
||||
from: string
|
||||
localField: string
|
||||
foreignField: string
|
||||
as: string
|
||||
}
|
||||
|
||||
abstract class MongoAdapterBase extends TxProcessor {
|
||||
constructor (protected readonly db: Db, protected readonly hierarchy: Hierarchy, protected readonly modelDb: ModelDb, protected readonly client: MongoClient) {
|
||||
super()
|
||||
@ -76,6 +82,118 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
return translated
|
||||
}
|
||||
|
||||
private async getLookupValue<T extends Doc> (lookup: Lookup<T>, result: LookupStep[], parent?: string): Promise<void> {
|
||||
for (const key in lookup) {
|
||||
if (key === '_id') {
|
||||
await this.getReverseLookupValue(lookup, result, parent)
|
||||
continue
|
||||
}
|
||||
const value = (lookup as any)[key]
|
||||
const fullKey = parent !== undefined ? parent + '.' + key : key
|
||||
if (Array.isArray(value)) {
|
||||
const [_class, nested] = value
|
||||
const domain = this.hierarchy.getDomain(_class)
|
||||
if (domain !== DOMAIN_MODEL) {
|
||||
result.push({
|
||||
from: domain,
|
||||
localField: fullKey,
|
||||
foreignField: '_id',
|
||||
as: fullKey.split('.').join('') + '_lookup'
|
||||
})
|
||||
}
|
||||
await this.getLookupValue(nested, result, fullKey + '_lookup')
|
||||
} else {
|
||||
const _class = value as Ref<Class<Doc>>
|
||||
const domain = this.hierarchy.getDomain(_class)
|
||||
if (domain !== DOMAIN_MODEL) {
|
||||
result.push({
|
||||
from: domain,
|
||||
localField: fullKey,
|
||||
foreignField: '_id',
|
||||
as: fullKey.split('.').join('') + '_lookup'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getReverseLookupValue (lookup: ReverseLookups, result: LookupStep[], parent?: string): Promise<any | undefined> {
|
||||
const fullKey = parent !== undefined ? parent + '.' + '_id' : '_id'
|
||||
for (const key in lookup._id) {
|
||||
const as = parent !== undefined ? parent + key : key
|
||||
const value = lookup._id[key]
|
||||
const domain = this.hierarchy.getDomain(value)
|
||||
if (domain !== DOMAIN_MODEL) {
|
||||
const step = {
|
||||
from: domain,
|
||||
localField: fullKey,
|
||||
foreignField: 'attachedTo',
|
||||
as: as.split('.').join('') + '_lookup'
|
||||
}
|
||||
result.push(step)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async getLookups<T extends Doc> (lookup: Lookup<T> | undefined, parent?: string): Promise<LookupStep[]> {
|
||||
if (lookup === undefined) return []
|
||||
const result: [] = []
|
||||
await this.getLookupValue(lookup, result, parent)
|
||||
return result
|
||||
}
|
||||
|
||||
private async fillLookup<T extends Doc> (_class: Ref<Class<T>>, object: any, key: string, fullKey: string, targetObject: any): Promise<void> {
|
||||
if (targetObject.$lookup === undefined) {
|
||||
targetObject.$lookup = {}
|
||||
}
|
||||
const domain = this.hierarchy.getDomain(_class)
|
||||
if (domain !== DOMAIN_MODEL) {
|
||||
const arr = object[fullKey]
|
||||
targetObject.$lookup[key] = arr?.[0]
|
||||
} else {
|
||||
targetObject.$lookup[key] = this.modelDb.getObject(targetObject[key])
|
||||
}
|
||||
}
|
||||
|
||||
private async fillLookupValue<T extends Doc> (lookup: Lookup<T> | undefined, object: any, parent?: string, parentObject?: any): Promise<void> {
|
||||
if (lookup === undefined) return
|
||||
for (const key in lookup) {
|
||||
if (key === '_id') {
|
||||
await this.fillReverseLookup(lookup, object, parent, parentObject)
|
||||
continue
|
||||
}
|
||||
const value = (lookup as any)[key]
|
||||
const fullKey = parent !== undefined ? parent + key + '_lookup' : key + '_lookup'
|
||||
const targetObject = parentObject ?? object
|
||||
if (Array.isArray(value)) {
|
||||
const [_class, nested] = value
|
||||
await this.fillLookup(_class, object, key, fullKey, targetObject)
|
||||
await this.fillLookupValue(nested, object, fullKey, targetObject.$lookup[key])
|
||||
} else {
|
||||
await this.fillLookup(value, object, key, fullKey, targetObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async fillReverseLookup (lookup: ReverseLookups, object: any, parent?: string, parentObject?: any): Promise<void> {
|
||||
const targetObject = parentObject ?? object
|
||||
if (targetObject.$lookup === undefined) {
|
||||
targetObject.$lookup = {}
|
||||
}
|
||||
for (const key in lookup._id) {
|
||||
const value = lookup._id[key]
|
||||
const domain = this.hierarchy.getDomain(value)
|
||||
const fullKey = parent !== undefined ? parent + key + '_lookup' : key + '_lookup'
|
||||
if (domain !== DOMAIN_MODEL) {
|
||||
const arr = object[fullKey]
|
||||
targetObject.$lookup[key] = arr
|
||||
} else {
|
||||
const arr = await this.modelDb.findAll(value, { attachedTo: targetObject._id })
|
||||
targetObject.$lookup[key] = arr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async lookup<T extends Doc>(
|
||||
clazz: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
@ -83,29 +201,29 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
): Promise<FindResult<T>> {
|
||||
const pipeline = []
|
||||
pipeline.push({ $match: this.translateQuery(clazz, query) })
|
||||
const lookups = options.lookup as any
|
||||
for (const key in lookups) {
|
||||
const clazz = lookups[key]
|
||||
const domain = this.hierarchy.getDomain(clazz)
|
||||
if (domain !== DOMAIN_MODEL) {
|
||||
const step = {
|
||||
from: domain,
|
||||
localField: key,
|
||||
foreignField: '_id',
|
||||
as: key + '_lookup'
|
||||
}
|
||||
pipeline.push({ $lookup: step })
|
||||
}
|
||||
const steps = await this.getLookups(options.lookup)
|
||||
for (const step of steps) {
|
||||
pipeline.push({ $lookup: step })
|
||||
}
|
||||
if (options.sort !== undefined) {
|
||||
const sort = {} as any
|
||||
for (const _key in options.sort) {
|
||||
let key = _key as string
|
||||
if (_key.startsWith('$lookup.')) {
|
||||
key = key.replace('$lookup.', '')
|
||||
const keys = key.split('.')
|
||||
keys[0] = keys[0] + '_lookup'
|
||||
key = keys.join('.')
|
||||
const arr = key.split('.').filter((p) => p)
|
||||
key = ''
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const element = arr[i]
|
||||
if (element === '$lookup') {
|
||||
key += arr[++i] + '_lookup'
|
||||
} else {
|
||||
if (!key.endsWith('.') && i > 0) {
|
||||
key += '.'
|
||||
}
|
||||
key += arr[i]
|
||||
if (i !== arr.length - 1) {
|
||||
key += '.'
|
||||
}
|
||||
}
|
||||
}
|
||||
sort[key] = options.sort[_key] === SortingOrder.Ascending ? 1 : -1
|
||||
}
|
||||
@ -118,18 +236,8 @@ abstract class MongoAdapterBase extends TxProcessor {
|
||||
const cursor = this.db.collection(domain).aggregate(pipeline)
|
||||
const result = (await cursor.toArray()) as FindResult<T>
|
||||
for (const row of result) {
|
||||
const object = row as any
|
||||
object.$lookup = {}
|
||||
for (const key in lookups) {
|
||||
const clazz = lookups[key]
|
||||
const domain = this.hierarchy.getDomain(clazz)
|
||||
if (domain !== DOMAIN_MODEL) {
|
||||
const arr = object[key + '_lookup']
|
||||
object.$lookup[key] = arr[0]
|
||||
} else {
|
||||
object.$lookup[key] = this.modelDb.getObject(object[key])
|
||||
}
|
||||
}
|
||||
row.$lookup = {}
|
||||
await this.fillLookupValue(options.lookup, row)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
"author": "Anticrm Platform Contributors",
|
||||
"license": "EPL-2.0",
|
||||
"scripts": {
|
||||
"start": "MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin ts-node src/__start.ts",
|
||||
"start": "cross-env MONGO_URL=mongodb://localhost:27017 ELASTIC_URL=http://localhost:9200 MINIO_ENDPOINT=localhost MINIO_ACCESS_KEY=minioadmin MINIO_SECRET_KEY=minioadmin ts-node src/__start.ts",
|
||||
"build": "heft build",
|
||||
"lint:fix": "eslint --fix src",
|
||||
"bundle": "esbuild src/__start.ts --bundle --platform=node > bundle.js",
|
||||
@ -16,6 +16,7 @@
|
||||
"format": "prettier --write src && eslint --fix src"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"@anticrm/platform-rig": "~0.6.0",
|
||||
"@types/heft-jest": "^1.0.2",
|
||||
"@types/node": "^16.6.2",
|
||||
|
Loading…
Reference in New Issue
Block a user