zitejs 0.9.51 → 0.9.53

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.
@@ -4,4 +4,5 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown> {
4
4
  context: unknown;
5
5
  }) => Promise<TOutput> | TOutput;
6
6
  }
7
+ export declare function createCaller<TInput, TOutput>(name: string, flowId?: string): (input: TInput) => Promise<TOutput>;
7
8
  export declare function createCaller<TInput, TOutput>(endpoint: EndpointConfig<TInput, TOutput>, name: string, flowId?: string): (input: TInput) => Promise<TOutput>;
@@ -3,13 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createCaller = createCaller;
4
4
  const env_js_1 = require("../internal/env.js");
5
5
  const ENVIRONMENTS = {
6
- production: 'https://workflows.zite.com',
7
- staging: 'https://workflows.zitestaging.com',
8
- local: 'http://localhost:2506',
6
+ production: "https://workflows.zite.com",
7
+ staging: "https://workflows.zitestaging.com",
8
+ local: "http://localhost:2506",
9
9
  };
10
10
  function getRunnerUrl() {
11
- const env = (0, env_js_1.getEnv)('ZITE_ENV', 'VITE_ZITE_ENV') ?? 'production';
12
- return (0, env_js_1.getEnv)('ZITE_RUNNER_URL', 'VITE_ZITE_RUNNER_URL') ?? ENVIRONMENTS[env] ?? ENVIRONMENTS.production;
11
+ const env = (0, env_js_1.getEnv)("ZITE_ENV", "VITE_ZITE_ENV") ?? "production";
12
+ return ((0, env_js_1.getEnv)("ZITE_RUNNER_URL", "VITE_ZITE_RUNNER_URL") ??
13
+ ENVIRONMENTS[env] ??
14
+ ENVIRONMENTS.production);
13
15
  }
14
16
  /**
15
17
  * Get the user's usage token for endpoint auth.
@@ -17,33 +19,39 @@ function getRunnerUrl() {
17
19
  * In external mode: stored in localStorage by the auth flow
18
20
  */
19
21
  function getUsageToken() {
20
- if (typeof window !== 'undefined') {
22
+ if (typeof window !== "undefined") {
21
23
  const win = window;
22
- if (typeof win._ziteUsageToken === 'string')
24
+ if (typeof win._ziteUsageToken === "string")
23
25
  return win._ziteUsageToken;
24
26
  }
25
27
  // Fallback to stored auth token (external mode)
26
- if (typeof localStorage !== 'undefined') {
27
- const stored = localStorage.getItem('zite.auth.token');
28
+ if (typeof localStorage !== "undefined") {
29
+ const stored = localStorage.getItem("zite.auth.token");
28
30
  if (stored)
29
31
  return stored;
30
32
  }
31
- return '';
33
+ return "";
32
34
  }
33
- function createCaller(endpoint, name, flowId) {
35
+ function createCaller(endpointOrName, nameOrFlowId, flowId) {
36
+ const name = typeof endpointOrName === "string" ? endpointOrName : nameOrFlowId;
37
+ const resolvedFlowId = typeof endpointOrName === "string" ? nameOrFlowId : flowId;
34
38
  return async (input) => {
35
- const appId = flowId ?? (0, env_js_1.getEnv)('ZITE_FLOW_ID', 'VITE_ZITE_FLOW_ID') ?? '';
39
+ const appId = resolvedFlowId ?? (0, env_js_1.getEnv)("ZITE_FLOW_ID", "VITE_ZITE_FLOW_ID") ?? "";
36
40
  const token = getUsageToken();
37
- const res = await fetch(getRunnerUrl() + '/public/' + appId + '/api/' + name, {
38
- method: 'POST',
41
+ const res = await fetch(getRunnerUrl() + "/public/" + appId + "/api/" + name, {
42
+ method: "POST",
39
43
  headers: {
40
44
  ...(token ? { Authorization: `Bearer ${token}` } : {}),
41
- 'Content-Type': 'application/json',
45
+ "Content-Type": "application/json",
42
46
  },
43
- body: JSON.stringify({ inputs: input, mode: 'preview', usageToken: token }),
47
+ body: JSON.stringify({
48
+ inputs: input,
49
+ mode: "preview",
50
+ usageToken: token,
51
+ }),
44
52
  });
45
53
  if (!res.ok) {
46
- const text = await res.text().catch(() => '');
54
+ const text = await res.text().catch(() => "");
47
55
  throw new Error(`API call failed (${res.status}): ${text}`);
48
56
  }
49
57
  return res.json();
@@ -15,18 +15,18 @@ function debounce(key, fn, ms) {
15
15
  debounceTimers.set(key, setTimeout(fn, ms));
16
16
  }
17
17
  function findAppDirs() {
18
- const appsDir = 'apps';
18
+ const appsDir = "apps";
19
19
  if (!(0, fs_2.existsSync)(appsDir))
20
20
  return [];
21
21
  return (0, fs_2.readdirSync)(appsDir, { withFileTypes: true })
22
- .filter(d => d.isDirectory())
23
- .map(d => d.name);
22
+ .filter((d) => d.isDirectory())
23
+ .map((d) => d.name);
24
24
  }
25
25
  function getFlowId(appDir) {
26
26
  try {
27
- const configPath = (0, path_1.join)('apps', appDir, 'zite.config.json');
27
+ const configPath = (0, path_1.join)("apps", appDir, "zite.config.json");
28
28
  if ((0, fs_2.existsSync)(configPath)) {
29
- const config = JSON.parse((0, fs_2.readFileSync)(configPath, 'utf-8'));
29
+ const config = JSON.parse((0, fs_2.readFileSync)(configPath, "utf-8"));
30
30
  return config.id;
31
31
  }
32
32
  }
@@ -34,32 +34,32 @@ function getFlowId(appDir) {
34
34
  return undefined;
35
35
  }
36
36
  function regenerateAppTypedWrappers(appDir) {
37
- const outDir = (0, path_1.join)('apps', appDir, '.zite');
37
+ const outDir = (0, path_1.join)("apps", appDir, ".zite");
38
38
  (0, fs_2.mkdirSync)(outDir, { recursive: true });
39
- (0, fs_2.writeFileSync)((0, path_1.join)(outDir, 'user.ts'), (0, lib_js_1.generateUserTs)());
40
- (0, fs_2.writeFileSync)((0, path_1.join)(outDir, 'auth.ts'), (0, lib_js_1.generateAuthWrapperTs)());
41
- (0, fs_2.writeFileSync)((0, path_1.join)(outDir, 'backend.ts'), (0, lib_js_1.generateBackendWrapperTs)());
39
+ (0, fs_2.writeFileSync)((0, path_1.join)(outDir, "user.ts"), (0, lib_js_1.generateUserTs)());
40
+ (0, fs_2.writeFileSync)((0, path_1.join)(outDir, "auth.ts"), (0, lib_js_1.generateAuthWrapperTs)());
41
+ (0, fs_2.writeFileSync)((0, path_1.join)(outDir, "backend.ts"), (0, lib_js_1.generateBackendWrapperTs)());
42
42
  }
43
43
  function regenerateAppApiTs(appDir) {
44
- const apiDir = (0, path_1.join)('apps', appDir, 'src', 'api');
44
+ const apiDir = (0, path_1.join)("apps", appDir, "src", "api");
45
45
  if (!(0, fs_2.existsSync)(apiDir))
46
46
  return;
47
- const endpointFiles = (0, fs_2.readdirSync)(apiDir).filter((f) => f.endsWith('.ts') || f.endsWith('.js'));
47
+ const endpointFiles = (0, fs_2.readdirSync)(apiDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
48
48
  const content = (0, lib_js_1.generateApiTs)(endpointFiles);
49
49
  if (content) {
50
- const outDir = (0, path_1.join)('apps', appDir, '.zite');
50
+ const outDir = (0, path_1.join)("apps", appDir, ".zite");
51
51
  (0, fs_2.mkdirSync)(outDir, { recursive: true });
52
- (0, fs_2.writeFileSync)((0, path_1.join)(outDir, 'api.ts'), content);
52
+ (0, fs_2.writeFileSync)((0, path_1.join)(outDir, "api.ts"), content);
53
53
  console.log(`Updated apps/${appDir}/.zite/api.ts`);
54
54
  }
55
55
  }
56
56
  function regenerateAppAirtableSdk(appDir) {
57
- const lockPath = (0, path_1.join)('apps', appDir, 'zite.lock');
57
+ const lockPath = (0, path_1.join)("apps", appDir, "zite.lock");
58
58
  console.log(`[airtable-sdk] Checking ${lockPath} exists: ${(0, fs_2.existsSync)(lockPath)}`);
59
59
  if (!(0, fs_2.existsSync)(lockPath))
60
60
  return;
61
61
  try {
62
- const lockContent = JSON.parse((0, fs_2.readFileSync)(lockPath, 'utf-8'));
62
+ const lockContent = JSON.parse((0, fs_2.readFileSync)(lockPath, "utf-8"));
63
63
  const integrations = lockContent.integrations ?? {};
64
64
  console.log(`[airtable-sdk] Found ${Object.keys(integrations).length} integrations in lock:`, Object.keys(integrations));
65
65
  for (const [integrationId, integration] of Object.entries(integrations)) {
@@ -75,9 +75,9 @@ function regenerateAppAirtableSdk(appDir) {
75
75
  };
76
76
  const content = (0, lib_js_1.generateAirtableTs)(airtableLock);
77
77
  if (content) {
78
- const outDir = (0, path_1.join)('apps', appDir, '.zite', 'integrations');
78
+ const outDir = (0, path_1.join)("apps", appDir, ".zite", "integrations");
79
79
  (0, fs_2.mkdirSync)(outDir, { recursive: true });
80
- (0, fs_2.writeFileSync)((0, path_1.join)(outDir, 'airtable.ts'), content);
80
+ (0, fs_2.writeFileSync)((0, path_1.join)(outDir, "airtable.ts"), content);
81
81
  console.log(`Updated apps/${appDir}/.zite/integrations/airtable.ts`);
82
82
  }
83
83
  }
@@ -96,16 +96,16 @@ async function runGenerate() {
96
96
  // metadata from the backend API).
97
97
  // 1. Generate root .zite/db.ts from zite.schema.json
98
98
  try {
99
- if ((0, fs_2.existsSync)('zite.schema.json')) {
100
- const schema = JSON.parse((0, fs_2.readFileSync)('zite.schema.json', 'utf-8'));
99
+ if ((0, fs_2.existsSync)("zite.schema.json")) {
100
+ const schema = JSON.parse((0, fs_2.readFileSync)("zite.schema.json", "utf-8"));
101
101
  const dbTs = (0, lib_js_1.generateDbTs)(schema);
102
- (0, fs_2.mkdirSync)('.zite', { recursive: true });
103
- (0, fs_2.writeFileSync)((0, path_1.join)('.zite', 'db.ts'), dbTs);
104
- console.log('Wrote .zite/db.ts');
102
+ (0, fs_2.mkdirSync)(".zite", { recursive: true });
103
+ (0, fs_2.writeFileSync)((0, path_1.join)(".zite", "db.ts"), dbTs);
104
+ console.log("Wrote .zite/db.ts");
105
105
  }
106
106
  }
107
107
  catch (err) {
108
- console.warn('DB SDK generation failed:', err instanceof Error ? err.message : err);
108
+ console.warn("DB SDK generation failed:", err instanceof Error ? err.message : err);
109
109
  }
110
110
  // 2. Find all apps and regenerate their .zite/ files
111
111
  const appDirs = findAppDirs();
@@ -114,20 +114,20 @@ async function runGenerate() {
114
114
  regenerateAppTypedWrappers(app);
115
115
  regenerateAppAirtableSdk(app);
116
116
  }
117
- console.log('Done!');
117
+ console.log("Done!");
118
118
  }
119
119
  async function runDev() {
120
- const env = process.env.ZITE_ENV ?? 'production';
120
+ const env = process.env.ZITE_ENV ?? "production";
121
121
  console.log(`zitejs dev — environment: ${env}`);
122
- console.log('');
122
+ console.log("");
123
123
  // 1. Run initial sync (generates root .zite/db.ts)
124
124
  try {
125
125
  await (0, index_js_1.runSync)();
126
126
  }
127
127
  catch (err) {
128
- console.warn('Initial sync failed (continuing with watcher):', err instanceof Error ? err.message : err);
128
+ console.warn("Initial sync failed (continuing with watcher):", err instanceof Error ? err.message : err);
129
129
  }
130
- console.log('');
130
+ console.log("");
131
131
  // 2. Find all apps and regenerate their .zite/ files
132
132
  const appDirs = findAppDirs();
133
133
  for (const app of appDirs) {
@@ -137,7 +137,7 @@ async function runDev() {
137
137
  // 3. Watch each app's src/api/ for endpoint changes
138
138
  let watchingAny = false;
139
139
  for (const app of appDirs) {
140
- const apiDir = (0, path_1.join)('apps', app, 'src', 'api');
140
+ const apiDir = (0, path_1.join)("apps", app, "src", "api");
141
141
  if (!(0, fs_2.existsSync)(apiDir))
142
142
  continue;
143
143
  watchingAny = true;
@@ -145,7 +145,7 @@ async function runDev() {
145
145
  (0, fs_1.watch)(apiDir, { recursive: true }, (_event, filename) => {
146
146
  if (!filename)
147
147
  return;
148
- if (!filename.endsWith('.ts') && !filename.endsWith('.js'))
148
+ if (!filename.endsWith(".ts") && !filename.endsWith(".js"))
149
149
  return;
150
150
  debounce(app, () => {
151
151
  console.log(`Endpoint changed in ${app}: ${filename}`);
@@ -154,24 +154,24 @@ async function runDev() {
154
154
  });
155
155
  }
156
156
  if (!watchingAny) {
157
- console.log('No apps with src/api/ found.');
157
+ console.log("No apps with src/api/ found.");
158
158
  }
159
159
  // 4. Watch zite.schema.json for schema drift
160
- if ((0, fs_2.existsSync)('zite.schema.json')) {
161
- console.log('Watching zite.schema.json for schema changes...');
162
- (0, fs_1.watch)('zite.schema.json', () => {
163
- debounce('schema', async () => {
164
- console.log('Schema changed — re-running sync...');
160
+ if ((0, fs_2.existsSync)("zite.schema.json")) {
161
+ console.log("Watching zite.schema.json for schema changes...");
162
+ (0, fs_1.watch)("zite.schema.json", () => {
163
+ debounce("schema", async () => {
164
+ console.log("Schema changed — re-running sync...");
165
165
  try {
166
166
  await (0, index_js_1.runSync)();
167
167
  }
168
168
  catch (err) {
169
- console.error('Re-sync failed:', err instanceof Error ? err.message : err);
169
+ console.error("Re-sync failed:", err instanceof Error ? err.message : err);
170
170
  }
171
171
  }, 500);
172
172
  });
173
173
  }
174
- console.log('');
175
- console.log('Ready. Watching for changes... (Ctrl+C to stop)');
174
+ console.log("");
175
+ console.log("Ready. Watching for changes... (Ctrl+C to stop)");
176
176
  await new Promise(() => { });
177
177
  }
@@ -40,11 +40,13 @@ export type AirtableLockField = {
40
40
  id: string;
41
41
  sdkName: string;
42
42
  type: string;
43
- options?: string[];
43
+ name: string;
44
+ config?: Record<string, unknown>;
44
45
  };
45
46
  export type AirtableLockTable = {
46
47
  id: string;
47
48
  sdkName: string;
49
+ primaryFieldId: string;
48
50
  fields: AirtableLockField[];
49
51
  };
50
52
  export type AirtableLock = {
@@ -282,18 +282,18 @@ function generateApiTs(endpointFiles) {
282
282
  for (const file of endpointFiles) {
283
283
  const name = file.replace(/\.(ts|js)$/, "");
284
284
  const camelName = toCamelCase(name);
285
- lines.push(`import ${camelName}Endpoint from '../src/api/${name}';`);
285
+ const pascal = toPascalCase(camelName);
286
+ lines.push(`import type { default as _${pascal}Ep } from '../src/api/${name}';`);
286
287
  endpointNames.push(camelName);
287
288
  }
288
289
  lines.push("");
289
- for (const name of endpointNames) {
290
- lines.push(`export const ${name} = createCaller(${name}Endpoint, '${name}');`);
291
- }
292
- lines.push("");
293
290
  for (const name of endpointNames) {
294
291
  const pascal = toPascalCase(name);
295
- lines.push(`export type ${pascal}InputType = Parameters<typeof ${name}Endpoint.execute>[0]['input'];`);
296
- lines.push(`export type ${pascal}OutputType = Awaited<ReturnType<typeof ${name}Endpoint.execute>>;`);
292
+ lines.push(`type _${pascal}Cfg = typeof _${pascal}Ep;`);
293
+ lines.push(`export type ${pascal}InputType = Parameters<_${pascal}Cfg['execute']>[0]['input'];`);
294
+ lines.push(`export type ${pascal}OutputType = Awaited<ReturnType<_${pascal}Cfg['execute']>>;`);
295
+ lines.push(`export const ${name} = createCaller<${pascal}InputType, ${pascal}OutputType>('${name}');`);
296
+ lines.push("");
297
297
  }
298
298
  lines.push("");
299
299
  lines.push("export const api = {");
@@ -394,11 +394,12 @@ const AIRTABLE_FIELD_TYPE_MAP = {
394
394
  externalSyncSource: "unknown",
395
395
  aiText: "string",
396
396
  };
397
- function airtableTsType(field) {
397
+ function airtableTsType(field, lock, depth) {
398
+ const choices = field.config?.choices;
398
399
  if ((field.type === "singleSelect" || field.type === "multipleSelects") &&
399
- field.options &&
400
- field.options.length > 0) {
401
- const literals = field.options
400
+ choices &&
401
+ choices.length > 0) {
402
+ const literals = choices
402
403
  .slice(0, MAX_SELECT_OPTIONS)
403
404
  .map((o) => `"${o.replace(/"/g, '\\"')}"`)
404
405
  .join(" | ");
@@ -407,15 +408,106 @@ function airtableTsType(field) {
407
408
  return `(${union})[]`;
408
409
  return union;
409
410
  }
411
+ if (field.type === "multipleLookupValues" && lock && (depth ?? 0) < 5) {
412
+ const linkFieldId = field.config?.recordLinkFieldId;
413
+ const targetFieldId = field.config?.fieldIdInLinkedTable;
414
+ if (linkFieldId && targetFieldId) {
415
+ const table = lock.tables.find((t) => t.fields.some((f) => f.id === linkFieldId));
416
+ const linkField = table?.fields.find((f) => f.id === linkFieldId);
417
+ const linkedTableId = linkField?.config?.linkedTableId;
418
+ if (linkedTableId) {
419
+ const linkedTable = lock.tables.find((t) => t.id === linkedTableId);
420
+ const targetField = linkedTable?.fields.find((f) => f.id === targetFieldId);
421
+ if (targetField) {
422
+ const resolved = airtableTsType(targetField, lock, (depth ?? 0) + 1);
423
+ return `${resolved}[] | undefined`;
424
+ }
425
+ }
426
+ }
427
+ }
410
428
  return AIRTABLE_FIELD_TYPE_MAP[field.type] ?? "unknown";
411
429
  }
430
+ const READ_ONLY_AIRTABLE_FIELDS = new Set([
431
+ "autoNumber",
432
+ "count",
433
+ "formula",
434
+ "rollup",
435
+ "multipleLookupValues",
436
+ "createdTime",
437
+ "lastModifiedTime",
438
+ "createdBy",
439
+ "lastModifiedBy",
440
+ "button",
441
+ "externalSyncSource",
442
+ "aiText",
443
+ ]);
444
+ function airtableFieldJsdoc(field, table) {
445
+ const parts = [];
446
+ if (table.primaryFieldId === field.id)
447
+ parts.push("Primary field");
448
+ if (READ_ONLY_AIRTABLE_FIELDS.has(field.type))
449
+ parts.push("Read-only; do not write");
450
+ if (field.type === "multipleRecordLinks") {
451
+ const linkedTableId = field.config?.linkedTableId;
452
+ if (linkedTableId)
453
+ parts.push(`Links to table ${linkedTableId}`);
454
+ if (field.config?.prefersSingleRecordLink)
455
+ parts.push("Single record only");
456
+ }
457
+ if (field.type === "multipleLookupValues")
458
+ parts.push("Lookup: value is an array — index or aggregate when displaying");
459
+ if (field.type === "percent")
460
+ parts.push("Raw decimal — multiply by 100 for display percentage");
461
+ parts.push(`"${field.name}"`);
462
+ if (field.type === "currency") {
463
+ const symbol = field.config?.symbol;
464
+ const precision = field.config?.precision;
465
+ if (symbol)
466
+ parts.push(`Display as currency with "${symbol}", ${precision ?? 2} decimals`);
467
+ }
468
+ if (field.type === "date") {
469
+ const fmt = field.config?.dateFormat;
470
+ if (fmt)
471
+ parts.push(`Date-only (ISO string), display as "${fmt}"`);
472
+ }
473
+ if (field.type === "dateTime") {
474
+ const dateFmt = field.config?.dateFormat;
475
+ const timeFmt = field.config?.timeFormat;
476
+ const tz = field.config?.timeZone;
477
+ const fmtParts = ["Date+time (ISO string)"];
478
+ if (dateFmt)
479
+ fmtParts.push(`date: ${dateFmt}`);
480
+ if (timeFmt)
481
+ fmtParts.push(`time: ${timeFmt}`);
482
+ if (tz)
483
+ fmtParts.push(`tz: ${tz}`);
484
+ parts.push(fmtParts.join(", "));
485
+ }
486
+ if (field.type === "rating") {
487
+ const max = field.config?.max;
488
+ if (max)
489
+ parts.push(`Max: ${max}`);
490
+ }
491
+ if (field.type === "duration") {
492
+ const fmt = field.config?.format;
493
+ if (fmt)
494
+ parts.push(`Value in seconds, display as "${fmt}"`);
495
+ }
496
+ if (field.type === "number" || field.type === "percent") {
497
+ const precision = field.config?.precision;
498
+ if (precision != null)
499
+ parts.push(`${precision} decimal places`);
500
+ }
501
+ if (parts.length <= 1)
502
+ return undefined;
503
+ return parts.join(". ");
504
+ }
412
505
  function generateAirtableTs(lock) {
413
506
  if (lock.tables.length === 0)
414
507
  return null;
415
508
  const lines = [
416
509
  "// Auto-generated by zitejs generate from zite.lock. Do not edit manually.",
417
510
  "// Airtable SDK — uses createAirtableClient from zitejs/runtime.",
418
- "// The airtable package is externalized (not bundled per-endpoint).",
419
511
  "//",
420
512
  "// Each exported table client has these methods:",
421
513
  "// findAll(options?: { offset?: string; limit?: number; filters?: unknown })",
@@ -425,6 +517,12 @@ function generateAirtableTs(lock) {
425
517
  "// bulkCreate(records: Partial<T>[]) => Promise<T[]>",
426
518
  "// update(id: string, data: { record: Partial<T> }) => Promise<T>",
427
519
  "// delete(id: string) => Promise<{ deleted: true }>",
520
+ "//",
521
+ "// Usage tips:",
522
+ "// - Airtable has a strict rate limit of 5 requests/second per base",
523
+ "// - Use bulkCreate() instead of calling create() in a loop",
524
+ "// - findAll() offsets are opaque strings from previous calls, NOT numbers",
525
+ "// - Always destructure record properties individually in create/update calls",
428
526
  "",
429
527
  "import { createAirtableClient } from 'zitejs/runtime';",
430
528
  "",
@@ -436,7 +534,11 @@ function generateAirtableTs(lock) {
436
534
  for (const field of table.fields) {
437
535
  if (field.sdkName === "id")
438
536
  continue;
439
- const tsType = airtableTsType(field);
537
+ const jsdoc = airtableFieldJsdoc(field, table);
538
+ if (jsdoc) {
539
+ lines.push(` /** ${jsdoc} */`);
540
+ }
541
+ const tsType = airtableTsType(field, lock);
440
542
  lines.push(` ${field.sdkName}: ${tsType};`);
441
543
  }
442
544
  lines.push("};");
@@ -4,4 +4,5 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown> {
4
4
  context: unknown;
5
5
  }) => Promise<TOutput> | TOutput;
6
6
  }
7
+ export declare function createCaller<TInput, TOutput>(name: string, flowId?: string): (input: TInput) => Promise<TOutput>;
7
8
  export declare function createCaller<TInput, TOutput>(endpoint: EndpointConfig<TInput, TOutput>, name: string, flowId?: string): (input: TInput) => Promise<TOutput>;
@@ -1,12 +1,14 @@
1
- import { getEnv } from '../internal/env.js';
1
+ import { getEnv } from "../internal/env.js";
2
2
  const ENVIRONMENTS = {
3
- production: 'https://workflows.zite.com',
4
- staging: 'https://workflows.zitestaging.com',
5
- local: 'http://localhost:2506',
3
+ production: "https://workflows.zite.com",
4
+ staging: "https://workflows.zitestaging.com",
5
+ local: "http://localhost:2506",
6
6
  };
7
7
  function getRunnerUrl() {
8
- const env = getEnv('ZITE_ENV', 'VITE_ZITE_ENV') ?? 'production';
9
- return getEnv('ZITE_RUNNER_URL', 'VITE_ZITE_RUNNER_URL') ?? ENVIRONMENTS[env] ?? ENVIRONMENTS.production;
8
+ const env = getEnv("ZITE_ENV", "VITE_ZITE_ENV") ?? "production";
9
+ return (getEnv("ZITE_RUNNER_URL", "VITE_ZITE_RUNNER_URL") ??
10
+ ENVIRONMENTS[env] ??
11
+ ENVIRONMENTS.production);
10
12
  }
11
13
  /**
12
14
  * Get the user's usage token for endpoint auth.
@@ -14,33 +16,39 @@ function getRunnerUrl() {
14
16
  * In external mode: stored in localStorage by the auth flow
15
17
  */
16
18
  function getUsageToken() {
17
- if (typeof window !== 'undefined') {
19
+ if (typeof window !== "undefined") {
18
20
  const win = window;
19
- if (typeof win._ziteUsageToken === 'string')
21
+ if (typeof win._ziteUsageToken === "string")
20
22
  return win._ziteUsageToken;
21
23
  }
22
24
  // Fallback to stored auth token (external mode)
23
- if (typeof localStorage !== 'undefined') {
24
- const stored = localStorage.getItem('zite.auth.token');
25
+ if (typeof localStorage !== "undefined") {
26
+ const stored = localStorage.getItem("zite.auth.token");
25
27
  if (stored)
26
28
  return stored;
27
29
  }
28
- return '';
30
+ return "";
29
31
  }
30
- export function createCaller(endpoint, name, flowId) {
32
+ export function createCaller(endpointOrName, nameOrFlowId, flowId) {
33
+ const name = typeof endpointOrName === "string" ? endpointOrName : nameOrFlowId;
34
+ const resolvedFlowId = typeof endpointOrName === "string" ? nameOrFlowId : flowId;
31
35
  return async (input) => {
32
- const appId = flowId ?? getEnv('ZITE_FLOW_ID', 'VITE_ZITE_FLOW_ID') ?? '';
36
+ const appId = resolvedFlowId ?? getEnv("ZITE_FLOW_ID", "VITE_ZITE_FLOW_ID") ?? "";
33
37
  const token = getUsageToken();
34
- const res = await fetch(getRunnerUrl() + '/public/' + appId + '/api/' + name, {
35
- method: 'POST',
38
+ const res = await fetch(getRunnerUrl() + "/public/" + appId + "/api/" + name, {
39
+ method: "POST",
36
40
  headers: {
37
41
  ...(token ? { Authorization: `Bearer ${token}` } : {}),
38
- 'Content-Type': 'application/json',
42
+ "Content-Type": "application/json",
39
43
  },
40
- body: JSON.stringify({ inputs: input, mode: 'preview', usageToken: token }),
44
+ body: JSON.stringify({
45
+ inputs: input,
46
+ mode: "preview",
47
+ usageToken: token,
48
+ }),
41
49
  });
42
50
  if (!res.ok) {
43
- const text = await res.text().catch(() => '');
51
+ const text = await res.text().catch(() => "");
44
52
  throw new Error(`API call failed (${res.status}): ${text}`);
45
53
  }
46
54
  return res.json();
@@ -1,8 +1,8 @@
1
- import { watch } from 'fs';
2
- import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, } from 'fs';
3
- import { join } from 'path';
4
- import { runSync } from '../sync/index.js';
5
- import { generateDbTs, generateApiTs, generateUserTs, generateAuthWrapperTs, generateBackendWrapperTs, generateAirtableTs, } from '../sync/lib.js';
1
+ import { watch } from "fs";
2
+ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, } from "fs";
3
+ import { join } from "path";
4
+ import { runSync } from "../sync/index.js";
5
+ import { generateDbTs, generateApiTs, generateUserTs, generateAuthWrapperTs, generateBackendWrapperTs, generateAirtableTs, } from "../sync/lib.js";
6
6
  const debounceTimers = new Map();
7
7
  function debounce(key, fn, ms) {
8
8
  const existing = debounceTimers.get(key);
@@ -11,18 +11,18 @@ function debounce(key, fn, ms) {
11
11
  debounceTimers.set(key, setTimeout(fn, ms));
12
12
  }
13
13
  function findAppDirs() {
14
- const appsDir = 'apps';
14
+ const appsDir = "apps";
15
15
  if (!existsSync(appsDir))
16
16
  return [];
17
17
  return readdirSync(appsDir, { withFileTypes: true })
18
- .filter(d => d.isDirectory())
19
- .map(d => d.name);
18
+ .filter((d) => d.isDirectory())
19
+ .map((d) => d.name);
20
20
  }
21
21
  function getFlowId(appDir) {
22
22
  try {
23
- const configPath = join('apps', appDir, 'zite.config.json');
23
+ const configPath = join("apps", appDir, "zite.config.json");
24
24
  if (existsSync(configPath)) {
25
- const config = JSON.parse(readFileSync(configPath, 'utf-8'));
25
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
26
26
  return config.id;
27
27
  }
28
28
  }
@@ -30,32 +30,32 @@ function getFlowId(appDir) {
30
30
  return undefined;
31
31
  }
32
32
  function regenerateAppTypedWrappers(appDir) {
33
- const outDir = join('apps', appDir, '.zite');
33
+ const outDir = join("apps", appDir, ".zite");
34
34
  mkdirSync(outDir, { recursive: true });
35
- writeFileSync(join(outDir, 'user.ts'), generateUserTs());
36
- writeFileSync(join(outDir, 'auth.ts'), generateAuthWrapperTs());
37
- writeFileSync(join(outDir, 'backend.ts'), generateBackendWrapperTs());
35
+ writeFileSync(join(outDir, "user.ts"), generateUserTs());
36
+ writeFileSync(join(outDir, "auth.ts"), generateAuthWrapperTs());
37
+ writeFileSync(join(outDir, "backend.ts"), generateBackendWrapperTs());
38
38
  }
39
39
  function regenerateAppApiTs(appDir) {
40
- const apiDir = join('apps', appDir, 'src', 'api');
40
+ const apiDir = join("apps", appDir, "src", "api");
41
41
  if (!existsSync(apiDir))
42
42
  return;
43
- const endpointFiles = readdirSync(apiDir).filter((f) => f.endsWith('.ts') || f.endsWith('.js'));
43
+ const endpointFiles = readdirSync(apiDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js"));
44
44
  const content = generateApiTs(endpointFiles);
45
45
  if (content) {
46
- const outDir = join('apps', appDir, '.zite');
46
+ const outDir = join("apps", appDir, ".zite");
47
47
  mkdirSync(outDir, { recursive: true });
48
- writeFileSync(join(outDir, 'api.ts'), content);
48
+ writeFileSync(join(outDir, "api.ts"), content);
49
49
  console.log(`Updated apps/${appDir}/.zite/api.ts`);
50
50
  }
51
51
  }
52
52
  function regenerateAppAirtableSdk(appDir) {
53
- const lockPath = join('apps', appDir, 'zite.lock');
53
+ const lockPath = join("apps", appDir, "zite.lock");
54
54
  console.log(`[airtable-sdk] Checking ${lockPath} exists: ${existsSync(lockPath)}`);
55
55
  if (!existsSync(lockPath))
56
56
  return;
57
57
  try {
58
- const lockContent = JSON.parse(readFileSync(lockPath, 'utf-8'));
58
+ const lockContent = JSON.parse(readFileSync(lockPath, "utf-8"));
59
59
  const integrations = lockContent.integrations ?? {};
60
60
  console.log(`[airtable-sdk] Found ${Object.keys(integrations).length} integrations in lock:`, Object.keys(integrations));
61
61
  for (const [integrationId, integration] of Object.entries(integrations)) {
@@ -71,9 +71,9 @@ function regenerateAppAirtableSdk(appDir) {
71
71
  };
72
72
  const content = generateAirtableTs(airtableLock);
73
73
  if (content) {
74
- const outDir = join('apps', appDir, '.zite', 'integrations');
74
+ const outDir = join("apps", appDir, ".zite", "integrations");
75
75
  mkdirSync(outDir, { recursive: true });
76
- writeFileSync(join(outDir, 'airtable.ts'), content);
76
+ writeFileSync(join(outDir, "airtable.ts"), content);
77
77
  console.log(`Updated apps/${appDir}/.zite/integrations/airtable.ts`);
78
78
  }
79
79
  }
@@ -92,16 +92,16 @@ export async function runGenerate() {
92
92
  // metadata from the backend API).
93
93
  // 1. Generate root .zite/db.ts from zite.schema.json
94
94
  try {
95
- if (existsSync('zite.schema.json')) {
96
- const schema = JSON.parse(readFileSync('zite.schema.json', 'utf-8'));
95
+ if (existsSync("zite.schema.json")) {
96
+ const schema = JSON.parse(readFileSync("zite.schema.json", "utf-8"));
97
97
  const dbTs = generateDbTs(schema);
98
- mkdirSync('.zite', { recursive: true });
99
- writeFileSync(join('.zite', 'db.ts'), dbTs);
100
- console.log('Wrote .zite/db.ts');
98
+ mkdirSync(".zite", { recursive: true });
99
+ writeFileSync(join(".zite", "db.ts"), dbTs);
100
+ console.log("Wrote .zite/db.ts");
101
101
  }
102
102
  }
103
103
  catch (err) {
104
- console.warn('DB SDK generation failed:', err instanceof Error ? err.message : err);
104
+ console.warn("DB SDK generation failed:", err instanceof Error ? err.message : err);
105
105
  }
106
106
  // 2. Find all apps and regenerate their .zite/ files
107
107
  const appDirs = findAppDirs();
@@ -110,20 +110,20 @@ export async function runGenerate() {
110
110
  regenerateAppTypedWrappers(app);
111
111
  regenerateAppAirtableSdk(app);
112
112
  }
113
- console.log('Done!');
113
+ console.log("Done!");
114
114
  }
115
115
  export async function runDev() {
116
- const env = process.env.ZITE_ENV ?? 'production';
116
+ const env = process.env.ZITE_ENV ?? "production";
117
117
  console.log(`zitejs dev — environment: ${env}`);
118
- console.log('');
118
+ console.log("");
119
119
  // 1. Run initial sync (generates root .zite/db.ts)
120
120
  try {
121
121
  await runSync();
122
122
  }
123
123
  catch (err) {
124
- console.warn('Initial sync failed (continuing with watcher):', err instanceof Error ? err.message : err);
124
+ console.warn("Initial sync failed (continuing with watcher):", err instanceof Error ? err.message : err);
125
125
  }
126
- console.log('');
126
+ console.log("");
127
127
  // 2. Find all apps and regenerate their .zite/ files
128
128
  const appDirs = findAppDirs();
129
129
  for (const app of appDirs) {
@@ -133,7 +133,7 @@ export async function runDev() {
133
133
  // 3. Watch each app's src/api/ for endpoint changes
134
134
  let watchingAny = false;
135
135
  for (const app of appDirs) {
136
- const apiDir = join('apps', app, 'src', 'api');
136
+ const apiDir = join("apps", app, "src", "api");
137
137
  if (!existsSync(apiDir))
138
138
  continue;
139
139
  watchingAny = true;
@@ -141,7 +141,7 @@ export async function runDev() {
141
141
  watch(apiDir, { recursive: true }, (_event, filename) => {
142
142
  if (!filename)
143
143
  return;
144
- if (!filename.endsWith('.ts') && !filename.endsWith('.js'))
144
+ if (!filename.endsWith(".ts") && !filename.endsWith(".js"))
145
145
  return;
146
146
  debounce(app, () => {
147
147
  console.log(`Endpoint changed in ${app}: ${filename}`);
@@ -150,24 +150,24 @@ export async function runDev() {
150
150
  });
151
151
  }
152
152
  if (!watchingAny) {
153
- console.log('No apps with src/api/ found.');
153
+ console.log("No apps with src/api/ found.");
154
154
  }
155
155
  // 4. Watch zite.schema.json for schema drift
156
- if (existsSync('zite.schema.json')) {
157
- console.log('Watching zite.schema.json for schema changes...');
158
- watch('zite.schema.json', () => {
159
- debounce('schema', async () => {
160
- console.log('Schema changed — re-running sync...');
156
+ if (existsSync("zite.schema.json")) {
157
+ console.log("Watching zite.schema.json for schema changes...");
158
+ watch("zite.schema.json", () => {
159
+ debounce("schema", async () => {
160
+ console.log("Schema changed — re-running sync...");
161
161
  try {
162
162
  await runSync();
163
163
  }
164
164
  catch (err) {
165
- console.error('Re-sync failed:', err instanceof Error ? err.message : err);
165
+ console.error("Re-sync failed:", err instanceof Error ? err.message : err);
166
166
  }
167
167
  }, 500);
168
168
  });
169
169
  }
170
- console.log('');
171
- console.log('Ready. Watching for changes... (Ctrl+C to stop)');
170
+ console.log("");
171
+ console.log("Ready. Watching for changes... (Ctrl+C to stop)");
172
172
  await new Promise(() => { });
173
173
  }
@@ -40,11 +40,13 @@ export type AirtableLockField = {
40
40
  id: string;
41
41
  sdkName: string;
42
42
  type: string;
43
- options?: string[];
43
+ name: string;
44
+ config?: Record<string, unknown>;
44
45
  };
45
46
  export type AirtableLockTable = {
46
47
  id: string;
47
48
  sdkName: string;
49
+ primaryFieldId: string;
48
50
  fields: AirtableLockField[];
49
51
  };
50
52
  export type AirtableLock = {
@@ -271,18 +271,18 @@ export function generateApiTs(endpointFiles) {
271
271
  for (const file of endpointFiles) {
272
272
  const name = file.replace(/\.(ts|js)$/, "");
273
273
  const camelName = toCamelCase(name);
274
- lines.push(`import ${camelName}Endpoint from '../src/api/${name}';`);
274
+ const pascal = toPascalCase(camelName);
275
+ lines.push(`import type { default as _${pascal}Ep } from '../src/api/${name}';`);
275
276
  endpointNames.push(camelName);
276
277
  }
277
278
  lines.push("");
278
- for (const name of endpointNames) {
279
- lines.push(`export const ${name} = createCaller(${name}Endpoint, '${name}');`);
280
- }
281
- lines.push("");
282
279
  for (const name of endpointNames) {
283
280
  const pascal = toPascalCase(name);
284
- lines.push(`export type ${pascal}InputType = Parameters<typeof ${name}Endpoint.execute>[0]['input'];`);
285
- lines.push(`export type ${pascal}OutputType = Awaited<ReturnType<typeof ${name}Endpoint.execute>>;`);
281
+ lines.push(`type _${pascal}Cfg = typeof _${pascal}Ep;`);
282
+ lines.push(`export type ${pascal}InputType = Parameters<_${pascal}Cfg['execute']>[0]['input'];`);
283
+ lines.push(`export type ${pascal}OutputType = Awaited<ReturnType<_${pascal}Cfg['execute']>>;`);
284
+ lines.push(`export const ${name} = createCaller<${pascal}InputType, ${pascal}OutputType>('${name}');`);
285
+ lines.push("");
286
286
  }
287
287
  lines.push("");
288
288
  lines.push("export const api = {");
@@ -383,11 +383,12 @@ const AIRTABLE_FIELD_TYPE_MAP = {
383
383
  externalSyncSource: "unknown",
384
384
  aiText: "string",
385
385
  };
386
- function airtableTsType(field) {
386
+ function airtableTsType(field, lock, depth) {
387
+ const choices = field.config?.choices;
387
388
  if ((field.type === "singleSelect" || field.type === "multipleSelects") &&
388
- field.options &&
389
- field.options.length > 0) {
390
- const literals = field.options
389
+ choices &&
390
+ choices.length > 0) {
391
+ const literals = choices
391
392
  .slice(0, MAX_SELECT_OPTIONS)
392
393
  .map((o) => `"${o.replace(/"/g, '\\"')}"`)
393
394
  .join(" | ");
@@ -396,15 +397,106 @@ function airtableTsType(field) {
396
397
  return `(${union})[]`;
397
398
  return union;
398
399
  }
400
+ if (field.type === "multipleLookupValues" && lock && (depth ?? 0) < 5) {
401
+ const linkFieldId = field.config?.recordLinkFieldId;
402
+ const targetFieldId = field.config?.fieldIdInLinkedTable;
403
+ if (linkFieldId && targetFieldId) {
404
+ const table = lock.tables.find((t) => t.fields.some((f) => f.id === linkFieldId));
405
+ const linkField = table?.fields.find((f) => f.id === linkFieldId);
406
+ const linkedTableId = linkField?.config?.linkedTableId;
407
+ if (linkedTableId) {
408
+ const linkedTable = lock.tables.find((t) => t.id === linkedTableId);
409
+ const targetField = linkedTable?.fields.find((f) => f.id === targetFieldId);
410
+ if (targetField) {
411
+ const resolved = airtableTsType(targetField, lock, (depth ?? 0) + 1);
412
+ return `${resolved}[] | undefined`;
413
+ }
414
+ }
415
+ }
416
+ }
399
417
  return AIRTABLE_FIELD_TYPE_MAP[field.type] ?? "unknown";
400
418
  }
419
+ const READ_ONLY_AIRTABLE_FIELDS = new Set([
420
+ "autoNumber",
421
+ "count",
422
+ "formula",
423
+ "rollup",
424
+ "multipleLookupValues",
425
+ "createdTime",
426
+ "lastModifiedTime",
427
+ "createdBy",
428
+ "lastModifiedBy",
429
+ "button",
430
+ "externalSyncSource",
431
+ "aiText",
432
+ ]);
433
+ function airtableFieldJsdoc(field, table) {
434
+ const parts = [];
435
+ if (table.primaryFieldId === field.id)
436
+ parts.push("Primary field");
437
+ if (READ_ONLY_AIRTABLE_FIELDS.has(field.type))
438
+ parts.push("Read-only; do not write");
439
+ if (field.type === "multipleRecordLinks") {
440
+ const linkedTableId = field.config?.linkedTableId;
441
+ if (linkedTableId)
442
+ parts.push(`Links to table ${linkedTableId}`);
443
+ if (field.config?.prefersSingleRecordLink)
444
+ parts.push("Single record only");
445
+ }
446
+ if (field.type === "multipleLookupValues")
447
+ parts.push("Lookup: value is an array — index or aggregate when displaying");
448
+ if (field.type === "percent")
449
+ parts.push("Raw decimal — multiply by 100 for display percentage");
450
+ parts.push(`"${field.name}"`);
451
+ if (field.type === "currency") {
452
+ const symbol = field.config?.symbol;
453
+ const precision = field.config?.precision;
454
+ if (symbol)
455
+ parts.push(`Display as currency with "${symbol}", ${precision ?? 2} decimals`);
456
+ }
457
+ if (field.type === "date") {
458
+ const fmt = field.config?.dateFormat;
459
+ if (fmt)
460
+ parts.push(`Date-only (ISO string), display as "${fmt}"`);
461
+ }
462
+ if (field.type === "dateTime") {
463
+ const dateFmt = field.config?.dateFormat;
464
+ const timeFmt = field.config?.timeFormat;
465
+ const tz = field.config?.timeZone;
466
+ const fmtParts = ["Date+time (ISO string)"];
467
+ if (dateFmt)
468
+ fmtParts.push(`date: ${dateFmt}`);
469
+ if (timeFmt)
470
+ fmtParts.push(`time: ${timeFmt}`);
471
+ if (tz)
472
+ fmtParts.push(`tz: ${tz}`);
473
+ parts.push(fmtParts.join(", "));
474
+ }
475
+ if (field.type === "rating") {
476
+ const max = field.config?.max;
477
+ if (max)
478
+ parts.push(`Max: ${max}`);
479
+ }
480
+ if (field.type === "duration") {
481
+ const fmt = field.config?.format;
482
+ if (fmt)
483
+ parts.push(`Value in seconds, display as "${fmt}"`);
484
+ }
485
+ if (field.type === "number" || field.type === "percent") {
486
+ const precision = field.config?.precision;
487
+ if (precision != null)
488
+ parts.push(`${precision} decimal places`);
489
+ }
490
+ if (parts.length <= 1)
491
+ return undefined;
492
+ return parts.join(". ");
493
+ }
401
494
  export function generateAirtableTs(lock) {
402
495
  if (lock.tables.length === 0)
403
496
  return null;
404
497
  const lines = [
405
498
  "// Auto-generated by zitejs generate from zite.lock. Do not edit manually.",
406
499
  "// Airtable SDK — uses createAirtableClient from zitejs/runtime.",
407
- "// The airtable package is externalized (not bundled per-endpoint).",
408
500
  "//",
409
501
  "// Each exported table client has these methods:",
410
502
  "// findAll(options?: { offset?: string; limit?: number; filters?: unknown })",
@@ -414,6 +506,12 @@ export function generateAirtableTs(lock) {
414
506
  "// bulkCreate(records: Partial<T>[]) => Promise<T[]>",
415
507
  "// update(id: string, data: { record: Partial<T> }) => Promise<T>",
416
508
  "// delete(id: string) => Promise<{ deleted: true }>",
509
+ "//",
510
+ "// Usage tips:",
511
+ "// - Airtable has a strict rate limit of 5 requests/second per base",
512
+ "// - Use bulkCreate() instead of calling create() in a loop",
513
+ "// - findAll() offsets are opaque strings from previous calls, NOT numbers",
514
+ "// - Always destructure record properties individually in create/update calls",
417
515
  "",
418
516
  "import { createAirtableClient } from 'zitejs/runtime';",
419
517
  "",
@@ -425,7 +523,11 @@ export function generateAirtableTs(lock) {
425
523
  for (const field of table.fields) {
426
524
  if (field.sdkName === "id")
427
525
  continue;
428
- const tsType = airtableTsType(field);
526
+ const jsdoc = airtableFieldJsdoc(field, table);
527
+ if (jsdoc) {
528
+ lines.push(` /** ${jsdoc} */`);
529
+ }
530
+ const tsType = airtableTsType(field, lock);
429
531
  lines.push(` ${field.sdkName}: ${tsType};`);
430
532
  }
431
533
  lines.push("};");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.51",
3
+ "version": "0.9.53",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",