Skip to content

Commit

Permalink
feat: crud task according to workflow (#19)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jean-michelet authored Sep 13, 2024
1 parent 3d165b2 commit 13e73b4
Show file tree
Hide file tree
Showing 11 changed files with 493 additions and 42 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
12 changes: 7 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 @@ -36,6 +37,7 @@
"@fastify/type-provider-typebox": "^4.0.0",
"@fastify/under-pressure": "^8.3.0",
"@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 +49,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"
}
}
10 changes: 10 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,20 @@ import path from "node:path";
import fastifyAutoload from "@fastify/autoload";
import { FastifyInstance, FastifyPluginOptions } from "fastify";

export const options = {
ajv: {
customOptions: {
coerceTypes: "array",
removeAdditional: "all"
}
}
};

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
File renamed without changes.
162 changes: 160 additions & 2 deletions src/routes/api/tasks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import {
FastifyPluginAsyncTypebox,
Type
} from "@fastify/type-provider-typebox";
import { TaskSchema } from "../../../schemas/tasks.js";
import {
TaskSchema,
Task,
CreateTaskSchema,
UpdateTaskSchema,
TaskStatus
} from "../../../schemas/tasks.js";
import { FastifyReply } from "fastify";

const plugin: FastifyPluginAsyncTypebox = async (fastify) => {
fastify.get(
Expand All @@ -16,9 +23,160 @@ 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.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);
}
);

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

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

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

task.assigned_user_id = userId

return task;
}
)
};

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

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

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())
});

47 changes: 32 additions & 15 deletions test/helper.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
// This file contains code that we reuse
// between our tests.

import { InjectOptions } from "fastify";
import { FastifyInstance, InjectOptions } from "fastify";
import { build as buildApplication } from "fastify-cli/helper.js";
import path from "node:path";
import { TestContext } from "node:test";
import { options as serverOptions } from "../src/app.js";

declare module "fastify" {
interface FastifyInstance {
login: typeof login;
injectWithLogin: typeof injectWithLogin
}
}

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

// Fill in this config with all the configurations
// needed for testing the application
export function config() {
return {};
return {
skipOverride: "true" // Register our application with fastify-plugin
};
}

const tokens: Record<string, string> = {}
// We will create different users with different roles
async function login(username: string) {
async function login(this: FastifyInstance, username: string) {
if (tokens[username]) {
return tokens[username]
}

const res = await this.inject({
method: "POST",
url: "/api/auth/login",
Expand All @@ -25,9 +38,20 @@ async function login(username: string) {
}
});

return JSON.parse(res.payload).token;
tokens[username] = JSON.parse(res.payload).token;

return tokens[username]
}

async function injectWithLogin(this: FastifyInstance, username: string, opts: InjectOptions) {
opts.headers = {
...opts.headers,
Authorization: `Bearer ${await this.login(username)}`
};

return this.inject(opts);
};

// automatically build and tear down our instance
export async function build(t: TestContext) {
// you can set all the options supported by the fastify CLI command
Expand All @@ -36,18 +60,11 @@ export async function build(t: TestContext) {
// fastify-plugin ensures that all decorators
// are exposed for testing purposes, this is
// different from the production setup
const app = await buildApplication(argv, config());
const app = await buildApplication(argv, config(), serverOptions) as FastifyInstance;

app.login = login;

app.injectWithLogin = async (username: string, opts: InjectOptions) => {
opts.headers = {
...opts.headers,
Authorization: `Bearer ${await app.login(username)}`
};

return app.inject(opts);
};
app.injectWithLogin = injectWithLogin

// close the app after we are done
t.after(() => app.close());
Expand Down
2 changes: 1 addition & 1 deletion test/plugins/repository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import assert from "node:assert";
import { execSync } from "child_process";
import Fastify from "fastify";
import repository from "../../src/plugins/custom/repository.js";
import * as envPlugin from "../../src/plugins/external/1-env.js";
import * as envPlugin from "../../src/plugins/external/env.js";
import * as mysqlPlugin from "../../src/plugins/external/mysql.js";
import { Auth } from '../../src/schemas/auth.js';

Expand Down
2 changes: 1 addition & 1 deletion test/plugins/scrypt.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from "tap";
import Fastify from "fastify";
import scryptPlugin from "../../src/plugins/custom/scrypt.ts";
import scryptPlugin from "../../src/plugins/custom/scrypt.js";

test("scrypt works standalone", async t => {
const app = Fastify();
Expand Down
Loading

0 comments on commit 13e73b4

Please sign in to comment.