@syncmatters/connector-sdk 1.0.0 → 1.0.1

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.
@@ -0,0 +1,115 @@
1
+ # Vendor SDKs and non-HTTP connectors
2
+
3
+ Not every system speaks plain REST/JSON. When the right client is a vendor npm package
4
+ (`googleapis`, `box-node-sdk`, AWS SDK), a database driver (`pg`, `tedious`), or a protocol
5
+ library (`ssh2` for SFTP, `soap` for WSDL services), the SDK's `httpClient` leaves the picture
6
+ — and with it the rate limiting, retries and monitoring it provided. This topic is how the
7
+ fleet fills those gaps.
8
+
9
+ ## Declaring the dependency
10
+
11
+ List the package in the entry sidecar's `npm_packages` (name → exact version,
12
+ [01-workspace-and-meta-json.md](./01-workspace-and-meta-json.md)); the platform installs it at
13
+ runtime and the `sm` CLI adds it to the workspace `package.json` so IntelliSense resolves it.
14
+
15
+ ```json
16
+ "npm_packages": { "box-node-sdk": "2.4.0" }
17
+ ```
18
+
19
+ Vendor packages are often untyped or loosely typed under `checkJs`: isolate them behind one
20
+ `#client` field and use `@ts-ignore` sparingly at the boundary, not throughout your code.
21
+
22
+ ## Wiring the client in `init()`
23
+
24
+ Build the vendor client from settings, exactly where you would have built `httpClient`:
25
+
26
+ ```js
27
+ import { google } from "googleapis";
28
+
29
+ async init(args) {
30
+ SDK.verifyType(this);
31
+ let auth;
32
+ if (args.settings.get("service_account_json")) {
33
+ // vendor wants a key FILE: stage the setting's content through a FileProvider
34
+ const keyFile = await SDK.utilities.fileProvider({
35
+ string: args.settings.get("service_account_json"),
36
+ }).save();
37
+ auth = new google.auth.GoogleAuth({ keyFile, scopes: ["https://www.googleapis.com/auth/drive"] });
38
+ } else {
39
+ // platform-managed OAuth2: feed the token in AND keep it fresh (03)
40
+ auth = new google.auth.OAuth2();
41
+ auth.setCredentials({ access_token: args.oauth2Provider.token });
42
+ args.oauth2Provider.onRefresh((token) => auth.setCredentials({ access_token: token }));
43
+ }
44
+ this.#drive = google.drive({ version: "v3", auth });
45
+ }
46
+ ```
47
+
48
+ The two patterns to copy: **`oauth2Provider.token` + `onRefresh` feed the vendor auth
49
+ object** (the platform refreshes; you re-inject), and **file-based credentials are staged via
50
+ `fileProvider({string}).save()`** rather than written by hand.
51
+
52
+ Rarely-used sub-clients load lazily with a dynamic import, so cold starts stay fast:
53
+
54
+ ```js
55
+ const STS = await import("@aws-sdk/client-sts");
56
+ const identity = await new STS.STSClient(config).send(new STS.GetCallerIdentityCommand());
57
+ ```
58
+
59
+ ## Rate limiting without `httpClient`
60
+
61
+ `httpClient` normally owns throttling. With a vendor client, wrap **every** call in a
62
+ rate limiter yourself:
63
+
64
+ ```js
65
+ this.#limiter = SDK.utilities.rateLimiter({ type: "rolling", count: 10, ms: 1000 });
66
+
67
+ /** all vendor calls flow through here - the vendor SDK does not respect our limits */
68
+ #callApi(exec) {
69
+ return this.#limiter.execute({ exec });
70
+ }
71
+ // usage:
72
+ const resp = await this.#callApi(() => this.#drive.files.list({ pageSize: 200 }));
73
+ ```
74
+
75
+ Retries are also yours now: vendor SDKs have their own retry options (configure them to match
76
+ [03-http-auth-ratelimit.md](./03-http-auth-ratelimit.md)'s policy) — don't stack a hand-rolled
77
+ retry loop on top of one you didn't disable. And map vendor errors into `ConnectorError`s at
78
+ the same boundary ([08-errors.md](./08-errors.md)); a raw vendor stack trace helps nobody.
79
+
80
+ ## The database-connector archetype (`agent_mode: "always"`)
81
+
82
+ Databases and on-premise systems are a connector shape of their own:
83
+
84
+ - **`agent_mode: "always"`** in the sidecar — the connector runs on the customer's on-premise
85
+ agent, because the database is not reachable from the cloud
86
+ ([01-workspace-and-meta-json.md](./01-workspace-and-meta-json.md)).
87
+ - **No HTTP at all** — the driver connection (`pg`, `tedious`) is built in `init()` purely
88
+ from discrete settings: host, port, database, credentials, TLS/timeout options.
89
+ - **`meta()` comes from schema introspection** — query the information schema / catalog for
90
+ tables and columns, map column types to `ObjectField` types, primary keys to `isKey`
91
+ (composite PKs → [11-composite-keys.md](./11-composite-keys.md)), foreign keys to
92
+ `relationships`.
93
+ - **`query()` pages with real cursors** — a streaming query held open across pages: keep the
94
+ live cursor in a `Map` keyed by an id stored in your `queryState`, feed rows out per page,
95
+ and close it on `finalPage` (and in `dispose()` as a best-effort backstop).
96
+ - **Escape SQL values** exactly as [10-style-and-pitfalls.md](./10-style-and-pitfalls.md)
97
+ demands for any string-interpolated query language — or better, use the driver's
98
+ parameterized queries.
99
+
100
+ Note: on-premise (`agent_mode: "always"`) connectors do not support `upsertClean`
101
+ ([06-upsert-delete.md](./06-upsert-delete.md)).
102
+
103
+ ## XML and SOAP systems
104
+
105
+ - SOAP/WSDL: the `soap` npm package, with the WSDL files bundled as workspace resource files
106
+ (listed in `module_script_file_paths`) and loaded from `args.sourcePath`.
107
+ - Plain XML responses over HTTP: keep `httpClient` and parse with
108
+ `await SDK.utilities.xmlParse({ string: responseBody })` — no extra dependency needed.
109
+
110
+ ## C# connectors (out of scope)
111
+
112
+ A small number of connectors are C# scripts (`.csx`, `agent_mode: "always"`) for
113
+ Windows/COM-interop targets. They use a different SDK surface and carry hard limitations (no
114
+ OAuth2 provider, no webhooks, no `upsertClean`, single file, no npm packages). Do not apply
115
+ this guide's JavaScript patterns to them — they are built by the platform team.
@@ -0,0 +1,158 @@
1
+ # Building SyncMatters connectors — the authoring guide
2
+
3
+ This is the canonical guide to writing connectors for the SyncMatters platform. It ships inside
4
+ `@syncmatters/connector-sdk` and is version-locked to the type definitions next to it. It is
5
+ written to be consumed by AI coding assistants as much as by humans: read this file first, then
6
+ open only the topic files your task touches.
7
+
8
+ A **connector** is a JavaScript class (an `.mjs` file) that teaches the platform how to talk to
9
+ one external system: how to authenticate, what objects and fields exist, and how to query,
10
+ create, update and delete rows. Your code **executes on the SyncMatters platform**, not
11
+ locally — the local workspace is for editing, type-checking and pushing via the `sm` CLI.
12
+
13
+ ## Topic files (read what your task touches)
14
+
15
+ | File | Read when you are... |
16
+ | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
17
+ | [01-workspace-and-meta-json.md](./01-workspace-and-meta-json.md) | adding files, wiring modules, editing any `*.meta.json` sidecar, declaring settings/OAuth/webhooks/npm packages |
18
+ | [02-lifecycle-and-shape.md](./02-lifecycle-and-shape.md) | starting a connector; implementing `init`/`test`/`meta`; anything about `InitArgs` |
19
+ | [03-http-auth-ratelimit.md](./03-http-auth-ratelimit.md) | making HTTP calls, rate limiting, retries, API keys, basic auth, OAuth2 |
20
+ | [04-meta-objects-fields.md](./04-meta-objects-fields.md) | writing `meta()` — objects, fields, flags, keys, relationships, filters |
21
+ | [05-query.md](./05-query.md) | implementing `query()` — paging, checkpoints, ids/match/related filters, `Row` |
22
+ | [06-upsert-delete.md](./06-upsert-delete.md) | implementing `upsert()`, `delete()`, `upsertClean()`, `upsertFieldOptions()`, batching |
23
+ | [07-events-webhooks.md](./07-events-webhooks.md) | webhooks: `parseEvents()`, `handleParsedEvent()`, signature verification |
24
+ | [08-errors.md](./08-errors.md) | error handling — `ConnectorError`, `ErrorCode` selection, `HttpError` |
25
+ | [09-testing.md](./09-testing.md) | the test harness: `mod Test.mjs` + `mod ObjectTestFeatures.mjs` |
26
+ | [10-style-and-pitfalls.md](./10-style-and-pitfalls.md) | style rules, deprecated APIs to avoid, known traps — skim this once regardless |
27
+ | [11-composite-keys.md](./11-composite-keys.md) | objects with multi-part identity (child REST resources, junction rows) — key encoding across query/upsert/delete |
28
+ | [12-example-connector.md](./12-example-connector.md) | **the canonical worked example** — a complete three-object connector (relationships, matching, composite keys, custom fields, upsertClean, query/upsert/delete) on one page |
29
+ | [13-file-fields.md](./13-file-fields.md) | objects carrying file content — fields of `type: "file"`, FileProvider, fields-driven inclusion (do NOT copy the fleet's legacy `row.file`) |
30
+ | [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 |
31
+
32
+ ## The 60-second version
33
+
34
+ A minimal read-only connector (the full-featured canonical example — relationships, matching,
35
+ custom fields, writes — is [12-example-connector.md](./12-example-connector.md)):
36
+
37
+ ```js
38
+ import SDK from "@syncmatters/connector-sdk";
39
+
40
+ export default class Acme {
41
+ /** @type {SDK.HttpClient | undefined} */ #http;
42
+ /** @type {SDK.Logger | undefined} */ #log;
43
+ /** @type {string} */ #apiKey = "";
44
+
45
+ /** @param {SDK.InitArgs} args @returns {Promise<void>} */
46
+ async init(args) {
47
+ SDK.verifyType(this); // compile-time check that this class matches SDK.Connector
48
+ this.#log = args.log;
49
+ this.#apiKey = args.settings.get("api_key");
50
+ this.#http = SDK.utilities.httpClient({
51
+ limiter: SDK.utilities.rateLimiter({ type: "rolling", count: 10, ms: 1000 }),
52
+ maxRetry: 3,
53
+ });
54
+ }
55
+
56
+ /** @returns {Promise<SDK.TestResult>} */
57
+ async test() {
58
+ try {
59
+ await this.#call({ method: "GET", url: "me" });
60
+ return { success: true };
61
+ } catch (err) {
62
+ return { success: false, message: err instanceof Error ? err.message : String(err) };
63
+ }
64
+ }
65
+
66
+ /** @returns {Promise<SDK.ConnectorMeta>} */
67
+ async meta() {
68
+ const whoAmI = await this.#call({ method: "GET", url: "me" });
69
+ return {
70
+ identity: whoAmi.email,
71
+ objects: [
72
+ {
73
+ id: "contacts",
74
+ name: "Contacts",
75
+ canQueryByCheckpoint: true,
76
+ canQueryList: true,
77
+ canQueryByIds: true,
78
+ queryFields: [
79
+ { id: "id", type: "string", isKey: true },
80
+ { id: "email", type: "string" },
81
+ ],
82
+ matchRules: ["email[ci]", "name[ci]"],
83
+ },
84
+ ],
85
+ };
86
+ }
87
+
88
+ /** @param {SDK.QueryOptions} options @returns {Promise<SDK.QueryPage>} */
89
+ async query(options) {
90
+ const queryState = options.queryState || { page: 1 };
91
+ const resp = await this.#call({ method: "GET", url: `contacts?page=${queryState.page}` });
92
+ const rows = resp.items.map((item) => ({ meta: { key: String(item.id) }, data: item }));
93
+ queryState.page += 1;
94
+ return { rows, queryState, finalPage: rows.length === 0 };
95
+ }
96
+
97
+ /** @param {SDK.HttpRequest} request */
98
+ async #call(request) {
99
+ if (!request.url.startsWith("https://")) {
100
+ request.url = "https://api.example.com/v1/" + request.url;
101
+ }
102
+ request.headers = { ...request.headers, Authorization: `Bearer ${this.#apiKey}` };
103
+ return await this.#http.execute(request);
104
+ }
105
+ }
106
+ ```
107
+
108
+ Implementing more methods unlocks more platform capability:
109
+
110
+ | You implement | The platform can |
111
+ | --------------------------------------- | ------------------------------------------------------------------- |
112
+ | `test()` + `meta()` (**required**) | validate connections, show objects/fields in the UI |
113
+ | `query()` | read data — lists, differential syncs, lookups |
114
+ | `upsert()` / `delete()` | write data |
115
+ | `upsertClean()` | pre-validate + change-detect rows before writing (`canUpsertClean`) |
116
+ | `upsertFieldOptions()` | auto-add picklist values (`canUpsertFieldOptions`) |
117
+ | `parseEvents()` / `handleParsedEvent()` | receive webhooks and keep caches fresh |
118
+ | `optimalBatchSize()` | pick batch sizes per operation |
119
+
120
+ ## Golden rules (every one is explained in a topic file)
121
+
122
+ 1. Import the SDK exactly as `import SDK from "@syncmatters/connector-sdk";` in workspace `.mjs` files.
123
+ 2. **Prefer** calling HTTP through `SDK.utilities.httpClient({ limiter: SDK.utilities.rateLimiter(...) })`.
124
+ More out of the box than `fetch`, `axios`, or `node:https`.
125
+ 3. Rows are `{ meta: { key: string }, data: object }`. `key` must be the value of the field your
126
+ `meta()` flags with `isKey` (or `isUserKey`). Keys are strings.
127
+ 4. `meta().objects` is an **array** of `ObjectMeta`. Every `canQuery*`/`canUpsert*` flag you set
128
+ is a promise — the platform will call the corresponding operation.
129
+ 5. Platform-visible failures are `throw new SDK.ConnectorError(SDK.ErrorCode.<Code>, message)`.
130
+ `test()` is the exception: it catches and returns `{ success: false, message }`.
131
+ 6. `query()` is called repeatedly: return `queryState` to continue, `finalPage: true` to stop.
132
+ Checkpoint queries must emit a `checkpoint` computed with an **overlap window** (see 05).
133
+ 7. `upsert()`/`delete()` results must be **positionally aligned** with `options.rows`.
134
+ 8. Every file needs a `meta/**/*.meta.json` sidecar; multi-file connectors are wired by
135
+ `module_script_file_paths` (entry sidecar lists ALL files, module sidecars list direct deps).
136
+ 9. JSDoc-type everything — workspaces run with `checkJs`. Private state in `#fields`. First line
137
+ of `init()` is `SDK.verifyType(this);`.
138
+ 10. Don't use deprecated APIs: `ObjectMetaSummary`/`ObjectMetaDetail`, `cacheRefreshOptions`,
139
+ `QueryPage.cacheState`, `RateLimiter.take()`. (See 10.)
140
+
141
+ ## Building a new connector, end to end
142
+
143
+ 1. **Scaffold**: `sm connector init` (wizard: settings, agent mode, OAuth mode) — or copy an
144
+ existing connector's file pair as a pattern.
145
+ 2. **Wire auth + HTTP** in `init()` (03). Add connection settings to the sidecar (01).
146
+ 3. **Make `test()` real**: cheapest authenticated call the API offers (02).
147
+ 4. **Describe objects** in `meta()` (04). Start with one object and its `isKey` field; grow from
148
+ there. `SDK.utilities.fieldsFromJSON(sample)` can draft field lists from a sample response.
149
+ 5. **Implement `query()`** for `canQueryList`, then `idsFilter`, then `checkpointFilter` (05).
150
+ 6. **Implement writes** if needed: `upsert()`, `delete()` (06).
151
+ 7. **Validate + push**: `sm validate`, `sm status` / `sm diff`, `sm push -m "message"`.
152
+ A push 409 means the platform copy changed: `sm pull`, merge, push again — never force.
153
+ 8. **Test harness**: add `mod Test.mjs` (+ generated `mod ObjectTestFeatures.mjs`) so the
154
+ platform's connector test suite can exercise your objects (09).
155
+
156
+ ## Support
157
+
158
+ - <https://syncmatters.com> · <support@syncmatters.com>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncmatters/connector-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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": "39bdb6461d3d6962e0d284c2f4b6c71f1d08a6cd6a4b422754b3c731414fe6da",
15
+ "typesContentHash": "285fcc040f1a62f7472910cf06536de4f33574a6f82c5ac405aa51a6b0797174",
16
16
  "dependencies": {
17
17
  "@types/node": "*",
18
- "@syncmatters/script-api": "^1.0.0"
18
+ "@syncmatters/script-api": "^1.0.1"
19
19
  }
20
20
  }