Skip to content

feat(dms)!: move to interface-database 0.4.0 - #907

Open
Upd4ting wants to merge 12 commits into
mainfrom
feat/interface-database-0-4
Open

Upd4ting wants to merge 12 commits into
mainfrom
feat/interface-database-0-4

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

ANT-2382. Moves dms onto @antelopejs/interface-database 0.4.0, where a query is a plain object that runs only through .run(). Stacked on #888 (fix/interface-dms-stored-row).

⚠ sensitive: it changes the interface-dms contract (peer @antelopejs/interface-database >=0.4.0 <1.0.0) and the database module a deployment must run.

tgz-pending:

  • @antelopejs/interface-data-api comes through fix(interface-dms)!: move to interface-data-api 0.7's StoredRow #888's tarball: main 170f079, packed as 0.8.0.
  • @antelopejs/interface-dms-builder comes through .tgz-deps/antelopejs-interface-dms-builder-b04af21.tgz: this repo's own builder, packed as 0.4.2. npm's 0.4.1 is not standalone, so a project loading the DMS without a builder module fails with "Unresolved interface dependencies: dms requires @antelopejs/interface-dms-builder".
  • swap-tgz.sh restores >=0.8.0 <0.9.0 and >=0.4.2 <1.0.0.

Summary

 packages/dms, packages/dms/playground, packages/interface-dms (peer and dev)
-  "@antelopejs/interface-database": ">=0.3.0 <1.0.0"     (interface-dms peer: >=0.1.11)
+  "@antelopejs/interface-database": ">=0.4.0 <1.0.0"
 test host (src/test/antelope.test.ts), playground (antelope.config.ts)
-  @antelopejs/mongodb 1.6.2 / ^1.4.3
+  @antelopejs/mongodb 1.8.0 / ^1.8.0
 Builder engine (query-emit, query-structure, query-preview, resource-schema, table-readers)
-  return { value: await model.paidRevenue() };
+  return { value: await model.paidRevenue().run() };
 tenant-instances.ts
-  function getTenantSchema(): Schema
+  function getTenantSchema(): NonNullable<ReturnType<typeof Schema.get>>   // Schema.get takes no type arguments

#903 already made the runtime run its own queries through .run(). This PR adds:

  • the dependency move;
  • the database module the tests and the playground run;
  • the code the Builder generates for modules;
  • the docs.

packages/dms/docs/02.building/20.migration-interface-database-0-4.md is the deployment and module guide. packages/interface-dms/UPGRADING.md (Unreleased) lists the peer range.

Evidence

  • Before: on main with interface-database 0.4.0 and mongodb 1.8.0, the playground does not start. registerExistingTenantInstances builds a Set from a query object ("object is not iterable"). ANT-2375's live run saw "this.table.get(...).then is not a function" with decorators 0.3.1.
    After: dms-control up --prepare boots the playground on mongodb 1.8.0 (loaded from .antelope/cache, version 1.8.0), doctor passes, and the smoke journey passes (15 steps, invite mail captured).
  • No query left unrun. A ts-morph scan over dms, interface-dms and interface-dms-builder looks for any expression with run() and no then that is awaited, put in Promise.all/race, given a .then, or returned from an async function. It finds 0 on this head. On main's old tenant-instances.ts it reports the 4 broken sites, so it sees them. Queries typed any would escape it.
  • Suites on interface-database 0.4.0 and mongodb 1.8.0 (before the final docs commit):
    • table-view-capabilities 17 (/count/batch answers { mine: 2 }, POST …/new no longer answers 500);
    • test:unit 2629;
    • integration 419;
    • builder 668;
    • pnpm run checks 11/11.
  • Consumer resolution. Core's own findUnresolvedInterfaces, run on a fresh npm install of the packed dms:
    • Before: with npm's builder 0.4.1, dms requires @antelopejs/interface-dms-builder is unresolved.
    • After: with the 0.4.2 tarball, the builder is stubbed standalone and nothing is unresolved.

Attention

  • Release order:
    1. interface-data-api 0.8.0;
    2. swap and merge fix(interface-dms)!: move to interface-data-api 0.7's StoredRow #888;
    3. swap this PR's data-api pin and merge;
    4. release interface-dms-builder 0.4.2 from main;
    5. swap the builder pin;
    6. interface-dms 0.14.0;
    7. dms 0.20.0.
  • dms's interface-dms range stays >=0.13.0 <0.14.0 here. The release validate job requires the interface version in the tree to satisfy that range, and the workspace links interface-dms only while its 0.13.x version satisfies it. So raising it before interface-dms 0.14.0 is bumped breaks install and validation. interface-dms's after:bump runs sync and stages dms's README, but does not move dms's range. The raise to >=0.14.0 <0.15.0 therefore has to be committed right after the interface-dms 0.14.0 release and before dms 0.20.0.
  • Module code that awaits queries needs .run(). Builder-generated stats routes written before this PR answer with the query object until .run() is added; the guide says so.

Merge Danger

Door: one-way once released. Deployments must run a database module that implements interface-database 0.4, and modules must run their queries through .run().

Blast Radius: wide. Every deployment's database module, every module that queries the database, and the code the Builder generates.

@Upd4ting Upd4ting added the tgz-pending Waits for a release; holds a .tgz-deps tarball label Nov 13, 2026
interface-data-api 0.7 removed RowFields, which interface-dms 0.13.3 names in its published declarations, so a project that resolves data-api 0.7 fails tsc. interface-dms and dms now build against data-api 0.7 and name StoredRow. The guard-row read in column-conditions is typed from the published fetchRowForGuard, and dms reads a row id as text through rowIdText, which refuses an id holding a row, a list or a document.
…tarball

interface-data-api main (d70049c, #129) declares the decorators-0.5 ContainerModifier fix, packed as 0.7.2. The tarball stands in for the release until it ships; interface-dms requires >=0.7.2 <0.8.0.
interface-data-api 0.7.2 declares its modifier keys as Class<ContainerModifier> and requires decorators >=0.5.0; decorators 0.3.1's LocalizationModifier does not satisfy it. dms, the playground and interface-dms's development build move to decorators >=0.5.2.
… run every query

interface-data-api #133 runs each DefaultRoutes query through .run(), which interface-database 0.4 needs. Packed as 0.7.2 like the tarball it replaces.
…terValue holds a boolean

interface-data-api 0.8 types a filter value as string | boolean (ANT-2463). DataType.filter takes that value and hands its compare mode String(value), which the value parser reads back as the same boolean; the date type's override reads it as text. interface-dms and dms require data-api >=0.8.0 <0.9.0.
interface-data-api 0.8 types a filter value as string | boolean, so archiveFilter returns [true, mode] as typed and the SAFETY comment and chained assertion go.
interface-database 0.4.0 queries are not thenable: every query runs through .run(). dms, the playground and interface-dms require interface-database >=0.4.0 and interface-database-decorators >=0.5.2; the test host and the playground run @antelopejs/mongodb 1.8.0.
…r through a tarball

The published @antelopejs/interface-dms-builder 0.4.1 is not standalone, so a project installing dms from this branch fails to load with an unresolved interface dependency. The branch's builder, packed as 0.4.2, stands in until it ships.
@Upd4ting
Upd4ting force-pushed the fix/interface-dms-stored-row branch from ca837f0 to 42b25e5 Compare November 13, 2026 20:10
@Upd4ting
Upd4ting force-pushed the feat/interface-database-0-4 branch from e031d8c to 0068a03 Compare November 13, 2026 20:11
Upd4ting added a commit that referenced this pull request Nov 14, 2026
…auth routes read (#909)

ANT-1093's dms part. AntelopeJS/dms-frontend#338
(`refactor/unknown-server-json`) makes `event.backend()` and
`readJson()` resolve to `unknown` instead of a type the caller picks.
frontend-vue's auth routes read those results as typed values. This
narrows each backend answer with a zod schema and each request body with
an object check (`readJsonObject`; the route then checks the fields it
reads), so frontend-vue compiles against today's dms-frontend and
against #338. A malformed answer is refused, never cast through.

⚠ sensitive: auth (sign-in, 2FA, tenant switch, stored accounts, session
renewal, OAuth).

## Summary

```diff
 layers/dms-auth/server/actions.ts, session.ts, oauth.ts; server/html-render.ts
-  const answer = await event.backend<SessionTokens>("/api/auth/refresh", …);
+  const answer = tokenPair.parse(await event.backend("/api/auth/refresh", …));
-  const input = await readJson<Input>(event);
+  const input = readJsonObject(await readJson(event));        // 400 "Invalid request body" otherwise
-  input.accountId as string
+  isString(input.accountId) ? … : …
+layers/dms-auth/server/backend-answers.ts   loginOutcome (tagged union), tokenPair, sessionUser, oauthAuthorization
+server/request-body.ts                      readJsonObject (packages/dms/frontend-vue/server/)
```

Neither new file sits in an auto-imported folder. `SessionTokens` is now
derived from the schema, so `EstablishResult` and `OAuthResult` are
gone.

| Call site | Backend route | Schema or guard | A malformed answer |
| --- | --- | --- | --- |
| actions.ts login, register, verify-2fa, establish | `/api/auth/login`,
`/signup`, `/verify-2fa`, establish endpoint | `loginOutcome` | 502
`error.500.description`; no session written |
| actions.ts switch-tenant | `/api/auth/switch-tenant`, `/api/auth/me` |
`tokenPair`, `sessionUser` | 502 |
| actions.ts switch-account | `/api/auth/refresh`, `/api/auth/me` |
`tokenPair`, `sessionUser` | 502; the account is kept |
| actions.ts validate-account | `/api/auth/refresh` | `tokenPair` | `{
valid: null }`; the account is kept |
| session.ts renewSession | `/api/auth/refresh`, `/api/auth/me` |
`tokenPair`, `sessionUser` | session kept; nothing new used for the
request |
| oauth.ts start | `/api/auth/oauth/:provider/authorize-url` |
`oauthAuthorization` | redirect to `error.oauth.failed` |
| oauth.ts callback | `/api/auth/oauth/:provider/callback` |
`loginOutcome` | redirect to `error.oauth.failed`; no handoff, no
session |
| actions.ts request bodies (9 routes: login, register, verify-2fa,
request-2fa-email, establish, remove-account, switch-tenant,
switch-account, validate-account), html-render.ts | client body |
`readJsonObject` (an object; the route checks its own fields) | 400
`Invalid request body`, as for a non-JSON body; no backend call |

A malformed backend answer fails like a backend error: the existing 502
path. It is never taken as a refusal, so nothing is cleared or removed.
Valid answers behave as before. `sessionUser` and the 2FA and
tenant-assignment steps use `.passthrough()`, because the session and
the browser receive them whole. `tokenPair` and `oauthAuthorization`
strip the fields nobody reads.

## Evidence

- **Typecheck, `ajs dms verify-source`:**

  | dms-frontend | Before | After |
  | --- | ---: | ---: |
  | installed 0.10.6 | 0 | 0 |
| #338 head e015184 (tarball, not committed) | 57 (actions.ts 39,
oauth.ts 7, session.ts 4, html-render.ts 7) | 0 |

- **Review round: a step answer never opens a session.** At a46b600 the
session shape ignored the step flags, so an answer with `requires_2fa:
true` (or `requires_tenant_assignment: true`), a malformed step and a
token pair failed both step shapes and opened a session; main refused
it. The session shape now accepts the flags only absent or `false`. Two
new tests:

- `opens no session from a malformed step answer that also carries a
token pair, and answers it as a failed backend` (login, register,
verify-2fa, establish);
- `ends a provider sign-in on a malformed step answer that also carries
a token pair as on a failed backend` (OAuth callback).

Each tries `{ ...CHALLENGE, ...PAIR, methods: [5] }` and `{
...ASSIGNMENT, ...PAIR, tenant_assignment_token: 5 }`.

**Before** (a46b600's `backend-answers.ts`, `accounts.ts`,
`actions.ts`):
  ```
× opens no session from a malformed step answer that also carries a
token pair, and answers it as a failed backend
× ends a provider sign-in on a malformed step answer that also carries a
token pair as on a failed backend
AssertionError: /auth/login
{"requires_2fa":true,"two_factor_token":"challenge-1","methods":[5],"access_token":"header.eyJ0ZW5hbnRJZCI6InQxIn0.signature","refresh_token":"refresh-1"}:
expected { status: 200, location: null, …(3) } to deeply equal { status:
502, location: null, …(3) }
AssertionError:
{"requires_2fa":true,"two_factor_token":"challenge-1","methods":[5],"access_token":"header.eyJ0ZW5hbnRJZCI6InQxIn0.signature","refresh_token":"refresh-1"}:
expected { status: 302, …(4) } to deeply equal { status: 302, …(4) }
        Tests  2 failed | 9 skipped (11)
  ```
  **After:**
  ```
        Tests  2 passed | 9 skipped (11)
  ```
- **`tests/server-backend-answers.test.ts`.** It runs the real router
against a fake backend and checks that each spoiled answer gives the
same whole route answer as a failing backend.
- It covers the sign-in answer of another shape (4 routes), the
malformed step that carries a token pair (4 routes and the OAuth
callback), empty tokens, empty OAuth `state` and `authorizeUrl`,
stored-user fields of another type, a sign-in step handed over whole,
the OAuth callback, authorize-url, session renewal, switch-tenant, the
stored-account switch and validation, and request and render bodies that
are not objects.
- Each of 9 schema or guard mutations, applied one at a time and then
reverted, makes a test fail.
- Against main's code, the cast, 8 of 9 tests fail. The ninth is the
valid-answer test.
- **Suites:**
- `server-backend-answers.test.ts`: 11 tests (re-run alone after this
round, `--maxWorkers=4`);
- full frontend vitest locally: 3516 passed, 1 failed
(`live-block-families` polling count, a timing test this PR does not
touch, at load average 45–73); CI runs the full suite green;
  - the dms auth integration subset (15 files): 83 passing, unchanged;
  - `pnpm run checks`: 11 passed;
  - root typecheck, oxlint and frontend `eslint .` are clean.

API: 3 packages, 379 entries unchanged against origin/main (committed
HEAD).
API: skipped ., private.
API: skipped packages/dms/playground, private.
API: skipped packages/dms/playground/frontend-vue, private.
Runtime API: 3 packages, 376 entries unchanged against origin/main
(committed HEAD).
Auto-imports: 1 packages, 944 entries unchanged against origin/main
(committed HEAD).
No behavioural probe ran.

Suppressions in authored code files (origin/main and committed HEAD). A
lint directive counts once per rule it names; `*` is a directive naming
none.
| Suppressions | base | HEAD |
| --- | ---: | ---: |
| Lint suppressions, all rules | 163 | 163 |
| Lint suppressions, `@typescript-eslint/no-unused-vars` | 2 | 2 |
| Lint suppressions, `anti-slop/no-chained-type-assertions` | 1 | 1 |
| Lint suppressions, `anti-slop/no-known-value-widening` | 1 | 1 |
| Lint suppressions, `anti-slop/no-object-parameters` | 1 | 1 |
| Lint suppressions, `anti-slop/no-reflect-apply` | 1 | 1 |
| Lint suppressions, `anti-slop/no-reflect-get` | 2 | 2 |
| Lint suppressions, `anti-slop/no-shape-in-symbol-names` | 16 | 16 |
| Lint suppressions, `anti-slop/no-unknown-parameters` | 39 | 39 |
| Lint suppressions, `anti-slop/no-unknown-returns` | 7 | 7 |
| Lint suppressions, `anti-slop/no-unsafe-dictionary-type` | 79 | 79 |
| Lint suppressions, `eslint/max-params` | 6 | 6 |
| Lint suppressions, `no-empty` | 3 | 3 |
| Lint suppressions, `typescript/no-redundant-type-constituents` | 1 | 1
|
| Lint suppressions, `typescript/no-unsafe-assignment` | 2 | 2 |
| Lint suppressions, `typescript/no-unsafe-return` | 2 | 2 |
| `@ts-expect-error` | 0 | 0 |
| `@ts-ignore` | 0 | 0 |
| `SAFETY:` comments | 35 | 35 |
| Lint config rule-offs, all rules | 7 | 7 |
| Lint config rule-offs, `no-redeclare for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
| Lint config rule-offs, `no-unused-vars for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
| Lint config rule-offs, `vue/html-indent for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
| Lint config rule-offs, `vue/max-attributes-per-line for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
| Lint config rule-offs, `vue/multi-word-component-names for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
| Lint config rule-offs, `vue/require-default-prop for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
| Lint config rule-offs, `vue/singleline-html-element-content-newline
for packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
<!-- antelopejs-proof base=debd8d0ad82074c42be8cae54f15e4dc0d70c530
head=bc89cc71b2cf89ae7b048a67f17e53903bff0348 lines=37
digest=f18e0ecdf4da36d7 command: antelopejs-api-diff origin/main
--allow-added '**/internal/**' -->

## Attention

- **Behaviour changes, on malformed input only:**
- A JSON body that is not an object now gets a 400. Before, `null`
crashed with a 502, and other values reached the backend.
- A 2FA step or tenant assignment without its token or a `string[]` of
methods is refused.
- A sign-in answer that sets `requires_2fa` or
`requires_tenant_assignment` never opens a session; with a malformed
step it fails as a backend error (main returned it as a step). A flag
that is `null`, `0` or `""` on a session answer is refused too; the
backend never sends a flag on one.
- An empty `access_token`, `refresh_token`, `two_factor_token`,
`tenant_assignment_token`, OAuth `state` or `authorizeUrl` is refused,
so an empty sealed `state` can no longer match an empty `?state=`.
- A user whose `_id`, `id`, `email`, `name` or `language` is not a
string is refused at sign-in. No backend write path stores one. A stored
account reads its user through the same schema, which replaces the `as
SessionUser` assertion in `accounts.ts`.
- `register` and `request-2fa-email` now refuse a body that is not a
JSON object with 400, like the other routes.
- **Live sign-in did not run.** `dms-control up --prepare` fails to
start the backend on main itself (`this.table.get(...).then is not a
function`): main's playground pins `@antelopejs/mongodb` 1.6.2, which
predates interface-database 0.4. #907 fixes that.
- 2FA and OAuth are proven only by route tests against a fake backend;
the playground configures neither.
- The schemas were matched to the backend routes by reading their types
(`AuthResponse`, `LoginOutcome`, `OAuthAuthorizeUrl`, `sanitizeUser`).
- **No independent review has run yet.**
- **Order:** this PR lands first; dms-frontend#338 follows. The two
removed `as string` assertions may meet ANT-2241's later slices on
rebase.

## Merge Danger

**Door:** two-way. Server-side parsing only. Published auto-imports and
runtime exports are unchanged (api-diff above).

**Blast Radius:** moderate. Every sign-in, session renewal, tenant and
account switch, and OAuth flow, on malformed backend answers or bodies.
Upd4ting added a commit that referenced this pull request Nov 14, 2026
…910)

Part of ANT-1088: every suppression in `guardrails.suppressions.json`
goes. Antony decided to rename the single-word Vue components to
multi-word names and to turn `vue/multi-word-component-names` back on as
an error, exempting only the Nuxt pages so routes do not change. This
removes the frontend-vue config off for that rule.

⚠ sensitive: it is breaking. Component names are registered under the
`Dms` prefix and named by string in backend code, stored mockups and
page drafts. It changes the strings interface-dms's builders emit, and
it is well over 400 lines (mostly renames).

## Summary

All 38 single-word components are renamed with `git mv`. Each row is
`Dms<Old>` → `Dms<New>`, and the kebab form follows (`dms-card` →
`dms-card-panel`).

| Old | New | Old | New |
| --- | --- | --- | --- |
| Container | LayoutContainer | Cascader | InputCascader |
| Footer | LayoutFooter | Checkbox | InputCheckbox |
| Spacer | StackSpacer | Display | DisplayValue |
| Platform | StepPlatform | File | InputFile |
| Ready | StepReady | Image | InputImage |
| Register | StepRegister | Relation | InputRelation |
| Actions | TableActions | Repeater | InputRepeater |
| Empty | TableEmpty | Select | InputSelect |
| Menu | TableMenu | Slider | InputSlider |
| Pagination | TablePagination | Switch | InputSwitch |
| Table | TableRoot | Textarea | InputTextarea |
| Tabs | TableTabs | Grid | GridBlock |
| Views | TableViews | Guard | GuardBlock |
| Placeholder | PlaceholderBlock | Meter | MeterBar |
| Banner | NoticeBanner | Eyebrow | EyebrowLabel |
| Card | CardPanel | Section | SectionPanel |
| Chart | ChartBlock | Segmented | SegmentedControl |
| Sparkline | SparklineChart | Tab | TabBlock |
| Form | FormBlock | Calendar | InputCalendar |

- **Entry files renamed to match their component:** `layout/Footer`,
`onboarding/steps/ready`, `table/Actions`, `table/Empty`,
`table/Pagination`, `table/Table` and `table/Views`.
- **References updated:**
- relative and `#dms-*` imports, entry re-exports and `import.meta.glob`
paths;
  - `Dms<Old>` in templates, `resolveComponent` and test registrations;
- the kebab strings emitted in `packages/interface-dms/src`,
`packages/dms/src`, frontend-vue and the playground (`dms-chart`,
`dms-form`, `dms-grid`, `dms-guard`, `dms-placeholder`, `dms-spacer`,
`dms-tab` and the eleven inputs);
  - `COMPONENT_ALIASES`;
  - the probe manifest and `check-page-invariants.mjs`;
  - the mockup-starters fixture;
  - `docs-data/blocks.json` (regenerated) and `examples.json`;
  - the prose docs.
- **Not updated:** the older migration guides and changelogs, which stay
as history.
- **Rule configuration:**

  ```js
  "vue/multi-word-component-names": ["error", { ignores: [
"Icon", "2fa", "accounts", "backup", "error", "forgot", "login",
"modules",
"onboarding", "recover", "roles", "shortcuts", "signup", "validate",
"[...slug]",
  ] }]
  ```

- The page names are the twelve custom pages, the `[...slug]` dynamic
page and the app-level `error.vue`.
- `Icon` is Nuxt's own component. Two tests register a stub under that
fixed upstream name.
- **Budget:** the config entry for `vue/multi-word-component-names` goes
from 1 to 0, and nothing else changes.
- **Documentation:**
- `packages/interface-dms/UPGRADING.md` (Unreleased) has the full old →
new table.
- `packages/dms/docs/02.building/18.migration-0-13-to-0-14.md` explains
what breaks: hand-written name strings, stored mockups and drafts,
templates in modules, and the seven entry paths. Its Cascader section
now names `DmsInputCascader`.

## Evidence

- **Before:** `eslint` with the rule on reports every single-word
component.
**After:** `pnpm --dir packages/dms/frontend-vue exec eslint .` gives 0
errors with the rule at `error`.
- **Suites:**
  - `pnpm run checks` 11 passed;
- frontend-vue typecheck clean; vitest 3506 passed, 1 skipped
(`--testTimeout=60000`; at the default, 2 to 4 heavy-import tests time
out under a load average of 30 and pass alone);
  - `dms test:unit` 2627, `test:integration` 419;
  - the playground tests pass.
- **Live:** with the `verify-dms` skill, the `smoke` journey PASSES,
21/21 checks over 15 steps. Doctor is clean, and no log says "failed to
resolve component".
- The playground backend does not start on main, because main's
playground resolves `@antelopejs/mongodb` 1.8.0 against
interface-database-decorators 0.3.1 (#907 fixes that).
- The run used a verification-only `@antelopejs/mongodb` 1.6.4 pin,
which is not committed.

API: differences against origin/main (committed HEAD).
diff --git
a/base/packages/interface-dms/exports/subpath/base%2Finternal%2Fchart.d.ts
b/head/packages/interface-dms/exports/subpath/base%2Finternal%2Fchart.d.ts
index 1a31648..6a592cf 100644
---
a/base/packages/interface-dms/exports/subpath/base%2Finternal%2Fchart.d.ts
+++
b/head/packages/interface-dms/exports/subpath/base%2Finternal%2Fchart.d.ts
@@ -1,4 +1,4 @@
-export declare const CHART_COMPONENT_NAME = "dms-chart";
+export declare const CHART_COMPONENT_NAME = "dms-chart-block";
export declare const CHART_META_BY_TYPE: Record<ChartType,
ChartTypeMeta>;
 declare enum ChartType {
     LINE = "line",
diff --git
a/base/packages/interface-dms/exports/subpath/base%2Finternal%2Fform-block-schema.d.ts
b/head/packages/interface-dms/exports/subpath/base%2Finternal%2Fform-block-schema.d.ts
index ee3e30d..77eca52 100644
---
a/base/packages/interface-dms/exports/subpath/base%2Finternal%2Fform-block-schema.d.ts
+++
b/head/packages/interface-dms/exports/subpath/base%2Finternal%2Fform-block-schema.d.ts
@@ -1,2 +1,2 @@
 export {};
-export declare const FORM_COMPONENT_NAME = "dms-form";
+export declare const FORM_COMPONENT_NAME = "dms-form-block";
API: skipped ., private.
API: skipped packages/dms/playground, private.
API: skipped packages/dms/playground/frontend-vue, private.
Runtime API: 3 packages, 376 entries unchanged against origin/main
(committed HEAD).
Auto-imports: 1 packages, 944 entries unchanged against origin/main
(committed HEAD).
No behavioural probe ran.

Suppressions in authored code files (origin/main and committed HEAD). A
lint directive counts once per rule it names; `*` is a directive naming
none.
| Suppressions | base | HEAD |
| --- | ---: | ---: |
| Lint suppressions, all rules | 163 | 163 |
| Lint suppressions, `@typescript-eslint/no-unused-vars` | 2 | 2 |
| Lint suppressions, `anti-slop/no-chained-type-assertions` | 1 | 1 |
| Lint suppressions, `anti-slop/no-known-value-widening` | 1 | 1 |
| Lint suppressions, `anti-slop/no-object-parameters` | 1 | 1 |
| Lint suppressions, `anti-slop/no-reflect-apply` | 1 | 1 |
| Lint suppressions, `anti-slop/no-reflect-get` | 2 | 2 |
| Lint suppressions, `anti-slop/no-shape-in-symbol-names` | 16 | 16 |
| Lint suppressions, `anti-slop/no-unknown-parameters` | 39 | 39 |
| Lint suppressions, `anti-slop/no-unknown-returns` | 7 | 7 |
| Lint suppressions, `anti-slop/no-unsafe-dictionary-type` | 79 | 79 |
| Lint suppressions, `eslint/max-params` | 6 | 6 |
| Lint suppressions, `no-empty` | 3 | 3 |
| Lint suppressions, `typescript/no-redundant-type-constituents` | 1 | 1
|
| Lint suppressions, `typescript/no-unsafe-assignment` | 2 | 2 |
| Lint suppressions, `typescript/no-unsafe-return` | 2 | 2 |
| `@ts-expect-error` | 0 | 0 |
| `@ts-ignore` | 0 | 0 |
| `SAFETY:` comments | 35 | 35 |
| Lint config rule-offs, all rules | 7 | 6 |
| Lint config rule-offs, `no-redeclare for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
| Lint config rule-offs, `no-unused-vars for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
| Lint config rule-offs, `vue/html-indent for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
| Lint config rule-offs, `vue/max-attributes-per-line for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
| Lint config rule-offs, `vue/multi-word-component-names for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 0 |
| Lint config rule-offs, `vue/require-default-prop for
packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
| Lint config rule-offs, `vue/singleline-html-element-content-newline
for packages/dms/frontend-vue/**/*.{js,mjs,cjs,ts,vue}` | 1 | 1 |
<!-- antelopejs-proof base=debd8d0ad82074c42be8cae54f15e4dc0d70c530
head=4193595036a99111ccb28288156b1015e053f98b lines=55
digest=fe26bf8b5740506d command: antelopejs-api-diff origin/main
--allow-added '**/internal/**' -->

Runtime exports and auto-imports are unchanged, because components are
registered, not auto-imported. The only differences api-diff reports are
two `@internal` constants: `CHART_COMPONENT_NAME` (`dms-chart-block`)
and `FORM_COMPONENT_NAME` (`dms-form-block`).

## Attention

- **Stored mockups and page drafts** that hold an old `componentName`
fail to compile after the upgrade. The guide says so, and no alias is
kept, by Antony's decision that this is breaking.
- **Names to check:** `StackSpacer`, `TableRoot`, and the `*Block`
suffix on Grid, Guard, Tab, Chart, Form and Placeholder were judgement
calls.
- **After the Nuxt-preset PR:** the preset's `onResolved` rewrites
`vue/multi-word-component-names` to off in any block appended after it.
Whichever of the two PRs lands second passes this rule through
`antelopeNuxtConfig({ rules: … })`, so it stays on. I do that rebase.
- One test thumbnail prop changed from `demo` to `demo-mockup`.

## Merge Danger

**Door:** one-way once released. Modules and stored specs that name
components by string must migrate.

**Blast Radius:** wide. Every registered DMS component name, the strings
the interface builders emit, stored mockups and drafts, and modules that
render DMS components by name.
Upd4ting added a commit that referenced this pull request Nov 14, 2026
…form ownership from user.owner (#913)

⚠ sensitive (permissions)

## Why

A member allowed to edit roles could make themselves the platform owner.
The roles API let them add `*` to any role, their own included. Four
checks read `*` as platform ownership: the module store, the modules
listing, `isOwner` in the site layout, and `permissionsOpenPage` for
owner-only pages. #887 already stopped `assertPlatformOwner` from
accepting `*`, and this PR closes the rest.

Task: ANT-2425, found by the review of ANT-2391 (#887).

## What changes

- **The roles API grants only what the editor holds.** Creating,
updating or copying a role answers 403
(`$page.settings.roles.error.ungrantable`) when the role would gain `*`
or a permission the editor does not hold. That covers the editor routes
(`create`, `PUT :id`, `:id/duplicate`) and the roles table routes
(`/api/tables/roles/new` and `/edit`, through new `new` and `edit`
guards). `*` is refused to everyone, the platform owner included, so no
role holds it. The check runs on the stored set, after
`withRequiredPermissions` adds ancestors and dependencies. An edit may
keep what the role held already, so an editor can rename a role that
holds more than they do.
- **Ownership comes from `user.owner`.** `buildAccessContext` sets
`isOwner` from `user.owner`, not from `*` in the effective set. That
feeds `isPlatformOwner` (the module store), `/dms/modules-listing`,
`isOwner` in `/dms/sitelayout`, and menu access. `permissionsOpenPage`
takes `{ permissions, isOwner }`. `userCanAccessPage` passes
`user.owner`, and `page_verify`'s `roleOpensPage` passes `false`,
because a role is never the owner. `assertPlatformOwner` drops its
separate `user.owner` test, which `isPlatformOwner` now carries.
- **Docs.** "Who can grant what" says the roles routes cap grants to
what the editor holds. The member and invitation editors keep no cap, as
before. The Modules docs and the `dms-module` skill call the platform
owner the user flagged `owner`, not the `*` holder.

Unchanged on purpose:
- The role preview still treats a previewed `*` as the owner. It writes
nothing, and it shows only what the viewer is already served.
- A member editor can still give any member any role, as "Who can grant
what" documents.
- The `dms-saas` resolvers keep `*` for a platform owner outside a
preview, so reading `user.owner` gives no owner more access than before.

```mermaid
flowchart LR
  subgraph Roles API
    W[create / update / duplicate / table new, edit] --> C{role gains * or a permission the editor lacks?}
    C -- yes --> F[403 ungrantable]
    C -- no --> S[role saved]
  end
  subgraph Owner checks
    R[store, modules-listing, sitelayout, owner-only page] --> O{user.owner === true}
    O -- no --> D[refused / isOwner false]
    O -- yes --> G{tenant gate allows}
    G -- yes --> A[served]
    G -- no --> D
  end
```

Before, the owner checks branched on `permissions.has("*")`, which any
role holding `*` passed.

## Proof

New tests: `src/test/integration/role-wildcard-owner.test.ts` and one
case in `src/test/unit/implementations/dms/owner-only-page.test.ts`. A
member whose stored role holds `*` is refused on each of the four
checks. The roles API refuses `*` and unheld permissions on every write
route, and still saves held ones and keeps a role's existing ones.

**Before**: the new tests against origin/main (debd8d0):

```
  6 passing (4s)
  9 failing

  1) [integration] a member whose role holds *  is refused the module store before its body is read:
      AssertionError: [{"code":"invalid_type",...,"path":["id"],"message":"Required"}]: expected 400 to equal 403
  2) [integration] a member whose role holds *  is refused the modules listing:
      -200
      +403
  3) [integration] a member whose role holds *  is not the owner in the site layout:
      AssertionError: expected true to equal false
  4) [integration] the roles API grants only what the editor holds  refuses * on the editor's own role, and leaves it unchanged:
      -200
      +403
  5) [integration] the roles API grants only what the editor holds  refuses a permission the editor does not hold:
      -200
      +403
  6) [integration] the roles API grants only what the editor holds  refuses * to the platform owner too:
      AssertionError: {"id":"cffd5836-…"}: expected 200 to equal 403
  7) [integration] the roles API grants only what the editor holds  refuses * and unheld permissions through the roles table routes:
      AssertionError: {"_id":"dfb7acc8-…","name":"Everything","permissions":["*"],…}: expected 200 to equal 403
  8) [integration] the roles API grants only what the editor holds  refuses to copy a role holding what the editor does not:
      AssertionError: {"id":"4950bf63-…"}: expected 200 to equal 403
  9) [unit] implementations/dms/page — ownerOnly pages  denies a member whose role holds *, which only the owner holds:
      AssertionError: expected true to equal false
```

The member got past the store's owner check to body validation (400),
was served the modules listing, and was `isOwner: true` in the site
layout.

**After**: on this branch, the new tests with the existing role suites:

```
  [integration] a member whose role holds *
    ✔ is refused the module store before its body is read
    ✔ is refused the modules listing
    ✔ is not the owner in the site layout
  [integration] the roles API grants only what the editor holds
    ✔ refuses * on the editor's own role, and leaves it unchanged
    ✔ refuses a permission the editor does not hold
    ✔ refuses * to the platform owner too
    ✔ refuses * and unheld permissions through the roles table routes
    ✔ refuses to copy a role holding what the editor does not
    ✔ grants what the editor holds, and keeps what a role held already
  [integration] role permissions and their ancestors
    ✔ (7 cases)
  [integration] role delete with reassignment
    ✔ (2 cases)
  [unit] implementations/dms/page — ownerOnly pages
    ✔ denies a member whose role holds *, which only the owner holds
    ✔ (5 other cases)
  24 passing (18s)
```

**verify-dms.** The `settings-roles` feature gains the
`role-editor-grants-only-held` journey. A member who can edit roles
ticks "Export reports" on their own role and saves. They get "You can’t
grant permissions you don’t hold yourself.", the stored role is
unchanged, and their `PUT` adding `*` and their `GET
/dms/modules-listing` both answer 403. The platform owner then saves the
same tick. The journey has not run yet. `dms-control up` cannot start
the playground on this base: its backend stops with
`this.table.get(...).then is not a function`, because the playground's
`interface-database-decorators` 0.3.1 awaits a query that
`interface-database` 0.4.0 no longer runs. This PR changes no manifest,
and #907 moves the playground to 0.4.0.

The full backend suites pass on this branch: `test:unit` 2628 passing,
`test:integration` 430 passing.

Stored data: a role saved with `*` before this change keeps it. It no
longer passes the four owner checks above, and a role editor can no
longer save it with `*` kept. `HasPermission` still reads `*` as every
permission, module-scoped ones included, so such a role still opens
module table routes. Only a role written before this change, or straight
to the database, can hold `*`. Making `GetUserPermissions` drop `*` from
role grants closes that in `interface-dms`, and it is proposed as a
follow-up.
Upd4ting added a commit that referenced this pull request Nov 14, 2026
## Why

A mockup spec may write a `Form` whose `sections` each hold their own
`fields`, the shape the `Form` factory takes and the block schema
describes. The `dms-form` component reads the shape the factory
serializes instead: every field in `fields`, and each section naming its
fields by `fieldIds`. So such a spec passed validation, and its preview
then crashed (`Cannot read properties of undefined (reading 'map')`).
Because of that, the Edit-mode Form starter could not ship with a
section.

## What

- The mockup compiler hands a `Form` or `ResourceForm` the layout the
factory serializes. Each section's own fields move after the form's
`fields`, in order, and the section names them in `fieldIds`. It still
validates the spec as written, so an issue points into the section where
the author wrote the field.
- `componentOptionsOf` in `validate.ts` runs a new `toComponentOptions`
step on each preview adapter. Only the form adapter changes anything.
- A section that writes both `fields` and `fieldIds` gets an issue at
its `fieldIds`. Before, the written ids were dropped with no issue.
- The Edit-mode Form starter now has a `Contact` section holding
`Email`, next to a top-level `Name`. The starter fixture `nodes.json` is
regenerated.
- The mockups docs page describes both section shapes.

```mermaid
flowchart LR
  spec["spec Form: sections[].fields"] --> validate["validateBlockOptions (spec as written)"]
  validate --> issues["issues at /sections/i/fields/j"]
  spec --> serialize["componentOptionsOf: fields += section fields, section.fieldIds"]
  serialize --> component["dms-form draws the sections"]
```

## Proof

`packages/dms/src/test/unit/mockup/form-sections.test.ts` compares the
compiled options with what the real `Form` factory serializes, for
`Form` and for `ResourceForm`.

Before (on `origin/main` sources):

```
  [unit] a mockup form written with sections that hold their fields
    1) reaches the component in the layout the Form factory serializes (fails when the sections reach it with fields and no fieldIds)
    2) reaches the component of a ResourceForm in the same layout (fails when only the Form block is serialized)
    ✔ keeps a section that names its fields by id as written (fails when the serialized layout is rewritten)
    ✔ reports a bad field inside a section at its pointer in the spec (fails when validation reads the serialized layout)

  2 passing (136ms)
  2 failing

      AssertionError: expected { fieldIds: [ 'intro' ], …(1) } to deeply equal { fieldIds: [ 'intro', …(3) ], …(1) }
```

After:

```
    ✔ reaches the component in the layout the Form factory serializes (fails when the sections reach it with fields and no fieldIds)
    ✔ reaches the component of a ResourceForm in the same layout (fails when only the Form block is serialized)
    ✔ keeps a section that names its fields by id as written (fails when the serialized layout is rewritten)
    ✔ reports a bad field inside a section at its pointer in the spec (fails when validation reads the serialized layout)
    ✔ reports a section that both holds fields and names them by id, at its fieldIds (fails when the written ids are dropped in silence)
```

The `mockup/compile`, `mockup/starter-nodes` and frontend
`mockup-starters` and `mockup-catalog` suites pass. On a verify-dms
playground run, the `smoke`, `mockups` and `module-pages` journeys all
pass. A spec Form with sections draws them in `/mockup`, and adding a
Form from the Edit-mode Add tab draws its `Contact` section. Screenshots
and a video are kept outside the PR.

## Attention

The playground's dev backend does not start on `main` today. The
`@antelopejs/mongodb` `^1.4.3` range resolves 1.8.0, which implements
interface-database 0.4.0, and `interface-database-decorators` 0.3.1
still calls `.then` on a query. For the proof runs only, the mongodb
module was pinned to 1.7.0 in the run's copy, and that pin is not
committed. #907 moves dms to interface-database 0.4.0.
Upd4ting added a commit that referenced this pull request Nov 14, 2026
…ol up --dev-backend (#942)

## Why

An agent that reads the verify-dms feature files to drive a
development-only page should be pointed at the harness command that
starts a development backend. It should not learn that it has to start
one by hand, or that it cannot drive the page at all. dms#881 added
`dms-control up --dev-backend`, and journeys name their backend with
`backend: "dev"` or `"production"`.

## What

- `features/branding.md`. The gotcha said a run's backend is always `ajs
project start`, so the editor "cannot be driven there". It now says
`branding-absent-outside-dev` pins `backend: "production"`, and that
driving the editor by hand needs `dms-control up --dev-backend` and an
administrator session. Both pages have been owner-only since #887.
- `features/mockups.md`. #887 already pointed this gotcha at `up
--dev-backend`. It now names the full command, `dms-control up
--dev-backend`, as Branding does.

## Proof

Docs-only change to two agent-facing Markdown files. No code, test or
config changes.

Gotcha before:

```
- A run's backend is `ajs project start` on a build, so it is not in development and the editor itself cannot be driven there; only its absence is.
```

Gotcha after:

```
- `branding-absent-outside-dev` sets `backend: "production"`, so it passes in any run. To drive the editor by hand, run `dms-control up --dev-backend` and sign the browser in as the administrator first: the page refuses anyone else.
```

The statements are taken from the code. `journeys/branding.mjs` sets
`backend: "production"`. SKILL.md describes `--dev-backend` as running
`ajs project dev -e docs`.
`src/test/integration/owner-only-dev-pages.test.ts` covers the
owner-only Branding and mockup pages.

The drive itself did not run end to end. On main (`debd8d0ad`),
`dms-control up --prepare --dev-backend` and a plain `dms-control up`
both stop with a backend that fails to start:

```
✖ Failed to start modules
  this.table.get(...).then is not a function
```

The playground resolves `@antelopejs/interface-database-decorators`
0.3.1, and its `Model.get` calls `.then` on a query. That breakage is
unrelated to this PR. #907 moves to interface-database 0.4.0.
Upd4ting added a commit that referenced this pull request Nov 14, 2026
…944)

## Why

The verify-dms journey `mcp-server/page-verify-reports-a-page` fails
with `desktop: block table (dms-table-view) is missing` on the
playground's users page. That page draws its table. The cause is
`page_verify` itself, not the journey's page.

## Cause

The page probe walked the Vue tree from
`element.__vue_app__._instance.subTree`. Vue sets `app._instance` only
in a development build or one with `__VUE_PROD_DEVTOOLS__`. The
dashboard `ajs dms build` serves is a production build, so the walk
found no `RecursiveComponent` and every block of the layout read as
missing. The table's data check still passed, because it reads the
`data-dms-block-state` attributes from the DOM.

On the live run, the users page held one mount root (`#app`) with
`__vue_app__` set, `_instance` null, and `#app._vnode` set to the root
component vnode.

## Change

The probe walks from `element._vnode`, the root vnode that Vue's
`render` and `hydrate` store on the mount element in every build.

```mermaid
flowchart LR
  root["mount element (#app, data-server-rendered)"] -->|before| inst["__vue_app__._instance.subTree<br/>(null in a production build)"] --> none["no block found: every block 'is missing'"]
  root -->|after| vnode["_vnode (set by render and hydrate in every build)"] --> walk["walk to each RecursiveComponent"] --> found["blocks found by layoutPath"]
```

## Proof

`src/test/unit/mcp/page-verify-probe.test.ts` mounts an app from Vue's
production build in jsdom, asserts the app holds no `_instance`, and
runs `probePage` on it. Probing from `_instance`, as before, it finds no
block.

Before (the new test against the old probe):

```
  0 passing (161ms)
  1 failing

  1) [unit] mcp page_verify probe
       finds the blocks of an app mounted from Vue's production build, which keeps no app instance:

      AssertionError: expected [] to deeply equal [ { path: 'table', …(1) } ]
      + expected - actual

      -[]
      +[
      +  {
      +    "componentName": "dms-table-view"
      +    "path": "table"
      +  }
      +]
```

After (`page-verify-probe.test.js` and `page-verify.test.js`, under
`scripts/backend-runtime-boundaries.cjs` as `test:unit` runs them):

```
  9 passing (234ms)
```

The journey on a dms-control run, before:

```
FAIL mcp-server/page-verify-reports-a-page
    03-the-users-page-passes-and-the-unknown-route-fails: users page passes (["desktop: block table (dms-table-view) is missing"])
```

After, the whole `mcp-server` feature:

```
PASS bridge-initializes-and-lists-tools
PASS bridge-calls-a-read-tool
PASS bridge-refuses-unknown-tool-and-method
PASS route-refuses-uncredentialled-callers
PASS page-verify-reports-a-page
PASS bridge-files-published
```

Both runs used #907's head with and without this change. On main today
the playground backend does not start (`this.table.get(...).then is not
a function`). The playground's `@antelopejs/mongodb` `^1.4.3` now
resolves 1.8.0, which implements interface-database 0.4.0, while the
lockfile pins interface-database-decorators 0.3.1. #907 moves dms to
interface-database 0.4.0. The journey passes on main once both PRs land.
Upd4ting added a commit that referenced this pull request Nov 14, 2026
…945)

## Why

No test ran `page_verify` as a member role against a mockup (seen in
ANT-2391). The unit harness has no browser, so the path where the page
opens as a member while the `/mockup` viewer, which only the owner may
open, opens through the owner's session was never exercised.

## What

A verify-dms journey, `mcp-server/page-verify-member-against-mockup`, on
a development backend:

1. writes a one-table mockup to the run's `mockups/page-verify-member/`;
2. creates a role holding the users page and its table, and invites a
member with only that role;
3. checks the owner reads `GET /api/mockup/page-verify-member` (200) and
the member is refused (403);
4. calls `page_verify` through the stdio bridge with `role` and
`mockup`, and checks the report names the member, the viewer drew the
mockup, its screenshot is written, the mockup table pairs with the page
table, and the member's page drew that table.

The feature file lists the journey and a gotcha: run the `page_verify`
journeys on `up --dev-frontend`. On the built dashboard the tool's block
probe misses the users table, which the page draws. The existing
`page-verify-reports-a-page` fails there for that reason, and passes on
the dev frontend.

```mermaid
sequenceDiagram
  participant J as journey
  participant B as page_verify (bridge)
  participant M as member context
  participant O as owner context
  J->>J: write mockups/page-verify-member/spec.json
  J->>J: role + invited member (only that role)
  J->>B: page_verify {page, role, mockup}
  B->>M: open /table-view/table-view-users
  B->>O: open /mockup?name=page-verify-member (owner-only)
  B-->>J: report: account=member, mockup matched
```

## Proof

Run on a playground started with `dms-control up --dev-frontend`. `main`
cannot boot the playground today: `@antelopejs/mongodb ^1.4.3` resolves
a release for interface-database 0.4.0 while `main` runs decorators
0.3.1 (`this.table.get(...).then is not a function`). #907 fixes that,
so the run used a scratch checkout of #907's head with this PR's two
files copied in.

Before (the journey did not exist):

```
$ dms-control journey "$RUN" mcp-server page-verify-member-against-mockup
dms-control: Unknown journeys of mcp-server: page-verify-member-against-mockup
```

After, the checks of the final step:

```
True signed in as a member
True the member is the invited one
True the member holds the asked role
True the comparison names the mockup
True the viewer drew the mockup
True the mockup screenshot is written
True the mockup table pairs with the page table
True the member's page drew that table
```

`page-verify-reports-a-page` and `page-verify-member-against-mockup`
both `PASS` on the same run.
@Upd4ting
Upd4ting force-pushed the fix/interface-dms-stored-row branch from 8b13090 to 0ff7f02 Compare November 14, 2026 02:44
Upd4ting added a commit that referenced this pull request Nov 14, 2026
ANT-2384. interface-data-api 0.7.0 replaced the published `RowFields`
with `StoredRow`. `@antelopejs/interface-dms` 0.13.3 still names
`RowFields` in its published declarations and accepts data-api `>=0.6.0
<1.0.0`. A fresh project resolves data-api 0.7, so a module the
Builder's Connect generates fails `tsc`.

⚠ sensitive: this changes the interface-dms contract: its published
declarations (metadata constructors, `.target`, `CreatedPage.target`,
`DataType.filter`, `StoredRow`) and its peer ranges.

## Published declarations and query parameters that change

Published declarations (interface-dms):
- `TableViewMeta`, `SearchableMeta` and `PageMetadata`: the constructor
parameter and the public `target` are `DecoratorTarget` (were `Class` /
`ControllerClass`). interface-core 0.5's `GetMetadata` constructs a
metadata class with whatever it is called on, prototype-chain links
included, down to `Object.prototype`.
- `CreatedPage.target` (what an `onCreated` hook receives) is
`DecoratorTarget` (was `ControllerClass`), since the hook receives the
`PageMetadata` itself.
- A reader that needs the controller narrows through `@internal`
`pageController` (`page/internal/page-controller`), which throws
`PageTargetError` (a `TypeError`) for a non-controller target; dms's
bulk selection checks that the target is a function and answers 500
otherwise, before its existing prototype check.
- `DataType.filter`'s `value` is `FilterValue[0]` (`string | boolean`),
and the table-view routes name `StoredRow` (see Summary).
- Peers: interface-data-api `>=0.8.0 <0.9.0`,
interface-database-decorators `>=0.5.2`, interface-core `>=0.5.1`; dms
and the playground take interface-data-api `>=0.8.0 <1.0.0` (the
interface range check refuses a cap below the next breaking release
there), interface-api `>=0.3.0`, interface-core `>=0.5.1` and
`@antelopejs/core` `>=1.23.0` (core 1.21 pins interface-core 0.4.0
exactly).
- interface-database stays on 0.3 here: interface-data-api 0.8.0's
`>=0.4.0 <0.5.0` peer is unmet on this PR and CI is green with it; #907
moves to 0.4.

Query parameters (interface-api 0.3 types `@Parameter(name, "query")` as
`string | true | undefined`; `?name` with no value arrives as `true`).
Main's #934 typed the 12 dms parameters and added `queryText`, which
reads a valueless parameter as absent. Four of them still let `true`
through, and this PR routes them through `queryText`: notifications
`limit` and `offset` (`Number(true)` is 1, so `?offset` skipped the
first notification) and files `resourceKey` and `storage` (`true`
reached the metadata schema, a 400 with another message). None of the 12
is read as a flag:

| Route | Parameter | A valueless `?name` now |
| --- | --- | --- |
| `GET /dms/page` | `path` | 400 `error.path_required`, as when missing
|
| `GET /dms/page` | `shared` | absent: the shared page is included, as
without it |
| `GET /dms/frontend` | `clientUrl`, `renderer`, `rendererVersion` |
absent, the manifest as without them |
| `GET /dms/pagelayout` | `slug` | 400 `error.slug_required`, as when
missing |
| `GET <notifications>/list` and `/inbox` | `limit`, `offset` | absent:
default page size and offset (fixed here) |
| `GET /api/files/metadata` | `resourceKey` | 400, as when missing
(fixed here) |
| `GET /api/files/metadata` | `storage` | absent, the default storage
(fixed here) |

`src/test/integration/valueless-query.test.ts` checks each against the
same request without the parameter (5 tests, 12 parameters). Before the
fix, the notifications and file metadata tests failed:

```
/dms/pages/settings/user/notifications/list?limit&offset
-  "body": []
+  "body": [ { "_id": "notification-recent:…", "title": "$dms.notifications.messages.welcome.title", … } ]

/api/files/metadata?resourceKey
-      "message": "Expected string, received boolean"
+      "message": "Required"
```

The playground (a private demo module) follows the same rule at its
query parameters through its own `queryText` (main's #934); it is
covered by its typecheck only.

## Summary

```diff
 @antelopejs/interface-dms
-  peer interface-data-api >=0.6.0 <1.0.0
+  peer interface-data-api >=0.8.0 <0.9.0
-  DataType.filter(…, value: string, …)
+  DataType.filter(…, value: FilterValue[0], …)   // string | boolean; compare modes get String(value)
   dist/base/table-view/routes.d.ts  (TableViewRoutes.Get, Edit, get, edit)
-    DataControllerCallback<…, import("@antelopejs/interface-data-api/metadata").RowFields, …>
+    DataControllerCallback<…, import("@antelopejs/interface-data-api/metadata").StoredRow, …>
   internal/column-conditions.ts
-    row: RowFields
+    row: GuardRow   // NonNullable<Awaited<ReturnType<typeof fetchRowForGuard>>>

 @antelopejs/dms
-  RowFields in guard-row, footer-summary, search-route, table-view, invite routes, export jobs, row rules
+  StoredRow
-  String(row[idField])
+  rowIdText(row[idField])   // same text for primitives and dates; refuses a row, list or document id
   interface-database-decorators >=0.3.1  ->  >=0.5.2
```

Why decorators move: data-api 0.8 declares its modifier keys as
`Class<ContainerModifier>` and requires decorators `>=0.5.0`. Decorators
0.3.1's `LocalizationModifier` does not satisfy that type (TS2345 in
`src/test/attachment-host/native.ts`). With decorators 0.5.2 the
workspace typechecks on interface-database 0.3.1, and the unit suite
passes on mongodb 1.6.2. The interface-database 0.4.0 move stays in its
own PR (ANT-2382).

The `ContainerModifier` TS2635 in the task comes from data-api's own
declarations: data-api 0.7.1 and 0.6.3 write `typeof
ContainerModifier<any>`, which decorators 0.5.x refuses. data-api 0.8
fixes it, so the new floor covers it.

Why `DataType.filter` widens: data-api 0.8 types a filter value as
`string | boolean`, so a field's filter can be handed a boolean.
`DataType.filter` takes that type and hands its compare mode
`String(value)`, which the value parser reads back as the same boolean;
the date type's override reads it as text. Main's #947 now sends the
archive filter as the text `"true"`; its `@internal`
`filterArchiveField` takes the same `string | boolean` here and reads a
boolean as itself (`table-view-archive-field.test.ts` fails when a
boolean is refused). `packages/interface-dms/UPGRADING.md` has an
Unreleased section with these changes.

## Evidence

Consumer typecheck of a module the Builder generates. It is produced by
the engine (`MapResources` on a two-table catalog with a relation, then
`scaffoldLocalModule`) with the Builder's default compiler options and
no hand edit. It is installed from public npm with interface-database
0.4.0, decorators 0.5.2, interface-core 0.5.1 and interface-api 0.3.2.
Then `npx tsc --noEmit -p .`:

- **Before:** published interface-dms 0.13.3 with data-api 0.7.1, exit
2:
  ```
node_modules/@antelopejs/interface-data-api/dist/metadata.d.ts(39,48):
error TS2635: Type 'typeof ContainerModifier' has no signatures for
which the type argument list is applicable.
node_modules/@antelopejs/interface-data-api/dist/metadata.d.ts(166,74):
error TS2635: Type 'typeof ContainerModifier' has no signatures for
which the type argument list is applicable.
node_modules/@antelopejs/interface-data-api/dist/metadata.d.ts(278,76):
error TS2635: Type 'typeof ContainerModifier' has no signatures for
which the type argument list is applicable.

node_modules/@antelopejs/interface-dms/dist/base/table-view/routes.d.ts(12,115):
error TS2694: Namespace '".../interface-data-api/dist/metadata"' has no
exported member 'RowFields'.

node_modules/@antelopejs/interface-dms/dist/base/table-view/routes.d.ts(44,117):
error TS2694: Namespace '".../interface-data-api/dist/metadata"' has no
exported member 'RowFields'.

node_modules/@antelopejs/interface-dms/dist/base/table-view/routes.d.ts(77,122):
error TS2694: Namespace '".../interface-data-api/dist/metadata"' has no
exported member 'RowFields'.

node_modules/@antelopejs/interface-dms/dist/base/table-view/routes.d.ts(86,124):
error TS2694: Namespace '".../interface-data-api/dist/metadata"' has no
exported member 'RowFields'.
  ```
**After:** interface-dms packed from this branch with the data-api
tarball (then 2def09c, packed as 0.7.2): `tsc` exits 0 with no output.
- Forcing data-api 0.6.3 under this branch's interface-dms fails on
`StoredRow` at the same four lines, so the floor is needed.
- Workspace, rebased on main 1416277:
  - `pnpm run typecheck` passes, and backend-checks pass.
- Locally: the builder specs that failed on the previous CI run
(`query-response`, `query-roundtrip`, `compose-page`) pass 31/31 on
#934's emitter with no emitter change; `valueless-query` and
`table-view-archive` integration pass 9/9; `table-view-archive-field`
passes 5/5. CI runs the full suites.
- `pnpm run checks` fails only on `eslint(max-lines)` in
`dms-builder/engine/project-config.ts`, which fails the same way on main
and is being fixed outside this PR.

## Merge Danger

**Door:** two-way in the repo. The published contract is one-way once
released: interface-dms requires data-api `>=0.8.0 <0.9.0`, so a module
on data-api 0.6 must move with it.

**Blast Radius:** wide. It covers every module that types against
`TableViewRoutes.Get` or `Edit`, and every project's data-api and
decorators versions. dms-saas and dms-database consume this branch.
Base automatically changed from fix/interface-dms-stored-row to main November 14, 2026 03:03

This branch has not been deployed

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

Labels

tgz-pending Waits for a release; holds a .tgz-deps tarball

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant