@nano-app/backend 0.6.5 → 0.7.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/LICENSE +21 -0
- package/README.md +233 -0
- package/build/index.d.ts +149 -0
- package/build/index.js +544 -0
- package/build/nano-app.js +41 -0
- package/package.json +67 -27
- package/dist/app.d.ts +0 -9
- package/dist/app.js +0 -74
- package/dist/auth/ApiKeys.d.ts +0 -25
- package/dist/auth/ApiKeys.js +0 -146
- package/dist/auth/ApiKeysEndpoint.d.ts +0 -19
- package/dist/auth/ApiKeysEndpoint.js +0 -36
- package/dist/http/Endpoint.d.ts +0 -17
- package/dist/http/Endpoint.js +0 -59
- package/dist/http/Router.d.ts +0 -6
- package/dist/http/Router.js +0 -20
- package/dist/index.d.ts +0 -12
- package/dist/index.js +0 -28
- package/dist/odata/Entities.d.ts +0 -10
- package/dist/odata/Entities.js +0 -52
- package/dist/odata/QueryBuilder.d.ts +0 -10
- package/dist/odata/QueryBuilder.js +0 -60
- package/dist/prisma/Client.d.ts +0 -12
- package/dist/prisma/Client.js +0 -34
- package/dist/types/api.d.ts +0 -6
- package/dist/types/api.js +0 -2
- package/dist/types/app.d.ts +0 -29
- package/dist/types/app.js +0 -2
- package/dist/types/odata.d.ts +0 -8
- package/dist/types/odata.js +0 -2
- package/dist/utils/Formatter.d.ts +0 -6
- package/dist/utils/Formatter.js +0 -31
package/build/index.js
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
// src/app/NanoApp.ts
|
|
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";
|
|
7
|
+
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
|
|
92
|
+
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
|
+
}
|
|
121
|
+
};
|
|
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
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ../core/utils/Formatter.ts
|
|
193
|
+
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
|
+
// }
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// src/routes/Endpoint.ts
|
|
222
|
+
var Endpoint = class {
|
|
223
|
+
methods;
|
|
224
|
+
/** Allowed methods */
|
|
225
|
+
constructor(methods) {
|
|
226
|
+
this.methods = methods;
|
|
227
|
+
}
|
|
228
|
+
/** Handler for all methods */
|
|
229
|
+
getHandler = (db) => (req, res, next) => {
|
|
230
|
+
const method = req.method.toUpperCase();
|
|
231
|
+
if (this.methods[method]) {
|
|
232
|
+
this.methods[method](req, db).then((response) => {
|
|
233
|
+
res.status(response.success ? 200 : 400).json(response);
|
|
234
|
+
}).catch(next);
|
|
235
|
+
} else {
|
|
236
|
+
res.status(405).json({ success: false, message: `Method ${method} not implemented` });
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// src/routes/Router.ts
|
|
242
|
+
var Router = class {
|
|
243
|
+
routes = {};
|
|
244
|
+
routesDir;
|
|
245
|
+
basePath;
|
|
246
|
+
constructor(routesDir, basePath) {
|
|
247
|
+
this.routesDir = routesDir;
|
|
248
|
+
this.basePath = basePath ?? "/";
|
|
249
|
+
}
|
|
250
|
+
async mount() {
|
|
251
|
+
if (!!this.routesDir) {
|
|
252
|
+
this.routes = await FileLoader.findMany({
|
|
253
|
+
sourceDir: this.routesDir,
|
|
254
|
+
extension: "route"
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return Object.entries(this.routes).map(([filePath, { expression, ...methods }]) => {
|
|
258
|
+
const path = this.createPath(filePath, expression);
|
|
259
|
+
console.log(`[nano-app] [INFO] ${path}`);
|
|
260
|
+
return { path, endpoint: new Endpoint(methods) };
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
createPath(endpoint, expression) {
|
|
264
|
+
const isIndex = endpoint === "/index";
|
|
265
|
+
let path = isIndex ? "/" : endpoint;
|
|
266
|
+
if (!!expression) {
|
|
267
|
+
path = `${isIndex ? "" : endpoint.replace(/\/+$/, "")}/${expression.replace(/^\/+/, "")}`;
|
|
268
|
+
}
|
|
269
|
+
const basePath = this.basePath.replace(/\/+$/, "");
|
|
270
|
+
if (basePath) {
|
|
271
|
+
path = path === "/" ? basePath : `${basePath}${path}`;
|
|
272
|
+
}
|
|
273
|
+
return path;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/database/Database.ts
|
|
278
|
+
import { existsSync as existsSync2 } from "fs";
|
|
279
|
+
|
|
280
|
+
// const.ts
|
|
281
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
282
|
+
import { fileURLToPath } from "url";
|
|
283
|
+
var BASE_DIR = dirname2(fileURLToPath(import.meta.url));
|
|
284
|
+
var getDir = (...parts) => resolve3(BASE_DIR, ...parts);
|
|
285
|
+
|
|
286
|
+
// src/database/Database.ts
|
|
287
|
+
var Database = class {
|
|
288
|
+
static adapter;
|
|
289
|
+
/** Mounted clients, by name */
|
|
290
|
+
static clients = {};
|
|
291
|
+
/** Initializes the Database client */
|
|
292
|
+
static async init() {
|
|
293
|
+
const url = process.env.DATABASE_URL;
|
|
294
|
+
if (!url) {
|
|
295
|
+
throw new Error("[nano-app] [ERROR] The DATABASE_URL environment variable is not defined.");
|
|
296
|
+
}
|
|
297
|
+
const { PrismaPg } = await import("@prisma/adapter-pg");
|
|
298
|
+
this.adapter = new PrismaPg({ connectionString: url });
|
|
299
|
+
}
|
|
300
|
+
/** Gets a client by name */
|
|
301
|
+
static get(name) {
|
|
302
|
+
return this.clients[name];
|
|
303
|
+
}
|
|
304
|
+
/** Mounts a generated Prisma client */
|
|
305
|
+
static async mount(name, { schemaPath }) {
|
|
306
|
+
if (this.get(name)) {
|
|
307
|
+
throw new Error(`[nano-app] [ERROR] The database "${name}" is already mounted.`);
|
|
308
|
+
}
|
|
309
|
+
const clientPath = `${process.cwd()}/.nano-app/prisma-clients/${name}/generated/client.ts`;
|
|
310
|
+
if (!existsSync2(clientPath)) {
|
|
311
|
+
throw new Error(`[nano-app] [ERROR] The prisma client of "${name}" does not exist. Run \`npx nano-app generate ${name}\`.`);
|
|
312
|
+
}
|
|
313
|
+
if (!this.adapter) await this.init();
|
|
314
|
+
const PrismaClient = await FileLoader.findPrismaClient(clientPath, schemaPath);
|
|
315
|
+
const isPlugin = schemaPath.startsWith(getDir("plugins"));
|
|
316
|
+
const declared = FileReader.loadFile(schemaPath).replace(/\/\/.*$/gm, "").match(/datasource\s+\w+\s*\{[^}]*schemas\s*=\s*\[([^\]]*)\]/s)?.[1] ?? "";
|
|
317
|
+
const schemas = [...declared.matchAll(/["']([^"']+)["']/g)].map(([, schema]) => schema);
|
|
318
|
+
if (isPlugin ? !schemas.length || schemas.some((schema) => !schema.startsWith("nano_")) : schemas.some((schema) => schema.startsWith("nano_"))) {
|
|
319
|
+
throw new Error(`[nano-app] [ERROR] ${schemaPath}: ${isPlugin ? "a plugin declares only nano_* schemas" : "the nano_* schemas are reserved for plugins"}.`);
|
|
320
|
+
}
|
|
321
|
+
let client;
|
|
322
|
+
try {
|
|
323
|
+
client = new PrismaClient({ adapter: this.adapter });
|
|
324
|
+
} catch (error) {
|
|
325
|
+
throw new Error(`[nano-app] [ERROR] Cannot instantiate the database "${name}" from ${clientPath}: ${error?.message ?? error}`);
|
|
326
|
+
}
|
|
327
|
+
if (isPlugin) {
|
|
328
|
+
for (const [, model] of FileReader.loadFile(schemaPath).matchAll(/^\s*model\s+(\w+)/gm)) {
|
|
329
|
+
await client[model[0].toLowerCase() + model.slice(1)].findFirst().catch((error) => {
|
|
330
|
+
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}\`.`);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
this.clients[name] = client;
|
|
335
|
+
console.log(`[nano-app] [INFO] Database "${name}" mounted`);
|
|
336
|
+
return client;
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/routes/Plugin.ts
|
|
341
|
+
var Plugin = class {
|
|
342
|
+
config;
|
|
343
|
+
/** Creates a new Plugin instance */
|
|
344
|
+
constructor(config) {
|
|
345
|
+
this.config = config;
|
|
346
|
+
}
|
|
347
|
+
/** Mounts its endpoints on express app */
|
|
348
|
+
async mount(app) {
|
|
349
|
+
const { name, routesDir, basePath, database, init } = this.config;
|
|
350
|
+
const router = new Router(routesDir, basePath);
|
|
351
|
+
const db = database?.enabled ? await Database.mount(name, database) : void 0;
|
|
352
|
+
await init?.(app, db);
|
|
353
|
+
for (const { path, endpoint } of await router.mount()) app.all(path, endpoint.getHandler(db));
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// src/app/NanoApp.ts
|
|
358
|
+
import morgan from "morgan";
|
|
359
|
+
import cors from "cors";
|
|
360
|
+
import helmet from "helmet";
|
|
361
|
+
import compression from "compression";
|
|
362
|
+
import rateLimit from "express-rate-limit";
|
|
363
|
+
import "dotenv/config";
|
|
364
|
+
|
|
365
|
+
// src/app/config.ts
|
|
366
|
+
var INITIAL_CONFIG = {
|
|
367
|
+
/** Initial application configuration */
|
|
368
|
+
port: 3e3,
|
|
369
|
+
/** Application listener */
|
|
370
|
+
listener: () => {
|
|
371
|
+
},
|
|
372
|
+
/** Express settings */
|
|
373
|
+
settings: {},
|
|
374
|
+
/** Initial middlewares */
|
|
375
|
+
packages: {
|
|
376
|
+
morgan: {
|
|
377
|
+
tokens: {
|
|
378
|
+
timestamp: () => Formatter.logDate()
|
|
379
|
+
},
|
|
380
|
+
format: ":timestamp | :method :url :status"
|
|
381
|
+
},
|
|
382
|
+
cors: {},
|
|
383
|
+
helmet: true,
|
|
384
|
+
compression: true,
|
|
385
|
+
rateLimit: {
|
|
386
|
+
max: 1e3,
|
|
387
|
+
message: "Too many requests, please try again later."
|
|
388
|
+
},
|
|
389
|
+
express: {
|
|
390
|
+
limit: "10kb"
|
|
391
|
+
},
|
|
392
|
+
urlencoded: {
|
|
393
|
+
extended: false
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
/** Main plugin to mount */
|
|
397
|
+
mainPlugin: {
|
|
398
|
+
routesDir: "src/routes",
|
|
399
|
+
basePath: "/",
|
|
400
|
+
database: {
|
|
401
|
+
enabled: false,
|
|
402
|
+
schemaPath: "src/prisma/schema.prisma"
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
/** Plugins around the consumer routes */
|
|
406
|
+
plugins: []
|
|
407
|
+
};
|
|
408
|
+
function mergeConfig(config = {}) {
|
|
409
|
+
return {
|
|
410
|
+
...INITIAL_CONFIG,
|
|
411
|
+
...config,
|
|
412
|
+
settings: config.settings ?? INITIAL_CONFIG.settings,
|
|
413
|
+
packages: {
|
|
414
|
+
...INITIAL_CONFIG.packages,
|
|
415
|
+
...config.packages || {},
|
|
416
|
+
morgan: config.packages?.morgan ?? INITIAL_CONFIG.packages.morgan,
|
|
417
|
+
cors: config.packages?.cors ?? INITIAL_CONFIG.packages.cors,
|
|
418
|
+
helmet: config.packages?.helmet ?? INITIAL_CONFIG.packages.helmet,
|
|
419
|
+
compression: config.packages?.compression ?? INITIAL_CONFIG.packages.compression,
|
|
420
|
+
rateLimit: config.packages?.rateLimit ?? INITIAL_CONFIG.packages.rateLimit,
|
|
421
|
+
express: config.packages?.express ?? INITIAL_CONFIG.packages.express,
|
|
422
|
+
urlencoded: config.packages?.urlencoded ?? INITIAL_CONFIG.packages.urlencoded
|
|
423
|
+
},
|
|
424
|
+
mainPlugin: {
|
|
425
|
+
...INITIAL_CONFIG.mainPlugin,
|
|
426
|
+
...config.mainPlugin,
|
|
427
|
+
name: "app",
|
|
428
|
+
database: {
|
|
429
|
+
...INITIAL_CONFIG.mainPlugin.database,
|
|
430
|
+
...config.mainPlugin?.database
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
plugins: config.plugins ?? INITIAL_CONFIG.plugins
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/app/errors.ts
|
|
438
|
+
function normalize(error) {
|
|
439
|
+
if (typeof error === "string") {
|
|
440
|
+
return { status: 400, body: { success: false, message: error } };
|
|
441
|
+
}
|
|
442
|
+
const { status, message, warning } = error ?? {};
|
|
443
|
+
return {
|
|
444
|
+
status: status ?? 500,
|
|
445
|
+
body: {
|
|
446
|
+
success: false,
|
|
447
|
+
warning: warning ?? false,
|
|
448
|
+
message: message || "Internal server error"
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
var notFoundHandler = (req, _res, next) => {
|
|
453
|
+
next({ status: 404, message: `${req.path} not found` });
|
|
454
|
+
};
|
|
455
|
+
var errorHandler = (error, _req, res, _next) => {
|
|
456
|
+
const { status, body } = normalize(error);
|
|
457
|
+
if (status >= 500) {
|
|
458
|
+
console.error("[nano-app] [ERROR]", error);
|
|
459
|
+
}
|
|
460
|
+
res.status(status).json(body);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/app/NanoApp.ts
|
|
464
|
+
var NanoApp = class {
|
|
465
|
+
app;
|
|
466
|
+
config;
|
|
467
|
+
constructor(config = {}) {
|
|
468
|
+
this.app = express();
|
|
469
|
+
this.config = mergeConfig(config);
|
|
470
|
+
this.start();
|
|
471
|
+
}
|
|
472
|
+
/** Starts the application */
|
|
473
|
+
async start() {
|
|
474
|
+
try {
|
|
475
|
+
await this.initialize();
|
|
476
|
+
this.app.listen(Number(this.config.port), () => {
|
|
477
|
+
this.config.listener?.();
|
|
478
|
+
console.log(`
|
|
479
|
+
[nano-app] [INFO] Nano App running on port ${this.config.port}
|
|
480
|
+
`);
|
|
481
|
+
});
|
|
482
|
+
return this;
|
|
483
|
+
} catch (error) {
|
|
484
|
+
console.error("[nano-app] [ERROR] Error trying to start the server.", error);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/** Initializes the application in the right order */
|
|
489
|
+
async initialize() {
|
|
490
|
+
Object.entries(this.config.settings ?? {}).forEach(([key, value]) => this.app.set(key, value));
|
|
491
|
+
this.setPackages(this.config.packages);
|
|
492
|
+
const oPlugins = { before: [], after: [] };
|
|
493
|
+
this.config.plugins?.forEach((plugin) => {
|
|
494
|
+
oPlugins[plugin.stage ?? "before"].push(plugin);
|
|
495
|
+
});
|
|
496
|
+
for (const plugin of oPlugins.before) await new Plugin(plugin).mount(this.app);
|
|
497
|
+
await new Plugin(this.config.mainPlugin).mount(this.app);
|
|
498
|
+
for (const plugin of oPlugins.after) await new Plugin(plugin).mount(this.app);
|
|
499
|
+
this.app.use(notFoundHandler);
|
|
500
|
+
this.app.use(errorHandler);
|
|
501
|
+
}
|
|
502
|
+
/* Adds the packages according to the configuration */
|
|
503
|
+
setPackages(config = {}) {
|
|
504
|
+
if (config.morgan) {
|
|
505
|
+
Object.entries(config.morgan.tokens ?? {}).forEach(([key, value]) => morgan.token(key, value));
|
|
506
|
+
this.app.use(morgan(config.morgan.format));
|
|
507
|
+
}
|
|
508
|
+
if (config.cors) {
|
|
509
|
+
this.app.use(cors(config.cors));
|
|
510
|
+
}
|
|
511
|
+
if (config.helmet) {
|
|
512
|
+
this.app.use(helmet());
|
|
513
|
+
}
|
|
514
|
+
if (config.compression) {
|
|
515
|
+
this.app.use(compression());
|
|
516
|
+
}
|
|
517
|
+
if (config.rateLimit) {
|
|
518
|
+
this.app.use(rateLimit(config.rateLimit));
|
|
519
|
+
}
|
|
520
|
+
if (config.express) {
|
|
521
|
+
this.app.use(express.json(config.express));
|
|
522
|
+
}
|
|
523
|
+
if (config.urlencoded) {
|
|
524
|
+
this.app.use(express.urlencoded(config.urlencoded));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// src/app/creators.ts
|
|
530
|
+
function createNanoApp(config) {
|
|
531
|
+
return new NanoApp(config);
|
|
532
|
+
}
|
|
533
|
+
function createRoute(route) {
|
|
534
|
+
return route;
|
|
535
|
+
}
|
|
536
|
+
function createPlugin(creator) {
|
|
537
|
+
return creator;
|
|
538
|
+
}
|
|
539
|
+
export {
|
|
540
|
+
NanoApp,
|
|
541
|
+
createNanoApp,
|
|
542
|
+
createPlugin,
|
|
543
|
+
createRoute
|
|
544
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/nano-app.ts
|
|
4
|
+
import "dotenv/config";
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
|
|
9
|
+
// const.ts
|
|
10
|
+
import { dirname, resolve } from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
var BASE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
var getDir = (...parts) => resolve(BASE_DIR, ...parts);
|
|
14
|
+
|
|
15
|
+
// bin/nano-app.ts
|
|
16
|
+
var [command, ...args] = process.argv.slice(2);
|
|
17
|
+
var prisma = (...prismaArgs) => execFileSync(process.execPath, [createRequire(`${process.cwd()}/`).resolve("prisma/build/index.js"), ...prismaArgs], { stdio: "inherit" });
|
|
18
|
+
try {
|
|
19
|
+
if (command !== "generate" && command !== "push") {
|
|
20
|
+
prisma(...process.argv.slice(2));
|
|
21
|
+
} else {
|
|
22
|
+
const schemaFlag = args.indexOf("--schema");
|
|
23
|
+
const appSchema = schemaFlag >= 0 ? args.splice(schemaFlag, 2)[1] : "src/prisma/schema.prisma";
|
|
24
|
+
const names = args.filter((arg) => !arg.startsWith("-"));
|
|
25
|
+
const flags = args.filter((arg) => arg.startsWith("-"));
|
|
26
|
+
for (const name of names.length ? names : ["app"]) {
|
|
27
|
+
const source = name === "app" ? appSchema : getDir("plugins", name, "schema.prisma");
|
|
28
|
+
if (!existsSync(source)) {
|
|
29
|
+
console.error(`[nano-app] [ERROR] "${name}" has no schema: ${source}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const dir = `${process.cwd()}/.nano-app/prisma-clients/${name}`;
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
writeFileSync(`${dir}/schema.prisma`, readFileSync(source, "utf-8").replace(/output\s*=\s*"[^"]*"/, 'output = "./generated"'));
|
|
35
|
+
prisma("generate", "--schema", `${dir}/schema.prisma`);
|
|
36
|
+
if (command === "push") prisma("db", "push", "--schema", `${dir}/schema.prisma`, "--url", process.env.DATABASE_URL ?? "", ...flags);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
process.exit(error?.status ?? 1);
|
|
41
|
+
}
|