diff --git a/.github/CONTRIBUTING.MD b/.github/CONTRIBUTING.MD index 2546e67df0..c44b99f455 100644 --- a/.github/CONTRIBUTING.MD +++ b/.github/CONTRIBUTING.MD @@ -127,7 +127,7 @@ Here is the list of its sub-directories: | Directory | Description | | --- | --- | | generators | Contains the code which renders the templates or updates the files | -| mocks | Contains some pieces of code used to test the file "updaters" | +| fixtures | Contains some pieces of code used to test the file "updaters" | | specs | Defines how the generated files should look like in different scenarios (specifications) | | templates | Contains the actual templates used to generate the files | | utils | Contains some helpers shared by all the generators | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6ca60b536..3c992d7d57 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,9 +16,6 @@ jobs: node-version: [8, 10] env: - AUTH0_DOMAIN: ${{ secrets.AUTH0_DOMAIN }} - AUTH0_AUDIENCE: ${{ secrets.AUTH0_AUDIENCE }} - AUTH0_TOKEN: ${{ secrets.AUTH0_TOKEN }} SETTINGS_AWS_ACCESS_KEY_ID: ${{ secrets.SETTINGS_AWS_ACCESS_KEY_ID }} SETTINGS_AWS_SECRET_ACCESS_KEY: ${{ secrets.SETTINGS_AWS_SECRET_ACCESS_KEY }} NODE_VERSION: ${{ matrix.node-version }} diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 35f5ea34ec..1ceca0afca 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -38,10 +38,10 @@ * [Hooks](./architecture/hooks.md) * [Initialization](./architecture/initialization.md) * Databases - * [TypeORM (SQL & noSQL)](./databases/typeorm.md) + * [SQL Databases (TypeORM)](./databases/typeorm.md) * [Create Models & Queries](./databases/create-models-and-queries.md) * [Generate & Run Migrations](./databases/generate-and-run-migrations.md) - * [Use Mongoose (MongoDB)](./databases/using-mongoose.md) + * [MongoDB (TypeORM or Mongoose)](./databases/mongodb.md) * [Use Another ORM](./databases/using-another-orm.md) * Authentication & Access Control * [Quick Start](./authentication-and-access-control/quick-start.md) diff --git a/docs/api-section/rest-blueprints.md b/docs/api-section/rest-blueprints.md index 361bfb008d..75433a454c 100644 --- a/docs/api-section/rest-blueprints.md +++ b/docs/api-section/rest-blueprints.md @@ -5,10 +5,10 @@ foal generate rest-api product --register ``` -Building a REST API is often a common task when creating an application. To avoid reinventing the wheel, FoalTS provides an integrated command to achieve this. +Building a REST API is often a common task when creating an application. To avoid reinventing the wheel, FoalTS provides a CLI command to achieve this. ``` -foal generate rest-api [--register] +foal generate rest-api [--register] [--auth] ``` This command generates three files: an entity, a controller and the controller's test. Depending on your directory structure, they may be generated in different locations: @@ -116,7 +116,18 @@ export const productSchhema = { }; ``` -## Generate OpenAPI documentation +## Using Authentication + +If you wish to attach a user to the resource, you can use the `--auth` flag to do so. + +*Example:* +``` +foal generate rest-api product --auth +``` + +This flags adds an `owner: User` column to your entity and uses it in the API. + +## Generating OpenAPI documentation The generated controllers also have OpenAPI decorators on their methods to document the API. diff --git a/docs/architecture/controllers.md b/docs/architecture/controllers.md index 87eaa19248..5e8276e635 100644 --- a/docs/architecture/controllers.md +++ b/docs/architecture/controllers.md @@ -120,7 +120,7 @@ import { Context, HttpResponseCreated, Post } from '@foal/core'; class AppController { @Post('/products') createProduct(ctx: Context) { - const requestBody = ctx.request.body; + const body = ctx.request.body; // Do something. return new HttpResponseCreated(); } @@ -140,7 +140,7 @@ import { Context, HttpResponseOK, Post } from '@foal/core'; class AppController { @Get('/products/:id') - createProduct(ctx: Context) { + readProduct(ctx: Context) { const productId = ctx.request.params.id; // Do something. return new HttpResponseOK(/* something */); @@ -161,7 +161,7 @@ import { Context, HttpResponseOK, Post } from '@foal/core'; class AppController { @Get('/products') - createProduct(ctx: Context) { + readProducts(ctx: Context) { const limit = ctx.request.query.limit; // Do something. return new HttpResponseOK(/* something */); @@ -201,6 +201,25 @@ class AppController { } ``` + +#### The Controller Method Arguments + +> Available in Foal v1.9.0 onwards. + +The path paramaters and request body are also passed as second and third arguments to the controller method. + +```typescript +import { Context, HttpResponseCreated, Put } from '@foal/core'; + +class AppController { + @Put('/products/:id') + updateProduct(ctx: Context, { id }, body) { + // Do something. + return new HttpResponseCreated(); + } +} +``` + ## HTTP Responses HTTP responses are defined using `HttpResponse` objects. Each controller method must return an instance of this class (or a *promise* of this instance). diff --git a/docs/databases/mongodb.md b/docs/databases/mongodb.md new file mode 100644 index 0000000000..7c7c91292b --- /dev/null +++ b/docs/databases/mongodb.md @@ -0,0 +1,139 @@ +# MongoDB + +FoalTS provides two ways to interact with a MongoDB database in your application: [Mongoose](https://mongoosejs.com/) and [TypeORM](https://typeorm.io/#/). + +## Usage with Mongoose + +### Generating a new project with Mongoose + +When creating an application with the `--mongodb` flag, the CLI generates a new project with `mongoose` and `@foal/mongoose` installed. The `User` model is defined using this ODM as well as the `create-user` script. + +``` +foal createapp my-app --mongodb +``` + +### Generating a model + +You cannot create *entities* in a Mongoose project, as it is specific to TypeORM. Instead, you can use this command to generate a new model: + +``` +foal g model +``` + +### Configuration + +The URI of the MongoDB database can be passed through: +- the config file `config/default.json` with the `mongodb.uri` key, +- or with the environment variable `MONGODB_URI`. + +*Example (`config/default.json`)*: +```json +{ + ... + "mongodb": { + "uri": "mongodb://localhost:27017/db" + } +} +``` + +### Authentication + +#### The `MongoDBStore` + +``` +npm install @foal/mongodb +``` + +If you use sessions with `@TokenRequired` or `@TokenOptional`, you must use the `MongoDBStore` from `@foal/mongodb`. + +#### The `fetchUser` function + +*Example with JSON Web Tokens*: +```typescript +import { JWTRequired } from '@foal/jwt'; +import { fetchUser } from '@foal/mongoose'; + +import { User } from '../models'; + +@JWTRequired({ user: fetchUser(User) }) +class MyController {} +``` + +## Usage with TypeORM + +``` +npm uninstall sqlite3 +npm install mongodb +``` + +### Configuration + +*ormconfig.js* +```js +const { Config } = require('@foal/core'); + +module.exports = { + type: "mongodb", + database: Config.get2('database.database', 'string'), + dropSchema: Config.get2('database.dropSchema', 'boolean', false), + entities: ["build/app/**/*.entity.js"], + host: Config.get2('database.host', 'string'), + port: Config.get2('database.port', 'number'), + synchronize: Config.get2('database.synchronize', 'boolean', false) +} + +``` + + +*config/default.json* +```json +{ + "database": { + "database": "mydb", + "host": "localhost", + "port": 27017 + } +} +``` + +### Authentication + +#### The `MongoDBStore` + +``` +npm install @foal/mongodb +``` + +If you use sessions with `@TokenRequired` or `@TokenOptional`, you must use the `MongoDBStore` from `@foal/mongodb`. **The TypeORMStore does not work with noSQL databases.** + +#### The `fetchMongoDBUser` function + +*user.entity.ts* +```typescript +import { Entity, ObjectID, ObjectIdColumn } from 'typeorm'; + +@Entity() +export class User { + + @ObjectIdColumn() + id: ObjectID; + +} +``` + +*Example with JSON Web Tokens*: +```typescript +import { JWTRequired } from '@foal/jwt'; +import { fetchMongoDBUser } from '@foal/typeorm'; + +import { User } from '../entities'; + +@JWTRequired({ user: fetchMongoDBUser(User) }) +class MyController {} +``` + +## Limitations + +When using MongoDB, there are some features that are not available: +- the `foal g rest-api ` command, +- and the *Groups & Permissions* system. \ No newline at end of file diff --git a/docs/databases/typeorm.md b/docs/databases/typeorm.md index c74ead4f4d..3def354ff3 100644 --- a/docs/databases/typeorm.md +++ b/docs/databases/typeorm.md @@ -1,5 +1,7 @@ # TypeORM +> *FoalTS components using TypeORM officially support the following databases: PostgreSQL, MySQL, MariaDB and SQLite*. + *A simple model:* ```typescript import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; diff --git a/docs/databases/using-mongoose.md b/docs/databases/using-mongoose.md deleted file mode 100644 index e3dc416e3e..0000000000 --- a/docs/databases/using-mongoose.md +++ /dev/null @@ -1,60 +0,0 @@ -# Using Mongoose (MongoDB) - -The previous sections have shown how to use TypeORM with FoalTS. But Foal provides also a support for using [Mongoose](https://mongoosejs.com/), a popular MongoDB ODM. - -## Generating a new project with Mongoose/MongoDB - -When creating an application with the `--mongodb` flag, the CLI generates a new project with `mongoose` and `@foal/mongoose` installed. The `User` model is defined using this ODM as well as the `create-user` script. - -``` -foal createapp my-app --mongodb -``` - -## Generating a model - -You cannot create *entities* in a Mongoose project, as it is specific to TypeORM. Instead, you can use this command to generate a new model: - -``` -foal g model -``` - -## Mongoose configuration - -The URI of the MongoDB database can be passed through: -- the config file `config/default.json` with the `mongodb.uri` key, -- or with the environment variable `MONGODB_URI`. - -*Example (`config/default.json`)*: -```json -{ - ... - "mongodb": { - "uri": "mongodb://localhost:27017/db" - } -} -``` - -## Running migrations - -The concept of migrations does not exist in MongoDB. That's why there is no migration commands in a Mongoose project. - -## Usage with `JWTRequired` - -The `@foal/mongoose` package provides a `fetchUser` function to be used with `JWTRequired` or `TokenRequired`. It takes an id as parameter and returns a Mongoose model or undefined if the id does not match any user. - -*Example with JSON Web Tokens*: -```typescript -import { JWTRequired } from '@foal/jwt'; -import { fetchUser } from '@foal/mongoose'; - -import { User } from '../models'; - -@JWTRequired({ user: fetchUser(User) }) -class MyController {} -``` - -## Limitations - -When using Mongoose in place of TypeORM, there are some features that are not available: -- the `foal g rest-api ` command, -- and the *Groups & Permissions* system. \ No newline at end of file diff --git a/docs/development-environment/code-generation.md b/docs/development-environment/code-generation.md index e40b14a9f3..1b734c3202 100644 --- a/docs/development-environment/code-generation.md +++ b/docs/development-environment/code-generation.md @@ -20,12 +20,69 @@ foal g controller Create a new controller in `./src/app/controllers`, in `./controllers` or in the current directory depending on which folders are found. -If you are in the root directory and you want to automatically register the controller within the app controller you can add the `--register` flag. +*Example* +```shell +foal g controller auth +foal g controller api/product +``` + +*Output* +``` +src/ + '- app/ + '- controllers/ + |- api/ + | |- product.controller.ts + | '- index.ts + |- auth.controller.ts + '- index.ts +``` + +### The `--register` flag ```shell foal g controller --register ``` +If you wish to automatically create a new route attached to this controller, you can use the `--register` flag to do so. + +*Example* +```shell +foal g controller api --register +foal g controller api/product --register +``` + +*Output* +``` +src/ + '- app/ + |- controllers/ + | |- api/ + | | |- product.controller.ts + | | '- index.ts + | |- api.controller.ts + | '- index.ts + '- app.controller.ts +``` + +*app.controller.ts* +```typescript +export class AppController { + subControllers = [ + controller('/api', ApiController) + ] +} +``` + +*api.controller.ts* +```typescript +export class ApiController { + subControllers = [ + controller('/product', ProductController) + ] +} +``` + ## Create an entity (simple model) ```shell @@ -89,4 +146,22 @@ Create a new sub-app with all its files in `./src/app/sub-apps`, in `./sub-apps` foal g service ``` -Create a new service in `./src/app/services`, in `./services` or in the current directory depending on which folders are found. \ No newline at end of file +Create a new service in `./src/app/services`, in `./services` or in the current directory depending on which folders are found. + +*Example* +```shell +foal g service auth +foal g service apis/github +``` + +*Output* +``` +src/ + '- app/ + '- services/ + |- apis/ + | '- github.service.ts + | '- index.ts + |- auth.service.ts + '- index.ts +``` \ No newline at end of file diff --git a/e2e_test.sh b/e2e_test.sh index 807fbe36e0..d28f83eab9 100755 --- a/e2e_test.sh +++ b/e2e_test.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash set -e mkdir e2e-test-temp diff --git a/lerna.json b/lerna.json index 19b3475be8..9c9da19d33 100644 --- a/lerna.json +++ b/lerna.json @@ -3,5 +3,5 @@ "packages": [ "packages/*" ], - "version": "1.8.1" + "version": "1.9.0" } diff --git a/package-lock.json b/package-lock.json index 71186876a5..3b344e2913 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2692,9 +2692,9 @@ "dev": true }, "typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", + "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", "dev": true }, "uglify-js": { diff --git a/packages/acceptance-tests/package-lock.json b/packages/acceptance-tests/package-lock.json index 28991bb44e..1dbf6c7250 100644 --- a/packages/acceptance-tests/package-lock.json +++ b/packages/acceptance-tests/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/acceptance-tests", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/acceptance-tests/package.json b/packages/acceptance-tests/package.json index fa208ae842..cf88b99e40 100644 --- a/packages/acceptance-tests/package.json +++ b/packages/acceptance-tests/package.json @@ -1,7 +1,7 @@ { "name": "@foal/acceptance-tests", "private": true, - "version": "1.8.1", + "version": "1.9.0", "description": "Acceptance tests of the framework", "scripts": { "test": "mocha --require ts-node/register \"./src/**/*.spec.{ts,tsx}\"", @@ -11,16 +11,16 @@ "url": "https://github.com/sponsors/LoicPoullain" }, "dependencies": { - "@foal/core": "^1.8.1", - "@foal/csrf": "^1.8.1", - "@foal/formidable": "^1.8.1", - "@foal/jwks-rsa": "^1.8.1", - "@foal/jwt": "^1.8.1", - "@foal/mongodb": "^1.8.1", - "@foal/mongoose": "^1.8.1", - "@foal/redis": "^1.8.1", - "@foal/typeorm": "^1.8.1", - "@foal/typestack": "^1.8.1", + "@foal/core": "^1.9.0", + "@foal/csrf": "^1.9.0", + "@foal/formidable": "^1.9.0", + "@foal/jwks-rsa": "^1.9.0", + "@foal/jwt": "^1.9.0", + "@foal/mongodb": "^1.9.0", + "@foal/mongoose": "^1.9.0", + "@foal/redis": "^1.9.0", + "@foal/typeorm": "^1.9.0", + "@foal/typestack": "^1.9.0", "@types/express": "~4.17.2", "@types/express-rate-limit": "~3.3.3", "@types/formidable": "~1.0.31", diff --git a/packages/acceptance-tests/src/authentication/jwt.jwks.spec.ts b/packages/acceptance-tests/src/authentication/jwt.jwks.spec.ts index d6ced48784..44a1015f20 100644 --- a/packages/acceptance-tests/src/authentication/jwt.jwks.spec.ts +++ b/packages/acceptance-tests/src/authentication/jwt.jwks.spec.ts @@ -7,10 +7,9 @@ import { join } from 'path'; // 3p import { sign } from 'jsonwebtoken'; import * as superagent from 'superagent'; -import * as request from 'supertest'; // FoalTS -import { Config, createApp, Get, HttpResponseOK } from '@foal/core'; +import { createApp, Get, HttpResponseOK } from '@foal/core'; import { getRSAPublicKeyFromJWKS } from '@foal/jwks-rsa'; import { JWTRequired } from '@foal/jwt'; @@ -82,110 +81,4 @@ describe('[Authentication|JWT|JWKS] Users can be authenticated with a JWKS retre } }); - it('from Auth0.', () => { - const domain = Config.get2('auth0.domain', 'string'); - const audience = Config.get2('auth0.audience', 'string'); - const token = Config.get2('auth0.token', 'string'); - - if (token === undefined) { - console.warn('AUTH0_TOKEN not defined. Skipping this test...'); - return; - } - - class AppController { - - @Get('/api/users/me') - @JWTRequired({ - secretOrPublicKey: getRSAPublicKeyFromJWKS({ - cache: true, - jwksRequestsPerMinute: 5, - jwksUri: `https://${domain}/.well-known/jwks.json`, - rateLimit: true, - }) - }, { - algorithms: [ 'RS256' ], - audience, - issuer: `https://${domain}/`, - }) - getUser() { - return new HttpResponseOK({ - name: 'Alix' - }); - } - - } - - const app = createApp(AppController); - - return request(app) - .get('/api/users/me') - .set('Authorization', 'Bearer ' + token) - .expect(200) - .then(response => { - deepStrictEqual(response.body, { - name: 'Alix' - }); - }); - }); - - it('from AWS Cognito.', async () => { - const clientId = Config.get2('cognito.clientId', 'string'); - const domain = Config.get2('cognito.domain', 'string'); - const refreshToken = Config.get2('cognito.refreshToken', 'string'); - let token: string; - const region = Config.get2('cognito.region', 'string'); - const userPoolId = Config.get2('cognito.userPoolId', 'string'); - - if (refreshToken === undefined) { - console.warn('COGNITO_REFRESH_TOKEN not defined. Skipping this test...'); - return; - } - - try { - const { body } = await superagent - .post(`https://${domain}.auth.${region}.amazoncognito.com/oauth2/token`) - .send('grant_type=refresh_token') - .send(`client_id=${clientId}`) - .send(`refresh_token=${refreshToken}`); - token = body.id_token; - } catch (error) { - throw new Error('Requesting a new access token failed.'); - } - - class AppController { - - @Get('/api/users/me') - @JWTRequired({ - secretOrPublicKey: getRSAPublicKeyFromJWKS({ - cache: true, - jwksRequestsPerMinute: 5, - jwksUri: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/jwks.json`, - rateLimit: true, - }) - }, { - algorithms: [ 'RS256' ], - audience: clientId, - issuer: `https://cognito-idp.${region}.amazonaws.com/${userPoolId}`, - }) - getUser() { - return new HttpResponseOK({ - name: 'Alix' - }); - } - - } - - const app = createApp(AppController); - - return request(app) - .get('/api/users/me') - .set('Authorization', 'Bearer ' + token) - .expect(200) - .then(response => { - deepStrictEqual(response.body, { - name: 'Alix' - }); - }); - }); - }); diff --git a/packages/acceptance-tests/src/typeorm.mongodb-store.spec.ts b/packages/acceptance-tests/src/typeorm.mongodb-store.spec.ts new file mode 100644 index 0000000000..305229d953 --- /dev/null +++ b/packages/acceptance-tests/src/typeorm.mongodb-store.spec.ts @@ -0,0 +1,232 @@ +// std +import { strictEqual } from 'assert'; + +// 3p +import { + Context, + createApp, + dependency, + ExpressApplication, + Get, + hashPassword, + Hook, + HttpResponseForbidden, + HttpResponseNoContent, + HttpResponseOK, + HttpResponseUnauthorized, + Post, + Session, + TokenRequired, + ValidateBody, + verifyPassword +} from '@foal/core'; +import { MongoDBStore } from '@foal/mongodb'; +import { MongoClient } from 'mongodb'; +import * as request from 'supertest'; + +// FoalTS +import { fetchMongoDBUser } from '@foal/typeorm'; +import { + Column, + Connection, + createConnection, + Entity, + getMongoRepository, + ObjectID, + ObjectIdColumn +} from '@foal/typeorm/node_modules/typeorm'; + +describe('[Sample] TypeORM & MongoDB Store', async () => { + + const MONGODB_URI = 'mongodb://localhost:27017/e2e_db'; + + let app: ExpressApplication; + let token: string; + let mongoClient: MongoClient; + + @Entity() + class User { + @ObjectIdColumn() + id: ObjectID; + + @Column({ unique: true }) + email: string; + + @Column() + password: string; + + @Column() + isAdmin: boolean; + } + + function AdminRequired() { + return Hook((ctx: Context) => { + if (!ctx.user.isAdmin) { + return new HttpResponseForbidden(); + } + }); + } + + @TokenRequired({ user: fetchMongoDBUser(User), store: MongoDBStore }) + class MyController { + @Get('/foo') + foo() { + return new HttpResponseOK(); + } + + @Get('/bar') + @AdminRequired() + bar() { + return new HttpResponseOK(); + } + } + + class AuthController { + @dependency + store: MongoDBStore; + + @Post('/logout') + @TokenRequired({ store: MongoDBStore, extendLifeTimeOrUpdate: false }) + async logout(ctx: Context) { + await this.store.destroy(ctx.session.sessionID); + return new HttpResponseNoContent(); + } + + @Post('/login') + @ValidateBody({ + additionalProperties: false, + properties: { + email: { type: 'string', format: 'email' }, + password: { type: 'string' } + }, + required: ['email', 'password'], + type: 'object', + }) + async login(ctx: Context) { + const user = await getMongoRepository(User).findOne({ email: ctx.request.body.email }); + + if (!user) { + return new HttpResponseUnauthorized(); + } + + if (!await verifyPassword(ctx.request.body.password, user.password)) { + return new HttpResponseUnauthorized(); + } + const session = await this.store.createAndSaveSessionFromUser({ id: user.id.toString() }); + return new HttpResponseOK({ + token: session.getToken() + }); + } + } + + class AppController { + subControllers = [ + MyController, + AuthController + ]; + } + + let connection: Connection; + + before(async () => { + process.env.SETTINGS_SESSION_SECRET = 'session-secret'; + process.env.MONGODB_URI = 'mongodb://localhost:27017/e2e_db'; + connection = await createConnection({ + database: 'e2e_db', + dropSchema: true, + entities: [User], + host: 'localhost', + port: 27017, + type: 'mongodb', + }); + + mongoClient = await MongoClient.connect(MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true }); + + await mongoClient.db().collection('foalSessions').deleteMany({}); + + const user = new User(); + user.email = 'john@foalts.org'; + user.password = await hashPassword('password'); + user.isAdmin = false; + await getMongoRepository(User).save(user); + + app = createApp(AppController); + }); + + after(async () => { + delete process.env.SETTINGS_SESSION_SECRET; + delete process.env.MONGODB_URI; + return Promise.all([ + connection.close(), + (await app.foal.services.get(MongoDBStore).getMongoDBInstance()).close(), + mongoClient.close() + ]); + }); + + it('should work.', async () => { + /* Try to access routes that require authentication and a specific permission */ + + await Promise.all([ + request(app).get('/foo').expect(400), + request(app).get('/bar').expect(400), + ]); + + /* Try to login with a wrong email */ + + await request(app) + .post('/login') + .send({ email: 'mary@foalts.org', password: 'password' }) + .expect(401); + + /* Try to login with a wrong password */ + + await request(app) + .post('/login') + .send({ email: 'john@foalts.org', password: 'wrong-password' }) + .expect(401); + + /* Log in */ + + await request(app) + .post('/login') + .send({ email: 'john@foalts.org', password: 'password' }) + .expect(200) + .then(response => { + strictEqual(typeof response.body.token, 'string'); + token = response.body.token; + }); + + /* Access and try to access routes that require authentication and a specific permission */ + + await Promise.all([ + request(app).get('/foo').set('Authorization', `Bearer ${token}`).expect(200), + request(app).get('/bar').set('Authorization', `Bearer ${token}`).expect(403), + ]); + + /* Add the admin group and permission */ + + const user2 = await getMongoRepository(User).findOne({ email: 'john@foalts.org' }); + if (!user2) { + throw new Error('John was not found in the database.'); + } + + user2.isAdmin = true; + await getMongoRepository(User).save(user2); + + /* Access the route that requires a specific permission */ + + await request(app).get('/bar').set('Authorization', `Bearer ${token}`).expect(200); + + /* Log out */ + + await request(app).post('/logout').set('Authorization', `Bearer ${token}`).expect(204); + + /* Try to access routes that require authentication and a specific permission */ + + await Promise.all([ + request(app).get('/foo').set('Authorization', `Bearer ${token}`).expect(401), + request(app).get('/bar').set('Authorization', `Bearer ${token}`).expect(401), + ]); + }); + +}); diff --git a/packages/acceptance-tests/src/upload-and-download.spec.ts b/packages/acceptance-tests/src/upload-and-download.spec.ts index e4d7f438c2..16affc2570 100644 --- a/packages/acceptance-tests/src/upload-and-download.spec.ts +++ b/packages/acceptance-tests/src/upload-and-download.spec.ts @@ -77,7 +77,7 @@ describe('Upload & Download Files', () => { .expect(200) .expect('Content-Type', 'image/png') .expect('Content-Length', image.length.toString()) - .expect('Content-Disposition', 'attachement; filename="download.png"') + .expect('Content-Disposition', 'attachment; filename="download.png"') .expect(image); }); diff --git a/packages/acceptance-tests/src/upload-and-download.typeorm.spec.ts b/packages/acceptance-tests/src/upload-and-download.typeorm.spec.ts index 7b5b483bc8..1c44ad08b3 100644 --- a/packages/acceptance-tests/src/upload-and-download.typeorm.spec.ts +++ b/packages/acceptance-tests/src/upload-and-download.typeorm.spec.ts @@ -134,7 +134,7 @@ describe('Upload & Download Files (TypoORM)', () => { .expect(200) .expect('Content-Type', 'image/png') .expect('Content-Length', image.length.toString()) - .expect('Content-Disposition', 'attachement; filename="download.png"') + .expect('Content-Disposition', 'attachment; filename="download.png"') .expect(image); }); diff --git a/packages/aws-s3/package-lock.json b/packages/aws-s3/package-lock.json index ab6d38946f..ea628a301a 100644 --- a/packages/aws-s3/package-lock.json +++ b/packages/aws-s3/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/aws-s3", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/aws-s3/package.json b/packages/aws-s3/package.json index ed7b6b0df0..9ae8ee3ec4 100644 --- a/packages/aws-s3/package.json +++ b/packages/aws-s3/package.json @@ -1,6 +1,6 @@ { "name": "@foal/aws-s3", - "version": "1.8.1", + "version": "1.9.0", "description": "AWS S3 storage components for FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -44,8 +44,8 @@ "lib/" ], "dependencies": { - "@foal/core": "^1.8.1", - "@foal/storage": "^1.8.1", + "@foal/core": "^1.9.0", + "@foal/storage": "^1.9.0", "aws-sdk": "~2.640.0" }, "devDependencies": { diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore new file mode 100644 index 0000000000..a7a637a6e3 --- /dev/null +++ b/packages/cli/.gitignore @@ -0,0 +1 @@ +test-generators diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index 86fbca8700..026a943a8b 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/cli", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 93bce34201..52b97e0b53 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,11 +1,13 @@ { "name": "@foal/cli", - "version": "1.8.1", + "version": "1.9.0", "description": "CLI tool for FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", "scripts": { - "test": "npm run test:generators && npm run test:run-script && npm run test:create-secret && npm run test:rmdir", + "test": "npm run test:generators && npm run test:run-script && npm run test:create-secret && npm run test:rmdir && npm run test:fs", + "test:fs": "mocha --file \"./src/test.ts\" --require ts-node/register \"./src/generate/file-system.spec.ts\"", + "dev:test:fs": "mocha --file \"./src/test.ts\" --require ts-node/register --watch --watch-extensions ts \"./src/generate/file-system.spec.ts\"", "test:generators": "mocha --file \"./src/test.ts\" --require ts-node/register \"./src/generate/generators/**/*.spec.ts\"", "dev:test:generators": "mocha --file \"./src/test.ts\" --require ts-node/register --watch --watch-extensions ts \"./src/generate/generators/**/*.spec.ts\"", "test:rmdir": "mocha --file \"./src/test.ts\" --require ts-node/register \"./src/rmdir/**/*.spec.ts\"", diff --git a/packages/cli/src/generate/file-system.spec.ts b/packages/cli/src/generate/file-system.spec.ts new file mode 100644 index 0000000000..5cb6fa03e6 --- /dev/null +++ b/packages/cli/src/generate/file-system.spec.ts @@ -0,0 +1,1065 @@ +// std +import { notStrictEqual, strictEqual } from 'assert'; +import { existsSync, mkdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +// FoalTS +import { ClientError, FileSystem } from './file-system'; + +function rmdir(path: string) { + if (existsSync(path)) { + rmdirSync(path); + } +} + +function rmfile(path: string) { + if (existsSync(path)) { + unlinkSync(path); + } +} + +function mkdir(path: string) { + if (!existsSync(path)) { + mkdirSync(path); + } +} + +describe('FileSystem', () => { + + let fs: FileSystem; + + beforeEach(() => fs = new FileSystem()); + + describe('has a "cd" method that', () => { + + it('should change the current directory.', () => { + strictEqual(fs.currentDir, ''); + fs.cd('foobar/foo'); + strictEqual(fs.currentDir.replace(/\\/g, '/'), 'foobar/foo'); + fs.cd('../bar'); + strictEqual(fs.currentDir.replace(/\\/g, '/'), 'foobar/bar'); + }); + + }); + + describe('has a "cdProjectRootDir" that', () => { + + let cliPkg: Buffer; + let rootPkg: Buffer; + + before(() => { + cliPkg = readFileSync('package.json'); + unlinkSync('package.json'); + + rootPkg = readFileSync('../../package.json'); + unlinkSync('../../package.json'); + }); + + after(() => { + writeFileSync('../../package.json', rootPkg); + writeFileSync('package.json', cliPkg); + }); + + beforeEach(() => { + mkdir('test-generators'); + mkdir('test-generators/foo'); + mkdir('test-generators/foo/bar'); + }); + + afterEach(() => { + rmfile('test-generators/package.json'); + rmdir('test-generators/foo/bar'); + rmdir('test-generators/foo'); + rmdir('test-generators'); + }); + + it('should root the current working directory to the project root directory.', () => { + writeFileSync( + 'test-generators/package.json', + JSON.stringify({ + dependencies: { + '@foal/core': 'versionNumber' + } + }), + 'utf8' + ); + fs.cd('foo/bar'); + fs.cdProjectRootDir(); + strictEqual(fs.currentDir, '.'); + }); + + it('should throw a ClienError if the package.json is not a valid JSON.', () => { + writeFileSync( + 'test-generators/package.json', + 'hello', + 'utf8' + ); + + fs.cd('foo/bar'); + try { + fs.cdProjectRootDir(); + throw new Error('An error should have been thrown'); + } catch (error) { + if (!(error instanceof ClientError)) { + throw new Error('The error thrown should be an instance of ClientError.'); + } + strictEqual( + error.message, + 'The file package.json is not a valid JSON. Unexpected token h in JSON at position 0' + ); + } + }); + + it('should throw a ClienError if the package.json found does not have @foal/core as dependency (no deps).', () => { + writeFileSync( + 'test-generators/package.json', + JSON.stringify({}), + 'utf8' + ); + + fs.cd('foo/bar'); + try { + fs.cdProjectRootDir(); + throw new Error('An error should have been thrown'); + } catch (error) { + if (!(error instanceof ClientError)) { + throw new Error('The error thrown should be an instance of ClientError.'); + } + strictEqual( + error.message, + 'This project is not a FoalTS project. The dependency @foal/core is missing in package.json.' + ); + } + }); + + it('should throw a ClienError if the package.json found does not have @foal/core as dependency (>=1 dep).', () => { + writeFileSync( + 'test-generators/package.json', + JSON.stringify({ + dependencies: {} + }), + 'utf8' + ); + + fs.cd('foo/bar'); + try { + fs.cdProjectRootDir(); + throw new Error('An error should have been thrown'); + } catch (error) { + if (!(error instanceof ClientError)) { + throw new Error('The error thrown should be an instance of ClientError.'); + } + strictEqual( + error.message, + 'This project is not a FoalTS project. The dependency @foal/core is missing in package.json.' + ); + } + }); + + it('should throw a ClientError if no package.json is found.', () => { + fs.cd('foo/bar'); + try { + fs.cdProjectRootDir(); + throw new Error('An error should have been thrown'); + } catch (error) { + if (!(error instanceof ClientError)) { + throw new Error('The error thrown should be an instance of ClientError.'); + } + strictEqual( + error.message, + 'This project is not a FoalTS project. No package.json found.' + ); + } + }); + + }); + + describe('has a "exists" method that', () => { + + beforeEach(() => { + mkdir('test-generators'); + writeFileSync('test-generators/foo.txt', Buffer.alloc(3)); + }); + + afterEach(() => { + rmfile('test-generators/foo.txt'); + rmdir('test-generators'); + }); + + it('should return true if the file or directory exists.', () => { + strictEqual(fs.exists('foo.txt'), true); + }); + + it('should return true if the file or directory does not exist.', () => { + strictEqual(fs.exists('bar.txt'), false); + }); + + }); + + describe('has a "ensureDir" method that', () => { + + beforeEach(() => { + mkdir('test-generators'); + mkdir('test-generators/foo'); + }); + + afterEach(() => { + rmdir('test-generators/foo'); + rmdir('test-generators/bar/foo/foobar'); + rmdir('test-generators/bar/foo'); + rmdir('test-generators/bar'); + rmdir('test-generators'); + }); + + it('should create the directory if it does not exist.', () => { + fs.ensureDir('bar'); + if (!existsSync('test-generators/bar')) { + throw new Error('The directory "bar" does not exist.'); + } + }); + + it('should not throw if the directory already exists.', () => { + fs.ensureDir('foo'); + }); + + it('should create all intermediate directories.', () => { + fs.ensureDir('bar/foo/foobar'); + if (!existsSync('test-generators/bar/foo/foobar')) { + throw new Error('The directory "bar/foo/foobar" does not exist.'); + } + }); + + }); + + describe('has a "ensureDirOnlyIf" method that', () => { + + beforeEach(() => { + mkdir('test-generators'); + }); + + afterEach(() => { + rmdir('test-generators/foo'); + rmdir('test-generators'); + }); + + it('should create the directory if the condition is true.', () => { + fs.ensureDirOnlyIf(true, 'foo'); + if (!existsSync('test-generators/foo')) { + throw new Error('The directory "foo" does not exist.'); + } + }); + + it('should not create the directory if the condition is false.', () => { + fs.ensureDirOnlyIf(false, 'foo'); + if (existsSync('test-generators/foo')) { + throw new Error('The directory "foo" should not exist.'); + } + }); + + }); + + describe('has a "ensureFile" method that', () => { + + beforeEach(() => { + mkdir('test-generators'); + writeFileSync('test-generators/foo.txt', 'hello', 'utf8'); + }); + + afterEach(() => { + rmfile('test-generators/bar.txt'); + rmfile('test-generators/foo.txt'); + rmdir('test-generators'); + }); + + it('should create the file if it does not exist.', () => { + fs.ensureFile('bar.txt'); + if (!existsSync('test-generators/bar.txt')) { + throw new Error('The file "bar.txt" does not exist.'); + } + }); + + it('should not erase the file if it exists.', () => { + fs.ensureFile('foo.txt'); + strictEqual( + readFileSync('test-generators/foo.txt', 'utf8'), + 'hello' + ); + }); + + }); + + describe('has a "copy" method that', () => { + + const templateDir = join(__dirname, 'templates/test-file-system'); + const templatePath = join(__dirname, 'templates/test-file-system/tpl.txt'); + + beforeEach(() => { + mkdir('test-generators'); + mkdir(templateDir); + writeFileSync(templatePath, 'hello', 'utf8'); + }); + + afterEach(() => { + rmfile(templatePath); + rmdir(templateDir); + + rmfile('test-generators/hello.txt'); + rmdir('test-generators'); + }); + + it('should copy the file from the `templates` directory.', () => { + fs.copy('test-file-system/tpl.txt', 'hello.txt'); + if (!existsSync('test-generators/hello.txt')) { + throw new Error('The file "test-generators/hello.txt" does not exist.'); + } + strictEqual( + readFileSync('test-generators/hello.txt', 'utf8'), + 'hello' + ); + }); + + it('should throw an error if the file does not exist.', () => { + try { + fs.copy('test-file-system/foobar.txt', 'hello.txt'); + throw new Error('An error should have been thrown'); + } catch (error) { + strictEqual(error.message, 'The template "test-file-system/foobar.txt" does not exist.'); + } + }); + + }); + + describe('has a "copyOnlyIf" method that', () => { + + const templateDir = join(__dirname, 'templates/test-file-system'); + const templatePath = join(__dirname, 'templates/test-file-system/tpl.txt'); + + beforeEach(() => { + mkdir('test-generators'); + mkdir(templateDir); + writeFileSync(templatePath, 'hello', 'utf8'); + }); + + afterEach(() => { + rmfile(templatePath); + rmdir(templateDir); + + rmfile('test-generators/hello.txt'); + rmdir('test-generators'); + }); + + it('should copy the file if the condition is true.', () => { + fs.copyOnlyIf(true, 'test-file-system/tpl.txt', 'hello.txt'); + if (!existsSync('test-generators/hello.txt')) { + throw new Error('The file "test-generators/hello.txt" does not exist.'); + } + }); + + it('should not copy the file if the condition is false.', () => { + fs.copyOnlyIf(false, 'test-file-system/tpl.txt', 'hello.txt'); + if (existsSync('test-generators/hello.txt')) { + throw new Error('The file "test-generators/hello.txt" should not exist.'); + } + }); + + }); + + describe('has a "render" method that', () => { + + const templateDir = join(__dirname, 'templates/test-file-system'); + const templatePath = join(__dirname, 'templates/test-file-system/tpl.txt'); + + beforeEach(() => { + mkdir('test-generators'); + mkdir(templateDir); + writeFileSync(templatePath, '/* foobar */ /* foobar */ /* barfoo */!', 'utf8'); + }); + + afterEach(() => { + rmfile(templatePath); + rmdir(templateDir); + + rmfile('test-generators/hello.txt'); + rmdir('test-generators'); + }); + + it('should copy and render the template from the `templates` directory.', () => { + fs.render('test-file-system/tpl.txt', 'hello.txt', { + barfoo: 'world', + foobar: 'hello', + }); + if (!existsSync('test-generators/hello.txt')) { + throw new Error('The file "test-generators/hello.txt" does not exist.'); + } + strictEqual( + readFileSync('test-generators/hello.txt', 'utf8'), + 'hello hello world!' + ); + }); + + it('should throw an error if the template does not exist.', () => { + try { + fs.render('test-file-system/foobar.txt', 'hello.txt', {}); + throw new Error('An error should have been thrown'); + } catch (error) { + strictEqual(error.message, 'The template "test-file-system/foobar.txt" does not exist.'); + } + }); + + }); + + describe('has a "renderOnlyIf" method that', () => { + + const templateDir = join(__dirname, 'templates/test-file-system'); + const templatePath = join(__dirname, 'templates/test-file-system/tpl.txt'); + + beforeEach(() => { + mkdir('test-generators'); + mkdir(templateDir); + writeFileSync(templatePath, 'hello', 'utf8'); + }); + + afterEach(() => { + rmfile(templatePath); + rmdir(templateDir); + + rmfile('test-generators/hello.txt'); + rmdir('test-generators'); + }); + + it('should copy the file if the condition is true.', () => { + fs.renderOnlyIf(true, 'test-file-system/tpl.txt', 'hello.txt', {}); + if (!existsSync('test-generators/hello.txt')) { + throw new Error('The file "test-generators/hello.txt" does not exist.'); + } + }); + + it('should not copy the file if the condition is false.', () => { + fs.renderOnlyIf(false, 'test-file-system/tpl.txt', 'hello.txt', {}); + if (existsSync('test-generators/hello.txt')) { + throw new Error('The file "test-generators/hello.txt" should not exist.'); + } + }); + + }); + + describe('has a "modify" method that', () => { + + beforeEach(() => { + mkdir('test-generators'); + writeFileSync('test-generators/hello.txt', 'hello', 'utf8'); + }); + + afterEach(() => { + rmfile('test-generators/hello.txt'); + rmdir('test-generators'); + }); + + it('should modify the file with the given callback.', () => { + fs.modify('hello.txt', content => content + ' world!'); + strictEqual( + readFileSync('test-generators/hello.txt', 'utf8'), + 'hello world!' + ); + }); + + it('should throw a ClientError if the file does not exist.', () => { + try { + fs.modify('test-file-system/foobar.txt', content => content); + throw new Error('An error should have been thrown'); + } catch (error) { + if (!(error instanceof ClientError)) { + throw new Error('The error thrown should be an instance of ClientError.'); + } + strictEqual(error.message, 'Impossible to modify "test-file-system/foobar.txt": the file does not exist.'); + } + }); + + }); + + describe('has a "modifyOnlyIf" method that should', () => { + + beforeEach(() => { + mkdir('test-generators'); + writeFileSync('test-generators/hello.txt', 'hello', 'utf8'); + }); + + afterEach(() => { + rmfile('test-generators/hello.txt'); + rmdir('test-generators'); + }); + + it('should modify the file with the given callback if the condition is true.', () => { + fs.modifyOnlyfIf(true, 'hello.txt', content => content + ' world!'); + strictEqual( + readFileSync('test-generators/hello.txt', 'utf8'), + 'hello world!' + ); + }); + + it('should not modify the file with the given callback if the condition is false.', () => { + fs.modifyOnlyfIf(false, 'hello.txt', content => content + ' world!'); + strictEqual( + readFileSync('test-generators/hello.txt', 'utf8'), + 'hello' + ); + }); + + }); + + describe('has a "addNamedExportIn" method that should', () => { + + beforeEach(() => { + mkdir('test-generators'); + writeFileSync('test-generators/hello.txt', 'export { foo } from \'foo.txt\';\n', 'utf8'); + }); + + afterEach(() => { + rmfile('test-generators/hello.txt'); + rmdir('test-generators'); + }); + + it('should add a named import at the bottom of the file.', () => { + fs.addNamedExportIn('hello.txt', 'bar', 'bar.txt'); + strictEqual( + readFileSync('test-generators/hello.txt', 'utf8'), + 'export { foo } from \'foo.txt\';\nexport { bar } from \'bar.txt\';\n' + ); + }); + + }); + + describe('has an "addOrExtendNamedImportIn" method that should', () => { + + beforeEach(() => { + mkdir('test-generators'); + writeFileSync( + 'test-generators/empty.txt', + 'class FooBar {}', + 'utf8' + ); + writeFileSync( + 'test-generators/hello.txt', + '// 3p\n' + + 'import { Hello } from \'./foo.txt\';\n' + + 'import { World } from \'./bar.txt\';\n' + + '\n' + + 'class FooBar {}', + 'utf8' + ); + }); + + afterEach(() => { + rmfile('test-generators/empty.txt'); + rmfile('test-generators/hello.txt'); + rmdir('test-generators'); + }); + + it('should add a named import at the beginning of the file if none exists.', () => { + fs.addOrExtendNamedImportIn('empty.txt', 'FooController', './controllers/foo.controller.txt'); + strictEqual( + readFileSync('test-generators/empty.txt', 'utf8'), + 'import { FooController } from \'./controllers/foo.controller.txt\';\n' + + '\n' + + 'class FooBar {}', + ); + }); + + it('should add a named import after all the imports if it does not already exist.', () => { + fs.addOrExtendNamedImportIn('hello.txt', 'FooController', './controllers/foo.controller.txt'); + strictEqual( + readFileSync('test-generators/hello.txt', 'utf8'), + '// 3p\n' + + 'import { Hello } from \'./foo.txt\';\n' + + 'import { World } from \'./bar.txt\';\n' + + 'import { FooController } from \'./controllers/foo.controller.txt\';\n' + + '\n' + + 'class FooBar {}', + ); + }); + + it('should extend the named import if it already exists and it does not have the specifier.', () => { + fs.addOrExtendNamedImportIn('hello.txt', 'MyController', './bar.txt'); + strictEqual( + readFileSync('test-generators/hello.txt', 'utf8'), + '// 3p\n' + + 'import { Hello } from \'./foo.txt\';\n' + + 'import { MyController, World } from \'./bar.txt\';\n' + + '\n' + + 'class FooBar {}', + ); + }); + + it('should not extend the named import if it already exists but it has already the specifier.', () => { + fs.addOrExtendNamedImportIn('hello.txt', 'World', './bar.txt'); + strictEqual( + readFileSync('test-generators/hello.txt', 'utf8'), + '// 3p\n' + + 'import { Hello } from \'./foo.txt\';\n' + + 'import { World } from \'./bar.txt\';\n' + + '\n' + + 'class FooBar {}', + ); + }); + + }); + + describe('has an "addOrExtendClassArrayProperty" method that should', () => { + + beforeEach(() => { + mkdir('test-generators'); + }); + + afterEach(() => { + rmfile('test-generators/foo.txt'); + rmdir('test-generators'); + }); + + it('should add the class property if it does not exist (empty class).', () => { + writeFileSync( + 'test-generators/foo.txt', + 'class FooBar {}', + 'utf8' + ); + fs.addOrExtendClassArrayPropertyIn( + 'foo.txt', + 'subControllers', + 'controller(\'/api\', ApiController)' + ); + strictEqual( + readFileSync('test-generators/foo.txt', 'utf8'), + 'class FooBar {\n' + + ' subControllers = [\n' + + ' controller(\'/api\', ApiController)\n' + + ' ];\n' + + '}', + ); + }); + + it('should add the class property if it does not exist (empty class with line returns).', () => { + writeFileSync( + 'test-generators/foo.txt', + 'class FooBar {\n\n}', + 'utf8' + ); + fs.addOrExtendClassArrayPropertyIn( + 'foo.txt', + 'subControllers', + 'controller(\'/api\', ApiController)' + ); + strictEqual( + readFileSync('test-generators/foo.txt', 'utf8'), + 'class FooBar {\n' + + ' subControllers = [\n' + + ' controller(\'/api\', ApiController)\n' + + ' ];\n' + + '}', + ); + }); + + it('should add the class property if it does not exist (class with existing properties).', () => { + writeFileSync( + 'test-generators/foo.txt', + 'class FooBar {\n' + + ' foo = 3;\n' + + ' bar() {};\n' + + '}', + 'utf8' + ); + fs.addOrExtendClassArrayPropertyIn( + 'foo.txt', + 'subControllers', + 'controller(\'/api\', ApiController)' + ); + strictEqual( + readFileSync('test-generators/foo.txt', 'utf8'), + 'class FooBar {\n' + + ' subControllers = [\n' + + ' controller(\'/api\', ApiController)\n' + + ' ];\n' + + '\n' + + ' foo = 3;\n' + + ' bar() {};\n' + + '}', + ); + }); + + it('should extend the class property if it already exists (empty array).', () => { + writeFileSync( + 'test-generators/foo.txt', + 'class FooBar {\n' + + ' subControllers = [];\n' + + '}', + 'utf8' + ); + fs.addOrExtendClassArrayPropertyIn( + 'foo.txt', + 'subControllers', + 'controller(\'/api\', ApiController)' + ); + strictEqual( + readFileSync('test-generators/foo.txt', 'utf8'), + 'class FooBar {\n' + + ' subControllers = [\n' + + ' controller(\'/api\', ApiController)\n' + + ' ];\n' + + '}', + ); + }); + + it('should extend the class property if it already exists (empty array with line returns).', () => { + writeFileSync( + 'test-generators/foo.txt', + 'class FooBar {\n' + + ' subControllers = [\n' + + '\n' + + ' ];\n' + + '}', + 'utf8' + ); + fs.addOrExtendClassArrayPropertyIn( + 'foo.txt', + 'subControllers', + 'controller(\'/api\', ApiController)' + ); + strictEqual( + readFileSync('test-generators/foo.txt', 'utf8'), + 'class FooBar {\n' + + ' subControllers = [\n' + + ' controller(\'/api\', ApiController)\n' + + ' ];\n' + + '}', + ); + }); + + it('should extend the class property if it already exists (empty array with existing items).', () => { + writeFileSync( + 'test-generators/foo.txt', + 'class FooBar {\n' + + ' subControllers = [\n' + + ' controller(\'\/foo\', FooController),\n' + + ' BarController,\n' + + ' ];\n' + + '}', + 'utf8' + ); + fs.addOrExtendClassArrayPropertyIn( + 'foo.txt', + 'subControllers', + 'controller(\'/api\', ApiController)' + ); + strictEqual( + readFileSync('test-generators/foo.txt', 'utf8'), + 'class FooBar {\n' + + ' subControllers = [\n' + + ' controller(\'\/foo\', FooController),\n' + + ' BarController,\n' + + ' controller(\'/api\', ApiController)\n' + + ' ];\n' + + '}', + ); + }); + + }); + + describe('has a "setUp" method that', () => { + + afterEach(() => { + rmdir('test-generators'); + }); + + it('should create the test client directory.', () => { + fs.setUp(); + if (!existsSync('test-generators')) { + throw new Error('The directory "test-generators" does not exist.'); + } + }); + + it('should set the current directory to none.', () => { + fs.cd('foobar'); + fs.setUp(); + strictEqual(fs.currentDir, ''); + }); + + }); + + describe('has a "projectHasDependency" method that', () => { + + let initialPkg: Buffer; + + before(() => { + mkdir('test-generators'); + initialPkg = readFileSync('package.json'); + writeFileSync('package.json', JSON.stringify({ + dependencies: { + '@foal/core': 'hello', + 'bar': 'world' + } + }), 'utf8'); + }); + + after(() => { + writeFileSync('package.json', initialPkg); + rmdir('test-generators'); + }); + + it('should return true if the project has the dependency in its package.json.', () => { + strictEqual(fs.projectHasDependency('bar'), true); + }); + + it('should return false if the project does not have the dependency in its package.json.', () => { + strictEqual(fs.projectHasDependency('foo'), false); + }); + + it('should not change the current working directory.', () => { + fs.projectHasDependency('commander'); + strictEqual(fs.currentDir, ''); + }); + + }); + + describe('has a "tearDown" method that', () => { + + beforeEach(() => { + mkdir('test-generators'); + mkdir('test-generators/foo'); + writeFileSync('test-generators/foo/bar', Buffer.alloc(2)); + }); + + afterEach(() => { + rmfile('test-generators/foo/bar'); + rmdir('test-generators/foo'); + rmdir('test-generators'); + }); + + it('should remove the test client directory and all its contents.', () => { + fs.tearDown(); + if (existsSync('test-generators')) { + throw new Error('The directory "test-generators" should not exist.'); + } + }); + + it('should set the current directory to none.', () => { + fs.cd('foobar'); + fs.tearDown(); + strictEqual(fs.currentDir, ''); + }); + + }); + + describe('has an "assetExists" method that', () => { + + beforeEach(() => { + mkdir('test-generators'); + writeFileSync('test-generators/foo', Buffer.alloc(2)); + }); + + afterEach(() => { + rmfile('test-generators/foo'); + rmdir('test-generators'); + }); + + it('should throw an error if the file does not exist.', () => { + try { + fs.assertExists('bar'); + throw new Error('An error should have been thrown.'); + } catch (error) { + strictEqual(error.message, 'The file "bar" does not exist.'); + } + }); + + it('should not throw an error if the file exits.', () => { + fs.assertExists('foo'); + }); + + }); + + describe('has an "assetNotExists" method that', () => { + + beforeEach(() => { + mkdir('test-generators'); + writeFileSync('test-generators/foo', Buffer.alloc(2)); + }); + + afterEach(() => { + rmfile('test-generators/foo'); + rmdir('test-generators'); + }); + + it('should throw an error if the file exits.', () => { + try { + fs.assertNotExists('foo'); + throw new Error('An error should have been thrown.'); + } catch (error) { + strictEqual(error.message, 'The file "foo" should not exist.'); + } + }); + + it('should not throw an error if the file does not exist.', () => { + fs.assertNotExists('bar'); + }); + + }); + + describe('has an "assertEmptyDir" that', () => { + + beforeEach(() => { + mkdir('test-generators'); + mkdir('test-generators/foo'); + mkdir('test-generators/bar'); + writeFileSync('test-generators/foo/foobar', Buffer.alloc(2)); + }); + + afterEach(() => { + rmfile('test-generators/foo/foobar'); + rmdir('test-generators/foo'); + rmdir('test-generators/bar'); + rmdir('test-generators'); + }); + + it('should throw an error if the directory is not empty.', () => { + try { + fs.assertEmptyDir('foo'); + throw new Error('An error should have been thrown.'); + } catch (error) { + strictEqual(error.message, 'The directory "foo" should be empty.'); + } + }); + + it('should not throw an error if the directory is empty.', () => { + fs.assertEmptyDir('bar'); + }); + + }); + + describe('has a "assertEqual" that', () => { + + const specDir = join(__dirname, 'specs/test-file-system'); + const specPath = join(__dirname, 'specs/test-file-system/foo.spec'); + const stringSpecPath = join(__dirname, 'specs/test-file-system/foo.spec.txt'); + + beforeEach(() => { + mkdir(specDir); + writeFileSync(specPath, Buffer.alloc(3)); + writeFileSync(stringSpecPath, 'hello\nmy\nworld', 'utf8'); + + mkdir('test-generators'); + writeFileSync('test-generators/foo', Buffer.alloc(3)); + writeFileSync('test-generators/bar', Buffer.alloc(2)); + writeFileSync('test-generators/foo.txt', 'hello\nmy\nworld', 'utf8'); + writeFileSync('test-generators/bar.txt', 'hi\nmy\nearth\n!', 'utf8'); + }); + + afterEach(() => { + rmfile(stringSpecPath); + rmfile(specPath); + rmdir(specDir); + + rmfile('test-generators/foo.txt'); + rmfile('test-generators/bar.txt'); + rmfile('test-generators/foo'); + rmfile('test-generators/bar'); + rmdir('test-generators'); + }); + + it('should throw an error if the two files are different (binary).', () => { + try { + fs.assertEqual('bar', 'test-file-system/foo.spec'); + throw new Error('An error should have been thrown.'); + } catch (error) { + notStrictEqual(error.message, 'An error should have been thrown.'); + } + }); + + it('should not throw an error if the two files are equal (binary).', () => { + fs.assertEqual('foo', 'test-file-system/foo.spec'); + }); + + it('should throw an error if the two files are different (string).', () => { + try { + fs.assertEqual('bar.txt', 'test-file-system/foo.spec.txt'); + throw new Error('An error should have been thrown.'); + } catch (error) { + strictEqual(error.code, 'ERR_ASSERTION'); + strictEqual(error.message.includes('\'hi\\nmy\\nearth\\n!\''), true); + strictEqual(error.message.includes('\'hello\\nmy\\nworld\''), true); + } + }); + + it('should not throw an error if the two files are equal (string).', () => { + fs.assertEqual('foo.txt', 'test-file-system/foo.spec.txt'); + }); + + it('should throw an error if the spec file does not exist.', () => { + try { + fs.assertEqual('foobar', 'test-file-system/hello.txt'); + throw new Error('An error should have been thrown'); + } catch (error) { + strictEqual(error.message, 'The spec file "test-file-system/hello.txt" does not exist.'); + } + }); + + }); + + describe('has a "copyFixture" method that', () => { + + const fixtureDir = join(__dirname, 'fixtures/test-file-system'); + const fixturePath = join(__dirname, 'fixtures/test-file-system/tpl.txt'); + + beforeEach(() => { + mkdir('test-generators'); + mkdir(fixtureDir); + writeFileSync(fixturePath, 'hello', 'utf8'); + }); + + afterEach(() => { + rmfile(fixturePath); + rmdir(fixtureDir); + + rmfile('test-generators/hello.txt'); + rmdir('test-generators'); + }); + + it('should copy the file from the `fixtures` directory.', () => { + fs.copyFixture('test-file-system/tpl.txt', 'hello.txt'); + if (!existsSync('test-generators/hello.txt')) { + throw new Error('The file "test-generators/hello.txt" does not exist.'); + } + strictEqual( + readFileSync('test-generators/hello.txt', 'utf8'), + 'hello' + ); + }); + + it('should throw an error if the file does not exist.', () => { + try { + fs.copyFixture('test-file-system/foobar.txt', 'hello.txt'); + throw new Error('An error should have been thrown'); + } catch (error) { + strictEqual(error.message, 'The fixture file "test-file-system/foobar.txt" does not exist.'); + } + }); + + }); + + describe('has a "rmfile" method that', () => { + + beforeEach(() => { + mkdir('test-generators'); + writeFileSync('test-generators/hello.txt', 'hello', 'utf8'); + }); + + afterEach(() => { + rmfile('test-generators/hello.txt'); + rmdir('test-generators'); + }); + + it('should remove the file.', () => { + fs.rmfile('hello.txt'); + if (existsSync('test-generators/hello.txt')) { + throw new Error('The file "hello.txt" should have been removed.'); + } + }); + }); + +}); diff --git a/packages/cli/src/generate/file-system.ts b/packages/cli/src/generate/file-system.ts new file mode 100644 index 0000000000..076791be43 --- /dev/null +++ b/packages/cli/src/generate/file-system.ts @@ -0,0 +1,605 @@ +// std +import { deepStrictEqual, strictEqual } from 'assert'; +import { + copyFileSync, + existsSync, + mkdirSync, + readdirSync, + readFileSync, + rmdirSync, + statSync, + unlinkSync, + writeFileSync +} from 'fs'; +import { dirname, join, parse } from 'path'; + +// 3p +import { cyan, green } from 'colors/safe'; + +function rmDirAndFiles(path: string) { + const files = readdirSync(path); + for (const file of files) { + const stats = statSync(join(path, file)); + + if (stats.isDirectory()) { + rmDirAndFiles(join(path, file)); + } else { + unlinkSync(join(path, file)); + } + } + + rmdirSync(path); +} + +/** + * Error thrown by the FileSystem which aims to be pretty + * printed without the stacktrace. + * + * @export + * @class ClientError + * @extends {Error} + */ +export class ClientError extends Error {} + +/** + * This class provides more methods that Node std "fs". + * It also allows to create an isolated directory during directory. + * + * @export + * @class FileSystem + */ +export class FileSystem { + + currentDir = ''; + + private readonly testDir = 'test-generators'; + private logs = true; + + /** + * Do not show create and update logs. + * + * @returns {this} + * @memberof FileSystem + */ + hideLogs(): this { + this.logs = false; + return this; + } + + /** + * Changes the current working directory. + * + * @param {string} path - Relative path of the directory. + * @returns {this} + * @memberof FileSystem + */ + cd(path: string): this { + this.currentDir = join(this.currentDir, path); + return this; + } + + /** + * Changes the current working directory to the project root + * directory. + * + * It searches for the closer package.json containing @foal/core + * as dependency. + * + * @returns {this} + * @memberof FileSystem + */ + cdProjectRootDir(): this { + // "/" on Unix, C:\ on Windows + const root = parse(process.cwd()).root; + + while (!this.exists('package.json')) { + if (join(process.cwd(), this.parse('.')) === root) { + throw new ClientError( + 'This project is not a FoalTS project. No package.json found.' + ); + } + this.cd('..'); + } + const content = readFileSync(this.parse('package.json'), 'utf8'); + + let pkg: any; + try { + pkg = JSON.parse(content); + } catch (error) { + throw new ClientError( + `The file package.json is not a valid JSON. ${error.message}` + ); + } + + if (!pkg.dependencies || !pkg.dependencies['@foal/core']) { + throw new ClientError( + 'This project is not a FoalTS project. The dependency @foal/core is missing in package.json.' + ); + } + + return this; + } + + /** + * Checks if a file or directory exists. + * + * @param {string} path - The path relative to the client directory. + * @returns {boolean} + * @memberof FileSystem + */ + exists(path: string): boolean { + return existsSync(this.parse(path)); + } + + /** + * Recursively ensures that a directory exists. If the directory structure does not + * exist, it is created. + * + * @param {string} path - The path relative to the client directory. + * @returns {this} + * @memberof FileSystem + */ + ensureDir(path: string): this { + const dir = dirname(path); + if (dir !== '.') { + this.ensureDir(dir); + } + if (!existsSync(this.parse(path))) { + mkdirSync(this.parse(path)); + } + return this; + } + + /** + * Recursively ensures that a directory exists if the condition is true. + * If the directory structure does not exist, it is created. + * + * @param {boolean} condition - The condition. + * @param {string} path - The path relative to the client directory. + * @returns {this} + * @memberof FileSystem + */ + ensureDirOnlyIf(condition: boolean, path: string): this { + if (condition) { + this.ensureDir(path); + } + return this; + } + + /** + * Ensures that the file exists. If the file does not exist, it is created. + * + * @param {string} path - The path relative to the client directory. + * @returns {this} + * @memberof FileSystem + */ + ensureFile(path: string): this { + if (!existsSync(this.parse(path))) { + this.logCreate(path); + writeFileSync(this.parse(path), '', 'utf8'); + } + return this; + } + + /** + * Copies a file from the `templates` directory. + * + * @param {string} src - The source path relative to the `templates/` directory. + * @param {string} dest - The destination path relative to the client directory. + * @returns {this} + * @memberof FileSystem + */ + copy(src: string, dest: string): this { + const templatePath = join(__dirname, 'templates', src); + if (!existsSync(templatePath)) { + throw new Error(`The template "${src}" does not exist.`); + } + this.logCreate(dest); + copyFileSync( + templatePath, + this.parse(dest) + ); + return this; + } + + /** + * Copies a file from the `templates` directory if the condition is true. + * + * @param {boolean} condition - The condition. + * @param {string} src - The source path relative to the `templates/` directory. + * @param {string} dest - The destination path relative to the client directory. + * @returns {this} + * @memberof FileSystem + */ + copyOnlyIf(condition: boolean, src: string, dest: string): this { + if (condition) { + this.copy(src, dest); + } + return this; + } + + /** + * Copies and renders a template from the `templates` directory. + * + * @param {string} src - The source path relative to the `templates/` directory. + * @param {string} dest - The destination path relative to the client directory. + * @param {*} locals - The template variables. + * @returns {this} + * @memberof FileSystem + */ + render(src: string, dest: string, locals: any): this { + const templatePath = join(__dirname, 'templates', src); + if (!existsSync(templatePath)) { + throw new Error(`The template "${src}" does not exist.`); + } + let content = readFileSync(templatePath, 'utf8'); + for (const key in locals) { + content = content.split(`/* ${key} */`).join(locals[key]); + } + this.logCreate(dest); + writeFileSync(this.parse(dest), content, 'utf8'); + return this; + } + + /** + * Copies and renders a template from the `templates` directory if the condition is true. + * + * @param {boolean} condition - The condition. + * @param {string} src - The source path relative to the `templates/` directory. + * @param {string} dest - The destination path relative to the client directory. + * @param {*} locals - The template variables. + * @returns {this} + * @memberof FileSystem + */ + renderOnlyIf(condition: boolean, src: string, dest: string, locals: any): this { + if (condition) { + this.render(src, dest, locals); + } + return this; + } + + /** + * Reads and modifies the content of a file. + * + * @param {string} path - The path relative to the client directory. + * @param {(content: string) => string} callback - The callback that modifies the content. + * @returns {this} + * @memberof FileSystem + */ + modify(path: string, callback: (content: string) => string): this { + if (!existsSync(this.parse(path))) { + throw new ClientError(`Impossible to modify "${path}": the file does not exist.`); + } + const content = readFileSync(this.parse(path), 'utf8'); + this.logUpdate(path); + writeFileSync(this.parse(path), callback(content), 'utf8'); + return this; + } + + /** + * Reads and modifies the content of a file if the condition is true. + * + * @param {boolean} condition - The condition. + * @param {string} path - The path relative to the client directory. + * @param {(content: string) => string} callback - The callback that modifies the content. + * @returns {this} + * @memberof FileSystem + */ + modifyOnlyfIf(condition: boolean, path: string, callback: (content: string) => string): this { + if (condition) { + this.modify(path, callback); + } + return this; + } + + /** + * Add a named import at the bottom of the file. + * + * @param {string} path - The file path relative to the client directory. + * @param {string} specifier - The import specifier. + * @param {string} source - The import source. + * @returns {this} + * @memberof FileSystem + */ + addNamedExportIn(path: string, specifier: string, source: string): this { + this.modify(path, content => `${content}export { ${specifier} } from '${source}';\n`); + return this; + } + + /** + * Adds or extends a named import at the beginning of the file. + * + * If an import already exists with this source path, it is completed. + * If it does not already exist, it is added at the end of all imports. + * + * @param {string} path - The file path relative to the client directory. + * @param {string} specifier - The import specifier. + * @param {string} source - The import source. + * @returns {this} + * @memberof FileSystem + */ + addOrExtendNamedImportIn(path: string, specifier: string, source: string, options?: { logs: boolean }): this { + const initialLogs = this.logs; + if (options) { + this.logs = options.logs; + } + + this.modify(path, content => { + // TODO: add tests to support double quotes. + const regex = /import (.*) from '(.*)';/g; + let endPos = 0; + + let specifierAlreadyExists = false; + const replacedContent = content.replace(regex, (match, p1, p2, offset: number) => { + endPos = offset + match.length; + const namedImportRegex = new RegExp(`import {(.*)} from \'(.*)\';`); + return match.replace(namedImportRegex, (subString, specifiersStr: string, path: string) => { + if (path !== source) { + return subString; + } + const specifiers = specifiersStr + .split(',') + .map(imp => imp.trim()); + + if (specifiers.includes(specifier)) { + specifierAlreadyExists = true; + return subString; + } + + const newSpecifiers = specifiers + .concat(specifier) + .sort((a, b) => a.localeCompare(b)) + .join(', '); + return `import { ${newSpecifiers} } from '${source}';`; + }); + }); + + if (specifierAlreadyExists) { + return content; + } + + if (replacedContent !== content) { + return replacedContent; + } + + const newImport = `import { ${specifier} } from '${source}';`; + if (endPos === 0) { + return `${newImport}\n\n${content}`; + } + + return content.substr(0, endPos) + '\n' + newImport + content.substr(endPos); + }); + + this.logs = initialLogs; + + return this; + } + + /** + * Creates or adds an element to the array property of a class. + * + * If the class does not exist, this method does nothing. + * + * @param {string} path - The file path relative to the client directory. + * @param {string} className - The class name. + * @param {string} propertyName - The property name. + * @param {string} element - The item to add to the array. + * @returns {this} + * @memberof FileSystem + */ + addOrExtendClassArrayPropertyIn( + path: string, propertyName: string, element: string, options?: { logs: boolean } + ): this { + const initialLogs = this.logs; + if (options) { + this.logs = options.logs; + } + + this.modify(path, content => content.replace( + new RegExp(`class (\\w*) {(.*)}`, 's'), + (match, className: string, p2: string) => { + if (/^(\s)*$/.test(p2)) { + return `class ${className} {\n ${propertyName} = [\n ${element}\n ];\n}`; + } + + const replacedMatch = match.replace( + new RegExp(`( *)${propertyName} = \\[(.*)\\];`, 's'), + (_, spaces, content: string) => { + const items = content + .replace(/,\n/g, '\n') + .split('\n') + .map(e => e.trim()) + .concat(element) + .map(e => `${spaces}${spaces}${e}`); + + const cleanItems: string[] = []; + for (const item of items) { + if (item.trim() !== '') { + cleanItems.push(item); + } + } + return `${spaces}${propertyName} = [\n${cleanItems.join(',\n')}\n${spaces}];`; + } + ); + + if (replacedMatch !== match) { + return replacedMatch; + } + + return `class ${className} {\n ${propertyName} = [\n ${element}\n ];\n${p2}}`; + } + )); + + this.logs = initialLogs; + + return this; + } + + /** + * Returns true if the project package.json has this dependency. + * Returns false otherwise. + * + * @param {string} name - The name of the dependency. + * @returns {boolean} + * @memberof FileSystem + */ + projectHasDependency(name: string): boolean { + const initialCurrentDir = this.currentDir; + + this.cdProjectRootDir(); + const pkg = JSON.parse(readFileSync(this.parse('package.json'), 'utf8')); + + this.currentDir = initialCurrentDir; + return pkg.dependencies.hasOwnProperty(name); + } + + /************************ + Testing Methods + ************************/ + + /** + * Creates the test client directory. Sets current directory to none. + * + * @memberof FileSystem + */ + setUp(): void { + mkdirSync(this.testDir); + this.currentDir = ''; + } + + /** + * Empties and removes the test client directory. Sets current directory to none. + * + * @memberof FileSystem + */ + tearDown(): void { + rmDirAndFiles(this.testDir); + this.currentDir = ''; + } + + /** + * Throws an error if the file or directory does not exist. + * + * @param {string} path - The path relative to the client directory. + * @returns {this} + * @memberof FileSystem + */ + assertExists(path: string): this { + if (!existsSync(this.parse(path))) { + throw new Error(`The file "${path}" does not exist.`); + } + return this; + } + + /** + * Throws an error if the file or directory exists. + * + * @param {string} path - The path relative to the client directory. + * @returns {this} + * @memberof FileSystem + */ + assertNotExists(path: string): this { + if (existsSync(this.parse(path))) { + throw new Error(`The file "${path}" should not exist.`); + } + return this; + } + + /** + * Throws an error if the directory is not empty. + * + * @param {string} path - The path relative to the client directory. + * @memberof FileSystem + */ + assertEmptyDir(path: string): void { + if (readdirSync(this.parse(path)).length > 0) { + throw new Error(`The directory "${path}" should be empty.`); + } + } + + /** + * Throws an error if the two files are different. + * + * @param {string} actual - The path relative to the client directory. + * @param {string} expected - The path relative to the `specs/` directory. + * @param {{ binary: boolean }} [{ binary }={ binary: true }] - Specify if the file is binary. + * @returns {this} + * @memberof FileSystem + */ + assertEqual(actual: string, expected: string, { binary }: { binary: boolean } = { binary: false }): this { + const specPath = join(__dirname, 'specs', expected); + if (!existsSync(specPath)) { + throw new Error(`The spec file "${expected}" does not exist.`); + } + if (binary) { + deepStrictEqual( + readFileSync(this.parse(actual)), + readFileSync(specPath) + ); + } else { + strictEqual( + readFileSync(this.parse(actual), 'utf8'), + readFileSync(specPath, 'utf8') + ); + } + return this; + } + + /** + * Copies a file from the `fixtures` directory. + * + * @param {string} src - The source path relative to the `fixtures/` directory. + * @param {string} dest - The destination path relative to the client directory. + * @returns {this} + * @memberof FileSystem + */ + copyFixture(src: string, dest: string): this { + const fixturePath = join(__dirname, 'fixtures', src); + if (!existsSync(fixturePath)) { + throw new Error(`The fixture file "${src}" does not exist.`); + } + copyFileSync( + fixturePath, + this.parse(dest) + ); + return this; + } + + /** + * Removes a file. + * + * @param {string} path - The path relative to the client directory. + * @memberof FileSystem + */ + rmfile(path: string): void { + unlinkSync(this.parse(path)); + } + + private isTestingEnvironment(): boolean { + return process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST === 'true'; + } + + private parse(path: string) { + if (this.isTestingEnvironment()) { + return join(this.testDir, this.currentDir, path); + } + return join(this.currentDir, path); + } + + private logCreate(path: string) { + path = join(this.currentDir, path); + // && !this.options.noLogs + if (!this.isTestingEnvironment() && this.logs) { + console.log(`${green('CREATE')} ${path}`); + } + } + + private logUpdate(path: string) { + // && !this.options.noLogs + path = join(this.currentDir, path); + if (!this.isTestingEnvironment() && this.logs) { + console.log(`${cyan('UPDATE')} ${path}`); + } + } + +} diff --git a/packages/cli/src/generate/mocks/angular/angular.json b/packages/cli/src/generate/fixtures/angular/angular.json similarity index 100% rename from packages/cli/src/generate/mocks/angular/angular.json rename to packages/cli/src/generate/fixtures/angular/angular.json diff --git a/packages/cli/src/generate/mocks/angular/package.json b/packages/cli/src/generate/fixtures/angular/package.json similarity index 100% rename from packages/cli/src/generate/mocks/angular/package.json rename to packages/cli/src/generate/fixtures/angular/package.json diff --git a/packages/cli/src/generate/fixtures/controller/api.controller.ts b/packages/cli/src/generate/fixtures/controller/api.controller.ts new file mode 100644 index 0000000000..92a663a828 --- /dev/null +++ b/packages/cli/src/generate/fixtures/controller/api.controller.ts @@ -0,0 +1,9 @@ +// 3p +import { MyController, MyController2 } from './api'; + +export class ApiController { + subControllers = [ + controller('/', MyController), + controller('/', MyController2), + ]; +} diff --git a/packages/cli/src/generate/fixtures/controller/app.controller.ts b/packages/cli/src/generate/fixtures/controller/app.controller.ts new file mode 100644 index 0000000000..b73f970b12 --- /dev/null +++ b/packages/cli/src/generate/fixtures/controller/app.controller.ts @@ -0,0 +1,9 @@ +// 3p +import { MyController, MyController2 } from './controllers'; + +export class AppController { + subControllers = [ + controller('/', MyController), + controller('/', MyController2), + ]; +} diff --git a/packages/cli/src/generate/mocks/controller/index.ts b/packages/cli/src/generate/fixtures/controller/index.ts similarity index 100% rename from packages/cli/src/generate/mocks/controller/index.ts rename to packages/cli/src/generate/fixtures/controller/index.ts diff --git a/packages/cli/src/generate/mocks/entity/index.ts b/packages/cli/src/generate/fixtures/entity/index.ts similarity index 100% rename from packages/cli/src/generate/mocks/entity/index.ts rename to packages/cli/src/generate/fixtures/entity/index.ts diff --git a/packages/cli/src/generate/mocks/hook/index.ts b/packages/cli/src/generate/fixtures/hook/index.ts similarity index 100% rename from packages/cli/src/generate/mocks/hook/index.ts rename to packages/cli/src/generate/fixtures/hook/index.ts diff --git a/packages/cli/src/generate/mocks/model/index.ts b/packages/cli/src/generate/fixtures/model/index.ts similarity index 100% rename from packages/cli/src/generate/mocks/model/index.ts rename to packages/cli/src/generate/fixtures/model/index.ts diff --git a/packages/cli/src/generate/fixtures/model/package.json b/packages/cli/src/generate/fixtures/model/package.json new file mode 100644 index 0000000000..dd31387aa4 --- /dev/null +++ b/packages/cli/src/generate/fixtures/model/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@foal/core": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/cli/src/generate/fixtures/model/package.mongoose.json b/packages/cli/src/generate/fixtures/model/package.mongoose.json new file mode 100644 index 0000000000..563af45af7 --- /dev/null +++ b/packages/cli/src/generate/fixtures/model/package.mongoose.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@foal/core": "^1.0.0", + "mongoose": "xxxx" + } +} \ No newline at end of file diff --git a/packages/cli/src/generate/mocks/react/package.json b/packages/cli/src/generate/fixtures/react/package.json similarity index 100% rename from packages/cli/src/generate/mocks/react/package.json rename to packages/cli/src/generate/fixtures/react/package.json diff --git a/packages/cli/src/generate/fixtures/rest-api/app.controller.ts b/packages/cli/src/generate/fixtures/rest-api/app.controller.ts new file mode 100644 index 0000000000..b73f970b12 --- /dev/null +++ b/packages/cli/src/generate/fixtures/rest-api/app.controller.ts @@ -0,0 +1,9 @@ +// 3p +import { MyController, MyController2 } from './controllers'; + +export class AppController { + subControllers = [ + controller('/', MyController), + controller('/', MyController2), + ]; +} diff --git a/packages/cli/src/generate/mocks/rest-api/index.controllers.ts b/packages/cli/src/generate/fixtures/rest-api/index.controllers.ts similarity index 100% rename from packages/cli/src/generate/mocks/rest-api/index.controllers.ts rename to packages/cli/src/generate/fixtures/rest-api/index.controllers.ts diff --git a/packages/cli/src/generate/mocks/rest-api/index.current-dir.ts b/packages/cli/src/generate/fixtures/rest-api/index.current-dir.ts similarity index 100% rename from packages/cli/src/generate/mocks/rest-api/index.current-dir.ts rename to packages/cli/src/generate/fixtures/rest-api/index.current-dir.ts diff --git a/packages/cli/src/generate/mocks/rest-api/index.entities.ts b/packages/cli/src/generate/fixtures/rest-api/index.entities.ts similarity index 100% rename from packages/cli/src/generate/mocks/rest-api/index.entities.ts rename to packages/cli/src/generate/fixtures/rest-api/index.entities.ts diff --git a/packages/cli/src/generate/fixtures/rest-api/package.json b/packages/cli/src/generate/fixtures/rest-api/package.json new file mode 100644 index 0000000000..dd31387aa4 --- /dev/null +++ b/packages/cli/src/generate/fixtures/rest-api/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@foal/core": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/cli/src/generate/fixtures/rest-api/package.mongoose.json b/packages/cli/src/generate/fixtures/rest-api/package.mongoose.json new file mode 100644 index 0000000000..563af45af7 --- /dev/null +++ b/packages/cli/src/generate/fixtures/rest-api/package.mongoose.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@foal/core": "^1.0.0", + "mongoose": "xxxx" + } +} \ No newline at end of file diff --git a/packages/cli/src/generate/fixtures/script/package.json b/packages/cli/src/generate/fixtures/script/package.json new file mode 100644 index 0000000000..dd31387aa4 --- /dev/null +++ b/packages/cli/src/generate/fixtures/script/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@foal/core": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/cli/src/generate/fixtures/script/package.mongoose.json b/packages/cli/src/generate/fixtures/script/package.mongoose.json new file mode 100644 index 0000000000..28c7c4d24a --- /dev/null +++ b/packages/cli/src/generate/fixtures/script/package.mongoose.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@foal/core": "^1.0.0", + "mongoose": "xxx" + } +} \ No newline at end of file diff --git a/packages/cli/src/generate/mocks/service/index.ts b/packages/cli/src/generate/fixtures/service/index.ts similarity index 100% rename from packages/cli/src/generate/mocks/service/index.ts rename to packages/cli/src/generate/fixtures/service/index.ts diff --git a/packages/cli/src/generate/fixtures/vscode-config/package.json b/packages/cli/src/generate/fixtures/vscode-config/package.json new file mode 100644 index 0000000000..dd31387aa4 --- /dev/null +++ b/packages/cli/src/generate/fixtures/vscode-config/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@foal/core": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/cli/src/generate/mocks/vue/package.json b/packages/cli/src/generate/fixtures/vue/package.json similarity index 100% rename from packages/cli/src/generate/mocks/vue/package.json rename to packages/cli/src/generate/fixtures/vue/package.json diff --git a/packages/cli/src/generate/generators/angular/connect-angular.spec.ts b/packages/cli/src/generate/generators/angular/connect-angular.spec.ts index f9f07e4b48..bad5736ed9 100644 --- a/packages/cli/src/generate/generators/angular/connect-angular.spec.ts +++ b/packages/cli/src/generate/generators/angular/connect-angular.spec.ts @@ -1,24 +1,25 @@ -import { mkdirIfDoesNotExist, rmDirAndFilesIfExist, TestEnvironment } from '../../utils'; +import { FileSystem } from '../../file-system'; import { connectAngular } from './connect-angular'; // TODO: To improve: make the tests (more) independent from each other. describe('connectAngular', () => { - afterEach(() => rmDirAndFilesIfExist('connector-test')); + const fs = new FileSystem(); - const testEnv = new TestEnvironment('angular'); + beforeEach(() => fs.setUp()); - it('should create a proxy.conf.json file in ${path}/src.', () => { - mkdirIfDoesNotExist('connector-test/angular/src'); + afterEach(() => fs.tearDown()); - testEnv - .copyFileFromMocks('angular.json', 'connector-test/angular/angular.json') - .copyFileFromMocks('package.json', 'connector-test/angular/package.json'); + it('should create a proxy.conf.json file in ${path}/src.', () => { + fs + .ensureDir('connector-test/angular/src') + .copyFixture('angular/angular.json', 'connector-test/angular/angular.json') + .copyFixture('angular/package.json', 'connector-test/angular/package.json'); connectAngular('./connector-test/angular'); - testEnv - .validateSpec('proxy.conf.json', 'connector-test/angular/src/proxy.conf.json'); + fs + .assertEqual('connector-test/angular/src/proxy.conf.json', 'angular/proxy.conf.json'); }); it('should not throw if the path does not exist.', () => { @@ -26,39 +27,39 @@ describe('connectAngular', () => { }); it('should update angular.json with the proxy file and the output dir.', () => { - mkdirIfDoesNotExist('connector-test/angular/src'); - - testEnv - .copyFileFromMocks('angular.json', 'connector-test/angular/angular.json') - .copyFileFromMocks('package.json', 'connector-test/angular/package.json'); + fs + .ensureDir('connector-test/angular/src') + .copyFixture('angular/angular.json', 'connector-test/angular/angular.json') + .copyFixture('angular/package.json', 'connector-test/angular/package.json'); connectAngular('./connector-test/angular'); - testEnv - .validateSpec('angular.json', 'connector-test/angular/angular.json'); + fs + .assertEqual('connector-test/angular/angular.json', 'angular/angular.json'); }); it('should not throw if angular.json does not exist.', () => { - mkdirIfDoesNotExist('connector-test/angular/src'); + fs + .ensureDir('connector-test/angular/src'); connectAngular('./connector-test/angular'); }); it('should update package.json with the "--prod" flag.', () => { - mkdirIfDoesNotExist('connector-test/angular/src'); - - testEnv - .copyFileFromMocks('angular.json', 'connector-test/angular/angular.json') - .copyFileFromMocks('package.json', 'connector-test/angular/package.json'); + fs + .ensureDir('connector-test/angular/src') + .copyFixture('angular/angular.json', 'connector-test/angular/angular.json') + .copyFixture('angular/package.json', 'connector-test/angular/package.json'); connectAngular('./connector-test/angular'); - testEnv - .validateSpec('package.json', 'connector-test/angular/package.json'); + fs + .assertEqual('connector-test/angular/package.json', 'angular/package.json'); }); it('should not throw if package.json does not exist.', () => { - mkdirIfDoesNotExist('connector-test/angular/src'); + fs + .ensureDir('connector-test/angular/src'); connectAngular('./connector-test/angular'); }); diff --git a/packages/cli/src/generate/generators/angular/connect-angular.ts b/packages/cli/src/generate/generators/angular/connect-angular.ts index 772c54a95d..690ceb5334 100644 --- a/packages/cli/src/generate/generators/angular/connect-angular.ts +++ b/packages/cli/src/generate/generators/angular/connect-angular.ts @@ -1,48 +1,52 @@ import { join, relative } from 'path'; import { red } from 'colors/safe'; -import { existsSync } from 'fs'; -import { Generator } from '../../utils'; +import { FileSystem } from '../../file-system'; export function connectAngular(path: string) { - if (!existsSync(path)) { - if (process.env.NODE_ENV !== 'test') { + const fs = new FileSystem(); + + if (!fs.exists(path)) { + if (process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST !== 'true') { console.log(red(` The directory ${path} does not exist.`)); } return; } - if (!existsSync(join(path, 'angular.json'))) { - if (process.env.NODE_ENV !== 'test') { + if (!fs.exists(join(path, 'angular.json'))) { + if (process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST !== 'true') { console.log(red(` The directory ${path} is not an Angular project (missing angular.json).`)); } return; } - if (!existsSync(join(path, 'package.json'))) { - if (process.env.NODE_ENV !== 'test') { + if (!fs.exists(join(path, 'package.json'))) { + if (process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST !== 'true') { console.log(red(` The directory ${path} is not an Angular project (missing package.json).`)); } return; } - new Generator('angular', path) - .copyFileFromTemplates('proxy.conf.json', 'src/proxy.conf.json') - .updateFile('package.json', content => { + fs + .cd(path) + .copy('angular/proxy.conf.json', 'src/proxy.conf.json') + .modify('package.json', content => { const pkg = JSON.parse(content); pkg.scripts.build = 'ng build --prod'; return JSON.stringify(pkg, null, 2); }) - .updateFile('angular.json', content => { + .modify('angular.json', content => { const config = JSON.parse(content); // Proxy configuration config.projects[config.defaultProject].architect.serve.options.proxyConfig = 'src/proxy.conf.json'; // Output build directory - const outputPath = join(relative(path, process.cwd()), 'public'); + const outputPath = join(relative(path, process.cwd()), 'public') + // Make projects generated on Windows build on Unix. + .replace(/\\/g, '/'); config.projects[config.defaultProject].architect.build.options.outputPath = outputPath; return JSON.stringify(config, null, 2); diff --git a/packages/cli/src/generate/generators/app/create-app.spec.ts b/packages/cli/src/generate/generators/app/create-app.spec.ts index 38844154c6..8bd030f8c4 100644 --- a/packages/cli/src/generate/generators/app/create-app.spec.ts +++ b/packages/cli/src/generate/generators/app/create-app.spec.ts @@ -1,261 +1,277 @@ -// std -import { strictEqual } from 'assert'; -import { mkdirSync, readdirSync } from 'fs'; - // FoalTS -import { TestEnvironment } from '../../utils'; +import { FileSystem } from '../../file-system'; import { createApp } from './create-app'; describe('createApp', () => { - const testEnv = new TestEnvironment('app', 'test-foo-bar'); + const fs = new FileSystem(); + + beforeEach(() => fs.setUp()); - afterEach(() => testEnv.rmDirAndFilesIfExist('../test-foo-bar')); + afterEach(() => fs.tearDown()); it('should abort the project creation if a directory already exists.', async () => { - mkdirSync('test-foo-bar'); + fs.ensureDir('test-foo-bar'); await createApp({ name: 'test-fooBar' }); - const files = readdirSync('test-foo-bar'); - - strictEqual(files.length, 0); + fs.assertEmptyDir('test-foo-bar'); }); it('should render the config templates.', async () => { await createApp({ name: 'test-fooBar' }); - testEnv - .validateSpec('config/default.json') - .shouldNotExist('config/default.yml') - .validateSpec('config/development.json') - .shouldNotExist('config/development.yml') - .validateSpec('config/e2e.json') - .shouldNotExist('config/e2e.yml') - .validateSpec('config/production.json') - .shouldNotExist('config/production.yml') - .validateSpec('config/test.json') - .shouldNotExist('config/test.yml'); + fs + .cd('test-foo-bar/config') + .assertEqual('default.json', 'app/config/default.json') + .assertNotExists('default.yml') + .assertEqual('development.json', 'app/config/development.json') + .assertNotExists('development.yml') + .assertEqual('e2e.json', 'app/config/e2e.json') + .assertNotExists('e2e.yml') + .assertEqual('production.json', 'app/config/production.json') + .assertNotExists('production.yml') + .assertEqual('test.json', 'app/config/test.json') + .assertNotExists('test.yml'); }); it('should render the config templates (YAML option).', async () => { await createApp({ name: 'test-fooBar', yaml: true }); - testEnv - .shouldNotExist('config/default.json') - .validateSpec('config/default.yml') - .shouldNotExist('config/development.json') - .validateSpec('config/development.yml') - .shouldNotExist('config/e2e.json') - .validateSpec('config/e2e.yml') - .shouldNotExist('config/production.json') - .validateSpec('config/production.yml') - .shouldNotExist('config/test.json') - .validateSpec('config/test.yml'); + fs + .cd('test-foo-bar/config') + .assertNotExists('default.json') + .assertEqual('default.yml', 'app/config/default.yml') + .assertNotExists('development.json') + .assertEqual('development.yml', 'app/config/development.yml') + .assertNotExists('e2e.json') + .assertEqual('e2e.yml', 'app/config/e2e.yml') + .assertNotExists('production.json') + .assertEqual('production.yml', 'app/config/production.yml') + .assertNotExists('test.json') + .assertEqual('test.yml', 'app/config/test.yml'); }); it('should render the config templates (MongoDB option).', async () => { await createApp({ name: 'test-fooBar', mongodb: true }); - testEnv - .validateSpec('config/default.mongodb.json', 'config/default.json') - .shouldNotExist('config/default.yml') - .validateSpec('config/development.json') - .shouldNotExist('config/development.yml') - .validateSpec('config/e2e.mongodb.json', 'config/e2e.json') - .shouldNotExist('config/e2e.yml') - .validateSpec('config/production.json') - .shouldNotExist('config/production.yml') - .validateSpec('config/test.mongodb.json', 'config/test.json') - .shouldNotExist('config/test.yml'); + fs + .cd('test-foo-bar/config') + .assertEqual('default.json', 'app/config/default.mongodb.json') + .assertNotExists('default.yml') + .assertEqual('development.json', 'app/config/development.json') + .assertNotExists('development.yml') + .assertEqual('e2e.json', 'app/config/e2e.mongodb.json') + .assertNotExists('e2e.yml') + .assertEqual('production.json', 'app/config/production.json') + .assertNotExists('production.yml') + .assertEqual('test.json', 'app/config/test.mongodb.json') + .assertNotExists('test.yml'); }); it('should render the config templates (MongoDB & YAML options).', async () => { await createApp({ name: 'test-fooBar', mongodb: true, yaml: true }); - testEnv - .shouldNotExist('config/default.json') - .validateSpec('config/default.mongodb.yml', 'config/default.yml') - .shouldNotExist('config/development.json') - .validateSpec('config/development.yml') - .shouldNotExist('config/e2e.json') - .validateSpec('config/e2e.mongodb.yml', 'config/e2e.yml') - .shouldNotExist('config/production.json') - .validateSpec('config/production.yml') - .shouldNotExist('config/test.json') - .validateSpec('config/test.mongodb.yml', 'config/test.yml'); + fs + .cd('test-foo-bar/config') + .assertNotExists('default.json') + .assertEqual('default.yml', 'app/config/default.mongodb.yml') + .assertNotExists('development.json') + .assertEqual('development.yml', 'app/config/development.yml') + .assertNotExists('e2e.json') + .assertEqual('e2e.yml', 'app/config/e2e.mongodb.yml') + .assertNotExists('production.json') + .assertEqual('production.yml', 'app/config/production.yml') + .assertNotExists('test.json') + .assertEqual('test.yml', 'app/config/test.mongodb.yml'); }); it('should copy the public directory.', async () => { await createApp({ name: 'test-fooBar' }); - testEnv - .validateSpec('public/index.html') - .validateFileSpec('public/logo.png'); + fs + .cd('test-foo-bar/public') + .assertEqual('index.html', 'app/public/index.html') + .assertEqual('logo.png', 'app/public/logo.png'); }); it('shoud copy the src/e2e templates.', async () => { await createApp({ name: 'test-fooBar' }); - testEnv - .validateSpec('src/e2e/index.ts'); + fs + .cd('test-foo-bar/src/e2e') + .assertEqual('index.ts', 'app/src/e2e/index.ts'); }); it('shoud copy the src/e2e templates (MongoDB option).', async () => { await createApp({ name: 'test-fooBar', mongodb: true }); - testEnv - .validateSpec('src/e2e/index.mongodb.ts', 'src/e2e/index.ts'); + fs + .cd('test-foo-bar/src/e2e') + .assertEqual('index.ts', 'app/src/e2e/index.mongodb.ts'); }); it('shoud copy the src/scripts templates.', async () => { await createApp({ name: 'test-fooBar' }); - testEnv - .validateSpec('src/scripts/create-user.ts'); + fs + .cd('test-foo-bar/src/scripts') + .assertEqual('create-user.ts', 'app/src/scripts/create-user.ts'); }); it('shoud copy the src/scripts templates (MongoDB option).', async () => { await createApp({ name: 'test-fooBar', mongodb: true }); - testEnv - .validateSpec( - 'src/scripts/create-user.mongodb.ts', - 'src/scripts/create-user.ts' - ); + fs + .cd('test-foo-bar/src/scripts') + .assertEqual('create-user.ts', 'app/src/scripts/create-user.mongodb.ts'); }); it('should render the src/app/controllers templates.', async () => { await createApp({ name: 'test-fooBar' }); - testEnv - .validateSpec('src/app/controllers/index.ts') - .validateSpec('src/app/controllers/api.controller.spec.ts') - .validateSpec('src/app/controllers/api.controller.ts'); + fs + .cd('test-foo-bar/src/app/controllers') + .assertEqual('index.ts', 'app/src/app/controllers/index.ts') + .assertEqual('api.controller.spec.ts', 'app/src/app/controllers/api.controller.spec.ts') + .assertEqual('api.controller.ts', 'app/src/app/controllers/api.controller.ts'); }); it('should render the src/app/hooks templates.', async () => { await createApp({ name: 'test-fooBar' }); - testEnv - .validateSpec('src/app/hooks/index.ts'); + fs + .cd('test-foo-bar/src/app/hooks') + .assertEqual('index.ts', 'app/src/app/hooks/index.ts'); }); it('should render the src/app/entities templates.', async () => { await createApp({ name: 'test-fooBar' }); - testEnv - .validateSpec('src/app/entities/index.ts') - .validateSpec('src/app/entities/user.entity.ts') - .shouldNotExist('src/app/models'); + fs + .cd('test-foo-bar/src/app/entities') + .assertEqual('index.ts', 'app/src/app/entities/index.ts') + .assertEqual('user.entity.ts', 'app/src/app/entities/user.entity.ts') + .cd('..') + .assertNotExists('models'); }); it('should render the src/app/models templates (MongoDB option).', async () => { await createApp({ name: 'test-fooBar', mongodb: true }); - testEnv - .validateSpec('src/app/models/index.ts') - .validateSpec('src/app/models/user.model.ts') - .shouldNotExist('src/app/entities'); + fs + .cd('test-foo-bar/src/app/models') + .assertEqual('index.ts', 'app/src/app/models/index.ts') + .assertEqual('user.model.ts', 'app/src/app/models/user.model.ts') + .cd('..') + .assertNotExists('entities'); }); it('should render the src/app/services templates.', async () => { await createApp({ name: 'test-fooBar' }); - testEnv - .validateSpec('src/app/services/index.ts'); + fs + .cd('test-foo-bar/src/app/services') + .assertEqual('index.ts', 'app/src/app/services/index.ts'); }); it('should render the src/app templates.', async () => { await createApp({ name: 'test-fooBar' }); - testEnv - .validateSpec('src/app/app.controller.ts'); + fs + .cd('test-foo-bar/src/app') + .assertEqual('app.controller.ts', 'app/src/app/app.controller.ts'); }); it('should render the src templates.', async () => { await createApp({ name: 'test-fooBar' }); - testEnv - .validateSpec('src/e2e.ts') - .validateSpec('src/index.ts') - .validateSpec('src/test.ts'); + fs + .cd('test-foo-bar/src') + .assertEqual('e2e.ts', 'app/src/e2e.ts') + .assertEqual('index.ts', 'app/src/index.ts') + .assertEqual('test.ts', 'app/src/test.ts'); }); it('should render the src templates (MongoDB option).', async () => { await createApp({ name: 'test-fooBar', mongodb: true }); - testEnv - .validateSpec('src/e2e.ts') - .validateSpec('src/index.mongodb.ts', 'src/index.ts') - .validateSpec('src/test.ts'); + fs + .cd('test-foo-bar/src') + .assertEqual('e2e.ts', 'app/src/e2e.ts') + .assertEqual('index.ts', 'app/src/index.mongodb.ts') + .assertEqual('test.ts', 'app/src/test.ts'); }); it('should render the root templates.', async () => { await createApp({ name: 'test-fooBar' }); - testEnv - .validateSpec('gitignore', '.gitignore') - .validateSpec('ormconfig.js') - .validateSpec('package.json') - .validateSpec('tsconfig.json') - .validateSpec('tsconfig.app.json') - .validateSpec('tsconfig.e2e.json') - .validateSpec('tsconfig.json') - .validateSpec('tsconfig.migrations.json') - .validateSpec('tsconfig.scripts.json') - .validateSpec('tsconfig.test.json') - .validateSpec('.eslintrc.js'); + fs + .cd('test-foo-bar') + .assertEqual('.gitignore', 'app/gitignore') + .assertEqual('ormconfig.js', 'app/ormconfig.js') + .assertEqual('package.json', 'app/package.json') + .assertEqual('tsconfig.json', 'app/tsconfig.json') + .assertEqual('tsconfig.app.json', 'app/tsconfig.app.json') + .assertEqual('tsconfig.e2e.json', 'app/tsconfig.e2e.json') + .assertEqual('tsconfig.json', 'app/tsconfig.json') + .assertEqual('tsconfig.migrations.json', 'app/tsconfig.migrations.json') + .assertEqual('tsconfig.scripts.json', 'app/tsconfig.scripts.json') + .assertEqual('tsconfig.test.json', 'app/tsconfig.test.json') + .assertEqual('.eslintrc.js', 'app/.eslintrc.js'); }); it('should render the root templates (YAML option).', async () => { await createApp({ name: 'test-fooBar', yaml: true }); - testEnv - .validateSpec('gitignore', '.gitignore') - .validateSpec('ormconfig.js') - .validateSpec('package.yaml.json', 'package.json') - .validateSpec('tsconfig.json') - .validateSpec('tsconfig.app.json') - .validateSpec('tsconfig.e2e.json') - .validateSpec('tsconfig.json') - .validateSpec('tsconfig.migrations.json') - .validateSpec('tsconfig.scripts.json') - .validateSpec('tsconfig.test.json') - .validateSpec('.eslintrc.js'); + fs + .cd('test-foo-bar') + .assertEqual('.gitignore', 'app/gitignore') + .assertEqual('ormconfig.js', 'app/ormconfig.js') + .assertEqual('package.json', 'app/package.yaml.json') + .assertEqual('tsconfig.json', 'app/tsconfig.json') + .assertEqual('tsconfig.app.json', 'app/tsconfig.app.json') + .assertEqual('tsconfig.e2e.json', 'app/tsconfig.e2e.json') + .assertEqual('tsconfig.json', 'app/tsconfig.json') + .assertEqual('tsconfig.migrations.json', 'app/tsconfig.migrations.json') + .assertEqual('tsconfig.scripts.json', 'app/tsconfig.scripts.json') + .assertEqual('tsconfig.test.json', 'app/tsconfig.test.json') + .assertEqual('.eslintrc.js', 'app/.eslintrc.js'); }); it('should render the root templates (MongoDB option).', async () => { await createApp({ name: 'test-fooBar', mongodb: true }); - testEnv - .validateSpec('gitignore', '.gitignore') - .shouldNotExist('ormconfig.js') - .validateSpec('package.mongodb.json', 'package.json') - .validateSpec('tsconfig.json') - .validateSpec('tsconfig.app.json') - .validateSpec('tsconfig.e2e.json') - .validateSpec('tsconfig.json') - .shouldNotExist('tsconfig.migrations.json') - .validateSpec('tsconfig.scripts.json') - .validateSpec('tsconfig.test.json') - .validateSpec('.eslintrc.js'); + fs + .cd('test-foo-bar') + .assertEqual('.gitignore', 'app/gitignore') + .assertNotExists('ormconfig.js') + .assertEqual('package.json', 'app/package.mongodb.json') + .assertEqual('tsconfig.json', 'app/tsconfig.json') + .assertEqual('tsconfig.app.json', 'app/tsconfig.app.json') + .assertEqual('tsconfig.e2e.json', 'app/tsconfig.e2e.json') + .assertEqual('tsconfig.json', 'app/tsconfig.json') + .assertNotExists('tsconfig.migrations.json') + .assertEqual('tsconfig.scripts.json', 'app/tsconfig.scripts.json') + .assertEqual('tsconfig.test.json', 'app/tsconfig.test.json') + .assertEqual('.eslintrc.js', 'app/.eslintrc.js'); }); it('should render the root templates (MongoDB & YAML options).', async () => { await createApp({ name: 'test-fooBar', mongodb: true, yaml: true }); - testEnv - .validateSpec('gitignore', '.gitignore') - .shouldNotExist('ormconfig.js') - .validateSpec('package.mongodb.yaml.json', 'package.json') - .validateSpec('tsconfig.json') - .validateSpec('tsconfig.app.json') - .validateSpec('tsconfig.e2e.json') - .validateSpec('tsconfig.json') - .shouldNotExist('tsconfig.migrations.json') - .validateSpec('tsconfig.scripts.json') - .validateSpec('tsconfig.test.json') - .validateSpec('.eslintrc.js'); + fs + .cd('test-foo-bar') + .assertEqual('.gitignore', 'app/gitignore') + .assertNotExists('ormconfig.js') + .assertEqual('package.json', 'app/package.mongodb.yaml.json') + .assertEqual('tsconfig.json', 'app/tsconfig.json') + .assertEqual('tsconfig.app.json', 'app/tsconfig.app.json') + .assertEqual('tsconfig.e2e.json', 'app/tsconfig.e2e.json') + .assertEqual('tsconfig.json', 'app/tsconfig.json') + .assertNotExists('tsconfig.migrations.json') + .assertEqual('tsconfig.scripts.json', 'app/tsconfig.scripts.json') + .assertEqual('tsconfig.test.json', 'app/tsconfig.test.json') + .assertEqual('.eslintrc.js', 'app/.eslintrc.js'); }); }); diff --git a/packages/cli/src/generate/generators/app/create-app.ts b/packages/cli/src/generate/generators/app/create-app.ts index a66e354211..1aca1121e8 100644 --- a/packages/cli/src/generate/generators/app/create-app.ts +++ b/packages/cli/src/generate/generators/app/create-app.ts @@ -1,13 +1,12 @@ // std import { execSync, spawn, SpawnOptions } from 'child_process'; -import { existsSync, mkdirSync } from 'fs'; // 3p import { cyan, red } from 'colors/safe'; // FoalTS +import { FileSystem } from '../../file-system'; import { - Generator, getNames, initGitRepo, } from '../../utils'; @@ -22,7 +21,7 @@ function isYarnInstalled() { } function log(msg: string) { - if (process.env.NODE_ENV !== 'test') { + if (process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST !== 'true') { console.log(msg); } } @@ -32,7 +31,7 @@ export async function createApp({ name, autoInstall, initRepo, mongodb = false, yaml?: boolean }) { const names = getNames(name); - if (process.env.NODE_ENV !== 'test') { + if (process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST !== 'true') { console.log(cyan( `==================================================================== @@ -51,95 +50,123 @@ export async function createApp({ name, autoInstall, initRepo, mongodb = false, const locals = names; - if (existsSync(names.kebabName)) { - console.log( - red(`\n The target directory "${names.kebabName}" already exists. Please remove it before proceeding.`) - ); + const fs = new FileSystem(); + + if (fs.exists(names.kebabName)) { + if (process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST !== 'true') { + console.log( + red(`\n The target directory "${names.kebabName}" already exists. Please remove it before proceeding.`) + ); + } return; } - mkdirSync(names.kebabName); + + fs + .ensureDir(names.kebabName) + .cd(names.kebabName); log(' 📂 Creating files...'); - const generator = new Generator('app', names.kebabName, { noLogs: true }); - - generator - .copyFileFromTemplates('gitignore', '.gitignore') - .copyFileFromTemplatesOnlyIf(!mongodb, 'ormconfig.js') - .renderTemplateOnlyIf(!mongodb && !yaml, 'package.json', locals) - .renderTemplateOnlyIf(!mongodb && yaml, 'package.yaml.json', locals, 'package.json') - .renderTemplateOnlyIf(mongodb && !yaml, 'package.mongodb.json', locals, 'package.json') - .renderTemplateOnlyIf(mongodb && yaml, 'package.mongodb.yaml.json', locals, 'package.json') - .copyFileFromTemplates('tsconfig.app.json') - .copyFileFromTemplates('tsconfig.e2e.json') - .copyFileFromTemplates('tsconfig.json') - .copyFileFromTemplatesOnlyIf(!mongodb, 'tsconfig.migrations.json') - .copyFileFromTemplates('tsconfig.scripts.json') - .copyFileFromTemplates('tsconfig.test.json') - .copyFileFromTemplates('.eslintrc.js') + + fs + .hideLogs() + .copy('app/gitignore', '.gitignore') + .copyOnlyIf(!mongodb, 'app/ormconfig.js', 'ormconfig.js') + .renderOnlyIf(!mongodb && !yaml, 'app/package.json', 'package.json', locals) + .renderOnlyIf(!mongodb && yaml, 'app/package.yaml.json', 'package.json', locals) + .renderOnlyIf(mongodb && !yaml, 'app/package.mongodb.json', 'package.json', locals) + .renderOnlyIf(mongodb && yaml, 'app/package.mongodb.yaml.json', 'package.json', locals) + .copy('app/tsconfig.app.json', 'tsconfig.app.json') + .copy('app/tsconfig.e2e.json', 'tsconfig.e2e.json') + .copy('app/tsconfig.json', 'tsconfig.json') + .copyOnlyIf(!mongodb, 'app/tsconfig.migrations.json', 'tsconfig.migrations.json') + .copy('app/tsconfig.scripts.json', 'tsconfig.scripts.json') + .copy('app/tsconfig.test.json', 'tsconfig.test.json') + .copy('app/.eslintrc.js', '.eslintrc.js') // Config - .mkdirIfDoesNotExist('config') - .renderTemplateOnlyIf(!mongodb && !yaml, 'config/default.json', locals) - .renderTemplateOnlyIf(!mongodb && yaml, 'config/default.yml', locals) - .renderTemplateOnlyIf(mongodb && !yaml, 'config/default.mongodb.json', locals, 'config/default.json') - .renderTemplateOnlyIf(mongodb && yaml, 'config/default.mongodb.yml', locals, 'config/default.yml') - .renderTemplateOnlyIf(!yaml, 'config/development.json', locals) - .renderTemplateOnlyIf(yaml, 'config/development.yml', locals) - .renderTemplateOnlyIf(!mongodb && !yaml, 'config/e2e.json', locals) - .renderTemplateOnlyIf(!mongodb && yaml, 'config/e2e.yml', locals) - .renderTemplateOnlyIf(mongodb && !yaml, 'config/e2e.mongodb.json', locals, 'config/e2e.json') - .renderTemplateOnlyIf(mongodb && yaml, 'config/e2e.mongodb.yml', locals, 'config/e2e.yml') - .renderTemplateOnlyIf(!yaml, 'config/production.json', locals) - .renderTemplateOnlyIf(yaml, 'config/production.yml', locals) - .renderTemplateOnlyIf(!mongodb && !yaml, 'config/test.json', locals) - .renderTemplateOnlyIf(!mongodb && yaml, 'config/test.yml', locals) - .renderTemplateOnlyIf(mongodb && !yaml, 'config/test.mongodb.json', locals, 'config/test.json') - .renderTemplateOnlyIf(mongodb && yaml, 'config/test.mongodb.yml', locals, 'config/test.yml') + .ensureDir('config') + .cd('config') + .renderOnlyIf(!mongodb && !yaml, 'app/config/default.json', 'default.json', locals) + .renderOnlyIf(!mongodb && yaml, 'app/config/default.yml', 'default.yml', locals) + .renderOnlyIf(mongodb && !yaml, 'app/config/default.mongodb.json', 'default.json', locals) + .renderOnlyIf(mongodb && yaml, 'app/config/default.mongodb.yml', 'default.yml', locals) + .renderOnlyIf(!yaml, 'app/config/development.json', 'development.json', locals) + .renderOnlyIf(yaml, 'app/config/development.yml', 'development.yml', locals) + .renderOnlyIf(!mongodb && !yaml, 'app/config/e2e.json', 'e2e.json', locals) + .renderOnlyIf(!mongodb && yaml, 'app/config/e2e.yml', 'e2e.yml', locals) + .renderOnlyIf(mongodb && !yaml, 'app/config/e2e.mongodb.json', 'e2e.json', locals) + .renderOnlyIf(mongodb && yaml, 'app/config/e2e.mongodb.yml', 'e2e.yml', locals) + .renderOnlyIf(!yaml, 'app/config/production.json', 'production.json', locals) + .renderOnlyIf(yaml, 'app/config/production.yml', 'production.yml', locals) + .renderOnlyIf(!mongodb && !yaml, 'app/config/test.json', 'test.json', locals) + .renderOnlyIf(!mongodb && yaml, 'app/config/test.yml', 'test.yml', locals) + .renderOnlyIf(mongodb && !yaml, 'app/config/test.mongodb.json', 'test.json', locals) + .renderOnlyIf(mongodb && yaml, 'app/config/test.mongodb.yml', 'test.yml', locals) + .cd('..') // Public - .mkdirIfDoesNotExist('public') - .copyFileFromTemplates('public/index.html') - .copyFileFromTemplates('public/logo.png') + .ensureDir('public') + .cd('public') + .copy('app/public/index.html', 'index.html') + .copy('app/public/logo.png', 'logo.png') + .cd('..') // Src - .mkdirIfDoesNotExist('src') - .copyFileFromTemplates('src/e2e.ts') - .copyFileFromTemplatesOnlyIf(mongodb, 'src/index.mongodb.ts', 'src/index.ts') - .copyFileFromTemplatesOnlyIf(!mongodb, 'src/index.ts') - .copyFileFromTemplates('src/test.ts') + .ensureDir('src') + .cd('src') + .copy('app/src/e2e.ts', 'e2e.ts') + .copyOnlyIf(mongodb, 'app/src/index.mongodb.ts', 'index.ts') + .copyOnlyIf(!mongodb, 'app/src/index.ts', 'index.ts') + .copy('app/src/test.ts', 'test.ts') // App - .mkdirIfDoesNotExist('src/app') - .copyFileFromTemplates('src/app/app.controller.ts') + .ensureDir('app') + .cd('app') + .copy('app/src/app/app.controller.ts', 'app.controller.ts') // Controllers - .mkdirIfDoesNotExist('src/app/controllers') - .copyFileFromTemplates('src/app/controllers/index.ts') - .copyFileFromTemplates('src/app/controllers/api.controller.ts') - .copyFileFromTemplates('src/app/controllers/api.controller.spec.ts') + .ensureDir('controllers') + .cd('controllers') + .copy('app/src/app/controllers/index.ts', 'index.ts') + .copy('app/src/app/controllers/api.controller.ts', 'api.controller.ts') + .copy('app/src/app/controllers/api.controller.spec.ts', 'api.controller.spec.ts') + .cd('..') // Entities - .mkdirIfDoesNotExistOnlyIf(!mongodb, 'src/app/entities') - .copyFileFromTemplatesOnlyIf(!mongodb, 'src/app/entities/index.ts') - .copyFileFromTemplatesOnlyIf(!mongodb, 'src/app/entities/user.entity.ts') + .ensureDirOnlyIf(!mongodb, 'entities') + .cd('entities') + .copyOnlyIf(!mongodb, 'app/src/app/entities/index.ts', 'index.ts') + .copyOnlyIf(!mongodb, 'app/src/app/entities/user.entity.ts', 'user.entity.ts') + .cd('..') // Hooks - .mkdirIfDoesNotExist('src/app/hooks') - .copyFileFromTemplates('src/app/hooks/index.ts') + .ensureDir('hooks') + .cd('hooks') + .copy('app/src/app/hooks/index.ts', 'index.ts') + .cd('..') // Models - .mkdirIfDoesNotExistOnlyIf(mongodb, 'src/app/models') - .copyFileFromTemplatesOnlyIf(mongodb, 'src/app/models/index.ts') - .copyFileFromTemplatesOnlyIf(mongodb, 'src/app/models/user.model.ts') + .ensureDirOnlyIf(mongodb, 'models') + .cd('models') + .copyOnlyIf(mongodb, 'app/src/app/models/index.ts', 'index.ts') + .copyOnlyIf(mongodb, 'app/src/app/models/user.model.ts', 'user.model.ts') + .cd('..') // Services - .mkdirIfDoesNotExist('src/app/services') - .copyFileFromTemplates('src/app/services/index.ts') + .ensureDir('services') + .cd('services') + .copy('app/src/app/services/index.ts', 'index.ts') + .cd('..') + .cd('..') // E2E - .mkdirIfDoesNotExist('src/e2e') - .copyFileFromTemplatesOnlyIf(!mongodb, 'src/e2e/index.ts') - .copyFileFromTemplatesOnlyIf(mongodb, 'src/e2e/index.mongodb.ts', 'src/e2e/index.ts') + .ensureDir('e2e') + .cd('e2e') + .copyOnlyIf(!mongodb, 'app/src/e2e/index.ts', 'index.ts') + .copyOnlyIf(mongodb, 'app/src/e2e/index.mongodb.ts', 'index.ts') + .cd('..') // Scripts - .mkdirIfDoesNotExist('src/scripts') - .copyFileFromTemplatesOnlyIf(!mongodb, 'src/scripts/create-user.ts') - .copyFileFromTemplatesOnlyIf(mongodb, 'src/scripts/create-user.mongodb.ts', 'src/scripts/create-user.ts'); + .ensureDir('scripts') + .cd('scripts') + .copyOnlyIf(!mongodb, 'app/src/scripts/create-user.ts', 'create-user.ts') + .copyOnlyIf(mongodb, 'app/src/scripts/create-user.mongodb.ts', 'create-user.ts'); if (autoInstall) { log(''); log(' 📦 Installing the dependencies...'); const packageManager = isYarnInstalled() ? 'yarn' : 'npm'; - const args = [ 'install' ]; + // TODO: in version 2, remove the hack "--ignore-engines" + const args = [ 'install', '--ignore-engines' ]; const options: SpawnOptions = { cwd: names.kebabName, shell: true, diff --git a/packages/cli/src/generate/generators/controller/create-controller.spec.ts b/packages/cli/src/generate/generators/controller/create-controller.spec.ts index 220e7fc3b2..ff045e6960 100644 --- a/packages/cli/src/generate/generators/controller/create-controller.spec.ts +++ b/packages/cli/src/generate/generators/controller/create-controller.spec.ts @@ -1,70 +1,48 @@ // FoalTS -import { - rmDirAndFilesIfExist, - rmfileIfExists, - TestEnvironment -} from '../../utils'; +import { FileSystem } from '../../file-system'; import { createController } from './create-controller'; describe('createController', () => { - afterEach(() => { - rmDirAndFilesIfExist('src/app'); - // We cannot remove src/ since the generator code lives within. This is bad testing - // approach. - rmDirAndFilesIfExist('controllers'); - rmfileIfExists('test-foo-bar.controller.ts'); - rmfileIfExists('test-foo-bar.controller.spec.ts'); - rmfileIfExists('index.ts'); - }); + const fs = new FileSystem(); + + beforeEach(() => fs.setUp()); + + afterEach(() => fs.tearDown()); function test(root: string) { describe(`when the directory ${root}/ exists`, () => { - const testEnv = new TestEnvironment('controller', root); - beforeEach(() => { - testEnv.mkRootDirIfDoesNotExist(); - testEnv.copyFileFromMocks('index.ts'); + fs + .ensureDir(root) + .cd(root) + .copyFixture('controller/index.ts', 'index.ts'); }); it('should render the empty templates in the proper directory.', () => { - createController({ name: 'test-fooBar', type: 'Empty', register: false }); + createController({ name: 'test-fooBar', register: false }); - testEnv - .validateSpec('test-foo-bar.controller.empty.ts', 'test-foo-bar.controller.ts') - .validateSpec('test-foo-bar.controller.spec.empty.ts', 'test-foo-bar.controller.spec.ts') - .validateSpec('index.ts', 'index.ts'); + fs + .assertEqual('test-foo-bar.controller.ts', 'controller/test-foo-bar.controller.empty.ts') + .assertEqual('test-foo-bar.controller.spec.ts', 'controller/test-foo-bar.controller.spec.empty.ts') + .assertEqual('index.ts', 'controller/index.ts'); }); - it('should render the REST templates in the proper directory.', () => { - createController({ name: 'test-fooBar', type: 'REST', register: false }); + it('should create the directory if it does not exist.', () => { + createController({ name: 'barfoo/hello/test-fooBar', register: false }); - testEnv - .validateSpec('test-foo-bar.controller.rest.ts', 'test-foo-bar.controller.ts') - .validateSpec('index.ts', 'index.ts'); + fs + .assertExists('barfoo/hello/test-foo-bar.controller.ts'); }); - it('should render the GraphQL templates in the proper directory.', () => { - createController({ name: 'test-fooBar', type: 'GraphQL', register: false }); + it('should create index.ts if it does not exist.', () => { + fs.rmfile('index.ts'); - testEnv - .validateSpec('test-foo-bar.controller.graphql.ts', 'test-foo-bar.controller.ts') - .validateSpec('index.ts', 'index.ts'); - }); + createController({ name: 'test-fooBar', register: false }); - it('should render the Login templates in the proper directory.', () => { - createController({ name: 'test-fooBar', type: 'Login', register: false }); - - testEnv - .validateSpec('test-foo-bar.controller.login.ts', 'test-foo-bar.controller.ts') - .validateSpec('index.ts', 'index.ts'); - }); - - it('should not throw an error if index.ts does not exist.', () => { - testEnv.rmfileIfExists('index.ts'); - createController({ name: 'test-fooBar', type: 'Empty', register: false }); + fs.assertExists('index.ts'); }); }); @@ -77,86 +55,33 @@ describe('createController', () => { describe('when the directory src/app/controllers exists and if register is true', () => { - const testEnv = new TestEnvironment('controller', 'src/app/controllers'); - beforeEach(() => { - testEnv.mkRootDirIfDoesNotExist(); - testEnv.copyFileFromMocks('index.ts'); - }); - - // TODO: refactor these tests and their mock and spec files. - - it('should add all the imports if none exists.', () => { - testEnv.copyFileFromMocks('app.controller.no-import.ts', '../app.controller.ts'); - - createController({ name: 'test-fooBar', type: 'Empty', register: true }); - - testEnv - .validateSpec('app.controller.no-import.ts', '../app.controller.ts'); - }); - - it('should update the "subControllers" import in src/app/app.controller.ts if it exists.', () => { - testEnv.copyFileFromMocks('app.controller.controller-import.ts', '../app.controller.ts'); - - createController({ name: 'test-fooBar', type: 'Empty', register: true }); - - testEnv - .validateSpec('app.controller.controller-import.ts', '../app.controller.ts'); - }); - - it('should add a "subControllers" import in src/app/app.controller.ts if none already exists.', () => { - testEnv.copyFileFromMocks('app.controller.no-controller-import.ts', '../app.controller.ts'); - - createController({ name: 'test-fooBar', type: 'Empty', register: true }); - - testEnv - .validateSpec('app.controller.no-controller-import.ts', '../app.controller.ts'); - }); - - it('should update the "@foal/core" import in src/app/app.controller.ts if it exists.', () => { - testEnv.copyFileFromMocks('app.controller.core-import.ts', '../app.controller.ts'); - - createController({ name: 'test-fooBar', type: 'Empty', register: true }); - - testEnv - .validateSpec('app.controller.core-import.ts', '../app.controller.ts'); - }); - - it('should update the "subControllers = []" property in src/app/app.controller.ts if it exists.', () => { - testEnv.copyFileFromMocks('app.controller.empty-property.ts', '../app.controller.ts'); - - createController({ name: 'test-fooBar', type: 'Empty', register: true }); - - testEnv - .validateSpec('app.controller.empty-property.ts', '../app.controller.ts'); - }); - - it('should update the "subControllers = [ \\n \\n ]" property in src/app/app.controller.ts if it exists.', () => { - testEnv.copyFileFromMocks('app.controller.empty-spaced-property.ts', '../app.controller.ts'); - - createController({ name: 'test-fooBar', type: 'Empty', register: true }); - - testEnv - .validateSpec('app.controller.empty-spaced-property.ts', '../app.controller.ts'); + fs + .ensureDir('src/app/controllers') + .cd('src/app/controllers') + .copyFixture('controller/index.ts', 'index.ts') + .cd('..'); }); - it('should update the "subControllers = [ \\n (.*) \\n ]" property in' - + ' src/app/app.controller.ts if it exists.', () => { - testEnv.copyFileFromMocks('app.controller.no-empty-property.ts', '../app.controller.ts'); + it('should register the controller in app.controller.ts.', () => { + fs + .copyFixture('controller/app.controller.ts', 'app.controller.ts'); - createController({ name: 'test-fooBar', type: 'Empty', register: true }); + createController({ name: 'test-fooBar', register: true }); - testEnv - .validateSpec('app.controller.no-empty-property.ts', '../app.controller.ts'); + fs + .assertEqual('app.controller.ts', 'controller/app.controller.ts'); }); - it('should update the "subControllers" property with a special URL if the controller is a REST controller.', () => { - testEnv.copyFileFromMocks('app.controller.rest.ts', '../app.controller.ts'); + it('should register the controller in a parent controller (subdir).', () => { + fs + .ensureDir('controllers/hello') + .copyFixture('controller/api.controller.ts', 'controllers/hello/api.controller.ts'); - createController({ name: 'test-fooBar', type: 'REST', register: true }); + createController({ name: 'hello/api/test-fooBar', register: true }); - testEnv - .validateSpec('app.controller.rest.ts', '../app.controller.ts'); + fs + .assertEqual('controllers/hello/api.controller.ts', 'controller/api.controller.ts'); }); }); diff --git a/packages/cli/src/generate/generators/controller/create-controller.ts b/packages/cli/src/generate/generators/controller/create-controller.ts index 3d8cc3ba1c..d1bb7f1762 100644 --- a/packages/cli/src/generate/generators/controller/create-controller.ts +++ b/packages/cli/src/generate/generators/controller/create-controller.ts @@ -1,43 +1,55 @@ -// std -import { existsSync } from 'fs'; - // FoalTS -import { Generator, getNames } from '../../utils'; -import { registerController } from './register-controller'; - -export type ControllerType = 'Empty'|'REST'|'GraphQL'|'Login'; +import { basename, dirname } from 'path'; +import { FileSystem } from '../../file-system'; +import { getNames } from '../../utils'; -export function createController({ name, type, register }: { name: string, type: ControllerType, register: boolean }) { - const names = getNames(name); - - const fileName = `${names.kebabName}.controller.ts`; - const specFileName = `${names.kebabName}.controller.spec.ts`; +export function createController({ name, register }: { name: string, register: boolean }) { + const fs = new FileSystem(); let root = ''; - - if (existsSync('src/app/controllers')) { + if (fs.exists('src/app/controllers')) { root = 'src/app/controllers'; - } else if (existsSync('controllers')) { + } else if (fs.exists('controllers')) { root = 'controllers'; } - const generator = new Generator('controller', root) - .renderTemplate(`controller.${type.toLowerCase()}.ts`, names, fileName) - .updateFile('index.ts', content => { - content += `export { ${names.upperFirstCamelName}Controller } from './${names.kebabName}.controller';\n`; - return content; - }, { allowFailure: true }); + const names = getNames(basename(name)); + const subdir = dirname(name); + const parentControllerPath = `${subdir === '.' ? 'app' : basename(subdir)}.controller.ts`; - if (register) { - const path = `/${names.kebabName}${type === 'REST' ? 's' : ''}`; - generator - .updateFile('../app.controller.ts', content => { - return registerController(content, `${names.upperFirstCamelName}Controller`, path); - }, { allowFailure: true }); - } + const fileName = `${names.kebabName}.controller.ts`; + const specFileName = `${names.kebabName}.controller.spec.ts`; - if (type === 'Empty') { - generator - .renderTemplate('controller.spec.empty.ts', names, specFileName); + const className = `${names.upperFirstCamelName}Controller`; + + fs + .cd(root) + .ensureDir(subdir) + .cd(subdir) + .render('controller/controller.empty.ts', fileName, names) + .render('controller/controller.spec.empty.ts', specFileName, names) + .ensureFile('index.ts') + .addNamedExportIn('index.ts', className, `./${names.kebabName}.controller`); + + if (register) { + fs + .cd('..') + .addOrExtendNamedImportIn( + parentControllerPath, + 'controller', + '@foal/core', + { logs: false } + ) + .addOrExtendNamedImportIn( + parentControllerPath, + className, + `./${subdir === '.' ? 'controllers' : basename(subdir)}`, + { logs: false } + ) + .addOrExtendClassArrayPropertyIn( + parentControllerPath, + 'subControllers', + `controller('/${names.kebabName}', ${className})` + ); } } diff --git a/packages/cli/src/generate/generators/controller/index.ts b/packages/cli/src/generate/generators/controller/index.ts index 783d132ab3..9ee1a7bec9 100644 --- a/packages/cli/src/generate/generators/controller/index.ts +++ b/packages/cli/src/generate/generators/controller/index.ts @@ -1 +1 @@ -export { ControllerType, createController } from './create-controller'; +export { createController } from './create-controller'; diff --git a/packages/cli/src/generate/generators/controller/register-controller.ts b/packages/cli/src/generate/generators/controller/register-controller.ts deleted file mode 100644 index 49bb1d9a6d..0000000000 --- a/packages/cli/src/generate/generators/controller/register-controller.ts +++ /dev/null @@ -1,61 +0,0 @@ -class ImportNotFound extends Error {} - -function createNamedImport(specifiers: string[], path: string): string { - return `import { ${specifiers.join(', ')} } from '${path}';`; -} - -function addImport(fileContent: string, importDeclaration: string): string { - const regex = new RegExp('import (.*) from (.*);', 'g'); - let lastOccurence: RegExpExecArray|undefined; - let lastExec: RegExpExecArray|null; - while ((lastExec = regex.exec(fileContent)) !== null) { - lastOccurence = lastExec; - } - if (lastOccurence === undefined) { - return `${importDeclaration}\n\n${fileContent}`; - } - const endPos = lastOccurence.index + lastOccurence[0].length; - return fileContent.substr(0, endPos) + '\n' + importDeclaration + fileContent.substr(endPos); -} - -function extendImport(fileContent: string, path: string, specifier: string): string { - const pathRegex = path.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - const importRegex = new RegExp(`import {(.*)} from \'${pathRegex}\';`); - - if (!importRegex.test(fileContent)) { - throw new ImportNotFound(); - } - - return fileContent - .replace(importRegex, (str, content: string) => { - const importSpecifiers = content.split(',').map(imp => imp.trim()); - if (importSpecifiers.includes(specifier)) { - return str; - } - importSpecifiers.push(specifier); - importSpecifiers.sort((a, b) => a.localeCompare(b)); - return createNamedImport(importSpecifiers, path); - }); -} - -function addOrExtendImport(fileContent: string, specifier: string, path: string): string { - try { - return extendImport(fileContent, path, specifier); - } catch (err) { - const namedImport = createNamedImport([ specifier ], path); - return addImport(fileContent, namedImport); - } -} - -export function registerController(fileContent: string, controllerName: string, path: string): string { - fileContent = addOrExtendImport(fileContent, controllerName, './controllers'); - fileContent = addOrExtendImport(fileContent, 'controller', '@foal/core'); - return fileContent - .replace(/( *)subControllers = \[((.|\n)*)\];/, (str, spaces, content: string) => { - const regex = /controller\((.*)\)/g; - const controllerCalls = content.match(regex) || []; - controllerCalls.push(`controller('${path}', ${controllerName})`); - const formattedCalls = controllerCalls.join(`,\n${spaces} `); - return `${spaces}subControllers = [\n${spaces} ${formattedCalls}\n${spaces}];`; - }); -} diff --git a/packages/cli/src/generate/generators/entity/create-entity.spec.ts b/packages/cli/src/generate/generators/entity/create-entity.spec.ts index 996558755a..32816345ea 100644 --- a/packages/cli/src/generate/generators/entity/create-entity.spec.ts +++ b/packages/cli/src/generate/generators/entity/create-entity.spec.ts @@ -1,44 +1,40 @@ // FoalTS -import { - rmDirAndFilesIfExist, - rmfileIfExists, - TestEnvironment, -} from '../../utils'; +import { FileSystem } from '../../file-system'; import { createEntity } from './create-entity'; describe('createEntity', () => { - afterEach(() => { - rmDirAndFilesIfExist('src/app'); - // We cannot remove src/ since the generator code lives within. This is bad testing - // approach. - rmDirAndFilesIfExist('entities'); - rmfileIfExists('test-foo-bar.entity.ts'); - rmfileIfExists('index.ts'); - }); + const fs = new FileSystem(); + + beforeEach(() => fs.setUp()); + + afterEach(() => fs.tearDown()); function test(root: string) { describe(`when the directory ${root}/ exists`, () => { - const testEnv = new TestEnvironment('entity', root); - beforeEach(() => { - testEnv.mkRootDirIfDoesNotExist(); - testEnv.copyFileFromMocks('index.ts'); + fs + .ensureDir(root) + .cd(root) + .copyFixture('entity/index.ts', 'index.ts'); }); it('should render the templates in the proper directory.', () => { createEntity({ name: 'test-fooBar' }); - testEnv - .validateSpec('test-foo-bar.entity.ts') - .validateSpec('index.ts', 'index.ts'); + fs + .assertEqual('test-foo-bar.entity.ts', 'entity/test-foo-bar.entity.ts') + .assertEqual('index.ts', 'entity/index.ts'); }); - it('should not throw an error if index.ts does not exist.', () => { - testEnv.rmfileIfExists('index.ts'); + it('create index.ts if it does not exist.', () => { + fs.rmfile('index.ts'); + createEntity({ name: 'test-fooBar' }); + + fs.assertExists('index.ts'); }); }); diff --git a/packages/cli/src/generate/generators/entity/create-entity.ts b/packages/cli/src/generate/generators/entity/create-entity.ts index 19f81b9548..707f6024d5 100644 --- a/packages/cli/src/generate/generators/entity/create-entity.ts +++ b/packages/cli/src/generate/generators/entity/create-entity.ts @@ -1,24 +1,22 @@ -// std -import { existsSync } from 'fs'; - // FoalTS -import { Generator, getNames } from '../../utils'; +import { FileSystem } from '../../file-system'; +import { getNames } from '../../utils'; export function createEntity({ name }: { name: string }) { - const names = getNames(name); + const fs = new FileSystem(); let root = ''; - - if (existsSync('src/app/entities')) { + if (fs.exists('src/app/entities')) { root = 'src/app/entities'; - } else if (existsSync('entities')) { + } else if (fs.exists('entities')) { root = 'entities'; } - new Generator('entity', root) - .renderTemplate('entity.ts', names, `${names.kebabName}.entity.ts`) - .updateFile('index.ts', content => { - content += `export { ${names.upperFirstCamelName} } from './${names.kebabName}.entity';\n`; - return content; - }, { allowFailure: true }); + const names = getNames(name); + + fs + .cd(root) + .render('entity/entity.ts', `${names.kebabName}.entity.ts`, names) + .ensureFile('index.ts') + .addNamedExportIn('index.ts', names.upperFirstCamelName, `./${names.kebabName}.entity`); } diff --git a/packages/cli/src/generate/generators/hook/create-hook.spec.ts b/packages/cli/src/generate/generators/hook/create-hook.spec.ts index c436d01daa..a80071db5a 100644 --- a/packages/cli/src/generate/generators/hook/create-hook.spec.ts +++ b/packages/cli/src/generate/generators/hook/create-hook.spec.ts @@ -1,44 +1,40 @@ // FoalTS -import { - rmDirAndFilesIfExist, - rmfileIfExists, - TestEnvironment, -} from '../../utils'; +import { FileSystem } from '../../file-system'; import { createHook } from './create-hook'; describe('createHook', () => { - afterEach(() => { - rmDirAndFilesIfExist('src/app'); - // We cannot remove src/ since the generator code lives within. This is bad testing - // approach. - rmDirAndFilesIfExist('hooks'); - rmfileIfExists('test-foo-bar.hook.ts'); - rmfileIfExists('index.ts'); - }); + const fs = new FileSystem(); + + beforeEach(() => fs.setUp()); + + afterEach(() => fs.tearDown()); function test(root: string) { describe(`when the directory ${root}/ exists`, () => { - const testEnv = new TestEnvironment('hook', root); - beforeEach(() => { - testEnv.mkRootDirIfDoesNotExist(); - testEnv.copyFileFromMocks('index.ts'); + fs + .ensureDir(root) + .cd(root) + .copyFixture('hook/index.ts', 'index.ts'); }); it('should render the templates in the proper directory.', () => { createHook({ name: 'test-fooBar' }); - testEnv - .validateSpec('test-foo-bar.hook.ts') - .validateSpec('index.ts', 'index.ts'); + fs + .assertEqual('test-foo-bar.hook.ts', 'hook/test-foo-bar.hook.ts') + .assertEqual('index.ts', 'hook/index.ts'); }); - it('should not throw an error if index.ts does not exist.', () => { - testEnv.rmfileIfExists('index.ts'); + it('should create index.ts if it does not exist.', () => { + fs.rmfile('index.ts'); + createHook({ name: 'test-fooBar' }); + + fs.assertExists('index.ts'); }); }); diff --git a/packages/cli/src/generate/generators/hook/create-hook.ts b/packages/cli/src/generate/generators/hook/create-hook.ts index d1f3a0855c..9d87c7ec52 100644 --- a/packages/cli/src/generate/generators/hook/create-hook.ts +++ b/packages/cli/src/generate/generators/hook/create-hook.ts @@ -1,24 +1,22 @@ -// std -import { existsSync } from 'fs'; - // FoalTS -import { Generator, getNames } from '../../utils'; +import { FileSystem } from '../../file-system'; +import { getNames } from '../../utils'; export function createHook({ name }: { name: string }) { - const names = getNames(name); + const fs = new FileSystem(); let root = ''; - - if (existsSync('src/app/hooks')) { + if (fs.exists('src/app/hooks')) { root = 'src/app/hooks'; - } else if (existsSync('hooks')) { + } else if (fs.exists('hooks')) { root = 'hooks'; } - new Generator('hook', root) - .renderTemplate('hook.ts', names, `${names.kebabName}.hook.ts`) - .updateFile('index.ts', content => { - content += `export { ${names.upperFirstCamelName} } from './${names.kebabName}.hook';\n`; - return content; - }, { allowFailure: true }); + const names = getNames(name); + + fs + .cd(root) + .render('hook/hook.ts', `${names.kebabName}.hook.ts`, names) + .ensureFile('index.ts') + .addNamedExportIn('index.ts', names.upperFirstCamelName, `./${names.kebabName}.hook`); } diff --git a/packages/cli/src/generate/generators/model/create-model.spec.ts b/packages/cli/src/generate/generators/model/create-model.spec.ts index 95eb6cde35..8a39d01559 100644 --- a/packages/cli/src/generate/generators/model/create-model.spec.ts +++ b/packages/cli/src/generate/generators/model/create-model.spec.ts @@ -1,45 +1,45 @@ +// std +import { strictEqual } from 'assert'; + // FoalTS -import { - rmDirAndFilesIfExist, - rmfileIfExists, - TestEnvironment, -} from '../../utils'; +import { yellow } from 'colors/safe'; +import { ClientError, FileSystem } from '../../file-system'; import { createModel } from './create-model'; describe('createModel', () => { - afterEach(() => { - rmDirAndFilesIfExist('src/app'); - // We cannot remove src/ since the generator code lives within. This is bad testing - // approach. - rmDirAndFilesIfExist('models'); - rmfileIfExists('a-test-foo-bar.model.ts'); - rmfileIfExists('test-foo-bar.model.ts'); - rmfileIfExists('index.ts'); - }); + const fs = new FileSystem(); + + beforeEach(() => fs.setUp()); + + afterEach(() => fs.tearDown()); function test(root: string) { describe(`when the directory ${root}/ exists`, () => { - const testEnv = new TestEnvironment('model', root); - beforeEach(() => { - testEnv.mkRootDirIfDoesNotExist(); - testEnv.copyFileFromMocks('index.ts'); + fs + .copyFixture('model/package.mongoose.json', 'package.json') + .ensureDir(root) + .cd(root) + .copyFixture('model/index.ts', 'index.ts'); }); it('should render the templates in the proper directory.', () => { createModel({ name: 'test-fooBar' }); - testEnv - .validateSpec('test-foo-bar.model.ts') - .validateSpec('index.ts', 'index.ts'); + fs + .assertEqual('test-foo-bar.model.ts', 'model/test-foo-bar.model.ts') + .assertEqual('index.ts', 'model/index.ts'); }); - it('should not throw an error if index.ts does not exist.', () => { - testEnv.rmfileIfExists('index.ts'); + it('should create index.ts if it does not exist.', () => { + fs.rmfile('index.ts'); + createModel({ name: 'test-fooBar' }); + + fs.assertExists('index.ts'); }); }); @@ -50,4 +50,24 @@ describe('createModel', () => { test('models'); test(''); + it('should throw a ClientError if the project does not have the dependency "mongoose".', () => { + fs + .copyFixture('model/index.ts', 'index.ts') + .copyFixture('model/package.json', 'package.json'); + + try { + createModel({ name: 'test-fooBar' }); + throw new Error('An error should have been thrown'); + } catch (error) { + if (!(error instanceof ClientError)) { + throw new Error('The error thrown should be an instance of ClientError.'); + } + strictEqual( + error.message, + `"foal generate|g ${yellow('model')} " can only be used in a Mongoose project.\n` + + ` Please use "foal generate|g ${yellow('entity')} " instead.` + ); + } + }); + }); diff --git a/packages/cli/src/generate/generators/model/create-model.ts b/packages/cli/src/generate/generators/model/create-model.ts index 7da7709b36..0f6baa16ee 100644 --- a/packages/cli/src/generate/generators/model/create-model.ts +++ b/packages/cli/src/generate/generators/model/create-model.ts @@ -1,41 +1,32 @@ -// std -import { existsSync, readFileSync } from 'fs'; -import { join } from 'path'; - // 3p -import { red, yellow } from 'colors/safe'; +import { yellow } from 'colors/safe'; // FoalTS -import { findProjectPath, Generator, getNames } from '../../utils'; +import { ClientError, FileSystem } from '../../file-system'; +import { getNames } from '../../utils'; -export function createModel({ name, checkMongoose }: { name: string, checkMongoose?: boolean }) { - const projectPath = findProjectPath(); +export function createModel({ name }: { name: string }) { + const fs = new FileSystem(); - if (checkMongoose && projectPath !== null) { - const pkg = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')); - if (!pkg.dependencies || !pkg.dependencies.mongoose) { - console.log(red( - `\n "foal generate|g ${yellow('model')} " can only be used in a Mongoose project. ` - + `\n Please use "foal generate|g ${yellow('entity')} " instead.\n` - )); - return; - } + if (!fs.projectHasDependency('mongoose')) { + throw new ClientError( + `"foal generate|g ${yellow('model')} " can only be used in a Mongoose project.\n` + + ` Please use "foal generate|g ${yellow('entity')} " instead.` + ); } - const names = getNames(name); - let root = ''; - - if (existsSync('src/app/models')) { + if (fs.exists('src/app/models')) { root = 'src/app/models'; - } else if (existsSync('models')) { + } else if (fs.exists('models')) { root = 'models'; } - new Generator('model', root) - .renderTemplate('model.ts', names, `${names.kebabName}.model.ts`) - .updateFile('index.ts', content => { - content += `export { ${names.upperFirstCamelName} } from './${names.kebabName}.model';\n`; - return content; - }, { allowFailure: true }); + const names = getNames(name); + + fs + .cd(root) + .render('model/model.ts', `${names.kebabName}.model.ts`, names) + .ensureFile('index.ts') + .addNamedExportIn('index.ts', names.upperFirstCamelName, `./${names.kebabName}.model`); } diff --git a/packages/cli/src/generate/generators/react/connect-react.spec.ts b/packages/cli/src/generate/generators/react/connect-react.spec.ts index 97cdf01c96..2d16780763 100644 --- a/packages/cli/src/generate/generators/react/connect-react.spec.ts +++ b/packages/cli/src/generate/generators/react/connect-react.spec.ts @@ -1,22 +1,23 @@ -import { mkdirIfDoesNotExist, rmDirAndFilesIfExist, TestEnvironment } from '../../utils'; +import { FileSystem } from '../../file-system'; import { connectReact } from './connect-react'; describe('connectReact', () => { - afterEach(() => rmDirAndFilesIfExist('connector-test')); + const fs = new FileSystem(); - const testEnv = new TestEnvironment('react'); + beforeEach(() => fs.setUp()); - it('should update package.json to set up the proxy, install ncp and change the output dir.', () => { - mkdirIfDoesNotExist('connector-test/react'); + afterEach(() => fs.tearDown()); - testEnv - .copyFileFromMocks('package.json', 'connector-test/react/package.json'); + it('should update package.json to set up the proxy, install ncp and change the output dir.', () => { + fs + .ensureDir('connector-test/react') + .copyFixture('react/package.json', 'connector-test/react/package.json'); connectReact('./connector-test/react'); - testEnv - .validateSpec('package.json', 'connector-test/react/package.json'); + fs + .assertEqual('connector-test/react/package.json', 'react/package.json'); }); it('should not throw if the path does not exist.', () => { @@ -24,7 +25,8 @@ describe('connectReact', () => { }); it('should not throw if package.json does not exist.', () => { - mkdirIfDoesNotExist('connector-test/react'); + fs + .ensureDir('connector-test/react'); connectReact('./connector-test/react'); }); diff --git a/packages/cli/src/generate/generators/react/connect-react.ts b/packages/cli/src/generate/generators/react/connect-react.ts index c60c83a320..6b632038be 100644 --- a/packages/cli/src/generate/generators/react/connect-react.ts +++ b/packages/cli/src/generate/generators/react/connect-react.ts @@ -1,26 +1,28 @@ import { join } from 'path'; import { red } from 'colors/safe'; -import { existsSync } from 'fs'; -import { Generator } from '../../utils'; +import { FileSystem } from '../../file-system'; export function connectReact(path: string) { - if (!existsSync(path)) { - if (process.env.NODE_ENV !== 'test') { + const fs = new FileSystem(); + + if (!fs.exists(path)) { + if (process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST !== 'true') { console.log(red(` The directory ${path} does not exist.`)); } return; } - if (!existsSync(join(path, 'package.json'))) { - if (process.env.NODE_ENV !== 'test') { + if (!fs.exists(join(path, 'package.json'))) { + if (process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST !== 'true') { console.log(red(` The directory ${path} is not a React project (missing package.json).`)); } return; } - new Generator('react', path) - .updateFile('package.json', content => { + fs + .cd(path) + .modify('package.json', content => { const pkg = JSON.parse(content); pkg.proxy = 'http://localhost:3001'; return JSON.stringify(pkg, null, 2); diff --git a/packages/cli/src/generate/generators/rest-api/create-rest-api.spec.ts b/packages/cli/src/generate/generators/rest-api/create-rest-api.spec.ts index 608e77af41..b35a9fa950 100644 --- a/packages/cli/src/generate/generators/rest-api/create-rest-api.spec.ts +++ b/packages/cli/src/generate/generators/rest-api/create-rest-api.spec.ts @@ -1,119 +1,104 @@ +// std +import { strictEqual } from 'assert'; + // FoalTS -import { - rmDirAndFilesIfExist, - rmfileIfExists, - TestEnvironment, -} from '../../utils'; +import { ClientError, FileSystem } from '../../file-system'; import { createRestApi } from './create-rest-api'; +// TODO: add tests like "should create index.ts if it does not exist." + describe('createRestApi', () => { - afterEach(() => { - rmDirAndFilesIfExist('src/app'); - // We cannot remove src/ since the generator code lives within. This is bad testing - // approach. - rmDirAndFilesIfExist('entities'); - rmDirAndFilesIfExist('controllers'); - rmfileIfExists('app.controller.ts'); - rmfileIfExists('test-foo-bar.entity.ts'); - rmfileIfExists('test-foo-bar.controller.ts'); - rmfileIfExists('test-foo-bar.controller.spec.ts'); - }); + const fs = new FileSystem(); + + beforeEach(() => fs.setUp()); + + afterEach(() => fs.tearDown()); function test(root: string) { describe(`when the directories ${root}entities/ and ${root}controllers/ exist`, () => { - const testEntityEnv = new TestEnvironment('rest-api', root + 'entities'); - const testControllerEnv = new TestEnvironment('rest-api', root + 'controllers'); - beforeEach(() => { - testEntityEnv.mkRootDirIfDoesNotExist(); - testEntityEnv.copyFileFromMocks('index.entities.ts', 'index.ts'); - testControllerEnv.mkRootDirIfDoesNotExist(); - testControllerEnv.copyFileFromMocks('index.controllers.ts', 'index.ts'); + fs + .copyFixture('rest-api/package.json', 'package.json') + .ensureDir(root) + .cd(root) + .ensureDir('entities') + .cd('entities') + .copyFixture('rest-api/index.entities.ts', 'index.ts') + .cd('..') + .ensureDir('controllers') + .cd('controllers') + .copyFixture('rest-api/index.controllers.ts', 'index.ts') + .cd('..'); }); it('should render the templates in the proper directory.', () => { createRestApi({ name: 'test-fooBar', register: false }); - testEntityEnv - .validateSpec('test-foo-bar.entity.ts') - .validateSpec('index.entities.ts', 'index.ts'); - - testControllerEnv - .validateSpec('test-foo-bar.controller.ts') - .validateSpec('test-foo-bar.controller.spec.ts') - .validateSpec('index.controllers.ts', 'index.ts'); - }); - - }); - - describe(`when the directories ${root}entities/ and ${root}controllers/ exist and "register" is true.`, () => { - - const testEntityEnv = new TestEnvironment('rest-api', root + 'entities'); - const testControllerEnv = new TestEnvironment('rest-api', root + 'controllers'); - - beforeEach(() => { - testEntityEnv.mkRootDirIfDoesNotExist(); - testEntityEnv.copyFileFromMocks('index.entities.ts', 'index.ts'); - testControllerEnv.mkRootDirIfDoesNotExist(); - testControllerEnv.copyFileFromMocks('index.controllers.ts', 'index.ts'); - }); - - it('should update the "subControllers" import in src/app/app.controller.ts if it exists.', () => { - testControllerEnv.copyFileFromMocks('app.controller.controller-import.ts', '../app.controller.ts'); - - createRestApi({ name: 'test-fooBar', register: true }); - - testControllerEnv - .validateSpec('app.controller.controller-import.ts', '../app.controller.ts'); + fs + .cd('entities') + .assertEqual('test-foo-bar.entity.ts', 'rest-api/test-foo-bar.entity.ts') + .assertEqual('index.ts', 'rest-api/index.entities.ts') + .cd('..') + .cd('controllers') + .assertEqual('test-foo-bar.controller.ts', 'rest-api/test-foo-bar.controller.ts') + .assertEqual('test-foo-bar.controller.spec.ts', 'rest-api/test-foo-bar.controller.spec.ts') + .assertEqual('index.ts', 'rest-api/index.controllers.ts'); }); - it('should add a "subControllers" import in src/app/app.controller.ts if none already exists.', () => { - testControllerEnv.copyFileFromMocks('app.controller.no-controller-import.ts', '../app.controller.ts'); - - createRestApi({ name: 'test-fooBar', register: true }); - - testControllerEnv - .validateSpec('app.controller.no-controller-import.ts', '../app.controller.ts'); + it('should render the templates in the proper directory (--auth flag).', () => { + createRestApi({ name: 'test-fooBar', register: false, auth: true }); + + fs + .cd('entities') + .assertEqual('test-foo-bar.entity.ts', 'rest-api/test-foo-bar.entity.auth.ts') + .assertEqual('index.ts', 'rest-api/index.entities.ts') + .cd('..') + .cd('controllers') + .assertEqual('test-foo-bar.controller.ts', 'rest-api/test-foo-bar.controller.auth.ts') + .assertEqual('test-foo-bar.controller.spec.ts', 'rest-api/test-foo-bar.controller.spec.auth.ts') + .assertEqual('index.ts', 'rest-api/index.controllers.ts'); }); - it('should update the "@foal/core" import in src/app/app.controller.ts if it exists.', () => { - testControllerEnv.copyFileFromMocks('app.controller.core-import.ts', '../app.controller.ts'); + it('should create the index.ts if they do not exist.', () => { + fs.rmfile('entities/index.ts'); + fs.rmfile('controllers/index.ts'); - createRestApi({ name: 'test-fooBar', register: true }); - - testControllerEnv - .validateSpec('app.controller.core-import.ts', '../app.controller.ts'); - }); - - it('should update the "subControllers = []" property in src/app/app.controller.ts if it exists.', () => { - testControllerEnv.copyFileFromMocks('app.controller.empty-property.ts', '../app.controller.ts'); - - createRestApi({ name: 'test-fooBar', register: true }); + createRestApi({ name: 'test-fooBar', register: false }); - testControllerEnv - .validateSpec('app.controller.empty-property.ts', '../app.controller.ts'); + fs.assertExists('entities/index.ts'); + fs.assertExists('controllers/index.ts'); }); - it('should update the "subControllers = [ \\n \\n ]" property in src/app/app.controller.ts if it exists.', () => { - testControllerEnv.copyFileFromMocks('app.controller.empty-spaced-property.ts', '../app.controller.ts'); + }); - createRestApi({ name: 'test-fooBar', register: true }); + describe(`when the directories ${root}entities/ and ${root}controllers/ exist and "register" is true.`, () => { - testControllerEnv - .validateSpec('app.controller.empty-spaced-property.ts', '../app.controller.ts'); + beforeEach(() => { + fs + .copyFixture('rest-api/package.json', 'package.json') + .ensureDir(root) + .cd(root) + .ensureDir('entities') + .cd('entities') + .copyFixture('rest-api/index.entities.ts', 'index.ts') + .cd('..') + .ensureDir('controllers') + .cd('controllers') + .copyFixture('rest-api/index.controllers.ts', 'index.ts') + .cd('..'); }); - it('should update the "subControllers = [ \\n (.*) \\n ]" property in' - + ' src/app/app.controller.ts if it exists.', () => { - testControllerEnv.copyFileFromMocks('app.controller.no-empty-property.ts', '../app.controller.ts'); + it('should register the controller in app.controller.ts.', () => { + fs + .copyFixture('rest-api/app.controller.ts', 'app.controller.ts'); createRestApi({ name: 'test-fooBar', register: true }); - testControllerEnv - .validateSpec('app.controller.no-empty-property.ts', '../app.controller.ts'); + fs + .assertEqual('app.controller.ts', 'rest-api/app.controller.ts'); }); }); @@ -125,23 +110,59 @@ describe('createRestApi', () => { describe('when the directory entities/ or the directory controllers/ does not exist.', () => { - const testEnv = new TestEnvironment('rest-api', ''); - beforeEach(() => { - testEnv.mkRootDirIfDoesNotExist(); - testEnv.copyFileFromMocks('index.current-dir.ts', 'index.ts'); + fs + .copyFixture('rest-api/package.json', 'package.json') + .copyFixture('rest-api/index.current-dir.ts', 'index.ts'); }); it('should render the templates in the current directory.', () => { createRestApi({ name: 'test-fooBar', register: false }); - testEnv - .validateSpec('test-foo-bar.entity.ts') - .validateSpec('test-foo-bar.controller.current-dir.ts', 'test-foo-bar.controller.ts') - .validateSpec('test-foo-bar.controller.spec.current-dir.ts', 'test-foo-bar.controller.spec.ts') - .validateSpec('index.current-dir.ts', 'index.ts'); + fs + .assertEqual('test-foo-bar.entity.ts', 'rest-api/test-foo-bar.entity.ts') + .assertEqual('test-foo-bar.controller.ts', 'rest-api/test-foo-bar.controller.current-dir.ts') + .assertEqual('test-foo-bar.controller.spec.ts', 'rest-api/test-foo-bar.controller.spec.current-dir.ts') + .assertEqual('index.ts', 'rest-api/index.current-dir.ts'); }); + it('should render the templates in the current directory (--auth flag).', () => { + createRestApi({ name: 'test-fooBar', register: false, auth: true }); + + fs + .assertEqual('test-foo-bar.entity.ts', 'rest-api/test-foo-bar.entity.auth.ts') + .assertEqual('test-foo-bar.controller.ts', 'rest-api/test-foo-bar.controller.current-dir.auth.ts') + .assertEqual('test-foo-bar.controller.spec.ts', 'rest-api/test-foo-bar.controller.spec.current-dir.auth.ts') + .assertEqual('index.ts', 'rest-api/index.current-dir.ts'); + }); + + it('should create index.ts if it does not exist.', () => { + fs.rmfile('index.ts'); + + createRestApi({ name: 'test-fooBar', register: false }); + + fs.assertExists('index.ts'); + }); + + }); + + it('should throw a ClientError if the project has the dependency "mongoose".', () => { + fs + .copyFixture('model/index.ts', 'index.ts') + .copyFixture('model/package.mongoose.json', 'package.json'); + + try { + createRestApi({ name: 'test-fooBar', register: false }); + throw new Error('An error should have been thrown'); + } catch (error) { + if (!(error instanceof ClientError)) { + throw new Error('The error thrown should be an instance of ClientError.'); + } + strictEqual( + error.message, + `"foal generate|g rest-api " cannot be used in a Mongoose project.` + ); + } }); }); diff --git a/packages/cli/src/generate/generators/rest-api/create-rest-api.ts b/packages/cli/src/generate/generators/rest-api/create-rest-api.ts index f9c83dce89..0b7ac027ad 100644 --- a/packages/cli/src/generate/generators/rest-api/create-rest-api.ts +++ b/packages/cli/src/generate/generators/rest-api/create-rest-api.ts @@ -1,73 +1,96 @@ -// std -import { existsSync, readFileSync } from 'fs'; -import { join } from 'path'; - // 3p -import { red, underline } from 'colors/safe'; +import { underline } from 'colors/safe'; // FoalTS -import { findProjectPath, Generator, getNames } from '../../utils'; -import { registerController } from '../controller/register-controller'; - -export function createRestApi({ name, register }: { name: string, register: boolean }) { - const projectPath = findProjectPath(); - - if (projectPath !== null) { - const pkg = JSON.parse(readFileSync(join(projectPath, 'package.json'), 'utf8')); - if (pkg.dependencies && pkg.dependencies.mongoose) { - console.log(red( - '\n "foal generate|g rest-api " cannot be used in a Mongoose project.\n' - )); - return; - } - } +import { ClientError, FileSystem } from '../../file-system'; +import { getNames } from '../../utils'; - const names = getNames(name); +export function createRestApi({ name, register, auth }: { name: string, register: boolean, auth?: boolean }) { + auth = auth || false; + + const fs = new FileSystem(); + + if (fs.projectHasDependency('mongoose')) { + throw new ClientError('"foal generate|g rest-api " cannot be used in a Mongoose project.'); + } let entityRoot = ''; let controllerRoot = ''; - - if (existsSync('src/app/entities') && existsSync('src/app/controllers')) { + if (fs.exists('src/app/entities') && fs.exists('src/app/controllers')) { entityRoot = 'src/app/entities'; controllerRoot = 'src/app/controllers'; - } else if (existsSync('entities') && existsSync('controllers')) { + } else if (fs.exists('entities') && fs.exists('controllers')) { entityRoot = 'entities'; controllerRoot = 'controllers'; } - new Generator('rest-api', entityRoot) - .renderTemplate('entity.ts', names, `${names.kebabName}.entity.ts`) - .updateFile('index.ts', content => { - content += `export { ${names.upperFirstCamelName} } from './${names.kebabName}.entity';\n`; - return content; - }); + const names = getNames(name); + + const className = `${names.upperFirstCamelName}Controller`; - const controllerGenerator = new Generator('rest-api', controllerRoot); + fs + .cd(entityRoot) + .renderOnlyIf(!auth, 'rest-api/entity.ts', `${names.kebabName}.entity.ts`, names) + .renderOnlyIf(auth, 'rest-api/entity.auth.ts', `${names.kebabName}.entity.ts`, names) + .ensureFile('index.ts') + .addNamedExportIn('index.ts', names.upperFirstCamelName, `./${names.kebabName}.entity`); - controllerGenerator - .renderTemplate( - controllerRoot ? 'controller.ts' : 'controller.current-dir.ts', - names, - `${names.kebabName}.controller.ts` + fs.currentDir = ''; + + const isCurrentDir = !controllerRoot; + + fs + .cd(controllerRoot) + + .renderOnlyIf(!isCurrentDir && !auth, 'rest-api/controller.ts', `${names.kebabName}.controller.ts`, names) + .renderOnlyIf(!isCurrentDir && auth, 'rest-api/controller.auth.ts', `${names.kebabName}.controller.ts`, names) + .renderOnlyIf( + isCurrentDir && !auth, 'rest-api/controller.current-dir.ts', `${names.kebabName}.controller.ts`, names + ) + .renderOnlyIf( + isCurrentDir && auth, 'rest-api/controller.current-dir.auth.ts', `${names.kebabName}.controller.ts`, names + ) + + .renderOnlyIf(!isCurrentDir && !auth, 'rest-api/controller.spec.ts', `${names.kebabName}.controller.spec.ts`, names) + .renderOnlyIf( + !isCurrentDir && auth, 'rest-api/controller.spec.auth.ts', `${names.kebabName}.controller.spec.ts`, names + ) + .renderOnlyIf( + isCurrentDir && !auth, 'rest-api/controller.spec.current-dir.ts', `${names.kebabName}.controller.spec.ts`, names ) - .renderTemplate( - controllerRoot ? 'controller.spec.ts' : 'controller.spec.current-dir.ts', - names, - `${names.kebabName}.controller.spec.ts` + .renderOnlyIf( + isCurrentDir && auth, + 'rest-api/controller.spec.current-dir.auth.ts', + `${names.kebabName}.controller.spec.ts`, + names ) - .updateFile('index.ts', content => { - content += `export { ${names.upperFirstCamelName}Controller } from './${names.kebabName}.controller';\n`; - return content; - }); + + .ensureFile('index.ts') + .addNamedExportIn('index.ts', className, `./${names.kebabName}.controller`); if (register) { - controllerGenerator - .updateFile('../app.controller.ts', content => { - return registerController(content, `${names.upperFirstCamelName}Controller`, `/${names.kebabName}s`); - }, { allowFailure: true }); - } + fs + .cd('..') + .addOrExtendNamedImportIn( + 'app.controller.ts', + 'controller', + '@foal/core', + { logs: false } + ) + .addOrExtendNamedImportIn( + 'app.controller.ts', + className, + './controllers', + { logs: false } + ) + .addOrExtendClassArrayPropertyIn( + 'app.controller.ts', + 'subControllers', + `controller('/${names.kebabName}s', ${className})` + ); + } - if (process.env.NODE_ENV !== 'test') { + if (process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST !== 'true') { console.log( `\n${underline('Next steps:')} Complete ${names.upperFirstCamelName} (${names.kebabName}.entity)` + ` and ${names.camelName}Schema (${names.kebabName}.controller).` diff --git a/packages/cli/src/generate/generators/script/create-script.spec.ts b/packages/cli/src/generate/generators/script/create-script.spec.ts index 5996411885..75f7a8663a 100644 --- a/packages/cli/src/generate/generators/script/create-script.spec.ts +++ b/packages/cli/src/generate/generators/script/create-script.spec.ts @@ -1,43 +1,37 @@ // FoalTS -import { - rmDirAndFilesIfExist, - TestEnvironment, -} from '../../utils'; +import { FileSystem } from '../../file-system'; import { createScript } from './create-script'; -// TODO: Improve the tests. They currently cover partially `createScript`. - describe('createScript', () => { - afterEach(() => { - rmDirAndFilesIfExist('src/scripts'); - // We cannot remove src/ since the generator code lives within. This is bad testing - // approach. - }); - - describe(`when the directory src/scripts/ exists`, () => { + const fs = new FileSystem(); - const testEnv = new TestEnvironment('script', 'src/scripts'); + beforeEach(() => fs.setUp()); - beforeEach(() => { - testEnv.mkRootDirIfDoesNotExist(); - }); + afterEach(() => fs.tearDown()); - it('should copy the empty script file in the proper directory.', () => { - createScript({ name: 'test-fooBar' }); + it('should copy the empty script file in the proper directory.', () => { + fs + .copyFixture('script/package.json', 'package.json') + .ensureDir('src/scripts'); - testEnv - .validateSpec('test-foo-bar.ts'); - }); + createScript({ name: 'test-fooBar' }); + fs + .cd('src/scripts') + .assertEqual('test-foo-bar.ts', 'script/test-foo-bar.ts'); }); - describe(`when the directory src/scripts/ does not exist`, () => { + it('should copy the empty script file in the proper directory (mongoose).', () => { + fs + .copyFixture('script/package.mongoose.json', 'package.json') + .ensureDir('src/scripts'); - it('should not throw an error.', () => { - createScript({ name: 'test-fooBar' }); - }); + createScript({ name: 'test-fooBar' }); + fs + .cd('src/scripts') + .assertEqual('test-foo-bar.ts', 'script/test-foo-bar.mongoose.ts'); }); }); diff --git a/packages/cli/src/generate/generators/script/create-script.ts b/packages/cli/src/generate/generators/script/create-script.ts index 4d2a6d0773..0d8b96acd9 100644 --- a/packages/cli/src/generate/generators/script/create-script.ts +++ b/packages/cli/src/generate/generators/script/create-script.ts @@ -1,32 +1,17 @@ -// std -import { existsSync } from 'fs'; -import { join, relative } from 'path'; - -// 3p -import { red } from 'colors/safe'; - // FoalTS -import { findProjectPath, Generator, getNames } from '../../utils'; +import { FileSystem } from '../../file-system'; +import { getNames } from '../../utils'; export function createScript({ name }: { name: string }) { const names = getNames(name); - const root = findProjectPath(); - - if (!root) { - console.log(red('\n This directory is not a Foal project (missing package.json).\n')); - return; - } - - const scriptPath = join(root, './src/scripts/'); - if (!existsSync(scriptPath)) { - if (process.env.NODE_ENV !== 'test') { - console.log(red(`\n This directory is not a Foal project (${scriptPath} does not exist).\n`)); - } - return; - } + const fs = new FileSystem(); + const isMongoose = fs.projectHasDependency('mongoose'); - // Use `relative` to have pretty CREATE logs. - new Generator('script', relative(process.cwd(), scriptPath)) - .copyFileFromTemplates('script.ts', `${names.kebabName}.ts`); + fs + // TODO: test this line + .cdProjectRootDir() + .cd('src/scripts') + .copyOnlyIf(!isMongoose, 'script/script.ts', `${names.kebabName}.ts`) + .copyOnlyIf(isMongoose, 'script/script.mongoose.ts', `${names.kebabName}.ts`); } diff --git a/packages/cli/src/generate/generators/service/create-service.spec.ts b/packages/cli/src/generate/generators/service/create-service.spec.ts index 7a25293495..19dd2364a0 100644 --- a/packages/cli/src/generate/generators/service/create-service.spec.ts +++ b/packages/cli/src/generate/generators/service/create-service.spec.ts @@ -1,46 +1,47 @@ // FoalTS -import { - rmDirAndFilesIfExist, - rmfileIfExists, - TestEnvironment -} from '../../utils'; +import { FileSystem } from '../../file-system'; import { createService } from './create-service'; describe('createService', () => { - afterEach(() => { - rmDirAndFilesIfExist('src/app'); - // We cannot remove src/ since the generator code lives within. This is bad testing - // approach. - rmDirAndFilesIfExist('services'); - rmfileIfExists('test-foo-bar.service.ts'); - rmfileIfExists('test-foo-bar-collection.service.ts'); - rmfileIfExists('test-foo-bar-resolver.service.ts'); - rmfileIfExists('index.ts'); - }); + const fs = new FileSystem(); + + beforeEach(() => fs.setUp()); + + afterEach(() => fs.tearDown()); function test(root: string) { describe(`when the directory ${root}/ exists`, () => { - const testEnv = new TestEnvironment('service', root); - beforeEach(() => { - testEnv.mkRootDirIfDoesNotExist(); - testEnv.copyFileFromMocks('index.ts'); + fs + .ensureDir(root) + .cd(root) + .copyFixture('service/index.ts', 'index.ts'); }); it('should render the empty templates in the proper directory.', () => { createService({ name: 'test-fooBar' }); - testEnv - .validateSpec('test-foo-bar.service.empty.ts', 'test-foo-bar.service.ts') - .validateSpec('index.ts', 'index.ts'); + fs + .assertEqual('test-foo-bar.service.ts', 'service/test-foo-bar.service.empty.ts') + .assertEqual('index.ts', 'service/index.ts'); + }); + + it('should create the directory if it does not exist.', () => { + createService({ name: 'barfoo/hello/test-fooBar' }); + + fs + .assertExists('barfoo/hello/test-foo-bar.service.ts'); }); - it('should not throw an error if index.ts does not exist.', () => { - testEnv.rmfileIfExists('index.ts'); + it('should should create index.ts if it does not exist.', () => { + fs.rmfile('index.ts'); + createService({ name: 'test-fooBar' }); + + fs.assertExists('index.ts'); }); }); diff --git a/packages/cli/src/generate/generators/service/create-service.ts b/packages/cli/src/generate/generators/service/create-service.ts index 44a772c745..b337aaf57c 100644 --- a/packages/cli/src/generate/generators/service/create-service.ts +++ b/packages/cli/src/generate/generators/service/create-service.ts @@ -1,25 +1,27 @@ -// std -import { existsSync } from 'fs'; - // FoalTS -import { Generator, getNames } from '../../utils'; +import { basename, dirname } from 'path'; +import { FileSystem } from '../../file-system'; +import { getNames } from '../../utils'; export function createService({ name }: { name: string }) { - const names = getNames(name); + const fs = new FileSystem(); let root = ''; - - if (existsSync('src/app/services')) { + if (fs.exists('src/app/services')) { root = 'src/app/services'; - } else if (existsSync('services')) { + } else if (fs.exists('services')) { root = 'services'; } - new Generator('service', root) - .renderTemplate('service.empty.ts', names, `${names.kebabName}.service.ts`) - .updateFile('index.ts', content => { - content += `export { ${names.upperFirstCamelName} } from './${names.kebabName}.service';\n`; - return content; - }, { allowFailure: true }); + const names = getNames(basename(name)); + const subdir = dirname(name); + + fs + .cd(root) + .ensureDir(subdir) + .cd(subdir) + .render('service/service.empty.ts', `${names.kebabName}.service.ts`, names) + .ensureFile('index.ts') + .addNamedExportIn('index.ts', names.upperFirstCamelName, `./${names.kebabName}.service`); } diff --git a/packages/cli/src/generate/generators/sub-app/create-sub-app.spec.ts b/packages/cli/src/generate/generators/sub-app/create-sub-app.spec.ts index 084801b8b5..b804cce333 100644 --- a/packages/cli/src/generate/generators/sub-app/create-sub-app.spec.ts +++ b/packages/cli/src/generate/generators/sub-app/create-sub-app.spec.ts @@ -1,18 +1,30 @@ // std import { strictEqual } from 'assert'; -import { existsSync, writeFileSync } from 'fs'; +import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; +import { join } from 'path'; // FoalTS import { mkdirIfDoesNotExist, - readFileFromRoot, - readFileFromTemplatesSpec, rmDirAndFilesIfExist, - rmfileIfExists } from '../../utils'; import { createSubApp } from './create-sub-app'; -// TODO: Use TestEnvironment. +// TODO: Use FileSystem or remove this command. + +function rmfileIfExists(path: string) { + if (existsSync(path)) { + unlinkSync(path); + } +} + +function readFileFromTemplatesSpec(src: string): string { + return readFileSync(join(__dirname, '../../specs', src), 'utf8'); +} + +function readFileFromRoot(src: string): string { + return readFileSync(src, 'utf8'); +} describe('createSubApp', () => { @@ -188,32 +200,4 @@ describe('createSubApp', () => { }); - describe('should create the controllers/templates directory.', () => { - - function test(prefix = '') { - writeFileSync(`${prefix}index.ts`, indexInitialContent, 'utf8'); - - createSubApp({ name: 'test-fooBar' }); - - if (!existsSync(`${prefix}test-foo-bar/controllers/templates`)) { - throw new Error('The controllers/templates directory should be created.'); - } - } - - it('in src/app/sub-apps/ if the directory exists.', () => { - mkdirIfDoesNotExist('src/app/sub-apps'); - test('src/app/sub-apps/'); - }); - - it('in sub-apps/ if the directory exists.', () => { - mkdirIfDoesNotExist('sub-apps'); - test('sub-apps/'); - }); - - it('in the current directory otherwise.', () => { - test(); - }); - - }); - }); diff --git a/packages/cli/src/generate/generators/sub-app/create-sub-app.ts b/packages/cli/src/generate/generators/sub-app/create-sub-app.ts index 5560db7298..f092382c09 100644 --- a/packages/cli/src/generate/generators/sub-app/create-sub-app.ts +++ b/packages/cli/src/generate/generators/sub-app/create-sub-app.ts @@ -1,46 +1,48 @@ -// 3p -import { existsSync, writeFileSync } from 'fs'; - // FoalTS -import { Generator, getNames, mkdirIfDoesNotExist } from '../../utils'; +import { FileSystem } from '../../file-system'; +import { getNames } from '../../utils'; export function createSubApp({ name }: { name: string }) { - const names = getNames(name); + const fs = new FileSystem(); + (fs as any).testDir = ''; let root = ''; - - if (existsSync('src/app')) { - mkdirIfDoesNotExist('src/app/sub-apps'); - if (!existsSync('src/app/sub-apps/index.ts')) { - writeFileSync('src/app/sub-apps/index.ts', '', 'utf8'); - } + if (fs.exists('src/app')) { + fs + .ensureDir('src/app/sub-apps') + .ensureFile('src/app/sub-apps/index.ts'); root = 'src/app/sub-apps'; - } else if (existsSync('sub-apps')) { + } else if (fs.exists('sub-apps')) { root = 'sub-apps'; } - new Generator('sub-app', root) - .mkdirIfDoesNotExist(names.kebabName) - .updateFile('index.ts', content => { - content += `export { ${names.upperFirstCamelName}Controller } from './${names.kebabName}';\n`; - return content; - }); + const names = getNames(name); - new Generator('sub-app', root ? root + '/' + names.kebabName : names.kebabName) - .renderTemplate('index.ts', names) - .renderTemplate('controller.ts', names, `${names.kebabName}.controller.ts`) - // Controllers - .mkdirIfDoesNotExist('controllers') - .copyFileFromTemplates('controllers/index.ts') - .mkdirIfDoesNotExist('controllers/templates') - // Hooks - .mkdirIfDoesNotExist('hooks') - .copyFileFromTemplates('hooks/index.ts') - // Entities - .mkdirIfDoesNotExist('entities') - .copyFileFromTemplates('entities/index.ts') - // Services - .mkdirIfDoesNotExist('services') - .copyFileFromTemplates('services/index.ts'); + fs + .cd(root) + .ensureDir(names.kebabName) + .addNamedExportIn('index.ts', `${names.upperFirstCamelName}Controller`, `./${names.kebabName}`) + .cd(names.kebabName) + .render('sub-app/index.ts', 'index.ts', names) + .render('sub-app/controller.ts', `${names.kebabName}.controller.ts`, names) + // Controllers + .ensureDir('controllers') + .cd('controllers') + .copy('sub-app/controllers/index.ts', 'index.ts') + .cd('..') + // Hooks + .ensureDir('hooks') + .cd('hooks') + .copy('sub-app/hooks/index.ts', 'index.ts') + .cd('..') + // Entities + .ensureDir('entities') + .cd('entities') + .copy('sub-app/entities/index.ts', 'index.ts') + .cd('..') + // Services + .ensureDir('services') + .cd('services') + .copy('sub-app/services/index.ts', 'index.ts'); } diff --git a/packages/cli/src/generate/generators/vscode-config/create-vscode-config.spec.ts b/packages/cli/src/generate/generators/vscode-config/create-vscode-config.spec.ts index cdfc6ef911..a3a457fc17 100644 --- a/packages/cli/src/generate/generators/vscode-config/create-vscode-config.spec.ts +++ b/packages/cli/src/generate/generators/vscode-config/create-vscode-config.spec.ts @@ -1,22 +1,24 @@ // FoalTS -import { - rmDirAndFilesIfExist, - TestEnvironment, -} from '../../utils'; +import { FileSystem } from '../../file-system'; import { createVSCodeConfig } from './create-vscode-config'; describe('createVSCodeConfig', () => { + const fs = new FileSystem(); - afterEach(() => rmDirAndFilesIfExist('.vscode')); + beforeEach(() => fs.setUp()); - const testEnv = new TestEnvironment('vscode-config', '.vscode'); + afterEach(() => fs.tearDown()); it('should create the directory .vscode/ with default launch.json and tasks.json files.', () => { + fs + .copyFixture('vscode-config/package.json', 'package.json'); + createVSCodeConfig(); - testEnv - .validateSpec('launch.json') - .validateSpec('tasks.json'); + fs + .cd('.vscode') + .assertEqual('launch.json', 'vscode-config/launch.json') + .assertEqual('tasks.json', 'vscode-config/tasks.json'); }); }); diff --git a/packages/cli/src/generate/generators/vscode-config/create-vscode-config.ts b/packages/cli/src/generate/generators/vscode-config/create-vscode-config.ts index 72e7c599c9..c1a0383a9b 100644 --- a/packages/cli/src/generate/generators/vscode-config/create-vscode-config.ts +++ b/packages/cli/src/generate/generators/vscode-config/create-vscode-config.ts @@ -1,10 +1,12 @@ // FoalTS -import { Generator, mkdirIfDoesNotExist } from '../../utils'; +import { FileSystem } from '../../file-system'; export function createVSCodeConfig() { - mkdirIfDoesNotExist('.vscode'); - - new Generator('vscode-config', '.vscode') - .copyFileFromTemplates('launch.json') - .copyFileFromTemplates('tasks.json'); + new FileSystem() + // TODO: test this line + .cdProjectRootDir() + .ensureDir('.vscode') + .cd('.vscode') + .copy('vscode-config/launch.json', 'launch.json') + .copy('vscode-config/tasks.json', 'tasks.json'); } diff --git a/packages/cli/src/generate/generators/vue/connect-vue.spec.ts b/packages/cli/src/generate/generators/vue/connect-vue.spec.ts index ea2a3e8167..47f273a063 100644 --- a/packages/cli/src/generate/generators/vue/connect-vue.spec.ts +++ b/packages/cli/src/generate/generators/vue/connect-vue.spec.ts @@ -1,22 +1,23 @@ -import { mkdirIfDoesNotExist, rmDirAndFilesIfExist, TestEnvironment } from '../../utils'; +import { FileSystem } from '../../file-system'; import { connectVue } from './connect-vue'; describe('connectVue', () => { - afterEach(() => rmDirAndFilesIfExist('connector-test')); + const fs = new FileSystem(); - const testEnv = new TestEnvironment('vue'); + beforeEach(() => fs.setUp()); - it('should update package.json to set up the proxy, install ncp and change the output dir.', () => { - mkdirIfDoesNotExist('connector-test/vue'); + afterEach(() => fs.tearDown()); - testEnv - .copyFileFromMocks('package.json', 'connector-test/vue/package.json'); + it('should update package.json to set up the proxy, install ncp and change the output dir.', () => { + fs + .ensureDir('connector-test/vue') + .copyFixture('vue/package.json', 'connector-test/vue/package.json'); - connectVue('./connector-test/vue'); + connectVue('./connector-test/vue'); - testEnv - .validateSpec('package.json', 'connector-test/vue/package.json'); + fs + .assertEqual('connector-test/vue/package.json', 'vue/package.json'); }); it('should not throw if the path does not exist.', () => { @@ -24,7 +25,8 @@ describe('connectVue', () => { }); it('should not throw if package.json does not exist.', () => { - mkdirIfDoesNotExist('connector-test/vue'); + fs + .ensureDir('connector-test/vue'); connectVue('./connector-test/vue'); }); diff --git a/packages/cli/src/generate/generators/vue/connect-vue.ts b/packages/cli/src/generate/generators/vue/connect-vue.ts index b44cb5f93b..b018ec4137 100644 --- a/packages/cli/src/generate/generators/vue/connect-vue.ts +++ b/packages/cli/src/generate/generators/vue/connect-vue.ts @@ -1,26 +1,28 @@ import { join, relative } from 'path'; import { red } from 'colors/safe'; -import { existsSync } from 'fs'; -import { Generator } from '../../utils'; +import { FileSystem } from '../../file-system'; export function connectVue(path: string) { - if (!existsSync(path)) { - if (process.env.NODE_ENV !== 'test') { + const fs = new FileSystem(); + + if (!fs.exists(path)) { + if (process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST !== 'true') { console.log(red(` The directory ${path} does not exist.`)); } return; } - if (!existsSync(join(path, 'package.json'))) { - if (process.env.NODE_ENV !== 'test') { + if (!fs.exists(join(path, 'package.json'))) { + if (process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST !== 'true') { console.log(red(` The directory ${path} is not a Vue project (missing package.json).`)); } return; } - new Generator('vue', path) - .updateFile('package.json', content => { + fs + .cd(path) + .modify('package.json', content => { const pkg = JSON.parse(content); pkg.vue = pkg.vue || {}; @@ -30,7 +32,9 @@ export function connectVue(path: string) { pkg.vue.devServer.proxy['^/api'] = { target: 'http://localhost:3001' }; // Output build directory - const outputPath = join(relative(path, process.cwd()), 'public'); + const outputPath = join(relative(path, process.cwd()), 'public') + // Make projects generated on Windows build on Unix. + .replace(/\\/g, '/'); pkg.vue.outputDir = outputPath; return JSON.stringify(pkg, null, 2); diff --git a/packages/cli/src/generate/mocks/controller/app.controller.controller-import.ts b/packages/cli/src/generate/mocks/controller/app.controller.controller-import.ts deleted file mode 100644 index 0708239bd0..0000000000 --- a/packages/cli/src/generate/mocks/controller/app.controller.controller-import.ts +++ /dev/null @@ -1,4 +0,0 @@ -// App -import { ViewController } from './controllers'; - -export class MyController {} diff --git a/packages/cli/src/generate/mocks/controller/app.controller.core-import.ts b/packages/cli/src/generate/mocks/controller/app.controller.core-import.ts deleted file mode 100644 index ae19148cfb..0000000000 --- a/packages/cli/src/generate/mocks/controller/app.controller.core-import.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 3p -import { something } from '@foal/core'; - -export class MyController {} diff --git a/packages/cli/src/generate/mocks/controller/app.controller.empty-property.ts b/packages/cli/src/generate/mocks/controller/app.controller.empty-property.ts deleted file mode 100644 index 65d111a3c6..0000000000 --- a/packages/cli/src/generate/mocks/controller/app.controller.empty-property.ts +++ /dev/null @@ -1,6 +0,0 @@ -// 3p -import {} from 'somewhere'; - -export class MyController { - subControllers = []; -} diff --git a/packages/cli/src/generate/mocks/controller/app.controller.empty-spaced-property.ts b/packages/cli/src/generate/mocks/controller/app.controller.empty-spaced-property.ts deleted file mode 100644 index 9c02d968a7..0000000000 --- a/packages/cli/src/generate/mocks/controller/app.controller.empty-spaced-property.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 3p -import {} from 'somewhere'; - -export class MyController { - subControllers = [ - - ]; -} diff --git a/packages/cli/src/generate/mocks/controller/app.controller.no-controller-import.ts b/packages/cli/src/generate/mocks/controller/app.controller.no-controller-import.ts deleted file mode 100644 index 34dee3c713..0000000000 --- a/packages/cli/src/generate/mocks/controller/app.controller.no-controller-import.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 3p -import { Something } from '@somewhere'; - -export class MyController {} diff --git a/packages/cli/src/generate/mocks/controller/app.controller.no-empty-property.ts b/packages/cli/src/generate/mocks/controller/app.controller.no-empty-property.ts deleted file mode 100644 index 8a6921dd94..0000000000 --- a/packages/cli/src/generate/mocks/controller/app.controller.no-empty-property.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 3p -import { controller } from '@foal/core'; - -export class MyController { - subControllers = [ - controller('/', MyController), - controller('/', MyController2) - ]; -} diff --git a/packages/cli/src/generate/mocks/controller/app.controller.no-import.ts b/packages/cli/src/generate/mocks/controller/app.controller.no-import.ts deleted file mode 100644 index 273000c0fb..0000000000 --- a/packages/cli/src/generate/mocks/controller/app.controller.no-import.ts +++ /dev/null @@ -1 +0,0 @@ -export class MyController {} diff --git a/packages/cli/src/generate/mocks/controller/app.controller.rest.ts b/packages/cli/src/generate/mocks/controller/app.controller.rest.ts deleted file mode 100644 index 65d111a3c6..0000000000 --- a/packages/cli/src/generate/mocks/controller/app.controller.rest.ts +++ /dev/null @@ -1,6 +0,0 @@ -// 3p -import {} from 'somewhere'; - -export class MyController { - subControllers = []; -} diff --git a/packages/cli/src/generate/mocks/rest-api/app.controller.controller-import.ts b/packages/cli/src/generate/mocks/rest-api/app.controller.controller-import.ts deleted file mode 100644 index 0708239bd0..0000000000 --- a/packages/cli/src/generate/mocks/rest-api/app.controller.controller-import.ts +++ /dev/null @@ -1,4 +0,0 @@ -// App -import { ViewController } from './controllers'; - -export class MyController {} diff --git a/packages/cli/src/generate/mocks/rest-api/app.controller.core-import.ts b/packages/cli/src/generate/mocks/rest-api/app.controller.core-import.ts deleted file mode 100644 index ae19148cfb..0000000000 --- a/packages/cli/src/generate/mocks/rest-api/app.controller.core-import.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 3p -import { something } from '@foal/core'; - -export class MyController {} diff --git a/packages/cli/src/generate/mocks/rest-api/app.controller.empty-property.ts b/packages/cli/src/generate/mocks/rest-api/app.controller.empty-property.ts deleted file mode 100644 index 65d111a3c6..0000000000 --- a/packages/cli/src/generate/mocks/rest-api/app.controller.empty-property.ts +++ /dev/null @@ -1,6 +0,0 @@ -// 3p -import {} from 'somewhere'; - -export class MyController { - subControllers = []; -} diff --git a/packages/cli/src/generate/mocks/rest-api/app.controller.empty-spaced-property.ts b/packages/cli/src/generate/mocks/rest-api/app.controller.empty-spaced-property.ts deleted file mode 100644 index 9c02d968a7..0000000000 --- a/packages/cli/src/generate/mocks/rest-api/app.controller.empty-spaced-property.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 3p -import {} from 'somewhere'; - -export class MyController { - subControllers = [ - - ]; -} diff --git a/packages/cli/src/generate/mocks/rest-api/app.controller.no-controller-import.ts b/packages/cli/src/generate/mocks/rest-api/app.controller.no-controller-import.ts deleted file mode 100644 index 34dee3c713..0000000000 --- a/packages/cli/src/generate/mocks/rest-api/app.controller.no-controller-import.ts +++ /dev/null @@ -1,4 +0,0 @@ -// 3p -import { Something } from '@somewhere'; - -export class MyController {} diff --git a/packages/cli/src/generate/mocks/rest-api/app.controller.no-empty-property.ts b/packages/cli/src/generate/mocks/rest-api/app.controller.no-empty-property.ts deleted file mode 100644 index 8a6921dd94..0000000000 --- a/packages/cli/src/generate/mocks/rest-api/app.controller.no-empty-property.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 3p -import { controller } from '@foal/core'; - -export class MyController { - subControllers = [ - controller('/', MyController), - controller('/', MyController2) - ]; -} diff --git a/packages/cli/src/generate/specs/app/package.json b/packages/cli/src/generate/specs/app/package.json index 24e5373106..18755f2fc9 100644 --- a/packages/cli/src/generate/specs/app/package.json +++ b/packages/cli/src/generate/specs/app/package.json @@ -35,7 +35,7 @@ "@foal/typeorm": "^1.0.0", "source-map-support": "^0.5.1", "sqlite3": "^4.0.0", - "typeorm": "^0.2.6" + "typeorm": "0.2.24" }, "devDependencies": { "@types/mocha": "^5.2.1", @@ -46,8 +46,8 @@ "supertest": "^3.3.0", "supervisor": "^0.12.0", "eslint": "^6.7.0", - "@typescript-eslint/eslint-plugin": "^2.7.0", - "@typescript-eslint/parser": "^2.7.0", + "@typescript-eslint/eslint-plugin": "~2.7.0", + "@typescript-eslint/parser": "~2.7.0", "typescript": "~3.5.3" } } diff --git a/packages/cli/src/generate/specs/app/package.mongodb.json b/packages/cli/src/generate/specs/app/package.mongodb.json index 1be88bcee2..46cd57ed86 100644 --- a/packages/cli/src/generate/specs/app/package.mongodb.json +++ b/packages/cli/src/generate/specs/app/package.mongodb.json @@ -41,8 +41,8 @@ "supertest": "^3.3.0", "supervisor": "^0.12.0", "eslint": "^6.7.0", - "@typescript-eslint/eslint-plugin": "^2.7.0", - "@typescript-eslint/parser": "^2.7.0", + "@typescript-eslint/eslint-plugin": "~2.7.0", + "@typescript-eslint/parser": "~2.7.0", "typescript": "~3.5.3" } } diff --git a/packages/cli/src/generate/specs/app/package.mongodb.yaml.json b/packages/cli/src/generate/specs/app/package.mongodb.yaml.json index f1caf7d05f..f60c59df30 100644 --- a/packages/cli/src/generate/specs/app/package.mongodb.yaml.json +++ b/packages/cli/src/generate/specs/app/package.mongodb.yaml.json @@ -42,8 +42,8 @@ "supertest": "^3.3.0", "supervisor": "^0.12.0", "eslint": "^6.7.0", - "@typescript-eslint/eslint-plugin": "^2.7.0", - "@typescript-eslint/parser": "^2.7.0", + "@typescript-eslint/eslint-plugin": "~2.7.0", + "@typescript-eslint/parser": "~2.7.0", "typescript": "~3.5.3" } } diff --git a/packages/cli/src/generate/specs/app/package.yaml.json b/packages/cli/src/generate/specs/app/package.yaml.json index 21aa656e38..df84fda641 100644 --- a/packages/cli/src/generate/specs/app/package.yaml.json +++ b/packages/cli/src/generate/specs/app/package.yaml.json @@ -35,7 +35,7 @@ "@foal/typeorm": "^1.0.0", "source-map-support": "^0.5.1", "sqlite3": "^4.0.0", - "typeorm": "^0.2.6", + "typeorm": "0.2.24", "yamljs": "^0.3.0" }, "devDependencies": { @@ -47,8 +47,8 @@ "supertest": "^3.3.0", "supervisor": "^0.12.0", "eslint": "^6.7.0", - "@typescript-eslint/eslint-plugin": "^2.7.0", - "@typescript-eslint/parser": "^2.7.0", + "@typescript-eslint/eslint-plugin": "~2.7.0", + "@typescript-eslint/parser": "~2.7.0", "typescript": "~3.5.3" } } diff --git a/packages/cli/src/generate/specs/app/src/scripts/create-user.mongodb.ts b/packages/cli/src/generate/specs/app/src/scripts/create-user.mongodb.ts index 997c6701a5..ccf41907cc 100644 --- a/packages/cli/src/generate/specs/app/src/scripts/create-user.mongodb.ts +++ b/packages/cli/src/generate/specs/app/src/scripts/create-user.mongodb.ts @@ -26,7 +26,7 @@ export async function main(/*args*/) { // await user.setPassword(args.password); const uri = Config.getOrThrow('mongodb.uri', 'string'); - connect(uri, { useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true }); + await connect(uri, { useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true }); try { console.log( @@ -35,6 +35,6 @@ export async function main(/*args*/) { } catch (error) { console.log(error.message); } finally { - disconnect(); + await disconnect(); } } diff --git a/packages/cli/src/generate/specs/app/src/scripts/create-user.ts b/packages/cli/src/generate/specs/app/src/scripts/create-user.ts index 80124466dd..615bcc7629 100644 --- a/packages/cli/src/generate/specs/app/src/scripts/create-user.ts +++ b/packages/cli/src/generate/specs/app/src/scripts/create-user.ts @@ -1,7 +1,7 @@ // 3p // import { Group, Permission } from '@foal/typeorm'; // import { isCommon } from '@foal/password'; -import { createConnection, getManager, /*getRepository*/ } from 'typeorm'; +import { createConnection, getConnection, getManager, /*getRepository*/ } from 'typeorm'; // App import { User } from '../app/entities'; @@ -55,5 +55,7 @@ export async function main(/*args*/) { ); } catch (error) { console.log(error.message); + } finally { + await getConnection().close(); } } diff --git a/packages/cli/src/generate/specs/controller/app.controller.no-empty-property.ts b/packages/cli/src/generate/specs/controller/api.controller.ts similarity index 65% rename from packages/cli/src/generate/specs/controller/app.controller.no-empty-property.ts rename to packages/cli/src/generate/specs/controller/api.controller.ts index 1a58d3d483..41f18dbce1 100644 --- a/packages/cli/src/generate/specs/controller/app.controller.no-empty-property.ts +++ b/packages/cli/src/generate/specs/controller/api.controller.ts @@ -1,8 +1,8 @@ // 3p +import { MyController, MyController2, TestFooBarController } from './api'; import { controller } from '@foal/core'; -import { TestFooBarController } from './controllers'; -export class MyController { +export class ApiController { subControllers = [ controller('/', MyController), controller('/', MyController2), diff --git a/packages/cli/src/generate/specs/controller/app.controller.controller-import.ts b/packages/cli/src/generate/specs/controller/app.controller.controller-import.ts deleted file mode 100644 index 1216d179ca..0000000000 --- a/packages/cli/src/generate/specs/controller/app.controller.controller-import.ts +++ /dev/null @@ -1,5 +0,0 @@ -// App -import { TestFooBarController, ViewController } from './controllers'; -import { controller } from '@foal/core'; - -export class MyController {} diff --git a/packages/cli/src/generate/specs/controller/app.controller.core-import.ts b/packages/cli/src/generate/specs/controller/app.controller.core-import.ts deleted file mode 100644 index 180e176d29..0000000000 --- a/packages/cli/src/generate/specs/controller/app.controller.core-import.ts +++ /dev/null @@ -1,5 +0,0 @@ -// 3p -import { controller, something } from '@foal/core'; -import { TestFooBarController } from './controllers'; - -export class MyController {} diff --git a/packages/cli/src/generate/specs/controller/app.controller.empty-property.ts b/packages/cli/src/generate/specs/controller/app.controller.empty-property.ts deleted file mode 100644 index 5f560d0043..0000000000 --- a/packages/cli/src/generate/specs/controller/app.controller.empty-property.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 3p -import {} from 'somewhere'; -import { TestFooBarController } from './controllers'; -import { controller } from '@foal/core'; - -export class MyController { - subControllers = [ - controller('/test-foo-bar', TestFooBarController) - ]; -} diff --git a/packages/cli/src/generate/specs/controller/app.controller.empty-spaced-property.ts b/packages/cli/src/generate/specs/controller/app.controller.empty-spaced-property.ts deleted file mode 100644 index 5f560d0043..0000000000 --- a/packages/cli/src/generate/specs/controller/app.controller.empty-spaced-property.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 3p -import {} from 'somewhere'; -import { TestFooBarController } from './controllers'; -import { controller } from '@foal/core'; - -export class MyController { - subControllers = [ - controller('/test-foo-bar', TestFooBarController) - ]; -} diff --git a/packages/cli/src/generate/specs/controller/app.controller.no-controller-import.ts b/packages/cli/src/generate/specs/controller/app.controller.no-controller-import.ts deleted file mode 100644 index 1b2bea7108..0000000000 --- a/packages/cli/src/generate/specs/controller/app.controller.no-controller-import.ts +++ /dev/null @@ -1,6 +0,0 @@ -// 3p -import { Something } from '@somewhere'; -import { TestFooBarController } from './controllers'; -import { controller } from '@foal/core'; - -export class MyController {} diff --git a/packages/cli/src/generate/specs/controller/app.controller.no-import.ts b/packages/cli/src/generate/specs/controller/app.controller.no-import.ts deleted file mode 100644 index 88ee2e4427..0000000000 --- a/packages/cli/src/generate/specs/controller/app.controller.no-import.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { TestFooBarController } from './controllers'; -import { controller } from '@foal/core'; - -export class MyController {} diff --git a/packages/cli/src/generate/specs/controller/app.controller.rest.ts b/packages/cli/src/generate/specs/controller/app.controller.rest.ts deleted file mode 100644 index 4c250d8151..0000000000 --- a/packages/cli/src/generate/specs/controller/app.controller.rest.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 3p -import {} from 'somewhere'; -import { TestFooBarController } from './controllers'; -import { controller } from '@foal/core'; - -export class MyController { - subControllers = [ - controller('/test-foo-bars', TestFooBarController) - ]; -} diff --git a/packages/cli/src/generate/specs/controller/app.controller.ts b/packages/cli/src/generate/specs/controller/app.controller.ts new file mode 100644 index 0000000000..787f9cf306 --- /dev/null +++ b/packages/cli/src/generate/specs/controller/app.controller.ts @@ -0,0 +1,11 @@ +// 3p +import { MyController, MyController2, TestFooBarController } from './controllers'; +import { controller } from '@foal/core'; + +export class AppController { + subControllers = [ + controller('/', MyController), + controller('/', MyController2), + controller('/test-foo-bar', TestFooBarController) + ]; +} diff --git a/packages/cli/src/generate/specs/controller/test-foo-bar.controller.graphql.ts b/packages/cli/src/generate/specs/controller/test-foo-bar.controller.graphql.ts deleted file mode 100644 index 7e7a1df05d..0000000000 --- a/packages/cli/src/generate/specs/controller/test-foo-bar.controller.graphql.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { GraphQLController } from '@foal/core'; - -export class TestFooBarController extends GraphQLController { - -} diff --git a/packages/cli/src/generate/specs/controller/test-foo-bar.controller.login.ts b/packages/cli/src/generate/specs/controller/test-foo-bar.controller.login.ts deleted file mode 100644 index 8a7aa26894..0000000000 --- a/packages/cli/src/generate/specs/controller/test-foo-bar.controller.login.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { LoginController } from '@foal/core'; - -export class TestFooBarController extends LoginController { - strategies = [ - // strategy('login', MyAuthenticator, mySchema) - ]; - - redirect = { - // failure: '/login', - // logout: '/login', - // success: '/home', - }; -} diff --git a/packages/cli/src/generate/specs/controller/test-foo-bar.controller.rest.ts b/packages/cli/src/generate/specs/controller/test-foo-bar.controller.rest.ts deleted file mode 100644 index e29d0737e7..0000000000 --- a/packages/cli/src/generate/specs/controller/test-foo-bar.controller.rest.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { dependency, RestController } from '@foal/core'; - -import { TestFooBarCollection } from '../services'; - -export class TestFooBarController extends RestController { - @dependency - collection: TestFooBarCollection; -} diff --git a/packages/cli/src/generate/specs/rest-api/app.controller.controller-import.ts b/packages/cli/src/generate/specs/rest-api/app.controller.controller-import.ts deleted file mode 100644 index 1216d179ca..0000000000 --- a/packages/cli/src/generate/specs/rest-api/app.controller.controller-import.ts +++ /dev/null @@ -1,5 +0,0 @@ -// App -import { TestFooBarController, ViewController } from './controllers'; -import { controller } from '@foal/core'; - -export class MyController {} diff --git a/packages/cli/src/generate/specs/rest-api/app.controller.core-import.ts b/packages/cli/src/generate/specs/rest-api/app.controller.core-import.ts deleted file mode 100644 index 180e176d29..0000000000 --- a/packages/cli/src/generate/specs/rest-api/app.controller.core-import.ts +++ /dev/null @@ -1,5 +0,0 @@ -// 3p -import { controller, something } from '@foal/core'; -import { TestFooBarController } from './controllers'; - -export class MyController {} diff --git a/packages/cli/src/generate/specs/rest-api/app.controller.empty-property.ts b/packages/cli/src/generate/specs/rest-api/app.controller.empty-property.ts deleted file mode 100644 index 4c250d8151..0000000000 --- a/packages/cli/src/generate/specs/rest-api/app.controller.empty-property.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 3p -import {} from 'somewhere'; -import { TestFooBarController } from './controllers'; -import { controller } from '@foal/core'; - -export class MyController { - subControllers = [ - controller('/test-foo-bars', TestFooBarController) - ]; -} diff --git a/packages/cli/src/generate/specs/rest-api/app.controller.empty-spaced-property.ts b/packages/cli/src/generate/specs/rest-api/app.controller.empty-spaced-property.ts deleted file mode 100644 index 4c250d8151..0000000000 --- a/packages/cli/src/generate/specs/rest-api/app.controller.empty-spaced-property.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 3p -import {} from 'somewhere'; -import { TestFooBarController } from './controllers'; -import { controller } from '@foal/core'; - -export class MyController { - subControllers = [ - controller('/test-foo-bars', TestFooBarController) - ]; -} diff --git a/packages/cli/src/generate/specs/rest-api/app.controller.no-controller-import.ts b/packages/cli/src/generate/specs/rest-api/app.controller.no-controller-import.ts deleted file mode 100644 index 1b2bea7108..0000000000 --- a/packages/cli/src/generate/specs/rest-api/app.controller.no-controller-import.ts +++ /dev/null @@ -1,6 +0,0 @@ -// 3p -import { Something } from '@somewhere'; -import { TestFooBarController } from './controllers'; -import { controller } from '@foal/core'; - -export class MyController {} diff --git a/packages/cli/src/generate/specs/rest-api/app.controller.no-empty-property.ts b/packages/cli/src/generate/specs/rest-api/app.controller.ts similarity index 64% rename from packages/cli/src/generate/specs/rest-api/app.controller.no-empty-property.ts rename to packages/cli/src/generate/specs/rest-api/app.controller.ts index 76a35ea36a..3efce8aa26 100644 --- a/packages/cli/src/generate/specs/rest-api/app.controller.no-empty-property.ts +++ b/packages/cli/src/generate/specs/rest-api/app.controller.ts @@ -1,8 +1,8 @@ // 3p +import { MyController, MyController2, TestFooBarController } from './controllers'; import { controller } from '@foal/core'; -import { TestFooBarController } from './controllers'; -export class MyController { +export class AppController { subControllers = [ controller('/', MyController), controller('/', MyController2), diff --git a/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.auth.ts b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.auth.ts new file mode 100644 index 0000000000..8ea606f658 --- /dev/null +++ b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.auth.ts @@ -0,0 +1,154 @@ +import { + ApiOperationDescription, ApiOperationId, ApiOperationSummary, ApiResponse, + ApiUseTag, Context, Delete, Get, HttpResponseCreated, + HttpResponseNoContent, HttpResponseNotFound, HttpResponseOK, Patch, Post, + Put, ValidateBody, ValidateParams, ValidateQuery +} from '@foal/core'; +import { getRepository } from 'typeorm'; + +import { TestFooBar, User } from '../entities'; + +const testFooBarSchema = { + additionalProperties: false, + properties: { + text: { type: 'string', maxLength: 255 }, + }, + required: [ 'text' ], + type: 'object', +}; + +@ApiUseTag('testFooBar') +export class TestFooBarController { + + @Get() + @ApiOperationId('findTestFooBars') + @ApiOperationSummary('Find testFooBars.') + @ApiOperationDescription( + 'The query parameters "skip" and "take" can be used for pagination. The first ' + + 'is the offset and the second is the number of elements to be returned.' + ) + @ApiResponse(400, { description: 'Invalid query parameters.' }) + @ApiResponse(200, { description: 'Returns a list of testFooBars.' }) + @ValidateQuery({ + properties: { + skip: { type: 'number' }, + take: { type: 'number' }, + }, + type: 'object', + }) + async findTestFooBars(ctx: Context) { + const testFooBars = await getRepository(TestFooBar).find({ + skip: ctx.request.query.skip, + take: ctx.request.query.take, + where: { + owner: ctx.user + } + }); + return new HttpResponseOK(testFooBars); + } + + @Get('/:testFooBarId') + @ApiOperationId('findTestFooBarById') + @ApiOperationSummary('Find a testFooBar by ID.') + @ApiResponse(404, { description: 'TestFooBar not found.' }) + @ApiResponse(200, { description: 'Returns the testFooBar.' }) + @ValidateParams({ properties: { testFooBarId: { type: 'number' } }, type: 'object' }) + async findTestFooBarById(ctx: Context) { + const testFooBar = await getRepository(TestFooBar).findOne({ + id: ctx.request.params.testFooBarId, + owner: ctx.user + }); + + if (!testFooBar) { + return new HttpResponseNotFound(); + } + + return new HttpResponseOK(testFooBar); + } + + @Post() + @ApiOperationId('createTestFooBar') + @ApiOperationSummary('Create a new testFooBar.') + @ApiResponse(400, { description: 'Invalid testFooBar.' }) + @ApiResponse(201, { description: 'TestFooBar successfully created. Returns the testFooBar.' }) + @ValidateBody(testFooBarSchema) + async createTestFooBar(ctx: Context) { + const testFooBar = await getRepository(TestFooBar).save({ + ...ctx.request.body, + owner: ctx.user + }); + return new HttpResponseCreated(testFooBar); + } + + @Patch('/:testFooBarId') + @ApiOperationId('modifyTestFooBar') + @ApiOperationSummary('Update/modify an existing testFooBar.') + @ApiResponse(400, { description: 'Invalid testFooBar.' }) + @ApiResponse(404, { description: 'TestFooBar not found.' }) + @ApiResponse(200, { description: 'TestFooBar successfully updated. Returns the testFooBar.' }) + @ValidateParams({ properties: { testFooBarId: { type: 'number' } }, type: 'object' }) + @ValidateBody({ ...testFooBarSchema, required: [] }) + async modifyTestFooBar(ctx: Context) { + const testFooBar = await getRepository(TestFooBar).findOne({ + id: ctx.request.params.testFooBarId, + owner: ctx.user + }); + + if (!testFooBar) { + return new HttpResponseNotFound(); + } + + Object.assign(testFooBar, ctx.request.body); + + await getRepository(TestFooBar).save(testFooBar); + + return new HttpResponseOK(testFooBar); + } + + @Put('/:testFooBarId') + @ApiOperationId('replaceTestFooBar') + @ApiOperationSummary('Update/replace an existing testFooBar.') + @ApiResponse(400, { description: 'Invalid testFooBar.' }) + @ApiResponse(404, { description: 'TestFooBar not found.' }) + @ApiResponse(200, { description: 'TestFooBar successfully updated. Returns the testFooBar.' }) + @ValidateParams({ properties: { testFooBarId: { type: 'number' } }, type: 'object' }) + @ValidateBody(testFooBarSchema) + async replaceTestFooBar(ctx: Context) { + const testFooBar = await getRepository(TestFooBar).findOne({ + id: ctx.request.params.testFooBarId, + owner: ctx.user + }); + + if (!testFooBar) { + return new HttpResponseNotFound(); + } + + Object.assign(testFooBar, ctx.request.body); + + await getRepository(TestFooBar).save(testFooBar); + + return new HttpResponseOK(testFooBar); + } + + @Delete('/:testFooBarId') + @ApiOperationId('deleteTestFooBar') + @ApiOperationSummary('Delete a testFooBar.') + @ApiResponse(404, { description: 'TestFooBar not found.' }) + @ApiResponse(204, { description: 'TestFooBar successfully deleted.' }) + @ValidateParams({ properties: { testFooBarId: { type: 'number' } }, type: 'object' }) + async deleteTestFooBar(ctx: Context) { + const testFooBar = await getRepository(TestFooBar).findOne({ + id: ctx.request.params.testFooBarId, + owner: ctx.user + }); + + if (!testFooBar) { + return new HttpResponseNotFound(); + } + + await getRepository(TestFooBar).delete(ctx.request.params.testFooBarId); + + return new HttpResponseNoContent(); + } + +} diff --git a/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.current-dir.auth.ts b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.current-dir.auth.ts new file mode 100644 index 0000000000..8dedd44efc --- /dev/null +++ b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.current-dir.auth.ts @@ -0,0 +1,155 @@ +import { + ApiOperationDescription, ApiOperationId, ApiOperationSummary, ApiResponse, + ApiUseTag, Context, Delete, Get, HttpResponseCreated, + HttpResponseNoContent, HttpResponseNotFound, HttpResponseOK, Patch, Post, + Put, ValidateBody, ValidateParams, ValidateQuery +} from '@foal/core'; +import { getRepository } from 'typeorm'; + +import { TestFooBar } from './test-foo-bar.entity'; +import { User } from './user.entity'; + +const testFooBarSchema = { + additionalProperties: false, + properties: { + text: { type: 'string', maxLength: 255 }, + }, + required: [ 'text' ], + type: 'object', +}; + +@ApiUseTag('testFooBar') +export class TestFooBarController { + + @Get() + @ApiOperationId('findTestFooBars') + @ApiOperationSummary('Find testFooBars.') + @ApiOperationDescription( + 'The query parameters "skip" and "take" can be used for pagination. The first ' + + 'is the offset and the second is the number of elements to be returned.' + ) + @ApiResponse(400, { description: 'Invalid query parameters.' }) + @ApiResponse(200, { description: 'Returns a list of testFooBars.' }) + @ValidateQuery({ + properties: { + skip: { type: 'number' }, + take: { type: 'number' }, + }, + type: 'object', + }) + async findTestFooBars(ctx: Context) { + const testFooBars = await getRepository(TestFooBar).find({ + skip: ctx.request.query.skip, + take: ctx.request.query.take, + where: { + owner: ctx.user + } + }); + return new HttpResponseOK(testFooBars); + } + + @Get('/:testFooBarId') + @ApiOperationId('findTestFooBarById') + @ApiOperationSummary('Find a testFooBar by ID.') + @ApiResponse(404, { description: 'TestFooBar not found.' }) + @ApiResponse(200, { description: 'Returns the testFooBar.' }) + @ValidateParams({ properties: { testFooBarId: { type: 'number' } }, type: 'object' }) + async findTestFooBarById(ctx: Context) { + const testFooBar = await getRepository(TestFooBar).findOne({ + id: ctx.request.params.testFooBarId, + owner: ctx.user + }); + + if (!testFooBar) { + return new HttpResponseNotFound(); + } + + return new HttpResponseOK(testFooBar); + } + + @Post() + @ApiOperationId('createTestFooBar') + @ApiOperationSummary('Create a new testFooBar.') + @ApiResponse(400, { description: 'Invalid testFooBar.' }) + @ApiResponse(201, { description: 'TestFooBar successfully created. Returns the testFooBar.' }) + @ValidateBody(testFooBarSchema) + async createTestFooBar(ctx: Context) { + const testFooBar = await getRepository(TestFooBar).save({ + ...ctx.request.body, + owner: ctx.user + }); + return new HttpResponseCreated(testFooBar); + } + + @Patch('/:testFooBarId') + @ApiOperationId('modifyTestFooBar') + @ApiOperationSummary('Update/modify an existing testFooBar.') + @ApiResponse(400, { description: 'Invalid testFooBar.' }) + @ApiResponse(404, { description: 'TestFooBar not found.' }) + @ApiResponse(200, { description: 'TestFooBar successfully updated. Returns the testFooBar.' }) + @ValidateParams({ properties: { testFooBarId: { type: 'number' } }, type: 'object' }) + @ValidateBody({ ...testFooBarSchema, required: [] }) + async modifyTestFooBar(ctx: Context) { + const testFooBar = await getRepository(TestFooBar).findOne({ + id: ctx.request.params.testFooBarId, + owner: ctx.user + }); + + if (!testFooBar) { + return new HttpResponseNotFound(); + } + + Object.assign(testFooBar, ctx.request.body); + + await getRepository(TestFooBar).save(testFooBar); + + return new HttpResponseOK(testFooBar); + } + + @Put('/:testFooBarId') + @ApiOperationId('replaceTestFooBar') + @ApiOperationSummary('Update/replace an existing testFooBar.') + @ApiResponse(400, { description: 'Invalid testFooBar.' }) + @ApiResponse(404, { description: 'TestFooBar not found.' }) + @ApiResponse(200, { description: 'TestFooBar successfully updated. Returns the testFooBar.' }) + @ValidateParams({ properties: { testFooBarId: { type: 'number' } }, type: 'object' }) + @ValidateBody(testFooBarSchema) + async replaceTestFooBar(ctx: Context) { + const testFooBar = await getRepository(TestFooBar).findOne({ + id: ctx.request.params.testFooBarId, + owner: ctx.user + }); + + if (!testFooBar) { + return new HttpResponseNotFound(); + } + + Object.assign(testFooBar, ctx.request.body); + + await getRepository(TestFooBar).save(testFooBar); + + return new HttpResponseOK(testFooBar); + } + + @Delete('/:testFooBarId') + @ApiOperationId('deleteTestFooBar') + @ApiOperationSummary('Delete a testFooBar.') + @ApiResponse(404, { description: 'TestFooBar not found.' }) + @ApiResponse(204, { description: 'TestFooBar successfully deleted.' }) + @ValidateParams({ properties: { testFooBarId: { type: 'number' } }, type: 'object' }) + async deleteTestFooBar(ctx: Context) { + const testFooBar = await getRepository(TestFooBar).findOne({ + id: ctx.request.params.testFooBarId, + owner: ctx.user + }); + + if (!testFooBar) { + return new HttpResponseNotFound(); + } + + await getRepository(TestFooBar).delete(ctx.request.params.testFooBarId); + + return new HttpResponseNoContent(); + } + +} diff --git a/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.auth.ts b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.auth.ts new file mode 100644 index 0000000000..df3512e375 --- /dev/null +++ b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.auth.ts @@ -0,0 +1,469 @@ +// std +import { notStrictEqual, ok, strictEqual } from 'assert'; + +// 3p +import { + Context, createController, getHttpMethod, getPath, + isHttpResponseCreated, isHttpResponseNoContent, + isHttpResponseNotFound, isHttpResponseOK +} from '@foal/core'; +import { createConnection, getConnection, getRepository } from 'typeorm'; + +// App +import { TestFooBar, User } from '../entities'; +import { TestFooBarController } from './test-foo-bar.controller'; + +describe('TestFooBarController', () => { + + let controller: TestFooBarController; + let testFooBar0: TestFooBar; + let testFooBar1: TestFooBar; + let testFooBar2: TestFooBar; + let user1: User; + let user2: User; + + before(() => createConnection()); + + after(() => getConnection().close()); + + beforeEach(async () => { + controller = createController(TestFooBarController); + + const testFooBarRepository = getRepository(TestFooBar); + const userRepository = getRepository(User); + + await testFooBarRepository.clear(); + await userRepository.clear(); + + [ user1, user2 ] = await userRepository.save([ + {}, + {}, + ]); + + [ testFooBar0, testFooBar1, testFooBar2 ] = await testFooBarRepository.save([ + { + owner: user1, + text: 'TestFooBar 0', + }, + { + owner: user2, + text: 'TestFooBar 1', + }, + { + owner: user2, + text: 'TestFooBar 2', + }, + ]); + }); + + describe('has a "findTestFooBars" method that', () => { + + it('should handle requests at GET /.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'findTestFooBars'), 'GET'); + strictEqual(getPath(TestFooBarController, 'findTestFooBars'), undefined); + }); + + it('should return an HttpResponseOK object with the testFooBar list.', async () => { + const ctx = new Context({ query: {} }); + ctx.user = user2; + const response = await controller.findTestFooBars(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + if (!Array.isArray(response.body)) { + throw new Error('The response body should be an array of testFooBars.'); + } + + strictEqual(response.body.length, 2); + ok(response.body.find(testFooBar => testFooBar.text === testFooBar1.text)); + ok(response.body.find(testFooBar => testFooBar.text === testFooBar2.text)); + }); + + it('should support pagination', async () => { + const testFooBar3 = await getRepository(TestFooBar).save({ + owner: user2, + text: 'TestFooBar 3', + }); + + let ctx = new Context({ + query: { + take: 2 + } + }); + ctx.user = user2; + let response = await controller.findTestFooBars(ctx); + + strictEqual(response.body.length, 2); + ok(response.body.find(testFooBar => testFooBar.id === testFooBar1.id)); + ok(response.body.find(testFooBar => testFooBar.id === testFooBar2.id)); + ok(!response.body.find(testFooBar => testFooBar.id === testFooBar3.id)); + + ctx = new Context({ + query: { + skip: 1 + } + }); + ctx.user = user2; + response = await controller.findTestFooBars(ctx); + + strictEqual(response.body.length, 2); + ok(!response.body.find(testFooBar => testFooBar.id === testFooBar1.id)); + ok(response.body.find(testFooBar => testFooBar.id === testFooBar2.id)); + ok(response.body.find(testFooBar => testFooBar.id === testFooBar3.id)); + }); + + }); + + describe('has a "findTestFooBarById" method that', () => { + + it('should handle requests at GET /:testFooBarId.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'findTestFooBarById'), 'GET'); + strictEqual(getPath(TestFooBarController, 'findTestFooBarById'), '/:testFooBarId'); + }); + + it('should return an HttpResponseOK object if the testFooBar was found.', async () => { + const ctx = new Context({ + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + const response = await controller.findTestFooBarById(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + strictEqual(response.body.id, testFooBar2.id); + strictEqual(response.body.text, testFooBar2.text); + }); + + it('should return an HttpResponseNotFound object if the testFooBar was not found.', async () => { + const ctx = new Context({ + params: { + testFooBarId: -1 + } + }); + ctx.user = user2; + const response = await controller.findTestFooBarById(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound object if the testFooBar belongs to another user.', async () => { + const ctx = new Context({ + params: { + testFooBarId: testFooBar0.id + } + }); + ctx.user = user2; + const response = await controller.findTestFooBarById(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "createTestFooBar" method that', () => { + + it('should handle requests at POST /.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'createTestFooBar'), 'POST'); + strictEqual(getPath(TestFooBarController, 'createTestFooBar'), undefined); + }); + + it('should create the testFooBar in the database and return it through ' + + 'an HttpResponseCreated object.', async () => { + const ctx = new Context({ + body: { + text: 'TestFooBar 3', + } + }); + ctx.user = user2; + const response = await controller.createTestFooBar(ctx); + + if (!isHttpResponseCreated(response)) { + throw new Error('The returned value should be an HttpResponseCreated object.'); + } + + const testFooBar = await getRepository(TestFooBar).findOne({ + relations: [ 'owner' ], + where: { text: 'TestFooBar 3' }, + }); + + if (!testFooBar) { + throw new Error('No testFooBar 3 was found in the database.'); + } + + strictEqual(testFooBar.text, 'TestFooBar 3'); + strictEqual(testFooBar.owner.id, user2.id); + + strictEqual(response.body.id, testFooBar.id); + strictEqual(response.body.text, testFooBar.text); + }); + + }); + + describe('has a "modifyTestFooBar" method that', () => { + + it('should handle requests at PATCH /:testFooBarId.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'modifyTestFooBar'), 'PATCH'); + strictEqual(getPath(TestFooBarController, 'modifyTestFooBar'), '/:testFooBarId'); + }); + + it('should update the testFooBar in the database and return it through an HttpResponseOK object.', async () => { + const ctx = new Context({ + body: { + text: 'TestFooBar 2 (version 2)', + }, + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + const response = await controller.modifyTestFooBar(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar2.id); + + if (!testFooBar) { + throw new Error(); + } + + strictEqual(testFooBar.text, 'TestFooBar 2 (version 2)'); + + strictEqual(response.body.id, testFooBar.id); + strictEqual(response.body.text, testFooBar.text); + }); + + it('should not update the other testFooBars.', async () => { + const ctx = new Context({ + body: { + text: 'TestFooBar 2 (version 2)', + }, + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + await controller.modifyTestFooBar(ctx); + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar1.id); + + if (!testFooBar) { + throw new Error(); + } + + notStrictEqual(testFooBar.text, 'TestFooBar 2 (version 2)'); + }); + + it('should return an HttpResponseNotFound if the object does not exist.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + testFooBarId: -1 + } + }); + ctx.user = user2; + const response = await controller.modifyTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the object belongs to another user.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + testFooBarId: testFooBar0.id + } + }); + ctx.user = user2; + const response = await controller.modifyTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "replaceTestFooBar" method that', () => { + + it('should handle requests at PUT /:testFooBarId.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'replaceTestFooBar'), 'PUT'); + strictEqual(getPath(TestFooBarController, 'replaceTestFooBar'), '/:testFooBarId'); + }); + + it('should update the testFooBar in the database and return it through an HttpResponseOK object.', async () => { + const ctx = new Context({ + body: { + text: 'TestFooBar 2 (version 2)', + }, + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + const response = await controller.replaceTestFooBar(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar2.id); + + if (!testFooBar) { + throw new Error(); + } + + strictEqual(testFooBar.text, 'TestFooBar 2 (version 2)'); + + strictEqual(response.body.id, testFooBar.id); + strictEqual(response.body.text, testFooBar.text); + }); + + it('should not update the other testFooBars.', async () => { + const ctx = new Context({ + body: { + text: 'TestFooBar 2 (version 2)', + }, + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + await controller.replaceTestFooBar(ctx); + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar1.id); + + if (!testFooBar) { + throw new Error(); + } + + notStrictEqual(testFooBar.text, 'TestFooBar 2 (version 2)'); + }); + + it('should return an HttpResponseNotFound if the object does not exist.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + testFooBarId: -1 + } + }); + ctx.user = user2; + const response = await controller.replaceTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the object belongs to another user.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + testFooBarId: testFooBar0.id + } + }); + ctx.user = user2; + const response = await controller.replaceTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "deleteTestFooBar" method that', () => { + + it('should handle requests at DELETE /:testFooBarId.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'deleteTestFooBar'), 'DELETE'); + strictEqual(getPath(TestFooBarController, 'deleteTestFooBar'), '/:testFooBarId'); + }); + + it('should delete the testFooBar and return an HttpResponseNoContent object.', async () => { + const ctx = new Context({ + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + const response = await controller.deleteTestFooBar(ctx); + + if (!isHttpResponseNoContent(response)) { + throw new Error('The returned value should be an HttpResponseNoContent object.'); + } + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar2.id); + + strictEqual(testFooBar, undefined); + }); + + it('should not delete the other testFooBars.', async () => { + const ctx = new Context({ + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + const response = await controller.deleteTestFooBar(ctx); + + if (!isHttpResponseNoContent(response)) { + throw new Error('The returned value should be an HttpResponseNoContent object.'); + } + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar1.id); + + notStrictEqual(testFooBar, undefined); + }); + + it('should return an HttpResponseNotFound if the testFooBar was not found.', async () => { + const ctx = new Context({ + params: { + testFooBarId: -1 + } + }); + ctx.user = user2; + const response = await controller.deleteTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the testFooBar belongs to another user.', async () => { + const ctx = new Context({ + params: { + testFooBarId: testFooBar0.id + } + }); + ctx.user = user2; + const response = await controller.deleteTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + +}); diff --git a/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.current-dir.auth.ts b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.current-dir.auth.ts new file mode 100644 index 0000000000..4f6f07a056 --- /dev/null +++ b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.current-dir.auth.ts @@ -0,0 +1,470 @@ +// std +import { notStrictEqual, ok, strictEqual } from 'assert'; + +// 3p +import { + Context, createController, getHttpMethod, getPath, + isHttpResponseCreated, isHttpResponseNoContent, + isHttpResponseNotFound, isHttpResponseOK +} from '@foal/core'; +import { createConnection, getConnection, getRepository } from 'typeorm'; + +// App +import { TestFooBarController } from './test-foo-bar.controller'; +import { TestFooBar } from './test-foo-bar.entity'; +import { User } from './user.entity'; + +describe('TestFooBarController', () => { + + let controller: TestFooBarController; + let testFooBar0: TestFooBar; + let testFooBar1: TestFooBar; + let testFooBar2: TestFooBar; + let user1: User; + let user2: User; + + before(() => createConnection()); + + after(() => getConnection().close()); + + beforeEach(async () => { + controller = createController(TestFooBarController); + + const testFooBarRepository = getRepository(TestFooBar); + const userRepository = getRepository(User); + + await testFooBarRepository.clear(); + await userRepository.clear(); + + [ user1, user2 ] = await userRepository.save([ + {}, + {}, + ]); + + [ testFooBar0, testFooBar1, testFooBar2 ] = await testFooBarRepository.save([ + { + owner: user1, + text: 'TestFooBar 0', + }, + { + owner: user2, + text: 'TestFooBar 1', + }, + { + owner: user2, + text: 'TestFooBar 2', + }, + ]); + }); + + describe('has a "findTestFooBars" method that', () => { + + it('should handle requests at GET /.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'findTestFooBars'), 'GET'); + strictEqual(getPath(TestFooBarController, 'findTestFooBars'), undefined); + }); + + it('should return an HttpResponseOK object with the testFooBar list.', async () => { + const ctx = new Context({ query: {} }); + ctx.user = user2; + const response = await controller.findTestFooBars(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + if (!Array.isArray(response.body)) { + throw new Error('The response body should be an array of testFooBars.'); + } + + strictEqual(response.body.length, 2); + ok(response.body.find(testFooBar => testFooBar.text === testFooBar1.text)); + ok(response.body.find(testFooBar => testFooBar.text === testFooBar2.text)); + }); + + it('should support pagination', async () => { + const testFooBar3 = await getRepository(TestFooBar).save({ + owner: user2, + text: 'TestFooBar 3', + }); + + let ctx = new Context({ + query: { + take: 2 + } + }); + ctx.user = user2; + let response = await controller.findTestFooBars(ctx); + + strictEqual(response.body.length, 2); + ok(response.body.find(testFooBar => testFooBar.id === testFooBar1.id)); + ok(response.body.find(testFooBar => testFooBar.id === testFooBar2.id)); + ok(!response.body.find(testFooBar => testFooBar.id === testFooBar3.id)); + + ctx = new Context({ + query: { + skip: 1 + } + }); + ctx.user = user2; + response = await controller.findTestFooBars(ctx); + + strictEqual(response.body.length, 2); + ok(!response.body.find(testFooBar => testFooBar.id === testFooBar1.id)); + ok(response.body.find(testFooBar => testFooBar.id === testFooBar2.id)); + ok(response.body.find(testFooBar => testFooBar.id === testFooBar3.id)); + }); + + }); + + describe('has a "findTestFooBarById" method that', () => { + + it('should handle requests at GET /:testFooBarId.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'findTestFooBarById'), 'GET'); + strictEqual(getPath(TestFooBarController, 'findTestFooBarById'), '/:testFooBarId'); + }); + + it('should return an HttpResponseOK object if the testFooBar was found.', async () => { + const ctx = new Context({ + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + const response = await controller.findTestFooBarById(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + strictEqual(response.body.id, testFooBar2.id); + strictEqual(response.body.text, testFooBar2.text); + }); + + it('should return an HttpResponseNotFound object if the testFooBar was not found.', async () => { + const ctx = new Context({ + params: { + testFooBarId: -1 + } + }); + ctx.user = user2; + const response = await controller.findTestFooBarById(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound object if the testFooBar belongs to another user.', async () => { + const ctx = new Context({ + params: { + testFooBarId: testFooBar0.id + } + }); + ctx.user = user2; + const response = await controller.findTestFooBarById(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "createTestFooBar" method that', () => { + + it('should handle requests at POST /.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'createTestFooBar'), 'POST'); + strictEqual(getPath(TestFooBarController, 'createTestFooBar'), undefined); + }); + + it('should create the testFooBar in the database and return it through ' + + 'an HttpResponseCreated object.', async () => { + const ctx = new Context({ + body: { + text: 'TestFooBar 3', + } + }); + ctx.user = user2; + const response = await controller.createTestFooBar(ctx); + + if (!isHttpResponseCreated(response)) { + throw new Error('The returned value should be an HttpResponseCreated object.'); + } + + const testFooBar = await getRepository(TestFooBar).findOne({ + relations: [ 'owner' ], + where: { text: 'TestFooBar 3' }, + }); + + if (!testFooBar) { + throw new Error('No testFooBar 3 was found in the database.'); + } + + strictEqual(testFooBar.text, 'TestFooBar 3'); + strictEqual(testFooBar.owner.id, user2.id); + + strictEqual(response.body.id, testFooBar.id); + strictEqual(response.body.text, testFooBar.text); + }); + + }); + + describe('has a "modifyTestFooBar" method that', () => { + + it('should handle requests at PATCH /:testFooBarId.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'modifyTestFooBar'), 'PATCH'); + strictEqual(getPath(TestFooBarController, 'modifyTestFooBar'), '/:testFooBarId'); + }); + + it('should update the testFooBar in the database and return it through an HttpResponseOK object.', async () => { + const ctx = new Context({ + body: { + text: 'TestFooBar 2 (version 2)', + }, + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + const response = await controller.modifyTestFooBar(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar2.id); + + if (!testFooBar) { + throw new Error(); + } + + strictEqual(testFooBar.text, 'TestFooBar 2 (version 2)'); + + strictEqual(response.body.id, testFooBar.id); + strictEqual(response.body.text, testFooBar.text); + }); + + it('should not update the other testFooBars.', async () => { + const ctx = new Context({ + body: { + text: 'TestFooBar 2 (version 2)', + }, + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + await controller.modifyTestFooBar(ctx); + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar1.id); + + if (!testFooBar) { + throw new Error(); + } + + notStrictEqual(testFooBar.text, 'TestFooBar 2 (version 2)'); + }); + + it('should return an HttpResponseNotFound if the object does not exist.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + testFooBarId: -1 + } + }); + ctx.user = user2; + const response = await controller.modifyTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the object belongs to another user.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + testFooBarId: testFooBar0.id + } + }); + ctx.user = user2; + const response = await controller.modifyTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "replaceTestFooBar" method that', () => { + + it('should handle requests at PUT /:testFooBarId.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'replaceTestFooBar'), 'PUT'); + strictEqual(getPath(TestFooBarController, 'replaceTestFooBar'), '/:testFooBarId'); + }); + + it('should update the testFooBar in the database and return it through an HttpResponseOK object.', async () => { + const ctx = new Context({ + body: { + text: 'TestFooBar 2 (version 2)', + }, + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + const response = await controller.replaceTestFooBar(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar2.id); + + if (!testFooBar) { + throw new Error(); + } + + strictEqual(testFooBar.text, 'TestFooBar 2 (version 2)'); + + strictEqual(response.body.id, testFooBar.id); + strictEqual(response.body.text, testFooBar.text); + }); + + it('should not update the other testFooBars.', async () => { + const ctx = new Context({ + body: { + text: 'TestFooBar 2 (version 2)', + }, + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + await controller.replaceTestFooBar(ctx); + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar1.id); + + if (!testFooBar) { + throw new Error(); + } + + notStrictEqual(testFooBar.text, 'TestFooBar 2 (version 2)'); + }); + + it('should return an HttpResponseNotFound if the object does not exist.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + testFooBarId: -1 + } + }); + ctx.user = user2; + const response = await controller.replaceTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the object belongs to another user.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + testFooBarId: testFooBar0.id + } + }); + ctx.user = user2; + const response = await controller.replaceTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "deleteTestFooBar" method that', () => { + + it('should handle requests at DELETE /:testFooBarId.', () => { + strictEqual(getHttpMethod(TestFooBarController, 'deleteTestFooBar'), 'DELETE'); + strictEqual(getPath(TestFooBarController, 'deleteTestFooBar'), '/:testFooBarId'); + }); + + it('should delete the testFooBar and return an HttpResponseNoContent object.', async () => { + const ctx = new Context({ + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + const response = await controller.deleteTestFooBar(ctx); + + if (!isHttpResponseNoContent(response)) { + throw new Error('The returned value should be an HttpResponseNoContent object.'); + } + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar2.id); + + strictEqual(testFooBar, undefined); + }); + + it('should not delete the other testFooBars.', async () => { + const ctx = new Context({ + params: { + testFooBarId: testFooBar2.id + } + }); + ctx.user = user2; + const response = await controller.deleteTestFooBar(ctx); + + if (!isHttpResponseNoContent(response)) { + throw new Error('The returned value should be an HttpResponseNoContent object.'); + } + + const testFooBar = await getRepository(TestFooBar).findOne(testFooBar1.id); + + notStrictEqual(testFooBar, undefined); + }); + + it('should return an HttpResponseNotFound if the testFooBar was not found.', async () => { + const ctx = new Context({ + params: { + testFooBarId: -1 + } + }); + ctx.user = user2; + const response = await controller.deleteTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the testFooBar belongs to another user.', async () => { + const ctx = new Context({ + params: { + testFooBarId: testFooBar0.id + } + }); + ctx.user = user2; + const response = await controller.deleteTestFooBar(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + +}); diff --git a/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.current-dir.ts b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.current-dir.ts index 8fe3ed7340..8b9f0790a2 100644 --- a/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.current-dir.ts +++ b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.current-dir.ts @@ -351,7 +351,7 @@ describe('TestFooBarController', () => { notStrictEqual(testFooBar, undefined); }); - it('should return an HttpResponseNotFound if the testFooBar was not fond.', async () => { + it('should return an HttpResponseNotFound if the testFooBar was not found.', async () => { const ctx = new Context({ params: { testFooBarId: -1 diff --git a/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.ts b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.ts index ea85e2f578..d82a90b23d 100644 --- a/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.ts +++ b/packages/cli/src/generate/specs/rest-api/test-foo-bar.controller.spec.ts @@ -351,7 +351,7 @@ describe('TestFooBarController', () => { notStrictEqual(testFooBar, undefined); }); - it('should return an HttpResponseNotFound if the testFooBar was not fond.', async () => { + it('should return an HttpResponseNotFound if the testFooBar was not found.', async () => { const ctx = new Context({ params: { testFooBarId: -1 diff --git a/packages/cli/src/generate/specs/rest-api/test-foo-bar.entity.auth.ts b/packages/cli/src/generate/specs/rest-api/test-foo-bar.entity.auth.ts new file mode 100644 index 0000000000..c5fcbc9fc4 --- /dev/null +++ b/packages/cli/src/generate/specs/rest-api/test-foo-bar.entity.auth.ts @@ -0,0 +1,17 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; + +import { User } from './user.entity'; + +@Entity() +export class TestFooBar { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + text: string; + + @ManyToOne(() => User, { nullable: false }) + owner: User; + +} diff --git a/packages/cli/src/generate/specs/script/test-foo-bar.mongoose.ts b/packages/cli/src/generate/specs/script/test-foo-bar.mongoose.ts new file mode 100644 index 0000000000..f4f7be9e3a --- /dev/null +++ b/packages/cli/src/generate/specs/script/test-foo-bar.mongoose.ts @@ -0,0 +1,26 @@ +// 3p +import { Config } from '@foal/core'; +import { connect, disconnect } from 'mongoose'; + +export const schema = { + additionalProperties: false, + properties: { + /* To complete */ + }, + required: [ /* To complete */ ], + type: 'object', +}; + +export async function main(args: any) { + const uri = Config.getOrThrow('mongodb.uri', 'string'); + await connect(uri, { useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true }); + + try { + // Do something. + + } catch (error) { + console.error(error); + } finally { + await disconnect(); + } +} diff --git a/packages/cli/src/generate/specs/script/test-foo-bar.ts b/packages/cli/src/generate/specs/script/test-foo-bar.ts index 803eb73366..d645d0cc6d 100644 --- a/packages/cli/src/generate/specs/script/test-foo-bar.ts +++ b/packages/cli/src/generate/specs/script/test-foo-bar.ts @@ -11,7 +11,14 @@ export const schema = { }; export async function main(args: any) { - await createConnection(); + const connection = await createConnection(); - // Do something. + try { + // Do something. + + } catch (error) { + console.error(error); + } finally { + await connection.close(); + } } diff --git a/packages/cli/src/generate/templates/app/package.json b/packages/cli/src/generate/templates/app/package.json index c5fecfa7c9..c624463531 100644 --- a/packages/cli/src/generate/templates/app/package.json +++ b/packages/cli/src/generate/templates/app/package.json @@ -35,7 +35,7 @@ "@foal/typeorm": "^1.0.0", "source-map-support": "^0.5.1", "sqlite3": "^4.0.0", - "typeorm": "^0.2.6" + "typeorm": "0.2.24" }, "devDependencies": { "@types/mocha": "^5.2.1", @@ -46,8 +46,8 @@ "supertest": "^3.3.0", "supervisor": "^0.12.0", "eslint": "^6.7.0", - "@typescript-eslint/eslint-plugin": "^2.7.0", - "@typescript-eslint/parser": "^2.7.0", + "@typescript-eslint/eslint-plugin": "~2.7.0", + "@typescript-eslint/parser": "~2.7.0", "typescript": "~3.5.3" } } diff --git a/packages/cli/src/generate/templates/app/package.mongodb.json b/packages/cli/src/generate/templates/app/package.mongodb.json index ab26b2ad6e..653fe6bd7e 100644 --- a/packages/cli/src/generate/templates/app/package.mongodb.json +++ b/packages/cli/src/generate/templates/app/package.mongodb.json @@ -41,8 +41,8 @@ "supertest": "^3.3.0", "supervisor": "^0.12.0", "eslint": "^6.7.0", - "@typescript-eslint/eslint-plugin": "^2.7.0", - "@typescript-eslint/parser": "^2.7.0", + "@typescript-eslint/eslint-plugin": "~2.7.0", + "@typescript-eslint/parser": "~2.7.0", "typescript": "~3.5.3" } } diff --git a/packages/cli/src/generate/templates/app/package.mongodb.yaml.json b/packages/cli/src/generate/templates/app/package.mongodb.yaml.json index 761d65435f..7f0a129556 100644 --- a/packages/cli/src/generate/templates/app/package.mongodb.yaml.json +++ b/packages/cli/src/generate/templates/app/package.mongodb.yaml.json @@ -42,8 +42,8 @@ "supertest": "^3.3.0", "supervisor": "^0.12.0", "eslint": "^6.7.0", - "@typescript-eslint/eslint-plugin": "^2.7.0", - "@typescript-eslint/parser": "^2.7.0", + "@typescript-eslint/eslint-plugin": "~2.7.0", + "@typescript-eslint/parser": "~2.7.0", "typescript": "~3.5.3" } } diff --git a/packages/cli/src/generate/templates/app/package.yaml.json b/packages/cli/src/generate/templates/app/package.yaml.json index 41b6227317..81cad78807 100644 --- a/packages/cli/src/generate/templates/app/package.yaml.json +++ b/packages/cli/src/generate/templates/app/package.yaml.json @@ -35,7 +35,7 @@ "@foal/typeorm": "^1.0.0", "source-map-support": "^0.5.1", "sqlite3": "^4.0.0", - "typeorm": "^0.2.6", + "typeorm": "0.2.24", "yamljs": "^0.3.0" }, "devDependencies": { @@ -47,8 +47,8 @@ "supertest": "^3.3.0", "supervisor": "^0.12.0", "eslint": "^6.7.0", - "@typescript-eslint/eslint-plugin": "^2.7.0", - "@typescript-eslint/parser": "^2.7.0", + "@typescript-eslint/eslint-plugin": "~2.7.0", + "@typescript-eslint/parser": "~2.7.0", "typescript": "~3.5.3" } } diff --git a/packages/cli/src/generate/templates/app/src/scripts/create-user.mongodb.ts b/packages/cli/src/generate/templates/app/src/scripts/create-user.mongodb.ts index 997c6701a5..ccf41907cc 100644 --- a/packages/cli/src/generate/templates/app/src/scripts/create-user.mongodb.ts +++ b/packages/cli/src/generate/templates/app/src/scripts/create-user.mongodb.ts @@ -26,7 +26,7 @@ export async function main(/*args*/) { // await user.setPassword(args.password); const uri = Config.getOrThrow('mongodb.uri', 'string'); - connect(uri, { useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true }); + await connect(uri, { useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true }); try { console.log( @@ -35,6 +35,6 @@ export async function main(/*args*/) { } catch (error) { console.log(error.message); } finally { - disconnect(); + await disconnect(); } } diff --git a/packages/cli/src/generate/templates/app/src/scripts/create-user.ts b/packages/cli/src/generate/templates/app/src/scripts/create-user.ts index 80124466dd..615bcc7629 100644 --- a/packages/cli/src/generate/templates/app/src/scripts/create-user.ts +++ b/packages/cli/src/generate/templates/app/src/scripts/create-user.ts @@ -1,7 +1,7 @@ // 3p // import { Group, Permission } from '@foal/typeorm'; // import { isCommon } from '@foal/password'; -import { createConnection, getManager, /*getRepository*/ } from 'typeorm'; +import { createConnection, getConnection, getManager, /*getRepository*/ } from 'typeorm'; // App import { User } from '../app/entities'; @@ -55,5 +55,7 @@ export async function main(/*args*/) { ); } catch (error) { console.log(error.message); + } finally { + await getConnection().close(); } } diff --git a/packages/cli/src/generate/templates/controller/controller.graphql.ts b/packages/cli/src/generate/templates/controller/controller.graphql.ts deleted file mode 100644 index dd6663d097..0000000000 --- a/packages/cli/src/generate/templates/controller/controller.graphql.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { GraphQLController } from '@foal/core'; - -export class /* upperFirstCamelName */Controller extends GraphQLController { - -} diff --git a/packages/cli/src/generate/templates/controller/controller.login.ts b/packages/cli/src/generate/templates/controller/controller.login.ts deleted file mode 100644 index 3b67643ecd..0000000000 --- a/packages/cli/src/generate/templates/controller/controller.login.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { LoginController } from '@foal/core'; - -export class /* upperFirstCamelName */Controller extends LoginController { - strategies = [ - // strategy('login', MyAuthenticator, mySchema) - ]; - - redirect = { - // failure: '/login', - // logout: '/login', - // success: '/home', - }; -} diff --git a/packages/cli/src/generate/templates/controller/controller.rest.ts b/packages/cli/src/generate/templates/controller/controller.rest.ts deleted file mode 100644 index 29e6591b50..0000000000 --- a/packages/cli/src/generate/templates/controller/controller.rest.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { dependency, RestController } from '@foal/core'; - -import { /* upperFirstCamelName */Collection } from '../services'; - -export class /* upperFirstCamelName */Controller extends RestController { - @dependency - collection: /* upperFirstCamelName */Collection; -} diff --git a/packages/cli/src/generate/templates/rest-api/controller.auth.ts b/packages/cli/src/generate/templates/rest-api/controller.auth.ts new file mode 100644 index 0000000000..28926eaca0 --- /dev/null +++ b/packages/cli/src/generate/templates/rest-api/controller.auth.ts @@ -0,0 +1,154 @@ +import { + ApiOperationDescription, ApiOperationId, ApiOperationSummary, ApiResponse, + ApiUseTag, Context, Delete, Get, HttpResponseCreated, + HttpResponseNoContent, HttpResponseNotFound, HttpResponseOK, Patch, Post, + Put, ValidateBody, ValidateParams, ValidateQuery +} from '@foal/core'; +import { getRepository } from 'typeorm'; + +import { /* upperFirstCamelName */, User } from '../entities'; + +const /* camelName */Schema = { + additionalProperties: false, + properties: { + text: { type: 'string', maxLength: 255 }, + }, + required: [ 'text' ], + type: 'object', +}; + +@ApiUseTag('/* camelName */') +export class /* upperFirstCamelName */Controller { + + @Get() + @ApiOperationId('find/* upperFirstCamelName */s') + @ApiOperationSummary('Find /* camelName */s.') + @ApiOperationDescription( + 'The query parameters "skip" and "take" can be used for pagination. The first ' + + 'is the offset and the second is the number of elements to be returned.' + ) + @ApiResponse(400, { description: 'Invalid query parameters.' }) + @ApiResponse(200, { description: 'Returns a list of /* camelName */s.' }) + @ValidateQuery({ + properties: { + skip: { type: 'number' }, + take: { type: 'number' }, + }, + type: 'object', + }) + async find/* upperFirstCamelName */s(ctx: Context) { + const /* camelName */s = await getRepository(/* upperFirstCamelName */).find({ + skip: ctx.request.query.skip, + take: ctx.request.query.take, + where: { + owner: ctx.user + } + }); + return new HttpResponseOK(/* camelName */s); + } + + @Get('/:/* camelName */Id') + @ApiOperationId('find/* upperFirstCamelName */ById') + @ApiOperationSummary('Find a /* camelName */ by ID.') + @ApiResponse(404, { description: '/* upperFirstCamelName */ not found.' }) + @ApiResponse(200, { description: 'Returns the /* camelName */.' }) + @ValidateParams({ properties: { /* camelName */Id: { type: 'number' } }, type: 'object' }) + async find/* upperFirstCamelName */ById(ctx: Context) { + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne({ + id: ctx.request.params./* camelName */Id, + owner: ctx.user + }); + + if (!/* camelName */) { + return new HttpResponseNotFound(); + } + + return new HttpResponseOK(/* camelName */); + } + + @Post() + @ApiOperationId('create/* upperFirstCamelName */') + @ApiOperationSummary('Create a new /* camelName */.') + @ApiResponse(400, { description: 'Invalid /* camelName */.' }) + @ApiResponse(201, { description: '/* upperFirstCamelName */ successfully created. Returns the /* camelName */.' }) + @ValidateBody(/* camelName */Schema) + async create/* upperFirstCamelName */(ctx: Context) { + const /* camelName */ = await getRepository(/* upperFirstCamelName */).save({ + ...ctx.request.body, + owner: ctx.user + }); + return new HttpResponseCreated(/* camelName */); + } + + @Patch('/:/* camelName */Id') + @ApiOperationId('modify/* upperFirstCamelName */') + @ApiOperationSummary('Update/modify an existing /* camelName */.') + @ApiResponse(400, { description: 'Invalid /* camelName */.' }) + @ApiResponse(404, { description: '/* upperFirstCamelName */ not found.' }) + @ApiResponse(200, { description: '/* upperFirstCamelName */ successfully updated. Returns the /* camelName */.' }) + @ValidateParams({ properties: { /* camelName */Id: { type: 'number' } }, type: 'object' }) + @ValidateBody({ .../* camelName */Schema, required: [] }) + async modify/* upperFirstCamelName */(ctx: Context) { + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne({ + id: ctx.request.params./* camelName */Id, + owner: ctx.user + }); + + if (!/* camelName */) { + return new HttpResponseNotFound(); + } + + Object.assign(/* camelName */, ctx.request.body); + + await getRepository(/* upperFirstCamelName */).save(/* camelName */); + + return new HttpResponseOK(/* camelName */); + } + + @Put('/:/* camelName */Id') + @ApiOperationId('replace/* upperFirstCamelName */') + @ApiOperationSummary('Update/replace an existing /* camelName */.') + @ApiResponse(400, { description: 'Invalid /* camelName */.' }) + @ApiResponse(404, { description: '/* upperFirstCamelName */ not found.' }) + @ApiResponse(200, { description: '/* upperFirstCamelName */ successfully updated. Returns the /* camelName */.' }) + @ValidateParams({ properties: { /* camelName */Id: { type: 'number' } }, type: 'object' }) + @ValidateBody(/* camelName */Schema) + async replace/* upperFirstCamelName */(ctx: Context) { + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne({ + id: ctx.request.params./* camelName */Id, + owner: ctx.user + }); + + if (!/* camelName */) { + return new HttpResponseNotFound(); + } + + Object.assign(/* camelName */, ctx.request.body); + + await getRepository(/* upperFirstCamelName */).save(/* camelName */); + + return new HttpResponseOK(/* camelName */); + } + + @Delete('/:/* camelName */Id') + @ApiOperationId('delete/* upperFirstCamelName */') + @ApiOperationSummary('Delete a /* camelName */.') + @ApiResponse(404, { description: '/* upperFirstCamelName */ not found.' }) + @ApiResponse(204, { description: '/* upperFirstCamelName */ successfully deleted.' }) + @ValidateParams({ properties: { /* camelName */Id: { type: 'number' } }, type: 'object' }) + async delete/* upperFirstCamelName */(ctx: Context) { + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne({ + id: ctx.request.params./* camelName */Id, + owner: ctx.user + }); + + if (!/* camelName */) { + return new HttpResponseNotFound(); + } + + await getRepository(/* upperFirstCamelName */).delete(ctx.request.params./* camelName */Id); + + return new HttpResponseNoContent(); + } + +} diff --git a/packages/cli/src/generate/templates/rest-api/controller.current-dir.auth.ts b/packages/cli/src/generate/templates/rest-api/controller.current-dir.auth.ts new file mode 100644 index 0000000000..4f5c383249 --- /dev/null +++ b/packages/cli/src/generate/templates/rest-api/controller.current-dir.auth.ts @@ -0,0 +1,155 @@ +import { + ApiOperationDescription, ApiOperationId, ApiOperationSummary, ApiResponse, + ApiUseTag, Context, Delete, Get, HttpResponseCreated, + HttpResponseNoContent, HttpResponseNotFound, HttpResponseOK, Patch, Post, + Put, ValidateBody, ValidateParams, ValidateQuery +} from '@foal/core'; +import { getRepository } from 'typeorm'; + +import { /* upperFirstCamelName */ } from './/* kebabName */.entity'; +import { User } from './user.entity'; + +const /* camelName */Schema = { + additionalProperties: false, + properties: { + text: { type: 'string', maxLength: 255 }, + }, + required: [ 'text' ], + type: 'object', +}; + +@ApiUseTag('/* camelName */') +export class /* upperFirstCamelName */Controller { + + @Get() + @ApiOperationId('find/* upperFirstCamelName */s') + @ApiOperationSummary('Find /* camelName */s.') + @ApiOperationDescription( + 'The query parameters "skip" and "take" can be used for pagination. The first ' + + 'is the offset and the second is the number of elements to be returned.' + ) + @ApiResponse(400, { description: 'Invalid query parameters.' }) + @ApiResponse(200, { description: 'Returns a list of /* camelName */s.' }) + @ValidateQuery({ + properties: { + skip: { type: 'number' }, + take: { type: 'number' }, + }, + type: 'object', + }) + async find/* upperFirstCamelName */s(ctx: Context) { + const /* camelName */s = await getRepository(/* upperFirstCamelName */).find({ + skip: ctx.request.query.skip, + take: ctx.request.query.take, + where: { + owner: ctx.user + } + }); + return new HttpResponseOK(/* camelName */s); + } + + @Get('/:/* camelName */Id') + @ApiOperationId('find/* upperFirstCamelName */ById') + @ApiOperationSummary('Find a /* camelName */ by ID.') + @ApiResponse(404, { description: '/* upperFirstCamelName */ not found.' }) + @ApiResponse(200, { description: 'Returns the /* camelName */.' }) + @ValidateParams({ properties: { /* camelName */Id: { type: 'number' } }, type: 'object' }) + async find/* upperFirstCamelName */ById(ctx: Context) { + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne({ + id: ctx.request.params./* camelName */Id, + owner: ctx.user + }); + + if (!/* camelName */) { + return new HttpResponseNotFound(); + } + + return new HttpResponseOK(/* camelName */); + } + + @Post() + @ApiOperationId('create/* upperFirstCamelName */') + @ApiOperationSummary('Create a new /* camelName */.') + @ApiResponse(400, { description: 'Invalid /* camelName */.' }) + @ApiResponse(201, { description: '/* upperFirstCamelName */ successfully created. Returns the /* camelName */.' }) + @ValidateBody(/* camelName */Schema) + async create/* upperFirstCamelName */(ctx: Context) { + const /* camelName */ = await getRepository(/* upperFirstCamelName */).save({ + ...ctx.request.body, + owner: ctx.user + }); + return new HttpResponseCreated(/* camelName */); + } + + @Patch('/:/* camelName */Id') + @ApiOperationId('modify/* upperFirstCamelName */') + @ApiOperationSummary('Update/modify an existing /* camelName */.') + @ApiResponse(400, { description: 'Invalid /* camelName */.' }) + @ApiResponse(404, { description: '/* upperFirstCamelName */ not found.' }) + @ApiResponse(200, { description: '/* upperFirstCamelName */ successfully updated. Returns the /* camelName */.' }) + @ValidateParams({ properties: { /* camelName */Id: { type: 'number' } }, type: 'object' }) + @ValidateBody({ .../* camelName */Schema, required: [] }) + async modify/* upperFirstCamelName */(ctx: Context) { + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne({ + id: ctx.request.params./* camelName */Id, + owner: ctx.user + }); + + if (!/* camelName */) { + return new HttpResponseNotFound(); + } + + Object.assign(/* camelName */, ctx.request.body); + + await getRepository(/* upperFirstCamelName */).save(/* camelName */); + + return new HttpResponseOK(/* camelName */); + } + + @Put('/:/* camelName */Id') + @ApiOperationId('replace/* upperFirstCamelName */') + @ApiOperationSummary('Update/replace an existing /* camelName */.') + @ApiResponse(400, { description: 'Invalid /* camelName */.' }) + @ApiResponse(404, { description: '/* upperFirstCamelName */ not found.' }) + @ApiResponse(200, { description: '/* upperFirstCamelName */ successfully updated. Returns the /* camelName */.' }) + @ValidateParams({ properties: { /* camelName */Id: { type: 'number' } }, type: 'object' }) + @ValidateBody(/* camelName */Schema) + async replace/* upperFirstCamelName */(ctx: Context) { + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne({ + id: ctx.request.params./* camelName */Id, + owner: ctx.user + }); + + if (!/* camelName */) { + return new HttpResponseNotFound(); + } + + Object.assign(/* camelName */, ctx.request.body); + + await getRepository(/* upperFirstCamelName */).save(/* camelName */); + + return new HttpResponseOK(/* camelName */); + } + + @Delete('/:/* camelName */Id') + @ApiOperationId('delete/* upperFirstCamelName */') + @ApiOperationSummary('Delete a /* camelName */.') + @ApiResponse(404, { description: '/* upperFirstCamelName */ not found.' }) + @ApiResponse(204, { description: '/* upperFirstCamelName */ successfully deleted.' }) + @ValidateParams({ properties: { /* camelName */Id: { type: 'number' } }, type: 'object' }) + async delete/* upperFirstCamelName */(ctx: Context) { + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne({ + id: ctx.request.params./* camelName */Id, + owner: ctx.user + }); + + if (!/* camelName */) { + return new HttpResponseNotFound(); + } + + await getRepository(/* upperFirstCamelName */).delete(ctx.request.params./* camelName */Id); + + return new HttpResponseNoContent(); + } + +} diff --git a/packages/cli/src/generate/templates/rest-api/controller.spec.auth.ts b/packages/cli/src/generate/templates/rest-api/controller.spec.auth.ts new file mode 100644 index 0000000000..30d1035bd3 --- /dev/null +++ b/packages/cli/src/generate/templates/rest-api/controller.spec.auth.ts @@ -0,0 +1,469 @@ +// std +import { notStrictEqual, ok, strictEqual } from 'assert'; + +// 3p +import { + Context, createController, getHttpMethod, getPath, + isHttpResponseCreated, isHttpResponseNoContent, + isHttpResponseNotFound, isHttpResponseOK +} from '@foal/core'; +import { createConnection, getConnection, getRepository } from 'typeorm'; + +// App +import { /* upperFirstCamelName */, User } from '../entities'; +import { /* upperFirstCamelName */Controller } from './/* kebabName */.controller'; + +describe('/* upperFirstCamelName */Controller', () => { + + let controller: /* upperFirstCamelName */Controller; + let /* camelName */0: /* upperFirstCamelName */; + let /* camelName */1: /* upperFirstCamelName */; + let /* camelName */2: /* upperFirstCamelName */; + let user1: User; + let user2: User; + + before(() => createConnection()); + + after(() => getConnection().close()); + + beforeEach(async () => { + controller = createController(/* upperFirstCamelName */Controller); + + const /* camelName */Repository = getRepository(/* upperFirstCamelName */); + const userRepository = getRepository(User); + + await /* camelName */Repository.clear(); + await userRepository.clear(); + + [ user1, user2 ] = await userRepository.save([ + {}, + {}, + ]); + + [ /* camelName */0, /* camelName */1, /* camelName */2 ] = await /* camelName */Repository.save([ + { + owner: user1, + text: '/* upperFirstCamelName */ 0', + }, + { + owner: user2, + text: '/* upperFirstCamelName */ 1', + }, + { + owner: user2, + text: '/* upperFirstCamelName */ 2', + }, + ]); + }); + + describe('has a "find/* upperFirstCamelName */s" method that', () => { + + it('should handle requests at GET /.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'find/* upperFirstCamelName */s'), 'GET'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'find/* upperFirstCamelName */s'), undefined); + }); + + it('should return an HttpResponseOK object with the /* camelName */ list.', async () => { + const ctx = new Context({ query: {} }); + ctx.user = user2; + const response = await controller.find/* upperFirstCamelName */s(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + if (!Array.isArray(response.body)) { + throw new Error('The response body should be an array of /* camelName */s.'); + } + + strictEqual(response.body.length, 2); + ok(response.body.find(/* camelName */ => /* camelName */.text === /* camelName */1.text)); + ok(response.body.find(/* camelName */ => /* camelName */.text === /* camelName */2.text)); + }); + + it('should support pagination', async () => { + const /* camelName */3 = await getRepository(/* upperFirstCamelName */).save({ + owner: user2, + text: '/* upperFirstCamelName */ 3', + }); + + let ctx = new Context({ + query: { + take: 2 + } + }); + ctx.user = user2; + let response = await controller.find/* upperFirstCamelName */s(ctx); + + strictEqual(response.body.length, 2); + ok(response.body.find(/* camelName */ => /* camelName */.id === /* camelName */1.id)); + ok(response.body.find(/* camelName */ => /* camelName */.id === /* camelName */2.id)); + ok(!response.body.find(/* camelName */ => /* camelName */.id === /* camelName */3.id)); + + ctx = new Context({ + query: { + skip: 1 + } + }); + ctx.user = user2; + response = await controller.find/* upperFirstCamelName */s(ctx); + + strictEqual(response.body.length, 2); + ok(!response.body.find(/* camelName */ => /* camelName */.id === /* camelName */1.id)); + ok(response.body.find(/* camelName */ => /* camelName */.id === /* camelName */2.id)); + ok(response.body.find(/* camelName */ => /* camelName */.id === /* camelName */3.id)); + }); + + }); + + describe('has a "find/* upperFirstCamelName */ById" method that', () => { + + it('should handle requests at GET /:/* camelName */Id.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'find/* upperFirstCamelName */ById'), 'GET'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'find/* upperFirstCamelName */ById'), '/:/* camelName */Id'); + }); + + it('should return an HttpResponseOK object if the /* camelName */ was found.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + const response = await controller.find/* upperFirstCamelName */ById(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + strictEqual(response.body.id, /* camelName */2.id); + strictEqual(response.body.text, /* camelName */2.text); + }); + + it('should return an HttpResponseNotFound object if the /* camelName */ was not found.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: -1 + } + }); + ctx.user = user2; + const response = await controller.find/* upperFirstCamelName */ById(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound object if the /* camelName */ belongs to another user.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: /* camelName */0.id + } + }); + ctx.user = user2; + const response = await controller.find/* upperFirstCamelName */ById(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "create/* upperFirstCamelName */" method that', () => { + + it('should handle requests at POST /.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'create/* upperFirstCamelName */'), 'POST'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'create/* upperFirstCamelName */'), undefined); + }); + + it('should create the /* camelName */ in the database and return it through ' + + 'an HttpResponseCreated object.', async () => { + const ctx = new Context({ + body: { + text: '/* upperFirstCamelName */ 3', + } + }); + ctx.user = user2; + const response = await controller.create/* upperFirstCamelName */(ctx); + + if (!isHttpResponseCreated(response)) { + throw new Error('The returned value should be an HttpResponseCreated object.'); + } + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne({ + relations: [ 'owner' ], + where: { text: '/* upperFirstCamelName */ 3' }, + }); + + if (!/* camelName */) { + throw new Error('No /* camelName */ 3 was found in the database.'); + } + + strictEqual(/* camelName */.text, '/* upperFirstCamelName */ 3'); + strictEqual(/* camelName */.owner.id, user2.id); + + strictEqual(response.body.id, /* camelName */.id); + strictEqual(response.body.text, /* camelName */.text); + }); + + }); + + describe('has a "modify/* upperFirstCamelName */" method that', () => { + + it('should handle requests at PATCH /:/* camelName */Id.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'modify/* upperFirstCamelName */'), 'PATCH'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'modify/* upperFirstCamelName */'), '/:/* camelName */Id'); + }); + + it('should update the /* camelName */ in the database and return it through an HttpResponseOK object.', async () => { + const ctx = new Context({ + body: { + text: '/* upperFirstCamelName */ 2 (version 2)', + }, + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + const response = await controller.modify/* upperFirstCamelName */(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */2.id); + + if (!/* camelName */) { + throw new Error(); + } + + strictEqual(/* camelName */.text, '/* upperFirstCamelName */ 2 (version 2)'); + + strictEqual(response.body.id, /* camelName */.id); + strictEqual(response.body.text, /* camelName */.text); + }); + + it('should not update the other /* camelName */s.', async () => { + const ctx = new Context({ + body: { + text: '/* upperFirstCamelName */ 2 (version 2)', + }, + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + await controller.modify/* upperFirstCamelName */(ctx); + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */1.id); + + if (!/* camelName */) { + throw new Error(); + } + + notStrictEqual(/* camelName */.text, '/* upperFirstCamelName */ 2 (version 2)'); + }); + + it('should return an HttpResponseNotFound if the object does not exist.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + /* camelName */Id: -1 + } + }); + ctx.user = user2; + const response = await controller.modify/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the object belongs to another user.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + /* camelName */Id: /* camelName */0.id + } + }); + ctx.user = user2; + const response = await controller.modify/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "replace/* upperFirstCamelName */" method that', () => { + + it('should handle requests at PUT /:/* camelName */Id.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'replace/* upperFirstCamelName */'), 'PUT'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'replace/* upperFirstCamelName */'), '/:/* camelName */Id'); + }); + + it('should update the /* camelName */ in the database and return it through an HttpResponseOK object.', async () => { + const ctx = new Context({ + body: { + text: '/* upperFirstCamelName */ 2 (version 2)', + }, + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + const response = await controller.replace/* upperFirstCamelName */(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */2.id); + + if (!/* camelName */) { + throw new Error(); + } + + strictEqual(/* camelName */.text, '/* upperFirstCamelName */ 2 (version 2)'); + + strictEqual(response.body.id, /* camelName */.id); + strictEqual(response.body.text, /* camelName */.text); + }); + + it('should not update the other /* camelName */s.', async () => { + const ctx = new Context({ + body: { + text: '/* upperFirstCamelName */ 2 (version 2)', + }, + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + await controller.replace/* upperFirstCamelName */(ctx); + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */1.id); + + if (!/* camelName */) { + throw new Error(); + } + + notStrictEqual(/* camelName */.text, '/* upperFirstCamelName */ 2 (version 2)'); + }); + + it('should return an HttpResponseNotFound if the object does not exist.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + /* camelName */Id: -1 + } + }); + ctx.user = user2; + const response = await controller.replace/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the object belongs to another user.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + /* camelName */Id: /* camelName */0.id + } + }); + ctx.user = user2; + const response = await controller.replace/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "delete/* upperFirstCamelName */" method that', () => { + + it('should handle requests at DELETE /:/* camelName */Id.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'delete/* upperFirstCamelName */'), 'DELETE'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'delete/* upperFirstCamelName */'), '/:/* camelName */Id'); + }); + + it('should delete the /* camelName */ and return an HttpResponseNoContent object.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + const response = await controller.delete/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNoContent(response)) { + throw new Error('The returned value should be an HttpResponseNoContent object.'); + } + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */2.id); + + strictEqual(/* camelName */, undefined); + }); + + it('should not delete the other /* camelName */s.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + const response = await controller.delete/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNoContent(response)) { + throw new Error('The returned value should be an HttpResponseNoContent object.'); + } + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */1.id); + + notStrictEqual(/* camelName */, undefined); + }); + + it('should return an HttpResponseNotFound if the /* camelName */ was not found.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: -1 + } + }); + ctx.user = user2; + const response = await controller.delete/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the /* camelName */ belongs to another user.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: /* camelName */0.id + } + }); + ctx.user = user2; + const response = await controller.delete/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + +}); diff --git a/packages/cli/src/generate/templates/rest-api/controller.spec.current-dir.auth.ts b/packages/cli/src/generate/templates/rest-api/controller.spec.current-dir.auth.ts new file mode 100644 index 0000000000..5d7751bd1b --- /dev/null +++ b/packages/cli/src/generate/templates/rest-api/controller.spec.current-dir.auth.ts @@ -0,0 +1,470 @@ +// std +import { notStrictEqual, ok, strictEqual } from 'assert'; + +// 3p +import { + Context, createController, getHttpMethod, getPath, + isHttpResponseCreated, isHttpResponseNoContent, + isHttpResponseNotFound, isHttpResponseOK +} from '@foal/core'; +import { createConnection, getConnection, getRepository } from 'typeorm'; + +// App +import { /* upperFirstCamelName */Controller } from './/* kebabName */.controller'; +import { /* upperFirstCamelName */ } from './/* kebabName */.entity'; +import { User } from './user.entity'; + +describe('/* upperFirstCamelName */Controller', () => { + + let controller: /* upperFirstCamelName */Controller; + let /* camelName */0: /* upperFirstCamelName */; + let /* camelName */1: /* upperFirstCamelName */; + let /* camelName */2: /* upperFirstCamelName */; + let user1: User; + let user2: User; + + before(() => createConnection()); + + after(() => getConnection().close()); + + beforeEach(async () => { + controller = createController(/* upperFirstCamelName */Controller); + + const /* camelName */Repository = getRepository(/* upperFirstCamelName */); + const userRepository = getRepository(User); + + await /* camelName */Repository.clear(); + await userRepository.clear(); + + [ user1, user2 ] = await userRepository.save([ + {}, + {}, + ]); + + [ /* camelName */0, /* camelName */1, /* camelName */2 ] = await /* camelName */Repository.save([ + { + owner: user1, + text: '/* upperFirstCamelName */ 0', + }, + { + owner: user2, + text: '/* upperFirstCamelName */ 1', + }, + { + owner: user2, + text: '/* upperFirstCamelName */ 2', + }, + ]); + }); + + describe('has a "find/* upperFirstCamelName */s" method that', () => { + + it('should handle requests at GET /.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'find/* upperFirstCamelName */s'), 'GET'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'find/* upperFirstCamelName */s'), undefined); + }); + + it('should return an HttpResponseOK object with the /* camelName */ list.', async () => { + const ctx = new Context({ query: {} }); + ctx.user = user2; + const response = await controller.find/* upperFirstCamelName */s(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + if (!Array.isArray(response.body)) { + throw new Error('The response body should be an array of /* camelName */s.'); + } + + strictEqual(response.body.length, 2); + ok(response.body.find(/* camelName */ => /* camelName */.text === /* camelName */1.text)); + ok(response.body.find(/* camelName */ => /* camelName */.text === /* camelName */2.text)); + }); + + it('should support pagination', async () => { + const /* camelName */3 = await getRepository(/* upperFirstCamelName */).save({ + owner: user2, + text: '/* upperFirstCamelName */ 3', + }); + + let ctx = new Context({ + query: { + take: 2 + } + }); + ctx.user = user2; + let response = await controller.find/* upperFirstCamelName */s(ctx); + + strictEqual(response.body.length, 2); + ok(response.body.find(/* camelName */ => /* camelName */.id === /* camelName */1.id)); + ok(response.body.find(/* camelName */ => /* camelName */.id === /* camelName */2.id)); + ok(!response.body.find(/* camelName */ => /* camelName */.id === /* camelName */3.id)); + + ctx = new Context({ + query: { + skip: 1 + } + }); + ctx.user = user2; + response = await controller.find/* upperFirstCamelName */s(ctx); + + strictEqual(response.body.length, 2); + ok(!response.body.find(/* camelName */ => /* camelName */.id === /* camelName */1.id)); + ok(response.body.find(/* camelName */ => /* camelName */.id === /* camelName */2.id)); + ok(response.body.find(/* camelName */ => /* camelName */.id === /* camelName */3.id)); + }); + + }); + + describe('has a "find/* upperFirstCamelName */ById" method that', () => { + + it('should handle requests at GET /:/* camelName */Id.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'find/* upperFirstCamelName */ById'), 'GET'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'find/* upperFirstCamelName */ById'), '/:/* camelName */Id'); + }); + + it('should return an HttpResponseOK object if the /* camelName */ was found.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + const response = await controller.find/* upperFirstCamelName */ById(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + strictEqual(response.body.id, /* camelName */2.id); + strictEqual(response.body.text, /* camelName */2.text); + }); + + it('should return an HttpResponseNotFound object if the /* camelName */ was not found.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: -1 + } + }); + ctx.user = user2; + const response = await controller.find/* upperFirstCamelName */ById(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound object if the /* camelName */ belongs to another user.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: /* camelName */0.id + } + }); + ctx.user = user2; + const response = await controller.find/* upperFirstCamelName */ById(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "create/* upperFirstCamelName */" method that', () => { + + it('should handle requests at POST /.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'create/* upperFirstCamelName */'), 'POST'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'create/* upperFirstCamelName */'), undefined); + }); + + it('should create the /* camelName */ in the database and return it through ' + + 'an HttpResponseCreated object.', async () => { + const ctx = new Context({ + body: { + text: '/* upperFirstCamelName */ 3', + } + }); + ctx.user = user2; + const response = await controller.create/* upperFirstCamelName */(ctx); + + if (!isHttpResponseCreated(response)) { + throw new Error('The returned value should be an HttpResponseCreated object.'); + } + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne({ + relations: [ 'owner' ], + where: { text: '/* upperFirstCamelName */ 3' }, + }); + + if (!/* camelName */) { + throw new Error('No /* camelName */ 3 was found in the database.'); + } + + strictEqual(/* camelName */.text, '/* upperFirstCamelName */ 3'); + strictEqual(/* camelName */.owner.id, user2.id); + + strictEqual(response.body.id, /* camelName */.id); + strictEqual(response.body.text, /* camelName */.text); + }); + + }); + + describe('has a "modify/* upperFirstCamelName */" method that', () => { + + it('should handle requests at PATCH /:/* camelName */Id.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'modify/* upperFirstCamelName */'), 'PATCH'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'modify/* upperFirstCamelName */'), '/:/* camelName */Id'); + }); + + it('should update the /* camelName */ in the database and return it through an HttpResponseOK object.', async () => { + const ctx = new Context({ + body: { + text: '/* upperFirstCamelName */ 2 (version 2)', + }, + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + const response = await controller.modify/* upperFirstCamelName */(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */2.id); + + if (!/* camelName */) { + throw new Error(); + } + + strictEqual(/* camelName */.text, '/* upperFirstCamelName */ 2 (version 2)'); + + strictEqual(response.body.id, /* camelName */.id); + strictEqual(response.body.text, /* camelName */.text); + }); + + it('should not update the other /* camelName */s.', async () => { + const ctx = new Context({ + body: { + text: '/* upperFirstCamelName */ 2 (version 2)', + }, + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + await controller.modify/* upperFirstCamelName */(ctx); + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */1.id); + + if (!/* camelName */) { + throw new Error(); + } + + notStrictEqual(/* camelName */.text, '/* upperFirstCamelName */ 2 (version 2)'); + }); + + it('should return an HttpResponseNotFound if the object does not exist.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + /* camelName */Id: -1 + } + }); + ctx.user = user2; + const response = await controller.modify/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the object belongs to another user.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + /* camelName */Id: /* camelName */0.id + } + }); + ctx.user = user2; + const response = await controller.modify/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "replace/* upperFirstCamelName */" method that', () => { + + it('should handle requests at PUT /:/* camelName */Id.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'replace/* upperFirstCamelName */'), 'PUT'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'replace/* upperFirstCamelName */'), '/:/* camelName */Id'); + }); + + it('should update the /* camelName */ in the database and return it through an HttpResponseOK object.', async () => { + const ctx = new Context({ + body: { + text: '/* upperFirstCamelName */ 2 (version 2)', + }, + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + const response = await controller.replace/* upperFirstCamelName */(ctx); + + if (!isHttpResponseOK(response)) { + throw new Error('The returned value should be an HttpResponseOK object.'); + } + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */2.id); + + if (!/* camelName */) { + throw new Error(); + } + + strictEqual(/* camelName */.text, '/* upperFirstCamelName */ 2 (version 2)'); + + strictEqual(response.body.id, /* camelName */.id); + strictEqual(response.body.text, /* camelName */.text); + }); + + it('should not update the other /* camelName */s.', async () => { + const ctx = new Context({ + body: { + text: '/* upperFirstCamelName */ 2 (version 2)', + }, + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + await controller.replace/* upperFirstCamelName */(ctx); + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */1.id); + + if (!/* camelName */) { + throw new Error(); + } + + notStrictEqual(/* camelName */.text, '/* upperFirstCamelName */ 2 (version 2)'); + }); + + it('should return an HttpResponseNotFound if the object does not exist.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + /* camelName */Id: -1 + } + }); + ctx.user = user2; + const response = await controller.replace/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the object belongs to another user.', async () => { + const ctx = new Context({ + body: { + text: '', + }, + params: { + /* camelName */Id: /* camelName */0.id + } + }); + ctx.user = user2; + const response = await controller.replace/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + + describe('has a "delete/* upperFirstCamelName */" method that', () => { + + it('should handle requests at DELETE /:/* camelName */Id.', () => { + strictEqual(getHttpMethod(/* upperFirstCamelName */Controller, 'delete/* upperFirstCamelName */'), 'DELETE'); + strictEqual(getPath(/* upperFirstCamelName */Controller, 'delete/* upperFirstCamelName */'), '/:/* camelName */Id'); + }); + + it('should delete the /* camelName */ and return an HttpResponseNoContent object.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + const response = await controller.delete/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNoContent(response)) { + throw new Error('The returned value should be an HttpResponseNoContent object.'); + } + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */2.id); + + strictEqual(/* camelName */, undefined); + }); + + it('should not delete the other /* camelName */s.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: /* camelName */2.id + } + }); + ctx.user = user2; + const response = await controller.delete/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNoContent(response)) { + throw new Error('The returned value should be an HttpResponseNoContent object.'); + } + + const /* camelName */ = await getRepository(/* upperFirstCamelName */).findOne(/* camelName */1.id); + + notStrictEqual(/* camelName */, undefined); + }); + + it('should return an HttpResponseNotFound if the /* camelName */ was not found.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: -1 + } + }); + ctx.user = user2; + const response = await controller.delete/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + it('should return an HttpResponseNotFound if the /* camelName */ belongs to another user.', async () => { + const ctx = new Context({ + params: { + /* camelName */Id: /* camelName */0.id + } + }); + ctx.user = user2; + const response = await controller.delete/* upperFirstCamelName */(ctx); + + if (!isHttpResponseNotFound(response)) { + throw new Error('The returned value should be an HttpResponseNotFound object.'); + } + }); + + }); + +}); diff --git a/packages/cli/src/generate/templates/rest-api/controller.spec.current-dir.ts b/packages/cli/src/generate/templates/rest-api/controller.spec.current-dir.ts index d84be4f740..c2ac55f4fa 100644 --- a/packages/cli/src/generate/templates/rest-api/controller.spec.current-dir.ts +++ b/packages/cli/src/generate/templates/rest-api/controller.spec.current-dir.ts @@ -351,7 +351,7 @@ describe('/* upperFirstCamelName */Controller', () => { notStrictEqual(/* camelName */, undefined); }); - it('should return an HttpResponseNotFound if the /* camelName */ was not fond.', async () => { + it('should return an HttpResponseNotFound if the /* camelName */ was not found.', async () => { const ctx = new Context({ params: { /* camelName */Id: -1 diff --git a/packages/cli/src/generate/templates/rest-api/controller.spec.ts b/packages/cli/src/generate/templates/rest-api/controller.spec.ts index 3f1708712d..3dfb623764 100644 --- a/packages/cli/src/generate/templates/rest-api/controller.spec.ts +++ b/packages/cli/src/generate/templates/rest-api/controller.spec.ts @@ -351,7 +351,7 @@ describe('/* upperFirstCamelName */Controller', () => { notStrictEqual(/* camelName */, undefined); }); - it('should return an HttpResponseNotFound if the /* camelName */ was not fond.', async () => { + it('should return an HttpResponseNotFound if the /* camelName */ was not found.', async () => { const ctx = new Context({ params: { /* camelName */Id: -1 diff --git a/packages/cli/src/generate/templates/rest-api/entity.auth.ts b/packages/cli/src/generate/templates/rest-api/entity.auth.ts new file mode 100644 index 0000000000..00eb22a700 --- /dev/null +++ b/packages/cli/src/generate/templates/rest-api/entity.auth.ts @@ -0,0 +1,17 @@ +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; + +import { User } from './user.entity'; + +@Entity() +export class /* upperFirstCamelName */ { + + @PrimaryGeneratedColumn() + id: number; + + @Column() + text: string; + + @ManyToOne(() => User, { nullable: false }) + owner: User; + +} diff --git a/packages/cli/src/generate/templates/script/script.mongoose.ts b/packages/cli/src/generate/templates/script/script.mongoose.ts new file mode 100644 index 0000000000..f4f7be9e3a --- /dev/null +++ b/packages/cli/src/generate/templates/script/script.mongoose.ts @@ -0,0 +1,26 @@ +// 3p +import { Config } from '@foal/core'; +import { connect, disconnect } from 'mongoose'; + +export const schema = { + additionalProperties: false, + properties: { + /* To complete */ + }, + required: [ /* To complete */ ], + type: 'object', +}; + +export async function main(args: any) { + const uri = Config.getOrThrow('mongodb.uri', 'string'); + await connect(uri, { useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true }); + + try { + // Do something. + + } catch (error) { + console.error(error); + } finally { + await disconnect(); + } +} diff --git a/packages/cli/src/generate/templates/script/script.ts b/packages/cli/src/generate/templates/script/script.ts index 803eb73366..d645d0cc6d 100644 --- a/packages/cli/src/generate/templates/script/script.ts +++ b/packages/cli/src/generate/templates/script/script.ts @@ -11,7 +11,14 @@ export const schema = { }; export async function main(args: any) { - await createConnection(); + const connection = await createConnection(); - // Do something. + try { + // Do something. + + } catch (error) { + console.error(error); + } finally { + await connection.close(); + } } diff --git a/packages/cli/src/generate/utils/find-project-path.ts b/packages/cli/src/generate/utils/find-project-path.ts deleted file mode 100644 index ae72885bf0..0000000000 --- a/packages/cli/src/generate/utils/find-project-path.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { existsSync } from 'fs'; -import { join, parse } from 'path'; - -export function findProjectPath(): string|null { - let path = process.cwd(); - const root = parse(path).root; - - while (path !== root) { - if (existsSync(join(path, 'package.json'))) { - break; - } - path = parse(path).dir; - } - - if (path === root) { - return null; - } - - return path; -} diff --git a/packages/cli/src/generate/utils/generator.ts b/packages/cli/src/generate/utils/generator.ts deleted file mode 100644 index d0b2e71f31..0000000000 --- a/packages/cli/src/generate/utils/generator.ts +++ /dev/null @@ -1,119 +0,0 @@ -// std -import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'fs'; -import { join } from 'path'; - -// 3p -import { cyan, green } from 'colors/safe'; - -// FoalTS -import { mkdirIfDoesNotExist } from './mkdir-if-does-not-exist'; - -export class Generator { - constructor( - private name: string, - private root: string, - private options: { - noLogs?: boolean - } = {} - ) {} - - /* Create architecture */ - - mkdirIfDoesNotExist(path: string): this { - mkdirIfDoesNotExist(join(this.root, path)); - return this; - } - - mkdirIfDoesNotExistOnlyIf(condition: boolean, path: string): this { - if (condition) { - return this.mkdirIfDoesNotExist(path); - } - return this; - } - - /* Create files */ - - copyFileFromTemplates(srcPath: string, destPath?: string): this { - destPath = destPath || srcPath; - - const absoluteSrcPath = join(__dirname, '../templates', this.name, srcPath); - - if (!existsSync(absoluteSrcPath)) { - throw new Error(`Template not found: ${srcPath}`); - } - - this.logCreate(destPath); - copyFileSync(absoluteSrcPath, join(this.root, destPath)); - return this; - } - - copyFileFromTemplatesOnlyIf(condition: boolean, srcPath: string, destPath?: string): this { - if (condition) { - return this.copyFileFromTemplates(srcPath, destPath); - } - return this; - } - - renderTemplate(templatePath: string, locals: object, destPath?: string): this { - destPath = destPath || templatePath; - - const absoluteTemplatePath = join(__dirname, '../templates', this.name, templatePath); - - if (!existsSync(absoluteTemplatePath)) { - throw new Error(`Template not found: ${templatePath}`); - } - - this.logCreate(destPath); - const template = readFileSync(absoluteTemplatePath, 'utf8'); - let content = template; - for (const key in locals) { - if (locals.hasOwnProperty(key)) { - content = content.split(`/* ${key} */`).join((locals as any)[key]); - } - } - writeFileSync(join(this.root, destPath), content, 'utf8'); - return this; - } - - renderTemplateOnlyIf(condition: boolean, templatePath: string, locals: object, destPath?: string): this { - if (condition) { - return this.renderTemplate(templatePath, locals, destPath); - } - return this; - } - - /* Update files */ - - updateFile(path: string, cb: (content: string) => string, options: { allowFailure?: boolean } = {}) { - let content: string; - try { - content = readFileSync(join(this.root, path), 'utf8'); - } catch (err) { - if (options.allowFailure) { - return this; - } - throw err; - } - this.logUpdate(path); - writeFileSync(join(this.root, path), cb(content), 'utf8'); - return this; - } - - private logCreate(path: string) { - if (this.root) { - path = join(this.root, path); - } - if (process.env.NODE_ENV !== 'test' && !this.options.noLogs) { - console.log(`${green('CREATE')} ${path}`); - } - } - - private logUpdate(path: string) { - if (this.root) { - path = join(this.root, path); - } - if (process.env.NODE_ENV !== 'test' && !this.options.noLogs) { - console.log(`${cyan('UPDATE')} ${path}`); - } - } -} diff --git a/packages/cli/src/generate/utils/index.ts b/packages/cli/src/generate/utils/index.ts index fa2ef7d630..dc64bfe433 100644 --- a/packages/cli/src/generate/utils/index.ts +++ b/packages/cli/src/generate/utils/index.ts @@ -1,28 +1,4 @@ -// std -import * as fs from 'fs'; -import { join } from 'path'; - -// FoalTS -export { findProjectPath } from './find-project-path'; -export { Generator } from './generator'; export { initGitRepo } from './init-git-repo'; export { rmDirAndFilesIfExist } from './rm-dir-and-files-if-exist'; export { mkdirIfDoesNotExist } from './mkdir-if-does-not-exist'; -export { TestEnvironment } from './test-environment'; export { getNames } from './get-names'; - -export function rmfileIfExists(path: string) { - if (fs.existsSync(path)) { - fs.unlinkSync(path); - } -} - -// TODO: remove this. -export function readFileFromTemplatesSpec(src: string): string { - return fs.readFileSync(join(__dirname, '../specs', src), 'utf8'); -} - -// TODO: remove this. -export function readFileFromRoot(src: string): string { - return fs.readFileSync(src, 'utf8'); -} diff --git a/packages/cli/src/generate/utils/test-environment.ts b/packages/cli/src/generate/utils/test-environment.ts deleted file mode 100644 index 68eb45e955..0000000000 --- a/packages/cli/src/generate/utils/test-environment.ts +++ /dev/null @@ -1,93 +0,0 @@ -// std -import { ok, strictEqual } from 'assert'; -import { copyFileSync, existsSync, readdirSync, readFileSync, rmdirSync, statSync, unlinkSync } from 'fs'; -import { join } from 'path'; - -// FoalTS -import { mkdirIfDoesNotExist } from './mkdir-if-does-not-exist'; - -export class TestEnvironment { - constructor(private generatorName: string, private root: string = '') {} - - /* Create environment */ - mkRootDirIfDoesNotExist() { - if (this.root) { - mkdirIfDoesNotExist(this.root); - } - } - copyFileFromMocks(srcPath: string, destPath?: string) { - destPath = destPath || srcPath; - copyFileSync( - join(__dirname, '../mocks', this.generatorName, srcPath), - join(this.root, destPath) - ); - return this; - } - - /* Remove environment */ - rmfileIfExists(path: string) { - path = join(this.root, path); - if (existsSync(path)) { - unlinkSync(path); - } - } - rmDirAndFilesIfExist(path: string) { - const absolutePath = join(this.root, path); - if (!existsSync(absolutePath)) { - return; - } - - const files = readdirSync(absolutePath); - for (const file of files) { - const stat = statSync(join(absolutePath, file)); - if (stat.isDirectory()) { - this.rmDirAndFilesIfExist(join(path, file)); - } else { - unlinkSync(join(absolutePath, file)); - } - } - - rmdirSync(absolutePath); - } - - /* Test */ - validateSpec(specPath: string, filePath?: string) { - filePath = filePath || specPath; - - const absoluteSpecPath = join(__dirname, '../specs', this.generatorName, specPath); - - if (!existsSync(absoluteSpecPath)) { - throw new Error(`Spec file not found: ${specPath}`); - } - - const spec = readFileSync(absoluteSpecPath, 'utf8'); - const actual = readFileSync(join(this.root, filePath), 'utf8'); - - strictEqual(actual.replace(/\r\n/g, '\n'), spec.replace(/\r\n/g, '\n')); - - return this; - } - - validateFileSpec(specPath: string, filePath?: string) { - filePath = filePath || specPath; - - const absoluteSpecPath = join(__dirname, '../specs', this.generatorName, specPath); - - if (!existsSync(absoluteSpecPath)) { - throw new Error(`Spec file not found: ${specPath}`); - } - - const spec = readFileSync(absoluteSpecPath); - const actual = readFileSync(join(this.root, filePath)); - - ok(actual.equals(spec)); - - return this; - } - - shouldNotExist(filePath: string) { - const exists = existsSync(join(this.root, filePath)); - ok(!exists, `${filePath} should not exist.`); - return this; - } -} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 076864acff..441cdacdcf 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -26,6 +26,7 @@ import { createSubApp, createVSCodeConfig, } from './generate'; +import { ClientError } from './generate/file-system'; import { rmdir } from './rmdir'; import { runScript } from './run-script'; @@ -103,51 +104,77 @@ const generateTypes: GenerateType[] = [ ]; program - .command('generate ') + .command('generate [name]') .description('Generate and/or modify files.') - .option('-r, --register', 'Register the controller into app.controller.ts (only available if type=controller)', false) + .option( + '-r, --register', + 'Register the controller into app.controller.ts (only available if type=controller|rest-api)', + false + ) + .option( + '-a, --auth', + 'Add an owner to the entities of the generated REST API (only available if type=rest-api)', + false + ) .alias('g') .on('--help', () => { console.log(); console.log('Available types:'); generateTypes.forEach(t => console.log(` ${t}`)); }) - .action(async (type: GenerateType, name: string, options: { register: boolean }) => { - switch (type) { - case 'controller': - createController({ name, type: 'Empty', register: options.register }); - break; - case 'entity': - createEntity({ name }); - break; - case 'rest-api': - createRestApi({ name, register: options.register }); - break; - case 'hook': - createHook({ name }); - break; - case 'model': - createModel({ name, checkMongoose: true }); - break; - case 'sub-app': - createSubApp({ name }); - break; - case 'script': - createScript({ name }); - break; - case 'service': - createService({ name }); - break; - case 'vscode-config': - createVSCodeConfig(); - break; - default: - console.error(); - console.error(red(`Unknown type ${yellow(type)}. Please provide a valid one:`)); + .action(async (type: GenerateType, name: string, options: { register: boolean, auth: boolean }) => { + if (!name && type !== 'vscode-config') { + console.error(); + console.error(red(`Argument "name" is required when creating a ${type}. Please provide one.`)); + console.error(); + return; + } + try { + switch (type) { + case 'controller': + createController({ name, register: options.register }); + break; + case 'entity': + createEntity({ name }); + break; + case 'rest-api': + createRestApi({ name, register: options.register, auth: options.auth }); + break; + case 'hook': + createHook({ name }); + break; + case 'model': + createModel({ name }); + break; + case 'sub-app': + createSubApp({ name }); + break; + case 'script': + createScript({ name }); + break; + case 'service': + createService({ name }); + break; + case 'vscode-config': + createVSCodeConfig(); + break; + default: + console.error(); + console.error(red(`Unknown type ${yellow(type)}. Please provide a valid one:`)); + console.error(); + generateTypes.forEach(t => console.error(red(` ${t}`))); + console.error(); + } + } catch (error) { + if (error instanceof ClientError) { console.error(); - generateTypes.forEach(t => console.error(red(` ${t}`))); + console.error(red(error.message)); console.error(); + return; + } + throw error; } + }); program diff --git a/packages/cli/src/run-script/run-script.spec.ts b/packages/cli/src/run-script/run-script.spec.ts index 787f37fd67..0e5910f35e 100644 --- a/packages/cli/src/run-script/run-script.spec.ts +++ b/packages/cli/src/run-script/run-script.spec.ts @@ -3,10 +3,16 @@ import { deepStrictEqual, strictEqual } from 'assert'; import { join } from 'path'; // FoalTS -import { existsSync, readFileSync, writeFileSync } from 'fs'; -import { mkdirIfDoesNotExist, rmDirAndFilesIfExist, rmfileIfExists } from '../generate/utils'; +import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; +import { mkdirIfDoesNotExist, rmDirAndFilesIfExist } from '../generate/utils'; import { runScript } from './run-script'; +function rmfileIfExists(path: string) { + if (existsSync(path)) { + unlinkSync(path); + } +} + describe('runScript', () => { afterEach(() => { diff --git a/packages/cli/src/test.ts b/packages/cli/src/test.ts index f7534b31d9..51ab3a3ae3 100644 --- a/packages/cli/src/test.ts +++ b/packages/cli/src/test.ts @@ -1 +1,5 @@ -process.env.NODE_ENV = 'test'; +// The environment variable NODE_ENV is not used on purpose because +// one could have this variable defined globally on their host. +// Here we are sure that the name "P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST" +// is not used by anyone. +process.env.P1Z7kEbSUUPMxF8GqPwD8Gx_FOAL_CLI_TEST = 'true'; diff --git a/packages/cli/tsconfig-build.json b/packages/cli/tsconfig-build.json index 9228b97e30..6d04566c8e 100644 --- a/packages/cli/tsconfig-build.json +++ b/packages/cli/tsconfig-build.json @@ -5,7 +5,7 @@ }, "exclude": [ "./src/**/*.spec.ts", - "./src/generate/mocks/**/*.ts", + "./src/generate/fixtures/**/*.ts", "./src/generate/specs/**/*.ts", "./src/generate/templates/**/*.ts" ] diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index 80d2f8f5b6..942a5ecfd7 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/core", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/core/package.json b/packages/core/package.json index e055ba550f..6a61fe9805 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@foal/core", - "version": "1.8.1", + "version": "1.9.0", "description": "A Node.js and TypeScript framework, all-inclusive.", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -88,7 +88,7 @@ "reflect-metadata": "~0.1.13" }, "devDependencies": { - "@foal/ejs": "^1.8.1", + "@foal/ejs": "^1.9.0", "@types/mocha": "~2.2.43", "@types/node": "~10.1.2", "@types/supertest": "~2.0.5", diff --git a/packages/core/src/core/http/contexts.ts b/packages/core/src/core/http/contexts.ts index 5732d72118..e469a63c92 100644 --- a/packages/core/src/core/http/contexts.ts +++ b/packages/core/src/core/http/contexts.ts @@ -108,8 +108,8 @@ interface Request extends IncomingMessage { * @class Context * @template User */ -export class Context { - state: { [key: string]: any } = {}; +export class Context { + state: ContextState = {} as ContextState; user: User; session: ContextSession; request: Request; diff --git a/packages/core/src/core/http/http-responses.spec.ts b/packages/core/src/core/http/http-responses.spec.ts index a9c67c0da5..a194c3cd7f 100644 --- a/packages/core/src/core/http/http-responses.spec.ts +++ b/packages/core/src/core/http/http-responses.spec.ts @@ -23,6 +23,7 @@ import { HttpResponseRedirection, HttpResponseServerError, HttpResponseSuccess, + HttpResponseTooManyRequests, HttpResponseUnauthorized, isHttpResponse, isHttpResponseBadRequest, @@ -41,6 +42,7 @@ import { isHttpResponseRedirection, isHttpResponseServerError, isHttpResponseSuccess, + isHttpResponseTooManyRequests, isHttpResponseUnauthorized } from './http-responses'; @@ -302,14 +304,14 @@ describe('createHttpResponseFile', () => { ...pngFileOptions, forceDownload: true }); - strictEqual(httpResponse.getHeader('Content-Disposition'), 'attachement; filename="test-file.png"'); + strictEqual(httpResponse.getHeader('Content-Disposition'), 'attachment; filename="test-file.png"'); httpResponse = await createHttpResponseFile({ ...pngFileOptions, filename: 'download.png', forceDownload: true, }); - strictEqual(httpResponse.getHeader('Content-Disposition'), 'attachement; filename="download.png"'); + strictEqual(httpResponse.getHeader('Content-Disposition'), 'attachment; filename="download.png"'); }); it('should sanitize the "file" option to only keep the base name.', async () => { @@ -906,6 +908,61 @@ describe('isHttpResponseConflict', () => { }); +describe('HttpResponseTooManyRequests', () => { + + it('should inherit from HttpResponseClientError and HttpResponse', () => { + const httpResponse = new HttpResponseTooManyRequests(); + ok(httpResponse instanceof HttpResponse); + ok(httpResponse instanceof HttpResponseClientError); + }); + + it('should have the correct status.', () => { + const httpResponse = new HttpResponseTooManyRequests(); + strictEqual(httpResponse.statusCode, 429); + strictEqual(httpResponse.statusMessage, 'TOO MANY REQUESTS'); + }); + + it('should accept an optional body.', () => { + let httpResponse = new HttpResponseTooManyRequests(); + strictEqual(httpResponse.body, undefined); + + const body = { foo: 'bar' }; + httpResponse = new HttpResponseTooManyRequests(body); + strictEqual(httpResponse.body, body); + }); + + it('should accept optional options.', () => { + let httpResponse = new HttpResponseTooManyRequests(); + strictEqual(httpResponse.stream, false); + + httpResponse = new HttpResponseTooManyRequests({}, { stream: true }); + strictEqual(httpResponse.stream, true); + }); + +}); + +describe('isHttpResponseTooManyRequests', () => { + + it('should return true if the given object is an instance of HttpResponseTooManyRequests.', () => { + const response = new HttpResponseTooManyRequests(); + strictEqual(isHttpResponseTooManyRequests(response), true); + }); + + it('should return true if the given object has an isHttpResponseTooManyRequests property equal to true.', () => { + const response = { isHttpResponseTooManyRequests: true }; + strictEqual(isHttpResponseTooManyRequests(response), true); + }); + + it('should return false if the given object is not an instance of HttpResponseTooManyRequests and if it ' + + 'has no property isHttpResponseTooManyRequests.', () => { + const response = {}; + strictEqual(isHttpResponseTooManyRequests(response), false); + strictEqual(isHttpResponseTooManyRequests(undefined), false); + strictEqual(isHttpResponseTooManyRequests(null), false); + }); + +}); + describe('isHttpResponseServerError', () => { it('should return true if the given object is an instance of HttpResponseServerError.', () => { diff --git a/packages/core/src/core/http/http-responses.ts b/packages/core/src/core/http/http-responses.ts index 13afc11066..cfc4e61b52 100644 --- a/packages/core/src/core/http/http-responses.ts +++ b/packages/core/src/core/http/http-responses.ts @@ -25,7 +25,7 @@ export interface CookieOptions { } /** - * Reprensent an HTTP response. This class must be extended. + * Represent an HTTP response. This class must be extended. * Instances of HttpResponse are returned in hooks and controller * methods. * @@ -233,7 +233,7 @@ export function isHttpResponseSuccess(obj: any): obj is HttpResponseSuccess { */ export class HttpResponseOK extends HttpResponseSuccess { /** - * Property used internally by isHttpResponOK. + * Property used internally by isHttpResponseOK. * * @memberof HttpResponseOK */ @@ -309,7 +309,7 @@ export async function createHttpResponseFile(options: .setHeader('Content-Length', stats.size.toString()) .setHeader( 'Content-Disposition', - (options.forceDownload ? 'attachement' : 'inline') + (options.forceDownload ? 'attachment' : 'inline') + `; filename="${options.filename || file}"` ); @@ -417,7 +417,7 @@ export function isHttpResponseNoContent(obj: any): obj is HttpResponseNoContent */ export abstract class HttpResponseRedirection extends HttpResponse { /** - * Property used internally by isHttpResponseRediction. + * Property used internally by isHttpResponseRedirection. * * @memberof HttpResponseRedirection */ @@ -865,6 +865,52 @@ export function isHttpResponseConflict(obj: any): obj is HttpResponseConflict { (typeof obj === 'object' && obj !== null && obj.isHttpResponseConflict === true); } +/** + * Represent an HTTP response with the status 429 - TOO MANY REQUESTS. + * + * @export + * @class HttpResponseTooManyRequests + * @extends {HttpResponseClientError} + */ +export class HttpResponseTooManyRequests extends HttpResponseClientError { + /** + * Property used internally by isHttpResponseTooManyRequests. + * + * @memberof HttpResponseTooManyRequests + */ + readonly isHttpResponseTooManyRequests = true; + statusCode = 429; + statusMessage = 'TOO MANY REQUESTS'; + + /** + * Create an instance of HttpResponseTooManyRequests. + * @param {*} [body] - Optional body of the response. + * @memberof HttpResponseTooManyRequests + */ + constructor(body?: any, options: { stream?: boolean } = {}) { + super(body, options); + } +} + +/** + * Check if an object is an instance of HttpResponseTooManyRequests. + * + * This function is a help when you have several packages using @foal/core. + * Npm can install the package several times, which leads to duplicate class + * definitions. If this is the case, the keyword `instanceof` may return false + * while the object is an instance of the class. This function fixes this + * problem. + * + * @export + * @param {*} obj - The object to check. + * @returns {obj is HttpResponseTooManyRequests} - True if the error is an instance of HttpResponseTooManyRequests. + * False otherwise. + */ +export function isHttpResponseTooManyRequests(obj: any): obj is HttpResponseTooManyRequests { + return obj instanceof HttpResponseTooManyRequests || + (typeof obj === 'object' && obj !== null && obj.isHttpResponseTooManyRequests === true); +} + /* 5xx Server Error */ /** diff --git a/packages/core/src/express/create-middleware.spec.ts b/packages/core/src/express/create-middleware.spec.ts index aa0f776920..52fe1fd66f 100644 --- a/packages/core/src/express/create-middleware.spec.ts +++ b/packages/core/src/express/create-middleware.spec.ts @@ -53,10 +53,14 @@ describe('createMiddleware', () => { }); it('should call the controller method with a context created from the request.', async () => { - let body = {}; + let ctxBody = null; + let params = null; + let body = null; const route: Route = { - controller: { bar: (ctx: Context) => { - body = ctx.request.body; + controller: { bar: (ctx: Context, paramsBody: any, requestBody: any) => { + ctxBody = ctx.request.body; + params = paramsBody; + body = requestBody; return new HttpResponseOK(); }}, hooks: [], @@ -64,14 +68,16 @@ describe('createMiddleware', () => { path: '', propertyKey: 'bar' }; - const request = createRequest({ body: { foo: 'bar' } }); + const request = createRequest({ body: { foo: 'bar' }, params: { id: '1' } }); const response = createResponse(); const middleware = createMiddleware(route, new ServiceManager()); await middleware(request, response, () => {}); - deepStrictEqual(body, request.body); + deepStrictEqual(ctxBody, request.body); + deepStrictEqual(params, request.params, 'The request params should be passed as the second argument.'); + deepStrictEqual(body, request.body, 'The request body should be passed as the third argument.'); }); it('should call the sync and async hooks (with the ctx and the given ServiceManager)' diff --git a/packages/core/src/express/create-middleware.ts b/packages/core/src/express/create-middleware.ts index 9539cf62eb..9e50c4a46a 100644 --- a/packages/core/src/express/create-middleware.ts +++ b/packages/core/src/express/create-middleware.ts @@ -38,7 +38,7 @@ export function createMiddleware(route: Route, services: ServiceManager) { } if (!isHttpResponse(response)) { - response = await route.controller[route.propertyKey](ctx); + response = await route.controller[route.propertyKey](ctx, ctx.request.params, ctx.request.body); } if (!isHttpResponse(response)) { diff --git a/packages/core/src/sessions/session.ts b/packages/core/src/sessions/session.ts index bfde9a89ec..466d34ff76 100644 --- a/packages/core/src/sessions/session.ts +++ b/packages/core/src/sessions/session.ts @@ -37,7 +37,7 @@ export class Session { } /** - * Return true if an element was added/replaces in the session + * Return true if an element was added/replaced in the session * * @readonly * @type {boolean} diff --git a/packages/core/src/sessions/token.hook.spec.ts b/packages/core/src/sessions/token.hook.spec.ts index ebb9627545..6565eedefe 100644 --- a/packages/core/src/sessions/token.hook.spec.ts +++ b/packages/core/src/sessions/token.hook.spec.ts @@ -469,7 +469,6 @@ export function testSuite(Token: typeof TokenRequired|typeof TokenOptional, requ strictEqual(ctx.user, user); }); - // TODO: In versions 2+ of FoalTS, the userID should be of type any. it('with the user retrieved from the database (userId is a MongoDB ObjectID).', async () => { const fetchUser = async (id: number|string) => id === 'xjeldksjqkd' ? user : null; const hook = getHookFunction(Token({ store: Store, user: fetchUser })); diff --git a/packages/csrf/package-lock.json b/packages/csrf/package-lock.json index fb7b5ab8cb..16777a0cdd 100644 --- a/packages/csrf/package-lock.json +++ b/packages/csrf/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/csrf", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/csrf/package.json b/packages/csrf/package.json index e2cbffe397..0a3d5224c9 100644 --- a/packages/csrf/package.json +++ b/packages/csrf/package.json @@ -1,6 +1,6 @@ { "name": "@foal/csrf", - "version": "1.8.1", + "version": "1.9.0", "description": "CSRF protection for FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -53,6 +53,6 @@ "typescript": "~3.5.3" }, "dependencies": { - "@foal/core": "^1.8.1" + "@foal/core": "^1.9.0" } } diff --git a/packages/ejs/package-lock.json b/packages/ejs/package-lock.json index 4dbc915daa..9b7d592cf3 100644 --- a/packages/ejs/package-lock.json +++ b/packages/ejs/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/ejs", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/ejs/package.json b/packages/ejs/package.json index a0209c6171..5d3b8dd239 100644 --- a/packages/ejs/package.json +++ b/packages/ejs/package.json @@ -1,6 +1,6 @@ { "name": "@foal/ejs", - "version": "1.8.1", + "version": "1.9.0", "description": "EJS template package for FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/packages/examples/package-lock.json b/packages/examples/package-lock.json index 32a380f36b..883a38bbc6 100644 --- a/packages/examples/package-lock.json +++ b/packages/examples/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/examples", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/examples/package.json b/packages/examples/package.json index c44142f463..b45dade55f 100644 --- a/packages/examples/package.json +++ b/packages/examples/package.json @@ -1,6 +1,6 @@ { "name": "@foal/examples", - "version": "1.8.1", + "version": "1.9.0", "description": "FoalTs examples", "scripts": { "build": "tsc && copy-cli \"src/**/*.html\" build", @@ -43,19 +43,19 @@ }, "license": "MIT", "dependencies": { - "@foal/aws-s3": "^1.8.1", - "@foal/core": "^1.8.1", - "@foal/social": "^1.8.1", - "@foal/storage": "^1.8.1", - "@foal/swagger": "^1.8.1", - "@foal/typeorm": "^1.8.1", + "@foal/aws-s3": "^1.9.0", + "@foal/core": "^1.9.0", + "@foal/social": "^1.9.0", + "@foal/storage": "^1.9.0", + "@foal/swagger": "^1.9.0", + "@foal/typeorm": "^1.9.0", "source-map-support": "~0.5.16", "sqlite3": "~4.1.0", "typeorm": "~0.2.20", "yamljs": "~0.3.0" }, "devDependencies": { - "@foal/cli": "^1.8.1", + "@foal/cli": "^1.9.0", "@types/mocha": "~2.2.43", "@types/node": "~10.1.1", "concurrently": "~3.5.1", diff --git a/packages/formidable/package-lock.json b/packages/formidable/package-lock.json index ce2693683b..32d8947f8d 100644 --- a/packages/formidable/package-lock.json +++ b/packages/formidable/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/formidable", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/formidable/package.json b/packages/formidable/package.json index f9c7ad58ae..89dd4b909d 100644 --- a/packages/formidable/package.json +++ b/packages/formidable/package.json @@ -1,6 +1,6 @@ { "name": "@foal/formidable", - "version": "1.8.1", + "version": "1.9.0", "description": "Small package to use formidable with promises", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -43,7 +43,7 @@ "lib/" ], "dependencies": { - "@foal/core": "^1.8.1" + "@foal/core": "^1.9.0" }, "devDependencies": { "@types/mocha": "~2.2.43", diff --git a/packages/graphql/package-lock.json b/packages/graphql/package-lock.json index 2cdf41a0a4..ea3b32fb06 100644 --- a/packages/graphql/package-lock.json +++ b/packages/graphql/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/graphql", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/graphql/package.json b/packages/graphql/package.json index b32f78de86..befe860cee 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@foal/graphql", - "version": "1.8.1", + "version": "1.9.0", "description": "GraphQL integration for FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -41,7 +41,7 @@ "lib/" ], "dependencies": { - "@foal/core": "^1.8.1", + "@foal/core": "^1.9.0", "ajv": "~6.12.0", "glob": "~7.1.4" }, diff --git a/packages/internal-test/package-lock.json b/packages/internal-test/package-lock.json index 6c3596a84a..444d11746d 100644 --- a/packages/internal-test/package-lock.json +++ b/packages/internal-test/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/internal-test", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/internal-test/package.json b/packages/internal-test/package.json index 57447848e5..6015cbf713 100644 --- a/packages/internal-test/package.json +++ b/packages/internal-test/package.json @@ -1,7 +1,7 @@ { "name": "@foal/internal-test", "private": true, - "version": "1.8.1", + "version": "1.9.0", "description": "Unpublished package used to run some tests.", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/packages/jwks-rsa/package-lock.json b/packages/jwks-rsa/package-lock.json index 1f037fb821..134cd9541e 100644 --- a/packages/jwks-rsa/package-lock.json +++ b/packages/jwks-rsa/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/jwks-rsa", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/jwks-rsa/package.json b/packages/jwks-rsa/package.json index 2893772bdc..02d1e164cc 100644 --- a/packages/jwks-rsa/package.json +++ b/packages/jwks-rsa/package.json @@ -1,6 +1,6 @@ { "name": "@foal/jwks-rsa", - "version": "1.8.1", + "version": "1.9.0", "description": "Integration of the library jwks-rsa with FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -51,8 +51,8 @@ "@foal/jwt": "^1.2.0" }, "devDependencies": { - "@foal/core": "^1.8.1", - "@foal/jwt": "^1.8.1", + "@foal/core": "^1.9.0", + "@foal/jwt": "^1.9.0", "@types/mocha": "~2.2.43", "@types/node": "~10.5.6", "mocha": "~5.2.0", diff --git a/packages/jwt/package-lock.json b/packages/jwt/package-lock.json index 84983b6aea..8dbc4ba5e4 100644 --- a/packages/jwt/package-lock.json +++ b/packages/jwt/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/jwt", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 5663cb1356..4f321348db 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@foal/jwt", - "version": "1.8.1", + "version": "1.9.0", "description": "Authentication with JWT for FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -43,7 +43,7 @@ "lib/" ], "dependencies": { - "@foal/core": "^1.8.1", + "@foal/core": "^1.9.0", "@types/jsonwebtoken": "~8.3.0", "jsonwebtoken": "~8.5.0" }, diff --git a/packages/mongodb/package-lock.json b/packages/mongodb/package-lock.json index f3a5528830..4d95003424 100644 --- a/packages/mongodb/package-lock.json +++ b/packages/mongodb/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/mongodb", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/mongodb/package.json b/packages/mongodb/package.json index c3e0f33d3c..e1ec8ef85b 100644 --- a/packages/mongodb/package.json +++ b/packages/mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@foal/mongodb", - "version": "1.8.1", + "version": "1.9.0", "description": "MongoDB package for FoalTS session", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -46,7 +46,7 @@ "lib/" ], "dependencies": { - "@foal/core": "^1.8.1", + "@foal/core": "^1.9.0", "mongodb": "~3.5.0" }, "devDependencies": { diff --git a/packages/mongoose/package-lock.json b/packages/mongoose/package-lock.json index f1b5f7a1ab..82196b0b40 100644 --- a/packages/mongoose/package-lock.json +++ b/packages/mongoose/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/mongoose", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/mongoose/package.json b/packages/mongoose/package.json index fa6ae4144b..a9b3ebc548 100644 --- a/packages/mongoose/package.json +++ b/packages/mongoose/package.json @@ -1,6 +1,6 @@ { "name": "@foal/mongoose", - "version": "1.8.1", + "version": "1.9.0", "description": "FoalTS integration of Mongoose", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/packages/mongoose/src/utils/fetch-user.util.spec.ts b/packages/mongoose/src/utils/fetch-user.util.spec.ts index ad63246a0b..447b09b46d 100644 --- a/packages/mongoose/src/utils/fetch-user.util.spec.ts +++ b/packages/mongoose/src/utils/fetch-user.util.spec.ts @@ -32,6 +32,15 @@ describe('fetchUser', () => { after(() => disconnect()); + it('should throw an Error if the ID is a number.', async () => { + try { + await fetchUser(User)(46); + throw new Error('An error should have been thrown'); + } catch (error) { + strictEqual(error.message, 'Unexpected type for MongoDB user ID: number.'); + } + }); + it('should return the user fetched from the database.', async () => { const actual = await fetchUser(User)(user._id.toString()); notStrictEqual(actual, undefined); diff --git a/packages/mongoose/src/utils/fetch-user.util.ts b/packages/mongoose/src/utils/fetch-user.util.ts index bc0e7a93c2..a4e6f53df1 100644 --- a/packages/mongoose/src/utils/fetch-user.util.ts +++ b/packages/mongoose/src/utils/fetch-user.util.ts @@ -15,6 +15,9 @@ */ export function fetchUser(userModel: any): (id: number|string) => Promise { return (id: number|string) => { + if (typeof id === 'number') { + throw new Error('Unexpected type for MongoDB user ID: number.'); + } return new Promise((resolve, reject) => { userModel.findOne({ _id: id }, (err: any, res: any) => { if (err) { diff --git a/packages/password/package-lock.json b/packages/password/package-lock.json index c834f6b048..80fa699a48 100644 --- a/packages/password/package-lock.json +++ b/packages/password/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/password", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/password/package.json b/packages/password/package.json index 85620046bc..af65d74e8f 100644 --- a/packages/password/package.json +++ b/packages/password/package.json @@ -1,6 +1,6 @@ { "name": "@foal/password", - "version": "1.8.1", + "version": "1.9.0", "description": "Password utilities for FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/packages/redis/package-lock.json b/packages/redis/package-lock.json index 53760e2d7b..29cecce30d 100644 --- a/packages/redis/package-lock.json +++ b/packages/redis/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/redis", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/redis/package.json b/packages/redis/package.json index 01f065ca35..49381239d6 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -1,6 +1,6 @@ { "name": "@foal/redis", - "version": "1.8.1", + "version": "1.9.0", "description": "Redis sessions for FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -42,7 +42,7 @@ "lib/" ], "dependencies": { - "@foal/core": "^1.8.1", + "@foal/core": "^1.9.0", "redis": "~3.0.2" }, "devDependencies": { diff --git a/packages/social/package-lock.json b/packages/social/package-lock.json index 8be1578ecc..b8c3c18f96 100644 --- a/packages/social/package-lock.json +++ b/packages/social/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/social", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/social/package.json b/packages/social/package.json index 1e6bcd1add..d54cfe9ef0 100644 --- a/packages/social/package.json +++ b/packages/social/package.json @@ -1,6 +1,6 @@ { "name": "@foal/social", - "version": "1.8.1", + "version": "1.9.0", "description": "Social authentication for FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -53,7 +53,7 @@ "lib/" ], "dependencies": { - "@foal/core": "^1.8.1", + "@foal/core": "^1.9.0", "node-fetch": "~2.6.0" }, "devDependencies": { diff --git a/packages/storage/package-lock.json b/packages/storage/package-lock.json index 194d908430..a0de06e843 100644 --- a/packages/storage/package-lock.json +++ b/packages/storage/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/storage", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/storage/package.json b/packages/storage/package.json index fa05b9630b..4faf423627 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -1,6 +1,6 @@ { "name": "@foal/storage", - "version": "1.8.1", + "version": "1.9.0", "description": "Storage components for FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -45,14 +45,14 @@ "lib/" ], "dependencies": { - "@foal/core": "^1.8.1", + "@foal/core": "^1.9.0", "@types/busboy": "~0.2.3", "busboy": "~0.3.1", "mime": "~2.4.4", "pump": "~3.0.0" }, "devDependencies": { - "@foal/internal-test": "^1.8.1", + "@foal/internal-test": "^1.9.0", "@types/mocha": "~2.2.43", "@types/node": "~10.5.6", "@types/supertest": "~2.0.8", diff --git a/packages/storage/src/abstract-disk.service.spec.ts b/packages/storage/src/abstract-disk.service.spec.ts index e1abde31e1..75231a6702 100644 --- a/packages/storage/src/abstract-disk.service.spec.ts +++ b/packages/storage/src/abstract-disk.service.spec.ts @@ -132,13 +132,13 @@ describe('AbstractDisk', () => { response = await disk.createHttpResponse(path, { forceDownload: true }); - strictEqual(response.getHeader('Content-Disposition'), 'attachement; filename="test-file.png"'); + strictEqual(response.getHeader('Content-Disposition'), 'attachment; filename="test-file.png"'); response = await disk.createHttpResponse(path, { filename: 'download.png', forceDownload: true, }); - strictEqual(response.getHeader('Content-Disposition'), 'attachement; filename="download.png"'); + strictEqual(response.getHeader('Content-Disposition'), 'attachment; filename="download.png"'); }); }); diff --git a/packages/storage/src/abstract-disk.service.ts b/packages/storage/src/abstract-disk.service.ts index ad27f2486a..e8d4a534f6 100644 --- a/packages/storage/src/abstract-disk.service.ts +++ b/packages/storage/src/abstract-disk.service.ts @@ -134,7 +134,7 @@ export abstract class AbstractDisk { .setHeader('Content-Length', size.toString()) .setHeader( 'Content-Disposition', - (options.forceDownload ? 'attachement' : 'inline') + (options.forceDownload ? 'attachment' : 'inline') + `; filename="${options.filename || basename(path)}"` ); } diff --git a/packages/swagger/package-lock.json b/packages/swagger/package-lock.json index 688be2af27..277fd8f901 100644 --- a/packages/swagger/package-lock.json +++ b/packages/swagger/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/swagger", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/swagger/package.json b/packages/swagger/package.json index 40b6700e56..c93a37f9aa 100644 --- a/packages/swagger/package.json +++ b/packages/swagger/package.json @@ -1,6 +1,6 @@ { "name": "@foal/swagger", - "version": "1.8.1", + "version": "1.9.0", "description": "Swagger UI for FoalTS", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -44,7 +44,7 @@ "lib/" ], "dependencies": { - "@foal/core": "^1.8.1", + "@foal/core": "^1.9.0", "swagger-ui-dist": "~3.25.0" }, "devDependencies": { diff --git a/packages/typeorm/package-lock.json b/packages/typeorm/package-lock.json index b7c4ea292f..0eb57ec96d 100644 --- a/packages/typeorm/package-lock.json +++ b/packages/typeorm/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/typeorm", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -269,6 +269,16 @@ "integrity": "sha512-eJzYkFYy9L4JzXsbymsFn3p54D+llV27oTQ+ziJG7WFRheJcNZilgVXMG0LoZtlQSKBsJdWtLFqOD0u+U0jZKA==", "dev": true }, + "bl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", + "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", + "dev": true, + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -291,6 +301,12 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "bson": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.4.tgz", + "integrity": "sha512-S/yKGU1syOMzO86+dGpg2qGoDL0zvzcb262G+gqEy6TgP6rt6z6qxSFX/8X6vLC91P7G7C3nLs0+bvDzmvBA3Q==", + "dev": true + }, "buffer": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", @@ -616,6 +632,12 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==", + "dev": true + }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -1341,6 +1363,13 @@ "mimic-fn": "^1.0.0" } }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "dev": true, + "optional": true + }, "mime-db": { "version": "1.42.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", @@ -1424,6 +1453,20 @@ "supports-color": "5.4.0" } }, + "mongodb": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.5.7.tgz", + "integrity": "sha512-lMtleRT+vIgY/JhhTn1nyGwnSMmJkJELp+4ZbrjctrnBxuLbj6rmLuJFz8W2xUzUqWmqoyVxJLYuC58ZKpcTYQ==", + "dev": true, + "requires": { + "bl": "^2.2.0", + "bson": "^1.1.4", + "denque": "^1.4.1", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -2023,6 +2066,24 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "dev": true, + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "resolve": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz", @@ -2032,6 +2093,12 @@ "path-parse": "^1.0.6" } }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=", + "dev": true + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -2069,6 +2136,16 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "dev": true, + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -2134,6 +2211,16 @@ "source-map": "^0.5.6" } }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "dev": true, + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, "split": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", diff --git a/packages/typeorm/package.json b/packages/typeorm/package.json index aae3ad8dff..58aeb352c2 100644 --- a/packages/typeorm/package.json +++ b/packages/typeorm/package.json @@ -1,6 +1,6 @@ { "name": "@foal/typeorm", - "version": "1.8.1", + "version": "1.9.0", "description": "FoalTS integration of TypeORM", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -49,7 +49,7 @@ "lib/" ], "dependencies": { - "@foal/core": "^1.8.1" + "@foal/core": "^1.9.0" }, "peerDependencies": { "typeorm": "^0.2.6" @@ -58,6 +58,7 @@ "@types/mocha": "~2.2.43", "@types/node": "~10.5.6", "mocha": "~5.2.0", + "mongodb": "~3.5.0", "mysql": "~2.16.0", "pg": "~7.7.1", "rimraf": "~2.6.2", diff --git a/packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts b/packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts new file mode 100644 index 0000000000..8bccf1fc51 --- /dev/null +++ b/packages/typeorm/src/utils/fetch-mongodb-user.util.spec.ts @@ -0,0 +1,82 @@ +// std +import { notStrictEqual, strictEqual } from 'assert'; + +// 3p +import { Column, createConnection, Entity, getConnection, getMongoManager, ObjectID, ObjectIdColumn } from 'typeorm'; + +// FoalTS +import { fetchMongoDBUser } from './fetch-mongodb-user.util'; + +describe('fetchMongoDBUser', () => { + + @Entity() + class User { + @ObjectIdColumn() + id: ObjectID; + + @Column() + name: string; + } + + @Entity() + class User2 { + @ObjectIdColumn() + // tslint:disable-next-line:variable-name + _id: ObjectID; + + @Column() + name: string; + } + + let user: User; + let user2: User2; + + before(async () => { + await createConnection({ + database: 'test', + dropSchema: true, + entities: [User, User2], + host: 'localhost', + port: 27017, + synchronize: true, + type: 'mongodb', + }); + + user = new User(); + user.name = 'foobar'; + await getMongoManager().save(user); + + user2 = new User2(); + user2.name = 'foobar2'; + await getMongoManager().save(user2); + }); + + after(() => getConnection().close()); + + it('should throw an Error if the ID is a number.', async () => { + try { + await fetchMongoDBUser(User)(46); + throw new Error('An error should have been thrown'); + } catch (error) { + strictEqual(error.message, 'Unexpected type for MongoDB user ID: number.'); + } + }); + + it('should return the user fetched from the database (id).', async () => { + const actual = await fetchMongoDBUser(User)(user.id.toString()); + notStrictEqual(actual, undefined); + strictEqual(user.id.equals(actual.id), true); + }); + + it('should return the user fetched from the database (_id).', async () => { + const actual = await fetchMongoDBUser(User2)(user2._id.toString()); + notStrictEqual(actual, undefined); + strictEqual(user2._id.equals(actual._id), true); + }); + + it('should return undefined if no user is found in the database (string).', async () => { + const actual = await fetchMongoDBUser(User)('5c584690ba14b143235f195d'); + strictEqual(actual, undefined); + }); + +}); diff --git a/packages/typeorm/src/utils/fetch-mongodb-user.util.ts b/packages/typeorm/src/utils/fetch-mongodb-user.util.ts new file mode 100644 index 0000000000..f039963637 --- /dev/null +++ b/packages/typeorm/src/utils/fetch-mongodb-user.util.ts @@ -0,0 +1,30 @@ +// 3p +import { Class } from '@foal/core'; +// tslint:disable-next-line:no-unused-variable +import { getMongoRepository, ObjectID } from 'typeorm'; + +/** + * Create a function that finds the first MongoDB entity that matches some id. + * + * It returns undefined if no entity can be found. + * + * This function is usually used by: + * - TokenRequired (@foal/core) + * - TokenOptional (@foal/core) + * - JWTRequired (@foal/jwt) + * - JWTOptional (@foal/jwt) + * + * @export + * @param {(Class<{ id: ObjectID }|{ _id: ObjectID }>)} userEntityClass - The entity class. + * @returns {((id: number|string) => Promise)} The returned function expecting an id. + */ +export function fetchMongoDBUser( + userEntityClass: Class<{ id: ObjectID }|{ _id: ObjectID }> +): (id: number|string) => Promise { + return (id: number|string) => { + if (typeof id === 'number') { + throw new Error('Unexpected type for MongoDB user ID: number.'); + } + return getMongoRepository(userEntityClass).findOne(id); + }; +} diff --git a/packages/typeorm/src/utils/index.ts b/packages/typeorm/src/utils/index.ts index 3ac7a2f278..29704ac32a 100644 --- a/packages/typeorm/src/utils/index.ts +++ b/packages/typeorm/src/utils/index.ts @@ -1,2 +1,3 @@ +export { fetchMongoDBUser } from './fetch-mongodb-user.util'; export { fetchUserWithPermissions } from './fetch-user-with-permissions.util'; export { fetchUser } from './fetch-user.util'; diff --git a/packages/typestack/package-lock.json b/packages/typestack/package-lock.json index 6a3e8a07f1..31c6c0b027 100644 --- a/packages/typestack/package-lock.json +++ b/packages/typestack/package-lock.json @@ -1,6 +1,6 @@ { "name": "@foal/typestack", - "version": "1.8.1", + "version": "1.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/typestack/package.json b/packages/typestack/package.json index 124724f6c2..fcf777b474 100644 --- a/packages/typestack/package.json +++ b/packages/typestack/package.json @@ -1,6 +1,6 @@ { "name": "@foal/typestack", - "version": "1.8.1", + "version": "1.9.0", "description": "FoalTS for validation and serialization using TypeStack classes", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -49,7 +49,7 @@ "class-validator": "^0.10.0" }, "dependencies": { - "@foal/core": "^1.8.1" + "@foal/core": "^1.9.0" }, "devDependencies": { "@types/mocha": "~2.2.43", diff --git a/tslint.json b/tslint.json index 2546555455..9d6b58706f 100644 --- a/tslint.json +++ b/tslint.json @@ -25,10 +25,11 @@ "linterOptions": { "exclude": [ "**/src/migrations/*.ts", - "**/src/generate/mocks/**/*.ts", + "**/src/generate/fixtures/**/*.ts", "**/src/generate/templates/model/*.ts", "**/src/generate/templates/rest-api/*.ts", - "**/src/generate/specs/**/app.controller.*.ts", + "**/src/generate/specs/**/app.controller.ts", + "**/src/generate/specs/**/api.controller.ts", "**/src/generate/specs/entity/test-foo-bar.entity.ts", "**/src/generate/templates/entity/entity.ts" ]