Skip to content

Commit e9caebe

Browse files
docs(skills): objectstack-data and the data-modeling guides teach external.connector, the credential a datasource borrows from a connector instance, and point plugin drivers at the conformance kit (#27954)
epic:#27261#1 · marchtian · session_017ihL2eXpMhQkzjoYjCTKR6 <!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSi7kfzPizGsPKjJnaEN8RzYK)_ **Fable review owed.** Written while the Fable quota is out. This PR touches `skills/**` (Tier H): it lands only after an independent full review at `CONTRACT_REVIEW_TIER`, a contract review record PASS on its head, and an approver's APPROVE. Kept in draft until then. Fixes #27786 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 技能 `objectstack-data` 的数据源规则和三篇数据建模指南开始教 `external.connector`:API 型数据源(插件驱动,如 OData)可以借用一个已声明连接器实例的凭据,一次授权同时服务连接器的流程动作和数据源的记录;与 `credentialsRef` 二选一,内置数据库驱动和 managed 数据源上被拒。指南给出一个完整示例(OData 数据源点名连接器实例)、拒绝规则、连接时 fail-closed、凭据未设置时每次请求 503、401 刷新一次重试。`virtual-objects.mdx` 另说明自写插件驱动如何自己借用并出示凭据,并新增「Run the conformance kit」一节,指向 `@objectstack/objectql` 的 `definePluginDriverConformanceSuite`。 - **为什么改:** D7 裁决(6486136301)的技能与文档一半;spec(#27783)和运行时(#27819)都已在 `main`,但技能和指南只教 `credentialsRef`,AI 作者不知道这个键。 - **风险与回滚:** 只改技能规则、文档和一个 `@objectstack/skills` patch changeset;不动代码与 schema。技能文件在 token 上限内(909/911,上限不动)。回滚即 revert。 - **先看:** `skills/objectstack-data/rules/datasources.md` 的 Credentials 一节;`external-datasources.mdx` 的「Borrowing a connector instance's credential」。 - **需要您做的:** Fable 复审与契约复审通过后批准(Tier H)。 ## What changes - **`skills/objectstack-data/rules/datasources.md`** — "Credentials — one channel, fail-closed": `external` names exactly one credential channel, `credentialsRef` (a secret-store handle resolved at connect) or `connector` (an API-backed datasource on a plugin driver such as `@objectstack/driver-odata` borrows the credential of a declared `connectors` instance with a `provider` and an `auth`). Where `connector` is refused (beside `credentialsRef`, on a built-in driver, on a `managed` datasource), and that an instance that cannot lend, or a stack with no automation service, leaves the datasource unconnected with the reason. The form is shown as `schemaMode: 'external', external: { connector: 'erp_api' }`, the instance's `auth` must be other than `none`, an undeclared instance is refused at `defineStack` (else at connect), and a connector credential not set yet answers `503` per request. Paid for inside the file (the `field.columnName` history clause, the auto-connect lead-in, the escape-hatch note and phrases that restated their neighbours shortened or dropped): 903 to 909 tokens against a ceiling of 911, which does not move. - **`content/docs/data-modeling/external-datasources.mdx`** — §4 Credentials gains "Borrowing a connector instance's credential": the channel, an `os:check` example (a `rest` connector instance `erp_api` with a bearer `credentialRef`, and an OData datasource naming it), how the instance's `auth` is presented, and the rules: one channel, API-backed datasources only, an instance that can lend (judged when the stack is validated and when a datasource is saved under Setup), fail-closed at connect, a credential not set yet answered `503` per request until it is set, rotation (a store write reaches the next request; a `401` is retried once with a fresh credential). - **`content/docs/data-modeling/odata-datasources.mdx`** (scope addition 6513665711) — §3 Credentials names the two channels; the existing text becomes "A secret of its own: `credentialsRef`", unchanged, the `os serve` caveat sentence included (#27875's); a new "Borrowing a connector instance's credential" subsection shows the stack fragment with the plugin's `auth` left at `none`, how each `auth` type is sent, the borrow at the first request and the refresh/401 rule, and the fail-closed list. The "Check the service first" bullet and the `auth` option row mention the connector path. - **`content/docs/data-modeling/virtual-objects.mdx`** — the Credentials section says what `external.connector` does for a driver of your own: the connect step judges the instance but hands an already-built driver nothing, so the driver borrows from the `connector-credential-source` service and presents through `callWithBorrowedCredential`, scrubbing upstream text with `withoutBorrowedCredential` (a sketch, with the OData driver as the reference). New "Run the conformance kit" subsection (scope addition 6513665711): `definePluginDriverConformanceSuite` / `runPluginDriverConformance` from `@objectstack/objectql`, a minimal call, what the floor tier asserts, `setupUnavailable`, `writeProbes`, the opt-in `direct` tier, and the OData driver's own run. Floor step 3 now says the kit ships and the trust built on it does not exist yet. - **`.changeset/27786-skills-external-connector.md`** — `@objectstack/skills` patch (route 1), both declaration lines. ## Why ADR-0015 amendment D7 (ruling 6486136301 on #27298): one Connect per vendor serves the connector's flow actions and the datasource's virtual objects. The spec half (#27742, PR #27783) and the runtime half (#27746, PR #27819, `ed8b2375`) are on `main`; the datasources skill rule and the data-modeling guides still taught only `credentialsRef`, and no guide pointed an out-of-repo driver at the conformance kit (#27744, PR #27798). ## Verification - **The examples parse against `main`'s schema.** With `@objectstack/spec` built from this tree, `defineStack` accepts the `os:check` example in `external-datasources.mdx` and the OData fragment wrapped into a stack. Each refusal the pages name was run too and refused with the message described: both channels, `connector` on `postgres`, `connector` on a `managed` datasource, an undeclared instance, a catalog descriptor (no `provider`), and `auth: { type: 'none' }`. A disabled instance is accepted at build, as the pages say (it is refused when the datasource connects). - **The two `virtual-objects.mdx` sketches type-check.** The credential sketch against `@objectstack/types` and `@objectstack/core`, the conformance-kit call against `@objectstack/objectql`, under `strict` (a planted type error in the same program was reported, so the run checks). - **Every runtime claim was read off the code at `ed8b2375`:** `packages/services/service-automation/src/connector-credential-source.ts` (the lend refusals, disabled included; the handle's 503s; the generation move on a credential write), `packages/services/service-datasource/src/datasource-connection-service.ts` (the already-registered guard's borrow verdict; the factory path) and `datasource-connector-credential.ts`, `packages/types/src/connector-credential-source.ts` (`callWithBorrowedCredential`'s expiry, 401 and 503 rules), `packages/drivers/driver-odata/src/plugin.ts` and `transport.ts` (first-request borrow, presentation by `auth` type, the fail-closed refusals), `packages/objectql/src/external-driver-floor.ts` (a 503 answered `ERR_DATASOURCE_UNAVAILABLE`), and `packages/objectql/src/plugin-driver-conformance.ts` (options, tiers, `not-offered`). - **Local gates:** `node scripts/pm/dispatch-gates.mjs --commands` derived 67 families from the 5 changed files; after `pnpm build` (exit 0) every one was run with its exit code recorded before any pipe, all 67 exit 0, and `dispatch-gates --ran` reads "67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED (a DERIVED zero)". Among them: `check:skill-examples` (276 prose examples type-check, the new `os:check` block included), `check:doc-anchors` (the new `#borrowing-a-connector-instances-credential` and `#run-the-conformance-kit` links resolve), `check-skills-token-ratchet` and `check:skill-identifier-liveness`. Re-run in full on `de6aaf6b` (the should-fix commit): again 67 derived, 67 run, all exit 0, a DERIVED zero. Head `de6aaf6b`, `origin/main` merge base `ed0e71db`. The full battery is CI's. ## Contract review 6515906243 (PASS on `ecb4cd3a`), should-fixes applied in `de6aaf6b` No must-fix. The four should-fixes on `rules/datasources.md` are applied, inside the same token ceiling (909/911): - **The one-line form.** It now reads `schemaMode: 'external', external: { connector: 'erp_api' }`, so copying it alone no longer lands on the managed refusal. - **`auth` other than `none`.** Said in the rule, as `collectDatasourceConnectorErrors` and `lendRefusal` judge it. - **Where an undeclared instance is refused.** "naming an undeclared instance (at `defineStack`, else at connect)". - **A credential not set yet.** "A connector credential not set yet answers `503` per request", separate from the connect failures. - **PR body.** The model name is removed from the opening paragraph. - Not changed: the pre-existing "`credentialsRef` … resolved at connect, before the driver is built" sentence, which the review notes for the next edit of the file. The commit only applies findings, so it owes a delta review by the same reviewer. ## Acceptance notes - **The skill rule carries no runnable example.** The file had 8 tokens of headroom; the example lives in the guides, and the rule shows the one-line `external: { connector: 'erp_api' }` form. - **"Setup → Connector credentials"** appears in the runtime's own refusal text, but no Console page by that name was found under `ui/`; the guides point at the connectors page's "What `credentialRef` resolves through" instead of naming a Setup page. - **Worktree name:** the Claim names `objectstack-issue-27786`; the worktree is `objectstack-27786` on the claimed branch. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (Not touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; no source file changed (`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: not touched. - [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 with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ihL2eXpMhQkzjoYjCTKR6 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent e8cdf4e commit e9caebe

5 files changed

Lines changed: 265 additions & 28 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-data teaches `external.connector`, the datasource credential channel that borrows a declared connector instance's credential, beside `credentialsRef`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** A datasource can name a declared connector instance in `external.connector` and borrow that instance's credential (ADR-0015 amendment D7), so one credential serves the connector's flow actions and an API-backed datasource's records. `objectstack-data`'s `rules/datasources.md` taught only `credentialsRef`, so an agent following it bound a second copy of a vendor credential the stack already held, and had no way to know the key exists or where it is refused.
11+
- **Now.** The rule's credentials section names the two channels and that exactly one is written: `credentialsRef`, a secret-store handle resolved at connect; or `connector`, for a datasource on a plugin driver such as `@objectstack/driver-odata`, naming a `connectors` instance with a `provider` and an `auth` other than `none`, shown as `schemaMode: 'external', external: { connector: 'erp_api' }`. It says where `connector` is refused (beside `credentialsRef`, on a built-in driver, on a `managed` datasource, and naming an undeclared instance, at `defineStack` or else at connect), that a disabled instance or a stack with no automation service leaves the datasource unconnected with the reason, and that a connector credential not set yet answers `503` per request instead of failing the connect.
12+
- The new text is paid for inside the same file: the `field.columnName` history clause, the auto-connect lead-in and the escape-hatch note are shortened, and phrases that restated their neighbours (the `columnMap` direction the example already shows, the mapping-routing tail, the write-gate tail) are dropped. The file stays under its token ceiling, which does not move. No schema, key or runtime behaviour changes.

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,76 @@ for the datasources listed under [When auto-connect fails](#when-auto-connect-fa
263263
and degrades with a warning otherwise. (Credential-less drivers such as SQLite
264264
simply have no `credentialsRef`.)
265265

266+
### Borrowing a connector instance's credential
267+
268+
An **API-backed** datasource, one served by a plugin driver such as the
269+
[OData driver](/docs/data-modeling/odata-datasources), has a second channel. When a
270+
[connector instance](/docs/automation/connectors#declaring-a-provider-bound-instance)
271+
already authorizes the vendor, name it in `external.connector` and the datasource
272+
borrows that instance's credential (ADR-0015 amendment D7). The credential is set once,
273+
where the connector's own is set
274+
([What `credentialRef` resolves through](/docs/automation/connectors#what-credentialref-resolves-through)),
275+
and it serves both the connector's flow actions and the datasource's records.
276+
277+
{/* os:check */}
278+
```typescript
279+
import { defineStack } from '@objectstack/spec';
280+
281+
export default defineStack({
282+
manifest: { id: 'com.example.sales', name: 'Sales', version: '1.0.0', type: 'app' },
283+
connectors: [
284+
{
285+
name: 'erp_api',
286+
label: 'ERP API',
287+
type: 'api',
288+
provider: 'rest',
289+
providerConfig: { baseUrl: 'https://erp.example.com' },
290+
auth: { type: 'bearer', credentialRef: 'ERP_API_TOKEN' },
291+
},
292+
],
293+
datasources: [
294+
{
295+
name: 'erp',
296+
driver: 'erp', // the OData plugin's name, i.e. the `driver.erp` service
297+
schemaMode: 'external',
298+
config: {},
299+
external: {
300+
connector: 'erp_api', // no credentialsRef: exactly one channel
301+
validation: { onMismatch: 'warn', checkOnBoot: false },
302+
},
303+
},
304+
],
305+
});
306+
```
307+
308+
The instance's `auth` decides what is sent: a bearer token, basic auth, or an API key in
309+
its header or query parameter. The rules:
310+
311+
- **One channel.** `connector` beside a `credentialsRef` is refused. Keep the one that
312+
should supply the credential.
313+
- **API-backed datasources only.** `connector` is refused on a built-in driver
314+
(`postgres`, `mysql`, `mongodb` and the rest), which takes a database credential: bind
315+
that with `credentialsRef`. It is refused on a `managed` datasource too, since an API's
316+
schema is never ObjectStack's to manage: set `schemaMode: 'external'` (or
317+
`'validate-only'`).
318+
- **An instance that can lend.** The name must match a `connectors` entry of the stack
319+
that has a `provider` and an `auth` other than `none`. A catalog descriptor (no
320+
`provider`) or an instance with no `auth` is refused when the stack is validated, and a
321+
datasource created or edited under Setup → Integrations → Datasources is judged the
322+
same way when it is saved.
323+
- **Fail-closed at connect.** An instance that cannot lend when the datasource connects
324+
(not declared, disabled with `enabled: false`, no `auth`), or a stack with no
325+
automation service (`@objectstack/service-automation` is what lends the credential),
326+
leaves the datasource unconnected with the reason, the same connect failure as an
327+
unresolvable `credentialsRef`.
328+
- **A credential not set yet does not fail the connect.** Each request is answered
329+
`503 ERR_DATASOURCE_UNAVAILABLE`, with the reference named in the server log, until
330+
an administrator sets it; the next request after that reads with it, with no restart.
331+
- **Rotation.** A credential set again in the connector credential store reaches the
332+
next request. One the upstream refuses with a `401` is obtained afresh once and the
333+
request retried, which also picks up a rotated environment variable; a second `401`
334+
answers `503`.
335+
266336
## 5. Writes (double opt-in)
267337

268338
Federation is read-only by default. To allow writes, **both** the datasource and the

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

Lines changed: 64 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ disagree.
3333
- **Each entity set you bind has a key.** The key becomes the record's `id`. An entity
3434
type with no usable key is refused at the first request.
3535
- **You know how it authenticates.** The driver sends no credential, a bearer token, or
36-
HTTP basic. It does not run an OAuth2 flow ([Credentials](#3-credentials)).
36+
HTTP basic, or, borrowed from a connector instance, an API key too. It does not run an
37+
OAuth2 flow ([Credentials](#3-credentials)).
3738

3839
## 1. Mount the plugin
3940

@@ -61,7 +62,7 @@ export default defineStack({
6162
| `name` | `odata` | The datasource it serves. It is also the driver name and the `driver.<name>` service suffix. |
6263
| `serviceRoot` | (required) | The service root URL, http or https, with no credentials and no query string. |
6364
| `version` | `4.0` | The protocol version. `4.0` is the only value. |
64-
| `auth` | `{ scheme: 'none' }` | `none`, `bearer`, or `basic` with a `username`. The secret is the datasource's `external.credentialsRef`. |
65+
| `auth` | `{ scheme: 'none' }` | `none`, `bearer`, or `basic` with a `username`. The secret is the datasource's `external.credentialsRef`. Leave it `none` when the datasource borrows a connector instance's credential ([Credentials](#3-credentials)). |
6566
| `secrets` | (none) | Where `credentialsRef` is resolved: an object with `resolve(credentialsRef)`. When absent, the host's `datasource-secret-resolver` service is used; `os serve` registers one. |
6667
| `pageSize` | `500` | Rows asked for per server page (`Prefer: odata.maxpagesize`). |
6768
| `requestTimeoutMs` | `30000` | The timeout of each request. |
@@ -164,8 +165,17 @@ What each piece does on this driver:
164165
## 3. Credentials
165166

166167
⛔ Never put a credential in the stack, in the plugin options, or in the `serviceRoot`
167-
URL. The secret is the datasource's `external.credentialsRef`, and the plugin resolves it
168-
through the host's secret store:
168+
URL. The datasource names exactly one of two channels:
169+
170+
- **A secret of its own**, `external.credentialsRef`, resolved through the host's secret
171+
store. This is the example in step 2.
172+
- **A connector instance's credential**, `external.connector`, when a connector already
173+
authorizes the same service
174+
([Borrowing a connector instance's credential](#borrowing-a-connector-instances-credential)).
175+
176+
### A secret of its own: `credentialsRef`
177+
178+
The plugin resolves `external.credentialsRef` through the host's secret store:
169179

170180
- **`bearer`** sends `Authorization: Bearer <secret>`.
171181
- **`basic`** takes `username` from the options and the password from the secret.
@@ -197,6 +207,56 @@ the credential: an auth scheme with no `credentialsRef`, a `credentialsRef` with
197207
`secrets` nor the service, and a handle that does not resolve. The caller is answered
198208
`503 ERR_DATASOURCE_UNAVAILABLE`, and the cause goes to the server log.
199209

210+
### Borrowing a connector instance's credential
211+
212+
When a [connector instance](/docs/automation/connectors#declaring-a-provider-bound-instance)
213+
already authorizes the ERP for your flows, the datasource can borrow its credential instead
214+
of holding a secret of its own (ADR-0015 amendment D7). One credential, set where the
215+
connector's is set, then serves the connector's actions and the datasource's records.
216+
Name the instance in `external.connector`, leave `credentialsRef` out, and create the
217+
plugin with `auth` left at `none`:
218+
219+
```typescript
220+
connectors: [
221+
{
222+
name: 'erp_api', label: 'ERP API', type: 'api',
223+
provider: 'rest', providerConfig: { baseUrl: 'https://erp.example.com' },
224+
auth: { type: 'bearer', credentialRef: 'ERP_API_TOKEN' },
225+
},
226+
],
227+
datasources: [
228+
{
229+
name: 'erp', driver: 'erp', schemaMode: 'external', config: {},
230+
external: { connector: 'erp_api', allowWrites: true }, // no credentialsRef
231+
},
232+
],
233+
plugins: [
234+
createODataDriverPlugin({
235+
name: 'erp',
236+
serviceRoot: 'https://erp.example.com/odata/v4/sales/', // no auth, no secrets
237+
}),
238+
],
239+
```
240+
241+
- **The instance's `auth` decides how the credential is sent:** `Authorization: Bearer`,
242+
`Authorization: Basic`, or an API key in its `headerName` header (default `X-API-Key`)
243+
or, with `paramName`, a query parameter.
244+
- **The plugin borrows at the first request**, from the `connector-credential-source`
245+
service that `@objectstack/service-automation` registers, and keeps the handle. Every
246+
request then presents the current credential: one past its expiry is obtained afresh
247+
before it is sent, and one the service refuses with a `401` is obtained afresh once and
248+
the request retried. A second `401` answers `503`, naming the instance in the server
249+
log.
250+
- **It fails closed.** Each of these refuses the request with nothing sent, answered
251+
`503 ERR_DATASOURCE_UNAVAILABLE` with the cause in the server log: no automation
252+
service in the stack, an instance that cannot lend (not declared, disabled, no
253+
`auth`), a credential that is not set yet, a `credentialsRef` beside the connector,
254+
and an `auth` scheme other than `none` on the plugin, which would send a second
255+
credential. An instance that cannot lend also fails the datasource's connect, so
256+
Setup → Integrations → Datasources shows why
257+
([Borrowing a connector instance's credential](/docs/data-modeling/external-datasources#borrowing-a-connector-instances-credential)
258+
has the rules the stack is validated against).
259+
200260
## 4. What the service is asked
201261

202262
Behind the floor, a list read is one `GET` on the entity set:

‎content/docs/data-modeling/virtual-objects.mdx‎

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,8 @@ So a driver grows in three steps, each correct:
460460
3. **Skip the floor with a conformance-tested driver.** This is planned for a later
461461
phase ([#27261](https://github.com/objectstack-ai/objectstack/issues/27261)): a driver
462462
that passes the platform's conformance kit would be trusted with paging and the cap.
463-
It does not exist yet.
463+
The kit ships ([Run the conformance kit](#run-the-conformance-kit)); the trust does
464+
not exist yet.
464465

465466
**The 1000-row cap.** A read that matches more than 1000 rows is refused with
466467
`422 EXTERNAL_ROW_CAP_EXCEEDED` rather than answered from a partial set. The fix is to
@@ -492,6 +493,61 @@ configured by the host that composes it, through the options it passes to the pl
492493
keep the credential in the host's secret handling and out of metadata and the
493494
repository.
494495

496+
When a [connector instance](/docs/automation/connectors#declaring-a-provider-bound-instance)
497+
already authorizes the vendor your driver calls, the datasource can name it in
498+
`external.connector` instead
499+
([Borrowing a connector instance's credential](/docs/data-modeling/external-datasources#borrowing-a-connector-instances-credential)):
500+
one credential then serves the connector's flow actions and your driver's requests. The
501+
connect step checks that the named instance can lend, and fails the datasource when it
502+
cannot, but it hands your already-built driver nothing. Your driver borrows the
503+
credential itself, from the `connector-credential-source` service that the automation
504+
service registers:
505+
506+
```typescript
507+
import type { PluginContext } from '@objectstack/core';
508+
import {
509+
CONNECTOR_CREDENTIAL_SOURCE_SERVICE,
510+
callWithBorrowedCredential,
511+
withoutBorrowedCredential,
512+
type BorrowedConnectorAuth,
513+
type ConnectorCredentialHandle,
514+
type IConnectorCredentialSource,
515+
} from '@objectstack/types';
516+
517+
// At the first request: `connector` is the datasource's `external.connector`.
518+
async function borrow(ctx: PluginContext, connector: string): Promise<ConnectorCredentialHandle> {
519+
const source = ctx.getService<IConnectorCredentialSource>(CONNECTOR_CREDENTIAL_SOURCE_SERVICE);
520+
return source.borrow(connector);
521+
}
522+
523+
// Every request: the helper refreshes an expired credential, and on a 401 obtains it
524+
// afresh once and retries; a second 401 throws a 503 naming the instance.
525+
async function request(handle: ConnectorCredentialHandle, url: string): Promise<Response> {
526+
let sent: BorrowedConnectorAuth | undefined;
527+
const response = await callWithBorrowedCredential(handle, (auth) => {
528+
sent = auth;
529+
return fetch(url, {
530+
headers: auth.type === 'bearer' ? { Authorization: `Bearer ${auth.token}` } : {},
531+
});
532+
});
533+
if (!response.ok) {
534+
// An upstream may echo the credential in its error text: remove it before throwing.
535+
const text = await response.text();
536+
throw Object.assign(new Error(sent ? withoutBorrowedCredential(text, sent) : text), {
537+
status: response.status,
538+
});
539+
}
540+
return response;
541+
}
542+
```
543+
544+
The sketch sends only a bearer token. Present `auth` as its `type` says: `bearer` as
545+
`Authorization: Bearer`, `basic` as `Authorization: Basic`, and `api-key` in its
546+
`headerName` header or, with `paramName`, as a query parameter. Refuse the request,
547+
sending nothing, when the service is absent or the datasource also names a
548+
`credentialsRef`. The OData driver is the reference implementation
549+
(`packages/drivers/driver-odata/src/plugin.ts` and `transport.ts`).
550+
495551
## What your driver receives
496552

497553
Measured on the reference test, request by request, with the engine's floor
@@ -604,6 +660,41 @@ operator from "refused" to a shape case, then teach `OPERATORS` the mapping. To
604660
same shapes arrive through a booted stack — HTTP door, permissions, formula, hook —
605661
start from the reference test named at the top of this page.
606662

663+
### Run the conformance kit
664+
665+
The shape test pins what *your* driver sends. The platform's plugin-driver conformance
666+
kit pins what the *engine* relies on, from outside this repository: a driver in its own
667+
repository runs it in its own CI, so a platform change to that contract fails there
668+
before it reaches your users. It ships in `@objectstack/objectql`:
669+
`definePluginDriverConformanceSuite` registers one test per case under your runner's
670+
`describe` / `it` (vitest and jest alike), and `runPluginDriverConformance` returns a
671+
report without a test framework.
672+
673+
```typescript
674+
import { afterAll, describe, it } from 'vitest';
675+
import { definePluginDriverConformanceSuite } from '@objectstack/objectql';
676+
import { createPortScheduleDriver } from './port-schedule-driver';
677+
import { fakeRemoteHolding } from './fake-remote'; // your fake of the remote's API
678+
679+
definePluginDriverConformanceSuite(
680+
{ describe, it, afterAll },
681+
{
682+
// A driver over a remote holding exactly the fixture's rows, served under its object.
683+
setup: (fixture) => ({ driver: createPortScheduleDriver(fakeRemoteHolding(fixture)) }),
684+
},
685+
);
686+
```
687+
688+
The kit's oracle is the spec's driver conformance tables, the standard every built-in
689+
driver is held to. Each read is checked against the floor: the driver may push all of a
690+
filter down, part of it or none, and may refuse what it cannot translate, but must never
691+
return fewer rows than match, invent a row, or answer a failure as rows. Add
692+
`setupUnavailable` (a remote that is down) for the outage cases and `writeProbes` (one
693+
write the remote accepts, one it refuses) for the write cases; without them those cases
694+
report `not-offered`. `tiers: ['floor', 'direct']` adds the stricter tier, where the
695+
driver answers the whole query itself; the floor does not need it. The OData driver runs the
696+
kit in `packages/drivers/driver-odata/src/odata-driver.conformance-kit.test.ts`.
697+
607698
## Five business examples
608699

609700
**Port berth scheduling — computed, real-time.** The worked example above. The

0 commit comments

Comments
 (0)