You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit d359857
Browse filesBrowse the repository at this point in the historyBrowse files
feat(driver-odata): an OData v2 dialect beside v4, selected with version: '2.0' (#28177)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DVZtFRFfhUNep8fsh1opTud)_
domain:server#4 · os-elon-musk · session_016WV4cykf9LVyVYxnw1NL95
Fixes#28125
Clause-②: yes (widening)
Metadata-protocol: added
## 维护者速读
- 改了什么:`@objectstack/driver-odata` 在 v4 留好的方言接缝上新增 OData v2 方言,插件配置
`version: '2.0'` 即可接 v2 服务(默认仍是 `4.0`)。覆盖卡上每一项:`d.results` / `__count` /
`__next` 响应封装、`$inlinecount`、`substringof` 与 v2
带类型字面量(`datetime'…'`、`guid'…'`、`7L`、`1.5M` 等)、键谓词、`MERGE` 写入(`POST` +
`X-HTTP-Method`)、EDMX 1.0 的 Association / AssociationSet。spec 的类型矩阵补
`Edm.DateTime → datetime`、`Edm.Time → time`。
- 为什么改:v2 仍是 SAP Gateway 大部分标准服务、旧版 Dynamics CRM 和 SharePoint 2013 REST
的协议;main 上 `version: '2.0'` 直接拒绝启动。SAP 专有机制(CSRF 握手、`sap-client`、`sap:*`
注解)按 10-29 裁定没有放进来。
- 风险与回滚:v4 行为不变:v4 原有 732 个测试在 main 源码和本 PR 上都是绿的。方言接口只新增 4
个可选成员,仓库外的方言照常编译。spec 两个别名只放宽(以前是 `unknown`)。回滚即 revert。
- 先看:`packages/drivers/driver-odata/src/dialect-v2.ts`,以及
`odata-driver.ts` 里 4 个可选钩子的接入点。
- 两分钟验证:`pnpm --filter @objectstack/driver-odata exec vitest run`,预期
1383 passed(其中 v2 651 条:一致性用例集 336、插件套件 276、录制驱动 15、方言 24)。
- 需要你做的:无。欠一次契约复审(`packages/spec/src/**`),由席位安排。
## What changes
- **`packages/drivers/driver-odata/src/dialect-v2.ts`** (new):
`odataV2Dialect` and `parseCsdlV2`, implementing the existing
`ODataDialect`.
- Requests carry `DataServiceVersion: 2.0` / `MaxDataServiceVersion:
2.0` and `Accept: application/json` (verbose JSON).
- Collections are read out of `d.results` (or a 1.0 service's bare `d`
array), with `__next` as the next link and the string `__count` as the
count. An entity is read out of `d`, and its ETag comes from
`__metadata.etag`. `__metadata` and each navigation's `{ __deferred }`
stub are dropped from the row.
- The count is `$inlinecount=allpages`, and `contains` is
`substringof('x',P)`.
- Typed literals: `datetime'…'` (only for a comparand that names its
instant, spelled as its UTC wall clock), `datetimeoffset'…'`,
`time'PT…'`, `guid'…'`, `7L`, `1.5M` (no exponent), `1.5d`, `1.5f`.
- `keyValue` reads a composite key's typed literal back, so
`TrackingId=guid'…',Leg=2L` round-trips.
- Values: `/Date(ms)/` → the UTC instant, or the date under a field
declared `date` (v2 has no `Edm.Date`). `Edm.Int64` / `Edm.Decimal` text
→ number, except an `Edm.Int64` beyond the safe range, which stays text.
`Edm.Time` `PT13H20M` → `13:20:00`. Writes use the inverse forms.
- A partial update is `POST` with `X-HTTP-Method: MERGE`.
- EDMX 1.0 `$metadata`: a navigation's target and cardinality come from
its `Association` ends. Its `ReferentialConstraint` pairs are taken only
on the dependent end, as v4 writes them on that navigation. Its
entity-set binding comes from the `AssociationSet`s, base types
included.
- ⛔ No vendor mechanics (no CSRF handshake, `sap-client` or `sap:*`).
- **`dialect.ts`**: the seam gains four optional members (`readValue`,
`writeValue`, `keyValue`, `updateRequest`). If a dialect leaves one out,
the driver keeps v4's behaviour. `odataV4Dialect` implements none of
them.
- **`odata-driver.ts`**:
- `DIALECTS['2.0']` is the v2 dialect.
- `toRow` reads each declared property through `readValue`, and
`toRemote` writes it through `writeValue`.
- `update` takes its verb from `updateRequest`, and the composite key
predicate is re-spelled through `keyValue`.
- `registerExternalObject` records each field's declared type from the
object's `fields`, the way the SQL driver learns field types, so
`readValue` sees it.
- **`config.ts`**: `version: z.enum(['4.0', '2.0'])`, default `4.0`.
- **`dialect-v4.ts`**: pure refactor: the XML helpers are exported, and
the inheritance flattening is extracted into `flattenEntityTypes` for
the v2 reader. The v1/EDMX refusal message now names `version: '2.0'`
and still matches the v4 suite's pin.
- **`packages/spec/src/data/type-compat.ts`**: `DIALECT_ALIASES.odata`
gains `edm.datetime` → `datetime` and `edm.time` → `time`, and nothing
else.
- **Fixtures and tests**:
- `northwind-v2-metadata.xml` is Northwind's v2 shape, abridged, plus
two fixture-own sets with a Guid key and a composite (Guid, Int64) key.
`recorded-service-v2.ts` holds the verbose JSON recordings. Both are
written from the service's shape, because the container has no network.
- `dialect-v2.test.ts` has 24 tests and `odata-driver-v2.test.ts` has
15.
- The in-process service (`odata-service.ts`) gains a strict `version:
'2.0'` mode: EDMX 1.0, the `d` envelope, `$inlinecount` and `MERGE`. It
refuses v4's `$count`, `contains`, bare temporal literals and `PATCH`.
- The conformance case-sets (`odata-driver.conformance.test.ts`) and the
plugin-driver kit (`…conformance-kit.test.ts`) now also run over that v2
service. The v4 describe names are unchanged; the kit's object is
registered with its `fields`, as the engine registers it.
- `service-datasource`'s `external-object-draft-odata-metadata.test.ts`
gains a v2 describe over its own copy of the v2 `$metadata`. This
test-only path was added to the claim's surface by the seat.
- **Docs**: `content/docs/data-modeling/odata-datasources.mdx` gains an
"OData v2 services" section with the `version: '2.0'` line, a v4/v2
spelling table, and what stays out. `README.md` and the package
description name v2.
- **`.changeset/28125-driver-odata-v2-dialect.md`**:
`@objectstack/driver-odata` minor and `@objectstack/spec` minor
(`added`).
## Why
### Before (measured at `origin/main` `062a2323`, production sources
reverted, new tests kept)
- driver-odata: 627 failed, 732 passed. Every v2 test failed: 336 in the
case-sets, 276 in the kit and 15 in the recorded-driver suite, each on
`driver-odata: invalid config — version: Invalid input: expected "4.0"`.
`dialect-v2.test.ts` could not load, because the module did not exist.
The 732 passing tests are the whole v4 suite.
- service-datasource v2 import: 5 failed (the same config refusal), 13
passed (v4).
- spec `type-compat.test.ts`: 1 failed, `expected 'unknown' to be
'datetime'`.
### After
All green: driver-odata 1383 passed, the service-datasource import 18
passed, and spec `type-compat` 29 passed.
### Clause-② and the kind
`yes (widening)`: the driver config accepts a second `version` value,
and the type matrix answers `datetime` / `time` where it answered
`unknown` (a draft typed those columns `text` for review, and Gate 2
refused a `datetime` / `time` field over one). Nothing accepted before
is refused now. `Metadata-protocol: added`: `version: '2.0'` is a new
value a datasource author can write, so both moved packages take
`minor`.
### One finding met on the way
The plugin-driver kit's "temporal: every row, as stored" check failed on
the first v2 run. A `date` field over v2's `Edm.DateTime` read back as
`1969-12-31T00:00:00.000Z` while the remote held `1969-12-31`. v2 has no
`Edm.Date`, and a v2 service keeps a date in an `Edm.DateTime` at
midnight. So the driver now reads the date alone under a field the
object declares `date`, and the kit registers its object with its
`fields`, as the engine does.
## Verification
- **Pins red first, ablated, restored.** Each ablation used
`scripts/ablation-replace.mjs` on the committed WIP head, each restored
with `git diff HEAD` empty. Every one went red:
- the envelope (`d.results`)
- the Association `ReferentialConstraint`
- the AssociationSet bindings
- `substringof`
- the numeric suffixes
- the `datetime'…'` prefix
- the `guid'…'` key literal
- `$inlinecount`
- `version` accepting `2.0`
- `2.0` selecting the v2 dialect
- the spec aliases
- `MERGE`
- a date field over `Edm.DateTime`
- the dropped `__deferred` stubs
One extra run stayed green: the spec-alias ablation against the
service-datasource import test. service-datasource resolves
`@objectstack/spec` from its built `dist`, so a `src` mutation does not
reach it. The alias pin is the spec test, which went red.
- **The v4 suite is unchanged.** Its 732 tests passed on `main`'s
sources and pass on this head. The shared test files gained a version
loop, and the v4 run keeps its exact describe names.
- **Gates.** On the committed head `667660ad`, after a whole-tree `turbo
run build` (exit 0): `dispatch-gates --commands` derived 117 families,
all run, all exit 0. `--ran`: 117 derived, 117 run, 0 NOT-MEASURED. Also
run: `pnpm lint` exit 0; `typecheck` of driver-odata, service-datasource
and spec, each exit 0; driver-odata 1383 passed; service-datasource 1074
passed; spec `src/data` 4591 passed. The head is 15 commits behind
`origin/main`; none of them touches a file this diff changes.
- **Generated spec artifacts.** `pnpm --filter @objectstack/spec build`
then `check:generated`: all 14 up to date (the alias table feeds no
artifact).
## Acceptance notes
- **The v2 fixtures are written from the services' published shape, not
recorded live.** The container has no route to services.odata.org. The
XML header says what is abridged and what is fixture-own, as the v4
TripPin fixture's header does.
- **`Prefer: odata.maxpagesize` is still sent on a v2 GET.** A v2
service ignores a preference it does not know (RFC 7240 §2), and the
page size it uses is its own. The docs and the `pageSize` describe say
so. The transport is untouched.
- **A zone-less date comparand on `Edm.DateTime` is not pushed down.**
It is left to the engine, the same rule v4 applies to
`Edm.DateTimeOffset`, so the service is never asked a narrower question.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(Two alias entries in a pure mapping table.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [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 checked
(`check:generated`: all up to date, nothing to regenerate).
- [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_016WV4cykf9LVyVYxnw1NL95)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4cykf9LVyVYxnw1NL95
Co-authored-by: Claude <noreply@anthropic.com>
feat(driver-odata): an OData v2 service is served by the same driver with `version: '2.0'` — the verbose JSON envelope, `$inlinecount`, typed literals, `MERGE`, and `$metadata` Associations read into the lookups an import drafts
7
+
8
+
Clause-②: yes (widening)
9
+
Metadata-protocol: added
10
+
11
+
`createODataDriverPlugin({ …, version: '2.0' })` (and `parseODataDriverConfig`) now accepts `'2.0'` beside `'4.0'`, which stays the default. Before, any value but `'4.0'` refused the boot. The v2 dialect (`odataV2Dialect`, `parseCsdlV2`, exported beside the v4 ones) plugs into the existing dialect seam; the driver and the `$filter` translator are unchanged, so the never-narrower rule, paging, keys, ETags and the error mapping hold as on v4. Only the public v2 protocol ships: SAP Gateway's CSRF token handshake, `sap-client` and the `sap:*` annotations stay out of this repository, for a vendor plugin.
12
+
13
+
-**Requests and the envelope.** Requests carry `DataServiceVersion: 2.0` and ask for `application/json`. A collection is read out of `d.results` (or a 1.0 service's bare `d` array), with `__next` followed as `@odata.nextLink` is and the string `__count` read as the count. An entity is read out of `d`; its ETag comes from `__metadata.etag`; `__metadata` and each navigation's `{ __deferred }` stub are dropped from the row.
-**`$filter`.**`$contains` is `substringof('x',P)`; `startswith` / `endswith` / `tolower` as on v4. Literals are typed: `datetime'…'`, `datetimeoffset'…'`, `time'PT…'`, `guid'…'`, `7L`, `1.5M` (no exponent), `1.5d`, `1.5f`. An `Edm.DateTime` comparand is pushed down only when it names its instant (a zone, an offset or a `Date`), as that instant's UTC wall clock; a bare date or a zone-less date-time is left to the engine.
16
+
-**Keys.** Key predicates take the same typed literals (`Shipments(guid'…')`, `Items(7L)`), and a composite key's `id` (`TrackingId=guid'…',Leg=2L`) is read back into the same predicate.
17
+
-**Values.**`Edm.DateTime` (`/Date(ms)/`) is read as the UTC instant, or as its date under a field the object declares `date` (v2 has no `Edm.Date`); `Edm.Int64` and `Edm.Decimal` text is read as a number (an `Edm.Int64` beyond the safe range stays text); `Edm.Time` (`PT13H20M`) is read as the time of day. Writes go out in the same forms.
18
+
-**Writes.**`create` is `POST`; a partial update is `MERGE`, sent as a `POST` tunnelled with `X-HTTP-Method: MERGE`; `delete` is `DELETE`. `If-Match` carries the `__metadata.etag` of the row's last read.
19
+
-**`$metadata`.** EDMX 1.0 is read: a navigation's target and cardinality come from its `Association` ends, its foreign key from the Association's `ReferentialConstraint` when this type is the dependent end, and its target entity set from the `AssociationSet`s. `introspectSchema()` reports dialect `odata-2.0` and the same navigation shape v4 yields, so `generateObjectDraft` / `importObject` draft the same lookups over either version. A v2 document under `version: '4.0'`, or a v4 one under `'2.0'`, is refused with the version that reads it named.
20
+
-**The dialect seam** gains four optional members (`readValue`, `writeValue`, `keyValue`, `updateRequest`); a dialect that leaves one out keeps the v4 behaviour, so an out-of-repo dialect still compiles. `readValue` is given the declared type of the field a property backs, which the driver now records from the object's `fields` in `registerExternalObject`.
21
+
-**`@objectstack/spec`:** the type matrix's `odata` arm reads the two primitives only v2's CSDL declares, `Edm.DateTime` → `datetime` and `Edm.Time` → `time`. Both were `unknown`: a draft typed them `text` and flagged them for review, and a `datetime` or `time` field declared over one failed Gate 2 as a `type_mismatch`. Nothing that passed before fails now.
Copy file name to clipboardExpand all lines: content/docs/data-modeling/odata-datasources.mdx
+66-14Lines changed: 66 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,12 +1,14 @@
1
1
---
2
2
title: "OData datasources — ERP records, live, with no driver of your own"
3
3
navTitle: OData Datasources
4
-
description: Declare a datasource over an OData v4 service such as SAP S/4HANA or Dynamics 365, bind objects to its entity sets, and read and write them live, with no code per entity set.
4
+
description: Declare a datasource over an OData v4 or v2 service such as SAP S/4HANA, SAP Gateway or Dynamics 365, bind objects to its entity sets, and read and write them live, with no code per entity set.
5
5
---
6
6
7
7
Many business systems already publish their records over **OData v4**: SAP S/4HANA,
8
8
Microsoft Dynamics 365 and Dataverse, and any service that serves a CSDL 4.0
9
-
`$metadata` document. For those you need no driver of your own. Mount
9
+
`$metadata` document. Many others still speak **OData v2**: most SAP Gateway services,
10
+
the older Dynamics CRM `OrganizationData.svc` and SharePoint 2013 REST. For those you
11
+
need no driver of your own. Mount
10
12
`@objectstack/driver-odata` once per service, declare a datasource, and bind an object
11
13
to each entity set you want. Every view, form, permission set, formula and hook then
12
14
works on those records, and no row is stored on the platform side.
@@ -20,16 +22,17 @@ says when to choose it over a driver of your own or a synced copy.
20
22
21
23
Everything on this page is what the driver's own tests assert, against an in-process
22
24
OData service and behind the real engine
23
-
(`packages/drivers/driver-odata/src/odata-driver.floor.test.ts` and
24
-
`odata-driver.conformance.test.ts`). Read them when this page and the runtime seem to
`odata-driver.conformance.test.ts`, which runs over a v4 and a v2 service, and
27
+
`odata-driver-v2.test.ts`). Read them when this page and the runtime seem to disagree.
26
28
27
29
## Check the service first
28
30
29
-
-**It speaks OData v4.** Open `<serviceRoot>$metadata`. The root element must say
30
-
`Version="4.0"`. Only the v4 dialect ships. A service that speaks only OData v2 needs a
31
-
v2 dialect, which this package does not include. Many older SAP Gateway services are
32
-
v2 only, so check each service you plan to bind.
31
+
-**Which OData version it speaks.** Open `<serviceRoot>$metadata`. A root element
32
+
that says `Version="4.0"` is a v4 service, the default. One that says `Version="1.0"`
33
+
(EDMX 1.0, with `m:DataServiceVersion="2.0"` or `"1.0"` below it) is a v2 service:
34
+
declare `version: '2.0'` ([OData v2 services](#odata-v2-services)). A document of the
35
+
other version is refused at the first request, with the version that reads it named.
33
36
-**Each entity set you bind has a key.** The key becomes the record's `id`. An entity
34
37
type with no usable key is refused at the first request.
35
38
-**You know how it authenticates.** The driver sends no credential, a bearer token, or
@@ -61,15 +64,15 @@ export default defineStack({
61
64
| :--- | :--- | :--- |
62
65
|`name`|`odata`| The datasource it serves. It is also the driver name and the `driver.<name>` service suffix. |
63
66
|`serviceRoot`| (required) | The service root URL, http or https, with no credentials and no query string. |
64
-
|`version`|`4.0`| The protocol version.`4.0`is the only value. |
67
+
|`version`|`4.0`| The protocol version:`4.0`or `2.0` ([OData v2 services](#odata-v2-services)). |
65
68
|`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)). |
66
69
|`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. |
67
-
|`pageSize`|`500`| Rows asked for per server page (`Prefer: odata.maxpagesize`). |
70
+
|`pageSize`|`500`| Rows asked for per server page (`Prefer: odata.maxpagesize`). A v2 service has no such preference and pages by its own size. |
68
71
|`requestTimeoutMs`|`30000`| The timeout of each request. |
69
72
70
73
The options are checked when the plugin is built. An unknown key, a `serviceRoot` with
71
-
a user name or a query in it, or a version other than `4.0` refuses the boot with every
72
-
problem named.
74
+
a user name or a query in it, or a version other than `4.0`or `2.0`refuses the boot
75
+
with every problem named.
73
76
74
77
## 2. Declare the datasource and bind objects
75
78
@@ -324,6 +327,54 @@ and the ways out are the same as on any plugin driver
324
327
-**Editable data:** declare `sharingModel: 'public_read_write'`, and decide who may edit
325
328
in a `beforeUpdate` hook. A member's delete is still refused on this model.
326
329
330
+
## OData v2 services
331
+
332
+
Declare the version on the plugin; nothing else in the datasource or the objects changes:
Copy file name to clipboardExpand all lines: packages/drivers/driver-odata/README.md
+5-4Lines changed: 5 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,7 +1,8 @@
1
1
# @objectstack/driver-odata
2
2
3
-
An OData v4 datasource driver for ObjectStack. Declare a datasource over an OData
4
-
service (SAP S/4HANA, Dynamics 365, Dataverse, any CSDL 4.0 service), bind objects to
3
+
An OData v4 and v2 datasource driver for ObjectStack. Declare a datasource over an OData
4
+
service (SAP S/4HANA, Dynamics 365, Dataverse, any CSDL 4.0 service; with `version: '2.0'`,
5
+
SAP Gateway and any other EDMX 1.0 service), bind objects to
5
6
its entity sets, and every view, form, permission and hook works on them, live. No row
6
7
is stored on the platform side, and no code is written per entity set.
7
8
@@ -66,10 +67,10 @@ export default defineStack({
66
67
| :--- | :--- | :--- |
67
68
|`name`|`odata`| The datasource name, the driver name and its `driver.<name>` service suffix. |
68
69
|`serviceRoot`| — | The service root URL (http or https, no credentials, no query). |
69
-
|`version`|`4.0`| The protocol version. Only the v4 dialect ships. |
70
+
|`version`|`4.0`| The protocol version: `4.0`, or `2.0` for the v2 dialect (verbose JSON, `$inlinecount`, typed literals, `MERGE`, Associations). Vendor mechanics such as SAP's CSRF handshake are not in this package. |
70
71
|`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 (below). |
71
72
|`secrets`| (none) | An object with `resolve(credentialsRef)` that the secret is resolved through. When absent, the kernel service `datasource-secret-resolver` is read at the first request; `os serve` registers it over its own `sys_secret` store. A host composing its own kernel passes the binder it gives `DatasourceAdminServicePlugin`, or registers that service. |
72
-
|`pageSize`|`500`| Rows asked for per server page (`Prefer: odata.maxpagesize`). |
73
+
|`pageSize`|`500`| Rows asked for per server page (`Prefer: odata.maxpagesize`; a v2 service pages by its own size). |
0 commit comments