@pextran/db 0.0.3

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 ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@pextran/db",
3
+ "version": "0.0.3",
4
+ "description": "pextran DB Utilities",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "files": [
8
+ "src"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc --noEmit",
12
+ "prepublishOnly": "npm run build",
13
+ "test": "echo \"Error: no test specified\" && exit 1",
14
+ "patch": "pnpm pkg:addonly && pnpm run build && git add package.json ../../pnpm-lock.yaml && (git diff --staged --quiet || HUSKY=0 git commit -m \"chore(@pextran/db): sync dependencies\" --no-verify) && HUSKY=0 npm version patch && npm publish && git push --follow-tags",
15
+ "pkg:addonly": "pnpm remove @pextran/core && pnpm add @pextran/core@latest --ignore-scripts",
16
+ "pkg": "pnpm run pkg:addonly && git add . && git commit --no-verify -m \"pextran packages upgraded\"",
17
+ "pkg:push": "pnpm install && pnpm run pkg && git push"
18
+ },
19
+ "keywords": [
20
+ "typescript",
21
+ "db",
22
+ "utils"
23
+ ],
24
+ "author": "Hassen Mohammed",
25
+ "license": "MIT",
26
+ "peerDependencies": {
27
+ "next": "^14.0.0",
28
+ "next-auth": "5.0.0-beta.4",
29
+ "react": "^18.0.0",
30
+ "react-dom": "^18.0.0",
31
+ "zustand": "^4.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/jsonwebtoken": "^9.0.9",
35
+ "@types/node": "^20.6.0",
36
+ "@types/react": "^18.0.0",
37
+ "@types/react-dom": "^18.0.0",
38
+ "typescript": "^5.2.2"
39
+ },
40
+ "publishConfig": {
41
+ "access": "restricted"
42
+ },
43
+ "dependencies": {
44
+ "@pextran/core": "^0.0.5",
45
+ "jsonwebtoken": "^9.0.2",
46
+ "pdf-lib": "^1.17.1"
47
+ }
48
+ }
package/src/README.md ADDED
@@ -0,0 +1 @@
1
+ # Arcisol DB Package
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './prisma';
2
+ export * from './repository/approvalRepository';
3
+ export * from './repository/employeesRepository';
4
+ export * from './repository/mediaRepository';
5
+
package/src/prisma.ts ADDED
@@ -0,0 +1,40 @@
1
+ export interface IPrisma {
2
+ employee: {
3
+ findFirst: (args: any) => Promise<any>
4
+ }
5
+ tenant: {
6
+ findFirst: (args: any) => Promise<any>
7
+ }
8
+ organization: {
9
+ findFirst: (args: any) => Promise<any>
10
+ }
11
+ role: {
12
+ findMany: (args: any) => Promise<any>
13
+ }
14
+ // Add media model methods to satisfy mediaRepository.ts usage
15
+ media: {
16
+ findFirst: (args: any) => Promise<any>
17
+ findUnique: (args: any) => Promise<any>
18
+ findMany: (args: any) => Promise<any>
19
+ create: (args: any) => Promise<any>
20
+ update: (args: any) => Promise<any>
21
+ delete: (args: any) => Promise<any>
22
+ }
23
+ // Add approval model methods to satisfy approvalRepository.ts usage
24
+ approval: {
25
+ findFirst: (args: any) => Promise<any>
26
+ findUnique: (args: any) => Promise<any>
27
+ findMany: (args: any) => Promise<any>
28
+ create: (args: any) => Promise<any>
29
+ update: (args: any) => Promise<any>
30
+ delete: (args: any) => Promise<any>
31
+ groupBy: (args: any) => Promise<any>
32
+ }
33
+ department: {
34
+ findFirst: (args: any) => Promise<any>
35
+ }
36
+ resourceApproval: {
37
+ findFirst: (args: any) => Promise<any>
38
+ delete: (args: any) => Promise<any>
39
+ }
40
+ }
@@ -0,0 +1,122 @@
1
+ 'use server'
2
+ import { ApprovalDTO, logger, NexiResponse, ResourceApprovalDTO, ResponseFactory } from "@pextran/core";
3
+ import { IPrisma } from "../prisma";
4
+
5
+ export async function findApprovalById(prisma: IPrisma, tenant: string, id: string): Promise<NexiResponse<ApprovalDTO>> {
6
+ try {
7
+ const approval = await prisma.approval.findFirst({
8
+ where: { id, tenant },
9
+ orderBy: { version: 'desc' },
10
+ take: 1
11
+ });
12
+
13
+ if (!approval) {
14
+ return ResponseFactory.error('Approval not found');
15
+ }
16
+
17
+ return ResponseFactory.success(approvalDbToDTO(approval));
18
+ } catch (error) {
19
+ logger.error('Error finding approval by ID:', error);
20
+ return ResponseFactory.error('Error finding approval');
21
+ }
22
+ }
23
+
24
+ export async function findApprovalByResourceType(
25
+ prisma: IPrisma,
26
+ tenant: string,
27
+ resourceType: string
28
+ ): Promise<NexiResponse<ApprovalDTO>> {
29
+ try {
30
+ const approval = await prisma.approval.findFirst({
31
+ where: {
32
+ resourceType,
33
+ tenant
34
+ },
35
+ orderBy: { version: 'desc' },
36
+ take: 1
37
+ });
38
+
39
+ if (!approval) {
40
+ return ResponseFactory.error('Approval not found for resource type');
41
+ }
42
+
43
+ return ResponseFactory.success(approvalDbToDTO(approval));
44
+ } catch (error) {
45
+ logger.error('Error finding approval by resource type:', error);
46
+ return ResponseFactory.error('Error finding approval');
47
+ }
48
+ }
49
+
50
+ export async function getApprovalsForTenant(
51
+ prisma: IPrisma,
52
+ tenant: string
53
+ ): Promise<NexiResponse<ApprovalDTO[]>> {
54
+ try {
55
+ // First, get the latest version for each resourceType
56
+ const latestApprovalIds = await prisma.approval.groupBy({
57
+ by: ['resourceType'],
58
+ where: { tenant },
59
+ _max: {
60
+ version: true
61
+ }
62
+ });
63
+
64
+ // Then fetch the full approval records for the latest versions
65
+ const approvals = await Promise.all(
66
+ latestApprovalIds.map(async (group: any) => {
67
+ return await prisma.approval.findFirst({
68
+ where: {
69
+ tenant,
70
+ resourceType: group.resourceType,
71
+ version: group._max.version
72
+ }
73
+ });
74
+ })
75
+ );
76
+
77
+ // Filter out any null results and convert to DTOs
78
+ const approvalDTOs = approvals
79
+ .filter((approval: any) => approval !== null)
80
+ .map(approvalDbToDTO);
81
+
82
+ return ResponseFactory.success(approvalDTOs);
83
+ } catch (error) {
84
+ logger.error('Error getting approvals for tenant:', error);
85
+ return ResponseFactory.error('Error retrieving approvals');
86
+ }
87
+ }
88
+
89
+ function approvalDbToDTO(approval: any): ApprovalDTO {
90
+ return {
91
+ id: approval.id,
92
+ version: approval.version,
93
+ resourceType: approval.resourceType,
94
+ moduleId: approval.moduleId,
95
+ approvalLevels: approval.approvalLevels,
96
+ tenant: approval.tenant,
97
+ createdAt: approval.createdAt?.toISOString(),
98
+ updatedAt: approval.updatedAt?.toISOString(),
99
+ audits: approval.audits || []
100
+ } as ApprovalDTO;
101
+ }
102
+
103
+ export async function getResourceApprovalsForResource(
104
+ prisma: IPrisma,
105
+ tenant: string,
106
+ resourceId: string,
107
+ resourceType: string
108
+ ): Promise<NexiResponse<ResourceApprovalDTO | null>> {
109
+ try {
110
+ const resourceApproval = await prisma.resourceApproval.findFirst({
111
+ where: {
112
+ tenant,
113
+ resourceId,
114
+ resourceType
115
+ }
116
+ }) as unknown as ResourceApprovalDTO;
117
+ return ResponseFactory.success(resourceApproval);
118
+ } catch (error) {
119
+ logger.error('Error getting resource approvals for resource:', error);
120
+ return ResponseFactory.error('Failed to get resource approvals for resource');
121
+ }
122
+ }
@@ -0,0 +1,161 @@
1
+ import { AuthClaims, DepartmentClaims, MODULES, RoleDTO } from "@pextran/core";
2
+ import { IPrisma } from "../prisma";
3
+
4
+ export async function getEmployeeClaims(prisma: IPrisma, tenantId: string, email: string, role: string, getOrgLogo: (orgId: string) => Promise<string | null>): Promise<{ claims: AuthClaims } | null> {
5
+ if (!email || !tenantId) return null
6
+
7
+ const employee = await prisma.employee.findFirst({
8
+ where: {
9
+ orgId: tenantId,
10
+ email: {
11
+ equals: email,
12
+ mode: 'insensitive'
13
+ },
14
+ active: true,
15
+ deleted: false
16
+ },
17
+ select: {
18
+ id: true,
19
+ email: true,
20
+ userId: true,
21
+ emailVerified: true,
22
+ firstName: true,
23
+ middleName: true,
24
+ lastName: true,
25
+ phone: true,
26
+ roles: true,
27
+ preferences: true,
28
+ orgId: true,
29
+ employment: true,
30
+ compensations: true,
31
+ financialDetails: true,
32
+ metadata: true,
33
+ }
34
+ }) as any
35
+
36
+ if (!employee) {
37
+ console.warn(`No employee found ! request tenantId:${tenantId}, email:${email}`)
38
+ return null
39
+ }
40
+ let department: DepartmentClaims | null = null;
41
+ if (employee.employment?.departmentId) {
42
+ const departmentData = await prisma.department.findFirst({
43
+ where: { id: employee.employment?.departmentId, tenant: tenantId },
44
+ select: {
45
+ id: true,
46
+ name: true,
47
+ managerId: true
48
+ }
49
+ }) as any
50
+ department = {
51
+ id: departmentData?.id,
52
+ name: departmentData?.name,
53
+ isHead: departmentData?.managerId === employee.id
54
+ }
55
+ }
56
+
57
+ employee.profilePicture = await getOrgLogo(tenantId);
58
+
59
+ const tenantData = await prisma.tenant.findFirst({
60
+ where: { id: employee?.orgId },
61
+ select: {
62
+ id: true,
63
+ organization: {
64
+ select: {
65
+ name: true
66
+ }
67
+ },
68
+ modules: true,
69
+ preferences: true,
70
+ subscription: true
71
+ }
72
+ })
73
+
74
+ const orgData = await prisma.organization.findFirst({
75
+ where: { id: employee?.orgId },
76
+ select: {
77
+ email: true,
78
+ website: true,
79
+ phone: true
80
+ }
81
+ }) as any
82
+
83
+ const roles = await prisma.role.findMany({
84
+ where: { orgId: tenantId, key: { in: employee.roles } },
85
+ select: {
86
+ key: true,
87
+ permissions: true,
88
+ modules: true
89
+ }
90
+ })
91
+
92
+ const otherRoles: { role: string, modules: MODULES[] }[] = [];
93
+ tenantData.modules.forEach((m: MODULES) => {
94
+ const rolesWithModule = roles.filter((r: RoleDTO) => r.modules.includes(m) || r.modules.includes(MODULES.ALL)) as RoleDTO[]
95
+ if (rolesWithModule.length > 0) {
96
+ rolesWithModule.forEach((role: RoleDTO) => {
97
+ if (otherRoles.find((r: { role: string, modules: MODULES[] }) => r.role === role.key)) {
98
+ //update the modules
99
+ const modules = otherRoles.find((r: { role: string, modules: MODULES[] }) => r.role === role.key)!.modules
100
+ const mergedModules = modules.concat(role.modules)
101
+ const uniqueModules = mergedModules.filter((m, idx) => mergedModules.indexOf(m) === idx)
102
+ otherRoles.find((r: { role: string, modules: MODULES[] }) => r.role === role.key)!.modules = uniqueModules
103
+ } else {
104
+ otherRoles.push({ role: role.key, modules: role.modules })
105
+ }
106
+ })
107
+ }
108
+ })
109
+
110
+ if (role && roles.find((r: RoleDTO) => r.key === role)) {
111
+ employee.preferences = { ...(employee.preferences as any), activeRole: role }
112
+ } else if (!(employee.preferences as any)?.activeRole && roles.length > 0) {
113
+ employee.preferences = { ...(employee.preferences as any), activeRole: roles[0].key }
114
+ }
115
+
116
+ const claims = {
117
+ claims: {
118
+ employee: {
119
+ id: employee.id,
120
+ userId: employee.userId,
121
+ email: employee.email,
122
+ emailVerified: employee.emailVerified,
123
+ firstName: employee.firstName,
124
+ middleName: employee.middleName || '',
125
+ lastName: employee.lastName,
126
+ phone: employee.phone || '',
127
+ name: employee.firstName + ' ' + employee.lastName,
128
+ roles: roles.map((r: RoleDTO) => r.key === employee.preferences?.activeRole ? ({
129
+ role: r.key,
130
+ permissions: r.permissions,
131
+ modules: r.modules
132
+ }) : ({
133
+ role: r.key,
134
+ modules: r.modules
135
+ })),
136
+ preferences: employee.preferences,
137
+ profilePicture: employee.profilePicture,
138
+ department,
139
+ otherRoles: otherRoles
140
+ },
141
+ tenant: {
142
+ id: tenantData?.id,
143
+ name: tenantData?.organization?.name,
144
+ modules: tenantData?.modules,
145
+ preferences: tenantData?.preferences,
146
+ subscription: tenantData?.subscription
147
+ },
148
+ org: {
149
+ id: tenantData?.id,
150
+ name: tenantData?.organization?.name,
151
+ email: orgData?.email,
152
+ website: orgData?.website,
153
+ logo: employee.profilePicture,
154
+ phone: orgData?.phone
155
+ },
156
+ user: {} as any
157
+ } as AuthClaims
158
+ }
159
+
160
+ return claims as any
161
+ }
@@ -0,0 +1,228 @@
1
+ 'use server'
2
+ import { CATEGORY_UTILIZED_MEDIA_TYPES, logger, MediaDTO, MediaType, MULTI_FILE_MEDIA_TYPES, NexiResponse, ResourceType, ResponseFactory } from "@pextran/core";
3
+ import { IPrisma } from "../prisma";
4
+
5
+ export async function upsertMedia(prisma: IPrisma, tenant: string, media: MediaDTO, resourceId: string, resourceType?: ResourceType): Promise<MediaDTO> {
6
+ const upserted = await upsertMedias(prisma, tenant, [media], resourceId, resourceType) || [];
7
+ return upserted[0];
8
+ }
9
+
10
+ export async function upsertMedias(prisma: IPrisma, tenant: string, medias: MediaDTO[], resourceId: string, resourceType?: ResourceType): Promise<MediaDTO[]> {
11
+
12
+ //if medias is empty, return empty array
13
+ if (!medias || medias.length === 0) {
14
+ return [];
15
+ }
16
+
17
+ const updatedMedias: MediaDTO[] = [];
18
+ // console.log('upserting medias:', medias, tenant, resourceId);
19
+ for (const media of (medias || []).filter(m => !!m && (m.url || m.signedUrl)) || []) {
20
+ // console.log('upserting media:', media);
21
+ const { modified, ...rest } = media;
22
+ let updatedMedia = {
23
+ ...rest,
24
+ tenant,
25
+ resourceId,
26
+ resourceType,
27
+ name: media.name || ''
28
+ } as MediaDTO
29
+ // console.log('updatedMedia:', updatedMedia);
30
+ let id = media.id
31
+ let existingButDeleted = false;
32
+ //ttachments have order is as unique key allowing multiple media of same type
33
+ const type = media.type as MediaType;
34
+ if (!id && !MULTI_FILE_MEDIA_TYPES.includes(type) && !(media.category && CATEGORY_UTILIZED_MEDIA_TYPES.includes(type))) {
35
+ const existingMD = await prisma.media.findFirst({
36
+ where: {
37
+ tenant, resourceId, type, deleted: false
38
+ }
39
+ })
40
+ if (existingMD?.id) {
41
+ id = existingMD.id;
42
+ existingButDeleted = existingMD.deleted;
43
+ }
44
+ }
45
+
46
+ let order = media.order || 1;
47
+
48
+ if (!id && MULTI_FILE_MEDIA_TYPES.includes(type)) {
49
+ //get the last attachment and increment the order
50
+ const lastAttachment = await prisma.media.findFirst({
51
+ where: {
52
+ tenant, resourceId, type
53
+ },
54
+ orderBy: {
55
+ order: 'desc'
56
+ }
57
+ }).then(m => m?.order);
58
+
59
+ order = lastAttachment ? lastAttachment + 1 : 1;
60
+ }
61
+ updatedMedia = {
62
+ ...updatedMedia,
63
+ order
64
+ } as MediaDTO;
65
+
66
+ let existingMedia: MediaDTO | null = null;
67
+ if (id) {
68
+ existingMedia = await prisma.media.findUnique({
69
+ where: { id, tenant }
70
+ }) as unknown as MediaDTO;
71
+ }
72
+
73
+ if (!id && !MULTI_FILE_MEDIA_TYPES.includes(type) && CATEGORY_UTILIZED_MEDIA_TYPES.includes(type)) {
74
+ //check if media with same name and type already exists
75
+ const existingMedia = await prisma.media.findFirst({
76
+ where: { tenant, resourceId, type, category: updatedMedia.category ?? null, name: media.name }
77
+ })
78
+ if (existingMedia?.id) {
79
+ id = existingMedia.id;
80
+ }
81
+ }
82
+ if (!id && !MULTI_FILE_MEDIA_TYPES.includes(type) && !CATEGORY_UTILIZED_MEDIA_TYPES.includes(type)) {
83
+ //check if media with same name and type already exists
84
+ const existingMedia = await prisma.media.findFirst({
85
+ where: { tenant, resourceId, type, name: media.name }
86
+ })
87
+ if (existingMedia?.id) {
88
+ id = existingMedia.id;
89
+ }
90
+ }
91
+
92
+ if (!id || !existingMedia) {
93
+ const { id, ...rest } = updatedMedia;
94
+ // console.log('creating media:', rest);
95
+ const md = await prisma.media.create({
96
+ data: rest
97
+ }) as unknown as MediaDTO;
98
+ updatedMedias.push(md);
99
+ } else if (modified || existingButDeleted) {
100
+ // console.log('updating media:', updatedMedia);
101
+ const md = await prisma.media.update({
102
+ where: { id, tenant },
103
+ data: updatedMedia
104
+ }) as unknown as MediaDTO;
105
+ updatedMedias.push(md);
106
+ } else {
107
+ updatedMedias.push(updatedMedia);
108
+ }
109
+ }
110
+ return updatedMedias;
111
+ }
112
+
113
+ export const getOrgLogo = async (prisma: IPrisma, orgId: string): Promise<string | null> => {
114
+ const logo = await prisma.media.findFirst({
115
+ where: { tenant: orgId, type: MediaType.Logo, resourceId: orgId, deleted: false }
116
+ }) as unknown as MediaDTO;
117
+ return logo?.url || logo?.signedUrl || null;
118
+ }
119
+
120
+ export const moveAndDeleteMedia = async (prisma: IPrisma, tenant: string, id: string, resourceId: string): Promise<MediaDTO | null> => {
121
+ const newId = resourceId + '_' + new Date().getTime().toString();
122
+ const movedMedia = await prisma.media.update({
123
+ where: { tenant, id },
124
+ data: {
125
+ deleted: true,
126
+ resourceId: newId
127
+ }
128
+ }) as unknown as MediaDTO;
129
+ return movedMedia;
130
+ }
131
+
132
+ export const findMediasForResourceId = async (prisma: IPrisma, tenant: string, resourceId: string): Promise<NexiResponse<MediaDTO[]>> => {
133
+ console.log('findMediasForResourceId:', tenant, resourceId);
134
+ const medias = await prisma.media.findMany({
135
+ where: { tenant, resourceId, deleted: false }
136
+ }) as unknown as MediaDTO[];
137
+ // console.log('medias:', medias);
138
+ return ResponseFactory.success(medias as MediaDTO[]);
139
+ }
140
+
141
+ export const findMediasForTypeAndResourceId = async (prisma: IPrisma, tenant: string, type: MediaType, resourceId: string, includeDeleted: boolean = false): Promise<MediaDTO[]> => {
142
+ const medias = includeDeleted ? await prisma.media.findMany({
143
+ where: { tenant, type, resourceId }
144
+ }) as unknown as MediaDTO[] : await prisma.media.findMany({
145
+ where: { tenant, type, resourceId, deleted: false }
146
+ }) as unknown as MediaDTO[];
147
+ return medias;
148
+ }
149
+
150
+ export const findMediasForTypeAndResourceIdAndCategory = async (prisma: IPrisma, tenant: string, type: MediaType, resourceId: string, category: string, includeDeleted: boolean = false): Promise<MediaDTO[]> => {
151
+ const medias = includeDeleted ? await prisma.media.findMany({
152
+ where: { tenant, type, resourceId, category }
153
+ }) as unknown as MediaDTO[] : await prisma.media.findMany({
154
+ where: { tenant, type, resourceId, category, deleted: false }
155
+ }) as unknown as MediaDTO[];
156
+ return medias;
157
+ }
158
+
159
+ export const findMediaById = async (prisma: IPrisma, id: string): Promise<MediaDTO | null> => {
160
+
161
+ return await prisma.media.findFirst({
162
+ where: { id, deleted: false },
163
+ }) as unknown as MediaDTO;
164
+ }
165
+
166
+ export const getMediasForQuery = async (prisma: IPrisma, query: Record<string, string>, tenant: string): Promise<MediaDTO[]> => {
167
+ const medias = await prisma.media.findMany({
168
+ where: {
169
+ tenant,
170
+ AND: Object.entries(query).map(([key, value]) => ({
171
+ [key]: { contains: value, mode: 'insensitive' }
172
+ })),
173
+ deleted: false
174
+ }
175
+ }) as unknown as MediaDTO[];
176
+ return medias as MediaDTO[];
177
+ }
178
+
179
+ export const deleteMedia = async (prisma: IPrisma, tenant: string, id: string): Promise<MediaDTO | null> => {
180
+ try {
181
+ return await prisma.media.update({
182
+ where: { tenant, id },
183
+ data: {
184
+ deleted: true
185
+ }
186
+ }) as unknown as MediaDTO;
187
+
188
+ // this will flag the media as deleted and associated images will be deleted from the file storage by cron job
189
+ } catch (error) {
190
+ logger.error('Error deleting media', { error, tenant, id });
191
+ return null;
192
+ }
193
+ }
194
+
195
+ export const getDeletedMedias = async (prisma: IPrisma): Promise<MediaDTO[]> => {
196
+ try {
197
+ return await prisma.media.findMany({
198
+ where: { deleted: true },
199
+ select: {
200
+ id: true,
201
+ tenant: true,
202
+ type: true,
203
+ resourceId: true,
204
+ resourceType: true,
205
+ name: true,
206
+ url: true,
207
+ signedUrl: true,
208
+ deleted: true,
209
+ order: true,
210
+ storagePath: true
211
+ }
212
+ }) as unknown as MediaDTO[];
213
+ } catch (error) {
214
+ logger.error('Error getting deleted medias', { error });
215
+ return [];
216
+ }
217
+ }
218
+
219
+ export const hardDeleteMedia = async (prisma: IPrisma, tenant: string, id: string): Promise<MediaDTO | null> => {
220
+ try {
221
+ return await prisma.media.delete({
222
+ where: { tenant, id }
223
+ }) as unknown as MediaDTO;
224
+ } catch (error) {
225
+ logger.error('Error hard deleting media', { error, tenant, id });
226
+ return null;
227
+ }
228
+ }