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

feat: base de code minimal #10

Merged
merged 7 commits into from
Mar 23, 2023
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
6 changes: 3 additions & 3 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"main": "src/index.ts",
"scripts": {
"start": "node dist/index.js",
"dev": "tsup src/index.ts migrations --watch . --onSuccess 'yarn run start'",
"dev": "tsup src/index.ts migrations --watch . --watch ../shared --onSuccess 'yarn run start'",
"build": "tsup src/index.ts migrations",
"typecheck": "tsc",
"migration:create": "migrate-mongo create",
Expand All @@ -20,8 +20,7 @@
},
"dependencies": {
"@fastify/cors": "^8.2.0",
"@fastify/type-provider-typebox": "^2.4.0",
"@sinclair/typebox": "^0.25.21",
"@fastify/type-provider-json-schema-to-ts": "^2.2.2",
"axios": "0.24.0",
"boom": "7.3.0",
"bunyan": "1.8.15",
Expand All @@ -34,6 +33,7 @@
"dotenv": "^16.0.3",
"env-var": "7.1.1",
"fastify": "^4.12.0",
"json-schema-to-ts": "^2.7.2",
"jsonwebtoken": "8.5.1",
"lodash": "^4.17.21",
"luxon": "2.3.0",
Expand Down
86 changes: 63 additions & 23 deletions server/src/db/mongodb.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { MongoClient } from "mongodb";
import { CollectionInfo, MongoClient } from "mongodb";
import omitDeep from "omit-deep";

import { asyncForEach } from "../utils/asyncUtils";
import { IModelDescriptor } from "../models/collections";
import logger from "../utils/logger";

/** @type {MongoClient} */
let mongodbClient: MongoClient;

const ensureInitialization = () => {
if (!mongodbClient) {
throw new Error("Database connection does not exist. Please call connectToMongodb before.");
throw new Error(
"Database connection does not exist. Please call connectToMongodb before."
);
}
};

Expand Down Expand Up @@ -54,7 +56,9 @@ export const getDbCollectionIndexes = async (name: string) => {
const createCollectionIfDoesNotExist = async (collectionName: string) => {
const db = getDatabase();
const collectionsInDb = await db.listCollections().toArray();
const collectionExistsInDb = collectionsInDb.map(({ name }) => name).includes(collectionName);
const collectionExistsInDb = collectionsInDb
.map(({ name }) => name)
.includes(collectionName);

if (!collectionExistsInDb) {
await db.createCollection(collectionName);
Expand All @@ -67,32 +71,68 @@ const createCollectionIfDoesNotExist = async (collectionName: string) => {
* @param {*} collectionName
* @returns
*/
export const collectionExistInDb = (collectionsInDb: any[], collectionName: string) =>
collectionsInDb.map(({ name }: {name: string}) => name).includes(collectionName);
export const collectionExistInDb = (
collectionsInDb: CollectionInfo[],
collectionName: string
) =>
collectionsInDb
.map(({ name }: { name: string }) => name)
.includes(collectionName);

/**
* Conversion du schema pour le format mongoDB
*/
const convertSchemaToMongoSchema = (schema: unknown) => {
let replacedTypes = JSON.parse(
JSON.stringify(schema).replaceAll("type", "bsonType")
);

// remplacer _id de "string" à "objectId"
replacedTypes = {
...replacedTypes,
properties: {
...replacedTypes.properties,
_id: { bsonType: "objectId" },
},
};

// strip example field because NON STANDARD jsonSchema
return omitDeep(replacedTypes, ["example"]);
};

/**
* Config de la validation
* @param {*} modelDescriptors
*/
export const configureDbSchemaValidation = async (modelDescriptors: any[]) => {
export const configureDbSchemaValidation = async (
modelDescriptors: IModelDescriptor[]
) => {
const db = getDatabase();
ensureInitialization();
await asyncForEach(modelDescriptors, async ({ collectionName, schema }: {collectionName: string, schema: any}) => {
await createCollectionIfDoesNotExist(collectionName);

if (!schema) {
return;
}

await db.command({
collMod: collectionName,
validationLevel: "strict",
validationAction: "error",
validator: {
$jsonSchema: { title: `${collectionName} validation schema`, ...omitDeep(schema, ["example"]) }, // strip example field because NON STANDARD jsonSchema
},
});
});

await Promise.all(
modelDescriptors.map(async ({ collectionName, schema }) => {
await createCollectionIfDoesNotExist(collectionName);

if (!schema) {
return;
}

const convertedSchema = convertSchemaToMongoSchema(schema);

await db.command({
collMod: collectionName,
validationLevel: "strict",
validationAction: "error",
validator: {
$jsonSchema: {
title: `${collectionName} validation schema`,
...convertedSchema,
},
},
});
})
);
};

/**
Expand Down
9 changes: 4 additions & 5 deletions server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import fastifyCors from "@fastify/cors";
import { config } from "config/config";

import { connectToMongodb } from "./db/mongodb";
import { configureDbSchemaValidation, connectToMongodb } from "./db/mongodb";
import { modelDescriptors } from "./models/collections";
import { registerCoreModule } from "./modules/core";
import { server } from "./server";


(async function () {
try {
await connectToMongodb(config.mongodb.uri);
await configureDbSchemaValidation(modelDescriptors);

server.register(fastifyCors, {});
server.register(
Expand All @@ -17,7 +18,7 @@ import { server } from "./server";
},
{ prefix: "/api" }
);

server.listen({ port: 5000, host: "0.0.0.0" }, function (err) {
if (err) {
console.log(err);
Expand All @@ -28,5 +29,3 @@ import { server } from "./server";
process.exit(1); // eslint-disable-line no-process-exit
}
})();


9 changes: 9 additions & 0 deletions server/src/models/collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import usersModelDescriptor from "shared/models/user.model";

export interface IModelDescriptor {
schema: unknown;
indexes: unknown;
collectionName: string;
}

export const modelDescriptors: IModelDescriptor[] = [usersModelDescriptor];
2 changes: 2 additions & 0 deletions server/src/modules/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Server } from "../../server";
import { coreRoutes } from "./routes/core.routes";
import { userRoutes } from "./routes/user.routes";

export const registerCoreModule = ({ server }: { server: Server }) => {
coreRoutes({ server });
userRoutes({ server });
};
39 changes: 39 additions & 0 deletions server/src/modules/core/routes/user.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { IUser } from "shared/models/user.model";
import { SReqPostUser, SResPostUser } from "shared/routes/user.routes";

import { getDbCollection } from "../../../db/mongodb";
import { Server } from "../../../server";

export const userRoutes = ({ server }: { server: Server }) => {
/**
* Créer un utilisateur
*/
server.post(
"/user",
{
schema: {
body: SReqPostUser,
response: { 200: SResPostUser },
} as const,
},
async (request, response) => {
try {
const { insertedId: userId } = await getDbCollection("users").insertOne(
request.body
);

const user = await getDbCollection("users").findOne<IUser>({
_id: userId,
});

if (!user) {
throw new Error("User not created");
}

return response.status(200).send(user);
} catch (error) {
console.error(error);
}
}
);
};
4 changes: 2 additions & 2 deletions server/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
import { JsonSchemaToTsProvider } from "@fastify/type-provider-json-schema-to-ts";
import fastify from "fastify";

export const server = fastify({
Expand All @@ -9,6 +9,6 @@ export const server = fastify({
keywords: ["kind", "modifier"],
},
},
}).withTypeProvider<TypeBoxTypeProvider>();
}).withTypeProvider<JsonSchemaToTsProvider>();

export type Server = typeof server;
5 changes: 4 additions & 1 deletion server/src/utils/asyncUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const asyncForEach = async (array: any[], callback: (item:any, index?: number, array?: any[]) => void) => {
export const asyncForEach = async <T>(
array: T[],
callback: (item: T, index?: number, array?: T[]) => void
) => {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
Expand Down
2 changes: 1 addition & 1 deletion server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"listFiles": false,
"pretty": true,
"isolatedModules": false,
"lib": ["ES2020"] /* Emit ECMAScript-standard-compliant class fields. */,
"lib": ["ES2022"] /* Emit ECMAScript-standard-compliant class fields. */,
// "module": "ES2020" /* Specify what module code is generated. */,
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"resolveJsonModule": true,
Expand Down
21 changes: 21 additions & 0 deletions shared/models/user.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { FromSchema } from "json-schema-to-ts";

const collectionName = "users";

const indexes = () => {
return [[{ name: 1 }, { name: "name" }]];
};

export const SUser = {
type: "object",
properties: {
_id: { type: "string" },
name: { type: "string" },
email: { type: "string" },
},
required: ["name"],
} as const;

export type IUser = FromSchema<typeof SUser>;

export default { schema: SUser, indexes, collectionName };
18 changes: 18 additions & 0 deletions shared/routes/user.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { FromSchema } from "json-schema-to-ts";

import { IUser, SUser } from "../models/user.model";

export const SReqPostUser = {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string", format: "email" },
},
required: ["name"],
} as const;

export type IReqPostUser = FromSchema<typeof SReqPostUser>;


export const SResPostUser = SUser;
export interface IResPostUser extends IUser {}
Loading