@rapidd/core 2.1.0 → 2.1.1
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/.env.example +3 -3
- package/README.md +49 -17
- package/bin/cli.js +23 -3
- package/config/app.json +3 -0
- package/package.json +1 -1
- package/routes/api/v1/index.ts +1 -2
- package/src/auth/Auth.ts +4 -4
- package/src/index.ts +1 -1
- package/src/plugins/auth.ts +43 -8
- package/src/types.ts +3 -3
package/.env.example
CHANGED
|
@@ -40,10 +40,10 @@ DB_USER_PASSWORD_FIELD=
|
|
|
40
40
|
# Auto-detected from unique string fields; comma-separated (default: email)
|
|
41
41
|
DB_USER_IDENTIFIER_FIELDS=
|
|
42
42
|
# Comma-separated: bearer, basic, cookie, header (default: bearer)
|
|
43
|
-
|
|
44
|
-
# Cookie name for cookie
|
|
43
|
+
AUTH_METHODS=bearer
|
|
44
|
+
# Cookie name for cookie auth method (default: token)
|
|
45
45
|
AUTH_COOKIE_NAME=token
|
|
46
|
-
# Header name for header
|
|
46
|
+
# Header name for header auth method (default: X-Auth-Token)
|
|
47
47
|
AUTH_CUSTOM_HEADER=X-Auth-Token
|
|
48
48
|
|
|
49
49
|
# ── API Settings ───────────────────────────────────────
|
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Code-first REST API framework for TypeScript. Database in, API out.
|
|
6
6
|
|
|
7
|
+
[](https://www.npmjs.com/package/@rapidd/core)
|
|
7
8
|
[](https://www.typescriptlang.org/)
|
|
8
9
|
[](https://fastify.dev/)
|
|
9
10
|
[](https://www.prisma.io/)
|
|
@@ -29,7 +30,9 @@ Rapidd generates a fully-featured REST API from your database schema — then ge
|
|
|
29
30
|
## Quick Start
|
|
30
31
|
|
|
31
32
|
```bash
|
|
32
|
-
|
|
33
|
+
mkdir my-api && cd my-api
|
|
34
|
+
npx rapidd create-project # scaffold project files
|
|
35
|
+
npm install
|
|
33
36
|
```
|
|
34
37
|
|
|
35
38
|
```env
|
|
@@ -44,7 +47,7 @@ npm run dev # http://localhost:3000
|
|
|
44
47
|
|
|
45
48
|
Every table gets full CRUD endpoints. Auth is enabled automatically when a user table is detected. Every auto-detected value — auth fields, password hashing, JWT secrets, session store — is overridable via env vars. See [`.env.example`](.env.example) for the full list.
|
|
46
49
|
|
|
47
|
-
>
|
|
50
|
+
> **[Getting Started guide](https://github.com/MertDalbudak/rapidd/wiki/Getting-Started)** — full walkthrough with project structure
|
|
48
51
|
|
|
49
52
|
---
|
|
50
53
|
|
|
@@ -56,7 +59,7 @@ Every table gets full CRUD endpoints. Auth is enabled automatically when a user
|
|
|
56
59
|
| Query filtering (20+ operators) | ✓ | ✓ |
|
|
57
60
|
| Relations & deep includes | ✓ | ✓ |
|
|
58
61
|
| Field selection | ✓ | ✓ |
|
|
59
|
-
| JWT authentication (4
|
|
62
|
+
| JWT authentication (4 methods) | ✓ | ✓ |
|
|
60
63
|
| Per-model ACL | ✓ | ✓ |
|
|
61
64
|
| Row-Level Security (database-enforced) | ✓ | — |
|
|
62
65
|
| Rate limiting (Redis + memory fallback) | ✓ | ✓ |
|
|
@@ -83,7 +86,7 @@ GET /api/v1/posts?sortBy=createdAt&sortOrder=desc&limit=10&offset=20
|
|
|
83
86
|
|
|
84
87
|
20+ filter operators for strings, numbers, dates, arrays, nulls, and nested relation fields. Responses include pagination metadata with `total`, `count`, `limit`, `offset`, and `hasMore`.
|
|
85
88
|
|
|
86
|
-
>
|
|
89
|
+
> **[Query API wiki](https://github.com/MertDalbudak/rapidd/wiki/Query-API)** — all operators, composite PKs, relation filtering
|
|
87
90
|
|
|
88
91
|
---
|
|
89
92
|
|
|
@@ -98,11 +101,24 @@ POST /auth/refresh { "refreshToken": "..." }
|
|
|
98
101
|
GET /auth/me Authorization: Bearer <token>
|
|
99
102
|
```
|
|
100
103
|
|
|
101
|
-
Four
|
|
104
|
+
Four methods — **bearer** (default), **basic**, **cookie**, and **custom header** — configurable globally via `AUTH_METHODS` env var or per endpoint prefix in `config/app.json`:
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"endpointAuthMethod": {
|
|
109
|
+
"/api/v1": ["basic", "bearer"],
|
|
110
|
+
"/api/v2": "bearer"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Set `null` for the global default, a string for a single method, or an array for multiple. Route-level config takes highest priority, then prefix match, then global default.
|
|
116
|
+
|
|
117
|
+
Multi-identifier login lets users authenticate with any unique field (email, username, phone) in a single endpoint.
|
|
104
118
|
|
|
105
|
-
|
|
119
|
+
**Production:** `JWT_SECRET` and `JWT_REFRESH_SECRET` must be set explicitly. The server refuses to start without them to prevent session invalidation on restart.
|
|
120
|
+
|
|
121
|
+
> **[Authentication wiki](https://github.com/MertDalbudak/rapidd/wiki/Authentication)** — session stores, route protection, per-endpoint method overrides
|
|
106
122
|
|
|
107
123
|
---
|
|
108
124
|
|
|
@@ -126,7 +142,7 @@ const acl: AclConfig = {
|
|
|
126
142
|
|
|
127
143
|
Return `{}` for full access, a filter object to scope records, or `false` to deny.
|
|
128
144
|
|
|
129
|
-
>
|
|
145
|
+
> **[Access Control wiki](https://github.com/MertDalbudak/rapidd/wiki/Access-Control-(ACL))** — all 5 ACL methods, relation ACL, 404 vs 403 distinction
|
|
130
146
|
|
|
131
147
|
---
|
|
132
148
|
|
|
@@ -152,7 +168,7 @@ Model.middleware.use('before', 'delete', async (ctx) => {
|
|
|
152
168
|
|
|
153
169
|
Supports `create`, `update`, `upsert`, `upsertMany`, `delete`, `get`, `getMany`, and `count`. Middleware can abort operations, modify data, and short-circuit with cached results.
|
|
154
170
|
|
|
155
|
-
>
|
|
171
|
+
> **[Model Middleware wiki](https://github.com/MertDalbudak/rapidd/wiki/Model-Middleware)** — all hooks, context object, patterns (soft delete, validation, caching)
|
|
156
172
|
|
|
157
173
|
---
|
|
158
174
|
|
|
@@ -173,7 +189,7 @@ CREATE POLICY tenant_isolation ON orders
|
|
|
173
189
|
USING (tenant_id = current_setting('app.current_tenant_id')::int);
|
|
174
190
|
```
|
|
175
191
|
|
|
176
|
-
>
|
|
192
|
+
> **[Row-Level Security wiki](https://github.com/MertDalbudak/rapidd/wiki/Row%E2%80%90Level-Security-(RLS))** — policy examples, RLS vs ACL comparison
|
|
177
193
|
|
|
178
194
|
---
|
|
179
195
|
|
|
@@ -181,11 +197,11 @@ CREATE POLICY tenant_isolation ON orders
|
|
|
181
197
|
|
|
182
198
|
| Utility | Description | Docs |
|
|
183
199
|
|---------|-------------|------|
|
|
184
|
-
| **ApiClient** | Config-driven HTTP client with Bearer, Basic, API Key, and OAuth2 auth. Automatic token caching, retries, and fluent builder. | [Wiki
|
|
185
|
-
| **Mailer** | SMTP email with EJS template rendering, layout wrappers, i18n support, batch sending, and attachments. | [Wiki
|
|
186
|
-
| **File Uploads** | Multipart uploads with MIME validation, size limits, and type presets (`images`, `documents`, etc.). | [Wiki
|
|
187
|
-
| **Rate Limiting** | Redis-backed with automatic memory fallback. Per-path configuration via `config/rate-limit.json`. | [Wiki
|
|
188
|
-
| **i18n** | 10 languages included. Auto-detected from `Accept-Language` header. Parameter interpolation in error messages. | [Wiki
|
|
200
|
+
| **ApiClient** | Config-driven HTTP client with Bearer, Basic, API Key, and OAuth2 auth. Automatic token caching, retries, and fluent builder. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/ApiClient) |
|
|
201
|
+
| **Mailer** | SMTP email with EJS template rendering, layout wrappers, i18n support, batch sending, and attachments. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Mailer) |
|
|
202
|
+
| **File Uploads** | Multipart uploads with MIME validation, size limits, and type presets (`images`, `documents`, etc.). | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/File-Uploads) |
|
|
203
|
+
| **Rate Limiting** | Redis-backed with automatic memory fallback. Per-path configuration via `config/rate-limit.json`. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Rate-Limiting) |
|
|
204
|
+
| **i18n** | 10 languages included. Auto-detected from `Accept-Language` header. Parameter interpolation in error messages. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Internationalization-(i18n)) |
|
|
189
205
|
|
|
190
206
|
---
|
|
191
207
|
|
|
@@ -203,12 +219,28 @@ TRUST_PROXY=true
|
|
|
203
219
|
npm run build && npm start
|
|
204
220
|
|
|
205
221
|
# or Docker
|
|
206
|
-
docker build -t
|
|
222
|
+
docker build -t my-api . && docker run -p 3000:3000 --env-file .env my-api
|
|
207
223
|
```
|
|
208
224
|
|
|
209
225
|
**Security defaults in production:** HSTS, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, and CORS with explicit origin whitelisting — all enabled automatically.
|
|
210
226
|
|
|
211
|
-
>
|
|
227
|
+
> **[Deployment wiki](https://github.com/MertDalbudak/rapidd/wiki/Deployment-&-Production)** — Docker Compose, nginx reverse proxy, production checklist, horizontal scaling
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Packages
|
|
232
|
+
|
|
233
|
+
| Package | Description |
|
|
234
|
+
|---------|-------------|
|
|
235
|
+
| [`@rapidd/core`](https://www.npmjs.com/package/@rapidd/core) | Framework runtime, project scaffolding, and unified `npx rapidd` CLI |
|
|
236
|
+
| [`@rapidd/build`](https://www.npmjs.com/package/@rapidd/build) | Code generation — models, routes, and ACL from your Prisma schema |
|
|
237
|
+
|
|
238
|
+
All commands go through `npx rapidd`:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
npx rapidd create-project # scaffold a new project (@rapidd/core)
|
|
242
|
+
npx rapidd build # generate from schema (@rapidd/build)
|
|
243
|
+
```
|
|
212
244
|
|
|
213
245
|
---
|
|
214
246
|
|
package/bin/cli.js
CHANGED
|
@@ -2,20 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
5
6
|
|
|
6
7
|
const COMMANDS = { 'create-project': createProject };
|
|
7
8
|
|
|
8
9
|
const args = process.argv.slice(2);
|
|
9
10
|
const command = args[0];
|
|
10
11
|
|
|
11
|
-
if (!command
|
|
12
|
+
if (!command) {
|
|
12
13
|
console.log('Usage: npx rapidd <command>\n');
|
|
13
14
|
console.log('Commands:');
|
|
14
15
|
console.log(' create-project Scaffold a new Rapidd project in the current directory');
|
|
15
|
-
|
|
16
|
+
console.log(' build Generate models, routes & ACL from Prisma schema (@rapidd/build)');
|
|
17
|
+
process.exit(0);
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
COMMANDS[command]
|
|
20
|
+
if (COMMANDS[command]) {
|
|
21
|
+
COMMANDS[command](args.slice(1));
|
|
22
|
+
} else if (command === 'build') {
|
|
23
|
+
// Proxy to @rapidd/build
|
|
24
|
+
try {
|
|
25
|
+
const buildBin = require.resolve('@rapidd/build/bin/cli.js');
|
|
26
|
+
execFileSync(process.execPath, [buildBin, ...args], { stdio: 'inherit' });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err.code === 'MODULE_NOT_FOUND') {
|
|
29
|
+
console.error('@rapidd/build is not installed.\n');
|
|
30
|
+
console.error(' npm install -D @rapidd/build');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
process.exit(err.status ?? 1);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
console.error(`Unknown command: ${command}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
19
39
|
|
|
20
40
|
function createProject() {
|
|
21
41
|
const targetDir = process.cwd();
|
package/config/app.json
CHANGED
package/package.json
CHANGED
package/routes/api/v1/index.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
-
import { Auth } from '../../../src/auth/Auth';
|
|
3
2
|
import { ErrorResponse } from '../../../src/core/errors';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Auth routes — /register, /login, /logout, /refresh, /me
|
|
7
6
|
*/
|
|
8
7
|
const rootRoutes: FastifyPluginAsync = async (fastify) => {
|
|
9
|
-
const auth = fastify.auth
|
|
8
|
+
const auth = fastify.auth;
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* POST /register
|
package/src/auth/Auth.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { authPrisma } from '../core/prisma';
|
|
|
5
5
|
import { ErrorResponse } from '../core/errors';
|
|
6
6
|
import { createStore, SessionStoreManager } from './stores';
|
|
7
7
|
import { loadDMMF, findUserModel, findIdentifierFields, findPasswordField } from '../core/dmmf';
|
|
8
|
-
import type { RapiddUser, AuthOptions,
|
|
8
|
+
import type { RapiddUser, AuthOptions, AuthMethod, ISessionStore } from '../types';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Authentication class for user login, logout, and session management
|
|
@@ -26,7 +26,7 @@ import type { RapiddUser, AuthOptions, AuthStrategy, ISessionStore } from '../ty
|
|
|
26
26
|
export class Auth {
|
|
27
27
|
options: Required<Pick<AuthOptions, 'passwordField' | 'saltRounds'>> & AuthOptions & {
|
|
28
28
|
identifierFields: string[];
|
|
29
|
-
|
|
29
|
+
methods: AuthMethod[];
|
|
30
30
|
cookieName: string;
|
|
31
31
|
customHeaderName: string;
|
|
32
32
|
session: { ttl: number; store?: string };
|
|
@@ -68,8 +68,8 @@ export class Auth {
|
|
|
68
68
|
...options.jwt,
|
|
69
69
|
},
|
|
70
70
|
saltRounds: options.saltRounds || parseInt(process.env.AUTH_SALT_ROUNDS || '10', 10),
|
|
71
|
-
|
|
72
|
-
|| (process.env.
|
|
71
|
+
methods: options.methods
|
|
72
|
+
|| (process.env.AUTH_METHODS?.split(',').map(s => s.trim()) as AuthMethod[])
|
|
73
73
|
|| ['bearer'],
|
|
74
74
|
cookieName: options.cookieName || process.env.AUTH_COOKIE_NAME || 'token',
|
|
75
75
|
customHeaderName: options.customHeaderName || process.env.AUTH_CUSTOM_HEADER || 'X-Auth-Token',
|
package/src/index.ts
CHANGED
package/src/plugins/auth.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { FastifyPluginAsync, FastifyRequest } from 'fastify';
|
|
2
3
|
import fp from 'fastify-plugin';
|
|
3
4
|
import { Auth } from '../auth/Auth';
|
|
4
5
|
import { ErrorResponse } from '../core/errors';
|
|
5
|
-
import type { RapiddUser, AuthOptions,
|
|
6
|
+
import type { RapiddUser, AuthOptions, AuthMethod, RouteAuthConfig } from '../types';
|
|
6
7
|
|
|
7
8
|
interface AuthPluginOptions {
|
|
8
9
|
auth?: Auth;
|
|
@@ -35,20 +36,54 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
|
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
//
|
|
39
|
-
|
|
39
|
+
// Load endpointAuthMethod from config/app.json (prefix → method(s) mapping)
|
|
40
|
+
let endpointAuthMethod: Record<string, AuthMethod | AuthMethod[] | null> = {};
|
|
41
|
+
try {
|
|
42
|
+
const appConfig = require(path.join(process.cwd(), 'config', 'app.json'));
|
|
43
|
+
if (appConfig.endpointAuthMethod) {
|
|
44
|
+
endpointAuthMethod = appConfig.endpointAuthMethod;
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// No app.json or no endpointAuthMethod — use global default
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Pre-sort prefixes by length (longest first) for correct matching
|
|
51
|
+
const sortedPrefixes = Object.keys(endpointAuthMethod)
|
|
52
|
+
.sort((a, b) => b.length - a.length);
|
|
53
|
+
|
|
54
|
+
// Parse auth on every request using configured methods (checked in order).
|
|
55
|
+
// Priority: route config > endpointAuthMethod prefix match > global default
|
|
40
56
|
fastify.addHook('onRequest', async (request) => {
|
|
41
57
|
const routeAuth = (request.routeOptions?.config as any)?.auth as RouteAuthConfig | undefined;
|
|
42
|
-
|
|
58
|
+
|
|
59
|
+
let methods: AuthMethod[];
|
|
60
|
+
if (routeAuth?.methods) {
|
|
61
|
+
methods = routeAuth.methods;
|
|
62
|
+
} else {
|
|
63
|
+
const matchedPrefix = sortedPrefixes.find(p => request.url.startsWith(p));
|
|
64
|
+
if (matchedPrefix) {
|
|
65
|
+
const value = endpointAuthMethod[matchedPrefix];
|
|
66
|
+
if (value === null) {
|
|
67
|
+
methods = auth.options.methods;
|
|
68
|
+
} else if (typeof value === 'string') {
|
|
69
|
+
methods = [value];
|
|
70
|
+
} else {
|
|
71
|
+
methods = value;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
methods = auth.options.methods;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
43
78
|
const cookieName = routeAuth?.cookieName || auth.options.cookieName;
|
|
44
79
|
const customHeaderName = routeAuth?.customHeaderName || auth.options.customHeaderName;
|
|
45
80
|
|
|
46
81
|
let user: RapiddUser | null = null;
|
|
47
82
|
|
|
48
|
-
for (const
|
|
83
|
+
for (const method of methods) {
|
|
49
84
|
if (user) break;
|
|
50
85
|
|
|
51
|
-
switch (
|
|
86
|
+
switch (method) {
|
|
52
87
|
case 'bearer': {
|
|
53
88
|
const h = request.headers.authorization;
|
|
54
89
|
if (h?.startsWith('Bearer ')) {
|
|
@@ -90,7 +125,7 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
|
|
|
90
125
|
fastify.post('/auth/login', async (request, reply) => {
|
|
91
126
|
const result = await auth.login(request.body as { user: string; password: string });
|
|
92
127
|
|
|
93
|
-
if (auth.options.
|
|
128
|
+
if (auth.options.methods.includes('cookie')) {
|
|
94
129
|
reply.setCookie(auth.options.cookieName, result.accessToken, {
|
|
95
130
|
path: '/',
|
|
96
131
|
httpOnly: true,
|
|
@@ -106,7 +141,7 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
|
|
|
106
141
|
fastify.post('/auth/logout', async (request, reply) => {
|
|
107
142
|
const result = await auth.logout(request.headers.authorization);
|
|
108
143
|
|
|
109
|
-
if (auth.options.
|
|
144
|
+
if (auth.options.methods.includes('cookie')) {
|
|
110
145
|
reply.clearCookie(auth.options.cookieName, { path: '/' });
|
|
111
146
|
}
|
|
112
147
|
|
package/src/types.ts
CHANGED
|
@@ -10,10 +10,10 @@ export interface RapiddUser {
|
|
|
10
10
|
[key: string]: unknown;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export type
|
|
13
|
+
export type AuthMethod = 'bearer' | 'basic' | 'cookie' | 'header';
|
|
14
14
|
|
|
15
15
|
export interface RouteAuthConfig {
|
|
16
|
-
|
|
16
|
+
methods?: AuthMethod[];
|
|
17
17
|
cookieName?: string;
|
|
18
18
|
customHeaderName?: string;
|
|
19
19
|
}
|
|
@@ -32,7 +32,7 @@ export interface AuthOptions {
|
|
|
32
32
|
refreshExpiry?: string;
|
|
33
33
|
};
|
|
34
34
|
saltRounds?: number;
|
|
35
|
-
|
|
35
|
+
methods?: AuthMethod[];
|
|
36
36
|
cookieName?: string;
|
|
37
37
|
customHeaderName?: string;
|
|
38
38
|
}
|