diff --git a/package.json b/package.json index 288a81454..cdead3fce 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "lru-cache": "^10.4.3", "node-fetch-native": "^1.6.6", "ofetch": "^1.4.1", - "ufo": "^1.6.1" + "ufo": "^1.6.1", + "undio": "^0.2.0" }, "devDependencies": { "@azure/app-configuration": "^1.9.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 314d9969f..01a617b86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: ufo: specifier: ^1.6.1 version: 1.6.1 + undio: + specifier: ^0.2.0 + version: 0.2.0 devDependencies: '@azure/app-configuration': specifier: ^1.9.0 @@ -4459,6 +4462,9 @@ packages: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} + undio@0.2.0: + resolution: {integrity: sha512-1LH824ipsUNqX1qsO6qpcusv0oGPlfFWVykwWq5jJB0Mq6x4kEHO/izSq2KLjGZvOosEd91+HXoxYUSoVI0zPg==} + unenv@2.0.0-rc.15: resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==} @@ -9374,6 +9380,8 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 + undio@0.2.0: {} + unenv@2.0.0-rc.15: dependencies: defu: 6.1.4 diff --git a/src/storage.ts b/src/storage.ts index eb6578471..eb4986f57 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -17,6 +17,7 @@ import { filterKeyByDepth, filterKeyByBase, } from "./utils"; +import { toBlob, toReadableStream, toUint8Array } from "undio"; interface StorageCTX { mounts: Record; @@ -172,12 +173,71 @@ export function createStorage( const { relativeKey, driver } = getMount(key); return asyncCall(driver.hasItem, relativeKey, opts); }, - getItem(key: string, opts = {}) { + getItem: async ( + key: string, + opts: TransactionOptions & { + type?: "json" | "text" | "bytes" | "stream" | "blob"; + } = {} + ) => { key = normalizeKey(key); const { relativeKey, driver } = getMount(key); - return asyncCall(driver.getItem, relativeKey, opts).then( - (value) => destr(value) as StorageValue - ); + + const type = opts.type; + + if (type === "bytes" || type === "stream" || type === "blob") { + const raw = driver.getItemRaw + ? await asyncCall(driver.getItemRaw, relativeKey, opts) + : await asyncCall(driver.getItem, relativeKey, opts).then((val) => + deserializeRaw(val) + ); + + if (raw === null || raw === undefined) { + return null; + } + + switch (type) { + case "bytes": { + return await toUint8Array(raw); + } + case "blob": { + if (typeof Blob === "undefined") { + throw new TypeError("Blob is not supported in this environment."); + } + return await toBlob(raw); + } + case "stream": { + if (typeof ReadableStream === "undefined") { + throw new TypeError( + "ReadableStream is not supported in this environment." + ); + } + return await toReadableStream(raw); + } + default: { + throw new Error(`Invalid binary type: ${type}`); + } + } + } + + const value = await asyncCall(driver.getItem, relativeKey, opts); + if (value == null) { + return null; + } + + switch (type) { + case "text": { + return typeof value === "string" ? value : String(value); + } + case "json": { + if (typeof value === "string") { + return JSON.parse(value); + } + return value; + } + default: { + return destr(value); + } + } }, getItems( items: (string | { key: string; options?: TransactionOptions })[], @@ -473,7 +533,8 @@ export function createStorage( }, // Aliases keys: (base, opts = {}) => storage.getKeys(base, opts), - get: (key: string, opts = {}) => storage.getItem(key, opts), + get: ((key: string, opts: TransactionOptions = {}) => + storage.getItem(key, opts)) as Storage["get"], set: (key: string, value: T, opts = {}) => storage.setItem(key, value, opts), has: (key: string, opts = {}) => storage.hasItem(key, opts), diff --git a/src/types.ts b/src/types.ts index d347dbd06..1885e90a8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,6 +6,12 @@ type MaybePromise = T | Promise; type MaybeDefined = T extends any ? T : any; +// prettier-ignore +export type JSONValue = string | number | boolean | null | JSONObject | JSONArray; +// prettier-ignore +export interface JSONObject { [key: string]: JSONValue; } +export type JSONArray = JSONValue[]; + export type Unwatch = () => MaybePromise; export interface StorageMeta { @@ -96,14 +102,40 @@ export interface Storage { getItem< U extends Extract, - K extends string & keyof StorageItemMap, + K extends keyof StorageItemMap, >( key: K, - ops?: TransactionOptions + opts?: TransactionOptions & { type?: undefined } ): Promise | null>; + + getItem( + key: string, + opts: { type: "text" } & TransactionOptions + ): Promise; + + getItem( + key: string, + opts: { type: "json" } & TransactionOptions + ): Promise; + + getItem( + key: string, + opts: { type: "bytes" } & TransactionOptions + ): Promise; + + getItem( + key: string, + opts: { type: "stream" } & TransactionOptions + ): Promise; + + getItem( + key: string, + opts: { type: "blob" } & TransactionOptions + ): Promise; + getItem>( key: string, - opts?: TransactionOptions + opts?: TransactionOptions & { type?: undefined } ): Promise; /** @experimental */ diff --git a/test/storage.test.ts b/test/storage.test.ts index d5a2542d7..db54939b0 100644 --- a/test/storage.test.ts +++ b/test/storage.test.ts @@ -289,3 +289,57 @@ describe("Regression", () => { ]); }); }); + +describe("get() with type option", () => { + const storage = createStorage(); + + it("should get JSON object with type=json", async () => { + const obj = { foo: "bar", num: 42 }; + await storage.setItem("json-key", obj); + const result = await storage.get("json-key", { type: "json" }); + expect(result).toEqual(obj); + }); + + it("should get string with type=text", async () => { + await storage.setItem("text-key", "hello world"); + const result = await storage.get("text-key", { type: "text" }); + expect(result).toBe("hello world"); + }); + + it("should get bytes with type=bytes", async () => { + const bytes = new Uint8Array([1, 2, 3, 4]); + await storage.setItemRaw("bytes-key", bytes); + const result = await storage.get("bytes-key", { type: "bytes" }); + const len = result?.length || result?.byteLength; + expect(len).toBe(4); + }); + + it("should get bytes with type=stream", async () => { + const bytes = new Uint8Array([1, 2, 3, 4]); + await storage.setItemRaw("stream-key", bytes); + const result = await storage.get("stream-key", { type: "stream" }); + expect(result).toBeInstanceOf(ReadableStream); + const reader = result?.getReader(); + if (!reader) { + throw new Error("Reader is not defined"); + } + const { done, value } = await reader.read(); + expect(done).toBe(false); + expect(value).toEqual(bytes); + const len = value?.length || value?.byteLength; + expect(len).toBe(4); + + const { done: doneAfter, value: valueAfter } = await reader.read(); + expect(doneAfter).toBe(true); + expect(valueAfter).toBeUndefined(); + }); + + it("should get bytes with type=blob", async () => { + const bytes = new Uint8Array([1, 2, 3, 4]); + await storage.setItemRaw("blob-key", bytes); + const result = await storage.get("blob-key", { type: "blob" }); + expect(result).toBeInstanceOf(Blob); + const arrayBuffer = await result?.arrayBuffer(); + expect(new Uint8Array(arrayBuffer || [])).toEqual(bytes); + }); +});