From 87318be252c1ebf77f763d2c29ee5555ac726e76 Mon Sep 17 00:00:00 2001 From: Yeganathan S Date: Thu, 24 Apr 2025 21:11:24 +0530 Subject: [PATCH 1/7] chore(api): add memberIdOrLfid support in param and user validation endpoints - Add endpoints for detected identities, work history status, and user validation (identity/org) supporting both memberId and LFID - Implement memberIdOrLfid middleware for flexible ID resolution - Update permissions and roles for new endpoints and external service access - Add audit logging for user validation actions - Refactor and extend data-access-layer for new validation and activity count logic - Add migrations for memberUserValidations table --- backend/src/api/member/identity/index.ts | 5 ++ .../member/identity/memberIdentityCreate.ts | 2 +- .../identity/memberIdentityCreateMultiple.ts | 2 +- .../member/identity/memberIdentityDelete.ts | 2 +- .../identity/memberIdentityDetectedList.ts | 28 +++++++ .../api/member/identity/memberIdentityList.ts | 2 +- .../member/identity/memberIdentityUpdate.ts | 2 +- .../identity/memberIdentityUserValidation.ts | 27 +++++++ backend/src/api/member/organization/index.ts | 19 ++++- .../organization/memberOrganizationCreate.ts | 2 +- .../organization/memberOrganizationDelete.ts | 2 +- .../organization/memberOrganizationList.ts | 6 +- .../organization/memberOrganizationStatus.ts | 26 +++++++ .../organization/memberOrganizationUpdate.ts | 2 +- .../memberOrganizationUserValidation.ts | 27 +++++++ .../U1745401102__memberUserValidations.sql | 1 + .../V1745401102__memberUserValidations.sql | 13 ++++ .../middlewares/memberIdOrLfidMiddleware.ts | 28 +++++++ backend/src/security/permissions.ts | 36 +++++++++ backend/src/security/roles.ts | 1 + .../services/member/memberIdentityService.ts | 73 ++++++++++++++++++- .../member/memberOrganizationsService.ts | 33 ++++++++- services/libs/audit-logs/src/actions.ts | 7 ++ .../data-access-layer/src/activities/sql.ts | 70 +++++++++++------- .../data-access-layer/src/audit_logs/repo.ts | 2 + .../src/members/identities.ts | 40 +++++++++- .../data-access-layer/src/members/index.ts | 1 + .../src/members/organizations.ts | 15 ++++ .../data-access-layer/src/members/types.ts | 26 ++++++- .../src/members/userValidations.ts | 55 ++++++++++++++ services/libs/types/src/members.ts | 37 ++++++++++ 31 files changed, 547 insertions(+), 45 deletions(-) create mode 100644 backend/src/api/member/identity/memberIdentityDetectedList.ts create mode 100644 backend/src/api/member/identity/memberIdentityUserValidation.ts create mode 100644 backend/src/api/member/organization/memberOrganizationStatus.ts create mode 100644 backend/src/api/member/organization/memberOrganizationUserValidation.ts create mode 100644 backend/src/database/migrations/U1745401102__memberUserValidations.sql create mode 100644 backend/src/database/migrations/V1745401102__memberUserValidations.sql create mode 100644 backend/src/middlewares/memberIdOrLfidMiddleware.ts create mode 100644 services/libs/data-access-layer/src/members/userValidations.ts diff --git a/backend/src/api/member/identity/index.ts b/backend/src/api/member/identity/index.ts index c2019c562f..5ee65ecb1f 100644 --- a/backend/src/api/member/identity/index.ts +++ b/backend/src/api/member/identity/index.ts @@ -1,4 +1,5 @@ import { safeWrap } from '@/middlewares/errorMiddleware' +import { memberIdOrLfidMiddleware } from '@/middlewares/memberIdOrLfidMiddleware' export default (app) => { // Member Identity List @@ -15,4 +16,8 @@ export default (app) => { // Member Identity Delete app.delete(`/member/:memberId/identity/:id`, safeWrap(require('./memberIdentityDelete').default)) + + app.get(`/member/:memberIdOrLfid/detected-identity`, memberIdOrLfidMiddleware, safeWrap(require('./memberIdentityDetectedList').default)) + + app.post(`/member/:memberIdOrLfid/user-validation`, memberIdOrLfidMiddleware, safeWrap(require('./memberIdentityUserValidation').default)) } diff --git a/backend/src/api/member/identity/memberIdentityCreate.ts b/backend/src/api/member/identity/memberIdentityCreate.ts index 374b768575..ac3f618214 100644 --- a/backend/src/api/member/identity/memberIdentityCreate.ts +++ b/backend/src/api/member/identity/memberIdentityCreate.ts @@ -17,7 +17,7 @@ import PermissionChecker from '../../../services/user/permissionChecker' * @response 429 - Too many requests */ export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + new PermissionChecker(req).validateHas(Permissions.values.memberIdentityCreate) const memberIdentityService = new MemberIdentityService(req) diff --git a/backend/src/api/member/identity/memberIdentityCreateMultiple.ts b/backend/src/api/member/identity/memberIdentityCreateMultiple.ts index 1ac2913ada..46fdfa42e4 100644 --- a/backend/src/api/member/identity/memberIdentityCreateMultiple.ts +++ b/backend/src/api/member/identity/memberIdentityCreateMultiple.ts @@ -17,7 +17,7 @@ import PermissionChecker from '../../../services/user/permissionChecker' * @response 429 - Too many requests */ export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + new PermissionChecker(req).validateHas(Permissions.values.memberIdentityCreate) const memberIdentityService = new MemberIdentityService(req) diff --git a/backend/src/api/member/identity/memberIdentityDelete.ts b/backend/src/api/member/identity/memberIdentityDelete.ts index cb2c81bd1e..585beb0084 100644 --- a/backend/src/api/member/identity/memberIdentityDelete.ts +++ b/backend/src/api/member/identity/memberIdentityDelete.ts @@ -18,7 +18,7 @@ import PermissionChecker from '../../../services/user/permissionChecker' * @response 429 - Too many requests */ export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + new PermissionChecker(req).validateHas(Permissions.values.memberIdentityDestroy) const memberIdentityService = new MemberIdentityService(req) diff --git a/backend/src/api/member/identity/memberIdentityDetectedList.ts b/backend/src/api/member/identity/memberIdentityDetectedList.ts new file mode 100644 index 0000000000..caf2e4e2d8 --- /dev/null +++ b/backend/src/api/member/identity/memberIdentityDetectedList.ts @@ -0,0 +1,28 @@ +import MemberIdentityService from '@/services/member/memberIdentityService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * GET /member/:memberIdOrLfid/detected-identity + * @summary Query detected identities for a given LFID or memberID + * @tag Members + * @security Bearer + * @description Query detected identities for a given LFID or memberID + * @pathParam {string} memberIdOrLfid - member ID or LFID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.Member + * @response 401 - Unauthorized + * @response 404 - Not found + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberIdentityRead) + + const memberIdentityService = new MemberIdentityService(req) + + const payload = await memberIdentityService.detectedList(req.memberId) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/identity/memberIdentityList.ts b/backend/src/api/member/identity/memberIdentityList.ts index b053b8ce77..89f41939cb 100644 --- a/backend/src/api/member/identity/memberIdentityList.ts +++ b/backend/src/api/member/identity/memberIdentityList.ts @@ -18,7 +18,7 @@ import PermissionChecker from '../../../services/user/permissionChecker' * @response 429 - Too many requests */ export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberRead) + new PermissionChecker(req).validateHas(Permissions.values.memberIdentityRead) const memberIdentityService = new MemberIdentityService(req) diff --git a/backend/src/api/member/identity/memberIdentityUpdate.ts b/backend/src/api/member/identity/memberIdentityUpdate.ts index d39168c1f4..809f8c8bd9 100644 --- a/backend/src/api/member/identity/memberIdentityUpdate.ts +++ b/backend/src/api/member/identity/memberIdentityUpdate.ts @@ -18,7 +18,7 @@ import PermissionChecker from '../../../services/user/permissionChecker' * @response 429 - Too many requests */ export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + new PermissionChecker(req).validateHas(Permissions.values.memberIdentityEdit) const memberIdentityService = new MemberIdentityService(req) diff --git a/backend/src/api/member/identity/memberIdentityUserValidation.ts b/backend/src/api/member/identity/memberIdentityUserValidation.ts new file mode 100644 index 0000000000..afd43d58e6 --- /dev/null +++ b/backend/src/api/member/identity/memberIdentityUserValidation.ts @@ -0,0 +1,27 @@ +import MemberIdentityService from '@/services/member/memberIdentityService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * POST /member/:memberIdOrLfid/user-validation + * @summary Create user validation for member identity + * @tag Members + * @security Bearer + * @description Creates user validation when identity is accepted or rejected. + * @pathParam {string} memberIdOrLfid - member ID or LFID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.MemberIdentity + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberUserValidationCreate) + + const memberIdentityService = new MemberIdentityService(req) + + const payload = await memberIdentityService.userValidation(req.memberId, req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/member/organization/index.ts b/backend/src/api/member/organization/index.ts index 1a8723e0c5..2d708de7b4 100644 --- a/backend/src/api/member/organization/index.ts +++ b/backend/src/api/member/organization/index.ts @@ -1,8 +1,13 @@ import { safeWrap } from '@/middlewares/errorMiddleware' +import { memberIdOrLfidMiddleware } from '@/middlewares/memberIdOrLfidMiddleware' export default (app) => { // Member Organiaztion List - app.get(`/member/:memberId/organization`, safeWrap(require('./memberOrganizationList').default)) + app.get( + `/member/:memberIdOrLfid/organization`, + memberIdOrLfidMiddleware, + safeWrap(require('./memberOrganizationList').default), + ) // Member Organiaztion Create app.post( @@ -21,4 +26,16 @@ export default (app) => { `/member/:memberId/organization/:id`, safeWrap(require('./memberOrganizationDelete').default), ) + + app.get( + `/member/:memberIdOrLfid/organization/status`, + memberIdOrLfidMiddleware, + safeWrap(require('./memberOrganizationStatus').default), + ) + + app.post( + `/member/:memberIdOrLfid/organization/user-validation`, + memberIdOrLfidMiddleware, + safeWrap(require('./memberOrganizationUserValidation').default), + ) } diff --git a/backend/src/api/member/organization/memberOrganizationCreate.ts b/backend/src/api/member/organization/memberOrganizationCreate.ts index 462e222733..8b06d5cc8d 100644 --- a/backend/src/api/member/organization/memberOrganizationCreate.ts +++ b/backend/src/api/member/organization/memberOrganizationCreate.ts @@ -16,7 +16,7 @@ import PermissionChecker from '../../../services/user/permissionChecker' * @response 429 - Too many requests */ export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + new PermissionChecker(req).validateHas(Permissions.values.memberOrganizationCreate) const memberOrganizationsService = new MemberOrganizationsService(req) diff --git a/backend/src/api/member/organization/memberOrganizationDelete.ts b/backend/src/api/member/organization/memberOrganizationDelete.ts index 84a7f0e338..54008b62b2 100644 --- a/backend/src/api/member/organization/memberOrganizationDelete.ts +++ b/backend/src/api/member/organization/memberOrganizationDelete.ts @@ -18,7 +18,7 @@ import PermissionChecker from '../../../services/user/permissionChecker' * @response 429 - Too many requests */ export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + new PermissionChecker(req).validateHas(Permissions.values.memberOrganizationDestroy) const memberOrganizationsService = new MemberOrganizationsService(req) diff --git a/backend/src/api/member/organization/memberOrganizationList.ts b/backend/src/api/member/organization/memberOrganizationList.ts index 451a29ed1c..ccbab868ae 100644 --- a/backend/src/api/member/organization/memberOrganizationList.ts +++ b/backend/src/api/member/organization/memberOrganizationList.ts @@ -4,7 +4,7 @@ import Permissions from '../../../security/permissions' import PermissionChecker from '../../../services/user/permissionChecker' /** - * GET /member/:memberId/organization + * GET /member/:memberIdOrLfid/organization * @summary Query member organizations * @tag Members * @security Bearer @@ -16,11 +16,11 @@ import PermissionChecker from '../../../services/user/permissionChecker' * @response 429 - Too many requests */ export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberRead) + new PermissionChecker(req).validateHas(Permissions.values.memberOrganizationRead) const memberOrganizationsService = new MemberOrganizationsService(req) - const payload = await memberOrganizationsService.list(req.params.memberId) + const payload = await memberOrganizationsService.list(req.memberId) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/member/organization/memberOrganizationStatus.ts b/backend/src/api/member/organization/memberOrganizationStatus.ts new file mode 100644 index 0000000000..c6dc044fc2 --- /dev/null +++ b/backend/src/api/member/organization/memberOrganizationStatus.ts @@ -0,0 +1,26 @@ +import MemberOrganizationsService from '@/services/member/memberOrganizationsService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * GET /member/:memberIdOrLfid/organization/status + * @summary Check if work history records exist + * @tag Members + * @security Bearer + * @description Returns { status: true } if work history exists, otherwise { status: false } + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.Organization + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberOrganizationRead) + + const memberOrganizationsService = new MemberOrganizationsService(req) + + const status = await memberOrganizationsService.getStatus(req.memberId) + + await req.responseHandler.success(req, res, { status }) +} diff --git a/backend/src/api/member/organization/memberOrganizationUpdate.ts b/backend/src/api/member/organization/memberOrganizationUpdate.ts index 117a5a3f10..fe6d7de128 100644 --- a/backend/src/api/member/organization/memberOrganizationUpdate.ts +++ b/backend/src/api/member/organization/memberOrganizationUpdate.ts @@ -18,7 +18,7 @@ import PermissionChecker from '../../../services/user/permissionChecker' * @response 429 - Too many requests */ export default async (req, res) => { - new PermissionChecker(req).validateHas(Permissions.values.memberEdit) + new PermissionChecker(req).validateHas(Permissions.values.memberOrganizationEdit) const memberOrganizationsService = new MemberOrganizationsService(req) diff --git a/backend/src/api/member/organization/memberOrganizationUserValidation.ts b/backend/src/api/member/organization/memberOrganizationUserValidation.ts new file mode 100644 index 0000000000..0ab70451fd --- /dev/null +++ b/backend/src/api/member/organization/memberOrganizationUserValidation.ts @@ -0,0 +1,27 @@ +import MemberOrganizationsService from '@/services/member/memberOrganizationsService' + +import Permissions from '../../../security/permissions' +import PermissionChecker from '../../../services/user/permissionChecker' + +/** + * POST /member/:memberIdOrLfid/organization/user-validation + * @summary Create user validation for member organization + * @tag Members + * @security Bearer + * @description Creates user validation when organization is created, updated or deleted. + * @pathParam {string} memberIdOrLfid - member ID or LFID + * @response 200 - Ok + * @responseContent {MemberList} 200.application/json + * @responseExample {MemberList} 200.application/json.Organization + * @response 401 - Unauthorized + * @response 429 - Too many requests + */ +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberUserValidationCreate) + + const memberOrganizationsService = new MemberOrganizationsService(req) + + const payload = await memberOrganizationsService.userValidation(req.memberId, req.body) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/database/migrations/U1745401102__memberUserValidations.sql b/backend/src/database/migrations/U1745401102__memberUserValidations.sql new file mode 100644 index 0000000000..e7bc2a5243 --- /dev/null +++ b/backend/src/database/migrations/U1745401102__memberUserValidations.sql @@ -0,0 +1 @@ +drop table if exists "memberUserValidations"; \ No newline at end of file diff --git a/backend/src/database/migrations/V1745401102__memberUserValidations.sql b/backend/src/database/migrations/V1745401102__memberUserValidations.sql new file mode 100644 index 0000000000..22d3bd6367 --- /dev/null +++ b/backend/src/database/migrations/V1745401102__memberUserValidations.sql @@ -0,0 +1,13 @@ +create table "memberUserValidations"( + id uuid primary key default uuid_generate_v4(), + "memberId" uuid not null references members(id), + action varchar(255) not null, + type varchar(255) not null, + details jsonb not null + -- todo: ask joana if we need timestamp or can reuse createdAt/updatedAt + -- createdAt timestamp with time zone default now(), + -- updatedAt timestamp with time zone default now(), + -- deletedAt timestamp with time zone +); + + diff --git a/backend/src/middlewares/memberIdOrLfidMiddleware.ts b/backend/src/middlewares/memberIdOrLfidMiddleware.ts new file mode 100644 index 0000000000..536c295f15 --- /dev/null +++ b/backend/src/middlewares/memberIdOrLfidMiddleware.ts @@ -0,0 +1,28 @@ +import { validateUUID } from '@crowd/common' +import { lfidToMemberId } from '@crowd/data-access-layer' +import SequelizeRepository from '@/database/repositories/sequelizeRepository' + +/** + * Middleware to resolve `memberIdOrLfid` route param to a member UUID. + * + * - If a memberId (uuid), sets `req.memberId` and continues. + * - If an lfid (used by external services), resolves to memberId via db lookup. + * Used in routes that accept either type for member identification. + */ +export async function memberIdOrLfidMiddleware(req, res, next) { + const identifier = req.params.memberIdOrLfid + const qx = SequelizeRepository.getQueryExecutor({ database: req.database, ...req }) + + if (validateUUID(identifier)) { + req.memberId = identifier + return next() + } + + const memberId = await lfidToMemberId(qx, identifier) + if (!memberId) { + return res.status(404).json({ error: 'Member not found for given lfid!' }) + } + + req.memberId = memberId + return next() +} \ No newline at end of file diff --git a/backend/src/security/permissions.ts b/backend/src/security/permissions.ts index 22764fbba3..2df7f2879f 100644 --- a/backend/src/security/permissions.ts +++ b/backend/src/security/permissions.ts @@ -84,6 +84,42 @@ class Permissions { id: 'memberAutocomplete', allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly], }, + memberIdentityCreate: { + id: 'memberIdentityCreate', + allowedRoles: [roles.admin, roles.projectAdmin], + }, + memberIdentityEdit: { + id: 'memberIdentityEdit', + allowedRoles: [roles.admin, roles.projectAdmin], + }, + memberIdentityDestroy: { + id: 'memberIdentityDestroy', + allowedRoles: [roles.admin, roles.projectAdmin], + }, + memberIdentityRead: { + id: 'memberIdentityRead', + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly, roles.externalService], + }, + memberOrganizationCreate: { + id: 'memberOrganizationCreate', + allowedRoles: [roles.admin, roles.projectAdmin], + }, + memberOrganizationEdit: { + id: 'memberOrganizationEdit', + allowedRoles: [roles.admin, roles.projectAdmin], + }, + memberOrganizationDestroy: { + id: 'memberOrganizationDestroy', + allowedRoles: [roles.admin, roles.projectAdmin], + }, + memberOrganizationRead: { + id: 'memberOrganizationRead', + allowedRoles: [roles.admin, roles.projectAdmin, roles.readonly, roles.externalService], + }, + memberUserValidationCreate: { + id: 'memberUserValidationCreate', + allowedRoles: [roles.externalService], + }, activityImport: { id: 'activityImport', allowedRoles: [roles.admin, roles.projectAdmin], diff --git a/backend/src/security/roles.ts b/backend/src/security/roles.ts index 7718effc48..dd3990157d 100644 --- a/backend/src/security/roles.ts +++ b/backend/src/security/roles.ts @@ -4,6 +4,7 @@ class Roles { admin: 'admin', readonly: 'readonly', projectAdmin: 'projectAdmin', + externalService: 'externalService', } } } diff --git a/backend/src/services/member/memberIdentityService.ts b/backend/src/services/member/memberIdentityService.ts index 83a53ae34a..85ae83e29b 100644 --- a/backend/src/services/member/memberIdentityService.ts +++ b/backend/src/services/member/memberIdentityService.ts @@ -1,7 +1,7 @@ /* eslint-disable no-continue */ import lodash from 'lodash' -import { captureApiChange, memberEditIdentitiesAction } from '@crowd/audit-logs' +import { captureApiChange, memberEditIdentitiesAction, memberUserValidationAction } from '@crowd/audit-logs' import { Error409 } from '@crowd/common' import { checkIdentityExistance, @@ -11,10 +11,13 @@ import { findMemberIdentityById, touchMemberUpdatedAt, updateMemberIdentity, + createMemberUserValidation, + getMemberUserValidations, } from '@crowd/data-access-layer/src/members' import { LoggerBase } from '@crowd/logging' -import { IMemberIdentity } from '@crowd/types' +import { IMemberIdentity, IMemberIdentityUserValidationDetails, IMemberIdentityWithActivityCount, IMemberUserValidationInput, MemberIdentityType, MemberUserValidationType } from '@crowd/types' +import { getActivityCountOfMemberIdentities } from '@crowd/data-access-layer' import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' import MemberRepository from '@/database/repositories/memberRepository' import SequelizeRepository from '@/database/repositories/sequelizeRepository' @@ -35,6 +38,72 @@ export default class MemberIdentityService extends LoggerBase { return fetchMemberIdentities(qx, memberId) } + /** + * Returns a list of detected identities with activity count + * @warning This method is used by external systems + */ + async detectedList(memberId: string): Promise { + const qx = SequelizeRepository.getQueryExecutor(this.options) + + // Fetch all identities + const [identities, validated] = await Promise.all([ + fetchMemberIdentities( + qx, + memberId, + { verified: true }, + ['id', 'platform', 'type', 'value'] + ), + getMemberUserValidations(qx, memberId, { + type: MemberUserValidationType.IDENTITY, + }), + ]) + + this.options.log.info('validated', validated) + + const validatedIdentityIds = validated.map((r) => r.details.identityId) + + // Filter out identities that were previously validated + const filteredIdentities = identities.filter( + (identity) => !validatedIdentityIds.includes(identity.id) + ) + + // Only username identities for activity count + const usernameIdentities = filteredIdentities.filter( + (identity) => identity.type === MemberIdentityType.USERNAME + ) + + // Get activity count map per username identity + const activityMap = await getActivityCountOfMemberIdentities(this.options.qdb, memberId, usernameIdentities, true) + + return filteredIdentities.map((identity) => ({ + ...identity, + ...(identity.type === MemberIdentityType.USERNAME && { + activityCount: activityMap[`${identity.platform}:${identity.value}`] || 0, + }), + })) + } + + async userValidation(memberId: string, data: IMemberUserValidationInput): Promise { + try { + const qx = SequelizeRepository.getQueryExecutor(this.options) + const result = await createMemberUserValidation(qx, memberId, { + type: MemberUserValidationType.IDENTITY, + ...data + }) + + // record changes for audit logs + await captureApiChange( + this.options, + memberUserValidationAction(memberId, async (captureState) => { + captureState(result) + }), + ) + } catch (error) { + this.log.error(error) + throw error + } + } + // Member identity creation async create(memberId: string, data: Partial): Promise { let tx diff --git a/backend/src/services/member/memberOrganizationsService.ts b/backend/src/services/member/memberOrganizationsService.ts index 890945112c..72191e16b4 100644 --- a/backend/src/services/member/memberOrganizationsService.ts +++ b/backend/src/services/member/memberOrganizationsService.ts @@ -4,15 +4,18 @@ import { OrganizationField, cleanSoftDeletedMemberOrganization, createMemberOrganization, + createMemberUserValidation, deleteMemberOrganization, fetchMemberOrganizations, + getMemberOrganizationStatus, queryOrgs, updateMemberOrganization, } from '@crowd/data-access-layer' import { findOverrides as findMemberOrganizationAffiliationOverrides } from '@crowd/data-access-layer/src/member_organization_affiliation_overrides' import { LoggerBase } from '@crowd/logging' -import { IMemberOrganization, IOrganization, IRenderFriendlyMemberOrganization } from '@crowd/types' +import { IMemberOrganization, IMemberOrganizationUserValidationDetails, IMemberUserValidationInput, IOrganization, IRenderFriendlyMemberOrganization, MemberUserValidationType } from '@crowd/types' +import { captureApiChange, memberUserValidationAction } from '@crowd/audit-logs' import SequelizeRepository from '@/database/repositories/sequelizeRepository' import { IServiceOptions } from '../IServiceOptions' @@ -181,4 +184,30 @@ export default class MemberOrganizationsService extends LoggerBase { throw error } } -} + + async getStatus(memberId: string): Promise { + const qx = SequelizeRepository.getQueryExecutor(this.options) + return getMemberOrganizationStatus(qx, memberId) + } + + async userValidation(memberId: string, data: IMemberUserValidationInput): Promise { + try { + const qx = SequelizeRepository.getQueryExecutor(this.options) + const result = await createMemberUserValidation(qx, memberId, { + type: MemberUserValidationType.WORK_HISTORY, + ...data + }) + + // record changes for audit logs + await captureApiChange( + this.options, + memberUserValidationAction(memberId, async (captureState) => { + captureState(result) + }), + ) + } catch (error) { + this.log.error(error) + throw error + } + } +} \ No newline at end of file diff --git a/services/libs/audit-logs/src/actions.ts b/services/libs/audit-logs/src/actions.ts index ae46d605b8..a7a51003f6 100644 --- a/services/libs/audit-logs/src/actions.ts +++ b/services/libs/audit-logs/src/actions.ts @@ -127,6 +127,13 @@ export function integrationConnectAction( return createEntityAction(ActionType.INTEGRATIONS_CONNECT, entityId, captureFn) } +export function memberUserValidationAction( + entityId: string, + captureFn: CaptureOneFn, +): BuildActionFn { + return createEntityAction(ActionType.MEMBER_USER_VALIDATION, entityId, captureFn) +} + export function memberEditIdentitiesAction( entityId: string, captureFn: CaptureFn, diff --git a/services/libs/data-access-layer/src/activities/sql.ts b/services/libs/data-access-layer/src/activities/sql.ts index 5a067b6153..637743d9ac 100644 --- a/services/libs/data-access-layer/src/activities/sql.ts +++ b/services/libs/data-access-layer/src/activities/sql.ts @@ -16,7 +16,6 @@ import { IEnrichableMemberIdentityActivityAggregate, IMemberIdentity, ITimeseriesDatapoint, - MemberIdentityType, PageData, PlatformType, } from '@crowd/types' @@ -960,47 +959,66 @@ export async function getMemberAggregates( }) } +/** + * Returns activity counts for the given identities of a member. + * If perIdentity is true, returns a map of counts per identity (platform:value), + * otherwise returns the total count across all identities. + */ export async function getActivityCountOfMemberIdentities( qdbConn: DbConnOrTx, memberId: string, identities: IMemberIdentity[], -): Promise { + perIdentity = false, +): Promise> { if (identities.length === 0) { throw new Error(`No identities sent!`) } - let query = ` - select count(id) from activities - where "deletedAt" is null and "memberId" = $(memberId) - ` - + // Build conditions and params for each identity const replacementKey = (key: string, index: number) => `${key}${index}` - const conditions: string[] = [] + const params: Record = { memberId } - const params = { - memberId, - } + identities.forEach((identity, idx) => { + const platformKey = replacementKey('platform', idx) + const usernameKey = replacementKey('username', idx) + conditions.push(`(platform = $(${platformKey}) and username = $(${usernameKey}))`) + params[platformKey] = identity.platform + params[usernameKey] = identity.value + }) - for ( - let i = 0; - i < identities.filter((i) => i.type === MemberIdentityType.USERNAME).length; - i++ - ) { - const platformKey = replacementKey('platform', i) - const usernameKey = replacementKey('username', i) + let query = '' + + // calculate activity count per identity + if (perIdentity) { + query = ` + SELECT platform, username, COUNT(id) AS count + FROM activities + WHERE "deletedAt" IS NULL + AND "memberId" = $(memberId) + ${conditions.length ? `AND (${conditions.join(' OR ')})` : ''} + GROUP BY platform, username + ` + const rows = await qdbConn.any(query, params) + const result: Record = {} - conditions.push(`(platform = $(${platformKey}) and username = $(${usernameKey}))`) + for (const row of rows) { + result[`${row.platform}:${row.username}`] = Number(row.count) + } - params[platformKey] = identities[i].platform - params[usernameKey] = identities[i].value - } + return result + } else { + query = ` + SELECT count(id) as count FROM activities + WHERE "deletedAt" IS NULL AND "memberId" = $(memberId) + ` + if (conditions.length > 0) { + query = `${query} AND (${conditions.join(' OR ')})` + } - if (conditions.length > 0) { - query = `${query} and (${conditions.join(' or ')})` + const row = await qdbConn.one(query, params) + return Number(row?.count || 0) } - - return await qdbConn.one(query, params, (a) => a.count) } export async function countMembersWithActivities( diff --git a/services/libs/data-access-layer/src/audit_logs/repo.ts b/services/libs/data-access-layer/src/audit_logs/repo.ts index 54e56c684b..f5d125034a 100644 --- a/services/libs/data-access-layer/src/audit_logs/repo.ts +++ b/services/libs/data-access-layer/src/audit_logs/repo.ts @@ -42,6 +42,7 @@ export enum ActionType { ORGANIZATIONS_CREATE = 'organizations-create', INTEGRATIONS_CONNECT = 'integrations-connect', INTEGRATIONS_RECONNECT = 'integrations-reconnect', + MEMBER_USER_VALIDATION = 'member-user-validation', } const ACTION_TYPES_ENTITY_TYPES = { @@ -59,6 +60,7 @@ const ACTION_TYPES_ENTITY_TYPES = { [ActionType.ORGANIZATIONS_CREATE]: EntityType.ORGANIZATION, [ActionType.INTEGRATIONS_CONNECT]: EntityType.INTEGRATION, [ActionType.INTEGRATIONS_RECONNECT]: EntityType.INTEGRATION, + [ActionType.MEMBER_USER_VALIDATION]: EntityType.MEMBER, } export async function addAuditAction( diff --git a/services/libs/data-access-layer/src/members/identities.ts b/services/libs/data-access-layer/src/members/identities.ts index f075a0c447..3e4084a65c 100644 --- a/services/libs/data-access-layer/src/members/identities.ts +++ b/services/libs/data-access-layer/src/members/identities.ts @@ -2,18 +2,38 @@ import { IMemberIdentity, MemberIdentityType } from '@crowd/types' import { QueryExecutor } from '../queryExecutor' +const DEFAULT_COLUMNS: (keyof IMemberIdentity)[] = [ + 'id', + 'platform', + 'sourceId', + 'type', + 'value', + 'verified', +] + export async function fetchMemberIdentities( qx: QueryExecutor, memberId: string, + filter: { verified?: boolean } = {}, + columns: (keyof IMemberIdentity)[] = DEFAULT_COLUMNS, ): Promise { + const where: string[] = ['"memberId" = $(memberId)'] + + if (filter.verified) { + where.push('verified = $(verified)') + } + + const selectedColumns = columns.map((c) => `"${c}"`).join(', ') + return qx.select( ` - SELECT id, platform, "sourceId", type, value, verified + SELECT ${selectedColumns} FROM "memberIdentities" - WHERE "memberId" = $(memberId) + WHERE ${where.join(' AND ')} `, { memberId, + verified: filter.verified, }, ) } @@ -162,3 +182,19 @@ export async function deleteMemberIdentity( }, ) } + +export async function lfidToMemberId(qx: QueryExecutor, lfid: string): Promise { + const result = await qx.selectOneOrNone( + ` + select "memberId" + from "memberIdentities" + where "platform" = 'lfid' and "value" = $(lfid) + and verified = true limit 1; + `, + { + lfid, + }, + ) + + return result?.memberId ?? null +} diff --git a/services/libs/data-access-layer/src/members/index.ts b/services/libs/data-access-layer/src/members/index.ts index 62519c1927..eb900f4219 100644 --- a/services/libs/data-access-layer/src/members/index.ts +++ b/services/libs/data-access-layer/src/members/index.ts @@ -6,3 +6,4 @@ export * from './others' export * from './attributes' export * from './dashboard' export * from './contributions' +export * from './userValidations' diff --git a/services/libs/data-access-layer/src/members/organizations.ts b/services/libs/data-access-layer/src/members/organizations.ts index eed30be1d6..773e99fb3b 100644 --- a/services/libs/data-access-layer/src/members/organizations.ts +++ b/services/libs/data-access-layer/src/members/organizations.ts @@ -119,6 +119,21 @@ export async function deleteMemberOrganization( ) } +export async function getMemberOrganizationStatus( + qx: QueryExecutor, + memberId: string, +): Promise { + const result = await qx.selectOneOrNone( + ` + SELECT 1 FROM "memberOrganizations" + WHERE "memberId" = $(memberId) AND "deletedAt" IS NULL LIMIT 1; + `, + { memberId }, + ) + + return !!result +} + export async function cleanSoftDeletedMemberOrganization( qx: QueryExecutor, memberId: string, diff --git a/services/libs/data-access-layer/src/members/types.ts b/services/libs/data-access-layer/src/members/types.ts index 7e03f8dd49..6d301cdb34 100644 --- a/services/libs/data-access-layer/src/members/types.ts +++ b/services/libs/data-access-layer/src/members/types.ts @@ -1,4 +1,11 @@ -import { IMemberAttribute, MemberAttributeType } from '@crowd/types' +import { + IMemberAttribute, + IMemberUserValidationInput, + MemberAttributeType, + MemberIdentityUserValidationAction, + MemberOrganizationUserValidationAction, + MemberUserValidationType, +} from '@crowd/types' export interface IQueryNumberOfNewMembers { segmentIds?: string[] @@ -63,3 +70,20 @@ export interface IDbMemberData { averageSentiment?: number activeDaysCount?: number } + +export interface IMemberUserValidationRecord extends IMemberUserValidationInput { + type: MemberUserValidationType +} + +export interface IMemberUserValidationFilter { + action?: MemberIdentityUserValidationAction | MemberOrganizationUserValidationAction + type?: MemberUserValidationType +} + +export interface IMemberUserValidation { + id: string + memberId: string + action: string + type: MemberUserValidationType + details: Record +} diff --git a/services/libs/data-access-layer/src/members/userValidations.ts b/services/libs/data-access-layer/src/members/userValidations.ts new file mode 100644 index 0000000000..a0530d12ad --- /dev/null +++ b/services/libs/data-access-layer/src/members/userValidations.ts @@ -0,0 +1,55 @@ +import { QueryExecutor } from '../queryExecutor' + +import { + IMemberUserValidation, + IMemberUserValidationFilter, + IMemberUserValidationRecord, +} from './types' + +export async function createMemberUserValidation( + qx: QueryExecutor, + memberId: string, + data: IMemberUserValidationRecord, +): Promise { + return qx.selectOne( + ` + INSERT INTO "memberUserValidations" ("memberId", "action", "type", "details") + VALUES ($(memberId), $(action), $(type), $(details)) + RETURNING * + `, + { + memberId, + action: data.action, + type: data.type, + details: data.details, + }, + ) +} + +export async function getMemberUserValidations( + qx: QueryExecutor, + memberId: string, + filter: IMemberUserValidationFilter, +): Promise { + const where: string[] = ['"memberId" = $(memberId)'] + + if (filter.action) { + where.push('"action" = $(action)') + } + + if (filter.type) { + where.push('"type" = $(type)') + } + + const result = await qx.result( + `SELECT * FROM "memberUserValidations" + WHERE ${where.join(' AND ')}`, + { + memberId, + action: filter.action, + type: filter.type, + }, + ) + + return Array.isArray(result.rows) ? result.rows : [] +} diff --git a/services/libs/types/src/members.ts b/services/libs/types/src/members.ts index 26c8ba6cc0..0fba133ee2 100644 --- a/services/libs/types/src/members.ts +++ b/services/libs/types/src/members.ts @@ -32,6 +32,10 @@ export interface IMemberIdentity { verified: boolean } +export interface IMemberIdentityWithActivityCount extends IMemberIdentity { + activityCount?: number +} + export interface IActivityIdentity { username: string platform: string @@ -227,3 +231,36 @@ export interface IMemberOrganizationAffiliationOverride { allowAffiliation: boolean isPrimaryWorkExperience: boolean } + +export enum MemberUserValidationType { + IDENTITY = 'identity', + WORK_HISTORY = 'work-history', +} + +export enum MemberIdentityUserValidationAction { + ACCEPT = 'accept', + REJECT = 'reject', +} + +export enum MemberOrganizationUserValidationAction { + CREATE = 'create', + UPDATE = 'update', + DELETE = 'delete', +} + +export interface IMemberUserValidationInput { + action: string + details: T +} + +export interface IMemberIdentityUserValidationDetails { + identityId: string +} + +export interface IMemberOrganizationUserValidationDetails { + organizationId: string + organizationName: string + title: string + dateStart: string + dateEnd: string +} From 2350f265ca46a78ba25bcfa2b7b3075e3b381b91 Mon Sep 17 00:00:00 2001 From: Yeganathan S Date: Fri, 25 Apr 2025 00:53:36 +0530 Subject: [PATCH 2/7] make prettier happy --- backend/src/api/member/identity/index.ts | 12 ++++- .../middlewares/memberIdOrLfidMiddleware.ts | 3 +- .../services/member/memberIdentityService.ts | 50 ++++++++++++------- .../member/memberOrganizationsService.ts | 22 +++++--- 4 files changed, 60 insertions(+), 27 deletions(-) diff --git a/backend/src/api/member/identity/index.ts b/backend/src/api/member/identity/index.ts index 5ee65ecb1f..093dafd569 100644 --- a/backend/src/api/member/identity/index.ts +++ b/backend/src/api/member/identity/index.ts @@ -17,7 +17,15 @@ export default (app) => { // Member Identity Delete app.delete(`/member/:memberId/identity/:id`, safeWrap(require('./memberIdentityDelete').default)) - app.get(`/member/:memberIdOrLfid/detected-identity`, memberIdOrLfidMiddleware, safeWrap(require('./memberIdentityDetectedList').default)) + app.get( + `/member/:memberIdOrLfid/detected-identity`, + memberIdOrLfidMiddleware, + safeWrap(require('./memberIdentityDetectedList').default), + ) - app.post(`/member/:memberIdOrLfid/user-validation`, memberIdOrLfidMiddleware, safeWrap(require('./memberIdentityUserValidation').default)) + app.post( + `/member/:memberIdOrLfid/user-validation`, + memberIdOrLfidMiddleware, + safeWrap(require('./memberIdentityUserValidation').default), + ) } diff --git a/backend/src/middlewares/memberIdOrLfidMiddleware.ts b/backend/src/middlewares/memberIdOrLfidMiddleware.ts index 536c295f15..c65c536401 100644 --- a/backend/src/middlewares/memberIdOrLfidMiddleware.ts +++ b/backend/src/middlewares/memberIdOrLfidMiddleware.ts @@ -1,5 +1,6 @@ import { validateUUID } from '@crowd/common' import { lfidToMemberId } from '@crowd/data-access-layer' + import SequelizeRepository from '@/database/repositories/sequelizeRepository' /** @@ -25,4 +26,4 @@ export async function memberIdOrLfidMiddleware(req, res, next) { req.memberId = memberId return next() -} \ No newline at end of file +} diff --git a/backend/src/services/member/memberIdentityService.ts b/backend/src/services/member/memberIdentityService.ts index 85ae83e29b..45c8bdfb03 100644 --- a/backend/src/services/member/memberIdentityService.ts +++ b/backend/src/services/member/memberIdentityService.ts @@ -1,23 +1,34 @@ /* eslint-disable no-continue */ import lodash from 'lodash' -import { captureApiChange, memberEditIdentitiesAction, memberUserValidationAction } from '@crowd/audit-logs' +import { + captureApiChange, + memberEditIdentitiesAction, + memberUserValidationAction, +} from '@crowd/audit-logs' import { Error409 } from '@crowd/common' +import { getActivityCountOfMemberIdentities } from '@crowd/data-access-layer' import { checkIdentityExistance, createMemberIdentity, + createMemberUserValidation, deleteMemberIdentity, fetchMemberIdentities, findMemberIdentityById, + getMemberUserValidations, touchMemberUpdatedAt, updateMemberIdentity, - createMemberUserValidation, - getMemberUserValidations, } from '@crowd/data-access-layer/src/members' import { LoggerBase } from '@crowd/logging' -import { IMemberIdentity, IMemberIdentityUserValidationDetails, IMemberIdentityWithActivityCount, IMemberUserValidationInput, MemberIdentityType, MemberUserValidationType } from '@crowd/types' +import { + IMemberIdentity, + IMemberIdentityUserValidationDetails, + IMemberIdentityWithActivityCount, + IMemberUserValidationInput, + MemberIdentityType, + MemberUserValidationType, +} from '@crowd/types' -import { getActivityCountOfMemberIdentities } from '@crowd/data-access-layer' import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' import MemberRepository from '@/database/repositories/memberRepository' import SequelizeRepository from '@/database/repositories/sequelizeRepository' @@ -44,15 +55,10 @@ export default class MemberIdentityService extends LoggerBase { */ async detectedList(memberId: string): Promise { const qx = SequelizeRepository.getQueryExecutor(this.options) - + // Fetch all identities const [identities, validated] = await Promise.all([ - fetchMemberIdentities( - qx, - memberId, - { verified: true }, - ['id', 'platform', 'type', 'value'] - ), + fetchMemberIdentities(qx, memberId, { verified: true }, ['id', 'platform', 'type', 'value']), getMemberUserValidations(qx, memberId, { type: MemberUserValidationType.IDENTITY, }), @@ -64,16 +70,21 @@ export default class MemberIdentityService extends LoggerBase { // Filter out identities that were previously validated const filteredIdentities = identities.filter( - (identity) => !validatedIdentityIds.includes(identity.id) + (identity) => !validatedIdentityIds.includes(identity.id), ) // Only username identities for activity count const usernameIdentities = filteredIdentities.filter( - (identity) => identity.type === MemberIdentityType.USERNAME + (identity) => identity.type === MemberIdentityType.USERNAME, ) // Get activity count map per username identity - const activityMap = await getActivityCountOfMemberIdentities(this.options.qdb, memberId, usernameIdentities, true) + const activityMap = await getActivityCountOfMemberIdentities( + this.options.qdb, + memberId, + usernameIdentities, + true, + ) return filteredIdentities.map((identity) => ({ ...identity, @@ -83,14 +94,17 @@ export default class MemberIdentityService extends LoggerBase { })) } - async userValidation(memberId: string, data: IMemberUserValidationInput): Promise { + async userValidation( + memberId: string, + data: IMemberUserValidationInput, + ): Promise { try { const qx = SequelizeRepository.getQueryExecutor(this.options) const result = await createMemberUserValidation(qx, memberId, { type: MemberUserValidationType.IDENTITY, - ...data + ...data, }) - + // record changes for audit logs await captureApiChange( this.options, diff --git a/backend/src/services/member/memberOrganizationsService.ts b/backend/src/services/member/memberOrganizationsService.ts index 72191e16b4..d0145067ec 100644 --- a/backend/src/services/member/memberOrganizationsService.ts +++ b/backend/src/services/member/memberOrganizationsService.ts @@ -1,4 +1,5 @@ /* eslint-disable no-continue */ +import { captureApiChange, memberUserValidationAction } from '@crowd/audit-logs' import { Error404 } from '@crowd/common' import { OrganizationField, @@ -13,9 +14,15 @@ import { } from '@crowd/data-access-layer' import { findOverrides as findMemberOrganizationAffiliationOverrides } from '@crowd/data-access-layer/src/member_organization_affiliation_overrides' import { LoggerBase } from '@crowd/logging' -import { IMemberOrganization, IMemberOrganizationUserValidationDetails, IMemberUserValidationInput, IOrganization, IRenderFriendlyMemberOrganization, MemberUserValidationType } from '@crowd/types' +import { + IMemberOrganization, + IMemberOrganizationUserValidationDetails, + IMemberUserValidationInput, + IOrganization, + IRenderFriendlyMemberOrganization, + MemberUserValidationType, +} from '@crowd/types' -import { captureApiChange, memberUserValidationAction } from '@crowd/audit-logs' import SequelizeRepository from '@/database/repositories/sequelizeRepository' import { IServiceOptions } from '../IServiceOptions' @@ -190,14 +197,17 @@ export default class MemberOrganizationsService extends LoggerBase { return getMemberOrganizationStatus(qx, memberId) } - async userValidation(memberId: string, data: IMemberUserValidationInput): Promise { + async userValidation( + memberId: string, + data: IMemberUserValidationInput, + ): Promise { try { const qx = SequelizeRepository.getQueryExecutor(this.options) const result = await createMemberUserValidation(qx, memberId, { type: MemberUserValidationType.WORK_HISTORY, - ...data + ...data, }) - + // record changes for audit logs await captureApiChange( this.options, @@ -210,4 +220,4 @@ export default class MemberOrganizationsService extends LoggerBase { throw error } } -} \ No newline at end of file +} From 233f54610d496fe14b34438fa790bf10940d5909 Mon Sep 17 00:00:00 2001 From: Yeganathan S Date: Fri, 25 Apr 2025 16:28:26 +0530 Subject: [PATCH 3/7] refactor: file rename userValidations to memberUserValidations --- services/libs/data-access-layer/src/members/index.ts | 2 +- .../members/{userValidations.ts => memberUserValidations.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename services/libs/data-access-layer/src/members/{userValidations.ts => memberUserValidations.ts} (100%) diff --git a/services/libs/data-access-layer/src/members/index.ts b/services/libs/data-access-layer/src/members/index.ts index eb900f4219..3d870f729a 100644 --- a/services/libs/data-access-layer/src/members/index.ts +++ b/services/libs/data-access-layer/src/members/index.ts @@ -6,4 +6,4 @@ export * from './others' export * from './attributes' export * from './dashboard' export * from './contributions' -export * from './userValidations' +export * from './memberUserValidations' diff --git a/services/libs/data-access-layer/src/members/userValidations.ts b/services/libs/data-access-layer/src/members/memberUserValidations.ts similarity index 100% rename from services/libs/data-access-layer/src/members/userValidations.ts rename to services/libs/data-access-layer/src/members/memberUserValidations.ts From 9fe6742f1a61c012e7e8e1e581b0bf000ab3520b Mon Sep 17 00:00:00 2001 From: Yeganathan S Date: Mon, 28 Apr 2025 17:15:26 +0530 Subject: [PATCH 4/7] chore: add timestamp column in db and auto fill --- .../migrations/V1745401102__memberUserValidations.sql | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/backend/src/database/migrations/V1745401102__memberUserValidations.sql b/backend/src/database/migrations/V1745401102__memberUserValidations.sql index 22d3bd6367..5443b60a2c 100644 --- a/backend/src/database/migrations/V1745401102__memberUserValidations.sql +++ b/backend/src/database/migrations/V1745401102__memberUserValidations.sql @@ -3,11 +3,6 @@ create table "memberUserValidations"( "memberId" uuid not null references members(id), action varchar(255) not null, type varchar(255) not null, - details jsonb not null - -- todo: ask joana if we need timestamp or can reuse createdAt/updatedAt - -- createdAt timestamp with time zone default now(), - -- updatedAt timestamp with time zone default now(), - -- deletedAt timestamp with time zone + details jsonb not null, + timestamp timestamp with time zone default now() ); - - From 07febeac901dbb9cd38b52643d99be5ac7fc864e Mon Sep 17 00:00:00 2001 From: Yeganathan S Date: Mon, 28 Apr 2025 17:19:17 +0530 Subject: [PATCH 5/7] refactor: rename timestamp to createdAt --- .../database/migrations/V1745401102__memberUserValidations.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/database/migrations/V1745401102__memberUserValidations.sql b/backend/src/database/migrations/V1745401102__memberUserValidations.sql index 5443b60a2c..80e6e7faba 100644 --- a/backend/src/database/migrations/V1745401102__memberUserValidations.sql +++ b/backend/src/database/migrations/V1745401102__memberUserValidations.sql @@ -4,5 +4,5 @@ create table "memberUserValidations"( action varchar(255) not null, type varchar(255) not null, details jsonb not null, - timestamp timestamp with time zone default now() + "createdAt" timestamp with time zone default now() ); From 3b3e3bf82d2205ea7352b99c6dee507cdf7b7a39 Mon Sep 17 00:00:00 2001 From: Yeganathan S Date: Mon, 28 Apr 2025 17:19:44 +0530 Subject: [PATCH 6/7] fix(commitlint): use CommonJS syntax in config --- commitlint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitlint.config.js b/commitlint.config.js index 3f5e287f9e..3d88028984 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1 +1 @@ -export default { extends: ['@commitlint/config-conventional'] }; +module.exports = { extends: ['@commitlint/config-conventional'] }; \ No newline at end of file From 2760286265e2107d1006c9fbfeeb9e4206a7d402 Mon Sep 17 00:00:00 2001 From: Yeganathan S Date: Mon, 28 Apr 2025 21:07:45 +0530 Subject: [PATCH 7/7] feat(database): add updatedAt column to memberUserValidations table --- .../database/migrations/V1745401102__memberUserValidations.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/database/migrations/V1745401102__memberUserValidations.sql b/backend/src/database/migrations/V1745401102__memberUserValidations.sql index 80e6e7faba..093a106da4 100644 --- a/backend/src/database/migrations/V1745401102__memberUserValidations.sql +++ b/backend/src/database/migrations/V1745401102__memberUserValidations.sql @@ -4,5 +4,6 @@ create table "memberUserValidations"( action varchar(255) not null, type varchar(255) not null, details jsonb not null, - "createdAt" timestamp with time zone default now() + "createdAt" timestamp with time zone default now(), + "updatedAt" timestamp with time zone default now() );