@nano-app/backend 0.10.0 → 0.12.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 +36 -35
- package/build/index.d.ts +6 -3
- package/build/index.js +33 -26
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
Preconfigured Express server with **file-convention routes** and **plugins**: it mounts the base middlewares, discovers your routes, formats every error as JSON, and groups extra endpoints in plugins.
|
|
6
6
|
|
|
7
|
-
- Node
|
|
7
|
+
- Node.js 22.23.4, 24.22.1 or 26.12.1 and newer. ESM only.
|
|
8
8
|
- Optional PostgreSQL layer through your generated Prisma client. Disabled by default: without it no database code is loaded.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
|
|
11
|
+
pnpm add @nano-app/backend
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Quick start
|
|
@@ -24,17 +24,17 @@ src/
|
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
26
|
// src/app.ts
|
|
27
|
-
import { createNanoApp } from
|
|
27
|
+
import { createNanoApp } from '@nano-app/backend';
|
|
28
28
|
|
|
29
29
|
createNanoApp({ port: 3000 });
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
33
|
// src/routes/health.route.ts
|
|
34
|
-
import { createRoute } from
|
|
34
|
+
import { createRoute } from '@nano-app/backend';
|
|
35
35
|
|
|
36
36
|
export default createRoute({
|
|
37
|
-
GET: async () => ({ success: true, result: { status:
|
|
37
|
+
GET: async () => ({ success: true, result: { status: 'ok' } }),
|
|
38
38
|
});
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -52,9 +52,9 @@ Each `*.route.ts` under `mainPlugin.routesDir` default-exports an object with th
|
|
|
52
52
|
|
|
53
53
|
```ts
|
|
54
54
|
export default createRoute({
|
|
55
|
-
expression:
|
|
55
|
+
expression: '/:id', // → /users/:id
|
|
56
56
|
GET: async req => ({ success: true, result: { id: req.params.id } }),
|
|
57
|
-
DELETE: async req => ({ success: true, message:
|
|
57
|
+
DELETE: async req => ({ success: true, message: 'Deleted' }),
|
|
58
58
|
});
|
|
59
59
|
```
|
|
60
60
|
|
|
@@ -69,6 +69,7 @@ type ApiResponse<T> = {
|
|
|
69
69
|
};
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
+
- A write returns the item as it is after it in `result`: `POST` the created item, `PUT` the updated item, `DELETE` the deleted item. The frontend keeps what an update returns.
|
|
72
73
|
- Undeclared methods respond `405`.
|
|
73
74
|
- An invalid file is discarded with a warning; the other routes keep working.
|
|
74
75
|
|
|
@@ -85,13 +86,13 @@ Every option is optional.
|
|
|
85
86
|
| `mainPlugin` | `{ routesDir: "src/routes" }` | Your routes (`routesDir`, relative to `cwd`), `basePath` (e.g. `/api`, no trailing slash) and database: see [Database](#database). |
|
|
86
87
|
| `plugins` | `[]` | Plugins mounted around your routes, in order: see [Plugins](#plugins). |
|
|
87
88
|
|
|
88
|
-
`packages` accepts `morgan`, `cors`, `helmet`, `compression`, `rateLimit`, `express` (`express.json` options), `urlencoded` and `betterAuth` (off by default: see [Better Auth](#better-auth)). The merge is **per section**: passing one key keeps the others; pass `false
|
|
89
|
+
`packages` accepts `morgan`, `cors`, `helmet`, `compression`, `rateLimit`, `express` (`express.json` options), `urlencoded` and `betterAuth` (off by default: see [Better Auth](#better-auth)). The merge is **per section**: passing one key keeps the others; pass `false` to disable one.
|
|
89
90
|
|
|
90
91
|
```ts
|
|
91
92
|
createNanoApp({
|
|
92
|
-
settings: {
|
|
93
|
+
settings: { 'trust proxy': 1 },
|
|
93
94
|
packages: {
|
|
94
|
-
cors: { origin:
|
|
95
|
+
cors: { origin: 'https://my-app.com' },
|
|
95
96
|
helmet: false,
|
|
96
97
|
rateLimit: { windowMs: 60_000, max: 120 },
|
|
97
98
|
},
|
|
@@ -100,10 +101,10 @@ createNanoApp({
|
|
|
100
101
|
|
|
101
102
|
## Database
|
|
102
103
|
|
|
103
|
-
Off until enabled: at startup nano-app generates the Prisma client of your schema, checks that its tables can be read and passes it to your handlers as their second parameter. **It never modifies your
|
|
104
|
+
Off until enabled: at startup nano-app generates the Prisma client of your schema, checks that its tables can be read and passes it to your handlers as their second parameter. **It never modifies your schemas**: applying them (`prisma db push`, `prisma migrate dev`, `prisma migrate deploy`) is up to you, with Prisma.
|
|
104
105
|
|
|
105
106
|
```bash
|
|
106
|
-
|
|
107
|
+
pnpm add prisma @prisma/client @prisma/adapter-pg pg # prisma too: the startup generates the client
|
|
107
108
|
```
|
|
108
109
|
|
|
109
110
|
```prisma
|
|
@@ -130,8 +131,8 @@ createNanoApp({ mainPlugin: { database: { enabled: true } } });
|
|
|
130
131
|
|
|
131
132
|
```ts
|
|
132
133
|
// src/routes/users.route.ts
|
|
133
|
-
import { createRoute } from
|
|
134
|
-
import type { PrismaClient } from
|
|
134
|
+
import { createRoute } from '@nano-app/backend';
|
|
135
|
+
import type { PrismaClient } from '../../.nano-app/prisma-clients/app/generated/client';
|
|
135
136
|
|
|
136
137
|
export default createRoute<PrismaClient>({
|
|
137
138
|
GET: async (_req, { db }) => ({ success: true, result: await db!.user.findMany() }),
|
|
@@ -157,12 +158,12 @@ export default createRoute<PrismaClient>({
|
|
|
157
158
|
Users, sign-in and sessions with [Better Auth](https://www.better-auth.com), on your database. Off until `packages.betterAuth` gets its options (everything but `database`: nano-app passes your Prisma client).
|
|
158
159
|
|
|
159
160
|
```bash
|
|
160
|
-
|
|
161
|
+
pnpm add better-auth
|
|
161
162
|
```
|
|
162
163
|
|
|
163
164
|
```ts
|
|
164
165
|
// src/auth.ts: the options, and an instance only for the Better Auth CLI
|
|
165
|
-
import { betterAuth, type BetterAuthOptions } from
|
|
166
|
+
import { betterAuth, type BetterAuthOptions } from 'better-auth';
|
|
166
167
|
|
|
167
168
|
export const authOptions = { emailAndPassword: { enabled: true } } satisfies BetterAuthOptions;
|
|
168
169
|
export const auth = betterAuth(authOptions);
|
|
@@ -170,8 +171,8 @@ export const auth = betterAuth(authOptions);
|
|
|
170
171
|
|
|
171
172
|
```ts
|
|
172
173
|
// src/app.ts
|
|
173
|
-
import { createNanoApp } from
|
|
174
|
-
import { authOptions } from
|
|
174
|
+
import { createNanoApp } from '@nano-app/backend';
|
|
175
|
+
import { authOptions } from './auth';
|
|
175
176
|
|
|
176
177
|
createNanoApp({
|
|
177
178
|
packages: { betterAuth: authOptions },
|
|
@@ -203,20 +204,20 @@ A plugin is a group of endpoints mounted under a base path. Your routes are moun
|
|
|
203
204
|
|
|
204
205
|
```ts
|
|
205
206
|
// src/plugins/hello/index.ts
|
|
206
|
-
import { createPlugin } from
|
|
207
|
-
import { dirname, resolve } from
|
|
208
|
-
import { fileURLToPath } from
|
|
207
|
+
import { createPlugin } from '@nano-app/backend';
|
|
208
|
+
import { dirname, resolve } from 'node:path';
|
|
209
|
+
import { fileURLToPath } from 'node:url';
|
|
209
210
|
|
|
210
211
|
export default createPlugin((options?: { basePath?: string }) => ({
|
|
211
|
-
name:
|
|
212
|
-
basePath: options?.basePath ??
|
|
213
|
-
routesDir: resolve(dirname(fileURLToPath(import.meta.url)),
|
|
212
|
+
name: 'hello',
|
|
213
|
+
basePath: options?.basePath ?? '/hello',
|
|
214
|
+
routesDir: resolve(dirname(fileURLToPath(import.meta.url)), 'routes'),
|
|
214
215
|
}));
|
|
215
216
|
```
|
|
216
217
|
|
|
217
218
|
```ts
|
|
218
219
|
// src/plugins/hello/routes/index.route.ts → /hello
|
|
219
|
-
import { createRoute } from
|
|
220
|
+
import { createRoute } from '@nano-app/backend';
|
|
220
221
|
|
|
221
222
|
export default createRoute({
|
|
222
223
|
GET: async () => ({ success: true, result: { ok: true } }),
|
|
@@ -225,8 +226,8 @@ export default createRoute({
|
|
|
225
226
|
|
|
226
227
|
```ts
|
|
227
228
|
// src/app.ts
|
|
228
|
-
import { createNanoApp } from
|
|
229
|
-
import hello from
|
|
229
|
+
import { createNanoApp } from '@nano-app/backend';
|
|
230
|
+
import hello from './plugins/hello';
|
|
230
231
|
|
|
231
232
|
createNanoApp({ plugins: [hello()] });
|
|
232
233
|
```
|
|
@@ -245,12 +246,12 @@ Two levels. Only the plugin's receives the Express app, so nothing app-wide depe
|
|
|
245
246
|
|
|
246
247
|
```ts
|
|
247
248
|
export default createPlugin((options?: { publicPaths?: string[] }) => ({
|
|
248
|
-
name:
|
|
249
|
-
routesDir: resolve(dirname(fileURLToPath(import.meta.url)),
|
|
249
|
+
name: 'guard',
|
|
250
|
+
routesDir: resolve(dirname(fileURLToPath(import.meta.url)), 'routes'),
|
|
250
251
|
options: { publicPaths: options?.publicPaths ?? [] },
|
|
251
252
|
callbacks: {
|
|
252
253
|
onMount: async (app, { publicPaths }) => {
|
|
253
|
-
app.use((req, _res, next) => next(publicPaths.includes(req.path) ? undefined : { status: 401, message:
|
|
254
|
+
app.use((req, _res, next) => next(publicPaths.includes(req.path) ? undefined : { status: 401, message: 'Unauthorized' }));
|
|
254
255
|
},
|
|
255
256
|
},
|
|
256
257
|
}));
|
|
@@ -260,16 +261,16 @@ The endpoints of one plugin have no guaranteed order: one route's `onInit` must
|
|
|
260
261
|
|
|
261
262
|
Everything is mounted on one Express application, in this order: base middlewares → `before` plugins → your routes → `after` plugins → 404 → error handler.
|
|
262
263
|
|
|
263
|
-
A plugin with a database follows the same rules, with its client in `.nano-app/prisma-clients/<name>`: a plugin of yours points `database.schemaPath` at its schema and you apply it with Prisma. If that folder holds the client of another schema
|
|
264
|
+
A plugin with a database follows the same rules, with its client in `.nano-app/prisma-clients/<name>`: a plugin of yours points `database.schemaPath` at its schema and you apply it with Prisma. Two plugins with the same name and a database fail at startup. If that folder holds the client of another schema from an earlier start, the startup warns and replaces it. An official plugin of `@nano-app/plugins` needs nothing from you: at startup nano-app applies its migrations to its own `nano_*` schema. See [PLUGINS.md](https://github.com/Nano-Nathan/nano-app/blob/main/PLUGINS.md).
|
|
264
265
|
|
|
265
266
|
## Errors
|
|
266
267
|
|
|
267
268
|
The framework mounts the 404 and the error handler: an unmatched request becomes a `404`, and anything a handler throws becomes an `ApiResponse`.
|
|
268
269
|
|
|
269
270
|
```ts
|
|
270
|
-
throw { status: 409, message:
|
|
271
|
-
throw
|
|
272
|
-
throw new Error(
|
|
271
|
+
throw { status: 409, message: 'Already exists' }; // → 409 { success: false, warning: false, message: "Already exists" }
|
|
272
|
+
throw 'Field name is required'; // → 400 { success: false, message: "Field name is required" }
|
|
273
|
+
throw new Error('boom'); // → 500, logged to stderr
|
|
273
274
|
GET /unknown // → 404 { success: false, warning: false, message: "/unknown not found" }
|
|
274
275
|
```
|
|
275
276
|
|
|
@@ -282,7 +283,7 @@ GET /unknown // → 404 { success: false,
|
|
|
282
283
|
| `createPlugin(creator)` | Types a plugin factory and its options. |
|
|
283
284
|
| `createPluginCallbacks(callbacks)` | Types the callbacks of a plugin (`createPluginCallbacks<PrismaClient>` types `db`), e.g. to keep them in their own file. |
|
|
284
285
|
|
|
285
|
-
Also exported: the configuration types (`NanoAppConfig`, `PackagesConfig`, `PluginConfig`, `PluginStage`, `MainPluginConfig`, `DatabaseConfig`), the Express `Request` type and the HTTP contract (`ApiResponse`, `ApiPromise`, `QueryParams`, `RouteEntry`, `Handler`, `Handlers`, `NanoObject`) and the callback types (`Params`, `Callbacks`, `PluginCallbacks`).
|
|
286
|
+
Also exported: the `NanoApp` class that `createNanoApp` returns, the configuration types (`NanoAppConfig`, `PackagesConfig`, `PluginConfig`, `PluginStage`, `MainPluginConfig`, `DatabaseConfig`), the Express `Request` type and the HTTP contract (`ApiResponse`, `ApiPromise`, `QueryParams`, `RouteEntry`, `Handler`, `Handlers`, `NanoObject`) and the callback types (`Params`, `Callbacks`, `PluginCallbacks`).
|
|
286
287
|
|
|
287
288
|
## Development
|
|
288
289
|
|
package/build/index.d.ts
CHANGED
|
@@ -31,8 +31,9 @@ type Params<DB = any> = Partial<NanoObject & {
|
|
|
31
31
|
/** `{ user, session }` of Better Auth, on the routes behind its guard */
|
|
32
32
|
session?: NanoObject;
|
|
33
33
|
}>;
|
|
34
|
-
/** Endpoint
|
|
34
|
+
/** Endpoint handler */
|
|
35
35
|
type Handler<DB> = (req: Request$1, params: Params<DB>) => ApiPromise;
|
|
36
|
+
/** Endpoint handlers, one per HTTP method */
|
|
36
37
|
type Handlers<DB = any> = {
|
|
37
38
|
GET?: Handler<DB>;
|
|
38
39
|
POST?: Handler<DB>;
|
|
@@ -40,9 +41,11 @@ type Handlers<DB = any> = {
|
|
|
40
41
|
DELETE?: Handler<DB>;
|
|
41
42
|
QUERY?: Handler<DB>;
|
|
42
43
|
};
|
|
43
|
-
/**
|
|
44
|
+
/** Callback run with the params */
|
|
44
45
|
type Callback<DB> = (params: Params<DB>) => Promise<void>;
|
|
46
|
+
/** Callback run before a method, with its request */
|
|
45
47
|
type CallbackMethod<DB> = (req: Request$1, method: string, params: Params<DB>) => Promise<NanoObject>;
|
|
48
|
+
/** Callback run with the Express app */
|
|
46
49
|
type CallbackMount<DB> = (app: Express, params: Params<DB>) => Promise<void>;
|
|
47
50
|
/** Callbacks of an endpoint. They never receive the app: what affects the whole app goes in the plugin's onMount */
|
|
48
51
|
type Callbacks<DB = any> = {
|
|
@@ -79,7 +82,7 @@ type DatabaseConfig = {
|
|
|
79
82
|
type PluginStage = 'before' | 'after';
|
|
80
83
|
/** Plugin configuration: a group of endpoints */
|
|
81
84
|
type PluginConfig<Options = NanoObject> = {
|
|
82
|
-
/** Name of the plugin,
|
|
85
|
+
/** Name of the plugin, also the name of its database when it has one */
|
|
83
86
|
name: string;
|
|
84
87
|
/** Directory used to discover the `*.route.ts` files */
|
|
85
88
|
routesDir: string;
|
package/build/index.js
CHANGED
|
@@ -13,12 +13,7 @@ import { createJiti } from "jiti";
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
15
|
//#region ../core/const.ts
|
|
16
|
-
/**
|
|
17
|
-
const OFFICIAL_PLUGINS = "@nano-app/plugins";
|
|
18
|
-
/** Error when the database URL is missing, at startup and on `nano-app push` */
|
|
19
|
-
const MISSING_DATABASE_URL = "[nano-app] [ERROR] The DATABASE_URL environment variable is not defined.";
|
|
20
|
-
//#endregion
|
|
21
|
-
//#region ../core/loader/Validator.ts
|
|
16
|
+
/** HTTP methods a route can handle */
|
|
22
17
|
const HTTP_METHODS = [
|
|
23
18
|
"GET",
|
|
24
19
|
"POST",
|
|
@@ -26,6 +21,12 @@ const HTTP_METHODS = [
|
|
|
26
21
|
"DELETE",
|
|
27
22
|
"QUERY"
|
|
28
23
|
];
|
|
24
|
+
/** Package of the official plugins: only its schemas may declare nano_* */
|
|
25
|
+
const OFFICIAL_PLUGINS = "@nano-app/plugins";
|
|
26
|
+
/** Error when the database URL is missing at startup */
|
|
27
|
+
const MISSING_DATABASE_URL = "[nano-app] [ERROR] The DATABASE_URL environment variable is not defined.";
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region ../core/loader/Validator.ts
|
|
29
30
|
const STORE_TYPES = ["app", "api"];
|
|
30
31
|
/** Checks the consumer's files: invalid ones are warned about and discarded */
|
|
31
32
|
var Validator = class Validator {
|
|
@@ -44,15 +45,12 @@ var Validator = class Validator {
|
|
|
44
45
|
if (oEntry && typeof oEntry === "object") return true;
|
|
45
46
|
return Validator.warn(sFilePath, "the default export is not an object.");
|
|
46
47
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Type guard for a `*.route.ts` entry.
|
|
49
|
-
*/
|
|
48
|
+
/** Type guard for a `*.route.ts` entry */
|
|
50
49
|
static isRoute(oEntry, sFilePath) {
|
|
51
50
|
if (!Validator.isObject(oEntry, sFilePath)) return false;
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
if (!HTTP_METHODS.some((sMethod) => oRoute[sMethod])) return Validator.warn(sFilePath, "defines no HTTP method.");
|
|
51
|
+
if (oEntry.expression && typeof oEntry.expression !== "string") return Validator.warn(sFilePath, "\"expression\" must be a string.");
|
|
52
|
+
for (const sMethod of HTTP_METHODS) if (oEntry[sMethod] && typeof oEntry[sMethod] !== "function") return Validator.warn(sFilePath, `"${sMethod}" must be a function.`);
|
|
53
|
+
if (!HTTP_METHODS.some((sMethod) => oEntry[sMethod])) return Validator.warn(sFilePath, "defines no HTTP method.");
|
|
56
54
|
return true;
|
|
57
55
|
}
|
|
58
56
|
/** Type guard for a `*.store.ts` entry */
|
|
@@ -61,11 +59,10 @@ var Validator = class Validator {
|
|
|
61
59
|
if (!STORE_TYPES.includes(oEntry.type)) return Validator.warn(sFilePath, `its type must be "app" or "api".`);
|
|
62
60
|
if (oEntry.data !== void 0 && (!oEntry.data || typeof oEntry.data !== "object")) return Validator.warn(sFilePath, "\"data\" must be an object.");
|
|
63
61
|
if (oEntry.headers !== void 0 && (!Validator.isObject(oEntry.headers, sFilePath) || Object.values(oEntry.headers).some((sValue) => typeof sValue !== "string"))) return Validator.warn(sFilePath, "\"headers\" must be an object of strings.");
|
|
62
|
+
if (oEntry.auth !== void 0 && typeof oEntry.auth !== "string") return Validator.warn(sFilePath, "\"auth\" must be a string: the base path of Better Auth.");
|
|
64
63
|
return true;
|
|
65
64
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Type guard for a `.prisma` entry: the contents of the file, without comments.
|
|
68
|
-
*/
|
|
65
|
+
/** Type guard for a `.prisma` entry: the contents of the file, without comments */
|
|
69
66
|
static isPrisma(oEntry, sFilePath) {
|
|
70
67
|
if (typeof oEntry !== "string") return Validator.warn(sFilePath, "the contents are not a string.");
|
|
71
68
|
const sProvider = oEntry.match(/datasource\s+\w+\s*\{[^}]*provider\s*=\s*["']([^"']+)["']/s)?.[1];
|
|
@@ -90,6 +87,7 @@ var Validator = class Validator {
|
|
|
90
87
|
//#region ../core/loader/FileReader.ts
|
|
91
88
|
/** Reads and loads files from disk, resolved from `cwd` */
|
|
92
89
|
var FileReader = class {
|
|
90
|
+
/** Loader of the consumer's modules; the Prisma runtime is loaded natively */
|
|
93
91
|
static oJiti = createJiti(import.meta.url, { nativeModules: ["@prisma/client"] });
|
|
94
92
|
/** Loads a module via jiti */
|
|
95
93
|
static async loadModule(sFilePath, oOptions = {}) {
|
|
@@ -118,7 +116,8 @@ var FileReader = class {
|
|
|
118
116
|
var Formatter = class {
|
|
119
117
|
/** Message of anything thrown: its `message` when it has one, else the value itself */
|
|
120
118
|
static error(oError) {
|
|
121
|
-
return String(oError
|
|
119
|
+
if (oError && typeof oError === "object" && "message" in oError) return String(oError.message ?? oError);
|
|
120
|
+
return String(oError);
|
|
122
121
|
}
|
|
123
122
|
/** Formats a date for the request log, in UTC: `2026-10-19 12:03:10` */
|
|
124
123
|
static logDate(oDate = /* @__PURE__ */ new Date()) {
|
|
@@ -178,7 +177,8 @@ var Endpoint = class {
|
|
|
178
177
|
createHandler(oParams) {
|
|
179
178
|
return (oReq, oRes, fnNext) => {
|
|
180
179
|
const sMethod = oReq.method.toUpperCase();
|
|
181
|
-
const
|
|
180
|
+
const sHandled = HTTP_METHODS.find((sHttpMethod) => sHttpMethod === sMethod);
|
|
181
|
+
const fnMethod = sHandled && this.oMethods[sHandled];
|
|
182
182
|
if (!fnMethod) return fnNext({
|
|
183
183
|
status: 405,
|
|
184
184
|
message: `Method ${sMethod} not implemented`
|
|
@@ -320,7 +320,7 @@ var Prisma = class {
|
|
|
320
320
|
try {
|
|
321
321
|
sCli = createRequire(`${process.cwd()}/`).resolve("prisma/build/index.js");
|
|
322
322
|
} catch {
|
|
323
|
-
throw new Error(`[nano-app] [ERROR] The database "${sName}" needs Prisma
|
|
323
|
+
throw new Error(`[nano-app] [ERROR] The database "${sName}" needs Prisma. Run \`pnpm add prisma\`.`);
|
|
324
324
|
}
|
|
325
325
|
try {
|
|
326
326
|
execFileSync(process.execPath, [sCli, ...aArgs], {
|
|
@@ -331,9 +331,8 @@ var Prisma = class {
|
|
|
331
331
|
stdio: "pipe"
|
|
332
332
|
});
|
|
333
333
|
} catch (oError) {
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
throw new Error(`[nano-app] [ERROR] prisma ${aArgs[0]} failed for "${sName}": ${sOutput}`);
|
|
334
|
+
const sOutput = oError instanceof Error && "stderr" in oError && "stdout" in oError ? `${oError.stderr}${oError.stdout}`.trim() : "";
|
|
335
|
+
throw new Error(`[nano-app] [ERROR] prisma ${aArgs[0]} failed for "${sName}": ${sOutput || Formatter.error(oError)}`);
|
|
337
336
|
}
|
|
338
337
|
}
|
|
339
338
|
};
|
|
@@ -394,8 +393,7 @@ var Database = class {
|
|
|
394
393
|
if (dirname(sDir) === sDir) return false;
|
|
395
394
|
sDir = dirname(sDir);
|
|
396
395
|
}
|
|
397
|
-
|
|
398
|
-
return name === OFFICIAL_PLUGINS;
|
|
396
|
+
return JSON.parse(readFileSync(join(sDir, "package.json"), "utf-8")).name === OFFICIAL_PLUGINS;
|
|
399
397
|
}
|
|
400
398
|
};
|
|
401
399
|
//#endregion
|
|
@@ -432,7 +430,7 @@ var Plugin = class {
|
|
|
432
430
|
//#region src/app/config.ts
|
|
433
431
|
/** Initial application configuration */
|
|
434
432
|
const INITIAL_CONFIG = {
|
|
435
|
-
/**
|
|
433
|
+
/** Application port */
|
|
436
434
|
port: 3e3,
|
|
437
435
|
/** Application listener */
|
|
438
436
|
listener: () => {},
|
|
@@ -488,9 +486,14 @@ function deepMerge(oDefault, oValue) {
|
|
|
488
486
|
});
|
|
489
487
|
return Object.fromEntries(aKeys.map((sKey) => [sKey, deepMerge(oDefault[sKey], oValue[sKey])]));
|
|
490
488
|
}
|
|
489
|
+
/** Whether a merged configuration kept the plain object of the initial one */
|
|
490
|
+
function isMergedConfig(oValue) {
|
|
491
|
+
return isPlainObject(oValue);
|
|
492
|
+
}
|
|
491
493
|
/** Merges the consumer configuration over the initial one; the main plugin is always named "app" */
|
|
492
494
|
function mergeConfig(oConfig = {}) {
|
|
493
495
|
const oMerged = deepMerge(INITIAL_CONFIG, oConfig);
|
|
496
|
+
if (!isMergedConfig(oMerged)) throw new Error("[nano-app] [ERROR] The configuration of createNanoApp must be an object.");
|
|
494
497
|
return {
|
|
495
498
|
...oMerged,
|
|
496
499
|
mainPlugin: {
|
|
@@ -501,6 +504,10 @@ function mergeConfig(oConfig = {}) {
|
|
|
501
504
|
}
|
|
502
505
|
//#endregion
|
|
503
506
|
//#region src/app/errors.ts
|
|
507
|
+
/** Whether anything thrown is an object, read as a HandlerError */
|
|
508
|
+
function isHandlerError(oError) {
|
|
509
|
+
return typeof oError === "object" && oError !== null;
|
|
510
|
+
}
|
|
504
511
|
/** Normalizes anything thrown by a handler into an ApiResponse plus its status */
|
|
505
512
|
function normalize(oError) {
|
|
506
513
|
if (typeof oError === "string") return {
|
|
@@ -510,7 +517,7 @@ function normalize(oError) {
|
|
|
510
517
|
message: oError
|
|
511
518
|
}
|
|
512
519
|
};
|
|
513
|
-
const { status, message, warning } = oError
|
|
520
|
+
const { status, message, warning } = isHandlerError(oError) ? oError : {};
|
|
514
521
|
return {
|
|
515
522
|
status: status ?? 500,
|
|
516
523
|
body: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nano-app/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
|
-
"node": ">=
|
|
43
|
+
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"compression": "^1.8.2",
|