Repository navigation
Commit b373263
feat(spec): an API-backed datasource names a declared connector instance in external.connector and borrows its credential (ADR-0015 D7) (#27783)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSi3f3f6RH93TSgeMT1ET96P6)_
**Fable review owed.** Developed on Opus while the Fable quota is out.
This PR touches the published spec (`packages/spec/src/**`) and an ADR,
so it lands only after a full Fable review, a `CONTRACT_REVIEW_TIER`
contract review record PASS on the current head, and 田騏's approval (Tier
H).
Fixes #27742
Clause-②: yes
Metadata-protocol: added
Skills-docs: follow-up owed —
skills/objectstack-data/rules/datasources.md,
content/docs/data-modeling/virtual-objects.mdx,
content/docs/data-modeling/external-datasources.mdx
## 维护者速读
- **改了什么**:数据源的 `external` 块新增一个可选键
`connector`,写一个已声明的连接器实例名,数据源借用该实例的授权(静态、oauth2、app-credential、signed
皆可)。写错时在构建时响亮拒收:实例不存在、该连接器没有 `provider`(只是目录描述)、实例没有 `auth`;与
`credentialsRef` 同写、用在内置数据库驱动(postgres/mysql/sqlite/mongodb 等)上、或用在
`managed` 数据源上,解析时即拒收并给出改法。ADR-0015 加 D7 修订注。
- **为什么改**:D7 裁决(6486136301,字母 A):SAP、Dynamics 365 这类 OAuth2 供应商,一次
Connect 让流程动作和虚拟对象读数共用,重连一次两边都恢复。
- **风险与回滚**:只加键、不改已有键含义,`credentialsRef` 照旧;运行时尚未读取该键(运行时半边是
#27746,被本卡阻塞),在它落地前该键只校验不生效,两者应同版发布。回滚即 revert 本 PR。
-
**先看**:`packages/spec/src/data/datasource.zod.ts`(新键与三条拒收);`packages/spec/src/stack.zod.ts`
的
`collectDatasourceConnectorErrors`(构建时实例核对);`docs/adr/0015-external-datasource-federation.md`
顶部 D7 注。
- **两分钟验证**:`pnpm --filter @objectstack/spec exec vitest run
src/stack-datasource-connector.test.ts
src/data/datasource.test.ts`,预期两个文件全绿;用例里能看到四种构建时拒收与三种解析时拒收的原文。
- **需要你做的**:Fable 复审与契约复审通过后,批准本 PR(Tier H)。
## What changes
**The key.** `ExternalDatasourceSettingsSchema` gains `connector`
(`packages/spec/src/data/datasource.zod.ts`): an optional snake_case
name of a declared connector instance whose credential an API-backed
datasource borrows (ADR-0015 amendment D7, ruling 6486136301 on #27298).
Near-miss spellings (`connectorName`, `connectorId`, `connectorRef`,
`connectorInstance`) are refused with the "did you mean `connector`"
prescription, and a `connector` written one level too high, on the
datasource itself, is pointed into the `external` block.
**Refused at parse** (`DatasourceSchema`'s refinement, issue path
`external.connector`, each message naming the fix):
- beside a bound `credentialsRef`: the two channels are exclusive. The
card allowed "mutually exclusive, or the precedence named and pinned";
exclusivity was chosen because a precedence rule leaves the loser
written and ignored, the silent no-op the closed shapes in this module
exist to stop. An empty-string `credentialsRef` is not a binding (the
connect path's test is truthy), so it is not the second channel.
- on a built-in database driver (every id in `DRIVER_VOCABULARY`, via
`resolveDriverId`): a connector lends an HTTP credential; the message
prescribes `credentialsRef`.
- on a managed datasource with a plugin driver: the message prescribes
`schemaMode: 'external'` (or `'validate-only'`), not the generic "remove
these keys" of the managed-block refusal, which no longer lists
`connector`.
**Refused at build** (`collectDatasourceConnectorErrors` in
`packages/spec/src/stack.zod.ts`, run by `validateCrossReferences`, so
`defineStack` and `os validate` both refuse with
`STACK_CROSS_REFERENCE_INVALID` / 422), judged against the stack's own
`connectors`, like a job's mapping reference:
- no `connectors` entry of that name;
- an entry with no `provider` (an ADR-0097 catalog descriptor, never
materialized);
- an instance with no `auth`, or `auth: { type: 'none' }` (nothing to
lend).
A disabled instance (`enabled: false`) is accepted: the switch turning
the vendor off for its actions and its records alike is the one-grant
behaviour D7 asks for.
**ADR-0015.** An amendment note D7 at the top and a pointer in §10,
recording the second credential channel, the three exclusions, that
execution identity is not reopened (ADR-0143 Q1 = A; object permissions
and RLS stay the only gate), and the answer to the ruling's open reading
below.
**Ledgers and generated files.**
`packages/spec/liveness/datasource.json` gets a `planned` row for
`external.connector` (evidence: the two refusal sites; flips to `live`
with the runtime card), and the credential-name ratchet in
`metadata-credential-fields.test.ts` allow-lists
`datasource:external.connector` with its reason: its describe says
"credential", but the field holds a connector name and the credential
stays on the instance's own `auth.credentialRef`.
`content/docs/references/data/datasource.mdx`,
`packages/spec/authorable-surface/data.json` and
`packages/spec/liveness/state-counts/datasource.md` are regenerated.
Changeset: `minor` on `@objectstack/spec`.
## The ruling's open reading: does the `(organization, connector
instance)` binding fit an environment-level datasource?
Yes, under the posture #27230 S1 ruled. A datasource stays
environment-scoped (ADR-0015 §10). The connection it borrows is selected
per dispatch by the request's organization (ADR-0143 Q1 = A, D4), so a
virtual-object read made in an organization uses that organization's
grant, exactly as a flow dispatch does. A read that carries no
organization (the boot-time drift check, a schedule) falls under D4's
rule: it uses the instance's connection only when exactly one exists,
and is refused otherwise, never answered from an arbitrary
organization's grant. S1 ruled one organization per environment for
connector credentials ("暂时不考虑 一个环境里住多个组织的情况"), so there is always
exactly one connection, and the environment-level datasource and the
organization-bound connection record meet with no new rule. A shared
environment hosting several organizations is outside S1; there the
no-organization read is refused by D4, which is fail-closed. Recorded in
the ADR-0015 D7 note.
The ruling's other open reading (whether the `objectql`-side driver
reaches the automation service's resolver without a new dependency
direction) is the runtime half's question, carried by #27746; nothing in
this diff decides it.
## Tests
On `eda26e3c`, each under `scripts/pm/os-verify-lock.sh`:
- `pnpm --filter @objectstack/spec test`: Test Files 756 passed (756),
Tests 22468 passed | 1 todo (22469); command-exit 0 (4m15s).
- `pnpm --filter @objectstack/spec typecheck`: command-exit 0 (1m22s).
- New: `packages/spec/src/stack-datasource-connector.test.ts` (8 cases:
the four build-time refusals with the `STACK_CROSS_REFERENCE_INVALID` /
422 envelope, one finding per borrowing datasource in a stack with
objects, and three controls) and the `external.connector` block in
`packages/spec/src/data/datasource.test.ts` (11 cases: acceptance in
`external` and `validate-only`, the declared key, the four alias
prescriptions, the name pattern, the three parse refusals with their
paths and fixes, the empty-`credentialsRef` control, the top-level
guidance, and a `credentialsRef`-alone control).
<details><summary>Ablations (both refusal halves can go red)</summary>
Mutations through `scripts/ablation-replace.mjs` (anchor hit 1, blob
moved, restored by the tool, `git diff HEAD` empty afterwards):
- `stack.zod.ts`:
`errors.push(...collectDatasourceConnectorErrors(config));` replaced by
`void collectDatasourceConnectorErrors;` (blob `c8cc7c644281` to
`e0bfdb0f2204`): `stack-datasource-connector.test.ts` 5 failed, 3 passed
(the five refusal cases red, the three controls green).
- `datasource.zod.ts`: the `if (ds.external?.connector !== undefined) {`
guard made false (blob `e0f177402a59` to `1090eb4caaf4`): the
`external.connector` block 4 failed, 7 passed (the four parse-refusal
cases red).
</details>
## Acceptance notes
- **Declared ahead of its reader.** No runtime reads
`external.connector` yet: the datasource connection service handing the
borrowed credential to the driver is #27746, blocked by this card. Until
it lands, the only shape the spec accepts is a plugin-contributed
driver's datasource naming a valid instance, and no such driver ships in
this repository today (the OData driver is #27741). The liveness row
says `planned`, and the changeset says the key is validated and not yet
read. #27746 should land in the same release.
- **A runtime-created datasource** (Studio, `origin: 'runtime'`) is
parsed by `DatasourceSchema` but never passes through `defineStack`, so
the "instance is declared" half does not judge it here. That check
belongs at the runtime half's boot and save seams (#27746).
- **Cross-package borrowing** (a datasource in one package naming a
connector another package declares) is refused per stack, the same scope
a job's mapping reference uses. No current producer does this; widening
to the composed artifact is a separate change if one appears.
- **No ADR anchor file** was added (`scripts/adr-anchors/`): the claim's
file surface did not list it, and both refusal sites carry the ADR-0015
D7 reference in their own comments.
- **Epic body.** The ruling's amendment of #27261 §4 item 8 ("through
`credentialsRef`, or through a declared connector instance for an
API-backed datasource") is the epic PM's edit, not this diff's.
- **Skills and guide.** `Skills-docs:` above names the follow-up: the
data skill and the virtual-objects guide still teach `credentialsRef` as
the only channel. Teaching the new key before its runtime lands would
advertise a capability the runtime does not deliver, so it rides with or
after #27746.
## Verification
Local gates on `eda26e3c`, exit codes captured to a file before any
pipe. `node scripts/pm/dispatch-gates.mjs --commands` derived 111
families from the 11 changed files; every one was run.
- 105 exit 0 on the first pass, among them spec `check:generated`,
`check:docs`, `check:api-surface`, `check:authorable-surface`,
`check:liveness`, `check:strictness-ledger`, `check:export-origins`,
`check:llms-txt`, `check:spec-parsed-alias`,
`check:spec-docblock-symbol-anchors`, `check:stack-collection-maps`,
`check:doc-authoring`, `check:adr-links`, `check:adr-anchors`,
`check:adr-index`, `check:issue-citations`, `check:nul-bytes`,
`check:merge-driver`, `check:pm-prior-rulings`, `check:type-check-debt`,
`check-adr-0087-registration --base origin/main`, `check-empty-changeset
--base origin/main`, `check-changeset-no-major --base origin/main` and
the `--self-test` batteries.
- 6 exit 3 (prerequisite not met) on the first pass, each re-run green
after its prerequisite: `check-plugin-teardown-shape --self-test`
(fixture commit fetched), lint `check:doc-formula-expressions` and
`check:doc-security-posture` (`@objectstack/lint` built), spec
`check:skill-examples`, `check:dual-build-cjs-loads` and
`check:lean-entry-closure` (the published packages' `dist/` built by
`check:type-check-debt`).
- `dispatch-gates --ran`: "111 derived, 111 run, 0 NOT-MEASURED, 0
UNRUN" (a derived zero).
- `pnpm --filter @objectstack/spec check:generated` on the merged head
after a build: 14 of 14 up to date (exit 0).
- `origin/main` merged once before opening (`eda26e3c`); `packages/spec`
had moved on main (analytics), so spec was rebuilt, `check:generated`
re-run, and the full spec suite above was taken on the merged head.
- The full battery (`pnpm lint`, Test Core shards, UI jobs) is CI's and
was not re-run locally. No dogfood run: no `!` in the title and no
`(narrowing)` arm.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` is CI's.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
repo's own Chinese reader section).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ihL2eRfGhMmx9N4rdwNtG)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0a4b0fb commit b373263
13 files changed
Lines changed: 495 additions & 7 deletions
File tree
- .changeset
- content/docs/references/data
- docs/adr
- packages/spec
- authorable-surface
- liveness
- state-counts
- src
- data
- kernel
- scripts/adr-anchors
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| |||
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
| 78 | + | |
78 | 79 | | |
79 | 80 | | |
80 | 81 | | |
| |||
106 | 107 | | |
107 | 108 | | |
108 | 109 | | |
109 | | - | |
| 110 | + | |
110 | 111 | | |
111 | 112 | | |
112 | 113 | | |
| |||
116 | 117 | | |
117 | 118 | | |
118 | 119 | | |
| 120 | + | |
119 | 121 | | |
120 | 122 | | |
121 | 123 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
8 | 40 | | |
9 | 41 | | |
10 | 42 | | |
| |||
706 | 738 | | |
707 | 739 | | |
708 | 740 | | |
709 | | - | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
710 | 745 | | |
711 | 746 | | |
712 | 747 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
271 | 271 | | |
272 | 272 | | |
273 | 273 | | |
| 274 | + | |
274 | 275 | | |
275 | 276 | | |
276 | 277 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
144 | 144 | | |
145 | 145 | | |
146 | 146 | | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
147 | 153 | | |
148 | 154 | | |
149 | 155 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
848 | 848 | | |
849 | 849 | | |
850 | 850 | | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
| 872 | + | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
| 884 | + | |
| 885 | + | |
| 886 | + | |
| 887 | + | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
| 891 | + | |
| 892 | + | |
| 893 | + | |
| 894 | + | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
| 914 | + | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
0 commit comments