Перейти к содержимому
Pre-MVP. Кодовая база 1.0 не является выпущенным продуктом — см. границы продукта 1.0.

Расширение вашего бэкенда

Вы сгенерировали автономный бэкенд из сайта. Это обычный репозиторий, и во время выполнения он ничего не импортирует из 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>…:

  1. Путь, начинающийся с имени модели, проверяется на права доступа КАК ЭТА МОДЕЛЬ. /api/articles-report совпадает с моделью article, поэтому вызывающему нужно право доступа к article, чтобы до него добраться, — больше или меньше, чем вы задумывали.
  2. Путь, не совпадающий ни с одной моделью, не относится ни к одной модели. Тогда вызывающий с сессией регулируется только операцией (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 на тех же условиях.

Добавьте 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. Повторная генерация записывает их в конфигурацию сгенерированных маршрутов; неустановленный параметр использует значение пакета по умолчанию.

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 under src/owned/, then delete this file so it can be regenerated.”
  • файл по сгенерированному пути, который предыдущий манифест никогда не записывал, — <path> is not in the previous manifest. If it is yours, move it under src/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 first
npm run data:load -- --d1 DB --config wrangler.deploy.jsonc # adopter D1

Пошагово: один маршрут, одна зависимость, повторная генерация, развёртывание

Заголовок раздела «Пошагово: один маршрут, одна зависимость, повторная генерация, развёртывание»

Каждая команда ниже — это скрипт, который уже есть в выданном проекте.

1. Установите зависимости и получите рабочую базу данных.

Окно терминала
node scripts/install-dependencies.mjs
npm run db:init -- --database ./graduated.db

2. Добавьте маршрут. Отредактируйте src/owned/routes.ts — добавьте routes.post('/checkout', …), как выше. Больше ничего подключать не нужно: src/app.ts уже монтирует ownedRoutes под /api.

3. Добавьте зависимость. Добавьте её в dependencies в package.json, затем:

Окно терминала
node scripts/install-dependencies.mjs

4. Проверьте локально.

Окно терминала
npm run typecheck
npm run build:serve
DATABASE_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, portability
npm run migrate:emit # the next migration, append-only
npm run typecheck
node scripts/install-dependencies.mjs apps/site
npm run --prefix apps/site check
npm run --prefix apps/site build
npm 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”:

  1. сгенерируйте и разверните,
  2. отредактируйте src/owned/ и добавьте строку в базу данных,
  3. добавьте поле в модель в CMS,
  4. выполните повторную генерацию.

После шага 4: собственный файл идентичен побайтно, добавлена новая миграция, старые миграции не тронуты, а существующие строки по-прежнему на месте.