@nano-app/backend 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # @nano-app/backend
2
2
 
3
+ [![backend](https://img.shields.io/npm/v/@nano-app/backend?label=backend)](https://www.npmjs.com/package/@nano-app/backend)
4
+
3
5
  Preconfigured Express server with **file-convention routes** and **plugins**: it mounts the base middlewares, discovers your routes, formats every error as JSON, and groups extra endpoints in plugins.
4
6
 
5
- - Node ≥ 20, ESM.
7
+ - Node ≥ 22, ESM.
6
8
  - Optional PostgreSQL layer through your generated Prisma client. Disabled by default: without it no database code is loaded.
7
9
 
8
10
  ```bash
9
- npm install @nano-app/backend
11
+ pnpm add @nano-app/backend
10
12
  ```
11
13
 
12
14
  ## Quick start
@@ -22,17 +24,17 @@ src/
22
24
 
23
25
  ```ts
24
26
  // src/app.ts
25
- import { createNanoApp } from "@nano-app/backend";
27
+ import { createNanoApp } from '@nano-app/backend';
26
28
 
27
29
  createNanoApp({ port: 3000 });
28
30
  ```
29
31
 
30
32
  ```ts
31
33
  // src/routes/health.route.ts
32
- import { createRoute } from "@nano-app/backend";
34
+ import { createRoute } from '@nano-app/backend';
33
35
 
34
36
  export default createRoute({
35
- GET: async () => ({ success: true, result: { status: "ok" } }),
37
+ GET: async () => ({ success: true, result: { status: 'ok' } }),
36
38
  });
37
39
  ```
38
40
 
@@ -50,9 +52,9 @@ Each `*.route.ts` under `mainPlugin.routesDir` default-exports an object with th
50
52
 
51
53
  ```ts
52
54
  export default createRoute({
53
- expression: "/:id", // → /users/:id
55
+ expression: '/:id', // → /users/:id
54
56
  GET: async req => ({ success: true, result: { id: req.params.id } }),
55
- DELETE: async req => ({ success: true, message: "Deleted" }),
57
+ DELETE: async req => ({ success: true, message: 'Deleted' }),
56
58
  });
57
59
  ```
58
60
 
@@ -83,13 +85,13 @@ Every option is optional.
83
85
  | `mainPlugin` | `{ routesDir: "src/routes" }` | Your routes (`routesDir`, relative to `cwd`), `basePath` (e.g. `/api`, no trailing slash) and database: see [Database](#database). |
84
86
  | `plugins` | `[]` | Plugins mounted around your routes, in order: see [Plugins](#plugins). |
85
87
 
86
- `packages` accepts `morgan`, `cors`, `helmet`, `compression`, `rateLimit`, `express` (`express.json` options) and `urlencoded`. The merge is **per section**: passing one key keeps the others; pass `false`/`undefined` to disable one.
88
+ `packages` accepts `morgan`, `cors`, `helmet`, `compression`, `rateLimit`, `express` (`express.json` options), `urlencoded` and `betterAuth` (off by default: see [Better Auth](#better-auth)). The merge is **per section**: passing one key keeps the others; pass `false`/`undefined` to disable one.
87
89
 
88
90
  ```ts
89
91
  createNanoApp({
90
- settings: { "trust proxy": 1 },
92
+ settings: { 'trust proxy': 1 },
91
93
  packages: {
92
- cors: { origin: "https://my-app.com" },
94
+ cors: { origin: 'https://my-app.com' },
93
95
  helmet: false,
94
96
  rateLimit: { windowMs: 60_000, max: 120 },
95
97
  },
@@ -101,7 +103,7 @@ createNanoApp({
101
103
  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
104
 
103
105
  ```bash
104
- npm install prisma @prisma/client @prisma/adapter-pg pg # prisma too: the startup generates the client
106
+ pnpm add prisma @prisma/client @prisma/adapter-pg pg # prisma too: the startup generates the client
105
107
  ```
106
108
 
107
109
  ```prisma
@@ -128,8 +130,8 @@ createNanoApp({ mainPlugin: { database: { enabled: true } } });
128
130
 
129
131
  ```ts
130
132
  // src/routes/users.route.ts
131
- import { createRoute } from "@nano-app/backend";
132
- import type { PrismaClient } from "../../.nano-app/prisma-clients/app/generated/client";
133
+ import { createRoute } from '@nano-app/backend';
134
+ import type { PrismaClient } from '../../.nano-app/prisma-clients/app/generated/client';
133
135
 
134
136
  export default createRoute<PrismaClient>({
135
137
  GET: async (_req, { db }) => ({ success: true, result: await db!.user.findMany() }),
@@ -143,33 +145,78 @@ export default createRoute<PrismaClient>({
143
145
 
144
146
  - Each key falls back to its default, so `{ enabled: true }` is enough with the default layout.
145
147
  - 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.
148
+ - The client exists after the first startup: start the app once before your editor or `tsc` can import its types. In a container, `.nano-app/` must be writable.
147
149
  - 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
150
  - The `nano_` prefix is reserved for the official plugins (`@nano-app/plugins`): a datasource schema of yours starting with it fails the startup.
149
151
  - Pass your generated `PrismaClient` to `createRoute<PrismaClient>` to type `db`; otherwise it is `any`, and without an enabled database `undefined`.
150
152
  - For several PostgreSQL schemas, declare `schemas = ["public", "billing"]` in the datasource and `@@schema("billing")` on each model; one client covers them all.
151
153
  - Prisma is loaded only when your database or a mounted plugin's is enabled.
152
154
 
155
+ ## Better Auth
156
+
157
+ Users, sign-in and sessions with [Better Auth](https://www.better-auth.com), on your database. Off until `packages.betterAuth` gets its options (everything but `database`: nano-app passes your Prisma client).
158
+
159
+ ```bash
160
+ pnpm add better-auth
161
+ ```
162
+
163
+ ```ts
164
+ // src/auth.ts: the options, and an instance only for the Better Auth CLI
165
+ import { betterAuth, type BetterAuthOptions } from 'better-auth';
166
+
167
+ export const authOptions = { emailAndPassword: { enabled: true } } satisfies BetterAuthOptions;
168
+ export const auth = betterAuth(authOptions);
169
+ ```
170
+
171
+ ```ts
172
+ // src/app.ts
173
+ import { createNanoApp } from '@nano-app/backend';
174
+ import { authOptions } from './auth';
175
+
176
+ createNanoApp({
177
+ packages: { betterAuth: authOptions },
178
+ mainPlugin: { database: { enabled: true } },
179
+ });
180
+ ```
181
+
182
+ Its tables are yours, like the rest of your schema. Add its models with its CLI, then apply them with Prisma; run both again when you change the options:
183
+
184
+ ```bash
185
+ npx auth generate --config src/auth.ts --adapter prisma --dialect postgresql --output src/prisma/schema.prisma
186
+ npx prisma migrate dev
187
+ ```
188
+
189
+ - It needs the database: `packages.betterAuth` without `mainPlugin.database.enabled` fails at startup.
190
+ - Startup also fails if your schema lacks a model the options need (`user`, `session`, ...), naming them.
191
+ - Its endpoints are mounted with your routes, at its `basePath` (default `/api/auth`), and stay public.
192
+ - Every route of yours and of the `after` plugins needs a session: without one, 401. The `before` plugins stay public (a landing, an about page).
193
+ - Handlers and `onBefore` receive the session in `params.session` (`{ user, session }`).
194
+ - Sign-up works as Better Auth sets it: with `emailAndPassword` anyone can sign up and reach your routes. Close it with `disableSignUp: true`, or check roles in your handlers.
195
+ - Relate your models to `User` in the same schema, as any Prisma relation. Extra user fields go in its `user.additionalFields`.
196
+ - With several PostgreSQL schemas, add `@@schema(...)` to the generated models.
197
+ - Set `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` in the environment, as Better Auth asks.
198
+ - Better Auth is loaded only when it is on.
199
+
153
200
  ## Plugins
154
201
 
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.
202
+ 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 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.
156
203
 
157
204
  ```ts
158
205
  // src/plugins/hello/index.ts
159
- import { createPlugin } from "@nano-app/backend";
160
- import { dirname, resolve } from "node:path";
161
- import { fileURLToPath } from "node:url";
206
+ import { createPlugin } from '@nano-app/backend';
207
+ import { dirname, resolve } from 'node:path';
208
+ import { fileURLToPath } from 'node:url';
162
209
 
163
210
  export default createPlugin((options?: { basePath?: string }) => ({
164
- name: "hello",
165
- basePath: options?.basePath ?? "/hello",
166
- routesDir: resolve(dirname(fileURLToPath(import.meta.url)), "routes"),
211
+ name: 'hello',
212
+ basePath: options?.basePath ?? '/hello',
213
+ routesDir: resolve(dirname(fileURLToPath(import.meta.url)), 'routes'),
167
214
  }));
168
215
  ```
169
216
 
170
217
  ```ts
171
218
  // src/plugins/hello/routes/index.route.ts → /hello
172
- import { createRoute } from "@nano-app/backend";
219
+ import { createRoute } from '@nano-app/backend';
173
220
 
174
221
  export default createRoute({
175
222
  GET: async () => ({ success: true, result: { ok: true } }),
@@ -178,8 +225,8 @@ export default createRoute({
178
225
 
179
226
  ```ts
180
227
  // src/app.ts
181
- import { createNanoApp } from "@nano-app/backend";
182
- import hello from "./plugins/hello";
228
+ import { createNanoApp } from '@nano-app/backend';
229
+ import hello from './plugins/hello';
183
230
 
184
231
  createNanoApp({ plugins: [hello()] });
185
232
  ```
@@ -198,12 +245,12 @@ Two levels. Only the plugin's receives the Express app, so nothing app-wide depe
198
245
 
199
246
  ```ts
200
247
  export default createPlugin((options?: { publicPaths?: string[] }) => ({
201
- name: "guard",
202
- routesDir: resolve(dirname(fileURLToPath(import.meta.url)), "routes"),
248
+ name: 'guard',
249
+ routesDir: resolve(dirname(fileURLToPath(import.meta.url)), 'routes'),
203
250
  options: { publicPaths: options?.publicPaths ?? [] },
204
251
  callbacks: {
205
252
  onMount: async (app, { publicPaths }) => {
206
- app.use((req, _res, next) => next(publicPaths.includes(req.path) ? undefined : { status: 401, message: "Unauthorized" }));
253
+ app.use((req, _res, next) => next(publicPaths.includes(req.path) ? undefined : { status: 401, message: 'Unauthorized' }));
207
254
  },
208
255
  },
209
256
  }));
@@ -220,9 +267,9 @@ A plugin with a database follows the same rules, with its client in `.nano-app/p
220
267
  The framework mounts the 404 and the error handler: an unmatched request becomes a `404`, and anything a handler throws becomes an `ApiResponse`.
221
268
 
222
269
  ```ts
223
- throw { status: 409, message: "Already exists" }; // → 409 { success: false, warning: false, message: "Already exists" }
224
- throw "Field name is required"; // → 400 { success: false, message: "Field name is required" }
225
- throw new Error("boom"); // → 500, logged to stderr
270
+ throw { status: 409, message: 'Already exists' }; // → 409 { success: false, warning: false, message: "Already exists" }
271
+ throw 'Field name is required'; // → 400 { success: false, message: "Field name is required" }
272
+ throw new Error('boom'); // → 500, logged to stderr
226
273
  GET /unknown // → 404 { success: false, warning: false, message: "/unknown not found" }
227
274
  ```
228
275
 
@@ -235,7 +282,7 @@ GET /unknown // → 404 { success: false,
235
282
  | `createPlugin(creator)` | Types a plugin factory and its options. |
236
283
  | `createPluginCallbacks(callbacks)` | Types the callbacks of a plugin (`createPluginCallbacks<PrismaClient>` types `db`), e.g. to keep them in their own file. |
237
284
 
238
- Also exported: the configuration types (`NanoAppConfig`, `PackagesConfig`, `PluginConfig`, `PluginStage`, `MainPluginConfig`, `DatabaseConfig`), the Express `Request` type and the HTTP contract (`ApiResponse`, `ApiPromise`, `QueryParams`, `RouteEntry`, `Handler`, `Handlers`, `NanoObject`) and the callback types (`Params`, `Callbacks`, `PluginCallbacks`).
285
+ Also exported: the `NanoApp` class that `createNanoApp` returns, the configuration types (`NanoAppConfig`, `PackagesConfig`, `PluginConfig`, `PluginStage`, `MainPluginConfig`, `DatabaseConfig`), the Express `Request` type and the HTTP contract (`ApiResponse`, `ApiPromise`, `QueryParams`, `RouteEntry`, `Handler`, `Handlers`, `NanoObject`) and the callback types (`Params`, `Callbacks`, `PluginCallbacks`).
239
286
 
240
287
  ## Development
241
288
 
package/build/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import { CorsOptions } from "cors";
3
3
  import { Options } from "express-rate-limit";
4
4
  import "dotenv/config";
5
5
  import "jiti";
6
+ import { BetterAuthOptions } from "better-auth";
6
7
  //#region ../core/types/api.d.ts
7
8
  /** API response */
8
9
  type ApiResponse<T = any> = {
@@ -13,12 +14,12 @@ type ApiResponse<T = any> = {
13
14
  };
14
15
  /** Promise of an API response, what a handler returns */
15
16
  type ApiPromise<T = any> = Promise<ApiResponse<T>>;
16
- /** Query parameters sent in the `query` header (JSON). */
17
+ /** Query parameters, sent as the JSON body of a `QUERY` request */
17
18
  type QueryParams = {
18
19
  skip?: number;
19
20
  top?: number;
20
21
  filter?: NanoObject;
21
- orderby?: Record<string, 'asc' | 'desc'>;
22
+ orderby?: NanoObject<'asc' | 'desc'>;
22
23
  select?: string;
23
24
  };
24
25
  //#endregion
@@ -27,6 +28,8 @@ type QueryParams = {
27
28
  type Params<DB = any> = Partial<NanoObject & {
28
29
  db?: DB;
29
30
  onBeforeResult?: NanoObject;
31
+ /** `{ user, session }` of Better Auth, on the routes behind its guard */
32
+ session?: NanoObject;
30
33
  }>;
31
34
  /** Endpoint handlers */
32
35
  type Handler<DB> = (req: Request$1, params: Params<DB>) => ApiPromise;
@@ -116,7 +119,7 @@ type PackagesConfig = {
116
119
  /** Morgan configuration */
117
120
  morgan?: false | {
118
121
  /** Morgan tokens */
119
- tokens?: Record<string, any>;
122
+ tokens?: NanoObject<any>;
120
123
  /** Morgan format */
121
124
  format: string;
122
125
  };
@@ -132,6 +135,8 @@ type PackagesConfig = {
132
135
  express?: Partial<Parameters<typeof express.json>[0]> | false;
133
136
  /** urlencoded configuration */
134
137
  urlencoded?: Partial<Parameters<typeof express.urlencoded>[0]> | false;
138
+ /** Better Auth options, without `database`: it uses the app's Prisma client. Defaults to false */
139
+ betterAuth?: Omit<BetterAuthOptions, 'database'> | false;
135
140
  };
136
141
  /** Application configuration */
137
142
  type NanoAppConfig = {
@@ -140,7 +145,7 @@ type NanoAppConfig = {
140
145
  /** Application port. Defaults to 3000 */
141
146
  port?: number;
142
147
  /** Values applied with Express `app.set()` (e.g. `{ "trust proxy": 1 }`) */
143
- settings?: Record<string, unknown>;
148
+ settings?: NanoObject<unknown>;
144
149
  /** Default middlewares configuration */
145
150
  packages?: PackagesConfig;
146
151
  /** Plugins to mount. Defaults to [] */
package/build/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as Formatter, i as FileLoader, n as MISSING_DATABASE_URL, o as FileReader, r as OFFICIAL_PLUGINS, t as Prisma } from "./Prisma-Dq-lYuz3.js";
1
+ import { createRequire } from "node:module";
2
2
  import express from "express";
3
3
  import morgan from "morgan";
4
4
  import cors from "cors";
@@ -6,8 +6,159 @@ import helmet from "helmet";
6
6
  import compression from "compression";
7
7
  import rateLimit from "express-rate-limit";
8
8
  import "dotenv/config";
9
- import { dirname, join } from "node:path";
10
- import { existsSync, readFileSync } from "node:fs";
9
+ import { glob } from "glob";
10
+ import { dirname, isAbsolute, join, resolve } from "node:path";
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { createJiti } from "jiti";
13
+ import { execFileSync } from "node:child_process";
14
+ import { createHash } from "node:crypto";
15
+ //#region ../core/const.ts
16
+ /** Package of the official plugins: only its schemas may declare nano_* */
17
+ const OFFICIAL_PLUGINS = "@nano-app/plugins";
18
+ /** Error when the database URL is missing, at startup and on `nano-app push` */
19
+ const MISSING_DATABASE_URL = "[nano-app] [ERROR] The DATABASE_URL environment variable is not defined.";
20
+ //#endregion
21
+ //#region ../core/loader/Validator.ts
22
+ const HTTP_METHODS = [
23
+ "GET",
24
+ "POST",
25
+ "PUT",
26
+ "DELETE",
27
+ "QUERY"
28
+ ];
29
+ const STORE_TYPES = ["app", "api"];
30
+ /** Checks the consumer's files: invalid ones are warned about and discarded */
31
+ var Validator = class Validator {
32
+ static oValidators = {
33
+ route: () => this.isRoute,
34
+ prisma: () => this.isPrisma,
35
+ store: () => this.isStore
36
+ };
37
+ /** Warns about an invalid file and returns `false`, to discard it */
38
+ static warn(sFilePath, sMessage) {
39
+ console.warn(`[nano-app] [WARNING] ${sFilePath}: ${sMessage}`);
40
+ return false;
41
+ }
42
+ /** Checks that the default export is an object */
43
+ static isObject(oEntry, sFilePath) {
44
+ if (oEntry && typeof oEntry === "object") return true;
45
+ return Validator.warn(sFilePath, "the default export is not an object.");
46
+ }
47
+ /**
48
+ * Type guard for a `*.route.ts` entry.
49
+ */
50
+ static isRoute(oEntry, sFilePath) {
51
+ if (!Validator.isObject(oEntry, sFilePath)) return false;
52
+ const oRoute = oEntry;
53
+ if (oRoute.expression && typeof oRoute.expression !== "string") return Validator.warn(sFilePath, "\"expression\" must be a string.");
54
+ for (const sMethod of HTTP_METHODS) if (oRoute[sMethod] && typeof oRoute[sMethod] !== "function") return Validator.warn(sFilePath, `"${String(sMethod)}" must be a function.`);
55
+ if (!HTTP_METHODS.some((sMethod) => oRoute[sMethod])) return Validator.warn(sFilePath, "defines no HTTP method.");
56
+ return true;
57
+ }
58
+ /** Type guard for a `*.store.ts` entry */
59
+ static isStore(oEntry, sFilePath) {
60
+ if (!Validator.isObject(oEntry, sFilePath)) return false;
61
+ if (!STORE_TYPES.includes(oEntry.type)) return Validator.warn(sFilePath, `its type must be "app" or "api".`);
62
+ if (oEntry.data !== void 0 && (!oEntry.data || typeof oEntry.data !== "object")) return Validator.warn(sFilePath, "\"data\" must be an object.");
63
+ if (oEntry.headers !== void 0 && (!Validator.isObject(oEntry.headers, sFilePath) || Object.values(oEntry.headers).some((sValue) => typeof sValue !== "string"))) return Validator.warn(sFilePath, "\"headers\" must be an object of strings.");
64
+ return true;
65
+ }
66
+ /**
67
+ * Type guard for a `.prisma` entry: the contents of the file, without comments.
68
+ */
69
+ static isPrisma(oEntry, sFilePath) {
70
+ if (typeof oEntry !== "string") return Validator.warn(sFilePath, "the contents are not a string.");
71
+ const sProvider = oEntry.match(/datasource\s+\w+\s*\{[^}]*provider\s*=\s*["']([^"']+)["']/s)?.[1];
72
+ if (sProvider !== "postgresql") return Validator.warn(sFilePath, `the datasource provider must be "postgresql"${sProvider ? `, not "${sProvider}"` : ""}.`);
73
+ return true;
74
+ }
75
+ /** Validates a full map of entries and returns only the valid ones */
76
+ static validate(oEntries, sExtension) {
77
+ const oValid = {};
78
+ for (const [sFilePath, oEntry] of Object.entries(oEntries)) {
79
+ const oValidated = this.validateUnique(oEntry, sExtension, sFilePath);
80
+ if (oValidated !== void 0) oValid[sFilePath] = oValidated;
81
+ }
82
+ return oValid;
83
+ }
84
+ /** Validates one entry: returns it when valid, `undefined` otherwise */
85
+ static validateUnique(oEntry, sExtension, sFilePath = "") {
86
+ return this.oValidators[sExtension]?.()(oEntry, sFilePath) ? oEntry : void 0;
87
+ }
88
+ };
89
+ //#endregion
90
+ //#region ../core/loader/FileReader.ts
91
+ /** Reads and loads files from disk, resolved from `cwd` */
92
+ var FileReader = class {
93
+ static oJiti = createJiti(import.meta.url, { nativeModules: ["@prisma/client"] });
94
+ /** Loads a module via jiti */
95
+ static async loadModule(sFilePath, oOptions = {}) {
96
+ return await this.oJiti.import(this.getAbsolutePath(sFilePath), oOptions);
97
+ }
98
+ /** Reads the contents of a file */
99
+ static loadFile(sFilePath) {
100
+ const sContent = readFileSync(this.getAbsolutePath(sFilePath), "utf-8");
101
+ if (!sContent) throw new Error(`[nano-app] [ERROR] The file ${sFilePath} does not have valid content.`);
102
+ return sContent;
103
+ }
104
+ /** Reads a Prisma schema without its `//` comments */
105
+ static loadSchema(sFilePath) {
106
+ return this.loadFile(sFilePath).replace(/\/\/.*$/gm, "");
107
+ }
108
+ /** Resolves the absolute path of a file */
109
+ static getAbsolutePath(sFilePath) {
110
+ const sAbsolutePath = isAbsolute(sFilePath) ? sFilePath : resolve(process.cwd(), sFilePath);
111
+ if (!existsSync(sAbsolutePath)) throw new Error(`[nano-app] [ERROR] ${sFilePath} not found.`);
112
+ return sAbsolutePath;
113
+ }
114
+ };
115
+ //#endregion
116
+ //#region ../core/utils/Formatter.ts
117
+ /** Formats values for logs and messages */
118
+ var Formatter = class {
119
+ /** Message of anything thrown: its `message` when it has one, else the value itself */
120
+ static error(oError) {
121
+ return String(oError?.message ?? oError);
122
+ }
123
+ /** Formats a date for the request log, in UTC: `2026-10-19 12:03:10` */
124
+ static logDate(oDate = /* @__PURE__ */ new Date()) {
125
+ return oDate.toISOString().slice(0, 19).replace("T", " ");
126
+ }
127
+ };
128
+ //#endregion
129
+ //#region ../core/loader/FileLoader.ts
130
+ /** Finds, loads and validates the consumer's files */
131
+ var FileLoader = class {
132
+ /**
133
+ * 1. Validates a `.prisma` file.
134
+ * 2. Loads a generated Prisma client module (`client.ts`, or `client.js`)
135
+ */
136
+ static async findPrismaClient(sClientPath, sSchemaPath) {
137
+ const sContent = FileReader.loadSchema(sSchemaPath);
138
+ if (!Validator.validateUnique(sContent, "prisma", sSchemaPath)) throw new Error(`[nano-app] [ERROR] ${sSchemaPath} is not a valid Prisma schema.`);
139
+ try {
140
+ return (await FileReader.loadModule(sClientPath))?.PrismaClient;
141
+ } catch (oError) {
142
+ throw new Error(`[nano-app] [ERROR] The Prisma client ${sClientPath} could not be loaded: ${Formatter.error(oError)}`);
143
+ }
144
+ }
145
+ /** Finds `*.route.ts` (or built `.js`) files and returns `{ [subPath]: defaultExport }` */
146
+ static async findMany({ extension, sourceDir }) {
147
+ const sSrcDir = (sourceDir ?? `src/${extension}s`).replace(/^\.\//, "").replace(/\/$/, "");
148
+ const aFilePaths = await glob(`${sSrcDir}/**/*.${extension}.{ts,js}`, {
149
+ posix: true,
150
+ ignore: "**/*.d.ts"
151
+ });
152
+ const oEntries = {};
153
+ for (const sFilePath of aFilePaths) {
154
+ const sFileSubPath = sFilePath.slice(sSrcDir.length).replace(new RegExp(`\\.${extension}\\.(ts|js)$`), "");
155
+ const sKey = extension === "store" ? sFileSubPath.slice(1).replaceAll("/", "_") : sFileSubPath;
156
+ oEntries[sKey] = await FileReader.loadModule(sFilePath, { default: true });
157
+ }
158
+ return Validator.validate(oEntries, extension);
159
+ }
160
+ };
161
+ //#endregion
11
162
  //#region src/routes/Endpoint.ts
12
163
  /** Base class for all endpoints */
13
164
  var Endpoint = class {
@@ -32,8 +183,12 @@ var Endpoint = class {
32
183
  status: 405,
33
184
  message: `Method ${sMethod} not implemented`
34
185
  });
35
- (this.oCallbacks.onBefore?.(oReq, sMethod, oParams) ?? Promise.resolve({})).then((onBeforeResult) => fnMethod(oReq, {
186
+ const oRequestParams = {
36
187
  ...oParams,
188
+ session: oRes.locals.session
189
+ };
190
+ (this.oCallbacks.onBefore?.(oReq, sMethod, oRequestParams) ?? Promise.resolve({})).then((onBeforeResult) => fnMethod(oReq, {
191
+ ...oRequestParams,
37
192
  onBeforeResult
38
193
  })).then((oResponse) => {
39
194
  oRes.status(oResponse.success ? 200 : 400).json(oResponse);
@@ -73,6 +228,116 @@ var Router = class {
73
228
  }
74
229
  };
75
230
  //#endregion
231
+ //#region src/auth/BetterAuth.ts
232
+ /** Better Auth on the app's Prisma client: the instance, its handler and a session guard */
233
+ var BetterAuth = class {
234
+ /** Creates the instance on the app's client, its handler and the guard */
235
+ static async create(oOptions, oDb) {
236
+ const { betterAuth } = await import("better-auth");
237
+ const { prismaAdapter } = await import("better-auth/adapters/prisma");
238
+ const { getSchema } = await import("better-auth/db");
239
+ const { fromNodeHeaders, toNodeHandler } = await import("better-auth/node");
240
+ const aMissing = Object.keys(getSchema(oOptions)).filter((sModel) => !oDb[sModel]);
241
+ if (aMissing.length) throw new Error(`[nano-app] [ERROR] Better Auth needs the models ${aMissing.join(", ")} in the app's schema. Run \`npx auth generate\` and apply your schema with Prisma.`);
242
+ const oAuth = betterAuth({
243
+ ...oOptions,
244
+ database: prismaAdapter(oDb, { provider: "postgresql" })
245
+ });
246
+ const fnGuard = (oReq, oRes, fnNext) => {
247
+ oAuth.api.getSession({ headers: fromNodeHeaders(oReq.headers) }).then((oSession) => {
248
+ if (!oSession) return fnNext({
249
+ status: 401,
250
+ message: "Not authenticated"
251
+ });
252
+ oRes.locals.session = oSession;
253
+ fnNext();
254
+ }).catch(fnNext);
255
+ };
256
+ return {
257
+ oHandler: toNodeHandler(oAuth),
258
+ fnGuard,
259
+ sPath: `${oOptions.basePath ?? "/api/auth"}/{*path}`
260
+ };
261
+ }
262
+ };
263
+ //#endregion
264
+ //#region src/database/Prisma.ts
265
+ /** Runs the consumer's Prisma CLI: generates clients and applies official plugins' migrations */
266
+ var Prisma = class {
267
+ /** Folder of a client and its files, in the project */
268
+ static dir(sName) {
269
+ return join(process.cwd(), ".nano-app", "prisma-clients", sName);
270
+ }
271
+ /** Generates the client of a schema in `.nano-app/prisma-clients/<name>`, unless it is up to date */
272
+ static generate(sName, sSchemaPath) {
273
+ const sDir = this.dir(sName);
274
+ const sSource = FileReader.getAbsolutePath(sSchemaPath);
275
+ const sHash = createHash("sha256").update(readFileSync(sSource)).digest("hex");
276
+ const sSourceFile = join(sDir, "source.json");
277
+ const oCurrent = existsSync(sSourceFile) ? JSON.parse(readFileSync(sSourceFile, "utf-8")) : void 0;
278
+ if (oCurrent && oCurrent.schemaPath !== sSource) console.warn(`[nano-app] [WARNING] .nano-app/prisma-clients/${sName} has the client of ${oCurrent.schemaPath}: it is replaced by the one of ${sSource}.`);
279
+ if (oCurrent?.schemaPath === sSource && oCurrent.hash === sHash && existsSync(join(sDir, "generated", "client.ts"))) return;
280
+ mkdirSync(sDir, { recursive: true });
281
+ const sSchema = readFileSync(sSource, "utf-8");
282
+ writeFileSync(join(sDir, "schema.prisma"), sSchema.replace(/output\s*=\s*"[^"]*"/, "output = \"./generated\""));
283
+ this.run(sName, [
284
+ "generate",
285
+ "--schema",
286
+ join(sDir, "schema.prisma")
287
+ ]);
288
+ writeFileSync(sSourceFile, JSON.stringify({
289
+ schemaPath: sSource,
290
+ hash: sHash
291
+ }));
292
+ console.log(`[nano-app] [INFO] Prisma client "${sName}" generated`);
293
+ }
294
+ /** Applies the pending migrations an official plugin ships next to its schema */
295
+ static migrate(sName, sSchemaPath, sDbSchema) {
296
+ const sMigrations = join(dirname(FileReader.getAbsolutePath(sSchemaPath)), "migrations");
297
+ if (!existsSync(sMigrations)) throw new Error(`[nano-app] [ERROR] The plugin "${sName}" ships no migrations: ${sMigrations}`);
298
+ if (!process.env.DATABASE_URL) throw new Error(MISSING_DATABASE_URL);
299
+ const oUrl = new URL(process.env.DATABASE_URL);
300
+ oUrl.searchParams.set("schema", sDbSchema);
301
+ const sConfig = join(this.dir(sName), "prisma.config.mjs");
302
+ writeFileSync(sConfig, [
303
+ "export default {",
304
+ ` schema: ${JSON.stringify(join(this.dir(sName), "schema.prisma"))},`,
305
+ ` migrations: { path: ${JSON.stringify(sMigrations)} },`,
306
+ " datasource: { url: process.env.NANO_APP_MIGRATION_URL },",
307
+ "};",
308
+ ""
309
+ ].join("\n"));
310
+ this.run(sName, [
311
+ "migrate",
312
+ "deploy",
313
+ "--config",
314
+ sConfig
315
+ ], { NANO_APP_MIGRATION_URL: oUrl.toString() });
316
+ }
317
+ /** Runs a Prisma command of the consumer's `prisma` package */
318
+ static run(sName, aArgs, oEnv = {}) {
319
+ let sCli;
320
+ try {
321
+ sCli = createRequire(`${process.cwd()}/`).resolve("prisma/build/index.js");
322
+ } catch {
323
+ throw new Error(`[nano-app] [ERROR] The database "${sName}" needs Prisma: install the prisma package.`);
324
+ }
325
+ try {
326
+ execFileSync(process.execPath, [sCli, ...aArgs], {
327
+ env: {
328
+ ...process.env,
329
+ ...oEnv
330
+ },
331
+ stdio: "pipe"
332
+ });
333
+ } catch (oError) {
334
+ const oFailed = oError;
335
+ const sOutput = `${oFailed.stderr ?? ""}${oFailed.stdout ?? ""}`.trim() || Formatter.error(oError);
336
+ throw new Error(`[nano-app] [ERROR] prisma ${aArgs[0]} failed for "${sName}": ${sOutput}`);
337
+ }
338
+ }
339
+ };
340
+ //#endregion
76
341
  //#region src/database/Database.ts
77
342
  /** Prisma clients of the app and its plugins, one per name */
78
343
  var Database = class {
@@ -138,9 +403,11 @@ var Database = class {
138
403
  /** A group of routes, with its options, callbacks and database */
139
404
  var Plugin = class {
140
405
  oConfig;
141
- /** Creates a new Plugin instance */
142
- constructor(oConfig) {
406
+ oAuthOptions;
407
+ /** Creates a new Plugin instance; the app's plugin also gets the Better Auth options */
408
+ constructor(oConfig, oAuthOptions) {
143
409
  this.oConfig = oConfig;
410
+ this.oAuthOptions = oAuthOptions;
144
411
  }
145
412
  /** Mounts its endpoints on express app */
146
413
  async mount(oApp) {
@@ -150,6 +417,12 @@ var Plugin = class {
150
417
  ...options,
151
418
  db: oDb
152
419
  };
420
+ if (this.oAuthOptions) {
421
+ if (!oDb) throw new Error("[nano-app] [ERROR] packages.betterAuth needs the database of the app: set mainPlugin.database.enabled to true.");
422
+ const { sPath, oHandler, fnGuard } = await BetterAuth.create(this.oAuthOptions, oDb);
423
+ oApp.all(sPath, oHandler);
424
+ oApp.use(fnGuard);
425
+ }
153
426
  await callbacks?.onMount?.(oApp, oParams);
154
427
  const oRouter = new Router(routesDir, basePath);
155
428
  for (const { path, endpoint } of await oRouter.mount()) oApp.all(path, await endpoint.mount(oParams));
@@ -186,7 +459,8 @@ const INITIAL_CONFIG = {
186
459
  message: "Too many requests, please try again later."
187
460
  },
188
461
  express: { limit: "10kb" },
189
- urlencoded: { extended: false }
462
+ urlencoded: { extended: false },
463
+ betterAuth: false
190
464
  },
191
465
  /** Main plugin to mount */
192
466
  mainPlugin: {
@@ -294,7 +568,8 @@ var NanoApp = class {
294
568
  };
295
569
  for (const oPlugin of this.oConfig.plugins) oPlugins[oPlugin.stage ?? "before"].push(oPlugin);
296
570
  for (const oPlugin of oPlugins.before) await new Plugin(oPlugin).mount(this.oApp);
297
- await new Plugin(this.oConfig.mainPlugin).mount(this.oApp);
571
+ const { mainPlugin, packages } = this.oConfig;
572
+ await new Plugin(mainPlugin, packages.betterAuth).mount(this.oApp);
298
573
  for (const oPlugin of oPlugins.after) await new Plugin(oPlugin).mount(this.oApp);
299
574
  this.oApp.use(notFoundHandler);
300
575
  this.oApp.use(errorHandler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nano-app/backend",
3
- "version": "0.9.0",
3
+ "version": "0.11.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",
@@ -18,7 +18,7 @@
18
18
  "scripts": {
19
19
  "build": "tsdown",
20
20
  "dev": "tsdown --watch",
21
- "test": "tsx --env-file-if-exists=../.env --test tests/*.test.ts",
21
+ "test": "tsx --env-file-if-exists=../.env --test tests/*.test.ts ../core/tests/*.test.ts",
22
22
  "prepublishOnly": "pnpm run build"
23
23
  },
24
24
  "keywords": [
@@ -56,6 +56,7 @@
56
56
  "peerDependencies": {
57
57
  "@prisma/adapter-pg": "^7.10.1",
58
58
  "@prisma/client": "^7.10.1",
59
+ "better-auth": "^1.7.12",
59
60
  "pg": "^8.23.1"
60
61
  },
61
62
  "peerDependenciesMeta": {
@@ -65,6 +66,9 @@
65
66
  "@prisma/client": {
66
67
  "optional": true
67
68
  },
69
+ "better-auth": {
70
+ "optional": true
71
+ },
68
72
  "pg": {
69
73
  "optional": true
70
74
  }
@@ -78,13 +82,11 @@
78
82
  "@types/morgan": "^1.9.10",
79
83
  "@types/node": "^26.6.5",
80
84
  "@types/pg": "^8.20.0",
85
+ "better-auth": "^1.7.12",
81
86
  "pg": "^8.23.1",
82
87
  "prisma": "^7.10.1",
83
88
  "tsdown": "^0.23.1",
84
89
  "tsx": "^4.23.15",
85
90
  "typescript": "^7.0.2"
86
- },
87
- "bin": {
88
- "nano-app": "build/nano-app.js"
89
91
  }
90
92
  }
@@ -1,221 +0,0 @@
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/nano-app.js DELETED
@@ -1,46 +0,0 @@
1
- #!/usr/bin/env node
2
- import { r as OFFICIAL_PLUGINS, t as Prisma } from "./Prisma-Dq-lYuz3.js";
3
- import { dirname, join } from "node:path";
4
- import { existsSync, readFileSync } from "node:fs";
5
- //#region bin/nano-app.ts
6
- /**
7
- * Generates a Prisma client where nano-app reads it, as the startup does: for the build or the editor.
8
- * nano-app generate the app's schema (src/prisma/schema.prisma)
9
- * nano-app generate <name> an official plugin's, from the installed @nano-app/plugins
10
- * nano-app generate <name> --schema <path>
11
- */
12
- const [sCommand, ...aArgs] = process.argv.slice(2);
13
- /** package.json of @nano-app/plugins: the project itself (its tests) or one in a node_modules */
14
- function officialPackage() {
15
- for (let sDir = process.cwd();; sDir = dirname(sDir)) {
16
- const sOwn = join(sDir, "package.json");
17
- if (existsSync(sOwn) && JSON.parse(readFileSync(sOwn, "utf-8")).name === "@nano-app/plugins") return sOwn;
18
- const sInstalled = join(sDir, "node_modules", OFFICIAL_PLUGINS, "package.json");
19
- if (existsSync(sInstalled)) return sInstalled;
20
- if (dirname(sDir) === sDir) return void 0;
21
- }
22
- }
23
- /** Schema of an official plugin: next to the entry @nano-app/plugins exports for it */
24
- function pluginSchema(sName) {
25
- const sPackage = officialPackage();
26
- if (!sPackage) return `${OFFICIAL_PLUGINS} is not installed`;
27
- const oExport = JSON.parse(readFileSync(sPackage, "utf-8")).exports["./*"];
28
- const sTarget = oExport.import ?? oExport.default;
29
- return join(dirname(sPackage), dirname(sTarget.replaceAll("*", sName)), "schema.prisma");
30
- }
31
- try {
32
- if (sCommand !== "generate") throw new Error("[nano-app] [ERROR] Usage: nano-app generate [name] [--schema <path>]");
33
- const iSchemaFlag = aArgs.indexOf("--schema");
34
- const sSchemaFlag = iSchemaFlag >= 0 ? aArgs[iSchemaFlag + 1] : void 0;
35
- const fnIsFlagPart = (iIndex) => iSchemaFlag >= 0 && (iIndex === iSchemaFlag || iIndex === iSchemaFlag + 1);
36
- const sName = aArgs.find((_sArg, iIndex) => !fnIsFlagPart(iIndex));
37
- const sDefault = sName ? pluginSchema(sName) : "src/prisma/schema.prisma";
38
- const sSchema = sSchemaFlag ?? sDefault;
39
- if (!existsSync(sSchema)) throw new Error(`[nano-app] [ERROR] "${sName ?? "app"}" has no schema: ${sSchema}`);
40
- Prisma.generate(sName ?? "app", sSchema);
41
- } catch (oError) {
42
- console.error(oError.message);
43
- process.exit(1);
44
- }
45
- //#endregion
46
- export {};