Skip to content

Commit

Permalink
feat: crud task according to workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
jean-michelet committed Aug 25, 2024
1 parent 3d165b2 commit d2336d0
Show file tree
Hide file tree
Showing 13 changed files with 625 additions and 41 deletions.
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,6 @@ To build the project:
npm run build
```

In dev mode, you can use:
```bash
npm run watch
```

### Start the server
In dev mode:
```bash
Expand Down
13 changes: 8 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
"test": "test"
},
"scripts": {
"build": "rm -rf dist && tsc",
"watch": "npm run build -- --watch",
"start": "npm run build && fastify start -l info dist/app.js",
"build": "tsc",
"watch": "tsc -w",
"dev": "npm run build && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch\" \"npm:dev:start\"",
"dev:start": "fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js",
"test": "npm run db:seed && tap --jobs=1 test/**/*",
"start": "fastify start -l info dist/app.js",
"dev": "fastify start -w -l info -P dist/app.js",
"standalone": "node --env-file=.env dist/server.js",
"lint": "eslint --ignore-pattern=dist",
"lint:fix": "npm run lint -- --fix",
Expand All @@ -35,7 +36,9 @@
"@fastify/swagger-ui": "^4.0.1",
"@fastify/type-provider-typebox": "^4.0.0",
"@fastify/under-pressure": "^8.3.0",
"@jean-michelet/workflow": "^1.0.7",
"@sinclair/typebox": "^0.33.7",
"concurrently": "^8.2.2",
"fastify": "^4.26.1",
"fastify-cli": "^6.1.1",
"fastify-plugin": "^4.0.0",
Expand All @@ -47,7 +50,7 @@
"fastify-tsconfig": "^2.0.0",
"mysql2": "^3.10.1",
"neostandard": "^0.7.0",
"tap": "^19.2.2",
"tap": "^21.0.1",
"typescript": "^5.4.5"
}
}
1 change: 1 addition & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default async function serviceApp(
fastify: FastifyInstance,
opts: FastifyPluginOptions
) {
delete opts.skipOverride // This option only serves testing purpose
// This loads all external plugins defined in plugins/external
// those should be registered first as your custom plugins might depend on them
await fastify.register(fastifyAutoload, {
Expand Down
33 changes: 33 additions & 0 deletions src/plugins/custom/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import fp from 'fastify-plugin';
import { MultiOriginTransition, Transition, Workflow } from '@jean-michelet/workflow';
import { Task, TaskStatus, TaskTransitions } from '../../schemas/tasks.js';

declare module "fastify" {
export interface FastifyInstance {
taskWorkflow: ReturnType<typeof createTaskWorkflow>;
}
}

function createTaskWorkflow() {
const wf = new Workflow<Task>({
stateProperty: 'status'
})

wf.addTransition(TaskTransitions.Start, new Transition(TaskStatus.New, TaskStatus.InProgress));
wf.addTransition(TaskTransitions.Complete, new Transition(TaskStatus.InProgress, TaskStatus.Completed));
wf.addTransition(TaskTransitions.Hold, new Transition(TaskStatus.InProgress, TaskStatus.OnHold));
wf.addTransition(TaskTransitions.Resume, new Transition(TaskStatus.OnHold, TaskStatus.InProgress));
wf.addTransition(TaskTransitions.Cancel, new MultiOriginTransition(
[TaskStatus.New, TaskStatus.InProgress, TaskStatus.OnHold],
TaskStatus.Canceled
));
wf.addTransition(TaskTransitions.Archive, new Transition(TaskStatus.Completed, TaskStatus.Archived));

return wf
}

export default fp(async (fastify) => {
fastify.decorate('taskWorkflow', createTaskWorkflow());
}, {
name: 'workflow'
});
File renamed without changes.
171 changes: 168 additions & 3 deletions src/routes/api/tasks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import {
FastifyPluginAsyncTypebox,
Type
} from "@fastify/type-provider-typebox";
import { TaskSchema } from "../../../schemas/tasks.js";
import {
TaskSchema,
Task,
CreateTaskSchema,
UpdateTaskSchema,
TaskStatus,
PatchTaskTransitionSchema
} from "../../../schemas/tasks.js";
import { FastifyReply } from "fastify";

const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.get(
Expand All @@ -16,9 +24,166 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
}
},
async function () {
return [{ id: 1, name: "Do something..." }];
const tasks = await fastify.repository.findMany<Task>("tasks");

return tasks;
}
);

fastify.get(
"/:id",
{
schema: {
params: Type.Object({
id: Type.Number()
}),
response: {
200: TaskSchema,
404: Type.Object({ message: Type.String() })
},
tags: ["Tasks"]
}
},
async function (request, reply) {
const { id } = request.params;
const task = await fastify.repository.find<Task>("tasks", { where: { id } });

if (!task) {
return notFound(reply);
}

return task;
}
);

fastify.post(
"/",
{
schema: {
body: CreateTaskSchema,
response: {
201: {
id: Type.Number()
}
},
tags: ["Tasks"]
}
},
async function (request, reply) {
const id = await fastify.repository.create("tasks", { data: {...request.body, status: TaskStatus.New} });
reply.code(201);

return {
id
};
}
);

fastify.patch(
"/:id",
{
schema: {
params: Type.Object({
id: Type.Number()
}),
body: UpdateTaskSchema,
response: {
200: TaskSchema,
404: Type.Object({ message: Type.String() })
},
tags: ["Tasks"]
}
},
async function (request, reply) {
const { id } = request.params;
const affectedRows = await fastify.repository.update("tasks", {
data: request.body,
where: { id }
});

if (affectedRows === 0) {
return notFound(reply)
}

const task = await fastify.repository.find<Task>("tasks", { where: { id } });

return task as Task;
}
);

fastify.patch(
"/:id/transition",
{
schema: {
params: Type.Object({
id: Type.Number()
}),
body: PatchTaskTransitionSchema,
response: {
200: Type.Object({ message: Type.String() }),
404: Type.Object({ message: Type.String() })
},
tags: ["Tasks"]
}
},
async function (request, reply) {
const { id } = request.params;
const { transition } = request.body;

const task = await fastify.repository.find<Task>("tasks", { select: 'status', where: { id } });

if (!task) {
return notFound(reply);
}

if (!fastify.taskWorkflow.can(transition, task)) {
reply.status(400)
return { message: `Transition "${transition}" can not be applied to task with status "${task.status}"` }
}

fastify.taskWorkflow.apply(transition, task)

await fastify.repository.update("tasks", {
data: { status: task.status },
where: { id }
});

return {
message: "Status changed"
};
}
);

fastify.delete(
"/:id",
{
schema: {
params: Type.Object({
id: Type.Number()
}),
response: {
204: Type.Null(),
404: Type.Object({ message: Type.String() })
},
tags: ["Tasks"]
}
},
async function (request, reply) {
const { id } = request.params;
const affectedRows = await fastify.repository.delete("tasks", { id });

if (affectedRows === 0) {
return notFound(reply)
}

reply.code(204).send(null);
}
);
};

export default plugin;
function notFound(reply: FastifyReply) {
reply.code(404)
return { message: "Task not found" }
}

export default plugin;
53 changes: 51 additions & 2 deletions src/schemas/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,55 @@
import { Type } from "@sinclair/typebox";
import { Static, Type } from "@sinclair/typebox";

export const TaskTransitions = {
Start: "start",
Complete: "complete",
Hold: "hold",
Resume: "resume",
Cancel: "cancel",
Archive: "archive"
} as const;

export const TaskStatus = {
New: 'new',
InProgress: 'in-progress',
OnHold: 'on-hold',
Completed: 'completed',
Canceled: 'canceled',
Archived: 'archived'
} as const;

export type TaskStatusType = typeof TaskStatus[keyof typeof TaskStatus];

export const TaskSchema = Type.Object({
id: Type.Number(),
name: Type.String()
name: Type.String(),
author_id: Type.Number(),
assigned_user_id: Type.Optional(Type.Number()),
status: Type.String(),
created_at: Type.String({ format: "date-time" }),
updated_at: Type.String({ format: "date-time" })
});

export interface Task extends Static<typeof TaskSchema> {}

export const CreateTaskSchema = Type.Object({
name: Type.String(),
author_id: Type.Number(),
assigned_user_id: Type.Optional(Type.Number())
});

export const UpdateTaskSchema = Type.Object({
name: Type.Optional(Type.String()),
assigned_user_id: Type.Optional(Type.Number())
});

export const PatchTaskTransitionSchema = Type.Object({
transition: Type.Union([
Type.Literal(TaskTransitions.Start),
Type.Literal(TaskTransitions.Complete),
Type.Literal(TaskTransitions.Hold),
Type.Literal(TaskTransitions.Resume),
Type.Literal(TaskTransitions.Cancel),
Type.Literal(TaskTransitions.Archive)
])
});
Loading

0 comments on commit d2336d0

Please sign in to comment.