diff --git a/api/package.json b/api/package.json index 7052ebc3..fc4f08c3 100644 --- a/api/package.json +++ b/api/package.json @@ -23,7 +23,7 @@ "dependencies": { "@data-fair/lib-express": "^1.22.5", "@data-fair/lib-node": "^2.12.1", - "@data-fair/lib-node-registry": "^0.5.0", + "@data-fair/lib-node-registry": "^0.6.0", "@data-fair/lib-utils": "^1.10.1", "@data-fair/processings-shared": "*", "ajv": "^8.17.1", diff --git a/docker-compose.yml b/docker-compose.yml index e084899d..36e598a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -112,7 +112,7 @@ services: registry: profiles: - dev - image: ghcr.io/data-fair/registry:feat-npm-noarch + image: ghcr.io/data-fair/registry:main network_mode: host depends_on: - mongo diff --git a/package-lock.json b/package-lock.json index 8375559a..a58324fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,7 @@ "dependencies": { "@data-fair/lib-express": "^1.22.5", "@data-fair/lib-node": "^2.12.1", - "@data-fair/lib-node-registry": "^0.5.0", + "@data-fair/lib-node-registry": "^0.6.0", "@data-fair/lib-utils": "^1.10.1", "@data-fair/processings-shared": "*", "ajv": "^8.17.1", @@ -596,9 +596,9 @@ } }, "node_modules/@data-fair/lib-node-registry": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@data-fair/lib-node-registry/-/lib-node-registry-0.5.0.tgz", - "integrity": "sha512-WFh2BFrzFiRFCzYO/x41Ax7RUop8Gs9DVcOr2wHtC7zsZiVpre13PsJvNwkL0L3pdiwtGSUuaHFv3C6XNfBwdQ==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@data-fair/lib-node-registry/-/lib-node-registry-0.6.0.tgz", + "integrity": "sha512-TT3IFJwOwksl6rGi1JIz7tBM0M3NWyDcHR3orsAtF2UGn64cvQti829lfuhTAmGOhWA/nWjWL4kxDYliOKsJNA==", "license": "MIT", "dependencies": { "@types/resolve-path": "^1.4.3", @@ -12221,7 +12221,7 @@ "worker": { "dependencies": { "@data-fair/lib-node": "^2.12.1", - "@data-fair/lib-node-registry": "^0.5.0", + "@data-fair/lib-node-registry": "^0.6.0", "@data-fair/processings-shared": "*", "@mdi/js": "^7.4.47", "axios": "^1.8.3", diff --git a/ui/src/pages/processings/[id]/index.vue b/ui/src/pages/processings/[id]/index.vue index cc9a09d6..dcbc4708 100644 --- a/ui/src/pages/processings/[id]/index.vue +++ b/ui/src/pages/processings/[id]/index.vue @@ -54,6 +54,10 @@ {{ timezoneLabel(node.data.timeZone) }} + import type { Processing } from '#api/types' -import type { RegistryArtefact } from '~/composables/use-plugin-fetch' -import { ofetch } from 'ofetch' import cronstrue from 'cronstrue' import 'cronstrue/locales/en' import 'cronstrue/locales/fr' +import type { RegistryArtefact } from '~/composables/use-plugin-fetch' + +import useFetch from '@data-fair/lib-vue/fetch.js' import { resolvedSchema as contractProcessing } from '#api/types/processing/index.ts' import timeZones from 'timezones.json' import Vjsf, { type Options as VjsfOptions } from '@koumoul/vjsf' @@ -111,9 +116,6 @@ const valid = ref(false) const edited = ref(false) const editProcessing: Ref = ref(null) const processing: Ref = ref(null) -const plugin: Ref = ref(null) -const pluginBroken = ref(false) -const configSchema: Ref | null> = ref(null) const runs: Ref> = ref([]) /* @@ -128,51 +130,58 @@ onMounted(async () => { }, { text: processing.value?.title || '' }]) - await fetchPlugin() }) async function fetchProcessing () { processing.value = await $fetch(`/processings/${processingId}`) if (processing.value) editProcessing.value = { ...processing.value } } -async function fetchPlugin () { - pluginBroken.value = false - if (!processing.value?.plugin) return - // Display metadata comes from registry — `processing.plugin` is the artefact id. - // The config schema is read out of the cached package.json by the - // processings API — registry doesn't know or care what's inside packages. - // - // Registry returns 404 when the plugin has been deleted, 403 when the - // owner has lost access. We collapse both into pluginBroken=true and - // render a banner; the config-schema fetch's 404 (no schema for this - // plugin) is a separate, narrower state that does NOT trigger the banner. - // - // Hit the registry with the bare `ofetch` — not the app's `$fetch`, whose - // `/processings/api/v1` baseURL would rewrite this to - // `/processings/api/v1/registry/...` and 404. Registry is mounted at - // `/registry` of the current domain (same convention as use-plugin-fetch). - const artefactResult = await ofetch( - `/registry/api/v1/artefacts/${encodeURIComponent(processing.value.plugin)}` - ).then( - (data) => ({ ok: true as const, data }), - (err) => { - const status = err?.statusCode ?? err?.status - if (status === 404 || status === 403) return { ok: false as const } - throw err - } - ) - if (!artefactResult.ok) { - pluginBroken.value = true - return - } - plugin.value = artefactResult.data - configSchema.value = await $fetch>( - `/processings/${processingId}/plugin-config-schema` - ).catch(err => { - if (err?.statusCode === 404 || err?.status === 404) return null - throw err - }) -} + +/* + Plugin metadata + config schema fetches. + + Both kick off in parallel as soon as `processing` resolves — useFetch + watches its reactive URL and waits while it's null. + + - pluginBroken: 404 (deleted) or 403 (access revoked) on the artefact. + Surfaces the banner and suppresses the form. + - configSchema null: legitimate "this plugin ships no schema" — distinct + from a registry error and does NOT trigger the banner. + + Same-domain assumption: registry is mounted at `/registry` of the current + domain, so an absolute path bypasses `$fetch`'s `/processings/api/v1` + baseURL (which would rewrite `/registry/...` to a 404). +*/ +const pluginFetch = useFetch( + computed(() => processing.value?.plugin + ? `/registry/api/v1/artefacts/${encodeURIComponent(processing.value.plugin)}` + : null), + { notifError: false } +) +const plugin = computed(() => pluginFetch.data.value) +const pluginBroken = computed(() => { + const status = pluginFetch.error.value?.statusCode + return status === 404 || status === 403 +}) + +const configSchemaFetch = useFetch>( + computed(() => processing.value?.plugin + ? `${$apiPath}/processings/${processingId}/plugin-config-schema` + : null), + { notifError: false } +) +const configSchema = computed | null>(() => { + const err = configSchemaFetch.error.value + // The endpoint returns 404 to mean "this plugin doesn't ship a schema"; we + // collapse that to null so the page degrades gracefully (no form, no + // banner). Other errors are left to surface through the data binding. + if (err && (err.statusCode === 404 || err.status === 404)) return null + return configSchemaFetch.data.value +}) + +const pluginFetchPending = computed(() => + pluginFetch.loading.value || configSchemaFetch.loading.value +) /* Permissions diff --git a/worker/package.json b/worker/package.json index c72cf119..aaefe973 100644 --- a/worker/package.json +++ b/worker/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@data-fair/lib-node": "^2.12.1", - "@data-fair/lib-node-registry": "^0.5.0", + "@data-fair/lib-node-registry": "^0.6.0", "@data-fair/processings-shared": "*", "@mdi/js": "^7.4.47", "axios": "^1.8.3",