zitejs 0.9.129 → 0.9.130
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 +3 -0
- package/dist/cjs/integrations/rules.js +7 -2
- package/dist/cjs/integrations/rules.test.js +14 -5
- package/dist/cjs/sync/lib.js +5 -3
- package/dist/cjs/sync/lib.test.js +25 -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 +3 -0
- package/dist/esm/integrations/rules.js +7 -2
- package/dist/esm/integrations/rules.test.js +14 -5
- package/dist/esm/sync/lib.js +5 -3
- package/dist/esm/sync/lib.test.js +25 -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>>>>>;
|
|
@@ -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
|
|
@@ -105,8 +105,13 @@ const gettableIntegrations = (rootIntegrations, appIntegrations) => {
|
|
|
105
105
|
if (typeof entry?.type !== "string")
|
|
106
106
|
continue;
|
|
107
107
|
const serviceType = (0, exports.normalizeIntegrationServiceType)(entry.type);
|
|
108
|
-
if ((0, exports.isCredentialServiceType)(serviceType))
|
|
109
|
-
visible.push({
|
|
108
|
+
if ((0, exports.isCredentialServiceType)(serviceType)) {
|
|
109
|
+
visible.push({
|
|
110
|
+
name,
|
|
111
|
+
serviceType,
|
|
112
|
+
perUser: entry.connectionMode === "app-user",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
110
115
|
}
|
|
111
116
|
return visible.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
112
117
|
};
|
|
@@ -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,34 @@ 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 }>;");
|
|
505
509
|
(0, vitest_1.expect)(typeErrorsIn(generated, `import { integrations } from 'zitejs/integrations';
|
|
506
510
|
const jira = await integrations.get('tickets');
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const siteUrl: string = jira.siteUrl;
|
|
510
|
-
}
|
|
511
|
+
const cloudId: string = jira.cloudId;
|
|
512
|
+
const siteUrl: string = jira.siteUrl;
|
|
511
513
|
`)).toEqual([
|
|
512
514
|
vitest_1.expect.stringContaining("Type 'string | undefined' is not assignable"),
|
|
513
515
|
]);
|
|
514
516
|
});
|
|
517
|
+
// A workspace entry is used as is; only a per-user one makes code check first.
|
|
515
518
|
(0, vitest_1.it)('types get per name in app code', () => {
|
|
516
519
|
const generated = (0, lib_js_1.generateIntegrationsIndex)([
|
|
517
|
-
{ name: 'crm', serviceType: 'salesforce' },
|
|
518
|
-
{ name: 'warehouse', serviceType: 'clickhouse' },
|
|
520
|
+
{ name: 'crm', serviceType: 'salesforce', perUser: false },
|
|
521
|
+
{ name: 'warehouse', serviceType: 'clickhouse', perUser: false },
|
|
522
|
+
{ name: 'inbox', serviceType: 'gmail', perUser: true },
|
|
519
523
|
], { reexportAirtable: false });
|
|
520
524
|
(0, vitest_1.expect)(typeErrorsIn(generated, [
|
|
521
525
|
"import { integrations } from 'zitejs/integrations';",
|
|
522
526
|
'export const run = async () => {',
|
|
523
527
|
" const crm = await integrations.get('crm');",
|
|
524
|
-
" if (crm.status === 'connected') return crm.instanceUrl + crm.accessToken;",
|
|
525
528
|
" const warehouse = await integrations.get('warehouse');",
|
|
526
|
-
"
|
|
527
|
-
'
|
|
529
|
+
" const inbox = await integrations.get('inbox');",
|
|
530
|
+
" if (inbox.status !== 'connected') return inbox.connectUrl;",
|
|
531
|
+
' return crm.instanceUrl + warehouse.url + inbox.accessToken;',
|
|
528
532
|
'};',
|
|
529
533
|
].join('\n'))).toEqual([]);
|
|
534
|
+
(0, vitest_1.expect)(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nexport const f = async () => (await integrations.get('inbox')).accessToken;")).toHaveLength(1);
|
|
530
535
|
(0, vitest_1.expect)(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nvoid integrations.get('nope');")).toHaveLength(1);
|
|
531
536
|
});
|
|
532
537
|
(0, vitest_1.it)('compiles with nothing visible, naming nothing', () => {
|
|
@@ -542,13 +547,13 @@ if (jira.status === 'connected') {
|
|
|
542
547
|
['a newline', 'crm\nprod'],
|
|
543
548
|
['a trailing backslash', 'crm\\'],
|
|
544
549
|
])('emits parseable TypeScript for a name with %s', (_what, name) => {
|
|
545
|
-
(0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateIntegrationsIndex)([{ name, serviceType: 'slack' }], {
|
|
550
|
+
(0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateIntegrationsIndex)([{ name, serviceType: 'slack', perUser: false }], {
|
|
546
551
|
reexportAirtable: false,
|
|
547
552
|
}))).toEqual([]);
|
|
548
553
|
});
|
|
549
554
|
// One module for everything integration: the Airtable clients keep resolving.
|
|
550
555
|
(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 });
|
|
556
|
+
const generated = (0, lib_js_1.generateIntegrationsIndex)([{ name: 'team', serviceType: 'slack', perUser: false }], { reexportAirtable: true });
|
|
552
557
|
(0, vitest_1.expect)(generated).toContain("export * from './airtable';");
|
|
553
558
|
(0, vitest_1.expect)(typeErrorsIn(generated, [
|
|
554
559
|
"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>>>>>;
|
|
@@ -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
|
|
@@ -100,8 +100,13 @@ export const gettableIntegrations = (rootIntegrations, appIntegrations) => {
|
|
|
100
100
|
if (typeof entry?.type !== "string")
|
|
101
101
|
continue;
|
|
102
102
|
const serviceType = normalizeIntegrationServiceType(entry.type);
|
|
103
|
-
if (isCredentialServiceType(serviceType))
|
|
104
|
-
visible.push({
|
|
103
|
+
if (isCredentialServiceType(serviceType)) {
|
|
104
|
+
visible.push({
|
|
105
|
+
name,
|
|
106
|
+
serviceType,
|
|
107
|
+
perUser: entry.connectionMode === "app-user",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
105
110
|
}
|
|
106
111
|
return visible.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
107
112
|
};
|
|
@@ -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,34 @@ 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 }>;");
|
|
500
504
|
expect(typeErrorsIn(generated, `import { integrations } from 'zitejs/integrations';
|
|
501
505
|
const jira = await integrations.get('tickets');
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const siteUrl: string = jira.siteUrl;
|
|
505
|
-
}
|
|
506
|
+
const cloudId: string = jira.cloudId;
|
|
507
|
+
const siteUrl: string = jira.siteUrl;
|
|
506
508
|
`)).toEqual([
|
|
507
509
|
expect.stringContaining("Type 'string | undefined' is not assignable"),
|
|
508
510
|
]);
|
|
509
511
|
});
|
|
512
|
+
// A workspace entry is used as is; only a per-user one makes code check first.
|
|
510
513
|
it('types get per name in app code', () => {
|
|
511
514
|
const generated = generateIntegrationsIndex([
|
|
512
|
-
{ name: 'crm', serviceType: 'salesforce' },
|
|
513
|
-
{ name: 'warehouse', serviceType: 'clickhouse' },
|
|
515
|
+
{ name: 'crm', serviceType: 'salesforce', perUser: false },
|
|
516
|
+
{ name: 'warehouse', serviceType: 'clickhouse', perUser: false },
|
|
517
|
+
{ name: 'inbox', serviceType: 'gmail', perUser: true },
|
|
514
518
|
], { reexportAirtable: false });
|
|
515
519
|
expect(typeErrorsIn(generated, [
|
|
516
520
|
"import { integrations } from 'zitejs/integrations';",
|
|
517
521
|
'export const run = async () => {',
|
|
518
522
|
" const crm = await integrations.get('crm');",
|
|
519
|
-
" if (crm.status === 'connected') return crm.instanceUrl + crm.accessToken;",
|
|
520
523
|
" const warehouse = await integrations.get('warehouse');",
|
|
521
|
-
"
|
|
522
|
-
'
|
|
524
|
+
" const inbox = await integrations.get('inbox');",
|
|
525
|
+
" if (inbox.status !== 'connected') return inbox.connectUrl;",
|
|
526
|
+
' return crm.instanceUrl + warehouse.url + inbox.accessToken;',
|
|
523
527
|
'};',
|
|
524
528
|
].join('\n'))).toEqual([]);
|
|
529
|
+
expect(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nexport const f = async () => (await integrations.get('inbox')).accessToken;")).toHaveLength(1);
|
|
525
530
|
expect(typeErrorsIn(generated, "import { integrations } from 'zitejs/integrations';\nvoid integrations.get('nope');")).toHaveLength(1);
|
|
526
531
|
});
|
|
527
532
|
it('compiles with nothing visible, naming nothing', () => {
|
|
@@ -537,13 +542,13 @@ if (jira.status === 'connected') {
|
|
|
537
542
|
['a newline', 'crm\nprod'],
|
|
538
543
|
['a trailing backslash', 'crm\\'],
|
|
539
544
|
])('emits parseable TypeScript for a name with %s', (_what, name) => {
|
|
540
|
-
expect(syntaxErrorsIn(generateIntegrationsIndex([{ name, serviceType: 'slack' }], {
|
|
545
|
+
expect(syntaxErrorsIn(generateIntegrationsIndex([{ name, serviceType: 'slack', perUser: false }], {
|
|
541
546
|
reexportAirtable: false,
|
|
542
547
|
}))).toEqual([]);
|
|
543
548
|
});
|
|
544
549
|
// One module for everything integration: the Airtable clients keep resolving.
|
|
545
550
|
it('re-exports the generated Airtable clients beside the integrations client', () => {
|
|
546
|
-
const generated = generateIntegrationsIndex([{ name: 'team', serviceType: 'slack' }], { reexportAirtable: true });
|
|
551
|
+
const generated = generateIntegrationsIndex([{ name: 'team', serviceType: 'slack', perUser: false }], { reexportAirtable: true });
|
|
547
552
|
expect(generated).toContain("export * from './airtable';");
|
|
548
553
|
expect(typeErrorsIn(generated, [
|
|
549
554
|
"import { Customers, integrations } from 'zitejs/integrations';",
|