@howells/stow-server 2.2.1 → 2.3.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.
- package/LICENSE +21 -0
- package/README.md +3 -5
- package/dist/index.d.mts +373 -56
- package/dist/index.d.ts +373 -56
- package/dist/index.js +303 -105
- package/dist/index.mjs +303 -105
- package/package.json +24 -24
package/dist/index.js
CHANGED
|
@@ -25,7 +25,10 @@ __export(index_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(index_exports);
|
|
27
27
|
var import_node_crypto = require("crypto");
|
|
28
|
+
var import_promises = require("timers/promises");
|
|
28
29
|
var import_zod = require("zod");
|
|
30
|
+
|
|
31
|
+
// src/stow-error.ts
|
|
29
32
|
var StowError = class extends Error {
|
|
30
33
|
status;
|
|
31
34
|
code;
|
|
@@ -36,6 +39,8 @@ var StowError = class extends Error {
|
|
|
36
39
|
this.code = code;
|
|
37
40
|
}
|
|
38
41
|
};
|
|
42
|
+
|
|
43
|
+
// src/index.ts
|
|
39
44
|
var fileColorSchema = import_zod.z.object({
|
|
40
45
|
position: import_zod.z.number().int(),
|
|
41
46
|
proportion: import_zod.z.number(),
|
|
@@ -194,10 +199,7 @@ var presignDedupeResultSchema = import_zod.z.object({
|
|
|
194
199
|
size: import_zod.z.number(),
|
|
195
200
|
contentType: import_zod.z.string()
|
|
196
201
|
});
|
|
197
|
-
var presignResultSchema = import_zod.z.union([
|
|
198
|
-
presignDedupeResultSchema,
|
|
199
|
-
presignNewResultSchema
|
|
200
|
-
]);
|
|
202
|
+
var presignResultSchema = import_zod.z.union([presignDedupeResultSchema, presignNewResultSchema]);
|
|
201
203
|
var confirmResultSchema = import_zod.z.object({
|
|
202
204
|
key: import_zod.z.string(),
|
|
203
205
|
url: import_zod.z.string().nullable(),
|
|
@@ -249,6 +251,42 @@ var profileResultSchema = import_zod.z.object({
|
|
|
249
251
|
createdAt: import_zod.z.string(),
|
|
250
252
|
updatedAt: import_zod.z.string()
|
|
251
253
|
});
|
|
254
|
+
var clusterGroupResultSchema = import_zod.z.object({
|
|
255
|
+
id: import_zod.z.string(),
|
|
256
|
+
index: import_zod.z.number().int(),
|
|
257
|
+
name: import_zod.z.string().nullable(),
|
|
258
|
+
description: import_zod.z.string().nullable(),
|
|
259
|
+
fileCount: import_zod.z.number().int(),
|
|
260
|
+
nameGeneratedAt: import_zod.z.string().nullable()
|
|
261
|
+
});
|
|
262
|
+
var clusterResourceResultSchema = import_zod.z.object({
|
|
263
|
+
id: import_zod.z.string(),
|
|
264
|
+
name: import_zod.z.string().nullable(),
|
|
265
|
+
clusterCount: import_zod.z.number().int(),
|
|
266
|
+
fileCount: import_zod.z.number().int(),
|
|
267
|
+
clusteredAt: import_zod.z.string().nullable(),
|
|
268
|
+
createdAt: import_zod.z.string(),
|
|
269
|
+
updatedAt: import_zod.z.string(),
|
|
270
|
+
clusters: import_zod.z.array(clusterGroupResultSchema)
|
|
271
|
+
});
|
|
272
|
+
var clusterFileResultSchema = import_zod.z.object({
|
|
273
|
+
id: import_zod.z.string(),
|
|
274
|
+
key: import_zod.z.string(),
|
|
275
|
+
bucketId: import_zod.z.string(),
|
|
276
|
+
originalFilename: import_zod.z.string().nullable(),
|
|
277
|
+
size: import_zod.z.number(),
|
|
278
|
+
contentType: import_zod.z.string(),
|
|
279
|
+
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).nullable(),
|
|
280
|
+
createdAt: import_zod.z.string(),
|
|
281
|
+
distance: import_zod.z.number().nullable()
|
|
282
|
+
});
|
|
283
|
+
var clusterFilesResultSchema = import_zod.z.object({
|
|
284
|
+
clusterId: import_zod.z.string(),
|
|
285
|
+
files: import_zod.z.array(clusterFileResultSchema),
|
|
286
|
+
limit: import_zod.z.number().int(),
|
|
287
|
+
offset: import_zod.z.number().int(),
|
|
288
|
+
total: import_zod.z.number().int()
|
|
289
|
+
});
|
|
252
290
|
var profileFilesResultSchema = import_zod.z.object({
|
|
253
291
|
id: import_zod.z.string(),
|
|
254
292
|
fileCount: import_zod.z.number()
|
|
@@ -296,12 +334,48 @@ var anchorResponseSchema = import_zod.z.object({
|
|
|
296
334
|
var anchorListResponseSchema = import_zod.z.object({
|
|
297
335
|
anchors: import_zod.z.array(anchorResponseSchema)
|
|
298
336
|
});
|
|
337
|
+
function sleep(ms) {
|
|
338
|
+
return (0, import_promises.setTimeout)(ms);
|
|
339
|
+
}
|
|
340
|
+
function buildTransformUrl(url, options) {
|
|
341
|
+
if (!(options && (options.width || options.height || options.quality || options.format))) {
|
|
342
|
+
return url;
|
|
343
|
+
}
|
|
344
|
+
const parsed = new URL(url);
|
|
345
|
+
if (options.width) {
|
|
346
|
+
parsed.searchParams.set("w", String(options.width));
|
|
347
|
+
}
|
|
348
|
+
if (options.height) {
|
|
349
|
+
parsed.searchParams.set("h", String(options.height));
|
|
350
|
+
}
|
|
351
|
+
if (options.quality) {
|
|
352
|
+
parsed.searchParams.set("q", String(options.quality));
|
|
353
|
+
}
|
|
354
|
+
if (options.format) {
|
|
355
|
+
parsed.searchParams.set("f", options.format);
|
|
356
|
+
}
|
|
357
|
+
return parsed.toString();
|
|
358
|
+
}
|
|
299
359
|
var StowServer = class {
|
|
300
360
|
apiKey;
|
|
301
361
|
baseUrl;
|
|
302
362
|
bucket;
|
|
303
363
|
timeout;
|
|
304
364
|
retries;
|
|
365
|
+
/**
|
|
366
|
+
* Pure helper for building signed transform URLs from an existing public file URL.
|
|
367
|
+
*
|
|
368
|
+
* This does not perform any network I/O and is safe to pass around as a plain
|
|
369
|
+
* function, for example to view-layer code that needs responsive image URLs.
|
|
370
|
+
*/
|
|
371
|
+
getTransformUrl;
|
|
372
|
+
/**
|
|
373
|
+
* Create a server SDK instance.
|
|
374
|
+
*
|
|
375
|
+
* Pass a bare string when you only need the API key and want default values
|
|
376
|
+
* for `baseUrl`, `timeout`, and retries. Pass an object when you want a
|
|
377
|
+
* default bucket, a non-production API origin, or custom transport settings.
|
|
378
|
+
*/
|
|
305
379
|
constructor(config) {
|
|
306
380
|
if (typeof config === "string") {
|
|
307
381
|
this.apiKey = config;
|
|
@@ -315,10 +389,9 @@ var StowServer = class {
|
|
|
315
389
|
this.timeout = config.timeout ?? 3e4;
|
|
316
390
|
this.retries = config.retries ?? 3;
|
|
317
391
|
}
|
|
392
|
+
this.getTransformUrl = buildTransformUrl;
|
|
318
393
|
}
|
|
319
|
-
/**
|
|
320
|
-
* Get the base URL for this instance (used by client SDK)
|
|
321
|
-
*/
|
|
394
|
+
/** Return the configured API origin, mainly for adapter packages such as `stow-next`. */
|
|
322
395
|
getBaseUrl() {
|
|
323
396
|
return this.baseUrl;
|
|
324
397
|
}
|
|
@@ -432,7 +505,7 @@ var StowServer = class {
|
|
|
432
505
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: retry + timeout + error normalization intentionally handled in one request pipeline
|
|
433
506
|
async request(path, options, schema) {
|
|
434
507
|
const maxAttempts = this.retries + 1;
|
|
435
|
-
for (let attempt = 0; attempt < maxAttempts; attempt
|
|
508
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
436
509
|
const controller = new AbortController();
|
|
437
510
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
438
511
|
if (options.signal) {
|
|
@@ -454,32 +527,28 @@ var StowServer = class {
|
|
|
454
527
|
const code = error.success ? error.data.code : void 0;
|
|
455
528
|
const isRetryable = response.status === 429 || response.status >= 500;
|
|
456
529
|
if (isRetryable && attempt < maxAttempts - 1) {
|
|
457
|
-
await
|
|
530
|
+
await sleep(1e3 * 2 ** attempt);
|
|
458
531
|
continue;
|
|
459
532
|
}
|
|
460
533
|
throw new StowError(message, response.status, code);
|
|
461
534
|
}
|
|
462
535
|
return schema ? schema.parse(data) : data;
|
|
463
|
-
} catch (
|
|
464
|
-
if (
|
|
465
|
-
throw
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (error instanceof StowError) {
|
|
538
|
+
throw error;
|
|
466
539
|
}
|
|
467
|
-
if (
|
|
468
|
-
throw new StowError(
|
|
469
|
-
"Invalid response format",
|
|
470
|
-
500,
|
|
471
|
-
"INVALID_RESPONSE"
|
|
472
|
-
);
|
|
540
|
+
if (error instanceof import_zod.z.ZodError) {
|
|
541
|
+
throw new StowError("Invalid response format", 500, "INVALID_RESPONSE");
|
|
473
542
|
}
|
|
474
|
-
if (
|
|
543
|
+
if (error instanceof DOMException || error instanceof Error && error.name === "AbortError") {
|
|
475
544
|
throw new StowError("Request timed out", 408, "TIMEOUT");
|
|
476
545
|
}
|
|
477
546
|
if (attempt < maxAttempts - 1) {
|
|
478
|
-
await
|
|
547
|
+
await sleep(1e3 * 2 ** attempt);
|
|
479
548
|
continue;
|
|
480
549
|
}
|
|
481
550
|
throw new StowError(
|
|
482
|
-
|
|
551
|
+
error instanceof Error ? error.message : "Network error",
|
|
483
552
|
0,
|
|
484
553
|
"NETWORK_ERROR"
|
|
485
554
|
);
|
|
@@ -489,11 +558,30 @@ var StowServer = class {
|
|
|
489
558
|
}
|
|
490
559
|
throw new StowError("Max retries exceeded", 0, "MAX_RETRIES");
|
|
491
560
|
}
|
|
492
|
-
sleep(ms) {
|
|
493
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
494
|
-
}
|
|
495
561
|
/**
|
|
496
|
-
* Upload
|
|
562
|
+
* Upload bytes from a trusted server environment.
|
|
563
|
+
*
|
|
564
|
+
* This is the highest-level server upload helper:
|
|
565
|
+
* 1. compute a SHA-256 hash for dedupe
|
|
566
|
+
* 2. request a presigned upload URL
|
|
567
|
+
* 3. PUT bytes to storage
|
|
568
|
+
* 4. confirm the upload with optional AI metadata generation
|
|
569
|
+
*
|
|
570
|
+
* Prefer this method when your code already has the file bytes in memory.
|
|
571
|
+
* Use `getPresignedUrl()` + `confirmUpload()` for direct browser uploads
|
|
572
|
+
* instead, and `uploadFromUrl()` when the source is an external URL.
|
|
573
|
+
*
|
|
574
|
+
* @example
|
|
575
|
+
* ```typescript
|
|
576
|
+
* await stow.uploadFile(buffer, {
|
|
577
|
+
* filename: "product.jpg",
|
|
578
|
+
* contentType: "image/jpeg",
|
|
579
|
+
* bucket: "catalog",
|
|
580
|
+
* metadata: { sku: "SKU-123" },
|
|
581
|
+
* title: true,
|
|
582
|
+
* altText: true,
|
|
583
|
+
* });
|
|
584
|
+
* ```
|
|
497
585
|
*/
|
|
498
586
|
async uploadFile(file, options) {
|
|
499
587
|
const filename = options?.filename || "file";
|
|
@@ -527,10 +615,7 @@ var StowServer = class {
|
|
|
527
615
|
throw new StowError("Failed to upload to storage", uploadRes.status);
|
|
528
616
|
}
|
|
529
617
|
return this.request(
|
|
530
|
-
this.withBucket(
|
|
531
|
-
presign.confirmUrl || "/presign/confirm",
|
|
532
|
-
options?.bucket
|
|
533
|
-
),
|
|
618
|
+
this.withBucket(presign.confirmUrl || "/presign/confirm", options?.bucket),
|
|
534
619
|
{
|
|
535
620
|
method: "POST",
|
|
536
621
|
headers: { "Content-Type": "application/json" },
|
|
@@ -550,7 +635,12 @@ var StowServer = class {
|
|
|
550
635
|
);
|
|
551
636
|
}
|
|
552
637
|
/**
|
|
553
|
-
*
|
|
638
|
+
* Import a remote asset by URL.
|
|
639
|
+
*
|
|
640
|
+
* Stow fetches the remote URL server-side, stores the resulting bytes, and
|
|
641
|
+
* persists the file as if it had been uploaded normally. This is useful for
|
|
642
|
+
* migrations, ingestion pipelines, or bringing third-party assets into Stow
|
|
643
|
+
* without downloading them into your own process first.
|
|
554
644
|
*/
|
|
555
645
|
async uploadFromUrl(url, filename, options) {
|
|
556
646
|
const result = await this.request(
|
|
@@ -585,7 +675,7 @@ var StowServer = class {
|
|
|
585
675
|
* The actual fetch, validation, and upload happen in a background worker.
|
|
586
676
|
* Use this for bulk imports where you don't need immediate confirmation.
|
|
587
677
|
*/
|
|
588
|
-
|
|
678
|
+
queueUploadFromUrl(url, filename, options) {
|
|
589
679
|
return this.request(
|
|
590
680
|
this.withBucket("/upload", options?.bucket),
|
|
591
681
|
{
|
|
@@ -606,24 +696,20 @@ var StowServer = class {
|
|
|
606
696
|
);
|
|
607
697
|
}
|
|
608
698
|
/**
|
|
609
|
-
* Get a presigned URL for direct client
|
|
699
|
+
* Get a presigned URL for a direct client upload.
|
|
700
|
+
*
|
|
701
|
+
* This is the server-side half of the browser upload flow used by
|
|
702
|
+
* `@howells/stow-client` and `@howells/stow-next`:
|
|
703
|
+
* 1. browser calls your app
|
|
704
|
+
* 2. your app calls `getPresignedUrl()`
|
|
705
|
+
* 3. browser PUTs bytes to `uploadUrl`
|
|
706
|
+
* 4. browser or your app calls `confirmUpload()`
|
|
610
707
|
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
* 2. Your endpoint calls this method
|
|
614
|
-
* 3. Client PUTs directly to the returned uploadUrl
|
|
615
|
-
* 4. Client calls confirmUpload to finalize
|
|
708
|
+
* If `contentHash` matches an existing file in the target bucket, the API
|
|
709
|
+
* short-circuits with `{ dedupe: true, ... }` and no upload is required.
|
|
616
710
|
*/
|
|
617
711
|
getPresignedUrl(request) {
|
|
618
|
-
const {
|
|
619
|
-
filename,
|
|
620
|
-
contentType,
|
|
621
|
-
size,
|
|
622
|
-
route,
|
|
623
|
-
bucket,
|
|
624
|
-
metadata,
|
|
625
|
-
contentHash
|
|
626
|
-
} = request;
|
|
712
|
+
const { filename, contentType, size, route, bucket, metadata, contentHash } = request;
|
|
627
713
|
return this.request(
|
|
628
714
|
this.withBucket("/presign", bucket),
|
|
629
715
|
{
|
|
@@ -642,8 +728,12 @@ var StowServer = class {
|
|
|
642
728
|
);
|
|
643
729
|
}
|
|
644
730
|
/**
|
|
645
|
-
* Confirm a
|
|
646
|
-
*
|
|
731
|
+
* Confirm a direct upload after the client has finished the storage PUT.
|
|
732
|
+
*
|
|
733
|
+
* This finalizes the file record in the database and optionally triggers
|
|
734
|
+
* post-processing such as AI-generated title/description/alt text.
|
|
735
|
+
*
|
|
736
|
+
* Call this exactly once per successful presigned upload.
|
|
647
737
|
*/
|
|
648
738
|
confirmUpload(request) {
|
|
649
739
|
const {
|
|
@@ -679,9 +769,13 @@ var StowServer = class {
|
|
|
679
769
|
);
|
|
680
770
|
}
|
|
681
771
|
/**
|
|
682
|
-
* List files in
|
|
772
|
+
* List files in a bucket with optional prefix filtering and enrichment blocks.
|
|
773
|
+
*
|
|
774
|
+
* Use `cursor` to continue pagination from a previous page. When requesting
|
|
775
|
+
* `include`, Stow expands those relationships inline so you can avoid follow-up
|
|
776
|
+
* per-file lookups.
|
|
683
777
|
*
|
|
684
|
-
* @param options.include
|
|
778
|
+
* @param options.include Optional enrichment fields: `"tags"`, `"taxonomies"`
|
|
685
779
|
*/
|
|
686
780
|
listFiles(options) {
|
|
687
781
|
const params = new URLSearchParams();
|
|
@@ -701,11 +795,7 @@ var StowServer = class {
|
|
|
701
795
|
params.set("include", options.include.join(","));
|
|
702
796
|
}
|
|
703
797
|
const path = `/files?${params}`;
|
|
704
|
-
return this.request(
|
|
705
|
-
this.withBucket(path, options?.bucket),
|
|
706
|
-
{ method: "GET" },
|
|
707
|
-
listFilesSchema
|
|
708
|
-
);
|
|
798
|
+
return this.request(this.withBucket(path, options?.bucket), { method: "GET" }, listFilesSchema);
|
|
709
799
|
}
|
|
710
800
|
/**
|
|
711
801
|
* Delete a file by key
|
|
@@ -728,9 +818,12 @@ var StowServer = class {
|
|
|
728
818
|
});
|
|
729
819
|
}
|
|
730
820
|
/**
|
|
731
|
-
* Get
|
|
821
|
+
* Get one file by key.
|
|
822
|
+
*
|
|
823
|
+
* This is the detailed file view and includes dimensions, embeddings status,
|
|
824
|
+
* extracted colors, and AI metadata fields when available.
|
|
732
825
|
*
|
|
733
|
-
* @param options.include
|
|
826
|
+
* @param options.include Optional enrichment fields: `"tags"`, `"taxonomies"`
|
|
734
827
|
*/
|
|
735
828
|
getFile(key, options) {
|
|
736
829
|
const params = new URLSearchParams();
|
|
@@ -838,35 +931,6 @@ var StowServer = class {
|
|
|
838
931
|
replaceResultSchema
|
|
839
932
|
);
|
|
840
933
|
}
|
|
841
|
-
/**
|
|
842
|
-
* Get a transform URL for an image.
|
|
843
|
-
*
|
|
844
|
-
* Appends transform query params (?w=, ?h=, ?q=, ?f=) to a file URL.
|
|
845
|
-
* Transforms are applied at the edge by the Cloudflare Worker — no
|
|
846
|
-
* server round-trip needed.
|
|
847
|
-
*
|
|
848
|
-
* @param url - Full file URL (e.g. from upload result's fileUrl)
|
|
849
|
-
* @param options - Transform options (width, height, quality, format)
|
|
850
|
-
*/
|
|
851
|
-
getTransformUrl(url, options) {
|
|
852
|
-
if (!(options && (options.width || options.height || options.quality || options.format))) {
|
|
853
|
-
return url;
|
|
854
|
-
}
|
|
855
|
-
const parsed = new URL(url);
|
|
856
|
-
if (options.width) {
|
|
857
|
-
parsed.searchParams.set("w", String(options.width));
|
|
858
|
-
}
|
|
859
|
-
if (options.height) {
|
|
860
|
-
parsed.searchParams.set("h", String(options.height));
|
|
861
|
-
}
|
|
862
|
-
if (options.quality) {
|
|
863
|
-
parsed.searchParams.set("q", String(options.quality));
|
|
864
|
-
}
|
|
865
|
-
if (options.format) {
|
|
866
|
-
parsed.searchParams.set("f", options.format);
|
|
867
|
-
}
|
|
868
|
-
return parsed.toString();
|
|
869
|
-
}
|
|
870
934
|
// ============================================================
|
|
871
935
|
// TAGS - Org-scoped labels for file organization
|
|
872
936
|
// ============================================================
|
|
@@ -944,7 +1008,34 @@ var StowServer = class {
|
|
|
944
1008
|
// SEARCH - Vector similarity search
|
|
945
1009
|
// ============================================================
|
|
946
1010
|
/**
|
|
947
|
-
*
|
|
1011
|
+
* Semantic search namespace.
|
|
1012
|
+
*
|
|
1013
|
+
* Methods:
|
|
1014
|
+
* - `text(...)` embeds text and finds matching files
|
|
1015
|
+
* - `similar(...)` finds nearest neighbors for a file, anchor, profile, or cluster
|
|
1016
|
+
* - `diverse(...)` balances similarity against result spread
|
|
1017
|
+
* - `color(...)` performs palette similarity search
|
|
1018
|
+
* - `image(...)` searches using an existing file or an external image URL
|
|
1019
|
+
*
|
|
1020
|
+
* Cluster-aware search examples:
|
|
1021
|
+
*
|
|
1022
|
+
* @example
|
|
1023
|
+
* ```typescript
|
|
1024
|
+
* const cluster = await stow.clusters.create({
|
|
1025
|
+
* bucket: "inspiration",
|
|
1026
|
+
* fileKeys,
|
|
1027
|
+
* clusterCount: 12,
|
|
1028
|
+
* });
|
|
1029
|
+
*
|
|
1030
|
+
* const firstGroup = cluster.clusters[0];
|
|
1031
|
+
* if (firstGroup) {
|
|
1032
|
+
* const related = await stow.search.similar({
|
|
1033
|
+
* bucket: "inspiration",
|
|
1034
|
+
* clusterId: firstGroup.id,
|
|
1035
|
+
* limit: 20,
|
|
1036
|
+
* });
|
|
1037
|
+
* }
|
|
1038
|
+
* ```
|
|
948
1039
|
*/
|
|
949
1040
|
get search() {
|
|
950
1041
|
return {
|
|
@@ -1116,7 +1207,11 @@ var StowServer = class {
|
|
|
1116
1207
|
// PROFILES - Taste/preference profiles from file collections
|
|
1117
1208
|
// ============================================================
|
|
1118
1209
|
/**
|
|
1119
|
-
*
|
|
1210
|
+
* Taste-profile namespace.
|
|
1211
|
+
*
|
|
1212
|
+
* Profiles are long-lived preference objects. They can be seeded from files,
|
|
1213
|
+
* updated through weighted signals, clustered into interpretable segments, and
|
|
1214
|
+
* then reused as semantic search seeds.
|
|
1120
1215
|
*/
|
|
1121
1216
|
get profiles() {
|
|
1122
1217
|
return {
|
|
@@ -1132,6 +1227,49 @@ var StowServer = class {
|
|
|
1132
1227
|
renameCluster: (profileId, clusterId, params, bucket) => this.renameProfileCluster(profileId, clusterId, params, bucket)
|
|
1133
1228
|
};
|
|
1134
1229
|
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Curated cluster namespace.
|
|
1232
|
+
*
|
|
1233
|
+
* Use this when you have an explicit file set that should be grouped by visual
|
|
1234
|
+
* similarity, but should not be modeled as a behavioral profile.
|
|
1235
|
+
*
|
|
1236
|
+
* Typical workflow:
|
|
1237
|
+
* 1. `create({ fileKeys, clusterCount })`
|
|
1238
|
+
* 2. poll `get(id)` until `clusteredAt` is non-null
|
|
1239
|
+
* 3. inspect `clusters`
|
|
1240
|
+
* 4. fetch representative files with `files(id, clusterId)`
|
|
1241
|
+
* 5. optionally `renameCluster(...)`
|
|
1242
|
+
* 6. use `clusterId` with `search.similar(...)` or `search.diverse(...)`
|
|
1243
|
+
*
|
|
1244
|
+
* @example
|
|
1245
|
+
* ```typescript
|
|
1246
|
+
* const resource = await stow.clusters.create({
|
|
1247
|
+
* bucket: "featured-products",
|
|
1248
|
+
* fileKeys: featuredKeys,
|
|
1249
|
+
* clusterCount: 12,
|
|
1250
|
+
* name: "Navigator groups",
|
|
1251
|
+
* });
|
|
1252
|
+
*
|
|
1253
|
+
* const latest = await stow.clusters.get(resource.id, "featured-products");
|
|
1254
|
+
* const group = latest.clusters[0];
|
|
1255
|
+
* if (group) {
|
|
1256
|
+
* const representatives = await stow.clusters.files(resource.id, group.id, {
|
|
1257
|
+
* limit: 12,
|
|
1258
|
+
* offset: 0,
|
|
1259
|
+
* });
|
|
1260
|
+
* }
|
|
1261
|
+
* ```
|
|
1262
|
+
*/
|
|
1263
|
+
get clusters() {
|
|
1264
|
+
return {
|
|
1265
|
+
create: (params) => this.createClustersResource(params),
|
|
1266
|
+
get: (id, bucket) => this.getClustersResource(id, bucket),
|
|
1267
|
+
recluster: (id, params, bucket) => this.reclusterClustersResource(id, params, bucket),
|
|
1268
|
+
files: (id, clusterId, params, bucket) => this.getClusterFiles(id, clusterId, params, bucket),
|
|
1269
|
+
renameCluster: (id, clusterId, params, bucket) => this.renameClusterGroup(id, clusterId, params, bucket),
|
|
1270
|
+
delete: (id, bucket) => this.deleteClustersResource(id, bucket)
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1135
1273
|
createProfile(params) {
|
|
1136
1274
|
return this.request(
|
|
1137
1275
|
this.withBucket("/profiles", params?.bucket),
|
|
@@ -1154,10 +1292,9 @@ var StowServer = class {
|
|
|
1154
1292
|
);
|
|
1155
1293
|
}
|
|
1156
1294
|
async deleteProfile(id, bucket) {
|
|
1157
|
-
await this.request(
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
);
|
|
1295
|
+
await this.request(this.withBucket(`/profiles/${encodeURIComponent(id)}`, bucket), {
|
|
1296
|
+
method: "DELETE"
|
|
1297
|
+
});
|
|
1161
1298
|
}
|
|
1162
1299
|
addProfileFiles(id, fileKeys, bucket) {
|
|
1163
1300
|
return this.request(
|
|
@@ -1204,36 +1341,98 @@ var StowServer = class {
|
|
|
1204
1341
|
);
|
|
1205
1342
|
}
|
|
1206
1343
|
getProfileClusters(id, bucket) {
|
|
1344
|
+
return this.request(this.withBucket(`/profiles/${encodeURIComponent(id)}/clusters`, bucket), {
|
|
1345
|
+
method: "GET"
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
reclusterProfile(id, params, bucket) {
|
|
1349
|
+
return this.request(this.withBucket(`/profiles/${encodeURIComponent(id)}/clusters`, bucket), {
|
|
1350
|
+
method: "POST",
|
|
1351
|
+
headers: { "Content-Type": "application/json" },
|
|
1352
|
+
body: JSON.stringify(
|
|
1353
|
+
params?.clusterCount === void 0 ? {} : { clusterCount: params.clusterCount }
|
|
1354
|
+
)
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
renameProfileCluster(profileId, clusterId, params, bucket) {
|
|
1207
1358
|
return this.request(
|
|
1208
|
-
this.withBucket(
|
|
1209
|
-
|
|
1359
|
+
this.withBucket(
|
|
1360
|
+
`/profiles/${encodeURIComponent(profileId)}/clusters/${encodeURIComponent(clusterId)}`,
|
|
1361
|
+
bucket
|
|
1362
|
+
),
|
|
1363
|
+
{
|
|
1364
|
+
method: "PUT",
|
|
1365
|
+
headers: { "Content-Type": "application/json" },
|
|
1366
|
+
body: JSON.stringify(params)
|
|
1367
|
+
}
|
|
1210
1368
|
);
|
|
1211
1369
|
}
|
|
1212
|
-
|
|
1370
|
+
createClustersResource(params) {
|
|
1213
1371
|
return this.request(
|
|
1214
|
-
this.withBucket(
|
|
1372
|
+
this.withBucket("/clusters", params.bucket),
|
|
1215
1373
|
{
|
|
1216
1374
|
method: "POST",
|
|
1217
1375
|
headers: { "Content-Type": "application/json" },
|
|
1218
1376
|
body: JSON.stringify({
|
|
1219
|
-
|
|
1377
|
+
fileKeys: params.fileKeys,
|
|
1378
|
+
...params.clusterCount === void 0 ? {} : { clusterCount: params.clusterCount },
|
|
1379
|
+
...params.name ? { name: params.name } : {}
|
|
1220
1380
|
})
|
|
1221
|
-
}
|
|
1381
|
+
},
|
|
1382
|
+
clusterResourceResultSchema
|
|
1222
1383
|
);
|
|
1223
1384
|
}
|
|
1224
|
-
|
|
1385
|
+
getClustersResource(id, bucket) {
|
|
1386
|
+
return this.request(
|
|
1387
|
+
this.withBucket(`/clusters/${encodeURIComponent(id)}`, bucket),
|
|
1388
|
+
{ method: "GET" },
|
|
1389
|
+
clusterResourceResultSchema
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
reclusterClustersResource(id, params, bucket) {
|
|
1393
|
+
return this.request(
|
|
1394
|
+
this.withBucket(`/clusters/${encodeURIComponent(id)}/recluster`, bucket),
|
|
1395
|
+
{
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
headers: { "Content-Type": "application/json" },
|
|
1398
|
+
body: JSON.stringify(
|
|
1399
|
+
params?.clusterCount === void 0 ? {} : { clusterCount: params.clusterCount }
|
|
1400
|
+
)
|
|
1401
|
+
},
|
|
1402
|
+
clusterResourceResultSchema
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
getClusterFiles(id, clusterId, params, bucket) {
|
|
1406
|
+
const searchParams = new URLSearchParams();
|
|
1407
|
+
if (params?.limit !== void 0) {
|
|
1408
|
+
searchParams.set("limit", String(params.limit));
|
|
1409
|
+
}
|
|
1410
|
+
if (params?.offset !== void 0) {
|
|
1411
|
+
searchParams.set("offset", String(params.offset));
|
|
1412
|
+
}
|
|
1413
|
+
const qs = searchParams.toString();
|
|
1414
|
+
const path = `/clusters/${encodeURIComponent(id)}/clusters/${encodeURIComponent(clusterId)}/files${qs ? `?${qs}` : ""}`;
|
|
1415
|
+
return this.request(this.withBucket(path, bucket), { method: "GET" }, clusterFilesResultSchema);
|
|
1416
|
+
}
|
|
1417
|
+
renameClusterGroup(id, clusterId, params, bucket) {
|
|
1225
1418
|
return this.request(
|
|
1226
1419
|
this.withBucket(
|
|
1227
|
-
`/
|
|
1420
|
+
`/clusters/${encodeURIComponent(id)}/clusters/${encodeURIComponent(clusterId)}`,
|
|
1228
1421
|
bucket
|
|
1229
1422
|
),
|
|
1230
1423
|
{
|
|
1231
1424
|
method: "PUT",
|
|
1232
1425
|
headers: { "Content-Type": "application/json" },
|
|
1233
1426
|
body: JSON.stringify(params)
|
|
1234
|
-
}
|
|
1427
|
+
},
|
|
1428
|
+
clusterGroupResultSchema
|
|
1235
1429
|
);
|
|
1236
1430
|
}
|
|
1431
|
+
async deleteClustersResource(id, bucket) {
|
|
1432
|
+
await this.request(this.withBucket(`/clusters/${encodeURIComponent(id)}`, bucket), {
|
|
1433
|
+
method: "DELETE"
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1237
1436
|
// ============================================================
|
|
1238
1437
|
// ANCHORS - Named semantic reference points in vector space
|
|
1239
1438
|
// ============================================================
|
|
@@ -1295,10 +1494,9 @@ var StowServer = class {
|
|
|
1295
1494
|
);
|
|
1296
1495
|
}
|
|
1297
1496
|
async deleteAnchor(id, options) {
|
|
1298
|
-
await this.request(
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
);
|
|
1497
|
+
await this.request(this.withBucket(`/anchors/${encodeURIComponent(id)}`, options?.bucket), {
|
|
1498
|
+
method: "DELETE"
|
|
1499
|
+
});
|
|
1302
1500
|
}
|
|
1303
1501
|
};
|
|
1304
1502
|
// Annotate the CommonJS export names for ESM import in node:
|