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 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
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "supatypes",
3
- "version": "1.0.3",
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
+ }