Skip to content

Latest commit

Β 

History

History
284 lines (200 loc) Β· 18.4 KB

File metadata and controls

284 lines (200 loc) Β· 18.4 KB

AGENTS.md

This file provides guidance to all AI agents (Claude, Codex, Gemini, etc.) working with code in this repository.

What This App Is

Nextcloud Tables is a Nextcloud app (PHP backend + Vue.js frontend) that lets users create and manage custom data tables with typed columns, views, sharing, and import/export. It ships a full OCS REST API and integrates with the Nextcloud event, activity, search, and reference systems.

Nextcloud Contribution Policy

All contributions generated or assisted by this agent must fully comply with:

  • AI Contribution Policy - the primary reference for AI-specific rules, covering disclosure, author accountability, communication, security, licensing, code quality, and autonomous agent behavior.
  • Contribution Guidelines - covering testing requirements, the Developer Certificate of Origin (DCO), license headers, conventional commits, and translations. These apply in full to all contributions regardless of how they were produced.

What this agent must always do

  • Add an Assisted-by: AGENT_NAME:MODEL_VERSION git trailer to every commit containing AI-assisted content.
  • Ensure every pull request includes a disclosure of AI tool use in the PR description.
  • Produce focused, scoped pull requests that address exactly one concern. Do not touch unrelated files or introduce incidental refactors.
  • Verify all dependencies against actual package registries before suggesting them. Do not use hallucinated or unverified package names.
  • Explicitly inform the contributor when any action they are about to take, or have taken, would violate the AI Contribution Policy or the Contribution Guidelines. Do not silently proceed. State which rule is at risk and what the contributor should do instead.
  • Warn the contributor if a pull request is growing too large. A PR approaching several thousand lines of changed code is a signal that it should be split into smaller, focused PRs. Suggest a logical split before the PR is opened, not after.
  • Recommend opening a ticket for discussion before starting implementation whenever a feature or change is sufficiently complex - for example when it touches multiple subsystems, requires architectural decisions, or the right approach is not yet clear. A ticket allows maintainers and the contributor to align on direction before code is written, avoiding wasted effort on a PR that may be rejected or require fundamental rework.

What this agent must never do

  • Open issues, submit pull requests, post review comments, or send security reports autonomously. Every contribution must be reviewed and submitted by a human.
  • Add Signed-off-by tags to commits. Only the human contributor can certify the Developer Certificate of Origin.
  • Generate or submit security reports without independent human verification. Report verified vulnerabilities via HackerOne, not as GitHub issues.
  • Write PR descriptions, review comments, or issue reports on behalf of the contributor. These must be in the contributor's own words.
  • Fully automate the resolution of issues labeled good first issue or similar beginner-friendly labels.
  • Submit code that has not been reviewed and cleaned up by the contributor. Dead code, redundant logic, excessive comments, and unrelated changes must be removed before submission.

Development Setup

composer install    # PHP dependencies
npm install         # JS dependencies
npm run watch       # Frontend dev build with watch

Common Commands

Building

npm run build           # Production JS build
make build              # Full production build (JS + PHP assembly)

Linting

make lint               # All linting (PHP, JS, CSS, XML)
make lint-fix           # Auto-fix all
composer cs:check       # PHP coding standards check
composer cs:fix         # PHP coding standards fix
composer psalm          # PHP static analysis (Psalm)
npm run lint            # ESLint (JS/Vue/TS)
npm run lint:fix        # ESLint auto-fix
npm run stylelint       # CSS/SCSS linting
npm run stylelint:fix   # CSS/SCSS auto-fix

Testing

make test               # All tests (unit + Behat + Cypress)
composer test           # PHP unit tests
composer test:unit:local  # PHP unit tests without Nextcloud bootstrap
npm run tests:component # Cypress component tests
npm run test:e2e        # Playwright E2E tests
npm run test:e2e:ui     # Playwright with interactive UI

To run a single PHP test file:

vendor/bin/phpunit --bootstrap tests/unit/bootstrap.php tests/unit/path/to/TestFile.php

Code Generation

composer run openapi         # Regenerate openapi.json and TS types from PHP annotations
npm run typescript:generate  # Regenerate TS types from openapi.json only

Architecture

Backend (PHP β€” lib/)

The app follows the standard Nextcloud layered pattern:

  • lib/Controller/ β€” OCS REST API controllers. Api1Controller.php is the monolithic v1 handler; newer controllers (e.g. ApiTablesController, ContextController) are split by resource type. Public (unauthenticated share-token) variants exist alongside standard controllers.
  • lib/Service/ β€” All business logic lives here. Each resource has its own service (TableService, RowService, ColumnService, ViewService, ShareService, ContextService, ImportService, etc.). PermissionsService is the central access-control authority.
  • lib/Db/ β€” Doctrine DBAL mappers and entity classes. Row data is stored in typed cell tables (tables_row_cells_text, _number, _datetime, etc.) rather than a single JSON column. Row2Mapper handles the join/assembly logic.
  • lib/Migration/ β€” Versioned schema migrations (e.g. Version000900Date20250710000000.php).
  • lib/Middleware/ β€” PermissionMiddleware and ShareControlMiddleware enforce access control before controllers run.
  • lib/Event/ + lib/Listener/ β€” Domain events (TableDeletedEvent, RowDeletedEvent, etc.) dispatched through Nextcloud's event system; listeners handle audit logging, user-deletion cleanup, and analytics integration.
  • lib/Model/ β€” Value objects and DTOs (FilterGroup, SortRuleSet, ViewUpdateInput, RowDataInput, TableScheme, ColumnSettings, Permissions).

Routes are defined in appinfo/routes.php (~100 routes covering the full REST API plus Nextcloud UI hooks).

Frontend (Vue.js β€” src/)

Single-page app built with Vue 2.7, Vue Router 3, and Pinia (state management). Compiled with Vite.

  • src/store/store.js β€” Primary Pinia store (tables, columns, views, shares, contexts).
  • src/store/data.js β€” Row/cell data state.
  • src/modules/ β€” UI regions: navigation/ (left sidebar with contexts/tables), main/ (table grid view), sidebar/ (right panel), modals/ (all dialogs).
  • src/types/ β€” TypeScript interfaces auto-generated from openapi.json; do not edit manually.
  • src/shared/ β€” Reusable components and utilities shared across modules.

Database

Supports PostgreSQL, MySQL, and SQLite. The unusual design detail is that row cell values are stored in per-type tables (tables_row_cells_text, tables_row_cells_number, tables_row_cells_datetime, tables_row_cells_selection, tables_row_cells_usergroup) rather than in one polymorphic column, which affects any queries or migrations touching row data.

API

openapi.json (auto-generated) is the source of truth for the REST API contract. The TypeScript types in src/types/ are derived from it β€” always regenerate both together with composer run openapi when changing API shapes. Never edit openapi.json or src/types/openapi/openapi.ts by hand; they are generated artifacts.

Commits

  • All commits must be signed off (git commit -s) per the Developer Certificate of Origin (DCO). All PRs target master. Backports use /backport to stable-X.Y in a PR comment.

  • Commit messages must follow the Conventional Commits v1.0.0 specification β€” e.g. feat(import): support remapping selection options, fix(rows): handle empty cell values on export.

  • Every commit made with AI assistance must include the Assisted-by trailer mandated by the AI Contribution Policy (see "What this agent must always do" above):

    Assisted-by: ClaudeCode:claude-sonnet-4-6
    Assisted-by: Copilot:gpt-4o
    

    General pattern: Assisted-by: AGENT_NAME:MODEL_VERSION

    If multiple agents or models contributed to a commit, add one trailer per agent/model combination:

    Assisted-by: OpenCode:claude-opus-4-5
    Assisted-by: OpenCode:claude-sonnet-4-5
    

Pull Requests

  • Include a short summary of what changed. Example: fix: prevent crash on empty todo title.
  • Pull Request: When the agent creates a PR, it should include a description summarizing the changes and why they were made. If a GitHub issue exists, reference it (e.g., β€œCloses #123”).

Code Style

  • Do not use decorative section-divider comments of any kind (e.g. // ── Title ───, // ------, // ======).
  • Every new file must end with exactly one empty trailing line (no more, no less).
  • After writing or modifying any PHP code, run the following checks before considering the task complete:
    1. composer run cs:fix β€” auto-correct coding-standard violations, then verify with composer cs:check that no issues remain.
    2. composer run psalm β€” static analysis; fix every reported type error or logical issue (no suppressions).
    3. composer run lint β€” PHP syntax check across all source files.
  • After writing or modifying any frontend code (Vue, JS, TS, CSS/SCSS), run npm run dev to verify the frontend compiles without errors before considering the task complete.

Clean code

Apply standard clean-code practices to every file you touch:

  • Single responsibility β€” each class and method does one thing. Split large methods if they handle multiple concerns.
  • Meaningful names β€” variables, parameters, and methods must describe their purpose. Avoid abbreviations and generic names like $data, $arr, or $tmp.
  • No dead code β€” do not leave commented-out code, unused variables, or unreachable branches in the codebase.
  • Early returns β€” prefer guard clauses over deeply nested if/else trees.
  • Boolean casts β€” use explicit (bool) only when a value truly represents a boolean; do not silently coerce unrelated types.
  • Avoid double negatives β€” name booleans positively (isEnabled, hasShares) rather than negatively (isNotDisabled).

Psalm annotations

Never add @psalm-suppress annotations to work around a type error. A suppression is a red flag that signals the code or its type annotation is wrong. Fix the root cause instead:

  • If the return type annotation does not match what the method actually returns, fix the annotation or the implementation.
  • Use explicit, closed Psalm array shapes β€” array{columnId: int, order: int} β€” never leave a trailing ... in a shape literal.
  • Do not use @psalm-suppress MismatchingDocblockReturnType (or any other suppression) just because a Psalm rule is inconvenient to satisfy.

Architecture Patterns

Icons (Vue frontend)

Always use the outline variant of a vue-material-design-icons icon. Import from e.g. ArchiveArrowDownOutline.vue, never the filled variant (ArchiveArrowDown.vue). This keeps the icon style consistent across the app.

Boolean getters on Nextcloud DB entities

Do not implement an explicit isXxx(): bool method on a class that extends Entity (or EntitySuper). The base class handles isXxx calls via __call magic for any protected bool $xxx property. Instead, declare the method in the class-level @method docblock so that static analysis and IDE completion still work:

 * @method isArchived(): bool

Database queries inside loops

Never build a IQueryBuilder query inside a loop. Construct the query once before the loop using $qb->createParameter('name') as a placeholder for the value that changes per iteration. Inside the loop call $qb->setParameter('name', $value, IQueryBuilder::PARAM_*) to bind the new value. This avoids re-parsing and re-compiling the query on every iteration.

IN clauses must be chunked to at most 1 000 items for Oracle compatibility. Use a named constant (DB_CHUNK_SIZE = 1_000) rather than a magic number. When collecting results across chunks, accumulate into an array and spread with array_merge(...$results) after the loop β€” never call array_merge inside a loop, as that rebuilds the array on every iteration.

private const DB_CHUNK_SIZE = 1_000;

// ...

$qb = $this->db->getQueryBuilder();
$qb->select('*')->from($this->table)
    ->where($qb->expr()->in('node_id', $qb->createParameter('chunk')));

$results = [];
foreach (array_chunk($ids, self::DB_CHUNK_SIZE) as $chunk) {
    $qb->setParameter('chunk', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
    $results[] = $this->findEntities($qb);
}
return array_merge(...$results);

Unit tests for services with injected dependencies

When a service constructor gains a new dependency, add a corresponding $this->createMock(NewDependency::class) in every setUp() method that instantiates that service, and pass the mock as the matching constructor argument. Failing to do so causes ArgumentCountError at test runtime.

New REST endpoints

Every new OCS endpoint must carry:

  • #[NoAdminRequired]
  • #[RequirePermission(...)] on every method that accesses a resource by ID β€” not just mutation endpoints. Without it, access is only implicitly enforced by the mapper's SQL filter, which is correct but non-obvious and inconsistent. Use PERMISSION_READ for read-only or soft-state operations (e.g. archive/unarchive); use PERMISSION_MANAGE for mutations.
  • #[UserRateLimit(limit: 20, period: 60)] for mutation endpoints (see ImportController for the pattern)

When adding or auditing a #[RequirePermission] attribute, also verify the method body and docblock are consistent:

  • a PermissionError catch block returning $this->handlePermissionError($e)
  • Http::STATUS_FORBIDDEN in the @return docblock union type
  • a 403: No permissions OpenAPI annotation line

Every controller method must return ->jsonSerialize() directly. Do not add a separate GET round-trip after a create/update/delete β€” the response body is the authoritative post-mutation state.

After any of the following changes, run composer run openapi to regenerate openapi.json and the TypeScript types in src/types/openapi/openapi.ts. CI fails if either file is stale.

Triggers that require regeneration:

  • Adding, removing, or renaming a controller route
  • Changing a controller method signature (parameters, return type, or PHPDoc @param/@return annotations)
  • Changing a response shape (adding/removing fields in ResponseDefinitions.php or a jsonSerialize() method)
  • Adding or removing an HTTP status code from a controller method's return type annotation
  • Changing a Psalm array-shape type that appears in a public API response

Selection option values

Selection column values are encoded as magic strings: @selection-id-{id} where {id} is the selection option's DB primary key. This format is used by the filter component, the sort evaluator, and any condition-based feature (e.g. conditional formatting). Always use this format β€” never store or compare bare option labels.

On import/export, selection option IDs change. ColumnService::importColumn() must return a selectionOptionIdMap alongside the new column ID so callers can remap stored option references. Unmapped IDs should be flagged as broken: true rather than silently dropped.

Soft-invalidation ("broken" flag)

When a stored rule, filter, or condition references a column or option that no longer exists (due to deletion, type change, or import remapping), mark it broken: true instead of deleting it. Surface the broken state in the UI. Provide an auto-clear path that removes the flag when the rule is next saved in a valid state.

XSS / CSS injection

  • Never use v-html, innerHTML, eval, or new Function with user-supplied values.
  • Apply dynamic styles via Vue's :style binding only.
  • Validate any user-supplied CSS color values on the backend with /^#[0-9a-fA-F]{3,6}$/ before storing. Reject other formats.

Input validation and value objects

Validate structured array input at the controller boundary, before the service layer.

When a controller parameter accepts a structured array (e.g. $columnSettings, $sort), parse and validate it into a typed value object β€” such as ColumnSettings::createFromInputArray() or SortRuleSet::createFromInputArray() β€” immediately in the controller, before calling any service method. If the input is invalid, return Http::STATUS_BAD_REQUEST with a descriptive message. Never pass a raw unvalidated array into a service method.

// Good β€” validate at controller boundary, pass value objects downstream
try {
    $columnSettingsObj = $columnSettings !== null
        ? ColumnSettings::createFromInputArray($columnSettings)
        : null;
    $sortObj = $sort !== null ? SortRuleSet::createFromInputArray($sort) : null;
} catch (\InvalidArgumentException $e) {
    return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}
return new DataResponse($this->service->update(..., $columnSettingsObj, $sortObj)->jsonSerialize());

Service methods must declare typed value-object parameters, not raw arrays.

TableService::update() and equivalent methods must accept ?ColumnSettings and ?SortRuleSet, not ?array. This makes the contract explicit and prevents the service from receiving unvalidated data.

Value objects must throw, not silently coerce.

fromArray() / __construct() methods on value objects must throw \InvalidArgumentException when required fields are missing or have an incompatible type. Do not add silent casts like (int)$data['columnId'] that accept garbage input without error β€” that hides bugs and lets invalid data propagate to the database. The correct pattern is to call static::assertRequiredFields($data) (which throws) before casting.