spindb 0.24.0 → 0.26.2

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.
Files changed (35) hide show
  1. package/README.md +53 -14
  2. package/cli/commands/engines.ts +89 -1
  3. package/cli/commands/menu/backup-handlers.ts +19 -0
  4. package/cli/commands/menu/container-handlers.ts +4 -2
  5. package/cli/commands/menu/shell-handlers.ts +52 -2
  6. package/cli/commands/menu/sql-handlers.ts +7 -1
  7. package/cli/constants.ts +4 -0
  8. package/cli/helpers.ts +144 -0
  9. package/cli/index.ts +1 -1
  10. package/config/backup-formats.ts +28 -0
  11. package/config/engine-defaults.ts +26 -0
  12. package/config/engines.json +32 -0
  13. package/core/config-manager.ts +5 -0
  14. package/core/container-manager.ts +10 -4
  15. package/core/dependency-manager.ts +4 -0
  16. package/engines/base-engine.ts +16 -0
  17. package/engines/cockroachdb/backup.ts +363 -0
  18. package/engines/cockroachdb/binary-manager.ts +45 -0
  19. package/engines/cockroachdb/binary-urls.ts +37 -0
  20. package/engines/cockroachdb/cli-utils.ts +384 -0
  21. package/engines/cockroachdb/hostdb-releases.ts +111 -0
  22. package/engines/cockroachdb/index.ts +1052 -0
  23. package/engines/cockroachdb/restore.ts +448 -0
  24. package/engines/cockroachdb/version-maps.ts +42 -0
  25. package/engines/index.ts +8 -0
  26. package/engines/surrealdb/backup.ts +122 -0
  27. package/engines/surrealdb/binary-manager.ts +45 -0
  28. package/engines/surrealdb/binary-urls.ts +37 -0
  29. package/engines/surrealdb/cli-utils.ts +175 -0
  30. package/engines/surrealdb/hostdb-releases.ts +111 -0
  31. package/engines/surrealdb/index.ts +949 -0
  32. package/engines/surrealdb/restore.ts +297 -0
  33. package/engines/surrealdb/version-maps.ts +41 -0
  34. package/package.json +3 -1
  35. package/types/index.ts +18 -0
@@ -0,0 +1,297 @@
1
+ /**
2
+ * SurrealDB restore module
3
+ * Supports SurrealQL-based restores using surreal import
4
+ */
5
+
6
+ import { spawn } from 'child_process'
7
+ import { open } from 'fs/promises'
8
+ import { existsSync, statSync } from 'fs'
9
+ import { logDebug } from '../../core/error-handler'
10
+ import { requireSurrealPath } from './cli-utils'
11
+ import type { BackupFormat, RestoreResult } from '../../types'
12
+
13
+ /**
14
+ * SurrealQL keywords that indicate a SurrealDB backup
15
+ */
16
+ const SURREALQL_KEYWORDS = [
17
+ 'DEFINE',
18
+ 'CREATE',
19
+ 'INSERT',
20
+ 'UPDATE',
21
+ 'SELECT',
22
+ 'DELETE',
23
+ 'RELATE',
24
+ 'LET',
25
+ 'BEGIN',
26
+ 'COMMIT',
27
+ 'USE NS',
28
+ 'USE DB',
29
+ 'OPTION IMPORT',
30
+ ]
31
+
32
+ /**
33
+ * Check if file content looks like SurrealQL
34
+ * Only reads first 8KB to avoid loading large files into memory
35
+ */
36
+ async function looksLikeSurql(filePath: string): Promise<boolean> {
37
+ try {
38
+ const HEADER_SIZE = 8192
39
+ const buffer = Buffer.alloc(HEADER_SIZE)
40
+
41
+ const fd = await open(filePath, 'r')
42
+ let bytesRead: number
43
+ try {
44
+ const result = await fd.read(buffer, 0, HEADER_SIZE, 0)
45
+ bytesRead = result.bytesRead
46
+ } finally {
47
+ await fd.close()
48
+ }
49
+
50
+ const content = buffer.toString('utf-8', 0, bytesRead)
51
+ const lines = content.split(/\r?\n/)
52
+
53
+ let surqlStatementsFound = 0
54
+ const linesToCheck = 20
55
+ let checkedLines = 0
56
+
57
+ for (const line of lines) {
58
+ if (checkedLines >= linesToCheck) break
59
+
60
+ const trimmed = line.trim().toUpperCase()
61
+
62
+ // Skip empty lines and comments
63
+ if (!trimmed || trimmed.startsWith('--') || trimmed.startsWith('#')) continue
64
+
65
+ checkedLines++
66
+
67
+ // Check for SurrealQL keywords
68
+ for (const keyword of SURREALQL_KEYWORDS) {
69
+ if (trimmed.startsWith(keyword)) {
70
+ surqlStatementsFound++
71
+ break
72
+ }
73
+ }
74
+
75
+ if (surqlStatementsFound >= 2) {
76
+ return true
77
+ }
78
+ }
79
+
80
+ return surqlStatementsFound > 0
81
+ } catch {
82
+ return false
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Detect backup format from file
88
+ * Supports:
89
+ * - SurrealQL: Schema + data statements
90
+ */
91
+ export async function detectBackupFormat(
92
+ filePath: string,
93
+ ): Promise<BackupFormat> {
94
+ if (!existsSync(filePath)) {
95
+ throw new Error(`Backup file not found: ${filePath}`)
96
+ }
97
+
98
+ const stats = statSync(filePath)
99
+
100
+ if (stats.isDirectory()) {
101
+ return {
102
+ format: 'unknown',
103
+ description: 'Directory found - SurrealDB restore expects a single file',
104
+ restoreCommand:
105
+ 'SurrealDB requires a single .surql file for restore',
106
+ }
107
+ }
108
+
109
+ // Check file extension first for .surql files
110
+ if (filePath.endsWith('.surql')) {
111
+ return {
112
+ format: 'surql',
113
+ description: 'SurrealDB SurrealQL backup',
114
+ restoreCommand:
115
+ 'Execute SurrealQL statements via surreal import (spindb restore handles this)',
116
+ }
117
+ }
118
+
119
+ // Content-based detection
120
+ if (await looksLikeSurql(filePath)) {
121
+ return {
122
+ format: 'surql',
123
+ description: 'SurrealDB SurrealQL backup (detected by content)',
124
+ restoreCommand:
125
+ 'Execute SurrealQL statements via surreal import (spindb restore handles this)',
126
+ }
127
+ }
128
+
129
+ return {
130
+ format: 'unknown',
131
+ description: 'Unknown backup format',
132
+ restoreCommand: 'Use .surql file with SurrealQL statements',
133
+ }
134
+ }
135
+
136
+ // Restore options for SurrealDB
137
+ export type RestoreOptions = {
138
+ containerName: string
139
+ port: number
140
+ database?: string
141
+ version?: string
142
+ }
143
+
144
+ /**
145
+ * Restore from SurrealQL backup using surreal import
146
+ */
147
+ async function restoreSurqlBackup(
148
+ backupPath: string,
149
+ port: number,
150
+ namespace: string,
151
+ database: string,
152
+ version?: string,
153
+ ): Promise<RestoreResult> {
154
+ const surrealPath = await requireSurrealPath(version)
155
+
156
+ return new Promise<RestoreResult>((resolve, reject) => {
157
+ const args = [
158
+ 'import',
159
+ '--endpoint', `http://127.0.0.1:${port}`,
160
+ '--user', 'root',
161
+ '--pass', 'root',
162
+ '--ns', namespace,
163
+ '--db', database,
164
+ backupPath,
165
+ ]
166
+
167
+ logDebug(`Running: surreal ${args.join(' ')}`)
168
+
169
+ const proc = spawn(surrealPath, args, {
170
+ stdio: ['ignore', 'pipe', 'pipe'],
171
+ })
172
+
173
+ let stdout = ''
174
+ let stderr = ''
175
+
176
+ proc.stdout.on('data', (data: Buffer) => {
177
+ stdout += data.toString()
178
+ })
179
+
180
+ proc.stderr.on('data', (data: Buffer) => {
181
+ stderr += data.toString()
182
+ })
183
+
184
+ proc.on('close', (code) => {
185
+ if (code === 0) {
186
+ resolve({
187
+ format: 'surql',
188
+ stdout: stdout || 'SurrealQL statements imported successfully',
189
+ stderr: stderr || undefined,
190
+ code: 0,
191
+ })
192
+ } else {
193
+ reject(
194
+ new Error(
195
+ `surreal import exited with code ${code}${stderr ? `: ${stderr}` : ''}`,
196
+ ),
197
+ )
198
+ }
199
+ })
200
+
201
+ proc.on('error', (error) => {
202
+ reject(new Error(`Failed to spawn surreal import: ${error.message}`))
203
+ })
204
+ })
205
+ }
206
+
207
+ /**
208
+ * Restore from backup
209
+ * Supports:
210
+ * - SurrealQL: Execute statements via surreal import
211
+ */
212
+ export async function restoreBackup(
213
+ backupPath: string,
214
+ options: RestoreOptions,
215
+ ): Promise<RestoreResult> {
216
+ const { containerName, port, database = 'default', version } = options
217
+ // Use container name as namespace (convert dashes to underscores)
218
+ const namespace = containerName.replace(/-/g, '_')
219
+
220
+ if (!existsSync(backupPath)) {
221
+ throw new Error(`Backup file not found: ${backupPath}`)
222
+ }
223
+
224
+ // Detect backup format
225
+ const format = await detectBackupFormat(backupPath)
226
+ logDebug(`Detected backup format: ${format.format}`)
227
+
228
+ if (format.format === 'surql') {
229
+ return restoreSurqlBackup(backupPath, port, namespace, database, version)
230
+ }
231
+
232
+ throw new Error(
233
+ `Invalid backup format: ${format.format}. Use .surql file with SurrealQL statements.`,
234
+ )
235
+ }
236
+
237
+ /**
238
+ * Parse SurrealDB connection string
239
+ * Format: surrealdb://[user:password@]host[:port][/namespace/database]
240
+ * Or: ws://host:port or http://host:port
241
+ */
242
+ export function parseConnectionString(connectionString: string): {
243
+ host: string
244
+ port: number
245
+ namespace: string
246
+ database: string
247
+ user?: string
248
+ password?: string
249
+ } {
250
+ if (!connectionString || typeof connectionString !== 'string') {
251
+ throw new Error(
252
+ 'Invalid SurrealDB connection string: expected a non-empty string',
253
+ )
254
+ }
255
+
256
+ let url: URL
257
+ try {
258
+ url = new URL(connectionString)
259
+ } catch (error) {
260
+ // Mask credentials in error message if present
261
+ const sanitized = connectionString.replace(
262
+ /\/\/([^:]+):([^@]+)@/,
263
+ '//***:***@',
264
+ )
265
+ throw new Error(
266
+ `Invalid SurrealDB connection string: "${sanitized}". ` +
267
+ `Expected format: surrealdb://[user:password@]host[:port][/namespace/database]`,
268
+ { cause: error },
269
+ )
270
+ }
271
+
272
+ // Validate protocol
273
+ const validProtocols = ['surrealdb:', 'ws:', 'wss:', 'http:', 'https:']
274
+ if (!validProtocols.includes(url.protocol)) {
275
+ throw new Error(
276
+ `Invalid SurrealDB connection string: unsupported protocol "${url.protocol}". ` +
277
+ `Expected one of: ${validProtocols.join(', ')}`,
278
+ )
279
+ }
280
+
281
+ const host = url.hostname || '127.0.0.1'
282
+ const port = parseInt(url.port, 10) || 8000
283
+
284
+ // Parse namespace/database from pathname (e.g., /myns/mydb)
285
+ const pathParts = url.pathname.split('/').filter(Boolean)
286
+ const namespace = pathParts[0] || 'test'
287
+ const database = pathParts[1] || 'test'
288
+
289
+ return {
290
+ host,
291
+ port,
292
+ namespace,
293
+ database,
294
+ user: url.username || undefined,
295
+ password: url.password || undefined,
296
+ }
297
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * SurrealDB version mapping
3
+ *
4
+ * Maps short version aliases to full versions from hostdb releases.
5
+ * MUST stay in sync with hostdb releases.json
6
+ */
7
+
8
+ // Full version map for SurrealDB
9
+ export const SURREALDB_VERSION_MAP: Record<string, string> = {
10
+ '2': '2.3.2',
11
+ '2.3': '2.3.2',
12
+ '2.3.2': '2.3.2',
13
+ }
14
+
15
+ // Supported major versions (for CLI display)
16
+ export const SUPPORTED_MAJOR_VERSIONS = ['2']
17
+
18
+ // Default version
19
+ export const DEFAULT_VERSION = '2'
20
+
21
+ /**
22
+ * Normalize a version string to its full version
23
+ * e.g., '2' -> '2.3.2', '2.3' -> '2.3.2'
24
+ */
25
+ export function normalizeVersion(version: string): string {
26
+ return SURREALDB_VERSION_MAP[version] || version
27
+ }
28
+
29
+ /**
30
+ * Check if a version is supported
31
+ */
32
+ export function isVersionSupported(version: string): boolean {
33
+ return version in SURREALDB_VERSION_MAP
34
+ }
35
+
36
+ /**
37
+ * Get the latest patch version for a major version
38
+ */
39
+ export function getLatestPatch(majorVersion: string): string | undefined {
40
+ return SURREALDB_VERSION_MAP[majorVersion]
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spindb",
3
- "version": "0.24.0",
3
+ "version": "0.26.2",
4
4
  "description": "Zero-config Docker-free local database containers. Create, backup, and clone a variety of popular databases.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,6 +42,8 @@
42
42
  "qdrant",
43
43
  "meilisearch",
44
44
  "couchdb",
45
+ "cockroachdb",
46
+ "surrealdb",
45
47
  "ferretdb",
46
48
  "sqlite",
47
49
  "duckdb",
package/types/index.ts CHANGED
@@ -34,6 +34,8 @@ export enum Engine {
34
34
  Qdrant = 'qdrant',
35
35
  Meilisearch = 'meilisearch',
36
36
  CouchDB = 'couchdb',
37
+ CockroachDB = 'cockroachdb',
38
+ SurrealDB = 'surrealdb',
37
39
  }
38
40
 
39
41
  // Supported operating systems (matches Node.js process.platform)
@@ -67,6 +69,8 @@ export const ALL_ENGINES = [
67
69
  Engine.Qdrant,
68
70
  Engine.Meilisearch,
69
71
  Engine.CouchDB,
72
+ Engine.CockroachDB,
73
+ Engine.SurrealDB,
70
74
  ] as const
71
75
 
72
76
  // File-based engines (no server process, data stored in user project directories)
@@ -193,6 +197,8 @@ export type QdrantFormat = 'snapshot'
193
197
  export type MeilisearchFormat = 'snapshot'
194
198
  export type FerretDBFormat = 'sql' | 'custom'
195
199
  export type CouchDBFormat = 'json'
200
+ export type CockroachDBFormat = 'sql'
201
+ export type SurrealDBFormat = 'surql'
196
202
 
197
203
  // Union of all backup formats
198
204
  export type BackupFormatType =
@@ -209,6 +215,8 @@ export type BackupFormatType =
209
215
  | QdrantFormat
210
216
  | MeilisearchFormat
211
217
  | CouchDBFormat
218
+ | CockroachDBFormat
219
+ | SurrealDBFormat
212
220
 
213
221
  // Mapping from Engine to its corresponding backup format type
214
222
  type EngineFormatMap = {
@@ -225,6 +233,8 @@ type EngineFormatMap = {
225
233
  [Engine.Qdrant]: QdrantFormat
226
234
  [Engine.Meilisearch]: MeilisearchFormat
227
235
  [Engine.CouchDB]: CouchDBFormat
236
+ [Engine.CockroachDB]: CockroachDBFormat
237
+ [Engine.SurrealDB]: SurrealDBFormat
228
238
  }
229
239
 
230
240
  // Helper type to get format type for a specific engine
@@ -330,6 +340,10 @@ export type BinaryTool =
330
340
  | 'ferretdb'
331
341
  // CouchDB tools
332
342
  | 'couchdb'
343
+ // CockroachDB tools
344
+ | 'cockroach'
345
+ // SurrealDB tools
346
+ | 'surreal'
333
347
  // Enhanced shells (optional)
334
348
  | 'pgcli'
335
349
  | 'mycli'
@@ -405,6 +419,10 @@ export type SpinDBConfig = {
405
419
  ferretdb?: BinaryConfig
406
420
  // CouchDB tools
407
421
  couchdb?: BinaryConfig
422
+ // CockroachDB tools
423
+ cockroach?: BinaryConfig
424
+ // SurrealDB tools
425
+ surreal?: BinaryConfig
408
426
  // Enhanced shells (optional)
409
427
  pgcli?: BinaryConfig
410
428
  mycli?: BinaryConfig