Імпорт даних і демодані
Замість ручної роботи віддавайте перевагу навичці author-import-package (у .claude/skills/ вашого
згенерованого репозиторію).
Kandra має одну підсистему для завантаження даних у конфігурацію: пакет імпорту (JSON-файл), який застосовує ідемпотентний рушій імпорту. Той самий формат і той самий рушій виконують дві задачі:
- Демодані. Ваша конфігурація постачає демокомпанію як пакет із
"origin": "demoSeed". Демоінсталяція застосовує його й отримує заповнену базу даних. - Реальний імпорт. Адміністратор, виклик ендпоінта або ваш власний обробник даних застосовує пакет
із
"origin": "import": довідкові дані зі старої системи, прайс-лист постачальника, нічне завантаження.
Кожен рядок, який пише рушій, проходить через справжні CRUD-сервіси: поведінки, валідатори, нумерація, проведення й авторизація працюють точнісінько так, ніби користувач увів дані вручну. Чорного ходу в таблиці немає.
Довідкова сторінка Формат пакета імпорту описує кожне поле й правило. Ця сторінка пояснює, як усе поєднується, і розбирає реальні приклади.
Поняття
Що тут означає «ідемпотентний»
Той самий пакет можна застосовувати скільки завгодно разів. Другий запуск нічого не змінює; запуск після того, як ви змінили один об'єкт у пакеті, змінює цей об'єкт і більше нічого. Це працює, тому що:
- кожен об'єкт у пакеті має
$id, унікальний у межах пакета; - пакет має власний ідентифікатор (
"package": "acme-demo"); - кожен рядок, який створює рушій, позначається цією парою, а також хешем об'єкта в тому вигляді, як він записаний у пакеті.
Під час наступного запуску рушій знаходить рядок за ідентифікатором пакета й $id, порівнює хеш і
повідомляє один результат на кожен об'єкт: Inserted, Unchanged, Updated або один із
результатів Skipped*, якщо користувач відтоді змінив або видалив рядок. Що рушій робить у кожному
випадку, визначають політики кроку; типові політики захищають зміни користувачів (див.
Політики).
Глобальної транзакції немає. Кожен об'єкт записується у власній одиниці роботи, як це зробив би один HTTP-запит, і запуск, що завершився помилкою, можна просто повторити: він продовжить з того місця, де зупинився.
Позначка імпорту
Позначка зберігається в самому рядку, у стовпцях відстеження змін, які вже має кожна сутність
(стовпці Import_*, частина ChangesInfo):
| Поле | Значення |
|---|---|
Origin | Import або DemoSeed (із заголовка пакета) |
Source | Ідентифікатор пакета |
Key | $id об'єкта |
SourceHash | SHA-256 об'єкта в тому вигляді, як він записаний у пакеті |
ImportedAt | Коли рушій востаннє записав рядок |
Оскільки позначка є частиною рядка, вона переживає м'яке видалення й зникає разом із жорстким.
«Змінено користувачем після імпорту» — це просто Modified > ImportedAt, а звичайне збереження з UI
позначку зберігає. Окремої таблиці-журналу, яку треба синхронізувати, немає.
Де це видно в UI
- Сторінка перегляду запису, вкладка «Інформація про зміни»: блок Імпорт даних із походженням (Імпорт / Демонстраційні дані), пакетом, ключем, часом імпорту та ознакою Змінено після імпорту.
- Таблиці історії змін (та історія константи): стовпець Походження. Кожна зміна позначена як Користувач, API-ключ, MCP-агент, Заплановане завдання, Імпорт, Демонстраційні дані або Система. Для рядків «Імпорт» і «Демонстраційні дані» позначка веде на звіт відповідного запуску імпорту (лише для адміністраторів).
- Адміністрування → Імпорт даних (
/admin/import): вбудовані пакети з їхньою версією та застосованою версією, кнопки Пробний запуск та Імпортувати, звіт кожного запуску та історія запусків. Кнопка звіту Зміни, внесені цим запуском показує кожну подію зміни, яку створив запуск.
Покроковий приклад: розширення демопакета Kandra WMS
Еталонна конфігурація постачає свою демокомпанію як
KandraWms.Application/ImportPackages/kandrawms-demo-uk.json (27 кроків: одиниці виміру, філія,
склади, контрагенти, номенклатура, потім документи). Ваша згенерована конфігурація має таке саме місце,
Acme.Application/ImportPackages/acme-demo.json, поки що без кроків. Кроки нижче використовують пакет
WMS, бо в ньому є дані. До вашого вони застосовуються без змін.
1. Додайте склад і прибуткову накладну
Кроки виконуються по черзі, тож новий об'єкт іде після всього, на що він посилається. Додайте склад до
наявного кроку Warehouses:
{ "dictionary": "Warehouses", "items": [
{ "$id": "wh-main", "code": "WH-MAIN", "name": "Головний склад", "branchId": { "$ref": "br-kyiv01" } },
{ "$id": "wh-lviv", "code": "WH-LVIV", "name": "Склад Львів", "branchId": { "$ref": "br-kyiv01" } }
] }
і надходження на нього в кроці GoodsReceipts:
{ "$id": "gr-demo-11", "code": "GR-DEMO-11", "date": "$today-5d",
"counterpartyId": { "$ref": "cp-sup-roshen" },
"toWarehouseId": { "$ref": "wh-lviv" },
"lines": [ { "itemId": { "$ref": "itm-sugar" }, "quantity": 40, "price": 28 } ] }
На що звернути увагу:
- Назви — це назви з каталогу (
Warehouses,GoodsReceipts:Nameатрибута[Kandra*Form]кожного Dto), а властивості — це імена Dto на дроті (camelCase, як в API). Відкрийте згенеровану JSON Schema у своєму редакторі, і ви отримаєте автодоповнення для обох (див. JSON Schema). { "$ref": "wh-lviv" }вказує на об'єкт, визначений раніше в тому самому пакеті. Щоб послатися на рядок, який уже є в базі даних, використовуйте{ "$code": "WH-MAIN" }."$today-5d"— це токен дати. Він обчислюється під час застосування пакета, тож демодані завжди виглядають свіжими.- Крок має
"submit": true, тому накладна додається проведеною й проводиться так само, як та, яку провів користувач.
Збільшуйте "version" пакета, коли змінюєте його вміст. Демоінсталяція застосовує нову версію після
оновлення (див. Демодані). На ручний запуск версія не впливає.
2. Пробний запуск
Відкрийте Адміністрування → Імпорт даних, оберіть kandrawms-demo-uk і натисніть Пробний запуск.
Пробний запуск розбирає пакет, перевіряє його, розв'язує кожне посилання, шукає кожен рядок і виконує
перевірку ліцензії, а потім нічого не записує. На базі даних, де вже є попередня версія, ви отримаєте:
| Результат | Кількість | Чому |
|---|---|---|
| Unchanged | усе інше | Той самий хеш, що й минулого разу |
| Inserted | 2 | wh-lviv і gr-demo-11 нові |
Друкарська помилка з'являється тут, а не посеред реального запуску, як діагностика зі шляхом у JSON:
steps[13].items[10].toWarehouseId: Unknown or later-defined "$ref" "wh-lvov".
3. Запустіть, потім запустіть ще раз
Кнопка Імпортувати стає доступною, щойно пробний запуск того самого джерела пройшов без помилок.
Реальний запуск повідомляє ті самі два додавання; новий склад і накладна тепер існують, і накладна
проведена. Запустіть ще раз, і кожен об'єкт буде Unchanged. Другий запуск нічого не записує, тож і
подій змін не створює.
4. Змініть об'єкт, запустіть ще раз
Перейменуйте склад у пакеті ("name": "Склад Львів (центр)") і змініть кількість у накладній на 50,
потім запустіть:
| Ключ | Результат | Чому |
|---|---|---|
wh-lviv | Updated | Кроки довідників за замовчуванням мають "onChange": "update" |
gr-demo-11 | SkippedDrift | Кроки документів за замовчуванням мають "onChange": "skip": змінений документ пакета потрапляє у звіт, але не перезаписується |
Щоб зміни документів застосовувалися, задайте "onChange": "update" для цього кроку. Оновлення
проведеного документа скасовує його проведення, зберігає новий вміст і проводить його знову, тож його
рухи по регістрах і проведення перераховуються (див. Підводні камені).
Тепер змініть склад в UI і запустіть пакет знову після ще однієї зміни пакета: результат буде
SkippedUserModified. Типова політика "onUserModified": "skip" зберігає зміну користувача. Те саме
стосується рядка, який користувач видалив (SkippedDeleted, політика onDeleted).
Точки входу
Сторінка адміністрування
Адміністрування → Імпорт даних (/admin/import, лише для адміністраторів) показує пакети, вбудовані у
вашу конфігурацію, а також приймає завантажений файл. Файл завантажується один раз, і пробний, і
реальний запуск використовують це завантаження. Звіт кожного запуску показує кількість за кожним
результатом як чипи-фільтри, таблицю об'єктів і діагностику. /admin/import?run=<run id> відкриває звіт
запуску напряму.
Ендпоінти
Ті самі операції доступні на api/v1/Import, лише для ролі admin. Запуски асинхронні:
POST .../run одразу повертає 202 з ідентифікатором запуску, а звіт ви отримуєте опитуванням.
# Packages embedded in the configuration, with the version last applied
curl -H "X-Api-Key: kdr_..." https://acme.example.com/api/v1/Import/packages
# Dry run of an embedded package
curl -X POST -H "X-Api-Key: kdr_..." -H "Content-Type: application/json" \
-d '{"package":"acme-demo","dryRun":true}' https://acme.example.com/api/v1/Import/run
# Run an uploaded file (multipart: file + dryRun + continueOnError)
curl -X POST -H "X-Api-Key: kdr_..." -F file=@prices.json -F dryRun=false -F continueOnError=true \
https://acme.example.com/api/v1/Import/run
# Status and report of one run; recent runs of one package
curl -H "X-Api-Key: kdr_..." https://acme.example.com/api/v1/Import/runs/01a0f801-5869-70e4-80c3-fb95e8f47b14
curl -H "X-Api-Key: kdr_..." "https://acme.example.com/api/v1/Import/runs?package=acme-demo"
API-ключ має належати адміністратору (див. розділ про API-ключі на сторінці
Identity & Auth, англійською). Другий запуск, поки
триває перший, отримує 409. GET .../schema повертає JSON Schema пакетів вашої конфігурації. Розмір
завантаження обмежує Import:MaxPackageSizeBytes (за замовчуванням 50 МБ).
З коду
IDataImportService (простір імен Kandra.DataImport) — це сам рушій. Його можна впровадити через
конструктор, і він працює від імені користувача, який його викликає, синхронно, у запиті того, хто
викликає:
var report = await import.ImportAsync(stream, new ImportOptions(DryRun: false, ContinueOnError: true), cancellationToken);
if (report.HasErrors) { /* report.Diagnostics, report.Items.Where(i => i.Outcome == ImportItemOutcome.Failed) */ }
var inserted = report.Counts.GetValueOrDefault(ImportItemOutcome.Inserted);
ImportOptions має три параметри: DryRun, ContinueOnError (коли false, як за замовчуванням,
запуск зупиняється на першому об'єкті з помилкою) і RunId (зазвичай залишається порожнім).
IImportRunService.Start(...) запускає той самий рушій у фоні від імені користувача, який почав запуск,
і одразу повертає ідентифікатор запуску; саме його використовують сторінка адміністрування й ендпоінти.
Для реального (не пробного) запуску обидва шляхи записують запуск в історію пакета разом зі звітом, тож
запуски з вашого коду теж з'являються на сторінці адміністрування.
Автоматизований імпорт — це ваш власний обробник даних
Платформа не постачає ні завдання імпорту, ні конвертерів файлових форматів. Автоматизація імпорту — це
обробник даних, який належить вашій конфігурації: він отримує
дані, за потреби перетворює їх на пакет і викликає IDataImportService. Обробник вирішує все, що
залежить від ваших даних: звідки береться файл, обмеження розміру, чи продовжувати після помилок, чи
пропонувати пробний запуск. Запис пакета й робота рушія — це справа платформи.
Робочий приклад: номенклатура з CSV-файлу
Еталонна конфігурація має ImportItemsCsv, який приймає CSV-файл із заголовком
code;name;unit;parent і імпортує рядки як номенклатуру. Це звичайний обробник даних: вхідний Dto,
Dto результату, валідатор і поведінка.
// Acme.Forms/DataProcessors/ImportItemsCsv.cs
[KandraDataProcessorForm(Name = "ImportItemsCsv", ValidatorType = typeof(ImportItemsCsvValidator), ResultDtoType = typeof(ImportItemsCsvResult))]
[Description("Imports items from a CSV file (header row: code;name;unit;parent).")]
public class ImportItemsCsvDto : IDataProcessorInputDto
{
[Caption("ImportItemsCsv_File")]
[FilePicker(Extensions = "*.csv")]
public Guid? FileId { get; set; }
[Caption("Import_DryRun")]
public bool DryRun { get; set; }
}
public class ImportItemsCsvResult : IDataProcessorResultDto
{
public Guid RunId { get; set; } // no [Caption]: not shown
[Order(1)] [Caption("Import_Outcome_Inserted")] public int Inserted { get; set; }
[Order(2)] [Caption("Import_Outcome_Updated")] public int Updated { get; set; }
[Order(3)] [Caption("Import_Outcome_Unchanged")] public int Unchanged { get; set; }
[Order(4)] [Caption("ImportItemsCsv_Skipped")] public int Skipped { get; set; }
[Order(5)] [Caption("Import_Outcome_Failed")] public int Failed { get; set; }
[Order(6)] [Caption("Import_Message")] public string? Message { get; set; }
}
Підписи Import_* — це власні ключі локалізації рушія (їх використовує сторінка адміністрування), тож
записи у ваших resx-файлах потрібні лише для двох ключів ImportItemsCsv_* і назви обробника.
Поле файлу — це Guid? з [FilePicker], як на будь-якій формі (див.
Зберігання файлів). На сторінці обробника вибирач завантажує обраний файл,
коли користувач натискає Виконати, і до моменту запуску вашої поведінки FileId уже містить
ідентифікатор посилання. Не робіть його обов'язковим у валідаторі: форма перевіряється до
завантаження, коли FileId ще порожній, тож правило обов'язковості завжди спрацьовувало б. Перевіряйте
його в поведінці.
// Acme.Application/DataProcessors/ImportItemsCsv.cs (excerpt)
[KandraDataProcessorBehavior(typeof(ImportItemsCsvDto))]
public class ImportItemsCsvBehavior(IBlobService blobs, IDataImportService import, IStringLocalizer localizer)
: IDataProcessorBehavior<ImportItemsCsvDto, ImportItemsCsvResult>
{
public const string PackageId = "acme-items-csv";
private const int MaxFileChars = 5 * 1024 * 1024;
public string ObjectName => "ImportItemsCsv";
public ValueTask OnNewAsync(ImportItemsCsvDto input, CancellationToken cancellationToken) => ValueTask.CompletedTask;
public async Task<ImportItemsCsvResult> OnProcessAsync(ImportItemsCsvDto input, CancellationToken cancellationToken)
{
if (input.FileId is not { } fileId)
{
throw new InvalidStateException(localizer["ImportItemsCsv_NoFile"]);
}
string text;
var (content, _, _) = await blobs.OpenReadAsync(fileId, cancellationToken);
await using (content)
{
using var reader = new StreamReader(content, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
text = await reader.ReadToEndAsync(cancellationToken);
}
// The processor sets its own limits: the engine accepts whatever package it is given.
if (text.Length > MaxFileChars)
{
throw new InvalidStateException(localizer["ImportItemsCsv_TooLarge"]);
}
var package = ToPackage(text); // CSV -> kandra-import/1, below
using var stream = new MemoryStream(JsonSerializer.SerializeToUtf8Bytes(package));
var report = await import.ImportAsync(stream, new ImportOptions(DryRun: input.DryRun, ContinueOnError: true), cancellationToken);
var counts = report.Counts;
int Count(params ImportItemOutcome[] outcomes) => outcomes.Sum(o => counts.GetValueOrDefault(o));
return new ImportItemsCsvResult
{
RunId = report.RunId,
Inserted = Count(ImportItemOutcome.Inserted, ImportItemOutcome.Adopted),
Updated = Count(ImportItemOutcome.Updated, ImportItemOutcome.Restored),
Unchanged = Count(ImportItemOutcome.Unchanged),
Skipped = Count(ImportItemOutcome.SkippedUserModified, ImportItemOutcome.SkippedDrift, ImportItemOutcome.SkippedDeleted),
Failed = Count(ImportItemOutcome.Failed),
Message = report.Diagnostics.FirstOrDefault(d => d.Severity == ImportDiagnosticSeverity.Error)?.Message
?? report.Items.FirstOrDefault(i => i.Outcome == ImportItemOutcome.Failed)?.Message,
};
}
}
Перетворення — звичайний код на System.Text.Json.Nodes. Кожен рядок CSV стає одним об'єктом кроку
Items:
var item = new JsonObject
{
["$id"] = "item:" + code, // stable: derived from the business key, not the row number
["isFolder"] = false,
["code"] = code,
["name"] = name,
["category"] = "goods",
["unitOfMeasureId"] = new JsonObject { ["$code"] = unit },
};
if (parent.Length > 0)
{
item["parentId"] = new JsonObject { ["$code"] = parent }; // an existing item folder, by code
}
var package = new JsonObject
{
["format"] = "kandra-import/1",
["package"] = PackageId,
["version"] = 1,
["origin"] = "import",
["configuration"] = "Acme",
["steps"] = new JsonArray
{
new JsonObject { ["dictionary"] = "Items", ["match"] = new JsonArray("code"), ["items"] = items },
},
};
Рішення, що стоять за цими кількома рядками:
$idбереться з коду номенклатури, тож той самий рядок у файлі наступного тижня — це той самий об'єкт, а змінена назва повертається якUpdated.$id, побудований із номера рядка, перетворив би кожен файл зі зміненим порядком на мішанину оновлень."match": ["code"]дозволяє першому запуску прив'язатися до номенклатури, яку користувач уже ввів вручну (результатAdopted), замість помилки через дублікат коду. Match розглядає лише рядки без позначки імпорту.- Пошук через
$codeдля одиниці виміру й батьківської папки означає, що файл говорить мовою користувача (кодами), а не ідентифікаторами бази даних. Рядки, на які він посилається, мають уже існувати. ContinueOnError: trueімпортує правильні рядки й повідомляє про погані. Для файлу, де частковий імпорт гірший за жоден, використовуйтеfalseразом із попереднім пробним запуском.
Запуск на базі даних, де є папка GOODS-FOOD і одиниця PCS: перший запуск повідомив Inserted: 2,
другий — Unchanged: 2. Тести еталонної конфігурації також покривають прив'язаний рядок і змінений
рядок, що повертається як Updated із батьківською папкою, заданою через $code. Обидва запуски
з'явилися в Історії запусків на сторінці Адміністрування → Імпорт даних разом зі звітами. Пробний
запуск із вашого обробника повертає звіт вам, але в історію не записується.
Усе інше обробник отримує безкоштовно: свою сторінку, дозвіл ExecuteImportItemsCsv і місце в списку
обробників планувальника завдань.
Планування
Обробник запускається за розкладом як заплановане завдання: тригер Cron
або Інтервал, від імені обраного користувача (Запуск від імені). Запуск за розкладом не може
запитати вхідні дані. Виконавець викликає крок New обробника й виконує результат, тож кожне вхідне поле
має мати придатне значення за замовчуванням. У вибирача файлу його немає: запуск ImportItemsCsv за
розкладом отримує FileId == null і завершується помилкою «Choose a CSV file».
Тригер відстежуваної папки, який забирає файли, покладені в папку, і передає їх обробнику, поки недоступний. До того часу плануйте обробники, які самі отримують свої дані (наступний розділ), а обробники, що працюють із файлами, запускайте вручну.
URL-джерело на тригері Cron
Дані, опубліковані за URL (прайс-лист постачальника, залишки партнера), добре лягають на тригер Cron: обробнику не потрібні вхідні дані. Форма, як ескіз (у еталонній конфігурації цього немає):
public async Task<SupplierPricesResult> OnProcessAsync(SupplierPricesDto input, CancellationToken cancellationToken)
{
var options = feedOptions.Value; // IOptions<SupplierFeedOptions>, bound from configuration
if (options.Url.Scheme != Uri.UriSchemeHttps)
{
throw new InvalidStateException("The supplier feed must be an https:// URL.");
}
using var request = new HttpRequestMessage(HttpMethod.Get, options.Url);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", options.Token);
var lastEtag = (await constants.GetAsync<SupplierFeedEtagConstant>(null, cancellationToken)).Value;
if (!string.IsNullOrEmpty(lastEtag))
{
request.Headers.IfNoneMatch.Add(EntityTagHeaderValue.Parse(lastEtag));
}
using var response = await http.SendAsync(request, cancellationToken); // IHttpClientFactory client
if (response.StatusCode == HttpStatusCode.NotModified)
{
return new SupplierPricesResult { NotModified = true };
}
response.EnsureSuccessStatusCode();
var package = ToPackage(await response.Content.ReadAsStringAsync(cancellationToken));
// ... ImportAsync as in the CSV example ...
if (response.Headers.ETag is { } etag)
{
await constants.SetAsync<string>("SupplierFeedEtag", etag.ToString(), FixedDate, cancellationToken);
}
// ...
}
- Лише HTTPS. Відхиляйте в обробнику все інше, щоб помилка в налаштуваннях не могла надіслати токен відкритим текстом.
- Тримайте секрет поза кодом і поза базою даних. Прив'язуйте URL і токен із конфігурації
(
SupplierFeed__Tokenяк змінна середовища, user secrets під час розробки), так само як секрет JWT (Розгортання). Не кладіть його у вхідний Dto: запуск за розкладом отримує лише значення за замовчуванням ізNew, а кожне вхідне поле надсилається в браузер і показується на сторінці обробника. - Пропуск незміненого вмісту — це оптимізація, а не виправлення коректності. Повторний імпорт тих
самих даних дає лише результати
Unchanged, бо рушій і так ідемпотентний.ETag(або SHA-256 тіла, якщо сервер не надсилаєETag) економить завантаження й запуск. Константа — природне місце для цього одного значення. Записуйте її з фіксованою датою дії, щоб кожен запис виправляв той самий рядок, а не додавав історію (constantsтут — цеIConstantsManageService). Записи перевіряються на дозволи, тож користувачу «Запуск від імені» завдання потрібен дозвіл на константи.
Демодані
Демокомпанія конфігурації — це вбудований пакет із "origin": "demoSeed". У згенерованій конфігурації
все вже підключено:
<!-- Acme.Application.csproj -->
<EmbeddedResource Include="ImportPackages\*.json" Exclude="ImportPackages\*.schema.json" />
// Acme.Application/ConfigureServices.cs
services.AddKandraDataImport("Acme");
services.AddImportPackages(typeof(ConfigureServices).Assembly); // every ImportPackages/*.json with format kandra-import/1
services.AddSingleton<IDemoUserProvider, AcmeDemoUsers>();
services.AddKandraDemoSeeding<DomainUser, DomainRole>();
Заповніть ImportPackages/acme-demo.json кроками; сторінка адміністрування одразу його покаже.
Демокористувачі
Звичайна інсталяція створює admin і двох службових користувачів (mcp-agent, jobs-scheduler) і
жодних зразкових користувачів. Зразкові користувачі існують лише в демоінсталяції. Вони беруться з
вашого IDemoUserProvider, а не з пакета, бо користувачу потрібен пароль і в нього немає рядка
відстеження змін, який можна позначити:
internal sealed class AcmeDemoUsers : IDemoUserProvider
{
public IReadOnlyList<DemoUser> GetDemoUsers() =>
[
new DemoUser("user0", "user0@example.com", "User!0", ["user"]),
new DemoUser("user1", "user1@example.com", "User!1", ["user"]),
];
}
Ролі мають уже існувати (сідер створює admin, employee і user).
Обмеження безкоштовного тарифу
Демо має вміщатися в безкоштовний тариф, і рушій перевіряє це, перш ніж щось записати:
- Користувачі: щонайбільше п'ять разом з
admin(службові користувачі не враховуються). Демосідинг рахує наявних користувачів плюс ваших демокористувачів і відмовляється перевищувати ліміт, тож тримайте список на рівні чотирьох або менше. - Документи на місяць і рядки на документ: попередня перевірка ліцензії рушієм імпорту рахує документи, які створив би пакет. Кожен із них створюється в поточному місяці, хоч би що казав його токен дати. Пакет, що перевищує ліміт, не проходить пробний запуск із помилкою, замість того щоб зупинитися на півдорозі.
Як демоінсталяція заповнюється й залишається актуальною
Роботу виконує IDemoSeeding (реєструється через AddKandraDemoSeeding):
SeedAsync(packageId)застосовує демопакет (за замовчуванням перший вбудований пакет із походженнямdemoSeed), створює демокористувачів і записує маркер (DemoInstallation: пакет і дата). Щойно маркер існує, метод нічого не робить, тож демокористувача, якого хтось видалив, ніколи не буде створено знову. Пакет із помилками не записує ні маркер, ні користувачів, тож повторний виклик продовжує роботу.GetInstallationAsync()читає маркер:nullозначає, що це не демоінсталяція.ApplyPendingUpdateAsync()виконується під час кожного запуску застосунку. У демоінсталяції, чий вбудований демопакет має вищуversion, ніж застосована, він застосовує пакет знову. Типові політики додають нове й зберігають те, що змінили користувачі. Саме тому треба збільшувати версію щоразу, коли ви змінюєте демопакет. Помилка тут записується в журнал і ніколи не блокує запуск.
Майстер першого запуску (EULA, пароль адміністратора, «реальна компанія чи демо») і автоматичний
демо-режим для хостованих демо поки недоступні. Обидва викликатимуть SeedAsync.
Підводні камені
$id— назавжди. Позначка прив'язує рядок до ідентифікатора пакета й$id. Перейменуйте$id, і рушій побачить новий об'єкт (другий рядок або помилку дубліката коду), а старий рядок залишиться. Перейменування ідентифікатора пакета має такий самий наслідок для кожного рядка.- Оновлення проведених документів проводить їх знову. З
"onChange": "update"змінений документ розпроводиться, оновлюється й проводиться знову в одному збереженні. Собівартість запасів і бухгалтерські проведення перераховуються з нового вмісту, а пізніший документ, що залежав від старих кількостей, може не пройти власні перевірки. Тому кроки документів за замовчуванням маютьskip. Документ, створений крокомcreateFrom, повторний запуск ніколи не оновлює. $codeпотребує унікального наявного коду. Пошук іде в таблицю, на яку посилається поле, а сутність відома з метаданих поля. Він завершується помилкою, якщо жоден рядок не має такого коду. На ієрархічні довідники можна посилатися й кодом папки (parentId). Об'єкт, доданий раніше в тому самому запуску, теж можна знайти через$code.- Будь-який рядок, що починається з
$, — це токен. Допустимі лише$now,$todayі$today±Nd; буквальне значення на кшталт"$100"відхиляється. - Токени дат використовують часовий пояс того, хто запускає.
$today— це локальна дата того, хто виконує імпорт: часовий пояс браузера адміністратора на сторінці адміністрування, часовий пояс завдання під час запуску за розкладом. ПоляDateTimeотримують локальну північ, переведену в UTC, поляDateOnly— локальну дату. Токени хешуються необчисленими, тож пакет, повний$today-5d, не вважається зміненим щодня. - Попередня перевірка ліцензії спрацьовує рано. Запуск, який перевищив би кількість документів на
місяць або рядків на документ, завершується помилкою повністю, з діагностикою за шляхом
$, ще до будь-якого запису. - Один імпорт одночасно на інсталяцію. Другий запуск, поки триває перший, завершується помилкою
(
409від ендпоінтів). Це стосується й викликуIDataImportServiceз обробника, який пакет запускає в кроціdataProcessor: такий вкладений виклик завершиться помилкою, тож не будуйте цей цикл. - Права — того, хто викликає. Рушій авторизує кожен запис як користувача, який його запустив. На
сторінці адміністрування це адміністратор; з вашого обробника — той, хто натиснув Виконати; у
запланованому завданні — користувач Запуск від імені. Користувач без прав на додавання
номенклатури отримає результат
Failedдля кожного рядка, а не тихий пропуск. - Обов'язкові властивості треба вказувати явно. Властивість Dto з
[Required]є помилкою, якщо пакет її пропускає, навіть коли Dto задав би їй значення за замовчуванням. Пишіть явні значення (наприклад,"isIncome": trueу банківському надходженні). Винятки —codeтаisActiveдокумента: код дає нумерація, аisActiveзадає прапорецьsubmitкроку. - Папки перед елементами. У кроці ієрархічного довідника папка має йти перед елементами, які
посилаються на неї через
$ref.
Див. також
- Формат пакета імпорту — кожне поле, політика й результат
- Створення обробника даних
- Планувальник завдань
- Зберігання файлів і вкладення
- Створення константи
- Розгортання