Skip to content

Commit 705402b

Browse files
authored
Merge pull request #219 from ConductionNL/feature/softwarecatalog-store-migration
fix: migrate softwarecatalog to @conduction/nextcloud-vue useObjectStore
2 parents 8c0638f + f7ecaf6 commit 705402b

5 files changed

Lines changed: 429 additions & 4 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Design — SoftwareCatalog store migration
2+
3+
## Architectural decision: keep the four vanilla stores
4+
5+
The project-memory rule reads "Do not use custom stores; use Options
6+
API with createObjectStore." Read literally that would suggest every
7+
Pinia store in the app moves to `createObjectStore`. That reading
8+
is wrong, and the lib's API confirms it: `createObjectStore` is a
9+
factory **for OpenRegister CRUD** — its base accepts `register` /
10+
`schema` per type, exposes `fetchObject` / `fetchCollection` /
11+
`patchObject` / `lockObject` / `publishObject` and so on. State that
12+
isn't a CRUD wrapper around an OpenRegister entity does NOT fit the
13+
factory.
14+
15+
softwarecatalog has five Pinia stores and we map each to its correct
16+
shape:
17+
18+
| Store | Shape | What it holds | Decision |
19+
| ------------- | ---------------------------------- | ---------------------------------------------------------------- | ---------------- |
20+
| object | `createObjectStore` + 4 plugins | All OpenRegister CRUD across voorzieningen schemas | Migrated |
21+
| navigation | vanilla `defineStore` | `selected` menu item, modal, dialog, transferData | Stay vanilla |
22+
| settings | vanilla `defineStore` | settings load/save, ArchiMate import/export polling, configs | Stay vanilla |
23+
| catalog | vanilla `defineStore` | placeholder (currentCatalog, loading, error) | Stay vanilla |
24+
| organisatie | vanilla `defineStore` | contactpersoon endpoints, user-mgmt (password, groups, enable) | Stay vanilla |
25+
26+
The four vanilla stores hit **softwarecatalog-specific backend
27+
endpoints** under `/index.php/apps/softwarecatalog/api/...`, NOT
28+
OpenRegister. Forcing them through `createObjectStore` would mean:
29+
30+
- inventing fake "type" registrations for non-entity APIs
31+
(ArchiMate import status, email config, user-group config),
32+
- tunnelling state mutations through an unfit
33+
`register / schema / objectId` URL builder, and
34+
- exposing a CRUD surface (`fetchObject`, `patchObject`, etc.)
35+
that has no semantics for these endpoints.
36+
37+
That's the same anti-pattern the rule was written to avoid, just
38+
inverted.
39+
40+
## What the object store now exposes
41+
42+
The migration centred on `src/store/modules/object.js`:
43+
44+
```js
45+
import { createObjectStore, filesPlugin, auditTrailsPlugin, relationsPlugin } from '@conduction/nextcloud-vue'
46+
import { softwarecatalogPlugin } from '../plugins/softwarecatalogPlugin.js'
47+
48+
export const useObjectStore = createObjectStore('object', {
49+
plugins: [
50+
filesPlugin(),
51+
auditTrailsPlugin(),
52+
relationsPlugin(),
53+
softwarecatalogPlugin(),
54+
],
55+
})
56+
```
57+
58+
The `'object'` ID matches the legacy Pinia store ID, so all 41
59+
existing importers of `objectStore` from `store/store.js` keep
60+
working without source edits. The plugin contributes everything
61+
the legacy store had that the lib base does not:
62+
63+
- `settings`, `objectItem`, `activeObjects`, `relatedData`,
64+
`selectedObjects`, `success`, `objectErrors`, `metadata`,
65+
`properties`, `columnFilters` state slots
66+
- 16 lib-getter shims (`objectTypes`, `availableRegisters`,
67+
`availableSchemas`, `getActiveObject`, `getRelatedData`,
68+
`getAuditTrails`, `getCollection` array-or-results normaliser, …)
69+
- 27 actions split across:
70+
1. settings management (`fetchSettings`,
71+
`initializeVoorzieningenObjectTypes`, `getSchemaConfig`)
72+
2. active-object management (`setActiveObject`, `clearActiveObject`,
73+
`setObjectItem`, `downloadObject`, `fetchRelatedData`)
74+
3. CRUD shims that accept BOTH the legacy
75+
`(objectItem, {register,schema})` AND the new
76+
`(type, data)` signatures (`saveObject`, `deleteObject`,
77+
`patchObject`, `copyObject`)
78+
4. lifecycle ops (`publishObject`, `depublishObject`, `lockObject`,
79+
`unlockObject`, `validateObject`)
80+
5. mass ops (`_runMassOperation`, `massPublishObjects`,
81+
`massDepublishObjects`, `massDeleteObjects`, `massLockObjects`,
82+
`massUnlockObjects`, `massValidateObjects`)
83+
6. selection mgmt (`setSelectedObjects`, `toggleSelectAllObjects`)
84+
7. error mgmt (`setObjectError`, `clearObjectError`,
85+
`clearAllObjectErrors`, `getObjectError`)
86+
8. column mgmt (`updateColumnFilter`, `initializeProperties`,
87+
`initializeColumnFilters`)
88+
9. merge & migration (`mergeObjects`, `getMappings`,
89+
`refreshObjectList`)
90+
10. state mgmt (`setState`, `clearSoftwarecatalog`)
91+
92+
## Plugin fates
93+
94+
This change creates no new plugins. For the record, the existing
95+
plugin's responsibilities map to lib equivalents as follows:
96+
97+
| softwarecatalogPlugin section | Lib alternative | Decision |
98+
| ----------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99+
| settings management | none | KEEP local — softwarecatalog has its own `/api/settings` shape (voorzieningen + amef configs, version info, etc.) that the lib doesn't model. |
100+
| 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. |
101+
| 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. |
102+
| lifecycle ops | `lifecyclePlugin` | KEEP local — softwarecatalog's `lockObject` accepts `(process, duration)` extras the base `lifecyclePlugin` doesn't carry. Refactoring is its own change. |
103+
| mass ops | `selectionPlugin` | KEEP local — `_runMassOperation` adds per-object error tracking (`setObjectError`) the lib doesn't yet model. Refactoring is its own change. |
104+
| selection mgmt | `selectionPlugin` | KEEP local — couples to the local `objectErrors` flow. |
105+
| error mgmt | none | KEEP local — per-object error keyed map (used by mass-ops UX). |
106+
| column mgmt | none | KEEP local — `metadata` + `properties` + `columnFilters` are softwarecatalog-specific UI state. |
107+
| merge & migration | none | KEEP local — `mergeObjects` is a softwarecatalog escalation flow over the OR merge endpoint. |
108+
109+
## Lib gaps flagged
110+
111+
### Gap 1 — `@resolve:` sentinel not implemented
112+
113+
`src/manifest.json` ships 12 `@resolve:voorzieningen_register`
114+
sentinels (PR #218). The lib openspec change
115+
`nextcloud-vue/openspec/changes/manifest-resolve-sentinel/` defines
116+
the loader semantics:
117+
118+
> "The loader walks an object tree and replaces every `@resolve:{key}`
119+
> string with the result of `getAppConfigValue(appId, key)`. … walks
120+
> only `pages[].config` subtrees by default."
121+
122+
Tasks are unchecked. Phase 1 (`src/utils/resolveManifestSentinels.js`)
123+
is **not** implemented. The frontend still receives literal
124+
`@resolve:voorzieningen_register` strings as the `register` field
125+
on 12 manifest pages. Until the loader lands, every consumer (incl.
126+
softwarecatalog) must either:
127+
128+
- pre-resolve the manifest server-side before serving it, OR
129+
- substitute at runtime in the consumer (forbidden — that's exactly
130+
the divergence the lib change is supposed to prevent).
131+
132+
This change DOES NOT fix the gap. It flags it for the lib roadmap.
133+
134+
### Gap 2 — `liveUpdatesPlugin` not wired
135+
136+
The motivation for the project-memory rule (decidesk #162) is that
137+
`liveUpdatesPlugin` requires `fetchObject` / `fetchCollection` on the
138+
store. softwarecatalog's `useObjectStore` now satisfies that contract
139+
via `createObjectStore`, but the plugin is NOT in the plugin list.
140+
Adding it requires:
141+
142+
- backend SSE/WS endpoint exposing object-changed events,
143+
- a plugin-options block declaring the exclusion key namespace,
144+
- end-to-end test of cache-invalidation on remote edits.
145+
146+
That work belongs in its own change; the migration only unblocks it.
147+
148+
## Validation checklist
149+
150+
- `npx eslint src` — zero new errors. Pre-existing warnings
151+
(`jsdoc/no-defaults` × 4 in `softwarecatalogPlugin.js`) cleared
152+
in this change.
153+
- `node tests/validate-manifest.js` — PRE-EXISTING failure
154+
unrelated to store migration (`ajv-formats` constructor
155+
TypeError on `addFormats` — a tooling bug introduced separately).
156+
Documented; not in-scope.
157+
- `npx webpack --mode production` — succeeds, three
158+
entrypoint-size warnings (pre-existing).
159+
- All 41 importers of `objectStore` from `store/store.js`
160+
continue to compile and resolve their named exports.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# SoftwareCatalog — store migration: createObjectStore + plugins
2+
3+
## Why
4+
5+
Project memory rule **`feedback_store-pattern.md`** is unambiguous:
6+
7+
> "Store pattern guidance — Do not use custom stores; use Options API
8+
> with createObjectStore"
9+
10+
Decidesk **#162** is the canonical failure mode of *not* following
11+
this rule: the live-updates plugin can't activate when an app keeps
12+
its own custom Pinia store, because the lib's plugins reach into
13+
fetchObject / fetchCollection on a `createObjectStore`-shaped store.
14+
Apps with hand-rolled stores can't opt into:
15+
16+
- `liveUpdatesPlugin` (server-pushed cache invalidation),
17+
- `searchPlugin`, `selectionPlugin`, `lifecyclePlugin` (full lib
18+
catalog),
19+
- `useListView` / `useDetailView` composables (require a store
20+
exposing fetchCollection / fetchObject and registerObjectType).
21+
22+
Today's softwarecatalog tree under `src/store/`:
23+
24+
```
25+
store.js (re-exports a Pinia composable per module)
26+
modules/
27+
catalog.js defineStore — placeholder (4 actions)
28+
navigation.js defineStore — UI state (menu / modal / dialog / transferData)
29+
navigation.spec.js jest spec for navigation
30+
object.js createObjectStore('object', { plugins: [...] }) ← already migrated
31+
organisatie.js defineStore — contactpersonen + user-mgmt actions
32+
settings.js defineStore — settings load/save + ArchiMate import-export polling
33+
plugins/
34+
softwarecatalogPlugin.js createObjectStore-compatible plugin (state/getters/actions)
35+
```
36+
37+
PR #189 already migrated the **object** store to
38+
`createObjectStore('object', { plugins: [filesPlugin(),
39+
auditTrailsPlugin(), relationsPlugin(), softwarecatalogPlugin()] })`
40+
and re-implemented the legacy single-app helpers as a
41+
`softwarecatalogPlugin()` factory consumed by that same call. The
42+
remaining `defineStore` modules deliberately stay vanilla: they hold
43+
**non-OpenRegister** state (UI shell, settings shell, custom
44+
backend endpoints).
45+
46+
This change is the **specification + verification** that the migration
47+
is complete for the OpenRegister-CRUD surface, plus the spec entry
48+
that future contributors must read before adding a new `defineStore`.
49+
50+
## What Changes
51+
52+
1. **Spec capture** — add `softwarecatalog-store-migration/spec.md`
53+
stating the two MUST rules:
54+
- Every store wrapping an OpenRegister-CRUD entity MUST be
55+
created via `createObjectStore(id, { plugins: [...] })`.
56+
Vanilla `defineStore` for OpenRegister CRUD is forbidden.
57+
- Per-app extensions to the CRUD surface (settings glue, mass
58+
ops, app-specific metadata) MUST be expressed as
59+
`createObjectStore` plugins (state/getters/actions),
60+
not separate `defineStore` modules with parallel CRUD
61+
methods. softwarecatalog's `softwarecatalogPlugin.js` is the
62+
reference implementation.
63+
64+
2. **Verify state** — confirm that:
65+
- `src/store/modules/object.js` matches the rule.
66+
- All 41 importers of `objectStore` from `store/store.js` keep
67+
working under the lib-backed store.
68+
- The four remaining vanilla `defineStore` modules
69+
(catalog, navigation, organisatie, settings) hold
70+
non-OpenRegister state and are exempt per the spec.
71+
72+
3. **Lib-gap flags** — document, but do not fix in-app:
73+
- `@resolve:` sentinel resolution is **not yet implemented** in
74+
`@conduction/nextcloud-vue` (the openspec change exists at
75+
`nextcloud-vue/openspec/changes/manifest-resolve-sentinel/`
76+
but tasks are unchecked). softwarecatalog's `manifest.json`
77+
uses 12 `@resolve:voorzieningen_register` sentinels that the
78+
consumer-side reader must currently still resolve manually
79+
until the lib lands the loader. This is tracked upstream;
80+
no in-repo workaround is added here.
81+
- `liveUpdatesPlugin` is **not** wired into softwarecatalog's
82+
plugin list yet. This change does NOT enable it (live-updates
83+
wiring is its own change with backend SSE/WS work). The
84+
migration only ensures the store *can* accept it without
85+
refactor.
86+
87+
4. **Lint cleanup** — clear the four `jsdoc/no-defaults` warnings
88+
in `softwarecatalogPlugin.js` (single-line edit per warning).
89+
90+
## Why Not …
91+
92+
- **Drop the vanilla defineStore modules and re-implement them as
93+
plugins.** Out of scope: those four hold non-CRUD state. A
94+
`defineStore` for UI shell state is the correct Pinia pattern;
95+
the project memory rule applies specifically to OpenRegister-CRUD
96+
stores.
97+
- **Wire `liveUpdatesPlugin` here.** Out of scope: live updates need
98+
matching backend SSE/WS endpoints, exclusion-key wiring, and a
99+
dedicated test pass.
100+
- **Implement the `@resolve:` sentinel in this app.** Out of scope:
101+
the loader belongs in `@conduction/nextcloud-vue` per the lib's
102+
own `manifest-resolve-sentinel` openspec change. Implementing it
103+
here would create drift the lib release will need to delete.
104+
105+
## References
106+
107+
- Project memory: `feedback_store-pattern.md`
108+
- Decidesk failure case: ConductionNL/decidesk#162
109+
- Lib openspec change: `nextcloud-vue/openspec/changes/manifest-resolve-sentinel/`
110+
- Prior PR that migrated object.js + introduced softwarecatalogPlugin: ConductionNL/softwarecatalog#189
111+
- Prior PR that introduced the `@resolve:` sentinel in manifest.json: ConductionNL/softwarecatalog#218
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# SoftwareCatalog — Store-migration Spec (delta)
2+
3+
## ADDED Requirement: createObjectStore for OpenRegister-CRUD stores
4+
5+
Every Pinia store in softwarecatalog that wraps an
6+
**OpenRegister-CRUD entity** (an OR `register / schema / object`
7+
triple) MUST be instantiated via
8+
`createObjectStore(id, { plugins: [...] })` from
9+
`@conduction/nextcloud-vue`. Vanilla `defineStore` MUST NOT be used
10+
for OpenRegister-CRUD stores.
11+
12+
The factory contract gives apps:
13+
14+
- a uniform `fetchObject(type, id)` /
15+
`fetchCollection(type, params)` / `saveObject(type, data)` /
16+
`patchObject(type, id, changes)` / `deleteObject(type, id)`
17+
surface,
18+
- compatibility with lib plugins (`liveUpdatesPlugin`,
19+
`searchPlugin`, `selectionPlugin`, `lifecyclePlugin`,
20+
`auditTrailsPlugin`, `relationsPlugin`, `filesPlugin`,
21+
`logsPlugin`, `registerMappingPlugin`),
22+
- compatibility with the lib composables (`useListView`,
23+
`useDetailView`).
24+
25+
#### Scenario: object store uses createObjectStore
26+
27+
- **GIVEN** the store at `src/store/modules/object.js`
28+
- **WHEN** the file is parsed
29+
- **THEN** the default export MUST be the result of
30+
`createObjectStore('object', { plugins: [...] })`
31+
- **AND** the store ID `'object'` MUST be preserved across
32+
refactors so existing `from '../store/store.js'` importers
33+
do not break
34+
35+
#### Scenario: registerObjectType is invoked on bootstrap
36+
37+
- **GIVEN** the softwarecatalogPlugin's `initializeVoorzieningenObjectTypes`
38+
action
39+
- **WHEN** `fetchSettings` resolves a non-empty `availableRegisters`
40+
list with a `voorzieningen` register
41+
- **THEN** every schema in that register MUST be registered via
42+
`registerObjectType(slug, schemaId, registerId)` so subsequent
43+
`fetchCollection(slug)` calls have a known
44+
`register / schema` mapping
45+
46+
## ADDED Requirement: plugin shape for app-specific extensions
47+
48+
Per-app extensions to the OpenRegister-CRUD surface (settings glue,
49+
mass operations, app-specific metadata, related-data fan-out) MUST
50+
be expressed as a `createObjectStore` plugin — a factory returning
51+
`{ name, state, getters, actions }` — and added to the plugins array
52+
on the same `createObjectStore` call. Apps MUST NOT create
53+
parallel `defineStore` modules with their own CRUD methods that
54+
duplicate the lib base.
55+
56+
#### Scenario: plugin uses lib helpers
57+
58+
- **GIVEN** `src/store/plugins/softwarecatalogPlugin.js`
59+
- **WHEN** the file is parsed
60+
- **THEN** it MUST import `buildHeaders` and `buildQueryString` from
61+
`@conduction/nextcloud-vue` rather than redefining them locally
62+
- **AND** it MUST export a factory function
63+
`softwarecatalogPlugin()` that returns
64+
`{ name: 'Softwarecatalog', state, getters, actions }`
65+
66+
#### Scenario: legacy CRUD signatures keep working
67+
68+
- **GIVEN** the softwarecatalogPlugin's `saveObject` and
69+
`deleteObject` actions
70+
- **WHEN** an existing view calls
71+
`objectStore.saveObject(objectItem, { register, schema })` (legacy)
72+
or `objectStore.saveObject('organisatie', objectData)` (new)
73+
- **THEN** both signatures MUST resolve to the same
74+
OpenRegister POST/PUT call against
75+
`/index.php/apps/openregister/api/objects/{registerId}/{schemaId}[/{id}]`
76+
77+
## ADDED Requirement: vanilla defineStore allowed for non-CRUD state
78+
79+
Stores that hold UI shell state (active menu item, modal/dialog
80+
flags), settings glue (load/save against an app-specific endpoint,
81+
ArchiMate import polling), or wrappers over **non-OpenRegister
82+
backend endpoints** (softwarecatalog's `/api/contactpersonen/*`,
83+
`/api/email/*`, `/api/user-groups/*`) MAY use vanilla `defineStore`.
84+
85+
These stores MUST NOT be migrated to `createObjectStore`
86+
`createObjectStore` exposes a CRUD surface
87+
(`register / schema / objectId` URL builder) that has no semantics
88+
for these endpoints.
89+
90+
#### Scenario: non-CRUD stores remain vanilla
91+
92+
- **GIVEN** the four current vanilla stores
93+
`navigation`, `settings`, `catalog`, `organisatie`
94+
- **WHEN** linted and built
95+
- **THEN** they MUST remain `defineStore`-based
96+
- **AND** their state MUST NOT include OpenRegister
97+
`register` / `schema` / `objectId` references that would
98+
qualify them as OR-CRUD stores

0 commit comments

Comments
 (0)