# Step 2 - Discovering app template

By
The Quable Team

Project structure
Linter & prettier
Application configuration and environnement variables
Index.ts
Application middleware
Application router
Application controller
Application services



# Project structure

This Quable application model is built over the MVC (Model-View-Controller) pattern to provide an organized structure, a clear separation of concerns and a more fluid and efficient development experience.

.
├── database/
│   └── schema.prisma
├── public/
│   ├── images/
│   │   └── quable-logo.png
│   ├── scripts/
│   │   ├── ui/
│   │   │   ├── error.js
│   │   │   └── loader.js
│   │   ├── utils/
│   │   │   └── custom-fetch.ts
│   │   └── index.js
│   ├── styles/
│   │   └── app.css
│   └── views/
│       ├── component/
│       │   └── banner.ejs
│       ├── layout/
│       │   └── layout.ejs
│       └── pages/
│           └── index.ejs
├── src/
│   ├── controllers/
│   │   └── app.controller.ts
│   ├── middlewares/
│   │   └── http-logger.middleware.ts
│   │   └── session.middleware.ts
│   ├── routes/
│   │   └── app.routes.ts
│   ├── services/
│   │   ├── app.service.ts
│   │   └── database.service.ts
│   ├── helper/
│   │   ├── session/
│   │   │   ├── route.ts
│   │   │   ├── auth.ts
│   │   ├── config.ts
│   │   └── types.ts
│   └── index.ts
├── .env
├── .eslintrc.json
├── .eslintignore
├── .prettierignore
├── .prettierrc
├── quable.app.yml
├── package.json
├── README.md
└── tsconfig.json

In this project, there's a database directory containing a Prisma schema file, followed by a public directory for public resources. In a src folder, there are directories for controllers, middleware, routes and services. In addition, in the root of the project, there's an .env file for environment variables, as well as configuration files for linter and prettier.


# Linter & prettier

Linters and Prettier are essential tools in modern software development for maintaining code quality and consistency.

Linters analyzes source code to detect and report patterns or practices that could lead to errors or issues, enforcing coding standards and best practices. They ensure code correctness, improve readability, and identify potential bugs early in the development process. The linter configuration is defined .eslintrc.json

Prettier focuses on code formatting, automatically enforcing a consistent and aesthetically pleasing style guide. It eliminates debates over code formatting preferences and ensures that code is consistently formatted regardless of individual developers' styles. The prettier configuration is defined .prettierrc.json


# Application configuration and environnement variables

Our project's root contains 2 essential files: the application configuration file quable.app.yml and the environment variables file .env:

  • Application configuration file : It holds essential configuration settings for our Quable application. The application_type field specifies how the application should be displayed (in_app or document). The quable_pim_scope field determines the permission level for the application on the Quable pim. Possible value are full_access or read_access.
application_type: in_app
quable_pim_scope:
  - full_access
  • Environment variables file : It contains environment variables necessary for our application runtime. These variables are essential for configuring database connections (DATABASE_URL), specifying the application's port (QUABLE_APP_PORT), defining the application host url (QUABLE_APP_HOST_URL) and your partner credentials (QUABLE_PARTNER_SECRET & QUABLE_PARTNER_ID).
DATABASE_URL=file:./dev.db
QUABLE_APP_PORT=4000
QUABLE_APP_HOST_URL=<your-application-host>
QUABLE_PARTNER_SECRET=<your-partner-secret>
QUABLE_PARTNER_ID=<your-partner-id>

# Index.ts

In the src directory, there's index.ts. It initializes a Node.js application using Express.js and define application routing, middleware, and runtime configurations.

  • The function setupAppConfig initializes the application's configuration by readingquable.app.yml and applying the found configuration to the Express.js app.

  • Security middlewares: Enabling CORS support, JSON parsing, cookie handling, and URL-encoded form data processing to ensure the security and integrity of the application.

// Security
app.use(cors());
app.use(express.json());
app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));
  • Application middlewares: Used in the application to enhance functionality of our application.
// Middleware
app.use(httpLoggerMiddleware);
app.use(sessionMiddleware);
  • View engine and static files: prepares the application for rendering views using EJS templates, specifies the location of views and layouts, and serves static assets from the public directory
app.use(expressLayouts);
app.use(express.static(join(__dirname, "..", "public")));
app.set("view engine", "ejs");
app.set("views", join(__dirname, "..", "public", "views"));
app.set("layout", "layouts/layout");
  • Application routers: Specifies that for the root path (/), we'll use the appRouter for routing and handling incoming requests. Any request to the root URL of the application will be processed and routed according to the logic defined in the appRouter.
app.use("/", appRouter);

# Application middleware

Middlewares are intermediary functions used in web applications to process HTTP requests. They enable the addition of features like authentication, data validation, and error handling before requests reach the final route handlers. We have 2 middlewares a located in src/middlewares folder:

  • The sessionMiddleware manages user sessions and authentication for all routes. It intercepts incoming requests, distinguish between public and protected routes. Public routes are allowed without authentication, while protected routes invoke the handleAuthAndSession function for authentication and session management.
export async function sessionMiddleware(
  req: any,
  res: Response,
  next: NextFunction,
) {
  if (isPublicRoute(req)) {
    next();
    return;
  }

  try {
    await handleAuthAndSession(req, res);
    next();
  } catch (error) {
    res.status(401).send(`Unauthorized: ${error.message}`);
  }
}

async function handleAuthAndSession(req: any, res: Response) {
  let authToken = req.cookies.token;
  let decoded: SessionData | undefined;

  try {
    if (isNewThirdPartyCall(req) || !authToken) {
      authToken = generateNewAuthToken(req.query);
    }
    decoded = verifyAuthToken(authToken) as SessionData;
  } catch (error) {
    authToken = handleTokenError(error, authToken);
  }
  await finalizeRequest(req, res, decoded!, authToken);
}

The isPublicRoute function determines if a route requires authentication. It also maintains a list of public routes. For example, if you want a route like /health-check to bypass the handleAuthAndSession process, add it to the array of unProtectedRoutes inside this function.

The session middleware also checks that the application is correctly installed for the current instance. This verification process adds two additional properties, customer and quableInstance, to the request object. These properties are accessible to your entire code base once the middleware has been executed.

  • The httpLoggerMiddleware configures HTTP request logging with the morgan library. A custom token is used to extract the customer identifier from the request object.
export function httpLoggerMiddleware(
  req: any,
  res: Response,
  next: NextFunction
) {
  morgan.token("instance", (req: any) =>
    req.customer ? `[${req.customer}]` : ""
  );

  const logFormat = ":instance :method :url :status :response-time ms";

  const excludeRoutePattern = /\.\w+(\?.*)?$/;

  if (!excludeRoutePattern.test(req.url)) {
    morgan(logFormat)(req, res, next);
  } else {
    next();
  }
}

# Application router

Routes are important components in web applications that define the mapping between URLs and actions to be executed. They direct user requests to the appropriate controller handlers for processing and responding to requests. We have one router located in src/routes folder:

  • GET / : When a GET request is made to the root path, it is routed to the appController.renderIndexPage method for processing.

  • POST / : When a POST request is made to the root path, it is routed to the appController.launchDocumentApp method for processing. This route is typically used for launching a document type Quable app.

  • GET /permission : When a GET request is made to /permission, it's routed to the appController.getQuablePIMScope method for processing.

  • GET /install: When a POST request is made to "/install," it is routed to the appController.installApp method for processing.

const appRouter = Router();

appRouter.get("/", appController.renderIndexPage);
appRouter.post("/", appController.launchDocumentApp);
appRouter.get("/permission", appController.getQuablePIMScope);
appRouter.post("/install", appController.installApp);

# Application controller

Controllers handle actions and data processing based on user requests. They act as intermediaries between routes and the application's business logic. We have one controller located in src/controllers folder:

  • renderIndexPage : It renders an EJS view located at pages/index.ejs and sends the rendered HTML as a response. This view is typically used to display the application's main page.

  • getQuablePIMScope : It's used to determine permissions scopes of the application, by returning the quable_pim_scope value.

  • installApp : It calls appService.installApp method with the request body to initiate the installation process.

  • launchDocumentApp : It calls the appService.launchDocumentApp method with the request body to generate a URL for launching the document app.

public renderIndexPage = async (_req: any, res: Response) => {
  return res.render('pages/index.ejs');
};

public getQuablePIMScope = async (req: any, res: Response) => {
  const quablePIMScope = req.app?.settings?.quable_pim_scope;
  if (quablePIMScope) {
    return res.status(200).send(quablePIMScope);
  } else {
    return res.status(404).send('quable_pim_scope not found');
  }
};

public installApp = async (req: any, res: Response) => {
  const installationResponse = await appService.installApp(req.body);
  return res
    .status(installationResponse.statusCode)
    .send(installationResponse);
};

public launchDocumentApp = async (req: any, res: Response) => {
  const launchResponse = await appService.launchDocumentApp(req.body);
  return res.status(launchResponse.statusCode).send(launchResponse);
};

# Application services

Services handle business logic and data operations. They encapsulate application functionality for efficient reuse across different parts of the application.

# 1. Application service

The service class provided here encapsulates essential functionalities for managing interactions with the Quable platform within your application. It offers methods ensure the secure installation to check and save authentication tokens.

  • The checkAuthToken method validates an authentication token by making an HTTP GET request to Quable API.
public checkAuthToken = async (instanceName: string, authToken: string) => {
  try {
    await axios.get(`https://${instanceName}.quable.com/api`, {
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${authToken}`,
      },
    });
    return true;
  } catch (error) {
    return false;
  }
};
  • The installApp method manages installation of the Quable app. It validates the token using checkAuthToken and updates/creates a Quable instance record in the database.
  public installApp = async (payload: Record<string, any>) => {
    const response = { statusCode: 201, message: 'OK', data: null };

    try {
      const { quableInstanceName, quableAuthToken } = payload.data;

      if (!(await this.checkAuthToken(quableInstanceName, quableAuthToken))) {
        response.statusCode = 500;
        response.message =
          'Installation failed. Please check your information.';
        return response;
      }

      const instanceData = {
        authToken: quableAuthToken,
        name: quableInstanceName,
      };

      let instance = await databaseService.quableInstance.findFirst({
        where: { name: quableInstanceName },
      });

      instance = instance
        ? await databaseService.quableInstance.update({
            where: { id: instance.id },
            data: instanceData,
          })
        : await databaseService.quableInstance.create({ data: instanceData });

      response.message = `Installed on ${instance.name}.quable.com`;
    } catch (error) {
      response.statusCode = 500;
      response.message = error.message;
    }

    return response;
  };
  • The launchDocumentApp ensures that document application works properly. Since document applications are triggered by a POST request and await a session URL, we'll return a new url builded with current URL parameters.
public launchDocumentApp = async (payload: Record<string, any>) => {
  const response = { statusCode: 200, message: 'OK', url: '', err: 0 };

  try {
    const { quableInstanceName, dataLocale, interfaceLocale, userId } =
      payload.data;

    response.url = `${process.env.QUABLE_APP_HOST_URL}?quableInstanceName=${quableInstanceName}&dataLocale=${dataLocale}&interfaceLocale=${interfaceLocale}&userId=${userId}`;
  } catch (error) {
    response.statusCode = 500;
    response.message = error.message;
    response.err = 1;
  }

  return response;
};

# 2. Database service

As we mentioned earlier, we're using Prisma as the ORM for the database. At the root of the project, there's a database folder containing a schema that describes our database. This service will enable you to manipulate your database tables more easily.

class DatabaseService extends PrismaClient {
  private static instance: DatabaseService;

  private constructor() {
    super({
      datasources: {
        db: {
          url: process.env.DATABASE_URL,
        },
      },
    });
  }

  public static getInstance(): DatabaseService {
    if (!this.instance) {
      this.instance = new DatabaseService();
    }
    return this.instance;
  }
}