@nano-app/backend 0.18.0 → 0.19.0
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/README.md +34 -50
- package/build/index.d.ts +31 -21
- package/build/index.js +211 -264
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ export default createRoute({
|
|
|
48
48
|
|
|
49
49
|
## Routes
|
|
50
50
|
|
|
51
|
-
Each `*.route.ts` under `
|
|
51
|
+
Each `*.route.ts` under `routesDir` default-exports an object with the HTTP methods it implements (`GET`, `POST`, `PUT`, `DELETE`, `QUERY`). **The file path is the URL** (`index.route.ts` → `/`, `api/users.route.ts` → `/api/users`). A route file holds only those methods and its options (`expression`, `public`, `permissions`, `callbacks`): any other key, or an invalid value, stops the startup with the file and what is wrong. `expression` is appended to that path as is, in Express syntax (`/:id`, `{/:id}` for optional):
|
|
52
52
|
|
|
53
53
|
```ts
|
|
54
54
|
export default createRoute({
|
|
@@ -84,10 +84,12 @@ Every option is optional.
|
|
|
84
84
|
| `listener` | `() => {}` | Callback once the server is listening. |
|
|
85
85
|
| `settings` | `{ "trust proxy": <private networks> }` | Values for `app.set()`: see [Client IP](#client-ip). |
|
|
86
86
|
| `packages` | see below | Framework base middlewares. |
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
87
|
+
| `routesDir` | `"src/routes"` | Folder of your routes, relative to `cwd`. |
|
|
88
|
+
| `database` | none | Your Drizzle schema: see [Database](#database). |
|
|
89
|
+
| `auth` | none | Better Auth options: see [Better Auth](#better-auth). |
|
|
90
|
+
| `plugins` | `{}` | Plugins mounted `before` and `after` your routes, in order: see [Plugins](#plugins). |
|
|
89
91
|
|
|
90
|
-
`packages` accepts `morgan`, `cors`, `helmet`, `compression`, `express` (`express.json` options) and `urlencoded`.
|
|
92
|
+
`packages` accepts `morgan`, `cors`, `helmet`, `compression`, `express` (`express.json` options) and `urlencoded`. Each one you set replaces its default whole and the others keep theirs; pass `false` to disable one. `settings` works the same way, key by key.
|
|
91
93
|
|
|
92
94
|
`compression` is off by default: the proxy in front of the app (an ingress, nginx) usually compresses the answers. Turn it on with `packages: { compression: true }` when nothing does.
|
|
93
95
|
|
|
@@ -114,7 +116,7 @@ By default the backend reads that header only when the request comes from loopba
|
|
|
114
116
|
|
|
115
117
|
## Database
|
|
116
118
|
|
|
117
|
-
Off until the app passes its schema: nano-app creates a [Drizzle](https://orm.drizzle.team) client of it on `DATABASE_URL` and passes it to your handlers as `db`.
|
|
119
|
+
Off until the app passes its schema: nano-app creates a [Drizzle](https://orm.drizzle.team) client of it on `DATABASE_URL` and passes it to your handlers as `db`. It changes your tables only when you ask: `push` or `migrations`. Without them, applying the schema (`drizzle-kit push`, `drizzle-kit generate` and `migrate`) is up to you.
|
|
118
120
|
|
|
119
121
|
```bash
|
|
120
122
|
pnpm add drizzle-orm pg
|
|
@@ -136,7 +138,7 @@ export const users = pgTable('users', {
|
|
|
136
138
|
import { createNanoApp } from '@nano-app/backend';
|
|
137
139
|
import * as schema from './db/schema';
|
|
138
140
|
|
|
139
|
-
createNanoApp({
|
|
141
|
+
createNanoApp({ database: { schema } });
|
|
140
142
|
```
|
|
141
143
|
|
|
142
144
|
```ts
|
|
@@ -149,63 +151,46 @@ export default createRoute({
|
|
|
149
151
|
```
|
|
150
152
|
|
|
151
153
|
- `database.schema` is the module of your tables: every export that is a table, and `relations` (from `defineRelations`) when you relate them. A `pgSchema` the tables use is exported too, so drizzle-kit creates it.
|
|
152
|
-
-
|
|
154
|
+
- `database.push: true` applies the schema at startup with drizzle-kit's push (`pnpm add drizzle-kit`), only to the Postgres schemas of its tables. A change that would lose data stops the startup.
|
|
155
|
+
- `database.migrations` is the folder `drizzle-kit generate` writes: its pending migrations are applied at startup.
|
|
156
|
+
- One pool serves the app, Better Auth and every plugin with a database. It connects on its first query: a missing table fails that query, not the startup.
|
|
153
157
|
- Startup fails if `DATABASE_URL` is missing.
|
|
154
|
-
-
|
|
158
|
+
- Each `nano_*` Postgres schema is reserved for the plugin of its name (`nano_auth` for Better Auth): a table of yours in one fails the startup.
|
|
155
159
|
- Without a database, `db` is `undefined`.
|
|
156
160
|
- Drizzle and pg are loaded only when your database or a mounted plugin's is on.
|
|
157
161
|
|
|
158
162
|
## Better Auth
|
|
159
163
|
|
|
160
|
-
Users, sign-in and sessions with [Better Auth](https://www.better-auth.com)
|
|
164
|
+
Users, sign-in and sessions with [Better Auth](https://www.better-auth.com). On when `createNanoApp` has `auth`: its options, everything but `database`. Without it, it stays off.
|
|
161
165
|
|
|
162
166
|
```bash
|
|
163
|
-
pnpm add better-auth
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
```ts
|
|
167
|
-
// src/auth.ts: nano-app and the Better Auth CLI read `options`
|
|
168
|
-
import type { BetterAuthOptions } from 'better-auth';
|
|
169
|
-
|
|
170
|
-
export default {
|
|
171
|
-
options: { emailAndPassword: { enabled: true } } satisfies BetterAuthOptions,
|
|
172
|
-
};
|
|
167
|
+
pnpm add better-auth kysely pg
|
|
173
168
|
```
|
|
174
169
|
|
|
175
170
|
```ts
|
|
176
171
|
// src/app.ts
|
|
177
172
|
import { createNanoApp } from '@nano-app/backend';
|
|
178
|
-
import * as schema from './db/schema';
|
|
179
|
-
|
|
180
|
-
createNanoApp({ mainPlugin: { database: { schema } } });
|
|
181
|
-
```
|
|
182
173
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
```bash
|
|
186
|
-
npx auth generate --adapter drizzle --dialect pg --output src/db/auth-schema.ts # then export its tables from src/db/schema.ts
|
|
187
|
-
pnpm drizzle-kit push
|
|
174
|
+
createNanoApp({ auth: { emailAndPassword: { enabled: true } } });
|
|
188
175
|
```
|
|
189
176
|
|
|
190
|
-
- It
|
|
191
|
-
-
|
|
192
|
-
- Its endpoints are mounted
|
|
177
|
+
- It uses the pool of `DATABASE_URL`, with or without a database of the app: startup fails if it is missing.
|
|
178
|
+
- Its tables live in the Postgres schema `nano_auth`. nano-app creates them at startup with Better Auth's migrations, and adds what new options need; it never drops anything.
|
|
179
|
+
- Its endpoints are mounted before your routes, at its `basePath` (default `/api/auth`), and stay public.
|
|
193
180
|
- In production, Better Auth limits sign-in, sign-up and password reset to a few attempts per client IP (its `rateLimit`): see [Client IP](#client-ip).
|
|
194
|
-
-
|
|
195
|
-
-
|
|
181
|
+
- With it on, every route of the app and of every plugin needs a session: without one it answers 401 `Not authenticated`.
|
|
182
|
+
- `createRoute({ public: true, ... })` answers without a session. An unknown path answers 404.
|
|
183
|
+
- Handlers and `onBefore` receive the session in `params.session` (`{ user, session }`), also on a public route when the request has one.
|
|
184
|
+
- The middlewares a plugin adds with `app.use` in `onMount` are not checked: a plugin that exposes something public says it in its README.
|
|
196
185
|
- Sign-up works as Better Auth sets it: with `emailAndPassword` anyone can sign up and reach your routes. Close it with `disableSignUp: true`, or check roles in your handlers.
|
|
197
|
-
-
|
|
186
|
+
- Extra user fields go in its `user.additionalFields`.
|
|
198
187
|
- Set `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` in the environment, as Better Auth asks.
|
|
199
|
-
-
|
|
200
|
-
-
|
|
201
|
-
- Its own endpoints, the plugins mounted before the app and the middlewares a plugin adds with `app.use` in `onMount` are not checked: a plugin that exposes something public says it in its README.
|
|
202
|
-
- Better Auth is loaded only when it is on.
|
|
203
|
-
- Before 0.14.0 the options went in `packages.betterAuth`: move them to the default export of `src/auth.ts`.
|
|
204
|
-
- nano-app creates the one instance the app uses: do not call `betterAuth()` in `src/auth.ts`. The CLI's `generate` only reads `options` of its default export.
|
|
188
|
+
- Better Auth is loaded only when it is on, and nano-app creates the one instance the app uses.
|
|
189
|
+
- Before 0.19.0 the options went in the default export of `src/auth.ts` and its tables in your schema: move the options to `auth`.
|
|
205
190
|
|
|
206
191
|
### Permissions
|
|
207
192
|
|
|
208
|
-
A route asks for permissions of Better Auth's [admin plugin](https://www.better-auth.com/docs/plugins/admin): add `admin()` to the `plugins` of your options, with your roles (`ac` and `roles`)
|
|
193
|
+
A route asks for permissions of Better Auth's [admin plugin](https://www.better-auth.com/docs/plugins/admin): add `admin()` to the `plugins` of your options, with your roles (`ac` and `roles`). A user without them gets 403:
|
|
209
194
|
|
|
210
195
|
```ts
|
|
211
196
|
export default createRoute({
|
|
@@ -220,7 +205,7 @@ A public route does not check them. Without Better Auth the route answers anyway
|
|
|
220
205
|
|
|
221
206
|
## Plugins
|
|
222
207
|
|
|
223
|
-
A plugin is a group of endpoints mounted under a base path. Your routes are mounted as one (named `app`). The official plugins are in [`@nano-app/plugins`](https://github.com/Nano-Nathan/nano-app/blob/main/plugins/README.md). You write one with `createPlugin` and list it in `plugins`. Nothing is mounted unless you list it.
|
|
208
|
+
A plugin is a group of endpoints mounted under a base path. Your routes are mounted as one (named `app`). The official plugins are in [`@nano-app/plugins`](https://github.com/Nano-Nathan/nano-app/blob/main/plugins/README.md). You write one with `createPlugin` and list it in `plugins.before` or `plugins.after`. Nothing is mounted unless you list it.
|
|
224
209
|
|
|
225
210
|
```ts
|
|
226
211
|
// src/plugins/hello/index.ts
|
|
@@ -249,10 +234,10 @@ export default createRoute({
|
|
|
249
234
|
import { createNanoApp } from '@nano-app/backend';
|
|
250
235
|
import hello from './plugins/hello';
|
|
251
236
|
|
|
252
|
-
createNanoApp({ plugins: [hello()] });
|
|
237
|
+
createNanoApp({ plugins: { before: [hello()] } });
|
|
253
238
|
```
|
|
254
239
|
|
|
255
|
-
`createPlugin` types the options of that plugin. Each plugin chooses its base path
|
|
240
|
+
`createPlugin` types the options of that plugin. Each plugin chooses its base path; the app chooses whether it mounts `before` or `after` its routes. What the plugin puts in `options` reaches every callback and handler of its routes in `params`.
|
|
256
241
|
|
|
257
242
|
### Callbacks
|
|
258
243
|
|
|
@@ -277,11 +262,11 @@ export default createPlugin((options?: { publicPaths?: string[] }) => ({
|
|
|
277
262
|
}));
|
|
278
263
|
```
|
|
279
264
|
|
|
280
|
-
The endpoints of one plugin have no guaranteed order: one route's `onInit` must not depend on another's.
|
|
265
|
+
The endpoints of one plugin have no guaranteed order: one route's `onInit` must not depend on another's.
|
|
281
266
|
|
|
282
|
-
Everything is mounted on one Express application, in this order: base middlewares → `before` plugins → your routes → `after` plugins → 404 → error handler.
|
|
267
|
+
Everything is mounted on one Express application, in this order: base middlewares → Better Auth → `before` plugins → your routes → `after` plugins → 404 → error handler.
|
|
283
268
|
|
|
284
|
-
A plugin with a database passes its own `database
|
|
269
|
+
A plugin with a database passes its own `database` (`schema`, `push`, `migrations`, as the app's). Only it may keep tables in the Postgres schema `nano_<its name>`: a table in the `nano_*` schema of another fails the startup. Without a database of its own, a plugin receives the app's `db`. See [`@nano-app/plugins`](https://github.com/Nano-Nathan/nano-app/blob/main/plugins/README.md#database).
|
|
285
270
|
|
|
286
271
|
## Errors
|
|
287
272
|
|
|
@@ -289,10 +274,9 @@ The framework mounts the 404 and the error handler: an unmatched request becomes
|
|
|
289
274
|
|
|
290
275
|
```ts
|
|
291
276
|
throw { status: 409, message: 'Already exists' }; // → 409 { success: false, warning: false, message: "Already exists" }
|
|
292
|
-
throw 'Field name is required'; // → 400 { success: false, message: "Field name is required" }
|
|
277
|
+
throw 'Field name is required'; // → 400 { success: false, warning: false, message: "Field name is required" }
|
|
293
278
|
throw new Error('boom'); // → 500 "Internal server error", logged to stderr
|
|
294
|
-
// A Postgres error
|
|
295
|
-
// 23505 and 23503 → 409 "Conflict", 23502 and 22P02 → 400 "Invalid request"
|
|
279
|
+
// A Postgres error is one more Error: 500. Catch it in your handler to answer a 409
|
|
296
280
|
GET /unknown // → 404 { success: false, warning: false, message: "/unknown not found" }
|
|
297
281
|
```
|
|
298
282
|
|
package/build/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { CorsOptions } from "cors";
|
|
|
3
3
|
import "dotenv/config";
|
|
4
4
|
import "jiti";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import "better-auth";
|
|
6
|
+
import { BetterAuthOptions } from "better-auth";
|
|
7
7
|
/** API response */
|
|
8
8
|
type ApiResponse<T = any> = {
|
|
9
9
|
status?: number;
|
|
@@ -83,6 +83,8 @@ type RouteEntry<Deps = NanoObject<any>> = {
|
|
|
83
83
|
} & Handlers<Deps>;
|
|
84
84
|
/** Generic object */
|
|
85
85
|
type NanoObject<T = any> = Record<string, T>;
|
|
86
|
+
/** Better Auth options, without `database`: it uses the app's Drizzle client */
|
|
87
|
+
type AuthOptions = Omit<BetterAuthOptions, 'database'>;
|
|
86
88
|
/** Error of the framework, with the status a handler answers */
|
|
87
89
|
declare class NanoError extends Error {
|
|
88
90
|
status?: number;
|
|
@@ -95,9 +97,11 @@ declare class NanoError extends Error {
|
|
|
95
97
|
type DatabaseConfig = {
|
|
96
98
|
/** Its Drizzle schema: the module of its tables and `relations` */
|
|
97
99
|
schema: NanoObject;
|
|
100
|
+
/** Applies the schema at startup with drizzle-kit's push, failing on data loss. Defaults to false */
|
|
101
|
+
push?: boolean;
|
|
102
|
+
/** Folder of the migrations `drizzle-kit generate` writes, applied at startup */
|
|
103
|
+
migrations?: string;
|
|
98
104
|
};
|
|
99
|
-
/** When a plugin is mounted, relative to the consumer routes */
|
|
100
|
-
type PluginStage = 'before' | 'after';
|
|
101
105
|
/** Plugin configuration: a group of endpoints */
|
|
102
106
|
type PluginConfig<Options = NanoObject> = {
|
|
103
107
|
/** Name of the plugin, also the name of its database when it has one */
|
|
@@ -106,8 +110,6 @@ type PluginConfig<Options = NanoObject> = {
|
|
|
106
110
|
routesDir: string;
|
|
107
111
|
/** Base path prepended to every endpoint, e.g. "/auth" (no trailing slash). Defaults to none */
|
|
108
112
|
basePath?: string;
|
|
109
|
-
/** Mount point relative to the consumer routes. Defaults to "before" */
|
|
110
|
-
stage?: PluginStage;
|
|
111
113
|
/** Drizzle schema of the plugin's database, mounted before its endpoints */
|
|
112
114
|
database?: DatabaseConfig;
|
|
113
115
|
/** Plugin options */
|
|
@@ -115,8 +117,20 @@ type PluginConfig<Options = NanoObject> = {
|
|
|
115
117
|
/** Plugin callbacks */
|
|
116
118
|
callbacks?: PluginCallbacks;
|
|
117
119
|
};
|
|
118
|
-
/**
|
|
119
|
-
type
|
|
120
|
+
/** What PluginManager reads: the app's plugin and the ones around it */
|
|
121
|
+
type PluginManagerConfig = {
|
|
122
|
+
/** Folder of the app's routes. Defaults to "src/routes" */
|
|
123
|
+
routesDir?: string;
|
|
124
|
+
/** Drizzle schema of the app's database */
|
|
125
|
+
database?: DatabaseConfig;
|
|
126
|
+
/** Plugins mounted before and after the app's routes */
|
|
127
|
+
plugins?: {
|
|
128
|
+
/** Before the app's routes */
|
|
129
|
+
before?: PluginConfig[];
|
|
130
|
+
/** After the app's routes */
|
|
131
|
+
after?: PluginConfig[];
|
|
132
|
+
};
|
|
133
|
+
};
|
|
120
134
|
/** Configuration of the framework's built-in packages */
|
|
121
135
|
type PackagesConfig = {
|
|
122
136
|
/** Morgan configuration */
|
|
@@ -137,29 +151,25 @@ type PackagesConfig = {
|
|
|
137
151
|
/** urlencoded configuration */
|
|
138
152
|
urlencoded?: Partial<Parameters<typeof express.urlencoded>[0]> | false;
|
|
139
153
|
};
|
|
140
|
-
/**
|
|
141
|
-
type
|
|
154
|
+
/** What NanoApp reads: the server */
|
|
155
|
+
type ServerConfig = {
|
|
142
156
|
/** Listener run once the server is listening */
|
|
143
157
|
listener?: () => void;
|
|
144
158
|
/** Application port. Defaults to 3000 */
|
|
145
159
|
port?: number;
|
|
146
|
-
/** Values
|
|
160
|
+
/** Values for Express `app.set()` (e.g. `{ "trust proxy": 1 }`), over the defaults */
|
|
147
161
|
settings?: NanoObject<unknown>;
|
|
148
|
-
/**
|
|
162
|
+
/** Base middlewares: each one set replaces its default */
|
|
149
163
|
packages?: PackagesConfig;
|
|
150
|
-
/**
|
|
151
|
-
|
|
152
|
-
/** Main plugin to mount. */
|
|
153
|
-
mainPlugin?: MainPluginConfig;
|
|
154
|
-
};
|
|
155
|
-
/** Merges the consumer configuration over the initial one; the main plugin is always named "app" */
|
|
156
|
-
declare function mergeConfig(oConfig?: NanoAppConfig): NanoAppConfig & Required<Pick<NanoAppConfig, 'port' | 'settings' | 'plugins' | 'packages'>> & {
|
|
157
|
-
mainPlugin: PluginConfig;
|
|
164
|
+
/** Better Auth options: with them, every route needs a session unless it is `public` */
|
|
165
|
+
auth?: AuthOptions;
|
|
158
166
|
};
|
|
167
|
+
/** Application configuration: the server and its plugins */
|
|
168
|
+
type NanoAppConfig = ServerConfig & PluginManagerConfig;
|
|
159
169
|
/** Main NanoApp class */
|
|
160
170
|
declare class NanoApp {
|
|
161
171
|
protected oApp: Express;
|
|
162
|
-
protected oConfig:
|
|
172
|
+
protected oConfig: NanoAppConfig;
|
|
163
173
|
constructor(oConfig?: NanoAppConfig);
|
|
164
174
|
/** Starts the application */
|
|
165
175
|
private start;
|
|
@@ -179,4 +189,4 @@ export declare function createPlugin<Options>(fnCreator: Creator<Options>): Crea
|
|
|
179
189
|
export declare function createPluginCallbacks<Deps = NanoObject<any>>(oCallbacks: PluginCallbacks<Deps>): PluginCallbacks<Deps>;
|
|
180
190
|
/** Whether the session's user has the permissions; always without Better Auth or permissions */
|
|
181
191
|
declare function isGranted({ auth, session }: Params, oPermissions?: NanoObject<string[]>): Promise<boolean>;
|
|
182
|
-
export { type ApiPromise, type ApiResponse, type Callbacks, type DatabaseConfig, type Handler, type HandlerParams, type Handlers, type
|
|
192
|
+
export { type ApiPromise, type ApiResponse, type Callbacks, type DatabaseConfig, type Handler, type HandlerParams, type Handlers, type MethodEntry, NanoApp, type NanoAppConfig, NanoError, type NanoObject, type PackagesConfig, type Params, type PluginCallbacks, type PluginConfig, type PluginManagerConfig, type QueryParams, type Request, type RouteEntry, type ServerConfig, isGranted };
|
package/build/index.js
CHANGED
|
@@ -161,6 +161,117 @@ z.object({
|
|
|
161
161
|
}).refine(({ match, redirect }) => !Paths.matches(match, redirect ?? "/"), { error: "Its \"redirect\" must be outside its \"match\"" })).optional()
|
|
162
162
|
}).optional()
|
|
163
163
|
});
|
|
164
|
+
// src/database/DatabaseManager.ts
|
|
165
|
+
/** The databases of the app and its plugins: one Drizzle client per name on the pool of `DATABASE_URL` */
|
|
166
|
+
var DatabaseManager = class {
|
|
167
|
+
static oPool;
|
|
168
|
+
static oClients = {};
|
|
169
|
+
/** The Drizzle client of a database, created on the first call; without a config, the app's */
|
|
170
|
+
static async mount(sName, oConfig) {
|
|
171
|
+
if (!oConfig) return this.oClients["app"];
|
|
172
|
+
this.oClients[sName] ??= this.create(sName, oConfig);
|
|
173
|
+
return this.oClients[sName];
|
|
174
|
+
}
|
|
175
|
+
/** Creates a client: the `nano_*` rule of its tables, then its push and its migrations */
|
|
176
|
+
static async create(sName, oConfig) {
|
|
177
|
+
const { schema, push, migrations } = oConfig;
|
|
178
|
+
const { defineRelations, is } = await import("drizzle-orm");
|
|
179
|
+
const { getTableConfig, PgTable } = await import("drizzle-orm/pg-core");
|
|
180
|
+
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
181
|
+
const sOwn = sName === "app" ? void 0 : `nano_${sName}`;
|
|
182
|
+
const aSchemas = Object.values(schema).filter((oValue) => is(oValue, PgTable)).map((oTable) => {
|
|
183
|
+
const { name, schema: sSchema = "public" } = getTableConfig(oTable);
|
|
184
|
+
if (sSchema.startsWith("nano_") && sSchema !== sOwn) throw new NanoError(`The database "${sName}": table "${sSchema}.${name}" breaks the rule, each nano_* schema is reserved for the plugin of its name.`);
|
|
185
|
+
return sSchema;
|
|
186
|
+
});
|
|
187
|
+
const oClient = drizzle({
|
|
188
|
+
client: await this.pool(),
|
|
189
|
+
relations: schema.relations ?? defineRelations(schema)
|
|
190
|
+
});
|
|
191
|
+
if (push && aSchemas.length) {
|
|
192
|
+
const { pushSchema } = await import("drizzle-kit/api-postgres");
|
|
193
|
+
const { hints, apply } = await pushSchema(schema, oClient, {
|
|
194
|
+
schemas: aSchemas,
|
|
195
|
+
tables: void 0,
|
|
196
|
+
entities: void 0,
|
|
197
|
+
extensions: void 0
|
|
198
|
+
});
|
|
199
|
+
if (hints.length) throw new NanoError(`The database "${sName}" cannot be pushed without losing data: ${hints.map((oHint) => oHint.hint).join("; ")}. Run \`pnpm drizzle-kit push\` by hand.`);
|
|
200
|
+
await apply();
|
|
201
|
+
}
|
|
202
|
+
if (migrations) {
|
|
203
|
+
const { migrate } = await import("drizzle-orm/node-postgres/migrator");
|
|
204
|
+
await migrate(oClient, { migrationsFolder: migrations });
|
|
205
|
+
}
|
|
206
|
+
Logger.info(`Database "${sName}" mounted`);
|
|
207
|
+
return oClient;
|
|
208
|
+
}
|
|
209
|
+
/** The pool of `DATABASE_URL`, shared by every client and Better Auth, created on the first call */
|
|
210
|
+
static pool() {
|
|
211
|
+
this.oPool ??= this.createPool();
|
|
212
|
+
return this.oPool;
|
|
213
|
+
}
|
|
214
|
+
/** Creates the pool of `DATABASE_URL` */
|
|
215
|
+
static async createPool() {
|
|
216
|
+
const sUrl = process.env.DATABASE_URL;
|
|
217
|
+
if (!sUrl) throw new NanoError("The DATABASE_URL environment variable is not defined.");
|
|
218
|
+
const { Pool } = await import("pg");
|
|
219
|
+
return new Pool({ connectionString: sUrl });
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
// src/auth/BetterAuth.ts
|
|
223
|
+
/** Postgres schema of Better Auth's tables */
|
|
224
|
+
const SCHEMA = "nano_auth";
|
|
225
|
+
/** Key of the Postgres lock that lets one startup migrate at a time */
|
|
226
|
+
const MIGRATION_LOCK = 7310002;
|
|
227
|
+
/** Better Auth of the app: its tables in `nano_auth`, its endpoints and the session of a request */
|
|
228
|
+
var BetterAuth = class {
|
|
229
|
+
static oAuth;
|
|
230
|
+
static fnHeaders;
|
|
231
|
+
/** Better Auth's instance, once mounted */
|
|
232
|
+
static instance() {
|
|
233
|
+
return this.oAuth;
|
|
234
|
+
}
|
|
235
|
+
/** The session of a request, when it has one */
|
|
236
|
+
static async session(oReq) {
|
|
237
|
+
if (!this.oAuth || !this.fnHeaders) return void 0;
|
|
238
|
+
const oHeaders = this.fnHeaders(oReq.headers);
|
|
239
|
+
return await this.oAuth.api.getSession({ headers: oHeaders }) ?? void 0;
|
|
240
|
+
}
|
|
241
|
+
/** Migrates its tables, creates the instance and mounts its endpoints; off without options */
|
|
242
|
+
static async mount(oApp, oOptions) {
|
|
243
|
+
if (!oOptions) return;
|
|
244
|
+
const { betterAuth } = await import("better-auth");
|
|
245
|
+
const { getMigrations } = await import("better-auth/db/migration");
|
|
246
|
+
const { fromNodeHeaders, toNodeHandler } = await import("better-auth/node");
|
|
247
|
+
const { PostgresDialect } = await import("kysely");
|
|
248
|
+
const oPool = await DatabaseManager.pool();
|
|
249
|
+
const { advanced, basePath = "/api/auth" } = oOptions;
|
|
250
|
+
const oFullOptions = {
|
|
251
|
+
...oOptions,
|
|
252
|
+
advanced: {
|
|
253
|
+
...advanced,
|
|
254
|
+
ipAddress: {
|
|
255
|
+
trustedProxies: PRIVATE_NETWORKS,
|
|
256
|
+
...advanced?.ipAddress
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
database: {
|
|
260
|
+
dialect: new PostgresDialect({ pool: oPool }),
|
|
261
|
+
type: "postgres",
|
|
262
|
+
schemaName: SCHEMA
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const oConnection = await oPool.connect();
|
|
266
|
+
await oConnection.query("SELECT pg_advisory_lock($1)", [MIGRATION_LOCK]);
|
|
267
|
+
await (await getMigrations(oFullOptions)).runMigrations();
|
|
268
|
+
oConnection.release(true);
|
|
269
|
+
const oAuth = betterAuth(oFullOptions);
|
|
270
|
+
this.oAuth = oAuth;
|
|
271
|
+
this.fnHeaders = fromNodeHeaders;
|
|
272
|
+
oApp.all(`${basePath}/{*path}`, toNodeHandler(oAuth));
|
|
273
|
+
}
|
|
274
|
+
};
|
|
164
275
|
// src/auth/permissions.ts
|
|
165
276
|
/** Whether the session's user has the permissions; always without Better Auth or permissions */
|
|
166
277
|
async function isGranted({ auth, session }, oPermissions) {
|
|
@@ -172,7 +283,7 @@ async function isGranted({ auth, session }, oPermissions) {
|
|
|
172
283
|
} });
|
|
173
284
|
return success;
|
|
174
285
|
}
|
|
175
|
-
// src/
|
|
286
|
+
// src/router/Endpoint.ts
|
|
176
287
|
/** Base class for all endpoints */
|
|
177
288
|
var Endpoint = class {
|
|
178
289
|
oMethods;
|
|
@@ -192,39 +303,42 @@ var Endpoint = class {
|
|
|
192
303
|
}
|
|
193
304
|
/** Handler for all methods */
|
|
194
305
|
async mount(oParams, sPath) {
|
|
195
|
-
|
|
306
|
+
const isUnchecked = Object.values(this.oMethods).some(({ permissions }) => permissions);
|
|
307
|
+
if (!oParams.auth && isUnchecked) Logger.warn(`${sPath}: its permissions are not checked without Better Auth. Set auth in createNanoApp.`);
|
|
196
308
|
await this.oCallbacks.onInit?.(oParams);
|
|
197
309
|
return this.createHandler(oParams);
|
|
198
310
|
}
|
|
199
|
-
/** Express handler:
|
|
311
|
+
/** Express handler: the request, answered as an ApiResponse */
|
|
200
312
|
createHandler(oParams) {
|
|
201
|
-
return (oReq, oRes,
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
...oParams,
|
|
208
|
-
session: oRes.locals.session
|
|
209
|
-
};
|
|
210
|
-
return isGranted(oRequestParams, this.isPublic ? void 0 : oMethod.permissions).then((isAllowed) => {
|
|
211
|
-
if (!isAllowed) throw new NanoError("Forbidden", 403);
|
|
212
|
-
return this.oCallbacks.onBefore?.(oReq, sMethod, oRequestParams) ?? {};
|
|
213
|
-
}).then((onBeforeResult) => oMethod.handler(oReq, {
|
|
214
|
-
...oRequestParams,
|
|
215
|
-
onBeforeResult
|
|
216
|
-
})).then((oResponse) => {
|
|
217
|
-
const { status, ...oBody } = oResponse;
|
|
218
|
-
const iStatus = status ?? 200;
|
|
219
|
-
oRes.status(iStatus).json({
|
|
220
|
-
...oBody,
|
|
221
|
-
success: iStatus < 400
|
|
222
|
-
});
|
|
313
|
+
return (oReq, oRes) => this.run(oReq, oParams).then((oResponse) => {
|
|
314
|
+
const { status, ...oBody } = oResponse;
|
|
315
|
+
const iStatus = status ?? 200;
|
|
316
|
+
oRes.status(iStatus).json({
|
|
317
|
+
...oBody,
|
|
318
|
+
success: iStatus < 400
|
|
223
319
|
});
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/** Checks the session and the permissions, then runs onBefore and the method */
|
|
323
|
+
async run(oReq, oParams) {
|
|
324
|
+
const oSession = await BetterAuth.session(oReq);
|
|
325
|
+
const oRequestParams = {
|
|
326
|
+
...oParams,
|
|
327
|
+
session: oSession
|
|
224
328
|
};
|
|
329
|
+
if (oParams.auth && !oSession && !this.isPublic) throw new NanoError("Not authenticated", 401);
|
|
330
|
+
const sMethod = oReq.method;
|
|
331
|
+
const oMethod = this.oMethods[sMethod];
|
|
332
|
+
if (!oMethod) throw new NanoError(`Method ${sMethod} not implemented`, 405);
|
|
333
|
+
if (!await isGranted(oRequestParams, this.isPublic ? void 0 : oMethod.permissions)) throw new NanoError("Forbidden", 403);
|
|
334
|
+
const onBeforeResult = await this.oCallbacks.onBefore?.(oReq, sMethod, oRequestParams) ?? {};
|
|
335
|
+
return oMethod.handler(oReq, {
|
|
336
|
+
...oRequestParams,
|
|
337
|
+
onBeforeResult
|
|
338
|
+
});
|
|
225
339
|
}
|
|
226
340
|
};
|
|
227
|
-
// src/
|
|
341
|
+
// src/router/Router.ts
|
|
228
342
|
/** Turns a folder of `*.route.ts` files into Express paths and endpoints */
|
|
229
343
|
var Router = class {
|
|
230
344
|
sRoutesDir;
|
|
@@ -256,104 +370,7 @@ var Router = class {
|
|
|
256
370
|
return `${this.sBasePath}${sEndpoint.replace(/\/index$/, "")}${sExpression}` || "/";
|
|
257
371
|
}
|
|
258
372
|
};
|
|
259
|
-
// src/
|
|
260
|
-
/** Files of the app's Better Auth options, relative to `cwd`: source, then built */
|
|
261
|
-
const AUTH_FILES = ["src/auth.ts", "src/auth.js"];
|
|
262
|
-
/** Better Auth on the app's Drizzle client: the instance, its handler and the session lookup */
|
|
263
|
-
var BetterAuth = class {
|
|
264
|
-
static oAuth;
|
|
265
|
-
/** The options of the app's `src/auth.ts`, or undefined without it: Better Auth stays off */
|
|
266
|
-
static async findOptions() {
|
|
267
|
-
const sFilePath = AUTH_FILES.find(existsSync);
|
|
268
|
-
if (!sFilePath) return void 0;
|
|
269
|
-
const oFile = await FileReader.loadModule(sFilePath, { default: true });
|
|
270
|
-
if (!Validator.isObject(oFile?.options)) throw new NanoError(`${sFilePath} must default export { options }: the Better Auth options.`);
|
|
271
|
-
return oFile.options;
|
|
272
|
-
}
|
|
273
|
-
/** The instance, once the app's plugin created it */
|
|
274
|
-
static get() {
|
|
275
|
-
return this.oAuth;
|
|
276
|
-
}
|
|
277
|
-
/** Creates the instance on the app's client and schema, its handler and the session lookup */
|
|
278
|
-
static async create(oOptions, oDb, oSchema) {
|
|
279
|
-
const { betterAuth } = await import("better-auth");
|
|
280
|
-
const { drizzleAdapter } = await import("better-auth/adapters/drizzle");
|
|
281
|
-
const { getSchema } = await import("better-auth/db");
|
|
282
|
-
const { fromNodeHeaders, toNodeHandler } = await import("better-auth/node");
|
|
283
|
-
const aMissing = Object.keys(getSchema(oOptions)).filter((sModel) => !oSchema[sModel]);
|
|
284
|
-
if (aMissing.length) throw new NanoError(`Better Auth needs the tables ${aMissing.join(", ")} in the app's schema. Run \`npx auth generate\` and apply your schema with drizzle-kit.`);
|
|
285
|
-
const oAuth = betterAuth({
|
|
286
|
-
...oOptions,
|
|
287
|
-
advanced: {
|
|
288
|
-
...oOptions.advanced,
|
|
289
|
-
ipAddress: {
|
|
290
|
-
trustedProxies: PRIVATE_NETWORKS,
|
|
291
|
-
...oOptions.advanced?.ipAddress
|
|
292
|
-
}
|
|
293
|
-
},
|
|
294
|
-
database: drizzleAdapter(oDb, {
|
|
295
|
-
provider: "pg",
|
|
296
|
-
schema: oSchema
|
|
297
|
-
})
|
|
298
|
-
});
|
|
299
|
-
this.oAuth = oAuth;
|
|
300
|
-
const fnSession = (oReq, oRes, fnNext) => oAuth.api.getSession({ headers: fromNodeHeaders(oReq.headers) }).then((oSession) => {
|
|
301
|
-
oRes.locals.session = oSession ?? void 0;
|
|
302
|
-
fnNext();
|
|
303
|
-
});
|
|
304
|
-
return {
|
|
305
|
-
oHandler: toNodeHandler(oAuth),
|
|
306
|
-
fnSession,
|
|
307
|
-
sPath: `${oOptions.basePath ?? "/api/auth"}/{*path}`
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
// src/database/Database.ts
|
|
312
|
-
/** Drizzle clients of the app and its plugins, one per name, on one pool of `DATABASE_URL` */
|
|
313
|
-
var Database = class {
|
|
314
|
-
static oPool;
|
|
315
|
-
/** Mounted clients, by name */
|
|
316
|
-
static oClients = {};
|
|
317
|
-
/** Gets a client by name */
|
|
318
|
-
static get(sName) {
|
|
319
|
-
return this.oClients[sName];
|
|
320
|
-
}
|
|
321
|
-
/** Mounts the Drizzle client of a schema, checking the `nano_*` rule of its tables */
|
|
322
|
-
static async mount(sName, { schema }) {
|
|
323
|
-
if (this.get(sName)) throw new NanoError(`The database "${sName}" is already mounted.`);
|
|
324
|
-
const { defineRelations, is } = await import("drizzle-orm");
|
|
325
|
-
const { getTableConfig, PgTable } = await import("drizzle-orm/pg-core");
|
|
326
|
-
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
327
|
-
const sOwn = `nano_${sName}`;
|
|
328
|
-
const { breaks, rule } = sName === "app" ? {
|
|
329
|
-
breaks: (sSchema) => sSchema.startsWith("nano_"),
|
|
330
|
-
rule: "the nano_* schemas are reserved for plugins"
|
|
331
|
-
} : {
|
|
332
|
-
breaks: (sSchema) => sSchema !== sOwn,
|
|
333
|
-
rule: `a plugin keeps its tables in "${sOwn}"`
|
|
334
|
-
};
|
|
335
|
-
for (const oTable of Object.values(schema).filter((oValue) => is(oValue, PgTable))) {
|
|
336
|
-
const { name, schema: sSchema = "public" } = getTableConfig(oTable);
|
|
337
|
-
if (breaks(sSchema)) throw new NanoError(`The database "${sName}": table "${sSchema}.${name}" breaks the rule, ${rule}.`);
|
|
338
|
-
}
|
|
339
|
-
this.oPool ??= await this.pool();
|
|
340
|
-
const oClient = drizzle({
|
|
341
|
-
client: this.oPool,
|
|
342
|
-
relations: schema.relations ?? defineRelations(schema)
|
|
343
|
-
});
|
|
344
|
-
this.oClients[sName] = oClient;
|
|
345
|
-
Logger.info(`Database "${sName}" mounted`);
|
|
346
|
-
return oClient;
|
|
347
|
-
}
|
|
348
|
-
/** The pool of `DATABASE_URL`, shared by every client */
|
|
349
|
-
static async pool() {
|
|
350
|
-
const sUrl = process.env.DATABASE_URL;
|
|
351
|
-
if (!sUrl) throw new NanoError("The DATABASE_URL environment variable is not defined.");
|
|
352
|
-
const { Pool } = await import("pg");
|
|
353
|
-
return new Pool({ connectionString: sUrl });
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
// src/routes/Plugin.ts
|
|
373
|
+
// src/plugins/Plugin.ts
|
|
357
374
|
/** A group of routes, with its options, callbacks and database */
|
|
358
375
|
var Plugin = class {
|
|
359
376
|
oConfig;
|
|
@@ -364,146 +381,78 @@ var Plugin = class {
|
|
|
364
381
|
/** Mounts its endpoints on express app */
|
|
365
382
|
async mount(oApp) {
|
|
366
383
|
const { name, routesDir, basePath, database, options, callbacks } = this.oConfig;
|
|
367
|
-
const
|
|
368
|
-
const oDb = database ? await Database.mount(name, database) : Database.get("app");
|
|
369
|
-
if (oAuthOptions) {
|
|
370
|
-
if (!database) throw new NanoError("src/auth.ts needs the database of the app: set mainPlugin.database.schema.");
|
|
371
|
-
const { sPath, oHandler, fnSession } = await BetterAuth.create(oAuthOptions, oDb, database.schema);
|
|
372
|
-
oApp.all(sPath, oHandler);
|
|
373
|
-
oApp.use(fnSession);
|
|
374
|
-
}
|
|
384
|
+
const oDb = await DatabaseManager.mount(name, database);
|
|
375
385
|
const oParams = Object.freeze({
|
|
376
386
|
...options,
|
|
377
387
|
db: oDb,
|
|
378
|
-
auth: BetterAuth.
|
|
388
|
+
auth: BetterAuth.instance()
|
|
379
389
|
});
|
|
380
390
|
await callbacks?.onMount?.(oApp, oParams);
|
|
381
391
|
const oRouter = new Router(routesDir, basePath);
|
|
382
392
|
for (const { path, endpoint } of await oRouter.mount()) oApp.all(path, await endpoint.mount(oParams, path));
|
|
383
393
|
}
|
|
384
394
|
};
|
|
385
|
-
// src/
|
|
386
|
-
/**
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
"POST",
|
|
406
|
-
"DELETE",
|
|
407
|
-
"QUERY"
|
|
408
|
-
] },
|
|
409
|
-
helmet: true,
|
|
410
|
-
compression: false,
|
|
411
|
-
express: { limit: "10kb" },
|
|
412
|
-
urlencoded: { extended: false }
|
|
413
|
-
},
|
|
414
|
-
/** Main plugin to mount */
|
|
415
|
-
mainPlugin: {
|
|
416
|
-
routesDir: "src/routes",
|
|
417
|
-
basePath: ""
|
|
418
|
-
},
|
|
419
|
-
/** Plugins around the consumer routes */
|
|
420
|
-
plugins: []
|
|
395
|
+
// src/plugins/PluginManager.ts
|
|
396
|
+
/** The plugins of the app: its own, the ones before it and the ones after it */
|
|
397
|
+
var PluginManager = class {
|
|
398
|
+
/** Mounts every plugin on express app, in order: before, the app's, after */
|
|
399
|
+
static async mount(oApp, oConfig) {
|
|
400
|
+
const { routesDir = "src/routes", database, plugins } = oConfig;
|
|
401
|
+
await DatabaseManager.mount("app", database);
|
|
402
|
+
const oAppPlugin = new Plugin({
|
|
403
|
+
name: "app",
|
|
404
|
+
routesDir,
|
|
405
|
+
database
|
|
406
|
+
});
|
|
407
|
+
const aBefore = (plugins?.before ?? []).map((oPlugin) => new Plugin(oPlugin));
|
|
408
|
+
const aAfter = (plugins?.after ?? []).map((oPlugin) => new Plugin(oPlugin));
|
|
409
|
+
for (const oPlugin of [
|
|
410
|
+
...aBefore,
|
|
411
|
+
oAppPlugin,
|
|
412
|
+
...aAfter
|
|
413
|
+
]) await oPlugin.mount(oApp);
|
|
414
|
+
}
|
|
421
415
|
};
|
|
422
|
-
/**
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
/** Whether a merged configuration kept the plain object of the initial one */
|
|
433
|
-
function isMergedConfig(oValue) {
|
|
434
|
-
return Validator.isObject(oValue);
|
|
435
|
-
}
|
|
436
|
-
/** Merges the consumer configuration over the initial one; the main plugin is always named "app" */
|
|
437
|
-
function mergeConfig(oConfig = {}) {
|
|
438
|
-
const oMerged = deepMerge(INITIAL_CONFIG, oConfig);
|
|
439
|
-
if (!isMergedConfig(oMerged)) throw new NanoError("The configuration of createNanoApp must be an object.");
|
|
440
|
-
return {
|
|
441
|
-
...oMerged,
|
|
442
|
-
mainPlugin: {
|
|
443
|
-
...oMerged.mainPlugin,
|
|
444
|
-
name: "app"
|
|
445
|
-
}
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
// src/app/errors.ts
|
|
449
|
-
/** Database errors the request caused, by their Postgres code or Drizzle's name */
|
|
450
|
-
const DATABASE_ERRORS = {
|
|
451
|
-
"23505": {
|
|
452
|
-
status: 409,
|
|
453
|
-
message: "Conflict"
|
|
454
|
-
},
|
|
455
|
-
"23503": {
|
|
456
|
-
status: 409,
|
|
457
|
-
message: "Conflict"
|
|
458
|
-
},
|
|
459
|
-
"23502": {
|
|
460
|
-
status: 400,
|
|
461
|
-
message: "Invalid request"
|
|
462
|
-
},
|
|
463
|
-
"22P02": {
|
|
464
|
-
status: 400,
|
|
465
|
-
message: "Invalid request"
|
|
416
|
+
/** Express settings: the client IP comes from `X-Forwarded-For` only through private proxies */
|
|
417
|
+
const SETTINGS = { "trust proxy": PRIVATE_NETWORKS };
|
|
418
|
+
/** Base middlewares */
|
|
419
|
+
const PACKAGES = {
|
|
420
|
+
morgan: {
|
|
421
|
+
tokens: {
|
|
422
|
+
timestamp: () => Logger.date(),
|
|
423
|
+
path: (oReq) => oReq.path
|
|
424
|
+
},
|
|
425
|
+
format: ":timestamp | :method :path :status"
|
|
466
426
|
},
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
427
|
+
cors: { methods: [
|
|
428
|
+
"GET",
|
|
429
|
+
"HEAD",
|
|
430
|
+
"PUT",
|
|
431
|
+
"POST",
|
|
432
|
+
"DELETE",
|
|
433
|
+
"QUERY"
|
|
434
|
+
] },
|
|
435
|
+
helmet: true,
|
|
436
|
+
compression: false,
|
|
437
|
+
express: { limit: "10kb" },
|
|
438
|
+
urlencoded: { extended: false }
|
|
471
439
|
};
|
|
472
|
-
|
|
473
|
-
function isHandlerError(oError) {
|
|
474
|
-
return typeof oError === "object" && oError !== null;
|
|
475
|
-
}
|
|
476
|
-
/** Normalizes anything thrown by a handler into an ApiResponse plus its status */
|
|
477
|
-
function normalize(oError) {
|
|
478
|
-
if (typeof oError === "string") return {
|
|
479
|
-
status: 400,
|
|
480
|
-
body: {
|
|
481
|
-
success: false,
|
|
482
|
-
message: oError
|
|
483
|
-
}
|
|
484
|
-
};
|
|
485
|
-
const oThrown = isHandlerError(oError) ? oError : {};
|
|
486
|
-
const sKey = oThrown.cause?.code ?? oThrown.name ?? "";
|
|
487
|
-
const { status, message, warning } = DATABASE_ERRORS[sKey] ?? oThrown;
|
|
488
|
-
const iStatus = status ?? 500;
|
|
489
|
-
return {
|
|
490
|
-
status: iStatus,
|
|
491
|
-
body: {
|
|
492
|
-
success: false,
|
|
493
|
-
warning: warning ?? false,
|
|
494
|
-
message: iStatus < 500 && message || "Internal server error"
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
}
|
|
440
|
+
// src/app/handlers/errors.ts
|
|
498
441
|
/** Turns every unmatched request into a 404 for the error handler */
|
|
499
442
|
const notFoundHandler = (oReq, _oRes, fnNext) => {
|
|
500
443
|
fnNext(new NanoError(`${oReq.path} not found`, 404));
|
|
501
444
|
};
|
|
502
|
-
/**
|
|
445
|
+
/** Answers anything a handler throws as an ApiResponse */
|
|
503
446
|
const errorHandler = (oError, _oReq, oRes, _fnNext) => {
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
447
|
+
const oThrown = typeof oError === "string" ? new NanoError(oError, 400) : oError;
|
|
448
|
+
const iStatus = oThrown?.status ?? 500;
|
|
449
|
+
const sMessage = iStatus < 500 ? oThrown.message : "Internal server error";
|
|
450
|
+
if (iStatus >= 500) Logger.error(sMessage, oError);
|
|
451
|
+
oRes.status(iStatus).json({
|
|
452
|
+
success: false,
|
|
453
|
+
warning: oThrown.warning ?? false,
|
|
454
|
+
message: sMessage
|
|
455
|
+
});
|
|
507
456
|
};
|
|
508
457
|
// src/app/NanoApp.ts
|
|
509
458
|
/** Main NanoApp class */
|
|
@@ -512,16 +461,17 @@ var NanoApp = class {
|
|
|
512
461
|
oConfig;
|
|
513
462
|
constructor(oConfig = {}) {
|
|
514
463
|
this.oApp = express();
|
|
515
|
-
this.oConfig =
|
|
464
|
+
this.oConfig = oConfig;
|
|
516
465
|
this.start();
|
|
517
466
|
}
|
|
518
467
|
/** Starts the application */
|
|
519
468
|
async start() {
|
|
520
469
|
try {
|
|
521
470
|
await this.initialize();
|
|
522
|
-
this.
|
|
471
|
+
const iPort = this.oConfig.port ?? 3e3;
|
|
472
|
+
this.oApp.listen(iPort, () => {
|
|
523
473
|
this.oConfig.listener?.();
|
|
524
|
-
Logger.info(`Nano App running on port ${
|
|
474
|
+
Logger.info(`Nano App running on port ${iPort}`);
|
|
525
475
|
});
|
|
526
476
|
} catch (oError) {
|
|
527
477
|
Logger.error("Error trying to start the server.", oError);
|
|
@@ -530,19 +480,16 @@ var NanoApp = class {
|
|
|
530
480
|
}
|
|
531
481
|
/** Initializes the application in the right order */
|
|
532
482
|
async initialize() {
|
|
533
|
-
for (const [sKey, oValue] of Object.entries(
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
mainPlugin,
|
|
544
|
-
...oPlugins.after
|
|
545
|
-
]) await new Plugin(oPlugin).mount(this.oApp);
|
|
483
|
+
for (const [sKey, oValue] of Object.entries({
|
|
484
|
+
...SETTINGS,
|
|
485
|
+
...this.oConfig.settings
|
|
486
|
+
})) this.oApp.set(sKey, oValue);
|
|
487
|
+
this.setPackages({
|
|
488
|
+
...PACKAGES,
|
|
489
|
+
...this.oConfig.packages
|
|
490
|
+
});
|
|
491
|
+
await BetterAuth.mount(this.oApp, this.oConfig.auth);
|
|
492
|
+
await PluginManager.mount(this.oApp, this.oConfig);
|
|
546
493
|
this.oApp.use(notFoundHandler);
|
|
547
494
|
this.oApp.use(errorHandler);
|
|
548
495
|
}
|
|
@@ -562,7 +509,7 @@ var NanoApp = class {
|
|
|
562
509
|
for (const fnPackage of aPackages) if (fnPackage) this.oApp.use(fnPackage);
|
|
563
510
|
}
|
|
564
511
|
};
|
|
565
|
-
// src/
|
|
512
|
+
// src/lib/creators.ts
|
|
566
513
|
/** Creates a new NanoApp instance */
|
|
567
514
|
function createNanoApp(oConfig) {
|
|
568
515
|
return new NanoApp(oConfig);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nano-app/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Express server with file-convention routes, plugins and an optional PostgreSQL layer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -56,13 +56,21 @@
|
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"better-auth": "^1.7.18",
|
|
59
|
+
"drizzle-kit": "^1.0.0-rc.5",
|
|
59
60
|
"drizzle-orm": "^1.0.0-rc.5",
|
|
61
|
+
"kysely": "^0.29.8",
|
|
60
62
|
"pg": "^8.23.1"
|
|
61
63
|
},
|
|
62
64
|
"peerDependenciesMeta": {
|
|
65
|
+
"kysely": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
63
68
|
"better-auth": {
|
|
64
69
|
"optional": true
|
|
65
70
|
},
|
|
71
|
+
"drizzle-kit": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
66
74
|
"drizzle-orm": {
|
|
67
75
|
"optional": true
|
|
68
76
|
},
|
|
@@ -80,6 +88,7 @@
|
|
|
80
88
|
"better-auth": "^1.7.18",
|
|
81
89
|
"drizzle-kit": "^1.0.0-rc.5",
|
|
82
90
|
"drizzle-orm": "^1.0.0-rc.5",
|
|
91
|
+
"kysely": "^0.29.8",
|
|
83
92
|
"pg": "^8.23.1",
|
|
84
93
|
"tsdown": "^0.23.5",
|
|
85
94
|
"tsx": "^4.23.15",
|