Skip to content

Commit 7e5c739

Browse files
docs: kv, db, and cache updates (#454)
Co-authored-by: Sébastien Chopin <[email protected]>
1 parent 03b6218 commit 7e5c739

File tree

12 files changed

+462
-265
lines changed

12 files changed

+462
-265
lines changed

.github/workflows/autofix.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ jobs:
1111

1212
steps:
1313
- uses: actions/checkout@v4
14-
- run: corepack enable
14+
- run: npm i -g --force corepack && corepack enable
1515
- uses: actions/setup-node@v4
1616
with:
17-
node-version: 20
17+
node-version: 22
1818
cache: "pnpm"
1919

2020
- name: Install dependencies

.github/workflows/ci.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ jobs:
3131

3232
steps:
3333
- uses: actions/checkout@v4
34-
- run: corepack enable
34+
- run: npm i -g --force corepack && corepack enable
3535
- uses: actions/setup-node@v4
3636
with:
37-
node-version: 20
37+
node-version: 22
3838
cache: "pnpm"
3939

4040
- name: Install dependencies
+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
tabs?: string[]
4+
}>()
5+
6+
const { data: table } = await useAsyncData('pricing-table', () => queryContent('/_partials/pricing').findOne())
7+
</script>
8+
9+
<template>
10+
<UTabs
11+
class="pt-8 sm:pt-0 pb-20 sm:pb-0 sm:w-full w-[calc(100vw-32px)] not-prose"
12+
:items="table?.plans.filter(plan => !tabs || tabs.includes(plan.label))"
13+
:ui="{
14+
list: {
15+
base: tabs?.length === 1 ? 'hidden' : '',
16+
background: 'dark:bg-gray-950 border dark:border-gray-800 bg-white',
17+
height: 'h-[42px]',
18+
marker: {
19+
background: 'bg-gray-100 dark:bg-gray-800'
20+
},
21+
tab: {
22+
icon: 'hidden sm:inline-flex'
23+
}
24+
}
25+
}"
26+
>
27+
<template #item="{ item }">
28+
<UTable
29+
v-bind="item"
30+
class="border dark:border-gray-800 border-gray-200 rounded-lg"
31+
:ui="{
32+
divide: 'dark:divide-gray-800 divide-gray-200'
33+
}"
34+
>
35+
<template #paid-data="{ row }">
36+
<span v-html="row.paid" />
37+
</template>
38+
</UTable>
39+
<div v-if="item.buttons?.length" class="mt-2 flex items-center gap-2 justify-center">
40+
<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" />
41+
</div>
42+
</template>
43+
</UTabs>
44+
</template>

docs/content/1.docs/2.features/ai.md

+4
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,7 @@ Learn more about the [`useChat()` Vue composable](https://sdk.vercel.ai/docs/ref
226226
::callout
227227
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).
228228
::
229+
230+
## Pricing
231+
232+
:pricing-table{:tabs='["AI"]'}

docs/content/1.docs/2.features/blob.md

+5
Original file line numberDiff line numberDiff line change
@@ -984,3 +984,8 @@ That's it! You can now upload files to R2 using the presigned URLs.
984984
::callout
985985
Read more about presigned URLs on Cloudflare's [official documentation](https://developers.cloudflare.com/r2/api/s3/presigned-urls/).
986986
::
987+
988+
## Pricing
989+
990+
:pricing-table{:tabs='["Blob"]'}
991+

docs/content/1.docs/2.features/cache.md

+57-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ navigation.title: Cache
44
description: Learn how to cache Nuxt pages, API routes and functions in with NuxtHub cache storage.
55
---
66

7+
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.
8+
79
## Getting Started
810

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

86+
## Routes Caching
87+
88+
You can enable route caching in your `nuxt.config.ts` file.
89+
90+
```ts [nuxt.config.ts]
91+
export default defineNuxtConfig({
92+
routeRules: {
93+
'/blog/**': {
94+
cache: {
95+
maxAge: 60 * 60,
96+
// other options like name, group, swr...
97+
}
98+
}
99+
}
100+
})
101+
```
102+
103+
::note
104+
Read more about [Nuxt's route rules](https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering).
105+
::
106+
84107
## Cache Invalidation
85108

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

92115
The defaults are:
93116
- `group`: `'nitro'`
94-
- `name`: `'handlers'` for api routes and `'functions'` for server functions
117+
- `name`: `'handlers'` for API routes, `'functions'` for server functions, or `'routes'` for route handlers
95118

96119
For example, the following function:
97120

98121
```ts
99122
const getAccessToken = defineCachedFunction(() => {
100123
return String(Date.now())
101124
}, {
102-
maxAge: 10,
125+
maxAge: 60,
103126
name: 'getAccessToken',
104127
getKey: () => 'default'
105128
})
@@ -111,12 +134,20 @@ Will generate the following cache key:
111134
nitro:functions:getAccessToken:default.json
112135
```
113136

114-
You can invalidate the cached function entry with:
137+
You can invalidate the cached function entry from your storage using cache key.
115138

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

143+
You can use the `group` and `name` options to invalidate multiple cache entries based on their prefixes.
144+
145+
```ts
146+
// Gets all keys that start with nitro:handlers
147+
await useStorage('cache').clear('nitro:handlers')
148+
```
149+
150+
120151
::note{to="https://nitro.unjs.io/guide/cache"}
121152
Read more about Nitro Cache.
122153
::
@@ -125,6 +156,29 @@ Read more about Nitro Cache.
125156

126157
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.
127158

159+
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.
160+
161+
To disable this behavior, set `swr` to `false` when defining a cache rule. This will delete the cache entry once `maxAge` is reached.
162+
163+
```ts [nuxt.config.ts]
164+
export default defineNuxtConfig({
165+
nitro: {
166+
routeRules: {
167+
'/blog/**': {
168+
cache: {
169+
maxAge: 60 * 60,
170+
swr: false
171+
// other options like name and group...
172+
}
173+
}
174+
}
175+
})
176+
```
177+
128178
::note
129179
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.
130180
::
181+
182+
## Pricing
183+
184+
:pricing-table{:tabs='["KV"]'}

docs/content/1.docs/2.features/database.md

+95-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ navigation.title: Database
44
description: Access a SQL database in your Nuxt application to store and retrieve relational data.
55
---
66

7+
NuxtHub Database uses [Cloudflare D1](https://developers.cloudflare.com/d1/), a managed, serverless database built on SQLite to store and retrieve relational data.
8+
79
## Getting Started
810

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

29+
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.
30+
2731
::tabs
2832
::div{label="Nuxt DevTools"}
2933
: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"}
@@ -59,22 +63,35 @@ Best practice is to use prepared statements which are precompiled objects used b
5963

6064
### `bind()`
6165

62-
Binds parameters to a prepared statement.
66+
Binds parameters to a prepared statement, allowing you to pass dynamic values to the query.
6367

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

6771
stmt.bind('Evan You')
72+
73+
// SELECT * FROM users WHERE name = 'Evan You'
6874
```
6975

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

7478
```ts
7579
const stmt = db
7680
.prepare('SELECT * FROM users WHERE name = ?2 AND age = ?1')
7781
.bind(3, 'Leo Chopin')
82+
// SELECT * FROM users WHERE name = 'Leo Chopin' AND age = 3
83+
```
84+
85+
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.
86+
87+
It's recommended to use ordered parameters to improve maintainable and ensure that removing or reordering parameters will not break your queries.
88+
89+
```ts
90+
const stmt = db
91+
.prepare('SELECT * FROM users WHERE name = ? AND age = ?')
92+
.bind('Leo Chopin', 3)
93+
94+
// SELECT * FROM users WHERE name = 'Leo Chopin' AND age = 3
7895
```
7996

8097
### `all()`
@@ -190,7 +207,9 @@ console.log(result)
190207

191208
### `batch()`
192209

193-
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.
210+
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.
211+
212+
`batch` acts as a SQL transaction, meaning that if any statement fails, the entire transaction is aborted and rolled back.
194213

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

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

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

227246
```ts
228247
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)`)
@@ -236,9 +255,52 @@ console.log(result)
236255
```
237256

238257
::callout
239-
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.
258+
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).
259+
::
260+
261+
## Working with JSON
262+
263+
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.
264+
265+
JSON columns are stored as `TEXT` columns in your database.
266+
267+
```ts
268+
const framework = {
269+
name: 'Nuxt',
270+
year: 2016,
271+
projects: [
272+
'NuxtHub',
273+
'Nuxt UI'
274+
]
275+
}
276+
277+
await hubDatabase()
278+
.prepare('INSERT INTO frameworks (info) VALUES (?1)')
279+
.bind(JSON.stringify(framework))
280+
.run()
281+
```
282+
283+
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.
284+
285+
```ts
286+
const framework = await db.prepare('SELECT * FROM frameworks WHERE (json_extract(info, "$.name") = "Nuxt")').first()
287+
console.log(framework)
288+
/*
289+
{
290+
"id": 1,
291+
"info": "{\"name\":\"Nuxt\",\"year\":2016,\"projects\":[\"NuxtHub\",\"Nuxt UI\"]}"
292+
}
293+
*/
294+
```
295+
296+
::callout
297+
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).
240298
::
241299

300+
## Using an ORM
301+
302+
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.
303+
242304
## Database Migrations
243305

244306
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`).
@@ -318,14 +380,26 @@ npx nuxthub database migrations create <name>
318380
Migration names must only contain alphanumeric characters and `-` (spaces are converted to `-`).
319381
::
320382

321-
Migration files are created in `server/database/migrations/`.
383+
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.
322384

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

328-
After creation, add your SQL queries to modify the database schema.
390+
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.
391+
392+
```sql [0001_create-todos.sql]
393+
-- Migration number: 0001 2025-01-30T17:17:37.252Z
394+
395+
CREATE TABLE `todos` (
396+
`id` integer PRIMARY KEY NOT NULL,
397+
`user_id` integer NOT NULL,
398+
`title` text NOT NULL,
399+
`completed` integer DEFAULT 0 NOT NULL,
400+
`created_at` integer NOT NULL
401+
);
402+
```
329403

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

418492
### Foreign Key Constraints
419493

420-
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).
494+
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).
421495

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

@@ -430,3 +504,14 @@ ALTER TABLE ...
430504
-PRAGMA foreign_keys = ON;
431505
+PRAGMA defer_foreign_keys = off;
432506
```
507+
508+
## Limits
509+
510+
- The maximum database size is 10 GB
511+
- The maximum number of columns per table is 100
512+
513+
See all of the [D1 Limits](https://developers.cloudflare.com/d1/platform/limits/)
514+
515+
## Pricing
516+
517+
:pricing-table{:tabs='["DB"]'}

0 commit comments

Comments
 (0)