Self-provisioning: создание структур данных¶
См. также: data-api.md · catalog-rules.md · catalog-settings.md · korfix-catalogs.md ← Home
Приложение может создавать себе каталоги (таблицы) и кастомные поля при установке или первом запуске.
Встроенный инсталлер (рекомендуемый)¶
Приложение при первом запуске проверяет наличие каталога и предлагает кнопку установки.
Не требует токенов или доступа к терминалу — работает через сессионную авторизацию (App.fetch()).
Проверка существования каталога¶
Важно: нельзя проверять существование кастомного каталога через /db/{catalog}.json —
при несуществующем каталоге CRM не возвращает ошибку, а fallback'ит на дефолтный каталог
(возвращает данные другого каталога со status: "ok"). Приложение ошибочно решит что каталог
существует и не покажет экран установки.
Правильный способ — проверять через реестр custom_dbtables:
const CATALOG = 'custom_quicknotes';
async function checkCatalogExists() {
try {
// dbname = имя таблицы без префикса custom_ (quicknotes, не custom_quicknotes)
const resp = await App.fetch('/db/custom_dbtables.json?form[dbname]=quicknotes');
return !!(resp && resp.data && Array.isArray(resp.data) && resp.data.length > 0);
} catch (e) {}
return false;
}
Для приложений с несколькими каталогами — универсальная функция:
async function checkCatalogExists(catalogName) {
try {
const tablename = catalogName.replace('custom_', '');
const resp = await App.fetch('/db/custom_dbtables.json?form[dbname]=' + tablename);
return !!(resp && resp.data && Array.isArray(resp.data) && resp.data.length > 0);
} catch (e) {}
return false;
}
Создание каталога и полей¶
const FIELDS = [
{ name: 'Текст заметки', dbname: 'content', type: 'textarea', f_maxlen: 65535 },
{ name: 'Приоритет', dbname: 'priority', type: 'select', f_arr: 'обычный\nважный\nсрочный', f_default: 'обычный' },
{ name: 'Статус', dbname: 'status', type: 'select', f_arr: 'активная\nвыполнена\nархив', f_default: 'активная' },
{ name: 'Дедлайн', dbname: 'due_date', type: 'datetime' },
];
// Получить ID текущего пользователя (для from_auth/from_group)
let currentUserId = 0;
async function loadUserId() {
const schema = await App.fetch('/db/custom_dbtables/sheme.json');
const arr = schema?.data?.from_auth?.arr || {};
currentUserId = Object.keys(arr).find(k => k !== '0') || 0;
}
function uid() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 8);
}
async function createTable() {
return App.fetch('/db/custom_dbtables/add?edit&ajax=1', {
method: 'POST',
body: {
'form[alias]': uid(),
'form[name]': 'Quick Notes',
'form[dbname]': 'quicknotes',
'form[from_auth]': currentUserId,
'form[from_group]': currentUserId,
submit: 1
}
});
}
async function createField(field) {
const body = {
'form[alias]': uid(),
'form[name]': field.name,
'form[dbname]': field.dbname,
'form[type]': field.type,
'form[scheme]': CATALOG,
'form[from_auth]': currentUserId,
'form[from_group]': currentUserId,
submit: 1
};
if (field.f_maxlen) body['form[f_maxlen]'] = field.f_maxlen;
if (field.f_arr) body['form[f_arr]'] = field.f_arr;
if (field.f_default) body['form[f_default]'] = field.f_default;
return App.fetch('/db/custom_dbfields/add?edit&ajax=1', { method: 'POST', body });
}
async function runInstall() {
await createTable();
for (const field of FIELDS) {
await createField(field);
}
}
Полный паттерн: HTML с экраном установки¶
<!-- Экран установки (скрыт по умолчанию) -->
<div id="installScreen" style="display:none;">
<h2>Название приложения</h2>
<p>Для работы нужен каталог данных. Нажмите кнопку для установки.</p>
<button id="btnInstall">Установить структуру данных</button>
<div id="installLog"></div>
</div>
<!-- Рабочий UI (скрыт по умолчанию) -->
<div id="mainUI" style="display:none;">
<!-- ... основной интерфейс приложения ... -->
</div>
<script type="module">
import VMCRMUserApp from '/templates/def/db/marketplace/vmcrm-user-app.js';
const App = new VMCRMUserApp();
async function init() {
const exists = await checkCatalogExists();
if (exists) {
document.getElementById('mainUI').style.display = '';
loadData();
} else {
document.getElementById('installScreen').style.display = '';
}
}
document.getElementById('btnInstall').addEventListener('click', async () => {
const btn = document.getElementById('btnInstall');
btn.disabled = true;
btn.textContent = 'Установка...';
try {
await runInstall();
document.getElementById('installScreen').style.display = 'none';
document.getElementById('mainUI').style.display = '';
loadData();
} catch (e) {
btn.disabled = false;
btn.textContent = 'Повторить установку';
}
});
init();
</script>
Внешний скрипт (curl / CI/CD)¶
Создание структуры через терминал с bearer-токеном:
# Создать каталог
curl -s -X POST "$API_URL/api/db/custom_dbtables" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"form": {"name": "Quick Notes", "dbname": "quicknotes"}}'
# Добавить поле
curl -s -X POST "$API_URL/api/db/custom_dbfields" \
-H "Authorization: Bearer $TOKEN" \
-d 'form[name]=Текст&form[dbname]=content&form[type]=textarea&form[scheme]=custom_quicknotes&submit=1'
Типы полей¶
type |
Описание |
|---|---|
textbox |
Однострочный текст |
textarea |
Многострочный текст |
select |
Выпадающий список |
checkbox |
Чекбокс |
datetime |
Дата и время |
photo |
Загрузка файла |
select_from_table |
Выбор из другого каталога |
Значения для select: передавать в f_arr через \n (перенос строки).
Важно: в JS-коде поля кастомного каталога доступны с префиксом custom_ — например note.custom_content, note.custom_priority.
Права доступа (access_db) — ОБЯЗАТЕЛЬНО¶
После создания нового кастомного каталога он не виден обычным пользователям. Платформа автоматически создаёт запись в служебном каталоге access_db для нового каталога, но только с правами для администраторов (acctype_root=1, acctype_adm=1), остальные роли — 0 (нет доступа).
Это означает: - Менеджер, оператор, клиент, бухгалтер и т.д. не увидят твой каталог в меню/списках - Если миниап встроен как виджет каталога, который установил админ — работать будет только у админа - Для корректной работы — обязательно прописать права нужным ролям
Структура access_db¶
| Поле | Значение |
|---|---|
dbmodule |
Alias каталога с префиксом custom_ (custom_my_catalog) |
name |
Название (обычно совпадает с dbmodule) |
from_group |
Тенант пользователя (из getUser().group) |
acctype_root |
Права для роли «Администратор» (0/1/2) |
acctype_adm |
Права для роли «Менеджер» (0/1/2) |
acctype_res, acctype_fin, acctype_ag1..ag6, acctype_ec1..ec5, acctype_b2b1..b2b3, acctype_md1..md3 |
Права для соответствующих ролей |
Значения acctype_*:
| Значение | Доступ | Когда использовать |
|---|---|---|
0 |
Нет доступа (каталог скрыт) | Роль не должна видеть этот каталог вообще |
1 |
Все записи организации (своего from_group) |
Совместная работа: задачи, клиенты, сделки — видят все сотрудники |
2 |
Только свои (from_auth = свой user_id) |
Персональные данные: заметки, черновики, настройки |
Список ролей специфичен для инстанса — на panel.korfix.ru один набор, на self-hosted может быть другой. Получить актуальный список: App.fetch('/db/access_db/sheme.json') → смотреть поля с префиксом acctype_ в ответе.
Best practice: дефолт «2 всем ролям» (self-access)¶
Самый безопасный и типовой дефолт — выдать всем ролям значение 2 (только свои записи). Этого достаточно для большинства приложений: каждый пользователь видит только то, что создал сам.
Когда выбирать другое:
- Значение 1 (все записи организации) — для коллаборативных каталогов: задачи (все видят все задачи компании), клиенты CRM, сделки, склад
- Значение 0 (нет доступа) — для технических/внутренних каталогов, которые не должен видеть конкретный тип аккаунта
- Смешанно — если роли реально отличаются (админ видит всё, менеджер видит всё своей группы, клиент видит только свои заявки)
Готовый паттерн: «self всем» (рекомендуемый дефолт)¶
async function configureAccess(catalog, defaultValue = 2) {
// 1. Получить список полей acctype_* из схемы
const schema = await App.fetch('/db/access_db/sheme.json');
const acctypeFields = Object.keys(schema.data || {})
.filter(k => k.startsWith('acctype_'));
// 2. Найти auto-created запись для нашего каталога
const resp = await App.fetch(
`/db/access_db.json?form[dbmodule]=${catalog}`
);
const access = resp.data?.[0];
if (!access) {
console.warn(`access_db entry for ${catalog} not found`);
return;
}
// 3. Собрать body со всеми acctype_* = defaultValue
const body = {
'form[id]': access.id,
'form[alias]': access.alias,
'form[dbmodule]': access.dbmodule,
submit: 1,
};
for (const field of acctypeFields) {
body[`form[${field}]`] = defaultValue;
}
// 4. Обновить запись
await App.fetch(`/db/access_db/${access.alias}?edit&ajax=1`, {
method: 'POST',
body
});
}
// Использование в installer:
await configureAccess('custom_quicknotes'); // все роли → self (2)
await configureAccess('custom_shared_tasks', 1); // все роли → all (1)
Функция сама подтянет актуальный список ролей текущего инстанса — не надо хардкодить acctype_adm, acctype_b2b2 и т.д.
Что делать миниапу при установке¶
Три варианта по убыванию предпочтения:
1. Автоматически: self всем ролям (best default — большинство апп-случаев)
2. Автоматически: конкретные значения для конкретных ролей (если нужна коллаборация)
await configureAccess('custom_tasks', 1); // все видят все
// или точечно:
await App.fetch(`/db/access_db/${access.alias}?edit&ajax=1`, {
method: 'POST',
body: {
'form[id]': access.id,
'form[alias]': access.alias,
'form[dbmodule]': access.dbmodule,
'form[acctype_adm]': 1, // менеджер — все
'form[acctype_b2b2]': 2, // оператор — только свои
'form[acctype_b2b3]': 0, // клиент — скрыто
submit: 1
}
});
3. Делегировать админу (если бизнес-логика прав сложная)
В about → «Настройка» написать: «После установки откройте /db/access_db, найдите запись для custom_my_catalog, проставьте права ролям». Это OK если администратор однозначно понимает что выставлять.
Важные нюансы¶
dbmoduleвaccess_dbсодержит полный alias с префиксомcustom_— НЕdbnameизcustom_dbtables(там без префикса).- Изменение
access_dbтребует прав уровня администратор — обычный пользователь не сможет обновить права через свой токен. Миниап должен либо запускаться в контексте админа при установке, либо делегировать это пользователю. from_groupдолжен совпадать с тенантом пользователя, иначе запись относится к чужой группе.
Дальше: styling.md · ← Home