zitejs 0.9.41 → 0.9.43

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.
@@ -2,7 +2,7 @@ import { watch } from 'fs';
2
2
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { runSync } from '../sync/index.js';
5
- import { generateApiTs, generateUserTs, generateAuthWrapperTs, generateBackendWrapperTs, } from '../sync/lib.js';
5
+ import { 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);
@@ -49,6 +49,42 @@ function regenerateAppApiTs(appDir) {
49
49
  console.log(`Updated apps/${appDir}/.zite/api.ts`);
50
50
  }
51
51
  }
52
+ function regenerateAppAirtableSdk(appDir) {
53
+ const lockPath = join('apps', appDir, 'zite.lock');
54
+ if (!existsSync(lockPath))
55
+ return;
56
+ try {
57
+ const lockContent = JSON.parse(readFileSync(lockPath, 'utf-8'));
58
+ const integrations = lockContent.integrations ?? {};
59
+ for (const [integrationId, integration] of Object.entries(integrations)) {
60
+ const int = integration;
61
+ if (!int.idMappings?.tables)
62
+ continue;
63
+ const airtableLock = {
64
+ integrationId,
65
+ tables: Object.entries(int.idMappings.tables).map(([sdkName, tableId]) => ({
66
+ id: tableId,
67
+ sdkName,
68
+ fields: Object.entries(int.idMappings?.fields?.[sdkName] ?? {}).map(([fieldSdkName, fieldId]) => ({
69
+ id: fieldId,
70
+ sdkName: fieldSdkName,
71
+ type: 'singleLineText',
72
+ })),
73
+ })),
74
+ };
75
+ const content = generateAirtableTs(airtableLock);
76
+ if (content) {
77
+ const outDir = join('apps', appDir, '.zite', 'integrations');
78
+ mkdirSync(outDir, { recursive: true });
79
+ writeFileSync(join(outDir, 'airtable.ts'), content);
80
+ console.log(`Updated apps/${appDir}/.zite/integrations/airtable.ts`);
81
+ }
82
+ }
83
+ }
84
+ catch {
85
+ // Lock file invalid or missing — skip
86
+ }
87
+ }
52
88
  export async function runGenerate() {
53
89
  // 1. Run sync (generates root .zite/db.ts)
54
90
  try {
@@ -62,6 +98,7 @@ export async function runGenerate() {
62
98
  for (const app of appDirs) {
63
99
  regenerateAppApiTs(app);
64
100
  regenerateAppTypedWrappers(app);
101
+ regenerateAppAirtableSdk(app);
65
102
  }
66
103
  console.log('Done!');
67
104
  }
@@ -39,5 +39,31 @@ export declare function createSqlClient(): (params: {
39
39
  query: string;
40
40
  params?: unknown[];
41
41
  }) => Promise<SqlResult>;
42
+ export interface AirtableTableClient<T> {
43
+ findAll(options?: {
44
+ offset?: string;
45
+ limit?: number;
46
+ filters?: unknown;
47
+ }): Promise<{
48
+ records: T[];
49
+ offset: string | undefined;
50
+ hasMore: boolean;
51
+ }>;
52
+ findOne(params: {
53
+ id?: string;
54
+ filters?: unknown;
55
+ }): Promise<T | undefined>;
56
+ create(data: {
57
+ record: Partial<T>;
58
+ }): Promise<T>;
59
+ bulkCreate(records: Partial<T>[]): Promise<T[]>;
60
+ update(id: string, data: {
61
+ record: Partial<T>;
62
+ }): Promise<T>;
63
+ delete(id: string): Promise<{
64
+ deleted: true;
65
+ }>;
66
+ }
67
+ export declare function createAirtableClient<T>(integrationId: string, className: string, implicitParams: Record<string, unknown>): AirtableTableClient<T>;
42
68
  export { createCaller } from "../caller/index.js";
43
69
  export type { EndpointConfig } from "../caller/index.js";
@@ -71,4 +71,33 @@ export function createSqlClient() {
71
71
  ...params,
72
72
  });
73
73
  }
74
+ export function createAirtableClient(integrationId, className, implicitParams) {
75
+ return {
76
+ findAll: (options) => getSdkCall()(integrationId, className, "findAll", {
77
+ ...implicitParams,
78
+ ...options,
79
+ }),
80
+ findOne: (params) => getSdkCall()(integrationId, className, "findOne", {
81
+ ...implicitParams,
82
+ ...params,
83
+ }),
84
+ create: (data) => getSdkCall()(integrationId, className, "create", {
85
+ ...implicitParams,
86
+ ...data,
87
+ }),
88
+ bulkCreate: (records) => getSdkCall()(integrationId, className, "bulkCreate", {
89
+ ...implicitParams,
90
+ records,
91
+ }),
92
+ update: (id, data) => getSdkCall()(integrationId, className, "update", {
93
+ ...implicitParams,
94
+ id,
95
+ ...data,
96
+ }),
97
+ delete: (id) => getSdkCall()(integrationId, className, "delete", {
98
+ ...implicitParams,
99
+ id,
100
+ }),
101
+ };
102
+ }
74
103
  export { createCaller } from "../caller/index.js";
@@ -1,16 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, } from "fs";
3
3
  import { join } from "path";
4
- import { generateSchema, generateDbTs, generateApiTs } from "./lib.js";
4
+ import { generateSchema, generateDbTs, generateApiTs, } from "./lib.js";
5
5
  const DB_ENVIRONMENTS = {
6
6
  production: "https://tables.fillout.com/api/v1",
7
7
  staging: "https://tables.filloutstaging.com/api/v1",
8
8
  local: "http://localhost:2507/api/v1",
9
9
  };
10
10
  const env = process.env.ZITE_ENV ?? "production";
11
- const BASE_URL = process.env.ZITE_DB_URL ?? DB_ENVIRONMENTS[env] ?? DB_ENVIRONMENTS.production;
11
+ const BASE_URL = process.env.ZITE_DB_URL ??
12
+ DB_ENVIRONMENTS[env] ??
13
+ DB_ENVIRONMENTS.production;
12
14
  const TOKEN = process.env.ZITE_DB_TOKEN ?? "";
13
- async function fetchBaseMetadata(baseId) {
15
+ async function fetchDatabase(baseId) {
14
16
  const url = `${BASE_URL}/bases/${encodeURIComponent(baseId)}`;
15
17
  const res = await fetch(url, {
16
18
  headers: { Authorization: `Bearer ${TOKEN}` },
@@ -31,6 +33,15 @@ function resolveBaseId() {
31
33
  return undefined;
32
34
  }
33
35
  }
36
+ function readExistingSchema() {
37
+ try {
38
+ if (existsSync("zite.schema.json")) {
39
+ return JSON.parse(readFileSync("zite.schema.json", "utf-8"));
40
+ }
41
+ }
42
+ catch { }
43
+ return undefined;
44
+ }
34
45
  export async function runSync() {
35
46
  if (!TOKEN) {
36
47
  throw new Error("ZITE_DB_TOKEN is required.");
@@ -40,12 +51,13 @@ export async function runSync() {
40
51
  throw new Error("Could not determine base ID.");
41
52
  }
42
53
  console.log(`Syncing database ${baseId}...`);
43
- const base = await fetchBaseMetadata(baseId);
44
- console.log(`Found ${base.tables?.length ?? 0} tables`);
45
- const schema = generateSchema(base);
54
+ const database = await fetchDatabase(baseId);
55
+ console.log(`Found ${database.tables?.length ?? 0} tables`);
56
+ const existingSchema = readExistingSchema();
57
+ const schema = generateSchema(database, existingSchema);
46
58
  writeFileSync("zite.schema.json", JSON.stringify(schema, null, 2));
47
59
  console.log("Wrote zite.schema.json");
48
- const dbTs = generateDbTs(base, schema);
60
+ const dbTs = generateDbTs(schema);
49
61
  const dotZite = ".zite";
50
62
  mkdirSync(dotZite, { recursive: true });
51
63
  writeFileSync(join(dotZite, "db.ts"), dbTs);
@@ -1,30 +1,55 @@
1
- export type BaseMetadata = {
2
- tables?: Array<{
3
- id: string;
4
- name: string;
5
- fields?: Array<{
6
- id: string;
7
- name: string;
8
- type: string;
9
- }>;
10
- }>;
1
+ import type { PublicFieldDefinition } from "../types/fields.js";
2
+ import type { Database } from "../types/tables.js";
3
+ export type ZiteSchemaField = {
4
+ id: string;
5
+ sdkName: string;
6
+ definition: PublicFieldDefinition;
7
+ };
8
+ export type ZiteSchemaTable = {
9
+ id: string;
10
+ sdkName: string;
11
+ primaryFieldId?: string;
12
+ fields: ZiteSchemaField[];
11
13
  };
12
14
  export type ZiteSchema = {
13
- tables: Record<string, {
14
- id: string;
15
- fields: Record<string, {
16
- id: string;
17
- }>;
18
- }>;
15
+ tables: ZiteSchemaTable[];
19
16
  };
20
17
  export declare function toPascalCase(name: string): string;
21
18
  export declare function toCamelCase(name: string): string;
22
- export declare function generateSchema(base: BaseMetadata): ZiteSchema;
23
- export declare function generateDbTs(base: BaseMetadata, schema: ZiteSchema): string;
19
+ /**
20
+ * Build a ZiteSchema from a Database API response.
21
+ *
22
+ * If `existingSchema` is provided, locked SDK names are preserved for
23
+ * tables/fields that already exist (matched by ID). New tables/fields
24
+ * get fresh camelCase SDK names. This is how `zitejs sync` preserves
25
+ * name stability across schema changes.
26
+ */
27
+ export declare function generateSchema(database: Database, existingSchema?: ZiteSchema): ZiteSchema;
28
+ /**
29
+ * Generate .zite/db.ts purely from ZiteSchema.
30
+ * Pure function — no external data needed beyond what's in the schema file.
31
+ */
32
+ export declare function generateDbTs(schema: ZiteSchema): string;
24
33
  export declare function generateApiTs(endpointFiles: string[]): string | null;
25
34
  export declare function generateUserTs(usersTableFields?: Array<{
26
35
  name: string;
27
36
  type: string;
28
37
  }>): string;
29
38
  export declare function generateAuthWrapperTs(): string;
39
+ export type AirtableLockField = {
40
+ id: string;
41
+ sdkName: string;
42
+ type: string;
43
+ options?: string[];
44
+ };
45
+ export type AirtableLockTable = {
46
+ id: string;
47
+ sdkName: string;
48
+ fields: AirtableLockField[];
49
+ };
50
+ export type AirtableLock = {
51
+ integrationId: string;
52
+ tables: AirtableLockTable[];
53
+ };
54
+ export declare function generateAirtableTs(lock: AirtableLock): string | null;
30
55
  export declare function generateBackendWrapperTs(): string;
@@ -1,6 +1,7 @@
1
1
  const FIELD_TYPE_MAP = {
2
2
  single_line_text: "string",
3
3
  long_text: "string",
4
+ rich_text: "string",
4
5
  email: "string",
5
6
  url: "string",
6
7
  phone_number: "string",
@@ -19,6 +20,25 @@ const FIELD_TYPE_MAP = {
19
20
  lookup: "unknown",
20
21
  autonumber: "number",
21
22
  source: "string",
23
+ formula: "unknown",
24
+ created_at: "string",
25
+ updated_at: "string",
26
+ };
27
+ const MAX_SELECT_OPTIONS = 100;
28
+ const NUMBER_FORMAT_EXAMPLES = {
29
+ local: "1,000,000.50",
30
+ comma_period: "1,000,000.50",
31
+ period_comma: "1.000.000,50",
32
+ space_comma: "1 000 000,50",
33
+ space_period: "1 000 000.50",
34
+ no_separator: "1000000.50",
35
+ };
36
+ const DURATION_FORMAT_EXAMPLES = {
37
+ "h:mm": "1:23",
38
+ "h:mm:ss": "1:23:03",
39
+ "h:mm:ss.s": "1:23:03.0",
40
+ "h:mm:ss.ss": "1:23:03.00",
41
+ "h:mm:ss.sss": "1:23:03.000",
22
42
  };
23
43
  export function toPascalCase(name) {
24
44
  return name
@@ -29,33 +49,157 @@ export function toCamelCase(name) {
29
49
  const pascal = toPascalCase(name);
30
50
  return pascal.charAt(0).toLowerCase() + pascal.slice(1);
31
51
  }
32
- function tsTypeForField(field) {
33
- if (FIELD_TYPE_MAP[field.type])
34
- return FIELD_TYPE_MAP[field.type];
35
- const lowerName = field.name?.toLowerCase() ?? "";
36
- if (lowerName === "createdat" ||
37
- lowerName === "created_at" ||
38
- lowerName === "updatedat" ||
39
- lowerName === "updated_at") {
40
- return "string";
52
+ function tsTypeForSchemaField(def) {
53
+ if (def.type === "single_select" || def.type === "multiple_select") {
54
+ const options = def.template
55
+ ?.options;
56
+ if (options && options.length > 0) {
57
+ const literals = options
58
+ .slice(0, MAX_SELECT_OPTIONS)
59
+ .map((o) => `"${o.label.replace(/"/g, '\\"')}"`)
60
+ .join(" | ");
61
+ const union = `${literals} | string`;
62
+ if (def.type === "multiple_select")
63
+ return `(${union})[]`;
64
+ return union;
65
+ }
41
66
  }
67
+ if (FIELD_TYPE_MAP[def.type])
68
+ return FIELD_TYPE_MAP[def.type];
42
69
  return "string";
43
70
  }
44
- export function generateSchema(base) {
45
- const schema = { tables: {} };
46
- for (const table of base.tables ?? []) {
47
- const tableName = toCamelCase(table.name);
48
- const fields = {};
49
- for (const field of table.fields ?? []) {
50
- fields[toCamelCase(field.name)] = { id: field.id };
71
+ function fieldJsdoc(schemaField, table) {
72
+ const def = schemaField.definition;
73
+ const parts = [];
74
+ if (table.primaryFieldId === schemaField.id)
75
+ parts.push("Primary field");
76
+ if (def.type === "created_at" ||
77
+ def.type === "updated_at" ||
78
+ def.type === "lookup" ||
79
+ def.type === "formula" ||
80
+ def.type === "autonumber") {
81
+ parts.push("Read-only");
82
+ }
83
+ if (def.type === "linked_record") {
84
+ const tpl = def.template;
85
+ if (tpl.tableId)
86
+ parts.push(`Links to table ${tpl.tableId}`);
87
+ if (tpl.allowMultiple === false)
88
+ parts.push("Single record only");
89
+ }
90
+ parts.push(`"${def.name}"`);
91
+ if (def.type === "date") {
92
+ const tpl = def.template;
93
+ if (tpl.dateFormat)
94
+ parts.push(`Date-only field (YYYY-MM-DD string), display as "${tpl.dateFormat}" format`);
95
+ }
96
+ if (def.type === "datetime") {
97
+ const tpl = def.template;
98
+ const timeParts = [
99
+ "Date+time field (ISO 8601 timestamp)",
100
+ ];
101
+ if (tpl.dateFormat)
102
+ timeParts.push(`date: ${tpl.dateFormat}`);
103
+ if (tpl.timeFormat)
104
+ timeParts.push(`time: ${tpl.timeFormat}`);
105
+ if (tpl.timezone)
106
+ timeParts.push(`tz: ${tpl.timezone}`);
107
+ if (tpl.displayTimeZone)
108
+ timeParts.push("timezone displayed to user");
109
+ parts.push(timeParts.join(", "));
110
+ }
111
+ if (def.type === "currency") {
112
+ const tpl = def.template;
113
+ if (tpl.currencySymbol) {
114
+ const formatHint = NUMBER_FORMAT_EXAMPLES[tpl.numberFormat ?? "local"] ?? "";
115
+ parts.push(`Value is a raw number; display as currency with symbol "${tpl.currencySymbol}", ${tpl.decimalPlaces ?? 2} decimal places, format: ${tpl.numberFormat ?? "local"} (e.g. ${formatHint})`);
116
+ }
117
+ }
118
+ if (def.type === "number") {
119
+ const tpl = def.template;
120
+ const formatHint = NUMBER_FORMAT_EXAMPLES[tpl.numberFormat ?? "local"] ?? "";
121
+ parts.push(`${tpl.decimalPlaces ?? 0} decimal places, format: ${tpl.numberFormat ?? "local"} (e.g. ${formatHint})`);
122
+ }
123
+ if (def.type === "percent") {
124
+ const tpl = def.template;
125
+ if (tpl.decimalPlaces !== undefined)
126
+ parts.push(`${tpl.decimalPlaces} decimal places`);
127
+ }
128
+ if (def.type === "duration") {
129
+ const tpl = def.template;
130
+ if (tpl.format) {
131
+ const example = DURATION_FORMAT_EXAMPLES[tpl.format] ?? "";
132
+ parts.push(`Value is in seconds; display in "${tpl.format}" format (${example})`);
133
+ }
134
+ }
135
+ if (def.type === "rating") {
136
+ const tpl = def.template;
137
+ if (tpl.maxRating)
138
+ parts.push(`Max rating: ${tpl.maxRating}`);
139
+ }
140
+ if (def.type === "created_at") {
141
+ parts.push("Auto-set when record was created (ISO 8601)");
142
+ }
143
+ if (def.type === "updated_at") {
144
+ parts.push("Auto-set when record was last modified (ISO 8601)");
145
+ }
146
+ if ((def.type === "single_select" || def.type === "multiple_select") &&
147
+ def.template?.options) {
148
+ const count = (def.template.options ?? []).length;
149
+ if (count > MAX_SELECT_OPTIONS) {
150
+ parts.push(`${count - MAX_SELECT_OPTIONS} more options omitted`);
151
+ }
152
+ }
153
+ if (parts.length <= 1)
154
+ return undefined;
155
+ return parts.join(". ");
156
+ }
157
+ /**
158
+ * Build a ZiteSchema from a Database API response.
159
+ *
160
+ * If `existingSchema` is provided, locked SDK names are preserved for
161
+ * tables/fields that already exist (matched by ID). New tables/fields
162
+ * get fresh camelCase SDK names. This is how `zitejs sync` preserves
163
+ * name stability across schema changes.
164
+ */
165
+ export function generateSchema(database, existingSchema) {
166
+ const existingTableById = new Map();
167
+ for (const t of existingSchema?.tables ?? []) {
168
+ existingTableById.set(t.id, t);
169
+ }
170
+ const tables = [];
171
+ for (const table of database.tables) {
172
+ const existingTable = existingTableById.get(table.id);
173
+ const existingFieldById = new Map();
174
+ for (const f of existingTable?.fields ?? []) {
175
+ existingFieldById.set(f.id, f);
51
176
  }
52
- schema.tables[tableName] = { id: table.id, fields };
177
+ const fields = [];
178
+ for (const field of table.fields) {
179
+ const existing = existingFieldById.get(field.id);
180
+ const { id: _id, order: _order, ...definition } = field;
181
+ fields.push({
182
+ id: field.id,
183
+ sdkName: existing?.sdkName ?? toCamelCase(field.name),
184
+ definition,
185
+ });
186
+ }
187
+ tables.push({
188
+ id: table.id,
189
+ sdkName: existingTable?.sdkName ?? toCamelCase(table.name),
190
+ primaryFieldId: table.primaryFieldId,
191
+ fields,
192
+ });
53
193
  }
54
- return schema;
194
+ return { tables };
55
195
  }
56
- export function generateDbTs(base, schema) {
196
+ /**
197
+ * Generate .zite/db.ts purely from ZiteSchema.
198
+ * Pure function — no external data needed beyond what's in the schema file.
199
+ */
200
+ export function generateDbTs(schema) {
57
201
  const lines = [];
58
- lines.push("// Auto-generated by zitejs sync. Do not edit manually.");
202
+ lines.push("// Auto-generated by zitejs generate. Do not edit manually.");
59
203
  lines.push("//");
60
204
  lines.push("// Usage in endpoint files (src/api/*.ts):");
61
205
  lines.push('// import { zite } from "zitejs/db";');
@@ -87,26 +231,28 @@ export function generateDbTs(base, schema) {
87
231
  lines.push('// - Load the "zite:sql" skill for full SQL reference (formulas, lookups, etc.)');
88
232
  lines.push("import { createTableClient, createSqlClient } from 'zitejs/runtime';");
89
233
  lines.push("");
90
- for (const table of base.tables ?? []) {
91
- const className = toPascalCase(table.name);
234
+ for (const table of schema.tables) {
235
+ const className = toPascalCase(table.sdkName);
92
236
  const recordType = `${className}RecordType`;
93
237
  lines.push(`export type ${recordType} = {`);
94
238
  lines.push(" id: string;");
95
- for (const field of table.fields ?? []) {
96
- const fieldName = toCamelCase(field.name);
97
- if (fieldName === "id")
239
+ for (const field of table.fields) {
240
+ if (field.sdkName === "id")
98
241
  continue;
99
- const tsType = tsTypeForField(field);
100
- lines.push(` ${fieldName}: ${tsType};`);
242
+ const tsType = tsTypeForSchemaField(field.definition);
243
+ const jsdoc = fieldJsdoc(field, table);
244
+ if (jsdoc) {
245
+ lines.push(` /** ${jsdoc} */`);
246
+ }
247
+ lines.push(` ${field.sdkName}: ${tsType};`);
101
248
  }
102
249
  lines.push("};");
103
250
  lines.push("");
104
251
  }
105
252
  lines.push("export const zite = {");
106
- for (const table of base.tables ?? []) {
107
- const className = toPascalCase(table.name);
108
- const propName = toCamelCase(table.name);
109
- lines.push(` ${propName}: createTableClient<${className}RecordType>('${className}'),`);
253
+ for (const table of schema.tables) {
254
+ const className = toPascalCase(table.sdkName);
255
+ lines.push(` ${table.sdkName}: createTableClient<${className}RecordType>('${className}'),`);
110
256
  }
111
257
  lines.push(` sql: createSqlClient(),`);
112
258
  lines.push("};");
@@ -134,14 +280,12 @@ export function generateApiTs(endpointFiles) {
134
280
  lines.push(`export const ${name} = createCaller(${name}Endpoint, '${name}');`);
135
281
  }
136
282
  lines.push("");
137
- // Inferred input/output types per endpoint (e.g. GetDashboardInputType, GetDashboardOutputType)
138
283
  for (const name of endpointNames) {
139
284
  const pascal = toPascalCase(name);
140
285
  lines.push(`export type ${pascal}InputType = Parameters<typeof ${name}Endpoint.execute>[0]['input'];`);
141
286
  lines.push(`export type ${pascal}OutputType = Awaited<ReturnType<typeof ${name}Endpoint.execute>>;`);
142
287
  }
143
288
  lines.push("");
144
- // Also export as a single api object
145
289
  lines.push("export const api = {");
146
290
  for (const name of endpointNames) {
147
291
  lines.push(` ${name},`);
@@ -150,6 +294,11 @@ export function generateApiTs(endpointFiles) {
150
294
  lines.push("");
151
295
  return lines.join("\n");
152
296
  }
297
+ function tsTypeForField(field) {
298
+ if (FIELD_TYPE_MAP[field.type])
299
+ return FIELD_TYPE_MAP[field.type];
300
+ return "string";
301
+ }
153
302
  export function generateUserTs(usersTableFields) {
154
303
  const lines = [
155
304
  "// Auto-generated by zitejs sync. Do not edit manually.",
@@ -198,6 +347,86 @@ export function generateAuthWrapperTs() {
198
347
  "",
199
348
  ].join("\n");
200
349
  }
350
+ const AIRTABLE_FIELD_TYPE_MAP = {
351
+ singleLineText: "string",
352
+ multilineText: "string",
353
+ richText: "string",
354
+ email: "string",
355
+ url: "string",
356
+ phoneNumber: "string",
357
+ number: "number",
358
+ currency: "number",
359
+ percent: "number",
360
+ rating: "number",
361
+ duration: "number",
362
+ singleSelect: "string",
363
+ multipleSelects: "string[]",
364
+ checkbox: "boolean",
365
+ date: "string",
366
+ dateTime: "string",
367
+ attachment: "Array<{ url: string; filename?: string }>",
368
+ multipleRecordLinks: "string | string[]",
369
+ formula: "unknown",
370
+ rollup: "unknown",
371
+ lookup: "unknown",
372
+ count: "number",
373
+ autoNumber: "number",
374
+ barcode: "string",
375
+ button: "unknown",
376
+ createdTime: "string",
377
+ lastModifiedTime: "string",
378
+ createdBy: "unknown",
379
+ lastModifiedBy: "unknown",
380
+ externalSyncSource: "unknown",
381
+ aiText: "string",
382
+ };
383
+ function airtableTsType(field) {
384
+ if ((field.type === "singleSelect" || field.type === "multipleSelects") &&
385
+ field.options &&
386
+ field.options.length > 0) {
387
+ const literals = field.options
388
+ .slice(0, MAX_SELECT_OPTIONS)
389
+ .map((o) => `"${o.replace(/"/g, '\\"')}"`)
390
+ .join(" | ");
391
+ const union = `${literals} | string`;
392
+ if (field.type === "multipleSelects")
393
+ return `(${union})[]`;
394
+ return union;
395
+ }
396
+ return AIRTABLE_FIELD_TYPE_MAP[field.type] ?? "unknown";
397
+ }
398
+ export function generateAirtableTs(lock) {
399
+ if (lock.tables.length === 0)
400
+ return null;
401
+ const lines = [
402
+ "// Auto-generated by zitejs generate from zite.lock. Do not edit manually.",
403
+ "// Airtable SDK — uses createAirtableClient from zitejs/runtime.",
404
+ "// The airtable package is externalized (not bundled per-endpoint).",
405
+ "",
406
+ "import { createAirtableClient } from 'zitejs/runtime';",
407
+ "",
408
+ ];
409
+ for (const table of lock.tables) {
410
+ const recordType = `${table.sdkName}RecordType`;
411
+ lines.push(`export type ${recordType} = {`);
412
+ lines.push(" id: string;");
413
+ for (const field of table.fields) {
414
+ if (field.sdkName === "id")
415
+ continue;
416
+ const tsType = airtableTsType(field);
417
+ lines.push(` ${field.sdkName}: ${tsType};`);
418
+ }
419
+ lines.push("};");
420
+ lines.push("");
421
+ lines.push(`export const ${table.sdkName} = createAirtableClient<${recordType}>(`);
422
+ lines.push(` '${lock.integrationId}',`);
423
+ lines.push(` '${table.sdkName}',`);
424
+ lines.push(` { tableId: '${table.id}' },`);
425
+ lines.push(`);`);
426
+ lines.push("");
427
+ }
428
+ return lines.join("\n");
429
+ }
201
430
  export function generateBackendWrapperTs() {
202
431
  return [
203
432
  "// Auto-generated type-narrowing wrapper. Do not edit manually.",