my-crud-lib 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/adapters/prisma.d.ts +3 -0
- package/dist/adapters/prisma.js +132 -0
- package/dist/core/ports/user.repo.d.ts +34 -0
- package/dist/core/ports/user.repo.js +1 -0
- package/dist/dev.js +5 -1
- package/dist/modules/user/user.controller.d.ts +4 -1
- package/dist/modules/user/user.controller.js +25 -17
- package/dist/modules/user/user.schemas.d.ts +5 -4
- package/dist/modules/user/user.schemas.js +16 -5
- package/dist/modules/user/user.service.d.ts +14 -68
- package/dist/modules/user/user.service.js +39 -123
- package/dist/modules/user/user.types.d.ts +46 -1
- package/package.json +21 -6
package/README.md
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export function makePrismaUserRepo(prisma) {
|
|
2
|
+
return {
|
|
3
|
+
async count({ role, search }) {
|
|
4
|
+
const where = {};
|
|
5
|
+
if (role)
|
|
6
|
+
where.role = role;
|
|
7
|
+
if (search?.trim()) {
|
|
8
|
+
const s = search.trim();
|
|
9
|
+
where.OR = [
|
|
10
|
+
{ email: { contains: s, mode: 'insensitive' } },
|
|
11
|
+
{ name: { contains: s, mode: 'insensitive' } },
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
return prisma.user.count({ where });
|
|
15
|
+
},
|
|
16
|
+
async findMany({ page, pageSize, role, search, sortField, sortDir }) {
|
|
17
|
+
const where = {};
|
|
18
|
+
if (role)
|
|
19
|
+
where.role = role;
|
|
20
|
+
if (search?.trim()) {
|
|
21
|
+
const s = search.trim();
|
|
22
|
+
where.OR = [
|
|
23
|
+
{ email: { contains: s, mode: 'insensitive' } },
|
|
24
|
+
{ name: { contains: s, mode: 'insensitive' } },
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
const orderBy = { [sortField]: sortDir };
|
|
28
|
+
const users = await prisma.user.findMany({
|
|
29
|
+
where,
|
|
30
|
+
orderBy,
|
|
31
|
+
skip: (page - 1) * pageSize,
|
|
32
|
+
take: pageSize,
|
|
33
|
+
select: {
|
|
34
|
+
id: true,
|
|
35
|
+
email: true,
|
|
36
|
+
name: true,
|
|
37
|
+
role: true,
|
|
38
|
+
createdAt: true,
|
|
39
|
+
updatedAt: true,
|
|
40
|
+
profile: { select: { bio: true, avatarUrl: true } },
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
return users;
|
|
44
|
+
},
|
|
45
|
+
async findById(id) {
|
|
46
|
+
return prisma.user.findUnique({
|
|
47
|
+
where: { id: id },
|
|
48
|
+
select: {
|
|
49
|
+
id: true,
|
|
50
|
+
email: true,
|
|
51
|
+
name: true,
|
|
52
|
+
role: true,
|
|
53
|
+
createdAt: true,
|
|
54
|
+
updatedAt: true,
|
|
55
|
+
profile: { select: { bio: true, avatarUrl: true } },
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
async findByEmail(email) {
|
|
60
|
+
return prisma.user.findUnique({
|
|
61
|
+
where: { email },
|
|
62
|
+
select: {
|
|
63
|
+
id: true,
|
|
64
|
+
email: true,
|
|
65
|
+
name: true,
|
|
66
|
+
role: true,
|
|
67
|
+
passwordHash: true,
|
|
68
|
+
createdAt: true,
|
|
69
|
+
updatedAt: true,
|
|
70
|
+
profile: { select: { bio: true, avatarUrl: true } },
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
async create(input) {
|
|
75
|
+
return prisma.user.create({
|
|
76
|
+
data: {
|
|
77
|
+
email: input.email,
|
|
78
|
+
passwordHash: input.passwordHash,
|
|
79
|
+
name: input.name ?? null,
|
|
80
|
+
role: input.role ?? 'USER',
|
|
81
|
+
profile: { create: { bio: input.bio ?? null, avatarUrl: input.avatarUrl ?? null } },
|
|
82
|
+
},
|
|
83
|
+
select: {
|
|
84
|
+
id: true, email: true, name: true, role: true,
|
|
85
|
+
createdAt: true, updatedAt: true,
|
|
86
|
+
profile: { select: { bio: true, avatarUrl: true } },
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
async update(id, input) {
|
|
91
|
+
return prisma.user.update({
|
|
92
|
+
where: { id: id },
|
|
93
|
+
data: {
|
|
94
|
+
name: input.name ?? undefined,
|
|
95
|
+
role: input.role ?? undefined,
|
|
96
|
+
profile: {
|
|
97
|
+
upsert: {
|
|
98
|
+
create: { bio: input.bio ?? null, avatarUrl: input.avatarUrl ?? null },
|
|
99
|
+
update: { bio: input.bio ?? null, avatarUrl: input.avatarUrl ?? null },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
select: {
|
|
104
|
+
id: true, email: true, name: true, role: true,
|
|
105
|
+
profile: { select: { bio: true, avatarUrl: true } },
|
|
106
|
+
createdAt: true, updatedAt: true,
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
async delete(id) {
|
|
111
|
+
await prisma.user.delete({ where: { id: id } });
|
|
112
|
+
},
|
|
113
|
+
async updateMe(userId, data) {
|
|
114
|
+
return prisma.user.update({
|
|
115
|
+
where: { id: userId },
|
|
116
|
+
data: {
|
|
117
|
+
name: data.name ?? undefined,
|
|
118
|
+
profile: {
|
|
119
|
+
upsert: {
|
|
120
|
+
create: { bio: data.bio ?? null, avatarUrl: data.avatarUrl ?? null },
|
|
121
|
+
update: { bio: data.bio ?? null, avatarUrl: data.avatarUrl ?? null },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
select: {
|
|
126
|
+
id: true, email: true, name: true, role: true,
|
|
127
|
+
profile: { select: { bio: true, avatarUrl: true } },
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AdminUpdateUserInput, UserListItem } from "../../modules/user/user.types.js";
|
|
2
|
+
export interface UserRepo {
|
|
3
|
+
count(where: {
|
|
4
|
+
role?: string;
|
|
5
|
+
search?: string;
|
|
6
|
+
}): Promise<number>;
|
|
7
|
+
findMany(params: {
|
|
8
|
+
page: number;
|
|
9
|
+
pageSize: number;
|
|
10
|
+
role?: string;
|
|
11
|
+
search?: string;
|
|
12
|
+
sortField: 'createdAt' | 'updatedAt' | 'email' | 'name';
|
|
13
|
+
sortDir: 'asc' | 'desc';
|
|
14
|
+
}): Promise<UserListItem[]>;
|
|
15
|
+
findById(id: number | string): Promise<UserListItem | null>;
|
|
16
|
+
findByEmail(email: string): Promise<UserListItem & {
|
|
17
|
+
passwordHash?: string;
|
|
18
|
+
} | null>;
|
|
19
|
+
create(input: {
|
|
20
|
+
email: string;
|
|
21
|
+
passwordHash: string;
|
|
22
|
+
name?: string | null;
|
|
23
|
+
role?: string;
|
|
24
|
+
bio?: string | null;
|
|
25
|
+
avatarUrl?: string | null;
|
|
26
|
+
}): Promise<UserListItem>;
|
|
27
|
+
update(id: number | string, input: AdminUpdateUserInput): Promise<UserListItem>;
|
|
28
|
+
delete(id: number | string): Promise<void>;
|
|
29
|
+
updateMe(userId: number | string, input: {
|
|
30
|
+
name?: string | null;
|
|
31
|
+
bio?: string | null;
|
|
32
|
+
avatarUrl?: string | null;
|
|
33
|
+
}): Promise<UserListItem>;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/dev.js
CHANGED
|
@@ -2,9 +2,13 @@ import { createServer } from './index.js';
|
|
|
2
2
|
import { createAuthRouter } from './modules/auth/auth.controller.js';
|
|
3
3
|
import 'dotenv/config';
|
|
4
4
|
import { createUserRouter } from './modules/user/user.controller.js';
|
|
5
|
+
import { makePrismaUserRepo } from './adapters/prisma.js';
|
|
6
|
+
import { PrismaClient } from '@prisma/client';
|
|
5
7
|
const app = createServer();
|
|
8
|
+
const prisma = new PrismaClient();
|
|
9
|
+
const userRepo = makePrismaUserRepo(prisma);
|
|
6
10
|
app.use('/auth', createAuthRouter());
|
|
7
|
-
app.use('/users', createUserRouter());
|
|
11
|
+
app.use('/users', createUserRouter({ userRepo }));
|
|
8
12
|
const PORT = Number(process.env.PORT) || 3000;
|
|
9
13
|
app.listen(PORT, () => {
|
|
10
14
|
console.log(`API dev up on http://localhost:${PORT}`);
|
|
@@ -1 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import type { UserRepo } from '../../core/ports/user.repo.js';
|
|
2
|
+
export declare function createUserRouter(deps: {
|
|
3
|
+
userRepo: UserRepo;
|
|
4
|
+
}): import("express-serve-static-core").Router;
|
|
@@ -2,13 +2,14 @@ import { Router } from 'express';
|
|
|
2
2
|
import { isAuth } from '../../middleware/isAuth.js';
|
|
3
3
|
import { hasRole, isSelfOrAdmin } from '../../middleware/hasRole.js';
|
|
4
4
|
import { listUsersQuerySchema, updateMeSchema, adminCreateUserSchema, adminUpdateUserSchema, } from './user.schemas.js';
|
|
5
|
-
import {
|
|
6
|
-
export function createUserRouter() {
|
|
5
|
+
import { makeUserService } from './user.service.js';
|
|
6
|
+
export function createUserRouter(deps) {
|
|
7
7
|
const router = Router();
|
|
8
|
+
const service = makeUserService({ userRepo: deps.userRepo });
|
|
8
9
|
router.get('/', isAuth, hasRole('ADMIN'), async (req, res) => {
|
|
9
10
|
try {
|
|
10
11
|
const q = listUsersQuerySchema.parse(req.query);
|
|
11
|
-
const data = await listUsers(q);
|
|
12
|
+
const data = await service.listUsers(q);
|
|
12
13
|
res.json(data);
|
|
13
14
|
}
|
|
14
15
|
catch (e) {
|
|
@@ -17,15 +18,8 @@ export function createUserRouter() {
|
|
|
17
18
|
res.status(500).json({ error: 'InternalError' });
|
|
18
19
|
}
|
|
19
20
|
});
|
|
20
|
-
router.get('
|
|
21
|
-
const
|
|
22
|
-
const data = await getUserById(id);
|
|
23
|
-
if (!data)
|
|
24
|
-
return res.status(404).json({ error: 'User not found' });
|
|
25
|
-
res.json(data);
|
|
26
|
-
});
|
|
27
|
-
router.get('/me/self', isAuth, async (req, res) => {
|
|
28
|
-
const data = await getUserById(req.user.id);
|
|
21
|
+
router.get('/me', isAuth, async (req, res) => {
|
|
22
|
+
const data = await service.getUserById(req.user.id);
|
|
29
23
|
if (!data)
|
|
30
24
|
return res.status(404).json({ error: 'User not found' });
|
|
31
25
|
res.json(data);
|
|
@@ -33,7 +27,7 @@ export function createUserRouter() {
|
|
|
33
27
|
router.put('/me', isAuth, async (req, res) => {
|
|
34
28
|
try {
|
|
35
29
|
const body = updateMeSchema.parse(req.body);
|
|
36
|
-
const data = await updateMe(req.user.id, body);
|
|
30
|
+
const data = await service.updateMe(req.user.id, body);
|
|
37
31
|
res.json(data);
|
|
38
32
|
}
|
|
39
33
|
catch (e) {
|
|
@@ -45,7 +39,7 @@ export function createUserRouter() {
|
|
|
45
39
|
router.post('/', isAuth, hasRole('ADMIN'), async (req, res) => {
|
|
46
40
|
try {
|
|
47
41
|
const body = adminCreateUserSchema.parse(req.body);
|
|
48
|
-
const data = await adminCreateUser(body);
|
|
42
|
+
const data = await service.adminCreateUser(body);
|
|
49
43
|
res.status(201).json(data);
|
|
50
44
|
}
|
|
51
45
|
catch (e) {
|
|
@@ -56,14 +50,26 @@ export function createUserRouter() {
|
|
|
56
50
|
res.status(500).json({ error: 'InternalError' });
|
|
57
51
|
}
|
|
58
52
|
});
|
|
53
|
+
router.get('/:id', isAuth, isSelfOrAdmin(), async (req, res) => {
|
|
54
|
+
const id = Number(req.params.id);
|
|
55
|
+
if (Number.isNaN(id))
|
|
56
|
+
return res.status(400).json({ error: 'Invalid id' });
|
|
57
|
+
const data = await service.getUserById(id);
|
|
58
|
+
if (!data)
|
|
59
|
+
return res.status(404).json({ error: 'User not found' });
|
|
60
|
+
res.json(data);
|
|
61
|
+
});
|
|
59
62
|
router.put('/:id', isAuth, isSelfOrAdmin(), async (req, res) => {
|
|
60
63
|
try {
|
|
61
64
|
const id = Number(req.params.id);
|
|
65
|
+
if (Number.isNaN(id))
|
|
66
|
+
return res.status(400).json({ error: 'Invalid id' });
|
|
62
67
|
const body = adminUpdateUserSchema.parse(req.body);
|
|
63
68
|
if (req.user.role !== 'ADMIN' && body.role) {
|
|
64
69
|
return res.status(403).json({ error: 'Forbidden: cannot change role' });
|
|
65
70
|
}
|
|
66
|
-
const
|
|
71
|
+
const sanitizedBody = { ...body, role: body.role === null ? undefined : body.role };
|
|
72
|
+
const data = await service.adminUpdateUser(id, sanitizedBody);
|
|
67
73
|
res.json(data);
|
|
68
74
|
}
|
|
69
75
|
catch (e) {
|
|
@@ -75,8 +81,10 @@ export function createUserRouter() {
|
|
|
75
81
|
router.delete('/:id', isAuth, hasRole('ADMIN'), async (req, res) => {
|
|
76
82
|
try {
|
|
77
83
|
const id = Number(req.params.id);
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
if (Number.isNaN(id))
|
|
85
|
+
return res.status(400).json({ error: 'Invalid id' });
|
|
86
|
+
await service.adminDeleteUser(id);
|
|
87
|
+
res.status(204).end();
|
|
80
88
|
}
|
|
81
89
|
catch {
|
|
82
90
|
res.status(500).json({ error: 'InternalError' });
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export declare const SortEnum: z.ZodEnum<["createdAt:desc", "createdAt:asc", "updatedAt:asc", "updatedAt:desc", "email:asc", "email:desc", "name:asc", "name:desc"]>;
|
|
2
3
|
export declare const listUsersQuerySchema: z.ZodObject<{
|
|
3
4
|
page: z.ZodDefault<z.ZodNumber>;
|
|
4
5
|
pageSize: z.ZodDefault<z.ZodNumber>;
|
|
5
|
-
search: z.ZodOptional<z.ZodString
|
|
6
|
+
search: z.ZodOptional<z.ZodEffects<z.ZodString, string | undefined, string>>;
|
|
6
7
|
role: z.ZodOptional<z.ZodEnum<["USER", "ADMIN"]>>;
|
|
7
|
-
sort: z.ZodDefault<z.
|
|
8
|
+
sort: z.ZodDefault<z.ZodEnum<["createdAt:desc", "createdAt:asc", "updatedAt:asc", "updatedAt:desc", "email:asc", "email:desc", "name:asc", "name:desc"]>>;
|
|
8
9
|
}, "strip", z.ZodTypeAny, {
|
|
9
|
-
sort:
|
|
10
|
+
sort: "createdAt:desc" | "createdAt:asc" | "updatedAt:asc" | "updatedAt:desc" | "email:asc" | "email:desc" | "name:asc" | "name:desc";
|
|
10
11
|
page: number;
|
|
11
12
|
pageSize: number;
|
|
12
13
|
search?: string | undefined;
|
|
13
14
|
role?: "USER" | "ADMIN" | undefined;
|
|
14
15
|
}, {
|
|
15
|
-
sort?:
|
|
16
|
+
sort?: "createdAt:desc" | "createdAt:asc" | "updatedAt:asc" | "updatedAt:desc" | "email:asc" | "email:desc" | "name:asc" | "name:desc" | undefined;
|
|
16
17
|
search?: string | undefined;
|
|
17
18
|
role?: "USER" | "ADMIN" | undefined;
|
|
18
19
|
page?: number | undefined;
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export const SortEnum = z.enum([
|
|
3
|
+
'createdAt:desc',
|
|
4
|
+
'createdAt:asc',
|
|
5
|
+
'updatedAt:asc',
|
|
6
|
+
'updatedAt:desc',
|
|
7
|
+
'email:asc',
|
|
8
|
+
'email:desc',
|
|
9
|
+
'name:asc',
|
|
10
|
+
'name:desc',
|
|
11
|
+
]);
|
|
2
12
|
export const listUsersQuerySchema = z.object({
|
|
3
13
|
page: z.coerce.number().int().min(1).default(1),
|
|
4
14
|
pageSize: z.coerce.number().int().min(1).max(100).default(10),
|
|
5
|
-
search: z
|
|
6
|
-
role: z.enum(['USER', 'ADMIN']).optional(),
|
|
7
|
-
sort: z
|
|
15
|
+
search: z
|
|
8
16
|
.string()
|
|
9
|
-
.
|
|
10
|
-
.
|
|
17
|
+
.trim()
|
|
18
|
+
.transform((v) => (v === '' ? undefined : v))
|
|
19
|
+
.optional(),
|
|
20
|
+
role: z.enum(['USER', 'ADMIN']).optional(),
|
|
21
|
+
sort: SortEnum.default('createdAt:desc'),
|
|
11
22
|
});
|
|
12
23
|
export const updateMeSchema = z.object({
|
|
13
24
|
name: z.string().min(1).max(100).nullish(),
|
|
@@ -1,68 +1,14 @@
|
|
|
1
|
-
import type { AdminCreateUserInput, AdminUpdateUserInput, ListUsersQuery, UpdateMeInput } from './user.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
avatarUrl: string | null;
|
|
16
|
-
} | null;
|
|
17
|
-
}[];
|
|
18
|
-
totalPages: number;
|
|
19
|
-
}>;
|
|
20
|
-
export declare function getUserById(id: number): Promise<{
|
|
21
|
-
email: string;
|
|
22
|
-
name: string | null;
|
|
23
|
-
id: number;
|
|
24
|
-
role: import("@prisma/client").$Enums.Role;
|
|
25
|
-
createdAt: Date;
|
|
26
|
-
updatedAt: Date;
|
|
27
|
-
profile: {
|
|
28
|
-
bio: string | null;
|
|
29
|
-
avatarUrl: string | null;
|
|
30
|
-
} | null;
|
|
31
|
-
} | null>;
|
|
32
|
-
export declare function updateMe(userId: number, data: UpdateMeInput): Promise<{
|
|
33
|
-
email: string;
|
|
34
|
-
name: string | null;
|
|
35
|
-
id: number;
|
|
36
|
-
role: import("@prisma/client").$Enums.Role;
|
|
37
|
-
profile: {
|
|
38
|
-
bio: string | null;
|
|
39
|
-
avatarUrl: string | null;
|
|
40
|
-
} | null;
|
|
41
|
-
}>;
|
|
42
|
-
export declare function adminCreateUser(input: AdminCreateUserInput): Promise<{
|
|
43
|
-
email: string;
|
|
44
|
-
name: string | null;
|
|
45
|
-
id: number;
|
|
46
|
-
role: import("@prisma/client").$Enums.Role;
|
|
47
|
-
createdAt: Date;
|
|
48
|
-
updatedAt: Date;
|
|
49
|
-
profile: {
|
|
50
|
-
bio: string | null;
|
|
51
|
-
avatarUrl: string | null;
|
|
52
|
-
} | null;
|
|
53
|
-
}>;
|
|
54
|
-
export declare function adminUpdateUser(id: number, input: AdminUpdateUserInput): Promise<{
|
|
55
|
-
email: string;
|
|
56
|
-
name: string | null;
|
|
57
|
-
id: number;
|
|
58
|
-
role: import("@prisma/client").$Enums.Role;
|
|
59
|
-
createdAt: Date;
|
|
60
|
-
updatedAt: Date;
|
|
61
|
-
profile: {
|
|
62
|
-
bio: string | null;
|
|
63
|
-
avatarUrl: string | null;
|
|
64
|
-
} | null;
|
|
65
|
-
}>;
|
|
66
|
-
export declare function adminDeleteUser(id: number): Promise<{
|
|
67
|
-
ok: boolean;
|
|
68
|
-
}>;
|
|
1
|
+
import type { AdminCreateUserInput, AdminUpdateUserInput, ListUsersQuery, Paginated, UpdateMeInput, UserListItem } from './user.types.js';
|
|
2
|
+
import type { UserRepo } from '../../core/ports/user.repo.js';
|
|
3
|
+
export declare function makeUserService(deps: {
|
|
4
|
+
userRepo: UserRepo;
|
|
5
|
+
}): {
|
|
6
|
+
listUsers(q: ListUsersQuery): Promise<Paginated<UserListItem>>;
|
|
7
|
+
getUserById(id: number | string): Promise<UserListItem | null>;
|
|
8
|
+
updateMe(userId: number | string, data: UpdateMeInput): Promise<UserListItem>;
|
|
9
|
+
adminCreateUser(input: AdminCreateUserInput): Promise<UserListItem>;
|
|
10
|
+
adminUpdateUser(id: number | string, input: AdminUpdateUserInput): Promise<UserListItem>;
|
|
11
|
+
adminDeleteUser(id: number | string): Promise<{
|
|
12
|
+
ok: true;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
@@ -1,136 +1,52 @@
|
|
|
1
|
-
import { prisma } from '../../utils/prisma.js';
|
|
2
1
|
import bcrypt from 'bcryptjs';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const [rawField, rawDir] = (
|
|
6
|
-
const allowedFields = ['createdAt', 'updatedAt', 'email', 'name'];
|
|
2
|
+
const allowedFields = ['createdAt', 'updatedAt', 'email', 'name'];
|
|
3
|
+
function parseSort(sort) {
|
|
4
|
+
const [rawField, rawDir] = (sort ?? 'createdAt:desc').split(':');
|
|
7
5
|
const sortField = allowedFields.includes(rawField || '')
|
|
8
6
|
? rawField
|
|
9
7
|
: 'createdAt';
|
|
10
8
|
const sortDir = rawDir === 'asc' || rawDir === 'desc' ? rawDir : 'desc';
|
|
11
|
-
|
|
12
|
-
if (role)
|
|
13
|
-
where.role = role;
|
|
14
|
-
if (search && search.trim() !== '') {
|
|
15
|
-
const s = search.trim();
|
|
16
|
-
where.OR = [
|
|
17
|
-
{ email: { contains: s, mode: 'insensitive' } },
|
|
18
|
-
{ name: { contains: s, mode: 'insensitive' } },
|
|
19
|
-
];
|
|
20
|
-
}
|
|
21
|
-
const orderBy = { [sortField]: sortDir };
|
|
22
|
-
const [total, items] = await Promise.all([
|
|
23
|
-
prisma.user.count({ where }),
|
|
24
|
-
prisma.user.findMany({
|
|
25
|
-
where,
|
|
26
|
-
orderBy,
|
|
27
|
-
skip: (page - 1) * pageSize,
|
|
28
|
-
take: pageSize,
|
|
29
|
-
select: {
|
|
30
|
-
id: true,
|
|
31
|
-
email: true,
|
|
32
|
-
name: true,
|
|
33
|
-
role: true,
|
|
34
|
-
createdAt: true,
|
|
35
|
-
updatedAt: true,
|
|
36
|
-
profile: { select: { bio: true, avatarUrl: true } },
|
|
37
|
-
},
|
|
38
|
-
}),
|
|
39
|
-
]);
|
|
40
|
-
return {
|
|
41
|
-
page,
|
|
42
|
-
pageSize,
|
|
43
|
-
total,
|
|
44
|
-
items,
|
|
45
|
-
totalPages: Math.ceil(total / pageSize),
|
|
46
|
-
};
|
|
9
|
+
return { sortField, sortDir };
|
|
47
10
|
}
|
|
48
|
-
export
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
11
|
+
export function makeUserService(deps) {
|
|
12
|
+
const { userRepo } = deps;
|
|
13
|
+
return {
|
|
14
|
+
async listUsers(q) {
|
|
15
|
+
const { page, pageSize, role, search } = q;
|
|
16
|
+
const { sortField, sortDir } = parseSort(q.sort);
|
|
17
|
+
const [total, items] = await Promise.all([
|
|
18
|
+
userRepo.count({ role, search }),
|
|
19
|
+
userRepo.findMany({ page, pageSize, role, search, sortField, sortDir }),
|
|
20
|
+
]);
|
|
21
|
+
return { page, pageSize, total, totalPages: Math.ceil(total / pageSize), items };
|
|
59
22
|
},
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export async function updateMe(userId, data) {
|
|
63
|
-
return prisma.user.update({
|
|
64
|
-
where: { id: userId },
|
|
65
|
-
data: {
|
|
66
|
-
name: data.name ?? undefined,
|
|
67
|
-
profile: {
|
|
68
|
-
upsert: {
|
|
69
|
-
create: { bio: data.bio ?? null, avatarUrl: data.avatarUrl ?? null },
|
|
70
|
-
update: { bio: data.bio ?? null, avatarUrl: data.avatarUrl ?? null },
|
|
71
|
-
},
|
|
72
|
-
},
|
|
23
|
+
getUserById(id) {
|
|
24
|
+
return userRepo.findById(id);
|
|
73
25
|
},
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
email: true,
|
|
77
|
-
name: true,
|
|
78
|
-
role: true,
|
|
79
|
-
profile: { select: { bio: true, avatarUrl: true } },
|
|
26
|
+
updateMe(userId, data) {
|
|
27
|
+
return userRepo.updateMe(userId, data);
|
|
80
28
|
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
create: {
|
|
96
|
-
bio: input.bio ?? null,
|
|
97
|
-
avatarUrl: input.avatarUrl ?? null,
|
|
98
|
-
},
|
|
99
|
-
},
|
|
29
|
+
async adminCreateUser(input) {
|
|
30
|
+
const exists = await userRepo.findByEmail(input.email);
|
|
31
|
+
if (exists)
|
|
32
|
+
throw new Error('EMAIL_TAKEN');
|
|
33
|
+
const rounds = Number(process.env.BCRYPT_SALT) || 10;
|
|
34
|
+
const passwordHash = await bcrypt.hash(input.password, rounds);
|
|
35
|
+
return userRepo.create({
|
|
36
|
+
email: input.email,
|
|
37
|
+
passwordHash,
|
|
38
|
+
name: input.name ?? null,
|
|
39
|
+
role: input.role ?? 'USER',
|
|
40
|
+
bio: input.bio ?? null,
|
|
41
|
+
avatarUrl: input.avatarUrl ?? null,
|
|
42
|
+
});
|
|
100
43
|
},
|
|
101
|
-
|
|
102
|
-
id
|
|
103
|
-
email: true,
|
|
104
|
-
name: true,
|
|
105
|
-
role: true,
|
|
106
|
-
profile: { select: { bio: true, avatarUrl: true } },
|
|
107
|
-
createdAt: true,
|
|
108
|
-
updatedAt: true,
|
|
44
|
+
adminUpdateUser(id, input) {
|
|
45
|
+
return userRepo.update(id, input);
|
|
109
46
|
},
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return prisma.user.update({
|
|
114
|
-
where: { id },
|
|
115
|
-
data: {
|
|
116
|
-
name: input.name ?? undefined,
|
|
117
|
-
role: input.role ?? undefined,
|
|
118
|
-
profile: {
|
|
119
|
-
upsert: {
|
|
120
|
-
create: { bio: input.bio ?? null, avatarUrl: input.avatarUrl ?? null },
|
|
121
|
-
update: { bio: input.bio ?? null, avatarUrl: input.avatarUrl ?? null },
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
select: {
|
|
126
|
-
id: true, email: true, name: true, role: true,
|
|
127
|
-
profile: { select: { bio: true, avatarUrl: true } },
|
|
128
|
-
createdAt: true, updatedAt: true,
|
|
47
|
+
async adminDeleteUser(id) {
|
|
48
|
+
await userRepo.delete(id);
|
|
49
|
+
return { ok: true };
|
|
129
50
|
},
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
export async function adminDeleteUser(id) {
|
|
133
|
-
const user = await prisma.user.findUnique({ where: { id } });
|
|
134
|
-
await prisma.user.delete({ where: { id } });
|
|
135
|
-
return { ok: true };
|
|
51
|
+
};
|
|
136
52
|
}
|
|
@@ -1 +1,46 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type Role = 'USER' | 'ADMIN';
|
|
2
|
+
export type UserListItem = {
|
|
3
|
+
id: number | string;
|
|
4
|
+
email: string;
|
|
5
|
+
name: string | null;
|
|
6
|
+
role: Role;
|
|
7
|
+
createdAt: Date | string;
|
|
8
|
+
updatedAt: Date | string;
|
|
9
|
+
profile?: {
|
|
10
|
+
bio: string | null;
|
|
11
|
+
avatarUrl: string | null;
|
|
12
|
+
} | null;
|
|
13
|
+
};
|
|
14
|
+
export type ListUsersQuery = {
|
|
15
|
+
page: number;
|
|
16
|
+
pageSize: number;
|
|
17
|
+
role?: Role;
|
|
18
|
+
search?: string;
|
|
19
|
+
sort?: `${'createdAt' | 'updatedAt' | 'email' | 'name'}:${'asc' | 'desc'}`;
|
|
20
|
+
};
|
|
21
|
+
export type Paginated<T> = {
|
|
22
|
+
page: number;
|
|
23
|
+
pageSize: number;
|
|
24
|
+
total: number;
|
|
25
|
+
totalPages: number;
|
|
26
|
+
items: T[];
|
|
27
|
+
};
|
|
28
|
+
export type UpdateMeInput = {
|
|
29
|
+
name?: string | null;
|
|
30
|
+
bio?: string | null;
|
|
31
|
+
avatarUrl?: string | null;
|
|
32
|
+
};
|
|
33
|
+
export type AdminCreateUserInput = {
|
|
34
|
+
email: string;
|
|
35
|
+
password: string;
|
|
36
|
+
name?: string | null;
|
|
37
|
+
role?: Role;
|
|
38
|
+
bio?: string | null;
|
|
39
|
+
avatarUrl?: string | null;
|
|
40
|
+
};
|
|
41
|
+
export type AdminUpdateUserInput = {
|
|
42
|
+
name?: string | null;
|
|
43
|
+
role?: Role;
|
|
44
|
+
bio?: string | null;
|
|
45
|
+
avatarUrl?: string | null;
|
|
46
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-crud-lib",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Libreria CRUD modulare (Auth/User/Profile) con Prisma
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Libreria CRUD modulare (Auth/User/Profile) con TS; Prisma opzionale via adapter",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -10,10 +10,16 @@
|
|
|
10
10
|
".": {
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"types": "./dist/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./adapter-prisma": {
|
|
15
|
+
"import": "./dist/adapters/prisma.js",
|
|
16
|
+
"types": "./dist/adapters/prisma.d.ts"
|
|
13
17
|
}
|
|
14
18
|
},
|
|
15
19
|
"files": [
|
|
16
20
|
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
17
23
|
"prisma/schema.prisma"
|
|
18
24
|
],
|
|
19
25
|
"engines": {
|
|
@@ -31,7 +37,6 @@
|
|
|
31
37
|
"prisma:generate": "prisma generate",
|
|
32
38
|
"prisma:migrate": "prisma migrate dev",
|
|
33
39
|
"prisma:studio": "prisma studio",
|
|
34
|
-
"postinstall": "prisma generate",
|
|
35
40
|
"prepublishOnly": "npm run build",
|
|
36
41
|
"db:schema": "docker exec -i mycrud_postgres psql -U app -d mycrud -v ON_ERROR_STOP=1 -f /dev/stdin < prisma/schema.sql",
|
|
37
42
|
"db:seed": "docker exec -i mycrud_postgres psql -U app -d mycrud -v ON_ERROR_STOP=1 -f /dev/stdin < prisma/seed.sql",
|
|
@@ -45,20 +50,30 @@
|
|
|
45
50
|
"@types/jsonwebtoken": "^9.0.10",
|
|
46
51
|
"@types/node": "^24.3.0",
|
|
47
52
|
"prisma": "^6.14.0",
|
|
53
|
+
"@prisma/client": "^6.14.0",
|
|
48
54
|
"ts-node": "^10.9.2",
|
|
49
55
|
"typescript": "^5.9.2"
|
|
50
56
|
},
|
|
51
57
|
"dependencies": {
|
|
52
|
-
"@prisma/client": "^6.14.0",
|
|
53
58
|
"bcryptjs": "^2.4.3",
|
|
54
59
|
"dotenv": "^16.3.1",
|
|
55
60
|
"jsonwebtoken": "^9.0.2",
|
|
56
61
|
"zod": "^3.23.8"
|
|
57
62
|
},
|
|
58
63
|
"peerDependencies": {
|
|
59
|
-
"
|
|
64
|
+
"express": "^4.18.2",
|
|
60
65
|
"cors": "^2.8.5",
|
|
61
|
-
"
|
|
66
|
+
"body-parser": "^1.20.2",
|
|
67
|
+
"@prisma/client": "^6.14.0",
|
|
68
|
+
"prisma": "^6.14.0"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"@prisma/client": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"prisma": {
|
|
75
|
+
"optional": true
|
|
76
|
+
}
|
|
62
77
|
},
|
|
63
78
|
"publishConfig": {
|
|
64
79
|
"access": "public"
|