Skip to content

Commit 0e5c7dc

Browse files
committed
feat: expose organization domains in public APIs
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
1 parent 827c956 commit 0e5c7dc

11 files changed

Lines changed: 195 additions & 67 deletions

File tree

backend/src/api/public/openapi.yaml

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -779,8 +779,11 @@ paths:
779779
/organizations:
780780
get:
781781
operationId: getOrganization
782-
summary: Look up an organization by domain
783-
description: Find a verified organization by its primary domain.
782+
summary: Look up an organization by domain or name
783+
description: >
784+
Provide domain, name, or both. When both are provided, the domain and
785+
name must belong to the same organization. If multiple organizations
786+
match, the most active one is returned.
784787
tags:
785788
- Organizations
786789
security:
@@ -789,12 +792,20 @@ paths:
789792
parameters:
790793
- name: domain
791794
in: query
792-
required: true
795+
required: false
793796
description: Primary domain of the organization.
794797
schema:
795798
type: string
796799
minLength: 1
797800
example: linuxfoundation.org
801+
- name: name
802+
in: query
803+
required: false
804+
description: Exact display name of the organization.
805+
schema:
806+
type: string
807+
minLength: 1
808+
example: Linux Foundation
798809
responses:
799810
'200':
800811
description: Organization found.
@@ -804,14 +815,15 @@ paths:
804815
$ref: '#/components/schemas/Organization'
805816
example:
806817
id: 550e8400-e29b-41d4-a716-446655440000
807-
name: Linux Foundation
818+
displayName: Linux Foundation
819+
domain: linuxfoundation.org
808820
logo: https://example.com/logo.png
809821
'401':
810822
$ref: '#/components/responses/Unauthorized'
811823
'403':
812824
$ref: '#/components/responses/Forbidden'
813825
'404':
814-
description: No verified organization found for the given domain.
826+
description: No organization found for the given domain or name.
815827
content:
816828
application/json:
817829
schema:
@@ -876,15 +888,26 @@ paths:
876888
required:
877889
- id
878890
- name
891+
- domain
879892
properties:
880893
id:
881894
type: string
882895
format: uuid
883896
name:
884897
type: string
898+
domain:
899+
type: string
900+
description: Verified primary domain of the organization.
901+
logo:
902+
type:
903+
- string
904+
- 'null'
905+
description: URL of the organization logo.
885906
example:
886907
id: 550e8400-e29b-41d4-a716-446655440000
887908
name: Acme Corp
909+
domain: acme.com
910+
logo: https://example.com/logo.png
888911
'400':
889912
$ref: '#/components/responses/BadRequest'
890913
'401':
@@ -1218,6 +1241,7 @@ components:
12181241
- id
12191242
- organizationId
12201243
- organizationName
1244+
- organizationDomains
12211245
- jobTitle
12221246
- verified
12231247
- verifiedBy
@@ -1243,6 +1267,11 @@ components:
12431267
- string
12441268
- 'null'
12451269
description: URL of the organization logo.
1270+
organizationDomains:
1271+
type: array
1272+
items:
1273+
type: string
1274+
description: Verified primary domains for the organization, in alphabetical order.
12461275
jobTitle:
12471276
type:
12481277
- string
@@ -1530,14 +1559,18 @@ components:
15301559
type: object
15311560
required:
15321561
- id
1533-
- name
1562+
- displayName
1563+
- domain
15341564
properties:
15351565
id:
15361566
type: string
15371567
format: uuid
1538-
name:
1568+
displayName:
15391569
type: string
15401570
description: Display name of the organization.
1571+
domain:
1572+
type: string
1573+
description: Verified primary domain.
15411574
logo:
15421575
type: string
15431576
description: URL of the organization logo. Only present if available.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export async function createMemberWorkExperience(req: Request, res: Response): P
117117
memberOrganizationIds: [data.organizationId],
118118
})
119119

120-
const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId])
120+
const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId], { withDomains: true })
121121
createdMo = (orgsMap.get(memberId) ?? []).find((mo) => mo.id === newMemberOrgId)
122122

123123
captureNewState(createdMo ?? null)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export async function getMemberWorkExperiences(req: Request, res: Response): Pro
2727
throw new NotFoundError('Member not found')
2828
}
2929

30-
const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId])
30+
const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId], { withDomains: true })
3131
const workExperiences = groupMemberOrganizations(orgsMap.get(memberId) ?? []).map(
3232
toMemberWorkExperience,
3333
)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export async function updateMemberWorkExperience(req: Request, res: Response): P
120120
memberOrganizationIds: [data.organizationId],
121121
})
122122

123-
const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId])
123+
const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId], { withDomains: true })
124124

125125
const updatedMo = groupMemberOrganizations(orgsMap.get(memberId) ?? []).find(
126126
(mo) => mo.id === workExperienceId,

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,19 @@ export async function verifyMemberWorkExperience(req: Request, res: Response): P
100100
}),
101101
)
102102

103-
const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId])
103+
const orgsMap = await fetchManyMemberOrgsWithOrgData(qx, [memberId], { withDomains: true })
104104

105105
const responseMo: IMemberRoleWithOrganization =
106106
groupMemberOrganizations(orgsMap.get(memberId) ?? []).find(
107107
(mo) => mo.id === workExperienceId,
108108
) ??
109-
({ ...memberOrg, ...updatedMemberOrg, verified, verifiedBy } as IMemberRoleWithOrganization)
109+
({
110+
...memberOrg,
111+
...updatedMemberOrg,
112+
verified,
113+
verifiedBy,
114+
organizationDomains: [],
115+
} as IMemberRoleWithOrganization)
110116

111117
ok(res, toMemberWorkExperience(responseMo))
112118
}

backend/src/api/public/v1/organizations/createOrganization.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,5 @@ export async function createOrganization(req: Request, res: Response): Promise<v
6161
)
6262
})
6363

64-
created(res, { id: organizationId, name, logo })
64+
created(res, { id: organizationId, name, logo, domain })
6565
}

backend/src/api/public/v1/organizations/getOrganization.ts

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

44
import { NotFoundError, normalizeHostname } from '@crowd/common'
5-
import {
6-
OrgIdentityField,
7-
OrganizationField,
8-
findOrgAttributes,
9-
findOrgById,
10-
optionsQx,
11-
queryOrgIdentities,
12-
} from '@crowd/data-access-layer'
13-
import { OrganizationIdentityType } from '@crowd/types'
5+
import { findOrganizationByNameOrDomain, optionsQx } from '@crowd/data-access-layer'
146

157
import { ok } from '@/utils/api'
168
import { validateOrThrow } from '@/utils/validation'
179

18-
const querySchema = z.object({
19-
domain: z.string().trim().min(1),
20-
})
10+
const querySchema = z
11+
.object({
12+
name: z.string().trim().min(1).optional(),
13+
domain: z.string().trim().min(1).optional(),
14+
})
15+
.refine((data) => data.name || data.domain, {
16+
message: 'Either name or domain must be provided',
17+
})
2118

2219
export async function getOrganization(req: Request, res: Response): Promise<void> {
23-
const { domain } = validateOrThrow(querySchema, req.query)
24-
20+
const { name, domain } = validateOrThrow(querySchema, req.query)
2521
const qx = optionsQx(req)
2622

27-
const results = await queryOrgIdentities(qx, {
28-
fields: [OrgIdentityField.ORGANIZATION_ID],
29-
filter: {
30-
and: [
31-
{ value: { eq: normalizeHostname(domain, false) } },
32-
{ type: { eq: OrganizationIdentityType.PRIMARY_DOMAIN } },
33-
{ verified: { eq: true } },
34-
],
35-
},
23+
const organization = await findOrganizationByNameOrDomain(qx, {
24+
name,
25+
domain: domain ? normalizeHostname(domain, false) : undefined,
3626
})
3727

38-
const organizationId = results[0]?.organizationId
39-
40-
if (!organizationId) {
28+
if (!organization) {
4129
throw new NotFoundError('Organization not found')
4230
}
4331

44-
const org = await findOrgById(qx, organizationId, [
45-
OrganizationField.ID,
46-
OrganizationField.DISPLAY_NAME,
47-
])
48-
49-
const attributes = await findOrgAttributes(qx, organizationId)
50-
const logo = attributes.find((a) => a.name === 'logo')?.value
51-
52-
ok(res, {
53-
id: org.id,
54-
name: org.displayName,
55-
...(logo ? { logo } : {}),
56-
})
32+
ok(res, organization)
5733
}

backend/src/utils/mapper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export function toMemberWorkExperience(mo: IMemberRoleWithOrganization) {
182182
organizationId: mo.organizationId,
183183
organizationName: mo.organizationName,
184184
organizationLogo: mo.organizationLogo,
185+
organizationDomains: mo.organizationDomains ?? [],
185186
jobTitle: mo.title ?? null,
186187
verified: mo.verified ?? false,
187188
verifiedBy: mo.verifiedBy ?? null,

services/libs/data-access-layer/src/members/organizations.ts

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -213,27 +213,60 @@ export async function fetchManyMemberOrgs(
213213
export async function fetchManyMemberOrgsWithOrgData(
214214
qx: QueryExecutor,
215215
memberIds: string[],
216+
{ withDomains = false }: { withDomains?: boolean } = {},
216217
): Promise<Map<string, IMemberRoleWithOrganization[]>> {
217-
const memberRoles = (await qx.select(
218-
`
219-
SELECT mo.*, o."displayName" as "organizationName", o.logo as "organizationLogo"
220-
FROM "memberOrganizations" mo
221-
join "organizations" o on mo."organizationId" = o.id
222-
WHERE mo."memberId" in ($(memberIds:csv))
218+
const domainSelect = withDomains
219+
? `,
220+
COALESCE(oid.domains, '{}') AS "organizationDomains"`
221+
: ''
222+
223+
const domainJoin = withDomains
224+
? `
225+
LEFT JOIN (
226+
SELECT
227+
oi."organizationId",
228+
array_agg(DISTINCT lower(oi.value) ORDER BY lower(oi.value)) AS domains
229+
FROM "organizationIdentities" oi
230+
WHERE oi.type = 'primary-domain'
231+
AND oi.verified = true
232+
AND oi."organizationId" IN (
233+
SELECT DISTINCT mo2."organizationId"
234+
FROM "memberOrganizations" mo2
235+
WHERE mo2."memberId" IN ($(memberIds:csv))
236+
AND mo2."deletedAt" IS NULL
237+
)
238+
GROUP BY oi."organizationId"
239+
) oid ON oid."organizationId" = mo."organizationId"`
240+
: ''
241+
242+
const sql = `
243+
SELECT
244+
mo.*,
245+
o."displayName" AS "organizationName",
246+
o.logo AS "organizationLogo"
247+
${domainSelect}
248+
FROM "memberOrganizations" mo
249+
JOIN organizations o ON o.id = mo."organizationId"
250+
${domainJoin}
251+
WHERE mo."memberId" IN ($(memberIds:csv))
223252
AND mo."deletedAt" IS NULL;
224-
`,
225-
{
226-
memberIds,
227-
},
228-
)) as IMemberRoleWithOrganization[]
253+
`
254+
255+
const memberRoles = (await qx.select(sql, {
256+
memberIds,
257+
})) as IMemberRoleWithOrganization[]
258+
259+
const result = new Map<string, IMemberRoleWithOrganization[]>()
229260

230-
const resultMap = new Map<string, IMemberRoleWithOrganization[]>()
231261
for (const memberId of memberIds) {
232-
const roles = memberRoles.filter((r) => r.memberId === memberId)
233-
resultMap.set(memberId, roles)
262+
result.set(memberId, [])
234263
}
235264

236-
return resultMap
265+
for (const role of memberRoles) {
266+
result.get(role.memberId)!.push(role)
267+
}
268+
269+
return result
237270
}
238271

239272
export async function fetchManyOrganizationAffiliationPolicies(

0 commit comments

Comments
 (0)