zitejs 0.9.51 → 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 = {
@@ -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("};");
@@ -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 = {
@@ -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.52",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",