@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.
@@ -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 with the same `domain`. The determination of which field(s) holds email is made by the connector, e.g. for a comapny it may search `website`.
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.
@@ -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
@@ -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.20",
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": "376c97b772af51a69f5155aff7c80362062a74710e0672b3dd0582679b5cfffb",
15
+ "typesContentHash": "35909f8b0b17e27f3d540dfe193fd4802c3c595e646e9a64828fd91d49605500",
16
16
  "dependencies": {
17
17
  "@types/node": "*",
18
- "@syncmatters/script-api": "^1.0.21"
18
+ "@syncmatters/script-api": "^1.0.25"
19
19
  }
20
20
  }