Repository navigation
feat(datasource): import objects from an OData datasource's $metadata, and import several entity sets from Studio and MCP - #27880
Conversation
EDM types map to field types through a new 'odata' arm of the spec's type matrix, PascalCase properties draft as snake_case fields bound by external.columnMap, and a single-valued navigation with a referential constraint onto an imported target becomes a lookup; every other navigation is left out of the draft and named in its review. The matrix also accepts lookup/master_detail over text, integer and bigint key columns, so Gate 2 no longer refuses a federated lookup over a foreign key that is not a UUID. Studio's datasource page imports several entity sets at once, targets first, with the write opt-in offered only where the datasource allows writes. MCP gains list_remote_tables / import_object behind metadata:write, refused by the runtime bridge on the same federation capabilities as the REST routes. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2c8AWH1pFbfa5XHrbg
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2c8AWH1pFbfa5XHrbg
📓 Docs Drift CheckThis PR changes 6 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 8 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 179 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d431e3aea47a842fcf72a6e4becb2792911d5d94 && git checkout d431e3aea47a842fcf72a6e4becb2792911d5d94
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e320e366da668b65359bd48fff1923d227513faf 14e38cf674ddbc9d9b416b34cf468687bbcad88b && git checkout -B drift-repro e320e366da668b65359bd48fff1923d227513faf && git merge --no-ff 14e38cf674ddbc9d9b416b34cf468687bbcad88b
node scripts/docs-audit/affected-docs.mjs --json e320e366da668b65359bd48fff1923d227513faf
|
…ce for the OData draft pin Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2c8AWH1pFbfa5XHrbg
The guide said nothing generates fields from $metadata. Studio's tables tab and MCP import_object now draft and save the objects, with lookups onto imported targets, so the bullet says how and which test asserts it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2c8AWH1pFbfa5XHrbg
|
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV The seat's at-tier review call was refused by the model quota, and per contract-review.md the tier is not lowered. Owed on this head: one same-shape
|
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: Generated by Claude Code |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: Inputs: card #27745 (body, claim 6512798387, amendments 6512853588 and 6512860835, scope addition 6513671180, os-dev-report 6514133439), PR #27880 body and 26-file list, the net diff against ① Derived judgments
② Semver levelChangeset Clause-②: yes (widening) is the correct declaration for this head. ③ Boundary flags
④ Design judgment
Holds:
Findings:
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV 维护者速读
席位意见:ACCEPT。契约复核 PASS 在案(分诊 6514424141, 席位核对
|
epic:#27261#1 · marchtian · session_017ihL2c8AWH1pFbfa5XHrbg
Requested by 田騏 · project thread
Fable review owed. Developed on Opus while the Fable quota is out. This PR touches
packages/spec(the type matrix) and isClause-②: yes (widening), so it lands only after a full Fable review and aCONTRACT_REVIEW_TIERcontract review record PASS on the current head.Fixes #27745
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
$metadata导入对象。EDM 类型经 spec 类型矩阵新增的odata方言映射为字段类型;PascalCase 属性生成 snake_case 字段名并用external.columnMap绑定远端名;带ReferentialConstraint且目标已导入的单值导航属性生成 lookup,其余导航属性不进草稿并在 review 里写明原因。Studio 数据源页可多选实体集一次导入(先导入被引用的目标),只有数据源允许写时才出现「允许写入」勾选,默认只读。MCP 新增list_remote_tables/import_object,仅对带metadata:write的令牌注册,运行时按 REST 同样的两个权限拒绝。lookup/master_detail加入text/integer/bigint行的精确集合,Gate 2 对 SQL 联邦也变宽松(以前通过的仍通过)。远端表名首字母大写时对象名从_products变为products。回滚即 revert 本 PR。packages/spec/src/data/type-compat.ts;external-datasource-service.ts的draftLookups;ui/.../external/api.ts的importRemoteTables。pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/external-object-draft-odata-metadata.test.ts,预期 13 个用例全绿:真实 OData 驱动读取录制的 Northwind$metadata,草稿、导入、Gate 2 校验和 lookup 读取都走通。What changes
Type matrix (
packages/spec/src/data/type-compat.ts).SqlDialectgains'odata'. Its aliases map the EDM primitives:Edm.Stringto text,Edm.Byte/SByte/Int16/Int32to integer,Edm.Int64to bigint,Edm.Decimalto decimal,Edm.Single/Doubleto float,Edm.Boolean,Edm.DateandEdm.TimeOfDayto boolean, date and time,Edm.DateTimeOffsetto datetime,Edm.Guidto uuid,Edm.Binary/Streamto binary, andCollection(...)to array (the parameter strip leavescollection).Edm.Duration, the geography and geometry families, and a service's own enum or complex types stayunknown, so a draft flags them for review.lookupandmaster_detailjoin the exact sets of thetext,integerandbigintrows, as they already sat underuuid. A reference stores the target's id, which on a federated object is the remote key a foreign-key column carries. Before this, Gate 2 refused a lookup overCustomerID intor avarcharkey as atype_mismatcherror. This loosens Gate 2 for SQL federation too. Nothing that passed before fails now.Edm.Int64andEdm.Decimalwould be marked lossy. The matrix decides lossiness per canonical row, and SQL'sbigintandnumericrows marknumberexact. Making the EDM pair lossy would need new canonical rows that only OData uses, so they take the SQL verdict. That was my misstatement in the option text.SqlDialect: only theDIALECTSlist intype-compat.test.ts(updated).ui/holds no copy (grep for the union's members finds none).Draft (
packages/services/service-datasource/src/external-datasource-service.ts).typeVocabulary(dialect)reads the driver'sodata-4.0as theodatavocabulary. Drafting,refreshCatalogandvalidateObjectall go through it.CategoryIDtocategory_id), andexternal.columnMapcarries the remote name, which the driver binds through and Gate 2 inverts.renameoverrides now land incolumnMaptoo, so a renamed field stays bound to its column (before, it lost its column and Gate 2 reported it missing). When two columns spell one field name, the later one is numbered and named in review.Productsdrafted as_productsbefore, because the leading capital was prefixed before lower-casing.draftLookups: a single-valued navigation (navigationPropertieson the OData driver's introspected table, read structurally) becomes{ type: 'lookup', reference }on itsReferentialConstraint's dependent column when all of these hold:importObject).review(remoteTypenavigation → ENTITY_SET) with the reason. A target imported later needs this table imported after it, because re-saving a scalar as a lookup is a retype the save door's destructive-change gate refuses.columnMap, the lookup'sreference, review comments keyed by field, and left-out navigations as comments.Driver (
packages/drivers/driver-odata).introspectSchema()lists each navigation'sreferentialConstraints.Docs.
content/docs/data-modeling/odata-datasources.mdx§2 said nothing generates fields from$metadata(it landed in #27871 while this PR was open). The bullet now names the import (Studio's Tables tab, Import / Import selected, MCPlist_remote_tables/import_object), what the draft does, the lookup rule, and the test that asserts it.Studio (
ui/packages/app-shell/.../metadata-admin/external).importRemoteTables. Each round drafts the pending tables and saves the ones whose draft waits on no other pending table. A cycle is saved as drafted.external.allowWrites. It is off by default, and the single-table dialog has it too, so an imported object is read-only unless the author ticks it.studiorows inenandzh.MCP.
@objectstack/mcpaddsMcpFederationBridgeandregisterFederationTools:list_remote_tables(read-only) andimport_object(a non-destructive write that saves a live object, not a draft, and says so).grantsAuthoringTools(scopes)holds (the token namesmetadata:write) and the bridge carries both members.federationTools).@objectstack/runtime's bridge reaches theexternal-datasourceservice. It refuses403 PERMISSION_DENIEDwithoutFEDERATION_READ_CAPABILITY/FEDERATION_WRITE_CAPABILITY, asked first as the REST routes ask it. It answers503with no service.@objectstack/restexports the two constants.Changeset:
patchon spec, driver-odata, service-datasource, rest, runtime, mcp,@object-ui/app-shell,@object-ui/i18n.Why
Epic #27261 P2, 田騏 at 2026-10-29T21:58Z 「现在启动全部 P2」. Tier B is declarative OData (ruling 6486145005). Without an import, an administrator still writes every object by hand. Spec scope per the epic PM's amendments 6512853588 and 6512860835 (option A, both parts).
Verification
Head
14e38cf6: the feature commit, a test-typing follow-up, a vitest alias that resolves@objectstack/driver-odatato its source (the test-source alias gate), a merge oforigin/mainat e320e36, and the guide edit. Afterpnpm installand builds of the touched packages:@objectstack/service-datasource: 64 files, 1041 tests passed, including the newexternal-object-draft-odata-metadata.test.ts(13). It drives the realODataDriver.introspectSchema()(resolved to its source by the vitest alias) over a recorded Northwind$metadata(__fixtures__/northwind-metadata.xml): EDM type mapping, snake_case fields pluscolumnMap, lookups only onto imported targets (per datasource, self-reference under an override name, composite-key source), left-out navigations named, Gate 2validateObjectclean on four imported objects with a negative control, and the driver readingcategory_idand finding the category by it (Categories?$filter=CategoryID eq 1).@objectstack/driver-odata: 6 files, 439 passed.@objectstack/spectype-compat.test.ts: 27 passed.pnpm --filter @objectstack/spec check:generated: all 14 artifacts current.@objectstack/mcp: 46 files, 595 passed, includingmcp-federation-tools.test.ts(registration by grant and bridge, argument forwarding, unknown-key refusal, coded refusal, SKILL.md parity).@objectstack/runtimemcp-federation-bridge.test.ts(4: the capability asked before the service lookup, 503 without a service) plus the two existing MCP domain files (156).ui/:metadata-admin/externalandmetadata-admin/previews, 154 files, 1722 passed, includingSchemaBrowser.batchImport-27745.test.tsx(7) against a mocked federation service through the real REST client.@object-ui/app-shelltype-checkexit 0.tsc --noEmitexit 0; its test-layer step reports one error inorganization-ownership-boot-guard.turso-remote.kernel.test.ts(@objectstack/driver-tursonot built in this container), a file this PR does not touch.eslinton every changed file: 0 errors. The warnings are the two pre-existingset-state-in-effectlines.scripts/pm/dispatch-gates.mjs --ranreports 122 derived, 122 run, all exit 0, 0 NOT-MEASURED. The 7 that first exited 3 (prerequisites: the teardown fixture commit, a builtspec/dist) were rerun after their prerequisites and pass.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_017ihL2c8AWH1pFbfa5XHrbg
Generated by Claude Code