dolphin-server-modules 1.0.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/auth/auth.ts +685 -0
- package/controller/controller.ts +248 -0
- package/curd/crud.ts +267 -0
- package/dist/auth/auth.d.ts +81 -0
- package/dist/auth/auth.js +565 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/controller/controller.d.ts +36 -0
- package/dist/controller/controller.js +185 -0
- package/dist/controller/controller.js.map +1 -0
- package/dist/curd/crud.d.ts +71 -0
- package/dist/curd/crud.js +217 -0
- package/dist/curd/crud.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/zod.d.ts +20 -0
- package/dist/middleware/zod.js +76 -0
- package/dist/middleware/zod.js.map +1 -0
- package/index.ts +12 -0
- package/middleware/zod.ts +73 -0
- package/package.json +39 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// packages/core/controller.ts
|
|
2
|
+
|
|
3
|
+
export interface DolphinController<T = any> {
|
|
4
|
+
list(req: any, userId?: string): Promise<any>;
|
|
5
|
+
get(req: any, userId?: string): Promise<T | null>;
|
|
6
|
+
create(req: any, userId?: string): Promise<T>;
|
|
7
|
+
update(req: any, userId?: string): Promise<T | null>;
|
|
8
|
+
delete(req: any, userId?: string): Promise<T | null>;
|
|
9
|
+
restore?(req: any, userId?: string): Promise<T | null>;
|
|
10
|
+
bulkUpdate?(req: any, userId?: string): Promise<number>;
|
|
11
|
+
bulkDelete?(req: any, userId?: string): Promise<number>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const createDolphinController = (
|
|
15
|
+
crud: any,
|
|
16
|
+
collection: string,
|
|
17
|
+
options?: {
|
|
18
|
+
softDelete?: boolean;
|
|
19
|
+
bulkOps?: boolean;
|
|
20
|
+
}
|
|
21
|
+
): DolphinController => {
|
|
22
|
+
|
|
23
|
+
const controller: DolphinController = {
|
|
24
|
+
|
|
25
|
+
async list(req: any, userId?: string) {
|
|
26
|
+
const { page, limit, ...filter } = req.query || {};
|
|
27
|
+
|
|
28
|
+
if (page !== undefined || limit !== undefined) {
|
|
29
|
+
return await crud.paginate(
|
|
30
|
+
collection,
|
|
31
|
+
filter,
|
|
32
|
+
Number(page) || 1,
|
|
33
|
+
Number(limit) || 100,
|
|
34
|
+
userId
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return await crud.read(collection, filter, {}, userId);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async get(req: any, userId?: string) {
|
|
42
|
+
const id = req.params?.id || req.query?.id;
|
|
43
|
+
return await crud.readOne(collection, id, userId);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async create(req: any, userId?: string) {
|
|
47
|
+
const data = req.body || {};
|
|
48
|
+
return await crud.create(collection, data, userId);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async update(req: any, userId?: string) {
|
|
52
|
+
const id = req.params?.id || req.query?.id;
|
|
53
|
+
const data = req.body || {};
|
|
54
|
+
return await crud.updateOne(collection, id, data, userId);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async delete(req: any, userId?: string) {
|
|
58
|
+
const id = req.params?.id || req.query?.id;
|
|
59
|
+
return await crud.deleteOne(collection, id, userId);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
if (options?.softDelete) {
|
|
64
|
+
controller.restore = async (req: any, userId?: string) => {
|
|
65
|
+
const id = req.params?.id || req.query?.id;
|
|
66
|
+
return await crud.restore(collection, id, userId);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (options?.bulkOps) {
|
|
71
|
+
controller.bulkUpdate = async (req: any, userId?: string) => {
|
|
72
|
+
const { filter, data } = req.body || {};
|
|
73
|
+
return await crud.updateMany(collection, filter, data, userId);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
controller.bulkDelete = async (req: any, userId?: string) => {
|
|
77
|
+
const { filter } = req.body || {};
|
|
78
|
+
return await crud.deleteMany(collection, filter, userId);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return controller;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ===== PAGES ROUTER HANDLER (req, res) =====
|
|
86
|
+
export const createNextPagesHandler = (
|
|
87
|
+
controller: DolphinController,
|
|
88
|
+
options?: { requireAuth?: boolean; require2FA?: boolean }
|
|
89
|
+
) => {
|
|
90
|
+
return async (req: any, res: any) => {
|
|
91
|
+
try {
|
|
92
|
+
const { method } = req;
|
|
93
|
+
let result;
|
|
94
|
+
|
|
95
|
+
const userId = req.user?.id;
|
|
96
|
+
|
|
97
|
+
switch (method) {
|
|
98
|
+
case 'GET':
|
|
99
|
+
if (req.query?.id) {
|
|
100
|
+
result = await controller.get(req, userId);
|
|
101
|
+
} else {
|
|
102
|
+
result = await controller.list(req, userId);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case 'POST':
|
|
107
|
+
result = await controller.create(req, userId);
|
|
108
|
+
break;
|
|
109
|
+
|
|
110
|
+
case 'PUT':
|
|
111
|
+
case 'PATCH':
|
|
112
|
+
result = await controller.update(req, userId);
|
|
113
|
+
break;
|
|
114
|
+
|
|
115
|
+
case 'DELETE':
|
|
116
|
+
result = await controller.delete(req, userId);
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
default:
|
|
120
|
+
return res.status(405).json({ error: 'Method not allowed' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return res.status(200).json(result);
|
|
124
|
+
|
|
125
|
+
} catch (error: any) {
|
|
126
|
+
const status = error.status || 500;
|
|
127
|
+
return res.status(status).json({ error: error.message });
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// ===== APP ROUTER HANDLER (Request, Response) =====
|
|
133
|
+
export const createNextAppRoute = (controller: DolphinController) => {
|
|
134
|
+
|
|
135
|
+
const handler = async (req: Request, context: { params: any }) => {
|
|
136
|
+
try {
|
|
137
|
+
const url = new URL(req.url);
|
|
138
|
+
const method = req.method;
|
|
139
|
+
|
|
140
|
+
// Query params parse
|
|
141
|
+
const query = Object.fromEntries(url.searchParams.entries());
|
|
142
|
+
|
|
143
|
+
// JSON body handle — safe parsing
|
|
144
|
+
let body = {};
|
|
145
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
146
|
+
try {
|
|
147
|
+
body = await req.json();
|
|
148
|
+
} catch {
|
|
149
|
+
body = {};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Mock request object for controller
|
|
154
|
+
const mockReq = {
|
|
155
|
+
query,
|
|
156
|
+
params: context.params || {},
|
|
157
|
+
body,
|
|
158
|
+
headers: Object.fromEntries(req.headers.entries()),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// User from header (auth middleware le set gareko)
|
|
162
|
+
const userId = req.headers.get('x-user-id') || undefined;
|
|
163
|
+
|
|
164
|
+
let result;
|
|
165
|
+
|
|
166
|
+
switch (method) {
|
|
167
|
+
case 'GET':
|
|
168
|
+
if (context.params?.id || query.id) {
|
|
169
|
+
result = await controller.get(mockReq, userId);
|
|
170
|
+
} else {
|
|
171
|
+
result = await controller.list(mockReq, userId);
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
case 'POST':
|
|
176
|
+
result = await controller.create(mockReq, userId);
|
|
177
|
+
break;
|
|
178
|
+
|
|
179
|
+
case 'PUT':
|
|
180
|
+
case 'PATCH':
|
|
181
|
+
result = await controller.update(mockReq, userId);
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case 'DELETE':
|
|
185
|
+
result = await controller.delete(mockReq, userId);
|
|
186
|
+
break;
|
|
187
|
+
|
|
188
|
+
default:
|
|
189
|
+
return new Response(
|
|
190
|
+
JSON.stringify({ error: 'Method not allowed' }),
|
|
191
|
+
{ status: 405, headers: { 'Content-Type': 'application/json' } }
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return new Response(
|
|
196
|
+
JSON.stringify(result),
|
|
197
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
} catch (error: any) {
|
|
201
|
+
const status = error.status || 500;
|
|
202
|
+
return new Response(
|
|
203
|
+
JSON.stringify({ error: error.message }),
|
|
204
|
+
{ status, headers: { 'Content-Type': 'application/json' } }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Return route handlers for App Router
|
|
210
|
+
return {
|
|
211
|
+
GET: handler,
|
|
212
|
+
POST: handler,
|
|
213
|
+
PUT: handler,
|
|
214
|
+
PATCH: handler,
|
|
215
|
+
DELETE: handler,
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// ===== DYNAMIC ROUTE HANDLER (for [id].ts) =====
|
|
220
|
+
export const createNextDynamicHandler = (controller: DolphinController) => {
|
|
221
|
+
return async (req: any, res: any) => {
|
|
222
|
+
try {
|
|
223
|
+
const { method } = req;
|
|
224
|
+
const userId = req.user?.id;
|
|
225
|
+
let result;
|
|
226
|
+
|
|
227
|
+
switch (method) {
|
|
228
|
+
case 'GET':
|
|
229
|
+
result = await controller.get(req, userId);
|
|
230
|
+
break;
|
|
231
|
+
case 'PUT':
|
|
232
|
+
case 'PATCH':
|
|
233
|
+
result = await controller.update(req, userId);
|
|
234
|
+
break;
|
|
235
|
+
case 'DELETE':
|
|
236
|
+
result = await controller.delete(req, userId);
|
|
237
|
+
break;
|
|
238
|
+
default:
|
|
239
|
+
return res.status(405).json({ error: 'Method not allowed' });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return res.status(200).json(result);
|
|
243
|
+
|
|
244
|
+
} catch (error: any) {
|
|
245
|
+
return res.status(error.status || 500).json({ error: error.message });
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
};
|
package/curd/crud.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// crud-lite.ts — World-class lightweight CRUD (2026 style, full-featured)
|
|
2
|
+
// Compatible with your auth DatabaseAdapter
|
|
3
|
+
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
// ===== DATABASE ADAPTER INTERFACE =====
|
|
7
|
+
export interface DatabaseAdapter {
|
|
8
|
+
createUser(data: any): Promise<any>;
|
|
9
|
+
findUserByEmail(email: string): Promise<any>;
|
|
10
|
+
findUserById(id: string): Promise<any>;
|
|
11
|
+
updateUser(id: string, data: any): Promise<any>;
|
|
12
|
+
saveRefreshToken(data: any): Promise<void>;
|
|
13
|
+
findRefreshToken(token: string): Promise<any>;
|
|
14
|
+
deleteRefreshToken(token: string): Promise<void>;
|
|
15
|
+
create(collection: string, data: any): Promise<any>;
|
|
16
|
+
read(collection: string, query: any): Promise<any[]>;
|
|
17
|
+
update(collection: string, query: any, data: any): Promise<any>;
|
|
18
|
+
delete(collection: string, query: any): Promise<any>;
|
|
19
|
+
advancedRead?(collection: string, query: any, options: any): Promise<any[]>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ===== DOCUMENT BASE TYPE =====
|
|
23
|
+
export interface BaseDocument {
|
|
24
|
+
id: string;
|
|
25
|
+
userId?: string;
|
|
26
|
+
createdAt?: string;
|
|
27
|
+
updatedAt?: string;
|
|
28
|
+
deletedAt?: string | null;
|
|
29
|
+
[key: string]: any; // Index signature for dynamic fields
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ===== Query Filter Type =====
|
|
33
|
+
export type QueryFilter<T extends BaseDocument = BaseDocument> = {
|
|
34
|
+
[K in keyof T]?: T[K] | { $eq?: T[K]; $ne?: T[K]; $gt?: T[K]; $gte?: T[K]; $lt?: T[K]; $lte?: T[K]; $in?: T[K][]; $nin?: T[K][]; $like?: string };
|
|
35
|
+
} & {
|
|
36
|
+
$and?: QueryFilter<T>[];
|
|
37
|
+
$or?: QueryFilter<T>[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ===== Pagination =====
|
|
41
|
+
export interface PaginationOptions {
|
|
42
|
+
limit?: number;
|
|
43
|
+
offset?: number;
|
|
44
|
+
sort?: { [key: string]: 'asc' | 'desc' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ===== CRUD Factory =====
|
|
48
|
+
export function createCRUD<T extends BaseDocument = BaseDocument>(
|
|
49
|
+
db: DatabaseAdapter,
|
|
50
|
+
options: { enforceOwnership?: boolean; softDelete?: boolean; defaultLimit?: number } = {}
|
|
51
|
+
) {
|
|
52
|
+
const { enforceOwnership = true, softDelete = false, defaultLimit = 100 } = options;
|
|
53
|
+
|
|
54
|
+
const generateId = () => crypto.randomBytes(12).toString('hex');
|
|
55
|
+
|
|
56
|
+
const applyOwnership = <U extends BaseDocument>(query: QueryFilter<U>, userId?: string): QueryFilter<U> => {
|
|
57
|
+
if (!enforceOwnership || !userId) return query;
|
|
58
|
+
return { ...query, userId: { $eq: userId } };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const matchFilter = <U extends BaseDocument>(item: any, filter: QueryFilter<U>): boolean => {
|
|
62
|
+
const ops = (field: string, val: any, cond: any): boolean => {
|
|
63
|
+
if (cond === undefined) return val !== undefined;
|
|
64
|
+
if (typeof cond !== 'object') return val === cond;
|
|
65
|
+
if ('$eq' in cond) return val === cond.$eq;
|
|
66
|
+
if ('$ne' in cond) return val !== cond.$ne;
|
|
67
|
+
if ('$gt' in cond) return val > cond.$gt;
|
|
68
|
+
if ('$gte' in cond) return val >= cond.$gte;
|
|
69
|
+
if ('$lt' in cond) return val < cond.$lt;
|
|
70
|
+
if ('$lte' in cond) return val <= cond.$lte;
|
|
71
|
+
if ('$in' in cond) return Array.isArray(cond.$in) && cond.$in.includes(val);
|
|
72
|
+
if ('$nin' in cond) return Array.isArray(cond.$nin) && !cond.$nin.includes(val);
|
|
73
|
+
if ('$like' in cond) return typeof val === 'string' && val.includes(String(cond.$like));
|
|
74
|
+
return true;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const keys = Object.keys(filter);
|
|
78
|
+
for (const key of keys) {
|
|
79
|
+
const fval: any = (filter as any)[key];
|
|
80
|
+
if (key === '$and') {
|
|
81
|
+
if (!Array.isArray(fval) || !fval.every((f: any) => matchFilter(item, f))) return false;
|
|
82
|
+
} else if (key === '$or') {
|
|
83
|
+
if (!Array.isArray(fval) || !fval.some((f: any) => matchFilter(item, f))) return false;
|
|
84
|
+
} else {
|
|
85
|
+
if (!ops(key, (item as any)[key], fval)) return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const applySoftDelete = (items: T[]): T[] => {
|
|
92
|
+
if (!softDelete) return items;
|
|
93
|
+
return items.filter(i => !i.deletedAt);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const applySort = (items: T[], sort?: { [key: string]: 'asc' | 'desc' }) => {
|
|
97
|
+
if (!sort) return items;
|
|
98
|
+
const keys = Object.keys(sort);
|
|
99
|
+
return [...items].sort((a, b) => {
|
|
100
|
+
for (const k of keys) {
|
|
101
|
+
const aVal = (a as any)[k];
|
|
102
|
+
const bVal = (b as any)[k];
|
|
103
|
+
if (aVal === bVal) continue;
|
|
104
|
+
return sort[k] === 'asc' ? (aVal > bVal ? 1 : -1) : (aVal > bVal ? -1 : 1);
|
|
105
|
+
}
|
|
106
|
+
return 0;
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
async create(collection: string, data: Partial<T>, userId?: string): Promise<T> {
|
|
112
|
+
const now = new Date().toISOString();
|
|
113
|
+
const doc = {
|
|
114
|
+
id: generateId(),
|
|
115
|
+
createdAt: now,
|
|
116
|
+
updatedAt: now,
|
|
117
|
+
...(userId && { userId }),
|
|
118
|
+
...data
|
|
119
|
+
} as T;
|
|
120
|
+
return db.create(collection, doc);
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async createMany(collection: string, items: Array<Partial<T>>, userId?: string): Promise<T[]> {
|
|
124
|
+
const now = new Date().toISOString();
|
|
125
|
+
const docs = items.map(item => ({
|
|
126
|
+
id: generateId(),
|
|
127
|
+
createdAt: now,
|
|
128
|
+
updatedAt: now,
|
|
129
|
+
...(userId && { userId }),
|
|
130
|
+
...item
|
|
131
|
+
})) as T[];
|
|
132
|
+
|
|
133
|
+
const results: T[] = [];
|
|
134
|
+
for (const doc of docs) {
|
|
135
|
+
results.push(await db.create(collection, doc));
|
|
136
|
+
}
|
|
137
|
+
return results;
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
async readOne(collection: string, id: string, userId?: string): Promise<T | null> {
|
|
141
|
+
const filter = applyOwnership({ id }, userId);
|
|
142
|
+
const results = await db.read(collection, filter);
|
|
143
|
+
const filtered = applySoftDelete(results);
|
|
144
|
+
return filtered[0] || null;
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
async read(
|
|
148
|
+
collection: string,
|
|
149
|
+
filter: QueryFilter<T> = {},
|
|
150
|
+
options: PaginationOptions = {},
|
|
151
|
+
userId?: string
|
|
152
|
+
): Promise<T[]> {
|
|
153
|
+
const safeFilter = applyOwnership(filter, userId);
|
|
154
|
+
let items: T[] = [];
|
|
155
|
+
|
|
156
|
+
if (db.advancedRead) {
|
|
157
|
+
items = await db.advancedRead(collection, safeFilter, options);
|
|
158
|
+
} else {
|
|
159
|
+
const raw = await db.read(collection, {});
|
|
160
|
+
items = applySoftDelete(raw).filter(i => matchFilter(i, safeFilter));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (options.sort) items = applySort(items, options.sort);
|
|
164
|
+
if (options.offset || options.limit !== undefined) {
|
|
165
|
+
const start = options.offset || 0;
|
|
166
|
+
const end = start + (options.limit ?? items.length);
|
|
167
|
+
items = items.slice(start, end);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return items;
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async updateOne(collection: string, id: string, data: Partial<T>, userId?: string): Promise<T | null> {
|
|
174
|
+
const filter = applyOwnership({ id }, userId);
|
|
175
|
+
const results = await db.read(collection, filter);
|
|
176
|
+
if (results.length === 0) return null;
|
|
177
|
+
|
|
178
|
+
const updateData = { ...data, updatedAt: new Date().toISOString() };
|
|
179
|
+
await db.update(collection, filter, updateData);
|
|
180
|
+
return { ...results[0], ...updateData } as T;
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
async updateMany(collection: string, filter: QueryFilter<T>, data: Partial<T>, userId?: string): Promise<number> {
|
|
184
|
+
const safeFilter = applyOwnership(filter, userId);
|
|
185
|
+
const items = await this.read(collection, safeFilter, {}, userId);
|
|
186
|
+
const updateData = { ...data, updatedAt: new Date().toISOString() };
|
|
187
|
+
|
|
188
|
+
for (const item of items) {
|
|
189
|
+
await db.update(collection, { id: item.id }, updateData);
|
|
190
|
+
}
|
|
191
|
+
return items.length;
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
async deleteOne(collection: string, id: string, userId?: string): Promise<T | null> {
|
|
195
|
+
const filter = applyOwnership({ id }, userId);
|
|
196
|
+
const results = await db.read(collection, filter);
|
|
197
|
+
if (results.length === 0) return null;
|
|
198
|
+
|
|
199
|
+
if (softDelete) {
|
|
200
|
+
await db.update(collection, filter, { deletedAt: new Date().toISOString() });
|
|
201
|
+
return results[0] as T;
|
|
202
|
+
} else {
|
|
203
|
+
await db.delete(collection, filter);
|
|
204
|
+
return results[0] as T;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
async deleteMany(collection: string, filter: QueryFilter<T>, userId?: string): Promise<number> {
|
|
209
|
+
const safeFilter = applyOwnership(filter, userId);
|
|
210
|
+
const items = await this.read(collection, safeFilter, {}, userId);
|
|
211
|
+
|
|
212
|
+
if (softDelete) {
|
|
213
|
+
for (const item of items) {
|
|
214
|
+
await db.update(collection, { id: item.id }, { deletedAt: new Date().toISOString() });
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
await db.delete(collection, safeFilter);
|
|
218
|
+
}
|
|
219
|
+
return items.length;
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
async restore(collection: string, id: string, userId?: string): Promise<T | null> {
|
|
223
|
+
if (!softDelete) throw new Error('Soft delete not enabled');
|
|
224
|
+
const filter = applyOwnership({ id, deletedAt: { $ne: null } }, userId);
|
|
225
|
+
const results = await db.read(collection, filter);
|
|
226
|
+
if (results.length === 0) return null;
|
|
227
|
+
|
|
228
|
+
// ✅ FIX: Set deletedAt to null to explicitly restore
|
|
229
|
+
const updateData = { ...results[0], deletedAt: null, updatedAt: new Date().toISOString() };
|
|
230
|
+
await db.update(collection, filter, updateData);
|
|
231
|
+
return this.readOne(collection, id, userId);
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
async count(collection: string, filter: QueryFilter<T> = {}, userId?: string): Promise<number> {
|
|
235
|
+
const safeFilter = applyOwnership(filter, userId);
|
|
236
|
+
const raw = await db.read(collection, {});
|
|
237
|
+
return applySoftDelete(raw).filter(i => matchFilter(i, safeFilter)).length;
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async exists(collection: string, filter: QueryFilter<T>, userId?: string): Promise<boolean> {
|
|
241
|
+
const count = await this.count(collection, filter, userId);
|
|
242
|
+
return count > 0;
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
async paginate(
|
|
246
|
+
collection: string,
|
|
247
|
+
filter: QueryFilter<T> = {},
|
|
248
|
+
page: number = 1,
|
|
249
|
+
limit: number = defaultLimit,
|
|
250
|
+
userId?: string
|
|
251
|
+
) {
|
|
252
|
+
const offset = (page - 1) * limit;
|
|
253
|
+
const items = await this.read(collection, filter, { limit, offset }, userId);
|
|
254
|
+
const total = await this.count(collection, filter, userId);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
items,
|
|
258
|
+
total,
|
|
259
|
+
page,
|
|
260
|
+
limit,
|
|
261
|
+
totalPages: Math.ceil(total / limit),
|
|
262
|
+
hasNext: page * limit < total,
|
|
263
|
+
hasPrev: page > 1
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface RefreshTokenRecord {
|
|
2
|
+
token: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
expiresAt: Date;
|
|
5
|
+
twoFactorVerified: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface DatabaseAdapter {
|
|
8
|
+
createUser(data: any): Promise<any>;
|
|
9
|
+
findUserByEmail(email: string): Promise<any>;
|
|
10
|
+
findUserById(id: string): Promise<any>;
|
|
11
|
+
updateUser(id: string, data: any): Promise<any>;
|
|
12
|
+
saveRefreshToken(data: RefreshTokenRecord): Promise<void>;
|
|
13
|
+
findRefreshToken(token: string): Promise<RefreshTokenRecord | null>;
|
|
14
|
+
deleteRefreshToken(token: string): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export declare function createAuth(config: {
|
|
17
|
+
secret: string;
|
|
18
|
+
redisClient?: any;
|
|
19
|
+
cookieMaxAge?: number;
|
|
20
|
+
issuer?: string;
|
|
21
|
+
rateLimit?: {
|
|
22
|
+
max: number;
|
|
23
|
+
window: number;
|
|
24
|
+
};
|
|
25
|
+
}): {
|
|
26
|
+
register(db: DatabaseAdapter, data: {
|
|
27
|
+
email: string;
|
|
28
|
+
password: string;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
id: any;
|
|
31
|
+
email: any;
|
|
32
|
+
role: any;
|
|
33
|
+
}>;
|
|
34
|
+
login(db: DatabaseAdapter, input: {
|
|
35
|
+
email: string;
|
|
36
|
+
password: string;
|
|
37
|
+
totp?: string;
|
|
38
|
+
recovery?: string;
|
|
39
|
+
}, res?: {
|
|
40
|
+
cookie: (name: string, value: string, options: any) => void;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
accessToken: string;
|
|
43
|
+
user: {
|
|
44
|
+
id: any;
|
|
45
|
+
email: any;
|
|
46
|
+
role: any;
|
|
47
|
+
twoFactorEnabled: any;
|
|
48
|
+
};
|
|
49
|
+
}>;
|
|
50
|
+
enable2FA(db: DatabaseAdapter, userId: string): Promise<{
|
|
51
|
+
secret: string;
|
|
52
|
+
uri: string;
|
|
53
|
+
}>;
|
|
54
|
+
verify2FA(db: DatabaseAdapter, userId: string, totp: string): Promise<{
|
|
55
|
+
recoveryCodes: string[];
|
|
56
|
+
}>;
|
|
57
|
+
refresh(db: DatabaseAdapter, refreshToken: string, res?: {
|
|
58
|
+
cookie: (name: string, value: string, options: any) => void;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
accessToken: string;
|
|
61
|
+
user: {
|
|
62
|
+
id: any;
|
|
63
|
+
email: any;
|
|
64
|
+
role: any;
|
|
65
|
+
twoFactorEnabled: any;
|
|
66
|
+
};
|
|
67
|
+
}>;
|
|
68
|
+
logout(db: DatabaseAdapter, refreshToken: string): Promise<{
|
|
69
|
+
success: boolean;
|
|
70
|
+
}>;
|
|
71
|
+
disable2FA(db: DatabaseAdapter, userId: string, totp: string): Promise<{
|
|
72
|
+
success: boolean;
|
|
73
|
+
}>;
|
|
74
|
+
regenerateRecoveryCodes(db: DatabaseAdapter, userId: string, totp: string): Promise<{
|
|
75
|
+
recoveryCodes: string[];
|
|
76
|
+
}>;
|
|
77
|
+
middleware(opts?: {
|
|
78
|
+
require2FA?: boolean;
|
|
79
|
+
}): (req: any, res: any, next: Function) => Promise<any>;
|
|
80
|
+
verifyToken: (token: string) => Promise<any>;
|
|
81
|
+
};
|