@syncmatters/connector-sdk 1.0.20 → 1.0.21

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.
@@ -135,6 +135,17 @@ To disable those tests use `cannotTestReason` instead
135
135
  contract under "What each test needs" below. Use `withIssueSimulators` /
136
136
  `UpsertIssueSimulator` (`verifyFields`, expected issue `type` + `fatal`) to deliberately
137
137
  exercise your `upsertClean` issue reporting.
138
+ - **File fields in `upsertPrepare`.** When the object has an upsert field of `type: "file"`,
139
+ every `upsertPrepare` call (upsert, delete and upsertClean tests) carries `withFileField`
140
+ set to its path (a field with `constraints: { mandatory: "add" }` is preferred, else the
141
+ first file field). Put an `SDK.FileProvider` at that path in `insert` (and `update`), and
142
+ return a **fresh provider on every call**: the harness passes the connector a clone
143
+ (`SDK.utilities.clone(row, { withFileProviders: true })`), so the connector may close what
144
+ it is handed, and the harness closes your originals and its clones once the test has
145
+ verified them ([13-file-fields.md](./13-file-fields.md)). A file field listed in `verifyFields` is
146
+ compared by bytes with `FileProvider.equals()`: the query-back must return a provider with
147
+ the same content. The harness never adds the file field to the query-back on its own,
148
+ since that downloads the content; list it in `verifyFields` or `fields` to opt in.
138
149
  - delete features support soft-delete verification — `isDeleted: { path, valueWhenDeleted }`
139
150
  checks a flag field instead of row absence, and `afterDeleteMaxIndexWaitTimeMs` tolerates
140
151
  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]);`
@@ -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.21",
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": "cbce2eec280ba657cbe2b0aa321b46b3578ab203622d95a3030beb8e5180cd78",
16
16
  "dependencies": {
17
17
  "@types/node": "*",
18
- "@syncmatters/script-api": "^1.0.21"
18
+ "@syncmatters/script-api": "^1.0.23"
19
19
  }
20
20
  }