feat: add movie and category services
This commit is contained in:
commit
827c1a431b
56
.gitignore
vendored
Normal file
56
.gitignore
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
98
README.md
Normal file
98
README.md
Normal file
@ -0,0 +1,98 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Project setup
|
||||
|
||||
```bash
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
## Compile and run the project
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ yarn run start
|
||||
|
||||
# watch mode
|
||||
$ yarn run start:dev
|
||||
|
||||
# production mode
|
||||
$ yarn run start:prod
|
||||
```
|
||||
|
||||
## Run tests
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ yarn run test
|
||||
|
||||
# e2e tests
|
||||
$ yarn run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ yarn run test:cov
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||
|
||||
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||
|
||||
```bash
|
||||
$ yarn install -g @nestjs/mau
|
||||
$ mau deploy
|
||||
```
|
||||
|
||||
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||
|
||||
## Resources
|
||||
|
||||
Check out a few resources that may come in handy when working with NestJS:
|
||||
|
||||
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
23
drizzle.config.ts
Normal file
23
drizzle.config.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export default defineConfig({
|
||||
out: process.env.DRIZZLE_OUT,
|
||||
schema: './src/infra/database/drizzle/schema',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
host: process.env.DATABASE_HOST,
|
||||
port: Number(process.env.DATABASE_PORT),
|
||||
user: process.env.DATABASE_USERNAME,
|
||||
password: process.env.DATABASE_PASSWORD,
|
||||
database: process.env.DATABASE_NAME,
|
||||
ssl:
|
||||
process.env.DATABASE_SSL === 'true'
|
||||
? { rejectUnauthorized: false }
|
||||
: false
|
||||
},
|
||||
verbose: true,
|
||||
strict: true
|
||||
})
|
||||
27
drizzle/0000_mixed_the_watchers.sql
Normal file
27
drizzle/0000_mixed_the_watchers.sql
Normal file
@ -0,0 +1,27 @@
|
||||
CREATE TABLE "categories" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
"title" varchar(255) NOT NULL,
|
||||
"slug" varchar(255),
|
||||
CONSTRAINT "categories_slug_unique" UNIQUE("slug")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "movies" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
"title" varchar(255) NOT NULL,
|
||||
"slug" varchar(255),
|
||||
"description" text NOT NULL,
|
||||
"poster" text,
|
||||
"duration" integer DEFAULT 0 NOT NULL,
|
||||
"release_year" integer,
|
||||
"release_date" timestamp,
|
||||
"rating_age" integer DEFAULT 0,
|
||||
"country" varchar(255),
|
||||
"category_id" uuid,
|
||||
CONSTRAINT "movies_slug_unique" UNIQUE("slug")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "movies" ADD CONSTRAINT "movies_category_id_categories_id_fk" FOREIGN KEY ("category_id") REFERENCES "public"."categories"("id") ON DELETE cascade ON UPDATE no action;
|
||||
2
drizzle/0001_flippant_scrambler.sql
Normal file
2
drizzle/0001_flippant_scrambler.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE "movies" ALTER COLUMN "poster" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "movies" ADD COLUMN "banner" text;
|
||||
191
drizzle/meta/0000_snapshot.json
Normal file
191
drizzle/meta/0000_snapshot.json
Normal file
@ -0,0 +1,191 @@
|
||||
{
|
||||
"id": "18088c29-e51e-4bf6-96d6-72273adab2e4",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.categories": {
|
||||
"name": "categories",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"categories_slug_unique": {
|
||||
"name": "categories_slug_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"slug"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.movies": {
|
||||
"name": "movies",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"poster": {
|
||||
"name": "poster",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"release_year": {
|
||||
"name": "release_year",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"release_date": {
|
||||
"name": "release_date",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"rating_age": {
|
||||
"name": "rating_age",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 0
|
||||
},
|
||||
"country": {
|
||||
"name": "country",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"category_id": {
|
||||
"name": "category_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"movies_category_id_categories_id_fk": {
|
||||
"name": "movies_category_id_categories_id_fk",
|
||||
"tableFrom": "movies",
|
||||
"tableTo": "categories",
|
||||
"columnsFrom": [
|
||||
"category_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"movies_slug_unique": {
|
||||
"name": "movies_slug_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"slug"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
197
drizzle/meta/0001_snapshot.json
Normal file
197
drizzle/meta/0001_snapshot.json
Normal file
@ -0,0 +1,197 @@
|
||||
{
|
||||
"id": "46b9aaed-8501-4aa0-837c-312bf24a66b1",
|
||||
"prevId": "18088c29-e51e-4bf6-96d6-72273adab2e4",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.categories": {
|
||||
"name": "categories",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"categories_slug_unique": {
|
||||
"name": "categories_slug_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"slug"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.movies": {
|
||||
"name": "movies",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"poster": {
|
||||
"name": "poster",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"banner": {
|
||||
"name": "banner",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"duration": {
|
||||
"name": "duration",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"release_year": {
|
||||
"name": "release_year",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"release_date": {
|
||||
"name": "release_date",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"rating_age": {
|
||||
"name": "rating_age",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": 0
|
||||
},
|
||||
"country": {
|
||||
"name": "country",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"category_id": {
|
||||
"name": "category_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"movies_category_id_categories_id_fk": {
|
||||
"name": "movies_category_id_categories_id_fk",
|
||||
"tableFrom": "movies",
|
||||
"tableTo": "categories",
|
||||
"columnsFrom": [
|
||||
"category_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"movies_slug_unique": {
|
||||
"name": "movies_slug_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"slug"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
20
drizzle/meta/_journal.json
Normal file
20
drizzle/meta/_journal.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1775616500784,
|
||||
"tag": "0000_mixed_the_watchers",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1775620058610,
|
||||
"tag": "0001_flippant_scrambler",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
35
eslint.config.mjs
Normal file
35
eslint.config.mjs
Normal file
@ -0,0 +1,35 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
sourceType: 'commonjs',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||
},
|
||||
},
|
||||
);
|
||||
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
89
package.json
Normal file
89
package.json
Normal file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "movie-service",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"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",
|
||||
"db:seed": "node -r tsconfig-paths/register -r ts-node/register ./src/infra/database/seed.ts",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "ts-node ./src/infra/database/drizzle/migration.runner.ts",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:validate": "drizzle-kit check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.3",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/microservices": "^11.1.18",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@teacinema/common": "^1.1.6",
|
||||
"@teacinema/contracts": "^1.2.1",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"pg": "^8.20.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@teacinema/core": "^1.0.9",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/pg": "^8.20.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"drizzle-kit": "^0.31.10",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^30.0.0",
|
||||
"prettier": "^3.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
7
prettier.config.mjs
Normal file
7
prettier.config.mjs
Normal file
@ -0,0 +1,7 @@
|
||||
import config from '@teacinema/core/prettier'
|
||||
|
||||
export default {
|
||||
...config,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
};
|
||||
16
src/core/app.module.ts
Normal file
16
src/core/app.module.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { ConfigModule } from '@nestjs/config'
|
||||
import { DatabaseModule } from 'src/infra/database/database.module'
|
||||
|
||||
import { CategoryModule } from '../modules/category/category.module'
|
||||
import { MovieModule } from '../modules/movie/movie.module'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
DatabaseModule,
|
||||
MovieModule,
|
||||
CategoryModule
|
||||
]
|
||||
})
|
||||
export class AppModule {}
|
||||
26
src/core/bootstrap/grpc.ts
Normal file
26
src/core/bootstrap/grpc.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { INestApplication } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import { MicroserviceOptions, Transport } from '@nestjs/microservices'
|
||||
import { PROTO_PATHS } from '@teacinema/contracts'
|
||||
|
||||
export function setupGrpc(app: INestApplication, config: ConfigService) {
|
||||
const host = config.getOrThrow<string>('GRPC_HOST')
|
||||
const port = config.getOrThrow<string>('GRPC_PORT')
|
||||
const url = `${host}:${port}`
|
||||
|
||||
app.connectMicroservice<MicroserviceOptions>({
|
||||
transport: Transport.GRPC,
|
||||
options: {
|
||||
url,
|
||||
package: ['movie.v1', 'category.v1'],
|
||||
protoPath: [PROTO_PATHS.MOVIE, PROTO_PATHS.CATEGORY],
|
||||
loader: {
|
||||
keepCase: false,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
oneofs: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
11
src/infra/database/database.module.ts
Normal file
11
src/infra/database/database.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Global, Module } from '@nestjs/common'
|
||||
|
||||
import { connectionProvider } from './drizzle/connection.provider'
|
||||
import { drizzleProvider } from './drizzle/drizzle.provider'
|
||||
|
||||
Global()
|
||||
@Module({
|
||||
providers: [connectionProvider, drizzleProvider],
|
||||
exports: [drizzleProvider]
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
26
src/infra/database/drizzle/connection.provider.ts
Normal file
26
src/infra/database/drizzle/connection.provider.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Provider } from '@nestjs/common'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import { Client } from 'pg'
|
||||
|
||||
export const PG_CONNECTION = Symbol('PG_CONNECTION')
|
||||
|
||||
export const connectionProvider: Provider = {
|
||||
provide: PG_CONNECTION,
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
const client = new Client({
|
||||
host: configService.getOrThrow<string>('DATABASE_HOST'),
|
||||
port: configService.getOrThrow<number>('DATABASE_PORT'),
|
||||
user: configService.getOrThrow<string>('DATABASE_USERNAME'),
|
||||
password: configService.getOrThrow<string>('DATABASE_PASSWORD'),
|
||||
database: configService.getOrThrow<string>('DATABASE_NAME'),
|
||||
ssl:
|
||||
configService.getOrThrow<string>('DATABASE_SSL') === 'true'
|
||||
? { rejectUnauthorized: false }
|
||||
: false
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
return client
|
||||
},
|
||||
inject: [ConfigService]
|
||||
}
|
||||
14
src/infra/database/drizzle/drizzle.provider.ts
Normal file
14
src/infra/database/drizzle/drizzle.provider.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Provider } from '@nestjs/common'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { Client } from 'pg'
|
||||
import { PG_CONNECTION } from './connection.provider'
|
||||
|
||||
export const DRIZZLE_DB = Symbol('DRIZZLE_DB')
|
||||
|
||||
export const drizzleProvider: Provider = {
|
||||
provide: DRIZZLE_DB,
|
||||
useFactory: (client: Client) => {
|
||||
return drizzle(client)
|
||||
},
|
||||
inject: [PG_CONNECTION]
|
||||
}
|
||||
30
src/infra/database/drizzle/migration.runner.ts
Normal file
30
src/infra/database/drizzle/migration.runner.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { migrate } from 'drizzle-orm/node-postgres/migrator'
|
||||
import { Pool } from 'pg'
|
||||
|
||||
dotenv.config()
|
||||
const pool = new Pool({
|
||||
host: process.env.DATABASE_HOST,
|
||||
port: Number(process.env.DATABASE_PORT),
|
||||
user: process.env.DATABASE_USERNAME,
|
||||
password: process.env.DATABASE_PASSWORD,
|
||||
database: process.env.DATABASE_NAME
|
||||
})
|
||||
|
||||
const main = async () => {
|
||||
const client = await pool.connect()
|
||||
const db = drizzle(client)
|
||||
|
||||
console.log('Running drizzle migrations...')
|
||||
|
||||
await migrate(db, {
|
||||
migrationsFolder: process.env.DRIZZLE_OUT ?? './drizzle'
|
||||
})
|
||||
|
||||
console.log('Migrations completed')
|
||||
client.release()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main()
|
||||
9
src/infra/database/drizzle/schema/categories,schema.ts
Normal file
9
src/infra/database/drizzle/schema/categories,schema.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { pgTable, varchar } from 'drizzle-orm/pg-core'
|
||||
|
||||
import { baseColumns } from './common.schema'
|
||||
|
||||
export const categories = pgTable('categories', {
|
||||
...baseColumns,
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
slug: varchar('slug', { length: 255 }).unique()
|
||||
})
|
||||
7
src/infra/database/drizzle/schema/common.schema.ts
Normal file
7
src/infra/database/drizzle/schema/common.schema.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||
|
||||
export const baseColumns = {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow()
|
||||
}
|
||||
36
src/infra/database/drizzle/schema/movies.schema.ts
Normal file
36
src/infra/database/drizzle/schema/movies.schema.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { relations } from 'drizzle-orm'
|
||||
import {
|
||||
integer,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
uuid,
|
||||
varchar
|
||||
} from 'drizzle-orm/pg-core'
|
||||
|
||||
import { categories } from './categories,schema'
|
||||
import { baseColumns } from './common.schema'
|
||||
|
||||
export const movies = pgTable('movies', {
|
||||
...baseColumns,
|
||||
title: varchar('title', { length: 255 }).notNull(),
|
||||
slug: varchar('slug', { length: 255 }).unique(),
|
||||
description: text('description').notNull(),
|
||||
poster: text('poster').notNull(),
|
||||
banner: text('banner'),
|
||||
duration: integer('duration').default(0).notNull(),
|
||||
releaseYear: integer('release_year'),
|
||||
releaseDate: timestamp('release_date'),
|
||||
ratingAge: integer('rating_age').default(0),
|
||||
country: varchar('country', { length: 255 }),
|
||||
categoryId: uuid('category_id').references(() => categories.id, {
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
})
|
||||
|
||||
export const movieRelations = relations(movies, ({ one }) => ({
|
||||
category: one(categories, {
|
||||
fields: [movies.categoryId],
|
||||
references: [categories.id]
|
||||
})
|
||||
}))
|
||||
274
src/infra/database/seed.ts
Normal file
274
src/infra/database/seed.ts
Normal file
@ -0,0 +1,274 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import { migrate } from 'drizzle-orm/node-postgres/migrator'
|
||||
import { Pool } from 'pg'
|
||||
|
||||
import { categories } from './drizzle/schema/categories,schema'
|
||||
import { movies } from './drizzle/schema/movies.schema'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const CATEGORIES = [
|
||||
{
|
||||
title: 'Классика',
|
||||
slug: 'classic'
|
||||
},
|
||||
{
|
||||
title: 'Гоночные фильмы',
|
||||
slug: 'racing'
|
||||
},
|
||||
{
|
||||
title: 'Кино Балабанова',
|
||||
slug: 'balabanov'
|
||||
}
|
||||
]
|
||||
|
||||
const MOVIES = [
|
||||
{
|
||||
title: 'Крёстный отец',
|
||||
slug: 'the-godfather',
|
||||
description: `АмериканСкая мафия. Сила семьи. Драма б том, как сын дона Корлеоне втягивается в мир, где правят честь и кровь`,
|
||||
poster: '/posters/the-godfather.webp',
|
||||
banner: '/banners/the-godfather.webp',
|
||||
duration: 175,
|
||||
releaseDate: null,
|
||||
releaseYear: 1972,
|
||||
ratingAge: 16,
|
||||
country: 'США',
|
||||
category: 'classic'
|
||||
},
|
||||
{
|
||||
title: 'Крестный отец 2',
|
||||
slug: 'the-godfather-2',
|
||||
description: `История семьи Корлеоне продолжается: параллельно - путь юного Вито и падение Майкла`,
|
||||
poster: '/posters/the-godfather-2.webp',
|
||||
banner: null,
|
||||
duration: 202,
|
||||
releaseDate: new Date('2025-12-31'),
|
||||
releaseYear: 1974,
|
||||
ratingAge: 16,
|
||||
country: 'США',
|
||||
category: 'classic'
|
||||
},
|
||||
{
|
||||
title: 'Форрест Гамп',
|
||||
slug: 'forrest-gump',
|
||||
description:
|
||||
'Простой парень с чистой душой проживает эпоху перемен и сам становится их частью. Невинность, любовь и великая американская история в одном сердце',
|
||||
poster: '/posters/forrest-gump.webp',
|
||||
banner: '/banners/forrest-gump.webp',
|
||||
duration: 142,
|
||||
releaseDate: null,
|
||||
releaseYear: 1994,
|
||||
ratingAge: 18,
|
||||
country: 'США',
|
||||
category: 'classic'
|
||||
},
|
||||
{
|
||||
title: 'Однажды в Америке',
|
||||
slug: 'once-upon-a-time-in-america',
|
||||
description:
|
||||
'Дружба, предательство и тень гангстерской эпохи. Долгий путь от детских улиц до мрачных разборок взрослых, где прошлое всегда догоняет',
|
||||
poster: '/posters/once-upon-a-time-in-america.webp',
|
||||
banner: '/banners/once-upon-a-time-in-america.webp',
|
||||
duration: 229,
|
||||
releaseDate: null,
|
||||
releaseYear: 1984,
|
||||
ratingAge: 18,
|
||||
country: 'США',
|
||||
category: 'classic'
|
||||
},
|
||||
{
|
||||
title: 'Побег из Шоушенка',
|
||||
slug: 'the-shawshank-redemption',
|
||||
description:
|
||||
'Банкир, несправедливо осуждённый, находит свободу - и надежду - в самых тёмных стенах. История силе духа и долгом пути к свету',
|
||||
poster: '/posters/the-shawshank-redemption.webp',
|
||||
banner: '/banners/the-shawshank-redemption.webp',
|
||||
duration: 142,
|
||||
releaseDate: null,
|
||||
releaseYear: 1994,
|
||||
ratingAge: 18,
|
||||
country: 'США',
|
||||
category: 'classic'
|
||||
},
|
||||
{
|
||||
title: 'Зелёная миля',
|
||||
slug: 'the-green-mile',
|
||||
description:
|
||||
'Тюрьма смертников, чудо посреди ужаса. История чудесах и боли, которую несут даже самые добрые сердца',
|
||||
poster: '/posters/the-green-mile.webp',
|
||||
banner: '/banners/the-green-mile.webp',
|
||||
duration: 189,
|
||||
releaseDate: null,
|
||||
releaseYear: 1999,
|
||||
ratingAge: 12,
|
||||
country: 'США',
|
||||
category: 'classic'
|
||||
},
|
||||
{
|
||||
title: 'Апокалипсис сегодня',
|
||||
slug: 'apocalypse-now',
|
||||
description:
|
||||
'Путешествие в сердце войны во Вьетнаме, где грань между разумом и безумием стирается',
|
||||
poster: '/posters/apocalypse-now.webp',
|
||||
banner: '/banners/apocalypse-now.webp',
|
||||
duration: 147,
|
||||
releaseDate: null,
|
||||
releaseYear: 1979,
|
||||
ratingAge: 18,
|
||||
country: 'США',
|
||||
category: 'classic'
|
||||
},
|
||||
{
|
||||
title: 'Славные парни',
|
||||
slug: 'goodfellas',
|
||||
description:
|
||||
'История о Генри Хилле - начинающем гангстере, занимающемся грабежами. История вместе с подельниками',
|
||||
poster: '/posters/goodfellas.webp',
|
||||
banner: '/banners/goodfellas.webp',
|
||||
duration: 140,
|
||||
releaseDate: null,
|
||||
releaseYear: 1990,
|
||||
ratingAge: 18,
|
||||
country: 'США',
|
||||
category: 'classic'
|
||||
},
|
||||
{
|
||||
title: 'Формула-1',
|
||||
slug: 'f1',
|
||||
description:
|
||||
'прошлом звезда «Формулы-1», Сонни Хейс возвращается в большой спорт спустя 30 лет, чтобы помочь команде-аутсайдеру и стать наставником молодого гонщика',
|
||||
poster: '/posters/f1.webp',
|
||||
banner: '/banners/f1. webp',
|
||||
duration: 156,
|
||||
releaseDate: new Date('2025-06-03'),
|
||||
releaseYear: 2025,
|
||||
ratingAge: 12,
|
||||
country: 'США',
|
||||
category: null
|
||||
},
|
||||
{
|
||||
title: 'Оппенгеймер',
|
||||
slug: 'oppenheimer',
|
||||
description:
|
||||
'История жизни американского физика-теоретика Роберта Оппенгеймера, который во времена Второй мировой войны руководил Манхэттенским проектом - секретными разработками ядерного оружия',
|
||||
poster: '/posters/oppenheimer.webp',
|
||||
banner: '/banners/oppenheimer.webp',
|
||||
duration: 181,
|
||||
releaseDate: new Date('2025-10-01'),
|
||||
releaseYear: 2023,
|
||||
ratingAge: 16,
|
||||
country: 'США',
|
||||
category: null
|
||||
},
|
||||
{
|
||||
title: 'Груз 200',
|
||||
slug: 'cargo-200',
|
||||
description:
|
||||
'Жесткий триллер о советской провинции середины 1980-х убийствах, насилии и распаде системы',
|
||||
poster: '/posters/cargo-200. webp',
|
||||
banner: '/banners/cargo-200.webp',
|
||||
duration: 85,
|
||||
releaseDate: new Date('2025-12-31'),
|
||||
releaseYear: 2007,
|
||||
ratingAge: 18,
|
||||
country: 'Россия',
|
||||
category: 'balabanov'
|
||||
},
|
||||
{
|
||||
title: 'Солнцестояние',
|
||||
slug: 'midsommar',
|
||||
description:
|
||||
'Шведский культ. Летний фестиваль. Драма о группе друзей, группе друзей, которые попадают в загадочный мир древних ритуалов',
|
||||
poster: '/posters/midsommar.webp',
|
||||
banner: '/banners/midsommar.webp',
|
||||
duration: 147,
|
||||
releaseDate: new Date('2025-10-31'),
|
||||
releaseYear: 2019,
|
||||
ratingAge: 18,
|
||||
country: 'США',
|
||||
category: null
|
||||
},
|
||||
{
|
||||
title: 'Человек-паук: Через вселенные',
|
||||
slug: 'spider-man-into-the-spider-verse',
|
||||
description:
|
||||
'Мы всё знаем Питере Паркере. Он спас город, влюбился, потом спасал город снова и снова... все это в нашем измерении. если в результате работы гигантского коллайдера откроется окно из одного измерения в другое? Найдется ли в нем свой Человек-паук? И как он будет выглядеть? Приготовьтесь к тому, что в разных вселенных могут быть разные Люди-пауки и однажды им придется собраться вместе для борьбы с почти непобедимым врагом',
|
||||
poster: '/posters/spider-man-into-the-spider-verse.webp',
|
||||
banner: '/banners/spider-man-into-the-spider-verse.webp',
|
||||
duration: 117,
|
||||
releaseDate: new Date('2025-09-25'),
|
||||
releaseYear: 2018,
|
||||
ratingAge: 6,
|
||||
country: 'США',
|
||||
category: null
|
||||
},
|
||||
{
|
||||
title: 'Интерстеллар',
|
||||
slug: 'interstellar',
|
||||
description:
|
||||
'Путешествие сквозь червоточины и расстояния, где каждая секунда ценнее жизни',
|
||||
poster: '/posters/interstellar.webp',
|
||||
banner: '/banners/interstellar.webp',
|
||||
duration: 169,
|
||||
releaseDate: new Date('2025-12-31'),
|
||||
releaseYear: 2014,
|
||||
ratingAge: 12,
|
||||
country: 'США',
|
||||
category: null
|
||||
},
|
||||
{
|
||||
title: 'Форсаж: Токийский дрифт',
|
||||
slug: 'fast-and-furious-tokyo-drift',
|
||||
description: 'Мир подпольных гонок, дрифта и уличной культуры Токио. ',
|
||||
poster: '/posters/fast-and-furious-tokyo-drift.webp',
|
||||
banner: '/banners/fast-and-furious-tokyo-drift.webp',
|
||||
duration: 104,
|
||||
releaseDate: null,
|
||||
releaseYear: 2006,
|
||||
ratingAge: 12,
|
||||
country: 'США, Япония',
|
||||
category: 'racing'
|
||||
}
|
||||
]
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DATABASE_HOST,
|
||||
port: Number(process.env.DATABASE_PORT),
|
||||
user: process.env.DATABASE_USERNAME,
|
||||
password: process.env.DATABASE_PASSWORD,
|
||||
database: process.env.DATABASE_NAME
|
||||
})
|
||||
|
||||
const main = async () => {
|
||||
const client = await pool.connect()
|
||||
const db = drizzle(client)
|
||||
|
||||
console.log('Seeding categories...')
|
||||
await db
|
||||
.insert(categories)
|
||||
.values([...CATEGORIES])
|
||||
.onConflictDoNothing()
|
||||
console.log('Categories completed')
|
||||
|
||||
console.log('Seeding movies...')
|
||||
const dbCategories = await db.select().from(categories)
|
||||
const categoriesMap = new Map(dbCategories.map(c => [c.slug, c.id]))
|
||||
await db
|
||||
.insert(movies)
|
||||
.values(
|
||||
MOVIES.map(movie => ({
|
||||
...movie,
|
||||
categoryId: movie.category
|
||||
? (categoriesMap.get(movie.category) ?? null)
|
||||
: null
|
||||
}))
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
console.log('Movies completed')
|
||||
client.release()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main()
|
||||
15
src/main.ts
Normal file
15
src/main.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
import { NestFactory } from '@nestjs/core'
|
||||
|
||||
import { AppModule } from './core/app.module'
|
||||
import { setupGrpc } from './core/bootstrap/grpc'
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule)
|
||||
const config = app.get(ConfigService)
|
||||
|
||||
setupGrpc(app, config)
|
||||
app.startAllMicroservices()
|
||||
app.init()
|
||||
}
|
||||
bootstrap()
|
||||
15
src/modules/category/category.controller.ts
Normal file
15
src/modules/category/category.controller.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Controller } from '@nestjs/common'
|
||||
import { GrpcMethod } from '@nestjs/microservices'
|
||||
import { GetAllCategoriesResponse } from '@teacinema/contracts/gen/ts/category'
|
||||
|
||||
import { CategoryService } from './category.service'
|
||||
|
||||
@Controller()
|
||||
export class CategoryController {
|
||||
constructor(private readonly categoryService: CategoryService) {}
|
||||
|
||||
@GrpcMethod('CategoryService', 'GetAllCategories')
|
||||
public async getAll(): Promise<GetAllCategoriesResponse> {
|
||||
return this.categoryService.getAll()
|
||||
}
|
||||
}
|
||||
13
src/modules/category/category.module.ts
Normal file
13
src/modules/category/category.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
import { DatabaseModule } from '../../infra/database/database.module'
|
||||
|
||||
import { CategoryController } from './category.controller'
|
||||
import { CategoryRepository } from './category.repository'
|
||||
import { CategoryService } from './category.service'
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule],
|
||||
controllers: [CategoryController],
|
||||
providers: [CategoryService, CategoryRepository]
|
||||
})
|
||||
export class CategoryModule {}
|
||||
22
src/modules/category/category.repository.ts
Normal file
22
src/modules/category/category.repository.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Inject, Injectable } from '@nestjs/common'
|
||||
import { desc } from 'drizzle-orm'
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||
|
||||
import { DRIZZLE_DB } from '../../infra/database/drizzle/drizzle.provider'
|
||||
import { categories } from '../../infra/database/drizzle/schema/categories,schema'
|
||||
|
||||
@Injectable()
|
||||
export class CategoryRepository {
|
||||
public constructor(@Inject(DRIZZLE_DB) private readonly db: NodePgDatabase) {}
|
||||
|
||||
public async findAll() {
|
||||
return this.db
|
||||
.select({
|
||||
id: categories.id,
|
||||
title: categories.title,
|
||||
slug: categories.slug
|
||||
})
|
||||
.from(categories)
|
||||
.orderBy(desc(categories.createdAt))
|
||||
}
|
||||
}
|
||||
14
src/modules/category/category.service.ts
Normal file
14
src/modules/category/category.service.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { Category } from '@teacinema/contracts/gen/ts/category'
|
||||
|
||||
import { CategoryRepository } from './category.repository'
|
||||
|
||||
@Injectable()
|
||||
export class CategoryService {
|
||||
public constructor(private readonly repository: CategoryRepository) {}
|
||||
|
||||
public async getAll() {
|
||||
const categories: Category[] = await this.repository.findAll()
|
||||
return { categories }
|
||||
}
|
||||
}
|
||||
25
src/modules/movie/movie.controller.ts
Normal file
25
src/modules/movie/movie.controller.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Controller } from '@nestjs/common'
|
||||
import { GrpcMethod } from '@nestjs/microservices'
|
||||
import type {
|
||||
GetMovieRequest,
|
||||
GetMovieResponse,
|
||||
ListMovieRequest,
|
||||
ListMovieResponse
|
||||
} from '@teacinema/contracts/gen/ts/movie'
|
||||
|
||||
import { MovieService } from './movie.service'
|
||||
|
||||
@Controller()
|
||||
export class MovieController {
|
||||
constructor(private readonly movieService: MovieService) {}
|
||||
|
||||
@GrpcMethod('MovieService', 'ListMovies')
|
||||
public async list(data: ListMovieRequest): Promise<ListMovieResponse> {
|
||||
return this.movieService.getAll(data)
|
||||
}
|
||||
|
||||
@GrpcMethod('MovieService', 'GetMovie')
|
||||
public async getOne(data: GetMovieRequest): Promise<GetMovieResponse> {
|
||||
return this.movieService.getOne(data)
|
||||
}
|
||||
}
|
||||
19
src/modules/movie/movie.mapper.ts
Normal file
19
src/modules/movie/movie.mapper.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export class MovieMapper {
|
||||
// todo улучшить типизацию
|
||||
public static toMovie<T>(entity: any): T {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (!entity.releaseDate) return entity as T
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
|
||||
const ms = entity.releaseDate.getTime()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return {
|
||||
...entity,
|
||||
releaseDate: {
|
||||
seconds: Math.floor(ms / 1000),
|
||||
nanos: (ms % 1000) * 1_000_000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/modules/movie/movie.module.ts
Normal file
14
src/modules/movie/movie.module.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common'
|
||||
|
||||
import { DatabaseModule } from '../../infra/database/database.module'
|
||||
|
||||
import { MovieController } from './movie.controller'
|
||||
import { MovieRepository } from './movie.repository'
|
||||
import { MovieService } from './movie.service'
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule],
|
||||
controllers: [MovieController],
|
||||
providers: [MovieService, MovieRepository]
|
||||
})
|
||||
export class MovieModule {}
|
||||
82
src/modules/movie/movie.repository.ts
Normal file
82
src/modules/movie/movie.repository.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { Inject, Injectable } from '@nestjs/common'
|
||||
import { ListMovieRequest } from '@teacinema/contracts/gen/ts/movie'
|
||||
import { and, desc, eq, gt, isNull, lte, or, sql } from 'drizzle-orm'
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
||||
|
||||
import { DRIZZLE_DB } from '../../infra/database/drizzle/drizzle.provider'
|
||||
import { categories } from '../../infra/database/drizzle/schema/categories,schema'
|
||||
import { movies } from '../../infra/database/drizzle/schema/movies.schema'
|
||||
|
||||
@Injectable()
|
||||
export class MovieRepository {
|
||||
public constructor(@Inject(DRIZZLE_DB) private readonly db: NodePgDatabase) {}
|
||||
|
||||
public async findAll(filter: ListMovieRequest) {
|
||||
const where = this.buildWhere(filter)
|
||||
const orderBy = this.buildOrder(filter)
|
||||
|
||||
const query = this.db
|
||||
.select({
|
||||
id: movies.id,
|
||||
title: movies.title,
|
||||
slug: movies.slug,
|
||||
poster: movies.poster,
|
||||
ratingAge: movies.ratingAge,
|
||||
releaseDate: movies.releaseDate
|
||||
})
|
||||
.from(movies)
|
||||
.leftJoin(categories, eq(movies.categoryId, categories.id))
|
||||
.where(where)
|
||||
.orderBy(orderBy)
|
||||
|
||||
if (filter.limit && filter.limit > 0) {
|
||||
return query.limit(filter.limit)
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
public async findBySlug(slug: string) {
|
||||
return this.db
|
||||
.select()
|
||||
.from(movies)
|
||||
.where(eq(movies.slug, slug))
|
||||
.limit(1)
|
||||
.then(result => result[0] ?? null)
|
||||
}
|
||||
|
||||
public async findById(id: string) {
|
||||
return this.db
|
||||
.select()
|
||||
.from(movies)
|
||||
.where(eq(movies.id, id))
|
||||
.limit(1)
|
||||
.then(result => result[0] ?? null)
|
||||
}
|
||||
|
||||
private buildWhere(filter: ListMovieRequest) {
|
||||
const now = new Date()
|
||||
const conditions = []
|
||||
|
||||
if (!filter.category || filter.category === 'all') {
|
||||
} else if (filter.category === 'now') {
|
||||
conditions.push(lte(movies.releaseDate, now))
|
||||
} else if (filter.category === 'soon') {
|
||||
conditions.push(
|
||||
or(gt(movies.releaseDate, now), isNull(movies.releaseDate))
|
||||
)
|
||||
} else {
|
||||
conditions.push(eq(categories.slug, filter.category))
|
||||
}
|
||||
|
||||
return conditions.length ? and(...conditions) : undefined
|
||||
}
|
||||
|
||||
private buildOrder(filter: ListMovieRequest) {
|
||||
if (filter.random) {
|
||||
return sql`RANDOM()`
|
||||
}
|
||||
|
||||
return desc(movies.releaseDate)
|
||||
}
|
||||
}
|
||||
46
src/modules/movie/movie.service.ts
Normal file
46
src/modules/movie/movie.service.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Injectable } from '@nestjs/common'
|
||||
import { RpcException } from '@nestjs/microservices'
|
||||
import { RpcStatus } from '@teacinema/common'
|
||||
import {
|
||||
GetMovieRequest,
|
||||
ListMovieRequest,
|
||||
Movie,
|
||||
MovieDetails
|
||||
} from '@teacinema/contracts/gen/ts/movie'
|
||||
|
||||
import { MovieMapper } from './movie.mapper'
|
||||
import { MovieRepository } from './movie.repository'
|
||||
|
||||
@Injectable()
|
||||
export class MovieService {
|
||||
public constructor(private readonly movieRepository: MovieRepository) {}
|
||||
|
||||
public async getAll(data: ListMovieRequest) {
|
||||
const filter = {
|
||||
category: data.category ?? undefined,
|
||||
random: data.random === true,
|
||||
limit: data.limit > 0 ? data.limit : undefined
|
||||
}
|
||||
|
||||
const movies = await this.movieRepository.findAll(filter)
|
||||
|
||||
return {
|
||||
movies: movies.map(movie => MovieMapper.toMovie<Movie>(movie))
|
||||
}
|
||||
}
|
||||
|
||||
public async getOne(data: GetMovieRequest) {
|
||||
const movie = data.id
|
||||
? await this.movieRepository.findById(data.id)
|
||||
: await this.movieRepository.findBySlug(data.slug)
|
||||
|
||||
if (!movie) {
|
||||
throw new RpcException({
|
||||
code: RpcStatus.NOT_FOUND,
|
||||
details: 'Movie not found'
|
||||
})
|
||||
}
|
||||
|
||||
return { movie: MovieMapper.toMovie<MovieDetails>(movie) }
|
||||
}
|
||||
}
|
||||
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"resolvePackageJsonExports": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"strict": false
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user