Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions services/libs/data-access-layer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from './segments'
export * from './systemSettings'
export * from './connectors'
export * from './integrations'
export * from './octolens'
export * from './auditLogs'
export * from './maintainers'
export * from './packages'
Expand Down
149 changes: 149 additions & 0 deletions services/libs/data-access-layer/src/octolens/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { test as base, describe, expect } from 'vitest'

import { DEFAULT_TENANT_ID, generateUUIDv1 } from '@crowd/common'
import type { QueryExecutor } from '@crowd/database'
import { withQx } from '@crowd/test-kit/db'

import {
addOctolensKeywordMapping,
findOctolensKeywordMapping,
listOctolensKeywordMappings,
removeOctolensKeywordMapping,
} from './index'

const test = withQx(base)

async function createIntegration(qx: QueryExecutor): Promise<string> {
const id = generateUUIDv1()
await qx.result(
`
INSERT INTO public.integrations (id, platform, status, "tenantId", "createdAt", "updatedAt", "deletedAt")
VALUES ($(id), 'octolens', 'done', $(tenantId), NOW(), NOW(), NULL)
`,
{ id, tenantId: DEFAULT_TENANT_ID },
)
return id
}

async function createSegment(qx: QueryExecutor): Promise<string> {
const id = generateUUIDv1()
const slug = `segment-${id}`
await qx.result(
`
INSERT INTO public.segments (id, slug, name, "tenantId", "createdAt", "updatedAt")
VALUES ($(id), $(slug), $(slug), $(tenantId), NOW(), NOW())
`,
{ id, slug, tenantId: DEFAULT_TENANT_ID },
)
return id
}

describe('octolens keyword mappings', () => {
test('addOctolensKeywordMapping + findOctolensKeywordMapping', async ({ qx }) => {
const integrationId = await createIntegration(qx)
const segmentId = await createSegment(qx)

const id = await addOctolensKeywordMapping(qx, {
integrationId,
segmentId,
keywordId: 101,
keyword: 'crowd.dev',
})

const mapping = await findOctolensKeywordMapping(qx, integrationId, 101)

expect(mapping).toMatchObject({
id,
integrationId,
segmentId,
keywordId: 101,
keyword: 'crowd.dev',
})
})

test('findOctolensKeywordMapping returns null when no mapping exists', async ({ qx }) => {
const integrationId = await createIntegration(qx)

const mapping = await findOctolensKeywordMapping(qx, integrationId, 999)

expect(mapping).toBeNull()
})

test('listOctolensKeywordMappings returns only mappings for the given integration', async ({
qx,
}) => {
const integrationId = await createIntegration(qx)
const otherIntegrationId = await createIntegration(qx)
const segmentId = await createSegment(qx)

await addOctolensKeywordMapping(qx, {
integrationId,
segmentId,
keywordId: 1,
keyword: 'alpha',
})
await addOctolensKeywordMapping(qx, {
integrationId,
segmentId,
keywordId: 2,
keyword: 'beta',
})
await addOctolensKeywordMapping(qx, {
integrationId: otherIntegrationId,
segmentId,
keywordId: 3,
keyword: 'gamma',
})

const mappings = await listOctolensKeywordMappings(qx, integrationId)

expect(mappings).toHaveLength(2)
expect(mappings.map((m) => m.keyword).sort()).toEqual(['alpha', 'beta'])
})

test('addOctolensKeywordMapping rejects a duplicate (integrationId, keywordId) pair', async ({
qx,
}) => {
const integrationId = await createIntegration(qx)
const segmentId = await createSegment(qx)

await addOctolensKeywordMapping(qx, {
integrationId,
segmentId,
keywordId: 42,
keyword: 'first',
})

await expect(
addOctolensKeywordMapping(qx, {
integrationId,
segmentId,
keywordId: 42,
keyword: 'second',
}),
).rejects.toThrow()
})

test('removeOctolensKeywordMapping deletes the mapping', async ({ qx }) => {
const integrationId = await createIntegration(qx)
const segmentId = await createSegment(qx)

await addOctolensKeywordMapping(qx, {
integrationId,
segmentId,
keywordId: 7,
keyword: 'delete-me',
})

await removeOctolensKeywordMapping(qx, integrationId, 7)

const mapping = await findOctolensKeywordMapping(qx, integrationId, 7)
expect(mapping).toBeNull()
})

test('removeOctolensKeywordMapping is a no-op when no mapping matches', async ({ qx }) => {
const integrationId = await createIntegration(qx)

await expect(removeOctolensKeywordMapping(qx, integrationId, 404)).resolves.toBeUndefined()
})
})
76 changes: 76 additions & 0 deletions services/libs/data-access-layer/src/octolens/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { QueryExecutor } from '../queryExecutor'

export interface IOctolensKeywordMapping {
id: string
integrationId: string
segmentId: string
keywordId: number
keyword: string
}

export async function findOctolensKeywordMapping(
qx: QueryExecutor,
integrationId: string,
keywordId: number,
): Promise<IOctolensKeywordMapping | null> {
return qx.selectOneOrNone(
`
SELECT id, "integrationId", "segmentId", "keywordId", keyword
FROM integration."octolensKeywordMappings"
WHERE "integrationId" = $(integrationId) AND "keywordId" = $(keywordId)
`,
{ integrationId, keywordId },
)
}

export async function listOctolensKeywordMappings(
qx: QueryExecutor,
integrationId: string,
): Promise<IOctolensKeywordMapping[]> {
return qx.select(
`
SELECT id, "integrationId", "segmentId", "keywordId", keyword
FROM integration."octolensKeywordMappings"
WHERE "integrationId" = $(integrationId)
ORDER BY keyword
`,
{ integrationId },
)
}

export interface IAddOctolensKeywordMapping {
integrationId: string
segmentId: string
keywordId: number
keyword: string
}

export async function addOctolensKeywordMapping(
qx: QueryExecutor,
data: IAddOctolensKeywordMapping,
): Promise<string> {
const result = await qx.selectOne(
`
INSERT INTO integration."octolensKeywordMappings" ("integrationId", "segmentId", "keywordId", keyword)
VALUES ($(integrationId), $(segmentId), $(keywordId), $(keyword))
RETURNING id
`,
data,
)

return result.id
}

export async function removeOctolensKeywordMapping(
qx: QueryExecutor,
integrationId: string,
keywordId: number,
): Promise<void> {
await qx.result(
`
DELETE FROM integration."octolensKeywordMappings"
WHERE "integrationId" = $(integrationId) AND "keywordId" = $(keywordId)
`,
{ integrationId, keywordId },
)
}
Loading