zitejs 0.10.0-start.5 → 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.
- package/dist/cjs/backend/index.d.ts +115 -3
- package/dist/cjs/backend/index.test.js +58 -0
- package/dist/cjs/integrations/rules.d.ts +13 -0
- package/dist/cjs/integrations/rules.js +29 -0
- package/dist/cjs/integrations/rules.test.js +1 -0
- package/dist/cjs/sync/lib.js +6 -6
- package/dist/esm/backend/index.d.ts +115 -3
- package/dist/esm/backend/index.test.js +58 -0
- package/dist/esm/integrations/rules.d.ts +13 -0
- package/dist/esm/integrations/rules.js +29 -0
- package/dist/esm/integrations/rules.test.js +1 -0
- package/dist/esm/start/authHooks.d.ts +7 -0
- package/dist/esm/start/authHooks.js +10 -0
- package/dist/esm/start/authHooksModule.d.ts +1 -0
- package/dist/esm/start/authHooksModule.js +3 -0
- package/dist/esm/start/clientErrorMessage.test.d.ts +1 -0
- package/dist/esm/start/clientErrorMessage.test.js +105 -0
- package/dist/esm/start/describe.d.ts +25 -2
- package/dist/esm/start/describe.js +55 -6
- package/dist/esm/start/errors.d.ts +46 -0
- package/dist/esm/start/errors.js +99 -0
- package/dist/esm/start/errors.test.d.ts +1 -0
- package/dist/esm/start/errors.test.js +23 -0
- package/dist/esm/start/index.d.ts +5 -21
- package/dist/esm/start/index.js +29 -37
- package/dist/esm/start/instance.d.ts +1 -2
- package/dist/esm/start/instance.js +11 -4
- package/dist/esm/start/previewClaim.d.ts +10 -0
- package/dist/esm/start/previewClaim.js +32 -0
- package/dist/esm/start/redactSecrets.d.ts +7 -0
- package/dist/esm/start/redactSecrets.js +131 -0
- package/dist/esm/start/redactSecrets.test.d.ts +1 -0
- package/dist/esm/start/redactSecrets.test.js +151 -0
- package/dist/esm/start/registration.d.ts +13 -0
- package/dist/esm/start/registration.js +21 -0
- package/dist/esm/start/router.d.ts +6 -3
- package/dist/esm/start/router.js +47 -7
- package/dist/esm/start/runLog.d.ts +7 -4
- package/dist/esm/start/runLog.js +64 -6
- package/dist/esm/start/server.d.ts +7 -3
- package/dist/esm/start/server.js +92 -21
- package/dist/esm/start/types.d.ts +28 -3
- package/dist/esm/start/version.d.ts +1 -0
- package/dist/esm/start/version.js +2 -0
- package/dist/esm/start/vite.js +31 -3
- package/dist/esm/start/workflow.d.ts +16 -2
- package/dist/esm/start/workflow.test-d.js +16 -1
- package/dist/esm/start/workflowRun.js +2 -1
- package/dist/esm/sync/lib.js +6 -6
- 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
|
-
|
|
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) {
|
package/dist/cjs/sync/lib.js
CHANGED
|
@@ -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(
|
|
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)
|
|
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
|
-
|
|
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;
|