Skip to content

Commit

Permalink
feat: base de code minimal (#10)
Browse files Browse the repository at this point in the history
* feat: base de code minimal
* feat: shared models and types
* fix: missing await
* feat: watch shared on server
* feat: wip naming types and schemas
* fix: path
* chore: improve typings
---------

Co-authored-by: David Dela Cruz <david.dela.cruz@beta.gouv.fr>
Co-authored-by: Antoine Bigard <bigard.antoine@gmail.com>
  • Loading branch information
3 people authored Mar 23, 2023
1 parent d4d7702 commit fb6ff23
Show file tree
Hide file tree
Showing 12 changed files with 200 additions and 49 deletions.
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

0 comments on commit fb6ff23

Please sign in to comment.