@kitr/db 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +12 -2
- package/dist/index.d.ts +42 -0
- package/dist/index.js +37 -0
- package/dist/kitr.d.ts +11 -0
- package/dist/kitr.js +19 -0
- package/docs/api.md +69 -0
- package/docs/index.md +638 -0
- package/docs/overview.md +89 -0
- package/package.json +58 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Platon Podkopov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kitr/db
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Database.** Connect your database so every request can use it.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm kitr add db
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`kitr add` installs the package and wires it into your app: the provider in `kitrrc.ts`, its config file and its `AGENTS.md` section. Import it from `@kitr/db`.
|
|
10
|
+
|
|
11
|
+
Read the guide at https://kitr.dev/packages/db.
|
|
12
|
+
|
|
13
|
+
Part of [kitr](https://kitr.dev), the TypeScript framework where everything is an add-on. MIT licensed.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { KitrApp, KitrProvider } from "@kitr/core";
|
|
2
|
+
/**
|
|
3
|
+
* kitr ships no ORM. `defineDb` binds whatever you use (Drizzle, Prisma, Kysely,
|
|
4
|
+
* a raw pool) as `db` in the container with connect/disconnect in the lifecycle.
|
|
5
|
+
*
|
|
6
|
+
* // config/db.ts
|
|
7
|
+
* export default defineDb({ connect: () => drizzle(process.env.DATABASE_URL!), disconnect: (db) => db.$client.end() })
|
|
8
|
+
*/
|
|
9
|
+
export interface DbDefinition<T> {
|
|
10
|
+
/** Opens the connection and returns the client. Runs once during `app.boot()`; a rejection fails boot. */
|
|
11
|
+
connect: (app: KitrApp) => T | Promise<T>;
|
|
12
|
+
/** Closes the client when the server shuts down (SIGINT, SIGTERM or `server.close()`). */
|
|
13
|
+
disconnect?: (db: T, app: KitrApp) => void | Promise<void>;
|
|
14
|
+
/** Runs once the server is ready to serve, after boot. Use it to check for pending migrations or warm a pool. */
|
|
15
|
+
onReady?: (db: T, app: KitrApp) => void | Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
declare module '@kitr/core' {
|
|
18
|
+
interface ConfigRegistry {
|
|
19
|
+
db: DbDefinition<any>;
|
|
20
|
+
}
|
|
21
|
+
interface ContainerBindings {
|
|
22
|
+
db: unknown;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Declares how the app connects to its database, in `config/db.ts`. Returns the definition
|
|
27
|
+
* unchanged; it exists so `disconnect` and `onReady` receive the client type `connect` returns.
|
|
28
|
+
*/
|
|
29
|
+
export declare function defineDb<T>(definition: DbDefinition<T>): DbDefinition<T>;
|
|
30
|
+
/** Untyped alias of `defineDb` for the `defineConfig` convention; prefer `defineDb`, which infers the client type. */
|
|
31
|
+
export declare const defineConfig: <const C extends DbDefinition<any>>(config: C) => C;
|
|
32
|
+
/**
|
|
33
|
+
* The provider `kitr add db` puts in `kitrrc.ts`. It binds `db` in the container from `config/db.ts`,
|
|
34
|
+
* connects during boot, runs `onReady` when the server is ready and `disconnect` on shutdown.
|
|
35
|
+
*/
|
|
36
|
+
export declare const dbProvider: KitrProvider;
|
|
37
|
+
/**
|
|
38
|
+
* Returns the connected client from an app's container, typed as `T`: `useDb<AppDb>(ctx.app)`.
|
|
39
|
+
* Throws when the app has no `db` binding, or when an async `connect` has not finished (call it after `app.boot()`).
|
|
40
|
+
*/
|
|
41
|
+
export declare function useDb<T>(app: KitrApp): T;
|
|
42
|
+
export { dbProvider as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createDefineConfig, defineProvider } from "@kitr/core";
|
|
2
|
+
// src/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* Declares how the app connects to its database, in `config/db.ts`. Returns the definition
|
|
5
|
+
* unchanged; it exists so `disconnect` and `onReady` receive the client type `connect` returns.
|
|
6
|
+
*/
|
|
7
|
+
function defineDb(definition) {
|
|
8
|
+
return definition;
|
|
9
|
+
}
|
|
10
|
+
/** Untyped alias of `defineDb` for the `defineConfig` convention; prefer `defineDb`, which infers the client type. */
|
|
11
|
+
const defineConfig = createDefineConfig();
|
|
12
|
+
/**
|
|
13
|
+
* The provider `kitr add db` puts in `kitrrc.ts`. It binds `db` in the container from `config/db.ts`,
|
|
14
|
+
* connects during boot, runs `onReady` when the server is ready and `disconnect` on shutdown.
|
|
15
|
+
*/
|
|
16
|
+
const dbProvider = defineProvider({
|
|
17
|
+
name: "db",
|
|
18
|
+
register(app) {
|
|
19
|
+
const def = app.config.get("db");
|
|
20
|
+
app.container.singleton("db", () => def.connect(app));
|
|
21
|
+
},
|
|
22
|
+
async ready(app) {
|
|
23
|
+
await app.config.get("db").onReady?.(await app.container.make("db"), app);
|
|
24
|
+
},
|
|
25
|
+
async shutdown(app) {
|
|
26
|
+
const def = app.config.get("db");
|
|
27
|
+
if (app.container.has("db")) await def.disconnect?.(await app.container.make("db"), app);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Returns the connected client from an app's container, typed as `T`: `useDb<AppDb>(ctx.app)`.
|
|
32
|
+
* Throws when the app has no `db` binding, or when an async `connect` has not finished (call it after `app.boot()`).
|
|
33
|
+
*/
|
|
34
|
+
function useDb(app) {
|
|
35
|
+
return app.container.use("db");
|
|
36
|
+
}
|
|
37
|
+
export { dbProvider, dbProvider as default, defineConfig, defineDb, useDb };
|
package/dist/kitr.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** `kitr add db`: bind any ORM as `db` with connect/disconnect in the lifecycle. No ORM shipped. */
|
|
2
|
+
declare const _default: {
|
|
3
|
+
readonly name: "db";
|
|
4
|
+
readonly requires: ["server"];
|
|
5
|
+
readonly add: (ctx: import("@kitr/core/package").AddContext) => void;
|
|
6
|
+
readonly remove: (ctx: import("@kitr/core/package").AddContext) => void;
|
|
7
|
+
readonly vite: {
|
|
8
|
+
readonly serverOnlyConfig: true;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export { _default as default };
|
package/dist/kitr.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { definePackage } from "@kitr/core/package";
|
|
2
|
+
// src/kitr.ts
|
|
3
|
+
/** `kitr add db`: bind any ORM as `db` with connect/disconnect in the lifecycle. No ORM shipped. */
|
|
4
|
+
var kitr_default = definePackage({
|
|
5
|
+
name: "db",
|
|
6
|
+
requires: ["server"],
|
|
7
|
+
add(ctx) {
|
|
8
|
+
ctx.provider.add("@kitr/db");
|
|
9
|
+
ctx.env.add({ DATABASE_URL: "postgres://localhost:5432/app" }, "Database");
|
|
10
|
+
ctx.config("db", `import { defineDb } from '@kitr/db'\n\n// Example with Drizzle + postgres.js: \`pnpm add drizzle-orm postgres\`\n// import { drizzle } from 'drizzle-orm/postgres-js'\n// import postgres from 'postgres'\n\nexport default defineDb({\n connect: async () => {\n // const client = postgres(process.env.DATABASE_URL!)\n // return drizzle(client)\n return {} as unknown\n },\n // disconnect: (db) => db.$client.end(),\n})\n`);
|
|
11
|
+
ctx.agents.add("db", `- \`config/db.ts\` connects the ORM; controllers and actions read it with \`useDb<AppDb>(ctx.app)\` or \`ctx.app.container.use('db')\`. Migrations are the ORM's own tooling.`);
|
|
12
|
+
},
|
|
13
|
+
remove(ctx) {
|
|
14
|
+
ctx.provider.remove("@kitr/db");
|
|
15
|
+
ctx.agents.remove("db");
|
|
16
|
+
},
|
|
17
|
+
vite: { serverOnlyConfig: true }
|
|
18
|
+
});
|
|
19
|
+
export { kitr_default as default };
|
package/docs/api.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: API reference
|
|
3
|
+
description: Every export of @kitr/db, with its signature.
|
|
4
|
+
order: 2
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# API reference
|
|
8
|
+
|
|
9
|
+
Every export of `@kitr/db`, read from the published type definitions. The [guide](/packages/db/guide) shows them in use.
|
|
10
|
+
|
|
11
|
+
## @kitr/db
|
|
12
|
+
|
|
13
|
+
### defineDb
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
function defineDb<T>(definition: DbDefinition<T>): DbDefinition<T>;
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Declares how the app connects to its database, in `config/db.ts`. Returns the definition
|
|
20
|
+
unchanged; it exists so `disconnect` and `onReady` receive the client type `connect` returns.
|
|
21
|
+
|
|
22
|
+
### useDb
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
function useDb<T>(app: KitrApp): T;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Returns the connected client from an app's container, typed as `T`: `useDb<AppDb>(ctx.app)`.
|
|
29
|
+
Throws when the app has no `db` binding, or when an async `connect` has not finished (call it after `app.boot()`).
|
|
30
|
+
|
|
31
|
+
### dbProvider
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const dbProvider: KitrProvider;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The provider `kitr add db` puts in `kitrrc.ts`. It binds `db` in the container from `config/db.ts`,
|
|
38
|
+
connects during boot, runs `onReady` when the server is ready and `disconnect` on shutdown.
|
|
39
|
+
|
|
40
|
+
### defineConfig
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const defineConfig: <const C extends DbDefinition<any>>(config: C) => C;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Untyped alias of `defineDb` for the `defineConfig` convention; prefer `defineDb`, which infers the client type.
|
|
47
|
+
|
|
48
|
+
### DbDefinition
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
interface DbDefinition<T> {
|
|
52
|
+
/** Opens the connection and returns the client. Runs once during `app.boot()`; a rejection fails boot. */
|
|
53
|
+
connect: (app: KitrApp) => T | Promise<T>;
|
|
54
|
+
/** Closes the client when the server shuts down (SIGINT, SIGTERM or `server.close()`). */
|
|
55
|
+
disconnect?: (db: T, app: KitrApp) => void | Promise<void>;
|
|
56
|
+
/** Runs once the server is ready to serve, after boot. Use it to check for pending migrations or warm a pool. */
|
|
57
|
+
onReady?: (db: T, app: KitrApp) => void | Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
kitr ships no ORM. `defineDb` binds whatever you use (Drizzle, Prisma, Kysely,
|
|
62
|
+
a raw pool) as `db` in the container with connect/disconnect in the lifecycle.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// config/db.ts
|
|
66
|
+
export default defineDb({ connect: () => drizzle(process.env.DATABASE_URL!), disconnect: (db) => db.$client.end() })
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
<!-- GENERATED by scripts/api-docs.mjs from the type definitions. Do not edit. -->
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Database
|
|
3
|
+
description: Connect your database so every request can use it.
|
|
4
|
+
order: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Database
|
|
8
|
+
|
|
9
|
+
Almost every app keeps its data in a database. `@kitr/db` opens the connection when your app starts, hands the same client to every controller, action and job, and closes it cleanly when the server stops.
|
|
10
|
+
|
|
11
|
+
> [!SUMMARY]
|
|
12
|
+
> Connect your database so every request can use it. Add it with `kitr add db`; the overview is at [/packages/db](/packages/db).
|
|
13
|
+
|
|
14
|
+
It ships no ORM. Bring [Drizzle](https://orm.drizzle.team), [Prisma](https://www.prisma.io/docs), [Kysely](https://kysely.dev) or a plain [`pg`](https://node-postgres.com) pool, and kitr runs it inside the [app lifecycle](/docs/configuration). It runs on the server only; for browser or device data, use [storage](/packages/storage).
|
|
15
|
+
|
|
16
|
+
The examples use Acme's Postgres database with `orders` and `projects` tables, read through Drizzle.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
kitr add db
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
<details>
|
|
25
|
+
<summary>Everything kitr add changes</summary>
|
|
26
|
+
|
|
27
|
+
What `kitr add` does:
|
|
28
|
+
|
|
29
|
+
- Adds the [server](/packages/server) package first when missing.
|
|
30
|
+
- Installs `@kitr/db`.
|
|
31
|
+
- Adds `'@kitr/db'` to `packages` and `() => import('@kitr/db')` to `providers` in `kitrrc.ts`.
|
|
32
|
+
- Appends `DATABASE_URL=postgres://localhost:5432/app` under a `# Database` comment to `.env.example`, and to `.env` when it exists.
|
|
33
|
+
- Writes `config/db.ts` (shown under Configure). An existing file is left alone.
|
|
34
|
+
- Regenerates `config/index.ts`, the config list the standalone server imports.
|
|
35
|
+
- Adds a `## kitr: db` section to `AGENTS.md`: "`config/db.ts` connects the ORM; controllers and actions read it with `useDb<AppDb>(ctx.app)` or `ctx.app.container.use('db')`. Migrations are the ORM's own tooling."
|
|
36
|
+
|
|
37
|
+
`kitr add db` asks no questions. Pass `--dry-run` to preview or `--no-install` to skip the package manager. The `server` kit includes db.
|
|
38
|
+
|
|
39
|
+
`kitr remove db` removes the provider, the `packages` entry, the dependency, the `AGENTS.md` section and `config/db.ts`. It leaves `DATABASE_URL`, your ORM packages and schema files.
|
|
40
|
+
|
|
41
|
+
</details>
|
|
42
|
+
|
|
43
|
+
## Configure
|
|
44
|
+
|
|
45
|
+
`kitr add db` writes this file. `connect` returns an empty object until you fill in your ORM.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// config/db.ts
|
|
49
|
+
import { defineDb } from '@kitr/db'
|
|
50
|
+
|
|
51
|
+
// Example with Drizzle + postgres.js: `pnpm add drizzle-orm postgres`
|
|
52
|
+
// import { drizzle } from 'drizzle-orm/postgres-js'
|
|
53
|
+
// import postgres from 'postgres'
|
|
54
|
+
|
|
55
|
+
export default defineDb({
|
|
56
|
+
connect: async () => {
|
|
57
|
+
// const client = postgres(process.env.DATABASE_URL!)
|
|
58
|
+
// return drizzle(client)
|
|
59
|
+
return {} as unknown
|
|
60
|
+
},
|
|
61
|
+
// disconnect: (db) => db.$client.end(),
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`defineDb` takes three functions, each receiving the app.
|
|
66
|
+
|
|
67
|
+
| Option | Default | Description |
|
|
68
|
+
| --- | --- | --- |
|
|
69
|
+
| `connect(app)` | required | Opens the connection and returns the client. Runs once, during `app.boot()`. If it throws or rejects, boot fails with that error. |
|
|
70
|
+
| `disconnect(db, app)` | none | Closes the client. Runs when the server shuts down: on `SIGINT`, `SIGTERM` or `server.close()`. |
|
|
71
|
+
| `onReady(db, app)` | none | Runs once after boot, when the server starts serving. Use it to check for pending migrations. |
|
|
72
|
+
|
|
73
|
+
| Variable | Used for |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `DATABASE_URL` | The connection string your `connect` reads. `kitr add` writes `postgres://localhost:5432/app` as the example value. |
|
|
76
|
+
|
|
77
|
+
Prefer `defineDb` to `defineConfig`: it infers the client type from `connect`, so `disconnect` and `onReady` receive a typed `db`.
|
|
78
|
+
|
|
79
|
+
## Connect with Drizzle
|
|
80
|
+
|
|
81
|
+
Install Drizzle and the postgres.js driver:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
pnpm add drizzle-orm postgres
|
|
85
|
+
pnpm add -D drizzle-kit
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Describe the tables in plain Drizzle.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// server/db/schema.ts
|
|
92
|
+
import { integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
|
|
93
|
+
|
|
94
|
+
export const projects = pgTable('projects', {
|
|
95
|
+
id: text('id').primaryKey(),
|
|
96
|
+
teamId: text('team_id').notNull(),
|
|
97
|
+
name: text('name').notNull(),
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
export const orders = pgTable('orders', {
|
|
101
|
+
id: text('id').primaryKey(),
|
|
102
|
+
teamId: text('team_id').notNull(),
|
|
103
|
+
customerEmail: text('customer_email').notNull(),
|
|
104
|
+
totalCents: integer('total_cents').notNull(),
|
|
105
|
+
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Fill in `config/db.ts` and export the client type as `AppDb`.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// config/db.ts
|
|
113
|
+
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
114
|
+
import postgres from 'postgres'
|
|
115
|
+
import { defineDb } from '@kitr/db'
|
|
116
|
+
import * as schema from '#server/db/schema'
|
|
117
|
+
|
|
118
|
+
const createDb = () => drizzle(postgres(process.env.DATABASE_URL!), { schema })
|
|
119
|
+
|
|
120
|
+
/** The client every controller and action gets from useDb<AppDb>(). */
|
|
121
|
+
export type AppDb = ReturnType<typeof createDb>
|
|
122
|
+
|
|
123
|
+
export default defineDb({
|
|
124
|
+
connect: () => createDb(),
|
|
125
|
+
disconnect: (db) => db.$client.end(),
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Point `DATABASE_URL` in `.env` at a running database:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
DATABASE_URL=postgres://ada@localhost:5432/acme
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Start the server with `kitr dev`. `connect` runs during boot, before the first request. If Postgres is not running, the server does not start and the log shows the driver's error, such as `ECONNREFUSED`.
|
|
136
|
+
|
|
137
|
+
## Query from a controller
|
|
138
|
+
|
|
139
|
+
In a controller, pass `ctx.app` to `useDb` with your client type:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// server/controllers/orders-controller.ts
|
|
143
|
+
import { desc, eq } from 'drizzle-orm'
|
|
144
|
+
import type { HttpContext } from '@kitr/api'
|
|
145
|
+
import { useDb } from '@kitr/db'
|
|
146
|
+
import type { AppDb } from '#config/db'
|
|
147
|
+
import { orders } from '#server/db/schema'
|
|
148
|
+
|
|
149
|
+
export default class OrdersController {
|
|
150
|
+
async index({ app, params }: HttpContext<{ teamId: string }>) {
|
|
151
|
+
const db = useDb<AppDb>(app)
|
|
152
|
+
return db
|
|
153
|
+
.select()
|
|
154
|
+
.from(orders)
|
|
155
|
+
.where(eq(orders.teamId, params.teamId))
|
|
156
|
+
.orderBy(desc(orders.createdAt))
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Register the route as usual with [api](/packages/api):
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// server/routes.ts
|
|
165
|
+
import { defineRoutes } from '@kitr/api'
|
|
166
|
+
import OrdersController from '#server/controllers/orders-controller'
|
|
167
|
+
|
|
168
|
+
export const routes = defineRoutes((router) =>
|
|
169
|
+
router.get('/api/teams/:teamId/orders', [OrdersController, 'index']),
|
|
170
|
+
)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A request returns the rows as JSON:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
curl http://localhost:3001/api/teams/team_acme/orders
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
[{ "id": "ord_1", "teamId": "team_acme", "customerEmail": "grace@acme.com", "totalCents": 4900, "createdAt": "2026-11-02T09:14:00.000Z" }]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Every request shares the same pooled client, not a new connection.
|
|
184
|
+
|
|
185
|
+
## Query from an action
|
|
186
|
+
|
|
187
|
+
An [action](/packages/actions) handler has no `ctx`; get the request's app with `useRequestApp` from `@kitr/server`:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
// app/actions/projects/rename.ts
|
|
191
|
+
import { eq } from 'drizzle-orm'
|
|
192
|
+
import { z } from 'zod'
|
|
193
|
+
import { defineMutation } from '@kitr/actions'
|
|
194
|
+
import { useDb } from '@kitr/db'
|
|
195
|
+
import { useRequestApp } from '@kitr/server'
|
|
196
|
+
import { app } from '#app/app'
|
|
197
|
+
import type { AppDb } from '#config/db'
|
|
198
|
+
import { projects } from '#server/db/schema'
|
|
199
|
+
|
|
200
|
+
export const renameProject = defineMutation('projects.rename', {
|
|
201
|
+
input: z.object({ id: z.string(), name: z.string().min(2) }),
|
|
202
|
+
effects: (project) => ({ invalidate: [['projects', 'list']], set: [{ key: ['projects', 'detail', project.id], data: project }] }),
|
|
203
|
+
handler: async ({ input }) => {
|
|
204
|
+
const db = useDb<AppDb>(useRequestApp(app))
|
|
205
|
+
const [project] = await db
|
|
206
|
+
.update(projects)
|
|
207
|
+
.set({ name: input.name })
|
|
208
|
+
.where(eq(projects.id, input.id))
|
|
209
|
+
.returning()
|
|
210
|
+
return project
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
> [!WARNING]
|
|
216
|
+
> Don't import `config/db.ts` into a component; import it from server code and actions only.
|
|
217
|
+
|
|
218
|
+
## Query from a service or job
|
|
219
|
+
|
|
220
|
+
Outside a request, such as a scheduled [job](/packages/jobs), pass the root app to `useDb`:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
// server/services/revenue.ts
|
|
224
|
+
import { eq, sum } from 'drizzle-orm'
|
|
225
|
+
import type { KitrApp } from 'kitr/app'
|
|
226
|
+
import { useDb } from '@kitr/db'
|
|
227
|
+
import type { AppDb } from '#config/db'
|
|
228
|
+
import { orders } from '#server/db/schema'
|
|
229
|
+
|
|
230
|
+
/** Total revenue for one team, in cents. */
|
|
231
|
+
export async function teamRevenue(app: KitrApp, teamId: string): Promise<number> {
|
|
232
|
+
const db = useDb<AppDb>(app)
|
|
233
|
+
const [row] = await db
|
|
234
|
+
.select({ total: sum(orders.totalCents) })
|
|
235
|
+
.from(orders)
|
|
236
|
+
.where(eq(orders.teamId, teamId))
|
|
237
|
+
return Number(row?.total ?? 0)
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Taking `app` as a parameter keeps the function testable (see Testing).
|
|
242
|
+
|
|
243
|
+
## Read the client without a type
|
|
244
|
+
|
|
245
|
+
`useDb<T>(app)` is a typed shortcut for `app.container.use('db')`:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
// server/controllers/health-controller.ts
|
|
249
|
+
import type { HttpContext } from '@kitr/api'
|
|
250
|
+
|
|
251
|
+
export default class HealthController {
|
|
252
|
+
async show({ app }: HttpContext) {
|
|
253
|
+
const db = app.container.use('db') // typed as unknown
|
|
254
|
+
return { ok: db !== undefined }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
> [!WARNING]
|
|
260
|
+
> `useDb` only casts. Derive `AppDb` from `connect`, as in the Drizzle example, rather than writing the type by hand.
|
|
261
|
+
|
|
262
|
+
Without the provider in `providers`, both throw `No binding named "db"`.
|
|
263
|
+
|
|
264
|
+
## Await the client before boot finishes
|
|
265
|
+
|
|
266
|
+
The provider resolves `connect` during `app.boot()`, so `useDb` is synchronous after boot. Code that runs earlier, such as another provider's `register`, must await the binding:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
// app/providers/project_stats.ts
|
|
270
|
+
import { defineProvider } from 'kitr/app'
|
|
271
|
+
import type { AppDb } from '#config/db'
|
|
272
|
+
import { projects } from '#server/db/schema'
|
|
273
|
+
|
|
274
|
+
export default defineProvider({
|
|
275
|
+
name: 'projectStats',
|
|
276
|
+
register(app) {
|
|
277
|
+
app.container.singleton('projectStats', async (container) => {
|
|
278
|
+
const db = (await container.make('db')) as AppDb
|
|
279
|
+
return { count: async () => (await db.select().from(projects)).length }
|
|
280
|
+
})
|
|
281
|
+
},
|
|
282
|
+
})
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
## Connect other ORMs
|
|
287
|
+
|
|
288
|
+
Any library works: whatever `connect` returns is the client.
|
|
289
|
+
|
|
290
|
+
### Prisma
|
|
291
|
+
|
|
292
|
+
```sh
|
|
293
|
+
pnpm add @prisma/client
|
|
294
|
+
pnpm add -D prisma
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
// config/db.ts
|
|
299
|
+
import { PrismaClient } from '@prisma/client'
|
|
300
|
+
import { defineDb } from '@kitr/db'
|
|
301
|
+
|
|
302
|
+
const createDb = () => new PrismaClient()
|
|
303
|
+
export type AppDb = ReturnType<typeof createDb>
|
|
304
|
+
|
|
305
|
+
export default defineDb({
|
|
306
|
+
connect: async () => {
|
|
307
|
+
const prisma = createDb()
|
|
308
|
+
await prisma.$connect()
|
|
309
|
+
return prisma
|
|
310
|
+
},
|
|
311
|
+
disconnect: (prisma) => prisma.$disconnect(),
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Calling `$connect()` makes a bad URL fail boot instead of the first query. See the [Prisma Client docs](https://www.prisma.io/docs/orm/prisma-client).
|
|
316
|
+
|
|
317
|
+
### Kysely
|
|
318
|
+
|
|
319
|
+
```sh
|
|
320
|
+
pnpm add kysely pg
|
|
321
|
+
pnpm add -D @types/pg
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
// config/db.ts
|
|
326
|
+
import { Kysely, PostgresDialect } from 'kysely'
|
|
327
|
+
import { Pool } from 'pg'
|
|
328
|
+
import { defineDb } from '@kitr/db'
|
|
329
|
+
import type { Database } from '#server/db/types'
|
|
330
|
+
|
|
331
|
+
export type AppDb = Kysely<Database>
|
|
332
|
+
|
|
333
|
+
export default defineDb({
|
|
334
|
+
connect: (): AppDb =>
|
|
335
|
+
new Kysely<Database>({
|
|
336
|
+
dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) }),
|
|
337
|
+
}),
|
|
338
|
+
disconnect: (db) => db.destroy(),
|
|
339
|
+
})
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`Database` is your table interface, by hand or from `kysely-codegen`. See the [Kysely docs](https://kysely.dev/docs/getting-started).
|
|
343
|
+
|
|
344
|
+
### A plain pg pool
|
|
345
|
+
|
|
346
|
+
```sh
|
|
347
|
+
pnpm add pg
|
|
348
|
+
pnpm add -D @types/pg
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
// config/db.ts
|
|
353
|
+
import { Pool } from 'pg'
|
|
354
|
+
import { defineDb } from '@kitr/db'
|
|
355
|
+
|
|
356
|
+
export type AppDb = Pool
|
|
357
|
+
|
|
358
|
+
export default defineDb({
|
|
359
|
+
connect: async () => {
|
|
360
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 })
|
|
361
|
+
await pool.query('select 1')
|
|
362
|
+
return pool
|
|
363
|
+
},
|
|
364
|
+
disconnect: (pool) => pool.end(),
|
|
365
|
+
})
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The `select 1` checks the URL during boot, because a `Pool` opens no connection until its first query. See the [pooling docs](https://node-postgres.com/features/pooling).
|
|
369
|
+
|
|
370
|
+
### SQLite with Drizzle
|
|
371
|
+
|
|
372
|
+
```sh
|
|
373
|
+
pnpm add drizzle-orm better-sqlite3
|
|
374
|
+
pnpm add -D @types/better-sqlite3
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
// config/db.ts
|
|
379
|
+
import Database from 'better-sqlite3'
|
|
380
|
+
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
|
381
|
+
import { defineDb } from '@kitr/db'
|
|
382
|
+
import * as schema from '#server/db/schema'
|
|
383
|
+
|
|
384
|
+
const createDb = () => drizzle(new Database(process.env.DATABASE_URL ?? 'acme.db'), { schema })
|
|
385
|
+
export type AppDb = ReturnType<typeof createDb>
|
|
386
|
+
|
|
387
|
+
export default defineDb({
|
|
388
|
+
connect: () => createDb(),
|
|
389
|
+
disconnect: (db) => db.$client.close(),
|
|
390
|
+
})
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
Here `DATABASE_URL` is a file path such as `acme.db`, and tables come from `drizzle-orm/sqlite-core`. See [Drizzle with SQLite](https://orm.drizzle.team/docs/get-started-sqlite).
|
|
394
|
+
|
|
395
|
+
## Validate DATABASE_URL at startup
|
|
396
|
+
|
|
397
|
+
A missing `process.env.DATABASE_URL!` crashes deep inside the driver. Declare it in the typed env so the app refuses to start with a clear message:
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
// app/env.ts
|
|
401
|
+
import { createEnv, env as e } from 'kitr/env'
|
|
402
|
+
|
|
403
|
+
export const env = createEnv({
|
|
404
|
+
public: {},
|
|
405
|
+
server: {
|
|
406
|
+
DATABASE_URL: e.url(),
|
|
407
|
+
},
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
// config/db.ts
|
|
413
|
+
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
414
|
+
import postgres from 'postgres'
|
|
415
|
+
import { defineDb } from '@kitr/db'
|
|
416
|
+
import { env } from '#app/env'
|
|
417
|
+
import * as schema from '#server/db/schema'
|
|
418
|
+
|
|
419
|
+
const createDb = () => drizzle(postgres(env.DATABASE_URL), { schema })
|
|
420
|
+
export type AppDb = ReturnType<typeof createDb>
|
|
421
|
+
|
|
422
|
+
export default defineDb({
|
|
423
|
+
connect: () => createDb(),
|
|
424
|
+
disconnect: (db) => db.$client.end(),
|
|
425
|
+
})
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
## Change settings per environment
|
|
430
|
+
|
|
431
|
+
Read `app.isProduction` or `app.environment` in `connect` to size the pool or turn on TLS only where it matters:
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
// config/db.ts
|
|
435
|
+
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
436
|
+
import postgres from 'postgres'
|
|
437
|
+
import { defineDb } from '@kitr/db'
|
|
438
|
+
import * as schema from '#server/db/schema'
|
|
439
|
+
|
|
440
|
+
const createDb = (production: boolean) =>
|
|
441
|
+
drizzle(
|
|
442
|
+
postgres(process.env.DATABASE_URL!, {
|
|
443
|
+
max: production ? 20 : 5,
|
|
444
|
+
ssl: production ? 'require' : false,
|
|
445
|
+
}),
|
|
446
|
+
{ schema },
|
|
447
|
+
)
|
|
448
|
+
export type AppDb = ReturnType<typeof createDb>
|
|
449
|
+
|
|
450
|
+
export default defineDb({
|
|
451
|
+
connect: (app) => createDb(app.isProduction),
|
|
452
|
+
disconnect: (db) => db.$client.end(),
|
|
453
|
+
})
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
## Check migrations when the server starts
|
|
458
|
+
|
|
459
|
+
`onReady` runs once after boot, when the server starts serving. Use it to warn about, or refuse to run with, an outdated schema:
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
// config/db.ts
|
|
463
|
+
import { sql } from 'drizzle-orm'
|
|
464
|
+
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
465
|
+
import postgres from 'postgres'
|
|
466
|
+
import { defineDb } from '@kitr/db'
|
|
467
|
+
import * as schema from '#server/db/schema'
|
|
468
|
+
|
|
469
|
+
const createDb = () => drizzle(postgres(process.env.DATABASE_URL!), { schema })
|
|
470
|
+
export type AppDb = ReturnType<typeof createDb>
|
|
471
|
+
|
|
472
|
+
const EXPECTED_MIGRATIONS = 12
|
|
473
|
+
|
|
474
|
+
export default defineDb({
|
|
475
|
+
connect: () => createDb(),
|
|
476
|
+
disconnect: (db) => db.$client.end(),
|
|
477
|
+
onReady: async (db, app) => {
|
|
478
|
+
const rows = await db.execute(sql`select count(*)::int as count from drizzle.__drizzle_migrations`)
|
|
479
|
+
const applied = Number(rows[0]?.count ?? 0)
|
|
480
|
+
if (applied < EXPECTED_MIGRATIONS) {
|
|
481
|
+
if (app.isProduction) throw new Error(`Database is ${EXPECTED_MIGRATIONS - applied} migrations behind`)
|
|
482
|
+
app.container.use('logger').warn(`Run pnpm db:migrate: ${EXPECTED_MIGRATIONS - applied} pending`)
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
})
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
`onReady` does not run under `createTestApp`. The logger comes from [logger](/packages/logger).
|
|
489
|
+
|
|
490
|
+
## Run migrations
|
|
491
|
+
|
|
492
|
+
Your ORM's migration tools work unchanged. Add scripts to `package.json`:
|
|
493
|
+
|
|
494
|
+
```json
|
|
495
|
+
{
|
|
496
|
+
"scripts": {
|
|
497
|
+
"db:generate": "drizzle-kit generate",
|
|
498
|
+
"db:migrate": "drizzle-kit migrate",
|
|
499
|
+
"db:studio": "drizzle-kit studio"
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
// drizzle.config.ts
|
|
506
|
+
import { defineConfig } from 'drizzle-kit'
|
|
507
|
+
|
|
508
|
+
export default defineConfig({
|
|
509
|
+
schema: './server/db/schema.ts',
|
|
510
|
+
out: './server/db/migrations',
|
|
511
|
+
dialect: 'postgresql',
|
|
512
|
+
dbCredentials: { url: process.env.DATABASE_URL! },
|
|
513
|
+
})
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Run `pnpm db:migrate` in your deploy step, before the new server starts. See [Drizzle migrations](https://orm.drizzle.team/docs/migrations).
|
|
517
|
+
|
|
518
|
+
> [!WARNING]
|
|
519
|
+
> Don't run migrations from `connect` or `onReady`: two instances migrating at once can leave the database half-migrated. Run them once, from the deploy pipeline.
|
|
520
|
+
|
|
521
|
+
## Use a transaction
|
|
522
|
+
|
|
523
|
+
Transactions belong to your ORM; call its API on the client from `useDb`:
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
// server/services/checkout.ts
|
|
527
|
+
import { eq } from 'drizzle-orm'
|
|
528
|
+
import type { KitrApp } from 'kitr/app'
|
|
529
|
+
import { useDb } from '@kitr/db'
|
|
530
|
+
import type { AppDb } from '#config/db'
|
|
531
|
+
import { orders, projects } from '#server/db/schema'
|
|
532
|
+
|
|
533
|
+
export async function placeOrder(app: KitrApp, input: { id: string; teamId: string; projectId: string }) {
|
|
534
|
+
const db = useDb<AppDb>(app)
|
|
535
|
+
return db.transaction(async (tx) => {
|
|
536
|
+
const [project] = await tx.select().from(projects).where(eq(projects.id, input.projectId))
|
|
537
|
+
if (!project) throw new Error(`Project ${input.projectId} not found`)
|
|
538
|
+
const [order] = await tx
|
|
539
|
+
.insert(orders)
|
|
540
|
+
.values({ id: input.id, teamId: input.teamId, customerEmail: 'linus@acme.com', totalCents: 4_900 })
|
|
541
|
+
.returning()
|
|
542
|
+
return order
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
## Close the connection on shutdown
|
|
549
|
+
|
|
550
|
+
On `SIGINT` or `SIGTERM` the server stops accepting requests, then calls your `disconnect`, so open queries finish before the pool closes:
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
// server/main.ts
|
|
554
|
+
import { mountRoutes } from '@kitr/api'
|
|
555
|
+
import { createServer } from '@kitr/server'
|
|
556
|
+
import { config } from '../config'
|
|
557
|
+
import rc from '../kitrrc'
|
|
558
|
+
import { routes } from './routes'
|
|
559
|
+
|
|
560
|
+
export const server = createServer({ providers: rc.providers, config })
|
|
561
|
+
mountRoutes(server, routes)
|
|
562
|
+
|
|
563
|
+
await server.listen({ port: 3001 })
|
|
564
|
+
// Ctrl-C logs "SIGINT received, shutting down", then calls disconnect(db, app).
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Without `disconnect`, the database sees connections drop instead of closing.
|
|
568
|
+
|
|
569
|
+
## Errors
|
|
570
|
+
|
|
571
|
+
Driver errors (`ECONNREFUSED`, authentication failures) pass through unchanged. These come from a wrong database binding:
|
|
572
|
+
|
|
573
|
+
| Error | When | What to do |
|
|
574
|
+
| --- | --- | --- |
|
|
575
|
+
| `[kitr] Missing config "db". Create config/db.ts or pass it to createApp().` | The provider is registered but there is no `config/db.ts`, or a test passed no `db` config. | Run `kitr add db` again, or pass `config: { db: defineDb(...) }`. |
|
|
576
|
+
| `[kitr] No binding named "db". Did you add the package that provides it?` | `useDb` on an app without the db provider. | Add `() => import('@kitr/db')` to `providers` in `kitrrc.ts`, or `providers: [dbProvider]` in a test. |
|
|
577
|
+
| `[kitr] Binding "db" has an async factory; await container.make('db') before use('db').` | `useDb` before `app.boot()` finished. | Await `app.boot()` first, or use `await app.container.make('db')`. |
|
|
578
|
+
| The error `connect` threw | `connect` failed, for example the database is down. | Boot rejects with it and the server does not start. Fix the URL or start the database. |
|
|
579
|
+
|
|
580
|
+
## Testing
|
|
581
|
+
|
|
582
|
+
Build the app with `createTestApp` from [testing](/packages/testing) and give `config` a `db` whose `connect` returns a fake. Code under test calls `useDb` unchanged. This tests `teamRevenue` from "Query from a service or job":
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
// server/services/revenue.test.ts
|
|
586
|
+
import { dbProvider, defineDb } from '@kitr/db'
|
|
587
|
+
import { createTestApp } from '@kitr/testing'
|
|
588
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
589
|
+
import type { AppDb } from '#config/db'
|
|
590
|
+
import { teamRevenue } from '#server/services/revenue'
|
|
591
|
+
|
|
592
|
+
describe('teamRevenue', () => {
|
|
593
|
+
it('sums the team orders and closes the client on shutdown', async () => {
|
|
594
|
+
const where = vi.fn(async () => [{ total: '16900' }])
|
|
595
|
+
const fakeDb = { select: () => ({ from: () => ({ where }) }) } as unknown as AppDb
|
|
596
|
+
const disconnect = vi.fn()
|
|
597
|
+
|
|
598
|
+
const app = await createTestApp({
|
|
599
|
+
providers: [dbProvider],
|
|
600
|
+
config: { db: defineDb({ connect: () => fakeDb, disconnect }) },
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
expect(await teamRevenue(app, 'team_acme')).toBe(16_900)
|
|
604
|
+
expect(where).toHaveBeenCalledOnce()
|
|
605
|
+
|
|
606
|
+
await app.shutdown()
|
|
607
|
+
expect(disconnect).toHaveBeenCalledWith(fakeDb, app)
|
|
608
|
+
})
|
|
609
|
+
})
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
`createTestApp` runs `connect` but not `onReady`; call `await app.ready()` for that. To replace the client in an app that has booted, bind a value over it:
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
// test/orders.test.ts
|
|
616
|
+
import { dbProvider, defineDb, useDb } from '@kitr/db'
|
|
617
|
+
import { createTestApp } from '@kitr/testing'
|
|
618
|
+
import { expect, it } from 'vitest'
|
|
619
|
+
|
|
620
|
+
it('uses the stub bound after boot', async () => {
|
|
621
|
+
const app = await createTestApp({
|
|
622
|
+
providers: [dbProvider],
|
|
623
|
+
config: { db: defineDb({ connect: () => ({ name: 'real' }) }) },
|
|
624
|
+
})
|
|
625
|
+
app.container.value('db', { name: 'stub' })
|
|
626
|
+
expect(useDb<{ name: string }>(app)).toEqual({ name: 'stub' })
|
|
627
|
+
})
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Bind after boot, not before: the provider's `register` replaces anything bound under `db`.
|
|
631
|
+
|
|
632
|
+
For real queries, point `DATABASE_URL` at a throwaway database in `.env.test`, or use [PGlite](https://pglite.dev), which runs Postgres in-process through `drizzle-orm/pglite`. To test a controller end to end, call it with `createTestClient(server)` from [testing](/packages/testing).
|
|
633
|
+
|
|
634
|
+
## Next steps
|
|
635
|
+
|
|
636
|
+
- [API reference](/packages/db/api): `defineDb`, `dbProvider`, `useDb`.
|
|
637
|
+
- [API routes](/packages/api): routes and controllers that read the database.
|
|
638
|
+
- [Actions](/packages/actions): server functions your components call.
|
package/docs/overview.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Overview
|
|
3
|
+
description: Connect your database so every request can use it.
|
|
4
|
+
order: 0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Database
|
|
8
|
+
|
|
9
|
+
## What it is
|
|
10
|
+
|
|
11
|
+
`@kitr/db` connects your database to the app lifecycle. Describe the connection once with `defineDb`; the provider opens it at boot, hands it to `useDb()` everywhere on the server and closes it on shutdown.
|
|
12
|
+
|
|
13
|
+
It ships no ORM and runs on the server only. For data on the device, use [storage](/packages/storage).
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
kitr add db
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Adds `@kitr/db`, its provider, `config/db.ts` and `DATABASE_URL` in `.env.example`. Bring your own ORM. [Every change](/packages/db/guide#install).
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
With Drizzle and postgres.js installed (`pnpm add drizzle-orm postgres`), describe the connection once:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// config/db.ts
|
|
29
|
+
import { drizzle } from 'drizzle-orm/postgres-js'
|
|
30
|
+
import postgres from 'postgres'
|
|
31
|
+
import { defineDb } from '@kitr/db'
|
|
32
|
+
import * as schema from '#server/db/schema'
|
|
33
|
+
|
|
34
|
+
const createDb = () => drizzle(postgres(process.env.DATABASE_URL!), { schema })
|
|
35
|
+
|
|
36
|
+
/** The client every controller and action gets from useDb<AppDb>(). */
|
|
37
|
+
export type AppDb = ReturnType<typeof createDb>
|
|
38
|
+
|
|
39
|
+
export default defineDb({
|
|
40
|
+
connect: () => createDb(),
|
|
41
|
+
disconnect: (db) => db.$client.end(),
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then read it anywhere on the server:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// server/controllers/orders_controller.ts
|
|
49
|
+
import type { HttpContext } from '@kitr/api'
|
|
50
|
+
import { useDb } from '@kitr/db'
|
|
51
|
+
import type { AppDb } from '#config/db'
|
|
52
|
+
|
|
53
|
+
export default class OrdersController {
|
|
54
|
+
async index({ app }: HttpContext) {
|
|
55
|
+
return useDb<AppDb>(app).query.orders.findMany()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Point `DATABASE_URL` at a running database and run `kitr dev`. The provider connects at boot and refuses to start if the database is unreachable. Next in the guide: [Connect with Drizzle](/packages/db/guide#connect-with-drizzle).
|
|
61
|
+
|
|
62
|
+
## Built on
|
|
63
|
+
|
|
64
|
+
`@kitr/db` is built on the kitr [server](/packages/server) lifecycle and container. The database client itself is whichever you bring:
|
|
65
|
+
|
|
66
|
+
| ORM or driver | Docs |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| Drizzle (the guide's examples) | [orm.drizzle.team](https://orm.drizzle.team/docs/overview) |
|
|
69
|
+
| Prisma | [prisma.io/docs](https://www.prisma.io/docs) |
|
|
70
|
+
| Kysely | [kysely.dev](https://kysely.dev) |
|
|
71
|
+
| node-postgres | [node-postgres.com](https://node-postgres.com) |
|
|
72
|
+
|
|
73
|
+
kitr adds the typed definition, startup validation of `DATABASE_URL`, the migration check, `useDb()` and clean shutdown.
|
|
74
|
+
|
|
75
|
+
## Problems it solves
|
|
76
|
+
|
|
77
|
+
- **A client created in every file.** [Connect with Drizzle](/packages/db/guide#connect-with-drizzle).
|
|
78
|
+
- **Database code in controllers, actions and jobs looking different.** [Query from a controller](/packages/db/guide#query-from-a-controller).
|
|
79
|
+
- **A bad connection string found on the first request.** [Validate DATABASE_URL at startup](/packages/db/guide#validate-database_url-at-startup).
|
|
80
|
+
- **Deploying code ahead of its migrations.** [Check migrations when the server starts](/packages/db/guide#check-migrations-when-the-server-starts).
|
|
81
|
+
- **Connections left open on shutdown.** [Close the connection on shutdown](/packages/db/guide#close-the-connection-on-shutdown).
|
|
82
|
+
|
|
83
|
+
## Further reading
|
|
84
|
+
|
|
85
|
+
- [Guide](/packages/db/guide).
|
|
86
|
+
- [API reference](/packages/db/api).
|
|
87
|
+
- [Drizzle ORM docs](https://orm.drizzle.team/docs/overview).
|
|
88
|
+
- [API routes](/packages/api) and [Actions](/packages/actions).
|
|
89
|
+
- [Server](/packages/server).
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitr/db",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "@kitr/db: bind any ORM (Drizzle, Prisma, Kysely) as ctx.db with lifecycle hooks. No ORM shipped.",
|
|
5
|
+
"kitr": {
|
|
6
|
+
"name": "db",
|
|
7
|
+
"title": "Database",
|
|
8
|
+
"summary": "Connect your database so every request can use it.",
|
|
9
|
+
"category": "backend",
|
|
10
|
+
"runsOn": "server",
|
|
11
|
+
"requires": [
|
|
12
|
+
"server"
|
|
13
|
+
],
|
|
14
|
+
"docs": "docs",
|
|
15
|
+
"setup": "./kitr"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/kitrjs/kitr.git",
|
|
22
|
+
"directory": "packages/db"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://kitr.dev/packages/db",
|
|
25
|
+
"bugs": "https://github.com/kitrjs/kitr/issues",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./kitr": {
|
|
36
|
+
"types": "./dist/kitr.d.ts",
|
|
37
|
+
"default": "./dist/kitr.js"
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"docs"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@kitr/core": "0.2.1"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"tsdown": "^0.23.7",
|
|
51
|
+
"typescript": "^5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"keywords": [
|
|
54
|
+
"kitr",
|
|
55
|
+
"kitr-package"
|
|
56
|
+
],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsdown",
|
|
59
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
60
|
+
}
|
|
6
61
|
}
|