Skip to content

Let a destination declare several tables, each found by its key - #1768

Closed
michielbdejong wants to merge 4 commits into
claude/installation-connectfrom
claude/app-setup-multi-class
Closed

michielbdejong wants to merge 4 commits into
claude/installation-connectfrom
claude/app-setup-multi-class

Conversation

@michielbdejong

Copy link
Copy Markdown
Contributor

Closes #1741

What

A plugin's manifest destination can now declare keyed tables beside (or instead of) its single table. Set up creates each table beneath the plugin, with its own row class and a default view. Each kind of record gets its own table, so statements never show up as rows of the transactions table.

"destination": {
  "schema": { "properties": [...], "classes": [...] },
  "table": { "name": "Bank transactions", "rowClass": "bank-transaction", "columns": ["bank-amount"] },
  "tables": {
    "statements": { "name": "Bank statements", "rowClass": "bank-statement", "columns": ["statement-number"] },
    "closingBalances": { "name": "Closing balances", "rowClass": "closing-balance", "columns": ["balance-date", "bank-amount"] }
  }
}

The stored config (under config.key) is { table, rowClass, properties, tables: { statements: { table, rowClass }, closingBalances: { table, rowClass } } }. The importer writes a statement with parent: config.tables.statements.table. A drive app shown as a view of any of these tables gets the same map from await store.getData(), as { table, rowClass, tables }.

The money app needs this for stored statements and closing balances (ontola/atomic-plugins#148, M-7 and M-12). Once this lands, money can keep table for transactions, add tables, and add tables to config.required. Existing installations then show Set up once more, and it creates only the new tables.

Rules (the same in TS and Rust, same messages)

  • Declare table, tables or both. tables must not be empty.
  • A key starts with a lower-case letter and has only letters and digits, at most 64 characters. Names of Object.prototype members (constructor, ...) are refused.
  • Each table needs its own rowClass, and it must be a class declared in schema. Columns must be declared properties.

Unchanged for single-table manifests

  • Same config { table, rowClass, properties }, same table localId (atomic:destination:table), so existing installs resume the same table.
  • The release id is unchanged, because destination is still carried verbatim. destination_fixtures_keep_their_release_ids pins the single-table fixture's id, which matches release-ids.json on claude/atomic-plugins-pin.
  • App package content is unchanged: the stored bytes of a package carrying the single-table manifest are pinned by sha-256 in app-package.test.ts.

Base

The destination Set up step (#1653, branch claude/plugin-accepts-file) is on develop (via #1699) and on the atomic-plugins pin branches, but not on claude/installation-connect. So this branch is claude/installation-connect plus a merge of claude/plugin-accepts-file (2 commits). The conflicts in plugin-manifest.ts, index.json and en.po were resolved by keeping both sides. That merge is the first commit. The change itself is the second.

Changes

  • browser/lib/src/plugin-manifest.ts: DeclaredDestinationTable, destination.tables and their validation.
  • browser/lib/src/plugin-destination.ts (new): provisionDestination moved here from FileImport.tsx and extended to cover tables. Also adds destinationTablesFor.
  • server/src/plugins/manifest.rs: validate_destination mirrors the TS checks. Before this, the server only required destination to be a map.
  • hostStore.ts: the data op adds tables when the table belongs to a multi-table destination.
  • FileImport.tsx: when there is more than one table, Set up lists them.
  • docs/src/plugins/creating-plugins.md: documents accepts and destinations with one table or several.
  • browser/lib/src/test-schema-store.ts: the fake store from plugin-app.test.ts, now shared.

Tests

  • Shared fixtures in testdata/plugin-manifest/ (2 accepted, 11 rejected), run by both plugin-manifest.test.ts and shared_manifest_conformance.
  • plugin-destination.test.ts:
    • single-table config unchanged
    • multi-class Set up creates every table with its own class and view
    • keyed tables with no primary table
    • Set up resumes, and a later release that adds a table creates only that one
    • destinationTablesFor from any of the tables, with or without a config key, and nothing for a single-table destination
  • hostStore.test.ts: data returns tables for a multi-table destination and the old shape otherwise.
  • app-package.test.ts: package content is pinned. A multi-table release is carried, and an invalid one is refused.
  • destination_fixtures_keep_their_release_ids (Rust).
  • Runs: browser/lib vitest (998 passed), data-browser AppPage/PluginRuns/AppSetup vitest (45 passed), pnpm typecheck, cargo test -p atomic-server --lib (288 passed), clippy clean on manifest.rs.
  • Checked by hand against a running server: Set up created the 3 tables, and handleRequest(…'data'…) on the transactions table and on the statements table both returned the keyed tables.

Translations

The catalogs were settled with one vite run after rm -rf src/locales/.wuchale. The diff has one new message ("Set up creates {0} tables for this importer. Nothing is imported yet.") and a changed placeholder comment on the single-table message.

Screenshots

Set up for a three-table importer, before and after, on desktop (1280) and mobile (390). They were taken with Playwright against the running app and shared in the session:
multiclass-desktop-before.png, multiclass-desktop-after.png, multiclass-mobile-before.png, multiclass-mobile-after.png.

🤖 Generated with Claude Code

Michiel de Jong and others added 4 commits September 24, 2026 11:06
A plugin can declare `accepts` (extensions, media types, maxBytes, as
text) and a `destination` (a schema and one table) in its version-two
manifest. PluginPage then shows an Import tab instead of Run, Schedule and
Trigger: Set up creates the destination table beneath the plugin and
stores { table, rowClass, properties } as its config; choosing a file
decodes it (UTF-8, else Windows-1252), runs the plugin on the server with
input.upload, and reviews the verdict in RunPluginDialog before anything
is written.

/plugin-run refuses an upload the manifest does not accept or that is
larger than its maxBytes, and takes bodies up to PAYLOAD_MAX so a 5 MB
statement fits after JSON escaping. Both manifest parsers accept the new
fields and share the conformance fixtures.

This replaces the plugin-specific ImportMT940 dialog removed in 4bab16e
with a generic entry point, for atomic-plugins' Bank statements importer
(atomic-plugins#95).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Table's genesis commit must carry its classtype, so Set up now creates
the table and view with all their properties instead of setting them after
creation. The new UI strings are added to the wuchale catalogs, without
which they render empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plugin's manifest `destination` can now declare keyed `tables` beside
(or instead of) its single `table`. Set up creates each one beneath the
plugin with its own row class and default view, and stores
`tables: { <key>: { table, rowClass } }` in the plugin's config next to
the unchanged `{ table, rowClass, properties }`. A drive app shown as a
view of any of those tables gets the same map from `store.getData()`.
This is what the money app needs for statements and closing balances
(atomic-plugins#148, M-7 and M-12).

- provisionDestination moves from FileImport into @tomic/lib
  (plugin-destination.ts), with destinationTablesFor for the host's
  `data` op. Set up resumes: a release that adds a key creates only that
  table.
- The server now checks `destination` with the browser's rules and
  messages instead of only requiring a map. Both parsers are held to new
  shared fixtures in testdata/plugin-manifest/.
- A single-table manifest keeps its config, its table localId and its
  release id; the single-table fixture's id is pinned, and so is the
  stored bytes of an app package carrying it.
- Docs: creating-plugins.md describes `accepts`, `destination.table`
  and `destination.tables`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@michielbdejong michielbdejong added enhancement New feature or request plugin Should probably be an Atomic Plugin browser labels Sep 24, 2026
@michielbdejong

Copy link
Copy Markdown
Contributor Author

Merge-risk triage (2026-09-28). This is a read-only review of this PR's own diff. The rule it applies: nothing that works today may break for existing users or clients, and new behaviour is added alongside the old. The stack is being rebased onto develop as one chain before merging, so line references are against the current stacked base.

Verdict: SAFE-WITH-FIX

The client side is additive (data only adds tables), and single-table config and release ids are pinned.

Fix before merging: The new server-side validate_destination is much stricter than develop's "a map" check, and it runs inside Manifest::parse, which runs on every read of a stored release (plugin_sync.rs, plugin_external.rs, actions.rs). A release already stored with a looser destination (e.g. published through the CLI or API) would stop parsing, and runs and sync for its existing install would break. Validate on publish/ingest only, not when parsing stored releases.

Note: It carries a merge of claude/plugin-accepts-file (#1653), whose content is already on develop via #1699. Those hunks should disappear after the rebase.

🤖 Generated with Claude Code

@michielbdejong

Copy link
Copy Markdown
Contributor Author

Replaced by the consolidated #2148 (Q-101), which carries this change onto develop. The branch is kept. Closed by Claude (VPS coordinator) on behalf of Michiel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

browser enhancement New feature or request plugin Should probably be an Atomic Plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant