tthr 0.3.15 → 0.3.17
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-DR6CWO75.js +733 -0
- package/dist/generate-M7G5AXMR.js +8 -0
- package/dist/index.js +53 -53
- package/package.json +1 -1
|
@@ -0,0 +1,733 @@
|
|
|
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 fs2 from "fs-extra";
|
|
10
|
+
import path2 from "path";
|
|
11
|
+
|
|
12
|
+
// src/utils/config.ts
|
|
13
|
+
import fs from "fs-extra";
|
|
14
|
+
import path from "path";
|
|
15
|
+
var DEFAULT_CONFIG = {
|
|
16
|
+
schema: "./tether/schema.ts",
|
|
17
|
+
functions: "./tether/functions",
|
|
18
|
+
output: "./tether/_generated",
|
|
19
|
+
dev: {
|
|
20
|
+
port: 3001,
|
|
21
|
+
host: "localhost"
|
|
22
|
+
},
|
|
23
|
+
database: {
|
|
24
|
+
walMode: true
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
28
|
+
const configPath = path.resolve(cwd, "tether.config.ts");
|
|
29
|
+
if (!await fs.pathExists(configPath)) {
|
|
30
|
+
return DEFAULT_CONFIG;
|
|
31
|
+
}
|
|
32
|
+
const configSource = await fs.readFile(configPath, "utf-8");
|
|
33
|
+
const config = {};
|
|
34
|
+
const schemaMatch = configSource.match(/schema\s*:\s*['"]([^'"]+)['"]/);
|
|
35
|
+
if (schemaMatch) {
|
|
36
|
+
config.schema = schemaMatch[1];
|
|
37
|
+
}
|
|
38
|
+
const functionsMatch = configSource.match(/functions\s*:\s*['"]([^'"]+)['"]/);
|
|
39
|
+
if (functionsMatch) {
|
|
40
|
+
config.functions = functionsMatch[1];
|
|
41
|
+
}
|
|
42
|
+
const outputMatch = configSource.match(/output\s*:\s*['"]([^'"]+)['"]/);
|
|
43
|
+
if (outputMatch) {
|
|
44
|
+
config.output = outputMatch[1];
|
|
45
|
+
}
|
|
46
|
+
const projectIdMatch = configSource.match(/projectId\s*:\s*['"]([^'"]+)['"]/);
|
|
47
|
+
if (projectIdMatch) {
|
|
48
|
+
config.projectId = projectIdMatch[1];
|
|
49
|
+
}
|
|
50
|
+
const urlMatch = configSource.match(/(?:^|\n)\s*url\s*:\s*['"]([^'"]+)['"]/);
|
|
51
|
+
if (urlMatch) {
|
|
52
|
+
config.url = urlMatch[1];
|
|
53
|
+
}
|
|
54
|
+
const envMatch = configSource.match(/environment\s*:\s*['"]([^'"]+)['"]/);
|
|
55
|
+
if (envMatch) {
|
|
56
|
+
config.environment = envMatch[1];
|
|
57
|
+
}
|
|
58
|
+
const portMatch = configSource.match(/port\s*:\s*(\d+)/);
|
|
59
|
+
if (portMatch) {
|
|
60
|
+
config.dev = { ...config.dev, port: parseInt(portMatch[1], 10) };
|
|
61
|
+
}
|
|
62
|
+
const hostMatch = configSource.match(/host\s*:\s*['"]([^'"]+)['"]/);
|
|
63
|
+
if (hostMatch) {
|
|
64
|
+
config.dev = { ...config.dev, host: hostMatch[1] };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...DEFAULT_CONFIG,
|
|
68
|
+
...config,
|
|
69
|
+
dev: { ...DEFAULT_CONFIG.dev, ...config.dev },
|
|
70
|
+
database: { ...DEFAULT_CONFIG.database, ...config.database }
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function resolveEnvironmentFromApiKey(cwd = process.cwd()) {
|
|
74
|
+
const envPath = path.resolve(cwd, ".env");
|
|
75
|
+
if (!await fs.pathExists(envPath)) return void 0;
|
|
76
|
+
const envContent = await fs.readFile(envPath, "utf-8");
|
|
77
|
+
const match = envContent.match(/TETHER_API_KEY=\s*(tthr_([^_]+)_[0-9a-f]+)/);
|
|
78
|
+
if (!match) return void 0;
|
|
79
|
+
const prefix = match[2];
|
|
80
|
+
switch (prefix) {
|
|
81
|
+
case "dev":
|
|
82
|
+
return "development";
|
|
83
|
+
case "prod":
|
|
84
|
+
return "production";
|
|
85
|
+
default:
|
|
86
|
+
return prefix;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
var DEFAULT_API_URL = "https://api.tether.ngo";
|
|
90
|
+
function getApiUrl(config) {
|
|
91
|
+
if (process.env.TETHER_API_URL) {
|
|
92
|
+
return process.env.TETHER_API_URL.replace(/\/+$/, "");
|
|
93
|
+
}
|
|
94
|
+
if (config?.url) {
|
|
95
|
+
return config.url.replace(/\/+$/, "");
|
|
96
|
+
}
|
|
97
|
+
const isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
98
|
+
return isDev ? "http://localhost:3001" : DEFAULT_API_URL;
|
|
99
|
+
}
|
|
100
|
+
async function resolveApiUrl(config) {
|
|
101
|
+
if (process.env.TETHER_API_URL) {
|
|
102
|
+
return process.env.TETHER_API_URL.replace(/\/+$/, "");
|
|
103
|
+
}
|
|
104
|
+
const resolved = config ?? await loadConfig();
|
|
105
|
+
if (resolved?.url) {
|
|
106
|
+
return resolved.url.replace(/\/+$/, "");
|
|
107
|
+
}
|
|
108
|
+
const isDev = process.env.NODE_ENV === "development" || process.env.TETHER_DEV === "true";
|
|
109
|
+
return isDev ? "http://localhost:3001" : DEFAULT_API_URL;
|
|
110
|
+
}
|
|
111
|
+
function resolvePath(configPath, cwd = process.cwd()) {
|
|
112
|
+
const normalised = configPath.replace(/^\.\//, "");
|
|
113
|
+
return path.resolve(cwd, normalised);
|
|
114
|
+
}
|
|
115
|
+
async function detectFramework(cwd = process.cwd()) {
|
|
116
|
+
const packageJsonPath = path.resolve(cwd, "package.json");
|
|
117
|
+
if (!await fs.pathExists(packageJsonPath)) {
|
|
118
|
+
return "unknown";
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
122
|
+
const deps = {
|
|
123
|
+
...packageJson.dependencies,
|
|
124
|
+
...packageJson.devDependencies
|
|
125
|
+
};
|
|
126
|
+
if (deps.nuxt || deps["@nuxt/kit"]) {
|
|
127
|
+
return "nuxt";
|
|
128
|
+
}
|
|
129
|
+
if (deps.next) {
|
|
130
|
+
return "next";
|
|
131
|
+
}
|
|
132
|
+
if (deps["@sveltejs/kit"]) {
|
|
133
|
+
return "sveltekit";
|
|
134
|
+
}
|
|
135
|
+
if (deps.vite && !deps.nuxt && !deps.next && !deps["@sveltejs/kit"]) {
|
|
136
|
+
return "vite";
|
|
137
|
+
}
|
|
138
|
+
return "vanilla";
|
|
139
|
+
} catch {
|
|
140
|
+
return "unknown";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function getFrameworkDevCommand(framework) {
|
|
144
|
+
switch (framework) {
|
|
145
|
+
case "nuxt":
|
|
146
|
+
return "nuxt dev";
|
|
147
|
+
case "next":
|
|
148
|
+
return "next dev";
|
|
149
|
+
case "sveltekit":
|
|
150
|
+
return "vite dev";
|
|
151
|
+
case "vite":
|
|
152
|
+
return "vite dev";
|
|
153
|
+
case "vanilla":
|
|
154
|
+
return null;
|
|
155
|
+
// No default dev server for vanilla
|
|
156
|
+
default:
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/utils/auth.ts
|
|
162
|
+
var CONFIG_DIR = path2.join(process.env.HOME || process.env.USERPROFILE || "", ".tether");
|
|
163
|
+
var CREDENTIALS_FILE = path2.join(CONFIG_DIR, "credentials.json");
|
|
164
|
+
var REFRESH_THRESHOLD_DAYS = 7;
|
|
165
|
+
async function getCredentials() {
|
|
166
|
+
try {
|
|
167
|
+
if (await fs2.pathExists(CREDENTIALS_FILE)) {
|
|
168
|
+
return await fs2.readJSON(CREDENTIALS_FILE);
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
async function refreshSession(credentials) {
|
|
175
|
+
try {
|
|
176
|
+
const apiUrl = `${await resolveApiUrl()}/api/v1`;
|
|
177
|
+
const response = await fetch(`${apiUrl}/auth/session/refresh`, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: {
|
|
180
|
+
"Content-Type": "application/json",
|
|
181
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
const data = await response.json();
|
|
188
|
+
const updated = {
|
|
189
|
+
...credentials,
|
|
190
|
+
expiresAt: data.expiresAt
|
|
191
|
+
};
|
|
192
|
+
await saveCredentials(updated);
|
|
193
|
+
return updated;
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function requireAuth() {
|
|
199
|
+
const credentials = await getCredentials();
|
|
200
|
+
if (!credentials) {
|
|
201
|
+
console.error(chalk.red("\n\u2717 Not logged in\n"));
|
|
202
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
if (credentials.expiresAt) {
|
|
206
|
+
const expiresAt = new Date(credentials.expiresAt);
|
|
207
|
+
const now = /* @__PURE__ */ new Date();
|
|
208
|
+
if (now > expiresAt) {
|
|
209
|
+
await clearCredentials();
|
|
210
|
+
console.error(chalk.red("\n\u2717 Session expired \u2014 you have been signed out\n"));
|
|
211
|
+
console.log(chalk.dim("Run `tthr login` to authenticate\n"));
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
const daysUntilExpiry = (expiresAt.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24);
|
|
215
|
+
if (daysUntilExpiry <= REFRESH_THRESHOLD_DAYS) {
|
|
216
|
+
const refreshed = await refreshSession(credentials);
|
|
217
|
+
if (refreshed) {
|
|
218
|
+
return refreshed;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return credentials;
|
|
223
|
+
}
|
|
224
|
+
async function saveCredentials(credentials) {
|
|
225
|
+
await fs2.ensureDir(CONFIG_DIR);
|
|
226
|
+
await fs2.writeJSON(CREDENTIALS_FILE, credentials, { spaces: 2, mode: 384 });
|
|
227
|
+
}
|
|
228
|
+
async function clearCredentials() {
|
|
229
|
+
try {
|
|
230
|
+
await fs2.remove(CREDENTIALS_FILE);
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/commands/generate.ts
|
|
236
|
+
function parseSchemaFile(source) {
|
|
237
|
+
const tables = [];
|
|
238
|
+
const schemaMatch = source.match(/defineSchema\s*\(\s*\{([\s\S]*)\}\s*\)/);
|
|
239
|
+
if (!schemaMatch) return tables;
|
|
240
|
+
const schemaContent = schemaMatch[1];
|
|
241
|
+
const tableStartRegex = /(\w+)\s*:\s*\{/g;
|
|
242
|
+
let match;
|
|
243
|
+
while ((match = tableStartRegex.exec(schemaContent)) !== null) {
|
|
244
|
+
const tableName = match[1];
|
|
245
|
+
const startOffset = match.index + match[0].length;
|
|
246
|
+
let braceCount = 1;
|
|
247
|
+
let endOffset = startOffset;
|
|
248
|
+
for (let i = startOffset; i < schemaContent.length && braceCount > 0; i++) {
|
|
249
|
+
const char = schemaContent[i];
|
|
250
|
+
if (char === "{") braceCount++;
|
|
251
|
+
else if (char === "}") braceCount--;
|
|
252
|
+
endOffset = i;
|
|
253
|
+
}
|
|
254
|
+
const columnsContent = schemaContent.slice(startOffset, endOffset);
|
|
255
|
+
const columns = parseColumns(columnsContent);
|
|
256
|
+
tables.push({ name: tableName, columns });
|
|
257
|
+
}
|
|
258
|
+
return tables;
|
|
259
|
+
}
|
|
260
|
+
function parseColumns(content) {
|
|
261
|
+
const columns = [];
|
|
262
|
+
const columnRegex = /(\w+)\s*:\s*(\w+)(?:<([^>]+)>)?\s*\(\s*\)((?:\[.*?\]|[^,\n}])*)/g;
|
|
263
|
+
let match;
|
|
264
|
+
while ((match = columnRegex.exec(content)) !== null) {
|
|
265
|
+
const name = match[1];
|
|
266
|
+
const schemaType = match[2];
|
|
267
|
+
const genericType = match[3];
|
|
268
|
+
const modifiers = match[4] || "";
|
|
269
|
+
const nullable = !modifiers.includes(".notNull()");
|
|
270
|
+
const oneOfMatch = modifiers.match(/\.oneOf\s*\(\s*\[(.*?)\]\s*\)/);
|
|
271
|
+
let oneOf;
|
|
272
|
+
if (oneOfMatch) {
|
|
273
|
+
oneOf = [...oneOfMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map((m) => m[1]);
|
|
274
|
+
}
|
|
275
|
+
columns.push({
|
|
276
|
+
name,
|
|
277
|
+
type: schemaTypeToTS(schemaType),
|
|
278
|
+
nullable,
|
|
279
|
+
jsonType: genericType,
|
|
280
|
+
// Store the generic type for json columns
|
|
281
|
+
oneOf
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return columns;
|
|
285
|
+
}
|
|
286
|
+
function schemaTypeToTS(schemaType) {
|
|
287
|
+
switch (schemaType) {
|
|
288
|
+
case "text":
|
|
289
|
+
return "string";
|
|
290
|
+
case "integer":
|
|
291
|
+
return "number";
|
|
292
|
+
case "real":
|
|
293
|
+
return "number";
|
|
294
|
+
case "boolean":
|
|
295
|
+
return "boolean";
|
|
296
|
+
case "timestamp":
|
|
297
|
+
return "string";
|
|
298
|
+
case "json":
|
|
299
|
+
return "unknown";
|
|
300
|
+
case "blob":
|
|
301
|
+
return "Uint8Array";
|
|
302
|
+
case "asset":
|
|
303
|
+
return "TetherAsset";
|
|
304
|
+
// Asset object returned by API
|
|
305
|
+
default:
|
|
306
|
+
return "unknown";
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function extractSingleType(source, typeName) {
|
|
310
|
+
const interfaceStart = new RegExp(`(?:export\\s+)?interface\\s+${typeName}\\s*\\{`);
|
|
311
|
+
const interfaceMatch = interfaceStart.exec(source);
|
|
312
|
+
if (interfaceMatch) {
|
|
313
|
+
const braceStart = interfaceMatch.index + interfaceMatch[0].length;
|
|
314
|
+
let braceCount = 1;
|
|
315
|
+
let endIdx = braceStart;
|
|
316
|
+
for (let i = braceStart; i < source.length && braceCount > 0; i++) {
|
|
317
|
+
if (source[i] === "{") braceCount++;
|
|
318
|
+
else if (source[i] === "}") braceCount--;
|
|
319
|
+
endIdx = i;
|
|
320
|
+
}
|
|
321
|
+
return source.slice(interfaceMatch.index, endIdx + 1).replace(/^export\s+/, "");
|
|
322
|
+
}
|
|
323
|
+
const typeStart = new RegExp(`(?:export\\s+)?type\\s+${typeName}\\s*=`);
|
|
324
|
+
const typeMatch = typeStart.exec(source);
|
|
325
|
+
if (typeMatch) {
|
|
326
|
+
const semiIdx = source.indexOf(";", typeMatch.index);
|
|
327
|
+
if (semiIdx !== -1) {
|
|
328
|
+
return source.slice(typeMatch.index, semiIdx + 1).replace(/^export\s+/, "");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
function extractTypeDefinitions(source, typeNames) {
|
|
334
|
+
if (typeNames.size === 0) return [];
|
|
335
|
+
const allDefinedTypes = /* @__PURE__ */ new Set();
|
|
336
|
+
const typeDefRegex = /(?:export\s+)?(?:interface|type)\s+(\w+)/g;
|
|
337
|
+
let m;
|
|
338
|
+
while ((m = typeDefRegex.exec(source)) !== null) {
|
|
339
|
+
allDefinedTypes.add(m[1]);
|
|
340
|
+
}
|
|
341
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
342
|
+
const queue = [...typeNames];
|
|
343
|
+
while (queue.length > 0) {
|
|
344
|
+
const name = queue.shift();
|
|
345
|
+
if (resolved.has(name)) continue;
|
|
346
|
+
const block = extractSingleType(source, name);
|
|
347
|
+
if (!block) continue;
|
|
348
|
+
resolved.set(name, block);
|
|
349
|
+
for (const definedType of allDefinedTypes) {
|
|
350
|
+
if (!resolved.has(definedType) && !queue.includes(definedType)) {
|
|
351
|
+
const refRegex = new RegExp(`\\b${definedType}\\b`);
|
|
352
|
+
if (refRegex.test(block)) {
|
|
353
|
+
queue.push(definedType);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const ordered = [];
|
|
359
|
+
const visited = /* @__PURE__ */ new Set();
|
|
360
|
+
function visit(name) {
|
|
361
|
+
if (visited.has(name)) return;
|
|
362
|
+
visited.add(name);
|
|
363
|
+
const block = resolved.get(name);
|
|
364
|
+
if (!block) return;
|
|
365
|
+
for (const dep of resolved.keys()) {
|
|
366
|
+
if (dep !== name && new RegExp(`\\b${dep}\\b`).test(block)) {
|
|
367
|
+
visit(dep);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
ordered.push(name);
|
|
371
|
+
}
|
|
372
|
+
for (const name of resolved.keys()) {
|
|
373
|
+
visit(name);
|
|
374
|
+
}
|
|
375
|
+
return ordered.map((name) => resolved.get(name));
|
|
376
|
+
}
|
|
377
|
+
function tableNameToInterface(tableName) {
|
|
378
|
+
let name = tableName;
|
|
379
|
+
if (name.endsWith("ies")) {
|
|
380
|
+
name = name.slice(0, -3) + "y";
|
|
381
|
+
} else if (name.endsWith("s") && !name.endsWith("ss")) {
|
|
382
|
+
name = name.slice(0, -1);
|
|
383
|
+
}
|
|
384
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
385
|
+
}
|
|
386
|
+
function generateDbFile(tables, schemaSource) {
|
|
387
|
+
const TS_PRIMITIVES = /* @__PURE__ */ new Set(["number", "string", "boolean", "unknown", "any", "void", "never", "undefined", "null", "object"]);
|
|
388
|
+
const jsonTypes = /* @__PURE__ */ new Set();
|
|
389
|
+
for (const table of tables) {
|
|
390
|
+
for (const col of table.columns) {
|
|
391
|
+
if (col.jsonType) {
|
|
392
|
+
const baseType = col.jsonType.replace(/\s*\|\s*null/g, "").replace(/\[\]/g, "").trim();
|
|
393
|
+
if (!TS_PRIMITIVES.has(baseType)) {
|
|
394
|
+
jsonTypes.add(baseType);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const lines = [
|
|
400
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
401
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
402
|
+
"",
|
|
403
|
+
"import { createDatabaseProxy, type TetherDatabase } from '@tthr/client';",
|
|
404
|
+
"import {",
|
|
405
|
+
" type QueryDefinition,",
|
|
406
|
+
" type MutationDefinition,",
|
|
407
|
+
" z,",
|
|
408
|
+
"} from '@tthr/server';",
|
|
409
|
+
"import type { TetherEnv } from './env';"
|
|
410
|
+
];
|
|
411
|
+
if (jsonTypes.size > 0) {
|
|
412
|
+
const typeBlocks = extractTypeDefinitions(schemaSource, jsonTypes);
|
|
413
|
+
if (typeBlocks.length > 0) {
|
|
414
|
+
lines.push("");
|
|
415
|
+
lines.push("// Schema types (inlined from schema.ts)");
|
|
416
|
+
for (const block of typeBlocks) {
|
|
417
|
+
lines.push(block);
|
|
418
|
+
lines.push("");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
lines.push("");
|
|
423
|
+
lines.push("// Asset type returned by the API for asset columns");
|
|
424
|
+
lines.push("export interface TetherAsset {");
|
|
425
|
+
lines.push(" id: string;");
|
|
426
|
+
lines.push(" filename: string;");
|
|
427
|
+
lines.push(" contentType: string;");
|
|
428
|
+
lines.push(" size: number;");
|
|
429
|
+
lines.push(" url: string;");
|
|
430
|
+
lines.push(" createdAt: string;");
|
|
431
|
+
lines.push("}");
|
|
432
|
+
lines.push("");
|
|
433
|
+
for (const table of tables) {
|
|
434
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
435
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
436
|
+
lines.push(" _id: string;");
|
|
437
|
+
for (const col of table.columns) {
|
|
438
|
+
let colType;
|
|
439
|
+
if (col.oneOf && col.oneOf.length > 0) {
|
|
440
|
+
colType = col.oneOf.map((v) => `'${v}'`).join(" | ");
|
|
441
|
+
} else {
|
|
442
|
+
colType = col.jsonType || col.type;
|
|
443
|
+
}
|
|
444
|
+
const typeStr = col.nullable ? `${colType} | null` : colType;
|
|
445
|
+
const optional = col.nullable ? "?" : "";
|
|
446
|
+
lines.push(` ${col.name}${optional}: ${typeStr};`);
|
|
447
|
+
}
|
|
448
|
+
lines.push(" _createdAt: string;");
|
|
449
|
+
lines.push(" _updatedAt?: string | null;");
|
|
450
|
+
lines.push(" _deletedAt?: string | null;");
|
|
451
|
+
lines.push("}");
|
|
452
|
+
lines.push("");
|
|
453
|
+
}
|
|
454
|
+
lines.push("export interface Schema {");
|
|
455
|
+
for (const table of tables) {
|
|
456
|
+
const interfaceName = tableNameToInterface(table.name);
|
|
457
|
+
lines.push(` ${table.name}: ${interfaceName};`);
|
|
458
|
+
}
|
|
459
|
+
lines.push("}");
|
|
460
|
+
lines.push("");
|
|
461
|
+
lines.push("// Database client with typed tables");
|
|
462
|
+
lines.push("// This is a proxy that will be populated by the Tether runtime");
|
|
463
|
+
lines.push("export const db: TetherDatabase<Schema> = createDatabaseProxy<Schema>();");
|
|
464
|
+
lines.push("");
|
|
465
|
+
lines.push("// ============================================================================");
|
|
466
|
+
lines.push("// Typed function wrappers - use these instead of importing from @tthr/server");
|
|
467
|
+
lines.push("// This ensures the `db` and `env` parameters in handlers are properly typed");
|
|
468
|
+
lines.push("// ============================================================================");
|
|
469
|
+
lines.push("");
|
|
470
|
+
lines.push("/**");
|
|
471
|
+
lines.push(" * Define a query function with typed database and environment variable access.");
|
|
472
|
+
lines.push(" * The `db` and `env` parameters in the handler will have full type safety.");
|
|
473
|
+
lines.push(" */");
|
|
474
|
+
lines.push("export function query<TArgs = void, TResult = unknown>(");
|
|
475
|
+
lines.push(" definition: QueryDefinition<TArgs, TResult, Schema, TetherEnv>");
|
|
476
|
+
lines.push("): QueryDefinition<TArgs, TResult, Schema, TetherEnv> {");
|
|
477
|
+
lines.push(" return definition;");
|
|
478
|
+
lines.push("}");
|
|
479
|
+
lines.push("");
|
|
480
|
+
lines.push("/**");
|
|
481
|
+
lines.push(" * Define a mutation function with typed database and environment variable access.");
|
|
482
|
+
lines.push(" * The `db` and `env` parameters in the handler will have full type safety.");
|
|
483
|
+
lines.push(" */");
|
|
484
|
+
lines.push("export function mutation<TArgs = void, TResult = unknown>(");
|
|
485
|
+
lines.push(" definition: MutationDefinition<TArgs, TResult, Schema, TetherEnv>");
|
|
486
|
+
lines.push("): MutationDefinition<TArgs, TResult, Schema, TetherEnv> {");
|
|
487
|
+
lines.push(" return definition;");
|
|
488
|
+
lines.push("}");
|
|
489
|
+
lines.push("");
|
|
490
|
+
lines.push("// Re-export z for convenience");
|
|
491
|
+
lines.push("export { z };");
|
|
492
|
+
lines.push("");
|
|
493
|
+
return lines.join("\n");
|
|
494
|
+
}
|
|
495
|
+
async function parseFunctionsDir(functionsDir) {
|
|
496
|
+
const functions = [];
|
|
497
|
+
if (!await fs3.pathExists(functionsDir)) {
|
|
498
|
+
return functions;
|
|
499
|
+
}
|
|
500
|
+
const files = await fs3.readdir(functionsDir);
|
|
501
|
+
for (const file of files) {
|
|
502
|
+
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
|
|
503
|
+
const filePath = path3.join(functionsDir, file);
|
|
504
|
+
const stat = await fs3.stat(filePath);
|
|
505
|
+
if (!stat.isFile()) continue;
|
|
506
|
+
const moduleName = file.replace(/\.(ts|js)$/, "");
|
|
507
|
+
const source = await fs3.readFile(filePath, "utf-8");
|
|
508
|
+
const exportRegex = /export\s+const\s+(\w+)\s*=\s*(query|mutation)\s*\(/g;
|
|
509
|
+
let match;
|
|
510
|
+
while ((match = exportRegex.exec(source)) !== null) {
|
|
511
|
+
functions.push({
|
|
512
|
+
name: match[1],
|
|
513
|
+
moduleName
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return functions;
|
|
518
|
+
}
|
|
519
|
+
async function generateApiFile(functionsDir) {
|
|
520
|
+
const functions = await parseFunctionsDir(functionsDir);
|
|
521
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
522
|
+
for (const fn of functions) {
|
|
523
|
+
if (!moduleMap.has(fn.moduleName)) {
|
|
524
|
+
moduleMap.set(fn.moduleName, []);
|
|
525
|
+
}
|
|
526
|
+
moduleMap.get(fn.moduleName).push(fn.name);
|
|
527
|
+
}
|
|
528
|
+
const lines = [
|
|
529
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
530
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
531
|
+
"",
|
|
532
|
+
"import { createApiProxy } from '@tthr/client';",
|
|
533
|
+
"",
|
|
534
|
+
"/**",
|
|
535
|
+
" * API function reference type for useQuery/useMutation.",
|
|
536
|
+
' * The _name property contains the function path (e.g., "users.list").',
|
|
537
|
+
" */",
|
|
538
|
+
"export interface ApiFunction<TArgs = unknown, TResult = unknown> {",
|
|
539
|
+
" _name: string;",
|
|
540
|
+
" _args?: TArgs;",
|
|
541
|
+
" _result?: TResult;",
|
|
542
|
+
"}",
|
|
543
|
+
""
|
|
544
|
+
];
|
|
545
|
+
if (moduleMap.size > 0) {
|
|
546
|
+
for (const [moduleName, fnNames] of moduleMap) {
|
|
547
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
548
|
+
lines.push(`export interface ${interfaceName} {`);
|
|
549
|
+
for (const fnName of fnNames) {
|
|
550
|
+
lines.push(` ${fnName}: ApiFunction;`);
|
|
551
|
+
}
|
|
552
|
+
lines.push("}");
|
|
553
|
+
lines.push("");
|
|
554
|
+
}
|
|
555
|
+
lines.push("export interface Api {");
|
|
556
|
+
for (const moduleName of moduleMap.keys()) {
|
|
557
|
+
const interfaceName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1) + "Api";
|
|
558
|
+
lines.push(` ${moduleName}: ${interfaceName};`);
|
|
559
|
+
}
|
|
560
|
+
lines.push("}");
|
|
561
|
+
} else {
|
|
562
|
+
lines.push("/**");
|
|
563
|
+
lines.push(" * Flexible API type that allows access to any function path.");
|
|
564
|
+
lines.push(" * Functions are accessed as api.moduleName.functionName");
|
|
565
|
+
lines.push(" * The actual types depend on your function definitions.");
|
|
566
|
+
lines.push(" */");
|
|
567
|
+
lines.push("export type Api = {");
|
|
568
|
+
lines.push(" [module: string]: {");
|
|
569
|
+
lines.push(" [fn: string]: ApiFunction;");
|
|
570
|
+
lines.push(" };");
|
|
571
|
+
lines.push("};");
|
|
572
|
+
}
|
|
573
|
+
lines.push("");
|
|
574
|
+
lines.push("// API client proxy - provides typed access to your functions");
|
|
575
|
+
lines.push("// On the client: returns { _name } references for useQuery/useMutation");
|
|
576
|
+
lines.push("// In Tether functions: calls the actual function implementation");
|
|
577
|
+
lines.push("export const api = createApiProxy<Api>();");
|
|
578
|
+
lines.push("");
|
|
579
|
+
return lines.join("\n");
|
|
580
|
+
}
|
|
581
|
+
async function fetchEnvVarKeys(projectId, environment, apiUrl) {
|
|
582
|
+
const credentials = await getCredentials();
|
|
583
|
+
if (!credentials) return [];
|
|
584
|
+
const baseUrl = apiUrl || `${await resolveApiUrl()}/api/v1`;
|
|
585
|
+
const envPath = environment !== "production" ? `/projects/${projectId}/env/${environment}/env-vars` : `/projects/${projectId}/env-vars`;
|
|
586
|
+
try {
|
|
587
|
+
const response = await fetch(`${baseUrl}${envPath}`, {
|
|
588
|
+
headers: {
|
|
589
|
+
"Authorization": `Bearer ${credentials.accessToken}`
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
if (!response.ok) return [];
|
|
593
|
+
const data = await response.json();
|
|
594
|
+
return (data.envVars || []).map((v) => v.key).sort();
|
|
595
|
+
} catch {
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function generateEnvFile(keys) {
|
|
600
|
+
const lines = [
|
|
601
|
+
"// Auto-generated by Tether CLI - do not edit manually",
|
|
602
|
+
`// Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
603
|
+
"",
|
|
604
|
+
"/**",
|
|
605
|
+
" * Typed environment variables for this project.",
|
|
606
|
+
" * Keys are fetched from the Tether API \u2014 values are never exposed.",
|
|
607
|
+
' * Use `env.get("KEY")` in your functions to access them.',
|
|
608
|
+
" */",
|
|
609
|
+
""
|
|
610
|
+
];
|
|
611
|
+
if (keys.length > 0) {
|
|
612
|
+
lines.push("export type EnvVarKey =");
|
|
613
|
+
for (let i = 0; i < keys.length; i++) {
|
|
614
|
+
const sep = i === keys.length - 1 ? ";" : "";
|
|
615
|
+
lines.push(` | '${keys[i]}'${sep}`);
|
|
616
|
+
}
|
|
617
|
+
lines.push("");
|
|
618
|
+
lines.push("export interface TetherEnv {");
|
|
619
|
+
for (const key of keys) {
|
|
620
|
+
lines.push(` ${key}: string;`);
|
|
621
|
+
}
|
|
622
|
+
lines.push("}");
|
|
623
|
+
} else {
|
|
624
|
+
lines.push("export type EnvVarKey = string;");
|
|
625
|
+
lines.push("");
|
|
626
|
+
lines.push("export interface TetherEnv {");
|
|
627
|
+
lines.push(" [key: string]: string;");
|
|
628
|
+
lines.push("}");
|
|
629
|
+
}
|
|
630
|
+
lines.push("");
|
|
631
|
+
return lines.join("\n");
|
|
632
|
+
}
|
|
633
|
+
async function generateTypes(options = {}) {
|
|
634
|
+
const config = await loadConfig();
|
|
635
|
+
const schemaPath = resolvePath(config.schema);
|
|
636
|
+
const outputDir = resolvePath(config.output);
|
|
637
|
+
const functionsDir = resolvePath(config.functions);
|
|
638
|
+
if (!await fs3.pathExists(schemaPath)) {
|
|
639
|
+
throw new Error(`Schema file not found: ${schemaPath}`);
|
|
640
|
+
}
|
|
641
|
+
const schemaSource = await fs3.readFile(schemaPath, "utf-8");
|
|
642
|
+
const tables = parseSchemaFile(schemaSource);
|
|
643
|
+
const environment = config.environment || await resolveEnvironmentFromApiKey() || "development";
|
|
644
|
+
let projectId;
|
|
645
|
+
const envFilePath = path3.resolve(process.cwd(), ".env");
|
|
646
|
+
if (await fs3.pathExists(envFilePath)) {
|
|
647
|
+
const envContent = await fs3.readFile(envFilePath, "utf-8");
|
|
648
|
+
const match = envContent.match(/TETHER_PROJECT_ID=(.+)/);
|
|
649
|
+
projectId = match?.[1]?.trim();
|
|
650
|
+
}
|
|
651
|
+
const apiUrl = `${await resolveApiUrl(config)}/api/v1`;
|
|
652
|
+
const envVarKeys = projectId ? await fetchEnvVarKeys(projectId, environment, apiUrl) : [];
|
|
653
|
+
await fs3.ensureDir(outputDir);
|
|
654
|
+
await fs3.writeFile(
|
|
655
|
+
path3.join(outputDir, "db.ts"),
|
|
656
|
+
generateDbFile(tables, schemaSource)
|
|
657
|
+
);
|
|
658
|
+
await fs3.writeFile(
|
|
659
|
+
path3.join(outputDir, "api.ts"),
|
|
660
|
+
await generateApiFile(functionsDir)
|
|
661
|
+
);
|
|
662
|
+
await fs3.writeFile(
|
|
663
|
+
path3.join(outputDir, "env.ts"),
|
|
664
|
+
generateEnvFile(envVarKeys)
|
|
665
|
+
);
|
|
666
|
+
await fs3.writeFile(
|
|
667
|
+
path3.join(outputDir, "index.ts"),
|
|
668
|
+
`// Auto-generated by Tether CLI - do not edit manually
|
|
669
|
+
export * from './db';
|
|
670
|
+
export * from './api';
|
|
671
|
+
export * from './env';
|
|
672
|
+
`
|
|
673
|
+
);
|
|
674
|
+
return { tables, envVarKeys, outputDir };
|
|
675
|
+
}
|
|
676
|
+
async function generateCommand() {
|
|
677
|
+
await requireAuth();
|
|
678
|
+
const configPath = path3.resolve(process.cwd(), "tether.config.ts");
|
|
679
|
+
if (!await fs3.pathExists(configPath)) {
|
|
680
|
+
console.log(chalk2.red("\nError: Not a Tether project"));
|
|
681
|
+
console.log(chalk2.dim("Run `tthr init` to create a new project\n"));
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
console.log(chalk2.bold("\n\u26A1 Generating types from schema\n"));
|
|
685
|
+
const spinner = ora("Reading schema...").start();
|
|
686
|
+
try {
|
|
687
|
+
spinner.text = "Generating types...";
|
|
688
|
+
const { tables, envVarKeys, outputDir } = await generateTypes();
|
|
689
|
+
if (tables.length === 0) {
|
|
690
|
+
spinner.warn("No tables found in schema");
|
|
691
|
+
console.log(chalk2.dim(" Make sure your schema uses defineSchema({ ... })\n"));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
spinner.succeed(`Types generated for ${tables.length} table(s)`);
|
|
695
|
+
console.log("\n" + chalk2.green("\u2713") + " Tables:");
|
|
696
|
+
for (const table of tables) {
|
|
697
|
+
console.log(chalk2.dim(` - ${table.name} (${table.columns.length} columns)`));
|
|
698
|
+
}
|
|
699
|
+
if (envVarKeys.length > 0) {
|
|
700
|
+
console.log("\n" + chalk2.green("\u2713") + ` Environment variables: ${envVarKeys.length} key(s)`);
|
|
701
|
+
for (const key of envVarKeys) {
|
|
702
|
+
console.log(chalk2.dim(` - ${key}`));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const relativeOutput = path3.relative(process.cwd(), outputDir);
|
|
706
|
+
console.log("\n" + chalk2.green("\u2713") + " Generated files:");
|
|
707
|
+
console.log(chalk2.dim(` ${relativeOutput}/db.ts`));
|
|
708
|
+
console.log(chalk2.dim(` ${relativeOutput}/api.ts`));
|
|
709
|
+
console.log(chalk2.dim(` ${relativeOutput}/env.ts`));
|
|
710
|
+
console.log(chalk2.dim(` ${relativeOutput}/index.ts
|
|
711
|
+
`));
|
|
712
|
+
} catch (error) {
|
|
713
|
+
spinner.fail("Failed to generate types");
|
|
714
|
+
console.error(chalk2.red(error instanceof Error ? error.message : "Unknown error"));
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export {
|
|
720
|
+
loadConfig,
|
|
721
|
+
resolveEnvironmentFromApiKey,
|
|
722
|
+
getApiUrl,
|
|
723
|
+
resolveApiUrl,
|
|
724
|
+
resolvePath,
|
|
725
|
+
detectFramework,
|
|
726
|
+
getFrameworkDevCommand,
|
|
727
|
+
getCredentials,
|
|
728
|
+
requireAuth,
|
|
729
|
+
saveCredentials,
|
|
730
|
+
clearCredentials,
|
|
731
|
+
generateTypes,
|
|
732
|
+
generateCommand
|
|
733
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -4,15 +4,16 @@ import {
|
|
|
4
4
|
detectFramework,
|
|
5
5
|
generateCommand,
|
|
6
6
|
generateTypes,
|
|
7
|
-
getApiUrl,
|
|
7
|
+
getApiUrl as getApiUrl2,
|
|
8
8
|
getCredentials,
|
|
9
9
|
getFrameworkDevCommand,
|
|
10
10
|
loadConfig,
|
|
11
11
|
requireAuth,
|
|
12
|
+
resolveApiUrl,
|
|
12
13
|
resolveEnvironmentFromApiKey,
|
|
13
14
|
resolvePath,
|
|
14
15
|
saveCredentials
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-DR6CWO75.js";
|
|
16
17
|
|
|
17
18
|
// src/index.ts
|
|
18
19
|
import { Command } from "commander";
|
|
@@ -58,11 +59,11 @@ async function deployCommand(options) {
|
|
|
58
59
|
process.exit(1);
|
|
59
60
|
}
|
|
60
61
|
const environment = options.env || config.environment || await resolveEnvironmentFromApiKey() || "development";
|
|
61
|
-
const
|
|
62
|
+
const API_URL2 = `${getApiUrl(config)}/api/v1`;
|
|
62
63
|
console.log(chalk.bold("\n\u26A1 Deploying to Tether\n"));
|
|
63
64
|
console.log(chalk.dim(` Project: ${projectId}`));
|
|
64
65
|
console.log(chalk.dim(` Environment: ${environment}`));
|
|
65
|
-
console.log(chalk.dim(` API: ${
|
|
66
|
+
console.log(chalk.dim(` API: ${API_URL2}
|
|
66
67
|
`));
|
|
67
68
|
console.log(chalk.dim(" Generating types..."));
|
|
68
69
|
try {
|
|
@@ -75,16 +76,16 @@ async function deployCommand(options) {
|
|
|
75
76
|
const deployFunctions = options.functions || !options.schema && !options.functions;
|
|
76
77
|
if (deploySchema) {
|
|
77
78
|
const schemaPath = resolvePath(config.schema);
|
|
78
|
-
await deploySchemaToServer(projectId, credentials.accessToken, schemaPath, environment, options.dryRun,
|
|
79
|
+
await deploySchemaToServer(projectId, credentials.accessToken, schemaPath, environment, options.dryRun, API_URL2);
|
|
79
80
|
}
|
|
80
81
|
if (deployFunctions) {
|
|
81
82
|
const functionsDir = resolvePath(config.functions);
|
|
82
|
-
await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, environment, options.dryRun,
|
|
83
|
+
await deployFunctionsToServer(projectId, credentials.accessToken, functionsDir, environment, options.dryRun, API_URL2);
|
|
83
84
|
}
|
|
84
85
|
console.log(chalk.green("\n\u2713 Deployment complete\n"));
|
|
85
86
|
}
|
|
86
87
|
async function deploySchemaToServer(projectId, token, schemaPath, environment, dryRun, apiUrl) {
|
|
87
|
-
const
|
|
88
|
+
const API_URL2 = apiUrl || `${await resolveApiUrl()}/api/v1`;
|
|
88
89
|
const spinner = ora("Reading schema...").start();
|
|
89
90
|
try {
|
|
90
91
|
if (!await fs.pathExists(schemaPath)) {
|
|
@@ -114,7 +115,7 @@ async function deploySchemaToServer(projectId, token, schemaPath, environment, d
|
|
|
114
115
|
return;
|
|
115
116
|
}
|
|
116
117
|
spinner.text = "Deploying schema...";
|
|
117
|
-
const schemaUrl = `${
|
|
118
|
+
const schemaUrl = `${API_URL2}/projects/${projectId}/env/${environment}/deploy/schema`;
|
|
118
119
|
console.log(chalk.dim(`
|
|
119
120
|
URL: ${schemaUrl}`));
|
|
120
121
|
const rlsConfigs = tables.filter((t) => t.rls).map((t) => ({
|
|
@@ -189,7 +190,7 @@ ${messages.join("\n")}`);
|
|
|
189
190
|
return result.outputFiles[0].text;
|
|
190
191
|
}
|
|
191
192
|
async function deployFunctionsToServer(projectId, token, functionsDir, environment, dryRun, apiUrl) {
|
|
192
|
-
const
|
|
193
|
+
const API_URL2 = apiUrl || `${await resolveApiUrl()}/api/v1`;
|
|
193
194
|
const spinner = ora("Reading functions...").start();
|
|
194
195
|
try {
|
|
195
196
|
if (!await fs.pathExists(functionsDir)) {
|
|
@@ -246,7 +247,7 @@ ${helperSource}`;
|
|
|
246
247
|
return;
|
|
247
248
|
}
|
|
248
249
|
spinner.text = "Deploying functions...";
|
|
249
|
-
const functionsUrl = `${
|
|
250
|
+
const functionsUrl = `${API_URL2}/projects/${projectId}/env/${environment}/deploy/functions`;
|
|
250
251
|
console.log(chalk.dim(`
|
|
251
252
|
URL: ${functionsUrl}`));
|
|
252
253
|
const response = await fetch(functionsUrl, {
|
|
@@ -584,7 +585,6 @@ function parseFunctions(moduleName, source) {
|
|
|
584
585
|
}
|
|
585
586
|
|
|
586
587
|
// src/commands/init.ts
|
|
587
|
-
var API_URL = `${getApiUrl()}/api/v1`;
|
|
588
588
|
async function getLatestVersion(packageName) {
|
|
589
589
|
try {
|
|
590
590
|
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
@@ -686,7 +686,8 @@ ${packageManager} is not installed on your system.`));
|
|
|
686
686
|
await scaffoldVanillaProject(projectName, projectPath, spinner);
|
|
687
687
|
}
|
|
688
688
|
spinner.text = "Creating project on Tether...";
|
|
689
|
-
const
|
|
689
|
+
const API_URL2 = `${await resolveApiUrl()}/api/v1`;
|
|
690
|
+
const response = await fetch(`${API_URL2}/projects`, {
|
|
690
691
|
method: "POST",
|
|
691
692
|
headers: {
|
|
692
693
|
"Content-Type": "application/json",
|
|
@@ -1704,7 +1705,7 @@ async function runGenerate(spinner) {
|
|
|
1704
1705
|
}
|
|
1705
1706
|
isGenerating = true;
|
|
1706
1707
|
try {
|
|
1707
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1708
|
+
const { generateTypes: generateTypes2 } = await import("./generate-M7G5AXMR.js");
|
|
1708
1709
|
spinner.text = "Regenerating types...";
|
|
1709
1710
|
await generateTypes2({ silent: true });
|
|
1710
1711
|
spinner.succeed("Types regenerated");
|
|
@@ -1812,7 +1813,7 @@ async function validateFunctions(functionsDir) {
|
|
|
1812
1813
|
return { valid: errors.length === 0, errors };
|
|
1813
1814
|
}
|
|
1814
1815
|
function connectToEnvironment(projectId, environment, token, spinner, apiBase) {
|
|
1815
|
-
const base = apiBase ||
|
|
1816
|
+
const base = apiBase || getApiUrl2();
|
|
1816
1817
|
const wsBase = base.replace("https://", "wss://").replace("http://", "ws://");
|
|
1817
1818
|
const wsUrl = environment && environment !== "production" ? `${wsBase}/ws/${projectId}/${environment}?token=${encodeURIComponent(token)}` : `${wsBase}/ws/${projectId}?token=${encodeURIComponent(token)}`;
|
|
1818
1819
|
let reconnectAttempts = 0;
|
|
@@ -1934,7 +1935,7 @@ async function devCommand(options) {
|
|
|
1934
1935
|
const devCmd = config.dev?.command || getFrameworkDevCommand(framework);
|
|
1935
1936
|
const environment = options.env || config.environment || "development";
|
|
1936
1937
|
const isLocal = options.local ?? false;
|
|
1937
|
-
const apiUrl = `${
|
|
1938
|
+
const apiUrl = `${getApiUrl2(config)}/api/v1`;
|
|
1938
1939
|
console.log(chalk3.bold("\n\u26A1 Starting Tether development server\n"));
|
|
1939
1940
|
if (framework !== "unknown") {
|
|
1940
1941
|
console.log(chalk3.dim(` Framework: ${framework}`));
|
|
@@ -1976,7 +1977,7 @@ async function devCommand(options) {
|
|
|
1976
1977
|
}
|
|
1977
1978
|
}
|
|
1978
1979
|
spinner.text = "Generating types...";
|
|
1979
|
-
const { generateTypes: generateTypes2 } = await import("./generate-
|
|
1980
|
+
const { generateTypes: generateTypes2 } = await import("./generate-M7G5AXMR.js");
|
|
1980
1981
|
await generateTypes2({ silent: true });
|
|
1981
1982
|
spinner.succeed("Types generated");
|
|
1982
1983
|
if (config.projectId) {
|
|
@@ -2033,7 +2034,7 @@ async function devCommand(options) {
|
|
|
2033
2034
|
}
|
|
2034
2035
|
spinner.succeed("File watchers ready");
|
|
2035
2036
|
if (config.projectId) {
|
|
2036
|
-
connectToEnvironment(config.projectId, environment, credentials.accessToken, spinner,
|
|
2037
|
+
connectToEnvironment(config.projectId, environment, credentials.accessToken, spinner, getApiUrl2(config));
|
|
2037
2038
|
}
|
|
2038
2039
|
if (devCmd && !options.skipFramework) {
|
|
2039
2040
|
spinner.start(`Starting ${framework} dev server...`);
|
|
@@ -2061,8 +2062,9 @@ import ora4 from "ora";
|
|
|
2061
2062
|
import os from "os";
|
|
2062
2063
|
import { exec } from "child_process";
|
|
2063
2064
|
import readline from "readline";
|
|
2064
|
-
var
|
|
2065
|
+
var API_URL = "";
|
|
2065
2066
|
async function loginCommand() {
|
|
2067
|
+
API_URL = `${await resolveApiUrl()}/api/v1`;
|
|
2066
2068
|
console.log(chalk4.bold("\u26A1 Login to Tether\n"));
|
|
2067
2069
|
const existing = await getCredentials();
|
|
2068
2070
|
if (existing) {
|
|
@@ -2070,6 +2072,8 @@ async function loginCommand() {
|
|
|
2070
2072
|
console.log(chalk4.dim("\nRun `tthr logout` to sign out\n"));
|
|
2071
2073
|
return;
|
|
2072
2074
|
}
|
|
2075
|
+
console.log(chalk4.dim(` URL: ${API_URL}
|
|
2076
|
+
`));
|
|
2073
2077
|
const spinner = ora4("Generating authentication code...").start();
|
|
2074
2078
|
try {
|
|
2075
2079
|
const deviceCode = await requestDeviceCode();
|
|
@@ -2080,10 +2084,13 @@ async function loginCommand() {
|
|
|
2080
2084
|
`));
|
|
2081
2085
|
console.log(chalk4.dim(`Your code: ${chalk4.white.bold(deviceCode.userCode)}
|
|
2082
2086
|
`));
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
+
const pollPromise = pollForApproval(deviceCode.deviceCode, deviceCode.interval, deviceCode.expiresIn);
|
|
2088
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2089
|
+
rl.question(chalk4.dim("Press Enter to open in browser..."), () => {
|
|
2090
|
+
openBrowser(authUrl);
|
|
2091
|
+
});
|
|
2092
|
+
const credentials = await pollPromise;
|
|
2093
|
+
rl.close();
|
|
2087
2094
|
await saveCredentials(credentials);
|
|
2088
2095
|
console.log(chalk4.green("\u2713") + ` Logged in as ${chalk4.cyan(credentials.email)}`);
|
|
2089
2096
|
console.log();
|
|
@@ -2150,7 +2157,7 @@ async function requestDeviceCode() {
|
|
|
2150
2157
|
const userCode = generateUserCode();
|
|
2151
2158
|
const deviceCode = crypto.randomUUID();
|
|
2152
2159
|
const deviceName = os.hostname();
|
|
2153
|
-
const response = await fetch(`${
|
|
2160
|
+
const response = await fetch(`${API_URL}/auth/device`, {
|
|
2154
2161
|
method: "POST",
|
|
2155
2162
|
headers: { "Content-Type": "application/json" },
|
|
2156
2163
|
body: JSON.stringify({ userCode, deviceCode, deviceName })
|
|
@@ -2158,7 +2165,7 @@ async function requestDeviceCode() {
|
|
|
2158
2165
|
if (response?.ok) {
|
|
2159
2166
|
return await response.json();
|
|
2160
2167
|
}
|
|
2161
|
-
const baseUrl =
|
|
2168
|
+
const baseUrl = API_URL.replace(/\/api\/v1$/, "");
|
|
2162
2169
|
return {
|
|
2163
2170
|
deviceCode,
|
|
2164
2171
|
userCode,
|
|
@@ -2186,9 +2193,12 @@ async function pollForApproval(deviceCode, interval, expiresIn) {
|
|
|
2186
2193
|
while (Date.now() < expiresAt) {
|
|
2187
2194
|
await sleep(interval * 1e3);
|
|
2188
2195
|
spinner.text = `Checking... ${chalk4.dim(`(${Math.ceil((expiresAt - Date.now()) / 1e3)}s remaining)`)}`;
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2196
|
+
let response = null;
|
|
2197
|
+
try {
|
|
2198
|
+
response = await fetch(`${API_URL}/auth/device/${deviceCode}`);
|
|
2199
|
+
} catch (err) {
|
|
2200
|
+
spinner.text = `Waiting for approval... ${chalk4.dim(`(connection error, retrying)`)}`;
|
|
2201
|
+
}
|
|
2192
2202
|
updateCountdown();
|
|
2193
2203
|
if (response?.ok) {
|
|
2194
2204
|
const data = await response.json();
|
|
@@ -2204,6 +2214,8 @@ async function pollForApproval(deviceCode, interval, expiresIn) {
|
|
|
2204
2214
|
spinner.fail("Authentication expired");
|
|
2205
2215
|
throw new Error("Authentication request expired");
|
|
2206
2216
|
}
|
|
2217
|
+
} else if (response) {
|
|
2218
|
+
spinner.text = `Waiting for approval... ${chalk4.dim(`(server: ${response.status})`)}`;
|
|
2207
2219
|
}
|
|
2208
2220
|
}
|
|
2209
2221
|
spinner.fail("Authentication timed out");
|
|
@@ -2231,18 +2243,6 @@ function openBrowser(url) {
|
|
|
2231
2243
|
}
|
|
2232
2244
|
});
|
|
2233
2245
|
}
|
|
2234
|
-
function waitForEnter(prompt) {
|
|
2235
|
-
return new Promise((resolve) => {
|
|
2236
|
-
const rl = readline.createInterface({
|
|
2237
|
-
input: process.stdin,
|
|
2238
|
-
output: process.stdout
|
|
2239
|
-
});
|
|
2240
|
-
rl.question(prompt, () => {
|
|
2241
|
-
rl.close();
|
|
2242
|
-
resolve();
|
|
2243
|
-
});
|
|
2244
|
-
});
|
|
2245
|
-
}
|
|
2246
2246
|
|
|
2247
2247
|
// src/commands/update.ts
|
|
2248
2248
|
import chalk5 from "chalk";
|
|
@@ -2418,10 +2418,10 @@ async function execCommand(sql) {
|
|
|
2418
2418
|
process.exit(1);
|
|
2419
2419
|
}
|
|
2420
2420
|
const config = await loadConfig();
|
|
2421
|
-
const
|
|
2421
|
+
const API_URL2 = `${getApiUrl2(config)}/api/v1`;
|
|
2422
2422
|
const spinner = ora6("Executing query...").start();
|
|
2423
2423
|
try {
|
|
2424
|
-
const response = await fetch(`${
|
|
2424
|
+
const response = await fetch(`${API_URL2}/projects/${projectId}/exec`, {
|
|
2425
2425
|
method: "POST",
|
|
2426
2426
|
headers: {
|
|
2427
2427
|
"Content-Type": "application/json",
|
|
@@ -2471,9 +2471,9 @@ import chalk7 from "chalk";
|
|
|
2471
2471
|
import ora7 from "ora";
|
|
2472
2472
|
import fs6 from "fs-extra";
|
|
2473
2473
|
import path6 from "path";
|
|
2474
|
-
async function
|
|
2474
|
+
async function resolveApiUrl2() {
|
|
2475
2475
|
const config = await loadConfig();
|
|
2476
|
-
return `${
|
|
2476
|
+
return `${getApiUrl2(config)}/api/v1`;
|
|
2477
2477
|
}
|
|
2478
2478
|
async function getProjectId() {
|
|
2479
2479
|
const envPath = path6.resolve(process.cwd(), ".env");
|
|
@@ -2493,10 +2493,10 @@ async function getProjectId() {
|
|
|
2493
2493
|
async function envListCommand() {
|
|
2494
2494
|
const credentials = await requireAuth();
|
|
2495
2495
|
const projectId = await getProjectId();
|
|
2496
|
-
const
|
|
2496
|
+
const API_URL2 = await resolveApiUrl2();
|
|
2497
2497
|
const spinner = ora7("Fetching environments...").start();
|
|
2498
2498
|
try {
|
|
2499
|
-
const response = await fetch(`${
|
|
2499
|
+
const response = await fetch(`${API_URL2}/projects/${projectId}/environments`, {
|
|
2500
2500
|
headers: {
|
|
2501
2501
|
"Authorization": `Bearer ${credentials.accessToken}`
|
|
2502
2502
|
}
|
|
@@ -2526,7 +2526,7 @@ async function envListCommand() {
|
|
|
2526
2526
|
async function envCreateCommand(name, options) {
|
|
2527
2527
|
const credentials = await requireAuth();
|
|
2528
2528
|
const projectId = await getProjectId();
|
|
2529
|
-
const
|
|
2529
|
+
const API_URL2 = await resolveApiUrl2();
|
|
2530
2530
|
const normalizedName = name.toLowerCase();
|
|
2531
2531
|
if (!/^[a-z][a-z0-9-_]*$/.test(normalizedName)) {
|
|
2532
2532
|
console.log(chalk7.red("\nError: Invalid environment name"));
|
|
@@ -2540,7 +2540,7 @@ async function envCreateCommand(name, options) {
|
|
|
2540
2540
|
body.cloneFrom = options.from;
|
|
2541
2541
|
spinner.text = `Creating environment '${normalizedName}' from '${options.from}'...`;
|
|
2542
2542
|
}
|
|
2543
|
-
const response = await fetch(`${
|
|
2543
|
+
const response = await fetch(`${API_URL2}/projects/${projectId}/environments`, {
|
|
2544
2544
|
method: "POST",
|
|
2545
2545
|
headers: {
|
|
2546
2546
|
"Content-Type": "application/json",
|
|
@@ -2569,10 +2569,10 @@ async function envCreateCommand(name, options) {
|
|
|
2569
2569
|
async function envDeleteCommand(name) {
|
|
2570
2570
|
const credentials = await requireAuth();
|
|
2571
2571
|
const projectId = await getProjectId();
|
|
2572
|
-
const
|
|
2572
|
+
const API_URL2 = await resolveApiUrl2();
|
|
2573
2573
|
const spinner = ora7(`Deleting environment '${name}'...`).start();
|
|
2574
2574
|
try {
|
|
2575
|
-
const response = await fetch(`${
|
|
2575
|
+
const response = await fetch(`${API_URL2}/projects/${projectId}/environments/${name}`, {
|
|
2576
2576
|
method: "DELETE",
|
|
2577
2577
|
headers: {
|
|
2578
2578
|
"Authorization": `Bearer ${credentials.accessToken}`
|
|
@@ -2592,10 +2592,10 @@ async function envDeleteCommand(name) {
|
|
|
2592
2592
|
async function envDefaultCommand(name) {
|
|
2593
2593
|
const credentials = await requireAuth();
|
|
2594
2594
|
const projectId = await getProjectId();
|
|
2595
|
-
const
|
|
2595
|
+
const API_URL2 = await resolveApiUrl2();
|
|
2596
2596
|
const spinner = ora7(`Setting '${name}' as default environment...`).start();
|
|
2597
2597
|
try {
|
|
2598
|
-
const response = await fetch(`${
|
|
2598
|
+
const response = await fetch(`${API_URL2}/projects/${projectId}/environments/default`, {
|
|
2599
2599
|
method: "PATCH",
|
|
2600
2600
|
headers: {
|
|
2601
2601
|
"Content-Type": "application/json",
|
|
@@ -2661,8 +2661,8 @@ async function migrateSystemColumnsCommand(options) {
|
|
|
2661
2661
|
const spinner = ora8("Analysing database tables...").start();
|
|
2662
2662
|
try {
|
|
2663
2663
|
const config = await loadConfig();
|
|
2664
|
-
const
|
|
2665
|
-
const baseUrl = `${
|
|
2664
|
+
const API_URL2 = `${getApiUrl2(config)}/api/v1`;
|
|
2665
|
+
const baseUrl = `${API_URL2}/projects/${projectId}/env/${environment}/migrate/system-columns`;
|
|
2666
2666
|
const dryRunResponse = await fetch(`${baseUrl}?dry_run=true`, {
|
|
2667
2667
|
method: "POST",
|
|
2668
2668
|
headers: {
|