zitejs 0.9.104 → 0.9.106
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.
- package/dist/cjs/auth/index.js +16 -4
- package/dist/cjs/auth/index.test.js +39 -0
- package/dist/cjs/sync/lib.d.ts +2 -13
- package/dist/cjs/sync/lib.js +62 -65
- package/dist/cjs/sync/sdkNames.d.ts +111 -0
- package/dist/cjs/sync/sdkNames.js +279 -0
- package/dist/cjs/sync/sdkNames.test.d.ts +1 -0
- package/dist/cjs/sync/sdkNames.test.js +205 -0
- package/dist/esm/auth/index.js +16 -4
- package/dist/esm/auth/index.test.js +39 -0
- package/dist/esm/sync/lib.d.ts +2 -13
- package/dist/esm/sync/lib.js +52 -56
- package/dist/esm/sync/sdkNames.d.ts +111 -0
- package/dist/esm/sync/sdkNames.js +269 -0
- package/dist/esm/sync/sdkNames.test.d.ts +1 -0
- package/dist/esm/sync/sdkNames.test.js +203 -0
- package/package.json +1 -1
|
@@ -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
|
+
});
|
package/dist/esm/auth/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAuthClient } from 'better-auth/react';
|
|
2
2
|
import { magicLinkClient, inferAdditionalFields, } from 'better-auth/client/plugins';
|
|
3
|
+
import { USAGE_TOKEN_QUERY_PARAM } from './constants.js';
|
|
3
4
|
const authClient = createAuthClient({
|
|
4
5
|
baseURL: '',
|
|
5
6
|
plugins: [
|
|
@@ -50,10 +51,21 @@ export function loginWithRedirect(opts) {
|
|
|
50
51
|
// the address bar of every logged-out visitor to the front page, which is the
|
|
51
52
|
// common case. Only pass a destination when it IS one.
|
|
52
53
|
const isRoot = target.pathname === '/' && target.search === '' && target.hash === '';
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const params = new URLSearchParams();
|
|
55
|
+
if (!isRoot)
|
|
56
|
+
params.set('redirectUrl', target.toString());
|
|
57
|
+
// The editor preview rides its usageToken on the app URL; the sign-in page
|
|
58
|
+
// only shows its editor surface ("Preview as" picker) when that token reaches
|
|
59
|
+
// it. Capturing the current URL carries it implicitly — but an explicit
|
|
60
|
+
// `redirectUrl` names a bare path, and would silently drop it. Forward it
|
|
61
|
+
// top-level either way, so how the app phrases its redirect can't decide
|
|
62
|
+
// whether the editor gets a preview. Published visitors never have one.
|
|
63
|
+
const usageToken = new URL(window.location.href).searchParams.get(USAGE_TOKEN_QUERY_PARAM) ??
|
|
64
|
+
window._ziteUsageToken;
|
|
65
|
+
if (usageToken)
|
|
66
|
+
params.set(USAGE_TOKEN_QUERY_PARAM, usageToken);
|
|
67
|
+
const query = params.toString();
|
|
68
|
+
window.location.href = query ? `/auth/login?${query}` : '/auth/login';
|
|
57
69
|
}
|
|
58
70
|
export function logout(opts) {
|
|
59
71
|
signOut().then(() => {
|
|
@@ -112,4 +112,43 @@ describe('loginWithRedirect', () => {
|
|
|
112
112
|
expect(params.get('view')).toBeNull();
|
|
113
113
|
expect(params.get('redirectUrl')).toBe('https://app.zite.so/pricing');
|
|
114
114
|
});
|
|
115
|
+
it('forwards the editor usageToken past an explicit redirectUrl', () => {
|
|
116
|
+
// The editor preview rides its token on the app URL. An explicit
|
|
117
|
+
// redirectUrl names a bare path, which used to silently drop it — and with
|
|
118
|
+
// it the sign-in page's whole editor surface ("Preview as" picker).
|
|
119
|
+
const location = stubLocation('https://app.zite.so/?usageToken=tok-123');
|
|
120
|
+
authExports.loginWithRedirect({ redirectUrl: '/dashboard' });
|
|
121
|
+
const params = new URLSearchParams(location.href.split('?')[1] ?? '');
|
|
122
|
+
expect(params.get('usageToken')).toBe('tok-123');
|
|
123
|
+
expect(params.get('redirectUrl')).toBe('https://app.zite.so/dashboard');
|
|
124
|
+
});
|
|
125
|
+
it('forwards the usageToken top-level on a bare call too', () => {
|
|
126
|
+
const location = stubLocation('https://app.zite.so/orders/123?usageToken=tok-123');
|
|
127
|
+
authExports.loginWithRedirect();
|
|
128
|
+
const params = new URLSearchParams(location.href.split('?')[1] ?? '');
|
|
129
|
+
expect(params.get('usageToken')).toBe('tok-123');
|
|
130
|
+
});
|
|
131
|
+
it('carries a token to a root destination that would otherwise say nothing', () => {
|
|
132
|
+
const location = stubLocation('https://app.zite.so/?usageToken=tok-123');
|
|
133
|
+
// The token is the whole search string here only if nothing else rides
|
|
134
|
+
// along — an explicit root override drops the query, exercising the
|
|
135
|
+
// token-only branch.
|
|
136
|
+
authExports.loginWithRedirect({ redirectUrl: '/' });
|
|
137
|
+
expect(location.href).toBe('/auth/login?usageToken=tok-123');
|
|
138
|
+
});
|
|
139
|
+
it('falls back to window._ziteUsageToken when the URL was scrubbed', () => {
|
|
140
|
+
// app-runner's injected boot script moves the token off the address bar
|
|
141
|
+
// into this global before the bundle runs.
|
|
142
|
+
const location = stubLocation('https://app.zite.so/pricing');
|
|
143
|
+
window._ziteUsageToken = 'tok-456';
|
|
144
|
+
authExports.loginWithRedirect({ redirectUrl: '/dashboard' });
|
|
145
|
+
const params = new URLSearchParams(location.href.split('?')[1] ?? '');
|
|
146
|
+
expect(params.get('usageToken')).toBe('tok-456');
|
|
147
|
+
});
|
|
148
|
+
it('adds no token param for ordinary visitors', () => {
|
|
149
|
+
const location = stubLocation('https://app.zite.so/pricing');
|
|
150
|
+
authExports.loginWithRedirect();
|
|
151
|
+
const params = new URLSearchParams(location.href.split('?')[1] ?? '');
|
|
152
|
+
expect(params.get('usageToken')).toBeNull();
|
|
153
|
+
});
|
|
115
154
|
});
|
package/dist/esm/sync/lib.d.ts
CHANGED
|
@@ -14,18 +14,7 @@ export type ZiteSchemaTable = {
|
|
|
14
14
|
export type ZiteSchema = {
|
|
15
15
|
tables: ZiteSchemaTable[];
|
|
16
16
|
};
|
|
17
|
-
export
|
|
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(
|
|
31
|
+
export declare function generateDbTs(inputSchema: ZiteSchema): string;
|
|
43
32
|
export type EndpointFileInfo = {
|
|
44
33
|
fileName: string;
|
|
45
34
|
content?: string;
|