zitejs 0.9.134 → 0.9.135

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.
@@ -7,8 +7,8 @@ export type ZiteWebhook = {
7
7
  * A provider trigger: what should fire the endpoint, declared as a literal.
8
8
  * The platform registers it with the provider at publish, verifies each
9
9
  * delivery and queues it; the endpoint only sees the payload. A `poll`
10
- * trigger runs the endpoint on a cadence with `input.__poll.cursor` from the
11
- * previous run, and the endpoint returns `{ cursor }` to advance it.
10
+ * trigger runs the endpoint on a cadence. Either way `execute` gets `trigger`
11
+ * ({@link ZiteTriggerContext}) to read the cursor and record what it handled.
12
12
  */
13
13
  export type ZiteTrigger = {
14
14
  /**
@@ -86,45 +86,39 @@ export type ZiteTriggerUnsubscribeInput = {
86
86
  };
87
87
  export type ZiteTriggerSubscribe = (input: ZiteTriggerSubscribeInput) => Promise<ZiteTriggerSubscribeResult> | ZiteTriggerSubscribeResult;
88
88
  export type ZiteTriggerUnsubscribe = (input: ZiteTriggerUnsubscribeInput) => Promise<void> | void;
89
- /**
90
- * What a poll-triggered run receives as `input.__poll`. Only on a fire from
91
- * the poll's schedule; a request call to the same endpoint carries none.
92
- */
93
- export type ZitePollInput = {
94
- /** What the previous successful run returned as `cursor`; null on the first run. */
95
- cursor: string | null;
96
- /** Ids earlier runs returned as `handled`, newest last. Skip these. */
97
- seen: string[];
98
- };
99
- /**
100
- * What a provider trigger's delivery carries as `input.__webhook`, beside the
101
- * provider's own body. `cursor` and `seen` work as they do for a poll: for a
102
- * provider whose notification only says that something changed, `subscribe`
103
- * returns a starting `cursor` and each delivery returns the next one. A
104
- * trigger that keeps a cursor gets one delivery at a time.
105
- */
106
- export type ZiteTriggerDelivery = {
89
+ /** Where a provider delivery came from; app event triggers get only this. */
90
+ export type ZiteTriggerDeliveryInfo = {
91
+ /** The delivery's headers, lowercased. */
107
92
  headers: Record<string, string>;
108
93
  /** The provider's id for this delivery; redeliveries share it. */
109
94
  deliveryId?: string;
110
95
  provider?: string;
111
- /** What `subscribe` or the previous successful delivery returned as `cursor`. */
112
- cursor: string | null;
113
- /** Ids earlier deliveries returned as `handled`, newest last. Skip these. */
114
- seen: string[];
115
96
  };
116
- /** What an app event trigger's run carries as `input.__webhook`: one event, no cursor. */
117
- export type ZiteAppEventDelivery = Omit<ZiteTriggerDelivery, "cursor" | "seen">;
118
97
  /**
119
- * What a poll-triggered run, or a provider trigger's delivery, returns for
120
- * the platform to keep: its new `cursor`, and the id of every item it acted
121
- * on, so the next run's `seen` includes them even if the cursor hands them
122
- * back.
98
+ * What a poll run, or a provider trigger's delivery, gets as `trigger`. Record
99
+ * each item acted on with `handled` as you go: it is kept even if the run
100
+ * later throws, so the next run skips it. The cursor staged with `advance` is
101
+ * committed only when the run succeeds.
123
102
  */
124
- export type ZitePollResult = {
125
- cursor?: string;
126
- handled?: string[];
103
+ export type ZiteTriggerRun = {
104
+ /** The last committed cursor: what `subscribe` returned, or the last successful run's `advance`. Null when there is none yet. */
105
+ cursor: string | null;
106
+ /** Whether an earlier run, or this one, recorded `id` as handled. */
107
+ seen(id: string): boolean;
108
+ /** Record an item as acted on (1 to 512 characters). */
109
+ handled(id: string): void;
110
+ /** Stage the next cursor (at most 4096 characters). The last call wins. */
111
+ advance(cursor: string): void;
127
112
  };
113
+ export type ZiteTriggerDeliveryRun = ZiteTriggerRun & ZiteTriggerDeliveryInfo;
114
+ /** The `trigger` param for an endpoint declaring `TTrigger`. */
115
+ export type ZiteTriggerContext<TTrigger> = TTrigger extends {
116
+ every: string;
117
+ } ? ZiteTriggerRun : TTrigger extends {
118
+ events: string[];
119
+ } ? ZiteTriggerDeliveryInfo : TTrigger extends {
120
+ integration: string;
121
+ } ? ZiteTriggerDeliveryRun : never;
128
122
  export interface ZiteRequestContext {
129
123
  user: {
130
124
  id: string;
@@ -221,22 +215,12 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
221
215
  */
222
216
  validatesInput?: boolean;
223
217
  execute: (params: {
224
- input: TTrigger extends {
225
- every: string;
226
- } ? TInput & {
227
- __poll?: ZitePollInput;
228
- } : TTrigger extends {
229
- events: string[];
230
- } ? TInput & {
231
- __webhook?: ZiteAppEventDelivery;
232
- } : TTrigger extends {
233
- integration: string;
234
- } ? TInput & {
235
- __webhook?: ZiteTriggerDelivery;
236
- } : TInput;
218
+ input: TInput;
237
219
  context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
238
220
  } & (TStream extends true ? {
239
221
  stream: ZiteStreamInterface;
222
+ } : {}) & (TTrigger extends ZiteTrigger ? {
223
+ trigger?: ZiteTriggerContext<TTrigger>;
240
224
  } : {})) => Promise<TOutput> | TOutput;
241
225
  }
242
226
  /**
@@ -74,6 +74,10 @@ function createEndpoint(config) {
74
74
  // request callers doesn't describe them.
75
75
  if (isPlatformTriggeredInput(params.input))
76
76
  return execute(params);
77
+ // A trigger's run carries the provider's body, not a request's input.
78
+ if (params.trigger !== undefined) {
79
+ return execute(params);
80
+ }
77
81
  // Same, for a fire with no input object to hold the sentinel: the runner
78
82
  // only merges `__cron` into a plain object.
79
83
  if (firesWithoutARequest && params.input === undefined) {
@@ -133,28 +133,51 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
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 () => {
136
+ (0, vitest_1.it)("hands a poll a typed trigger and skips the input schema for it", async () => {
137
+ const handled = [];
137
138
  const endpoint = (0, index_js_1.createEndpoint)({
138
- inputSchema: zod_1.z.object({}),
139
+ inputSchema: zod_1.z.object({ required: zod_1.z.string() }),
139
140
  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 };
141
+ execute: async ({ trigger }) => {
142
+ if (!trigger)
143
+ return { ran: 0 };
144
+ const fresh = ["a", "b"].filter((id) => !trigger.seen(id));
145
+ for (const id of fresh)
146
+ trigger.handled(id);
147
+ trigger.advance("c2");
148
+ return { ran: fresh.length, from: trigger.cursor };
144
149
  },
145
150
  });
151
+ const seen = new Set(["a"]);
146
152
  await (0, vitest_1.expect)(endpoint.execute({
147
- input: { __poll: { cursor: "c1", seen: ["a"] } },
153
+ input: {},
148
154
  context: { user: null },
149
- })).resolves.toEqual({ cursor: "c1", handled: ["b"] });
155
+ trigger: {
156
+ cursor: "c1",
157
+ seen: (id) => seen.has(id),
158
+ handled: (id) => void handled.push(id),
159
+ advance: () => { },
160
+ },
161
+ })).resolves.toEqual({ ran: 1, from: "c1" });
162
+ (0, vitest_1.expect)(handled).toEqual(["b"]);
150
163
  });
151
- (0, vitest_1.it)("gives __poll only to poll triggers", () => {
164
+ (0, vitest_1.it)("types trigger by the declared kind", () => {
152
165
  (0, index_js_1.createEndpoint)({
153
- inputSchema: zod_1.z.object({ action: zod_1.z.string() }),
154
166
  trigger: { integration: "github" },
155
- execute: async ({ input }) => {
156
- // @ts-expect-error a provider trigger's input has no __poll
157
- return input.__poll;
167
+ execute: async ({ trigger }) => trigger?.deliveryId ?? trigger?.cursor,
168
+ });
169
+ (0, index_js_1.createEndpoint)({
170
+ trigger: { integration: "team-slack", events: ["message.channels"] },
171
+ execute: async ({ trigger }) => {
172
+ // @ts-expect-error an app event has no cursor API
173
+ trigger?.advance("x");
174
+ return trigger?.headers;
175
+ },
176
+ });
177
+ (0, index_js_1.createEndpoint)({
178
+ execute: async (params) => {
179
+ // @ts-expect-error a plain endpoint gets no trigger
180
+ return params.trigger;
158
181
  },
159
182
  });
160
183
  });
@@ -184,40 +207,28 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
184
207
  endpointId: "onPush",
185
208
  });
186
209
  });
187
- (0, vitest_1.it)("hands a provider trigger's delivery its cursor and seen ids, typed", async () => {
210
+ (0, vitest_1.it)("hands a provider delivery its body as input and the cursor API on trigger", async () => {
188
211
  const endpoint = (0, index_js_1.createEndpoint)({
189
- inputSchema: zod_1.z.object({}),
212
+ inputSchema: zod_1.z.object({ required: zod_1.z.string() }),
190
213
  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
- },
214
+ execute: async ({ input, trigger }) => ({
215
+ body: input,
216
+ cursor: trigger?.cursor,
217
+ delivery: trigger?.deliveryId,
218
+ }),
196
219
  });
197
220
  await (0, vitest_1.expect)(endpoint.execute({
198
- input: {
199
- __webhook: { headers: {}, cursor: "sync1", seen: ["e1"] },
200
- },
221
+ input: { kind: "sync" },
201
222
  context: { user: null },
202
- })).resolves.toEqual({ cursor: "sync1+1", handled: ["e2"] });
203
- });
204
- (0, vitest_1.it)("types an app event trigger's run as one event, with no cursor to keep", async () => {
205
- const endpoint = (0, index_js_1.createEndpoint)({
206
- inputSchema: zod_1.z.object({ event: zod_1.z.object({ text: zod_1.z.string() }) }),
207
- trigger: { integration: "team-slack", events: ["message.channels"] },
208
- execute: async ({ input }) => {
209
- // @ts-expect-error an app event delivery has no cursor
210
- void input.__webhook?.cursor;
211
- return { id: input.__webhook?.deliveryId ?? null };
212
- },
213
- });
214
- await (0, vitest_1.expect)(endpoint.execute({
215
- input: {
216
- event: { text: "hi" },
217
- __webhook: { headers: {}, deliveryId: "slack:Ev1" },
223
+ trigger: {
224
+ cursor: "sync1",
225
+ seen: () => false,
226
+ handled: () => { },
227
+ advance: () => { },
228
+ headers: {},
229
+ deliveryId: "chan:2",
218
230
  },
219
- context: { user: null },
220
- })).resolves.toEqual({ id: "slack:Ev1" });
231
+ })).resolves.toEqual({ body: { kind: "sync" }, cursor: "sync1", delivery: "chan:2" });
221
232
  });
222
233
  (0, vitest_1.it)("refuses events on a trigger that also polls", () => {
223
234
  (0, index_js_1.createEndpoint)({
@@ -1232,13 +1232,13 @@ function generateBackendWrapperTs(envVarNames = []) {
1232
1232
  "// Auto-generated type-narrowing wrapper. Do not edit manually.",
1233
1233
  "// Re-exports createEndpoint with context.user typed to the app User.",
1234
1234
  "",
1235
- "import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger, ZiteTriggerDelivery, ZiteAppEventDelivery, ZitePollInput, ZiteWebhook } from 'zitejs/backend/base';",
1235
+ "import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteWebhook } from 'zitejs/backend/base';",
1236
1236
  "import type { User } from 'zitejs/auth';",
1237
1237
  "",
1238
1238
  "export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteTrigger, ZiteWebhook };",
1239
1239
  // A trigger's registration exports are typed against these, and they
1240
1240
  // only reach app code through this file.
1241
- "export type { ZiteAppEventDelivery, ZitePollInput, ZitePollResult, ZiteTriggerDelivery, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput } from 'zitejs/backend/base';",
1241
+ "export type { ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput } from 'zitejs/backend/base';",
1242
1242
  // The pre-monorepo SDK put this in scope for every endpoint, so migrated
1243
1243
  // code can name it. `createEndpoint` infers the same thing without it.
1244
1244
  "export type InferSchemaType<T> = T extends { _output: infer U } ? U : T;",
@@ -1330,11 +1330,10 @@ function generateBackendWrapperTs(envVarNames = []) {
1330
1330
  " trigger?: TTrigger;",
1331
1331
  " execute: (",
1332
1332
  " params: {",
1333
- // Mirrors the base module: a poll's run carries `__poll`, a provider
1334
- // trigger's delivery `__webhook`.
1335
- " input: TTrigger extends { every: string } ? TInput & { __poll?: ZitePollInput } : TTrigger extends { events: string[] } ? TInput & { __webhook?: ZiteAppEventDelivery } : TTrigger extends { integration: string } ? TInput & { __webhook?: ZiteTriggerDelivery } : TInput;",
1333
+ " input: TInput;",
1336
1334
  " context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
1337
- " } & (TStream extends true ? { stream: ZiteStreamInterface } : {}),",
1335
+ // Mirrors the base module: a trigger's run gets `trigger`.
1336
+ " } & (TStream extends true ? { stream: ZiteStreamInterface } : {}) & (TTrigger extends ZiteTrigger ? { trigger?: ZiteTriggerContext<TTrigger> } : {}),",
1338
1337
  " ) => Promise<TOutput> | TOutput;",
1339
1338
  "}",
1340
1339
  "",
@@ -390,9 +390,9 @@ const duplicateIdentifierErrorsIn = (source) => {
390
390
  (0, vitest_1.it)('passes the trigger registration types and trigger inputs through to app code', () => {
391
391
  (0, vitest_1.expect)(output).toContain('ZiteTriggerSubscribe, ZiteTriggerSubscribeInput');
392
392
  (0, vitest_1.expect)(output).toContain('ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput');
393
- (0, vitest_1.expect)(output).toContain("TTrigger extends { every: string } ? TInput & { __poll?: ZitePollInput }");
394
- (0, vitest_1.expect)(output).toContain('TInput & { __webhook?: ZiteTriggerDelivery }');
395
- (0, vitest_1.expect)(output).toContain("TTrigger extends { events: string[] } ? TInput & { __webhook?: ZiteAppEventDelivery }");
393
+ (0, vitest_1.expect)(output).toContain('ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun');
394
+ (0, vitest_1.expect)(output).toContain('(TTrigger extends ZiteTrigger ? { trigger?: ZiteTriggerContext<TTrigger> } : {})');
395
+ (0, vitest_1.expect)(output).not.toContain('__poll');
396
396
  (0, vitest_1.expect)(output).not.toContain('src/triggers/');
397
397
  });
398
398
  (0, vitest_1.it)('exports ZiteError', () => {
@@ -7,8 +7,8 @@ export type ZiteWebhook = {
7
7
  * A provider trigger: what should fire the endpoint, declared as a literal.
8
8
  * The platform registers it with the provider at publish, verifies each
9
9
  * delivery and queues it; the endpoint only sees the payload. A `poll`
10
- * trigger runs the endpoint on a cadence with `input.__poll.cursor` from the
11
- * previous run, and the endpoint returns `{ cursor }` to advance it.
10
+ * trigger runs the endpoint on a cadence. Either way `execute` gets `trigger`
11
+ * ({@link ZiteTriggerContext}) to read the cursor and record what it handled.
12
12
  */
13
13
  export type ZiteTrigger = {
14
14
  /**
@@ -86,45 +86,39 @@ export type ZiteTriggerUnsubscribeInput = {
86
86
  };
87
87
  export type ZiteTriggerSubscribe = (input: ZiteTriggerSubscribeInput) => Promise<ZiteTriggerSubscribeResult> | ZiteTriggerSubscribeResult;
88
88
  export type ZiteTriggerUnsubscribe = (input: ZiteTriggerUnsubscribeInput) => Promise<void> | void;
89
- /**
90
- * What a poll-triggered run receives as `input.__poll`. Only on a fire from
91
- * the poll's schedule; a request call to the same endpoint carries none.
92
- */
93
- export type ZitePollInput = {
94
- /** What the previous successful run returned as `cursor`; null on the first run. */
95
- cursor: string | null;
96
- /** Ids earlier runs returned as `handled`, newest last. Skip these. */
97
- seen: string[];
98
- };
99
- /**
100
- * What a provider trigger's delivery carries as `input.__webhook`, beside the
101
- * provider's own body. `cursor` and `seen` work as they do for a poll: for a
102
- * provider whose notification only says that something changed, `subscribe`
103
- * returns a starting `cursor` and each delivery returns the next one. A
104
- * trigger that keeps a cursor gets one delivery at a time.
105
- */
106
- export type ZiteTriggerDelivery = {
89
+ /** Where a provider delivery came from; app event triggers get only this. */
90
+ export type ZiteTriggerDeliveryInfo = {
91
+ /** The delivery's headers, lowercased. */
107
92
  headers: Record<string, string>;
108
93
  /** The provider's id for this delivery; redeliveries share it. */
109
94
  deliveryId?: string;
110
95
  provider?: string;
111
- /** What `subscribe` or the previous successful delivery returned as `cursor`. */
112
- cursor: string | null;
113
- /** Ids earlier deliveries returned as `handled`, newest last. Skip these. */
114
- seen: string[];
115
96
  };
116
- /** What an app event trigger's run carries as `input.__webhook`: one event, no cursor. */
117
- export type ZiteAppEventDelivery = Omit<ZiteTriggerDelivery, "cursor" | "seen">;
118
97
  /**
119
- * What a poll-triggered run, or a provider trigger's delivery, returns for
120
- * the platform to keep: its new `cursor`, and the id of every item it acted
121
- * on, so the next run's `seen` includes them even if the cursor hands them
122
- * back.
98
+ * What a poll run, or a provider trigger's delivery, gets as `trigger`. Record
99
+ * each item acted on with `handled` as you go: it is kept even if the run
100
+ * later throws, so the next run skips it. The cursor staged with `advance` is
101
+ * committed only when the run succeeds.
123
102
  */
124
- export type ZitePollResult = {
125
- cursor?: string;
126
- handled?: string[];
103
+ export type ZiteTriggerRun = {
104
+ /** The last committed cursor: what `subscribe` returned, or the last successful run's `advance`. Null when there is none yet. */
105
+ cursor: string | null;
106
+ /** Whether an earlier run, or this one, recorded `id` as handled. */
107
+ seen(id: string): boolean;
108
+ /** Record an item as acted on (1 to 512 characters). */
109
+ handled(id: string): void;
110
+ /** Stage the next cursor (at most 4096 characters). The last call wins. */
111
+ advance(cursor: string): void;
127
112
  };
113
+ export type ZiteTriggerDeliveryRun = ZiteTriggerRun & ZiteTriggerDeliveryInfo;
114
+ /** The `trigger` param for an endpoint declaring `TTrigger`. */
115
+ export type ZiteTriggerContext<TTrigger> = TTrigger extends {
116
+ every: string;
117
+ } ? ZiteTriggerRun : TTrigger extends {
118
+ events: string[];
119
+ } ? ZiteTriggerDeliveryInfo : TTrigger extends {
120
+ integration: string;
121
+ } ? ZiteTriggerDeliveryRun : never;
128
122
  export interface ZiteRequestContext {
129
123
  user: {
130
124
  id: string;
@@ -221,22 +215,12 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
221
215
  */
222
216
  validatesInput?: boolean;
223
217
  execute: (params: {
224
- input: TTrigger extends {
225
- every: string;
226
- } ? TInput & {
227
- __poll?: ZitePollInput;
228
- } : TTrigger extends {
229
- events: string[];
230
- } ? TInput & {
231
- __webhook?: ZiteAppEventDelivery;
232
- } : TTrigger extends {
233
- integration: string;
234
- } ? TInput & {
235
- __webhook?: ZiteTriggerDelivery;
236
- } : TInput;
218
+ input: TInput;
237
219
  context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
238
220
  } & (TStream extends true ? {
239
221
  stream: ZiteStreamInterface;
222
+ } : {}) & (TTrigger extends ZiteTrigger ? {
223
+ trigger?: ZiteTriggerContext<TTrigger>;
240
224
  } : {})) => Promise<TOutput> | TOutput;
241
225
  }
242
226
  /**
@@ -69,6 +69,10 @@ export function createEndpoint(config) {
69
69
  // request callers doesn't describe them.
70
70
  if (isPlatformTriggeredInput(params.input))
71
71
  return execute(params);
72
+ // A trigger's run carries the provider's body, not a request's input.
73
+ if (params.trigger !== undefined) {
74
+ return execute(params);
75
+ }
72
76
  // Same, for a fire with no input object to hold the sentinel: the runner
73
77
  // only merges `__cron` into a plain object.
74
78
  if (firesWithoutARequest && params.input === undefined) {
@@ -131,28 +131,51 @@ 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 () => {
134
+ it("hands a poll a typed trigger and skips the input schema for it", async () => {
135
+ const handled = [];
135
136
  const endpoint = createEndpoint({
136
- inputSchema: z.object({}),
137
+ inputSchema: z.object({ required: z.string() }),
137
138
  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 };
139
+ execute: async ({ trigger }) => {
140
+ if (!trigger)
141
+ return { ran: 0 };
142
+ const fresh = ["a", "b"].filter((id) => !trigger.seen(id));
143
+ for (const id of fresh)
144
+ trigger.handled(id);
145
+ trigger.advance("c2");
146
+ return { ran: fresh.length, from: trigger.cursor };
142
147
  },
143
148
  });
149
+ const seen = new Set(["a"]);
144
150
  await expect(endpoint.execute({
145
- input: { __poll: { cursor: "c1", seen: ["a"] } },
151
+ input: {},
146
152
  context: { user: null },
147
- })).resolves.toEqual({ cursor: "c1", handled: ["b"] });
153
+ trigger: {
154
+ cursor: "c1",
155
+ seen: (id) => seen.has(id),
156
+ handled: (id) => void handled.push(id),
157
+ advance: () => { },
158
+ },
159
+ })).resolves.toEqual({ ran: 1, from: "c1" });
160
+ expect(handled).toEqual(["b"]);
148
161
  });
149
- it("gives __poll only to poll triggers", () => {
162
+ it("types trigger by the declared kind", () => {
150
163
  createEndpoint({
151
- inputSchema: z.object({ action: z.string() }),
152
164
  trigger: { integration: "github" },
153
- execute: async ({ input }) => {
154
- // @ts-expect-error a provider trigger's input has no __poll
155
- return input.__poll;
165
+ execute: async ({ trigger }) => trigger?.deliveryId ?? trigger?.cursor,
166
+ });
167
+ createEndpoint({
168
+ trigger: { integration: "team-slack", events: ["message.channels"] },
169
+ execute: async ({ trigger }) => {
170
+ // @ts-expect-error an app event has no cursor API
171
+ trigger?.advance("x");
172
+ return trigger?.headers;
173
+ },
174
+ });
175
+ createEndpoint({
176
+ execute: async (params) => {
177
+ // @ts-expect-error a plain endpoint gets no trigger
178
+ return params.trigger;
156
179
  },
157
180
  });
158
181
  });
@@ -182,40 +205,28 @@ describe("platform-triggered runs", () => {
182
205
  endpointId: "onPush",
183
206
  });
184
207
  });
185
- it("hands a provider trigger's delivery its cursor and seen ids, typed", async () => {
208
+ it("hands a provider delivery its body as input and the cursor API on trigger", async () => {
186
209
  const endpoint = createEndpoint({
187
- inputSchema: z.object({}),
210
+ inputSchema: z.object({ required: z.string() }),
188
211
  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
- },
212
+ execute: async ({ input, trigger }) => ({
213
+ body: input,
214
+ cursor: trigger?.cursor,
215
+ delivery: trigger?.deliveryId,
216
+ }),
194
217
  });
195
218
  await expect(endpoint.execute({
196
- input: {
197
- __webhook: { headers: {}, cursor: "sync1", seen: ["e1"] },
198
- },
219
+ input: { kind: "sync" },
199
220
  context: { user: null },
200
- })).resolves.toEqual({ cursor: "sync1+1", handled: ["e2"] });
201
- });
202
- it("types an app event trigger's run as one event, with no cursor to keep", async () => {
203
- const endpoint = createEndpoint({
204
- inputSchema: z.object({ event: z.object({ text: z.string() }) }),
205
- trigger: { integration: "team-slack", events: ["message.channels"] },
206
- execute: async ({ input }) => {
207
- // @ts-expect-error an app event delivery has no cursor
208
- void input.__webhook?.cursor;
209
- return { id: input.__webhook?.deliveryId ?? null };
210
- },
211
- });
212
- await expect(endpoint.execute({
213
- input: {
214
- event: { text: "hi" },
215
- __webhook: { headers: {}, deliveryId: "slack:Ev1" },
221
+ trigger: {
222
+ cursor: "sync1",
223
+ seen: () => false,
224
+ handled: () => { },
225
+ advance: () => { },
226
+ headers: {},
227
+ deliveryId: "chan:2",
216
228
  },
217
- context: { user: null },
218
- })).resolves.toEqual({ id: "slack:Ev1" });
229
+ })).resolves.toEqual({ body: { kind: "sync" }, cursor: "sync1", delivery: "chan:2" });
219
230
  });
220
231
  it("refuses events on a trigger that also polls", () => {
221
232
  createEndpoint({
@@ -1217,13 +1217,13 @@ export function generateBackendWrapperTs(envVarNames = []) {
1217
1217
  "// Auto-generated type-narrowing wrapper. Do not edit manually.",
1218
1218
  "// Re-exports createEndpoint with context.user typed to the app User.",
1219
1219
  "",
1220
- "import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger, ZiteTriggerDelivery, ZiteAppEventDelivery, ZitePollInput, ZiteWebhook } from 'zitejs/backend/base';",
1220
+ "import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteWebhook } from 'zitejs/backend/base';",
1221
1221
  "import type { User } from 'zitejs/auth';",
1222
1222
  "",
1223
1223
  "export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteTrigger, ZiteWebhook };",
1224
1224
  // A trigger's registration exports are typed against these, and they
1225
1225
  // only reach app code through this file.
1226
- "export type { ZiteAppEventDelivery, ZitePollInput, ZitePollResult, ZiteTriggerDelivery, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput } from 'zitejs/backend/base';",
1226
+ "export type { ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput } from 'zitejs/backend/base';",
1227
1227
  // The pre-monorepo SDK put this in scope for every endpoint, so migrated
1228
1228
  // code can name it. `createEndpoint` infers the same thing without it.
1229
1229
  "export type InferSchemaType<T> = T extends { _output: infer U } ? U : T;",
@@ -1315,11 +1315,10 @@ export function generateBackendWrapperTs(envVarNames = []) {
1315
1315
  " trigger?: TTrigger;",
1316
1316
  " execute: (",
1317
1317
  " params: {",
1318
- // Mirrors the base module: a poll's run carries `__poll`, a provider
1319
- // trigger's delivery `__webhook`.
1320
- " input: TTrigger extends { every: string } ? TInput & { __poll?: ZitePollInput } : TTrigger extends { events: string[] } ? TInput & { __webhook?: ZiteAppEventDelivery } : TTrigger extends { integration: string } ? TInput & { __webhook?: ZiteTriggerDelivery } : TInput;",
1318
+ " input: TInput;",
1321
1319
  " context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
1322
- " } & (TStream extends true ? { stream: ZiteStreamInterface } : {}),",
1320
+ // Mirrors the base module: a trigger's run gets `trigger`.
1321
+ " } & (TStream extends true ? { stream: ZiteStreamInterface } : {}) & (TTrigger extends ZiteTrigger ? { trigger?: ZiteTriggerContext<TTrigger> } : {}),",
1323
1322
  " ) => Promise<TOutput> | TOutput;",
1324
1323
  "}",
1325
1324
  "",
@@ -385,9 +385,9 @@ describe('generateBackendWrapperTs', () => {
385
385
  it('passes the trigger registration types and trigger inputs through to app code', () => {
386
386
  expect(output).toContain('ZiteTriggerSubscribe, ZiteTriggerSubscribeInput');
387
387
  expect(output).toContain('ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput');
388
- expect(output).toContain("TTrigger extends { every: string } ? TInput & { __poll?: ZitePollInput }");
389
- expect(output).toContain('TInput & { __webhook?: ZiteTriggerDelivery }');
390
- expect(output).toContain("TTrigger extends { events: string[] } ? TInput & { __webhook?: ZiteAppEventDelivery }");
388
+ expect(output).toContain('ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun');
389
+ expect(output).toContain('(TTrigger extends ZiteTrigger ? { trigger?: ZiteTriggerContext<TTrigger> } : {})');
390
+ expect(output).not.toContain('__poll');
391
391
  expect(output).not.toContain('src/triggers/');
392
392
  });
393
393
  it('exports ZiteError', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.134",
3
+ "version": "0.9.135",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "sideEffects": false,