[598-25][@ant] fix: авто-seed карты ПВЗ при reboot:extra и sentinel-username для анонимных мутаций — убрать ручной psql INSERT после ребута и устранить лог-шум NOT NULL в mutation_logs
- boot: seedMarketplaceKuDetails (idempotent ON CONFLICT) + CLI seed:marketplace-ku + шаг в extra_reboot.sh после coopback с ожиданием создания таблицы; на свежем стенде krg/odn/myt (ACTIVE) появляются автоматически. - controller: MutationLoggingInterceptor пишет username='anonymous' вместо undefined для мутаций без JWT (login/register) — INSERT в mutation_logs больше не падает на NOT NULL, лог анонимной мутации сохраняется для аудита. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"boot:clean": "esno src/index.ts boot:clean",
|
||||
"boot:extra": "esno src/index.ts boot:extra",
|
||||
"boot:remote": "esno src/index.ts boot:remote",
|
||||
"seed:marketplace-ku": "esno src/scripts/seed-marketplace-ku-details.ts",
|
||||
"reboot:clean": "cd components/boot/scripts && bash clean_reboot.sh",
|
||||
"reboot:extra": "cd components/boot/scripts && bash extra_reboot.sh",
|
||||
"create-coop": "esno src/index.ts create-coop",
|
||||
|
||||
@@ -57,4 +57,24 @@ docker compose up -d cooparser
|
||||
echo "Запускаем контроллер..."
|
||||
docker compose up -d coopback
|
||||
|
||||
# Контроллер создаёт marketplace-таблицы через TypeORM synchronize при старте.
|
||||
# Ждём появления marketplace_ku_details, затем засеваем 3 ПВЗ Подмосковья
|
||||
# (krg/odn/myt, ACTIVE) — иначе на свежем стенде таблица пуста, select ПВЗ
|
||||
# приходит пустым/disabled и harness-сценарии ломаются. Сидер идемпотентен.
|
||||
echo "Ждём создания marketplace_ku_details контроллером..."
|
||||
KU_READY=""
|
||||
for _ in $(seq 1 60); do
|
||||
if docker compose exec -T postgres psql -U "${POSTGRES_USERNAME:-postgres}" -d "${POSTGRES_DATABASE:-voskhod}" -tAc "SELECT to_regclass('public.marketplace_ku_details')" 2>/dev/null | grep -q marketplace_ku_details; then
|
||||
KU_READY=1
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ -n "$KU_READY" ]; then
|
||||
echo "Засеваем 3 ПВЗ Подмосковья (krg/odn/myt)..."
|
||||
pnpm run seed:marketplace-ku || echo "⚠ seed ПВЗ не выполнен (не критично для остального стенда)"
|
||||
else
|
||||
echo "⚠ marketplace_ku_details не появилась за отведённое время — пропускаем seed ПВЗ"
|
||||
fi
|
||||
|
||||
echo "Перезапуск завершен!"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable node/prefer-global/process */
|
||||
import { Client } from 'pg'
|
||||
import type { Cooperative } from 'cooptypes'
|
||||
|
||||
export async function initSystemStatus() {
|
||||
console.log('Инициализация статуса системы для coopname: voskhod')
|
||||
@@ -157,6 +156,82 @@ export async function initUsersInPostgres(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Засев dev-фикстуры карты ПВЗ: 3 кооперативных участка Подмосковья
|
||||
* (krg/odn/myt, status=ACTIVE) в таблицу `marketplace_ku_details`.
|
||||
*
|
||||
* Зачем: на свежем стенде после `reboot:extra` таблица пуста — select ПВЗ
|
||||
* приходит пустым/disabled, harness-сценарии и UI выбора участка ломаются.
|
||||
* Раньше строки заводили ручным `psql INSERT` после каждого reboot.
|
||||
*
|
||||
* Ordering: DDL `marketplace_ku_details` создаёт контроллер (TypeORM
|
||||
* `synchronize:true`) при старте coopback — ПОЗЖЕ, чем boot:extra. Поэтому
|
||||
* сидер вызывается из `extra_reboot.sh` уже ПОСЛЕ `up -d coopback`, а не из
|
||||
* installExtraData; здесь дополнительно проверяем существование таблицы и
|
||||
* мягко пропускаем seed, если контроллер ещё не создал её. Идемпотентно
|
||||
* (`ON CONFLICT (coopname, core_braname) DO NOTHING`). Данные совпадают с
|
||||
* `seed-marketplace-branches.ts` (адреса/контакты тех же krg/odn/myt).
|
||||
*
|
||||
* Не для прода: прод не запускает `extra_reboot.sh`, ПВЗ там заводит
|
||||
* председатель через UI — этот сидер живёт только в dev-reboot-цепочке.
|
||||
*/
|
||||
export async function seedMarketplaceKuDetails(coopname = 'voskhod') {
|
||||
const client = new Client({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
port: parseInt(process.env.POSTGRES_PORT || '5432'),
|
||||
user: process.env.POSTGRES_USERNAME,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
database: process.env.POSTGRES_DATABASE,
|
||||
})
|
||||
|
||||
// Будни 09:00–18:00, суббота 10:00–16:00, воскресенье выходной.
|
||||
const weekday = { open: '09:00', close: '18:00', breaks: [] as Array<{ start: string, end: string }> }
|
||||
const workingHours = {
|
||||
mon: weekday,
|
||||
tue: weekday,
|
||||
wed: weekday,
|
||||
thu: weekday,
|
||||
fri: weekday,
|
||||
sat: { open: '10:00', close: '16:00', breaks: [] as Array<{ start: string, end: string }> },
|
||||
}
|
||||
|
||||
const KU = [
|
||||
{ braname: 'krg', address: 'Московская область, г. Красногорск, ул. Заводская, д. 1', phone: '+79991230101', email: 'krg@voskhod.coop' },
|
||||
{ braname: 'odn', address: 'Московская область, г. Одинцово, ул. Центральная, д. 12', phone: '+79991230202', email: 'odn@voskhod.coop' },
|
||||
{ braname: 'myt', address: 'Московская область, г. Мытищи, Олимпийский проспект, д. 5', phone: '+79991230303', email: 'myt@voskhod.coop' },
|
||||
]
|
||||
|
||||
try {
|
||||
await client.connect()
|
||||
console.log('Подключение к PostgreSQL установлено для сидинга ПВЗ marketplace')
|
||||
|
||||
const exists = await client.query(`SELECT to_regclass('public.marketplace_ku_details') AS t`)
|
||||
if (!exists.rows[0]?.t) {
|
||||
console.warn('⚠ marketplace_ku_details ещё не создана контроллером — пропускаем seed ПВЗ')
|
||||
return
|
||||
}
|
||||
|
||||
for (const ku of KU) {
|
||||
await client.query(`
|
||||
INSERT INTO marketplace_ku_details
|
||||
(coopname, core_braname, address_full, contact_phone, contact_email,
|
||||
working_hours_json, status, geocode_status, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'ACTIVE', 'PENDING', now(), now())
|
||||
ON CONFLICT (coopname, core_braname) DO NOTHING
|
||||
`, [coopname, ku.braname, ku.address, ku.phone, ku.email, JSON.stringify(workingHours)])
|
||||
}
|
||||
|
||||
console.log(`Засеяно ${KU.length} ПВЗ (krg/odn/myt, ACTIVE) в marketplace_ku_details для ${coopname}`)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Ошибка сидинга ПВЗ marketplace в PostgreSQL:', error)
|
||||
throw error
|
||||
}
|
||||
finally {
|
||||
await client.end()
|
||||
}
|
||||
}
|
||||
|
||||
export async function initVaultInPostgres() {
|
||||
const client = new Client({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
@@ -241,9 +316,9 @@ export async function initExtensionsInPostgres() {
|
||||
// Вставляем запись для capital extension
|
||||
const capitalConfig = {
|
||||
level_depth_base: 100000000,
|
||||
onboarding_init_at: "2026-02-09T07:16:18.380Z",
|
||||
onboarding_init_at: '2026-02-09T07:16:18.380Z',
|
||||
expense_pool_percent: 100,
|
||||
onboarding_expire_at: "2026-03-11T07:16:18.380Z",
|
||||
onboarding_expire_at: '2026-03-11T07:16:18.380Z',
|
||||
voting_period_in_days: 1,
|
||||
authors_voting_percent: 62.8,
|
||||
creators_voting_percent: 62.8,
|
||||
@@ -253,15 +328,15 @@ export async function initExtensionsInPostgres() {
|
||||
energy_decay_rate_per_day: 0.02,
|
||||
coordinator_invite_validity_days: 30,
|
||||
onboarding_blagorost_provision_done: true,
|
||||
onboarding_blagorost_provision_hash: "CDE57D987E3C945E79E108920CE02A4A80CFA7980CAA912949BB6C2111B7027A",
|
||||
onboarding_blagorost_provision_hash: 'CDE57D987E3C945E79E108920CE02A4A80CFA7980CAA912949BB6C2111B7027A',
|
||||
onboarding_blagorost_offer_template_done: true,
|
||||
onboarding_blagorost_offer_template_hash: "5CA88BBD303E5CCDA01E565FFE47E51855515176EC6C957F0FBDBCA4C53DBFD2",
|
||||
onboarding_blagorost_offer_template_hash: '5CA88BBD303E5CCDA01E565FFE47E51855515176EC6C957F0FBDBCA4C53DBFD2',
|
||||
onboarding_generator_offer_template_done: true,
|
||||
onboarding_generator_offer_template_hash: "8DA31574E8CC764C3A1FCAE1172726656A3DCDB1BB82AB0E567E2732070C3A44",
|
||||
onboarding_generator_offer_template_hash: '8DA31574E8CC764C3A1FCAE1172726656A3DCDB1BB82AB0E567E2732070C3A44',
|
||||
onboarding_generator_program_template_done: true,
|
||||
onboarding_generator_program_template_hash: "E55564D8946C55C93490B5277968FC890FDCB10A049DB5B2E0FE9F67FDA80896",
|
||||
onboarding_generator_program_template_hash: 'E55564D8946C55C93490B5277968FC890FDCB10A049DB5B2E0FE9F67FDA80896',
|
||||
onboarding_generation_contract_template_done: true,
|
||||
onboarding_generation_contract_template_hash: "A4BD579D6130CCE2D8C34337DFA591807C1F028A148DD53689881B12AC2627E2",
|
||||
onboarding_generation_contract_template_hash: 'A4BD579D6130CCE2D8C34337DFA591807C1F028A148DD53689881B12AC2627E2',
|
||||
github_sync_branch: 'dev',
|
||||
github_sync_poll_interval_minutes: 5,
|
||||
github_api_token_encrypted: '',
|
||||
@@ -282,7 +357,7 @@ export async function initExtensionsInPostgres() {
|
||||
JSON.stringify(capitalConfig),
|
||||
1,
|
||||
new Date('2026-02-09T02:13:06.620Z'),
|
||||
new Date('2026-02-09T02:27:57.155Z')
|
||||
new Date('2026-02-09T02:27:57.155Z'),
|
||||
])
|
||||
|
||||
// reports — встроенный extension, конфиг пустой (BuiltinSchema).
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/* eslint-disable node/prefer-global/process */
|
||||
/**
|
||||
* CLI-обёртка для сидинга карты ПВЗ (3 КУ Подмосковья krg/odn/myt, ACTIVE)
|
||||
* в `marketplace_ku_details`. Запускается из `extra_reboot.sh` ПОСЛЕ подъёма
|
||||
* контроллера (он создаёт таблицу через TypeORM synchronize), чтобы свежий
|
||||
* стенд сразу имел непустой select ПВЗ. Идемпотентно.
|
||||
*
|
||||
* pnpm --filter @coopenomics/boot run seed:marketplace-ku
|
||||
*/
|
||||
import { seedMarketplaceKuDetails } from '../postgres-init'
|
||||
|
||||
seedMarketplaceKuDetails()
|
||||
.then(() => process.exit(0))
|
||||
.catch((e) => {
|
||||
console.error('seed:marketplace-ku failed:', e)
|
||||
process.exit(1)
|
||||
})
|
||||
+11
-4
@@ -38,6 +38,13 @@ export class MutationLoggingInterceptor implements NestInterceptor {
|
||||
const mutationName = info.fieldName;
|
||||
const args = gqlContext.getArgs();
|
||||
const user = ctx.req?.user;
|
||||
// Публичные мутации (логин, регистрация) выполняются без JWT — у них нет
|
||||
// user.username, а колонка mutation_logs.username NOT NULL. Пишем sentinel
|
||||
// вместо undefined, иначе INSERT падает «null value in column username» на
|
||||
// каждой анонимной мутации (sentinel вместо nullable-колонки — чтобы фикс не
|
||||
// зависел от synchronize и работал на проде, где миграция эту колонку не
|
||||
// трогала).
|
||||
const username = user?.username ?? 'anonymous';
|
||||
const startTime = Date.now();
|
||||
|
||||
// Санитизация аргументов - удаляем wif ключи
|
||||
@@ -49,14 +56,14 @@ export class MutationLoggingInterceptor implements NestInterceptor {
|
||||
// Логируем успешное выполнение
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.info(`[INTERCEPTOR] Мутация ${mutationName} выполнена успешно за ${duration}мс`, {
|
||||
username: user?.username,
|
||||
username,
|
||||
coopname,
|
||||
arguments: sanitizedArgs,
|
||||
});
|
||||
this.saveMutationLog({
|
||||
coopname,
|
||||
mutation_name: mutationName,
|
||||
username: user?.username,
|
||||
username,
|
||||
arguments: sanitizedArgs,
|
||||
duration_ms: duration,
|
||||
status: 'success',
|
||||
@@ -68,7 +75,7 @@ export class MutationLoggingInterceptor implements NestInterceptor {
|
||||
// Логируем ошибку
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.info(`[INTERCEPTOR] Мутация ${mutationName} завершилась ошибкой за ${duration}мс: ${error.message}`, {
|
||||
username: user?.username,
|
||||
username,
|
||||
coopname,
|
||||
arguments: sanitizedArgs,
|
||||
error: error.message,
|
||||
@@ -76,7 +83,7 @@ export class MutationLoggingInterceptor implements NestInterceptor {
|
||||
this.saveMutationLog({
|
||||
coopname,
|
||||
mutation_name: mutationName,
|
||||
username: user?.username,
|
||||
username,
|
||||
arguments: sanitizedArgs,
|
||||
duration_ms: duration,
|
||||
status: 'error',
|
||||
|
||||
Reference in New Issue
Block a user