tthr 0.0.58 → 0.0.59
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/chunk-GKELUQ44.js +502 -0
- package/dist/chunk-JPZQRYD3.js +502 -0
- package/dist/generate-5AY6JNCG.js +8 -0
- package/dist/generate-KZ2UQ2E6.js +8 -0
- package/dist/index.js +16 -6
- package/package.json +1 -1
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
// src/commands/generate.ts
|
|
2
|
+
import chalk2 from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import fs3 from "fs-extra";
|
|
5
|
+
import path3 from "path";
|
|
6
|
+
|
|
7
|
+
// src/utils/auth.ts
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import fs from "fs-extra";
|
|
10
|
+
import path from "path";
|
|
11
|
+
var CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".tether");
|
|
12
|
+
var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
13
|
+
async function getCredentials() {
|
|
14
|
+
try {
|
|
15
|
+
if (await fs.pathExists(CREDENTIALS_FILE)) {
|
|
16
|
+
return await fs.readJSON(CREDENTIALS_FILE);
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
async function requireAuth() {
|
|
23
|
+
const credentials = await getCredentials();
|
|
24
|
+
if (!credentials) {
|
|
25
|
+
console.error(chalk.red("\n\u2717 Not logged in\n"));
|
|
26
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (credentials.expiresAt) {
|
|
30
|
+
const expiresAt = new Date(credentials.expiresAt);
|
|
31
|
+
if (/* @__PURE__ */ new Date() > expiresAt) {
|
|
32
|
+
console.error(chalk.red("\n\u2717 Session expired\n"));
|
|
33
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return credentials;
|
|
38
|
+
}
|
|
39
|
+
async function saveCredentials(credentials) {
|
|
40
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
41
|
+
await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
|
|
42
|
+
}
|
|
43
|
+
async function clearCredentials() {
|
|
44
|
+
try {
|
|
45
|
+
await fs.remove(CREDENTIALS_FILE);
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/utils/config.ts
|
|
51
|
+
import fs2 from "fs-extra";
|
|
52
|
+
import path2 from "path";
|
|
53
|
+
var DEFAULT_CONFIG = {
|
|
54
|
+
schema: "./tether/schema.ts",
|
|
55
|
+
functions: "./tether/functions",
|
|
56
|
+
output: "./tether/_generated",
|
|
57
|
+
dev: {
|
|
58
|
+
port: 3001,
|
|
59
|
+
host: "localhost"
|
|
60
|
+
},
|
|
61
|
+
database: {
|
|
62
|
+
walMode: true
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
66
|
+
const configPath = path2.resolve(cwd, "tether.config.ts");
|
|
67
|
+
if (!await fs2.pathExists(configPath)) {
|
|
68
|
+
return DEFAULT_CONFIG;
|
|
69
|
+
}
|
|
70
|
+
const configSource = await fs2.readFile(configPath, "utf-8");
|
|
71
|
+
const config = {};
|
|
72
|
+
const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
|
|
73
|
+
if (schemaMatch) {
|
|
74
|
+
config.schema = schemaMatch[1];
|
|
75
|
+
}
|
|
76
|
+
const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
|
|
77
|
+
if (functionsMatch) {
|
|
78
|
+
config.functions = functionsMatch[1];
|
|
79
|
+
}
|
|
80
|
+
const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
|
|
81
|
+
if (outputMatch) {
|
|
82
|
+
config.output = outputMatch[1];
|
|
83
|
+
}
|
|
84
|
+
const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
|
|
85
|
+
if (envMatch) {
|
|
86
|
+
config.environment = envMatch[1];
|
|
87
|
+
}
|
|
88
|
+
const portMatch = configSource.match(/port\s*:\s*(\d+)/);
|
|
89
|
+
if (portMatch) {
|
|
90
|
+
config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
|
|
91
|
+
}
|
|
92
|
+
const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
|
|
93
|
+
if (hostMatch) {
|
|
94
|
+
config.dev = { ...config.dev, host: hostMatch[1] };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
...DEFAULT_CONFIG,
|
|
98
|
+
...config,
|
|
99
|
+
dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
|
|
100
|
+
database: { ...DEFAULT_CONFIG.database, ...config.database }
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function resolvePath(configPath, cwd = process.cwd()) {
|
|
104
|
+
const normalised = configPath.replace(/^\.\//, "");
|
|
105
|
+
return path2.resolve(cwd, normalised);
|
|
106
|
+
}
|
|
107
|
+
async function detectFramework(cwd = process.cwd()) {
|
|
108
|
+
const packageJsonPath = path2.resolve(cwd, "package.json");
|
|
109
|
+
if (!await fs2.pathExists(packageJsonPath)) {
|
|
110
|
+
return "unknown";
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const packageJson = await fs2.readJson(packageJsonPath);
|
|
114
|
+
const deps = {
|
|
115
|
+
...packageJson.dependencies,
|
|
116
|
+
...packageJson.devDependencies
|
|
117
|
+
};
|
|
118
|
+
if (deps.nuxt || deps["@nuxt/kit"]) {
|
|
119
|
+
return "nuxt";
|
|
120
|
+
}
|
|
121
|
+
if (deps.next) {
|
|
122
|
+
return "next";
|
|
123
|
+
}
|
|
124
|
+
if (deps["@sveltejs/kit"]) {
|
|
125
|
+
return "sveltekit";
|
|
126
|
+
}
|
|
127
|
+
if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
|
|
128
|
+
return "vite";
|
|
129
|
+
}
|
|
130
|
+
return "vanilla";
|
|
131
|
+
} catch {
|
|
132
|
+
return "unknown";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function getFrameworkDevCommand(framework) {
|
|
136
|
+
switch (framework) {
|
|
137
|
+
case "nuxt":
|
|
138
|
+
return "nuxt dev";
|
|
139
|
+
case "next":
|
|
140
|
+
return "next dev";
|
|
141
|
+
case "sveltekit":
|
|
142
|
+
return "vite dev";
|
|
143
|
+
case "vite":
|
|
144
|
+
return "vite dev";
|
|
145
|
+
case "vanilla":
|
|
146
|
+
return null;
|
|
147
|
+
// No default dev server for vanilla
|
|
148
|
+
default:
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/commands/generate.ts
|
|
154
|
+
function parseSchemaFile(source) {
|
|
155
|
+
const tables = [];
|
|
156
|
+
const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
|
|
157
|
+
if (!schemaMatch) return tables;
|
|
158
|
+
const schemaContent = schemaMatch[1];
|
|
159
|
+
const tableStartRegex = /(\w+)\s*:\s*\{/g;
|
|
160
|
+
let match;
|
|
161
|
+
while ((match = tableStartRegex.exec(schemaContent)) !== null) {
|
|
162
|
+
const tableName = match[1];
|
|
163
|
+
const startOffset = match.index + match[0].length;
|
|
164
|
+
let braceCount = 1;
|
|
165
|
+
let endOffset = startOffset;
|
|
166
|
+
for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
|
|
167
|
+
const char = schemaContent[i];
|
|
168
|
+
if (char === "{") braceCount++;
|
|
169
|
+
else if (char === "}") braceCount--;
|
|
170
|
+
endOffset = i;
|
|
171
|
+
}
|
|
172
|
+
const columnsContent = schemaContent.slice(startOffset, endOffset);
|
|
173
|
+
const columns = parseColumns(columnsContent);
|
|
174
|
+
tables.push({ name: tableName, columns });
|
|
175
|
+
}
|
|
176
|
+
return tables;
|
|
177
|
+
}
|
|
178
|
+
function parseColumns(content) {
|
|
179
|
+
const columns = [];
|
|
180
|
+
const columnRegex = /(\w+)\s*:\s*(\w+)(?:<([^>]+)>)?\s*\(\s*\)((?:\[.*?\]|[^,\n}])*)/g;
|
|
181
|
+
let match;
|
|
182
|
+
while ((match = columnRegex.exec(content)) !== null) {
|
|
183
|
+
const name = match[1];
|
|
184
|
+
const schemaType = match[2];
|
|
185
|
+
const genericType = match[3];
|
|
186
|
+
const modifiers = match[4] || "";
|
|
187
|
+
const nullable = !modifiers.includes(".notNull()");
|
|
188
|
+
const oneOfMatch = modifiers.match(/\.oneOf\s*\(\s*\[(.*?)\]\s*\)/);
|
|
189
|
+
let oneOf;
|
|
190
|
+
if (oneOfMatch) {
|
|
191
|
+
oneOf = [...oneOfMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
192
|
+
}
|
|
193
|
+
columns.push({
|
|
194
|
+
name,
|
|
195
|
+
type: schemaTypeToTS(schemaType),
|
|
196
|
+
nullable,
|
|
197
|
+
jsonType: genericType,
|
|
198
|
+
// Store the generic type for json columns
|
|
199
|
+
oneOf
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return columns;
|
|
203
|
+
}
|
|
204
|
+
function schemaTypeToTS(schemaType) {
|
|
205
|
+
switch (schemaType) {
|
|
206
|
+
case "text":
|
|
207
|
+
return "string";
|
|
208
|
+
case "integer":
|
|
209
|
+
return "number";
|
|
210
|
+
case "real":
|
|
211
|
+
return "number";
|
|
212
|
+
case "boolean":
|
|
213
|
+
return "boolean";
|
|
214
|
+
case "timestamp":
|
|
215
|
+
return "string";
|
|
216
|
+
case "json":
|
|
217
|
+
return "unknown";
|
|
218
|
+
case "blob":
|
|
219
|
+
return "Uint8Array";
|
|
220
|
+
case "asset":
|
|
221
|
+
return "TetherAsset";
|
|
222
|
+
// Asset object returned by API
|
|
223
|
+
default:
|
|
224
|
+
return "unknown";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function tableNameToInterface(tableName) {
|
|
228
|
+
let name = tableName;
|
|
229
|
+
if (name.endsWith("ies")) {
|
|
230
|
+
name = name.slice(0, -3) + "y";
|
|
231
|
+
} else if (name.endsWith("s") && !name.endsWith("ss")) {
|
|
232
|
+
name = name.slice(0, -1);
|
|
233
|
+
}
|
|
234
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
235
|
+
}
|
|
236
|
+
function generateDbFile(tables) {
|
|
237
|
+
const jsonTypes = /* @__PURE__ */ new Set();
|
|
238
|
+
for (const table of tables) {
|
|
239
|
+
for (const col of table.columns) {
|
|
240
|
+
if (col.jsonType) {
|
|
241
|
+
jsonTypes.add(col.jsonType.replace(/\[\]$/, ""));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const lines = [
|
|
246
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
247
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
248
|
+
"",
|
|
249
|
+
"import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
|
|
250
|
+
"import {",
|
|
251
|
+
" query as baseQuery,",
|
|
252
|
+
" mutation as baseMutation,",
|
|
253
|
+
" type QueryDefinition,",
|
|
254
|
+
" type MutationDefinition,",
|
|
255
|
+
" z,",
|
|
256
|
+
"} from '@tthr/server';"
|
|
257
|
+
];
|
|
258
|
+
if (jsonTypes.size > 0) {
|
|
259
|
+
const typeImports = Array.from(jsonTypes).sort().join(", ");
|
|
260
|
+
lines.push(`import type { ${typeImports} } from '../schema';`);
|
|
261
|
+
}
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push("// Asset type returned by the API for asset columns");
|
|
264
|
+
lines.push("export interface TetherAsset {");
|
|
265
|
+
lines.push(" id: string;");
|
|
266
|
+
lines.push(" filename: string;");
|
|
267
|
+
lines.push(" contentType: string;");
|
|
268
|
+
lines.push(" size: number;");
|
|
269
|
+
lines.push(" url: string;");
|
|
270
|
+
lines.push(" createdAt: string;");
|
|
271
|
+
lines.push("}");
|
|
272
|
+
lines.push("");
|
|
273
|
+
for (const table of tables) {
|
|
274
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
275
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
276
|
+
lines.push(" _id: string;");
|
|
277
|
+
for (const col of table.columns) {
|
|
278
|
+
let colType;
|
|
279
|
+
if (col.oneOf && col.oneOf.length > 0) {
|
|
280
|
+
colType = col.oneOf.map((v) => `'${v}'`).join(" | ");
|
|
281
|
+
} else {
|
|
282
|
+
colType = col.jsonType || col.type;
|
|
283
|
+
}
|
|
284
|
+
const typeStr = col.nullable ? `${colType} | null` : colType;
|
|
285
|
+
const optional = col.nullable ? "?" : "";
|
|
286
|
+
lines.push(` ${col.name}${optional}: ${typeStr};`);
|
|
287
|
+
}
|
|
288
|
+
lines.push(" _createdAt: string;");
|
|
289
|
+
lines.push(" _updatedAt?: string | null;");
|
|
290
|
+
lines.push(" _deletedAt?: string | null;");
|
|
291
|
+
lines.push("}");
|
|
292
|
+
lines.push("");
|
|
293
|
+
}
|
|
294
|
+
lines.push("export interface Schema {");
|
|
295
|
+
for (const table of tables) {
|
|
296
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
297
|
+
lines.push(` ${table.name}: ${interfaceName};`);
|
|
298
|
+
}
|
|
299
|
+
lines.push("}");
|
|
300
|
+
lines.push("");
|
|
301
|
+
lines.push("// Database client with typed tables");
|
|
302
|
+
lines.push("// This is a proxy that will be populated by the Tether runtime");
|
|
303
|
+
lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
|
|
304
|
+
lines.push("");
|
|
305
|
+
lines.push("// ============================================================================");
|
|
306
|
+
lines.push("// Typed function wrappers - use these instead of importing from @tthr/server");
|
|
307
|
+
lines.push("// This ensures the `db` parameter in handlers is properly typed with Schema");
|
|
308
|
+
lines.push("// ============================================================================");
|
|
309
|
+
lines.push("");
|
|
310
|
+
lines.push("/**");
|
|
311
|
+
lines.push(" * Define a query function with typed database access.");
|
|
312
|
+
lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
|
|
313
|
+
lines.push(" */");
|
|
314
|
+
lines.push("export function query<TArgs = void, TResult = unknown>(");
|
|
315
|
+
lines.push(" definition: QueryDefinition<TArgs, TResult, Schema>");
|
|
316
|
+
lines.push("): QueryDefinition<TArgs, TResult, Schema> {");
|
|
317
|
+
lines.push(" return baseQuery(definition);");
|
|
318
|
+
lines.push("}");
|
|
319
|
+
lines.push("");
|
|
320
|
+
lines.push("/**");
|
|
321
|
+
lines.push(" * Define a mutation function with typed database access.");
|
|
322
|
+
lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
|
|
323
|
+
lines.push(" */");
|
|
324
|
+
lines.push("export function mutation<TArgs = void, TResult = unknown>(");
|
|
325
|
+
lines.push(" definition: MutationDefinition<TArgs, TResult, Schema>");
|
|
326
|
+
lines.push("): MutationDefinition<TArgs, TResult, Schema> {");
|
|
327
|
+
lines.push(" return baseMutation(definition);");
|
|
328
|
+
lines.push("}");
|
|
329
|
+
lines.push("");
|
|
330
|
+
lines.push("// Re-export z for convenience");
|
|
331
|
+
lines.push("export { z };");
|
|
332
|
+
if (jsonTypes.size > 0) {
|
|
333
|
+
lines.push("");
|
|
334
|
+
lines.push("// Re-export JSON schema types");
|
|
335
|
+
const typeExports = Array.from(jsonTypes).sort().join(", ");
|
|
336
|
+
lines.push(`export type { ${typeExports} } from '../schema';`);
|
|
337
|
+
}
|
|
338
|
+
lines.push("");
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
}
|
|
341
|
+
async function parseFunctionsDir(functionsDir) {
|
|
342
|
+
const functions = [];
|
|
343
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
344
|
+
return functions;
|
|
345
|
+
}
|
|
346
|
+
const files = await fs3.readdir(functionsDir);
|
|
347
|
+
for (const file of files) {
|
|
348
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
349
|
+
const filePath = path3.join(functionsDir, file);
|
|
350
|
+
const stat = await fs3.stat(filePath);
|
|
351
|
+
if (!stat.isFile()) continue;
|
|
352
|
+
const moduleName = file.replace(/\.(ts|js)$/, "");
|
|
353
|
+
const source = await fs3.readFile(filePath, "utf-8");
|
|
354
|
+
const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation)\s*\(/g;
|
|
355
|
+
let match;
|
|
356
|
+
while ((match = exportRegex.exec(source)) !== null) {
|
|
357
|
+
functions.push({
|
|
358
|
+
name: match[1],
|
|
359
|
+
moduleName
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return functions;
|
|
364
|
+
}
|
|
365
|
+
async function generateApiFile(functionsDir) {
|
|
366
|
+
const functions = await parseFunctionsDir(functionsDir);
|
|
367
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
368
|
+
for (const fn of functions) {
|
|
369
|
+
if (!moduleMap.has(fn.moduleName)) {
|
|
370
|
+
moduleMap.set(fn.moduleName, []);
|
|
371
|
+
}
|
|
372
|
+
moduleMap.get(fn.moduleName).push(fn.name);
|
|
373
|
+
}
|
|
374
|
+
const lines = [
|
|
375
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
376
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
377
|
+
"",
|
|
378
|
+
"import { createApiProxy } from '@tthr/client';",
|
|
379
|
+
"",
|
|
380
|
+
"/**",
|
|
381
|
+
" * API function reference type for useQuery/useMutation.",
|
|
382
|
+
' * The _name property contains the function path (e.g., "users.list").',
|
|
383
|
+
" */",
|
|
384
|
+
"export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
|
|
385
|
+
" _name: string;",
|
|
386
|
+
" _args?: TArgs;",
|
|
387
|
+
" _result?: TResult;",
|
|
388
|
+
"}",
|
|
389
|
+
""
|
|
390
|
+
];
|
|
391
|
+
if (moduleMap.size > 0) {
|
|
392
|
+
for (const [moduleName, fnNames] of moduleMap) {
|
|
393
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
394
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
395
|
+
for (const fnName of fnNames) {
|
|
396
|
+
lines.push(` ${fnName}: ApiFunction;`);
|
|
397
|
+
}
|
|
398
|
+
lines.push("}");
|
|
399
|
+
lines.push("");
|
|
400
|
+
}
|
|
401
|
+
lines.push("export interface Api {");
|
|
402
|
+
for (const moduleName of moduleMap.keys()) {
|
|
403
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
404
|
+
lines.push(` ${moduleName}: ${interfaceName};`);
|
|
405
|
+
}
|
|
406
|
+
lines.push("}");
|
|
407
|
+
} else {
|
|
408
|
+
lines.push("/**");
|
|
409
|
+
lines.push(" * Flexible API type that allows access to any function path.");
|
|
410
|
+
lines.push(" * Functions are accessed as api.moduleName.functionName");
|
|
411
|
+
lines.push(" * The actual types depend on your function definitions.");
|
|
412
|
+
lines.push(" */");
|
|
413
|
+
lines.push("export type Api = {");
|
|
414
|
+
lines.push(" [module: string]: {");
|
|
415
|
+
lines.push(" [fn: string]: ApiFunction;");
|
|
416
|
+
lines.push(" };");
|
|
417
|
+
lines.push("};");
|
|
418
|
+
}
|
|
419
|
+
lines.push("");
|
|
420
|
+
lines.push("// API client proxy - provides typed access to your functions");
|
|
421
|
+
lines.push("// On the client: returns { _name } references for useQuery/useMutation");
|
|
422
|
+
lines.push("// In Tether functions: calls the actual function implementation");
|
|
423
|
+
lines.push("export const api = createApiProxy<Api>();");
|
|
424
|
+
lines.push("");
|
|
425
|
+
return lines.join("\n");
|
|
426
|
+
}
|
|
427
|
+
async function generateTypes(options = {}) {
|
|
428
|
+
const config = await loadConfig();
|
|
429
|
+
const schemaPath = resolvePath(config.schema);
|
|
430
|
+
const outputDir = resolvePath(config.output);
|
|
431
|
+
const functionsDir = resolvePath(config.functions);
|
|
432
|
+
if (!await fs3.pathExists(schemaPath)) {
|
|
433
|
+
throw new Error(`Schema file not found: ${schemaPath}`);
|
|
434
|
+
}
|
|
435
|
+
const schemaSource = await fs3.readFile(schemaPath, "utf-8");
|
|
436
|
+
const tables = parseSchemaFile(schemaSource);
|
|
437
|
+
await fs3.ensureDir(outputDir);
|
|
438
|
+
await fs3.writeFile(
|
|
439
|
+
path3.join(outputDir, "db.ts"),
|
|
440
|
+
generateDbFile(tables)
|
|
441
|
+
);
|
|
442
|
+
await fs3.writeFile(
|
|
443
|
+
path3.join(outputDir, "api.ts"),
|
|
444
|
+
await generateApiFile(functionsDir)
|
|
445
|
+
);
|
|
446
|
+
await fs3.writeFile(
|
|
447
|
+
path3.join(outputDir, "index.ts"),
|
|
448
|
+
`// Auto-generated by Tether CLI - do not edit manually
|
|
449
|
+
export * from './db';
|
|
450
|
+
export * from './api';
|
|
451
|
+
`
|
|
452
|
+
);
|
|
453
|
+
return { tables, outputDir };
|
|
454
|
+
}
|
|
455
|
+
async function generateCommand() {
|
|
456
|
+
await requireAuth();
|
|
457
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
458
|
+
if (!await fs3.pathExists(configPath)) {
|
|
459
|
+
console.log(chalk2.red("\nError: Not a Tether project"));
|
|
460
|
+
console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
|
|
464
|
+
const spinner = ora("Reading schema...").start();
|
|
465
|
+
try {
|
|
466
|
+
spinner.text = "Generating types...";
|
|
467
|
+
const { tables, outputDir } = await generateTypes();
|
|
468
|
+
if (tables.length === 0) {
|
|
469
|
+
spinner.warn("No tables found in schema");
|
|
470
|
+
console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
spinner.succeed(`Types generated for ${tables.length} table(s)`);
|
|
474
|
+
console.log("\n" + chalk2.green("\u2713") + " Tables:");
|
|
475
|
+
for (const table of tables) {
|
|
476
|
+
console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
|
|
477
|
+
}
|
|
478
|
+
const relativeOutput = path3.relative(process.cwd(), outputDir);
|
|
479
|
+
console.log("\n" + chalk2.green("\u2713") + " Generated files:");
|
|
480
|
+
console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
|
|
481
|
+
console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
|
|
482
|
+
console.log(chalk2.dim(` ${relativeOutput}/index.ts
|
|
483
|
+
`));
|
|
484
|
+
} catch (error) {
|
|
485
|
+
spinner.fail("Failed to generate types");
|
|
486
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export {
|
|
492
|
+
getCredentials,
|
|
493
|
+
requireAuth,
|
|
494
|
+
saveCredentials,
|
|
495
|
+
clearCredentials,
|
|
496
|
+
loadConfig,
|
|
497
|
+
resolvePath,
|
|
498
|
+
detectFramework,
|
|
499
|
+
getFrameworkDevCommand,
|
|
500
|
+
generateTypes,
|
|
501
|
+
generateCommand
|
|
502
|
+
};
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
// src/commands/generate.ts
|
|
2
|
+
import chalk2 from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import fs3 from "fs-extra";
|
|
5
|
+
import path3 from "path";
|
|
6
|
+
|
|
7
|
+
// src/utils/auth.ts
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import fs from "fs-extra";
|
|
10
|
+
import path from "path";
|
|
11
|
+
var CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || "", ".tether");
|
|
12
|
+
var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
13
|
+
async function getCredentials() {
|
|
14
|
+
try {
|
|
15
|
+
if (await fs.pathExists(CREDENTIALS_FILE)) {
|
|
16
|
+
return await fs.readJSON(CREDENTIALS_FILE);
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
async function requireAuth() {
|
|
23
|
+
const credentials = await getCredentials();
|
|
24
|
+
if (!credentials) {
|
|
25
|
+
console.error(chalk.red("\n\u2717 Not logged in\n"));
|
|
26
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (credentials.expiresAt) {
|
|
30
|
+
const expiresAt = new Date(credentials.expiresAt);
|
|
31
|
+
if (/* @__PURE__ */ new Date() > expiresAt) {
|
|
32
|
+
console.error(chalk.red("\n\u2717 Session expired\n"));
|
|
33
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return credentials;
|
|
38
|
+
}
|
|
39
|
+
async function saveCredentials(credentials) {
|
|
40
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
41
|
+
await fs.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
|
|
42
|
+
}
|
|
43
|
+
async function clearCredentials() {
|
|
44
|
+
try {
|
|
45
|
+
await fs.remove(CREDENTIALS_FILE);
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/utils/config.ts
|
|
51
|
+
import fs2 from "fs-extra";
|
|
52
|
+
import path2 from "path";
|
|
53
|
+
var DEFAULT_CONFIG = {
|
|
54
|
+
schema: "./tether/schema.ts",
|
|
55
|
+
functions: "./tether/functions",
|
|
56
|
+
output: "./tether/_generated",
|
|
57
|
+
dev: {
|
|
58
|
+
port: 3001,
|
|
59
|
+
host: "localhost"
|
|
60
|
+
},
|
|
61
|
+
database: {
|
|
62
|
+
walMode: true
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
66
|
+
const configPath = path2.resolve(cwd, "tether.config.ts");
|
|
67
|
+
if (!await fs2.pathExists(configPath)) {
|
|
68
|
+
return DEFAULT_CONFIG;
|
|
69
|
+
}
|
|
70
|
+
const configSource = await fs2.readFile(configPath, "utf-8");
|
|
71
|
+
const config = {};
|
|
72
|
+
const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
|
|
73
|
+
if (schemaMatch) {
|
|
74
|
+
config.schema = schemaMatch[1];
|
|
75
|
+
}
|
|
76
|
+
const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
|
|
77
|
+
if (functionsMatch) {
|
|
78
|
+
config.functions = functionsMatch[1];
|
|
79
|
+
}
|
|
80
|
+
const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
|
|
81
|
+
if (outputMatch) {
|
|
82
|
+
config.output = outputMatch[1];
|
|
83
|
+
}
|
|
84
|
+
const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
|
|
85
|
+
if (envMatch) {
|
|
86
|
+
config.environment = envMatch[1];
|
|
87
|
+
}
|
|
88
|
+
const portMatch = configSource.match(/port\s*:\s*(\d+)/);
|
|
89
|
+
if (portMatch) {
|
|
90
|
+
config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
|
|
91
|
+
}
|
|
92
|
+
const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
|
|
93
|
+
if (hostMatch) {
|
|
94
|
+
config.dev = { ...config.dev, host: hostMatch[1] };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
...DEFAULT_CONFIG,
|
|
98
|
+
...config,
|
|
99
|
+
dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
|
|
100
|
+
database: { ...DEFAULT_CONFIG.database, ...config.database }
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function resolvePath(configPath, cwd = process.cwd()) {
|
|
104
|
+
const normalised = configPath.replace(/^\.\//, "");
|
|
105
|
+
return path2.resolve(cwd, normalised);
|
|
106
|
+
}
|
|
107
|
+
async function detectFramework(cwd = process.cwd()) {
|
|
108
|
+
const packageJsonPath = path2.resolve(cwd, "package.json");
|
|
109
|
+
if (!await fs2.pathExists(packageJsonPath)) {
|
|
110
|
+
return "unknown";
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const packageJson = await fs2.readJson(packageJsonPath);
|
|
114
|
+
const deps = {
|
|
115
|
+
...packageJson.dependencies,
|
|
116
|
+
...packageJson.devDependencies
|
|
117
|
+
};
|
|
118
|
+
if (deps.nuxt || deps["@nuxt/kit"]) {
|
|
119
|
+
return "nuxt";
|
|
120
|
+
}
|
|
121
|
+
if (deps.next) {
|
|
122
|
+
return "next";
|
|
123
|
+
}
|
|
124
|
+
if (deps["@sveltejs/kit"]) {
|
|
125
|
+
return "sveltekit";
|
|
126
|
+
}
|
|
127
|
+
if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
|
|
128
|
+
return "vite";
|
|
129
|
+
}
|
|
130
|
+
return "vanilla";
|
|
131
|
+
} catch {
|
|
132
|
+
return "unknown";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function getFrameworkDevCommand(framework) {
|
|
136
|
+
switch (framework) {
|
|
137
|
+
case "nuxt":
|
|
138
|
+
return "nuxt dev";
|
|
139
|
+
case "next":
|
|
140
|
+
return "next dev";
|
|
141
|
+
case "sveltekit":
|
|
142
|
+
return "vite dev";
|
|
143
|
+
case "vite":
|
|
144
|
+
return "vite dev";
|
|
145
|
+
case "vanilla":
|
|
146
|
+
return null;
|
|
147
|
+
// No default dev server for vanilla
|
|
148
|
+
default:
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/commands/generate.ts
|
|
154
|
+
function parseSchemaFile(source) {
|
|
155
|
+
const tables = [];
|
|
156
|
+
const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
|
|
157
|
+
if (!schemaMatch) return tables;
|
|
158
|
+
const schemaContent = schemaMatch[1];
|
|
159
|
+
const tableStartRegex = /(\w+)\s*:\s*\{/g;
|
|
160
|
+
let match;
|
|
161
|
+
while ((match = tableStartRegex.exec(schemaContent)) !== null) {
|
|
162
|
+
const tableName = match[1];
|
|
163
|
+
const startOffset = match.index + match[0].length;
|
|
164
|
+
let braceCount = 1;
|
|
165
|
+
let endOffset = startOffset;
|
|
166
|
+
for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
|
|
167
|
+
const char = schemaContent[i];
|
|
168
|
+
if (char === "{") braceCount++;
|
|
169
|
+
else if (char === "}") braceCount--;
|
|
170
|
+
endOffset = i;
|
|
171
|
+
}
|
|
172
|
+
const columnsContent = schemaContent.slice(startOffset, endOffset);
|
|
173
|
+
const columns = parseColumns(columnsContent);
|
|
174
|
+
tables.push({ name: tableName, columns });
|
|
175
|
+
}
|
|
176
|
+
return tables;
|
|
177
|
+
}
|
|
178
|
+
function parseColumns(content) {
|
|
179
|
+
const columns = [];
|
|
180
|
+
const columnRegex = /(\w+)\s*:\s*(\w+)(?:<([^>]+)>)?\s*\(\s*\)((?:[^,\n}]|\[.*?\])*)/g;
|
|
181
|
+
let match;
|
|
182
|
+
while ((match = columnRegex.exec(content)) !== null) {
|
|
183
|
+
const name = match[1];
|
|
184
|
+
const schemaType = match[2];
|
|
185
|
+
const genericType = match[3];
|
|
186
|
+
const modifiers = match[4] || "";
|
|
187
|
+
const nullable = !modifiers.includes(".notNull()");
|
|
188
|
+
const oneOfMatch = modifiers.match(/\.oneOf\s*\(\s*\[(.*?)\]\s*\)/);
|
|
189
|
+
let oneOf;
|
|
190
|
+
if (oneOfMatch) {
|
|
191
|
+
oneOf = [...oneOfMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
192
|
+
}
|
|
193
|
+
columns.push({
|
|
194
|
+
name,
|
|
195
|
+
type: schemaTypeToTS(schemaType),
|
|
196
|
+
nullable,
|
|
197
|
+
jsonType: genericType,
|
|
198
|
+
// Store the generic type for json columns
|
|
199
|
+
oneOf
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return columns;
|
|
203
|
+
}
|
|
204
|
+
function schemaTypeToTS(schemaType) {
|
|
205
|
+
switch (schemaType) {
|
|
206
|
+
case "text":
|
|
207
|
+
return "string";
|
|
208
|
+
case "integer":
|
|
209
|
+
return "number";
|
|
210
|
+
case "real":
|
|
211
|
+
return "number";
|
|
212
|
+
case "boolean":
|
|
213
|
+
return "boolean";
|
|
214
|
+
case "timestamp":
|
|
215
|
+
return "string";
|
|
216
|
+
case "json":
|
|
217
|
+
return "unknown";
|
|
218
|
+
case "blob":
|
|
219
|
+
return "Uint8Array";
|
|
220
|
+
case "asset":
|
|
221
|
+
return "TetherAsset";
|
|
222
|
+
// Asset object returned by API
|
|
223
|
+
default:
|
|
224
|
+
return "unknown";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function tableNameToInterface(tableName) {
|
|
228
|
+
let name = tableName;
|
|
229
|
+
if (name.endsWith("ies")) {
|
|
230
|
+
name = name.slice(0, -3) + "y";
|
|
231
|
+
} else if (name.endsWith("s") && !name.endsWith("ss")) {
|
|
232
|
+
name = name.slice(0, -1);
|
|
233
|
+
}
|
|
234
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
235
|
+
}
|
|
236
|
+
function generateDbFile(tables) {
|
|
237
|
+
const jsonTypes = /* @__PURE__ */ new Set();
|
|
238
|
+
for (const table of tables) {
|
|
239
|
+
for (const col of table.columns) {
|
|
240
|
+
if (col.jsonType) {
|
|
241
|
+
jsonTypes.add(col.jsonType.replace(/\[\]$/, ""));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const lines = [
|
|
246
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
247
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
248
|
+
"",
|
|
249
|
+
"import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
|
|
250
|
+
"import {",
|
|
251
|
+
" query as baseQuery,",
|
|
252
|
+
" mutation as baseMutation,",
|
|
253
|
+
" type QueryDefinition,",
|
|
254
|
+
" type MutationDefinition,",
|
|
255
|
+
" z,",
|
|
256
|
+
"} from '@tthr/server';"
|
|
257
|
+
];
|
|
258
|
+
if (jsonTypes.size > 0) {
|
|
259
|
+
const typeImports = Array.from(jsonTypes).sort().join(", ");
|
|
260
|
+
lines.push(`import type { ${typeImports} } from '../schema';`);
|
|
261
|
+
}
|
|
262
|
+
lines.push("");
|
|
263
|
+
lines.push("// Asset type returned by the API for asset columns");
|
|
264
|
+
lines.push("export interface TetherAsset {");
|
|
265
|
+
lines.push(" id: string;");
|
|
266
|
+
lines.push(" filename: string;");
|
|
267
|
+
lines.push(" contentType: string;");
|
|
268
|
+
lines.push(" size: number;");
|
|
269
|
+
lines.push(" url: string;");
|
|
270
|
+
lines.push(" createdAt: string;");
|
|
271
|
+
lines.push("}");
|
|
272
|
+
lines.push("");
|
|
273
|
+
for (const table of tables) {
|
|
274
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
275
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
276
|
+
lines.push(" _id: string;");
|
|
277
|
+
for (const col of table.columns) {
|
|
278
|
+
let colType;
|
|
279
|
+
if (col.oneOf && col.oneOf.length > 0) {
|
|
280
|
+
colType = col.oneOf.map((v) => `'${v}'`).join(" | ");
|
|
281
|
+
} else {
|
|
282
|
+
colType = col.jsonType || col.type;
|
|
283
|
+
}
|
|
284
|
+
const typeStr = col.nullable ? `${colType} | null` : colType;
|
|
285
|
+
const optional = col.nullable ? "?" : "";
|
|
286
|
+
lines.push(` ${col.name}${optional}: ${typeStr};`);
|
|
287
|
+
}
|
|
288
|
+
lines.push(" _createdAt: string;");
|
|
289
|
+
lines.push(" _updatedAt?: string | null;");
|
|
290
|
+
lines.push(" _deletedAt?: string | null;");
|
|
291
|
+
lines.push("}");
|
|
292
|
+
lines.push("");
|
|
293
|
+
}
|
|
294
|
+
lines.push("export interface Schema {");
|
|
295
|
+
for (const table of tables) {
|
|
296
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
297
|
+
lines.push(` ${table.name}: ${interfaceName};`);
|
|
298
|
+
}
|
|
299
|
+
lines.push("}");
|
|
300
|
+
lines.push("");
|
|
301
|
+
lines.push("// Database client with typed tables");
|
|
302
|
+
lines.push("// This is a proxy that will be populated by the Tether runtime");
|
|
303
|
+
lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
|
|
304
|
+
lines.push("");
|
|
305
|
+
lines.push("// ============================================================================");
|
|
306
|
+
lines.push("// Typed function wrappers - use these instead of importing from @tthr/server");
|
|
307
|
+
lines.push("// This ensures the `db` parameter in handlers is properly typed with Schema");
|
|
308
|
+
lines.push("// ============================================================================");
|
|
309
|
+
lines.push("");
|
|
310
|
+
lines.push("/**");
|
|
311
|
+
lines.push(" * Define a query function with typed database access.");
|
|
312
|
+
lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
|
|
313
|
+
lines.push(" */");
|
|
314
|
+
lines.push("export function query<TArgs = void, TResult = unknown>(");
|
|
315
|
+
lines.push(" definition: QueryDefinition<TArgs, TResult, Schema>");
|
|
316
|
+
lines.push("): QueryDefinition<TArgs, TResult, Schema> {");
|
|
317
|
+
lines.push(" return baseQuery(definition);");
|
|
318
|
+
lines.push("}");
|
|
319
|
+
lines.push("");
|
|
320
|
+
lines.push("/**");
|
|
321
|
+
lines.push(" * Define a mutation function with typed database access.");
|
|
322
|
+
lines.push(" * The `db` parameter in the handler will have full type safety for your schema.");
|
|
323
|
+
lines.push(" */");
|
|
324
|
+
lines.push("export function mutation<TArgs = void, TResult = unknown>(");
|
|
325
|
+
lines.push(" definition: MutationDefinition<TArgs, TResult, Schema>");
|
|
326
|
+
lines.push("): MutationDefinition<TArgs, TResult, Schema> {");
|
|
327
|
+
lines.push(" return baseMutation(definition);");
|
|
328
|
+
lines.push("}");
|
|
329
|
+
lines.push("");
|
|
330
|
+
lines.push("// Re-export z for convenience");
|
|
331
|
+
lines.push("export { z };");
|
|
332
|
+
if (jsonTypes.size > 0) {
|
|
333
|
+
lines.push("");
|
|
334
|
+
lines.push("// Re-export JSON schema types");
|
|
335
|
+
const typeExports = Array.from(jsonTypes).sort().join(", ");
|
|
336
|
+
lines.push(`export type { ${typeExports} } from '../schema';`);
|
|
337
|
+
}
|
|
338
|
+
lines.push("");
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
}
|
|
341
|
+
async function parseFunctionsDir(functionsDir) {
|
|
342
|
+
const functions = [];
|
|
343
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
344
|
+
return functions;
|
|
345
|
+
}
|
|
346
|
+
const files = await fs3.readdir(functionsDir);
|
|
347
|
+
for (const file of files) {
|
|
348
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
349
|
+
const filePath = path3.join(functionsDir, file);
|
|
350
|
+
const stat = await fs3.stat(filePath);
|
|
351
|
+
if (!stat.isFile()) continue;
|
|
352
|
+
const moduleName = file.replace(/\.(ts|js)$/, "");
|
|
353
|
+
const source = await fs3.readFile(filePath, "utf-8");
|
|
354
|
+
const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation)\s*\(/g;
|
|
355
|
+
let match;
|
|
356
|
+
while ((match = exportRegex.exec(source)) !== null) {
|
|
357
|
+
functions.push({
|
|
358
|
+
name: match[1],
|
|
359
|
+
moduleName
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return functions;
|
|
364
|
+
}
|
|
365
|
+
async function generateApiFile(functionsDir) {
|
|
366
|
+
const functions = await parseFunctionsDir(functionsDir);
|
|
367
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
368
|
+
for (const fn of functions) {
|
|
369
|
+
if (!moduleMap.has(fn.moduleName)) {
|
|
370
|
+
moduleMap.set(fn.moduleName, []);
|
|
371
|
+
}
|
|
372
|
+
moduleMap.get(fn.moduleName).push(fn.name);
|
|
373
|
+
}
|
|
374
|
+
const lines = [
|
|
375
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
376
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
377
|
+
"",
|
|
378
|
+
"import { createApiProxy } from '@tthr/client';",
|
|
379
|
+
"",
|
|
380
|
+
"/**",
|
|
381
|
+
" * API function reference type for useQuery/useMutation.",
|
|
382
|
+
' * The _name property contains the function path (e.g., "users.list").',
|
|
383
|
+
" */",
|
|
384
|
+
"export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
|
|
385
|
+
" _name: string;",
|
|
386
|
+
" _args?: TArgs;",
|
|
387
|
+
" _result?: TResult;",
|
|
388
|
+
"}",
|
|
389
|
+
""
|
|
390
|
+
];
|
|
391
|
+
if (moduleMap.size > 0) {
|
|
392
|
+
for (const [moduleName, fnNames] of moduleMap) {
|
|
393
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
394
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
395
|
+
for (const fnName of fnNames) {
|
|
396
|
+
lines.push(` ${fnName}: ApiFunction;`);
|
|
397
|
+
}
|
|
398
|
+
lines.push("}");
|
|
399
|
+
lines.push("");
|
|
400
|
+
}
|
|
401
|
+
lines.push("export interface Api {");
|
|
402
|
+
for (const moduleName of moduleMap.keys()) {
|
|
403
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
404
|
+
lines.push(` ${moduleName}: ${interfaceName};`);
|
|
405
|
+
}
|
|
406
|
+
lines.push("}");
|
|
407
|
+
} else {
|
|
408
|
+
lines.push("/**");
|
|
409
|
+
lines.push(" * Flexible API type that allows access to any function path.");
|
|
410
|
+
lines.push(" * Functions are accessed as api.moduleName.functionName");
|
|
411
|
+
lines.push(" * The actual types depend on your function definitions.");
|
|
412
|
+
lines.push(" */");
|
|
413
|
+
lines.push("export type Api = {");
|
|
414
|
+
lines.push(" [module: string]: {");
|
|
415
|
+
lines.push(" [fn: string]: ApiFunction;");
|
|
416
|
+
lines.push(" };");
|
|
417
|
+
lines.push("};");
|
|
418
|
+
}
|
|
419
|
+
lines.push("");
|
|
420
|
+
lines.push("// API client proxy - provides typed access to your functions");
|
|
421
|
+
lines.push("// On the client: returns { _name } references for useQuery/useMutation");
|
|
422
|
+
lines.push("// In Tether functions: calls the actual function implementation");
|
|
423
|
+
lines.push("export const api = createApiProxy<Api>();");
|
|
424
|
+
lines.push("");
|
|
425
|
+
return lines.join("\n");
|
|
426
|
+
}
|
|
427
|
+
async function generateTypes(options = {}) {
|
|
428
|
+
const config = await loadConfig();
|
|
429
|
+
const schemaPath = resolvePath(config.schema);
|
|
430
|
+
const outputDir = resolvePath(config.output);
|
|
431
|
+
const functionsDir = resolvePath(config.functions);
|
|
432
|
+
if (!await fs3.pathExists(schemaPath)) {
|
|
433
|
+
throw new Error(`Schema file not found: ${schemaPath}`);
|
|
434
|
+
}
|
|
435
|
+
const schemaSource = await fs3.readFile(schemaPath, "utf-8");
|
|
436
|
+
const tables = parseSchemaFile(schemaSource);
|
|
437
|
+
await fs3.ensureDir(outputDir);
|
|
438
|
+
await fs3.writeFile(
|
|
439
|
+
path3.join(outputDir, "db.ts"),
|
|
440
|
+
generateDbFile(tables)
|
|
441
|
+
);
|
|
442
|
+
await fs3.writeFile(
|
|
443
|
+
path3.join(outputDir, "api.ts"),
|
|
444
|
+
await generateApiFile(functionsDir)
|
|
445
|
+
);
|
|
446
|
+
await fs3.writeFile(
|
|
447
|
+
path3.join(outputDir, "index.ts"),
|
|
448
|
+
`// Auto-generated by Tether CLI - do not edit manually
|
|
449
|
+
export * from './db';
|
|
450
|
+
export * from './api';
|
|
451
|
+
`
|
|
452
|
+
);
|
|
453
|
+
return { tables, outputDir };
|
|
454
|
+
}
|
|
455
|
+
async function generateCommand() {
|
|
456
|
+
await requireAuth();
|
|
457
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
458
|
+
if (!await fs3.pathExists(configPath)) {
|
|
459
|
+
console.log(chalk2.red("\nError: Not a Tether project"));
|
|
460
|
+
console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
|
|
464
|
+
const spinner = ora("Reading schema...").start();
|
|
465
|
+
try {
|
|
466
|
+
spinner.text = "Generating types...";
|
|
467
|
+
const { tables, outputDir } = await generateTypes();
|
|
468
|
+
if (tables.length === 0) {
|
|
469
|
+
spinner.warn("No tables found in schema");
|
|
470
|
+
console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
spinner.succeed(`Types generated for ${tables.length} table(s)`);
|
|
474
|
+
console.log("\n" + chalk2.green("\u2713") + " Tables:");
|
|
475
|
+
for (const table of tables) {
|
|
476
|
+
console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
|
|
477
|
+
}
|
|
478
|
+
const relativeOutput = path3.relative(process.cwd(), outputDir);
|
|
479
|
+
console.log("\n" + chalk2.green("\u2713") + " Generated files:");
|
|
480
|
+
console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
|
|
481
|
+
console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
|
|
482
|
+
console.log(chalk2.dim(` ${relativeOutput}/index.ts
|
|
483
|
+
`));
|
|
484
|
+
} catch (error) {
|
|
485
|
+
spinner.fail("Failed to generate types");
|
|
486
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export {
|
|
492
|
+
getCredentials,
|
|
493
|
+
requireAuth,
|
|
494
|
+
saveCredentials,
|
|
495
|
+
clearCredentials,
|
|
496
|
+
loadConfig,
|
|
497
|
+
resolvePath,
|
|
498
|
+
detectFramework,
|
|
499
|
+
getFrameworkDevCommand,
|
|
500
|
+
generateTypes,
|
|
501
|
+
generateCommand
|
|
502
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
requireAuth,
|
|
11
11
|
resolvePath,
|
|
12
12
|
saveCredentials
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-GKELUQ44.js";
|
|
14
14
|
|
|
15
15
|
// src/index.ts
|
|
16
16
|
import { Command } from "commander";
|
|
@@ -274,7 +274,7 @@ function parseSchema(source) {
|
|
|
274
274
|
${normalisedContent.split("\n").join("\n ")}
|
|
275
275
|
}`;
|
|
276
276
|
const columns = {};
|
|
277
|
-
const columnRegex = /(\w+)\s*:\s*(\w+)(?:<[^>]*>)?\s*\(\s*\)([^,\n]*)/g;
|
|
277
|
+
const columnRegex = /(\w+)\s*:\s*(\w+)(?:<[^>]*>)?\s*\(\s*\)((?:\[.*?\]|[^,\n}])*)/g;
|
|
278
278
|
let colMatch;
|
|
279
279
|
while ((colMatch = columnRegex.exec(columnsContent)) !== null) {
|
|
280
280
|
const colName = colMatch[1];
|
|
@@ -290,12 +290,18 @@ function parseSchema(source) {
|
|
|
290
290
|
`Cannot use column name "id" in table "${tableName}". Tether automatically adds a "_id" primary key column. Use a different column name or rely on the system-provided "_id".`
|
|
291
291
|
);
|
|
292
292
|
}
|
|
293
|
+
const oneOfMatch = modifiers.match(/\.oneOf\s*\(\s*\[(.*?)\]\s*\)/);
|
|
294
|
+
let oneOf;
|
|
295
|
+
if (oneOfMatch) {
|
|
296
|
+
oneOf = [...oneOfMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
297
|
+
}
|
|
293
298
|
columns[colName] = {
|
|
294
299
|
type: colType,
|
|
295
300
|
notNull: modifiers.includes(".notNull()"),
|
|
296
301
|
unique: modifiers.includes(".unique()"),
|
|
297
302
|
hasDefault: modifiers.includes(".default("),
|
|
298
|
-
references: modifiers.match(/\.references\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1]
|
|
303
|
+
references: modifiers.match(/\.references\s*\(\s*['"]([^'"]+)['"]\s*\)/)?.[1],
|
|
304
|
+
oneOf
|
|
299
305
|
};
|
|
300
306
|
}
|
|
301
307
|
tables.push({ name: tableName, columns, source: tableSource });
|
|
@@ -337,6 +343,10 @@ function buildColumnSql(colName, def, forAlterTable = false) {
|
|
|
337
343
|
const [refTable, refCol] = def.references.split(".");
|
|
338
344
|
colSql += ` REFERENCES "${refTable}"("${refCol || "_id"}")`;
|
|
339
345
|
}
|
|
346
|
+
if (def.oneOf && def.oneOf.length > 0) {
|
|
347
|
+
const values = def.oneOf.map((v) => `'${v}'`).join(", ");
|
|
348
|
+
colSql += ` CHECK ("${colName}" IN (${values}))`;
|
|
349
|
+
}
|
|
340
350
|
return colSql;
|
|
341
351
|
}
|
|
342
352
|
function generateSchemaSQL(tables) {
|
|
@@ -755,7 +765,7 @@ export default defineSchema({
|
|
|
755
765
|
|
|
756
766
|
// Example: comments table
|
|
757
767
|
comments: {
|
|
758
|
-
postId: text().notNull().references('posts
|
|
768
|
+
postId: text().notNull().references('posts'),
|
|
759
769
|
content: text().notNull(),
|
|
760
770
|
authorId: text().notNull(),
|
|
761
771
|
},
|
|
@@ -1534,7 +1544,7 @@ async function runGenerate(spinner) {
|
|
|
1534
1544
|
}
|
|
1535
1545
|
isGenerating = true;
|
|
1536
1546
|
try {
|
|
1537
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1547
|
+
const { generateTypes: generateTypes2 } = await import("./generate-5AY6JNCG.js");
|
|
1538
1548
|
spinner.text = "Regenerating types...";
|
|
1539
1549
|
await generateTypes2({ silent: true });
|
|
1540
1550
|
spinner.succeed("Types regenerated");
|
|
@@ -1719,7 +1729,7 @@ async function devCommand(options) {
|
|
|
1719
1729
|
}
|
|
1720
1730
|
}
|
|
1721
1731
|
spinner.text = "Generating types...";
|
|
1722
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1732
|
+
const { generateTypes: generateTypes2 } = await import("./generate-5AY6JNCG.js");
|
|
1723
1733
|
await generateTypes2({ silent: true });
|
|
1724
1734
|
spinner.succeed("Types generated");
|
|
1725
1735
|
spinner.start("Setting up file watchers...");
|