diff --git a/appinfo/info.xml b/appinfo/info.xml index ccc72ed9..494958f8 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -22,7 +22,7 @@ Vrij en open source onder de EUPL-1.2-licentie. **Ondersteuning:** Voor ondersteuning, neem contact op via support@conduction.nl. ]]> - 0.3.3 + 0.3.5 agpl Conduction OpenBuilt diff --git a/lib/Controller/ApplicationVersionsController.php b/lib/Controller/ApplicationVersionsController.php index 6e1f75b5..30e41d10 100644 --- a/lib/Controller/ApplicationVersionsController.php +++ b/lib/Controller/ApplicationVersionsController.php @@ -137,13 +137,17 @@ public function index(string $slug): JSONResponse _multitenancy: false )->getId(); + // OR's searchObjects doesn't reliably filter by relation-string equality + // on the `application` field (the value is stored both inline AND in + // @self.relations, and the matcher matches neither shape consistently). + // Fetch every ApplicationVersion row and filter client-side by the + // parent Application UUID. Cheap — we expect ~3 rows per app. $rows = $this->objectService->searchObjects( query: [ - '@self' => [ + '@self' => [ 'register' => $registerId, 'schema' => $schemaId, ], - 'application' => $applicationUuid, ] ); @@ -152,10 +156,16 @@ public function index(string $slug): JSONResponse $rowsList = $rows; } - $normalised = array_map( - fn ($row): array => $this->normaliseObject(object: $row), - $rowsList - ); + $normalised = []; + foreach ($rowsList as $row) { + $normalisedRow = $this->normaliseObject(object: $row); + $rowAppUuid = (string) ($normalisedRow['application'] ?? ''); + if ($rowAppUuid !== $applicationUuid) { + continue; + } + + $normalised[] = $normalisedRow; + } return new JSONResponse(data: $normalised, statusCode: Http::STATUS_OK); } catch (Throwable $e) { diff --git a/lib/Repair/MigrateToVersionedModel.php b/lib/Repair/MigrateToVersionedModel.php index bfc2642f..ac017c65 100644 --- a/lib/Repair/MigrateToVersionedModel.php +++ b/lib/Repair/MigrateToVersionedModel.php @@ -147,25 +147,37 @@ public function run(IOutput $output): void */ private function isAlreadyVersioned(): bool { - // Test 1 — does the versioned schema exist? + // The previous check was "does the versioned schema exist?" — but + // InitializeSettings imports the schema register BEFORE this step + // runs, so the `applicationVersion` schema is always present by + // the time we reach here. That made the short-circuit always fire + // and the migration always skip (issue #69). + // + // Correct check: examine the Application rows themselves. If any + // row carries a legacy top-level `manifest` / `version` / `status` + // / `currentVersion` field (the pre-spec-C shape), the install + // still has pre-migration data. If no Application rows exist OR + // all surviving rows already match the post-C shape, we're + // versioned. try { - $this->schemaMapper->find(self::VERSIONED_SCHEMA, _multitenancy: false); - return true; + $applications = $this->enumerateApplications(); } catch (Throwable) { - // Not found — fall through to Test 2. + // No openbuilt register or no Application schema — fresh + // install. Nothing to migrate. + return true; } - // Test 2 — do any Application rows carry the legacy `currentVersion` field? - try { - $applications = $this->enumerateApplications(); - } catch (Throwable) { - // If the register or schema do not exist yet, we are on a - // fresh install — no pre-migration data to migrate. + if ($applications === []) { return true; } foreach ($applications as $row) { - if (array_key_exists('currentVersion', $row) === true) { + // Pre-C shape keys we need to migrate away from. + if (array_key_exists('currentVersion', $row) === true + || array_key_exists('manifest', $row) === true + || array_key_exists('version', $row) === true + || array_key_exists('status', $row) === true + ) { return false; } } diff --git a/src/components/applicationDetail/ApplicationDetailHeader.vue b/src/components/applicationDetail/ApplicationDetailHeader.vue index 9547825e..61c263c1 100644 --- a/src/components/applicationDetail/ApplicationDetailHeader.vue +++ b/src/components/applicationDetail/ApplicationDetailHeader.vue @@ -523,7 +523,14 @@ export default { try { const url = generateUrl(`/apps/openregister/api/objects/openbuilt/application/${encodeURIComponent(uuid)}`) const { data } = await axios.get(url) - this.application = (data && (data['@self'] ? { ...data, ...(data['@self'] || {}) } : data)) || null + // Keep user-visible fields from `data` and stash OR's internal + // metadata block separately. The previous merge spread `@self` + // OVER `data`, which overwrote `data.description` (the real + // app description) with `@self.description` (an internal OR + // metadata field carrying e.g. node IDs). See issue #73. + this.application = data + ? { ...data, '@self': data['@self'] || {} } + : null this.loadVersions() } catch (e) { this.error = e instanceof Error ? e : new Error(String(e)) @@ -544,7 +551,13 @@ export default { const list = Array.isArray(data) ? data : (data && Array.isArray(data.results) ? data.results : []) - this.versions = list + // OR's object-list shape carries the UUID in `id` (mirrored + // from `@self.id`) but the chain/pill logic reads `uuid`. + // Normalise once so every consumer sees `v.uuid`. + this.versions = list.map((v) => ({ + ...v, + uuid: v.uuid || v.id || (v['@self'] && v['@self'].id) || null, + })) const versionSlugFromRoute = (this.$route && this.$route.query && this.$route.query._version) || '' const match = versionSlugFromRoute diff --git a/src/store/schemas.js b/src/store/schemas.js index 59f36ef1..841773f1 100644 --- a/src/store/schemas.js +++ b/src/store/schemas.js @@ -27,12 +27,21 @@ import { createObjectStore } from '@conduction/nextcloud-vue' const STORE_ID = 'openbuilt-schemas' -// Runtime schema CRUD lives under /api/registers (chain #3) — not the -// /api/objects surface that backs ordinary OR object CRUD. -const RUNTIME_SCHEMA_BASE_URL = '/apps/openregister/api/registers' +// We point at OR's existing schemas CRUD (`/api/schemas/{id}`) because +// the proposed runtime per-register schema endpoint (`/api/registers/{r}/ +// schemas/{slug}`) hasn't shipped on the OR floor yet (spec C chain #3). +// +// `useObjectStore._buildUrl` concatenates `${baseUrl}/${register}/${schema} +// /${id}`. We satisfy that template by splitting `/apps/openregister`, +// `api`, and `schemas` across the baseUrl + register + schema slots, then +// fetching by slug in the id position. The store's `register` slot is no +// longer carrying the OR register name; we keep the per-version register +// info on the type config under `slugs.registerSlug` for callers that +// need to filter the collection. +const SCHEMA_API_BASE_URL = '/apps/openregister' const useSchemasStoreRaw = createObjectStore(STORE_ID, { - baseUrl: RUNTIME_SCHEMA_BASE_URL, + baseUrl: SCHEMA_API_BASE_URL, }) /** @@ -77,14 +86,16 @@ export function useSchemasStore(appSlug, versionSlug) { const register = registerSlugForApp(appSlug, versionSlug) const type = 'schema' - // Register the object type once per (store, type, register) tuple. The base - // store records the register/schema pair under the type slug; here - // we use literal `schema` for both because chain #3's endpoint shape - // expects `/registers/{register}/schemas[/{slug}]`. - // Re-register when register changes (different version selected). + // Slot the URL segments so `_buildUrl` produces + // `/apps/openregister/api/schemas[/{slug}]` + // (OR's existing schemas CRUD). + // We keep the per-version OR register name on `slugs.registerSlug` + // so the consumer can filter the global schema collection client-side + // to only the schemas owned by the selected version's register. if (!store.objectTypeRegistry[type] - || store.objectTypeRegistry[type].register !== register) { - store.registerObjectType(type, 'schemas', register) + || !store.objectTypeRegistry[type].slugs + || store.objectTypeRegistry[type].slugs.registerSlug !== register) { + store.registerObjectType(type, 'schemas', 'api', { registerSlug: register }) } return store } diff --git a/src/views/SchemaDesigner.vue b/src/views/SchemaDesigner.vue index f727b6df..58654533 100644 --- a/src/views/SchemaDesigner.vue +++ b/src/views/SchemaDesigner.vue @@ -329,7 +329,19 @@ export default { this.loadingList = true try { const results = await this.store.fetchCollection(SCHEMA_TYPE) - this.schemas = Array.isArray(results) ? results : [] + const all = Array.isArray(results) ? results : [] + // OR's schemas endpoint returns every schema in the + // organisation. Filter to the namespaced subset that + // belongs to this app+version register so the designer + // only shows the user's relevant schemas. Per the wizard + // (issue #71) seed slugs are `{appSlug}-{versionSlug}-X`. + const prefix = this.versionSlug + ? `${this.appSlug}-${this.versionSlug}-` + : `${this.appSlug}-` + this.schemas = all.filter((s) => { + const slug = s.slug || (s['@self'] && s['@self'].slug) || '' + return typeof slug === 'string' && slug.startsWith(prefix) + }) const err = this.store.errors[SCHEMA_TYPE] if (err) { showError(this.t('openbuilt', 'Failed to load schemas: {error}', { error: err }))