diff --git a/src/server/graphql/__tests__/resolvers.test.ts b/src/server/graphql/__tests__/resolvers.test.ts new file mode 100644 index 0000000..d86278d --- /dev/null +++ b/src/server/graphql/__tests__/resolvers.test.ts @@ -0,0 +1,836 @@ +import { jest } from '@jest/globals'; +import { testDb } from '../../db/__tests__/setup'; +import { resolvers } from '../resolvers'; +import { groups, tasks, steps, images, users, notes, printHistory } from '../../db'; +import type { Printer } from '../../printer/types'; + +const mockPrinter: Printer = { + printTask: jest.fn(async () => {}), + printStep: jest.fn(async () => {}), +}; + +describe('GraphQL Resolvers', () => { + const context = { db: testDb, printer: mockPrinter }; + + beforeEach(async () => { + // Clean up the database before each test + await testDb('print_history').del(); + await testDb('notes').del(); + await testDb('images').del(); + await testDb('steps').del(); + await testDb('tasks').del(); + await testDb('groups').del(); + await testDb('users').del(); + + // Reset mocks + jest.clearAllMocks(); + }); + + describe('Queries', () => { + describe('groups', () => { + it('should return all groups', async () => { + // Create test groups + await groups(testDb).insert({ name: 'Group 1' }); + await groups(testDb).insert({ name: 'Group 2' }); + + const result = await resolvers.Query.groups(null, null, context); + expect(result).toHaveLength(2); + expect(result[0]?.name).toBe('Group 1'); + expect(result[1]?.name).toBe('Group 2'); + }); + + it('should return a specific group', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const result = await resolvers.Query.group(null, { id: groupId.toString() }, context); + expect(result?.name).toBe('Test Group'); + }); + + it('should return null for non-existent group', async () => { + const result = await resolvers.Query.group(null, { id: '999' }, context); + expect(result).toBeNull(); + }); + }); + + describe('tasks', () => { + it('should return tasks for a group', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + await tasks(testDb).insert({ title: 'Task 1', group_id: groupId }); + await tasks(testDb).insert({ title: 'Task 2', group_id: groupId }); + + const result = await resolvers.Query.tasks(null, { groupId: groupId.toString() }, context); + expect(result).toHaveLength(2); + expect(result[0]?.title).toBe('Task 1'); + expect(result[1]?.title).toBe('Task 2'); + }); + + it('should return empty array for non-existent group', async () => { + const result = await resolvers.Query.tasks(null, { groupId: '999' }, context); + expect(result).toHaveLength(0); + }); + + it('should return a specific task', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + + const result = await resolvers.Query.task(null, { id: taskId.toString() }, context); + expect(result?.title).toBe('Test Task'); + }); + + it('should return null for non-existent task', async () => { + const result = await resolvers.Query.task(null, { id: '999' }, context); + expect(result).toBeNull(); + }); + }); + + describe('steps', () => { + it('should return steps for a task', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + await steps(testDb).insert({ + title: 'Step 1', + instructions: 'Step 1', + task_id: taskId, + order: 1, + }); + await steps(testDb).insert({ + title: 'Step 2', + instructions: 'Step 2', + task_id: taskId, + order: 2, + }); + + const result = await resolvers.Query.steps(null, { taskId: taskId.toString() }, context); + expect(result).toHaveLength(2); + expect(result[0]?.title).toBe('Step 1'); + expect(result[1]?.title).toBe('Step 2'); + }); + + it('should return empty array for non-existent task', async () => { + const result = await resolvers.Query.steps(null, { taskId: '999' }, context); + expect(result).toHaveLength(0); + }); + + it('should return a specific step', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + + const result = await resolvers.Query.step(null, { id: stepId.toString() }, context); + expect(result?.title).toBe('Test Step'); + }); + + it('should return null for non-existent step', async () => { + const result = await resolvers.Query.step(null, { id: '999' }, context); + expect(result).toBeNull(); + }); + }); + + describe('users', () => { + it('should return all users', async () => { + await users(testDb).insert({ name: 'User 1' }); + await users(testDb).insert({ name: 'User 2' }); + + const result = await resolvers.Query.users(null, null, context); + expect(result).toHaveLength(2); + expect(result[0]?.name).toBe('User 1'); + expect(result[1]?.name).toBe('User 2'); + }); + + it('should return a specific user', async () => { + const [userId] = await users(testDb).insert({ name: 'Test User' }); + const result = await resolvers.Query.user(null, { id: userId.toString() }, context); + expect(result?.name).toBe('Test User'); + }); + + it('should return null for non-existent user', async () => { + const result = await resolvers.Query.user(null, { id: '999' }, context); + expect(result).toBeNull(); + }); + }); + + describe('notes', () => { + it('should return notes for a task', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + await notes(testDb).insert({ + content: 'Note 1', + task_id: taskId, + created_by: userId, + }); + await notes(testDb).insert({ + content: 'Note 2', + task_id: taskId, + created_by: userId, + }); + + const result = await resolvers.Query.notes(null, { taskId: taskId.toString() }, context); + expect(result).toHaveLength(2); + expect(result[0]?.content).toBe('Note 1'); + expect(result[1]?.content).toBe('Note 2'); + }); + + it('should return empty array for non-existent task', async () => { + const result = await resolvers.Query.notes(null, { taskId: '999' }, context); + expect(result).toHaveLength(0); + }); + + it('should return notes for a step', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + const [noteId] = await notes(testDb).insert({ + content: 'Test Note', + step_id: stepId, + created_by: userId, + }); + + const result = await resolvers.Query.notes(null, { stepId: stepId.toString() }, context); + expect(result).toHaveLength(1); + expect(result[0]?.content).toBe('Test Note'); + }); + + it('should return empty array for non-existent step', async () => { + const result = await resolvers.Query.notes(null, { stepId: '999' }, context); + expect(result).toHaveLength(0); + }); + }); + + describe('printHistory', () => { + it('should return print history for a task', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + await printHistory(testDb).insert({ + user_id: userId, + task_id: taskId, + printed_at: new Date().toISOString(), + }); + await printHistory(testDb).insert({ + user_id: userId, + task_id: taskId, + printed_at: new Date().toISOString(), + }); + + const result = await resolvers.Query.printHistory(null, { taskId: taskId.toString() }, context); + expect(result).toHaveLength(2); + }); + + it('should return empty array for non-existent task', async () => { + const result = await resolvers.Query.printHistory(null, { taskId: '999' }, context); + expect(result).toHaveLength(0); + }); + + it('should return print history for a step', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + await printHistory(testDb).insert({ + user_id: userId, + step_id: stepId, + printed_at: new Date().toISOString(), + }); + + const result = await resolvers.Query.printHistory(null, { stepId: stepId.toString() }, context); + expect(result).toHaveLength(1); + }); + + it('should return empty array for non-existent step', async () => { + const result = await resolvers.Query.printHistory(null, { stepId: '999' }, context); + expect(result).toHaveLength(0); + }); + }); + }); + + describe('Mutations', () => { + describe('createGroup', () => { + it('should create a new group', async () => { + const result = await resolvers.Mutation.createGroup(null, { name: 'Test Group' }, context); + expect(result?.name).toBe('Test Group'); + }); + + it('should create a nested group', async () => { + const [parentId] = await groups(testDb).insert({ name: 'Parent Group' }); + const result = await resolvers.Mutation.createGroup( + null, + { name: 'Child Group', parentId: parentId.toString() }, + context + ); + expect(result?.name).toBe('Child Group'); + expect(result?.parent_id).toBe(parentId); + }); + + it('should throw error for non-existent parent group', async () => { + await expect( + resolvers.Mutation.createGroup(null, { name: 'Child Group', parentId: '999' }, context) + ).rejects.toThrow(); + }); + }); + + describe('createTask', () => { + it('should create a new task', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const result = await resolvers.Mutation.createTask( + null, + { title: 'Test Task', groupId: groupId.toString() }, + context + ); + expect(result?.title).toBe('Test Task'); + expect(result?.group_id).toBe(groupId); + }); + + it('should throw error for non-existent group', async () => { + await expect( + resolvers.Mutation.createTask(null, { title: 'Test Task', groupId: '999' }, context) + ).rejects.toThrow(); + }); + }); + + describe('createStep', () => { + it('should create a new step', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const result = await resolvers.Mutation.createStep( + null, + { + title: 'Test Step', + instructions: 'Test Instructions', + taskId: taskId.toString(), + order: 1, + }, + context + ); + expect(result?.title).toBe('Test Step'); + expect(result?.task_id).toBe(taskId); + }); + + it('should throw error for non-existent task', async () => { + await expect( + resolvers.Mutation.createStep( + null, + { + title: 'Test Step', + instructions: 'Test Instructions', + taskId: '999', + order: 1, + }, + context + ) + ).rejects.toThrow(); + }); + }); + + describe('createImage', () => { + it('should create a new image', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + const result = await resolvers.Mutation.createImage( + null, + { + stepId: stepId.toString(), + originalPath: '/path/to/original.jpg', + bwPath: '/path/to/bw.jpg', + order: 1, + }, + context + ); + expect(result?.original_path).toBe('/path/to/original.jpg'); + expect(result?.bw_path).toBe('/path/to/bw.jpg'); + expect(result?.step_id).toBe(stepId); + }); + + it('should throw error for non-existent step', async () => { + await expect( + resolvers.Mutation.createImage( + null, + { + stepId: '999', + originalPath: '/path/to/original.jpg', + bwPath: '/path/to/bw.jpg', + order: 1, + }, + context + ) + ).rejects.toThrow(); + }); + }); + + describe('createUser', () => { + it('should create a new user', async () => { + const result = await resolvers.Mutation.createUser(null, { name: 'Test User' }, context); + expect(result?.name).toBe('Test User'); + }); + }); + + describe('createNote', () => { + it('should create a note for a task', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + const result = await resolvers.Mutation.createNote( + null, + { + content: 'Test Note', + taskId: taskId.toString(), + createdBy: userId.toString(), + }, + context + ); + expect(result?.content).toBe('Test Note'); + expect(result?.task_id).toBe(taskId); + expect(result?.created_by).toBe(userId); + }); + + it('should create a note for a step', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + const result = await resolvers.Mutation.createNote( + null, + { + content: 'Test Note', + stepId: stepId.toString(), + createdBy: userId.toString(), + }, + context + ); + expect(result?.content).toBe('Test Note'); + expect(result?.step_id).toBe(stepId); + expect(result?.created_by).toBe(userId); + }); + + it('should throw error for non-existent task', async () => { + const [userId] = await users(testDb).insert({ name: 'Test User' }); + await expect( + resolvers.Mutation.createNote( + null, + { + content: 'Test Note', + taskId: '999', + createdBy: userId.toString(), + }, + context + ) + ).rejects.toThrow(); + }); + + it('should throw error for non-existent step', async () => { + const [userId] = await users(testDb).insert({ name: 'Test User' }); + await expect( + resolvers.Mutation.createNote( + null, + { + content: 'Test Note', + stepId: '999', + createdBy: userId.toString(), + }, + context + ) + ).rejects.toThrow(); + }); + + it('should throw error for non-existent user', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + await expect( + resolvers.Mutation.createNote( + null, + { + content: 'Test Note', + taskId: taskId.toString(), + createdBy: '999', + }, + context + ) + ).rejects.toThrow(); + }); + }); + + describe('printTask', () => { + it('should print a task and update print history', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + + const result = await resolvers.Mutation.printTask( + null, + { id: taskId.toString(), userId: userId.toString() }, + context + ); + + expect(mockPrinter.printTask).toHaveBeenCalled(); + expect(result?.print_count).toBe(1); + expect(result?.last_printed_at).toBeDefined(); + + const history = await printHistory(testDb) + .where({ task_id: taskId, user_id: userId }) + .first(); + expect(history).toBeDefined(); + }); + + it('should throw error if task not found', async () => { + const [userId] = await users(testDb).insert({ name: 'Test User' }); + await expect( + resolvers.Mutation.printTask(null, { id: '999', userId: userId.toString() }, context) + ).rejects.toThrow('Task not found'); + }); + + it('should throw error if user not found', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + await expect( + resolvers.Mutation.printTask(null, { id: taskId.toString(), userId: '999' }, context) + ).rejects.toThrow('User not found'); + }); + }); + + describe('printStep', () => { + it('should print a step and update print history', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + + const result = await resolvers.Mutation.printStep( + null, + { id: stepId.toString(), userId: userId.toString() }, + context + ); + + expect(mockPrinter.printStep).toHaveBeenCalled(); + expect(result?.print_count).toBe(1); + expect(result?.last_printed_at).toBeDefined(); + + const history = await printHistory(testDb) + .where({ step_id: stepId, user_id: userId }) + .first(); + expect(history).toBeDefined(); + }); + + it('should throw error if step not found', async () => { + const [userId] = await users(testDb).insert({ name: 'Test User' }); + await expect( + resolvers.Mutation.printStep(null, { id: '999', userId: userId.toString() }, context) + ).rejects.toThrow('Step not found'); + }); + + it('should throw error if user not found', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + await expect( + resolvers.Mutation.printStep(null, { id: stepId.toString(), userId: '999' }, context) + ).rejects.toThrow('User not found'); + }); + }); + }); + + describe('Field Resolvers', () => { + describe('Group', () => { + it('should resolve tasks field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [task1Id] = await tasks(testDb).insert({ title: 'Task 1', group_id: groupId }); + const [task2Id] = await tasks(testDb).insert({ title: 'Task 2', group_id: groupId }); + + const group = await groups(testDb).where('id', groupId).first(); + if (!group) throw new Error('Group not found'); + const result = await resolvers.Group.tasks(group, null, context); + expect(result).toHaveLength(2); + expect(result[0]?.title).toBe('Task 1'); + expect(result[1]?.title).toBe('Task 2'); + }); + }); + + describe('Task', () => { + it('should resolve group field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + + const task = await tasks(testDb).where('id', taskId).first(); + if (!task) throw new Error('Task not found'); + const result = await resolvers.Task.group(task, null, context); + expect(result?.name).toBe('Test Group'); + }); + + it('should resolve steps field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [step1Id] = await steps(testDb).insert({ + title: 'Step 1', + instructions: 'Step 1', + task_id: taskId, + order: 1, + }); + const [step2Id] = await steps(testDb).insert({ + title: 'Step 2', + instructions: 'Step 2', + task_id: taskId, + order: 2, + }); + + const task = await tasks(testDb).where('id', taskId).first(); + if (!task) throw new Error('Task not found'); + const result = await resolvers.Task.steps(task, null, context); + expect(result).toHaveLength(2); + expect(result[0]?.title).toBe('Step 1'); + expect(result[1]?.title).toBe('Step 2'); + }); + + it('should resolve notes field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + const [note1Id] = await notes(testDb).insert({ + content: 'Note 1', + task_id: taskId, + created_by: userId, + }); + const [note2Id] = await notes(testDb).insert({ + content: 'Note 2', + task_id: taskId, + created_by: userId, + }); + + const task = await tasks(testDb).where('id', taskId).first(); + if (!task) throw new Error('Task not found'); + const result = await resolvers.Task.notes(task, null, context); + expect(result).toHaveLength(2); + expect(result[0]?.content).toBe('Note 1'); + expect(result[1]?.content).toBe('Note 2'); + }); + + it('should resolve printHistory field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + await printHistory(testDb).insert({ + user_id: userId, + task_id: taskId, + printed_at: new Date().toISOString(), + }); + await printHistory(testDb).insert({ + user_id: userId, + task_id: taskId, + printed_at: new Date().toISOString(), + }); + + const task = await tasks(testDb).where('id', taskId).first(); + if (!task) throw new Error('Task not found'); + const result = await resolvers.Task.printHistory(task, null, context); + expect(result).toHaveLength(2); + }); + }); + + describe('Step', () => { + it('should resolve task field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + + const step = await steps(testDb).where('id', stepId).first(); + if (!step) throw new Error('Step not found'); + const result = await resolvers.Step.task(step, null, context); + expect(result?.title).toBe('Test Task'); + }); + + it('should resolve images field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + const [image1Id] = await images(testDb).insert({ + step_id: stepId, + original_path: '/path/to/original1.jpg', + bw_path: '/path/to/bw1.jpg', + order: 1, + }); + const [image2Id] = await images(testDb).insert({ + step_id: stepId, + original_path: '/path/to/original2.jpg', + bw_path: '/path/to/bw2.jpg', + order: 2, + }); + + const step = await steps(testDb).where('id', stepId).first(); + if (!step) throw new Error('Step not found'); + const result = await resolvers.Step.images(step, null, context); + expect(result).toHaveLength(2); + expect(result[0]?.original_path).toBe('/path/to/original1.jpg'); + expect(result[1]?.original_path).toBe('/path/to/original2.jpg'); + }); + + it('should resolve notes field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + const [note1Id] = await notes(testDb).insert({ + content: 'Note 1', + step_id: stepId, + created_by: userId, + }); + const [note2Id] = await notes(testDb).insert({ + content: 'Note 2', + step_id: stepId, + created_by: userId, + }); + + const step = await steps(testDb).where('id', stepId).first(); + if (!step) throw new Error('Step not found'); + const result = await resolvers.Step.notes(step, null, context); + expect(result).toHaveLength(2); + expect(result[0]?.content).toBe('Note 1'); + expect(result[1]?.content).toBe('Note 2'); + }); + + it('should resolve printHistory field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + await printHistory(testDb).insert({ + user_id: userId, + step_id: stepId, + printed_at: new Date().toISOString(), + }); + + const history = await printHistory(testDb).where('step_id', stepId).first(); + if (!history) throw new Error('Print history not found'); + const result = await resolvers.PrintHistory.step(history, null, context); + expect(result?.title).toBe('Test Step'); + }); + }); + + describe('Note', () => { + it('should resolve createdBy field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + await notes(testDb).insert({ + content: 'Test Note', + task_id: taskId, + created_by: userId, + }); + + const note = await notes(testDb).where('task_id', taskId).first(); + if (!note) throw new Error('Note not found'); + const result = await resolvers.Note.createdBy(note, null, context); + expect(result?.name).toBe('Test User'); + }); + }); + + describe('PrintHistory', () => { + it('should resolve user field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + const [historyId] = await printHistory(testDb).insert({ + user_id: userId, + task_id: taskId, + printed_at: new Date().toISOString(), + }); + + const history = await printHistory(testDb).where('id', historyId).first(); + if (!history) throw new Error('Print history not found'); + const result = await resolvers.PrintHistory.user(history, null, context); + expect(result?.name).toBe('Test User'); + }); + + it('should resolve task field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + const [historyId] = await printHistory(testDb).insert({ + user_id: userId, + task_id: taskId, + printed_at: new Date().toISOString(), + }); + + const history = await printHistory(testDb).where('id', historyId).first(); + if (!history) throw new Error('Print history not found'); + const result = await resolvers.PrintHistory.task(history, null, context); + expect(result?.title).toBe('Test Task'); + }); + + it('should resolve step field', async () => { + const [groupId] = await groups(testDb).insert({ name: 'Test Group' }); + const [taskId] = await tasks(testDb).insert({ title: 'Test Task', group_id: groupId }); + const [stepId] = await steps(testDb).insert({ + title: 'Test Step', + instructions: 'Test Instructions', + task_id: taskId, + order: 1, + }); + const [userId] = await users(testDb).insert({ name: 'Test User' }); + await printHistory(testDb).insert({ + user_id: userId, + step_id: stepId, + printed_at: new Date().toISOString(), + }); + + const history = await printHistory(testDb).where('step_id', stepId).first(); + if (!history) throw new Error('Print history not found'); + const result = await resolvers.PrintHistory.step(history, null, context); + expect(result?.title).toBe('Test Step'); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/server/graphql/resolvers.ts b/src/server/graphql/resolvers.ts index 67685e2..dd0f364 100644 --- a/src/server/graphql/resolvers.ts +++ b/src/server/graphql/resolvers.ts @@ -1,9 +1,10 @@ import { Knex } from 'knex'; -import { groups, tasks, steps, images } from '../db'; -import { printTask, printStep } from '../printer'; +import { groups, tasks, steps, images, users, notes, printHistory } from '../db'; +import type { Printer } from '../printer'; interface Context { db: Knex; + printer: Printer; } export const resolvers = { @@ -12,19 +13,22 @@ export const resolvers = { return await groups(db).select('*'); }, group: async (_: any, { id }: { id: string }, { db }: Context) => { - return await groups(db).where('id', id).first(); + const group = await groups(db).where('id', id).first(); + return group || null; }, tasks: async (_: any, { groupId }: { groupId: string }, { db }: Context) => { return await tasks(db).where('group_id', groupId).select('*'); }, task: async (_: any, { id }: { id: string }, { db }: Context) => { - return await tasks(db).where('id', id).first(); + const task = await tasks(db).where('id', id).first(); + return task || null; }, steps: async (_: any, { taskId }: { taskId: string }, { db }: Context) => { return await steps(db).where('task_id', taskId).orderBy('order').select('*'); }, step: async (_: any, { id }: { id: string }, { db }: Context) => { - return await steps(db).where('id', id).first(); + const step = await steps(db).where('id', id).first(); + return step || null; }, recentTasks: async (_: any, __: any, { db }: Context) => { return await tasks(db) @@ -38,10 +42,42 @@ export const resolvers = { .limit(10) .select('*'); }, + users: async (_: any, __: any, { db }: Context) => { + return await users(db).select('*'); + }, + user: async (_: any, { id }: { id: string }, { db }: Context) => { + const user = await users(db).where('id', id).first(); + return user || null; + }, + notes: async (_: any, { taskId, stepId }: { taskId?: string; stepId?: string }, { db }: Context) => { + const query = notes(db); + if (taskId) { + query.where('task_id', taskId); + } + if (stepId) { + query.where('step_id', stepId); + } + return await query.select('*'); + }, + printHistory: async (_: any, { taskId, stepId }: { taskId?: string; stepId?: string }, { db }: Context) => { + const query = printHistory(db); + if (taskId) { + query.where('task_id', taskId); + } + if (stepId) { + query.where('step_id', stepId); + } + return await query.select('*'); + }, }, Mutation: { createGroup: async (_: any, { name, parentId }: { name: string; parentId?: string }, { db }: Context) => { + if (parentId) { + const parent = await groups(db).where('id', parentId).first(); + if (!parent) throw new Error('Parent group not found'); + } + const [id] = await groups(db).insert({ name, parent_id: parentId ? parseInt(parentId) : null, @@ -49,6 +85,9 @@ export const resolvers = { return await groups(db).where('id', id).first(); }, createTask: async (_: any, { title, groupId }: { title: string; groupId: string }, { db }: Context) => { + const group = await groups(db).where('id', groupId).first(); + if (!group) throw new Error('Group not found'); + const [id] = await tasks(db).insert({ title, group_id: parseInt(groupId), @@ -61,6 +100,9 @@ export const resolvers = { { title, instructions, taskId, order }: { title: string; instructions: string; taskId: string; order: number }, { db }: Context ) => { + const task = await tasks(db).where('id', taskId).first(); + if (!task) throw new Error('Task not found'); + const [id] = await steps(db).insert({ title, instructions, @@ -75,6 +117,9 @@ export const resolvers = { { stepId, originalPath, bwPath, order }: { stepId: string; originalPath: string; bwPath: string; order: number }, { db }: Context ) => { + const step = await steps(db).where('id', stepId).first(); + if (!step) throw new Error('Step not found'); + const [id] = await images(db).insert({ step_id: parseInt(stepId), original_path: originalPath, @@ -83,11 +128,51 @@ export const resolvers = { }); return await images(db).where('id', id).first(); }, - printTask: async (_: any, { id }: { id: string }, { db }: Context) => { + createUser: async (_: any, { name }: { name: string }, { db }: Context) => { + const [id] = await users(db).insert({ name }); + return await users(db).where('id', id).first(); + }, + createNote: async ( + _: any, + { content, taskId, stepId, createdBy }: { content: string; taskId?: string; stepId?: string; createdBy: string }, + { db }: Context + ) => { + const user = await users(db).where('id', createdBy).first(); + if (!user) throw new Error('User not found'); + + if (taskId) { + const task = await tasks(db).where('id', taskId).first(); + if (!task) throw new Error('Task not found'); + } + + if (stepId) { + const step = await steps(db).where('id', stepId).first(); + if (!step) throw new Error('Step not found'); + } + + const [id] = await notes(db).insert({ + content, + task_id: taskId ? parseInt(taskId) : null, + step_id: stepId ? parseInt(stepId) : null, + created_by: parseInt(createdBy), + }); + return await notes(db).where('id', id).first(); + }, + printTask: async (_: any, { id, userId }: { id: string; userId: string }, { db, printer }: Context) => { const task = await tasks(db).where('id', id).first(); if (!task) throw new Error('Task not found'); - await printTask(task, db); + const user = await users(db).where('id', userId).first(); + if (!user) throw new Error('User not found'); + + await printer.printTask(task, db); + + // Record print history + await printHistory(db).insert({ + user_id: parseInt(userId), + task_id: parseInt(id), + printed_at: new Date().toISOString(), + }); return await tasks(db) .where('id', id) @@ -97,11 +182,21 @@ export const resolvers = { }) .then(() => tasks(db).where('id', id).first()); }, - printStep: async (_: any, { id }: { id: string }, { db }: Context) => { + printStep: async (_: any, { id, userId }: { id: string; userId: string }, { db, printer }: Context) => { const step = await steps(db).where('id', id).first(); if (!step) throw new Error('Step not found'); - await printStep(step); + const user = await users(db).where('id', userId).first(); + if (!user) throw new Error('User not found'); + + await printer.printStep(step, db); + + // Record print history + await printHistory(db).insert({ + user_id: parseInt(userId), + step_id: parseInt(id), + printed_at: new Date().toISOString(), + }); return await steps(db) .where('id', id) @@ -121,19 +216,57 @@ export const resolvers = { Task: { group: async (task: { group_id: number }, _: any, { db }: Context) => { - return await groups(db).where('id', task.group_id).first(); + const group = await groups(db).where('id', task.group_id).first(); + return group || null; }, steps: async (task: { id: number }, _: any, { db }: Context) => { return await steps(db).where('task_id', task.id).orderBy('order').select('*'); }, + notes: async (task: { id: number }, _: any, { db }: Context) => { + return await notes(db).where('task_id', task.id).select('*'); + }, + printHistory: async (task: { id: number }, _: any, { db }: Context) => { + return await printHistory(db).where('task_id', task.id).select('*'); + }, }, Step: { task: async (step: { task_id: number }, _: any, { db }: Context) => { - return await tasks(db).where('id', step.task_id).first(); + const task = await tasks(db).where('id', step.task_id).first(); + return task || null; }, images: async (step: { id: number }, _: any, { db }: Context) => { return await images(db).where('step_id', step.id).orderBy('order').select('*'); }, + notes: async (step: { id: number }, _: any, { db }: Context) => { + return await notes(db).where('step_id', step.id).select('*'); + }, + printHistory: async (step: { id: number }, _: any, { db }: Context) => { + return await printHistory(db).where('step_id', step.id).select('*'); + }, + }, + + Note: { + createdBy: async (note: { created_by: number }, _: any, { db }: Context) => { + const user = await users(db).where('id', note.created_by).first(); + return user || null; + }, + }, + + PrintHistory: { + user: async (history: { user_id: number }, _: any, { db }: Context) => { + const user = await users(db).where('id', history.user_id).first(); + return user || null; + }, + task: async (history: { task_id: number | null }, _: any, { db }: Context) => { + if (!history.task_id) return null; + const task = await tasks(db).where('id', history.task_id).first(); + return task || null; + }, + step: async (history: { step_id: number | null }, _: any, { db }: Context) => { + if (!history.step_id) return null; + const step = await steps(db).where('id', history.step_id).first(); + return step || null; + }, }, }; \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index df9cd5b..2695b42 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -5,42 +5,54 @@ import { json } from 'body-parser'; import { typeDefs } from './graphql/schema'; import { resolvers } from './graphql/resolvers'; import { createDb } from './db'; +import logger from './logger'; +import { TestPrinter } from './printer'; const app = express(); -const port = process.env.PORT || 3000; -const db = createDb(); +const port = process.env.PORT || 4000; async function startServer() { - // Create Apollo Server + const db = createDb(); + const printer = new TestPrinter(); + const server = new ApolloServer({ typeDefs, resolvers, }); - // Start Apollo Server await server.start(); - // Apply middleware app.use(json()); - app.use('/graphql', expressMiddleware(server, { - context: async () => ({ db }), - })); + app.use( + '/graphql', + expressMiddleware(server, { + context: async () => ({ db, printer }), + }) + ); - // Start Express server - app.listen(port, () => { - console.log(`Server running at http://localhost:${port}`); - console.log(`GraphQL endpoint: http://localhost:${port}/graphql`); + const httpServer = app.listen(port, () => { + logger.info(`Server running at http://localhost:${port}/graphql`); + }); + + // Graceful shutdown + process.on('SIGTERM', () => { + logger.info('SIGTERM received. Shutting down gracefully...'); + httpServer.close(() => { + logger.info('Server closed'); + process.exit(0); + }); + }); + + process.on('SIGINT', () => { + logger.info('SIGINT received. Shutting down gracefully...'); + httpServer.close(() => { + logger.info('Server closed'); + process.exit(0); + }); }); } -// Handle graceful shutdown -process.on('SIGTERM', async () => { - console.log('SIGTERM received. Closing database connection...'); - await db.destroy(); - process.exit(0); -}); - startServer().catch((error) => { - console.error('Failed to start server:', error); + logger.error('Failed to start server:', error); process.exit(1); }); \ No newline at end of file diff --git a/src/server/printer/index.ts b/src/server/printer/index.ts index 565f471..93fc983 100644 --- a/src/server/printer/index.ts +++ b/src/server/printer/index.ts @@ -1,16 +1,14 @@ import { Knex } from 'knex'; import { Task, Step } from '../db/types'; -import { TestPrinter } from './test-printer'; -import { steps } from '../db'; +export { TestPrinter } from './test-printer'; // This will be replaced with a real printer implementation later const printer = new TestPrinter(); export async function printTask(task: Task, db: Knex): Promise { - const taskSteps = await steps(db).where('task_id', task.id).orderBy('order').select('*'); - await printer.printTask(task, taskSteps); + await printer.printTask(db, task); } -export async function printStep(step: Step): Promise { - await printer.printStep(step); +export async function printStep(step: Step, db: Knex): Promise { + await printer.printStep(db, step); } \ No newline at end of file diff --git a/src/server/printer/test-printer.ts b/src/server/printer/test-printer.ts index e5f475a..6735e15 100644 --- a/src/server/printer/test-printer.ts +++ b/src/server/printer/test-printer.ts @@ -2,8 +2,11 @@ import fs from 'fs/promises'; import path from 'path'; import { Task, Step } from '../db/types'; import { steps } from '../db'; +import { Knex } from 'knex'; +import logger from '../logger'; +import { Printer } from './index'; -export class TestPrinter { +export class TestPrinter implements Printer { private readonly outputDir: string; constructor() { @@ -15,15 +18,16 @@ export class TestPrinter { try { await fs.mkdir(this.outputDir, { recursive: true }); } catch (error) { - console.error('Failed to create output directory:', error); + logger.error('Failed to create output directory:', error); } } - async getTaskSteps(task: Task): Promise { - return await steps().where('task_id', task.id).orderBy('order').select('*'); + async getTaskSteps(db: Knex, task: Task): Promise { + return await steps(db).where('task_id', task.id).orderBy('order').select('*'); } - async printTask(task: Task, taskSteps: Step[]): Promise { + async printTask(task: Task, db: Knex): Promise { + const taskSteps = await this.getTaskSteps(db, task); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = path.join(this.outputDir, `task-${task.id}-${timestamp}.txt`); @@ -40,9 +44,11 @@ export class TestPrinter { ].join('\n'); await fs.writeFile(filename, content); + logger.info(`Printed task ${task.id} to ${filename}`); } - async printStep(step: Step): Promise { + async printStep(step: Step, db: Knex): Promise { + void db; // avoid unused parameter warning const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = path.join(this.outputDir, `step-${step.id}-${timestamp}.txt`); @@ -55,5 +61,6 @@ export class TestPrinter { ].join('\n'); await fs.writeFile(filename, content); + logger.info(`Printed step ${step.id} to ${filename}`); } } \ No newline at end of file