Micro-services with Node.js



Resources
Principle

Micro-services architecture relies on the Back-end For Front-end -BFF- design pattern

Database tiers ⤳ MongoDB-Mongoose

“index” as primary key support

import * as Mongoose from "mongoose";

export default interface CriminalCase extends Mongoose.Document {
    criminal_case_number: string;
    jurisdiction_name: string;
    date_of_criminal_case: Date;
}
// create table CRIMINAL_CASE(
//     CRIMINAL_CASE_NUMBER varchar(10),
//     JURISDICTION_NAME varchar(30),
//     DATE_OF_CRIMINAL_CASE Date,
//     constraint CRIMINAL_CASE_key primary key(CRIMINAL_CASE_NUMBER,JURISDICTION_NAME));
const schema = new Mongoose.Schema(
    {
        criminal_case_number: {
            type: Mongoose.Schema.Types.String,
            required: true,
            maxlength: 10
        },
        jurisdiction_name: {
            type: Mongoose.Schema.Types.String,
            required: true,
            maxlength: 30
        },
        date_of_criminal_case: {
            type: Mongoose.Schema.Types.Date,
            required: true,
            default: Date.now
        }
    }
);
// Primary key (performance issues):
schema.index({'criminal_case_number': 1 /* ascending '1', descending '-1' */, 'jurisdiction_name': 1}, {unique: true});

“index” as foreign key support

import * as Mongoose from "mongoose";
import Prisoner from "./Prisoner";

export default interface JudicialDecision extends Mongoose.Document {
    decision_type_number: string;
    prisoner: Prisoner;
    date_of_decision: Date;
}
// create table JUDICIAL_DECISION(
//     DECISION_TYPE_NUMBER varchar(1),
//     PRISON_FILE_NUMBER varchar(10),
//     DATE_OF_DECISION Date,
//     constraint JUDICIAL_DECISION_key primary key(DECISION_TYPE_NUMBER,PRISON_FILE_NUMBER,DATE_OF_DECISION),
//     constraint JUDICIAL_DECISION_fk foreign key(PRISON_FILE_NUMBER) references PRISONER(PRISON_FILE_NUMBER));
const schema = new Mongoose.Schema(
    {
        decision_type_number: {
            type: Mongoose.Schema.Types.String,
            required: true,
            enum: ['1', '2', '3'],
            maxlength: 1
        },
        prisoner: {
            type: Mongoose.Schema.Types.ObjectId,
            required: true,
            ref: 'PRISONER'
        },
        date_of_decision: {
            type: Mongoose.Schema.Types.Date,
            required: true,
            default: Date.now
        }
    },
    // 'discriminatorKey' option tells mongoose to add a path to the schema called 'decision_type_number' so that it tracks which type of document this is:
    {discriminatorKey: 'decision_type_number'}
);
// Primary key (performance issues!):
schema.index({decision_type_number: 1, prisoner: 1, date_of_decision: 1}, {unique: true});
Data Access Object -DAO- ⤳ MongoDB-Mongoose

Create, Read, Update, Delete -CRUD- services

import CriminalCase, {CriminalCaseModel} from '../schemes/CriminalCase';

export const Get_collection_name = () => CriminalCaseModel.collection.name;

export default class CriminalCaseDAO {
    public static Create(criminal_case: CriminalCase): Promise<CriminalCase> {
        return CriminalCaseModel.create(criminal_case);
    }

    public static Delete(criminal_case: CriminalCase): Promise<any>;
    public static Delete(criminal_case_number: string, jurisdiction_name: string): Promise<any>;
    public static Delete(x: CriminalCase | string, jurisdiction_name?: string) {
        return typeof x === "string" ?
            CriminalCaseModel.deleteOne({criminal_case_number: x, jurisdiction_name: jurisdiction_name}).exec() :
            CriminalCaseModel.deleteOne({
                criminal_case_number: x.criminal_case_number,
                jurisdiction_name: jurisdiction_name
            }).exec();
    }

    public static GetCriminalCase(criminal_case_number: string, jurisdiction_name: string, id = false): Promise<CriminalCase | null> {
        return CriminalCaseModel.findOne({
            criminal_case_number: criminal_case_number,
            jurisdiction_name: jurisdiction_name
        })
            .select({_id: id, __v: false})
            .lean()
            .exec();
    }

    public static GetCriminalCases(): Promise<CriminalCase[]> {
        return CriminalCaseModel.find()
            .select({_id: false, __v: false})
            .lean()
            .exec();
    }

    public static Update(criminal_case: CriminalCase): Promise<CriminalCase | any> {
        return CriminalCaseModel.updateOne({
                criminal_case_number: criminal_case.criminal_case_number,
                jurisdiction_name: criminal_case.jurisdiction_name
            },
            {$set: {...criminal_case}})
            .select({_id: false, __v: false})
            .exec();
    }
}
Micro-service ⤳ Express, joi
import express from "express";
import Joi from 'joi';

import CriminalCase from "../../persistence/schemes/CriminalCase";
import CriminalCaseDAO, {Get_collection_name} from '../../persistence/DAOs/CriminalCaseDAO';
import Validator, {ValidationSource} from "./Validator";

export {Get_collection_name};

const criminal_case_id = Joi.object().keys({
    criminal_case_number: Joi.string().required().max(10),
    jurisdiction_name: Joi.string().required().max(30)
});
const criminal_case_Joi_schema = Joi.object().keys({
    criminal_case_number: Joi.string().required().max(10),
    jurisdiction_name: Joi.string().required().max(30),
    date_of_criminal_case: Joi.date().required()
});

// Mini. app. for '/criminal_cases' sub-path:
export const router = express.Router();
// http://localhost:1963/NYCP/API/criminal_cases
router.get('', (request, response) => {
    console.log("READ: " + request.baseUrl + request.path);
    response.redirect(request.baseUrl + request.path + 'all');
});
// http://localhost:1963/NYCP/API/criminal_cases/one?criminal_case_number=M13000&jurisdiction_name=Marseille
router.get('/one', Validator(criminal_case_id /*, ValidationSource.QUERY */), (request, response) => {
    console.log("READ: " + request.baseUrl + request.path + JSON.stringify(request.query));
    CriminalCaseDAO.GetCriminalCase(request.query.criminal_case_number as string,
        request.query.jurisdiction_name as string).then(data => response.status(200).json(data))
        .catch(error => response.status(400).json({error, message: error.message}));
});
// http://localhost:1963/NYCP/API/criminal_cases/all
router.get('/all', (request, response) => {
    console.log("READ: " + request.baseUrl + request.path);
    CriminalCaseDAO.GetCriminalCases().then(data => response.status(200).json(data))
        .catch(error => response.status(400).json({error, message: error.message}));
});
// curl -d "criminal_case_number=P64000&jurisdiction_name=Pau&date_of_criminal_case=2002-03-23" http://localhost:1963/NYCP/API/criminal_cases/
router.post('/', Validator(criminal_case_Joi_schema, ValidationSource.BODY),
    async (request, response) => {
        console.log("CREATE: " + request.baseUrl + request.path + JSON.stringify(request.body));
        try {
            const criminal_case = await CriminalCaseDAO.GetCriminalCase(request.body.criminal_case_number, request.body.jurisdiction_name);
            if (criminal_case) throw new Error('Criminal case ' + JSON.stringify(request.body) + ' already exists...');
            CriminalCaseDAO.Create(request.body as CriminalCase).then(data => response.status(201).json(data))
                .catch(error => response.status(400).json({error, message: error.message}));
        } catch (error: unknown) {
            response.status(400).json({error, message: (error as Error).message});
        }
    });
// curl -X PUT http://localhost:1963/NYCP/API/criminal_cases/P64000\&Pau\&2002-03-23
router.put('/:criminal_case_number&:jurisdiction_name&:date_of_criminal_case', Validator(criminal_case_Joi_schema, ValidationSource.PARAMS),
    async (request, response) => {
        console.log("UPDATE: " + request.baseUrl + request.path + JSON.stringify(request.params));
        try {
            const criminal_case = await CriminalCaseDAO.GetCriminalCase(request.params.criminal_case_number, request.params.jurisdiction_name);
            if (!criminal_case) throw new Error('Criminal case ' + request.params.criminal_case_number + '-' + request.params.jurisdiction_name + ' does not exist...');
            criminal_case.date_of_criminal_case = new Date(request.params.date_of_criminal_case);
            CriminalCaseDAO.Update(criminal_case)
                .then(data => response.status(200).json(data))
                .catch(error => response.status(400).json({error, message: error.message}));
        } catch (error: unknown) {
            response.status(400).json({error, message: (error as Error).message});
        }
    });
// curl -X DELETE http://localhost:1963/NYCP/API/criminal_cases/P64000\&Pau
router.delete('/:criminal_case_number&:jurisdiction_name', Validator(criminal_case_id, ValidationSource.PARAMS),
    async (request, response) => {
        console.log("DELETE: " + request.baseUrl + request.path + JSON.stringify(request.params));
        try {
            const criminal_case = await CriminalCaseDAO.GetCriminalCase(request.params.criminal_case_number, request.params.jurisdiction_name);
            if (!criminal_case) throw new Error('Criminal case ' + request.params.criminal_case_number + '-' + request.params.jurisdiction_name + ' does not exist...');
            CriminalCaseDAO.Delete(criminal_case)
                .then(data => response.status(200).json(data))
                .catch(error => response.status(400).json({error, message: error.message}));
        } catch (error: unknown) {
            response.status(400).json({error, message: (error as Error).message});
        }
    });
REpresentational State Transfer -RESTful- Web service ⤳ Express

SQL query

SELECT * FROM Prisoner WHERE prison_file_number NOT IN (SELECT prison_file_number FROM Conviction);

NoSQL query

import express from 'express';

import {Get_collection_name} from '../../persistence/DAOs/JudicialDecisionDAO';
import {PrisonerModel} from "../../persistence/schemes/Prisoner";

// Mini. app. for '/Under_remand' sub-path:
export const router = express.Router();
// curl http://localhost:1963/NYCP/API/Under_remand
router.get('/', (request, response) => {
    console.log("Use case: " + request.baseUrl + request.path);
    PrisonerModel.aggregate([
        {
            $lookup:
                {
                    from: Get_collection_name(), // Search in 'judicial_decisions' collection...
                    let: {prisoner_id: "$_id"}, // Get '_id' field in 'prisoners' collection...
                    pipeline: [
                        {
                            $match:
                                {
                                    $expr:
                                        { // 'prisoner' field in 'judicial_decisions' collection matches with 'prisoner_id'
                                            $and: [{$eq: ["$decision_type_number", "1"]}, {$eq: ["$prisoner", "$$prisoner_id"]}]
                                        }
                                }
                        }
                    ],
                    as: "convictions"
                }
        },
        // Only keep those for whom 'convictions' array (added) field is empty, i.e., no conviction yet...
        {$match: {convictions: {$size: 0}}},
        // Only 'prison_file_number' data...
        {$project: {_id: false, prison_file_number: true}}
    ]).then(data => response.status(200).json(data))
        .catch(error => response.status(400).json({error, message: error.message}));
});