@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,180 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
-
import { Context } from 'hono'
|
|
3
|
-
import { EntryNotFoundError, SlugConflictError } from '@beechcms/core'
|
|
4
|
-
import { normalizeBody, contentValidationProblem, logContentActivity, dispatchContentAutomation, handleContentDatabaseError } from './helpers'
|
|
5
|
-
import { CONTENT_ERRORS } from '../constants'
|
|
6
|
-
|
|
7
|
-
// Mock dependencies
|
|
8
|
-
vi.mock('../../../public/problem-details', () => ({
|
|
9
|
-
publicProblem: vi.fn((_ctx, details) => ({ mockProblem: true, ...details })),
|
|
10
|
-
fkProblemOrNull: vi.fn((_ctx, err, method) => {
|
|
11
|
-
if (err instanceof Error && err.message === 'FK_ERROR') return { mockFkProblem: true }
|
|
12
|
-
return null
|
|
13
|
-
}),
|
|
14
|
-
}))
|
|
15
|
-
|
|
16
|
-
describe('content handlers helpers', () => {
|
|
17
|
-
describe('normalizeBody', () => {
|
|
18
|
-
it('returns the object when input is an object', () => {
|
|
19
|
-
const obj = { a: 1 }
|
|
20
|
-
expect(normalizeBody(obj)).toBe(obj)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('returns an empty object when input is null', () => {
|
|
24
|
-
expect(normalizeBody(null)).toEqual({})
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('returns an empty object when input is a primitive', () => {
|
|
28
|
-
expect(normalizeBody('string')).toEqual({})
|
|
29
|
-
expect(normalizeBody(123)).toEqual({})
|
|
30
|
-
expect(normalizeBody(true)).toEqual({})
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
describe('contentValidationProblem', () => {
|
|
35
|
-
it('calls publicProblem with correct parameters', () => {
|
|
36
|
-
const ctx = {} as Context
|
|
37
|
-
const errors = [{ field: 'title', expected: 'string', received: 'number', message: 'Wrong type' }]
|
|
38
|
-
const result = contentValidationProblem(ctx, errors)
|
|
39
|
-
|
|
40
|
-
expect(result).toEqual({
|
|
41
|
-
mockProblem: true,
|
|
42
|
-
type: 'content-validation-failed',
|
|
43
|
-
title: 'Bad Request',
|
|
44
|
-
status: 400,
|
|
45
|
-
detail: 'Validation failed',
|
|
46
|
-
errors,
|
|
47
|
-
})
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe('logContentActivity', () => {
|
|
52
|
-
it('logs activity with correct payload', () => {
|
|
53
|
-
const mockLog = vi.fn()
|
|
54
|
-
const ctx = {
|
|
55
|
-
get: vi.fn((key) => {
|
|
56
|
-
if (key === 'jwtPayload') return { sub: 'user-1', email: 'test@test.com', name: 'John', surname: 'Doe' }
|
|
57
|
-
if (key === 'activityLogger') return { log: mockLog }
|
|
58
|
-
})
|
|
59
|
-
} as unknown as Context<any>
|
|
60
|
-
|
|
61
|
-
logContentActivity(ctx, 'create', 'entry-1', 'test-slug', 'Test Title')
|
|
62
|
-
|
|
63
|
-
expect(mockLog).toHaveBeenCalledWith({
|
|
64
|
-
action: 'create',
|
|
65
|
-
entityType: 'content',
|
|
66
|
-
entityId: 'entry-1',
|
|
67
|
-
entitySlug: 'test-slug',
|
|
68
|
-
details: { title: 'Test Title' },
|
|
69
|
-
actor: {
|
|
70
|
-
id: 'user-1',
|
|
71
|
-
email: 'test@test.com',
|
|
72
|
-
name: 'John Doe',
|
|
73
|
-
},
|
|
74
|
-
})
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('handles missing name and surname', () => {
|
|
78
|
-
const mockLog = vi.fn()
|
|
79
|
-
const ctx = {
|
|
80
|
-
get: vi.fn((key) => {
|
|
81
|
-
if (key === 'jwtPayload') return { sub: 'user-1' } // Missing name, surname, email
|
|
82
|
-
if (key === 'activityLogger') return { log: mockLog }
|
|
83
|
-
})
|
|
84
|
-
} as unknown as Context<any>
|
|
85
|
-
|
|
86
|
-
logContentActivity(ctx, 'update', 'entry-1', 'test-slug', 'Test Title')
|
|
87
|
-
|
|
88
|
-
expect(mockLog).toHaveBeenCalledWith({
|
|
89
|
-
action: 'update',
|
|
90
|
-
entityType: 'content',
|
|
91
|
-
entityId: 'entry-1',
|
|
92
|
-
entitySlug: 'test-slug',
|
|
93
|
-
details: { title: 'Test Title' },
|
|
94
|
-
actor: {
|
|
95
|
-
id: 'user-1',
|
|
96
|
-
email: 'unknown',
|
|
97
|
-
name: null,
|
|
98
|
-
},
|
|
99
|
-
})
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe('dispatchContentAutomation', () => {
|
|
104
|
-
it('schedules automation run', () => {
|
|
105
|
-
const mockWaitUntil = vi.fn()
|
|
106
|
-
const mockRun = vi.fn().mockReturnValue('mock-promise')
|
|
107
|
-
const ctx = {
|
|
108
|
-
get: vi.fn((key) => {
|
|
109
|
-
if (key === 'scheduler') return { waitUntil: mockWaitUntil }
|
|
110
|
-
if (key === 'automationRunner') return { run: mockRun }
|
|
111
|
-
})
|
|
112
|
-
} as unknown as Context<any>
|
|
113
|
-
|
|
114
|
-
const entry = { id: 1 }
|
|
115
|
-
dispatchContentAutomation(ctx, 'posts', 'create', entry)
|
|
116
|
-
|
|
117
|
-
expect(mockRun).toHaveBeenCalledWith({
|
|
118
|
-
seedSlug: 'posts',
|
|
119
|
-
event: 'create',
|
|
120
|
-
entry,
|
|
121
|
-
})
|
|
122
|
-
expect(mockWaitUntil).toHaveBeenCalledWith('mock-promise')
|
|
123
|
-
})
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
describe('handleContentDatabaseError', () => {
|
|
127
|
-
it('handles EntryNotFoundError', () => {
|
|
128
|
-
const ctx = {} as Context<any>
|
|
129
|
-
const error = new EntryNotFoundError('Not found')
|
|
130
|
-
const result = handleContentDatabaseError(ctx, error)
|
|
131
|
-
|
|
132
|
-
expect(result).toEqual({
|
|
133
|
-
mockProblem: true,
|
|
134
|
-
type: 'content-not-found',
|
|
135
|
-
title: 'Not Found',
|
|
136
|
-
status: 404,
|
|
137
|
-
detail: CONTENT_ERRORS.NOT_FOUND,
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('handles SlugConflictError', () => {
|
|
142
|
-
const ctx = {} as Context<any>
|
|
143
|
-
const error = new SlugConflictError('Conflict', 'slug')
|
|
144
|
-
const result = handleContentDatabaseError(ctx, error)
|
|
145
|
-
|
|
146
|
-
expect(result).toEqual({
|
|
147
|
-
mockProblem: true,
|
|
148
|
-
type: 'content-slug-conflict',
|
|
149
|
-
title: 'Conflict',
|
|
150
|
-
status: 409,
|
|
151
|
-
detail: CONTENT_ERRORS.SLUG_CONFLICT,
|
|
152
|
-
})
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
it('handles foreign key error', () => {
|
|
156
|
-
const ctx = { req: { method: 'POST' } } as unknown as Context<any>
|
|
157
|
-
const error = new Error('FK_ERROR')
|
|
158
|
-
const result = handleContentDatabaseError(ctx, error)
|
|
159
|
-
|
|
160
|
-
expect(result).toEqual({ mockFkProblem: true })
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('handles generic errors as 500', () => {
|
|
164
|
-
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
165
|
-
const ctx = { req: { method: 'POST' } } as unknown as Context<any>
|
|
166
|
-
const error = new Error('Some db error')
|
|
167
|
-
const result = handleContentDatabaseError(ctx, error)
|
|
168
|
-
|
|
169
|
-
expect(result).toEqual({
|
|
170
|
-
mockProblem: true,
|
|
171
|
-
type: 'content-database-error',
|
|
172
|
-
title: 'Internal Server Error',
|
|
173
|
-
status: 500,
|
|
174
|
-
detail: CONTENT_ERRORS.DATABASE_ERROR,
|
|
175
|
-
})
|
|
176
|
-
expect(consoleSpy).toHaveBeenCalledWith('Content database error:', error)
|
|
177
|
-
consoleSpy.mockRestore()
|
|
178
|
-
})
|
|
179
|
-
})
|
|
180
|
-
})
|
|
@@ -1,93 +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
|
-
import { Context } from 'hono'
|
|
6
|
-
import { EntryNotFoundError, SlugConflictError } from '@beechcms/core'
|
|
7
|
-
import { publicProblem, fkProblemOrNull } from '../../../public/problem-details'
|
|
8
|
-
import { CONTENT_ERRORS } from '../constants'
|
|
9
|
-
import { AppEnv } from '../../../types'
|
|
10
|
-
|
|
11
|
-
export function normalizeBody(raw: unknown): Record<string, unknown> {
|
|
12
|
-
return typeof raw === 'object' && raw !== null ? (raw as Record<string, unknown>) : {}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function contentValidationProblem(
|
|
16
|
-
context: Context,
|
|
17
|
-
details: Array<{ field: string; expected: string; received: string; message: string }>
|
|
18
|
-
) {
|
|
19
|
-
return publicProblem(context, {
|
|
20
|
-
type: 'content-validation-failed',
|
|
21
|
-
title: 'Bad Request',
|
|
22
|
-
status: 400,
|
|
23
|
-
detail: 'Validation failed',
|
|
24
|
-
errors: details
|
|
25
|
-
})
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function logContentActivity(
|
|
29
|
-
context: Context<AppEnv>,
|
|
30
|
-
action: 'create' | 'update' | 'delete',
|
|
31
|
-
id: string,
|
|
32
|
-
slug: string,
|
|
33
|
-
title: string
|
|
34
|
-
) {
|
|
35
|
-
const jwtPayload = context.get('jwtPayload')
|
|
36
|
-
context.get('activityLogger').log({
|
|
37
|
-
action,
|
|
38
|
-
entityType: 'content',
|
|
39
|
-
entityId: id,
|
|
40
|
-
entitySlug: slug,
|
|
41
|
-
details: { title },
|
|
42
|
-
actor: {
|
|
43
|
-
id: jwtPayload.sub,
|
|
44
|
-
email: jwtPayload.email ?? 'unknown',
|
|
45
|
-
name: [jwtPayload.name, jwtPayload.surname].filter(Boolean).join(' ') || null,
|
|
46
|
-
},
|
|
47
|
-
})
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function dispatchContentAutomation(
|
|
51
|
-
context: Context<AppEnv>,
|
|
52
|
-
seedSlug: string,
|
|
53
|
-
event: 'create' | 'update' | 'delete',
|
|
54
|
-
entry: Record<string, unknown>
|
|
55
|
-
) {
|
|
56
|
-
context.get('scheduler').waitUntil(
|
|
57
|
-
context.get('automationRunner').run({
|
|
58
|
-
seedSlug,
|
|
59
|
-
event,
|
|
60
|
-
entry,
|
|
61
|
-
}),
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function handleContentDatabaseError(context: Context<AppEnv>, error: unknown) {
|
|
66
|
-
if (error instanceof EntryNotFoundError) {
|
|
67
|
-
return publicProblem(context, {
|
|
68
|
-
type: 'content-not-found',
|
|
69
|
-
title: 'Not Found',
|
|
70
|
-
status: 404,
|
|
71
|
-
detail: CONTENT_ERRORS.NOT_FOUND
|
|
72
|
-
})
|
|
73
|
-
}
|
|
74
|
-
if (error instanceof SlugConflictError) {
|
|
75
|
-
return publicProblem(context, {
|
|
76
|
-
type: 'content-slug-conflict',
|
|
77
|
-
title: 'Conflict',
|
|
78
|
-
status: 409,
|
|
79
|
-
detail: CONTENT_ERRORS.SLUG_CONFLICT,
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const fkResponse = fkProblemOrNull(context, error, context.req.method)
|
|
84
|
-
if (fkResponse) return fkResponse
|
|
85
|
-
|
|
86
|
-
console.error('Content database error:', error)
|
|
87
|
-
return publicProblem(context, {
|
|
88
|
-
type: 'content-database-error',
|
|
89
|
-
title: 'Internal Server Error',
|
|
90
|
-
status: 500,
|
|
91
|
-
detail: CONTENT_ERRORS.DATABASE_ERROR,
|
|
92
|
-
})
|
|
93
|
-
}
|
|
@@ -1,174 +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
|
-
import { Context } from 'hono'
|
|
6
|
-
import { parsePositiveInt, parseQueryFilters, cleanStr, toEngineFilters } from '../../../shared/query-utils'
|
|
7
|
-
import { applyVisibility } from '../../../shared/apply-policies'
|
|
8
|
-
import { publicProblem } from '../../../public/problem-details'
|
|
9
|
-
import { CONTENT_ERRORS } from '../constants'
|
|
10
|
-
import { AppEnv } from '../../../types'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Builds a compact `relations` map for the list response.
|
|
14
|
-
* For each relation branch in the seed, collects non-null referenced ids from
|
|
15
|
-
* the page items, then fires ONE batched query per target seed to retrieve
|
|
16
|
-
* only the label column. This is O(R) extra queries per page (R = # relation
|
|
17
|
-
* branches) instead of O(N*R) per-row fetches.
|
|
18
|
-
*
|
|
19
|
-
* Shape: { [branchAlias]: { [targetId]: labelString } }
|
|
20
|
-
*/
|
|
21
|
-
async function buildRelationsMap(
|
|
22
|
-
context: Context<AppEnv>,
|
|
23
|
-
seed: Parameters<typeof applyVisibility>[1],
|
|
24
|
-
entries: Record<string, unknown>[]
|
|
25
|
-
): Promise<Record<string, Record<string, string>>> {
|
|
26
|
-
const relationBranches = seed.branches.filter(
|
|
27
|
-
(b: { type: string }) => b.type === 'relation'
|
|
28
|
-
) as Array<{ alias: string; targetSeed?: string }>
|
|
29
|
-
|
|
30
|
-
if (relationBranches.length === 0) return {}
|
|
31
|
-
|
|
32
|
-
const relations: Record<string, Record<string, string>> = {}
|
|
33
|
-
const repository = context.get('repository')
|
|
34
|
-
const seedRegistry = context.get('seedRegistry')
|
|
35
|
-
|
|
36
|
-
for (const branch of relationBranches) {
|
|
37
|
-
const targetSlug = branch.targetSeed
|
|
38
|
-
if (!targetSlug) continue
|
|
39
|
-
|
|
40
|
-
const targetSeedDef = seedRegistry.get(targetSlug)
|
|
41
|
-
if (!targetSeedDef) continue
|
|
42
|
-
|
|
43
|
-
const labelAlias = targetSeedDef.displayNameAlias ?? 'title'
|
|
44
|
-
|
|
45
|
-
// Collect unique non-null ids referenced by this branch
|
|
46
|
-
const ids = Array.from(
|
|
47
|
-
new Set(
|
|
48
|
-
entries
|
|
49
|
-
.map((entry) => {
|
|
50
|
-
const data = entry.data as Record<string, unknown>
|
|
51
|
-
return typeof data[branch.alias] === 'string' && data[branch.alias]
|
|
52
|
-
? (data[branch.alias] as string)
|
|
53
|
-
: null
|
|
54
|
-
})
|
|
55
|
-
.filter((id): id is string => id !== null)
|
|
56
|
-
)
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
if (ids.length === 0) continue
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const { items } = await repository.findMany(targetSeedDef, {
|
|
63
|
-
filters: [
|
|
64
|
-
{
|
|
65
|
-
column: 'id',
|
|
66
|
-
type: 'system' as const,
|
|
67
|
-
conditions: [{ op: 'in' as const, value: ids }],
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
fields: ['id', labelAlias],
|
|
71
|
-
pagination: { limit: ids.length, offset: 0 },
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
const map: Record<string, string> = {}
|
|
75
|
-
for (const item of items) {
|
|
76
|
-
const id = (item as Record<string, unknown>).id as string
|
|
77
|
-
const data = (item as Record<string, unknown>).data as Record<string, unknown>
|
|
78
|
-
const label = data[labelAlias]
|
|
79
|
-
map[id] = label != null && label !== '' ? String(label) : id
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
relations[branch.alias] = map
|
|
83
|
-
} catch {
|
|
84
|
-
// Non-fatal: if the target seed table doesn't exist yet, skip
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return relations
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export async function listHandler(context: Context<AppEnv>) {
|
|
92
|
-
const slug = context.req.param('slug')
|
|
93
|
-
if (!slug) {
|
|
94
|
-
return publicProblem(context, {
|
|
95
|
-
type: 'content-invalid-slug',
|
|
96
|
-
title: 'Bad Request',
|
|
97
|
-
status: 400,
|
|
98
|
-
detail: CONTENT_ERRORS.INVALID_SLUG
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const seed = context.get('getSeed')(slug)
|
|
103
|
-
if (!seed) {
|
|
104
|
-
return publicProblem(context, {
|
|
105
|
-
type: 'content-seed-not-found',
|
|
106
|
-
title: 'Not Found',
|
|
107
|
-
status: 404,
|
|
108
|
-
detail: CONTENT_ERRORS.SEED_NOT_FOUND
|
|
109
|
-
})
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const query = context.req.query()
|
|
114
|
-
const search = cleanStr(query.search) ?? ''
|
|
115
|
-
const sortBy = cleanStr(query.sortBy) ?? ''
|
|
116
|
-
const sortDirRaw = cleanStr(query.sortDir)?.toLowerCase() ?? 'asc'
|
|
117
|
-
const rawFilters = parseQueryFilters(query.filters)
|
|
118
|
-
const engineFilters = toEngineFilters(rawFilters)
|
|
119
|
-
|
|
120
|
-
// Note: repository doesn't yet support has_pending_draft filter/column natively
|
|
121
|
-
// in findMany without SQL manipulation. We'll stick to basic findMany for now
|
|
122
|
-
// and might need to enhance the repository if this feature is critical for v1 of this refactor.
|
|
123
|
-
// The legacy code was doing a lot of SQL injection here.
|
|
124
|
-
|
|
125
|
-
const page = parsePositiveInt(query.page, 1)
|
|
126
|
-
const limit = Math.min(parsePositiveInt(query.limit, 25), 100)
|
|
127
|
-
const offset = (page - 1) * limit
|
|
128
|
-
|
|
129
|
-
const orderBy = sortBy
|
|
130
|
-
? { column: sortBy, dir: (sortDirRaw === 'desc' ? 'DESC' : 'ASC') as 'ASC' | 'DESC' }
|
|
131
|
-
: undefined
|
|
132
|
-
|
|
133
|
-
const repository = context.get('repository')
|
|
134
|
-
const { items, total } = await repository.findMany(seed, {
|
|
135
|
-
filters: engineFilters,
|
|
136
|
-
orderBy,
|
|
137
|
-
search: search || undefined,
|
|
138
|
-
pagination: { limit, offset },
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
const entries = await Promise.all(items.map(async (item) => {
|
|
142
|
-
// Check for pending draft if allowed
|
|
143
|
-
let hasPendingDraft = false
|
|
144
|
-
if (seed.allowDrafts) {
|
|
145
|
-
hasPendingDraft = await repository.hasDraft(seed, item.id)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return {
|
|
149
|
-
...item,
|
|
150
|
-
has_pending_draft: hasPendingDraft,
|
|
151
|
-
data: applyVisibility(item, seed) // Repository returns "pure" data including system fields
|
|
152
|
-
}
|
|
153
|
-
}))
|
|
154
|
-
|
|
155
|
-
// If no query params (except slug), return array directly (legacy compatibility)
|
|
156
|
-
const hasQueryParams = Boolean(search) || Boolean(sortBy) || Boolean(query.filters) || query.page !== undefined || query.limit !== undefined
|
|
157
|
-
if (!hasQueryParams) {
|
|
158
|
-
return context.json(entries)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Build compact relation labels map for N+1 mitigation
|
|
162
|
-
const relations = await buildRelationsMap(context, seed, entries)
|
|
163
|
-
|
|
164
|
-
return context.json({ items: entries, total, page, limit, relations })
|
|
165
|
-
} catch (error) {
|
|
166
|
-
console.error('Content list error:', error)
|
|
167
|
-
return publicProblem(context, {
|
|
168
|
-
type: 'content-database-error',
|
|
169
|
-
title: 'Internal Server Error',
|
|
170
|
-
status: 500,
|
|
171
|
-
detail: CONTENT_ERRORS.DATABASE_ERROR
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
}
|
|
@@ -1,174 +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
|
-
import { Context } from 'hono'
|
|
6
|
-
import {
|
|
7
|
-
slugify,
|
|
8
|
-
isValidContentStatus,
|
|
9
|
-
validateAndSanitizeSeedPayload,
|
|
10
|
-
resolvePolicies
|
|
11
|
-
} from '@beechcms/core'
|
|
12
|
-
import { applyPrivacy, PrivacyPolicyError } from '../../../shared/apply-policies'
|
|
13
|
-
import { publicProblem } from '../../../public/problem-details'
|
|
14
|
-
import {
|
|
15
|
-
normalizeBody,
|
|
16
|
-
contentValidationProblem,
|
|
17
|
-
logContentActivity,
|
|
18
|
-
dispatchContentAutomation,
|
|
19
|
-
handleContentDatabaseError
|
|
20
|
-
} from './helpers'
|
|
21
|
-
import { CONTENT_ERRORS } from '../constants'
|
|
22
|
-
import { cleanStr } from '../../../shared/query-utils'
|
|
23
|
-
import { AppEnv } from '../../../types'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export async function updateHandler(context: Context<AppEnv>) {
|
|
28
|
-
const slug = context.req.param('slug')
|
|
29
|
-
const id = context.req.param('id')
|
|
30
|
-
if (!slug || !id) {
|
|
31
|
-
return publicProblem(context, {
|
|
32
|
-
type: 'content-invalid-slug-or-id',
|
|
33
|
-
title: 'Bad Request',
|
|
34
|
-
status: 400,
|
|
35
|
-
detail: CONTENT_ERRORS.INVALID_SLUG_OR_ID
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const seed = context.get('getSeed')(slug)
|
|
40
|
-
if (!seed) {
|
|
41
|
-
return publicProblem(context, {
|
|
42
|
-
type: 'content-seed-not-found',
|
|
43
|
-
title: 'Not Found',
|
|
44
|
-
status: 404,
|
|
45
|
-
detail: CONTENT_ERRORS.SEED_NOT_FOUND
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let body: Record<string, unknown>
|
|
50
|
-
try {
|
|
51
|
-
body = normalizeBody(await context.req.json<unknown>())
|
|
52
|
-
} catch {
|
|
53
|
-
return publicProblem(context, {
|
|
54
|
-
type: 'content-invalid-json',
|
|
55
|
-
title: 'Bad Request',
|
|
56
|
-
status: 400,
|
|
57
|
-
detail: CONTENT_ERRORS.INVALID_JSON_BODY
|
|
58
|
-
})
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const bodyForData = { ...body }
|
|
62
|
-
delete bodyForData.slug
|
|
63
|
-
delete bodyForData.status
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const repository = context.get('repository')
|
|
67
|
-
// We need current data to handle slug and status if not provided,
|
|
68
|
-
// and to verify existence.
|
|
69
|
-
const current = await repository.findById(seed, id)
|
|
70
|
-
|
|
71
|
-
const newSlug = body.slug !== undefined ? slugify(String(body.slug)) : (current.slug as string)
|
|
72
|
-
const newStatus = body.status !== undefined ? (cleanStr(body.status) ?? current.status as string) : current.status as string
|
|
73
|
-
|
|
74
|
-
if (!isValidContentStatus(newStatus)) {
|
|
75
|
-
return publicProblem(context, {
|
|
76
|
-
type: 'content-invalid-status',
|
|
77
|
-
title: 'Bad Request',
|
|
78
|
-
status: 400,
|
|
79
|
-
detail: 'Invalid status. Allowed values are: draft, review, published'
|
|
80
|
-
})
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!newSlug) {
|
|
84
|
-
return publicProblem(context, {
|
|
85
|
-
type: 'content-missing-slug',
|
|
86
|
-
title: 'Bad Request',
|
|
87
|
-
status: 400,
|
|
88
|
-
detail: 'Missing required field: slug'
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
let mergedData: Record<string, unknown> = {}
|
|
93
|
-
|
|
94
|
-
if (Object.keys(bodyForData).length > 0) {
|
|
95
|
-
const sensitiveAliases = Object.keys(bodyForData).filter((alias) => {
|
|
96
|
-
const branch = seed.branches.find((b) => b.alias === alias)
|
|
97
|
-
return branch != null && resolvePolicies(branch).privacy !== 'plain'
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
if (sensitiveAliases.length > 0) {
|
|
101
|
-
return publicProblem(context, {
|
|
102
|
-
type: 'content-sensitive-field-edit',
|
|
103
|
-
title: 'Unprocessable Entity',
|
|
104
|
-
status: 422,
|
|
105
|
-
detail: `${CONTENT_ERRORS.SENSITIVE_FIELD_EDIT}: ${sensitiveAliases.join(', ')}`
|
|
106
|
-
})
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const validation = validateAndSanitizeSeedPayload(seed, bodyForData, {
|
|
110
|
-
operation: 'update',
|
|
111
|
-
allowNull: true,
|
|
112
|
-
requireAtLeastOneValidField: true,
|
|
113
|
-
enforceRequiredFields: true,
|
|
114
|
-
idGenerator: context.get('idGenerator'),
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
if (validation.dangerousFields.length > 0) {
|
|
118
|
-
return publicProblem(context, {
|
|
119
|
-
type: 'content-dangerous-content',
|
|
120
|
-
title: 'Unprocessable Entity',
|
|
121
|
-
status: 422,
|
|
122
|
-
detail: `Content rejected: dangerous markup detected in field '${validation.dangerousFields[0]}'`
|
|
123
|
-
})
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (validation.details.length > 0) return contentValidationProblem(context, validation.details)
|
|
127
|
-
|
|
128
|
-
let privacyPatch: Record<string, unknown>
|
|
129
|
-
try {
|
|
130
|
-
privacyPatch = await applyPrivacy(validation.data, seed)
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if (error instanceof PrivacyPolicyError) {
|
|
133
|
-
return publicProblem(context, {
|
|
134
|
-
type: 'content-policy-not-implemented',
|
|
135
|
-
title: 'Not Implemented',
|
|
136
|
-
status: 501,
|
|
137
|
-
detail: error.message
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
|
-
throw error
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Remove null values from patch (patch semantics: null = leave unchanged)
|
|
144
|
-
for (const [k, v] of Object.entries(privacyPatch)) {
|
|
145
|
-
if (v !== null) mergedData[k] = v
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (newSlug !== current.slug) {
|
|
150
|
-
if (await repository.existsSlug(seed, newSlug, id)) {
|
|
151
|
-
return publicProblem(context, {
|
|
152
|
-
type: 'content-slug-conflict',
|
|
153
|
-
title: 'Conflict',
|
|
154
|
-
status: 409,
|
|
155
|
-
detail: CONTENT_ERRORS.SLUG_CONFLICT
|
|
156
|
-
})
|
|
157
|
-
}
|
|
158
|
-
// Add slug to mergedData if changed
|
|
159
|
-
mergedData.slug = newSlug
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
await repository.update(seed, id, mergedData, newStatus)
|
|
163
|
-
|
|
164
|
-
const jwtPayload = context.get('jwtPayload')
|
|
165
|
-
const title = mergedData.title || mergedData.name || newSlug
|
|
166
|
-
|
|
167
|
-
logContentActivity(context, 'update', id, slug, String(title))
|
|
168
|
-
dispatchContentAutomation(context, slug, 'update', { ...current, ...mergedData, id, status: newStatus })
|
|
169
|
-
|
|
170
|
-
return context.json({ success: true })
|
|
171
|
-
} catch (error) {
|
|
172
|
-
return handleContentDatabaseError(context, error)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
@@ -1,26 +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
|
-
import { Hono } from 'hono'
|
|
6
|
-
import { AppEnv } from '../../types'
|
|
7
|
-
import { listHandler } from './handlers/list'
|
|
8
|
-
import { getByIdHandler, getBySlugHandler } from './handlers/get'
|
|
9
|
-
import { createHandler } from './handlers/create'
|
|
10
|
-
import { updateHandler } from './handlers/update'
|
|
11
|
-
import { deleteHandler } from './handlers/delete'
|
|
12
|
-
import { facetsHandler } from './handlers/facets'
|
|
13
|
-
import { bulkHandler } from './handlers/bulk.handler'
|
|
14
|
-
|
|
15
|
-
const content = new Hono<AppEnv>()
|
|
16
|
-
|
|
17
|
-
content.get('/:slug', listHandler)
|
|
18
|
-
content.get('/:slug/facets', facetsHandler)
|
|
19
|
-
content.get('/:schema_slug/by-slug/:entry_slug', getBySlugHandler)
|
|
20
|
-
content.get('/:slug/:id', getByIdHandler)
|
|
21
|
-
content.post('/:slug', createHandler)
|
|
22
|
-
content.patch('/:slug/bulk', bulkHandler)
|
|
23
|
-
content.put('/:slug/:id', updateHandler)
|
|
24
|
-
content.delete('/:slug/:id', deleteHandler)
|
|
25
|
-
|
|
26
|
-
export default content
|