diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9ed600e4..ca8ac665 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,6 +4,15 @@ ## How can it be tested? +## Impacted packages + +Check the packages that require a new publication or release: + +- [ ] @mnfst/manifest +- [ ] @mnfst/types +- [ ] @mnfst/admin +- [ ] doc + ## Check list before submitting - [ ] I have performed a self-review of my code (no debugs, no commented code, good naming, etc.) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index e60eae58..af077caf 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -20,6 +20,10 @@ jobs: - name: Install dependencies run: npm ci working-directory: ./packages/core/manifest + # Enure that we are using the latest version of the types package (even if it is not published yet). + - name: Link local types + run: npm run link-local-types + working-directory: ./packages/core/manifest - name: Build App run: npm run build working-directory: ./packages/core/manifest diff --git a/.vscode/settings.json b/.vscode/settings.json index 703ef541..2dacf2b3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -34,6 +34,7 @@ "metas", "mnfst", "nestjs", + "openapi", "typeorm", "uniqid" ], diff --git a/packages/core/admin/package-lock.json b/packages/core/admin/package-lock.json index 2ae35b62..afeca7db 100644 --- a/packages/core/admin/package-lock.json +++ b/packages/core/admin/package-lock.json @@ -18,7 +18,7 @@ "@angular/platform-browser-dynamic": "^17.3.1", "@angular/router": "^17.3.1", "@auth0/angular-jwt": "^5.2.0", - "@mnfst/types": "^0.1.0-alpha.3", + "@mnfst/types": "^0.1.0-alpha.4", "bulma": "^0.9.4", "bulma-tooltip": "^3.0.2", "rxjs": "~7.5.0", @@ -2943,9 +2943,9 @@ } }, "node_modules/@mnfst/types": { - "version": "0.1.0-alpha.3", - "resolved": "https://registry.npmjs.org/@mnfst/types/-/types-0.1.0-alpha.3.tgz", - "integrity": "sha512-dz/G7m0JYD8VA4MI65F8L4K6nS12LxSyL0tJVsU723sQcPjKnFzKe+7J+OpxQTXVnnY494aWr+vXPLZV/LNwJQ==" + "version": "0.1.0-alpha.4", + "resolved": "https://registry.npmjs.org/@mnfst/types/-/types-0.1.0-alpha.4.tgz", + "integrity": "sha512-kn4aJtRRW8iG5yByKP+iUSl4T8NRC5EiLKK19pRA6hU9smqFz8TU7yVMUUja1ZmmX1xY3NkfDeFa7CK4CIMRfw==" }, "node_modules/@ngtools/webpack": { "version": "17.3.1", diff --git a/packages/core/admin/package.json b/packages/core/admin/package.json index 3aac9b7e..c0174a14 100644 --- a/packages/core/admin/package.json +++ b/packages/core/admin/package.json @@ -1,6 +1,6 @@ { "name": "@mnfst/admin", - "version": "0.1.0-alpha.4", + "version": "0.1.0-alpha.5", "homepage": "https://manifest.build", "keywords": [ "admin", @@ -28,7 +28,8 @@ "start": "ng serve", "build": "ng build --configuration production", "watch": "ng build --watch --configuration development", - "test": "ng test" + "test": "ng test", + "link-local-types": "cd ../types && npm install && npm run build && npm link && cd ../manifest && npm link @mnfst/types" }, "private": false, "license": "MIT", @@ -42,7 +43,7 @@ "@angular/platform-browser-dynamic": "^17.3.1", "@angular/router": "^17.3.1", "@auth0/angular-jwt": "^5.2.0", - "@mnfst/types": "^0.1.0-alpha.3", + "@mnfst/types": "^0.1.0-alpha.4", "bulma": "^0.9.4", "bulma-tooltip": "^3.0.2", "rxjs": "~7.5.0", diff --git a/packages/core/admin/src/app/modules/shared/inputs/input.component.ts b/packages/core/admin/src/app/modules/shared/inputs/input.component.ts index 357ec003..7248bb65 100644 --- a/packages/core/admin/src/app/modules/shared/inputs/input.component.ts +++ b/packages/core/admin/src/app/modules/shared/inputs/input.component.ts @@ -21,6 +21,7 @@ import { SelectInputComponent } from './select-input/select-input.component' import { TextInputComponent } from './text-input/text-input.component' import { TextareaInputComponent } from './textarea-input/textarea-input.component' import { UrlInputComponent } from './url-input/url-input.component' +import { TimestampInputComponent } from './timestamp-input/timestamp-input.component' @Component({ selector: 'app-input', @@ -30,6 +31,7 @@ import { UrlInputComponent } from './url-input/url-input.component' BooleanInputComponent, CurrencyInputComponent, DateInputComponent, + TimestampInputComponent, EmailInputComponent, UrlInputComponent, MultiSelectInputComponent, @@ -111,6 +113,14 @@ import { UrlInputComponent } from './url-input/url-input.component' *ngIf="prop?.type === PropType.Date" > + + { + let component: TimestampInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TimestampInputComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TimestampInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/packages/core/admin/src/app/modules/shared/inputs/timestamp-input/timestamp-input.component.ts b/packages/core/admin/src/app/modules/shared/inputs/timestamp-input/timestamp-input.component.ts new file mode 100644 index 00000000..5c10bef0 --- /dev/null +++ b/packages/core/admin/src/app/modules/shared/inputs/timestamp-input/timestamp-input.component.ts @@ -0,0 +1,44 @@ +import { NgClass } from '@angular/common' +import { + Component, + ElementRef, + EventEmitter, + Input, + Output, + ViewChild +} from '@angular/core' +import { PropertyManifest } from '@mnfst/types' + +@Component({ + selector: 'app-timestamp-input', + standalone: true, + imports: [NgClass], + template: ` + ` +}) +export class TimestampInputComponent { + @Input() prop: PropertyManifest + @Input() value: string + @Input() isError: boolean + + @Output() valueChanged: EventEmitter = new EventEmitter() + + @ViewChild('input', { static: true }) input: ElementRef + + ngOnInit(): void { + if (this.value !== undefined) { + this.input.nativeElement.value = this.value + } + } + + onChange(event: any) { + this.valueChanged.emit(event.target.value) + } +} diff --git a/packages/core/admin/src/app/modules/shared/yields/date-yield/date-yield.component.ts b/packages/core/admin/src/app/modules/shared/yields/date-yield/date-yield.component.ts index 3358b1c3..ee280a33 100644 --- a/packages/core/admin/src/app/modules/shared/yields/date-yield/date-yield.component.ts +++ b/packages/core/admin/src/app/modules/shared/yields/date-yield/date-yield.component.ts @@ -1,10 +1,10 @@ -import { CommonModule } from '@angular/common' +import { DatePipe, NgIf } from '@angular/common' import { Component, Input } from '@angular/core' @Component({ selector: 'app-date-yield', standalone: true, - imports: [CommonModule], + imports: [DatePipe, NgIf], template: `{{ value | date : 'MM/dd/yy' }} - `, styleUrls: ['./date-yield.component.scss'] diff --git a/packages/core/admin/src/app/modules/shared/yields/timestamp-yield/timestamp-yield.component.spec.ts b/packages/core/admin/src/app/modules/shared/yields/timestamp-yield/timestamp-yield.component.spec.ts new file mode 100644 index 00000000..4b14dec9 --- /dev/null +++ b/packages/core/admin/src/app/modules/shared/yields/timestamp-yield/timestamp-yield.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TimestampYieldComponent } from './timestamp-yield.component'; + +describe('TimestampYieldComponent', () => { + let component: TimestampYieldComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TimestampYieldComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(TimestampYieldComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/packages/core/admin/src/app/modules/shared/yields/timestamp-yield/timestamp-yield.component.ts b/packages/core/admin/src/app/modules/shared/yields/timestamp-yield/timestamp-yield.component.ts new file mode 100644 index 00000000..03809b65 --- /dev/null +++ b/packages/core/admin/src/app/modules/shared/yields/timestamp-yield/timestamp-yield.component.ts @@ -0,0 +1,14 @@ +import { DatePipe } from '@angular/common' +import { Component, Input } from '@angular/core' + +@Component({ + selector: 'app-timestamp-yield', + standalone: true, + imports: [DatePipe], + template: `{{ + value | date : 'yyyy-MM-dd HH:mm:ss' + }}` +}) +export class TimestampYieldComponent { + @Input() value: string +} diff --git a/packages/core/admin/src/app/modules/shared/yields/yield.component.ts b/packages/core/admin/src/app/modules/shared/yields/yield.component.ts index 6ebf16a9..db3ce217 100644 --- a/packages/core/admin/src/app/modules/shared/yields/yield.component.ts +++ b/packages/core/admin/src/app/modules/shared/yields/yield.component.ts @@ -13,6 +13,7 @@ import { LocationYieldComponent } from './location-yield/location-yield.componen import { NumberYieldComponent } from './number-yield/number-yield.component' import { ProgressBarYieldComponent } from './progress-bar-yield/progress-bar-yield.component' import { TextYieldComponent } from './text-yield/text-yield.component' +import { TimestampYieldComponent } from './timestamp-yield/timestamp-yield.component' @Component({ selector: 'app-yield', @@ -22,6 +23,7 @@ import { TextYieldComponent } from './text-yield/text-yield.component' BooleanYieldComponent, CurrencyYieldComponent, DateYieldComponent, + TimestampYieldComponent, EmailYieldComponent, NumberYieldComponent, LinkYieldComponent, @@ -45,22 +47,23 @@ import { TextYieldComponent } from './text-yield/text-yield.component' [value]="value" [compact]="compact" > - - - + { // Store request object in global scope to use in tests. global.request = supertest(app.getHttpServer()) + // Set the SwaggerModule to serve the OpenAPI doc. + const openApiService = app.get(OpenApiService) + SwaggerModule.setup('api', app, openApiService.generateOpenApiObject()) + await app.init() }) diff --git a/packages/core/manifest/e2e/tests/crud.e2e-spec.ts b/packages/core/manifest/e2e/tests/crud.e2e-spec.ts index 4a59d93f..1a4ab5a0 100644 --- a/packages/core/manifest/e2e/tests/crud.e2e-spec.ts +++ b/packages/core/manifest/e2e/tests/crud.e2e-spec.ts @@ -3,7 +3,16 @@ import { Paginator, SelectOption } from '@mnfst/types' describe('CRUD (e2e)', () => { const dummyDog = { name: 'Fido', - age: 5 + age: 5, + website: 'https://example.com', + description: 'lorem ipsum', + birthdate: new Date().toISOString(), + password: 'password', + price: 100, + isGoodBoy: true, + acquiredAt: new Date().toISOString(), + email: 'test@example.com', + favoriteToy: 'ball' } it('POST /dynamic/:entity', async () => { @@ -12,20 +21,50 @@ describe('CRUD (e2e)', () => { expect(response.status).toBe(201) }) - it('GET /dynamic/:entity', async () => { - const response = await global.request.get('/dynamic/dogs') + describe('GET /dynamic/:entity', () => { + it('should return all items', async () => { + const response = await global.request.get('/dynamic/dogs') + + expect(response.status).toBe(200) + expect(response.body).toMatchObject>({ + data: expect.any(Array), + currentPage: expect.any(Number), + lastPage: expect.any(Number), + from: expect.any(Number), + to: expect.any(Number), + total: expect.any(Number), + perPage: expect.any(Number) + }) + expect(response.body.data.length).toBe(1) + }) - expect(response.status).toBe(200) - expect(response.body).toMatchObject>({ - data: expect.any(Array), - currentPage: expect.any(Number), - lastPage: expect.any(Number), - from: expect.any(Number), - to: expect.any(Number), - total: expect.any(Number), - perPage: expect.any(Number) + it('should filter items by field', async () => { + const response = await global.request.get( + `/dynamic/dogs?name_eq=${dummyDog.name}` + ) + + expect(response.status).toBe(200) + expect(response.body.data.length).toBe(1) + }) + + it('should filter items by relationship', async () => { + const bigNumber: number = 999 + + const response = await global.request.get( + `/dynamic/dogs?relations=owner&owner.id_eq=${bigNumber}` + ) + + expect(response.status).toBe(200) + expect(response.body.data.length).toBe(0) + }) + + it('should return a 400 error if the filter field does not exist', async () => { + const response = await global.request.get( + `/dynamic/dogs?invalidField_eq=${dummyDog.name}` + ) + + expect(response.status).toBe(400) }) - expect(response.body.data.length).toBe(1) }) it('GET /dynamic/:entity/select-options', async () => { diff --git a/packages/core/manifest/e2e/tests/open-api.e2e-spec.ts b/packages/core/manifest/e2e/tests/open-api.e2e-spec.ts new file mode 100644 index 00000000..4976b38c --- /dev/null +++ b/packages/core/manifest/e2e/tests/open-api.e2e-spec.ts @@ -0,0 +1,8 @@ +describe('Open API (e2e)', () => { + it('GET /api', async () => { + const response = await global.request.get('/api') + + expect(response.status).toBe(200) + expect(response.text).toContain('') + }) +}) diff --git a/packages/core/manifest/package-lock.json b/packages/core/manifest/package-lock.json index d827a576..b31564d2 100644 --- a/packages/core/manifest/package-lock.json +++ b/packages/core/manifest/package-lock.json @@ -10,12 +10,13 @@ "license": "MIT", "dependencies": { "@faker-js/faker": "^8.4.1", - "@mnfst/admin": "^0.1.0-alpha.4", - "@mnfst/types": "^0.1.0-alpha.3", + "@mnfst/admin": "^0.1.0-alpha.5", + "@mnfst/types": "^0.1.0-alpha.4", "@nestjs/common": "^10.3.3", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.3.3", "@nestjs/platform-express": "^10.3.3", + "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", "ajv": "^8.12.0", "better-sqlite3": "^9.6.0", @@ -1819,10 +1820,15 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" + }, "node_modules/@mnfst/admin": { - "version": "0.1.0-alpha.4", - "resolved": "https://registry.npmjs.org/@mnfst/admin/-/admin-0.1.0-alpha.4.tgz", - "integrity": "sha512-iA78+xb8raoBrdbmj65dDvkB8vIoKyGfDR0KxWjJOok1dLG0zlnxV+5T6a4BVYD5UhSqNtBu8tO2N9OZLX4HHA==", + "version": "0.1.0-alpha.5", + "resolved": "https://registry.npmjs.org/@mnfst/admin/-/admin-0.1.0-alpha.5.tgz", + "integrity": "sha512-TaUz4T2Kivn3LNXjoSSzO4ADoWUI7z18UW12HxhVJj2t7uz+cItGQeTKYywQCW68QtVmwdzQH8AZU3oyg/AvVw==", "dependencies": { "@angular/animations": "^17.3.1", "@angular/common": "^17.3.1", @@ -1833,7 +1839,7 @@ "@angular/platform-browser-dynamic": "^17.3.1", "@angular/router": "^17.3.1", "@auth0/angular-jwt": "^5.2.0", - "@mnfst/types": "^0.1.0-alpha.3", + "@mnfst/types": "^0.1.0-alpha.4", "bulma": "^0.9.4", "bulma-tooltip": "^3.0.2", "rxjs": "~7.5.0", @@ -1850,9 +1856,9 @@ } }, "node_modules/@mnfst/types": { - "version": "0.1.0-alpha.3", - "resolved": "https://registry.npmjs.org/@mnfst/types/-/types-0.1.0-alpha.3.tgz", - "integrity": "sha512-dz/G7m0JYD8VA4MI65F8L4K6nS12LxSyL0tJVsU723sQcPjKnFzKe+7J+OpxQTXVnnY494aWr+vXPLZV/LNwJQ==" + "version": "0.1.0-alpha.4", + "resolved": "https://registry.npmjs.org/@mnfst/types/-/types-0.1.0-alpha.4.tgz", + "integrity": "sha512-kn4aJtRRW8iG5yByKP+iUSl4T8NRC5EiLKK19pRA6hU9smqFz8TU7yVMUUja1ZmmX1xY3NkfDeFa7CK4CIMRfw==" }, "node_modules/@nestjs/cli": { "version": "10.3.2", @@ -2040,6 +2046,25 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.5.tgz", + "integrity": "sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "10.3.8", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.8.tgz", @@ -2082,6 +2107,38 @@ "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, + "node_modules/@nestjs/swagger": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.1.tgz", + "integrity": "sha512-LUC4mr+5oAleEC/a2j8pNRh1S5xhKXJ1Gal5ZdRjt9XebQgbngXCdW7JTA9WOEcwGtFZN9EnKYdquzH971LZfw==", + "dependencies": { + "@microsoft/tsdoc": "^0.14.2", + "@nestjs/mapped-types": "2.0.5", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "5.11.2" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0 || ^7.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "10.3.3", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.3.tgz", @@ -10062,6 +10119,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.11.2.tgz", + "integrity": "sha512-jQG0cRgJNMZ7aCoiFofnoojeSaa/+KgWaDlfgs8QN+BXoGMpxeMVY5OEnjq4OlNvF3yjftO8c9GRAgcHlO+u7A==" + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/packages/core/manifest/package.json b/packages/core/manifest/package.json index 4f153f6b..a567ddbe 100644 --- a/packages/core/manifest/package.json +++ b/packages/core/manifest/package.json @@ -1,6 +1,6 @@ { "name": "manifest", - "version": "4.0.0-alpha.8", + "version": "4.0.0-alpha.9", "description": "Effortless backends", "author": "Manifest", "license": "MIT", @@ -37,6 +37,7 @@ "test:e2e": "jest --config ./e2e/jest-e2e.json", "test:e2e:ci": "jest --config ./e2e/jest-e2e.json --ci", "generate-types": "ts-node src/manifest/json-schema/GenerateTypes.ts", + "link-local-types": "cd ../types && npm install && npm run build && npm link && cd ../manifest && npm link @mnfst/types", "prebuild": "npm run generate-types" }, "files": [ @@ -46,12 +47,13 @@ ], "dependencies": { "@faker-js/faker": "^8.4.1", - "@mnfst/admin": "^0.1.0-alpha.4", - "@mnfst/types": "^0.1.0-alpha.3", + "@mnfst/admin": "^0.1.0-alpha.5", + "@mnfst/types": "^0.1.0-alpha.4", "@nestjs/common": "^10.3.3", "@nestjs/config": "^3.2.0", "@nestjs/core": "^10.3.3", "@nestjs/platform-express": "^10.3.3", + "@nestjs/swagger": "^7.3.1", "@nestjs/typeorm": "^10.0.2", "ajv": "^8.12.0", "better-sqlite3": "^9.6.0", diff --git a/packages/core/manifest/src/app.module.ts b/packages/core/manifest/src/app.module.ts index 7f15e8ac..98c9c77c 100644 --- a/packages/core/manifest/src/app.module.ts +++ b/packages/core/manifest/src/app.module.ts @@ -16,6 +16,7 @@ import { ManifestModule } from './manifest/manifest.module' import { SeedModule } from './seed/seed.module' import { HealthModule } from './health/health.module' import { BetterSqlite3ConnectionOptions } from 'typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions' +import { OpenApiModule } from './open-api/open-api.module' @Module({ imports: [ @@ -44,7 +45,8 @@ import { BetterSqlite3ConnectionOptions } from 'typeorm/driver/better-sqlite3/Be CrudModule, AuthModule, LoggerModule, - HealthModule + HealthModule, + OpenApiModule ] }) export class AppModule { @@ -57,8 +59,9 @@ 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) { + if (!isSeed && !isTest && !isProduction) { this.loggerService.initMessage() } } diff --git a/packages/core/manifest/src/auth/auth.controller.ts b/packages/core/manifest/src/auth/auth.controller.ts index 9df304f6..ba354894 100644 --- a/packages/core/manifest/src/auth/auth.controller.ts +++ b/packages/core/manifest/src/auth/auth.controller.ts @@ -4,7 +4,9 @@ import { AuthenticableEntity } from '@mnfst/types' import { Request } from 'express' import { AuthService } from './auth.service' import { SignupAuthenticableEntityDto } from './dtos/signup-authenticable-entity.dto' +import { ApiTags } from '@nestjs/swagger' +@ApiTags('Auth') @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} 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 5524ddb2..491b10cd 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,10 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger' import { IsEmail, IsNotEmpty } from 'class-validator' export class SignupAuthenticableEntityDto { + @ApiProperty({ + description: 'The email of the user', + example: 'admin@manifest.build' + }) @IsNotEmpty() @IsEmail() public email: string + @ApiProperty({ + description: 'The password of the user', + example: 'admin' + }) @IsNotEmpty() public password: string } diff --git a/packages/core/manifest/src/config/paths.ts b/packages/core/manifest/src/config/paths.ts index 6ba5480d..73e702f8 100644 --- a/packages/core/manifest/src/config/paths.ts +++ b/packages/core/manifest/src/config/paths.ts @@ -1,7 +1,8 @@ -export default (): { paths: { admin: string } } => { +export default (): { paths: { admin: string; database: string } } => { return { paths: { - admin: `${process.cwd()}/node_modules/@mnfst/admin/dist` + admin: `${process.cwd()}/node_modules/@mnfst/admin/dist`, + database: `${process.cwd()}/manifest/backend.yml` } } } diff --git a/packages/core/manifest/src/constants.ts b/packages/core/manifest/src/constants.ts index 9b62e682..587175a1 100644 --- a/packages/core/manifest/src/constants.ts +++ b/packages/core/manifest/src/constants.ts @@ -1,7 +1,3 @@ -// Manifest. -export const MANIFEST_FOLDER_NAME = 'manifest' -export const MANIFEST_FILE_NAME = 'backend.yml' - // Default values. export const DEFAULT_PORT = 1111 export const DEFAULT_RESULTS_PER_PAGE = 20 diff --git a/packages/core/manifest/src/crud/services/crud.service.ts b/packages/core/manifest/src/crud/services/crud.service.ts index 3f7a420e..ae2c422e 100644 --- a/packages/core/manifest/src/crud/services/crud.service.ts +++ b/packages/core/manifest/src/crud/services/crud.service.ts @@ -77,13 +77,6 @@ export class CrudService { let query: SelectQueryBuilder = entityRepository.createQueryBuilder('entity') - // Apply filters. - this.filterQuery({ - query, - queryParams, - entityManifest - }) - // Select only visible props. query.select( this.getVisibleProps({ @@ -91,6 +84,21 @@ export class CrudService { }) ) + // Load relations. + this.loadRelations({ + query, + entityMetadata, + belongsTo: entityManifest.belongsTo, + requestedRelations: queryParams?.relations?.toString().split(',') + }) + + // Apply filters. + this.filterQuery({ + query, + queryParams, + entityManifest + }) + // Apply ordering. if (queryParams?.orderBy) { if ( @@ -112,14 +120,6 @@ export class CrudService { query.orderBy('entity.id', 'DESC') } - // Load relations. - this.loadRelations({ - query, - entityMetadata, - belongsTo: entityManifest.belongsTo, - requestedRelations: queryParams?.relations?.toString().split(',') - }) - // Paginate. return this.paginationService.paginate({ query, @@ -382,12 +382,14 @@ export class CrudService { const suffix: WhereKeySuffix = Object.values(WhereKeySuffix) .reverse() .find((suffix) => key.includes(suffix)) + if (!suffix) { throw new HttpException( 'Query param key should include an operator suffix', HttpStatus.BAD_REQUEST ) } + const operator: WhereOperator = HelperService.getRecordKeyByValue( whereOperatorKeySuffix, suffix @@ -398,27 +400,46 @@ export class CrudService { (prop: PropertyManifest) => prop.name === propName && !prop.hidden ) - if (!prop) { + const relation: RelationshipManifest = entityManifest.belongsTo.find( + (belongsTo: RelationshipManifest) => + belongsTo.name === propName.split('.')[0] + ) + + if (!prop && !relation) { throw new HttpException( `Property ${propName} does not exist in ${entityManifest.className}`, HttpStatus.BAD_REQUEST ) } + + let whereKey: string + + if (relation) { + const aliasName: string = HelperService.camelCaseTwoStrings( + 'entity', + relation.name + ) + whereKey = `${aliasName}.${propName.split('.')[1]}` + } else { + whereKey = `entity.${propName}` + } + // Allow "true" and "false" to be used for boolean props for convenience. - if (prop.type === PropType.Boolean) { + if (prop && prop.type === PropType.Boolean) { if (value === 'true') { value = '1' } else if (value === 'false') { value = '0' } } + // Finally and the where query. "In" is a bit special as it expects an array of values. if (operator === WhereOperator.In) { - query.where(`entity.${propName} ${operator} (:...value)`, { + query.where(`${whereKey} ${operator} (:...value)`, { value: JSON.parse(`[${value}]`) }) } else { - query.where(`entity.${propName} ${operator} :value`, { + query.where(`${whereKey} ${operator} :value`, { value }) } diff --git a/packages/core/manifest/src/crud/tests/crud.service.spec.ts b/packages/core/manifest/src/crud/tests/crud.service.spec.ts new file mode 100644 index 00000000..9c2edc51 --- /dev/null +++ b/packages/core/manifest/src/crud/tests/crud.service.spec.ts @@ -0,0 +1,41 @@ +import { CrudService } from '../services/crud.service' +import { Test, TestingModule } from '@nestjs/testing' +import { ManifestService } from '../../manifest/services/manifest/manifest.service' +import { PaginationService } from '../services/pagination.service' +import { EntityService } from '../../entity/services/entity/entity.service' + +describe('CrudService', () => { + let service: CrudService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CrudService, + { + provide: ManifestService, + useValue: { + getEntityRepository: jest.fn() + } + }, + { + provide: PaginationService, + useValue: { + paginate: jest.fn() + } + }, + { + provide: EntityService, + useValue: { + findOne: jest.fn() + } + } + ] + }).compile() + + service = module.get(CrudService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/packages/core/manifest/src/entity/records/prop-type-column-types.ts b/packages/core/manifest/src/entity/records/prop-type-column-types.ts index abd28a31..76014131 100644 --- a/packages/core/manifest/src/entity/records/prop-type-column-types.ts +++ b/packages/core/manifest/src/entity/records/prop-type-column-types.ts @@ -9,6 +9,7 @@ export const propTypeColumnTypes: Record = { [PropType.Text]: 'text', [PropType.Money]: 'decimal', [PropType.Date]: 'date', + [PropType.Timestamp]: 'text', [PropType.Email]: 'varchar', [PropType.Boolean]: 'boolean', [PropType.Password]: 'varchar', diff --git a/packages/core/manifest/src/entity/records/prop-type-seed-functions.ts b/packages/core/manifest/src/entity/records/prop-type-seed-functions.ts index 3e3dc1a1..e57233f9 100644 --- a/packages/core/manifest/src/entity/records/prop-type-seed-functions.ts +++ b/packages/core/manifest/src/entity/records/prop-type-seed-functions.ts @@ -16,6 +16,7 @@ export const propTypeSeedFunctions: Record any> = { dec: 2 }), [PropType.Date]: () => faker.date.past(), + [PropType.Timestamp]: () => faker.date.recent(), [PropType.Email]: () => faker.internet.email(), [PropType.Boolean]: () => faker.datatype.boolean(), [PropType.Password]: () => SHA3('manifest').toString(), diff --git a/packages/core/manifest/src/entity/services/entity/entity.service.spec.ts b/packages/core/manifest/src/entity/services/entity/entity.service.spec.ts index b72e8b64..70ce25a9 100644 --- a/packages/core/manifest/src/entity/services/entity/entity.service.spec.ts +++ b/packages/core/manifest/src/entity/services/entity/entity.service.spec.ts @@ -5,6 +5,7 @@ import { DataSource } from 'typeorm' describe('EntityService', () => { let service: EntityService + let dataSource: DataSource beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -27,9 +28,30 @@ describe('EntityService', () => { }).compile() service = module.get(EntityService) + dataSource = module.get(DataSource) }) it('should be defined', () => { expect(service).toBeDefined() }) + + describe('getEntityRepository', () => { + it('should fail if no entity metadata or entity slug is provided', () => { + expect(() => { + service.getEntityRepository({}) + }).toThrow() + }) + + it('should return a repository', () => { + const entityMetadata = { + target: 'Entity' + } as any + + const result = service.getEntityRepository({ + entityMetadata + }) + + expect(dataSource.getRepository).toHaveBeenCalledWith('Entity') + }) + }) }) diff --git a/packages/core/manifest/src/logger/logger.service.ts b/packages/core/manifest/src/logger/logger.service.ts index 6bbeb1dc..948f0f97 100644 --- a/packages/core/manifest/src/logger/logger.service.ts +++ b/packages/core/manifest/src/logger/logger.service.ts @@ -17,13 +17,21 @@ export class LoggerService { console.log() + console.log(chalk.blue('Manifest backend successfully started! ')) + console.log() + console.log( chalk.blue( - '🎉 Manifest successfully started! See your admin panel: ', + '🖥️ Admin Panel: ', chalk.underline.blue(`http://localhost:${port}`) ) ) - + console.log( + chalk.blue( + '📚 API Doc: ', + chalk.underline.blue(`http://localhost:${port}/api`) + ) + ) console.log() } } diff --git a/packages/core/manifest/src/main.ts b/packages/core/manifest/src/main.ts index f72f0cdb..b5adf486 100644 --- a/packages/core/manifest/src/main.ts +++ b/packages/core/manifest/src/main.ts @@ -1,12 +1,14 @@ import { ValidationPipe } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { NestFactory } from '@nestjs/core' +import { SwaggerModule } from '@nestjs/swagger' import connectLiveReload from 'connect-livereload' import * as express from 'express' import * as livereload from 'livereload' import { join } from 'path' import { AppModule } from './app.module' import { DEFAULT_PORT } from './constants' +import { OpenApiService } from './open-api/services/open-api.service' async function bootstrap() { const app = await NestFactory.create(AppModule, { @@ -21,10 +23,11 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe()) // Live reload. - const production: boolean = configService.get('NODE_ENV') === 'production' + const isProduction: boolean = configService.get('NODE_ENV') === 'production' + const isTest: boolean = configService.get('NODE_ENV') === 'test' // Reload the browser when server files change. - if (!production) { + if (!isProduction && !isTest) { const liveReloadServer = livereload.createServer() liveReloadServer.server.once('connection', () => { setTimeout(() => { @@ -47,6 +50,1710 @@ async function bootstrap() { } }) + if (!isProduction) { + const openApiService: OpenApiService = app.get(OpenApiService) + + SwaggerModule.setup('api', app, openApiService.generateOpenApiObject(), { + customfavIcon: 'assets/images/favicon.png', + customSiteTitle: 'Manifest API Doc', + customCss: ` + +.swagger-ui html { + box-sizing: border-box +} + +.swagger-ui *, .swagger-ui :after, .swagger-ui :before { + box-sizing: inherit +} + +.swagger-ui body { + margin: 0; + background: #F6F7F9 +} + +.swagger-ui .wrapper { + width: 100%; + max-width: 1460px; + margin: 0 auto; + padding: 0 20px +} + +.swagger-ui .opblock-tag-section { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column +} + +.swagger-ui .opblock-tag { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + padding: 10px 20px 10px 10px; + cursor: pointer; + -webkit-transition: all .2s; + transition: all .2s; + border-bottom: 1px solid #EAEAEF; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center +} + +.swagger-ui .opblock-tag:hover { + background: rgba(57, 57, 60, .02) +} + +.swagger-ui .opblock-tag { + font-size: 24px; + margin: 0 0 5px; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui .opblock-tag.no-desc span { + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1 +} + +.swagger-ui .opblock-tag svg { + -webkit-transition: all .4s; + transition: all .4s +} + +.swagger-ui .opblock-tag small { + font-size: 14px; + font-weight: 400; + padding: 0 10px; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + font-family: Open Sans, sans-serif; + color: #3b4151 +} + +.swagger-ui .parаmeter__type { + font-size: 12px; + padding: 5px 0; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #3b4151 +} + +.swagger-ui .view-line-link { + position: relative; + top: 3px; + width: 20px; + margin: 0 5px; + cursor: pointer; + -webkit-transition: all .5s; + transition: all .5s +} + +.swagger-ui .opblock { + margin: 0 0 15px; + border: 1px solid #000; + border-radius: 4px; + box-shadow: none; +} + +.swagger-ui .opblock.is-open .opblock-summary { + border-bottom: 1px solid #000 +} + +.swagger-ui .opblock .opblock-section-header { + padding: 8px 20px; + background: hsla(0, 0%, 100%, .8); + box-shadow: none; +} + +.swagger-ui .opblock .opblock-section-header, .swagger-ui .opblock .opblock-section-header label { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center +} + +.swagger-ui .opblock .opblock-section-header label { + font-size: 12px; + font-weight: 700; + margin: 0; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui .opblock .opblock-section-header label span { + padding: 0 10px 0 0 +} + +.swagger-ui .opblock .opblock-section-header h4 { + font-size: 14px; + margin: 0; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui .opblock .opblock-summary-method { + font-size: 14px; + font-weight: 700; + min-width: 80px; + padding: 6px 15px; + text-align: center; + border-radius: 3px; + background: #000; + text-shadow: 0 1px 0 rgba(0, 0, 0, .1); + font-family: Titillium Web, sans-serif; + color: #fff +} + +.swagger-ui .opblock .opblock-summary-path, .swagger-ui .opblock .opblock-summary-path__deprecated { + font-size: 16px; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + padding: 0 10px; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #3b4151; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center +} + +.swagger-ui .opblock .opblock-summary-path .view-line-link, .swagger-ui .opblock .opblock-summary-path__deprecated .view-line-link { + position: relative; + top: 2px; + width: 0; + margin: 0; + cursor: pointer; + -webkit-transition: all .5s; + transition: all .5s +} + +.swagger-ui .opblock .opblock-summary-path:hover .view-line-link, .swagger-ui .opblock .opblock-summary-path__deprecated:hover .view-line-link { + width: 18px; + margin: 0 5px +} + +.swagger-ui .opblock .opblock-summary-path__deprecated { + text-decoration: line-through +} + +.swagger-ui .opblock .opblock-summary-description { + font-size: 13px; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + font-family: Open Sans, sans-serif; + color: #3b4151 +} + +.swagger-ui .opblock .opblock-summary { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + padding: 5px; + cursor: pointer; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center +} + +.swagger-ui .opblock.opblock-post { + border-color: #57b3a0; + background: rgba(73, 204, 144, .1) +} + +.swagger-ui .opblock.opblock-post .opblock-summary-method { + background: #57b3a0 +} + +.swagger-ui .opblock.opblock-post .opblock-summary { + border-color: #57b3a0 +} + +.swagger-ui .opblock.opblock-put { + border-color: #f8a94a; + background: rgba(252, 161, 48, .1) +} + +.swagger-ui .opblock.opblock-put .opblock-summary-method { + background: #f8a94a +} + +.swagger-ui .opblock.opblock-put .opblock-summary { + border-color: #f8a94a +} + +.swagger-ui .opblock.opblock-delete { + border-color: #f46470; + background: rgba(249, 62, 62, .1) +} + +.swagger-ui .opblock.opblock-delete .opblock-summary-method { + background: #f46470 +} + +.swagger-ui .opblock.opblock-delete .opblock-summary { + border-color: #f46470 +} + +.swagger-ui .opblock.opblock-get { + border-color: #2430F0; + background: #F5F7F9 +} + +.swagger-ui .opblock.opblock-get .opblock-summary-method { + background: #2430F0 +} + +.swagger-ui .opblock.opblock-get .opblock-summary { + border-color: #2430F0 +} + +.swagger-ui .opblock.opblock-patch { + border-color: #50e3c2; + background: rgba(80, 227, 194, .1) +} + +.swagger-ui .opblock.opblock-patch .opblock-summary-method { + background: #50e3c2 +} + +.swagger-ui .opblock.opblock-patch .opblock-summary { + border-color: #50e3c2 +} + .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span:after { + background: #f8a94a; + } + + .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span:after { + background: #57b3a0; + } + +.swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span:after { +background: #f46470; +} + + +.swagger-ui .opblock.opblock-head { + border-color: #9012fe; + background: rgba(144, 18, 254, .1) +} + +.swagger-ui .opblock.opblock-head .opblock-summary-method { + background: #9012fe +} + +.swagger-ui .opblock.opblock-head .opblock-summary { + border-color: #9012fe +} + +.swagger-ui .opblock.opblock-options { + border-color: #0d5aa7; + background: rgba(13, 90, 167, .1) +} + +.swagger-ui .opblock.opblock-options .opblock-summary-method { + background: #0d5aa7 +} + +.swagger-ui .opblock.opblock-options .opblock-summary { + border-color: #0d5aa7 +} + +.swagger-ui .opblock.opblock-deprecated { + opacity: .6; + border-color: #ebebeb; + background: hsla(0, 0%, 92%, .1) +} + +.swagger-ui .opblock.opblock-deprecated .opblock-summary-method { + background: #ebebeb +} + +.swagger-ui .opblock.opblock-deprecated .opblock-summary { + border-color: #ebebeb +} + +.swagger-ui .tab { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + margin: 20px 0 10px; + padding: 0; + list-style: none +} + +.swagger-ui .tab li { + font-size: 12px; + min-width: 100px; + min-width: 90px; + padding: 0; + cursor: pointer; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui .tab li:first-of-type { + position: relative; + padding-left: 0 +} + +.swagger-ui .tab li:first-of-type:after { + position: absolute; + top: 0; + right: 6px; + width: 1px; + height: 100%; + content: ""; + background: rgba(0, 0, 0, .2) +} + +.swagger-ui .tab li.active { + font-weight: 700 +} + +.swagger-ui .opblock-description-wrapper, .swagger-ui .opblock-title_normal { + padding: 15px 20px +} + +.swagger-ui .opblock-description-wrapper, .swagger-ui .opblock-description-wrapper h4, .swagger-ui .opblock-title_normal, .swagger-ui .opblock-title_normal h4 { + font-size: 12px; + margin: 0 0 5px; + font-family: Open Sans, sans-serif; + color: #3b4151 +} + +.swagger-ui .opblock-description-wrapper p, .swagger-ui .opblock-title_normal p { + font-size: 14px; + margin: 0; + font-family: Open Sans, sans-serif; + color: #3b4151 +} + +.swagger-ui .execute-wrapper { + padding: 20px; + text-align: right +} + +.swagger-ui .execute-wrapper .btn { + width: 100%; + padding: 8px 40px +} + +.swagger-ui .body-param-options { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column +} + +.swagger-ui .body-param-options .body-param-edit { + padding: 10px 0 +} + +.swagger-ui .body-param-options label { + padding: 8px 0 +} + +.swagger-ui .body-param-options label select { + margin: 3px 0 0 +} + +.swagger-ui .responses-inner { + padding: 20px +} + +.swagger-ui .responses-inner h4, .swagger-ui .responses-inner h5 { + font-size: 12px; + margin: 10px 0 5px; + font-family: Open Sans, sans-serif; + color: #3b4151 +} + +.swagger-ui .response-col_status { + font-size: 14px; + font-family: Open Sans, sans-serif; + color: #3b4151 +} + +.swagger-ui .response-col_status .response-undocumented { + font-size: 11px; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #999 +} + +.swagger-ui .response-col_description__inner span { + font-size: 12px; + font-style: italic; + display: block; + margin: 10px 0; + padding: 10px; + border-radius: 4px; + background: #41444e; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #fff +} + +.swagger-ui .response-col_description__inner span p { + margin: 0 +} + +.swagger-ui .opblock-body pre { + font-size: 12px; + margin: 0; + padding: 10px; + white-space: pre-wrap; + border-radius: 4px; + background: #41444e; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #fff +} + +.swagger-ui .opblock-body pre span { + color: #fff!important +} + +.swagger-ui .scheme-container { + margin: 0 0 20px; + padding: 30px 0; + background: #fff; + box-shadow: none; +} + +.swagger-ui .scheme-container .schemes { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center +} + +.swagger-ui .scheme-container .schemes>label { + font-size: 12px; + font-weight: 700; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + margin: -20px 15px 0 0; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui .scheme-container .schemes>label select { + min-width: 130px; + text-transform: uppercase +} + +.swagger-ui .loading-container { + padding: 40px 0 60px +} + +.swagger-ui .loading-container .loading { + position: relative +} + +.swagger-ui .loading-container .loading:after { + font-size: 10px; + font-weight: 700; + position: absolute; + top: 50%; + left: 50%; + content: "loading"; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + text-transform: uppercase; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui .loading-container .loading:before { + position: absolute; + top: 50%; + left: 50%; + display: block; + width: 60px; + height: 60px; + margin: -30px; + content: ""; + -webkit-animation: rotation 1s infinite linear, opacity .5s; + animation: rotation 1s infinite linear, opacity .5s; + opacity: 1; + border: 2px solid rgba(85, 85, 85, .1); + border-top-color: rgba(0, 0, 0, .6); + border-radius: 100%; + -webkit-backface-visibility: hidden; + backface-visibility: hidden +} + +@-webkit-keyframes rotation { + to { + -webkit-transform: rotate(1turn); + transform: rotate(1turn) + } +} + +@keyframes rotation { + to { + -webkit-transform: rotate(1turn); + transform: rotate(1turn) + } +} + +@-webkit-keyframes blinker { + 50% { + opacity: 0 + } +} + +@keyframes blinker { + 50% { + opacity: 0 + } +} + +.swagger-ui .btn { + font-size: 14px; + font-weight: 700; + padding: 5px 23px; + -webkit-transition: all .3s; + transition: all .3s; + border: 2px solid #888; + border-radius: 4px; + background: transparent; + box-shadow: none; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui .btn[disabled] { + cursor: not-allowed; + opacity: .3 +} + +.swagger-ui .btn:hover { + box-shadow: none; +} + +.swagger-ui .btn.cancel { + border-color: #f46470; + font-family: Titillium Web, sans-serif; + color: #f46470 +} + +.swagger-ui .btn.authorize { + line-height: 1; + display: inline; + color: #57b3a0; + border-color: #57b3a0 +} + +.swagger-ui .btn.authorize span { + float: left; + padding: 4px 20px 0 0 +} + +.swagger-ui .btn.authorize svg { + fill: #57b3a0 +} + +.swagger-ui .btn.execute { + -webkit-animation: pulse 2s infinite; + animation: pulse 2s infinite; + color: #fff; + border-color: #2430F0 +} + +@-webkit-keyframes pulse { + 0% { + color: #fff; + background: #2430F0; + box-shadow: none; + } + 70% { + box-shadow: none; + } + to { + color: #fff; + background: #2430F0; + box-shadow: none; + } +} + +@keyframes pulse { + 0% { + color: #fff; + background: #2430F0; + box-shadow: none; + } + 70% { + box-shadow: none; + } + to { + color: #fff; + background: #2430F0; + box-shadow: none; + } +} + +.swagger-ui .btn-group { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + padding: 30px +} + +.swagger-ui .btn-group .btn { + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1 +} + +.swagger-ui .btn-group .btn:first-child { + border-radius: 4px 0 0 4px +} + +.swagger-ui .btn-group .btn:last-child { + border-radius: 0 4px 4px 0 +} + +.swagger-ui .authorization__btn { + padding: 0 10px; + border: none; + background: none +} + +.swagger-ui .authorization__btn.locked { + opacity: 1 +} + +.swagger-ui .authorization__btn.unlocked { + opacity: .4 +} + +.swagger-ui .expand-methods, .swagger-ui .expand-operation { + border: none; + background: none +} + +.swagger-ui .expand-methods svg, .swagger-ui .expand-operation svg { + width: 20px; + height: 20px +} + +.swagger-ui .expand-methods { + padding: 0 10px +} + +.swagger-ui .expand-methods:hover svg { + fill: #444 +} + +.swagger-ui .expand-methods svg { + -webkit-transition: all .3s; + transition: all .3s; + fill: #777 +} + +.swagger-ui button { + cursor: pointer; + outline: none +} + +.swagger-ui select { + font-size: 14px; + font-weight: 700; + padding: 5px 40px 5px 10px; + border: 2px solid #41444e; + border-radius: 4px; + background: #f7f7f7 url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+ICAgIDxwYXRoIGQ9Ik0xMy40MTggNy44NTljLjI3MS0uMjY4LjcwOS0uMjY4Ljk3OCAwIC4yNy4yNjguMjcyLjcwMSAwIC45NjlsLTMuOTA4IDMuODNjLS4yNy4yNjgtLjcwNy4yNjgtLjk3OSAwbC0zLjkwOC0zLjgzYy0uMjctLjI2Ny0uMjctLjcwMSAwLS45NjkuMjcxLS4yNjguNzA5LS4yNjguOTc4IDBMMTAgMTFsMy40MTgtMy4xNDF6Ii8+PC9zdmc+) right 10px center no-repeat; + background-size: 20px; + box-shadow: none; + font-family: Titillium Web, sans-serif; + color: #3b4151; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none +} + +.swagger-ui select[multiple] { + margin: 5px 0; + padding: 5px; + background: #f7f7f7 +} + +.swagger-ui .opblock-body select { + min-width: 230px +} + +.swagger-ui label { + font-size: 12px; + font-weight: 700; + margin: 0 0 5px; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui input[type=email], .swagger-ui input[type=password], .swagger-ui input[type=search], .swagger-ui input[type=text] { + min-width: 100px; + margin: 5px 0; + padding: 8px 10px; + border: 1px solid #d9d9d9; + border-radius: 4px; + background: #fff +} + +.swagger-ui input[type=email].invalid, .swagger-ui input[type=password].invalid, .swagger-ui input[type=search].invalid, .swagger-ui input[type=text].invalid { + -webkit-animation: shake .4s 1; + animation: shake .4s 1; + border-color: #f46470; + background: #feebeb +} + +@-webkit-keyframes shake { + 10%, 90% { + -webkit-transform: translate3d(-1px, 0, 0); + transform: translate3d(-1px, 0, 0) + } + 20%, 80% { + -webkit-transform: translate3d(2px, 0, 0); + transform: translate3d(2px, 0, 0) + } + 30%, 50%, 70% { + -webkit-transform: translate3d(-4px, 0, 0); + transform: translate3d(-4px, 0, 0) + } + 40%, 60% { + -webkit-transform: translate3d(4px, 0, 0); + transform: translate3d(4px, 0, 0) + } +} + +@keyframes shake { + 10%, 90% { + -webkit-transform: translate3d(-1px, 0, 0); + transform: translate3d(-1px, 0, 0) + } + 20%, 80% { + -webkit-transform: translate3d(2px, 0, 0); + transform: translate3d(2px, 0, 0) + } + 30%, 50%, 70% { + -webkit-transform: translate3d(-4px, 0, 0); + transform: translate3d(-4px, 0, 0) + } + 40%, 60% { + -webkit-transform: translate3d(4px, 0, 0); + transform: translate3d(4px, 0, 0) + } +} + +.swagger-ui textarea { + font-size: 12px; + width: 100%; + min-height: 280px; + padding: 10px; + border: none; + border-radius: 4px; + outline: none; + background: hsla(0, 0%, 100%, .8); + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #3b4151 +} + +.swagger-ui textarea:focus { + border: 2px solid #2430F0 +} + +.swagger-ui textarea.curl { + font-size: 12px; + min-height: 100px; + margin: 0; + padding: 10px; + resize: none; + border-radius: 4px; + background: #41444e; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #fff +} + .swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span:after { + background: #2430F0; + } + + + .swagger-ui .response-control-media-type__accept-message { + color: #57b3a0 + } + .swagger-ui .response-control-media-type--accept-controller select { + border-color: #57b3a0} + + .swagger-ui .checkbox { + padding: 5px 0 10px; + -webkit-transition: opacity .5s; + transition: opacity .5s; + color: #333 +} + +.swagger-ui .checkbox label { + display: -webkit-box; + display: -ms-flexbox; + display: flex +} + +.swagger-ui .checkbox p { + font-weight: 400!important; + font-style: italic; + margin: 0!important; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #3b4151 +} + +.swagger-ui .checkbox input[type=checkbox] { + display: none +} + +.swagger-ui .checkbox input[type=checkbox]+label>.item { + position: relative; + top: 3px; + display: inline-block; + width: 16px; + height: 16px; + margin: 0 8px 0 0; + padding: 5px; + cursor: pointer; + border-radius: 1px; + background: #e8e8e8; + box-shadow: none; + -webkit-box-flex: 0; + -ms-flex: none; + flex: none +} + +.swagger-ui .checkbox input[type=checkbox]+label>.item:active { + -webkit-transform: scale(.9); + transform: scale(.9) +} + +.swagger-ui .checkbox input[type=checkbox]:checked+label>.item { + background: #e8e8e8 url("data:image/svg+xml;charset=utf-8,%3Csvg width='10' height='8' viewBox='3 7 10 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%2341474E' fill-rule='evenodd' d='M6.333 15L3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z'/%3E%3C/svg%3E") 50% no-repeat +} + +.swagger-ui .dialog-ux { + position: fixed; + z-index: 9999; + top: 0; + right: 0; + bottom: 0; + left: 0 +} + +.swagger-ui .dialog-ux .backdrop-ux { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(0, 0, 0, .8) +} + +.swagger-ui .dialog-ux .modal-ux { + position: absolute; + z-index: 9999; + top: 50%; + left: 50%; + width: 100%; + min-width: 300px; + max-width: 650px; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + border: 1px solid #ebebeb; + border-radius: 4px; + background: #fff; + box-shadow: none; +} + +.swagger-ui .dialog-ux .modal-ux-content { + overflow-y: auto; + max-height: 540px; + padding: 20px +} + +.swagger-ui .dialog-ux .modal-ux-content p { + font-size: 12px; + margin: 0 0 5px; + color: #41444e; + font-family: Open Sans, sans-serif; + color: #3b4151 +} + +.swagger-ui .dialog-ux .modal-ux-content h4 { + font-size: 18px; + font-weight: 600; + margin: 15px 0 0; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui .dialog-ux .modal-ux-header { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + padding: 12px 0; + border-bottom: 1px solid #ebebeb; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center +} + +.swagger-ui .dialog-ux .modal-ux-header .close-modal { + padding: 0 10px; + border: none; + background: none; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none +} + +.swagger-ui .dialog-ux .modal-ux-header h3 { + font-size: 20px; + font-weight: 600; + margin: 0; + padding: 0 20px; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui .model { + font-size: 12px; + font-weight: 300; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #3b4151 +} + +.swagger-ui .model-toggle { + font-size: 10px; + position: relative; + top: 6px; + display: inline-block; + margin: auto .3em; + cursor: pointer; + -webkit-transition: -webkit-transform .15s ease-in; + transition: -webkit-transform .15s ease-in; + transition: transform .15s ease-in; + transition: transform .15s ease-in, -webkit-transform .15s ease-in; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-transform-origin: 50% 50%; + transform-origin: 50% 50% +} + +.swagger-ui .model-toggle.collapsed { + -webkit-transform: rotate(0deg); + transform: rotate(0deg) +} + +.swagger-ui .model-toggle:after { + display: block; + width: 20px; + height: 20px; + content: ""; + background: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E") 50% no-repeat; + background-size: 100% +} + +.swagger-ui .model-jump-to-path { + position: relative; + cursor: pointer +} + +.swagger-ui .model-jump-to-path .view-line-link { + position: absolute; + top: -.4em; + cursor: pointer +} + +.swagger-ui .model-title { + position: relative +} + +.swagger-ui .model-title:hover .model-hint { + visibility: visible +} + +.swagger-ui .model-hint { + position: absolute; + top: -1.8em; + visibility: hidden; + padding: .1em .5em; + white-space: nowrap; + color: #ebebeb; + border-radius: 4px; + background: rgba(0, 0, 0, .7) +} + +.swagger-ui section.models { + margin: 30px 0; + border: 1px solid #EAEAEF; + border-radius: 4px +} + +.swagger-ui section.models.is-open { + padding: 0 0 20px +} + +.swagger-ui section.models.is-open h4 { + margin: 0 0 5px; + border-bottom: 1px solid #EAEAEF +} + +.swagger-ui section.models.is-open h4 svg { + -webkit-transform: rotate(90deg); + transform: rotate(90deg) +} + +.swagger-ui section.models h4 { + font-size: 16px; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + margin: 0; + padding: 10px 20px 10px 10px; + cursor: pointer; + -webkit-transition: all .2s; + transition: all .2s; + font-family: Titillium Web, sans-serif; + color: #777; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center +} + +.swagger-ui section.models h4 svg { + -webkit-transition: all .4s; + transition: all .4s +} + +.swagger-ui section.models h4 span { + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1 +} + +.swagger-ui section.models h4:hover { + 57rg57a(60, 0, 0, .02) +} + +.swagger-ui section.models h5 { + font-size: 16px; + margin: 0 0 10px; + font-family: Titillium Web, sans-serif; + color: #777 +} + +.swagger-ui section.models .model-jump-to-path { + position: relative; + top: 5px +} + +.swagger-ui section.models .model-container { + margin: 0 20px 15px; + -webkit-transition: all .5s; + transition: all .5s; + border-radius: 4px; + background: rgba(0, 0, 0, .05) +} + +.swagger-ui section.models .model-container:hover { + background: rgba(0, 0, 0, .07) +} + +.swagger-ui section.models .model-container:first-of-type { + margin: 20px +} + +.swagger-ui section.models .model-container:last-of-type { + margin: 0 20px +} + +.swagger-ui section.models .model-box { + background: none +} + +.swagger-ui .model-box { + padding: 10px; + border-radius: 4px; + background: rgba(0, 0, 0, .1) +} + +.swagger-ui .model-box .model-jump-to-path { + position: relative; + top: 4px +} + +.swagger-ui .model-title { + font-size: 16px; + font-family: Titillium Web, sans-serif; + color: #555 +} + +.swagger-ui span>span.model, .swagger-ui span>span.model .brace-close { + padding: 0 0 0 10px +} + +.swagger-ui .prop-type { + color: #55a +} + +.swagger-ui .prop-enum { + display: block +} + +.swagger-ui .prop-format { + color: #999 +} + +.swagger-ui table { + width: 100%; + padding: 0 10px; + border-collapse: collapse +} + +.swagger-ui table.model tbody tr td { + padding: 0; + vertical-align: top +} + +.swagger-ui table.model tbody tr td:first-of-type { + width: 100px; + padding: 0 +} + +.swagger-ui table.headers td { + font-size: 12px; + font-weight: 300; + vertical-align: middle; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #3b4151 +} + +.swagger-ui table tbody tr td { + padding: 10px 0 0; + vertical-align: top +} + +.swagger-ui table tbody tr td:first-of-type { + width: 20%; + padding: 10px 0 +} + +.swagger-ui table thead tr td, .swagger-ui table thead tr th { + font-size: 12px; + font-weight: 700; + padding: 12px 0; + text-align: left; + border-bottom: 1px solid rgba(59, 65, 81, .2); + font-family: Open Sans, sans-serif; + color: #3b4151 +} + +.swagger-ui .parameters-col_description p { + font-size: 14px; + margin: 0; + font-family: Open Sans, sans-serif; + color: #3b4151 +} + +.swagger-ui .parameters-col_description input[type=text] { + width: 100%; + max-width: 340px +} + +.swagger-ui .parameter__name { + font-size: 16px; + font-weight: 400; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui .parameter__name.required { + font-weight: 700 +} + +.swagger-ui .parameter__name.required:after { + font-size: 10px; + position: relative; + top: -6px; + padding: 5px; + content: "required"; + color: rgba(255, 0, 0, .6) +} + +.swagger-ui .parameter__in { + font-size: 12px; + font-style: italic; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #888 +} + +.swagger-ui .table-container { + padding: 20px +} + +.swagger-ui .topbar { + padding: 8px 30px; + background-color: #2430F0 +} + +.swagger-ui .topbar .topbar-wrapper { + -ms-flex-align: center +} + +.swagger-ui .topbar .topbar-wrapper, .swagger-ui .topbar a { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + align-items: center +} + +.swagger-ui .topbar a { + font-size: 1.5em; + font-weight: 700; + text-decoration: none; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + -ms-flex-align: center; + font-family: Titillium Web, sans-serif; + color: #fff +} + +.swagger-ui .topbar a span { + margin: 0; + padding: 0 10px +} + +.swagger-ui .topbar .download-url-wrapper { + display: -webkit-box; + display: -ms-flexbox; + display: flex +} + +.swagger-ui .topbar .download-url-wrapper input[type=text] { + min-width: 350px; + margin: 0; + border: 2px solid #547f00; + border-radius: 4px 0 0 4px; + outline: none +} + +.swagger-ui .topbar .download-url-wrapper .download-url-button { + font-size: 16px; + font-weight: 700; + padding: 4px 40px; + border: none; + border-radius: 0 4px 4px 0; + background: #547f00; + font-family: Titillium Web, sans-serif; + color: #fff +} + +.swagger-ui .info { + margin: 50px 0 +} + +.swagger-ui .info hgroup.main { + margin: 0 0 20px +} + +.swagger-ui .info hgroup.main a { + font-size: 12px +} + +.swagger-ui .info p { + font-size: 14px; + font-family: Open Sans, sans-serif; + color: #3b4151 +} + +.swagger-ui .info code { + padding: 3px 5px; + border-radius: 4px; + background: rgba(0, 0, 0, .05); + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #9012fe +} + +.swagger-ui .info a { + font-size: 14px; + -webkit-transition: all .4s; + transition: all .4s; + font-family: Open Sans, sans-serif; + color: #2430F0 +} + +.swagger-ui .info a:hover { + color: #1f69c0 +} + +.swagger-ui .info>div { + margin: 0 0 5px +} + +.swagger-ui .info .base-url { + font-size: 12px; + font-weight: 300!important; + margin: 0; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #3b4151 +} + +.swagger-ui .info .title { + font-size: 36px; + margin: 0; + font-family: Open Sans, sans-serif; + color: #3b4151 +} + +.swagger-ui .info .title small { + font-size: 10px; + position: relative; + top: -5px; + display: inline-block; + margin: 0 0 0 5px; + padding: 2px 4px; + vertical-align: super; + border-radius: 57px; + background: #7d8492 +} + +.swagger-ui .info .title small pre { + margin: 0; + font-family: Titillium Web, sans-serif; + color: #fff +} + +.swagger-ui .auth-btn-wrapper { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + padding: 10px 0; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center +} + +.swagger-ui .auth-wrapper { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-box-pack: end; + -ms-flex-pack: end; + justify-content: flex-end +} + +.swagger-ui .auth-wrapper .authorize { + padding-right: 20px +} + +.swagger-ui .auth-container { + margin: 0 0 10px; + padding: 10px 20px; + border-bottom: 1px solid #ebebeb +} + +.swagger-ui .auth-container:last-of-type { + margin: 0; + padding: 10px 20px; + border: 0 +} + +.swagger-ui .auth-container h4 { + margin: 5px 0 15px!important +} + +.swagger-ui .auth-container .wrapper { + margin: 0; + padding: 0 +} + +.swagger-ui .auth-container input[type=password], .swagger-ui .auth-container input[type=text] { + min-width: 230px +} + +.swagger-ui .auth-container .errors { + font-size: 12px; + padding: 10px; + border-radius: 4px; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #3b4151 +} + +.swagger-ui .scopes h2 { + font-size: 14px; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +.swagger-ui .scope-def { + padding: 0 0 20px +} + +.swagger-ui .errors-wrapper { + margin: 20px; + padding: 10px 20px; + -webkit-animation: scaleUp .5s; + animation: scaleUp .5s; + border: 2px solid #f46470; + border-radius: 4px; + background: rgba(249, 62, 62, .1) +} + +.swagger-ui .errors-wrapper .error-wrapper { + margin: 0 0 10px +} + +.swagger-ui .errors-wrapper .errors h4 { + font-size: 14px; + margin: 0; + font-family: Source Code Pro, monospace; + font-weight: 600; + color: #3b4151 +} + +.swagger-ui .errors-wrapper hgroup { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center +} + +.swagger-ui .errors-wrapper hgroup h4 { + font-size: 20px; + margin: 0; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + font-family: Titillium Web, sans-serif; + color: #3b4151 +} + +@-webkit-keyframes scaleUp { + 0% { + -webkit-transform: scale(.8); + transform: scale(.8); + opacity: 0 + } + to { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 1 + } +} + +@keyframes scaleUp { + 0% { + -webkit-transform: scale(.8); + transform: scale(.8); + opacity: 0 + } + to { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 1 + } +} + +.swagger-ui .Resizer.vertical.disabled { + display: none +} + +/*# sourceMappingURL=swagger-ui.css.map*/ + +/** + * Swagger UI Theme Overrides + * + * Theme: Newspaper + * Author: Mark Ostrander + * Github: https://github.com/ostranme/swagger-ui-themes + */ + + .swagger-ui .opblock.opblock-post { + border-color: #F2F2F6; + background: #F5F7F9; + } + + .swagger-ui .opblock.opblock-post .opblock-summary-method { + background: #535356; + } + + .swagger-ui .opblock.opblock-post .opblock-summary { + border-color: #F2F2F6; + } + + .swagger-ui .opblock.opblock-put { + border-color: #F2F2F6; + background: #F5F7F9; + } + + .swagger-ui .opblock.opblock-put .opblock-summary-method { + background: #535356; + } + + .swagger-ui .opblock.opblock-put .opblock-summary { + border-color: #F2F2F6; + } + + .swagger-ui .opblock.opblock-delete { + border-color: #F2F2F6; + background: #F5F7F9; + } + + .swagger-ui .opblock.opblock-delete .opblock-summary-method { + background: #535356; + } + + .swagger-ui .opblock.opblock-delete .opblock-summary { + border-color: #F2F2F6; + } + + .swagger-ui .opblock.opblock-get { + border-color: #F2F2F6; + background: #F5F7F9; + } + + .swagger-ui .opblock.opblock-get .opblock-summary-method { + background: #535356; + } + + .swagger-ui .opblock.opblock-get .opblock-summary { + border-color: #F2F2F6; + } + + .swagger-ui .opblock.opblock-patch { + border-color: #F2F2F6; + background: #F5F7F9; + } + + .swagger-ui .opblock.opblock-patch .opblock-summary-method { + background: #535356; + } + + .swagger-ui .opblock.opblock-patch .opblock-summary { + border-color: #F2F2F6; + } + + .swagger-ui .opblock.opblock-head { + border-color: #F2F2F6; + background: #F5F7F9; + } + + .swagger-ui .opblock.opblock-head .opblock-summary-method { + background: #535356; + } + + .swagger-ui .opblock.opblock-head .opblock-summary { + border-color: #F2F2F6; + } + + .swagger-ui .opblock.opblock-options { + border-color: #F2F2F6; + background: #F5F7F9; + } + + .swagger-ui .opblock.opblock-options .opblock-summary-method { + background: #535356; + } + + .swagger-ui .opblock.opblock-options .opblock-summary { + border-color: #F2F2F6; + } + + .swagger-ui .topbar { + padding: 8px 30px; + background-color: #535356; + } + .swagger-ui .topbar .download-url-wrapper input[type=text] { + min-width: 350px; + margin: 0; + border: 2px solid #F2F2F6; + border-radius: 4px 0 0 4px; + outline: none; + } + + .swagger-ui .topbar .download-url-wrapper .download-url-button { + font-size: 16px; + font-weight: 700; + padding: 4px 40px; + border: none; + border-radius: 0 4px 4px 0; + background: #F2F2F6; + font-family: Titillium Web, sans-serif; + color: #535356; + } + + .swagger-ui .info a { + font-size: 14px; + -webkit-transition: all .4s; + transition: all .4s; + font-family: Open Sans, sans-serif; + color: #535356; + } + + .swagger-ui .info a:hover { + color: #535356; + } + + .swagger-ui .btn.authorize { + line-height: 1; + display: inline; + color: #535356; + border-color: #535356; + } + .swagger-ui .btn.authorize svg { + fill: #535356; + } +` + }) + } + await app.listen(configService.get('PORT') || DEFAULT_PORT) } bootstrap() diff --git a/packages/core/manifest/src/manifest/controllers/manifest.controller.ts b/packages/core/manifest/src/manifest/controllers/manifest.controller.ts index 17154a90..7fc17262 100644 --- a/packages/core/manifest/src/manifest/controllers/manifest.controller.ts +++ b/packages/core/manifest/src/manifest/controllers/manifest.controller.ts @@ -3,7 +3,9 @@ import { Controller, Get, Param, Req } from '@nestjs/common' import { Request } from 'express' import { AuthService } from '../../auth/auth.service' import { ManifestService } from '../services/manifest/manifest.service' +import { ApiTags } from '@nestjs/swagger' +@ApiTags('Manifest') @Controller('manifest') export class ManifestController { constructor( diff --git a/packages/core/manifest/src/manifest/json-schema/definitions/property-schema.json b/packages/core/manifest/src/manifest/json-schema/definitions/property-schema.json index 15574c1b..468f793e 100644 --- a/packages/core/manifest/src/manifest/json-schema/definitions/property-schema.json +++ b/packages/core/manifest/src/manifest/json-schema/definitions/property-schema.json @@ -21,6 +21,7 @@ "link", "money", "date", + "timestamp", "email", "boolean", "relation", diff --git a/packages/core/manifest/src/manifest/json-schema/schema.json b/packages/core/manifest/src/manifest/json-schema/schema.json index a023d36c..deb11686 100644 --- a/packages/core/manifest/src/manifest/json-schema/schema.json +++ b/packages/core/manifest/src/manifest/json-schema/schema.json @@ -1,14 +1,18 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://schema.manifest.build/schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", "title": "App Manifest Schema", - "description": "A complete backend in a single file.", + "description": "A complete backend in a single file", "type": "object", "properties": { "name": { "description": "The name of your app", "type": "string" }, + "version": { + "description": "The version of your app", + "type": "string" + }, "entities": { "description": "The entities in your app. Doc: https://manifest.build/docs/entities", "type": "object", diff --git a/packages/core/manifest/src/manifest/services/manifest/manifest.service.ts b/packages/core/manifest/src/manifest/services/manifest/manifest.service.ts index 5db58170..d6db0406 100644 --- a/packages/core/manifest/src/manifest/services/manifest/manifest.service.ts +++ b/packages/core/manifest/src/manifest/services/manifest/manifest.service.ts @@ -126,12 +126,16 @@ export class ManifestService { } /** - * Transform an AppManifestSchema into an AppManifest. + * Transform an AppManifestSchema into an AppManifest ensuring that undefined properties are filled in with defaults. * * @param manifestSchema the manifest schema to transform. * @returns the manifest with defaults filled in and short form properties transformed into long form. */ transformAppManifest(manifestSchema: AppManifestSchema): AppManifest { + if (!manifestSchema.version) { + manifestSchema.version = '0.0.1' + } + if (!manifestSchema.entities) { manifestSchema.entities = {} } diff --git a/packages/core/manifest/src/manifest/services/yaml/yaml.service.spec.ts b/packages/core/manifest/src/manifest/services/yaml/yaml.service.spec.ts index 2756b1d1..81411813 100644 --- a/packages/core/manifest/src/manifest/services/yaml/yaml.service.spec.ts +++ b/packages/core/manifest/src/manifest/services/yaml/yaml.service.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing' import { YamlService } from './yaml.service' import * as fs from 'fs' +import { ConfigService } from '@nestjs/config' jest.mock('fs') @@ -15,7 +16,17 @@ describe('YamlService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [YamlService] + providers: [ + YamlService, + { + provide: ConfigService, + useValue: { + get: jest.fn(() => ({ + database: 'mocked database path' + })) + } + } + ] }).compile() service = module.get(YamlService) diff --git a/packages/core/manifest/src/manifest/services/yaml/yaml.service.ts b/packages/core/manifest/src/manifest/services/yaml/yaml.service.ts index 97cce022..a72f25ae 100644 --- a/packages/core/manifest/src/manifest/services/yaml/yaml.service.ts +++ b/packages/core/manifest/src/manifest/services/yaml/yaml.service.ts @@ -4,10 +4,12 @@ import * as fs from 'fs' import * as yaml from 'js-yaml' import { AppManifestSchema } from '@mnfst/types' -import { MANIFEST_FILE_NAME, MANIFEST_FOLDER_NAME } from '../../../constants' +import { ConfigService } from '@nestjs/config' @Injectable() export class YamlService { + constructor(private readonly configService: ConfigService) {} + /** * * Load the manifest from the YML file and transform it into a AppManifest object. @@ -17,7 +19,7 @@ export class YamlService { **/ load(): AppManifestSchema { let fileContent: string = fs.readFileSync( - `${process.cwd()}/${MANIFEST_FOLDER_NAME}/${MANIFEST_FILE_NAME}`, + this.configService.get('paths').database, 'utf8' ) diff --git a/packages/core/manifest/src/open-api/open-api.module.ts b/packages/core/manifest/src/open-api/open-api.module.ts new file mode 100644 index 00000000..722cdf67 --- /dev/null +++ b/packages/core/manifest/src/open-api/open-api.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common' +import { OpenApiService } from './services/open-api.service' +import { OpenApiCrudService } from './services/open-api-crud.service' +import { ManifestModule } from '../manifest/manifest.module' +import { OpenApiManifestService } from './services/open-api-manifest.service' + +@Module({ + imports: [ManifestModule], + providers: [OpenApiService, OpenApiCrudService, OpenApiManifestService] +}) +export class OpenApiModule {} diff --git a/packages/core/manifest/src/open-api/services/open-api-crud.service.ts b/packages/core/manifest/src/open-api/services/open-api-crud.service.ts new file mode 100644 index 00000000..558386da --- /dev/null +++ b/packages/core/manifest/src/open-api/services/open-api-crud.service.ts @@ -0,0 +1,321 @@ +import { EntityManifest } from '@mnfst/types' +import { Injectable } from '@nestjs/common' +import { PathItemObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface' + +@Injectable() +export class OpenApiCrudService { + /** + * Generates the paths for the entities. For each entity, it generates the paths for listing, creating, updating and deleting. + * + * @param entityManifests The entity manifests. + * @returns The paths object. + * + */ + generateEntityPaths( + entityManifests: EntityManifest[] + ): Record { + const paths: Record = {} + + entityManifests.forEach((entityManifest: EntityManifest) => { + paths[`/api/dynamic/${entityManifest.slug}`] = { + ...this.generateListPath(entityManifest), + ...this.generateCreatePath(entityManifest) + } + paths[`/api/dynamic/${entityManifest.slug}/select-options`] = + this.generateListSelectOptionsPath(entityManifest) + paths[`/api/dynamic/${entityManifest.slug}/{id}`] = { + ...this.generateDetailPath(entityManifest), + ...this.generateUpdatePath(entityManifest), + ...this.generateDeletePath(entityManifest) + } + }) + + return paths + } + + /** + * Generates the path for listing entities. + * + * @param entityManifest The entity manifest. + * @returns The path item object. + * + */ + generateListPath(entityManifest: EntityManifest): PathItemObject { + return { + get: { + summary: `List ${entityManifest.namePlural}`, + description: `Retrieves a paginated list of ${entityManifest.namePlural}. In addition to the general parameters below, each property of the ${entityManifest.nameSingular} can be used as a filter: https://manifest.build/docs/rest-api#filters`, + tags: [this.capitalizeFirstLetter(entityManifest.namePlural)], + parameters: [ + { + name: 'page', + in: 'query', + description: 'The page number', + required: false, + schema: { + type: 'integer', + default: 1 + } + }, + { + name: 'perPage', + in: 'query', + description: 'The number of items per page', + required: false, + schema: { + type: 'integer', + default: 10 + } + }, + { + name: 'orderBy', + in: 'query', + description: 'The field to order by', + required: false, + schema: { + type: 'string' + } + }, + { + name: 'order', + in: 'query', + description: 'The order direction', + required: false, + schema: { + type: 'string', + enum: ['ASC', 'DESC'] + } + }, + { + name: 'relations', + in: 'query', + description: + 'The relations to include. For several relations, use a comma-separated list', + required: false, + schema: { + type: 'string' + } + } + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Paginator' + } + } + } + } + } + } + } + } + + /** + * Generates the path for listing entities for select options. + * This is used to fill select dropdown options. + * + * @param entityManifest The entity manifest. + * @returns The path item object. + * + */ + generateListSelectOptionsPath( + entityManifest: EntityManifest + ): PathItemObject { + return { + get: { + summary: `List ${entityManifest.namePlural} for select options`, + description: `Retrieves a list of ${entityManifest.namePlural} for select options. The response is an array of objects with the properties 'id' and 'label'.`, + tags: [this.capitalizeFirstLetter(entityManifest.namePlural)], + responses: { + '200': { + description: `List of ${entityManifest.namePlural} for select options`, + content: { + 'application/json': { + schema: { + type: 'array', + items: { + $ref: `#/components/schemas/SelectOption` + } + } + } + } + } + } + } + } + } + + /** + * Generates the path for creating an entity. + * This is used to create a new entity. + * + * @param entityManifest The entity manifest. + * @returns The path item object. + * + */ + generateCreatePath(entityManifest: EntityManifest): PathItemObject { + return { + post: { + summary: `Create a new ${entityManifest.nameSingular}`, + description: `Creates a new ${entityManifest.nameSingular} passing the properties in the request body as JSON.`, + tags: [this.capitalizeFirstLetter(entityManifest.namePlural)], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object' + } + } + } + }, + responses: { + '201': { + description: `OK` + }, + '400': { + description: `Bad request` + } + } + } + } + } + + /** + * Generates the path for retrieving the details of an entity. + * + * @param entityManifest The entity manifest. + * @returns The path item object. + * + */ + generateDetailPath(entityManifest: EntityManifest): PathItemObject { + return { + get: { + summary: `Get a single ${entityManifest.nameSingular}`, + description: `Retrieves the details of a single ${entityManifest.nameSingular} by its ID.`, + tags: [this.capitalizeFirstLetter(entityManifest.namePlural)], + parameters: [ + { + name: 'id', + in: 'path', + description: `The ID of the ${entityManifest.nameSingular}`, + required: true, + schema: { + type: 'integer' + } + } + ], + responses: { + '200': { + description: `OK`, + content: { + 'application/json': { + schema: { + type: 'object' + } + } + } + }, + '404': { + description: `The ${entityManifest.nameSingular} was not found` + } + } + } + } + } + + /** + * Generates the path for updating an entity. + * + * @param entityManifest The entity manifest. + * @returns The path item object. + * + */ + generateUpdatePath(entityManifest: EntityManifest): PathItemObject { + return { + put: { + summary: `Update an existing ${entityManifest.nameSingular}`, + description: `Updates a single ${entityManifest.nameSingular} by its ID. The properties to update are passed in the request body as JSON.`, + tags: [this.capitalizeFirstLetter(entityManifest.namePlural)], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object' + } + } + } + }, + parameters: [ + { + name: 'id', + in: 'path', + description: `The ID of the ${entityManifest.nameSingular}`, + required: true, + schema: { + type: 'integer' + } + } + ], + responses: { + '200': { + description: `OK`, + content: { + 'application/json': { + schema: { + type: 'object' + } + } + } + }, + '404': { + description: `Not found` + } + } + } + } + } + + /** + * Generates the path for deleting an entity. + * + * @param entityManifest The entity manifest. + * @returns The path item object. + * + */ + generateDeletePath(entityManifest: EntityManifest): PathItemObject { + return { + delete: { + summary: `Delete an existing ${entityManifest.nameSingular}`, + description: `Deletes a single ${entityManifest.nameSingular} by its ID.`, + tags: [this.capitalizeFirstLetter(entityManifest.namePlural)], + parameters: [ + { + name: 'id', + in: 'path', + description: `The ID of the ${entityManifest.nameSingular}`, + required: true, + schema: { + type: 'integer' + } + } + ], + responses: { + '200': { + description: `OK` + }, + '404': { + description: `The ${entityManifest.nameSingular} was not found` + } + } + } + } + } + + private capitalizeFirstLetter(str: string) { + if (str.length === 0) return str // Handle empty string case + return str.charAt(0).toUpperCase() + str.slice(1) + } +} diff --git a/packages/core/manifest/src/open-api/services/open-api-manifest.service.ts b/packages/core/manifest/src/open-api/services/open-api-manifest.service.ts new file mode 100644 index 00000000..0c201271 --- /dev/null +++ b/packages/core/manifest/src/open-api/services/open-api-manifest.service.ts @@ -0,0 +1,77 @@ +import { AppManifest, EntityManifest } from '@mnfst/types' +import { Injectable } from '@nestjs/common' +import { PathItemObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface' + +@Injectable() +export class OpenApiManifestService { + /** + * Generates the paths for the manifest endpoints. + * + * @param appManifest The manifest of the application. + * @returns The paths for the manifest endpoints. + * + */ + generateManifestPaths( + appManifest: AppManifest + ): Record { + const paths: Record = { + '/api/manifest': { + get: { + summary: 'Get the manifest', + description: 'Retrieves the manifest of the application.', + tags: ['Manifest'], + responses: { + '200': { + description: 'The manifest of the application.', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/AppManifest' + } + } + } + } + } + } + } + } + + Object.values(appManifest.entities).forEach( + (entityManifest: EntityManifest) => { + paths[`/api/manifest/entities/${entityManifest.slug}`] = + this.generateEntityManifestPath(entityManifest) + } + ) + + return paths + } + + /** + * Generates the path for the entity manifest endpoint. + * + * @param entityManifest The manifest of the entity. + * @returns The path for the entity manifest endpoint. + * + */ + generateEntityManifestPath(entityManifest: EntityManifest): PathItemObject { + return { + get: { + summary: `Get the ${entityManifest.nameSingular} manifest`, + description: `Retrieves the manifest of the ${entityManifest.nameSingular} entity with all its properties.`, + tags: ['Manifest'], + responses: { + '200': { + description: `The manifest of the ${entityManifest.nameSingular} entity.`, + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/EntityManifest' + } + } + } + } + } + } + } + } +} diff --git a/packages/core/manifest/src/open-api/services/open-api.service.ts b/packages/core/manifest/src/open-api/services/open-api.service.ts new file mode 100644 index 00000000..5903fc97 --- /dev/null +++ b/packages/core/manifest/src/open-api/services/open-api.service.ts @@ -0,0 +1,157 @@ +import { Injectable } from '@nestjs/common' +import { OpenApiCrudService } from './open-api-crud.service' +import { OpenAPIObject } from '@nestjs/swagger' +import { ManifestService } from '../../manifest/services/manifest/manifest.service' +import { AppManifest } from '@mnfst/types' +import { OpenApiManifestService } from './open-api-manifest.service' + +@Injectable() +export class OpenApiService { + constructor( + private readonly manifestService: ManifestService, + private readonly openApiCrudService: OpenApiCrudService, + private readonly openApiManifestService: OpenApiManifestService + ) {} + + /** + * Generates the OpenAPI object for the application. + * + * @returns The OpenAPI object. + * + */ + generateOpenApiObject(): OpenAPIObject { + const appManifest: AppManifest = this.manifestService.getAppManifest() + + return { + openapi: '3.1.0', + info: { + title: appManifest.name, + version: '' // Version is not supported yet. + }, + paths: { + ...this.openApiCrudService.generateEntityPaths( + Object.values(appManifest.entities) + ), + ...this.openApiManifestService.generateManifestPaths(appManifest) + }, + components: { + schemas: { + Paginator: { + type: 'object', + properties: { + data: { + type: 'array', + items: { + type: 'object' + } + }, + currentPage: { + type: 'integer' + }, + lastPage: { + type: 'integer' + }, + from: { + type: 'integer' + }, + to: { + type: 'integer' + }, + total: { + type: 'integer' + }, + perPage: { + type: 'integer' + } + } + }, + SelectOption: { + type: 'object', + properties: { + id: { + type: 'number' + }, + label: { + type: 'string' + } + } + }, + AppManifest: { + type: 'object', + properties: { + name: { + type: 'string' + }, + entities: { + type: 'object', + additionalProperties: { + $ref: '#/components/schemas/EntityManifest' + } + } + } + }, + EntityManifest: { + type: 'object', + properties: { + className: { + type: 'string' + }, + nameSingular: { + type: 'string' + }, + namePlural: { + type: 'string' + }, + slug: { + type: 'string' + }, + mainProp: { + type: 'string' + }, + seedCount: { + type: 'number' + }, + belongsTo: { + type: 'array', + items: { + $ref: '#/components/schemas/RelationshipManifest' + } + }, + properties: { + type: 'array', + items: { + $ref: '#/components/schemas/PropertyManifest' + } + } + } + }, + RelationshipManifest: { + type: 'object', + properties: { + name: { + type: 'string' + }, + entity: { + type: 'string' + }, + eager: { + type: 'boolean' + } + } + }, + PropertyManifest: { + type: 'object', + properties: { + name: { + type: 'string' + }, + type: { + type: 'string' + } + } + } + } + } + } + } +} diff --git a/packages/core/manifest/src/open-api/tests/open-api-crud.service.spec.ts b/packages/core/manifest/src/open-api/tests/open-api-crud.service.spec.ts new file mode 100644 index 00000000..0d2e6714 --- /dev/null +++ b/packages/core/manifest/src/open-api/tests/open-api-crud.service.spec.ts @@ -0,0 +1,56 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { OpenApiCrudService } from '../services/open-api-crud.service' +import { EntityManifest, PropType } from '@mnfst/types' + +describe('OpenApiCrudService', () => { + let service: OpenApiCrudService + + const dummyEntityManifest: EntityManifest = { + className: 'Cat', + nameSingular: 'cat', + namePlural: 'cats', + slug: 'cats', + mainProp: 'name', + seedCount: 50, + belongsTo: [], + properties: [ + { + name: 'name', + type: PropType.String + } + ] + } + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [OpenApiCrudService] + }).compile() + + service = module.get(OpenApiCrudService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) + + it('should generate all 6 entity paths', () => { + jest.spyOn(service, 'generateListPath').mockReturnValue({}) + jest.spyOn(service, 'generateListSelectOptionsPath').mockReturnValue({}) + jest.spyOn(service, 'generateCreatePath').mockReturnValue({}) + jest.spyOn(service, 'generateDetailPath').mockReturnValue({}) + jest.spyOn(service, 'generateUpdatePath').mockReturnValue({}) + jest.spyOn(service, 'generateDeletePath').mockReturnValue({}) + + const paths = service.generateEntityPaths([dummyEntityManifest]) + + expect(paths).toBeDefined() + expect(Object.keys(paths).length).toBe(3) + + expect(service.generateListPath).toHaveBeenCalled() + expect(service.generateListSelectOptionsPath).toHaveBeenCalled() + expect(service.generateCreatePath).toHaveBeenCalled() + expect(service.generateDetailPath).toHaveBeenCalled() + expect(service.generateUpdatePath).toHaveBeenCalled() + expect(service.generateDeletePath).toHaveBeenCalled() + }) +}) diff --git a/packages/core/manifest/src/open-api/tests/open-api-manifest.service.spec.ts b/packages/core/manifest/src/open-api/tests/open-api-manifest.service.spec.ts new file mode 100644 index 00000000..87f36a31 --- /dev/null +++ b/packages/core/manifest/src/open-api/tests/open-api-manifest.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { OpenApiManifestService } from '../services/open-api-manifest.service' + +describe('OpenApiManifestService', () => { + let service: OpenApiManifestService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [OpenApiManifestService] + }).compile() + + service = module.get(OpenApiManifestService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/packages/core/manifest/src/open-api/tests/open-api.service.spec.ts b/packages/core/manifest/src/open-api/tests/open-api.service.spec.ts new file mode 100644 index 00000000..f2a4417d --- /dev/null +++ b/packages/core/manifest/src/open-api/tests/open-api.service.spec.ts @@ -0,0 +1,93 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { OpenApiService } from '../services/open-api.service' +import { AppManifest, EntityManifest, PropType } from '@mnfst/types' +import { OpenAPIObject } from '@nestjs/swagger' +import { OpenApiCrudService } from '../services/open-api-crud.service' +import { ManifestService } from '../../manifest/services/manifest/manifest.service' +import { OpenApiManifestService } from '../services/open-api-manifest.service' + +describe('OpenApiService', () => { + let service: OpenApiService + let openApiCrudService: OpenApiCrudService + let openApiManifestService: OpenApiManifestService + + const dummyAppManifest: AppManifest = { + name: 'Test App', + entities: { + Invoice: { + className: 'Invoice', + properties: [ + { + name: 'name', + type: PropType.String + } + ], + belongsTo: [] + } + } + } + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + OpenApiService, + { + provide: OpenApiCrudService, + useValue: { + generateEntityPaths: jest.fn((entityManifest: EntityManifest) => {}) + } + }, + { + provide: OpenApiManifestService, + useValue: { + generateManifestPaths: jest.fn((appManifest: AppManifest) => {}) + } + }, + { + provide: ManifestService, + useValue: { + getAppManifest: jest.fn(() => dummyAppManifest) + } + } + ] + }).compile() + + service = module.get(OpenApiService) + openApiCrudService = module.get(OpenApiCrudService) + openApiManifestService = module.get( + OpenApiManifestService + ) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) + + it('should return an OpenAPIObject', () => { + const openApiObject: OpenAPIObject = service.generateOpenApiObject() + + expect(openApiObject.openapi).toBe('3.1.0') + expect(openApiObject.info.title).toBe('Test App') + expect(openApiObject.info.version).toBe('') + }) + + it('should generate paths for each entity', () => { + jest.spyOn(openApiCrudService, 'generateEntityPaths') + + service.generateOpenApiObject() + + expect(openApiCrudService.generateEntityPaths).toHaveBeenCalledTimes( + Object.keys(dummyAppManifest.entities).length + ) + }) + + it('should generate the manifest paths', () => { + jest.spyOn(openApiManifestService, 'generateManifestPaths') + + service.generateOpenApiObject() + + expect(openApiManifestService.generateManifestPaths).toHaveBeenCalledWith( + dummyAppManifest + ) + }) +}) diff --git a/packages/core/types/README.md b/packages/core/types/README.md index 5613a4a3..100a0156 100644 --- a/packages/core/types/README.md +++ b/packages/core/types/README.md @@ -2,6 +2,16 @@ Utility types used by Manifest. +## Development + +When updating this library, you may need to fetch your local copy of it instead of the published packages from other projects. + +The `packages/core/admin` and `packages/core/manifest` projects in this repository have a command to do that it their package.json file. Simply run: + +```bash +npm run link-local-types +``` + ## Build ``` diff --git a/packages/core/types/package.json b/packages/core/types/package.json index 2fe821cc..0a7dda14 100644 --- a/packages/core/types/package.json +++ b/packages/core/types/package.json @@ -1,6 +1,6 @@ { "name": "@mnfst/types", - "version": "0.1.0-alpha.3", + "version": "0.1.0-alpha.4", "description": "Utility types used by Manifest", "author": "Manifest", "homepage": "https://manifest.build", diff --git a/packages/core/types/src/crud/PropType.ts b/packages/core/types/src/crud/PropType.ts index 5fccdbae..241cc506 100644 --- a/packages/core/types/src/crud/PropType.ts +++ b/packages/core/types/src/crud/PropType.ts @@ -8,6 +8,7 @@ export enum PropType { Link = 'link', Money = 'money', Date = 'date', + Timestamp = 'timestamp', Email = 'email', Boolean = 'boolean', Password = 'password', diff --git a/packages/core/types/src/manifests/AppManifest.ts b/packages/core/types/src/manifests/AppManifest.ts index 24195227..dbe270d8 100644 --- a/packages/core/types/src/manifests/AppManifest.ts +++ b/packages/core/types/src/manifests/AppManifest.ts @@ -7,6 +7,11 @@ export interface AppManifest extends AppManifestSchema { */ name: string + /** + * The version of the app. + */ + version?: string + /** * The entities of the app. */ diff --git a/packages/core/types/src/manifests/ManifestSchema.ts b/packages/core/types/src/manifests/ManifestSchema.ts index f35536a1..93f240b6 100644 --- a/packages/core/types/src/manifests/ManifestSchema.ts +++ b/packages/core/types/src/manifests/ManifestSchema.ts @@ -24,6 +24,7 @@ export type PropertyManifestSchema = | 'link' | 'money' | 'date' + | 'timestamp' | 'email' | 'boolean' | 'relation' @@ -67,13 +68,17 @@ export type RelationshipManifestSchema = | string /** - * A complete backend in a single file. + * A complete backend in a single file */ export interface AppManifestSchema { /** * The name of your app */ name: string + /** + * The version of your app + */ + version?: string /** * The entities in your app. Doc: https://manifest.build/docs/entities */