Расширение вашего бэкенда
Вы сгенерировали автономный бэкенд из сайта. Это обычный репозиторий, и во время выполнения он ничего не импортирует из CMS. Этот документ объясняет, куда помещать ваш код, что переписывает повторная генерация и в чём она отказывает.
Короткий ответ: src/owned/ принадлежит вам, навсегда. apps/site/src/owned/ и
apps/site/src/pages/owned/ принадлежат вам в выданном сайте. package.json,
wrangler.jsonc, .nvmrc, .gitignore, README.md и ваши собственные workflow
остаются вашими. Всё остальное переписывается из определений моделей при каждой
повторной генерации.
Источник правила: backend/src/lib/graduate/zones.ts. Таблица ниже
сверяется с этим кодом тестом backend/test/extending-your-backend-doc.test.ts,
поэтому этот документ не может разойтись с генератором.
Чем владеете вы и что переписывает повторная генерация
Заголовок раздела «Чем владеете вы и что переписывает повторная генерация»В сгенерированном бэкенде есть три вида файлов. Правило определяет зону только по ПУТИ — не по комментарию-маркеру и не по списку, который кто-то поддерживает.
| Зона | Кто владеет | Что делает повторная генерация |
|---|---|---|
generated |
генератор | переписывает при каждом запуске |
owned |
разработчик | записывает один раз как заготовку, больше никогда не трогает |
shared |
оба | записывает, если файла нет, никогда не перезаписывает |
Одно исключение, по ключам: package.json — и корневой, и тот, что в
apps/site/, — СЛИВАЕТСЯ, а не остаётся нетронутым. См. «Слияние манифеста».
Точное правило приведено ниже. Вопросы про сайт задаются ПЕРВЫМИ, потому что
apps/site/package.json носит общее имя, которое использует и API, а
apps/site/src/owned/x не начинается с src/owned/:
- Путь, который начинается с
apps/site/src/owned/илиapps/site/src/pages/owned/, является owned. - Путь
apps/site/package.json,apps/site/package-lock.json,apps/site/.npmrcилиapps/site/wrangler.jsoncявляется shared. - Всё остальное в
apps/site/является generated. - Путь, который начинается с
src/owned/, является owned. - Путь
.github/workflows/dee-wan-build.ymlили.github/workflows/deploy-from-repository.ymlявляется generated. Оба — машинная конфигурация; исправление в них должно доходить до принятых владельцами проектов. - Путь
package.json,package-lock.json,.npmrc,.nvmrc,wrangler.jsonc,.gitignore,README.mdили путь, начинающийся с.github/, является shared. - Всё остальное является generated.
Два пути вообще никогда не сканируются, поэтому для них никогда не снимается отпечаток и они никогда не
добавляются в индекс git: src/serve.local.mjs и src/credential-digest.mjs. Это РЕЗУЛЬТАТ
СБОРКИ, который лежит в src/, потому что так сгенерированный клиент Prisma остаётся внешним.
Они указаны в .gitignore — когда их предлагали git add, весь push завершался неудачей.
Отредактированный сгенерированный файл при следующей повторной генерации получает ОТКАЗ С УКАЗАНИЕМ ИМЕНИ и места, куда перенести работу. Он никогда не перезаписывается молча. См. «Что получает отказ».
Зона каждого выдаваемого пути
Заголовок раздела «Зона каждого выдаваемого пути»| Путь | Зона |
|---|---|
prisma/schema.prisma |
generated |
graduation.manifest.json |
generated |
src/app.ts |
generated |
src/worker.ts |
generated |
src/security.ts |
generated |
src/owned/routes.ts |
owned |
src/serve-cli.ts |
generated |
migrations/0002_graduation_ledger.sql |
generated |
scripts/regenerate.mjs |
generated |
scripts/emit-migration.mjs |
generated |
scripts/file-manifest.mjs |
generated |
scripts/portable-client.mjs |
generated |
scripts/configure-deployment.mjs |
generated |
tsconfig.json |
generated |
wrangler.example.jsonc |
generated |
wrangler.build.jsonc |
generated |
scripts/load-snapshot.mjs |
generated |
data/published-snapshot.json |
generated |
.github/workflows/dee-wan-build.yml |
generated |
.github/workflows/deploy-from-repository.yml |
generated |
.github/workflows/anything-else.yml |
shared |
package.json |
shared |
package-lock.json |
shared |
.npmrc |
shared |
.nvmrc |
shared |
wrangler.jsonc |
shared |
.gitignore |
shared |
README.md |
shared |
apps/site/src/owned/config.mjs |
owned |
apps/site/src/pages/owned/thanks.astro |
owned |
apps/site/package.json |
shared |
apps/site/package-lock.json |
shared |
apps/site/.npmrc |
shared |
apps/site/wrangler.jsonc |
shared |
apps/site/src/lib/pages.json |
generated |
Добавить собственный маршрут
Заголовок раздела «Добавить собственный маршрут»src/owned/routes.ts — это заготовка, записанная один раз, и она намеренно поставляется ПУСТОЙ.
Помещайте свои маршруты туда:
export function ownedRoutes(prisma: GuardedPrisma) { const routes = new Hono()
routes.post('/checkout', async (c) => { const body = await c.req.json() return c.json({ ok: true }) })
return routes}src/app.ts монтирует его через app.route('/api', ownedRoutes(deps.prisma)), ПОСЛЕДНИМ —
после всех роутеров моделей, поэтому выбранный вами путь никогда не может затенить сгенерированный.
Ваш /checkout выше отдаётся по адресу /api/checkout.
Что вы наследуете: аутентификация выполняется в app.use('*') до того, как что-либо
смонтировано, поэтому каждый запрос, который доходит до вас, уже идентифицирован или получил отказ.
Внутри обработчика у вас есть c.get('principal'), c.get('accessVariant') и
c.get('prisma'). Защищённый клиент ПЕРЕДАЁТСЯ, а не импортируется, поэтому ваши
запросы выполняются под тем же расширением, что и в сгенерированных роутерах.
Чего нельзя предполагать — проверка прав доступа спрашивает, к какой МОДЕЛИ относится запрос,
и отвечает на это по ПРЕФИКСУ /api/<model>…:
- Путь, начинающийся с имени модели, проверяется на права доступа КАК ЭТА МОДЕЛЬ.
/api/articles-reportсовпадает с модельюarticle, поэтому вызывающему нужно право доступа к article, чтобы до него добраться, — больше или меньше, чем вы задумывали. - Путь, не совпадающий ни с одной моделью, не относится ни к одной модели. Тогда вызывающий с сессией
регулируется только операцией (GET, POST…); вызывающий с токеном чтения получает отказ,
если его scope не
*.
Поэтому выбирайте первый сегмент, который не может занять ни один slug модели, — /checkout,
/webhooks/stripe, /reports/daily. Если маршруту нужно собственное правило, применяйте
его в обработчике. Там ничего не проверяется за вас.
Для маршрута сайта, а не маршрута API, добавьте файл в
apps/site/src/pages/owned/. Маршрут Astro существует благодаря тому, что является файлом в
src/pages, поэтому собственная страница не может жить в apps/site/src/owned/. Она отдаётся
по /owned/…, а резолвер адресов отказывает любой сгенерированной странице, служебному (chrome)
маршруту или редиректу, которые затенили бы это пространство URL.
Добавить бизнес-логику
Заголовок раздела «Добавить бизнес-логику»То же место, любой файл на ваш выбор: всё дерево src/owned/ принадлежит вам. Добавьте
src/owned/pricing.ts и импортируйте его из src/owned/routes.ts. Помещайте туда:
- собственные маршруты,
- бизнес-логику,
- дополнительную аутентификацию сверх выдаваемой проверки bearer,
- интеграции со сторонними системами,
- ваши собственные таблицы и их миграции,
- всё, что должно пережить повторную генерацию.
Для собственного (owned) файла никогда не снимается отпечаток — в этом и СОСТОИТ обещание. Не помещайте свой код в сгенерированный файл «пока что»: следующая повторная генерация откажет во всём запуске, назвав этот файл.
В сайте типизированные точки расширения находятся в apps/site/src/owned/:
Head.astro, BodyEnd.astro, слоты макета, Wrapper/overrides
секций и config.mjs для интеграций Astro и origin site.
Добавить зависимость
Заголовок раздела «Добавить зависимость»Отредактируйте package.json и установите через проверенный установщик:
node scripts/install-dependencies.mjsВаши записи сохраняются — см. «Слияние манифеста». npm ci и npm install — не
путь установки: установщик сверяет npm с .nvmrc и packageManager,
распаковывает с выключенными скриптами, проверяет дерево по точному allowScripts, а затем
запускает npm rebuild только для одобренных скриптов.
Зависимости сайта указываются в apps/site/package.json и устанавливаются через
node scripts/install-dependencies.mjs apps/site. Для острова Svelte или React также
нужно добавить его закреплённую интеграцию в apps/site/src/owned/config.mjs.
Добавить привязку
Заголовок раздела «Добавить привязку»Отредактируйте wrangler.jsonc. Он shared: записывается, если его нет, и никогда не перезаписывается, поэтому
ваши привязки остаются. wrangler.example.jsonc рядом с ним — GENERATED: шаблон
без id учётной записи, без маршрута и без id базы данных, потому что этот генератор не
решает, куда вы развёртываете. Копируйте из него; не редактируйте его.
wrangler.build.jsonc тоже сгенерирован, и именно с ним собирает бандл npm run build.
wrangler.deploy.jsonc записывается развёртыванием
(npm run deploy:configure) и указан в gitignore.
Привязки сайта указываются в apps/site/wrangler.jsonc, который является shared на тех же условиях.
Добавить собственный CI
Заголовок раздела «Добавить собственный CI»Добавьте workflow в .github/workflows/ с любым именем, кроме
dee-wan-build.yml и deploy-from-repository.yml. .github/ — shared-префикс,
поэтому ваш файл пишете вы, и он никогда не трогается. Эти два имени
генерируются: это машинная конфигурация, и исправление в них должно дойти до вашего
проекта при следующей повторной генерации.
Что меняет повторная генерация
Заголовок раздела «Что меняет повторная генерация»Повторная генерация переписывает каждый путь generated из текущих определений моделей:
схему, маршруты, точку входа Worker, скрипты сборки, выдаваемые workflow, выдаваемый сайт. Она
добавляет миграцию при изменении схемы — она никогда не переписывает прошлую миграцию.
Она НЕ трогает:
- ничего в
src/owned/,apps/site/src/owned/илиapps/site/src/pages/owned/, - уже существующие shared-файлы (
wrangler.jsoncсохраняет ваши привязки), - уже применённые миграции,
- вашу историю git.
package.json — единственный shared-файл, который она ВСЁ ЖЕ меняет, и только ДОБАВЛЕНИЕМ. См.
«Слияние манифеста».
Параметры защиты (guard) сгенерированных маршрутов берутся из Settings → Source & delivery → Generated API guards. Повторная генерация записывает их в конфигурацию сгенерированных маршрутов; неустановленный параметр использует значение пакета по умолчанию.

Вспомогательный файл .dee-wan-files.json хранит отпечаток каждого сгенерированного файла в том виде,
в каком генератор записал его в последний раз. Так обнаруживается «вы это отредактировали». Для собственного (owned)
файла никогда не снимается отпечаток — в этом и СОСТОИТ обещание.
Повторная генерация выполняется без CMS. Генератор компилируется в ваш репозиторий
при graduation (scripts/dee-wan-generator.mjs), закрепляется по дайджесту в
graduation.generator.json, и каждый выдаваемый скрипт проверяет закрепление, прежде чем
импортировать его. npm run regenerate последовательно выполняет схему, приложение, клиент Prisma
и перезапись для переносимости, в этом порядке.
Что получает отказ и как перенести работу
Заголовок раздела «Что получает отказ и как перенести работу»Отредактированный сгенерированный файл не перезаписывается и не принимается. Запуск останавливается и называет файл:
- сгенерированный файл, байты которого изменились, —
“
<path>is generated and has been edited. Move your change undersrc/owned/, then delete this file so it can be regenerated.” - файл по сгенерированному пути, который предыдущий манифест никогда не записывал, —
“
<path>is not in the previous manifest. If it is yours, move it undersrc/owned/and delete it from here.”
Перенос работы: скопируйте изменение в новый файл в src/owned/, импортируйте его
из src/owned/routes.ts, удалите отредактированный файл и выполните повторную генерацию. Генератор
перепишет удалённый файл из определений.
Файл, который генератор удалил, стирается только если он наш И не изменён. Всё остальное остаётся на месте.
Строки контента следуют тому же контракту уровнем ниже: сгенерированные таблицы контента
принадлежат запуску, и строка, записанная в такую таблицу, приводит к отказу следующего запуска с
observed_row_set_mismatch. Ваши собственные таблицы и ваши собственные строки сохраняются — именно
для этого существуют src/owned/ и ваши собственные миграции.
Опубликованный снимок data/published-snapshot.json является GENERATED. Отредактируйте его —
и следующая повторная генерация откажет в нём по имени. Загрузчик тоже отказывает: не тот сайт,
не тот артефакт, изменённые байты, частичная загрузка, конфликтующее повторное воспроизведение. Загрузка одних и тех же
байтов дважды безопасна и ничего не записывает.
После отсоединения ничто ничего не генерирует повторно, поэтому каждый файл — включая сгенерированные — вы можете редактировать. До отсоединения отредактированный сгенерированный файл получает отказ по его точному пути.
Слияние манифеста
Заголовок раздела «Слияние манифеста»Оба файла package.json — корневой и в apps/site/ — являются shared, но СЛИВАЮТСЯ. Чистое
«никогда не перезаписывать» означает также «никогда не добавлять», поэтому в день, когда генератор начинает писать
код, импортирующий новый пакет, уже принятый владельцем проект после повторной генерации получает
исходный код, который npm ci не может разрешить. Сбой проявляется двумя шагами позже как
tsc --noEmit или astro check, называющие модуль, а не отсутствующую
зависимость.
Правило слияния, одинаковое для обоих файлов:
- сливаемые блоки:
dependencies,devDependencies,overrides; - ключ, который уже есть в вашем файле: ВАШ, значение не трогается, включая версию, которую вы намеренно увели от закреплённой;
- ключ, который есть только в шаблоне: ДОБАВЛЯЕТСЯ;
- ничего никогда не удаляется;
packageManagerдобавляется, если отсутствует, и остаётся вашим, если присутствует;- запись
allowScriptsдобавляется, только если вы не приняли решения по этому пакету и версии, — вашиtrueиfalseостаются в силе; - вторая идентичная повторная генерация меняет ноль байтов.
package.json, который не удаётся разобрать, остаётся нетронутым. Перезапись — это деструктивное
направление, а сборка в любом случае откажет за вас.
Lock-файл следует за манифестом. Когда слияние добавляет запись, перенесённый
package-lock.json больше не описывает манифест, поэтому генератор обновляет его
через npm install --package-lock-only перед установкой. Lock-файл и
манифест не могут расходиться. Это доказывается тестом backend/test/graduate-site-build.test.ts
(включается явно, DEEWAN_SITE_BUILD=1): принятый владельцем манифест сайта, созданный до появления блока
devDependencies, после повторной генерации устанавливается, проходит проверку типов и собирается.
Два входа, ОДИН путь развёртывания
Заголовок раздела «Два входа, ОДИН путь развёртывания»| Вход | Workflow | Читает CMS? | Генерирует повторно? | Развёртывает сам? |
|---|---|---|---|---|
| управляемый | dee-wan-build.yml |
да — DEE_WAN_URL, токен сборки, версия модели |
да, затем делает коммит | НЕТ — он вызывает другой |
| только репозиторий | deploy-from-repository.yml |
НЕТ | нет | да |
У управляемого workflow три задачи: build забирает определения, генерирует повторно,
проверяет и делает коммит; deploy вызывает deploy-from-repository.yml
(workflow_call) для этого конкретного коммита и ждёт; report сообщает Dee Wan
результат в рамках попытки сборки, которая его запустила. В нём нет шага миграции, нет
wrangler deploy и нет собственной проверки. Когда-то две копии разошлись: только
управляемая когда-либо запускалась при управляемой сборке, и только репозиторная когда-либо загружала
закоммиченный снимок — поэтому управляемое развёртывание отдавало пустую базу данных и
сообщало об успехе.
Путь «только репозиторий» — это то, что переживает отсоединение, то, через что развёртывает
управляемая сборка, и то, что использует откат. Он не читает URL Dee Wan, не выпускает токен сборки и
не вызывает ни одного эндпоинта CMS. Он развёртывает ровно тот коммит, который ему передан, и записывает
коммит, который фактически развернул. Он возвращает вызывающему git_sha, d1_uuid и
worker_name.
Что делает репозиторий развёртываемым без CMS
Заголовок раздела «Что делает репозиторий развёртываемым без CMS»Три файла. Отсутствие любого из них означает репозиторий, который собирается и ничего не отдаёт.
| Файл | Зона | Что он содержит |
|---|---|---|
package-lock.json |
shared | разрешённое дерево с целостностью (integrity). Он нужен scripts/install-dependencies.mjs |
data/published-snapshot.json |
generated | опубликованные строки, привязанные к сайту + артефакту + манифесту + дайджесту контента |
.github/workflows/deploy-from-repository.yml |
generated | делает checkout точного коммита, проверенная установка, сборка, определение D1, миграция, загрузка снимка, развёртывание, проверка |
Проверенная установка означает node scripts/install-dependencies.mjs [root]. npm,
который поставляется с версией Node из .nvmrc, должен совпадать с packageManager. npm ci
распаковывает с выключенными скриптами, установленное дерево проверяется по точному
allowScripts, затем npm rebuild запускает только одобренные скрипты. .nvmrc — shared:
записывается, если его нет, никогда не переписывается.
Lock-файл является SHARED по той же причине, что и package.json: владелец добавляет
свои собственные зависимости, и генератор не должен их выбрасывать. Генератор
всё равно обновляет его при каждом запуске, выполняя npm install --package-lock-only над
слитым package.json, поэтому lock-файл и манифест не могут расходиться.
Загрузите снимок в базу данных так:
npm run db:init -- --database ./graduated.db # local SQLite, migration firstnpm run data:load -- --d1 DB --config wrangler.deploy.jsonc # adopter D1Пошагово: один маршрут, одна зависимость, повторная генерация, развёртывание
Заголовок раздела «Пошагово: один маршрут, одна зависимость, повторная генерация, развёртывание»Каждая команда ниже — это скрипт, который уже есть в выданном проекте.
1. Установите зависимости и получите рабочую базу данных.
node scripts/install-dependencies.mjsnpm run db:init -- --database ./graduated.db2. Добавьте маршрут. Отредактируйте src/owned/routes.ts — добавьте
routes.post('/checkout', …), как выше. Больше ничего подключать не нужно: src/app.ts
уже монтирует ownedRoutes под /api.
3. Добавьте зависимость. Добавьте её в dependencies в package.json, затем:
node scripts/install-dependencies.mjs4. Проверьте локально.
npm run typechecknpm run build:serveDATABASE_PATH=./graduated.db SITE_ID=<your site id> \ ACCESS_CONFIG_FILE=./access.local.json PORT=8787 npm run serve:localЗатем в другой оболочке:
curl -i -X POST -H 'content-type: application/json' -d '{}' \ http://127.0.0.1:8787/api/checkoutДайджест для access.local.json берётся из npm run credential:digest, который
читает секрет из stdin, поэтому он никогда не попадает в историю вашей оболочки или в ps.
5. Выполните повторную генерацию. Используйте тот же порядок, что и управляемая сборка:
npm run regenerate # schema, app, security, local, manifest, Prisma client, portabilitynpm run migrate:emit # the next migration, append-onlynpm run typechecknode scripts/install-dependencies.mjs apps/sitenpm run --prefix apps/site checknpm run --prefix apps/site buildnpm run files:manifest # LAST: the ownership sidecar, from the tree that existsВаш маршрут и ваша зависимость по-прежнему на месте. git diff показывает только сгенерированные
файлы и добавленную миграцию.
6. Соберите бандл и разверните.
npm run build # the Worker bundle, via wrangler.build.jsoncРазвёртывание — это workflow, а не локальный скрипт: сделайте коммит, затем запустите
.github/workflows/deploy-from-repository.yml для этого конкретного коммита со своими
учётными данными Cloudflare. Он устанавливает зависимости, собирает, находит или создаёт D1,
применяет миграции, загружает снимок, развёртывает и проверяет. Откат — это тот же workflow
с более старым sha. npm run dev запускает Worker локально с wrangler.jsonc,
если вместо этого вам нужен wrangler dev.
После развёртывания npm run verify:deployment спрашивает у развёрнутого origin, отдаёт ли
он в точности контент этого коммита, строка в строку, а npm run verify:media
проверяет, указывают ли закоммиченные проекции всё ещё на Dee Wan.
Два поколения с изменением схемы
Заголовок раздела «Два поколения с изменением схемы»Доказано тестом backend/test/graduate-adoption.test.ts — “keeps a developer’s own
file across a schema change”, “keeps rows the adopter wrote into their own table
across a schema change”, “regenerates after a schema change, appending to the
migration lineage”:
- сгенерируйте и разверните,
- отредактируйте
src/owned/и добавьте строку в базу данных, - добавьте поле в модель в CMS,
- выполните повторную генерацию.
После шага 4: собственный файл идентичен побайтно, добавлена новая миграция, старые миграции не тронуты, а существующие строки по-прежнему на месте.