@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.
- package/README.md +13 -6
- package/docs/01-workspace-and-meta-json.md +132 -0
- package/docs/02-lifecycle-and-shape.md +116 -0
- package/docs/03-http-auth-ratelimit.md +188 -0
- package/docs/04-meta-objects-fields.md +162 -0
- package/docs/05-query.md +151 -0
- package/docs/06-upsert-delete.md +194 -0
- package/docs/07-events-webhooks.md +123 -0
- package/docs/08-errors.md +68 -0
- package/docs/09-testing.md +127 -0
- package/docs/10-style-and-pitfalls.md +100 -0
- package/docs/11-composite-keys.md +164 -0
- package/docs/12-example-connector.md +566 -0
- package/docs/13-file-fields.md +123 -0
- package/docs/14-vendor-sdks-and-non-http.md +115 -0
- package/docs/connector-authoring.md +158 -0
- package/package.json +3 -3
|
@@ -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.
|
|
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": "
|
|
15
|
+
"typesContentHash": "285fcc040f1a62f7472910cf06536de4f33574a6f82c5ac405aa51a6b0797174",
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@types/node": "*",
|
|
18
|
-
"@syncmatters/script-api": "^1.0.
|
|
18
|
+
"@syncmatters/script-api": "^1.0.1"
|
|
19
19
|
}
|
|
20
20
|
}
|