Skip to content

feat: add getDeployStore method #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,25 +126,23 @@ know whether an item is still relevant or safe to delete.
But sometimes it's useful to have data pegged to a specific deploy, and shift to the platform the responsibility of
managing that data — keep it as long as the deploy is around, and wipe it if the deploy is deleted.

You can opt-in to this behavior by supplying a `deployID` instead of a `name` to the `getStore` method.
You can opt-in to this behavior by creating the store using the `getDeployStore` method.

```ts
import { assert } from 'node:assert'

import { getStore } from '@netlify/blobs'
import { getDeployStore } from '@netlify/blobs'

// Using API access
const store1 = getStore({
const store1 = getDeployStore({
deployID: 'MY_DEPLOY_ID',
token: 'MY_API_TOKEN',
})

await store1.set('my-key', 'my value')

// Using environment-based configuration
const store2 = getStore({
deployID: 'MY_DEPLOY_ID',
})
const store2 = getDeployStore()

assert.equal(await store2.get('my-key'), 'my value')
```
Expand Down
115 changes: 54 additions & 61 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,7 @@
import { Buffer } from 'node:buffer'
import { env } from 'node:process'

import { EnvironmentContext, getEnvironmentContext, MissingBlobsEnvironmentError } from './environment.ts'
import { fetchAndRetry } from './retry.ts'
import { BlobInput, Fetcher, HTTPMethod } from './types.ts'

/**
* The name of the environment variable that holds the context in a Base64,
* JSON-encoded object. If we ever need to change the encoding or the shape
* of this object, we should bump the version and create a new variable, so
* that the client knows how to consume the data and can advise the user to
* update the client if needed.
*/
export const NETLIFY_CONTEXT_VARIABLE = 'NETLIFY_BLOBS_CONTEXT'

export interface Context {
apiURL?: string
edgeURL?: string
siteID?: string
token?: string
}

interface PopulatedContext extends Context {
siteID: string
token: string
}

interface MakeStoreRequestOptions {
body?: BlobInput | null
headers?: Record<string, string>
Expand All @@ -33,59 +10,45 @@ interface MakeStoreRequestOptions {
storeName: string
}

export interface ClientOptions {
apiURL?: string
edgeURL?: string
fetch?: Fetcher
siteID: string
token: string
}

export class Client {
private context?: Context
private apiURL?: string
private edgeURL?: string
private fetch?: Fetcher
private siteID: string
private token: string

constructor(context?: Context, fetch?: Fetcher) {
this.context = context ?? {}
constructor({ apiURL, edgeURL, fetch, siteID, token }: ClientOptions) {
this.apiURL = apiURL
this.edgeURL = edgeURL
this.fetch = fetch
}

private static getEnvironmentContext() {
if (!env[NETLIFY_CONTEXT_VARIABLE]) {
return
}

const data = Buffer.from(env[NETLIFY_CONTEXT_VARIABLE], 'base64').toString()

try {
return JSON.parse(data) as Context
} catch {
// no-op
}
}

private getContext() {
const context = {
...Client.getEnvironmentContext(),
...this.context,
}

if (!context.siteID || !context.token) {
throw new Error(`The blob store is unavailable because it's missing required configuration properties`)
}

return context as PopulatedContext
this.siteID = siteID
this.token = token
}

private async getFinalRequest(storeName: string, key: string, method: string) {
const context = this.getContext()
const encodedKey = encodeURIComponent(key)

if ('edgeURL' in context) {
if (this.edgeURL) {
return {
headers: {
authorization: `Bearer ${context.token}`,
authorization: `Bearer ${this.token}`,
},
url: `${context.edgeURL}/${context.siteID}/${storeName}/${encodedKey}`,
url: `${this.edgeURL}/${this.siteID}/${storeName}/${encodedKey}`,
}
}

const apiURL = `${context.apiURL ?? 'https://api.netlify.com'}/api/v1/sites/${
context.siteID
const apiURL = `${this.apiURL ?? 'https://api.netlify.com'}/api/v1/sites/${
this.siteID
}/blobs/${encodedKey}?context=${storeName}`
const headers = { authorization: `Bearer ${context.token}` }
const headers = { authorization: `Bearer ${this.token}` }
const fetch = this.fetch ?? globalThis.fetch
const res = await fetch(apiURL, { headers, method })

Expand Down Expand Up @@ -137,3 +100,33 @@ export class Client {
return res
}
}

/**
* Merges a set of options supplied by the user when getting a reference to a
* store with a context object found in the environment.
*
* @param options User-supplied options
* @param contextOverride Context to be used instead of the environment object
*/
export const getClientOptions = (
options: Partial<ClientOptions>,
contextOverride?: EnvironmentContext,
): ClientOptions => {
const context = contextOverride ?? getEnvironmentContext()
const siteID = context.siteID ?? options.siteID
const token = context.token ?? options.token

if (!siteID || !token) {
throw new MissingBlobsEnvironmentError(['siteID', 'token'])
}

const clientOptions = {
apiURL: context.apiURL ?? options.apiURL,
edgeURL: context.edgeURL ?? options.edgeURL,
fetch: options.fetch,
siteID,
token,
}

return clientOptions
}
50 changes: 50 additions & 0 deletions src/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Buffer } from 'node:buffer'
import { env } from 'node:process'

/**
* The name of the environment variable that holds the context in a Base64,
* JSON-encoded object. If we ever need to change the encoding or the shape
* of this object, we should bump the version and create a new variable, so
* that the client knows how to consume the data and can advise the user to
* update the client if needed.
*/
const NETLIFY_CONTEXT_VARIABLE = 'NETLIFY_BLOBS_CONTEXT'

/**
* The context object that we expect in the environment.
*/
export interface EnvironmentContext {
apiURL?: string
deployID?: string
edgeURL?: string
siteID?: string
token?: string
}

export const getEnvironmentContext = (): EnvironmentContext => {
if (!env[NETLIFY_CONTEXT_VARIABLE]) {
return {}
}

const data = Buffer.from(env[NETLIFY_CONTEXT_VARIABLE], 'base64').toString()

try {
return JSON.parse(data) as EnvironmentContext
} catch {
// no-op
}

return {}
}

export class MissingBlobsEnvironmentError extends Error {
constructor(requiredProperties: string[]) {
super(
`The environment has not been configured to use Netlify Blobs. To use it manually, supply the following properties when creating a store: ${requiredProperties.join(
', ',
)}`,
)

this.name = 'MissingBlobsEnvironmentError'
}
}
Loading