@michaelthielemann/kestrel 3.0.1 → 4.0.1
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 +69 -11
- package/layers/core/modules/auto-discovery/package-registry.ts +19 -0
- package/layers/core/modules/auto-discovery/scan.ts +92 -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 +1 -3
- 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/Richtext.vue +1 -1
- package/layers/ui/app/components/ui/RichtextToolbar.vue +1 -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 +160 -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
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { buildIntrospectionPipelines, registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
3
|
+
export default defineNitroPlugin(() => {
|
|
4
|
+
for (const def of buildIntrospectionPipelines()) registerPipeline(def)
|
|
5
|
+
})
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { buildOpenApiPipelines, registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
3
|
+
export default defineNitroPlugin(() => {
|
|
4
|
+
for (const def of buildOpenApiPipelines()) registerPipeline(def)
|
|
5
|
+
})
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { buildToolingPipelines, registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
3
|
+
export default defineNitroPlugin(() => {
|
|
4
|
+
for (const def of buildToolingPipelines()) registerPipeline(def)
|
|
5
|
+
})
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import collections from '#kestrel/collections'
|
|
2
2
|
import schemaTables from '#kestrel/schema-tables'
|
|
3
3
|
import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
4
|
-
import
|
|
5
|
-
import type { CollectionDef } from '
|
|
4
|
+
import { describeOp, desiredFromCollections, isDestructive, planOps, runDevSchemaSync, sqlite, useDb } from '@michaelthielemann/kestrel-core'
|
|
5
|
+
import type { BuiltCollection, CollectionDef, SyncDb } from '@michaelthielemann/kestrel-core'
|
|
6
6
|
import { resolveServerKestrel, serverRuntimeConfig } from '../utils/server-config'
|
|
7
|
-
import { desiredFromCollections } from '../schema/bootstrap'
|
|
8
|
-
import { planOps, describeOp, isDestructive, type SyncDb } from '../schema/sync'
|
|
9
|
-
import { sqlite } from '../schema/dialect'
|
|
10
|
-
import { runDevSchemaSync } from '../schema/dev-sync'
|
|
11
|
-
|
|
12
7
|
// Dev-only schema auto-sync (ADR-0002). Runs after 00.migrate, so built-in collections are already
|
|
13
8
|
// migrated and this is a no-op on them; it additively reconciles the live DB to whatever the registered
|
|
14
9
|
// collections (+ the discovered standalone tables) now compile to — so adding a collection or a field
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { buildOutboxPipelines, makeTicker, registerPipeline, useDb } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
/** Hard-coded to `content` — the only module with an outbox table today; extend alongside
|
|
3
|
+
* `pipelines/outbox.ts` if a second module gets one. */
|
|
4
|
+
const OUTBOX_MODULE = 'content'
|
|
5
|
+
const POLL_INTERVAL_MS = 1000
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The outbox worker: NOT a Nitro `scheduledTask` (those run per-request-worker in a way that doesn't suit
|
|
9
|
+
* an always-on background loop here) — a plain `setInterval` loop started at plugin init, `unref`'d so it
|
|
10
|
+
* never keeps the process alive on its own (a test/dev server can still exit while it's running).
|
|
11
|
+
*
|
|
12
|
+
* Polls every tick, unconditionally — no `PRAGMA data_version` short-circuit: `useDb()` is the process-
|
|
13
|
+
* wide singleton connection every write also goes through, and a connection never observes its OWN writes
|
|
14
|
+
* bumping `data_version` (only a write from a DIFFERENT connection does), so gating on it here would go
|
|
15
|
+
* permanently blind after the first write this same connection ever made. The poll query itself is a
|
|
16
|
+
* covered index search (see `outbox.ts`'s `readPendingOutbox`), cheap enough at a 1s interval to not need
|
|
17
|
+
* a short-circuit at all.
|
|
18
|
+
*/
|
|
19
|
+
export default defineNitroPlugin(() => {
|
|
20
|
+
// `makeTicker` takes `useDb` itself (a thunk), not its result — nothing here calls it, so the db is
|
|
21
|
+
// still untouched at plugin-init time; the interval callback is the first thing that ever does.
|
|
22
|
+
const tick = makeTicker(useDb, OUTBOX_MODULE)
|
|
23
|
+
|
|
24
|
+
// Registration only — nothing here resolves a pipeline or reads the collection registry at init.
|
|
25
|
+
for (const def of buildOutboxPipelines()) registerPipeline(def)
|
|
26
|
+
|
|
27
|
+
const timer = setInterval(() => {
|
|
28
|
+
tick().catch((error: unknown) => console.error('[kestrel] outbox worker tick failed', error))
|
|
29
|
+
}, POLL_INTERVAL_MS)
|
|
30
|
+
timer.unref?.()
|
|
31
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { allCollections, buildPipelineIndex, renderDashboard } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
import { PLUGIN_ORDER } from '../../../modules/plugin-order/plugin-order'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dev-only introspection dashboard: gathers live registry state, so it reflects the CONSUMER's own
|
|
6
|
+
* collections/pipelines/overrides — not a static analysis of Kestrel's own source. File-based
|
|
7
|
+
* `server/routes/**` modules are compiled into production builds unconditionally, so the
|
|
8
|
+
* `import.meta.dev` check below is the real guard: Nitro inlines it to `false` in production and
|
|
9
|
+
* dead-code-eliminates everything past it, leaving only the 404.
|
|
10
|
+
*/
|
|
11
|
+
export default defineEventHandler((event) => {
|
|
12
|
+
if (!import.meta.dev) {
|
|
13
|
+
throw createError({ statusCode: 404, statusMessage: 'Not found' })
|
|
14
|
+
}
|
|
15
|
+
const collections = allCollections().map((c) => ({
|
|
16
|
+
name: c.name,
|
|
17
|
+
mode: c.def.mode,
|
|
18
|
+
translatable: Boolean(c.def.translatable),
|
|
19
|
+
fieldCount: Object.keys(c.def.fields).length,
|
|
20
|
+
}))
|
|
21
|
+
const html = renderDashboard({
|
|
22
|
+
pipelines: buildPipelineIndex(),
|
|
23
|
+
pluginOrder: PLUGIN_ORDER.map((p) => ({ layer: p.layer, file: p.file, after: p.after })),
|
|
24
|
+
collections,
|
|
25
|
+
generatedAt: new Date().toISOString(),
|
|
26
|
+
})
|
|
27
|
+
setHeader(event, 'content-type', 'text/html; charset=utf-8')
|
|
28
|
+
return html
|
|
29
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import collections from '#kestrel/collections'
|
|
2
|
+
import schemaTables from '#kestrel/schema-tables'
|
|
3
|
+
import moduleManifests from '#kestrel/module-manifests'
|
|
4
|
+
import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
5
|
+
import type { OwnershipManifest } from '@michaelthielemann/kestrel-contracts'
|
|
6
|
+
import { buildContentManifest, describeOp, desiredFromCollections, isDestructive, opTable, orderManifests, planOps, sqlite, syncSchema, useDb } from '@michaelthielemann/kestrel-core'
|
|
7
|
+
import type { BuiltCollection, CollectionDef, Dialect, SchemaSnapshot, SyncDb } from '@michaelthielemann/kestrel-core'
|
|
8
|
+
import { resolveServerKestrel, serverRuntimeConfig } from '../../utils/server-config'
|
|
9
|
+
// The per-module counterpart to `db:migrate` (ADR-0012): runs the SAME diff/apply engine, but scoped to
|
|
10
|
+
// one ownership manifest's tables at a time, in a fixed order — `content` (the collection/base schema
|
|
11
|
+
// other modules are conceptually layered on) first, then every discovered module manifest
|
|
12
|
+
// (`module-order.ts`'s `MODULE_MIGRATION_ORDER`), then an `unmanaged` catch-all for any table no
|
|
13
|
+
// manifest yet claims (never silently skipped). Payload/trigger mirror `db:migrate` exactly — {module:
|
|
14
|
+
// "media"}=one module only, otherwise every module in order; {check:true}=dry-run, {force:true}=rebuilds,
|
|
15
|
+
// {drop:[names]}=drop tables. Same dev/prod trigger shape as `db:migrate` (see that file's header).
|
|
16
|
+
type ModulePayload = { module?: string; check?: boolean; force?: boolean; drop?: string[] }
|
|
17
|
+
type ModuleResult =
|
|
18
|
+
| { module: string; check: true; pending: string[]; destructive: string[] }
|
|
19
|
+
| { module: string; applied: string[]; skipped: string[] }
|
|
20
|
+
|
|
21
|
+
function runOne(
|
|
22
|
+
module: string,
|
|
23
|
+
tables: readonly string[],
|
|
24
|
+
client: SyncDb,
|
|
25
|
+
desired: SchemaSnapshot,
|
|
26
|
+
dialect: Dialect,
|
|
27
|
+
p: ModulePayload,
|
|
28
|
+
): ModuleResult {
|
|
29
|
+
if (p.check === true) {
|
|
30
|
+
const ops = planOps(client, desired, dialect).filter((op) => tables.includes(opTable(op)))
|
|
31
|
+
const destructive = ops.filter(isDestructive).map(describeOp)
|
|
32
|
+
return { module, check: true, pending: dialect.render(ops), destructive }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { applied, skipped } = syncSchema(client, desired, {
|
|
36
|
+
allowDestructive: p.force === true,
|
|
37
|
+
dropTables: Array.isArray(p.drop) ? p.drop : undefined,
|
|
38
|
+
tables,
|
|
39
|
+
}, dialect)
|
|
40
|
+
if (skipped.length) {
|
|
41
|
+
console.warn(`[kestrel] db:migrate-module(${module}) withheld ${skipped.length} destructive change(s) — opt in with {"force":true} (rebuilds) / {"drop":[…]} (drops):\n - ${skipped.map(describeOp).join('\n - ')}`)
|
|
42
|
+
}
|
|
43
|
+
console.info(`[kestrel] db:migrate-module(${module}) applied ${applied.length} statement(s)`)
|
|
44
|
+
return { module, applied, skipped: skipped.map(describeOp) }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default defineTask<ModuleResult[]>({
|
|
48
|
+
meta: {
|
|
49
|
+
name: 'db:migrate-module',
|
|
50
|
+
description: 'Per-module schema migration, ordered (content → media → publishing → unmanaged). {module:"name"}=one module only, otherwise every module in order. {check}/{force}/{drop} match db:migrate.',
|
|
51
|
+
},
|
|
52
|
+
run({ payload }) {
|
|
53
|
+
const p = (payload ?? {}) as ModulePayload
|
|
54
|
+
const desired = desiredFromCollections(collections as (CollectionDef | BuiltCollection)[], {
|
|
55
|
+
toggles: (serverRuntimeConfig()?.kestrel?.collections ?? resolveServerKestrel().collections) as Record<string, boolean>,
|
|
56
|
+
extraTables: schemaTables as AnySQLiteTable[],
|
|
57
|
+
})
|
|
58
|
+
const client = (useDb() as unknown as { $client: SyncDb }).$client
|
|
59
|
+
const dialect = sqlite
|
|
60
|
+
|
|
61
|
+
const manifests = orderManifests([buildContentManifest(), ...(moduleManifests as OwnershipManifest[])])
|
|
62
|
+
const claimed = new Set(manifests.flatMap((m) => m.tables))
|
|
63
|
+
// The `unmanaged` bucket's table list: every table any pending op touches that no manifest claims —
|
|
64
|
+
// computed up front so both `check` and apply modes scope to the SAME set (rather than an unfiltered
|
|
65
|
+
// rescan, which in apply mode would re-touch already-migrated tables and in check mode would re-list
|
|
66
|
+
// every module's pending ops a second time under `unmanaged`).
|
|
67
|
+
const unmanagedTables = [...new Set(planOps(client, desired, dialect).map(opTable))].filter((t) => !claimed.has(t))
|
|
68
|
+
|
|
69
|
+
if (p.module) {
|
|
70
|
+
const manifest = manifests.find((m) => m.module === p.module)
|
|
71
|
+
const tables = manifest ? manifest.tables : p.module === 'unmanaged' ? unmanagedTables : undefined
|
|
72
|
+
if (!tables) {
|
|
73
|
+
const known = [...manifests.map((m) => m.module), 'unmanaged'].join(', ')
|
|
74
|
+
throw new Error(`kestrel: db:migrate-module: unknown module "${p.module}" — known: ${known}`)
|
|
75
|
+
}
|
|
76
|
+
return { result: [runOne(p.module, tables, client, desired, dialect, p)] }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const results = manifests.map((m) => runOne(m.module, m.tables, client, desired, dialect, p))
|
|
80
|
+
results.push(runOne('unmanaged', unmanagedTables, client, desired, dialect, p))
|
|
81
|
+
return { result: results }
|
|
82
|
+
},
|
|
83
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { migrateRevisions, useDb, type MigrateRevisionsResult } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
|
|
3
|
+
// Thin Nitro wrapper (defineTask is a runtime global) around the core, independently-unit-tested
|
|
4
|
+
// `migrateRevisions` (db/revision-migration.ts) — same sync/task split as db:migrate (schema/sync.ts +
|
|
5
|
+
// tasks/db/migrate.ts). Dev: GET/POST http://localhost:3000/_nitro/tasks/db:migrate-revisions ; prod: call
|
|
6
|
+
// runTask('db:migrate-revisions', { payload }) from inside the process — the built node-server has no task
|
|
7
|
+
// CLI/endpoint. Payload: {force:true} required, or the run refuses.
|
|
8
|
+
export default defineTask<MigrateRevisionsResult[]>({
|
|
9
|
+
meta: {
|
|
10
|
+
name: 'db:migrate-revisions',
|
|
11
|
+
description: 'Seed revision 1 for every existing row with no revision history yet. Requires {"force":true}.',
|
|
12
|
+
},
|
|
13
|
+
run({ payload }) {
|
|
14
|
+
const p = (payload ?? {}) as { force?: boolean }
|
|
15
|
+
const result = migrateRevisions(useDb(), { force: p.force === true })
|
|
16
|
+
const totalSeeded = result.reduce((n, r) => n + r.seeded, 0)
|
|
17
|
+
console.info(`[kestrel] db:migrate-revisions seeded ${totalSeeded} revision(s) across ${result.length} collection(s)`)
|
|
18
|
+
return { result }
|
|
19
|
+
},
|
|
20
|
+
})
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import collections from '#kestrel/collections'
|
|
2
2
|
import schemaTables from '#kestrel/schema-tables'
|
|
3
3
|
import type { AnySQLiteTable } from 'drizzle-orm/sqlite-core'
|
|
4
|
-
import
|
|
5
|
-
import type { CollectionDef } from '
|
|
4
|
+
import { describeOp, desiredFromCollections, isDestructive, planOps, sqlite, syncSchema, useDb } from '@michaelthielemann/kestrel-core'
|
|
5
|
+
import type { BuiltCollection, CollectionDef, SyncDb } from '@michaelthielemann/kestrel-core'
|
|
6
6
|
import { resolveServerKestrel, serverRuntimeConfig } from '../../utils/server-config'
|
|
7
|
-
import { desiredFromCollections } from '../../schema/bootstrap'
|
|
8
|
-
import { planOps, syncSchema, isDestructive, describeOp, type SyncDb } from '../../schema/sync'
|
|
9
|
-
import { sqlite } from '../../schema/dialect'
|
|
10
|
-
|
|
11
7
|
// Explicit schema-migrate step for production (ADR-0002). Dev auto-syncs additively at boot
|
|
12
8
|
// (02.schema-sync); prod boot never auto-DDLs, so an operator runs this task. Triggering (Nuxt 4.4 /
|
|
13
9
|
// Nitro 2.13 — there is NO `nuxi task run`):
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import kestrelConfig from '../../../../kestrel.config'
|
|
2
|
-
import { resolveKestrel } from '
|
|
2
|
+
import { resolveKestrel, type ResolvedKestrel } from '@michaelthielemann/kestrel-core'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The Nitro runtimeConfig, or `undefined` for non-Nitro callers (node scripts, build) so they don't
|
|
6
6
|
* throw on `useRuntimeConfig()`. Server utils prefer the values the kestrel module put here (from the
|
|
7
7
|
* consumer's `kestrel: {}`), falling back to `resolveServerKestrel()` when it isn't populated.
|
|
8
8
|
*/
|
|
9
|
-
export function serverRuntimeConfig(): { kestrel?: Record<string, unknown>; public?: Record<string, unknown> } | undefined {
|
|
9
|
+
export function serverRuntimeConfig(): { kestrel?: Record<string, unknown>; media?: Record<string, unknown>; public?: Record<string, unknown> } | undefined {
|
|
10
10
|
return typeof useRuntimeConfig === 'function'
|
|
11
|
-
? (useRuntimeConfig() as { kestrel?: Record<string, unknown>; public?: Record<string, unknown> })
|
|
11
|
+
? (useRuntimeConfig() as { kestrel?: Record<string, unknown>; media?: Record<string, unknown>; public?: Record<string, unknown> })
|
|
12
12
|
: undefined
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -16,3 +16,66 @@ export function serverRuntimeConfig(): { kestrel?: Record<string, unknown>; publ
|
|
|
16
16
|
export function resolveServerKestrel(): ReturnType<typeof resolveKestrel> {
|
|
17
17
|
return resolveKestrel(kestrelConfig, process.env, process.cwd())
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The full `ResolvedKestrel` shape, merging every server util's own `runtimeConfig ?? resolveServerKestrel()`
|
|
22
|
+
* precedence into ONE call — the boot-time config-wiring plugin (`00.config.ts`) calls this once and
|
|
23
|
+
* pushes the result into the (eventually package-side) config provider, so package code never calls
|
|
24
|
+
* `useRuntimeConfig()` itself. Per-field precedence matches each field's existing reader exactly (the
|
|
25
|
+
* kestrel module writes a CLOSED set of fields into `runtimeConfig.kestrel`/`.public` — see
|
|
26
|
+
* `layers/core/modules/kestrel/index.ts`; anything it doesn't write there falls through to the
|
|
27
|
+
* `resolveServerKestrel()` base for every reader, unchanged from before this function existed).
|
|
28
|
+
*/
|
|
29
|
+
export function resolveServerKestrelConfig(): ResolvedKestrel {
|
|
30
|
+
const rc = serverRuntimeConfig()
|
|
31
|
+
const fallback = resolveServerKestrel()
|
|
32
|
+
const srv = (rc?.kestrel ?? {}) as Partial<Record<string, unknown>>
|
|
33
|
+
const pub = rc?.public as { locales?: string[]; primaryLocale?: string; prefixPrimary?: boolean } | undefined
|
|
34
|
+
// `runtimeConfig.media` is a TOP-LEVEL key (sibling to `.kestrel`/`.public`, not nested under `.kestrel`
|
|
35
|
+
// — see `layers/core/modules/kestrel/index.ts`'s `rc.media = {...}`), and it is the ONLY place the S3
|
|
36
|
+
// credentials + every Nitro `NUXT_MEDIA_*`/`NUXT_MEDIA_S3_*` env override (docs/guide/configuration.md) ever
|
|
37
|
+
// land — `resolveKestrel`'s own pure computation never sees them. Merged in wholesale (undefined ⇒ the
|
|
38
|
+
// module never ran ⇒ fall through to the pure fallback), mirroring `output`'s existing merge below.
|
|
39
|
+
const rcMedia = rc?.media as {
|
|
40
|
+
driver?: string
|
|
41
|
+
maxUploadBytes?: number
|
|
42
|
+
allowedMimes?: string
|
|
43
|
+
imagePolicy?: ResolvedKestrel['media']['imagePolicy']
|
|
44
|
+
local?: { dir?: string; baseUrl?: string }
|
|
45
|
+
s3?: { bucket?: string; region?: string; endpoint?: string; prefix?: string; publicBaseUrl?: string; accessKeyId?: string; secretAccessKey?: string; sessionToken?: string }
|
|
46
|
+
} | undefined
|
|
47
|
+
// The locale trio is populated together or not at all (see `locale.ts`'s original `resolved()`) — a
|
|
48
|
+
// partial `runtimeConfig.public` (e.g. `locales` present but `primaryLocale` absent) falls through to
|
|
49
|
+
// the fallback for all three rather than mixing sources mid-trio.
|
|
50
|
+
const locale = pub?.locales?.length && pub.primaryLocale
|
|
51
|
+
? { supportedLocales: pub.locales, primaryLocale: pub.primaryLocale, prefixPrimary: pub.prefixPrimary === true }
|
|
52
|
+
: { supportedLocales: fallback.supportedLocales, primaryLocale: fallback.primaryLocale, prefixPrimary: fallback.prefixPrimary }
|
|
53
|
+
return {
|
|
54
|
+
...fallback,
|
|
55
|
+
...locale,
|
|
56
|
+
dbPath: (srv.dbPath as string | undefined) ?? fallback.dbPath,
|
|
57
|
+
siteUrl: (srv.siteUrl as string | undefined) ?? fallback.siteUrl,
|
|
58
|
+
siteName: (srv.siteName as string | undefined) ?? fallback.siteName,
|
|
59
|
+
siteDescription: (srv.siteDescription as string | undefined) ?? fallback.siteDescription,
|
|
60
|
+
media: rcMedia ? {
|
|
61
|
+
dir: rcMedia.local?.dir ?? fallback.media.dir,
|
|
62
|
+
baseUrl: rcMedia.local?.baseUrl ?? fallback.media.baseUrl,
|
|
63
|
+
driver: (rcMedia.driver as 'local' | 's3' | undefined) ?? fallback.media.driver,
|
|
64
|
+
maxUploadBytes: rcMedia.maxUploadBytes ?? fallback.media.maxUploadBytes,
|
|
65
|
+
allowedMimes: rcMedia.allowedMimes ?? fallback.media.allowedMimes,
|
|
66
|
+
imagePolicy: rcMedia.imagePolicy ?? fallback.media.imagePolicy,
|
|
67
|
+
s3: { ...fallback.media.s3, ...rcMedia.s3 },
|
|
68
|
+
} : fallback.media,
|
|
69
|
+
collections: (srv.collections as ResolvedKestrel['collections'] | undefined) ?? fallback.collections,
|
|
70
|
+
aiDisclosure: (srv.aiDisclosure as ResolvedKestrel['aiDisclosure'] | undefined) ?? fallback.aiDisclosure,
|
|
71
|
+
seo: (srv.seo as ResolvedKestrel['seo'] | undefined) ?? fallback.seo,
|
|
72
|
+
delivery: (srv.delivery as ResolvedKestrel['delivery'] | undefined) ?? fallback.delivery,
|
|
73
|
+
deliveryExempt: (srv.deliveryExempt as string[] | undefined) ?? fallback.deliveryExempt,
|
|
74
|
+
output: (srv.output as ResolvedKestrel['output'] | undefined) ?? fallback.output,
|
|
75
|
+
// `revisions` is NOT written into `runtimeConfig.kestrel` by the module today (pre-existing —
|
|
76
|
+
// unrelated to this seam), so this reads as `fallback.revisions` in every real boot right now; kept
|
|
77
|
+
// symmetric with every other field rather than special-cased, so a future module fix (writing
|
|
78
|
+
// `srv.revisions`) is picked up here for free.
|
|
79
|
+
revisions: (srv.revisions as ResolvedKestrel['revisions'] | undefined) ?? fallback.revisions,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
import { buildBlocksPipelines } from '@michaelthielemann/kestrel-fields'
|
|
3
|
+
|
|
4
|
+
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
5
|
+
export default defineNitroPlugin(() => {
|
|
6
|
+
for (const def of buildBlocksPipelines()) registerPipeline(def)
|
|
7
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { registerPopulator } from '
|
|
2
|
-
import { buildFieldTreePopulator } from '
|
|
1
|
+
import { registerPopulator } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
import { buildFieldTreePopulator } from '@michaelthielemann/kestrel-fields'
|
|
3
3
|
|
|
4
4
|
// The single global row populator: walks a record's fields + block tree and dispatches each field to its
|
|
5
5
|
// per-type populator (media / link / richtext / relation), each registered by its owning layer. It recurses
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { useBlurUp } from '../composables/useBlurUp'
|
|
3
3
|
import { useMediaVariant } from '../composables/useMediaVariant'
|
|
4
4
|
import { aiSourceTypeLabel } from '../utils/ai-disclosure'
|
|
5
|
-
import type { ResolvedMedia } from '
|
|
6
|
-
import type { VariantFit, VariantFormat } from '
|
|
5
|
+
import type { ResolvedMedia } from '@michaelthielemann/kestrel-media'
|
|
6
|
+
import type { VariantFit, VariantFormat } from '@michaelthielemann/kestrel-core'
|
|
7
7
|
|
|
8
8
|
// The engine's responsive-image component: it renders a <picture> from the media's generated derivatives
|
|
9
9
|
// AND its usage declares (to the generate-time scan) which sizes/formats are actually needed — "not in the
|
|
@@ -21,7 +21,7 @@ const props = defineProps<{
|
|
|
21
21
|
/** Opt into a structurally-present but UNSTYLED EU AI Act disclosure badge (`.kestrel-img__ai-badge`),
|
|
22
22
|
* rendered only when `media.aiDisclosure` is set. Off by default: Kestrel must never publish a claim
|
|
23
23
|
* the consumer did not ask for, in a place/language/style it cannot know. The alternative escape hatch
|
|
24
|
-
* is to read `media.aiDisclosure` and render your own element — see docs/
|
|
24
|
+
* is to read `media.aiDisclosure` and render your own element — see docs/guide/ai-disclosure.md. */
|
|
25
25
|
aiBadge?: boolean
|
|
26
26
|
}>()
|
|
27
27
|
|
|
@@ -62,8 +62,8 @@ async function onSaveMeta(alt: string, ai?: { aiSourceType: string | null; aiNot
|
|
|
62
62
|
// so it rides alongside as top-level keys — and only when the viewer actually offered it, so a save
|
|
63
63
|
// from a consumer with the feature off never clears a recorded disclosure. The precondition header
|
|
64
64
|
// lets the server 409 instead of silently losing a concurrent edit from another open viewer tab.
|
|
65
|
-
await $fetch(`/api/media/${f.id}`, {
|
|
66
|
-
method: '
|
|
65
|
+
await $fetch(`/api/media/updateAsset/${f.id}`, {
|
|
66
|
+
method: 'POST',
|
|
67
67
|
body: { translations: { [primary]: { alt } }, ...(ai ?? {}) },
|
|
68
68
|
...(f.updatedAt ? { headers: { 'x-kestrel-if-unmodified-since': String(new Date(f.updatedAt).getTime()) } } : {}),
|
|
69
69
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, watch, nextTick } from 'vue'
|
|
3
3
|
import type { FieldComponentProps } from '../../../../ui/app/utils/field-component'
|
|
4
|
-
import type { FieldOf } from '
|
|
4
|
+
import type { FieldOf } from '@michaelthielemann/kestrel-core'
|
|
5
5
|
import { reorder } from '../../../../ui/app/utils/reorder'
|
|
6
6
|
import { commonFolder } from '../../utils/library'
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { reactive, toValue } from 'vue'
|
|
2
2
|
import type { MaybeRefOrGetter } from 'vue'
|
|
3
|
-
import type { ResolvedMedia } from '
|
|
3
|
+
import type { ResolvedMedia } from '@michaelthielemann/kestrel-media'
|
|
4
4
|
|
|
5
5
|
// The client-facing resolved-media shape (`GET /api/media/resolve`) is the server resolver's own
|
|
6
6
|
// return type — re-exported so consumers keep importing it from here, single-sourced so it can't drift.
|
|
@@ -35,7 +35,7 @@ export function useMediaUpload(cb: UploadCallbacks = {}) {
|
|
|
35
35
|
fd.append('filename', opts.filename ?? item.filename)
|
|
36
36
|
if (opts.overwrite) fd.append('overwrite', 'true')
|
|
37
37
|
try {
|
|
38
|
-
await $fetch('/api/media', { method: 'POST', body: fd })
|
|
38
|
+
await $fetch('/api/media/upload', { method: 'POST', body: fd })
|
|
39
39
|
item.status = 'done'
|
|
40
40
|
} catch (e) {
|
|
41
41
|
type ConflictData = { suggestion?: string; existingId?: number }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
|
|
2
|
-
import type { ResolvedMedia } from '
|
|
3
|
-
import type { ResolvedVariant } from '
|
|
2
|
+
import type { ResolvedMedia } from '@michaelthielemann/kestrel-media'
|
|
3
|
+
import type { ResolvedVariant } from '@michaelthielemann/kestrel-core'
|
|
4
4
|
import { buildPictureModel, requestVariantSpecs, type VariantRequest, type PictureModel } from '../utils/picture-model'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { LibraryItem } from './library'
|
|
2
|
-
import type { OpItem, DeleteReport } from '
|
|
3
|
-
import type { Conflict, RelocationReport } from '../../server/utils/relocate-ops'
|
|
4
|
-
import type { MediaUsage } from '../../server/utils/usages'
|
|
2
|
+
import type { OpItem, DeleteReport, Conflict, RelocationReport, MediaUsage } from '@michaelthielemann/kestrel-media'
|
|
5
3
|
|
|
6
4
|
// Single-source the HTTP wire shapes from the server utils that own them — one definition, no drift.
|
|
7
5
|
export type { OpItem, DeleteReport, Conflict, RelocationReport, MediaUsage }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ResolvedMedia } from '
|
|
2
|
-
import type { VariantFit, VariantFormat, ResolvedVariant } from '
|
|
1
|
+
import type { ResolvedMedia } from '@michaelthielemann/kestrel-media'
|
|
2
|
+
import type { VariantFit, VariantFormat, ResolvedVariant } from '@michaelthielemann/kestrel-core'
|
|
3
3
|
import { variantName } from './variant-name'
|
|
4
4
|
|
|
5
5
|
/** What a `KestrelImg` / `useMediaVariant` call declares it needs. One call may declare MULTIPLE sizes and
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { H3Event } from 'h3'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { clientIp } from '@michaelthielemann/kestrel-auth'
|
|
3
|
+
import { allowlistMode, ipAllowed, parseAllowlist } from '@michaelthielemann/kestrel-access'
|
|
4
|
+
import { deriveOnDemand, variantKeyFromPath, useStorageDriver, mediaRuntimeConfig, useMediaDb } from '@michaelthielemann/kestrel-media'
|
|
5
|
+
import { DEFAULT_IMAGE_POLICY } from '@michaelthielemann/kestrel-core'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Whether the IP allow-list would deny this request, checked directly rather than relied on by ordering:
|
|
@@ -21,13 +22,13 @@ export default defineEventHandler(async (event) => {
|
|
|
21
22
|
if (!import.meta.dev || event.method !== 'GET') return
|
|
22
23
|
if (deniedByAllowlist(event)) return
|
|
23
24
|
const cfg = mediaRuntimeConfig()
|
|
24
|
-
const key = variantKeyFromPath(event.path, cfg.
|
|
25
|
+
const key = variantKeyFromPath(event.path, cfg.baseUrl)
|
|
25
26
|
if (key == null) return
|
|
26
27
|
const driver = useStorageDriver()
|
|
27
28
|
if (await driver.exists?.(key)) return
|
|
28
29
|
// Dev is the diagnosis environment, so surface a real derive failure (missing original, sharp reject)
|
|
29
30
|
// instead of silently 404ing — a broken preview <img> with no log violates the fail-loud convention.
|
|
30
|
-
const result = await deriveOnDemand(
|
|
31
|
+
const result = await deriveOnDemand(useMediaDb().db, driver, cfg.imagePolicy ?? DEFAULT_IMAGE_POLICY, key).catch((error) => {
|
|
31
32
|
console.warn(`[kestrel] on-demand variant derive failed for ${key}:`, (error as Error)?.message ?? error)
|
|
32
33
|
return null
|
|
33
34
|
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { registerPipeline } from '@michaelthielemann/kestrel-core'
|
|
2
|
+
import { buildMediaPipelines } from '@michaelthielemann/kestrel-media'
|
|
3
|
+
|
|
4
|
+
// Registration only — nothing resolves a pipeline here; the registry is read on the first request.
|
|
5
|
+
export default defineNitroPlugin(() => {
|
|
6
|
+
for (const def of buildMediaPipelines()) registerPipeline(def)
|
|
7
|
+
})
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { eq, getTableColumns } from 'drizzle-orm'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { registerFieldPopulator } from '@michaelthielemann/kestrel-core'
|
|
3
|
+
import {
|
|
4
|
+
media as mediaTable,
|
|
5
|
+
buildMediaFieldPopulator,
|
|
6
|
+
resolveMedia,
|
|
7
|
+
useStorageDriver,
|
|
8
|
+
mediaCollectionEnabled,
|
|
9
|
+
useMediaDb,
|
|
10
|
+
} from '@michaelthielemann/kestrel-media'
|
|
8
11
|
|
|
9
12
|
export default defineNitroPlugin(() => {
|
|
10
13
|
// A consumer who turned the built-in off has no `media` table, so a row that still carries a media id
|
|
@@ -13,7 +16,7 @@ export default defineNitroPlugin(() => {
|
|
|
13
16
|
const cols = getTableColumns(mediaTable) as Record<string, never>
|
|
14
17
|
registerFieldPopulator('media', buildMediaFieldPopulator((id, locale) => {
|
|
15
18
|
const driver = useStorageDriver()
|
|
16
|
-
const row =
|
|
19
|
+
const row = useMediaDb().db.select().from(mediaTable).where(eq(cols.id, id)).get()
|
|
17
20
|
return row ? resolveMedia(row as never, locale, (k) => driver.publicUrl(k)) : null
|
|
18
21
|
}))
|
|
19
22
|
})
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isRendererContext } from '@michaelthielemann/kestrel-access'
|
|
2
|
+
import { recordVariants, saveDiscoveredVariants, useMediaDb } from '@michaelthielemann/kestrel-media'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Auto-discovery capture. `KestrelImg`/`useMediaVariant` stash their concrete specs on the render's
|
|
5
6
|
* `event.context.kestrelVariants` during SSR. Renders count when they are either a PUBLISH render (the
|
|
6
|
-
* incremental publisher
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* incremental publisher runs it under `runAsRenderer`, see `render-live.ts`) OR a build-time PRERENDER
|
|
8
|
+
* render (the classic `nuxt generate` topology) — so live preview / stray runtime renders never pollute the
|
|
9
|
+
* scan. For a publish, `publishFull` reconciles the drained accumulator once at the end; the generate
|
|
10
|
+
* topology has no such orchestrator, so each prerendered route reconciles inline WITHOUT draining and the
|
|
11
|
+
* growing accumulator converges to the full used set by the last route.
|
|
12
|
+
*
|
|
13
|
+
* Gating uses the unforgeable AsyncLocalStorage signal `isRendererContext()` (the same one
|
|
14
|
+
* `delivery-live`'s catch-all uses), not the spoofable `x-kestrel-publish` header.
|
|
11
15
|
*
|
|
12
16
|
* NOTE: relies on `beforeResponse` firing for the publisher's `localFetch` renders / the prerenderer, and on
|
|
13
17
|
* the app→server `event.context` bridge — verified by a real publish/generate, not the headless unit suite.
|
|
@@ -15,9 +19,9 @@ import { recordVariants, saveDiscoveredVariants } from '../utils/variant-capture
|
|
|
15
19
|
export default defineNitroPlugin((nitroApp) => {
|
|
16
20
|
nitroApp.hooks.hook('beforeResponse', (event) => {
|
|
17
21
|
const isPrerender = import.meta.prerender === true
|
|
18
|
-
if (
|
|
22
|
+
if (!isRendererContext() && !isPrerender) return
|
|
19
23
|
const specs = (event.context as { kestrelVariants?: unknown[] }).kestrelVariants
|
|
20
24
|
if (Array.isArray(specs) && specs.length) recordVariants(specs as never)
|
|
21
|
-
if (isPrerender) saveDiscoveredVariants(
|
|
25
|
+
if (isPrerender) saveDiscoveredVariants(useMediaDb().db, { clear: false })
|
|
22
26
|
})
|
|
23
27
|
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { runBackfill } from '
|
|
2
|
-
import {
|
|
3
|
-
import { DEFAULT_IMAGE_POLICY } from '../../../../core/server/utils/kestrel-config'
|
|
1
|
+
import { runBackfill, useStorageDriver, mediaRuntimeConfig, useMediaDb } from '@michaelthielemann/kestrel-media'
|
|
2
|
+
import { DEFAULT_IMAGE_POLICY } from '@michaelthielemann/kestrel-core'
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Reconcile every media row's derivatives to the active variant registry: generate the missing sizes/formats
|
|
@@ -12,7 +11,7 @@ export default defineTask({
|
|
|
12
11
|
meta: { name: 'media:backfill', description: 'Generate missing image variants + prune deregistered ones; {check:true}=dry-run' },
|
|
13
12
|
async run({ payload }) {
|
|
14
13
|
const policy = mediaRuntimeConfig().imagePolicy ?? DEFAULT_IMAGE_POLICY
|
|
15
|
-
const result = await runBackfill(
|
|
14
|
+
const result = await runBackfill(useMediaDb().db, useStorageDriver(), policy, { check: !!(payload as { check?: boolean })?.check })
|
|
16
15
|
return { result }
|
|
17
16
|
},
|
|
18
17
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, useSlots } from 'vue'
|
|
3
|
-
import type { LinkValue } from '
|
|
3
|
+
import type { LinkValue } from '@michaelthielemann/kestrel-core'
|
|
4
4
|
import { linkToHref, linkLabel } from '../utils/link-href'
|
|
5
5
|
|
|
6
6
|
// Generic renderer for the `link` field type, for consumer projects + blocks. Renders an <a> for any
|
|
@@ -99,7 +99,7 @@ const previewNotice = computed(() => {
|
|
|
99
99
|
const previewRequested = route.query[PREVIEW_QUERY] === '1'
|
|
100
100
|
const { data: previewSession } = previewRequested
|
|
101
101
|
? await useAsyncData('kestrel-preview-session', () =>
|
|
102
|
-
requestFetch('/api/
|
|
102
|
+
requestFetch<{ authenticated: boolean }>('/api/session').catch(() => ({ authenticated: false })))
|
|
103
103
|
: { data: ref<{ authenticated: boolean } | null>(null) }
|
|
104
104
|
const previewActive = computed(() => previewRequested && previewSession.value?.authenticated === true)
|
|
105
105
|
|