Skip to content

Commit 13e73b4

Browse files
feat: crud task according to workflow (#19)
* feat: crud task according to workflow * refactor: remove workflow * feat: add endpoint to assign/unassign task * fix: remove additional properties by default * fix: disable allErrors ajv
1 parent 3d165b2 commit 13e73b4

File tree

11 files changed

+493
-42
lines changed

11 files changed

+493
-42
lines changed

README.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,6 @@ To build the project:
4141
npm run build
4242
```
4343

44-
In dev mode, you can use:
45-
```bash
46-
npm run watch
47-
```
48-
4944
### Start the server
5045
In dev mode:
5146
```bash

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
"test": "test"
99
},
1010
"scripts": {
11-
"build": "rm -rf dist && tsc",
12-
"watch": "npm run build -- --watch",
11+
"start": "npm run build && fastify start -l info dist/app.js",
12+
"build": "tsc",
13+
"watch": "tsc -w",
14+
"dev": "npm run build && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch\" \"npm:dev:start\"",
15+
"dev:start": "fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js",
1316
"test": "npm run db:seed && tap --jobs=1 test/**/*",
14-
"start": "fastify start -l info dist/app.js",
15-
"dev": "fastify start -w -l info -P dist/app.js",
1617
"standalone": "node --env-file=.env dist/server.js",
1718
"lint": "eslint --ignore-pattern=dist",
1819
"lint:fix": "npm run lint -- --fix",
@@ -36,6 +37,7 @@
3637
"@fastify/type-provider-typebox": "^4.0.0",
3738
"@fastify/under-pressure": "^8.3.0",
3839
"@sinclair/typebox": "^0.33.7",
40+
"concurrently": "^8.2.2",
3941
"fastify": "^4.26.1",
4042
"fastify-cli": "^6.1.1",
4143
"fastify-plugin": "^4.0.0",
@@ -47,7 +49,7 @@
4749
"fastify-tsconfig": "^2.0.0",
4850
"mysql2": "^3.10.1",
4951
"neostandard": "^0.7.0",
50-
"tap": "^19.2.2",
52+
"tap": "^21.0.1",
5153
"typescript": "^5.4.5"
5254
}
5355
}

src/app.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,20 @@ import path from "node:path";
66
import fastifyAutoload from "@fastify/autoload";
77
import { FastifyInstance, FastifyPluginOptions } from "fastify";
88

9+
export const options = {
10+
ajv: {
11+
customOptions: {
12+
coerceTypes: "array",
13+
removeAdditional: "all"
14+
}
15+
}
16+
};
17+
918
export default async function serviceApp(
1019
fastify: FastifyInstance,
1120
opts: FastifyPluginOptions
1221
) {
22+
delete opts.skipOverride // This option only serves testing purpose
1323
// This loads all external plugins defined in plugins/external
1424
// those should be registered first as your custom plugins might depend on them
1525
await fastify.register(fastifyAutoload, {
File renamed without changes.

src/routes/api/tasks/index.ts

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import {
22
FastifyPluginAsyncTypebox,
33
Type
44
} from "@fastify/type-provider-typebox";
5-
import { TaskSchema } from "../../../schemas/tasks.js";
5+
import {
6+
TaskSchema,
7+
Task,
8+
CreateTaskSchema,
9+
UpdateTaskSchema,
10+
TaskStatus
11+
} from "../../../schemas/tasks.js";
12+
import { FastifyReply } from "fastify";
613

714
const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
815
fastify.get(
@@ -16,9 +23,160 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
1623
}
1724
},
1825
async function () {
19-
return [{ id: 1, name: "Do something..." }];
26+
const tasks = await fastify.repository.findMany<Task>("tasks");
27+
28+
return tasks;
29+
}
30+
);
31+
32+
fastify.get(
33+
"/:id",
34+
{
35+
schema: {
36+
params: Type.Object({
37+
id: Type.Number()
38+
}),
39+
response: {
40+
200: TaskSchema,
41+
404: Type.Object({ message: Type.String() })
42+
},
43+
tags: ["Tasks"]
44+
}
45+
},
46+
async function (request, reply) {
47+
const { id } = request.params;
48+
const task = await fastify.repository.find<Task>("tasks", { where: { id } });
49+
50+
if (!task) {
51+
return notFound(reply);
52+
}
53+
54+
return task;
55+
}
56+
);
57+
58+
fastify.post(
59+
"/",
60+
{
61+
schema: {
62+
body: CreateTaskSchema,
63+
response: {
64+
201: {
65+
id: Type.Number()
66+
}
67+
},
68+
tags: ["Tasks"]
69+
}
70+
},
71+
async function (request, reply) {
72+
const id = await fastify.repository.create("tasks", { data: {...request.body, status: TaskStatus.New} });
73+
reply.code(201);
74+
75+
return {
76+
id
77+
};
78+
}
79+
);
80+
81+
fastify.patch(
82+
"/:id",
83+
{
84+
schema: {
85+
params: Type.Object({
86+
id: Type.Number()
87+
}),
88+
body: UpdateTaskSchema,
89+
response: {
90+
200: TaskSchema,
91+
404: Type.Object({ message: Type.String() })
92+
},
93+
tags: ["Tasks"]
94+
}
95+
},
96+
async function (request, reply) {
97+
const { id } = request.params;
98+
const affectedRows = await fastify.repository.update("tasks", {
99+
data: request.body,
100+
where: { id }
101+
});
102+
103+
if (affectedRows === 0) {
104+
return notFound(reply)
105+
}
106+
107+
const task = await fastify.repository.find<Task>("tasks", { where: { id } });
108+
109+
return task as Task;
110+
}
111+
);
112+
113+
fastify.delete(
114+
"/:id",
115+
{
116+
schema: {
117+
params: Type.Object({
118+
id: Type.Number()
119+
}),
120+
response: {
121+
204: Type.Null(),
122+
404: Type.Object({ message: Type.String() })
123+
},
124+
tags: ["Tasks"]
125+
}
126+
},
127+
async function (request, reply) {
128+
const { id } = request.params;
129+
const affectedRows = await fastify.repository.delete("tasks", { id });
130+
131+
if (affectedRows === 0) {
132+
return notFound(reply)
133+
}
134+
135+
reply.code(204).send(null);
20136
}
21137
);
138+
139+
fastify.post(
140+
"/:id/assign",
141+
{
142+
schema: {
143+
params: Type.Object({
144+
id: Type.Number()
145+
}),
146+
body: Type.Object({
147+
userId: Type.Optional(Type.Number())
148+
}),
149+
response: {
150+
200: TaskSchema,
151+
404: Type.Object({ message: Type.String() })
152+
},
153+
tags: ["Tasks"]
154+
}
155+
},
156+
async function (request, reply) {
157+
const { id } = request.params;
158+
const { userId } = request.body;
159+
160+
const task = await fastify.repository.find<Task>("tasks", { where: { id } });
161+
if (!task) {
162+
return notFound(reply);
163+
}
164+
165+
await fastify.repository.update("tasks", {
166+
data: { assigned_user_id: userId },
167+
where: { id }
168+
});
169+
170+
task.assigned_user_id = userId
171+
172+
return task;
173+
}
174+
)
22175
};
23176

177+
function notFound(reply: FastifyReply) {
178+
reply.code(404)
179+
return { message: "Task not found" }
180+
}
181+
24182
export default plugin;

src/schemas/tasks.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
1-
import { Type } from "@sinclair/typebox";
1+
import { Static, Type } from "@sinclair/typebox";
2+
3+
export const TaskStatus = {
4+
New: 'new',
5+
InProgress: 'in-progress',
6+
OnHold: 'on-hold',
7+
Completed: 'completed',
8+
Canceled: 'canceled',
9+
Archived: 'archived'
10+
} as const;
11+
12+
export type TaskStatusType = typeof TaskStatus[keyof typeof TaskStatus];
213

314
export const TaskSchema = Type.Object({
415
id: Type.Number(),
5-
name: Type.String()
16+
name: Type.String(),
17+
author_id: Type.Number(),
18+
assigned_user_id: Type.Optional(Type.Number()),
19+
status: Type.String(),
20+
created_at: Type.String({ format: "date-time" }),
21+
updated_at: Type.String({ format: "date-time" })
22+
});
23+
24+
export interface Task extends Static<typeof TaskSchema> {}
25+
26+
export const CreateTaskSchema = Type.Object({
27+
name: Type.String(),
28+
author_id: Type.Number(),
29+
assigned_user_id: Type.Optional(Type.Number())
30+
});
31+
32+
export const UpdateTaskSchema = Type.Object({
33+
name: Type.Optional(Type.String()),
34+
assigned_user_id: Type.Optional(Type.Number())
635
});
36+

test/helper.ts

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,34 @@
1-
// This file contains code that we reuse
2-
// between our tests.
31

4-
import { InjectOptions } from "fastify";
2+
import { FastifyInstance, InjectOptions } from "fastify";
53
import { build as buildApplication } from "fastify-cli/helper.js";
64
import path from "node:path";
75
import { TestContext } from "node:test";
6+
import { options as serverOptions } from "../src/app.js";
7+
8+
declare module "fastify" {
9+
interface FastifyInstance {
10+
login: typeof login;
11+
injectWithLogin: typeof injectWithLogin
12+
}
13+
}
814

915
const AppPath = path.join(import.meta.dirname, "../src/app.ts");
1016

1117
// Fill in this config with all the configurations
1218
// needed for testing the application
1319
export function config() {
14-
return {};
20+
return {
21+
skipOverride: "true" // Register our application with fastify-plugin
22+
};
1523
}
1624

25+
const tokens: Record<string, string> = {}
1726
// We will create different users with different roles
18-
async function login(username: string) {
27+
async function login(this: FastifyInstance, username: string) {
28+
if (tokens[username]) {
29+
return tokens[username]
30+
}
31+
1932
const res = await this.inject({
2033
method: "POST",
2134
url: "/api/auth/login",
@@ -25,9 +38,20 @@ async function login(username: string) {
2538
}
2639
});
2740

28-
return JSON.parse(res.payload).token;
41+
tokens[username] = JSON.parse(res.payload).token;
42+
43+
return tokens[username]
2944
}
3045

46+
async function injectWithLogin(this: FastifyInstance, username: string, opts: InjectOptions) {
47+
opts.headers = {
48+
...opts.headers,
49+
Authorization: `Bearer ${await this.login(username)}`
50+
};
51+
52+
return this.inject(opts);
53+
};
54+
3155
// automatically build and tear down our instance
3256
export async function build(t: TestContext) {
3357
// you can set all the options supported by the fastify CLI command
@@ -36,18 +60,11 @@ export async function build(t: TestContext) {
3660
// fastify-plugin ensures that all decorators
3761
// are exposed for testing purposes, this is
3862
// different from the production setup
39-
const app = await buildApplication(argv, config());
63+
const app = await buildApplication(argv, config(), serverOptions) as FastifyInstance;
4064

4165
app.login = login;
4266

43-
app.injectWithLogin = async (username: string, opts: InjectOptions) => {
44-
opts.headers = {
45-
...opts.headers,
46-
Authorization: `Bearer ${await app.login(username)}`
47-
};
48-
49-
return app.inject(opts);
50-
};
67+
app.injectWithLogin = injectWithLogin
5168

5269
// close the app after we are done
5370
t.after(() => app.close());

test/plugins/repository.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import assert from "node:assert";
33
import { execSync } from "child_process";
44
import Fastify from "fastify";
55
import repository from "../../src/plugins/custom/repository.js";
6-
import * as envPlugin from "../../src/plugins/external/1-env.js";
6+
import * as envPlugin from "../../src/plugins/external/env.js";
77
import * as mysqlPlugin from "../../src/plugins/external/mysql.js";
88
import { Auth } from '../../src/schemas/auth.js';
99

test/plugins/scrypt.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test } from "tap";
22
import Fastify from "fastify";
3-
import scryptPlugin from "../../src/plugins/custom/scrypt.ts";
3+
import scryptPlugin from "../../src/plugins/custom/scrypt.js";
44

55
test("scrypt works standalone", async t => {
66
const app = Fastify();

0 commit comments

Comments
 (0)