From 25909fa9d6ba067d27a5eabe1e62af2a90d1593a Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 10 May 2026 09:38:24 +0200 Subject: [PATCH 1/2] docs(openspec): add softwarecatalog-store-migration change Captures the spec that softwarecatalog's OpenRegister-CRUD store at src/store/modules/object.js MUST be created via createObjectStore + plugins (already true since PR #189), and that the four remaining vanilla defineStore modules (navigation/settings/catalog/organisatie) hold non-OpenRegister state and are exempt. Flags two lib gaps: - @resolve: sentinel resolution unimplemented in nextcloud-vue - liveUpdatesPlugin available but not wired (out-of-scope here) References: - Project memory rule feedback_store-pattern.md - Decidesk #162 (canonical failure case) - nextcloud-vue/openspec/changes/manifest-resolve-sentinel/ --- .../softwarecatalog-store-migration/design.md | 160 ++++++++++++++++++ .../proposal.md | 111 ++++++++++++ .../softwarecatalog-store-migration/spec.md | 98 +++++++++++ .../softwarecatalog-store-migration/tasks.md | 56 ++++++ 4 files changed, 425 insertions(+) create mode 100644 openspec/changes/softwarecatalog-store-migration/design.md create mode 100644 openspec/changes/softwarecatalog-store-migration/proposal.md create mode 100644 openspec/changes/softwarecatalog-store-migration/specs/softwarecatalog-store-migration/spec.md create mode 100644 openspec/changes/softwarecatalog-store-migration/tasks.md diff --git a/openspec/changes/softwarecatalog-store-migration/design.md b/openspec/changes/softwarecatalog-store-migration/design.md new file mode 100644 index 00000000..414bcb95 --- /dev/null +++ b/openspec/changes/softwarecatalog-store-migration/design.md @@ -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. diff --git a/openspec/changes/softwarecatalog-store-migration/proposal.md b/openspec/changes/softwarecatalog-store-migration/proposal.md new file mode 100644 index 00000000..ecc701c9 --- /dev/null +++ b/openspec/changes/softwarecatalog-store-migration/proposal.md @@ -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 diff --git a/openspec/changes/softwarecatalog-store-migration/specs/softwarecatalog-store-migration/spec.md b/openspec/changes/softwarecatalog-store-migration/specs/softwarecatalog-store-migration/spec.md new file mode 100644 index 00000000..4d3c0749 --- /dev/null +++ b/openspec/changes/softwarecatalog-store-migration/specs/softwarecatalog-store-migration/spec.md @@ -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 diff --git a/openspec/changes/softwarecatalog-store-migration/tasks.md b/openspec/changes/softwarecatalog-store-migration/tasks.md new file mode 100644 index 00000000..d7edf8c9 --- /dev/null +++ b/openspec/changes/softwarecatalog-store-migration/tasks.md @@ -0,0 +1,56 @@ +# Tasks — SoftwareCatalog store migration + +## 1. Spec capture + +- [x] 1.1 Add `specs/softwarecatalog-store-migration/spec.md` with the + two MUST rules (createObjectStore for OR-CRUD; plugins for app + extensions). + +## 2. State verification + +- [x] 2.1 Walk `src/store/`. Confirm `modules/object.js` uses + `createObjectStore('object', { plugins: [filesPlugin(), + auditTrailsPlugin(), relationsPlugin(), softwarecatalogPlugin()] + })`. Confirm it keeps store ID `'object'` to preserve all 41 + importers. +- [x] 2.2 Inspect `plugins/softwarecatalogPlugin.js` and verify it + follows the lib's plugin shape (`{ name, state, getters, actions }`) + and uses the lib's `buildHeaders` / `buildQueryString`. +- [x] 2.3 Walk the four vanilla `defineStore` modules + (`navigation`, `settings`, `catalog`, `organisatie`) and document + in `design.md` that they hold non-OpenRegister state. +- [x] 2.4 Grep all `store/` consumers (`from '../store/store'` etc.). + Count: 32 import statements across 30 files. Spot-check three + random importers' usage matches the + base+softwarecatalogPlugin surface. + +## 3. Lint cleanup + +- [x] 3.1 Clear the four `jsdoc/no-defaults` warnings in + `softwarecatalogPlugin.js` (param-default → bracket + notation in JSDoc). + +## 4. Lib-gap documentation + +- [x] 4.1 Document `@resolve:` sentinel as an unimplemented lib + feature (link to `nextcloud-vue/openspec/changes/manifest-resolve-sentinel/`). +- [x] 4.2 Document `liveUpdatesPlugin` as available but not wired. + +## 5. Validation + +- [x] 5.1 `npx eslint src` — zero errors after lint cleanup. +- [x] 5.2 `node tests/validate-manifest.js` — pre-existing + `ajv-formats` failure on the base branch; documented in + `design.md` as out-of-scope. +- [x] 5.3 `npx webpack --mode production` — succeeds. + +## 6. Out-of-scope follow-ups + +- [ ] 6.1 (FOLLOW-UP) Wire `liveUpdatesPlugin` once the backend + SSE/WS endpoint is available. Owner: TBD. +- [ ] 6.2 (FOLLOW-UP, lib) Land `manifest-resolve-sentinel` in + `@conduction/nextcloud-vue` so manifest `@resolve:` + values are substituted before validation. Owner: lib team. +- [ ] 6.3 (FOLLOW-UP) Investigate fixing + `tests/validate-manifest.js` `ajv-formats` initialisation + bug — pre-existing on `development`. From f7ecaf6e148f26069671b918fd31dc4573a72821 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Sun, 10 May 2026 09:38:33 +0200 Subject: [PATCH 2/2] chore(store): clear jsdoc/no-defaults warnings in softwarecatalogPlugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESLint flagged four `@param {Type} [name=null]` JSDoc shapes as violations of `jsdoc/no-defaults`. The annotation is meant to declare optionality (`[name]`) — defaults belong in the description text. Rewriting the four offenders without semantic change. --- src/store/plugins/softwarecatalogPlugin.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/store/plugins/softwarecatalogPlugin.js b/src/store/plugins/softwarecatalogPlugin.js index 254ed2e4..e009a73c 100644 --- a/src/store/plugins/softwarecatalogPlugin.js +++ b/src/store/plugins/softwarecatalogPlugin.js @@ -29,7 +29,7 @@ function extractId(value) { * Build an API URL for an object using its @self metadata. * * @param {object} objectItem Object with @self metadata - * @param {string} [action=null] Optional action endpoint (publish, lock, etc.) + * @param {string} [action] Optional action endpoint (publish, lock, etc.). Defaults to null. * @return {string} The constructed URL */ function buildObjectUrl(objectItem, action = null) { @@ -777,8 +777,8 @@ export function softwarecatalogPlugin() { * Lock an object. * * @param {object} objectItem Object to lock - * @param {string} [process=null] Process name - * @param {number} [duration=null] Duration in seconds + * @param {string} [process] Process name. Defaults to null. + * @param {number} [duration] Duration in seconds. Defaults to null. * @return {Promise} Updated object */ async lockObject(objectItem, process = null, duration = null) { @@ -869,7 +869,7 @@ export function softwarecatalogPlugin() { * * @param {Array} objects Objects to process * @param {Function} operation Per-object operation function - * @param {Function} [onProgress=null] Progress callback + * @param {Function} [onProgress] Progress callback. Defaults to null. * @return {Promise<{successful: Array, failed: Array}>} Results */ async _runMassOperation(objects, operation, onProgress = null) {