@beechcms/api 0.6.0-preview.2 → 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/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/index.html +2 -2
- 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} +19 -24
- 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} +3 -16
- 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/{src/public/slug-utils.ts → dist/public/slug-utils.d.ts} +3 -10
- 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/package.json +27 -18
- package/assets/dashboard/assets/chart-BrJFdsGZ.js +0 -39
- package/assets/dashboard/assets/index-CIaoMxuB.css +0 -1
- package/assets/dashboard/assets/index-wTKgygsg.js +0 -636
- package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +0 -1
- package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +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 -370
- 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/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +0 -388
- package/src/features/dashboard-layout/dashboard-layout.handler.ts +0 -206
- package/src/features/dashboard-layout/index.ts +0 -5
- 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 -1199
- package/src/features/seeds/seeds.handler.ts +0 -558
- 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 -288
- 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.test.ts +0 -85
- package/src/middleware/auth-providers.middleware.ts +0 -45
- package/src/middleware/observability.middleware.test.ts +0 -93
- package/src/middleware/observability.middleware.ts +0 -70
- package/src/middleware/rate-limit.middleware.ts +0 -45
- package/src/middleware/repository.middleware.ts +0 -117
- 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/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 -507
- 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 -267
- package/src/shared/d1-widget.repository.ts +0 -385
- package/src/shared/dashboard-layout.repository.d1.test.ts +0 -135
- package/src/shared/dashboard-layout.repository.d1.ts +0 -58
- package/src/shared/demo-data-sql.ts +0 -216
- 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 -58
- 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.test.ts +0 -142
- 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 -90
- package/src/upload.ts +0 -198
- package/src/widget.test.ts +0 -85
- package/src/widget.ts +0 -251
package/src/public/sanitize.ts
DELETED
|
@@ -1,69 +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 { validateAndSanitizeSeedPayload } from '@beechcms/core'
|
|
6
|
-
import type { Seed, ValidationDetail } from '@beechcms/core'
|
|
7
|
-
|
|
8
|
-
type PublicSanitizeSuccess = {
|
|
9
|
-
ok: true
|
|
10
|
-
data: Record<string, unknown>
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
type PublicSanitizeFailure = {
|
|
14
|
-
ok: false
|
|
15
|
-
status: 400 | 422
|
|
16
|
-
code: 'validation_failed' | 'dangerous_content'
|
|
17
|
-
message: string
|
|
18
|
-
details?: ValidationDetail[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type PublicSanitizeResult = PublicSanitizeSuccess | PublicSanitizeFailure
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Public API Adapter: uses the core foundation and maps errors into the sprint format.
|
|
25
|
-
*/
|
|
26
|
-
export function sanitizePublicPayload(
|
|
27
|
-
seed: Seed,
|
|
28
|
-
payload: Record<string, unknown>,
|
|
29
|
-
options: {
|
|
30
|
-
allowNull?: boolean
|
|
31
|
-
operation?: 'create' | 'update'
|
|
32
|
-
requireAtLeastOneValidField?: boolean
|
|
33
|
-
enforceRequiredFields?: boolean
|
|
34
|
-
} = {}
|
|
35
|
-
): PublicSanitizeResult {
|
|
36
|
-
const operation = options.operation ?? 'create'
|
|
37
|
-
const result = validateAndSanitizeSeedPayload(seed, payload, {
|
|
38
|
-
allowNull: options.allowNull ?? false,
|
|
39
|
-
operation,
|
|
40
|
-
requireAtLeastOneValidField: options.requireAtLeastOneValidField ?? true,
|
|
41
|
-
enforceRequiredFields: options.enforceRequiredFields ?? true,
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
if (result.dangerousFields.length > 0) {
|
|
45
|
-
const field = result.dangerousFields[0]
|
|
46
|
-
return {
|
|
47
|
-
ok: false,
|
|
48
|
-
status: 422,
|
|
49
|
-
code: 'dangerous_content',
|
|
50
|
-
message: `Content rejected: dangerous markup detected in field '${field}'`,
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (result.details.length > 0) {
|
|
55
|
-
return {
|
|
56
|
-
ok: false,
|
|
57
|
-
status: 400,
|
|
58
|
-
code: 'validation_failed',
|
|
59
|
-
message: 'Validation failed',
|
|
60
|
-
details: result.details,
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
ok: true,
|
|
66
|
-
data: result.data,
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
@@ -1,30 +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 { describe, it, expect, vi } from 'vitest'
|
|
6
|
-
import { CloudflareRateLimiter } from './cloudflare-rate-limiter'
|
|
7
|
-
|
|
8
|
-
describe('CloudflareRateLimiter', () => {
|
|
9
|
-
it('returns isAllowed: true when the Cloudflare binding grants the request', async () => {
|
|
10
|
-
const mockBinding = { limit: vi.fn().mockResolvedValue({ success: true }) }
|
|
11
|
-
const limiter = new CloudflareRateLimiter(mockBinding as any)
|
|
12
|
-
const result = await limiter.checkLimit('192.168.1.1:login')
|
|
13
|
-
expect(result.isAllowed).toBe(true)
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('returns isAllowed: false when the Cloudflare binding blocks the request', async () => {
|
|
17
|
-
const mockBinding = { limit: vi.fn().mockResolvedValue({ success: false }) }
|
|
18
|
-
const limiter = new CloudflareRateLimiter(mockBinding as any)
|
|
19
|
-
const result = await limiter.checkLimit('192.168.1.1:login')
|
|
20
|
-
expect(result.isAllowed).toBe(false)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('forwards the exact key to the binding so per-key accounting is correct', async () => {
|
|
24
|
-
const mockBinding = { limit: vi.fn().mockResolvedValue({ success: true }) }
|
|
25
|
-
const limiter = new CloudflareRateLimiter(mockBinding as any)
|
|
26
|
-
const key = '10.0.0.1:some-seed:publicApiRead'
|
|
27
|
-
await limiter.checkLimit(key)
|
|
28
|
-
expect(mockBinding.limit).toHaveBeenCalledWith({ key })
|
|
29
|
-
})
|
|
30
|
-
})
|
|
@@ -1,15 +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 type { IRateLimiter, RateLimitResult } from '@beechcms/core'
|
|
7
|
-
|
|
8
|
-
export class CloudflareRateLimiter implements IRateLimiter {
|
|
9
|
-
constructor(private readonly binding: RateLimit) {}
|
|
10
|
-
|
|
11
|
-
async checkLimit(key: string): Promise<RateLimitResult> {
|
|
12
|
-
const { success } = await this.binding.limit({ key })
|
|
13
|
-
return { isAllowed: success }
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,37 +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 { describe, it, expect } from 'vitest'
|
|
6
|
-
import { InMemoryRateLimiter } from './in-memory-rate-limiter'
|
|
7
|
-
|
|
8
|
-
describe('InMemoryRateLimiter', () => {
|
|
9
|
-
it('allows requests up to the configured maximum hit count', async () => {
|
|
10
|
-
const limiter = new InMemoryRateLimiter(3)
|
|
11
|
-
expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
|
|
12
|
-
expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
|
|
13
|
-
expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('blocks the very next request after maxAllowedHits is reached', async () => {
|
|
17
|
-
const limiter = new InMemoryRateLimiter(2)
|
|
18
|
-
await limiter.checkLimit('key')
|
|
19
|
-
await limiter.checkLimit('key')
|
|
20
|
-
expect((await limiter.checkLimit('key')).isAllowed).toBe(false)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('tracks hit counts independently for each key', async () => {
|
|
24
|
-
const limiter = new InMemoryRateLimiter(1)
|
|
25
|
-
expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
|
|
26
|
-
expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(true)
|
|
27
|
-
// Both keys exhausted independently
|
|
28
|
-
expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(false)
|
|
29
|
-
expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(false)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('maxAllowedHits = 1 allows exactly one request before blocking', async () => {
|
|
33
|
-
const limiter = new InMemoryRateLimiter(1)
|
|
34
|
-
expect((await limiter.checkLimit('k')).isAllowed).toBe(true)
|
|
35
|
-
expect((await limiter.checkLimit('k')).isAllowed).toBe(false)
|
|
36
|
-
})
|
|
37
|
-
})
|
|
@@ -1,17 +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 type { IRateLimiter, RateLimitResult } from '@beechcms/core'
|
|
6
|
-
|
|
7
|
-
export class InMemoryRateLimiter implements IRateLimiter {
|
|
8
|
-
private readonly hitCounts = new Map<string, number>()
|
|
9
|
-
|
|
10
|
-
constructor(private readonly maxAllowedHits: number) {}
|
|
11
|
-
|
|
12
|
-
async checkLimit(key: string): Promise<RateLimitResult> {
|
|
13
|
-
const currentHits = (this.hitCounts.get(key) ?? 0) + 1
|
|
14
|
-
this.hitCounts.set(key, currentHits)
|
|
15
|
-
return { isAllowed: currentHits <= this.maxAllowedHits }
|
|
16
|
-
}
|
|
17
|
-
}
|
|
@@ -1,22 +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 { describe, it, expect } from 'vitest'
|
|
6
|
-
import { NoOpRateLimiter } from './no-op-rate-limiter'
|
|
7
|
-
|
|
8
|
-
describe('NoOpRateLimiter', () => {
|
|
9
|
-
it('always returns isAllowed: true regardless of the key', async () => {
|
|
10
|
-
const limiter = new NoOpRateLimiter()
|
|
11
|
-
for (let i = 0; i < 100; i++) {
|
|
12
|
-
expect((await limiter.checkLimit('any-key')).isAllowed).toBe(true)
|
|
13
|
-
}
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('allows all keys without accumulating state', async () => {
|
|
17
|
-
const limiter = new NoOpRateLimiter()
|
|
18
|
-
expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
|
|
19
|
-
expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(true)
|
|
20
|
-
expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
|
|
21
|
-
})
|
|
22
|
-
})
|
|
@@ -1,11 +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 type { IRateLimiter, RateLimitResult } from '@beechcms/core'
|
|
6
|
-
|
|
7
|
-
export class NoOpRateLimiter implements IRateLimiter {
|
|
8
|
-
async checkLimit(_key: string): Promise<RateLimitResult> {
|
|
9
|
-
return { isAllowed: true }
|
|
10
|
-
}
|
|
11
|
-
}
|
package/src/search-utils.test.ts
DELETED
|
@@ -1,211 +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 { describe, it, expect } from 'vitest'
|
|
6
|
-
import { encodeCursor, decodeCursor, buildFtsQuery, mapFtsRow } from './search-utils'
|
|
7
|
-
import type { Seed } from '@beechcms/core'
|
|
8
|
-
|
|
9
|
-
const TEXT_SEED = {
|
|
10
|
-
slug: 'articoli',
|
|
11
|
-
displayNameAlias: 'title',
|
|
12
|
-
branches: [
|
|
13
|
-
{ id: 'br_01', alias: 'title', type: 'text', policies: { search: true } },
|
|
14
|
-
],
|
|
15
|
-
} as unknown as Seed
|
|
16
|
-
|
|
17
|
-
const SECOND_SEED = {
|
|
18
|
-
slug: 'team',
|
|
19
|
-
displayNameAlias: 'name',
|
|
20
|
-
branches: [
|
|
21
|
-
{ id: 'br_01', alias: 'name', type: 'text', policies: { search: true } },
|
|
22
|
-
],
|
|
23
|
-
} as unknown as Seed
|
|
24
|
-
|
|
25
|
-
const NO_FTS_SEED = {
|
|
26
|
-
slug: 'prodotti',
|
|
27
|
-
displayNameAlias: 'nome',
|
|
28
|
-
branches: [
|
|
29
|
-
{ id: 'br_01', alias: 'price', type: 'number' },
|
|
30
|
-
],
|
|
31
|
-
} as unknown as Seed
|
|
32
|
-
|
|
33
|
-
// ─── encodeCursor / decodeCursor ─────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
describe('encodeCursor / decodeCursor', () => {
|
|
36
|
-
it('roundtrip preserves rank and entryId', () => {
|
|
37
|
-
const cursor = encodeCursor(-1.23456, 'entry-abc')
|
|
38
|
-
const decoded = decodeCursor(cursor)
|
|
39
|
-
expect(decoded?.rank).toBeCloseTo(-1.23456)
|
|
40
|
-
expect(decoded?.entryId).toBe('entry-abc')
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('entryId without colons roundtrips correctly (UUID-style IDs)', () => {
|
|
44
|
-
const entryId = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
|
|
45
|
-
const cursor = encodeCursor(-3.7, entryId)
|
|
46
|
-
const decoded = decodeCursor(cursor)
|
|
47
|
-
expect(decoded?.rank).toBeCloseTo(-3.7)
|
|
48
|
-
expect(decoded?.entryId).toBe(entryId)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('returns null for invalid base64', () => {
|
|
52
|
-
expect(decodeCursor('!!!not-base64!!!')).toBeNull()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('returns null when decoded string has no colon separator', () => {
|
|
56
|
-
expect(decodeCursor(btoa('noseparator'))).toBeNull()
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
// ─── buildFtsQuery ───────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
describe('buildFtsQuery', () => {
|
|
63
|
-
it('returns empty-result query when no seed has a searchable FTS branch', () => {
|
|
64
|
-
const result = buildFtsQuery(
|
|
65
|
-
{ q: 'hello', schemaSlug: null, status: null, limit: 20, cursor: null },
|
|
66
|
-
[NO_FTS_SEED],
|
|
67
|
-
)
|
|
68
|
-
expect(result.sql).toContain('WHERE 1=0')
|
|
69
|
-
expect(result.binds).toHaveLength(0)
|
|
70
|
-
expect(result.countSql).toContain('SELECT 0 as total')
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('throws EMPTY_QUERY when all terms are stripped or too short (single chars)', () => {
|
|
74
|
-
expect(() =>
|
|
75
|
-
buildFtsQuery({ q: 'a b', schemaSlug: null, status: null, limit: 20, cursor: null }, [TEXT_SEED]),
|
|
76
|
-
).toThrow('EMPTY_QUERY')
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('generates a query referencing the seed FTS and content tables', () => {
|
|
80
|
-
const result = buildFtsQuery(
|
|
81
|
-
{ q: 'hello', schemaSlug: null, status: null, limit: 20, cursor: null },
|
|
82
|
-
[TEXT_SEED],
|
|
83
|
-
)
|
|
84
|
-
expect(result.sql).toContain('fts_articoli')
|
|
85
|
-
expect(result.sql).toContain('content_articoli')
|
|
86
|
-
expect(result.sql).toContain('LIMIT ?')
|
|
87
|
-
expect(result.binds.at(-1)).toBe(21) // limit + 1 for has-more detection
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('UNION ALLs multiple seeds when no schemaSlug filter is set', () => {
|
|
91
|
-
const result = buildFtsQuery(
|
|
92
|
-
{ q: 'test', schemaSlug: null, status: null, limit: 10, cursor: null },
|
|
93
|
-
[TEXT_SEED, SECOND_SEED],
|
|
94
|
-
)
|
|
95
|
-
expect(result.sql).toContain('UNION ALL')
|
|
96
|
-
expect(result.sql).toContain('fts_articoli')
|
|
97
|
-
expect(result.sql).toContain('fts_team')
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('limits query to the requested schemaSlug when provided', () => {
|
|
101
|
-
const result = buildFtsQuery(
|
|
102
|
-
{ q: 'test', schemaSlug: 'articoli', status: null, limit: 20, cursor: null },
|
|
103
|
-
[TEXT_SEED, SECOND_SEED],
|
|
104
|
-
)
|
|
105
|
-
expect(result.sql).toContain('fts_articoli')
|
|
106
|
-
expect(result.sql).not.toContain('fts_team')
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('adds status filter to WHERE clause and bind values', () => {
|
|
110
|
-
const result = buildFtsQuery(
|
|
111
|
-
{ q: 'hello', schemaSlug: null, status: 'published', limit: 20, cursor: null },
|
|
112
|
-
[TEXT_SEED],
|
|
113
|
-
)
|
|
114
|
-
expect(result.sql).toContain('ce.status = ?')
|
|
115
|
-
expect(result.binds).toContain('published')
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('adds cursor-based pagination condition when cursor is valid', () => {
|
|
119
|
-
const cursor = encodeCursor(-1.5, 'entry-123')
|
|
120
|
-
const result = buildFtsQuery(
|
|
121
|
-
{ q: 'hello', schemaSlug: null, status: null, limit: 20, cursor },
|
|
122
|
-
[TEXT_SEED],
|
|
123
|
-
)
|
|
124
|
-
expect(result.sql).toContain('bm25')
|
|
125
|
-
expect(result.binds).toContain('entry-123')
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('ignores an invalid cursor and produces no pagination condition', () => {
|
|
129
|
-
const result = buildFtsQuery(
|
|
130
|
-
{ q: 'hello', schemaSlug: null, status: null, limit: 20, cursor: 'bad-cursor' },
|
|
131
|
-
[TEXT_SEED],
|
|
132
|
-
)
|
|
133
|
-
expect(result.binds).not.toContain('entry-123')
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('countSql wraps each seed count in a SUM', () => {
|
|
137
|
-
const result = buildFtsQuery(
|
|
138
|
-
{ q: 'test', schemaSlug: null, status: null, limit: 10, cursor: null },
|
|
139
|
-
[TEXT_SEED, SECOND_SEED],
|
|
140
|
-
)
|
|
141
|
-
expect(result.countSql).toContain('SUM')
|
|
142
|
-
expect(result.countSql).toContain('fts_articoli')
|
|
143
|
-
expect(result.countSql).toContain('fts_team')
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('count binds do not include the limit+1 sentinel', () => {
|
|
147
|
-
const result = buildFtsQuery(
|
|
148
|
-
{ q: 'hello', schemaSlug: null, status: null, limit: 5, cursor: null },
|
|
149
|
-
[TEXT_SEED],
|
|
150
|
-
)
|
|
151
|
-
expect(result.countBinds).not.toContain(6) // limit + 1 must not appear in count binds
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
it('single-character terms (length < 2) are filtered out', () => {
|
|
155
|
-
// 'a b c' — single-char terms discarded; result depends on remaining terms
|
|
156
|
-
expect(() =>
|
|
157
|
-
buildFtsQuery({ q: 'a b c', schemaSlug: null, status: null, limit: 20, cursor: null }, [TEXT_SEED]),
|
|
158
|
-
).toThrow('EMPTY_QUERY')
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('numeric terms are quoted without prefix expansion', () => {
|
|
162
|
-
const result = buildFtsQuery(
|
|
163
|
-
{ q: '2024', schemaSlug: null, status: null, limit: 20, cursor: null },
|
|
164
|
-
[TEXT_SEED],
|
|
165
|
-
)
|
|
166
|
-
expect(result.binds[0]).toContain('"2024"')
|
|
167
|
-
})
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
// ─── mapFtsRow ───────────────────────────────────────────────────────────────
|
|
171
|
-
|
|
172
|
-
describe('mapFtsRow', () => {
|
|
173
|
-
it('maps all FtsRow fields to SearchResultItem', () => {
|
|
174
|
-
const row = {
|
|
175
|
-
entry_id: 'e1', schema_slug: 'articoli', slug: 'my-post',
|
|
176
|
-
status: 'published', title: 'My Post', excerpt: 'A snippet', rank: -1,
|
|
177
|
-
}
|
|
178
|
-
const result = mapFtsRow(row)
|
|
179
|
-
expect(result.id).toBe('e1')
|
|
180
|
-
expect(result.schema_slug).toBe('articoli')
|
|
181
|
-
expect(result.slug).toBe('my-post')
|
|
182
|
-
expect(result.status).toBe('published')
|
|
183
|
-
expect(result.title).toBe('My Post')
|
|
184
|
-
expect(result.excerpt).toBe('A snippet')
|
|
185
|
-
expect(result.data).toEqual({})
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
it('strips HTML tags from excerpt but preserves <mark> and </mark>', () => {
|
|
189
|
-
const row = {
|
|
190
|
-
entry_id: 'e1', schema_slug: 'a', slug: null, status: 'draft',
|
|
191
|
-
title: null, excerpt: '<p>A <mark>word</mark> here</p>', rank: 0,
|
|
192
|
-
}
|
|
193
|
-
expect(mapFtsRow(row).excerpt).toBe('A <mark>word</mark> here')
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it('collapses multiple whitespace characters in excerpt', () => {
|
|
197
|
-
const row = {
|
|
198
|
-
entry_id: 'e1', schema_slug: 'a', slug: null, status: 'draft',
|
|
199
|
-
title: null, excerpt: '<p> lots of space </p>', rank: 0,
|
|
200
|
-
}
|
|
201
|
-
expect(mapFtsRow(row).excerpt).toBe('lots of space')
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
it('returns empty string for null title', () => {
|
|
205
|
-
const row = {
|
|
206
|
-
entry_id: 'e1', schema_slug: 'a', slug: null, status: 'draft',
|
|
207
|
-
title: null, excerpt: '', rank: 0,
|
|
208
|
-
}
|
|
209
|
-
expect(mapFtsRow(row).title).toBe('')
|
|
210
|
-
})
|
|
211
|
-
})
|
package/src/search-utils.ts
DELETED
|
@@ -1,213 +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
|
-
// apps/api/src/search-utils.ts
|
|
6
|
-
// Pure functions — zero Hono dependencies, importable from Vitest.
|
|
7
|
-
// v0.4.0: FTS is per-seed (fts_{slug}), joined with content_{slug} for metadata.
|
|
8
|
-
|
|
9
|
-
import type { Seed, SearchResultRow } from "@beechcms/core"
|
|
10
|
-
|
|
11
|
-
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
// Row returned by UNION ALL query across fts_{slug} JOIN content_{slug}
|
|
14
|
-
export interface FtsRow {
|
|
15
|
-
entry_id: string
|
|
16
|
-
schema_slug: string
|
|
17
|
-
slug: string | null
|
|
18
|
-
status: string
|
|
19
|
-
title: string | null
|
|
20
|
-
excerpt: string
|
|
21
|
-
rank: number
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface SearchResultItem {
|
|
25
|
-
id: string
|
|
26
|
-
schema_slug: string
|
|
27
|
-
slug: string | null
|
|
28
|
-
status: string
|
|
29
|
-
title: string
|
|
30
|
-
excerpt: string
|
|
31
|
-
data: Record<string, unknown>
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface SearchResponse {
|
|
35
|
-
items: SearchResultItem[]
|
|
36
|
-
nextCursor: string | null
|
|
37
|
-
total: number
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface SearchQueryParams {
|
|
41
|
-
q: string
|
|
42
|
-
schemaSlug: string | null
|
|
43
|
-
status: string | null
|
|
44
|
-
limit: number // already clamped 1–50 by handler
|
|
45
|
-
cursor: string | null
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ─── Cursor ──────────────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
export function encodeCursor(rank: number, entryId: string): string {
|
|
51
|
-
return btoa(`${rank}:${entryId}`)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function decodeCursor(cursor: string): { rank: number; entryId: string } | null {
|
|
55
|
-
try {
|
|
56
|
-
const decoded = atob(cursor)
|
|
57
|
-
const sep = decoded.lastIndexOf(":")
|
|
58
|
-
if (sep === -1) return null
|
|
59
|
-
return {
|
|
60
|
-
rank: Number.parseFloat(decoded.slice(0, sep)),
|
|
61
|
-
entryId: decoded.slice(sep + 1),
|
|
62
|
-
}
|
|
63
|
-
} catch {
|
|
64
|
-
return null
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
function hasSearchableFts(seed: Seed): boolean {
|
|
71
|
-
return seed.branches.some(b =>
|
|
72
|
-
(b.type === 'text' || b.type === 'richtext') && b.policies?.search !== false
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function buildMatchExpr(q: string): string {
|
|
77
|
-
const MIN_PREFIX = 3
|
|
78
|
-
const safeQ = q.replace(/["*^()]/g, " ").trim()
|
|
79
|
-
const terms = safeQ.split(/\s+/).filter(t => t.length >= 2)
|
|
80
|
-
if (terms.length === 0) throw new Error("EMPTY_QUERY")
|
|
81
|
-
|
|
82
|
-
return terms
|
|
83
|
-
.map(t => {
|
|
84
|
-
if (/^\d+$/.test(t)) return `"${t}"`
|
|
85
|
-
if (t.length <= MIN_PREFIX) return `"${t}"*`
|
|
86
|
-
const prefixes: string[] = []
|
|
87
|
-
for (let i = MIN_PREFIX; i <= t.length; i++) {
|
|
88
|
-
prefixes.push(`"${t.slice(0, i)}"*`)
|
|
89
|
-
}
|
|
90
|
-
return `(${prefixes.join(" OR ")})`
|
|
91
|
-
})
|
|
92
|
-
.join(" ")
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ─── Query builder ───────────────────────────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Builds a UNION ALL query across all per-seed fts_{slug} tables (v0.4.0).
|
|
99
|
-
* Each SELECT joins fts_{slug} with content_{slug} to fetch title, slug, status.
|
|
100
|
-
* Seeds param: full registry — filtered internally by schemaSlug and FTS availability.
|
|
101
|
-
*/
|
|
102
|
-
export function buildFtsQuery(
|
|
103
|
-
params: SearchQueryParams,
|
|
104
|
-
seeds: Seed[],
|
|
105
|
-
): {
|
|
106
|
-
sql: string
|
|
107
|
-
binds: unknown[]
|
|
108
|
-
countSql: string
|
|
109
|
-
countBinds: unknown[]
|
|
110
|
-
} {
|
|
111
|
-
const { q, schemaSlug, status, limit, cursor } = params
|
|
112
|
-
|
|
113
|
-
const matchExpr = buildMatchExpr(q) // throws EMPTY_QUERY if needed
|
|
114
|
-
|
|
115
|
-
const targetSeeds = seeds.filter(s =>
|
|
116
|
-
hasSearchableFts(s) && (schemaSlug === null || s.slug === schemaSlug)
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
if (targetSeeds.length === 0) {
|
|
120
|
-
return {
|
|
121
|
-
sql: "SELECT NULL as entry_id, NULL as schema_slug, NULL as slug, NULL as status, NULL as title, '' as excerpt, 0 as rank WHERE 1=0",
|
|
122
|
-
binds: [],
|
|
123
|
-
countSql: "SELECT 0 as total",
|
|
124
|
-
countBinds: [],
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const decoded = cursor ? decodeCursor(cursor) : null
|
|
129
|
-
|
|
130
|
-
const parts: string[] = []
|
|
131
|
-
const binds: unknown[] = []
|
|
132
|
-
const countParts: string[] = []
|
|
133
|
-
const countBinds: unknown[] = []
|
|
134
|
-
|
|
135
|
-
for (const seed of targetSeeds) {
|
|
136
|
-
const fts = `fts_${seed.slug}`
|
|
137
|
-
const table = `content_${seed.slug}`
|
|
138
|
-
const title = seed.displayNameAlias
|
|
139
|
-
|
|
140
|
-
// Main query per seed
|
|
141
|
-
const where: string[] = [`${fts} MATCH ?`]
|
|
142
|
-
const lb: unknown[] = [matchExpr]
|
|
143
|
-
|
|
144
|
-
if (status) { where.push("ce.status = ?"); lb.push(status) }
|
|
145
|
-
|
|
146
|
-
if (decoded) {
|
|
147
|
-
where.push(`(bm25(${fts}) > ? OR (bm25(${fts}) = ? AND f.entry_id > ?))`)
|
|
148
|
-
lb.push(decoded.rank, decoded.rank, decoded.entryId)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
parts.push(
|
|
152
|
-
`SELECT f.entry_id, '${seed.slug}' AS schema_slug, ce.slug, ce.status,` +
|
|
153
|
-
` ce.${title} AS title,` +
|
|
154
|
-
` snippet(${fts}, 1, '<mark>', '</mark>', '…', 16) AS excerpt,` +
|
|
155
|
-
` bm25(${fts}) AS rank` +
|
|
156
|
-
` FROM ${fts} f JOIN ${table} ce ON ce.id = f.entry_id` +
|
|
157
|
-
` WHERE ${where.join(' AND ')}`
|
|
158
|
-
)
|
|
159
|
-
binds.push(...lb)
|
|
160
|
-
|
|
161
|
-
// Count per seed (no cursor, no limit)
|
|
162
|
-
const cw: string[] = [`${fts} MATCH ?`]
|
|
163
|
-
const cb: unknown[] = [matchExpr]
|
|
164
|
-
if (status) { cw.push("ce.status = ?"); cb.push(status) }
|
|
165
|
-
|
|
166
|
-
countParts.push(
|
|
167
|
-
`SELECT COUNT(*) as c FROM ${fts} f JOIN ${table} ce ON ce.id = f.entry_id WHERE ${cw.join(' AND ')}`
|
|
168
|
-
)
|
|
169
|
-
countBinds.push(...cb)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const sql = `${parts.join(' UNION ALL ')} ORDER BY rank, entry_id LIMIT ?`
|
|
173
|
-
binds.push(limit + 1)
|
|
174
|
-
|
|
175
|
-
const countSql = `SELECT SUM(c) as total FROM (${countParts.join(' UNION ALL ')})`
|
|
176
|
-
|
|
177
|
-
return { sql, binds, countSql, countBinds }
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ─── Mapper ──────────────────────────────────────────────────────────────────
|
|
181
|
-
|
|
182
|
-
function stripHtmlPreserveMark(html: string): string {
|
|
183
|
-
return html.replace(/<(?!\/?mark\b)[^>]*>/gi, ' ').replace(/\s+/g, ' ').trim()
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function mapFtsRow(row: FtsRow): SearchResultItem {
|
|
187
|
-
return {
|
|
188
|
-
id: row.entry_id,
|
|
189
|
-
schema_slug: row.schema_slug,
|
|
190
|
-
slug: row.slug,
|
|
191
|
-
status: row.status,
|
|
192
|
-
title: row.title ?? "",
|
|
193
|
-
excerpt: stripHtmlPreserveMark(row.excerpt ?? ""),
|
|
194
|
-
data: {},
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Maps a repository-shaped SearchResultRow (camelCase) to the wire-format
|
|
200
|
-
* SearchResultItem returned by the /api/search route. Keeps the HTML
|
|
201
|
-
* stripping behaviour previously inlined inside mapFtsRow.
|
|
202
|
-
*/
|
|
203
|
-
export function mapSearchResultRow(row: SearchResultRow): SearchResultItem {
|
|
204
|
-
return {
|
|
205
|
-
id: row.entryId,
|
|
206
|
-
schema_slug: row.schemaSlug,
|
|
207
|
-
slug: row.slug,
|
|
208
|
-
status: row.status,
|
|
209
|
-
title: row.title ?? "",
|
|
210
|
-
excerpt: stripHtmlPreserveMark(row.excerpt ?? ""),
|
|
211
|
-
data: {},
|
|
212
|
-
}
|
|
213
|
-
}
|