@beechcms/api 0.6.0-preview.1 → 0.6.0-preview.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/assets/dashboard/BeechLogo.svg +18 -18
- package/assets/dashboard/BeechLogoLIght.svg +48 -48
- package/assets/dashboard/assets/chart-Cws4Yj6K.js +39 -0
- package/assets/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/assets/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/assets/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/assets/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/assets/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/assets/dashboard/assets/index-BjPpppHe.css +1 -0
- package/assets/dashboard/assets/index-CrW-zhkA.js +642 -0
- package/assets/dashboard/assets/lora-cyrillic-ext-wght-normal-YcVxMP-u.woff2 +0 -0
- package/assets/dashboard/assets/lora-cyrillic-wght-normal-UWPuZLjM.woff2 +0 -0
- package/assets/dashboard/assets/lora-latin-ext-wght-normal-C2Wlntb9.woff2 +0 -0
- package/assets/dashboard/assets/lora-latin-wght-normal-BiLcIKcI.woff2 +0 -0
- package/assets/dashboard/assets/lora-math-wght-normal-sOy7rEfW.woff2 +0 -0
- package/assets/dashboard/assets/lora-symbols-wght-normal-DQ5VrUkH.woff2 +0 -0
- package/assets/dashboard/assets/lora-vietnamese-wght-normal-CfJ7gtf3.woff2 +0 -0
- package/assets/dashboard/assets/pie-chart-recharts-CAxoDQp2.js +1 -0
- package/assets/dashboard/assets/timeseries-chart-recharts-CixDW-LM.js +1 -0
- package/assets/dashboard/beechLogoDark.svg +48 -48
- package/assets/dashboard/index.html +18 -18
- package/assets/dashboard/noResult.svg +42 -42
- package/assets/dashboard/sol.svg +3 -3
- package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
- package/dist/auth/__fixtures__/in-memory-hash-provider.d.ts +5 -0
- package/dist/auth/__fixtures__/static-token-service.d.ts +6 -0
- package/dist/auth/bcrypt-hash-provider.d.ts +8 -0
- package/dist/auth/constants.d.ts +10 -0
- package/dist/auth/jose-token-service.d.ts +14 -0
- package/dist/auth/login.d.ts +32 -0
- package/dist/auth/refresh.d.ts +1 -0
- package/dist/factory.d.ts +39 -0
- package/dist/features/automations/action-executors/create-entry.executor.d.ts +7 -0
- package/dist/features/automations/action-executors/edit-field.executor.d.ts +7 -0
- package/dist/features/automations/action-executors/index.d.ts +13 -0
- package/dist/features/automations/action-executors/send-mail.executor.d.ts +7 -0
- package/dist/features/automations/action-executors/set-variable.executor.d.ts +15 -0
- package/dist/features/automations/action-executors/webhook.executor.d.ts +7 -0
- package/dist/features/automations/automation-runner.d.ts +13 -0
- package/dist/features/automations/automation-runner.utils.d.ts +3 -0
- package/dist/features/automations/automations.handler.d.ts +7 -0
- package/dist/features/automations/automations.schema.d.ts +191 -0
- package/dist/features/automations/context-resolver.d.ts +8 -0
- package/dist/features/automations/cron-runner.d.ts +9 -0
- package/dist/features/automations/cron-runner.utils.d.ts +1 -0
- package/dist/features/automations/filter-translation.d.ts +9 -0
- package/dist/features/automations/index.d.ts +7 -0
- package/dist/features/automations/template-grammar.d.ts +73 -0
- package/dist/features/automations/var-access-resolver.d.ts +6 -0
- package/dist/features/automations/when-evaluator.d.ts +3 -0
- package/dist/features/automations/when-pushdown.d.ts +2 -0
- package/dist/features/backrefs/backrefs.handler.d.ts +3 -0
- package/dist/features/backrefs/d1-backref.repository.d.ts +17 -0
- package/dist/features/backrefs/index.d.ts +1 -0
- package/dist/features/content/constants.d.ts +12 -0
- package/dist/features/content/handlers/bulk.handler.d.ts +3 -0
- package/dist/features/content/handlers/create.d.ts +3 -0
- package/dist/features/content/handlers/delete.d.ts +3 -0
- package/dist/features/content/handlers/facets.d.ts +3 -0
- package/dist/features/content/handlers/get.d.ts +4 -0
- package/dist/features/content/handlers/helpers.d.ts +12 -0
- package/dist/features/content/handlers/list.d.ts +3 -0
- package/dist/features/content/handlers/update.d.ts +3 -0
- package/dist/features/content/index.d.ts +4 -0
- package/dist/features/dashboard-layout/dashboard-layout.handler.d.ts +7 -0
- package/dist/features/dashboard-layout/index.d.ts +1 -0
- package/dist/features/draft/draft.handler.d.ts +4 -0
- package/dist/features/draft/draft.middleware.d.ts +9 -0
- package/dist/features/draft/index.d.ts +1 -0
- package/{src/features/email/email.provider.ts → dist/features/email/email.provider.d.ts} +37 -42
- package/dist/features/email/email.service.d.ts +13 -0
- package/dist/features/email/email.types.d.ts +94 -0
- package/{src/features/email/index.ts → dist/features/email/index.d.ts} +20 -33
- package/dist/features/email/providers/resend.d.ts +36 -0
- package/dist/features/email/providers/smtp.d.ts +11 -0
- package/dist/features/email/templates/automation-mail.d.ts +8 -0
- package/dist/features/email/templates/password-changed.d.ts +15 -0
- package/dist/features/email/templates/password-reset.d.ts +18 -0
- package/dist/features/email/templates/shell.d.ts +58 -0
- package/dist/features/notifications/index.d.ts +1 -0
- package/dist/features/notifications/notifications.handler.d.ts +15 -0
- package/dist/features/password-reset/index.d.ts +6 -0
- package/dist/features/password-reset/request.d.ts +10 -0
- package/dist/features/password-reset/reset.d.ts +9 -0
- package/dist/features/rotate-field/index.d.ts +1 -0
- package/dist/features/rotate-field/rotate-field.handler.d.ts +7 -0
- package/dist/features/rotate-field/rotate-field.schema.d.ts +11 -0
- package/dist/features/schema/schema.handler.d.ts +7 -0
- package/dist/features/seeds/index.d.ts +1 -0
- package/dist/features/seeds/seeds.handler.d.ts +6 -0
- package/dist/features/settings/settings.handler.d.ts +7 -0
- package/dist/features/setup/index.d.ts +7 -0
- package/dist/features/stats/index.d.ts +1 -0
- package/dist/features/stats/stats.handler.d.ts +7 -0
- package/dist/features/webhooks/index.d.ts +3 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9666 -0
- package/dist/media-utils.d.ts +25 -0
- package/dist/middleware/auth-providers.middleware.d.ts +8 -0
- package/dist/middleware/observability.middleware.d.ts +20 -0
- package/dist/middleware/rate-limit.middleware.d.ts +9 -0
- package/dist/middleware/repository.middleware.d.ts +34 -0
- package/dist/middleware/seed-registry.middleware.d.ts +10 -0
- package/dist/middleware/storage.middleware.d.ts +7 -0
- package/dist/middleware.d.ts +11 -0
- package/dist/public/access-policy.d.ts +12 -0
- package/dist/public/api-key-middleware.d.ts +6 -0
- package/dist/public/cache-utils.d.ts +10 -0
- package/dist/public/entry-projection.d.ts +2 -0
- package/dist/public/idempotency.d.ts +9 -0
- package/dist/public/index.d.ts +11 -0
- package/dist/public/problem-details.d.ts +31 -0
- package/dist/public/public-add.d.ts +3 -0
- package/dist/public/public-edit.d.ts +5 -0
- package/dist/public/public-errors.d.ts +13 -0
- package/dist/public/public-read.d.ts +3 -0
- package/dist/public/public-routes.d.ts +3 -0
- package/dist/public/query-builder.d.ts +28 -0
- package/dist/public/rate-limit-middleware.d.ts +3 -0
- package/dist/public/read-list.d.ts +26 -0
- package/dist/public/read-single.d.ts +22 -0
- package/dist/public/response-builder.d.ts +22 -0
- package/dist/public/sanitize.d.ts +23 -0
- package/dist/public/slug-utils.d.ts +11 -0
- package/dist/rate-limit/cloudflare-rate-limiter.d.ts +6 -0
- package/dist/rate-limit/in-memory-rate-limiter.d.ts +7 -0
- package/dist/rate-limit/no-op-rate-limiter.d.ts +4 -0
- package/dist/search-utils.d.ts +54 -0
- package/dist/search.d.ts +6 -0
- package/dist/shared/apply-policies.d.ts +10 -0
- package/dist/shared/automations.repository.d1.d.ts +12 -0
- package/dist/shared/background-notification-service.d.ts +18 -0
- package/dist/shared/base.repository.d1.d.ts +18 -0
- package/dist/shared/content-utils.d.ts +21 -0
- package/dist/shared/content.repository.d1.d.ts +68 -0
- package/dist/shared/d1-activity-log.repository.d.ts +13 -0
- package/dist/shared/d1-activity-logger.d.ts +20 -0
- package/dist/shared/d1-analytics.repository.d.ts +20 -0
- package/dist/shared/d1-content-scan.repository.d.ts +7 -0
- package/dist/shared/d1-notification.repository.d.ts +17 -0
- package/dist/shared/d1-password-reset-token.repository.d.ts +10 -0
- package/dist/shared/d1-search.repository.d.ts +14 -0
- package/dist/shared/d1-session.repository.d.ts +12 -0
- package/dist/shared/d1-setup-checklist.repository.d.ts +12 -0
- package/dist/shared/d1-user.repository.d.ts +18 -0
- package/dist/shared/d1-widget.repository.d.ts +31 -0
- package/dist/shared/dashboard-layout.repository.d1.d.ts +9 -0
- package/dist/shared/demo-data-sql.d.ts +1 -0
- package/dist/shared/demo-data.repository.d1.d.ts +6 -0
- package/dist/shared/execution-context-scheduler.d.ts +6 -0
- package/dist/shared/fixed-clock.d.ts +13 -0
- package/dist/shared/fts-sync.d.ts +1 -0
- package/dist/shared/idempotency.repository.d1.d.ts +16 -0
- package/dist/shared/in-memory-activity-logger.d.ts +11 -0
- package/dist/shared/in-memory-notification-service.d.ts +11 -0
- package/dist/shared/in-memory-seed.repository.d.ts +18 -0
- package/dist/shared/media.repository.d1.d.ts +35 -0
- package/dist/shared/qstash-notification-service.d.ts +18 -0
- package/dist/shared/query-utils.d.ts +36 -0
- package/dist/shared/request-utils.d.ts +13 -0
- package/dist/shared/schema-mutator.d1.d.ts +12 -0
- package/dist/shared/seed-layout.repository.d1.d.ts +9 -0
- package/dist/shared/seed-registry-cache.d.ts +12 -0
- package/dist/shared/seed.repository.d1.d.ts +13 -0
- package/dist/shared/sequential-id-generator.d.ts +13 -0
- package/dist/shared/site-settings.repository.d1.d.ts +7 -0
- package/dist/shared/storage/factory.d.ts +8 -0
- package/dist/shared/storage/s3-bucket.d.ts +43 -0
- package/dist/shared/storage-utils.d.ts +7 -0
- package/dist/shared/system-stats.repository.d1.d.ts +25 -0
- package/dist/types.d.ts +85 -0
- package/dist/upload.d.ts +16 -0
- package/dist/widget.d.ts +7 -0
- package/migrations/0000_v040_base.sql +265 -265
- package/migrations/0029_automations.sql +14 -14
- package/migrations/0030_test_seeds.sql +125 -125
- package/package.json +27 -18
- package/assets/dashboard/assets/index-BSjk0Sx9.js +0 -634
- package/assets/dashboard/assets/index-kRCYVN2B.css +0 -1
- package/src/auth/__fixtures__/in-memory-hash-provider.ts +0 -17
- package/src/auth/__fixtures__/static-token-service.ts +0 -22
- package/src/auth/bcrypt-hash-provider.ts +0 -24
- package/src/auth/constants.ts +0 -14
- package/src/auth/generate-refresh-token.test.ts +0 -23
- package/src/auth/hash-provider.test.ts +0 -55
- package/src/auth/jose-token-service.ts +0 -62
- package/src/auth/jwt-claims-passthrough.test.ts +0 -87
- package/src/auth/login.test.ts +0 -100
- package/src/auth/login.ts +0 -78
- package/src/auth/refresh.ts +0 -11
- package/src/auth/token-service.test.ts +0 -90
- package/src/factory.ts +0 -368
- package/src/features/automations/__tests__/action-executors.test.ts +0 -255
- package/src/features/automations/__tests__/automation-runner.test.ts +0 -196
- package/src/features/automations/__tests__/automation-runner.utils.test.ts +0 -60
- package/src/features/automations/__tests__/automations.handler.test.ts +0 -264
- package/src/features/automations/__tests__/automations.repository.test.ts +0 -163
- package/src/features/automations/__tests__/automations.schema.test.ts +0 -224
- package/src/features/automations/__tests__/context-resolver.test.ts +0 -126
- package/src/features/automations/__tests__/cron-runner.test.ts +0 -267
- package/src/features/automations/__tests__/cron-runner.utils.test.ts +0 -144
- package/src/features/automations/__tests__/set-variable.executor.test.ts +0 -383
- package/src/features/automations/__tests__/template-grammar.test.ts +0 -308
- package/src/features/automations/__tests__/webhook.executor.test.ts +0 -142
- package/src/features/automations/__tests__/when-evaluator.test.ts +0 -274
- package/src/features/automations/__tests__/when-pushdown.test.ts +0 -281
- package/src/features/automations/action-executors/create-entry.executor.ts +0 -27
- package/src/features/automations/action-executors/edit-field.executor.ts +0 -26
- package/src/features/automations/action-executors/index.ts +0 -37
- package/src/features/automations/action-executors/send-mail.executor.ts +0 -53
- package/src/features/automations/action-executors/set-variable.executor.ts +0 -159
- package/src/features/automations/action-executors/webhook.executor.ts +0 -67
- package/src/features/automations/automation-runner.ts +0 -85
- package/src/features/automations/automation-runner.utils.ts +0 -47
- package/src/features/automations/automations.handler.ts +0 -197
- package/src/features/automations/automations.schema.ts +0 -177
- package/src/features/automations/context-resolver.ts +0 -152
- package/src/features/automations/cron-runner.ts +0 -140
- package/src/features/automations/cron-runner.utils.ts +0 -44
- package/src/features/automations/filter-translation.ts +0 -46
- package/src/features/automations/index.ts +0 -16
- package/src/features/automations/template-grammar.ts +0 -245
- package/src/features/automations/var-access-resolver.ts +0 -140
- package/src/features/automations/when-evaluator.ts +0 -87
- package/src/features/automations/when-pushdown.ts +0 -57
- package/src/features/backrefs/__tests__/backrefs.handler.test.ts +0 -359
- package/src/features/backrefs/backrefs.handler.ts +0 -168
- package/src/features/backrefs/d1-backref.repository.ts +0 -84
- package/src/features/backrefs/index.ts +0 -5
- package/src/features/content/constants.ts +0 -16
- package/src/features/content/handlers/bulk.handler.ts +0 -185
- package/src/features/content/handlers/create.ts +0 -129
- package/src/features/content/handlers/delete.ts +0 -62
- package/src/features/content/handlers/facets.ts +0 -49
- package/src/features/content/handlers/get.ts +0 -120
- package/src/features/content/handlers/helpers.test.ts +0 -180
- package/src/features/content/handlers/helpers.ts +0 -93
- package/src/features/content/handlers/list.ts +0 -174
- package/src/features/content/handlers/update.ts +0 -174
- package/src/features/content/index.ts +0 -26
- package/src/features/draft/draft.handler.ts +0 -195
- package/src/features/draft/draft.middleware.ts +0 -66
- package/src/features/draft/index.ts +0 -5
- package/src/features/email/email.service.ts +0 -98
- package/src/features/email/email.types.ts +0 -119
- package/src/features/email/providers/resend.ts +0 -67
- package/src/features/email/providers/smtp.ts +0 -55
- package/src/features/email/templates/automation-mail.ts +0 -19
- package/src/features/email/templates/password-changed.ts +0 -63
- package/src/features/email/templates/password-reset.ts +0 -68
- package/src/features/email/templates/shell.ts +0 -96
- package/src/features/notifications/index.ts +0 -5
- package/src/features/notifications/notifications.handler.ts +0 -105
- package/src/features/password-reset/index.ts +0 -19
- package/src/features/password-reset/request.ts +0 -92
- package/src/features/password-reset/reset.ts +0 -102
- package/src/features/rotate-field/index.ts +0 -5
- package/src/features/rotate-field/rotate-field.handler.ts +0 -132
- package/src/features/rotate-field/rotate-field.schema.ts +0 -17
- package/src/features/schema/schema.handler.ts +0 -131
- package/src/features/seeds/index.ts +0 -5
- package/src/features/seeds/seeds.handler.test.ts +0 -632
- package/src/features/seeds/seeds.handler.ts +0 -693
- package/src/features/settings/__tests__/settings.handler.test.ts +0 -555
- package/src/features/settings/settings.handler.ts +0 -392
- package/src/features/setup/index.ts +0 -204
- package/src/features/stats/index.ts +0 -5
- package/src/features/stats/stats.handler.ts +0 -420
- package/src/features/webhooks/index.ts +0 -63
- package/src/index.ts +0 -65
- package/src/media-utils.ts +0 -82
- package/src/middleware/auth-providers.middleware.ts +0 -45
- package/src/middleware/observability.middleware.ts +0 -70
- package/src/middleware/rate-limit.middleware.ts +0 -45
- package/src/middleware/repository.middleware.ts +0 -114
- package/src/middleware/seed-registry.middleware.test.ts +0 -97
- package/src/middleware/seed-registry.middleware.ts +0 -21
- package/src/middleware/storage.middleware.ts +0 -25
- package/src/middleware.ts +0 -45
- package/src/public/access-policy.ts +0 -27
- package/src/public/api-key-middleware.ts +0 -57
- package/src/public/cache-utils.ts +0 -38
- package/src/public/entry-projection.ts +0 -46
- package/src/public/idempotency.ts +0 -23
- package/src/public/index.ts +0 -16
- package/src/public/problem-details.ts +0 -124
- package/src/public/public-add.ts +0 -114
- package/src/public/public-edit.ts +0 -163
- package/src/public/public-errors.ts +0 -19
- package/src/public/public-read.ts +0 -63
- package/src/public/public-routes.ts +0 -88
- package/src/public/query-builder.test.ts +0 -224
- package/src/public/query-builder.ts +0 -156
- package/src/public/rate-limit-middleware.ts +0 -34
- package/src/public/read-list.ts +0 -54
- package/src/public/read-single.ts +0 -48
- package/src/public/response-builder.ts +0 -30
- package/src/public/sanitize.ts +0 -69
- package/src/public/slug-utils.ts +0 -18
- package/src/rate-limit/cloudflare-rate-limiter.test.ts +0 -30
- package/src/rate-limit/cloudflare-rate-limiter.ts +0 -15
- package/src/rate-limit/in-memory-rate-limiter.test.ts +0 -37
- package/src/rate-limit/in-memory-rate-limiter.ts +0 -17
- package/src/rate-limit/no-op-rate-limiter.test.ts +0 -22
- package/src/rate-limit/no-op-rate-limiter.ts +0 -11
- package/src/search-utils.test.ts +0 -211
- package/src/search-utils.ts +0 -213
- package/src/search.ts +0 -65
- package/src/shared/apply-policies.test.ts +0 -81
- package/src/shared/apply-policies.ts +0 -67
- package/src/shared/automations.repository.d1.ts +0 -150
- package/src/shared/background-notification-service.test.ts +0 -62
- package/src/shared/background-notification-service.ts +0 -52
- package/src/shared/base.repository.d1.ts +0 -32
- package/src/shared/content-utils.test.ts +0 -165
- package/src/shared/content-utils.ts +0 -86
- package/src/shared/content.repository.d1.test.ts +0 -458
- package/src/shared/content.repository.d1.ts +0 -851
- package/src/shared/d1-activity-log.repository.test.ts +0 -140
- package/src/shared/d1-activity-log.repository.ts +0 -105
- package/src/shared/d1-activity-logger.test.ts +0 -86
- package/src/shared/d1-activity-logger.ts +0 -67
- package/src/shared/d1-analytics.repository.test.ts +0 -78
- package/src/shared/d1-analytics.repository.ts +0 -85
- package/src/shared/d1-content-scan.repository.test.ts +0 -80
- package/src/shared/d1-content-scan.repository.ts +0 -37
- package/src/shared/d1-notification.repository.test.ts +0 -128
- package/src/shared/d1-notification.repository.ts +0 -118
- package/src/shared/d1-password-reset-token.repository.test.ts +0 -81
- package/src/shared/d1-password-reset-token.repository.ts +0 -56
- package/src/shared/d1-search.repository.test.ts +0 -87
- package/src/shared/d1-search.repository.ts +0 -88
- package/src/shared/d1-session.repository.test.ts +0 -125
- package/src/shared/d1-session.repository.ts +0 -102
- package/src/shared/d1-setup-checklist.repository.ts +0 -36
- package/src/shared/d1-user.repository.test.ts +0 -151
- package/src/shared/d1-user.repository.ts +0 -119
- package/src/shared/d1-widget.repository.test.ts +0 -221
- package/src/shared/d1-widget.repository.ts +0 -341
- package/src/shared/demo-data-sql.ts +0 -54
- package/src/shared/demo-data.repository.d1.ts +0 -20
- package/src/shared/execution-context-scheduler.ts +0 -13
- package/src/shared/fixed-clock.ts +0 -25
- package/src/shared/fts-sync.ts +0 -8
- package/src/shared/idempotency.repository.d1.test.ts +0 -83
- package/src/shared/idempotency.repository.d1.ts +0 -60
- package/src/shared/in-memory-activity-logger.ts +0 -19
- package/src/shared/in-memory-notification-service.ts +0 -19
- package/src/shared/in-memory-seed.repository.ts +0 -51
- package/src/shared/media.repository.d1.test.ts +0 -107
- package/src/shared/media.repository.d1.ts +0 -68
- package/src/shared/qstash-notification-service.test.ts +0 -67
- package/src/shared/qstash-notification-service.ts +0 -55
- package/src/shared/query-utils.ts +0 -141
- package/src/shared/request-utils.ts +0 -26
- package/src/shared/schema-mutator.d1.ts +0 -64
- package/src/shared/seed-layout.repository.d1.test.ts +0 -114
- package/src/shared/seed-layout.repository.d1.ts +0 -62
- package/src/shared/seed-registry-cache.test.ts +0 -68
- package/src/shared/seed-registry-cache.ts +0 -49
- package/src/shared/seed.repository.d1.test.ts +0 -143
- package/src/shared/seed.repository.d1.ts +0 -98
- package/src/shared/sequential-id-generator.ts +0 -33
- package/src/shared/site-settings.repository.d1.ts +0 -53
- package/src/shared/storage/factory.ts +0 -41
- package/src/shared/storage/s3-bucket.ts +0 -195
- package/src/shared/storage-utils.ts +0 -40
- package/src/shared/system-stats.repository.d1.test.ts +0 -58
- package/src/shared/system-stats.repository.d1.ts +0 -48
- package/src/types.ts +0 -87
- package/src/upload.ts +0 -198
- package/src/widget.ts +0 -212
|
@@ -1,693 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: BUSL-1.1
|
|
2
|
-
// Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
|
|
3
|
-
// See LICENSE in the repository root for license terms.
|
|
4
|
-
|
|
5
|
-
/// <reference types="@cloudflare/workers-types" />
|
|
6
|
-
import { Hono } from 'hono'
|
|
7
|
-
import type { Branch, Seed } from '@beechcms/core'
|
|
8
|
-
import {
|
|
9
|
-
nextBranchId,
|
|
10
|
-
validateSeedDefinitions,
|
|
11
|
-
planCreateSeed,
|
|
12
|
-
planExtendSeed,
|
|
13
|
-
planFtsRebuild,
|
|
14
|
-
generateDropTable,
|
|
15
|
-
generateDropColumn,
|
|
16
|
-
generateRenameColumn,
|
|
17
|
-
generateRetypeColumn,
|
|
18
|
-
BRANCH_ALIAS_RE,
|
|
19
|
-
} from '@beechcms/core'
|
|
20
|
-
import { publicProblem, internalErrorDetail } from '../../public/problem-details'
|
|
21
|
-
import { deleteR2Objects } from '../../upload'
|
|
22
|
-
import { extractMediaKeysFromData } from '../../media-utils'
|
|
23
|
-
import type { Env, Variables } from '../../types'
|
|
24
|
-
|
|
25
|
-
const SLUG_RE = /^[a-z0-9_]+$/
|
|
26
|
-
|
|
27
|
-
function requireAdmin(context: any) {
|
|
28
|
-
const role = context.get('jwtPayload')?.role
|
|
29
|
-
if (role !== 'admin') {
|
|
30
|
-
return publicProblem(context, {
|
|
31
|
-
type: 'forbidden',
|
|
32
|
-
title: 'Forbidden',
|
|
33
|
-
status: 403,
|
|
34
|
-
detail: 'Seed management requires admin role.',
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
return null
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function actorFromContext(context: any) {
|
|
41
|
-
const jwt = context.get('jwtPayload')
|
|
42
|
-
return {
|
|
43
|
-
id: jwt?.sub ?? 'unknown',
|
|
44
|
-
email: jwt?.email ?? 'unknown',
|
|
45
|
-
name: [jwt?.name, jwt?.surname].filter(Boolean).join(' ') || null,
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Assign ids to branches that are missing one, mutates each branch in place. */
|
|
50
|
-
function assignBranchIds(branches: Branch[], accSeed: Pick<Seed, 'branches'>): void {
|
|
51
|
-
for (const branch of branches) {
|
|
52
|
-
if (!branch.id) {
|
|
53
|
-
branch.id = nextBranchId(accSeed)
|
|
54
|
-
;(accSeed.branches as Branch[]).push(branch)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const seedsApp = new Hono<{ Bindings: Env; Variables: Variables }>()
|
|
60
|
-
|
|
61
|
-
/** GET /api/seeds — list all seed records (active + deleted). Admin-only. */
|
|
62
|
-
seedsApp.get('/', async (context) => {
|
|
63
|
-
const denied = requireAdmin(context)
|
|
64
|
-
if (denied) return denied
|
|
65
|
-
const records = await context.get('seedRepository').listAll()
|
|
66
|
-
return context.json(records)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
/** GET /api/seeds/:slug — single record by slug. Admin-only. */
|
|
70
|
-
seedsApp.get('/:slug', async (context) => {
|
|
71
|
-
const denied = requireAdmin(context)
|
|
72
|
-
if (denied) return denied
|
|
73
|
-
const slug = context.req.param('slug')
|
|
74
|
-
const record = await context.get('seedRepository').get(slug)
|
|
75
|
-
if (!record) {
|
|
76
|
-
return publicProblem(context, {
|
|
77
|
-
type: 'seed-not-found',
|
|
78
|
-
title: 'Seed not found',
|
|
79
|
-
status: 404,
|
|
80
|
-
detail: `No seed with slug '${slug}'.`,
|
|
81
|
-
})
|
|
82
|
-
}
|
|
83
|
-
return context.json(record)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
/** POST /api/seeds — create a new content type. */
|
|
87
|
-
seedsApp.post('/', async (context) => {
|
|
88
|
-
const denied = requireAdmin(context)
|
|
89
|
-
if (denied) return denied
|
|
90
|
-
|
|
91
|
-
let body: unknown
|
|
92
|
-
try { body = await context.req.json() } catch {
|
|
93
|
-
return publicProblem(context, { type: 'invalid-json', title: 'Invalid JSON', status: 400, detail: 'Body must be valid JSON.' })
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const candidate = body as Seed
|
|
97
|
-
const slug = candidate?.slug
|
|
98
|
-
|
|
99
|
-
if (!slug || !SLUG_RE.test(slug)) {
|
|
100
|
-
return publicProblem(context, { type: 'invalid-json', title: 'Bad Request', status: 400, detail: `slug must match ${SLUG_RE.source}.` })
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const repo = context.get('seedRepository')
|
|
104
|
-
const existing = await repo.get(slug)
|
|
105
|
-
|
|
106
|
-
if (existing && existing.status === 'active') {
|
|
107
|
-
return publicProblem(context, {
|
|
108
|
-
type: 'slug-conflict',
|
|
109
|
-
title: 'Slug conflict',
|
|
110
|
-
status: 409,
|
|
111
|
-
detail: `An active seed with slug '${slug}' already exists.`,
|
|
112
|
-
})
|
|
113
|
-
}
|
|
114
|
-
// If deleted, treat as revive+replace (upsert reactivates). Content table reused (additive).
|
|
115
|
-
|
|
116
|
-
// Assign ids to branches that are missing one
|
|
117
|
-
const accSeed: Pick<Seed, 'branches'> = { branches: [] }
|
|
118
|
-
const branches: Branch[] = Array.isArray(candidate.branches) ? candidate.branches : []
|
|
119
|
-
for (const branch of branches) {
|
|
120
|
-
if (!branch.id) {
|
|
121
|
-
branch.id = nextBranchId(accSeed)
|
|
122
|
-
}
|
|
123
|
-
;(accSeed.branches as Branch[]).push({ ...branch })
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Default displayNameAlias to first text branch if not provided
|
|
127
|
-
if (!candidate.displayNameAlias) {
|
|
128
|
-
const firstText = branches.find(b => b.type === 'text')
|
|
129
|
-
if (!firstText) {
|
|
130
|
-
return publicProblem(context, { type: 'invalid-json', title: 'Bad Request', status: 400, detail: 'displayNameAlias is required (or include at least one text branch so it can be inferred).' })
|
|
131
|
-
}
|
|
132
|
-
candidate.displayNameAlias = firstText.alias
|
|
133
|
-
}
|
|
134
|
-
candidate.branches = branches
|
|
135
|
-
|
|
136
|
-
// Validate against full active set
|
|
137
|
-
const activeSeeds = await repo.listActive()
|
|
138
|
-
const candidateSet = [...activeSeeds.filter(s => s.slug !== slug), candidate]
|
|
139
|
-
const issues = validateSeedDefinitions(candidateSet)
|
|
140
|
-
const fatalIssues = issues.filter(i => i.fatal && i.slug === slug)
|
|
141
|
-
if (fatalIssues.length > 0) {
|
|
142
|
-
return publicProblem(context, {
|
|
143
|
-
type: 'validation-failed',
|
|
144
|
-
title: 'Validation failed',
|
|
145
|
-
status: 422,
|
|
146
|
-
detail: fatalIssues.flatMap(i => i.messages).join('; '),
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const schemaMutator = context.get('schemaMutator')
|
|
151
|
-
const tableName = `content_${slug}`
|
|
152
|
-
const existingCols = await schemaMutator.getColumns(tableName)
|
|
153
|
-
|
|
154
|
-
// Order: validate → execDdl → upsert → bump → log
|
|
155
|
-
try {
|
|
156
|
-
const stmts = existingCols === null
|
|
157
|
-
? planCreateSeed(candidate)
|
|
158
|
-
: planExtendSeed(candidate, existingCols).statements
|
|
159
|
-
await schemaMutator.execDdl(stmts)
|
|
160
|
-
} catch (err) {
|
|
161
|
-
return publicProblem(context, {
|
|
162
|
-
type: 'ddl-failed',
|
|
163
|
-
title: 'DDL execution failed',
|
|
164
|
-
status: 422,
|
|
165
|
-
detail: internalErrorDetail(context.env, err),
|
|
166
|
-
})
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
await repo.upsert(slug, candidate, 'runtime')
|
|
170
|
-
await repo.bumpRegistryVersion()
|
|
171
|
-
|
|
172
|
-
const actor = actorFromContext(context)
|
|
173
|
-
context.get('activityLogger').log({ action: 'create', entityType: 'seed', entityId: slug, details: { slug }, actor })
|
|
174
|
-
|
|
175
|
-
return context.json({ slug }, 201)
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
/** PUT /api/seeds/:slug — replace definition (additive-only). */
|
|
179
|
-
seedsApp.put('/:slug', async (context) => {
|
|
180
|
-
const denied = requireAdmin(context)
|
|
181
|
-
if (denied) return denied
|
|
182
|
-
|
|
183
|
-
const slug = context.req.param('slug')
|
|
184
|
-
let body: unknown
|
|
185
|
-
try { body = await context.req.json() } catch {
|
|
186
|
-
return publicProblem(context, { type: 'invalid-json', title: 'Invalid JSON', status: 400, detail: 'Body must be valid JSON.' })
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const repo = context.get('seedRepository')
|
|
190
|
-
const existing = await repo.get(slug)
|
|
191
|
-
if (!existing || existing.status === 'deleted') {
|
|
192
|
-
return publicProblem(context, { type: 'seed-not-found', title: 'Seed not found', status: 404, detail: `No active seed with slug '${slug}'.` })
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const incoming = body as Seed
|
|
196
|
-
const storedBranches = existing.definition.branches
|
|
197
|
-
const incomingBranches: Branch[] = Array.isArray(incoming.branches) ? incoming.branches : []
|
|
198
|
-
|
|
199
|
-
// Build map of stored branches by id
|
|
200
|
-
const storedById = new Map(storedBranches.map(b => [b.id, b]))
|
|
201
|
-
|
|
202
|
-
for (const branch of incomingBranches) {
|
|
203
|
-
const stored = branch.id ? storedById.get(branch.id) : undefined
|
|
204
|
-
if (stored) {
|
|
205
|
-
// Alias rename check (same id, different alias)
|
|
206
|
-
if (branch.alias !== stored.alias) {
|
|
207
|
-
return publicProblem(context, {
|
|
208
|
-
type: 'alias-rename-not-supported',
|
|
209
|
-
title: 'Alias rename not supported',
|
|
210
|
-
status: 422,
|
|
211
|
-
detail: `Branch '${branch.id}' alias rename from '${stored.alias}' to '${branch.alias}' is irreversible. Use PATCH /api/seeds/${slug}/branches/${branch.id}/rename with a typed confirmation.`,
|
|
212
|
-
})
|
|
213
|
-
}
|
|
214
|
-
// Type change check
|
|
215
|
-
if (branch.type !== stored.type) {
|
|
216
|
-
return publicProblem(context, {
|
|
217
|
-
type: 'branch-type-change-not-supported',
|
|
218
|
-
title: 'Branch type change not supported',
|
|
219
|
-
status: 422,
|
|
220
|
-
detail: `Branch '${branch.id}' type change from '${stored.type}' to '${branch.type}' is irreversible. Use PATCH /api/seeds/${slug}/branches/${branch.id}/retype with a typed confirmation.`,
|
|
221
|
-
})
|
|
222
|
-
}
|
|
223
|
-
} else if (!branch.id) {
|
|
224
|
-
// New branch — assign id
|
|
225
|
-
const accSeed = { branches: incomingBranches.filter(b => b.id) }
|
|
226
|
-
branch.id = nextBranchId(accSeed)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const candidate: Seed = { ...incoming, slug, branches: incomingBranches }
|
|
231
|
-
|
|
232
|
-
const activeSeeds = await repo.listActive()
|
|
233
|
-
const candidateSet = [...activeSeeds.filter(s => s.slug !== slug), candidate]
|
|
234
|
-
const issues = validateSeedDefinitions(candidateSet)
|
|
235
|
-
const fatalIssues = issues.filter(i => i.fatal && i.slug === slug)
|
|
236
|
-
if (fatalIssues.length > 0) {
|
|
237
|
-
return publicProblem(context, {
|
|
238
|
-
type: 'validation-failed',
|
|
239
|
-
title: 'Validation failed',
|
|
240
|
-
status: 422,
|
|
241
|
-
detail: fatalIssues.flatMap(i => i.messages).join('; '),
|
|
242
|
-
})
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const schemaMutator = context.get('schemaMutator')
|
|
246
|
-
const tableName = `content_${slug}`
|
|
247
|
-
const existingCols = await schemaMutator.getColumns(tableName)
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
const stmts = existingCols === null
|
|
251
|
-
? planCreateSeed(candidate)
|
|
252
|
-
: planExtendSeed(candidate, existingCols).statements
|
|
253
|
-
await schemaMutator.execDdl(stmts)
|
|
254
|
-
} catch (err) {
|
|
255
|
-
return publicProblem(context, {
|
|
256
|
-
type: 'ddl-failed',
|
|
257
|
-
title: 'DDL execution failed',
|
|
258
|
-
status: 422,
|
|
259
|
-
detail: internalErrorDetail(context.env, err),
|
|
260
|
-
})
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
await repo.upsert(slug, candidate, 'runtime')
|
|
264
|
-
await repo.bumpRegistryVersion()
|
|
265
|
-
|
|
266
|
-
const actor = actorFromContext(context)
|
|
267
|
-
context.get('activityLogger').log({ action: 'update', entityType: 'seed', entityId: slug, details: { slug }, actor })
|
|
268
|
-
|
|
269
|
-
return context.json({ slug })
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
/** POST /api/seeds/:slug/branches — add a single branch. */
|
|
273
|
-
seedsApp.post('/:slug/branches', async (context) => {
|
|
274
|
-
const denied = requireAdmin(context)
|
|
275
|
-
if (denied) return denied
|
|
276
|
-
|
|
277
|
-
const slug = context.req.param('slug')
|
|
278
|
-
let body: unknown
|
|
279
|
-
try { body = await context.req.json() } catch {
|
|
280
|
-
return publicProblem(context, { type: 'invalid-json', title: 'Invalid JSON', status: 400, detail: 'Body must be valid JSON.' })
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const repo = context.get('seedRepository')
|
|
284
|
-
const existing = await repo.get(slug)
|
|
285
|
-
if (!existing || existing.status === 'deleted') {
|
|
286
|
-
return publicProblem(context, { type: 'seed-not-found', title: 'Seed not found', status: 404, detail: `No active seed with slug '${slug}'.` })
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const newBranch = body as Branch
|
|
290
|
-
newBranch.id = nextBranchId(existing.definition)
|
|
291
|
-
|
|
292
|
-
const candidate: Seed = {
|
|
293
|
-
...existing.definition,
|
|
294
|
-
branches: [...existing.definition.branches, newBranch],
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const activeSeeds = await repo.listActive()
|
|
298
|
-
const candidateSet = [...activeSeeds.filter(s => s.slug !== slug), candidate]
|
|
299
|
-
const issues = validateSeedDefinitions(candidateSet)
|
|
300
|
-
const fatalIssues = issues.filter(i => i.fatal && i.slug === slug)
|
|
301
|
-
if (fatalIssues.length > 0) {
|
|
302
|
-
return publicProblem(context, {
|
|
303
|
-
type: 'validation-failed',
|
|
304
|
-
title: 'Validation failed',
|
|
305
|
-
status: 422,
|
|
306
|
-
detail: fatalIssues.flatMap(i => i.messages).join('; '),
|
|
307
|
-
})
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const schemaMutator = context.get('schemaMutator')
|
|
311
|
-
const tableName = `content_${slug}`
|
|
312
|
-
const existingCols = await schemaMutator.getColumns(tableName)
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
const stmts = existingCols === null
|
|
316
|
-
? planCreateSeed(candidate)
|
|
317
|
-
: planExtendSeed(candidate, existingCols).statements
|
|
318
|
-
await schemaMutator.execDdl(stmts)
|
|
319
|
-
} catch (err) {
|
|
320
|
-
return publicProblem(context, {
|
|
321
|
-
type: 'ddl-failed',
|
|
322
|
-
title: 'DDL execution failed',
|
|
323
|
-
status: 422,
|
|
324
|
-
detail: internalErrorDetail(context.env, err),
|
|
325
|
-
})
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
await repo.upsert(slug, candidate, 'runtime')
|
|
329
|
-
await repo.bumpRegistryVersion()
|
|
330
|
-
|
|
331
|
-
const actor = actorFromContext(context)
|
|
332
|
-
context.get('activityLogger').log({ action: 'update', entityType: 'seed', entityId: slug, details: { addedBranch: newBranch.id }, actor })
|
|
333
|
-
|
|
334
|
-
return context.json({ id: newBranch.id })
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
/** DELETE /api/seeds/:slug — soft-delete a content type. */
|
|
338
|
-
seedsApp.delete('/:slug', async (context) => {
|
|
339
|
-
const denied = requireAdmin(context)
|
|
340
|
-
if (denied) return denied
|
|
341
|
-
|
|
342
|
-
const slug = context.req.param('slug')
|
|
343
|
-
const repo = context.get('seedRepository')
|
|
344
|
-
const existing = await repo.get(slug)
|
|
345
|
-
|
|
346
|
-
if (!existing || existing.status === 'deleted') {
|
|
347
|
-
return publicProblem(context, { type: 'seed-not-found', title: 'Seed not found', status: 404, detail: `No active seed with slug '${slug}'.` })
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Backref guard: reject if other active seeds reference this slug via relation branches
|
|
351
|
-
const backrefMap = context.get('backrefMap')
|
|
352
|
-
const inbound = backrefMap.get(slug)
|
|
353
|
-
if (inbound && inbound.length > 0) {
|
|
354
|
-
const referencers = [...new Set(inbound.map((r) => r.sourceSlug))]
|
|
355
|
-
return publicProblem(context, {
|
|
356
|
-
type: 'seed-referenced',
|
|
357
|
-
title: 'Seed referenced',
|
|
358
|
-
status: 409,
|
|
359
|
-
detail: `Seed '${slug}' is referenced by: ${referencers.join(', ')}. Remove those relations first.`,
|
|
360
|
-
})
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
await repo.softDelete(slug)
|
|
364
|
-
await repo.bumpRegistryVersion()
|
|
365
|
-
|
|
366
|
-
const actor = actorFromContext(context)
|
|
367
|
-
context.get('activityLogger').log({ action: 'delete', entityType: 'seed', entityId: slug, details: { slug }, actor })
|
|
368
|
-
|
|
369
|
-
return context.json({ success: true })
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
// ─── Sprint 06: Destructive Danger Zone routes ────────────────────────────────
|
|
373
|
-
// All destructive ops require a typed confirmation body `{ confirm: "<slug>" }`
|
|
374
|
-
// (or `"<slug>.<alias>"` for field-level ops) that the handler checks before
|
|
375
|
-
// proceeding. Mismatch → 400 confirmation-required.
|
|
376
|
-
//
|
|
377
|
-
// None of these routes must ever call execDdl — they go through the dedicated
|
|
378
|
-
// ISchemaMutator destructive methods (dropTable / dropColumn / renameColumn /
|
|
379
|
-
// execDestructive) which are the only sanctioned channel for DROP / RENAME.
|
|
380
|
-
|
|
381
|
-
/** Typed confirm guard. Returns a 400 response on mismatch, null on success. */
|
|
382
|
-
function requireConfirm(context: any, expected: string, body: unknown) {
|
|
383
|
-
const confirm = (body as Record<string, unknown>)?.confirm
|
|
384
|
-
if (typeof confirm !== 'string' || confirm !== expected) {
|
|
385
|
-
return publicProblem(context, {
|
|
386
|
-
type: 'confirmation-required',
|
|
387
|
-
title: 'Confirmation required',
|
|
388
|
-
status: 400,
|
|
389
|
-
detail: `Destructive operation requires body field \`confirm\` equal to "${expected}".`,
|
|
390
|
-
})
|
|
391
|
-
}
|
|
392
|
-
return null
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/** GET /api/seeds/:slug/orphans — columns in DB but absent from the definition. */
|
|
396
|
-
seedsApp.get('/:slug/orphans', async (context) => {
|
|
397
|
-
const denied = requireAdmin(context)
|
|
398
|
-
if (denied) return denied
|
|
399
|
-
|
|
400
|
-
const slug = context.req.param('slug')
|
|
401
|
-
const repo = context.get('seedRepository')
|
|
402
|
-
const existing = await repo.get(slug)
|
|
403
|
-
if (!existing || existing.status === 'deleted') {
|
|
404
|
-
return publicProblem(context, { type: 'seed-not-found', title: 'Seed not found', status: 404, detail: `No active seed with slug '${slug}'.` })
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
const schemaMutator = context.get('schemaMutator')
|
|
408
|
-
const dbCols = await schemaMutator.getColumns(`content_${slug}`)
|
|
409
|
-
if (!dbCols) return context.json({ orphans: [] })
|
|
410
|
-
|
|
411
|
-
const knownAliases = new Set([
|
|
412
|
-
'id', 'slug', 'status', 'created_at', 'updated_at',
|
|
413
|
-
...existing.definition.branches
|
|
414
|
-
.filter((b: Branch) => !(b.type === 'relation' && b.multiple === true))
|
|
415
|
-
.map((b: Branch) => b.alias),
|
|
416
|
-
])
|
|
417
|
-
const orphans = [...dbCols].filter(col => !knownAliases.has(col))
|
|
418
|
-
return context.json({ orphans })
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
/** POST /api/seeds/:slug/fts/rebuild — rebuild FTS table + triggers. */
|
|
422
|
-
seedsApp.post('/:slug/fts/rebuild', async (context) => {
|
|
423
|
-
const denied = requireAdmin(context)
|
|
424
|
-
if (denied) return denied
|
|
425
|
-
|
|
426
|
-
const slug = context.req.param('slug')
|
|
427
|
-
const repo = context.get('seedRepository')
|
|
428
|
-
const existing = await repo.get(slug)
|
|
429
|
-
if (!existing || existing.status === 'deleted') {
|
|
430
|
-
return publicProblem(context, { type: 'seed-not-found', title: 'Seed not found', status: 404, detail: `No active seed with slug '${slug}'.` })
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const stmts = planFtsRebuild(existing.definition)
|
|
434
|
-
if (stmts.length > 0) {
|
|
435
|
-
await context.get('schemaMutator').execDestructive(stmts)
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const actor = actorFromContext(context)
|
|
439
|
-
context.get('activityLogger').log({ action: 'update', entityType: 'seed', entityId: slug, details: { op: 'fts-rebuild' }, actor })
|
|
440
|
-
|
|
441
|
-
return context.json({ success: true })
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
/** DELETE /api/seeds/:slug/hard — hard delete: drops tables + deletes row. */
|
|
445
|
-
seedsApp.delete('/:slug/hard', async (context) => {
|
|
446
|
-
const denied = requireAdmin(context)
|
|
447
|
-
if (denied) return denied
|
|
448
|
-
|
|
449
|
-
const slug = context.req.param('slug')
|
|
450
|
-
let body: unknown
|
|
451
|
-
try { body = await context.req.json() } catch {
|
|
452
|
-
return publicProblem(context, { type: 'invalid-json', title: 'Invalid JSON', status: 400, detail: 'Body must be valid JSON.' })
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const confirmErr = requireConfirm(context, slug, body)
|
|
456
|
-
if (confirmErr) return confirmErr
|
|
457
|
-
|
|
458
|
-
const repo = context.get('seedRepository')
|
|
459
|
-
const existing = await repo.get(slug)
|
|
460
|
-
if (!existing || existing.status === 'deleted') {
|
|
461
|
-
return publicProblem(context, { type: 'seed-not-found', title: 'Seed not found', status: 404, detail: `No active seed with slug '${slug}'.` })
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Backref guard — same logic as soft-delete.
|
|
465
|
-
const backrefMap = context.get('backrefMap')
|
|
466
|
-
const inbound = backrefMap.get(slug)
|
|
467
|
-
if (inbound && inbound.length > 0) {
|
|
468
|
-
const referencers = [...new Set(inbound.map((r: any) => r.sourceSlug))]
|
|
469
|
-
return publicProblem(context, {
|
|
470
|
-
type: 'seed-referenced',
|
|
471
|
-
title: 'Seed referenced',
|
|
472
|
-
status: 409,
|
|
473
|
-
detail: `Seed '${slug}' is referenced by: ${referencers.join(', ')}. Remove those relations first.`,
|
|
474
|
-
})
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
const schemaMutator = context.get('schemaMutator')
|
|
478
|
-
const seed = existing.definition
|
|
479
|
-
|
|
480
|
-
// R2 cascade — enumerate file branches, select values, delete best-effort.
|
|
481
|
-
const fileBranches = seed.branches.filter((b: Branch) => b.type === 'file')
|
|
482
|
-
if (fileBranches.length > 0) {
|
|
483
|
-
try {
|
|
484
|
-
const dbCols = await schemaMutator.getColumns(`content_${slug}`)
|
|
485
|
-
if (dbCols) {
|
|
486
|
-
const colList = fileBranches.map((b: Branch) => b.alias).filter((a: string) => dbCols.has(a)).join(', ')
|
|
487
|
-
if (colList) {
|
|
488
|
-
const rs = await context.env.DB.prepare(`SELECT ${colList} FROM content_${slug}`).all<Record<string, unknown>>()
|
|
489
|
-
const rows = rs.results ?? []
|
|
490
|
-
const r2Keys: string[] = []
|
|
491
|
-
for (const row of rows) {
|
|
492
|
-
for (const b of fileBranches) {
|
|
493
|
-
const val = row[b.alias]
|
|
494
|
-
if (!val) continue
|
|
495
|
-
const keys = extractMediaKeysFromData(seed, { [b.alias]: val })
|
|
496
|
-
r2Keys.push(...keys)
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
if (r2Keys.length > 0) {
|
|
500
|
-
await deleteR2Objects(context, r2Keys).catch(() => { /* non-fatal */ })
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
} catch { /* non-fatal: drop proceeds */ }
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Drop tables (junction → drafts → fts → main).
|
|
508
|
-
await schemaMutator.execDestructive(generateDropTable(seed))
|
|
509
|
-
|
|
510
|
-
// Remove the seeds row.
|
|
511
|
-
await repo.hardDelete(slug)
|
|
512
|
-
await repo.bumpRegistryVersion()
|
|
513
|
-
|
|
514
|
-
const actor = actorFromContext(context)
|
|
515
|
-
context.get('activityLogger').log({ action: 'delete', entityType: 'seed', entityId: slug, details: { op: 'hard-delete', slug }, actor })
|
|
516
|
-
|
|
517
|
-
return context.json({ success: true })
|
|
518
|
-
})
|
|
519
|
-
|
|
520
|
-
/** DELETE /api/seeds/:slug/branches/:branchId — drop a single field column. */
|
|
521
|
-
seedsApp.delete('/:slug/branches/:branchId', async (context) => {
|
|
522
|
-
const denied = requireAdmin(context)
|
|
523
|
-
if (denied) return denied
|
|
524
|
-
|
|
525
|
-
const slug = context.req.param('slug')
|
|
526
|
-
const branchId = context.req.param('branchId')
|
|
527
|
-
let body: unknown
|
|
528
|
-
try { body = await context.req.json() } catch {
|
|
529
|
-
return publicProblem(context, { type: 'invalid-json', title: 'Invalid JSON', status: 400, detail: 'Body must be valid JSON.' })
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const repo = context.get('seedRepository')
|
|
533
|
-
const existing = await repo.get(slug)
|
|
534
|
-
if (!existing || existing.status === 'deleted') {
|
|
535
|
-
return publicProblem(context, { type: 'seed-not-found', title: 'Seed not found', status: 404, detail: `No active seed with slug '${slug}'.` })
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const branch = existing.definition.branches.find((b: Branch) => b.id === branchId)
|
|
539
|
-
if (!branch) {
|
|
540
|
-
return publicProblem(context, { type: 'branch-not-found', title: 'Branch not found', status: 404, detail: `No branch with id '${branchId}' in seed '${slug}'.` })
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const confirmErr = requireConfirm(context, `${slug}.${branch.alias}`, body)
|
|
544
|
-
if (confirmErr) return confirmErr
|
|
545
|
-
|
|
546
|
-
const schemaMutator = context.get('schemaMutator')
|
|
547
|
-
try {
|
|
548
|
-
await schemaMutator.execDestructive(generateDropColumn(existing.definition, branch.alias))
|
|
549
|
-
} catch (err) {
|
|
550
|
-
return publicProblem(context, { type: 'ddl-failed', title: 'DDL failed', status: 422, detail: internalErrorDetail(context.env, err) })
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Remove the branch from the definition.
|
|
554
|
-
const updatedDef: Seed = {
|
|
555
|
-
...existing.definition,
|
|
556
|
-
branches: existing.definition.branches.filter((b: Branch) => b.id !== branchId),
|
|
557
|
-
}
|
|
558
|
-
await repo.upsert(slug, updatedDef, 'runtime')
|
|
559
|
-
await repo.bumpRegistryVersion()
|
|
560
|
-
|
|
561
|
-
const actor = actorFromContext(context)
|
|
562
|
-
context.get('activityLogger').log({ action: 'update', entityType: 'seed', entityId: slug, details: { op: 'drop-branch', branchId, alias: branch.alias }, actor })
|
|
563
|
-
|
|
564
|
-
return context.json({ success: true })
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
/** PATCH /api/seeds/:slug/branches/:branchId/rename — rename a field alias. */
|
|
568
|
-
seedsApp.patch('/:slug/branches/:branchId/rename', async (context) => {
|
|
569
|
-
const denied = requireAdmin(context)
|
|
570
|
-
if (denied) return denied
|
|
571
|
-
|
|
572
|
-
const slug = context.req.param('slug')
|
|
573
|
-
const branchId = context.req.param('branchId')
|
|
574
|
-
let body: unknown
|
|
575
|
-
try { body = await context.req.json() } catch {
|
|
576
|
-
return publicProblem(context, { type: 'invalid-json', title: 'Invalid JSON', status: 400, detail: 'Body must be valid JSON.' })
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const newAlias = (body as Record<string, unknown>)?.newAlias
|
|
580
|
-
if (typeof newAlias !== 'string' || !BRANCH_ALIAS_RE.test(newAlias)) {
|
|
581
|
-
return publicProblem(context, { type: 'invalid-json', title: 'Bad Request', status: 400, detail: `newAlias must match ${BRANCH_ALIAS_RE.source} (lowercase letter followed by lowercase letters, digits or underscores).` })
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const repo = context.get('seedRepository')
|
|
585
|
-
const existing = await repo.get(slug)
|
|
586
|
-
if (!existing || existing.status === 'deleted') {
|
|
587
|
-
return publicProblem(context, { type: 'seed-not-found', title: 'Seed not found', status: 404, detail: `No active seed with slug '${slug}'.` })
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const branch = existing.definition.branches.find((b: Branch) => b.id === branchId)
|
|
591
|
-
if (!branch) {
|
|
592
|
-
return publicProblem(context, { type: 'branch-not-found', title: 'Branch not found', status: 404, detail: `No branch with id '${branchId}' in seed '${slug}'.` })
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const confirmErr = requireConfirm(context, `${slug}.${branch.alias}`, body)
|
|
596
|
-
if (confirmErr) return confirmErr
|
|
597
|
-
|
|
598
|
-
const schemaMutator = context.get('schemaMutator')
|
|
599
|
-
|
|
600
|
-
// Rename column (+ drafts mirror). Then rebuild FTS if the branch is searchable.
|
|
601
|
-
const renamedDef: Seed = {
|
|
602
|
-
...existing.definition,
|
|
603
|
-
branches: existing.definition.branches.map((b: Branch) =>
|
|
604
|
-
b.id === branchId ? { ...b, alias: newAlias } : b
|
|
605
|
-
),
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
const renameStmts = generateRenameColumn(existing.definition, branch.alias, newAlias)
|
|
609
|
-
const ftsStmts = planFtsRebuild(renamedDef)
|
|
610
|
-
|
|
611
|
-
try {
|
|
612
|
-
await schemaMutator.execDestructive([...renameStmts, ...ftsStmts])
|
|
613
|
-
} catch (err) {
|
|
614
|
-
return publicProblem(context, { type: 'ddl-failed', title: 'DDL failed', status: 422, detail: internalErrorDetail(context.env, err) })
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// Scan automations for references to the old alias and warn.
|
|
618
|
-
let affectedAutomations: string[] = []
|
|
619
|
-
try {
|
|
620
|
-
const automationRepo = context.get('automationRepository')
|
|
621
|
-
if (automationRepo) {
|
|
622
|
-
const automations = await automationRepo.list(slug)
|
|
623
|
-
const oldAlias = branch.alias
|
|
624
|
-
affectedAutomations = automations
|
|
625
|
-
.filter((a: any) => JSON.stringify(a).includes(oldAlias))
|
|
626
|
-
.map((a: any) => a.id)
|
|
627
|
-
}
|
|
628
|
-
} catch { /* non-fatal */ }
|
|
629
|
-
|
|
630
|
-
await repo.upsert(slug, renamedDef, 'runtime')
|
|
631
|
-
await repo.bumpRegistryVersion()
|
|
632
|
-
|
|
633
|
-
const actor = actorFromContext(context)
|
|
634
|
-
context.get('activityLogger').log({ action: 'update', entityType: 'seed', entityId: slug, details: { op: 'rename-branch', branchId, from: branch.alias, to: newAlias }, actor })
|
|
635
|
-
|
|
636
|
-
return context.json({ success: true, affectedAutomations })
|
|
637
|
-
})
|
|
638
|
-
|
|
639
|
-
/** PATCH /api/seeds/:slug/branches/:branchId/retype — change field SQL type. */
|
|
640
|
-
seedsApp.patch('/:slug/branches/:branchId/retype', async (context) => {
|
|
641
|
-
const denied = requireAdmin(context)
|
|
642
|
-
if (denied) return denied
|
|
643
|
-
|
|
644
|
-
const slug = context.req.param('slug')
|
|
645
|
-
const branchId = context.req.param('branchId')
|
|
646
|
-
let body: unknown
|
|
647
|
-
try { body = await context.req.json() } catch {
|
|
648
|
-
return publicProblem(context, { type: 'invalid-json', title: 'Invalid JSON', status: 400, detail: 'Body must be valid JSON.' })
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const newType = (body as Record<string, unknown>)?.newType
|
|
652
|
-
const VALID_TYPES = new Set(['text','number','boolean','date','json','richtext','file','tags','relation'])
|
|
653
|
-
if (typeof newType !== 'string' || !VALID_TYPES.has(newType)) {
|
|
654
|
-
return publicProblem(context, { type: 'invalid-json', title: 'Bad Request', status: 400, detail: `newType must be one of: ${[...VALID_TYPES].join(', ')}.` })
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const repo = context.get('seedRepository')
|
|
658
|
-
const existing = await repo.get(slug)
|
|
659
|
-
if (!existing || existing.status === 'deleted') {
|
|
660
|
-
return publicProblem(context, { type: 'seed-not-found', title: 'Seed not found', status: 404, detail: `No active seed with slug '${slug}'.` })
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const branch = existing.definition.branches.find((b: Branch) => b.id === branchId)
|
|
664
|
-
if (!branch) {
|
|
665
|
-
return publicProblem(context, { type: 'branch-not-found', title: 'Branch not found', status: 404, detail: `No branch with id '${branchId}' in seed '${slug}'.` })
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
const confirmErr = requireConfirm(context, `${slug}.${branch.alias}`, body)
|
|
669
|
-
if (confirmErr) return confirmErr
|
|
670
|
-
|
|
671
|
-
const retypedBranch: Branch = { ...branch, type: newType as Branch['type'] }
|
|
672
|
-
const retypedDef: Seed = {
|
|
673
|
-
...existing.definition,
|
|
674
|
-
branches: existing.definition.branches.map((b: Branch) => b.id === branchId ? retypedBranch : b),
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
const retypeStmts = generateRetypeColumn(existing.definition, retypedBranch)
|
|
678
|
-
const ftsStmts = planFtsRebuild(retypedDef)
|
|
679
|
-
|
|
680
|
-
try {
|
|
681
|
-
await context.get('schemaMutator').execDestructive([...retypeStmts, ...ftsStmts])
|
|
682
|
-
} catch (err) {
|
|
683
|
-
return publicProblem(context, { type: 'ddl-failed', title: 'DDL failed', status: 422, detail: internalErrorDetail(context.env, err) })
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
await repo.upsert(slug, retypedDef, 'runtime')
|
|
687
|
-
await repo.bumpRegistryVersion()
|
|
688
|
-
|
|
689
|
-
const actor = actorFromContext(context)
|
|
690
|
-
context.get('activityLogger').log({ action: 'update', entityType: 'seed', entityId: slug, details: { op: 'retype-branch', branchId, from: branch.type, to: newType }, actor })
|
|
691
|
-
|
|
692
|
-
return context.json({ success: true })
|
|
693
|
-
})
|