zitejs 0.9.105 → 0.9.107

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.
@@ -0,0 +1,279 @@
1
+ "use strict";
2
+ /**
3
+ * Allocation of the identifiers `generateDbTs` emits.
4
+ *
5
+ * Every accessor on the generated `zite` object, and every field key on a
6
+ * record type, has to be unique — and `generateSchema` used to choose them with
7
+ * `toSdkName` and no memory at all. Two consequences, both seen in the wild:
8
+ *
9
+ * - A table named "Notifications" allocated `notifications`, which
10
+ * `generateDbTs` also emits for the platform's notifications client. The
11
+ * sentinel is written last, so it won: the table lost its client entirely
12
+ * and typed as `{ create(params: NotificationsCreateParams) }`.
13
+ * - Two fields whose display names normalize alike both claimed one accessor,
14
+ * and the second silently overwrote the first in the emitted type. No
15
+ * error, no diagnostic — just a column you could no longer read. Zite 1.0's
16
+ * generator de-duplicated with numeric suffixes; 2.0 dropped that.
17
+ *
18
+ * `allocateSchemaNames` is idempotent: a schema whose names are already unique
19
+ * and unreserved allocates to itself. That is what lets `generateDbTs` run it
20
+ * defensively over its input — a schema file written before this existed
21
+ * repairs itself on the next generate instead of emitting TypeScript that
22
+ * cannot compile.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.RESERVED_FIELD_ACCESSORS = exports.RESERVED_IDENTIFIERS = exports.RESERVED_TABLE_ACCESSORS = void 0;
26
+ exports.toPascalCase = toPascalCase;
27
+ exports.toCamelCase = toCamelCase;
28
+ exports.toSdkName = toSdkName;
29
+ exports.keepValidSdkName = keepValidSdkName;
30
+ exports.allocateSchemaNames = allocateSchemaNames;
31
+ exports.normalizeSchemaNames = normalizeSchemaNames;
32
+ exports.describeRenames = describeRenames;
33
+ // The case helpers live here rather than in lib.ts so this module has no import
34
+ // back into it — `lib.ts` needs the allocator, and a cycle between the two is
35
+ // avoidable. `lib.ts` re-exports them, so the published surface is unchanged.
36
+ function toPascalCase(name) {
37
+ const pascal = name
38
+ .replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
39
+ .replace(/^(.)/, (_, c) => c.toUpperCase())
40
+ // Drop leftover non-alphanumerics (e.g. a trailing ">" from "</p>") so the
41
+ // sdkName is always a valid identifier in .zite/db.ts.
42
+ .replace(/[^a-zA-Z0-9]+/g, "");
43
+ if (!pascal)
44
+ return "_";
45
+ return /^[0-9]/.test(pascal) ? `_${pascal}` : pascal; // can't start with a digit
46
+ }
47
+ function toCamelCase(name) {
48
+ const pascal = toPascalCase(name);
49
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
50
+ }
51
+ /**
52
+ * camelCase for a FRESH sdkName, reading acronym runs as words: "VIP" ->
53
+ * "vip", "APIKey" -> "apiKey" (plain toCamelCase yields "vIP" / "aPIKey").
54
+ *
55
+ * Deliberately not folded into toCamelCase: that also derives endpoint
56
+ * identifiers from existing filenames on every generate, where normalizing
57
+ * would rename a working app's `api.sendSMS`. This is only for names being
58
+ * chosen for the first time — generateSchema preserves existing sdkNames.
59
+ */
60
+ function toSdkName(name) {
61
+ const deAcronymed = name
62
+ .replace(/([A-Z]+)([A-Z][a-z])/g, (_, run, next) => run.charAt(0) + run.slice(1).toLowerCase() + next)
63
+ .replace(/([A-Z])([A-Z]+)/g, (_, first, rest) => first + rest.toLowerCase());
64
+ return toCamelCase(deAcronymed);
65
+ }
66
+ /**
67
+ * Accessors `generateDbTs` puts on the `zite` object itself, after the tables.
68
+ * A table that allocates one of these produces a duplicate key.
69
+ */
70
+ exports.RESERVED_TABLE_ACCESSORS = [
71
+ "sql",
72
+ "notifications",
73
+ "auth",
74
+ ];
75
+ /**
76
+ * Never safe as a generated key, in any namespace. Assigning `__proto__` in an
77
+ * object literal sets the prototype rather than defining a property, and
78
+ * `constructor` / `prototype` shadow members every object already carries.
79
+ */
80
+ exports.RESERVED_IDENTIFIERS = [
81
+ "__proto__",
82
+ "constructor",
83
+ "prototype",
84
+ ];
85
+ /**
86
+ * `id` is emitted by hand on every record type and filtered out of every input
87
+ * type, so a field that allocated it vanished from both. Reserving it means a
88
+ * field genuinely named "ID" gets a usable accessor instead.
89
+ */
90
+ exports.RESERVED_FIELD_ACCESSORS = ["id"];
91
+ /** Guards the suffix loop. Far above any real schema; see `allocate`. */
92
+ const MAX_SUFFIX_ATTEMPTS = 10_000;
93
+ /**
94
+ * Unique identifiers within one namespace.
95
+ *
96
+ * `derive` names the extra keys an allocation also occupies. A table needs it:
97
+ * its sdkName becomes both a property on `zite` and, PascalCased, a pair of
98
+ * exported type declarations, so `orders` and `Orders` are distinct accessors
99
+ * that would emit `OrdersRecordType` twice.
100
+ */
101
+ class SdkNameAllocator {
102
+ taken = new Set();
103
+ reserved;
104
+ derive;
105
+ constructor(opts = {}) {
106
+ this.reserved = new Set([
107
+ ...exports.RESERVED_IDENTIFIERS,
108
+ ...(opts.reserved ?? []),
109
+ ]);
110
+ this.derive = opts.derive ?? (() => []);
111
+ }
112
+ keysFor(name) {
113
+ return [name, ...this.derive(name)];
114
+ }
115
+ isFree(name) {
116
+ return this.keysFor(name).every((key) => !this.taken.has(key) && !this.reserved.has(key));
117
+ }
118
+ /** Free right now, without claiming it. */
119
+ available(name) {
120
+ return this.isFree(name);
121
+ }
122
+ claim(name) {
123
+ for (const key of this.keysFor(name))
124
+ this.taken.add(key);
125
+ return name;
126
+ }
127
+ /**
128
+ * `name`, or the first `name2`, `name3`… that is free. Suffixes start at 2
129
+ * because that is what the name means: the second table called Notifications.
130
+ */
131
+ allocate(preferred) {
132
+ const base = preferred || "_";
133
+ if (this.isFree(base))
134
+ return this.claim(base);
135
+ for (let suffix = 2; suffix <= MAX_SUFFIX_ATTEMPTS; suffix++) {
136
+ const candidate = `${base}${suffix}`;
137
+ if (this.isFree(candidate))
138
+ return this.claim(candidate);
139
+ }
140
+ // Unreachable: each attempt blocks at most two keys, so a free candidate
141
+ // exists long before the cap. A generator must never return a duplicate,
142
+ // so say so rather than emitting one.
143
+ throw new Error(`Could not allocate a unique SDK name for "${preferred}" after ${MAX_SUFFIX_ATTEMPTS} attempts`);
144
+ }
145
+ }
146
+ /**
147
+ * Existing sdkNames are preserved across syncs so user code keeps compiling,
148
+ * but a schema written before the sanitizer stripped trailing symbols can carry
149
+ * an invalid identifier (e.g. `timeSeconds)`). Keep one only when it is a valid
150
+ * identifier; otherwise the caller recomputes it from the display name.
151
+ */
152
+ function keepValidSdkName(sdkName) {
153
+ if (!sdkName)
154
+ return undefined;
155
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(sdkName) ? sdkName : undefined;
156
+ }
157
+ /**
158
+ * Resolve every drafted name to a unique, unreserved one.
159
+ *
160
+ * Two passes per namespace: locked names claim first in declaration order, then
161
+ * fresh ones fill in around them. Without that ordering a new table could take
162
+ * an accessor an existing app already imports.
163
+ */
164
+ function allocateSchemaNames(drafts) {
165
+ const renames = [];
166
+ const tableNames = new SdkNameAllocator({
167
+ reserved: exports.RESERVED_TABLE_ACCESSORS,
168
+ // The PascalCase form is its own pair of exported declarations.
169
+ derive: (name) => [`type:${toPascalCase(name)}`],
170
+ });
171
+ const resolved = new Map();
172
+ for (const pass of [true, false]) {
173
+ for (const entry of drafts) {
174
+ if (entry.draft.locked !== pass)
175
+ continue;
176
+ if (pass && !tableNames.available(entry.draft.preferred))
177
+ continue;
178
+ const sdkName = tableNames.allocate(entry.draft.preferred);
179
+ resolved.set(entry.table.id, sdkName);
180
+ if (sdkName !== entry.draft.preferred) {
181
+ renames.push({
182
+ kind: "table",
183
+ tableId: entry.table.id,
184
+ from: entry.draft.preferred,
185
+ to: sdkName,
186
+ });
187
+ }
188
+ }
189
+ }
190
+ // A locked name skipped in pass 1 (reserved, or claimed by an earlier locked
191
+ // name) falls through to here and is suffixed like any other.
192
+ for (const entry of drafts) {
193
+ if (resolved.has(entry.table.id))
194
+ continue;
195
+ const sdkName = tableNames.allocate(entry.draft.preferred);
196
+ resolved.set(entry.table.id, sdkName);
197
+ renames.push({
198
+ kind: "table",
199
+ tableId: entry.table.id,
200
+ from: entry.draft.preferred,
201
+ to: sdkName,
202
+ });
203
+ }
204
+ const tables = drafts.map((entry) => {
205
+ const fieldNames = new SdkNameAllocator({
206
+ reserved: exports.RESERVED_FIELD_ACCESSORS,
207
+ });
208
+ const resolvedFields = new Map();
209
+ for (const pass of [true, false]) {
210
+ for (const { field, draft } of entry.fields) {
211
+ if (draft.locked !== pass)
212
+ continue;
213
+ if (pass && !fieldNames.available(draft.preferred))
214
+ continue;
215
+ const sdkName = fieldNames.allocate(draft.preferred);
216
+ resolvedFields.set(field.id, sdkName);
217
+ if (sdkName !== draft.preferred) {
218
+ renames.push({
219
+ kind: "field",
220
+ tableId: entry.table.id,
221
+ fieldId: field.id,
222
+ from: draft.preferred,
223
+ to: sdkName,
224
+ });
225
+ }
226
+ }
227
+ }
228
+ for (const { field, draft } of entry.fields) {
229
+ if (resolvedFields.has(field.id))
230
+ continue;
231
+ const sdkName = fieldNames.allocate(draft.preferred);
232
+ resolvedFields.set(field.id, sdkName);
233
+ renames.push({
234
+ kind: "field",
235
+ tableId: entry.table.id,
236
+ fieldId: field.id,
237
+ from: draft.preferred,
238
+ to: sdkName,
239
+ });
240
+ }
241
+ return {
242
+ ...entry.table,
243
+ sdkName: resolved.get(entry.table.id),
244
+ fields: entry.fields.map(({ field }) => ({
245
+ ...field,
246
+ sdkName: resolvedFields.get(field.id),
247
+ })),
248
+ };
249
+ });
250
+ return { tables, renames };
251
+ }
252
+ /**
253
+ * Repair a schema whose names may already be unique — the idempotent case — or
254
+ * may predate allocation entirely. Everything is treated as locked: these names
255
+ * are what user code compiles against, so only a genuine conflict moves one.
256
+ */
257
+ function normalizeSchemaNames(schema) {
258
+ const { tables, renames } = allocateSchemaNames(schema.tables.map((table) => ({
259
+ table,
260
+ draft: {
261
+ preferred: keepValidSdkName(table.sdkName) ?? toSdkName(table.sdkName),
262
+ locked: true,
263
+ },
264
+ fields: table.fields.map((field) => ({
265
+ field,
266
+ draft: {
267
+ preferred: keepValidSdkName(field.sdkName) ?? toSdkName(field.sdkName),
268
+ locked: true,
269
+ },
270
+ })),
271
+ })));
272
+ return { schema: { tables }, renames };
273
+ }
274
+ /** One line per rename, for a generator that has no logger of its own. */
275
+ function describeRenames(renames) {
276
+ return renames.map((r) => r.kind === "table"
277
+ ? `table "${r.from}" -> "${r.to}" (id ${r.tableId})`
278
+ : `field "${r.from}" -> "${r.to}" (table ${r.tableId}, field ${r.fieldId})`);
279
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const lib_js_1 = require("./lib.js");
5
+ const sdkNames_js_1 = require("./sdkNames.js");
6
+ // `generateSchema` warns on every rename; silence it so the suite stays
7
+ // readable, and assert on the emitted names instead.
8
+ (0, vitest_1.beforeEach)(() => {
9
+ vitest_1.vi.spyOn(console, "warn").mockImplementation(() => { });
10
+ });
11
+ (0, vitest_1.afterEach)(() => {
12
+ vitest_1.vi.restoreAllMocks();
13
+ });
14
+ const field = (id, name) => ({ id, name, type: "single_line_text", order: 0 });
15
+ const database = (tables) => ({
16
+ id: "base_1",
17
+ name: "Base",
18
+ tables,
19
+ createdAt: "",
20
+ updatedAt: "",
21
+ url: "",
22
+ });
23
+ /** Every key in the emitted `export const zite = { … }` literal. */
24
+ const ziteKeys = (source) => {
25
+ const body = source.slice(source.indexOf("export const zite = {"));
26
+ return [...body.matchAll(/^ {2}([A-Za-z_$][\w$]*):/gm)].map((m) => m[1]);
27
+ };
28
+ /** Every property key on one emitted record type. */
29
+ const recordTypeKeys = (source, typeName) => {
30
+ const start = source.indexOf(`export type ${typeName} = {`);
31
+ const body = source.slice(start, source.indexOf("};", start));
32
+ return [...body.matchAll(/^ {2}([A-Za-z_$][\w$]*)\??:/gm)].map((m) => m[1]);
33
+ };
34
+ (0, vitest_1.describe)("reserved platform accessors", () => {
35
+ // Sudzy Dashboard: a table named "Notifications" emitted a second
36
+ // `notifications:` key, and the platform client — written last — won. The
37
+ // table lost findAll and typed as `{ create(NotificationsCreateParams) }`.
38
+ (0, vitest_1.it)("moves a table off the notifications accessor", () => {
39
+ const schema = (0, lib_js_1.generateSchema)(database([{ id: "tbl_1", name: "Notifications", fields: [] }]));
40
+ (0, vitest_1.expect)(schema.tables[0].sdkName).toBe("notifications2");
41
+ const keys = ziteKeys((0, lib_js_1.generateDbTs)(schema));
42
+ (0, vitest_1.expect)(keys).toContain("notifications2");
43
+ (0, vitest_1.expect)(keys.filter((k) => k === "notifications")).toHaveLength(1);
44
+ (0, vitest_1.expect)(new Set(keys).size).toBe(keys.length);
45
+ });
46
+ vitest_1.it.each(["sql", "auth", "notifications"])("keeps the platform %s accessor for the platform", (reserved) => {
47
+ const schema = (0, lib_js_1.generateSchema)(database([{ id: "tbl_1", name: reserved, fields: [] }]));
48
+ (0, vitest_1.expect)(schema.tables[0].sdkName).toBe(`${reserved}2`);
49
+ const keys = ziteKeys((0, lib_js_1.generateDbTs)(schema));
50
+ (0, vitest_1.expect)(new Set(keys).size).toBe(keys.length);
51
+ });
52
+ vitest_1.it.each(["__proto__", "constructor", "prototype"])("refuses %s as a table accessor", (hazard) => {
53
+ const schema = (0, lib_js_1.generateSchema)(database([{ id: "tbl_1", name: hazard, fields: [] }]));
54
+ (0, vitest_1.expect)(schema.tables[0].sdkName).not.toBe(hazard);
55
+ });
56
+ });
57
+ (0, vitest_1.describe)("collision de-duplication", () => {
58
+ // Nevada Car Coalition: 1.0's generator suffixed colliding names, so the app
59
+ // was written against `category1` / `sponsor1`. 2.0 had no accumulator at
60
+ // all, so the second field silently overwrote the first.
61
+ (0, vitest_1.it)("keeps both fields when two display names normalize alike", () => {
62
+ const schema = (0, lib_js_1.generateSchema)(database([
63
+ {
64
+ id: "tbl_1",
65
+ name: "K9 Winners",
66
+ fields: [field("fld_1", "Category"), field("fld_2", "category")],
67
+ },
68
+ ]));
69
+ const names = schema.tables[0].fields.map((f) => f.sdkName);
70
+ (0, vitest_1.expect)(names).toEqual(["category", "category2"]);
71
+ const keys = recordTypeKeys((0, lib_js_1.generateDbTs)(schema), "K9WinnersRecordType");
72
+ (0, vitest_1.expect)(keys).toEqual(["id", "category", "category2"]);
73
+ });
74
+ (0, vitest_1.it)("de-duplicates two tables that normalize alike", () => {
75
+ const schema = (0, lib_js_1.generateSchema)(database([
76
+ { id: "tbl_1", name: "Orders", fields: [] },
77
+ { id: "tbl_2", name: "orders", fields: [] },
78
+ ]));
79
+ (0, vitest_1.expect)(schema.tables.map((t) => t.sdkName)).toEqual(["orders", "orders2"]);
80
+ });
81
+ // Two sdkNames sharing a PascalCase form emit `FooRecordType` twice, which is
82
+ // a duplicate identifier even though the accessors differ.
83
+ (0, vitest_1.it)("separates tables whose class names would collide", () => {
84
+ const source = (0, lib_js_1.generateDbTs)({
85
+ tables: [
86
+ { id: "tbl_1", sdkName: "orders", fields: [] },
87
+ { id: "tbl_2", sdkName: "Orders", fields: [] },
88
+ ],
89
+ });
90
+ const declared = [...source.matchAll(/export type (\w+RecordType)/g)].map((m) => m[1]);
91
+ (0, vitest_1.expect)(new Set(declared).size).toBe(declared.length);
92
+ });
93
+ (0, vitest_1.it)("reserves id so a field named ID stays readable", () => {
94
+ const schema = (0, lib_js_1.generateSchema)(database([{ id: "tbl_1", name: "Rows", fields: [field("fld_1", "ID")] }]));
95
+ (0, vitest_1.expect)(schema.tables[0].fields[0].sdkName).toBe("id2");
96
+ (0, vitest_1.expect)(recordTypeKeys((0, lib_js_1.generateDbTs)(schema), "RowsRecordType")).toEqual([
97
+ "id",
98
+ "id2",
99
+ ]);
100
+ });
101
+ });
102
+ (0, vitest_1.describe)("name stability", () => {
103
+ const existing = {
104
+ tables: [
105
+ {
106
+ id: "tbl_1",
107
+ sdkName: "priceList",
108
+ fields: [
109
+ { id: "fld_1", sdkName: "srp", definition: { type: "number" } },
110
+ ],
111
+ },
112
+ ],
113
+ };
114
+ // Kelly: the app was written against `srp`, and the field's display name has
115
+ // drifted since. A locked name is what the app compiles against.
116
+ (0, vitest_1.it)("preserves a locked name over the one the display name would produce", () => {
117
+ const schema = (0, lib_js_1.generateSchema)(database([
118
+ {
119
+ id: "tbl_1",
120
+ name: "Price List",
121
+ fields: [field("fld_1", "SRP (Suggested Retail Price)")],
122
+ },
123
+ ]), existing);
124
+ (0, vitest_1.expect)(schema.tables[0].fields[0].sdkName).toBe("srp");
125
+ });
126
+ (0, vitest_1.it)("does not let a new field displace a locked one", () => {
127
+ const schema = (0, lib_js_1.generateSchema)(database([
128
+ {
129
+ id: "tbl_1",
130
+ name: "Price List",
131
+ // The new field's fresh name is exactly what fld_1 holds.
132
+ fields: [field("fld_2", "SRP"), field("fld_1", "Anything")],
133
+ },
134
+ ]), existing);
135
+ const byId = Object.fromEntries(schema.tables[0].fields.map((f) => [f.id, f.sdkName]));
136
+ (0, vitest_1.expect)(byId.fld_1).toBe("srp");
137
+ (0, vitest_1.expect)(byId.fld_2).toBe("srp2");
138
+ });
139
+ (0, vitest_1.it)("is stable across repeated generation", () => {
140
+ const db = database([
141
+ {
142
+ id: "tbl_1",
143
+ name: "Notifications",
144
+ fields: [field("fld_1", "Category"), field("fld_2", "category")],
145
+ },
146
+ ]);
147
+ const first = (0, lib_js_1.generateSchema)(db);
148
+ const second = (0, lib_js_1.generateSchema)(db, first);
149
+ const third = (0, lib_js_1.generateSchema)(db, second);
150
+ (0, vitest_1.expect)(second).toEqual(first);
151
+ (0, vitest_1.expect)(third).toEqual(first);
152
+ });
153
+ (0, vitest_1.it)("recomputes a name that is not a valid identifier", () => {
154
+ const schema = (0, lib_js_1.generateSchema)(database([
155
+ { id: "tbl_1", name: "Rows", fields: [field("fld_1", "Time")] },
156
+ ]), {
157
+ tables: [
158
+ {
159
+ id: "tbl_1",
160
+ sdkName: "rows",
161
+ fields: [
162
+ {
163
+ id: "fld_1",
164
+ sdkName: "timeSeconds)",
165
+ definition: { type: "number" },
166
+ },
167
+ ],
168
+ },
169
+ ],
170
+ });
171
+ (0, vitest_1.expect)(schema.tables[0].fields[0].sdkName).toBe("time");
172
+ });
173
+ });
174
+ (0, vitest_1.describe)("normalizeSchemaNames", () => {
175
+ (0, vitest_1.it)("is a no-op on a well-formed schema", () => {
176
+ const schema = {
177
+ tables: [
178
+ {
179
+ id: "tbl_1",
180
+ sdkName: "orders",
181
+ fields: [
182
+ { id: "fld_1", sdkName: "total", definition: { type: "number" } },
183
+ ],
184
+ },
185
+ ],
186
+ };
187
+ const result = (0, sdkNames_js_1.normalizeSchemaNames)(schema);
188
+ (0, vitest_1.expect)(result.renames).toEqual([]);
189
+ (0, vitest_1.expect)(result.schema).toEqual(schema);
190
+ });
191
+ // A schema committed before allocation existed. The sandbox's boot-time
192
+ // `zitejs generate` reads it with no generateSchema pass in front, so this is
193
+ // the only thing standing between it and a TS1117 it cannot build past.
194
+ (0, vitest_1.it)("repairs a committed schema that already holds the collision", () => {
195
+ const source = (0, lib_js_1.generateDbTs)({
196
+ tables: [
197
+ { id: "tbl_1", sdkName: "notifications", fields: [] },
198
+ { id: "tbl_2", sdkName: "orders", fields: [] },
199
+ ],
200
+ });
201
+ const keys = ziteKeys(source);
202
+ (0, vitest_1.expect)(new Set(keys).size).toBe(keys.length);
203
+ (0, vitest_1.expect)(keys).toContain("notifications2");
204
+ });
205
+ });
@@ -0,0 +1,2 @@
1
+ export { createCaller } from '../caller/index.js';
2
+ export type { EndpointConfig } from '../caller/index.js';
@@ -0,0 +1 @@
1
+ export { createCaller } from '../caller/index.js';
package/dist/esm/cli.js CHANGED
File without changes
@@ -0,0 +1,2 @@
1
+ export { createTableClient } from '../runtime/index.js';
2
+ export type { TableClient } from '../runtime/index.js';
@@ -0,0 +1 @@
1
+ export { createTableClient } from '../runtime/index.js';
@@ -99,15 +99,37 @@ export interface TableFindAllOptions<T = Record<string, unknown>> {
99
99
  filter?: unknown;
100
100
  fields?: Array<FieldSelector<T>>;
101
101
  }
102
+ /**
103
+ * Both write results carry the record twice — flat, and again under `fields`.
104
+ *
105
+ * That is not a quirk of the type. Both dispatchers build it deliberately, with
106
+ * the same code and the same comment ("Return both flat fields and nested
107
+ * fields for backwards compatibility"):
108
+ * `cloudflare-lambda/src/integrations/databases.ts` and
109
+ * `workflow-runner/src/sdk/databases.ts`.
110
+ *
111
+ * The types said otherwise, each in a different direction — `BulkCreateResult`
112
+ * declared only the flat form, `UpdateResult` only the nested one. So a 1.0 app
113
+ * reading `created.fields.token`, which is what 1.0's SDK typed, stopped
114
+ * compiling after migrating even though the value was sitting right there at
115
+ * runtime. DSA Employee Portal migrated "successfully" and then would not build.
116
+ *
117
+ * Declaring both is the whole fix: nothing about the running code changes, and
118
+ * no app source has to be rewritten to match a type narrower than what the
119
+ * runtime actually returns.
120
+ */
102
121
  export interface BulkCreateResult<T> {
103
122
  /** Absent when `records: []` was passed — the dispatch short-circuits before setting it. */
104
123
  success?: boolean;
105
- records: T[];
124
+ records: Array<T & {
125
+ fields: T;
126
+ }>;
106
127
  }
107
- export interface UpdateResult<T> {
128
+ /** A type alias, not an interface: an interface cannot extend a generic `Partial<T>`. */
129
+ export type UpdateResult<T> = Partial<T> & {
108
130
  id: string;
109
131
  fields: Partial<T>;
110
- }
132
+ };
111
133
  export interface DeleteResult {
112
134
  success: true;
113
135
  id: string;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { describe, it, expectTypeOf } from "vitest";
2
+ describe("BulkCreateResult", () => {
3
+ // The 1.0 spelling. This is the one that regressed.
4
+ it("reads through .fields", () => {
5
+ expectTypeOf().toEqualTypeOf();
6
+ expectTypeOf().toEqualTypeOf();
7
+ });
8
+ // The 2.0 spelling, which must keep working — apps written since migration
9
+ // use it, so this is not a case of swapping one shape for the other.
10
+ it("reads flat", () => {
11
+ expectTypeOf().toEqualTypeOf();
12
+ expectTypeOf().toEqualTypeOf();
13
+ });
14
+ it("keeps success optional", () => {
15
+ expectTypeOf().toEqualTypeOf();
16
+ });
17
+ });
18
+ describe("UpdateResult", () => {
19
+ // The mirror image: `update` returned `{ ...transformed, fields: transformed }`
20
+ // but the type declared only `id` and `fields`, so reading a column straight
21
+ // off an update result failed to compile.
22
+ it("reads flat and through .fields", () => {
23
+ expectTypeOf().toEqualTypeOf();
24
+ expectTypeOf().toEqualTypeOf();
25
+ expectTypeOf().toEqualTypeOf();
26
+ });
27
+ });
@@ -14,18 +14,7 @@ export type ZiteSchemaTable = {
14
14
  export type ZiteSchema = {
15
15
  tables: ZiteSchemaTable[];
16
16
  };
17
- export declare function toPascalCase(name: string): string;
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;
17
+ export { toPascalCase, toCamelCase, toSdkName } from "./sdkNames.js";
29
18
  /**
30
19
  * Build a ZiteSchema from a Database API response.
31
20
  *
@@ -39,7 +28,7 @@ export declare function generateSchema(database: Database, existingSchema?: Zite
39
28
  * Generate .zite/db.ts purely from ZiteSchema.
40
29
  * Pure function — no external data needed beyond what's in the schema file.
41
30
  */
42
- export declare function generateDbTs(schema: ZiteSchema): string;
31
+ export declare function generateDbTs(inputSchema: ZiteSchema): string;
43
32
  export type EndpointFileInfo = {
44
33
  fileName: string;
45
34
  content?: string;