@nano-app/backend 0.16.0 → 0.17.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 CHANGED
@@ -48,7 +48,7 @@ export default createRoute({
48
48
 
49
49
  ## Routes
50
50
 
51
- Each `*.route.ts` under `mainPlugin.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`). `expression` is appended to that path as is, in Express syntax (`/:id`, `{/:id}` for optional):
51
+ Each `*.route.ts` under `mainPlugin.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({
package/build/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import express, { Express, Request, Request as Request$1 } from "express";
2
2
  import { CorsOptions } from "cors";
3
3
  import "dotenv/config";
4
4
  import "jiti";
5
+ import { z } from "zod";
5
6
  import "better-auth";
6
7
  /** API response */
7
8
  type ApiResponse<T = any> = {
@@ -55,7 +56,7 @@ type Callback<Deps> = (params: Params<Deps>) => Promise<void>;
55
56
  type CallbackMethod<Deps> = (req: Request$1, method: string, params: Params<Deps>) => Promise<NanoObject>;
56
57
  /** Callback run with the Express app */
57
58
  type CallbackMount<Deps> = (app: Express, params: Params<Deps>) => Promise<void>;
58
- /** Callbacks of an endpoint. They never receive the app: what affects the whole app goes in the plugin's onMount */
59
+ /** Callbacks of an endpoint; what affects the whole app goes in the plugin's onMount */
59
60
  type Callbacks<Deps = NanoObject<any>> = {
60
61
  /** Run once, when the endpoint is mounted */
61
62
  onInit?: Callback<Deps>;
@@ -79,7 +80,7 @@ type RouteEntry<Deps = NanoObject<any>> = {
79
80
  } & Handlers<Deps>;
80
81
  /** Generic object */
81
82
  type NanoObject<T = any> = Record<string, T>;
82
- /** Error of the framework: Node shows it as `[nano-app] [ERROR]: <message>`, and a handler answers its status */
83
+ /** Error of the framework, with the status a handler answers */
83
84
  declare class NanoError extends Error {
84
85
  status?: number;
85
86
  /** Error with its message, and the status a handler answers (500 without one) */
@@ -89,7 +90,7 @@ declare class NanoError extends Error {
89
90
  }
90
91
  /** Database of a plugin or of the app */
91
92
  type DatabaseConfig = {
92
- /** Its Drizzle schema: the module of its tables, with its `relations` when it has them (`import * as schema from './db/schema'`) */
93
+ /** Its Drizzle schema: the module of its tables and `relations` */
93
94
  schema: NanoObject;
94
95
  };
95
96
  /** When a plugin is mounted, relative to the consumer routes */
@@ -111,19 +112,8 @@ type PluginConfig<Options = NanoObject> = {
111
112
  /** Plugin callbacks */
112
113
  callbacks?: PluginCallbacks;
113
114
  };
114
- /** The project's plugin: nano-app names it "app" and mounts it between the "before" and "after" plugins */
115
- type MainPluginConfig<Options = NanoObject> = {
116
- /** Directory of the project's `*.route.ts` files. Defaults to "src/routes" */
117
- routesDir?: string;
118
- /** Base path prepended to every route, e.g. "/api" (no trailing slash). Defaults to none */
119
- basePath?: string;
120
- /** Drizzle schema of the project's database; without it the app has none */
121
- database?: DatabaseConfig;
122
- /** Plugin options */
123
- options?: Options;
124
- /** Plugin callbacks */
125
- callbacks?: PluginCallbacks;
126
- };
115
+ /** The project's plugin, named "app": `routesDir` defaults to "src/routes" */
116
+ type MainPluginConfig<Options = NanoObject> = Partial<Omit<PluginConfig<Options>, 'name' | 'stage'>>;
127
117
  /** Configuration of the framework's built-in packages */
128
118
  type PackagesConfig = {
129
119
  /** Morgan configuration */
@@ -160,7 +150,7 @@ type NanoAppConfig = {
160
150
  mainPlugin?: MainPluginConfig;
161
151
  };
162
152
  /** Merges the consumer configuration over the initial one; the main plugin is always named "app" */
163
- declare function mergeConfig(oConfig?: NanoAppConfig): NanoAppConfig & Required<Pick<NanoAppConfig, 'settings' | 'plugins' | 'packages'>> & {
153
+ declare function mergeConfig(oConfig?: NanoAppConfig): NanoAppConfig & Required<Pick<NanoAppConfig, 'port' | 'settings' | 'plugins' | 'packages'>> & {
164
154
  mainPlugin: PluginConfig;
165
155
  };
166
156
  /** Main NanoApp class */
package/build/index.js CHANGED
@@ -8,8 +8,9 @@ import { glob } from "glob";
8
8
  import { isAbsolute, resolve } from "node:path";
9
9
  import { existsSync } from "node:fs";
10
10
  import { createJiti } from "jiti";
11
+ import { z } from "zod";
11
12
  // ../core/utils/NanoError.ts
12
- /** Error of the framework: Node shows it as `[nano-app] [ERROR]: <message>`, and a handler answers its status */
13
+ /** Error of the framework, with the status a handler answers */
13
14
  var NanoError = class extends Error {
14
15
  status;
15
16
  /** Error with its message, and the status a handler answers (500 without one) */
@@ -41,7 +42,7 @@ var FileReader = class {
41
42
  // ../core/files/FileLoader.ts
42
43
  /** Finds and loads the consumer's files */
43
44
  var FileLoader = class {
44
- /** Finds `*.<extension>.ts` (or built `.js`) files in a folder (`src/<extension>s` by default) and returns `{ [subPath]: defaultExport }` */
45
+ /** Finds the `*.<extension>.ts` files of a folder: `{ [subPath]: defaultExport }` */
45
46
  static async findMany(sExtension, sSourceDir) {
46
47
  const sSrcDir = (sSourceDir ?? `src/${sExtension}s`).replace(/^\.\//, "").replace(/\/$/, "");
47
48
  const aFilePaths = await glob(`${sSrcDir}/**/*.${sExtension}.{ts,js}`, {
@@ -57,89 +58,20 @@ var FileLoader = class {
57
58
  return oEntries;
58
59
  }
59
60
  };
60
- // ../core/const.ts
61
- /** HTTP methods a route can handle */
62
- const HTTP_METHODS = [
63
- "GET",
64
- "POST",
65
- "PUT",
66
- "DELETE",
67
- "QUERY"
68
- ];
69
- /** Loopback and private networks: the proxies the backend trusts by default */
70
- const PRIVATE_NETWORKS = [
71
- "127.0.0.0/8",
72
- "::1/128",
73
- "10.0.0.0/8",
74
- "172.16.0.0/12",
75
- "192.168.0.0/16",
76
- "fc00::/7"
77
- ];
78
- // ../core/utils/Paths.ts
79
- /** Paths of the app's pages */
80
- var Paths = class {
81
- /** Whether a path matches a pattern: exact (`/admin`), or the path and all below it (`/admin/*`) */
82
- static matches(sPattern, sPath) {
83
- if (!sPattern.endsWith("/*")) return sPattern === sPath;
84
- const sBase = sPattern.slice(0, -2);
85
- return sPath === sBase || sPath.startsWith(`${sBase}/`);
86
- }
87
- };
88
61
  // ../core/utils/Validator.ts
89
- const STORE_TYPES = ["app", "api"];
90
62
  /** Checks values of the framework and the consumer's files; an invalid file throws a `NanoError` */
91
- var Validator = class Validator {
63
+ var Validator = class {
92
64
  /** Whether a value is a plain object */
93
65
  static isObject(oValue) {
94
66
  return Object.prototype.toString.call(oValue) === "[object Object]";
95
67
  }
96
- /** Whether a value is a plain object of strings */
97
- static isStringMap(oValue) {
98
- return Validator.isObject(oValue) && Object.values(oValue).every((sItem) => typeof sItem === "string");
99
- }
100
- /** Whether a value is a path that starts with `/` */
101
- static isPath(oValue) {
102
- return typeof oValue === "string" && oValue.startsWith("/");
103
- }
104
- /** First environment variable of a list that is not defined */
105
- static missingEnv(aNames) {
106
- return aNames.find((sName) => !process.env[sName]);
107
- }
108
- /** Checks every entry with a guard (`Validator.isRoute`...): the first invalid one throws */
109
- static validate(oEntries, fnGuard) {
110
- return Object.fromEntries(Object.entries(oEntries).filter(([sKey, oEntry]) => fnGuard(oEntry, sKey)));
111
- }
112
- /** Type guard for a `*.route.ts` entry */
113
- static isRoute(oEntry, sFilePath) {
114
- if (!Validator.isObject(oEntry)) throw new NanoError(`${sFilePath}: the default export is not an object.`);
115
- if (oEntry.expression && typeof oEntry.expression !== "string") throw new NanoError(`${sFilePath}: "expression" must be a string.`);
116
- if (oEntry.public !== void 0 && typeof oEntry.public !== "boolean") throw new NanoError(`${sFilePath}: "public" must be a boolean.`);
117
- for (const sMethod of HTTP_METHODS) {
118
- const oMethod = oEntry[sMethod];
119
- if (oMethod && typeof oMethod !== "function" && typeof oMethod.handler !== "function") throw new NanoError(`${sFilePath}: "${sMethod}" must be a function or { handler, permissions }.`);
120
- }
121
- if (!HTTP_METHODS.some((sMethod) => oEntry[sMethod])) throw new NanoError(`${sFilePath}: defines no HTTP method.`);
122
- return true;
123
- }
124
- /** Type guard for a `*.store.ts` entry */
125
- static isStore(oEntry, sFilePath) {
126
- if (!Validator.isObject(oEntry)) throw new NanoError(`${sFilePath}: the default export is not an object.`);
127
- if (!STORE_TYPES.includes(oEntry.type)) throw new NanoError(`${sFilePath}: its type must be "app" or "api".`);
128
- if (oEntry.data !== void 0 && !Validator.isObject(oEntry.data)) throw new NanoError(`${sFilePath}: "data" must be an object.`);
129
- if (oEntry.headers !== void 0 && !Validator.isStringMap(oEntry.headers)) throw new NanoError(`${sFilePath}: "headers" must be an object of strings.`);
130
- if (oEntry.auth !== void 0 && !Validator.isAuth(oEntry.auth)) throw new NanoError(`${sFilePath}: "auth" must be { path, routes }: each route { match, redirect, roles, permissions } with paths starting with /, roles as "admin,user", permissions as { resource: "read,update" }, and a redirect outside its match.`);
131
- return true;
132
- }
133
- /** Whether `auth` has the base path of Better Auth and valid routes, none redirecting to itself */
134
- static isAuth(oAuth) {
135
- if (!Validator.isObject(oAuth) || typeof oAuth.path !== "string") return false;
136
- const aRoutes = oAuth.routes ?? [];
137
- if (!Array.isArray(aRoutes)) return false;
138
- return aRoutes.every((oRoute) => {
139
- if (!Validator.isObject(oRoute)) return false;
140
- const { match, redirect = "/", roles, permissions } = oRoute;
141
- return Validator.isPath(match) && Validator.isPath(redirect) && !Paths.matches(match, redirect) && (roles === void 0 || typeof roles === "string") && (permissions === void 0 || Validator.isStringMap(permissions));
142
- });
68
+ /** Checks every entry with a zod schema (`ROUTE`, `STORE`): the first invalid one throws with its key */
69
+ static validate(oEntries, oSchema) {
70
+ return Object.fromEntries(Object.entries(oEntries).map(([sKey, oEntry]) => {
71
+ const oResult = oSchema.safeParse(oEntry);
72
+ if (!oResult.success) throw new NanoError(`${sKey}:\n${z.prettifyError(oResult.error)}`);
73
+ return [sKey, oResult.data];
74
+ }));
143
75
  }
144
76
  };
145
77
  // ../core/utils/Logger.ts
@@ -166,58 +98,69 @@ var Logger = class Logger {
166
98
  return oDate.toISOString().slice(0, 19).replace("T", " ");
167
99
  }
168
100
  };
169
- // src/auth/BetterAuth.ts
170
- /** Files of the app's Better Auth options, relative to `cwd`: source, then built */
171
- const AUTH_FILES = ["src/auth.ts", "src/auth.js"];
172
- /** Better Auth on the app's Drizzle client: the instance, its handler and the session lookup */
173
- var BetterAuth = class {
174
- static oAuth;
175
- /** The options of the app's `src/auth.ts`, or undefined without it: Better Auth stays off */
176
- static async findOptions() {
177
- const sFilePath = AUTH_FILES.find(existsSync);
178
- if (!sFilePath) return void 0;
179
- const oFile = await FileReader.loadModule(sFilePath, { default: true });
180
- if (!Validator.isObject(oFile?.options)) throw new NanoError(`${sFilePath} must default export { options }: the Better Auth options.`);
181
- return oFile.options;
182
- }
183
- /** The instance, once the app's plugin created it */
184
- static get() {
185
- return this.oAuth;
186
- }
187
- /** Creates the instance on the app's client and schema, its handler and the session lookup */
188
- static async create(oOptions, oDb, oSchema) {
189
- const { betterAuth } = await import("better-auth");
190
- const { drizzleAdapter } = await import("better-auth/adapters/drizzle");
191
- const { getSchema } = await import("better-auth/db");
192
- const { fromNodeHeaders, toNodeHandler } = await import("better-auth/node");
193
- const aMissing = Object.keys(getSchema(oOptions)).filter((sModel) => !oSchema[sModel]);
194
- 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.`);
195
- const oAuth = betterAuth({
196
- ...oOptions,
197
- advanced: {
198
- ...oOptions.advanced,
199
- ipAddress: {
200
- trustedProxies: PRIVATE_NETWORKS,
201
- ...oOptions.advanced?.ipAddress
202
- }
203
- },
204
- database: drizzleAdapter(oDb, {
205
- provider: "pg",
206
- schema: oSchema
207
- })
208
- });
209
- this.oAuth = oAuth;
210
- const fnSession = (oReq, oRes, fnNext) => oAuth.api.getSession({ headers: fromNodeHeaders(oReq.headers) }).then((oSession) => {
211
- oRes.locals.session = oSession ?? void 0;
212
- fnNext();
213
- });
214
- return {
215
- oHandler: toNodeHandler(oAuth),
216
- fnSession,
217
- sPath: `${oOptions.basePath ?? "/api/auth"}/{*path}`
218
- };
101
+ // ../core/utils/Paths.ts
102
+ /** Paths of the app's pages */
103
+ var Paths = class {
104
+ /** Whether a path matches a pattern: exact (`/admin`), or the path and all below it (`/admin/*`) */
105
+ static matches(sPattern, sPath) {
106
+ if (!sPattern.endsWith("/*")) return sPattern === sPath;
107
+ const sBase = sPattern.slice(0, -2);
108
+ return sPath === sBase || sPath.startsWith(`${sBase}/`);
219
109
  }
220
110
  };
111
+ // ../core/const.ts
112
+ /** Loopback and private networks: the proxies the backend trusts by default */
113
+ const PRIVATE_NETWORKS = [
114
+ "127.0.0.0/8",
115
+ "::1/128",
116
+ "10.0.0.0/8",
117
+ "172.16.0.0/12",
118
+ "192.168.0.0/16",
119
+ "fc00::/7"
120
+ ];
121
+ // ../core/utils/validations.ts
122
+ /** A function of the consumer, typed as what the framework calls */
123
+ function callable() {
124
+ return z.custom((oValue) => typeof oValue === "function");
125
+ }
126
+ /** Permissions of Better Auth's admin plugin: the actions per resource */
127
+ const PERMISSIONS = z.record(z.string(), z.array(z.string())).optional();
128
+ /** A method of a route: its handler, or it with its own permissions */
129
+ const METHOD = z.union([callable(), z.object({
130
+ handler: callable(),
131
+ permissions: PERMISSIONS
132
+ })]).optional();
133
+ /** A `*.route.ts` file: its options, its callbacks and at least one HTTP method, nothing else */
134
+ const ROUTE = z.strictObject({
135
+ expression: z.string().optional(),
136
+ public: z.boolean().optional(),
137
+ permissions: PERMISSIONS,
138
+ callbacks: z.object({
139
+ onInit: callable().optional(),
140
+ onBefore: callable().optional()
141
+ }).optional(),
142
+ GET: METHOD,
143
+ POST: METHOD,
144
+ PUT: METHOD,
145
+ DELETE: METHOD,
146
+ QUERY: METHOD
147
+ }).refine(({ GET, POST, PUT, DELETE, QUERY }) => GET ?? POST ?? PUT ?? DELETE ?? QUERY, { error: "It defines no HTTP method" });
148
+ /** A path of the app, starting with `/` */
149
+ const PATH = z.string().startsWith("/");
150
+ z.object({
151
+ type: z.enum(["app", "api"]),
152
+ data: z.record(z.string(), z.any()).optional(),
153
+ headers: z.record(z.string(), z.string()).optional(),
154
+ auth: z.object({
155
+ path: z.string(),
156
+ routes: z.array(z.object({
157
+ match: PATH,
158
+ redirect: PATH.optional(),
159
+ roles: z.string().optional(),
160
+ permissions: z.record(z.string(), z.string()).optional()
161
+ }).refine(({ match, redirect }) => !Paths.matches(match, redirect ?? "/"), { error: "Its \"redirect\" must be outside its \"match\"" })).optional()
162
+ }).optional()
163
+ });
221
164
  // src/auth/permissions.ts
222
165
  /** Whether the session's user has the permissions; always without Better Auth or permissions */
223
166
  async function isGranted({ auth, session }, oPermissions) {
@@ -257,9 +200,8 @@ var Endpoint = class {
257
200
  createHandler(oParams) {
258
201
  return (oReq, oRes, fnNext) => {
259
202
  if (oParams.auth && !oRes.locals.session && !this.isPublic) return fnNext(new NanoError("Not authenticated", 401));
260
- const sMethod = oReq.method.toUpperCase();
261
- const sHandled = HTTP_METHODS.find((sHttpMethod) => sHttpMethod === sMethod);
262
- const oMethod = sHandled && this.oMethods[sHandled];
203
+ const sMethod = oReq.method;
204
+ const oMethod = this.oMethods[sMethod];
263
205
  if (!oMethod) return fnNext(new NanoError(`Method ${sMethod} not implemented`, 405));
264
206
  const oRequestParams = {
265
207
  ...oParams,
@@ -295,7 +237,7 @@ var Router = class {
295
237
  async mount() {
296
238
  if (!this.sRoutesDir) return [];
297
239
  const oFiles = await FileLoader.findMany("route", this.sRoutesDir);
298
- const oRoutes = Validator.validate(oFiles, Validator.isRoute);
240
+ const oRoutes = Validator.validate(oFiles, ROUTE);
299
241
  return Object.entries(oRoutes).map(([sFilePath, { expression, callbacks, public: isPublic, permissions, ...methods }]) => {
300
242
  const sPath = this.createPath(sFilePath, expression);
301
243
  Logger.info(sPath);
@@ -314,6 +256,58 @@ var Router = class {
314
256
  return `${this.sBasePath}${sEndpoint.replace(/\/index$/, "")}${sExpression}` || "/";
315
257
  }
316
258
  };
259
+ // src/auth/BetterAuth.ts
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
+ };
317
311
  // src/database/Database.ts
318
312
  /** Drizzle clients of the app and its plugins, one per name, on one pool of `DATABASE_URL` */
319
313
  var Database = class {
@@ -324,17 +318,23 @@ var Database = class {
324
318
  static get(sName) {
325
319
  return this.oClients[sName];
326
320
  }
327
- /** Mounts the Drizzle client of a schema: the app's tables outside `nano_*`, a plugin's only in `nano_<name>` */
321
+ /** Mounts the Drizzle client of a schema, checking the `nano_*` rule of its tables */
328
322
  static async mount(sName, { schema }) {
329
323
  if (this.get(sName)) throw new NanoError(`The database "${sName}" is already mounted.`);
330
324
  const { defineRelations, is } = await import("drizzle-orm");
331
325
  const { getTableConfig, PgTable } = await import("drizzle-orm/pg-core");
332
326
  const { drizzle } = await import("drizzle-orm/node-postgres");
333
327
  const sOwn = `nano_${sName}`;
334
- const isApp = sName === "app";
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
335
  for (const oTable of Object.values(schema).filter((oValue) => is(oValue, PgTable))) {
336
336
  const { name, schema: sSchema = "public" } = getTableConfig(oTable);
337
- if (isApp ? sSchema.startsWith("nano_") : sSchema !== sOwn) throw new NanoError(`The database "${sName}": table "${sSchema}.${name}" breaks the rule, ${isApp ? "the nano_* schemas are reserved for plugins" : `a plugin keeps its tables in "${sOwn}"`}.`);
337
+ if (breaks(sSchema)) throw new NanoError(`The database "${sName}": table "${sSchema}.${name}" breaks the rule, ${rule}.`);
338
338
  }
339
339
  this.oPool ??= await this.pool();
340
340
  const oClient = drizzle({
@@ -357,19 +357,18 @@ var Database = class {
357
357
  /** A group of routes, with its options, callbacks and database */
358
358
  var Plugin = class {
359
359
  oConfig;
360
- oAuthOptions;
361
- /** Creates a new Plugin instance; the app's plugin also gets the Better Auth options */
362
- constructor(oConfig, oAuthOptions) {
360
+ /** Creates a new Plugin instance */
361
+ constructor(oConfig) {
363
362
  this.oConfig = oConfig;
364
- this.oAuthOptions = oAuthOptions;
365
363
  }
366
364
  /** Mounts its endpoints on express app */
367
365
  async mount(oApp) {
368
366
  const { name, routesDir, basePath, database, options, callbacks } = this.oConfig;
367
+ const oAuthOptions = name === "app" ? await BetterAuth.findOptions() : void 0;
369
368
  const oDb = database ? await Database.mount(name, database) : Database.get("app");
370
- if (this.oAuthOptions) {
369
+ if (oAuthOptions) {
371
370
  if (!database) throw new NanoError("src/auth.ts needs the database of the app: set mainPlugin.database.schema.");
372
- const { sPath, oHandler, fnSession } = await BetterAuth.create(this.oAuthOptions, oDb, database.schema);
371
+ const { sPath, oHandler, fnSession } = await BetterAuth.create(oAuthOptions, oDb, database.schema);
373
372
  oApp.all(sPath, oHandler);
374
373
  oApp.use(fnSession);
375
374
  }
@@ -388,8 +387,6 @@ var Plugin = class {
388
387
  const INITIAL_CONFIG = {
389
388
  /** Application port */
390
389
  port: 3e3,
391
- /** Application listener */
392
- listener: () => {},
393
390
  /** Express settings: the client IP comes from `X-Forwarded-For` only through private proxies */
394
391
  settings: { "trust proxy": PRIVATE_NETWORKS },
395
392
  /** Initial middlewares */
@@ -449,7 +446,7 @@ function mergeConfig(oConfig = {}) {
449
446
  };
450
447
  }
451
448
  // src/app/errors.ts
452
- /** Database errors the request caused, by their Postgres code or Drizzle's name; their own messages name tables and columns */
449
+ /** Database errors the request caused, by their Postgres code or Drizzle's name */
453
450
  const DATABASE_ERRORS = {
454
451
  "23505": {
455
452
  status: 409,
@@ -522,7 +519,7 @@ var NanoApp = class {
522
519
  async start() {
523
520
  try {
524
521
  await this.initialize();
525
- this.oApp.listen(Number(this.oConfig.port), () => {
522
+ this.oApp.listen(this.oConfig.port, () => {
526
523
  this.oConfig.listener?.();
527
524
  Logger.info(`Nano App running on port ${this.oConfig.port}`);
528
525
  });
@@ -535,20 +532,22 @@ var NanoApp = class {
535
532
  async initialize() {
536
533
  for (const [sKey, oValue] of Object.entries(this.oConfig.settings)) this.oApp.set(sKey, oValue);
537
534
  this.setPackages(this.oConfig.packages);
535
+ const { mainPlugin, plugins } = this.oConfig;
538
536
  const oPlugins = {
539
537
  before: [],
540
538
  after: []
541
539
  };
542
- for (const oPlugin of this.oConfig.plugins) oPlugins[oPlugin.stage ?? "before"].push(oPlugin);
543
- for (const oPlugin of oPlugins.before) await new Plugin(oPlugin).mount(this.oApp);
544
- const { mainPlugin } = this.oConfig;
545
- await new Plugin(mainPlugin, await BetterAuth.findOptions()).mount(this.oApp);
546
- for (const oPlugin of oPlugins.after) await new Plugin(oPlugin).mount(this.oApp);
540
+ for (const oPlugin of plugins) oPlugins[oPlugin.stage ?? "before"].push(oPlugin);
541
+ for (const oPlugin of [
542
+ ...oPlugins.before,
543
+ mainPlugin,
544
+ ...oPlugins.after
545
+ ]) await new Plugin(oPlugin).mount(this.oApp);
547
546
  this.oApp.use(notFoundHandler);
548
547
  this.oApp.use(errorHandler);
549
548
  }
550
549
  /** Adds the packages according to the configuration */
551
- setPackages(oConfig = {}) {
550
+ setPackages(oConfig) {
552
551
  if (oConfig.morgan) {
553
552
  for (const [sKey, fnToken] of Object.entries(oConfig.morgan.tokens ?? {})) morgan.token(sKey, fnToken);
554
553
  this.oApp.use(morgan(oConfig.morgan.format));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nano-app/backend",
3
- "version": "0.16.0",
3
+ "version": "0.17.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",
@@ -50,7 +50,8 @@
50
50
  "glob": "^14.0.4",
51
51
  "helmet": "^8.3.0",
52
52
  "jiti": "^2.7.0",
53
- "morgan": "^1.12.1"
53
+ "morgan": "^1.12.1",
54
+ "zod": "^4.6.7"
54
55
  },
55
56
  "peerDependencies": {
56
57
  "better-auth": "^1.7.18",