zitejs 0.9.129 → 0.9.131
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/dist/cjs/integrations/client.d.ts +6 -3
- package/dist/cjs/integrations/client.js +4 -4
- package/dist/cjs/integrations/client.test-d.js +8 -9
- package/dist/cjs/integrations/index.d.ts +2 -2
- package/dist/cjs/integrations/rules.d.ts +4 -1
- package/dist/cjs/integrations/rules.js +8 -4
- package/dist/cjs/integrations/rules.test.js +14 -5
- package/dist/cjs/sync/lib.js +5 -3
- package/dist/cjs/sync/lib.test.js +27 -20
- package/dist/esm/integrations/client.d.ts +6 -3
- package/dist/esm/integrations/client.js +4 -4
- package/dist/esm/integrations/client.test-d.js +8 -9
- package/dist/esm/integrations/index.d.ts +2 -2
- package/dist/esm/integrations/index.js +1 -1
- package/dist/esm/integrations/rules.d.ts +4 -1
- package/dist/esm/integrations/rules.js +8 -4
- package/dist/esm/integrations/rules.test.js +14 -5
- package/dist/esm/sync/lib.js +5 -3
- package/dist/esm/sync/lib.test.js +27 -20
- package/package.json +1 -1
|
@@ -3,12 +3,14 @@ export declare const INTEGRATIONS_CLASS_NAME = "Connections";
|
|
|
3
3
|
/**
|
|
4
4
|
* `accessToken` is the service's primary credential (for Twilio the account SID).
|
|
5
5
|
* Call `get` where the value is used: a token refreshed mid-run is what the next call returns.
|
|
6
|
+
* For a workspace connection that isn't connected, `get` throws a `ZiteError`
|
|
7
|
+
* whose `userFacingMessage` is safe to show visitors.
|
|
6
8
|
*/
|
|
7
9
|
export type Connected<Extra = {}> = {
|
|
8
10
|
status: "connected";
|
|
9
11
|
accessToken: string;
|
|
10
12
|
} & Extra;
|
|
11
|
-
/**
|
|
13
|
+
/** A per-user entry the signed-in user hasn't connected, or whose token expired: send them to `connectUrl`. */
|
|
12
14
|
export type NeedsConnect = {
|
|
13
15
|
status: "missing" | "expired";
|
|
14
16
|
connectUrl: string;
|
|
@@ -19,7 +21,8 @@ export interface IntegrationsClient<ByName extends Record<string, unknown>> {
|
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* `ByName` maps each entry name to its answer, e.g.
|
|
22
|
-
* `{ crm: Connected<{ instanceUrl: string }
|
|
23
|
-
* writes it per
|
|
24
|
+
* `{ crm: Connected<{ instanceUrl: string }>; inbox: Connected | NeedsConnect }`
|
|
25
|
+
* (only per-user entries can need connecting); `zitejs generate` writes it per
|
|
26
|
+
* app into `.zite/integrations/index.ts`.
|
|
24
27
|
*/
|
|
25
28
|
export declare function createIntegrationsClient<ByName extends Record<string, unknown>>(): IntegrationsClient<ByName>;
|
|
@@ -3,14 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.INTEGRATIONS_CLASS_NAME = exports.INTEGRATIONS_SDK_INTEGRATION_ID = void 0;
|
|
4
4
|
exports.createIntegrationsClient = createIntegrationsClient;
|
|
5
5
|
const sdkCall_js_1 = require("../internal/sdkCall.js");
|
|
6
|
-
// The
|
|
7
|
-
// wire protocol would break every deployed bundle for no user-visible gain.
|
|
6
|
+
// The platform id the worker answers `get` under, as `__pdf__` is for PDFs.
|
|
8
7
|
exports.INTEGRATIONS_SDK_INTEGRATION_ID = "__connections__";
|
|
9
8
|
exports.INTEGRATIONS_CLASS_NAME = "Connections";
|
|
10
9
|
/**
|
|
11
10
|
* `ByName` maps each entry name to its answer, e.g.
|
|
12
|
-
* `{ crm: Connected<{ instanceUrl: string }
|
|
13
|
-
* writes it per
|
|
11
|
+
* `{ crm: Connected<{ instanceUrl: string }>; inbox: Connected | NeedsConnect }`
|
|
12
|
+
* (only per-user entries can need connecting); `zitejs generate` writes it per
|
|
13
|
+
* app into `.zite/integrations/index.ts`.
|
|
14
14
|
*/
|
|
15
15
|
function createIntegrationsClient() {
|
|
16
16
|
return {
|
|
@@ -8,6 +8,7 @@ const index_js_2 = require("../index.js");
|
|
|
8
8
|
const client = (0, client_js_1.createIntegrationsClient)();
|
|
9
9
|
(0, vitest_1.it)("types each name's answer", () => {
|
|
10
10
|
(0, vitest_1.expectTypeOf)(client.get("crm")).resolves.toEqualTypeOf();
|
|
11
|
+
(0, vitest_1.expectTypeOf)(client.get("inbox")).resolves.toEqualTypeOf();
|
|
11
12
|
});
|
|
12
13
|
(0, vitest_1.it)("rejects a name the app cannot see", () => {
|
|
13
14
|
// @ts-expect-error not a visible entry
|
|
@@ -18,23 +19,21 @@ const index_js_2 = require("../index.js");
|
|
|
18
19
|
const none = (0, client_js_1.createIntegrationsClient)();
|
|
19
20
|
(0, vitest_1.expectTypeOf)().toEqualTypeOf();
|
|
20
21
|
});
|
|
21
|
-
(0, vitest_1.it)("narrows to the connected fields on status", async () => {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
(0, vitest_1.expectTypeOf)(
|
|
22
|
+
(0, vitest_1.it)("narrows a per-user entry to the connected fields on status", async () => {
|
|
23
|
+
const inbox = await client.get("inbox");
|
|
24
|
+
if (inbox.status === "connected") {
|
|
25
|
+
(0, vitest_1.expectTypeOf)(inbox.accessToken).toEqualTypeOf();
|
|
25
26
|
}
|
|
26
27
|
else {
|
|
27
|
-
(0, vitest_1.expectTypeOf)(
|
|
28
|
+
(0, vitest_1.expectTypeOf)(inbox.connectUrl).toEqualTypeOf();
|
|
28
29
|
}
|
|
29
30
|
});
|
|
30
31
|
});
|
|
31
32
|
(0, vitest_1.describe)("the untyped zitejs/integrations client", () => {
|
|
32
33
|
(0, vitest_1.it)("takes any name; only accessToken is certain", async () => {
|
|
33
34
|
const entry = await index_js_1.integrations.get("anything");
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
(0, vitest_1.expectTypeOf)(entry.instanceUrl).toEqualTypeOf();
|
|
37
|
-
}
|
|
35
|
+
(0, vitest_1.expectTypeOf)(entry.accessToken).toEqualTypeOf();
|
|
36
|
+
(0, vitest_1.expectTypeOf)(entry.instanceUrl).toEqualTypeOf();
|
|
38
37
|
});
|
|
39
38
|
});
|
|
40
39
|
(0, vitest_1.describe)("CREDENTIAL_FIELDS", () => {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { type Connected
|
|
1
|
+
import { type Connected } from "./client.js";
|
|
2
2
|
export type { Connected, NeedsConnect } from "./client.js";
|
|
3
|
-
export declare const integrations: import("./client.js").IntegrationsClient<Record<string,
|
|
3
|
+
export declare const integrations: import("./client.js").IntegrationsClient<Record<string, Connected<Partial<Record<string, string>>>>>;
|
|
@@ -47,7 +47,7 @@ export type CredentialServiceType = keyof typeof FIELDS_BY_TYPE;
|
|
|
47
47
|
export declare const CREDENTIAL_FIELDS: Readonly<Record<CredentialServiceType, readonly string[]>>;
|
|
48
48
|
/**
|
|
49
49
|
* Extras the platform omits when the account doesn't have them: a Jira site
|
|
50
|
-
* connected before its URL was stored
|
|
50
|
+
* connected before its URL was stored.
|
|
51
51
|
*/
|
|
52
52
|
export declare const OPTIONAL_CREDENTIAL_FIELDS: Readonly<Partial<Record<CredentialServiceType, readonly string[]>>>;
|
|
53
53
|
/**
|
|
@@ -61,9 +61,12 @@ export declare const isCredentialServiceType: (type: string) => type is Credenti
|
|
|
61
61
|
export type GettableIntegration = {
|
|
62
62
|
name: string;
|
|
63
63
|
serviceType: CredentialServiceType;
|
|
64
|
+
/** Each signed-in user connects their own account, so `get` can answer that they haven't. */
|
|
65
|
+
perUser: boolean;
|
|
64
66
|
};
|
|
65
67
|
export type ConfigIntegrations = Readonly<Record<string, {
|
|
66
68
|
type?: unknown;
|
|
69
|
+
connectionMode?: unknown;
|
|
67
70
|
} | undefined>>;
|
|
68
71
|
/**
|
|
69
72
|
* The entries a source dir can name, by name: its own, then the workspace's under
|
|
@@ -54,11 +54,10 @@ const FIELDS_BY_TYPE = {
|
|
|
54
54
|
exports.CREDENTIAL_FIELDS = FIELDS_BY_TYPE;
|
|
55
55
|
/**
|
|
56
56
|
* Extras the platform omits when the account doesn't have them: a Jira site
|
|
57
|
-
* connected before its URL was stored
|
|
57
|
+
* connected before its URL was stored.
|
|
58
58
|
*/
|
|
59
59
|
exports.OPTIONAL_CREDENTIAL_FIELDS = {
|
|
60
60
|
jira: ["siteUrl"],
|
|
61
|
-
trello: ["clientApiKey"],
|
|
62
61
|
};
|
|
63
62
|
/**
|
|
64
63
|
* Types the runtime never serves: those with a generated client (Airtable, email,
|
|
@@ -105,8 +104,13 @@ const gettableIntegrations = (rootIntegrations, appIntegrations) => {
|
|
|
105
104
|
if (typeof entry?.type !== "string")
|
|
106
105
|
continue;
|
|
107
106
|
const serviceType = (0, exports.normalizeIntegrationServiceType)(entry.type);
|
|
108
|
-
if ((0, exports.isCredentialServiceType)(serviceType))
|
|
109
|
-
visible.push({
|
|
107
|
+
if ((0, exports.isCredentialServiceType)(serviceType)) {
|
|
108
|
+
visible.push({
|
|
109
|
+
name,
|
|
110
|
+
serviceType,
|
|
111
|
+
perUser: entry.connectionMode === "app-user",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
110
114
|
}
|
|
111
115
|
return visible.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
112
116
|
};
|
|
@@ -47,9 +47,9 @@ const index_js_1 = require("../index.js");
|
|
|
47
47
|
(0, vitest_1.describe)("gettableIntegrations", () => {
|
|
48
48
|
(0, vitest_1.it)("lists the app's entries, then the workspace's under untaken keys, by name", () => {
|
|
49
49
|
(0, vitest_1.expect)((0, index_js_1.gettableIntegrations)({ crm: { type: "salesforce" }, team: { type: "slack" } }, { billing: { type: "stripe" }, team: { type: "microsoftTeams" } })).toEqual([
|
|
50
|
-
{ name: "billing", serviceType: "stripe" },
|
|
51
|
-
{ name: "crm", serviceType: "salesforce" },
|
|
52
|
-
{ name: "team", serviceType: "microsoftTeams" },
|
|
50
|
+
{ name: "billing", serviceType: "stripe", perUser: false },
|
|
51
|
+
{ name: "crm", serviceType: "salesforce", perUser: false },
|
|
52
|
+
{ name: "team", serviceType: "microsoftTeams", perUser: false },
|
|
53
53
|
]);
|
|
54
54
|
});
|
|
55
55
|
(0, vitest_1.it)("lets an app entry of a generated-client type shadow a workspace credential entry", () => {
|
|
@@ -70,7 +70,7 @@ const index_js_1 = require("../index.js");
|
|
|
70
70
|
});
|
|
71
71
|
(0, vitest_1.it)("normalizes legacy type spellings", () => {
|
|
72
72
|
(0, vitest_1.expect)((0, index_js_1.gettableIntegrations)({ cal: { type: "google-calendar" } })).toEqual([
|
|
73
|
-
{ name: "cal", serviceType: "googleCalendar" },
|
|
73
|
+
{ name: "cal", serviceType: "googleCalendar", perUser: false },
|
|
74
74
|
]);
|
|
75
75
|
});
|
|
76
76
|
(0, vitest_1.it)("drops a type it does not know, as the runtime does", () => {
|
|
@@ -78,7 +78,16 @@ const index_js_1 = require("../index.js");
|
|
|
78
78
|
});
|
|
79
79
|
(0, vitest_1.it)("shadows by own keys only", () => {
|
|
80
80
|
(0, vitest_1.expect)((0, index_js_1.gettableIntegrations)({ constructor: { type: "slack" } }, {})).toEqual([
|
|
81
|
-
{ name: "constructor", serviceType: "slack" },
|
|
81
|
+
{ name: "constructor", serviceType: "slack", perUser: false },
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)("marks an entry each signed-in user connects for themselves", () => {
|
|
85
|
+
(0, vitest_1.expect)((0, index_js_1.gettableIntegrations)({
|
|
86
|
+
inbox: { type: "gmail", connectionMode: "app-user" },
|
|
87
|
+
support: { type: "gmail" },
|
|
88
|
+
})).toEqual([
|
|
89
|
+
{ name: "inbox", serviceType: "gmail", perUser: true },
|
|
90
|
+
{ name: "support", serviceType: "gmail", perUser: false },
|
|
82
91
|
]);
|
|
83
92
|
});
|
|
84
93
|
(0, vitest_1.it)("works with no configs", () => {
|
package/dist/cjs/sync/lib.js
CHANGED
|
@@ -1427,7 +1427,7 @@ function generateIntegrationsIndex(entries, opts) {
|
|
|
1427
1427
|
? ["export type IntegrationByName = {};"]
|
|
1428
1428
|
: [
|
|
1429
1429
|
"export type IntegrationByName = {",
|
|
1430
|
-
...sorted.map(({ name, serviceType }) => ` ${tsSingleQuoted(name)}: ${connectedTypeFor(serviceType)} | NeedsConnect;`),
|
|
1430
|
+
...sorted.map(({ name, serviceType, perUser }) => ` ${tsSingleQuoted(name)}: ${connectedTypeFor(serviceType)}${perUser ? " | NeedsConnect" : ""};`),
|
|
1431
1431
|
"};",
|
|
1432
1432
|
];
|
|
1433
1433
|
return [
|
|
@@ -1437,8 +1437,10 @@ function generateIntegrationsIndex(entries, opts) {
|
|
|
1437
1437
|
"// Usage in endpoints (src/api/*.ts) and automations (src/index.ts):",
|
|
1438
1438
|
"// import { integrations } from 'zitejs/integrations';",
|
|
1439
1439
|
"//",
|
|
1440
|
-
"// const
|
|
1441
|
-
"//
|
|
1440
|
+
"// const { accessToken } = await integrations.get('<entry name>');",
|
|
1441
|
+
"//",
|
|
1442
|
+
"// A workspace connection that isn't connected throws a ZiteError. A per-user",
|
|
1443
|
+
"// entry answers `missing`/`expired` with a connectUrl for the signed-in user.",
|
|
1442
1444
|
"//",
|
|
1443
1445
|
"// Call get where the value is used, not once at module scope: a token refreshed",
|
|
1444
1446
|
"// mid-run is what the next call returns.",
|
|
@@ -467,8 +467,9 @@ const duplicateIdentifierErrorsIn = (source) => {
|
|
|
467
467
|
};
|
|
468
468
|
(0, vitest_1.it)('emits the client typed by name', () => {
|
|
469
469
|
(0, vitest_1.expect)((0, lib_js_1.generateIntegrationsIndex)([
|
|
470
|
-
{ name: 'team', serviceType: 'slack' },
|
|
471
|
-
{ name: 'crm', serviceType: 'salesforce' },
|
|
470
|
+
{ name: 'team', serviceType: 'slack', perUser: false },
|
|
471
|
+
{ name: 'crm', serviceType: 'salesforce', perUser: false },
|
|
472
|
+
{ name: 'inbox', serviceType: 'gmail', perUser: true },
|
|
472
473
|
], { reexportAirtable: false })).toMatchInlineSnapshot(`
|
|
473
474
|
"// Auto-generated by zitejs generate from zite.config.json. Do not edit manually.
|
|
474
475
|
// zitejs/integrations — an integration's credentials by its zite.config.json entry name.
|
|
@@ -476,8 +477,10 @@ const duplicateIdentifierErrorsIn = (source) => {
|
|
|
476
477
|
// Usage in endpoints (src/api/*.ts) and automations (src/index.ts):
|
|
477
478
|
// import { integrations } from 'zitejs/integrations';
|
|
478
479
|
//
|
|
479
|
-
// const
|
|
480
|
-
//
|
|
480
|
+
// const { accessToken } = await integrations.get('<entry name>');
|
|
481
|
+
//
|
|
482
|
+
// A workspace connection that isn't connected throws a ZiteError. A per-user
|
|
483
|
+
// entry answers \`missing\`/\`expired\` with a connectUrl for the signed-in user.
|
|
481
484
|
//
|
|
482
485
|
// Call get where the value is used, not once at module scope: a token refreshed
|
|
483
486
|
// mid-run is what the next call returns.
|
|
@@ -487,11 +490,12 @@ const duplicateIdentifierErrorsIn = (source) => {
|
|
|
487
490
|
|
|
488
491
|
export type { Connected, NeedsConnect };
|
|
489
492
|
|
|
490
|
-
export type IntegrationName = 'crm' | 'team';
|
|
493
|
+
export type IntegrationName = 'crm' | 'inbox' | 'team';
|
|
491
494
|
|
|
492
495
|
export type IntegrationByName = {
|
|
493
|
-
'crm': Connected<{ instanceUrl: string }
|
|
494
|
-
'
|
|
496
|
+
'crm': Connected<{ instanceUrl: string }>;
|
|
497
|
+
'inbox': Connected | NeedsConnect;
|
|
498
|
+
'team': Connected;
|
|
495
499
|
};
|
|
496
500
|
|
|
497
501
|
export const integrations = createIntegrationsClient<IntegrationByName>();
|
|
@@ -500,33 +504,36 @@ const duplicateIdentifierErrorsIn = (source) => {
|
|
|
500
504
|
});
|
|
501
505
|
// The platform leaves these out for some accounts, so the type can't promise them.
|
|
502
506
|
(0, vitest_1.it)('marks an extra the platform may omit as optional', () => {
|
|
503
|
-
const generated = (0, lib_js_1.generateIntegrationsIndex)([{ name: 'tickets', serviceType: 'jira' }], { reexportAirtable: false });
|
|
504
|
-
(0, vitest_1.expect)(generated).toContain("'tickets': Connected<{ cloudId: string; siteUrl?: string }
|
|
507
|
+
const generated = (0, lib_js_1.generateIntegrationsIndex)([{ name: 'tickets', serviceType: 'jira', perUser: false }], { reexportAirtable: false });
|
|
508
|
+
(0, vitest_1.expect)(generated).toContain("'tickets': Connected<{ cloudId: string; siteUrl?: string }>;");
|
|
509
|
+
// The runner supplies Trello's key for every account, or refuses get.
|
|
510
|
+
(0, vitest_1.expect)((0, lib_js_1.generateIntegrationsIndex)([{ name: 'boards', serviceType: 'trello', perUser: false }], { reexportAirtable: false })).toContain("'boards': Connected<{ clientApiKey: string }>;");
|
|
505
511
|
(0, vitest_1.expect)(typeErrorsIn(generated, `import { integrations } from 'zitejs/integrations';
|
|
506
512
|
const jira = await integrations.get('tickets');
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const siteUrl: string = jira.siteUrl;
|
|
510
|
-
}
|
|
513
|
+
const cloudId: string = jira.cloudId;
|
|
514
|
+
const siteUrl: string = jira.siteUrl;
|
|
511
515
|
`)).toEqual([
|
|
512
516
|
vitest_1.expect.stringContaining("Type 'string | undefined' is not assignable"),
|
|
513
517
|
]);
|
|
514
518
|
});
|
|
519
|
+
// A workspace entry is used as is; only a per-user one makes code check first.
|
|
515
520
|
(0, vitest_1.it)('types get per name in app code', () => {
|
|
516
521
|
const generated = (0, lib_js_1.generateIntegrationsIndex)([
|
|
517
|
-
{ name: 'crm', serviceType: 'salesforce' },
|
|
518
|
-
{ name: 'warehouse', serviceType: 'clickhouse' },
|
|
522
|
+
{ name: 'crm', serviceType: 'salesforce', perUser: false },
|
|
523
|
+
{ name: 'warehouse', serviceType: 'clickhouse', perUser: false },
|
|
524
|
+
{ name: 'inbox', serviceType: 'gmail', perUser: true },
|
|
519
525
|
], { reexportAirtable: false });
|
|
520
526
|
(0, vitest_1.expect)(typeErrorsIn(generated, [
|
|
521
527
|
"import { integrations } from 'zitejs/integrations';",
|
|
522
528
|
'export const run = async () => {',
|
|
523
529
|
" const crm = await integrations.get('crm');",
|
|
524
|
-
" if (crm.status === 'connected') return crm.instanceUrl + crm.accessToken;",
|
|
525
530
|
" const warehouse = await integrations.get('warehouse');",
|
|
526
|
-
"
|
|
527
|
-
'
|
|
531
|
+
" const inbox = await integrations.get('inbox');",
|
|
532
|
+
" if (inbox.status !== 'connected') return inbox.connectUrl;",
|
|
533
|
+
' return crm.instanceUrl + warehouse.url + inbox.accessToken;',
|
|
528
534
|
'};',
|
|
529
535
|
].join('\n'))).toEqual([]);
|
|
536
|
+
(0, vitest_1.expect)(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nexport const f = async () => (await integrations.get('inbox')).accessToken;")).toHaveLength(1);
|
|
530
537
|
(0, vitest_1.expect)(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nvoid integrations.get('nope');")).toHaveLength(1);
|
|
531
538
|
});
|
|
532
539
|
(0, vitest_1.it)('compiles with nothing visible, naming nothing', () => {
|
|
@@ -542,13 +549,13 @@ if (jira.status === 'connected') {
|
|
|
542
549
|
['a newline', 'crm\nprod'],
|
|
543
550
|
['a trailing backslash', 'crm\\'],
|
|
544
551
|
])('emits parseable TypeScript for a name with %s', (_what, name) => {
|
|
545
|
-
(0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateIntegrationsIndex)([{ name, serviceType: 'slack' }], {
|
|
552
|
+
(0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateIntegrationsIndex)([{ name, serviceType: 'slack', perUser: false }], {
|
|
546
553
|
reexportAirtable: false,
|
|
547
554
|
}))).toEqual([]);
|
|
548
555
|
});
|
|
549
556
|
// One module for everything integration: the Airtable clients keep resolving.
|
|
550
557
|
(0, vitest_1.it)('re-exports the generated Airtable clients beside the integrations client', () => {
|
|
551
|
-
const generated = (0, lib_js_1.generateIntegrationsIndex)([{ name: 'team', serviceType: 'slack' }], { reexportAirtable: true });
|
|
558
|
+
const generated = (0, lib_js_1.generateIntegrationsIndex)([{ name: 'team', serviceType: 'slack', perUser: false }], { reexportAirtable: true });
|
|
552
559
|
(0, vitest_1.expect)(generated).toContain("export * from './airtable';");
|
|
553
560
|
(0, vitest_1.expect)(typeErrorsIn(generated, [
|
|
554
561
|
"import { Customers, integrations } from 'zitejs/integrations';",
|
|
@@ -3,12 +3,14 @@ export declare const INTEGRATIONS_CLASS_NAME = "Connections";
|
|
|
3
3
|
/**
|
|
4
4
|
* `accessToken` is the service's primary credential (for Twilio the account SID).
|
|
5
5
|
* Call `get` where the value is used: a token refreshed mid-run is what the next call returns.
|
|
6
|
+
* For a workspace connection that isn't connected, `get` throws a `ZiteError`
|
|
7
|
+
* whose `userFacingMessage` is safe to show visitors.
|
|
6
8
|
*/
|
|
7
9
|
export type Connected<Extra = {}> = {
|
|
8
10
|
status: "connected";
|
|
9
11
|
accessToken: string;
|
|
10
12
|
} & Extra;
|
|
11
|
-
/**
|
|
13
|
+
/** A per-user entry the signed-in user hasn't connected, or whose token expired: send them to `connectUrl`. */
|
|
12
14
|
export type NeedsConnect = {
|
|
13
15
|
status: "missing" | "expired";
|
|
14
16
|
connectUrl: string;
|
|
@@ -19,7 +21,8 @@ export interface IntegrationsClient<ByName extends Record<string, unknown>> {
|
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* `ByName` maps each entry name to its answer, e.g.
|
|
22
|
-
* `{ crm: Connected<{ instanceUrl: string }
|
|
23
|
-
* writes it per
|
|
24
|
+
* `{ crm: Connected<{ instanceUrl: string }>; inbox: Connected | NeedsConnect }`
|
|
25
|
+
* (only per-user entries can need connecting); `zitejs generate` writes it per
|
|
26
|
+
* app into `.zite/integrations/index.ts`.
|
|
24
27
|
*/
|
|
25
28
|
export declare function createIntegrationsClient<ByName extends Record<string, unknown>>(): IntegrationsClient<ByName>;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { getSdkCall } from "../internal/sdkCall.js";
|
|
2
|
-
// The
|
|
3
|
-
// wire protocol would break every deployed bundle for no user-visible gain.
|
|
2
|
+
// The platform id the worker answers `get` under, as `__pdf__` is for PDFs.
|
|
4
3
|
export const INTEGRATIONS_SDK_INTEGRATION_ID = "__connections__";
|
|
5
4
|
export const INTEGRATIONS_CLASS_NAME = "Connections";
|
|
6
5
|
/**
|
|
7
6
|
* `ByName` maps each entry name to its answer, e.g.
|
|
8
|
-
* `{ crm: Connected<{ instanceUrl: string }
|
|
9
|
-
* writes it per
|
|
7
|
+
* `{ crm: Connected<{ instanceUrl: string }>; inbox: Connected | NeedsConnect }`
|
|
8
|
+
* (only per-user entries can need connecting); `zitejs generate` writes it per
|
|
9
|
+
* app into `.zite/integrations/index.ts`.
|
|
10
10
|
*/
|
|
11
11
|
export function createIntegrationsClient() {
|
|
12
12
|
return {
|
|
@@ -6,6 +6,7 @@ describe("createIntegrationsClient", () => {
|
|
|
6
6
|
const client = createIntegrationsClient();
|
|
7
7
|
it("types each name's answer", () => {
|
|
8
8
|
expectTypeOf(client.get("crm")).resolves.toEqualTypeOf();
|
|
9
|
+
expectTypeOf(client.get("inbox")).resolves.toEqualTypeOf();
|
|
9
10
|
});
|
|
10
11
|
it("rejects a name the app cannot see", () => {
|
|
11
12
|
// @ts-expect-error not a visible entry
|
|
@@ -16,23 +17,21 @@ describe("createIntegrationsClient", () => {
|
|
|
16
17
|
const none = createIntegrationsClient();
|
|
17
18
|
expectTypeOf().toEqualTypeOf();
|
|
18
19
|
});
|
|
19
|
-
it("narrows to the connected fields on status", async () => {
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
expectTypeOf(
|
|
20
|
+
it("narrows a per-user entry to the connected fields on status", async () => {
|
|
21
|
+
const inbox = await client.get("inbox");
|
|
22
|
+
if (inbox.status === "connected") {
|
|
23
|
+
expectTypeOf(inbox.accessToken).toEqualTypeOf();
|
|
23
24
|
}
|
|
24
25
|
else {
|
|
25
|
-
expectTypeOf(
|
|
26
|
+
expectTypeOf(inbox.connectUrl).toEqualTypeOf();
|
|
26
27
|
}
|
|
27
28
|
});
|
|
28
29
|
});
|
|
29
30
|
describe("the untyped zitejs/integrations client", () => {
|
|
30
31
|
it("takes any name; only accessToken is certain", async () => {
|
|
31
32
|
const entry = await untyped.get("anything");
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
expectTypeOf(entry.instanceUrl).toEqualTypeOf();
|
|
35
|
-
}
|
|
33
|
+
expectTypeOf(entry.accessToken).toEqualTypeOf();
|
|
34
|
+
expectTypeOf(entry.instanceUrl).toEqualTypeOf();
|
|
36
35
|
});
|
|
37
36
|
});
|
|
38
37
|
describe("CREDENTIAL_FIELDS", () => {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { type Connected
|
|
1
|
+
import { type Connected } from "./client.js";
|
|
2
2
|
export type { Connected, NeedsConnect } from "./client.js";
|
|
3
|
-
export declare const integrations: import("./client.js").IntegrationsClient<Record<string,
|
|
3
|
+
export declare const integrations: import("./client.js").IntegrationsClient<Record<string, Connected<Partial<Record<string, string>>>>>;
|
|
@@ -47,7 +47,7 @@ export type CredentialServiceType = keyof typeof FIELDS_BY_TYPE;
|
|
|
47
47
|
export declare const CREDENTIAL_FIELDS: Readonly<Record<CredentialServiceType, readonly string[]>>;
|
|
48
48
|
/**
|
|
49
49
|
* Extras the platform omits when the account doesn't have them: a Jira site
|
|
50
|
-
* connected before its URL was stored
|
|
50
|
+
* connected before its URL was stored.
|
|
51
51
|
*/
|
|
52
52
|
export declare const OPTIONAL_CREDENTIAL_FIELDS: Readonly<Partial<Record<CredentialServiceType, readonly string[]>>>;
|
|
53
53
|
/**
|
|
@@ -61,9 +61,12 @@ export declare const isCredentialServiceType: (type: string) => type is Credenti
|
|
|
61
61
|
export type GettableIntegration = {
|
|
62
62
|
name: string;
|
|
63
63
|
serviceType: CredentialServiceType;
|
|
64
|
+
/** Each signed-in user connects their own account, so `get` can answer that they haven't. */
|
|
65
|
+
perUser: boolean;
|
|
64
66
|
};
|
|
65
67
|
export type ConfigIntegrations = Readonly<Record<string, {
|
|
66
68
|
type?: unknown;
|
|
69
|
+
connectionMode?: unknown;
|
|
67
70
|
} | undefined>>;
|
|
68
71
|
/**
|
|
69
72
|
* The entries a source dir can name, by name: its own, then the workspace's under
|
|
@@ -51,11 +51,10 @@ const FIELDS_BY_TYPE = {
|
|
|
51
51
|
export const CREDENTIAL_FIELDS = FIELDS_BY_TYPE;
|
|
52
52
|
/**
|
|
53
53
|
* Extras the platform omits when the account doesn't have them: a Jira site
|
|
54
|
-
* connected before its URL was stored
|
|
54
|
+
* connected before its URL was stored.
|
|
55
55
|
*/
|
|
56
56
|
export const OPTIONAL_CREDENTIAL_FIELDS = {
|
|
57
57
|
jira: ["siteUrl"],
|
|
58
|
-
trello: ["clientApiKey"],
|
|
59
58
|
};
|
|
60
59
|
/**
|
|
61
60
|
* Types the runtime never serves: those with a generated client (Airtable, email,
|
|
@@ -100,8 +99,13 @@ export const gettableIntegrations = (rootIntegrations, appIntegrations) => {
|
|
|
100
99
|
if (typeof entry?.type !== "string")
|
|
101
100
|
continue;
|
|
102
101
|
const serviceType = normalizeIntegrationServiceType(entry.type);
|
|
103
|
-
if (isCredentialServiceType(serviceType))
|
|
104
|
-
visible.push({
|
|
102
|
+
if (isCredentialServiceType(serviceType)) {
|
|
103
|
+
visible.push({
|
|
104
|
+
name,
|
|
105
|
+
serviceType,
|
|
106
|
+
perUser: entry.connectionMode === "app-user",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
105
109
|
}
|
|
106
110
|
return visible.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
107
111
|
};
|
|
@@ -45,9 +45,9 @@ describe("normalizeIntegrationServiceType", () => {
|
|
|
45
45
|
describe("gettableIntegrations", () => {
|
|
46
46
|
it("lists the app's entries, then the workspace's under untaken keys, by name", () => {
|
|
47
47
|
expect(gettableIntegrations({ crm: { type: "salesforce" }, team: { type: "slack" } }, { billing: { type: "stripe" }, team: { type: "microsoftTeams" } })).toEqual([
|
|
48
|
-
{ name: "billing", serviceType: "stripe" },
|
|
49
|
-
{ name: "crm", serviceType: "salesforce" },
|
|
50
|
-
{ name: "team", serviceType: "microsoftTeams" },
|
|
48
|
+
{ name: "billing", serviceType: "stripe", perUser: false },
|
|
49
|
+
{ name: "crm", serviceType: "salesforce", perUser: false },
|
|
50
|
+
{ name: "team", serviceType: "microsoftTeams", perUser: false },
|
|
51
51
|
]);
|
|
52
52
|
});
|
|
53
53
|
it("lets an app entry of a generated-client type shadow a workspace credential entry", () => {
|
|
@@ -68,7 +68,7 @@ describe("gettableIntegrations", () => {
|
|
|
68
68
|
});
|
|
69
69
|
it("normalizes legacy type spellings", () => {
|
|
70
70
|
expect(gettableIntegrations({ cal: { type: "google-calendar" } })).toEqual([
|
|
71
|
-
{ name: "cal", serviceType: "googleCalendar" },
|
|
71
|
+
{ name: "cal", serviceType: "googleCalendar", perUser: false },
|
|
72
72
|
]);
|
|
73
73
|
});
|
|
74
74
|
it("drops a type it does not know, as the runtime does", () => {
|
|
@@ -76,7 +76,16 @@ describe("gettableIntegrations", () => {
|
|
|
76
76
|
});
|
|
77
77
|
it("shadows by own keys only", () => {
|
|
78
78
|
expect(gettableIntegrations({ constructor: { type: "slack" } }, {})).toEqual([
|
|
79
|
-
{ name: "constructor", serviceType: "slack" },
|
|
79
|
+
{ name: "constructor", serviceType: "slack", perUser: false },
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
it("marks an entry each signed-in user connects for themselves", () => {
|
|
83
|
+
expect(gettableIntegrations({
|
|
84
|
+
inbox: { type: "gmail", connectionMode: "app-user" },
|
|
85
|
+
support: { type: "gmail" },
|
|
86
|
+
})).toEqual([
|
|
87
|
+
{ name: "inbox", serviceType: "gmail", perUser: true },
|
|
88
|
+
{ name: "support", serviceType: "gmail", perUser: false },
|
|
80
89
|
]);
|
|
81
90
|
});
|
|
82
91
|
it("works with no configs", () => {
|
package/dist/esm/sync/lib.js
CHANGED
|
@@ -1412,7 +1412,7 @@ export function generateIntegrationsIndex(entries, opts) {
|
|
|
1412
1412
|
? ["export type IntegrationByName = {};"]
|
|
1413
1413
|
: [
|
|
1414
1414
|
"export type IntegrationByName = {",
|
|
1415
|
-
...sorted.map(({ name, serviceType }) => ` ${tsSingleQuoted(name)}: ${connectedTypeFor(serviceType)} | NeedsConnect;`),
|
|
1415
|
+
...sorted.map(({ name, serviceType, perUser }) => ` ${tsSingleQuoted(name)}: ${connectedTypeFor(serviceType)}${perUser ? " | NeedsConnect" : ""};`),
|
|
1416
1416
|
"};",
|
|
1417
1417
|
];
|
|
1418
1418
|
return [
|
|
@@ -1422,8 +1422,10 @@ export function generateIntegrationsIndex(entries, opts) {
|
|
|
1422
1422
|
"// Usage in endpoints (src/api/*.ts) and automations (src/index.ts):",
|
|
1423
1423
|
"// import { integrations } from 'zitejs/integrations';",
|
|
1424
1424
|
"//",
|
|
1425
|
-
"// const
|
|
1426
|
-
"//
|
|
1425
|
+
"// const { accessToken } = await integrations.get('<entry name>');",
|
|
1426
|
+
"//",
|
|
1427
|
+
"// A workspace connection that isn't connected throws a ZiteError. A per-user",
|
|
1428
|
+
"// entry answers `missing`/`expired` with a connectUrl for the signed-in user.",
|
|
1427
1429
|
"//",
|
|
1428
1430
|
"// Call get where the value is used, not once at module scope: a token refreshed",
|
|
1429
1431
|
"// mid-run is what the next call returns.",
|
|
@@ -462,8 +462,9 @@ describe('generateIntegrationsIndex', () => {
|
|
|
462
462
|
};
|
|
463
463
|
it('emits the client typed by name', () => {
|
|
464
464
|
expect(generateIntegrationsIndex([
|
|
465
|
-
{ name: 'team', serviceType: 'slack' },
|
|
466
|
-
{ name: 'crm', serviceType: 'salesforce' },
|
|
465
|
+
{ name: 'team', serviceType: 'slack', perUser: false },
|
|
466
|
+
{ name: 'crm', serviceType: 'salesforce', perUser: false },
|
|
467
|
+
{ name: 'inbox', serviceType: 'gmail', perUser: true },
|
|
467
468
|
], { reexportAirtable: false })).toMatchInlineSnapshot(`
|
|
468
469
|
"// Auto-generated by zitejs generate from zite.config.json. Do not edit manually.
|
|
469
470
|
// zitejs/integrations — an integration's credentials by its zite.config.json entry name.
|
|
@@ -471,8 +472,10 @@ describe('generateIntegrationsIndex', () => {
|
|
|
471
472
|
// Usage in endpoints (src/api/*.ts) and automations (src/index.ts):
|
|
472
473
|
// import { integrations } from 'zitejs/integrations';
|
|
473
474
|
//
|
|
474
|
-
// const
|
|
475
|
-
//
|
|
475
|
+
// const { accessToken } = await integrations.get('<entry name>');
|
|
476
|
+
//
|
|
477
|
+
// A workspace connection that isn't connected throws a ZiteError. A per-user
|
|
478
|
+
// entry answers \`missing\`/\`expired\` with a connectUrl for the signed-in user.
|
|
476
479
|
//
|
|
477
480
|
// Call get where the value is used, not once at module scope: a token refreshed
|
|
478
481
|
// mid-run is what the next call returns.
|
|
@@ -482,11 +485,12 @@ describe('generateIntegrationsIndex', () => {
|
|
|
482
485
|
|
|
483
486
|
export type { Connected, NeedsConnect };
|
|
484
487
|
|
|
485
|
-
export type IntegrationName = 'crm' | 'team';
|
|
488
|
+
export type IntegrationName = 'crm' | 'inbox' | 'team';
|
|
486
489
|
|
|
487
490
|
export type IntegrationByName = {
|
|
488
|
-
'crm': Connected<{ instanceUrl: string }
|
|
489
|
-
'
|
|
491
|
+
'crm': Connected<{ instanceUrl: string }>;
|
|
492
|
+
'inbox': Connected | NeedsConnect;
|
|
493
|
+
'team': Connected;
|
|
490
494
|
};
|
|
491
495
|
|
|
492
496
|
export const integrations = createIntegrationsClient<IntegrationByName>();
|
|
@@ -495,33 +499,36 @@ describe('generateIntegrationsIndex', () => {
|
|
|
495
499
|
});
|
|
496
500
|
// The platform leaves these out for some accounts, so the type can't promise them.
|
|
497
501
|
it('marks an extra the platform may omit as optional', () => {
|
|
498
|
-
const generated = generateIntegrationsIndex([{ name: 'tickets', serviceType: 'jira' }], { reexportAirtable: false });
|
|
499
|
-
expect(generated).toContain("'tickets': Connected<{ cloudId: string; siteUrl?: string }
|
|
502
|
+
const generated = generateIntegrationsIndex([{ name: 'tickets', serviceType: 'jira', perUser: false }], { reexportAirtable: false });
|
|
503
|
+
expect(generated).toContain("'tickets': Connected<{ cloudId: string; siteUrl?: string }>;");
|
|
504
|
+
// The runner supplies Trello's key for every account, or refuses get.
|
|
505
|
+
expect(generateIntegrationsIndex([{ name: 'boards', serviceType: 'trello', perUser: false }], { reexportAirtable: false })).toContain("'boards': Connected<{ clientApiKey: string }>;");
|
|
500
506
|
expect(typeErrorsIn(generated, `import { integrations } from 'zitejs/integrations';
|
|
501
507
|
const jira = await integrations.get('tickets');
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const siteUrl: string = jira.siteUrl;
|
|
505
|
-
}
|
|
508
|
+
const cloudId: string = jira.cloudId;
|
|
509
|
+
const siteUrl: string = jira.siteUrl;
|
|
506
510
|
`)).toEqual([
|
|
507
511
|
expect.stringContaining("Type 'string | undefined' is not assignable"),
|
|
508
512
|
]);
|
|
509
513
|
});
|
|
514
|
+
// A workspace entry is used as is; only a per-user one makes code check first.
|
|
510
515
|
it('types get per name in app code', () => {
|
|
511
516
|
const generated = generateIntegrationsIndex([
|
|
512
|
-
{ name: 'crm', serviceType: 'salesforce' },
|
|
513
|
-
{ name: 'warehouse', serviceType: 'clickhouse' },
|
|
517
|
+
{ name: 'crm', serviceType: 'salesforce', perUser: false },
|
|
518
|
+
{ name: 'warehouse', serviceType: 'clickhouse', perUser: false },
|
|
519
|
+
{ name: 'inbox', serviceType: 'gmail', perUser: true },
|
|
514
520
|
], { reexportAirtable: false });
|
|
515
521
|
expect(typeErrorsIn(generated, [
|
|
516
522
|
"import { integrations } from 'zitejs/integrations';",
|
|
517
523
|
'export const run = async () => {',
|
|
518
524
|
" const crm = await integrations.get('crm');",
|
|
519
|
-
" if (crm.status === 'connected') return crm.instanceUrl + crm.accessToken;",
|
|
520
525
|
" const warehouse = await integrations.get('warehouse');",
|
|
521
|
-
"
|
|
522
|
-
'
|
|
526
|
+
" const inbox = await integrations.get('inbox');",
|
|
527
|
+
" if (inbox.status !== 'connected') return inbox.connectUrl;",
|
|
528
|
+
' return crm.instanceUrl + warehouse.url + inbox.accessToken;',
|
|
523
529
|
'};',
|
|
524
530
|
].join('\n'))).toEqual([]);
|
|
531
|
+
expect(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nexport const f = async () => (await integrations.get('inbox')).accessToken;")).toHaveLength(1);
|
|
525
532
|
expect(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nvoid integrations.get('nope');")).toHaveLength(1);
|
|
526
533
|
});
|
|
527
534
|
it('compiles with nothing visible, naming nothing', () => {
|
|
@@ -537,13 +544,13 @@ if (jira.status === 'connected') {
|
|
|
537
544
|
['a newline', 'crm\nprod'],
|
|
538
545
|
['a trailing backslash', 'crm\\'],
|
|
539
546
|
])('emits parseable TypeScript for a name with %s', (_what, name) => {
|
|
540
|
-
expect(syntaxErrorsIn(generateIntegrationsIndex([{ name, serviceType: 'slack' }], {
|
|
547
|
+
expect(syntaxErrorsIn(generateIntegrationsIndex([{ name, serviceType: 'slack', perUser: false }], {
|
|
541
548
|
reexportAirtable: false,
|
|
542
549
|
}))).toEqual([]);
|
|
543
550
|
});
|
|
544
551
|
// One module for everything integration: the Airtable clients keep resolving.
|
|
545
552
|
it('re-exports the generated Airtable clients beside the integrations client', () => {
|
|
546
|
-
const generated = generateIntegrationsIndex([{ name: 'team', serviceType: 'slack' }], { reexportAirtable: true });
|
|
553
|
+
const generated = generateIntegrationsIndex([{ name: 'team', serviceType: 'slack', perUser: false }], { reexportAirtable: true });
|
|
547
554
|
expect(generated).toContain("export * from './airtable';");
|
|
548
555
|
expect(typeErrorsIn(generated, [
|
|
549
556
|
"import { Customers, integrations } from 'zitejs/integrations';",
|