Skip to content
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
4 changes: 2 additions & 2 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
"@vitnode/core": "workspace:*",
"@vitnode/node-cron": "workspace:*",
"@vitnode/supabase-storage": "workspace:*",
"drizzle-kit": "^0.31.10",
"drizzle-orm": "^0.45.2",
"drizzle-kit": "1.0.0-rc.4",
"drizzle-orm": "1.0.0-rc.4",
"hono": "^4.12.31",
"next-intl": "^4.13.3",
"react": "^19.2.8",
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/vitnode.api.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// import { S3StorageAdapter } from "@vitnode/s3";
import { SupabaseStorageAdapter } from "@vitnode/supabase-storage";
import { config } from "dotenv";
import { coreRelations } from "@vitnode/core/database/relations";

Check warning on line 11 in apps/api/src/vitnode.api.config.ts

View workflow job for this annotation

GitHub Actions / build

Expected "@vitnode/core/database/relations" to come before "dotenv"
import { drizzle } from "drizzle-orm/postgres-js";

config({
Expand Down Expand Up @@ -45,7 +46,7 @@
// override strings from `src/locales/<pluginId>/<locale>.json`.
dbProvider: drizzle({
connection: POSTGRES_URL,
casing: "camelCase",
relations: coreRelations,
}),
cron: NodeCronAdapter(),
redis: process.env.REDIS_URL
Expand Down
78 changes: 67 additions & 11 deletions apps/docs/content/docs/dev/database/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,33 @@ VitNode plugins seamlessly integrate with databases using [Drizzle ORM](https://
Create your database schema in the `database` directory of your plugin. Each table should be defined in its own file for better organization.

```ts title="plugins/{plugin_name}/src/database/categories.ts"
import { pgTable } from "drizzle-orm/pg-core";

export const blog_categories = pgTable("blog_categories", t => ({
id: t.serial().primaryKey(),
createdAt: t.timestamp().notNull().defaultNow(),
updatedAt: t
.timestamp()
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
})).enableRLS();
import { camelCase } from "drizzle-orm/pg-core";

export const blog_categories = camelCase.table.withRLS(
"blog_categories",
t => ({
id: t.serial().primaryKey(),
createdAt: t.timestamp().notNull().defaultNow(),
updatedAt: t
.timestamp()
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
}),
);
```

Two pieces of that are worth naming, because both moved in Drizzle v1:

- **`camelCase.table`** rather than `pgTable`. Column names are derived from the
object keys, so `createdAt` is `"createdAt"` in Postgres rather than
`created_at`. This used to be a client-wide `casing: "camelCase"` option; it is
now chosen per table, and every VitNode table uses `camelCase` - so match it,
or your columns will not line up with the rest of the schema.
- **`.withRLS(...)`** rather than a trailing `.enableRLS()`. Row level security
is now part of building the table. The old method still exists and still works,
but it is deprecated.

<Callout type="info" title="Let the Content Engine write this for you">
Hand-writing the table, its schemas, its routes and its AdminCP screen is a
lot of repetition. The [Content Engine](/docs/dev/content-engine) generates all
Expand Down Expand Up @@ -52,6 +66,48 @@ export const postsRoute = buildRoute({
});
```

## Relational Queries

`c.get("db")` is built with the core schema's relations, so `db.query` can load a
record together with what it points at - one round trip, and no join to write:

```ts
const user = await c.get("db").query.core_users.findFirst({
where: { id: 1 },
with: { group: true, secondary_roles: true },
});
```

Filters and ordering are plain objects rather than callbacks:

```ts
await c.get("db").query.core_users.findMany({
where: { emailVerified: true, name: { like: "A%" } },
orderBy: { createdAt: "desc" },
limit: 20,
});
```

Relations live in one place - `@vitnode/core/database/relations` - rather than
beside each table, and a relation is named on the side that reads it:

```ts title="src/database/relations.ts"
import { defineRelations } from "drizzle-orm";

export const coreRelations = defineRelations(schema, r => ({
core_users: {
group: r.one.core_roles({ from: r.core_users.roleId, to: r.core_roles.id }),
secondary_roles: r.many.core_users_secondary_roles(),
},
}));
```

<Callout type="warn" title="A relation cannot reuse a column name">
Relations and columns share one namespace. `core_users` already has a
`language` column, so its relation to `core_languages` is called
`language_ref` - naming it `language` is an error, not a shadow.
</Callout>

## Database Operations

VitNode provides convenient commands for managing your database schema and migrations.
Expand Down
8 changes: 6 additions & 2 deletions apps/docs/content/docs/dev/i18n/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,14 @@ export const vitNodeConfig = buildConfig({
```

```ts title="src/vitnode.api.config.ts"
import { coreRelations } from "@vitnode/core/database/relations";

import { i18n } from "./i18n"; // [!code ++]

export const vitNodeApiConfig = buildApiConfig({
i18n, // [!code ++]
plugins: [blogApiPlugin()],
dbProvider: drizzle({ connection: POSTGRES_URL, casing: "camelCase" }),
dbProvider: drizzle({ connection: POSTGRES_URL, relations: coreRelations }),
});
```

Expand Down Expand Up @@ -310,9 +312,11 @@ field) and it returns exactly as it was.
An API-only app needs no `i18n` block at all. The languages the installed packages ship are picked up on their own, and `defaultLocale` is `en`:

```ts title="src/vitnode.api.config.ts"
import { coreRelations } from "@vitnode/core/database/relations";

export const vitNodeApiConfig = buildApiConfig({
plugins: [blogApiPlugin()],
dbProvider: drizzle({ connection: POSTGRES_URL, casing: "camelCase" }),
dbProvider: drizzle({ connection: POSTGRES_URL, relations: coreRelations }),
});
```

Expand Down
Loading
Loading