zitejs 0.10.0-start.6 → 0.10.0-start.8

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 (70) 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 +10 -13
  7. package/dist/cjs/vite/index.js +8 -3
  8. package/dist/cjs/vite/index.test.js +10 -2
  9. package/dist/esm/backend/index.d.ts +115 -3
  10. package/dist/esm/backend/index.test.js +58 -0
  11. package/dist/esm/integrations/rules.d.ts +13 -0
  12. package/dist/esm/integrations/rules.js +29 -0
  13. package/dist/esm/integrations/rules.test.js +1 -0
  14. package/dist/esm/start/airtable/arrayFields.d.ts +10 -0
  15. package/dist/esm/start/airtable/arrayFields.js +30 -0
  16. package/dist/esm/start/airtable/dispatch.d.ts +54 -0
  17. package/dist/esm/start/airtable/dispatch.js +465 -0
  18. package/dist/esm/start/airtable/formulas.d.ts +69 -0
  19. package/dist/esm/start/airtable/formulas.js +235 -0
  20. package/dist/esm/start/airtable/index.d.ts +11 -0
  21. package/dist/esm/start/airtable/index.js +70 -0
  22. package/dist/esm/start/authHooks.d.ts +7 -0
  23. package/dist/esm/start/authHooks.js +10 -0
  24. package/dist/esm/start/authHooksModule.d.ts +1 -0
  25. package/dist/esm/start/authHooksModule.js +3 -0
  26. package/dist/esm/start/clientErrorMessage.test.d.ts +1 -0
  27. package/dist/esm/start/clientErrorMessage.test.js +105 -0
  28. package/dist/esm/start/data/baseRunner.d.ts +92 -0
  29. package/dist/esm/start/data/baseRunner.js +83 -0
  30. package/dist/esm/start/data/dispatch.d.ts +16 -0
  31. package/dist/esm/start/data/dispatch.js +198 -0
  32. package/dist/esm/start/data/index.d.ts +18 -0
  33. package/dist/esm/start/data/index.js +76 -0
  34. package/dist/esm/start/data/sqlNameMappings.d.ts +58 -0
  35. package/dist/esm/start/data/sqlNameMappings.js +92 -0
  36. package/dist/esm/start/data/transformations.d.ts +128 -0
  37. package/dist/esm/start/data/transformations.js +318 -0
  38. package/dist/esm/start/data/types.d.ts +91 -0
  39. package/dist/esm/start/data/types.js +14 -0
  40. package/dist/esm/start/describe.d.ts +17 -1
  41. package/dist/esm/start/describe.js +27 -5
  42. package/dist/esm/start/errors.d.ts +46 -0
  43. package/dist/esm/start/errors.js +99 -0
  44. package/dist/esm/start/errors.test.d.ts +1 -0
  45. package/dist/esm/start/errors.test.js +23 -0
  46. package/dist/esm/start/index.d.ts +3 -19
  47. package/dist/esm/start/index.js +28 -36
  48. package/dist/esm/start/instance.d.ts +1 -2
  49. package/dist/esm/start/instance.js +11 -4
  50. package/dist/esm/start/previewClaim.d.ts +10 -0
  51. package/dist/esm/start/previewClaim.js +32 -0
  52. package/dist/esm/start/redactSecrets.d.ts +7 -0
  53. package/dist/esm/start/redactSecrets.js +131 -0
  54. package/dist/esm/start/redactSecrets.test.d.ts +1 -0
  55. package/dist/esm/start/redactSecrets.test.js +151 -0
  56. package/dist/esm/start/router.d.ts +6 -3
  57. package/dist/esm/start/router.js +47 -7
  58. package/dist/esm/start/runLog.d.ts +7 -4
  59. package/dist/esm/start/runLog.js +64 -6
  60. package/dist/esm/start/server.d.ts +7 -3
  61. package/dist/esm/start/server.js +124 -22
  62. package/dist/esm/start/types.d.ts +68 -3
  63. package/dist/esm/start/version.d.ts +1 -0
  64. package/dist/esm/start/version.js +2 -0
  65. package/dist/esm/start/vite.js +33 -4
  66. package/dist/esm/start/workflowRun.js +2 -1
  67. package/dist/esm/sync/lib.js +10 -13
  68. package/dist/esm/vite/index.js +8 -3
  69. package/dist/esm/vite/index.test.js +10 -2
  70. 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) {
@@ -83,6 +83,7 @@ const FIELD_TYPE_MAP = {
83
83
  formula: "any",
84
84
  created_at: "string",
85
85
  updated_at: "string",
86
+ created_by: "string",
86
87
  updated_by: "string",
87
88
  };
88
89
  /**
@@ -108,7 +109,7 @@ const FIELD_INPUT_TYPE_MAP = {
108
109
  * rather than typed and then refused at runtime.
109
110
  *
110
111
  * Mirrors `COMPUTED_FIELD_TYPES` in the product's own `shared/bases/fields`.
111
- * The first six are rejected by `createRecordDataSchema` — each has a
112
+ * The first seven are rejected by `createRecordDataSchema` — each has a
112
113
  * `refine(val => val == null)`, so a non-null write errors (null is a no-op
113
114
  * clear). `created_at` / `updated_at` are refused differently: they have no
114
115
  * column at all, so a write is silently dropped, which is the worse outcome to
@@ -120,6 +121,7 @@ const READ_ONLY_FIELD_TYPES = new Set([
120
121
  "source",
121
122
  "formula",
122
123
  "autonumber",
124
+ "created_by",
123
125
  "updated_by",
124
126
  "created_at",
125
127
  "updated_at",
@@ -260,13 +262,8 @@ function fieldJsdoc(schemaField, table, schema) {
260
262
  const parts = [];
261
263
  if (table.primaryFieldId === schemaField.id)
262
264
  parts.push("Primary field");
263
- if (def.type === "created_at" ||
264
- def.type === "updated_at" ||
265
- def.type === "lookup" ||
266
- def.type === "formula" ||
267
- def.type === "autonumber") {
265
+ if (READ_ONLY_FIELD_TYPES.has(def.type))
268
266
  parts.push("Read-only");
269
- }
270
267
  if (def.type === "linked_record") {
271
268
  const tpl = def.template;
272
269
  if (tpl.tableId) {
@@ -524,7 +521,7 @@ function generateDbTs(inputSchema) {
524
521
  lines.push('// `not: null` means "is set". An empty `in: []` matches nothing; an', "// empty `notIn: []` applies no filter.");
525
522
  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
523
  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.');
524
+ lines.push("// a filter value of `undefined` is REJECTED (it would query unfiltered", "// and match an arbitrary record). Check the value first.");
528
525
  lines.push("// .findOne({ id?, filters?, fields? }) → T | undefined");
529
526
  lines.push("// .create({ record }) → T");
530
527
  lines.push("// .update({ id, record }) → { id: string, fields: Partial<T> }");
@@ -1232,13 +1229,13 @@ function generateBackendWrapperTs(envVarNames = []) {
1232
1229
  "// Auto-generated type-narrowing wrapper. Do not edit manually.",
1233
1230
  "// Re-exports createEndpoint with context.user typed to the app User.",
1234
1231
  "",
1235
- "import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStep, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteWebhook } from 'zitejs/backend/base';",
1232
+ "import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStep, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteEndpointInput, ZiteWebhook } from 'zitejs/backend/base';",
1236
1233
  "import type { User } from 'zitejs/auth';",
1237
1234
  "",
1238
1235
  "export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStep, ZiteStreamInterface, ZiteTrigger, ZiteWebhook };",
1239
1236
  // A trigger's registration exports are typed against these, and they
1240
1237
  // only reach app code through this file.
1241
- "export type { ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput } from 'zitejs/backend/base';",
1238
+ "export type { ZiteDatabaseTriggerEvent, ZiteDatabaseTriggerInput, ZiteFormTriggerInput, ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput } from 'zitejs/backend/base';",
1242
1239
  // The pre-monorepo SDK put this in scope for every endpoint, so migrated
1243
1240
  // code can name it. `createEndpoint` infers the same thing without it.
1244
1241
  "export type InferSchemaType<T> = T extends { _output: infer U } ? U : T;",
@@ -1326,12 +1323,12 @@ function generateBackendWrapperTs(envVarNames = []) {
1326
1323
  " schedule?: TSchedule;",
1327
1324
  " /** When set, an inbound webhook can also trigger this endpoint. Like `schedule`, it widens `context` — a webhook fire has no session. */",
1328
1325
  " 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. */",
1326
+ " /** 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
1327
  " trigger?: TTrigger;",
1331
1328
  " execute: (",
1332
1329
  " 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. */",
1330
+ " input: ZiteEndpointInput<TInput, TTrigger>;",
1331
+ " /** 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
1332
  " step: ZiteStep;",
1336
1333
  " context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
1337
1334
  // Mirrors the base module: a trigger's run gets `trigger`.
@@ -75,11 +75,12 @@ function ziteId() {
75
75
  name: "vite-plugin-zite-id",
76
76
  enforce: "pre",
77
77
  async transform(code, id) {
78
- if (!validExtensions.has(path_1.default.extname(id)) ||
79
- id.includes("node_modules")) {
78
+ // TanStack's route code splitting moves a route's component into its own module, `route.tsx?tsr-split=…`.
79
+ const file = id.split("?")[0];
80
+ if (!validExtensions.has(path_1.default.extname(file)) || file.includes("node_modules")) {
80
81
  return null;
81
82
  }
82
- const relativePath = path_1.default.relative(cwd, id);
83
+ const relativePath = path_1.default.relative(cwd, file);
83
84
  try {
84
85
  const ast = (0, parser_1.parse)(code, {
85
86
  sourceType: "module",
@@ -134,6 +135,10 @@ function ziteId() {
134
135
  if (!isTagged) {
135
136
  return;
136
137
  }
138
+ // A module split from one already tagged keeps its tags.
139
+ if (openingElement.attributes?.some((attr) => attr.type === "JSXAttribute" && attr.name?.name === "data-zite-id")) {
140
+ return;
141
+ }
137
142
  const line = openingElement.loc?.start?.line ?? 0;
138
143
  const col = openingElement.loc?.start?.column ?? 0;
139
144
  const dataComponentId = `${relativePath}|${line}|${col}`;
@@ -6,16 +6,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const path_1 = __importDefault(require("path"));
7
7
  const vitest_1 = require("vitest");
8
8
  const index_js_1 = require("./index.js");
9
- async function transform(code) {
9
+ async function transform(code, file = 'src/App.tsx') {
10
10
  const plugin = (0, index_js_1.ziteId)();
11
11
  const handler = plugin.transform;
12
- const result = await handler(code, path_1.default.join(process.cwd(), 'src/App.tsx'));
12
+ const result = await handler(code, path_1.default.join(process.cwd(), file));
13
13
  if (!result) {
14
14
  throw new Error('transform returned null');
15
15
  }
16
16
  return result.code;
17
17
  }
18
18
  (0, vitest_1.describe)('ziteId', () => {
19
+ (0, vitest_1.it)("tags a route's component that TanStack split into its own module, by the source file", async () => {
20
+ const code = await transform('const a = <p>Hi</p>;', 'src/routes/index.tsx?tsr-split=component');
21
+ (0, vitest_1.expect)(code).toContain('<p data-zite-id="src/routes/index.tsx|1|10" data-zite-editable="true">');
22
+ });
23
+ (0, vitest_1.it)('leaves an element that already has its tag', async () => {
24
+ const code = await transform('const a = <p data-zite-id="src/routes/index.tsx|1|10">Hi</p>;');
25
+ (0, vitest_1.expect)(code.match(/data-zite-id/g)).toHaveLength(1);
26
+ });
19
27
  (0, vitest_1.it)('tags html and svg elements without a component name', async () => {
20
28
  const code = await transform('const a = <div><svg><path d="M0" /></svg><p>Hello</p></div>;');
21
29
  (0, vitest_1.expect)(code).toContain('<div data-zite-id="src/App.tsx|1|10">');
@@ -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