import { BitrixEntityMapping, BitrixFieldMapping, BitrixSyncDoc, CopyValueOperation, CreateChannelOperation, CreateTagOperation, MappingOperation } from '@hcengineering/bitrix' import { Comment } from '@hcengineering/chunter' import contact, { Channel, EmployeeAccount } from '@hcengineering/contact' import core, { AnyAttribute, Class, Client, Data, Doc, generateId, Mixin, Ref, Space } from '@hcengineering/core' import tags, { TagElement, TagReference } from '@hcengineering/tags' import { getColorNumberByText } from '@hcengineering/ui' export function collectFields (fieldMapping: BitrixFieldMapping[]): string[] { const fields: string[] = ['ID'] for (const f of fieldMapping) { switch (f.operation.kind) { case MappingOperation.CopyValue: fields.push( ...Array.from(f.operation.patterns.map((it) => it.field).filter((it) => it !== undefined) as string[]) ) break case MappingOperation.CreateChannel: fields.push(...Array.from(f.operation.fields.map((it) => it.field).filter((it) => it !== undefined))) break case MappingOperation.CreateTag: fields.push(...Array.from(f.operation.fields.map((it) => it.field).filter((it) => it !== undefined))) break } } return fields } export interface ConvertResult { document: BitrixSyncDoc // Document we should achive mixins: Record>, Data> // Mixins of document we will achive extraDocs: Doc[] // Extra documents we will achive, comments etc. blobs: File[] // comments?: Promise> } export async function convert ( client: Client, entity: BitrixEntityMapping, space: Ref, fields: BitrixFieldMapping[], rawDocument: any, prevExtra: Doc[], // <<-- a list of previous extra documents, so for example TagElement will be reused, if present for more what one item and required to be created tagElements: Map>, TagElement[]>, // TagElement cache. userList: Map> ): Promise { const hierarchy = client.getHierarchy() const document: BitrixSyncDoc = { _id: generateId(), type: entity.type, bitrixId: `${rawDocument.ID as string}`, _class: entity.ofClass, space, modifiedOn: new Date(rawDocument.DATE_CREATE).getTime(), modifiedBy: userList.get(rawDocument.CREATED_BY_ID) ?? core.account.System } // Obtain a proper modified by for document const newExtraDocs: Doc[] = [] const blobs: File[] = [] const mixins: Record>, Data> = {} const extractValue = (field?: string, alternatives?: string[]): any | undefined => { if (field !== undefined) { let lval = rawDocument[field] if ((lval == null || lval === '') && alternatives !== undefined) { for (const alt of alternatives) { lval = rawDocument[alt] if (lval != null) { break } } } const bfield = entity.bitrixFields[field] if (bfield === undefined) { console.trace('Bitrix field not found', field) } else if (bfield.type === 'integer' || bfield.type === 'double') { if (bfield.isMultiple && Array.isArray(lval)) { return lval[0] ?? 0 } return lval } else if (bfield.type === 'crm_multifield') { if (Array.isArray(lval)) { return lval.map((it) => it.VALUE) } } else if (bfield.type === 'string' || bfield.type === 'url') { if (bfield.isMultiple && Array.isArray(lval)) { return lval.join(', ') } return lval } else if (bfield.type === 'date') { if (lval !== '' && lval != null) { return new Date(lval) } } else if (bfield.type === 'char') { return lval === 'Y' } else if (bfield.type === 'enumeration' || bfield.type === 'crm_status') { if (lval != null && lval !== '') { if (bfield.isMultiple && Array.isArray(lval)) { lval = lval[0] ?? '' } const eValue = bfield.items?.find((it) => it.ID === lval)?.VALUE if (eValue !== undefined) { return eValue } } } } } const getCopyValue = async (attr: AnyAttribute, operation: CopyValueOperation): Promise => { const r: Array = [] for (const o of operation.patterns) { if (o.text.length > 0) { r.push(o.text) } const lval = extractValue(o.field, o.alternatives) if (lval != null) { r.push(lval) } } if (r.length === 1) { return r[0] } if (r.length === 0) { return } return r.join('').trim() } const getChannelValue = async (attr: AnyAttribute, operation: CreateChannelOperation): Promise => { for (const f of operation.fields) { const lval = extractValue(f.field) if (lval != null && lval !== '') { const vals = Array.isArray(lval) ? lval : [lval] for (const llVal of vals) { const c: Channel = { _id: generateId(), _class: contact.class.Channel, attachedTo: document._id, attachedToClass: attr.attributeOf, collection: attr.name, modifiedBy: document.modifiedBy, value: llVal, provider: f.provider, space: document.space, modifiedOn: document.modifiedOn } newExtraDocs.push(c) } } } return undefined } const getTagValue = async (attr: AnyAttribute, operation: CreateTagOperation): Promise => { const elements = tagElements.get(attr.attributeOf) ?? (await client.findAll(tags.class.TagElement, { targetClass: attr.attributeOf })) const references = await client.findAll(tags.class.TagReference, { attachedTo: document._id }) // Add tags creation requests from previous conversions. elements.push(...prevExtra.filter((it) => it._class === tags.class.TagElement).map((it) => it as TagElement)) tagElements.set(attr.attributeOf, elements) const defaultCategory = await client.findOne(tags.class.TagCategory, { targetClass: attr.attributeOf, default: true }) if (defaultCategory === undefined) { console.error('could not proceed tags without default category') return } for (const o of operation.fields) { const lval = extractValue(o.field) let vals: string[] = [] if (lval == null) { continue } if (o.split !== '' && o.split != null) { vals = `${lval as string}`.split(o.split) } else { vals = [lval as string] } for (let vv of vals) { vv = vv.trim() if (vv === '') { continue } // Find existing element and create reference based on it. let tag: TagElement | undefined = elements.find((it) => it.title === vv) if (tag === undefined) { tag = { _id: generateId(), _class: tags.class.TagElement, category: defaultCategory._id, color: getColorNumberByText(vv), description: '', title: vv, targetClass: attr.attributeOf, space: tags.space.Tags, modifiedBy: document.modifiedBy, modifiedOn: document.modifiedOn } newExtraDocs.push(tag) } const ref: TagReference = { _id: generateId(), attachedTo: document._id, attachedToClass: attr.attributeOf, collection: attr.name, _class: tags.class.TagReference, tag: tag._id, color: getColorNumberByText(vv), title: vv, weight: o.weight, modifiedBy: document.modifiedBy, modifiedOn: document.modifiedOn, space: tags.space.Tags } if (references.find((it) => it.title === vv) === undefined) { // Add only if not already added newExtraDocs.push(ref) } } } return undefined } for (const f of fields) { const attr = hierarchy.getAttribute(f.ofClass, f.attributeName) if (attr === undefined) { console.trace('Attribue not found', f) continue } let value: any switch (f.operation.kind) { case MappingOperation.CopyValue: value = await getCopyValue(attr, f.operation) break case MappingOperation.CreateChannel: value = await getChannelValue(attr, f.operation) break case MappingOperation.CreateTag: value = await getTagValue(attr, f.operation) break } if (value !== undefined) { if (hierarchy.isMixin(attr.attributeOf)) { mixins[attr.attributeOf] = { ...mixins[attr.attributeOf], [attr.name]: value } } else { ;(document as any)[attr.name] = value } } } return { document, mixins, extraDocs: newExtraDocs, blobs } } /** * @public */ export function toClassRef (val: any): Ref> { return val as Ref> }