@nano-app/backend 0.8.3 → 0.9.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 +17 -25
- package/build/Prisma-Dq-lYuz3.js +221 -0
- package/build/index.d.ts +136 -128
- package/build/index.js +314 -531
- package/build/nano-app.js +43 -38
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ export default createRoute({
|
|
|
46
46
|
|
|
47
47
|
## Routes
|
|
48
48
|
|
|
49
|
-
Each `*.route.ts` under `mainPlugin.routesDir` default-exports an object with the HTTP methods it implements. **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):
|
|
49
|
+
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):
|
|
50
50
|
|
|
51
51
|
```ts
|
|
52
52
|
export default createRoute({
|
|
@@ -98,11 +98,10 @@ createNanoApp({
|
|
|
98
98
|
|
|
99
99
|
## Database
|
|
100
100
|
|
|
101
|
-
Off until enabled:
|
|
101
|
+
Off until enabled: at startup nano-app generates the Prisma client of your schema, checks that its tables can be read and passes it to your handlers as their second parameter. **It never modifies your database**: applying the schema (`prisma db push`, `prisma migrate dev`, `prisma migrate deploy`) is up to you, with Prisma.
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
npm install @prisma/client @prisma/adapter-pg pg
|
|
105
|
-
npm install -D prisma
|
|
104
|
+
npm install prisma @prisma/client @prisma/adapter-pg pg # prisma too: the startup generates the client
|
|
106
105
|
```
|
|
107
106
|
|
|
108
107
|
```prisma
|
|
@@ -113,7 +112,7 @@ datasource db {
|
|
|
113
112
|
|
|
114
113
|
generator client {
|
|
115
114
|
provider = "prisma-client"
|
|
116
|
-
output = "./client"
|
|
115
|
+
output = "./client" // any value: nano-app generates in .nano-app/prisma-clients/app
|
|
117
116
|
}
|
|
118
117
|
|
|
119
118
|
model User {
|
|
@@ -122,11 +121,6 @@ model User {
|
|
|
122
121
|
}
|
|
123
122
|
```
|
|
124
123
|
|
|
125
|
-
```bash
|
|
126
|
-
npx nano-app generate # client in .nano-app/prisma-clients/app (add .nano-app/ to .gitignore)
|
|
127
|
-
npx nano-app push # generate + prisma db push; or any Prisma command: npx nano-app migrate dev …
|
|
128
|
-
```
|
|
129
|
-
|
|
130
124
|
```ts
|
|
131
125
|
// src/app.ts — DATABASE_URL comes from the environment (or a .env file)
|
|
132
126
|
createNanoApp({ mainPlugin: { database: { enabled: true } } });
|
|
@@ -144,18 +138,21 @@ export default createRoute<PrismaClient>({
|
|
|
144
138
|
|
|
145
139
|
| Option | Default | Description |
|
|
146
140
|
|---|---|---|
|
|
147
|
-
| `mainPlugin.database.enabled` | `false` |
|
|
148
|
-
| `mainPlugin.database.schemaPath` | `"src/prisma/schema.prisma"` |
|
|
149
|
-
|
|
150
|
-
- Each key falls back to its default, so `{ enabled: true }` is enough with the default layout.
|
|
151
|
-
-
|
|
152
|
-
-
|
|
141
|
+
| `mainPlugin.database.enabled` | `false` | Generates and mounts your client at startup. |
|
|
142
|
+
| `mainPlugin.database.schemaPath` | `"src/prisma/schema.prisma"` | Your single `.prisma` file; its datasource must be `postgresql`. |
|
|
143
|
+
|
|
144
|
+
- Each key falls back to its default, so `{ enabled: true }` is enough with the default layout.
|
|
145
|
+
- The client lives in `.nano-app/prisma-clients/app/` (add `.nano-app/` to `.gitignore`). It is generated again only when the schema changes. Do not run `prisma generate`: a second client would be out of sync.
|
|
146
|
+
- `npx nano-app generate` does what the startup does, without starting: for your editor's types, or for the build when the filesystem is read-only in production.
|
|
147
|
+
- Startup fails if `DATABASE_URL` is missing, the schema is not a valid PostgreSQL schema, or a table or column of a model cannot be read. The message says which: apply your schema with Prisma and start again.
|
|
148
|
+
- The `nano_` prefix is reserved for the official plugins (`@nano-app/plugins`): a datasource schema of yours starting with it fails the startup.
|
|
153
149
|
- Pass your generated `PrismaClient` to `createRoute<PrismaClient>` to type `db`; otherwise it is `any`, and without an enabled database `undefined`.
|
|
154
150
|
- For several PostgreSQL schemas, declare `schemas = ["public", "billing"]` in the datasource and `@@schema("billing")` on each model; one client covers them all.
|
|
151
|
+
- Prisma is loaded only when your database or a mounted plugin's is enabled.
|
|
155
152
|
|
|
156
153
|
## Plugins
|
|
157
154
|
|
|
158
|
-
A plugin is a group of endpoints mounted under a base path. Your routes are mounted as one (named `app`). The official plugins are in [`@nano-app/plugins`](https://github.com/Nano-Nathan/nano-app/blob/main/plugins/README.md). You write one with `createPlugin` and list it in `plugins`. Nothing is mounted unless you list it.
|
|
155
|
+
A plugin is a group of endpoints mounted under a base path. Your routes are mounted as one (named `app`). The official plugins are in [`@nano-app/plugins`](https://github.com/Nano-Nathan/nano-app/blob/main/plugins/README.md). You write one with `createPlugin` and list it in `plugins`. Nothing is mounted unless you list it. Official plugins that have tables (like `auth`) ship their own migrations: nano-app applies them at startup, each in its `nano_*` schema. That is the only change it makes to your database.
|
|
159
156
|
|
|
160
157
|
```ts
|
|
161
158
|
// src/plugins/hello/index.ts
|
|
@@ -216,12 +213,7 @@ The endpoints of one plugin have no guaranteed order: one route's `onInit` must
|
|
|
216
213
|
|
|
217
214
|
Everything is mounted on one Express application, in this order: base middlewares → `before` plugins → your routes → `after` plugins → 404 → error handler.
|
|
218
215
|
|
|
219
|
-
A plugin with
|
|
220
|
-
|
|
221
|
-
```bash
|
|
222
|
-
npx nano-app generate # the app schema (src/prisma/schema.prisma) → .nano-app/prisma-clients/app
|
|
223
|
-
npx nano-app push # generate + prisma db push
|
|
224
|
-
```
|
|
216
|
+
A plugin with a database follows the same rules, with its client in `.nano-app/prisma-clients/<name>`: a plugin of yours points `database.schemaPath` at its schema and you apply it with Prisma. If that folder holds the client of another schema (another plugin with the same name), the startup warns and replaces it. An official plugin of `@nano-app/plugins` needs nothing from you: at startup nano-app applies its migrations to its own `nano_*` schema. See [PLUGINS.md](https://github.com/Nano-Nathan/nano-app/blob/main/PLUGINS.md).
|
|
225
217
|
|
|
226
218
|
## Errors
|
|
227
219
|
|
|
@@ -248,8 +240,8 @@ Also exported: the configuration types (`NanoAppConfig`, `PackagesConfig`, `Plug
|
|
|
248
240
|
## Development
|
|
249
241
|
|
|
250
242
|
```bash
|
|
251
|
-
pnpm build #
|
|
252
|
-
pnpm dev #
|
|
243
|
+
pnpm build # tsdown → build/ (ESM + .d.ts)
|
|
244
|
+
pnpm dev # tsdown --watch
|
|
253
245
|
pnpm test # node:test against the real server (tsx --test)
|
|
254
246
|
DATABASE_URL=postgres://… pnpm test # also runs the PostgreSQL suite
|
|
255
247
|
```
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { glob } from "glob";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { createJiti } from "jiti";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
//#region ../core/loader/Validator.ts
|
|
9
|
+
const HTTP_METHODS = [
|
|
10
|
+
"GET",
|
|
11
|
+
"POST",
|
|
12
|
+
"PUT",
|
|
13
|
+
"DELETE",
|
|
14
|
+
"QUERY"
|
|
15
|
+
];
|
|
16
|
+
/** Checks the consumer's files: invalid ones are warned about and discarded */
|
|
17
|
+
var Validator = class Validator {
|
|
18
|
+
static oValidators = {
|
|
19
|
+
route: () => this.isRoute,
|
|
20
|
+
prisma: () => this.isPrisma
|
|
21
|
+
};
|
|
22
|
+
/** Warns about an invalid file and returns `false`, to discard it */
|
|
23
|
+
static warn(sFilePath, sMessage) {
|
|
24
|
+
console.warn(`[nano-app] [WARNING] ${sFilePath}: ${sMessage}`);
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/** Checks that the default export is an object */
|
|
28
|
+
static isObject(oEntry, sFilePath) {
|
|
29
|
+
if (oEntry && typeof oEntry === "object") return true;
|
|
30
|
+
return Validator.warn(sFilePath, "the default export is not an object.");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Type guard for a `*.route.ts` entry.
|
|
34
|
+
*/
|
|
35
|
+
static isRoute(oEntry, sFilePath) {
|
|
36
|
+
if (!Validator.isObject(oEntry, sFilePath)) return false;
|
|
37
|
+
const oRoute = oEntry;
|
|
38
|
+
if (oRoute.expression && typeof oRoute.expression !== "string") return Validator.warn(sFilePath, "\"expression\" must be a string.");
|
|
39
|
+
for (const sMethod of HTTP_METHODS) if (oRoute[sMethod] && typeof oRoute[sMethod] !== "function") return Validator.warn(sFilePath, `"${String(sMethod)}" must be a function.`);
|
|
40
|
+
if (!HTTP_METHODS.some((sMethod) => oRoute[sMethod])) return Validator.warn(sFilePath, "defines no HTTP method.");
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Type guard for a `.prisma` entry: the contents of the file, without comments.
|
|
45
|
+
*/
|
|
46
|
+
static isPrisma(oEntry, sFilePath) {
|
|
47
|
+
if (typeof oEntry !== "string") return Validator.warn(sFilePath, "the contents are not a string.");
|
|
48
|
+
const sProvider = oEntry.match(/datasource\s+\w+\s*\{[^}]*provider\s*=\s*["']([^"']+)["']/s)?.[1];
|
|
49
|
+
if (sProvider !== "postgresql") return Validator.warn(sFilePath, `the datasource provider must be "postgresql"${sProvider ? `, not "${sProvider}"` : ""}.`);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/** Validates a full map of entries and returns only the valid ones */
|
|
53
|
+
static validate(oEntries, sExtension) {
|
|
54
|
+
const oValid = {};
|
|
55
|
+
for (const [sFilePath, oEntry] of Object.entries(oEntries)) {
|
|
56
|
+
const oValidated = this.validateUnique(oEntry, sExtension, sFilePath);
|
|
57
|
+
if (oValidated !== void 0) oValid[sFilePath] = oValidated;
|
|
58
|
+
}
|
|
59
|
+
return oValid;
|
|
60
|
+
}
|
|
61
|
+
/** Validates one entry: returns it when valid, `undefined` otherwise */
|
|
62
|
+
static validateUnique(oEntry, sExtension, sFilePath = "") {
|
|
63
|
+
return this.oValidators[sExtension]?.()(oEntry, sFilePath) ? oEntry : void 0;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region ../core/loader/FileReader.ts
|
|
68
|
+
/** Reads and loads files from disk, resolved from `cwd` */
|
|
69
|
+
var FileReader = class {
|
|
70
|
+
static oJiti = createJiti(import.meta.url, { nativeModules: ["@prisma/client"] });
|
|
71
|
+
/** Loads a module via jiti */
|
|
72
|
+
static async loadModule(sFilePath, oOptions = {}) {
|
|
73
|
+
return await this.oJiti.import(this.getAbsolutePath(sFilePath), oOptions);
|
|
74
|
+
}
|
|
75
|
+
/** Reads the contents of a file */
|
|
76
|
+
static loadFile(sFilePath) {
|
|
77
|
+
const sContent = readFileSync(this.getAbsolutePath(sFilePath), "utf-8");
|
|
78
|
+
if (!sContent) throw new Error(`[nano-app] [ERROR] The file ${sFilePath} does not have valid content.`);
|
|
79
|
+
return sContent;
|
|
80
|
+
}
|
|
81
|
+
/** Reads a Prisma schema without its `//` comments */
|
|
82
|
+
static loadSchema(sFilePath) {
|
|
83
|
+
return this.loadFile(sFilePath).replace(/\/\/.*$/gm, "");
|
|
84
|
+
}
|
|
85
|
+
/** Resolves the absolute path of a file */
|
|
86
|
+
static getAbsolutePath(sFilePath) {
|
|
87
|
+
const sAbsolutePath = isAbsolute(sFilePath) ? sFilePath : resolve(process.cwd(), sFilePath);
|
|
88
|
+
if (!existsSync(sAbsolutePath)) throw new Error(`[nano-app] [ERROR] ${sFilePath} not found.`);
|
|
89
|
+
return sAbsolutePath;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region ../core/utils/Formatter.ts
|
|
94
|
+
/** Formats values for logs and messages */
|
|
95
|
+
var Formatter = class {
|
|
96
|
+
/** Message of anything thrown: its `message` when it has one, else the value itself */
|
|
97
|
+
static error(oError) {
|
|
98
|
+
return String(oError?.message ?? oError);
|
|
99
|
+
}
|
|
100
|
+
/** Formats a date for the request log, in UTC: `2026-10-19 12:03:10` */
|
|
101
|
+
static logDate(oDate = /* @__PURE__ */ new Date()) {
|
|
102
|
+
return oDate.toISOString().slice(0, 19).replace("T", " ");
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region ../core/loader/FileLoader.ts
|
|
107
|
+
/** Finds, loads and validates the consumer's files */
|
|
108
|
+
var FileLoader = class {
|
|
109
|
+
/**
|
|
110
|
+
* 1. Validates a `.prisma` file.
|
|
111
|
+
* 2. Loads a generated Prisma client module (`client.ts`, or `client.js`)
|
|
112
|
+
*/
|
|
113
|
+
static async findPrismaClient(sClientPath, sSchemaPath) {
|
|
114
|
+
const sContent = FileReader.loadSchema(sSchemaPath);
|
|
115
|
+
if (!Validator.validateUnique(sContent, "prisma", sSchemaPath)) throw new Error(`[nano-app] [ERROR] ${sSchemaPath} is not a valid Prisma schema.`);
|
|
116
|
+
try {
|
|
117
|
+
return (await FileReader.loadModule(sClientPath))?.PrismaClient;
|
|
118
|
+
} catch (oError) {
|
|
119
|
+
throw new Error(`[nano-app] [ERROR] The Prisma client ${sClientPath} could not be loaded: ${Formatter.error(oError)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Finds `*.route.ts` (or built `.js`) files and returns `{ [subPath]: defaultExport }` */
|
|
123
|
+
static async findMany({ extension, sourceDir }) {
|
|
124
|
+
const sSrcDir = (sourceDir ?? `src/${extension}s`).replace(/^\.\//, "").replace(/\/$/, "");
|
|
125
|
+
const aFilePaths = await glob(`${sSrcDir}/**/*.${extension}.{ts,js}`, {
|
|
126
|
+
posix: true,
|
|
127
|
+
ignore: "**/*.d.ts"
|
|
128
|
+
});
|
|
129
|
+
const oEntries = {};
|
|
130
|
+
for (const sFilePath of aFilePaths) {
|
|
131
|
+
const sFileSubPath = sFilePath.slice(sSrcDir.length).replace(new RegExp(`\\.${extension}\\.(ts|js)$`), "");
|
|
132
|
+
oEntries[sFileSubPath] = await FileReader.loadModule(sFilePath, { default: true });
|
|
133
|
+
}
|
|
134
|
+
return Validator.validate(oEntries, extension);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region const.ts
|
|
139
|
+
/** Package of the official plugins: only its schemas may declare nano_* */
|
|
140
|
+
const OFFICIAL_PLUGINS = "@nano-app/plugins";
|
|
141
|
+
/** Error when the database URL is missing, at startup and on `nano-app push` */
|
|
142
|
+
const MISSING_DATABASE_URL = "[nano-app] [ERROR] The DATABASE_URL environment variable is not defined.";
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/database/Prisma.ts
|
|
145
|
+
/** Runs the consumer's Prisma CLI: generates clients and applies official plugins' migrations */
|
|
146
|
+
var Prisma = class {
|
|
147
|
+
/** Folder of a client and its files, in the project */
|
|
148
|
+
static dir(sName) {
|
|
149
|
+
return join(process.cwd(), ".nano-app", "prisma-clients", sName);
|
|
150
|
+
}
|
|
151
|
+
/** Generates the client of a schema in `.nano-app/prisma-clients/<name>`, unless it is up to date */
|
|
152
|
+
static generate(sName, sSchemaPath) {
|
|
153
|
+
const sDir = this.dir(sName);
|
|
154
|
+
const sSource = FileReader.getAbsolutePath(sSchemaPath);
|
|
155
|
+
const sHash = createHash("sha256").update(readFileSync(sSource)).digest("hex");
|
|
156
|
+
const sSourceFile = join(sDir, "source.json");
|
|
157
|
+
const oCurrent = existsSync(sSourceFile) ? JSON.parse(readFileSync(sSourceFile, "utf-8")) : void 0;
|
|
158
|
+
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}.`);
|
|
159
|
+
if (oCurrent?.schemaPath === sSource && oCurrent.hash === sHash && existsSync(join(sDir, "generated", "client.ts"))) return;
|
|
160
|
+
mkdirSync(sDir, { recursive: true });
|
|
161
|
+
const sSchema = readFileSync(sSource, "utf-8");
|
|
162
|
+
writeFileSync(join(sDir, "schema.prisma"), sSchema.replace(/output\s*=\s*"[^"]*"/, "output = \"./generated\""));
|
|
163
|
+
this.run(sName, [
|
|
164
|
+
"generate",
|
|
165
|
+
"--schema",
|
|
166
|
+
join(sDir, "schema.prisma")
|
|
167
|
+
]);
|
|
168
|
+
writeFileSync(sSourceFile, JSON.stringify({
|
|
169
|
+
schemaPath: sSource,
|
|
170
|
+
hash: sHash
|
|
171
|
+
}));
|
|
172
|
+
console.log(`[nano-app] [INFO] Prisma client "${sName}" generated`);
|
|
173
|
+
}
|
|
174
|
+
/** Applies the pending migrations an official plugin ships next to its schema */
|
|
175
|
+
static migrate(sName, sSchemaPath, sDbSchema) {
|
|
176
|
+
const sMigrations = join(dirname(FileReader.getAbsolutePath(sSchemaPath)), "migrations");
|
|
177
|
+
if (!existsSync(sMigrations)) throw new Error(`[nano-app] [ERROR] The plugin "${sName}" ships no migrations: ${sMigrations}`);
|
|
178
|
+
if (!process.env.DATABASE_URL) throw new Error(MISSING_DATABASE_URL);
|
|
179
|
+
const oUrl = new URL(process.env.DATABASE_URL);
|
|
180
|
+
oUrl.searchParams.set("schema", sDbSchema);
|
|
181
|
+
const sConfig = join(this.dir(sName), "prisma.config.mjs");
|
|
182
|
+
writeFileSync(sConfig, [
|
|
183
|
+
"export default {",
|
|
184
|
+
` schema: ${JSON.stringify(join(this.dir(sName), "schema.prisma"))},`,
|
|
185
|
+
` migrations: { path: ${JSON.stringify(sMigrations)} },`,
|
|
186
|
+
" datasource: { url: process.env.NANO_APP_MIGRATION_URL },",
|
|
187
|
+
"};",
|
|
188
|
+
""
|
|
189
|
+
].join("\n"));
|
|
190
|
+
this.run(sName, [
|
|
191
|
+
"migrate",
|
|
192
|
+
"deploy",
|
|
193
|
+
"--config",
|
|
194
|
+
sConfig
|
|
195
|
+
], { NANO_APP_MIGRATION_URL: oUrl.toString() });
|
|
196
|
+
}
|
|
197
|
+
/** Runs a Prisma command of the consumer's `prisma` package */
|
|
198
|
+
static run(sName, aArgs, oEnv = {}) {
|
|
199
|
+
let sCli;
|
|
200
|
+
try {
|
|
201
|
+
sCli = createRequire(`${process.cwd()}/`).resolve("prisma/build/index.js");
|
|
202
|
+
} catch {
|
|
203
|
+
throw new Error(`[nano-app] [ERROR] The database "${sName}" needs Prisma: install the prisma package.`);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
execFileSync(process.execPath, [sCli, ...aArgs], {
|
|
207
|
+
env: {
|
|
208
|
+
...process.env,
|
|
209
|
+
...oEnv
|
|
210
|
+
},
|
|
211
|
+
stdio: "pipe"
|
|
212
|
+
});
|
|
213
|
+
} catch (oError) {
|
|
214
|
+
const oFailed = oError;
|
|
215
|
+
const sOutput = `${oFailed.stderr ?? ""}${oFailed.stdout ?? ""}`.trim() || Formatter.error(oError);
|
|
216
|
+
throw new Error(`[nano-app] [ERROR] prisma ${aArgs[0]} failed for "${sName}": ${sOutput}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
//#endregion
|
|
221
|
+
export { Formatter as a, FileLoader as i, MISSING_DATABASE_URL as n, FileReader as o, OFFICIAL_PLUGINS as r, Prisma as t };
|
package/build/index.d.ts
CHANGED
|
@@ -1,175 +1,183 @@
|
|
|
1
|
-
import express, { Request,
|
|
2
|
-
|
|
3
|
-
import { Options } from
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type DatabaseConfig = {
|
|
8
|
-
/** Whether the database is enabled. Defaults to false */
|
|
9
|
-
enabled?: boolean;
|
|
10
|
-
/** Prisma schema file (`.prisma`), relative to `cwd` */
|
|
11
|
-
schemaPath: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
1
|
+
import express, { Express, Request, Request as Request$1 } from "express";
|
|
2
|
+
import { CorsOptions } from "cors";
|
|
3
|
+
import { Options } from "express-rate-limit";
|
|
4
|
+
import "dotenv/config";
|
|
5
|
+
import "jiti";
|
|
6
|
+
//#region ../core/types/api.d.ts
|
|
14
7
|
/** API response */
|
|
15
8
|
type ApiResponse<T = any> = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
success?: boolean;
|
|
10
|
+
result?: T;
|
|
11
|
+
message?: string;
|
|
12
|
+
warning?: boolean;
|
|
20
13
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
result?: T;
|
|
24
|
-
message?: string;
|
|
25
|
-
warning?: boolean;
|
|
26
|
-
}>;
|
|
14
|
+
/** Promise of an API response, what a handler returns */
|
|
15
|
+
type ApiPromise<T = any> = Promise<ApiResponse<T>>;
|
|
27
16
|
/** Query parameters sent in the `query` header (JSON). */
|
|
28
17
|
type QueryParams = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
skip?: number;
|
|
19
|
+
top?: number;
|
|
20
|
+
filter?: NanoObject;
|
|
21
|
+
orderby?: Record<string, 'asc' | 'desc'>;
|
|
22
|
+
select?: string;
|
|
34
23
|
};
|
|
35
|
-
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region ../core/types/objects.d.ts
|
|
36
26
|
/** Extra params of handlers and callbacks */
|
|
37
27
|
type Params<DB = any> = Partial<NanoObject & {
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
db?: DB;
|
|
29
|
+
onBeforeResult?: NanoObject;
|
|
40
30
|
}>;
|
|
41
31
|
/** Endpoint handlers */
|
|
42
|
-
type Handler<DB> = (req: Request, params: Params<DB>) => ApiPromise;
|
|
32
|
+
type Handler<DB> = (req: Request$1, params: Params<DB>) => ApiPromise;
|
|
43
33
|
type Handlers<DB = any> = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
GET?: Handler<DB>;
|
|
35
|
+
POST?: Handler<DB>;
|
|
36
|
+
PUT?: Handler<DB>;
|
|
37
|
+
DELETE?: Handler<DB>;
|
|
38
|
+
QUERY?: Handler<DB>;
|
|
48
39
|
};
|
|
49
40
|
/** Callbacks */
|
|
50
41
|
type Callback<DB> = (params: Params<DB>) => Promise<void>;
|
|
51
|
-
type CallbackMethod<DB> = (req: Request, method: string, params: Params<DB>) => Promise<NanoObject>;
|
|
42
|
+
type CallbackMethod<DB> = (req: Request$1, method: string, params: Params<DB>) => Promise<NanoObject>;
|
|
52
43
|
type CallbackMount<DB> = (app: Express, params: Params<DB>) => Promise<void>;
|
|
53
44
|
/** Callbacks of an endpoint. They never receive the app: what affects the whole app goes in the plugin's onMount */
|
|
54
45
|
type Callbacks<DB = any> = {
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
/** Run once, when the endpoint is mounted */
|
|
47
|
+
onInit?: Callback<DB>;
|
|
48
|
+
/** Run before the endpoint is executed */
|
|
49
|
+
onBefore?: CallbackMethod<DB>;
|
|
57
50
|
};
|
|
58
51
|
/** Callbacks of a plugin */
|
|
59
52
|
type PluginCallbacks<DB = any> = {
|
|
60
|
-
|
|
53
|
+
/** Run once with the Express app, after its database and before its endpoints (e.g. app-level middlewares) */
|
|
54
|
+
onMount?: CallbackMount<DB>;
|
|
61
55
|
};
|
|
62
56
|
/** Entry of a route file (`*.route.ts`) */
|
|
63
57
|
type RouteEntry<DB = any> = {
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
/** Express route expression appended to the path (e.g. `/:id`) */
|
|
59
|
+
expression?: string;
|
|
60
|
+
callbacks?: Callbacks<DB>;
|
|
66
61
|
} & Handlers<DB>;
|
|
62
|
+
/** Generic object */
|
|
67
63
|
type NanoObject<T = any> = Record<string, T>;
|
|
68
|
-
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/types/database.d.ts
|
|
66
|
+
/** Database client configuration. */
|
|
67
|
+
type DatabaseConfig = {
|
|
68
|
+
/** Whether the database is enabled. Defaults to false */
|
|
69
|
+
enabled?: boolean;
|
|
70
|
+
/** Prisma schema file (`.prisma`), relative to `cwd` */
|
|
71
|
+
schemaPath: string;
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/types/plugin.d.ts
|
|
69
75
|
/** When a plugin is mounted, relative to the consumer routes */
|
|
70
|
-
type PluginStage =
|
|
76
|
+
type PluginStage = 'before' | 'after';
|
|
71
77
|
/** Plugin configuration: a group of endpoints */
|
|
72
78
|
type PluginConfig<Options = NanoObject> = {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
/** Name of the plugin, same as his database (If have one) */
|
|
80
|
+
name: string;
|
|
81
|
+
/** Directory used to discover the `*.route.ts` files */
|
|
82
|
+
routesDir: string;
|
|
83
|
+
/** Base path prepended to every endpoint, e.g. "/auth" (no trailing slash). Defaults to none */
|
|
84
|
+
basePath?: string;
|
|
85
|
+
/** Mount point relative to the consumer routes. Defaults to "before" */
|
|
86
|
+
stage?: PluginStage;
|
|
87
|
+
/** Prisma client the plugin needs, mounted before its endpoints */
|
|
88
|
+
database?: DatabaseConfig;
|
|
89
|
+
/** Plugin options */
|
|
90
|
+
options?: Options;
|
|
91
|
+
/** Plugin callbacks */
|
|
92
|
+
callbacks?: PluginCallbacks;
|
|
87
93
|
};
|
|
88
94
|
/** The project's plugin: nano-app names it "app" and mounts it between the "before" and "after" plugins */
|
|
89
95
|
type MainPluginConfig<Options = NanoObject> = {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
96
|
+
/** Directory of the project's `*.route.ts` files. Defaults to "src/routes" */
|
|
97
|
+
routesDir?: string;
|
|
98
|
+
/** Base path prepended to every route, e.g. "/api" (no trailing slash). Defaults to none */
|
|
99
|
+
basePath?: string;
|
|
100
|
+
/** The project's database */
|
|
101
|
+
database?: {
|
|
102
|
+
/** Whether the database is enabled. Defaults to false */
|
|
103
|
+
enabled?: boolean;
|
|
104
|
+
/** Prisma schema file, relative to `cwd`. Defaults to "src/prisma/schema.prisma" */
|
|
105
|
+
schemaPath?: string;
|
|
106
|
+
};
|
|
107
|
+
/** Plugin options */
|
|
108
|
+
options?: Options;
|
|
109
|
+
/** Plugin callbacks */
|
|
110
|
+
callbacks?: PluginCallbacks;
|
|
105
111
|
};
|
|
106
|
-
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/types/app.d.ts
|
|
107
114
|
/** Configuration of the framework's built-in packages */
|
|
108
115
|
type PackagesConfig = {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
116
|
+
/** Morgan configuration */
|
|
117
|
+
morgan?: false | {
|
|
118
|
+
/** Morgan tokens */
|
|
119
|
+
tokens?: Record<string, any>;
|
|
120
|
+
/** Morgan format */
|
|
121
|
+
format: string;
|
|
122
|
+
};
|
|
123
|
+
/** CORS configuration */
|
|
124
|
+
cors?: CorsOptions | false;
|
|
125
|
+
/** Helmet configuration */
|
|
126
|
+
helmet?: boolean;
|
|
127
|
+
/** Compression configuration */
|
|
128
|
+
compression?: boolean;
|
|
129
|
+
/** Rate-limit configuration */
|
|
130
|
+
rateLimit?: Partial<Options> | false;
|
|
131
|
+
/** express.json configuration */
|
|
132
|
+
express?: Partial<Parameters<typeof express.json>[0]> | false;
|
|
133
|
+
/** urlencoded configuration */
|
|
134
|
+
urlencoded?: Partial<Parameters<typeof express.urlencoded>[0]> | false;
|
|
128
135
|
};
|
|
129
136
|
/** Application configuration */
|
|
130
137
|
type NanoAppConfig = {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
/** Listener run once the server is listening */
|
|
139
|
+
listener?: () => void;
|
|
140
|
+
/** Application port. Defaults to 3000 */
|
|
141
|
+
port?: number;
|
|
142
|
+
/** Values applied with Express `app.set()` (e.g. `{ "trust proxy": 1 }`) */
|
|
143
|
+
settings?: Record<string, unknown>;
|
|
144
|
+
/** Default middlewares configuration */
|
|
145
|
+
packages?: PackagesConfig;
|
|
146
|
+
/** Plugins to mount. Defaults to [] */
|
|
147
|
+
plugins?: PluginConfig[];
|
|
148
|
+
/** Main plugin to mount. */
|
|
149
|
+
mainPlugin?: MainPluginConfig;
|
|
143
150
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
mainPlugin: PluginConfig;
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/app/config.d.ts
|
|
153
|
+
/** Merges the consumer configuration over the initial one; the main plugin is always named "app" */
|
|
154
|
+
declare function mergeConfig(oConfig?: NanoAppConfig): NanoAppConfig & Required<Pick<NanoAppConfig, 'settings' | 'plugins' | 'packages'>> & {
|
|
155
|
+
mainPlugin: PluginConfig;
|
|
150
156
|
};
|
|
151
|
-
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/app/NanoApp.d.ts
|
|
152
159
|
/** Main NanoApp class */
|
|
153
160
|
declare class NanoApp {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
protected oApp: Express;
|
|
162
|
+
protected oConfig: ReturnType<typeof mergeConfig>;
|
|
163
|
+
constructor(oConfig?: NanoAppConfig);
|
|
164
|
+
/** Starts the application */
|
|
165
|
+
private start;
|
|
166
|
+
/** Initializes the application in the right order */
|
|
167
|
+
private initialize;
|
|
168
|
+
/** Adds the packages according to the configuration */
|
|
169
|
+
private setPackages;
|
|
162
170
|
}
|
|
163
|
-
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/app/creators.d.ts
|
|
164
173
|
/** Creates a new NanoApp instance */
|
|
165
|
-
declare function createNanoApp(
|
|
174
|
+
export declare function createNanoApp(oConfig?: NanoAppConfig): NanoApp;
|
|
166
175
|
/** Creates and types a route object. */
|
|
167
|
-
declare function createRoute<DB>(
|
|
176
|
+
export declare function createRoute<DB>(oRoute: RouteEntry<DB>): RouteEntry<DB>;
|
|
168
177
|
/** Creates a plugin: a function of the plugin's own options that returns its configuration. */
|
|
169
178
|
type Creator<Options> = (options?: Options) => PluginConfig<Options>;
|
|
170
|
-
declare function createPlugin<Options>(
|
|
179
|
+
export declare function createPlugin<Options>(fnCreator: Creator<Options>): Creator<Options>;
|
|
171
180
|
/** Creates and types the callbacks of a plugin. */
|
|
172
|
-
declare function createPluginCallbacks<DB>(
|
|
173
|
-
|
|
174
|
-
export { NanoApp,
|
|
175
|
-
export type { ApiPromise, ApiResponse, Callbacks, DatabaseConfig, Handler, Handlers, MainPluginConfig, NanoAppConfig, NanoObject, PackagesConfig, Params, PluginCallbacks, PluginConfig, PluginStage, QueryParams, RouteEntry };
|
|
181
|
+
export declare function createPluginCallbacks<DB>(oCallbacks: PluginCallbacks<DB>): PluginCallbacks<DB>;
|
|
182
|
+
//#endregion
|
|
183
|
+
export { type ApiPromise, type ApiResponse, type Callbacks, type DatabaseConfig, type Handler, type Handlers, type MainPluginConfig, NanoApp, type NanoAppConfig, type NanoObject, type PackagesConfig, type Params, type PluginCallbacks, type PluginConfig, type PluginStage, type QueryParams, type Request, type RouteEntry };
|