refactor: merge tracker-service into platform-service
Phase 3 of service consolidation (5→2 services). Moved modules: - items (16 tests) - comments (6 tests) - votes (5 tests) - public (16 tests) — rate-limited, no auth required Changes: - Copied 4 modules from tracker-service - Added DEFAULT_PRODUCT_ID alias in product-config.ts (Gap 1) - Created src/lib/auth.ts re-exporting extractAuth from @bytelyst/auth (Gap 2) - Added @bytelyst/auth and @fastify/rate-limit to package.json (Gap 2) - Registered itemRoutes, commentRoutes, voteRoutes, publicRoutes in server.ts - Public routes at top level (no auth scope) - Removed tracker-service directory Tests: 158 passing (115 + 43 from tracker = 158) ✅ Build: clean ✅ Service consolidation Phases 1-3 complete: - growth-service: merged ✅ - billing-service: merged ✅ - tracker-service: merged ✅ Remaining: 2 services (platform-service + extraction-service)
This commit is contained in:
parent
0933e931d4
commit
29fc8124e4
61
pnpm-lock.yaml
generated
61
pnpm-lock.yaml
generated
@ -252,6 +252,9 @@ importers:
|
||||
'@azure/storage-blob':
|
||||
specifier: ^12.31.0
|
||||
version: 12.31.0
|
||||
'@bytelyst/auth':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/auth
|
||||
'@bytelyst/blob':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/blob
|
||||
@ -270,6 +273,9 @@ importers:
|
||||
'@fastify/cors':
|
||||
specifier: ^10.0.2
|
||||
version: 10.1.0
|
||||
'@fastify/rate-limit':
|
||||
specifier: ^10.3.0
|
||||
version: 10.3.0
|
||||
'@fastify/swagger':
|
||||
specifier: ^9.4.2
|
||||
version: 9.7.0
|
||||
@ -308,61 +314,6 @@ importers:
|
||||
specifier: ^3.0.5
|
||||
version: 3.2.4(@types/node@22.19.11)(happy-dom@18.0.1)(jsdom@28.0.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
services/tracker-service:
|
||||
dependencies:
|
||||
'@azure/cosmos':
|
||||
specifier: ^4.2.0
|
||||
version: 4.9.1(@azure/core-client@1.10.1)
|
||||
'@bytelyst/auth':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/auth
|
||||
'@bytelyst/config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/config
|
||||
'@bytelyst/cosmos':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/cosmos
|
||||
'@bytelyst/errors':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/errors
|
||||
'@bytelyst/fastify-core':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/fastify-core
|
||||
'@fastify/cors':
|
||||
specifier: ^10.0.2
|
||||
version: 10.1.0
|
||||
'@fastify/rate-limit':
|
||||
specifier: ^10.3.0
|
||||
version: 10.3.0
|
||||
'@fastify/swagger':
|
||||
specifier: ^9.4.2
|
||||
version: 9.7.0
|
||||
fastify:
|
||||
specifier: ^5.2.1
|
||||
version: 5.7.4
|
||||
fastify-metrics:
|
||||
specifier: ^10.3.0
|
||||
version: 10.6.0(fastify@5.7.4)
|
||||
jose:
|
||||
specifier: ^6.0.8
|
||||
version: 6.1.3
|
||||
zod:
|
||||
specifier: ^3.24.2
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.12.0
|
||||
version: 22.19.11
|
||||
tsx:
|
||||
specifier: ^4.19.2
|
||||
version: 4.21.0
|
||||
typescript:
|
||||
specifier: ^5.7.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^3.0.5
|
||||
version: 3.2.4(@types/node@22.19.11)(happy-dom@18.0.1)(jsdom@28.0.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages:
|
||||
|
||||
'@acemir/cssom@0.9.31':
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@bytelyst/blob": "workspace:*",
|
||||
"@bytelyst/auth": "workspace:*",
|
||||
"@bytelyst/config": "workspace:*",
|
||||
"@bytelyst/cosmos": "workspace:*",
|
||||
"@bytelyst/errors": "workspace:*",
|
||||
@ -21,6 +22,7 @@
|
||||
"@azure/cosmos": "^4.2.0",
|
||||
"@azure/storage-blob": "^12.31.0",
|
||||
"@fastify/cors": "^10.0.2",
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@fastify/swagger": "^9.4.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"fastify": "^5.2.1",
|
||||
|
||||
@ -7,3 +7,5 @@ const _id = loadProductIdentity();
|
||||
export const PRODUCT_ID = _id.productId;
|
||||
export const DISPLAY_NAME = _id.displayName;
|
||||
export const LICENSE_PREFIX = _id.licensePrefix;
|
||||
/** Alias for tracker modules that use DEFAULT_PRODUCT_ID */
|
||||
export const DEFAULT_PRODUCT_ID = PRODUCT_ID;
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
*
|
||||
* Modules: auth, audit, notifications, feature flags, blob,
|
||||
* invitations, referrals, promos (merged from growth-service),
|
||||
* subscriptions, usage, plans, licenses, stripe (merged from billing-service).
|
||||
* subscriptions, usage, plans, licenses, stripe (merged from billing-service),
|
||||
* items, comments, votes, public (merged from tracker-service).
|
||||
* Port: 4003 (configurable via PORT env var).
|
||||
*/
|
||||
|
||||
@ -22,6 +23,10 @@ import { usageRoutes } from './modules/usage/routes.js';
|
||||
import { planRoutes } from './modules/plans/routes.js';
|
||||
import { licenseRoutes } from './modules/licenses/routes.js';
|
||||
import { stripeRoutes } from './modules/stripe/routes.js';
|
||||
import { itemRoutes } from './modules/items/routes.js';
|
||||
import { commentRoutes } from './modules/comments/routes.js';
|
||||
import { voteRoutes } from './modules/votes/routes.js';
|
||||
import { publicRoutes } from './modules/public/routes.js';
|
||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||
import { config } from './lib/config.js';
|
||||
|
||||
@ -75,5 +80,11 @@ if (BILLING_KEY) {
|
||||
}
|
||||
// Stripe routes outside billing scope (webhook has its own signature verification)
|
||||
await app.register(stripeRoutes, { prefix: '/api' });
|
||||
// Tracker modules (merged from tracker-service)
|
||||
await app.register(itemRoutes, { prefix: '/api' });
|
||||
await app.register(commentRoutes, { prefix: '/api' });
|
||||
await app.register(voteRoutes, { prefix: '/api' });
|
||||
// Public routes — no auth, registered at top level
|
||||
await app.register(publicRoutes, { prefix: '/api' });
|
||||
|
||||
await startService(app, { port: config.PORT, host: config.HOST });
|
||||
|
||||
2
services/tracker-service/.gitignore
vendored
2
services/tracker-service/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
@ -1,44 +0,0 @@
|
||||
# Build context: repo root (docker compose sets context: .)
|
||||
FROM node:22-alpine AS builder
|
||||
RUN npm install -g pnpm@10
|
||||
WORKDIR /app
|
||||
|
||||
# Copy workspace config + lockfile for dependency resolution
|
||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml tsconfig.base.json ./
|
||||
|
||||
# Copy all package.json files (pnpm needs these for workspace resolution)
|
||||
COPY packages/errors/package.json packages/errors/
|
||||
COPY packages/cosmos/package.json packages/cosmos/
|
||||
COPY packages/blob/package.json packages/blob/
|
||||
COPY packages/config/package.json packages/config/
|
||||
COPY packages/auth/package.json packages/auth/
|
||||
COPY packages/api-client/package.json packages/api-client/
|
||||
COPY packages/fastify-core/package.json packages/fastify-core/
|
||||
COPY packages/logger/package.json packages/logger/
|
||||
COPY packages/monitoring/package.json packages/monitoring/
|
||||
COPY packages/react-auth/package.json packages/react-auth/
|
||||
COPY packages/design-tokens/package.json packages/design-tokens/
|
||||
COPY packages/testing/package.json packages/testing/
|
||||
COPY services/tracker-service/package.json services/tracker-service/
|
||||
|
||||
# Install all workspace deps
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source
|
||||
COPY packages/ packages/
|
||||
COPY services/tracker-service/tsconfig.json services/tracker-service/
|
||||
COPY services/tracker-service/src/ services/tracker-service/src/
|
||||
|
||||
# Build packages first, then service
|
||||
RUN pnpm -r --filter @lysnrai/tracker-service... build
|
||||
|
||||
# Deploy to isolated directory (production deps only)
|
||||
RUN pnpm --filter @lysnrai/tracker-service deploy --legacy /app/deploy
|
||||
|
||||
# ── Production ─────────────────────────────────────────────
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/deploy ./
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 4004
|
||||
CMD ["node", "dist/server.js"]
|
||||
@ -1,58 +0,0 @@
|
||||
# Tracker Service
|
||||
|
||||
Product-agnostic issue tracker for feature requests, bugs, and task management.
|
||||
Built with Fastify + TypeScript + Azure Cosmos DB.
|
||||
|
||||
## Port
|
||||
|
||||
`4004` (configurable via `PORT` env var)
|
||||
|
||||
## Modules
|
||||
|
||||
- **items** — CRUD for tracker items (bugs, features, tasks) with filtering, pagination, and stats
|
||||
- **comments** — Threaded discussion on items
|
||||
- **votes** — Upvote toggle (1 per user per item)
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | ----------------------------- | ---------------------------------------- |
|
||||
| GET | `/items` | List/filter/search items |
|
||||
| POST | `/items` | Create item |
|
||||
| GET | `/items/stats` | Aggregate counts by type/status/priority |
|
||||
| GET | `/items/:id` | Get single item |
|
||||
| PUT | `/items/:id` | Update item |
|
||||
| PATCH | `/items/:id/status` | Quick status transition |
|
||||
| DELETE | `/items/:id` | Delete item |
|
||||
| GET | `/items/:itemId/comments` | List comments |
|
||||
| POST | `/items/:itemId/comments` | Add comment |
|
||||
| PUT | `/items/:itemId/comments/:id` | Edit comment |
|
||||
| DELETE | `/items/:itemId/comments/:id` | Delete comment |
|
||||
| POST | `/items/:itemId/vote` | Toggle upvote |
|
||||
| GET | `/items/:itemId/votes` | List voters |
|
||||
| GET | `/health` | Health check |
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cp .env .env # fill in values
|
||||
npm install
|
||||
npm run dev # starts with tsx watch on port 4004
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
npm test # vitest run (29 tests)
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
See `.env` for required variables:
|
||||
|
||||
- `COSMOS_ENDPOINT` — Azure Cosmos DB endpoint
|
||||
- `COSMOS_KEY` — Cosmos DB primary key
|
||||
- `COSMOS_DATABASE` — Database name
|
||||
- `JWT_SECRET` — Shared secret for JWT verification (from platform-service)
|
||||
- `DEFAULT_PRODUCT_ID` — Default product scope (e.g., `lysnrai`)
|
||||
- `PORT` — Server port (default `4004`)
|
||||
@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "@lysnrai/tracker-service",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Tracker Service — feature requests, bugs, tasks management (product-agnostic)",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/server.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/server.js",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"lint": "eslint src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bytelyst/auth": "workspace:*",
|
||||
"@bytelyst/config": "workspace:*",
|
||||
"@bytelyst/cosmos": "workspace:*",
|
||||
"@bytelyst/errors": "workspace:*",
|
||||
"@bytelyst/fastify-core": "workspace:*",
|
||||
"@azure/cosmos": "^4.2.0",
|
||||
"@fastify/cors": "^10.0.2",
|
||||
"@fastify/rate-limit": "^10.3.0",
|
||||
"@fastify/swagger": "^9.4.2",
|
||||
"fastify": "^5.2.1",
|
||||
"fastify-metrics": "^10.3.0",
|
||||
"jose": "^6.0.8",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.12.0",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const envSchema = z.object({
|
||||
PORT: z.coerce.number().default(4004),
|
||||
HOST: z.string().default('0.0.0.0'),
|
||||
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
||||
CORS_ORIGIN: z.string().optional(),
|
||||
SERVICE_NAME: z.string().default('tracker-service'),
|
||||
COSMOS_ENDPOINT: z.string().min(1, 'COSMOS_ENDPOINT is required'),
|
||||
COSMOS_KEY: z.string().min(1, 'COSMOS_KEY is required'),
|
||||
COSMOS_DATABASE: z.string().default('lysnrai'),
|
||||
JWT_SECRET: z.string().min(1, 'JWT_SECRET is required'),
|
||||
DEFAULT_PRODUCT_ID: z.string().default('lysnrai'),
|
||||
});
|
||||
|
||||
export const config = envSchema.parse(process.env);
|
||||
@ -1,26 +0,0 @@
|
||||
import { initializeAllContainers, registerContainers } from '@bytelyst/cosmos';
|
||||
import type { ContainerConfig } from '@bytelyst/cosmos';
|
||||
import { config } from './config.js';
|
||||
|
||||
const CONTAINER_DEFS: Record<string, ContainerConfig> = {
|
||||
tracker_items: { partitionKeyPath: '/id' },
|
||||
tracker_comments: { partitionKeyPath: '/id' },
|
||||
tracker_votes: { partitionKeyPath: '/id' },
|
||||
};
|
||||
|
||||
export async function initCosmosIfNeeded(): Promise<void> {
|
||||
registerContainers(CONTAINER_DEFS);
|
||||
|
||||
const shouldInit = config.NODE_ENV !== 'production' || process.env.COSMOS_AUTO_INIT === 'true';
|
||||
if (!shouldInit) return;
|
||||
|
||||
try {
|
||||
await initializeAllContainers();
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('[tracker-service] Cosmos containers ensured');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[tracker-service] Cosmos init failed: ${msg}`);
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
/**
|
||||
* Re-export from @bytelyst/cosmos — shared across all services.
|
||||
*/
|
||||
export { getContainer, getCosmosClient, getDatabase } from '@bytelyst/cosmos';
|
||||
@ -1,59 +0,0 @@
|
||||
/**
|
||||
* Error classes unit tests.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
ServiceError,
|
||||
NotFoundError,
|
||||
BadRequestError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
ConflictError,
|
||||
} from './errors.js';
|
||||
|
||||
describe('ServiceError', () => {
|
||||
it('creates error with status code', () => {
|
||||
const err = new ServiceError(418, "I'm a teapot");
|
||||
expect(err.statusCode).toBe(418);
|
||||
expect(err.message).toBe("I'm a teapot");
|
||||
expect(err.name).toBe('ServiceError');
|
||||
});
|
||||
});
|
||||
|
||||
describe('typed errors', () => {
|
||||
it('NotFoundError is 404', () => {
|
||||
const err = new NotFoundError();
|
||||
expect(err.statusCode).toBe(404);
|
||||
expect(err.message).toBe('Not found');
|
||||
});
|
||||
|
||||
it('BadRequestError is 400', () => {
|
||||
const err = new BadRequestError('Invalid input');
|
||||
expect(err.statusCode).toBe(400);
|
||||
expect(err.message).toBe('Invalid input');
|
||||
});
|
||||
|
||||
it('UnauthorizedError is 401', () => {
|
||||
const err = new UnauthorizedError();
|
||||
expect(err.statusCode).toBe(401);
|
||||
});
|
||||
|
||||
it('ForbiddenError is 403', () => {
|
||||
const err = new ForbiddenError();
|
||||
expect(err.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('ConflictError is 409', () => {
|
||||
const err = new ConflictError();
|
||||
expect(err.statusCode).toBe(409);
|
||||
});
|
||||
|
||||
it('all extend ServiceError', () => {
|
||||
expect(new NotFoundError()).toBeInstanceOf(ServiceError);
|
||||
expect(new BadRequestError()).toBeInstanceOf(ServiceError);
|
||||
expect(new UnauthorizedError()).toBeInstanceOf(ServiceError);
|
||||
expect(new ForbiddenError()).toBeInstanceOf(ServiceError);
|
||||
expect(new ConflictError()).toBeInstanceOf(ServiceError);
|
||||
});
|
||||
});
|
||||
@ -1,12 +0,0 @@
|
||||
/**
|
||||
* Re-export from @bytelyst/errors — shared across all services.
|
||||
*/
|
||||
export {
|
||||
ServiceError,
|
||||
BadRequestError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
TooManyRequestsError,
|
||||
} from '@bytelyst/errors';
|
||||
@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Re-export from @bytelyst/config — shared product identity.
|
||||
* The tracker service is product-agnostic; every document carries its own productId.
|
||||
*/
|
||||
import { getProductId } from '@bytelyst/config';
|
||||
|
||||
export const DEFAULT_PRODUCT_ID = process.env.DEFAULT_PRODUCT_ID || getProductId();
|
||||
@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Tracker Service — Fastify server entry point.
|
||||
*
|
||||
* Modules: items, comments, votes.
|
||||
* Port: 4004 (configurable via PORT env var).
|
||||
* Product-agnostic: all data scoped by productId.
|
||||
*/
|
||||
|
||||
import { createServiceApp, startService } from '@bytelyst/fastify-core';
|
||||
import { itemRoutes } from './modules/items/routes.js';
|
||||
import { commentRoutes } from './modules/comments/routes.js';
|
||||
import { voteRoutes } from './modules/votes/routes.js';
|
||||
import { publicRoutes } from './modules/public/routes.js';
|
||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||
import { config } from './lib/config.js';
|
||||
|
||||
await initCosmosIfNeeded();
|
||||
|
||||
const app = await createServiceApp({
|
||||
name: 'tracker-service',
|
||||
version: '0.1.0',
|
||||
description: 'Feature requests, bugs, tasks — product-agnostic',
|
||||
corsOrigin: config.CORS_ORIGIN,
|
||||
swagger: {
|
||||
title: 'Tracker Service',
|
||||
description: 'Feature requests, bugs, tasks — product-agnostic',
|
||||
port: config.PORT,
|
||||
},
|
||||
metrics: true,
|
||||
});
|
||||
|
||||
// Register route modules
|
||||
await app.register(itemRoutes, { prefix: '/api' });
|
||||
await app.register(commentRoutes, { prefix: '/api' });
|
||||
await app.register(voteRoutes, { prefix: '/api' });
|
||||
await app.register(publicRoutes, { prefix: '/api' });
|
||||
|
||||
await startService(app, { port: config.PORT, host: config.HOST });
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user