@syncmatters/connector-sdk 1.0.20 → 1.0.22
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/docs/04-meta-objects-fields.md +9 -0
- package/docs/05-query.md +6 -1
- package/docs/09-testing.md +18 -1
- package/docs/10-style-and-pitfalls.md +11 -1
- package/docs/13-file-fields.md +30 -0
- package/lib/file-provider-buffer.d.ts +2 -0
- package/lib/file-provider-disk.d.ts +2 -0
- package/lib/file-provider-string.d.ts +2 -0
- package/package.json +3 -3
|
@@ -188,6 +188,15 @@ picklist label or differently-cased id to the option id) before filtering.
|
|
|
188
188
|
field pairs that must ALL be equal (`destA = a AND destB = b ...`). Declare it only when the API
|
|
189
189
|
can apply several equality filters in one query (search filter groups, SOQL/OData `and`, ...).
|
|
190
190
|
|
|
191
|
+
`"domain[ci]"` matches rows **at the host the sync sends**. The sync cleans the source value
|
|
192
|
+
first (`acme.com`, never `https://www.acme.com/`), and a row holding
|
|
193
|
+
`https://www.acme.com/about` must match it. Declare it only when the API can find such rows:
|
|
194
|
+
equality on a bare-domain field, or contains / `like` (or equality with `IN` / `OR` over the
|
|
195
|
+
common URL variants) on a website field, followed by a `domainCompare` check of each
|
|
196
|
+
candidate. A website field with single-value equality only cannot honour the rule: do not
|
|
197
|
+
declare it.
|
|
198
|
+
See the `domain[ci]` bullet in [05-query.md](./05-query.md).
|
|
199
|
+
|
|
191
200
|
**Commonly forgotten:** an object that supports `idsFilter` can ALWAYS offer
|
|
192
201
|
`"field_value_equals[ci]"` — with `canMatch: true` on its id field, the match is just a
|
|
193
202
|
lookup of the supplied value by id. There is no reason for such an object to declare no
|
package/docs/05-query.md
CHANGED
|
@@ -236,7 +236,12 @@ candidates }])` — the platform reserves un-mapped matches and returns the sele
|
|
|
236
236
|
|
|
237
237
|
- **name[ci]** - inspect the `match.name` values and search for rows with the same `name`. The determination of which field holds name is made by the connector, e.g. for a company it may be `companyName`, for a deal it may be `title`.
|
|
238
238
|
- **email[ci]** - inspect the `match.email` values and search for rows with the same `email`. The determination of which field(s) holds email is made by the connector, e.g. for a contact it may search `email` and `alternativeEmails`.
|
|
239
|
-
- **domain[ci]** - inspect the `match.domain` values and search for rows
|
|
239
|
+
- **domain[ci]** - inspect the `match.domain` values and search for rows **at the host passed**. The determination of which field(s) holds the domain is made by the connector, e.g. for a company it may search `website`. Do not reinterpret the value: passed `software.example.com`, a row at `example.com` is not a match.
|
|
240
|
+
- What arrives: a sync cleans the source value before sending it. The user chooses how far on the rule: the company domain (the default: `shop.acme.co.uk` arrives as `acme.co.uk`), the full host (`shop.acme.co.uk`: only scheme, `www`, port and path are dropped), or the raw source value. The connector is not told which. In the first two, values that are not domains, or that are a page or subdomain on a shared host such as `facebook.com/acme`, are not sent; a shared host's own domain (`facebook.com`) is, because it belongs to that company. An internationalized domain arrives in the form the source wrote it (`münchen.de` or `xn--mnchen-3ya.de`).
|
|
241
|
+
- Field holds bare domains (an identity, like a CRM company domain), API offers equality: an equality filter; nothing else to do.
|
|
242
|
+
- Field holds website addresses, API offers contains or `like`: fetch candidates by contains, then keep the right ones with `domainCompare(storedValue, match.domain)`. It returns `"host"` when the stored value is at exactly the host sought, `"company"` when the domain sought has no subdomain and the stored value is on a subdomain of it (`https://portal.acme.com/login` for `acme.com`), and `undefined` otherwise, including for a stored page on a shared host. **If any candidate is `"host"`, pass only those to `canUse`; otherwise you may pass the `"company"` ones.** The check is required: contains over-matches (`acme.com` is contained in `notacme.com` and `acme.com.au`, and `facebook.com` in every `facebook.com/<page>`).
|
|
243
|
+
- Field holds website addresses, API offers equality with `IN` / `OR`: query the common variants (`acme.com`, `www.acme.com`, `http(s)://[www.]acme.com`, each with and without a trailing `/`), then keep the right ones as above. This misses deep paths; say so in the connector notes.
|
|
244
|
+
- Field holds website addresses, API offers single-value equality only: **do not declare `domain[ci]`**.
|
|
240
245
|
- **first_and_last_name[ci]** - inspect the `match.firstname` and values and `match.lastname` and search for rows with the same name.
|
|
241
246
|
- **id** - inspect the `match.id` value and search in the selected `destFieldPath` (path to the field on the target system) to find rows holding this exact value in this field. This search is case sensitive.
|
|
242
247
|
- **field_value_equals[ci]** - inspect the `match.custom` value and search in the selected `destFieldPath` (path to the field on the target system) to find rows holding this value (case insensitive) in this field.
|
package/docs/09-testing.md
CHANGED
|
@@ -122,7 +122,13 @@ To disable those tests use `cannotTestReason` instead
|
|
|
122
122
|
- `checkpointFilterStep1Prepare` / `Step2Prepare` — the two-step round-trip test of your
|
|
123
123
|
differential contract; see [Checkpoint testing](#checkpoint-testing-what-the-two-steps-prove)
|
|
124
124
|
below, it is the most-misimplemented pair.
|
|
125
|
-
- `matchFilterPrepare` — returns `MatchData` including `expectedMatchRowIds`.
|
|
125
|
+
- `matchFilterPrepare` — returns `MatchData` including `expectedMatchRowIds`. For
|
|
126
|
+
`domain[ci]` the harness does not go through a sync, so send `match.domain` already cleansed
|
|
127
|
+
(`API.utilities.domainCleanse`, e.g. `acme.com`), exactly as a sync would. When the object's
|
|
128
|
+
domain field is a website field, store a URL-form value on the test row (e.g.
|
|
129
|
+
`https://www.acme.com/about`) so the test proves the connector matches on the host rather
|
|
130
|
+
than on the raw text, and add a case that sends a host with a subdomain
|
|
131
|
+
(`software.example.com`) and expects a row at `example.com` not to match.
|
|
126
132
|
- `relatedFilterPrepare({ fromObjectMeta, toObjectMeta, fromRelationshipId })` —
|
|
127
133
|
`fromObjectMeta` is the object where the relationship is DECLARED (the parent, in the
|
|
128
134
|
parent-owns-children shape), `toObjectMeta` is its `relObjectId`. Return
|
|
@@ -135,6 +141,17 @@ To disable those tests use `cannotTestReason` instead
|
|
|
135
141
|
contract under "What each test needs" below. Use `withIssueSimulators` /
|
|
136
142
|
`UpsertIssueSimulator` (`verifyFields`, expected issue `type` + `fatal`) to deliberately
|
|
137
143
|
exercise your `upsertClean` issue reporting.
|
|
144
|
+
- **File fields in `upsertPrepare`.** When the object has an upsert field of `type: "file"`,
|
|
145
|
+
every `upsertPrepare` call (upsert, delete and upsertClean tests) carries `withFileField`
|
|
146
|
+
set to its path (a field with `constraints: { mandatory: "add" }` is preferred, else the
|
|
147
|
+
first file field). Put an `SDK.FileProvider` at that path in `insert` (and `update`), and
|
|
148
|
+
return a **fresh provider on every call**: the harness passes the connector a clone
|
|
149
|
+
(`SDK.utilities.clone(row, { withFileProviders: true })`), so the connector may close what
|
|
150
|
+
it is handed, and the harness closes your originals and its clones once the test has
|
|
151
|
+
verified them ([13-file-fields.md](./13-file-fields.md)). A file field listed in `verifyFields` is
|
|
152
|
+
compared by bytes with `FileProvider.equals()`: the query-back must return a provider with
|
|
153
|
+
the same content. The harness never adds the file field to the query-back on its own,
|
|
154
|
+
since that downloads the content; list it in `verifyFields` or `fields` to opt in.
|
|
138
155
|
- delete features support soft-delete verification — `isDeleted: { path, valueWhenDeleted }`
|
|
139
156
|
checks a flag field instead of row absence, and `afterDeleteMaxIndexWaitTimeMs` tolerates
|
|
140
157
|
APIs whose deletions surface asynchronously.
|
|
@@ -119,7 +119,9 @@
|
|
|
119
119
|
The two you will use constantly (they appear in nearly every fleet connector):
|
|
120
120
|
|
|
121
121
|
- `SDK.utilities.clone(value)` — deep clone. Use before mutating anything you received or
|
|
122
|
-
return (metadata, rows, options) — shared references are how callers get corrupted.
|
|
122
|
+
return (metadata, rows, options) — shared references are how callers get corrupted. A
|
|
123
|
+
`FileProvider` in the value becomes an unreadable shim unless you pass
|
|
124
|
+
`{ withFileProviders: true }` ([13-file-fields.md](./13-file-fields.md)).
|
|
123
125
|
- `SDK.utilities.tryGet(() => deeply.nested.maybe.missing)` — returns the value or
|
|
124
126
|
`undefined`, never throws. The idiom for prodding uncertain API payloads:
|
|
125
127
|
`const sig = tryGet(() => options.payload.headers["x-signature"][0]);`
|
|
@@ -132,6 +134,14 @@ Frequently used:
|
|
|
132
134
|
settings values ("a, b,c" → `["a","b","c"]` / a `Set`).
|
|
133
135
|
- `emailCleanse(email)` / `emailIsPublic` / `domainParse` / `nameParse` — normalize match-key
|
|
134
136
|
values before comparing (pairs with `matchRules`).
|
|
137
|
+
- `domainCompare(storedValue, matchDomain)` — the check for `domain[ci]` candidates: `"host"`,
|
|
138
|
+
`"company"` or `undefined` ([05-query.md](./05-query.md)). It ignores case and the unicode or
|
|
139
|
+
punycode form of an internationalized name, and rejects a stored page on a shared host.
|
|
140
|
+
- `domainCleanse(value, level?)` / `domainIsShared(value)` — reduce a hostname or URL to its
|
|
141
|
+
lowercased company domain (`https://www.shop.acme.co.uk/x` → `acme.co.uk`) or, with
|
|
142
|
+
`"full_host"`, to its host without `www` (`shop.acme.co.uk`); `undefined` when not a domain.
|
|
143
|
+
`domainIsShared` spots a page or subdomain on a shared host (`facebook.com/acme`,
|
|
144
|
+
`sites.google.com/view/acme`; not the host's own `facebook.com`). Both take the original value.
|
|
135
145
|
- `xmlParse({ source: { string | fileProvider, encoding? } })` — preferred full-document XML
|
|
136
146
|
parse. Deprecated: `xmlParse({ string })` (still supported).
|
|
137
147
|
- `xmlParse({ source, emit, skip? })` — stream matching elements as xml2js-shaped chunks (XPath
|
package/docs/13-file-fields.md
CHANGED
|
@@ -82,10 +82,34 @@ SDK.utilities.fileProvider({ file: { path: tempPath, deleteOnClose: true } });
|
|
|
82
82
|
| `save(path?)` | persist to disk, returns the path (pair with `SDK.utilities.tempFile`) |
|
|
83
83
|
| `length()` | size in bytes (e.g. for a `size` field or Content-Length header) |
|
|
84
84
|
| `close()` | release the resource — **always, in a `finally`** |
|
|
85
|
+
| `clone()` | an independent provider over the same bytes; close it like any other |
|
|
86
|
+
| `equals(other)` | `true` when both hold the same bytes; `false` if either side is closed |
|
|
85
87
|
|
|
86
88
|
Parsing structured file content: `SDK.utilities.csvReader({ source: { fileProvider } })`
|
|
87
89
|
(same idea for `xlsxReader`).
|
|
88
90
|
|
|
91
|
+
## Cloning and comparing
|
|
92
|
+
|
|
93
|
+
`clone()` returns an independent provider: closing the original does not close the clone, and
|
|
94
|
+
the clone **must be closed too**. A clone of a temp-file provider (`deleteOnClose`) shares the
|
|
95
|
+
file by reference count, so the file is removed only when the last holder closes. A provider
|
|
96
|
+
dropped without `close()` is released when the runtime collects it, which may be much later, so
|
|
97
|
+
closing stays the rule. Cloning a closed provider throws `ResourceAlreadyClosed`.
|
|
98
|
+
|
|
99
|
+
Deep-copying a row that holds a file:
|
|
100
|
+
|
|
101
|
+
- `SDK.utilities.clone(row)` copies the data and replaces each provider with a **shim**. The
|
|
102
|
+
shim serialises like a provider (`"[object FileProvider]"`) and needs no close, but every read
|
|
103
|
+
(`stream()`, `length()`, `save()`, `blob()`, `formDataValue()`, `clone()`, `equals()`) throws
|
|
104
|
+
`SCRIPT_FILE_PROVIDER_SHALLOW_CLONE`. That is what a snapshot for logging wants.
|
|
105
|
+
- `SDK.utilities.clone(row, { withFileProviders: true })` clones each provider with `clone()`,
|
|
106
|
+
so the copy can read the files. The code that made the copy owns its providers and must
|
|
107
|
+
close them.
|
|
108
|
+
|
|
109
|
+
`equals(other)` compares bytes: lengths first, then both streams chunk by chunk, without
|
|
110
|
+
reading whole files into memory. It resolves `false` rather than throwing when either side has
|
|
111
|
+
been closed.
|
|
112
|
+
|
|
89
113
|
## Upserting: the file arrives as a field value
|
|
90
114
|
|
|
91
115
|
The upsert row carries a `FileProvider` at the file field's position. Stream it out and close
|
|
@@ -121,3 +145,9 @@ async upsert(options) {
|
|
|
121
145
|
3. **Close what you open.** Providers can be backed by temp files; `close()` in a `finally`
|
|
122
146
|
on both the query and upsert paths.
|
|
123
147
|
|
|
148
|
+
## Testing
|
|
149
|
+
|
|
150
|
+
The connector test harness exercises file fields through the upsert, delete and upsertClean
|
|
151
|
+
tests: it asks `upsertPrepare` for a file at `withFileField`, passes the connector a clone, and compares a file
|
|
152
|
+
field listed in `verifyFields` by bytes. See
|
|
153
|
+
[09-testing.md](./09-testing.md#preparecleanup-hooks-and-their-data-types).
|
|
@@ -7,5 +7,7 @@ export declare class FileProviderBuffer implements FileProvider {
|
|
|
7
7
|
blob(fileName?: string): Promise<Blob>;
|
|
8
8
|
save(path?: string): Promise<string>;
|
|
9
9
|
close(): Promise<void>;
|
|
10
|
+
clone(): FileProvider;
|
|
11
|
+
equals(other: FileProvider): Promise<boolean>;
|
|
10
12
|
toJSON(): string;
|
|
11
13
|
}
|
|
@@ -7,5 +7,7 @@ export declare class FileProviderDisk implements FileProvider {
|
|
|
7
7
|
blob(fileName?: string): Promise<Blob>;
|
|
8
8
|
save(path?: string): Promise<string>;
|
|
9
9
|
close(): Promise<void>;
|
|
10
|
+
clone(): FileProvider;
|
|
11
|
+
equals(other: FileProvider): Promise<boolean>;
|
|
10
12
|
toJSON(): string;
|
|
11
13
|
}
|
|
@@ -7,5 +7,7 @@ export declare class FileProviderString implements FileProvider {
|
|
|
7
7
|
blob(fileName?: string): Promise<Blob>;
|
|
8
8
|
save(path?: string): Promise<string>;
|
|
9
9
|
close(): Promise<void>;
|
|
10
|
+
clone(): FileProvider;
|
|
11
|
+
equals(other: FileProvider): Promise<boolean>;
|
|
10
12
|
toJSON(): string;
|
|
11
13
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncmatters/connector-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.22",
|
|
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": "35909f8b0b17e27f3d540dfe193fd4802c3c595e646e9a64828fd91d49605500",
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@types/node": "*",
|
|
18
|
-
"@syncmatters/script-api": "^1.0.
|
|
18
|
+
"@syncmatters/script-api": "^1.0.25"
|
|
19
19
|
}
|
|
20
20
|
}
|