diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 15147794..de3d2ae9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,6 +9,7 @@ Check the NPM packages that require a new publication or release: - [ ] [manifest](https://www.npmjs.com/package/manifest) +- [] [add-manifest](https://www.npmjs.com/package/add-manifest) - [ ] [@mnfst/sdk](https://www.npmjs.com/package/@mnfst/sdk) ## Check list before submitting diff --git a/README.md b/README.md index c831ee95..fec5ff0c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

-A backend so simple that it fits in a YAML file +A backend so simple that it fits into 1 YAML file

npm CodeFactor Grade diff --git a/package-lock.json b/package-lock.json index 4127e00a..79cdad89 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24384,7 +24384,7 @@ "license": "MIT" }, "packages/add-manifest": { - "version": "1.0.0", + "version": "1.0.2", "license": "MIT", "dependencies": { "@oclif/core": "^3", @@ -24457,7 +24457,7 @@ } }, "packages/core/manifest": { - "version": "4.0.1", + "version": "4.0.4", "license": "MIT", "dependencies": { "@faker-js/faker": "^8.4.1", @@ -24552,7 +24552,7 @@ }, "packages/js-sdk": { "name": "@mnfst/sdk", - "version": "1.0.2", + "version": "1.0.5", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.12", diff --git a/packages/add-manifest/README.md b/packages/add-manifest/README.md index e6067011..31290f2d 100644 --- a/packages/add-manifest/README.md +++ b/packages/add-manifest/README.md @@ -14,9 +14,11 @@ npm install # Run from a test folder to prevent messing with project files. mkdir test-folder cd test-folder -../bin/dev.js create +../bin/dev.js ``` +However due to the monorepo workspace structure, the launch script will fail as the path to the node modules folder is different than when served. + ## Publish ```bash diff --git a/packages/add-manifest/assets/README.md b/packages/add-manifest/assets/README.md new file mode 100644 index 00000000..0cd97613 --- /dev/null +++ b/packages/add-manifest/assets/README.md @@ -0,0 +1,58 @@ +
+

+ + manifest + + + manifest + +

+ +

+A backend so simple that it fits into 1 YAML file +

+ npm + CodeFactor Grade + Discord + Support us + CodeTriage + License MIT +
+

+ +## Description + +This project was made with [Manifest](https://github.com/mnfst/manifest). + +## Installation + +```bash +$ npm install +``` + +## Running the app + +To run the app in the development mode: + +```bash +npm run manifest +``` + +- Open [http://localhost:1111](http://localhost:1111) to open your admin UI it in your browser +- Open [http://localhost:1111/api](http://localhost:111/api) to view your REST API documentation + +The page will reload when you make changes. + +## Seed dummy data + +Seeds some dummy data for your entities: + +```bash +npm run manifest:seed +``` + +## Community & Resources + +- [Docs](https://manifest.build/docs) - Get started with Manifest +- [Discord](https://discord.gg/FepAked3W7) - Come chat with the community +- [Github](https://github.com/mnfst/manifest/issues) - Report bugs and share ideas to improve the product. diff --git a/packages/add-manifest/package.json b/packages/add-manifest/package.json index 1021bc0e..c68ee86b 100644 --- a/packages/add-manifest/package.json +++ b/packages/add-manifest/package.json @@ -1,6 +1,6 @@ { "name": "add-manifest", - "version": "1.0.2", + "version": "1.0.3", "author": "Manifest", "description": "Add Manifest backend", "homepage": "https://manifest.build", diff --git a/packages/add-manifest/src/commands/index.ts b/packages/add-manifest/src/commands/index.ts index c9eba975..158707cb 100644 --- a/packages/add-manifest/src/commands/index.ts +++ b/packages/add-manifest/src/commands/index.ts @@ -28,11 +28,12 @@ export class MyCommand extends Command { * 5. Update the .vscode/settings.json file with the recommended settings. * 6. Update the .gitignore file with the recommended settings. * 7. Update the .env file with the environment variables. - * 8. Install the new packages. - * 9. Serve the new app. - * 10. Wait for the server to start. - * 11. Seed the database. - * 12. Open the browser. + * 8. If no README.md file exists, create one. + * 9. Install the new packages. + * 10. Serve the new app. + * 11. Wait for the server to start. + * 12. Seed the database. + * 13. Open the browser. */ async run(): Promise { const folderName = 'manifest' @@ -42,6 +43,7 @@ export class MyCommand extends Command { const spinner = ora('Add Manifest to your project...').start() + // * 1. Create a folder with the name `manifest`. // Construct the folder path. This example creates the folder in the current working directory. const folderPath = path.join(process.cwd(), folderName) @@ -56,6 +58,7 @@ export class MyCommand extends Command { // Create the folder fs.mkdirSync(folderPath) + // * 2. Create a file inside the folder with the name `manifest.yml`. // Path where the new file should be created const newFilePath = path.join(folderPath, initialFileName) @@ -91,7 +94,7 @@ export class MyCommand extends Command { updatePackageJsonFile({ fileContent: packageJson, newPackages: { - manifest: '^4.0.4' + manifest: '^4.0.5' }, newScripts: { manifest: 'node node_modules/manifest/scripts/watch/watch.js', @@ -155,7 +158,7 @@ export class MyCommand extends Command { }) ) - // Update the .gitignore file with the recommended settings. + // * 7. Update the .env file with the environment variables. const gitignorePath = path.join(process.cwd(), '.gitignore') let gitignoreContent = '' @@ -163,14 +166,32 @@ export class MyCommand extends Command { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } - if (!gitignoreContent.includes('node_modules')) { - gitignoreContent += '\nnode_modules' - gitignoreContent += '\n.env' - } + const newGitignoreLines: string[] = [ + 'node_modules', + '.env', + 'public', + 'manifest/backend.db' + ] + newGitignoreLines.forEach((line) => { + if (!gitignoreContent.includes(line)) { + gitignoreContent += `\n${line}` + } + }) fs.writeFileSync(gitignorePath, gitignoreContent) spinner.succeed() + + // * 8. Add a README.md file if it doesn't exist. + const readmeFilePath = path.join(process.cwd(), 'README.md') + if (!fs.existsSync(readmeFilePath)) { + fs.writeFileSync( + readmeFilePath, + fs.readFileSync(path.join(assetFolderPath, 'README.md'), 'utf8') + ) + } + + // * 9. Install the new packages. spinner.start('Install dependencies...') // Install deps. diff --git a/packages/core/admin/src/app/app.component.html b/packages/core/admin/src/app/app.component.html index d2e65e82..3577b25c 100644 --- a/packages/core/admin/src/app/app.component.html +++ b/packages/core/admin/src/app/app.component.html @@ -1,11 +1,10 @@ - - + -
+
-
-
- -
+
+
+
+
- -
+ +
-
+
diff --git a/packages/core/admin/src/app/modules/auth/views/login/login.component.ts b/packages/core/admin/src/app/modules/auth/views/login/login.component.ts index 2a411167..f91d5ca0 100644 --- a/packages/core/admin/src/app/modules/auth/views/login/login.component.ts +++ b/packages/core/admin/src/app/modules/auth/views/login/login.component.ts @@ -31,6 +31,7 @@ export class LoginComponent implements OnInit { ngOnInit(): void { this.activatedRoute.queryParams.subscribe(async (queryParams: Params) => { + // Set suggested email and password from query params or default admin credentials. if (queryParams['email'] && queryParams['password']) { this.suggestedEmail = queryParams['email'] this.suggestedPassword = queryParams['password'] @@ -40,15 +41,20 @@ export class LoginComponent implements OnInit { this.suggestedPassword = DEFAULT_ADMIN_CREDENTIALS.password } } - this.form = new FormGroup({ email: new FormControl(this.suggestedEmail || '', [ - Validators.required + Validators.required, + Validators.email ]), password: new FormControl(this.suggestedPassword || '', [ Validators.required ]) }) + + // Redirect to register first admin if the database is empty. + if (await this.authService.isDbEmpty()) { + this.router.navigate(['/auth/welcome']) + } }) } diff --git a/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.html b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.html new file mode 100644 index 00000000..5ab7bf88 --- /dev/null +++ b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.html @@ -0,0 +1,75 @@ +
+
+
+
+
+
+ manifest logo +
+
+
+

Welcome

+ +

+ Welcome to your admin panel. Create your first admin account + to continue. You can always change those values later. +

+ +
+
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+
+
+
diff --git a/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.scss b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.scss new file mode 100644 index 00000000..88db893c --- /dev/null +++ b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.scss @@ -0,0 +1,61 @@ +@import '../../../../../styles/variables/all'; +@import 'bulma/sass/utilities/mixins'; + +.container { + max-width: 100%; + width: 100%; +} + +.hero.is-fullheight { + min-height: calc(100vh - 64px); + + @include touch { + min-height: calc(100vh - 64px); + } +} + +.notification { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: auto; + border-radius: 0; + bottom: unset; +} + +.col-welcome { + position: fixed; + display: flex; + align-items: center; + justify-content: center; + background-color: $white; + left: 0; + top: 0; + width: 100vw; + height: 100vh; + + > div { + margin: auto; + + @include widescreen { + max-width: 482px; + min-width: 382px; + } + + @include desktop { + max-width: 424px; + min-width: 360px; + } + + @include tablet { + max-width: 424px; + min-width: 320px; + } + + @include mobile { + min-width: 50%; + max-width: 380px; + } + } +} diff --git a/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.ts b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.ts new file mode 100644 index 00000000..2755f27e --- /dev/null +++ b/packages/core/admin/src/app/modules/auth/views/register-first-admin/register-first-admin.component.ts @@ -0,0 +1,63 @@ +import { Component, OnInit } from '@angular/core' +import { FormControl, FormGroup, Validators } from '@angular/forms' +import { PropType } from '@repo/types' +import { confirmPasswordValidator } from '../../utlis/confirm-password-validator' +import { AuthService } from '../../auth.service' +import { Router } from '@angular/router' +import { FlashMessageService } from '../../../shared/services/flash-message.service' + +@Component({ + selector: 'app-register-first-admin', + templateUrl: './register-first-admin.component.html', + styleUrl: './register-first-admin.component.scss' +}) +export class RegisterFirstAdminComponent implements OnInit { + form: FormGroup + PropType = PropType + + constructor( + private authService: AuthService, + private router: Router, + private flashMessageService: FlashMessageService + ) {} + + ngOnInit(): void { + this.form = new FormGroup({ + email: new FormControl('', [Validators.required, Validators.email]), + password: new FormControl('', [Validators.required]), + confirmPassword: new FormControl('', [ + Validators.required, + confirmPasswordValidator('password') + ]) + }) + } + + /** + * Patch value to the form + * + * @param controlName + * @param value + * + * @returns void + */ + patchValue(controlName: string, value: string) { + this.form.get(controlName)?.patchValue(value) + } + + /** + * Submit the form + */ + async submit(): Promise { + const token: string = await this.authService.signup(this.form.value) + + if (!token) { + return this.flashMessageService.error('Error: Failed to register') + } + + this.flashMessageService.success( + 'Welcome! You have successfully registered as an admin.' + ) + + this.router.navigate(['/']) + } +} diff --git a/packages/core/admin/src/app/modules/shared/inputs/email-input/email-input.component.ts b/packages/core/admin/src/app/modules/shared/inputs/email-input/email-input.component.ts index c613dfed..a1350192 100644 --- a/packages/core/admin/src/app/modules/shared/inputs/email-input/email-input.component.ts +++ b/packages/core/admin/src/app/modules/shared/inputs/email-input/email-input.component.ts @@ -20,7 +20,7 @@ import { PropertyManifest } from '@repo/types' class="input" [ngClass]="{ 'is-danger': isError }" type="email" - placeholder="Email" + placeholder="Email..." autocomplete="email" (change)="onChange($event)" #input diff --git a/packages/core/admin/src/app/modules/shared/inputs/password-input/password-input.component.ts b/packages/core/admin/src/app/modules/shared/inputs/password-input/password-input.component.ts index 7f9c2f42..95e6ae68 100644 --- a/packages/core/admin/src/app/modules/shared/inputs/password-input/password-input.component.ts +++ b/packages/core/admin/src/app/modules/shared/inputs/password-input/password-input.component.ts @@ -18,6 +18,7 @@ import { PropertyManifest } from '@repo/types' {{ appManifest.name }}

@@ -48,7 +48,7 @@

{{ appManifest.name }}

@@ -80,7 +80,7 @@

{{ appManifest.name }}

@@ -90,7 +90,7 @@

{{ appManifest.name }}

@@ -116,7 +116,7 @@

{{ appManifest.name }}

@@ -126,7 +126,7 @@

{{ appManifest.name }}

@@ -156,7 +156,7 @@

{{ appManifest.name }}

@@ -167,7 +167,7 @@

{{ appManifest.name }}

diff --git a/packages/core/admin/src/assets/images/ext-link-01.svg b/packages/core/admin/src/assets/images/ext-link-01.svg index b90b59ef..b8ed3b6f 100644 --- a/packages/core/admin/src/assets/images/ext-link-01.svg +++ b/packages/core/admin/src/assets/images/ext-link-01.svg @@ -1,17 +1,68 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/core/admin/src/styles/variables/_cards.scss b/packages/core/admin/src/styles/variables/_cards.scss index acbae357..2f30a0f3 100644 --- a/packages/core/admin/src/styles/variables/_cards.scss +++ b/packages/core/admin/src/styles/variables/_cards.scss @@ -1,11 +1,15 @@ $card-color: $dark !default; -$card-radius: 6px !default; +$card-radius: 12px !default; $border-color: $white-ter !default; $card-header-color: inherit !default; $card-header-padding: 2rem !default; $card-content-padding: 2rem !default; $card-header-shadow: 0 1px 0 $grey-lighter !default; -$card-shadow: 0 10px 20px rgba($dark, 0.07) !default; +$card-shadow: + rgba($dark, 0) 0px 0px 0px 0px, + rgba($dark, 0) 0px 0px 0px 0px, + rgba($dark, 0.1) 0px 1px 3px 0px, + rgba($dark, 0.1) 0px 1px 2px -1px !default; $card-footer-padding: 2rem !default; $card-footer-border-top: 1px solid $grey-lighter !default; diff --git a/packages/core/admin/src/styles/variables/_generic.scss b/packages/core/admin/src/styles/variables/_generic.scss index cd9ea33b..6f50a290 100644 --- a/packages/core/admin/src/styles/variables/_generic.scss +++ b/packages/core/admin/src/styles/variables/_generic.scss @@ -1,5 +1,9 @@ // generic -$shadow: 0 10px 20px 0 rgba($dark, 0.07) !default; +$shadow: + rgba($dark, 0) 0px 0px 0px 0px, + rgba($dark, 0) 0px 0px 0px 0px, + rgba($dark, 0.1) 0px 1px 3px 0px, + rgba($dark, 0.1) 0px 1px 2px -1px !default; $border-color: $white-ter !default; $radius-large: 6px !default; $link-hover: $primary !default; diff --git a/packages/core/manifest/package.json b/packages/core/manifest/package.json index dd27cb55..17b9452a 100644 --- a/packages/core/manifest/package.json +++ b/packages/core/manifest/package.json @@ -1,7 +1,7 @@ { "name": "manifest", - "version": "4.0.4", - "description": "A backend so simple that it fits in a YAML file", + "version": "4.0.5", + "description": "A backend so simple that it fits into 1 YAML file", "author": "Manifest", "license": "MIT", "homepage": "https://manifest.build", @@ -16,11 +16,12 @@ "manifest", "backend", "backend-as-a-service", - "bass", + "baas", "api", "rest", "fullstack", - "yaml" + "yaml", + "headless" ], "scripts": { "build": "nest build && cd ../admin && npm run build", diff --git a/packages/core/manifest/src/app.module.ts b/packages/core/manifest/src/app.module.ts index 5c46f8b5..c12e0fef 100644 --- a/packages/core/manifest/src/app.module.ts +++ b/packages/core/manifest/src/app.module.ts @@ -66,9 +66,8 @@ export class AppModule { private async init() { const isSeed: boolean = process.argv[1].includes('seed') const isTest: boolean = process.env.NODE_ENV === 'test' - const isProduction: boolean = process.env.NODE_ENV === 'production' - if (!isSeed && !isTest && !isProduction) { + if (!isSeed && !isTest) { this.loggerService.initMessage() } } diff --git a/packages/core/manifest/src/auth/auth.controller.ts b/packages/core/manifest/src/auth/auth.controller.ts index 1d51e5b2..e22e3b17 100644 --- a/packages/core/manifest/src/auth/auth.controller.ts +++ b/packages/core/manifest/src/auth/auth.controller.ts @@ -14,6 +14,7 @@ import { AuthService } from './auth.service' import { SignupAuthenticableEntityDto } from './dtos/signup-authenticable-entity.dto' import { Rule } from './decorators/rule.decorator' import { AuthorizationGuard } from './guards/authorization.guard' +import { IsDbEmptyGuard } from './guards/is-db-empty.guard' @Controller('auth') @UseGuards(AuthorizationGuard) @@ -30,6 +31,16 @@ export class AuthController { return this.authService.createToken(entity, signupUserDto) } + @Post('admins/signup') + @UseGuards(IsDbEmptyGuard) + public async signupAdmin( + @Body() signupUserDto: SignupAuthenticableEntityDto + ): Promise<{ + token: string + }> { + return this.authService.signup('admins', signupUserDto, true) + } + @Post(':entity/signup') @Rule('signup') public async signup( diff --git a/packages/core/manifest/src/auth/auth.module.ts b/packages/core/manifest/src/auth/auth.module.ts index 6afc6c91..1df89597 100644 --- a/packages/core/manifest/src/auth/auth.module.ts +++ b/packages/core/manifest/src/auth/auth.module.ts @@ -4,11 +4,12 @@ import { EntityModule } from '../entity/entity.module' import { AuthController } from './auth.controller' import { AuthService } from './auth.service' import { ManifestModule } from '../manifest/manifest.module' +import { DatabaseService } from '../crud/services/database.service' @Module({ imports: [EntityModule, forwardRef(() => ManifestModule)], controllers: [AuthController], - providers: [AuthService], + providers: [AuthService, DatabaseService], exports: [AuthService] }) export class AuthModule {} diff --git a/packages/core/manifest/src/auth/auth.service.ts b/packages/core/manifest/src/auth/auth.service.ts index f3cd53cc..528600dd 100644 --- a/packages/core/manifest/src/auth/auth.service.ts +++ b/packages/core/manifest/src/auth/auth.service.ts @@ -78,15 +78,17 @@ export class AuthService { * @param entitySlug The slug of the AuthenticableEntity where the user is going to be created * @param email The email of the user * @param password The password of the user + * @param byPassAdminCheck If true, the method will not check if the entity is an admin * * @returns A JWT token of the created user * */ async signup( entitySlug: string, - signupUserDto: SignupAuthenticableEntityDto + signupUserDto: SignupAuthenticableEntityDto, + byPassAdminCheck = false ): Promise<{ token: string }> { - if (entitySlug === ADMIN_ENTITY_MANIFEST.slug) { + if (entitySlug === ADMIN_ENTITY_MANIFEST.slug && !byPassAdminCheck) { throw new HttpException( 'Admins cannot be created with this method.', HttpStatus.BAD_REQUEST diff --git a/packages/core/manifest/src/auth/dtos/signup-authenticable-entity.dto.ts b/packages/core/manifest/src/auth/dtos/signup-authenticable-entity.dto.ts index 9bba208f..a7aa2130 100644 --- a/packages/core/manifest/src/auth/dtos/signup-authenticable-entity.dto.ts +++ b/packages/core/manifest/src/auth/dtos/signup-authenticable-entity.dto.ts @@ -1,9 +1,11 @@ -import { IsEmail, IsNotEmpty } from 'class-validator' +import { IsEmail, IsNotEmpty, IsString } from 'class-validator' export class SignupAuthenticableEntityDto { @IsEmail() + @IsNotEmpty() public email: string + @IsString() @IsNotEmpty() public password: string } diff --git a/packages/core/manifest/src/auth/guards/is-db-empty.guard.ts b/packages/core/manifest/src/auth/guards/is-db-empty.guard.ts new file mode 100644 index 00000000..80fc483f --- /dev/null +++ b/packages/core/manifest/src/auth/guards/is-db-empty.guard.ts @@ -0,0 +1,16 @@ +import { CanActivate, Injectable } from '@nestjs/common' +import { DatabaseService } from '../../crud/services/database.service' + +@Injectable() +export class IsDbEmptyGuard implements CanActivate { + constructor(private readonly databaseService: DatabaseService) {} + + /** + * Check if the database is empty (no items in any entity, even admin). + * + * @returns True if the database is empty, false otherwise. + * */ + async canActivate(): Promise { + return this.databaseService.isDbEmpty() + } +} diff --git a/packages/core/manifest/src/auth/tests/is-db-empty.guard.spec.ts b/packages/core/manifest/src/auth/tests/is-db-empty.guard.spec.ts new file mode 100644 index 00000000..341a9cb0 --- /dev/null +++ b/packages/core/manifest/src/auth/tests/is-db-empty.guard.spec.ts @@ -0,0 +1,45 @@ +import { Test } from '@nestjs/testing' +import { IsDbEmptyGuard } from '../guards/is-db-empty.guard' +import { DatabaseService } from '../../crud/services/database.service' + +describe('IsDbEmptyGuard', () => { + let databaseService: DatabaseService + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + IsDbEmptyGuard, + { + provide: DatabaseService, + useValue: { + isDbEmpty: jest.fn().mockReturnValue(Promise.resolve(true)) + } + } + ] + }).compile() + + databaseService = module.get(DatabaseService) + }) + + it('should be defined', () => { + expect(new IsDbEmptyGuard(databaseService)).toBeDefined() + }) + + it('should return true if the database is empty', async () => { + const isDbEmptyGuard = new IsDbEmptyGuard(databaseService) + const res = await isDbEmptyGuard.canActivate() + + expect(res).toBe(true) + }) + + it('should return false if the database is not empty', async () => { + jest + .spyOn(databaseService, 'isDbEmpty') + .mockReturnValue(Promise.resolve(false)) + + const isDbEmptyGuard = new IsDbEmptyGuard(databaseService) + const res = await isDbEmptyGuard.canActivate() + + expect(res).toBe(false) + }) +}) diff --git a/packages/core/manifest/src/crud/controllers/database.controller.ts b/packages/core/manifest/src/crud/controllers/database.controller.ts new file mode 100644 index 00000000..8eaae478 --- /dev/null +++ b/packages/core/manifest/src/crud/controllers/database.controller.ts @@ -0,0 +1,16 @@ +import { Controller, Get } from '@nestjs/common' +import { DatabaseService } from '../services/database.service' + +@Controller('db') +export class DatabaseController { + constructor(private readonly databaseService: DatabaseService) {} + + @Get('is-db-empty') + public async isDbEmpty(): Promise<{ + empty: boolean + }> { + const empty = await this.databaseService.isDbEmpty() + + return { empty } + } +} diff --git a/packages/core/manifest/src/crud/crud.module.ts b/packages/core/manifest/src/crud/crud.module.ts index 6d1a06d1..7e8daee7 100644 --- a/packages/core/manifest/src/crud/crud.module.ts +++ b/packages/core/manifest/src/crud/crud.module.ts @@ -8,10 +8,13 @@ import { CrudService } from './services/crud.service' import { PaginationService } from './services/pagination.service' import { ValidationModule } from '../validation/validation.module' import { AuthService } from '../auth/auth.service' +import { DatabaseService } from './services/database.service' +import { DatabaseController } from './controllers/database.controller' @Module({ imports: [EntityModule, ManifestModule, ValidationModule], - controllers: [CrudController], - providers: [CrudService, PaginationService, AuthService] + controllers: [CrudController, DatabaseController], + providers: [CrudService, PaginationService, AuthService, DatabaseService], + exports: [DatabaseService] }) export class CrudModule {} diff --git a/packages/core/manifest/src/crud/services/database.service.ts b/packages/core/manifest/src/crud/services/database.service.ts new file mode 100644 index 00000000..639c1a70 --- /dev/null +++ b/packages/core/manifest/src/crud/services/database.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common' +import { ManifestService } from '../../manifest/services/manifest.service' +import { EntityService } from '../../entity/services/entity.service' +import { AppManifest, EntityManifest } from '../../../../types/src' +import { ADMIN_ENTITY_MANIFEST } from '../../constants' + +@Injectable() +export class DatabaseService { + constructor( + private manifestService: ManifestService, + private entityService: EntityService + ) {} + + /** + * Check if the database is empty (no items in any entity, even admin). + * + * @returns true if the database is empty, false otherwise. + * */ + async isDbEmpty(): Promise { + const appManifest: AppManifest = this.manifestService.getAppManifest() + + const entities = [ + ...Object.values(appManifest.entities), + ADMIN_ENTITY_MANIFEST + ] + let totalItems = 0 + + await Promise.all( + Object.values(entities).map(async (entityManifest: EntityManifest) => { + return this.entityService + .getEntityRepository({ + entitySlug: entityManifest.slug + }) + .createQueryBuilder('entity') + .getCount() + }) + ).then((counts: number[]) => { + totalItems = counts.reduce((acc, count) => acc + count, 0) + }) + + return totalItems === 0 + } +} diff --git a/packages/core/manifest/src/crud/tests/database.controller.spec.ts b/packages/core/manifest/src/crud/tests/database.controller.spec.ts new file mode 100644 index 00000000..80525a7d --- /dev/null +++ b/packages/core/manifest/src/crud/tests/database.controller.spec.ts @@ -0,0 +1,27 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { DatabaseController } from '../controllers/database.controller' +import { DatabaseService } from '../services/database.service' + +describe('DatabaseController', () => { + let controller: DatabaseController + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [DatabaseController], + providers: [ + { + provide: DatabaseService, + useValue: { + isDbEmpty: jest.fn().mockReturnValue(Promise.resolve(true)) + } + } + ] + }).compile() + + controller = module.get(DatabaseController) + }) + + it('should be defined', () => { + expect(controller).toBeDefined() + }) +}) diff --git a/packages/core/manifest/src/crud/tests/database.service.spec.ts b/packages/core/manifest/src/crud/tests/database.service.spec.ts new file mode 100644 index 00000000..6c1a7c5b --- /dev/null +++ b/packages/core/manifest/src/crud/tests/database.service.spec.ts @@ -0,0 +1,62 @@ +import { Test } from '@nestjs/testing' +import { DatabaseService } from '../services/database.service' +import { ManifestService } from '../../manifest/services/manifest.service' +import { EntityService } from '../../entity/services/entity.service' + +describe('DatabaseService', () => { + let manifestService: ManifestService + let entityService: EntityService + let service: DatabaseService + + beforeEach(async () => { + const module = await Test.createTestingModule({ + providers: [ + DatabaseService, + { + provide: ManifestService, + useValue: { + getAppManifest: jest.fn().mockReturnValue({ + entities: {} + }) + } + }, + { + provide: EntityService, + useValue: { + getEntityRepository: jest.fn().mockReturnValue({ + createQueryBuilder: jest.fn().mockReturnValue({ + getCount: jest.fn().mockReturnValue(0) + }) + }) + } + } + ] + }).compile() + + manifestService = module.get(ManifestService) + entityService = module.get(EntityService) + service = module.get(DatabaseService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) + + it('should return true if the database is empty', async () => { + const res = await service.isDbEmpty() + + expect(res).toBe(true) + }) + + it('should return false if the database is not empty', async () => { + jest.spyOn(entityService, 'getEntityRepository').mockReturnValue({ + createQueryBuilder: jest.fn().mockReturnValue({ + getCount: jest.fn().mockReturnValue(1) + }) + } as any) + + const res = await service.isDbEmpty() + + expect(res).toBe(false) + }) +}) diff --git a/packages/core/manifest/src/main.ts b/packages/core/manifest/src/main.ts index 02f1d37d..0def0a9c 100644 --- a/packages/core/manifest/src/main.ts +++ b/packages/core/manifest/src/main.ts @@ -51,14 +51,13 @@ async function bootstrap() { } }) - if (!isProduction) { - const openApiService: OpenApiService = app.get(OpenApiService) + const openApiService: OpenApiService = app.get(OpenApiService) - SwaggerModule.setup('api', app, openApiService.generateOpenApiObject(), { - customfavIcon: 'assets/images/open-api/favicon.ico', - customSiteTitle: 'Manifest API Doc', + SwaggerModule.setup('api', app, openApiService.generateOpenApiObject(), { + customfavIcon: 'assets/images/open-api/favicon.ico', + customSiteTitle: 'Manifest API Doc', - customCss: ` + customCss: ` .swagger-ui html { box-sizing: border-box; @@ -1791,8 +1790,7 @@ background: #ce107c; fill: #535356; } ` - }) - } + }) await app.listen(configService.get('PORT') || DEFAULT_PORT) }