spindb 0.1.0 → 0.2.0

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.
@@ -88,6 +88,14 @@ export abstract class BaseEngine {
88
88
  database: string,
89
89
  ): Promise<void>
90
90
 
91
+ /**
92
+ * Drop a database within the container
93
+ */
94
+ abstract dropDatabase(
95
+ container: ContainerConfig,
96
+ database: string,
97
+ ): Promise<void>
98
+
91
99
  /**
92
100
  * Check if binaries are installed
93
101
  */
@@ -100,4 +108,18 @@ export abstract class BaseEngine {
100
108
  version: string,
101
109
  onProgress?: ProgressCallback,
102
110
  ): Promise<string>
111
+
112
+ /**
113
+ * Fetch all available versions from remote source (grouped by major version)
114
+ * Returns a map of major version -> array of full versions (sorted latest first)
115
+ * Falls back to hardcoded versions if network fails
116
+ */
117
+ async fetchAvailableVersions(): Promise<Record<string, string[]>> {
118
+ // Default implementation returns supported versions as single-item arrays
119
+ const versions: Record<string, string[]> = {}
120
+ for (const v of this.supportedVersions) {
121
+ versions[v] = [v]
122
+ }
123
+ return versions
124
+ }
103
125
  }
@@ -1,15 +1,126 @@
1
+ import { platform, arch } from 'os'
1
2
  import { defaults } from '@/config/defaults'
2
3
 
3
4
  /**
4
- * Map major versions to latest stable patch versions
5
+ * Fallback map of major versions to stable patch versions
6
+ * Used when Maven repository is unreachable
5
7
  */
6
- export const VERSION_MAP: Record<string, string> = {
7
- '14': '14.15.0',
8
- '15': '15.10.0',
9
- '16': '16.6.0',
10
- '17': '17.2.0',
8
+ export const FALLBACK_VERSION_MAP: Record<string, string> = {
9
+ '14': '14.20.0',
10
+ '15': '15.15.0',
11
+ '16': '16.11.0',
12
+ '17': '17.7.0',
11
13
  }
12
14
 
15
+ /**
16
+ * Supported major versions (in order of display)
17
+ */
18
+ export const SUPPORTED_MAJOR_VERSIONS = ['14', '15', '16', '17']
19
+
20
+ // Cache for fetched versions
21
+ let cachedVersions: Record<string, string[]> | null = null
22
+ let cacheTimestamp = 0
23
+ const CACHE_TTL_MS = 5 * 60 * 1000 // 5 minutes
24
+
25
+ /**
26
+ * Fetch available versions from Maven repository
27
+ */
28
+ export async function fetchAvailableVersions(): Promise<
29
+ Record<string, string[]>
30
+ > {
31
+ // Return cached versions if still valid
32
+ if (cachedVersions && Date.now() - cacheTimestamp < CACHE_TTL_MS) {
33
+ return cachedVersions
34
+ }
35
+
36
+ const zonkyPlatform = getZonkyPlatform(platform(), arch())
37
+ if (!zonkyPlatform) {
38
+ throw new Error(`Unsupported platform: ${platform()}-${arch()}`)
39
+ }
40
+
41
+ const url = `https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-${zonkyPlatform}/`
42
+
43
+ try {
44
+ const response = await fetch(url, { signal: AbortSignal.timeout(5000) })
45
+ if (!response.ok) {
46
+ throw new Error(`HTTP ${response.status}`)
47
+ }
48
+
49
+ const html = await response.text()
50
+
51
+ // Parse version directories from the HTML listing
52
+ // Format: <a href="14.15.0/">14.15.0/</a>
53
+ const versionRegex = /href="(\d+\.\d+\.\d+)\/"/g
54
+ const versions: string[] = []
55
+ let match
56
+
57
+ while ((match = versionRegex.exec(html)) !== null) {
58
+ versions.push(match[1])
59
+ }
60
+
61
+ // Group versions by major version
62
+ const grouped: Record<string, string[]> = {}
63
+ for (const major of SUPPORTED_MAJOR_VERSIONS) {
64
+ grouped[major] = versions
65
+ .filter((v) => v.startsWith(`${major}.`))
66
+ .sort((a, b) => compareVersions(b, a)) // Sort descending (latest first)
67
+ }
68
+
69
+ // Cache the results
70
+ cachedVersions = grouped
71
+ cacheTimestamp = Date.now()
72
+
73
+ return grouped
74
+ } catch {
75
+ // Return fallback on any error
76
+ return getFallbackVersions()
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Get fallback versions when network is unavailable
82
+ */
83
+ function getFallbackVersions(): Record<string, string[]> {
84
+ const grouped: Record<string, string[]> = {}
85
+ for (const major of SUPPORTED_MAJOR_VERSIONS) {
86
+ grouped[major] = [FALLBACK_VERSION_MAP[major]]
87
+ }
88
+ return grouped
89
+ }
90
+
91
+ /**
92
+ * Compare two version strings (e.g., "16.11.0" vs "16.9.0")
93
+ * Returns positive if a > b, negative if a < b, 0 if equal
94
+ */
95
+ function compareVersions(a: string, b: string): number {
96
+ const partsA = a.split('.').map(Number)
97
+ const partsB = b.split('.').map(Number)
98
+
99
+ for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
100
+ const numA = partsA[i] || 0
101
+ const numB = partsB[i] || 0
102
+ if (numA !== numB) {
103
+ return numA - numB
104
+ }
105
+ }
106
+ return 0
107
+ }
108
+
109
+ /**
110
+ * Get the latest version for a major version
111
+ */
112
+ export async function getLatestVersion(major: string): Promise<string> {
113
+ const versions = await fetchAvailableVersions()
114
+ const majorVersions = versions[major]
115
+ if (majorVersions && majorVersions.length > 0) {
116
+ return majorVersions[0] // First is latest due to descending sort
117
+ }
118
+ return FALLBACK_VERSION_MAP[major] || `${major}.0.0`
119
+ }
120
+
121
+ // Legacy export for backward compatibility
122
+ export const VERSION_MAP = FALLBACK_VERSION_MAP
123
+
13
124
  /**
14
125
  * Get the zonky.io platform identifier
15
126
  */
@@ -34,16 +145,23 @@ export function getBinaryUrl(
34
145
  throw new Error(`Unsupported platform: ${platform}-${arch}`)
35
146
  }
36
147
 
37
- const fullVersion = VERSION_MAP[version]
38
- if (!fullVersion) {
39
- throw new Error(
40
- `Unsupported PostgreSQL version: ${version}. Supported: ${Object.keys(VERSION_MAP).join(', ')}`,
41
- )
42
- }
148
+ // Use VERSION_MAP for major versions, otherwise treat as full version
149
+ const fullVersion = VERSION_MAP[version] || normalizeVersion(version)
43
150
 
44
151
  return `https://repo1.maven.org/maven2/io/zonky/test/postgres/embedded-postgres-binaries-${zonkyPlatform}/${fullVersion}/embedded-postgres-binaries-${zonkyPlatform}-${fullVersion}.jar`
45
152
  }
46
153
 
154
+ /**
155
+ * Normalize version string to X.Y.Z format
156
+ */
157
+ function normalizeVersion(version: string): string {
158
+ const parts = version.split('.')
159
+ if (parts.length === 2) {
160
+ return `${version}.0`
161
+ }
162
+ return version
163
+ }
164
+
47
165
  /**
48
166
  * Get the full version string for a major version
49
167
  */
@@ -8,7 +8,11 @@ import { processManager } from '@/core/process-manager'
8
8
  import { configManager } from '@/core/config-manager'
9
9
  import { paths } from '@/config/paths'
10
10
  import { defaults } from '@/config/defaults'
11
- import { getBinaryUrl, VERSION_MAP } from './binary-urls'
11
+ import {
12
+ getBinaryUrl,
13
+ SUPPORTED_MAJOR_VERSIONS,
14
+ fetchAvailableVersions,
15
+ } from './binary-urls'
12
16
  import { detectBackupFormat, restoreBackup } from './restore'
13
17
  import type {
14
18
  ContainerConfig,
@@ -24,7 +28,15 @@ export class PostgreSQLEngine extends BaseEngine {
24
28
  name = 'postgresql'
25
29
  displayName = 'PostgreSQL'
26
30
  defaultPort = 5432
27
- supportedVersions = Object.keys(VERSION_MAP)
31
+ supportedVersions = SUPPORTED_MAJOR_VERSIONS
32
+
33
+ /**
34
+ * Fetch all available versions from Maven (grouped by major version)
35
+ * Falls back to hardcoded versions if network fails
36
+ */
37
+ async fetchAvailableVersions(): Promise<Record<string, string[]>> {
38
+ return fetchAvailableVersions()
39
+ }
28
40
 
29
41
  /**
30
42
  * Get current platform info
@@ -178,6 +190,7 @@ export class PostgreSQLEngine extends BaseEngine {
178
190
  port,
179
191
  database,
180
192
  user: defaults.superuser,
193
+ pgRestorePath: options.pgRestorePath as string, // Use custom path if provided
181
194
  ...(options as { format?: string }),
182
195
  })
183
196
  }
@@ -187,8 +200,8 @@ export class PostgreSQLEngine extends BaseEngine {
187
200
  */
188
201
  getConnectionString(container: ContainerConfig, database?: string): string {
189
202
  const { port } = container
190
- const db = database || 'postgres'
191
- return `postgresql://${defaults.superuser}@localhost:${port}/${db}`
203
+ const db = database || container.database || 'postgres'
204
+ return `postgresql://${defaults.superuser}@127.0.0.1:${port}/${db}`
192
205
  }
193
206
 
194
207
  /**
@@ -293,6 +306,29 @@ export class PostgreSQLEngine extends BaseEngine {
293
306
  }
294
307
  }
295
308
  }
309
+
310
+ /**
311
+ * Drop a database
312
+ */
313
+ async dropDatabase(
314
+ container: ContainerConfig,
315
+ database: string,
316
+ ): Promise<void> {
317
+ const { port } = container
318
+ const psqlPath = await this.getPsqlPath()
319
+
320
+ try {
321
+ await execAsync(
322
+ `"${psqlPath}" -h 127.0.0.1 -p ${port} -U ${defaults.superuser} -d postgres -c 'DROP DATABASE IF EXISTS "${database}"'`,
323
+ )
324
+ } catch (error) {
325
+ const err = error as Error
326
+ // Ignore "database does not exist" error
327
+ if (!err.message.includes('does not exist')) {
328
+ throw error
329
+ }
330
+ }
331
+ }
296
332
  }
297
333
 
298
334
  export const postgresqlEngine = new PostgreSQLEngine()
@@ -2,6 +2,7 @@ import { readFile } from 'fs/promises'
2
2
  import { exec } from 'child_process'
3
3
  import { promisify } from 'util'
4
4
  import { configManager } from '@/core/config-manager'
5
+ import { findBinaryPathFresh } from '@/core/postgres-binary-manager'
5
6
  import type { BackupFormat, RestoreResult } from '@/types'
6
7
 
7
8
  const execAsync = promisify(exec)
@@ -77,11 +78,12 @@ export async function detectBackupFormat(
77
78
  }
78
79
  }
79
80
 
80
- export interface RestoreOptions {
81
+ export type RestoreOptions = {
81
82
  port: number
82
83
  database: string
83
84
  user?: string
84
85
  format?: string
86
+ pgRestorePath?: string
85
87
  }
86
88
 
87
89
  /**
@@ -101,19 +103,27 @@ async function getPsqlPath(): Promise<string> {
101
103
  }
102
104
 
103
105
  /**
104
- * Get pg_restore path from config, with helpful error message
106
+ * Get pg_restore path from config or system PATH, with helpful error message
105
107
  */
106
108
  async function getPgRestorePath(): Promise<string> {
107
- const pgRestorePath = await configManager.getBinaryPath('pg_restore')
108
- if (!pgRestorePath) {
109
+ // First try to get from config (in case user has set a custom path)
110
+ const configPath = await configManager.getBinaryPath('pg_restore')
111
+ if (configPath) {
112
+ return configPath
113
+ }
114
+
115
+ // Fall back to finding it on the system PATH with cache refresh
116
+ const systemPath = await findBinaryPathFresh('pg_restore')
117
+ if (!systemPath) {
109
118
  throw new Error(
110
119
  'pg_restore not found. Install PostgreSQL client tools:\n' +
111
120
  ' macOS: brew install libpq && brew link --force libpq\n' +
112
- ' Ubuntu/Debian: apt install postgresql-client\n\n' +
121
+ ' Ubuntu/Debian: apt install postgresql-client\n' +
122
+ ' CentOS/RHEL/Fedora: yum install postgresql\n\n' +
113
123
  'Or configure manually: spindb config set pg_restore /path/to/pg_restore',
114
124
  )
115
125
  }
116
- return pgRestorePath
126
+ return systemPath
117
127
  }
118
128
 
119
129
  /**
@@ -124,7 +134,7 @@ export async function restoreBackup(
124
134
  backupPath: string,
125
135
  options: RestoreOptions,
126
136
  ): Promise<RestoreResult> {
127
- const { port, database, user = 'postgres', format } = options
137
+ const { port, database, user = 'postgres', format, pgRestorePath } = options
128
138
 
129
139
  const detectedFormat = format || (await detectBackupFormat(backupPath)).format
130
140
 
@@ -141,7 +151,8 @@ export async function restoreBackup(
141
151
  ...result,
142
152
  }
143
153
  } else {
144
- const pgRestorePath = await getPgRestorePath()
154
+ // Use custom path if provided, otherwise find it dynamically
155
+ const restorePath = pgRestorePath || (await getPgRestorePath())
145
156
 
146
157
  try {
147
158
  const formatFlag =
@@ -151,7 +162,7 @@ export async function restoreBackup(
151
162
  ? '-Ft'
152
163
  : ''
153
164
  const result = await execAsync(
154
- `"${pgRestorePath}" -h 127.0.0.1 -p ${port} -U ${user} -d ${database} --no-owner --no-privileges ${formatFlag} "${backupPath}"`,
165
+ `"${restorePath}" -h 127.0.0.1 -p ${port} -U ${user} -d ${database} --no-owner --no-privileges ${formatFlag} "${backupPath}"`,
155
166
  { maxBuffer: 50 * 1024 * 1024 },
156
167
  )
157
168
 
@@ -1,54 +1,56 @@
1
- export interface ContainerConfig {
1
+ export type ContainerConfig = {
2
2
  name: string
3
3
  engine: string
4
4
  version: string
5
5
  port: number
6
+ database: string
6
7
  created: string
7
8
  status: 'created' | 'running' | 'stopped'
8
9
  clonedFrom?: string
9
10
  }
10
11
 
11
- export interface ProgressCallback {
12
- (progress: { stage: string; message: string }): void
13
- }
12
+ export type ProgressCallback = (progress: {
13
+ stage: string
14
+ message: string
15
+ }) => void
14
16
 
15
- export interface InstalledBinary {
17
+ export type InstalledBinary = {
16
18
  engine: string
17
19
  version: string
18
20
  platform: string
19
21
  arch: string
20
22
  }
21
23
 
22
- export interface PortResult {
24
+ export type PortResult = {
23
25
  port: number
24
26
  isDefault: boolean
25
27
  }
26
28
 
27
- export interface ProcessResult {
29
+ export type ProcessResult = {
28
30
  stdout: string
29
31
  stderr: string
30
32
  code?: number
31
33
  }
32
34
 
33
- export interface StatusResult {
35
+ export type StatusResult = {
34
36
  running: boolean
35
37
  message: string
36
38
  }
37
39
 
38
- export interface BackupFormat {
40
+ export type BackupFormat = {
39
41
  format: string
40
42
  description: string
41
43
  restoreCommand: string
42
44
  }
43
45
 
44
- export interface RestoreResult {
46
+ export type RestoreResult = {
45
47
  format: string
46
48
  stdout?: string
47
49
  stderr?: string
48
50
  code?: number
49
51
  }
50
52
 
51
- export interface EngineInfo {
53
+ export type EngineInfo = {
52
54
  name: string
53
55
  displayName: string
54
56
  defaultPort: number
@@ -68,7 +70,7 @@ export type BinarySource = 'bundled' | 'system' | 'custom'
68
70
  /**
69
71
  * Configuration for a single binary tool
70
72
  */
71
- export interface BinaryConfig {
73
+ export type BinaryConfig = {
72
74
  tool: BinaryTool
73
75
  path: string
74
76
  source: BinarySource
@@ -78,7 +80,7 @@ export interface BinaryConfig {
78
80
  /**
79
81
  * Global spindb configuration stored in ~/.spindb/config.json
80
82
  */
81
- export interface SpinDBConfig {
83
+ export type SpinDBConfig = {
82
84
  // Binary paths for client tools
83
85
  binaries: {
84
86
  psql?: BinaryConfig
package/tsconfig.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
6
  "lib": ["ES2022"],
7
7
  "outDir": "./dist",
8
8
  "rootDir": "./src",
9
9
  "strict": true,
10
10
  "esModuleInterop": true,
11
+ "allowSyntheticDefaultImports": true,
11
12
  "skipLibCheck": true,
12
13
  "forceConsistentCasingInFileNames": true,
13
14
  "resolveJsonModule": true,
@@ -17,7 +18,9 @@
17
18
  "baseUrl": ".",
18
19
  "paths": {
19
20
  "@/*": ["./src/*"]
20
- }
21
+ },
22
+ "allowImportingTsExtensions": true,
23
+ "noEmit": true
21
24
  },
22
25
  "include": ["src/**/*"],
23
26
  "exclude": ["node_modules", "dist"]