@syncmatters/connector-sdk 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69,7 +69,7 @@ await this.#core.init(args);
69
69
  | Field | Values / meaning |
70
70
  | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
71
71
  | `name` | the file name, e.g. `"Acme.mjs"` |
72
- | `type` | `"connector"` (entry) · `"module"` · `"connectortest"` · `"json"` · `"yaml"` |
72
+ | `type` | `"connector"` (entry) · `"module"` · `"connectortest"` · `"json"` · `"yaml"` · `"markdown"` (help docs shipped beside the connector, e.g. `Connectors/<Name>/help/index.md` — see [17-user-help-pages.md](./17-user-help-pages.md); never a module, never staged at runtime) |
73
73
  | `runnable` | `false` for connector files (they are invoked by the platform, not run directly) |
74
74
  | `engine` | runtime engine version int; leave as scaffolded unless told otherwise |
75
75
  | `resource_size` | `""` or `"medium"` — runtime resource class hint |
@@ -105,16 +105,22 @@ Keyed by setting id; the values arrive in `init()` via `args.settings.get("<id>"
105
105
  "type": "string", "control": "select",
106
106
  "options": [ { "id": "1", "name": "1 (Superseded)", "order": "00" }, { "id": "2", "name": "2 (Recommended)", "order": "01" } ],
107
107
  "default": { "type": "string", "expression": "2" } },
108
+ "time_zone": { "order": "04", "name": "Time Zone", "type": "string", "control": "timezone",
109
+ "default": { "type": "string", "expression": "UTC" } },
108
110
  "debug": { "order": "03", "name": "Debug", "advanced": true, "type": "boolean", "control": "singlelinetext" }
109
111
  }
110
112
  ```
111
113
 
112
114
  Per-setting fields: `order` (two-digit string, display order), `name`, `type` (`string` |
113
- `boolean`), `control` (`singlelinetext` | `select`), `secret: true` (masked + encrypted),
115
+ `boolean`), `control` (`singlelinetext` | `textarea` | `select` | `timezone`), `secret: true` (masked + encrypted),
114
116
  `advanced: true` (collapsed by default), `hidden: true`, `readonly: true`, `placeholder`,
115
- `options` (for `select`), `default: { "type": "string", "expression": "<value>" }`, and
117
+ `options` (for `select` only — not used for `timezone`), `default: { "type": "string", "expression": "<value>" }`, and
116
118
  `defines_version: true` on the setting whose value picks a version core.
117
119
 
120
+ Use `control: "timezone"` for IANA time zone pickers (e.g. fleet `time_zone` setting). The stored
121
+ value is a plain string (`"America/New_York"`, `"UTC"`, …); the UI renders a searchable list of
122
+ IANA zones — do not list zones in `options`.
123
+
118
124
  Conventions worth copying: a secret `api_key`/`clientId`+`clientSecret` pair for credentials; an
119
125
  advanced boolean `debug` setting that gates verbose request/response logging; a
120
126
  `connectorVersion` select with `defines_version` when you need breaking revisions to coexist.
package/docs/05-query.md CHANGED
@@ -115,6 +115,40 @@ the final page; the platform stores it and hands it back as `checkpointFilter.va
115
115
  rows modified while your query ran would otherwise be lost forever. Re-reading a few rows is
116
116
  free; losing one is a data bug.
117
117
 
118
+ #### Time zones
119
+
120
+ **Emit the checkpoint as a UTC ISO-8601 string.** The value is opaque to the platform, but
121
+ UTC keeps logs, the results UI and your own debugging sane — and wall-clock arithmetic across
122
+ DST transitions is where checkpoint bugs breed.
123
+
124
+ Some systems — especially on-prem APIs (SysPro is the fleet example) — expect their
125
+ changed-since filter as **wall-clock time in the server's local zone**, not UTC. The pattern:
126
+
127
+ 1. Add a connection setting for the IANA time zone (fleet convention: `time_zone`, e.g.
128
+ `"America/New_York"`) with `type: "string"` and `control: "timezone"` — the connector cannot
129
+ guess the server's zone. See [01-workspace-and-meta-json.md](./01-workspace-and-meta-json.md).
130
+ 2. Keep the STORED checkpoint UTC; convert **at the filter**, when building the request from
131
+ `checkpointFilter.value`:
132
+
133
+ ```js
134
+ const localSince = SDK.utilities.dateTransform({
135
+ input: options.queryOptions.checkpointFilter.value ?? "1970-01-01T00:00:00Z",
136
+ inputFormat: "iso8601",
137
+ outputFormat: "yyyy-MM-dd HH:mm:ss", // or "custom" + outputFormatCustom for odd APIs
138
+ outputTimeZone: this.#timeZone, // the connection's IANA time_zone setting
139
+ });
140
+ ```
141
+
142
+ `dateTransform` also carries `inputTimeZone` (parse a zone-less API timestamp as wall clock),
143
+ `dateTimeMath` (the overlap subtraction: `[{ count: -5, interval: "minutes" }]`) and
144
+ truncation; `dateFormat(date, { format, tz })` is the light variant for rendering a `Date`.
145
+
146
+ DST note: converting through a wall-clock zone makes one hour a year ambiguous or absent —
147
+ your overlap window is what absorbs it, which is one more reason the overlap rule is not
148
+ optional. If you find fleet code emitting a wall-clock checkpoint with a re-tagged offset
149
+ (SysPro does), that is a workaround for an API that round-trips its own local time — prefer
150
+ UTC-stored/convert-at-filter for new connectors.
151
+
118
152
  ### rowFilter — user-parameterized augmentation of the standard filters
119
153
 
120
154
  The system-specific filters you declared in `meta().queryFilter.rowFilter`
@@ -123,9 +123,11 @@ To disable those tests use `cannotTestReason` instead
123
123
  object; the harness fetches it via `idsFilter`, then queries the related object with
124
124
  `relatedFilter` and compares against `expectedRelatedRowIds`. The test spec side
125
125
  (`relationshipsTo`) also lives on the declaring object's features.
126
- - `upsertPrepare` — returns `UpsertData`; use `withIssueSimulators` / `UpsertIssueSimulator`
127
- (`verifyFields`, expected issue `type` + `fatal`) to deliberately exercise your
128
- `upsertClean` issue reporting.
126
+ - `upsertPrepare` — returns `UpsertData`; every `verifyFields` path must appear with a
127
+ non-null (and ideally CHANGED) value in the `update` payload too see the `verifyFields`
128
+ contract under "What each test needs" below. Use `withIssueSimulators` /
129
+ `UpsertIssueSimulator` (`verifyFields`, expected issue `type` + `fatal`) to deliberately
130
+ exercise your `upsertClean` issue reporting.
129
131
  - delete features support soft-delete verification — `isDeleted: { path, valueWhenDeleted }`
130
132
  checks a flag field instead of row absence, and `afterDeleteMaxIndexWaitTimeMs` tolerates
131
133
  APIs whose deletions surface asynchronously.
@@ -177,9 +179,20 @@ Requirements that only surface as cryptic failures when unmet:
177
179
  one row (or no list support and no prepare hook) fails with `"No test data"`. Each id is
178
180
  then queried alone and must return exactly that one row.
179
181
  - **The upsert test is insert THEN update, twice over.** `upsertPrepare` is called twice; both
180
- rows are inserted, re-queried by `idsFilter` and compared on `verifyFields` (which must be
181
- non-null on the payload), then BOTH are updated via the declared `upsert.key` and re-queried
182
- again. Objects whose rows can never update declare `upsert.insertOnly` — see below.
182
+ rows are inserted, re-queried by `idsFilter` and compared on `verifyFields`, then BOTH are
183
+ updated via the declared `upsert.key` and re-queried again. Objects whose rows can never
184
+ update declare `upsert.insertOnly` — see below.
185
+ - **`verifyFields` is a contract on BOTH payloads.** After each write the harness re-queries
186
+ the row and compares every `verifyFields` path against the payload it just sent — so each
187
+ listed field must be present with a non-null value in the `insert` payload AND in the
188
+ `update` payload. An update payload that omits a verify field fails with
189
+ `"verifyField value on the upsert row was blank"` (the classic mistake: updating one field
190
+ while `verifyFields` names another). Give the update DIFFERENT values than the insert —
191
+ the comparison is payload-vs-queried-row, so unchanged values would let a silently ignored
192
+ update pass; mutating is what makes the update phase prove anything. Values must round-trip
193
+ exactly (string-equal after re-query) — pick fields the API echoes verbatim, not ones it
194
+ normalizes or formats. And return the SAME `verifyFields` paths from both prepare calls:
195
+ the harness reads row 2's values through call 1's paths.
183
196
  - **relatedFilterPrepare needs a declaring-object row that actually has related rows** —
184
197
  `expectedRelatedRowIds` must be non-empty. In the parent-owns-children shape that means a
185
198
  parent with at least one child in the sandbox (create one in `before()` when the API allows;
@@ -403,7 +403,9 @@ export default class TestFeatureProvider {
403
403
  const update = { domain: `${uid}-updated.example.com` };
404
404
  result.insert = insert;
405
405
  result.update = update;
406
- // the harness re-queries the row after each write and compares these paths
406
+ // the harness re-queries the row after EACH write and compares these paths against
407
+ // the payload it just sent - so every verify field must be present (non-null) in
408
+ // BOTH insert and update, and the update should carry a CHANGED value (09)
407
409
  result.verifyFields.push([{ path: "domain" }]);
408
410
  break;
409
411
  }
@@ -1,9 +1,23 @@
1
1
  # Writing the connector's user help pages
2
2
 
3
- A finished connector ships with **customer-facing help pages**. They live in the platform
4
- repo under `docs/help-markdown/connections/<slug>/` (slug = kebab-case product name, e.g.
5
- `epicor-prophet-21`) and follow a fixed house pattern — copy the structure of an existing
6
- current page (`greenhouse/` is a clean modern example) rather than inventing one:
3
+ A finished connector ships with **customer-facing help pages** and they ship WITH the
4
+ connector, in the script tree, versioned alongside the code they describe:
5
+
6
+ ```
7
+ Connectors/<Name>/help/index.md (sidecar type: "markdown")
8
+ Connectors/<Name>/help/scripting.md (sidecar type: "markdown")
9
+ Connectors/<Name>/help/guides/*.md (optional deep dives)
10
+ ```
11
+
12
+ Author them in the workspace like any other file (`sm push` creates/updates them; each needs
13
+ a sidecar with `type: "markdown"`, `runnable: false`). They are documentation, not code —
14
+ never listed in `module_script_file_paths`, never staged at runtime. When the connector is
15
+ published as a bundle, the platform promotes these pages into the app help under
16
+ `connections/<slug>/…` (promotion is rolling out — until then the docs team lifts them into
17
+ the help repo verbatim, which is why following the house pattern below matters).
18
+
19
+ The house pattern — copy the structure of an existing current page (`greenhouse/` in the
20
+ platform help is a clean modern example) rather than inventing one:
7
21
 
8
22
  | File | Audience | Content |
9
23
  | -------------- | --------- | -------------------------------------------------------------------- |
@@ -36,7 +36,7 @@ queryable.
36
36
  | [14-vendor-sdks-and-non-http.md](./14-vendor-sdks-and-non-http.md) | vendor npm SDKs (googleapis, AWS, box), database/on-prem `agent_mode: "always"` connectors, SOAP/XML, self-managed rate limiting |
37
37
  | [15-example-test-file.md](./15-example-test-file.md) | **the canonical worked test file** — the complete `mod Test.mjs` + `mod ObjectTestFeatures.mjs` pair for the Acme connector of 12 (TestFeatureProvider, prepare/cleanup hooks, issue simulators, feature scoping) |
38
38
  | [16-the-end-game-syncs.md](./16-the-end-game-syncs.md) | **why any of this exists** — how the sync pipeline consumes each connector capability, and what silently degrades when one is missing. Read once before designing `meta()` |
39
- | [17-user-help-pages.md](./17-user-help-pages.md) | writing the connector's customer help pages (`connections/<slug>/index.md` + `scripting.md`) the house structure, frontmatter, and example conventions |
39
+ | [17-user-help-pages.md](./17-user-help-pages.md) | writing the connector's customer help pages — they ship IN the connector (`Connectors/<Name>/help/*.md`, sidecar type `"markdown"`); the house structure, frontmatter, and example conventions |
40
40
 
41
41
  ## The 60-second version
42
42
 
@@ -169,8 +169,10 @@ Implementing more methods unlocks more platform capability:
169
169
  9. **Run it**: `sm test` = the full suite (identical to the web UI's connector test — including
170
170
  write features, so use a sandbox connection); `--only connection|meta|query` narrows. A
171
171
  passing `--only` run is NOT suite coverage (09).
172
- 10. **Document it**: write the customer help pages `connections/<slug>/index.md` +
173
- `scripting.md` in the platform repo, derived from `meta()` and the sidecar (17).
172
+ 10. **Document it**: write the customer help pages IN the connector —
173
+ `Connectors/<Name>/help/index.md` + `help/scripting.md` (sidecar type `"markdown"`),
174
+ derived from `meta()` and the sidecar; they version with the code and publish with the
175
+ bundle (17).
174
176
 
175
177
  ## Support
176
178
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncmatters/connector-sdk",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "TypeScript type definitions for the SyncMatters connector SDK (types only - connectors execute on the SyncMatters platform)",
5
5
  "types": "./index.d.ts",
6
6
  "exports": {
@@ -12,9 +12,9 @@
12
12
  "license": "MIT",
13
13
  "author": "SyncMatters",
14
14
  "homepage": "https://syncmatters.com",
15
- "typesContentHash": "9d23ef0dc02fc7886f97e31eba1d50be7f8f6dae368c9b3ef4b1c969807d005c",
15
+ "typesContentHash": "ae459fd0936deb05ec10d5b85156b8a268d06d5568f1c94d2f81d97c1224efac",
16
16
  "dependencies": {
17
17
  "@types/node": "*",
18
- "@syncmatters/script-api": "^1.0.5"
18
+ "@syncmatters/script-api": "^1.0.6"
19
19
  }
20
20
  }