zitejs 0.10.0-start.6 → 0.10.0-start.7

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.
Files changed (46) hide show
  1. package/dist/cjs/backend/index.d.ts +115 -3
  2. package/dist/cjs/backend/index.test.js +58 -0
  3. package/dist/cjs/integrations/rules.d.ts +13 -0
  4. package/dist/cjs/integrations/rules.js +29 -0
  5. package/dist/cjs/integrations/rules.test.js +1 -0
  6. package/dist/cjs/sync/lib.js +6 -6
  7. package/dist/esm/backend/index.d.ts +115 -3
  8. package/dist/esm/backend/index.test.js +58 -0
  9. package/dist/esm/integrations/rules.d.ts +13 -0
  10. package/dist/esm/integrations/rules.js +29 -0
  11. package/dist/esm/integrations/rules.test.js +1 -0
  12. package/dist/esm/start/authHooks.d.ts +7 -0
  13. package/dist/esm/start/authHooks.js +10 -0
  14. package/dist/esm/start/authHooksModule.d.ts +1 -0
  15. package/dist/esm/start/authHooksModule.js +3 -0
  16. package/dist/esm/start/clientErrorMessage.test.d.ts +1 -0
  17. package/dist/esm/start/clientErrorMessage.test.js +105 -0
  18. package/dist/esm/start/describe.d.ts +17 -1
  19. package/dist/esm/start/describe.js +27 -5
  20. package/dist/esm/start/errors.d.ts +46 -0
  21. package/dist/esm/start/errors.js +99 -0
  22. package/dist/esm/start/errors.test.d.ts +1 -0
  23. package/dist/esm/start/errors.test.js +23 -0
  24. package/dist/esm/start/index.d.ts +3 -19
  25. package/dist/esm/start/index.js +28 -36
  26. package/dist/esm/start/instance.d.ts +1 -2
  27. package/dist/esm/start/instance.js +11 -4
  28. package/dist/esm/start/previewClaim.d.ts +10 -0
  29. package/dist/esm/start/previewClaim.js +32 -0
  30. package/dist/esm/start/redactSecrets.d.ts +7 -0
  31. package/dist/esm/start/redactSecrets.js +131 -0
  32. package/dist/esm/start/redactSecrets.test.d.ts +1 -0
  33. package/dist/esm/start/redactSecrets.test.js +151 -0
  34. package/dist/esm/start/router.d.ts +6 -3
  35. package/dist/esm/start/router.js +47 -7
  36. package/dist/esm/start/runLog.d.ts +7 -4
  37. package/dist/esm/start/runLog.js +64 -6
  38. package/dist/esm/start/server.d.ts +7 -3
  39. package/dist/esm/start/server.js +74 -21
  40. package/dist/esm/start/types.d.ts +28 -3
  41. package/dist/esm/start/version.d.ts +1 -0
  42. package/dist/esm/start/version.js +2 -0
  43. package/dist/esm/start/vite.js +31 -3
  44. package/dist/esm/start/workflowRun.js +2 -1
  45. package/dist/esm/sync/lib.js +6 -6
  46. package/package.json +16 -8
@@ -23,6 +23,8 @@ export type ZiteTrigger = {
23
23
  paused?: boolean;
24
24
  every?: never;
25
25
  events?: never;
26
+ table?: never;
27
+ form?: never;
26
28
  } | {
27
29
  /**
28
30
  * The connection's name in `zite.config.json`, for a provider whose
@@ -37,6 +39,8 @@ export type ZiteTrigger = {
37
39
  label?: string;
38
40
  paused?: boolean;
39
41
  every?: never;
42
+ table?: never;
43
+ form?: never;
40
44
  } | {
41
45
  /** `<n>m` (1–59) or `<n>h` (1–23). What makes the trigger a poll. */
42
46
  every: string;
@@ -46,6 +50,78 @@ export type ZiteTrigger = {
46
50
  label?: string;
47
51
  paused?: boolean;
48
52
  events?: never;
53
+ table?: never;
54
+ form?: never;
55
+ } | {
56
+ /**
57
+ * A table in the project's own database, as `zite.<table>` names it.
58
+ * Every write to it reaches the trigger, from apps, forms, imports, the
59
+ * database view and other automations; nothing is registered.
60
+ */
61
+ table: string;
62
+ on: ZiteDatabaseTriggerEvent[];
63
+ /** Narrows `"updated"` to updates that changed one of these fields. */
64
+ fields?: string[];
65
+ /** The sentence the editor shows, e.g. "When an order ships". */
66
+ label?: string;
67
+ paused?: boolean;
68
+ integration?: never;
69
+ every?: never;
70
+ events?: never;
71
+ form?: never;
72
+ } | {
73
+ /**
74
+ * One of the project's own Fillout forms, by its name as the project
75
+ * lists it, or its id. Each finished live submission fires the
76
+ * endpoint; nothing is registered.
77
+ */
78
+ form: string;
79
+ on: "submitted"[];
80
+ /** The sentence the editor shows, e.g. "When someone fills in the intake form". */
81
+ label?: string;
82
+ paused?: boolean;
83
+ integration?: never;
84
+ every?: never;
85
+ events?: never;
86
+ table?: never;
87
+ };
88
+ /**
89
+ * What a form trigger's run gets as `input`: one submission. `answers` is
90
+ * keyed by question name, `answersById` by question id; `submission` is the
91
+ * whole export, as the Fillout API returns it (`questions`, `calculations`,
92
+ * `urlParameters`, …). Two questions with the same label are named
93
+ * `Email` and `Email (1)` in an order the form doesn't show, so read those by
94
+ * id.
95
+ */
96
+ export type ZiteFormTriggerInput = {
97
+ event: "submitted";
98
+ form: string;
99
+ formId: string;
100
+ submissionId: string;
101
+ submittedAt: string | null;
102
+ answers: Record<string, unknown>;
103
+ answersById: Record<string, unknown>;
104
+ submission: Record<string, unknown> & {
105
+ submissionId: string;
106
+ };
107
+ };
108
+ export type ZiteDatabaseTriggerEvent = "created" | "updated" | "deleted";
109
+ /**
110
+ * What a database trigger's run gets as `input`: one changed record. Field
111
+ * keys are the SDK's, as `zite.<table>.findOne` returns them; cast `record`
112
+ * and `previous` to the table's generated `<Table>RecordType` from
113
+ * `zitejs/db` to type their fields.
114
+ */
115
+ export type ZiteDatabaseTriggerInput<TRecord extends Record<string, unknown> = Record<string, unknown>> = {
116
+ event: ZiteDatabaseTriggerEvent;
117
+ table: string;
118
+ recordId: string;
119
+ /** Null if it was deleted before the run read it; on `"deleted"`, the record as it was. */
120
+ record: TRecord | null;
121
+ /** The record before an `"updated"`; null otherwise, and on a very large bulk write. */
122
+ previous: TRecord | null;
123
+ /** What an `"updated"` changed. */
124
+ changedFields: string[];
49
125
  };
50
126
  /**
51
127
  * What a provider trigger's `subscribe` export receives. Write it as
@@ -115,10 +191,20 @@ export type ZiteTriggerDeliveryRun = ZiteTriggerRun & ZiteTriggerDeliveryInfo;
115
191
  export type ZiteTriggerContext<TTrigger> = TTrigger extends {
116
192
  every: string;
117
193
  } ? ZiteTriggerRun : TTrigger extends {
194
+ table: string;
195
+ } | {
196
+ form: string;
197
+ } ? ZiteTriggerDeliveryInfo : TTrigger extends {
118
198
  events: string[];
119
199
  } ? ZiteTriggerDeliveryInfo : TTrigger extends {
120
200
  integration: string;
121
201
  } ? ZiteTriggerDeliveryRun : never;
202
+ /** A database or form trigger's runs get the change or submission as `input`. */
203
+ export type ZiteEndpointInput<TInput, TTrigger> = TTrigger extends {
204
+ table: string;
205
+ } ? ZiteDatabaseTriggerInput : TTrigger extends {
206
+ form: string;
207
+ } ? ZiteFormTriggerInput : TInput;
122
208
  export interface ZiteRequestContext {
123
209
  user: {
124
210
  id: string;
@@ -215,13 +301,15 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
215
301
  */
216
302
  validatesInput?: boolean;
217
303
  execute: (params: {
218
- input: TInput;
304
+ input: ZiteEndpointInput<TInput, TTrigger>;
219
305
  /**
220
306
  * Durable steps. `step.run(name, fn)` runs `fn` once per logical run:
221
307
  * its result is saved before anything after it executes, and when a
222
308
  * failed run is resumed, completed steps return their saved results
223
309
  * instead of re-running their side effects. Names must be unique within
224
- * a run, and results must be plain JSON data under 256KB.
310
+ * a run, and results must be plain JSON data under 256KB. In a run
311
+ * fired by a schedule, webhook or trigger, `step.sleep` pauses the run
312
+ * and a failed step is tried again.
225
313
  */
226
314
  step: ZiteStep;
227
315
  context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
@@ -240,8 +328,32 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
240
328
  * it would matter, and give a step inside a loop the item's id in its name.
241
329
  */
242
330
  export interface ZiteStep {
243
- run<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
331
+ /**
332
+ * In a run fired by a schedule, webhook or trigger, a step that throws is
333
+ * tried again `retries` more times (default 3, at most 10), 30 seconds,
334
+ * 2 minutes, then 10 minutes apart. Elsewhere its error fails the request.
335
+ */
336
+ run<T>(name: string, fn: () => Promise<T> | T, options?: ZiteStepOptions): Promise<T>;
337
+ /**
338
+ * Pause the run for `duration`: `"30s"`, `"15m"`, `"2h"`, `"3d"`, `"1w"`,
339
+ * or milliseconds, at most 30 days. Only a run fired by a schedule, webhook
340
+ * or trigger can wait; an editor test run goes straight past, and a request
341
+ * from the app throws.
342
+ *
343
+ * The run continues in a later call, which runs `execute` again from the
344
+ * top: finished steps return their saved results, and code outside a step
345
+ * runs again, so keep side effects inside steps.
346
+ */
347
+ sleep(name: string, duration: ZiteStepDuration): Promise<void>;
348
+ /** Pause the run until `when`, at most 30 days ahead. See {@link ZiteStep.sleep}. */
349
+ sleepUntil(name: string, when: Date | string): Promise<void>;
350
+ }
351
+ export interface ZiteStepOptions {
352
+ /** How many more times to try the step after it throws: 0 to 10, default 3. */
353
+ retries?: number;
244
354
  }
355
+ /** Milliseconds, or a number with a unit: `ms`, `s`, `m`, `h`, `d` or `w`. */
356
+ export type ZiteStepDuration = number | `${number}${"ms" | "s" | "m" | "h" | "d" | "w"}`;
245
357
  /**
246
358
  * Create an endpoint definition, parsing `input` against `inputSchema` before
247
359
  * `execute` runs. Nothing downstream parses it — the worker calls `execute` on
@@ -8,6 +8,8 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
8
8
  // Direct execute() calls need the step surface the runtime always provides.
9
9
  const stepStub = {
10
10
  run: async (_name, fn) => await fn(),
11
+ sleep: async () => { },
12
+ sleepUntil: async () => { },
11
13
  };
12
14
  (0, vitest_1.afterEach)(() => {
13
15
  vitest_1.vi.unstubAllGlobals();
@@ -207,6 +209,62 @@ const stepStub = {
207
209
  return params.trigger;
208
210
  },
209
211
  });
212
+ (0, index_js_1.createEndpoint)({
213
+ trigger: { table: "orders", on: ["updated"], fields: ["status"] },
214
+ execute: async ({ input, trigger }) => {
215
+ // @ts-expect-error a database change has no cursor API
216
+ trigger?.advance("x");
217
+ const status = input.record?.status;
218
+ return { status, changed: input.changedFields, at: input.event };
219
+ },
220
+ });
221
+ (0, index_js_1.createEndpoint)({
222
+ // @ts-expect-error a database trigger names its table, not a connection
223
+ trigger: { table: "orders", on: ["created"], integration: "x" },
224
+ execute: async () => 1,
225
+ });
226
+ (0, index_js_1.createEndpoint)({
227
+ trigger: { form: "Intake form", on: ["submitted"] },
228
+ execute: async ({ input, trigger }) => {
229
+ // @ts-expect-error a form submission has no cursor API
230
+ trigger?.advance("x");
231
+ return {
232
+ email: input.answers.Email,
233
+ workEmail: input.answersById["wWorkEmail"],
234
+ id: input.submissionId,
235
+ };
236
+ },
237
+ });
238
+ (0, index_js_1.createEndpoint)({
239
+ // @ts-expect-error a form trigger watches submissions only
240
+ trigger: { form: "Intake form", on: ["updated"] },
241
+ execute: async () => 1,
242
+ });
243
+ (0, index_js_1.createEndpoint)({
244
+ // @ts-expect-error only created, updated and deleted
245
+ trigger: { table: "orders", on: ["archived"] },
246
+ execute: async () => 1,
247
+ });
248
+ });
249
+ (0, vitest_1.it)("hands a database trigger its change without parsing it against the input schema", async () => {
250
+ const endpoint = (0, index_js_1.createEndpoint)({
251
+ inputSchema: zod_1.z.object({ orderId: zod_1.z.string() }),
252
+ trigger: { table: "orders", on: ["created"] },
253
+ execute: async ({ input }) => input.recordId,
254
+ });
255
+ await (0, vitest_1.expect)(endpoint.execute({
256
+ step: stepStub,
257
+ input: {
258
+ event: "created",
259
+ table: "orders",
260
+ recordId: "rec_1",
261
+ record: { id: "rec_1" },
262
+ previous: null,
263
+ changedFields: [],
264
+ },
265
+ context: { user: null },
266
+ trigger: { headers: {}, deliveryId: "db:e1:rec_1" },
267
+ })).resolves.toBe("rec_1");
210
268
  });
211
269
  (0, vitest_1.it)("types a trigger's registration exports and checks what subscribe returns", async () => {
212
270
  const subscribe = (async (input) => ({
@@ -38,11 +38,24 @@ declare const FIELDS_BY_TYPE: {
38
38
  jira: readonly string[];
39
39
  typeform: readonly string[];
40
40
  apollo: readonly string[];
41
+ clay: readonly string[];
41
42
  attio: readonly string[];
42
43
  granola: readonly string[];
44
+ clickup: readonly string[];
43
45
  dropbox: readonly string[];
46
+ zendesk: readonly string[];
47
+ smartsheet: readonly string[];
44
48
  amazonS3: readonly string[];
45
49
  snowflake: readonly string[];
50
+ databricks: readonly string[];
51
+ confluence: readonly string[];
52
+ pipedrive: readonly string[];
53
+ shopify: readonly string[];
54
+ sharepoint: readonly string[];
55
+ excel: readonly string[];
56
+ x: readonly string[];
57
+ zoomMeetings: readonly string[];
58
+ smartsuite: readonly string[];
46
59
  };
47
60
  /** A service type `integrations.get` serves: one used through its npm package with a token. */
48
61
  export type CredentialServiceType = keyof typeof FIELDS_BY_TYPE;
@@ -47,15 +47,43 @@ const FIELDS_BY_TYPE = {
47
47
  jira: extras("cloudId", "siteUrl"),
48
48
  typeform: extras(),
49
49
  apollo: extras(),
50
+ clay: extras(),
50
51
  attio: extras(),
51
52
  granola: extras(),
53
+ clickup: extras(),
52
54
  dropbox: extras(),
55
+ // Every path is https://{subdomain}.zendesk.com/api/v2/..., and a token is
56
+ // only valid on the account it was issued for, so the subdomain travels with
57
+ // it rather than being configured per app.
58
+ zendesk: extras("subdomain"),
59
+ // Smartsheet's US, EU, AU and Gov regions are separate, and a token is only
60
+ // valid on its own region's host, so the host travels with it.
61
+ smartsheet: extras("apiBaseUrl"),
53
62
  // The IAM key alone reaches nothing: every call needs the region and the
54
63
  // bucket, and S3-compatible providers need the endpoint too.
55
64
  amazonS3: extras("keyId", "region", "bucket", "endpoint", "pathPrefix"),
56
65
  // The SQL API lives on each account's own host, and the token is pinned to
57
66
  // one role; the warehouse is optional and falls back to the user's default.
58
67
  snowflake: extras("accountUrl", "role", "warehouse"),
68
+ // A statement is addressed by workspace host and SQL warehouse, and the token
69
+ // derives neither.
70
+ databricks: extras("host", "warehouseId"),
71
+ // Cloud Confluence is addressed through api.atlassian.com by cloud id, the
72
+ // same as Jira; the site URL is only for links a human will click.
73
+ confluence: extras("cloudId", "siteUrl"),
74
+ // OAuth returns a per-company api_domain alongside the token, so there is no
75
+ // one host to call.
76
+ pipedrive: extras("apiBaseUrl"),
77
+ // Shopify has no global API host: every call goes to the store's own domain
78
+ // under a pinned API version. The storefront pair is the shopper-facing API,
79
+ // which installs predating those scopes do not have.
80
+ shopify: extras("shopDomain", "apiBaseUrl", "storefrontApiUrl", "storefrontAccessToken"),
81
+ sharepoint: extras(),
82
+ excel: extras(),
83
+ x: extras(),
84
+ zoomMeetings: extras(),
85
+ // Every request names the workspace in an Account-Id header beside the key.
86
+ smartsuite: extras("workspaceId"),
59
87
  };
60
88
  /** Per service type: `accessToken`, then its extra labels sorted (Salesforce `instanceUrl`). */
61
89
  exports.CREDENTIAL_FIELDS = FIELDS_BY_TYPE;
@@ -68,6 +96,7 @@ exports.OPTIONAL_CREDENTIAL_FIELDS = {
68
96
  // Only S3-compatible providers set an endpoint, and a prefix is opt-in.
69
97
  amazonS3: ["endpoint", "pathPrefix"],
70
98
  snowflake: ["warehouse"],
99
+ shopify: ["storefrontApiUrl", "storefrontAccessToken"],
71
100
  };
72
101
  /**
73
102
  * Types the runtime never serves: those with a generated client (Airtable, email,
@@ -18,6 +18,7 @@ const index_js_1 = require("../index.js");
18
18
  ]);
19
19
  (0, vitest_1.expect)(index_js_1.CREDENTIAL_FIELDS.jira).toEqual(["accessToken", "cloudId", "siteUrl"]);
20
20
  (0, vitest_1.expect)(index_js_1.CREDENTIAL_FIELDS.granola).toEqual(["accessToken"]);
21
+ (0, vitest_1.expect)(index_js_1.CREDENTIAL_FIELDS.smartsheet).toEqual(["accessToken", "apiBaseUrl"]);
21
22
  });
22
23
  (0, vitest_1.it)("has no type the runtime never serves", () => {
23
24
  for (const type of index_js_1.NON_CREDENTIAL_SERVICE_TYPES) {
@@ -524,7 +524,7 @@ function generateDbTs(inputSchema) {
524
524
  lines.push('// `not: null` means "is set". An empty `in: []` matches nothing; an', "// empty `notIn: []` applies no filter.");
525
525
  lines.push("// sort is currently IGNORED here — it is stripped before the request.", "// (it does work on zite.auth.findAllUsers). Order in SQL instead.");
526
526
  lines.push("// limit defaults to 500, max 2000; offset is a row count (a number)");
527
- lines.push('// a filter value of `undefined` is REJECTED (it would query unfiltered', '// and match an arbitrary record). Check the value first.');
527
+ lines.push("// a filter value of `undefined` is REJECTED (it would query unfiltered", "// and match an arbitrary record). Check the value first.");
528
528
  lines.push("// .findOne({ id?, filters?, fields? }) → T | undefined");
529
529
  lines.push("// .create({ record }) → T");
530
530
  lines.push("// .update({ id, record }) → { id: string, fields: Partial<T> }");
@@ -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, ZiteStep, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteWebhook } from 'zitejs/backend/base';",
1235
+ "import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStep, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteEndpointInput, ZiteWebhook } from 'zitejs/backend/base';",
1236
1236
  "import type { User } from 'zitejs/auth';",
1237
1237
  "",
1238
1238
  "export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStep, 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 { ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput } from 'zitejs/backend/base';",
1241
+ "export type { ZiteDatabaseTriggerEvent, ZiteDatabaseTriggerInput, ZiteFormTriggerInput, 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;",
@@ -1326,12 +1326,12 @@ function generateBackendWrapperTs(envVarNames = []) {
1326
1326
  " schedule?: TSchedule;",
1327
1327
  " /** When set, an inbound webhook can also trigger this endpoint. Like `schedule`, it widens `context` — a webhook fire has no session. */",
1328
1328
  " webhook?: TWebhook;",
1329
- " /** When set, a connection's events (registered by this file's own `subscribe` export) or a poll (`every`) also fire this endpoint. Like `webhook`, it widens `context` — a trigger fire has no session. */",
1329
+ " /** When set, a connection's events (registered by this file's own `subscribe` export), a poll (`every`), a change to one of the project's tables (`table`) or a submission to one of its forms (`form`) also fire this endpoint. Like `webhook`, it widens `context` — a trigger fire has no session. */",
1330
1330
  " trigger?: TTrigger;",
1331
1331
  " execute: (",
1332
1332
  " params: {",
1333
- " input: TInput;",
1334
- " /** Durable steps: step.run(name, fn) runs fn once per logical run — its result is saved before anything after it executes, and a resumed run replays completed steps from their saved results. Provided by the platform; never define a local stand-in. */",
1333
+ " input: ZiteEndpointInput<TInput, TTrigger>;",
1334
+ " /** Durable steps: step.run(name, fn) runs fn once per logical run — its result is saved before anything after it executes, and a resumed run replays completed steps from their saved results. In a run fired by a schedule, webhook or trigger, step.sleep(name, \"2d\") pauses the run and a failed step is tried again. Provided by the platform; never define a local stand-in. */",
1335
1335
  " step: ZiteStep;",
1336
1336
  " context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
1337
1337
  // Mirrors the base module: a trigger's run gets `trigger`.
@@ -23,6 +23,8 @@ export type ZiteTrigger = {
23
23
  paused?: boolean;
24
24
  every?: never;
25
25
  events?: never;
26
+ table?: never;
27
+ form?: never;
26
28
  } | {
27
29
  /**
28
30
  * The connection's name in `zite.config.json`, for a provider whose
@@ -37,6 +39,8 @@ export type ZiteTrigger = {
37
39
  label?: string;
38
40
  paused?: boolean;
39
41
  every?: never;
42
+ table?: never;
43
+ form?: never;
40
44
  } | {
41
45
  /** `<n>m` (1–59) or `<n>h` (1–23). What makes the trigger a poll. */
42
46
  every: string;
@@ -46,6 +50,78 @@ export type ZiteTrigger = {
46
50
  label?: string;
47
51
  paused?: boolean;
48
52
  events?: never;
53
+ table?: never;
54
+ form?: never;
55
+ } | {
56
+ /**
57
+ * A table in the project's own database, as `zite.<table>` names it.
58
+ * Every write to it reaches the trigger, from apps, forms, imports, the
59
+ * database view and other automations; nothing is registered.
60
+ */
61
+ table: string;
62
+ on: ZiteDatabaseTriggerEvent[];
63
+ /** Narrows `"updated"` to updates that changed one of these fields. */
64
+ fields?: string[];
65
+ /** The sentence the editor shows, e.g. "When an order ships". */
66
+ label?: string;
67
+ paused?: boolean;
68
+ integration?: never;
69
+ every?: never;
70
+ events?: never;
71
+ form?: never;
72
+ } | {
73
+ /**
74
+ * One of the project's own Fillout forms, by its name as the project
75
+ * lists it, or its id. Each finished live submission fires the
76
+ * endpoint; nothing is registered.
77
+ */
78
+ form: string;
79
+ on: "submitted"[];
80
+ /** The sentence the editor shows, e.g. "When someone fills in the intake form". */
81
+ label?: string;
82
+ paused?: boolean;
83
+ integration?: never;
84
+ every?: never;
85
+ events?: never;
86
+ table?: never;
87
+ };
88
+ /**
89
+ * What a form trigger's run gets as `input`: one submission. `answers` is
90
+ * keyed by question name, `answersById` by question id; `submission` is the
91
+ * whole export, as the Fillout API returns it (`questions`, `calculations`,
92
+ * `urlParameters`, …). Two questions with the same label are named
93
+ * `Email` and `Email (1)` in an order the form doesn't show, so read those by
94
+ * id.
95
+ */
96
+ export type ZiteFormTriggerInput = {
97
+ event: "submitted";
98
+ form: string;
99
+ formId: string;
100
+ submissionId: string;
101
+ submittedAt: string | null;
102
+ answers: Record<string, unknown>;
103
+ answersById: Record<string, unknown>;
104
+ submission: Record<string, unknown> & {
105
+ submissionId: string;
106
+ };
107
+ };
108
+ export type ZiteDatabaseTriggerEvent = "created" | "updated" | "deleted";
109
+ /**
110
+ * What a database trigger's run gets as `input`: one changed record. Field
111
+ * keys are the SDK's, as `zite.<table>.findOne` returns them; cast `record`
112
+ * and `previous` to the table's generated `<Table>RecordType` from
113
+ * `zitejs/db` to type their fields.
114
+ */
115
+ export type ZiteDatabaseTriggerInput<TRecord extends Record<string, unknown> = Record<string, unknown>> = {
116
+ event: ZiteDatabaseTriggerEvent;
117
+ table: string;
118
+ recordId: string;
119
+ /** Null if it was deleted before the run read it; on `"deleted"`, the record as it was. */
120
+ record: TRecord | null;
121
+ /** The record before an `"updated"`; null otherwise, and on a very large bulk write. */
122
+ previous: TRecord | null;
123
+ /** What an `"updated"` changed. */
124
+ changedFields: string[];
49
125
  };
50
126
  /**
51
127
  * What a provider trigger's `subscribe` export receives. Write it as
@@ -115,10 +191,20 @@ export type ZiteTriggerDeliveryRun = ZiteTriggerRun & ZiteTriggerDeliveryInfo;
115
191
  export type ZiteTriggerContext<TTrigger> = TTrigger extends {
116
192
  every: string;
117
193
  } ? ZiteTriggerRun : TTrigger extends {
194
+ table: string;
195
+ } | {
196
+ form: string;
197
+ } ? ZiteTriggerDeliveryInfo : TTrigger extends {
118
198
  events: string[];
119
199
  } ? ZiteTriggerDeliveryInfo : TTrigger extends {
120
200
  integration: string;
121
201
  } ? ZiteTriggerDeliveryRun : never;
202
+ /** A database or form trigger's runs get the change or submission as `input`. */
203
+ export type ZiteEndpointInput<TInput, TTrigger> = TTrigger extends {
204
+ table: string;
205
+ } ? ZiteDatabaseTriggerInput : TTrigger extends {
206
+ form: string;
207
+ } ? ZiteFormTriggerInput : TInput;
122
208
  export interface ZiteRequestContext {
123
209
  user: {
124
210
  id: string;
@@ -215,13 +301,15 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
215
301
  */
216
302
  validatesInput?: boolean;
217
303
  execute: (params: {
218
- input: TInput;
304
+ input: ZiteEndpointInput<TInput, TTrigger>;
219
305
  /**
220
306
  * Durable steps. `step.run(name, fn)` runs `fn` once per logical run:
221
307
  * its result is saved before anything after it executes, and when a
222
308
  * failed run is resumed, completed steps return their saved results
223
309
  * instead of re-running their side effects. Names must be unique within
224
- * a run, and results must be plain JSON data under 256KB.
310
+ * a run, and results must be plain JSON data under 256KB. In a run
311
+ * fired by a schedule, webhook or trigger, `step.sleep` pauses the run
312
+ * and a failed step is tried again.
225
313
  */
226
314
  step: ZiteStep;
227
315
  context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
@@ -240,8 +328,32 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
240
328
  * it would matter, and give a step inside a loop the item's id in its name.
241
329
  */
242
330
  export interface ZiteStep {
243
- run<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
331
+ /**
332
+ * In a run fired by a schedule, webhook or trigger, a step that throws is
333
+ * tried again `retries` more times (default 3, at most 10), 30 seconds,
334
+ * 2 minutes, then 10 minutes apart. Elsewhere its error fails the request.
335
+ */
336
+ run<T>(name: string, fn: () => Promise<T> | T, options?: ZiteStepOptions): Promise<T>;
337
+ /**
338
+ * Pause the run for `duration`: `"30s"`, `"15m"`, `"2h"`, `"3d"`, `"1w"`,
339
+ * or milliseconds, at most 30 days. Only a run fired by a schedule, webhook
340
+ * or trigger can wait; an editor test run goes straight past, and a request
341
+ * from the app throws.
342
+ *
343
+ * The run continues in a later call, which runs `execute` again from the
344
+ * top: finished steps return their saved results, and code outside a step
345
+ * runs again, so keep side effects inside steps.
346
+ */
347
+ sleep(name: string, duration: ZiteStepDuration): Promise<void>;
348
+ /** Pause the run until `when`, at most 30 days ahead. See {@link ZiteStep.sleep}. */
349
+ sleepUntil(name: string, when: Date | string): Promise<void>;
350
+ }
351
+ export interface ZiteStepOptions {
352
+ /** How many more times to try the step after it throws: 0 to 10, default 3. */
353
+ retries?: number;
244
354
  }
355
+ /** Milliseconds, or a number with a unit: `ms`, `s`, `m`, `h`, `d` or `w`. */
356
+ export type ZiteStepDuration = number | `${number}${"ms" | "s" | "m" | "h" | "d" | "w"}`;
245
357
  /**
246
358
  * Create an endpoint definition, parsing `input` against `inputSchema` before
247
359
  * `execute` runs. Nothing downstream parses it — the worker calls `execute` on
@@ -6,6 +6,8 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
6
6
  // Direct execute() calls need the step surface the runtime always provides.
7
7
  const stepStub = {
8
8
  run: async (_name, fn) => await fn(),
9
+ sleep: async () => { },
10
+ sleepUntil: async () => { },
9
11
  };
10
12
  afterEach(() => {
11
13
  vi.unstubAllGlobals();
@@ -205,6 +207,62 @@ describe("platform-triggered runs", () => {
205
207
  return params.trigger;
206
208
  },
207
209
  });
210
+ createEndpoint({
211
+ trigger: { table: "orders", on: ["updated"], fields: ["status"] },
212
+ execute: async ({ input, trigger }) => {
213
+ // @ts-expect-error a database change has no cursor API
214
+ trigger?.advance("x");
215
+ const status = input.record?.status;
216
+ return { status, changed: input.changedFields, at: input.event };
217
+ },
218
+ });
219
+ createEndpoint({
220
+ // @ts-expect-error a database trigger names its table, not a connection
221
+ trigger: { table: "orders", on: ["created"], integration: "x" },
222
+ execute: async () => 1,
223
+ });
224
+ createEndpoint({
225
+ trigger: { form: "Intake form", on: ["submitted"] },
226
+ execute: async ({ input, trigger }) => {
227
+ // @ts-expect-error a form submission has no cursor API
228
+ trigger?.advance("x");
229
+ return {
230
+ email: input.answers.Email,
231
+ workEmail: input.answersById["wWorkEmail"],
232
+ id: input.submissionId,
233
+ };
234
+ },
235
+ });
236
+ createEndpoint({
237
+ // @ts-expect-error a form trigger watches submissions only
238
+ trigger: { form: "Intake form", on: ["updated"] },
239
+ execute: async () => 1,
240
+ });
241
+ createEndpoint({
242
+ // @ts-expect-error only created, updated and deleted
243
+ trigger: { table: "orders", on: ["archived"] },
244
+ execute: async () => 1,
245
+ });
246
+ });
247
+ it("hands a database trigger its change without parsing it against the input schema", async () => {
248
+ const endpoint = createEndpoint({
249
+ inputSchema: z.object({ orderId: z.string() }),
250
+ trigger: { table: "orders", on: ["created"] },
251
+ execute: async ({ input }) => input.recordId,
252
+ });
253
+ await expect(endpoint.execute({
254
+ step: stepStub,
255
+ input: {
256
+ event: "created",
257
+ table: "orders",
258
+ recordId: "rec_1",
259
+ record: { id: "rec_1" },
260
+ previous: null,
261
+ changedFields: [],
262
+ },
263
+ context: { user: null },
264
+ trigger: { headers: {}, deliveryId: "db:e1:rec_1" },
265
+ })).resolves.toBe("rec_1");
208
266
  });
209
267
  it("types a trigger's registration exports and checks what subscribe returns", async () => {
210
268
  const subscribe = (async (input) => ({
@@ -38,11 +38,24 @@ declare const FIELDS_BY_TYPE: {
38
38
  jira: readonly string[];
39
39
  typeform: readonly string[];
40
40
  apollo: readonly string[];
41
+ clay: readonly string[];
41
42
  attio: readonly string[];
42
43
  granola: readonly string[];
44
+ clickup: readonly string[];
43
45
  dropbox: readonly string[];
46
+ zendesk: readonly string[];
47
+ smartsheet: readonly string[];
44
48
  amazonS3: readonly string[];
45
49
  snowflake: readonly string[];
50
+ databricks: readonly string[];
51
+ confluence: readonly string[];
52
+ pipedrive: readonly string[];
53
+ shopify: readonly string[];
54
+ sharepoint: readonly string[];
55
+ excel: readonly string[];
56
+ x: readonly string[];
57
+ zoomMeetings: readonly string[];
58
+ smartsuite: readonly string[];
46
59
  };
47
60
  /** A service type `integrations.get` serves: one used through its npm package with a token. */
48
61
  export type CredentialServiceType = keyof typeof FIELDS_BY_TYPE;