@michaelthielemann/kestrel 3.0.0 → 4.0.0
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 +35 -110
- package/kestrel.config.ts +1 -1
- package/layers/access/server/middleware/00.ip-allowlist.ts +3 -18
- package/layers/access/server/middleware/access-guard.ts +25 -23
- package/layers/access/server/types.d.ts +1 -2
- package/layers/admin/app/components/AdminAccount.vue +1 -1
- package/layers/admin/app/components/BlockFields.vue +1 -2
- package/layers/admin/app/components/BlockPreview.vue +17 -5
- package/layers/admin/app/components/BlocksBody.vue +1 -1
- package/layers/admin/app/components/CollectionEditor.vue +35 -16
- package/layers/admin/app/components/CollectionList.vue +34 -3
- package/layers/admin/app/components/CollectionListBulkBar.vue +17 -3
- package/layers/admin/app/components/CollectionListFilterPanel.vue +1 -1
- package/layers/admin/app/components/CollectionListPager.vue +1 -1
- package/layers/admin/app/components/CollectionListTable.vue +41 -2
- package/layers/admin/app/components/EditorStatus.vue +8 -7
- package/layers/admin/app/components/LocaleBar.vue +3 -1
- package/layers/admin/app/components/PageFields.vue +2 -4
- package/layers/admin/app/components/SeoFields.vue +2 -4
- package/layers/admin/app/components/SingletonEditor.vue +3 -2
- package/layers/admin/app/composables/useAuth.ts +3 -3
- package/layers/admin/app/composables/useBlockTree.ts +1 -1
- package/layers/admin/app/composables/useBlocks.ts +1 -1
- package/layers/admin/app/composables/useCollectionOps.ts +45 -8
- package/layers/admin/app/composables/useCollections.ts +1 -2
- package/layers/admin/app/composables/useEditForm.ts +35 -17
- package/layers/admin/app/composables/useLinkResolver.ts +1 -1
- package/layers/admin/app/composables/useListBatchActions.ts +20 -3
- package/layers/admin/app/composables/useListColumns.ts +1 -1
- package/layers/admin/app/composables/useListFilterDraft.ts +3 -2
- package/layers/admin/app/composables/useListRows.ts +7 -2
- package/layers/admin/app/composables/useListUrlState.ts +1 -1
- package/layers/admin/app/composables/usePublishStatus.ts +2 -2
- package/layers/admin/app/layouts/admin.vue +0 -1
- package/layers/admin/app/pages/admin/[collection]/[id].vue +5 -4
- package/layers/admin/app/pages/admin/index.vue +1 -1
- package/layers/admin/app/pages/admin/references.vue +2 -2
- package/layers/admin/app/utils/block-tree.ts +1 -1
- package/layers/admin/app/utils/collection-ops.ts +28 -3
- package/layers/admin/app/utils/edit-form.ts +34 -2
- package/layers/admin/app/utils/editor-expose.ts +2 -0
- package/layers/admin/app/utils/editor-form-context.ts +1 -2
- package/layers/admin/app/utils/field-keys.ts +1 -1
- package/layers/admin/app/utils/list-columns.ts +3 -2
- package/layers/admin/app/utils/list-query.ts +2 -2
- package/layers/admin/app/utils/populate-blocks.ts +2 -2
- package/layers/admin/app/utils/reauth.ts +5 -6
- package/layers/auth/server/plugins/01.register-auth-pipelines.ts +7 -0
- package/layers/collections/server/plugins/01.register.ts +9 -12
- package/layers/collections/server/plugins/02.register-relation-populate.ts +5 -7
- package/layers/{fields → core}/app/utils/field-factories.ts +1 -1
- package/layers/core/modules/auto-discovery/extract-block.ts +3 -3
- package/layers/core/modules/auto-discovery/index.ts +68 -11
- package/layers/core/modules/auto-discovery/package-registry.ts +19 -0
- package/layers/core/modules/auto-discovery/scan.ts +77 -0
- package/layers/core/modules/auto-discovery/virtual.d.ts +5 -0
- package/layers/core/modules/component-namespace/index.ts +24 -0
- package/layers/core/modules/kestrel/index.ts +6 -1
- package/layers/core/modules/plugin-order/index.ts +69 -0
- package/layers/core/modules/plugin-order/plugin-order.ts +273 -0
- package/layers/core/nuxt.config.ts +1 -0
- package/layers/core/server/api/[...path].ts +98 -0
- package/layers/core/server/plugins/00.config.ts +12 -0
- package/layers/core/server/plugins/00.migrate.ts +1 -0
- package/layers/core/server/plugins/01.register-introspection-pipeline.ts +5 -0
- package/layers/core/server/plugins/01.register-openapi-pipeline.ts +5 -0
- package/layers/core/server/plugins/01.register-tooling-pipelines.ts +5 -0
- package/layers/core/server/plugins/02.schema-sync.ts +2 -7
- package/layers/core/server/plugins/04.outbox-worker.ts +31 -0
- package/layers/core/server/plugins/05.reindex-refs.ts +5 -0
- package/layers/core/server/routes/__kestrel/dashboard.get.ts +29 -0
- package/layers/core/server/tasks/db/migrate-module.ts +83 -0
- package/layers/core/server/tasks/db/migrate-revisions.ts +20 -0
- package/layers/core/server/tasks/db/migrate.ts +2 -6
- package/layers/core/server/utils/server-config.ts +66 -3
- package/layers/fields/server/plugins/01.register-blocks-pipeline.ts +7 -0
- package/layers/fields/server/plugins/01.register-field-populate.ts +2 -2
- package/layers/media/app/components/KestrelImg.vue +3 -3
- package/layers/media/app/components/MediaLibrary.vue +2 -2
- package/layers/media/app/components/field/Media.vue +1 -1
- package/layers/media/app/composables/useMediaResolver.ts +1 -1
- package/layers/media/app/composables/useMediaUpload.ts +1 -1
- package/layers/media/app/composables/useMediaVariant.ts +2 -2
- package/layers/media/app/utils/ai-disclosure.ts +1 -1
- package/layers/media/app/utils/library.ts +1 -1
- package/layers/media/app/utils/ops.ts +1 -3
- package/layers/media/app/utils/picture-model.ts +2 -2
- package/layers/media/app/utils/variant-name.ts +1 -1
- package/layers/media/server/middleware/ondemand-variants.ts +6 -5
- package/layers/media/server/plugins/01.register-media-pipelines.ts +7 -0
- package/layers/media/server/plugins/02.register-media.ts +10 -7
- package/layers/media/server/plugins/04.variant-capture.ts +12 -8
- package/layers/media/server/plugins/05.media-cleanup.ts +5 -0
- package/layers/media/server/tasks/media/backfill.ts +3 -4
- package/layers/public/app/components/KestrelLink.vue +1 -1
- package/layers/public/app/pages/[...slug].vue +1 -1
- package/layers/public/app/pages/__kestrel/preview.vue +2 -2
- package/layers/public/app/utils/json-ld.ts +1 -1
- package/layers/public/app/utils/link-href.ts +1 -1
- package/layers/public/app/utils/page-head.ts +1 -1
- package/layers/public/modules/deploy-output/index.ts +4 -6
- package/layers/public/modules/prerender-routes/index.ts +4 -4
- package/layers/public/modules/prerender-routes/routes.ts +1 -1
- package/layers/public/modules/prune-media/index.ts +2 -2
- package/layers/public/server/delivery-live/serve.ts +111 -0
- package/layers/public/server/middleware/delivery-live-catchall.ts +7 -0
- package/layers/public/server/pipelines/index.ts +23 -0
- package/layers/public/server/plugins/00.ensure-snapshot-triggers.ts +14 -0
- package/layers/public/server/plugins/01.register-public-pipelines.ts +7 -0
- package/layers/public/server/plugins/02.register-links.ts +2 -3
- package/layers/public/server/plugins/03.redirects.ts +41 -23
- package/layers/public/server/plugins/05.plan-publish.ts +5 -0
- package/layers/public/server/plugins/zz.publish.ts +79 -24
- package/layers/public/server/routes/llms-full.txt.get.ts +5 -5
- package/layers/public/server/routes/llms.txt.get.ts +4 -2
- package/layers/public/server/routes/redirects.json.get.ts +2 -2
- package/layers/public/server/routes/robots.txt.get.ts +2 -0
- package/layers/public/server/routes/sitemap.xml.get.ts +16 -6
- package/layers/public/server/tasks/publish/run.ts +9 -4
- package/layers/public/server/utils/publish/render-live.ts +26 -0
- package/layers/ui/app/components/field/Choice.vue +1 -1
- package/layers/ui/app/components/field/Datetime.vue +1 -1
- package/layers/ui/app/components/field/Json.vue +1 -2
- package/layers/ui/app/components/field/Layout.vue +2 -4
- package/layers/ui/app/components/field/Link.vue +1 -1
- package/layers/ui/app/components/field/LinkInternalPicker.vue +3 -3
- package/layers/ui/app/components/field/Number.vue +2 -2
- package/layers/ui/app/components/field/Relation.vue +1 -1
- package/layers/ui/app/components/field/Repeater.vue +6 -8
- package/layers/ui/app/components/field/Slug.vue +2 -3
- package/layers/ui/app/components/field/Text.vue +1 -1
- package/layers/ui/app/components/ui/Button.vue +2 -2
- package/layers/ui/app/components/ui/ButtonGroup.vue +2 -2
- package/layers/ui/app/components/ui/Checkbox.vue +2 -2
- package/layers/ui/app/components/ui/Combobox.vue +5 -5
- package/layers/ui/app/components/ui/DatePicker.vue +2 -2
- package/layers/ui/app/components/ui/DateRangePicker.vue +2 -2
- package/layers/ui/app/components/ui/NumberInput.vue +4 -4
- package/layers/ui/app/components/ui/Richtext.vue +1 -1
- package/layers/ui/app/components/ui/RichtextToolbar.vue +1 -3
- package/layers/ui/app/components/ui/Select.vue +2 -2
- package/layers/ui/app/components/ui/TextInput.vue +4 -4
- package/layers/ui/app/components/ui/Textarea.vue +2 -2
- package/layers/ui/app/components/ui/TimeInput.vue +2 -2
- package/layers/ui/app/components/ui/Toasts.vue +3 -3
- package/layers/ui/app/composables/useLinkField.ts +1 -1
- package/layers/ui/app/composables/useRepeater.ts +1 -1
- package/layers/ui/app/i18n/de.ts +7 -7
- package/layers/ui/app/i18n/en.ts +7 -39
- package/layers/ui/app/utils/field-component.ts +1 -1
- package/layers/ui/app/utils/field-registry.ts +1 -1
- package/layers/ui/app/utils/icons.ts +2 -6
- package/layers/ui/app/utils/localized.ts +1 -1
- package/nuxt.config.ts +1 -1
- package/package.json +53 -19
- package/scripts/consumer-template-ci.mjs +157 -0
- package/scripts/coverage-all.mjs +163 -0
- package/scripts/dashboard.mjs +89 -0
- package/scripts/hash-password.mjs +1 -1
- package/scripts/lib/coverage-model.mjs +51 -0
- package/scripts/port-graph.mjs +350 -0
- package/scripts/typecheck.mjs +11 -0
- package/templates/starter/app/blocks/Prose.vue +1 -1
- package/layers/access/server/utils/csrf.ts +0 -39
- package/layers/access/server/utils/grant-registry.ts +0 -36
- package/layers/access/server/utils/guard.ts +0 -44
- package/layers/access/server/utils/ip-allowlist.ts +0 -79
- package/layers/access/server/utils/policy.ts +0 -98
- package/layers/access/server/utils/public-resources.ts +0 -11
- package/layers/access/server/utils/render-context.ts +0 -41
- package/layers/access/server/utils/require-admin.ts +0 -15
- package/layers/admin/app/pages/admin/[collection]/[id].nuxt.test.ts +0 -200
- package/layers/admin/app/pages/admin/[collection]/publish-preview.nuxt.test.ts +0 -142
- package/layers/auth/server/api/auth/login.post.ts +0 -38
- package/layers/auth/server/api/auth/logout.post.ts +0 -9
- package/layers/auth/server/api/auth/session.get.ts +0 -1
- package/layers/auth/server/utils/admin-credential.ts +0 -42
- package/layers/auth/server/utils/client-ip.ts +0 -88
- package/layers/auth/server/utils/login-throttle.ts +0 -126
- package/layers/auth/server/utils/password.ts +0 -52
- package/layers/auth/server/utils/session-cookie.ts +0 -44
- package/layers/auth/server/utils/session-epoch.ts +0 -96
- package/layers/auth/server/utils/session.ts +0 -88
- package/layers/collections/server/collections/pages.ts +0 -19
- package/layers/collections/server/utils/populate-relations.ts +0 -79
- package/layers/core/app/utils/bulk-actions.ts +0 -9
- package/layers/core/app/utils/filter-ops.ts +0 -98
- package/layers/core/app/utils/list-limits.ts +0 -27
- package/layers/core/app/utils/locale-path.ts +0 -12
- package/layers/core/app/utils/slugify.ts +0 -16
- package/layers/core/server/api/[collection]/[id]/dead-refs.get.ts +0 -8
- package/layers/core/server/api/[collection]/[id]/translations.get.ts +0 -4
- package/layers/core/server/api/[collection]/[id].delete.ts +0 -5
- package/layers/core/server/api/[collection]/[id].get.ts +0 -11
- package/layers/core/server/api/[collection]/[id].patch.ts +0 -8
- package/layers/core/server/api/[collection]/bulk.post.ts +0 -37
- package/layers/core/server/api/[collection]/index.get.ts +0 -24
- package/layers/core/server/api/[collection]/index.post.ts +0 -8
- package/layers/core/server/api/[collection]/index.put.test.ts +0 -76
- package/layers/core/server/api/[collection]/index.put.ts +0 -24
- package/layers/core/server/api/[collection]/options.get.test.ts +0 -60
- package/layers/core/server/api/[collection]/options.get.ts +0 -18
- package/layers/core/server/api/[collection]/populate-scope.test.ts +0 -119
- package/layers/core/server/api/[collection]/translations.get.test.ts +0 -90
- package/layers/core/server/api/[collection]/translations.get.ts +0 -25
- package/layers/core/server/api/collections/[name].get.ts +0 -4
- package/layers/core/server/api/collections/index.get.ts +0 -3
- package/layers/core/server/api/references/broken.get.ts +0 -9
- package/layers/core/server/api/references/referrers.get.ts +0 -24
- package/layers/core/server/database/record-refs.ts +0 -20
- package/layers/core/server/plugins/03.record-refs.ts +0 -12
- package/layers/core/server/schema/bootstrap.ts +0 -41
- package/layers/core/server/schema/desired.ts +0 -99
- package/layers/core/server/schema/dev-sync.ts +0 -23
- package/layers/core/server/schema/dialect.ts +0 -61
- package/layers/core/server/schema/diff.ts +0 -110
- package/layers/core/server/schema/introspect.ts +0 -68
- package/layers/core/server/schema/model.ts +0 -61
- package/layers/core/server/schema/render-sqlite.ts +0 -89
- package/layers/core/server/schema/sync.ts +0 -201
- package/layers/core/server/utils/blocks.ts +0 -6
- package/layers/core/server/utils/collection-types.ts +0 -28
- package/layers/core/server/utils/crud.ts +0 -578
- package/layers/core/server/utils/db.ts +0 -26
- package/layers/core/server/utils/defineCollection.ts +0 -213
- package/layers/core/server/utils/duplicate.ts +0 -162
- package/layers/core/server/utils/field-layout.ts +0 -125
- package/layers/core/server/utils/filter-predicate.ts +0 -101
- package/layers/core/server/utils/http.ts +0 -50
- package/layers/core/server/utils/if-unmodified.ts +0 -13
- package/layers/core/server/utils/kestrel-config.ts +0 -451
- package/layers/core/server/utils/key-lock.ts +0 -33
- package/layers/core/server/utils/locale.ts +0 -53
- package/layers/core/server/utils/page-route.ts +0 -58
- package/layers/core/server/utils/page-slug.ts +0 -127
- package/layers/core/server/utils/picker.ts +0 -82
- package/layers/core/server/utils/populate.ts +0 -50
- package/layers/core/server/utils/prerender-memo.ts +0 -23
- package/layers/core/server/utils/read-capture.ts +0 -37
- package/layers/core/server/utils/record-ref-index.ts +0 -200
- package/layers/core/server/utils/registry.ts +0 -24
- package/layers/core/server/utils/resolve-scope.ts +0 -79
- package/layers/core/server/utils/seo.ts +0 -42
- package/layers/core/server/utils/serialize-collection.ts +0 -147
- package/layers/core/server/utils/sql.ts +0 -2
- package/layers/core/server/utils/storage.local.ts +0 -117
- package/layers/core/server/utils/storage.s3.ts +0 -197
- package/layers/core/server/utils/storage.ts +0 -90
- package/layers/core/server/utils/write-effects.ts +0 -40
- package/layers/core/server/utils/write-events.ts +0 -31
- package/layers/fields/app/utils/condition.ts +0 -71
- package/layers/fields/app/utils/define-block.ts +0 -13
- package/layers/fields/app/utils/field-constraints.ts +0 -17
- package/layers/fields/app/utils/field-meta.ts +0 -33
- package/layers/fields/app/utils/field-validate.ts +0 -75
- package/layers/fields/app/utils/field-value.ts +0 -7
- package/layers/fields/app/utils/richtext-links.ts +0 -48
- package/layers/fields/server/api/blocks/index.get.ts +0 -9
- package/layers/fields/server/field-registry/index.ts +0 -249
- package/layers/fields/server/field-registry/naming.ts +0 -10
- package/layers/fields/server/field-registry/sanitize.ts +0 -43
- package/layers/fields/server/field-registry/types.ts +0 -19
- package/layers/fields/server/utils/block-ids.ts +0 -43
- package/layers/fields/server/utils/block-populate.ts +0 -41
- package/layers/fields/server/utils/buildCollection.ts +0 -90
- package/layers/fields/server/utils/buildFieldSchema.ts +0 -22
- package/layers/fields/server/utils/buildTable.ts +0 -113
- package/layers/fields/server/utils/conditional-required.ts +0 -25
- package/layers/fields/server/utils/defineBlock.ts +0 -86
- package/layers/fields/server/utils/defineFieldType.ts +0 -33
- package/layers/fields/server/utils/extract-refs.ts +0 -152
- package/layers/fields/server/utils/field-populate.ts +0 -68
- package/layers/media/server/api/media/[id]/usages.get.ts +0 -8
- package/layers/media/server/api/media/[id].delete.ts +0 -18
- package/layers/media/server/api/media/[id].get.ts +0 -10
- package/layers/media/server/api/media/[id].patch.test.ts +0 -66
- package/layers/media/server/api/media/[id].patch.ts +0 -65
- package/layers/media/server/api/media/backfill.post.ts +0 -18
- package/layers/media/server/api/media/copy.post.ts +0 -7
- package/layers/media/server/api/media/delete.post.ts +0 -13
- package/layers/media/server/api/media/folders.post.ts +0 -18
- package/layers/media/server/api/media/index.get.ts +0 -15
- package/layers/media/server/api/media/index.post.ts +0 -173
- package/layers/media/server/api/media/library.get.ts +0 -17
- package/layers/media/server/api/media/move.post.ts +0 -7
- package/layers/media/server/api/media/rename.post.ts +0 -7
- package/layers/media/server/api/media/resolve.get.ts +0 -13
- package/layers/media/server/collections/media-settings.ts +0 -25
- package/layers/media/server/collections/media.ts +0 -43
- package/layers/media/server/database/folders.ts +0 -10
- package/layers/media/server/plugins/03.media-cleanup.ts +0 -20
- package/layers/media/server/schema-tables/folders.ts +0 -4
- package/layers/media/server/utils/ai-disclosure-enabled.ts +0 -16
- package/layers/media/server/utils/ai-signal-detect.ts +0 -155
- package/layers/media/server/utils/backfill.ts +0 -144
- package/layers/media/server/utils/derive.ts +0 -59
- package/layers/media/server/utils/folder-paths.ts +0 -42
- package/layers/media/server/utils/folders.ts +0 -12
- package/layers/media/server/utils/library.ts +0 -91
- package/layers/media/server/utils/media-cleanup.ts +0 -27
- package/layers/media/server/utils/media-enabled.ts +0 -23
- package/layers/media/server/utils/media-ops.ts +0 -118
- package/layers/media/server/utils/media-write.ts +0 -13
- package/layers/media/server/utils/naming.ts +0 -52
- package/layers/media/server/utils/ondemand.ts +0 -69
- package/layers/media/server/utils/persist-upload.ts +0 -62
- package/layers/media/server/utils/populate.ts +0 -47
- package/layers/media/server/utils/record.ts +0 -71
- package/layers/media/server/utils/relocate-ops.ts +0 -341
- package/layers/media/server/utils/relocate-plan.ts +0 -63
- package/layers/media/server/utils/resolve.ts +0 -98
- package/layers/media/server/utils/sanitize-svg.ts +0 -12
- package/layers/media/server/utils/sniff.ts +0 -68
- package/layers/media/server/utils/storage-relocate.ts +0 -139
- package/layers/media/server/utils/translations.ts +0 -19
- package/layers/media/server/utils/usages.ts +0 -66
- package/layers/media/server/utils/variant-capture.ts +0 -70
- package/layers/media/server/utils/variants.ts +0 -123
- package/layers/public/modules/deploy-output/deploy-output.ts +0 -384
- package/layers/public/server/api/links/resolve.get.ts +0 -19
- package/layers/public/server/api/preview.get.ts +0 -28
- package/layers/public/server/api/preview.post.ts +0 -93
- package/layers/public/server/api/publish-status.get.ts +0 -58
- package/layers/public/server/api/publish.post.ts +0 -84
- package/layers/public/server/api/route.get.ts +0 -54
- package/layers/public/server/collections/redirects.ts +0 -75
- package/layers/public/server/collections/site.ts +0 -44
- package/layers/public/server/database/publish-deps.ts +0 -12
- package/layers/public/server/database/publish-status.ts +0 -19
- package/layers/public/server/schema-tables/publish-deps.ts +0 -4
- package/layers/public/server/schema-tables/publish-status.ts +0 -4
- package/layers/public/server/utils/link-resolve.ts +0 -38
- package/layers/public/server/utils/llms-full.ts +0 -125
- package/layers/public/server/utils/llms.ts +0 -52
- package/layers/public/server/utils/page-resolve.ts +0 -208
- package/layers/public/server/utils/populate-links.ts +0 -66
- package/layers/public/server/utils/preview-token.ts +0 -109
- package/layers/public/server/utils/publish/coalesce.ts +0 -22
- package/layers/public/server/utils/publish/deps-persistence.ts +0 -43
- package/layers/public/server/utils/publish/deps.ts +0 -87
- package/layers/public/server/utils/publish/invalidation.ts +0 -225
- package/layers/public/server/utils/publish/pending.ts +0 -74
- package/layers/public/server/utils/publish/publish-runtime.ts +0 -28
- package/layers/public/server/utils/publish/publish-status.ts +0 -72
- package/layers/public/server/utils/publish/publisher.ts +0 -362
- package/layers/public/server/utils/publish/queue.ts +0 -67
- package/layers/public/server/utils/publish/redirect-rules.ts +0 -221
- package/layers/public/server/utils/publish/redirects-artifact.ts +0 -20
- package/layers/public/server/utils/publish/route-for-record.ts +0 -16
- package/layers/public/server/utils/publish/route-keys.ts +0 -13
- package/layers/public/server/utils/richtext-markdown.ts +0 -260
- package/layers/public/server/utils/site-url.ts +0 -40
- package/layers/public/server/utils/sitemap.ts +0 -99
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { pageRowHref } from '../../../../core/server/utils/page-route'
|
|
2
|
-
|
|
3
|
-
/** The shape of the collection a write targets (the subset invalidation needs). */
|
|
4
|
-
export interface WriteCollection {
|
|
5
|
-
name: string
|
|
6
|
-
pageLike?: boolean
|
|
7
|
-
/** Whether the collection has a draft/published `status` column. */
|
|
8
|
-
status?: boolean
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
type Row = Record<string, unknown> | null
|
|
12
|
-
|
|
13
|
-
/** A content write reduced to what invalidation needs (computed by `classifyWrite`). */
|
|
14
|
-
export interface WriteClassification {
|
|
15
|
-
collection: string
|
|
16
|
-
pageLike: boolean
|
|
17
|
-
status: 'created' | 'updated' | 'deleted'
|
|
18
|
-
id: number | null
|
|
19
|
-
/** pageLike `path` changed (the old static file must be pruned, the new one written). */
|
|
20
|
-
pathChanged: boolean
|
|
21
|
-
/** published-ness changed (publish or unpublish — adds/removes the page + sitemap entry). */
|
|
22
|
-
statusChanged: boolean
|
|
23
|
-
isPublished: boolean
|
|
24
|
-
wasPublished: boolean
|
|
25
|
-
/** The page's own public route from the NEW row (pageLike + has a path), else null. */
|
|
26
|
-
selfRoute: string | null
|
|
27
|
-
/** The page's own public route from the OLD row (pageLike + had a path), else null — the route whose
|
|
28
|
-
* static file must be pruned on a slug change / unpublish / delete (symmetric to `selfRoute`). */
|
|
29
|
-
oldRoute: string | null
|
|
30
|
-
groupTag: string | null
|
|
31
|
-
/** Page-path tags for the breadcrumb edge — this record's own path before and/or after the write (both,
|
|
32
|
-
* on a rename); empty when it is nobody's crumb step on either side. See `pagePathTag`. */
|
|
33
|
-
crumbTags: string[]
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** The data tag naming a translation group. `#` keeps it clear of the `<coll>:<id>` record namespace. */
|
|
37
|
-
export function translationGroupTag(coll: string, group: string): string {
|
|
38
|
-
return `${coll}#group:${group}`
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The data tag naming a page-like PATH rather than a record — the edge a DESCENDANT's breadcrumb hangs on.
|
|
43
|
-
*
|
|
44
|
-
* It has to be the path, because Kestrel has no parent/child relation between pages: `path` is a plain
|
|
45
|
-
* column, a slug is flat unless an editor types slashes into it, and "descendant" is nothing but a
|
|
46
|
-
* path-prefix match. So the case that matters most — a page CREATED at `/blog` after `/blog/hello` was
|
|
47
|
-
* already published — has no record id that anything could have captured beforehand. A path, by contrast,
|
|
48
|
-
* is knowable before its page exists, so a descendant subscribes to the path it looked in.
|
|
49
|
-
*
|
|
50
|
-
* Deliberately locale-LESS: a non-translatable record has no locale to name, and a descendant looking up
|
|
51
|
-
* `/blog` in its own locale must still be reached when the page that appears there is a locale-less one.
|
|
52
|
-
* It therefore over-approximates across locales (an `en` `/blog` write also re-renders a `de` descendant
|
|
53
|
-
* of the same spelling) — extra renders, never a stale page.
|
|
54
|
-
*
|
|
55
|
-
* The leading `#` keeps it clear of the `<coll>` and `<coll>:<id>` namespaces (a collection name never
|
|
56
|
-
* starts with one).
|
|
57
|
-
*/
|
|
58
|
-
export function pagePathTag(path: string): string {
|
|
59
|
-
return `#path:${path}`
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** What to republish for a write. Routes are resolved from `tags` against the captured deps index. */
|
|
63
|
-
export type Invalidation =
|
|
64
|
-
| { type: 'full' }
|
|
65
|
-
| { type: 'tags'; tags: string[]; render: string[]; prune: string[] }
|
|
66
|
-
| { type: 'noop' }
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* The routes to actually delete from a coalesced invalidation: `prune` minus anything we just wrote live.
|
|
70
|
-
* A render and a prune for the SAME route can coalesce into one window (e.g. a new page created at /x while
|
|
71
|
-
* another page vacates /x) — pruning a freshly-rendered route would delete the live page, so render wins.
|
|
72
|
-
* Filtered against the ACTUALLY-rendered set (not the render candidates), so a route whose record is now a
|
|
73
|
-
* draft — skipped by the renderer — is still pruned.
|
|
74
|
-
*/
|
|
75
|
-
export function routesToPrune(prune: string[], rendered: string[]): string[] {
|
|
76
|
-
const live = new Set(rendered)
|
|
77
|
-
return prune.filter((r) => !live.has(r))
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** A collection with no `status` column is always public; otherwise public ⇔ status === 'published'. */
|
|
81
|
-
function isPublic(def: WriteCollection, row: Row): boolean {
|
|
82
|
-
if (!def.status) return true
|
|
83
|
-
return !!row && row.status === 'published'
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* The path this record contributes as a breadcrumb step, or null when it contributes none. Mirrors
|
|
88
|
-
* `publishedAncestors` exactly — that lookup skips anything unpublished, `noindex`ed or path-less, so a
|
|
89
|
-
* record in one of those states is in nobody's trail and its writes owe nobody a re-render.
|
|
90
|
-
*/
|
|
91
|
-
function crumbPath(def: WriteCollection, row: Row): string | null {
|
|
92
|
-
if (!def.pageLike || !row) return null
|
|
93
|
-
if (!isPublic(def, row)) return null
|
|
94
|
-
if ((row.seo as { noindex?: boolean } | null | undefined)?.noindex) return null
|
|
95
|
-
return typeof row.path === 'string' && row.path ? row.path : null
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Reduce a before/after row pair (either may be null) to a `WriteClassification`. Pure (routes via the shared `pageRowHref`). */
|
|
99
|
-
export function classifyWrite(def: WriteCollection, before: Row, after: Row, primaryLocale: string, prefixPrimary = false): WriteClassification {
|
|
100
|
-
const status: WriteClassification['status'] = before === null ? 'created' : after === null ? 'deleted' : 'updated'
|
|
101
|
-
const row = after ?? before
|
|
102
|
-
const id = typeof row?.id === 'number' ? row.id : null
|
|
103
|
-
const pageLike = !!def.pageLike
|
|
104
|
-
|
|
105
|
-
const isPublished = isPublic(def, after)
|
|
106
|
-
const wasPublished = isPublic(def, before)
|
|
107
|
-
const statusChanged = status === 'updated' && !!def.status && isPublished !== wasPublished
|
|
108
|
-
|
|
109
|
-
const routeFrom = (r: Row): string | null => (pageLike ? pageRowHref(r, primaryLocale, prefixPrimary) : null)
|
|
110
|
-
const selfRoute = routeFrom(after)
|
|
111
|
-
const oldRoute = routeFrom(before)
|
|
112
|
-
// Compare the RESOLVED routes, not raw `path`: the route is localePath(path, locale), so a locale-only
|
|
113
|
-
// change (path unchanged) still moves the route — the old-locale file must be pruned.
|
|
114
|
-
const pathChanged = pageLike && status === 'updated' && oldRoute !== selfRoute
|
|
115
|
-
|
|
116
|
-
// `update` refuses to move a row between groups, so the surviving row's group is the group either way.
|
|
117
|
-
const group = row?.translationGroup
|
|
118
|
-
const groupTag = typeof group === 'string' && group ? translationGroupTag(def.name, group) : null
|
|
119
|
-
|
|
120
|
-
// Both sides, so a rename repairs the descendants of the OLD path as well as the new one. Deliberately
|
|
121
|
-
// NOT narrowed to "did the crumb's label actually change": the explicit publish action classifies its
|
|
122
|
-
// write as before === after (`publish.post.ts` — a re-render of the record's current state), so a diff
|
|
123
|
-
// would see nothing on the very publish that makes the crumb appear.
|
|
124
|
-
const crumbTags = [...new Set([crumbPath(def, before), crumbPath(def, after)].filter((p): p is string => p !== null))]
|
|
125
|
-
.map(pagePathTag)
|
|
126
|
-
|
|
127
|
-
return { collection: def.name, pageLike, status, id, pathChanged, statusChanged, isPublished, wasPublished, selfRoute, oldRoute, groupTag, crumbTags }
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* What the write listener enqueues for a content write. `publishOnSave` (`output.publishOnSave`) is the
|
|
132
|
-
* documented way back to the pre-2.0 model where a save WAS a publish: with it on, a write plans exactly
|
|
133
|
-
* what it always did. Off (the default), only removals pass — see `planSaveInvalidation`.
|
|
134
|
-
*/
|
|
135
|
-
export function planWrite(ev: WriteClassification, publishOnSave: boolean): Invalidation {
|
|
136
|
-
return publishOnSave ? planInvalidation(ev) : planSaveInvalidation(ev)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* What a plain SAVE may do to the static output. Saving persists to the DB; writing a page's file is the
|
|
141
|
-
* explicit publish action's job (`planInvalidation`, driven by `POST /api/publish`), so a save renders
|
|
142
|
-
* nothing — the live site keeps serving the last published version while the editor works on the next one.
|
|
143
|
-
*
|
|
144
|
-
* REMOVAL is the asymmetry, and it is deliberate: an unpublished or deleted record must not keep a live
|
|
145
|
-
* page, so those two branches act immediately. Their referrer/listing re-renders come along, because a
|
|
146
|
-
* baked link to a page that just went offline is stale the moment it goes — the same "availability" rule
|
|
147
|
-
* `planInvalidation` documents, minus everything that would put NEW content on the live site.
|
|
148
|
-
*/
|
|
149
|
-
export function planSaveInvalidation(ev: WriteClassification): Invalidation {
|
|
150
|
-
const removal = ev.status === 'deleted' || (ev.statusChanged && !ev.isPublished)
|
|
151
|
-
return removal ? planInvalidation(ev) : { type: 'noop' }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Decide what a write invalidates, per the maintainer-agreed model. Three notions of "dependent":
|
|
156
|
-
* - LISTINGS — pages that QUERY the collection (overviews) → captured as the `<coll>` tag.
|
|
157
|
-
* - EXPLICIT REFERRERS — pages that LINK/EMBED/relate-to a specific record → captured as `<coll>:<id>`.
|
|
158
|
-
* - TRANSLATION SIBLINGS — every member of a group bakes the group's hreflang set → captured as the
|
|
159
|
-
* group tag, the only edge that reaches members which rendered before this row existed.
|
|
160
|
-
*
|
|
161
|
-
* Two principles drive the split:
|
|
162
|
-
* 1. FRESHENING (content or path changed) re-renders BOTH listings and explicit referrers (`[coll, coll:id]`)
|
|
163
|
-
* so their output stays fresh.
|
|
164
|
-
* 2. AVAILABILITY (publish / unpublish / delete) re-renders listings (membership changed) AND the explicit
|
|
165
|
-
* referrers (`[coll, coll:id]`), because a referrer's BAKED output encodes the target's availability:
|
|
166
|
-
* link resolution is status-gated (a draft target bakes `href="#"`) and a page's hreflang set lists only
|
|
167
|
-
* its published translation siblings — both go dead/stale the moment availability flips, and only a
|
|
168
|
-
* re-render can fix them. A record's OWN artifact always matches its DB state (render on publish, prune
|
|
169
|
-
* on unpublish/delete). A draft that stays a draft is a no-op.
|
|
170
|
-
*
|
|
171
|
-
* `full` is reserved for the boot publish + the reconciler (enqueued directly), never produced here.
|
|
172
|
-
*/
|
|
173
|
-
export function planInvalidation(ev: WriteClassification): Invalidation {
|
|
174
|
-
const coll = ev.collection
|
|
175
|
-
const recordTag = ev.id != null ? `${coll}:${ev.id}` : null
|
|
176
|
-
// Unlike recordTag (dropped on create — no referrer can target a brand-new id), groupTag is included even
|
|
177
|
-
// there: a new sibling still changes every existing member's hreflang set.
|
|
178
|
-
const groupTags = ev.groupTag ? [ev.groupTag] : []
|
|
179
|
-
// Like groupTag, the crumb tags ride EVERY branch including create — a page appearing at an ancestor
|
|
180
|
-
// path is exactly what a descendant's breadcrumb was waiting for.
|
|
181
|
-
const tags = recordTag ? [coll, recordTag, ...groupTags, ...ev.crumbTags] : [coll, ...groupTags, ...ev.crumbTags]
|
|
182
|
-
const selfRender = ev.pageLike && ev.selfRoute ? [ev.selfRoute] : []
|
|
183
|
-
|
|
184
|
-
// DELETE — leaves the collection. Listings re-render, referrers too (their baked link/hreflang now points
|
|
185
|
-
// at a route that is about to disappear); the own (pageLike) route is pruned.
|
|
186
|
-
if (ev.status === 'deleted') {
|
|
187
|
-
const prune = ev.pageLike && ev.oldRoute ? [ev.oldRoute] : []
|
|
188
|
-
return { type: 'tags', tags, render: [], prune }
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// CREATE — joins the collection. A created draft produces no public output (noop); a created published
|
|
192
|
-
// record re-renders listings + its own route. No referrer can point at a brand-new id, so no `coll:id`.
|
|
193
|
-
if (ev.status === 'created') {
|
|
194
|
-
if (!ev.isPublished) return { type: 'noop' }
|
|
195
|
-
return { type: 'tags', tags: [coll, ...groupTags, ...ev.crumbTags], render: selfRender, prune: [] }
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// UNPUBLISH — leaves the published set. Listings re-render; referrers re-render so their link falls back to
|
|
199
|
-
// '#' and their hreflang drops this sibling; the own route is pruned (the live file sits at the OLD path if
|
|
200
|
-
// this save also renamed).
|
|
201
|
-
if (ev.statusChanged && !ev.isPublished) {
|
|
202
|
-
const route = ev.oldRoute ?? ev.selfRoute
|
|
203
|
-
const prune = ev.pageLike && route ? [route] : []
|
|
204
|
-
return { type: 'tags', tags, render: [], prune }
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// PUBLISH — joins the published set. Listings re-render; referrers re-render because a link to a draft
|
|
208
|
-
// resolves to '#' — their baked href only becomes the real path on a re-render; the own route renders.
|
|
209
|
-
if (ev.statusChanged && ev.isPublished) {
|
|
210
|
-
return { type: 'tags', tags, render: selfRender, prune: [] }
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Below: a plain update, no availability change. A draft that stays a draft touches no public output.
|
|
214
|
-
if (!ev.isPublished) return { type: 'noop' }
|
|
215
|
-
|
|
216
|
-
// SLUG / PATH CHANGE — path freshening: listings + referrers re-render (their link path updates); the new
|
|
217
|
-
// route renders, the old route is pruned.
|
|
218
|
-
if (ev.pathChanged) {
|
|
219
|
-
const prune = ev.oldRoute ? [ev.oldRoute] : []
|
|
220
|
-
return { type: 'tags', tags, render: selfRender, prune }
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// CONTENT EDIT — content freshening: listings + explicit referrers re-render; the own route re-renders.
|
|
224
|
-
return { type: 'tags', tags, render: selfRender, prune: [] }
|
|
225
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* "Saved but not published": the state a deferred-publish model needs to name. Saving writes the DB,
|
|
3
|
-
* publishing writes the static file, so the two stamps drift apart on purpose — a record edited after its
|
|
4
|
-
* page was last published keeps serving the published version until someone publishes again.
|
|
5
|
-
*
|
|
6
|
-
* The tolerance is not cosmetic: `publish_status.updated_at` is stored in whole seconds while a record's
|
|
7
|
-
* `updatedAt` is milliseconds, so the publish that directly followed a save can carry a stamp up to a
|
|
8
|
-
* second BEHIND it. Without the slack every freshly published page would report unpublished changes.
|
|
9
|
-
*/
|
|
10
|
-
const TOLERANCE_MS = 1000
|
|
11
|
-
|
|
12
|
-
export function hasPendingChanges(savedAtMs: number | null | undefined, publishedAtMs: number | null | undefined, toleranceMs = TOLERANCE_MS): boolean {
|
|
13
|
-
// Never published (no status row) → nothing to protect: there is no older artifact this edit could
|
|
14
|
-
// overtake, so the route is a normal render candidate, not a pending change.
|
|
15
|
-
if (savedAtMs == null || publishedAtMs == null) return false
|
|
16
|
-
return savedAtMs > publishedAtMs + toleranceMs
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** The subset of `savedAt` routes whose record moved on after the route's last publish. Pure. */
|
|
20
|
-
export function pendingRoutes(savedAt: Map<string, number>, publishedAt: Map<string, number>): string[] {
|
|
21
|
-
const out: string[] = []
|
|
22
|
-
for (const [route, saved] of savedAt) {
|
|
23
|
-
if (hasPendingChanges(saved, publishedAt.get(route) ?? null)) out.push(route)
|
|
24
|
-
}
|
|
25
|
-
return out
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface HeldRoutes {
|
|
29
|
-
/** Routes to leave un-rendered: their record is serving an older published version somewhere. */
|
|
30
|
-
hold: Set<string>
|
|
31
|
-
/** Previously-published routes that are still the live artifact of a held record — never prune these. */
|
|
32
|
-
keep: Set<string>
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Withholding by RECORD rather than by route string, which is what a rename needs. `pendingRoutes` compares
|
|
37
|
-
* a route against its own publish stamp, so a renamed record — whose new route has no stamp at all — falls
|
|
38
|
-
* through the "never published, nothing to protect" carve-out: the unpublished rename gets rendered and the
|
|
39
|
-
* old route, still the live one, is left looking abandoned to the prune. The carve-out is about a FIRST
|
|
40
|
-
* deploy having no older version; a rename has one, at the previous route.
|
|
41
|
-
*
|
|
42
|
-
* So a record's prior published routes are consulted too: if the record has moved on since the newest of
|
|
43
|
-
* them, the new route is held back and those prior routes are protected from the prune. A record with no
|
|
44
|
-
* prior published route keeps the carve-out — otherwise a first deploy would produce an empty site.
|
|
45
|
-
*
|
|
46
|
-
* Pure: `routesForTag` is the deps index's lookup, passed in.
|
|
47
|
-
*/
|
|
48
|
-
export function heldRoutes(
|
|
49
|
-
savedAt: Map<string, number>,
|
|
50
|
-
publishedAt: Map<string, number>,
|
|
51
|
-
recordTag: Map<string, string>,
|
|
52
|
-
routesForTag: (tag: string) => Iterable<string>,
|
|
53
|
-
toleranceMs = TOLERANCE_MS,
|
|
54
|
-
): HeldRoutes {
|
|
55
|
-
const hold = new Set<string>()
|
|
56
|
-
const keep = new Set<string>()
|
|
57
|
-
for (const [route, saved] of savedAt) {
|
|
58
|
-
const own = publishedAt.get(route) ?? null
|
|
59
|
-
if (own != null) {
|
|
60
|
-
if (hasPendingChanges(saved, own, toleranceMs)) hold.add(route)
|
|
61
|
-
continue
|
|
62
|
-
}
|
|
63
|
-
// No stamp of its own: either genuinely never published, or published under a previous route.
|
|
64
|
-
const tag = recordTag.get(route)
|
|
65
|
-
if (!tag) continue
|
|
66
|
-
const priors = [...routesForTag(tag)].filter((r) => r !== route && publishedAt.has(r))
|
|
67
|
-
if (!priors.length) continue // first publish of this record — nothing to protect
|
|
68
|
-
const newest = Math.max(...priors.map((r) => publishedAt.get(r)!))
|
|
69
|
-
if (!hasPendingChanges(saved, newest, toleranceMs)) continue
|
|
70
|
-
hold.add(route)
|
|
71
|
-
for (const prior of priors) keep.add(prior)
|
|
72
|
-
}
|
|
73
|
-
return { hold, keep }
|
|
74
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { PublishQueue } from './queue'
|
|
2
|
-
import type { DepsStore } from './deps'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The process-wide handle on the running publish machinery. The `zz.publish` Nitro plugin owns the queue
|
|
6
|
-
* and the deps index (it builds the output driver and does the logging), but the explicit publish action —
|
|
7
|
-
* `POST /api/publish` — has to reach both from inside a request: the queue to enqueue the run, the deps
|
|
8
|
-
* index to find the routes this record was baked into. Mirrors core's `write-events` registry: module
|
|
9
|
-
* state, set once at boot.
|
|
10
|
-
*
|
|
11
|
-
* `null` wherever the runtime publisher does not run at all (dev, or `output.auto` off). That is not an
|
|
12
|
-
* error — it is what the endpoint reports back so the editor can say "nothing is generated here" instead
|
|
13
|
-
* of pretending a publish happened.
|
|
14
|
-
*/
|
|
15
|
-
export interface PublishRuntime {
|
|
16
|
-
queue: PublishQueue
|
|
17
|
-
deps: DepsStore
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
let runtime: PublishRuntime | null = null
|
|
21
|
-
|
|
22
|
-
export function setPublishRuntime(next: PublishRuntime | null): void {
|
|
23
|
-
runtime = next
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function usePublishRuntime(): PublishRuntime | null {
|
|
27
|
-
return runtime
|
|
28
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { eq } from 'drizzle-orm'
|
|
2
|
-
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
|
|
3
|
-
import { publishStatus } from '../../database/publish-status'
|
|
4
|
-
|
|
5
|
-
export interface PublishOutcome {
|
|
6
|
-
status: 'success' | 'error'
|
|
7
|
-
/** The failure message (render / write / S3) on an 'error' outcome; omitted/null on success. */
|
|
8
|
-
error?: string | null
|
|
9
|
-
/** Which output the attempt wrote to — `outputConfig().driver` ('local' | 's3'). */
|
|
10
|
-
target: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Upsert the LATEST publish outcome for a route — one row per route (latest-state, not a history). Called
|
|
15
|
-
* by the runtime publisher after each render+put (success) or when one throws (error). Resilient by design:
|
|
16
|
-
* a missing `publish_status` table (a not-yet-migrated deploy — prod never auto-DDLs) degrades to a warn
|
|
17
|
-
* rather than breaking the publish loop or masking the real render/write error in the caller's catch. The
|
|
18
|
-
* timestamp is an explicit `new Date()` so the conflict-update branch (which does not fire `$defaultFn`)
|
|
19
|
-
* also refreshes it; drizzle maps it to UNIX seconds (mode:'timestamp'), never raw milliseconds.
|
|
20
|
-
*/
|
|
21
|
-
export function recordPublishStatus(db: BetterSQLite3Database, route: string, outcome: PublishOutcome): void {
|
|
22
|
-
try {
|
|
23
|
-
const set = { status: outcome.status, error: outcome.error ?? null, target: outcome.target, updatedAt: new Date() }
|
|
24
|
-
db.insert(publishStatus).values({ route, ...set }).onConflictDoUpdate({ target: publishStatus.route, set }).run()
|
|
25
|
-
} catch (error) {
|
|
26
|
-
console.warn('[kestrel] could not record publish status (is `publish_status` migrated? run `db:migrate`):', (error as Error).message)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Classify a route's render result into a publish outcome:
|
|
32
|
-
* - `success` — a 200 with a rendered body (write it + record success).
|
|
33
|
-
* - `error` — a 5xx: the page itself rendered to a server error (a bad block / relation / template).
|
|
34
|
-
* Crucially this surfaces as a non-200 RESPONSE, not a thrown exception, so without this
|
|
35
|
-
* branch it would be a silent skip leaving a stale `success` row — the failure mode the
|
|
36
|
-
* editor ampel most needs to show.
|
|
37
|
-
* - `skip` — any other non-200 (a draft / unpublish race resolving to a 404 / redirect): not an error,
|
|
38
|
-
* leave the route's status untouched (the prune path handles a real unpublish).
|
|
39
|
-
*/
|
|
40
|
-
export function renderOutcome(status: number, hasBody: boolean): 'success' | 'error' | 'skip' {
|
|
41
|
-
if (status === 200 && hasBody) return 'success'
|
|
42
|
-
if (status >= 500) return 'error'
|
|
43
|
-
return 'skip'
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Every route's last successful-or-failed publish time, in ms — the "last published" half of the
|
|
48
|
-
* saved-vs-published comparison a deferred publish needs. Same missing-table resilience as the writers:
|
|
49
|
-
* an unmigrated deploy yields an empty map, which reads as "nothing was ever published here" and so
|
|
50
|
-
* holds nothing back.
|
|
51
|
-
*/
|
|
52
|
-
export function lastPublishedAt(db: BetterSQLite3Database): Map<string, number> {
|
|
53
|
-
const out = new Map<string, number>()
|
|
54
|
-
try {
|
|
55
|
-
for (const row of db.select({ route: publishStatus.route, updatedAt: publishStatus.updatedAt }).from(publishStatus).all()) {
|
|
56
|
-
if (row.updatedAt instanceof Date) out.set(row.route, row.updatedAt.getTime())
|
|
57
|
-
}
|
|
58
|
-
} catch (error) {
|
|
59
|
-
console.warn('[kestrel] could not read publish status:', (error as Error).message)
|
|
60
|
-
}
|
|
61
|
-
return out
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Clear a route's status row — its static file was pruned (unpublish / delete / slug change), so it is no
|
|
65
|
-
* longer live. Idempotent; same missing-table resilience as `recordPublishStatus`. */
|
|
66
|
-
export function clearPublishStatus(db: BetterSQLite3Database, route: string): void {
|
|
67
|
-
try {
|
|
68
|
-
db.delete(publishStatus).where(eq(publishStatus.route, route)).run()
|
|
69
|
-
} catch (error) {
|
|
70
|
-
console.warn('[kestrel] could not clear publish status:', (error as Error).message)
|
|
71
|
-
}
|
|
72
|
-
}
|