Skip to content

Commit 8368f1c

Browse files
feat(service-automation,core,types): the connector sync executor pulls a mapping's connectorSource through the import runner, moved beside bulkWrite (#21084)
Fixes #20919 Clause-②: yes (widening) Stage ② of ruling A on #20281: the **connector sync executor**. `service-automation` pulls a `mapping` whose `connectorSource` names a declared `rest` / `openapi` connector, and writes the records through the import runner's upsert-by-match-key. Built to the seat's stage-② direction (`5922160386` on #20281) and the amended claim surface (`5922189483`). ## What changed **1. The move (destination A, 0 new `package.json` edges).** `service-automation` → `rest` would invert a layer, so the runner's shared pieces moved down. ⛔ None of them is a copy. - To `@objectstack/core` (`src/utils/`, beside `bulkWrite`): `import-runner.ts` and `import-coerce.ts` (whole files), the mapping apply (`applyMappingToRows`, `refuseUnknownMappingTargets`, `MappingArtifactLike`, `MappingFailure`) as `import-mapping.ts`, and the field meta (`ExportFieldMeta`, `buildFieldMetaMap`) as `import-field-meta.ts`. Their suites moved with them: 6 `import-runner-*` suites, `import-coerce`, `import-mapping`. - To `@objectstack/types`: the classification half of `rest/src/error-response.ts` (`CLIENT_MESSAGE_MAX` through `classifyDataError`, 1,892 lines) as `data-error-classification.ts`. `rest` keeps the emitters. - `rest` re-exports every moved name: from `export-format.ts`, `import-mapping.ts`, `error-response.ts` and the package index. Its production callers are unchanged: `rest-server.ts`'s two import routes, and `plugin-auth`'s `admin-import-users.ts` through the index. The one exception is an import line in `rest-server.ts` (`runImport` now comes from `@objectstack/core`). - Two suites stayed in `rest` because they need `objectql` / `metadata-protocol`, which `core` cannot depend on: `import-runner-historical-readonly-insert`, `import-runner-unique-violation-row`. They now import from the new home. **2. The executor:** `packages/services/service-automation/src/connector-pull.ts`. It is exposed as `pullConnectorSource(deps, opts)` and as `AutomationServicePlugin.pullConnectorSource({ mapping, context })`. - It reads the mapping through the protocol's `getMetaItem({ type: 'mapping', name })`, as the import door does. - It resolves `connectorSource.connector` to a **declared** instance. The plugin now records each materialized instance's provider, because the registered def carries none. Only `rest` / `openapi` is accepted. - It makes **one** action call per pull. `watermark.param` goes into the call's `query`. The records are read at `recordsPath` (default `body`). - **W1 watermark.** The starting point is the highest value stored in the target field that a transform-`none` `fieldMapping` entry copies `watermark.field` onto. It is one read, under the caller's context. Nothing else stores it. - It writes through the moved `runImport`. Every knob `connectorSource` does not declare takes the door's default: `runAutomations` true, `trimWhitespace` true, the rest false. - Nothing schedules a pull. That is stage ③ (a `job`). **3. The texts the executor makes true** (`domain:spec`, the amendment's list): - the `connectorSource` and `watermark` describes; - the `mapping.json` ledger rows: `planned` → `live`, citing `connector-pull.ts#pullConnectorSource`. `authorWarn` stays, with the hint reworded to "pulled when a job drives it; nothing schedules it until stage ③"; - the `syncConfig` prescription and its pin; - the D3 entry (it drops "validated at authoring"); - `SYNC_ARCHITECTURE.md`: how a pull runs, the one-response limit, and the two stale Best Practices bullets rewritten; - the `packages/lint` group-C comments and test title. The migrations registry region and `content/docs/references/**` were regenerated with `check:generated --fix` and `gen:schema` / `gen:docs`. ⛔ No new key, cadence key, credential or paging convention. ## The executor's refusals Every refusal is a typed `ConnectorPullError` (`code`, `status`, `reason`). It is raised **before anything is written**, and each pin asserts that no write reached the protocol. The codes are standard-catalog members or the code the import door already answers with, so no new ledger code was minted. | Condition | `code` | `status` | `reason` | |---|---|---|---| | action answered `ok: false` | `EXTERNAL_SERVICE_ERROR` | 502 | `upstream_not_ok` | | non-array at `recordsPath` | `INTEGRATION_ERROR` | 502 | `records_not_array` | | a record that is not an object | `INTEGRATION_ERROR` | 502 | `record_not_object` | | plugin-origin connector (not a `connectors[]` entry) | `VALIDATION_ERROR` | 400 | `connector_plugin_origin` | | unmapped `watermark.field` (none, or only through a transform) | `VALIDATION_ERROR` | 400 | `watermark_field_unmapped` | | `update` / `upsert` with an empty `upsertKey` | `VALIDATION_ERROR` | 400 | `upsert_key_empty` | | unknown mapping | `MAPPING_NOT_FOUND` | 404 | `mapping_not_found` | | mapping without `connectorSource` | `VALIDATION_ERROR` | 400 | `no_connector_source` | | connector not registered | `VALIDATION_ERROR` | 400 | `connector_not_registered` | | degraded instance | `SERVICE_UNAVAILABLE` | 503 | `connector_degraded` | | provider not `rest` / `openapi` | `VALIDATION_ERROR` | 400 | `connector_provider_unsupported` | | action the connector does not declare | `VALIDATION_ERROR` | 400 | `connector_action_unknown` | | `javascript` transform | `UNSUPPORTED_TRANSFORM` | 400 | `unsupported_transform` | | `connectorSource.input` / `input.query` not an object (watermarked pull) | `VALIDATION_ERROR` | 400 | `input_query_not_object` | | target that is no field of the object | `INVALID_FIELD` (the door's) | 400 | `mapping_target_refused` | The first six are the order's mandated refusals. ## Pins - `connector-pull.test.ts` (18 cases). Every refusal above, plus the watermark read: the target field, highest first, `limit: 1`, merged into the declared `input.query`. A first incremental pull sends no starting point. - `connector-pull.integration.test.ts`: **end to end through a real `rest` connector**. - A declared `connectors[]` entry is materialized by the real `ConnectorRestPlugin` provider. It reads a local `node:http` fixture server over a real socket, and writes into a real ObjectQL engine on SQLite. - Pull 1 sends no `since` and creates 2 rows. - Pull 2 sends `since=2026-01-02T00:00:00.000Z`, the max `synced_at` **read from the target**. It **updates** `c2` by `upsertKey` and creates `c3`. - It imports `connector-rest`'s source by relative path. That package already dev-depends on `service-automation`, so a manifest edge back would close a cycle. The read is declared in `scripts/cross-package-test-inputs.mjs` and `turbo.json`, and `service-automation`'s type-check `rootDir` is widened to admit it (tsup emit unaffected; `check-dts-emitted` 2/2). - Red first: `4f4260ee` committed the pins alone. The run against that state failed both files: `Cannot find module './connector-pull.js'` and `automation.pullConnectorSource is not a function`. `4b301797` then added the code. The executor file's hash was identical before and after that red run (`26a83b0d4a20fdab`). - The moved suites are green at their new homes: `core` 69 files / 2044 tests, `rest` 241 / 4576. ## Verification ### Move proof (a move is a move) An extraction script compared every moved range at base `e161ad35` with its destination: ``` IDENTICAL packages/rest/src/export-format.ts:35-65 (31 lines) -> packages/core/src/utils/import-field-meta.ts IDENTICAL packages/rest/src/export-format.ts:139-182 (44 lines) -> packages/core/src/utils/import-field-meta.ts IDENTICAL packages/rest/src/import-mapping.ts:37-55 (19 lines) -> packages/core/src/utils/import-mapping.ts IDENTICAL packages/rest/src/import-mapping.ts:117-328 (212 lines) -> packages/core/src/utils/import-mapping.ts IDENTICAL packages/rest/src/error-response.ts:66-1957 (1892 lines) -> packages/types/src/data-error-classification.ts WHOLE-FILE packages/rest/src/import-runner.ts -> packages/core/src/utils/import-runner.ts: 7 changed line(s), all import lines WHOLE-FILE packages/rest/src/import-coerce.ts -> packages/core/src/utils/import-coerce.ts: 9 changed line(s), all import lines ``` `git diff -M --color-moved=zebra` over the split files, counting `+`/`-` lines painted as moved against those not: ``` packages/types/src/data-error-classification.ts: moved+=1896 non-moved+=49 (header, import lines, trailing export list) packages/rest/src/error-response.ts: moved-=1896 non-moved+=28 non-moved-=5 (header note, import/re-export lines) packages/core/src/utils/import-mapping.ts: moved+=241 non-moved+=14 (header, import lines) packages/rest/src/import-mapping.ts: moved-=241 non-moved+=11 (import/re-export lines) packages/core/src/utils/import-field-meta.ts: moved+=75 non-moved+=13 (header) packages/rest/src/export-format.ts: moved-=75 non-moved+=4 non-moved-=1 (import/re-export lines) ``` The eight classification helpers the REST emitters compose are exported through a **trailing** `export { … }` list, so no declaration line was edited. Never a copy: no declaration of `mapDataError`, `classifyDataError`, `structuredCodeAnswer`, `declaredHttpStatus`, `applyMappingToRows`, `refuseUnknownMappingTargets`, `buildFieldMetaMap`, `runImport`, `coerceRow`, `ExportFieldMeta` or `MappingArtifactLike` is left in `packages/rest/src` (0 each). The control: the same grep finds each one at its new home. R2 held: the moved half has 0 code references to the emission half or to `log.js`. Its only mentions of the emitters are `{@link}` prose in docblocks. ### #20963 (PR #21001) It had landed (`e161ad35`) before the split, so the branch starts from it. The moved half carries #21001's 18 added lines byte-equal (`18 of 18` found verbatim in `data-error-classification.ts`). There was no git conflict. ### Tests and typechecks Run on head `ec59b16a`, after merging `main`: - `types` 22 files / 685 tests; - `core` 69 / 2044; - `rest` 241 / 4576 (+135 skipped), `test:repo` 5 / 179; - `service-automation` 162 / 2021; - `spec` 587 / 17235; - `plugin-auth` import suites 2 / 50; - `lint` 118 / 5452; - `runtime` vocabulary suites 4 / 70. `typecheck` is green for `types`, `core`, `rest`, `service-automation`, `spec`, `lint`, `runtime` and `plugin-auth`. `spec check:generated`: all 15 artifacts up to date. `check:liveness`: green. ### Gates `dispatch-gates --ran` reconciles: 139 derived, 138 run, 1 NOT-MEASURED, 0 UNRUN, run on head `ec59b16a`. - **NOT MEASURED: `check:pm-dispatch-gates`.** Its non-self-test half outlives the ~10-minute foreground cap here (killed at 595s); its `--self-test` half exits 0. CI runs it. - **One red, not this PR's: `check:platform-checklist`.** The only finding is `areas/identity-auth.json` → `plugin-auth/src/auth-plugin.ts#twoFactor`, and it is red identically at base `e161ad35` and on `origin/main`. This PR's own checklist anchors (`classifyDataError`, the mapping `transform` switch) are repointed and resolve. - The 11 ⛔ roster families the derivation printed are all exit 0. ## Changed lines **6,032** (`additions + deletions`, rename detection on: 3,672 / 2,360, 64 files), over the 5,000 line. Per AGENTS.md this PR takes the **human-merge path** (an authorised approval, then the owning seat, or the maintainer's merge), not the queue alone. About 4,100 of those lines are relocation, and GitHub shows the whole-file moves as renames. ## Acceptance notes **Carriers, not touched here:** - **ADR-0114**'s consumer line names `@objectstack/rest`'s `import-coerce.ts` / `import-runner.ts`. That is `docs/adr` (Tier H), left for a docs follow-up. - `packages/objectql` comments cite `packages/rest/src/import-runner.ts` (`index.ts`, `summary-errors.ts`, and two `plugin-audit-created-*` test comments). They are outside the surface. - Further comment-only path citations of the moved files live in `driver-turso`, `metadata-protocol`, `plugin-security`, `spec` (`conversions/registry.ts`, `api/export.zod.ts`, `data/mapping.zod.ts`'s older docblocks), `scripts/check-dispatcher-error-vocabulary.mjs`, `check-error-status-conformance.mjs` and `check-durability-degradation-log-level.mjs`. They are prose only, and every gate is green. - `.changeset/20281-connector-sync-moved-to-mapping.md` (stage ①, unreleased) still says the binding is "declared, not yet executed". This PR's spec changeset supersedes it; whoever compiles the release notes should read the two together. **Surface extensions, each a mechanical consequence of the move or of the pin:** - `turbo.json` and `scripts/cross-package-test-inputs.mjs`: `rest`'s source-reading pins read the moved runner and classification, and `service-automation`'s integration pin reads `connector-rest` source; - `packages/rest/vitest.repo-tests.json`; - `packages/services/service-automation/tsconfig.json` (`rootDir`); - `docs/qa/platform-checklist/areas/{attachments-storage,records-forms}.json` anchors; - `packages/spec/liveness/state-counts/mapping.md` (generated). **Choices the direction left open, settled here:** - The refusal codes come from the standard catalog, so the ADR-0112 ledger is not edited. `reason` is the discriminator. - The watermark only reads a transform-`none` `fieldMapping` entry: a transformed target does not hold the pulled value. **Not measured:** - a real third-party `openapi` target (the pin uses `rest`); - the paging behaviour of any real upstream (no measured target exists; the one-response limit is documented); - `spec test:repo` was narrowed to the 7 repo suites that read this diff's files. The full project outlives the foreground cap here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_012Kijp4k6e9fvgq63BLGsxY)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2742e53 commit 8368f1c

71 files changed

Lines changed: 3723 additions & 2373 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/core': minor
3+
---
4+
5+
feat(core): the bulk-import runner, its row coercion, the mapping apply and the field-meta map now live in `@objectstack/core`, beside `bulkWrite` (#20919)
6+
7+
`runImport` (with `sanitizeRowError` and its option/result types), the cell
8+
coercion (`coerceRow`, `coerceFieldValue`, `parseDateCell`, `parseNumberCell`,
9+
`parseBooleanCell`, `matchOption`, `splitMulti`, `isBlank`), the `mapping`
10+
artifact pipeline (`applyMappingToRows`, `refuseUnknownMappingTargets`,
11+
`MappingArtifactLike`, `MappingFailure`, `ApplyMappingOptions`) and the field
12+
metadata map (`buildFieldMetaMap`, `ExportFieldMeta`) are exported from
13+
`@objectstack/core`. They moved here unchanged from `@objectstack/rest` so the
14+
connector sync executor in `@objectstack/service-automation` writes through the
15+
same runner as the HTTP import door without depending on the HTTP layer. Nothing
16+
to change for consumers: `@objectstack/rest` re-exports every name it exported
17+
before.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/rest': patch
3+
---
4+
5+
refactor(rest): the import runner, coercion, mapping apply, field-meta map and error classification moved to `@objectstack/core` / `@objectstack/types`; `rest` re-exports them (#20919)
6+
7+
`runImport`, `coerceRow`, `buildFieldMetaMap` and their types (from the package
8+
index), and `mapDataError` with its sibling classification exports, are now
9+
re-exported from their new homes — byte-identical code, the same names, the same
10+
behaviour at both import routes and at `plugin-auth`'s identity import. Nothing to
11+
change for consumers.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@objectstack/service-automation': minor
3+
---
4+
5+
feat(service-automation): the connector sync executor pulls a `mapping`'s `connectorSource` and writes it through the import runner (#20919)
6+
7+
`AutomationServicePlugin.pullConnectorSource({ mapping, context })` (and the
8+
exported `pullConnectorSource(deps, opts)`) reads the mapping through the
9+
protocol's `getMetaItem`, resolves `connectorSource.connector` to a declared
10+
(`connectors[]`) `rest` or `openapi` instance, makes ONE call to its read action,
11+
takes the array at `recordsPath` (default `body`), projects it through the
12+
mapping's `fieldMapping` and writes it with `@objectstack/core`'s `runImport` —
13+
the import door's coercion, `mode` / `upsertKey` matching and per-row verdicts,
14+
with the door's defaults for every knob `connectorSource` does not declare.
15+
16+
- **Watermark, read from the target.** For `connectorSource.watermark`, the
17+
starting point sent as `query[watermark.param]` is the highest value already
18+
stored in the target field a `fieldMapping` entry copies `watermark.field` onto
19+
(transform `none`). Nothing else stores it.
20+
- **One response per pull.** The connector's paging is not followed.
21+
- **Loud refusals,** each a `ConnectorPullError` (`code`, `status`, `reason`)
22+
raised before anything is written: a plugin-registered or unregistered
23+
connector, a degraded instance, a provider other than `rest` / `openapi`, an
24+
undeclared action, `update` / `upsert` with an empty `upsertKey`, a
25+
`javascript` transform, an unmapped `watermark.field`, an `ok: false` answer,
26+
a non-array at `recordsPath` and a non-object record.
27+
- **Nothing schedules a pull** — a `job` will drive it; the caller supplies the
28+
execution context.
29+
30+
The plugin now records the provider of each declared connector instance it
31+
materializes, which the executor reads.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): `mapping.connectorSource` is pulled when a job drives it — the describes say where the watermark is read from and that a pull reads one response (#20919)
6+
7+
The `connectorSource` describe no longer says the pull is not executed: the
8+
connector sync executor in `@objectstack/service-automation` reads the binding,
9+
and nothing schedules a pull until the `job` stage lands. `watermark.field` now
10+
states that the next pull's starting point is read from the TARGET field a
11+
`fieldMapping` entry copies it onto (an unmapped one is refused at pull time), and
12+
`watermark` states the one-response limit: the connector's paging is not followed,
13+
so a paged endpoint yields its first page only. The liveness ledger's
14+
`connectorSource` rows are `live` and keep `authorWarn` (nothing schedules a pull
15+
yet). The retired `connector.syncConfig` prescription and the
16+
`connector-sync-keys-retired` upgrade entry say the same, and the entry's
17+
acceptance criterion no longer claims the connector is validated at authoring.
18+
No key, value or default changed.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/types': minor
3+
---
4+
5+
feat(types): the data-error classification table (`mapDataError` and its judgements) is exported from `@objectstack/types` (#20919)
6+
7+
The classification half of `@objectstack/rest`'s error boundary — `mapDataError`
8+
(a thrown error → the `{ status, body }` ADR-0112 answer, without emitting it),
9+
`declaredHttpStatus`, `declaredServerFaultAnswer`, `sandboxBusinessMessage`,
10+
`boundedDeclaredUserMessage`, `boundedDeclaredRefusalMessage` and
11+
`isEngineDuplicateRecordEnvelope` — moved here unchanged, beside the primitives it
12+
composes, so the bulk-import runner in `@objectstack/core` judges a failed row with
13+
the same table the REST door answers with. `@objectstack/rest` keeps the emitters
14+
and re-exports every name it exported before. The module also exports the eight
15+
helpers the REST emitters compose (`truncateClientMessage`, `thrownCodeFields`,
16+
`withoutDeclaredCodePrefix`, `withDeclaredUserMessage`, `isSandboxOrigin`,
17+
`isSandboxCrash`, `fiveXxArmDisplacesDeclared4xx`, `structuredCodeAnswer`).

‎content/docs/references/data/mapping.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const result = ImportFieldMappingSchema.parse(data);
4949
| **fieldMapping** | `{ source: string \| string[]; target: string \| string[]; transform: Enum<'none' \| 'constant' \| 'lookup' \| 'split' \| 'join' \| 'javascript' \| 'map'>; params?: object }[]` | ✅ | |
5050
| **mode** | `Enum<'insert' \| 'update' \| 'upsert'>` | optional (default: `"insert"`) | |
5151
| **upsertKey** | `string[]` | optional | Fields to match for upsert (e.g. email) |
52-
| **connectorSource** | `{ connector: string; action: string; input?: Record<string, any>; recordsPath?: string; … }` | optional | Pull binding: the rest/openapi connector this mapping pulls rows from (one-way, full or timestamp-incremental; a `job` sets the cadence). Declared; the pull is not executed yet |
52+
| **connectorSource** | `{ connector: string; action: string; input?: Record<string, any>; recordsPath?: string; … }` | optional | Pull binding: the rest/openapi connector this mapping pulls rows from (one-way, full or timestamp-incremental; a `job` sets the cadence). Pulled when a job drives it; nothing schedules it yet |
5353
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
5454
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
5555
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
@@ -75,7 +75,7 @@ const result = ImportFieldMappingSchema.parse(data);
7575
| **action** | `string` | ✅ | Key of the connector action that reads the records: `request` on a `rest` connector, an operation key on an `openapi` connector |
7676
| **input** | `Record<string, any>` | optional | Fixed input passed to the action on every pull (e.g. `{ method: 'GET', path: '/contacts' }` for `rest`) |
7777
| **recordsPath** | `string` | optional | Dot path, within the action's result, to the array of records (e.g. `body.results`); omitted ⇒ the result's `body` is the array |
78-
| **watermark** | `{ field: string; param: string }` | optional | Timestamp-incremental pull; omitted ⇒ every pull reads the full set |
78+
| **watermark** | `{ field: string; param: string }` | optional | Timestamp-incremental pull; omitted ⇒ every pull reads the full set. Either way a pull makes ONE action call and reads ONE response: the connector's paging is not followed, so a paged endpoint yields its first page only — and an incremental pull over a newest-first paged endpoint moves its starting point past the pages it never read |
7979

8080

8181
---

‎content/docs/references/integration/connector.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ const result = ConnectorSchema.parse(data);
191191
| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
192192
| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record<string, any>; … }[]` | optional | |
193193
| **triggers** | `never` | optional | [REMOVED] `connector.triggers` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a connector trigger never started anything: `AutomationEngine.registerConnector` registers a connector's actions only, no polling loop read `intervalSeconds` (or the `interval` spelling it was renamed from), and no receiver was driven by a `webhook` trigger. Delete the key; the `ConnectorTrigger` shape leaves with it. To start work from an external system, write a flow that calls the connector's action in a `connector_action` node: for an external event, an `api` flow that the event's sender calls; for a scheduled pull, a `schedule` flow. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
194-
| **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. That binding is declared but its pull is not executed yet, and authoring it warns until it is. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
194+
| **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. Its pull runs when a `job` drives it; nothing schedules one yet, and authoring the binding warns until a job can. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
195195
| **fieldMappings** | `never` | optional | [REMOVED] `connector.fieldMappings` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever moved a value through a connector field mapping: nothing read `source`, `target`, `defaultValue`, `dataType`, `required` or `syncMode`. Delete the key; the `ConnectorFieldMapping` shape leaves with it. Map fields on the sync's TARGET instead: a `mapping`'s `fieldMapping` (`source` → `target`, with a `transform` the import path executes), which its `connectorSource` pulls through. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
196196
| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so it was never materialized into `sys_webhook` and never delivered, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on record events — note that doing so STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
197197
| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
@@ -488,7 +488,7 @@ Connector type
488488
| **auth** | `{ type: 'none' } \| { type: 'bearer'; credentialRef: string } \| { type: 'api-key'; credentialRef: string; headerName?: string; paramName?: string } \| { type: 'basic'; username: string; credentialRef: string }` | optional | Declarative instance auth — references credentials via `credentialRef` (resolved at boot), never inline secrets. Requires `provider` (ADR-0097). |
489489
| **actions** | `{ key: string; label: string; description?: string; inputSchema?: Record<string, any>; … }[]` | optional | |
490490
| **triggers** | `never` | optional | [REMOVED] `connector.triggers` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a connector trigger never started anything: `AutomationEngine.registerConnector` registers a connector's actions only, no polling loop read `intervalSeconds` (or the `interval` spelling it was renamed from), and no receiver was driven by a `webhook` trigger. Delete the key; the `ConnectorTrigger` shape leaves with it. To start work from an external system, write a flow that calls the connector's action in a `connector_action` node: for an external event, an `api` flow that the event's sender calls; for a scheduled pull, a `schedule` flow. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
491-
| **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. That binding is declared but its pull is not executed yet, and authoring it warns until it is. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
491+
| **syncConfig** | `never` | optional | [REMOVED] `connector.syncConfig` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever ran a connector-attached sync: nothing read `strategy`, `direction`, `realtimeSync`, `timestampField`, `conflictResolution`, `batchSize`, `deleteMode` or `filters`, so the `latest_wins` and `soft_delete` defaults resolved and deleted nothing. Delete the key; the `DataSyncConfig` shape leaves with it. A sync is defined on its TARGET: a `mapping` (`targetObject`, `fieldMapping`, `mode`, `upsertKey`) whose `connectorSource` names the `rest` or `openapi` connector it pulls from, the read action and an optional timestamp `watermark`, with a `job` for the cadence. Its pull runs when a `job` drives it; nothing schedules one yet, and authoring the binding warns until a job can. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
492492
| **fieldMappings** | `never` | optional | [REMOVED] `connector.fieldMappings` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no engine ever moved a value through a connector field mapping: nothing read `source`, `target`, `defaultValue`, `dataType`, `required` or `syncMode`. Delete the key; the `ConnectorFieldMapping` shape leaves with it. Map fields on the sync's TARGET instead: a `mapping`'s `fieldMapping` (`source` → `target`, with a `transform` the import path executes), which its `connectorSource` pulls through. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
493493
| **webhooks** | `never` | optional | [REMOVED] `connector.webhooks` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — a webhook nested inside a connector was never registered as a `webhook` item, so it was never materialized into `sys_webhook` and never delivered, and nothing emits the connector events its `events` list could name (`sync.completed`, `auth.expired` and the rest). Delete the key; the nested shape leaves with it (`WebhookConfig`, `WebhookEvent`, `WebhookSignatureAlgorithm`). To have a webhook actually sent, declare it in the stack's top-level `webhooks:` collection, which is materialized into `sys_webhook` and delivered on record events — note that doing so STARTS deliveries this connector never made. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
494494
| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |

0 commit comments

Comments
 (0)