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.
@@ -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({ input: {}, context: requestContext })).rejects.toBeInstanceOf(index_js_1.ZiteError);
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({ input: {}, context: requestContext })).rejects.toMatchObject({
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({ input: undefined, context: { user: null } })).resolves.toBe("(no input)");
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({ input: undefined, context: requestContext })).rejects.toMatchObject({ code: "BAD_REQUEST" });
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({ body: { kind: "sync" }, cursor: "sync1", delivery: "chan:2" });
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([]);
@@ -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
  /**
@@ -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({ input: {}, context: requestContext })).rejects.toBeInstanceOf(ZiteError);
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({ input: {}, context: requestContext })).rejects.toMatchObject({
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({ input: undefined, context: { user: null } })).resolves.toBe("(no input)");
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({ input: undefined, context: requestContext })).rejects.toMatchObject({ code: "BAD_REQUEST" });
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({ body: { kind: "sync" }, cursor: "sync1", delivery: "chan:2" });
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([]);
@@ -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
  /**
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.135",
3
+ "version": "0.9.137",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "sideEffects": false,