@howells/stow-server 2.2.0 → 2.3.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/README.md +3 -5
- package/dist/index.d.mts +373 -56
- package/dist/index.d.ts +373 -56
- package/dist/index.js +307 -108
- package/dist/index.mjs +307 -108
- package/package.json +17 -17
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
|
+
import { setTimeout as delay } from "timers/promises";
|
|
3
4
|
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
// src/stow-error.ts
|
|
4
7
|
var StowError = class extends Error {
|
|
5
8
|
status;
|
|
6
9
|
code;
|
|
@@ -11,6 +14,8 @@ var StowError = class extends Error {
|
|
|
11
14
|
this.code = code;
|
|
12
15
|
}
|
|
13
16
|
};
|
|
17
|
+
|
|
18
|
+
// src/index.ts
|
|
14
19
|
var fileColorSchema = z.object({
|
|
15
20
|
position: z.number().int(),
|
|
16
21
|
proportion: z.number(),
|
|
@@ -169,10 +174,7 @@ var presignDedupeResultSchema = z.object({
|
|
|
169
174
|
size: z.number(),
|
|
170
175
|
contentType: z.string()
|
|
171
176
|
});
|
|
172
|
-
var presignResultSchema = z.union([
|
|
173
|
-
presignDedupeResultSchema,
|
|
174
|
-
presignNewResultSchema
|
|
175
|
-
]);
|
|
177
|
+
var presignResultSchema = z.union([presignDedupeResultSchema, presignNewResultSchema]);
|
|
176
178
|
var confirmResultSchema = z.object({
|
|
177
179
|
key: z.string(),
|
|
178
180
|
url: z.string().nullable(),
|
|
@@ -224,6 +226,42 @@ var profileResultSchema = z.object({
|
|
|
224
226
|
createdAt: z.string(),
|
|
225
227
|
updatedAt: z.string()
|
|
226
228
|
});
|
|
229
|
+
var clusterGroupResultSchema = z.object({
|
|
230
|
+
id: z.string(),
|
|
231
|
+
index: z.number().int(),
|
|
232
|
+
name: z.string().nullable(),
|
|
233
|
+
description: z.string().nullable(),
|
|
234
|
+
fileCount: z.number().int(),
|
|
235
|
+
nameGeneratedAt: z.string().nullable()
|
|
236
|
+
});
|
|
237
|
+
var clusterResourceResultSchema = z.object({
|
|
238
|
+
id: z.string(),
|
|
239
|
+
name: z.string().nullable(),
|
|
240
|
+
clusterCount: z.number().int(),
|
|
241
|
+
fileCount: z.number().int(),
|
|
242
|
+
clusteredAt: z.string().nullable(),
|
|
243
|
+
createdAt: z.string(),
|
|
244
|
+
updatedAt: z.string(),
|
|
245
|
+
clusters: z.array(clusterGroupResultSchema)
|
|
246
|
+
});
|
|
247
|
+
var clusterFileResultSchema = z.object({
|
|
248
|
+
id: z.string(),
|
|
249
|
+
key: z.string(),
|
|
250
|
+
bucketId: z.string(),
|
|
251
|
+
originalFilename: z.string().nullable(),
|
|
252
|
+
size: z.number(),
|
|
253
|
+
contentType: z.string(),
|
|
254
|
+
metadata: z.record(z.string(), z.string()).nullable(),
|
|
255
|
+
createdAt: z.string(),
|
|
256
|
+
distance: z.number().nullable()
|
|
257
|
+
});
|
|
258
|
+
var clusterFilesResultSchema = z.object({
|
|
259
|
+
clusterId: z.string(),
|
|
260
|
+
files: z.array(clusterFileResultSchema),
|
|
261
|
+
limit: z.number().int(),
|
|
262
|
+
offset: z.number().int(),
|
|
263
|
+
total: z.number().int()
|
|
264
|
+
});
|
|
227
265
|
var profileFilesResultSchema = z.object({
|
|
228
266
|
id: z.string(),
|
|
229
267
|
fileCount: z.number()
|
|
@@ -271,12 +309,48 @@ var anchorResponseSchema = z.object({
|
|
|
271
309
|
var anchorListResponseSchema = z.object({
|
|
272
310
|
anchors: z.array(anchorResponseSchema)
|
|
273
311
|
});
|
|
312
|
+
function sleep(ms) {
|
|
313
|
+
return delay(ms);
|
|
314
|
+
}
|
|
315
|
+
function buildTransformUrl(url, options) {
|
|
316
|
+
if (!(options && (options.width || options.height || options.quality || options.format))) {
|
|
317
|
+
return url;
|
|
318
|
+
}
|
|
319
|
+
const parsed = new URL(url);
|
|
320
|
+
if (options.width) {
|
|
321
|
+
parsed.searchParams.set("w", String(options.width));
|
|
322
|
+
}
|
|
323
|
+
if (options.height) {
|
|
324
|
+
parsed.searchParams.set("h", String(options.height));
|
|
325
|
+
}
|
|
326
|
+
if (options.quality) {
|
|
327
|
+
parsed.searchParams.set("q", String(options.quality));
|
|
328
|
+
}
|
|
329
|
+
if (options.format) {
|
|
330
|
+
parsed.searchParams.set("f", options.format);
|
|
331
|
+
}
|
|
332
|
+
return parsed.toString();
|
|
333
|
+
}
|
|
274
334
|
var StowServer = class {
|
|
275
335
|
apiKey;
|
|
276
336
|
baseUrl;
|
|
277
337
|
bucket;
|
|
278
338
|
timeout;
|
|
279
339
|
retries;
|
|
340
|
+
/**
|
|
341
|
+
* Pure helper for building signed transform URLs from an existing public file URL.
|
|
342
|
+
*
|
|
343
|
+
* This does not perform any network I/O and is safe to pass around as a plain
|
|
344
|
+
* function, for example to view-layer code that needs responsive image URLs.
|
|
345
|
+
*/
|
|
346
|
+
getTransformUrl;
|
|
347
|
+
/**
|
|
348
|
+
* Create a server SDK instance.
|
|
349
|
+
*
|
|
350
|
+
* Pass a bare string when you only need the API key and want default values
|
|
351
|
+
* for `baseUrl`, `timeout`, and retries. Pass an object when you want a
|
|
352
|
+
* default bucket, a non-production API origin, or custom transport settings.
|
|
353
|
+
*/
|
|
280
354
|
constructor(config) {
|
|
281
355
|
if (typeof config === "string") {
|
|
282
356
|
this.apiKey = config;
|
|
@@ -290,10 +364,9 @@ var StowServer = class {
|
|
|
290
364
|
this.timeout = config.timeout ?? 3e4;
|
|
291
365
|
this.retries = config.retries ?? 3;
|
|
292
366
|
}
|
|
367
|
+
this.getTransformUrl = buildTransformUrl;
|
|
293
368
|
}
|
|
294
|
-
/**
|
|
295
|
-
* Get the base URL for this instance (used by client SDK)
|
|
296
|
-
*/
|
|
369
|
+
/** Return the configured API origin, mainly for adapter packages such as `stow-next`. */
|
|
297
370
|
getBaseUrl() {
|
|
298
371
|
return this.baseUrl;
|
|
299
372
|
}
|
|
@@ -407,7 +480,7 @@ var StowServer = class {
|
|
|
407
480
|
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: retry + timeout + error normalization intentionally handled in one request pipeline
|
|
408
481
|
async request(path, options, schema) {
|
|
409
482
|
const maxAttempts = this.retries + 1;
|
|
410
|
-
for (let attempt = 0; attempt < maxAttempts; attempt
|
|
483
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
411
484
|
const controller = new AbortController();
|
|
412
485
|
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
413
486
|
if (options.signal) {
|
|
@@ -429,32 +502,28 @@ var StowServer = class {
|
|
|
429
502
|
const code = error.success ? error.data.code : void 0;
|
|
430
503
|
const isRetryable = response.status === 429 || response.status >= 500;
|
|
431
504
|
if (isRetryable && attempt < maxAttempts - 1) {
|
|
432
|
-
await
|
|
505
|
+
await sleep(1e3 * 2 ** attempt);
|
|
433
506
|
continue;
|
|
434
507
|
}
|
|
435
508
|
throw new StowError(message, response.status, code);
|
|
436
509
|
}
|
|
437
510
|
return schema ? schema.parse(data) : data;
|
|
438
|
-
} catch (
|
|
439
|
-
if (
|
|
440
|
-
throw
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if (error instanceof StowError) {
|
|
513
|
+
throw error;
|
|
441
514
|
}
|
|
442
|
-
if (
|
|
443
|
-
throw new StowError(
|
|
444
|
-
"Invalid response format",
|
|
445
|
-
500,
|
|
446
|
-
"INVALID_RESPONSE"
|
|
447
|
-
);
|
|
515
|
+
if (error instanceof z.ZodError) {
|
|
516
|
+
throw new StowError("Invalid response format", 500, "INVALID_RESPONSE");
|
|
448
517
|
}
|
|
449
|
-
if (
|
|
518
|
+
if (error instanceof DOMException || error instanceof Error && error.name === "AbortError") {
|
|
450
519
|
throw new StowError("Request timed out", 408, "TIMEOUT");
|
|
451
520
|
}
|
|
452
521
|
if (attempt < maxAttempts - 1) {
|
|
453
|
-
await
|
|
522
|
+
await sleep(1e3 * 2 ** attempt);
|
|
454
523
|
continue;
|
|
455
524
|
}
|
|
456
525
|
throw new StowError(
|
|
457
|
-
|
|
526
|
+
error instanceof Error ? error.message : "Network error",
|
|
458
527
|
0,
|
|
459
528
|
"NETWORK_ERROR"
|
|
460
529
|
);
|
|
@@ -464,11 +533,30 @@ var StowServer = class {
|
|
|
464
533
|
}
|
|
465
534
|
throw new StowError("Max retries exceeded", 0, "MAX_RETRIES");
|
|
466
535
|
}
|
|
467
|
-
sleep(ms) {
|
|
468
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
469
|
-
}
|
|
470
536
|
/**
|
|
471
|
-
* Upload
|
|
537
|
+
* Upload bytes from a trusted server environment.
|
|
538
|
+
*
|
|
539
|
+
* This is the highest-level server upload helper:
|
|
540
|
+
* 1. compute a SHA-256 hash for dedupe
|
|
541
|
+
* 2. request a presigned upload URL
|
|
542
|
+
* 3. PUT bytes to storage
|
|
543
|
+
* 4. confirm the upload with optional AI metadata generation
|
|
544
|
+
*
|
|
545
|
+
* Prefer this method when your code already has the file bytes in memory.
|
|
546
|
+
* Use `getPresignedUrl()` + `confirmUpload()` for direct browser uploads
|
|
547
|
+
* instead, and `uploadFromUrl()` when the source is an external URL.
|
|
548
|
+
*
|
|
549
|
+
* @example
|
|
550
|
+
* ```typescript
|
|
551
|
+
* await stow.uploadFile(buffer, {
|
|
552
|
+
* filename: "product.jpg",
|
|
553
|
+
* contentType: "image/jpeg",
|
|
554
|
+
* bucket: "catalog",
|
|
555
|
+
* metadata: { sku: "SKU-123" },
|
|
556
|
+
* title: true,
|
|
557
|
+
* altText: true,
|
|
558
|
+
* });
|
|
559
|
+
* ```
|
|
472
560
|
*/
|
|
473
561
|
async uploadFile(file, options) {
|
|
474
562
|
const filename = options?.filename || "file";
|
|
@@ -502,10 +590,7 @@ var StowServer = class {
|
|
|
502
590
|
throw new StowError("Failed to upload to storage", uploadRes.status);
|
|
503
591
|
}
|
|
504
592
|
return this.request(
|
|
505
|
-
this.withBucket(
|
|
506
|
-
presign.confirmUrl || "/presign/confirm",
|
|
507
|
-
options?.bucket
|
|
508
|
-
),
|
|
593
|
+
this.withBucket(presign.confirmUrl || "/presign/confirm", options?.bucket),
|
|
509
594
|
{
|
|
510
595
|
method: "POST",
|
|
511
596
|
headers: { "Content-Type": "application/json" },
|
|
@@ -525,7 +610,12 @@ var StowServer = class {
|
|
|
525
610
|
);
|
|
526
611
|
}
|
|
527
612
|
/**
|
|
528
|
-
*
|
|
613
|
+
* Import a remote asset by URL.
|
|
614
|
+
*
|
|
615
|
+
* Stow fetches the remote URL server-side, stores the resulting bytes, and
|
|
616
|
+
* persists the file as if it had been uploaded normally. This is useful for
|
|
617
|
+
* migrations, ingestion pipelines, or bringing third-party assets into Stow
|
|
618
|
+
* without downloading them into your own process first.
|
|
529
619
|
*/
|
|
530
620
|
async uploadFromUrl(url, filename, options) {
|
|
531
621
|
const result = await this.request(
|
|
@@ -560,7 +650,7 @@ var StowServer = class {
|
|
|
560
650
|
* The actual fetch, validation, and upload happen in a background worker.
|
|
561
651
|
* Use this for bulk imports where you don't need immediate confirmation.
|
|
562
652
|
*/
|
|
563
|
-
|
|
653
|
+
queueUploadFromUrl(url, filename, options) {
|
|
564
654
|
return this.request(
|
|
565
655
|
this.withBucket("/upload", options?.bucket),
|
|
566
656
|
{
|
|
@@ -581,24 +671,20 @@ var StowServer = class {
|
|
|
581
671
|
);
|
|
582
672
|
}
|
|
583
673
|
/**
|
|
584
|
-
* Get a presigned URL for direct client
|
|
674
|
+
* Get a presigned URL for a direct client upload.
|
|
585
675
|
*
|
|
586
|
-
* This
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
676
|
+
* This is the server-side half of the browser upload flow used by
|
|
677
|
+
* `@howells/stow-client` and `@howells/stow-next`:
|
|
678
|
+
* 1. browser calls your app
|
|
679
|
+
* 2. your app calls `getPresignedUrl()`
|
|
680
|
+
* 3. browser PUTs bytes to `uploadUrl`
|
|
681
|
+
* 4. browser or your app calls `confirmUpload()`
|
|
682
|
+
*
|
|
683
|
+
* If `contentHash` matches an existing file in the target bucket, the API
|
|
684
|
+
* short-circuits with `{ dedupe: true, ... }` and no upload is required.
|
|
591
685
|
*/
|
|
592
686
|
getPresignedUrl(request) {
|
|
593
|
-
const {
|
|
594
|
-
filename,
|
|
595
|
-
contentType,
|
|
596
|
-
size,
|
|
597
|
-
route,
|
|
598
|
-
bucket,
|
|
599
|
-
metadata,
|
|
600
|
-
contentHash
|
|
601
|
-
} = request;
|
|
687
|
+
const { filename, contentType, size, route, bucket, metadata, contentHash } = request;
|
|
602
688
|
return this.request(
|
|
603
689
|
this.withBucket("/presign", bucket),
|
|
604
690
|
{
|
|
@@ -617,8 +703,12 @@ var StowServer = class {
|
|
|
617
703
|
);
|
|
618
704
|
}
|
|
619
705
|
/**
|
|
620
|
-
* Confirm a
|
|
621
|
-
*
|
|
706
|
+
* Confirm a direct upload after the client has finished the storage PUT.
|
|
707
|
+
*
|
|
708
|
+
* This finalizes the file record in the database and optionally triggers
|
|
709
|
+
* post-processing such as AI-generated title/description/alt text.
|
|
710
|
+
*
|
|
711
|
+
* Call this exactly once per successful presigned upload.
|
|
622
712
|
*/
|
|
623
713
|
confirmUpload(request) {
|
|
624
714
|
const {
|
|
@@ -654,9 +744,13 @@ var StowServer = class {
|
|
|
654
744
|
);
|
|
655
745
|
}
|
|
656
746
|
/**
|
|
657
|
-
* List files in
|
|
747
|
+
* List files in a bucket with optional prefix filtering and enrichment blocks.
|
|
658
748
|
*
|
|
659
|
-
*
|
|
749
|
+
* Use `cursor` to continue pagination from a previous page. When requesting
|
|
750
|
+
* `include`, Stow expands those relationships inline so you can avoid follow-up
|
|
751
|
+
* per-file lookups.
|
|
752
|
+
*
|
|
753
|
+
* @param options.include Optional enrichment fields: `"tags"`, `"taxonomies"`
|
|
660
754
|
*/
|
|
661
755
|
listFiles(options) {
|
|
662
756
|
const params = new URLSearchParams();
|
|
@@ -676,11 +770,7 @@ var StowServer = class {
|
|
|
676
770
|
params.set("include", options.include.join(","));
|
|
677
771
|
}
|
|
678
772
|
const path = `/files?${params}`;
|
|
679
|
-
return this.request(
|
|
680
|
-
this.withBucket(path, options?.bucket),
|
|
681
|
-
{ method: "GET" },
|
|
682
|
-
listFilesSchema
|
|
683
|
-
);
|
|
773
|
+
return this.request(this.withBucket(path, options?.bucket), { method: "GET" }, listFilesSchema);
|
|
684
774
|
}
|
|
685
775
|
/**
|
|
686
776
|
* Delete a file by key
|
|
@@ -703,9 +793,12 @@ var StowServer = class {
|
|
|
703
793
|
});
|
|
704
794
|
}
|
|
705
795
|
/**
|
|
706
|
-
* Get
|
|
796
|
+
* Get one file by key.
|
|
707
797
|
*
|
|
708
|
-
*
|
|
798
|
+
* This is the detailed file view and includes dimensions, embeddings status,
|
|
799
|
+
* extracted colors, and AI metadata fields when available.
|
|
800
|
+
*
|
|
801
|
+
* @param options.include Optional enrichment fields: `"tags"`, `"taxonomies"`
|
|
709
802
|
*/
|
|
710
803
|
getFile(key, options) {
|
|
711
804
|
const params = new URLSearchParams();
|
|
@@ -813,35 +906,6 @@ var StowServer = class {
|
|
|
813
906
|
replaceResultSchema
|
|
814
907
|
);
|
|
815
908
|
}
|
|
816
|
-
/**
|
|
817
|
-
* Get a transform URL for an image.
|
|
818
|
-
*
|
|
819
|
-
* Appends transform query params (?w=, ?h=, ?q=, ?f=) to a file URL.
|
|
820
|
-
* Transforms are applied at the edge by the Cloudflare Worker — no
|
|
821
|
-
* server round-trip needed.
|
|
822
|
-
*
|
|
823
|
-
* @param url - Full file URL (e.g. from upload result's fileUrl)
|
|
824
|
-
* @param options - Transform options (width, height, quality, format)
|
|
825
|
-
*/
|
|
826
|
-
getTransformUrl(url, options) {
|
|
827
|
-
if (!(options && (options.width || options.height || options.quality || options.format))) {
|
|
828
|
-
return url;
|
|
829
|
-
}
|
|
830
|
-
const parsed = new URL(url);
|
|
831
|
-
if (options.width) {
|
|
832
|
-
parsed.searchParams.set("w", String(options.width));
|
|
833
|
-
}
|
|
834
|
-
if (options.height) {
|
|
835
|
-
parsed.searchParams.set("h", String(options.height));
|
|
836
|
-
}
|
|
837
|
-
if (options.quality) {
|
|
838
|
-
parsed.searchParams.set("q", String(options.quality));
|
|
839
|
-
}
|
|
840
|
-
if (options.format) {
|
|
841
|
-
parsed.searchParams.set("f", options.format);
|
|
842
|
-
}
|
|
843
|
-
return parsed.toString();
|
|
844
|
-
}
|
|
845
909
|
// ============================================================
|
|
846
910
|
// TAGS - Org-scoped labels for file organization
|
|
847
911
|
// ============================================================
|
|
@@ -856,17 +920,17 @@ var StowServer = class {
|
|
|
856
920
|
};
|
|
857
921
|
}
|
|
858
922
|
listTags() {
|
|
859
|
-
return this.request("/tags", { method: "GET" });
|
|
923
|
+
return this.request(this.withBucket("/tags"), { method: "GET" });
|
|
860
924
|
}
|
|
861
925
|
createTag(params) {
|
|
862
|
-
return this.request("/tags", {
|
|
926
|
+
return this.request(this.withBucket("/tags"), {
|
|
863
927
|
method: "POST",
|
|
864
928
|
headers: { "Content-Type": "application/json" },
|
|
865
929
|
body: JSON.stringify(params)
|
|
866
930
|
});
|
|
867
931
|
}
|
|
868
932
|
async deleteTag(id) {
|
|
869
|
-
await this.request(`/tags/${encodeURIComponent(id)}
|
|
933
|
+
await this.request(this.withBucket(`/tags/${encodeURIComponent(id)}`), {
|
|
870
934
|
method: "DELETE"
|
|
871
935
|
});
|
|
872
936
|
}
|
|
@@ -919,7 +983,34 @@ var StowServer = class {
|
|
|
919
983
|
// SEARCH - Vector similarity search
|
|
920
984
|
// ============================================================
|
|
921
985
|
/**
|
|
922
|
-
*
|
|
986
|
+
* Semantic search namespace.
|
|
987
|
+
*
|
|
988
|
+
* Methods:
|
|
989
|
+
* - `text(...)` embeds text and finds matching files
|
|
990
|
+
* - `similar(...)` finds nearest neighbors for a file, anchor, profile, or cluster
|
|
991
|
+
* - `diverse(...)` balances similarity against result spread
|
|
992
|
+
* - `color(...)` performs palette similarity search
|
|
993
|
+
* - `image(...)` searches using an existing file or an external image URL
|
|
994
|
+
*
|
|
995
|
+
* Cluster-aware search examples:
|
|
996
|
+
*
|
|
997
|
+
* @example
|
|
998
|
+
* ```typescript
|
|
999
|
+
* const cluster = await stow.clusters.create({
|
|
1000
|
+
* bucket: "inspiration",
|
|
1001
|
+
* fileKeys,
|
|
1002
|
+
* clusterCount: 12,
|
|
1003
|
+
* });
|
|
1004
|
+
*
|
|
1005
|
+
* const firstGroup = cluster.clusters[0];
|
|
1006
|
+
* if (firstGroup) {
|
|
1007
|
+
* const related = await stow.search.similar({
|
|
1008
|
+
* bucket: "inspiration",
|
|
1009
|
+
* clusterId: firstGroup.id,
|
|
1010
|
+
* limit: 20,
|
|
1011
|
+
* });
|
|
1012
|
+
* }
|
|
1013
|
+
* ```
|
|
923
1014
|
*/
|
|
924
1015
|
get search() {
|
|
925
1016
|
return {
|
|
@@ -1091,7 +1182,11 @@ var StowServer = class {
|
|
|
1091
1182
|
// PROFILES - Taste/preference profiles from file collections
|
|
1092
1183
|
// ============================================================
|
|
1093
1184
|
/**
|
|
1094
|
-
*
|
|
1185
|
+
* Taste-profile namespace.
|
|
1186
|
+
*
|
|
1187
|
+
* Profiles are long-lived preference objects. They can be seeded from files,
|
|
1188
|
+
* updated through weighted signals, clustered into interpretable segments, and
|
|
1189
|
+
* then reused as semantic search seeds.
|
|
1095
1190
|
*/
|
|
1096
1191
|
get profiles() {
|
|
1097
1192
|
return {
|
|
@@ -1107,6 +1202,49 @@ var StowServer = class {
|
|
|
1107
1202
|
renameCluster: (profileId, clusterId, params, bucket) => this.renameProfileCluster(profileId, clusterId, params, bucket)
|
|
1108
1203
|
};
|
|
1109
1204
|
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Curated cluster namespace.
|
|
1207
|
+
*
|
|
1208
|
+
* Use this when you have an explicit file set that should be grouped by visual
|
|
1209
|
+
* similarity, but should not be modeled as a behavioral profile.
|
|
1210
|
+
*
|
|
1211
|
+
* Typical workflow:
|
|
1212
|
+
* 1. `create({ fileKeys, clusterCount })`
|
|
1213
|
+
* 2. poll `get(id)` until `clusteredAt` is non-null
|
|
1214
|
+
* 3. inspect `clusters`
|
|
1215
|
+
* 4. fetch representative files with `files(id, clusterId)`
|
|
1216
|
+
* 5. optionally `renameCluster(...)`
|
|
1217
|
+
* 6. use `clusterId` with `search.similar(...)` or `search.diverse(...)`
|
|
1218
|
+
*
|
|
1219
|
+
* @example
|
|
1220
|
+
* ```typescript
|
|
1221
|
+
* const resource = await stow.clusters.create({
|
|
1222
|
+
* bucket: "featured-products",
|
|
1223
|
+
* fileKeys: featuredKeys,
|
|
1224
|
+
* clusterCount: 12,
|
|
1225
|
+
* name: "Navigator groups",
|
|
1226
|
+
* });
|
|
1227
|
+
*
|
|
1228
|
+
* const latest = await stow.clusters.get(resource.id, "featured-products");
|
|
1229
|
+
* const group = latest.clusters[0];
|
|
1230
|
+
* if (group) {
|
|
1231
|
+
* const representatives = await stow.clusters.files(resource.id, group.id, {
|
|
1232
|
+
* limit: 12,
|
|
1233
|
+
* offset: 0,
|
|
1234
|
+
* });
|
|
1235
|
+
* }
|
|
1236
|
+
* ```
|
|
1237
|
+
*/
|
|
1238
|
+
get clusters() {
|
|
1239
|
+
return {
|
|
1240
|
+
create: (params) => this.createClustersResource(params),
|
|
1241
|
+
get: (id, bucket) => this.getClustersResource(id, bucket),
|
|
1242
|
+
recluster: (id, params, bucket) => this.reclusterClustersResource(id, params, bucket),
|
|
1243
|
+
files: (id, clusterId, params, bucket) => this.getClusterFiles(id, clusterId, params, bucket),
|
|
1244
|
+
renameCluster: (id, clusterId, params, bucket) => this.renameClusterGroup(id, clusterId, params, bucket),
|
|
1245
|
+
delete: (id, bucket) => this.deleteClustersResource(id, bucket)
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1110
1248
|
createProfile(params) {
|
|
1111
1249
|
return this.request(
|
|
1112
1250
|
this.withBucket("/profiles", params?.bucket),
|
|
@@ -1129,10 +1267,9 @@ var StowServer = class {
|
|
|
1129
1267
|
);
|
|
1130
1268
|
}
|
|
1131
1269
|
async deleteProfile(id, bucket) {
|
|
1132
|
-
await this.request(
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
);
|
|
1270
|
+
await this.request(this.withBucket(`/profiles/${encodeURIComponent(id)}`, bucket), {
|
|
1271
|
+
method: "DELETE"
|
|
1272
|
+
});
|
|
1136
1273
|
}
|
|
1137
1274
|
addProfileFiles(id, fileKeys, bucket) {
|
|
1138
1275
|
return this.request(
|
|
@@ -1179,36 +1316,99 @@ var StowServer = class {
|
|
|
1179
1316
|
);
|
|
1180
1317
|
}
|
|
1181
1318
|
getProfileClusters(id, bucket) {
|
|
1319
|
+
return this.request(this.withBucket(`/profiles/${encodeURIComponent(id)}/clusters`, bucket), {
|
|
1320
|
+
method: "GET"
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
reclusterProfile(id, params, bucket) {
|
|
1324
|
+
return this.request(this.withBucket(`/profiles/${encodeURIComponent(id)}/clusters`, bucket), {
|
|
1325
|
+
method: "POST",
|
|
1326
|
+
headers: { "Content-Type": "application/json" },
|
|
1327
|
+
body: JSON.stringify(
|
|
1328
|
+
params?.clusterCount === void 0 ? {} : { clusterCount: params.clusterCount }
|
|
1329
|
+
)
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
renameProfileCluster(profileId, clusterId, params, bucket) {
|
|
1182
1333
|
return this.request(
|
|
1183
|
-
this.withBucket(
|
|
1184
|
-
|
|
1334
|
+
this.withBucket(
|
|
1335
|
+
`/profiles/${encodeURIComponent(profileId)}/clusters/${encodeURIComponent(clusterId)}`,
|
|
1336
|
+
bucket
|
|
1337
|
+
),
|
|
1338
|
+
{
|
|
1339
|
+
method: "PUT",
|
|
1340
|
+
headers: { "Content-Type": "application/json" },
|
|
1341
|
+
body: JSON.stringify(params)
|
|
1342
|
+
}
|
|
1185
1343
|
);
|
|
1186
1344
|
}
|
|
1187
|
-
|
|
1345
|
+
createClustersResource(params) {
|
|
1188
1346
|
return this.request(
|
|
1189
|
-
|
|
1347
|
+
"/clusters",
|
|
1190
1348
|
{
|
|
1191
1349
|
method: "POST",
|
|
1192
1350
|
headers: { "Content-Type": "application/json" },
|
|
1193
1351
|
body: JSON.stringify({
|
|
1194
|
-
|
|
1352
|
+
fileKeys: params.fileKeys,
|
|
1353
|
+
...params.clusterCount === void 0 ? {} : { clusterCount: params.clusterCount },
|
|
1354
|
+
...params.name ? { name: params.name } : {},
|
|
1355
|
+
...params.bucket ? { bucket: params.bucket } : {}
|
|
1195
1356
|
})
|
|
1196
|
-
}
|
|
1357
|
+
},
|
|
1358
|
+
clusterResourceResultSchema
|
|
1197
1359
|
);
|
|
1198
1360
|
}
|
|
1199
|
-
|
|
1361
|
+
getClustersResource(id, bucket) {
|
|
1362
|
+
return this.request(
|
|
1363
|
+
this.withBucket(`/clusters/${encodeURIComponent(id)}`, bucket),
|
|
1364
|
+
{ method: "GET" },
|
|
1365
|
+
clusterResourceResultSchema
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
reclusterClustersResource(id, params, bucket) {
|
|
1369
|
+
return this.request(
|
|
1370
|
+
this.withBucket(`/clusters/${encodeURIComponent(id)}/recluster`, bucket),
|
|
1371
|
+
{
|
|
1372
|
+
method: "POST",
|
|
1373
|
+
headers: { "Content-Type": "application/json" },
|
|
1374
|
+
body: JSON.stringify(
|
|
1375
|
+
params?.clusterCount === void 0 ? {} : { clusterCount: params.clusterCount }
|
|
1376
|
+
)
|
|
1377
|
+
},
|
|
1378
|
+
clusterResourceResultSchema
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
getClusterFiles(id, clusterId, params, bucket) {
|
|
1382
|
+
const searchParams = new URLSearchParams();
|
|
1383
|
+
if (params?.limit !== void 0) {
|
|
1384
|
+
searchParams.set("limit", String(params.limit));
|
|
1385
|
+
}
|
|
1386
|
+
if (params?.offset !== void 0) {
|
|
1387
|
+
searchParams.set("offset", String(params.offset));
|
|
1388
|
+
}
|
|
1389
|
+
const qs = searchParams.toString();
|
|
1390
|
+
const path = `/clusters/${encodeURIComponent(id)}/clusters/${encodeURIComponent(clusterId)}/files${qs ? `?${qs}` : ""}`;
|
|
1391
|
+
return this.request(this.withBucket(path, bucket), { method: "GET" }, clusterFilesResultSchema);
|
|
1392
|
+
}
|
|
1393
|
+
renameClusterGroup(id, clusterId, params, bucket) {
|
|
1200
1394
|
return this.request(
|
|
1201
1395
|
this.withBucket(
|
|
1202
|
-
`/
|
|
1396
|
+
`/clusters/${encodeURIComponent(id)}/clusters/${encodeURIComponent(clusterId)}`,
|
|
1203
1397
|
bucket
|
|
1204
1398
|
),
|
|
1205
1399
|
{
|
|
1206
1400
|
method: "PUT",
|
|
1207
1401
|
headers: { "Content-Type": "application/json" },
|
|
1208
1402
|
body: JSON.stringify(params)
|
|
1209
|
-
}
|
|
1403
|
+
},
|
|
1404
|
+
clusterGroupResultSchema
|
|
1210
1405
|
);
|
|
1211
1406
|
}
|
|
1407
|
+
async deleteClustersResource(id, bucket) {
|
|
1408
|
+
await this.request(this.withBucket(`/clusters/${encodeURIComponent(id)}`, bucket), {
|
|
1409
|
+
method: "DELETE"
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1212
1412
|
// ============================================================
|
|
1213
1413
|
// ANCHORS - Named semantic reference points in vector space
|
|
1214
1414
|
// ============================================================
|
|
@@ -1270,10 +1470,9 @@ var StowServer = class {
|
|
|
1270
1470
|
);
|
|
1271
1471
|
}
|
|
1272
1472
|
async deleteAnchor(id, options) {
|
|
1273
|
-
await this.request(
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
);
|
|
1473
|
+
await this.request(this.withBucket(`/anchors/${encodeURIComponent(id)}`, options?.bucket), {
|
|
1474
|
+
method: "DELETE"
|
|
1475
|
+
});
|
|
1277
1476
|
}
|
|
1278
1477
|
};
|
|
1279
1478
|
export {
|
package/package.json
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howells/stow-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Server-side SDK for Stow file storage",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"file-storage",
|
|
7
|
+
"s3",
|
|
8
|
+
"sdk",
|
|
9
|
+
"stow",
|
|
10
|
+
"upload"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://stow.sh",
|
|
5
13
|
"license": "MIT",
|
|
6
14
|
"repository": {
|
|
7
15
|
"type": "git",
|
|
8
16
|
"url": "git+https://github.com/howells/stow.git",
|
|
9
17
|
"directory": "packages/stow-server"
|
|
10
18
|
},
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
"stow",
|
|
14
|
-
"file-storage",
|
|
15
|
-
"s3",
|
|
16
|
-
"upload",
|
|
17
|
-
"sdk"
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
18
21
|
],
|
|
19
22
|
"main": "./dist/index.js",
|
|
20
23
|
"module": "./dist/index.mjs",
|
|
@@ -26,24 +29,21 @@
|
|
|
26
29
|
"require": "./dist/index.js"
|
|
27
30
|
}
|
|
28
31
|
},
|
|
29
|
-
"files": [
|
|
30
|
-
"dist"
|
|
31
|
-
],
|
|
32
|
-
"peerDependencies": {
|
|
33
|
-
"zod": "^3.0.0 || ^4.0.0"
|
|
34
|
-
},
|
|
35
32
|
"devDependencies": {
|
|
36
33
|
"@types/node": "^25.5.0",
|
|
37
34
|
"tsup": "^8.5.1",
|
|
38
35
|
"typescript": "^5.9.3",
|
|
39
|
-
"
|
|
36
|
+
"vite-plus": "latest",
|
|
40
37
|
"zod": "^4.3.6",
|
|
41
38
|
"@stow/typescript-config": "0.0.0"
|
|
42
39
|
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"zod": "^3.0.0 || ^4.0.0"
|
|
42
|
+
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
45
45
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
46
|
-
"test": "
|
|
47
|
-
"test:watch": "
|
|
46
|
+
"test": "vp test run",
|
|
47
|
+
"test:watch": "vp test"
|
|
48
48
|
}
|
|
49
49
|
}
|