@nano-app/plugins 0.11.0 → 0.12.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 +15 -15
- package/build/const.js +10 -0
- package/build/core/files/FileLoader.d.ts +1 -0
- package/build/core/index.d.ts +4 -0
- package/build/core/types/objects.d.ts +2 -0
- package/build/core/utils/Validator.d.ts +1 -0
- package/build/crud/index.d.ts +5 -9
- package/build/crud/index.js +3 -5
- package/build/crud/route.js +81 -0
- package/build/health/index.d.ts +3 -5
- package/build/health/index.js +7 -5
- package/package.json +1 -4
- package/build/crud/routes/index.route.js +0 -78
- package/build/health/routes/index.route.js +0 -8
- package/build/rate-limit/index.d.ts +0 -14
- package/build/rate-limit/index.js +0 -43
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ pnpm add @nano-app/backend @nano-app/plugins
|
|
|
12
12
|
import { createNanoApp } from '@nano-app/backend';
|
|
13
13
|
import health from '@nano-app/plugins/health';
|
|
14
14
|
|
|
15
|
-
createNanoApp({ plugins: [health()] });
|
|
15
|
+
createNanoApp({ plugins: { before: [health()] } });
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Official plugins
|
|
@@ -20,10 +20,11 @@ createNanoApp({ plugins: [health()] });
|
|
|
20
20
|
| Plugin | Import | Endpoints | Options |
|
|
21
21
|
| -------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
|
22
22
|
| `health` | `import health from '@nano-app/plugins/health'` | `GET /health` → `{ success: true, result: { status: "ok" } }`. Public: it answers without a session | `basePath` (default `"/health"`) |
|
|
23
|
-
| `crud` | `import crud from '@nano-app/plugins/crud'` |
|
|
24
|
-
| `rate-limit` | `import rateLimit from '@nano-app/plugins/rate-limit'` | None: limits every request of the routes and plugins mounted after it, per client IP (see [Client IP](../backend/README.md#client-ip)); the addresses of an IPv6 `/56` network are one client. Each request spends a token of the client's bucket, which holds `capacity` and refills `refillPerSecond` tokens per second; without one it answers `429` with `message` and `Retry-After` (seconds until the next token). List it first in `plugins.before`. The buckets live in the memory of each process | `capacity` (default `100`), `refillPerSecond` (default `1`), `message` |
|
|
23
|
+
| `crud` | `import crud from '@nano-app/plugins/crud'` | One route per table of `models`, keyed by its name in the app's relations (any other answers 404): `GET` and `QUERY` on `/<model>{/<id>}` → a list `{ result }` or one record, with `QueryParams` as the body of `QUERY`: Drizzle's relational query (`where`, `columns`, `orderBy`, `limit`, `offset`, `with`); `POST /<model>`, `PUT` and `DELETE` on `/<model>/<id>` → the record created, updated or deleted. Postgres casts `<id>` to the type of the `id` column. With Better Auth, each method asks for a permission on the table, 403 without it: `read` (`GET`, `QUERY`), `create`, `update`, `delete`, see [crud and related tables](#crud-and-related-tables) | `models` (the published tables, `{ note, comment }`), `validations` (zod schemas of the bodies, see [crud and the bodies](#crud-and-the-bodies)), `basePath` (default none: `/<model>`) |
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
Every plugin takes `basePath` besides its own options, over its default: `crud({ models, basePath: '/api' })` answers on `/api/<model>`.
|
|
26
|
+
|
|
27
|
+
What is not CRUD, such as creating a record with business logic, goes in a route of your own with its own URL. A `crud` mounted `before` your routes takes its URLs from them; mounted `after`, a route of yours on one of its URLs takes every method of it.
|
|
27
28
|
|
|
28
29
|
### crud and the bodies
|
|
29
30
|
|
|
@@ -35,8 +36,7 @@ import { z } from 'zod';
|
|
|
35
36
|
import * as schema from './db/schema';
|
|
36
37
|
|
|
37
38
|
crud({
|
|
38
|
-
models:
|
|
39
|
-
dbSchema: schema,
|
|
39
|
+
models: { note: schema.note },
|
|
40
40
|
validations: {
|
|
41
41
|
note: {
|
|
42
42
|
create: createInsertSchema(schema.note).omit({ id: true }),
|
|
@@ -49,7 +49,7 @@ crud({
|
|
|
49
49
|
|
|
50
50
|
### crud and related tables
|
|
51
51
|
|
|
52
|
-
A `crud` query runs on the app's client, so it reaches the related tables Drizzle's relations give: `with` loads them and `where` filters by them, also the ones that are not in `models`. With Better Auth, the permission asked is only the one of the URL's table. What a query may load is up to the app: the `read` validation of the model decides it (a `z.strictObject` without `with` keeps a query in its table). Better Auth's tables live in `nano_auth`, outside the app's
|
|
52
|
+
A `crud` query runs on the app's client, so it reaches the related tables Drizzle's relations give: `with` loads them and `where` filters by them, also the ones that are not in `models`. With Better Auth, the permission asked is only the one of the URL's table. What a query may load is up to the app: the `read` validation of the model decides it (a `z.strictObject` without `with` keeps a query in its table). Better Auth's tables live in `nano_auth`, outside the app's relations: `crud` does not reach them. A Postgres error of a write (a duplicate, a missing reference) answers 500.
|
|
53
53
|
|
|
54
54
|
### Layout
|
|
55
55
|
|
|
@@ -58,7 +58,7 @@ plugins/
|
|
|
58
58
|
└── <name>/
|
|
59
59
|
├── index.ts default-exports the plugin factory: the import path
|
|
60
60
|
├── schema.ts its tables, when it has any, in the Postgres schema nano_<name>
|
|
61
|
-
└── routes/ *.route.ts,
|
|
61
|
+
└── routes/ *.route.ts, when its routes are files, under the plugin's basePath
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
## Write a plugin
|
|
@@ -83,9 +83,9 @@ import { createPlugin } from '@nano-app/backend';
|
|
|
83
83
|
import { dirname, resolve } from 'node:path';
|
|
84
84
|
import { fileURLToPath } from 'node:url';
|
|
85
85
|
|
|
86
|
-
export default createPlugin((
|
|
86
|
+
export default createPlugin(() => ({
|
|
87
87
|
name: 'hello',
|
|
88
|
-
basePath:
|
|
88
|
+
basePath: '/hello',
|
|
89
89
|
routesDir: resolve(dirname(fileURLToPath(import.meta.url)), 'routes'),
|
|
90
90
|
}));
|
|
91
91
|
```
|
|
@@ -107,11 +107,11 @@ import hello from './plugins/hello';
|
|
|
107
107
|
createNanoApp({ plugins: { before: [hello()] } });
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
The app lists it in `plugins.before` or `plugins.after` its own routes. Mount order: base middlewares, Better Auth, `before` plugins, your routes, `after` plugins, 404, error handler. With Better Auth on, its routes need a session unless they are `public`.
|
|
110
|
+
The app lists it in `plugins.before` or `plugins.after` its own routes, and moves it with `hello({ basePath: '/hi' })`: `createPlugin` adds `basePath` to the options of every plugin. Mount order: base middlewares, Better Auth, `before` plugins, your routes, `after` plugins, 404, error handler. With Better Auth on, its routes need a session unless they are `public`.
|
|
111
111
|
|
|
112
|
-
Set `options` in the returned config to pass values to the plugin's routes: every handler and callback receives them in `params`, next to `db`. Callbacks: `callbacks.onMount(
|
|
112
|
+
Set `options` in the returned config to pass values to the plugin's routes: every handler and callback receives them in `params`, next to `db`. Callbacks: `callbacks.onMount(params)` on the plugin, once, before its endpoints, for its setup; `onInit(params)` and `onBefore(req, method, params)` on each route. See the [backend README](../backend/README.md#callbacks).
|
|
113
113
|
|
|
114
|
-
`routesDir` must be a real directory of `*.route.ts` files when you run the app from source (`tsx`). `jiti` loads them. There is no compile step for your routes.
|
|
114
|
+
`routesDir` must be a real directory of `*.route.ts` files when you run the app from source (`tsx`). `jiti` loads them. There is no compile step for your routes. A plugin can also pass its routes made in code in `routes`, by path (`{ '/index': createRoute({...}) }`), beside or instead of `routesDir`: they are checked as the files.
|
|
115
115
|
|
|
116
116
|
### A plugin other people can install
|
|
117
117
|
|
|
@@ -134,9 +134,9 @@ Use MIT or Apache-2.0 if you want the plugin listed from this repo later. Open a
|
|
|
134
134
|
|
|
135
135
|
### Database
|
|
136
136
|
|
|
137
|
-
Optional. Pass `database: {
|
|
137
|
+
Optional. Pass `database: { relations }` on the plugin config: what `defineRelations` gives of its Drizzle tables. Keep its tables in the Postgres schema `nano_<plugin name>`, reserved for it, which drizzle-kit creates when the module exports its `pgSchema`; a table in the `nano_*` schema of another fails the startup.
|
|
138
138
|
|
|
139
|
-
`push: true` applies them at startup, and `
|
|
139
|
+
`push: true` applies them at startup, and `migrate` the folder the plugin ships, with its record in `nano_<plugin name>`; without them whoever installs it applies them with drizzle-kit. Handlers and callbacks receive its client as `params.db`.
|
|
140
140
|
|
|
141
141
|
### What belongs in this repository
|
|
142
142
|
|
package/build/const.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "zod";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "zod";
|
package/build/crud/index.d.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { CrudValidations } from "../types/crud.js";
|
|
2
2
|
import { NanoObject } from "@nano-app/backend";
|
|
3
|
-
/** Automatic CRUD: the
|
|
4
|
-
declare const _default: (options?: {
|
|
5
|
-
|
|
6
|
-
models: string[];
|
|
7
|
-
dbSchema: NanoObject;
|
|
3
|
+
/** Automatic CRUD: the tables of `models` of the app's database on `/<model>{/<id>}` */
|
|
4
|
+
declare const _default: (options?: ({
|
|
5
|
+
models: NanoObject;
|
|
8
6
|
validations?: NanoObject<CrudValidations>;
|
|
9
|
-
} | undefined) => import("@nano-app/backend").PluginConfig<{
|
|
10
|
-
|
|
11
|
-
models: string[];
|
|
12
|
-
dbSchema: NanoObject;
|
|
7
|
+
} & import("@nano-app/backend").PluginOptions) | undefined) => import("@nano-app/backend").PluginConfig<{
|
|
8
|
+
models: NanoObject;
|
|
13
9
|
validations?: NanoObject<CrudValidations>;
|
|
14
10
|
}>;
|
|
15
11
|
export { _default as default };
|
package/build/crud/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
+
import route from "./route.js";
|
|
1
2
|
import { createPlugin } from "@nano-app/backend";
|
|
2
|
-
import { join } from "node:path";
|
|
3
3
|
// crud/index.ts
|
|
4
|
-
/** Automatic CRUD: the
|
|
4
|
+
/** Automatic CRUD: the tables of `models` of the app's database on `/<model>{/<id>}` */
|
|
5
5
|
var crud_default = createPlugin((options) => ({
|
|
6
6
|
name: "crud",
|
|
7
|
-
|
|
8
|
-
routesDir: join(import.meta.dirname, "routes"),
|
|
9
|
-
options
|
|
7
|
+
routes: Object.fromEntries(Object.entries(options?.models ?? {}).map(([sModel, oTable]) => [`/${sModel}`, route(sModel, oTable, options?.validations?.[sModel])]))
|
|
10
8
|
}));
|
|
11
9
|
export { crud_default as default };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ACTIONS } from "../const.js";
|
|
2
|
+
import { NanoError, createRoute } from "@nano-app/backend";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
// crud/route.ts
|
|
5
|
+
/** The first record of a query or a write: 404 without it */
|
|
6
|
+
function first(aRecords) {
|
|
7
|
+
if (!aRecords[0]) throw new NanoError("Not found", 404);
|
|
8
|
+
return { result: aRecords[0] };
|
|
9
|
+
}
|
|
10
|
+
/** Every record of a query */
|
|
11
|
+
function list(aRecords) {
|
|
12
|
+
return { result: aRecords };
|
|
13
|
+
}
|
|
14
|
+
/** A list of the table, or one record by its id, with Drizzle's relational query of the body */
|
|
15
|
+
async function read(_oReq, { db, onBeforeResult }) {
|
|
16
|
+
const { model, page, answer, body } = onBeforeResult;
|
|
17
|
+
return answer(await db.query[model].findMany({
|
|
18
|
+
limit: 100,
|
|
19
|
+
...body,
|
|
20
|
+
...page
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
/** Inserts the body as a record */
|
|
24
|
+
async function create(_oReq, { db, onBeforeResult }) {
|
|
25
|
+
const { table, body } = onBeforeResult;
|
|
26
|
+
return first(await db.insert(table).values(body).returning());
|
|
27
|
+
}
|
|
28
|
+
/** Updates the record of the id with the body */
|
|
29
|
+
async function update(_oReq, { db, onBeforeResult }) {
|
|
30
|
+
const { table, id, body } = onBeforeResult;
|
|
31
|
+
return first(await db.update(table).set(body).where(eq(table.id, id)).returning());
|
|
32
|
+
}
|
|
33
|
+
/** Deletes the record of the id */
|
|
34
|
+
async function remove(_oReq, { db, onBeforeResult }) {
|
|
35
|
+
const { table, id } = onBeforeResult;
|
|
36
|
+
return first(await db.delete(table).where(eq(table.id, id)).returning());
|
|
37
|
+
}
|
|
38
|
+
/** The route of a table: `GET` and `QUERY` read, each method asks its permission on the table */
|
|
39
|
+
function route(sModel, oTable, oValidations = {}) {
|
|
40
|
+
const oMethods = Object.fromEntries(Object.entries({
|
|
41
|
+
GET: read,
|
|
42
|
+
QUERY: read,
|
|
43
|
+
POST: create,
|
|
44
|
+
PUT: update,
|
|
45
|
+
DELETE: remove
|
|
46
|
+
}).map(([sMethod, handler]) => [sMethod, {
|
|
47
|
+
handler,
|
|
48
|
+
permissions: { [sModel]: [ACTIONS[sMethod]] }
|
|
49
|
+
}]));
|
|
50
|
+
return createRoute({
|
|
51
|
+
expression: "{/:id}",
|
|
52
|
+
callbacks: { onBefore: async (oReq, sMethod) => {
|
|
53
|
+
const { id } = oReq.params;
|
|
54
|
+
const oBody = oReq.body ?? {};
|
|
55
|
+
const oResult = oValidations[ACTIONS[sMethod]]?.safeParse(oBody) ?? {
|
|
56
|
+
success: true,
|
|
57
|
+
data: oBody
|
|
58
|
+
};
|
|
59
|
+
if (!oResult.success) {
|
|
60
|
+
const { path, message } = oResult.error.issues[0];
|
|
61
|
+
throw new NanoError([...path, message].join(": "), 400);
|
|
62
|
+
}
|
|
63
|
+
const oRead = id ? {
|
|
64
|
+
page: {
|
|
65
|
+
where: { id },
|
|
66
|
+
limit: 1
|
|
67
|
+
},
|
|
68
|
+
answer: first
|
|
69
|
+
} : { answer: list };
|
|
70
|
+
return {
|
|
71
|
+
model: sModel,
|
|
72
|
+
table: oTable,
|
|
73
|
+
id,
|
|
74
|
+
body: oResult.data,
|
|
75
|
+
...oRead
|
|
76
|
+
};
|
|
77
|
+
} },
|
|
78
|
+
...oMethods
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
export { route as default };
|
package/build/health/index.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
+
import { NanoObject } from "../core/types/objects.js";
|
|
2
|
+
import "../core/index.js";
|
|
1
3
|
/** Health check: `GET /health` answers `{ status: "ok" }` while the server is up. */
|
|
2
|
-
declare const _default: (options?:
|
|
3
|
-
basePath?: string;
|
|
4
|
-
} | undefined) => import("@nano-app/backend").PluginConfig<{
|
|
5
|
-
basePath?: string;
|
|
6
|
-
}>;
|
|
4
|
+
declare const _default: (options?: (NanoObject & import("@nano-app/backend").PluginOptions) | undefined) => import("@nano-app/backend").PluginConfig<NanoObject>;
|
|
7
5
|
export { _default as default };
|
package/build/health/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { createPlugin } from "@nano-app/backend";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { createPlugin, createRoute } from "@nano-app/backend";
|
|
3
2
|
// health/index.ts
|
|
4
3
|
/** Health check: `GET /health` answers `{ status: "ok" }` while the server is up. */
|
|
5
|
-
var health_default = createPlugin((
|
|
4
|
+
var health_default = createPlugin(() => ({
|
|
6
5
|
name: "health",
|
|
7
|
-
basePath:
|
|
8
|
-
|
|
6
|
+
basePath: "/health",
|
|
7
|
+
routes: { "/index": createRoute({
|
|
8
|
+
public: true,
|
|
9
|
+
GET: async () => ({ result: { status: "ok" } })
|
|
10
|
+
}) }
|
|
9
11
|
}));
|
|
10
12
|
export { health_default as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nano-app/plugins",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Official plugins for @nano-app/backend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,8 +64,5 @@
|
|
|
64
64
|
"zod": {
|
|
65
65
|
"optional": true
|
|
66
66
|
}
|
|
67
|
-
},
|
|
68
|
-
"dependencies": {
|
|
69
|
-
"express-rate-limit": "^8.7.3"
|
|
70
67
|
}
|
|
71
68
|
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { NanoError, createRoute, isGranted } from "@nano-app/backend";
|
|
2
|
-
import { eq } from "drizzle-orm";
|
|
3
|
-
// crud/routes/index.route.ts
|
|
4
|
-
/** Action of each method: its permission of Better Auth and the validation of its body */
|
|
5
|
-
const ACTIONS = {
|
|
6
|
-
GET: "read",
|
|
7
|
-
QUERY: "read",
|
|
8
|
-
POST: "create",
|
|
9
|
-
PUT: "update",
|
|
10
|
-
DELETE: "delete"
|
|
11
|
-
};
|
|
12
|
-
/** Records a list answers when the request sends no `limit` */
|
|
13
|
-
const DEFAULT_LIMIT = 100;
|
|
14
|
-
/** The first record of a query or a write: 404 without it */
|
|
15
|
-
function first(aRecords) {
|
|
16
|
-
if (!aRecords[0]) throw new NanoError("Not found", 404);
|
|
17
|
-
return { result: aRecords[0] };
|
|
18
|
-
}
|
|
19
|
-
/** Every record of a query */
|
|
20
|
-
function list(aRecords) {
|
|
21
|
-
return { result: aRecords };
|
|
22
|
-
}
|
|
23
|
-
/** A list of the table, or one record by its id, with Drizzle's relational query of the body */
|
|
24
|
-
async function read(_oReq, { db, onBeforeResult }) {
|
|
25
|
-
const { model, page, answer, body } = onBeforeResult;
|
|
26
|
-
return answer(await db.query[model].findMany({
|
|
27
|
-
limit: DEFAULT_LIMIT,
|
|
28
|
-
...body,
|
|
29
|
-
...page
|
|
30
|
-
}));
|
|
31
|
-
}
|
|
32
|
-
/** `GET` reads without parameters, `QUERY` with `QueryParams` as its body */
|
|
33
|
-
var index_route_default = createRoute({
|
|
34
|
-
expression: "/:model{/:id}",
|
|
35
|
-
callbacks: { onBefore: async (oReq, sMethod, oParams) => {
|
|
36
|
-
const sModel = String(oReq.params.model);
|
|
37
|
-
const sAction = ACTIONS[sMethod];
|
|
38
|
-
const { id } = oReq.params;
|
|
39
|
-
const oTable = oParams.dbSchema[sModel];
|
|
40
|
-
if (!oParams.models.includes(sModel) || !oTable) throw new NanoError("Not found", 404);
|
|
41
|
-
if (!await isGranted(oParams, { [sModel]: [sAction] })) throw new NanoError("Forbidden", 403);
|
|
42
|
-
const oBody = oReq.body ?? {};
|
|
43
|
-
const oResult = (oParams.validations?.[sModel]?.[sAction])?.safeParse(oBody) ?? { data: oBody };
|
|
44
|
-
if (oResult.error) {
|
|
45
|
-
const { path, message } = oResult.error.issues[0];
|
|
46
|
-
throw new NanoError([...path, message].join(": "), 400);
|
|
47
|
-
}
|
|
48
|
-
const oRead = id ? {
|
|
49
|
-
page: {
|
|
50
|
-
where: { id },
|
|
51
|
-
limit: 1
|
|
52
|
-
},
|
|
53
|
-
answer: first
|
|
54
|
-
} : { answer: list };
|
|
55
|
-
return {
|
|
56
|
-
model: sModel,
|
|
57
|
-
table: oTable,
|
|
58
|
-
id,
|
|
59
|
-
body: oResult.data,
|
|
60
|
-
...oRead
|
|
61
|
-
};
|
|
62
|
-
} },
|
|
63
|
-
GET: read,
|
|
64
|
-
QUERY: read,
|
|
65
|
-
POST: async (_oReq, { db, onBeforeResult }) => {
|
|
66
|
-
const { table, body } = onBeforeResult;
|
|
67
|
-
return first(await db.insert(table).values(body).returning());
|
|
68
|
-
},
|
|
69
|
-
PUT: async (_oReq, { db, onBeforeResult }) => {
|
|
70
|
-
const { table, id, body } = onBeforeResult;
|
|
71
|
-
return first(await db.update(table).set(body).where(eq(table.id, id)).returning());
|
|
72
|
-
},
|
|
73
|
-
DELETE: async (_oReq, { db, onBeforeResult }) => {
|
|
74
|
-
const { table, id } = onBeforeResult;
|
|
75
|
-
return first(await db.delete(table).where(eq(table.id, id)).returning());
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
export { index_route_default as default };
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { createRoute } from "@nano-app/backend";
|
|
2
|
-
// health/routes/index.route.ts
|
|
3
|
-
/** Answers that the server is up */
|
|
4
|
-
var index_route_default = createRoute({
|
|
5
|
-
public: true,
|
|
6
|
-
GET: async () => ({ result: { status: "ok" } })
|
|
7
|
-
});
|
|
8
|
-
export { index_route_default as default };
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rate limit per client IP: each request spends a token of the client's bucket, refilled over time.
|
|
3
|
-
* Covers the app's routes and the plugins listed after it.
|
|
4
|
-
*/
|
|
5
|
-
declare const _default: (options?: {
|
|
6
|
-
capacity?: number;
|
|
7
|
-
refillPerSecond?: number;
|
|
8
|
-
message?: string;
|
|
9
|
-
} | undefined) => import("@nano-app/backend").PluginConfig<{
|
|
10
|
-
capacity?: number;
|
|
11
|
-
refillPerSecond?: number;
|
|
12
|
-
message?: string;
|
|
13
|
-
}>;
|
|
14
|
-
export { _default as default };
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { ipKeyGenerator } from "express-rate-limit";
|
|
2
|
-
import { createPlugin } from "@nano-app/backend";
|
|
3
|
-
// rate-limit/index.ts
|
|
4
|
-
/**
|
|
5
|
-
* Rate limit per client IP: each request spends a token of the client's bucket, refilled over time.
|
|
6
|
-
* Covers the app's routes and the plugins listed after it.
|
|
7
|
-
*/
|
|
8
|
-
var rate_limit_default = createPlugin((options) => {
|
|
9
|
-
const { capacity = 100, refillPerSecond = 1, message = "Too many requests, please try again later." } = options ?? {};
|
|
10
|
-
return {
|
|
11
|
-
name: "rate-limit",
|
|
12
|
-
routesDir: "",
|
|
13
|
-
callbacks: { onMount: async (oApp) => {
|
|
14
|
-
const fnRefill = (oBucket, iNow) => Math.min(capacity, oBucket.iTokens + (iNow - oBucket.iTime) / 1e3 * refillPerSecond);
|
|
15
|
-
const oBuckets = /* @__PURE__ */ new Map();
|
|
16
|
-
setInterval(() => {
|
|
17
|
-
const iNow = Date.now();
|
|
18
|
-
for (const [sIp, oBucket] of oBuckets) if (fnRefill(oBucket, iNow) >= capacity) oBuckets.delete(sIp);
|
|
19
|
-
}, 6e4).unref();
|
|
20
|
-
oApp.use((oReq, oRes, fnNext) => {
|
|
21
|
-
const sIp = ipKeyGenerator(oReq.ip ?? "");
|
|
22
|
-
const iNow = Date.now();
|
|
23
|
-
const iTokens = fnRefill(oBuckets.get(sIp) ?? {
|
|
24
|
-
iTokens: capacity,
|
|
25
|
-
iTime: iNow
|
|
26
|
-
}, iNow);
|
|
27
|
-
if (iTokens < 1) {
|
|
28
|
-
oRes.set("Retry-After", String(Math.ceil((1 - iTokens) / refillPerSecond)));
|
|
29
|
-
return fnNext({
|
|
30
|
-
status: 429,
|
|
31
|
-
message
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
oBuckets.set(sIp, {
|
|
35
|
-
iTokens: iTokens - 1,
|
|
36
|
-
iTime: iNow
|
|
37
|
-
});
|
|
38
|
-
fnNext();
|
|
39
|
-
});
|
|
40
|
-
} }
|
|
41
|
-
};
|
|
42
|
-
});
|
|
43
|
-
export { rate_limit_default as default };
|