@storyshelf/storage-gcs 0.0.0-stage → 0.5.3

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Siddhant Gupta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,79 @@
1
- # Temporary Holding Version
1
+ # @storyshelf/storage-gcs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Google Cloud Storage (GCS) adapter for StoryShelf: reads and writes blobs to a GCS bucket, including GCS-compatible emulators.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ nub add @storyshelf/storage-gcs
9
+ ```
10
+
11
+ or
12
+
13
+ ```sh
14
+ npm install @storyshelf/storage-gcs
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { createGcsStorage } from "@storyshelf/storage-gcs";
21
+ import { createShelfApp } from "@storyshelf/app";
22
+
23
+ const storage = createGcsStorage({
24
+ bucket: "my-shelf",
25
+ prefix: "storyshelf", // optional
26
+ projectId: "my-gcp-project", // optional, defaults to ADC
27
+ });
28
+
29
+ const app = createShelfApp({ database, storage });
30
+ ```
31
+
32
+ For local emulators or custom endpoints:
33
+
34
+ ```ts
35
+ const storage = createGcsStorage({
36
+ bucket: "my-shelf",
37
+ apiEndpoint: "http://localhost:9090",
38
+ projectId: "test-project",
39
+ });
40
+ ```
41
+
42
+ Inject a pre-configured client (tests / IAM-rotation):
43
+
44
+ ```ts
45
+ import { Storage } from "@google-cloud/storage";
46
+
47
+ const client = new Storage({ projectId: "my-project" });
48
+ const storage = createGcsStorage({ bucket: "my-shelf", client });
49
+ ```
50
+
51
+ ## API
52
+
53
+ ### `GcsStorageOptions`
54
+
55
+ ```ts
56
+ interface GcsStorageOptions {
57
+ bucket: string; // required GCS bucket name
58
+ prefix?: string; // optional key prefix, defaults to ""
59
+ client?: Storage; // optional pre-configured @google-cloud/storage client
60
+ projectId?: string; // optional GCP project id
61
+ keyFilename?: string; // optional path to service-account JSON
62
+ credentials?: object; // optional service-account credentials object
63
+ apiEndpoint?: string; // optional custom endpoint (emulator)
64
+ }
65
+ ```
66
+
67
+ ### `createGcsStorage(options: GcsStorageOptions): StorageAdapter`
68
+
69
+ Creates a `StorageAdapter` backed by the configured GCS bucket. The returned adapter implements every method of the `StorageAdapter` interface (`read`, `write`, `delete`, `exists`, `list(prefix)`, `readStream`, `writeStream`).
70
+
71
+ ### `gcsKey(prefix: string, path: string): string`
72
+
73
+ Helper that joins a storage prefix with a relative path into an object key.
74
+
75
+ ## How it fits in
76
+
77
+ `storage-gcs` is the `storage` option for `createShelfApp` in GCP deployments. It implements the same `StorageAdapter` interface as `@storyshelf/storage-local` and `@storyshelf/storage-s3`, so switching between local disk, S3, and GCS requires no changes elsewhere.
78
+
79
+ See `docs/architecture.md` and ADR 0006.
@@ -0,0 +1,27 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { Storage } from "@google-cloud/storage";
5
+ //#region src/client.ts
6
+ /** GCS Storage client construction and shared operation context. */
7
+ /** Build the bucket handle plus key prefix every GCS operation threads through. */
8
+ function createGcsContext(options) {
9
+ const { bucket: bucketName, prefix = "", client: injectedClient } = options;
10
+ return {
11
+ bucket: (injectedClient ?? new Storage(resolveClientOptions(options))).bucket(bucketName),
12
+ prefix
13
+ };
14
+ }
15
+ /** Translate adapter options into @google-cloud/storage constructor options. */
16
+ function resolveClientOptions(options) {
17
+ const config = {};
18
+ if (options.projectId !== void 0) config.projectId = options.projectId;
19
+ if (options.keyFilename !== void 0) config.keyFilename = options.keyFilename;
20
+ if (options.credentials !== void 0) config.credentials = options.credentials;
21
+ if (options.apiEndpoint !== void 0) config.apiEndpoint = options.apiEndpoint;
22
+ return config;
23
+ }
24
+ //#endregion
25
+ export { createGcsContext, resolveClientOptions };
26
+
27
+ //# sourceMappingURL=client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../src/client.ts"],"sourcesContent":["/** GCS Storage client construction and shared operation context. */\nimport { Storage } from \"@google-cloud/storage\";\nimport type { GcsStorageOptions } from \"./types.ts\";\n\n/** Build the bucket handle plus key prefix every GCS operation threads through. */\nexport function createGcsContext(options: GcsStorageOptions): GcsContext {\n const { bucket: bucketName, prefix = \"\", client: injectedClient } = options;\n const client = injectedClient ?? new Storage(resolveClientOptions(options));\n return { bucket: client.bucket(bucketName), prefix };\n}\n\n/** Shared GCS bucket handle plus key prefix. */\nexport interface GcsContext {\n bucket: ReturnType<Storage[\"bucket\"]>;\n prefix: string;\n}\n\n/** Translate adapter options into @google-cloud/storage constructor options. */\nexport function resolveClientOptions(\n options: GcsStorageOptions,\n): ConstructorParameters<typeof Storage>[0] {\n const config: ConstructorParameters<typeof Storage>[0] = {};\n if (options.projectId !== undefined) {\n config.projectId = options.projectId;\n }\n if (options.keyFilename !== undefined) {\n config.keyFilename = options.keyFilename;\n }\n if (options.credentials !== undefined) {\n config.credentials = options.credentials;\n }\n if (options.apiEndpoint !== undefined) {\n config.apiEndpoint = options.apiEndpoint;\n }\n return config;\n}\n"],"mappings":";;;;;;;AAKA,SAAgB,iBAAiB,SAAwC;CACvE,MAAM,EAAE,QAAQ,YAAY,SAAS,IAAI,QAAQ,mBAAmB;CAEpE,OAAO;EAAE,SADM,kBAAkB,IAAI,QAAQ,qBAAqB,OAAO,CAAC,EAAA,CAClD,OAAO,UAAU;EAAG;CAAO;AACrD;;AASA,SAAgB,qBACd,SAC0C;CAC1C,MAAM,SAAmD,CAAC;CAC1D,IAAI,QAAQ,cAAc,KAAA,GACxB,OAAO,YAAY,QAAQ;CAE7B,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,OAAO,cAAc,QAAQ;CAE/B,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,OAAO,cAAc,QAAQ;CAE/B,IAAI,QAAQ,gBAAgB,KAAA,GAC1B,OAAO,cAAc,QAAQ;CAE/B,OAAO;AACT"}
@@ -0,0 +1,16 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/errors.ts
5
+ /** Not-found guard for GCS API errors. */
6
+ /** Return whether a GCS error signals a missing object. */
7
+ function isNotFound(error) {
8
+ const err = error;
9
+ if (err.code === 404 || err.code === "404" || err.code === "notFound") return true;
10
+ if (err.statusCode === 404) return true;
11
+ return err.errors?.[0]?.reason === "notFound";
12
+ }
13
+ //#endregion
14
+ export { isNotFound };
15
+
16
+ //# sourceMappingURL=errors.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/** Not-found guard for GCS API errors. */\n\n/** Return whether a GCS error signals a missing object. */\nexport function isNotFound(error: unknown): boolean {\n const err = error as {\n code?: number | string;\n statusCode?: number;\n errors?: { reason?: string }[];\n };\n if (err.code === 404 || err.code === \"404\" || err.code === \"notFound\") {\n return true;\n }\n if (err.statusCode === 404) {\n return true;\n }\n return err.errors?.[0]?.reason === \"notFound\";\n}\n"],"mappings":";;;;;;AAGA,SAAgB,WAAW,OAAyB;CAClD,MAAM,MAAM;CAKZ,IAAI,IAAI,SAAS,OAAO,IAAI,SAAS,SAAS,IAAI,SAAS,YACzD,OAAO;CAET,IAAI,IAAI,eAAe,KACrB,OAAO;CAET,OAAO,IAAI,SAAS,EAAE,EAAE,WAAW;AACrC"}
@@ -0,0 +1,16 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { GcsStorageOptions } from "./types.mjs";
4
+ import { gcsKey } from "./keys.mjs";
5
+ import { StorageAdapter } from "@storyshelf/core/adapter/storage";
6
+ //#region src/index.d.ts
7
+ /**
8
+ * Create a Google Cloud Storage (GCS) StorageAdapter.
9
+ *
10
+ * @param options - GCS configuration options.
11
+ * @returns A StorageAdapter backed by the configured GCS bucket.
12
+ */
13
+ declare function createGcsStorage(options: GcsStorageOptions): StorageAdapter;
14
+ //#endregion
15
+ export { type GcsStorageOptions, createGcsStorage, gcsKey };
16
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,53 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { createGcsContext } from "./client.mjs";
5
+ import { createGcsLifecycle } from "./lifecycle.mjs";
6
+ import { gcsKey } from "./keys.mjs";
7
+ import { gcsDelete, gcsExists, gcsList, gcsRead, gcsWrite } from "./operations.mjs";
8
+ import { gcsReadStream, gcsWriteStream } from "./streams.mjs";
9
+ //#region src/index.ts
10
+ /**
11
+ * Create a Google Cloud Storage (GCS) StorageAdapter.
12
+ *
13
+ * @param options - GCS configuration options.
14
+ * @returns A StorageAdapter backed by the configured GCS bucket.
15
+ */
16
+ function createGcsStorage(options) {
17
+ const ctx = createGcsContext(options);
18
+ return {
19
+ metadata: {
20
+ name: "GCS Storage",
21
+ version: globalThis.__PKG_VERSION__ ?? "0.0.0",
22
+ description: "Google Cloud Storage adapter",
23
+ kind: "gcs",
24
+ category: "storage"
25
+ },
26
+ lifecycle: createGcsLifecycle(ctx),
27
+ async read(path) {
28
+ return await gcsRead(ctx, path);
29
+ },
30
+ async write(path, data) {
31
+ await gcsWrite(ctx, path, data);
32
+ },
33
+ async delete(path) {
34
+ await gcsDelete(ctx, path);
35
+ },
36
+ async exists(path) {
37
+ return await gcsExists(ctx, path);
38
+ },
39
+ async list(listPrefix) {
40
+ return await gcsList(ctx, listPrefix);
41
+ },
42
+ async writeStream(path, stream) {
43
+ await gcsWriteStream(ctx, path, stream);
44
+ },
45
+ async readStream(path) {
46
+ return await gcsReadStream(ctx, path);
47
+ }
48
+ };
49
+ }
50
+ //#endregion
51
+ export { createGcsStorage, gcsKey };
52
+
53
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Google Cloud Storage adapter for StoryShelf.\n *\n * Server-side blob storage for screenshots, diffs, and Storybook archives.\n * The factory wires per-concern modules (client, lifecycle, operations,\n * streams); key and option helpers are re-exported below.\n */\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport { createGcsContext } from \"./client.ts\";\nimport { createGcsLifecycle } from \"./lifecycle.ts\";\nimport { gcsDelete, gcsExists, gcsList, gcsRead, gcsWrite } from \"./operations.ts\";\nimport { gcsReadStream, gcsWriteStream } from \"./streams.ts\";\nimport type { GcsStorageOptions } from \"./types.ts\";\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/**\n * Create a Google Cloud Storage (GCS) StorageAdapter.\n *\n * @param options - GCS configuration options.\n * @returns A StorageAdapter backed by the configured GCS bucket.\n */\nexport function createGcsStorage(options: GcsStorageOptions): StorageAdapter {\n const ctx = createGcsContext(options);\n\n return {\n metadata: {\n name: \"GCS Storage\",\n version: (globalThis as unknown as { __PKG_VERSION__?: string }).__PKG_VERSION__ ?? \"0.0.0\",\n description: \"Google Cloud Storage adapter\",\n kind: \"gcs\",\n category: \"storage\",\n },\n lifecycle: createGcsLifecycle(ctx),\n async read(path) {\n return await gcsRead(ctx, path);\n },\n async write(path, data) {\n await gcsWrite(ctx, path, data);\n },\n async delete(path) {\n await gcsDelete(ctx, path);\n },\n async exists(path) {\n return await gcsExists(ctx, path);\n },\n async list(listPrefix) {\n return await gcsList(ctx, listPrefix);\n },\n async writeStream(path, stream) {\n await gcsWriteStream(ctx, path, stream);\n },\n async readStream(path) {\n return await gcsReadStream(ctx, path);\n },\n };\n}\n\nexport type { GcsStorageOptions } from \"./types.ts\";\nexport { gcsKey } from \"./keys.ts\";\n"],"mappings":";;;;;;;;;;;;;;;AAsBA,SAAgB,iBAAiB,SAA4C;CAC3E,MAAM,MAAM,iBAAiB,OAAO;CAEpC,OAAO;EACL,UAAU;GACR,MAAM;GACN,SAAU,WAAuD,mBAAmB;GACpF,aAAa;GACb,MAAM;GACN,UAAU;EACZ;EACA,WAAW,mBAAmB,GAAG;EACjC,MAAM,KAAK,MAAM;GACf,OAAO,MAAM,QAAQ,KAAK,IAAI;EAChC;EACA,MAAM,MAAM,MAAM,MAAM;GACtB,MAAM,SAAS,KAAK,MAAM,IAAI;EAChC;EACA,MAAM,OAAO,MAAM;GACjB,MAAM,UAAU,KAAK,IAAI;EAC3B;EACA,MAAM,OAAO,MAAM;GACjB,OAAO,MAAM,UAAU,KAAK,IAAI;EAClC;EACA,MAAM,KAAK,YAAY;GACrB,OAAO,MAAM,QAAQ,KAAK,UAAU;EACtC;EACA,MAAM,YAAY,MAAM,QAAQ;GAC9B,MAAM,eAAe,KAAK,MAAM,MAAM;EACxC;EACA,MAAM,WAAW,MAAM;GACrB,OAAO,MAAM,cAAc,KAAK,IAAI;EACtC;CACF;AACF"}
@@ -0,0 +1,9 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ //#region src/keys.d.ts
4
+ /** Key join/split for GCS object keys. */
5
+ /** Join an optional key prefix with a storage path. */
6
+ declare function gcsKey(prefix: string, path: string): string;
7
+ //#endregion
8
+ export { gcsKey };
9
+ //# sourceMappingURL=keys.d.mts.map
package/dist/keys.mjs ADDED
@@ -0,0 +1,17 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/keys.ts
5
+ /** Key join/split for GCS object keys. */
6
+ /** Join an optional key prefix with a storage path. */
7
+ function gcsKey(prefix, path) {
8
+ return prefix === "" ? path : `${prefix}/${path}`;
9
+ }
10
+ /** Strip a key prefix from a full GCS object name (inverse of gcsKey). */
11
+ function gcsRel(prefix, key) {
12
+ return prefix === "" ? key : key.slice(prefix.length + 1);
13
+ }
14
+ //#endregion
15
+ export { gcsKey, gcsRel };
16
+
17
+ //# sourceMappingURL=keys.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.mjs","names":[],"sources":["../src/keys.ts"],"sourcesContent":["/** Key join/split for GCS object keys. */\n\n/** Join an optional key prefix with a storage path. */\nexport function gcsKey(prefix: string, path: string): string {\n return prefix === \"\" ? path : `${prefix}/${path}`;\n}\n\n/** Strip a key prefix from a full GCS object name (inverse of gcsKey). */\nexport function gcsRel(prefix: string, key: string): string {\n return prefix === \"\" ? key : key.slice(prefix.length + 1);\n}\n"],"mappings":";;;;;;AAGA,SAAgB,OAAO,QAAgB,MAAsB;CAC3D,OAAO,WAAW,KAAK,OAAO,GAAG,OAAO,GAAG;AAC7C;;AAGA,SAAgB,OAAO,QAAgB,KAAqB;CAC1D,OAAO,WAAW,KAAK,MAAM,IAAI,MAAM,OAAO,SAAS,CAAC;AAC1D"}
@@ -0,0 +1,30 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ //#region src/lifecycle.ts
5
+ /** Build the setup/health/teardown hooks probing a GCS bucket. */
6
+ function createGcsLifecycle(ctx) {
7
+ return {
8
+ setup: async () => {
9
+ await ctx.bucket.getFiles(gcsHealthQuery(ctx.prefix));
10
+ },
11
+ teardown: async () => {
12
+ await Promise.resolve();
13
+ },
14
+ health: async () => {
15
+ await ctx.bucket.getFiles(gcsHealthQuery(ctx.prefix));
16
+ return { ok: true };
17
+ }
18
+ };
19
+ }
20
+ /** Single-object probe query scoped to the configured prefix. */
21
+ function gcsHealthQuery(prefix) {
22
+ return prefix === "" ? { maxResults: 1 } : {
23
+ prefix: `${prefix}/`,
24
+ maxResults: 1
25
+ };
26
+ }
27
+ //#endregion
28
+ export { createGcsLifecycle, gcsHealthQuery };
29
+
30
+ //# sourceMappingURL=lifecycle.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.mjs","names":[],"sources":["../src/lifecycle.ts"],"sourcesContent":["/** Setup/health/teardown lifecycle for the GCS adapter. */\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { GcsContext } from \"./client.ts\";\n\n/** Build the setup/health/teardown hooks probing a GCS bucket. */\nexport function createGcsLifecycle(ctx: GcsContext): NonNullable<StorageAdapter[\"lifecycle\"]> {\n return {\n setup: async () => {\n await ctx.bucket.getFiles(gcsHealthQuery(ctx.prefix));\n },\n teardown: async () => {\n // GCS Storage exposes no close — connections are process-global.\n await Promise.resolve();\n },\n health: async () => {\n await ctx.bucket.getFiles(gcsHealthQuery(ctx.prefix));\n return { ok: true };\n },\n };\n}\n\n/** Single-object probe query scoped to the configured prefix. */\nexport function gcsHealthQuery(prefix: string): { prefix?: string; maxResults: number } {\n return prefix === \"\" ? { maxResults: 1 } : { prefix: `${prefix}/`, maxResults: 1 };\n}\n"],"mappings":";;;;;AAKA,SAAgB,mBAAmB,KAA2D;CAC5F,OAAO;EACL,OAAO,YAAY;GACjB,MAAM,IAAI,OAAO,SAAS,eAAe,IAAI,MAAM,CAAC;EACtD;EACA,UAAU,YAAY;GAEpB,MAAM,QAAQ,QAAQ;EACxB;EACA,QAAQ,YAAY;GAClB,MAAM,IAAI,OAAO,SAAS,eAAe,IAAI,MAAM,CAAC;GACpD,OAAO,EAAE,IAAI,KAAK;EACpB;CACF;AACF;;AAGA,SAAgB,eAAe,QAAyD;CACtF,OAAO,WAAW,KAAK,EAAE,YAAY,EAAE,IAAI;EAAE,QAAQ,GAAG,OAAO;EAAI,YAAY;CAAE;AACnF"}
@@ -0,0 +1,51 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { isNotFound } from "./errors.mjs";
5
+ import { gcsKey, gcsRel } from "./keys.mjs";
6
+ //#region src/operations.ts
7
+ /** Download the full bytes stored at `path`. */
8
+ async function gcsRead(ctx, path) {
9
+ const [data] = await ctx.bucket.file(gcsKey(ctx.prefix, path)).download();
10
+ return data;
11
+ }
12
+ /** Save `data` at `path`, overwriting any existing object. */
13
+ async function gcsWrite(ctx, path, data) {
14
+ await ctx.bucket.file(gcsKey(ctx.prefix, path)).save(data);
15
+ }
16
+ /** Delete the object at `path`, ignoring not-found errors. */
17
+ async function gcsDelete(ctx, path) {
18
+ try {
19
+ await ctx.bucket.file(gcsKey(ctx.prefix, path)).delete();
20
+ } catch (error) {
21
+ if (!isNotFound(error)) throw error;
22
+ }
23
+ }
24
+ /** Return whether an object exists at `path`. */
25
+ async function gcsExists(ctx, path) {
26
+ const [exists] = await ctx.bucket.file(gcsKey(ctx.prefix, path)).exists();
27
+ return exists;
28
+ }
29
+ /** List paths under `listPrefix`, following pagination to exhaustion. */
30
+ async function gcsList(ctx, listPrefix) {
31
+ const prefix = gcsKey(ctx.prefix, listPrefix);
32
+ const results = [];
33
+ let query = prefix === "" ? { autoPaginate: false } : {
34
+ prefix,
35
+ autoPaginate: false
36
+ };
37
+ while (query !== void 0) {
38
+ const [files, nextQuery] = await ctx.bucket.getFiles(query);
39
+ for (const file of files) results.push(gcsRel(ctx.prefix, file.name));
40
+ query = nextQueryQuery(nextQuery);
41
+ }
42
+ return results;
43
+ }
44
+ function nextQueryQuery(nextQuery) {
45
+ if (!nextQuery || typeof nextQuery !== "object") return;
46
+ return nextQuery.pageToken ? nextQuery : void 0;
47
+ }
48
+ //#endregion
49
+ export { gcsDelete, gcsExists, gcsList, gcsRead, gcsWrite };
50
+
51
+ //# sourceMappingURL=operations.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operations.mjs","names":[],"sources":["../src/operations.ts"],"sourcesContent":["/** Buffered CRUD (read/write/delete/exists) plus paginated listing for GCS. */\nimport type { GcsContext } from \"./client.ts\";\nimport { isNotFound } from \"./errors.ts\";\nimport { gcsKey, gcsRel } from \"./keys.ts\";\n\n/** Download the full bytes stored at `path`. */\nexport async function gcsRead(ctx: GcsContext, path: string): Promise<Buffer> {\n const file = ctx.bucket.file(gcsKey(ctx.prefix, path));\n const [data] = await file.download();\n return data;\n}\n\n/** Save `data` at `path`, overwriting any existing object. */\nexport async function gcsWrite(ctx: GcsContext, path: string, data: Buffer): Promise<void> {\n await ctx.bucket.file(gcsKey(ctx.prefix, path)).save(data);\n}\n\n/** Delete the object at `path`, ignoring not-found errors. */\nexport async function gcsDelete(ctx: GcsContext, path: string): Promise<void> {\n try {\n await ctx.bucket.file(gcsKey(ctx.prefix, path)).delete();\n } catch (error) {\n if (!isNotFound(error)) {\n throw error;\n }\n }\n}\n\n/** Return whether an object exists at `path`. */\nexport async function gcsExists(ctx: GcsContext, path: string): Promise<boolean> {\n const [exists] = await ctx.bucket.file(gcsKey(ctx.prefix, path)).exists();\n return exists;\n}\n\n/** List paths under `listPrefix`, following pagination to exhaustion. */\nexport async function gcsList(ctx: GcsContext, listPrefix: string): Promise<string[]> {\n const prefix = gcsKey(ctx.prefix, listPrefix);\n const results: string[] = [];\n let query: Record<string, unknown> | undefined =\n prefix === \"\" ? { autoPaginate: false } : { prefix, autoPaginate: false };\n while (query !== undefined) {\n // oxlint-disable-next-line eslint/no-await-in-loop -- pagination requires sequential awaits\n const [files, nextQuery] = (await ctx.bucket.getFiles(query as never)) as unknown as [\n { name: string }[],\n Record<string, unknown> | null,\n ];\n for (const file of files) {\n results.push(gcsRel(ctx.prefix, file.name));\n }\n query = nextQueryQuery(nextQuery);\n }\n return results;\n}\n\nfunction nextQueryQuery(\n nextQuery: Record<string, unknown> | null,\n): Record<string, unknown> | undefined {\n if (!nextQuery || typeof nextQuery !== \"object\") {\n return undefined;\n }\n const token = (nextQuery as { pageToken?: string }).pageToken;\n return token ? nextQuery : undefined;\n}\n"],"mappings":";;;;;;;AAMA,eAAsB,QAAQ,KAAiB,MAA+B;CAE5E,MAAM,CAAC,QAAQ,MADF,IAAI,OAAO,KAAK,OAAO,IAAI,QAAQ,IAAI,CAC5B,CAAC,CAAC,SAAS;CACnC,OAAO;AACT;;AAGA,eAAsB,SAAS,KAAiB,MAAc,MAA6B;CACzF,MAAM,IAAI,OAAO,KAAK,OAAO,IAAI,QAAQ,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI;AAC3D;;AAGA,eAAsB,UAAU,KAAiB,MAA6B;CAC5E,IAAI;EACF,MAAM,IAAI,OAAO,KAAK,OAAO,IAAI,QAAQ,IAAI,CAAC,CAAC,CAAC,OAAO;CACzD,SAAS,OAAO;EACd,IAAI,CAAC,WAAW,KAAK,GACnB,MAAM;CAEV;AACF;;AAGA,eAAsB,UAAU,KAAiB,MAAgC;CAC/E,MAAM,CAAC,UAAU,MAAM,IAAI,OAAO,KAAK,OAAO,IAAI,QAAQ,IAAI,CAAC,CAAC,CAAC,OAAO;CACxE,OAAO;AACT;;AAGA,eAAsB,QAAQ,KAAiB,YAAuC;CACpF,MAAM,SAAS,OAAO,IAAI,QAAQ,UAAU;CAC5C,MAAM,UAAoB,CAAC;CAC3B,IAAI,QACF,WAAW,KAAK,EAAE,cAAc,MAAM,IAAI;EAAE;EAAQ,cAAc;CAAM;CAC1E,OAAO,UAAU,KAAA,GAAW;EAE1B,MAAM,CAAC,OAAO,aAAc,MAAM,IAAI,OAAO,SAAS,KAAc;EAIpE,KAAK,MAAM,QAAQ,OACjB,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,IAAI,CAAC;EAE5C,QAAQ,eAAe,SAAS;CAClC;CACA,OAAO;AACT;AAEA,SAAS,eACP,WACqC;CACrC,IAAI,CAAC,aAAa,OAAO,cAAc,UACrC;CAGF,OADe,UAAqC,YACrC,YAAY,KAAA;AAC7B"}
@@ -0,0 +1,30 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { gcsKey } from "./keys.mjs";
5
+ import { pipeline } from "node:stream/promises";
6
+ //#region src/streams.ts
7
+ /** Stream `stream` to `path`, deleting the partial object when the pipeline fails. */
8
+ async function gcsWriteStream(ctx, path, stream) {
9
+ const file = ctx.bucket.file(gcsKey(ctx.prefix, path));
10
+ const dest = file.createWriteStream({ resumable: false });
11
+ try {
12
+ await pipeline(stream, dest);
13
+ } catch (error) {
14
+ try {
15
+ await file.delete({ ignoreNotFound: true });
16
+ } catch {}
17
+ throw error;
18
+ }
19
+ }
20
+ /** Open a readable for the bytes at `path`; rejects when the object is missing. */
21
+ async function gcsReadStream(ctx, path) {
22
+ const file = ctx.bucket.file(gcsKey(ctx.prefix, path));
23
+ const [exists] = await file.exists();
24
+ if (!exists) throw new Error(`No object at "${path}"`);
25
+ return file.createReadStream();
26
+ }
27
+ //#endregion
28
+ export { gcsReadStream, gcsWriteStream };
29
+
30
+ //# sourceMappingURL=streams.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streams.mjs","names":[],"sources":["../src/streams.ts"],"sourcesContent":["/** Streaming reads/writes for GCS objects, with failure cleanup. */\nimport type { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport type { GcsContext } from \"./client.ts\";\nimport { gcsKey } from \"./keys.ts\";\n\n/** Stream `stream` to `path`, deleting the partial object when the pipeline fails. */\nexport async function gcsWriteStream(\n ctx: GcsContext,\n path: string,\n stream: Readable,\n): Promise<void> {\n const file = ctx.bucket.file(gcsKey(ctx.prefix, path));\n const dest = file.createWriteStream({ resumable: false });\n try {\n await pipeline(stream, dest);\n } catch (error) {\n try {\n await file.delete({ ignoreNotFound: true });\n } catch {\n // ignore cleanup failure, rethrow original error\n }\n throw error;\n }\n}\n\n/** Open a readable for the bytes at `path`; rejects when the object is missing. */\nexport async function gcsReadStream(ctx: GcsContext, path: string): Promise<Readable> {\n const file = ctx.bucket.file(gcsKey(ctx.prefix, path));\n const [exists] = await file.exists();\n if (!exists) {\n throw new Error(`No object at \"${path}\"`);\n }\n return file.createReadStream();\n}\n"],"mappings":";;;;;;;AAOA,eAAsB,eACpB,KACA,MACA,QACe;CACf,MAAM,OAAO,IAAI,OAAO,KAAK,OAAO,IAAI,QAAQ,IAAI,CAAC;CACrD,MAAM,OAAO,KAAK,kBAAkB,EAAE,WAAW,MAAM,CAAC;CACxD,IAAI;EACF,MAAM,SAAS,QAAQ,IAAI;CAC7B,SAAS,OAAO;EACd,IAAI;GACF,MAAM,KAAK,OAAO,EAAE,gBAAgB,KAAK,CAAC;EAC5C,QAAQ,CAER;EACA,MAAM;CACR;AACF;;AAGA,eAAsB,cAAc,KAAiB,MAAiC;CACpF,MAAM,OAAO,IAAI,OAAO,KAAK,OAAO,IAAI,QAAQ,IAAI,CAAC;CACrD,MAAM,CAAC,UAAU,MAAM,KAAK,OAAO;CACnC,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,iBAAiB,KAAK,EAAE;CAE1C,OAAO,KAAK,iBAAiB;AAC/B"}
@@ -0,0 +1,24 @@
1
+ import __tsdown_shims_path from 'node:path';
2
+ import __tsdown_shims_url from 'node:url';
3
+ import { Storage } from "@google-cloud/storage";
4
+ //#region src/types.d.ts
5
+ /** Options for configuring a GCS storage adapter. */
6
+ interface GcsStorageOptions {
7
+ /** GCS bucket name. */
8
+ bucket: string;
9
+ /** Optional key prefix applied to all stored objects. */
10
+ prefix?: string;
11
+ /** Pre-configured GCS client. Defaults to a client built from the other options. */
12
+ client?: Storage;
13
+ /** GCP project ID (defaults to ADC). */
14
+ projectId?: string;
15
+ /** Path to service-account JSON key file. */
16
+ keyFilename?: string;
17
+ /** Service-account credentials object. */
18
+ credentials?: Record<string, unknown>;
19
+ /** Custom API endpoint (e.g. emulator: http://localhost:9090). */
20
+ apiEndpoint?: string;
21
+ }
22
+ //#endregion
23
+ export { GcsStorageOptions };
24
+ //# sourceMappingURL=types.d.mts.map
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@storyshelf/storage-gcs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.5.3",
4
+ "description": "Google Cloud Storage adapter for StoryShelf.",
5
+ "keywords": [
6
+ "gcs",
7
+ "google-cloud-storage",
8
+ "self-hosted",
9
+ "storage",
10
+ "storybook"
11
+ ],
12
+ "homepage": "https://github.com/GuptaSiddhant/StoryShelf#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/GuptaSiddhant/StoryShelf/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Siddhant Gupta",
19
+ "url": "https://guptasiddhant.com"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/GuptaSiddhant/StoryShelf.git",
24
+ "directory": "packages/storage-gcs"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "type": "module",
30
+ "sideEffects": false,
31
+ "types": "./dist/index.d.mts",
32
+ "exports": {
33
+ ".": {
34
+ "source": "./src/index.ts",
35
+ "default": "./dist/index.mjs"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "publishConfig": {
40
+ "exports": {
41
+ ".": "./dist/index.mjs",
42
+ "./package.json": "./package.json"
43
+ },
44
+ "access": "public"
45
+ },
46
+ "scripts": {
47
+ "build": "tsdown",
48
+ "dev": "tsdown -w",
49
+ "fmt": "oxfmt ./src",
50
+ "lint": "oxlint --type-aware --type-check ./src",
51
+ "test": "vitest run",
52
+ "prepublishOnly": "nub run build"
53
+ },
54
+ "dependencies": {
55
+ "@google-cloud/storage": "^7.13.0",
56
+ "@storyshelf/core": "0.5.3"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^26.0.0",
60
+ "@vitest/coverage-v8": "^4.0.0",
61
+ "oxfmt": "^0.65.0",
62
+ "oxlint": "^1.80.0",
63
+ "oxlint-tsgolint": "^7.0.2001",
64
+ "tsdown": "^0.22.14",
65
+ "typescript": "^7.0.2",
66
+ "vitest": "^5.0.0"
67
+ },
68
+ "jsr": {
69
+ "runtimeCompat": {
70
+ "node": true,
71
+ "deno": true,
72
+ "browser": false,
73
+ "workerd": false,
74
+ "bun": true
75
+ }
76
+ }
77
+ }