@nano-app/backend 0.14.0 → 0.15.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 +4 -2
- package/build/index.d.ts +9 -1
- package/build/index.js +211 -218
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ type ApiResponse<T> = {
|
|
|
72
72
|
|
|
73
73
|
- 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.
|
|
74
74
|
- Undeclared methods respond `405`.
|
|
75
|
-
- An invalid file
|
|
75
|
+
- An invalid file stops the startup with its path and what is wrong.
|
|
76
76
|
|
|
77
77
|
## Configuration
|
|
78
78
|
|
|
@@ -89,7 +89,9 @@ Every option is optional.
|
|
|
89
89
|
|
|
90
90
|
`packages` accepts `morgan`, `cors`, `helmet`, `compression`, `express` (`express.json` options) and `urlencoded`. The merge is **per section**: passing one key keeps the others; pass `false` to disable one.
|
|
91
91
|
|
|
92
|
-
`
|
|
92
|
+
`compression` is off by default: the proxy in front of the app (an ingress, nginx) usually compresses the answers. Turn it on with `packages: { compression: true }` when nothing does.
|
|
93
|
+
|
|
94
|
+
`morgan` writes a line per request to stdout, synchronously; with the access log of the proxy in front, `packages: { morgan: false }` saves that cost. It logs `:timestamp | :method :path :status` by default: `:path` is the path without its query string, so tokens sent there stay out of the logs. Set `format` with `:url` to log the full URL.
|
|
93
95
|
|
|
94
96
|
```ts
|
|
95
97
|
createNanoApp({
|
package/build/index.d.ts
CHANGED
|
@@ -79,6 +79,14 @@ type RouteEntry<Deps = NanoObject<any>> = {
|
|
|
79
79
|
} & Handlers<Deps>;
|
|
80
80
|
/** Generic object */
|
|
81
81
|
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
|
+
declare class NanoError extends Error {
|
|
84
|
+
status?: number;
|
|
85
|
+
/** Error with its message, and the status a handler answers (500 without one) */
|
|
86
|
+
constructor(sMessage: string, iStatus?: number);
|
|
87
|
+
/** Prefix of the framework's messages, shown before the message */
|
|
88
|
+
get name(): string;
|
|
89
|
+
}
|
|
82
90
|
/** Database client configuration. */
|
|
83
91
|
type DatabaseConfig = {
|
|
84
92
|
/** Whether the database is enabled. Defaults to false */
|
|
@@ -185,4 +193,4 @@ export declare function createPlugin<Options>(fnCreator: Creator<Options>): Crea
|
|
|
185
193
|
export declare function createPluginCallbacks<Deps = NanoObject<any>>(oCallbacks: PluginCallbacks<Deps>): PluginCallbacks<Deps>;
|
|
186
194
|
/** Whether the session's user has the permissions; always without Better Auth or permissions */
|
|
187
195
|
declare function isGranted({ auth, session }: Params, oPermissions?: NanoObject<string[]>): Promise<boolean>;
|
|
188
|
-
export { type ApiPromise, type ApiResponse, type Callbacks, type DatabaseConfig, type Handler, type Handlers, type MainPluginConfig, type MethodEntry, NanoApp, type NanoAppConfig, type NanoObject, type PackagesConfig, type Params, type PluginCallbacks, type PluginConfig, type PluginStage, type QueryParams, type Request, type RouteEntry, isGranted };
|
|
196
|
+
export { type ApiPromise, type ApiResponse, type Callbacks, type DatabaseConfig, type Handler, type Handlers, type MainPluginConfig, type MethodEntry, NanoApp, type NanoAppConfig, NanoError, type NanoObject, type PackagesConfig, type Params, type PluginCallbacks, type PluginConfig, type PluginStage, type QueryParams, type Request, type RouteEntry, isGranted };
|
package/build/index.js
CHANGED
|
@@ -5,12 +5,83 @@ import cors from "cors";
|
|
|
5
5
|
import helmet from "helmet";
|
|
6
6
|
import compression from "compression";
|
|
7
7
|
import "dotenv/config";
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
8
|
import { glob } from "glob";
|
|
10
9
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { createJiti } from "jiti";
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
13
|
import { createHash } from "node:crypto";
|
|
14
|
+
// ../core/utils/NanoError.ts
|
|
15
|
+
/** Error of the framework: Node shows it as `[nano-app] [ERROR]: <message>`, and a handler answers its status */
|
|
16
|
+
var NanoError = class extends Error {
|
|
17
|
+
status;
|
|
18
|
+
/** Error with its message, and the status a handler answers (500 without one) */
|
|
19
|
+
constructor(sMessage, iStatus) {
|
|
20
|
+
super(sMessage);
|
|
21
|
+
this.status = iStatus;
|
|
22
|
+
}
|
|
23
|
+
/** Prefix of the framework's messages, shown before the message */
|
|
24
|
+
get name() {
|
|
25
|
+
return "[nano-app] [ERROR]";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
// ../core/files/FileReader.ts
|
|
29
|
+
/** Reads and loads files from disk, resolved from `cwd` */
|
|
30
|
+
var FileReader = class FileReader {
|
|
31
|
+
/** Loader of the consumer's modules; the Prisma runtime is loaded natively */
|
|
32
|
+
static oJiti = createJiti(import.meta.url, { nativeModules: ["@prisma/client"] });
|
|
33
|
+
/** Loads a module via jiti */
|
|
34
|
+
static async loadModule(sFilePath, oOptions = {}) {
|
|
35
|
+
return await this.oJiti.import(this.getAbsolutePath(sFilePath), oOptions);
|
|
36
|
+
}
|
|
37
|
+
/** Reads the contents of a file */
|
|
38
|
+
static loadFile(sFilePath) {
|
|
39
|
+
const sContent = readFileSync(this.getAbsolutePath(sFilePath), "utf-8");
|
|
40
|
+
if (!sContent) throw new NanoError(`The file ${sFilePath} does not have valid content.`);
|
|
41
|
+
return sContent;
|
|
42
|
+
}
|
|
43
|
+
/** Reads a Prisma schema without its `//` comments */
|
|
44
|
+
static loadSchema(sFilePath) {
|
|
45
|
+
return this.loadFile(sFilePath).replace(/\/\/.*$/gm, "");
|
|
46
|
+
}
|
|
47
|
+
/** Body of the `datasource` block of a Prisma schema, without its braces */
|
|
48
|
+
static datasource(sContent) {
|
|
49
|
+
return sContent.match(/datasource\s+\w+\s*\{([^}]*)\}/)?.[1] ?? "";
|
|
50
|
+
}
|
|
51
|
+
/** Schemas a Prisma schema declares in its datasource (`schemas = [...]`) */
|
|
52
|
+
static schemas(sContent) {
|
|
53
|
+
return [...(FileReader.datasource(sContent).match(/\bschemas\s*=\s*\[([^\]]*)\]/)?.[1] ?? "").matchAll(/["']([^"']+)["']/g)].map(([, sItem]) => sItem);
|
|
54
|
+
}
|
|
55
|
+
/** Resolves the absolute path of a file */
|
|
56
|
+
static getAbsolutePath(sFilePath) {
|
|
57
|
+
const sAbsolutePath = isAbsolute(sFilePath) ? sFilePath : resolve(process.cwd(), sFilePath);
|
|
58
|
+
if (!existsSync(sAbsolutePath)) throw new NanoError(`${sFilePath} not found.`);
|
|
59
|
+
return sAbsolutePath;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
// ../core/files/FileLoader.ts
|
|
63
|
+
/** Finds and loads the consumer's files */
|
|
64
|
+
var FileLoader = class {
|
|
65
|
+
/** Loads a generated Prisma client module (`client.ts`, or `client.js`) */
|
|
66
|
+
static async findPrismaClient(sClientPath) {
|
|
67
|
+
return (await FileReader.loadModule(sClientPath))?.PrismaClient;
|
|
68
|
+
}
|
|
69
|
+
/** Finds `*.<extension>.ts` (or built `.js`) files in a folder (`src/<extension>s` by default) and returns `{ [subPath]: defaultExport }` */
|
|
70
|
+
static async findMany(sExtension, sSourceDir) {
|
|
71
|
+
const sSrcDir = (sSourceDir ?? `src/${sExtension}s`).replace(/^\.\//, "").replace(/\/$/, "");
|
|
72
|
+
const aFilePaths = await glob(`${sSrcDir}/**/*.${sExtension}.{ts,js}`, {
|
|
73
|
+
posix: true,
|
|
74
|
+
ignore: "**/*.d.ts"
|
|
75
|
+
});
|
|
76
|
+
const oEntries = {};
|
|
77
|
+
for (const sFilePath of aFilePaths) {
|
|
78
|
+
const sFileSubPath = sFilePath.slice(sSrcDir.length).replace(new RegExp(`\\.${sExtension}\\.(ts|js)$`), "");
|
|
79
|
+
const sKey = sExtension === "store" ? sFileSubPath.slice(1).replaceAll("/", "_") : sFileSubPath;
|
|
80
|
+
oEntries[sKey] = await FileReader.loadModule(sFilePath, { default: true });
|
|
81
|
+
}
|
|
82
|
+
return oEntries;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
14
85
|
// ../core/const.ts
|
|
15
86
|
/** HTTP methods a route can handle */
|
|
16
87
|
const HTTP_METHODS = [
|
|
@@ -22,8 +93,6 @@ const HTTP_METHODS = [
|
|
|
22
93
|
];
|
|
23
94
|
/** Package of the official plugins: only its schemas may declare nano_* */
|
|
24
95
|
const OFFICIAL_PLUGINS = "@nano-app/plugins";
|
|
25
|
-
/** Error when the database URL is missing at startup */
|
|
26
|
-
const MISSING_DATABASE_URL = "[nano-app] [ERROR] The DATABASE_URL environment variable is not defined.";
|
|
27
96
|
/** Loopback and private networks: the proxies the backend trusts by default */
|
|
28
97
|
const PRIVATE_NETWORKS = [
|
|
29
98
|
"127.0.0.0/8",
|
|
@@ -33,138 +102,107 @@ const PRIVATE_NETWORKS = [
|
|
|
33
102
|
"192.168.0.0/16",
|
|
34
103
|
"fc00::/7"
|
|
35
104
|
];
|
|
36
|
-
// ../core/
|
|
105
|
+
// ../core/utils/Paths.ts
|
|
106
|
+
/** Paths of the app's pages */
|
|
107
|
+
var Paths = class {
|
|
108
|
+
/** Whether a path matches a pattern: exact (`/admin`), or the path and all below it (`/admin/*`) */
|
|
109
|
+
static matches(sPattern, sPath) {
|
|
110
|
+
if (!sPattern.endsWith("/*")) return sPattern === sPath;
|
|
111
|
+
const sBase = sPattern.slice(0, -2);
|
|
112
|
+
return sPath === sBase || sPath.startsWith(`${sBase}/`);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
// ../core/utils/Validator.ts
|
|
37
116
|
const STORE_TYPES = ["app", "api"];
|
|
38
|
-
/** Checks the consumer's files
|
|
117
|
+
/** Checks values of the framework and the consumer's files; an invalid file throws a `NanoError` */
|
|
39
118
|
var Validator = class Validator {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.warn(`[nano-app] [WARNING] ${sFilePath}: ${sMessage}`);
|
|
48
|
-
return false;
|
|
119
|
+
/** Whether a value is a plain object */
|
|
120
|
+
static isObject(oValue) {
|
|
121
|
+
return Object.prototype.toString.call(oValue) === "[object Object]";
|
|
122
|
+
}
|
|
123
|
+
/** Whether a value is a plain object of strings */
|
|
124
|
+
static isStringMap(oValue) {
|
|
125
|
+
return Validator.isObject(oValue) && Object.values(oValue).every((sItem) => typeof sItem === "string");
|
|
49
126
|
}
|
|
50
|
-
/**
|
|
51
|
-
static
|
|
52
|
-
|
|
53
|
-
|
|
127
|
+
/** Whether a value is a path that starts with `/` */
|
|
128
|
+
static isPath(oValue) {
|
|
129
|
+
return typeof oValue === "string" && oValue.startsWith("/");
|
|
130
|
+
}
|
|
131
|
+
/** First environment variable of a list that is not defined */
|
|
132
|
+
static missingEnv(aNames) {
|
|
133
|
+
return aNames.find((sName) => !process.env[sName]);
|
|
134
|
+
}
|
|
135
|
+
/** Checks every entry with a guard (`Validator.isRoute`...): the first invalid one throws */
|
|
136
|
+
static validate(oEntries, fnGuard) {
|
|
137
|
+
return Object.fromEntries(Object.entries(oEntries).filter(([sKey, oEntry]) => fnGuard(oEntry, sKey)));
|
|
54
138
|
}
|
|
55
139
|
/** Type guard for a `*.route.ts` entry */
|
|
56
140
|
static isRoute(oEntry, sFilePath) {
|
|
57
|
-
if (!Validator.isObject(oEntry
|
|
58
|
-
if (oEntry.expression && typeof oEntry.expression !== "string")
|
|
59
|
-
if (oEntry.public !== void 0 && typeof oEntry.public !== "boolean")
|
|
141
|
+
if (!Validator.isObject(oEntry)) throw new NanoError(`${sFilePath}: the default export is not an object.`);
|
|
142
|
+
if (oEntry.expression && typeof oEntry.expression !== "string") throw new NanoError(`${sFilePath}: "expression" must be a string.`);
|
|
143
|
+
if (oEntry.public !== void 0 && typeof oEntry.public !== "boolean") throw new NanoError(`${sFilePath}: "public" must be a boolean.`);
|
|
60
144
|
for (const sMethod of HTTP_METHODS) {
|
|
61
145
|
const oMethod = oEntry[sMethod];
|
|
62
|
-
if (oMethod && typeof oMethod !== "function" && typeof oMethod.handler !== "function")
|
|
146
|
+
if (oMethod && typeof oMethod !== "function" && typeof oMethod.handler !== "function") throw new NanoError(`${sFilePath}: "${sMethod}" must be a function or { handler, permissions }.`);
|
|
63
147
|
}
|
|
64
|
-
if (!HTTP_METHODS.some((sMethod) => oEntry[sMethod]))
|
|
148
|
+
if (!HTTP_METHODS.some((sMethod) => oEntry[sMethod])) throw new NanoError(`${sFilePath}: defines no HTTP method.`);
|
|
65
149
|
return true;
|
|
66
150
|
}
|
|
67
151
|
/** Type guard for a `*.store.ts` entry */
|
|
68
152
|
static isStore(oEntry, sFilePath) {
|
|
69
|
-
if (!Validator.isObject(oEntry
|
|
70
|
-
if (!STORE_TYPES.includes(oEntry.type))
|
|
71
|
-
if (oEntry.data !== void 0 &&
|
|
72
|
-
if (oEntry.headers !== void 0 &&
|
|
73
|
-
if (oEntry.auth !== void 0 &&
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
/** Type guard for a `.prisma` entry: the contents of the file, without comments */
|
|
77
|
-
static isPrisma(oEntry, sFilePath) {
|
|
78
|
-
if (typeof oEntry !== "string") return Validator.warn(sFilePath, "the contents are not a string.");
|
|
79
|
-
const sProvider = oEntry.match(/datasource\s+\w+\s*\{[^}]*provider\s*=\s*["']([^"']+)["']/s)?.[1];
|
|
80
|
-
if (sProvider !== "postgresql") return Validator.warn(sFilePath, `the datasource provider must be "postgresql"${sProvider ? `, not "${sProvider}"` : ""}.`);
|
|
153
|
+
if (!Validator.isObject(oEntry)) throw new NanoError(`${sFilePath}: the default export is not an object.`);
|
|
154
|
+
if (!STORE_TYPES.includes(oEntry.type)) throw new NanoError(`${sFilePath}: its type must be "app" or "api".`);
|
|
155
|
+
if (oEntry.data !== void 0 && !Validator.isObject(oEntry.data)) throw new NanoError(`${sFilePath}: "data" must be an object.`);
|
|
156
|
+
if (oEntry.headers !== void 0 && !Validator.isStringMap(oEntry.headers)) throw new NanoError(`${sFilePath}: "headers" must be an object of strings.`);
|
|
157
|
+
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.`);
|
|
81
158
|
return true;
|
|
82
159
|
}
|
|
83
|
-
/**
|
|
84
|
-
static
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
static validateUnique(oEntry, sExtension, sFilePath = "") {
|
|
94
|
-
return this.oValidators[sExtension]?.()(oEntry, sFilePath) ? oEntry : void 0;
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
// ../core/loader/FileReader.ts
|
|
98
|
-
/** Reads and loads files from disk, resolved from `cwd` */
|
|
99
|
-
var FileReader = class {
|
|
100
|
-
/** Loader of the consumer's modules; the Prisma runtime is loaded natively */
|
|
101
|
-
static oJiti = createJiti(import.meta.url, { nativeModules: ["@prisma/client"] });
|
|
102
|
-
/** Loads a module via jiti */
|
|
103
|
-
static async loadModule(sFilePath, oOptions = {}) {
|
|
104
|
-
return await this.oJiti.import(this.getAbsolutePath(sFilePath), oOptions);
|
|
105
|
-
}
|
|
106
|
-
/** Reads the contents of a file */
|
|
107
|
-
static loadFile(sFilePath) {
|
|
108
|
-
const sContent = readFileSync(this.getAbsolutePath(sFilePath), "utf-8");
|
|
109
|
-
if (!sContent) throw new Error(`[nano-app] [ERROR] The file ${sFilePath} does not have valid content.`);
|
|
110
|
-
return sContent;
|
|
111
|
-
}
|
|
112
|
-
/** Reads a Prisma schema without its `//` comments */
|
|
113
|
-
static loadSchema(sFilePath) {
|
|
114
|
-
return this.loadFile(sFilePath).replace(/\/\/.*$/gm, "");
|
|
160
|
+
/** Whether `auth` has the base path of Better Auth and valid routes, none redirecting to itself */
|
|
161
|
+
static isAuth(oAuth) {
|
|
162
|
+
if (!Validator.isObject(oAuth) || typeof oAuth.path !== "string") return false;
|
|
163
|
+
const aRoutes = oAuth.routes ?? [];
|
|
164
|
+
if (!Array.isArray(aRoutes)) return false;
|
|
165
|
+
return aRoutes.every((oRoute) => {
|
|
166
|
+
if (!Validator.isObject(oRoute)) return false;
|
|
167
|
+
const { match, redirect = "/", roles, permissions } = oRoute;
|
|
168
|
+
return Validator.isPath(match) && Validator.isPath(redirect) && !Paths.matches(match, redirect) && (roles === void 0 || typeof roles === "string") && (permissions === void 0 || Validator.isStringMap(permissions));
|
|
169
|
+
});
|
|
115
170
|
}
|
|
116
|
-
/**
|
|
117
|
-
static
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
|
|
171
|
+
/** Whether a Prisma schema, without comments, is PostgreSQL and declares the schemas it may */
|
|
172
|
+
static isPrisma(sContent, sFilePath, isPlugin) {
|
|
173
|
+
const sProvider = FileReader.datasource(sContent).match(/\bprovider\s*=\s*["']([^"']+)["']/)?.[1];
|
|
174
|
+
if (sProvider !== "postgresql") throw new NanoError(`${sFilePath}: the datasource provider must be "postgresql"${sProvider ? `, not "${sProvider}"` : ""}.`);
|
|
175
|
+
const aSchemas = FileReader.schemas(sContent);
|
|
176
|
+
const isPluginInvalid = !aSchemas.length || aSchemas.some((sItem) => !sItem.startsWith("nano_"));
|
|
177
|
+
const isConsumerInvalid = aSchemas.some((sItem) => sItem.startsWith("nano_"));
|
|
178
|
+
if (isPlugin ? isPluginInvalid : isConsumerInvalid) throw new NanoError(`${sFilePath}: ${isPlugin ? "a plugin declares only nano_* schemas" : "the nano_* schemas are reserved for plugins"}.`);
|
|
179
|
+
return true;
|
|
121
180
|
}
|
|
122
181
|
};
|
|
123
|
-
// ../core/utils/
|
|
124
|
-
/**
|
|
125
|
-
var
|
|
126
|
-
/** Message of
|
|
127
|
-
static
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
182
|
+
// ../core/utils/Logger.ts
|
|
183
|
+
/** Messages of the framework on the console */
|
|
184
|
+
var Logger = class Logger {
|
|
185
|
+
/** Message of the framework: `[nano-app] [<level>] <text>` */
|
|
186
|
+
static message(sLevel, sText) {
|
|
187
|
+
return `[nano-app] [${sLevel}] ${sText}`;
|
|
188
|
+
}
|
|
189
|
+
/** `[nano-app] [ERROR] <text>`, with what was thrown */
|
|
190
|
+
static error(sText, oError) {
|
|
191
|
+
console.error(Logger.message("ERROR", sText), oError);
|
|
192
|
+
}
|
|
193
|
+
/** `[nano-app] [WARNING] <text>` */
|
|
194
|
+
static warn(sText) {
|
|
195
|
+
console.warn(Logger.message("WARNING", sText));
|
|
196
|
+
}
|
|
197
|
+
/** `[nano-app] [INFO] <text>` */
|
|
198
|
+
static info(sText) {
|
|
199
|
+
console.log(Logger.message("INFO", sText));
|
|
200
|
+
}
|
|
201
|
+
/** Date of the request log, in UTC: `2026-10-19 12:03:10` */
|
|
202
|
+
static date(oDate = /* @__PURE__ */ new Date()) {
|
|
133
203
|
return oDate.toISOString().slice(0, 19).replace("T", " ");
|
|
134
204
|
}
|
|
135
205
|
};
|
|
136
|
-
// ../core/loader/FileLoader.ts
|
|
137
|
-
/** Finds, loads and validates the consumer's files */
|
|
138
|
-
var FileLoader = class {
|
|
139
|
-
/**
|
|
140
|
-
* 1. Validates a `.prisma` file.
|
|
141
|
-
* 2. Loads a generated Prisma client module (`client.ts`, or `client.js`)
|
|
142
|
-
*/
|
|
143
|
-
static async findPrismaClient(sClientPath, sSchemaPath) {
|
|
144
|
-
const sContent = FileReader.loadSchema(sSchemaPath);
|
|
145
|
-
if (!Validator.validateUnique(sContent, "prisma", sSchemaPath)) throw new Error(`[nano-app] [ERROR] ${sSchemaPath} is not a valid Prisma schema.`);
|
|
146
|
-
try {
|
|
147
|
-
return (await FileReader.loadModule(sClientPath))?.PrismaClient;
|
|
148
|
-
} catch (oError) {
|
|
149
|
-
throw new Error(`[nano-app] [ERROR] The Prisma client ${sClientPath} could not be loaded: ${Formatter.error(oError)}`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
/** Finds `*.route.ts` (or built `.js`) files and returns `{ [subPath]: defaultExport }` */
|
|
153
|
-
static async findMany({ extension, sourceDir }) {
|
|
154
|
-
const sSrcDir = (sourceDir ?? `src/${extension}s`).replace(/^\.\//, "").replace(/\/$/, "");
|
|
155
|
-
const aFilePaths = await glob(`${sSrcDir}/**/*.${extension}.{ts,js}`, {
|
|
156
|
-
posix: true,
|
|
157
|
-
ignore: "**/*.d.ts"
|
|
158
|
-
});
|
|
159
|
-
const oEntries = {};
|
|
160
|
-
for (const sFilePath of aFilePaths) {
|
|
161
|
-
const sFileSubPath = sFilePath.slice(sSrcDir.length).replace(new RegExp(`\\.${extension}\\.(ts|js)$`), "");
|
|
162
|
-
const sKey = extension === "store" ? sFileSubPath.slice(1).replaceAll("/", "_") : sFileSubPath;
|
|
163
|
-
oEntries[sKey] = await FileReader.loadModule(sFilePath, { default: true });
|
|
164
|
-
}
|
|
165
|
-
return Validator.validate(oEntries, extension);
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
206
|
// src/auth/BetterAuth.ts
|
|
169
207
|
/** Files of the app's Better Auth options, relative to `cwd`: source, then built */
|
|
170
208
|
const AUTH_FILES = ["src/auth.ts", "src/auth.js"];
|
|
@@ -176,7 +214,7 @@ var BetterAuth = class {
|
|
|
176
214
|
const sFilePath = AUTH_FILES.find(existsSync);
|
|
177
215
|
if (!sFilePath) return void 0;
|
|
178
216
|
const oFile = await FileReader.loadModule(sFilePath, { default: true });
|
|
179
|
-
if (!oFile?.options) throw new
|
|
217
|
+
if (!Validator.isObject(oFile?.options)) throw new NanoError(`${sFilePath} must default export { options }: the Better Auth options.`);
|
|
180
218
|
return oFile.options;
|
|
181
219
|
}
|
|
182
220
|
/** The instance, once the app's plugin created it */
|
|
@@ -190,7 +228,7 @@ var BetterAuth = class {
|
|
|
190
228
|
const { getSchema } = await import("better-auth/db");
|
|
191
229
|
const { fromNodeHeaders, toNodeHandler } = await import("better-auth/node");
|
|
192
230
|
const aMissing = Object.keys(getSchema(oOptions)).filter((sModel) => !oDb[sModel]);
|
|
193
|
-
if (aMissing.length) throw new
|
|
231
|
+
if (aMissing.length) throw new NanoError(`Better Auth needs the models ${aMissing.join(", ")} in the app's schema. Run \`npx auth generate\` and apply your schema with Prisma.`);
|
|
194
232
|
const oAuth = betterAuth({
|
|
195
233
|
...oOptions,
|
|
196
234
|
advanced: {
|
|
@@ -203,12 +241,10 @@ var BetterAuth = class {
|
|
|
203
241
|
database: prismaAdapter(oDb, { provider: "postgresql" })
|
|
204
242
|
});
|
|
205
243
|
this.oAuth = oAuth;
|
|
206
|
-
const fnSession = (oReq, oRes, fnNext) => {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}).catch(fnNext);
|
|
211
|
-
};
|
|
244
|
+
const fnSession = (oReq, oRes, fnNext) => oAuth.api.getSession({ headers: fromNodeHeaders(oReq.headers) }).then((oSession) => {
|
|
245
|
+
oRes.locals.session = oSession ?? void 0;
|
|
246
|
+
fnNext();
|
|
247
|
+
});
|
|
212
248
|
return {
|
|
213
249
|
oHandler: toNodeHandler(oAuth),
|
|
214
250
|
fnSession,
|
|
@@ -247,33 +283,24 @@ var Endpoint = class {
|
|
|
247
283
|
}
|
|
248
284
|
/** Handler for all methods */
|
|
249
285
|
async mount(oParams, sPath) {
|
|
250
|
-
if (!oParams.auth && Object.values(this.oMethods).some(({ permissions }) => permissions))
|
|
286
|
+
if (!oParams.auth && Object.values(this.oMethods).some(({ permissions }) => permissions)) Logger.warn(`${sPath}: its permissions are not checked without Better Auth. Add src/auth.ts.`);
|
|
251
287
|
await this.oCallbacks.onInit?.(oParams);
|
|
252
288
|
return this.createHandler(oParams);
|
|
253
289
|
}
|
|
254
290
|
/** Express handler: onBefore, then the method of the request */
|
|
255
291
|
createHandler(oParams) {
|
|
256
292
|
return (oReq, oRes, fnNext) => {
|
|
257
|
-
if (oParams.auth && !oRes.locals.session && !this.isPublic) return fnNext(
|
|
258
|
-
status: 401,
|
|
259
|
-
message: "Not authenticated"
|
|
260
|
-
});
|
|
293
|
+
if (oParams.auth && !oRes.locals.session && !this.isPublic) return fnNext(new NanoError("Not authenticated", 401));
|
|
261
294
|
const sMethod = oReq.method.toUpperCase();
|
|
262
295
|
const sHandled = HTTP_METHODS.find((sHttpMethod) => sHttpMethod === sMethod);
|
|
263
296
|
const oMethod = sHandled && this.oMethods[sHandled];
|
|
264
|
-
if (!oMethod) return fnNext({
|
|
265
|
-
status: 405,
|
|
266
|
-
message: `Method ${sMethod} not implemented`
|
|
267
|
-
});
|
|
297
|
+
if (!oMethod) return fnNext(new NanoError(`Method ${sMethod} not implemented`, 405));
|
|
268
298
|
const oRequestParams = {
|
|
269
299
|
...oParams,
|
|
270
300
|
session: oRes.locals.session
|
|
271
301
|
};
|
|
272
|
-
isGranted(oRequestParams, this.isPublic ? void 0 : oMethod.permissions).then((isAllowed) => {
|
|
273
|
-
if (!isAllowed) throw
|
|
274
|
-
status: 403,
|
|
275
|
-
message: "Forbidden"
|
|
276
|
-
};
|
|
302
|
+
return isGranted(oRequestParams, this.isPublic ? void 0 : oMethod.permissions).then((isAllowed) => {
|
|
303
|
+
if (!isAllowed) throw new NanoError("Forbidden", 403);
|
|
277
304
|
return this.oCallbacks.onBefore?.(oReq, sMethod, oRequestParams) ?? {};
|
|
278
305
|
}).then((onBeforeResult) => oMethod.handler(oReq, {
|
|
279
306
|
...oRequestParams,
|
|
@@ -285,14 +312,13 @@ var Endpoint = class {
|
|
|
285
312
|
...oBody,
|
|
286
313
|
success: iStatus < 400
|
|
287
314
|
});
|
|
288
|
-
})
|
|
315
|
+
});
|
|
289
316
|
};
|
|
290
317
|
}
|
|
291
318
|
};
|
|
292
319
|
// src/routes/Router.ts
|
|
293
320
|
/** Turns a folder of `*.route.ts` files into Express paths and endpoints */
|
|
294
321
|
var Router = class {
|
|
295
|
-
oRoutes = {};
|
|
296
322
|
sRoutesDir;
|
|
297
323
|
sBasePath;
|
|
298
324
|
constructor(sRoutesDir, sBasePath) {
|
|
@@ -301,13 +327,12 @@ var Router = class {
|
|
|
301
327
|
}
|
|
302
328
|
/** Loads the routes and creates one endpoint per file */
|
|
303
329
|
async mount() {
|
|
304
|
-
if (this.sRoutesDir)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
})
|
|
308
|
-
return Object.entries(this.oRoutes).map(([sFilePath, { expression, callbacks, public: isPublic, permissions, ...methods }]) => {
|
|
330
|
+
if (!this.sRoutesDir) return [];
|
|
331
|
+
const oFiles = await FileLoader.findMany("route", this.sRoutesDir);
|
|
332
|
+
const oRoutes = Validator.validate(oFiles, Validator.isRoute);
|
|
333
|
+
return Object.entries(oRoutes).map(([sFilePath, { expression, callbacks, public: isPublic, permissions, ...methods }]) => {
|
|
309
334
|
const sPath = this.createPath(sFilePath, expression);
|
|
310
|
-
|
|
335
|
+
Logger.info(sPath);
|
|
311
336
|
return {
|
|
312
337
|
path: sPath,
|
|
313
338
|
endpoint: new Endpoint(methods, {
|
|
@@ -337,12 +362,12 @@ var Prisma = class {
|
|
|
337
362
|
const sHash = createHash("sha256").update(readFileSync(sSource)).digest("hex");
|
|
338
363
|
const sSourceFile = join(sDir, "source.json");
|
|
339
364
|
const oCurrent = existsSync(sSourceFile) ? JSON.parse(readFileSync(sSourceFile, "utf-8")) : void 0;
|
|
340
|
-
if (oCurrent && oCurrent.schemaPath !== sSource)
|
|
365
|
+
if (oCurrent && oCurrent.schemaPath !== sSource) Logger.warn(`.nano-app/prisma-clients/${sName} has the client of ${oCurrent.schemaPath}: it is replaced by the one of ${sSource}.`);
|
|
341
366
|
if (oCurrent?.schemaPath === sSource && oCurrent.hash === sHash && existsSync(join(sDir, "generated", "client.ts"))) return;
|
|
342
367
|
mkdirSync(sDir, { recursive: true });
|
|
343
368
|
const sSchema = readFileSync(sSource, "utf-8");
|
|
344
369
|
writeFileSync(join(sDir, "schema.prisma"), sSchema.replace(/output\s*=\s*"[^"]*"/, "output = \"./generated\""));
|
|
345
|
-
this.run(
|
|
370
|
+
this.run([
|
|
346
371
|
"generate",
|
|
347
372
|
"--schema",
|
|
348
373
|
join(sDir, "schema.prisma")
|
|
@@ -351,14 +376,13 @@ var Prisma = class {
|
|
|
351
376
|
schemaPath: sSource,
|
|
352
377
|
hash: sHash
|
|
353
378
|
}));
|
|
354
|
-
|
|
379
|
+
Logger.info(`Prisma client "${sName}" generated`);
|
|
355
380
|
}
|
|
356
381
|
/** Applies the pending migrations an official plugin ships next to its schema */
|
|
357
382
|
static migrate(sName, sSchemaPath, sDbSchema) {
|
|
358
383
|
const sMigrations = join(dirname(FileReader.getAbsolutePath(sSchemaPath)), "migrations");
|
|
359
|
-
if (!existsSync(sMigrations)) throw new
|
|
360
|
-
|
|
361
|
-
const oUrl = new URL(process.env.DATABASE_URL);
|
|
384
|
+
if (!existsSync(sMigrations)) throw new NanoError(`The plugin "${sName}" ships no migrations: ${sMigrations}`);
|
|
385
|
+
const oUrl = new URL(process.env.DATABASE_URL ?? "");
|
|
362
386
|
oUrl.searchParams.set("schema", sDbSchema);
|
|
363
387
|
const sConfig = join(this.dir(sName), "prisma.config.mjs");
|
|
364
388
|
writeFileSync(sConfig, [
|
|
@@ -369,7 +393,7 @@ var Prisma = class {
|
|
|
369
393
|
"};",
|
|
370
394
|
""
|
|
371
395
|
].join("\n"));
|
|
372
|
-
this.run(
|
|
396
|
+
this.run([
|
|
373
397
|
"migrate",
|
|
374
398
|
"deploy",
|
|
375
399
|
"--config",
|
|
@@ -377,25 +401,15 @@ var Prisma = class {
|
|
|
377
401
|
], { NANO_APP_MIGRATION_URL: oUrl.toString() });
|
|
378
402
|
}
|
|
379
403
|
/** Runs a Prisma command of the consumer's `prisma` package */
|
|
380
|
-
static run(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
env: {
|
|
390
|
-
...process.env,
|
|
391
|
-
...oEnv
|
|
392
|
-
},
|
|
393
|
-
stdio: "pipe"
|
|
394
|
-
});
|
|
395
|
-
} catch (oError) {
|
|
396
|
-
const sOutput = oError instanceof Error && "stderr" in oError && "stdout" in oError ? `${oError.stderr}${oError.stdout}`.trim() : "";
|
|
397
|
-
throw new Error(`[nano-app] [ERROR] prisma ${aArgs[0]} failed for "${sName}": ${sOutput || Formatter.error(oError)}`);
|
|
398
|
-
}
|
|
404
|
+
static run(aArgs, oEnv = {}) {
|
|
405
|
+
const sCli = createRequire(`${process.cwd()}/`).resolve("prisma/build/index.js");
|
|
406
|
+
execFileSync(process.execPath, [sCli, ...aArgs], {
|
|
407
|
+
env: {
|
|
408
|
+
...process.env,
|
|
409
|
+
...oEnv
|
|
410
|
+
},
|
|
411
|
+
stdio: "pipe"
|
|
412
|
+
});
|
|
399
413
|
}
|
|
400
414
|
};
|
|
401
415
|
// src/database/Database.ts
|
|
@@ -407,7 +421,7 @@ var Database = class {
|
|
|
407
421
|
/** Initializes the Database client */
|
|
408
422
|
static async init() {
|
|
409
423
|
const sUrl = process.env.DATABASE_URL;
|
|
410
|
-
if (!sUrl) throw new
|
|
424
|
+
if (!sUrl) throw new NanoError("The DATABASE_URL environment variable is not defined.");
|
|
411
425
|
const { PrismaPg } = await import("@prisma/adapter-pg");
|
|
412
426
|
this.oAdapter = new PrismaPg({ connectionString: sUrl });
|
|
413
427
|
}
|
|
@@ -417,36 +431,20 @@ var Database = class {
|
|
|
417
431
|
}
|
|
418
432
|
/** Generates and mounts the Prisma client of a schema; an official plugin's is also migrated */
|
|
419
433
|
static async mount(sName, { schemaPath }) {
|
|
420
|
-
if (this.get(sName)) throw new
|
|
434
|
+
if (this.get(sName)) throw new NanoError(`The database "${sName}" is already mounted.`);
|
|
421
435
|
const isPlugin = this.isOfficial(schemaPath);
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
const isConsumerInvalid = aSchemas.some((sItem) => sItem.startsWith("nano_"));
|
|
425
|
-
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"}.`);
|
|
436
|
+
const sContent = FileReader.loadSchema(schemaPath);
|
|
437
|
+
Validator.isPrisma(sContent, schemaPath, isPlugin);
|
|
426
438
|
Prisma.generate(sName, schemaPath);
|
|
427
|
-
if (isPlugin) Prisma.migrate(sName, schemaPath, aSchemas[0]);
|
|
428
439
|
if (!this.oAdapter) await this.init();
|
|
440
|
+
if (isPlugin) Prisma.migrate(sName, schemaPath, FileReader.schemas(sContent)[0]);
|
|
429
441
|
const sClientPath = join(Prisma.dir(sName), "generated", "client.ts");
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
try {
|
|
433
|
-
oClient = new PrismaClient({ adapter: this.oAdapter });
|
|
434
|
-
} catch (oError) {
|
|
435
|
-
throw new Error(`[nano-app] [ERROR] Cannot instantiate the database "${sName}" from ${sClientPath}: ${Formatter.error(oError)}`);
|
|
436
|
-
}
|
|
437
|
-
const sFix = isPlugin ? "Update the plugin or report it." : "Apply your schema with Prisma (db push or migrations).";
|
|
438
|
-
for (const [, sModel] of FileReader.loadSchema(schemaPath).matchAll(/^\s*model\s+(\w+)/gm)) await oClient[`${sModel[0].toLowerCase()}${sModel.slice(1)}`].findFirst().catch((oError) => {
|
|
439
|
-
const sReason = Formatter.error(oError).trim().split("\n").pop();
|
|
440
|
-
throw new Error(`[nano-app] [ERROR] The database "${sName}" is not ready: ${sReason} ${sFix}`);
|
|
441
|
-
});
|
|
442
|
+
const oClient = new (await (FileLoader.findPrismaClient(sClientPath)))({ adapter: this.oAdapter });
|
|
443
|
+
for (const [, sModel] of sContent.matchAll(/^\s*model\s+(\w+)/gm)) await oClient[`${sModel[0].toLowerCase()}${sModel.slice(1)}`].findFirst();
|
|
442
444
|
this.oClients[sName] = oClient;
|
|
443
|
-
|
|
445
|
+
Logger.info(`Database "${sName}" mounted`);
|
|
444
446
|
return oClient;
|
|
445
447
|
}
|
|
446
|
-
/** Schemas a Prisma schema declares in its datasource (`schemas = [...]`) */
|
|
447
|
-
static declaredSchemas(sSchemaPath) {
|
|
448
|
-
return [...(FileReader.loadSchema(sSchemaPath).match(/datasource\s+\w+\s*\{[^}]*schemas\s*=\s*\[([^\]]*)\]/s)?.[1] ?? "").matchAll(/["']([^"']+)["']/g)].map(([, sItem]) => sItem);
|
|
449
|
-
}
|
|
450
448
|
/** Whether a schema ships with the official plugins: its nearest package.json is theirs */
|
|
451
449
|
static isOfficial(sSchemaPath) {
|
|
452
450
|
let sDir = dirname(FileReader.getAbsolutePath(sSchemaPath));
|
|
@@ -472,7 +470,7 @@ var Plugin = class {
|
|
|
472
470
|
const { name, routesDir, basePath, database, options, callbacks } = this.oConfig;
|
|
473
471
|
const oDb = database?.enabled ? await Database.mount(name, database) : Database.get("app");
|
|
474
472
|
if (this.oAuthOptions) {
|
|
475
|
-
if (!oDb) throw new
|
|
473
|
+
if (!oDb) throw new NanoError("src/auth.ts needs the database of the app: set mainPlugin.database.enabled to true.");
|
|
476
474
|
const { sPath, oHandler, fnSession } = await BetterAuth.create(this.oAuthOptions, oDb);
|
|
477
475
|
oApp.all(sPath, oHandler);
|
|
478
476
|
oApp.use(fnSession);
|
|
@@ -500,7 +498,7 @@ const INITIAL_CONFIG = {
|
|
|
500
498
|
packages: {
|
|
501
499
|
morgan: {
|
|
502
500
|
tokens: {
|
|
503
|
-
timestamp: () =>
|
|
501
|
+
timestamp: () => Logger.date(),
|
|
504
502
|
path: (oReq) => oReq.path
|
|
505
503
|
},
|
|
506
504
|
format: ":timestamp | :method :path :status"
|
|
@@ -514,7 +512,7 @@ const INITIAL_CONFIG = {
|
|
|
514
512
|
"QUERY"
|
|
515
513
|
] },
|
|
516
514
|
helmet: true,
|
|
517
|
-
compression:
|
|
515
|
+
compression: false,
|
|
518
516
|
express: { limit: "10kb" },
|
|
519
517
|
urlencoded: { extended: false }
|
|
520
518
|
},
|
|
@@ -530,14 +528,10 @@ const INITIAL_CONFIG = {
|
|
|
530
528
|
/** Plugins around the consumer routes */
|
|
531
529
|
plugins: []
|
|
532
530
|
};
|
|
533
|
-
/** Whether a value is a plain object (`{}`), the only kind merged field by field */
|
|
534
|
-
function isPlainObject(oValue) {
|
|
535
|
-
return Object.prototype.toString.call(oValue) === "[object Object]";
|
|
536
|
-
}
|
|
537
531
|
/** Merges a value over its default: plain objects key by key at every level; anything else replaces it */
|
|
538
532
|
function deepMerge(oDefault, oValue) {
|
|
539
533
|
if (oValue === void 0) return oDefault;
|
|
540
|
-
if (!
|
|
534
|
+
if (!Validator.isObject(oDefault) || !Validator.isObject(oValue)) return oValue;
|
|
541
535
|
const aKeys = Object.keys({
|
|
542
536
|
...oDefault,
|
|
543
537
|
...oValue
|
|
@@ -546,12 +540,12 @@ function deepMerge(oDefault, oValue) {
|
|
|
546
540
|
}
|
|
547
541
|
/** Whether a merged configuration kept the plain object of the initial one */
|
|
548
542
|
function isMergedConfig(oValue) {
|
|
549
|
-
return
|
|
543
|
+
return Validator.isObject(oValue);
|
|
550
544
|
}
|
|
551
545
|
/** Merges the consumer configuration over the initial one; the main plugin is always named "app" */
|
|
552
546
|
function mergeConfig(oConfig = {}) {
|
|
553
547
|
const oMerged = deepMerge(INITIAL_CONFIG, oConfig);
|
|
554
|
-
if (!isMergedConfig(oMerged)) throw new
|
|
548
|
+
if (!isMergedConfig(oMerged)) throw new NanoError("The configuration of createNanoApp must be an object.");
|
|
555
549
|
return {
|
|
556
550
|
...oMerged,
|
|
557
551
|
mainPlugin: {
|
|
@@ -607,15 +601,12 @@ function normalize(oError) {
|
|
|
607
601
|
}
|
|
608
602
|
/** Turns every unmatched request into a 404 for the error handler */
|
|
609
603
|
const notFoundHandler = (oReq, _oRes, fnNext) => {
|
|
610
|
-
fnNext({
|
|
611
|
-
status: 404,
|
|
612
|
-
message: `${oReq.path} not found`
|
|
613
|
-
});
|
|
604
|
+
fnNext(new NanoError(`${oReq.path} not found`, 404));
|
|
614
605
|
};
|
|
615
606
|
/** Turns any error raised by a handler into an ApiResponse */
|
|
616
607
|
const errorHandler = (oError, _oReq, oRes, _fnNext) => {
|
|
617
608
|
const { status, body } = normalize(oError);
|
|
618
|
-
if (status >= 500)
|
|
609
|
+
if (status >= 500) Logger.error("Internal server error", oError);
|
|
619
610
|
oRes.status(status).json(body);
|
|
620
611
|
};
|
|
621
612
|
// src/app/NanoApp.ts
|
|
@@ -634,11 +625,10 @@ var NanoApp = class {
|
|
|
634
625
|
await this.initialize();
|
|
635
626
|
this.oApp.listen(Number(this.oConfig.port), () => {
|
|
636
627
|
this.oConfig.listener?.();
|
|
637
|
-
|
|
628
|
+
Logger.info(`Nano App running on port ${this.oConfig.port}`);
|
|
638
629
|
});
|
|
639
|
-
return this;
|
|
640
630
|
} catch (oError) {
|
|
641
|
-
|
|
631
|
+
Logger.error("Error trying to start the server.", oError);
|
|
642
632
|
process.exit(1);
|
|
643
633
|
}
|
|
644
634
|
}
|
|
@@ -664,11 +654,14 @@ var NanoApp = class {
|
|
|
664
654
|
for (const [sKey, fnToken] of Object.entries(oConfig.morgan.tokens ?? {})) morgan.token(sKey, fnToken);
|
|
665
655
|
this.oApp.use(morgan(oConfig.morgan.format));
|
|
666
656
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
657
|
+
const aPackages = [
|
|
658
|
+
oConfig.cors && cors(oConfig.cors),
|
|
659
|
+
oConfig.helmet && helmet(),
|
|
660
|
+
oConfig.compression && compression(),
|
|
661
|
+
oConfig.express && express.json(oConfig.express),
|
|
662
|
+
oConfig.urlencoded && express.urlencoded(oConfig.urlencoded)
|
|
663
|
+
];
|
|
664
|
+
for (const fnPackage of aPackages) if (fnPackage) this.oApp.use(fnPackage);
|
|
672
665
|
}
|
|
673
666
|
};
|
|
674
667
|
// src/app/creators.ts
|
|
@@ -687,4 +680,4 @@ function createPlugin(fnCreator) {
|
|
|
687
680
|
function createPluginCallbacks(oCallbacks) {
|
|
688
681
|
return oCallbacks;
|
|
689
682
|
}
|
|
690
|
-
export { NanoApp, createNanoApp, createPlugin, createPluginCallbacks, createRoute, isGranted };
|
|
683
|
+
export { NanoApp, NanoError, createNanoApp, createPlugin, createPluginCallbacks, createRoute, isGranted };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nano-app/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"cors": "^2.8.6",
|
|
48
48
|
"dotenv": "^18.0.9",
|
|
49
49
|
"express": "^5.3.0",
|
|
50
|
-
"glob": "^14.0.
|
|
50
|
+
"glob": "^14.0.4",
|
|
51
51
|
"helmet": "^8.3.0",
|
|
52
52
|
"jiti": "^2.7.0",
|
|
53
53
|
"morgan": "^1.12.1"
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@prisma/adapter-pg": "^7.10.1",
|
|
57
57
|
"@prisma/client": "^7.10.1",
|
|
58
|
-
"better-auth": "^1.7.
|
|
58
|
+
"better-auth": "^1.7.18",
|
|
59
59
|
"pg": "^8.23.1"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
@@ -81,10 +81,10 @@
|
|
|
81
81
|
"@types/morgan": "^1.9.10",
|
|
82
82
|
"@types/node": "^26.6.6",
|
|
83
83
|
"@types/pg": "^8.20.0",
|
|
84
|
-
"better-auth": "^1.7.
|
|
84
|
+
"better-auth": "^1.7.18",
|
|
85
85
|
"pg": "^8.23.1",
|
|
86
86
|
"prisma": "^7.10.1",
|
|
87
|
-
"tsdown": "^0.23.
|
|
87
|
+
"tsdown": "^0.23.5",
|
|
88
88
|
"tsx": "^4.23.15",
|
|
89
89
|
"typescript": "^7.0.2"
|
|
90
90
|
}
|