Skip to content

Commit 1e8f370

Browse files
committed
Merge branch 'main' into better-error-message-handling-CM-1117
2 parents 29bcdda + 18ae414 commit 1e8f370

14 files changed

Lines changed: 240 additions & 39 deletions

File tree

backend/src/api/public/v1/members/work-experiences/createMemberWorkExperience.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import type { Request, Response } from 'express'
22
import { z } from 'zod'
33

44
import { captureApiChange, memberEditOrganizationsAction } from '@crowd/audit-logs'
5-
import { ConflictError, NotFoundError } from '@crowd/common'
5+
import {
6+
BadRequestError,
7+
ConflictError,
8+
NotFoundError,
9+
sanitizeMemberOrganizationDateRange,
10+
} from '@crowd/common'
611
import { CommonMemberService } from '@crowd/common_services'
712
import {
813
MemberField,
@@ -14,7 +19,11 @@ import {
1419
findMemberById,
1520
optionsQx,
1621
} from '@crowd/data-access-layer'
17-
import type { IMemberOrganization, IMemberRoleWithOrganization } from '@crowd/types'
22+
import type {
23+
IMemberOrganization,
24+
IMemberRoleWithOrganization,
25+
MemberOrganizationDateRange,
26+
} from '@crowd/types'
1827

1928
import { created } from '@/utils/api'
2029
import { toMemberWorkExperience } from '@/utils/mapper'
@@ -53,12 +62,20 @@ export async function createMemberWorkExperience(req: Request, res: Response): P
5362
memberEditOrganizationsAction(memberId, async (captureOldState, captureNewState) => {
5463
captureOldState({})
5564

65+
let dates: MemberOrganizationDateRange
66+
67+
try {
68+
dates = sanitizeMemberOrganizationDateRange(data.startDate, data.endDate, true)
69+
} catch (error) {
70+
throw new BadRequestError('Invalid work experience date range')
71+
}
72+
5673
const memberOrgData: IMemberOrganization = {
5774
memberId,
5875
organizationId: data.organizationId,
5976
title: data.jobTitle,
60-
dateStart: data.startDate,
61-
dateEnd: data.endDate,
77+
dateStart: dates.dateStart,
78+
dateEnd: dates.dateEnd,
6279
source: data.source,
6380
verified: data.verified,
6481
verifiedBy: data.verifiedBy,
@@ -67,7 +84,7 @@ export async function createMemberWorkExperience(req: Request, res: Response): P
6784
let newMemberOrgId: string | undefined
6885

6986
await qx.tx(async (tx) => {
70-
await cleanSoftDeletedMemberOrganization(tx, memberId, data.organizationId, data)
87+
await cleanSoftDeletedMemberOrganization(tx, memberId, data.organizationId, memberOrgData)
7188

7289
newMemberOrgId = await createMemberOrganization(tx, memberId, memberOrgData)
7390

backend/src/api/public/v1/members/work-experiences/updateMemberWorkExperience.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Request, Response } from 'express'
22
import { z } from 'zod'
33

44
import { captureApiChange, memberEditOrganizationsAction } from '@crowd/audit-logs'
5-
import { NotFoundError } from '@crowd/common'
5+
import { BadRequestError, NotFoundError, sanitizeMemberOrganizationDateRange } from '@crowd/common'
66
import { CommonMemberService } from '@crowd/common_services'
77
import {
88
MemberField,
@@ -12,7 +12,7 @@ import {
1212
optionsQx,
1313
updateMemberOrganization,
1414
} from '@crowd/data-access-layer'
15-
import type { MemberOrganizationUpdate } from '@crowd/types'
15+
import type { MemberOrganizationDateRange, MemberOrganizationUpdate } from '@crowd/types'
1616

1717
import { ok } from '@/utils/api'
1818
import { toMemberWorkExperience } from '@/utils/mapper'
@@ -52,14 +52,22 @@ export async function updateMemberWorkExperience(req: Request, res: Response): P
5252
throw new NotFoundError('Work experience not found')
5353
}
5454

55+
let dates: MemberOrganizationDateRange
56+
57+
try {
58+
dates = sanitizeMemberOrganizationDateRange(data.startDate, data.endDate, true)
59+
} catch (error) {
60+
throw new BadRequestError('Invalid work experience date range')
61+
}
62+
5563
const update: MemberOrganizationUpdate = {
5664
organizationId: data.organizationId,
5765
title: data.jobTitle,
5866
verified: data.verified,
5967
verifiedBy: data.verifiedBy,
6068
source: data.source,
61-
dateStart: data.startDate,
62-
dateEnd: data.endDate,
69+
dateStart: dates.dateStart,
70+
dateEnd: dates.dateEnd,
6371
}
6472

6573
let updated: ReturnType<typeof toMemberWorkExperience> | undefined

backend/src/database/repositories/organizationRepository.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,7 @@ class OrganizationRepository {
13161316
limit = 20,
13171317
offset = 0,
13181318
orderBy = undefined,
1319+
search = undefined as string | undefined,
13191320
segmentId = undefined,
13201321
},
13211322
options: IRepositoryOptions,
@@ -1332,6 +1333,7 @@ class OrganizationRepository {
13321333
limit,
13331334
offset,
13341335
orderBy,
1336+
search,
13351337
segmentId,
13361338
})
13371339

@@ -1345,6 +1347,7 @@ class OrganizationRepository {
13451347
cacheKey,
13461348
{
13471349
filter,
1350+
search,
13481351
limit,
13491352
offset,
13501353
orderBy,
@@ -1366,6 +1369,7 @@ class OrganizationRepository {
13661369
cacheKey,
13671370
{
13681371
filter,
1372+
search,
13691373
segmentId,
13701374
include,
13711375
},
@@ -1386,6 +1390,7 @@ class OrganizationRepository {
13861390
cacheKey,
13871391
{
13881392
filter,
1393+
search,
13891394
limit,
13901395
offset,
13911396
orderBy,
@@ -1403,6 +1408,7 @@ class OrganizationRepository {
14031408
cacheKey: string,
14041409
{
14051410
filter = {} as any,
1411+
search = undefined as string | undefined,
14061412
limit = 20,
14071413
offset = 0,
14081414
orderBy = undefined,
@@ -1458,13 +1464,19 @@ class OrganizationRepository {
14581464
segmentId = segment.id
14591465
}
14601466

1461-
const params = {
1467+
const params: Record<string, any> = {
14621468
limit,
14631469
offset,
14641470
segmentId,
14651471
tenantId: options.currentTenant.id,
14661472
}
14671473

1474+
let searchWhereClause = ''
1475+
if (search) {
1476+
params.searchTerm = `%${search}%`
1477+
searchWhereClause = `AND o."displayName" ILIKE $(searchTerm)`
1478+
}
1479+
14681480
const filterString = RawQueryParser.parseFilters(
14691481
filter,
14701482
OrganizationRepository.QUERY_FILTER_COLUMN_MAP,
@@ -1498,6 +1510,7 @@ class OrganizationRepository {
14981510
WHERE 1=1
14991511
AND o."tenantId" = $(tenantId)
15001512
${lfxMembershipFilterWhereClause}
1513+
${searchWhereClause}
15011514
AND (${filterString})
15021515
`
15031516
const countQuery = createQuery('COUNT(*)')
@@ -1649,6 +1662,7 @@ class OrganizationRepository {
16491662
params: {
16501663
// TODO: REMOVE this any
16511664
filter?: any
1665+
search?: string
16521666
limit: number
16531667
offset: number
16541668
orderBy?: string
@@ -1671,6 +1685,7 @@ class OrganizationRepository {
16711685
cacheKey: string,
16721686
params: {
16731687
filter?: any
1688+
search?: string
16741689
segmentId?: string
16751690
include: any
16761691
},

backend/src/database/repositories/organizationsQueryCache.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class OrganizationQueryCache {
3838
limit: number
3939
offset: number
4040
orderBy?: string
41+
search?: string
4142
segmentId?: string
4243
}): string {
4344
const cleanParams = Object.fromEntries(
@@ -49,6 +50,7 @@ export class OrganizationQueryCache {
4950
limit: params.limit,
5051
offset: params.offset,
5152
orderBy: params.orderBy,
53+
search: params.search,
5254
segmentId: params.segmentId,
5355
}).filter(([, value]) => value !== null && value !== undefined),
5456
)

backend/src/services/member/memberOrganizationsService.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/* eslint-disable no-continue */
2+
import lodash from 'lodash'
23
import { Transaction } from 'sequelize'
34

4-
import { Error404 } from '@crowd/common'
5+
import { Error404, sanitizeMemberOrganizationDateRange } from '@crowd/common'
56
import { CommonMemberService } from '@crowd/common_services'
67
import {
78
OrganizationField,
@@ -10,6 +11,7 @@ import {
1011
cleanSoftDeletedMemberOrganization,
1112
createMemberOrganization,
1213
deleteMemberOrganizations,
14+
fetchMemberOrganizationById,
1315
fetchMemberOrganizations,
1416
findMemberAffiliationOverrides,
1517
optionsQx,
@@ -167,12 +169,18 @@ export default class MemberOrganizationsService extends LoggerBase {
167169

168170
try {
169171
const qx = SequelizeRepository.getQueryExecutor(repositoryOptions)
172+
const dates = sanitizeMemberOrganizationDateRange(data.dateStart, data.dateEnd, true)
173+
const memberOrgData: Partial<IMemberOrganization> = {
174+
...data,
175+
dateStart: dates.dateStart,
176+
dateEnd: dates.dateEnd,
177+
}
170178

171179
// Clean up any soft-deleted entries
172-
await cleanSoftDeletedMemberOrganization(qx, memberId, data.organizationId, data)
180+
await cleanSoftDeletedMemberOrganization(qx, memberId, data.organizationId, memberOrgData)
173181

174182
// Create new member organization
175-
const newMemberOrgId = await createMemberOrganization(qx, memberId, data)
183+
const newMemberOrgId = await createMemberOrganization(qx, memberId, memberOrgData)
176184

177185
// Check if organization affiliation is blocked
178186
const isAffiliationBlocked = await checkOrganizationAffiliationPolicy(qx, data.organizationId)
@@ -214,26 +222,44 @@ export default class MemberOrganizationsService extends LoggerBase {
214222
try {
215223
const qx = SequelizeRepository.getQueryExecutor(repositoryOptions)
216224

217-
const update: MemberOrganizationUpdate = Object.fromEntries(
218-
Object.entries({
225+
const existing = await fetchMemberOrganizationById(qx, id)
226+
if (!existing || existing.memberId !== memberId) {
227+
throw new Error404(`Member organization with id ${id} not found!`)
228+
}
229+
230+
const hasDateStart = data.dateStart !== undefined
231+
const hasDateEnd = data.dateEnd !== undefined
232+
const targetDateRange = sanitizeMemberOrganizationDateRange(
233+
hasDateStart ? data.dateStart : existing.dateStart,
234+
hasDateEnd ? data.dateEnd : existing.dateEnd,
235+
true,
236+
)
237+
238+
const update = lodash.pickBy(
239+
{
219240
organizationId: data.organizationId,
220241
title: data.title,
221-
dateStart: data.dateStart,
222-
dateEnd: data.dateEnd,
242+
dateStart: hasDateStart ? targetDateRange.dateStart : undefined,
243+
dateEnd: hasDateEnd ? targetDateRange.dateEnd : undefined,
244+
223245
verified: data.verified,
224246
verifiedBy: data.verifiedBy,
225-
}).filter(([, v]) => v !== undefined),
226-
)
247+
},
248+
(v) => v !== undefined,
249+
) as MemberOrganizationUpdate
227250

228-
await cleanSoftDeletedMemberOrganization(qx, memberId, data.organizationId, data)
229-
// Any manual edit from the frontend promotes ownership to UI so automated
230-
// sources (e.g. email-domain inference) no longer overwrite user intent.
251+
await cleanSoftDeletedMemberOrganization(qx, memberId, data.organizationId, update)
231252
await updateMemberOrganization(qx, memberId, id, {
232253
...update,
233254
source: OrganizationSource.UI,
234255
})
235256

236-
await this.commonMemberService.startAffiliationRecalculation(memberId, [data.organizationId])
257+
// Trigger recalculation for old and new orgs if changed
258+
const orgsToRecalculate = Array.from(
259+
new Set([existing.organizationId, data.organizationId]),
260+
).filter((orgId): orgId is string => Boolean(orgId))
261+
262+
await this.commonMemberService.startAffiliationRecalculation(memberId, orgsToRecalculate)
237263

238264
const result = await this.list(memberId, transaction)
239265

backend/src/services/organizationService.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1163,10 +1163,19 @@ export default class OrganizationService extends LoggerBase {
11631163
}
11641164

11651165
async query(data) {
1166-
const { filter, orderBy, limit, offset, segments } = data
1166+
const { filter: rawFilter, orderBy, limit, offset, segments, search: rawSearch } = data
1167+
const searchTerm =
1168+
typeof rawSearch === 'string' && rawSearch.trim() ? rawSearch.trim() : undefined
1169+
1170+
// Strip frontend-state keys that are never valid filter columns or operators.
1171+
// These can appear when the raw Pinia filter state is sent instead of the
1172+
// processed output of buildApiFilter.
1173+
const { search: _s, relation: _r, order: _o, settings: _st, ...filter } = rawFilter ?? {}
1174+
11671175
return OrganizationRepository.findAndCountAll(
11681176
{
11691177
filter,
1178+
search: searchTerm,
11701179
orderBy,
11711180
limit,
11721181
offset,

services/apps/cron_service/src/jobs/inferMemberOrganizationStintChanges.job.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
changeMemberOrganizationAffiliationOverrides,
1111
checkOrganizationAffiliationPolicy,
1212
createMemberOrganization,
13+
deleteUndatedMemberOrganizations,
1314
fetchMemberOrganizationsBySource,
1415
updateMemberOrganization,
1516
} from '@crowd/data-access-layer'
@@ -136,6 +137,10 @@ async function applyStintChanges(qx: QueryExecutor, changes: MemberOrgStintChang
136137
})
137138
}
138139
}
140+
141+
await deleteUndatedMemberOrganizations(qx, changes[0].memberId, [
142+
...new Set(changes.map((c) => c.organizationId)),
143+
])
139144
}
140145

141146
export default job

services/apps/members_enrichment_worker/src/activities/enrichment.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
generateUUIDv1,
77
hasIntersection,
88
replaceDoubleQuotes,
9+
sanitizeMemberOrganizationDateRange,
910
setAttributesDefaultValues,
1011
} from '@crowd/common'
1112
import { CommonMemberService } from '@crowd/common_services'
@@ -383,6 +384,10 @@ export async function updateMemberUsingSquashedPayload(
383384

384385
const newOrUpdatedMemberOrgs = []
385386

387+
squashedPayload.memberOrganizations = sanitizeWorkExperienceDateRanges(
388+
squashedPayload.memberOrganizations,
389+
)
390+
386391
if (squashedPayload.memberOrganizations.length > 0) {
387392
const orgPromises = []
388393

@@ -754,6 +759,20 @@ interface IWorkExperienceChanges {
754759
toUpdate: Map<IMemberOrganizationData, Record<string, any>>
755760
}
756761

762+
function sanitizeWorkExperienceDateRanges(
763+
organizations: IMemberEnrichmentDataNormalizedOrganization[],
764+
): IMemberEnrichmentDataNormalizedOrganization[] {
765+
return organizations.map((org) => {
766+
const dates = sanitizeMemberOrganizationDateRange(org.startDate, org.endDate)
767+
768+
return {
769+
...org,
770+
startDate: dates.dateStart instanceof Date ? dates.dateStart.toISOString() : dates.dateStart,
771+
endDate: dates.dateEnd instanceof Date ? dates.dateEnd.toISOString() : dates.dateEnd,
772+
}
773+
})
774+
}
775+
757776
function prepareWorkExperiences(
758777
oldVersion: IMemberOrganizationData[],
759778
newVersion: IMemberEnrichmentDataNormalizedOrganization[],

services/apps/members_enrichment_worker/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ export interface IMemberEnrichmentDataNormalizedOrganization {
9393
identities?: IOrganizationIdentity[]
9494
title?: string
9595
organizationDescription?: string
96-
startDate?: string
97-
endDate?: string
96+
startDate?: string | null
97+
endDate?: string | null
9898
source: OrganizationSource
9999
}
100100

0 commit comments

Comments
 (0)