Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade v5 #30

Merged
merged 4 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions @types/fastify/fastify.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Auth } from "../../src/schemas/auth.ts";
import { Auth } from '../../src/schemas/auth.ts'

declare module "fastify" {
export interface FastifyRequest {
user: Auth
}
declare module 'fastify' {
export interface FastifyRequest {
user: Auth
}
}
2 changes: 1 addition & 1 deletion @types/node/environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ declare global {
}
}

export {};
export {}
44 changes: 22 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,32 @@
"author": "Michelet Jean <jean.antoine.michelet@gmail.com>",
"license": "MIT",
"dependencies": {
"@fastify/autoload": "^5.10.0",
"@fastify/cors": "^9.0.1",
"@fastify/env": "^4.3.0",
"@fastify/helmet": "^11.1.1",
"@fastify/jwt": "^8.0.1",
"@fastify/mysql": "^4.3.0",
"@fastify/rate-limit": "^9.1.0",
"@fastify/sensible": "^5.0.0",
"@fastify/swagger": "^8.14.0",
"@fastify/swagger-ui": "^4.0.1",
"@fastify/type-provider-typebox": "^4.0.0",
"@fastify/under-pressure": "^8.3.0",
"@sinclair/typebox": "^0.33.7",
"@fastify/autoload": "^6.0.0",
"@fastify/cors": "^10.0.0",
"@fastify/env": "^5.0.1",
"@fastify/helmet": "^12.0.0",
"@fastify/jwt": "^9.0.0",
"@fastify/mysql": "^5.0.1",
"@fastify/rate-limit": "^10.0.1",
"@fastify/sensible": "^6.0.1",
"@fastify/swagger": "^9.0.0",
"@fastify/swagger-ui": "^5.0.1",
"@fastify/type-provider-typebox": "^5.0.0",
"@fastify/under-pressure": "^9.0.1",
"@sinclair/typebox": "^0.33.12",
"concurrently": "^9.0.1",
"fastify": "^4.26.1",
"fastify-cli": "^6.1.1",
"fastify-plugin": "^4.0.0",
"postgrator": "^7.2.0"
"fastify": "^5.0.0",
"fastify-cli": "^7.0.0",
"fastify-plugin": "^5.0.1",
"postgrator": "^7.3.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"eslint": "^9.4.0",
"@types/node": "^22.5.5",
"eslint": "^9.11.0",
"fastify-tsconfig": "^2.0.0",
"mysql2": "^3.10.1",
"neostandard": "^0.7.0",
"mysql2": "^3.11.3",
"neostandard": "^0.11.5",
"tap": "^21.0.1",
"typescript": "^5.4.5"
"typescript": "^5.6.2"
}
}
81 changes: 40 additions & 41 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,47 @@
* If you would like to turn your application into a standalone executable, look at server.js file
*/

import path from "node:path";
import fastifyAutoload from "@fastify/autoload";
import { FastifyInstance, FastifyPluginOptions } from "fastify";
import path from 'node:path'
import fastifyAutoload from '@fastify/autoload'
import { FastifyInstance, FastifyPluginOptions } from 'fastify'

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

export default async function serviceApp(
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, {
dir: path.join(import.meta.dirname, "plugins/external"),
dir: path.join(import.meta.dirname, 'plugins/external'),
options: { ...opts }
});
})

// This loads all your custom plugins defined in plugins/custom
// those should be support plugins that are reused
// through your application
fastify.register(fastifyAutoload, {
dir: path.join(import.meta.dirname, "plugins/custom"),
dir: path.join(import.meta.dirname, 'plugins/custom'),
options: { ...opts }
});
})

// This loads all plugins defined in routes
// define your routes in one of these
fastify.register(fastifyAutoload, {
dir: path.join(import.meta.dirname, "routes"),
dir: path.join(import.meta.dirname, 'routes'),
autoHooks: true,
cascadeHooks: true,
options: { ...opts }
});
})

fastify.setErrorHandler((err, request, reply) => {
request.log.error(
Expand All @@ -55,43 +55,42 @@ export default async function serviceApp(
params: request.params
}
},
"Unhandled error occurred"
);
'Unhandled error occurred'
)

reply.code(err.statusCode ?? 500);
reply.code(err.statusCode ?? 500)

let message = "Internal Server Error";
let message = 'Internal Server Error'
if (err.statusCode === 401) {
message = err.message;
message = err.message
}

return { message };
});
return { message }
})

// An attacker could search for valid URLs if your 404 error handling is not rate limited.
fastify.setNotFoundHandler(
{
preHandler: fastify.rateLimit({
max: 3,
timeWindow: 500
})
},
{
preHandler: fastify.rateLimit({
max: 3,
timeWindow: 500
})
},
(request, reply) => {
request.log.warn(
{
request: {
method: request.method,
url: request.url,
query: request.query,
params: request.params
}
},
'Resource not found'
)

request.log.warn(
{
request: {
method: request.method,
url: request.url,
query: request.query,
params: request.params
}
},
"Resource not found"
);

reply.code(404);
reply.code(404)

return { message: "Not Found" };
});
return { message: 'Not Found' }
})
}
96 changes: 48 additions & 48 deletions src/plugins/custom/repository.ts
Original file line number Diff line number Diff line change
@@ -1,96 +1,96 @@
import { MySQLPromisePool } from "@fastify/mysql";
import { FastifyInstance } from "fastify";
import fp from "fastify-plugin";
import { RowDataPacket, ResultSetHeader } from "mysql2";
import { MySQLPromisePool } from '@fastify/mysql'
import { FastifyInstance } from 'fastify'
import fp from 'fastify-plugin'
import { RowDataPacket, ResultSetHeader } from 'mysql2'

declare module "fastify" {
declare module 'fastify' {
export interface FastifyInstance {
repository: Repository;
}
}

export type Repository = MySQLPromisePool & ReturnType<typeof createRepository>;
export type Repository = MySQLPromisePool & ReturnType<typeof createRepository>

type QuerySeparator = 'AND' | ',';
type QuerySeparator = 'AND' | ','

type QueryOptions = {
select?: string;
where?: Record<string, any>;
};
}

type WriteOptions = {
data: Record<string, any>;
where?: Record<string, any>;
};
}

function createRepository(fastify: FastifyInstance) {
function createRepository (fastify: FastifyInstance) {
const processAssignmentRecord = (record: Record<string, any>, separator: QuerySeparator) => {
const keys = Object.keys(record);
const values = Object.values(record);
const clause = keys.map((key) => `${key} = ?`).join(` ${separator} `);
const keys = Object.keys(record)
const values = Object.values(record)
const clause = keys.map((key) => `${key} = ?`).join(` ${separator} `)

return [clause, values] as const;
};
return [clause, values] as const
}

const repository = {
...fastify.mysql,
find: async <T>(table: string, opts: QueryOptions): Promise<T | null> => {
const { select = '*', where = {1:1} } = opts;
const [clause, values] = processAssignmentRecord(where, 'AND');
const { select = '*', where = { 1: 1 } } = opts
const [clause, values] = processAssignmentRecord(where, 'AND')

const query = `SELECT ${select} FROM ${table} WHERE ${clause} LIMIT 1`;
const [rows] = await fastify.mysql.query<RowDataPacket[]>(query, values);
const query = `SELECT ${select} FROM ${table} WHERE ${clause} LIMIT 1`
const [rows] = await fastify.mysql.query<RowDataPacket[]>(query, values)
if (rows.length < 1) {
return null;
return null
}

return rows[0] as T;
return rows[0] as T
},

findMany: async <T>(table: string, opts: QueryOptions = {}): Promise<T[]> => {
const { select = '*', where = {1:1} } = opts;
const [clause, values] = processAssignmentRecord(where, 'AND');
const { select = '*', where = { 1: 1 } } = opts
const [clause, values] = processAssignmentRecord(where, 'AND')

const query = `SELECT ${select} FROM ${table} WHERE ${clause}`;
const [rows] = await fastify.mysql.query<RowDataPacket[]>(query, values);
const query = `SELECT ${select} FROM ${table} WHERE ${clause}`
const [rows] = await fastify.mysql.query<RowDataPacket[]>(query, values)

return rows as T[];
return rows as T[]
},

create: async (table: string, opts: WriteOptions): Promise<number> => {
const { data } = opts;
const columns = Object.keys(data).join(', ');
const placeholders = Object.keys(data).map(() => '?').join(', ');
const values = Object.values(data);
const { data } = opts
const columns = Object.keys(data).join(', ')
const placeholders = Object.keys(data).map(() => '?').join(', ')
const values = Object.values(data)

const query = `INSERT INTO ${table} (${columns}) VALUES (${placeholders})`;
const [result] = await fastify.mysql.query<ResultSetHeader>(query, values);
const query = `INSERT INTO ${table} (${columns}) VALUES (${placeholders})`
const [result] = await fastify.mysql.query<ResultSetHeader>(query, values)

return result.insertId;
return result.insertId
},

update: async (table: string, opts: WriteOptions): Promise<number> => {
const { data, where = {} } = opts;
const [dataClause, dataValues] = processAssignmentRecord(data, ',');
const [whereClause, whereValues] = processAssignmentRecord(where, 'AND');
const { data, where = {} } = opts
const [dataClause, dataValues] = processAssignmentRecord(data, ',')
const [whereClause, whereValues] = processAssignmentRecord(where, 'AND')

const query = `UPDATE ${table} SET ${dataClause} WHERE ${whereClause}`;
const [result] = await fastify.mysql.query<ResultSetHeader>(query, [...dataValues, ...whereValues]);
const query = `UPDATE ${table} SET ${dataClause} WHERE ${whereClause}`
const [result] = await fastify.mysql.query<ResultSetHeader>(query, [...dataValues, ...whereValues])

return result.affectedRows;
return result.affectedRows
},

delete: async (table: string, where: Record<string, any>): Promise<number> => {
const [clause, values] = processAssignmentRecord(where, 'AND');
const [clause, values] = processAssignmentRecord(where, 'AND')

const query = `DELETE FROM ${table} WHERE ${clause}`;
const [result] = await fastify.mysql.query<ResultSetHeader>(query, values);
const query = `DELETE FROM ${table} WHERE ${clause}`
const [result] = await fastify.mysql.query<ResultSetHeader>(query, values)

return result.affectedRows;
return result.affectedRows
}
};
}

return repository;
return repository
}

/**
Expand All @@ -101,9 +101,9 @@ function createRepository(fastify: FastifyInstance) {
*/
export default fp(
async function (fastify) {
fastify.decorate("repository", createRepository(fastify));
fastify.decorate('repository', createRepository(fastify))
// You should name your plugins if you want to avoid name collisions
// and/or to perform dependency checks.
},
{ name: "repository", dependencies: ['mysql'] }
);
{ name: 'repository', dependencies: ['mysql'] }
)
Loading