fix(shorts): плановая очистка, бэкап, часовой пояс и экранирование
- purgeScheduledDeletions() теперь чистит short_messages по purge_at - short_messages добавлена в BACKUP_TABLES/BACKUP_SEQUENCE_TABLES, normalizeRestoreData и restore (поле автора проверяется на живость) - сид short_messages_retention_days перенесён и в db/init.sql - used_at по умолчанию берётся в зоне приложения (appTodayIso), а не по UTC - поле «Использовано» скрыто при создании, открыто при правке - удалён пустой escInline и мёртвые data-message/canEdit: сырой текст сообщения попадал в атрибут неэкранированным
This commit is contained in:
+17
-2
@@ -8,12 +8,13 @@ const BACKUP_TABLES = [
|
|||||||
'group_photos', 'entry_photos', 'modules', 'student_photos', 'share_links', 'photo_jobs',
|
'group_photos', 'entry_photos', 'modules', 'student_photos', 'share_links', 'photo_jobs',
|
||||||
'lesson_reports', 'lesson_report_versions', 'audit_log', 'notifications',
|
'lesson_reports', 'lesson_report_versions', 'audit_log', 'notifications',
|
||||||
'notification_reads', 'banned_ips', 'chat_threads', 'chat_messages', 'chat_attachments',
|
'notification_reads', 'banned_ips', 'chat_threads', 'chat_messages', 'chat_attachments',
|
||||||
|
'short_messages',
|
||||||
];
|
];
|
||||||
const BACKUP_SEQUENCE_TABLES = [
|
const BACKUP_SEQUENCE_TABLES = [
|
||||||
'groups', 'students', 'entries', 'project_files', 'branches', 'users', 'group_photos',
|
'groups', 'students', 'entries', 'project_files', 'branches', 'users', 'group_photos',
|
||||||
'entry_photos', 'modules', 'student_photos', 'share_links', 'photo_jobs', 'lesson_reports',
|
'entry_photos', 'modules', 'student_photos', 'share_links', 'photo_jobs', 'lesson_reports',
|
||||||
'lesson_report_versions', 'audit_log', 'notifications', 'chat_threads', 'chat_messages',
|
'lesson_report_versions', 'audit_log', 'notifications', 'chat_threads', 'chat_messages',
|
||||||
'chat_attachments',
|
'chat_attachments', 'short_messages',
|
||||||
];
|
];
|
||||||
|
|
||||||
function isSupportedBackupVersion(data) {
|
function isSupportedBackupVersion(data) {
|
||||||
@@ -500,7 +501,21 @@ function normalizeRestoreData(data) {
|
|||||||
size: optInt(x.size, 0, 2147483647) ?? 0,
|
size: optInt(x.size, 0, 2147483647) ?? 0,
|
||||||
created_at: optTs(x.created_at),
|
created_at: optTs(x.created_at),
|
||||||
}));
|
}));
|
||||||
return { groups, students, entries, project_files, settings, branches, users, user_branches, entry_photos, student_photos, group_photos, share_links, modules, photo_jobs, lesson_reports, lesson_report_versions, audit_log, notifications, notification_reads, banned_ips, chat_threads, chat_messages, chat_attachments };
|
const short_messages = (data.short_messages || []).map(x => ({
|
||||||
|
id: reqInt(x.id),
|
||||||
|
resident: reqStr(x.resident, 200),
|
||||||
|
lesson_date: reqDate(x.lesson_date),
|
||||||
|
topic: optStr(x.topic, 500),
|
||||||
|
message: reqStr(x.message, 10000),
|
||||||
|
used: !!x.used,
|
||||||
|
used_at: optDate(x.used_at),
|
||||||
|
author_id: optInt(x.author_id, 0, 2147483647),
|
||||||
|
deleted_at: optTs(x.deleted_at),
|
||||||
|
purge_at: optTs(x.purge_at),
|
||||||
|
created_at: optTs(x.created_at),
|
||||||
|
updated_at: optTs(x.updated_at),
|
||||||
|
}));
|
||||||
|
return { groups, students, entries, project_files, settings, branches, users, user_branches, entry_photos, student_photos, group_photos, share_links, modules, photo_jobs, lesson_reports, lesson_report_versions, audit_log, notifications, notification_reads, banned_ips, chat_threads, chat_messages, chat_attachments, short_messages };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -79,6 +79,26 @@ const ch = normalizeRestoreData({
|
|||||||
ok('chat_threads нормализуются', ch.chat_threads[0].tutor_id === 1 && ch.chat_threads[0].tutor_unread === 2, ch.chat_threads[0]);
|
ok('chat_threads нормализуются', ch.chat_threads[0].tutor_id === 1 && ch.chat_threads[0].tutor_unread === 2, ch.chat_threads[0]);
|
||||||
ok('chat_messages нормализуются', ch.chat_messages[0].thread_id === 3 && ch.chat_messages[0].body === 'привет', ch.chat_messages[0]);
|
ok('chat_messages нормализуются', ch.chat_messages[0].thread_id === 3 && ch.chat_messages[0].body === 'привет', ch.chat_messages[0]);
|
||||||
ok('chat_attachments нормализуются', ch.chat_attachments[0].name === 'photo.png' && ch.chat_attachments[0].size === 1234, ch.chat_attachments[0]);
|
ok('chat_attachments нормализуются', ch.chat_attachments[0].name === 'photo.png' && ch.chat_attachments[0].size === 1234, ch.chat_attachments[0]);
|
||||||
|
const sm = normalizeRestoreData({
|
||||||
|
...base,
|
||||||
|
short_messages: [{ id: 1, resident: 'Иванов И.И.', lesson_date: '2026-05-05', topic: 'T', message: 'текст', used: true, used_at: '2026-05-06', author_id: 1, deleted_at: '2026-05-07T00:00:00.000Z', purge_at: '2027-05-07T00:00:00.000Z' }],
|
||||||
|
});
|
||||||
|
ok('short_messages нормализуются', sm.short_messages[0].resident === 'Иванов И.И.' && sm.short_messages[0].used === true && sm.short_messages[0].used_at === '2026-05-06', sm.short_messages[0]);
|
||||||
|
ok('short_messages сохраняет purge_at', sm.short_messages[0].deleted_at === '2026-05-07T00:00:00.000Z' && sm.short_messages[0].purge_at === '2027-05-07T00:00:00.000Z', sm.short_messages[0]);
|
||||||
|
let smBadDate = false;
|
||||||
|
try {
|
||||||
|
normalizeRestoreData({ ...base, short_messages: [{ id: 1, resident: 'X', lesson_date: '05.05.2026', message: 't' }] });
|
||||||
|
} catch (e) {
|
||||||
|
smBadDate = true;
|
||||||
|
}
|
||||||
|
ok('кривая дата занятия в short_messages отклоняется', smBadDate);
|
||||||
|
let smNoResident = false;
|
||||||
|
try {
|
||||||
|
normalizeRestoreData({ ...base, short_messages: [{ id: 1, resident: '', lesson_date: '2026-05-05', message: 't' }] });
|
||||||
|
} catch (e) {
|
||||||
|
smNoResident = true;
|
||||||
|
}
|
||||||
|
ok('пустой резидент в short_messages отклоняется', smNoResident);
|
||||||
const chEdits = normalizeRestoreData({
|
const chEdits = normalizeRestoreData({
|
||||||
...base,
|
...base,
|
||||||
chat_messages: [
|
chat_messages: [
|
||||||
|
|||||||
@@ -476,3 +476,5 @@ CREATE TABLE IF NOT EXISTS short_messages (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_short_messages_date ON short_messages(lesson_date DESC);
|
CREATE INDEX IF NOT EXISTS idx_short_messages_date ON short_messages(lesson_date DESC);
|
||||||
CREATE INDEX IF NOT EXISTS idx_short_messages_deleted ON short_messages(deleted_at);
|
CREATE INDEX IF NOT EXISTS idx_short_messages_deleted ON short_messages(deleted_at);
|
||||||
|
|
||||||
|
INSERT INTO settings (key, value) VALUES ('short_messages_retention_days', '365') ON CONFLICT (key) DO NOTHING;
|
||||||
|
|||||||
+8
-10
@@ -58,21 +58,20 @@ async function loadShorts() {
|
|||||||
const dateStr = r.lesson_date ? fmtDateOnlyIso(r.lesson_date) : '';
|
const dateStr = r.lesson_date ? fmtDateOnlyIso(r.lesson_date) : '';
|
||||||
const usedDateStr = r.used_at ? fmtDateOnlyIso(r.used_at) : '—';
|
const usedDateStr = r.used_at ? fmtDateOnlyIso(r.used_at) : '—';
|
||||||
const author = r.author_name || r.author_username || '—';
|
const author = r.author_name || r.author_username || '—';
|
||||||
const canEdit = true;
|
|
||||||
return `
|
return `
|
||||||
<tr data-edit-row data-message="${escInline(r.message || '')}">
|
<tr data-edit-row>
|
||||||
<td>${esc(r.resident)}</td>
|
<td>${esc(r.resident)}</td>
|
||||||
<td style="white-space:nowrap">${dateStr}</td>
|
<td style="white-space:nowrap">${dateStr}</td>
|
||||||
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.topic || '')}">${esc(r.topic || '')}</td>
|
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.topic || '')}">${esc(r.topic || '')}</td>
|
||||||
<td style="max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.message || '')}">${esc(r.message || '')}</td>
|
<td style="max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.message || '')}">${esc(r.message || '')}</td>
|
||||||
<td style="white-space:nowrap">${esc(author)}</td>
|
<td style="white-space:nowrap">${esc(author)}</td>
|
||||||
<td style="white-space:nowrap">
|
<td style="white-space:nowrap">
|
||||||
<input type="checkbox" class="used-check" data-id="${r.id}" ${r.used ? 'checked' : ''} title="Использовано" ${canEdit ? '' : 'disabled'}>
|
<input type="checkbox" class="used-check" data-id="${r.id}" ${r.used ? 'checked' : ''} title="Использовано">
|
||||||
<div style="font-size:.72rem;color:var(--muted)">${r.used ? esc(usedDateStr) : '—'}</div>
|
<div style="font-size:.72rem;color:var(--muted)">${r.used ? esc(usedDateStr) : '—'}</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
${canEdit ? `<button class="row-btn" data-edit="${r.id}" title="Редактировать"><i data-lucide="pencil"></i></button>
|
<button class="row-btn" data-edit="${r.id}" title="Редактировать"><i data-lucide="pencil"></i></button>
|
||||||
<button class="row-btn del" data-del="${r.id}" title="Удалить (мягко)"><i data-lucide="trash-2"></i></button>` : '—'}
|
<button class="row-btn del" data-del="${r.id}" title="Удалить (мягко)"><i data-lucide="trash-2"></i></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -129,11 +128,14 @@ function openShortsModal(row) {
|
|||||||
editingId = row ? row.id : null;
|
editingId = row ? row.id : null;
|
||||||
document.getElementById('shortsModalTitle').textContent = row ? `Сообщение #${row.id}` : 'Новое сообщение';
|
document.getElementById('shortsModalTitle').textContent = row ? `Сообщение #${row.id}` : 'Новое сообщение';
|
||||||
document.getElementById('shortsLinesHint').textContent = row ? ' (только текст сообщения)' : ' (формат: ФИО - текст или ФИО — текст)';
|
document.getElementById('shortsLinesHint').textContent = row ? ' (только текст сообщения)' : ' (формат: ФИО - текст или ФИО — текст)';
|
||||||
document.getElementById('shortsDate').value = row ? row.lesson_date : '';
|
document.getElementById('shortsDate').value = row ? row.lesson_date : todayIso();
|
||||||
document.getElementById('shortsTopic').value = row ? (row.topic || '') : '';
|
document.getElementById('shortsTopic').value = row ? (row.topic || '') : '';
|
||||||
document.getElementById('shortsResident').value = row ? (row.resident || '') : '';
|
document.getElementById('shortsResident').value = row ? (row.resident || '') : '';
|
||||||
document.getElementById('shortsUsed').checked = row ? !!row.used : false;
|
document.getElementById('shortsUsed').checked = row ? !!row.used : false;
|
||||||
document.getElementById('shortsUsedAt').value = row && row.used_at ? String(row.used_at).slice(0, 10) : '';
|
document.getElementById('shortsUsedAt').value = row && row.used_at ? String(row.used_at).slice(0, 10) : '';
|
||||||
|
const usedDisplay = row ? '' : 'none';
|
||||||
|
document.getElementById('shortsUsedField').style.display = usedDisplay;
|
||||||
|
document.getElementById('shortsUsedAtField').style.display = usedDisplay;
|
||||||
if (row) {
|
if (row) {
|
||||||
document.getElementById('shortsLines').value = row.message || '';
|
document.getElementById('shortsLines').value = row.message || '';
|
||||||
} else {
|
} else {
|
||||||
@@ -317,8 +319,4 @@ function acceptShortsAi() {
|
|||||||
closeShortsAi();
|
closeShortsAi();
|
||||||
}
|
}
|
||||||
|
|
||||||
function escInline(s) {
|
|
||||||
return String(s || '').replace(/'/g, "'").replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
+2
-2
@@ -72,11 +72,11 @@
|
|||||||
<label>Резидент (для добавления без разбора строк)</label>
|
<label>Резидент (для добавления без разбора строк)</label>
|
||||||
<input type="text" id="shortsResident" class="settings-input" placeholder="Иванов И.И.">
|
<input type="text" id="shortsResident" class="settings-input" placeholder="Иванов И.И.">
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-field" style="flex-direction:row;align-items:center;gap:8px">
|
<div class="settings-field" id="shortsUsedField" style="flex-direction:row;align-items:center;gap:8px;display:none">
|
||||||
<input type="checkbox" id="shortsUsed">
|
<input type="checkbox" id="shortsUsed">
|
||||||
<label style="margin:0">Использовано</label>
|
<label style="margin:0">Использовано</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-field">
|
<div class="settings-field" id="shortsUsedAtField" style="display:none">
|
||||||
<label>Дата использования</label>
|
<label>Дата использования</label>
|
||||||
<input type="date" id="shortsUsedAt" class="settings-input">
|
<input type="date" id="shortsUsedAt" class="settings-input">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1329,6 +1329,12 @@ async function appTimezone() {
|
|||||||
return validTimezone(v) ? v : DEFAULT_TIMEZONE;
|
return validTimezone(v) ? v : DEFAULT_TIMEZONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function appTodayIso() {
|
||||||
|
const tz = await appTimezone();
|
||||||
|
const { rows } = await pool.query('SELECT to_char(now() AT TIME ZONE $1, \'YYYY-MM-DD\') AS d', [tz]);
|
||||||
|
return rows[0].d;
|
||||||
|
}
|
||||||
|
|
||||||
async function appHour12() {
|
async function appHour12() {
|
||||||
return (await getSetting('time_format', '24h')) === '12h';
|
return (await getSetting('time_format', '24h')) === '12h';
|
||||||
}
|
}
|
||||||
@@ -1362,12 +1368,15 @@ async function purgeScheduledDeletions() {
|
|||||||
publishChat({ type: 'thread_update', thread_id: c.id, tutor_id: null, deleted: true });
|
publishChat({ type: 'thread_update', thread_id: c.id, tutor_id: null, deleted: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (erefs.rowCount || gcount || ccount) {
|
const sres = await pool.query('DELETE FROM short_messages WHERE purge_at IS NOT NULL AND purge_at <= now()');
|
||||||
|
const scount = sres.rowCount;
|
||||||
|
if (erefs.rowCount || gcount || ccount || scount) {
|
||||||
invalidateEntries();
|
invalidateEntries();
|
||||||
invalidateGroups();
|
invalidateGroups();
|
||||||
invalidateStats();
|
invalidateStats();
|
||||||
}
|
}
|
||||||
return { entries: erefs.rowCount, groups: gcount, chat_threads: ccount };
|
if (scount) invalidateShorts();
|
||||||
|
return { entries: erefs.rowCount, groups: gcount, chat_threads: ccount, short_messages: scount };
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeUnlink(relPath) {
|
function safeUnlink(relPath) {
|
||||||
@@ -2667,7 +2676,7 @@ app.put('/api/shorts/:id', requireAuth, async (req, res) => {
|
|||||||
if (usedAt && !/^\d{4}-\d{2}-\d{2}$/.test(usedAt)) {
|
if (usedAt && !/^\d{4}-\d{2}-\d{2}$/.test(usedAt)) {
|
||||||
return res.status(400).json({ error: 'used_at должен быть в формате YYYY-MM-DD' });
|
return res.status(400).json({ error: 'used_at должен быть в формате YYYY-MM-DD' });
|
||||||
}
|
}
|
||||||
if (used && !usedAt) usedAt = new Date().toISOString().slice(0, 10);
|
if (used && !usedAt) usedAt = await appTodayIso();
|
||||||
if (usedAt) used = true;
|
if (usedAt) used = true;
|
||||||
if (!used) usedAt = null;
|
if (!used) usedAt = null;
|
||||||
if (lessonDate && !/^\d{4}-\d{2}-\d{2}$/.test(lessonDate)) {
|
if (lessonDate && !/^\d{4}-\d{2}-\d{2}$/.test(lessonDate)) {
|
||||||
@@ -3443,7 +3452,7 @@ function sweepBackupStorage() {
|
|||||||
async function buildBackupArchive() {
|
async function buildBackupArchive() {
|
||||||
const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'wido-bk-'));
|
const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'wido-bk-'));
|
||||||
try {
|
try {
|
||||||
const [g, s, e, st, pf, br, us, ub, gp, ep, md, sp, sl, pj, lr, lrv, al, nt, nr, bi, ct, cm, ca] = await Promise.all([
|
const [g, s, e, st, pf, br, us, ub, gp, ep, md, sp, sl, pj, lr, lrv, al, nt, nr, bi, ct, cm, ca, sm] = await Promise.all([
|
||||||
pool.query('SELECT * FROM groups ORDER BY id'),
|
pool.query('SELECT * FROM groups ORDER BY id'),
|
||||||
pool.query('SELECT * FROM students ORDER BY id'),
|
pool.query('SELECT * FROM students ORDER BY id'),
|
||||||
pool.query('SELECT * FROM entries ORDER BY id'),
|
pool.query('SELECT * FROM entries ORDER BY id'),
|
||||||
@@ -3467,6 +3476,7 @@ async function buildBackupArchive() {
|
|||||||
pool.query('SELECT * FROM chat_threads ORDER BY id'),
|
pool.query('SELECT * FROM chat_threads ORDER BY id'),
|
||||||
pool.query('SELECT * FROM chat_messages ORDER BY id'),
|
pool.query('SELECT * FROM chat_messages ORDER BY id'),
|
||||||
pool.query('SELECT * FROM chat_attachments ORDER BY id'),
|
pool.query('SELECT * FROM chat_attachments ORDER BY id'),
|
||||||
|
pool.query('SELECT * FROM short_messages ORDER BY id'),
|
||||||
]);
|
]);
|
||||||
const settings = {};
|
const settings = {};
|
||||||
st.rows.forEach(r => { settings[r.key] = r.value; });
|
st.rows.forEach(r => { settings[r.key] = r.value; });
|
||||||
@@ -3482,8 +3492,9 @@ async function buildBackupArchive() {
|
|||||||
lesson_report_versions: lrv.rowCount, audit_log: al.rowCount, notifications: nt.rowCount,
|
lesson_report_versions: lrv.rowCount, audit_log: al.rowCount, notifications: nt.rowCount,
|
||||||
notification_reads: nr.rowCount, banned_ips: bi.rowCount,
|
notification_reads: nr.rowCount, banned_ips: bi.rowCount,
|
||||||
chat_threads: ct.rowCount, chat_messages: cm.rowCount, chat_attachments: ca.rowCount,
|
chat_threads: ct.rowCount, chat_messages: cm.rowCount, chat_attachments: ca.rowCount,
|
||||||
|
short_messages: sm.rowCount,
|
||||||
},
|
},
|
||||||
groups: g.rows, students: s.rows, entries: e.rows, settings, project_files: pf.rows, branches: br.rows, users: us.rows, user_branches: ub.rows, group_photos: gp.rows, entry_photos: ep.rows, modules: md.rows, student_photos: sp.rows, share_links: sl.rows, photo_jobs: pj.rows, lesson_reports: lr.rows, lesson_report_versions: lrv.rows, audit_log: al.rows, notifications: nt.rows, notification_reads: nr.rows, banned_ips: bi.rows, chat_threads: ct.rows, chat_messages: cm.rows, chat_attachments: ca.rows,
|
groups: g.rows, students: s.rows, entries: e.rows, settings, project_files: pf.rows, branches: br.rows, users: us.rows, user_branches: ub.rows, group_photos: gp.rows, entry_photos: ep.rows, modules: md.rows, student_photos: sp.rows, share_links: sl.rows, photo_jobs: pj.rows, lesson_reports: lr.rows, lesson_report_versions: lrv.rows, audit_log: al.rows, notifications: nt.rows, notification_reads: nr.rows, banned_ips: bi.rows, chat_threads: ct.rows, chat_messages: cm.rows, chat_attachments: ca.rows, short_messages: sm.rows,
|
||||||
};
|
};
|
||||||
fs.writeFileSync(path.join(staging, 'data.json'), JSON.stringify(payload));
|
fs.writeFileSync(path.join(staging, 'data.json'), JSON.stringify(payload));
|
||||||
const files = await storage.downloadAll(path.join(staging, 'uploads'));
|
const files = await storage.downloadAll(path.join(staging, 'uploads'));
|
||||||
@@ -3649,6 +3660,7 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
|||||||
await client.query('DELETE FROM chat_attachments');
|
await client.query('DELETE FROM chat_attachments');
|
||||||
await client.query('DELETE FROM chat_messages');
|
await client.query('DELETE FROM chat_messages');
|
||||||
await client.query('DELETE FROM chat_threads');
|
await client.query('DELETE FROM chat_threads');
|
||||||
|
await client.query('DELETE FROM short_messages');
|
||||||
await client.query('DELETE FROM project_files');
|
await client.query('DELETE FROM project_files');
|
||||||
await client.query('DELETE FROM lesson_report_versions');
|
await client.query('DELETE FROM lesson_report_versions');
|
||||||
await client.query('DELETE FROM lesson_reports');
|
await client.query('DELETE FROM lesson_reports');
|
||||||
@@ -3802,6 +3814,13 @@ app.post('/api/restore', requireAdmin, uploadBackup.single('backup'), async (req
|
|||||||
[x.id, x.message_id, x.token, x.path, x.name, x.size || 0, x.created_at]
|
[x.id, x.message_id, x.token, x.path, x.name, x.size || 0, x.created_at]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
for (const x of ndata.short_messages) {
|
||||||
|
const okUser = x.author_id == null || (await client.query('SELECT 1 FROM users WHERE id = $1', [x.author_id])).rowCount;
|
||||||
|
await client.query(
|
||||||
|
'INSERT INTO short_messages (id, resident, lesson_date, topic, message, used, used_at, author_id, deleted_at, purge_at, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)',
|
||||||
|
[x.id, x.resident, x.lesson_date, x.topic, x.message, x.used, x.used_at, okUser ? x.author_id : null, x.deleted_at, x.purge_at, x.created_at, x.updated_at]
|
||||||
|
);
|
||||||
|
}
|
||||||
for (const x of ndata.audit_log) {
|
for (const x of ndata.audit_log) {
|
||||||
const okUser = x.user_id == null || (await client.query('SELECT 1 FROM users WHERE id = $1', [x.user_id])).rowCount;
|
const okUser = x.user_id == null || (await client.query('SELECT 1 FROM users WHERE id = $1', [x.user_id])).rowCount;
|
||||||
await client.query(
|
await client.query(
|
||||||
|
|||||||
Reference in New Issue
Block a user