...

Миграция данных из Creatio (Terrasoft) в ELMA365

Тема в разделе "Примеры решений и дополнительных модулей", создана пользователем ava_var, 29 апр 2022.

  1. ava_var

    ava_var Активный участник

    В этой статье рассмотрим, как мигрировать исторические данные из продукта Creatio (решение от Terrasoft).
    Для интеграции двух систем будем использовать протокол OData.
    Настроим Creatio
    Для интеграции будет использовать протокол OData
    Назначение протокола OData — выполнение запросов от внешних приложений к серверу баз данных Creatio.
    Приложение Creatio поддерживает протоколы OData 4 и OData 3. OData 4 предоставляет больше возможностей, чем OData 3. Основное отличие протоколов — ответ на запрос, возвращаемый сервером, имеет разный формат данных. Различия протоколов OData 3 и OData 4 описаны в официальной документации OData. При планировании интеграции с Creatio по протоколу OData необходимо использовать протокол версии 4.
    Ограничения при использовании протокола OData
    При использовании протокола OData необходимо учитывать следующие ограничения:
    • Невозможно создать системных пользователей.
    • Невозможно задать культуру возвращаемых данных. Культура определяется культурой пользователя от имени которого выполняется запрос.
    • Тело ответа на запрос может содержать максимум 20 000 строк.
    • Пакетный запрос может содержать максимум 100 подзапросов.
    • Максимальный размер файла, который можно загрузить с помощью запроса, задается системной настройкой [ MaxFileSize ] (по умолчанию — 10 Мб).

    Авторизация в Creatio
    Все внешние запросы к приложению Creatio должны быть аутентифицированы.
    Запросы должны выполняться под учетной записью, у которой есть права на OData.
    Права можно выставить следующим образом: перейти в настройки через правую панель.
    Затем пройти по пути Open System designer –> Users and administration –> Operation permissions –> Access to OData.
    Код:
    
    async function auththorize(): Promise<void> {
        try {
            const 
    resp await fetch(`${baseUrl}/ServiceModel/AuthService.svc/Login`, {
                
    method'POST',
                
    headers: {
                    
    "ForceUseSession""true",
                    
    "Content-Type""application/json"
                
    },
                
    bodyJSON.stringify({
                    
    "UserName"login,
                    
    "UserPassword"password
                
    })
            });
            const 
    res await resp.json();
            if (
    res.Code !== 0) {
                throw new 
    Error("authorization error: " res.Message);
            }
            const 
    cookieHeader resp.headers.get("set-cookie");
            if (!
    cookieHeader) {
                throw new 
    Error("auth headers not found");
            }
            
    await Namespace.storage.setItem("BPMCSRF_cookie"cookieHeader);
            const 
    start cookieHeader.indexOf("BPMCSRF");
            const 
    end cookieHeader.indexOf(";"start);
            const 
    csrf cookieHeader.substring(start 8end);
            
    await Namespace.storage.setItem("BPMCSRF"csrf);
        } catch (
    e) {
            throw 
    e;
        }
    }
    Рекомендуемым способом для запросов по протоколу OData является аутентификация на основе cookies (Forms-аутентификация), которая реализована с помощью веб-сервиса AuthService.svc.
    Первым запросом получаем cookie, затем добавляем в заголовки при последующих запросах
    Доступ к данным в Creatio
    Все доступные сущности Creatio можно получить по эндпоинту: ${baseUrl}/odata/
    Запрос к конкретной таблице формирует по протоколу OData, например к таблице Account: ${baseUrl}/odata/Account?$expand=Industry($select=Name),Country($select=Name),City($select=Name)&$top=50&$skip=0 - дополнительно к ответу с помощью $expand добавляются данные из таблиц Industry, Country и City, $select ограничивает список начитываемых полей.
    Код:
    
    async function getData(schemastring): Promise<Record<stringany>[] | undefined> {
        const 
    url = `${baseUrl}/odata/${schema}`;
        const 
    data = {
            
    headers: {
                
    "Accept""application/json",
                
    "Content-Type""application/json;odata=verbose",
                
    "ForceUseSession""true",
                
    "BPMCSRF"await Namespace.storage.getItem("BPMCSRF"),
                
    "Cookie"await Namespace.storage.getItem("BPMCSRF_cookie")
            },
        };
        try {
            const 
    resp await fetch(urldata as FetchRequest);
            if (
    resp.status === 500) {
                if (!
    Context.data.is500err) {
                    
    Context.data.is500err true;
                    return;
                } else {
                    throw new 
    Error(resp.status ": " resp.statusText)
                }
            }
            if (
    resp.status !== 200) {
                throw new 
    Error(resp.status ": " resp.statusText)
            }
            
    Context.data.is500err false;
            const 
    res await resp.json();
            
    Context.data.createdCount! += res.value.length;
            return 
    res.value;
        } catch (
    e) {
            throw 
    e;
        }
    }
    Особенности при работе c Creatio
    1. Если получение данных пакетами выполняется достаточно долго, то велика вероятность, что cookie станут невалидными, и вы получите ошибку 500. Отслеживайте это и выполняйте повторно команду авторизации.
    2. Если таблица содержит файл, при запросе к ней в ответе будет поле с названием типа Data@odata.mediaReadLink, в котором будет ссылка на файл, который можно получить по пути ${baseUrl}/odata/${row[‘Data@odata.mediaReadLink’]} (тоже с добавлением BPMCSRF и Cookie в заголовок).
    Код:
    
    const resp await fetch(`${baseUrl}/odata/${row["Data@odata.mediaReadLink"]}`, {
      
    headers: {
        
    "ForceUseSession""true",
        
    "BPMCSRF"await Namespace.storage.getItem("BPMCSRF"),
        
    "Cookie"await Namespace.storage.getItem("BPMCSRF_cookie")
       }
    } as 
    FetchRequest);
    const 
    file await Context.fields.files.create(row.Nameawait resp.arrayBuffer());
    Пример миграции контактов и компаний с сохранением связей.
    В Creatio контакт имеет ссылку на компанию AccountId. Поэтому мы сначала считаем данные о компаниях, затем уже о контактах.
    Во время получения данных о компаниях в объект creatioAccountsIdsToElmaId будем сохранять сопоставления id компании в Creatio и id компании в ELMA365. Аналогично сделаем для сохранения связи между компаниями и отраслями в объекте industriesCreatioToElma.
    Получение компаний и контактов идет пакетами, так как в системе ELMA365 есть ограничение на время выполнения сценария. Будем ходить по циклу Шлюз – Блок transferAccounts до тех пор, пока контекстная переменная createdEnd не станет равна true.


    [​IMG]

    Листинг сценария бизнес-процесса
    Код:
    
    const baseUrl 'http://192.168.10.210:5389';
    const 
    login "Supervisor";
    const 
    password "Supervisor";
    let creatioAccountsIdsToElmaIdRecord<stringstring>;
    let industriesCreatioToElmaRecord<stringstring>;
    // TYPES..
    // Размеры пакетов настраиваются эмпирическим путем
    const accountBatchSize 60;
    const 
    contactBatchSize 70;
    function 
    initMaps() {
        
    creatioAccountsIdsToElmaId Context.data.creatioAccountsIdsToElmaStr JSON.parse(Context.data.creatioAccountsIdsToElmaStr) : {};
        
    industriesCreatioToElma Context.data.industriesCreatioToElmaStr JSON.parse(Context.data.industriesCreatioToElmaStr) : {};
    }
    async function transferAccounts(): Promise<void> {
        const 
    accountsSchema = `Account?$expand=Industry($select=Name),Country($select=Name),City($select=Name)&$top=${accountBatchSize}&$skip=${Context.data.createdCount}`;
        try {
            
    Context.data.iterationsCounter!++;
            
    initMaps();
            const 
    accounts await getData(accountsSchema);
            
    // при 500 попробуем еще раз
            
    if (!Context.data.is500err) {
                if (
    accounts && accounts.length) {
                    
    await parseAccountsToElma(accounts as Account[]);
                } else {
                    
    Context.data.createdEnd true;
                }
                
    Context.data.creatioAccountsIdsToElmaStr JSON.stringify(creatioAccountsIdsToElmaId);
                
    Context.data.industriesCreatioToElmaStr JSON.stringify(industriesCreatioToElma);
            }
        } catch (
    e) {
            throw 
    e;
        }
    }
    // Миграция контактов
    async function transferContacts(): Promise<void> {
        const 
    contactsSchema = `Contact?$top=${contactBatchSize}&$skip=${Context.data.createdCount}`;
        try {
            
    Context.data.iterationsCounter!++;
            
    initMaps();
            const 
    contacts await getData(contactsSchema);
            if (!
    Context.data.is500err) {
                if (
    contacts && contacts.length) {
                    
    await parseContactsToElma(contacts as Contact[]);
                } else {
                    
    Context.data.createdEnd true;
                }
            }
        } catch (
    e) {
            throw 
    e;
        }
    }
    // Конвертируем контакты из Creatio в ELMA365
    async function parseContactsToElma(rowsContact[]): Promise<void> {
        try {
            for (const 
    row of rows) {
                const 
    contact Context.fields.contact.app.create();
                
    contact.data.__name row.Name;
                
    contact.data._account = [];
                if (
    row.Facebook) {
                    
    contact.data._account.push({ loginrow.Facebook } as TAccount<AccountType.Facebook>);
                }
                if (
    row.LinkedIn) {
                    
    contact.data._account.push({ loginrow.LinkedIn } as TAccount<AccountType.Other>);
                }
                if (
    row.Twitter) {
                    
    contact.data._account.push({ loginrow.Twitter } as TAccount<AccountType.Other>);
                }
                if (
    row.Skype) {
                    
    contact.data._account.push({ loginrow.Skype } as TAccount<AccountType.Skype>);
                    
    contact.data._skype row.Skype;
                }
                if (
    row.Email) {
                    
    contact.data._email = { emailrow.EmailtypeEmailType.Work };
                }
                
    contact.data._fullname = { firstnamerow.Namelastnamerow.Surnamemiddlenamerow.MiddleName };
                
    contact.data._phone = [];
                if (
    row.Phone) {
                    
    contact.data._phone.push({ telrow.Phone.replace(/-|\s/g""), typePhoneType.Main });
                }
                if (
    row.MobilePhone) {
                    
    contact.data._phone.push({ telrow.MobilePhone.replace(/-|\s/g""), typePhoneType.Mobile });
                }
                if (
    row.HomePhone) {
                    
    contact.data._phone.push({ telrow.HomePhone.replace(/-|\s/g""), typePhoneType.Home });
                }
                
    contact.data._position row.JobTitle;
                if (
    row.AccountId !== "") {
                    const 
    elmaCompanyId creatioAccountsIdsToElmaId[row.AccountId];
                    if (
    elmaCompanyId) {
                        const 
    elmaCompany await Context.fields.company.app.search().where(=> f.__id.eq(elmaCompanyId)).first();
                        if (
    elmaCompany) {
                            
    contact.data._companies elmaCompany;
                        }
                    }
                }
                
    await contact.save();
            }
        } catch (
    e) {
            throw 
    e;
        }
    }
    // Конвертируем компании из Creatio в ELMA365
    async function parseAccountsToElma(rowsAccount[]): Promise<void> {
        try {
            for (const 
    row of rows) {
                const 
    company Context.fields.company.app.create();
                
    company.data.name row.Name;
                if (
    row.IndustryId !== "") {
                    const 
    id industriesCreatioToElma[row.IndustryId];
                    
    let industryApplicationItem<Application$_clients$otrasl$DataApplication$_clients$otrasl$Params> | undefined undefined;
                    if (
    id) {
                        
    industry await Context.fields.otrasl.app.search().where(=> f.__id.eq(id)).first();
                    }
                    if (!
    industry) {
                        
    industry Context.fields.otrasl.app.create();
                        
    industry.data.__name row.Industry.Name;
                        
    await industry.save();
                    }
                    
    company.data.otrasl industry;
                    
    industriesCreatioToElma[row.IndustryId] = industry.id;
                }
                
    company.data._address "";
                if (
    row.Country) {
                    
    company.data._address += `Country: ${row.Country.Name}`;
                }
                if (
    row.City) {
                    
    company.data._address += `City: ${row.City.Name}`;
                }
                if (
    row.Address) {
                    
    company.data._address += `Address: ${row.Address}`;
                }
                if (
    row.Phone) {
                    
    company.data._phone = { telrow.Phone.replace(/-|\s/g""), typePhoneType.Work };
                }
                
    company.data._website row.Web;
                
    await company.save();
                
    creatioAccountsIdsToElmaId[row.Id] = company.id;
            }
        } catch (
    e) {
            throw 
    e;
        }
    }
    // Команда авторизации в Creation
    async function auththorize(): Promise<void> {
        try {
            const 
    resp await fetch(`${baseUrl}/ServiceModel/AuthService.svc/Login`, {
                
    method'POST',
                
    headers: {
                    
    "ForceUseSession""true",
                    
    "Content-Type""application/json"
                
    },
                
    bodyJSON.stringify({
                    
    "UserName"login,
                    
    "UserPassword"password
                
    })
            });
            const 
    res await resp.json();
            if (
    res.Code !== 0) {
                throw new 
    Error("authorization error: " res.Message);
            }
            const 
    cookieHeader resp.headers.get("set-cookie");
            if (!
    cookieHeader) {
                throw new 
    Error("auth headers not found");
            }
            
    // Сохраняем куки, чтоб использовать их в след.запросах  
            
    await Namespace.storage.setItem("BPMCSRF_cookie"cookieHeader);
            const 
    start cookieHeader.indexOf("BPMCSRF");
            const 
    end cookieHeader.indexOf(";"start);
            const 
    csrf cookieHeader.substring(start 8end);
            
    await Namespace.storage.setItem("BPMCSRF"csrf);
        } catch (
    e) {
            throw 
    e;
        }
    }
    // Универсальный метод, чтоб получать данные из Creatio
    // настройки берутся из схемы
    async function getData(schemastring): Promise<Record<stringany>[] | undefined> {
        const 
    url = `${baseUrl}/odata/${schema}`;
        const 
    data = {
            
    headers: {
                
    "Accept""application/json",
                
    "Content-Type""application/json;odata=verbose",
                
    "ForceUseSession""true",
                
    "BPMCSRF"await Namespace.storage.getItem("BPMCSRF"),
                
    "Cookie"await Namespace.storage.getItem("BPMCSRF_cookie")
            },
        };
        try {
            const 
    resp await fetch(urldata as FetchRequest);
            if (
    resp.status === 500) {
                if (!
    Context.data.is500err) {
                    
    Context.data.is500err true;
                    return;
                } else {
                    throw new 
    Error(resp.status ": " resp.statusText)
                }
            }
            if (
    resp.status !== 200) {
                throw new 
    Error(resp.status ": " resp.statusText)
            }
            
    Context.data.is500err false;
            const 
    res await resp.json();
            
    Context.data.createdCount! += res.value.length;
            return 
    res.value;
        } catch (
    e) {
            throw 
    e;
        }
    }
    async function resetAllParams(): Promise<void> {
        
    Context.data.createdCount 0;
        
    Context.data.createdEnd false;
        
    Context.data.iterationsCounter 0;
        
    Context.data.is500err false;
    }
    async function resetCounter(): Promise<void> {
        
    Context.data.iterationsCounter 0;
    }
    Последнее редактирование: 29 апр 2022
  2. dev

    dev Новичок

    Здравствуйте, спасибо вам за статью, а вы случайно не разобрались как получить список System Users из Creatio? А так же вложения к Accounts?