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.
- package/README.md +53 -14
- package/cli/commands/engines.ts +89 -1
- package/cli/commands/menu/backup-handlers.ts +19 -0
- package/cli/commands/menu/container-handlers.ts +4 -2
- package/cli/commands/menu/shell-handlers.ts +52 -2
- package/cli/commands/menu/sql-handlers.ts +7 -1
- package/cli/constants.ts +4 -0
- package/cli/helpers.ts +144 -0
- package/cli/index.ts +1 -1
- package/config/backup-formats.ts +28 -0
- package/config/engine-defaults.ts +26 -0
- package/config/engines.json +32 -0
- package/core/config-manager.ts +5 -0
- package/core/container-manager.ts +10 -4
- package/core/dependency-manager.ts +4 -0
- package/engines/base-engine.ts +16 -0
- package/engines/cockroachdb/backup.ts +363 -0
- package/engines/cockroachdb/binary-manager.ts +45 -0
- package/engines/cockroachdb/binary-urls.ts +37 -0
- package/engines/cockroachdb/cli-utils.ts +384 -0
- package/engines/cockroachdb/hostdb-releases.ts +111 -0
- package/engines/cockroachdb/index.ts +1052 -0
- package/engines/cockroachdb/restore.ts +448 -0
- package/engines/cockroachdb/version-maps.ts +42 -0
- package/engines/index.ts +8 -0
- package/engines/surrealdb/backup.ts +122 -0
- package/engines/surrealdb/binary-manager.ts +45 -0
- package/engines/surrealdb/binary-urls.ts +37 -0
- package/engines/surrealdb/cli-utils.ts +175 -0
- package/engines/surrealdb/hostdb-releases.ts +111 -0
- package/engines/surrealdb/index.ts +949 -0
- package/engines/surrealdb/restore.ts +297 -0
- package/engines/surrealdb/version-maps.ts +41 -0
- package/package.json +3 -1
- 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.
|
|
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
|