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 Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ RUN apk upgrade --no-cache \

COPY package.json pnpm-lock.yaml pnpm-workspace.yaml nest-cli.json tsconfig.json tsconfig.build.json \
eslint.config.mjs .prettierrc ./
COPY packages/projects-prisma-client/package.json ./packages/projects-prisma-client/package.json
COPY patches ./patches
COPY prisma ./prisma
RUN pnpm install --frozen-lockfile
Expand Down
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,33 @@ Talent Manager note:
| `POST` | `/v6/projects/:projectId/copilots/requests` | JWT / M2M | Create copilot request |
| `PATCH` | `/v6/projects/copilots/requests/:copilotRequestId` | JWT / M2M | Update copilot request |
| `POST` | `/v6/projects/:projectId/copilots/requests/:copilotRequestId/approve` | JWT / M2M | Approve request -> creates opportunity |
| `GET` | `/v6/projects/copilots/opportunities` | **Public** | List copilot opportunities |
| `GET` | `/v6/projects/copilot/opportunity/:id` | **Public** | Get opportunity details |
| `GET` | `/v6/projects/copilots/opportunities` | **Public** | Dynamically filter and page copilot opportunities |
| `GET` | `/v6/projects/copilot/opportunity/:id` (alias: `/copilots/opportunity/:id`) | **Public** | Get opportunity details and current-user application state |
| `POST` | `/v6/projects/copilots/opportunity/:id/apply` | JWT | Apply as copilot |
| `GET` | `/v6/projects/copilots/opportunity/:id/applications` | JWT | List applications |
| `POST` | `/v6/projects/copilots/opportunity/:id/assign` | JWT | Assign copilot (triggers member/state transitions) |
| `DELETE` | `/v6/projects/copilots/opportunity/:id/cancel` | JWT | Cancel opportunity (cascade) |

Copilot request management routes accept M2M tokens with project-write authorization such as `write:projects`, `all:projects`, or `all:connect_project`.

The opportunity list supports database-level `search`, `status`, `projectId`,
`projectName`, `type`, `skills`, requested-start/created date ranges, sorting,
and current-user `applied` / `applicationStatus` filters. The existing response
array and pagination headers remain compatible. See
[`docs/copilot-opportunities.md`](docs/copilot-opportunities.md) for the exact
contract, aliases, current-user fields, and examples.

### Projects Prisma client

Cross-service consumers can install the checked-in
`packages/projects-prisma-client` subdirectory as
`@topcoder/projects-api-v6`. It exports all generated Projects Prisma models,
enums, and `PrismaClient`, plus
`createProjectsPrismaClient(connectionString, options?)`, which configures the
required Prisma 7 PostgreSQL adapter. See the package
[`README`](packages/projects-prisma-client/README.md) for installation and
lifecycle usage.

### Metadata

See `docs/api-usage-analysis.md` (P2 section) for the complete metadata list.
Expand Down
8 changes: 8 additions & 0 deletions docs/DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ packages contain Prisma 6.19.x runtimes; each client is connection/query
smoke-tested under Node 26 in addition to the application's Prisma 7 health
check.

This repository also publishes its own generated client as the GitHub
subdirectory package `@topcoder/projects-api-v6` from
`packages/projects-prisma-client`. Unlike the legacy Prisma 6 clients above,
the Projects package is generated and pinned with Prisma 7.9.0. Its exported
`createProjectsPrismaClient(connectionString, options?)` factory supplies the
required `@prisma/adapter-pg` instance for aggregators such as
`opportunities-api-v6`; callers own and disconnect the returned client.

## Other Git dependencies

The remaining Topcoder libraries are pinned to immutable commits:
Expand Down
6 changes: 6 additions & 0 deletions docs/DIFFERENCES_FROM_V5.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ This document summarizes intentional differences and improvements in `project-se
- `/v6/projects/metadata/workManagementPermission?id=:id`
- Invite creation returns `201` when at least one invite is created and keeps partial-success response semantics:
- `{ success: Invite[], failed: ErrorInfo[] }`
- Copilot opportunity discovery keeps the legacy response array and route
aliases while adding database-side search, status/type/project/skills/date
filters, stable pagination, and authenticated-user application state.

## Authorization Improvements

Expand All @@ -42,6 +45,9 @@ This document summarizes intentional differences and improvements in `project-se
- Prisma-based query layer replaced Sequelize.
- Relation includes are explicit, reducing N+1 query patterns.
- Connection behavior is standardized through Prisma pool management and environment configuration.
- Copilot opportunity filtering/count/order/offset/limit now execute in
PostgreSQL. Composite opportunity/application indexes and the legacy request
JSON `startDate` expression index support the new discovery queries.

## Code Quality Improvements

Expand Down
2 changes: 1 addition & 1 deletion docs/api-usage-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
| POST | `/v5/projects/:projectId/copilots/requests` | `platform-ui` | none | `{data:{projectId,opportunityTitle,copilotUsername?,complexity,requiresCommunication,paymentType,otherPaymentType?,projectType,overview,skills[],startDate,numWeeks,tzRestrictions,numHoursPerWeek}}` | Created request object (`status` starts as `new`) |
| PATCH | `/v5/projects/copilots/requests/:copilotRequestId` | `platform-ui` | none | `{data:{...partial editable fields...}}` | Updated request object |
| POST | `/v5/projects/:projectId/copilots/requests/:copilotRequestId/approve` | `platform-ui` | none | `{type}` | Created copilot opportunity object |
| GET | `/v5/projects/copilots/opportunities` | `platform-ui`, `community-app` | `page`, `pageSize`, `sort`; `community-app` also sends `noGrouping=true` | none | Opportunity list derived from request `data`; public endpoint |
| GET | `/v5/projects/copilots/opportunities` | `platform-ui`, `community-app` | Existing: `page`, `pageSize`, `sort`, `noGrouping`; v6 Opportunities additions: `search`, `status`, `projectId`, `projectName`, `type`, `skills`, start/created date ranges, `applied`, `applicationStatus`, plus documented aliases | none | Bare opportunity array derived from request `data`; pagination headers; authenticated callers also receive their application summary; public endpoint |
| GET | `/v5/projects/copilot/opportunity/:id` | `platform-ui` | none | none | Single opportunity details; includes flattened request fields, plus `members`, `canApplyAsCopilot`, and admin/manager-only `project` metadata |
| POST | `/v5/projects/copilots/opportunity/:id/apply` | `platform-ui` | none | `{notes}` | Created (or existing) copilot application object |
| GET | `/v5/projects/copilots/opportunity/:id/applications` | `platform-ui` | Optional `sort` | none | For admin/PM: full applications (`id,userId,status,notes,existingMembership,...`); for non-admin: reduced fields (`userId,status,createdAt`) |
Expand Down
105 changes: 105 additions & 0 deletions docs/copilot-opportunities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Copilot opportunity discovery

`GET /v6/projects/copilots/opportunities` is the canonical marketplace listing
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.

## Query parameters

| Parameter | Type/default | Behavior |
| --- | --- | --- |
| `page` | integer, `1` | One-based page number. |
| `pageSize` | integer, `20`, max `200` | Page size. `perPage` is a compatibility alias. |
| `sort` | string, `createdAt desc` | `createdAt`, `updatedAt`, `status`, `type`, `projectName`, `opportunityTitle`, or `startDate`, followed by `asc` or `desc`. |
| `noGrouping` | boolean, `false` | When false, preserve legacy status grouping: active, canceled, completed. |
| `search` | string, max 200 | Case-insensitive match over title, overview, project name, type, and skills. `keyword` is an alias. |
| `status` | string list | `active`, `completed`, and/or `canceled`. Comma-separated, repeated, and `status[$in]` bracket forms are accepted. |
| `projectId` | numeric string | Exact project id. |
| `projectName` | string, max 200 | Case-insensitive partial project name. |
| `type` | string list | `dev`, `qa`, `design`, `ai`, and/or `datascience`. `projectType` is an alias. |
| `skills` | string list, max 50 | Match any request skill id or name, case-insensitively. `skill` is an alias. |
| `startDateFrom`, `startDateTo` | ISO date/date-time | Inclusive requested-start range stored in request data. Date-only upper bounds include the full UTC day. |
| `createdAtFrom`, `createdAtTo` | ISO date/date-time | Inclusive opportunity creation range. |
| `applied` | boolean | Whether the current authenticated user has a non-deleted application. |
| `myApplications` | boolean | Compatibility shortcut for `applied=true`; false leaves the list unfiltered. |
| `applicationStatus` | string list | Current-user application states: `pending`, `invited`, `accepted`, `canceled`. Implies `applied=true`. |

`applied`, `myApplications=true`, and `applicationStatus` require an
authenticated principal with a numeric Topcoder user id and return `401` when
that context is unavailable. `applicationStatus` cannot be combined with
`applied=false`. Anonymous requests remain supported. When an Authorization
header is supplied, it is validated so the response can include caller-specific
state; malformed or invalid supplied credentials return `401`.

Example:

```http
GET /v6/projects/copilots/opportunities?page=1&pageSize=12&status=active&type=dev,ai&skills=Node.js,React&startDateFrom=2026-08-01&sort=startDate%20asc&myApplications=true
Authorization: Bearer <jwt>
```

## Response

The body remains an array for compatibility:

```json
[
{
"id": "21",
"copilotRequestId": "11",
"status": "active",
"type": "dev",
"createdAt": "2026-08-01T00:00:00.000Z",
"updatedAt": "2026-08-02T00:00:00.000Z",
"opportunityTitle": "Lead the delivery team",
"projectType": "dev",
"overview": "Delivery overview",
"skills": [{ "id": "1", "name": "Node.js" }],
"startDate": "2026-08-15T00:00:00.000Z",
"numWeeks": 8,
"tzRestrictions": "UTC-5 to UTC+2",
"numHoursPerWeek": 20,
"canApplyAsCopilot": true,
"hasApplied": true,
"currentUserApplication": {
"id": "31",
"status": "pending",
"createdAt": "2026-08-03T00:00:00.000Z",
"updatedAt": "2026-08-03T00:00:00.000Z"
}
}
]
```

Documented public request data continues to be flattened into each result:
`opportunityTitle`, `copilotUsername`, `complexity`,
`requiresCommunication`, `paymentType`, `otherPaymentType`, `projectType`,
`overview`, `skills`, `startDate`, `numWeeks`, `tzRestrictions`, and
`numHoursPerWeek`. Other stored request JSON keys are not exposed, and request
JSON cannot override trusted opportunity identity, status, type, timestamps,
eligibility, or current-user application fields.

`canApplyAsCopilot` is true only for an authenticated human caller with a
numeric user id and the lowercase `copilot` role when the opportunity is
active, the caller has no existing application, and the caller is not already
a project member. It is always false for anonymous, M2M, and non-copilot
callers. `hasApplied` and `currentUserApplication` are included when a numeric
authenticated user id is available. Admin and manager responses retain the
minimal `projectId` and `project: { name }` metadata previously exposed. No
project member ids are returned by the public list or detail routes.

Pagination metadata is returned in `X-Page`, `X-Per-Page`, `X-Total`, and
`X-Total-Pages`. `X-Prev-Page`, `X-Next-Page`, and RFC 5988-style `Link` headers
are included when applicable. All pagination headers are exposed through CORS.

## Detail aliases

Both routes resolve the same detail response:

- `GET /v6/projects/copilot/opportunity/:id`
- `GET /v6/projects/copilots/opportunity/:id`

The detail includes the same allow-listed flattened fields, caller eligibility,
and current-user application summary as the list. Membership eligibility is
calculated server-side without exposing project member ids.
46 changes: 46 additions & 0 deletions packages/projects-prisma-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# `@topcoder/projects-api-v6`

This package exports the Projects API v6 Prisma models, enums, query types, and
`PrismaClient`. It also provides a stable factory that configures the Prisma 7
PostgreSQL driver adapter for service-to-service database aggregation.

Install it from the repository subdirectory, consistent with other Topcoder v6
Prisma clients:

```json
{
"dependencies": {
"@topcoder/projects-api-v6": "github:topcoder-platform/projects-api-v6#<commit>&path:packages/projects-prisma-client"
}
}
```

Create and own one client for the process:

```ts
import {
CopilotOpportunityStatus,
createProjectsPrismaClient,
} from '@topcoder/projects-api-v6';

const projects = createProjectsPrismaClient(process.env.PROJECTS_DB_URL!, {
clientOptions: { log: ['warn', 'error'] },
driverOptions: {
connectionTimeoutMillis: 5000,
query_timeout: 5000,
statement_timeout: 5000,
},
});

const activeCount = await projects.copilotOpportunity.count({
where: { status: CopilotOpportunityStatus.active, deletedAt: null },
});

await projects.$disconnect();
```

The optional `schema` setting overrides the connection string's `schema` query
parameter. `driverOptions` exposes bounded PostgreSQL pool/query settings while
the factory continues to own adapter construction. The returned client is
caller-owned and must be disconnected during application shutdown. The package
pins Prisma and `@prisma/adapter-pg` to 7.9.0.
1 change: 1 addition & 0 deletions packages/projects-prisma-client/generated/client.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./index"
5 changes: 5 additions & 0 deletions packages/projects-prisma-client/generated/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('.') }
1 change: 1 addition & 0 deletions packages/projects-prisma-client/generated/default.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./index"
5 changes: 5 additions & 0 deletions packages/projects-prisma-client/generated/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('#main-entry-point') }
1 change: 1 addition & 0 deletions packages/projects-prisma-client/generated/edge.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./default"
Loading
Loading