spindb 0.35.3 → 0.36.1

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.
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Weaviate restore module
3
+ * Supports snapshot-based restore using Weaviate's filesystem backup API.
4
+ *
5
+ * Restore flow:
6
+ * 1. Copy backup directory into target container's BACKUP_FILESYSTEM_PATH/<id>/
7
+ * 2. Start Weaviate (handled by caller)
8
+ * 3. Trigger restore via POST /v1/backups/filesystem/<id>/restore
9
+ */
10
+
11
+ import { cp, copyFile, open, mkdir, readFile } from 'fs/promises'
12
+ import { existsSync, statSync } from 'fs'
13
+ import { join, basename } from 'path'
14
+ import { paths } from '../../config/paths'
15
+ import { logDebug } from '../../core/error-handler'
16
+ import type { BackupFormat, RestoreResult } from '../../types'
17
+
18
+ /**
19
+ * Detect backup format from file or directory.
20
+ * Weaviate backups are directories containing backup metadata and class data.
21
+ */
22
+ export async function detectBackupFormat(
23
+ filePath: string,
24
+ ): Promise<BackupFormat> {
25
+ if (!existsSync(filePath)) {
26
+ throw new Error(`Backup file not found: ${filePath}`)
27
+ }
28
+
29
+ const stats = statSync(filePath)
30
+
31
+ // Weaviate filesystem backups are directories
32
+ if (stats.isDirectory()) {
33
+ // Check if it contains a backup_config.json (Weaviate backup marker)
34
+ const configPath = join(filePath, 'backup_config.json')
35
+ if (existsSync(configPath)) {
36
+ return {
37
+ format: 'snapshot',
38
+ description: 'Weaviate filesystem backup directory',
39
+ restoreCommand:
40
+ 'Copy to backups directory and use Weaviate restore API (spindb restore handles this)',
41
+ }
42
+ }
43
+
44
+ return {
45
+ format: 'snapshot',
46
+ description: 'Weaviate backup directory',
47
+ restoreCommand:
48
+ 'Copy to backups directory and use Weaviate restore API (spindb restore handles this)',
49
+ }
50
+ }
51
+
52
+ // Check file extension for .snapshot files
53
+ if (filePath.endsWith('.snapshot')) {
54
+ return {
55
+ format: 'snapshot',
56
+ description: 'Weaviate snapshot file',
57
+ restoreCommand:
58
+ 'Copy to backups directory and use Weaviate API (spindb restore handles this)',
59
+ }
60
+ }
61
+
62
+ // Check file contents for gzip magic bytes (snapshot files are compressed)
63
+ try {
64
+ const buffer = Buffer.alloc(4)
65
+ const fd = await open(filePath, 'r')
66
+ try {
67
+ await fd.read(buffer, 0, 4, 0)
68
+ // Gzip magic bytes: 1f 8b
69
+ if (buffer[0] === 0x1f && buffer[1] === 0x8b) {
70
+ return {
71
+ format: 'snapshot',
72
+ description: 'Weaviate snapshot file (detected by magic bytes)',
73
+ restoreCommand:
74
+ 'Copy to backups directory and use Weaviate API (spindb restore handles this)',
75
+ }
76
+ }
77
+ } finally {
78
+ await fd.close().catch(() => {})
79
+ }
80
+ } catch (error) {
81
+ logDebug(`Error reading backup file header: ${error}`)
82
+ }
83
+
84
+ // Check for JSON backup metadata
85
+ if (filePath.endsWith('.json')) {
86
+ return {
87
+ format: 'snapshot',
88
+ description: 'Weaviate backup metadata file',
89
+ restoreCommand: 'Use Weaviate restore API (spindb restore handles this)',
90
+ }
91
+ }
92
+
93
+ return {
94
+ format: 'unknown',
95
+ description: 'Unknown backup format',
96
+ restoreCommand: 'Use a Weaviate backup directory for restore',
97
+ }
98
+ }
99
+
100
+ // Restore options for Weaviate
101
+ export type RestoreOptions = {
102
+ containerName: string
103
+ dataDir?: string
104
+ }
105
+
106
+ /**
107
+ * Restore from snapshot backup.
108
+ *
109
+ * Copies the backup directory into the target container's backups path.
110
+ * The caller must then start Weaviate and trigger the restore via API.
111
+ */
112
+ export async function restoreBackup(
113
+ backupPath: string,
114
+ options: RestoreOptions,
115
+ ): Promise<RestoreResult> {
116
+ const { containerName, dataDir } = options
117
+
118
+ if (!existsSync(backupPath)) {
119
+ throw new Error(`Backup not found: ${backupPath}`)
120
+ }
121
+
122
+ // Detect backup format
123
+ const format = await detectBackupFormat(backupPath)
124
+ logDebug(`Detected backup format: ${format.format}`)
125
+
126
+ if (format.format !== 'snapshot') {
127
+ throw new Error(
128
+ `Invalid backup format: ${format.format}. Use a Weaviate backup directory for restore.`,
129
+ )
130
+ }
131
+
132
+ const targetDir =
133
+ dataDir || paths.getContainerDataPath(containerName, { engine: 'weaviate' })
134
+ const backupsDir = join(targetDir, 'backups')
135
+
136
+ // Read the real backup ID from backup_config.json inside the backup directory.
137
+ // Weaviate validates that the directory name matches the internal backup ID.
138
+ let backupId = basename(backupPath)
139
+ const stats = statSync(backupPath)
140
+ if (stats.isDirectory()) {
141
+ const configPath = join(backupPath, 'backup_config.json')
142
+ if (existsSync(configPath)) {
143
+ try {
144
+ const configData = JSON.parse(await readFile(configPath, 'utf-8')) as {
145
+ id?: string
146
+ }
147
+ if (configData.id) {
148
+ backupId = configData.id
149
+ logDebug(`Read backup ID from config: ${backupId}`)
150
+ }
151
+ } catch (error) {
152
+ logDebug(`Failed to read backup_config.json: ${error}`)
153
+ }
154
+ }
155
+ }
156
+
157
+ const targetPath = join(backupsDir, backupId)
158
+
159
+ logDebug(`Restoring backup to: ${targetPath}`)
160
+
161
+ // Ensure backups directory exists
162
+ if (!existsSync(backupsDir)) {
163
+ await mkdir(backupsDir, { recursive: true })
164
+ }
165
+
166
+ if (stats.isDirectory()) {
167
+ // Copy entire backup directory
168
+ await cp(backupPath, targetPath, { recursive: true })
169
+ } else {
170
+ // Single file - create directory and copy file into it
171
+ if (!existsSync(targetPath)) {
172
+ await mkdir(targetPath, { recursive: true })
173
+ }
174
+ await copyFile(backupPath, join(targetPath, basename(backupPath)))
175
+ }
176
+
177
+ return {
178
+ format: 'snapshot',
179
+ stdout:
180
+ `Restored backup to ${targetPath}.\n` +
181
+ `After starting Weaviate, restore via: POST /v1/backups/filesystem/${backupId}/restore`,
182
+ code: 0,
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Parse Weaviate connection string
188
+ * Format: http://host[:port], https://host[:port]
189
+ *
190
+ * Weaviate uses classes/collections instead of traditional databases
191
+ */
192
+ export function parseConnectionString(connectionString: string): {
193
+ host: string
194
+ port: number
195
+ protocol: 'http' | 'https'
196
+ } {
197
+ if (!connectionString || typeof connectionString !== 'string') {
198
+ throw new Error(
199
+ 'Invalid Weaviate connection string: expected a non-empty string',
200
+ )
201
+ }
202
+
203
+ let url: URL
204
+ try {
205
+ url = new URL(connectionString)
206
+ } catch (error) {
207
+ throw new Error(
208
+ `Invalid Weaviate connection string: "${connectionString}". ` +
209
+ `Expected format: http://host[:port]`,
210
+ { cause: error },
211
+ )
212
+ }
213
+
214
+ // Validate protocol
215
+ let protocol: 'http' | 'https'
216
+ if (url.protocol === 'http:') {
217
+ protocol = 'http'
218
+ } else if (url.protocol === 'https:') {
219
+ protocol = 'https'
220
+ } else {
221
+ throw new Error(
222
+ `Invalid Weaviate connection string: unsupported protocol "${url.protocol}". ` +
223
+ `Expected "http://" or "https://"`,
224
+ )
225
+ }
226
+
227
+ const host = url.hostname || '127.0.0.1'
228
+ const port = parseInt(url.port, 10) || 8080
229
+
230
+ return {
231
+ host,
232
+ port,
233
+ protocol,
234
+ }
235
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Weaviate Version Maps
3
+ *
4
+ * TEMPORARY: This version map will be replaced by the hostdb npm package once published.
5
+ * Until then, manually keep this in sync with robertjbass/hostdb releases.json:
6
+ * https://github.com/robertjbass/hostdb/blob/main/releases.json
7
+ *
8
+ * When updating versions:
9
+ * 1. Check hostdb releases.json for available versions
10
+ * 2. Update WEAVIATE_VERSION_MAP to match
11
+ */
12
+
13
+ import { logDebug } from '../../core/error-handler'
14
+
15
+ /**
16
+ * Map of major Weaviate versions to their latest stable patch versions.
17
+ * Must match versions available in hostdb releases.json.
18
+ */
19
+ export const WEAVIATE_VERSION_MAP: Record<string, string> = {
20
+ // 1-part: major version -> latest
21
+ '1': '1.35.7',
22
+ // 2-part: major.minor -> latest patch
23
+ '1.35': '1.35.7',
24
+ // 3-part: exact version (identity mapping)
25
+ '1.35.7': '1.35.7',
26
+ }
27
+
28
+ /**
29
+ * Supported major Weaviate versions (1-part format).
30
+ * Used for grouping and display purposes.
31
+ */
32
+ export const SUPPORTED_MAJOR_VERSIONS = ['1']
33
+
34
+ /**
35
+ * Get the full version string for a major version.
36
+ *
37
+ * @param majorVersion - Major version (e.g., '1')
38
+ * @returns Full version string (e.g., '1.35.7') or null if not supported
39
+ */
40
+ export function getFullVersion(majorVersion: string): string | null {
41
+ return WEAVIATE_VERSION_MAP[majorVersion] || null
42
+ }
43
+
44
+ /**
45
+ * Normalize a version string to X.Y.Z format.
46
+ *
47
+ * @param version - Version string (e.g., '1', '1.35', '1.35.7')
48
+ * @returns Normalized version (e.g., '1.35.7')
49
+ */
50
+ export function normalizeVersion(version: string): string {
51
+ // If it's in the version map (major, major.minor, or full version), return the mapped value
52
+ // Note: Full versions have identity mappings (e.g., '1.35.7' => '1.35.7')
53
+ const fullVersion = WEAVIATE_VERSION_MAP[version]
54
+ if (fullVersion) {
55
+ return fullVersion
56
+ }
57
+
58
+ // Unknown version - warn and return as-is
59
+ // This may cause download failures if the version doesn't exist in hostdb
60
+ const parts = version.split('.')
61
+
62
+ // Validate format: must be 1-3 numeric segments (e.g., "1", "1.35", "1.35.7")
63
+ const isValidFormat =
64
+ parts.length >= 1 &&
65
+ parts.length <= 3 &&
66
+ parts.every((p) => /^\d+$/.test(p))
67
+
68
+ if (!isValidFormat) {
69
+ logDebug(
70
+ `Weaviate version '${version}' has invalid format, may not be available in hostdb`,
71
+ )
72
+ } else {
73
+ logDebug(
74
+ `Weaviate version '${version}' not in version map, may not be available in hostdb`,
75
+ )
76
+ }
77
+ return version
78
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Weaviate version validation utilities
3
+ * Handles version parsing, comparison, and compatibility checking
4
+ */
5
+
6
+ /**
7
+ * Parse a Weaviate version string into components
8
+ * Handles formats like "1.35.7", "1.35", "v1.35.7"
9
+ */
10
+ export function parseVersion(versionString: string): {
11
+ major: number
12
+ minor: number
13
+ patch: number
14
+ raw: string
15
+ } | null {
16
+ const cleaned = versionString.replace(/^v/, '').trim()
17
+ const parts = cleaned.split('.')
18
+
19
+ if (parts.length < 1) return null
20
+
21
+ const major = parseInt(parts[0], 10)
22
+ const minor = parts[1] ? parseInt(parts[1], 10) : 0
23
+ const patch = parts[2] ? parseInt(parts[2], 10) : 0
24
+
25
+ if (isNaN(major)) return null
26
+ if (parts[1] && isNaN(minor)) return null
27
+ if (parts[2] && isNaN(patch)) return null
28
+
29
+ return { major, minor, patch, raw: cleaned }
30
+ }
31
+
32
+ /**
33
+ * Check if a Weaviate version is supported by SpinDB
34
+ * Minimum supported version: 1.0.0
35
+ */
36
+ export function isVersionSupported(version: string): boolean {
37
+ const parsed = parseVersion(version)
38
+ if (!parsed) return false
39
+
40
+ return parsed.major >= 1
41
+ }
42
+
43
+ /**
44
+ * Get major version from full version string
45
+ * e.g., "1.35.7" -> "1"
46
+ */
47
+ export function getMajorVersion(version: string): string {
48
+ const parsed = parseVersion(version)
49
+ return parsed ? String(parsed.major) : version
50
+ }
51
+
52
+ /**
53
+ * Get major.minor version from full version string
54
+ * e.g., "1.35.7" -> "1.35"
55
+ */
56
+ export function getMajorMinorVersion(version: string): string {
57
+ const parsed = parseVersion(version)
58
+ if (!parsed) return version
59
+ return `${parsed.major}.${parsed.minor}`
60
+ }
61
+
62
+ /**
63
+ * Compare two Weaviate versions
64
+ * Returns: -1 if a < b, 0 if a == b, 1 if a > b, null if either version cannot be parsed
65
+ */
66
+ export function compareVersions(a: string, b: string): number | null {
67
+ const parsedA = parseVersion(a)
68
+ const parsedB = parseVersion(b)
69
+
70
+ if (!parsedA || !parsedB) {
71
+ return null
72
+ }
73
+
74
+ if (parsedA.major !== parsedB.major) {
75
+ return parsedA.major < parsedB.major ? -1 : 1
76
+ }
77
+ if (parsedA.minor !== parsedB.minor) {
78
+ return parsedA.minor < parsedB.minor ? -1 : 1
79
+ }
80
+ if (parsedA.patch !== parsedB.patch) {
81
+ return parsedA.patch < parsedB.patch ? -1 : 1
82
+ }
83
+ return 0
84
+ }
85
+
86
+ /**
87
+ * Check if a backup version is compatible with the restore version
88
+ * Weaviate snapshots are generally forward-compatible within major versions
89
+ */
90
+ export function isVersionCompatible(
91
+ backupVersion: string,
92
+ restoreVersion: string,
93
+ ): { compatible: boolean; warning?: string } {
94
+ const backup = parseVersion(backupVersion)
95
+ const restore = parseVersion(restoreVersion)
96
+
97
+ if (!backup || !restore) {
98
+ return {
99
+ compatible: true,
100
+ warning: 'Could not parse versions, proceeding with restore',
101
+ }
102
+ }
103
+
104
+ // Cannot restore newer snapshot to older server
105
+ if (backup.major > restore.major) {
106
+ return {
107
+ compatible: false,
108
+ warning: `Cannot restore Weaviate ${backupVersion} snapshot to ${restoreVersion} server. The backup is from a newer major version.`,
109
+ }
110
+ }
111
+
112
+ // Allow same major version
113
+ if (backup.major === restore.major) {
114
+ return { compatible: true }
115
+ }
116
+
117
+ // Allow upgrading from older major version (restore.major > backup.major)
118
+ return {
119
+ compatible: true,
120
+ warning: `Restoring Weaviate ${backupVersion} snapshot to ${restoreVersion} server. Weaviate will upgrade the snapshot format on next save.`,
121
+ }
122
+ }
123
+
124
+ // Validate that a version string matches supported format
125
+ export function isValidVersionFormat(version: string): boolean {
126
+ const parsed = parseVersion(version)
127
+ return parsed !== null
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spindb",
3
- "version": "0.35.3",
3
+ "version": "0.36.1",
4
4
  "author": "Bob Bass <bob@bbass.co>",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "description": "Zero-config Docker-free local database containers. Create, backup, and clone a variety of popular databases.",
@@ -56,6 +56,7 @@
56
56
  "questdb",
57
57
  "typedb",
58
58
  "influxdb",
59
+ "weaviate",
59
60
  "layerbase",
60
61
  "docker-free",
61
62
  "tui",
package/types/index.ts CHANGED
@@ -39,6 +39,7 @@ export enum Engine {
39
39
  QuestDB = 'questdb',
40
40
  TypeDB = 'typedb',
41
41
  InfluxDB = 'influxdb',
42
+ Weaviate = 'weaviate',
42
43
  }
43
44
 
44
45
  // Icon display mode for engine icons in CLI output
@@ -80,6 +81,7 @@ export const ALL_ENGINES = [
80
81
  Engine.QuestDB,
81
82
  Engine.TypeDB,
82
83
  Engine.InfluxDB,
84
+ Engine.Weaviate,
83
85
  ] as const
84
86
 
85
87
  // File-based engines (no server process, data stored in user project directories)
@@ -211,6 +213,7 @@ export type SurrealDBFormat = 'surql'
211
213
  export type QuestDBFormat = 'sql'
212
214
  export type TypeDBFormat = 'typeql'
213
215
  export type InfluxDBFormat = 'sql'
216
+ export type WeaviateFormat = 'snapshot'
214
217
 
215
218
  // Query command types
216
219
  export type QueryResultRow = Record<string, unknown>
@@ -290,6 +293,7 @@ export type BackupFormatType =
290
293
  | QuestDBFormat
291
294
  | TypeDBFormat
292
295
  | InfluxDBFormat
296
+ | WeaviateFormat
293
297
 
294
298
  // Mapping from Engine to its corresponding backup format type
295
299
  type EngineFormatMap = {
@@ -311,6 +315,7 @@ type EngineFormatMap = {
311
315
  [Engine.QuestDB]: QuestDBFormat
312
316
  [Engine.TypeDB]: TypeDBFormat
313
317
  [Engine.InfluxDB]: InfluxDBFormat
318
+ [Engine.Weaviate]: WeaviateFormat
314
319
  }
315
320
 
316
321
  // Helper type to get format type for a specific engine
@@ -427,6 +432,8 @@ export type BinaryTool =
427
432
  | 'typedb_console_bin'
428
433
  // InfluxDB tools
429
434
  | 'influxdb3'
435
+ // Weaviate tools
436
+ | 'weaviate'
430
437
  // Web panels
431
438
  | 'pgweb'
432
439
  // TUI tools
@@ -523,6 +530,8 @@ export type SpinDBConfig = {
523
530
  typedb_console_bin?: BinaryConfig
524
531
  // InfluxDB tools
525
532
  influxdb3?: BinaryConfig
533
+ // Weaviate tools
534
+ weaviate?: BinaryConfig
526
535
  // Web panels
527
536
  pgweb?: BinaryConfig
528
537
  // TUI tools