Skip to content

feat(integration-registry): 16 leaf providers (mega-leaf — depends on umbrella stack)#1514

Merged
rubenvdlinde merged 14 commits into
developmentfrom
feat/integration-leaves-fleet
May 18, 2026
Merged

feat(integration-registry): 16 leaf providers (mega-leaf — depends on umbrella stack)#1514
rubenvdlinde merged 14 commits into
developmentfrom
feat/integration-leaves-fleet

Conversation

@rubenvdlinde
Copy link
Copy Markdown
Contributor

Summary

Ships the PHP IntegrationProvider classes for every leaf currently spec'd alongside the pluggable integration registry — 16 new providers + DI wiring + smoke test — so the registry surfaces every documented integration once the umbrella stack lands.

Blocked behind: #1467#1468#1469#1473#1475#1490#1493 (umbrella + xwiki). This branch is based on `feature/1326/integration-xwiki` so it stacks on top of the umbrella; rebase onto `development` after those merge.

What's in the box (16 leaves)

Leaf Type Behaviour
Calendar backend-shipped wraps `CalendarEventService` — list + delete (composite `calendarId/eventUri`).
Contacts backend-shipped wraps `ContactService` — list + update role + unlink.
Deck backend-shipped wraps `DeckCardService` — list + linkOrCreateCard + unlinkCard.
Email backend-shipped wraps `EmailService` — list (limit/page filters) + linkEmail + unlinkEmail.
OpenProject external mirrors XwikiProvider; full CRUD via `ExternalIntegrationRouter` against OpenConnector `openproject` source; HAL+JSON envelope handled.
Activity NC-app stub `query-time`; gates on `activity`; `list()=[]` until activity-filter service ships.
Analytics NC-app stub `link-table`; gates on `analytics`.
Bookmarks NC-app stub `link-table`; gates on `bookmarks`.
Collectives NC-app stub `link-table`; gates on `collectives`.
Cospend NC-app stub `link-table`; gates on `cospend`.
Flow NC-app stub `link-table`; gates on `workflowengine`.
Forms NC-app stub `link-table`; gates on `forms`.
Maps NC-app stub `link-table`; gates on `maps`.
Photos NC-app stub `link-table`; gates on `photos`.
Polls NC-app stub `link-table`; gates on `polls`.
Shares NC-core `query-time`; no requiredApp; delete delegates to `IShareManager::deleteShare()`.
Talk NC-app stub `link-table`; gates on `spreed`.
Time-tracker NC-app stub `link-table`; gates on `timemanager`.

"NC-app stub" = the Provider class registers the registry surface (sidebar slot, admin UI presence, OCS caps entry) gated on `IAppManager`, but `list()` returns `[]` until the wrapped service + link table land in per-leaf follow-up PRs. Mutation methods inherit `NotImplementedException` from `AbstractIntegrationProvider`. This is intentional honest scaffolding rather than fake-shipping CRUD that doesn't work.

What's NOT in this PR (deferred to per-leaf follow-ups)

  • Backend services + link-table migrations for the 12 stub leaves.
  • Vue components (CnXxxTab / CnXxxCard) — live in `@conduction/nextcloud-vue`, separate repo.
  • OpenAPI regen, NL/EN i18n catalogues, parity gate updates.
  • The 4 sub-resource controller refactors per spec (`*Controller` consolidations).

Each leaf's `tasks.md` has Provider-creation + DI-registration boxes ticked; the remainder stays open.

Wiring

`lib/AppInfo/Application.php`:

  • `registerBuiltinIntegrationProviders()` — 16 new `registerService()` blocks.
  • `bootBuiltinIntegrationProviders()` — provider classes added to the boot list so `IntegrationRegistry::addProvider()` picks them up.

Tests

`tests/Unit/Service/Integration/Providers/LeafProvidersMetadataTest.php`:

  • Per-provider metadata + delegation assertions for the 4 backend-shipped leaves + OpenProject + Shares.
  • Data-provider-driven contract test across all 12 greenfield stubs (id, label, icon, group, requiredApp, storage, isEnabled-gate, list()=[] stub).
  • Mirrors the structure of `BuiltinProvidersMetadataTest` to keep the umbrella's convention.

Quality

  • All 18 new files pass PHPCS strict (0 errors).
  • 1 pre-existing warning in XwikiProvider.php line 476 (not touched).
  • 3 pre-existing errors in Application.php lines 316/870/1291 from the umbrella PR (intentionally left for the umbrella reviewer).

Test plan

  • CI green on the stack (depends on umbrella PRs merging first).
  • PHPUnit `LeafProvidersMetadataTest` runs green inside the NC container.
  • After merge: install various NC apps (Calendar, Contacts, Deck, Mail, Bookmarks, etc.) and verify each integration appears in OR's admin UI Integrations page + the OCS capabilities response.
  • Uninstall an app and confirm the corresponding leaf's `isEnabled()` flips false → sidebar tab disappears.
  • OpenProject: configure OpenConnector source `openproject`; verify list() returns the user's WPs through the router.

Follow-ups

One per-leaf PR per stub will flesh out the wrapped service + link-table migration + Vue components + i18n + acceptance verification. They can land in any order once this merges.

rubenvdlinde and others added 13 commits May 11, 2026 14:02
… external router (umbrella PR 1/N)

First slice of the pluggable-integration-registry umbrella (#1307).
Lands the foundation that every leaf change in waves W1-W3 depends on:
the provider contract, the registry, external-routing plumbing, and
the query-time storage-strategy helper. No built-in provider
migration yet (tasks 12-17); no schema validator refactor yet (tasks
7-11); no frontend wiring yet (tasks 25+).

Tasks completed: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6 (6/69 of the umbrella).

What this adds:

- lib/Service/Integration/IntegrationProvider.php — 15-method
  interface per design.md normative contract. Storage strategies:
  magic-column / link-table / external / query-time.
- lib/Service/Integration/AbstractIntegrationProvider.php — base
  class with sensible defaults; mutation methods (get / create /
  update / delete) default to NotImplementedException so list-only
  and query-time providers don't have to spell that out.
- lib/Service/Integration/IntegrationRegistry.php — explicit
  addProvider() registration with collision detection (AD-13) and
  external-source rejection (AD-4). Spec called for DI-tag-based
  discovery, but modern Nextcloud doesn't expose
  IAppContainer::queryAll(<tag>) as public API. Switched to explicit
  addProvider() at app bootstrap with identical semantics —
  documented in the file's class docblock + in tasks.md.
- lib/Service/Integration/ExternalIntegrationRouter.php — dispatches
  storage='external' CRUD calls through OpenConnector. Surfaces
  failures via ProviderUnavailableException with a 3-way cause
  classification (openconnector-down / openconnector-source-missing
  / upstream-service-down) per AD-23. Includes probe() for admin
  health reporting.
- lib/Service/Integration/QueryTimeContract.php — codifies the 2 s
  render timeout (AD-22) and the HTTP 501 envelope builder for the
  ObjectsController to consume when tasks 7-11 refactor it.
- lib/Exception/NotImplementedException.php — thrown by query-time /
  list-only providers' unsupported CRUD methods.
- lib/Exception/ProviderUnavailableException.php — carries the
  3-way cause classification + a getDetails() helper that produces
  the `{cause: …}` payload the UI renders.
- lib/AppInfo/Application.php — new private
  registerIntegrationRegistry() phase wires the registry and the
  router as shared per-request singletons.
- tests/Unit/Service/Integration/*Test.php — 25 unit tests across 4
  classes covering addProvider() validation (collision + external
  source), getEnabled() filtering, AbstractIntegrationProvider's
  NotImplementedException defaults, ExternalIntegrationRouter's
  failure-mode classification, and QueryTimeContract's HTTP envelope
  shape. All green.

What's still pending (rest of the umbrella):

- Schema validator refactor (tasks 7-11): Schema::validateLinkedTypesValue
  consumes IntegrationRegistry::listIds; deprecation of
  VALID_LINKED_TYPES + LinkedEntityService::TYPE_COLUMN_MAP.
- Built-in providers (tasks 12-17): 5 BuiltinProviders/*Provider.php
  classes wrapping the existing files/notes/tasks/tags/audit-trail
  integrations.
- Routes + controller + capabilities (tasks 18-22): IntegrationsController,
  ObjectsController sub-resource dispatch via registry, OCS
  capabilities block.
- Admin UI + frontend registry + 3 missing widgets + CnObjectSidebar /
  CnDashboardPage / CnDetailPage / CnFormDialog / CnDetailGrid refactor
  (tasks 23-46).
- Tests / CI parity gate / scaffold script / ADR + docs / translations
  (tasks 47-67).
- E2E acceptance verification (tasks 68-71).

Spec adjustment: tasks.md and plan.json record the DI-tag → explicit
addProvider() pivot explicitly so the next session (or hydra builder)
doesn't trip on it.

Refs: #1307
…urveillance (umbrella PR 2/N)

Second slice of #1307 — completes tasks 7-11 of the umbrella (Backend
— Schema validator refactor). Builds on PR 1's IntegrationRegistry +
ExternalIntegrationRouter foundation. Stacked PR — base is
feature/1307/pluggable-integration-registry; GitHub will retarget to
development once PR 1 merges.

Tasks completed in this slice (5/69 → cumulative 11/69):

- 2.1 Schema::validateLinkedTypesValue() now consults both
  VALID_LINKED_TYPES (deprecated fallback) AND
  IntegrationRegistry::listIds(). Registry resolved lazily via
  \OC::$server->get() since Schema is a Nextcloud Entity, not a
  service. Falls back to fallback-only when container isn't booted
  (unit tests). AD-5 backwards-compat preserved: existing schemas
  with 'mail' / 'calendar' / 'talk' / 'deck' still validate while
  the leaves land.
- 2.2 VALID_LINKED_TYPES marked @deprecated with pointers to the
  registry + cleanup follow-up.
- 2.3 LinkedEntityService::TYPE_COLUMN_MAP marked @deprecated.
- 2.4 PropertyReferenceTypeValidator — new opt-in service that
  validates the `referenceType: <integration-id>` marker on schema
  property definitions (AD-18). Kept as a standalone validator so
  existing schema validation paths don't change; CnFormDialog /
  CnDetailGrid wire it in tasks 25-46.
- 2.5 LogDanglingLinkedTypes repair step — registered under <install>
  + <post-migration> in info.xml. Scans every schema, logs WARNING
  for any linkedTypes value not yet registered. Strictly
  informational; never throws, never modifies data.

Net new files:
- lib/Service/Integration/PropertyReferenceTypeValidator.php
- lib/Repair/LogDanglingLinkedTypes.php
- tests/Unit/Service/Integration/PropertyReferenceTypeValidatorTest.php

Modified:
- lib/Db/Schema.php — validator + deprecation note
- lib/Service/LinkedEntityService.php — deprecation note on
  TYPE_COLUMN_MAP
- lib/Service/Integration/IntegrationRegistry.php — added
  isValidIntegrationId() helper consumed by the new validator
- lib/AppInfo/Application.php — DI bindings for the new validator
  service + the repair step
- appinfo/info.xml — repair-steps block adds LogDanglingLinkedTypes
- openspec/changes/pluggable-integration-registry/tasks.md
- openspec/changes/pluggable-integration-registry/plan.json

Unit tests:
- 34 tests, 48 assertions — all green (9 new for
  PropertyReferenceTypeValidator + the 25 from PR 1)

Built-in providers (tasks 12-17) intentionally deferred to PR 3 to
keep this PR reviewable as one coherent slice (the validator + the
dangling-id surveillance form one story).

Refs: #1307
…tions (umbrella PR 3/N)

Third slice of #1307 — completes tasks 12-17 of the umbrella. Stacked
on PR #1468 (schema validator refactor), which is itself stacked on
PR #1467 (foundation).

Tasks completed in this slice (6/69 → cumulative 17/69):

- 3.1 FilesProvider wraps FileService (magic-column).
- 3.2 NotesProvider wraps NoteService (link-table, full CRUD).
- 3.3 TasksProvider wraps TaskService (link-table, CalDAV, full CRUD;
  composite {calendarId}/{taskUri} entity ids).
- 3.4 TagsProvider wraps the NC system tag manager (link-table; read
  via ISystemTagObjectMapper::getTagIdsForObjects).
- 3.5 AuditTrailProvider wraps AuditTrailMapper (query-time, AD-22 —
  read-only by design; mutation methods inherit NotImplementedException
  from AbstractIntegrationProvider).
- 3.6 All five register via addProvider() at Application::boot() time
  through a new bootBuiltinIntegrationProviders() helper. The DI
  bindings live in registerBuiltinIntegrationProviders() so each
  provider's wrapped service is resolved lazily.

Spec deviations flagged inline in tasks.md + plan.json:

- FilesProvider + TagsProvider keep their write paths at the existing
  FileController / TagsController routes for now. Mutation methods
  throw NotImplementedException with a pointer to the umbrella's
  controller refactor (tasks 18-22). Users see the right tab; the
  underlying write API stays where it is until the controller layer
  catches up. Surface change is zero.
- AuditTrailProvider intentionally inherits the abstract base's
  mutation defaults — audit-trail entries are immutable by
  construction; per AD-22 query-time providers MUST throw on
  create/update/delete.
- The referenceType: <id> declarations the spec calls for on the
  frontend registry side are deferred to tasks 25-30 (when each
  provider gets its JS registry counterpart). The PHP-side providers
  are complete.

Net new files:
- lib/Service/Integration/BuiltinProviders/FilesProvider.php
- lib/Service/Integration/BuiltinProviders/NotesProvider.php
- lib/Service/Integration/BuiltinProviders/TasksProvider.php
- lib/Service/Integration/BuiltinProviders/TagsProvider.php
- lib/Service/Integration/BuiltinProviders/AuditTrailProvider.php
- tests/Unit/Service/Integration/BuiltinProvidersMetadataTest.php

Modified:
- lib/AppInfo/Application.php — new registerBuiltinIntegrationProviders
  + bootBuiltinIntegrationProviders helpers
- openspec/changes/pluggable-integration-registry/tasks.md
- openspec/changes/pluggable-integration-registry/plan.json

Unit tests:
- 45 tests, 80 assertions — all green (11 new for the 5 providers + the
  34 from PR 1 & 2)

Refs: #1307
…brella PR 4/N)

Fourth slice of #1307 — completes tasks 18-22 of the umbrella (Backend
— Routes, Controller, Capabilities). Stacked on PR #1469 (built-in
providers), itself stacked on #1468 (schema validator) and #1467
(foundation).

Tasks completed in this slice (5/69 → cumulative 22/69):

- 4.1 IntegrationsController — read-only API over the registry.
  GET /api/integrations (with `group` and `enabled` filter params),
  GET /api/integrations/{id}. Role-redacted descriptor per AD-17 —
  every authed user sees public fields (id, label, icon, group,
  enabled, storageStrategy, surfaces); admins additionally get
  requiresPermission, openConnectorSource, authStatus. Non-admin
  fields are omitted (not null-stubbed) so absence matches
  non-existence.

- 4.2 Object-scoped sub-resource dispatch via the registry — new
  dedicated ObjectIntegrationsController owns
  /api/objects/{register}/{schema}/{id}/integrations/{integrationId}[/{entityId}]
  (GET / POST / PUT / DELETE). Additive — ObjectsController (2400+
  lines) stays untouched. Error translation per AD-22 / AD-23:
    NotImplementedException → 501 with QueryTimeContract envelope
    ProviderUnavailableException → 503 with details.cause payload
    unknown integration id → 404
    other Throwable → 500 (real exception logged, generic message
    returned per ADR-005).

- 4.3 Routes wired in appinfo/routes.php — both the discovery API
  and the object sub-resource dispatch.

- 4.4 IntegrationsCapability — surfaces the registry through
  /ocs/v2.php/cloud/capabilities, role-redacted per AD-17. Spec said
  'Update lib/Service/CapabilitiesService.php'; OR's capability
  pattern uses one ICapability class per concern (see UrnCapability),
  so the new file lives in lib/Capabilities/. Same end shape,
  idiomatic structure.

- 4.5 Registered via $context->registerCapability() in
  Application::register() — mirrors the existing UrnCapability
  pattern. No appinfo/info.xml change needed (OR doesn't carry
  capability declarations there).

Net new files:
- lib/Controller/IntegrationsController.php
- lib/Controller/ObjectIntegrationsController.php
- lib/Capabilities/IntegrationsCapability.php
- tests/Unit/Controller/ObjectIntegrationsControllerTest.php

Modified:
- appinfo/routes.php — 7 new routes (2 discovery + 5 sub-resource)
- lib/AppInfo/Application.php — DI bindings for the new controllers
  and the capability; registerCapability() call
- openspec/changes/pluggable-integration-registry/tasks.md
- openspec/changes/pluggable-integration-registry/plan.json

Unit tests:
- 52 tests, 95 assertions — all green inside the openregister-postgres
  dev container (7 new for ObjectIntegrationsController dispatch +
  error translation; the 45 from PRs 1-3 still pass)

Spec deviations flagged inline:
1. Sub-resource dispatch lives in a NEW ObjectIntegrationsController
   rather than refactoring the 2400-line ObjectsController. Additive,
   zero regression risk on existing routes.
2. Capability advertisement uses one ICapability class
   (IntegrationsCapability) registered via registerCapability(),
   mirroring UrnCapability. Spec called for editing
   CapabilitiesService — OR doesn't have that file as the canonical
   integration point; the per-concern ICapability pattern is
   idiomatic for this codebase.
3. info.xml capability section unchanged — OR registers capabilities
   in code, not XML.

Refs: #1307
…PR 5/N)

Renders the OpenRegister → "Integrations" admin section with a row per
registered IntegrationProvider:

  - id / label / group / storage strategy
  - required Nextcloud app + isInstalled status
  - isEnabled() result
  - authStatus / status / message from probe() (external) or health() (native)
  - Configure deep-link into OpenConnector's source-edit screen
    (external providers only)
  - Test connection URL (external providers only)

Per AD-15, OpenRegister hosts the unified surface; credential flows
stay in OpenConnector — this page only links out. Native providers
report through provider->health(); external providers route through
ExternalIntegrationRouter::probe() so failure messages match what
runtime callers see.

Implements tasks 5.1–5.3.

Files:
  - lib/Settings/IntegrationsAdminSettings.php (new) — ISettings
    implementation, builds rendered rows from IntegrationRegistry
  - templates/settings/integrations-admin.php (new) — server-rendered
    table with status badges (ok / unavailable / unknown), translation
    via $l throughout
  - appinfo/info.xml — registers the additional <admin> entry
  - lib/AppInfo/Application.php — DI binding for IntegrationsAdminSettings
  - tests/Unit/Settings/IntegrationsAdminSettingsTest.php (new) — 5 tests
    covering section/priority stability, row rendering, configure-URL
    presence for external providers, absence for native providers, and
    router routing for external probes
  - openspec/changes/pluggable-integration-registry/{tasks.md,plan.json}
    — mark 5.1–5.3 done

Spec deviation (documented in tasks.md):
  Spec called for "edit CapabilitiesService" — there's no such
  integration point on master. Capability is exposed via the idiomatic
  ICapability class added in umbrella PR 4 (IntegrationsCapability),
  not via this settings change.

Tests:
  ./vendor/bin/phpunit --configuration phpunit-unit.xml \
    tests/Unit/Settings/IntegrationsAdminSettingsTest.php
  -> 5 tests, 21 assertions, OK
…g sync (umbrella PR 10b)

The openregister-side companion to the nextcloud-vue PRs (#202/#204/
#209/#210/#211). No production-code change here — docs, a scaffold
script, and the umbrella's task-tracking sync.

  - docs/Integrations/pluggable-integration-registry.md (new) —
    "How to add an integration": the five built-ins table, storage
    strategies, the scaffold quickstart, a worked walkthrough (PHP
    provider → JS tab+widget → registration → surfaces light up →
    admin/health), and a reference section
  - scripts/scaffold-integration.sh (new) — `scaffold-integration.sh
    <id> [<Label>]` generates `openspec/changes/integration-<id>/`
    with proposal.md, tasks.md (within the ADR-028 cap), hydra.json
    (depends_on: pluggable-integration-registry), a PHP provider stub
    and a JS registration stub. Validates the id is kebab-case;
    derives the PascalCase class name; refuses to overwrite.
  - README.md — "Integrations" section now leads with the pluggable
    integration registry, pointing at the developer guide
  - openspec/changes/pluggable-integration-registry/{tasks.md,
    plan.json} — mark tasks 6.x / 7.x / 8.x / 9.x / 10.x / 12.1-12.2-
    12.4 / 13.x / 14.x / 15.x done, with the cross-repo PR references
    inline; documents the spec deviations (listByGroup → inline
    filter; check-integration-parity.js not .sh; parity step folded
    into code-quality.yml not a separate workflow); flags the two
    explicitly-deferred items (hydra quality-gate hook 12.3, ADR-019
    authoring — both separate hydra-repo PRs)

Implements tasks 13.1-13.2 and 14.2-14.3 of the umbrella, and syncs
the tracking for everything landed in the nextcloud-vue PRs.

Smoke-tested: `scaffold-integration.sh scaffoldtest "Scaffold Test"`
generates the expected tree; plan.json stays valid JSON.
…ed (leaf, backend)

The PHP half of the `integration-xwiki` leaf (issue #1326,
depends_on: pluggable-integration-registry) — the worked
external-storage example. `XwikiProvider` declares storage `external`,
group `external`, OpenConnector source `xwiki`, and delegates all CRUD
to `ExternalIntegrationRouter` (AD-4) — it carries no HTTP client and
no credentials; those live on the OpenConnector source (HTTP Basic or
OAuth2, customer-dependent — AD-15).

Per the leaf design.md:
  - AD-1: `get()` round-trips the page's rendered HTML under `content`;
    the @conduction/nextcloud-vue widget strips it to text + ~500 chars
    for the detail-page preview — macros are never executed in NC.
  - AD-2: `create()` passes `reference` through (a full XWiki URL or a
    `Space.Page` path); the OpenConnector source resolves both to a
    canonical reference.
  - AD-3: `normalizeRow()` ensures every row carries a `breadcrumb`
    (from the source, or derived from space + title) so the UI can
    disambiguate same-titled pages in different spaces.

Files:
  - lib/Service/Integration/Providers/XwikiProvider.php (new) —
    metadata getters + authRequirements (type: external, configuredVia:
    openconnector) + isEnabled (mirrors IAppManager::isInstalled
    'openconnector') + list/get/create/update/delete (all via
    router->call with the {register, schema, object} context) + health
    (defers to router->probe) + normalizeList/normalizeRow shaping
  - lib/AppInfo/Application.php — DI service registration for
    XwikiProvider + add it to bootBuiltinIntegrationProviders()'s list
    so it self-registers with the IntegrationRegistry at boot
  - docs/Integrations/xwiki-openconnector-source.yaml (new) — the
    OpenConnector source template to import (the repo's config/ dir is
    gitignored, so it lives under docs/Integrations/)
  - docs/Integrations/xwiki.md (new) — user-facing setup + usage guide
  - tests/Unit/Service/Integration/Providers/XwikiProviderTest.php (new)
    — 10 tests, 38 assertions: metadata matches the leaf spec,
    authRequirements shape, isEnabled mirrors OpenConnector install,
    list/get/create/update/delete delegate with the right method +
    path + context and normalise rows (breadcrumb fallback, id/title/
    space/page/url/content mapping, bare-array response), health defers
    to probe

Quality (verified in the nextcloud container):
  - composer phpcs (phpcs.xml) — 0 errors, 0 warnings
  - composer phpmd (phpmd.xml) — clean
  - composer psalm — no errors
  - phpunit (phpunit-unit.xml) XwikiProviderTest — 10 passed, 38 assertions
  - php -l Application.php — no syntax errors
…+ spec deviations

Syncs the integration-xwiki leaf tasks.md to reality:
  - backend (XwikiProvider + DI registration + source-config template
    + user guide + unit tests) — done in this branch
  - frontend (CnXwikiTab + CnXwikiCard + registration + tests) — done
    in ConductionNL/nextcloud-vue#213
  - acceptance: link-by-URL / reference-property / "macros not
    executed in preview" are covered by the component tests; the live
    E2E (against a running XWiki via an OpenConnector source) is
    deferred until the umbrella + leaf PRs merge and deploy

Spec deviations recorded inline:
  - `requiredApp` is `'openconnector'` (not `null`) — more accurate
    for the admin UI: the integration genuinely needs that app
  - the OpenConnector source-config template lives at
    `docs/Integrations/xwiki-openconnector-source.yaml`, not
    `config/openconnector-sources/xwiki.yaml` (the repo's `config/`
    dir is gitignored)
  - the provider is registered via `addProvider()` at boot (not a DI
    tag) — consistent with the umbrella's registration mechanism
  - `check-integration-parity.js` (Node, repo convention) not `.sh`
…ve XWiki REST verification

Spun up xwiki:lts 17.10 locally, created a test page via REST
(PUT /rest/wikis/xwiki/spaces/Sandbox/pages/IntegrationTest) and
inspected the page representation. Findings folded into the source
template:

  - XWiki is deployed at the ROOT context here — REST is at `/rest/`,
    not `/xwiki/rest/`. (The template's `location` is the wiki's REST
    base URL, so this is just a deployment note.)
  - The REST page representation's `content` field is RAW xwiki/2.1
    syntax, NOT rendered HTML. For a clean AD-1 text preview the
    source should fetch the rendered body from
    `GET /bin/get/{Space}/{Page}?xpage=plain` (XWiki renders macros
    server-side in its own sandbox — never inside Nextcloud) and map
    that to `content`. Mapping `content` straight from the REST field
    is still safe (macro markup is inert text) — documented both ways.
  - XWiki exposes a native breadcrumb at `hierarchy.items[].label`
    ("xwiki" / "Sandbox" / "IntegrationTest") — map `breadcrumb` from
    it (AD-3). Without it XwikiProvider derives a coarse one from
    space + title.
  - Confirmed the other mappings: `id` → `reference`
    ("xwiki:Space.Page"), `title`, `space`, `name` → `page`,
    `xwikiAbsoluteUrl` → `url`.

No PHP change needed — XwikiProvider::normalizeRow() already reads
`breadcrumb` (with the space+title fallback) and `content` (with the
`renderedContent` fallback), so it works against either mapping.
…s-built implementation

Brings the leaf's design.md and proposal.md in line with what
actually shipped (tasks.md was already synced):

  - design.md: AD-1 records the verified XWiki REST shape (the REST
    `content` field is raw xwiki/2.1 syntax — the rendered body comes
    from `/bin/get/{Space}/{Page}?xpage=plain`, macros run server-side
    in XWiki's sandbox, never in NC). AD-3 records that `breadcrumb`
    maps from XWiki's native `hierarchy.items[].label`. "Files
    Affected" updated to the real paths/structure. New "Implementation
    deviations" table (requiredApp `openconnector` not `null`; source
    template under `docs/Integrations/` not `config/`; `addProvider()`
    at boot not a DI tag; `check-integration-parity.js` not `.sh`).
    Softened the "established by integration-openproject" line — the
    external-storage machinery comes from the umbrella, this is the
    first external leaf.
  - proposal.md: `Required NC app` → `openconnector` (with the
    rationale); acceptance criteria checked off against the component/
    unit tests that cover them, with the live runtime check called out
    as deploy-gated.

Doc-only; no code change.
… / searchResults)

normalizeList() previously only un-wrapped `{ results: [...] }` / `{ items: [...] }`
(a purpose-built OpenConnector source) and otherwise iterated the response's *values*
as if they were rows — which mangles XWiki's own REST shapes. Now it also recognises
`pageSummaries` (space listing: GET /rest/wikis/<wiki>/spaces/<Space>/pages) and
`searchResults` (search), uses a bare list as-is, and returns an empty list for an
assoc envelope with no rows key (instead of garbage rows). normalizeRow() gains a
last-resort `url` fallback that pulls the `rel=".../rel/page"` href out of XWiki's
`links` array (present on page summaries / search hits) when the source didn't map a
flat URL field. This lets the `xwiki` integration work against an OpenConnector source
pointed straight at XWiki's REST API, not just a custom proxy endpoint.

Tests: +2 (raw pageSummaries envelope incl. the links-based url fallback; assoc
envelope with no rows key → []).

Relates to #1307 (umbrella) / #1326 (xwiki leaf).
…t: application/json

Three follow-ups that make the `xwiki` provider work against a stock
OpenConnector source (verified end-to-end against a live XWiki):

- ExternalIntegrationRouter::decodeResponse() — OpenConnector's CallService
  returns a CallLog whose getResponse() is { statusCode, headers, body, encoding };
  the upstream payload is the (usually JSON) `body` string. Unwrap + JSON-decode
  it (base64 bodies decoded first) instead of serialising the whole CallLog —
  previously the provider only ever saw the CallLog metadata, so list() returned [].
- ExternalIntegrationRouter::assertUpstreamOk() — a >= 400 upstream status (carried
  on the CallLog) now throws ProviderUnavailableException rather than letting an
  error page leak through as rows; 401/403 → the new CAUSE_PROVIDER_AUTH so the
  UI shows the reconnect banner.
- XwikiProvider — every call now carries `Accept: application/json` (XWiki's REST
  API negotiates XML by default; writes also declare a JSON Content-Type). A
  purpose-built source that already pins these is unaffected.
- ProviderUnavailableException::CAUSE_PROVIDER_AUTH constant added (the JS side —
  CnXwikiTab.degradedMessage — already treats `provider-auth` as a reconnect cause).

Tests: ExternalIntegrationRouterTest +4 (CallLog body unwrap, base64 body, 401→provider-auth,
500→upstream-down); XwikiProviderTest assertions updated for the new request headers.
phpunit: 21 green.

Relates to #1307 (umbrella) / #1326 (xwiki leaf) / ConductionNL/openconnector#755.
… NC-app stubs + OpenProject) — mega-leaf

Implements the PHP IntegrationProvider classes for every leaf currently
spec'd alongside the umbrella registry (xwiki was already in place). The
mega-leaf branches off feature/1326/integration-xwiki so the leaves can
land together once the umbrella stack (#1467#1490) merges to
development.

Per-leaf depth ships what's tractable in one mega-PR; the rest is
honest TODO that follow-up leaves can flesh out without rewriting:

Backend-shipped (Calendar, Contacts, Deck, Email) — wrap existing
services through the registry contract:
  - CalendarProvider → CalendarEventService (list, delete via
    "calendarId/eventUri" composite); create/update remain on the
    dedicated CalendarEventsController.
  - ContactsProvider → ContactService (list, update role, unlink);
    create() inherits NotImplementedException since the link-existing-
    vs-create-new flows are owned by ContactsController.
  - DeckProvider → DeckCardService (list, linkOrCreateCard, unlinkCard).
  - EmailProvider → EmailService (list with limit/page filters,
    linkEmail, unlinkEmail).

External (OpenProject) — mirrors XwikiProvider, routes all CRUD through
ExternalIntegrationRouter against the OpenConnector "openproject"
source. Surfaces HAL+JSON envelope rows from _embedded.elements and
normalises to the {id, title, status, url} contract.

NC-app-backed greenfield (Activity, Analytics, Bookmarks, Collectives,
Cospend, Flow, Forms, Maps, Photos, Polls, Talk, Time-tracker) — provider
registers the registry surface today (id, label, icon, group,
requiredApp, storage), gates on IAppManager::isInstalled() for the
named app, and returns [] from list(); mutation methods inherit
NotImplementedException from AbstractIntegrationProvider until the
wrapped service + link table ship in per-leaf follow-ups. health()
reports unavailable when the NC app is missing.

SharesProvider — NC core (no required app), query-time storage; delete()
delegates to OCP\Share\IManager::deleteShare(); list() stub until the
file-link → share-filter glue lands.

Wiring: lib/AppInfo/Application.php
  - registerBuiltinIntegrationProviders() — registers all 16 new
    provider services in the DI container.
  - bootBuiltinIntegrationProviders() — adds them to the providerClasses
    list so they self-register with the IntegrationRegistry on boot.

Tests: tests/Unit/Service/Integration/Providers/LeafProvidersMetadataTest.php
  - Per-provider contract metadata + delegation assertions for Calendar,
    Contacts, Deck, Email, OpenProject, Shares.
  - Data-provider-driven contract test covering all 12 greenfield stubs
    (asserts id/label/icon/group/requiredApp/storage and the
    list()=[] + isEnabled gate behaviour).

Tasks.md updates: ticks the Provider-creation + DI-registration
checkboxes across all 18 integration-* changes (the unit-test +
service/controller/migration/vue tasks remain open for the follow-up
leaves).

PHPCS strict: providers + new test file are clean (0 errors). Three
pre-existing errors remain in Application.php from the umbrella PR
(InlineComment cap-letter + named-param sniff on lines that pre-date
this branch) — left untouched.
@rubenvdlinde
Copy link
Copy Markdown
Contributor Author

Visually verified end-to-end via decidesk (15 May)

The full chain now renders in a dev container after porting this PR's PHP providers + nc-vue#230's Vue registrations + decidesk#205's main.js wiring (Pinia fix) into the running instance.

/ocs/v2.php/cloud/capabilities reports 24 providers — 11 enabled (required app installed), 13 disabled by app-gate (correct behaviour). window.OCA.OpenRegister.integrations.list().length === 24. <CnObjectSidebar :use-registry="true"> mounts 24 tabs (Files / Notes / Tags / Tasks / Audit trail / Shares / Meetings / Contacts / Emails / Chat / Articles / Projects / Bookmarks / Knowledge / Location / Photos / Activity / Analytics / Costs / Cards / Automation / Forms / Polls / Time).

Tab content currently returns 503 because the /api/objects/.../integrations/{id} routes ship in umbrella PR #1473, which hasn't merged yet. Once that's in, the providers from this PR drive the responses.

See decidesk#205 comment for the full verification trace.

…f API Playwright smoke

Two additions on top of the mega-leaf provider PR:

### docs/Integrations/

  - leaf-system.md (NEW) — architectural overview of the pluggable
    integration registry. <LeafGrid> of all 18+5 leaves with metadata
    headers, the three-piece wiring walk-through (PHP provider, JS
    registration, app-side activation), storage strategy reference,
    requiredApp gating, collision policy (AD-13), surface fallback
    (AD-19).

  - 18 per-leaf pages (xwiki rewrite + 17 new) in the design-system's
    'integrations.html' pattern: <LeafCard> metadata header + <Pair>
    system-pair diagram + What-it-does + Setup numbered steps +
    Configuration table + Authentication callout (where relevant) +
    Troubleshooting + Related links.

    Backend-shipped (full content): calendar, contacts, deck, email,
    openproject, shares, xwiki.
    Provider-stub (compact, marks 'pending follow-up'): activity,
    analytics, bookmarks, collectives, cospend, flow, forms, maps,
    photos, polls, talk, time-tracker.

  - Deck.md renamed to deck.md (lowercase for URL slug consistency
    with all other leaves).

  - index.md restructured: 'Leaf integrations' section listed first
    by group, then OR push events, then LLM + automation integrations.

Each per-leaf page uses <LeafCard> + <Pair> from
@conduction/docusaurus-preset (see design-system commit 70a0a65 for
the new LeafCard component).

### tests/e2e/integration-registry.spec.ts

  Playwright smoke covering the OR side of the chain:
   - OCS /cloud/capabilities exposes all 24 providers with shape
   - every provider declares the documented metadata (group, storage,
     surfaces array, authStatus object)
   - 18 per-leaf sub-resource probes:
     GET /api/objects/{r}/{s}/{o}/integrations/{leaf-id} returns
     <500 for every advertised leaf (200 / 503 acceptable; 5xx is a
     provider crash)
   - 200 returns standard envelope shape (results | items | array)
   - 503 carries a structured cause body
   - unknown provider id 4xx (not 5xx)
   - every advertised provider declares all 4 AD-19 surfaces

  Tests skip cleanly when the registry isn't wired (older deploys),
  so the spec is safe to run against partial deploys.

  Verified locally: 31 / 31 tests pass on the dev container.

### Writing style

  All pages follow design-system/CLAUDE.md: short sentences, sentence-
  case headings, no em-dashes, je/jij tone (where pages have NL —
  these are English-only for now), one claim per bullet, banned-word
  list respected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant