Skip to content

Honor annotation: immutability + retention enforcement #1614

@rubenvdlinde

Description

@rubenvdlinde

Problem

Schemas declaring x-openregister-archival (with retention.default + condition-based retention.rules[]) get the annotation silently stripped during import. Result: log-style schemas that are supposed to be immutable + auto-expire are deletable + non-expiring.

Reproducer in ConductionNL/openconnector/lib/Settings/openconnector_register.json:

"call_log": {
  "x-openregister-archival": {
    "retention": {
      "default": "P30D",
      "rules": [
        { "condition": "statusCode < 400", "retention": "PT1H", "reason": "..." },
        { "condition": "statusCode >= 400", "retention": "P30D", "reason": "..." }
      ]
    }
  }
}

On occ app:enable openconnector the import logs:

[OpenRegister.SchemaMapper] Dropped 2 unknown x-openregister-* key(s) on schema "call_log": x-openregister-seed, x-openregister-archival. Typo? See Schema::ANNOTATION_VOCABULARY for the declared keys.

Same for job_log, synchronization_log, synchronization_contract_log.

Schema::ANNOTATION_VOCABULARY (openregister/lib/Db/Schema.php:1853) currently:

private const ANNOTATION_VOCABULARY = [
    'x-openregister-lifecycle',
    'x-openregister-aggregations',
    'x-openregister-calculations',
    'x-openregister-notifications',
    'x-openregister-widgets',
    'x-openregister-relations',
    'x-openregister-processing-activity',
];

x-openregister-archival (and x-openregister-seed) are absent → stripped on line 1697.

Real-world impact

The openconnector dashboard (ConductionNL/openconnector#838) reads call_log / job_log / synchronization_log via OR's groupBy primitive (#1611). Users expect:

If everything worked during a Playwright / Newman journey, the logs (which should be immutable) would still be visible.

Today: logs are deletable like any object, no retention sweep ever runs, so the dashboard can be empty even when integrations actually ran successfully. Reported by Ruben.

Proposed

  1. Add 'x-openregister-archival' to Schema::ANNOTATION_VOCABULARY. Same for 'x-openregister-seed' if it's intended to be honored elsewhere.
  2. Validate the annotation shape on import (retention.default: ISO-8601 duration, retention.rules[i].condition: string, etc.) — same JSON-Schema pattern OR already uses for the other recognized annotations.
  3. Honor immutability: in ObjectService::deleteObject() (or its mapper), if schema.x-openregister-archival is set AND the request is a user-driven delete (not a retention-sweep job), reject with 403 Forbidden — schema is archival. Cascading deletes from a parent object's removal should also stop at archival children.
  4. Retention sweep: a new scheduled job (OCA\OpenRegister\Cron\ArchivalRetentionTask) that, per schema with archival declared, evaluates each row against the conditions in order (first match wins) and deletes rows past their effective retention. Use the same SettingsService::rebase()-style native SQL pattern openconnector currently has on the legacy tables.
  5. Surface the rule + retention state in the dashboard / object-detail UI so users can see WHY a row is being kept (which condition matched + how long until it expires).

Non-goals

  • Cross-schema cascading archival (e.g. "synchronization_contract is archival because its parent synchronization is") — not needed for the fleet's current schemas.
  • Per-tenant retention overrides — multi-tenant settings stay out of v1.

Suggested opsx change name

add-archival-annotation-support

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions