supatypes 1.0.2 → 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/README.md CHANGED
@@ -40,6 +40,7 @@ Running `init` creates a `.supatypes.json` file:
40
40
  ```json
41
41
  {
42
42
  "server": "root@1.2.3.4",
43
+ "sshPort": 2222,
43
44
  "sshKey": "~/.ssh/id_rsa",
44
45
  "dbContainer": "supabase-db-abc123",
45
46
  "output": "./database.types.ts"
@@ -51,6 +52,7 @@ Running `init` creates a `.supatypes.json` file:
51
52
  | Field | Required | Description |
52
53
  |-------|----------|-------------|
53
54
  | `server` | Yes | SSH connection string (e.g. `root@1.2.3.4`) |
55
+ | `sshPort` | No | SSH port (default: `22`) |
54
56
  | `sshKey` | One of these | Path to SSH private key |
55
57
  | `sshPassword` | One of these | SSH password (uses `sshpass`) |
56
58
  | `dbContainer` | Yes | Docker container name for the Supabase PostgreSQL instance |
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):
@@ -20,6 +22,7 @@ Options (generate):
20
22
  Config file (.supatypes.json):
21
23
  {
22
24
  "server": "root@1.2.3.4",
25
+ "sshPort": 2222,
23
26
  "sshKey": "~/.ssh/id_rsa",
24
27
  "dbContainer": "supabase-db-abc123",
25
28
  "output": "./database.types.ts"
@@ -28,6 +31,7 @@ Config file (.supatypes.json):
28
31
  You can also use a password instead of an SSH key:
29
32
  {
30
33
  "server": "root@1.2.3.4",
34
+ "sshPort": 2222,
31
35
  "sshPassword": "your-password",
32
36
  "dbContainer": "supabase-db-abc123",
33
37
  "output": "./database.types.ts"
@@ -65,6 +69,19 @@ if (command === "generate") {
65
69
  process.exit(0);
66
70
  }
67
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
+
68
85
  console.error(`Unknown command: ${command}`);
69
86
  console.log('Run "supatypes help" for usage.');
70
87
  process.exit(1);
package/lib/generate.js CHANGED
@@ -1,167 +1,171 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { execFileSync } from "node:child_process";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const REMOTE_SCRIPT = path.join(__dirname, "remote-generator.js");
8
- const REMOTE_DIR = "/tmp/supatypes";
9
- const REMOTE_SCRIPT_PATH = `${REMOTE_DIR}/generator.js`;
10
- const REMOTE_OUTPUT_PATH = `${REMOTE_DIR}/output.ts`;
11
-
12
- export async function generate({ configPath, outputOverride, dryRun }) {
13
- // Load config
14
- const resolvedConfig = path.resolve(configPath);
15
- if (!fs.existsSync(resolvedConfig)) {
16
- console.error(`Config file not found: ${resolvedConfig}`);
17
- console.error('Run "npx supatypes init" to create one.');
18
- process.exit(1);
19
- }
20
-
21
- const config = JSON.parse(fs.readFileSync(resolvedConfig, "utf8"));
22
- const { server, dbContainer } = config;
23
- const sshKey = config.sshKey ? expandHome(config.sshKey) : null;
24
- const sshPassword = config.sshPassword || null;
25
- const output = path.resolve(outputOverride || config.output || "./database.types.ts");
26
-
27
- // Validate
28
- if (!server) { console.error("Missing 'server' in config"); process.exit(1); }
29
- if (!dbContainer) { console.error("Missing 'dbContainer' in config"); process.exit(1); }
30
-
31
- if (sshKey && !fs.existsSync(sshKey)) {
32
- console.error(`SSH key not found: ${sshKey}`);
33
- process.exit(1);
34
- }
35
-
36
- const sshBase = buildSshBase(server, sshKey, sshPassword);
37
- const scpBase = buildScpBase(server, sshKey, sshPassword);
38
-
39
- if (dryRun) {
40
- console.log("Dry run — would execute:");
41
- console.log(` 1. Create ${REMOTE_DIR} on ${server}`);
42
- console.log(` 2. Upload generator script to ${REMOTE_SCRIPT_PATH}`);
43
- console.log(` 3. Run: node ${REMOTE_SCRIPT_PATH} ${dbContainer} ${REMOTE_OUTPUT_PATH}`);
44
- console.log(` 4. Download ${REMOTE_OUTPUT_PATH} to ${output}`);
45
- console.log(` 5. Clean up ${REMOTE_DIR} on server`);
46
- return;
47
- }
48
-
49
- console.log("supatypes\n");
50
- console.log(` Server: ${server}`);
51
- console.log(` Container: ${dbContainer}`);
52
- console.log(` Output: ${output}`);
53
- console.log(` Auth: ${sshKey ? "SSH key" : "password"}\n`);
54
-
55
- try {
56
- // Step 1: Create remote directory
57
- process.stdout.write("Creating remote workspace...");
58
- ssh(sshBase, `mkdir -p ${REMOTE_DIR}`);
59
- console.log(" done");
60
-
61
- // Step 2: Upload generator script
62
- process.stdout.write("Uploading generator...");
63
- scp(scpBase, REMOTE_SCRIPT, `${server}:${REMOTE_SCRIPT_PATH}`);
64
- console.log(" done");
65
-
66
- // Step 3: Run generator on server
67
- process.stdout.write("Generating types...");
68
- const remoteOutput = ssh(
69
- sshBase,
70
- `cd ${REMOTE_DIR} && node generator.js ${dbContainer} ${REMOTE_OUTPUT_PATH}`
71
- );
72
- console.log(" done");
73
-
74
- // Parse stats from output
75
- const statsMatch = remoteOutput.match(/__STATS__(.+)/);
76
- if (statsMatch) {
77
- const stats = JSON.parse(statsMatch[1]);
78
- console.log(`\n Tables: ${stats.tables}`);
79
- console.log(` Views: ${stats.views}`);
80
- console.log(` Functions: ${stats.functions}`);
81
- }
82
-
83
- // Step 4: Download result
84
- process.stdout.write("\nDownloading types...");
85
- const outputDir = path.dirname(output);
86
- if (!fs.existsSync(outputDir)) {
87
- fs.mkdirSync(outputDir, { recursive: true });
88
- }
89
- scp(scpBase, `${server}:${REMOTE_OUTPUT_PATH}`, output);
90
- console.log(" done");
91
-
92
- // Step 5: Clean up
93
- process.stdout.write("Cleaning up remote files...");
94
- ssh(sshBase, `rm -rf ${REMOTE_DIR}`);
95
- console.log(" done");
96
-
97
- const fileSize = fs.statSync(output).size;
98
- console.log(`\nTypes saved to ${output} (${formatBytes(fileSize)})`);
99
- } catch (err) {
100
- console.error(`\n\nFailed: ${err.message}`);
101
-
102
- // Try to clean up even on failure
103
- try {
104
- ssh(sshBase, `rm -rf ${REMOTE_DIR}`);
105
- } catch {
106
- // ignore cleanup errors
107
- }
108
-
109
- process.exit(1);
110
- }
111
- }
112
-
113
- function expandHome(p) {
114
- if (p.startsWith("~/")) {
115
- return path.join(process.env.HOME || process.env.USERPROFILE || "", p.slice(2));
116
- }
117
- return p;
118
- }
119
-
120
- function buildSshBase(server, sshKey, sshPassword) {
121
- const opts = ["-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=10"];
122
-
123
- if (sshPassword) {
124
- // Use sshpass for password-based auth
125
- return ["sshpass", "-p", sshPassword, "ssh", ...opts, server];
126
- }
127
-
128
- if (sshKey) {
129
- return ["ssh", "-i", sshKey, ...opts, server];
130
- }
131
-
132
- // Default: let SSH use its default key
133
- return ["ssh", ...opts, server];
134
- }
135
-
136
- function buildScpBase(server, sshKey, sshPassword) {
137
- const opts = ["-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=10"];
138
-
139
- if (sshPassword) {
140
- return ["sshpass", "-p", sshPassword, "scp", ...opts];
141
- }
142
-
143
- if (sshKey) {
144
- return ["scp", "-i", sshKey, ...opts];
145
- }
146
-
147
- return ["scp", ...opts];
148
- }
149
-
150
- function ssh(base, command) {
151
- const [bin, ...args] = base;
152
- return execFileSync(bin, [...args, command], {
153
- encoding: "utf8",
154
- maxBuffer: 50 * 1024 * 1024,
155
- });
156
- }
157
-
158
- function scp(base, src, dest) {
159
- const [bin, ...args] = base;
160
- execFileSync(bin, [...args, src, dest], { encoding: "utf8" });
161
- }
162
-
163
- function formatBytes(bytes) {
164
- if (bytes < 1024) return `${bytes} B`;
165
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
166
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
167
- }
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const REMOTE_SCRIPT = path.join(__dirname, "remote-generator.js");
8
+ const REMOTE_DIR = "/tmp/supatypes";
9
+ const REMOTE_SCRIPT_PATH = `${REMOTE_DIR}/generator.js`;
10
+ const REMOTE_OUTPUT_PATH = `${REMOTE_DIR}/output.ts`;
11
+
12
+ export async function generate({ configPath, outputOverride, dryRun }) {
13
+ // Load config
14
+ const resolvedConfig = path.resolve(configPath);
15
+ if (!fs.existsSync(resolvedConfig)) {
16
+ console.error(`Config file not found: ${resolvedConfig}`);
17
+ console.error('Run "npx supatypes init" to create one.');
18
+ process.exit(1);
19
+ }
20
+
21
+ const config = JSON.parse(fs.readFileSync(resolvedConfig, "utf8"));
22
+ const { server, dbContainer } = config;
23
+ const sshPort = config.sshPort || 22;
24
+ const sshKey = config.sshKey ? expandHome(config.sshKey) : null;
25
+ const sshPassword = config.sshPassword || null;
26
+ const output = path.resolve(outputOverride || config.output || "./database.types.ts");
27
+
28
+ // Validate
29
+ if (!server) { console.error("Missing 'server' in config"); process.exit(1); }
30
+ if (!dbContainer) { console.error("Missing 'dbContainer' in config"); process.exit(1); }
31
+
32
+ if (sshKey && !fs.existsSync(sshKey)) {
33
+ console.error(`SSH key not found: ${sshKey}`);
34
+ process.exit(1);
35
+ }
36
+
37
+ const sshBase = buildSshBase(server, sshPort, sshKey, sshPassword);
38
+ const scpBase = buildScpBase(server, sshPort, sshKey, sshPassword);
39
+
40
+ if (dryRun) {
41
+ console.log("Dry run would execute:");
42
+ console.log(` 1. Create ${REMOTE_DIR} on ${server}`);
43
+ console.log(` 2. Upload generator script to ${REMOTE_SCRIPT_PATH}`);
44
+ console.log(` 3. Run: node ${REMOTE_SCRIPT_PATH} ${dbContainer} ${REMOTE_OUTPUT_PATH}`);
45
+ console.log(` 4. Download ${REMOTE_OUTPUT_PATH} to ${output}`);
46
+ console.log(` 5. Clean up ${REMOTE_DIR} on server`);
47
+ return;
48
+ }
49
+
50
+ console.log("supatypes\n");
51
+ console.log(` Server: ${server}`);
52
+ console.log(` Port: ${sshPort}`);
53
+ console.log(` Container: ${dbContainer}`);
54
+ console.log(` Output: ${output}`);
55
+ console.log(` Auth: ${sshKey ? "SSH key" : "password"}\n`);
56
+
57
+ try {
58
+ // Step 1: Create remote directory
59
+ process.stdout.write("Creating remote workspace...");
60
+ ssh(sshBase, `mkdir -p ${REMOTE_DIR}`);
61
+ console.log(" done");
62
+
63
+ // Step 2: Upload generator script
64
+ process.stdout.write("Uploading generator...");
65
+ scp(scpBase, REMOTE_SCRIPT, `${server}:${REMOTE_SCRIPT_PATH}`);
66
+ console.log(" done");
67
+
68
+ // Step 3: Run generator on server
69
+ process.stdout.write("Generating types...");
70
+ const remoteOutput = ssh(
71
+ sshBase,
72
+ `cd ${REMOTE_DIR} && node generator.js ${dbContainer} ${REMOTE_OUTPUT_PATH}`
73
+ );
74
+ console.log(" done");
75
+
76
+ // Parse stats from output
77
+ const statsMatch = remoteOutput.match(/__STATS__(.+)/);
78
+ if (statsMatch) {
79
+ const stats = JSON.parse(statsMatch[1]);
80
+ console.log(`\n Tables: ${stats.tables}`);
81
+ console.log(` Views: ${stats.views}`);
82
+ console.log(` Functions: ${stats.functions}`);
83
+ console.log(` Enums: ${stats.enums}`);
84
+ console.log(` Composites:${stats.compositeTypes ? " " + stats.compositeTypes : " 0"}`);
85
+ }
86
+
87
+ // Step 4: Download result
88
+ process.stdout.write("\nDownloading types...");
89
+ const outputDir = path.dirname(output);
90
+ if (!fs.existsSync(outputDir)) {
91
+ fs.mkdirSync(outputDir, { recursive: true });
92
+ }
93
+ scp(scpBase, `${server}:${REMOTE_OUTPUT_PATH}`, output);
94
+ console.log(" done");
95
+
96
+ // Step 5: Clean up
97
+ process.stdout.write("Cleaning up remote files...");
98
+ ssh(sshBase, `rm -rf ${REMOTE_DIR}`);
99
+ console.log(" done");
100
+
101
+ const fileSize = fs.statSync(output).size;
102
+ console.log(`\nTypes saved to ${output} (${formatBytes(fileSize)})`);
103
+ } catch (err) {
104
+ console.error(`\n\nFailed: ${err.message}`);
105
+
106
+ // Try to clean up even on failure
107
+ try {
108
+ ssh(sshBase, `rm -rf ${REMOTE_DIR}`);
109
+ } catch {
110
+ // ignore cleanup errors
111
+ }
112
+
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ function expandHome(p) {
118
+ if (p.startsWith("~/")) {
119
+ return path.join(process.env.HOME || process.env.USERPROFILE || "", p.slice(2));
120
+ }
121
+ return p;
122
+ }
123
+
124
+ function buildSshBase(server, port, sshKey, sshPassword) {
125
+ const opts = ["-p", String(port), "-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=10"];
126
+
127
+ if (sshPassword) {
128
+ // Use sshpass for password-based auth
129
+ return ["sshpass", "-p", sshPassword, "ssh", ...opts, server];
130
+ }
131
+
132
+ if (sshKey) {
133
+ return ["ssh", "-i", sshKey, ...opts, server];
134
+ }
135
+
136
+ // Default: let SSH use its default key
137
+ return ["ssh", ...opts, server];
138
+ }
139
+
140
+ function buildScpBase(server, port, sshKey, sshPassword) {
141
+ const opts = ["-P", String(port), "-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=10"];
142
+
143
+ if (sshPassword) {
144
+ return ["sshpass", "-p", sshPassword, "scp", ...opts];
145
+ }
146
+
147
+ if (sshKey) {
148
+ return ["scp", "-i", sshKey, ...opts];
149
+ }
150
+
151
+ return ["scp", ...opts];
152
+ }
153
+
154
+ function ssh(base, command) {
155
+ const [bin, ...args] = base;
156
+ return execFileSync(bin, [...args, command], {
157
+ encoding: "utf8",
158
+ maxBuffer: 50 * 1024 * 1024,
159
+ });
160
+ }
161
+
162
+ function scp(base, src, dest) {
163
+ const [bin, ...args] = base;
164
+ execFileSync(bin, [...args, src, dest], { encoding: "utf8" });
165
+ }
166
+
167
+ function formatBytes(bytes) {
168
+ if (bytes < 1024) return `${bytes} B`;
169
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
170
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
171
+ }
package/lib/init.js CHANGED
@@ -33,6 +33,7 @@ export async function init() {
33
33
 
34
34
  const sshUser = await ask(rl, "SSH username", "root");
35
35
  const sshHost = await ask(rl, "SSH server host (e.g. 1.2.3.4)", "");
36
+ const sshPort = await ask(rl, "SSH port", "22");
36
37
  const server = `${sshUser}@${sshHost}`;
37
38
  const authMethod = await ask(rl, "Auth method: key or password?", "key");
38
39
 
@@ -52,6 +53,8 @@ export async function init() {
52
53
 
53
54
  const config = { server, dbContainer, output };
54
55
 
56
+ const parsedPort = parseInt(sshPort, 10);
57
+ if (parsedPort !== 22) config.sshPort = parsedPort;
55
58
  if (sshKey) config.sshKey = sshKey;
56
59
  if (sshPassword) config.sshPassword = sshPassword;
57
60
 
@@ -23,10 +23,51 @@ const schema = execSync(
23
23
  { encoding: "utf8", maxBuffer: 50 * 1024 * 1024 }
24
24
  );
25
25
 
26
- // Step 2: Parse tables
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
- Enums: {
222
- [_ in never]: never
223
- }
224
- CompositeTypes: {
225
- [_ in never]: never
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
- type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
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,27 +1,30 @@
1
- {
2
- "name": "supatypes",
3
- "version": "1.0.2",
4
- "description": "Generate TypeScript types from a remote Supabase PostgreSQL database via SSH",
5
- "license": "MIT",
6
- "author": "MadStoneDev",
7
- "type": "module",
8
- "bin": {
9
- "supatypes": "bin/cli.js"
10
- },
11
- "files": [
12
- "bin/",
13
- "lib/",
14
- "README.md"
15
- ],
16
- "keywords": [
17
- "supabase",
18
- "typescript",
19
- "types",
20
- "codegen",
21
- "postgresql",
22
- "database"
23
- ],
24
- "engines": {
25
- "node": ">=18"
26
- }
27
- }
1
+ {
2
+ "name": "supatypes",
3
+ "version": "1.0.4",
4
+ "description": "Generate TypeScript types from a remote Supabase PostgreSQL database via SSH",
5
+ "license": "MIT",
6
+ "author": "MadStoneDev",
7
+ "type": "module",
8
+ "bin": {
9
+ "supatypes": "bin/cli.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "lib/",
14
+ "README.md"
15
+ ],
16
+ "keywords": [
17
+ "supabase",
18
+ "typescript",
19
+ "types",
20
+ "codegen",
21
+ "postgresql",
22
+ "database"
23
+ ],
24
+ "scripts": {
25
+ "test": "node --test test/"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ }
30
+ }