@dreaminhex/hexdb 1.0.0 → 1.5.0
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/dist/index.d.ts +83 -2
- package/dist/index.js +169 -0
- package/package.json +47 -47
package/dist/index.d.ts
CHANGED
|
@@ -79,6 +79,10 @@ export declare class HexDB {
|
|
|
79
79
|
constructor(options: Options);
|
|
80
80
|
/** Send any request to the REST API and return the parsed JSON (undefined for 204). */
|
|
81
81
|
request<T = unknown>(method: Method, path: string, body?: unknown, headers?: Record<string, string>, timeoutMs?: number): Promise<T>;
|
|
82
|
+
/** Send raw bytes (or nothing) and get the raw response; throws HexDBError on an error answer. */
|
|
83
|
+
requestRaw(method: Method, path: string, body?: BodyInit, headers?: Record<string, string>, timeoutMs?: number): Promise<Response>;
|
|
84
|
+
/** A files tessellation (photos, audio, video, documents: any binary file). */
|
|
85
|
+
files(name: string): Files;
|
|
82
86
|
/** Sign in with a login and password (and a one-time code with MFA); later requests use the session. */
|
|
83
87
|
login(login: string, password: string, code?: string): Promise<{
|
|
84
88
|
user: unknown;
|
|
@@ -131,10 +135,10 @@ export declare class HexDB {
|
|
|
131
135
|
runFunction<T = unknown>(name: string, params?: Record<string, unknown>): Promise<T>;
|
|
132
136
|
}
|
|
133
137
|
export declare class Tessellation {
|
|
134
|
-
|
|
138
|
+
protected readonly db: HexDB;
|
|
135
139
|
readonly name: string;
|
|
136
140
|
constructor(db: HexDB, name: string);
|
|
137
|
-
|
|
141
|
+
protected get path(): string;
|
|
138
142
|
/** Insert a document; returns it with its new `id`. */
|
|
139
143
|
insert(doc: Record<string, unknown>, options?: WriteOptions): Promise<Document>;
|
|
140
144
|
/** Insert up to 1000 documents atomically; returns their IDs in order. */
|
|
@@ -208,3 +212,80 @@ export declare class Stream {
|
|
|
208
212
|
}): Promise<void>;
|
|
209
213
|
}
|
|
210
214
|
export default HexDB;
|
|
215
|
+
/** A file's document: its metadata. */
|
|
216
|
+
export type FileDocument = Document & {
|
|
217
|
+
name: string;
|
|
218
|
+
path: string;
|
|
219
|
+
size: number;
|
|
220
|
+
content_type: string;
|
|
221
|
+
sha256: string;
|
|
222
|
+
version: number;
|
|
223
|
+
meta?: Record<string, unknown>;
|
|
224
|
+
extract?: {
|
|
225
|
+
status: "pending" | "done";
|
|
226
|
+
extractors?: string[];
|
|
227
|
+
errors?: string[];
|
|
228
|
+
};
|
|
229
|
+
thumbnail?: {
|
|
230
|
+
width: number;
|
|
231
|
+
height: number;
|
|
232
|
+
content_type: string;
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
export interface UploadOptions {
|
|
236
|
+
contentType?: string;
|
|
237
|
+
/** When the file was last modified (RFC 3339 or epoch milliseconds). */
|
|
238
|
+
modified?: string | number;
|
|
239
|
+
/** Bytes sent so far, for progress. */
|
|
240
|
+
onProgress?: (sent: number) => void;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* A files tessellation: each file's metadata is a document (query it like
|
|
244
|
+
* any tessellation); its content is uploaded and downloaded here. Paths are
|
|
245
|
+
* `folder/name`. Uploading to a path that has a file makes a new version.
|
|
246
|
+
*/
|
|
247
|
+
export declare class Files extends Tessellation {
|
|
248
|
+
/** Create the files tessellation (no error if it exists). */
|
|
249
|
+
create(): Promise<{
|
|
250
|
+
name: string;
|
|
251
|
+
kind: string;
|
|
252
|
+
}>;
|
|
253
|
+
/** Upload bytes or a Blob to `path`. Files of 16 MB and more go in resumable parts. */
|
|
254
|
+
upload(path: string, data: Uint8Array | ArrayBuffer | Blob, options?: UploadOptions): Promise<FileDocument>;
|
|
255
|
+
private contentPath;
|
|
256
|
+
/** A file's content (by `folder/name` or ID), or an earlier version's. */
|
|
257
|
+
download(pathOrId: string, options?: {
|
|
258
|
+
version?: number;
|
|
259
|
+
}): Promise<Uint8Array>;
|
|
260
|
+
/** A file's content as a stream (for large files). */
|
|
261
|
+
downloadStream(pathOrId: string, options?: {
|
|
262
|
+
version?: number;
|
|
263
|
+
}): Promise<ReadableStream<Uint8Array>>;
|
|
264
|
+
/** Bytes `start` to `end` (exclusive) of a file. */
|
|
265
|
+
readRange(pathOrId: string, start: number, end: number): Promise<Uint8Array>;
|
|
266
|
+
/** A file's document (metadata), or null. */
|
|
267
|
+
stat(pathOrId: string): Promise<FileDocument | null>;
|
|
268
|
+
/** The files directly in a folder, by name. */
|
|
269
|
+
list(folder?: string, limit?: number): Promise<FileDocument[]>;
|
|
270
|
+
/** Files whose name, folders, tags or extracted metadata contain every word of `text`. */
|
|
271
|
+
search(text: string, filter?: Record<string, unknown>, limit?: number): Promise<FileDocument[]>;
|
|
272
|
+
/** A small JPEG or PNG of an image (once its metadata has been read). */
|
|
273
|
+
thumbnail(id: string): Promise<Uint8Array>;
|
|
274
|
+
versions(id: string): Promise<{
|
|
275
|
+
id: string;
|
|
276
|
+
current: Record<string, unknown>;
|
|
277
|
+
versions: Record<string, unknown>[];
|
|
278
|
+
}>;
|
|
279
|
+
/** Make an earlier version current again (as a new version). */
|
|
280
|
+
restoreVersion(id: string, version: number): Promise<FileDocument>;
|
|
281
|
+
/** Deleted files (`delete` moves a file to the trash). */
|
|
282
|
+
trash(): Promise<FileDocument[]>;
|
|
283
|
+
restore(id: string, options?: {
|
|
284
|
+
name?: string;
|
|
285
|
+
path?: string;
|
|
286
|
+
}): Promise<FileDocument>;
|
|
287
|
+
/** Delete a trashed file for good. */
|
|
288
|
+
purge(id: string): Promise<void>;
|
|
289
|
+
/** Read metadata again (one file, or all); returns how many were queued. */
|
|
290
|
+
reextract(id?: string): Promise<number>;
|
|
291
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
// const orders = db.tessellation("orders")
|
|
7
7
|
// const { id } = await orders.insert({ customer: "ada", total: 12 })
|
|
8
8
|
// const page = await orders.query({ filter: { total: { $gt: 10 } }, sort: "-total" })
|
|
9
|
+
//
|
|
10
|
+
// const media = db.files("media") // a files tessellation
|
|
11
|
+
// await media.upload("photos/IMG_1.jpg", bytes)
|
|
12
|
+
// const hits = await media.search("glacier") // names, tags and extracted metadata
|
|
9
13
|
/** An error answer from HexDB: `status` is the HTTP status, `code` HexDB's error code. */
|
|
10
14
|
export class HexDBError extends Error {
|
|
11
15
|
status;
|
|
@@ -76,6 +80,36 @@ export class HexDB {
|
|
|
76
80
|
throw new HexDBError(response.status, error?.code ?? "error", error?.message ?? (text || response.statusText), retryAfter);
|
|
77
81
|
}
|
|
78
82
|
}
|
|
83
|
+
/** Send raw bytes (or nothing) and get the raw response; throws HexDBError on an error answer. */
|
|
84
|
+
async requestRaw(method, path, body, headers = {}, timeoutMs = 600_000) {
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
87
|
+
let response;
|
|
88
|
+
try {
|
|
89
|
+
response = await this.fetcher(this.base + path, {
|
|
90
|
+
method,
|
|
91
|
+
headers: { ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), ...headers },
|
|
92
|
+
body,
|
|
93
|
+
signal: controller.signal,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
throw new HexDBError(0, "unreachable", `Couldn't reach HexDB at ${this.base}: ${e instanceof Error ? e.message : String(e)}`);
|
|
99
|
+
}
|
|
100
|
+
// The body may still be streaming; the timer covers the headers only.
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const text = await response.text();
|
|
104
|
+
const error = safeJson(text)?.error;
|
|
105
|
+
throw new HexDBError(response.status, error?.code ?? "error", error?.message ?? (text || response.statusText));
|
|
106
|
+
}
|
|
107
|
+
return response;
|
|
108
|
+
}
|
|
109
|
+
/** A files tessellation (photos, audio, video, documents: any binary file). */
|
|
110
|
+
files(name) {
|
|
111
|
+
return new Files(this, name);
|
|
112
|
+
}
|
|
79
113
|
/** Sign in with a login and password (and a one-time code with MFA); later requests use the session. */
|
|
80
114
|
async login(login, password, code) {
|
|
81
115
|
const result = await this.request("POST", "/auth/login", { login, password, code, return_token: true });
|
|
@@ -284,3 +318,138 @@ export class Stream {
|
|
|
284
318
|
}
|
|
285
319
|
}
|
|
286
320
|
export default HexDB;
|
|
321
|
+
const RESUMABLE_FROM = 16 * 1024 * 1024;
|
|
322
|
+
const PART = 8 * 1024 * 1024;
|
|
323
|
+
const ULID = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
324
|
+
function filePath(path) {
|
|
325
|
+
return path
|
|
326
|
+
.split(/[\\/]/)
|
|
327
|
+
.filter(Boolean)
|
|
328
|
+
.map(enc)
|
|
329
|
+
.join("/");
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* A files tessellation: each file's metadata is a document (query it like
|
|
333
|
+
* any tessellation); its content is uploaded and downloaded here. Paths are
|
|
334
|
+
* `folder/name`. Uploading to a path that has a file makes a new version.
|
|
335
|
+
*/
|
|
336
|
+
export class Files extends Tessellation {
|
|
337
|
+
/** Create the files tessellation (no error if it exists). */
|
|
338
|
+
async create() {
|
|
339
|
+
try {
|
|
340
|
+
return await this.db.request("POST", "/tessellations", { name: this.name, kind: "files" });
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
if (e instanceof HexDBError && e.status === 409)
|
|
344
|
+
return this.db.request("GET", `/tessellations/${enc(this.name)}`);
|
|
345
|
+
throw e;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** Upload bytes or a Blob to `path`. Files of 16 MB and more go in resumable parts. */
|
|
349
|
+
async upload(path, data, options = {}) {
|
|
350
|
+
const blob = data instanceof Blob ? data : new Blob([data]);
|
|
351
|
+
const modified = options.modified === undefined ? undefined : String(options.modified);
|
|
352
|
+
if (blob.size < RESUMABLE_FROM) {
|
|
353
|
+
const query = modified ? `?modified=${enc(modified)}` : "";
|
|
354
|
+
const response = await this.db.requestRaw("PUT", `${this.path}/_files/${filePath(path)}${query}`, blob, options.contentType ? { "Content-Type": options.contentType } : {});
|
|
355
|
+
options.onProgress?.(blob.size);
|
|
356
|
+
return (await response.json());
|
|
357
|
+
}
|
|
358
|
+
const session = await this.db.request("POST", `${this.path}/_uploads`, { path, size: blob.size, content_type: options.contentType, modified });
|
|
359
|
+
const url = `${this.path}/_uploads/${enc(session.id)}`;
|
|
360
|
+
for (let offset = 0; offset < blob.size;) {
|
|
361
|
+
const end = Math.min(offset + PART, blob.size);
|
|
362
|
+
for (let attempt = 0;; attempt++) {
|
|
363
|
+
try {
|
|
364
|
+
await this.db.requestRaw("PATCH", `${url}?offset=${offset}`, blob.slice(offset, end));
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
if (attempt >= 3)
|
|
369
|
+
throw e;
|
|
370
|
+
await sleep(1000 * (attempt + 1));
|
|
371
|
+
const status = await this.db.request("GET", url);
|
|
372
|
+
if (status.offset >= end)
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
offset = end;
|
|
377
|
+
options.onProgress?.(offset);
|
|
378
|
+
}
|
|
379
|
+
return this.db.request("POST", `${url}/complete`, undefined, {}, 600_000);
|
|
380
|
+
}
|
|
381
|
+
contentPath(pathOrId, version) {
|
|
382
|
+
const query = version === undefined ? "" : `?version=${version}`;
|
|
383
|
+
return ULID.test(pathOrId) ? `${this.path}/${enc(pathOrId)}/content${query}` : `${this.path}/_files/${filePath(pathOrId)}${query}`;
|
|
384
|
+
}
|
|
385
|
+
/** A file's content (by `folder/name` or ID), or an earlier version's. */
|
|
386
|
+
async download(pathOrId, options = {}) {
|
|
387
|
+
const response = await this.db.requestRaw("GET", this.contentPath(pathOrId, options.version));
|
|
388
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
389
|
+
}
|
|
390
|
+
/** A file's content as a stream (for large files). */
|
|
391
|
+
async downloadStream(pathOrId, options = {}) {
|
|
392
|
+
const response = await this.db.requestRaw("GET", this.contentPath(pathOrId, options.version));
|
|
393
|
+
if (!response.body)
|
|
394
|
+
throw new HexDBError(0, "empty", "The response had no body.");
|
|
395
|
+
return response.body;
|
|
396
|
+
}
|
|
397
|
+
/** Bytes `start` to `end` (exclusive) of a file. */
|
|
398
|
+
async readRange(pathOrId, start, end) {
|
|
399
|
+
const response = await this.db.requestRaw("GET", this.contentPath(pathOrId), undefined, { Range: `bytes=${start}-${end - 1}` });
|
|
400
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
401
|
+
}
|
|
402
|
+
/** A file's document (metadata), or null. */
|
|
403
|
+
async stat(pathOrId) {
|
|
404
|
+
if (ULID.test(pathOrId))
|
|
405
|
+
return (await this.get(pathOrId));
|
|
406
|
+
const parts = pathOrId.split(/[\\/]/).filter(Boolean);
|
|
407
|
+
const name = parts.pop() ?? "";
|
|
408
|
+
const folder = parts.length ? `/${parts.join("/")}/` : "/";
|
|
409
|
+
const page = await this.query({ filter: { path: folder, name }, limit: 1 });
|
|
410
|
+
return page.documents[0] ?? null;
|
|
411
|
+
}
|
|
412
|
+
/** The files directly in a folder, by name. */
|
|
413
|
+
async list(folder = "/", limit = 1000) {
|
|
414
|
+
const parts = folder.split(/[\\/]/).filter(Boolean);
|
|
415
|
+
const path = parts.length ? `/${parts.join("/")}/` : "/";
|
|
416
|
+
return (await this.query({ filter: { path }, sort: "name", limit })).documents;
|
|
417
|
+
}
|
|
418
|
+
/** Files whose name, folders, tags or extracted metadata contain every word of `text`. */
|
|
419
|
+
async search(text, filter = {}, limit = 100) {
|
|
420
|
+
return (await this.query({ filter: { $text: text, ...filter }, limit })).documents;
|
|
421
|
+
}
|
|
422
|
+
/** A small JPEG or PNG of an image (once its metadata has been read). */
|
|
423
|
+
async thumbnail(id) {
|
|
424
|
+
const response = await this.db.requestRaw("GET", `${this.path}/${enc(id)}/thumbnail`);
|
|
425
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
426
|
+
}
|
|
427
|
+
versions(id) {
|
|
428
|
+
return this.db.request("GET", `${this.path}/${enc(id)}/versions`);
|
|
429
|
+
}
|
|
430
|
+
/** Make an earlier version current again (as a new version). */
|
|
431
|
+
restoreVersion(id, version) {
|
|
432
|
+
return this.db.request("POST", `${this.path}/${enc(id)}/versions/${version}/restore`);
|
|
433
|
+
}
|
|
434
|
+
/** Deleted files (`delete` moves a file to the trash). */
|
|
435
|
+
async trash() {
|
|
436
|
+
return (await this.db.request("GET", `${this.path}/_trash`)).documents;
|
|
437
|
+
}
|
|
438
|
+
restore(id, options = {}) {
|
|
439
|
+
const params = new URLSearchParams();
|
|
440
|
+
if (options.name)
|
|
441
|
+
params.set("name", options.name);
|
|
442
|
+
if (options.path)
|
|
443
|
+
params.set("path", options.path);
|
|
444
|
+
const query = params.toString();
|
|
445
|
+
return this.db.request("POST", `${this.path}/_trash/${enc(id)}/restore${query ? `?${query}` : ""}`);
|
|
446
|
+
}
|
|
447
|
+
/** Delete a trashed file for good. */
|
|
448
|
+
async purge(id) {
|
|
449
|
+
await this.db.request("DELETE", `${this.path}/_trash/${enc(id)}`);
|
|
450
|
+
}
|
|
451
|
+
/** Read metadata again (one file, or all); returns how many were queued. */
|
|
452
|
+
async reextract(id) {
|
|
453
|
+
return (await this.db.request("POST", `${this.path}/_reextract${id ? `?id=${enc(id)}` : ""}`)).queued;
|
|
454
|
+
}
|
|
455
|
+
}
|
package/package.json
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@dreaminhex/hexdb",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Client for HexDB, the hexagonal document database.",
|
|
5
|
-
"license": "Apache-2.0",
|
|
6
|
-
"author": "Dream In Hex",
|
|
7
|
-
"homepage": "https://hexdb-website.vercel.app",
|
|
8
|
-
"repository": {
|
|
9
|
-
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/dreaminhex/hexdb.git",
|
|
11
|
-
"directory": "drivers/node"
|
|
12
|
-
},
|
|
13
|
-
"bugs": {
|
|
14
|
-
"url": "https://github.com/dreaminhex/hexdb/issues"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"hexdb",
|
|
18
|
-
"database",
|
|
19
|
-
"document",
|
|
20
|
-
"nosql",
|
|
21
|
-
"client"
|
|
22
|
-
],
|
|
23
|
-
"type": "module",
|
|
24
|
-
"main": "dist/index.js",
|
|
25
|
-
"types": "dist/index.d.ts",
|
|
26
|
-
"exports": {
|
|
27
|
-
".": {
|
|
28
|
-
"types": "./dist/index.d.ts",
|
|
29
|
-
"import": "./dist/index.js"
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"files": [
|
|
33
|
-
"dist",
|
|
34
|
-
"README.md"
|
|
35
|
-
],
|
|
36
|
-
"engines": {
|
|
37
|
-
"node": ">=18"
|
|
38
|
-
},
|
|
39
|
-
"scripts": {
|
|
40
|
-
"build": "tsc -p tsconfig.json",
|
|
41
|
-
"test": "npm run build && node --test test/client.test.mjs"
|
|
42
|
-
},
|
|
43
|
-
"devDependencies": {
|
|
44
|
-
"typescript": "~5.9.3",
|
|
45
|
-
"@types/node": "^22.15.18"
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@dreaminhex/hexdb",
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "Client for HexDB, the hexagonal document database.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Dream In Hex",
|
|
7
|
+
"homepage": "https://hexdb-website.vercel.app",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/dreaminhex/hexdb.git",
|
|
11
|
+
"directory": "drivers/node"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/dreaminhex/hexdb/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"hexdb",
|
|
18
|
+
"database",
|
|
19
|
+
"document",
|
|
20
|
+
"nosql",
|
|
21
|
+
"client"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "dist/index.js",
|
|
25
|
+
"types": "dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"test": "npm run build && node --test test/client.test.mjs"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "~5.9.3",
|
|
45
|
+
"@types/node": "^22.15.18"
|
|
46
|
+
}
|
|
47
|
+
}
|