zitejs 0.9.50 → 0.9.52

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.
@@ -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 = {
@@ -106,9 +106,7 @@ function fieldJsdoc(schemaField, table) {
106
106
  }
107
107
  if (def.type === "datetime") {
108
108
  const tpl = def.template;
109
- const timeParts = [
110
- "Date+time field (ISO 8601 timestamp)",
111
- ];
109
+ const timeParts = ["Date+time field (ISO 8601 timestamp)"];
112
110
  if (tpl.dateFormat)
113
111
  timeParts.push(`date: ${tpl.dateFormat}`);
114
112
  if (tpl.timeFormat)
@@ -156,7 +154,8 @@ function fieldJsdoc(schemaField, table) {
156
154
  }
157
155
  if ((def.type === "single_select" || def.type === "multiple_select") &&
158
156
  def.template?.options) {
159
- const count = (def.template.options ?? []).length;
157
+ const count = (def.template.options ?? [])
158
+ .length;
160
159
  if (count > MAX_SELECT_OPTIONS) {
161
160
  parts.push(`${count - MAX_SELECT_OPTIONS} more options omitted`);
162
161
  }
@@ -395,11 +394,12 @@ const AIRTABLE_FIELD_TYPE_MAP = {
395
394
  externalSyncSource: "unknown",
396
395
  aiText: "string",
397
396
  };
398
- function airtableTsType(field) {
397
+ function airtableTsType(field, lock, depth) {
398
+ const choices = field.config?.choices;
399
399
  if ((field.type === "singleSelect" || field.type === "multipleSelects") &&
400
- field.options &&
401
- field.options.length > 0) {
402
- const literals = field.options
400
+ choices &&
401
+ choices.length > 0) {
402
+ const literals = choices
403
403
  .slice(0, MAX_SELECT_OPTIONS)
404
404
  .map((o) => `"${o.replace(/"/g, '\\"')}"`)
405
405
  .join(" | ");
@@ -408,15 +408,121 @@ function airtableTsType(field) {
408
408
  return `(${union})[]`;
409
409
  return union;
410
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
+ }
411
428
  return AIRTABLE_FIELD_TYPE_MAP[field.type] ?? "unknown";
412
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
+ }
413
505
  function generateAirtableTs(lock) {
414
506
  if (lock.tables.length === 0)
415
507
  return null;
416
508
  const lines = [
417
509
  "// Auto-generated by zitejs generate from zite.lock. Do not edit manually.",
418
510
  "// Airtable SDK — uses createAirtableClient from zitejs/runtime.",
419
- "// The airtable package is externalized (not bundled per-endpoint).",
511
+ "//",
512
+ "// Each exported table client has these methods:",
513
+ "// findAll(options?: { offset?: string; limit?: number; filters?: unknown })",
514
+ "// => Promise<{ records: T[]; offset: string | undefined; hasMore: boolean }>",
515
+ "// findOne(params: { id?: string; filters?: unknown }) => Promise<T | undefined>",
516
+ "// create(data: { record: Partial<T> }) => Promise<T>",
517
+ "// bulkCreate(records: Partial<T>[]) => Promise<T[]>",
518
+ "// update(id: string, data: { record: Partial<T> }) => Promise<T>",
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",
420
526
  "",
421
527
  "import { createAirtableClient } from 'zitejs/runtime';",
422
528
  "",
@@ -428,7 +534,11 @@ function generateAirtableTs(lock) {
428
534
  for (const field of table.fields) {
429
535
  if (field.sdkName === "id")
430
536
  continue;
431
- const tsType = airtableTsType(field);
537
+ const jsdoc = airtableFieldJsdoc(field, table);
538
+ if (jsdoc) {
539
+ lines.push(` /** ${jsdoc} */`);
540
+ }
541
+ const tsType = airtableTsType(field, lock);
432
542
  lines.push(` ${field.sdkName}: ${tsType};`);
433
543
  }
434
544
  lines.push("};");
@@ -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 = {
@@ -95,9 +95,7 @@ function fieldJsdoc(schemaField, table) {
95
95
  }
96
96
  if (def.type === "datetime") {
97
97
  const tpl = def.template;
98
- const timeParts = [
99
- "Date+time field (ISO 8601 timestamp)",
100
- ];
98
+ const timeParts = ["Date+time field (ISO 8601 timestamp)"];
101
99
  if (tpl.dateFormat)
102
100
  timeParts.push(`date: ${tpl.dateFormat}`);
103
101
  if (tpl.timeFormat)
@@ -145,7 +143,8 @@ function fieldJsdoc(schemaField, table) {
145
143
  }
146
144
  if ((def.type === "single_select" || def.type === "multiple_select") &&
147
145
  def.template?.options) {
148
- const count = (def.template.options ?? []).length;
146
+ const count = (def.template.options ?? [])
147
+ .length;
149
148
  if (count > MAX_SELECT_OPTIONS) {
150
149
  parts.push(`${count - MAX_SELECT_OPTIONS} more options omitted`);
151
150
  }
@@ -384,11 +383,12 @@ const AIRTABLE_FIELD_TYPE_MAP = {
384
383
  externalSyncSource: "unknown",
385
384
  aiText: "string",
386
385
  };
387
- function airtableTsType(field) {
386
+ function airtableTsType(field, lock, depth) {
387
+ const choices = field.config?.choices;
388
388
  if ((field.type === "singleSelect" || field.type === "multipleSelects") &&
389
- field.options &&
390
- field.options.length > 0) {
391
- const literals = field.options
389
+ choices &&
390
+ choices.length > 0) {
391
+ const literals = choices
392
392
  .slice(0, MAX_SELECT_OPTIONS)
393
393
  .map((o) => `"${o.replace(/"/g, '\\"')}"`)
394
394
  .join(" | ");
@@ -397,15 +397,121 @@ function airtableTsType(field) {
397
397
  return `(${union})[]`;
398
398
  return union;
399
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
+ }
400
417
  return AIRTABLE_FIELD_TYPE_MAP[field.type] ?? "unknown";
401
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
+ }
402
494
  export function generateAirtableTs(lock) {
403
495
  if (lock.tables.length === 0)
404
496
  return null;
405
497
  const lines = [
406
498
  "// Auto-generated by zitejs generate from zite.lock. Do not edit manually.",
407
499
  "// Airtable SDK — uses createAirtableClient from zitejs/runtime.",
408
- "// The airtable package is externalized (not bundled per-endpoint).",
500
+ "//",
501
+ "// Each exported table client has these methods:",
502
+ "// findAll(options?: { offset?: string; limit?: number; filters?: unknown })",
503
+ "// => Promise<{ records: T[]; offset: string | undefined; hasMore: boolean }>",
504
+ "// findOne(params: { id?: string; filters?: unknown }) => Promise<T | undefined>",
505
+ "// create(data: { record: Partial<T> }) => Promise<T>",
506
+ "// bulkCreate(records: Partial<T>[]) => Promise<T[]>",
507
+ "// update(id: string, data: { record: Partial<T> }) => Promise<T>",
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",
409
515
  "",
410
516
  "import { createAirtableClient } from 'zitejs/runtime';",
411
517
  "",
@@ -417,7 +523,11 @@ export function generateAirtableTs(lock) {
417
523
  for (const field of table.fields) {
418
524
  if (field.sdkName === "id")
419
525
  continue;
420
- const tsType = airtableTsType(field);
526
+ const jsdoc = airtableFieldJsdoc(field, table);
527
+ if (jsdoc) {
528
+ lines.push(` /** ${jsdoc} */`);
529
+ }
530
+ const tsType = airtableTsType(field, lock);
421
531
  lines.push(` ${field.sdkName}: ${tsType};`);
422
532
  }
423
533
  lines.push("};");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.50",
3
+ "version": "0.9.52",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",