@walkthru-earth/objex-utils 1.1.0 → 1.2.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,253 @@
1
+ # Storage
2
+
3
+ URL parsing, provider registry, and the `StorageAdapter` contract.
4
+
5
+ Sources:
6
+
7
+ - `src/lib/utils/storage-url.ts` — generic URL / bucket parser
8
+ - `src/lib/utils/cloud-url.ts` — cloud-scheme → HTTPS resolver
9
+ - `src/lib/storage/providers.ts` — provider registry + access-mode logic
10
+ - `src/lib/storage/adapter.ts` — the `StorageAdapter` interface
11
+ - `src/lib/storage/url-adapter.ts` — adapter for arbitrary HTTPS URLs
12
+
13
+ ## URL parsing
14
+
15
+ ### `parseStorageUrl(input, defaults?)`
16
+
17
+ ```ts
18
+ function parseStorageUrl(
19
+ input: string,
20
+ defaults?: Defaults
21
+ ): ParsedStorageUrl
22
+ ```
23
+
24
+ Universal parser for any bucket / cloud URL a user might paste. Accepted formats:
25
+
26
+ - URI schemes: `s3://`, `s3a://`, `s3n://`, `r2://`, `gs://`, `gcs://`, `azure://`, `az://`, `abfs://`, `abfss://`, `wasbs://`, `swift://`, `file://`
27
+ - AWS S3 virtual-hosted, path-style, global URLs (region auto-detected from host)
28
+ - Cloudflare R2, Google Cloud Storage, Azure, DigitalOcean Spaces, Wasabi, Backblaze B2, Alibaba OSS, Tencent COS, Yandex, Storj, Contabo, Hetzner, Linode, OVHcloud, MinIO
29
+ - Generic custom endpoints
30
+ - Plain bucket names (defaults to `s3`)
31
+
32
+ **Types**
33
+
34
+ ```ts
35
+ interface ParsedStorageUrl {
36
+ bucket: string;
37
+ region: string;
38
+ endpoint: string;
39
+ provider: StorageProvider; // same ID space as ProviderId (see below)
40
+ prefix: string; // path after bucket (may be '')
41
+ }
42
+
43
+ interface Defaults {
44
+ region?: string;
45
+ endpoint?: string;
46
+ provider?: StorageProvider;
47
+ }
48
+ ```
49
+
50
+ **Example**
51
+
52
+ ```ts
53
+ parseStorageUrl('s3://us-west-2.my-bucket/data/*.parquet');
54
+ // {
55
+ // provider: 's3',
56
+ // bucket: 'my-bucket',
57
+ // region: 'us-west-2',
58
+ // endpoint: 'https://s3.us-west-2.amazonaws.com',
59
+ // prefix: 'data/*.parquet'
60
+ // }
61
+ ```
62
+
63
+ ### `looksLikeUrl(input)`
64
+
65
+ ```ts
66
+ function looksLikeUrl(input: string): boolean
67
+ ```
68
+
69
+ Return `true` if `input` resembles a URL/URI rather than a plain bucket name. Useful to decide whether to pass it to `parseStorageUrl`.
70
+
71
+ ### `describeParseResult(parsed)`
72
+
73
+ ```ts
74
+ function describeParseResult(parsed: ParsedStorageUrl): string
75
+ ```
76
+
77
+ Build a human-readable summary of a parse result (bucket, endpoint, region, provider, prefix). Used in the objex UI but safe to render anywhere.
78
+
79
+ ### `resolveCloudUrl(url)`
80
+
81
+ ```ts
82
+ function resolveCloudUrl(url: string): string
83
+ ```
84
+
85
+ Convert a cloud-protocol URL to an HTTPS URL that any fetch client can handle.
86
+
87
+ | Input | Output |
88
+ |-------|--------|
89
+ | `s3://bucket/key` | `https://bucket.s3.<region>.amazonaws.com/key` — region auto-detected from host prefix (e.g. `us-west-2.opendata.source.coop`), or `us-east-1` as fallback |
90
+ | `gs://bucket/key` | `https://storage.googleapis.com/bucket/key` |
91
+ | `http(s)://...` | returned unchanged |
92
+ | Anything else | returned unchanged |
93
+
94
+ ### `getNativeScheme(provider)`
95
+
96
+ ```ts
97
+ function getNativeScheme(provider: string): string
98
+ ```
99
+
100
+ Map a provider ID to its canonical URI scheme prefix (first entry in the provider registry). Falls back to `'s3'` for unknown S3-compatible providers.
101
+
102
+ ### `safeDecodeURIComponent(s)`
103
+
104
+ ```ts
105
+ function safeDecodeURIComponent(s: string): string
106
+ ```
107
+
108
+ Percent-decode a URL component without throwing on malformed input. Returns the original string if decoding fails.
109
+
110
+ ## Provider registry
111
+
112
+ ### `ProviderId`
113
+
114
+ ```ts
115
+ type ProviderId =
116
+ | 's3' | 'gcs' | 'r2' | 'minio' | 'azure' | 'storj' | 'b2'
117
+ | 'digitalocean' | 'wasabi' | 'contabo' | 'hetzner' | 'linode' | 'ovhcloud';
118
+ ```
119
+
120
+ ### `ProviderDef`
121
+
122
+ ```ts
123
+ interface ProviderDef {
124
+ label: string; // "Amazon S3"
125
+ description: string; // short helper text for UI
126
+ authMethod: 'sigv4' | 'sas-token';
127
+ needsRegion: boolean;
128
+ needsEndpoint: boolean;
129
+ defaultRegion: string;
130
+ endpointTemplate: string | null; // may contain {region}
131
+ regions: ProviderRegion[];
132
+ bucketLabel?: string; // e.g. Azure uses "Container"
133
+ endpointPlaceholder: string;
134
+ schemes: string[]; // e.g. ['s3', 's3a', 's3n']
135
+ }
136
+
137
+ interface ProviderRegion {
138
+ code: string;
139
+ label: string;
140
+ }
141
+ ```
142
+
143
+ ### Registry exports
144
+
145
+ ```ts
146
+ const PROVIDERS: Record<ProviderId, ProviderDef>;
147
+ const PROVIDER_IDS: ProviderId[];
148
+ ```
149
+
150
+ `PROVIDER_IDS` is the display order used in the objex connection dialog.
151
+
152
+ ### Helpers
153
+
154
+ ```ts
155
+ function getProvider(id: string): ProviderDef;
156
+ function buildEndpointFromTemplate(id: ProviderId, region: string): string;
157
+ function resolveProviderEndpoint(provider: string, region?: string): string;
158
+ function buildProviderBaseUrl(
159
+ provider: ProviderId,
160
+ endpoint: string,
161
+ bucket: string,
162
+ region: string
163
+ ): string;
164
+ function isGcsProvider(provider: string, endpoint: string): boolean;
165
+ ```
166
+
167
+ | Function | Semantics |
168
+ |----------|-----------|
169
+ | `getProvider` | Unknown IDs fall back to the S3 entry (never throws). |
170
+ | `buildEndpointFromTemplate` | Substitute `{region}` in the provider's template. |
171
+ | `resolveProviderEndpoint` | Same as `buildEndpointFromTemplate` but accepts an untyped `provider` string and falls back to the provider's `defaultRegion` when `region` is omitted. Returns `''` for providers without a template (plain S3, MinIO). |
172
+ | `buildProviderBaseUrl` | Produce the HTTPS base URL for API requests (endpoint + bucket, correctly interleaved for virtual-host vs path-style). |
173
+ | `isGcsProvider` | `true` when the connection uses the GCS JSON API rather than S3 XML — used to pick adapter implementation. |
174
+
175
+ ### Access mode
176
+
177
+ ```ts
178
+ type AccessMode = 'public-https' | 'sas-https' | 'signed-s3';
179
+
180
+ function getAccessMode(conn: AccessModeInput): AccessMode;
181
+ function isPubliclyStreamable(conn: AccessModeInput): boolean;
182
+ ```
183
+
184
+ `AccessMode` is the single source of truth for how an HTTP client (DuckDB httpfs, MapLibre, fetch) should read the connection's files:
185
+
186
+ | Mode | Meaning |
187
+ |------|---------|
188
+ | `'public-https'` | Plain HTTPS, no signing — anonymous S3, GCS, R2, public MinIO, etc. |
189
+ | `'sas-https'` | HTTPS with SAS token appended to the URL — Azure. |
190
+ | `'signed-s3'` | Requires SigV4 signing — authenticated S3-compatible connections. |
191
+
192
+ `isPubliclyStreamable` is `true` for `'public-https'` and `'sas-https'` (anything a plain `fetch()` / `<img>` / `<video>` can reach directly).
193
+
194
+ ## StorageAdapter
195
+
196
+ ### Interface
197
+
198
+ ```ts
199
+ interface StorageAdapter {
200
+ list(path: string, signal?: AbortSignal): Promise<FileEntry[]>;
201
+ read(
202
+ path: string,
203
+ offset?: number,
204
+ length?: number,
205
+ signal?: AbortSignal
206
+ ): Promise<Uint8Array>;
207
+ head(path: string, signal?: AbortSignal): Promise<FileEntry>;
208
+ listPage?(
209
+ path: string,
210
+ continuationToken?: string,
211
+ pageSize?: number,
212
+ signal?: AbortSignal
213
+ ): Promise<ListPage>;
214
+ put(key: string, data: Uint8Array, contentType?: string): Promise<WriteResult>;
215
+ delete(key: string): Promise<void>;
216
+ deletePrefix(prefix: string): Promise<{ deleted: number }>;
217
+ copy(srcKey: string, destKey: string): Promise<WriteResult>;
218
+ readonly supportsWrite: boolean;
219
+ }
220
+
221
+ interface ListPage {
222
+ entries: FileEntry[];
223
+ continuationToken?: string;
224
+ hasMore: boolean;
225
+ }
226
+ ```
227
+
228
+ See [`types-constants.md`](./types-constants.md#fileentry) for `FileEntry` and `WriteResult`.
229
+
230
+ **Conventions**
231
+
232
+ - `signal` is propagated all the way to the underlying `fetch()`. Callers should always pass an `AbortController.signal` so tab switches / cleanups don't leak requests.
233
+ - `read(path, offset, length)` uses HTTP Range when supported; omitting offset/length reads the whole object.
234
+ - `listPage` is optional — read-heavy viewers (e.g. paginated browser) should feature-detect it.
235
+ - Read-only adapters should throw a native `Error` from write methods and set `supportsWrite = false`.
236
+
237
+ ### `UrlAdapter`
238
+
239
+ ```ts
240
+ class UrlAdapter implements StorageAdapter {
241
+ readonly supportsWrite = false;
242
+
243
+ read(url: string, offset?: number, length?: number, signal?: AbortSignal): Promise<Uint8Array>;
244
+ head(url: string, signal?: AbortSignal): Promise<FileEntry>;
245
+ list(): Promise<FileEntry[]>; // always []
246
+ put(): Promise<WriteResult>; // throws
247
+ delete(): Promise<void>; // throws
248
+ deletePrefix(): Promise<{ deleted: number }>; // throws
249
+ copy(): Promise<WriteResult>; // throws
250
+ }
251
+ ```
252
+
253
+ Minimal adapter for arbitrary HTTPS URLs (`tab.source === 'url'`). `path` is the full URL. Supports `read()` (Range requests) and `head()` only. Listing returns an empty array, writes throw. Use when you have a raw HTTPS link and do not need connection credentials.
@@ -0,0 +1,173 @@
1
+ # Core types & constants
2
+
3
+ Shared data shapes (connections, tabs, files) and package-wide constants.
4
+
5
+ Sources: `src/lib/types.ts`, `src/lib/constants.ts`.
6
+
7
+ ## Types
8
+
9
+ ### `FileEntry`
10
+
11
+ ```ts
12
+ interface FileEntry {
13
+ name: string;
14
+ path: string;
15
+ is_dir: boolean;
16
+ size: number;
17
+ modified: number; // unix timestamp in milliseconds
18
+ extension: string;
19
+ }
20
+ ```
21
+
22
+ A single file or directory entry returned by any `StorageAdapter.list` / `list_page` / `head`. `extension` is lowercase without the leading dot (empty string for directories and extensionless files).
23
+
24
+ ### `Connection`
25
+
26
+ ```ts
27
+ interface Connection {
28
+ id: string;
29
+ name: string;
30
+ provider: string; // same ID space as ProviderId
31
+ endpoint: string;
32
+ bucket: string;
33
+ region: string;
34
+ anonymous: boolean;
35
+ authMethod?: 'sigv4' | 'sas-token';
36
+ rootPrefix?: string;
37
+ }
38
+ ```
39
+
40
+ Persisted connection record. **No credentials** — secrets live only in the session `ConnectionConfig`.
41
+
42
+ ### `ConnectionConfig`
43
+
44
+ ```ts
45
+ interface ConnectionConfig {
46
+ name: string;
47
+ provider: string;
48
+ endpoint: string;
49
+ bucket: string;
50
+ region: string;
51
+ access_key?: string;
52
+ secret_key?: string;
53
+ sas_token?: string;
54
+ anonymous: boolean;
55
+ authMethod?: 'sigv4' | 'sas-token';
56
+ rootPrefix?: string;
57
+ }
58
+ ```
59
+
60
+ Transient form with credentials. Never persist this directly.
61
+
62
+ ### `Tab`
63
+
64
+ ```ts
65
+ interface Tab {
66
+ id: string;
67
+ name: string;
68
+ path: string;
69
+ source: 'remote' | 'url';
70
+ connectionId?: string;
71
+ extension: string; // lowercase, no leading dot
72
+ size?: number;
73
+ sourceRef?: string; // FROM-clause ref when reading from a catalog table
74
+ }
75
+ ```
76
+
77
+ Represents one open viewer tab. When `sourceRef` is set the tab reads from a DuckDB/DuckLake FROM expression rather than a file URL — file-only metadata paths (hyparquet prefetch, `parquet_kv_metadata`) are skipped.
78
+
79
+ ### `WriteResult`
80
+
81
+ ```ts
82
+ interface WriteResult {
83
+ key: string;
84
+ size: number;
85
+ e_tag?: string;
86
+ }
87
+ ```
88
+
89
+ Returned from every `StorageAdapter` write method (`put`, `copy`).
90
+
91
+ ### `Theme`
92
+
93
+ ```ts
94
+ type Theme = 'light' | 'dark' | 'system';
95
+ ```
96
+
97
+ ## Constants
98
+
99
+ ### `STORAGE_KEYS`
100
+
101
+ ```ts
102
+ const STORAGE_KEYS = {
103
+ SETTINGS: 'obstore-explore-settings',
104
+ CONNECTIONS: 'obstore-explore-connections',
105
+ QUERY_HISTORY: 'obstore-explore-query-history',
106
+ } as const;
107
+ ```
108
+
109
+ Namespace for localStorage keys. Always use these when persisting app state with [`persistToStorage`](./local-storage.md#persisttostorage-key-value) / [`loadFromStorage`](./local-storage.md#loadfromstoragetkey-defaultvalue).
110
+
111
+ ### `WGS84_CODES`
112
+
113
+ ```ts
114
+ const WGS84_CODES: Set<number>; // { 4326, 4979 }
115
+ ```
116
+
117
+ EPSG codes considered equivalent to WGS84. Use to short-circuit reprojection.
118
+
119
+ ### `DEFAULT_TARGET_CRS`
120
+
121
+ ```ts
122
+ const DEFAULT_TARGET_CRS = 'OGC:CRS84';
123
+ ```
124
+
125
+ Canonical target for DuckDB `ST_Transform`. OGC:CRS84 (longitude, latitude) matches GeoParquet 1.1+ and is functionally equivalent to EPSG:4326 under `geometry_always_xy = true`.
126
+
127
+ ### `DUCKDB_INIT_TIMEOUT_MS`
128
+
129
+ ```ts
130
+ const DUCKDB_INIT_TIMEOUT_MS = 30_000;
131
+ ```
132
+
133
+ Max milliseconds the DuckDB-WASM worker has to boot before the UI surfaces an error.
134
+
135
+ ### `MAX_QUERY_HISTORY_ENTRIES`
136
+
137
+ ```ts
138
+ const MAX_QUERY_HISTORY_ENTRIES = 200;
139
+ ```
140
+
141
+ LRU cap for persisted query history.
142
+
143
+ ### `SQL_PREVIEW_LENGTH`
144
+
145
+ ```ts
146
+ const SQL_PREVIEW_LENGTH = 120;
147
+ ```
148
+
149
+ Characters used for SQL previews in the query history list.
150
+
151
+ ### `VIEWER_DIR_EXTENSIONS`
152
+
153
+ ```ts
154
+ const VIEWER_DIR_EXTENSIONS: Set<string>; // { 'zarr', 'zr3' }
155
+ ```
156
+
157
+ Extensions that open as a viewer even though the path is a directory (Zarr stores).
158
+
159
+ ### `LAYER_HUE_MULTIPLIER`
160
+
161
+ ```ts
162
+ const LAYER_HUE_MULTIPLIER = 137;
163
+ ```
164
+
165
+ Golden-angle-based hue step (137° ≈ 360° × (1 − 1/φ)). Multiply by layer index to get visually distinct hues.
166
+
167
+ ### `COPY_FEEDBACK_MS`
168
+
169
+ ```ts
170
+ const COPY_FEEDBACK_MS = 2000;
171
+ ```
172
+
173
+ Duration of the "Copied!" confirmation state on copy-to-clipboard buttons.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkthru-earth/objex-utils",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Pure TypeScript utilities from objex — WKB parser, GeoArrow builder, storage URL parser, file type registry",
5
5
  "author": "Youssef Harby <yharby@walkthru.earth>",
6
6
  "license": "CC-BY-4.0",
@@ -15,13 +15,19 @@
15
15
  "types": "./dist/index.d.ts",
16
16
  "exports": {
17
17
  ".": {
18
- "types": "./dist/index.d.ts",
19
- "import": "./dist/index.js",
20
- "require": "./dist/index.cjs"
18
+ "import": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "require": {
23
+ "types": "./dist/index.d.cts",
24
+ "default": "./dist/index.cjs"
25
+ }
21
26
  }
22
27
  },
23
28
  "files": [
24
29
  "dist",
30
+ "docs",
25
31
  "README.md"
26
32
  ],
27
33
  "peerDependencies": {