diff --git a/.github/CONTRIBUTING.MD b/.github/CONTRIBUTING.MD index 56719bd929..c44b99f455 100644 --- a/.github/CONTRIBUTING.MD +++ b/.github/CONTRIBUTING.MD @@ -4,43 +4,86 @@ Thanks for your interest in FoalTS! There are several ways to contribute. **Reporting bugs are greatly appreciated**, so do not hesitate to open an issue/PR for that! -- [Submit an issue](#submit-an-issue) -- [Submit a PR](#submit-a-pr) -- [Security](#security) - -## Submit an issue - -If you find a security vulnerability, please do NOT open an issue. Email loic.poullain@centraliens.net instead. - -- [Report a bug](https://github.com/FoalTS/foal/issues/new) -- [Suggest a new feature](https://github.com/FoalTS/foal/issues/new) -- [Other (ask a question, etc)](https://github.com/FoalTS/foal/issues/new) - -## Submit a PR - -If the PR is about code (not documentation), please submit an issue first to discuss on this. There are also [pending issues](https://github.com/FoalTS/foal/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) that may require your help. - -### Set up the development/test environment - -1. Install [docker](https://www.docker.com/). -2. Install [lerna](https://lernajs.io/) by running `npm i -g lerna` -3. Start the dev/test environment by running `npm run start-docker` (to stop use `npm run stop-docker`) - -### Install dependencies - -Run `lerna bootstrap`. - -### Run tests and linting - -Run `lerna run --no-bail test` and `npm run lint` from the root directory. - -You can also run the tests of only one package by going to its directory and running `npm run test` or `npm run dev:test` (watch mode). - -### General guidelines - -Do not install any new dependencies unless they have been approved. Dependencies (except peer ones) should point to *minor* versions (`~1.2.0` instead of `^1.2.0`). - -When writting code, use the *Test-Driven Developpement (TDD)* approach. +## Security Vulnerabilities + +If you think you have found a security hole, please do NOT submit an issue but send an email directly to loic.poullain@centraliens.net. + +## Pull Requests + +There are [pending issues](https://github.com/FoalTS/foal/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) that may require your help. + +If you wish to submit a PR, please first submit an issue for discussion (or add a comment on an existing issue). + +PRs that correct grammatical errors or small bugs can be submitted directly. + +## Development Environment + +The framework development environment uses [lerna](https://lernajs.io/) for managing packages and [docker](https://www.docker.com/) for database provisioning. + +**Steps:** +1. Install docker. +2. Install lerna + ``` + npm install -g lerna + ``` +3. Start the databases. + ```sh + npm run start-docker # use `npm run stop-docker` to stop them + ``` +4. Install the root dependencies. + ``` + npm install + ``` +5. Install the dependencies of each package and build each package. + ``` + lerna bootstrap + ``` +6. Check code format. + ``` + npm run lint + ``` +7. Run all the tests. + ``` + lerna run --no-bail test + ``` + +Tests can also be run individually for each package using `npm run test` or `npm run dev:test` (watch mode) at the root of the package directory. + +## Dependency Policy + +**Do not add new dependencies** (unless they have been improved). Do not install `@types` packages. + +FoalTS is based on very few dependencies for all these reasons: +- Adding a new dependency often means installing many other packages on which it depends. This phenomenon is often referred to as a *black hole* in Node's ecosystem. + - The size of the `node_modules` directory grows very fast. This can slow down deployment and cause problems if a size limit is imposed on the directory (e.g. in a serverless architecture). + - Due to the large number of dependencies to load, the application may be slow to start. + - The application is more vulnerable to the release of malicious packages. This is what happened on July 12, 2018 when an [attacker compromised the npm account](https://eslint.org/blog/2018/07/postmortem-for-malicious-package-publishes) of an ESLint maintainer. +- We have no guarantee that the maintainers follow the same Foal safety rules (2FA enabled on both Github and npm). +- When a new version of an external package is released (bug fixes, security updates, new features, etc.), it takes time to review each change made in the new version and time to verify that the framework still works as expected with it. +- Packages may support different versions of TypeScript and Node than those supported by the framework. +- External packages can become unmaintained. +- Semantic versioning is not always respected, which is problematic if we want to integrate a security update without introducing breaking changes. +- If we need a new feature in the external dependency, it may take time for the maintainer(s) to implement it. The feature may also be rejected. +- The `@types` packages very often lead to issues. + - The types may be outdated with respect to the current version. + - Semantic versioning is often not respected, which causes the code to break between two *patch* versions. + - Type choices may be arbitrary and not decided by the official maintainers. + - Two packages using the same `@types` module but with different versions may not work properly together. + - Type packages depend on each other by specifying `*` as the version number which causes incompatibilities and great difficulty in defining a replicable environment. +- The installation is often polluted by messages of indirect dependencies in search of funds. + +Some packages, however, can override this policy and be installed if they meet one of the following criteria: +- Rewriting the entire package would require too much work and would be difficult to maintain in the long term. Examples: `TypeORM`, `Mongoose`. +- The code requires very specific knowledge. Examples: `pump`, `jsonwebtoken`, `TypeORM`, `Mongoose`. +- The packages are base packages of the Express.Js framework and can therefore be considered stable, safe and mature. Examples: `cookie-parser`, `morgan`. + +> Dependencies (except peer ones) should point to *minor* versions (`~1.2.0` instead of `^1.2.0`). + +## Testing and Documentation Policy + +**Testing and documentating the framework is put on a very high priority**. Each line of code must be tested. It is okay to delay the release of a new version if it is to ensure that it is based on robust testing. + +If you wish to submit a PR, please use the *Test-Driven Developpement (TDD)* approach: 1. Write a test. 2. Check that the test fails. 3. Write just enough code to make the test pass. @@ -49,28 +92,45 @@ When writting code, use the *Test-Driven Developpement (TDD)* approach. This method may seem cumbersome at first glance, but it ensures that every line of code in the framework is tested. Reviewers must pull the branch and verify that the tests are actually testing something. If they change even one line of code, they must see that at least one of the tests fails. -**A PR without tests is automatically rejected.** +A PR without robust tests is automatically rejected. -## Security +## Semantic Versioning -To report a security issue please email directly loic.poullain@centraliens.net. +The framework follows the semantic versioning specification. -## Project Structure +| Code status | Stage | Example version | +| --- | --- | --- | +| Backward compatible bug fixes | Patch release | 1.0.1 | +| Backward compatible new features | Minor release | 1.1.0 | +| Changes that break backward compatibility | Major release | 2.0.0 | -The FoalTS project consists of several packages. The publication and dependency management is handled by [lerna](https://github.com/lerna/lerna), a tool for managing JavaScript projects with multiple packages. +## Long-Term Support Policy and Schedule + +All of major releases are supported for 18 months. +- 12 months of *active support* (new features, bug fixes, etc). +- 6 months of *maintenance (LTS)* (critical fixes and security patches). + +| Release | Status | Active Start | Maintenance Start | End-of-life | Node min version | TS min version | +| --- | --- | --- | --- | --- | --- | --- | +| 2.x | *Pending* | Summer 2020 | Summer 2021 | 2021-12-31 | 10.x | 3.5 | +| 1.x | *Active* | 2019-07-11 | Summer 2020 | 2020-12-31 | 8.x | 3.5 | +| 0.8 | *End-of-Life* | 2019-02-16 | - | 2019-07-11 | 8.x | 2.9 | + +## Project Architecture ### `@foal/cli` Package Structure -```sh -generate # Handles the commands `foal createapp` and `foal generate` - |- generators # Contains the code which renders the templates or updates the files - |- mocks # 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 -``` +The directory `src/generate/` contains the source code of the commands `foal createapp` and `foal generate`. + +Here is the list of its sub-directories: -Usually components that do not rely on a third-party library are located in the `@foal/core` package. +| Directory | Description | +| --- | --- | +| generators | Contains the code which renders the templates or updates the files | +| 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 | ## Conventions diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 251c1f906d..84ecfe816b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,10 @@ name: Test -on: [push] +on: + push: + branches: + - master + pull_request: jobs: build: @@ -12,9 +16,6 @@ jobs: node-version: [10, 12] 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/README.md b/README.md index efe7309c99..fbe0040adf 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@

Website - - Documentation + Documentation - Twitter - @@ -66,7 +66,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -:point_right: [Continue with the tutorial](https://foalts.gitbook.io/docs/content/) :seedling: +:point_right: [Continue with the tutorial](https://foalts.gitbook.io/docs/) :seedling: ![Screenshot](./docs/screenshot.png) diff --git a/benchmarks/run.sh b/benchmarks/run.sh index ddef1c64cb..8c98a0fc82 100755 --- a/benchmarks/run.sh +++ b/benchmarks/run.sh @@ -1,4 +1,5 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh +set -e # Empty benchmark.txt if it exists. :> benchmark.txt 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/architecture/services-and-dependency-injection.md b/docs/architecture/services-and-dependency-injection.md index eed4c41d48..01837efdd3 100644 --- a/docs/architecture/services-and-dependency-injection.md +++ b/docs/architecture/services-and-dependency-injection.md @@ -295,9 +295,10 @@ export class Logger implements ILogger { *src/index.ts (example)* ```typescript import { createApp, ServiceManager } from '@foal/core'; -import { Connection, createConnection } from 'typeorm'; +import { createConnection } from 'typeorm'; import { AppController } from './app/app.controller'; +import { Product } from './app/entities'; import { Logger } from './app/services'; async function main() { @@ -326,7 +327,7 @@ import { Repository } from 'typeorm'; import { Product } from '../entities'; import { ILogger } from '../services'; -class ApiController { +export class ApiController { @Dependency('product') productRepository: Repository; diff --git a/docs/authentication-and-access-control/groups-and-permissions.md b/docs/authentication-and-access-control/groups-and-permissions.md index 8f3e596a5c..1bff14cc56 100644 --- a/docs/authentication-and-access-control/groups-and-permissions.md +++ b/docs/authentication-and-access-control/groups-and-permissions.md @@ -220,6 +220,7 @@ export class User extends UserWithPermissions { } +// You MUST export Group and Permission so that TypeORM can generate migrations. export { Group, Permission } from '@foal/typeorm'; ``` 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/docs/frontend-integration/angular-react-vue.md b/docs/frontend-integration/angular-react-vue.md index adef3a42eb..2c0fa0b823 100644 --- a/docs/frontend-integration/angular-react-vue.md +++ b/docs/frontend-integration/angular-react-vue.md @@ -32,7 +32,7 @@ mkdir my-app cd my-app foal createapp backend -npx create-react-app frontend --typescript +npx create-react-app frontend --template typescript cd backend foal connect react ../frontend diff --git a/docs/tutorials/multi-user-todo-list/3-the-shell-scripts.md b/docs/tutorials/multi-user-todo-list/3-the-shell-scripts.md index 0ce29989d6..1e1c299e14 100644 --- a/docs/tutorials/multi-user-todo-list/3-the-shell-scripts.md +++ b/docs/tutorials/multi-user-todo-list/3-the-shell-scripts.md @@ -26,7 +26,7 @@ export const schema = { type: 'object', }; -export async function main(args: { email: string, password: string }) { +export async function main(args: { email: string; password: string }) { const connection = await createConnection(); try { const user = new User(); @@ -102,7 +102,7 @@ export const schema = { type: 'object', }; -export async function main(args: { owner: string, text: string }) { +export async function main(args: { owner: string; text: string }) { const connection = await createConnection(); try { const user = await connection.getRepository(User).findOne({ email: args.owner }); diff --git a/e2e_test.sh b/e2e_test.sh index 544fb7a5ef..5605ec7deb 100755 --- a/e2e_test.sh +++ b/e2e_test.sh @@ -1,8 +1,11 @@ +#!/usr/bin/env bash +set -e + mkdir e2e-test-temp cd e2e-test-temp # Test app creation -foal createapp my-app || exit 1 +foal createapp my-app cd my-app # Check some compilation errors @@ -12,34 +15,34 @@ if grep -Ril "../../Users/loicp" .; then fi # Test the generators -foal g entity flight || exit 1 -foal g hook foo-bar || exit 1 -foal g service foo || exit 1 -foal g controller bar --register || exit 1 -foal g rest-api product --register || exit 1 -foal g sub-app bar-foo || exit 1 -foal g script bar-script || exit 1 +foal g entity flight +foal g hook foo-bar +foal g service foo +foal g controller bar --register +foal g rest-api product --register +foal g sub-app bar-foo +foal g script bar-script # Test linting -npm run lint || exit 1 +npm run lint # Build and run the unit tests -npm run build:test || exit 1 -npm run start:test || exit 1 +npm run build:test +npm run start:test # Make and run the migrations -npm run makemigrations || exit 1 -npm run migrations || exit 1 +npm run makemigrations +npm run migrations # Build and run the e2e tests -npm run build:e2e || exit 1 -npm run start:e2e || exit 1 +npm run build:e2e +npm run start:e2e # Build the app -npm run build || exit 1 +npm run build # Test the application when it is started -pm2 start build/index.js || exit 1 +pm2 start build/index.js sleep 1 response=$( curl http://localhost:3001 \ @@ -113,10 +116,10 @@ test_rest_api DELETE "http://localhost:3001/products/1" 204 test_rest_api DELETE "http://localhost:3001/products/1" 404 test_rest_api DELETE "http://localhost:3001/products/ab" 400 -pm2 delete index || exit 1 +pm2 delete index # Test the default shell scripts to create users. -foal run create-user || exit 1 +foal run create-user ################################################################# # Repeat (almost) the same tests with a Mongoose and YAML project @@ -125,7 +128,7 @@ foal run create-user || exit 1 cd .. # Test app creation -foal createapp my-mongodb-app --mongodb --yaml || exit 1 +foal createapp my-mongodb-app --mongodb --yaml cd my-mongodb-app # Check some compilation errors @@ -135,24 +138,24 @@ if grep -Ril "../../Users/loicp" .; then fi # Test the generators -foal g model flight || exit 1 +foal g model flight # Test linting -npm run lint || exit 1 +npm run lint # Build and run the unit tests -npm run build:test || exit 1 -npm run start:test || exit 1 +npm run build:test +npm run start:test # Build and run the e2e tests -npm run build:e2e || exit 1 -npm run start:e2e || exit 1 +npm run build:e2e +npm run start:e2e # Build the app npm run build || exit 1 # Test the application when it is started -PORT=3001 pm2 start build/index.js || exit 1 +PORT=3001 pm2 start build/index.js sleep 1 response=$( curl http://localhost:3001 \ @@ -162,7 +165,7 @@ response=$( ) test "$response" -ge 200 && test "$response" -le 299 -pm2 delete index || exit 1 +pm2 delete index # Test the default shell scripts to create users. -foal run create-user || exit 1 +foal run create-user 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/README.md b/packages/aws-s3/README.md index 34daf45430..41a503ee0a 100644 --- a/packages/aws-s3/README.md +++ b/packages/aws-s3/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 eb236d972d..a9659171e2 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/README.md b/packages/cli/README.md index c09e7f9b33..3881c7a1f2 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 fad270c038..53e95e04b7 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 d37a4639bc..449988b5d4 100644 --- a/packages/cli/src/generate/generators/app/create-app.spec.ts +++ b/packages/cli/src/generate/generators/app/create-app.spec.ts @@ -1,253 +1,269 @@ -// 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.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.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.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.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') - .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') + .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') - .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') + .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 e999723d97..892833f668 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,93 +50,121 @@ 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') - .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') + .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 d6ed4cffdd..a4549d815f 100644 --- a/packages/cli/src/generate/specs/app/package.json +++ b/packages/cli/src/generate/specs/app/package.json @@ -30,7 +30,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", @@ -40,8 +40,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 792862208c..0ddc1f9977 100644 --- a/packages/cli/src/generate/specs/app/package.mongodb.json +++ b/packages/cli/src/generate/specs/app/package.mongodb.json @@ -35,8 +35,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 ece7168946..a850f3595d 100644 --- a/packages/cli/src/generate/specs/app/package.mongodb.yaml.json +++ b/packages/cli/src/generate/specs/app/package.mongodb.yaml.json @@ -36,8 +36,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 1ef99fd78c..d54c7137ce 100644 --- a/packages/cli/src/generate/specs/app/package.yaml.json +++ b/packages/cli/src/generate/specs/app/package.yaml.json @@ -30,7 +30,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": { @@ -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/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 1484e90396..b04bf8100b 100644 --- a/packages/cli/src/generate/templates/app/package.json +++ b/packages/cli/src/generate/templates/app/package.json @@ -30,7 +30,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", @@ -40,8 +40,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 00aac09855..b9118b62df 100644 --- a/packages/cli/src/generate/templates/app/package.mongodb.json +++ b/packages/cli/src/generate/templates/app/package.mongodb.json @@ -35,8 +35,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 3db6429ba1..6569d10566 100644 --- a/packages/cli/src/generate/templates/app/package.mongodb.yaml.json +++ b/packages/cli/src/generate/templates/app/package.mongodb.yaml.json @@ -36,8 +36,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 db73aa7687..ebb0a2c753 100644 --- a/packages/cli/src/generate/templates/app/package.yaml.json +++ b/packages/cli/src/generate/templates/app/package.yaml.json @@ -30,7 +30,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": { @@ -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/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 3dbc80405d..c55b8b3a1f 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/README.md b/packages/core/README.md index 0cdd0ad009..fbde7e410a 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -58,7 +58,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 bdc3bf1197..4b6441837b 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 b7b70e93d2..ab3bfb9f34 100644 --- a/packages/core/src/sessions/session.ts +++ b/packages/core/src/sessions/session.ts @@ -44,7 +44,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 7b5576e566..b90c14f912 100644 --- a/packages/core/src/sessions/token.hook.spec.ts +++ b/packages/core/src/sessions/token.hook.spec.ts @@ -467,7 +467,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/README.md b/packages/csrf/README.md index a033d203ca..00e3ba07ec 100644 --- a/packages/csrf/README.md +++ b/packages/csrf/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 5b079ff523..e60741eaa3 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/README.md b/packages/ejs/README.md index 4863de32c3..63912682d5 100644 --- a/packages/ejs/README.md +++ b/packages/ejs/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 8fcd4326ef..74380d9258 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 652c3e56cb..4e1af4f5a0 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/README.md b/packages/formidable/README.md index 97633447f6..b543cbeb9a 100644 --- a/packages/formidable/README.md +++ b/packages/formidable/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 7b7fe7c174..2d01aea795 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/README.md b/packages/graphql/README.md index 933c58f828..412bcd30e3 100644 --- a/packages/graphql/README.md +++ b/packages/graphql/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 6f55649efd..f7591c3eb5 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 e41d0de4af..7d5084c339 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/README.md b/packages/jwks-rsa/README.md index 55fb2210ae..7e19efa504 100644 --- a/packages/jwks-rsa/README.md +++ b/packages/jwks-rsa/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 12250e91f0..0116e743da 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/README.md b/packages/jwt/README.md index 819bd64235..6635a9c8de 100644 --- a/packages/jwt/README.md +++ b/packages/jwt/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 fceaed1b4c..2a7029788f 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/README.md b/packages/mongodb/README.md index 33fd56a22b..d9e4085caa 100644 --- a/packages/mongodb/README.md +++ b/packages/mongodb/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 a3aa48d4a8..afceca9f20 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/README.md b/packages/mongoose/README.md index 7dac4cdb87..5fee16aac4 100644 --- a/packages/mongoose/README.md +++ b/packages/mongoose/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 cf4e824585..f018b4831e 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/README.md b/packages/password/README.md index d1bb1618aa..7f9d92f97a 100644 --- a/packages/password/README.md +++ b/packages/password/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 186fc9e5e7..c96f638871 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/README.md b/packages/redis/README.md index 0be61a569c..225f38935e 100644 --- a/packages/redis/README.md +++ b/packages/redis/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 69fb98a04b..8dda3b1259 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/README.md b/packages/social/README.md index f5642b5e1e..f27a68de9b 100644 --- a/packages/social/README.md +++ b/packages/social/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 7458b9f4ce..e34a1db6a2 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/README.md b/packages/storage/README.md index 710a097447..9e0080dbf7 100644 --- a/packages/storage/README.md +++ b/packages/storage/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 f300753f6c..e54cb89c09 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/README.md b/packages/swagger/README.md index cd72907f62..c632ea043c 100644 --- a/packages/swagger/README.md +++ b/packages/swagger/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 0237742900..4c28e0ce46 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/README.md b/packages/typeorm/README.md index deb4396a16..15a1ffa369 100644 --- a/packages/typeorm/README.md +++ b/packages/typeorm/README.md @@ -55,7 +55,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 b0fee1f3bf..f1c8ae6562 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/README.md b/packages/typestack/README.md index f0f3be1ef8..b0f0f9850c 100644 --- a/packages/typestack/README.md +++ b/packages/typestack/README.md @@ -52,7 +52,7 @@ $ npm run develop The development server is started! Go to `http://localhost:3001` and find our welcoming page! -[=> Continue with the tutorial](https://foalts.gitbook.io/docs/content/) +[=> Continue with the tutorial](https://foalts.gitbook.io/docs/) ## Why? 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 56770e42f1..6bf2227580 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" ] diff --git a/unit_test.sh b/unit_test.sh index 8de3591368..31751468c1 100755 --- a/unit_test.sh +++ b/unit_test.sh @@ -1,23 +1,26 @@ +#!/usr/bin/env sh +set -e + cd packages -cd acceptance-tests && npm run test || exit 1 -cd ../aws-s3 && npm run test || exit 1 -cd ../cli && npm run test || exit 1 -cd ../csrf && npm run test || exit 1 -cd ../ejs && npm run test || exit 1 -cd ../examples && npm run test || exit 1 -cd ../formidable && npm run test || exit 1 -cd ../graphql && npm run test || exit 1 -cd ../jwks-rsa && npm run test || exit 1 -cd ../jwt && npm run test || exit 1 -cd ../mongodb && npm run test || exit 1 -cd ../mongoose && npm run test || exit 1 -cd ../password && npm run test || exit 1 -cd ../redis && npm run test || exit 1 -cd ../social && npm run test || exit 1 -cd ../storage && npm run test || exit 1 -cd ../swagger && npm run test || exit 1 -cd ../typeorm && npm run test || exit 1 -cd ../typestack && npm run test || exit 1 +cd acceptance-tests && npm run test +cd ../aws-s3 && npm run test +cd ../cli && npm run test +cd ../csrf && npm run test +cd ../ejs && npm run test +cd ../examples && npm run test +cd ../formidable && npm run test +cd ../graphql && npm run test +cd ../jwks-rsa && npm run test +cd ../jwt && npm run test +cd ../mongodb && npm run test +cd ../mongoose && npm run test +cd ../password && npm run test +cd ../redis && npm run test +cd ../social && npm run test +cd ../storage && npm run test +cd ../swagger && npm run test +cd ../typeorm && npm run test +cd ../typestack && npm run test # @foal/core at the end because code coverage takes time. -cd ../core && npm run test || exit 1 +cd ../core && npm run test cd ../.. \ No newline at end of file