diff --git a/.eslintignore b/.eslintignore index 3c3629e..a45755c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ node_modules +vite.config.ts diff --git a/apps/api/nest-cli.json b/apps/api/nest-cli.json index 579ca35..872c6ed 100644 --- a/apps/api/nest-cli.json +++ b/apps/api/nest-cli.json @@ -5,6 +5,14 @@ "compilerOptions": { "deleteOutDir": true, "builder": "swc", - "typeCheck": true + "typeCheck": true, + "plugins": [ + { + "name": "@nestjs/swagger", + "options": { + "introspectComments": true + } + } + ] } } diff --git a/apps/api/package.json b/apps/api/package.json index 8fcf122..b4c52fb 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -16,18 +16,24 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "db:migrate": "npx prisma migrate dev", + "db:deploy": "npx prisma migrate deploy", + "console": "npm run start -- --watch --entryFile repl" }, "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.3.1", "@prisma/client": "^5.11.0", "nest-winston": "^1.9.4", + "nestjs-zod": "^3.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "winston": "^3.13.0" + "winston": "^3.13.0", + "zod": "^3.22.4" }, "devDependencies": { "@automock/adapters.nestjs": "^2.1.0", @@ -66,7 +72,11 @@ "includedScripts": [ "build", "test", - "lint" + "lint", + "start:dev" ] + }, + "prisma": { + "schema": "src/database/schema.prisma" } } diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 06c0c5e..3da82c1 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -1,12 +1,22 @@ import { Logger, Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' +import { APP_PIPE } from '@nestjs/core' +import { ZodValidationPipe } from 'nestjs-zod' import { AppController } from './app.controller' import { AppService } from './app.service' +import { DatabaseModule } from '../database/database.module' @Module({ - imports: [ConfigModule.forRoot()], + imports: [ConfigModule.forRoot(), DatabaseModule], controllers: [AppController], - providers: [Logger, AppService] + providers: [ + Logger, + AppService, + { + provide: APP_PIPE, + useClass: ZodValidationPipe + } + ] }) export class AppModule {} diff --git a/apps/api/src/database/database.module.ts b/apps/api/src/database/database.module.ts new file mode 100644 index 0000000..1e848d5 --- /dev/null +++ b/apps/api/src/database/database.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common' +import { PrismaService } from './prisma.service' + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService] +}) +export class DatabaseModule {} diff --git a/apps/api/src/app/prisma.service.ts b/apps/api/src/database/prisma.service.ts similarity index 100% rename from apps/api/src/app/prisma.service.ts rename to apps/api/src/database/prisma.service.ts diff --git a/apps/api/prisma/schema.prisma b/apps/api/src/database/schema.prisma similarity index 100% rename from apps/api/prisma/schema.prisma rename to apps/api/src/database/schema.prisma diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 0c0ce59..f14d8bb 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -3,7 +3,11 @@ * This is only a minimal backend to get started. */ -import { Logger } from '@nestjs/common' +import { + Logger, INestApplication, + NestApplicationOptions +} from '@nestjs/common' +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger' import { NestFactory } from '@nestjs/core' import { @@ -11,40 +15,73 @@ import { WinstonModule } from 'nest-winston' import * as winston from 'winston' +import { patchNestJsSwagger } from 'nestjs-zod' + import { AppModule } from './app/app.module' -async function bootstrap () { +const API_PREFIX = 'api' +const OPENAPI_PREFIX = 'docs' + +export let viteNodeApp: Promise + +function bootstrapOpenAPI (app: INestApplication) { + patchNestJsSwagger() + const config = new DocumentBuilder() + .setTitle('Streaming Subscription API') + .setDescription('The API for managing subscriptions from streaming services') + .setVersion('0.2') + .addTag('subscriptions', 'Operations related to streaming subscriptions') + .build() + const document = SwaggerModule.createDocument(app, config) + SwaggerModule.setup(OPENAPI_PREFIX, app, document) +} + +function bootstrapLogger () { + const instance = WinstonModule.createLogger({ + transports: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.timestamp(), + winston.format.ms(), + nestWinstonModuleUtilities.format.nestLike('Streaming Subs', { + colors: true, + prettyPrint: true + }) + ) + }) + // other transports... + ] + } + ) + + return instance +} + +export async function createApp ( + options?: NestApplicationOptions +): Promise { const app = await NestFactory.create(AppModule, { - logger: WinstonModule.createLogger({ - transports: [ - new winston.transports.Console({ - format: winston.format.combine( - winston.format.timestamp(), - winston.format.ms(), - nestWinstonModuleUtilities.format.nestLike('Streaming Subs', { - colors: true, - prettyPrint: true - }) - ) - }) - // other transports... - ] - }) + logger: bootstrapLogger() }) - const globalPrefix = 'api' - app.setGlobalPrefix(globalPrefix) + app.setGlobalPrefix(API_PREFIX) + bootstrapOpenAPI(app) + return app +} + +async function main () { const port = process.env.PORT ?? 3000 + + const app = await createApp() await app.listen(port) Logger.log( - `🚀 Application is running on: http://localhost:${port}/${globalPrefix}` + `🚀 Application is running on: http://localhost:${port}/${API_PREFIX}` ) } -// eslint-disable-next-line @typescript-eslint/no-floating-promises if (process.env.NODE_ENV === 'production') { - void bootstrap() + void main() +} else { + viteNodeApp = createApp() } -console.log('process.env.NODE_ENV', process.env.NODE_ENV) -export const viteNodeApp = NestFactory.create(AppModule) diff --git a/apps/api/src/metadata.ts b/apps/api/src/metadata.ts new file mode 100644 index 0000000..67619ec --- /dev/null +++ b/apps/api/src/metadata.ts @@ -0,0 +1,5 @@ +/* eslint-disable */ +export default async () => { + const t = {}; + return { "@nestjs/swagger": { "models": [], "controllers": [] } }; +}; \ No newline at end of file diff --git a/apps/api/src/repl.ts b/apps/api/src/repl.ts new file mode 100644 index 0000000..f311088 --- /dev/null +++ b/apps/api/src/repl.ts @@ -0,0 +1,12 @@ +import { repl } from '@nestjs/core' +import { AppModule } from './app/app.module' + +async function bootstrap () { + const replServer = await repl(AppModule) + replServer.setupHistory('.nestjs_repl_history', (err) => { + if (err != null) { + console.error(err) + } + }) +} +void bootstrap() diff --git a/apps/api/vite.config.ts b/apps/api/vite.config.ts index 168a5e9..d9398f0 100644 --- a/apps/api/vite.config.ts +++ b/apps/api/vite.config.ts @@ -4,6 +4,7 @@ import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig(({ command, mode }: ConfigEnv) => { return { + base: '/api', server: { // vite server configs, for details see \[vite doc\](https://vitejs.dev/config/#server-host) port: 3000 diff --git a/package-lock.json b/package-lock.json index 606f690..e7a007b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,11 +31,14 @@ "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.3.1", "@prisma/client": "^5.11.0", "nest-winston": "^1.9.4", + "nestjs-zod": "^3.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "winston": "^3.13.0" + "winston": "^3.13.0", + "zod": "^3.22.4" }, "devDependencies": { "@automock/adapters.nestjs": "^2.1.0", @@ -5388,8 +5391,7 @@ "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==", - "dev": true + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" }, "node_modules/@microsoft/tsdoc-config": { "version": "0.16.2", @@ -5631,6 +5633,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.7", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.7.tgz", @@ -5673,6 +5694,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.7", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.7.tgz", @@ -9898,6 +9951,14 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -15098,6 +15159,25 @@ "is-callable": "^1.1.3" } }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -16394,6 +16474,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -17745,6 +17833,14 @@ "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -18529,6 +18625,78 @@ "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz", "integrity": "sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==" }, + "node_modules/merge-deep": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep/node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -19727,6 +19895,26 @@ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -19912,6 +20100,34 @@ "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==" }, + "node_modules/nestjs-zod": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/nestjs-zod/-/nestjs-zod-3.0.0.tgz", + "integrity": "sha512-vL9CHShCVj6TmjCVPOd4my46D8d7FdoB4nQvvh+lmVTuzvnwuD+slSxjT4EDdPDWDFtjhfpvQnnkr55/80KHEQ==", + "dependencies": { + "merge-deep": "^3.0.3" + }, + "peerDependencies": { + "@nestjs/common": ">= 8.0.0", + "@nestjs/core": ">= 8.0.0", + "@nestjs/swagger": ">= 5.0.0", + "zod": ">= 3.14.3" + }, + "peerDependenciesMeta": { + "@nestjs/common": { + "optional": true + }, + "@nestjs/core": { + "optional": true + }, + "@nestjs/swagger": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", @@ -23245,6 +23461,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",