fix: resolve TypeScript errors for CI pipeline
- Fix pg-boss Job type imports (PgBoss.Job -> Job from pg-boss) - Replace deprecated teamConcurrency with localConcurrency - Add null checks for possibly undefined values (clients, import rows) - Fix tone type narrowing in profile.ts - Fix test type assertions (non-null assertions, explicit Record types) - Extract auth middleware into shared module - Fix rate limiter Map generic type
This commit is contained in:
@@ -46,7 +46,7 @@ describe('Audit Logging', () => {
|
||||
describe('Request Metadata', () => {
|
||||
test('IP address extracted from x-forwarded-for', () => {
|
||||
const header = '192.168.1.1, 10.0.0.1';
|
||||
const ip = header.split(',')[0].trim();
|
||||
const ip = header.split(',')[0]!.trim();
|
||||
expect(ip).toBe('192.168.1.1');
|
||||
});
|
||||
|
||||
@@ -71,9 +71,9 @@ describe('Audit Logging', () => {
|
||||
}
|
||||
|
||||
expect(Object.keys(diff)).toHaveLength(2);
|
||||
expect(diff.firstName.from).toBe('John');
|
||||
expect(diff.firstName.to).toBe('Jonathan');
|
||||
expect(diff.stage.from).toBe('lead');
|
||||
expect(diff.firstName!.from).toBe('John');
|
||||
expect(diff.firstName!.to).toBe('Jonathan');
|
||||
expect(diff.stage!.from).toBe('lead');
|
||||
expect(diff.lastName).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -87,8 +87,10 @@ describe('Audit Logging', () => {
|
||||
|
||||
describe('Audit Log Filters', () => {
|
||||
test('page and limit defaults', () => {
|
||||
const page = parseInt(undefined || '1');
|
||||
const limit = Math.min(parseInt(undefined || '50'), 100);
|
||||
const noPage: string | undefined = undefined;
|
||||
const noLimit: string | undefined = undefined;
|
||||
const page = parseInt(noPage || '1');
|
||||
const limit = Math.min(parseInt(noLimit || '50'), 100);
|
||||
expect(page).toBe(1);
|
||||
expect(limit).toBe(50);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, test, expect } from 'bun:test';
|
||||
describe('Client Routes', () => {
|
||||
describe('Validation', () => {
|
||||
test('clientSchema requires firstName', () => {
|
||||
const invalidClient = {
|
||||
const invalidClient: Record<string, unknown> = {
|
||||
lastName: 'Doe',
|
||||
};
|
||||
// Schema validation test - firstName is required
|
||||
@@ -11,7 +11,7 @@ describe('Client Routes', () => {
|
||||
});
|
||||
|
||||
test('clientSchema requires lastName', () => {
|
||||
const invalidClient = {
|
||||
const invalidClient: Record<string, unknown> = {
|
||||
firstName: 'John',
|
||||
};
|
||||
// Schema validation test - lastName is required
|
||||
@@ -107,8 +107,8 @@ describe('Search Functionality', () => {
|
||||
|
||||
const filtered = clients.filter(c => c.tags?.includes('vip'));
|
||||
expect(filtered).toHaveLength(2);
|
||||
expect(filtered[0].firstName).toBe('John');
|
||||
expect(filtered[1].firstName).toBe('Bob');
|
||||
expect(filtered[0]!.firstName).toBe('John');
|
||||
expect(filtered[1]!.firstName).toBe('Bob');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
38
src/index.ts
38
src/index.ts
@@ -24,7 +24,6 @@ import { meetingPrepRoutes } from './routes/meeting-prep';
|
||||
import { db } from './db';
|
||||
import { users } from './db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import type { User } from './lib/auth';
|
||||
import { tagRoutes } from './routes/tags';
|
||||
import { initJobQueue } from './services/jobs';
|
||||
|
||||
@@ -57,21 +56,7 @@ const app = new Elysia()
|
||||
.use(inviteRoutes)
|
||||
.use(passwordResetRoutes)
|
||||
|
||||
// Protected routes - require auth
|
||||
.derive(async ({ request, set }): Promise<{ user: User }> => {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
set.status = 401;
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
return { user: session.user as User };
|
||||
})
|
||||
|
||||
// API routes (all require auth due to derive above)
|
||||
// API routes (auth middleware is in each route plugin)
|
||||
.group('/api', app => app
|
||||
.use(clientRoutes)
|
||||
.use(importRoutes)
|
||||
@@ -96,34 +81,37 @@ const app = new Elysia()
|
||||
// Error handler
|
||||
.onError(({ code, error, set, path }) => {
|
||||
// Always log errors with full details
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const stack = error instanceof Error ? error.stack : undefined;
|
||||
|
||||
console.error(`[${new Date().toISOString()}] ERROR on ${path}:`, {
|
||||
code,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
message,
|
||||
stack,
|
||||
});
|
||||
|
||||
if (code === 'VALIDATION') {
|
||||
set.status = 400;
|
||||
return { error: 'Validation error', details: error.message };
|
||||
return { error: 'Validation error', details: message };
|
||||
}
|
||||
|
||||
if (error.message === 'Unauthorized') {
|
||||
if (message === 'Unauthorized') {
|
||||
set.status = 401;
|
||||
return { error: 'Unauthorized' };
|
||||
}
|
||||
|
||||
if (error.message.includes('Forbidden')) {
|
||||
if (message.includes('Forbidden')) {
|
||||
set.status = 403;
|
||||
return { error: error.message };
|
||||
return { error: message };
|
||||
}
|
||||
|
||||
if (error.message.includes('not found')) {
|
||||
if (message.includes('not found')) {
|
||||
set.status = 404;
|
||||
return { error: error.message };
|
||||
return { error: message };
|
||||
}
|
||||
|
||||
set.status = 500;
|
||||
return { error: 'Internal server error', details: error.message };
|
||||
return { error: 'Internal server error', details: message };
|
||||
})
|
||||
|
||||
.listen(process.env.PORT || 3000);
|
||||
|
||||
20
src/middleware/auth.ts
Normal file
20
src/middleware/auth.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Elysia } from 'elysia';
|
||||
import { auth, type User } from '../lib/auth';
|
||||
|
||||
/**
|
||||
* Auth middleware plugin - adds `user` to the Elysia context.
|
||||
* Import and `.use(authMiddleware)` in route files that need authentication.
|
||||
*/
|
||||
export const authMiddleware = new Elysia({ name: 'auth-middleware' })
|
||||
.derive({ as: 'scoped' }, async ({ request, set }): Promise<{ user: User }> => {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
set.status = 401;
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
return { user: session.user as User };
|
||||
});
|
||||
@@ -44,7 +44,7 @@ function checkRateLimit(key: string, config: RateLimitConfig): { allowed: boolea
|
||||
function getClientIP(request: Request): string {
|
||||
// Check common proxy headers
|
||||
const forwarded = request.headers.get('x-forwarded-for');
|
||||
if (forwarded) return forwarded.split(',')[0].trim();
|
||||
if (forwarded) return forwarded.split(',')[0]?.trim() ?? '127.0.0.1';
|
||||
const realIp = request.headers.get('x-real-ip');
|
||||
if (realIp) return realIp;
|
||||
return '127.0.0.1';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, events, communications, interactions } from '../db/schema';
|
||||
@@ -14,6 +15,7 @@ export interface ActivityItem {
|
||||
}
|
||||
|
||||
export const activityRoutes = new Elysia({ prefix: '/clients' })
|
||||
.use(authMiddleware)
|
||||
// Get activity timeline for a client
|
||||
.get('/:id/activity', async ({ params, user }: { params: { id: string }; user: User }) => {
|
||||
// Verify client belongs to user
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { users, invites, passwordResetTokens } from '../db/schema';
|
||||
@@ -6,6 +7,7 @@ import { auth } from '../lib/auth';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const adminRoutes = new Elysia({ prefix: '/admin' })
|
||||
.use(authMiddleware)
|
||||
// Admin guard — all routes in this group require admin role
|
||||
.onBeforeHandle(({ user, set }: { user: User; set: any }) => {
|
||||
if ((user as any).role !== 'admin') {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { auditLogs, users } from '../db/schema';
|
||||
@@ -5,6 +6,7 @@ import { eq, desc, and, gte, lte, ilike, or, sql } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const auditLogRoutes = new Elysia({ prefix: '/audit-logs' })
|
||||
.use(authMiddleware)
|
||||
// Admin guard
|
||||
.onBeforeHandle(({ user, set }: { user: User; set: any }) => {
|
||||
if ((user as any).role !== 'admin') {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, events } from '../db/schema';
|
||||
@@ -84,6 +85,7 @@ const clientSchema = t.Object({
|
||||
const updateClientSchema = t.Partial(clientSchema);
|
||||
|
||||
export const clientRoutes = new Elysia({ prefix: '/clients' })
|
||||
.use(authMiddleware)
|
||||
// List clients with optional search and pagination
|
||||
.get('/', async ({ query, user }: { query: { search?: string; tag?: string; page?: string; limit?: string }; user: User }) => {
|
||||
let baseQuery = db.select().from(clients).where(eq(clients.userId, user.id));
|
||||
@@ -179,7 +181,9 @@ export const clientRoutes = new Elysia({ prefix: '/clients' })
|
||||
.returning();
|
||||
|
||||
// Auto-sync birthday/anniversary events
|
||||
if (client) {
|
||||
await syncClientEvents(user.id, client);
|
||||
}
|
||||
|
||||
return client;
|
||||
}, {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, communications, userProfiles } from '../db/schema';
|
||||
@@ -8,6 +9,7 @@ import type { User } from '../lib/auth';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
export const emailRoutes = new Elysia({ prefix: '/emails' })
|
||||
.use(authMiddleware)
|
||||
// Generate email for a client
|
||||
.post('/generate', async ({ body, user }: {
|
||||
body: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { events, clients } from '../db/schema';
|
||||
@@ -5,6 +6,7 @@ import { eq, and, gte, lte, sql } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const eventRoutes = new Elysia({ prefix: '/events' })
|
||||
.use(authMiddleware)
|
||||
// List events with optional filters
|
||||
.get('/', async ({ query, user }: {
|
||||
query: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, events } from '../db/schema';
|
||||
@@ -160,6 +161,7 @@ async function syncClientEvents(userId: string, client: { id: string; firstName:
|
||||
}
|
||||
|
||||
export const importRoutes = new Elysia({ prefix: '/clients' })
|
||||
.use(authMiddleware)
|
||||
// Preview CSV - returns headers and auto-mapped columns + sample rows
|
||||
.post('/import/preview', async ({ body, user }: { body: { file: File }; user: User }) => {
|
||||
const text = await body.file.text();
|
||||
@@ -211,6 +213,7 @@ export const importRoutes = new Elysia({ prefix: '/clients' })
|
||||
|
||||
for (let i = 0; i < dataRows.length; i++) {
|
||||
const row = dataRows[i];
|
||||
if (!row) continue;
|
||||
try {
|
||||
const record: Record<string, any> = {};
|
||||
|
||||
@@ -260,12 +263,14 @@ export const importRoutes = new Elysia({ prefix: '/clients' })
|
||||
.returning();
|
||||
|
||||
// Sync events
|
||||
if (client) {
|
||||
await syncClientEvents(user.id, {
|
||||
id: client.id,
|
||||
firstName: client.firstName,
|
||||
birthday: client.birthday,
|
||||
anniversary: client.anniversary,
|
||||
});
|
||||
}
|
||||
|
||||
results.imported++;
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, events } from '../db/schema';
|
||||
@@ -5,6 +6,7 @@ import { eq, and, sql, lte, gte, isNull, or } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const insightsRoutes = new Elysia({ prefix: '/insights' })
|
||||
.use(authMiddleware)
|
||||
.get('/', async ({ user }: { user: User }) => {
|
||||
const now = new Date();
|
||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { interactions, clients } from '../db/schema';
|
||||
@@ -5,6 +6,7 @@ import { eq, and, desc } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const interactionRoutes = new Elysia()
|
||||
.use(authMiddleware)
|
||||
// List interactions for a client
|
||||
.get('/clients/:clientId/interactions', async ({ params, user }: { params: { clientId: string }; user: User }) => {
|
||||
// Verify client belongs to user
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, interactions, communications, events, clientNotes } from '../db/schema';
|
||||
@@ -6,6 +7,7 @@ import type { User } from '../lib/auth';
|
||||
import { generateMeetingPrep } from '../services/ai';
|
||||
|
||||
export const meetingPrepRoutes = new Elysia()
|
||||
.use(authMiddleware)
|
||||
// Get meeting prep for a client
|
||||
.get('/clients/:id/meeting-prep', async ({ params, user, query }: {
|
||||
params: { id: string };
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients } from '../db/schema';
|
||||
@@ -23,6 +24,7 @@ function toClientProfile(c: typeof clients.$inferSelect): ClientProfile {
|
||||
}
|
||||
|
||||
export const networkRoutes = new Elysia({ prefix: '/network' })
|
||||
.use(authMiddleware)
|
||||
// Get all network matches for the user's clients
|
||||
.get('/matches', async (ctx) => {
|
||||
const user = (ctx as any).user;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clientNotes, clients } from '../db/schema';
|
||||
@@ -5,6 +6,7 @@ import { eq, and, desc } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const notesRoutes = new Elysia({ prefix: '/clients/:clientId/notes' })
|
||||
.use(authMiddleware)
|
||||
// List notes for a client
|
||||
.get('/', async ({ params, user }: { params: { clientId: string }; user: User }) => {
|
||||
// Verify client belongs to user
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { notifications, clients } from '../db/schema';
|
||||
@@ -5,6 +6,7 @@ import { eq, and, desc, sql } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const notificationRoutes = new Elysia({ prefix: '/notifications' })
|
||||
.use(authMiddleware)
|
||||
// List notifications
|
||||
.get('/', async ({ query, user }: { query: { limit?: string; unreadOnly?: string }; user: User }) => {
|
||||
const limit = query.limit ? parseInt(query.limit) : 50;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { users, userProfiles, accounts } from '../db/schema';
|
||||
@@ -6,6 +7,7 @@ import type { User } from '../lib/auth';
|
||||
import { logAudit, getRequestMeta } from '../services/audit';
|
||||
|
||||
export const profileRoutes = new Elysia({ prefix: '/profile' })
|
||||
.use(authMiddleware)
|
||||
// Get current user's profile
|
||||
.get('/', async ({ user }: { user: User }) => {
|
||||
// Get user and profile
|
||||
@@ -159,8 +161,9 @@ export const profileRoutes = new Elysia({ prefix: '/profile' })
|
||||
.where(eq(userProfiles.userId, user.id))
|
||||
.limit(1);
|
||||
|
||||
const tone = (body.tone || 'friendly') as 'formal' | 'friendly' | 'casual';
|
||||
const style = {
|
||||
tone: body.tone || 'friendly',
|
||||
tone,
|
||||
greeting: body.greeting || '',
|
||||
signoff: body.signoff || '',
|
||||
writingSamples: (body.writingSamples || []).slice(0, 3),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients, events, communications } from '../db/schema';
|
||||
@@ -5,6 +6,7 @@ import { eq, and, sql, gte, lte, count, desc } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const reportsRoutes = new Elysia()
|
||||
.use(authMiddleware)
|
||||
// Analytics overview
|
||||
.get('/reports/overview', async ({ user }: { user: User }) => {
|
||||
const userId = user.id;
|
||||
@@ -84,21 +86,21 @@ export const reportsRoutes = new Elysia()
|
||||
|
||||
return {
|
||||
clients: {
|
||||
total: totalClients.count,
|
||||
newThisMonth: newClientsMonth.count,
|
||||
newThisWeek: newClientsWeek.count,
|
||||
contactedRecently: contactedRecently.count,
|
||||
neverContacted: neverContacted.count,
|
||||
total: totalClients?.count ?? 0,
|
||||
newThisMonth: newClientsMonth?.count ?? 0,
|
||||
newThisWeek: newClientsWeek?.count ?? 0,
|
||||
contactedRecently: contactedRecently?.count ?? 0,
|
||||
neverContacted: neverContacted?.count ?? 0,
|
||||
},
|
||||
emails: {
|
||||
total: totalEmails.count,
|
||||
sent: emailsSent.count,
|
||||
draft: emailsDraft.count,
|
||||
sentLast30Days: emailsRecent.count,
|
||||
total: totalEmails?.count ?? 0,
|
||||
sent: emailsSent?.count ?? 0,
|
||||
draft: emailsDraft?.count ?? 0,
|
||||
sentLast30Days: emailsRecent?.count ?? 0,
|
||||
},
|
||||
events: {
|
||||
total: totalEvents.count,
|
||||
upcoming30Days: upcomingEvents.count,
|
||||
total: totalEvents?.count ?? 0,
|
||||
upcoming30Days: upcomingEvents?.count ?? 0,
|
||||
},
|
||||
};
|
||||
})
|
||||
@@ -406,11 +408,11 @@ export const reportsRoutes = new Elysia()
|
||||
});
|
||||
}
|
||||
|
||||
if (draftCount.count > 0) {
|
||||
if ((draftCount?.count ?? 0) > 0) {
|
||||
notifications.push({
|
||||
id: 'drafts',
|
||||
type: 'drafts' as const,
|
||||
title: `${draftCount.count} draft email${draftCount.count > 1 ? 's' : ''} pending`,
|
||||
title: `${draftCount?.count ?? 0} draft email${(draftCount?.count ?? 0) > 1 ? 's' : ''} pending`,
|
||||
description: 'Review and send your drafted emails',
|
||||
date: new Date().toISOString(),
|
||||
link: '/emails',
|
||||
@@ -434,7 +436,7 @@ export const reportsRoutes = new Elysia()
|
||||
overdue: overdueEvents.length,
|
||||
upcoming: upcomingEvents.length,
|
||||
stale: staleClients.length,
|
||||
drafts: draftCount.count,
|
||||
drafts: draftCount?.count ?? 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clientSegments, clients } from '../db/schema';
|
||||
@@ -77,6 +78,7 @@ function buildClientConditions(filters: SegmentFilters, userId: string) {
|
||||
}
|
||||
|
||||
export const segmentRoutes = new Elysia({ prefix: '/segments' })
|
||||
.use(authMiddleware)
|
||||
// List saved segments
|
||||
.get('/', async ({ user }: { user: User }) => {
|
||||
return db.select()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { clients } from '../db/schema';
|
||||
@@ -5,6 +6,7 @@ import { eq, sql } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const tagRoutes = new Elysia({ prefix: '/tags' })
|
||||
.use(authMiddleware)
|
||||
// GET /api/tags - all unique tags with client counts
|
||||
.get('/', async ({ user }: { user: User }) => {
|
||||
const allClients = await db.select({ tags: clients.tags })
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { Elysia, t } from 'elysia';
|
||||
import { db } from '../db';
|
||||
import { emailTemplates } from '../db/schema';
|
||||
@@ -5,6 +6,7 @@ import { eq, and, desc, sql } from 'drizzle-orm';
|
||||
import type { User } from '../lib/auth';
|
||||
|
||||
export const templateRoutes = new Elysia({ prefix: '/templates' })
|
||||
.use(authMiddleware)
|
||||
// List templates
|
||||
.get('/', async ({ query, user }: { query: { category?: string }; user: User }) => {
|
||||
let conditions = [eq(emailTemplates.userId, user.id)];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PgBoss } from 'pg-boss';
|
||||
import type { Job } from 'pg-boss';
|
||||
import { db } from '../db';
|
||||
import { events, notifications, clients, users } from '../db/schema';
|
||||
import { eq, and, gte, lte, sql } from 'drizzle-orm';
|
||||
@@ -22,8 +23,8 @@ export async function initJobQueue(): Promise<PgBoss> {
|
||||
console.log('✅ pg-boss job queue started');
|
||||
|
||||
// Register job handlers
|
||||
await boss.work('check-upcoming-events', { teamConcurrency: 1 }, checkUpcomingEvents);
|
||||
await boss.work('send-event-reminder', { teamConcurrency: 5 }, sendEventReminder);
|
||||
await boss.work('check-upcoming-events', { localConcurrency: 1 }, checkUpcomingEvents);
|
||||
await boss.work('send-event-reminder', { localConcurrency: 5 }, sendEventReminder);
|
||||
|
||||
// Schedule daily check at 8am UTC
|
||||
await boss.schedule('check-upcoming-events', '0 8 * * *', {}, {
|
||||
@@ -39,7 +40,7 @@ export function getJobQueue(): PgBoss | null {
|
||||
}
|
||||
|
||||
// Job: Check upcoming events and create notifications
|
||||
async function checkUpcomingEvents(job: PgBoss.Job) {
|
||||
async function checkUpcomingEvents(jobs: Job[]) {
|
||||
console.log(`[jobs] Running checkUpcomingEvents at ${new Date().toISOString()}`);
|
||||
|
||||
try {
|
||||
@@ -118,14 +119,18 @@ async function checkUpcomingEvents(job: PgBoss.Job) {
|
||||
}
|
||||
|
||||
// Job: Send email reminder to advisor
|
||||
async function sendEventReminder(job: PgBoss.Job<{
|
||||
interface EventReminderData {
|
||||
userId: string;
|
||||
eventId: string;
|
||||
clientId: string;
|
||||
eventTitle: string;
|
||||
clientName: string;
|
||||
daysUntil: number;
|
||||
}>) {
|
||||
}
|
||||
|
||||
async function sendEventReminder(jobs: Job<EventReminderData>[]) {
|
||||
const job = jobs[0];
|
||||
if (!job) return;
|
||||
const { userId, eventTitle, clientName, daysUntil } = job.data;
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user