Skip to content

Commit 01516dc

Browse files
docs(data-modeling): virtual objects — rows from your server, shown in any view (plugin-driver recipe) (#27349)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiVy7F2atKWpo5rp3YPrqddh)_ Fixes #27278 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 新增文档页「虚拟对象」:三分法(计算型 / 不可落地 / 实时 ⇒ 虚拟;慢变参考数据 ⇒ 真对象 + 定时拉取)、插件 driver 完整配方(driver 骨架、插件、数据源、外部对象、`current_user` 锁公式、`beforeUpdate` 守卫、甘特视图、权限集)、driver 实际收到的调用、共享模型规则、driver 形状 pin 测试模板、五个业务例;从 external-datasources、drivers 与模块首页链入。 - **为什么改:** #27130 这类客户要「行来自自家服务器、任何视图可看可改」,平台零协议改动已能做到,但没有一页可照抄的配方;AI 作者也照这页写。 - **风险与回滚:** 纯文档(外加文档审计范围清单自动生成的一行),revert 即回滚;页内整套栈与 driver 骨架已在真实 kernel 中启动跑通(见 Tests)。 - **先看:** `content/docs/data-modeling/virtual-objects.mdx` 的「Credentials」段(实测:插件 driver 的数据源上 `credentialsRef` 不被解析)与「Choose the sharing model on purpose」提示框;其次第一个 driver 骨架代码块。 - **两分钟验证:** `pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:skill-examples`,预期 `✅ 272 prose examples type-check`,页内两个 `os:check` 代码块在其中(变异后报 `virtual-objects.mdx:140` 与 `:288`)。 - **需要你做的:** 定夺插件 driver 的凭据通道(Acceptance notes 第 1 条);页面目前只写实测事实,并按插件约定让宿主经插件选项传入。 ## What changes - **New page `content/docs/data-modeling/virtual-objects.mdx`** ("Virtual objects — rows from your server, shown in any view"): - the three-way rule as a decision table (computed / must-not-land / real-time ⇒ virtual; slow-changing reference data ⇒ a real object fed by a scheduled pull), plus the pointer to federation when the remote is a reachable SQL database; - the recipe, worked as a port berth plan: the driver skeleton (translate the whole `where` or refuse by name, forward only remote-owned columns on `update`, `supports.transactionsUnsupported`), the plugin that publishes `driver.NAME` in `init`, and one `defineStack` block with the datasource (`driver` = the service suffix, `config: {}`, `checkOnBoot: false`), the external object (`sharingModel: 'public_read_write'`, the per-caller `locked` formula over `current_user`), the `beforeUpdate` guard with declared refusals, the gantt list view and a permission set; - the "a driver cannot opt out of correctness" rules, with #27277's engine floor named as planned, not existing; - the sharing-model callout (default model refuses every member edit; link #27332, no fix promised); - what the driver receives, call by call, as measured (object name not `remoteName`, `object` inside the query, the extra `id` / `updated_at` / `updated_by` keys, by-id reads before a write); - the driver shape-pin test template; - five business examples: port berth scheduling, SAP sales orders, HR directory, equipment live status, and the counter-example (codes and registries as a real object with a `mapping` + `job` pull). - **Links in:** a callout sentence and a See-also row on `external-datasources.mdx`, a paragraph after "Writing a driver" on `drivers.mdx`, a card on `data-modeling/index.mdx`, the nav entry in `data-modeling/meta.json`. - **`scripts/docs-audit/handwritten-docs.json`:** the one-line regeneration `check-audit-scope.mjs --write` produces for a new page. ## Tests Docs only; nothing published changes. Every code block on the page was run before it was written down: - **The whole recipe boots.** A throwaway copy of the reference test's boot harness (`packages/runtime`, same plugin set as `plugin-driver-external-object.spike.test.ts`) with the page's driver skeleton, plugin and `defineStack` block (identical but for comments) behind an in-memory transport: 6/6 passed under the verify lock (`VERDICT command-exit 0`). Measured: list read with the view's filter, sort and `top=50` reaches the remote as `filter [archived eq false]`, `sort start_date asc`, `limit 50`, then a `count` with the same filter; the formula reads `locked: false` for the planner and `true` for the administrator; a PATCH on a locked slot answers `409 RESOURCE_CONFLICT` / `declaredCode: PLAN_LOCKED`; the administrator's PATCH answers `403 PERMISSION_DENIED` / `declaredCode: NOT_PLANNER` with no remote write; the planner's PATCH makes three by-id reads and one `patch` carrying exactly `start_date`, `end_date`, `plan_note`; an outsider's list read is `403`; a filter the driver refuses (`>=`) answers `500 INTERNAL_ERROR`. The probe files were deleted; the tree carries no new test. - **The shape-pin template passes against the skeleton:** 5/5 (`vitest run`, identical text to the page's block except the import path). - **The reference test still holds on this base:** `plugin-driver-external-object.spike.test.ts` 12/12 (`VERDICT command-exit 0`). - **The `os:check` blocks are really compiled:** with `transactionsUnsupported: 'yes'` and `sharingModel: 'public_rw'` injected, `check:skill-examples` exits 1 naming `virtual-objects.mdx:140:17` and `:288:7`; restored by `git checkout HEAD`, blob hash equal to HEAD. ## Acceptance notes 1. **`credentialsRef` is not resolved for a plugin driver** (measured, seam level). `DatasourceConnectionService.attemptConnect` returns `already-registered` for a driver a plugin registered before it reaches credential resolution. Probe on `6d7634a9`: pre-registered driver + `credentialsRef` ⇒ `already-registered` with no secret store and with a resolver (resolver calls 0); not pre-registered ⇒ `failed-credentials` / `connected` (resolver calls 1). So the fail-closed sentence in external-datasources §4 does not hold for a plugin driver, and the epic's standing rule "credentials only through `credentialsRef`" has no channel on Tier A today. The page states the fact and points to the plugin convention (the host passes options to the plugin it composes); it does not claim a platform channel. Not filed: the reach is a seam probe, not a public door. 2. **"DataSync" in the card is spelled as what exists.** `DataSyncConfig` is retired (never executed); a sync is defined on its target as a `mapping` with `connectorSource` driven by a `job` `pull`. The counter-example links `/docs/automation/jobs#pulling-a-mapping` and says there is no separate data-sync type. 3. **Mounting through `defineStack({ plugins })` is not measured under `objectstack serve`.** The boot probe composes with `kernel.use`, as the reference test does; the `plugins` sentence follows the documented plugin convention (`plugins/development` step 6), and the "register in `init`" rule rests on reading `ObjectQLPlugin.start` (adopts `driver.*` services) and `AppPlugin.start` (connects declared datasources). 4. **Owner-column route** (the second way out of the sharing rule) is named and marked as not covered by the reference test. 5. **#27130 is not answered here**; per the card the triage seat replies after this lands. 6. **The card's #27277 item "Driver shape pin test template in the guide (P1-2)"** is carried by the template section; #27277's helper, row cap and refusal envelopes are described only as planned. ## Verification Final head `beadd67a`. `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 53 families for this diff; all 53 run, all exit 0; `--ran` reconciles `53 derived, 53 run, 0 NOT-MEASURED, 0 UNRUN`. <details><summary>Docs gates (selected verdict lines)</summary> - `check:doc-anchors`: 803 internal fragment links across 476 source files all resolve. - `check:docs-audit-scope`: green after the regeneration. - `node scripts/docs-audit/check-affected-docs.mjs`: exit 0; `affected-docs.mjs origin/main`: 0 docs (no `packages/**` change). - `check-section-landing-index`: 9 section index blocks enumerate their `meta.json` pages (red before the index card was added, green after). - `check:skill-examples`: 272 prose examples type-check (needs `@objectstack/client-react` built with declarations first, otherwise it refuses with exit 3). - `check:doc-formula-expressions`, `check:doc-security-posture`, `check:doc-authoring`, `check:doc-frontmatter`, `check:docs-single-h1`, `check:nul-bytes`: exit 0. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ihL2c5z3q4h8inrn5kwqV --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d34533c commit 01516dc

6 files changed

Lines changed: 554 additions & 1 deletion

File tree

‎content/docs/data-modeling/drivers.mdx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,11 @@ Checks that need no connection can still run in the constructor, which fails
305305
even earlier — that is where `MongoDBDriver` puts its
306306
[tenancy guard](#multi-tenancy-not-supported).
307307
308+
A driver need not front a database at all: a plugin can publish one over a REST API or
309+
any other system, and an `external` object bound to it is served from there on every
310+
read. The recipe, the rules such a driver keeps and a test template are in
311+
[Virtual objects](/docs/data-modeling/virtual-objects).
312+
308313
## PostgreSQL (via `@objectstack/driver-sql`)
309314
310315
```bash

‎content/docs/data-modeling/external-datasources.mdx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ auto-connected, validated at boot, and queryable — with no application code.**
1717
This guide is about reading/writing the *live* remote tables (federation). To
1818
*copy* rows into ObjectStack-owned tables instead, see seed data / data sync. For
1919
the plain multi-datasource routing of **managed** databases ObjectStack owns, see
20-
[Database Drivers](/docs/data-modeling/drivers).
20+
[Database Drivers](/docs/data-modeling/drivers). When the remote is not a database a
21+
built-in driver reaches — a REST API, a planning engine, a device gateway — serve the
22+
object from your own plugin driver instead: [Virtual objects](/docs/data-modeling/virtual-objects).
2123
</Callout>
2224

2325
## 1. Declare the datasource
@@ -305,6 +307,8 @@ default allows all connects (subject to the gating above).
305307
- [Database Drivers](/docs/data-modeling/drivers) — managed multi-datasource routing,
306308
[read-only via database privileges](/docs/data-modeling/drivers#read-only-grant-it-at-the-database-not-in-metadata),
307309
and [why the platform does not route read replicas](/docs/data-modeling/drivers#read-replicas-the-platform-does-not-route).
310+
- [Virtual objects](/docs/data-modeling/virtual-objects) — the same binding served by
311+
a plugin driver you write, for a remote that is not a database.
308312
- [Datasource reference](/docs/references/data/datasource) — every `defineDatasource` field.
309313
- The `examples/app-showcase` `showcase_external` datasource — a runnable end-to-end demo.
310314
- ADR-0015 (federation spec) and ADR-0062 (external-datasource runtime).

‎content/docs/data-modeling/index.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ That one definition is enough to get a persisted table, CRUD + query endpoints,
6060
<Card href="/docs/data-modeling/import-mappings" title="Import Mappings" description="Named column-to-field projections applied by the import endpoint" />
6161
<Card href="/docs/data-modeling/external-datasources" title="External Datasources" description="Federate external databases into the model" />
6262
<Card href="/docs/data-modeling/drivers" title="Database Drivers" description="Configure SQL, MongoDB, in-memory, and WASM drivers" />
63+
<Card href="/docs/data-modeling/virtual-objects" title="Virtual Objects" description="Serve an object's rows from your own server through a plugin driver" />
6364
<Card href="/docs/data-modeling/analytics" title="Analytics Datasets" description="Model datasets for dashboards and reports" />
6465
</Cards>
6566

‎content/docs/data-modeling/meta.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"import-mappings",
1818
"external-datasources",
1919
"drivers",
20+
"virtual-objects",
2021
"analytics"
2122
]
2223
}

0 commit comments

Comments
 (0)