Skip to content

fix(app-shell): a new datasource starts on External and says what each schema mode does; a driver box's default is a placeholder, not a value - #30561

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-5qt4jg
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-5qt4jg

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #30327
Fixes #30331

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:设置 → 数据源 → 新建数据源,结构模式默认改为「外部:读取现有数据库」,选择框下用业务语言说明所选模式对数据库做什么、读现有库该选哪个;托管数据源的行写明为什么没有「同步对象」;驱动表单里带默认值的文本框(SQLite 的 Filename 等)改为占位提示,不再预填成实际值。
  • 为什么改:原来默认托管,托管数据源不能同步现有表,也没有任何说明;实测托管模式下给对象绑定同名表时会给客户表加 9 列,带 _id 的表会被删表重建、数据丢失。Filename 预填 :memory:,用户输入路径会拼成 :memory:/…。
  • 风险与回滚:只改前端:数据源页面、studio 词条(新增 4 行、改 2 行,en/zh)。spec 的默认值(未声明即 managed)不变;留空的框仍发原来的默认值,请求与原来一致。回滚即还原本 PR,无数据迁移。
  • 先看:ui/packages/app-shell/src/views/metadata-admin/datasource/DatasourceResourcePage.tsx(SCHEMA_MODES、defaultConfig / withBoxDefaults、explainsNoSync)、ui/packages/i18n/src/namespaces/studio/zh.ts 的 engine.datasource.schemaMode* 与 noSyncManaged。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/datasource/,预期 9 个文件全绿(新文件 7 个用例)。
  • 需要你做的:无,席位落地。

What changes

Cards #30327 and the domain:ui half of #30331 (its server half, the test and save doors refusing a SQLite filename that is neither :memory: nor a usable path, landed in #30395). Both are the New datasource dialog in DatasourceResourcePage.tsx.

  • A new datasource starts on External (NEW_DATASOURCE_SCHEMA_MODE), sent with the external: {} block the schema requires (the existing draftBody rule). The page says it connects external databases and syncs their tables in as objects; only a federation source offers Sync. The spec's default (managed when a record declares none) is unchanged; only the dialog's starting choice moved. No reason was found that a new datasource needs managed: see What managed does to a source database below.
  • The modes in business words. The select is ordered External, Managed, Validate only; the two main options are relabelled ("External: read an existing database", "Managed: ObjectStack creates and changes the tables"), and a sentence under the select (data-testid="datasource-schema-mode-hint") says what the chosen mode does and which one reads an existing database.
  • A managed row says why it has no Sync (explainsNoSync): any managed row except the primary default carries "No Sync objects: this datasource is managed, so ObjectStack creates its tables and there are none to bring in. To read an existing database's tables, create a datasource for it with the schema mode External." It points to a new datasource rather than an edit to External, because what a switch does to objects already stored on a managed datasource was not measured.
  • A box default is a placeholder (New datasource: the file path field holds a real :memory: value, and Test connection passes on the broken path it produces #30331 ui half). defaultConfig no longer seeds a text or number box with its JSON-Schema default; the box starts empty with the default as its placeholder (:memory: for SQLite Filename). withBoxDefaults fills a still-empty box (or one typed then cleared) with its default for a new datasource's Test and Create, so the request is byte-for-byte what it was. This applies to every defaulted text or number box, not only Filename (PostgreSQL / MySQL / Mongo host and port, PostgreSQL schema): they had the same append trap, and their request is equally unchanged. An edit shows the stored values with no placeholder and sends them as they stand. Switches and selects keep their default as their value.
  • Widening: four new studio rows in @object-ui/i18n (en and zh): engine.datasource.schemaModeExternalHint, schemaModeManagedHint, schemaModeValidateOnlyHint, noSyncManaged; the en and zh text of schemaModeExternal and schemaModeManaged changes. The studio namespace ships en and zh only by its recorded ruling.
  • Changeset .changeset/30327-new-datasource-mode-and-placeholders.md, @object-ui/app-shell and @object-ui/i18n patch.

Branch: the harness-assigned claude/project-thread-5qt4jg, not claude/issue-30327-*; the cards are #30327 and #30331.

What managed does to a source database (the card's first question)

Measured on the live rig (base 8e5fabe5): a SQLite file with parts (part_no, part_name, qty), 2 rows, and legacy_items (_id, title), 2 rows. Registered once as a managed runtime datasource and once (a copy) as an external one, then an object bound to each table (PUT /api/v1/meta/object/NAME with datasource).

  • Managed: parts gained 9 columns (organization_id, created_at, created_by, updated_at, updated_by, owner_id, owning_business_unit_id, queue, __search), rows kept. legacy_items was dropped and recreated: 11 platform columns, _id gone, 0 rows (2 before). Code: driver-sql initObjects (sql-driver-rotation.ts) drops a table that has _id and no id, and ALTER TABLE ADD COLUMNs every declared field an existing table lacks. A managed datasource is a DDL target by ADR-0015 §4.1 ("ObjectStack owns this schema: DDL + migrations allowed").
  • External (control): both tables unchanged, same columns and rows; the object drafts carry external.remoteName and are bound DDL-free (registerExternalObject).

So binding an object on a managed datasource does alter a customer's tables, and can lose rows. That is why the dialog now starts on External, and why the managed sentence says to use it only for an empty database set aside for ObjectStack. The drop is a defect of its own, reported for the seat to file (see Acceptance notes).

Tests

  • DatasourceResourcePage.newDatasourceModes-30327.test.tsx (new, 7 cases; expectations read from Studio's table, never retyped):
    • (a) a new datasource starts on External with its sentence; Create sends schemaMode: 'external' with external: {} and the typed filename;
    • (b) choosing Managed / Validate only swaps the sentence; Create in Managed sends no external;
    • (c) Filename starts empty with :memory: as its placeholder; typing gives exactly the typed path; Test sends it;
    • (d) CONTROL: boxes left empty (and one typed then cleared) send their defaults (filename: ':memory:', host: 'localhost', port: 5432); a switch keeps its default (wal: true);
    • (e) an edit shows the stored value with no placeholder and PATCHes it as it stands;
    • (f) a managed row carries the note and no Sync button; an external row has Sync and no note; the primary default has no note;
    • (g) zh: the six rows are real Han rows differing from en; the note and the External sentence render in Chinese.
  • Moved pins (the default moved, deliberately): externalBlock-11368 now chooses a mode unless it is External (it used to skip choosing for Managed); locale-13460 reads the External option on the new dialog instead of Managed.

Acceptance notes

  • Driver form labels are server-side (the card's last Expected item). Filename, Auto-migrate schema, Host, Port and the field descriptions (Database file path, or ":memory:" for an ephemeral database) are the JSON-Schema title / description of the driver config Zod schemas in packages/spec/src/data/driver/*.zod.ts (.meta({ title }), .describe()), served by GET /api/v1/datasources/drivers (packages/services/service-datasource/src/driver-catalog.ts), as is the driver's description (File-backed (or in-memory) SQL database…). Measured live: the served sqlite entry carries "title": "Filename", "title": "Auto-migrate schema". Per the dispatch, packages/** is untouched; the seat files a domain:server card. The console does not translate them by key on its side, which would be a consumer-side dictionary of a producer's strings.
  • Defect, for the seat to file (class a): storing an object on a managed SQL datasource whose existing same-named table has _id and no id drops that table and recreates it empty (driver-sql initObjects, sql-driver-rotation.ts, the existingColumns.includes('_id') && !existingColumns.includes('id') branch). reach: PUT /api/v1/meta/object/legacy_items with datasource: customer_managed (runtime managed sqlite datasource on an existing file), 2 rows before, 0 after. Dedupe words: managed datasource drop table _id, initObjects legacy _id rebuild, schema sync data loss.
  • validate-only vs external, noted not filed: the spec describes validate-only as "Like external, but mismatches warn instead of fail", but no runtime code distinguishes the two (the boot validation reads external.validation.onMismatch only). The Validate only sentence therefore claims only what holds: it reads an existing database like External and never changes its tables. Not measured live.
  • The managed sentence says storing an object "adds columns to the table of the same name"; it does not describe the drop, which is a defect to fix rather than a behaviour to document.

Verification

Code head 21745ff7 (base 8e5fabe5); the PR head adds an evidence commit (83b029a7) and its removal, tree identical to 21745ff7.

Live rig and screenshots (zh-CN, 1440 wide)

examples/app-showcase objectstack dev --seed-admin on a private port with a file platform database, the console via vite in ui/apps/console with DEV_PROXY_TARGET, Chromium at browser locale zh-CN. Before shots: the page and the two studio tables restored to the base with git restore --source=8e5fabe5 on a freshly started vite; after shots: HEAD restored and vite restarted (served module confirmed by grepping NEW_DATASOURCE_SCHEMA_MODE).

Before (main): the dialog opens on 托管(表结构由 ObjectStack 管理), Filename holds :memory: as its value, typing a path gives :memory:/srv/data/parts.db, and the managed row customer_managed has no Sync and no word.

before list
before dialog
before typed path

After: the dialog opens on 外部:读取现有数据库 with its sentence; Filename is empty with :memory: as placeholder, typing gives /srv/data/parts.db; choosing 托管 shows the managed sentence; the managed row carries the note. A datasource created through the dialog with only Name and Filename filled was sent schemaMode: 'external', external: {}, config.filename exactly the typed path; its row offers 同步对象, which lists parts and legacy_items; the source file was unchanged.

after list
after dialog
after typed path
after managed chosen
after created external, sync

Tests, ablation, type-check, lint, gates
  • pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/datasource packages/app-shell/src/views/metadata-admin/ResourceHistoryPage.rollbackConfirm-13333.test.tsx packages/i18n on ce7fe5a1 (source identical to 21745ff7): Test Files 105 passed (105), Tests 1509 passed, 13 skipped; lock VERDICT command-exit 0.
  • Ablations (scripts/ablation-replace.mjs, each anchor x1 to x0, each restored with blob equal to HEAD and git diff HEAD empty), against the new test file:
    • start on managed: 2 failed / 5 passed ((a), (g));
    • seed box defaults as values again: 2 failed / 5 passed ((c), (d));
    • explainsNoSync returns false: 2 failed / 5 passed ((f), (g));
    • empty box sends nothing: 1 failed / 6 passed ((d)).
  • pnpm exec turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/i18n (tsc --noEmit and tsc -p tsconfig.test.json): 69 tasks successful, lock VERDICT command-exit 0.
  • eslint on the 6 changed source and test files: 0 errors (2 pre-existing warnings on untouched lines).
  • node scripts/pm/dispatch-gates.mjs --commands: 35 derived families, each exit 0; --ran: 35 derived, 35 run, 0 NOT-MEASURED (derived zero).
  • The Type Check UI job's 21 ui checks run locally (type-check:coverage, check:phantom-deps … check:spec-symbols … check:i18n-keys, check:i18n-drift, type-check:scripts, type-check:vitest-config, type-check:e2e, type-check:vitest-setup): exit 0 each. check:new-line-citations: 0 new.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; lint passes on the changed files.
  • 4. One issue pair, one branch off an up-to-date main, one pull request.
  • 5. The changeset carries both declaration lines, as does this body.
  • 6. packages/spec not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the fleet's required Chinese summary).

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgTJT1f9c473zqg7Zhd

…hema modes, box defaults as placeholders

Claude-Session: https://claude.ai/code/session_01L6bhgTJT1f9c473zqg7Zhd
Co-authored-by: Claude <noreply@anthropic.com>
…entences, box placeholders and managed no-sync note

Claude-Session: https://claude.ai/code/session_01L6bhgTJT1f9c473zqg7Zhd
Co-authored-by: Claude <noreply@anthropic.com>
…and box placeholders

Claude-Session: https://claude.ai/code/session_01L6bhgTJT1f9c473zqg7Zhd
Co-authored-by: Claude <noreply@anthropic.com>
…n the next commit)

Claude-Session: https://claude.ai/code/session_01L6bhgTJT1f9c473zqg7Zhd
Co-authored-by: Claude <noreply@anthropic.com>
… the previous commit)

Claude-Session: https://claude.ai/code/session_01L6bhgTJT1f9c473zqg7Zhd
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · os-sales · session_01L6bhgVwQZbHRRM9CaXeKif

Contract review

Served-tier: TIER_DEFAULT
Head-sha: fd64ec80b5d0934be306d4a3071d506c62be2b2e
Local-runs: none

Reviewed 2026-11-05T16:16Z. Face: the Clause-②: yes (widening) declaration alone (PR body and changeset). The committed tree touches no packages/spec/src/**, no governed path, and narrows nothing. The head is code 21745ff7 plus an evidence commit and its removal; the tree equals 21745ff7.

① Derived judgments

  • @object-ui/i18n gains four studio namespace keys, each in both files that namespace has (en.ts, zh.ts): engine.datasource.schemaModeExternalHint, schemaModeManagedHint, schemaModeValidateOnlyHint, noSyncManaged. None takes a placeholder. Two existing values change text (schemaModeManaged, schemaModeExternal); no key is renamed or removed, so the published key set only grows. ✔
  • The New datasource dialog starts on schemaMode: 'external' and sends external: {} with it (the existing body.external = {} branch fires because a new form has storedSchemaMode === null). The spec's own default (managed when a record declares none) is unchanged; only the dialog's starting choice moves. ✔
  • Typed boxes with a JSON-Schema default start empty, the default as placeholder. defaultConfig now skips text/number boxes (isTypedBox); withBoxDefaults fills an empty one with its default for a new datasource's Test and Create (requestConfig keys on storedSchemaMode === null, which only a new form has: an edit sets ds.schemaMode ?? 'managed', DatasourceResourcePage.tsx:578). So the request a new datasource sends is what it was; an edit sends the stored config unchanged. Switches and selects keep their defaults. ✔
  • A managed row (not the primary default) carries a note saying why it has no Sync. explainsNoSync reads an undeclared schemaMode as managed, matching the spec default. No exported signature changes. ✔

② Semver level

  • .changeset/30327-new-datasource-mode-and-placeholders.md: @object-ui/app-shell and @object-ui/i18n patch, Clause-②: yes (widening) / Metadata-protocol: none. Additive keys at patch match the lane's precedent (.changeset/30328-datasource-sync-next-steps.md). ✔

③ Boundary flags

Implemented-by: session_01L6bhgTJT1f9c473zqg7Zhd
Reviewed-by: session_01L6bhgVwQZbHRRM9CaXeKif

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#5 · os-sales · session_01L6bhgVwQZbHRRM9CaXeKif

维护者速读

席位核对 — ACCEPT · domain:ui#5 · head fd64ec80b5d0934be306d4a3071d506c62be2b2e

  • 形态: draft,作者 objectstack-fleet[bot](中继 pr_create),目标 main,Fixes #30327、Fixes #30331(服务端一半 PR fix(service-datasource)!: Test connection and Save refuse a SQLite filename that is neither :memory: nor a usable file path #30395 已合并)。共 7 个文件,+409 −17。check-governed-merges --pr 30561:NOT governed;426 行,未超 3000。mergeable_state 读为 clean。
  • 契约面: 不碰 packages/spec/**,不碰 governed 路径。Clause-②: yes (widening):只来自 4 个新 studio 键。TIER_DEFAULT 合同复核 PASS 6663047260,复核的就是这个 head。Metadata-protocol: none 与 diff 一致。
  • 前提实测: 卡上要求先查清 managed 对源库的影响。卡线程在 8e5fabe5 上实测:同名表 parts 被加 9 列、行保留;legacy_items(有 _id 无 id)被删表重建,2 行变 0 行。external 对照两张表都不变。据此把默认改为 external,理由写在 PR 和 changeset 里。
  • 抽查:
    • 只有新建表单的 storedSchemaMode 为 null(编辑时取 ds.schemaMode ?? 'managed'),所以 withBoxDefaults 只作用于新建;编辑照旧发送存储的配置。
    • 新建时 schemaMode 为 external,原有的 body.external = {} 分支会带上 spec 要求的 external 块。
    • explainsNoSync 把未声明的 schemaMode 读作 managed,和 spec 默认一致;主数据源 default 不显示这句。
  • 门禁 check-runs: fd64ec80 上 55 个 run:44 success,11 skipped,0 失败。
  • 测试: 新增 DatasourceResourcePage.newDatasourceModes-30327.test.tsx 7 个用例;两个旧测试的钉子随默认值移动。卡线程报告:受影响 105 个文件 1509 条用例通过;4 次消融各自变红;派生门禁 35/35 exit 0;dev 启动实测,新对话框建出的数据源带 external 和输入的文件名,并提供「同步对象」。证据提交及其删除后树与代码提交一致。
  • 接受的偏差: 占位提示覆盖所有带默认值的文本和数字框,不只 Filename:同一个陷阱,请求不变。
  • 范围外去向: 托管模式删表重建导致的数据丢失立了 Managed SQL datasource: storing an object over an existing table that has _id and no id drops and recreates the table, and its rows are lost #30562;驱动表单标签和描述来自 spec 驱动 schema、没有本地化,立了 Datasource driver forms: field labels and descriptions (Filename, Auto-migrate schema, Host, Port) are English in every console language #30563。validate-only 在运行时与 external 无区别,只记录在 os-dev-report,不立卡。
  • 落地条款: Fixes #30327、Fixes #30331。本席位按普通队列在这个 head 上落地:relay pr_ready + automerge_enable。MERGED 后核对两张卡已关闭、pm:dispatched 已摘。

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

Labels

documentation Improvements or additions to documentation size/m tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant