Skip to content
Open
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 .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20.19.6
18 changes: 11 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
FROM node:14-bullseye as builder
RUN apt-get -qy update && apt-get install -qy openssl
FROM node:20-alpine3.18 AS builder
RUN apk add --no-cache openssl postgresql-client
WORKDIR /app
COPY ./package.json ./yarn.lock /app/
RUN yarn install && npm i -g @prisma/cli@2.12.1
RUN yarn install

COPY ./ /app

RUN prisma generate && yarn run build
RUN yarn prisma generate && yarn run build

FROM node:14-bullseye as runtime
RUN apt-get -qy update && apt-get install -qy openssl
FROM node:20-alpine3.18 AS runtime
RUN apk add --no-cache openssl postgresql-client

WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder "/app/dist/" "/app/dist/"
COPY --from=builder "/app/node_modules/" "/app/node_modules/"
COPY --from=builder "/app/package.json" "/app/package.json"
COPY --from=builder "/app/prisma/" "/app/prisma/"
COPY ./docker-entrypoint.sh /docker-entrypoint.sh
RUN mkdir -p /app/dist
RUN chmod +x /docker-entrypoint.sh

CMD ["npm", "run", "start"]
CMD ["/docker-entrypoint.sh"]
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,29 @@ An admin token allowing editing all events in a region, giving awards, etc:
"e": "codeday-seattle-spring-2020"
}
```

## Local Testing Workflow

Start local dependencies (Postgres + Elasticsearch + app):

```bash
docker-compose up -d
```

Seed local test data:

```bash
yarn seed-dummy
```

Generate a JWT for local testing:

```bash
yarn generate-token event-test-2025 admin
```

Run sample GraphQL queries (public and auth when API_KEY is set):

```bash
API_KEY=<token-from-above> yarn test-queries
```
69 changes: 69 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
version: '3.9'

services:
postgres:
image: postgres:15-alpine
container_name: showcase-gql-postgres
ports:
- '5434:5432'
environment:
POSTGRES_DB: showcase
POSTGRES_USER: showcase
POSTGRES_PASSWORD: showcase
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U showcase -d showcase']
interval: 10s
timeout: 5s
retries: 10
volumes:
- postgres-data:/var/lib/postgresql/data

elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.9
container_name: showcase-gql-elasticsearch
ports:
- '9200:9200'
environment:
discovery.type: single-node
xpack.security.enabled: 'false'
ES_JAVA_OPTS: -Xms512m -Xmx512m
healthcheck:
test: ['CMD-SHELL', 'curl -fsS http://localhost:9200/_cluster/health >/dev/null || exit 1']
interval: 15s
timeout: 10s
retries: 10
volumes:
- elasticsearch-data:/usr/share/elasticsearch/data

app:
build:
context: .
dockerfile: Dockerfile
container_name: showcase-gql-app
depends_on:
postgres:
condition: service_healthy
elasticsearch:
condition: service_healthy
environment:
NODE_ENV: development
PORT: 5000
DATABASE_URL: postgresql://showcase:showcase@postgres:5432/showcase?schema=public
JWT_SECRET: local-dev-secret
JWT_AUDIENCE: showcase
UPLOADER_BASE: http://localhost:3001
ports:
- '5000:5000'
healthcheck:
test:
[
'CMD-SHELL',
'node -e "require(`http`).get(`http://localhost:5000/graphql`, (res) => process.exit(res.statusCode < 500 ? 0 : 1)).on(`error`, () => process.exit(1))"',
]
interval: 20s
timeout: 10s
retries: 10

volumes:
postgres-data:
elasticsearch-data:
8 changes: 8 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/sh
set -e

if [ -n "$DATABASE_URL" ]; then
yarn prisma migrate deploy
fi

exec yarn start
32 changes: 19 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,26 @@
"scripts": {
"start": "node dist",
"clean": "rm -rf dist",
"build": "npm -s run clean && npm -s run generate && npx tsc",
"build": "yarn -s clean && yarn -s generate && tsc && cp -r src/_types dist/_types",
"generate": "prisma generate",
"dev": "ts-node-dev --no-notify --respawn --transpile-only src",
"debug": "ts-node-dev --no-notify --respawn src"
"debug": "ts-node-dev --no-notify --respawn src",
"seed-dummy": "ts-node scripts/seedDummy.ts",
"generate-token": "ts-node scripts/generateToken.ts",
"test-queries": "ts-node scripts/testQueries.ts"
},
"dependencies": {
"@apollo/server": "^4.11.3",
"@as-integrations/express4": "^1.1.2",
"@codeday/uploader-node": "^1.0.1",
"@prisma/client": "^2.9.0",
"@prisma/client": "^4.16.2",
"@types/handlebars": "^4.1.0",
"@types/jsonwebtoken": "^8.5.0",
"apollo-server": "^2.16.1",
"apollo-server-express": "^2.16.1",
"class-validator": "^0.12.2",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"graphql": "^15.3.0",
"graphql-upload": "^11.0.0",
"graphql-upload": "^13.0.0",
"graphql-ws": "^2.0.0",
"handlebars": "^4.7.6",
"jsonwebtoken": "^8.5.1",
Expand All @@ -38,14 +41,17 @@
"devDependencies": {
"@codeday/eslint-config": "^2.1.4",
"@codeday/eslint-config-typescript": "^2.1.6",
"@prisma/cli": "2.12.1",
"prisma": "^4.16.2",
"@types/knuth-shuffle-seeded": "^1.0.0",
"@types/node": "^14.0.27",
"@types/graphql-upload": "^8.0.12",
"@types/node": "^20.17.30",
"@types/node-fetch": "^2.5.7",
"@types/ws": "^8.5.12",
"@typescript-eslint/parser": "^4.5.0",
"eslint": "^7.6.0",
"ts-node": "^8.10.2",
"ts-node-dev": "^1.0.0-pre.56",
"typescript": "^3.9.7"
}
}
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.7.3"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
47 changes: 47 additions & 0 deletions scripts/generateToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'dotenv/config';
import { sign } from 'jsonwebtoken';

type Role = 'admin' | 'manager' | 'mentor' | 'student';

const [eventId, roleArg] = process.argv.slice(2);
const role = roleArg as Role;

if (!eventId || !role || !['admin', 'manager', 'mentor', 'student'].includes(role)) {
console.error('Usage: yarn generate-token <event-id> <admin|manager|mentor|student>');
process.exit(1);
}

const jwtSecret = process.env.JWT_SECRET || 'local-dev-secret';
const jwtAudience = process.env.JWT_AUDIENCE || 'showcase';

const payloadByRole: Record<Role, Record<string, unknown>> = {
admin: {
a: true,
e: eventId,
},
manager: {
a: false,
u: 'event-manager',
e: eventId,
jvr: true,
},
mentor: {
a: false,
u: 'alice-mentor',
e: eventId,
},
student: {
a: false,
u: 'ava-student',
e: eventId,
},
};

const tokenPayload = payloadByRole[role];
const token = sign(tokenPayload, jwtSecret, {
audience: jwtAudience,
algorithm: 'HS256',
expiresIn: '7d',
});

console.log(token);
55 changes: 55 additions & 0 deletions scripts/seedDummy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'dotenv/config';
import { PrismaClient, MetadataVisibility, ProjectType } from '@prisma/client';

const prisma = new PrismaClient();

const eventId = process.env.SEED_EVENT_ID || 'event-test-2025';
const programId = process.env.SEED_PROGRAM_ID || 'codeday';
const eventGroupId = process.env.SEED_EVENT_GROUP_ID || 'spring-2025';
const regionId = process.env.SEED_REGION_ID || 'seattle';

async function run(): Promise<void> {
await prisma.project.deleteMany({ where: { eventId } });

await prisma.project.create({
data: {
name: 'Mentor Match Demo',
description: 'Dummy project for local GraphQL testing.',
eventId,
programId,
eventGroupId,
regionId,
type: ProjectType.APP,
members: {
create: [
{ username: 'alice-mentor' },
{ username: 'bob-mentor' },
{ username: 'ava-student' },
{ username: 'ben-student' },
],
},
metadata: {
create: [
{ key: 'eventName', value: 'Local Test Event 2025', visibility: MetadataVisibility.PUBLIC },
{ key: 'mentorCount', value: '2', visibility: MetadataVisibility.PUBLIC },
{ key: 'studentCount', value: '2', visibility: MetadataVisibility.PUBLIC },
{ key: 'internalNote', value: 'admin-only test metadata', visibility: MetadataVisibility.ADMIN },
],
},
},
});

console.log('Dummy seed complete.');
console.log(`Event: ${eventId}`);
console.log('Mentors: alice-mentor, bob-mentor');
console.log('Students: ava-student, ben-student');
}

run()
.catch((error) => {
console.error(error);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
63 changes: 63 additions & 0 deletions scripts/testQueries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'dotenv/config';
import fetch from 'node-fetch';

const endpoint = process.env.GRAPHQL_ENDPOINT || 'http://127.0.0.1:5000/graphql';
const apiKey = process.env.API_KEY;

async function runQuery(name: string, query: string, token?: string): Promise<void> {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ query }),
});

const body = await response.json();
console.log(`\n=== ${name} ===`);
console.log(JSON.stringify(body, null, 2));
}

async function run(): Promise<void> {
await runQuery(
'Public projects query',
`
query {
projects(take: 5) {
id
name
eventId
}
}
`,
);

if (!apiKey) {
console.log('\nNo API_KEY set; skipping authenticated query.');
return;
}

await runQuery(
'Authenticated metadata query',
`
query {
projects(where: { event: "event-test-2025" }, take: 1) {
id
name
metadata {
key
value
visibility
}
}
}
`,
apiKey,
);
}

run().catch((error) => {
console.error(error);
process.exit(1);
});
Loading