Skip to content
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
7 changes: 4 additions & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
module.exports = {
extends: [
'airbnb-typescript/base',
'plugin:@typescript-eslint/recommended'
'plugin:@typescript-eslint/recommended',
],
parser: '@typescript-eslint/parser',
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
project: './tsconfig.json',
},
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-explicit-any': ['error'],
'max-classes-per-file': 'off',
'no-param-reassign': ['error', { 'props': false }],
'no-param-reassign': ['error', { props: false }],
'import/prefer-default-export': 'off',
'no-underscore-dangle': 'off',
'no-use-before-define': ['error', 'nofunc'],
Expand Down
2 changes: 1 addition & 1 deletion .husky/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1 @@
_
_
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm run lint:ci && npm run db:format
npm run lint:ci && npm run prisma:format
80 changes: 80 additions & 0 deletions misc/new_resource/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable import/no-extraneous-dependencies */

import inquirer from 'inquirer';
import chalk from 'chalk';
import pluralize from 'pluralize';

import { Options } from './types';

/**
* Prompt the user for the name of the resource to be created
*
* @return Resource name (lowercase)
*/
export async function queryResourceName() {
const { resourceName } = await inquirer.prompt([{
name: 'resourceName',
type: 'input',
message: `How should your new API resource be named ? ${chalk.red('(singular)')}`,
validate: (input) => input !== '',
}]);
return resourceName.toLowerCase();
}

/**
* Prompt the user for the plural form of the resource to be created
* Try first to guess it using grammatical rules
* If incorrect, let the user write it
*
* @param resourceName The singular form of the resource name
* @return The plural form of the resource name
*/
export async function queryPluralizedResourceName(resourceName: string) {
const guess = pluralize(resourceName);
const { isPluralCorrect } = await inquirer.prompt([{
name: 'isPluralCorrect',
type: 'list',
message: `Is '${chalk.blue(guess)}' the correct plural form ?`,
choices: ['Yes', 'No'],
}]);

if (isPluralCorrect === 'Yes') {
return guess.toLowerCase();
}

const { pluralizedResourceName } = await inquirer.prompt([{
name: 'pluralizedResourceName',
type: 'input',
message: 'Write it in the plural form :',
validate: (input) => input !== '',
}]);
return pluralizedResourceName.toLowerCase();
}

/**
* Prompt the user for the given `options`
* The `desc` field of each option will be displayed, and `default` will be taken in account
* Directly fill the `value` field of each `options`
*
* @param options
*/
export async function queryOptions(options: Options) {
const { options: selectedOptions }: { options: string[] } = await inquirer.prompt([{
name: 'options',
type: 'checkbox',
message: 'Select options',
choices: Object
.entries(options)
.map(([name, option]) => ({
value: name,
name: option.desc,
checked: option.default,
})),
}]);

Object
.entries(options)
.forEach(([name, option]) => {
option.value = selectedOptions.includes(name);
});
}
36 changes: 36 additions & 0 deletions misc/new_resource/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/* eslint-disable import/no-extraneous-dependencies */

import clui from 'clui';

import type { Options } from './types';
import { queryOptions, queryPluralizedResourceName, queryResourceName } from './cli';
import { templateNewResource } from './templater';

async function main() {
const options: Options = {
database_model: {
desc: 'Create a default database model',
default: true,
},
tests: {
desc: 'Create test files and extend requester',
default: true,
},
};

const resourceName = await queryResourceName();
const resourceNamePluralized = await queryPluralizedResourceName(resourceName);
// await queryOptions(options);

const spinner = new clui.Spinner('Generating new API resource...');
spinner.start();
const success = await templateNewResource(resourceName, resourceNamePluralized);
spinner.stop();

if (!success) {
console.error('Aborting...');
process.exit(1);
}
}

main();
50 changes: 50 additions & 0 deletions misc/new_resource/template/controllers.template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { capitalize } from '../utils';

const controllersTemplate = (singular: string, plural: string) => `\
import httpStatus from 'http-status-codes';
import createError from 'http-errors';
import { ${capitalize(singular)} } from '@prisma/client';

import db from '../../appDatabase';

import type { ${capitalize(singular)}CreateDto, ${capitalize(singular)}UpdateDto } from './${singular}Types';
import { build${capitalize(singular)}Ro } from './${singular}Helpers';

export async function list${capitalize(plural)}(userId: string) {
const ${plural} = await db.${singular}.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
});
return ${plural}.map((${singular}) => build${capitalize(singular)}Ro(${singular}));
}

export async function createNew${capitalize(singular)}(userId: string, payload: ${capitalize(singular)}CreateDto) {
const ${singular} = await db.${singular}.create({
data: {
...payload,
user: {
connect: { id: userId },
},
},
});
return build${capitalize(singular)}Ro(${singular});
}

export async function get${capitalize(singular)}(${singular}: ${capitalize(singular)}) {
return build${capitalize(singular)}Ro(${singular});
}

export async function update${capitalize(singular)}(${singular}: ${capitalize(singular)}, payload: ${capitalize(singular)}UpdateDto) {
const updated${capitalize(singular)} = await db.${singular}.update({
where: { id: ${singular}.id },
data: payload,
});
return build${capitalize(singular)}Ro(updated${capitalize(singular)});
}

export async function delete${capitalize(singular)}(${singular}: ${capitalize(singular)}) {
await db.${singular}.delete({ where: { id: ${singular}.id } });
}
`;

export default controllersTemplate;
25 changes: 25 additions & 0 deletions misc/new_resource/template/helpers.template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { capitalize } from '../utils';

const helpersTemplate = (singular: string) => `\
import type { ${capitalize(singular)} } from '@prisma/client';

import type { ${capitalize(singular)}Ro } from './${singular}Types';

/**
* Build a ${singular} Response Object (RO) with only the fields to be shown to the user
* Can be used to compute or add extra informations to the ${singular}
* object, useful for front-end display
*
* @param ${singular} The ${singular} object to format
* @returns A ${singular} Response Object ready to be sent into API responses
*/
export function build${capitalize(singular)}Ro(${singular}: ${capitalize(singular)}): ${capitalize(singular)}Ro {
return {
id: ${singular}.id,
userId: ${singular}.userId,
createdAt: ${singular}.createdAt,
};
}
`;

export default helpersTemplate;
41 changes: 41 additions & 0 deletions misc/new_resource/template/middleware.template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { capitalize } from '../utils';

const middlewareTemplate = (singular: string, plural: string) => `\
import handler from 'express-async-handler';
import httpStatus from 'http-status-codes';
import createError from 'http-errors';

import db from '../../appDatabase';

/**
* Middleware used to check if the requested ${singular} exists in database
* If it exists, stores it in \`res.locals\` to forward it to controllers
*
* @throws 400 - Bad request | If the route parameters are missing
* @throws 404 - Not found | If the ${singular} doesn't exist
*
* @example
* router.get('/${singular}/:${singular}Id', ${singular}Middleware, (req, res) => {
* const { ${singular} } = res.locals;
* });
*/
const ${singular}Middleware = handler(async (req, res, next) => {
const { userId, ${singular}Id } = req.params;
if (!userId || !${singular}Id) {
next(createError(httpStatus.BAD_REQUEST, 'Missing route parameters "userId" and/or "${singular}Id"'));
return;
}

const ${singular} = await db.${singular}.findFirst({ where: { id: ${singular}Id, authorId: userId } });
if (!${singular}) {
next(createError(httpStatus.NOT_FOUND, \`${capitalize(singular)} \${${singular}Id} of user \${userId} not found\`));
} else {
res.locals.${singular} = ${singular};
next();
}
});

export default ${singular}Middleware;
`;

export default middlewareTemplate;
12 changes: 12 additions & 0 deletions misc/new_resource/template/prisma.template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { capitalize } from '../utils';

const prismaTemplate = (singular: string) => `\
model ${capitalize(singular)} {
id String @id @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String
createdAt DateTime @default(now())
}
`;

export default prismaTemplate;
71 changes: 71 additions & 0 deletions misc/new_resource/template/routes.template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { capitalize } from '../utils';

const routesTemplate = (singular: string, plural: string) => `\
import express from 'express';
import handler from 'express-async-handler';
import httpStatus from 'http-status-codes';

import ownershipMiddleware from '../../middlewares/ownershipMiddleware';
import authMiddleware from '../../middlewares/authMiddleware';
import validate from '../../middlewares/validationMiddleware';

import ${singular}Middleware from './${singular}Middleware';
import * as controllers from './${singular}Controllers';
import { ${capitalize(singular)}CreateDto, ${capitalize(singular)}UpdateDto } from './${singular}Types';

const router = express.Router();

router.get(
'/users/:userId/${plural}',
authMiddleware,
handler(async (req, res) => {
const ${plural} = await controllers.list${capitalize(plural)}(req.params.userId);
res.send(${plural});
}),
);

router.${singular}(
'/users/:userId/${plural}',
validate(${capitalize(singular)}CreateDto),
ownershipMiddleware,
handler(async (req, res) => {
const ${singular} = await controllers.createNew${capitalize(singular)}(req.params.userId, req.body);
res.status(httpStatus.CREATED).send(${singular});
}),
);

router.get(
'/users/:userId/${plural}/:${singular}Id',
authMiddleware,
${singular}Middleware,
handler(async (req, res) => {
const ${singular} = await controllers.get${capitalize(singular)}(res.locals.${singular});
res.send(${singular});
}),
);

router.patch(
'/users/:userId/${plural}/:${singular}Id',
validate(${capitalize(singular)}UpdateDto),
ownershipMiddleware,
${singular}Middleware,
handler(async (req, res) => {
const ${singular} = await controllers.update${capitalize(singular)}(res.locals.${singular}, req.body);
res.send(${singular});
}),
);

router.delete(
'/users/:userId/${plural}/:${singular}Id',
ownershipMiddleware,
${singular}Middleware,
handler(async (req, res) => {
await controllers.delete${capitalize(singular)}(res.locals.${singular});
res.sendStatus(httpStatus.NO_CONTENT);
}),
);

export default router;
`;

export default routesTemplate;
Loading