@syncmatters/connector-sdk 1.0.18 → 1.0.19
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/09-testing.md +165 -1
- package/docs/15-example-test-file.md +56 -4
- package/docs/connector-authoring.md +2 -2
- package/package.json +3 -3
package/docs/09-testing.md
CHANGED
|
@@ -34,6 +34,7 @@ export const objectFeatures = {
|
|
|
34
34
|
matchRules: [{ rule: "email[ci]" }],
|
|
35
35
|
canMatchFields: toJsonPaths(["id", "email", "name"]),
|
|
36
36
|
relationshipsTo: [{ id: "companies-companyId", relObjectId: "companies" }],
|
|
37
|
+
key: { path: toJsonPath("id") }, // the query field whose value becomes row.meta.key
|
|
37
38
|
},
|
|
38
39
|
upsert: { canUpsert: true, key: { path: toJsonPath("id") }, canUpsertClean: true },
|
|
39
40
|
delete: { canDelete: true, key: { path: toJsonPath("id") } },
|
|
@@ -111,7 +112,8 @@ To disable those tests use `cannotTestReason` instead
|
|
|
111
112
|
- `listPrepare` / `idsFilterPrepare` — arrange rows and return what the suite should expect.
|
|
112
113
|
(`rowFiltersPrepare` exists in the contract but the suite does NOT run rowFilter tests yet —
|
|
113
114
|
when `meta()` exposes `queryFilter.rowFilter`, cover it yourself with a direct query in a
|
|
114
|
-
hook you already implement.
|
|
115
|
+
hook you already implement. `RowFilterData` accepts an `expect` clause like every other
|
|
116
|
+
feature's data, but it is not evaluated until the row-filter feature ships.)
|
|
115
117
|
- `checkpointFilterStep1Prepare` / `Step2Prepare` — the two-step round-trip test of your
|
|
116
118
|
differential contract; see [Checkpoint testing](#checkpoint-testing-what-the-two-steps-prove)
|
|
117
119
|
below, it is the most-misimplemented pair.
|
|
@@ -132,6 +134,168 @@ To disable those tests use `cannotTestReason` instead
|
|
|
132
134
|
checks a flag field instead of row absence, and `afterDeleteMaxIndexWaitTimeMs` tolerates
|
|
133
135
|
APIs whose deletions surface asynchronously.
|
|
134
136
|
|
|
137
|
+
## Expected values and errors: the `expect` clause
|
|
138
|
+
|
|
139
|
+
The data every prepare hook returns (`ListData`, `IdsFilterData`, `CheckpointStep1Data`,
|
|
140
|
+
`CheckpointStep2Data`, `MatchData`, `RelationshipData`) takes an optional `expect`, for what a
|
|
141
|
+
row-id comparison cannot prove: that a value comes back in the right form, or that a call fails
|
|
142
|
+
the way it must.
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
/** @type {SDKTest.ListData} */
|
|
146
|
+
const listData = {
|
|
147
|
+
pageSize: 2,
|
|
148
|
+
expect: {
|
|
149
|
+
rows: {
|
|
150
|
+
// "*" applies to every row the list returns
|
|
151
|
+
"*": [
|
|
152
|
+
{ path: toJsonPath("RowVer"), matches: "^[0-9a-f]{16}$" }, // rowversion as 16 hex chars
|
|
153
|
+
{ path: toJsonPath("Guid"), matches: "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$" },
|
|
154
|
+
{ path: toJsonPath("Never_Set"), absent: true }, // a column the connector must not echo
|
|
155
|
+
],
|
|
156
|
+
// a row's own meta.key: these win over "*" on the same path
|
|
157
|
+
[alphaKey]: [{ path: toJsonPath("Amount"), equals: 10.5 }],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/** @type {SDKTest.IdsFilterData} */
|
|
163
|
+
const idsFilterData = {
|
|
164
|
+
id1: "not-a-number",
|
|
165
|
+
id2: "also-not",
|
|
166
|
+
// the connector must reject these ids; success is a failure of the test
|
|
167
|
+
expect: { error: { code: "SCRIPT_INVALID_INDEX_FILTER_VALUE", messagePattern: "not an integer" } },
|
|
168
|
+
};
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**`rows`** maps a row's `meta.key` (or `"*"` for every row) to assertions on `row.data` by path.
|
|
172
|
+
Each assertion is one of:
|
|
173
|
+
|
|
174
|
+
| Matcher | Passes when |
|
|
175
|
+
| ---------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
176
|
+
| `equals: value` | the value equals, compared per the field's declared type (below) |
|
|
177
|
+
| `matches: "regex"` | the value (as a string) matches the regex source; for hex, GUIDs and formats |
|
|
178
|
+
| `approx: { value, tolerance }` | the value is a number within `tolerance` of `value` |
|
|
179
|
+
| `present: true` | the path is in the row and its value is not `null` |
|
|
180
|
+
| `absent: true` | the path is not in the row at all; a `null` value is present and fails |
|
|
181
|
+
|
|
182
|
+
`equals` compares by the declared type of the field at the path in the object's
|
|
183
|
+
`queryFields`:
|
|
184
|
+
|
|
185
|
+
- `string` exactly (`"12"` does not equal `12`), `number` numerically (`"20.250"` equals
|
|
186
|
+
`20.25`, so a decimal's scale does not matter), `boolean` strictly, `object` and `array`
|
|
187
|
+
structurally (key order ignored, element order kept).
|
|
188
|
+
- A date: a field with `constraints.dt`, or a `format` naming a date (`date`, `date-time`,
|
|
189
|
+
`datetime`, `iso8601`, `unix`, `unix_ms`, a `yyyy-MM-dd`-style pattern), compares as an
|
|
190
|
+
instant at the precision its format implies: days for date-only formats, seconds for
|
|
191
|
+
`unix` and `yyyy-MM-dd HH:mm:ss`, milliseconds otherwise. So `"2026-09-27T10:00:00+02:00"`
|
|
192
|
+
equals `"2026-09-27T08:00:00Z"`.
|
|
193
|
+
- A path with no declared field compares structurally.
|
|
194
|
+
|
|
195
|
+
The details say which comparison each assertion used (`number`, `instant (seconds)`,
|
|
196
|
+
`structural (no declared field)`...).
|
|
197
|
+
|
|
198
|
+
A keyed row that the query did not return is a mismatch. The list reads one page plus one row
|
|
199
|
+
(`pageSize + 1`), so key only rows inside that read, or use `"*"`. If you pass `fields`, include
|
|
200
|
+
the paths you assert on — except `absent` ones. The query only returns what it asked for.
|
|
201
|
+
|
|
202
|
+
**`error`** makes the feature pass only when the operation throws, and throws like this: `code`
|
|
203
|
+
must equal the error's `code` (a `ConnectorError`/`ScriptError` code), and `messagePattern` is a
|
|
204
|
+
regex over its message. With neither, any error passes. The actual error — or, when the call
|
|
205
|
+
succeeded, the rows it returned — is recorded in the details either way. What must fail:
|
|
206
|
+
|
|
207
|
+
| Data | The operation `expect.error` applies to |
|
|
208
|
+
| --------------------- | ----------------------------------------------------------------------------------------------- |
|
|
209
|
+
| `ListData` | the list query |
|
|
210
|
+
| `IdsFilterData` | one query for `[id1, id2]`, which replaces the usual three. `id1`/`id2` must come from the hook; the harness does not look up ids from a list for an expected error |
|
|
211
|
+
| `CheckpointStep1Data` | the step 1 query. Step 2 is then skipped, since there is no emitted checkpoint |
|
|
212
|
+
| `CheckpointStep2Data` | the step 2 query, from the checkpoint step 1 emitted |
|
|
213
|
+
| `MatchData` | that match query |
|
|
214
|
+
| `RelationshipData` | the related query (the 'from' row is still fetched by `idsFilter` and must exist) |
|
|
215
|
+
|
|
216
|
+
`expectedRowIds` / `expectedMatchRowIds` / `expectedRelatedRowIds` may be empty when `error` is
|
|
217
|
+
set.
|
|
218
|
+
|
|
219
|
+
**Upsert**: `UpsertData.expect` is `{ insert?, update? }`, each an assertion list for this
|
|
220
|
+
data's own row as queried back after that phase. `upsertPrepare` runs once per row, and the
|
|
221
|
+
row's key is only assigned by the insert, so there is nothing to key by. The asserted paths are
|
|
222
|
+
added to the query-back's fields for you. `verifyFields` stays the shorthand for "what I wrote
|
|
223
|
+
comes back". Use `expect` for what the test did NOT write: identity and computed columns,
|
|
224
|
+
defaults, server timestamps, rowversions. For example, `update:
|
|
225
|
+
[{ path: toJsonPath("RowVer"), matches: ... }]` checks a value the update moved. There is no
|
|
226
|
+
`error` on upsert: `issueSimulators` already express an upsert that must fail. `update` is not
|
|
227
|
+
run for `insertOnly` objects, and the delete test (which reuses `upsertPrepare`) ignores
|
|
228
|
+
`expect`.
|
|
229
|
+
|
|
230
|
+
Failures land in the feature's existing status and details:
|
|
231
|
+
|
|
232
|
+
- **`Failed: Expected values did not match`**, with one error detail per assertion that did not
|
|
233
|
+
hold: the row key, the path, the expected form, the actual value and how they were
|
|
234
|
+
compared. Oversized values are truncated under the usual caps, not dropped.
|
|
235
|
+
- **`Failed: Expected error did not occur`**, when the call succeeded or failed differently.
|
|
236
|
+
|
|
237
|
+
Match rules and relationships report these on the rule or relationship result, and roll up as
|
|
238
|
+
`One or more rules/filters have issues`.
|
|
239
|
+
|
|
240
|
+
Compute expectations at prepare time from data your suite seeded (`before()` or the hook
|
|
241
|
+
itself), never by reading the connector under test. Asserting a returned value's shape is also
|
|
242
|
+
the right test of a type mapping: it proves the connector's behaviour, where a metadata check
|
|
243
|
+
would only prove its declaration.
|
|
244
|
+
|
|
245
|
+
## `query.key`: the rows' key is the field tagged as the key
|
|
246
|
+
|
|
247
|
+
[04](./04-meta-objects-fields.md) makes `isKey` a contract: the tagged query field's value
|
|
248
|
+
becomes `row.meta.key`, and exactly one query field carries `isKey` (or `isUserKey`). The
|
|
249
|
+
generator declares that field as `query.key { path, expectUserKey? }`, found the same way as
|
|
250
|
+
`upsert.key`. Regenerate `mod ObjectTestFeatures.mjs` to pick it up. A hand-authored spec can
|
|
251
|
+
declare it too. When it is declared:
|
|
252
|
+
|
|
253
|
+
- the **fieldMetadata** feature checks that the field at `path` is tagged `isKey` (`isUserKey`
|
|
254
|
+
when `expectUserKey`) and that no other query field carries either tag (`More than one query
|
|
255
|
+
field tagged with isKey or isUserKey`);
|
|
256
|
+
- the **list** and **idsFilter** features check that every returned row's `meta.key` equals
|
|
257
|
+
`String()` of that field's value (for a user key, of its `value`). The first mismatch fails
|
|
258
|
+
the feature as `Failed: Expected values did not match`, with a detail that says it is the key
|
|
259
|
+
check (`key check (query.key, not a value assertion)`). A query that names its `fields` also
|
|
260
|
+
gets the key path added.
|
|
261
|
+
|
|
262
|
+
This catches a connector that keys its rows on one column and tags another, which otherwise
|
|
263
|
+
only a sync finds. A spec without `query.key` (anything generated before it existed) skips
|
|
264
|
+
these checks.
|
|
265
|
+
|
|
266
|
+
## Fixtures on two-phase connections: fixed names, seeded in `before()`
|
|
267
|
+
|
|
268
|
+
A two-phase test connection collects only the objects it selected. So a fixture object — a
|
|
269
|
+
sandbox table, a custom object — must exist before discovery and be selected before the run.
|
|
270
|
+
**`before()` seeds rows, never objects.** Creating a table in `before()` cannot work: the run's
|
|
271
|
+
refresh covers only the selection, and the harness checks every object to test against the
|
|
272
|
+
connection right after it.
|
|
273
|
+
|
|
274
|
+
The procedure:
|
|
275
|
+
|
|
276
|
+
1. Create the fixtures once, with fixed names (the SQLServer suite's `dbo.sm_test_*` pattern).
|
|
277
|
+
2. Run `sm test --only discover`.
|
|
278
|
+
3. Select the fixtures on the test connection.
|
|
279
|
+
4. Run the suite.
|
|
280
|
+
|
|
281
|
+
A create-a-fresh-table-per-run pattern (`SMTST<random>`) cannot be ported to the harness.
|
|
282
|
+
|
|
283
|
+
`before()` already has the connection and runs before the harness's own object check. A suite
|
|
284
|
+
that wants a clearer message than "objects not on the connection" checks its own fixtures
|
|
285
|
+
there:
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
async before({ connection }) {
|
|
289
|
+
const missing = FIXTURES.filter((id) => !connection.objectIds.includes(id));
|
|
290
|
+
if (missing.length > 0) {
|
|
291
|
+
throw new Error(`fixtures not on the connection: ${missing.join(", ")} - create them, run --only discover, select them, run again`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
A thrown `before()` is the run's fatal error, message included, so this fails the run before
|
|
297
|
+
any feature runs.
|
|
298
|
+
|
|
135
299
|
## Checkpoint testing: what the two steps prove
|
|
136
300
|
|
|
137
301
|
The checkpoint contract is a ROUND-TRIP ([05](./05-query.md)): the platform stores whatever
|
|
@@ -71,7 +71,10 @@ export const objectFeatures = {
|
|
|
71
71
|
idsFilter: true,
|
|
72
72
|
checkpointFilter: true,
|
|
73
73
|
matchRules: [{"rule":"id"},{"rule":"name[ci]"}],
|
|
74
|
-
canMatchFields: toJsonPaths(["id", "name", "domain"])
|
|
74
|
+
canMatchFields: toJsonPaths(["id", "name", "domain"]),
|
|
75
|
+
key: {
|
|
76
|
+
path: toJsonPath("id")
|
|
77
|
+
}
|
|
75
78
|
},
|
|
76
79
|
upsert: {
|
|
77
80
|
canUpsert: true,
|
|
@@ -94,7 +97,10 @@ export const objectFeatures = {
|
|
|
94
97
|
checkpointFilter: true,
|
|
95
98
|
matchRules: [{"rule":"id"},{"rule":"email[ci]"},{"rule":"first_and_last_name[ci]"}],
|
|
96
99
|
canMatchFields: toJsonPaths(["id", "email", "first_name", "last_name"]),
|
|
97
|
-
relationshipsTo: [{"id":"companies-company_id","relObjectId":"companies"}]
|
|
100
|
+
relationshipsTo: [{"id":"companies-company_id","relObjectId":"companies"}],
|
|
101
|
+
key: {
|
|
102
|
+
path: toJsonPath("id")
|
|
103
|
+
}
|
|
98
104
|
},
|
|
99
105
|
upsert: {
|
|
100
106
|
canUpsert: true,
|
|
@@ -114,7 +120,10 @@ export const objectFeatures = {
|
|
|
114
120
|
salesorders: {
|
|
115
121
|
query: {
|
|
116
122
|
list: true,
|
|
117
|
-
idsFilter: true
|
|
123
|
+
idsFilter: true,
|
|
124
|
+
key: {
|
|
125
|
+
path: toJsonPath("$compositeKey")
|
|
126
|
+
}
|
|
118
127
|
},
|
|
119
128
|
upsert: {
|
|
120
129
|
canUpsert: true,
|
|
@@ -227,7 +236,17 @@ export default class TestFeatureProvider {
|
|
|
227
236
|
// a small pageSize forces real pagination through Acme's cursor (05)
|
|
228
237
|
switch (options.meta.id) {
|
|
229
238
|
case "salesorders":
|
|
230
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
pageSize: 25,
|
|
241
|
+
fields: [[{ path: "$compositeKey" }], [{ path: "status" }]],
|
|
242
|
+
// expected values (09): "*" asserts on every row the list returns - here the key
|
|
243
|
+
// encoding (11), sorted JSON with the parent part first. No row-id bookkeeping needed.
|
|
244
|
+
expect: {
|
|
245
|
+
rows: {
|
|
246
|
+
"*": [{ path: [{ path: "$compositeKey" }], matches: '^\\{"business_unit":".+","order_id":\\d+\\}$' }],
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
231
250
|
default:
|
|
232
251
|
return { pageSize: 25 };
|
|
233
252
|
}
|
|
@@ -407,6 +426,14 @@ export default class TestFeatureProvider {
|
|
|
407
426
|
// the payload it just sent - so every verify field must be present (non-null) in
|
|
408
427
|
// BOTH insert and update, and the update should carry a CHANGED value (09)
|
|
409
428
|
result.verifyFields.push([{ path: "domain" }]);
|
|
429
|
+
// ...and expect covers what the test did NOT write: Acme assigns id and created_at.
|
|
430
|
+
// The row's key is unknown until the insert, so these name the phase, not a key (09)
|
|
431
|
+
result.expect = {
|
|
432
|
+
insert: [
|
|
433
|
+
{ path: [{ path: "id" }], matches: "^\\d+$" },
|
|
434
|
+
{ path: [{ path: "created_at" }], present: true },
|
|
435
|
+
],
|
|
436
|
+
};
|
|
410
437
|
break;
|
|
411
438
|
}
|
|
412
439
|
case "contacts": {
|
|
@@ -546,6 +573,23 @@ the connector's doing. Widen the test connection's selection rather than scoping
|
|
|
546
573
|
`sm test --only discover` ([09](./09-testing.md#running-the-suite)) checks the catalogue against
|
|
547
574
|
the selection and prints what is missing.
|
|
548
575
|
|
|
576
|
+
The same rule applies to fixture objects a suite owns, such as a sandbox table or a custom
|
|
577
|
+
object. Create them once with fixed names, discover, select them, and have `before()` seed rows
|
|
578
|
+
only, never objects ([09](./09-testing.md#fixtures-on-two-phase-connections-fixed-names-seeded-in-before)).
|
|
579
|
+
A suite that wants its own message checks the fixtures first:
|
|
580
|
+
|
|
581
|
+
```js
|
|
582
|
+
const FIXTURES = ["companies", "contacts", "salesorders"];
|
|
583
|
+
|
|
584
|
+
async before({ connection }) {
|
|
585
|
+
const missing = FIXTURES.filter((id) => !connection.objectIds.includes(id));
|
|
586
|
+
if (missing.length > 0) {
|
|
587
|
+
throw new Error(`fixtures not on the connection: ${missing.join(", ")} - create them, run --only discover, select them, run again`);
|
|
588
|
+
}
|
|
589
|
+
// ...then seed rows as above
|
|
590
|
+
}
|
|
591
|
+
```
|
|
592
|
+
|
|
549
593
|
## What to notice (the parts assistants most often get wrong)
|
|
550
594
|
|
|
551
595
|
1. **Every created record is prefixed** (`sm_test`) and `before()`/`after()` sweep the
|
|
@@ -581,3 +625,11 @@ the selection and prints what is missing.
|
|
|
581
625
|
the differential window into `expectedRowIds` before seeding one guaranteed change, and
|
|
582
626
|
step 2 expects step-1 rows to reappear because the connector's checkpoint deliberately
|
|
583
627
|
overlaps (05, 12).
|
|
628
|
+
9. **Row ids prove which rows came back; `expect` proves what is in them.** Value fidelity
|
|
629
|
+
(a key encoding, a server-assigned id or timestamp, a column that must not be echoed) goes
|
|
630
|
+
in the prepare data's `expect` clause, computed from data the suite seeded. A call that must
|
|
631
|
+
fail (ids the API rejects, an invalid checkpoint) is `expect.error` on that feature's data.
|
|
632
|
+
`equals` compares by the field's declared type, and a date compares as an instant (09).
|
|
633
|
+
10. **`query.key` is generated, like `upsert.key`.** It names the field whose value IS
|
|
634
|
+
`row.meta.key`, and the list and idsFilter tests check every row against it. If the
|
|
635
|
+
generated file lacks it, it predates the check: regenerate.
|
|
@@ -28,13 +28,13 @@ queryable.
|
|
|
28
28
|
| [06-upsert-delete.md](./06-upsert-delete.md) | implementing `upsert()`, `delete()`, `upsertClean()`, `upsertFieldOptions()`, batching |
|
|
29
29
|
| [07-events-webhooks.md](./07-events-webhooks.md) | webhooks: `parseEvents()`, signature verification (and the usually-skipped `handleParsedEvent()`) |
|
|
30
30
|
| [08-errors.md](./08-errors.md) | error handling — `ConnectorError`, `ErrorCode` selection, `HttpError` |
|
|
31
|
-
| [09-testing.md](./09-testing.md) | the test harness: `mod Test.mjs` + `mod ObjectTestFeatures.mjs`
|
|
31
|
+
| [09-testing.md](./09-testing.md) | the test harness: `mod Test.mjs` + `mod ObjectTestFeatures.mjs`, expected values/errors (`expect`), `query.key`, fixtures |
|
|
32
32
|
| [10-style-and-pitfalls.md](./10-style-and-pitfalls.md) | style rules, deprecated APIs to avoid, known traps — skim this once regardless |
|
|
33
33
|
| [11-composite-keys.md](./11-composite-keys.md) | objects with multi-part identity (child REST resources, junction rows) — key encoding across query/upsert/delete |
|
|
34
34
|
| [12-example-connector.md](./12-example-connector.md) | **the canonical worked connector** — a complete three-object connector (relationships, matching, composite keys, custom fields, upsertClean, query/upsert/delete) on one page; its test-file counterpart is 15 |
|
|
35
35
|
| [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`) |
|
|
36
36
|
| [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 |
|
|
37
|
-
| [15-example-test-file.md](./15-example-test-file.md) | **the canonical worked test file** — the complete `mod Test.mjs` + `mod ObjectTestFeatures.mjs` pair for the Acme connector of 12 (TestFeatureProvider, prepare/cleanup hooks, issue simulators, feature scoping) |
|
|
37
|
+
| [15-example-test-file.md](./15-example-test-file.md) | **the canonical worked test file** — the complete `mod Test.mjs` + `mod ObjectTestFeatures.mjs` pair for the Acme connector of 12 (TestFeatureProvider, prepare/cleanup hooks, issue simulators, expected values, feature scoping) |
|
|
38
38
|
| [16-the-end-game-syncs.md](./16-the-end-game-syncs.md) | **why any of this exists** — how the sync pipeline consumes each connector capability, and what silently degrades when one is missing. Read once before designing `meta()` |
|
|
39
39
|
| [17-user-help-pages.md](./17-user-help-pages.md) | writing the connector's customer help pages — they ship IN the connector (`Connectors/<Name>/help/*.md`, sidecar type `"markdown"`); the house structure, frontmatter, and example conventions |
|
|
40
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncmatters/connector-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
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": "acd8d8f87e1b337a1deddca006f59a875fd740b920d602ed59b3a3775a862332",
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@types/node": "*",
|
|
18
|
-
"@syncmatters/script-api": "^1.0.
|
|
18
|
+
"@syncmatters/script-api": "^1.0.20"
|
|
19
19
|
}
|
|
20
20
|
}
|