zitejs 0.9.98 → 0.9.100

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,9 +15,9 @@ export type FilterCondition<V> = {
15
15
  contains?: any;
16
16
  /** Not equal, or — against `null` — "is set". */
17
17
  not?: V | null;
18
- /** Empty array means "no filter", not "match nothing". */
18
+ /** An empty array matches NOTHING — the SDK's filters convert to the nested format, where `in: []` is "is one of nothing". */
19
19
  in?: V extends Array<infer E> ? E[] : V[];
20
- /** Empty array means "no filter", not "match everything". */
20
+ /** An empty array applies no filter at all — "is not one of nothing" is always true. */
21
21
  notIn?: V extends Array<infer E> ? E[] : V[];
22
22
  lt?: V | number | Date;
23
23
  lte?: V | number | Date;
@@ -27,10 +27,9 @@ export type FilterCondition<V> = {
27
27
  /** Pre-monorepo name for {@link FilterCondition}. Kept so migrated app code that spells the type out still compiles. */
28
28
  export type FilterOperators<V> = FilterCondition<V> | V;
29
29
  /**
30
- * Airtable's filter set is the shared one plus two of its own, both compiled to
31
- * formulas in `integrations/airtable/formulas.ts`:
30
+ * Airtable's filter set is the shared one plus `ilike`, compiled to a formula in
31
+ * `integrations/airtable/formulas.ts`:
32
32
  *
33
- * - `equals` — explicit equality, alongside the bare-value shorthand.
34
33
  * - `ilike` — case-insensitive equality (`LOWER(f) = LOWER(v)`). Load-bearing:
35
34
  * the `zite.auth.ts` generated for a migrated user-sync app matches rostered
36
35
  * emails with it, because Airtable compares text case-sensitively while Zite
@@ -38,9 +37,18 @@ export type FilterOperators<V> = FilterCondition<V> | V;
38
37
  *
39
38
  * Kept separate from `FilterCondition` rather than merged into it, so a Zite DB
40
39
  * filter can't offer an operator its runtime ignores.
40
+ *
41
+ * NOT `equals`, despite `formulas.ts` having a `case 'equals'`: that case only
42
+ * fires for the bare-value shorthand. `equals` is absent from
43
+ * `WhereConditionOperators`, so `isNestedCondition` doesn't recognize the
44
+ * object, the whole `{ equals: … }` is stringified as the operand, and Airtable
45
+ * 422s on `{Email}=[object Object]`.
46
+ *
47
+ * One operator per field on this path — `formulas.ts` reads
48
+ * `Object.keys(condition)[0]` and drops the rest, unlike the Zite DB filter
49
+ * builder which iterates them all.
41
50
  */
42
51
  export type AirtableFilterCondition<V> = FilterCondition<V> & {
43
- equals?: V;
44
52
  ilike?: V | V[];
45
53
  };
46
54
  export type AirtableRecordFilters<T> = {
@@ -73,6 +81,13 @@ export type FieldSelector<T> = Extract<keyof T, string> | (string & {});
73
81
  export interface TableFindAllOptions<T = Record<string, unknown>> {
74
82
  limit?: number;
75
83
  offset?: number;
84
+ /**
85
+ * IGNORED on the table path. `DatabasesFindAllParamsSchema` has no `sort`
86
+ * key, so zod strips it before either dispatch, and neither destructures it.
87
+ * Kept in the type because removing it would only turn a silent no-op into a
88
+ * compile error for apps already passing it — the real fix is to forward it.
89
+ * It DOES work on `zite.auth.findAllUsers`, which is dispatched separately.
90
+ */
76
91
  sort?: unknown[];
77
92
  filters?: RecordFilters<T>;
78
93
  fields?: Array<FieldSelector<T>>;
@@ -10,33 +10,47 @@ exports.generateBackendWrapperTs = generateBackendWrapperTs;
10
10
  exports.generateEmailSdk = generateEmailSdk;
11
11
  const parser_1 = require("@babel/parser");
12
12
  const AUTH_USERS_TABLE_ID = "zite_user";
13
- /** What a field's value looks like when a record is READ back. */
13
+ /**
14
+ * What a field's value looks like when a record is READ back.
15
+ *
16
+ * `| null` on the nullable ones is load-bearing, not pedantry: every field
17
+ * column is created nullable with no default (`recordsTableManager`), and only
18
+ * the six text types get their NULL rewritten to `''` on the way out
19
+ * (`normalizeEmptyStringFields` keys on `emptyValue === ''`). The product's own
20
+ * test asserts `fld_number` and `fld_checkbox` read back as `null`. Typing them
21
+ * non-null let `task.estimate.toFixed(2)` compile and throw.
22
+ */
14
23
  const FIELD_TYPE_MAP = {
24
+ // Text: NULL is rewritten to '' on read, so these genuinely can't be null.
15
25
  single_line_text: "string",
16
26
  long_text: "string",
17
27
  rich_text: "string",
18
28
  email: "string",
19
29
  url: "string",
20
30
  phone_number: "string",
21
- number: "number",
22
- currency: "number",
23
- percent: "number",
24
- rating: "number",
25
- duration: "number",
26
- single_select: "string",
27
- multiple_select: "string[]",
28
- checkbox: "boolean",
31
+ number: "number | null",
32
+ currency: "number | null",
33
+ percent: "number | null",
34
+ rating: "number | null",
35
+ // DECIMAL seconds, unlike Airtable's, which is formatted to "HH:mm:ss".
36
+ duration: "number | null",
37
+ checkbox: "boolean | null",
38
+ single_select: "string | null",
39
+ multiple_select: "string[] | null",
29
40
  date: "string | null",
30
41
  datetime: "string | null",
31
- attachments: "ZiteAttachment[]",
32
- // base-runner normalizes both of these to an array on write and stores them
33
- // that way, so a read never produces a bare string.
34
- linked_record: "string[]",
35
- user: "string[]",
42
+ attachments: "ZiteAttachment[] | null",
43
+ // `string | string[]`, matching the pre-monorepo type. base-runner does force
44
+ // an array on read today, but 1.0 code is written with `typeof x === 'string'`
45
+ // guards — narrowing to `string[]` turns those branches into `never` and
46
+ // fails the app's typecheck for no gain.
47
+ linked_record: "string | string[]",
48
+ user: "string | string[]",
36
49
  lookup: "unknown",
37
50
  rollup: "unknown",
38
51
  autonumber: "number",
39
- source: "string",
52
+ // JSONB change metadata (`{ type: "PublicAPI", apiKeyId: 3 }`), not a string.
53
+ source: "unknown",
40
54
  formula: "unknown",
41
55
  created_at: "string",
42
56
  updated_at: "string",
@@ -47,9 +61,18 @@ const FIELD_TYPE_MAP = {
47
61
  * looser input than it stores.
48
62
  */
49
63
  const FIELD_INPUT_TYPE_MAP = {
50
- attachments: "ZiteAttachmentInput[]",
51
- linked_record: "string | string[] | null",
52
- user: "string | string[] | null",
64
+ attachments: "ZiteAttachmentInput[] | ZiteAttachmentInput",
65
+ linked_record: "string | string[]",
66
+ user: "string | string[]",
67
+ multiple_select: "string | string[]",
68
+ date: "string | Date",
69
+ datetime: "string | Date",
70
+ number: "string | number",
71
+ currency: "string | number",
72
+ percent: "string | number",
73
+ rating: "string | number",
74
+ duration: "string | number",
75
+ checkbox: "string | boolean",
53
76
  };
54
77
  /**
55
78
  * Computed and system-managed fields, omitted from the generated input type
@@ -156,6 +179,13 @@ function keepValidSdkName(sdkName) {
156
179
  return undefined;
157
180
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(sdkName) ? sdkName : undefined;
158
181
  }
182
+ /**
183
+ * `null` is how you CLEAR a cell — every scalar write schema in
184
+ * `flexible-inputs.ts` ends in `.nullable()` and update runs with
185
+ * `allowExplicitNull`. A write type without it makes the documented way to
186
+ * empty a field a compile error.
187
+ */
188
+ const withNull = (type) => type.includes("null") ? type : `${type} | null`;
159
189
  function tsTypeForSchemaField(def, variant = "read") {
160
190
  if (def.type === "single_select" || def.type === "multiple_select") {
161
191
  const options = def.template
@@ -166,13 +196,19 @@ function tsTypeForSchemaField(def, variant = "read") {
166
196
  .map((o) => `"${o.label.replace(/"/g, '\\"')}"`)
167
197
  .join(" | ");
168
198
  const union = `${literals} | string`;
169
- if (def.type === "multiple_select")
170
- return `(${union})[]`;
171
- return union;
199
+ const read = def.type === "multiple_select" ? `(${union})[] | null` : `${union} | null`;
200
+ if (variant === "write") {
201
+ return def.type === "multiple_select"
202
+ ? `${union} | (${union})[] | null`
203
+ : `${union} | null`;
204
+ }
205
+ return read;
172
206
  }
173
207
  }
174
- if (variant === "write" && FIELD_INPUT_TYPE_MAP[def.type]) {
175
- return FIELD_INPUT_TYPE_MAP[def.type];
208
+ if (variant === "write") {
209
+ const write = FIELD_INPUT_TYPE_MAP[def.type] ?? FIELD_TYPE_MAP[def.type];
210
+ if (write)
211
+ return withNull(write);
176
212
  }
177
213
  if (FIELD_TYPE_MAP[def.type])
178
214
  return FIELD_TYPE_MAP[def.type];
@@ -411,8 +447,8 @@ function generateDbTs(schema) {
411
447
  lines.push("// .findAll({ filters?, sort?, offset?, limit?, fields? }) → { records: T[], hasMore: boolean }");
412
448
  lines.push("// filters: { fieldName: value } for equality, or { fieldName: { <op>: value } }");
413
449
  lines.push("// operators: contains, not, in, notIn, lt, lte, gt, gte");
414
- lines.push('// `not: null` means "is set"; `in`/`notIn` with an empty array apply NO filter');
415
- lines.push("// sort: [{ field: 'fieldName', direction: 'asc' | 'desc' }]");
450
+ lines.push('// `not: null` means "is set". An empty `in: []` matches nothing; an', '// empty `notIn: []` applies no filter.');
451
+ lines.push("// sort is currently IGNORED here — it is stripped before the request.", "// (it does work on zite.auth.findAllUsers). Order in SQL instead.");
416
452
  lines.push("// limit defaults to 500, max 2000; offset is a row count (a number)");
417
453
  lines.push("// .findOne({ id?, filters?, fields? }) → T | undefined");
418
454
  lines.push("// .create({ record }) → T");
@@ -474,7 +510,11 @@ function generateDbTs(schema) {
474
510
  if (jsdoc) {
475
511
  lines.push(` /** ${jsdoc} */`);
476
512
  }
477
- lines.push(` ${field.sdkName}: ${tsType};`);
513
+ // Optional, matching the pre-monorepo type (`required: ['id']` — "none of
514
+ // these are required to be defined aside from 'id'"). A `fields:`
515
+ // projection omits every key it wasn't asked for, so any field really can
516
+ // be absent on a read.
517
+ lines.push(` ${field.sdkName}?: ${tsType};`);
478
518
  }
479
519
  lines.push("};");
480
520
  lines.push("");
@@ -712,14 +752,18 @@ const AIRTABLE_FIELD_TYPE_MAP = {
712
752
  currency: "number",
713
753
  percent: "number",
714
754
  rating: "number",
715
- duration: "number",
755
+ // Formatted to "HH:mm:ss" before it reaches the app — `formatAirtableDuration`
756
+ // in BOTH dispatches. Unlike the Zite DB's, which is DECIMAL seconds.
757
+ duration: "string",
716
758
  checkbox: "boolean",
717
759
  date: "string",
718
760
  dateTime: "string",
719
761
  singleSelect: "string",
720
762
  multipleSelects: "string[]",
721
763
  multipleAttachments: "AirtableAttachment[]",
722
- multipleRecordLinks: "string[]",
764
+ // `string | string[]`, matching the pre-monorepo type — see the note on the
765
+ // Zite DB's `linked_record`.
766
+ multipleRecordLinks: "string | string[]",
723
767
  singleCollaborator: "{ id: string; email: string; name?: string }",
724
768
  multipleCollaborators: "Array<{ id: string; email: string; name?: string }>",
725
769
  // Read-only fields
@@ -732,7 +776,8 @@ const AIRTABLE_FIELD_TYPE_MAP = {
732
776
  lastModifiedTime: "string",
733
777
  createdBy: "{ id: string; email: string; name?: string }",
734
778
  lastModifiedBy: "{ id: string; email: string; name?: string }",
735
- button: "unknown",
779
+ // Both dispatches extract the URL string off the button object.
780
+ button: "string",
736
781
  externalSyncSource: "unknown",
737
782
  aiText: "string",
738
783
  };
@@ -15,9 +15,9 @@ export type FilterCondition<V> = {
15
15
  contains?: any;
16
16
  /** Not equal, or — against `null` — "is set". */
17
17
  not?: V | null;
18
- /** Empty array means "no filter", not "match nothing". */
18
+ /** An empty array matches NOTHING — the SDK's filters convert to the nested format, where `in: []` is "is one of nothing". */
19
19
  in?: V extends Array<infer E> ? E[] : V[];
20
- /** Empty array means "no filter", not "match everything". */
20
+ /** An empty array applies no filter at all — "is not one of nothing" is always true. */
21
21
  notIn?: V extends Array<infer E> ? E[] : V[];
22
22
  lt?: V | number | Date;
23
23
  lte?: V | number | Date;
@@ -27,10 +27,9 @@ export type FilterCondition<V> = {
27
27
  /** Pre-monorepo name for {@link FilterCondition}. Kept so migrated app code that spells the type out still compiles. */
28
28
  export type FilterOperators<V> = FilterCondition<V> | V;
29
29
  /**
30
- * Airtable's filter set is the shared one plus two of its own, both compiled to
31
- * formulas in `integrations/airtable/formulas.ts`:
30
+ * Airtable's filter set is the shared one plus `ilike`, compiled to a formula in
31
+ * `integrations/airtable/formulas.ts`:
32
32
  *
33
- * - `equals` — explicit equality, alongside the bare-value shorthand.
34
33
  * - `ilike` — case-insensitive equality (`LOWER(f) = LOWER(v)`). Load-bearing:
35
34
  * the `zite.auth.ts` generated for a migrated user-sync app matches rostered
36
35
  * emails with it, because Airtable compares text case-sensitively while Zite
@@ -38,9 +37,18 @@ export type FilterOperators<V> = FilterCondition<V> | V;
38
37
  *
39
38
  * Kept separate from `FilterCondition` rather than merged into it, so a Zite DB
40
39
  * filter can't offer an operator its runtime ignores.
40
+ *
41
+ * NOT `equals`, despite `formulas.ts` having a `case 'equals'`: that case only
42
+ * fires for the bare-value shorthand. `equals` is absent from
43
+ * `WhereConditionOperators`, so `isNestedCondition` doesn't recognize the
44
+ * object, the whole `{ equals: … }` is stringified as the operand, and Airtable
45
+ * 422s on `{Email}=[object Object]`.
46
+ *
47
+ * One operator per field on this path — `formulas.ts` reads
48
+ * `Object.keys(condition)[0]` and drops the rest, unlike the Zite DB filter
49
+ * builder which iterates them all.
41
50
  */
42
51
  export type AirtableFilterCondition<V> = FilterCondition<V> & {
43
- equals?: V;
44
52
  ilike?: V | V[];
45
53
  };
46
54
  export type AirtableRecordFilters<T> = {
@@ -73,6 +81,13 @@ export type FieldSelector<T> = Extract<keyof T, string> | (string & {});
73
81
  export interface TableFindAllOptions<T = Record<string, unknown>> {
74
82
  limit?: number;
75
83
  offset?: number;
84
+ /**
85
+ * IGNORED on the table path. `DatabasesFindAllParamsSchema` has no `sort`
86
+ * key, so zod strips it before either dispatch, and neither destructures it.
87
+ * Kept in the type because removing it would only turn a silent no-op into a
88
+ * compile error for apps already passing it — the real fix is to forward it.
89
+ * It DOES work on `zite.auth.findAllUsers`, which is dispatched separately.
90
+ */
76
91
  sort?: unknown[];
77
92
  filters?: RecordFilters<T>;
78
93
  fields?: Array<FieldSelector<T>>;
@@ -1,32 +1,46 @@
1
1
  import { parse } from "@babel/parser";
2
2
  const AUTH_USERS_TABLE_ID = "zite_user";
3
- /** What a field's value looks like when a record is READ back. */
3
+ /**
4
+ * What a field's value looks like when a record is READ back.
5
+ *
6
+ * `| null` on the nullable ones is load-bearing, not pedantry: every field
7
+ * column is created nullable with no default (`recordsTableManager`), and only
8
+ * the six text types get their NULL rewritten to `''` on the way out
9
+ * (`normalizeEmptyStringFields` keys on `emptyValue === ''`). The product's own
10
+ * test asserts `fld_number` and `fld_checkbox` read back as `null`. Typing them
11
+ * non-null let `task.estimate.toFixed(2)` compile and throw.
12
+ */
4
13
  const FIELD_TYPE_MAP = {
14
+ // Text: NULL is rewritten to '' on read, so these genuinely can't be null.
5
15
  single_line_text: "string",
6
16
  long_text: "string",
7
17
  rich_text: "string",
8
18
  email: "string",
9
19
  url: "string",
10
20
  phone_number: "string",
11
- number: "number",
12
- currency: "number",
13
- percent: "number",
14
- rating: "number",
15
- duration: "number",
16
- single_select: "string",
17
- multiple_select: "string[]",
18
- checkbox: "boolean",
21
+ number: "number | null",
22
+ currency: "number | null",
23
+ percent: "number | null",
24
+ rating: "number | null",
25
+ // DECIMAL seconds, unlike Airtable's, which is formatted to "HH:mm:ss".
26
+ duration: "number | null",
27
+ checkbox: "boolean | null",
28
+ single_select: "string | null",
29
+ multiple_select: "string[] | null",
19
30
  date: "string | null",
20
31
  datetime: "string | null",
21
- attachments: "ZiteAttachment[]",
22
- // base-runner normalizes both of these to an array on write and stores them
23
- // that way, so a read never produces a bare string.
24
- linked_record: "string[]",
25
- user: "string[]",
32
+ attachments: "ZiteAttachment[] | null",
33
+ // `string | string[]`, matching the pre-monorepo type. base-runner does force
34
+ // an array on read today, but 1.0 code is written with `typeof x === 'string'`
35
+ // guards — narrowing to `string[]` turns those branches into `never` and
36
+ // fails the app's typecheck for no gain.
37
+ linked_record: "string | string[]",
38
+ user: "string | string[]",
26
39
  lookup: "unknown",
27
40
  rollup: "unknown",
28
41
  autonumber: "number",
29
- source: "string",
42
+ // JSONB change metadata (`{ type: "PublicAPI", apiKeyId: 3 }`), not a string.
43
+ source: "unknown",
30
44
  formula: "unknown",
31
45
  created_at: "string",
32
46
  updated_at: "string",
@@ -37,9 +51,18 @@ const FIELD_TYPE_MAP = {
37
51
  * looser input than it stores.
38
52
  */
39
53
  const FIELD_INPUT_TYPE_MAP = {
40
- attachments: "ZiteAttachmentInput[]",
41
- linked_record: "string | string[] | null",
42
- user: "string | string[] | null",
54
+ attachments: "ZiteAttachmentInput[] | ZiteAttachmentInput",
55
+ linked_record: "string | string[]",
56
+ user: "string | string[]",
57
+ multiple_select: "string | string[]",
58
+ date: "string | Date",
59
+ datetime: "string | Date",
60
+ number: "string | number",
61
+ currency: "string | number",
62
+ percent: "string | number",
63
+ rating: "string | number",
64
+ duration: "string | number",
65
+ checkbox: "string | boolean",
43
66
  };
44
67
  /**
45
68
  * Computed and system-managed fields, omitted from the generated input type
@@ -146,6 +169,13 @@ function keepValidSdkName(sdkName) {
146
169
  return undefined;
147
170
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(sdkName) ? sdkName : undefined;
148
171
  }
172
+ /**
173
+ * `null` is how you CLEAR a cell — every scalar write schema in
174
+ * `flexible-inputs.ts` ends in `.nullable()` and update runs with
175
+ * `allowExplicitNull`. A write type without it makes the documented way to
176
+ * empty a field a compile error.
177
+ */
178
+ const withNull = (type) => type.includes("null") ? type : `${type} | null`;
149
179
  function tsTypeForSchemaField(def, variant = "read") {
150
180
  if (def.type === "single_select" || def.type === "multiple_select") {
151
181
  const options = def.template
@@ -156,13 +186,19 @@ function tsTypeForSchemaField(def, variant = "read") {
156
186
  .map((o) => `"${o.label.replace(/"/g, '\\"')}"`)
157
187
  .join(" | ");
158
188
  const union = `${literals} | string`;
159
- if (def.type === "multiple_select")
160
- return `(${union})[]`;
161
- return union;
189
+ const read = def.type === "multiple_select" ? `(${union})[] | null` : `${union} | null`;
190
+ if (variant === "write") {
191
+ return def.type === "multiple_select"
192
+ ? `${union} | (${union})[] | null`
193
+ : `${union} | null`;
194
+ }
195
+ return read;
162
196
  }
163
197
  }
164
- if (variant === "write" && FIELD_INPUT_TYPE_MAP[def.type]) {
165
- return FIELD_INPUT_TYPE_MAP[def.type];
198
+ if (variant === "write") {
199
+ const write = FIELD_INPUT_TYPE_MAP[def.type] ?? FIELD_TYPE_MAP[def.type];
200
+ if (write)
201
+ return withNull(write);
166
202
  }
167
203
  if (FIELD_TYPE_MAP[def.type])
168
204
  return FIELD_TYPE_MAP[def.type];
@@ -401,8 +437,8 @@ export function generateDbTs(schema) {
401
437
  lines.push("// .findAll({ filters?, sort?, offset?, limit?, fields? }) → { records: T[], hasMore: boolean }");
402
438
  lines.push("// filters: { fieldName: value } for equality, or { fieldName: { <op>: value } }");
403
439
  lines.push("// operators: contains, not, in, notIn, lt, lte, gt, gte");
404
- lines.push('// `not: null` means "is set"; `in`/`notIn` with an empty array apply NO filter');
405
- lines.push("// sort: [{ field: 'fieldName', direction: 'asc' | 'desc' }]");
440
+ lines.push('// `not: null` means "is set". An empty `in: []` matches nothing; an', '// empty `notIn: []` applies no filter.');
441
+ lines.push("// sort is currently IGNORED here — it is stripped before the request.", "// (it does work on zite.auth.findAllUsers). Order in SQL instead.");
406
442
  lines.push("// limit defaults to 500, max 2000; offset is a row count (a number)");
407
443
  lines.push("// .findOne({ id?, filters?, fields? }) → T | undefined");
408
444
  lines.push("// .create({ record }) → T");
@@ -464,7 +500,11 @@ export function generateDbTs(schema) {
464
500
  if (jsdoc) {
465
501
  lines.push(` /** ${jsdoc} */`);
466
502
  }
467
- lines.push(` ${field.sdkName}: ${tsType};`);
503
+ // Optional, matching the pre-monorepo type (`required: ['id']` — "none of
504
+ // these are required to be defined aside from 'id'"). A `fields:`
505
+ // projection omits every key it wasn't asked for, so any field really can
506
+ // be absent on a read.
507
+ lines.push(` ${field.sdkName}?: ${tsType};`);
468
508
  }
469
509
  lines.push("};");
470
510
  lines.push("");
@@ -702,14 +742,18 @@ const AIRTABLE_FIELD_TYPE_MAP = {
702
742
  currency: "number",
703
743
  percent: "number",
704
744
  rating: "number",
705
- duration: "number",
745
+ // Formatted to "HH:mm:ss" before it reaches the app — `formatAirtableDuration`
746
+ // in BOTH dispatches. Unlike the Zite DB's, which is DECIMAL seconds.
747
+ duration: "string",
706
748
  checkbox: "boolean",
707
749
  date: "string",
708
750
  dateTime: "string",
709
751
  singleSelect: "string",
710
752
  multipleSelects: "string[]",
711
753
  multipleAttachments: "AirtableAttachment[]",
712
- multipleRecordLinks: "string[]",
754
+ // `string | string[]`, matching the pre-monorepo type — see the note on the
755
+ // Zite DB's `linked_record`.
756
+ multipleRecordLinks: "string | string[]",
713
757
  singleCollaborator: "{ id: string; email: string; name?: string }",
714
758
  multipleCollaborators: "Array<{ id: string; email: string; name?: string }>",
715
759
  // Read-only fields
@@ -722,7 +766,8 @@ const AIRTABLE_FIELD_TYPE_MAP = {
722
766
  lastModifiedTime: "string",
723
767
  createdBy: "{ id: string; email: string; name?: string }",
724
768
  lastModifiedBy: "{ id: string; email: string; name?: string }",
725
- button: "unknown",
769
+ // Both dispatches extract the URL string off the button object.
770
+ button: "string",
726
771
  externalSyncSource: "unknown",
727
772
  aiText: "string",
728
773
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.98",
3
+ "version": "0.9.100",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",