zitejs 0.9.107 → 0.9.109
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/check/index.js +14 -5
- package/dist/cjs/runtime/index.d.ts +8 -5
- package/dist/cjs/runtime/index.js +0 -7
- package/dist/cjs/runtime/writeResultTypes.test-d.js +11 -0
- package/dist/cjs/sync/lib.js +55 -16
- package/dist/cjs/sync/lib.test.js +154 -0
- package/dist/cjs/sync/sdkNames.d.ts +8 -1
- package/dist/cjs/sync/sdkNames.js +8 -5
- package/dist/cjs/sync/sdkNames.test.js +11 -7
- package/dist/esm/check/index.js +15 -6
- package/dist/esm/runtime/index.d.ts +8 -5
- package/dist/esm/runtime/index.js +0 -6
- package/dist/esm/runtime/writeResultTypes.test-d.js +11 -0
- package/dist/esm/sync/lib.js +55 -16
- package/dist/esm/sync/lib.test.js +152 -1
- package/dist/esm/sync/sdkNames.d.ts +8 -1
- package/dist/esm/sync/sdkNames.js +8 -5
- package/dist/esm/sync/sdkNames.test.js +11 -7
- package/package.json +1 -1
package/dist/cjs/check/index.js
CHANGED
|
@@ -12,9 +12,18 @@ function findAppDirs() {
|
|
|
12
12
|
.filter(d => d.isDirectory())
|
|
13
13
|
.map(d => d.name);
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* An argument array, not a command string, so no shell is involved.
|
|
17
|
+
*
|
|
18
|
+
* Every command below takes an app directory name straight off `readdirSync`.
|
|
19
|
+
* Through a shell, a directory named `x;touch PWNED;#` ran the injected command
|
|
20
|
+
* AND still printed `tsc --noEmit ... ✓` — a check that never executed
|
|
21
|
+
* reporting a pass, which is the worse half. Passing argv defeats both, and
|
|
22
|
+
* leaves nothing to escape.
|
|
23
|
+
*/
|
|
24
|
+
function run(file, args, cwd) {
|
|
16
25
|
try {
|
|
17
|
-
const output = (0, child_process_1.
|
|
26
|
+
const output = (0, child_process_1.execFileSync)(file, args, {
|
|
18
27
|
cwd,
|
|
19
28
|
encoding: 'utf-8',
|
|
20
29
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -78,7 +87,7 @@ async function runCheck() {
|
|
|
78
87
|
}
|
|
79
88
|
else {
|
|
80
89
|
process.stdout.write(' tsc --noEmit ... ');
|
|
81
|
-
const tsc = run(
|
|
90
|
+
const tsc = run('npx', ['tsc', '--noEmit', '-p', tsconfigAppPath], '.');
|
|
82
91
|
if (tsc.ok) {
|
|
83
92
|
console.log('✓');
|
|
84
93
|
}
|
|
@@ -93,7 +102,7 @@ async function runCheck() {
|
|
|
93
102
|
// `zitejs/*` alias has no generated file behind it; only the bundler knows.
|
|
94
103
|
if ((0, fs_1.existsSync)((0, path_1.join)(appPath, 'src', 'api'))) {
|
|
95
104
|
process.stdout.write(' bundle endpoints ... ');
|
|
96
|
-
const bundle = run(
|
|
105
|
+
const bundle = run('npx', ['zitejs', 'bundle', '--app', app], '.');
|
|
97
106
|
const failures = bundle.ok ? bundleFailures(bundle.output) : [bundle.output];
|
|
98
107
|
if (failures.length === 0) {
|
|
99
108
|
console.log('✓');
|
|
@@ -107,7 +116,7 @@ async function runCheck() {
|
|
|
107
116
|
const viteConfig = (0, path_1.join)(appPath, 'vite.config.ts');
|
|
108
117
|
if ((0, fs_1.existsSync)(viteConfig)) {
|
|
109
118
|
process.stdout.write(' vite build ... ');
|
|
110
|
-
const vite = run('npx vite build', appPath);
|
|
119
|
+
const vite = run('npx', ['vite', 'build'], appPath);
|
|
111
120
|
if (vite.ok) {
|
|
112
121
|
console.log('✓');
|
|
113
122
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { NotificationsCreateParams, NotificationsCreateResult } from "../notifications/index.js";
|
|
2
1
|
/**
|
|
3
2
|
* A comparison against one field. Mirrors base-runner's operator set
|
|
4
3
|
* (`LegacyWhereConditionOperators`); anything else in the object is ignored.
|
|
@@ -121,8 +120,15 @@ export interface TableFindAllOptions<T = Record<string, unknown>> {
|
|
|
121
120
|
export interface BulkCreateResult<T> {
|
|
122
121
|
/** Absent when `records: []` was passed — the dispatch short-circuits before setting it. */
|
|
123
122
|
success?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* The nested copy has no `id`: both dispatchers build it with
|
|
125
|
+
* `const { id, ...fields } = record`, so `fields` is the record *minus* the
|
|
126
|
+
* id. Typing it `T` (which requires `id`) made the common
|
|
127
|
+
* `{ id: rec.id, ...rec.fields }` idiom a TS2783 — the type promised the
|
|
128
|
+
* spread always overwrites `id` when at runtime it never does.
|
|
129
|
+
*/
|
|
124
130
|
records: Array<T & {
|
|
125
|
-
fields: T
|
|
131
|
+
fields: Omit<T, "id">;
|
|
126
132
|
}>;
|
|
127
133
|
}
|
|
128
134
|
/** A type alias, not an interface: an interface cannot extend a generic `Partial<T>`. */
|
|
@@ -321,9 +327,6 @@ export interface EmailClient {
|
|
|
321
327
|
* app's connected email integration key.
|
|
322
328
|
*/
|
|
323
329
|
export declare function createEmailClient(integrationId: string): EmailClient;
|
|
324
|
-
export declare function createNotificationsClient(): {
|
|
325
|
-
create(params: NotificationsCreateParams): Promise<NotificationsCreateResult>;
|
|
326
|
-
};
|
|
327
330
|
export type { NotificationLink, NotificationsCreateParams, NotificationsCreateResult, } from "../notifications/index.js";
|
|
328
331
|
export { createCaller } from "../caller/index.js";
|
|
329
332
|
export type { EndpointConfig } from "../caller/index.js";
|
|
@@ -6,7 +6,6 @@ exports.createSqlClient = createSqlClient;
|
|
|
6
6
|
exports.createAuthClient = createAuthClient;
|
|
7
7
|
exports.createAirtableClient = createAirtableClient;
|
|
8
8
|
exports.createEmailClient = createEmailClient;
|
|
9
|
-
exports.createNotificationsClient = createNotificationsClient;
|
|
10
9
|
const sdkCall_js_1 = require("../internal/sdkCall.js");
|
|
11
10
|
const DB_INTEGRATION_ID = "databases";
|
|
12
11
|
function getBaseId() {
|
|
@@ -126,11 +125,5 @@ function createEmailClient(integrationId) {
|
|
|
126
125
|
send: (params) => (0, sdkCall_js_1.getSdkCall)()(integrationId, "Email", "send", params),
|
|
127
126
|
};
|
|
128
127
|
}
|
|
129
|
-
const NOTIFICATIONS_SDK_INTEGRATION_ID = "__notifications__";
|
|
130
|
-
function createNotificationsClient() {
|
|
131
|
-
return {
|
|
132
|
-
create: (params) => (0, sdkCall_js_1.getSdkCall)()(NOTIFICATIONS_SDK_INTEGRATION_ID, "ZiteNotifications", "create", params),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
128
|
var index_js_1 = require("../caller/index.js");
|
|
136
129
|
Object.defineProperty(exports, "createCaller", { enumerable: true, get: function () { return index_js_1.createCaller; } });
|
|
@@ -13,6 +13,17 @@ const vitest_1 = require("vitest");
|
|
|
13
13
|
(0, vitest_1.expectTypeOf)().toEqualTypeOf();
|
|
14
14
|
(0, vitest_1.expectTypeOf)().toEqualTypeOf();
|
|
15
15
|
});
|
|
16
|
+
// Both dispatchers destructure `id` out before nesting
|
|
17
|
+
// (`const { id, ...fields } = record`), so `fields` must not claim one.
|
|
18
|
+
(0, vitest_1.it)("omits id from the nested copy", () => {
|
|
19
|
+
(0, vitest_1.expectTypeOf)().toEqualTypeOf();
|
|
20
|
+
});
|
|
21
|
+
// The idiom that regressed under `fields: T` (TS2783, "this spread always
|
|
22
|
+
// overwrites this property"). Compiling at all is the assertion.
|
|
23
|
+
(0, vitest_1.it)("allows the id-then-spread idiom", () => {
|
|
24
|
+
const rebuild = (rec) => ({ id: rec.id, ...rec.fields });
|
|
25
|
+
(0, vitest_1.expectTypeOf)().toEqualTypeOf();
|
|
26
|
+
});
|
|
16
27
|
(0, vitest_1.it)("keeps success optional", () => {
|
|
17
28
|
(0, vitest_1.expectTypeOf)().toEqualTypeOf();
|
|
18
29
|
});
|
package/dist/cjs/sync/lib.js
CHANGED
|
@@ -150,6 +150,31 @@ const ATTACHMENT_TYPES = [
|
|
|
150
150
|
"",
|
|
151
151
|
];
|
|
152
152
|
const MAX_SELECT_OPTIONS = 100;
|
|
153
|
+
/**
|
|
154
|
+
* A select option label as a TypeScript string-literal type.
|
|
155
|
+
*
|
|
156
|
+
* `JSON.stringify` rather than hand-rolled quote escaping: a label is arbitrary
|
|
157
|
+
* user text, and a newline in one produced an unterminated literal that made the
|
|
158
|
+
* whole of `.zite/db.ts` unparseable — which breaks typecheck for every app in
|
|
159
|
+
* the project, not just the one that owns the table. Backslashes and control
|
|
160
|
+
* characters break it the same way. TS literal syntax is a superset of JSON's
|
|
161
|
+
* for strings, so the output is always valid.
|
|
162
|
+
*/
|
|
163
|
+
const tsStringLiteral = (value) => JSON.stringify(value);
|
|
164
|
+
/**
|
|
165
|
+
* The same, single-quoted, for the emitters that write single-quoted source.
|
|
166
|
+
* The escaping still comes from `JSON.stringify` — only the delimiter differs —
|
|
167
|
+
* so newlines, backslashes and control characters stay handled.
|
|
168
|
+
*/
|
|
169
|
+
const tsSingleQuoted = (value) => `'${JSON.stringify(value).slice(1, -1).replace(/'/g, "\\'")}'`;
|
|
170
|
+
/**
|
|
171
|
+
* User-controlled text inside a generated comment. Names reach both a `//` line
|
|
172
|
+
* and a one-line JSDoc, so a newline would end the first and a comment-close
|
|
173
|
+
* sequence would end the second — leaving the remainder to parse as code.
|
|
174
|
+
* Applied where the comment is written rather than at each contributor, so
|
|
175
|
+
* anything added to one later is covered by default.
|
|
176
|
+
*/
|
|
177
|
+
const commentSafe = (value) => value.replace(/\*\//g, "* /").replace(/\s+/g, " ").trim();
|
|
153
178
|
const NUMBER_FORMAT_EXAMPLES = {
|
|
154
179
|
local: "1,000,000.50",
|
|
155
180
|
comma_period: "1,000,000.50",
|
|
@@ -200,7 +225,7 @@ function tsTypeForSchemaField(def, variant = "read") {
|
|
|
200
225
|
if (options && options.length > 0) {
|
|
201
226
|
const literals = options
|
|
202
227
|
.slice(0, MAX_SELECT_OPTIONS)
|
|
203
|
-
.map((o) =>
|
|
228
|
+
.map((o) => tsStringLiteral(o.label))
|
|
204
229
|
.join(" | ");
|
|
205
230
|
const union = `${literals} | string`;
|
|
206
231
|
// No `| null` on the read side — see FIELD_TYPE_MAP. The write side keeps
|
|
@@ -454,7 +479,7 @@ function buildLinkTableComments(schema) {
|
|
|
454
479
|
"// Link tables for zite.sql() JOINs (use these exact names):",
|
|
455
480
|
];
|
|
456
481
|
for (const e of entries) {
|
|
457
|
-
lines.push(`// "${e.name}" — columns: "${e.cols[0]}", "${e.cols[1]}"`);
|
|
482
|
+
lines.push(`// "${commentSafe(e.name)}" — columns: "${commentSafe(e.cols[0])}", "${commentSafe(e.cols[1])}"`);
|
|
458
483
|
}
|
|
459
484
|
return lines;
|
|
460
485
|
}
|
|
@@ -535,7 +560,7 @@ function generateDbTs(inputSchema) {
|
|
|
535
560
|
lines.push("// - SELECT only — use .create/.update/.delete for writes");
|
|
536
561
|
lines.push('// - Load the "zite:sql" skill for full SQL reference (formulas, lookups, etc.)');
|
|
537
562
|
lines.push(...buildLinkTableComments(schema));
|
|
538
|
-
lines.push("import { createTableClient, createSqlClient,
|
|
563
|
+
lines.push("import { createTableClient, createSqlClient, createAuthClient } from 'zitejs/runtime';");
|
|
539
564
|
lines.push("");
|
|
540
565
|
lines.push(...ATTACHMENT_TYPES);
|
|
541
566
|
for (const table of tables) {
|
|
@@ -550,7 +575,7 @@ function generateDbTs(inputSchema) {
|
|
|
550
575
|
const tsType = tsTypeForSchemaField(field.definition);
|
|
551
576
|
const jsdoc = fieldJsdoc(field, table, schema);
|
|
552
577
|
if (jsdoc) {
|
|
553
|
-
lines.push(` /** ${jsdoc} */`);
|
|
578
|
+
lines.push(` /** ${commentSafe(jsdoc)} */`);
|
|
554
579
|
}
|
|
555
580
|
// Optional, matching the pre-monorepo type (`required: ['id']` — "none of
|
|
556
581
|
// these are required to be defined aside from 'id'"). A `fields:`
|
|
@@ -580,7 +605,10 @@ function generateDbTs(inputSchema) {
|
|
|
580
605
|
lines.push(` ${table.sdkName}: createTableClient<${className}RecordType, ${className}RecordInput>('${className}'),`);
|
|
581
606
|
}
|
|
582
607
|
lines.push(` sql: createSqlClient(),`);
|
|
583
|
-
|
|
608
|
+
// No `notifications` here: it is a platform primitive, not something backed
|
|
609
|
+
// by the project database, so it lives at `zitejs/notifications` alongside
|
|
610
|
+
// `zitejs/pdf` and `zitejs/schedules`. Keeping it on `zite` cost a table
|
|
611
|
+
// named "Notifications" its accessor — see RESERVED_TABLE_ACCESSORS.
|
|
584
612
|
lines.push(` auth: createAuthClient<ZiteAuthUser>(),`);
|
|
585
613
|
lines.push("};");
|
|
586
614
|
lines.push("");
|
|
@@ -768,13 +796,13 @@ function generateApiTs(endpointFiles) {
|
|
|
768
796
|
if (skipped.length > 0) {
|
|
769
797
|
lines.push("// Not endpoints, so no callers were generated for them:");
|
|
770
798
|
for (const name of skipped)
|
|
771
|
-
lines.push(`// ${name}`);
|
|
799
|
+
lines.push(`// ${commentSafe(name)}`);
|
|
772
800
|
lines.push("");
|
|
773
801
|
}
|
|
774
802
|
for (const { pascal, baseName, typed } of endpoints) {
|
|
775
803
|
if (!typed)
|
|
776
804
|
continue;
|
|
777
|
-
lines.push(`import type { default as _${pascal}Ep } from
|
|
805
|
+
lines.push(`import type { default as _${pascal}Ep } from ${tsSingleQuoted(`../src/api/${baseName}`)};`);
|
|
778
806
|
}
|
|
779
807
|
lines.push("");
|
|
780
808
|
for (const { baseName, ident, pascal, stream, typed } of endpoints) {
|
|
@@ -782,10 +810,10 @@ function generateApiTs(endpointFiles) {
|
|
|
782
810
|
// Declared with `export const x = createEndpoint(...)` — the 1.0 shape.
|
|
783
811
|
// The route is real and the bundler deploys it, so the caller has to
|
|
784
812
|
// exist; there is just no default export to read its types from.
|
|
785
|
-
lines.push(`// '${baseName}' has no default export, so its input/output are untyped.`);
|
|
813
|
+
lines.push(`// ${commentSafe(`'${baseName}'`)} has no default export, so its input/output are untyped.`);
|
|
786
814
|
lines.push(`export type ${pascal}InputType = unknown;`);
|
|
787
815
|
lines.push(`export type ${pascal}OutputType = unknown;`);
|
|
788
|
-
lines.push(`export const ${ident} = ${stream ? "createStreamingCaller" : "createCaller"}<${pascal}InputType, ${pascal}OutputType>(
|
|
816
|
+
lines.push(`export const ${ident} = ${stream ? "createStreamingCaller" : "createCaller"}<${pascal}InputType, ${pascal}OutputType>(${tsSingleQuoted(baseName)});`);
|
|
789
817
|
lines.push("");
|
|
790
818
|
continue;
|
|
791
819
|
}
|
|
@@ -804,7 +832,7 @@ function generateApiTs(endpointFiles) {
|
|
|
804
832
|
// Calling it `sendEmail` here made every hyphenated or snake_cased endpoint
|
|
805
833
|
// a 404.
|
|
806
834
|
const caller = stream ? "createStreamingCaller" : "createCaller";
|
|
807
|
-
lines.push(`export const ${ident} = ${caller}<${pascal}InputType, ${pascal}OutputType>(
|
|
835
|
+
lines.push(`export const ${ident} = ${caller}<${pascal}InputType, ${pascal}OutputType>(${tsSingleQuoted(baseName)});`);
|
|
808
836
|
lines.push("");
|
|
809
837
|
}
|
|
810
838
|
lines.push("");
|
|
@@ -874,7 +902,14 @@ const AIRTABLE_FIELD_INPUT_TYPE_MAP = {
|
|
|
874
902
|
singleCollaborator: "{ id: string } | { email: string }",
|
|
875
903
|
multipleCollaborators: "Array<{ id: string } | { email: string }>",
|
|
876
904
|
};
|
|
877
|
-
/**
|
|
905
|
+
/**
|
|
906
|
+
* Airtable's attachment object as the REST API really returns it.
|
|
907
|
+
*
|
|
908
|
+
* Not quite `airtable/lib/attachment.d.ts`: that declaration omits `width` and
|
|
909
|
+
* `height`, which the API does return for image attachments. 1.0 exposed them
|
|
910
|
+
* and apps read them, so leaving them out turns a working `photo.width` into a
|
|
911
|
+
* migration type error. Optional, because a non-image attachment has neither.
|
|
912
|
+
*/
|
|
878
913
|
const AIRTABLE_ATTACHMENT_TYPE = [
|
|
879
914
|
"export type AirtableAttachment = {",
|
|
880
915
|
" id: string;",
|
|
@@ -884,6 +919,10 @@ const AIRTABLE_ATTACHMENT_TYPE = [
|
|
|
884
919
|
" size: number;",
|
|
885
920
|
" /** MIME type. */",
|
|
886
921
|
" type: string;",
|
|
922
|
+
" /** Pixel width — images only. */",
|
|
923
|
+
" width?: number;",
|
|
924
|
+
" /** Pixel height — images only. */",
|
|
925
|
+
" height?: number;",
|
|
887
926
|
" thumbnails?: {",
|
|
888
927
|
" small: { url: string; width: number; height: number };",
|
|
889
928
|
" large: { url: string; width: number; height: number };",
|
|
@@ -912,7 +951,7 @@ function airtableTsType(field, lock, depth, variant = "read") {
|
|
|
912
951
|
choices.length > 0) {
|
|
913
952
|
const literals = choices
|
|
914
953
|
.slice(0, MAX_SELECT_OPTIONS)
|
|
915
|
-
.map((o) =>
|
|
954
|
+
.map((o) => tsStringLiteral(o))
|
|
916
955
|
.join(" | ");
|
|
917
956
|
const union = `${literals} | string`;
|
|
918
957
|
if (field.type === "multipleSelects")
|
|
@@ -1058,7 +1097,7 @@ function generateAirtableTs(lock) {
|
|
|
1058
1097
|
lines.push(...AIRTABLE_ATTACHMENT_TYPE);
|
|
1059
1098
|
for (const table of lock.tables) {
|
|
1060
1099
|
const recordType = `${table.sdkName}RecordType`;
|
|
1061
|
-
lines.push(`/** A ${table.sdkName} record as it is read back. */`);
|
|
1100
|
+
lines.push(`/** A ${commentSafe(table.sdkName)} record as it is read back. */`);
|
|
1062
1101
|
lines.push(`export type ${recordType} = {`);
|
|
1063
1102
|
lines.push(" id: string;");
|
|
1064
1103
|
for (const field of table.fields) {
|
|
@@ -1066,7 +1105,7 @@ function generateAirtableTs(lock) {
|
|
|
1066
1105
|
continue;
|
|
1067
1106
|
const jsdoc = airtableFieldJsdoc(field, table, lock);
|
|
1068
1107
|
if (jsdoc) {
|
|
1069
|
-
lines.push(` /** ${jsdoc} */`);
|
|
1108
|
+
lines.push(` /** ${commentSafe(jsdoc)} */`);
|
|
1070
1109
|
}
|
|
1071
1110
|
const tsType = airtableTsType(field, lock);
|
|
1072
1111
|
// Optional, because Airtable omits a field from the response entirely
|
|
@@ -1080,7 +1119,7 @@ function generateAirtableTs(lock) {
|
|
|
1080
1119
|
// Read-only fields are omitted rather than typed: Airtable rejects a write
|
|
1081
1120
|
// to a formula, rollup, lookup or autonumber with a 422.
|
|
1082
1121
|
const writableFields = table.fields.filter((f) => f.sdkName !== "id" && !READ_ONLY_AIRTABLE_FIELDS.has(f.type));
|
|
1083
|
-
lines.push(`/** What you may write when creating or updating a ${table.sdkName}. */`);
|
|
1122
|
+
lines.push(`/** What you may write when creating or updating a ${commentSafe(table.sdkName)}. */`);
|
|
1084
1123
|
lines.push(`export type ${table.sdkName}RecordInput = {`);
|
|
1085
1124
|
for (const field of writableFields) {
|
|
1086
1125
|
lines.push(` ${field.sdkName}: ${airtableTsType(field, lock, 0, "write")};`);
|
|
@@ -1246,7 +1285,7 @@ function generateEmailSdk(integrationId) {
|
|
|
1246
1285
|
" SendEmailResult,",
|
|
1247
1286
|
"} from 'zitejs/runtime';",
|
|
1248
1287
|
"",
|
|
1249
|
-
`export const Email = createEmailClient(
|
|
1288
|
+
`export const Email = createEmailClient(${tsSingleQuoted(integrationId)});`,
|
|
1250
1289
|
"",
|
|
1251
1290
|
].join("\n");
|
|
1252
1291
|
}
|
|
@@ -1,7 +1,161 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const vitest_1 = require("vitest");
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
4
8
|
const lib_js_1 = require("./lib.js");
|
|
9
|
+
/**
|
|
10
|
+
* Syntax errors in the emitted source. `.zite/db.ts` sits at the repo root and
|
|
11
|
+
* every app imports it, so anything unparseable here fails typecheck for the
|
|
12
|
+
* whole project — and the file is generated, so no one can edit their way out.
|
|
13
|
+
*/
|
|
14
|
+
const syntaxErrorsIn = (source) => (typescript_1.default.transpileModule(source, {
|
|
15
|
+
reportDiagnostics: true,
|
|
16
|
+
compilerOptions: { target: typescript_1.default.ScriptTarget.Latest },
|
|
17
|
+
}).diagnostics ?? []).map(d => typescript_1.default.flattenDiagnosticMessageText(d.messageText, ' '));
|
|
18
|
+
const schemaWithSelectOption = (label) => ({
|
|
19
|
+
tables: [
|
|
20
|
+
{
|
|
21
|
+
id: 'tbl1',
|
|
22
|
+
sdkName: 'registrations',
|
|
23
|
+
fields: [
|
|
24
|
+
{
|
|
25
|
+
id: 'fld1',
|
|
26
|
+
sdkName: 'ticketType',
|
|
27
|
+
definition: {
|
|
28
|
+
type: 'single_select',
|
|
29
|
+
template: { options: [{ label }] },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
(0, vitest_1.describe)('generateDbTs select option literals', () => {
|
|
37
|
+
// Reported from a migrated app: a consent paragraph pasted in as an option
|
|
38
|
+
// label carried a newline, so the emitted literal never closed and the whole
|
|
39
|
+
// generated file failed with TS1002 — taking every sibling app with it.
|
|
40
|
+
vitest_1.it.each([
|
|
41
|
+
['a newline', 'I am 18 or above.\nI agree to the rules.'],
|
|
42
|
+
['a carriage return', 'Lite Pass\r\nRegular Pass'],
|
|
43
|
+
['a double quote', 'The "Premium" tier'],
|
|
44
|
+
['a trailing backslash', 'Group / Crew Purchase\\'],
|
|
45
|
+
['a tab', 'Lite\tPass'],
|
|
46
|
+
])('emits parseable TypeScript for a label containing %s', (_what, label) => {
|
|
47
|
+
(0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateDbTs)(schemaWithSelectOption(label)))).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.it)('keeps the option readable in the union', () => {
|
|
50
|
+
const out = (0, lib_js_1.generateDbTs)(schemaWithSelectOption('The "Premium" tier'));
|
|
51
|
+
(0, vitest_1.expect)(out).toContain('"The \\"Premium\\" tier"');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
(0, vitest_1.describe)('generateAirtableTs attachment type', () => {
|
|
55
|
+
const lockWithAttachment = {
|
|
56
|
+
integrationId: 'airtable',
|
|
57
|
+
tables: [
|
|
58
|
+
{
|
|
59
|
+
id: 'tbl1',
|
|
60
|
+
sdkName: 'Speakers',
|
|
61
|
+
primaryFieldId: 'fld1',
|
|
62
|
+
fields: [
|
|
63
|
+
{ id: 'fld1', sdkName: 'name', type: 'singleLineText', name: 'Name' },
|
|
64
|
+
{
|
|
65
|
+
id: 'fld2',
|
|
66
|
+
sdkName: 'headshot',
|
|
67
|
+
type: 'multipleAttachments',
|
|
68
|
+
name: 'Headshot',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
// Reported from a migrated app reading `photo.width` to lay out an image
|
|
75
|
+
// grid. Airtable returns width/height on image attachments and 1.0 exposed
|
|
76
|
+
// them; `airtable/lib/attachment.d.ts`, which this type was copied from,
|
|
77
|
+
// omits both — so the read became a TS2339 the app could not edit away.
|
|
78
|
+
vitest_1.it.each(['width', 'height'])('exposes %s, which Airtable returns for images', prop => {
|
|
79
|
+
const out = (0, lib_js_1.generateAirtableTs)(lockWithAttachment);
|
|
80
|
+
(0, vitest_1.expect)(out).not.toBeNull();
|
|
81
|
+
(0, vitest_1.expect)(out).toContain(` ${prop}?: number;`);
|
|
82
|
+
});
|
|
83
|
+
(0, vitest_1.it)('emits parseable TypeScript', () => {
|
|
84
|
+
(0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateAirtableTs)(lockWithAttachment) ?? '')).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
(0, vitest_1.describe)('generateDbTs field names in comments', () => {
|
|
88
|
+
// A field's display name is user text and reaches a one-line JSDoc. A
|
|
89
|
+
// comment-close sequence in it would end the comment early and leave the
|
|
90
|
+
// remainder to parse as code.
|
|
91
|
+
const schemaWithFieldNamed = (name) => ({
|
|
92
|
+
tables: [
|
|
93
|
+
{
|
|
94
|
+
id: 'tbl1',
|
|
95
|
+
sdkName: 'orders',
|
|
96
|
+
primaryFieldId: 'fld1',
|
|
97
|
+
fields: [
|
|
98
|
+
{
|
|
99
|
+
id: 'fld1',
|
|
100
|
+
sdkName: 'amount',
|
|
101
|
+
definition: {
|
|
102
|
+
type: 'currency',
|
|
103
|
+
name,
|
|
104
|
+
template: { currencySymbol: '$' },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
vitest_1.it.each([
|
|
112
|
+
['a comment-close sequence', 'Total */ console.log(1); /*'],
|
|
113
|
+
['a newline', 'Total\namount'],
|
|
114
|
+
])('emits parseable TypeScript for a field named with %s', (_what, name) => {
|
|
115
|
+
(0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateDbTs)(schemaWithFieldNamed(name)))).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
(0, vitest_1.describe)('generateApiTs endpoint file names', () => {
|
|
119
|
+
// An endpoint filename is the LLM's raw `writeFile` path argument — nothing
|
|
120
|
+
// validates its characters — and it lands in an import specifier, a comment
|
|
121
|
+
// and two string literals. `.zite/api.ts` is imported by every component that
|
|
122
|
+
// calls `api.*`, so one bad name takes the whole app down.
|
|
123
|
+
vitest_1.it.each([
|
|
124
|
+
['an apostrophe', "it's.ts"],
|
|
125
|
+
['a newline', 'ok\nreport.ts'],
|
|
126
|
+
['a trailing backslash', 'back\\.ts'],
|
|
127
|
+
['a double quote', 'say"hi.ts'],
|
|
128
|
+
])('emits parseable TypeScript for a file named with %s', (_what, name) => {
|
|
129
|
+
const out = (0, lib_js_1.generateApiTs)([
|
|
130
|
+
{ fileName: name, content: 'export default createEndpoint({});' },
|
|
131
|
+
]);
|
|
132
|
+
(0, vitest_1.expect)(out).not.toBeNull();
|
|
133
|
+
(0, vitest_1.expect)(syntaxErrorsIn(out)).toEqual([]);
|
|
134
|
+
});
|
|
135
|
+
// The no-default-export branch puts the name in a `//` comment instead, so it
|
|
136
|
+
// needs its own case: there a line terminator ends the comment, not a string.
|
|
137
|
+
(0, vitest_1.it)('emits parseable TypeScript for an untyped endpoint with a newline', () => {
|
|
138
|
+
const out = (0, lib_js_1.generateApiTs)([
|
|
139
|
+
{
|
|
140
|
+
fileName: 'ok\nreport.ts',
|
|
141
|
+
content: 'export const report = createEndpoint({});',
|
|
142
|
+
},
|
|
143
|
+
]);
|
|
144
|
+
(0, vitest_1.expect)(out).not.toBeNull();
|
|
145
|
+
(0, vitest_1.expect)(syntaxErrorsIn(out)).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
(0, vitest_1.describe)('generateEmailSdk integration id', () => {
|
|
149
|
+
// An LLM-authored `zite.config.json` key, validated only as
|
|
150
|
+
// `z.record(z.string(), …)`.
|
|
151
|
+
vitest_1.it.each([
|
|
152
|
+
['an apostrophe', "resend'prod"],
|
|
153
|
+
['a newline', 'resend\nprod'],
|
|
154
|
+
['a trailing backslash', 'resend\\'],
|
|
155
|
+
])('emits parseable TypeScript for an id with %s', (_what, id) => {
|
|
156
|
+
(0, vitest_1.expect)(syntaxErrorsIn((0, lib_js_1.generateEmailSdk)(id))).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
5
159
|
(0, vitest_1.describe)('generateBackendWrapperTs', () => {
|
|
6
160
|
const output = (0, lib_js_1.generateBackendWrapperTs)();
|
|
7
161
|
(0, vitest_1.it)('imports User from zitejs/auth', () => {
|
|
@@ -36,8 +36,15 @@ export declare function toSdkName(name: string): string;
|
|
|
36
36
|
/**
|
|
37
37
|
* Accessors `generateDbTs` puts on the `zite` object itself, after the tables.
|
|
38
38
|
* A table that allocates one of these produces a duplicate key.
|
|
39
|
+
*
|
|
40
|
+
* `notifications` used to be here and no longer is: it was a platform
|
|
41
|
+
* primitive hanging off `zite`, so a table named "Notifications" collided with
|
|
42
|
+
* it and — because the sentinel was written last — lost its client entirely.
|
|
43
|
+
* Moving it to `zitejs/notifications` removes the collision rather than
|
|
44
|
+
* managing it, and frees the name for the table that wants it. Everything left
|
|
45
|
+
* is genuinely backed by the project database.
|
|
39
46
|
*/
|
|
40
|
-
export declare const RESERVED_TABLE_ACCESSORS: readonly ["sql", "
|
|
47
|
+
export declare const RESERVED_TABLE_ACCESSORS: readonly ["sql", "auth"];
|
|
41
48
|
/**
|
|
42
49
|
* Never safe as a generated key, in any namespace. Assigning `__proto__` in an
|
|
43
50
|
* object literal sets the prototype rather than defining a property, and
|
|
@@ -66,12 +66,15 @@ function toSdkName(name) {
|
|
|
66
66
|
/**
|
|
67
67
|
* Accessors `generateDbTs` puts on the `zite` object itself, after the tables.
|
|
68
68
|
* A table that allocates one of these produces a duplicate key.
|
|
69
|
+
*
|
|
70
|
+
* `notifications` used to be here and no longer is: it was a platform
|
|
71
|
+
* primitive hanging off `zite`, so a table named "Notifications" collided with
|
|
72
|
+
* it and — because the sentinel was written last — lost its client entirely.
|
|
73
|
+
* Moving it to `zitejs/notifications` removes the collision rather than
|
|
74
|
+
* managing it, and frees the name for the table that wants it. Everything left
|
|
75
|
+
* is genuinely backed by the project database.
|
|
69
76
|
*/
|
|
70
|
-
exports.RESERVED_TABLE_ACCESSORS = [
|
|
71
|
-
"sql",
|
|
72
|
-
"notifications",
|
|
73
|
-
"auth",
|
|
74
|
-
];
|
|
77
|
+
exports.RESERVED_TABLE_ACCESSORS = ["sql", "auth"];
|
|
75
78
|
/**
|
|
76
79
|
* Never safe as a generated key, in any namespace. Assigning `__proto__` in an
|
|
77
80
|
* object literal sets the prototype rather than defining a property, and
|
|
@@ -33,17 +33,18 @@ const recordTypeKeys = (source, typeName) => {
|
|
|
33
33
|
};
|
|
34
34
|
(0, vitest_1.describe)("reserved platform accessors", () => {
|
|
35
35
|
// Sudzy Dashboard: a table named "Notifications" emitted a second
|
|
36
|
-
// `notifications:` key
|
|
36
|
+
// `notifications:` key and the platform client — written last — won, so the
|
|
37
37
|
// table lost findAll and typed as `{ create(NotificationsCreateParams) }`.
|
|
38
|
-
|
|
38
|
+
// The platform client has since moved to `zitejs/notifications`, so the name
|
|
39
|
+
// belongs to the table again and there is nothing left to collide with.
|
|
40
|
+
(0, vitest_1.it)("lets a table keep the notifications accessor", () => {
|
|
39
41
|
const schema = (0, lib_js_1.generateSchema)(database([{ id: "tbl_1", name: "Notifications", fields: [] }]));
|
|
40
|
-
(0, vitest_1.expect)(schema.tables[0].sdkName).toBe("
|
|
42
|
+
(0, vitest_1.expect)(schema.tables[0].sdkName).toBe("notifications");
|
|
41
43
|
const keys = ziteKeys((0, lib_js_1.generateDbTs)(schema));
|
|
42
|
-
(0, vitest_1.expect)(keys).toContain("notifications2");
|
|
43
44
|
(0, vitest_1.expect)(keys.filter((k) => k === "notifications")).toHaveLength(1);
|
|
44
45
|
(0, vitest_1.expect)(new Set(keys).size).toBe(keys.length);
|
|
45
46
|
});
|
|
46
|
-
vitest_1.it.each(["sql", "auth"
|
|
47
|
+
vitest_1.it.each(["sql", "auth"])("keeps the platform %s accessor for the platform", (reserved) => {
|
|
47
48
|
const schema = (0, lib_js_1.generateSchema)(database([{ id: "tbl_1", name: reserved, fields: [] }]));
|
|
48
49
|
(0, vitest_1.expect)(schema.tables[0].sdkName).toBe(`${reserved}2`);
|
|
49
50
|
const keys = ziteKeys((0, lib_js_1.generateDbTs)(schema));
|
|
@@ -192,14 +193,17 @@ const recordTypeKeys = (source, typeName) => {
|
|
|
192
193
|
// `zitejs generate` reads it with no generateSchema pass in front, so this is
|
|
193
194
|
// the only thing standing between it and a TS1117 it cannot build past.
|
|
194
195
|
(0, vitest_1.it)("repairs a committed schema that already holds the collision", () => {
|
|
196
|
+
// `sql`, not `notifications` — the latter stopped being a platform
|
|
197
|
+
// accessor when the client moved to `zitejs/notifications`, so a schema
|
|
198
|
+
// holding it is no longer a collision to repair.
|
|
195
199
|
const source = (0, lib_js_1.generateDbTs)({
|
|
196
200
|
tables: [
|
|
197
|
-
{ id: "tbl_1", sdkName: "
|
|
201
|
+
{ id: "tbl_1", sdkName: "sql", fields: [] },
|
|
198
202
|
{ id: "tbl_2", sdkName: "orders", fields: [] },
|
|
199
203
|
],
|
|
200
204
|
});
|
|
201
205
|
const keys = ziteKeys(source);
|
|
202
206
|
(0, vitest_1.expect)(new Set(keys).size).toBe(keys.length);
|
|
203
|
-
(0, vitest_1.expect)(keys).toContain("
|
|
207
|
+
(0, vitest_1.expect)(keys).toContain("sql2");
|
|
204
208
|
});
|
|
205
209
|
});
|
package/dist/esm/check/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
2
|
import { existsSync, readdirSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
function findAppDirs() {
|
|
@@ -9,9 +9,18 @@ function findAppDirs() {
|
|
|
9
9
|
.filter(d => d.isDirectory())
|
|
10
10
|
.map(d => d.name);
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* An argument array, not a command string, so no shell is involved.
|
|
14
|
+
*
|
|
15
|
+
* Every command below takes an app directory name straight off `readdirSync`.
|
|
16
|
+
* Through a shell, a directory named `x;touch PWNED;#` ran the injected command
|
|
17
|
+
* AND still printed `tsc --noEmit ... ✓` — a check that never executed
|
|
18
|
+
* reporting a pass, which is the worse half. Passing argv defeats both, and
|
|
19
|
+
* leaves nothing to escape.
|
|
20
|
+
*/
|
|
21
|
+
function run(file, args, cwd) {
|
|
13
22
|
try {
|
|
14
|
-
const output =
|
|
23
|
+
const output = execFileSync(file, args, {
|
|
15
24
|
cwd,
|
|
16
25
|
encoding: 'utf-8',
|
|
17
26
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -75,7 +84,7 @@ export async function runCheck() {
|
|
|
75
84
|
}
|
|
76
85
|
else {
|
|
77
86
|
process.stdout.write(' tsc --noEmit ... ');
|
|
78
|
-
const tsc = run(
|
|
87
|
+
const tsc = run('npx', ['tsc', '--noEmit', '-p', tsconfigAppPath], '.');
|
|
79
88
|
if (tsc.ok) {
|
|
80
89
|
console.log('✓');
|
|
81
90
|
}
|
|
@@ -90,7 +99,7 @@ export async function runCheck() {
|
|
|
90
99
|
// `zitejs/*` alias has no generated file behind it; only the bundler knows.
|
|
91
100
|
if (existsSync(join(appPath, 'src', 'api'))) {
|
|
92
101
|
process.stdout.write(' bundle endpoints ... ');
|
|
93
|
-
const bundle = run(
|
|
102
|
+
const bundle = run('npx', ['zitejs', 'bundle', '--app', app], '.');
|
|
94
103
|
const failures = bundle.ok ? bundleFailures(bundle.output) : [bundle.output];
|
|
95
104
|
if (failures.length === 0) {
|
|
96
105
|
console.log('✓');
|
|
@@ -104,7 +113,7 @@ export async function runCheck() {
|
|
|
104
113
|
const viteConfig = join(appPath, 'vite.config.ts');
|
|
105
114
|
if (existsSync(viteConfig)) {
|
|
106
115
|
process.stdout.write(' vite build ... ');
|
|
107
|
-
const vite = run('npx vite build', appPath);
|
|
116
|
+
const vite = run('npx', ['vite', 'build'], appPath);
|
|
108
117
|
if (vite.ok) {
|
|
109
118
|
console.log('✓');
|
|
110
119
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { NotificationsCreateParams, NotificationsCreateResult } from "../notifications/index.js";
|
|
2
1
|
/**
|
|
3
2
|
* A comparison against one field. Mirrors base-runner's operator set
|
|
4
3
|
* (`LegacyWhereConditionOperators`); anything else in the object is ignored.
|
|
@@ -121,8 +120,15 @@ export interface TableFindAllOptions<T = Record<string, unknown>> {
|
|
|
121
120
|
export interface BulkCreateResult<T> {
|
|
122
121
|
/** Absent when `records: []` was passed — the dispatch short-circuits before setting it. */
|
|
123
122
|
success?: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* The nested copy has no `id`: both dispatchers build it with
|
|
125
|
+
* `const { id, ...fields } = record`, so `fields` is the record *minus* the
|
|
126
|
+
* id. Typing it `T` (which requires `id`) made the common
|
|
127
|
+
* `{ id: rec.id, ...rec.fields }` idiom a TS2783 — the type promised the
|
|
128
|
+
* spread always overwrites `id` when at runtime it never does.
|
|
129
|
+
*/
|
|
124
130
|
records: Array<T & {
|
|
125
|
-
fields: T
|
|
131
|
+
fields: Omit<T, "id">;
|
|
126
132
|
}>;
|
|
127
133
|
}
|
|
128
134
|
/** A type alias, not an interface: an interface cannot extend a generic `Partial<T>`. */
|
|
@@ -321,9 +327,6 @@ export interface EmailClient {
|
|
|
321
327
|
* app's connected email integration key.
|
|
322
328
|
*/
|
|
323
329
|
export declare function createEmailClient(integrationId: string): EmailClient;
|
|
324
|
-
export declare function createNotificationsClient(): {
|
|
325
|
-
create(params: NotificationsCreateParams): Promise<NotificationsCreateResult>;
|
|
326
|
-
};
|
|
327
330
|
export type { NotificationLink, NotificationsCreateParams, NotificationsCreateResult, } from "../notifications/index.js";
|
|
328
331
|
export { createCaller } from "../caller/index.js";
|
|
329
332
|
export type { EndpointConfig } from "../caller/index.js";
|
|
@@ -117,10 +117,4 @@ export function createEmailClient(integrationId) {
|
|
|
117
117
|
send: (params) => getSdkCall()(integrationId, "Email", "send", params),
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
-
const NOTIFICATIONS_SDK_INTEGRATION_ID = "__notifications__";
|
|
121
|
-
export function createNotificationsClient() {
|
|
122
|
-
return {
|
|
123
|
-
create: (params) => getSdkCall()(NOTIFICATIONS_SDK_INTEGRATION_ID, "ZiteNotifications", "create", params),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
120
|
export { createCaller } from "../caller/index.js";
|
|
@@ -11,6 +11,17 @@ describe("BulkCreateResult", () => {
|
|
|
11
11
|
expectTypeOf().toEqualTypeOf();
|
|
12
12
|
expectTypeOf().toEqualTypeOf();
|
|
13
13
|
});
|
|
14
|
+
// Both dispatchers destructure `id` out before nesting
|
|
15
|
+
// (`const { id, ...fields } = record`), so `fields` must not claim one.
|
|
16
|
+
it("omits id from the nested copy", () => {
|
|
17
|
+
expectTypeOf().toEqualTypeOf();
|
|
18
|
+
});
|
|
19
|
+
// The idiom that regressed under `fields: T` (TS2783, "this spread always
|
|
20
|
+
// overwrites this property"). Compiling at all is the assertion.
|
|
21
|
+
it("allows the id-then-spread idiom", () => {
|
|
22
|
+
const rebuild = (rec) => ({ id: rec.id, ...rec.fields });
|
|
23
|
+
expectTypeOf().toEqualTypeOf();
|
|
24
|
+
});
|
|
14
25
|
it("keeps success optional", () => {
|
|
15
26
|
expectTypeOf().toEqualTypeOf();
|
|
16
27
|
});
|
package/dist/esm/sync/lib.js
CHANGED
|
@@ -141,6 +141,31 @@ const ATTACHMENT_TYPES = [
|
|
|
141
141
|
"",
|
|
142
142
|
];
|
|
143
143
|
const MAX_SELECT_OPTIONS = 100;
|
|
144
|
+
/**
|
|
145
|
+
* A select option label as a TypeScript string-literal type.
|
|
146
|
+
*
|
|
147
|
+
* `JSON.stringify` rather than hand-rolled quote escaping: a label is arbitrary
|
|
148
|
+
* user text, and a newline in one produced an unterminated literal that made the
|
|
149
|
+
* whole of `.zite/db.ts` unparseable — which breaks typecheck for every app in
|
|
150
|
+
* the project, not just the one that owns the table. Backslashes and control
|
|
151
|
+
* characters break it the same way. TS literal syntax is a superset of JSON's
|
|
152
|
+
* for strings, so the output is always valid.
|
|
153
|
+
*/
|
|
154
|
+
const tsStringLiteral = (value) => JSON.stringify(value);
|
|
155
|
+
/**
|
|
156
|
+
* The same, single-quoted, for the emitters that write single-quoted source.
|
|
157
|
+
* The escaping still comes from `JSON.stringify` — only the delimiter differs —
|
|
158
|
+
* so newlines, backslashes and control characters stay handled.
|
|
159
|
+
*/
|
|
160
|
+
const tsSingleQuoted = (value) => `'${JSON.stringify(value).slice(1, -1).replace(/'/g, "\\'")}'`;
|
|
161
|
+
/**
|
|
162
|
+
* User-controlled text inside a generated comment. Names reach both a `//` line
|
|
163
|
+
* and a one-line JSDoc, so a newline would end the first and a comment-close
|
|
164
|
+
* sequence would end the second — leaving the remainder to parse as code.
|
|
165
|
+
* Applied where the comment is written rather than at each contributor, so
|
|
166
|
+
* anything added to one later is covered by default.
|
|
167
|
+
*/
|
|
168
|
+
const commentSafe = (value) => value.replace(/\*\//g, "* /").replace(/\s+/g, " ").trim();
|
|
144
169
|
const NUMBER_FORMAT_EXAMPLES = {
|
|
145
170
|
local: "1,000,000.50",
|
|
146
171
|
comma_period: "1,000,000.50",
|
|
@@ -188,7 +213,7 @@ function tsTypeForSchemaField(def, variant = "read") {
|
|
|
188
213
|
if (options && options.length > 0) {
|
|
189
214
|
const literals = options
|
|
190
215
|
.slice(0, MAX_SELECT_OPTIONS)
|
|
191
|
-
.map((o) =>
|
|
216
|
+
.map((o) => tsStringLiteral(o.label))
|
|
192
217
|
.join(" | ");
|
|
193
218
|
const union = `${literals} | string`;
|
|
194
219
|
// No `| null` on the read side — see FIELD_TYPE_MAP. The write side keeps
|
|
@@ -442,7 +467,7 @@ function buildLinkTableComments(schema) {
|
|
|
442
467
|
"// Link tables for zite.sql() JOINs (use these exact names):",
|
|
443
468
|
];
|
|
444
469
|
for (const e of entries) {
|
|
445
|
-
lines.push(`// "${e.name}" — columns: "${e.cols[0]}", "${e.cols[1]}"`);
|
|
470
|
+
lines.push(`// "${commentSafe(e.name)}" — columns: "${commentSafe(e.cols[0])}", "${commentSafe(e.cols[1])}"`);
|
|
446
471
|
}
|
|
447
472
|
return lines;
|
|
448
473
|
}
|
|
@@ -523,7 +548,7 @@ export function generateDbTs(inputSchema) {
|
|
|
523
548
|
lines.push("// - SELECT only — use .create/.update/.delete for writes");
|
|
524
549
|
lines.push('// - Load the "zite:sql" skill for full SQL reference (formulas, lookups, etc.)');
|
|
525
550
|
lines.push(...buildLinkTableComments(schema));
|
|
526
|
-
lines.push("import { createTableClient, createSqlClient,
|
|
551
|
+
lines.push("import { createTableClient, createSqlClient, createAuthClient } from 'zitejs/runtime';");
|
|
527
552
|
lines.push("");
|
|
528
553
|
lines.push(...ATTACHMENT_TYPES);
|
|
529
554
|
for (const table of tables) {
|
|
@@ -538,7 +563,7 @@ export function generateDbTs(inputSchema) {
|
|
|
538
563
|
const tsType = tsTypeForSchemaField(field.definition);
|
|
539
564
|
const jsdoc = fieldJsdoc(field, table, schema);
|
|
540
565
|
if (jsdoc) {
|
|
541
|
-
lines.push(` /** ${jsdoc} */`);
|
|
566
|
+
lines.push(` /** ${commentSafe(jsdoc)} */`);
|
|
542
567
|
}
|
|
543
568
|
// Optional, matching the pre-monorepo type (`required: ['id']` — "none of
|
|
544
569
|
// these are required to be defined aside from 'id'"). A `fields:`
|
|
@@ -568,7 +593,10 @@ export function generateDbTs(inputSchema) {
|
|
|
568
593
|
lines.push(` ${table.sdkName}: createTableClient<${className}RecordType, ${className}RecordInput>('${className}'),`);
|
|
569
594
|
}
|
|
570
595
|
lines.push(` sql: createSqlClient(),`);
|
|
571
|
-
|
|
596
|
+
// No `notifications` here: it is a platform primitive, not something backed
|
|
597
|
+
// by the project database, so it lives at `zitejs/notifications` alongside
|
|
598
|
+
// `zitejs/pdf` and `zitejs/schedules`. Keeping it on `zite` cost a table
|
|
599
|
+
// named "Notifications" its accessor — see RESERVED_TABLE_ACCESSORS.
|
|
572
600
|
lines.push(` auth: createAuthClient<ZiteAuthUser>(),`);
|
|
573
601
|
lines.push("};");
|
|
574
602
|
lines.push("");
|
|
@@ -756,13 +784,13 @@ export function generateApiTs(endpointFiles) {
|
|
|
756
784
|
if (skipped.length > 0) {
|
|
757
785
|
lines.push("// Not endpoints, so no callers were generated for them:");
|
|
758
786
|
for (const name of skipped)
|
|
759
|
-
lines.push(`// ${name}`);
|
|
787
|
+
lines.push(`// ${commentSafe(name)}`);
|
|
760
788
|
lines.push("");
|
|
761
789
|
}
|
|
762
790
|
for (const { pascal, baseName, typed } of endpoints) {
|
|
763
791
|
if (!typed)
|
|
764
792
|
continue;
|
|
765
|
-
lines.push(`import type { default as _${pascal}Ep } from
|
|
793
|
+
lines.push(`import type { default as _${pascal}Ep } from ${tsSingleQuoted(`../src/api/${baseName}`)};`);
|
|
766
794
|
}
|
|
767
795
|
lines.push("");
|
|
768
796
|
for (const { baseName, ident, pascal, stream, typed } of endpoints) {
|
|
@@ -770,10 +798,10 @@ export function generateApiTs(endpointFiles) {
|
|
|
770
798
|
// Declared with `export const x = createEndpoint(...)` — the 1.0 shape.
|
|
771
799
|
// The route is real and the bundler deploys it, so the caller has to
|
|
772
800
|
// exist; there is just no default export to read its types from.
|
|
773
|
-
lines.push(`// '${baseName}' has no default export, so its input/output are untyped.`);
|
|
801
|
+
lines.push(`// ${commentSafe(`'${baseName}'`)} has no default export, so its input/output are untyped.`);
|
|
774
802
|
lines.push(`export type ${pascal}InputType = unknown;`);
|
|
775
803
|
lines.push(`export type ${pascal}OutputType = unknown;`);
|
|
776
|
-
lines.push(`export const ${ident} = ${stream ? "createStreamingCaller" : "createCaller"}<${pascal}InputType, ${pascal}OutputType>(
|
|
804
|
+
lines.push(`export const ${ident} = ${stream ? "createStreamingCaller" : "createCaller"}<${pascal}InputType, ${pascal}OutputType>(${tsSingleQuoted(baseName)});`);
|
|
777
805
|
lines.push("");
|
|
778
806
|
continue;
|
|
779
807
|
}
|
|
@@ -792,7 +820,7 @@ export function generateApiTs(endpointFiles) {
|
|
|
792
820
|
// Calling it `sendEmail` here made every hyphenated or snake_cased endpoint
|
|
793
821
|
// a 404.
|
|
794
822
|
const caller = stream ? "createStreamingCaller" : "createCaller";
|
|
795
|
-
lines.push(`export const ${ident} = ${caller}<${pascal}InputType, ${pascal}OutputType>(
|
|
823
|
+
lines.push(`export const ${ident} = ${caller}<${pascal}InputType, ${pascal}OutputType>(${tsSingleQuoted(baseName)});`);
|
|
796
824
|
lines.push("");
|
|
797
825
|
}
|
|
798
826
|
lines.push("");
|
|
@@ -862,7 +890,14 @@ const AIRTABLE_FIELD_INPUT_TYPE_MAP = {
|
|
|
862
890
|
singleCollaborator: "{ id: string } | { email: string }",
|
|
863
891
|
multipleCollaborators: "Array<{ id: string } | { email: string }>",
|
|
864
892
|
};
|
|
865
|
-
/**
|
|
893
|
+
/**
|
|
894
|
+
* Airtable's attachment object as the REST API really returns it.
|
|
895
|
+
*
|
|
896
|
+
* Not quite `airtable/lib/attachment.d.ts`: that declaration omits `width` and
|
|
897
|
+
* `height`, which the API does return for image attachments. 1.0 exposed them
|
|
898
|
+
* and apps read them, so leaving them out turns a working `photo.width` into a
|
|
899
|
+
* migration type error. Optional, because a non-image attachment has neither.
|
|
900
|
+
*/
|
|
866
901
|
const AIRTABLE_ATTACHMENT_TYPE = [
|
|
867
902
|
"export type AirtableAttachment = {",
|
|
868
903
|
" id: string;",
|
|
@@ -872,6 +907,10 @@ const AIRTABLE_ATTACHMENT_TYPE = [
|
|
|
872
907
|
" size: number;",
|
|
873
908
|
" /** MIME type. */",
|
|
874
909
|
" type: string;",
|
|
910
|
+
" /** Pixel width — images only. */",
|
|
911
|
+
" width?: number;",
|
|
912
|
+
" /** Pixel height — images only. */",
|
|
913
|
+
" height?: number;",
|
|
875
914
|
" thumbnails?: {",
|
|
876
915
|
" small: { url: string; width: number; height: number };",
|
|
877
916
|
" large: { url: string; width: number; height: number };",
|
|
@@ -900,7 +939,7 @@ function airtableTsType(field, lock, depth, variant = "read") {
|
|
|
900
939
|
choices.length > 0) {
|
|
901
940
|
const literals = choices
|
|
902
941
|
.slice(0, MAX_SELECT_OPTIONS)
|
|
903
|
-
.map((o) =>
|
|
942
|
+
.map((o) => tsStringLiteral(o))
|
|
904
943
|
.join(" | ");
|
|
905
944
|
const union = `${literals} | string`;
|
|
906
945
|
if (field.type === "multipleSelects")
|
|
@@ -1046,7 +1085,7 @@ export function generateAirtableTs(lock) {
|
|
|
1046
1085
|
lines.push(...AIRTABLE_ATTACHMENT_TYPE);
|
|
1047
1086
|
for (const table of lock.tables) {
|
|
1048
1087
|
const recordType = `${table.sdkName}RecordType`;
|
|
1049
|
-
lines.push(`/** A ${table.sdkName} record as it is read back. */`);
|
|
1088
|
+
lines.push(`/** A ${commentSafe(table.sdkName)} record as it is read back. */`);
|
|
1050
1089
|
lines.push(`export type ${recordType} = {`);
|
|
1051
1090
|
lines.push(" id: string;");
|
|
1052
1091
|
for (const field of table.fields) {
|
|
@@ -1054,7 +1093,7 @@ export function generateAirtableTs(lock) {
|
|
|
1054
1093
|
continue;
|
|
1055
1094
|
const jsdoc = airtableFieldJsdoc(field, table, lock);
|
|
1056
1095
|
if (jsdoc) {
|
|
1057
|
-
lines.push(` /** ${jsdoc} */`);
|
|
1096
|
+
lines.push(` /** ${commentSafe(jsdoc)} */`);
|
|
1058
1097
|
}
|
|
1059
1098
|
const tsType = airtableTsType(field, lock);
|
|
1060
1099
|
// Optional, because Airtable omits a field from the response entirely
|
|
@@ -1068,7 +1107,7 @@ export function generateAirtableTs(lock) {
|
|
|
1068
1107
|
// Read-only fields are omitted rather than typed: Airtable rejects a write
|
|
1069
1108
|
// to a formula, rollup, lookup or autonumber with a 422.
|
|
1070
1109
|
const writableFields = table.fields.filter((f) => f.sdkName !== "id" && !READ_ONLY_AIRTABLE_FIELDS.has(f.type));
|
|
1071
|
-
lines.push(`/** What you may write when creating or updating a ${table.sdkName}. */`);
|
|
1110
|
+
lines.push(`/** What you may write when creating or updating a ${commentSafe(table.sdkName)}. */`);
|
|
1072
1111
|
lines.push(`export type ${table.sdkName}RecordInput = {`);
|
|
1073
1112
|
for (const field of writableFields) {
|
|
1074
1113
|
lines.push(` ${field.sdkName}: ${airtableTsType(field, lock, 0, "write")};`);
|
|
@@ -1234,7 +1273,7 @@ export function generateEmailSdk(integrationId) {
|
|
|
1234
1273
|
" SendEmailResult,",
|
|
1235
1274
|
"} from 'zitejs/runtime';",
|
|
1236
1275
|
"",
|
|
1237
|
-
`export const Email = createEmailClient(
|
|
1276
|
+
`export const Email = createEmailClient(${tsSingleQuoted(integrationId)});`,
|
|
1238
1277
|
"",
|
|
1239
1278
|
].join("\n");
|
|
1240
1279
|
}
|
|
@@ -1,5 +1,156 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { generateAirtableTs, generateApiTs, generateBackendWrapperTs, generateDbTs, generateEmailSdk, } from './lib.js';
|
|
4
|
+
/**
|
|
5
|
+
* Syntax errors in the emitted source. `.zite/db.ts` sits at the repo root and
|
|
6
|
+
* every app imports it, so anything unparseable here fails typecheck for the
|
|
7
|
+
* whole project — and the file is generated, so no one can edit their way out.
|
|
8
|
+
*/
|
|
9
|
+
const syntaxErrorsIn = (source) => (ts.transpileModule(source, {
|
|
10
|
+
reportDiagnostics: true,
|
|
11
|
+
compilerOptions: { target: ts.ScriptTarget.Latest },
|
|
12
|
+
}).diagnostics ?? []).map(d => ts.flattenDiagnosticMessageText(d.messageText, ' '));
|
|
13
|
+
const schemaWithSelectOption = (label) => ({
|
|
14
|
+
tables: [
|
|
15
|
+
{
|
|
16
|
+
id: 'tbl1',
|
|
17
|
+
sdkName: 'registrations',
|
|
18
|
+
fields: [
|
|
19
|
+
{
|
|
20
|
+
id: 'fld1',
|
|
21
|
+
sdkName: 'ticketType',
|
|
22
|
+
definition: {
|
|
23
|
+
type: 'single_select',
|
|
24
|
+
template: { options: [{ label }] },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
describe('generateDbTs select option literals', () => {
|
|
32
|
+
// Reported from a migrated app: a consent paragraph pasted in as an option
|
|
33
|
+
// label carried a newline, so the emitted literal never closed and the whole
|
|
34
|
+
// generated file failed with TS1002 — taking every sibling app with it.
|
|
35
|
+
it.each([
|
|
36
|
+
['a newline', 'I am 18 or above.\nI agree to the rules.'],
|
|
37
|
+
['a carriage return', 'Lite Pass\r\nRegular Pass'],
|
|
38
|
+
['a double quote', 'The "Premium" tier'],
|
|
39
|
+
['a trailing backslash', 'Group / Crew Purchase\\'],
|
|
40
|
+
['a tab', 'Lite\tPass'],
|
|
41
|
+
])('emits parseable TypeScript for a label containing %s', (_what, label) => {
|
|
42
|
+
expect(syntaxErrorsIn(generateDbTs(schemaWithSelectOption(label)))).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
it('keeps the option readable in the union', () => {
|
|
45
|
+
const out = generateDbTs(schemaWithSelectOption('The "Premium" tier'));
|
|
46
|
+
expect(out).toContain('"The \\"Premium\\" tier"');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('generateAirtableTs attachment type', () => {
|
|
50
|
+
const lockWithAttachment = {
|
|
51
|
+
integrationId: 'airtable',
|
|
52
|
+
tables: [
|
|
53
|
+
{
|
|
54
|
+
id: 'tbl1',
|
|
55
|
+
sdkName: 'Speakers',
|
|
56
|
+
primaryFieldId: 'fld1',
|
|
57
|
+
fields: [
|
|
58
|
+
{ id: 'fld1', sdkName: 'name', type: 'singleLineText', name: 'Name' },
|
|
59
|
+
{
|
|
60
|
+
id: 'fld2',
|
|
61
|
+
sdkName: 'headshot',
|
|
62
|
+
type: 'multipleAttachments',
|
|
63
|
+
name: 'Headshot',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
// Reported from a migrated app reading `photo.width` to lay out an image
|
|
70
|
+
// grid. Airtable returns width/height on image attachments and 1.0 exposed
|
|
71
|
+
// them; `airtable/lib/attachment.d.ts`, which this type was copied from,
|
|
72
|
+
// omits both — so the read became a TS2339 the app could not edit away.
|
|
73
|
+
it.each(['width', 'height'])('exposes %s, which Airtable returns for images', prop => {
|
|
74
|
+
const out = generateAirtableTs(lockWithAttachment);
|
|
75
|
+
expect(out).not.toBeNull();
|
|
76
|
+
expect(out).toContain(` ${prop}?: number;`);
|
|
77
|
+
});
|
|
78
|
+
it('emits parseable TypeScript', () => {
|
|
79
|
+
expect(syntaxErrorsIn(generateAirtableTs(lockWithAttachment) ?? '')).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('generateDbTs field names in comments', () => {
|
|
83
|
+
// A field's display name is user text and reaches a one-line JSDoc. A
|
|
84
|
+
// comment-close sequence in it would end the comment early and leave the
|
|
85
|
+
// remainder to parse as code.
|
|
86
|
+
const schemaWithFieldNamed = (name) => ({
|
|
87
|
+
tables: [
|
|
88
|
+
{
|
|
89
|
+
id: 'tbl1',
|
|
90
|
+
sdkName: 'orders',
|
|
91
|
+
primaryFieldId: 'fld1',
|
|
92
|
+
fields: [
|
|
93
|
+
{
|
|
94
|
+
id: 'fld1',
|
|
95
|
+
sdkName: 'amount',
|
|
96
|
+
definition: {
|
|
97
|
+
type: 'currency',
|
|
98
|
+
name,
|
|
99
|
+
template: { currencySymbol: '$' },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
it.each([
|
|
107
|
+
['a comment-close sequence', 'Total */ console.log(1); /*'],
|
|
108
|
+
['a newline', 'Total\namount'],
|
|
109
|
+
])('emits parseable TypeScript for a field named with %s', (_what, name) => {
|
|
110
|
+
expect(syntaxErrorsIn(generateDbTs(schemaWithFieldNamed(name)))).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('generateApiTs endpoint file names', () => {
|
|
114
|
+
// An endpoint filename is the LLM's raw `writeFile` path argument — nothing
|
|
115
|
+
// validates its characters — and it lands in an import specifier, a comment
|
|
116
|
+
// and two string literals. `.zite/api.ts` is imported by every component that
|
|
117
|
+
// calls `api.*`, so one bad name takes the whole app down.
|
|
118
|
+
it.each([
|
|
119
|
+
['an apostrophe', "it's.ts"],
|
|
120
|
+
['a newline', 'ok\nreport.ts'],
|
|
121
|
+
['a trailing backslash', 'back\\.ts'],
|
|
122
|
+
['a double quote', 'say"hi.ts'],
|
|
123
|
+
])('emits parseable TypeScript for a file named with %s', (_what, name) => {
|
|
124
|
+
const out = generateApiTs([
|
|
125
|
+
{ fileName: name, content: 'export default createEndpoint({});' },
|
|
126
|
+
]);
|
|
127
|
+
expect(out).not.toBeNull();
|
|
128
|
+
expect(syntaxErrorsIn(out)).toEqual([]);
|
|
129
|
+
});
|
|
130
|
+
// The no-default-export branch puts the name in a `//` comment instead, so it
|
|
131
|
+
// needs its own case: there a line terminator ends the comment, not a string.
|
|
132
|
+
it('emits parseable TypeScript for an untyped endpoint with a newline', () => {
|
|
133
|
+
const out = generateApiTs([
|
|
134
|
+
{
|
|
135
|
+
fileName: 'ok\nreport.ts',
|
|
136
|
+
content: 'export const report = createEndpoint({});',
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
expect(out).not.toBeNull();
|
|
140
|
+
expect(syntaxErrorsIn(out)).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('generateEmailSdk integration id', () => {
|
|
144
|
+
// An LLM-authored `zite.config.json` key, validated only as
|
|
145
|
+
// `z.record(z.string(), …)`.
|
|
146
|
+
it.each([
|
|
147
|
+
['an apostrophe', "resend'prod"],
|
|
148
|
+
['a newline', 'resend\nprod'],
|
|
149
|
+
['a trailing backslash', 'resend\\'],
|
|
150
|
+
])('emits parseable TypeScript for an id with %s', (_what, id) => {
|
|
151
|
+
expect(syntaxErrorsIn(generateEmailSdk(id))).toEqual([]);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
3
154
|
describe('generateBackendWrapperTs', () => {
|
|
4
155
|
const output = generateBackendWrapperTs();
|
|
5
156
|
it('imports User from zitejs/auth', () => {
|
|
@@ -36,8 +36,15 @@ export declare function toSdkName(name: string): string;
|
|
|
36
36
|
/**
|
|
37
37
|
* Accessors `generateDbTs` puts on the `zite` object itself, after the tables.
|
|
38
38
|
* A table that allocates one of these produces a duplicate key.
|
|
39
|
+
*
|
|
40
|
+
* `notifications` used to be here and no longer is: it was a platform
|
|
41
|
+
* primitive hanging off `zite`, so a table named "Notifications" collided with
|
|
42
|
+
* it and — because the sentinel was written last — lost its client entirely.
|
|
43
|
+
* Moving it to `zitejs/notifications` removes the collision rather than
|
|
44
|
+
* managing it, and frees the name for the table that wants it. Everything left
|
|
45
|
+
* is genuinely backed by the project database.
|
|
39
46
|
*/
|
|
40
|
-
export declare const RESERVED_TABLE_ACCESSORS: readonly ["sql", "
|
|
47
|
+
export declare const RESERVED_TABLE_ACCESSORS: readonly ["sql", "auth"];
|
|
41
48
|
/**
|
|
42
49
|
* Never safe as a generated key, in any namespace. Assigning `__proto__` in an
|
|
43
50
|
* object literal sets the prototype rather than defining a property, and
|
|
@@ -56,12 +56,15 @@ export function toSdkName(name) {
|
|
|
56
56
|
/**
|
|
57
57
|
* Accessors `generateDbTs` puts on the `zite` object itself, after the tables.
|
|
58
58
|
* A table that allocates one of these produces a duplicate key.
|
|
59
|
+
*
|
|
60
|
+
* `notifications` used to be here and no longer is: it was a platform
|
|
61
|
+
* primitive hanging off `zite`, so a table named "Notifications" collided with
|
|
62
|
+
* it and — because the sentinel was written last — lost its client entirely.
|
|
63
|
+
* Moving it to `zitejs/notifications` removes the collision rather than
|
|
64
|
+
* managing it, and frees the name for the table that wants it. Everything left
|
|
65
|
+
* is genuinely backed by the project database.
|
|
59
66
|
*/
|
|
60
|
-
export const RESERVED_TABLE_ACCESSORS = [
|
|
61
|
-
"sql",
|
|
62
|
-
"notifications",
|
|
63
|
-
"auth",
|
|
64
|
-
];
|
|
67
|
+
export const RESERVED_TABLE_ACCESSORS = ["sql", "auth"];
|
|
65
68
|
/**
|
|
66
69
|
* Never safe as a generated key, in any namespace. Assigning `__proto__` in an
|
|
67
70
|
* object literal sets the prototype rather than defining a property, and
|
|
@@ -31,17 +31,18 @@ const recordTypeKeys = (source, typeName) => {
|
|
|
31
31
|
};
|
|
32
32
|
describe("reserved platform accessors", () => {
|
|
33
33
|
// Sudzy Dashboard: a table named "Notifications" emitted a second
|
|
34
|
-
// `notifications:` key
|
|
34
|
+
// `notifications:` key and the platform client — written last — won, so the
|
|
35
35
|
// table lost findAll and typed as `{ create(NotificationsCreateParams) }`.
|
|
36
|
-
|
|
36
|
+
// The platform client has since moved to `zitejs/notifications`, so the name
|
|
37
|
+
// belongs to the table again and there is nothing left to collide with.
|
|
38
|
+
it("lets a table keep the notifications accessor", () => {
|
|
37
39
|
const schema = generateSchema(database([{ id: "tbl_1", name: "Notifications", fields: [] }]));
|
|
38
|
-
expect(schema.tables[0].sdkName).toBe("
|
|
40
|
+
expect(schema.tables[0].sdkName).toBe("notifications");
|
|
39
41
|
const keys = ziteKeys(generateDbTs(schema));
|
|
40
|
-
expect(keys).toContain("notifications2");
|
|
41
42
|
expect(keys.filter((k) => k === "notifications")).toHaveLength(1);
|
|
42
43
|
expect(new Set(keys).size).toBe(keys.length);
|
|
43
44
|
});
|
|
44
|
-
it.each(["sql", "auth"
|
|
45
|
+
it.each(["sql", "auth"])("keeps the platform %s accessor for the platform", (reserved) => {
|
|
45
46
|
const schema = generateSchema(database([{ id: "tbl_1", name: reserved, fields: [] }]));
|
|
46
47
|
expect(schema.tables[0].sdkName).toBe(`${reserved}2`);
|
|
47
48
|
const keys = ziteKeys(generateDbTs(schema));
|
|
@@ -190,14 +191,17 @@ describe("normalizeSchemaNames", () => {
|
|
|
190
191
|
// `zitejs generate` reads it with no generateSchema pass in front, so this is
|
|
191
192
|
// the only thing standing between it and a TS1117 it cannot build past.
|
|
192
193
|
it("repairs a committed schema that already holds the collision", () => {
|
|
194
|
+
// `sql`, not `notifications` — the latter stopped being a platform
|
|
195
|
+
// accessor when the client moved to `zitejs/notifications`, so a schema
|
|
196
|
+
// holding it is no longer a collision to repair.
|
|
193
197
|
const source = generateDbTs({
|
|
194
198
|
tables: [
|
|
195
|
-
{ id: "tbl_1", sdkName: "
|
|
199
|
+
{ id: "tbl_1", sdkName: "sql", fields: [] },
|
|
196
200
|
{ id: "tbl_2", sdkName: "orders", fields: [] },
|
|
197
201
|
],
|
|
198
202
|
});
|
|
199
203
|
const keys = ziteKeys(source);
|
|
200
204
|
expect(new Set(keys).size).toBe(keys.length);
|
|
201
|
-
expect(keys).toContain("
|
|
205
|
+
expect(keys).toContain("sql2");
|
|
202
206
|
});
|
|
203
207
|
});
|