@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.
Files changed (59) hide show
  1. package/.dockerignore +71 -0
  2. package/.env.example +70 -0
  3. package/.gitignore +11 -0
  4. package/LICENSE +15 -0
  5. package/README.md +231 -0
  6. package/bin/cli.js +145 -0
  7. package/config/app.json +166 -0
  8. package/config/rate-limit.json +12 -0
  9. package/dist/main.js +26 -0
  10. package/dockerfile +57 -0
  11. package/locales/ar_SA.json +179 -0
  12. package/locales/de_DE.json +179 -0
  13. package/locales/en_US.json +180 -0
  14. package/locales/es_ES.json +179 -0
  15. package/locales/fr_FR.json +179 -0
  16. package/locales/it_IT.json +179 -0
  17. package/locales/ja_JP.json +179 -0
  18. package/locales/pt_BR.json +179 -0
  19. package/locales/ru_RU.json +179 -0
  20. package/locales/tr_TR.json +179 -0
  21. package/main.ts +25 -0
  22. package/package.json +126 -0
  23. package/prisma/schema.prisma +9 -0
  24. package/prisma.config.ts +12 -0
  25. package/public/static/favicon.ico +0 -0
  26. package/public/static/image/logo.png +0 -0
  27. package/routes/api/v1/index.ts +113 -0
  28. package/src/app.ts +197 -0
  29. package/src/auth/Auth.ts +446 -0
  30. package/src/auth/stores/ISessionStore.ts +19 -0
  31. package/src/auth/stores/MemoryStore.ts +70 -0
  32. package/src/auth/stores/RedisStore.ts +92 -0
  33. package/src/auth/stores/index.ts +149 -0
  34. package/src/config/acl.ts +9 -0
  35. package/src/config/rls.ts +38 -0
  36. package/src/core/dmmf.ts +226 -0
  37. package/src/core/env.ts +183 -0
  38. package/src/core/errors.ts +87 -0
  39. package/src/core/i18n.ts +144 -0
  40. package/src/core/middleware.ts +123 -0
  41. package/src/core/prisma.ts +236 -0
  42. package/src/index.ts +112 -0
  43. package/src/middleware/model.ts +61 -0
  44. package/src/orm/Model.ts +881 -0
  45. package/src/orm/QueryBuilder.ts +2078 -0
  46. package/src/plugins/auth.ts +162 -0
  47. package/src/plugins/language.ts +79 -0
  48. package/src/plugins/rateLimit.ts +210 -0
  49. package/src/plugins/response.ts +80 -0
  50. package/src/plugins/rls.ts +51 -0
  51. package/src/plugins/security.ts +23 -0
  52. package/src/plugins/upload.ts +299 -0
  53. package/src/types.ts +308 -0
  54. package/src/utils/ApiClient.ts +526 -0
  55. package/src/utils/Mailer.ts +348 -0
  56. package/src/utils/index.ts +25 -0
  57. package/templates/email/example.ejs +17 -0
  58. package/templates/layouts/email.ejs +35 -0
  59. package/tsconfig.json +33 -0
@@ -0,0 +1,144 @@
1
+ import { readdirSync } from 'fs';
2
+ import path from 'path';
3
+
4
+ const ROOT = process.env.ROOT || '';
5
+ const DEFAULT_STRINGS_PATH = ROOT ? path.join(ROOT, 'locales') : './locale';
6
+
7
+ /**
8
+ * Singleton LanguageDict class for efficient translation management.
9
+ * All dictionaries are loaded once at initialization and cached in memory.
10
+ */
11
+ export class LanguageDict {
12
+ private static _dictionaries: Record<string, Record<string, string>> = {};
13
+ private static _available: string[] = [];
14
+ private static _dictionaryPath: string | null = null;
15
+ private static _defaultLanguage: string = 'en_US';
16
+ private static _initialized: boolean = false;
17
+
18
+ /**
19
+ * Initialize the LanguageDict system
20
+ */
21
+ static initialize(dictionaryPath: string = DEFAULT_STRINGS_PATH, defaultLanguage: string = 'en_US'): void {
22
+ if (this._initialized && this._dictionaryPath === dictionaryPath) {
23
+ return;
24
+ }
25
+
26
+ this._dictionaryPath = dictionaryPath;
27
+ this._defaultLanguage = defaultLanguage;
28
+ this._dictionaries = {};
29
+ this._available = [];
30
+
31
+ try {
32
+ const files = readdirSync(dictionaryPath);
33
+
34
+ for (const fileName of files) {
35
+ if (path.extname(fileName) === '.json') {
36
+ const langCode = path.parse(fileName).name;
37
+ try {
38
+ const dictPath = path.join(dictionaryPath, fileName);
39
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
40
+ this._dictionaries[langCode] = require(dictPath);
41
+ this._available.push(langCode);
42
+ } catch (error) {
43
+ console.error(`Failed to load dictionary for ${langCode}:`, (error as Error).message);
44
+ }
45
+ }
46
+ }
47
+
48
+ this._initialized = true;
49
+ } catch (error) {
50
+ console.error('Failed to initialize LanguageDict:', (error as Error).message);
51
+ this._dictionaries = {};
52
+ this._available = [];
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Get a translated string with optional parameter interpolation
58
+ */
59
+ static get(key: string, data: Record<string, unknown> | null = null, language: string | null = null): string {
60
+ if (!this._initialized) {
61
+ this.initialize();
62
+ }
63
+
64
+ const lang = language || this._defaultLanguage;
65
+ const dictionary = this._dictionaries[lang] || this._dictionaries[this._defaultLanguage] || {};
66
+
67
+ let translated: string | undefined = dictionary[key];
68
+
69
+ if (translated === undefined) {
70
+ return key;
71
+ }
72
+
73
+ // Handle nested translations ({{key}} syntax)
74
+ translated = translated.replace(/{{\w+}}/g, (match: string) => {
75
+ const nestedKey = match.slice(2, -2);
76
+ return this.get(nestedKey, data, lang);
77
+ });
78
+
79
+ // Handle parameter interpolation ({key} syntax)
80
+ if (data !== null && typeof data === 'object') {
81
+ translated = translated.replace(/\{(\w+)\}/g, (match: string, paramKey: string) => {
82
+ return data[paramKey] !== undefined ? String(data[paramKey]) : match;
83
+ });
84
+ }
85
+
86
+ return translated;
87
+ }
88
+
89
+ /**
90
+ * Get all available language codes
91
+ */
92
+ static getAvailableLanguages(): string[] {
93
+ if (!this._initialized) {
94
+ this.initialize();
95
+ }
96
+ return [...this._available];
97
+ }
98
+
99
+ /**
100
+ * Get the entire dictionary for a specific language
101
+ */
102
+ static getDictionary(language: string): Record<string, string> {
103
+ if (!this._initialized) {
104
+ this.initialize();
105
+ }
106
+ return this._dictionaries[language] || this._dictionaries[this._defaultLanguage] || {};
107
+ }
108
+
109
+ /**
110
+ * Check if a language is available
111
+ */
112
+ static hasLanguage(language: string): boolean {
113
+ if (!this._initialized) {
114
+ this.initialize();
115
+ }
116
+ return this._available.includes(language);
117
+ }
118
+
119
+ /**
120
+ * Reload dictionaries from disk
121
+ */
122
+ static reload(): void {
123
+ this._initialized = false;
124
+ this.initialize(this._dictionaryPath || DEFAULT_STRINGS_PATH, this._defaultLanguage);
125
+ }
126
+
127
+ // Instance-based API for backward compatibility
128
+ private language: string;
129
+
130
+ constructor(language: string) {
131
+ if (!LanguageDict._initialized) {
132
+ LanguageDict.initialize();
133
+ }
134
+ this.language = language;
135
+ }
136
+
137
+ get(key: string, data: Record<string, unknown> | null = null): string {
138
+ return LanguageDict.get(key, data, this.language);
139
+ }
140
+
141
+ getDictionary(): Record<string, string> {
142
+ return LanguageDict.getDictionary(this.language);
143
+ }
144
+ }
@@ -0,0 +1,123 @@
1
+ import type { MiddlewareHook, MiddlewareOperation, MiddlewareContext, MiddlewareFn } from '../types';
2
+
3
+ const middlewareRegistry = new Map<string, MiddlewareFn[]>();
4
+
5
+ const OPERATIONS: MiddlewareOperation[] = ['create', 'update', 'upsert', 'upsertMany', 'delete', 'get', 'getMany', 'count'];
6
+ const HOOKS: MiddlewareHook[] = ['before', 'after'];
7
+
8
+ function getKey(hook: string, operation: string, model: string = '*'): string {
9
+ return `${hook}:${operation}:${model}`;
10
+ }
11
+
12
+ /**
13
+ * Register a middleware function
14
+ */
15
+ function use(hook: MiddlewareHook, operation: MiddlewareOperation, fn: MiddlewareFn, model: string = '*'): void {
16
+ if (!HOOKS.includes(hook)) {
17
+ throw new Error(`Invalid hook '${hook}'. Must be one of: ${HOOKS.join(', ')}`);
18
+ }
19
+ if (!OPERATIONS.includes(operation)) {
20
+ throw new Error(`Invalid operation '${operation}'. Must be one of: ${OPERATIONS.join(', ')}`);
21
+ }
22
+ if (typeof fn !== 'function') {
23
+ throw new Error('Middleware must be a function');
24
+ }
25
+
26
+ const key = getKey(hook, operation, model);
27
+ if (!middlewareRegistry.has(key)) {
28
+ middlewareRegistry.set(key, []);
29
+ }
30
+ middlewareRegistry.get(key)!.push(fn);
31
+ }
32
+
33
+ /**
34
+ * Remove a specific middleware function
35
+ */
36
+ function remove(hook: MiddlewareHook, operation: MiddlewareOperation, fn: MiddlewareFn, model: string = '*'): boolean {
37
+ const key = getKey(hook, operation, model);
38
+ const middlewares = middlewareRegistry.get(key);
39
+ if (!middlewares) return false;
40
+
41
+ const index = middlewares.indexOf(fn);
42
+ if (index > -1) {
43
+ middlewares.splice(index, 1);
44
+ return true;
45
+ }
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * Clear all middleware for a specific hook/operation/model combination
51
+ */
52
+ function clear(hook?: MiddlewareHook, operation?: MiddlewareOperation, model?: string): void {
53
+ if (!hook && !operation && !model) {
54
+ middlewareRegistry.clear();
55
+ return;
56
+ }
57
+ const key = getKey(hook!, operation!, model);
58
+ middlewareRegistry.delete(key);
59
+ }
60
+
61
+ /**
62
+ * Get all middleware functions for a specific hook/operation/model.
63
+ * Returns both model-specific and global ('*') middleware.
64
+ */
65
+ function getMiddleware(hook: MiddlewareHook, operation: MiddlewareOperation, model: string): MiddlewareFn[] {
66
+ const globalKey = getKey(hook, operation, '*');
67
+ const modelKey = getKey(hook, operation, model);
68
+
69
+ const globalMiddleware = middlewareRegistry.get(globalKey) || [];
70
+ const modelSpecific = middlewareRegistry.get(modelKey) || [];
71
+
72
+ return [...globalMiddleware, ...modelSpecific];
73
+ }
74
+
75
+ /**
76
+ * Execute middleware chain for a given context
77
+ */
78
+ async function execute(hook: MiddlewareHook, operation: MiddlewareOperation, context: MiddlewareContext): Promise<MiddlewareContext> {
79
+ const middlewares = getMiddleware(hook, operation, context.model.name);
80
+
81
+ let ctx: MiddlewareContext = { ...context };
82
+ for (const fn of middlewares) {
83
+ const result = await fn(ctx);
84
+ if (result !== undefined) {
85
+ ctx = result;
86
+ }
87
+ if (ctx.abort) break;
88
+ }
89
+
90
+ return ctx;
91
+ }
92
+
93
+ /**
94
+ * Create a middleware context object
95
+ */
96
+ function createContext(
97
+ model: { name: string },
98
+ operation: string,
99
+ params: Record<string, unknown>,
100
+ user: MiddlewareContext['user'] = null
101
+ ): MiddlewareContext {
102
+ return {
103
+ model,
104
+ operation,
105
+ user,
106
+ timestamp: new Date(),
107
+ ...params,
108
+ abort: false,
109
+ skip: false,
110
+ softDelete: false,
111
+ } as MiddlewareContext;
112
+ }
113
+
114
+ export const modelMiddleware = {
115
+ use,
116
+ remove,
117
+ clear,
118
+ getMiddleware,
119
+ execute,
120
+ createContext,
121
+ OPERATIONS,
122
+ HOOKS,
123
+ };
@@ -0,0 +1,236 @@
1
+ import { AsyncLocalStorage } from 'async_hooks';
2
+ import path from 'path';
3
+ import type { RLSVariables, DatabaseProvider, AdapterResult, AclConfig } from '../types';
4
+ import * as dmmf from './dmmf';
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
7
+ const { PrismaClient, Prisma } = require(path.join(process.cwd(), 'prisma', 'client'));
8
+
9
+ /** Request context storage for RLS variables across async operations */
10
+ export const requestContext = new AsyncLocalStorage<{ variables: RLSVariables }>();
11
+
12
+ /** Validates that an RLS identifier contains only safe characters (letters, digits, underscores) */
13
+ const IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
14
+ function validateIdentifier(value: string, name: string): string {
15
+ if (!IDENTIFIER_RE.test(value)) {
16
+ throw new Error(`[RLS] Invalid identifier for ${name}: "${value}". Only letters, numbers, and underscores allowed.`);
17
+ }
18
+ return value;
19
+ }
20
+
21
+ /** RLS namespace for SQL session variable prefix */
22
+ const RLS_NAMESPACE = validateIdentifier(process.env.RLS_NAMESPACE || 'app', 'RLS_NAMESPACE');
23
+
24
+ // =====================================================
25
+ // DATABASE ADAPTER FACTORY
26
+ // =====================================================
27
+
28
+ function detectProvider(connectionString: string): DatabaseProvider {
29
+ if (!connectionString) return 'postgresql';
30
+ if (connectionString.startsWith('mysql://') || connectionString.startsWith('mariadb://')) {
31
+ return 'mysql';
32
+ }
33
+ return 'postgresql';
34
+ }
35
+
36
+ function parseMySQLConnectionString(connectionString: string): Record<string, unknown> {
37
+ const url = new URL(connectionString);
38
+ return {
39
+ host: url.hostname,
40
+ port: parseInt(url.port, 10) || 3306,
41
+ user: url.username,
42
+ password: url.password,
43
+ database: url.pathname.slice(1),
44
+ connectionLimit: 10,
45
+ };
46
+ }
47
+
48
+ export function createAdapter(connectionString: string, provider: string | null = null): AdapterResult {
49
+ const detectedProvider = (provider || process.env.DATABASE_PROVIDER || detectProvider(connectionString)) as DatabaseProvider;
50
+
51
+ if (detectedProvider === 'mysql' || (detectedProvider as string) === 'mariadb') {
52
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
53
+ const { PrismaMariaDb } = require('@prisma/adapter-mariadb');
54
+ const config = parseMySQLConnectionString(connectionString);
55
+ const adapter = new PrismaMariaDb(config);
56
+ return { adapter, pool: null, provider: 'mysql' };
57
+ } else {
58
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
59
+ const { PrismaPg } = require('@prisma/adapter-pg');
60
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
61
+ const { Pool } = require('pg');
62
+ const pool = new Pool({ connectionString });
63
+ const adapter = new PrismaPg(pool);
64
+ return { adapter, pool, provider: 'postgresql' };
65
+ }
66
+ }
67
+
68
+ // =====================================================
69
+ // BASE PRISMA CLIENTS
70
+ // =====================================================
71
+
72
+ const authConnection = createAdapter(process.env.DATABASE_URL_ADMIN || process.env.DATABASE_URL || '');
73
+ const baseConnection = createAdapter(process.env.DATABASE_URL || '');
74
+
75
+ export const dbProvider: DatabaseProvider = baseConnection.provider;
76
+
77
+ /** Whether RLS is enabled. Auto: true for PostgreSQL, false for MySQL. Override with RLS_ENABLED env var. */
78
+ export const rlsEnabled: boolean = process.env.RLS_ENABLED !== undefined
79
+ ? process.env.RLS_ENABLED === 'true'
80
+ : dbProvider === 'postgresql';
81
+
82
+ export const authPrisma = new PrismaClient({
83
+ adapter: authConnection.adapter,
84
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
85
+ });
86
+
87
+ const basePrisma = new PrismaClient({
88
+ adapter: baseConnection.adapter,
89
+ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
90
+ });
91
+
92
+ // =====================================================
93
+ // RLS HELPER FUNCTIONS
94
+ // =====================================================
95
+
96
+ function sanitizeRLSValue(value: string | number | null | undefined): string {
97
+ if (value === null || value === undefined) return '';
98
+ return String(value).replace(/'/g, "''");
99
+ }
100
+
101
+ export async function setRLSVariables(tx: any, variables: RLSVariables): Promise<void> {
102
+ for (const [name, value] of Object.entries(variables)) {
103
+ if (value === null || value === undefined) continue;
104
+ const safeName = validateIdentifier(name, name);
105
+ const safeValue = sanitizeRLSValue(value);
106
+
107
+ if (dbProvider === 'mysql') {
108
+ await tx.$executeRawUnsafe(`SET @${RLS_NAMESPACE}_${safeName} = '${safeValue}'`);
109
+ } else {
110
+ await tx.$executeRawUnsafe(`SET LOCAL ${RLS_NAMESPACE}.${safeName} = '${safeValue}'`);
111
+ }
112
+ }
113
+ }
114
+
115
+ export async function resetRLSVariables(tx: any, variables: RLSVariables): Promise<void> {
116
+ try {
117
+ for (const name of Object.keys(variables)) {
118
+ const safeName = validateIdentifier(name, name);
119
+ if (dbProvider === 'mysql') {
120
+ await tx.$executeRawUnsafe(`SET @${RLS_NAMESPACE}_${safeName} = NULL`);
121
+ } else {
122
+ await tx.$executeRawUnsafe(`RESET ${RLS_NAMESPACE}.${safeName}`);
123
+ }
124
+ }
125
+ } catch {
126
+ // Ignore errors on reset
127
+ }
128
+ }
129
+
130
+ // =====================================================
131
+ // EXTENDED PRISMA WITH AUTOMATIC RLS
132
+ // =====================================================
133
+
134
+ export const prisma = basePrisma.$extends({
135
+ query: {
136
+ async $allOperations({ operation, args, query, model }: any) {
137
+ if (!rlsEnabled) return query(args);
138
+
139
+ const context = requestContext.getStore();
140
+ const variables = context?.variables;
141
+
142
+ if (!variables || Object.keys(variables).length === 0) {
143
+ return query(args);
144
+ }
145
+
146
+ if (operation === '$transaction') {
147
+ return basePrisma.$transaction(async (tx: any) => {
148
+ await setRLSVariables(tx, variables);
149
+ return query(args);
150
+ });
151
+ }
152
+
153
+ return basePrisma.$transaction(async (tx: any) => {
154
+ await setRLSVariables(tx, variables);
155
+
156
+ if (model) {
157
+ return tx[model][operation](args);
158
+ } else {
159
+ return tx[operation](args);
160
+ }
161
+ });
162
+ },
163
+ },
164
+ });
165
+
166
+ // =====================================================
167
+ // TRANSACTION HELPERS
168
+ // =====================================================
169
+
170
+ export async function prismaTransaction(
171
+ callback: ((tx: any) => Promise<any>) | Array<(tx: any) => Promise<any>>,
172
+ options?: { timeout?: number }
173
+ ): Promise<any> {
174
+ const context = requestContext.getStore();
175
+
176
+ return basePrisma.$transaction(async (tx: any) => {
177
+ const variables = context?.variables;
178
+ if (rlsEnabled && variables && Object.keys(variables).length > 0) {
179
+ await setRLSVariables(tx, variables);
180
+ }
181
+
182
+ if (Array.isArray(callback)) {
183
+ return await Promise.all(callback.map((fn: (tx: any) => Promise<any>) => fn(tx)));
184
+ }
185
+ return await callback(tx);
186
+ }, options);
187
+ }
188
+
189
+ // =====================================================
190
+ // GRACEFUL SHUTDOWN
191
+ // =====================================================
192
+
193
+ export async function disconnectAll(): Promise<void> {
194
+ await authPrisma.$disconnect();
195
+ await basePrisma.$disconnect();
196
+ if (authConnection.pool && typeof (authConnection.pool as any).end === 'function') {
197
+ await (authConnection.pool as any).end();
198
+ }
199
+ if (baseConnection.pool && typeof (baseConnection.pool as any).end === 'function') {
200
+ await (baseConnection.pool as any).end();
201
+ }
202
+ }
203
+
204
+ process.on('beforeExit', async () => {
205
+ await disconnectAll();
206
+ });
207
+
208
+ // =====================================================
209
+ // INITIALIZATION
210
+ // =====================================================
211
+
212
+ export async function initializeDMMF(): Promise<any> {
213
+ return dmmf.loadDMMF();
214
+ }
215
+
216
+ // Auto-initialize DMMF on module load
217
+ initializeDMMF();
218
+
219
+ // =====================================================
220
+ // LAZY ACL & MIDDLEWARE
221
+ // =====================================================
222
+
223
+ let _acl: AclConfig | null = null;
224
+ export function getAcl(): AclConfig {
225
+ if (!_acl) {
226
+ try {
227
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
228
+ _acl = require('../config/acl').default || require('../config/acl');
229
+ } catch {
230
+ _acl = { model: {} };
231
+ }
232
+ }
233
+ return _acl!;
234
+ }
235
+
236
+ export { PrismaClient, Prisma, dmmf };
package/src/index.ts ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Rapidd Framework - Main exports
3
+ *
4
+ * @example
5
+ * import { Model, QueryBuilder, prisma, Auth, ErrorResponse } from 'rapidd';
6
+ *
7
+ * class Users extends Model {
8
+ * constructor(options) {
9
+ * super('users', options);
10
+ * }
11
+ * }
12
+ */
13
+
14
+ // ── ORM ──────────────────────────────────────────────────────────────────────
15
+ export { Model } from './orm/Model';
16
+ export { QueryBuilder } from './orm/QueryBuilder';
17
+
18
+ // ── Database ─────────────────────────────────────────────────────────────────
19
+ export {
20
+ prisma,
21
+ authPrisma,
22
+ prismaTransaction,
23
+ getAcl,
24
+ setRLSVariables,
25
+ resetRLSVariables,
26
+ requestContext,
27
+ dbProvider,
28
+ rlsEnabled
29
+ } from './core/prisma';
30
+
31
+ export * as dmmf from './core/dmmf';
32
+
33
+ // ── Authentication ───────────────────────────────────────────────────────────
34
+ export { Auth } from './auth/Auth';
35
+ export { SessionStoreManager, createStore } from './auth/stores';
36
+ export type { ISessionStore } from './auth/stores/ISessionStore';
37
+
38
+ // ── Errors & Responses ───────────────────────────────────────────────────────
39
+ export { ErrorResponse, Response } from './core/errors';
40
+
41
+ // ── Middleware ───────────────────────────────────────────────────────────────
42
+ export { modelMiddleware } from './core/middleware';
43
+
44
+ // ── Utilities ────────────────────────────────────────────────────────────────
45
+ export { ApiClient, ApiClientError } from './utils/ApiClient';
46
+ export { Mailer } from './utils/Mailer';
47
+ export { env } from './utils';
48
+
49
+ // ── Environment ──────────────────────────────────────────────────────────────
50
+ export {
51
+ validateEnv,
52
+ getEnv,
53
+ getAllEnv,
54
+ isProduction,
55
+ isDevelopment,
56
+ isTest
57
+ } from './core/env';
58
+
59
+ // ── Plugins ──────────────────────────────────────────────────────────────────
60
+ export { uploadPlugin } from './plugins/upload';
61
+ export { default as responsePlugin } from './plugins/response';
62
+ export { default as apiPlugin } from './plugins/response'; // backward compat alias
63
+ export { default as authPlugin } from './plugins/auth';
64
+ export { default as languagePlugin } from './plugins/language';
65
+ export { default as securityPlugin } from './plugins/security';
66
+ export { default as rateLimitPlugin, RateLimiter } from './plugins/rateLimit';
67
+ export { default as rateLimiterPlugin } from './plugins/rateLimit'; // backward compat alias
68
+ export { default as rlsPlugin } from './plugins/rls';
69
+
70
+ // ── App Builder ──────────────────────────────────────────────────────────────
71
+ export { buildApp } from './app';
72
+
73
+ // ── Types ────────────────────────────────────────────────────────────────────
74
+ export type {
75
+ RapiddUser,
76
+ AuthStrategy,
77
+ RouteAuthConfig,
78
+ ModelOptions,
79
+ GetManyResult,
80
+ UpsertManyResult,
81
+ UpsertManyOptions,
82
+ ModelAcl,
83
+ AclConfig,
84
+ MiddlewareContext,
85
+ MiddlewareHook,
86
+ MiddlewareOperation,
87
+ RLSVariables,
88
+ RlsContextFn
89
+ } from './types';
90
+
91
+ export type {
92
+ ServiceConfig,
93
+ EndpointConfig,
94
+ AuthConfig,
95
+ RequestOptions,
96
+ ApiResponse
97
+ } from './utils/ApiClient';
98
+
99
+ export type {
100
+ EmailConfig,
101
+ EmailOptions,
102
+ EmailAttachment,
103
+ EmailResult
104
+ } from './utils/Mailer';
105
+
106
+ export type {
107
+ UploadOptions,
108
+ AllowedType,
109
+ UploadedFile
110
+ } from './plugins/upload';
111
+
112
+ export type { EnvConfig } from './core/env';
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Model Middleware — Register before/after hooks for CRUD operations.
3
+ *
4
+ * Import this file in main.ts (before start()) to activate middleware.
5
+ * Hooks run for all models by default; pass a model name as the last
6
+ * argument to scope a hook to a specific model.
7
+ *
8
+ * Usage:
9
+ * import './src/middleware/model';
10
+ *
11
+ * Available hooks: 'before' | 'after'
12
+ * Available ops: 'create' | 'update' | 'upsert' | 'upsertMany'
13
+ * | 'delete' | 'get' | 'getMany' | 'count'
14
+ */
15
+
16
+ import { modelMiddleware } from '../core/middleware';
17
+
18
+ // ── Timestamps (all models) ─────────────────────────
19
+ // Automatically set createdAt on create
20
+
21
+ // modelMiddleware.use('before', 'create', async (ctx) => {
22
+ // ctx.data = { ...ctx.data, createdAt: new Date(), createdBy: ctx.user?.id };
23
+ // return ctx;
24
+ // });
25
+
26
+ // Automatically set updatedAt on update
27
+
28
+ // modelMiddleware.use('before', 'update', async (ctx) => {
29
+ // ctx.data = { ...ctx.data, updatedAt: new Date(), updatedBy: ctx.user?.id };
30
+ // return ctx;
31
+ // });
32
+
33
+ // ── Soft Delete (specific model) ────────────────────
34
+ // Convert delete to an update that sets deletedAt
35
+
36
+ // modelMiddleware.use('before', 'delete', async (ctx) => {
37
+ // ctx.softDelete = true;
38
+ // ctx.data = { deletedAt: new Date(), deletedBy: ctx.user?.id };
39
+ // return ctx;
40
+ // }, 'posts');
41
+
42
+ // ── Transform Response ──────────────────────────────
43
+ // Add computed fields after fetching
44
+
45
+ // modelMiddleware.use('after', 'get', async (ctx) => {
46
+ // const result = ctx.result as Record<string, unknown> | undefined;
47
+ // if (result?.firstName && result?.lastName) {
48
+ // result.fullName = `${result.firstName} ${result.lastName}`;
49
+ // }
50
+ // return ctx;
51
+ // }, 'users');
52
+
53
+ // ── Abort on Condition ──────────────────────────────
54
+ // Prevent creation if a condition is not met
55
+
56
+ // modelMiddleware.use('before', 'create', async (ctx) => {
57
+ // if (!ctx.data?.email) {
58
+ // ctx.abort = true; // stops the operation
59
+ // }
60
+ // return ctx;
61
+ // }, 'users');