spindb 0.35.4 → 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,302 @@
1
+ # Weaviate Engine Implementation
2
+
3
+ ## Overview
4
+
5
+ Weaviate is an AI-native vector database with REST and gRPC APIs. Like Qdrant and Meilisearch, it uses HTTP for all operations. Uses classes/collections instead of traditional databases.
6
+
7
+ ## Platform Support
8
+
9
+ | Platform | Architecture | Status | Notes |
10
+ |----------|--------------|--------|-------|
11
+ | darwin | x64 | Supported | Uses hostdb binaries |
12
+ | darwin | arm64 | Supported | Uses hostdb binaries (Apple Silicon) |
13
+ | linux | x64 | Supported | Uses hostdb binaries |
14
+ | linux | arm64 | Supported | Uses hostdb binaries |
15
+ | win32 | x64 | Supported | Uses hostdb binaries |
16
+
17
+ ## Binary Packaging
18
+
19
+ ### Archive Format
20
+ - **Unix (macOS/Linux)**: `tar.gz`
21
+ - **Windows**: `zip`
22
+
23
+ ### Archive Structure
24
+ ```text
25
+ weaviate/
26
+ └── bin/
27
+ └── weaviate # Server binary
28
+ ```
29
+
30
+ ### Version Map Sync
31
+
32
+ ```typescript
33
+ export const WEAVIATE_VERSION_MAP: Record<string, string> = {
34
+ '1': '1.35.7',
35
+ }
36
+ ```
37
+
38
+ ## Implementation Details
39
+
40
+ ### Binary Manager
41
+
42
+ Weaviate uses `BaseBinaryManager` with a custom `verify()` override:
43
+
44
+ ```typescript
45
+ // Weaviate doesn't support --version (as of v1.35.x)
46
+ // Verification just checks binary existence
47
+ async verify(): Promise<boolean> {
48
+ return existsSync(binaryPath)
49
+ }
50
+ ```
51
+
52
+ See: https://github.com/weaviate/weaviate/issues/6571
53
+
54
+ ### Version Parsing
55
+
56
+ Not applicable for current version (no `--version` flag). The `parseVersionFromOutput` method is implemented for forward compatibility when the flag is added:
57
+ - **Parse pattern**: `/(?:weaviate\s+)?v?(\d+\.\d+\.\d+)/`
58
+
59
+ ### REST API Engine
60
+
61
+ Weaviate is a **REST API engine** - it doesn't have a CLI shell:
62
+ - `spindb run` is **NOT applicable**
63
+ - `spindb connect` opens the web dashboard in browser
64
+ - All data operations use HTTP REST API
65
+
66
+ ### Dual Ports
67
+
68
+ Weaviate uses two ports:
69
+ - **HTTP Port** (default 8080): REST API
70
+ - **gRPC Port** (default 8081): gRPC API (typically HTTP + 1)
71
+
72
+ ### Default Configuration
73
+
74
+ - **Default HTTP Port**: 8080 (auto-increments on conflict)
75
+ - **gRPC Port**: HTTP port + 1
76
+ - **Health Endpoint**: `/v1/.well-known/ready`
77
+ - **Schema Endpoint**: `/v1/schema`
78
+ - **PID File**: `weaviate.pid` in container directory
79
+
80
+ ### Environment Variable Configuration
81
+
82
+ Weaviate uses environment variables (not a config file):
83
+
84
+ ```bash
85
+ PERSISTENCE_DATA_PATH=/path/to/data
86
+ BACKUP_FILESYSTEM_PATH=/path/to/data/backups
87
+ QUERY_DEFAULTS_LIMIT=25
88
+ AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED=true
89
+ DEFAULT_VECTORIZER_MODULE=none
90
+ ENABLE_MODULES=backup-filesystem
91
+ GRPC_PORT=8081
92
+ CLUSTER_HOSTNAME=node-{port} # Must be unique per container
93
+ CLUSTER_GOSSIP_BIND_PORT={port+100} # Memberlist gossip (default 7946)
94
+ CLUSTER_DATA_BIND_PORT={port+101} # Memberlist data (default 7947)
95
+ RAFT_PORT={port+200} # Raft consensus (default 8300)
96
+ RAFT_INTERNAL_RPC_PORT={port+201} # Raft internal RPC (default 8301)
97
+ ```
98
+
99
+ ### Internal Cluster Ports
100
+
101
+ Weaviate uses 4 internal cluster ports in addition to HTTP and gRPC. These **must be unique per container** or Weaviate will fail to start (or silently conflict with other instances):
102
+
103
+ | Port | Default | SpinDB Formula | Purpose |
104
+ |------|---------|----------------|---------|
105
+ | HTTP | 8080 | `{port}` | REST API |
106
+ | gRPC | 8081 | `{port}+1` | gRPC API |
107
+ | Gossip | 7946 | `{port}+100` | Memberlist gossip |
108
+ | Data | 7947 | `{port}+101` | Memberlist data |
109
+ | Raft | 8300 | `{port}+200` | Raft consensus |
110
+ | Raft RPC | 8301 | `{port}+201` | Raft internal RPC |
111
+
112
+ ### Connection String Format
113
+
114
+ ```text
115
+ http://127.0.0.1:{port}
116
+ ```
117
+
118
+ ### Web Dashboard
119
+
120
+ The `connect` command opens the root URL in the default browser:
121
+ ```text
122
+ http://localhost:{port}/
123
+ ```
124
+
125
+ ## Backup & Restore
126
+
127
+ ### Backup Formats
128
+
129
+ | Format | Extension | Tool | Notes |
130
+ |--------|-----------|------|-------|
131
+ | snapshot | `.snapshot` | REST API | Weaviate filesystem backup |
132
+
133
+ ### Backup API
134
+
135
+ Backup and restore use Weaviate's filesystem backup endpoints:
136
+ - `POST /v1/backups/filesystem` - Create backup (with status polling)
137
+ - `GET /v1/backups/filesystem/{id}` - Check backup status
138
+ - `POST /v1/backups/filesystem/{id}/restore` - Restore backup
139
+
140
+ ### Backup Flow
141
+
142
+ 1. `BACKUP_FILESYSTEM_PATH` env var points to `{dataDir}/backups`
143
+ 2. `ENABLE_MODULES=backup-filesystem` must be set (or backup API returns 404)
144
+ 3. Trigger backup via `POST /v1/backups/filesystem` with `{ id: "spindb-backup-{ts}" }`
145
+ 4. Poll status via `GET /v1/backups/filesystem/{id}` until `SUCCESS`
146
+ 5. Copy backup **directory** (not a single file) from `{backupsDir}/{id}/` to output path
147
+
148
+ ### Restore Flow
149
+
150
+ 1. Copy backup directory into target container's `{backupsDir}/{backupId}/`
151
+ 2. **The directory name MUST match the internal backup ID** stored in `backup_config.json` inside the backup. Weaviate validates this and returns 422 on mismatch.
152
+ 3. Start Weaviate
153
+ 4. Trigger restore via `POST /v1/backups/filesystem/{backupId}/restore`
154
+ 5. If restoring to a container with a different `CLUSTER_HOSTNAME`, pass `node_mapping` in the request body:
155
+ ```json
156
+ { "node_mapping": { "node-8080": "node-9090" } }
157
+ ```
158
+ 6. Poll restore status until `SUCCESS`
159
+
160
+ ## Integration Test Notes
161
+
162
+ ### REST API Testing
163
+
164
+ Integration tests use `fetch()` for operations, not CLI tools.
165
+
166
+ ### Test Ports
167
+
168
+ ```typescript
169
+ weaviate: { base: 8090, clone: 8092, renamed: 8091 }
170
+ ```
171
+
172
+ ## Docker E2E Test Notes
173
+
174
+ Weaviate Docker E2E uses `curl` for all operations:
175
+
176
+ ```bash
177
+ # Health check
178
+ curl http://localhost:8080/v1/.well-known/ready
179
+
180
+ # Create class
181
+ curl -X POST http://localhost:8080/v1/schema \
182
+ -H 'Content-Type: application/json' \
183
+ -d '{"class":"TestVectors","vectorizer":"none","properties":[...]}'
184
+
185
+ # Insert objects (batch)
186
+ curl -X POST http://localhost:8080/v1/batch/objects \
187
+ -H 'Content-Type: application/json' \
188
+ -d '{"objects":[...]}'
189
+ ```
190
+
191
+ ## Known Issues & Gotchas
192
+
193
+ ### 1. No --version Flag
194
+
195
+ Weaviate binary doesn't support `--version` as of v1.35.x. Tracked in [weaviate/weaviate#6571](https://github.com/weaviate/weaviate/issues/6571). Binary verification only checks file existence. Same pattern as CouchDB.
196
+
197
+ ### 2. No CLI Shell
198
+
199
+ `spindb run` does nothing for Weaviate. Use the REST API or web dashboard.
200
+
201
+ ### 3. Vector Database Semantics
202
+
203
+ Weaviate uses "classes" (or "collections") instead of "databases". Operations are vector-centric:
204
+ - Create classes with property schemas
205
+ - Insert objects with optional vectors
206
+ - Search by vector similarity or filters
207
+
208
+ ### 4. Internal Cluster Ports Must Be Unique
209
+
210
+ Weaviate binds 4 internal ports (gossip 7946, data 7947, raft 8300, raft RPC 8301) by default. Running multiple Weaviate containers without unique ports causes silent conflicts or startup failures. SpinDB derives unique ports from the HTTP port (see "Internal Cluster Ports" above).
211
+
212
+ ### 5. ENABLE_MODULES Required for Backup
213
+
214
+ `ENABLE_MODULES=backup-filesystem` must be set at startup or the backup/restore API endpoints return 404.
215
+
216
+ ### 6. Backup Directory Name Must Match Internal ID
217
+
218
+ Weaviate backups are directories (not single files). The backup directory name **must match** the internal backup ID in `backup_config.json`. When copying a backup to a new location, `restore.ts` reads `backup_config.json` to discover the real ID and names the target directory accordingly.
219
+
220
+ ### 7. Node Mapping for Cross-Container Restore
221
+
222
+ When restoring a backup to a container with a different `CLUSTER_HOSTNAME`, the Weaviate restore API requires a `node_mapping` parameter. Without it, restore fails with "cannot resolve hostname" (422).
223
+
224
+ ### 8. Windows Backup Fails (LSM File Locking)
225
+
226
+ Weaviate on Windows holds exclusive locks on LSM storage files, preventing `fsync` during backup while the server is running. The backup API returns "Access is denied" errors. Integration tests skip the backup/restore clone test on Windows. Same pattern as Meilisearch.
227
+
228
+ ### 9. gRPC Port
229
+
230
+ The gRPC port is separate from HTTP (HTTP + 1). Ensure both ports are available if using gRPC clients.
231
+
232
+ ### 10. Snapshot Format
233
+
234
+ Snapshots are Weaviate's native backup format and are not compatible with other databases.
235
+
236
+ ### 11. Health Check Endpoint
237
+
238
+ Use `/v1/.well-known/ready` for health checks (returns 200 when ready).
239
+
240
+ ### 12. Class/Collection Naming
241
+
242
+ Weaviate class names must start with an uppercase letter (PascalCase). Container names with dashes are auto-converted (e.g., `my-app` becomes class `My_app`).
243
+
244
+ ## CI/CD Notes
245
+
246
+ ### curl-Based Testing
247
+
248
+ CI tests use `curl` commands rather than database CLI tools.
249
+
250
+ ### GitHub Actions Cache Step
251
+
252
+ ```yaml
253
+ - name: Cache Weaviate binaries
254
+ uses: actions/cache@v4
255
+ with:
256
+ path: ~/.spindb/bin/weaviate-*
257
+ key: weaviate-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('engines/weaviate/version-maps.ts') }}
258
+ ```
259
+
260
+ ## REST API Quick Reference
261
+
262
+ ### Schema (Classes)
263
+ ```bash
264
+ # List all classes
265
+ GET /v1/schema
266
+
267
+ # Get class info
268
+ GET /v1/schema/{class}
269
+
270
+ # Create class
271
+ POST /v1/schema
272
+
273
+ # Delete class
274
+ DELETE /v1/schema/{class}
275
+ ```
276
+
277
+ ### Objects
278
+ ```bash
279
+ # Batch insert objects
280
+ POST /v1/batch/objects
281
+
282
+ # Get object
283
+ GET /v1/objects/{class}/{id}
284
+
285
+ # Delete object
286
+ DELETE /v1/objects/{class}/{id}
287
+ ```
288
+
289
+ ### Search
290
+ ```bash
291
+ # GraphQL query
292
+ POST /v1/graphql
293
+ ```
294
+
295
+ ### Meta
296
+ ```bash
297
+ # Server meta info (includes version)
298
+ GET /v1/meta
299
+
300
+ # Health/ready check
301
+ GET /v1/.well-known/ready
302
+ ```
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Shared Weaviate REST API client utilities
3
+ */
4
+
5
+ /**
6
+ * Make an HTTP request to Weaviate REST API
7
+ *
8
+ * @param port - The HTTP port Weaviate is listening on
9
+ * @param method - HTTP method (GET, POST, PUT, DELETE)
10
+ * @param path - API path (e.g., '/v1/schema', '/v1/.well-known/ready')
11
+ * @param body - Optional JSON body for POST/PUT requests
12
+ * @param timeoutMs - Request timeout in milliseconds (default: 30s)
13
+ */
14
+ export async function weaviateApiRequest(
15
+ port: number,
16
+ method: string,
17
+ path: string,
18
+ body?: Record<string, unknown>,
19
+ timeoutMs = 30000,
20
+ ): Promise<{ status: number; data: unknown }> {
21
+ const url = `http://127.0.0.1:${port}${path}`
22
+
23
+ const controller = new AbortController()
24
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
25
+
26
+ const options: RequestInit = {
27
+ method,
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ },
31
+ signal: controller.signal,
32
+ }
33
+
34
+ if (body) {
35
+ options.body = JSON.stringify(body)
36
+ }
37
+
38
+ try {
39
+ const response = await fetch(url, options)
40
+
41
+ // Try to parse as JSON, fall back to text for endpoints like /v1/.well-known/ready
42
+ let data: unknown
43
+ const contentType = response.headers.get('content-type') || ''
44
+ if (contentType.includes('application/json')) {
45
+ data = await response.json()
46
+ } else {
47
+ data = await response.text()
48
+ }
49
+
50
+ return { status: response.status, data }
51
+ } catch (error) {
52
+ if (error instanceof Error && error.name === 'AbortError') {
53
+ throw new Error(
54
+ `Weaviate API request timed out after ${timeoutMs / 1000}s: ${method} ${path}`,
55
+ )
56
+ }
57
+ throw error
58
+ } finally {
59
+ clearTimeout(timeoutId)
60
+ }
61
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Weaviate backup module
3
+ * Supports snapshot-based backup using Weaviate's filesystem backup API.
4
+ *
5
+ * Weaviate's filesystem backup creates a directory at BACKUP_FILESYSTEM_PATH/<id>/
6
+ * containing backup metadata and class data. We copy this entire directory
7
+ * as the "snapshot" for backup/restore.
8
+ */
9
+
10
+ import { mkdir, stat, cp, readdir } from 'fs/promises'
11
+ import { existsSync } from 'fs'
12
+ import { join, dirname } from 'path'
13
+ import { logDebug } from '../../core/error-handler'
14
+ import { paths } from '../../config/paths'
15
+ import { weaviateApiRequest } from './api-client'
16
+ import type { ContainerConfig, BackupOptions, BackupResult } from '../../types'
17
+
18
+ /**
19
+ * Create a snapshot backup using Weaviate's REST API.
20
+ * Triggers a filesystem backup, polls for completion, then copies the
21
+ * backup directory to the output path.
22
+ */
23
+ export async function createBackup(
24
+ container: ContainerConfig,
25
+ outputPath: string,
26
+ _options: BackupOptions,
27
+ ): Promise<BackupResult> {
28
+ const { port, name } = container
29
+
30
+ // Ensure output parent directory exists
31
+ const outputDir = dirname(outputPath)
32
+ if (!existsSync(outputDir)) {
33
+ await mkdir(outputDir, { recursive: true })
34
+ }
35
+
36
+ // Generate a unique backup ID
37
+ const backupId = `spindb-backup-${Date.now()}`
38
+
39
+ // Trigger backup creation via REST API
40
+ logDebug(
41
+ `Creating Weaviate backup '${backupId}' via REST API on port ${port}`,
42
+ )
43
+
44
+ const response = await weaviateApiRequest(
45
+ port,
46
+ 'POST',
47
+ '/v1/backups/filesystem',
48
+ { id: backupId },
49
+ 600000, // 10 minute timeout
50
+ )
51
+
52
+ if (response.status !== 200) {
53
+ throw new Error(
54
+ `Failed to create Weaviate backup: ${JSON.stringify(response.data)}`,
55
+ )
56
+ }
57
+
58
+ logDebug(`Weaviate backup initiated: ${backupId}`)
59
+
60
+ // Poll for backup completion
61
+ const maxWait = 300000 // 5 minutes
62
+ const startTime = Date.now()
63
+
64
+ let backupCompleted = false
65
+
66
+ while (Date.now() - startTime < maxWait) {
67
+ const statusResponse = await weaviateApiRequest(
68
+ port,
69
+ 'GET',
70
+ `/v1/backups/filesystem/${backupId}`,
71
+ )
72
+
73
+ if (statusResponse.status === 200) {
74
+ const statusData = statusResponse.data as {
75
+ status?: string
76
+ path?: string
77
+ }
78
+
79
+ if (statusData.status === 'SUCCESS') {
80
+ logDebug(`Weaviate backup completed: ${backupId}`)
81
+ backupCompleted = true
82
+ break
83
+ }
84
+
85
+ if (statusData.status === 'FAILED') {
86
+ throw new Error(`Weaviate backup failed: ${JSON.stringify(statusData)}`)
87
+ }
88
+
89
+ logDebug(`Backup status: ${statusData.status}, waiting...`)
90
+ }
91
+
92
+ await new Promise((r) => setTimeout(r, 1000))
93
+ }
94
+
95
+ if (!backupCompleted) {
96
+ throw new Error(
97
+ `Weaviate backup '${backupId}' timed out after ${maxWait / 1000}s without completing`,
98
+ )
99
+ }
100
+
101
+ // Weaviate stores backup at BACKUP_FILESYSTEM_PATH/<backupId>/
102
+ // BACKUP_FILESYSTEM_PATH is set to <dataDir>/backups in start()
103
+ const dataDir = paths.getContainerDataPath(name, { engine: 'weaviate' })
104
+ const backupDir = join(dataDir, 'backups', backupId)
105
+
106
+ if (!existsSync(backupDir)) {
107
+ throw new Error(
108
+ `Weaviate backup directory not found at ${backupDir} after completion`,
109
+ )
110
+ }
111
+
112
+ // Copy entire backup directory to output path
113
+ await cp(backupDir, outputPath, { recursive: true })
114
+
115
+ // Get total size of backup directory
116
+ const files = await readdir(backupDir, { recursive: true })
117
+ let totalSize = 0
118
+ for (const file of files) {
119
+ try {
120
+ const filePath = join(backupDir, String(file))
121
+ const stats = await stat(filePath)
122
+ if (stats.isFile()) {
123
+ totalSize += stats.size
124
+ }
125
+ } catch {
126
+ // Skip inaccessible files
127
+ }
128
+ }
129
+
130
+ return {
131
+ path: outputPath,
132
+ format: 'snapshot',
133
+ size: totalSize,
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Create a backup for cloning purposes
139
+ */
140
+ export async function createCloneBackup(
141
+ container: ContainerConfig,
142
+ outputPath: string,
143
+ ): Promise<BackupResult> {
144
+ return createBackup(container, outputPath, { database: 'default' })
145
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Weaviate Binary Manager
3
+ *
4
+ * Handles downloading, extracting, and managing Weaviate binaries from hostdb.
5
+ * Extends BaseBinaryManager for shared download/extraction logic.
6
+ *
7
+ * Note: Weaviate doesn't support --version flag (as of v1.35.x). We override
8
+ * verify() to just check binary existence instead of running it.
9
+ */
10
+
11
+ import { existsSync } from 'fs'
12
+ import { join } from 'path'
13
+ import {
14
+ BaseBinaryManager,
15
+ type BinaryManagerConfig,
16
+ } from '../../core/base-binary-manager'
17
+ import { getBinaryUrl } from './binary-urls'
18
+ import { normalizeVersion } from './version-maps'
19
+ import { Engine, Platform, type Arch } from '../../types'
20
+ import { paths } from '../../config/paths'
21
+
22
+ class WeaviateBinaryManager extends BaseBinaryManager {
23
+ protected readonly config: BinaryManagerConfig = {
24
+ engine: Engine.Weaviate,
25
+ engineName: 'weaviate',
26
+ displayName: 'Weaviate',
27
+ serverBinary: 'weaviate',
28
+ }
29
+
30
+ protected getBinaryUrlFromModule(
31
+ version: string,
32
+ platform: Platform,
33
+ arch: Arch,
34
+ ): string {
35
+ return getBinaryUrl(version, platform, arch)
36
+ }
37
+
38
+ protected normalizeVersionFromModule(version: string): string {
39
+ return normalizeVersion(version)
40
+ }
41
+
42
+ protected parseVersionFromOutput(stdout: string): string | null {
43
+ // Extract version from output like "weaviate v1.35.7" or "1.35.7"
44
+ const match = stdout.match(/(?:weaviate\s+)?v?(\d+\.\d+\.\d+)/)
45
+ return match?.[1] ?? null
46
+ }
47
+
48
+ /**
49
+ * Override verify to just check binary existence.
50
+ * Weaviate doesn't support --version flag (as of v1.35.x).
51
+ * See: https://github.com/weaviate/weaviate/issues/6571
52
+ */
53
+ async verify(
54
+ version: string,
55
+ platform: Platform,
56
+ arch: Arch,
57
+ ): Promise<boolean> {
58
+ const fullVersion = this.getFullVersion(version)
59
+ const binPath = paths.getBinaryPath({
60
+ engine: this.config.engineName,
61
+ version: fullVersion,
62
+ platform,
63
+ arch,
64
+ })
65
+
66
+ const ext = platform === Platform.Win32 ? '.exe' : ''
67
+ const serverPath = join(binPath, 'bin', `${this.config.serverBinary}${ext}`)
68
+
69
+ if (!existsSync(serverPath)) {
70
+ throw new Error(
71
+ `${this.config.displayName} binary not found at ${binPath}/bin/`,
72
+ )
73
+ }
74
+
75
+ // Just verify the binary exists - we can't run --version on Weaviate
76
+ return true
77
+ }
78
+ }
79
+
80
+ export const weaviateBinaryManager = new WeaviateBinaryManager()
@@ -0,0 +1,115 @@
1
+ import { WEAVIATE_VERSION_MAP } from './version-maps'
2
+ import { buildHostdbUrl } from '../../core/hostdb-client'
3
+ import { logDebug } from '../../core/error-handler'
4
+ import { Engine, Platform, type Arch } from '../../types'
5
+
6
+ /**
7
+ * Supported platform identifiers for hostdb downloads.
8
+ * hostdb uses standard Node.js platform naming - this set validates
9
+ * that a platform/arch combination is supported, not transforms it.
10
+ */
11
+ const SUPPORTED_PLATFORMS = new Set([
12
+ 'darwin-arm64',
13
+ 'darwin-x64',
14
+ 'linux-arm64',
15
+ 'linux-x64',
16
+ 'win32-x64',
17
+ ])
18
+
19
+ /**
20
+ * Get the hostdb platform identifier
21
+ *
22
+ * hostdb uses standard platform naming that matches Node.js identifiers directly.
23
+ * This function validates the platform/arch combination is supported.
24
+ *
25
+ * @param platform - Node.js platform (e.g., 'darwin', 'linux', 'win32')
26
+ * @param arch - Node.js architecture (e.g., 'arm64', 'x64')
27
+ * @returns hostdb platform identifier or null if unsupported
28
+ */
29
+ export function getHostdbPlatform(
30
+ platform: Platform,
31
+ arch: Arch,
32
+ ): string | null {
33
+ const key = `${platform}-${arch}`
34
+ return SUPPORTED_PLATFORMS.has(key) ? key : null
35
+ }
36
+
37
+ /**
38
+ * Build the download URL for Weaviate binaries from hostdb
39
+ *
40
+ * Format: https://registry.layerbase.host/weaviate-{version}/weaviate-{version}-{platform}-{arch}.{ext}
41
+ *
42
+ * @param version - Weaviate version (e.g., '1', '1.35.7')
43
+ * @param platform - Platform identifier (e.g., 'darwin', 'linux', 'win32')
44
+ * @param arch - Architecture identifier (e.g., 'arm64', 'x64')
45
+ * @returns Download URL for the binary
46
+ */
47
+ export function getBinaryUrl(
48
+ version: string,
49
+ platform: Platform,
50
+ arch: Arch,
51
+ ): string {
52
+ const platformKey = `${platform}-${arch}`
53
+ const hostdbPlatform = getHostdbPlatform(platform, arch)
54
+ if (!hostdbPlatform) {
55
+ const supported = Array.from(SUPPORTED_PLATFORMS).join(', ')
56
+ throw new Error(
57
+ `Unsupported platform: ${platformKey}. Supported platforms: ${supported}`,
58
+ )
59
+ }
60
+
61
+ // Normalize version (handles major version lookup and X.Y -> X.Y.Z conversion)
62
+ const fullVersion = normalizeVersion(version, WEAVIATE_VERSION_MAP)
63
+ const ext = platform === Platform.Win32 ? 'zip' : 'tar.gz'
64
+
65
+ return buildHostdbUrl(Engine.Weaviate, {
66
+ version: fullVersion,
67
+ hostdbPlatform,
68
+ extension: ext,
69
+ })
70
+ }
71
+
72
+ /**
73
+ * Normalize version string to X.Y.Z format
74
+ *
75
+ * @param version - Version string (e.g., '1', '1.35', '1.35.7')
76
+ * @param versionMap - Optional version map for major version lookup
77
+ * @returns Normalized version (e.g., '1.35.7')
78
+ */
79
+ function normalizeVersion(
80
+ version: string,
81
+ versionMap: Record<string, string> = WEAVIATE_VERSION_MAP,
82
+ ): string {
83
+ // Check if it's an exact key in the map (handles "1", "1.35", etc.)
84
+ if (versionMap[version]) {
85
+ return versionMap[version]
86
+ }
87
+
88
+ const parts = version.split('.')
89
+
90
+ // If it's already a full version (X.Y.Z), return as-is
91
+ if (parts.length === 3) {
92
+ return version
93
+ }
94
+
95
+ // For two-part versions (e.g., "1.35"), first try exact two-part key, then fall back to major
96
+ if (parts.length === 2) {
97
+ const twoPart = `${parts[0]}.${parts[1]}`
98
+ if (versionMap[twoPart]) {
99
+ return versionMap[twoPart]
100
+ }
101
+ // Fall back to major version for latest patch
102
+ const major = parts[0]
103
+ const mapped = versionMap[major]
104
+ if (mapped) {
105
+ return mapped
106
+ }
107
+ }
108
+
109
+ // Unknown version format - log and return as-is
110
+ // This may cause download failures if the version doesn't exist in hostdb
111
+ logDebug(
112
+ `Weaviate version '${version}' not in version map, may not be available in hostdb`,
113
+ )
114
+ return version
115
+ }