@rapidd/core 2.1.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/.dockerignore +71 -0
- package/.env.example +70 -0
- package/.gitignore +11 -0
- package/LICENSE +15 -0
- package/README.md +231 -0
- package/bin/cli.js +145 -0
- package/config/app.json +166 -0
- package/config/rate-limit.json +12 -0
- package/dist/main.js +26 -0
- package/dockerfile +57 -0
- package/locales/ar_SA.json +179 -0
- package/locales/de_DE.json +179 -0
- package/locales/en_US.json +180 -0
- package/locales/es_ES.json +179 -0
- package/locales/fr_FR.json +179 -0
- package/locales/it_IT.json +179 -0
- package/locales/ja_JP.json +179 -0
- package/locales/pt_BR.json +179 -0
- package/locales/ru_RU.json +179 -0
- package/locales/tr_TR.json +179 -0
- package/main.ts +25 -0
- package/package.json +126 -0
- package/prisma/schema.prisma +9 -0
- package/prisma.config.ts +12 -0
- package/public/static/favicon.ico +0 -0
- package/public/static/image/logo.png +0 -0
- package/routes/api/v1/index.ts +113 -0
- package/src/app.ts +197 -0
- package/src/auth/Auth.ts +446 -0
- package/src/auth/stores/ISessionStore.ts +19 -0
- package/src/auth/stores/MemoryStore.ts +70 -0
- package/src/auth/stores/RedisStore.ts +92 -0
- package/src/auth/stores/index.ts +149 -0
- package/src/config/acl.ts +9 -0
- package/src/config/rls.ts +38 -0
- package/src/core/dmmf.ts +226 -0
- package/src/core/env.ts +183 -0
- package/src/core/errors.ts +87 -0
- package/src/core/i18n.ts +144 -0
- package/src/core/middleware.ts +123 -0
- package/src/core/prisma.ts +236 -0
- package/src/index.ts +112 -0
- package/src/middleware/model.ts +61 -0
- package/src/orm/Model.ts +881 -0
- package/src/orm/QueryBuilder.ts +2078 -0
- package/src/plugins/auth.ts +162 -0
- package/src/plugins/language.ts +79 -0
- package/src/plugins/rateLimit.ts +210 -0
- package/src/plugins/response.ts +80 -0
- package/src/plugins/rls.ts +51 -0
- package/src/plugins/security.ts +23 -0
- package/src/plugins/upload.ts +299 -0
- package/src/types.ts +308 -0
- package/src/utils/ApiClient.ts +526 -0
- package/src/utils/Mailer.ts +348 -0
- package/src/utils/index.ts +25 -0
- package/templates/email/example.ejs +17 -0
- package/templates/layouts/email.ejs +35 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import fp from 'fastify-plugin';
|
|
2
|
+
import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
3
|
+
import { createWriteStream, mkdirSync, existsSync } from 'fs';
|
|
4
|
+
import { pipeline } from 'stream/promises';
|
|
5
|
+
import { Transform } from 'stream';
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
|
|
9
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface UploadOptions {
|
|
12
|
+
maxFileSize?: number;
|
|
13
|
+
maxFiles?: number;
|
|
14
|
+
allowedTypes?: 'images' | 'documents' | 'media' | 'all' | AllowedType[];
|
|
15
|
+
tempDir?: string;
|
|
16
|
+
preserveExtension?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AllowedType {
|
|
20
|
+
mime: string;
|
|
21
|
+
extensions: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UploadedFile {
|
|
25
|
+
fieldname: string;
|
|
26
|
+
filename: string;
|
|
27
|
+
originalName: string;
|
|
28
|
+
mimetype: string;
|
|
29
|
+
size: number;
|
|
30
|
+
tempPath: string;
|
|
31
|
+
extension: string;
|
|
32
|
+
saveTo: (destination: string) => Promise<string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Type Presets ─────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const TYPE_PRESETS: Record<string, AllowedType[]> = {
|
|
38
|
+
images: [
|
|
39
|
+
{ mime: 'image/jpeg', extensions: ['.jpg', '.jpeg'] },
|
|
40
|
+
{ mime: 'image/png', extensions: ['.png'] },
|
|
41
|
+
{ mime: 'image/gif', extensions: ['.gif'] },
|
|
42
|
+
{ mime: 'image/webp', extensions: ['.webp'] },
|
|
43
|
+
{ mime: 'image/svg+xml', extensions: ['.svg'] }
|
|
44
|
+
],
|
|
45
|
+
documents: [
|
|
46
|
+
{ mime: 'application/pdf', extensions: ['.pdf'] },
|
|
47
|
+
{ mime: 'text/plain', extensions: ['.txt'] },
|
|
48
|
+
{ mime: 'application/msword', extensions: ['.doc'] },
|
|
49
|
+
{ mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', extensions: ['.docx'] },
|
|
50
|
+
{ mime: 'application/vnd.ms-excel', extensions: ['.xls'] },
|
|
51
|
+
{ mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', extensions: ['.xlsx'] },
|
|
52
|
+
{ mime: 'text/csv', extensions: ['.csv'] }
|
|
53
|
+
],
|
|
54
|
+
media: [
|
|
55
|
+
{ mime: 'image/jpeg', extensions: ['.jpg', '.jpeg'] },
|
|
56
|
+
{ mime: 'image/png', extensions: ['.png'] },
|
|
57
|
+
{ mime: 'image/gif', extensions: ['.gif'] },
|
|
58
|
+
{ mime: 'image/webp', extensions: ['.webp'] },
|
|
59
|
+
{ mime: 'video/mp4', extensions: ['.mp4'] },
|
|
60
|
+
{ mime: 'video/webm', extensions: ['.webm'] },
|
|
61
|
+
{ mime: 'audio/mpeg', extensions: ['.mp3'] },
|
|
62
|
+
{ mime: 'audio/wav', extensions: ['.wav'] }
|
|
63
|
+
],
|
|
64
|
+
all: []
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ── Defaults ─────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
|
70
|
+
const DEFAULT_MAX_FILES = 10;
|
|
71
|
+
const DEFAULT_TEMP_DIR = path.join(process.cwd(), 'temp', 'uploads');
|
|
72
|
+
|
|
73
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function getAllowedTypes(option: UploadOptions['allowedTypes']): AllowedType[] {
|
|
76
|
+
if (!option || option === 'all') {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(option)) {
|
|
80
|
+
return option;
|
|
81
|
+
}
|
|
82
|
+
return TYPE_PRESETS[option] || [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateFile(
|
|
86
|
+
file: { mimetype: string; filename: string },
|
|
87
|
+
allowedTypes: AllowedType[]
|
|
88
|
+
): { valid: boolean; error?: string } {
|
|
89
|
+
if (file.filename.includes('..') || file.filename.includes('/') || file.filename.includes('\\')) {
|
|
90
|
+
return { valid: false, error: 'Invalid filename' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (allowedTypes.length === 0) {
|
|
94
|
+
return { valid: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const ext = path.extname(file.filename).toLowerCase();
|
|
98
|
+
const allowedType = allowedTypes.find(t => t.mime === file.mimetype);
|
|
99
|
+
|
|
100
|
+
if (!allowedType) {
|
|
101
|
+
return { valid: false, error: `File type '${file.mimetype}' not allowed` };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!allowedType.extensions.includes(ext)) {
|
|
105
|
+
return { valid: false, error: `Extension '${ext}' does not match MIME type '${file.mimetype}'` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { valid: true };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createSizeTracker(maxSize: number): { tracker: Transform; getSize: () => number } {
|
|
112
|
+
let size = 0;
|
|
113
|
+
const tracker = new Transform({
|
|
114
|
+
transform(chunk, encoding, callback) {
|
|
115
|
+
size += chunk.length;
|
|
116
|
+
if (size > maxSize) {
|
|
117
|
+
callback(new Error(`File size exceeds limit of ${Math.round(maxSize / 1024 / 1024)}MB`));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
callback(null, chunk);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return { tracker, getSize: () => size };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function saveToTemp(
|
|
127
|
+
stream: NodeJS.ReadableStream,
|
|
128
|
+
tempDir: string,
|
|
129
|
+
filename: string,
|
|
130
|
+
maxSize: number
|
|
131
|
+
): Promise<{ tempPath: string; size: number }> {
|
|
132
|
+
if (!existsSync(tempDir)) {
|
|
133
|
+
mkdirSync(tempDir, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const tempPath = path.join(tempDir, `${randomUUID()}-${filename}`);
|
|
137
|
+
const writeStream = createWriteStream(tempPath);
|
|
138
|
+
const { tracker, getSize } = createSizeTracker(maxSize);
|
|
139
|
+
|
|
140
|
+
await pipeline(stream, tracker, writeStream);
|
|
141
|
+
|
|
142
|
+
return { tempPath, size: getSize() };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function createSaveToFn(tempPath: string, ext: string, preserveExtension: boolean) {
|
|
146
|
+
return async (destination: string): Promise<string> => {
|
|
147
|
+
const destDir = path.isAbsolute(destination)
|
|
148
|
+
? destination
|
|
149
|
+
: path.join(process.cwd(), destination);
|
|
150
|
+
|
|
151
|
+
if (!existsSync(destDir)) {
|
|
152
|
+
mkdirSync(destDir, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const finalName = preserveExtension ? `${randomUUID()}${ext}` : randomUUID();
|
|
156
|
+
const finalPath = path.join(destDir, finalName);
|
|
157
|
+
|
|
158
|
+
const fs = await import('fs/promises');
|
|
159
|
+
await fs.rename(tempPath, finalPath);
|
|
160
|
+
|
|
161
|
+
return finalPath;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Plugin ───────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Fastify multipart upload plugin with security validation.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* // Register plugin
|
|
172
|
+
* app.register(uploadPlugin, {
|
|
173
|
+
* maxFileSize: 5 * 1024 * 1024, // 5MB
|
|
174
|
+
* allowedTypes: 'images',
|
|
175
|
+
* tempDir: '/tmp/uploads'
|
|
176
|
+
* });
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* // In route - single file
|
|
180
|
+
* fastify.post('/upload', async (request, reply) => {
|
|
181
|
+
* const file = await request.uploadFile();
|
|
182
|
+
* if (!file) {
|
|
183
|
+
* return reply.sendError(400, 'no_file_uploaded');
|
|
184
|
+
* }
|
|
185
|
+
* const savedPath = await file.saveTo('./uploads');
|
|
186
|
+
* return { path: savedPath };
|
|
187
|
+
* });
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* // In route - multiple files
|
|
191
|
+
* fastify.post('/upload-many', async (request, reply) => {
|
|
192
|
+
* const files = await request.uploadFiles();
|
|
193
|
+
* const paths = await Promise.all(
|
|
194
|
+
* files.map(f => f.saveTo('./uploads'))
|
|
195
|
+
* );
|
|
196
|
+
* return { paths };
|
|
197
|
+
* });
|
|
198
|
+
*/
|
|
199
|
+
async function uploadPluginImpl(
|
|
200
|
+
fastify: FastifyInstance,
|
|
201
|
+
options: UploadOptions
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
204
|
+
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
205
|
+
const allowedTypes = getAllowedTypes(options.allowedTypes);
|
|
206
|
+
const tempDir = options.tempDir ?? DEFAULT_TEMP_DIR;
|
|
207
|
+
const preserveExtension = options.preserveExtension ?? true;
|
|
208
|
+
|
|
209
|
+
// Register @fastify/multipart
|
|
210
|
+
await fastify.register(import('@fastify/multipart'), {
|
|
211
|
+
limits: {
|
|
212
|
+
fileSize: maxFileSize,
|
|
213
|
+
files: maxFiles
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Add upload methods via hook (avoids decorateRequest typing issues)
|
|
218
|
+
fastify.addHook('preHandler', async (request: FastifyRequest, _reply: FastifyReply) => {
|
|
219
|
+
const contentType = request.headers['content-type'] || '';
|
|
220
|
+
|
|
221
|
+
// Always attach the methods, they'll handle non-multipart gracefully
|
|
222
|
+
(request as any).uploadFile = async (): Promise<UploadedFile | null> => {
|
|
223
|
+
if (!contentType.includes('multipart/form-data')) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const data = await (request as any).file();
|
|
228
|
+
if (!data) return null;
|
|
229
|
+
|
|
230
|
+
const validation = validateFile(data, allowedTypes);
|
|
231
|
+
if (!validation.valid) {
|
|
232
|
+
throw new Error(validation.error);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const { tempPath, size } = await saveToTemp(data.file, tempDir, data.filename, maxFileSize);
|
|
236
|
+
const ext = path.extname(data.filename);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
fieldname: data.fieldname,
|
|
240
|
+
filename: path.basename(tempPath),
|
|
241
|
+
originalName: data.filename,
|
|
242
|
+
mimetype: data.mimetype,
|
|
243
|
+
size,
|
|
244
|
+
tempPath,
|
|
245
|
+
extension: ext,
|
|
246
|
+
saveTo: createSaveToFn(tempPath, ext, preserveExtension)
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
(request as any).uploadFiles = async (): Promise<UploadedFile[]> => {
|
|
251
|
+
if (!contentType.includes('multipart/form-data')) {
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const files: UploadedFile[] = [];
|
|
256
|
+
const parts = (request as any).files();
|
|
257
|
+
|
|
258
|
+
for await (const part of parts) {
|
|
259
|
+
if (part.type !== 'file') continue;
|
|
260
|
+
|
|
261
|
+
const validation = validateFile(part, allowedTypes);
|
|
262
|
+
if (!validation.valid) {
|
|
263
|
+
throw new Error(validation.error);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const { tempPath, size } = await saveToTemp(part.file, tempDir, part.filename, maxFileSize);
|
|
267
|
+
const ext = path.extname(part.filename);
|
|
268
|
+
|
|
269
|
+
files.push({
|
|
270
|
+
fieldname: part.fieldname,
|
|
271
|
+
filename: path.basename(tempPath),
|
|
272
|
+
originalName: part.filename,
|
|
273
|
+
mimetype: part.mimetype,
|
|
274
|
+
size,
|
|
275
|
+
tempPath,
|
|
276
|
+
extension: ext,
|
|
277
|
+
saveTo: createSaveToFn(tempPath, ext, preserveExtension)
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return files;
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Extend FastifyRequest type
|
|
287
|
+
declare module 'fastify' {
|
|
288
|
+
interface FastifyRequest {
|
|
289
|
+
uploadFile: () => Promise<UploadedFile | null>;
|
|
290
|
+
uploadFiles: () => Promise<UploadedFile[]>;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export const uploadPlugin = fp(uploadPluginImpl, {
|
|
295
|
+
name: 'upload',
|
|
296
|
+
fastify: '5.x'
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
export default uploadPlugin;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
+
|
|
3
|
+
// =====================================================
|
|
4
|
+
// USER & AUTH
|
|
5
|
+
// =====================================================
|
|
6
|
+
|
|
7
|
+
export interface RapiddUser {
|
|
8
|
+
id: string | number;
|
|
9
|
+
role: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type AuthStrategy = 'bearer' | 'basic' | 'cookie' | 'header';
|
|
14
|
+
|
|
15
|
+
export interface RouteAuthConfig {
|
|
16
|
+
strategies?: AuthStrategy[];
|
|
17
|
+
cookieName?: string;
|
|
18
|
+
customHeaderName?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthOptions {
|
|
22
|
+
userModel?: string;
|
|
23
|
+
userSelect?: Record<string, boolean> | null;
|
|
24
|
+
userInclude?: Record<string, boolean | object> | null;
|
|
25
|
+
identifierFields?: string[];
|
|
26
|
+
passwordField?: string;
|
|
27
|
+
session?: { ttl?: number; store?: string };
|
|
28
|
+
jwt?: {
|
|
29
|
+
secret?: string;
|
|
30
|
+
refreshSecret?: string;
|
|
31
|
+
accessExpiry?: string;
|
|
32
|
+
refreshExpiry?: string;
|
|
33
|
+
};
|
|
34
|
+
saltRounds?: number;
|
|
35
|
+
strategies?: AuthStrategy[];
|
|
36
|
+
cookieName?: string;
|
|
37
|
+
customHeaderName?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// =====================================================
|
|
41
|
+
// ACL
|
|
42
|
+
// =====================================================
|
|
43
|
+
|
|
44
|
+
export interface ModelAcl {
|
|
45
|
+
canCreate?: (user: RapiddUser, data?: Record<string, unknown>) => boolean;
|
|
46
|
+
getAccessFilter?: (user: RapiddUser) => Record<string, unknown> | boolean;
|
|
47
|
+
getUpdateFilter?: (user: RapiddUser) => Record<string, unknown> | boolean | false;
|
|
48
|
+
getDeleteFilter?: (user: RapiddUser) => Record<string, unknown> | boolean | false;
|
|
49
|
+
getOmitFields?: (user: RapiddUser) => string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AclConfig {
|
|
53
|
+
model: Record<string, ModelAcl>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =====================================================
|
|
57
|
+
// MIDDLEWARE
|
|
58
|
+
// =====================================================
|
|
59
|
+
|
|
60
|
+
export type MiddlewareHook = 'before' | 'after';
|
|
61
|
+
export type MiddlewareOperation =
|
|
62
|
+
| 'create' | 'update' | 'upsert' | 'upsertMany'
|
|
63
|
+
| 'delete' | 'get' | 'getMany' | 'count';
|
|
64
|
+
|
|
65
|
+
export interface MiddlewareContext {
|
|
66
|
+
model: { name: string };
|
|
67
|
+
operation: string;
|
|
68
|
+
user: RapiddUser | null;
|
|
69
|
+
timestamp: Date;
|
|
70
|
+
abort: boolean;
|
|
71
|
+
skip: boolean;
|
|
72
|
+
softDelete: boolean;
|
|
73
|
+
data?: Record<string, unknown>;
|
|
74
|
+
id?: string | number;
|
|
75
|
+
result?: unknown;
|
|
76
|
+
query?: string | Record<string, unknown>;
|
|
77
|
+
include?: string | Record<string, unknown>;
|
|
78
|
+
take?: number;
|
|
79
|
+
skip_offset?: number;
|
|
80
|
+
sortBy?: string;
|
|
81
|
+
sortOrder?: string;
|
|
82
|
+
options?: Record<string, unknown>;
|
|
83
|
+
fields?: string | null;
|
|
84
|
+
unique_key?: string | string[];
|
|
85
|
+
prismaOptions?: Record<string, unknown>;
|
|
86
|
+
[key: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type MiddlewareFn = (context: MiddlewareContext) => Promise<MiddlewareContext | void> | MiddlewareContext | void;
|
|
90
|
+
|
|
91
|
+
// =====================================================
|
|
92
|
+
// API RESPONSES
|
|
93
|
+
// =====================================================
|
|
94
|
+
|
|
95
|
+
export interface ListMeta {
|
|
96
|
+
take: number;
|
|
97
|
+
skip: number;
|
|
98
|
+
total?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface ListResponseBody<T = unknown> {
|
|
102
|
+
data: T[];
|
|
103
|
+
meta: {
|
|
104
|
+
total?: number;
|
|
105
|
+
count: number;
|
|
106
|
+
limit: number;
|
|
107
|
+
offset: number;
|
|
108
|
+
hasMore?: boolean;
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ErrorResponseBody {
|
|
113
|
+
status_code: number;
|
|
114
|
+
message: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// =====================================================
|
|
118
|
+
// DMMF
|
|
119
|
+
// =====================================================
|
|
120
|
+
|
|
121
|
+
export interface DMMFField {
|
|
122
|
+
name: string;
|
|
123
|
+
kind: string;
|
|
124
|
+
type: string;
|
|
125
|
+
isList: boolean;
|
|
126
|
+
isRequired: boolean;
|
|
127
|
+
isId: boolean;
|
|
128
|
+
isUnique: boolean;
|
|
129
|
+
relationFromFields?: string[];
|
|
130
|
+
relationToFields?: string[];
|
|
131
|
+
relationName?: string;
|
|
132
|
+
relationOnDelete?: string;
|
|
133
|
+
[key: string]: unknown;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface DMMFModel {
|
|
137
|
+
name: string;
|
|
138
|
+
fields: DMMFField[];
|
|
139
|
+
primaryKey?: { fields: string[] } | null;
|
|
140
|
+
[key: string]: unknown;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface DMMF {
|
|
144
|
+
datamodel: {
|
|
145
|
+
models: DMMFModel[];
|
|
146
|
+
[key: string]: unknown;
|
|
147
|
+
};
|
|
148
|
+
[key: string]: unknown;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface RelationConfig {
|
|
152
|
+
name: string;
|
|
153
|
+
object: string;
|
|
154
|
+
isList: boolean;
|
|
155
|
+
field?: string;
|
|
156
|
+
foreignKey?: string;
|
|
157
|
+
fields?: string[];
|
|
158
|
+
foreignKeys?: string[];
|
|
159
|
+
relation?: RelationConfig[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// =====================================================
|
|
163
|
+
// PRISMA / RLS
|
|
164
|
+
// =====================================================
|
|
165
|
+
|
|
166
|
+
export type RLSVariables = Record<string, string | number | null>;
|
|
167
|
+
|
|
168
|
+
export type RlsContextFn = (request: any) => RLSVariables | Promise<RLSVariables>;
|
|
169
|
+
|
|
170
|
+
export interface RLSConfig {
|
|
171
|
+
namespace: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type DatabaseProvider = 'postgresql' | 'mysql';
|
|
175
|
+
|
|
176
|
+
export interface AdapterResult {
|
|
177
|
+
adapter: unknown;
|
|
178
|
+
pool: unknown | null;
|
|
179
|
+
provider: DatabaseProvider;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// =====================================================
|
|
183
|
+
// QUERY BUILDER
|
|
184
|
+
// =====================================================
|
|
185
|
+
|
|
186
|
+
export interface PrismaWhereClause {
|
|
187
|
+
[key: string]: unknown;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface PrismaIncludeClause {
|
|
191
|
+
[key: string]: boolean | PrismaIncludeContent;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface PrismaIncludeContent {
|
|
195
|
+
include?: PrismaIncludeClause;
|
|
196
|
+
where?: PrismaWhereClause;
|
|
197
|
+
omit?: Record<string, boolean>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface PrismaOrderBy {
|
|
201
|
+
[key: string]: 'asc' | 'desc' | PrismaOrderBy;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface PrismaErrorInfo {
|
|
205
|
+
status: number;
|
|
206
|
+
message: string | null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface QueryErrorResponse {
|
|
210
|
+
status_code: number;
|
|
211
|
+
message: string;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// =====================================================
|
|
215
|
+
// RATE LIMITER
|
|
216
|
+
// =====================================================
|
|
217
|
+
|
|
218
|
+
export interface RateLimitPathConfig {
|
|
219
|
+
maxRequests: number;
|
|
220
|
+
windowMs: number;
|
|
221
|
+
ignoreSuccessfulRequests?: boolean;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface RateLimitResult {
|
|
225
|
+
allowed: boolean;
|
|
226
|
+
count: number;
|
|
227
|
+
resetTime: number;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// =====================================================
|
|
231
|
+
// SESSION STORE
|
|
232
|
+
// =====================================================
|
|
233
|
+
|
|
234
|
+
export interface ISessionStore {
|
|
235
|
+
create(sessionId: string, data: Record<string, unknown>): Promise<void>;
|
|
236
|
+
get(sessionId: string): Promise<Record<string, unknown> | null>;
|
|
237
|
+
delete(sessionId: string): Promise<void>;
|
|
238
|
+
refresh(sessionId: string): Promise<void>;
|
|
239
|
+
isHealthy(): Promise<boolean>;
|
|
240
|
+
destroy?(): void | Promise<void>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// =====================================================
|
|
244
|
+
// MODEL
|
|
245
|
+
// =====================================================
|
|
246
|
+
|
|
247
|
+
export interface ModelOptions {
|
|
248
|
+
user?: RapiddUser;
|
|
249
|
+
[key: string]: unknown;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface GetManyResult<T = Record<string, unknown>> {
|
|
253
|
+
data: T[];
|
|
254
|
+
meta: { take: number; skip: number; total: number };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface UpsertManyResult {
|
|
258
|
+
created: number;
|
|
259
|
+
updated: number;
|
|
260
|
+
failed: Array<{ record?: unknown; records?: unknown; error: Error }>;
|
|
261
|
+
totalSuccess: number;
|
|
262
|
+
totalFailed: number;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface UpsertManyOptions {
|
|
266
|
+
validateRelation?: boolean;
|
|
267
|
+
transaction?: boolean;
|
|
268
|
+
timeout?: number;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// =====================================================
|
|
272
|
+
// FASTIFY TYPE AUGMENTATION
|
|
273
|
+
// =====================================================
|
|
274
|
+
|
|
275
|
+
declare module 'fastify' {
|
|
276
|
+
interface FastifyRequest {
|
|
277
|
+
user: RapiddUser | null;
|
|
278
|
+
language: string;
|
|
279
|
+
remoteAddress: string;
|
|
280
|
+
getTranslation(key: string, data?: Record<string, unknown> | null, language?: string): string;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
interface FastifyReply {
|
|
284
|
+
sendList(data: unknown[], meta: ListMeta): FastifyReply;
|
|
285
|
+
sendError(statusCode: number, message: string, data?: unknown): FastifyReply;
|
|
286
|
+
sendResponse(statusCode: number, message: string, params?: unknown): FastifyReply;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// =====================================================
|
|
291
|
+
// APP CONFIG
|
|
292
|
+
// =====================================================
|
|
293
|
+
|
|
294
|
+
export interface AppConfig {
|
|
295
|
+
languages: string[];
|
|
296
|
+
database?: Record<string, unknown>;
|
|
297
|
+
services?: Record<string, unknown>;
|
|
298
|
+
emails?: Record<string, unknown>;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface RapiddOptions {
|
|
302
|
+
routesPath?: string;
|
|
303
|
+
stringsPath?: string;
|
|
304
|
+
publicPath?: string;
|
|
305
|
+
config?: AppConfig;
|
|
306
|
+
cors?: Record<string, unknown>;
|
|
307
|
+
rateLimit?: boolean;
|
|
308
|
+
}
|