Skip to content
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

docs: kv, db, and cache updates #454

Merged
merged 11 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 10 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
44 changes: 44 additions & 0 deletions docs/components/content/PricingTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script setup lang="ts">
defineProps<{
tabs?: string[]
}>()

const { data: table } = await useAsyncData('pricing-table', () => queryContent('/_partials/pricing').findOne())
</script>

<template>
<UTabs
class="pt-8 sm:pt-0 pb-20 sm:pb-0 sm:w-full w-[calc(100vw-32px)] not-prose"
:items="table?.plans.filter(plan => !tabs || tabs.includes(plan.label))"
:ui="{
list: {
base: tabs?.length === 1 ? 'hidden' : '',
background: 'dark:bg-gray-950 border dark:border-gray-800 bg-white',
height: 'h-[42px]',
marker: {
background: 'bg-gray-100 dark:bg-gray-800'
},
tab: {
icon: 'hidden sm:inline-flex'
}
}
}"
>
<template #item="{ item }">
<UTable
v-bind="item"
class="border dark:border-gray-800 border-gray-200 rounded-lg"
:ui="{
divide: 'dark:divide-gray-800 divide-gray-200'
}"
>
<template #paid-data="{ row }">
<span v-html="row.paid" />
</template>
</UTable>
<div v-if="item.buttons?.length" class="mt-2 flex items-center gap-2 justify-center">
<UButton v-for="button of item.buttons" :key="button.to" v-bind="button" color="gray" size="xs" variant="link" trailing-icon="i-lucide-arrow-up-right" target="_blank" />
</div>
</template>
</UTabs>
</template>
4 changes: 4 additions & 0 deletions docs/content/1.docs/2.features/ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,3 +226,7 @@ Learn more about the [`useChat()` Vue composable](https://sdk.vercel.ai/docs/ref
::callout
Check out our [`pages/ai.vue` full example](https://github.com/nuxt-hub/core/blob/main/playground/app/pages/ai.vue) with Nuxt UI & [Nuxt MDC](https://github.com/nuxt-modules/mdc).
::

## Pricing

:pricing-table{:tabs='["AI"]'}
5 changes: 5 additions & 0 deletions docs/content/1.docs/2.features/blob.md
Original file line number Diff line number Diff line change
Expand Up @@ -984,3 +984,8 @@ That's it! You can now upload files to R2 using the presigned URLs.
::callout
Read more about presigned URLs on Cloudflare's [official documentation](https://developers.cloudflare.com/r2/api/s3/presigned-urls/).
::

## Pricing

:pricing-table{:tabs='["Blob"]'}

60 changes: 57 additions & 3 deletions docs/content/1.docs/2.features/cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ navigation.title: Cache
description: Learn how to cache Nuxt pages, API routes and functions in with NuxtHub cache storage.
---

NuxtHub Cache is powered by [Nitro's cache storage](https://nitro.unjs.io/guide/cache#customize-cache-storage) and uses [Cloudflare Workers KV](https://developers.cloudflare.com/kv) as the cache storage. It allows you to cache API routes, server functions, and pages in your application.

## Getting Started

Enable the cache storage in your NuxtHub project by adding the `cache` property to the `hub` object in your `nuxt.config.ts` file.
Expand Down Expand Up @@ -81,6 +83,27 @@ It is important to note that the `event` argument should always be the first arg
[Read more about this in the Nitro docs](https://nitro.unjs.io/guide/cache#edge-workers).
::

## Routes Caching

You can enable route caching in your `nuxt.config.ts` file.

```ts [nuxt.config.ts]
export default defineNuxtConfig({
routeRules: {
'/blog/**': {
cache: {
maxAge: 60 * 60,
// other options like name, group, swr...
}
}
}
})
```

::note
Read more about [Nuxt's route rules](https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering).
::

## Cache Invalidation

When using the `defineCachedFunction` or `defineCachedEventHandler` functions, the cache key is generated using the following pattern:
Expand All @@ -91,15 +114,15 @@ When using the `defineCachedFunction` or `defineCachedEventHandler` functions, t

The defaults are:
- `group`: `'nitro'`
- `name`: `'handlers'` for api routes and `'functions'` for server functions
- `name`: `'handlers'` for API routes, `'functions'` for server functions, or `'routes'` for route handlers

For example, the following function:

```ts
const getAccessToken = defineCachedFunction(() => {
return String(Date.now())
}, {
maxAge: 10,
maxAge: 60,
name: 'getAccessToken',
getKey: () => 'default'
})
Expand All @@ -111,12 +134,20 @@ Will generate the following cache key:
nitro:functions:getAccessToken:default.json
```

You can invalidate the cached function entry with:
You can invalidate the cached function entry from your storage using cache key.

```ts
await useStorage('cache').removeItem('nitro:functions:getAccessToken:default.json')
```

You can use the `group` and `name` options to invalidate multiple cache entries based on their prefixes.

```ts
// Gets all keys that start with nitro:handlers
await useStorage('cache').clear('nitro:handlers')
```


::note{to="https://nitro.unjs.io/guide/cache"}
Read more about Nitro Cache.
::
Expand All @@ -125,6 +156,29 @@ Read more about Nitro Cache.

As NuxtHub leverages Cloudflare Workers KV to store your cache entries, we leverage the [`expiration` property](https://developers.cloudflare.com/kv/api/write-key-value-pairs/#expiring-keys) of the KV binding to handle the cache expiration.

By default, `stale-while-revalidate` behavior is enabled. If an expired cache entry is requested, the stale value will be served while the cache is asynchronously refreshed. This also means that all cache entries will remain in your KV namespace until they are manually invalidated/deleted.

To disable this behavior, set `swr` to `false` when defining a cache rule. This will delete the cache entry once `maxAge` is reached.

```ts [nuxt.config.ts]
export default defineNuxtConfig({
nitro: {
routeRules: {
'/blog/**': {
cache: {
maxAge: 60 * 60,
swr: false
// other options like name and group...
}
}
}
})
```

::note
If you set an expiration (`maxAge`) lower than `60` seconds, NuxtHub will set the KV entry expiration to `60` seconds in the future (Cloudflare KV limitation) so it can be removed automatically.
::

## Pricing

:pricing-table{:tabs='["KV"]'}
105 changes: 95 additions & 10 deletions docs/content/1.docs/2.features/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ navigation.title: Database
description: Access a SQL database in your Nuxt application to store and retrieve relational data.
---

NuxtHub Database uses [Cloudflare D1](https://developers.cloudflare.com/d1/), a managed, serverless database built on SQLite to store and retrieve relational data.

## Getting Started

Enable the database in your NuxtHub project by adding the `database` property to the `hub` object in your `nuxt.config.ts` file.
Expand All @@ -24,6 +26,8 @@ This option will use Cloudflare platform proxy in development and automatically
Checkout our [Drizzle ORM recipe](/docs/recipes/drizzle) to get started with the database by providing a schema and migrations.
::

During local development, you can view and edit your database in the Nuxt DevTools. Once your project is deployed, you can inspect the database in the NuxtHub Admin Dashboard.

::tabs
::div{label="Nuxt DevTools"}
:nuxt-img{src="/images/landing/nuxt-devtools-database.png" alt="Nuxt DevTools Database" width="915" height="515" data-zoom-src="/images/landing/nuxt-devtools-database.png"}
Expand Down Expand Up @@ -59,22 +63,35 @@ Best practice is to use prepared statements which are precompiled objects used b

### `bind()`

Binds parameters to a prepared statement.
Binds parameters to a prepared statement, allowing you to pass dynamic values to the query.

```ts
const stmt = db.prepare('SELECT * FROM users WHERE name = ?1')

stmt.bind('Evan You')

// SELECT * FROM users WHERE name = 'Evan You'
```

::note
The `?` character followed by a number (1-999) represents an ordered parameter. The number represents the position of the parameter when calling `.bind(...params)`.
::
The `?` character followed by a number (1-999) represents an ordered parameter. The number represents the position of the parameter when calling `.bind(...params)`.

```ts
const stmt = db
.prepare('SELECT * FROM users WHERE name = ?2 AND age = ?1')
.bind(3, 'Leo Chopin')
// SELECT * FROM users WHERE name = 'Leo Chopin' AND age = 3
```

If you instead use anonymous parameters (without a number), the values passed to `bind` will be assigned in order to the `?` placeholders in the query.

It's recommended to use ordered parameters to improve maintainable and ensure that removing or reordering parameters will not break your queries.

```ts
const stmt = db
.prepare('SELECT * FROM users WHERE name = ? AND age = ?')
.bind('Leo Chopin', 3)

// SELECT * FROM users WHERE name = 'Leo Chopin' AND age = 3
```

### `all()`
Expand Down Expand Up @@ -190,7 +207,9 @@ console.log(result)

### `batch()`

Sends multiple SQL statements inside a single call to the database. This can have a huge performance impact as it reduces latency from network round trips to the database. Each statement in the list will execute and commit, sequentially, non-concurrently and return the results in the same order.
Sends multiple SQL statements inside a single call to the database. This can have a huge performance impact by reducing latency caused by multiple network round trips to the database. Each statement in the list will execute/commit sequentially and non-concurrently before returning the results in the same order.

`batch` acts as a SQL transaction, meaning that if any statement fails, the entire transaction is aborted and rolled back.

```ts
const [info1, info2] = await db.batch([
Expand Down Expand Up @@ -222,7 +241,7 @@ The object returned is the same as the [`.all()`](#all) method.

Executes one or more queries directly without prepared statements or parameters binding. The input can be one or multiple queries separated by \n.

If an error occurs, an exception is thrown with the query and error messages, execution stops and further statements are not executed.
If an error occurs, an exception is thrown with the query and error messages, execution stops, and further queries are not executed.

```ts
const result = await hubDatabase().exec(`CREATE TABLE IF NOT EXISTS frameworks (id INTEGER PRIMARY KEY, name TEXT NOT NULL, year INTEGER NOT NULL DEFAULT 0)`)
Expand All @@ -236,9 +255,52 @@ console.log(result)
```

::callout
This method can have poorer performance (prepared statements can be reused in some cases) and, more importantly, is less safe. Only use this method for maintenance and one-shot tasks (for example, migration jobs). The input can be one or multiple queries separated by \n.
This method can have poorer performance (prepared statements can be reused in some cases) and, more importantly, is less safe. Only use this method for maintenance and one-shot tasks (for example, migration jobs).
::

## Working with JSON

Cloudflare D1 supports querying and parsing JSON data. This can improve performance by reducing the number of round trips to your database. Instead of querying a JSON column, extracting the data you need, and using that data to make another query, you can do all of this work in a single query by using JSON functions.

JSON columns are stored as `TEXT` columns in your database.

```ts
const framework = {
name: 'Nuxt',
year: 2016,
projects: [
'NuxtHub',
'Nuxt UI'
]
}

await hubDatabase()
.prepare('INSERT INTO frameworks (info) VALUES (?1)')
.bind(JSON.stringify(framework))
.run()
```

Then, using D1's [JSON functions](https://developers.cloudflare.com/d1/sql-api/query-json/), which are built on the [SQLite JSON extension](https://www.sqlite.org/json1.html), you can make queries using the data in your JSON column.

```ts
const framework = await db.prepare('SELECT * FROM frameworks WHERE (json_extract(info, "$.name") = "Nuxt")').first()
console.log(framework)
/*
{
"id": 1,
"info": "{\"name\":\"Nuxt\",\"year\":2016,\"projects\":[\"NuxtHub\",\"Nuxt UI\"]}"
}
*/
```

::callout
For an in-depth guide on querying JSON and a list of all supported functions, see [Cloudlare's Query JSON documentation](https://developers.cloudflare.com/d1/sql-api/query-json/#generated-columns).
::

## Using an ORM

Instead of using `hubDatabase()` to make interact with your database, you can use an ORM like [Drizzle ORM](/docs/recipes/drizzle). This can improve the developer experience by providing a type-safe API, migrations, and more.

## Database Migrations

Database migrations provide version control for your database schema. They track changes and ensure consistent schema evolution across all environments through incremental updates. NuxtHub supports SQL migration files (`.sql`).
Expand Down Expand Up @@ -318,14 +380,26 @@ npx nuxthub database migrations create <name>
Migration names must only contain alphanumeric characters and `-` (spaces are converted to `-`).
::

Migration files are created in `server/database/migrations/`.
Migration files are created in `server/database/migrations/` and are prefixed by an auto-incrementing sequence number. This migration number is used to determine the order in which migrations are run.

```bash [Example]
> npx nuxthub database migrations create create-todos
✔ Created ./server/database/migrations/0001_create-todos.sql
```

After creation, add your SQL queries to modify the database schema.
After creation, add your SQL queries to modify the database schema. For example, migrations should be used to create tables, add/delete/modify columns, and add/remove indexes.

```sql [0001_create-todos.sql]
-- Migration number: 0001 2025-01-30T17:17:37.252Z

CREATE TABLE `todos` (
`id` integer PRIMARY KEY NOT NULL,
`user_id` integer NOT NULL,
`title` text NOT NULL,
`completed` integer DEFAULT 0 NOT NULL,
`created_at` integer NOT NULL
);
```

::note{to="/docs/recipes/drizzle#npm-run-dbgenerate"}
With [Drizzle ORM](/docs/recipes/drizzle), migrations are automatically created when you run `npx drizzle-kit generate`.
Expand Down Expand Up @@ -417,7 +491,7 @@ These queries run after all migrations are applied but are not tracked in the `_

### Foreign Key Constraints

If you are using [Drizzle ORM](/docs/recipes/drizzle) to generate your database migrations, note that is uses `PRAGMA foreign_keys = ON | OFF;` in the generated migration files. This is not supported by Cloudflare D1 as they support instead [defer foreign key constraints](https://developers.cloudflare.com/d1/sql-api/foreign-keys/#defer-foreign-key-constraints).
If you are using [Drizzle ORM](/docs/recipes/drizzle) to generate your database migrations, your generated migration files will use `PRAGMA foreign_keys = ON | OFF;`. This is not supported by Cloudflare D1. Instead, they support [defer foreign key constraints](https://developers.cloudflare.com/d1/sql-api/foreign-keys/#defer-foreign-key-constraints).

You need to update your migration file to use `PRAGMA defer_foreign_keys = on|off;` instead:

Expand All @@ -430,3 +504,14 @@ ALTER TABLE ...
-PRAGMA foreign_keys = ON;
+PRAGMA defer_foreign_keys = off;
```

## Limits

- The maximum database size is 10 GB
- The maximum number of columns per table is 100

See all of the [D1 Limits](https://developers.cloudflare.com/d1/platform/limits/)

## Pricing

:pricing-table{:tabs='["DB"]'}
Loading
Loading