tthr 0.0.42 → 0.0.45
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-2IDRK6NG.js +416 -0
- package/dist/chunk-TU5LOAQ5.js +418 -0
- package/dist/generate-F7VDD6JW.js +8 -0
- package/dist/generate-HRIVNCH3.js +8 -0
- package/dist/index.js +41 -53
- package/package.json +1 -1
|
@@ -0,0 +1,416 @@
|
|
|
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 modifiers = match[3] || "";
|
|
186
|
+
const nullable = !modifiers.includes(".notNull()");
|
|
187
|
+
columns.push({
|
|
188
|
+
name,
|
|
189
|
+
type: schemaTypeToTS(schemaType),
|
|
190
|
+
nullable
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return columns;
|
|
194
|
+
}
|
|
195
|
+
function schemaTypeToTS(schemaType) {
|
|
196
|
+
switch (schemaType) {
|
|
197
|
+
case "text":
|
|
198
|
+
return "string";
|
|
199
|
+
case "integer":
|
|
200
|
+
return "number";
|
|
201
|
+
case "real":
|
|
202
|
+
return "number";
|
|
203
|
+
case "boolean":
|
|
204
|
+
return "boolean";
|
|
205
|
+
case "timestamp":
|
|
206
|
+
return "string";
|
|
207
|
+
case "json":
|
|
208
|
+
return "unknown";
|
|
209
|
+
case "blob":
|
|
210
|
+
return "Uint8Array";
|
|
211
|
+
default:
|
|
212
|
+
return "unknown";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function tableNameToInterface(tableName) {
|
|
216
|
+
let name = tableName;
|
|
217
|
+
if (name.endsWith("ies")) {
|
|
218
|
+
name = name.slice(0, -3) + "y";
|
|
219
|
+
} else if (name.endsWith("s") && !name.endsWith("ss")) {
|
|
220
|
+
name = name.slice(0, -1);
|
|
221
|
+
}
|
|
222
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
223
|
+
}
|
|
224
|
+
function generateDbFile(tables) {
|
|
225
|
+
const lines = [
|
|
226
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
227
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
228
|
+
"",
|
|
229
|
+
"import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
|
|
230
|
+
""
|
|
231
|
+
];
|
|
232
|
+
for (const table of tables) {
|
|
233
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
234
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
235
|
+
for (const col of table.columns) {
|
|
236
|
+
const typeStr = col.nullable ? `${col.type} | null` : col.type;
|
|
237
|
+
lines.push(` ${col.name}: ${typeStr};`);
|
|
238
|
+
}
|
|
239
|
+
lines.push("}");
|
|
240
|
+
lines.push("");
|
|
241
|
+
}
|
|
242
|
+
lines.push("export interface Schema {");
|
|
243
|
+
for (const table of tables) {
|
|
244
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
245
|
+
lines.push(` ${table.name}: ${interfaceName};`);
|
|
246
|
+
}
|
|
247
|
+
lines.push("}");
|
|
248
|
+
lines.push("");
|
|
249
|
+
lines.push("// Database client with typed tables");
|
|
250
|
+
lines.push("// This is a proxy that will be populated by the Tether runtime");
|
|
251
|
+
lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
|
|
252
|
+
lines.push("");
|
|
253
|
+
return lines.join("\n");
|
|
254
|
+
}
|
|
255
|
+
async function parseFunctionsDir(functionsDir) {
|
|
256
|
+
const functions = [];
|
|
257
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
258
|
+
return functions;
|
|
259
|
+
}
|
|
260
|
+
const files = await fs3.readdir(functionsDir);
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
263
|
+
const filePath = path3.join(functionsDir, file);
|
|
264
|
+
const stat = await fs3.stat(filePath);
|
|
265
|
+
if (!stat.isFile()) continue;
|
|
266
|
+
const moduleName = file.replace(/\.(ts|js)$/, "");
|
|
267
|
+
const source = await fs3.readFile(filePath, "utf-8");
|
|
268
|
+
const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation|action)\s*\(/g;
|
|
269
|
+
let match;
|
|
270
|
+
while ((match = exportRegex.exec(source)) !== null) {
|
|
271
|
+
functions.push({
|
|
272
|
+
name: match[1],
|
|
273
|
+
moduleName
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return functions;
|
|
278
|
+
}
|
|
279
|
+
async function generateApiFile(functionsDir) {
|
|
280
|
+
const functions = await parseFunctionsDir(functionsDir);
|
|
281
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
282
|
+
for (const fn of functions) {
|
|
283
|
+
if (!moduleMap.has(fn.moduleName)) {
|
|
284
|
+
moduleMap.set(fn.moduleName, []);
|
|
285
|
+
}
|
|
286
|
+
moduleMap.get(fn.moduleName).push(fn.name);
|
|
287
|
+
}
|
|
288
|
+
const lines = [
|
|
289
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
290
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
291
|
+
"",
|
|
292
|
+
"import { createApiProxy } from '@tthr/client';",
|
|
293
|
+
"",
|
|
294
|
+
"/**",
|
|
295
|
+
" * API function reference type for useQuery/useMutation.",
|
|
296
|
+
' * The _name property contains the function path (e.g., "users.list").',
|
|
297
|
+
" */",
|
|
298
|
+
"export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
|
|
299
|
+
" _name: string;",
|
|
300
|
+
" _args?: TArgs;",
|
|
301
|
+
" _result?: TResult;",
|
|
302
|
+
"}",
|
|
303
|
+
""
|
|
304
|
+
];
|
|
305
|
+
if (moduleMap.size > 0) {
|
|
306
|
+
for (const [moduleName, fnNames] of moduleMap) {
|
|
307
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
308
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
309
|
+
for (const fnName of fnNames) {
|
|
310
|
+
lines.push(` ${fnName}: ApiFunction;`);
|
|
311
|
+
}
|
|
312
|
+
lines.push("}");
|
|
313
|
+
lines.push("");
|
|
314
|
+
}
|
|
315
|
+
lines.push("export interface Api {");
|
|
316
|
+
for (const moduleName of moduleMap.keys()) {
|
|
317
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
318
|
+
lines.push(` ${moduleName}: ${interfaceName};`);
|
|
319
|
+
}
|
|
320
|
+
lines.push("}");
|
|
321
|
+
} else {
|
|
322
|
+
lines.push("/**");
|
|
323
|
+
lines.push(" * Flexible API type that allows access to any function path.");
|
|
324
|
+
lines.push(" * Functions are accessed as api.moduleName.functionName");
|
|
325
|
+
lines.push(" * The actual types depend on your function definitions.");
|
|
326
|
+
lines.push(" */");
|
|
327
|
+
lines.push("export type Api = {");
|
|
328
|
+
lines.push(" [module: string]: {");
|
|
329
|
+
lines.push(" [fn: string]: ApiFunction;");
|
|
330
|
+
lines.push(" };");
|
|
331
|
+
lines.push("};");
|
|
332
|
+
}
|
|
333
|
+
lines.push("");
|
|
334
|
+
lines.push("// API client proxy - provides typed access to your functions");
|
|
335
|
+
lines.push("// On the client: returns { _name } references for useQuery/useMutation");
|
|
336
|
+
lines.push("// In Tether functions: calls the actual function implementation");
|
|
337
|
+
lines.push("export const api = createApiProxy<Api>();");
|
|
338
|
+
lines.push("");
|
|
339
|
+
return lines.join("\n");
|
|
340
|
+
}
|
|
341
|
+
async function generateTypes(options = {}) {
|
|
342
|
+
const config = await loadConfig();
|
|
343
|
+
const schemaPath = resolvePath(config.schema);
|
|
344
|
+
const outputDir = resolvePath(config.output);
|
|
345
|
+
const functionsDir = resolvePath(config.functions);
|
|
346
|
+
if (!await fs3.pathExists(schemaPath)) {
|
|
347
|
+
throw new Error(`Schema file not found: ${schemaPath}`);
|
|
348
|
+
}
|
|
349
|
+
const schemaSource = await fs3.readFile(schemaPath, "utf-8");
|
|
350
|
+
const tables = parseSchemaFile(schemaSource);
|
|
351
|
+
await fs3.ensureDir(outputDir);
|
|
352
|
+
await fs3.writeFile(
|
|
353
|
+
path3.join(outputDir, "db.ts"),
|
|
354
|
+
generateDbFile(tables)
|
|
355
|
+
);
|
|
356
|
+
await fs3.writeFile(
|
|
357
|
+
path3.join(outputDir, "api.ts"),
|
|
358
|
+
await generateApiFile(functionsDir)
|
|
359
|
+
);
|
|
360
|
+
await fs3.writeFile(
|
|
361
|
+
path3.join(outputDir, "index.ts"),
|
|
362
|
+
`// Auto-generated by Tether CLI - do not edit manually
|
|
363
|
+
export * from './db';
|
|
364
|
+
export * from './api';
|
|
365
|
+
`
|
|
366
|
+
);
|
|
367
|
+
return { tables, outputDir };
|
|
368
|
+
}
|
|
369
|
+
async function generateCommand() {
|
|
370
|
+
await requireAuth();
|
|
371
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
372
|
+
if (!await fs3.pathExists(configPath)) {
|
|
373
|
+
console.log(chalk2.red("\nError: Not a Tether project"));
|
|
374
|
+
console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
|
|
378
|
+
const spinner = ora("Reading schema...").start();
|
|
379
|
+
try {
|
|
380
|
+
spinner.text = "Generating types...";
|
|
381
|
+
const { tables, outputDir } = await generateTypes();
|
|
382
|
+
if (tables.length === 0) {
|
|
383
|
+
spinner.warn("No tables found in schema");
|
|
384
|
+
console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
spinner.succeed(`Types generated for ${tables.length} table(s)`);
|
|
388
|
+
console.log("\n" + chalk2.green("\u2713") + " Tables:");
|
|
389
|
+
for (const table of tables) {
|
|
390
|
+
console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
|
|
391
|
+
}
|
|
392
|
+
const relativeOutput = path3.relative(process.cwd(), outputDir);
|
|
393
|
+
console.log("\n" + chalk2.green("\u2713") + " Generated files:");
|
|
394
|
+
console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
|
|
395
|
+
console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
|
|
396
|
+
console.log(chalk2.dim(` ${relativeOutput}/index.ts
|
|
397
|
+
`));
|
|
398
|
+
} catch (error) {
|
|
399
|
+
spinner.fail("Failed to generate types");
|
|
400
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export {
|
|
406
|
+
getCredentials,
|
|
407
|
+
requireAuth,
|
|
408
|
+
saveCredentials,
|
|
409
|
+
clearCredentials,
|
|
410
|
+
loadConfig,
|
|
411
|
+
resolvePath,
|
|
412
|
+
detectFramework,
|
|
413
|
+
getFrameworkDevCommand,
|
|
414
|
+
generateTypes,
|
|
415
|
+
generateCommand
|
|
416
|
+
};
|
|
@@ -0,0 +1,418 @@
|
|
|
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 modifiers = match[3] || "";
|
|
186
|
+
const nullable = !modifiers.includes(".notNull()");
|
|
187
|
+
columns.push({
|
|
188
|
+
name,
|
|
189
|
+
type: schemaTypeToTS(schemaType),
|
|
190
|
+
nullable
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return columns;
|
|
194
|
+
}
|
|
195
|
+
function schemaTypeToTS(schemaType) {
|
|
196
|
+
switch (schemaType) {
|
|
197
|
+
case "text":
|
|
198
|
+
return "string";
|
|
199
|
+
case "integer":
|
|
200
|
+
return "number";
|
|
201
|
+
case "real":
|
|
202
|
+
return "number";
|
|
203
|
+
case "boolean":
|
|
204
|
+
return "boolean";
|
|
205
|
+
case "timestamp":
|
|
206
|
+
return "string";
|
|
207
|
+
case "json":
|
|
208
|
+
return "unknown";
|
|
209
|
+
case "blob":
|
|
210
|
+
return "Uint8Array";
|
|
211
|
+
default:
|
|
212
|
+
return "unknown";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function tableNameToInterface(tableName) {
|
|
216
|
+
let name = tableName;
|
|
217
|
+
if (name.endsWith("ies")) {
|
|
218
|
+
name = name.slice(0, -3) + "y";
|
|
219
|
+
} else if (name.endsWith("s") && !name.endsWith("ss")) {
|
|
220
|
+
name = name.slice(0, -1);
|
|
221
|
+
}
|
|
222
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
223
|
+
}
|
|
224
|
+
function generateDbFile(tables) {
|
|
225
|
+
const lines = [
|
|
226
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
227
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
228
|
+
"",
|
|
229
|
+
"import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
|
|
230
|
+
""
|
|
231
|
+
];
|
|
232
|
+
for (const table of tables) {
|
|
233
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
234
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
235
|
+
lines.push(" _id: string;");
|
|
236
|
+
for (const col of table.columns) {
|
|
237
|
+
const typeStr = col.nullable ? `${col.type} | null` : col.type;
|
|
238
|
+
lines.push(` ${col.name}: ${typeStr};`);
|
|
239
|
+
}
|
|
240
|
+
lines.push(" _createdAt: string;");
|
|
241
|
+
lines.push("}");
|
|
242
|
+
lines.push("");
|
|
243
|
+
}
|
|
244
|
+
lines.push("export interface Schema {");
|
|
245
|
+
for (const table of tables) {
|
|
246
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
247
|
+
lines.push(` ${table.name}: ${interfaceName};`);
|
|
248
|
+
}
|
|
249
|
+
lines.push("}");
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push("// Database client with typed tables");
|
|
252
|
+
lines.push("// This is a proxy that will be populated by the Tether runtime");
|
|
253
|
+
lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
|
|
254
|
+
lines.push("");
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
257
|
+
async function parseFunctionsDir(functionsDir) {
|
|
258
|
+
const functions = [];
|
|
259
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
260
|
+
return functions;
|
|
261
|
+
}
|
|
262
|
+
const files = await fs3.readdir(functionsDir);
|
|
263
|
+
for (const file of files) {
|
|
264
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
265
|
+
const filePath = path3.join(functionsDir, file);
|
|
266
|
+
const stat = await fs3.stat(filePath);
|
|
267
|
+
if (!stat.isFile()) continue;
|
|
268
|
+
const moduleName = file.replace(/\.(ts|js)$/, "");
|
|
269
|
+
const source = await fs3.readFile(filePath, "utf-8");
|
|
270
|
+
const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation|action)\s*\(/g;
|
|
271
|
+
let match;
|
|
272
|
+
while ((match = exportRegex.exec(source)) !== null) {
|
|
273
|
+
functions.push({
|
|
274
|
+
name: match[1],
|
|
275
|
+
moduleName
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return functions;
|
|
280
|
+
}
|
|
281
|
+
async function generateApiFile(functionsDir) {
|
|
282
|
+
const functions = await parseFunctionsDir(functionsDir);
|
|
283
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
284
|
+
for (const fn of functions) {
|
|
285
|
+
if (!moduleMap.has(fn.moduleName)) {
|
|
286
|
+
moduleMap.set(fn.moduleName, []);
|
|
287
|
+
}
|
|
288
|
+
moduleMap.get(fn.moduleName).push(fn.name);
|
|
289
|
+
}
|
|
290
|
+
const lines = [
|
|
291
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
292
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
293
|
+
"",
|
|
294
|
+
"import { createApiProxy } from '@tthr/client';",
|
|
295
|
+
"",
|
|
296
|
+
"/**",
|
|
297
|
+
" * API function reference type for useQuery/useMutation.",
|
|
298
|
+
' * The _name property contains the function path (e.g., "users.list").',
|
|
299
|
+
" */",
|
|
300
|
+
"export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
|
|
301
|
+
" _name: string;",
|
|
302
|
+
" _args?: TArgs;",
|
|
303
|
+
" _result?: TResult;",
|
|
304
|
+
"}",
|
|
305
|
+
""
|
|
306
|
+
];
|
|
307
|
+
if (moduleMap.size > 0) {
|
|
308
|
+
for (const [moduleName, fnNames] of moduleMap) {
|
|
309
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
310
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
311
|
+
for (const fnName of fnNames) {
|
|
312
|
+
lines.push(` ${fnName}: ApiFunction;`);
|
|
313
|
+
}
|
|
314
|
+
lines.push("}");
|
|
315
|
+
lines.push("");
|
|
316
|
+
}
|
|
317
|
+
lines.push("export interface Api {");
|
|
318
|
+
for (const moduleName of moduleMap.keys()) {
|
|
319
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
320
|
+
lines.push(` ${moduleName}: ${interfaceName};`);
|
|
321
|
+
}
|
|
322
|
+
lines.push("}");
|
|
323
|
+
} else {
|
|
324
|
+
lines.push("/**");
|
|
325
|
+
lines.push(" * Flexible API type that allows access to any function path.");
|
|
326
|
+
lines.push(" * Functions are accessed as api.moduleName.functionName");
|
|
327
|
+
lines.push(" * The actual types depend on your function definitions.");
|
|
328
|
+
lines.push(" */");
|
|
329
|
+
lines.push("export type Api = {");
|
|
330
|
+
lines.push(" [module: string]: {");
|
|
331
|
+
lines.push(" [fn: string]: ApiFunction;");
|
|
332
|
+
lines.push(" };");
|
|
333
|
+
lines.push("};");
|
|
334
|
+
}
|
|
335
|
+
lines.push("");
|
|
336
|
+
lines.push("// API client proxy - provides typed access to your functions");
|
|
337
|
+
lines.push("// On the client: returns { _name } references for useQuery/useMutation");
|
|
338
|
+
lines.push("// In Tether functions: calls the actual function implementation");
|
|
339
|
+
lines.push("export const api = createApiProxy<Api>();");
|
|
340
|
+
lines.push("");
|
|
341
|
+
return lines.join("\n");
|
|
342
|
+
}
|
|
343
|
+
async function generateTypes(options = {}) {
|
|
344
|
+
const config = await loadConfig();
|
|
345
|
+
const schemaPath = resolvePath(config.schema);
|
|
346
|
+
const outputDir = resolvePath(config.output);
|
|
347
|
+
const functionsDir = resolvePath(config.functions);
|
|
348
|
+
if (!await fs3.pathExists(schemaPath)) {
|
|
349
|
+
throw new Error(`Schema file not found: ${schemaPath}`);
|
|
350
|
+
}
|
|
351
|
+
const schemaSource = await fs3.readFile(schemaPath, "utf-8");
|
|
352
|
+
const tables = parseSchemaFile(schemaSource);
|
|
353
|
+
await fs3.ensureDir(outputDir);
|
|
354
|
+
await fs3.writeFile(
|
|
355
|
+
path3.join(outputDir, "db.ts"),
|
|
356
|
+
generateDbFile(tables)
|
|
357
|
+
);
|
|
358
|
+
await fs3.writeFile(
|
|
359
|
+
path3.join(outputDir, "api.ts"),
|
|
360
|
+
await generateApiFile(functionsDir)
|
|
361
|
+
);
|
|
362
|
+
await fs3.writeFile(
|
|
363
|
+
path3.join(outputDir, "index.ts"),
|
|
364
|
+
`// Auto-generated by Tether CLI - do not edit manually
|
|
365
|
+
export * from './db';
|
|
366
|
+
export * from './api';
|
|
367
|
+
`
|
|
368
|
+
);
|
|
369
|
+
return { tables, outputDir };
|
|
370
|
+
}
|
|
371
|
+
async function generateCommand() {
|
|
372
|
+
await requireAuth();
|
|
373
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
374
|
+
if (!await fs3.pathExists(configPath)) {
|
|
375
|
+
console.log(chalk2.red("\nError: Not a Tether project"));
|
|
376
|
+
console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
|
|
380
|
+
const spinner = ora("Reading schema...").start();
|
|
381
|
+
try {
|
|
382
|
+
spinner.text = "Generating types...";
|
|
383
|
+
const { tables, outputDir } = await generateTypes();
|
|
384
|
+
if (tables.length === 0) {
|
|
385
|
+
spinner.warn("No tables found in schema");
|
|
386
|
+
console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
spinner.succeed(`Types generated for ${tables.length} table(s)`);
|
|
390
|
+
console.log("\n" + chalk2.green("\u2713") + " Tables:");
|
|
391
|
+
for (const table of tables) {
|
|
392
|
+
console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
|
|
393
|
+
}
|
|
394
|
+
const relativeOutput = path3.relative(process.cwd(), outputDir);
|
|
395
|
+
console.log("\n" + chalk2.green("\u2713") + " Generated files:");
|
|
396
|
+
console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
|
|
397
|
+
console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
|
|
398
|
+
console.log(chalk2.dim(` ${relativeOutput}/index.ts
|
|
399
|
+
`));
|
|
400
|
+
} catch (error) {
|
|
401
|
+
spinner.fail("Failed to generate types");
|
|
402
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export {
|
|
408
|
+
getCredentials,
|
|
409
|
+
requireAuth,
|
|
410
|
+
saveCredentials,
|
|
411
|
+
clearCredentials,
|
|
412
|
+
loadConfig,
|
|
413
|
+
resolvePath,
|
|
414
|
+
detectFramework,
|
|
415
|
+
getFrameworkDevCommand,
|
|
416
|
+
generateTypes,
|
|
417
|
+
generateCommand
|
|
418
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
requireAuth,
|
|
10
10
|
resolvePath,
|
|
11
11
|
saveCredentials
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-TU5LOAQ5.js";
|
|
13
13
|
|
|
14
14
|
// src/index.ts
|
|
15
15
|
import { Command } from "commander";
|
|
@@ -98,8 +98,7 @@ async function deploySchemaToServer(projectId, token, schemaPath, environment, d
|
|
|
98
98
|
return;
|
|
99
99
|
}
|
|
100
100
|
spinner.text = "Deploying schema...";
|
|
101
|
-
const
|
|
102
|
-
const schemaUrl = `${API_URL}/projects/${projectId}${envPath}/deploy/schema`;
|
|
101
|
+
const schemaUrl = `${API_URL}/projects/${projectId}/env/${environment}/deploy/schema`;
|
|
103
102
|
console.log(chalk.dim(`
|
|
104
103
|
URL: ${schemaUrl}`));
|
|
105
104
|
const requestBody = {
|
|
@@ -179,8 +178,7 @@ async function deployFunctionsToServer(projectId, token, functionsDir, environme
|
|
|
179
178
|
return;
|
|
180
179
|
}
|
|
181
180
|
spinner.text = "Deploying functions...";
|
|
182
|
-
const
|
|
183
|
-
const functionsUrl = `${API_URL}/projects/${projectId}${envPath}/deploy/functions`;
|
|
181
|
+
const functionsUrl = `${API_URL}/projects/${projectId}/env/${environment}/deploy/functions`;
|
|
184
182
|
console.log(chalk.dim(`
|
|
185
183
|
URL: ${functionsUrl}`));
|
|
186
184
|
const response = await fetch(functionsUrl, {
|
|
@@ -279,9 +277,18 @@ function parseSchema(source) {
|
|
|
279
277
|
const colName = colMatch[1];
|
|
280
278
|
const colType = colMatch[2];
|
|
281
279
|
const modifiers = colMatch[3];
|
|
280
|
+
if (modifiers.includes(".primaryKey()")) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`Cannot use .primaryKey() on column "${colName}" in table "${tableName}". Tether automatically adds a "_id" primary key column to all tables. Remove the .primaryKey() modifier from your schema.`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (colName === "id") {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`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".`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
282
290
|
columns[colName] = {
|
|
283
291
|
type: colType,
|
|
284
|
-
primaryKey: modifiers.includes(".primaryKey()"),
|
|
285
292
|
notNull: modifiers.includes(".notNull()"),
|
|
286
293
|
unique: modifiers.includes(".unique()"),
|
|
287
294
|
hasDefault: modifiers.includes(".default("),
|
|
@@ -308,6 +315,9 @@ function getColumnSqlType(type) {
|
|
|
308
315
|
return "INTEGER";
|
|
309
316
|
case "json":
|
|
310
317
|
return "JSON";
|
|
318
|
+
case "asset":
|
|
319
|
+
return "TEXT";
|
|
320
|
+
// Stores asset ID (UUID reference to _tether_files)
|
|
311
321
|
default:
|
|
312
322
|
return "TEXT";
|
|
313
323
|
}
|
|
@@ -315,10 +325,8 @@ function getColumnSqlType(type) {
|
|
|
315
325
|
function buildColumnSql(colName, def, forAlterTable = false) {
|
|
316
326
|
const sqlType = getColumnSqlType(def.type);
|
|
317
327
|
let colSql = `${colName} ${sqlType}`;
|
|
318
|
-
const addingPrimaryKey = def.primaryKey && !forAlterTable;
|
|
319
|
-
if (addingPrimaryKey) colSql += " PRIMARY KEY";
|
|
320
328
|
if (def.notNull && !forAlterTable) colSql += " NOT NULL";
|
|
321
|
-
if (def.unique
|
|
329
|
+
if (def.unique) colSql += " UNIQUE";
|
|
322
330
|
if (def.hasDefault && def.type === "timestamp" && !forAlterTable) {
|
|
323
331
|
colSql += " DEFAULT (datetime('now'))";
|
|
324
332
|
}
|
|
@@ -703,40 +711,28 @@ export default defineConfig({
|
|
|
703
711
|
|
|
704
712
|
// API endpoint (defaults to Tether Cloud)
|
|
705
713
|
url: process.env.TETHER_URL || 'https://tether-api.strands.gg',
|
|
706
|
-
|
|
707
|
-
// Schema file location
|
|
708
|
-
schema: './tether/schema.ts',
|
|
709
|
-
|
|
710
|
-
// Functions directory
|
|
711
|
-
functions: './tether/functions',
|
|
712
|
-
|
|
713
|
-
// Generated types output
|
|
714
|
-
output: './tether/_generated',
|
|
715
714
|
});
|
|
716
715
|
`
|
|
717
716
|
);
|
|
718
717
|
await fs2.writeFile(
|
|
719
718
|
path2.join(projectPath, "tether", "schema.ts"),
|
|
720
|
-
`import { defineSchema, text,
|
|
719
|
+
`import { defineSchema, text, timestamp } from '@tthr/schema';
|
|
721
720
|
|
|
722
721
|
export default defineSchema({
|
|
723
722
|
// Example: posts table
|
|
723
|
+
// Note: _id and _createdAt are automatically added by Tether
|
|
724
724
|
posts: {
|
|
725
|
-
id: text().primaryKey(),
|
|
726
725
|
title: text().notNull(),
|
|
727
726
|
content: text(),
|
|
728
727
|
authorId: text().notNull(),
|
|
729
|
-
createdAt: timestamp().notNull().default('now'),
|
|
730
728
|
updatedAt: timestamp().notNull().default('now'),
|
|
731
729
|
},
|
|
732
730
|
|
|
733
731
|
// Example: comments table
|
|
734
732
|
comments: {
|
|
735
|
-
|
|
736
|
-
postId: text().notNull().references('posts.id'),
|
|
733
|
+
postId: text().notNull().references('posts._id'),
|
|
737
734
|
content: text().notNull(),
|
|
738
735
|
authorId: text().notNull(),
|
|
739
|
-
createdAt: timestamp().notNull().default('now'),
|
|
740
736
|
},
|
|
741
737
|
});
|
|
742
738
|
`
|
|
@@ -752,59 +748,55 @@ export const list = query({
|
|
|
752
748
|
}),
|
|
753
749
|
handler: async ({ args, db }) => {
|
|
754
750
|
return db.posts.findMany({
|
|
755
|
-
orderBy: {
|
|
751
|
+
orderBy: { _createdAt: 'desc' },
|
|
756
752
|
limit: args.limit,
|
|
757
753
|
});
|
|
758
754
|
},
|
|
759
755
|
});
|
|
760
756
|
|
|
761
|
-
// Get a single post
|
|
757
|
+
// Get a single post by _id
|
|
762
758
|
export const get = query({
|
|
763
759
|
args: z.object({
|
|
764
|
-
|
|
760
|
+
_id: z.string(),
|
|
765
761
|
}),
|
|
766
762
|
handler: async ({ args, db }) => {
|
|
767
763
|
return db.posts.findUnique({
|
|
768
|
-
where: {
|
|
764
|
+
where: { _id: args._id },
|
|
769
765
|
});
|
|
770
766
|
},
|
|
771
767
|
});
|
|
772
768
|
|
|
773
769
|
// Create a new post
|
|
770
|
+
// Note: _id and _createdAt are automatically added by Tether
|
|
774
771
|
export const create = mutation({
|
|
775
772
|
args: z.object({
|
|
776
773
|
title: z.string().min(1),
|
|
777
774
|
content: z.string().optional(),
|
|
778
775
|
}),
|
|
779
776
|
handler: async ({ args, ctx, db }) => {
|
|
780
|
-
const
|
|
781
|
-
const now = new Date().toISOString();
|
|
782
|
-
|
|
783
|
-
await db.posts.create({
|
|
777
|
+
const post = await db.posts.create({
|
|
784
778
|
data: {
|
|
785
|
-
id,
|
|
786
779
|
title: args.title,
|
|
787
780
|
content: args.content ?? '',
|
|
788
781
|
authorId: ctx.userId ?? 'anonymous',
|
|
789
|
-
|
|
790
|
-
updatedAt: now,
|
|
782
|
+
updatedAt: new Date().toISOString(),
|
|
791
783
|
},
|
|
792
784
|
});
|
|
793
785
|
|
|
794
|
-
return {
|
|
786
|
+
return { _id: post._id };
|
|
795
787
|
},
|
|
796
788
|
});
|
|
797
789
|
|
|
798
790
|
// Update a post
|
|
799
791
|
export const update = mutation({
|
|
800
792
|
args: z.object({
|
|
801
|
-
|
|
793
|
+
_id: z.string(),
|
|
802
794
|
title: z.string().min(1).optional(),
|
|
803
795
|
content: z.string().optional(),
|
|
804
796
|
}),
|
|
805
797
|
handler: async ({ args, db }) => {
|
|
806
798
|
await db.posts.update({
|
|
807
|
-
where: {
|
|
799
|
+
where: { _id: args._id },
|
|
808
800
|
data: {
|
|
809
801
|
...(args.title && { title: args.title }),
|
|
810
802
|
...(args.content !== undefined && { content: args.content }),
|
|
@@ -817,11 +809,11 @@ export const update = mutation({
|
|
|
817
809
|
// Delete a post
|
|
818
810
|
export const remove = mutation({
|
|
819
811
|
args: z.object({
|
|
820
|
-
|
|
812
|
+
_id: z.string(),
|
|
821
813
|
}),
|
|
822
814
|
handler: async ({ args, db }) => {
|
|
823
815
|
await db.posts.delete({
|
|
824
|
-
where: {
|
|
816
|
+
where: { _id: args._id },
|
|
825
817
|
});
|
|
826
818
|
},
|
|
827
819
|
});
|
|
@@ -847,7 +839,7 @@ export const list = query({
|
|
|
847
839
|
}),
|
|
848
840
|
handler: async ({ args, db }) => {
|
|
849
841
|
return db.comments.findMany({
|
|
850
|
-
orderBy: {
|
|
842
|
+
orderBy: { _createdAt: 'desc' },
|
|
851
843
|
limit: args.limit,
|
|
852
844
|
});
|
|
853
845
|
},
|
|
@@ -862,44 +854,40 @@ export const listByPost = query({
|
|
|
862
854
|
handler: async ({ args, db }) => {
|
|
863
855
|
return db.comments.findMany({
|
|
864
856
|
where: { postId: args.postId },
|
|
865
|
-
orderBy: {
|
|
857
|
+
orderBy: { _createdAt: 'asc' },
|
|
866
858
|
limit: args.limit,
|
|
867
859
|
});
|
|
868
860
|
},
|
|
869
861
|
});
|
|
870
862
|
|
|
871
863
|
// Create a new comment
|
|
864
|
+
// Note: _id and _createdAt are automatically added by Tether
|
|
872
865
|
export const create = mutation({
|
|
873
866
|
args: z.object({
|
|
874
867
|
postId: z.string(),
|
|
875
868
|
content: z.string().min(1),
|
|
876
869
|
}),
|
|
877
870
|
handler: async ({ args, ctx, db }) => {
|
|
878
|
-
const
|
|
879
|
-
const now = new Date().toISOString();
|
|
880
|
-
|
|
881
|
-
await db.comments.create({
|
|
871
|
+
const comment = await db.comments.create({
|
|
882
872
|
data: {
|
|
883
|
-
id,
|
|
884
873
|
postId: args.postId,
|
|
885
874
|
content: args.content,
|
|
886
875
|
authorId: ctx.userId ?? 'anonymous',
|
|
887
|
-
createdAt: now,
|
|
888
876
|
},
|
|
889
877
|
});
|
|
890
878
|
|
|
891
|
-
return {
|
|
879
|
+
return { _id: comment._id };
|
|
892
880
|
},
|
|
893
881
|
});
|
|
894
882
|
|
|
895
883
|
// Delete a comment
|
|
896
884
|
export const remove = mutation({
|
|
897
885
|
args: z.object({
|
|
898
|
-
|
|
886
|
+
_id: z.string(),
|
|
899
887
|
}),
|
|
900
888
|
handler: async ({ args, db }) => {
|
|
901
889
|
await db.comments.delete({
|
|
902
|
-
where: {
|
|
890
|
+
where: { _id: args._id },
|
|
903
891
|
});
|
|
904
892
|
},
|
|
905
893
|
});
|
|
@@ -1522,7 +1510,7 @@ async function runGenerate(spinner) {
|
|
|
1522
1510
|
}
|
|
1523
1511
|
isGenerating = true;
|
|
1524
1512
|
try {
|
|
1525
|
-
const { generateTypes } = await import("./generate-
|
|
1513
|
+
const { generateTypes } = await import("./generate-HRIVNCH3.js");
|
|
1526
1514
|
spinner.text = "Regenerating types...";
|
|
1527
1515
|
await generateTypes({ silent: true });
|
|
1528
1516
|
spinner.succeed("Types regenerated");
|
|
@@ -1703,7 +1691,7 @@ async function devCommand(options) {
|
|
|
1703
1691
|
}
|
|
1704
1692
|
}
|
|
1705
1693
|
spinner.text = "Generating types...";
|
|
1706
|
-
const { generateTypes } = await import("./generate-
|
|
1694
|
+
const { generateTypes } = await import("./generate-HRIVNCH3.js");
|
|
1707
1695
|
await generateTypes({ silent: true });
|
|
1708
1696
|
spinner.succeed("Types generated");
|
|
1709
1697
|
spinner.start("Setting up file watchers...");
|