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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# compiled output
/dist
/node_modules
node_modules/
/build

# Logs
Expand Down
114 changes: 112 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Engagements API for managing temporary contract work opportunities.

## Prerequisites

- Node.js 22+
- pnpm
- Node.js 22.23.1 (use the version in `.nvmrc`)
- pnpm 11.15.1

## Getting Started

Expand All @@ -37,6 +37,39 @@ pnpm prisma:migrate
pnpm start:dev
```

## External Prisma client

Services that aggregate engagement data directly can install the
`packages/engagements-prisma-client` Git subdirectory as
`@topcoder/engagements-api-v6`. Generated Prisma files are isolated in its
`generated` directory, so regeneration does not overwrite the public package
contract. The package re-exports all engagement models, enums, Prisma helpers,
and `PrismaClient`, together with the supported factory:

```ts
import { createEngagementsPrismaClient } from '@topcoder/engagements-api-v6';

const engagements = createEngagementsPrismaClient(
process.env.ENGAGEMENTS_DATABASE_URL,
{ log: ['warn', 'error'] },
);
```

`createEngagementsPrismaClient(connectionString, options?)` creates the Prisma
7 PostgreSQL driver adapter, preserves the optional `schema` query parameter in
the connection URL, and returns a disconnected client that connects lazily on
its first query. Call `$disconnect()` during application shutdown. The factory
throws `TypeError` when `connectionString` is empty or not a string; Prisma may
raise its normal configuration and database errors while creating or using the
client. Connection-defining `adapter` and `accelerateUrl` options are owned by
the factory and intentionally excluded from its options type.

Run the package smoke test without connecting to a database with:

```bash
pnpm test:external-client
```

## Configuration

Set the following environment variables (see `.env.example` for defaults):
Expand Down Expand Up @@ -92,6 +125,83 @@ M2M access uses Auth0 client credentials. Ensure the client is configured with t

## Response Notes

### Public skill filtering and display names

The public opportunities list accepts standardized skill UUIDs, human-readable
skill names, or a mixture of both:

```http
GET /v6/engagements/engagements?requiredSkills=React,11111111-1111-4111-8111-111111111111&page=1&perPage=20
```

- Supply `requiredSkills` as a comma-separated value or repeated query
parameter. Up to 20 values are accepted; larger requests receive HTTP 400.
- Values use OR semantics. UUIDs are applied directly. Names must equal a
standardized skill name after trimming and case normalization; partial and
fuzzy suggestions are never accepted as filters.
- Name resolution uses server-side M2M credentials and does not require or
forward a member token. The exact-name request is batched; bounded
case-insensitive fallback lookups are used only for names not returned by the
case-sensitive standardized-skills list seam.
- Unknown names and dependency/authentication failures fail closed. An
all-name request with no resolved IDs returns an empty page without querying
engagements; in a mixed request, valid UUIDs/resolved names still participate
in the OR filter.
- Public list, active-list, and detail rows keep `requiredSkills` as the
backward-compatible ID array and add `skills: [{ "id", "name" }]`. Display
hydration is batched across a page and is non-fatal; a missing display name
falls back to its ID. Protected/private response behavior is unchanged.

### Current-user application filter

The public opportunity list supports an optional current-user filter:

```http
GET /v6/engagements/engagements?appliedByMe=true&page=1&perPage=20
Authorization: Bearer <member JWT>
```

- `appliedByMe=true` requires an authenticated human-user JWT with a user id and
returns only engagements having an application from that user. Anonymous
requests receive HTTP 401 and M2M tokens receive HTTP 403.
- `appliedByMe=false`, or omitting the parameter, applies no current-user
filter. Authentication remains optional and the existing public-list privacy,
status, search, sorting, and pagination behavior is unchanged.
- The response contract remains the normal engagement list shape:

```json
{
"data": [
{
"id": "engagement-id",
"title": "Frontend developer",
"applicationsCount": 3,
"project": { "id": "project-id", "name": "Example project" },
"projectName": "Example project"
}
],
"meta": {
"page": 1,
"perPage": 20,
"totalCount": 1,
"totalPages": 1
}
}
```

The existing authenticated `GET /v6/engagements/applications` endpoint remains
the companion route for retrieving the current user's paginated application
records. The engagement-list filter performs its relation check in the same
database query and does not expose another member's application details.

Anonymous and ordinary-member responses from `GET /engagements`,
`GET /engagements/active`, and public `GET /engagements/:id` reads are produced
from an explicit public allow-list. They never include `account`, `smu`, `spoc`,
`receivedDateFromAccount`, `createdByEmail`, or assignment details. Privileged
`includePrivate=true` listings, authorized manager/M2M detail reads, assigned
members reading their own private engagement, and `GET /engagements/my-assignments`
retain the protected fields needed by those workflows.

- `GET /engagements`, `GET /engagements/active`, and `GET /engagements/my-assignments` include project metadata on each engagement record:
- `projectName` (if available)
- `project` object with `id` and optional `name`
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"packageManager": "pnpm@11.15.1",
"scripts": {
"prebuild": "pnpm run prisma:generate",
"build": "nest build",
"start": "node dist/main.js",
"start:dev": "nest start --watch",
Expand All @@ -12,6 +13,7 @@
"prisma:migrate": "prisma migrate dev",
"lint": "eslint \"src/**/*.ts\" --fix",
"test": "jest",
"test:external-client": "node --test packages/engagements-prisma-client/factory.test.js",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
Expand Down
41 changes: 41 additions & 0 deletions packages/engagements-prisma-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# `@topcoder/engagements-api-v6`

This package is the supported external Prisma client for the Engagements API v6
schema. It re-exports the generated Prisma surface and provides
`createEngagementsPrismaClient(connectionString, options?)`, which configures
the Prisma 7 PostgreSQL driver adapter and honors the connection URL's optional
`schema` query parameter.

```ts
import { createEngagementsPrismaClient } from '@topcoder/engagements-api-v6';

const client = createEngagementsPrismaClient(
process.env.ENGAGEMENTS_DATABASE_URL,
);
const openCount = await client.engagement.count({
where: { status: 'OPEN', isPrivate: false },
});
await client.$disconnect();
```

Callers that need bounded database work can pass PostgreSQL pool settings
without constructing the Prisma 7 adapter themselves:

```ts
const client = createEngagementsPrismaClient(databaseUrl, {
driverOptions: {
connectionTimeoutMillis: 5000,
query_timeout: 5000,
statement_timeout: 5000,
},
});
```

The client connects lazily. Applications own its lifecycle and must disconnect
it during shutdown. An empty or non-string connection URL raises `TypeError`;
Prisma reports its normal configuration and database errors during creation or
query execution.

The package supports Node.js 22.23.1 and newer so it can be consumed by newer
v6 services. The Engagements API itself continues to use the exact Node.js
version pinned in its root `.nvmrc`.
Loading