zitejs 0.9.125 → 0.9.127

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.
@@ -13,17 +13,100 @@ export type ZiteWebhook = {
13
13
  export type ZiteTrigger = {
14
14
  /**
15
15
  * The connection's name in `zite.config.json`. Registered with the
16
- * provider by `src/triggers/<endpointId>/subscribe.ts` and taken down
17
- * by `unsubscribe.ts`, which the platform runs at publish and teardown.
16
+ * provider by the endpoint file's own `subscribe` export and taken down
17
+ * by its `unsubscribe` export, which the platform runs at publish and
18
+ * teardown.
18
19
  */
19
20
  integration: string;
21
+ /** The sentence the editor shows for what fires this endpoint, e.g. "When a task is completed in Asana". */
22
+ label?: string;
20
23
  paused?: boolean;
24
+ every?: never;
21
25
  } | {
22
- provider: "poll";
23
- /** `<n>m` (1–59) or `<n>h` (1–23) */
26
+ /** `<n>m` (1–59) or `<n>h` (1–23). What makes the trigger a poll. */
24
27
  every: string;
28
+ /** The connection the poll checks, if any, so the editor can name it. */
29
+ integration?: string;
30
+ /** The sentence the editor shows for this poll, e.g. "Every 10 minutes, new Stripe charges". */
31
+ label?: string;
25
32
  paused?: boolean;
26
33
  };
34
+ /**
35
+ * What a provider trigger's `subscribe` export receives. Write it as
36
+ * `export const subscribe = (async input => { ... }) satisfies ZiteTriggerSubscribe`
37
+ * so the input is typed and a wrong return shape is a typecheck error.
38
+ */
39
+ export type ZiteTriggerSubscribeInput = {
40
+ /** Where the provider must deliver, token included. */
41
+ notificationUrl: string;
42
+ /** The connection's name, as the trigger names it. */
43
+ integration: string;
44
+ endpointId: string;
45
+ /** A fresh 64-char hex secret, for providers that let the caller choose the signing secret. */
46
+ secret: string;
47
+ /** Set on a re-subscribe, including a renewal. */
48
+ previous: {
49
+ externalId: string;
50
+ state: Record<string, unknown>;
51
+ } | null;
52
+ };
53
+ export type ZiteTriggerSubscribeResult = {
54
+ /** The provider's id for the registration; what `unsubscribe` receives. */
55
+ externalId: string;
56
+ /** The signing secret, if the provider issued one or accepted `input.secret`. */
57
+ secret?: string;
58
+ /** Anything `unsubscribe` needs later; handed back to it and to the next `subscribe`. */
59
+ state?: Record<string, unknown>;
60
+ /** The provider's starting change marker, for notifications that only say something changed. */
61
+ cursor?: string;
62
+ /** ISO time the provider says the registration lapses, read from its response. */
63
+ expiresAt?: string;
64
+ };
65
+ export type ZiteTriggerUnsubscribeInput = {
66
+ externalId: string;
67
+ state: Record<string, unknown>;
68
+ integration: string;
69
+ endpointId: string;
70
+ };
71
+ export type ZiteTriggerSubscribe = (input: ZiteTriggerSubscribeInput) => Promise<ZiteTriggerSubscribeResult> | ZiteTriggerSubscribeResult;
72
+ export type ZiteTriggerUnsubscribe = (input: ZiteTriggerUnsubscribeInput) => Promise<void> | void;
73
+ /**
74
+ * What a poll-triggered run receives as `input.__poll`. Only on a fire from
75
+ * the poll's schedule; a request call to the same endpoint carries none.
76
+ */
77
+ export type ZitePollInput = {
78
+ /** What the previous successful run returned as `cursor`; null on the first run. */
79
+ cursor: string | null;
80
+ /** Ids earlier runs returned as `handled`, newest last. Skip these. */
81
+ seen: string[];
82
+ };
83
+ /**
84
+ * What a provider trigger's delivery carries as `input.__webhook`, beside the
85
+ * provider's own body. `cursor` and `seen` work as they do for a poll: for a
86
+ * provider whose notification only says that something changed, `subscribe`
87
+ * returns a starting `cursor` and each delivery returns the next one. A
88
+ * trigger that keeps a cursor gets one delivery at a time.
89
+ */
90
+ export type ZiteTriggerDelivery = {
91
+ headers: Record<string, string>;
92
+ /** The provider's id for this delivery; redeliveries share it. */
93
+ deliveryId?: string;
94
+ provider?: string;
95
+ /** What `subscribe` or the previous successful delivery returned as `cursor`. */
96
+ cursor: string | null;
97
+ /** Ids earlier deliveries returned as `handled`, newest last. Skip these. */
98
+ seen: string[];
99
+ };
100
+ /**
101
+ * What a poll-triggered run, or a provider trigger's delivery, returns for
102
+ * the platform to keep: its new `cursor`, and the id of every item it acted
103
+ * on, so the next run's `seen` includes them even if the cursor hands them
104
+ * back.
105
+ */
106
+ export type ZitePollResult = {
107
+ cursor?: string;
108
+ handled?: string[];
109
+ };
27
110
  export interface ZiteRequestContext {
28
111
  user: {
29
112
  id: string;
@@ -116,7 +199,15 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
116
199
  */
117
200
  validatesInput?: boolean;
118
201
  execute: (params: {
119
- input: TInput;
202
+ input: TTrigger extends {
203
+ every: string;
204
+ } ? TInput & {
205
+ __poll?: ZitePollInput;
206
+ } : TTrigger extends {
207
+ integration: string;
208
+ } ? TInput & {
209
+ __webhook?: ZiteTriggerDelivery;
210
+ } : TInput;
120
211
  context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
121
212
  } & (TStream extends true ? {
122
213
  stream: ZiteStreamInterface;
@@ -133,6 +133,74 @@ const requestContext = { user: { id: "u1", email: "a@b.c" } };
133
133
  });
134
134
  await (0, vitest_1.expect)(endpoint.execute({ input: undefined, context: requestContext })).rejects.toMatchObject({ code: "BAD_REQUEST" });
135
135
  });
136
+ (0, vitest_1.it)("hands a poll its cursor and seen ids, typed", async () => {
137
+ const endpoint = (0, index_js_1.createEndpoint)({
138
+ inputSchema: zod_1.z.object({}),
139
+ trigger: { every: "5m", integration: "stripe" },
140
+ execute: async ({ input }) => {
141
+ const seen = new Set(input.__poll?.seen);
142
+ const items = ["a", "b"].filter((id) => !seen.has(id));
143
+ return { cursor: input.__poll?.cursor ?? "start", handled: items };
144
+ },
145
+ });
146
+ await (0, vitest_1.expect)(endpoint.execute({
147
+ input: { __poll: { cursor: "c1", seen: ["a"] } },
148
+ context: { user: null },
149
+ })).resolves.toEqual({ cursor: "c1", handled: ["b"] });
150
+ });
151
+ (0, vitest_1.it)("gives __poll only to poll triggers", () => {
152
+ (0, index_js_1.createEndpoint)({
153
+ inputSchema: zod_1.z.object({ action: zod_1.z.string() }),
154
+ trigger: { integration: "github" },
155
+ execute: async ({ input }) => {
156
+ // @ts-expect-error a provider trigger's input has no __poll
157
+ return input.__poll;
158
+ },
159
+ });
160
+ });
161
+ (0, vitest_1.it)("types a trigger's registration exports and checks what subscribe returns", async () => {
162
+ const subscribe = (async (input) => ({
163
+ externalId: `hook-for-${input.endpointId}`,
164
+ secret: input.secret,
165
+ state: { repo: "acme/api" },
166
+ }));
167
+ const unsubscribe = (async (input) => {
168
+ (0, vitest_1.expect)(input.state.repo).toBe("acme/api");
169
+ });
170
+ // @ts-expect-error subscribe has to return the provider's id
171
+ void (async () => ({ secret: "s" }));
172
+ const registered = await subscribe({
173
+ notificationUrl: "https://x",
174
+ integration: "github",
175
+ endpointId: "onPush",
176
+ secret: "abc",
177
+ previous: null,
178
+ });
179
+ (0, vitest_1.expect)(registered.externalId).toBe("hook-for-onPush");
180
+ await unsubscribe({
181
+ externalId: registered.externalId,
182
+ state: registered.state ?? {},
183
+ integration: "github",
184
+ endpointId: "onPush",
185
+ });
186
+ });
187
+ (0, vitest_1.it)("hands a provider trigger's delivery its cursor and seen ids, typed", async () => {
188
+ const endpoint = (0, index_js_1.createEndpoint)({
189
+ inputSchema: zod_1.z.object({}),
190
+ trigger: { integration: "googleCalendar" },
191
+ execute: async ({ input }) => {
192
+ const seen = new Set(input.__webhook?.seen);
193
+ const changed = ["e1", "e2"].filter((id) => !seen.has(id));
194
+ return { cursor: `${input.__webhook?.cursor}+1`, handled: changed };
195
+ },
196
+ });
197
+ await (0, vitest_1.expect)(endpoint.execute({
198
+ input: {
199
+ __webhook: { headers: {}, cursor: "sync1", seen: ["e1"] },
200
+ },
201
+ context: { user: null },
202
+ })).resolves.toEqual({ cursor: "sync1+1", handled: ["e2"] });
203
+ });
136
204
  (0, vitest_1.it)("does not validate a webhook fire", async () => {
137
205
  const endpoint = (0, index_js_1.createEndpoint)({
138
206
  inputSchema: zod_1.z.object({ token: zod_1.z.string().min(10) }),
@@ -2,7 +2,7 @@
2
2
  * `npx zitejs bundle` — Bundle endpoint files for cloudflare-lambda deployment.
3
3
  *
4
4
  * Replaces the legacy `scripts/bundle-endpoints.js` from zitejs-starter.
5
- * Reads an app's src/api/*.ts endpoints (an automation's single src/workflow.ts), resolves SDK imports via .zite/backend.ts
5
+ * Reads an app's src/api/*.ts endpoints (an automation's single src/index.ts), resolves SDK imports via .zite/backend.ts
6
6
  * (monorepo) or src/__zite__/integrations.ts (legacy), and outputs bundled ESM
7
7
  * code suitable for the cloudflare-lambda worker runtime.
8
8
  *
@@ -40,7 +40,7 @@ exports.runBundle = runBundle;
40
40
  * `npx zitejs bundle` — Bundle endpoint files for cloudflare-lambda deployment.
41
41
  *
42
42
  * Replaces the legacy `scripts/bundle-endpoints.js` from zitejs-starter.
43
- * Reads an app's src/api/*.ts endpoints (an automation's single src/workflow.ts), resolves SDK imports via .zite/backend.ts
43
+ * Reads an app's src/api/*.ts endpoints (an automation's single src/index.ts), resolves SDK imports via .zite/backend.ts
44
44
  * (monorepo) or src/__zite__/integrations.ts (legacy), and outputs bundled ESM
45
45
  * code suitable for the cloudflare-lambda worker runtime.
46
46
  *
@@ -420,7 +420,7 @@ async function bundleEndpointsImpl(baseDir, root, endpointNames) {
420
420
  for (const name of endpointNames) {
421
421
  if (root === 'automations' && name !== sourceRoots_js_1.AUTOMATION_ENDPOINT_NAME) {
422
422
  endpointErrors[name] =
423
- `An automation is one workflow, src/workflow.ts, bundled as "${sourceRoots_js_1.AUTOMATION_ENDPOINT_NAME}"; there is no endpoint "${name}"`;
423
+ `An automation is one workflow, src/index.ts, bundled as "${sourceRoots_js_1.AUTOMATION_ENDPOINT_NAME}"; there is no endpoint "${name}"`;
424
424
  continue;
425
425
  }
426
426
  const entryFile = (0, sourceRoots_js_1.endpointEntryFile)(root, name);
@@ -592,6 +592,10 @@ ${bodySection}
592
592
  },
593
593
  minify: false,
594
594
  logLevel: 'silent',
595
+ // A script that imports an endpoint file's named exports (a trigger's
596
+ // subscribe/unsubscribe) must not carry the endpoint along with them:
597
+ // unused, its `createEndpoint(...)` and everything `execute` imports go.
598
+ pure: ['createEndpoint'],
595
599
  plugins: [plugin],
596
600
  });
597
601
  if (result.errors.length > 0) {
package/dist/cjs/cli.js CHANGED
@@ -75,7 +75,7 @@ async function main() {
75
75
  console.error(' dev Run sync then watch for changes (like npx convex dev)');
76
76
  console.error(' generate Regenerate every app and automation .zite/ (monorepo)');
77
77
  console.error(' check Run tsc --noEmit (and vite build where there is one) for every app and automation');
78
- console.error(' bundle Bundle endpoints (src/api/*.ts, or an automation\'s src/workflow.ts) for cloudflare-lambda');
78
+ console.error(' bundle Bundle endpoints (src/api/*.ts, or an automation\'s src/index.ts) for cloudflare-lambda');
79
79
  process.exit(1);
80
80
  }
81
81
  }
@@ -0,0 +1,14 @@
1
+ export type ZiteProjectUser = {
2
+ uuid: string;
3
+ firstName: string | null;
4
+ lastName: string | null;
5
+ email: string;
6
+ profilePictureUrl: string | null;
7
+ };
8
+ export type MetaListUsersResult = {
9
+ users: ZiteProjectUser[];
10
+ };
11
+ export declare class ZiteMeta {
12
+ static listUsers(): Promise<MetaListUsersResult>;
13
+ }
14
+ export declare const Meta: typeof ZiteMeta;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Meta = exports.ZiteMeta = void 0;
4
+ const sdkCall_js_1 = require("../internal/sdkCall.js");
5
+ const META_SDK_INTEGRATION_ID = '__meta__';
6
+ class ZiteMeta {
7
+ static listUsers() {
8
+ return (0, sdkCall_js_1.getSdkCall)()(META_SDK_INTEGRATION_ID, 'ZiteMeta', 'listUsers', {});
9
+ }
10
+ }
11
+ exports.ZiteMeta = ZiteMeta;
12
+ exports.Meta = ZiteMeta;
@@ -0,0 +1,25 @@
1
+ export interface NotificationLink {
2
+ path?: string;
3
+ params?: Record<string, string>;
4
+ }
5
+ export type NotificationsCreateParams = {
6
+ recipients: string[];
7
+ title: string;
8
+ body?: string;
9
+ link?: NotificationLink;
10
+ path?: string;
11
+ params?: Record<string, string>;
12
+ payload?: Record<string, unknown>;
13
+ idempotencyKey?: string;
14
+ };
15
+ export type NotificationsCreateResult = {
16
+ created: number;
17
+ } | {
18
+ created: 0;
19
+ preview: true;
20
+ wouldCreate: number;
21
+ };
22
+ export declare class ZiteNotifications {
23
+ static create(params: NotificationsCreateParams): Promise<NotificationsCreateResult>;
24
+ }
25
+ export declare const Notifications: typeof ZiteNotifications;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Notifications = exports.ZiteNotifications = void 0;
4
+ const sdkCall_js_1 = require("../internal/sdkCall.js");
5
+ const NOTIFICATIONS_SDK_INTEGRATION_ID = '__notifications__';
6
+ class ZiteNotifications {
7
+ static create(params) {
8
+ return (0, sdkCall_js_1.getSdkCall)()(NOTIFICATIONS_SDK_INTEGRATION_ID, 'ZiteNotifications', 'create', params);
9
+ }
10
+ }
11
+ exports.ZiteNotifications = ZiteNotifications;
12
+ exports.Notifications = ZiteNotifications;
@@ -18,12 +18,12 @@ export declare function listSourceDirs(cwd?: string): SourceDir[];
18
18
  /** The source dir with this bare name, under whichever root holds it. */
19
19
  export declare function findSourceDir(name: string, cwd?: string): SourceDir | undefined;
20
20
  /**
21
- * An automation is one workflow: its single endpoint is `src/workflow.ts` and
21
+ * An automation is one workflow: its single endpoint is `src/index.ts` and
22
22
  * its name is fixed, so nothing about an automation is keyed on a file name
23
23
  * the way an app's `src/api/<name>.ts` endpoints are.
24
24
  */
25
- export declare const AUTOMATION_ENDPOINT_NAME = "workflow";
26
- export declare const AUTOMATION_WORKFLOW_FILE = "src/workflow.ts";
25
+ export declare const AUTOMATION_ENDPOINT_NAME = "index";
26
+ export declare const AUTOMATION_ENTRY_FILE = "src/index.ts";
27
27
  /** The root a source dir sits under, read from its parent directory. */
28
28
  export declare function sourceRootOf(sourceDirPath: string): SourceRoot | undefined;
29
29
  /** An endpoint's entry file, relative to the source dir. */
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AUTOMATION_WORKFLOW_FILE = exports.AUTOMATION_ENDPOINT_NAME = exports.SOURCE_ROOTS = void 0;
3
+ exports.AUTOMATION_ENTRY_FILE = exports.AUTOMATION_ENDPOINT_NAME = exports.SOURCE_ROOTS = void 0;
4
4
  exports.listSourceDirs = listSourceDirs;
5
5
  exports.findSourceDir = findSourceDir;
6
6
  exports.sourceRootOf = sourceRootOf;
@@ -35,12 +35,12 @@ function findSourceDir(name, cwd = ".") {
35
35
  return listSourceDirs(cwd).find((d) => d.dir === name);
36
36
  }
37
37
  /**
38
- * An automation is one workflow: its single endpoint is `src/workflow.ts` and
38
+ * An automation is one workflow: its single endpoint is `src/index.ts` and
39
39
  * its name is fixed, so nothing about an automation is keyed on a file name
40
40
  * the way an app's `src/api/<name>.ts` endpoints are.
41
41
  */
42
- exports.AUTOMATION_ENDPOINT_NAME = "workflow";
43
- exports.AUTOMATION_WORKFLOW_FILE = "src/workflow.ts";
42
+ exports.AUTOMATION_ENDPOINT_NAME = "index";
43
+ exports.AUTOMATION_ENTRY_FILE = "src/index.ts";
44
44
  /** The root a source dir sits under, read from its parent directory. */
45
45
  function sourceRootOf(sourceDirPath) {
46
46
  const parent = (0, path_1.basename)((0, path_1.dirname)((0, path_1.resolve)(sourceDirPath)));
@@ -51,13 +51,13 @@ function sourceRootOf(sourceDirPath) {
51
51
  /** An endpoint's entry file, relative to the source dir. */
52
52
  function endpointEntryFile(root, name) {
53
53
  return root === "automations"
54
- ? exports.AUTOMATION_WORKFLOW_FILE
54
+ ? exports.AUTOMATION_ENTRY_FILE
55
55
  : (0, path_1.join)("src", "api", `${name}.ts`);
56
56
  }
57
57
  /** Every endpoint name a source dir has on disk. */
58
58
  function listEndpointNames(root, sourceDirPath) {
59
59
  if (root === "automations") {
60
- return (0, fs_1.existsSync)((0, path_1.join)(sourceDirPath, exports.AUTOMATION_WORKFLOW_FILE))
60
+ return (0, fs_1.existsSync)((0, path_1.join)(sourceDirPath, exports.AUTOMATION_ENTRY_FILE))
61
61
  ? [exports.AUTOMATION_ENDPOINT_NAME]
62
62
  : [];
63
63
  }
@@ -11,9 +11,9 @@ const sourceRoots_js_1 = require("./sourceRoots.js");
11
11
  (0, vitest_1.expect)((0, sourceRoots_js_1.sourceRootOf)("/repo/automations/digest")).toBe("automations");
12
12
  (0, vitest_1.expect)((0, sourceRoots_js_1.sourceRootOf)("/repo/packages/ui")).toBeUndefined();
13
13
  });
14
- (0, vitest_1.it)("an app's endpoints are src/api/<name>.ts; an automation's one endpoint is src/workflow.ts", () => {
14
+ (0, vitest_1.it)("an app's endpoints are src/api/<name>.ts; an automation's one endpoint is src/index.ts", () => {
15
15
  (0, vitest_1.expect)((0, sourceRoots_js_1.endpointEntryFile)("apps", "listItems")).toBe("src/api/listItems.ts");
16
- (0, vitest_1.expect)((0, sourceRoots_js_1.endpointEntryFile)("automations", "workflow")).toBe("src/workflow.ts");
16
+ (0, vitest_1.expect)((0, sourceRoots_js_1.endpointEntryFile)("automations", "index")).toBe("src/index.ts");
17
17
  });
18
18
  (0, vitest_1.it)("lists what is on disk under each layout", () => {
19
19
  const repo = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), "zitejs-roots-"));
@@ -22,9 +22,9 @@ const sourceRoots_js_1 = require("./sourceRoots.js");
22
22
  (0, fs_1.writeFileSync)((0, path_1.join)(repo, "apps/crm/src/api/apiTypes.d.ts"), "");
23
23
  (0, fs_1.mkdirSync)((0, path_1.join)(repo, "automations/digest/src"), { recursive: true });
24
24
  (0, fs_1.mkdirSync)((0, path_1.join)(repo, "automations/empty/src"), { recursive: true });
25
- (0, fs_1.writeFileSync)((0, path_1.join)(repo, "automations/digest/src/workflow.ts"), "");
25
+ (0, fs_1.writeFileSync)((0, path_1.join)(repo, "automations/digest/src/index.ts"), "");
26
26
  (0, vitest_1.expect)((0, sourceRoots_js_1.listEndpointNames)("apps", (0, path_1.join)(repo, "apps/crm"))).toEqual(["listItems"]);
27
- (0, vitest_1.expect)((0, sourceRoots_js_1.listEndpointNames)("automations", (0, path_1.join)(repo, "automations/digest"))).toEqual(["workflow"]);
27
+ (0, vitest_1.expect)((0, sourceRoots_js_1.listEndpointNames)("automations", (0, path_1.join)(repo, "automations/digest"))).toEqual(["index"]);
28
28
  (0, vitest_1.expect)((0, sourceRoots_js_1.listEndpointNames)("automations", (0, path_1.join)(repo, "automations/empty"))).toEqual([]);
29
29
  });
30
30
  });
@@ -1426,7 +1426,7 @@ function generateIntegrationsIndex(entries, opts) {
1426
1426
  "// Auto-generated by zitejs generate from zite.config.json. Do not edit manually.",
1427
1427
  "// zitejs/integrations — an integration's credentials by its zite.config.json entry name.",
1428
1428
  "//",
1429
- "// Usage in endpoints (src/api/*.ts) and automations (src/workflow.ts):",
1429
+ "// Usage in endpoints (src/api/*.ts) and automations (src/index.ts):",
1430
1430
  "// import { integrations } from 'zitejs/integrations';",
1431
1431
  "//",
1432
1432
  "// const crm = await integrations.get('<entry name>');",
@@ -465,7 +465,7 @@ const duplicateIdentifierErrorsIn = (source) => {
465
465
  "// Auto-generated by zitejs generate from zite.config.json. Do not edit manually.
466
466
  // zitejs/integrations — an integration's credentials by its zite.config.json entry name.
467
467
  //
468
- // Usage in endpoints (src/api/*.ts) and automations (src/workflow.ts):
468
+ // Usage in endpoints (src/api/*.ts) and automations (src/index.ts):
469
469
  // import { integrations } from 'zitejs/integrations';
470
470
  //
471
471
  // const crm = await integrations.get('<entry name>');
@@ -13,17 +13,100 @@ export type ZiteWebhook = {
13
13
  export type ZiteTrigger = {
14
14
  /**
15
15
  * The connection's name in `zite.config.json`. Registered with the
16
- * provider by `src/triggers/<endpointId>/subscribe.ts` and taken down
17
- * by `unsubscribe.ts`, which the platform runs at publish and teardown.
16
+ * provider by the endpoint file's own `subscribe` export and taken down
17
+ * by its `unsubscribe` export, which the platform runs at publish and
18
+ * teardown.
18
19
  */
19
20
  integration: string;
21
+ /** The sentence the editor shows for what fires this endpoint, e.g. "When a task is completed in Asana". */
22
+ label?: string;
20
23
  paused?: boolean;
24
+ every?: never;
21
25
  } | {
22
- provider: "poll";
23
- /** `<n>m` (1–59) or `<n>h` (1–23) */
26
+ /** `<n>m` (1–59) or `<n>h` (1–23). What makes the trigger a poll. */
24
27
  every: string;
28
+ /** The connection the poll checks, if any, so the editor can name it. */
29
+ integration?: string;
30
+ /** The sentence the editor shows for this poll, e.g. "Every 10 minutes, new Stripe charges". */
31
+ label?: string;
25
32
  paused?: boolean;
26
33
  };
34
+ /**
35
+ * What a provider trigger's `subscribe` export receives. Write it as
36
+ * `export const subscribe = (async input => { ... }) satisfies ZiteTriggerSubscribe`
37
+ * so the input is typed and a wrong return shape is a typecheck error.
38
+ */
39
+ export type ZiteTriggerSubscribeInput = {
40
+ /** Where the provider must deliver, token included. */
41
+ notificationUrl: string;
42
+ /** The connection's name, as the trigger names it. */
43
+ integration: string;
44
+ endpointId: string;
45
+ /** A fresh 64-char hex secret, for providers that let the caller choose the signing secret. */
46
+ secret: string;
47
+ /** Set on a re-subscribe, including a renewal. */
48
+ previous: {
49
+ externalId: string;
50
+ state: Record<string, unknown>;
51
+ } | null;
52
+ };
53
+ export type ZiteTriggerSubscribeResult = {
54
+ /** The provider's id for the registration; what `unsubscribe` receives. */
55
+ externalId: string;
56
+ /** The signing secret, if the provider issued one or accepted `input.secret`. */
57
+ secret?: string;
58
+ /** Anything `unsubscribe` needs later; handed back to it and to the next `subscribe`. */
59
+ state?: Record<string, unknown>;
60
+ /** The provider's starting change marker, for notifications that only say something changed. */
61
+ cursor?: string;
62
+ /** ISO time the provider says the registration lapses, read from its response. */
63
+ expiresAt?: string;
64
+ };
65
+ export type ZiteTriggerUnsubscribeInput = {
66
+ externalId: string;
67
+ state: Record<string, unknown>;
68
+ integration: string;
69
+ endpointId: string;
70
+ };
71
+ export type ZiteTriggerSubscribe = (input: ZiteTriggerSubscribeInput) => Promise<ZiteTriggerSubscribeResult> | ZiteTriggerSubscribeResult;
72
+ export type ZiteTriggerUnsubscribe = (input: ZiteTriggerUnsubscribeInput) => Promise<void> | void;
73
+ /**
74
+ * What a poll-triggered run receives as `input.__poll`. Only on a fire from
75
+ * the poll's schedule; a request call to the same endpoint carries none.
76
+ */
77
+ export type ZitePollInput = {
78
+ /** What the previous successful run returned as `cursor`; null on the first run. */
79
+ cursor: string | null;
80
+ /** Ids earlier runs returned as `handled`, newest last. Skip these. */
81
+ seen: string[];
82
+ };
83
+ /**
84
+ * What a provider trigger's delivery carries as `input.__webhook`, beside the
85
+ * provider's own body. `cursor` and `seen` work as they do for a poll: for a
86
+ * provider whose notification only says that something changed, `subscribe`
87
+ * returns a starting `cursor` and each delivery returns the next one. A
88
+ * trigger that keeps a cursor gets one delivery at a time.
89
+ */
90
+ export type ZiteTriggerDelivery = {
91
+ headers: Record<string, string>;
92
+ /** The provider's id for this delivery; redeliveries share it. */
93
+ deliveryId?: string;
94
+ provider?: string;
95
+ /** What `subscribe` or the previous successful delivery returned as `cursor`. */
96
+ cursor: string | null;
97
+ /** Ids earlier deliveries returned as `handled`, newest last. Skip these. */
98
+ seen: string[];
99
+ };
100
+ /**
101
+ * What a poll-triggered run, or a provider trigger's delivery, returns for
102
+ * the platform to keep: its new `cursor`, and the id of every item it acted
103
+ * on, so the next run's `seen` includes them even if the cursor hands them
104
+ * back.
105
+ */
106
+ export type ZitePollResult = {
107
+ cursor?: string;
108
+ handled?: string[];
109
+ };
27
110
  export interface ZiteRequestContext {
28
111
  user: {
29
112
  id: string;
@@ -116,7 +199,15 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
116
199
  */
117
200
  validatesInput?: boolean;
118
201
  execute: (params: {
119
- input: TInput;
202
+ input: TTrigger extends {
203
+ every: string;
204
+ } ? TInput & {
205
+ __poll?: ZitePollInput;
206
+ } : TTrigger extends {
207
+ integration: string;
208
+ } ? TInput & {
209
+ __webhook?: ZiteTriggerDelivery;
210
+ } : TInput;
120
211
  context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
121
212
  } & (TStream extends true ? {
122
213
  stream: ZiteStreamInterface;
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, afterEach, vi } from "vitest";
2
2
  import { z } from "zod";
3
- import { createEndpoint, ZiteError } from "./index.js";
3
+ import { createEndpoint, ZiteError, } from "./index.js";
4
4
  import { createTableClient } from "../runtime/index.js";
5
5
  const requestContext = { user: { id: "u1", email: "a@b.c" } };
6
6
  afterEach(() => {
@@ -131,6 +131,74 @@ describe("platform-triggered runs", () => {
131
131
  });
132
132
  await expect(endpoint.execute({ input: undefined, context: requestContext })).rejects.toMatchObject({ code: "BAD_REQUEST" });
133
133
  });
134
+ it("hands a poll its cursor and seen ids, typed", async () => {
135
+ const endpoint = createEndpoint({
136
+ inputSchema: z.object({}),
137
+ trigger: { every: "5m", integration: "stripe" },
138
+ execute: async ({ input }) => {
139
+ const seen = new Set(input.__poll?.seen);
140
+ const items = ["a", "b"].filter((id) => !seen.has(id));
141
+ return { cursor: input.__poll?.cursor ?? "start", handled: items };
142
+ },
143
+ });
144
+ await expect(endpoint.execute({
145
+ input: { __poll: { cursor: "c1", seen: ["a"] } },
146
+ context: { user: null },
147
+ })).resolves.toEqual({ cursor: "c1", handled: ["b"] });
148
+ });
149
+ it("gives __poll only to poll triggers", () => {
150
+ createEndpoint({
151
+ inputSchema: z.object({ action: z.string() }),
152
+ trigger: { integration: "github" },
153
+ execute: async ({ input }) => {
154
+ // @ts-expect-error a provider trigger's input has no __poll
155
+ return input.__poll;
156
+ },
157
+ });
158
+ });
159
+ it("types a trigger's registration exports and checks what subscribe returns", async () => {
160
+ const subscribe = (async (input) => ({
161
+ externalId: `hook-for-${input.endpointId}`,
162
+ secret: input.secret,
163
+ state: { repo: "acme/api" },
164
+ }));
165
+ const unsubscribe = (async (input) => {
166
+ expect(input.state.repo).toBe("acme/api");
167
+ });
168
+ // @ts-expect-error subscribe has to return the provider's id
169
+ void (async () => ({ secret: "s" }));
170
+ const registered = await subscribe({
171
+ notificationUrl: "https://x",
172
+ integration: "github",
173
+ endpointId: "onPush",
174
+ secret: "abc",
175
+ previous: null,
176
+ });
177
+ expect(registered.externalId).toBe("hook-for-onPush");
178
+ await unsubscribe({
179
+ externalId: registered.externalId,
180
+ state: registered.state ?? {},
181
+ integration: "github",
182
+ endpointId: "onPush",
183
+ });
184
+ });
185
+ it("hands a provider trigger's delivery its cursor and seen ids, typed", async () => {
186
+ const endpoint = createEndpoint({
187
+ inputSchema: z.object({}),
188
+ trigger: { integration: "googleCalendar" },
189
+ execute: async ({ input }) => {
190
+ const seen = new Set(input.__webhook?.seen);
191
+ const changed = ["e1", "e2"].filter((id) => !seen.has(id));
192
+ return { cursor: `${input.__webhook?.cursor}+1`, handled: changed };
193
+ },
194
+ });
195
+ await expect(endpoint.execute({
196
+ input: {
197
+ __webhook: { headers: {}, cursor: "sync1", seen: ["e1"] },
198
+ },
199
+ context: { user: null },
200
+ })).resolves.toEqual({ cursor: "sync1+1", handled: ["e2"] });
201
+ });
134
202
  it("does not validate a webhook fire", async () => {
135
203
  const endpoint = createEndpoint({
136
204
  inputSchema: z.object({ token: z.string().min(10) }),
@@ -2,7 +2,7 @@
2
2
  * `npx zitejs bundle` — Bundle endpoint files for cloudflare-lambda deployment.
3
3
  *
4
4
  * Replaces the legacy `scripts/bundle-endpoints.js` from zitejs-starter.
5
- * Reads an app's src/api/*.ts endpoints (an automation's single src/workflow.ts), resolves SDK imports via .zite/backend.ts
5
+ * Reads an app's src/api/*.ts endpoints (an automation's single src/index.ts), resolves SDK imports via .zite/backend.ts
6
6
  * (monorepo) or src/__zite__/integrations.ts (legacy), and outputs bundled ESM
7
7
  * code suitable for the cloudflare-lambda worker runtime.
8
8
  *
@@ -2,7 +2,7 @@
2
2
  * `npx zitejs bundle` — Bundle endpoint files for cloudflare-lambda deployment.
3
3
  *
4
4
  * Replaces the legacy `scripts/bundle-endpoints.js` from zitejs-starter.
5
- * Reads an app's src/api/*.ts endpoints (an automation's single src/workflow.ts), resolves SDK imports via .zite/backend.ts
5
+ * Reads an app's src/api/*.ts endpoints (an automation's single src/index.ts), resolves SDK imports via .zite/backend.ts
6
6
  * (monorepo) or src/__zite__/integrations.ts (legacy), and outputs bundled ESM
7
7
  * code suitable for the cloudflare-lambda worker runtime.
8
8
  *
@@ -382,7 +382,7 @@ async function bundleEndpointsImpl(baseDir, root, endpointNames) {
382
382
  for (const name of endpointNames) {
383
383
  if (root === 'automations' && name !== AUTOMATION_ENDPOINT_NAME) {
384
384
  endpointErrors[name] =
385
- `An automation is one workflow, src/workflow.ts, bundled as "${AUTOMATION_ENDPOINT_NAME}"; there is no endpoint "${name}"`;
385
+ `An automation is one workflow, src/index.ts, bundled as "${AUTOMATION_ENDPOINT_NAME}"; there is no endpoint "${name}"`;
386
386
  continue;
387
387
  }
388
388
  const entryFile = endpointEntryFile(root, name);
@@ -554,6 +554,10 @@ ${bodySection}
554
554
  },
555
555
  minify: false,
556
556
  logLevel: 'silent',
557
+ // A script that imports an endpoint file's named exports (a trigger's
558
+ // subscribe/unsubscribe) must not carry the endpoint along with them:
559
+ // unused, its `createEndpoint(...)` and everything `execute` imports go.
560
+ pure: ['createEndpoint'],
557
561
  plugins: [plugin],
558
562
  });
559
563
  if (result.errors.length > 0) {
package/dist/esm/cli.js CHANGED
@@ -40,7 +40,7 @@ async function main() {
40
40
  console.error(' dev Run sync then watch for changes (like npx convex dev)');
41
41
  console.error(' generate Regenerate every app and automation .zite/ (monorepo)');
42
42
  console.error(' check Run tsc --noEmit (and vite build where there is one) for every app and automation');
43
- console.error(' bundle Bundle endpoints (src/api/*.ts, or an automation\'s src/workflow.ts) for cloudflare-lambda');
43
+ console.error(' bundle Bundle endpoints (src/api/*.ts, or an automation\'s src/index.ts) for cloudflare-lambda');
44
44
  process.exit(1);
45
45
  }
46
46
  }
@@ -0,0 +1,14 @@
1
+ export type ZiteProjectUser = {
2
+ uuid: string;
3
+ firstName: string | null;
4
+ lastName: string | null;
5
+ email: string;
6
+ profilePictureUrl: string | null;
7
+ };
8
+ export type MetaListUsersResult = {
9
+ users: ZiteProjectUser[];
10
+ };
11
+ export declare class ZiteMeta {
12
+ static listUsers(): Promise<MetaListUsersResult>;
13
+ }
14
+ export declare const Meta: typeof ZiteMeta;
@@ -0,0 +1,8 @@
1
+ import { getSdkCall } from '../internal/sdkCall.js';
2
+ const META_SDK_INTEGRATION_ID = '__meta__';
3
+ export class ZiteMeta {
4
+ static listUsers() {
5
+ return getSdkCall()(META_SDK_INTEGRATION_ID, 'ZiteMeta', 'listUsers', {});
6
+ }
7
+ }
8
+ export const Meta = ZiteMeta;
@@ -0,0 +1,25 @@
1
+ export interface NotificationLink {
2
+ path?: string;
3
+ params?: Record<string, string>;
4
+ }
5
+ export type NotificationsCreateParams = {
6
+ recipients: string[];
7
+ title: string;
8
+ body?: string;
9
+ link?: NotificationLink;
10
+ path?: string;
11
+ params?: Record<string, string>;
12
+ payload?: Record<string, unknown>;
13
+ idempotencyKey?: string;
14
+ };
15
+ export type NotificationsCreateResult = {
16
+ created: number;
17
+ } | {
18
+ created: 0;
19
+ preview: true;
20
+ wouldCreate: number;
21
+ };
22
+ export declare class ZiteNotifications {
23
+ static create(params: NotificationsCreateParams): Promise<NotificationsCreateResult>;
24
+ }
25
+ export declare const Notifications: typeof ZiteNotifications;
@@ -0,0 +1,8 @@
1
+ import { getSdkCall } from '../internal/sdkCall.js';
2
+ const NOTIFICATIONS_SDK_INTEGRATION_ID = '__notifications__';
3
+ export class ZiteNotifications {
4
+ static create(params) {
5
+ return getSdkCall()(NOTIFICATIONS_SDK_INTEGRATION_ID, 'ZiteNotifications', 'create', params);
6
+ }
7
+ }
8
+ export const Notifications = ZiteNotifications;
@@ -18,12 +18,12 @@ export declare function listSourceDirs(cwd?: string): SourceDir[];
18
18
  /** The source dir with this bare name, under whichever root holds it. */
19
19
  export declare function findSourceDir(name: string, cwd?: string): SourceDir | undefined;
20
20
  /**
21
- * An automation is one workflow: its single endpoint is `src/workflow.ts` and
21
+ * An automation is one workflow: its single endpoint is `src/index.ts` and
22
22
  * its name is fixed, so nothing about an automation is keyed on a file name
23
23
  * the way an app's `src/api/<name>.ts` endpoints are.
24
24
  */
25
- export declare const AUTOMATION_ENDPOINT_NAME = "workflow";
26
- export declare const AUTOMATION_WORKFLOW_FILE = "src/workflow.ts";
25
+ export declare const AUTOMATION_ENDPOINT_NAME = "index";
26
+ export declare const AUTOMATION_ENTRY_FILE = "src/index.ts";
27
27
  /** The root a source dir sits under, read from its parent directory. */
28
28
  export declare function sourceRootOf(sourceDirPath: string): SourceRoot | undefined;
29
29
  /** An endpoint's entry file, relative to the source dir. */
@@ -27,12 +27,12 @@ export function findSourceDir(name, cwd = ".") {
27
27
  return listSourceDirs(cwd).find((d) => d.dir === name);
28
28
  }
29
29
  /**
30
- * An automation is one workflow: its single endpoint is `src/workflow.ts` and
30
+ * An automation is one workflow: its single endpoint is `src/index.ts` and
31
31
  * its name is fixed, so nothing about an automation is keyed on a file name
32
32
  * the way an app's `src/api/<name>.ts` endpoints are.
33
33
  */
34
- export const AUTOMATION_ENDPOINT_NAME = "workflow";
35
- export const AUTOMATION_WORKFLOW_FILE = "src/workflow.ts";
34
+ export const AUTOMATION_ENDPOINT_NAME = "index";
35
+ export const AUTOMATION_ENTRY_FILE = "src/index.ts";
36
36
  /** The root a source dir sits under, read from its parent directory. */
37
37
  export function sourceRootOf(sourceDirPath) {
38
38
  const parent = basename(dirname(resolve(sourceDirPath)));
@@ -43,13 +43,13 @@ export function sourceRootOf(sourceDirPath) {
43
43
  /** An endpoint's entry file, relative to the source dir. */
44
44
  export function endpointEntryFile(root, name) {
45
45
  return root === "automations"
46
- ? AUTOMATION_WORKFLOW_FILE
46
+ ? AUTOMATION_ENTRY_FILE
47
47
  : join("src", "api", `${name}.ts`);
48
48
  }
49
49
  /** Every endpoint name a source dir has on disk. */
50
50
  export function listEndpointNames(root, sourceDirPath) {
51
51
  if (root === "automations") {
52
- return existsSync(join(sourceDirPath, AUTOMATION_WORKFLOW_FILE))
52
+ return existsSync(join(sourceDirPath, AUTOMATION_ENTRY_FILE))
53
53
  ? [AUTOMATION_ENDPOINT_NAME]
54
54
  : [];
55
55
  }
@@ -9,9 +9,9 @@ describe("source roots", () => {
9
9
  expect(sourceRootOf("/repo/automations/digest")).toBe("automations");
10
10
  expect(sourceRootOf("/repo/packages/ui")).toBeUndefined();
11
11
  });
12
- it("an app's endpoints are src/api/<name>.ts; an automation's one endpoint is src/workflow.ts", () => {
12
+ it("an app's endpoints are src/api/<name>.ts; an automation's one endpoint is src/index.ts", () => {
13
13
  expect(endpointEntryFile("apps", "listItems")).toBe("src/api/listItems.ts");
14
- expect(endpointEntryFile("automations", "workflow")).toBe("src/workflow.ts");
14
+ expect(endpointEntryFile("automations", "index")).toBe("src/index.ts");
15
15
  });
16
16
  it("lists what is on disk under each layout", () => {
17
17
  const repo = mkdtempSync(join(tmpdir(), "zitejs-roots-"));
@@ -20,9 +20,9 @@ describe("source roots", () => {
20
20
  writeFileSync(join(repo, "apps/crm/src/api/apiTypes.d.ts"), "");
21
21
  mkdirSync(join(repo, "automations/digest/src"), { recursive: true });
22
22
  mkdirSync(join(repo, "automations/empty/src"), { recursive: true });
23
- writeFileSync(join(repo, "automations/digest/src/workflow.ts"), "");
23
+ writeFileSync(join(repo, "automations/digest/src/index.ts"), "");
24
24
  expect(listEndpointNames("apps", join(repo, "apps/crm"))).toEqual(["listItems"]);
25
- expect(listEndpointNames("automations", join(repo, "automations/digest"))).toEqual(["workflow"]);
25
+ expect(listEndpointNames("automations", join(repo, "automations/digest"))).toEqual(["index"]);
26
26
  expect(listEndpointNames("automations", join(repo, "automations/empty"))).toEqual([]);
27
27
  });
28
28
  });
@@ -1411,7 +1411,7 @@ export function generateIntegrationsIndex(entries, opts) {
1411
1411
  "// Auto-generated by zitejs generate from zite.config.json. Do not edit manually.",
1412
1412
  "// zitejs/integrations — an integration's credentials by its zite.config.json entry name.",
1413
1413
  "//",
1414
- "// Usage in endpoints (src/api/*.ts) and automations (src/workflow.ts):",
1414
+ "// Usage in endpoints (src/api/*.ts) and automations (src/index.ts):",
1415
1415
  "// import { integrations } from 'zitejs/integrations';",
1416
1416
  "//",
1417
1417
  "// const crm = await integrations.get('<entry name>');",
@@ -460,7 +460,7 @@ describe('generateIntegrationsIndex', () => {
460
460
  "// Auto-generated by zitejs generate from zite.config.json. Do not edit manually.
461
461
  // zitejs/integrations — an integration's credentials by its zite.config.json entry name.
462
462
  //
463
- // Usage in endpoints (src/api/*.ts) and automations (src/workflow.ts):
463
+ // Usage in endpoints (src/api/*.ts) and automations (src/index.ts):
464
464
  // import { integrations } from 'zitejs/integrations';
465
465
  //
466
466
  // const crm = await integrations.get('<entry name>');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.125",
3
+ "version": "0.9.127",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "sideEffects": false,