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
160 changes: 160 additions & 0 deletions openspec/changes/softwarecatalog-store-migration/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Design — SoftwareCatalog store migration

## Architectural decision: keep the four vanilla stores

The project-memory rule reads "Do not use custom stores; use Options
API with createObjectStore." Read literally that would suggest every
Pinia store in the app moves to `createObjectStore`. That reading
is wrong, and the lib's API confirms it: `createObjectStore` is a
factory **for OpenRegister CRUD** — its base accepts `register` /
`schema` per type, exposes `fetchObject` / `fetchCollection` /
`patchObject` / `lockObject` / `publishObject` and so on. State that
isn't a CRUD wrapper around an OpenRegister entity does NOT fit the
factory.

softwarecatalog has five Pinia stores and we map each to its correct
shape:

| Store | Shape | What it holds | Decision |
| ------------- | ---------------------------------- | ---------------------------------------------------------------- | ---------------- |
| object | `createObjectStore` + 4 plugins | All OpenRegister CRUD across voorzieningen schemas | Migrated |
| navigation | vanilla `defineStore` | `selected` menu item, modal, dialog, transferData | Stay vanilla |
| settings | vanilla `defineStore` | settings load/save, ArchiMate import/export polling, configs | Stay vanilla |
| catalog | vanilla `defineStore` | placeholder (currentCatalog, loading, error) | Stay vanilla |
| organisatie | vanilla `defineStore` | contactpersoon endpoints, user-mgmt (password, groups, enable) | Stay vanilla |

The four vanilla stores hit **softwarecatalog-specific backend
endpoints** under `/index.php/apps/softwarecatalog/api/...`, NOT
OpenRegister. Forcing them through `createObjectStore` would mean:

- inventing fake "type" registrations for non-entity APIs
(ArchiMate import status, email config, user-group config),
- tunnelling state mutations through an unfit
`register / schema / objectId` URL builder, and
- exposing a CRUD surface (`fetchObject`, `patchObject`, etc.)
that has no semantics for these endpoints.

That's the same anti-pattern the rule was written to avoid, just
inverted.

## What the object store now exposes

The migration centred on `src/store/modules/object.js`:

```js
import { createObjectStore, filesPlugin, auditTrailsPlugin, relationsPlugin } from '@conduction/nextcloud-vue'
import { softwarecatalogPlugin } from '../plugins/softwarecatalogPlugin.js'

export const useObjectStore = createObjectStore('object', {
plugins: [
filesPlugin(),
auditTrailsPlugin(),
relationsPlugin(),
softwarecatalogPlugin(),
],
})
```

The `'object'` ID matches the legacy Pinia store ID, so all 41
existing importers of `objectStore` from `store/store.js` keep
working without source edits. The plugin contributes everything
the legacy store had that the lib base does not:

- `settings`, `objectItem`, `activeObjects`, `relatedData`,
`selectedObjects`, `success`, `objectErrors`, `metadata`,
`properties`, `columnFilters` state slots
- 16 lib-getter shims (`objectTypes`, `availableRegisters`,
`availableSchemas`, `getActiveObject`, `getRelatedData`,
`getAuditTrails`, `getCollection` array-or-results normaliser, …)
- 27 actions split across:
1. settings management (`fetchSettings`,
`initializeVoorzieningenObjectTypes`, `getSchemaConfig`)
2. active-object management (`setActiveObject`, `clearActiveObject`,
`setObjectItem`, `downloadObject`, `fetchRelatedData`)
3. CRUD shims that accept BOTH the legacy
`(objectItem, {register,schema})` AND the new
`(type, data)` signatures (`saveObject`, `deleteObject`,
`patchObject`, `copyObject`)
4. lifecycle ops (`publishObject`, `depublishObject`, `lockObject`,
`unlockObject`, `validateObject`)
5. mass ops (`_runMassOperation`, `massPublishObjects`,
`massDepublishObjects`, `massDeleteObjects`, `massLockObjects`,
`massUnlockObjects`, `massValidateObjects`)
6. selection mgmt (`setSelectedObjects`, `toggleSelectAllObjects`)
7. error mgmt (`setObjectError`, `clearObjectError`,
`clearAllObjectErrors`, `getObjectError`)
8. column mgmt (`updateColumnFilter`, `initializeProperties`,
`initializeColumnFilters`)
9. merge & migration (`mergeObjects`, `getMappings`,
`refreshObjectList`)
10. state mgmt (`setState`, `clearSoftwarecatalog`)

## Plugin fates

This change creates no new plugins. For the record, the existing
plugin's responsibilities map to lib equivalents as follows:

| softwarecatalogPlugin section | Lib alternative | Decision |
| ----------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| settings management | none | KEEP local — softwarecatalog has its own `/api/settings` shape (voorzieningen + amef configs, version info, etc.) that the lib doesn't model. |
| active-object management | partly `selectionPlugin` | KEEP local — `activeObjects` is keyed-per-type with related-data fan-out (`logs`/`uses`/`used`/`files`), which is richer than `selectionPlugin`'s single-active-object focus. |
| CRUD shims (dual signature) | base store | KEEP local — necessary for legacy `(objectItem, {register,schema})` callers in 41 view files. Could be deprecated incrementally; out-of-scope here. |
| lifecycle ops | `lifecyclePlugin` | KEEP local — softwarecatalog's `lockObject` accepts `(process, duration)` extras the base `lifecyclePlugin` doesn't carry. Refactoring is its own change. |
| mass ops | `selectionPlugin` | KEEP local — `_runMassOperation` adds per-object error tracking (`setObjectError`) the lib doesn't yet model. Refactoring is its own change. |
| selection mgmt | `selectionPlugin` | KEEP local — couples to the local `objectErrors` flow. |
| error mgmt | none | KEEP local — per-object error keyed map (used by mass-ops UX). |
| column mgmt | none | KEEP local — `metadata` + `properties` + `columnFilters` are softwarecatalog-specific UI state. |
| merge & migration | none | KEEP local — `mergeObjects` is a softwarecatalog escalation flow over the OR merge endpoint. |

## Lib gaps flagged

### Gap 1 — `@resolve:` sentinel not implemented

`src/manifest.json` ships 12 `@resolve:voorzieningen_register`
sentinels (PR #218). The lib openspec change
`nextcloud-vue/openspec/changes/manifest-resolve-sentinel/` defines
the loader semantics:

> "The loader walks an object tree and replaces every `@resolve:{key}`
> string with the result of `getAppConfigValue(appId, key)`. … walks
> only `pages[].config` subtrees by default."

Tasks are unchecked. Phase 1 (`src/utils/resolveManifestSentinels.js`)
is **not** implemented. The frontend still receives literal
`@resolve:voorzieningen_register` strings as the `register` field
on 12 manifest pages. Until the loader lands, every consumer (incl.
softwarecatalog) must either:

- pre-resolve the manifest server-side before serving it, OR
- substitute at runtime in the consumer (forbidden — that's exactly
the divergence the lib change is supposed to prevent).

This change DOES NOT fix the gap. It flags it for the lib roadmap.

### Gap 2 — `liveUpdatesPlugin` not wired

The motivation for the project-memory rule (decidesk #162) is that
`liveUpdatesPlugin` requires `fetchObject` / `fetchCollection` on the
store. softwarecatalog's `useObjectStore` now satisfies that contract
via `createObjectStore`, but the plugin is NOT in the plugin list.
Adding it requires:

- backend SSE/WS endpoint exposing object-changed events,
- a plugin-options block declaring the exclusion key namespace,
- end-to-end test of cache-invalidation on remote edits.

That work belongs in its own change; the migration only unblocks it.

## Validation checklist

- `npx eslint src` — zero new errors. Pre-existing warnings
(`jsdoc/no-defaults` × 4 in `softwarecatalogPlugin.js`) cleared
in this change.
- `node tests/validate-manifest.js` — PRE-EXISTING failure
unrelated to store migration (`ajv-formats` constructor
TypeError on `addFormats` — a tooling bug introduced separately).
Documented; not in-scope.
- `npx webpack --mode production` — succeeds, three
entrypoint-size warnings (pre-existing).
- All 41 importers of `objectStore` from `store/store.js`
continue to compile and resolve their named exports.
111 changes: 111 additions & 0 deletions openspec/changes/softwarecatalog-store-migration/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# SoftwareCatalog — store migration: createObjectStore + plugins

## Why

Project memory rule **`feedback_store-pattern.md`** is unambiguous:

> "Store pattern guidance — Do not use custom stores; use Options API
> with createObjectStore"

Decidesk **#162** is the canonical failure mode of *not* following
this rule: the live-updates plugin can't activate when an app keeps
its own custom Pinia store, because the lib's plugins reach into
fetchObject / fetchCollection on a `createObjectStore`-shaped store.
Apps with hand-rolled stores can't opt into:

- `liveUpdatesPlugin` (server-pushed cache invalidation),
- `searchPlugin`, `selectionPlugin`, `lifecyclePlugin` (full lib
catalog),
- `useListView` / `useDetailView` composables (require a store
exposing fetchCollection / fetchObject and registerObjectType).

Today's softwarecatalog tree under `src/store/`:

```
store.js (re-exports a Pinia composable per module)
modules/
catalog.js defineStore — placeholder (4 actions)
navigation.js defineStore — UI state (menu / modal / dialog / transferData)
navigation.spec.js jest spec for navigation
object.js createObjectStore('object', { plugins: [...] }) ← already migrated
organisatie.js defineStore — contactpersonen + user-mgmt actions
settings.js defineStore — settings load/save + ArchiMate import-export polling
plugins/
softwarecatalogPlugin.js createObjectStore-compatible plugin (state/getters/actions)
```

PR #189 already migrated the **object** store to
`createObjectStore('object', { plugins: [filesPlugin(),
auditTrailsPlugin(), relationsPlugin(), softwarecatalogPlugin()] })`
and re-implemented the legacy single-app helpers as a
`softwarecatalogPlugin()` factory consumed by that same call. The
remaining `defineStore` modules deliberately stay vanilla: they hold
**non-OpenRegister** state (UI shell, settings shell, custom
backend endpoints).

This change is the **specification + verification** that the migration
is complete for the OpenRegister-CRUD surface, plus the spec entry
that future contributors must read before adding a new `defineStore`.

## What Changes

1. **Spec capture** — add `softwarecatalog-store-migration/spec.md`
stating the two MUST rules:
- Every store wrapping an OpenRegister-CRUD entity MUST be
created via `createObjectStore(id, { plugins: [...] })`.
Vanilla `defineStore` for OpenRegister CRUD is forbidden.
- Per-app extensions to the CRUD surface (settings glue, mass
ops, app-specific metadata) MUST be expressed as
`createObjectStore` plugins (state/getters/actions),
not separate `defineStore` modules with parallel CRUD
methods. softwarecatalog's `softwarecatalogPlugin.js` is the
reference implementation.

2. **Verify state** — confirm that:
- `src/store/modules/object.js` matches the rule.
- All 41 importers of `objectStore` from `store/store.js` keep
working under the lib-backed store.
- The four remaining vanilla `defineStore` modules
(catalog, navigation, organisatie, settings) hold
non-OpenRegister state and are exempt per the spec.

3. **Lib-gap flags** — document, but do not fix in-app:
- `@resolve:` sentinel resolution is **not yet implemented** in
`@conduction/nextcloud-vue` (the openspec change exists at
`nextcloud-vue/openspec/changes/manifest-resolve-sentinel/`
but tasks are unchecked). softwarecatalog's `manifest.json`
uses 12 `@resolve:voorzieningen_register` sentinels that the
consumer-side reader must currently still resolve manually
until the lib lands the loader. This is tracked upstream;
no in-repo workaround is added here.
- `liveUpdatesPlugin` is **not** wired into softwarecatalog's
plugin list yet. This change does NOT enable it (live-updates
wiring is its own change with backend SSE/WS work). The
migration only ensures the store *can* accept it without
refactor.

4. **Lint cleanup** — clear the four `jsdoc/no-defaults` warnings
in `softwarecatalogPlugin.js` (single-line edit per warning).

## Why Not …

- **Drop the vanilla defineStore modules and re-implement them as
plugins.** Out of scope: those four hold non-CRUD state. A
`defineStore` for UI shell state is the correct Pinia pattern;
the project memory rule applies specifically to OpenRegister-CRUD
stores.
- **Wire `liveUpdatesPlugin` here.** Out of scope: live updates need
matching backend SSE/WS endpoints, exclusion-key wiring, and a
dedicated test pass.
- **Implement the `@resolve:` sentinel in this app.** Out of scope:
the loader belongs in `@conduction/nextcloud-vue` per the lib's
own `manifest-resolve-sentinel` openspec change. Implementing it
here would create drift the lib release will need to delete.

## References

- Project memory: `feedback_store-pattern.md`
- Decidesk failure case: ConductionNL/decidesk#162
- Lib openspec change: `nextcloud-vue/openspec/changes/manifest-resolve-sentinel/`
- Prior PR that migrated object.js + introduced softwarecatalogPlugin: ConductionNL/softwarecatalog#189
- Prior PR that introduced the `@resolve:` sentinel in manifest.json: ConductionNL/softwarecatalog#218
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# SoftwareCatalog — Store-migration Spec (delta)

## ADDED Requirement: createObjectStore for OpenRegister-CRUD stores

Every Pinia store in softwarecatalog that wraps an
**OpenRegister-CRUD entity** (an OR `register / schema / object`
triple) MUST be instantiated via
`createObjectStore(id, { plugins: [...] })` from
`@conduction/nextcloud-vue`. Vanilla `defineStore` MUST NOT be used
for OpenRegister-CRUD stores.

The factory contract gives apps:

- a uniform `fetchObject(type, id)` /
`fetchCollection(type, params)` / `saveObject(type, data)` /
`patchObject(type, id, changes)` / `deleteObject(type, id)`
surface,
- compatibility with lib plugins (`liveUpdatesPlugin`,
`searchPlugin`, `selectionPlugin`, `lifecyclePlugin`,
`auditTrailsPlugin`, `relationsPlugin`, `filesPlugin`,
`logsPlugin`, `registerMappingPlugin`),
- compatibility with the lib composables (`useListView`,
`useDetailView`).

#### Scenario: object store uses createObjectStore

- **GIVEN** the store at `src/store/modules/object.js`
- **WHEN** the file is parsed
- **THEN** the default export MUST be the result of
`createObjectStore('object', { plugins: [...] })`
- **AND** the store ID `'object'` MUST be preserved across
refactors so existing `from '../store/store.js'` importers
do not break

#### Scenario: registerObjectType is invoked on bootstrap

- **GIVEN** the softwarecatalogPlugin's `initializeVoorzieningenObjectTypes`
action
- **WHEN** `fetchSettings` resolves a non-empty `availableRegisters`
list with a `voorzieningen` register
- **THEN** every schema in that register MUST be registered via
`registerObjectType(slug, schemaId, registerId)` so subsequent
`fetchCollection(slug)` calls have a known
`register / schema` mapping

## ADDED Requirement: plugin shape for app-specific extensions

Per-app extensions to the OpenRegister-CRUD surface (settings glue,
mass operations, app-specific metadata, related-data fan-out) MUST
be expressed as a `createObjectStore` plugin — a factory returning
`{ name, state, getters, actions }` — and added to the plugins array
on the same `createObjectStore` call. Apps MUST NOT create
parallel `defineStore` modules with their own CRUD methods that
duplicate the lib base.

#### Scenario: plugin uses lib helpers

- **GIVEN** `src/store/plugins/softwarecatalogPlugin.js`
- **WHEN** the file is parsed
- **THEN** it MUST import `buildHeaders` and `buildQueryString` from
`@conduction/nextcloud-vue` rather than redefining them locally
- **AND** it MUST export a factory function
`softwarecatalogPlugin()` that returns
`{ name: 'Softwarecatalog', state, getters, actions }`

#### Scenario: legacy CRUD signatures keep working

- **GIVEN** the softwarecatalogPlugin's `saveObject` and
`deleteObject` actions
- **WHEN** an existing view calls
`objectStore.saveObject(objectItem, { register, schema })` (legacy)
or `objectStore.saveObject('organisatie', objectData)` (new)
- **THEN** both signatures MUST resolve to the same
OpenRegister POST/PUT call against
`/index.php/apps/openregister/api/objects/{registerId}/{schemaId}[/{id}]`

## ADDED Requirement: vanilla defineStore allowed for non-CRUD state

Stores that hold UI shell state (active menu item, modal/dialog
flags), settings glue (load/save against an app-specific endpoint,
ArchiMate import polling), or wrappers over **non-OpenRegister
backend endpoints** (softwarecatalog's `/api/contactpersonen/*`,
`/api/email/*`, `/api/user-groups/*`) MAY use vanilla `defineStore`.

These stores MUST NOT be migrated to `createObjectStore` —
`createObjectStore` exposes a CRUD surface
(`register / schema / objectId` URL builder) that has no semantics
for these endpoints.

#### Scenario: non-CRUD stores remain vanilla

- **GIVEN** the four current vanilla stores
`navigation`, `settings`, `catalog`, `organisatie`
- **WHEN** linted and built
- **THEN** they MUST remain `defineStore`-based
- **AND** their state MUST NOT include OpenRegister
`register` / `schema` / `objectId` references that would
qualify them as OR-CRUD stores
Loading
Loading