-
Notifications
You must be signed in to change notification settings - Fork 140
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add
netlify-blobs
driver (#337)
Co-authored-by: Pooya Parsa <[email protected]>
- Loading branch information
Showing
8 changed files
with
239 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,3 +9,4 @@ tmp | |
/test.* | ||
__* | ||
.vercel | ||
.netlify |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# Netlify Blobs | ||
|
||
Store data in a [Netlify Blobs](https://docs.netlify.com/blobs/overview/) store. This is supported in both edge and Node.js runtimes, as well at during builds. | ||
|
||
::alert{type="warning"} | ||
Netlify Blobs are in beta. | ||
:: | ||
|
||
```js | ||
import { createStorage } from "unstorage"; | ||
import netlifyBlobsDriver from "unstorage/drivers/netlify-blobs"; | ||
|
||
const storage = createStorage({ | ||
driver: netlifyBlobsDriver({ | ||
name: "blob-store-name", | ||
}), | ||
}); | ||
``` | ||
|
||
You can create a deploy-scoped store by settings `deployScoped` option to `true`. This will mean that the deploy only has access to its own store. The store is managed alongside the deploy, with the same deploy previews, deletes, and rollbacks. | ||
|
||
```js | ||
import { createStorage } from "unstorage"; | ||
import netlifyBlobsDriver from "unstorage/drivers/netlify-blobs"; | ||
|
||
const storage = createStorage({ | ||
driver: netlifyBlobsDriver({ | ||
deployScoped: true, | ||
}), | ||
}); | ||
``` | ||
|
||
To use, you will need to install `@netlify/blobs` as dependency or devDependency in your project: | ||
|
||
```json | ||
{ | ||
"devDependencies": { | ||
"@netlify/blobs": "*" | ||
} | ||
} | ||
``` | ||
|
||
**Options:** | ||
|
||
- `name` - The name of the store to use. It is created if needed. This is required except for deploy-scoped stores. | ||
- `deployScoped` - If set to `true`, the store is scoped to the deploy. This means that it is only available from that deploy, and will be deleted or rolled-back alongside it. | ||
- `siteID` - Required during builds, where it is available as `constants.SITE_ID`. At runtime this is set automatically. | ||
- `token` - Required during builds, where it is available as `constants.NETLIFY_API_TOKEN`. At runtime this is set automatically. | ||
|
||
**Advanced options:** | ||
|
||
These are not normally needed, but are available for advanced use cases or for use in unit tests. | ||
|
||
- `apiURL` | ||
- `edgeURL` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { createError, createRequiredError, defineDriver } from "./utils"; | ||
import { getStore, getDeployStore } from "@netlify/blobs"; | ||
import type { | ||
Store, | ||
BlobResponseType, | ||
SetOptions, | ||
ListOptions, | ||
} from "@netlify/blobs"; | ||
import { fetch } from "ofetch"; | ||
|
||
const DRIVER_NAME = "netlify-blobs"; | ||
|
||
type GetOptions = { type?: BlobResponseType }; | ||
|
||
export interface NetlifyBaseStoreOptions { | ||
/** The name of the store to use. It is created if needed. This is required except for deploy-scoped stores. */ | ||
name?: string; | ||
/** If set to `true`, the store is scoped to the deploy. This means that it is only available from that deploy, and will be deleted or rolled-back alongside it. */ | ||
deployScoped?: boolean; | ||
/** Required during builds, where it is available as `constants.SITE_ID`. At runtime this is set automatically. */ | ||
siteID?: string; | ||
/** Required during builds, where it is available as `constants.NETLIFY_API_TOKEN`. At runtime this is set automatically. */ | ||
token?: string; | ||
/** Used for advanced use cases and unit tests */ | ||
apiURL?: string; | ||
/** Used for advanced use cases and unit tests */ | ||
edgeURL?: string; | ||
} | ||
|
||
export interface NetlifyDeployStoreOptions extends NetlifyBaseStoreOptions { | ||
name?: never; | ||
deployScoped: true; | ||
deployID?: string; | ||
} | ||
|
||
export interface NetlifyNamedStoreOptions extends NetlifyBaseStoreOptions { | ||
name: string; | ||
deployScoped?: false; | ||
} | ||
|
||
export type NetlifyStoreOptions = | ||
| NetlifyDeployStoreOptions | ||
| NetlifyNamedStoreOptions; | ||
|
||
export default defineDriver( | ||
({ deployScoped, name, ...opts }: NetlifyStoreOptions) => { | ||
let store: Store; | ||
|
||
const getClient = () => { | ||
if (!store) { | ||
if (deployScoped) { | ||
if (name) { | ||
throw createError( | ||
DRIVER_NAME, | ||
"deploy-scoped stores cannot have a name" | ||
); | ||
} | ||
store = getDeployStore({ fetch, ...opts }); | ||
} else { | ||
if (!name) { | ||
throw createRequiredError(DRIVER_NAME, "name"); | ||
} | ||
// Ensures that reserved characters are encoded | ||
store = getStore({ name: encodeURIComponent(name), fetch, ...opts }); | ||
} | ||
} | ||
return store; | ||
}; | ||
|
||
return { | ||
name: DRIVER_NAME, | ||
options: {}, | ||
async hasItem(key) { | ||
return getClient().getMetadata(key).then(Boolean); | ||
}, | ||
getItem: (key, tops?: GetOptions) => { | ||
// @ts-expect-error has trouble with the overloaded types | ||
return getClient().get(key, tops); | ||
}, | ||
getMeta(key) { | ||
return getClient().getMetadata(key); | ||
}, | ||
getItemRaw(key, topts?: GetOptions) { | ||
// @ts-expect-error has trouble with the overloaded types | ||
return getClient().get(key, { type: topts?.type ?? "arrayBuffer" }); | ||
}, | ||
setItem(key, value, topts?: SetOptions) { | ||
return getClient().set(key, value, topts); | ||
}, | ||
setItemRaw(key, value: string | ArrayBuffer | Blob, topts?: SetOptions) { | ||
return getClient().set(key, value, topts); | ||
}, | ||
removeItem(key) { | ||
return getClient().delete(key); | ||
}, | ||
async getKeys( | ||
base?: string, | ||
tops?: Omit<ListOptions, "prefix" | "paginate"> | ||
) { | ||
return (await getClient().list({ ...tops, prefix: base })).blobs.map( | ||
(item) => item.key | ||
); | ||
}, | ||
async clear(base?: string) { | ||
const client = getClient(); | ||
return Promise.allSettled( | ||
(await client.list({ prefix: base })).blobs.map((item) => | ||
client.delete(item.key) | ||
) | ||
).then(() => {}); | ||
}, | ||
}; | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { afterAll, beforeAll, describe } from "vitest"; | ||
import driver from "../../src/drivers/netlify-blobs"; | ||
import { testDriver } from "./utils"; | ||
import { BlobsServer } from "@netlify/blobs"; | ||
import { resolve } from "path"; | ||
import { rm, mkdir } from "node:fs/promises"; | ||
|
||
describe("drivers: netlify-blobs", async () => { | ||
const dataDir = resolve(__dirname, "tmp/netlify-blobs"); | ||
await rm(dataDir, { recursive: true, force: true }).catch(() => {}); | ||
await mkdir(dataDir, { recursive: true }); | ||
|
||
let server: BlobsServer; | ||
const token = "mock"; | ||
const siteID = "1"; | ||
beforeAll(async () => { | ||
server = new BlobsServer({ | ||
directory: dataDir, | ||
debug: !true, | ||
token, | ||
port: 8971, | ||
}); | ||
await server.start(); | ||
}); | ||
|
||
testDriver({ | ||
driver: driver({ | ||
name: "test", | ||
edgeURL: `http://localhost:8971`, | ||
token, | ||
siteID, | ||
}), | ||
}); | ||
|
||
testDriver({ | ||
driver: driver({ | ||
deployScoped: true, | ||
edgeURL: `http://localhost:8971`, | ||
token, | ||
siteID, | ||
deployID: "test", | ||
}), | ||
}); | ||
|
||
afterAll(async () => { | ||
await server.stop(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters