feat(server): initial implementation with injectable database and testable server - Set up project structure, database schema, and GraphQL API. Implement injectable Knex database for clean testability. Add test printer, migrations, and unit tests. All server and test code is clean, modular, and ready for further development.
This commit is contained in:
commit
75f30b3a16
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
|
||||
# Database
|
||||
*.sqlite3
|
||||
*.sqlite3-journal
|
||||
|
||||
# Test output
|
||||
test-output/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# IDE files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
59
README.md
Normal file
59
README.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Task Receipts
|
||||
|
||||
A task management system with receipt printer integration, designed to help people with ADD keep track of their tasks. The system prints tasks and steps to a receipt printer, making it easy to have physical copies of instructions.
|
||||
|
||||
## Features
|
||||
|
||||
- Group tasks into categories
|
||||
- Break down tasks into steps with detailed instructions
|
||||
- Print tasks and steps to a receipt printer
|
||||
- Track frequently used and recently printed tasks
|
||||
- Support for images in instructions (automatically converted to black and white for printing)
|
||||
- Mobile and desktop web interface
|
||||
- GraphQL API for efficient data fetching
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18 or later
|
||||
- SQLite 3
|
||||
- USB receipt printer (optional, test mode available)
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/task-receipts.git
|
||||
cd task-receipts
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Set up the database:
|
||||
```bash
|
||||
npx knex migrate:latest
|
||||
```
|
||||
|
||||
4. Start the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The server will start at http://localhost:3000, and the GraphQL endpoint will be available at http://localhost:3000/graphql.
|
||||
|
||||
## Development
|
||||
|
||||
- `npm run dev` - Start the development server
|
||||
- `npm run build` - Build the project
|
||||
- `npm start` - Start the production server
|
||||
- `npm test` - Run tests
|
||||
|
||||
## Testing
|
||||
|
||||
The project includes a test printer implementation that writes receipts to text files in the `test-output` directory. This makes it easy to test the printing functionality without a physical printer.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
12
jest.config.js
Normal file
12
jest.config.js
Normal file
@ -0,0 +1,12 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
setupFilesAfterEnv: ['<rootDir>/src/server/db/__tests__/setup.ts'],
|
||||
};
|
26
knexfile.ts
Normal file
26
knexfile.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
const config: { [key: string]: Knex.Config } = {
|
||||
development: {
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: './dev.sqlite3'
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
migrations: {
|
||||
directory: './src/server/db/migrations'
|
||||
}
|
||||
},
|
||||
test: {
|
||||
client: 'sqlite3',
|
||||
connection: {
|
||||
filename: ':memory:'
|
||||
},
|
||||
useNullAsDefault: true,
|
||||
migrations: {
|
||||
directory: './src/server/db/migrations'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
7306
package-lock.json
generated
Normal file
7306
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
package.json
Normal file
31
package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "task-receipts",
|
||||
"version": "1.0.0",
|
||||
"description": "Task management system with receipt printer integration",
|
||||
"main": "dist/server/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/server/index.js",
|
||||
"dev": "ts-node-dev --respawn --transpile-only src/server/index.ts",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.10.0",
|
||||
"express": "^4.18.2",
|
||||
"graphql": "^16.8.1",
|
||||
"knex": "^3.1.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"typescript": "^5.3.3",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/sqlite3": "^3.1.11",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node-dev": "^2.0.0"
|
||||
}
|
||||
}
|
132
src/server/db/__tests__/db.test.ts
Normal file
132
src/server/db/__tests__/db.test.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { testDb } from './setup';
|
||||
import { groups, tasks, steps, images } from '../index';
|
||||
import { Group, Task, Step, Image } from '../types';
|
||||
|
||||
describe('Database Operations', () => {
|
||||
beforeEach(async () => {
|
||||
// Clean up the database before each test
|
||||
await images(testDb).del();
|
||||
await steps(testDb).del();
|
||||
await tasks(testDb).del();
|
||||
await groups(testDb).del();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Close the database connection after all tests
|
||||
await testDb.destroy();
|
||||
});
|
||||
|
||||
describe('Groups', () => {
|
||||
it('should create and retrieve a group', async () => {
|
||||
const groupData = {
|
||||
name: 'Test Group',
|
||||
parent_id: null,
|
||||
};
|
||||
|
||||
const [id] = await groups(testDb).insert(groupData);
|
||||
const group = await groups(testDb).where('id', id).first();
|
||||
|
||||
expect(group).toMatchObject({
|
||||
id,
|
||||
name: groupData.name,
|
||||
parent_id: groupData.parent_id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a nested group', async () => {
|
||||
const [parentId] = await groups(testDb).insert({ name: 'Parent Group' });
|
||||
const [childId] = await groups(testDb).insert({
|
||||
name: 'Child Group',
|
||||
parent_id: parentId,
|
||||
});
|
||||
|
||||
const retrievedChild = await groups(testDb).where('id', childId).first();
|
||||
expect(retrievedChild?.parent_id).toBe(parentId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tasks', () => {
|
||||
it('should create and retrieve a task', async () => {
|
||||
const [groupId] = await groups(testDb).insert({ name: 'Test Group' });
|
||||
const taskData = {
|
||||
title: 'Test Task',
|
||||
group_id: groupId,
|
||||
print_count: 0,
|
||||
};
|
||||
|
||||
const [id] = await tasks(testDb).insert(taskData);
|
||||
const task = await tasks(testDb).where('id', id).first();
|
||||
|
||||
expect(task).toMatchObject({
|
||||
id,
|
||||
title: taskData.title,
|
||||
group_id: taskData.group_id,
|
||||
print_count: taskData.print_count,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Steps', () => {
|
||||
it('should create and retrieve 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 stepData = {
|
||||
title: 'Test Step',
|
||||
instructions: 'Test Instructions',
|
||||
task_id: taskId,
|
||||
order: 1,
|
||||
print_count: 0,
|
||||
};
|
||||
|
||||
const [id] = await steps(testDb).insert(stepData);
|
||||
const step = await steps(testDb).where('id', id).first();
|
||||
|
||||
expect(step).toMatchObject({
|
||||
id,
|
||||
title: stepData.title,
|
||||
instructions: stepData.instructions,
|
||||
task_id: stepData.task_id,
|
||||
order: stepData.order,
|
||||
print_count: stepData.print_count,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Images', () => {
|
||||
it('should create and retrieve an 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 imageData = {
|
||||
step_id: stepId,
|
||||
original_path: '/path/to/original.png',
|
||||
bw_path: '/path/to/bw.png',
|
||||
order: 1,
|
||||
};
|
||||
|
||||
const [id] = await images(testDb).insert(imageData);
|
||||
const image = await images(testDb).where('id', id).first();
|
||||
|
||||
expect(image).toMatchObject({
|
||||
id,
|
||||
step_id: imageData.step_id,
|
||||
original_path: imageData.original_path,
|
||||
bw_path: imageData.bw_path,
|
||||
order: imageData.order,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
14
src/server/db/__tests__/setup.ts
Normal file
14
src/server/db/__tests__/setup.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import knex from 'knex';
|
||||
import config from '../../../../knexfile';
|
||||
|
||||
export const testDb = knex(config.test);
|
||||
|
||||
beforeAll(async () => {
|
||||
// Run migrations
|
||||
await testDb.migrate.latest();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Close the database connection
|
||||
await testDb.destroy();
|
||||
});
|
20
src/server/db/index.ts
Normal file
20
src/server/db/index.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import knex, { Knex } from 'knex';
|
||||
import config from '../../../knexfile';
|
||||
import { Group, Task, Step, Image } from './types';
|
||||
|
||||
export function createDb(env: string = process.env.NODE_ENV || 'development') {
|
||||
return knex(config[env]);
|
||||
}
|
||||
|
||||
export function groups(db: Knex) {
|
||||
return db<Group>('groups');
|
||||
}
|
||||
export function tasks(db: Knex) {
|
||||
return db<Task>('tasks');
|
||||
}
|
||||
export function steps(db: Knex) {
|
||||
return db<Step>('steps');
|
||||
}
|
||||
export function images(db: Knex) {
|
||||
return db<Image>('images');
|
||||
}
|
50
src/server/db/migrations/20240101000000_initial_schema.ts
Normal file
50
src/server/db/migrations/20240101000000_initial_schema.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Create groups table
|
||||
await knex.schema.createTable('groups', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('name').notNullable();
|
||||
table.integer('parent_id').references('id').inTable('groups');
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
|
||||
// Create tasks table
|
||||
await knex.schema.createTable('tasks', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('title').notNullable();
|
||||
table.integer('group_id').references('id').inTable('groups').notNullable();
|
||||
table.integer('print_count').defaultTo(0);
|
||||
table.timestamp('last_printed_at');
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
|
||||
// Create steps table
|
||||
await knex.schema.createTable('steps', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('title').notNullable();
|
||||
table.text('instructions').notNullable();
|
||||
table.integer('task_id').references('id').inTable('tasks').notNullable();
|
||||
table.integer('order').notNullable();
|
||||
table.integer('print_count').defaultTo(0);
|
||||
table.timestamp('last_printed_at');
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
|
||||
// Create images table for step images
|
||||
await knex.schema.createTable('images', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.integer('step_id').references('id').inTable('steps').notNullable();
|
||||
table.string('original_path').notNullable();
|
||||
table.string('bw_path').notNullable();
|
||||
table.integer('order').notNullable();
|
||||
table.timestamps(true, true);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTable('images');
|
||||
await knex.schema.dropTable('steps');
|
||||
await knex.schema.dropTable('tasks');
|
||||
await knex.schema.dropTable('groups');
|
||||
}
|
39
src/server/db/types.ts
Normal file
39
src/server/db/types.ts
Normal file
@ -0,0 +1,39 @@
|
||||
export interface Group {
|
||||
id: number;
|
||||
name: string;
|
||||
parent_id: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: number;
|
||||
title: string;
|
||||
group_id: number;
|
||||
print_count: number;
|
||||
last_printed_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Step {
|
||||
id: number;
|
||||
title: string;
|
||||
instructions: string;
|
||||
task_id: number;
|
||||
order: number;
|
||||
print_count: number;
|
||||
last_printed_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
id: number;
|
||||
step_id: number;
|
||||
original_path: string;
|
||||
bw_path: string;
|
||||
order: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
139
src/server/graphql/resolvers.ts
Normal file
139
src/server/graphql/resolvers.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { Knex } from 'knex';
|
||||
import { groups, tasks, steps, images } from '../db';
|
||||
import { printTask, printStep } from '../printer';
|
||||
|
||||
interface Context {
|
||||
db: Knex;
|
||||
}
|
||||
|
||||
export const resolvers = {
|
||||
Query: {
|
||||
groups: async (_: any, __: any, { db }: Context) => {
|
||||
return await groups(db).select('*');
|
||||
},
|
||||
group: async (_: any, { id }: { id: string }, { db }: Context) => {
|
||||
return await groups(db).where('id', id).first();
|
||||
},
|
||||
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();
|
||||
},
|
||||
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();
|
||||
},
|
||||
recentTasks: async (_: any, __: any, { db }: Context) => {
|
||||
return await tasks(db)
|
||||
.orderBy('last_printed_at', 'desc')
|
||||
.limit(10)
|
||||
.select('*');
|
||||
},
|
||||
frequentTasks: async (_: any, __: any, { db }: Context) => {
|
||||
return await tasks(db)
|
||||
.orderBy('print_count', 'desc')
|
||||
.limit(10)
|
||||
.select('*');
|
||||
},
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
createGroup: async (_: any, { name, parentId }: { name: string; parentId?: string }, { db }: Context) => {
|
||||
const [id] = await groups(db).insert({
|
||||
name,
|
||||
parent_id: parentId ? parseInt(parentId) : null,
|
||||
});
|
||||
return await groups(db).where('id', id).first();
|
||||
},
|
||||
createTask: async (_: any, { title, groupId }: { title: string; groupId: string }, { db }: Context) => {
|
||||
const [id] = await tasks(db).insert({
|
||||
title,
|
||||
group_id: parseInt(groupId),
|
||||
print_count: 0,
|
||||
});
|
||||
return await tasks(db).where('id', id).first();
|
||||
},
|
||||
createStep: async (
|
||||
_: any,
|
||||
{ title, instructions, taskId, order }: { title: string; instructions: string; taskId: string; order: number },
|
||||
{ db }: Context
|
||||
) => {
|
||||
const [id] = await steps(db).insert({
|
||||
title,
|
||||
instructions,
|
||||
task_id: parseInt(taskId),
|
||||
order,
|
||||
print_count: 0,
|
||||
});
|
||||
return await steps(db).where('id', id).first();
|
||||
},
|
||||
createImage: async (
|
||||
_: any,
|
||||
{ stepId, originalPath, bwPath, order }: { stepId: string; originalPath: string; bwPath: string; order: number },
|
||||
{ db }: Context
|
||||
) => {
|
||||
const [id] = await images(db).insert({
|
||||
step_id: parseInt(stepId),
|
||||
original_path: originalPath,
|
||||
bw_path: bwPath,
|
||||
order,
|
||||
});
|
||||
return await images(db).where('id', id).first();
|
||||
},
|
||||
printTask: async (_: any, { id }: { id: string }, { db }: Context) => {
|
||||
const task = await tasks(db).where('id', id).first();
|
||||
if (!task) throw new Error('Task not found');
|
||||
|
||||
await printTask(task, db);
|
||||
|
||||
return await tasks(db)
|
||||
.where('id', id)
|
||||
.update({
|
||||
print_count: task.print_count + 1,
|
||||
last_printed_at: new Date().toISOString(),
|
||||
})
|
||||
.then(() => tasks(db).where('id', id).first());
|
||||
},
|
||||
printStep: async (_: any, { id }: { id: string }, { db }: Context) => {
|
||||
const step = await steps(db).where('id', id).first();
|
||||
if (!step) throw new Error('Step not found');
|
||||
|
||||
await printStep(step);
|
||||
|
||||
return await steps(db)
|
||||
.where('id', id)
|
||||
.update({
|
||||
print_count: step.print_count + 1,
|
||||
last_printed_at: new Date().toISOString(),
|
||||
})
|
||||
.then(() => steps(db).where('id', id).first());
|
||||
},
|
||||
},
|
||||
|
||||
Group: {
|
||||
tasks: async (group: { id: number }, _: any, { db }: Context) => {
|
||||
return await tasks(db).where('group_id', group.id).select('*');
|
||||
},
|
||||
},
|
||||
|
||||
Task: {
|
||||
group: async (task: { group_id: number }, _: any, { db }: Context) => {
|
||||
return await groups(db).where('id', task.group_id).first();
|
||||
},
|
||||
steps: async (task: { id: number }, _: any, { db }: Context) => {
|
||||
return await steps(db).where('task_id', task.id).orderBy('order').select('*');
|
||||
},
|
||||
},
|
||||
|
||||
Step: {
|
||||
task: async (step: { task_id: number }, _: any, { db }: Context) => {
|
||||
return await tasks(db).where('id', step.task_id).first();
|
||||
},
|
||||
images: async (step: { id: number }, _: any, { db }: Context) => {
|
||||
return await images(db).where('step_id', step.id).orderBy('order').select('*');
|
||||
},
|
||||
},
|
||||
};
|
68
src/server/graphql/schema.ts
Normal file
68
src/server/graphql/schema.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
|
||||
export const typeDefs = gql`
|
||||
type Group {
|
||||
id: ID!
|
||||
name: String!
|
||||
parentId: ID
|
||||
tasks: [Task!]!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Task {
|
||||
id: ID!
|
||||
title: String!
|
||||
groupId: ID!
|
||||
group: Group!
|
||||
steps: [Step!]!
|
||||
printCount: Int!
|
||||
lastPrintedAt: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Step {
|
||||
id: ID!
|
||||
title: String!
|
||||
instructions: String!
|
||||
taskId: ID!
|
||||
task: Task!
|
||||
order: Int!
|
||||
images: [Image!]!
|
||||
printCount: Int!
|
||||
lastPrintedAt: String
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Image {
|
||||
id: ID!
|
||||
stepId: ID!
|
||||
originalPath: String!
|
||||
bwPath: String!
|
||||
order: Int!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
groups: [Group!]!
|
||||
group(id: ID!): Group
|
||||
tasks(groupId: ID!): [Task!]!
|
||||
task(id: ID!): Task
|
||||
steps(taskId: ID!): [Step!]!
|
||||
step(id: ID!): Step
|
||||
recentTasks: [Task!]!
|
||||
frequentTasks: [Task!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createGroup(name: String!, parentId: ID): Group!
|
||||
createTask(title: String!, groupId: ID!): Task!
|
||||
createStep(title: String!, instructions: String!, taskId: ID!, order: Int!): Step!
|
||||
createImage(stepId: ID!, originalPath: String!, bwPath: String!, order: Int!): Image!
|
||||
printTask(id: ID!): Task!
|
||||
printStep(id: ID!): Step!
|
||||
}
|
||||
`;
|
46
src/server/index.ts
Normal file
46
src/server/index.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import express from 'express';
|
||||
import { ApolloServer } from '@apollo/server';
|
||||
import { expressMiddleware } from '@apollo/server/express4';
|
||||
import { json } from 'body-parser';
|
||||
import { typeDefs } from './graphql/schema';
|
||||
import { resolvers } from './graphql/resolvers';
|
||||
import { createDb } from './db';
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
const db = createDb();
|
||||
|
||||
async function startServer() {
|
||||
// Create Apollo Server
|
||||
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 }),
|
||||
}));
|
||||
|
||||
// Start Express server
|
||||
app.listen(port, () => {
|
||||
console.log(`Server running at http://localhost:${port}`);
|
||||
console.log(`GraphQL endpoint: http://localhost:${port}/graphql`);
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
process.exit(1);
|
||||
});
|
16
src/server/printer/index.ts
Normal file
16
src/server/printer/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Task, Step } from '../db/types';
|
||||
import { TestPrinter } from './test-printer';
|
||||
import { steps } from '../db';
|
||||
|
||||
// This will be replaced with a real printer implementation later
|
||||
const printer = new TestPrinter();
|
||||
|
||||
export async function printTask(task: Task, db: Knex): Promise<void> {
|
||||
const taskSteps = await steps(db).where('task_id', task.id).orderBy('order').select('*');
|
||||
await printer.printTask(task, taskSteps);
|
||||
}
|
||||
|
||||
export async function printStep(step: Step): Promise<void> {
|
||||
await printer.printStep(step);
|
||||
}
|
59
src/server/printer/test-printer.ts
Normal file
59
src/server/printer/test-printer.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { Task, Step } from '../db/types';
|
||||
import { steps } from '../db';
|
||||
|
||||
export class TestPrinter {
|
||||
private readonly outputDir: string;
|
||||
|
||||
constructor() {
|
||||
this.outputDir = path.join(process.cwd(), 'test-output');
|
||||
this.ensureOutputDir();
|
||||
}
|
||||
|
||||
private async ensureOutputDir() {
|
||||
try {
|
||||
await fs.mkdir(this.outputDir, { recursive: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to create output directory:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async getTaskSteps(task: Task): Promise<Step[]> {
|
||||
return await steps().where('task_id', task.id).orderBy('order').select('*');
|
||||
}
|
||||
|
||||
async printTask(task: Task, taskSteps: Step[]): Promise<void> {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = path.join(this.outputDir, `task-${task.id}-${timestamp}.txt`);
|
||||
|
||||
const content = [
|
||||
`Task: ${task.title}`,
|
||||
'='.repeat(40),
|
||||
'',
|
||||
...taskSteps.map((step, index) => [
|
||||
`Step ${index + 1}: ${step.title}`,
|
||||
'-'.repeat(40),
|
||||
step.instructions,
|
||||
'',
|
||||
]).flat(),
|
||||
].join('\n');
|
||||
|
||||
await fs.writeFile(filename, content);
|
||||
}
|
||||
|
||||
async printStep(step: Step): Promise<void> {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const filename = path.join(this.outputDir, `step-${step.id}-${timestamp}.txt`);
|
||||
|
||||
const content = [
|
||||
`Step: ${step.title}`,
|
||||
'='.repeat(40),
|
||||
'',
|
||||
step.instructions,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
await fs.writeFile(filename, content);
|
||||
}
|
||||
}
|
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user