Пайплайн JSON + CSV → PostgreSQL с дедупликацией и валидацией, три
аналитических SQL-запроса и страница /companies на Next.js (App Router) поверх
этой базы.
| Задача | Где смотреть |
|---|---|
| 1. Выгрузка → Postgres | db/schema.sql, db/queries.sql, scripts/import-json.ts |
| 2. Мини-фича + доказательство | src/app/companies/page.tsx, docs/PROOF.md, docs/screenshots/ |
| 3. Данные с сюрпризом | ANOMALIES.md, scripts/import-csv.ts |
| 4. Вайбкод/LLM-стек | Отправляется отдельным текстовым файлом — по формату сдачи из задания |
| Как делалось | WORKLOG.md |
- Node.js 20+ (проверено на 24.14.0)
- Docker с
docker compose(для PostgreSQL 16)
Всё остальное ставится через npm ci.
npm ci
cp .env.example .env
npm run setupnpm run setup последовательно выполняет: поднять контейнер Postgres →
применить схему → загрузить JSON → загрузить CSV. Занимает около 20 секунд.
Дальше — приложение:
npm run build
npm run startОткрыть http://localhost:3000/companies.
Для разработки вместо
build/startможноnpm run dev.
Import finished (source: json_pack, run #1)
records read : 1000
inserted : 994
duplicates skipped: 6
Import finished (source: review_csv, run #2)
records read : 207
inserted : 190
updated : 6
duplicates skipped: 9
rejected rows : 2
Итого в базе 1184 компании. Повторный запуск npm run import:json и
npm run import:csv это число не меняет — импорт идемпотентен.
| Команда | Что делает |
|---|---|
npm run setup |
Полная установка: контейнер + схема + оба импорта |
npm run db:up / npm run db:down |
Поднять / остановить PostgreSQL |
npm run db:reset |
Снести том с данными и создать базу заново |
npm run db:schema |
Применить db/schema.sql (идемпотентно) |
npm run db:psql |
psql внутри контейнера |
npm run import:json |
Загрузить data/page_*.json |
npm run import:csv |
Загрузить data/review.csv |
npm run queries |
Выполнить три запроса из db/queries.sql и напечатать таблицы |
npm run queries -- --markdown |
То же в Markdown |
npm run dev / build / start |
Next.js |
npm test |
Все тесты (нужен поднятый Postgres) |
npm run test:unit |
Только юнит-тесты (Postgres не нужен) |
npm run typecheck |
tsc --noEmit |
npm run screenshots |
Перегенерировать скриншоты (нужен запущенный npm run start и один раз npx playwright install chromium) |
Секретов в репозитории нет. Все настройки — через переменные окружения,
образец в .env.example; .env в .gitignore.
| Переменная | По умолчанию | Назначение |
|---|---|---|
DATABASE_URL |
postgresql://polza:polza@localhost:5433/polza |
Строка подключения для скриптов и приложения |
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB |
polza |
Учётные данные контейнера |
POSTGRES_PORT |
5433 |
Порт на хосте (5433, чтобы не конфликтовать с локальным Postgres на 5432) |
PGSSLMODE |
disable |
require — если DATABASE_URL смотрит на Supabase или другой managed-хост |
Подключение к базе живёт только на сервере: страница /companies — Server
Component, в браузер уходит готовый HTML.
Задание допускает оба варианта. Для Supabase достаточно подменить окружение, код менять не нужно:
export DATABASE_URL='postgresql://postgres:...@db.<ref>.supabase.co:5432/postgres'
export PGSSLMODE=require
npm run db:schema && npm run import:json && npm run import:csvТри таблицы (полностью — в db/schema.sql):
companies— чистый слой, одна строка на реальную компанию. Типы настоящие (numeric,integer), диапазоны закрытыCHECK-ограничениями. Кроме отображаемых значений хранятся нормализованные (name_norm,city_norm,address_norm) — по ним работают дедупликация и поиск.import_issues— журнал: каждое исправленное, отклонённое или помеченное значение с исходным текстом и тем, что с ним сделали.ANOMALIES.mdсобран из этой таблицы, а не написан вручную.import_runs— история запусков, по ней проверяется идемпотентность.
Индексы: уникальный source_id (ключ upsert), уникальный бизнес-ключ
(name_norm, city_norm, address_norm), индексы под каждый из трёх
аналитических запросов и GIN-индекс pg_trgm под поиск по подстроке.
- По
source_idвнутри пачки — ловит повторы страниц JSON (6 записей). - По бизнес-ключу внутри пачки — одна компания под двумя id в одном файле.
- По бизнес-ключу против БД — ловит повторный экспорт с перенумерованными
id:
review.csvсодержит 6 таких записей, и дедупликация только поidих не заметила бы. Подробности — вANOMALIES.md.
Три запроса из задания — в db/queries.sql, с комментариями
о том, почему выбраны именно такие фильтры:
- топ-5 категорий по числу компаний;
- средний рейтинг по городам среди компаний с 10+ отзывами;
- доля компаний с сайтом по категориям.
npm run queriesРезультаты прогона — в
docs/evidence/verification-run.txt,
раздел 11.
npm run test:unit # 46 тестов, без базы
npm test # 59 тестов, нужен поднятый Postgres- Юнит-тесты — правила очистки на реальных значениях из выгрузки
(
-3,7.2,N/A,4,5,много,8 (925) abc-12-34,нет сайта,htp://, mojibake, сдвиг колонок). Это регрессионные тесты против конкретного датасета, а не выдуманные примеры. - Интеграционные тесты поднимают отдельную схему
test_importв той же базе, чтобы не трогать рабочие данные, и проверяют числа импорта, идемпотентность, отлов перенумерованного экспорта и инварианты качества (нет рейтингов вне 0..5, нет mojibake, нет дублей по бизнес-ключу).
docs/PROOF.md— что проверялось руками, что при этом ломалось и как чинилось.docs/screenshots/— 7 скриншотов: список, поиск, фильтр по городу, комбинация, пустой результат, пагинация, недоступная база.docs/evidence/verification-run.txt— полный протокол прогона с нуля: удаление тома, импорт, повторный импорт, проверки качества, запросы, тесты.docs/evidence/clean-clone-run.txt— проверка воспроизводимости: репозиторий склонирован в пустой каталог, и все команды из этого README выполнены ровно так, как они здесь записаны.
Скриншоты снимаются скриптом (scripts/screenshots.ts),
а не вручную, — чтобы доказательства можно было воспроизвести, а не принимать
на веру. Для этого нужен один раз выполненный npx playwright install chromium.
npm auditпоказывает предупреждение вsharp(транзитивная зависимость оптимизатора картинок Next.js). Приложение картинок не отдаёт, этот код не выполняется, аnpm audit fix --forceоткатывает Next.js до 9.x — что заведомо хуже. Оставлено осознанно.- Список канонических городов в
normalize.tsпокрывает те 20 городов, что есть в выгрузке. Незнакомый город не отбрасывается, а сохраняется с пометкойcity_unknown— молча терять данные хуже. - Строка со сдвигом колонок (
c_001015) не восстанавливается автоматически: где потерялась настоящая категория — неизвестно. Поля обнулены, запись помеченаis_suspect. - Поля
emailв исходных данных нет — см. пункт 12 вANOMALIES.md.