Repository navigation
Commit c6954d6
Fixes #21058
Clause-②: no
## What this changes
`POST /api/v1/datasources`, `PATCH /api/v1/datasources/:name` and `POST
/api/v1/datasources/test` now judge the datasource record against
`DatasourceSchema` as it will be persisted, before any secret or record
write. `os build` and `PUT /api/v1/meta/datasource/:name` already
enforce that contract.
- **The binding is included.** The judged record carries the binding it
will have: the existing `external.credentialsRef`, or the one a supplied
`secret` is about to write. The latter is a stand-in ref, because the
real one does not exist until `writeSecret` runs.
- **The envelope is reused.** A refusal answers with the door's existing
`400 DATASOURCE_ADMIN_ERROR`, the one an inline url password already
gets (card step 5), and carries `DatasourceSchema`'s own message.
- **Nothing new is added.** There is no new error code, no new spec
export, and no edit under `packages/spec`.
The change lands in one file,
`packages/services/service-datasource/src/datasource-admin-service.ts`.
It adds one private `recordRefusal` (a `DatasourceSchema.safeParse` of
the record, with each issue mapped to `path: message`) and three call
sites. No refinement is restated in `service-datasource`.
## Measured decisions
1. **The premise holds: no spec export is needed.** The record to be
persisted parses as a `Datasource`, and the refinements fire. Readings
at `53ed3d10`:
- card step 2 gives one issue at `config.url`
(`CREDENTIALS_REF_MONGO_URL_NO_USER_REFUSED`);
- card step 4 gives one issue at `config.username` (its composed twin);
- `mongodb://app@…` with a bound ref is valid;
- `schemaMode: 'external'` with no `external` block gives one issue at
`external`.
2. **The whole schema is judged, not a picked subset of refinements.**
zod runs a refinement only once the record parses structurally.
Measured: a seeded record with no `config` reports the `config` issue
alone, and the managed-federation refinement on the same record never
runs. So every issue the schema raises is a refusal. Two more inputs are
now refused, and the build and meta doors already refuse both:
- a create with no `config`;
- a `pool` key the schema does not declare.
The changeset lists both.
3. **Update: a write that changes only `label` and/or `active` is not
judged.** The existing config and pool gates already make this
exemption, so a stored row can always be renamed or taken out of
service. Any other patched key (`driver`, `schemaMode`, `config`,
`external`, `pool`), or a supplied `secret`, judges the whole merged
record. The judgement runs before the redacted-credential restore, on
the same subject the config gate reads: what the author wrote. The
restore cannot move a cross-field verdict, because `redactUrlPassword`
keeps the username in the userinfo.
4. **`testConnection` shares the gap, and this PR closes it too.**
Measured in `default-datasource-driver-factory.ts`:
- `buildMongoAuth` returns `undefined` for a url whose userinfo names no
user;
- `buildMongoUrl` writes no userinfo when `username` is absent.
So a probe that carries a secret connects anonymously and reports green
for a pairing that Save now refuses. The test door already guards
against that shape for `config`. It now answers `ok: false` with the
same message and does not probe.
The console's test call (objectui pin `e420df31`,
`DatasourceResourcePage.tsx:369`) sends `{ driver, config, secret }`
with no `name`, so the judged copy takes a fixed stand-in name. The
probe's subject is the connection, and the test door never judged
identity.
## Pins
The pins live in
`src/__tests__/datasource-admin-record-judgement.test.ts` and drive the
real routes over the real service. Each refusal asserts four things:
- status 400;
- `error.code` `DATASOURCE_ADMIN_ERROR`;
- a message containing the issue `DatasourceSchema` itself raises for
the same record (derived in the test, never restated);
- the store behind it: nothing persisted, no secret minted or unbound.
Cases by door:
- **Create.**
- Refusals: card step 2, card step 4, and `schemaMode: 'external'` with
no block.
- Controls: a url that names a user plus a secret gives 201 and is
bound; a user-less url with no secret gives 201; external mode with
`external: {}` gives 201.
- **Update.**
- Refusals: dropping the url user under an existing binding (the record
is unchanged); a secret for a user-less url (no secret is minted); a
composed config with no username plus a secret; switching to external
mode with no block.
- Control: a url that names a user plus a secret gives 200.
- Exemption: `active: false` and `label` on an already-stored invalid
row give 200.
- **Test door.**
- Refusal: a user-less url plus a secret gives `ok: false`, and the
probe is never called.
- Control: the console-shaped draft (no name) is probed.
Two seeds in `datasource-admin-service.test.ts` were never spec-valid.
One had no `config`; the other carried a federation key on a managed
row. Both are now judged, so both gain the missing declarations. Their
pins (secret rewrap, ref preservation) are unchanged.
### Ablations
Each leg went through `scripts/ablation-replace.mjs` at `c9c28fcf`: the
anchor hit once and dropped from 1 to 0, and every restore was proven by
the HEAD-blob hash `f4bb912e`.
| leg | mutation | what went red |
|---|---|---|
| A1 | drop the create judgement | 3 create refusals: `expected 201 to
be 400` |
| A2 | drop the update judgement | 4 update refusals |
| A3 | ignore the test-door refusal | the test-door refusal: `expected
true to be false` |
| A4b | write the secret before the create judgement | the step 2 and
step 4 pins: `expected [ 'writeSecret' ] to deeply equal []` and
`expected 1 to be +0`. Status stays 400, so the ordering assertions are
what catch it. |
| A5 | judge every update (drop the exemption) | the exemption pin:
`expected 400 to be 200` |
| A6 | judge the test draft's own name | the console-shaped control (no
name) |
The first A4 attempt was a no-op. Its replacement contained the anchor,
so the tool refused it (anchor count stayed at 1) and ran no test. A4b
re-ran it with a replacement that does not contain the anchor.
## Live repro
The card's steps were run on a fresh showcase boot at `38696df1`
(`objectstack dev --seed-admin`, my own port, torn down afterwards).
| request | card (before) | this branch |
|---|---|---|
| step 2: POST, user-less url + secret | 201 | 400
`DATASOURCE_ADMIN_ERROR`, `config.url: this mongo …names no user…`; GET
then gives 404 |
| step 4: POST, composed config + secret | 201 | 400 `config.username:
…`; GET then gives 404 |
| POST, external mode with no block | 201 | 400 `external:
schemaMode='external' requires 'external' settings.` |
| control: POST, `mongodb://app@…` + secret | (not run) | 201;
`hasSecret: true`; meta `_diagnostics.valid: true` |
| PATCH dropping the url user, on the really-stored record | (not run) |
400; the record is unchanged |
| PATCH, console-shaped round trip | (not run) | 200 |
| PATCH `active: false` | (not run) | 200 |
| test door: user-less url + secret | (not run) | `ok: false`; no probe
|
| `PUT /meta`: the same pairing | 422 | 422 (unchanged) |
| step 5: inline url password | 400 | 400 (unchanged) |
## Verification
All readings are at `38696df1` unless noted.
- **Typecheck.** `pnpm --filter @objectstack/service-datasource
typecheck` exits 0, and `tsc --listFiles` includes the new test file.
- **Tests.** `pnpm --filter @objectstack/service-datasource test`: 35
files, 707 tests passed.
- **Builds.** The dependency closure builds with exit 0. The package
build passes `check-dts-emitted` (4 of 4 declaration files).
- **Gate derivation.** `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` over 4 paths against merge base
`53ed3d10` gives 62 commands. Reconciled with `--ran`: 60 ran with exit
0, and 2 are NOT MEASURED (exit 3, PREREQUISITE NOT MET; each needs the
whole workspace built):
- `check:dual-build-cjs-loads`: in its place, this package's own
`dist/index.cjs` and `dist/index.js` were loaded, and both export
`DatasourceAdminService`;
- `check:type-check-debt`.
- **Shallow clone.** `check-plugin-teardown-shape --self-test` first
exited 3, because its fixture commit `621a4876` was unreachable. After
fetching that one commit, the re-run exited 0.
- **Lint, narrowed.** `eslint --no-inline-config --format json` over the
3 changed `.ts` files reports 3 files, 0 errors and 0 warnings. All 3
are in the lint population (`--print-config` resolves each).
`eslint.config.mjs` never enables type-aware linting, so this diff
cannot change the verdict on an untouched file. The full `pnpm lint` run
belongs to CI.
## Acceptance notes
- **objectui producer, measured at pin `e420df31`.**
- The console's datasource editor offers `schemaMode` `external` and
`validate-only` (`DatasourceResourcePage.tsx:166-167`), but its
`draftBody` never sends an `external` block (`:353-361`).
- Before this PR, a Save in those modes answered 201 and read back
`valid: false`.
- After this PR, such a Save **without** a secret answers 400 `external:
schemaMode='external' requires 'external' settings.`, and the console
has no field the user can act on. With a secret, the stored
`external.credentialsRef` supplies the block and the Save passes.
- Producer fix: send `external: {}` (or the federation settings)
whenever `schemaMode` is not `managed`. This is reported to the PM for
filing and not changed here.
- **Secret-only PATCH on an older row.** A `PATCH` that carries only a
`secret` judges the whole stored record. A row stored before the
inline-credential refusal, still holding `config.password`, is refused
on such a PATCH, and the message names that key. The console always
sends `config` (redacted) alongside a secret, so the console path is not
affected.
- **Body keys `toRecord` drops (read, not exercised).** The admin door's
`toRecord` copies only the `DatasourceDraft` keys, so a body's `ssl`,
`description` or `autoConnect` is still dropped without a word. This is
outside this card; it is noted only.
- **Neighbours.** #21086 and #21087 had not landed when this PR opened,
and nothing on `main` since `53ed3d10` touches these files.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 432c8ab commit c6954d6
4 files changed
Lines changed: 492 additions & 2 deletions
File tree
- .changeset
- packages/services/service-datasource/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 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 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
0 commit comments