zitejs 0.9.41 → 0.9.42

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.
@@ -12,9 +12,11 @@ const DB_ENVIRONMENTS = {
12
12
  local: "http://localhost:2507/api/v1",
13
13
  };
14
14
  const env = process.env.ZITE_ENV ?? "production";
15
- const BASE_URL = process.env.ZITE_DB_URL ?? DB_ENVIRONMENTS[env] ?? DB_ENVIRONMENTS.production;
15
+ const BASE_URL = process.env.ZITE_DB_URL ??
16
+ DB_ENVIRONMENTS[env] ??
17
+ DB_ENVIRONMENTS.production;
16
18
  const TOKEN = process.env.ZITE_DB_TOKEN ?? "";
17
- async function fetchBaseMetadata(baseId) {
19
+ async function fetchDatabase(baseId) {
18
20
  const url = `${BASE_URL}/bases/${encodeURIComponent(baseId)}`;
19
21
  const res = await fetch(url, {
20
22
  headers: { Authorization: `Bearer ${TOKEN}` },
@@ -35,6 +37,15 @@ function resolveBaseId() {
35
37
  return undefined;
36
38
  }
37
39
  }
40
+ function readExistingSchema() {
41
+ try {
42
+ if ((0, fs_1.existsSync)("zite.schema.json")) {
43
+ return JSON.parse((0, fs_1.readFileSync)("zite.schema.json", "utf-8"));
44
+ }
45
+ }
46
+ catch { }
47
+ return undefined;
48
+ }
38
49
  async function runSync() {
39
50
  if (!TOKEN) {
40
51
  throw new Error("ZITE_DB_TOKEN is required.");
@@ -44,12 +55,13 @@ async function runSync() {
44
55
  throw new Error("Could not determine base ID.");
45
56
  }
46
57
  console.log(`Syncing database ${baseId}...`);
47
- const base = await fetchBaseMetadata(baseId);
48
- console.log(`Found ${base.tables?.length ?? 0} tables`);
49
- const schema = (0, lib_js_1.generateSchema)(base);
58
+ const database = await fetchDatabase(baseId);
59
+ console.log(`Found ${database.tables?.length ?? 0} tables`);
60
+ const existingSchema = readExistingSchema();
61
+ const schema = (0, lib_js_1.generateSchema)(database, existingSchema);
50
62
  (0, fs_1.writeFileSync)("zite.schema.json", JSON.stringify(schema, null, 2));
51
63
  console.log("Wrote zite.schema.json");
52
- const dbTs = (0, lib_js_1.generateDbTs)(base, schema);
64
+ const dbTs = (0, lib_js_1.generateDbTs)(schema);
53
65
  const dotZite = ".zite";
54
66
  (0, fs_1.mkdirSync)(dotZite, { recursive: true });
55
67
  (0, fs_1.writeFileSync)((0, path_1.join)(dotZite, "db.ts"), dbTs);
@@ -11,6 +11,7 @@ exports.generateBackendWrapperTs = generateBackendWrapperTs;
11
11
  const FIELD_TYPE_MAP = {
12
12
  single_line_text: "string",
13
13
  long_text: "string",
14
+ rich_text: "string",
14
15
  email: "string",
15
16
  url: "string",
16
17
  phone_number: "string",
@@ -29,6 +30,25 @@ const FIELD_TYPE_MAP = {
29
30
  lookup: "unknown",
30
31
  autonumber: "number",
31
32
  source: "string",
33
+ formula: "unknown",
34
+ created_at: "string",
35
+ updated_at: "string",
36
+ };
37
+ const MAX_SELECT_OPTIONS = 100;
38
+ const NUMBER_FORMAT_EXAMPLES = {
39
+ local: "1,000,000.50",
40
+ comma_period: "1,000,000.50",
41
+ period_comma: "1.000.000,50",
42
+ space_comma: "1 000 000,50",
43
+ space_period: "1 000 000.50",
44
+ no_separator: "1000000.50",
45
+ };
46
+ const DURATION_FORMAT_EXAMPLES = {
47
+ "h:mm": "1:23",
48
+ "h:mm:ss": "1:23:03",
49
+ "h:mm:ss.s": "1:23:03.0",
50
+ "h:mm:ss.ss": "1:23:03.00",
51
+ "h:mm:ss.sss": "1:23:03.000",
32
52
  };
33
53
  function toPascalCase(name) {
34
54
  return name
@@ -39,33 +59,157 @@ function toCamelCase(name) {
39
59
  const pascal = toPascalCase(name);
40
60
  return pascal.charAt(0).toLowerCase() + pascal.slice(1);
41
61
  }
42
- function tsTypeForField(field) {
43
- if (FIELD_TYPE_MAP[field.type])
44
- return FIELD_TYPE_MAP[field.type];
45
- const lowerName = field.name?.toLowerCase() ?? "";
46
- if (lowerName === "createdat" ||
47
- lowerName === "created_at" ||
48
- lowerName === "updatedat" ||
49
- lowerName === "updated_at") {
50
- return "string";
62
+ function tsTypeForSchemaField(def) {
63
+ if (def.type === "single_select" || def.type === "multiple_select") {
64
+ const options = def.template
65
+ ?.options;
66
+ if (options && options.length > 0) {
67
+ const literals = options
68
+ .slice(0, MAX_SELECT_OPTIONS)
69
+ .map((o) => `"${o.label.replace(/"/g, '\\"')}"`)
70
+ .join(" | ");
71
+ const union = `${literals} | string`;
72
+ if (def.type === "multiple_select")
73
+ return `(${union})[]`;
74
+ return union;
75
+ }
51
76
  }
77
+ if (FIELD_TYPE_MAP[def.type])
78
+ return FIELD_TYPE_MAP[def.type];
52
79
  return "string";
53
80
  }
54
- function generateSchema(base) {
55
- const schema = { tables: {} };
56
- for (const table of base.tables ?? []) {
57
- const tableName = toCamelCase(table.name);
58
- const fields = {};
59
- for (const field of table.fields ?? []) {
60
- fields[toCamelCase(field.name)] = { id: field.id };
81
+ function fieldJsdoc(schemaField, table) {
82
+ const def = schemaField.definition;
83
+ const parts = [];
84
+ if (table.primaryFieldId === schemaField.id)
85
+ parts.push("Primary field");
86
+ if (def.type === "created_at" ||
87
+ def.type === "updated_at" ||
88
+ def.type === "lookup" ||
89
+ def.type === "formula" ||
90
+ def.type === "autonumber") {
91
+ parts.push("Read-only");
92
+ }
93
+ if (def.type === "linked_record") {
94
+ const tpl = def.template;
95
+ if (tpl.tableId)
96
+ parts.push(`Links to table ${tpl.tableId}`);
97
+ if (tpl.allowMultiple === false)
98
+ parts.push("Single record only");
99
+ }
100
+ parts.push(`"${def.name}"`);
101
+ if (def.type === "date") {
102
+ const tpl = def.template;
103
+ if (tpl.dateFormat)
104
+ parts.push(`Date-only field (YYYY-MM-DD string), display as "${tpl.dateFormat}" format`);
105
+ }
106
+ if (def.type === "datetime") {
107
+ const tpl = def.template;
108
+ const timeParts = [
109
+ "Date+time field (ISO 8601 timestamp)",
110
+ ];
111
+ if (tpl.dateFormat)
112
+ timeParts.push(`date: ${tpl.dateFormat}`);
113
+ if (tpl.timeFormat)
114
+ timeParts.push(`time: ${tpl.timeFormat}`);
115
+ if (tpl.timezone)
116
+ timeParts.push(`tz: ${tpl.timezone}`);
117
+ if (tpl.displayTimeZone)
118
+ timeParts.push("timezone displayed to user");
119
+ parts.push(timeParts.join(", "));
120
+ }
121
+ if (def.type === "currency") {
122
+ const tpl = def.template;
123
+ if (tpl.currencySymbol) {
124
+ const formatHint = NUMBER_FORMAT_EXAMPLES[tpl.numberFormat ?? "local"] ?? "";
125
+ 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})`);
126
+ }
127
+ }
128
+ if (def.type === "number") {
129
+ const tpl = def.template;
130
+ const formatHint = NUMBER_FORMAT_EXAMPLES[tpl.numberFormat ?? "local"] ?? "";
131
+ parts.push(`${tpl.decimalPlaces ?? 0} decimal places, format: ${tpl.numberFormat ?? "local"} (e.g. ${formatHint})`);
132
+ }
133
+ if (def.type === "percent") {
134
+ const tpl = def.template;
135
+ if (tpl.decimalPlaces !== undefined)
136
+ parts.push(`${tpl.decimalPlaces} decimal places`);
137
+ }
138
+ if (def.type === "duration") {
139
+ const tpl = def.template;
140
+ if (tpl.format) {
141
+ const example = DURATION_FORMAT_EXAMPLES[tpl.format] ?? "";
142
+ parts.push(`Value is in seconds; display in "${tpl.format}" format (${example})`);
143
+ }
144
+ }
145
+ if (def.type === "rating") {
146
+ const tpl = def.template;
147
+ if (tpl.maxRating)
148
+ parts.push(`Max rating: ${tpl.maxRating}`);
149
+ }
150
+ if (def.type === "created_at") {
151
+ parts.push("Auto-set when record was created (ISO 8601)");
152
+ }
153
+ if (def.type === "updated_at") {
154
+ parts.push("Auto-set when record was last modified (ISO 8601)");
155
+ }
156
+ if ((def.type === "single_select" || def.type === "multiple_select") &&
157
+ def.template?.options) {
158
+ const count = (def.template.options ?? []).length;
159
+ if (count > MAX_SELECT_OPTIONS) {
160
+ parts.push(`${count - MAX_SELECT_OPTIONS} more options omitted`);
161
+ }
162
+ }
163
+ if (parts.length <= 1)
164
+ return undefined;
165
+ return parts.join(". ");
166
+ }
167
+ /**
168
+ * Build a ZiteSchema from a Database API response.
169
+ *
170
+ * If `existingSchema` is provided, locked SDK names are preserved for
171
+ * tables/fields that already exist (matched by ID). New tables/fields
172
+ * get fresh camelCase SDK names. This is how `zitejs sync` preserves
173
+ * name stability across schema changes.
174
+ */
175
+ function generateSchema(database, existingSchema) {
176
+ const existingTableById = new Map();
177
+ for (const t of existingSchema?.tables ?? []) {
178
+ existingTableById.set(t.id, t);
179
+ }
180
+ const tables = [];
181
+ for (const table of database.tables) {
182
+ const existingTable = existingTableById.get(table.id);
183
+ const existingFieldById = new Map();
184
+ for (const f of existingTable?.fields ?? []) {
185
+ existingFieldById.set(f.id, f);
186
+ }
187
+ const fields = [];
188
+ for (const field of table.fields) {
189
+ const existing = existingFieldById.get(field.id);
190
+ const { id: _id, order: _order, ...definition } = field;
191
+ fields.push({
192
+ id: field.id,
193
+ sdkName: existing?.sdkName ?? toCamelCase(field.name),
194
+ definition,
195
+ });
61
196
  }
62
- schema.tables[tableName] = { id: table.id, fields };
197
+ tables.push({
198
+ id: table.id,
199
+ sdkName: existingTable?.sdkName ?? toCamelCase(table.name),
200
+ primaryFieldId: table.primaryFieldId,
201
+ fields,
202
+ });
63
203
  }
64
- return schema;
204
+ return { tables };
65
205
  }
66
- function generateDbTs(base, schema) {
206
+ /**
207
+ * Generate .zite/db.ts purely from ZiteSchema.
208
+ * Pure function — no external data needed beyond what's in the schema file.
209
+ */
210
+ function generateDbTs(schema) {
67
211
  const lines = [];
68
- lines.push("// Auto-generated by zitejs sync. Do not edit manually.");
212
+ lines.push("// Auto-generated by zitejs generate. Do not edit manually.");
69
213
  lines.push("//");
70
214
  lines.push("// Usage in endpoint files (src/api/*.ts):");
71
215
  lines.push('// import { zite } from "zitejs/db";');
@@ -97,26 +241,28 @@ function generateDbTs(base, schema) {
97
241
  lines.push('// - Load the "zite:sql" skill for full SQL reference (formulas, lookups, etc.)');
98
242
  lines.push("import { createTableClient, createSqlClient } from 'zitejs/runtime';");
99
243
  lines.push("");
100
- for (const table of base.tables ?? []) {
101
- const className = toPascalCase(table.name);
244
+ for (const table of schema.tables) {
245
+ const className = toPascalCase(table.sdkName);
102
246
  const recordType = `${className}RecordType`;
103
247
  lines.push(`export type ${recordType} = {`);
104
248
  lines.push(" id: string;");
105
- for (const field of table.fields ?? []) {
106
- const fieldName = toCamelCase(field.name);
107
- if (fieldName === "id")
249
+ for (const field of table.fields) {
250
+ if (field.sdkName === "id")
108
251
  continue;
109
- const tsType = tsTypeForField(field);
110
- lines.push(` ${fieldName}: ${tsType};`);
252
+ const tsType = tsTypeForSchemaField(field.definition);
253
+ const jsdoc = fieldJsdoc(field, table);
254
+ if (jsdoc) {
255
+ lines.push(` /** ${jsdoc} */`);
256
+ }
257
+ lines.push(` ${field.sdkName}: ${tsType};`);
111
258
  }
112
259
  lines.push("};");
113
260
  lines.push("");
114
261
  }
115
262
  lines.push("export const zite = {");
116
- for (const table of base.tables ?? []) {
117
- const className = toPascalCase(table.name);
118
- const propName = toCamelCase(table.name);
119
- lines.push(` ${propName}: createTableClient<${className}RecordType>('${className}'),`);
263
+ for (const table of schema.tables) {
264
+ const className = toPascalCase(table.sdkName);
265
+ lines.push(` ${table.sdkName}: createTableClient<${className}RecordType>('${className}'),`);
120
266
  }
121
267
  lines.push(` sql: createSqlClient(),`);
122
268
  lines.push("};");
@@ -144,14 +290,12 @@ function generateApiTs(endpointFiles) {
144
290
  lines.push(`export const ${name} = createCaller(${name}Endpoint, '${name}');`);
145
291
  }
146
292
  lines.push("");
147
- // Inferred input/output types per endpoint (e.g. GetDashboardInputType, GetDashboardOutputType)
148
293
  for (const name of endpointNames) {
149
294
  const pascal = toPascalCase(name);
150
295
  lines.push(`export type ${pascal}InputType = Parameters<typeof ${name}Endpoint.execute>[0]['input'];`);
151
296
  lines.push(`export type ${pascal}OutputType = Awaited<ReturnType<typeof ${name}Endpoint.execute>>;`);
152
297
  }
153
298
  lines.push("");
154
- // Also export as a single api object
155
299
  lines.push("export const api = {");
156
300
  for (const name of endpointNames) {
157
301
  lines.push(` ${name},`);
@@ -160,6 +304,11 @@ function generateApiTs(endpointFiles) {
160
304
  lines.push("");
161
305
  return lines.join("\n");
162
306
  }
307
+ function tsTypeForField(field) {
308
+ if (FIELD_TYPE_MAP[field.type])
309
+ return FIELD_TYPE_MAP[field.type];
310
+ return "string";
311
+ }
163
312
  function generateUserTs(usersTableFields) {
164
313
  const lines = [
165
314
  "// Auto-generated by zitejs sync. Do not edit manually.",
@@ -249,7 +249,7 @@ export declare class Zite {
249
249
  linkedRecordFieldId: string;
250
250
  lookupFieldId: string;
251
251
  formulaExpression?: string | undefined;
252
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
252
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
253
253
  };
254
254
  } | {
255
255
  name: string;
@@ -260,9 +260,9 @@ export declare class Zite {
260
260
  type: "formula";
261
261
  template: {
262
262
  expression: string;
263
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
263
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
264
264
  formatting?: {
265
- numberDisplayType?: "number" | "currency" | "percent" | "duration" | undefined;
265
+ numberDisplayType?: "number" | "currency" | "duration" | "percent" | undefined;
266
266
  currencyCode?: string | undefined;
267
267
  decimalPlaces?: number | undefined;
268
268
  numberFormat?: "local" | "comma_period" | "period_comma" | "space_comma" | "space_period" | undefined;
@@ -428,7 +428,7 @@ export declare class Zite {
428
428
  linkedRecordFieldId: string;
429
429
  lookupFieldId: string;
430
430
  formulaExpression?: string | undefined;
431
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
431
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
432
432
  };
433
433
  } | {
434
434
  name: string;
@@ -439,9 +439,9 @@ export declare class Zite {
439
439
  type: "formula";
440
440
  template: {
441
441
  expression: string;
442
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
442
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
443
443
  formatting?: {
444
- numberDisplayType?: "number" | "currency" | "percent" | "duration" | undefined;
444
+ numberDisplayType?: "number" | "currency" | "duration" | "percent" | undefined;
445
445
  currencyCode?: string | undefined;
446
446
  decimalPlaces?: number | undefined;
447
447
  numberFormat?: "local" | "comma_period" | "period_comma" | "space_comma" | "space_period" | undefined;
@@ -617,7 +617,7 @@ export declare class Zite {
617
617
  linkedRecordFieldId: string;
618
618
  lookupFieldId: string;
619
619
  formulaExpression?: string | undefined;
620
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
620
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
621
621
  };
622
622
  } | {
623
623
  name: string;
@@ -628,9 +628,9 @@ export declare class Zite {
628
628
  type: "formula";
629
629
  template: {
630
630
  expression: string;
631
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
631
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
632
632
  formatting?: {
633
- numberDisplayType?: "number" | "currency" | "percent" | "duration" | undefined;
633
+ numberDisplayType?: "number" | "currency" | "duration" | "percent" | undefined;
634
634
  currencyCode?: string | undefined;
635
635
  decimalPlaces?: number | undefined;
636
636
  numberFormat?: "local" | "comma_period" | "period_comma" | "space_comma" | "space_period" | undefined;
@@ -809,7 +809,7 @@ export declare class Zite {
809
809
  linkedRecordFieldId: string;
810
810
  lookupFieldId: string;
811
811
  formulaExpression?: string | undefined;
812
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
812
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
813
813
  };
814
814
  } | {
815
815
  name: string;
@@ -820,9 +820,9 @@ export declare class Zite {
820
820
  type: "formula";
821
821
  template: {
822
822
  expression: string;
823
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
823
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
824
824
  formatting?: {
825
- numberDisplayType?: "number" | "currency" | "percent" | "duration" | undefined;
825
+ numberDisplayType?: "number" | "currency" | "duration" | "percent" | undefined;
826
826
  currencyCode?: string | undefined;
827
827
  decimalPlaces?: number | undefined;
828
828
  numberFormat?: "local" | "comma_period" | "period_comma" | "space_comma" | "space_period" | undefined;
@@ -1009,7 +1009,7 @@ export declare class Zite {
1009
1009
  linkedRecordFieldId: string;
1010
1010
  lookupFieldId: string;
1011
1011
  formulaExpression?: string | undefined;
1012
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
1012
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
1013
1013
  };
1014
1014
  } | {
1015
1015
  name: string;
@@ -1020,9 +1020,9 @@ export declare class Zite {
1020
1020
  type: "formula";
1021
1021
  template: {
1022
1022
  expression: string;
1023
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
1023
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
1024
1024
  formatting?: {
1025
- numberDisplayType?: "number" | "currency" | "percent" | "duration" | undefined;
1025
+ numberDisplayType?: "number" | "currency" | "duration" | "percent" | undefined;
1026
1026
  currencyCode?: string | undefined;
1027
1027
  decimalPlaces?: number | undefined;
1028
1028
  numberFormat?: "local" | "comma_period" | "period_comma" | "space_comma" | "space_period" | undefined;
@@ -1210,7 +1210,7 @@ export declare class Zite {
1210
1210
  linkedRecordFieldId: string;
1211
1211
  lookupFieldId: string;
1212
1212
  formulaExpression?: string | undefined;
1213
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
1213
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
1214
1214
  };
1215
1215
  } | {
1216
1216
  name: string;
@@ -1221,9 +1221,9 @@ export declare class Zite {
1221
1221
  type: "formula";
1222
1222
  template: {
1223
1223
  expression: string;
1224
- resultType?: "number" | "boolean" | "date" | "text" | "array" | undefined;
1224
+ resultType?: "number" | "boolean" | "date" | "array" | "text" | undefined;
1225
1225
  formatting?: {
1226
- numberDisplayType?: "number" | "currency" | "percent" | "duration" | undefined;
1226
+ numberDisplayType?: "number" | "currency" | "duration" | "percent" | undefined;
1227
1227
  currencyCode?: string | undefined;
1228
1228
  decimalPlaces?: number | undefined;
1229
1229
  numberFormat?: "local" | "comma_period" | "period_comma" | "space_comma" | "space_period" | undefined;
@@ -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,26 +1,35 @@
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;
@@ -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);
176
+ }
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
+ });
51
186
  }
52
- schema.tables[tableName] = { id: table.id, fields };
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.",
@@ -11,29 +11,29 @@ export declare const fieldTypeEnum: readonly ["currency", "single_line_text", "l
11
11
  export type FieldType = (typeof fieldTypeEnum)[number];
12
12
  export declare const fieldTypeSchema: z.ZodEnum<{
13
13
  number: "number";
14
+ currency: "currency";
14
15
  single_line_text: "single_line_text";
15
16
  long_text: "long_text";
16
- email: "email";
17
- url: "url";
18
- phone_number: "phone_number";
19
- currency: "currency";
20
- percent: "percent";
21
- rating: "rating";
22
- duration: "duration";
17
+ rich_text: "rich_text";
23
18
  single_select: "single_select";
24
19
  multiple_select: "multiple_select";
25
- checkbox: "checkbox";
26
20
  date: "date";
27
21
  datetime: "datetime";
22
+ checkbox: "checkbox";
28
23
  attachments: "attachments";
24
+ email: "email";
25
+ url: "url";
26
+ phone_number: "phone_number";
27
+ rating: "rating";
28
+ duration: "duration";
29
+ percent: "percent";
29
30
  linked_record: "linked_record";
30
31
  lookup: "lookup";
31
- autonumber: "autonumber";
32
32
  source: "source";
33
+ formula: "formula";
34
+ autonumber: "autonumber";
33
35
  created_at: "created_at";
34
36
  updated_at: "updated_at";
35
- rich_text: "rich_text";
36
- formula: "formula";
37
37
  }>;
38
38
  export declare const selectOptionSchema: z.ZodObject<{
39
39
  value: z.ZodString;
@@ -185,8 +185,8 @@ export declare const lookupTemplateSchema: z.ZodObject<{
185
185
  number: "number";
186
186
  boolean: "boolean";
187
187
  date: "date";
188
- text: "text";
189
188
  array: "array";
189
+ text: "text";
190
190
  }>>;
191
191
  }, z.core.$strip>;
192
192
  export declare const sourceTemplateSchema: z.ZodObject<{}, z.core.$strip>;
@@ -196,15 +196,15 @@ export declare const formulaTemplateSchema: z.ZodObject<{
196
196
  number: "number";
197
197
  boolean: "boolean";
198
198
  date: "date";
199
- text: "text";
200
199
  array: "array";
200
+ text: "text";
201
201
  }>>;
202
202
  formatting: z.ZodOptional<z.ZodObject<{
203
203
  numberDisplayType: z.ZodOptional<z.ZodEnum<{
204
204
  number: "number";
205
205
  currency: "currency";
206
- percent: "percent";
207
206
  duration: "duration";
207
+ percent: "percent";
208
208
  }>>;
209
209
  currencyCode: z.ZodOptional<z.ZodString>;
210
210
  decimalPlaces: z.ZodOptional<z.ZodNumber>;
@@ -444,8 +444,8 @@ export declare const publicFieldDefinitionSchema: z.ZodDiscriminatedUnion<[z.Zod
444
444
  number: "number";
445
445
  boolean: "boolean";
446
446
  date: "date";
447
- text: "text";
448
447
  array: "array";
448
+ text: "text";
449
449
  }>>;
450
450
  }, z.core.$strip>;
451
451
  }, z.core.$strip>, z.ZodObject<{
@@ -461,15 +461,15 @@ export declare const publicFieldDefinitionSchema: z.ZodDiscriminatedUnion<[z.Zod
461
461
  number: "number";
462
462
  boolean: "boolean";
463
463
  date: "date";
464
- text: "text";
465
464
  array: "array";
465
+ text: "text";
466
466
  }>>;
467
467
  formatting: z.ZodOptional<z.ZodObject<{
468
468
  numberDisplayType: z.ZodOptional<z.ZodEnum<{
469
469
  number: "number";
470
470
  currency: "currency";
471
- percent: "percent";
472
471
  duration: "duration";
472
+ percent: "percent";
473
473
  }>>;
474
474
  currencyCode: z.ZodOptional<z.ZodString>;
475
475
  decimalPlaces: z.ZodOptional<z.ZodNumber>;
@@ -198,8 +198,8 @@ export declare const fieldSchema: z.ZodIntersection<z.ZodObject<{
198
198
  number: "number";
199
199
  boolean: "boolean";
200
200
  date: "date";
201
- text: "text";
202
201
  array: "array";
202
+ text: "text";
203
203
  }>>;
204
204
  }, z.core.$strip>;
205
205
  }, z.core.$strip>, z.ZodObject<{
@@ -215,15 +215,15 @@ export declare const fieldSchema: z.ZodIntersection<z.ZodObject<{
215
215
  number: "number";
216
216
  boolean: "boolean";
217
217
  date: "date";
218
- text: "text";
219
218
  array: "array";
219
+ text: "text";
220
220
  }>>;
221
221
  formatting: z.ZodOptional<z.ZodObject<{
222
222
  numberDisplayType: z.ZodOptional<z.ZodEnum<{
223
223
  number: "number";
224
224
  currency: "currency";
225
- percent: "percent";
226
225
  duration: "duration";
226
+ percent: "percent";
227
227
  }>>;
228
228
  currencyCode: z.ZodOptional<z.ZodString>;
229
229
  decimalPlaces: z.ZodOptional<z.ZodNumber>;
@@ -489,8 +489,8 @@ export declare const tableSchema: z.ZodObject<{
489
489
  number: "number";
490
490
  boolean: "boolean";
491
491
  date: "date";
492
- text: "text";
493
492
  array: "array";
493
+ text: "text";
494
494
  }>>;
495
495
  }, z.core.$strip>;
496
496
  }, z.core.$strip>, z.ZodObject<{
@@ -506,15 +506,15 @@ export declare const tableSchema: z.ZodObject<{
506
506
  number: "number";
507
507
  boolean: "boolean";
508
508
  date: "date";
509
- text: "text";
510
509
  array: "array";
510
+ text: "text";
511
511
  }>>;
512
512
  formatting: z.ZodOptional<z.ZodObject<{
513
513
  numberDisplayType: z.ZodOptional<z.ZodEnum<{
514
514
  number: "number";
515
515
  currency: "currency";
516
- percent: "percent";
517
516
  duration: "duration";
517
+ percent: "percent";
518
518
  }>>;
519
519
  currencyCode: z.ZodOptional<z.ZodString>;
520
520
  decimalPlaces: z.ZodOptional<z.ZodNumber>;
@@ -775,8 +775,8 @@ export declare const publicTableDefinitionSchema: z.ZodObject<{
775
775
  number: "number";
776
776
  boolean: "boolean";
777
777
  date: "date";
778
- text: "text";
779
778
  array: "array";
779
+ text: "text";
780
780
  }>>;
781
781
  }, z.core.$strip>;
782
782
  }, z.core.$strip>, z.ZodObject<{
@@ -792,15 +792,15 @@ export declare const publicTableDefinitionSchema: z.ZodObject<{
792
792
  number: "number";
793
793
  boolean: "boolean";
794
794
  date: "date";
795
- text: "text";
796
795
  array: "array";
796
+ text: "text";
797
797
  }>>;
798
798
  formatting: z.ZodOptional<z.ZodObject<{
799
799
  numberDisplayType: z.ZodOptional<z.ZodEnum<{
800
800
  number: "number";
801
801
  currency: "currency";
802
- percent: "percent";
803
802
  duration: "duration";
803
+ percent: "percent";
804
804
  }>>;
805
805
  currencyCode: z.ZodOptional<z.ZodString>;
806
806
  decimalPlaces: z.ZodOptional<z.ZodNumber>;
@@ -1063,8 +1063,8 @@ export declare const databaseSchema: z.ZodObject<{
1063
1063
  number: "number";
1064
1064
  boolean: "boolean";
1065
1065
  date: "date";
1066
- text: "text";
1067
1066
  array: "array";
1067
+ text: "text";
1068
1068
  }>>;
1069
1069
  }, z.core.$strip>;
1070
1070
  }, z.core.$strip>, z.ZodObject<{
@@ -1080,15 +1080,15 @@ export declare const databaseSchema: z.ZodObject<{
1080
1080
  number: "number";
1081
1081
  boolean: "boolean";
1082
1082
  date: "date";
1083
- text: "text";
1084
1083
  array: "array";
1084
+ text: "text";
1085
1085
  }>>;
1086
1086
  formatting: z.ZodOptional<z.ZodObject<{
1087
1087
  numberDisplayType: z.ZodOptional<z.ZodEnum<{
1088
1088
  number: "number";
1089
1089
  currency: "currency";
1090
- percent: "percent";
1091
1090
  duration: "duration";
1091
+ percent: "percent";
1092
1092
  }>>;
1093
1093
  currencyCode: z.ZodOptional<z.ZodString>;
1094
1094
  decimalPlaces: z.ZodOptional<z.ZodNumber>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.41",
3
+ "version": "0.9.42",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",