Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Vrij en open source onder de EUPL-1.2-licentie.

**Ondersteuning:** Voor ondersteuning, neem contact op via support@conduction.nl.
]]></description>
<version>0.3.3</version>
<version>0.3.5</version>
<licence>agpl</licence>
<author mail="info@conduction.nl" homepage="https://www.conduction.nl/">Conduction</author>
<namespace>OpenBuilt</namespace>
Expand Down
22 changes: 16 additions & 6 deletions lib/Controller/ApplicationVersionsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]
);

Expand All @@ -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) {
Expand Down
34 changes: 23 additions & 11 deletions lib/Repair/MigrateToVersionedModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
/**
* Schema slug introduced by the versioned-app model (post-migration).
*/
private const VERSIONED_SCHEMA = 'applicationVersion';

Check failure on line 56 in lib/Repair/MigrateToVersionedModel.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Constant OCA\OpenBuilt\Repair\MigrateToVersionedModel::VERSIONED_SCHEMA is unused.

/**
* Constructor.
Expand Down Expand Up @@ -147,25 +147,37 @@
*/
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;
}
}
Expand Down
17 changes: 15 additions & 2 deletions src/components/applicationDetail/ApplicationDetailHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand Down
33 changes: 22 additions & 11 deletions src/store/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})

/**
Expand Down Expand Up @@ -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
}
Expand Down
14 changes: 13 additions & 1 deletion src/views/SchemaDesigner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
Expand Down
Loading