zitejs 0.9.102 → 0.9.104

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.
@@ -16,6 +16,16 @@ export type ZiteSchema = {
16
16
  };
17
17
  export declare function toPascalCase(name: string): string;
18
18
  export declare function toCamelCase(name: string): string;
19
+ /**
20
+ * camelCase for a FRESH sdkName, reading acronym runs as words: "VIP" ->
21
+ * "vip", "APIKey" -> "apiKey" (plain toCamelCase yields "vIP" / "aPIKey").
22
+ *
23
+ * Deliberately not folded into toCamelCase: that also derives endpoint
24
+ * identifiers from existing filenames on every generate, where normalizing
25
+ * would rename a working app's `api.sendSMS`. This is only for names being
26
+ * chosen for the first time — generateSchema preserves existing sdkNames.
27
+ */
28
+ export declare function toSdkName(name: string): string;
19
29
  /**
20
30
  * Build a ZiteSchema from a Database API response.
21
31
  *
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.toPascalCase = toPascalCase;
4
4
  exports.toCamelCase = toCamelCase;
5
+ exports.toSdkName = toSdkName;
5
6
  exports.generateSchema = generateSchema;
6
7
  exports.generateDbTs = generateDbTs;
7
8
  exports.generateApiTs = generateApiTs;
@@ -13,48 +14,73 @@ const AUTH_USERS_TABLE_ID = "zite_user";
13
14
  /**
14
15
  * What a field's value looks like when a record is READ back.
15
16
  *
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.
17
+ * **These are deliberately optimistic, and that is load-bearing.** Every column
18
+ * is nullable with no default (`recordsTableManager`) and only the six text
19
+ * types get NULL rewritten to `''`, so the wire really does answer `null` for
20
+ * the rest. They are typed `T | undefined` anyway, matching the pre-monorepo
21
+ * SDK — `| null` appears zero times in its generator.
22
+ *
23
+ * Two things have been tried and both broke apps, so before "fixing" this:
24
+ *
25
+ * 1. **Typing the truth** (`9a44ab3`). 7880 of 7880 audited 1.0 endpoints
26
+ * declare an `outputSchema` written as a mirror of this type with every cell
27
+ * `.optional()`, and `createEndpoint` constrains `execute`'s return against
28
+ * it. `| null` fails the app's typecheck on lines nobody edited — 52 of 144
29
+ * apps, ~2,100 sites.
30
+ * 2. **Making the runtime emit `undefined`** so the type becomes true. The
31
+ * idiom that breaks is `.filter(m => m.homeScore !== null)`, which silently
32
+ * becomes a no-op and lets empty rows through: wrong results, no error.
33
+ * 10 of 328 exported apps do exactly this.
34
+ *
35
+ * TODO: make these accurate in a deliberate zitejs major, once apps are
36
+ * migrated. Each app pins its own version, so the change only reaches apps that
37
+ * opt in by bumping — which is the only way to do it without breaking (2).
38
+ *
39
+ * Writes are unaffected — `null` is how you clear a cell. See
40
+ * `FIELD_INPUT_TYPE_MAP` and `withNull`.
22
41
  */
23
42
  const FIELD_TYPE_MAP = {
24
- // Text: NULL is rewritten to '' on read, so these genuinely can't be null.
43
+ // Text: NULL is rewritten to '' on read, so these are never even absent.
25
44
  single_line_text: "string",
26
45
  long_text: "string",
27
46
  rich_text: "string",
28
47
  email: "string",
29
48
  url: "string",
30
49
  phone_number: "string",
31
- number: "number | null",
32
- currency: "number | null",
33
- percent: "number | null",
34
- rating: "number | null",
50
+ number: "number",
51
+ currency: "number",
52
+ percent: "number",
53
+ rating: "number",
35
54
  // 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",
40
- date: "string | null",
41
- datetime: "string | null",
42
- attachments: "ZiteAttachment[] | null",
55
+ // 1.0 typed this `string`, which was simply wrong — base-runner groups it
56
+ // with the numeric types — so it stays a number rather than reverting.
57
+ duration: "number",
58
+ checkbox: "boolean",
59
+ single_select: "string",
60
+ multiple_select: "string[]",
61
+ date: "string",
62
+ datetime: "string",
63
+ attachments: "ZiteAttachment[]",
43
64
  // `string | string[]`, matching the pre-monorepo type. base-runner does force
44
65
  // an array on read today, but 1.0 code is written with `typeof x === 'string'`
45
66
  // guards — narrowing to `string[]` turns those branches into `never` and
46
67
  // fails the app's typecheck for no gain.
47
68
  linked_record: "string | string[]",
48
69
  user: "string | string[]",
49
- lookup: "unknown",
50
- rollup: "unknown",
70
+ // `any`, not `unknown`, and deliberately: these are values the platform
71
+ // cannot type — a lookup is whatever the far side holds. 1.0 said `any`, and
72
+ // `unknown` buys nothing because there is no narrowing an app could have
73
+ // written in advance: `sum + (x || 0)` is a TS2365 on `unknown`, and the
74
+ // guarded spellings already narrowed fine under `any`.
75
+ lookup: "any",
76
+ rollup: "any",
51
77
  autonumber: "number",
52
78
  // JSONB change metadata (`{ type: "PublicAPI", apiKeyId: 3 }`), not a string.
53
- source: "unknown",
54
- formula: "unknown",
79
+ source: "any",
80
+ formula: "any",
55
81
  created_at: "string",
56
82
  updated_at: "string",
57
- updated_by: "string | null",
83
+ updated_by: "string",
58
84
  };
59
85
  /**
60
86
  * Field types whose WRITE shape differs from their read shape — the API accepts
@@ -170,6 +196,21 @@ function toCamelCase(name) {
170
196
  const pascal = toPascalCase(name);
171
197
  return pascal.charAt(0).toLowerCase() + pascal.slice(1);
172
198
  }
199
+ /**
200
+ * camelCase for a FRESH sdkName, reading acronym runs as words: "VIP" ->
201
+ * "vip", "APIKey" -> "apiKey" (plain toCamelCase yields "vIP" / "aPIKey").
202
+ *
203
+ * Deliberately not folded into toCamelCase: that also derives endpoint
204
+ * identifiers from existing filenames on every generate, where normalizing
205
+ * would rename a working app's `api.sendSMS`. This is only for names being
206
+ * chosen for the first time — generateSchema preserves existing sdkNames.
207
+ */
208
+ function toSdkName(name) {
209
+ const deAcronymed = name
210
+ .replace(/([A-Z]+)([A-Z][a-z])/g, (_, run, next) => run.charAt(0) + run.slice(1).toLowerCase() + next)
211
+ .replace(/([A-Z])([A-Z]+)/g, (_, first, rest) => first + rest.toLowerCase());
212
+ return toCamelCase(deAcronymed);
213
+ }
173
214
  /**
174
215
  * Existing sdkNames are preserved across syncs so user code keeps compiling,
175
216
  * but schemas written before the sanitizer stripped trailing symbols can carry
@@ -198,9 +239,9 @@ function tsTypeForSchemaField(def, variant = "read") {
198
239
  .map((o) => `"${o.label.replace(/"/g, '\\"')}"`)
199
240
  .join(" | ");
200
241
  const union = `${literals} | string`;
201
- const read = def.type === "multiple_select"
202
- ? `(${union})[] | null`
203
- : `${union} | null`;
242
+ // No `| null` on the read side — see FIELD_TYPE_MAP. The write side keeps
243
+ // it below, since null is how a select cell is cleared.
244
+ const read = def.type === "multiple_select" ? `(${union})[]` : union;
204
245
  if (variant === "write") {
205
246
  return def.type === "multiple_select"
206
247
  ? `${union} | (${union})[] | null`
@@ -338,13 +379,13 @@ function generateSchema(database, existingSchema) {
338
379
  const { id: _id, order: _order, ...definition } = field;
339
380
  fields.push({
340
381
  id: field.id,
341
- sdkName: keepValidSdkName(existing?.sdkName) ?? toCamelCase(field.name),
382
+ sdkName: keepValidSdkName(existing?.sdkName) ?? toSdkName(field.name),
342
383
  definition,
343
384
  });
344
385
  }
345
386
  tables.push({
346
387
  id: table.id,
347
- sdkName: keepValidSdkName(existingTable?.sdkName) ?? toCamelCase(table.name),
388
+ sdkName: keepValidSdkName(existingTable?.sdkName) ?? toSdkName(table.name),
348
389
  primaryFieldId: table.primaryFieldId,
349
390
  fields,
350
391
  });
@@ -16,6 +16,16 @@ export type ZiteSchema = {
16
16
  };
17
17
  export declare function toPascalCase(name: string): string;
18
18
  export declare function toCamelCase(name: string): string;
19
+ /**
20
+ * camelCase for a FRESH sdkName, reading acronym runs as words: "VIP" ->
21
+ * "vip", "APIKey" -> "apiKey" (plain toCamelCase yields "vIP" / "aPIKey").
22
+ *
23
+ * Deliberately not folded into toCamelCase: that also derives endpoint
24
+ * identifiers from existing filenames on every generate, where normalizing
25
+ * would rename a working app's `api.sendSMS`. This is only for names being
26
+ * chosen for the first time — generateSchema preserves existing sdkNames.
27
+ */
28
+ export declare function toSdkName(name: string): string;
19
29
  /**
20
30
  * Build a ZiteSchema from a Database API response.
21
31
  *
@@ -3,48 +3,73 @@ const AUTH_USERS_TABLE_ID = "zite_user";
3
3
  /**
4
4
  * What a field's value looks like when a record is READ back.
5
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.
6
+ * **These are deliberately optimistic, and that is load-bearing.** Every column
7
+ * is nullable with no default (`recordsTableManager`) and only the six text
8
+ * types get NULL rewritten to `''`, so the wire really does answer `null` for
9
+ * the rest. They are typed `T | undefined` anyway, matching the pre-monorepo
10
+ * SDK — `| null` appears zero times in its generator.
11
+ *
12
+ * Two things have been tried and both broke apps, so before "fixing" this:
13
+ *
14
+ * 1. **Typing the truth** (`9a44ab3`). 7880 of 7880 audited 1.0 endpoints
15
+ * declare an `outputSchema` written as a mirror of this type with every cell
16
+ * `.optional()`, and `createEndpoint` constrains `execute`'s return against
17
+ * it. `| null` fails the app's typecheck on lines nobody edited — 52 of 144
18
+ * apps, ~2,100 sites.
19
+ * 2. **Making the runtime emit `undefined`** so the type becomes true. The
20
+ * idiom that breaks is `.filter(m => m.homeScore !== null)`, which silently
21
+ * becomes a no-op and lets empty rows through: wrong results, no error.
22
+ * 10 of 328 exported apps do exactly this.
23
+ *
24
+ * TODO: make these accurate in a deliberate zitejs major, once apps are
25
+ * migrated. Each app pins its own version, so the change only reaches apps that
26
+ * opt in by bumping — which is the only way to do it without breaking (2).
27
+ *
28
+ * Writes are unaffected — `null` is how you clear a cell. See
29
+ * `FIELD_INPUT_TYPE_MAP` and `withNull`.
12
30
  */
13
31
  const FIELD_TYPE_MAP = {
14
- // Text: NULL is rewritten to '' on read, so these genuinely can't be null.
32
+ // Text: NULL is rewritten to '' on read, so these are never even absent.
15
33
  single_line_text: "string",
16
34
  long_text: "string",
17
35
  rich_text: "string",
18
36
  email: "string",
19
37
  url: "string",
20
38
  phone_number: "string",
21
- number: "number | null",
22
- currency: "number | null",
23
- percent: "number | null",
24
- rating: "number | null",
39
+ number: "number",
40
+ currency: "number",
41
+ percent: "number",
42
+ rating: "number",
25
43
  // 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",
30
- date: "string | null",
31
- datetime: "string | null",
32
- attachments: "ZiteAttachment[] | null",
44
+ // 1.0 typed this `string`, which was simply wrong — base-runner groups it
45
+ // with the numeric types — so it stays a number rather than reverting.
46
+ duration: "number",
47
+ checkbox: "boolean",
48
+ single_select: "string",
49
+ multiple_select: "string[]",
50
+ date: "string",
51
+ datetime: "string",
52
+ attachments: "ZiteAttachment[]",
33
53
  // `string | string[]`, matching the pre-monorepo type. base-runner does force
34
54
  // an array on read today, but 1.0 code is written with `typeof x === 'string'`
35
55
  // guards — narrowing to `string[]` turns those branches into `never` and
36
56
  // fails the app's typecheck for no gain.
37
57
  linked_record: "string | string[]",
38
58
  user: "string | string[]",
39
- lookup: "unknown",
40
- rollup: "unknown",
59
+ // `any`, not `unknown`, and deliberately: these are values the platform
60
+ // cannot type — a lookup is whatever the far side holds. 1.0 said `any`, and
61
+ // `unknown` buys nothing because there is no narrowing an app could have
62
+ // written in advance: `sum + (x || 0)` is a TS2365 on `unknown`, and the
63
+ // guarded spellings already narrowed fine under `any`.
64
+ lookup: "any",
65
+ rollup: "any",
41
66
  autonumber: "number",
42
67
  // JSONB change metadata (`{ type: "PublicAPI", apiKeyId: 3 }`), not a string.
43
- source: "unknown",
44
- formula: "unknown",
68
+ source: "any",
69
+ formula: "any",
45
70
  created_at: "string",
46
71
  updated_at: "string",
47
- updated_by: "string | null",
72
+ updated_by: "string",
48
73
  };
49
74
  /**
50
75
  * Field types whose WRITE shape differs from their read shape — the API accepts
@@ -160,6 +185,21 @@ export function toCamelCase(name) {
160
185
  const pascal = toPascalCase(name);
161
186
  return pascal.charAt(0).toLowerCase() + pascal.slice(1);
162
187
  }
188
+ /**
189
+ * camelCase for a FRESH sdkName, reading acronym runs as words: "VIP" ->
190
+ * "vip", "APIKey" -> "apiKey" (plain toCamelCase yields "vIP" / "aPIKey").
191
+ *
192
+ * Deliberately not folded into toCamelCase: that also derives endpoint
193
+ * identifiers from existing filenames on every generate, where normalizing
194
+ * would rename a working app's `api.sendSMS`. This is only for names being
195
+ * chosen for the first time — generateSchema preserves existing sdkNames.
196
+ */
197
+ export function toSdkName(name) {
198
+ const deAcronymed = name
199
+ .replace(/([A-Z]+)([A-Z][a-z])/g, (_, run, next) => run.charAt(0) + run.slice(1).toLowerCase() + next)
200
+ .replace(/([A-Z])([A-Z]+)/g, (_, first, rest) => first + rest.toLowerCase());
201
+ return toCamelCase(deAcronymed);
202
+ }
163
203
  /**
164
204
  * Existing sdkNames are preserved across syncs so user code keeps compiling,
165
205
  * but schemas written before the sanitizer stripped trailing symbols can carry
@@ -188,9 +228,9 @@ function tsTypeForSchemaField(def, variant = "read") {
188
228
  .map((o) => `"${o.label.replace(/"/g, '\\"')}"`)
189
229
  .join(" | ");
190
230
  const union = `${literals} | string`;
191
- const read = def.type === "multiple_select"
192
- ? `(${union})[] | null`
193
- : `${union} | null`;
231
+ // No `| null` on the read side — see FIELD_TYPE_MAP. The write side keeps
232
+ // it below, since null is how a select cell is cleared.
233
+ const read = def.type === "multiple_select" ? `(${union})[]` : union;
194
234
  if (variant === "write") {
195
235
  return def.type === "multiple_select"
196
236
  ? `${union} | (${union})[] | null`
@@ -328,13 +368,13 @@ export function generateSchema(database, existingSchema) {
328
368
  const { id: _id, order: _order, ...definition } = field;
329
369
  fields.push({
330
370
  id: field.id,
331
- sdkName: keepValidSdkName(existing?.sdkName) ?? toCamelCase(field.name),
371
+ sdkName: keepValidSdkName(existing?.sdkName) ?? toSdkName(field.name),
332
372
  definition,
333
373
  });
334
374
  }
335
375
  tables.push({
336
376
  id: table.id,
337
- sdkName: keepValidSdkName(existingTable?.sdkName) ?? toCamelCase(table.name),
377
+ sdkName: keepValidSdkName(existingTable?.sdkName) ?? toSdkName(table.name),
338
378
  primaryFieldId: table.primaryFieldId,
339
379
  fields,
340
380
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zitejs",
3
- "version": "0.9.102",
3
+ "version": "0.9.104",
4
4
  "description": "The Zite framework — build apps on Zite Database",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",