zitejs 0.9.136 → 0.9.138
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/README.md +7 -2
- package/dist/cjs/backend/index.d.ts +19 -0
- package/dist/cjs/backend/index.test.js +39 -5
- package/dist/cjs/dev/index.js +7 -0
- package/dist/cjs/sourceRoots.d.ts +7 -0
- package/dist/cjs/sourceRoots.js +17 -0
- package/dist/cjs/sourceRoots.test.js +15 -0
- package/dist/cjs/sync/lib.js +4 -2
- package/dist/esm/backend/index.d.ts +19 -0
- package/dist/esm/backend/index.test.js +39 -5
- package/dist/esm/dev/index.js +8 -1
- package/dist/esm/sourceRoots.d.ts +7 -0
- package/dist/esm/sourceRoots.js +16 -0
- package/dist/esm/sourceRoots.test.js +16 -1
- package/dist/esm/sync/lib.js +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,10 +23,15 @@ const result = await zite.sql({
|
|
|
23
23
|
query: 'SELECT "id", "email" FROM "ziteUsers"',
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
// Define API endpoints
|
|
26
|
+
// Define API endpoints. step.run saves each result as it completes, so a
|
|
27
|
+
// failed run resumed from run history skips the steps that already ran.
|
|
27
28
|
import { createEndpoint, ZiteError } from 'zitejs/api';
|
|
28
29
|
export default createEndpoint({
|
|
29
|
-
execute: async ({ input }) => {
|
|
30
|
+
execute: async ({ input, step }) => {
|
|
31
|
+
const order = await step.run('load-order', () => loadOrder(input.id));
|
|
32
|
+
await step.run('charge', () => charge(order));
|
|
33
|
+
return { charged: true };
|
|
34
|
+
},
|
|
30
35
|
});
|
|
31
36
|
|
|
32
37
|
// Authentication
|
|
@@ -216,6 +216,14 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
|
|
|
216
216
|
validatesInput?: boolean;
|
|
217
217
|
execute: (params: {
|
|
218
218
|
input: TInput;
|
|
219
|
+
/**
|
|
220
|
+
* Durable steps. `step.run(name, fn)` runs `fn` once per logical run:
|
|
221
|
+
* its result is saved before anything after it executes, and when a
|
|
222
|
+
* failed run is resumed, completed steps return their saved results
|
|
223
|
+
* instead of re-running their side effects. Names must be unique within
|
|
224
|
+
* a run, and results must be plain JSON data under 256KB.
|
|
225
|
+
*/
|
|
226
|
+
step: ZiteStep;
|
|
219
227
|
context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
|
|
220
228
|
} & (TStream extends true ? {
|
|
221
229
|
stream: ZiteStreamInterface;
|
|
@@ -223,6 +231,17 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
|
|
|
223
231
|
trigger?: ZiteTriggerContext<TTrigger>;
|
|
224
232
|
} : {})) => Promise<TOutput> | TOutput;
|
|
225
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Durable steps, provided by the platform inside `execute`. Wrap each unit of
|
|
236
|
+
* work with side effects — a provider call, a message send, a batch write —
|
|
237
|
+
* so a retry of a failed run resumes at the failure instead of redoing
|
|
238
|
+
* completed work. Steps run at least once — a crash between the side effect
|
|
239
|
+
* and the save re-runs the step — so make a step idempotent where repeating
|
|
240
|
+
* it would matter, and give a step inside a loop the item's id in its name.
|
|
241
|
+
*/
|
|
242
|
+
export interface ZiteStep {
|
|
243
|
+
run<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
|
|
244
|
+
}
|
|
226
245
|
/**
|
|
227
246
|
* Create an endpoint definition, parsing `input` against `inputSchema` before
|
|
228
247
|
* `execute` runs. Nothing downstream parses it — the worker calls `execute` on
|
|
@@ -5,6 +5,10 @@ const zod_1 = require("zod");
|
|
|
5
5
|
const index_js_1 = require("./index.js");
|
|
6
6
|
const index_js_2 = require("../runtime/index.js");
|
|
7
7
|
const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
8
|
+
// Direct execute() calls need the step surface the runtime always provides.
|
|
9
|
+
const stepStub = {
|
|
10
|
+
run: async (_name, fn) => await fn(),
|
|
11
|
+
};
|
|
8
12
|
(0, vitest_1.afterEach)(() => {
|
|
9
13
|
vitest_1.vi.unstubAllGlobals();
|
|
10
14
|
});
|
|
@@ -15,7 +19,11 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
15
19
|
inputSchema: zod_1.z.object({ token: zod_1.z.string().min(10) }),
|
|
16
20
|
execute,
|
|
17
21
|
});
|
|
18
|
-
await (0, vitest_1.expect)(endpoint.execute({
|
|
22
|
+
await (0, vitest_1.expect)(endpoint.execute({
|
|
23
|
+
input: {},
|
|
24
|
+
context: requestContext,
|
|
25
|
+
step: stepStub,
|
|
26
|
+
})).rejects.toBeInstanceOf(index_js_1.ZiteError);
|
|
19
27
|
(0, vitest_1.expect)(execute).not.toHaveBeenCalled();
|
|
20
28
|
});
|
|
21
29
|
(0, vitest_1.it)("surfaces the failure as BAD_REQUEST and names the offending field", async () => {
|
|
@@ -23,7 +31,11 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
23
31
|
inputSchema: zod_1.z.object({ token: zod_1.z.string().min(10) }),
|
|
24
32
|
execute: async () => "unreachable",
|
|
25
33
|
});
|
|
26
|
-
await (0, vitest_1.expect)(endpoint.execute({
|
|
34
|
+
await (0, vitest_1.expect)(endpoint.execute({
|
|
35
|
+
input: {},
|
|
36
|
+
context: requestContext,
|
|
37
|
+
step: stepStub,
|
|
38
|
+
})).rejects.toMatchObject({
|
|
27
39
|
code: "BAD_REQUEST",
|
|
28
40
|
message: vitest_1.expect.stringContaining("token"),
|
|
29
41
|
});
|
|
@@ -35,6 +47,7 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
35
47
|
execute,
|
|
36
48
|
});
|
|
37
49
|
await (0, vitest_1.expect)(endpoint.execute({
|
|
50
|
+
step: stepStub,
|
|
38
51
|
input: undefined,
|
|
39
52
|
context: requestContext,
|
|
40
53
|
})).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
|
@@ -49,6 +62,7 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
49
62
|
execute: async ({ input }) => input,
|
|
50
63
|
});
|
|
51
64
|
await (0, vitest_1.expect)(endpoint.execute({
|
|
65
|
+
step: stepStub,
|
|
52
66
|
input: { token: "abc" },
|
|
53
67
|
context: requestContext,
|
|
54
68
|
})).resolves.toEqual({ token: "abc", limit: 10 });
|
|
@@ -62,6 +76,7 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
62
76
|
execute: async ({ input }) => input.action,
|
|
63
77
|
});
|
|
64
78
|
await (0, vitest_1.expect)(endpoint.execute({
|
|
79
|
+
step: stepStub,
|
|
65
80
|
input: { token: "b69779ae-eeea", action: "load" },
|
|
66
81
|
context: requestContext,
|
|
67
82
|
})).resolves.toBe("load");
|
|
@@ -80,6 +95,7 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
80
95
|
execute: async ({ input }) => input,
|
|
81
96
|
});
|
|
82
97
|
await (0, vitest_1.expect)(endpoint.execute({
|
|
98
|
+
step: stepStub,
|
|
83
99
|
input: { anything: true },
|
|
84
100
|
context: requestContext,
|
|
85
101
|
})).resolves.toEqual({ anything: true });
|
|
@@ -92,6 +108,7 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
92
108
|
execute: async (params) => params.stream === stream,
|
|
93
109
|
});
|
|
94
110
|
await (0, vitest_1.expect)(endpoint.execute({
|
|
111
|
+
step: stepStub,
|
|
95
112
|
input: { prompt: "hi" },
|
|
96
113
|
context: requestContext,
|
|
97
114
|
stream,
|
|
@@ -110,6 +127,7 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
110
127
|
execute: async ({ input }) => input,
|
|
111
128
|
});
|
|
112
129
|
await (0, vitest_1.expect)(endpoint.execute({
|
|
130
|
+
step: stepStub,
|
|
113
131
|
input: { __cron: { scheduleId: "s1" } },
|
|
114
132
|
context: { user: null },
|
|
115
133
|
})).resolves.toEqual({ __cron: { scheduleId: "s1" } });
|
|
@@ -124,14 +142,22 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
124
142
|
},
|
|
125
143
|
execute: async ({ input }) => input ?? "(no input)",
|
|
126
144
|
});
|
|
127
|
-
await (0, vitest_1.expect)(endpoint.execute({
|
|
145
|
+
await (0, vitest_1.expect)(endpoint.execute({
|
|
146
|
+
input: undefined,
|
|
147
|
+
context: { user: null },
|
|
148
|
+
step: stepStub,
|
|
149
|
+
})).resolves.toBe("(no input)");
|
|
128
150
|
});
|
|
129
151
|
(0, vitest_1.it)("still validates an ordinary endpoint called with no input", async () => {
|
|
130
152
|
const endpoint = (0, index_js_1.createEndpoint)({
|
|
131
153
|
inputSchema: zod_1.z.string(),
|
|
132
154
|
execute: async ({ input }) => input,
|
|
133
155
|
});
|
|
134
|
-
await (0, vitest_1.expect)(endpoint.execute({
|
|
156
|
+
await (0, vitest_1.expect)(endpoint.execute({
|
|
157
|
+
input: undefined,
|
|
158
|
+
context: requestContext,
|
|
159
|
+
step: stepStub,
|
|
160
|
+
})).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
|
135
161
|
});
|
|
136
162
|
(0, vitest_1.it)("hands a poll a typed trigger and skips the input schema for it", async () => {
|
|
137
163
|
const handled = [];
|
|
@@ -150,6 +176,7 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
150
176
|
});
|
|
151
177
|
const seen = new Set(["a"]);
|
|
152
178
|
await (0, vitest_1.expect)(endpoint.execute({
|
|
179
|
+
step: stepStub,
|
|
153
180
|
input: {},
|
|
154
181
|
context: { user: null },
|
|
155
182
|
trigger: {
|
|
@@ -218,6 +245,7 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
218
245
|
}),
|
|
219
246
|
});
|
|
220
247
|
await (0, vitest_1.expect)(endpoint.execute({
|
|
248
|
+
step: stepStub,
|
|
221
249
|
input: { kind: "sync" },
|
|
222
250
|
context: { user: null },
|
|
223
251
|
trigger: {
|
|
@@ -228,7 +256,11 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
228
256
|
headers: {},
|
|
229
257
|
deliveryId: "chan:2",
|
|
230
258
|
},
|
|
231
|
-
})).resolves.toEqual({
|
|
259
|
+
})).resolves.toEqual({
|
|
260
|
+
body: { kind: "sync" },
|
|
261
|
+
cursor: "sync1",
|
|
262
|
+
delivery: "chan:2",
|
|
263
|
+
});
|
|
232
264
|
});
|
|
233
265
|
(0, vitest_1.it)("refuses events on a trigger that also polls", () => {
|
|
234
266
|
(0, index_js_1.createEndpoint)({
|
|
@@ -245,6 +277,7 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
245
277
|
execute: async ({ input }) => input,
|
|
246
278
|
});
|
|
247
279
|
await (0, vitest_1.expect)(endpoint.execute({
|
|
280
|
+
step: stepStub,
|
|
248
281
|
input: { __webhook: { headers: {} }, id: 7 },
|
|
249
282
|
context: { user: null },
|
|
250
283
|
})).resolves.toEqual({ __webhook: { headers: {} }, id: 7 });
|
|
@@ -283,6 +316,7 @@ const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
|
283
316
|
await (0, vitest_1.expect)(buildEndpoint((nom) => matched.push(nom)).execute({
|
|
284
317
|
input: {},
|
|
285
318
|
context: requestContext,
|
|
319
|
+
step: stepStub,
|
|
286
320
|
})).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
|
287
321
|
(0, vitest_1.expect)(dispatched).toEqual([]);
|
|
288
322
|
(0, vitest_1.expect)(matched).toEqual([]);
|
package/dist/cjs/dev/index.js
CHANGED
|
@@ -178,6 +178,13 @@ async function runGenerate() {
|
|
|
178
178
|
// The backend writes the schema/lock files to the sandbox before
|
|
179
179
|
// calling this. Use `zitejs sync` separately for local dev (fetches
|
|
180
180
|
// metadata from the backend API).
|
|
181
|
+
// Everything below reads relative paths, so run from an app dir this would
|
|
182
|
+
// find no source roots, write nothing, and still print "Done!".
|
|
183
|
+
const workspaceRoot = (0, sourceRoots_js_1.findWorkspaceRoot)();
|
|
184
|
+
if (workspaceRoot && workspaceRoot !== (0, path_1.resolve)(".")) {
|
|
185
|
+
console.log(`Running from the workspace root: ${workspaceRoot}`);
|
|
186
|
+
process.chdir(workspaceRoot);
|
|
187
|
+
}
|
|
181
188
|
// 1. Generate root .zite/db.ts from zite.schema.json
|
|
182
189
|
try {
|
|
183
190
|
if ((0, fs_2.existsSync)("zite.schema.json")) {
|
|
@@ -15,6 +15,13 @@ export interface SourceDir {
|
|
|
15
15
|
}
|
|
16
16
|
/** Every source dir under every root, in root order then name order. */
|
|
17
17
|
export declare function listSourceDirs(cwd?: string): SourceDir[];
|
|
18
|
+
/**
|
|
19
|
+
* The workspace root at or above `cwd`: the nearest directory holding a
|
|
20
|
+
* `zite.config.json` next to an `apps/` or `automations/` dir. An app dir has
|
|
21
|
+
* a config of its own but no source root under it, so it never matches.
|
|
22
|
+
* Undefined outside a monorepo (a single-app project runs where it is).
|
|
23
|
+
*/
|
|
24
|
+
export declare function findWorkspaceRoot(cwd?: string): string | undefined;
|
|
18
25
|
/** The source dir with this bare name, under whichever root holds it. */
|
|
19
26
|
export declare function findSourceDir(name: string, cwd?: string): SourceDir | undefined;
|
|
20
27
|
/**
|
package/dist/cjs/sourceRoots.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AUTOMATION_ENTRY_FILE = exports.AUTOMATION_ENDPOINT_NAME = exports.SOURCE_ROOTS = void 0;
|
|
4
4
|
exports.listSourceDirs = listSourceDirs;
|
|
5
|
+
exports.findWorkspaceRoot = findWorkspaceRoot;
|
|
5
6
|
exports.findSourceDir = findSourceDir;
|
|
6
7
|
exports.sourceRootOf = sourceRootOf;
|
|
7
8
|
exports.endpointEntryFile = endpointEntryFile;
|
|
@@ -30,6 +31,22 @@ function listSourceDirs(cwd = ".") {
|
|
|
30
31
|
}
|
|
31
32
|
return dirs;
|
|
32
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The workspace root at or above `cwd`: the nearest directory holding a
|
|
36
|
+
* `zite.config.json` next to an `apps/` or `automations/` dir. An app dir has
|
|
37
|
+
* a config of its own but no source root under it, so it never matches.
|
|
38
|
+
* Undefined outside a monorepo (a single-app project runs where it is).
|
|
39
|
+
*/
|
|
40
|
+
function findWorkspaceRoot(cwd = ".") {
|
|
41
|
+
for (let dir = (0, path_1.resolve)(cwd);; dir = (0, path_1.dirname)(dir)) {
|
|
42
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, "zite.config.json")) &&
|
|
43
|
+
exports.SOURCE_ROOTS.some((root) => (0, fs_1.existsSync)((0, path_1.join)(dir, root)))) {
|
|
44
|
+
return dir;
|
|
45
|
+
}
|
|
46
|
+
if ((0, path_1.dirname)(dir) === dir)
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
33
50
|
/** The source dir with this bare name, under whichever root holds it. */
|
|
34
51
|
function findSourceDir(name, cwd = ".") {
|
|
35
52
|
return listSourceDirs(cwd).find((d) => d.dir === name);
|
|
@@ -27,4 +27,19 @@ const sourceRoots_js_1 = require("./sourceRoots.js");
|
|
|
27
27
|
(0, vitest_1.expect)((0, sourceRoots_js_1.listEndpointNames)("automations", (0, path_1.join)(repo, "automations/digest"))).toEqual(["index"]);
|
|
28
28
|
(0, vitest_1.expect)((0, sourceRoots_js_1.listEndpointNames)("automations", (0, path_1.join)(repo, "automations/empty"))).toEqual([]);
|
|
29
29
|
});
|
|
30
|
+
(0, vitest_1.it)("finds the workspace root from inside an app dir, and not from an app's own config", () => {
|
|
31
|
+
const repo = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), "zitejs-root-"));
|
|
32
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(repo, "zite.config.json"), "{}");
|
|
33
|
+
(0, fs_1.mkdirSync)((0, path_1.join)(repo, "apps/crm/src/api"), { recursive: true });
|
|
34
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(repo, "apps/crm/zite.config.json"), "{}");
|
|
35
|
+
(0, vitest_1.expect)((0, sourceRoots_js_1.findWorkspaceRoot)(repo)).toBe(repo);
|
|
36
|
+
(0, vitest_1.expect)((0, sourceRoots_js_1.findWorkspaceRoot)((0, path_1.join)(repo, "apps/crm"))).toBe(repo);
|
|
37
|
+
(0, vitest_1.expect)((0, sourceRoots_js_1.findWorkspaceRoot)((0, path_1.join)(repo, "apps/crm/src/api"))).toBe(repo);
|
|
38
|
+
});
|
|
39
|
+
(0, vitest_1.it)("has no root for a single-app project", () => {
|
|
40
|
+
const app = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), "zitejs-single-"));
|
|
41
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(app, "zite.config.json"), "{}");
|
|
42
|
+
(0, fs_1.mkdirSync)((0, path_1.join)(app, "src/api"), { recursive: true });
|
|
43
|
+
(0, vitest_1.expect)((0, sourceRoots_js_1.findWorkspaceRoot)(app)).toBeUndefined();
|
|
44
|
+
});
|
|
30
45
|
});
|
package/dist/cjs/sync/lib.js
CHANGED
|
@@ -1232,10 +1232,10 @@ function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1232
1232
|
"// Auto-generated type-narrowing wrapper. Do not edit manually.",
|
|
1233
1233
|
"// Re-exports createEndpoint with context.user typed to the app User.",
|
|
1234
1234
|
"",
|
|
1235
|
-
"import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteWebhook } from 'zitejs/backend/base';",
|
|
1235
|
+
"import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStep, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteWebhook } from 'zitejs/backend/base';",
|
|
1236
1236
|
"import type { User } from 'zitejs/auth';",
|
|
1237
1237
|
"",
|
|
1238
|
-
"export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteTrigger, ZiteWebhook };",
|
|
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
1241
|
"export type { ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput } from 'zitejs/backend/base';",
|
|
@@ -1331,6 +1331,8 @@ function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1331
1331
|
" execute: (",
|
|
1332
1332
|
" params: {",
|
|
1333
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. */",
|
|
1335
|
+
" step: ZiteStep;",
|
|
1334
1336
|
" context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
|
|
1335
1337
|
// Mirrors the base module: a trigger's run gets `trigger`.
|
|
1336
1338
|
" } & (TStream extends true ? { stream: ZiteStreamInterface } : {}) & (TTrigger extends ZiteTrigger ? { trigger?: ZiteTriggerContext<TTrigger> } : {}),",
|
|
@@ -216,6 +216,14 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
|
|
|
216
216
|
validatesInput?: boolean;
|
|
217
217
|
execute: (params: {
|
|
218
218
|
input: TInput;
|
|
219
|
+
/**
|
|
220
|
+
* Durable steps. `step.run(name, fn)` runs `fn` once per logical run:
|
|
221
|
+
* its result is saved before anything after it executes, and when a
|
|
222
|
+
* failed run is resumed, completed steps return their saved results
|
|
223
|
+
* instead of re-running their side effects. Names must be unique within
|
|
224
|
+
* a run, and results must be plain JSON data under 256KB.
|
|
225
|
+
*/
|
|
226
|
+
step: ZiteStep;
|
|
219
227
|
context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
|
|
220
228
|
} & (TStream extends true ? {
|
|
221
229
|
stream: ZiteStreamInterface;
|
|
@@ -223,6 +231,17 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
|
|
|
223
231
|
trigger?: ZiteTriggerContext<TTrigger>;
|
|
224
232
|
} : {})) => Promise<TOutput> | TOutput;
|
|
225
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* Durable steps, provided by the platform inside `execute`. Wrap each unit of
|
|
236
|
+
* work with side effects — a provider call, a message send, a batch write —
|
|
237
|
+
* so a retry of a failed run resumes at the failure instead of redoing
|
|
238
|
+
* completed work. Steps run at least once — a crash between the side effect
|
|
239
|
+
* and the save re-runs the step — so make a step idempotent where repeating
|
|
240
|
+
* it would matter, and give a step inside a loop the item's id in its name.
|
|
241
|
+
*/
|
|
242
|
+
export interface ZiteStep {
|
|
243
|
+
run<T>(name: string, fn: () => Promise<T> | T): Promise<T>;
|
|
244
|
+
}
|
|
226
245
|
/**
|
|
227
246
|
* Create an endpoint definition, parsing `input` against `inputSchema` before
|
|
228
247
|
* `execute` runs. Nothing downstream parses it — the worker calls `execute` on
|
|
@@ -3,6 +3,10 @@ import { z } from "zod";
|
|
|
3
3
|
import { createEndpoint, ZiteError, } from "./index.js";
|
|
4
4
|
import { createTableClient } from "../runtime/index.js";
|
|
5
5
|
const requestContext = { user: { id: "u1", email: "a@b.c", roles: [] } };
|
|
6
|
+
// Direct execute() calls need the step surface the runtime always provides.
|
|
7
|
+
const stepStub = {
|
|
8
|
+
run: async (_name, fn) => await fn(),
|
|
9
|
+
};
|
|
6
10
|
afterEach(() => {
|
|
7
11
|
vi.unstubAllGlobals();
|
|
8
12
|
});
|
|
@@ -13,7 +17,11 @@ describe("createEndpoint input validation", () => {
|
|
|
13
17
|
inputSchema: z.object({ token: z.string().min(10) }),
|
|
14
18
|
execute,
|
|
15
19
|
});
|
|
16
|
-
await expect(endpoint.execute({
|
|
20
|
+
await expect(endpoint.execute({
|
|
21
|
+
input: {},
|
|
22
|
+
context: requestContext,
|
|
23
|
+
step: stepStub,
|
|
24
|
+
})).rejects.toBeInstanceOf(ZiteError);
|
|
17
25
|
expect(execute).not.toHaveBeenCalled();
|
|
18
26
|
});
|
|
19
27
|
it("surfaces the failure as BAD_REQUEST and names the offending field", async () => {
|
|
@@ -21,7 +29,11 @@ describe("createEndpoint input validation", () => {
|
|
|
21
29
|
inputSchema: z.object({ token: z.string().min(10) }),
|
|
22
30
|
execute: async () => "unreachable",
|
|
23
31
|
});
|
|
24
|
-
await expect(endpoint.execute({
|
|
32
|
+
await expect(endpoint.execute({
|
|
33
|
+
input: {},
|
|
34
|
+
context: requestContext,
|
|
35
|
+
step: stepStub,
|
|
36
|
+
})).rejects.toMatchObject({
|
|
25
37
|
code: "BAD_REQUEST",
|
|
26
38
|
message: expect.stringContaining("token"),
|
|
27
39
|
});
|
|
@@ -33,6 +45,7 @@ describe("createEndpoint input validation", () => {
|
|
|
33
45
|
execute,
|
|
34
46
|
});
|
|
35
47
|
await expect(endpoint.execute({
|
|
48
|
+
step: stepStub,
|
|
36
49
|
input: undefined,
|
|
37
50
|
context: requestContext,
|
|
38
51
|
})).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
|
@@ -47,6 +60,7 @@ describe("createEndpoint input validation", () => {
|
|
|
47
60
|
execute: async ({ input }) => input,
|
|
48
61
|
});
|
|
49
62
|
await expect(endpoint.execute({
|
|
63
|
+
step: stepStub,
|
|
50
64
|
input: { token: "abc" },
|
|
51
65
|
context: requestContext,
|
|
52
66
|
})).resolves.toEqual({ token: "abc", limit: 10 });
|
|
@@ -60,6 +74,7 @@ describe("createEndpoint input validation", () => {
|
|
|
60
74
|
execute: async ({ input }) => input.action,
|
|
61
75
|
});
|
|
62
76
|
await expect(endpoint.execute({
|
|
77
|
+
step: stepStub,
|
|
63
78
|
input: { token: "b69779ae-eeea", action: "load" },
|
|
64
79
|
context: requestContext,
|
|
65
80
|
})).resolves.toBe("load");
|
|
@@ -78,6 +93,7 @@ describe("createEndpoint input validation", () => {
|
|
|
78
93
|
execute: async ({ input }) => input,
|
|
79
94
|
});
|
|
80
95
|
await expect(endpoint.execute({
|
|
96
|
+
step: stepStub,
|
|
81
97
|
input: { anything: true },
|
|
82
98
|
context: requestContext,
|
|
83
99
|
})).resolves.toEqual({ anything: true });
|
|
@@ -90,6 +106,7 @@ describe("createEndpoint input validation", () => {
|
|
|
90
106
|
execute: async (params) => params.stream === stream,
|
|
91
107
|
});
|
|
92
108
|
await expect(endpoint.execute({
|
|
109
|
+
step: stepStub,
|
|
93
110
|
input: { prompt: "hi" },
|
|
94
111
|
context: requestContext,
|
|
95
112
|
stream,
|
|
@@ -108,6 +125,7 @@ describe("platform-triggered runs", () => {
|
|
|
108
125
|
execute: async ({ input }) => input,
|
|
109
126
|
});
|
|
110
127
|
await expect(endpoint.execute({
|
|
128
|
+
step: stepStub,
|
|
111
129
|
input: { __cron: { scheduleId: "s1" } },
|
|
112
130
|
context: { user: null },
|
|
113
131
|
})).resolves.toEqual({ __cron: { scheduleId: "s1" } });
|
|
@@ -122,14 +140,22 @@ describe("platform-triggered runs", () => {
|
|
|
122
140
|
},
|
|
123
141
|
execute: async ({ input }) => input ?? "(no input)",
|
|
124
142
|
});
|
|
125
|
-
await expect(endpoint.execute({
|
|
143
|
+
await expect(endpoint.execute({
|
|
144
|
+
input: undefined,
|
|
145
|
+
context: { user: null },
|
|
146
|
+
step: stepStub,
|
|
147
|
+
})).resolves.toBe("(no input)");
|
|
126
148
|
});
|
|
127
149
|
it("still validates an ordinary endpoint called with no input", async () => {
|
|
128
150
|
const endpoint = createEndpoint({
|
|
129
151
|
inputSchema: z.string(),
|
|
130
152
|
execute: async ({ input }) => input,
|
|
131
153
|
});
|
|
132
|
-
await expect(endpoint.execute({
|
|
154
|
+
await expect(endpoint.execute({
|
|
155
|
+
input: undefined,
|
|
156
|
+
context: requestContext,
|
|
157
|
+
step: stepStub,
|
|
158
|
+
})).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
|
133
159
|
});
|
|
134
160
|
it("hands a poll a typed trigger and skips the input schema for it", async () => {
|
|
135
161
|
const handled = [];
|
|
@@ -148,6 +174,7 @@ describe("platform-triggered runs", () => {
|
|
|
148
174
|
});
|
|
149
175
|
const seen = new Set(["a"]);
|
|
150
176
|
await expect(endpoint.execute({
|
|
177
|
+
step: stepStub,
|
|
151
178
|
input: {},
|
|
152
179
|
context: { user: null },
|
|
153
180
|
trigger: {
|
|
@@ -216,6 +243,7 @@ describe("platform-triggered runs", () => {
|
|
|
216
243
|
}),
|
|
217
244
|
});
|
|
218
245
|
await expect(endpoint.execute({
|
|
246
|
+
step: stepStub,
|
|
219
247
|
input: { kind: "sync" },
|
|
220
248
|
context: { user: null },
|
|
221
249
|
trigger: {
|
|
@@ -226,7 +254,11 @@ describe("platform-triggered runs", () => {
|
|
|
226
254
|
headers: {},
|
|
227
255
|
deliveryId: "chan:2",
|
|
228
256
|
},
|
|
229
|
-
})).resolves.toEqual({
|
|
257
|
+
})).resolves.toEqual({
|
|
258
|
+
body: { kind: "sync" },
|
|
259
|
+
cursor: "sync1",
|
|
260
|
+
delivery: "chan:2",
|
|
261
|
+
});
|
|
230
262
|
});
|
|
231
263
|
it("refuses events on a trigger that also polls", () => {
|
|
232
264
|
createEndpoint({
|
|
@@ -243,6 +275,7 @@ describe("platform-triggered runs", () => {
|
|
|
243
275
|
execute: async ({ input }) => input,
|
|
244
276
|
});
|
|
245
277
|
await expect(endpoint.execute({
|
|
278
|
+
step: stepStub,
|
|
246
279
|
input: { __webhook: { headers: {} }, id: 7 },
|
|
247
280
|
context: { user: null },
|
|
248
281
|
})).resolves.toEqual({ __webhook: { headers: {} }, id: 7 });
|
|
@@ -281,6 +314,7 @@ describe("regression: token lookup on an endpoint called without a token", () =>
|
|
|
281
314
|
await expect(buildEndpoint((nom) => matched.push(nom)).execute({
|
|
282
315
|
input: {},
|
|
283
316
|
context: requestContext,
|
|
317
|
+
step: stepStub,
|
|
284
318
|
})).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
|
285
319
|
expect(dispatched).toEqual([]);
|
|
286
320
|
expect(matched).toEqual([]);
|
package/dist/esm/dev/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { watch } from "fs";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, } from "fs";
|
|
3
3
|
import { dirname, join, resolve } from "path";
|
|
4
|
-
import { listSourceDirs } from "../sourceRoots.js";
|
|
4
|
+
import { findWorkspaceRoot, listSourceDirs, } from "../sourceRoots.js";
|
|
5
5
|
import { runSync } from "../sync/index.js";
|
|
6
6
|
import { generateDbTs, generateApiTs, generateBackendWrapperTs, generateAirtableTs, findEmailIntegrationId, generateEmailSdk, generateIntegrationsIndex, } from "../sync/lib.js";
|
|
7
7
|
import { gettableIntegrations, } from "../integrations/rules.js";
|
|
@@ -173,6 +173,13 @@ export async function runGenerate() {
|
|
|
173
173
|
// The backend writes the schema/lock files to the sandbox before
|
|
174
174
|
// calling this. Use `zitejs sync` separately for local dev (fetches
|
|
175
175
|
// metadata from the backend API).
|
|
176
|
+
// Everything below reads relative paths, so run from an app dir this would
|
|
177
|
+
// find no source roots, write nothing, and still print "Done!".
|
|
178
|
+
const workspaceRoot = findWorkspaceRoot();
|
|
179
|
+
if (workspaceRoot && workspaceRoot !== resolve(".")) {
|
|
180
|
+
console.log(`Running from the workspace root: ${workspaceRoot}`);
|
|
181
|
+
process.chdir(workspaceRoot);
|
|
182
|
+
}
|
|
176
183
|
// 1. Generate root .zite/db.ts from zite.schema.json
|
|
177
184
|
try {
|
|
178
185
|
if (existsSync("zite.schema.json")) {
|
|
@@ -15,6 +15,13 @@ export interface SourceDir {
|
|
|
15
15
|
}
|
|
16
16
|
/** Every source dir under every root, in root order then name order. */
|
|
17
17
|
export declare function listSourceDirs(cwd?: string): SourceDir[];
|
|
18
|
+
/**
|
|
19
|
+
* The workspace root at or above `cwd`: the nearest directory holding a
|
|
20
|
+
* `zite.config.json` next to an `apps/` or `automations/` dir. An app dir has
|
|
21
|
+
* a config of its own but no source root under it, so it never matches.
|
|
22
|
+
* Undefined outside a monorepo (a single-app project runs where it is).
|
|
23
|
+
*/
|
|
24
|
+
export declare function findWorkspaceRoot(cwd?: string): string | undefined;
|
|
18
25
|
/** The source dir with this bare name, under whichever root holds it. */
|
|
19
26
|
export declare function findSourceDir(name: string, cwd?: string): SourceDir | undefined;
|
|
20
27
|
/**
|
package/dist/esm/sourceRoots.js
CHANGED
|
@@ -22,6 +22,22 @@ export function listSourceDirs(cwd = ".") {
|
|
|
22
22
|
}
|
|
23
23
|
return dirs;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The workspace root at or above `cwd`: the nearest directory holding a
|
|
27
|
+
* `zite.config.json` next to an `apps/` or `automations/` dir. An app dir has
|
|
28
|
+
* a config of its own but no source root under it, so it never matches.
|
|
29
|
+
* Undefined outside a monorepo (a single-app project runs where it is).
|
|
30
|
+
*/
|
|
31
|
+
export function findWorkspaceRoot(cwd = ".") {
|
|
32
|
+
for (let dir = resolve(cwd);; dir = dirname(dir)) {
|
|
33
|
+
if (existsSync(join(dir, "zite.config.json")) &&
|
|
34
|
+
SOURCE_ROOTS.some((root) => existsSync(join(dir, root)))) {
|
|
35
|
+
return dir;
|
|
36
|
+
}
|
|
37
|
+
if (dirname(dir) === dir)
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
25
41
|
/** The source dir with this bare name, under whichever root holds it. */
|
|
26
42
|
export function findSourceDir(name, cwd = ".") {
|
|
27
43
|
return listSourceDirs(cwd).find((d) => d.dir === name);
|
|
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "fs";
|
|
|
2
2
|
import { tmpdir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
|
-
import { endpointEntryFile, listEndpointNames, sourceRootOf, } from "./sourceRoots.js";
|
|
5
|
+
import { endpointEntryFile, findWorkspaceRoot, listEndpointNames, sourceRootOf, } from "./sourceRoots.js";
|
|
6
6
|
describe("source roots", () => {
|
|
7
7
|
it("reads the root from the source dir's parent", () => {
|
|
8
8
|
expect(sourceRootOf("/repo/apps/crm")).toBe("apps");
|
|
@@ -25,4 +25,19 @@ describe("source roots", () => {
|
|
|
25
25
|
expect(listEndpointNames("automations", join(repo, "automations/digest"))).toEqual(["index"]);
|
|
26
26
|
expect(listEndpointNames("automations", join(repo, "automations/empty"))).toEqual([]);
|
|
27
27
|
});
|
|
28
|
+
it("finds the workspace root from inside an app dir, and not from an app's own config", () => {
|
|
29
|
+
const repo = mkdtempSync(join(tmpdir(), "zitejs-root-"));
|
|
30
|
+
writeFileSync(join(repo, "zite.config.json"), "{}");
|
|
31
|
+
mkdirSync(join(repo, "apps/crm/src/api"), { recursive: true });
|
|
32
|
+
writeFileSync(join(repo, "apps/crm/zite.config.json"), "{}");
|
|
33
|
+
expect(findWorkspaceRoot(repo)).toBe(repo);
|
|
34
|
+
expect(findWorkspaceRoot(join(repo, "apps/crm"))).toBe(repo);
|
|
35
|
+
expect(findWorkspaceRoot(join(repo, "apps/crm/src/api"))).toBe(repo);
|
|
36
|
+
});
|
|
37
|
+
it("has no root for a single-app project", () => {
|
|
38
|
+
const app = mkdtempSync(join(tmpdir(), "zitejs-single-"));
|
|
39
|
+
writeFileSync(join(app, "zite.config.json"), "{}");
|
|
40
|
+
mkdirSync(join(app, "src/api"), { recursive: true });
|
|
41
|
+
expect(findWorkspaceRoot(app)).toBeUndefined();
|
|
42
|
+
});
|
|
28
43
|
});
|
package/dist/esm/sync/lib.js
CHANGED
|
@@ -1217,10 +1217,10 @@ export function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1217
1217
|
"// Auto-generated type-narrowing wrapper. Do not edit manually.",
|
|
1218
1218
|
"// Re-exports createEndpoint with context.user typed to the app User.",
|
|
1219
1219
|
"",
|
|
1220
|
-
"import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteWebhook } from 'zitejs/backend/base';",
|
|
1220
|
+
"import type { ZiteRequestContext as _ZiteRequestContext, ZiteScheduledContext, ZiteErrorCode, ZiteSchedule, ZiteStep, ZiteStreamInterface, ZiteTrigger, ZiteTriggerContext, ZiteWebhook } from 'zitejs/backend/base';",
|
|
1221
1221
|
"import type { User } from 'zitejs/auth';",
|
|
1222
1222
|
"",
|
|
1223
|
-
"export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStreamInterface, ZiteTrigger, ZiteWebhook };",
|
|
1223
|
+
"export type { ZiteErrorCode, ZiteSchedule, ZiteScheduledContext, ZiteStep, ZiteStreamInterface, ZiteTrigger, ZiteWebhook };",
|
|
1224
1224
|
// A trigger's registration exports are typed against these, and they
|
|
1225
1225
|
// only reach app code through this file.
|
|
1226
1226
|
"export type { ZiteTriggerContext, ZiteTriggerDeliveryInfo, ZiteTriggerDeliveryRun, ZiteTriggerRun, ZiteTriggerSubscribe, ZiteTriggerSubscribeInput, ZiteTriggerSubscribeResult, ZiteTriggerUnsubscribe, ZiteTriggerUnsubscribeInput } from 'zitejs/backend/base';",
|
|
@@ -1316,6 +1316,8 @@ export function generateBackendWrapperTs(envVarNames = []) {
|
|
|
1316
1316
|
" execute: (",
|
|
1317
1317
|
" params: {",
|
|
1318
1318
|
" input: TInput;",
|
|
1319
|
+
" /** 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. */",
|
|
1320
|
+
" step: ZiteStep;",
|
|
1319
1321
|
" context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : TTrigger extends ZiteTrigger ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;",
|
|
1320
1322
|
// Mirrors the base module: a trigger's run gets `trigger`.
|
|
1321
1323
|
" } & (TStream extends true ? { stream: ZiteStreamInterface } : {}) & (TTrigger extends ZiteTrigger ? { trigger?: ZiteTriggerContext<TTrigger> } : {}),",
|