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
3 changes: 3 additions & 0 deletions docs/copilot-opportunities.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
used by the Opportunities experience. It remains public and returns the legacy
bare response array. Filtering, total count, stable sorting, offset, and limit
are performed in PostgreSQL before the selected page's relations are loaded.
Search and skill predicates normalize legacy `json` and current `jsonb`
request-data columns to JSONB at query time, so mixed deployment histories use
the same filtering contract.

## Query parameters

Expand Down
24 changes: 24 additions & 0 deletions src/api/copilot/copilot-opportunity.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,30 @@ describe('CopilotOpportunityService', () => {
);
});

it('casts legacy JSON request data before search and skill JSONB operations', async () => {
prismaMock.copilotOpportunity.findMany.mockResolvedValue([baseOpportunity]);

await service.listOpportunities(
{
search: 'Cadence SKILL',
skills: ['Cadence SKILL'],
},
regularUser,
);

const sql = prismaMock.$queryRaw.mock.calls[0][0] as Prisma.Sql;
expect(sql.text).toContain(
"LOWER(COALESCE(r.data::jsonb -> 'skills', '[]'::jsonb)::text)",
);
expect(sql.text).toContain(
"WHEN jsonb_typeof(r.data::jsonb -> 'skills') = 'array'",
);
expect(sql.text).toContain("THEN r.data::jsonb -> 'skills'");
expect(sql.values).toEqual(
expect.arrayContaining(['%cadence skill%', 'cadence skill']),
);
});

it('requires authentication for current-user application filters', async () => {
await expect(
service.listOpportunities({ applied: true }, undefined),
Expand Down
8 changes: 5 additions & 3 deletions src/api/copilot/copilot-opportunity.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,12 +902,14 @@ export class CopilotOpportunityService {

if (filters.search) {
const pattern = `%${this.escapeLikePattern(filters.search.toLowerCase())}%`;
// Some upgraded v5 databases retain this column as `json`; cast before
// the JSONB-only skill operations so both physical types are supported.
conditions.push(Prisma.sql`(
LOWER(COALESCE(r.data ->> 'opportunityTitle', '')) LIKE ${pattern} ESCAPE E'\\\\'
OR LOWER(COALESCE(r.data ->> 'overview', '')) LIKE ${pattern} ESCAPE E'\\\\'
OR LOWER(COALESCE(p.name, '')) LIKE ${pattern} ESCAPE E'\\\\'
OR LOWER(o.type::text) LIKE ${pattern} ESCAPE E'\\\\'
OR LOWER((COALESCE(r.data -> 'skills', '[]'::jsonb))::text) LIKE ${pattern} ESCAPE E'\\\\'
OR LOWER(COALESCE(r.data::jsonb -> 'skills', '[]'::jsonb)::text) LIKE ${pattern} ESCAPE E'\\\\'
)`);
}

Expand All @@ -918,8 +920,8 @@ export class CopilotOpportunityService {
SELECT 1
FROM jsonb_array_elements(
CASE
WHEN jsonb_typeof(r.data -> 'skills') = 'array'
THEN r.data -> 'skills'
WHEN jsonb_typeof(r.data::jsonb -> 'skills') = 'array'
THEN r.data::jsonb -> 'skills'
ELSE '[]'::jsonb
END
) AS requested_skill(value)
Expand Down
Loading