supatypes 1.0.3 → 1.0.4
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/bin/cli.js +15 -0
- package/lib/generate.js +2 -0
- package/lib/remote-generator.js +86 -14
- package/lib/test-connection.js +126 -0
- package/package.json +5 -2
package/bin/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { init } from "../lib/init.js";
|
|
5
5
|
import { generate } from "../lib/generate.js";
|
|
6
|
+
import { testConnection } from "../lib/test-connection.js";
|
|
6
7
|
|
|
7
8
|
const HELP = `
|
|
8
9
|
supatypes — Generate TypeScript types from a remote Supabase database
|
|
@@ -10,6 +11,7 @@ supatypes — Generate TypeScript types from a remote Supabase database
|
|
|
10
11
|
Usage:
|
|
11
12
|
supatypes init Create a config file in the current directory
|
|
12
13
|
supatypes generate [options] Generate types from the remote database
|
|
14
|
+
supatypes test [options] Test SSH and database connectivity
|
|
13
15
|
supatypes help Show this help message
|
|
14
16
|
|
|
15
17
|
Options (generate):
|
|
@@ -67,6 +69,19 @@ if (command === "generate") {
|
|
|
67
69
|
process.exit(0);
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
if (command === "test") {
|
|
73
|
+
const { values } = parseArgs({
|
|
74
|
+
args: process.argv.slice(3),
|
|
75
|
+
options: {
|
|
76
|
+
config: { type: "string", short: "c", default: "./.supatypes.json" },
|
|
77
|
+
},
|
|
78
|
+
strict: false,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await testConnection({ configPath: values.config });
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
70
85
|
console.error(`Unknown command: ${command}`);
|
|
71
86
|
console.log('Run "supatypes help" for usage.');
|
|
72
87
|
process.exit(1);
|
package/lib/generate.js
CHANGED
|
@@ -80,6 +80,8 @@ export async function generate({ configPath, outputOverride, dryRun }) {
|
|
|
80
80
|
console.log(`\n Tables: ${stats.tables}`);
|
|
81
81
|
console.log(` Views: ${stats.views}`);
|
|
82
82
|
console.log(` Functions: ${stats.functions}`);
|
|
83
|
+
console.log(` Enums: ${stats.enums}`);
|
|
84
|
+
console.log(` Composites:${stats.compositeTypes ? " " + stats.compositeTypes : " 0"}`);
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
// Step 4: Download result
|
package/lib/remote-generator.js
CHANGED
|
@@ -23,10 +23,51 @@ const schema = execSync(
|
|
|
23
23
|
{ encoding: "utf8", maxBuffer: 50 * 1024 * 1024 }
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
-
// Step 2: Parse
|
|
26
|
+
// Step 2: Parse enums
|
|
27
|
+
const enums = {};
|
|
28
|
+
const enumRegex = /CREATE TYPE (?:public\.)?(\w+) AS ENUM\s*\(\s*([\s\S]*?)\)/g;
|
|
29
|
+
let match;
|
|
30
|
+
|
|
31
|
+
while ((match = enumRegex.exec(schema)) !== null) {
|
|
32
|
+
const enumName = match[1];
|
|
33
|
+
const valuesStr = match[2];
|
|
34
|
+
const values = valuesStr
|
|
35
|
+
.split(",")
|
|
36
|
+
.map((v) => v.trim().replace(/^'|'$/g, ""))
|
|
37
|
+
.filter((v) => v.length > 0);
|
|
38
|
+
if (values.length > 0) {
|
|
39
|
+
enums[enumName] = values;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Step 3: Parse composite types
|
|
44
|
+
const compositeTypes = {};
|
|
45
|
+
const compositeRegex = /CREATE TYPE (?:public\.)?(\w+) AS\s*\(\s*([\s\S]*?)\)/g;
|
|
46
|
+
|
|
47
|
+
while ((match = compositeRegex.exec(schema)) !== null) {
|
|
48
|
+
const typeName = match[1];
|
|
49
|
+
if (enums[typeName]) continue; // skip enums already parsed
|
|
50
|
+
const fieldsStr = match[2];
|
|
51
|
+
const fields = [];
|
|
52
|
+
for (const line of fieldsStr.split(",").map((l) => l.trim())) {
|
|
53
|
+
const fieldMatch = line.match(
|
|
54
|
+
/^"?(\w+)"?\s+((?:character varying|double precision|timestamp with(?:out)? time zone|\w+)(?:\[\])?)/
|
|
55
|
+
);
|
|
56
|
+
if (fieldMatch) {
|
|
57
|
+
fields.push({
|
|
58
|
+
name: fieldMatch[1],
|
|
59
|
+
type: mapPostgresType(fieldMatch[2], enums),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (fields.length > 0) {
|
|
64
|
+
compositeTypes[typeName] = fields;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 4: Parse tables
|
|
27
69
|
const tables = {};
|
|
28
70
|
const tableRegex = /CREATE TABLE (?:public\.)?(\w+) \(([\s\S]*?)\);/g;
|
|
29
|
-
let match;
|
|
30
71
|
|
|
31
72
|
while ((match = tableRegex.exec(schema)) !== null) {
|
|
32
73
|
const tableName = match[1];
|
|
@@ -44,7 +85,7 @@ while ((match = tableRegex.exec(schema)) !== null) {
|
|
|
44
85
|
const [, name, type, constraints = ""] = colMatch;
|
|
45
86
|
columns.push({
|
|
46
87
|
name,
|
|
47
|
-
type: mapPostgresType(type),
|
|
88
|
+
type: mapPostgresType(type, enums, compositeTypes),
|
|
48
89
|
nullable: !constraints.includes("NOT NULL"),
|
|
49
90
|
hasDefault: constraints.includes("DEFAULT"),
|
|
50
91
|
});
|
|
@@ -144,7 +185,7 @@ while ((match = funcRegex.exec(schema)) !== null) {
|
|
|
144
185
|
if (paramMatch) {
|
|
145
186
|
params.push({
|
|
146
187
|
name: paramMatch[1],
|
|
147
|
-
type: mapPostgresType(paramMatch[2]),
|
|
188
|
+
type: mapPostgresType(paramMatch[2], enums, compositeTypes),
|
|
148
189
|
});
|
|
149
190
|
}
|
|
150
191
|
}
|
|
@@ -152,7 +193,7 @@ while ((match = funcRegex.exec(schema)) !== null) {
|
|
|
152
193
|
|
|
153
194
|
functions[funcName] = {
|
|
154
195
|
params,
|
|
155
|
-
returnType: returnType === "void" ? "undefined" : mapPostgresType(returnType),
|
|
196
|
+
returnType: returnType === "void" ? "undefined" : mapPostgresType(returnType, enums, compositeTypes),
|
|
156
197
|
};
|
|
157
198
|
}
|
|
158
199
|
|
|
@@ -217,17 +258,33 @@ if (funcEntries.length === 0) {
|
|
|
217
258
|
}
|
|
218
259
|
}
|
|
219
260
|
|
|
220
|
-
output += ` }
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
261
|
+
output += ` }\n Enums: {\n`;
|
|
262
|
+
|
|
263
|
+
const enumEntries = Object.entries(enums);
|
|
264
|
+
if (enumEntries.length === 0) {
|
|
265
|
+
output += ` [_ in never]: never\n`;
|
|
266
|
+
} else {
|
|
267
|
+
for (const [enumName, values] of enumEntries) {
|
|
268
|
+
output += ` ${enumName}: ${values.map((v) => `"${v}"`).join(" | ")}\n`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
output += ` }\n CompositeTypes: {\n`;
|
|
273
|
+
|
|
274
|
+
const compositeEntries = Object.entries(compositeTypes);
|
|
275
|
+
if (compositeEntries.length === 0) {
|
|
276
|
+
output += ` [_ in never]: never\n`;
|
|
277
|
+
} else {
|
|
278
|
+
for (const [typeName, fields] of compositeEntries) {
|
|
279
|
+
output += ` ${typeName}: {\n`;
|
|
280
|
+
for (const field of fields) {
|
|
281
|
+
output += ` ${field.name}: ${field.type}\n`;
|
|
226
282
|
}
|
|
283
|
+
output += ` }\n`;
|
|
227
284
|
}
|
|
228
285
|
}
|
|
229
286
|
|
|
230
|
-
|
|
287
|
+
output += ` }\n }\n}\n\ntype DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
|
|
231
288
|
|
|
232
289
|
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
|
|
233
290
|
|
|
@@ -346,7 +403,9 @@ export type CompositeTypes<
|
|
|
346
403
|
|
|
347
404
|
export const Constants = {
|
|
348
405
|
public: {
|
|
349
|
-
Enums: {
|
|
406
|
+
Enums: {${enumEntries.length > 0 ? "\n" + enumEntries.map(([name, values]) =>
|
|
407
|
+
` ${name}: [${values.map((v) => `"${v}"`).join(", ")}]`
|
|
408
|
+
).join(",\n") + ",\n " : ""}},
|
|
350
409
|
},
|
|
351
410
|
} as const
|
|
352
411
|
`;
|
|
@@ -358,15 +417,28 @@ const stats = {
|
|
|
358
417
|
tables: Object.keys(tables).length,
|
|
359
418
|
views: Object.keys(views).length,
|
|
360
419
|
functions: Object.keys(functions).length,
|
|
420
|
+
enums: Object.keys(enums).length,
|
|
421
|
+
compositeTypes: Object.keys(compositeTypes).length,
|
|
361
422
|
};
|
|
362
423
|
console.log("__STATS__" + JSON.stringify(stats));
|
|
363
424
|
|
|
364
425
|
// Helper functions
|
|
365
426
|
|
|
366
|
-
function mapPostgresType(pgType) {
|
|
427
|
+
function mapPostgresType(pgType, enumDefs, compositeDefs) {
|
|
367
428
|
const type = pgType.toLowerCase();
|
|
368
429
|
const isArray = type.endsWith("[]") || type.startsWith("_");
|
|
369
430
|
const baseType = type.replace("[]", "").replace(/^_/, "");
|
|
431
|
+
|
|
432
|
+
if (enumDefs && enumDefs[baseType]) {
|
|
433
|
+
const mapped = `Database["public"]["Enums"]["${baseType}"]`;
|
|
434
|
+
return isArray ? `(${mapped})[]` : mapped;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (compositeDefs && compositeDefs[baseType]) {
|
|
438
|
+
const mapped = `Database["public"]["CompositeTypes"]["${baseType}"]`;
|
|
439
|
+
return isArray ? `(${mapped})[]` : mapped;
|
|
440
|
+
}
|
|
441
|
+
|
|
370
442
|
const mapped = mapBaseType(baseType);
|
|
371
443
|
return isArray ? `${mapped}[]` : mapped;
|
|
372
444
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export async function testConnection({ configPath }) {
|
|
6
|
+
const resolvedConfig = path.resolve(configPath);
|
|
7
|
+
if (!fs.existsSync(resolvedConfig)) {
|
|
8
|
+
console.error(`Config file not found: ${resolvedConfig}`);
|
|
9
|
+
console.error('Run "npx supatypes init" to create one.');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const config = JSON.parse(fs.readFileSync(resolvedConfig, "utf8"));
|
|
14
|
+
const { server, dbContainer } = config;
|
|
15
|
+
const sshPort = config.sshPort || 22;
|
|
16
|
+
const sshKey = config.sshKey ? expandHome(config.sshKey) : null;
|
|
17
|
+
const sshPassword = config.sshPassword || null;
|
|
18
|
+
|
|
19
|
+
if (!server) { console.error("Missing 'server' in config"); process.exit(1); }
|
|
20
|
+
if (!dbContainer) { console.error("Missing 'dbContainer' in config"); process.exit(1); }
|
|
21
|
+
|
|
22
|
+
if (sshKey && !fs.existsSync(sshKey)) {
|
|
23
|
+
console.error(`SSH key not found: ${sshKey}`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sshBase = buildSshBase(server, sshPort, sshKey, sshPassword);
|
|
28
|
+
|
|
29
|
+
console.log("supatypes — connection test\n");
|
|
30
|
+
console.log(` Server: ${server}`);
|
|
31
|
+
console.log(` Port: ${sshPort}`);
|
|
32
|
+
console.log(` Container: ${dbContainer}`);
|
|
33
|
+
console.log(` Auth: ${sshKey ? "SSH key" : "password"}\n`);
|
|
34
|
+
|
|
35
|
+
// Test 1: SSH connectivity
|
|
36
|
+
process.stdout.write("1. Testing SSH connection...");
|
|
37
|
+
try {
|
|
38
|
+
ssh(sshBase, "echo ok");
|
|
39
|
+
console.log(" passed");
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.log(" FAILED");
|
|
42
|
+
console.error(` ${err.message}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Test 2: Docker available
|
|
47
|
+
process.stdout.write("2. Checking Docker access...");
|
|
48
|
+
try {
|
|
49
|
+
ssh(sshBase, "docker --version");
|
|
50
|
+
console.log(" passed");
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.log(" FAILED");
|
|
53
|
+
console.error(` Docker is not available on the server.`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Test 3: Container running
|
|
58
|
+
process.stdout.write(`3. Checking container '${dbContainer}'...`);
|
|
59
|
+
try {
|
|
60
|
+
const status = ssh(sshBase, `docker inspect -f '{{.State.Status}}' ${dbContainer}`).trim();
|
|
61
|
+
if (status === "running") {
|
|
62
|
+
console.log(" running");
|
|
63
|
+
} else {
|
|
64
|
+
console.log(` ${status}`);
|
|
65
|
+
console.error(` Container exists but is not running (status: ${status}).`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.log(" FAILED");
|
|
70
|
+
console.error(` Container '${dbContainer}' not found.`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Test 4: PostgreSQL accessible
|
|
75
|
+
process.stdout.write("4. Checking PostgreSQL access...");
|
|
76
|
+
try {
|
|
77
|
+
ssh(sshBase, `docker exec ${dbContainer} pg_isready -U postgres`);
|
|
78
|
+
console.log(" passed");
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.log(" FAILED");
|
|
81
|
+
console.error(` PostgreSQL is not ready in the container.`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Test 5: Node.js on server
|
|
86
|
+
process.stdout.write("5. Checking Node.js on server...");
|
|
87
|
+
try {
|
|
88
|
+
const nodeVersion = ssh(sshBase, "node --version").trim();
|
|
89
|
+
console.log(` ${nodeVersion}`);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.log(" FAILED");
|
|
92
|
+
console.error(` Node.js is not installed on the server. It's required to run the generator.`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log("\nAll checks passed. Ready to generate types.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function expandHome(p) {
|
|
100
|
+
if (p.startsWith("~/")) {
|
|
101
|
+
return path.join(process.env.HOME || process.env.USERPROFILE || "", p.slice(2));
|
|
102
|
+
}
|
|
103
|
+
return p;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildSshBase(server, port, sshKey, sshPassword) {
|
|
107
|
+
const opts = ["-p", String(port), "-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=10"];
|
|
108
|
+
|
|
109
|
+
if (sshPassword) {
|
|
110
|
+
return ["sshpass", "-p", sshPassword, "ssh", ...opts, server];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (sshKey) {
|
|
114
|
+
return ["ssh", "-i", sshKey, ...opts, server];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return ["ssh", ...opts, server];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ssh(base, command) {
|
|
121
|
+
const [bin, ...args] = base;
|
|
122
|
+
return execFileSync(bin, [...args, command], {
|
|
123
|
+
encoding: "utf8",
|
|
124
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
125
|
+
});
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supatypes",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Generate TypeScript types from a remote Supabase PostgreSQL database via SSH",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "MadStoneDev",
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
"postgresql",
|
|
22
22
|
"database"
|
|
23
23
|
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node --test test/"
|
|
26
|
+
},
|
|
24
27
|
"engines": {
|
|
25
28
|
"node": ">=18"
|
|
26
29
|
}
|
|
27
|
-
}
|
|
30
|
+
}
|