@nano-app/backend 0.8.3 → 0.10.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/build/index.js CHANGED
@@ -1,551 +1,609 @@
1
- // src/app/NanoApp.ts
1
+ import { createRequire } from "node:module";
2
2
  import express from "express";
3
-
4
- // ../core/loader/FileLoader.ts
5
- import { mkdirSync, writeFileSync } from "fs";
6
- import { dirname, resolve as resolve2 } from "path";
3
+ import morgan from "morgan";
4
+ import cors from "cors";
5
+ import helmet from "helmet";
6
+ import compression from "compression";
7
+ import rateLimit from "express-rate-limit";
8
+ import "dotenv/config";
7
9
  import { glob } from "glob";
8
-
9
- // ../core/loader/Validator.ts
10
- var HTTP_METHODS = ["GET", "POST", "PUT", "DELETE"];
11
- var Validator = class {
12
- static validators = {
13
- "route": () => this.isRoute,
14
- "store": () => this.isStore,
15
- "prisma": () => this.isPrisma
16
- };
17
- /**
18
- * Type guard for a `*.route.ts` entry.
19
- */
20
- static isRoute(entry, filePath) {
21
- if (!entry || typeof entry !== "object") {
22
- console.warn(`[nano-app] [WARNING] ${filePath}: the default export is not an object.`);
23
- return false;
24
- }
25
- const route = entry;
26
- if (route.expression && typeof route.expression !== "string") {
27
- console.warn(`[nano-app] [WARNING] ${filePath}: "expression" must be a string.`);
28
- return false;
29
- }
30
- for (const method of HTTP_METHODS) {
31
- if (route[method] && typeof route[method] !== "function") {
32
- console.warn(`[nano-app] [WARNING] ${filePath}: "${String(method)}" must be a function.`);
33
- return false;
34
- }
35
- }
36
- if (!HTTP_METHODS.some((method) => route[method])) {
37
- console.warn(`[nano-app] [WARNING] ${filePath}: no defines any HTTP method.`);
38
- return false;
39
- }
40
- return true;
41
- }
42
- /**
43
- * Type guard for a `*.store.ts` entry.
44
- */
45
- static isStore(entry, filePath) {
46
- if (!entry || typeof entry !== "object") {
47
- console.warn(`[nano-app] [WARNING] ${filePath}: the default export is not an object.`);
48
- return false;
49
- }
50
- const store = entry;
51
- if (store.type !== "app" && store.type !== "api") {
52
- console.warn(`[nano-app] [WARNING] ${filePath}: "type" must be "app" or "api".`);
53
- return false;
54
- }
55
- if (store.type === "api" && !store.path) {
56
- console.warn(`[nano-app] [WARNING] ${filePath}: "path" is required for type "api".`);
57
- return false;
58
- }
59
- return true;
60
- }
61
- /**
62
- * Type guard for a `.prisma` entry: the contents of the file, without comments.
63
- */
64
- static isPrisma(entry, filePath) {
65
- if (typeof entry !== "string") {
66
- console.warn(`[nano-app] [WARNING] ${filePath}: the contents are not a string.`);
67
- return false;
68
- }
69
- const provider = entry.match(/datasource\s+\w+\s*\{[^}]*provider\s*=\s*["']([^"']+)["']/s)?.[1];
70
- if (provider !== "postgresql") {
71
- console.warn(`[nano-app] [WARNING] ${filePath}: the datasource provider must be "postgresql"${provider ? `, not "${provider}"` : ""}.`);
72
- return false;
73
- }
74
- return true;
75
- }
76
- /** Validates a full map of entries and returns only the valid ones */
77
- static validate(entries, extension) {
78
- const valid = {};
79
- for (const [filePath, entry] of Object.entries(entries)) {
80
- const validated = this.validateUnique(entry, extension, filePath);
81
- if (validated !== void 0) valid[filePath] = validated;
82
- }
83
- return valid;
84
- }
85
- /** Validates one entry: returns it when valid, `undefined` otherwise */
86
- static validateUnique(entry, extension, filePath = "") {
87
- return this.validators[extension]?.()(entry, filePath) ? entry : void 0;
88
- }
89
- };
90
-
91
- // ../core/loader/FileReader.ts
10
+ import { dirname, isAbsolute, join, resolve } from "node:path";
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
92
12
  import { createJiti } from "jiti";
93
- import { isAbsolute, resolve } from "path";
94
- import { readFileSync, existsSync } from "fs";
95
- var FileReader = class {
96
- // The Prisma runtime is loaded natively: through jiti its interop breaks the error messages
97
- static jiti = createJiti(import.meta.url, { nativeModules: ["@prisma/client"] });
98
- /** Loads a module via jiti */
99
- static async loadModule(filePath, options = {}) {
100
- return await this.jiti.import(
101
- this.getAbsolutePath(filePath),
102
- options
103
- );
104
- }
105
- /** Reads the contents of a file */
106
- static loadFile(filePath) {
107
- const content = readFileSync(this.getAbsolutePath(filePath), "utf-8");
108
- if (!content) {
109
- throw new Error(`[nano-app] [ERROR] The file ${filePath} does not have valid content.`);
110
- }
111
- return content;
112
- }
113
- /** Resolves the absolute path of a file */
114
- static getAbsolutePath(filePath) {
115
- const absolutePath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
116
- if (!existsSync(absolutePath)) {
117
- throw new Error(`[nano-app] [ERROR] ${filePath} not found.`);
118
- }
119
- return absolutePath;
120
- }
13
+ import { execFileSync } from "node:child_process";
14
+ import { createHash } from "node:crypto";
15
+ //#region ../core/const.ts
16
+ /** Package of the official plugins: only its schemas may declare nano_* */
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
22
+ const HTTP_METHODS = [
23
+ "GET",
24
+ "POST",
25
+ "PUT",
26
+ "DELETE",
27
+ "QUERY"
28
+ ];
29
+ const STORE_TYPES = ["app", "api"];
30
+ /** Checks the consumer's files: invalid ones are warned about and discarded */
31
+ var Validator = class Validator {
32
+ static oValidators = {
33
+ route: () => this.isRoute,
34
+ prisma: () => this.isPrisma,
35
+ store: () => this.isStore
36
+ };
37
+ /** Warns about an invalid file and returns `false`, to discard it */
38
+ static warn(sFilePath, sMessage) {
39
+ console.warn(`[nano-app] [WARNING] ${sFilePath}: ${sMessage}`);
40
+ return false;
41
+ }
42
+ /** Checks that the default export is an object */
43
+ static isObject(oEntry, sFilePath) {
44
+ if (oEntry && typeof oEntry === "object") return true;
45
+ return Validator.warn(sFilePath, "the default export is not an object.");
46
+ }
47
+ /**
48
+ * Type guard for a `*.route.ts` entry.
49
+ */
50
+ static isRoute(oEntry, sFilePath) {
51
+ if (!Validator.isObject(oEntry, sFilePath)) return false;
52
+ const oRoute = oEntry;
53
+ if (oRoute.expression && typeof oRoute.expression !== "string") return Validator.warn(sFilePath, "\"expression\" must be a string.");
54
+ for (const sMethod of HTTP_METHODS) if (oRoute[sMethod] && typeof oRoute[sMethod] !== "function") return Validator.warn(sFilePath, `"${String(sMethod)}" must be a function.`);
55
+ if (!HTTP_METHODS.some((sMethod) => oRoute[sMethod])) return Validator.warn(sFilePath, "defines no HTTP method.");
56
+ return true;
57
+ }
58
+ /** Type guard for a `*.store.ts` entry */
59
+ static isStore(oEntry, sFilePath) {
60
+ if (!Validator.isObject(oEntry, sFilePath)) return false;
61
+ if (!STORE_TYPES.includes(oEntry.type)) return Validator.warn(sFilePath, `its type must be "app" or "api".`);
62
+ if (oEntry.data !== void 0 && (!oEntry.data || typeof oEntry.data !== "object")) return Validator.warn(sFilePath, "\"data\" must be an object.");
63
+ 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.");
64
+ return true;
65
+ }
66
+ /**
67
+ * Type guard for a `.prisma` entry: the contents of the file, without comments.
68
+ */
69
+ static isPrisma(oEntry, sFilePath) {
70
+ if (typeof oEntry !== "string") return Validator.warn(sFilePath, "the contents are not a string.");
71
+ const sProvider = oEntry.match(/datasource\s+\w+\s*\{[^}]*provider\s*=\s*["']([^"']+)["']/s)?.[1];
72
+ if (sProvider !== "postgresql") return Validator.warn(sFilePath, `the datasource provider must be "postgresql"${sProvider ? `, not "${sProvider}"` : ""}.`);
73
+ return true;
74
+ }
75
+ /** Validates a full map of entries and returns only the valid ones */
76
+ static validate(oEntries, sExtension) {
77
+ const oValid = {};
78
+ for (const [sFilePath, oEntry] of Object.entries(oEntries)) {
79
+ const oValidated = this.validateUnique(oEntry, sExtension, sFilePath);
80
+ if (oValidated !== void 0) oValid[sFilePath] = oValidated;
81
+ }
82
+ return oValid;
83
+ }
84
+ /** Validates one entry: returns it when valid, `undefined` otherwise */
85
+ static validateUnique(oEntry, sExtension, sFilePath = "") {
86
+ return this.oValidators[sExtension]?.()(oEntry, sFilePath) ? oEntry : void 0;
87
+ }
121
88
  };
122
-
123
- // ../core/loader/FileLoader.ts
124
- var FileLoader = class {
125
- /**
126
- * 1. Validates a `.prisma` file.
127
- * 2. Loads a generated Prisma client module (`client.ts`, or `client.js`)
128
- */
129
- static async findPrismaClient(clientPath, schemaPath) {
130
- const content = FileReader.loadFile(schemaPath).replace(/\/\/.*$/gm, "");
131
- if (!Validator.validateUnique(content, "prisma", schemaPath)) {
132
- throw new Error(`[nano-app] [ERROR] ${schemaPath} is not a valid Prisma schema.`);
133
- }
134
- try {
135
- return (await FileReader.loadModule(clientPath))?.PrismaClient;
136
- } catch (error) {
137
- throw new Error(`[nano-app] [ERROR] The Prisma client ${clientPath} could not be loaded: ${error?.message ?? error}`);
138
- }
139
- }
140
- /** Finds `*.<extension>.ts` (or built `.js`) files and returns `{ [subPath]: defaultExport }` */
141
- static async findMany({ extension, savePath, sourceDir, save = false }) {
142
- const srcDir = (sourceDir || `src/${extension}s`).replace(/^\.\//, "").replace(/\/$/, "");
143
- const filePaths = await glob(`${srcDir}/**/*.${extension}.{ts,js}`, { posix: true, ignore: "**/*.d.ts" });
144
- const entries = {};
145
- for (const filePath of filePaths) {
146
- const fileSubPath = filePath.slice(srcDir.length).replace(new RegExp(`\\.${extension}\\.(ts|js)$`), "");
147
- entries[fileSubPath] = await FileReader.loadModule(filePath, { default: true });
148
- }
149
- const valid = Validator.validate(entries, extension);
150
- if (save) {
151
- this.save(valid, savePath || resolve2(process.cwd(), "node_modules", "@nano-app", "data", `${extension}s.cjs`));
152
- }
153
- return valid;
154
- }
155
- /** Writes a `{ [filePath]: fileDefaultExport }` map to the given path. */
156
- static save(data, outPath) {
157
- const SERIALIZE = (value, indent = 0) => {
158
- const pad = " ".repeat(indent);
159
- const padInner = " ".repeat(indent + 1);
160
- if (value === null) return "null";
161
- if (value === void 0) return "undefined";
162
- if (typeof value === "function") return value.toString();
163
- if (typeof value === "string") return JSON.stringify(value);
164
- if (typeof value === "number" || typeof value === "boolean") return String(value);
165
- if (Array.isArray(value)) {
166
- if (value.length === 0) return "[]";
167
- const items = value.map((v) => `${padInner}${SERIALIZE(v, indent + 1)}`).join(",\n");
168
- return `[
169
- ${items}
170
- ${pad}]`;
171
- }
172
- if (typeof value === "object") {
173
- const entries = Object.entries(value);
174
- if (entries.length === 0) return "{}";
175
- const lines = entries.map(([k, v]) => `${padInner}${JSON.stringify(k)}: ${SERIALIZE(v, indent + 1)}`).join(",\n");
176
- return `{
177
- ${lines}
178
- ${pad}}`;
179
- }
180
- return String(value);
181
- };
182
- const GENERATE_EXPORT = (content) => `${'// Auto-generated by @nano-app.\n// Do not edit manually.\n"use strict";\nmodule.exports = {\n' + content + "\n};"}`;
183
- const output = GENERATE_EXPORT(
184
- Object.entries(data).map(([filePath, value]) => ` ${JSON.stringify(filePath)}: ${SERIALIZE(value, 1)}`).join(",\n")
185
- );
186
- mkdirSync(dirname(outPath), { recursive: true });
187
- writeFileSync(outPath, output, "utf-8");
188
- console.log(`[nano-app] File generated: ${outPath}`);
189
- }
89
+ //#endregion
90
+ //#region ../core/loader/FileReader.ts
91
+ /** Reads and loads files from disk, resolved from `cwd` */
92
+ var FileReader = class {
93
+ static oJiti = createJiti(import.meta.url, { nativeModules: ["@prisma/client"] });
94
+ /** Loads a module via jiti */
95
+ static async loadModule(sFilePath, oOptions = {}) {
96
+ return await this.oJiti.import(this.getAbsolutePath(sFilePath), oOptions);
97
+ }
98
+ /** Reads the contents of a file */
99
+ static loadFile(sFilePath) {
100
+ const sContent = readFileSync(this.getAbsolutePath(sFilePath), "utf-8");
101
+ if (!sContent) throw new Error(`[nano-app] [ERROR] The file ${sFilePath} does not have valid content.`);
102
+ return sContent;
103
+ }
104
+ /** Reads a Prisma schema without its `//` comments */
105
+ static loadSchema(sFilePath) {
106
+ return this.loadFile(sFilePath).replace(/\/\/.*$/gm, "");
107
+ }
108
+ /** Resolves the absolute path of a file */
109
+ static getAbsolutePath(sFilePath) {
110
+ const sAbsolutePath = isAbsolute(sFilePath) ? sFilePath : resolve(process.cwd(), sFilePath);
111
+ if (!existsSync(sAbsolutePath)) throw new Error(`[nano-app] [ERROR] ${sFilePath} not found.`);
112
+ return sAbsolutePath;
113
+ }
190
114
  };
191
-
192
- // ../core/utils/Formatter.ts
115
+ //#endregion
116
+ //#region ../core/utils/Formatter.ts
117
+ /** Formats values for logs and messages */
193
118
  var Formatter = class {
194
- static price(value) {
195
- return `$${new Intl.NumberFormat("de-DE", { maximumFractionDigits: 0 }).format(value || 0)}`;
196
- }
197
- static logDate(date = /* @__PURE__ */ new Date()) {
198
- return new Intl.DateTimeFormat("es-ES", {
199
- year: "2-digit",
200
- month: "2-digit",
201
- day: "2-digit",
202
- hour: "2-digit",
203
- minute: "2-digit",
204
- second: "2-digit"
205
- }).format(date);
206
- }
207
- // public static sanitizeAndParse(result: ApiResponse): ApiResponse {
208
- // const jsonString = JSON.stringify(result);
209
- // // The regular expression matches the key "password": "any_value"
210
- // // where:
211
- // // - "password" is wrapped in quotes.
212
- // // - Optional spaces may surround the colon.
213
- // // - The value is assumed to be wrapped in quotes.
214
- // const regex = /("password"\s*:\s*)"[^"]*"/gi;
215
- // // Replace the value with the JSON literal null (unquoted)
216
- // const sanitizedString = jsonString.replace(regex, '$1null');
217
- // return JSON.parse(sanitizedString);
218
- // }
119
+ /** Message of anything thrown: its `message` when it has one, else the value itself */
120
+ static error(oError) {
121
+ return String(oError?.message ?? oError);
122
+ }
123
+ /** Formats a date for the request log, in UTC: `2026-10-19 12:03:10` */
124
+ static logDate(oDate = /* @__PURE__ */ new Date()) {
125
+ return oDate.toISOString().slice(0, 19).replace("T", " ");
126
+ }
127
+ };
128
+ //#endregion
129
+ //#region ../core/loader/FileLoader.ts
130
+ /** Finds, loads and validates the consumer's files */
131
+ var FileLoader = class {
132
+ /**
133
+ * 1. Validates a `.prisma` file.
134
+ * 2. Loads a generated Prisma client module (`client.ts`, or `client.js`)
135
+ */
136
+ static async findPrismaClient(sClientPath, sSchemaPath) {
137
+ const sContent = FileReader.loadSchema(sSchemaPath);
138
+ if (!Validator.validateUnique(sContent, "prisma", sSchemaPath)) throw new Error(`[nano-app] [ERROR] ${sSchemaPath} is not a valid Prisma schema.`);
139
+ try {
140
+ return (await FileReader.loadModule(sClientPath))?.PrismaClient;
141
+ } catch (oError) {
142
+ throw new Error(`[nano-app] [ERROR] The Prisma client ${sClientPath} could not be loaded: ${Formatter.error(oError)}`);
143
+ }
144
+ }
145
+ /** Finds `*.route.ts` (or built `.js`) files and returns `{ [subPath]: defaultExport }` */
146
+ static async findMany({ extension, sourceDir }) {
147
+ const sSrcDir = (sourceDir ?? `src/${extension}s`).replace(/^\.\//, "").replace(/\/$/, "");
148
+ const aFilePaths = await glob(`${sSrcDir}/**/*.${extension}.{ts,js}`, {
149
+ posix: true,
150
+ ignore: "**/*.d.ts"
151
+ });
152
+ const oEntries = {};
153
+ for (const sFilePath of aFilePaths) {
154
+ const sFileSubPath = sFilePath.slice(sSrcDir.length).replace(new RegExp(`\\.${extension}\\.(ts|js)$`), "");
155
+ const sKey = extension === "store" ? sFileSubPath.slice(1).replaceAll("/", "_") : sFileSubPath;
156
+ oEntries[sKey] = await FileReader.loadModule(sFilePath, { default: true });
157
+ }
158
+ return Validator.validate(oEntries, extension);
159
+ }
219
160
  };
220
-
221
- // src/routes/Endpoint.ts
161
+ //#endregion
162
+ //#region src/routes/Endpoint.ts
163
+ /** Base class for all endpoints */
222
164
  var Endpoint = class {
223
- methods;
224
- callbacks;
225
- /** Allowed methods */
226
- constructor(methods, callbacks = {}) {
227
- this.methods = methods;
228
- this.callbacks = callbacks;
229
- }
230
- /** Handler for all methods */
231
- async mount(params) {
232
- await this.callbacks.onInit?.(params);
233
- return this.createHandler(params);
234
- }
235
- createHandler = (params) => (req, res, next) => {
236
- const method = req.method.toUpperCase();
237
- const fnMethod = this.methods[method];
238
- if (fnMethod) {
239
- (this.callbacks.onBefore?.(req, method, params) ?? Promise.resolve({})).then((onBeforeResult) => (
240
- // Runs the method
241
- fnMethod(req, { ...params, onBeforeResult }).then((response) => {
242
- res.status(response.success ? 200 : 400).json(response);
243
- })
244
- )).catch(next);
245
- } else {
246
- res.status(405).json({ success: false, message: `Method ${method} not implemented` });
247
- }
248
- };
165
+ oMethods;
166
+ oCallbacks;
167
+ /** Allowed methods */
168
+ constructor(oMethods, oCallbacks = {}) {
169
+ this.oMethods = oMethods;
170
+ this.oCallbacks = oCallbacks;
171
+ }
172
+ /** Handler for all methods */
173
+ async mount(oParams) {
174
+ await this.oCallbacks.onInit?.(oParams);
175
+ return this.createHandler(oParams);
176
+ }
177
+ /** Express handler: onBefore, then the method of the request */
178
+ createHandler(oParams) {
179
+ return (oReq, oRes, fnNext) => {
180
+ const sMethod = oReq.method.toUpperCase();
181
+ const fnMethod = this.oMethods[sMethod];
182
+ if (!fnMethod) return fnNext({
183
+ status: 405,
184
+ message: `Method ${sMethod} not implemented`
185
+ });
186
+ const oRequestParams = {
187
+ ...oParams,
188
+ session: oRes.locals.session
189
+ };
190
+ (this.oCallbacks.onBefore?.(oReq, sMethod, oRequestParams) ?? Promise.resolve({})).then((onBeforeResult) => fnMethod(oReq, {
191
+ ...oRequestParams,
192
+ onBeforeResult
193
+ })).then((oResponse) => {
194
+ oRes.status(oResponse.success ? 200 : 400).json(oResponse);
195
+ }).catch(fnNext);
196
+ };
197
+ }
249
198
  };
250
-
251
- // src/routes/Router.ts
199
+ //#endregion
200
+ //#region src/routes/Router.ts
201
+ /** Turns a folder of `*.route.ts` files into Express paths and endpoints */
252
202
  var Router = class {
253
- routes = {};
254
- routesDir;
255
- basePath;
256
- constructor(routesDir, basePath) {
257
- this.routesDir = routesDir;
258
- this.basePath = basePath ?? "";
259
- }
260
- async mount() {
261
- if (!!this.routesDir) {
262
- this.routes = await FileLoader.findMany({
263
- sourceDir: this.routesDir,
264
- extension: "route"
265
- });
266
- }
267
- return Object.entries(this.routes).map(([filePath, { expression, callbacks, ...methods }]) => {
268
- const path = this.createPath(filePath, expression);
269
- console.log(`[nano-app] [INFO] ${path}`);
270
- return { path, endpoint: new Endpoint(methods, callbacks) };
271
- });
272
- }
273
- /** `basePath` + file path (`/index` is the folder itself) + `expression`, as Express reads them */
274
- createPath(endpoint, expression = "") {
275
- return `${this.basePath}${endpoint.replace(/\/index$/, "")}${expression}` || "/";
276
- }
203
+ oRoutes = {};
204
+ sRoutesDir;
205
+ sBasePath;
206
+ constructor(sRoutesDir, sBasePath) {
207
+ this.sRoutesDir = sRoutesDir;
208
+ this.sBasePath = sBasePath ?? "";
209
+ }
210
+ /** Loads the routes and creates one endpoint per file */
211
+ async mount() {
212
+ if (this.sRoutesDir) this.oRoutes = await FileLoader.findMany({
213
+ sourceDir: this.sRoutesDir,
214
+ extension: "route"
215
+ });
216
+ return Object.entries(this.oRoutes).map(([sFilePath, { expression, callbacks, ...methods }]) => {
217
+ const sPath = this.createPath(sFilePath, expression);
218
+ console.log(`[nano-app] [INFO] ${sPath}`);
219
+ return {
220
+ path: sPath,
221
+ endpoint: new Endpoint(methods, callbacks)
222
+ };
223
+ });
224
+ }
225
+ /** `basePath` + file path (`/index` is the folder itself) + `expression`, as Express reads them */
226
+ createPath(sEndpoint, sExpression = "") {
227
+ return `${this.sBasePath}${sEndpoint.replace(/\/index$/, "")}${sExpression}` || "/";
228
+ }
277
229
  };
278
-
279
- // src/database/Database.ts
280
- import { existsSync as existsSync2 } from "fs";
281
-
282
- // const.ts
283
- import { dirname as dirname2, resolve as resolve3 } from "path";
284
- import { fileURLToPath } from "url";
285
- var BASE_DIR = dirname2(fileURLToPath(import.meta.url));
286
- var getDir = (...parts) => resolve3(BASE_DIR, ...parts);
287
-
288
- // src/database/Database.ts
230
+ //#endregion
231
+ //#region src/auth/BetterAuth.ts
232
+ /** Better Auth on the app's Prisma client: the instance, its handler and a session guard */
233
+ var BetterAuth = class {
234
+ /** Creates the instance on the app's client, its handler and the guard */
235
+ static async create(oOptions, oDb) {
236
+ const { betterAuth } = await import("better-auth");
237
+ const { prismaAdapter } = await import("better-auth/adapters/prisma");
238
+ const { getSchema } = await import("better-auth/db");
239
+ const { fromNodeHeaders, toNodeHandler } = await import("better-auth/node");
240
+ const aMissing = Object.keys(getSchema(oOptions)).filter((sModel) => !oDb[sModel]);
241
+ if (aMissing.length) throw new Error(`[nano-app] [ERROR] Better Auth needs the models ${aMissing.join(", ")} in the app's schema. Run \`npx auth generate\` and apply your schema with Prisma.`);
242
+ const oAuth = betterAuth({
243
+ ...oOptions,
244
+ database: prismaAdapter(oDb, { provider: "postgresql" })
245
+ });
246
+ const fnGuard = (oReq, oRes, fnNext) => {
247
+ oAuth.api.getSession({ headers: fromNodeHeaders(oReq.headers) }).then((oSession) => {
248
+ if (!oSession) return fnNext({
249
+ status: 401,
250
+ message: "Not authenticated"
251
+ });
252
+ oRes.locals.session = oSession;
253
+ fnNext();
254
+ }).catch(fnNext);
255
+ };
256
+ return {
257
+ oHandler: toNodeHandler(oAuth),
258
+ fnGuard,
259
+ sPath: `${oOptions.basePath ?? "/api/auth"}/{*path}`
260
+ };
261
+ }
262
+ };
263
+ //#endregion
264
+ //#region src/database/Prisma.ts
265
+ /** Runs the consumer's Prisma CLI: generates clients and applies official plugins' migrations */
266
+ var Prisma = class {
267
+ /** Folder of a client and its files, in the project */
268
+ static dir(sName) {
269
+ return join(process.cwd(), ".nano-app", "prisma-clients", sName);
270
+ }
271
+ /** Generates the client of a schema in `.nano-app/prisma-clients/<name>`, unless it is up to date */
272
+ static generate(sName, sSchemaPath) {
273
+ const sDir = this.dir(sName);
274
+ const sSource = FileReader.getAbsolutePath(sSchemaPath);
275
+ const sHash = createHash("sha256").update(readFileSync(sSource)).digest("hex");
276
+ const sSourceFile = join(sDir, "source.json");
277
+ const oCurrent = existsSync(sSourceFile) ? JSON.parse(readFileSync(sSourceFile, "utf-8")) : void 0;
278
+ if (oCurrent && oCurrent.schemaPath !== sSource) console.warn(`[nano-app] [WARNING] .nano-app/prisma-clients/${sName} has the client of ${oCurrent.schemaPath}: it is replaced by the one of ${sSource}.`);
279
+ if (oCurrent?.schemaPath === sSource && oCurrent.hash === sHash && existsSync(join(sDir, "generated", "client.ts"))) return;
280
+ mkdirSync(sDir, { recursive: true });
281
+ const sSchema = readFileSync(sSource, "utf-8");
282
+ writeFileSync(join(sDir, "schema.prisma"), sSchema.replace(/output\s*=\s*"[^"]*"/, "output = \"./generated\""));
283
+ this.run(sName, [
284
+ "generate",
285
+ "--schema",
286
+ join(sDir, "schema.prisma")
287
+ ]);
288
+ writeFileSync(sSourceFile, JSON.stringify({
289
+ schemaPath: sSource,
290
+ hash: sHash
291
+ }));
292
+ console.log(`[nano-app] [INFO] Prisma client "${sName}" generated`);
293
+ }
294
+ /** Applies the pending migrations an official plugin ships next to its schema */
295
+ static migrate(sName, sSchemaPath, sDbSchema) {
296
+ const sMigrations = join(dirname(FileReader.getAbsolutePath(sSchemaPath)), "migrations");
297
+ if (!existsSync(sMigrations)) throw new Error(`[nano-app] [ERROR] The plugin "${sName}" ships no migrations: ${sMigrations}`);
298
+ if (!process.env.DATABASE_URL) throw new Error(MISSING_DATABASE_URL);
299
+ const oUrl = new URL(process.env.DATABASE_URL);
300
+ oUrl.searchParams.set("schema", sDbSchema);
301
+ const sConfig = join(this.dir(sName), "prisma.config.mjs");
302
+ writeFileSync(sConfig, [
303
+ "export default {",
304
+ ` schema: ${JSON.stringify(join(this.dir(sName), "schema.prisma"))},`,
305
+ ` migrations: { path: ${JSON.stringify(sMigrations)} },`,
306
+ " datasource: { url: process.env.NANO_APP_MIGRATION_URL },",
307
+ "};",
308
+ ""
309
+ ].join("\n"));
310
+ this.run(sName, [
311
+ "migrate",
312
+ "deploy",
313
+ "--config",
314
+ sConfig
315
+ ], { NANO_APP_MIGRATION_URL: oUrl.toString() });
316
+ }
317
+ /** Runs a Prisma command of the consumer's `prisma` package */
318
+ static run(sName, aArgs, oEnv = {}) {
319
+ let sCli;
320
+ try {
321
+ sCli = createRequire(`${process.cwd()}/`).resolve("prisma/build/index.js");
322
+ } catch {
323
+ throw new Error(`[nano-app] [ERROR] The database "${sName}" needs Prisma: install the prisma package.`);
324
+ }
325
+ try {
326
+ execFileSync(process.execPath, [sCli, ...aArgs], {
327
+ env: {
328
+ ...process.env,
329
+ ...oEnv
330
+ },
331
+ stdio: "pipe"
332
+ });
333
+ } catch (oError) {
334
+ const oFailed = oError;
335
+ const sOutput = `${oFailed.stderr ?? ""}${oFailed.stdout ?? ""}`.trim() || Formatter.error(oError);
336
+ throw new Error(`[nano-app] [ERROR] prisma ${aArgs[0]} failed for "${sName}": ${sOutput}`);
337
+ }
338
+ }
339
+ };
340
+ //#endregion
341
+ //#region src/database/Database.ts
342
+ /** Prisma clients of the app and its plugins, one per name */
289
343
  var Database = class {
290
- static adapter;
291
- /** Mounted clients, by name */
292
- static clients = {};
293
- /** Initializes the Database client */
294
- static async init() {
295
- const url = process.env.DATABASE_URL;
296
- if (!url) {
297
- throw new Error("[nano-app] [ERROR] The DATABASE_URL environment variable is not defined.");
298
- }
299
- const { PrismaPg } = await import("@prisma/adapter-pg");
300
- this.adapter = new PrismaPg({ connectionString: url });
301
- }
302
- /** Gets a client by name */
303
- static get(name) {
304
- return this.clients[name];
305
- }
306
- /** Mounts a generated Prisma client */
307
- static async mount(name, { schemaPath }) {
308
- if (this.get(name)) {
309
- throw new Error(`[nano-app] [ERROR] The database "${name}" is already mounted.`);
310
- }
311
- const clientPath = `${process.cwd()}/.nano-app/prisma-clients/${name}/generated/client.ts`;
312
- if (!existsSync2(clientPath)) {
313
- throw new Error(`[nano-app] [ERROR] The prisma client of "${name}" does not exist. Run \`npx nano-app generate ${name}\`.`);
314
- }
315
- if (!this.adapter) await this.init();
316
- const PrismaClient = await FileLoader.findPrismaClient(clientPath, schemaPath);
317
- const isPlugin = schemaPath.startsWith(getDir("plugins"));
318
- const declared = FileReader.loadFile(schemaPath).replace(/\/\/.*$/gm, "").match(/datasource\s+\w+\s*\{[^}]*schemas\s*=\s*\[([^\]]*)\]/s)?.[1] ?? "";
319
- const schemas = [...declared.matchAll(/["']([^"']+)["']/g)].map(([, schema]) => schema);
320
- if (isPlugin ? !schemas.length || schemas.some((schema) => !schema.startsWith("nano_")) : schemas.some((schema) => schema.startsWith("nano_"))) {
321
- throw new Error(`[nano-app] [ERROR] ${schemaPath}: ${isPlugin ? "a plugin declares only nano_* schemas" : "the nano_* schemas are reserved for plugins"}.`);
322
- }
323
- let client;
324
- try {
325
- client = new PrismaClient({ adapter: this.adapter });
326
- } catch (error) {
327
- throw new Error(`[nano-app] [ERROR] Cannot instantiate the database "${name}" from ${clientPath}: ${error?.message ?? error}`);
328
- }
329
- if (isPlugin) {
330
- for (const [, model] of FileReader.loadFile(schemaPath).matchAll(/^\s*model\s+(\w+)/gm)) {
331
- await client[model[0].toLowerCase() + model.slice(1)].findFirst().catch((error) => {
332
- throw new Error(`[nano-app] [ERROR] The database "${name}" is not ready: ${String(error?.message ?? error).trim().split("\n").pop()} Run \`npx nano-app push ${name}\`.`);
333
- });
334
- }
335
- }
336
- this.clients[name] = client;
337
- console.log(`[nano-app] [INFO] Database "${name}" mounted`);
338
- return client;
339
- }
344
+ static oAdapter;
345
+ /** Mounted clients, by name */
346
+ static oClients = {};
347
+ /** Initializes the Database client */
348
+ static async init() {
349
+ const sUrl = process.env.DATABASE_URL;
350
+ if (!sUrl) throw new Error(MISSING_DATABASE_URL);
351
+ const { PrismaPg } = await import("@prisma/adapter-pg");
352
+ this.oAdapter = new PrismaPg({ connectionString: sUrl });
353
+ }
354
+ /** Gets a client by name */
355
+ static get(sName) {
356
+ return this.oClients[sName];
357
+ }
358
+ /** Generates and mounts the Prisma client of a schema; an official plugin's is also migrated */
359
+ static async mount(sName, { schemaPath }) {
360
+ if (this.get(sName)) throw new Error(`[nano-app] [ERROR] The database "${sName}" is already mounted.`);
361
+ const isPlugin = this.isOfficial(schemaPath);
362
+ const aSchemas = this.declaredSchemas(schemaPath);
363
+ const isPluginInvalid = !aSchemas.length || aSchemas.some((sItem) => !sItem.startsWith("nano_"));
364
+ const isConsumerInvalid = aSchemas.some((sItem) => sItem.startsWith("nano_"));
365
+ if (isPlugin ? isPluginInvalid : isConsumerInvalid) throw new Error(`[nano-app] [ERROR] ${schemaPath}: ${isPlugin ? "a plugin declares only nano_* schemas" : "the nano_* schemas are reserved for plugins"}.`);
366
+ Prisma.generate(sName, schemaPath);
367
+ if (isPlugin) Prisma.migrate(sName, schemaPath, aSchemas[0]);
368
+ if (!this.oAdapter) await this.init();
369
+ const sClientPath = join(Prisma.dir(sName), "generated", "client.ts");
370
+ const PrismaClient = await FileLoader.findPrismaClient(sClientPath, schemaPath);
371
+ let oClient;
372
+ try {
373
+ oClient = new PrismaClient({ adapter: this.oAdapter });
374
+ } catch (oError) {
375
+ throw new Error(`[nano-app] [ERROR] Cannot instantiate the database "${sName}" from ${sClientPath}: ${Formatter.error(oError)}`);
376
+ }
377
+ const sFix = isPlugin ? "Update the plugin or report it." : "Apply your schema with Prisma (db push or migrations).";
378
+ for (const [, sModel] of FileReader.loadSchema(schemaPath).matchAll(/^\s*model\s+(\w+)/gm)) await oClient[`${sModel[0].toLowerCase()}${sModel.slice(1)}`].findFirst().catch((oError) => {
379
+ const sReason = Formatter.error(oError).trim().split("\n").pop();
380
+ throw new Error(`[nano-app] [ERROR] The database "${sName}" is not ready: ${sReason} ${sFix}`);
381
+ });
382
+ this.oClients[sName] = oClient;
383
+ console.log(`[nano-app] [INFO] Database "${sName}" mounted`);
384
+ return oClient;
385
+ }
386
+ /** Schemas a Prisma schema declares in its datasource (`schemas = [...]`) */
387
+ static declaredSchemas(sSchemaPath) {
388
+ return [...(FileReader.loadSchema(sSchemaPath).match(/datasource\s+\w+\s*\{[^}]*schemas\s*=\s*\[([^\]]*)\]/s)?.[1] ?? "").matchAll(/["']([^"']+)["']/g)].map(([, sItem]) => sItem);
389
+ }
390
+ /** Whether a schema ships with the official plugins: its nearest package.json is theirs */
391
+ static isOfficial(sSchemaPath) {
392
+ let sDir = dirname(FileReader.getAbsolutePath(sSchemaPath));
393
+ while (!existsSync(join(sDir, "package.json"))) {
394
+ if (dirname(sDir) === sDir) return false;
395
+ sDir = dirname(sDir);
396
+ }
397
+ const { name } = JSON.parse(readFileSync(join(sDir, "package.json"), "utf-8"));
398
+ return name === OFFICIAL_PLUGINS;
399
+ }
340
400
  };
341
-
342
- // src/routes/Plugin.ts
401
+ //#endregion
402
+ //#region src/routes/Plugin.ts
403
+ /** A group of routes, with its options, callbacks and database */
343
404
  var Plugin = class {
344
- config;
345
- /** Creates a new Plugin instance */
346
- constructor(config) {
347
- this.config = config;
348
- }
349
- /** Mounts its endpoints on express app */
350
- async mount(app) {
351
- const { name, routesDir, basePath, database, options, callbacks } = this.config;
352
- const router = new Router(routesDir, basePath);
353
- const db = database?.enabled ? await Database.mount(name, database) : void 0;
354
- const params = { ...options, db };
355
- await callbacks?.onMount?.(app, params);
356
- for (const { path, endpoint } of await router.mount()) app.all(path, await endpoint.mount(params));
357
- }
405
+ oConfig;
406
+ oAuthOptions;
407
+ /** Creates a new Plugin instance; the app's plugin also gets the Better Auth options */
408
+ constructor(oConfig, oAuthOptions) {
409
+ this.oConfig = oConfig;
410
+ this.oAuthOptions = oAuthOptions;
411
+ }
412
+ /** Mounts its endpoints on express app */
413
+ async mount(oApp) {
414
+ const { name, routesDir, basePath, database, options, callbacks } = this.oConfig;
415
+ const oDb = database?.enabled ? await Database.mount(name, database) : void 0;
416
+ const oParams = {
417
+ ...options,
418
+ db: oDb
419
+ };
420
+ if (this.oAuthOptions) {
421
+ if (!oDb) throw new Error("[nano-app] [ERROR] packages.betterAuth needs the database of the app: set mainPlugin.database.enabled to true.");
422
+ const { sPath, oHandler, fnGuard } = await BetterAuth.create(this.oAuthOptions, oDb);
423
+ oApp.all(sPath, oHandler);
424
+ oApp.use(fnGuard);
425
+ }
426
+ await callbacks?.onMount?.(oApp, oParams);
427
+ const oRouter = new Router(routesDir, basePath);
428
+ for (const { path, endpoint } of await oRouter.mount()) oApp.all(path, await endpoint.mount(oParams));
429
+ }
358
430
  };
359
-
360
- // src/app/NanoApp.ts
361
- import morgan from "morgan";
362
- import cors from "cors";
363
- import helmet from "helmet";
364
- import compression from "compression";
365
- import rateLimit from "express-rate-limit";
366
- import "dotenv/config";
367
-
368
- // src/app/config.ts
369
- var INITIAL_CONFIG = {
370
- /** Initial application configuration */
371
- port: 3e3,
372
- /** Application listener */
373
- listener: () => {
374
- },
375
- /** Express settings */
376
- settings: {},
377
- /** Initial middlewares */
378
- packages: {
379
- morgan: {
380
- tokens: {
381
- timestamp: () => Formatter.logDate()
382
- },
383
- format: ":timestamp | :method :url :status"
384
- },
385
- cors: {},
386
- helmet: true,
387
- compression: true,
388
- rateLimit: {
389
- max: 1e3,
390
- message: "Too many requests, please try again later."
391
- },
392
- express: {
393
- limit: "10kb"
394
- },
395
- urlencoded: {
396
- extended: false
397
- }
398
- },
399
- /** Main plugin to mount */
400
- mainPlugin: {
401
- routesDir: "src/routes",
402
- basePath: "",
403
- database: {
404
- enabled: false,
405
- schemaPath: "src/prisma/schema.prisma"
406
- }
407
- },
408
- /** Plugins around the consumer routes */
409
- plugins: []
431
+ //#endregion
432
+ //#region src/app/config.ts
433
+ /** Initial application configuration */
434
+ const INITIAL_CONFIG = {
435
+ /** Initial application configuration */
436
+ port: 3e3,
437
+ /** Application listener */
438
+ listener: () => {},
439
+ /** Express settings */
440
+ settings: {},
441
+ /** Initial middlewares */
442
+ packages: {
443
+ morgan: {
444
+ tokens: { timestamp: () => Formatter.logDate() },
445
+ format: ":timestamp | :method :url :status"
446
+ },
447
+ cors: { methods: [
448
+ "GET",
449
+ "HEAD",
450
+ "PUT",
451
+ "POST",
452
+ "DELETE",
453
+ "QUERY"
454
+ ] },
455
+ helmet: true,
456
+ compression: true,
457
+ rateLimit: {
458
+ max: 1e3,
459
+ message: "Too many requests, please try again later."
460
+ },
461
+ express: { limit: "10kb" },
462
+ urlencoded: { extended: false },
463
+ betterAuth: false
464
+ },
465
+ /** Main plugin to mount */
466
+ mainPlugin: {
467
+ routesDir: "src/routes",
468
+ basePath: "",
469
+ database: {
470
+ enabled: false,
471
+ schemaPath: "src/prisma/schema.prisma"
472
+ }
473
+ },
474
+ /** Plugins around the consumer routes */
475
+ plugins: []
410
476
  };
411
- function mergeConfig(config = {}) {
412
- return {
413
- ...INITIAL_CONFIG,
414
- ...config,
415
- settings: config.settings ?? INITIAL_CONFIG.settings,
416
- packages: {
417
- ...INITIAL_CONFIG.packages,
418
- ...config.packages || {},
419
- morgan: config.packages?.morgan ?? INITIAL_CONFIG.packages.morgan,
420
- cors: config.packages?.cors ?? INITIAL_CONFIG.packages.cors,
421
- helmet: config.packages?.helmet ?? INITIAL_CONFIG.packages.helmet,
422
- compression: config.packages?.compression ?? INITIAL_CONFIG.packages.compression,
423
- rateLimit: config.packages?.rateLimit ?? INITIAL_CONFIG.packages.rateLimit,
424
- express: config.packages?.express ?? INITIAL_CONFIG.packages.express,
425
- urlencoded: config.packages?.urlencoded ?? INITIAL_CONFIG.packages.urlencoded
426
- },
427
- mainPlugin: {
428
- ...INITIAL_CONFIG.mainPlugin,
429
- ...config.mainPlugin,
430
- name: "app",
431
- database: {
432
- ...INITIAL_CONFIG.mainPlugin.database,
433
- ...config.mainPlugin?.database
434
- }
435
- },
436
- plugins: config.plugins ?? INITIAL_CONFIG.plugins
437
- };
477
+ /** Whether a value is a plain object (`{}`), the only kind merged field by field */
478
+ function isPlainObject(oValue) {
479
+ return Object.prototype.toString.call(oValue) === "[object Object]";
480
+ }
481
+ /** Merges a value over its default: plain objects key by key at every level; anything else replaces it */
482
+ function deepMerge(oDefault, oValue) {
483
+ if (oValue === void 0) return oDefault;
484
+ if (!isPlainObject(oDefault) || !isPlainObject(oValue)) return oValue;
485
+ const aKeys = Object.keys({
486
+ ...oDefault,
487
+ ...oValue
488
+ });
489
+ return Object.fromEntries(aKeys.map((sKey) => [sKey, deepMerge(oDefault[sKey], oValue[sKey])]));
438
490
  }
439
-
440
- // src/app/errors.ts
441
- function normalize(error) {
442
- if (typeof error === "string") {
443
- return { status: 400, body: { success: false, message: error } };
444
- }
445
- const { status, message, warning } = error ?? {};
446
- return {
447
- status: status ?? 500,
448
- body: {
449
- success: false,
450
- warning: warning ?? false,
451
- message: message || "Internal server error"
452
- }
453
- };
491
+ /** Merges the consumer configuration over the initial one; the main plugin is always named "app" */
492
+ function mergeConfig(oConfig = {}) {
493
+ const oMerged = deepMerge(INITIAL_CONFIG, oConfig);
494
+ return {
495
+ ...oMerged,
496
+ mainPlugin: {
497
+ ...oMerged.mainPlugin,
498
+ name: "app"
499
+ }
500
+ };
454
501
  }
455
- var notFoundHandler = (req, _res, next) => {
456
- next({ status: 404, message: `${req.path} not found` });
502
+ //#endregion
503
+ //#region src/app/errors.ts
504
+ /** Normalizes anything thrown by a handler into an ApiResponse plus its status */
505
+ function normalize(oError) {
506
+ if (typeof oError === "string") return {
507
+ status: 400,
508
+ body: {
509
+ success: false,
510
+ message: oError
511
+ }
512
+ };
513
+ const { status, message, warning } = oError ?? {};
514
+ return {
515
+ status: status ?? 500,
516
+ body: {
517
+ success: false,
518
+ warning: warning ?? false,
519
+ message: message || "Internal server error"
520
+ }
521
+ };
522
+ }
523
+ /** Turns every unmatched request into a 404 for the error handler */
524
+ const notFoundHandler = (oReq, _oRes, fnNext) => {
525
+ fnNext({
526
+ status: 404,
527
+ message: `${oReq.path} not found`
528
+ });
457
529
  };
458
- var errorHandler = (error, _req, res, _next) => {
459
- const { status, body } = normalize(error);
460
- if (status >= 500) {
461
- console.error("[nano-app] [ERROR]", error);
462
- }
463
- res.status(status).json(body);
530
+ /** Turns any error raised by a handler into an ApiResponse */
531
+ const errorHandler = (oError, _oReq, oRes, _fnNext) => {
532
+ const { status, body } = normalize(oError);
533
+ if (status >= 500) console.error("[nano-app] [ERROR]", oError);
534
+ oRes.status(status).json(body);
464
535
  };
465
-
466
- // src/app/NanoApp.ts
536
+ //#endregion
537
+ //#region src/app/NanoApp.ts
538
+ /** Main NanoApp class */
467
539
  var NanoApp = class {
468
- app;
469
- config;
470
- constructor(config = {}) {
471
- this.app = express();
472
- this.config = mergeConfig(config);
473
- this.start();
474
- }
475
- /** Starts the application */
476
- async start() {
477
- try {
478
- await this.initialize();
479
- this.app.listen(Number(this.config.port), () => {
480
- this.config.listener?.();
481
- console.log(`
482
- [nano-app] [INFO] Nano App running on port ${this.config.port}
483
- `);
484
- });
485
- return this;
486
- } catch (error) {
487
- console.error("[nano-app] [ERROR] Error trying to start the server.", error);
488
- process.exit(1);
489
- }
490
- }
491
- /** Initializes the application in the right order */
492
- async initialize() {
493
- Object.entries(this.config.settings ?? {}).forEach(([key, value]) => this.app.set(key, value));
494
- this.setPackages(this.config.packages);
495
- const oPlugins = { before: [], after: [] };
496
- this.config.plugins?.forEach((plugin) => {
497
- oPlugins[plugin.stage ?? "before"].push(plugin);
498
- });
499
- for (const plugin of oPlugins.before) await new Plugin(plugin).mount(this.app);
500
- await new Plugin(this.config.mainPlugin).mount(this.app);
501
- for (const plugin of oPlugins.after) await new Plugin(plugin).mount(this.app);
502
- this.app.use(notFoundHandler);
503
- this.app.use(errorHandler);
504
- }
505
- /* Adds the packages according to the configuration */
506
- setPackages(config = {}) {
507
- if (config.morgan) {
508
- Object.entries(config.morgan.tokens ?? {}).forEach(([key, value]) => morgan.token(key, value));
509
- this.app.use(morgan(config.morgan.format));
510
- }
511
- if (config.cors) {
512
- this.app.use(cors(config.cors));
513
- }
514
- if (config.helmet) {
515
- this.app.use(helmet());
516
- }
517
- if (config.compression) {
518
- this.app.use(compression());
519
- }
520
- if (config.rateLimit) {
521
- this.app.use(rateLimit(config.rateLimit));
522
- }
523
- if (config.express) {
524
- this.app.use(express.json(config.express));
525
- }
526
- if (config.urlencoded) {
527
- this.app.use(express.urlencoded(config.urlencoded));
528
- }
529
- }
540
+ oApp;
541
+ oConfig;
542
+ constructor(oConfig = {}) {
543
+ this.oApp = express();
544
+ this.oConfig = mergeConfig(oConfig);
545
+ this.start();
546
+ }
547
+ /** Starts the application */
548
+ async start() {
549
+ try {
550
+ await this.initialize();
551
+ this.oApp.listen(Number(this.oConfig.port), () => {
552
+ this.oConfig.listener?.();
553
+ console.log(`\n[nano-app] [INFO] Nano App running on port ${this.oConfig.port}\n`);
554
+ });
555
+ return this;
556
+ } catch (oError) {
557
+ console.error("[nano-app] [ERROR] Error trying to start the server.", oError);
558
+ process.exit(1);
559
+ }
560
+ }
561
+ /** Initializes the application in the right order */
562
+ async initialize() {
563
+ for (const [sKey, oValue] of Object.entries(this.oConfig.settings)) this.oApp.set(sKey, oValue);
564
+ this.setPackages(this.oConfig.packages);
565
+ const oPlugins = {
566
+ before: [],
567
+ after: []
568
+ };
569
+ for (const oPlugin of this.oConfig.plugins) oPlugins[oPlugin.stage ?? "before"].push(oPlugin);
570
+ for (const oPlugin of oPlugins.before) await new Plugin(oPlugin).mount(this.oApp);
571
+ const { mainPlugin, packages } = this.oConfig;
572
+ await new Plugin(mainPlugin, packages.betterAuth).mount(this.oApp);
573
+ for (const oPlugin of oPlugins.after) await new Plugin(oPlugin).mount(this.oApp);
574
+ this.oApp.use(notFoundHandler);
575
+ this.oApp.use(errorHandler);
576
+ }
577
+ /** Adds the packages according to the configuration */
578
+ setPackages(oConfig = {}) {
579
+ if (oConfig.morgan) {
580
+ for (const [sKey, fnToken] of Object.entries(oConfig.morgan.tokens ?? {})) morgan.token(sKey, fnToken);
581
+ this.oApp.use(morgan(oConfig.morgan.format));
582
+ }
583
+ if (oConfig.cors) this.oApp.use(cors(oConfig.cors));
584
+ if (oConfig.helmet) this.oApp.use(helmet());
585
+ if (oConfig.compression) this.oApp.use(compression());
586
+ if (oConfig.rateLimit) this.oApp.use(rateLimit(oConfig.rateLimit));
587
+ if (oConfig.express) this.oApp.use(express.json(oConfig.express));
588
+ if (oConfig.urlencoded) this.oApp.use(express.urlencoded(oConfig.urlencoded));
589
+ }
530
590
  };
531
-
532
- // src/app/creators.ts
533
- function createNanoApp(config) {
534
- return new NanoApp(config);
591
+ //#endregion
592
+ //#region src/app/creators.ts
593
+ /** Creates a new NanoApp instance */
594
+ function createNanoApp(oConfig) {
595
+ return new NanoApp(oConfig);
535
596
  }
536
- function createRoute(route) {
537
- return route;
597
+ /** Creates and types a route object. */
598
+ function createRoute(oRoute) {
599
+ return oRoute;
538
600
  }
539
- function createPlugin(creator) {
540
- return creator;
601
+ function createPlugin(fnCreator) {
602
+ return fnCreator;
541
603
  }
542
- function createPluginCallbacks(callbacks) {
543
- return callbacks;
604
+ /** Creates and types the callbacks of a plugin. */
605
+ function createPluginCallbacks(oCallbacks) {
606
+ return oCallbacks;
544
607
  }
545
- export {
546
- NanoApp,
547
- createNanoApp,
548
- createPlugin,
549
- createPluginCallbacks,
550
- createRoute
551
- };
608
+ //#endregion
609
+ export { NanoApp, createNanoApp, createPlugin, createPluginCallbacks, createRoute };