zitejs 0.9.135 → 0.9.137
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 +19 -0
- package/dist/cjs/backend/index.test.js +39 -5
- package/dist/cjs/dev/index.js +7 -0
- package/dist/cjs/integrations/rules.d.ts +1 -0
- package/dist/cjs/integrations/rules.js +4 -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/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/integrations/rules.d.ts +1 -0
- package/dist/esm/integrations/rules.js +4 -0
- package/dist/esm/sourceRoots.d.ts +7 -0
- package/dist/esm/sourceRoots.js +16 -0
- package/dist/esm/sourceRoots.test.js +16 -1
- package/package.json +1 -1
|
@@ -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")) {
|
|
@@ -42,6 +42,7 @@ declare const FIELDS_BY_TYPE: {
|
|
|
42
42
|
granola: readonly string[];
|
|
43
43
|
dropbox: readonly string[];
|
|
44
44
|
amazonS3: readonly string[];
|
|
45
|
+
snowflake: readonly string[];
|
|
45
46
|
};
|
|
46
47
|
/** A service type `integrations.get` serves: one used through its npm package with a token. */
|
|
47
48
|
export type CredentialServiceType = keyof typeof FIELDS_BY_TYPE;
|
|
@@ -53,6 +53,9 @@ const FIELDS_BY_TYPE = {
|
|
|
53
53
|
// The IAM key alone reaches nothing: every call needs the region and the
|
|
54
54
|
// bucket, and S3-compatible providers need the endpoint too.
|
|
55
55
|
amazonS3: extras("keyId", "region", "bucket", "endpoint", "pathPrefix"),
|
|
56
|
+
// The SQL API lives on each account's own host, and the token is pinned to
|
|
57
|
+
// one role; the warehouse is optional and falls back to the user's default.
|
|
58
|
+
snowflake: extras("accountUrl", "role", "warehouse"),
|
|
56
59
|
};
|
|
57
60
|
/** Per service type: `accessToken`, then its extra labels sorted (Salesforce `instanceUrl`). */
|
|
58
61
|
exports.CREDENTIAL_FIELDS = FIELDS_BY_TYPE;
|
|
@@ -64,6 +67,7 @@ exports.OPTIONAL_CREDENTIAL_FIELDS = {
|
|
|
64
67
|
jira: ["siteUrl"],
|
|
65
68
|
// Only S3-compatible providers set an endpoint, and a prefix is opt-in.
|
|
66
69
|
amazonS3: ["endpoint", "pathPrefix"],
|
|
70
|
+
snowflake: ["warehouse"],
|
|
67
71
|
};
|
|
68
72
|
/**
|
|
69
73
|
* Types the runtime never serves: those with a generated client (Airtable, email,
|
|
@@ -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
|
});
|
|
@@ -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")) {
|
|
@@ -42,6 +42,7 @@ declare const FIELDS_BY_TYPE: {
|
|
|
42
42
|
granola: readonly string[];
|
|
43
43
|
dropbox: readonly string[];
|
|
44
44
|
amazonS3: readonly string[];
|
|
45
|
+
snowflake: readonly string[];
|
|
45
46
|
};
|
|
46
47
|
/** A service type `integrations.get` serves: one used through its npm package with a token. */
|
|
47
48
|
export type CredentialServiceType = keyof typeof FIELDS_BY_TYPE;
|
|
@@ -50,6 +50,9 @@ const FIELDS_BY_TYPE = {
|
|
|
50
50
|
// The IAM key alone reaches nothing: every call needs the region and the
|
|
51
51
|
// bucket, and S3-compatible providers need the endpoint too.
|
|
52
52
|
amazonS3: extras("keyId", "region", "bucket", "endpoint", "pathPrefix"),
|
|
53
|
+
// The SQL API lives on each account's own host, and the token is pinned to
|
|
54
|
+
// one role; the warehouse is optional and falls back to the user's default.
|
|
55
|
+
snowflake: extras("accountUrl", "role", "warehouse"),
|
|
53
56
|
};
|
|
54
57
|
/** Per service type: `accessToken`, then its extra labels sorted (Salesforce `instanceUrl`). */
|
|
55
58
|
export const CREDENTIAL_FIELDS = FIELDS_BY_TYPE;
|
|
@@ -61,6 +64,7 @@ export const OPTIONAL_CREDENTIAL_FIELDS = {
|
|
|
61
64
|
jira: ["siteUrl"],
|
|
62
65
|
// Only S3-compatible providers set an endpoint, and a prefix is opt-in.
|
|
63
66
|
amazonS3: ["endpoint", "pathPrefix"],
|
|
67
|
+
snowflake: ["warehouse"],
|
|
64
68
|
};
|
|
65
69
|
/**
|
|
66
70
|
* Types the runtime never serves: those with a generated client (Airtable, email,
|
|
@@ -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
|
});
|