diff --git a/.eslintignore b/.eslintignore index 32017e04..4a3a4bab 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,5 @@ dist node_modules -.hub +.data .output .nuxt diff --git a/.gitignore b/.gitignore index 8242d1ae..7a3eb22b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,9 +24,6 @@ dist .build-* .netlify -# NuxtHUb -.hub - # Env .env diff --git a/docs/content/docs/1.getting-started/2.installation.md b/docs/content/docs/1.getting-started/2.installation.md index 51a8bc37..c5667690 100644 --- a/docs/content/docs/1.getting-started/2.installation.md +++ b/docs/content/docs/1.getting-started/2.installation.md @@ -50,7 +50,7 @@ bun add --dev @nuxthub/core :: -3. Add it to your `modules` section in your `nuxt.config`: +3. Add `@nuxthub/core` to your `modules` section in your `nuxt.config`: ```ts [nuxt.config.ts] export default defineNuxtConfig({ @@ -61,7 +61,7 @@ export default defineNuxtConfig({ That's it! You can now use the NuxtHub module in your Nuxt project. ::callout -The module will create a `.hub` directory in your project root, which contains the necessary configuration files and resources for the module to work. It will also add it to the `.gitignore` file to avoid committing it to your repository. +The module will create a `.data/hub` directory in your project root, which contains the necessary configuration files and resources for the module to work. It will also add it to the `.gitignore` file to avoid committing it to your repository. :: ## Options diff --git a/docs/content/docs/2.storage/2.kv.md b/docs/content/docs/2.storage/2.kv.md index 489f639b..b8706036 100644 --- a/docs/content/docs/2.storage/2.kv.md +++ b/docs/content/docs/2.storage/2.kv.md @@ -3,60 +3,86 @@ title: KV description: How to use key-value data storage with NuxtHub. --- -NuxtHub KV is a layer to [Cloudflare Workers KV](https://developers.cloudflare.com/kv){target=_blank}, a global, low-latency, key-value data storage. - - - -Once properly configured, NuxtHub module exposes a server composable to the application. +NuxtHub KV is a layer on top of [Cloudflare Workers KV](https://developers.cloudflare.com/kv), a global, low-latency, key-value data storage. ## `hubKV()` -Server composable that returns a [Storage](https://unstorage.unjs.io/getting-started/usage#interface){target=_blank}. +Server method that returns an [unstorage instance](https://unstorage.unjs.io/guide#interface) with `keys()`, `get()`, `set()` and `del()` aliases. -### `getKeys()` +### `keys()` -Retrieves all keys from the storage. +Retrieves all keys from the KV storage (alias of `getKeys()`). -```ts[/api/kv/index.get.ts] -export default eventHandler(async () => { - return await hubKV().getKeys() -}) +```ts +const keys = await hubKV().keys() +/* +[ + 'react', + 'react:gatsby', + 'react:next', + 'vue', + 'vue:nuxt', + 'vue:quasar' +] ``` -### `getItem()` +To get the keys starting with a specific prefix, you can pass the prefix as an argument. -Retrieves an item from the storage. +```ts +const vueKeys = await hubKV().keys('vue') +/* +[ + 'vue:nuxt', + 'vue:quasar' +] +*/ +``` -```ts[/api/kv/[key\\].get.ts] -export default eventHandler(async () => { - const { key } = getRouterParams(event) +### `get()` - return await hubKV().getItem(key) -}) +Retrieves an item from the Key-Value storage (alias of `getItem()`). + +```ts +const vue = await hubKV().get('vue') +/* +{ + year: 2014 +} +*/ ``` -### `setItem()` +### `set()` + +Puts an item in the storage (alias of `setItem()`) -Puts an item in the storage. +```ts +await hubKV().set('vue', { year: 2014 }) +``` -```ts[/api/kv/index.put.ts] -export default eventHandler(async () => { - const { key, value } = await readBody(event) +You can delimit the key with a `:` to create a namespace: - return await hubKV().setItem(key, value) -}) +```ts +await hubKV().set('vue:nuxt', { year: 2016 }) ``` -### `deleteItem()` +### `has()` + +Checks if an item exists in the storage (alias of `hasItem()`) -Deletes an item from the storage. +```ts +const hasAngular = await hubKV().has('angular') +``` -```ts[/api/kv/[key\\].delete.ts] -export default eventHandler(async (event) => { - const { key } = getRouterParams(event) +### `del()` - await hubKV().removeItem(key) +Delete an item from the storage (alias of `removeItem()`) - return { key } -}) +```ts +await hubKV().del('react') ``` + +### `...()` + +::callout +You can use any other method from [unstorage](https://unstorage.unjs.io/guide#interface) as well. +:: diff --git a/playground/server/api/kv/index.get.ts b/playground/server/api/kv/index.get.ts index ee3c041c..bbacce32 100644 --- a/playground/server/api/kv/index.get.ts +++ b/playground/server/api/kv/index.get.ts @@ -1,11 +1,11 @@ export default eventHandler(async () => { // List entries for the current user - const storage = hubKV() + const kv = hubKV() - const keys = await storage.getKeys() + const keys = await kv.keys() // const items = await storage.getItems(keys) const items = await Promise.all(keys.map(async (key) => { - const value = await storage.getItem(key) + const value = await kv.get(key) return { key, value } })) return items diff --git a/playground/server/api/test.ts b/playground/server/api/test.ts index 76c52da0..a4f07f02 100644 --- a/playground/server/api/test.ts +++ b/playground/server/api/test.ts @@ -1,5 +1,15 @@ export default eventHandler(async () => { - const db = hubDatabase() + const kv = hubKV() + + await kv.set('vue', { year: 2014 }) + await kv.set('vue:nuxt', { year: 2016 }) + await kv.set('vue:quasar', { version: 2015 }) + await kv.set('react', { version: 2013 }) + await kv.set('react:next', { version: 2016 }) + await kv.set('react:gatsby', { version: 2015 }) + + return kv.keys() + // const db = hubDatabase() // return useProjectKV(projectUrl).getKeys() // return await db.prepare('SELECT * from todos').all() // return await db.prepare("SELECT * from todos").first() diff --git a/src/module.ts b/src/module.ts index da85d18d..89af6003 100644 --- a/src/module.ts +++ b/src/module.ts @@ -136,14 +136,14 @@ export default defineNuxtModule({ logger.info(`Remote storage available: ${Object.keys(manifest.storage).filter(k => manifest.storage[k]).map(k => `\`${k}\``).join(', ')} `) return } else { - log.info('Using local data from `.hub/`') + log.info('Using local data from `.data/hub`') } // Local development without remote connection - // Create the .hub/ directory - const hubDir = join(rootDir, './.hub') + // Create the .data/hub/ directory + const hubDir = join(rootDir, './.data/hub') try { - await mkdir(hubDir) + await mkdir(hubDir, { recursive: true }) } catch (e: any) { if (e.errno === -17) { // File already exists @@ -155,8 +155,8 @@ export default defineNuxtModule({ // Add it to .gitignore const gitignorePath = join(workspaceDir , '.gitignore') const gitignore = await readFile(gitignorePath, 'utf-8').catch(() => '') - if (!gitignore.includes('.hub')) { - await writeFile(gitignorePath, `${gitignore ? gitignore + '\n' : gitignore}.hub`, 'utf-8') + if (!gitignore.includes('.data')) { + await writeFile(gitignorePath, `${gitignore ? gitignore + '\n' : gitignore}.data`, 'utf-8') } // Generate the wrangler.toml file diff --git a/src/runtime/server/api/_hub/analytics/index.put.ts b/src/runtime/server/api/_hub/analytics/index.put.ts index c6b96d2c..c3689296 100644 --- a/src/runtime/server/api/_hub/analytics/index.put.ts +++ b/src/runtime/server/api/_hub/analytics/index.put.ts @@ -1,14 +1,14 @@ import type { AnalyticsEngineDataPoint } from '@cloudflare/workers-types/experimental' import { eventHandler, readValidatedBody } from 'h3' import { z } from 'zod' -import { useAnalytics } from '../../../utils/analytics' +import { hubAnalytics } from '../../../utils/analytics' export default eventHandler(async (event) => { const { data } = await readValidatedBody(event, z.object({ data: z.custom() }).parse) - await useAnalytics().put(data) + await hubAnalytics().put(data) return true }) diff --git a/src/runtime/server/utils/analytics.ts b/src/runtime/server/utils/analytics.ts index 97626429..77e5da29 100644 --- a/src/runtime/server/utils/analytics.ts +++ b/src/runtime/server/utils/analytics.ts @@ -21,10 +21,10 @@ function _useDataset() { throw createError(`Missing Cloudflare ${name} binding (Analytics Engine)`) } -export function useAnalytics() { +export function hubAnalytics() { const hub = useRuntimeConfig().hub if (import.meta.dev && hub.projectUrl) { - return useProxyAnalytics(hub.projectUrl, hub.projectSecretKey || hub.userToken) + return proxyHubAnalytics(hub.projectUrl, hub.projectSecretKey || hub.userToken) } const dataset = _useDataset() @@ -36,7 +36,7 @@ export function useAnalytics() { } } -export function useProxyAnalytics(projectUrl: string, secretKey?: string) { +export function proxyHubAnalytics(projectUrl: string, secretKey?: string) { const analyticsAPI = ofetch.create({ baseURL: joinURL(projectUrl, '/api/_hub/analytics'), headers: { diff --git a/src/runtime/server/utils/blob.ts b/src/runtime/server/utils/blob.ts index 1b8d9ba9..12a8b124 100644 --- a/src/runtime/server/utils/blob.ts +++ b/src/runtime/server/utils/blob.ts @@ -55,7 +55,7 @@ function _useBucket() { export function hubBlob() { const hub = useRuntimeConfig().hub if (import.meta.dev && hub.projectUrl) { - return useProxyBlob(hub.projectUrl, hub.projectSecretKey || hub.userToken) + return proxyHubBlob(hub.projectUrl, hub.projectSecretKey || hub.userToken) } const bucket = _useBucket() @@ -132,7 +132,7 @@ export function hubBlob() { } } -export function useProxyBlob(projectUrl: string, secretKey?: string) { +export function proxyHubBlob(projectUrl: string, secretKey?: string) { const blobAPI = ofetch.create({ baseURL: joinURL(projectUrl, '/api/_hub/blob'), headers: { diff --git a/src/runtime/server/utils/database.ts b/src/runtime/server/utils/database.ts index f18608c8..ea8559cf 100644 --- a/src/runtime/server/utils/database.ts +++ b/src/runtime/server/utils/database.ts @@ -13,7 +13,7 @@ export function hubDatabase(): D1Database { } const hub = useRuntimeConfig().hub if (import.meta.dev && hub.projectUrl) { - _db = useProxyDatabase(hub.projectUrl, hub.projectSecretKey || hub.userToken) + _db = proxyHubDatabase(hub.projectUrl, hub.projectSecretKey || hub.userToken) return _db } // @ts-ignore @@ -25,7 +25,7 @@ export function hubDatabase(): D1Database { throw createError('Missing Cloudflare DB binding (D1)') } -export function useProxyDatabase(projectUrl: string, secretKey?: string): D1Database { +export function proxyHubDatabase(projectUrl: string, secretKey?: string): D1Database { const d1API = ofetch.create({ baseURL: joinURL(projectUrl, '/api/_hub/database'), method: 'POST', diff --git a/src/runtime/server/utils/kv.ts b/src/runtime/server/utils/kv.ts index 588575e9..0e2ecb19 100644 --- a/src/runtime/server/utils/kv.ts +++ b/src/runtime/server/utils/kv.ts @@ -6,31 +6,47 @@ import { joinURL } from 'ufo' import { createError } from 'h3' import { useRuntimeConfig } from '#imports' -let _kv: Storage +interface KV extends Storage { + keys: Storage['getKeys'] + get: Storage['getItem'] + set: Storage['setItem'] + has: Storage['hasItem'] + del: Storage['removeItem'] +} + +let _kv: KV -export function hubKV(): Storage { +export function hubKV(): KV { if (_kv) { return _kv } const hub = useRuntimeConfig().hub if (import.meta.dev && hub.projectUrl) { - return useProxyKV(hub.projectUrl, hub.projectSecretKey || hub.userToken) + return proxyHubKV(hub.projectUrl, hub.projectSecretKey || hub.userToken) } // @ts-ignore const binding = process.env.KV || globalThis.__env__?.KV || globalThis.KV if (binding) { - _kv = createStorage({ + const storage = createStorage({ driver: cloudflareKVBindingDriver({ binding }) }) + _kv = { + keys: storage.getKeys, + get: storage.getItem, + set: storage.setItem, + has: storage.hasItem, + del: storage.removeItem, + ...storage, + } return _kv } throw createError('Missing Cloudflare KV binding (KV)') } -export function useProxyKV(projectUrl: string, secretKey?: string): Storage { - return createStorage({ +export function proxyHubKV(projectUrl: string, secretKey?: string): KV { + const storage = createStorage({ driver: httpDriver({ base: joinURL(projectUrl, '/api/_hub/kv/'), headers: { @@ -38,4 +54,13 @@ export function useProxyKV(projectUrl: string, secretKey?: string): Storage { } }) }) + + return { + keys: storage.getKeys, + get: storage.getItem, + set: storage.setItem, + has: storage.hasItem, + del: storage.removeItem, + ...storage, + } }