get the server working

This commit is contained in:
Sean Sube 2025-06-14 18:36:27 -05:00
parent 3d02f532d9
commit fd9f9e3d85
No known key found for this signature in database
GPG Key ID: 3EED7B957D362AF1
10 changed files with 311 additions and 53 deletions

View File

@ -19,6 +19,7 @@ module.exports = {
'src/**/*.{ts,js}',
'!**/__tests__/**',
'!**/node_modules/**',
'!src/index.ts',
'!src/printer/serial-printer.ts',
],
coverageThreshold: {

View File

@ -0,0 +1,126 @@
import { InMemoryRepository, InMemoryPrintHistoryRepository, InMemoryStepRepository } from '../in-memory-repository';
describe('InMemoryRepository', () => {
let repository: InMemoryRepository<{ id: number; name: string }>;
beforeEach(() => {
repository = new InMemoryRepository();
});
it('should create and find items', async () => {
const item = await repository.create({ name: 'test' });
expect(item.id).toBe(1);
expect(item.name).toBe('test');
const found = await repository.findById(item.id);
expect(found).toEqual(item);
});
it('should return null for non-existent id', async () => {
const found = await repository.findById(999);
expect(found).toBeNull();
});
it('should update items', async () => {
const item = await repository.create({ name: 'test' });
const success = await repository.update(item.id, { name: 'updated' });
expect(success).toBe(true);
const updated = await repository.findById(item.id);
expect(updated?.name).toBe('updated');
});
it('should return false when updating non-existent item', async () => {
const success = await repository.update(999, { name: 'updated' });
expect(success).toBe(false);
});
it('should delete items', async () => {
const item = await repository.create({ name: 'test' });
const success = await repository.delete(item.id);
expect(success).toBe(true);
const found = await repository.findById(item.id);
expect(found).toBeNull();
});
it('should return false when deleting non-existent item', async () => {
const success = await repository.delete(999);
expect(success).toBe(false);
});
it('should list all items', async () => {
const item1 = await repository.create({ name: 'test1' });
const item2 = await repository.create({ name: 'test2' });
const all = await repository.findAll();
expect(all).toHaveLength(2);
expect(all).toContainEqual(item1);
expect(all).toContainEqual(item2);
});
});
describe('InMemoryPrintHistoryRepository', () => {
let repository: InMemoryPrintHistoryRepository;
beforeEach(() => {
repository = new InMemoryPrintHistoryRepository();
});
it('should find print history by task id', async () => {
const history1 = await repository.create({ task_id: 1, user_id: 1, printed_at: new Date() });
const history2 = await repository.create({ task_id: 1, user_id: 2, printed_at: new Date() });
await repository.create({ task_id: 2, user_id: 1, printed_at: new Date() });
const taskHistory = await repository.findByTaskId(1);
expect(taskHistory).toHaveLength(2);
expect(taskHistory).toContainEqual(history1);
expect(taskHistory).toContainEqual(history2);
});
it('should find print history by step id', async () => {
const history1 = await repository.create({ step_id: 1, user_id: 1, printed_at: new Date() });
const history2 = await repository.create({ step_id: 1, user_id: 2, printed_at: new Date() });
await repository.create({ step_id: 2, user_id: 1, printed_at: new Date() });
const stepHistory = await repository.findByStepId(1);
expect(stepHistory).toHaveLength(2);
expect(stepHistory).toContainEqual(history1);
expect(stepHistory).toContainEqual(history2);
});
});
describe('InMemoryStepRepository', () => {
let repository: InMemoryStepRepository;
beforeEach(() => {
repository = new InMemoryStepRepository();
});
it('should find steps by task id', async () => {
const step1 = await repository.create({ task_id: 1, name: 'Step 1', instructions: 'Test', order: 1, print_count: 0 });
const step2 = await repository.create({ task_id: 1, name: 'Step 2', instructions: 'Test', order: 2, print_count: 0 });
await repository.create({ task_id: 2, name: 'Step 3', instructions: 'Test', order: 1, print_count: 0 });
const taskSteps = await repository.findByTaskId(1);
expect(taskSteps).toHaveLength(2);
expect(taskSteps).toContainEqual(step1);
expect(taskSteps).toContainEqual(step2);
});
it('should increment print count', async () => {
const step = await repository.create({ task_id: 1, name: 'Step 1', instructions: 'Test', order: 1, print_count: 0 });
const success = await repository.incrementPrintCount(step.id);
expect(success).toBe(true);
const updated = await repository.findById(step.id);
expect(updated?.print_count).toBe(1);
expect(updated?.last_printed_at).toBeInstanceOf(Date);
});
it('should return false when incrementing non-existent step', async () => {
const success = await repository.incrementPrintCount(999);
expect(success).toBe(false);
});
});

View File

@ -0,0 +1,76 @@
import { testDb } from '../../__tests__/setup';
import { TaskRepository } from '../task-repository';
import { Task } from '@shared/index';
describe('TaskRepository (integration)', () => {
let repository: TaskRepository;
let groupId: number;
beforeEach(async () => {
// Clean up tasks and groups before each test
await testDb('tasks').delete();
await testDb('groups').delete();
// Insert a group for foreign key
[groupId] = await testDb('groups').insert({ name: 'Test Group' });
repository = new TaskRepository(testDb);
});
afterAll(async () => {
await testDb.destroy();
});
it('should create and find tasks by group id', async () => {
const task1: Omit<Task, 'id' | 'created_at' | 'updated_at'> = {
name: 'Task 1',
group_id: groupId,
print_count: 0,
last_printed_at: undefined,
};
const task2: Omit<Task, 'id' | 'created_at' | 'updated_at'> = {
name: 'Task 2',
group_id: groupId,
print_count: 0,
last_printed_at: undefined,
};
await testDb('tasks').insert([task1, task2]);
const found = await repository.findByGroupId(groupId);
expect(found).toHaveLength(2);
expect(found.map(t => t.name)).toEqual(expect.arrayContaining(['Task 1', 'Task 2']));
});
it('should find recent tasks', async () => {
const now = new Date();
const task1 = { name: 'Recent 1', group_id: groupId, print_count: 0, last_printed_at: undefined, created_at: new Date(now.getTime() - 2000).toISOString(), updated_at: new Date(now.getTime() - 2000).toISOString() };
const task2 = { name: 'Recent 2', group_id: groupId, print_count: 0, last_printed_at: undefined, created_at: new Date(now.getTime() - 1000).toISOString(), updated_at: new Date(now.getTime() - 1000).toISOString() };
const task3 = { name: 'Recent 3', group_id: groupId, print_count: 0, last_printed_at: undefined, created_at: now.toISOString(), updated_at: now.toISOString() };
await testDb('tasks').insert([task1, task2, task3]);
const found = await repository.findRecent(2);
expect(found).toHaveLength(2);
// Should be ordered by created_at desc, so the most recent first
expect(found[0].name).toBe('Recent 3');
expect(found[1].name).toBe('Recent 2');
});
it('should find frequent tasks', async () => {
const task1 = { name: 'Frequent 1', group_id: groupId, print_count: 10, last_printed_at: undefined };
const task2 = { name: 'Frequent 2', group_id: groupId, print_count: 5, last_printed_at: undefined };
await testDb('tasks').insert([task1, task2]);
const found = await repository.findFrequent(1);
expect(found).toHaveLength(1);
expect(found[0].name).toBe('Frequent 1');
});
it('should increment print count', async () => {
const [id] = await testDb('tasks').insert({ name: 'Print Task', group_id: groupId, print_count: 0, last_printed_at: undefined });
const result = await repository.incrementPrintCount(id);
expect(result).toBe(true);
const updated = await testDb('tasks').where({ id }).first();
expect(updated.print_count).toBe(1);
expect(updated.last_printed_at).toBeTruthy();
});
it('should return false when incrementing non-existent task', async () => {
const result = await repository.incrementPrintCount(99999);
expect(result).toBe(false);
});
});

View File

@ -1,6 +1,6 @@
import { Knex } from 'knex';
export abstract class BaseRepository<T> {
export abstract class BaseRepository<T extends { created_at?: Date; updated_at?: Date }> {
protected tableName: string;
protected db: Knex;
@ -18,8 +18,16 @@ export abstract class BaseRepository<T> {
return result || null;
}
async create(data: Omit<T, 'id' | 'created_at' | 'updated_at'>): Promise<T> {
const [id] = await this.db(this.tableName).insert(data);
async create(data: Omit<T, 'id' | 'created_at' | 'updated_at'> & Partial<Pick<T, 'created_at' | 'updated_at'>>): Promise<T> {
const now = new Date();
const created_at = (data.created_at ?? now).toISOString();
const updated_at = (data.updated_at ?? now).toISOString();
const row = {
...data,
created_at,
updated_at,
};
const [id] = await this.db(this.tableName).insert(row);
return await this.findById(id) as T;
}

View File

@ -1,4 +1,3 @@
import { BaseRepository } from './base-repository';
import { PrintHistory, Step } from '@shared/index';
export class InMemoryRepository<T extends { id: number }> {

View File

@ -1,9 +1,8 @@
import { jest } from '@jest/globals';
import { Knex } from 'knex';
import { testDb } from '../../db/__tests__/setup';
import { resolvers } from '../resolvers';
import { GroupRepository, TaskRepository, StepRepository, UserRepository, NoteRepository, PrintHistoryRepository, ImageRepository } from '../../db/repositories';
import type { Printer, Task, Step } from '@shared/index';
import type { Printer } from '@shared/index';
describe('GraphQL Resolvers', () => {
const context = {
@ -222,6 +221,33 @@ describe('GraphQL Resolvers', () => {
expect(result).toHaveLength(0);
});
});
describe('recentTasks', () => {
it('should return recent tasks', async () => {
const group = await groupRepo.create({ name: 'Test Group' });
const oldTimestamp = new Date('2020-01-01T00:00:00.000Z');
const newTimestamp = new Date('2021-01-01T00:00:00.000Z');
console.log('timestamps', oldTimestamp, newTimestamp);
await taskRepo.create({ name: 'Old Task', group_id: group.id, print_count: 0, created_at: oldTimestamp, updated_at: oldTimestamp });
await taskRepo.create({ name: 'New Task', group_id: group.id, print_count: 0, created_at: newTimestamp, updated_at: newTimestamp });
const result = await resolvers.Query.recentTasks(null, null, context);
console.log('result', JSON.stringify(result, null, 2));
expect(result.length).toBeGreaterThanOrEqual(2);
expect(result[0].name).toBe('New Task');
expect(result[1].name).toBe('Old Task');
});
});
describe('frequentTasks', () => {
it('should return frequent tasks', async () => {
const group = await groupRepo.create({ name: 'Test Group' });
await taskRepo.create({ name: 'Less Frequent', group_id: group.id, print_count: 1 });
await taskRepo.create({ name: 'More Frequent', group_id: group.id, print_count: 10 });
const result = await resolvers.Query.frequentTasks(null, null, context);
expect(result.length).toBeGreaterThanOrEqual(2);
expect(result[0].name).toBe('More Frequent');
});
});
});
describe('Mutations', () => {

View File

@ -1,5 +1,6 @@
import { Knex } from 'knex';
import { GroupRepository, TaskRepository, StepRepository, UserRepository, NoteRepository, PrintHistoryRepository, ImageRepository } from '../db/repositories';
import { printTask, printStep } from '../printer/helpers';
import type { Printer } from '@shared/index';
interface Context {
@ -148,44 +149,16 @@ export const resolvers = {
});
},
printTask: async (_: any, { id, userId }: { id: string; userId: string }, { db, printer }: Context) => {
const taskRepo = new TaskRepository(db);
const userRepo = new UserRepository(db);
const printHistoryRepo = new PrintHistoryRepository(db);
const task = await taskRepo.findById(parseInt(id));
if (!task) throw new Error('Task not found');
const user = await userRepo.findById(parseInt(userId));
if (!user) throw new Error('User not found');
await printer.printTask(task, db);
await printHistoryRepo.create({
user_id: parseInt(userId),
task_id: parseInt(id),
printed_at: new Date(),
});
await taskRepo.update(parseInt(id), {
print_count: (task.print_count || 0) + 1,
last_printed_at: new Date(),
});
return await taskRepo.findById(parseInt(id));
return await printTask(parseInt(id), parseInt(userId), db, printer);
},
printStep: async (_: any, { id, userId }: { id: string; userId: string }, { db, printer }: Context) => {
const stepRepo = new StepRepository(db);
const userRepo = new UserRepository(db);
const printHistoryRepo = new PrintHistoryRepository(db);
const step = await stepRepo.findById(parseInt(id));
if (!step) throw new Error('Step not found');
const user = await userRepo.findById(parseInt(userId));
if (!user) throw new Error('User not found');
await printer.printStep(step, db);
await printHistoryRepo.create({
user_id: parseInt(userId),
step_id: parseInt(id),
printed_at: new Date(),
});
await stepRepo.update(parseInt(id), {
print_count: (step.print_count || 0) + 1,
last_printed_at: new Date(),
});
return await stepRepo.findById(parseInt(id));
return await printStep(parseInt(id), parseInt(userId), db, printer);
},
},

View File

@ -7,6 +7,7 @@ import { resolvers } from './graphql/resolvers';
import { createDb } from './db';
import logger from './logger';
import { createPrinter } from './printer';
import { PrintHistoryRepository, StepRepository } from './db/repositories';
import cors from 'cors';
const app = express();
@ -14,7 +15,9 @@ const port = process.env.PORT || 4000;
async function startServer() {
const db = createDb();
const printer = createPrinter();
const printHistoryRepo = new PrintHistoryRepository(db);
const stepRepo = new StepRepository(db);
const printer = createPrinter(printHistoryRepo, stepRepo);
const server = new ApolloServer({
typeDefs,

View File

@ -0,0 +1,61 @@
import { Knex } from 'knex';
import { Printer, Task, Step } from '@shared/index';
import { TaskRepository, StepRepository, PrintHistoryRepository } from '../db/repositories';
export async function printTask(
taskId: number,
userId: number,
db: Knex,
printer: Printer
): Promise<Task> {
const taskRepo = new TaskRepository(db);
const printHistoryRepo = new PrintHistoryRepository(db);
const task = await taskRepo.findById(taskId);
if (!task) throw new Error('Task not found');
await printer.printTask(task, db);
await printHistoryRepo.create({
user_id: userId,
task_id: taskId,
printed_at: new Date(),
});
await taskRepo.update(taskId, {
print_count: (task.print_count || 0) + 1,
last_printed_at: new Date(),
});
const updatedTask = await taskRepo.findById(taskId);
if (!updatedTask) throw new Error('Task not found after update');
return updatedTask;
}
export async function printStep(
stepId: number,
userId: number,
db: Knex,
printer: Printer
): Promise<Step> {
const stepRepo = new StepRepository(db);
const printHistoryRepo = new PrintHistoryRepository(db);
const step = await stepRepo.findById(stepId);
if (!step) throw new Error('Step not found');
await printer.printStep(step, db);
await printHistoryRepo.create({
user_id: userId,
step_id: stepId,
printed_at: new Date(),
});
await stepRepo.update(stepId, {
print_count: (step.print_count || 0) + 1,
last_printed_at: new Date(),
});
const updatedStep = await stepRepo.findById(stepId);
if (!updatedStep) throw new Error('Step not found after update');
return updatedStep;
}

View File

@ -1,5 +1,4 @@
import { Knex } from 'knex';
import { Task, Step, Printer } from '@shared/index';
import { Printer } from '@shared/index';
import { TestPrinter } from './test-printer';
import { SerialPrinter } from './serial-printer';
import logger from '../logger';
@ -15,19 +14,5 @@ export function createPrinter(printHistoryRepo: PrintHistoryRepository, stepRepo
}
}
export async function printTask(task: Task, db: Knex): Promise<void> {
const printHistoryRepo = new PrintHistoryRepository(db);
const stepRepo = new StepRepository(db);
const printer = createPrinter(printHistoryRepo, stepRepo);
await printer.printTask(task, db);
}
export async function printStep(step: Step, db: Knex): Promise<void> {
const printHistoryRepo = new PrintHistoryRepository(db);
const stepRepo = new StepRepository(db);
const printer = createPrinter(printHistoryRepo, stepRepo);
await printer.printStep(step, db);
}
export { SerialPrinter } from './serial-printer';
export { TestPrinter } from './test-printer';