Skip to content

Commit d7f91a5

Browse files
committed
fix(store): drop the hand-rolled object store; route settings through cnFetchJson
The template shipped TWO object stores. `src/store/store.js` already created the canonical one with `createObjectStore` from @conduction/nextcloud-vue, while `src/store/modules/object.js` defined a second `useObjectStore` as a hand-rolled `defineStore('object')` doing raw `fetch()` with a hand-set `requesttoken`. Nothing imported the second one — but every app scaffolded from this template inherited it, which is why ADR-071 finds app-local `store/modules/object.js` variants in six apps and parallel generic stores in two more. The template was the source that kept re-seeding the pattern ADR-071 Decisions 1 and 2 delete. Changes: - Delete src/store/modules/object.js (dead, and the anti-pattern's seed). - settings.js now uses cnFetchJson instead of raw fetch + getRequestToken (ADR-071 Decision 1). URLs still go through generateUrl, because cnFetch's own prefixUrl only adds /index.php and does not know the instance webroot — handing it an already-generated URL is a no-op there, so this keeps the correct URL AND the library's blessed headers. - settings.js gains an `error` state. Both actions previously returned null on failure, and null was also the legitimate "nothing yet" value, so a settings endpoint returning 500 was indistinguishable from a fresh install. Failures are now logged AND observable; the actions still resolve rather than throw, so a broken endpoint cannot stop the SPA mounting. - Bump @conduction/nextcloud-vue ^2.3.0 -> ^2.16.0. REQUIRED, not cosmetic: cnFetch/cnFetchJson first shipped in v2.11.0, and the installed tree was on 2.2.0, so the import would not have resolved. ^2.16.0 matches openregister and decidesk; the lock resolves 2.17.0. The spec is corrected in the same commit, because it was wrong in two ways: - It REQUIRED the deleted module by path, and pinned `configure()` / `registerObjectType()` / `fetchObjects()` — an API the dead module had and the library store does not. The spec described the module nothing imported while the code used the other one. - Its "degrade gracefully" clause REQUIRED failures to resolve to a safe empty/null value and said nothing about surfacing them, writing the unobservable-failure pattern into a normative requirement. It now requires the safe value AND a recorded error. Verified: npm run build compiles (webpack 5.109.2, 2 pre-existing warnings). Refs ADR-071 Decisions 1-2, ADR-026, ADR-100 Decision 5.
1 parent 3fe1e98 commit d7f91a5

5 files changed

Lines changed: 148 additions & 161 deletions

File tree

openspec/specs/frontend-data-stores/spec.md

Lines changed: 86 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -21,94 +21,108 @@ Per ADR-022, template-derived apps own no database of their own — the Vue SPA
2121
talks to OpenRegister's object API directly through a thin Pinia store layer.
2222
This capability defines that store layer:
2323

24-
- A **generic object store** (`src/store/modules/object.js`) that is configured
25-
once with the OpenRegister API base URLs, then accepts the registration of
26-
named object types (each mapping a logical type name to a register + schema),
27-
and fetches collections of those objects on demand.
24+
- A **generic object store** obtained from `createObjectStore` in
25+
`@conduction/nextcloud-vue` and instantiated in `src/store/store.js`. The app
26+
does NOT define one of its own: per ADR-071 Decision 2 the library's factory
27+
is the only generic OpenRegister object store, and per ADR-026 an app-local
28+
copy drifts from the lib's action surface until a manifest-rendered page calls
29+
an action that no longer exists.
2830
- An **app-settings store** (`src/store/modules/settings.js`) that reads and
2931
writes the app's own settings through the backend `GET`/`POST /api/settings`
3032
endpoints defined by the settings-management capability — it is the frontend
3133
half of REQ-CFG-001 / REQ-CFG-002.
3234
- A **boot-time initializer** (`src/store/store.js`) that wires the object store
3335
to OpenRegister's URLs and primes the settings store before the SPA renders.
3436

35-
All store actions MUST degrade gracefully: a failed network request MUST be
36-
logged client-side and resolve to a safe empty/`null` value so that a single
37-
failing fetch never breaks the SPA mount (mirrors ADR-005's "log server-side,
38-
return safe fallback" rule on the client).
37+
All store actions MUST degrade gracefully: a failed network request MUST NOT
38+
throw out of the action, so a single failing fetch never breaks the SPA mount
39+
(mirrors ADR-005's "log server-side, return safe fallback" rule on the client).
40+
41+
**Degrading is not the same as going quiet.** An action that resolves to
42+
`null`/`[]` on failure MUST also record the failure where a caller can see it —
43+
logged client-side AND exposed in store state. The earlier wording required only
44+
the safe value, and `null` was simultaneously the legitimate "nothing yet"
45+
value, so a settings endpoint returning 500 was indistinguishable from a fresh
46+
install for as long as anyone cared to look.
3947

4048
## Requirements
4149

42-
### REQ-STORE-001: Configure the object store with OpenRegister URLs
50+
### REQ-STORE-001: The object store comes from the library, not from the app
4351

44-
The object store MUST expose a `configure({ baseUrl, schemaBaseUrl })` action
45-
that records the OpenRegister object-API and schema-API base URLs. No object
46-
fetch may be attempted before the store has been configured.
52+
The app MUST obtain its generic OpenRegister object store from
53+
`createObjectStore` in `@conduction/nextcloud-vue`, instantiated once in
54+
`src/store/store.js` with the app's register and schema slugs. The app MUST NOT
55+
define its own generic object store.
4756

48-
#### Scenario: Store is configured at boot
57+
Per ADR-071 Decision 2 the library's factory is the only generic object store in
58+
the fleet; per ADR-026 an app-local copy drifts from the lib's action surface,
59+
and the observed failure mode is a manifest-rendered page calling an action the
60+
local store never had (`fetchObject is not a function`, decidesk#162).
4961

50-
- GIVEN a freshly created object store with empty `baseUrl`
51-
- WHEN `configure({ baseUrl, schemaBaseUrl })` is called with the OpenRegister object/schema API URLs
52-
- THEN the store MUST persist `baseUrl` and `schemaBaseUrl` in its state
53-
- AND subsequent `fetchObjects` calls MUST build their request URL from the stored `baseUrl`
62+
#### Scenario: The store is created from the library factory
5463

55-
### REQ-STORE-002: Register named object types
64+
- GIVEN an app scaffolded from this template
65+
- WHEN `src/store/store.js` is loaded
66+
- THEN it MUST call `createObjectStore` imported from `@conduction/nextcloud-vue`
67+
- AND it MUST pass the app's `register` and `schema` slugs
68+
- AND the module MUST export the resulting store as `useObjectStore`
5669

57-
The object store MUST expose a `registerObjectType(type, schema, register)`
58-
action that maps a logical type name to its OpenRegister `schema` + `register`
59-
identifiers and initialises an empty result bucket for that type. Fetching an
60-
unregistered type MUST be a no-op that warns rather than throwing.
70+
#### Scenario: No app-local generic object store exists
6171

62-
#### Scenario: A type is registered
72+
- GIVEN the app's `src/store/` tree
73+
- WHEN it is searched for a Pinia store defining generic OpenRegister CRUD
74+
- THEN no module other than `store.js` MAY define one
75+
- AND in particular `src/store/modules/object.js` MUST NOT exist
6376

64-
- GIVEN a configured object store
65-
- WHEN `registerObjectType('item', '<schemaId>', '<registerId>')` is called
66-
- THEN the store MUST record `{ schema, register }` under `objectTypes.item`
67-
- AND it MUST initialise `objects.item` to an empty array if it was unset
77+
### REQ-STORE-002: Object types are registered through the library's surface
6878

69-
#### Scenario: Fetching an unregistered type
79+
Registering a logical type name against a register + schema, fetching
80+
collections, pagination, single-flight de-duplication and error surfacing are
81+
all the library store's responsibility. The app MUST use that surface rather
82+
than re-implementing any part of it.
7083

71-
- GIVEN a configured store with no `item` type registered
72-
- WHEN `fetchObjects('item')` is called
73-
- THEN the store MUST emit a client-side warning naming the unregistered type
74-
- AND it MUST return an empty array without performing a network request
84+
This requirement deliberately does not restate the library's action names. The
85+
previous version of this spec pinned `configure()`, `registerObjectType()` and
86+
`fetchObjects()` — an API the app's own dead module had and the library store
87+
does not — so the spec described a module nothing imported while the code used a
88+
different one.
7589

76-
### REQ-STORE-003: Fetch a collection of objects
90+
#### Scenario: The app needs a collection
7791

78-
The object store MUST expose an async `fetchObjects(type, params)` action that
79-
issues a `GET` to the configured `baseUrl` with the registered `register` and
80-
`schema` as query parameters (plus any caller-supplied `params`), carrying the
81-
Nextcloud request token. On success it MUST store and return the result
82-
collection; on any failure it MUST log client-side and return an empty array,
83-
and it MUST clear the per-type loading flag in all cases.
92+
- GIVEN a store created by `createObjectStore`
93+
- WHEN the app needs a collection of objects
94+
- THEN it MUST call the library store's own collection action
95+
- AND it MUST NOT construct the OpenRegister request URL itself
8496

85-
#### Scenario: Successful fetch
97+
### REQ-STORE-003: App code does not hand-set the request token
8698

87-
- GIVEN a registered, configured `item` type
88-
- WHEN `fetchObjects('item', { limit: 10 })` resolves with HTTP 200
89-
- THEN the request URL MUST carry `register`, `schema`, and `limit=10` query parameters
90-
- AND the request MUST send the `requesttoken` header
91-
- AND the store MUST set `objects.item` to `data.results` (or `data` when no `results` envelope) and return it
92-
- AND `loading.item` MUST be cleared to `false`
99+
Any HTTP the app performs outside the object store MUST go through `cnFetch` or
100+
`cnFetchJson` from `@conduction/nextcloud-vue`. A raw `fetch()` carrying a
101+
hand-set `requesttoken` header is forbidden (ADR-071 Decision 1): the library
102+
owns the one blessed CSRF idiom, URL prefixing and error normalisation, so a
103+
Nextcloud-version or CSP change is one fix rather than one per app.
93104

94-
#### Scenario: Network failure
105+
#### Scenario: The settings store reads the backend
95106

96-
- GIVEN a registered type
97-
- WHEN the fetch rejects or returns a non-OK status
98-
- THEN the store MUST log the error client-side
99-
- AND it MUST return an empty array
100-
- AND `loading.item` MUST still be cleared to `false`
107+
- GIVEN the settings store issues `GET /api/settings`
108+
- WHEN the request is constructed
109+
- THEN it MUST be issued through `cnFetchJson`
110+
- AND the app MUST NOT import `getRequestToken` to build the header itself
111+
- AND the URL MUST be resolved with `generateUrl` so an instance served from a
112+
webroot subdirectory is addressed correctly
101113

102114
### REQ-STORE-004: Read and write app settings from the SPA
103115

104116
The settings store MUST expose an async `fetchSettings()` action that `GET`s
105117
`/api/settings` and a `saveSettings(payload)` action that `POST`s a partial
106118
settings payload to the same endpoint, both carrying the request token. These
107119
are the client counterparts of the settings-management capability's REQ-CFG-001
108-
and REQ-CFG-002. `fetchSettings()` MUST additionally derive the
120+
and REQ-CFG-002. Both actions MUST be issued through `cnFetchJson`
121+
(REQ-STORE-003). `fetchSettings()` MUST additionally derive the
109122
`hasOpenRegisters` and `isAdmin` flags from the response so the UI can degrade
110-
gracefully (ADR-005 / REQ-CFG-004). Both actions MUST log and return `null` on
111-
failure rather than throwing.
123+
gracefully (ADR-005 / REQ-CFG-004). Both actions MUST return `null` on failure
124+
rather than throwing, and MUST record the failure in the store's `error` state
125+
so that "the request failed" is distinguishable from "there is nothing yet".
112126

113127
#### Scenario: Settings load
114128

@@ -131,18 +145,31 @@ failure rather than throwing.
131145
- WHEN the action handles the failure
132146
- THEN it MUST log the error client-side
133147
- AND it MUST return `null`
148+
- AND it MUST set `error` to the failure
134149
- AND `loading` MUST be reset to `false`
135150

151+
#### Scenario: A failure is distinguishable from an empty result
152+
153+
- GIVEN `fetchSettings()` has returned `null`
154+
- WHEN a caller needs to know whether the backend failed or simply had nothing
155+
- THEN `error` MUST be non-null if and only if the request failed
156+
- AND `error` MUST be cleared at the start of the next request
157+
136158
### REQ-STORE-005: Initialise the stores before the SPA renders
137159

138160
The system MUST expose an async `initializeStores()` boot helper that
139-
configures the object store to point at OpenRegister's object/schema API URLs
140-
and primes the settings store with a first `fetchSettings()` call, returning
141-
both store handles to the caller.
161+
instantiates the object store and primes the settings store with a first
162+
`fetchSettings()` call, returning both store handles to the caller.
163+
164+
The library store is configured by the `register`/`schema` passed to
165+
`createObjectStore` at module load, so there is no separate `configure()` step.
166+
The previous wording required one, describing the app-local store this template
167+
no longer ships.
142168

143169
#### Scenario: Boot sequence
144170

145171
- WHEN `initializeStores()` is awaited during SPA bootstrap
146-
- THEN it MUST call `objectStore.configure()` with the OpenRegister `/api/objects` and `/api/schemas` URLs
172+
- THEN it MUST instantiate the object store created by `createObjectStore`
147173
- AND it MUST await `settingsStore.fetchSettings()` so the first render has settings available
148174
- AND it MUST return `{ settingsStore, objectStore }`
175+
- AND it MUST NOT fail the boot when `fetchSettings()` resolves to `null`

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"dependencies": {
3232
"@babel/core": "^7.29.0",
3333
"@babel/preset-env": "^7.29.5",
34-
"@conduction/nextcloud-vue": "^2.3.0",
34+
"@conduction/nextcloud-vue": "^2.16.0",
3535
"@nextcloud/auth": "^2.5.0",
3636
"@nextcloud/axios": "~2.5.2",
3737
"@nextcloud/capabilities": "^1.2.1",

src/store/modules/object.js

Lines changed: 0 additions & 65 deletions
This file was deleted.

0 commit comments

Comments
 (0)