├── src ├── common │ ├── scalars │ │ ├── scalars.graphql │ │ └── date.scalar.ts │ └── common.module.ts ├── auth │ ├── jwt-payload.ts │ ├── has-any-role.decorator.ts │ ├── user.decorator.ts │ ├── graphql-auth.guard.ts │ ├── auth.service.ts │ ├── auth.module.ts │ ├── current-user.ts │ ├── jwt.strategy.ts │ └── role.guard.ts ├── app.service.ts ├── item │ ├── fallback-item │ │ ├── fallback-item.graphql │ │ ├── fallback-item.module.ts │ │ └── fallback-item.resolvers.ts │ ├── item.module.ts │ ├── commodity │ │ ├── dto │ │ │ ├── create-commodity.dto.ts │ │ │ └── update-commodity.dto.ts │ │ ├── commodity.module.ts │ │ ├── commodity.graphql │ │ ├── commodity.resolvers.ts │ │ └── commodity.service.ts │ ├── ship │ │ ├── dto │ │ │ ├── create-ship.dto.ts │ │ │ └── update-ship.dto.ts │ │ ├── ship.module.ts │ │ ├── ship.graphql │ │ ├── ship.resolvers.ts │ │ └── ship.service.ts │ ├── item.service.ts │ ├── item.graphql │ └── item.resolvers.ts ├── manufacturer │ ├── dto │ │ ├── create-manufacturer.dto.ts │ │ └── update-manufacturer.dto.ts │ ├── manufacturer.module.ts │ ├── manufacturer.graphql │ ├── manufacturer.resolvers.ts │ └── manufacturer.service.ts ├── location-type │ ├── dto │ │ ├── create-location-type.dto.ts │ │ └── update-location.dto.ts │ ├── location-type.module.ts │ ├── location-type.graphql │ ├── location-type.resolvers.ts │ └── location-type.service.ts ├── commodity-category │ ├── dto │ │ ├── create-commodity-category.dto.ts │ │ └── update-commodity-category.dto.ts │ ├── commodity-category.module.ts │ ├── commodity-category.graphql │ ├── commodity-category.resolvers.ts │ └── commodity-category.service.ts ├── possession │ ├── possession.module.ts │ ├── dto │ │ └── create-possession.dto.ts │ ├── possession.graphql │ ├── possession.service.ts │ └── possession.resolvers.ts ├── game-version │ ├── game-version.module.ts │ ├── dto │ │ ├── create-game-version.dto.ts │ │ └── update-game-version.dto.ts │ ├── game-version.graphql │ ├── game-version.resolvers.ts │ └── game-version.service.ts ├── organization │ ├── organization.module.ts │ ├── dto │ │ ├── create-organization.dto.ts │ │ └── update-organization.dto.ts │ ├── organization.graphql │ ├── organization.resolvers.ts │ └── organization.service.ts ├── app.controller.ts ├── account │ ├── dto │ │ └── create-account.dto.ts │ ├── account.module.ts │ ├── account.graphql │ ├── account.resolvers.ts │ └── account.service.ts ├── organization-member │ ├── dto │ │ └── join-organization.dto.ts │ ├── organization-member.graphql │ ├── organization-member.module.ts │ ├── organization-member.service.ts │ └── organization-member.resolvers.ts ├── mail.service.ts ├── app.roles.ts ├── trade │ ├── trade.graphql │ ├── trade.module.ts │ ├── trade.resolvers.ts │ └── trade.service.ts ├── transaction │ ├── dto │ │ ├── create-transaction.dto.ts │ │ └── create-first-transaction-detail.dto.ts │ ├── transaction.module.ts │ ├── transaction.graphql │ ├── transaction.service.ts │ └── transaction.resolvers.ts ├── location │ ├── dto │ │ ├── create-location.dto.ts │ │ └── update-location.dto.ts │ ├── location.module.ts │ ├── location.graphql │ ├── location.resolvers.ts │ └── location.service.ts ├── transaction-detail │ ├── transaction-detail.module.ts │ ├── dto │ │ ├── create-lost-based-on-transaction-detail.dto.ts │ │ ├── create-sold-transaction-detail.dto.ts │ │ ├── create-bought-transaction-detail.dto.ts │ │ ├── create-lost-transaction-detail.dto.ts │ │ └── create-transaction-detail.dto.ts │ ├── transaction-detail.graphql │ └── transaction-detail.service.ts ├── app.controller.spec.ts ├── database.service.ts ├── item-price │ ├── dto │ │ ├── create-item-price.dto.ts │ │ └── update-item-price.dto.ts │ ├── item-price.module.ts │ ├── item-price.graphql │ ├── item-price.service.ts │ └── item-price.resolvers.ts ├── main.ts └── app.module.ts ├── .prettierignore ├── .vscode ├── settings.json └── extensions.json ├── .eslintignore ├── tsconfig.build.json ├── docs ├── Star Citizen Trade Market.png └── Star Citizen Trade Market.xml ├── docker-entrypoint.sh ├── CHANGELOG.md ├── tsconfig.spec.json ├── .prettierrc.json ├── test ├── jest-e2e.json └── app.e2e-spec.ts ├── .editorconfig ├── migrations ├── 1543597986030_setup.ts ├── 1543870892068_organization-spectrum-id.ts ├── 1543943762464_account-password.ts ├── 1546972279894_location-can-trade.ts ├── 1543668154974_account-roles.ts ├── 1545494052177_item-details.ts ├── 1544035943442_possession.ts ├── 1544367125527_item-price-visibility-function.ts ├── 1544800398110_trade-function.ts ├── 1545127269223_update-trade-function.ts ├── 1543613769405_default-data.ts └── 1543603809087_init.ts ├── .dockerignore ├── .env.example ├── Dockerfile ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ └── ci.yml └── PULL_REQUEST_TEMPLATE.md ├── LICENSE ├── .gitignore ├── docker-compose.yml ├── README.md ├── package.json ├── CODE_OF_CONDUCT.md └── .eslintrc.js /src/common/scalars/scalars.graphql: -------------------------------------------------------------------------------- 1 | scalar Date 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/graphql.schema.ts 2 | graphql.schema.ts 3 | .history/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /src/auth/jwt-payload.ts: -------------------------------------------------------------------------------- 1 | export interface JwtPayload { 2 | username: string; 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifier": "relative" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | docs 4 | src/graphql.schema.ts 5 | graphql.schema.ts 6 | .history/ 7 | dist/ 8 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /docs/Star Citizen Trade Market.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shinigami92/star-citizen-trade-market-api/HEAD/docs/Star Citizen Trade Market.png -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -xe 2 | 3 | echo "Wait 10 seconds for database container" 4 | sleep 10 5 | 6 | yarn migrate up 7 | 8 | yarn start:prod 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # [0.1.0](https://github.com/Shinigami92/star-citizen-trade-market-api/compare/v0.1.0...v0.1.0) (2018-11-30) 4 | 5 | - Initial Version 6 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | public root(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "esbenp.prettier-vscode", 5 | "pflannery.vscode-versionlens", 6 | "tobermory.es6-string-html" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/common/common.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DateScalar } from './scalars/date.scalar'; 3 | 4 | @Module({ 5 | providers: [DateScalar] 6 | }) 7 | export class CommonModule {} 8 | -------------------------------------------------------------------------------- /src/item/fallback-item/fallback-item.graphql: -------------------------------------------------------------------------------- 1 | type FallbackItem implements Item { 2 | id: ID! 3 | name: String! 4 | inGameSinceVersionId: ID! 5 | inGameSinceVersion: GameVersion! 6 | inGameSince: Date 7 | type: ItemType! 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "printWidth": 120, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "tabWidth": 2, 9 | "useTabs": false 10 | } 11 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/auth/has-any-role.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | import { Role } from '../graphql.schema'; 3 | 4 | export const HasAnyRole: (...roles: Role[]) => any = (...roles) => SetMetadata('roles', roles); 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/manufacturer/dto/create-manufacturer.dto.ts: -------------------------------------------------------------------------------- 1 | import { Length } from 'class-validator'; 2 | import { CreateManufacturerInput } from '../../graphql.schema'; 3 | 4 | export class CreateManufacturerDto implements CreateManufacturerInput { 5 | @Length(2) 6 | public name!: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/location-type/dto/create-location-type.dto.ts: -------------------------------------------------------------------------------- 1 | import { Length } from 'class-validator'; 2 | import { CreateLocationTypeInput } from '../../graphql.schema'; 3 | 4 | export class CreateLocationTypeDto implements CreateLocationTypeInput { 5 | @Length(3) 6 | public name!: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/auth/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const CurrentUser: (...dataOrPipes: unknown[]) => ParameterDecorator = createParamDecorator( 4 | (_data: unknown, ctx: ExecutionContext) => ctx.getArgByIndex(2)?.req?.user 5 | ); 6 | -------------------------------------------------------------------------------- /src/commodity-category/dto/create-commodity-category.dto.ts: -------------------------------------------------------------------------------- 1 | import { Length } from 'class-validator'; 2 | import { CreateCommodityCategoryInput } from '../../graphql.schema'; 3 | 4 | export class CreateCommodityCategoryDto implements CreateCommodityCategoryInput { 5 | @Length(3) 6 | public name!: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/location-type/dto/update-location.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, Length } from 'class-validator'; 2 | import { UpdateLocationTypeInput } from '../../graphql.schema'; 3 | 4 | export class UpdateLocationTypeDto implements UpdateLocationTypeInput { 5 | @IsOptional() 6 | @Length(3) 7 | public name?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/manufacturer/dto/update-manufacturer.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, Length } from 'class-validator'; 2 | import { UpdateManufacturerInput } from '../../graphql.schema'; 3 | 4 | export class UpdateManufacturerDto implements UpdateManufacturerInput { 5 | @IsOptional() 6 | @Length(2) 7 | public name?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/possession/possession.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PossessionResolvers } from './possession.resolvers'; 3 | import { PossessionService } from './possession.service'; 4 | 5 | @Module({ 6 | providers: [PossessionService, PossessionResolvers] 7 | }) 8 | export class PossessionModule {} 9 | -------------------------------------------------------------------------------- /src/game-version/game-version.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GameVersionResolvers } from './game-version.resolvers'; 3 | import { GameVersionService } from './game-version.service'; 4 | 5 | @Module({ 6 | providers: [GameVersionService, GameVersionResolvers] 7 | }) 8 | export class GameVersionModule {} 9 | -------------------------------------------------------------------------------- /src/manufacturer/manufacturer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ManufacturerResolvers } from './manufacturer.resolvers'; 3 | import { ManufacturerService } from './manufacturer.service'; 4 | 5 | @Module({ 6 | providers: [ManufacturerService, ManufacturerResolvers] 7 | }) 8 | export class ManufacturerModule {} 9 | -------------------------------------------------------------------------------- /src/organization/organization.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { OrganizationResolvers } from './organization.resolvers'; 3 | import { OrganizationService } from './organization.service'; 4 | 5 | @Module({ 6 | providers: [OrganizationService, OrganizationResolvers] 7 | }) 8 | export class OrganizationModule {} 9 | -------------------------------------------------------------------------------- /src/commodity-category/dto/update-commodity-category.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, Length } from 'class-validator'; 2 | import { UpdateCommodityCategoryInput } from '../../graphql.schema'; 3 | 4 | export class UpdateCommodityCategoryDto implements UpdateCommodityCategoryInput { 5 | @IsOptional() 6 | @Length(3) 7 | public name?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/location-type/location-type.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { LocationTypeResolvers } from './location-type.resolvers'; 3 | import { LocationTypeService } from './location-type.service'; 4 | 5 | @Module({ 6 | providers: [LocationTypeService, LocationTypeResolvers] 7 | }) 8 | export class LocationTypeModule {} 9 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | public constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | public root(): string { 10 | return this.appService.root(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/organization/dto/create-organization.dto.ts: -------------------------------------------------------------------------------- 1 | import { Length } from 'class-validator'; 2 | import { CreateOrganizationInput } from '../../graphql.schema'; 3 | 4 | export class CreateOrganizationDto implements CreateOrganizationInput { 5 | @Length(1, 50) 6 | public name!: string; 7 | @Length(3, 10) 8 | public spectrumId!: string; 9 | } 10 | -------------------------------------------------------------------------------- /migrations/1543597986030_setup.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate'; 2 | 3 | export const shorthands: undefined = undefined; 4 | 5 | export function up(pgm: MigrationBuilder): void { 6 | pgm.createExtension('uuid-ossp'); 7 | } 8 | 9 | export function down(pgm: MigrationBuilder): void { 10 | pgm.dropExtension('uuid-ossp'); 11 | } 12 | -------------------------------------------------------------------------------- /src/commodity-category/commodity-category.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommodityCategoryResolvers } from './commodity-category.resolvers'; 3 | import { CommodityCategoryService } from './commodity-category.service'; 4 | 5 | @Module({ 6 | providers: [CommodityCategoryService, CommodityCategoryResolvers] 7 | }) 8 | export class CommodityCategoryModule {} 9 | -------------------------------------------------------------------------------- /src/game-version/dto/create-game-version.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsOptional, Length } from 'class-validator'; 2 | import { CreateGameVersionInput } from '../../graphql.schema'; 3 | 4 | export class CreateGameVersionDto implements CreateGameVersionInput { 5 | @Length(16, 18) 6 | public identifier!: string; 7 | @IsOptional() 8 | @IsDate() 9 | public release?: Date; 10 | } 11 | -------------------------------------------------------------------------------- /src/account/dto/create-account.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, Length } from 'class-validator'; 2 | import { CreateAccountInput } from '../../graphql.schema'; 3 | 4 | export class CreateAccountDto implements CreateAccountInput { 5 | @Length(3) 6 | public username!: string; 7 | @IsNotEmpty() 8 | public handle!: string; 9 | @IsEmail() 10 | public email!: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/game-version/dto/update-game-version.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsOptional, Length } from 'class-validator'; 2 | import { UpdateGameVersionInput } from '../../graphql.schema'; 3 | 4 | export class UpdateGameVersionDto implements UpdateGameVersionInput { 5 | @IsOptional() 6 | @Length(16, 18) 7 | public identifier?: string; 8 | @IsOptional() 9 | @IsDate() 10 | public release?: Date; 11 | } 12 | -------------------------------------------------------------------------------- /src/organization/dto/update-organization.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, Length } from 'class-validator'; 2 | import { UpdateOrganizationInput } from '../../graphql.schema'; 3 | 4 | export class UpdateOrganizationDto implements UpdateOrganizationInput { 5 | @IsOptional() 6 | @Length(1, 50) 7 | public name?: string; 8 | @IsOptional() 9 | @Length(3, 10) 10 | public spectrumId?: string; 11 | } 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .editorconfig 3 | .env 4 | .git 5 | .github 6 | .gitignore 7 | .prettierignore 8 | .prettierrc.json 9 | .travis.yml 10 | .vscode 11 | CHANGELOG.md 12 | CODE_OF_CONDUCT.md 13 | Dockerfile 14 | PULL_REQUEST_TEMPLATE.md 15 | README.md 16 | dist 17 | docker-compose.yml 18 | docker-volumes 19 | docs 20 | graphql.schema.ts 21 | node_modules 22 | test 23 | tsconfig.spec.json 24 | tslint.json 25 | -------------------------------------------------------------------------------- /migrations/1543870892068_organization-spectrum-id.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate'; 2 | 3 | export const shorthands: undefined = undefined; 4 | 5 | export function up(pgm: MigrationBuilder): void { 6 | pgm.renameColumn('organization', 'tag', 'spectrum_id'); 7 | } 8 | 9 | export function down(pgm: MigrationBuilder): void { 10 | pgm.renameColumn('organization', 'spectrum_id', 'tag'); 11 | } 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API_HOSTNAME=127.0.0.1 2 | API_PORT=3000 3 | 4 | JWT_SECRET_KEY=secret 5 | JWT_EXPIRES_IN=7d 6 | 7 | DATABASE_URL=postgres://sctm:sctm@localhost:5432/sctm 8 | DB_HOST=localhost 9 | DB_PORT=5432 10 | DB_USER=sctm 11 | DB_PASSWORD=sctm 12 | DB_NAME=sctm 13 | 14 | MAIL_SMTP_HOST=smtp.gmail.com 15 | MAIL_SMTP_PORT=587 16 | MAIL_AUTH_USER=mail@gmail.com 17 | MAIL_AUTH_PASS=password 18 | MAIL_DEFAULT_FROM=mail@gmail.com 19 | -------------------------------------------------------------------------------- /migrations/1543943762464_account-password.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder, PgType } from 'node-pg-migrate'; 2 | 3 | export const shorthands: undefined = undefined; 4 | 5 | export function up(pgm: MigrationBuilder): void { 6 | pgm.addColumn('account', { 7 | password: { type: PgType.TEXT, notNull: true, default: '' } 8 | }); 9 | } 10 | 11 | export function down(pgm: MigrationBuilder): void { 12 | pgm.dropColumn('account', 'password'); 13 | } 14 | -------------------------------------------------------------------------------- /src/organization-member/dto/join-organization.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsOptional, IsUUID } from 'class-validator'; 2 | import { JoinOrganizationInput } from '../../graphql.schema'; 3 | 4 | export class JoinOrganizationDto implements JoinOrganizationInput { 5 | @IsUUID('4') 6 | public organizationId!: string; 7 | @IsOptional() 8 | @IsUUID('4') 9 | public accountId?: string | undefined; 10 | @IsOptional() 11 | @IsDate() 12 | public since?: Date; 13 | } 14 | -------------------------------------------------------------------------------- /src/item/item.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommonModule } from '../common/common.module'; 3 | import { CommodityModule } from './commodity/commodity.module'; 4 | import { ItemResolvers } from './item.resolvers'; 5 | import { ItemService } from './item.service'; 6 | 7 | @Module({ 8 | imports: [CommodityModule, CommonModule], 9 | exports: [CommodityModule], 10 | providers: [ItemService, ItemResolvers] 11 | }) 12 | export class ItemModule {} 13 | -------------------------------------------------------------------------------- /src/mail.service.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import { createTransport, Transporter } from 'nodemailer'; 3 | 4 | dotenv.config(); 5 | 6 | export const transporter: Transporter = createTransport({ 7 | host: process.env.MAIL_SMTP_HOST, 8 | port: +(process.env.MAIL_SMTP_PORT ?? 587), 9 | secure: false, 10 | requireTLS: true, 11 | auth: { user: process.env.MAIL_AUTH_USER, pass: process.env.MAIL_AUTH_PASS }, 12 | from: process.env.MAIL_DEFAULT_FROM 13 | }); 14 | -------------------------------------------------------------------------------- /src/app.roles.ts: -------------------------------------------------------------------------------- 1 | import { RolesBuilder } from 'nest-access-control'; 2 | 3 | export enum AppRoles { 4 | USER = 'USER', 5 | ADVANCED = 'ADVANCED', 6 | USERADMIN = 'USERADMIN', 7 | ADMIN = 'ADMIN' 8 | } 9 | 10 | export const roles: RolesBuilder = new RolesBuilder(); 11 | 12 | roles 13 | .grant(AppRoles.USER) 14 | .readAny('commodityCategory') 15 | .grant(AppRoles.ADVANCED) 16 | .extend(AppRoles.USER) 17 | .createAny('commodityCategory') 18 | .updateAny('commodityCategory'); 19 | -------------------------------------------------------------------------------- /src/auth/graphql-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { GqlExecutionContext, GraphQLExecutionContext } from '@nestjs/graphql'; 3 | import { AuthGuard } from '@nestjs/passport'; 4 | 5 | @Injectable() 6 | export class GraphqlAuthGuard extends AuthGuard('jwt') { 7 | public getRequest(context: ExecutionContext): any { 8 | const ctx: GraphQLExecutionContext = GqlExecutionContext.create(context); 9 | return ctx.getContext().req; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /migrations/1546972279894_location-can-trade.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder, PgType } from 'node-pg-migrate'; 2 | 3 | export const shorthands: undefined = undefined; 4 | 5 | export function up(pgm: MigrationBuilder): void { 6 | pgm.addColumn('location', { 7 | can_trade: { 8 | type: PgType.BOOLEAN, 9 | notNull: true, 10 | default: false 11 | } 12 | }); 13 | } 14 | 15 | export function down(pgm: MigrationBuilder): void { 16 | pgm.dropColumn('location', 'can_trade'); 17 | } 18 | -------------------------------------------------------------------------------- /src/item/commodity/dto/create-commodity.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsOptional, IsUUID, Length } from 'class-validator'; 2 | import { CreateCommodityInput } from '../../../graphql.schema'; 3 | 4 | export class CreateCommodityDto implements CreateCommodityInput { 5 | @Length(3) 6 | public name!: string; 7 | @IsUUID('4') 8 | public inGameSinceVersionId!: string; 9 | @IsOptional() 10 | @IsDate() 11 | public inGameSince?: Date; 12 | @IsUUID('4') 13 | public commodityCategoryId!: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/account/account.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { OrganizationModule } from '../organization/organization.module'; 3 | import { OrganizationService } from '../organization/organization.service'; 4 | import { AccountResolvers } from './account.resolvers'; 5 | import { AccountService } from './account.service'; 6 | 7 | @Module({ 8 | imports: [OrganizationModule], 9 | providers: [AccountService, AccountResolvers, OrganizationService] 10 | }) 11 | export class AccountModule {} 12 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AccountService } from '../account/account.service'; 3 | import { Account } from '../graphql.schema'; 4 | import { JwtPayload } from './jwt-payload'; 5 | 6 | @Injectable() 7 | export class AuthService { 8 | public constructor(private readonly accountService: AccountService) {} 9 | 10 | public async validateUser({ username }: JwtPayload): Promise { 11 | return await this.accountService.findOneByUsername(username); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | import { AccountModule } from '../account/account.module'; 4 | import { AccountService } from '../account/account.service'; 5 | import { AuthService } from './auth.service'; 6 | import { JwtStrategy } from './jwt.strategy'; 7 | 8 | @Module({ 9 | imports: [PassportModule.register({ defaultStrategy: 'jwt' }), AccountModule], 10 | providers: [AuthService, JwtStrategy, AccountService] 11 | }) 12 | export class AuthModule {} 13 | -------------------------------------------------------------------------------- /src/trade/trade.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | trades(searchInput: TradeSearchInput): [Trade] 3 | } 4 | 5 | type Trade { 6 | buyItemPrice: ItemPrice! 7 | sellItemPrice: ItemPrice! 8 | item: Item! 9 | startLocation: Location! 10 | endLocation: Location! 11 | profit: Float! 12 | margin: Float! 13 | scanTime: Date! 14 | scannedInGameVersionId: ID! 15 | scannedInGameVersion: GameVersion! 16 | } 17 | 18 | input TradeSearchInput { 19 | startLocationId: ID 20 | endLocationId: ID 21 | itemIds: [ID] 22 | gameVersionId: ID 23 | } 24 | -------------------------------------------------------------------------------- /src/transaction/dto/create-transaction.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsOptional, IsUUID } from 'class-validator'; 2 | import { CreateTransactionInput } from '../../graphql.schema'; 3 | import { CreateFirstTransactionDetailDto } from './create-first-transaction-detail.dto'; 4 | 5 | export class CreateTransactionDto implements CreateTransactionInput { 6 | @IsOptional() 7 | @IsUUID('4') 8 | public accountId?: string | undefined; 9 | @IsUUID('4') 10 | public commodityId!: string; 11 | public transactionDetail!: CreateFirstTransactionDetailDto; 12 | } 13 | -------------------------------------------------------------------------------- /src/item/fallback-item/fallback-item.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommonModule } from '../../common/common.module'; 3 | import { GameVersionModule } from '../../game-version/game-version.module'; 4 | import { GameVersionService } from '../../game-version/game-version.service'; 5 | import { FallbackItemResolvers } from './fallback-item.resolvers'; 6 | 7 | @Module({ 8 | imports: [GameVersionModule, CommonModule], 9 | providers: [FallbackItemResolvers, GameVersionService] 10 | }) 11 | export class FallbackItemModule {} 12 | -------------------------------------------------------------------------------- /migrations/1543668154974_account-roles.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate'; 2 | 3 | export const shorthands: undefined = undefined; 4 | 5 | export function up(pgm: MigrationBuilder): void { 6 | pgm.createType('account_role', ['USER', 'ADVANCED', 'USERADMIN', 'ADMIN']); 7 | pgm.addColumn('account', { 8 | roles: { type: 'account_role[]', notNull: true, default: '{"USER"}' } 9 | }); 10 | } 11 | 12 | export function down(pgm: MigrationBuilder): void { 13 | pgm.dropColumn('account', 'roles'); 14 | pgm.dropType('account_role'); 15 | } 16 | -------------------------------------------------------------------------------- /src/item/commodity/dto/update-commodity.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsOptional, IsUUID, Length } from 'class-validator'; 2 | import { UpdateCommodityInput } from '../../../graphql.schema'; 3 | 4 | export class UpdateCommodityDto implements UpdateCommodityInput { 5 | @IsOptional() 6 | @Length(3) 7 | public name?: string; 8 | @IsOptional() 9 | @IsUUID('4') 10 | public inGameSinceVersionId?: string; 11 | @IsOptional() 12 | @IsDate() 13 | public inGameSince?: Date; 14 | @IsOptional() 15 | @IsUUID('4') 16 | public commodityCategoryId?: string; 17 | } 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine as build 2 | 3 | RUN apk --no-cache add --virtual native-deps \ 4 | g++ gcc libgcc libstdc++ linux-headers \ 5 | autoconf automake make nasm python git 6 | RUN yarn global add node-gyp 7 | 8 | WORKDIR /build 9 | 10 | COPY . . 11 | RUN yarn install --frozen-lockfile 12 | RUN yarn build 13 | 14 | FROM node:12-alpine 15 | LABEL maintainer="chrissi92@hotmail.de" 16 | WORKDIR /usr/src/app 17 | 18 | COPY --from=build /build . 19 | RUN chmod +x ./docker-entrypoint.sh 20 | 21 | EXPOSE 3000 22 | ENTRYPOINT ["/usr/src/app/docker-entrypoint.sh"] 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "allowSyntheticDefaultImports": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "noUnusedLocals": true, 16 | "strict": true 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /src/transaction/transaction.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { TransactionDetailModule } from '../transaction-detail/transaction-detail.module'; 3 | import { TransactionDetailService } from '../transaction-detail/transaction-detail.service'; 4 | import { TransactionResolvers } from './transaction.resolvers'; 5 | import { TransactionService } from './transaction.service'; 6 | 7 | @Module({ 8 | imports: [forwardRef(() => TransactionDetailModule)], 9 | providers: [TransactionService, TransactionResolvers, TransactionDetailService] 10 | }) 11 | export class TransactionModule {} 12 | -------------------------------------------------------------------------------- /src/location/dto/create-location.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsDate, IsOptional, IsUUID, Length } from 'class-validator'; 2 | import { CreateLocationInput } from '../../graphql.schema'; 3 | 4 | export class CreateLocationDto implements CreateLocationInput { 5 | @Length(3) 6 | public name!: string; 7 | @IsOptional() 8 | @IsUUID('4') 9 | public parentLocationId?: string; 10 | @IsUUID('4') 11 | public typeId!: string; 12 | @IsUUID('4') 13 | public inGameSinceVersionId!: string; 14 | @IsDate() 15 | public inGameSince?: Date; 16 | @IsOptional() 17 | @IsBoolean() 18 | public canTrade?: boolean; 19 | } 20 | -------------------------------------------------------------------------------- /src/possession/dto/create-possession.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsNumber, IsOptional, IsPositive, IsUUID } from 'class-validator'; 2 | import { CreatePossessionInput, PurchaseCurrency } from '../../graphql.schema'; 3 | 4 | export class CreatePossessionDto implements CreatePossessionInput { 5 | @IsOptional() 6 | @IsUUID('4') 7 | public accountId?: string | undefined; 8 | @IsUUID('4') 9 | public itemId!: string; 10 | @IsNumber() 11 | @IsPositive() 12 | public purchasePrice!: number; 13 | public purchaseCurrency!: PurchaseCurrency; 14 | @IsOptional() 15 | @IsDate() 16 | public purchaseDate?: Date; 17 | } 18 | -------------------------------------------------------------------------------- /src/transaction-detail/transaction-detail.module.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Module } from '@nestjs/common'; 2 | import { TransactionModule } from '../transaction/transaction.module'; 3 | import { TransactionService } from '../transaction/transaction.service'; 4 | import { TransactionDetailResolvers } from './transaction-detail.resolvers'; 5 | import { TransactionDetailService } from './transaction-detail.service'; 6 | 7 | @Module({ 8 | imports: [forwardRef(() => TransactionModule)], 9 | providers: [TransactionDetailService, TransactionDetailResolvers, TransactionService] 10 | }) 11 | export class TransactionDetailModule {} 12 | -------------------------------------------------------------------------------- /src/transaction-detail/dto/create-lost-based-on-transaction-detail.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsOptional, IsString, IsUUID } from 'class-validator'; 2 | import { CreateLostBasedOnTransactionDetailInput } from '../../graphql.schema'; 3 | 4 | export class CreateLostBasedOnTransactionDetailDto implements CreateLostBasedOnTransactionDetailInput { 5 | @IsUUID('4') 6 | public transactionDetailId!: string; 7 | @IsOptional() 8 | @IsUUID('4') 9 | public locationId?: string | undefined; 10 | @IsOptional() 11 | @IsString() 12 | public note?: string | undefined; 13 | @IsOptional() 14 | @IsDate() 15 | public timestamp?: Date; 16 | } 17 | -------------------------------------------------------------------------------- /src/item/ship/dto/create-ship.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsInt, IsNotEmpty, IsPositive, IsUUID, Length } from 'class-validator'; 2 | import { CreateShipInput } from '../../../graphql.schema'; 3 | 4 | export class CreateShipDto implements CreateShipInput { 5 | @Length(3) 6 | public name!: string; 7 | @IsUUID('4') 8 | public inGameSinceVersionId!: string; 9 | @IsDate() 10 | public inGameSince?: Date; 11 | @IsInt() 12 | @IsPositive() 13 | public scu!: number; 14 | @IsUUID('4') 15 | public manufacturerId!: string; 16 | @IsNotEmpty() 17 | public focus!: string; 18 | @IsInt() 19 | @IsPositive() 20 | public size!: number; 21 | } 22 | -------------------------------------------------------------------------------- /src/location-type/location-type.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | locationTypes: [LocationType] 3 | locationType(id: ID!): LocationType 4 | } 5 | 6 | type Mutation { 7 | createLocationType(input: CreateLocationTypeInput!): LocationType! 8 | updateLocationType(id: ID!, input: UpdateLocationTypeInput!): LocationType! 9 | } 10 | 11 | type Subscription { 12 | locationTypeCreated: LocationType! 13 | locationTypeUpdated: LocationType! 14 | } 15 | 16 | type LocationType { 17 | id: ID! 18 | name: String! 19 | } 20 | 21 | input CreateLocationTypeInput { 22 | name: String! 23 | } 24 | 25 | input UpdateLocationTypeInput { 26 | name: String 27 | } 28 | -------------------------------------------------------------------------------- /src/manufacturer/manufacturer.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | manufacturers: [Manufacturer] 3 | manufacturer(id: ID!): Manufacturer 4 | } 5 | 6 | type Mutation { 7 | createManufacturer(input: CreateManufacturerInput!): Manufacturer! 8 | updateManufacturer(id: ID!, input: UpdateManufacturerInput!): Manufacturer! 9 | } 10 | 11 | type Subscription { 12 | manufacturerCreated: Manufacturer! 13 | manufacturerUpdated: Manufacturer! 14 | } 15 | 16 | type Manufacturer { 17 | id: ID! 18 | name: String! 19 | } 20 | 21 | input CreateManufacturerInput { 22 | name: String! 23 | } 24 | 25 | input UpdateManufacturerInput { 26 | name: String 27 | } 28 | -------------------------------------------------------------------------------- /src/organization-member/organization-member.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | organizationMembers: [OrganizationMember] 3 | organizationMember(organizationId: ID!, accountId: ID!): OrganizationMember 4 | } 5 | 6 | type Mutation { 7 | joinOrganization(input: JoinOrganizationInput!): OrganizationMember! 8 | } 9 | 10 | type Subscription { 11 | organizationMemberCreated: OrganizationMember! 12 | } 13 | 14 | type OrganizationMember { 15 | organizationId: ID! 16 | organization: Organization! 17 | accountId: ID! 18 | account: Account! 19 | since: Date 20 | } 21 | 22 | input JoinOrganizationInput { 23 | organizationId: ID! 24 | accountId: ID 25 | since: Date 26 | } 27 | -------------------------------------------------------------------------------- /src/transaction/dto/create-first-transaction-detail.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsInt, IsNumber, IsOptional, IsPositive, IsString, IsUUID } from 'class-validator'; 2 | import { CreateFirstTransactionDetailInput } from '../../graphql.schema'; 3 | 4 | export class CreateFirstTransactionDetailDto implements CreateFirstTransactionDetailInput { 5 | @IsUUID('4') 6 | public locationId!: string; 7 | @IsNumber() 8 | @IsPositive() 9 | public price!: number; 10 | @IsInt() 11 | @IsPositive() 12 | public quantity!: number; 13 | @IsOptional() 14 | @IsString() 15 | public note?: string | undefined; 16 | @IsOptional() 17 | @IsDate() 18 | public timestamp?: Date; 19 | } 20 | -------------------------------------------------------------------------------- /src/location/dto/update-location.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsBoolean, IsDate, IsOptional, IsUUID, Length } from 'class-validator'; 2 | import { UpdateLocationInput } from '../../graphql.schema'; 3 | 4 | export class UpdateLocationDto implements UpdateLocationInput { 5 | @IsOptional() 6 | @Length(3) 7 | public name?: string; 8 | @IsOptional() 9 | @IsUUID('4') 10 | public parentLocationId?: string; 11 | @IsOptional() 12 | @IsUUID('4') 13 | public typeId?: string; 14 | @IsOptional() 15 | @IsUUID('4') 16 | public inGameSinceVersionId?: string; 17 | @IsOptional() 18 | @IsDate() 19 | public inGameSince?: Date; 20 | @IsOptional() 21 | @IsBoolean() 22 | public canTrade?: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule] 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let app: TestingModule; 7 | 8 | beforeAll(async () => { 9 | app = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService] 12 | }).compile(); 13 | }); 14 | 15 | describe('root', () => { 16 | it('should return "Hello World!"', () => { 17 | const appController: AppController = app.get(AppController); 18 | expect(appController.root()).toBe('Hello World!'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/game-version/game-version.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | gameVersions: [GameVersion] 3 | gameVersion(id: ID!): GameVersion 4 | } 5 | 6 | type Mutation { 7 | createGameVersion(input: CreateGameVersionInput!): GameVersion! 8 | updateGameVersion(id: ID!, input: UpdateGameVersionInput!): GameVersion! 9 | } 10 | 11 | type Subscription { 12 | gameVersionCreated: GameVersion! 13 | gameVersionUpdated: GameVersion! 14 | } 15 | 16 | type GameVersion { 17 | id: ID! 18 | identifier: String! 19 | release: Date 20 | } 21 | 22 | input CreateGameVersionInput { 23 | identifier: String! 24 | release: Date 25 | } 26 | 27 | input UpdateGameVersionInput { 28 | identifier: String 29 | release: Date 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[FEATURE REQUEST]' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /src/item/item.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { Item } from '../graphql.schema'; 5 | 6 | export const TABLENAME: string = 'item'; 7 | 8 | @Injectable() 9 | export class ItemService { 10 | public async findAll(): Promise { 11 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} ORDER BY name`); 12 | return result.rows; 13 | } 14 | 15 | public async findOneById(id: string): Promise { 16 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 17 | return result.rows[0]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/organization/organization.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | organizations: [Organization] 3 | organization(id: ID!): Organization 4 | } 5 | 6 | type Mutation { 7 | createOrganization(input: CreateOrganizationInput!): Organization! 8 | updateOrganization(id: ID!, input: UpdateOrganizationInput!): Organization! 9 | } 10 | 11 | type Subscription { 12 | organizationCreated: Organization! 13 | organizationUpdated: Organization! 14 | } 15 | 16 | type Organization { 17 | id: ID! 18 | name: String! 19 | spectrumId: String! 20 | } 21 | 22 | input CreateOrganizationInput { 23 | name: String! 24 | spectrumId: String! 25 | } 26 | 27 | input UpdateOrganizationInput { 28 | name: String 29 | spectrumId: String 30 | } 31 | -------------------------------------------------------------------------------- /src/transaction-detail/dto/create-sold-transaction-detail.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsInt, IsNumber, IsOptional, IsPositive, IsString, IsUUID } from 'class-validator'; 2 | import { CreateSoldTransactionDetailInput } from '../../graphql.schema'; 3 | 4 | export class CreateSoldTransactionDetailDto implements CreateSoldTransactionDetailInput { 5 | @IsUUID('4') 6 | public transactionId!: string; 7 | @IsUUID('4') 8 | public locationId!: string; 9 | @IsNumber() 10 | @IsPositive() 11 | public price!: number; 12 | @IsInt() 13 | @IsPositive() 14 | public quantity!: number; 15 | @IsOptional() 16 | @IsString() 17 | public note?: string | undefined; 18 | @IsOptional() 19 | @IsDate() 20 | public timestamp?: Date; 21 | } 22 | -------------------------------------------------------------------------------- /src/commodity-category/commodity-category.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | commodityCategories: [CommodityCategory] 3 | commodityCategory(id: ID!): CommodityCategory 4 | } 5 | 6 | type Mutation { 7 | createCommodityCategory(input: CreateCommodityCategoryInput!): CommodityCategory! 8 | updateCommodityCategory(id: ID!, input: UpdateCommodityCategoryInput!): CommodityCategory! 9 | } 10 | 11 | type Subscription { 12 | commodityCategoryCreated: CommodityCategory! 13 | commodityCategoryUpdated: CommodityCategory! 14 | } 15 | 16 | type CommodityCategory { 17 | id: ID! 18 | name: String! 19 | } 20 | 21 | input CreateCommodityCategoryInput { 22 | name: String! 23 | } 24 | 25 | input UpdateCommodityCategoryInput { 26 | name: String 27 | } 28 | -------------------------------------------------------------------------------- /src/transaction-detail/dto/create-bought-transaction-detail.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsInt, IsNumber, IsOptional, IsPositive, IsString, IsUUID } from 'class-validator'; 2 | import { CreateBoughtTransactionDetailInput } from '../../graphql.schema'; 3 | 4 | export class CreateBoughtTransactionDetailDto implements CreateBoughtTransactionDetailInput { 5 | @IsUUID('4') 6 | public transactionId!: string; 7 | @IsUUID('4') 8 | public locationId!: string; 9 | @IsNumber() 10 | @IsPositive() 11 | public price!: number; 12 | @IsInt() 13 | @IsPositive() 14 | public quantity!: number; 15 | @IsOptional() 16 | @IsString() 17 | public note?: string | undefined; 18 | @IsOptional() 19 | @IsDate() 20 | public timestamp?: Date; 21 | } 22 | -------------------------------------------------------------------------------- /src/transaction-detail/dto/create-lost-transaction-detail.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsInt, IsNumber, IsOptional, IsPositive, IsString, IsUUID } from 'class-validator'; 2 | import { CreateLostTransactionDetailInput } from '../../graphql.schema'; 3 | 4 | export class CreateLostTransactionDetailDto implements CreateLostTransactionDetailInput { 5 | @IsUUID('4') 6 | public transactionId!: string; 7 | @IsOptional() 8 | @IsUUID('4') 9 | public locationId?: string; 10 | @IsNumber() 11 | @IsPositive() 12 | public price!: number; 13 | @IsInt() 14 | @IsPositive() 15 | public quantity!: number; 16 | @IsOptional() 17 | @IsString() 18 | public note?: string; 19 | @IsOptional() 20 | @IsDate() 21 | public timestamp?: Date; 22 | } 23 | -------------------------------------------------------------------------------- /src/item/ship/ship.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommonModule } from '../../common/common.module'; 3 | import { GameVersionModule } from '../../game-version/game-version.module'; 4 | import { GameVersionService } from '../../game-version/game-version.service'; 5 | import { ManufacturerModule } from '../../manufacturer/manufacturer.module'; 6 | import { ManufacturerService } from '../../manufacturer/manufacturer.service'; 7 | import { ShipResolvers } from './ship.resolvers'; 8 | import { ShipService } from './ship.service'; 9 | 10 | @Module({ 11 | imports: [GameVersionModule, ManufacturerModule, CommonModule], 12 | providers: [ShipService, ShipResolvers, GameVersionService, ManufacturerService] 13 | }) 14 | export class ShipModule {} 15 | -------------------------------------------------------------------------------- /src/organization-member/organization-member.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AccountModule } from '../account/account.module'; 3 | import { AccountService } from '../account/account.service'; 4 | import { OrganizationModule } from '../organization/organization.module'; 5 | import { OrganizationService } from '../organization/organization.service'; 6 | import { OrganizationMemberResolvers } from './organization-member.resolvers'; 7 | import { OrganizationMemberService } from './organization-member.service'; 8 | 9 | @Module({ 10 | imports: [OrganizationModule, AccountModule], 11 | providers: [OrganizationMemberService, OrganizationMemberResolvers, OrganizationService, AccountService] 12 | }) 13 | export class OrganizationMemberModule {} 14 | -------------------------------------------------------------------------------- /src/transaction/transaction.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | transactions: [Transaction] 3 | transaction(id: ID!): Transaction 4 | } 5 | 6 | type Mutation { 7 | createTransaction(input: CreateTransactionInput!): Transaction! 8 | } 9 | 10 | type Subscription { 11 | transactionCreated: Transaction! 12 | } 13 | 14 | type Transaction { 15 | id: ID! 16 | accountId: ID! 17 | account: Account! 18 | commodityId: ID! 19 | commodity: Commodity! 20 | } 21 | 22 | input CreateFirstTransactionDetailInput { 23 | locationId: ID! 24 | price: Float! 25 | quantity: Int! 26 | note: String 27 | timestamp: Date 28 | } 29 | 30 | input CreateTransactionInput { 31 | accountId: ID 32 | commodityId: ID! 33 | transactionDetail: CreateFirstTransactionDetailInput! 34 | } 35 | -------------------------------------------------------------------------------- /src/auth/current-user.ts: -------------------------------------------------------------------------------- 1 | import { Account, Role } from '../graphql.schema'; 2 | 3 | export class CurrentAuthUser implements Partial { 4 | public readonly id: string; 5 | public readonly username: string; 6 | public readonly email: string; 7 | public readonly roles: Role[]; 8 | 9 | public constructor({ id, username, email, roles }: { id: string; username: string; email: string; roles: Role[] }) { 10 | this.id = id; 11 | this.username = username; 12 | this.email = email; 13 | this.roles = roles; 14 | } 15 | 16 | public hasRole(role: Role): boolean { 17 | return this.roles.find((r) => r === role) !== undefined; 18 | } 19 | 20 | public hasAnyRole(roles: Role[]): boolean { 21 | return roles.some((role) => this.hasRole(role)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/location/location.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommonModule } from '../common/common.module'; 3 | import { GameVersionModule } from '../game-version/game-version.module'; 4 | import { GameVersionService } from '../game-version/game-version.service'; 5 | import { LocationTypeModule } from '../location-type/location-type.module'; 6 | import { LocationTypeService } from '../location-type/location-type.service'; 7 | import { LocationResolvers } from './location.resolvers'; 8 | import { LocationService } from './location.service'; 9 | 10 | @Module({ 11 | imports: [GameVersionModule, LocationTypeModule, CommonModule], 12 | providers: [LocationService, LocationResolvers, GameVersionService, LocationTypeService] 13 | }) 14 | export class LocationModule {} 15 | -------------------------------------------------------------------------------- /src/trade/trade.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GameVersionModule } from '../game-version/game-version.module'; 3 | import { GameVersionService } from '../game-version/game-version.service'; 4 | import { ItemModule } from '../item/item.module'; 5 | import { ItemService } from '../item/item.service'; 6 | import { LocationModule } from '../location/location.module'; 7 | import { LocationService } from '../location/location.service'; 8 | import { TradeResolvers } from './trade.resolvers'; 9 | import { TradeService } from './trade.service'; 10 | 11 | @Module({ 12 | imports: [ItemModule, LocationModule, GameVersionModule], 13 | providers: [TradeService, TradeResolvers, ItemService, LocationService, GameVersionService] 14 | }) 15 | export class TradeModule {} 16 | -------------------------------------------------------------------------------- /src/database.service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-duplicates */ 2 | import * as dotenv from 'dotenv'; 3 | import * as pg from 'pg'; 4 | import { Client } from 'pg'; 5 | // @ts-expect-error: No type definitions :( 6 | import * as pgCamelCase from 'pg-camelcase'; 7 | 8 | pgCamelCase.inject(pg); 9 | 10 | dotenv.config(); 11 | 12 | let client: Client; 13 | 14 | const DATABASE_URL: string | undefined = process.env.DATABASE_URL; 15 | 16 | if (DATABASE_URL === undefined) { 17 | client = new Client({ 18 | host: process.env.DB_HOST, 19 | port: +(process.env.DB_PORT ?? 5432), 20 | user: process.env.DB_USER, 21 | password: process.env.DB_PASSWORD, 22 | database: process.env.DB_NAME 23 | }); 24 | } else { 25 | client = new Client(DATABASE_URL); 26 | } 27 | 28 | export { client }; 29 | -------------------------------------------------------------------------------- /src/possession/possession.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | possessions: [Possession] 3 | possession(id: ID!): Possession 4 | } 5 | 6 | type Mutation { 7 | createPossession(input: CreatePossessionInput!): Possession! 8 | } 9 | 10 | type Subscription { 11 | possessionCreated: Possession! 12 | } 13 | 14 | enum PurchaseCurrency { 15 | REAL_MONEY 16 | A_UEC 17 | REC 18 | UEC 19 | } 20 | 21 | type Possession { 22 | id: ID! 23 | accountId: ID! 24 | account: Account! 25 | itemId: ID! 26 | item: Item! 27 | purchasePrice: Float! 28 | purchaseCurrency: PurchaseCurrency! 29 | purchaseDate: Date 30 | } 31 | 32 | input CreatePossessionInput { 33 | accountId: ID 34 | itemId: ID! 35 | purchasePrice: Float! 36 | purchaseCurrency: PurchaseCurrency! 37 | purchaseDate: Date 38 | } 39 | -------------------------------------------------------------------------------- /src/item/ship/dto/update-ship.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsInt, IsNotEmpty, IsOptional, IsPositive, IsUUID, Length } from 'class-validator'; 2 | import { UpdateShipInput } from '../../../graphql.schema'; 3 | 4 | export class UpdateShipDto implements UpdateShipInput { 5 | @IsOptional() 6 | @Length(3) 7 | public name?: string; 8 | @IsOptional() 9 | @IsUUID('4') 10 | public inGameSinceVersionId?: string; 11 | @IsOptional() 12 | @IsDate() 13 | public inGameSince?: Date; 14 | @IsOptional() 15 | @IsInt() 16 | @IsPositive() 17 | public scu?: number; 18 | @IsOptional() 19 | @IsUUID('4') 20 | public manufacturerId?: string; 21 | @IsOptional() 22 | @IsNotEmpty() 23 | public focus?: string; 24 | @IsOptional() 25 | @IsInt() 26 | @IsPositive() 27 | public size?: number; 28 | } 29 | -------------------------------------------------------------------------------- /src/transaction-detail/dto/create-transaction-detail.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsInt, IsNumber, IsOptional, IsPositive, IsString, IsUUID } from 'class-validator'; 2 | import { CreateTransactionDetailInput, TransactionDetailType } from '../../graphql.schema'; 3 | 4 | export class CreateTransactionDetailDto implements CreateTransactionDetailInput { 5 | @IsUUID('4') 6 | public transactionId!: string; 7 | public type!: TransactionDetailType; 8 | @IsOptional() 9 | @IsUUID('4') 10 | public locationId?: string | undefined; 11 | @IsNumber() 12 | @IsPositive() 13 | public price!: number; 14 | @IsInt() 15 | @IsPositive() 16 | public quantity!: number; 17 | @IsOptional() 18 | @IsString() 19 | public note?: string | undefined; 20 | @IsOptional() 21 | @IsDate() 22 | public timestamp?: Date; 23 | } 24 | -------------------------------------------------------------------------------- /src/item/item.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | items: [Item] 3 | item(id: ID!): Item 4 | } 5 | 6 | type Mutation { 7 | createItem(input: CreateItemInput!): Item! 8 | } 9 | 10 | type Subscription { 11 | itemCreated: Item! 12 | } 13 | 14 | enum ItemType { 15 | ARMS 16 | ATTACHMENT 17 | COMMODITY 18 | COOLER 19 | GADGET 20 | GUN 21 | HELMET 22 | LEGS 23 | MISSILE 24 | ORDNANCE 25 | POWER_PLANT 26 | QUANTUM_DRIVE 27 | SHIELD_GENERATOR 28 | SHIP 29 | TORSO 30 | TURRET 31 | UNDERSUIT 32 | WEAPON 33 | } 34 | 35 | interface Item { 36 | id: ID! 37 | name: String! 38 | inGameSinceVersionId: ID! 39 | inGameSinceVersion: GameVersion! 40 | inGameSince: Date 41 | type: ItemType! 42 | } 43 | 44 | input CreateItemInput { 45 | name: String! 46 | inGameSinceVersionId: ID! 47 | inGameSince: String 48 | } 49 | -------------------------------------------------------------------------------- /src/account/account.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | accounts: [Account] 3 | account(id: ID!): Account 4 | signIn(username: String!, password: String!): AuthToken 5 | me: Account 6 | } 7 | 8 | type Mutation { 9 | signUp(input: CreateAccountInput!): Account! 10 | } 11 | 12 | type Subscription { 13 | accountSignedUp: Account! 14 | } 15 | 16 | enum Role { 17 | USER 18 | ADVANCED 19 | USERADMIN 20 | ADMIN 21 | } 22 | 23 | type Account { 24 | id: ID! 25 | username: String! 26 | handle: String! 27 | email: String 28 | roles: [Role!]! 29 | mainOrganizationId: ID 30 | mainOrganization: Organization 31 | } 32 | 33 | type AuthToken { 34 | id: ID! 35 | username: String! 36 | roles: [Role!]! 37 | token: String! 38 | } 39 | 40 | input CreateAccountInput { 41 | username: String! 42 | handle: String! 43 | email: String! 44 | } 45 | -------------------------------------------------------------------------------- /src/item/commodity/commodity.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CommodityCategoryModule } from '../../commodity-category/commodity-category.module'; 3 | import { CommodityCategoryService } from '../../commodity-category/commodity-category.service'; 4 | import { CommonModule } from '../../common/common.module'; 5 | import { GameVersionModule } from '../../game-version/game-version.module'; 6 | import { GameVersionService } from '../../game-version/game-version.service'; 7 | import { CommodityResolvers } from './commodity.resolvers'; 8 | import { CommodityService } from './commodity.service'; 9 | 10 | @Module({ 11 | imports: [GameVersionModule, CommodityCategoryModule, CommonModule], 12 | providers: [CommodityService, CommodityResolvers, GameVersionService, CommodityCategoryService] 13 | }) 14 | export class CommodityModule {} 15 | -------------------------------------------------------------------------------- /migrations/1545494052177_item-details.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder, PgType } from 'node-pg-migrate'; 2 | 3 | export const shorthands: undefined = undefined; 4 | 5 | export function up(pgm: MigrationBuilder): void { 6 | pgm.addColumn('item', { 7 | details: { 8 | type: PgType.JSONB, 9 | default: '{}', 10 | notNull: true 11 | } 12 | }); 13 | pgm.dropColumns('item', ['scu', 'focus', 'size', 'max_ammo', 'max_range', 'damage']); 14 | } 15 | 16 | export function down(pgm: MigrationBuilder): void { 17 | pgm.addColumns('item', { 18 | scu: { type: PgType.INTEGER }, 19 | focus: { type: PgType.TEXT }, 20 | size: { type: PgType.SMALLINT }, 21 | max_ammo: { type: PgType.SMALLINT }, 22 | max_range: { type: PgType.INTEGER }, 23 | damage: { type: PgType.NUMERIC } 24 | }); 25 | pgm.dropColumn('item', 'details'); 26 | } 27 | -------------------------------------------------------------------------------- /src/item-price/dto/create-item-price.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsInt, IsNumber, IsOptional, IsPositive, IsUUID } from 'class-validator'; 2 | import { CreateItemPriceInput, ItemPriceType, ItemPriceVisibility } from '../../graphql.schema'; 3 | 4 | export class CreateItemPriceDto implements CreateItemPriceInput { 5 | @IsOptional() 6 | @IsUUID('4') 7 | public scannedById?: string; 8 | @IsUUID('4') 9 | public itemId!: string; 10 | @IsUUID('4') 11 | public locationId!: string; 12 | @IsNumber() 13 | @IsPositive() 14 | public price!: number; 15 | @IsInt() 16 | @IsPositive() 17 | public quantity!: number; 18 | @IsOptional() 19 | @IsDate() 20 | public scanTime?: Date; 21 | public type!: ItemPriceType; 22 | public visibility?: ItemPriceVisibility; 23 | @IsOptional() 24 | @IsUUID('4') 25 | public scannedInGameVersionId?: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/item/fallback-item/fallback-item.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException } from '@nestjs/common'; 2 | import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; 3 | import { GameVersionService } from '../../game-version/game-version.service'; 4 | import { FallbackItem, GameVersion } from '../../graphql.schema'; 5 | 6 | @Resolver('FallbackItem') 7 | export class FallbackItemResolvers { 8 | public constructor(private readonly gameVersionService: GameVersionService) {} 9 | 10 | @ResolveField() 11 | public async inGameSinceVersion(@Parent() parent: FallbackItem): Promise { 12 | const gameVersion: GameVersion | undefined = await this.gameVersionService.findOneById(parent.inGameSinceVersionId); 13 | if (!gameVersion) { 14 | throw new NotFoundException(`GameVersion with id ${parent.inGameSinceVersionId} not found`); 15 | } 16 | return gameVersion; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/common/scalars/date.scalar.ts: -------------------------------------------------------------------------------- 1 | import { CustomScalar, Scalar } from '@nestjs/graphql'; 2 | import { Kind, ValueNode } from 'graphql'; 3 | import { Maybe } from 'graphql/jsutils/Maybe'; 4 | 5 | @Scalar('Date') 6 | export class DateScalar implements CustomScalar { 7 | public description: string = 'Date custom scalar type'; 8 | 9 | /** value from the client */ 10 | public parseValue(value: string): Date { 11 | return new Date(value); 12 | } 13 | 14 | /** value sent to the client */ 15 | public serialize(value: Date): string { 16 | return value.toISOString(); 17 | } 18 | 19 | // @ts-expect-error: Force number instead of Date 20 | public parseLiteral(valueNode: ValueNode): Maybe { 21 | console.log(valueNode); 22 | if (valueNode.kind === Kind.INT) { 23 | // valueNode is always in string format 24 | return parseInt(valueNode.value, 10); 25 | } 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | node: [12.x, 14.x] 12 | fail-fast: false 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Set node version to ${{ matrix.node }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node }} 22 | 23 | - name: Versions 24 | run: yarn versions 25 | 26 | - name: Install dependencies 27 | run: yarn install --frozen-lockfile 28 | 29 | - name: Lint 30 | run: yarn lint 31 | 32 | - name: Audit dependencies 33 | run: yarn audit --groups dependencies 34 | 35 | - name: Audit peerDependencies 36 | run: yarn audit --groups peerDependencies 37 | 38 | - name: Build 39 | run: yarn build 40 | -------------------------------------------------------------------------------- /src/item/commodity/commodity.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | commodities: [Commodity] 3 | commodity(id: ID!): Commodity 4 | } 5 | 6 | type Mutation { 7 | createCommodity(input: CreateCommodityInput!): Commodity! 8 | updateCommodity(id: ID!, input: UpdateCommodityInput!): Commodity! 9 | } 10 | 11 | type Subscription { 12 | commodityCreated: Commodity! 13 | commodityUpdated: Commodity! 14 | } 15 | 16 | type Commodity implements Item { 17 | id: ID! 18 | name: String! 19 | inGameSinceVersionId: ID! 20 | inGameSinceVersion: GameVersion! 21 | inGameSince: Date 22 | type: ItemType! 23 | commodityCategoryId: ID! 24 | commodityCategory: CommodityCategory! 25 | } 26 | 27 | input CreateCommodityInput { 28 | name: String! 29 | inGameSinceVersionId: ID! 30 | inGameSince: Date 31 | commodityCategoryId: ID! 32 | } 33 | 34 | input UpdateCommodityInput { 35 | name: String 36 | inGameSinceVersionId: ID 37 | inGameSince: Date 38 | commodityCategoryId: ID 39 | } 40 | -------------------------------------------------------------------------------- /src/item-price/dto/update-item-price.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsDate, IsInt, IsNumber, IsOptional, IsPositive, IsUUID } from 'class-validator'; 2 | import { ItemPriceType, ItemPriceVisibility, UpdateItemPriceInput } from '../../graphql.schema'; 3 | 4 | export class UpdateItemPriceDto implements UpdateItemPriceInput { 5 | @IsOptional() 6 | @IsUUID('4') 7 | public scannedById?: string; 8 | @IsOptional() 9 | @IsUUID('4') 10 | public itemId?: string; 11 | @IsOptional() 12 | @IsUUID('4') 13 | public locationId?: string; 14 | @IsOptional() 15 | @IsNumber() 16 | @IsPositive() 17 | public price?: number; 18 | @IsOptional() 19 | @IsInt() 20 | @IsPositive() 21 | public quantity?: number; 22 | @IsOptional() 23 | @IsDate() 24 | public scanTime?: Date; 25 | @IsOptional() 26 | public type?: ItemPriceType; 27 | @IsOptional() 28 | public visibility?: ItemPriceVisibility; 29 | @IsOptional() 30 | @IsUUID('4') 31 | public scannedInGameVersionId?: string; 32 | } 33 | -------------------------------------------------------------------------------- /migrations/1544035943442_possession.ts: -------------------------------------------------------------------------------- 1 | import { ColumnDefinitions, MigrationBuilder, PgLiteral, PgType } from 'node-pg-migrate'; 2 | 3 | export const shorthands: ColumnDefinitions = { 4 | id: { type: PgType.UUID, primaryKey: true, default: new PgLiteral('uuid_generate_v4()') } 5 | }; 6 | 7 | export function up(pgm: MigrationBuilder): void { 8 | pgm.createType('purchase_currency', ['REAL_MONEY', 'A_UEC', 'REC', 'UEC']); 9 | pgm.createTable('possession', { 10 | id: { type: 'id' }, 11 | account_id: { type: PgType.UUID, notNull: true, references: { name: 'account' } }, 12 | item_id: { type: PgType.UUID, notNull: true, references: { name: 'item' } }, 13 | purchase_price: { type: PgType.NUMERIC, notNull: true }, 14 | purchase_currency: { type: 'purchase_currency', notNull: true }, 15 | purchase_date: { type: PgType.DATE } 16 | }); 17 | } 18 | 19 | export function down(pgm: MigrationBuilder): void { 20 | pgm.dropTable('possession'); 21 | pgm.dropType('purchase_currency'); 22 | } 23 | -------------------------------------------------------------------------------- /src/item/ship/ship.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | ships: [Ship] 3 | ship(id: ID!): Ship 4 | } 5 | 6 | type Mutation { 7 | createShip(input: CreateShipInput!): Ship! 8 | updateShip(id: ID!, input: UpdateShipInput!): Ship! 9 | } 10 | 11 | type Subscription { 12 | shipCreated: Ship! 13 | shipUpdated: Ship! 14 | } 15 | 16 | type Ship implements Item { 17 | id: ID! 18 | name: String! 19 | inGameSinceVersionId: ID! 20 | inGameSinceVersion: GameVersion! 21 | inGameSince: Date 22 | type: ItemType! 23 | scu: Int! 24 | manufacturerId: ID! 25 | manufacturer: Manufacturer! 26 | focus: String! 27 | size: Int! 28 | } 29 | 30 | input CreateShipInput { 31 | name: String! 32 | inGameSinceVersionId: ID! 33 | inGameSince: Date 34 | scu: Int! 35 | manufacturerId: ID! 36 | focus: String! 37 | size: Int! 38 | } 39 | 40 | input UpdateShipInput { 41 | name: String 42 | inGameSinceVersionId: ID 43 | inGameSince: Date 44 | scu: Int 45 | manufacturerId: ID 46 | focus: String 47 | size: Int 48 | } 49 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, Logger, ValidationPipe } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import * as dotenv from 'dotenv'; 4 | import { AppModule } from './app.module'; 5 | import { client } from './database.service'; 6 | 7 | dotenv.config(); 8 | 9 | const HOSTNAME: string | undefined = process.env.API_HOSTNAME; 10 | const PORT: number = process.env.API_PORT !== undefined ? +process.env.API_PORT : 3000; 11 | 12 | async function bootstrap(): Promise { 13 | const app: INestApplication = await NestFactory.create(AppModule); 14 | app.useGlobalPipes(new ValidationPipe()); 15 | if (HOSTNAME) { 16 | await app.listen(PORT, HOSTNAME); 17 | } else { 18 | await app.listen(PORT); 19 | } 20 | const url: string = await app.getUrl(); 21 | Logger.log(`Started at ${url}`, 'bootstrap'); 22 | Logger.log(`GraphQL endpoint ${url}/graphql`, 'bootstrap'); 23 | 24 | await client.connect(); 25 | Logger.log('Connected to database', 'bootstrap'); 26 | } 27 | bootstrap(); 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[BUG]' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /src/item-price/item-price.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AccountModule } from '../account/account.module'; 3 | import { AccountService } from '../account/account.service'; 4 | import { CommonModule } from '../common/common.module'; 5 | import { GameVersionModule } from '../game-version/game-version.module'; 6 | import { GameVersionService } from '../game-version/game-version.service'; 7 | import { ItemModule } from '../item/item.module'; 8 | import { ItemService } from '../item/item.service'; 9 | import { LocationModule } from '../location/location.module'; 10 | import { LocationService } from '../location/location.service'; 11 | import { ItemPriceResolvers } from './item-price.resolvers'; 12 | import { ItemPriceService } from './item-price.service'; 13 | 14 | @Module({ 15 | imports: [AccountModule, ItemModule, LocationModule, GameVersionModule, CommonModule], 16 | providers: [ItemPriceService, ItemPriceResolvers, AccountService, ItemService, LocationService, GameVersionService] 17 | }) 18 | export class ItemPriceModule {} 19 | -------------------------------------------------------------------------------- /src/item/item.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { Args, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; 3 | import { Item } from '../graphql.schema'; 4 | import { ItemService } from './item.service'; 5 | 6 | @Resolver('Item') 7 | export class ItemResolvers { 8 | private readonly logger: Logger = new Logger(ItemResolvers.name); 9 | 10 | public constructor(private readonly itemService: ItemService) {} 11 | 12 | @Query() 13 | public async items(): Promise { 14 | return await this.itemService.findAll(); 15 | } 16 | 17 | @Query() 18 | public async item(@Args('id') id: string): Promise { 19 | return await this.itemService.findOneById(id); 20 | } 21 | 22 | @ResolveField('__resolveType') 23 | public resolveType(@Parent() item: Item): string { 24 | switch (item.type) { 25 | case 'COMMODITY': 26 | return 'Commodity'; 27 | case 'SHIP': 28 | return 'Ship'; 29 | } 30 | this.logger.debug(`Found unsupported item type ${item.type}, using fallback`); 31 | return 'FallbackItem'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 Christopher Quadflieg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/location/location.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | locations(searchInput: LocationSearchInput): [Location] 3 | location(id: ID!): Location 4 | } 5 | 6 | type Mutation { 7 | createLocation(input: CreateLocationInput!): Location! 8 | updateLocation(id: ID!, input: UpdateLocationInput!): Location! 9 | } 10 | 11 | type Subscription { 12 | locationCreated: Location! 13 | locationUpdated: Location! 14 | } 15 | 16 | type Location { 17 | id: ID! 18 | name: String! 19 | parentLocationId: ID 20 | parentLocation: Location 21 | typeId: ID! 22 | type: LocationType! 23 | inGameSinceVersionId: ID! 24 | inGameSinceVersion: GameVersion! 25 | inGameSince: Date 26 | canTrade: Boolean! 27 | children: [Location!]! 28 | } 29 | 30 | input CreateLocationInput { 31 | name: String! 32 | parentLocationId: ID 33 | typeId: ID! 34 | inGameSinceVersionId: ID! 35 | inGameSince: Date 36 | canTrade: Boolean 37 | } 38 | 39 | input UpdateLocationInput { 40 | name: String 41 | parentLocationId: ID 42 | typeId: ID 43 | inGameSinceVersionId: ID 44 | inGameSince: Date 45 | canTrade: Boolean 46 | } 47 | 48 | input LocationSearchInput { 49 | canTrade: Boolean 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Node package-lock.json 64 | package-lock.json 65 | 66 | # Output of build 67 | dist 68 | 69 | # Docker 70 | docker-volumes 71 | -------------------------------------------------------------------------------- /src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import * as dotenv from 'dotenv'; 4 | import { ExtractJwt, Strategy, StrategyOptions } from 'passport-jwt'; 5 | import { Account } from '../graphql.schema'; 6 | import { AuthService } from './auth.service'; 7 | import { CurrentAuthUser } from './current-user'; 8 | import { JwtPayload } from './jwt-payload'; 9 | 10 | dotenv.config(); 11 | 12 | @Injectable() 13 | export class JwtStrategy extends PassportStrategy(Strategy) { 14 | public constructor(private readonly authService: AuthService) { 15 | super({ 16 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 17 | secretOrKey: process.env.JWT_SECRET_KEY 18 | } as StrategyOptions); 19 | } 20 | 21 | public async validate(payload: JwtPayload): Promise { 22 | const user: Account | undefined = await this.authService.validateUser(payload); 23 | if (user === undefined) { 24 | throw new UnauthorizedException(); 25 | } 26 | return new CurrentAuthUser({ 27 | id: user.id, 28 | username: user.username, 29 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 30 | email: user.email!, 31 | roles: user.roles 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/item-price/item-price.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | itemPrices: [ItemPrice] 3 | itemPrice(id: ID!): ItemPrice 4 | } 5 | 6 | type Mutation { 7 | createItemPrice(input: CreateItemPriceInput!): ItemPrice! 8 | updateItemPrice(id: ID!, input: UpdateItemPriceInput!): ItemPrice! 9 | deleteItemPrice(id: ID!): ID! 10 | } 11 | 12 | type Subscription { 13 | itemPriceCreated: ItemPrice! 14 | itemPriceUpdated: ItemPrice! 15 | itemPriceDeleted: ID! 16 | } 17 | 18 | enum ItemPriceType { 19 | BUY 20 | SELL 21 | } 22 | 23 | enum ItemPriceVisibility { 24 | PRIVATE 25 | MAIN_ORGANIZATION 26 | MEMBER_ORGANIZATION 27 | PUBLIC 28 | } 29 | 30 | type ItemPrice { 31 | id: ID! 32 | scannedById: ID! 33 | scannedBy: Account! 34 | itemId: ID! 35 | item: Item! 36 | locationId: ID! 37 | location: Location! 38 | price: Float! 39 | quantity: Int! 40 | unitPrice: Float! 41 | scanTime: Date! 42 | type: ItemPriceType! 43 | visibility: ItemPriceVisibility! 44 | scannedInGameVersionId: ID! 45 | scannedInGameVersion: GameVersion! 46 | } 47 | 48 | input CreateItemPriceInput { 49 | scannedById: ID 50 | itemId: ID! 51 | locationId: ID! 52 | price: Float! 53 | quantity: Int! 54 | scanTime: Date 55 | type: ItemPriceType! 56 | visibility: ItemPriceVisibility 57 | scannedInGameVersionId: ID 58 | } 59 | 60 | input UpdateItemPriceInput { 61 | scannedById: ID 62 | itemId: ID 63 | locationId: ID 64 | price: Float 65 | quantity: Int 66 | scanTime: Date 67 | type: ItemPriceType 68 | visibility: ItemPriceVisibility 69 | scannedInGameVersionId: ID 70 | } 71 | -------------------------------------------------------------------------------- /src/possession/possession.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { Possession } from '../graphql.schema'; 5 | import { CreatePossessionDto } from './dto/create-possession.dto'; 6 | 7 | export const TABLENAME: string = 'possession'; 8 | 9 | @Injectable() 10 | export class PossessionService { 11 | private readonly logger: Logger = new Logger(PossessionService.name); 12 | 13 | public async create({ 14 | accountId, 15 | itemId, 16 | purchasePrice, 17 | purchaseCurrency, 18 | purchaseDate 19 | }: CreatePossessionDto): Promise { 20 | const result: QueryResult = await client.query( 21 | `INSERT INTO ${TABLENAME}(account_id, item_id, purchase_price, purchase_currency, purchase_date)` + 22 | ' VALUES ($1::uuid, $2::uuid, $3::numeric, $4::purchase_currency, $5::date) RETURNING *', 23 | [accountId, itemId, purchasePrice, purchaseCurrency, purchaseDate] 24 | ); 25 | const created: Possession = result.rows[0]; 26 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 27 | return created; 28 | } 29 | 30 | public async findAll(): Promise { 31 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME}`); 32 | return result.rows; 33 | } 34 | 35 | public async findOneById(id: string): Promise { 36 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 37 | return result.rows[0]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | sctm-database: 4 | container_name: SCTM_PostgreSQL 5 | image: postgres:11-alpine 6 | ports: 7 | - '5433:5432' 8 | environment: 9 | - POSTGRES_USER=postgres 10 | - POSTGRES_PASSWORD=postgres 11 | - POSTGRES_DB=sctm 12 | volumes: 13 | - ./docker-volumes/postgresql:/var/lib/postgresql 14 | networks: 15 | - sctm-net 16 | 17 | sctm-api: 18 | container_name: SCTM_API 19 | build: . 20 | #restart: always 21 | environment: 22 | - NODE_ENV=production 23 | - DATABASE_URL=postgres://postgres:postgres@sctm-database:5432/sctm 24 | - JWT_SECRET_KEY=secret 25 | - JWT_EXPIRES_IN=3600 26 | - MAIL_SMTP_HOST=smtp.gmail.com 27 | - MAIL_SMTP_PORT=587 28 | - MAIL_AUTH_USER=mygmail@gmail.com 29 | - MAIL_AUTH_PASS=mypassword 30 | - MAIL_DEFAULT_FROM=mygmail@gmail.com 31 | ports: 32 | - '3000:3000' 33 | depends_on: 34 | - 'sctm-database' 35 | networks: 36 | - sctm-net 37 | 38 | # You must first create the frontend image 39 | # You can comment out the frontend service if you only want the API 40 | sctm-frontend: 41 | container_name: SCTM_Frontend 42 | image: sctm-frontend 43 | #restart: always 44 | environment: 45 | - NODE_ENV=production 46 | - VUE_APP_GRAPHQL_HTTP=http://sctm-api:3000/graphql 47 | - VUE_APP_GRAPHQL_WS=ws://sctm-api:3000/graphql 48 | ports: 49 | - '8080:80' 50 | depends_on: 51 | - 'sctm-api' 52 | networks: 53 | - sctm-net 54 | 55 | networks: 56 | sctm-net: 57 | external: true 58 | -------------------------------------------------------------------------------- /src/auth/role.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable, Logger, Type } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { Role } from '../graphql.schema'; 4 | import { CurrentAuthUser } from './current-user'; 5 | 6 | @Injectable() 7 | export class RoleGuard implements CanActivate { 8 | private readonly logger: Logger = new Logger(RoleGuard.name); 9 | 10 | public constructor(private readonly reflector: Reflector) {} 11 | 12 | public canActivate(context: ExecutionContext): boolean { 13 | // eslint-disable-next-line @typescript-eslint/ban-types 14 | const ctxHandler: Function = context.getHandler(); 15 | const ctxClass: Type = context.getClass(); 16 | const functionReferenceName: string = `${ctxClass.name}::${ctxHandler.name}`; 17 | const roles: Role[] | undefined = this.reflector.get('roles', ctxHandler); 18 | if (roles === undefined) { 19 | this.logger.warn(`Attention! Decorator not present at ${functionReferenceName}`); 20 | return false; 21 | } else if (roles.length === 0) { 22 | this.logger.warn(`Attention! You should always provide at least one Role (${functionReferenceName})`); 23 | return false; 24 | } 25 | const user: CurrentAuthUser = context.getArgs()[2].req.user; 26 | const allowed: boolean = user.hasAnyRole(roles); 27 | if (!allowed) { 28 | this.logger.log( 29 | `User with id ${ 30 | user.id 31 | } tried to access ${functionReferenceName} but does not have one of the following roles: ${roles.join(', ')}` 32 | ); 33 | } 34 | return allowed; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # PR Details 2 | 3 | 4 | 5 | ## Description 6 | 7 | 8 | 9 | ## Related Issue 10 | 11 | 12 | 13 | 14 | 15 | 16 | ## Motivation and Context 17 | 18 | 19 | 20 | ## How Has This Been Tested 21 | 22 | 23 | 24 | 25 | 26 | ## Types of changes 27 | 28 | 29 | 30 | - [ ] Docs change / refactoring / dependency upgrade 31 | - [ ] Bug fix (non-breaking change which fixes an issue) 32 | - [ ] New feature (non-breaking change which adds functionality) 33 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 34 | 35 | ## Checklist 36 | 37 | 38 | 39 | 40 | - [ ] My code follows the code style of this project. 41 | - [ ] My change requires a change to the documentation. 42 | - [ ] I have updated the documentation accordingly. 43 | - [ ] I have read the **CONTRIBUTING** document. 44 | - [ ] I have added tests to cover my changes. 45 | - [ ] All new and existing tests passed. 46 | -------------------------------------------------------------------------------- /src/transaction/transaction.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { Transaction, TransactionDetailType } from '../graphql.schema'; 5 | import { TransactionDetailService } from '../transaction-detail/transaction-detail.service'; 6 | import { CreateTransactionDto } from './dto/create-transaction.dto'; 7 | 8 | export const TABLENAME: string = 'transaction'; 9 | 10 | @Injectable() 11 | export class TransactionService { 12 | private readonly logger: Logger = new Logger(TransactionService.name); 13 | public constructor(private readonly transactionDetailService: TransactionDetailService) {} 14 | 15 | public async create({ accountId, commodityId, transactionDetail }: CreateTransactionDto): Promise { 16 | const result: QueryResult = await client.query( 17 | `INSERT INTO ${TABLENAME}(account_id, commodity_id) VALUES ($1::uuid, $2::uuid) RETURNING *`, 18 | [accountId, commodityId] 19 | ); 20 | const createdTransaction: Transaction = result.rows[0]; 21 | this.logger.log(`Created ${TABLENAME} with id ${createdTransaction.id}`); 22 | await this.transactionDetailService.create({ 23 | ...transactionDetail, 24 | transactionId: createdTransaction.id, 25 | type: TransactionDetailType.BOUGHT 26 | }); 27 | // TODO: create an event for the subscription transactionDetailCreated 28 | return createdTransaction; 29 | } 30 | 31 | public async findAll(): Promise { 32 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME}`); 33 | return result.rows; 34 | } 35 | 36 | public async findOneById(id: string): Promise { 37 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 38 | return result.rows[0]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/organization-member/organization-member.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { OrganizationMember } from '../graphql.schema'; 5 | import { JoinOrganizationDto } from './dto/join-organization.dto'; 6 | 7 | export const TABLENAME: string = 'organization_member'; 8 | 9 | @Injectable() 10 | export class OrganizationMemberService { 11 | private readonly logger: Logger = new Logger(OrganizationMemberService.name); 12 | 13 | public async join({ organizationId, accountId, since }: JoinOrganizationDto): Promise { 14 | const result: QueryResult = await client.query( 15 | `INSERT INTO ${TABLENAME}(organization_id, account_id, since)` + 16 | ' VALUES ($1::uuid, $2::uuid, $3::timestamptz) RETURNING *', 17 | [organizationId, accountId, since || null] 18 | ); 19 | const created: OrganizationMember = result.rows[0]; 20 | this.logger.log( 21 | `Created ${TABLENAME} with accountId ${created.accountId} and organizationId ${created.organizationId}` 22 | ); 23 | return created; 24 | } 25 | 26 | public async findAll(): Promise { 27 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME}`); 28 | return result.rows; 29 | } 30 | 31 | public async findAllByOrganizationId(id: string): Promise { 32 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE organization_id = $1::uuid`, [id]); 33 | return result.rows; 34 | } 35 | 36 | public async findOneByOrganizationIdAndAccountId( 37 | organizationId: string, 38 | accountId: string 39 | ): Promise { 40 | const result: QueryResult = await client.query( 41 | `SELECT * FROM ${TABLENAME} WHERE organization_id = $1::uuid AND account_id = $2::uuid`, 42 | [organizationId, accountId] 43 | ); 44 | return result.rows[0]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/transaction-detail/transaction-detail.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | transactionDetails: [TransactionDetail] 3 | transactionDetail(id: ID!): TransactionDetail 4 | } 5 | 6 | type Mutation { 7 | createTransactionDetail(input: CreateTransactionDetailInput!): TransactionDetail! 8 | createBoughtTransactionDetail(input: CreateBoughtTransactionDetailInput!): TransactionDetail! 9 | createSoldTransactionDetail(input: CreateSoldTransactionDetailInput!): TransactionDetail! 10 | createLostTransactionDetail(input: CreateLostTransactionDetailInput!): TransactionDetail! 11 | createLostBasedOnTransactionDetail(input: CreateLostBasedOnTransactionDetailInput!): TransactionDetail! 12 | } 13 | 14 | type Subscription { 15 | transactionDetailCreated: TransactionDetail! 16 | } 17 | 18 | enum TransactionDetailType { 19 | BOUGHT 20 | SOLD 21 | LOST 22 | } 23 | 24 | type TransactionDetail { 25 | id: ID! 26 | transactionId: ID! 27 | transaction: Transaction! 28 | type: TransactionDetailType! 29 | locationId: ID! 30 | location: Location! 31 | price: Float! 32 | quantity: Int! 33 | note: String 34 | timestamp: Date! 35 | } 36 | 37 | input CreateTransactionDetailInput { 38 | transactionId: ID! 39 | type: TransactionDetailType! 40 | locationId: ID 41 | price: Float! 42 | quantity: Int! 43 | note: String 44 | timestamp: Date 45 | } 46 | 47 | input CreateBoughtTransactionDetailInput { 48 | transactionId: ID! 49 | locationId: ID! 50 | price: Float! 51 | quantity: Int! 52 | note: String 53 | timestamp: Date 54 | } 55 | 56 | input CreateSoldTransactionDetailInput { 57 | transactionId: ID! 58 | locationId: ID! 59 | price: Float! 60 | quantity: Int! 61 | note: String 62 | timestamp: Date 63 | } 64 | 65 | input CreateLostTransactionDetailInput { 66 | transactionId: ID! 67 | locationId: ID 68 | price: Float! 69 | quantity: Int! 70 | note: String 71 | timestamp: Date 72 | } 73 | 74 | input CreateLostBasedOnTransactionDetailInput { 75 | transactionDetailId: ID! 76 | locationId: ID 77 | note: String 78 | timestamp: Date 79 | } 80 | -------------------------------------------------------------------------------- /src/transaction/transaction.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { CurrentAuthUser } from '../auth/current-user'; 5 | import { GraphqlAuthGuard } from '../auth/graphql-auth.guard'; 6 | import { HasAnyRole } from '../auth/has-any-role.decorator'; 7 | import { RoleGuard } from '../auth/role.guard'; 8 | import { CurrentUser } from '../auth/user.decorator'; 9 | import { Role, Transaction } from '../graphql.schema'; 10 | import { CreateTransactionDto } from './dto/create-transaction.dto'; 11 | import { TransactionService } from './transaction.service'; 12 | 13 | const pubSub: PubSub = new PubSub(); 14 | 15 | @Resolver('Transaction') 16 | export class TransactionResolvers { 17 | public constructor(private readonly transactionService: TransactionService) {} 18 | 19 | @Query() 20 | @UseGuards(GraphqlAuthGuard) 21 | public async transactions(): Promise { 22 | return await this.transactionService.findAll(); 23 | } 24 | 25 | @Query() 26 | @UseGuards(GraphqlAuthGuard) 27 | public async transaction(@Args('id') id: string): Promise { 28 | return await this.transactionService.findOneById(id); 29 | } 30 | 31 | @Mutation() 32 | @UseGuards(GraphqlAuthGuard, RoleGuard) 33 | @HasAnyRole(Role.USER, Role.ADVANCED, Role.ADMIN) 34 | public async createTransaction( 35 | @Args('input') args: CreateTransactionDto, 36 | @CurrentUser() currentUser: CurrentAuthUser 37 | ): Promise { 38 | const created: Transaction = await this.transactionService.create({ 39 | ...args, 40 | accountId: currentUser.hasRole(Role.ADMIN) && args.accountId !== undefined ? args.accountId : currentUser.id 41 | }); 42 | pubSub.publish('transactionCreated', { transactionCreated: created }); 43 | return created; 44 | } 45 | 46 | @Subscription() 47 | @UseGuards(GraphqlAuthGuard) 48 | public transactionCreated(): AsyncIterator { 49 | return pubSub.asyncIterator('transactionCreated'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /migrations/1544367125527_item-price-visibility-function.ts: -------------------------------------------------------------------------------- 1 | import { ColumnDefinitions, MigrationBuilder, PgLiteral, PgType } from 'node-pg-migrate'; 2 | 3 | export const shorthands: ColumnDefinitions | undefined = undefined; 4 | 5 | export function up(pgm: MigrationBuilder): void { 6 | pgm.createFunction( 7 | 'f_item_price_visible', 8 | [ 9 | { 10 | name: 'p_account_id', 11 | type: PgType.UUID 12 | }, 13 | { 14 | name: 'p_id', 15 | type: PgType.UUID, 16 | default: new PgLiteral('NULL') 17 | } 18 | ], 19 | { 20 | language: 'plpgsql', 21 | returns: 'SETOF item_price' 22 | }, 23 | /*sql*/ `BEGIN 24 | RETURN QUERY 25 | SELECT ip.* FROM item_price ip 26 | WHERE ip.visibility = 'PUBLIC' AND (p_id::uuid IS NULL OR ip.id = p_id::uuid) 27 | UNION 28 | 29 | SELECT ip.* FROM item_price ip 30 | WHERE ip.visibility = 'PRIVATE' AND ip.scanned_by_id = p_account_id::uuid 31 | AND (p_id::uuid IS NULL OR ip.id = p_id::uuid) 32 | 33 | UNION 34 | SELECT ip.* FROM item_price ip 35 | WHERE ip.visibility = 'MAIN_ORGANIZATION' 36 | AND ip.scanned_by_id::text = any(( 37 | SELECT array_agg(om.account_id) 38 | FROM organization_member om 39 | JOIN organization o ON o.id = om.organization_id 40 | JOIN account a ON a.main_organization_id = o.id 41 | WHERE a.id = p_account_id::uuid 42 | GROUP BY om.account_id 43 | )::text[]) 44 | AND (p_id::uuid IS NULL OR ip.id = p_id::uuid) 45 | 46 | UNION 47 | SELECT ip.* FROM item_price ip 48 | WHERE ip.visibility = 'MEMBER_ORGANIZATION' 49 | AND ip.scanned_by_id::text = any(( 50 | SELECT array_agg(om.account_id) 51 | FROM organization_member om 52 | JOIN organization o ON o.id = om.organization_id 53 | JOIN organization_member aom ON o.id = aom.organization_id 54 | WHERE aom.account_id = p_account_id::uuid 55 | GROUP BY om.account_id 56 | )::text[]) 57 | AND (p_id::uuid IS NULL OR ip.id = p_id::uuid); 58 | END;` 59 | ); 60 | } 61 | 62 | export function down(pgm: MigrationBuilder): void { 63 | pgm.dropFunction('f_item_price_visible', [{ type: PgType.UUID }, { type: PgType.UUID }]); 64 | } 65 | -------------------------------------------------------------------------------- /src/possession/possession.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { CurrentAuthUser } from '../auth/current-user'; 5 | import { GraphqlAuthGuard } from '../auth/graphql-auth.guard'; 6 | import { HasAnyRole } from '../auth/has-any-role.decorator'; 7 | import { RoleGuard } from '../auth/role.guard'; 8 | import { CurrentUser } from '../auth/user.decorator'; 9 | import { Possession, Role } from '../graphql.schema'; 10 | import { CreatePossessionDto } from './dto/create-possession.dto'; 11 | import { PossessionService } from './possession.service'; 12 | 13 | const pubSub: PubSub = new PubSub(); 14 | 15 | @Resolver('Possession') 16 | export class PossessionResolvers { 17 | public constructor(private readonly possessionService: PossessionService) {} 18 | 19 | @Query() 20 | @UseGuards(GraphqlAuthGuard) 21 | public async possessions(): Promise { 22 | return await this.possessionService.findAll(); 23 | } 24 | 25 | @Query() 26 | @UseGuards(GraphqlAuthGuard) 27 | public async possession(@Args('id') id: string): Promise { 28 | return await this.possessionService.findOneById(id); 29 | } 30 | 31 | @Mutation() 32 | @UseGuards(GraphqlAuthGuard, RoleGuard) 33 | @HasAnyRole(Role.USER, Role.ADVANCED, Role.ADMIN) 34 | public async createPossession( 35 | @Args('input') args: CreatePossessionDto, 36 | @CurrentUser() currentUser: CurrentAuthUser 37 | ): Promise { 38 | if (!currentUser.hasRole(Role.ADMIN) && args.accountId !== currentUser.id) { 39 | throw new BadRequestException('You do not have permission to add possession to another account'); 40 | } 41 | const created: Possession = await this.possessionService.create(args); 42 | pubSub.publish('possessionCreated', { possessionCreated: created }); 43 | return created; 44 | } 45 | 46 | @Subscription() 47 | @UseGuards(GraphqlAuthGuard) 48 | public possessionCreated(): AsyncIterator { 49 | return pubSub.asyncIterator('possessionCreated'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/location-type/location-type.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { GraphqlAuthGuard } from '../auth/graphql-auth.guard'; 5 | import { HasAnyRole } from '../auth/has-any-role.decorator'; 6 | import { RoleGuard } from '../auth/role.guard'; 7 | import { LocationType, Role } from '../graphql.schema'; 8 | import { CreateLocationTypeDto } from './dto/create-location-type.dto'; 9 | import { LocationTypeService } from './location-type.service'; 10 | 11 | const pubSub: PubSub = new PubSub(); 12 | 13 | @Resolver('LocationType') 14 | export class LocationTypeResolvers { 15 | public constructor(private readonly locationTypeService: LocationTypeService) {} 16 | 17 | @Query() 18 | public async locationTypes(): Promise { 19 | return await this.locationTypeService.findAll(); 20 | } 21 | 22 | @Query() 23 | public async locationType(@Args('id') id: string): Promise { 24 | return await this.locationTypeService.findOneById(id); 25 | } 26 | 27 | @Mutation() 28 | @UseGuards(GraphqlAuthGuard, RoleGuard) 29 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 30 | public async createLocationType(@Args('input') args: CreateLocationTypeDto): Promise { 31 | const created: LocationType = await this.locationTypeService.create(args); 32 | pubSub.publish('locationTypeCreated', { locationTypeCreated: created }); 33 | return created; 34 | } 35 | 36 | @Mutation() 37 | @UseGuards(GraphqlAuthGuard, RoleGuard) 38 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 39 | public async updateLocationType( 40 | @Args('id') id: string, 41 | @Args('input') args: CreateLocationTypeDto 42 | ): Promise { 43 | const updated: LocationType = await this.locationTypeService.update(id, args); 44 | pubSub.publish('locationTypeUpdated', { locationTypeUpdated: updated }); 45 | return updated; 46 | } 47 | 48 | @Subscription() 49 | public locationTypeCreated(): AsyncIterator { 50 | return pubSub.asyncIterator('locationTypeCreated'); 51 | } 52 | 53 | @Subscription() 54 | public locationTypeUpdated(): AsyncIterator { 55 | return pubSub.asyncIterator('locationTypeUpdated'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/manufacturer/manufacturer.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { GraphqlAuthGuard } from '../auth/graphql-auth.guard'; 5 | import { HasAnyRole } from '../auth/has-any-role.decorator'; 6 | import { RoleGuard } from '../auth/role.guard'; 7 | import { Manufacturer, Role } from '../graphql.schema'; 8 | import { CreateManufacturerDto } from './dto/create-manufacturer.dto'; 9 | import { ManufacturerService } from './manufacturer.service'; 10 | 11 | const pubSub: PubSub = new PubSub(); 12 | 13 | @Resolver('Manufacturer') 14 | export class ManufacturerResolvers { 15 | public constructor(private readonly manufacturerService: ManufacturerService) {} 16 | 17 | @Query() 18 | public async manufacturers(): Promise { 19 | return await this.manufacturerService.findAll(); 20 | } 21 | 22 | @Query() 23 | public async manufacturer(@Args('id') id: string): Promise { 24 | return await this.manufacturerService.findOneById(id); 25 | } 26 | 27 | @Mutation() 28 | @UseGuards(GraphqlAuthGuard, RoleGuard) 29 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 30 | public async createManufacturer(@Args('input') args: CreateManufacturerDto): Promise { 31 | const created: Manufacturer = await this.manufacturerService.create(args); 32 | pubSub.publish('manufacturerCreated', { manufacturerCreated: created }); 33 | return created; 34 | } 35 | 36 | @Mutation() 37 | @UseGuards(GraphqlAuthGuard, RoleGuard) 38 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 39 | public async updateManufacturer( 40 | @Args('id') id: string, 41 | @Args('input') args: CreateManufacturerDto 42 | ): Promise { 43 | const updated: Manufacturer = await this.manufacturerService.update(id, args); 44 | pubSub.publish('manufacturerUpdated', { manufacturerUpdated: updated }); 45 | return updated; 46 | } 47 | 48 | @Subscription() 49 | public manufacturerCreated(): AsyncIterator { 50 | return pubSub.asyncIterator('manufacturerCreated'); 51 | } 52 | 53 | @Subscription() 54 | public manufacturerUpdated(): AsyncIterator { 55 | return pubSub.asyncIterator('manufacturerUpdated'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/organization/organization.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { GraphqlAuthGuard } from '../auth/graphql-auth.guard'; 5 | import { HasAnyRole } from '../auth/has-any-role.decorator'; 6 | import { RoleGuard } from '../auth/role.guard'; 7 | import { Organization, Role } from '../graphql.schema'; 8 | import { CreateOrganizationDto } from './dto/create-organization.dto'; 9 | import { OrganizationService } from './organization.service'; 10 | 11 | const pubSub: PubSub = new PubSub(); 12 | 13 | @Resolver('Organization') 14 | export class OrganizationResolvers { 15 | public constructor(private readonly organizationService: OrganizationService) {} 16 | 17 | @Query() 18 | public async organizations(): Promise { 19 | return await this.organizationService.findAll(); 20 | } 21 | 22 | @Query() 23 | public async organization(@Args('id') id: string): Promise { 24 | return await this.organizationService.findOneById(id); 25 | } 26 | 27 | @Mutation() 28 | @UseGuards(GraphqlAuthGuard, RoleGuard) 29 | @HasAnyRole(Role.USER, Role.ADVANCED, Role.ADMIN) 30 | public async createOrganization(@Args('input') args: CreateOrganizationDto): Promise { 31 | const created: Organization = await this.organizationService.create(args); 32 | pubSub.publish('organizationCreated', { organizationCreated: created }); 33 | return created; 34 | } 35 | 36 | @Mutation() 37 | @UseGuards(GraphqlAuthGuard, RoleGuard) 38 | @HasAnyRole(Role.USER, Role.ADVANCED, Role.ADMIN) 39 | public async updateOrganization( 40 | @Args('id') id: string, 41 | @Args('input') args: CreateOrganizationDto 42 | ): Promise { 43 | const updated: Organization = await this.organizationService.update(id, args); 44 | pubSub.publish('organizationUpdated', { organizationUpdated: updated }); 45 | return updated; 46 | } 47 | 48 | @Subscription() 49 | public organizationCreated(): AsyncIterator { 50 | return pubSub.asyncIterator('organizationCreated'); 51 | } 52 | 53 | @Subscription() 54 | public organizationUpdated(): AsyncIterator { 55 | return pubSub.asyncIterator('organizationUpdated'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/game-version/game-version.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { GraphqlAuthGuard } from '../auth/graphql-auth.guard'; 5 | import { HasAnyRole } from '../auth/has-any-role.decorator'; 6 | import { RoleGuard } from '../auth/role.guard'; 7 | import { GameVersion, Role } from '../graphql.schema'; 8 | import { CreateGameVersionDto } from './dto/create-game-version.dto'; 9 | import { UpdateGameVersionDto } from './dto/update-game-version.dto'; 10 | import { GameVersionService } from './game-version.service'; 11 | 12 | const pubSub: PubSub = new PubSub(); 13 | 14 | @Resolver('GameVersion') 15 | export class GameVersionResolvers { 16 | public constructor(private readonly gameVersionService: GameVersionService) {} 17 | 18 | @Query() 19 | public async gameVersions(): Promise { 20 | return await this.gameVersionService.findAll(); 21 | } 22 | 23 | @Query() 24 | public async gameVersion(@Args('id') id: string): Promise { 25 | return await this.gameVersionService.findOneById(id); 26 | } 27 | 28 | @Mutation() 29 | @UseGuards(GraphqlAuthGuard, RoleGuard) 30 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 31 | public async createGameVersion(@Args('input') args: CreateGameVersionDto): Promise { 32 | const created: GameVersion = await this.gameVersionService.create(args); 33 | pubSub.publish('gameVersionCreated', { gameVersionCreated: created }); 34 | return created; 35 | } 36 | 37 | @Mutation() 38 | @UseGuards(GraphqlAuthGuard, RoleGuard) 39 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 40 | public async updateGameVersion( 41 | @Args('id') id: string, 42 | @Args('input') args: UpdateGameVersionDto 43 | ): Promise { 44 | const updated: GameVersion = await this.gameVersionService.update(id, args); 45 | pubSub.publish('gameVersionUpdated', { gameVersionUpdated: updated }); 46 | return updated; 47 | } 48 | 49 | @Subscription() 50 | public gameVersionCreated(): AsyncIterator { 51 | return pubSub.asyncIterator('gameVersionCreated'); 52 | } 53 | 54 | @Subscription() 55 | public gameVersionUpdated(): AsyncIterator { 56 | return pubSub.asyncIterator('gameVersionUpdated'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/location-type/location-type.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NotFoundException } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { LocationType } from '../graphql.schema'; 5 | import { CreateLocationTypeDto } from './dto/create-location-type.dto'; 6 | import { UpdateLocationTypeDto } from './dto/update-location.dto'; 7 | 8 | export const TABLENAME: string = 'location_type'; 9 | 10 | @Injectable() 11 | export class LocationTypeService { 12 | private readonly logger: Logger = new Logger(LocationTypeService.name); 13 | 14 | public async create({ name }: CreateLocationTypeDto): Promise { 15 | const result: QueryResult = await client.query(`INSERT INTO ${TABLENAME}(name) VALUES ($1::text) RETURNING *`, [ 16 | name 17 | ]); 18 | const created: LocationType = result.rows[0]; 19 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 20 | return created; 21 | } 22 | 23 | public async update(id: string, { name }: UpdateLocationTypeDto): Promise { 24 | const updates: any[] = []; 25 | const values: any[] = []; 26 | let updateIndex: number = 2; 27 | if (name !== undefined) { 28 | updates.push(` name = $${updateIndex}::text`); 29 | values.push(name); 30 | updateIndex++; 31 | } 32 | if (updates.length === 0) { 33 | const locationType: LocationType | undefined = await this.findOneById(id); 34 | if (!locationType) { 35 | throw new NotFoundException(`LocationType with id ${id} not found`); 36 | } 37 | return locationType; 38 | } 39 | const result: QueryResult = await client.query( 40 | `UPDATE ${TABLENAME} SET${updates.join()} WHERE id = $1::uuid RETURNING *`, 41 | [id, ...values] 42 | ); 43 | const updated: LocationType = result.rows[0]; 44 | this.logger.log(`Updated ${TABLENAME} with id ${updated.id}`); 45 | return updated; 46 | } 47 | 48 | public async findAll(): Promise { 49 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} ORDER BY name`); 50 | return result.rows; 51 | } 52 | 53 | public async findOneById(id: string): Promise { 54 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 55 | return result.rows[0]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/manufacturer/manufacturer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NotFoundException } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { Manufacturer } from '../graphql.schema'; 5 | import { CreateManufacturerDto } from './dto/create-manufacturer.dto'; 6 | import { UpdateManufacturerDto } from './dto/update-manufacturer.dto'; 7 | 8 | export const TABLENAME: string = 'manufacturer'; 9 | 10 | @Injectable() 11 | export class ManufacturerService { 12 | private readonly logger: Logger = new Logger(ManufacturerService.name); 13 | 14 | public async create({ name }: CreateManufacturerDto): Promise { 15 | const result: QueryResult = await client.query(`INSERT INTO ${TABLENAME}(name) VALUES ($1::text) RETURNING *`, [ 16 | name 17 | ]); 18 | const created: Manufacturer = result.rows[0]; 19 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 20 | return created; 21 | } 22 | 23 | public async update(id: string, { name }: UpdateManufacturerDto): Promise { 24 | const updates: any[] = []; 25 | const values: any[] = []; 26 | let updateIndex: number = 2; 27 | if (name !== undefined) { 28 | updates.push(` name = $${updateIndex}::text`); 29 | values.push(name); 30 | updateIndex++; 31 | } 32 | if (updates.length === 0) { 33 | const manufacturer: Manufacturer | undefined = await this.findOneById(id); 34 | if (!manufacturer) { 35 | throw new NotFoundException(`Manufacturer with id ${id} not found`); 36 | } 37 | return manufacturer; 38 | } 39 | const result: QueryResult = await client.query( 40 | `UPDATE ${TABLENAME} SET${updates.join()} WHERE id = $1::uuid RETURNING *`, 41 | [id, ...values] 42 | ); 43 | const updated: Manufacturer = result.rows[0]; 44 | this.logger.log(`Updated ${TABLENAME} with id ${updated.id}`); 45 | return updated; 46 | } 47 | 48 | public async findAll(): Promise { 49 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} ORDER BY name`); 50 | return result.rows; 51 | } 52 | 53 | public async findOneById(id: string): Promise { 54 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 55 | return result.rows[0]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Star Citizen Trade Market - API 2 | 3 |

4 | Star Citizen Logo 5 |

6 | 7 |

A progressive Node.js API for Star Citizen to track the market with the help of the community.

8 | 9 | ## Frontend 10 | 11 | - Repository 12 | - Website 13 | 14 | ## Discord 15 | 16 | [![Discord Chat](https://img.shields.io/discord/522792182256500766.svg)](https://discord.gg/FxJmUYT) 17 | 18 | ## Why dont you use VerseMate? 19 | 20 | I really like VerseMate! 21 | However, there are a few things that VerseMate (currently) does not support 22 | 23 | This API and the associated frontend has the following advantages: 24 | 25 | - Community-based _the data is not read from the game_ 26 | - API _frontend is completely decoupled from the API_ 27 | - Open Source _everyone can contribute_ 28 | - You can provide item prices to your main organization or make them available to all your organizations 29 | 30 | ## Installation 31 | 32 | ```bash 33 | $ yarn 34 | ``` 35 | 36 | ## Init database 37 | 38 | ```bash 39 | $ yarn migrate up 40 | ``` 41 | 42 | ## Running the app 43 | 44 | ```bash 45 | # development 46 | $ yarn start 47 | 48 | # watch mode 49 | $ yarn start:dev 50 | 51 | # incremental rebuild (webpack) 52 | $ yarn webpack 53 | $ yarn start:hmr 54 | 55 | # production mode 56 | $ yarn start:prod 57 | ``` 58 | 59 | ## Test 60 | 61 | ```bash 62 | # unit tests 63 | $ yarn test 64 | 65 | # e2e tests 66 | $ yarn test:e2e 67 | 68 | # test coverage 69 | $ yarn test:cov 70 | ``` 71 | 72 | ## Local setup with docker 73 | 74 | ```bash 75 | $ docker network create sctm-net 76 | $ docker-compose up 77 | ``` 78 | 79 | Now you can connect into the database via pgadmin localhost:5433 80 | Also you can fetch the api via localhost:3000/graphql 81 | 82 | ### Docker Cleanup 83 | 84 | ```bash 85 | $ docker rm SCTM_API 86 | $ docker rmi star-citizen-trade-market-api_sctm-api 87 | $ docker rm SCTM_PostgreSQL 88 | ``` 89 | 90 | ## License 91 | 92 | [MIT licensed](LICENSE) 93 | -------------------------------------------------------------------------------- /src/commodity-category/commodity-category.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { GraphqlAuthGuard } from '../auth/graphql-auth.guard'; 5 | import { HasAnyRole } from '../auth/has-any-role.decorator'; 6 | import { RoleGuard } from '../auth/role.guard'; 7 | import { CommodityCategory, Role } from '../graphql.schema'; 8 | import { CommodityCategoryService } from './commodity-category.service'; 9 | import { CreateCommodityCategoryDto } from './dto/create-commodity-category.dto'; 10 | 11 | const pubSub: PubSub = new PubSub(); 12 | 13 | @Resolver('CommodityCategory') 14 | export class CommodityCategoryResolvers { 15 | public constructor(private readonly commodityCategoryService: CommodityCategoryService) {} 16 | 17 | @Query() 18 | public async commodityCategories(): Promise { 19 | return await this.commodityCategoryService.findAll(); 20 | } 21 | 22 | @Query() 23 | public async commodityCategory(@Args('id') id: string): Promise { 24 | return await this.commodityCategoryService.findOneById(id); 25 | } 26 | 27 | @Mutation() 28 | @UseGuards(GraphqlAuthGuard, RoleGuard) 29 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 30 | public async createCommodityCategory(@Args('input') args: CreateCommodityCategoryDto): Promise { 31 | const created: CommodityCategory = await this.commodityCategoryService.create(args); 32 | pubSub.publish('commodityCategoryCreated', { commodityCategoryCreated: created }); 33 | return created; 34 | } 35 | 36 | @Mutation() 37 | @UseGuards(GraphqlAuthGuard, RoleGuard) 38 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 39 | public async updateCommodityCategory( 40 | @Args('id') id: string, 41 | @Args('input') args: CreateCommodityCategoryDto 42 | ): Promise { 43 | const updated: CommodityCategory = await this.commodityCategoryService.update(id, args); 44 | pubSub.publish('commodityCategoryUpdated', { commodityCategoryUpdated: updated }); 45 | return updated; 46 | } 47 | 48 | @Subscription() 49 | public commodityCategoryCreated(): AsyncIterator { 50 | return pubSub.asyncIterator('commodityCategoryCreated'); 51 | } 52 | 53 | @Subscription() 54 | public commodityCategoryUpdated(): AsyncIterator { 55 | return pubSub.asyncIterator('commodityCategoryUpdated'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/commodity-category/commodity-category.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NotFoundException } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { CommodityCategory } from '../graphql.schema'; 5 | import { CreateCommodityCategoryDto } from './dto/create-commodity-category.dto'; 6 | import { UpdateCommodityCategoryDto } from './dto/update-commodity-category.dto'; 7 | 8 | export const TABLENAME: string = 'commodity_category'; 9 | 10 | @Injectable() 11 | export class CommodityCategoryService { 12 | private readonly logger: Logger = new Logger(CommodityCategoryService.name); 13 | 14 | public async create({ name }: CreateCommodityCategoryDto): Promise { 15 | const result: QueryResult = await client.query(`INSERT INTO ${TABLENAME}(name) VALUES ($1::text) RETURNING *`, [ 16 | name 17 | ]); 18 | const created: CommodityCategory = result.rows[0]; 19 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 20 | return created; 21 | } 22 | 23 | public async update(id: string, { name }: UpdateCommodityCategoryDto): Promise { 24 | const updates: any[] = []; 25 | const values: any[] = []; 26 | let updateIndex: number = 2; 27 | if (name !== undefined) { 28 | updates.push(` name = $${updateIndex}::text`); 29 | values.push(name); 30 | updateIndex++; 31 | } 32 | if (updates.length === 0) { 33 | const commodityCategory: CommodityCategory | undefined = await this.findOneById(id); 34 | if (!commodityCategory) { 35 | throw new NotFoundException(`CommodityCategory with id ${id} not found`); 36 | } 37 | return commodityCategory; 38 | } 39 | const result: QueryResult = await client.query( 40 | `UPDATE ${TABLENAME} SET${updates.join()} WHERE id = $1::uuid RETURNING *`, 41 | [id, ...values] 42 | ); 43 | const updated: CommodityCategory = result.rows[0]; 44 | this.logger.log(`Updated ${TABLENAME} with id ${updated.id}`); 45 | return updated; 46 | } 47 | 48 | public async findAll(): Promise { 49 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} ORDER BY name`); 50 | return result.rows; 51 | } 52 | 53 | public async findOneById(id: string): Promise { 54 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 55 | return result.rows[0]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GraphQLModule } from '@nestjs/graphql'; 3 | import { AccessControlModule } from 'nest-access-control'; 4 | import { join } from 'path'; 5 | import { AccountModule } from './account/account.module'; 6 | import { AppController } from './app.controller'; 7 | import { roles } from './app.roles'; 8 | import { AppService } from './app.service'; 9 | import { AuthModule } from './auth/auth.module'; 10 | import { CommodityCategoryModule } from './commodity-category/commodity-category.module'; 11 | import { CommonModule } from './common/common.module'; 12 | import { GameVersionModule } from './game-version/game-version.module'; 13 | import { ItemPriceModule } from './item-price/item-price.module'; 14 | import { FallbackItemModule } from './item/fallback-item/fallback-item.module'; 15 | import { ItemModule } from './item/item.module'; 16 | import { ShipModule } from './item/ship/ship.module'; 17 | import { LocationTypeModule } from './location-type/location-type.module'; 18 | import { LocationModule } from './location/location.module'; 19 | import { ManufacturerModule } from './manufacturer/manufacturer.module'; 20 | import { OrganizationMemberModule } from './organization-member/organization-member.module'; 21 | import { OrganizationModule } from './organization/organization.module'; 22 | import { PossessionModule } from './possession/possession.module'; 23 | import { TradeModule } from './trade/trade.module'; 24 | import { TransactionDetailModule } from './transaction-detail/transaction-detail.module'; 25 | import { TransactionModule } from './transaction/transaction.module'; 26 | 27 | @Module({ 28 | imports: [ 29 | CommonModule, 30 | AccountModule, 31 | AuthModule, 32 | OrganizationModule, 33 | OrganizationMemberModule, 34 | AccessControlModule.forRoles(roles), 35 | CommodityCategoryModule, 36 | GameVersionModule, 37 | ItemModule, 38 | LocationTypeModule, 39 | LocationModule, 40 | ItemPriceModule, 41 | TransactionDetailModule, 42 | TransactionModule, 43 | PossessionModule, 44 | ManufacturerModule, 45 | ShipModule, 46 | FallbackItemModule, 47 | TradeModule, 48 | GraphQLModule.forRoot({ 49 | debug: false, 50 | typePaths: ['./**/*.graphql'], 51 | definitions: { 52 | path: join(process.cwd(), './graphql.schema.ts'), 53 | outputAs: 'interface' 54 | }, 55 | installSubscriptionHandlers: true, 56 | context: ({ req }) => ({ req }) 57 | }) 58 | ], 59 | controllers: [AppController], 60 | providers: [AppService] 61 | }) 62 | export class AppModule {} 63 | -------------------------------------------------------------------------------- /src/organization/organization.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NotFoundException } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { Organization } from '../graphql.schema'; 5 | import { CreateOrganizationDto } from './dto/create-organization.dto'; 6 | import { UpdateOrganizationDto } from './dto/update-organization.dto'; 7 | 8 | export const TABLENAME: string = 'organization'; 9 | 10 | @Injectable() 11 | export class OrganizationService { 12 | private readonly logger: Logger = new Logger(OrganizationService.name); 13 | 14 | public async create({ name, spectrumId }: CreateOrganizationDto): Promise { 15 | const result: QueryResult = await client.query( 16 | `INSERT INTO ${TABLENAME}(name, spectrum_id) VALUES ($1::text, $2::text) RETURNING *`, 17 | [name, spectrumId] 18 | ); 19 | const created: Organization = result.rows[0]; 20 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 21 | return created; 22 | } 23 | 24 | public async update(id: string, { name, spectrumId }: UpdateOrganizationDto): Promise { 25 | const updates: any[] = []; 26 | const values: any[] = []; 27 | let updateIndex: number = 2; 28 | if (name !== undefined) { 29 | updates.push(` name = $${updateIndex}::text`); 30 | values.push(name); 31 | updateIndex++; 32 | } 33 | if (spectrumId !== undefined) { 34 | updates.push(` spectrum_id = $${updateIndex}::text`); 35 | values.push(spectrumId); 36 | updateIndex++; 37 | } 38 | if (updates.length === 0) { 39 | const organization: Organization | undefined = await this.findOneById(id); 40 | if (!organization) { 41 | throw new NotFoundException(`Organization with id ${id} not found`); 42 | } 43 | return organization; 44 | } 45 | const result: QueryResult = await client.query( 46 | `UPDATE ${TABLENAME} SET${updates.join()} WHERE id = $1::uuid RETURNING *`, 47 | [id, ...values] 48 | ); 49 | const updated: Organization = result.rows[0]; 50 | this.logger.log(`Updated ${TABLENAME} with id ${updated.id}`); 51 | return updated; 52 | } 53 | 54 | public async findAll(): Promise { 55 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME}`); 56 | return result.rows; 57 | } 58 | 59 | public async findOneById(id: string): Promise { 60 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 61 | return result.rows[0]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/transaction-detail/transaction-detail.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NotFoundException } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { TransactionDetail, TransactionDetailType } from '../graphql.schema'; 5 | import { CreateLostBasedOnTransactionDetailDto } from './dto/create-lost-based-on-transaction-detail.dto'; 6 | import { CreateTransactionDetailDto } from './dto/create-transaction-detail.dto'; 7 | 8 | export const TABLENAME: string = 'transaction_detail'; 9 | 10 | @Injectable() 11 | export class TransactionDetailService { 12 | private readonly logger: Logger = new Logger(TransactionDetailService.name); 13 | 14 | public async create({ 15 | transactionId, 16 | type, 17 | locationId, 18 | price, 19 | quantity, 20 | note, 21 | timestamp = new Date() 22 | }: CreateTransactionDetailDto): Promise { 23 | const result: QueryResult = await client.query( 24 | `INSERT INTO ${TABLENAME}(transaction_id, type, location_id, price, quantity, note, "timestamp")` + 25 | // eslint-disable-next-line max-len 26 | ' VALUES ($1::uuid, $2::transaction_detail_type, $3::uuid, $4::numeric, $5::bigint, $6::text, $7::timestamptz)' + 27 | ' RETURNING *', 28 | [transactionId, type, locationId, price, quantity, note, timestamp] 29 | ); 30 | const created: TransactionDetail = result.rows[0]; 31 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 32 | return created; 33 | } 34 | 35 | public async createLostBasedOnTransaction({ 36 | transactionDetailId, 37 | locationId, 38 | note, 39 | timestamp 40 | }: CreateLostBasedOnTransactionDetailDto): Promise { 41 | const transactionDetail: TransactionDetail | undefined = await this.findOneById(transactionDetailId); 42 | if (!transactionDetail) { 43 | throw new NotFoundException(`TransactionDetail with id ${transactionDetailId} not found`); 44 | } 45 | return await this.create({ 46 | ...transactionDetail, 47 | locationId, 48 | note, 49 | timestamp, 50 | type: TransactionDetailType.LOST 51 | }); 52 | } 53 | 54 | public async findAll(): Promise { 55 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME}`); 56 | return result.rows; 57 | } 58 | 59 | public async findOneById(id: string): Promise { 60 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 61 | return result.rows[0]; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/game-version/game-version.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NotFoundException } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { GameVersion } from '../graphql.schema'; 5 | import { CreateGameVersionDto } from './dto/create-game-version.dto'; 6 | import { UpdateGameVersionDto } from './dto/update-game-version.dto'; 7 | 8 | export const TABLENAME: string = 'game_version'; 9 | 10 | @Injectable() 11 | export class GameVersionService { 12 | private readonly logger: Logger = new Logger(GameVersionService.name); 13 | 14 | public async create({ identifier, release }: CreateGameVersionDto): Promise { 15 | const result: QueryResult = await client.query( 16 | `INSERT INTO ${TABLENAME}(identifier, release) VALUES ($1::text, $2::timestamptz) RETURNING *`, 17 | [identifier, release] 18 | ); 19 | const created: GameVersion = result.rows[0]; 20 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 21 | return created; 22 | } 23 | 24 | public async update(id: string, { identifier, release }: UpdateGameVersionDto): Promise { 25 | const updates: any[] = []; 26 | const values: any[] = []; 27 | let updateIndex: number = 2; 28 | if (identifier !== undefined) { 29 | updates.push(` identifier = $${updateIndex}::text`); 30 | values.push(identifier); 31 | updateIndex++; 32 | } 33 | if (release !== undefined) { 34 | updates.push(` release = $${updateIndex}::timestamptz`); 35 | values.push(release); 36 | updateIndex++; 37 | } 38 | if (updates.length === 0) { 39 | const gameVersion: GameVersion | undefined = await this.findOneById(id); 40 | if (!gameVersion) { 41 | throw new NotFoundException(`GameVersion with id ${id} not found`); 42 | } 43 | return gameVersion; 44 | } 45 | const result: QueryResult = await client.query( 46 | `UPDATE ${TABLENAME} SET${updates.join()} WHERE id = $1::uuid RETURNING *`, 47 | [id, ...values] 48 | ); 49 | const updated: GameVersion = result.rows[0]; 50 | this.logger.log(`Updated ${TABLENAME} with id ${updated.id}`); 51 | return updated; 52 | } 53 | 54 | public async findAll(): Promise { 55 | const result: QueryResult = await client.query( 56 | `SELECT * FROM ${TABLENAME} ORDER BY release DESC NULLS LAST, identifier DESC` 57 | ); 58 | return result.rows; 59 | } 60 | 61 | public async findOneById(id: string): Promise { 62 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 63 | return result.rows[0]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /migrations/1544800398110_trade-function.ts: -------------------------------------------------------------------------------- 1 | import { ColumnDefinitions, MigrationBuilder, PgType } from 'node-pg-migrate'; 2 | 3 | export const shorthands: ColumnDefinitions | undefined = undefined; 4 | 5 | export function up(pgm: MigrationBuilder): void { 6 | pgm.addColumn('item_price', { 7 | scanned_in_game_version_id: { type: PgType.UUID, references: { name: 'game_version' } } 8 | }); 9 | pgm.sql('UPDATE item_price SET scanned_in_game_version_id = g.id FROM (SELECT id FROM game_version LIMIT 1) g'); 10 | pgm.alterColumn('item_price', 'scanned_in_game_version_id', { notNull: true }); 11 | pgm.createFunction( 12 | 'f_trade', 13 | [ 14 | { 15 | name: 'p_account_id', 16 | type: PgType.UUID 17 | } 18 | ], 19 | { 20 | language: 'plpgsql', 21 | returns: 22 | 'TABLE(buy_id uuid, buy_scanned_by_id uuid, buy_location_id uuid, buy_price numeric,' + 23 | ' buy_quantity bigint, buy_unit_price numeric, buy_scan_time timestamptz,' + 24 | ' buy_visibility item_price_visibility, sell_id uuid, sell_scanned_by_id uuid,' + 25 | ' sell_location_id uuid, sell_price numeric, sell_quantity bigint, sell_unit_price numeric,' + 26 | ' sell_scan_time timestamptz, sell_visibility item_price_visibility, item_id uuid,' + 27 | ' scanned_in_game_version_id uuid, profit numeric, margin numeric)' 28 | }, 29 | /*sql*/ `BEGIN 30 | RETURN QUERY 31 | SELECT 32 | b.id AS buy_id, 33 | b.scanned_by_id AS buy_scanned_by_id, 34 | b.location_id AS buy_location_id, 35 | b.price AS buy_price, 36 | b.quantity AS buy_quantity, 37 | b.price / b.quantity::numeric AS buy_unit_price, 38 | b.scan_time AS buy_scan_time, 39 | b.visibility AS buy_visibility, 40 | s.id AS sell_id, 41 | s.scanned_by_id AS sell_scanned_by_id, 42 | s.location_id AS sell_location_id, 43 | s.price AS sell_price, 44 | s.quantity AS sell_quantity, 45 | s.price / s.quantity::numeric AS sell_unit_price, 46 | s.scan_time AS sell_scan_time, 47 | s.visibility AS sell_visibility, 48 | b.item_id, 49 | b.scanned_in_game_version_id, 50 | s.price / s.quantity::numeric - b.price / b.quantity::numeric AS profit, 51 | s.price / s.quantity::numeric / (b.price / b.quantity::numeric) * 100::numeric - 100::numeric AS margin 52 | FROM f_item_price_visible(p_account_id) b 53 | JOIN f_item_price_visible(p_account_id) s ON s.type = 'SELL'::item_price_type 54 | WHERE b.type = 'BUY'::item_price_type 55 | AND b.id <> s.id AND b.item_id = s.item_id 56 | AND b.scanned_in_game_version_id = s.scanned_in_game_version_id; 57 | END;` 58 | ); 59 | } 60 | 61 | export function down(pgm: MigrationBuilder): void { 62 | pgm.dropFunction('f_trade', [{ type: PgType.UUID }]); 63 | pgm.dropColumn('item_price', 'scanned_in_game_version_id'); 64 | } 65 | -------------------------------------------------------------------------------- /src/trade/trade.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException } from '@nestjs/common'; 2 | import { Args, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; 3 | import { CurrentUser } from '../auth/user.decorator'; 4 | import { GameVersionService } from '../game-version/game-version.service'; 5 | import { Account, GameVersion, Item, Location, Trade, TradeSearchInput } from '../graphql.schema'; 6 | import { ItemService } from '../item/item.service'; 7 | import { LocationService } from '../location/location.service'; 8 | import { TradeService } from './trade.service'; 9 | 10 | @Resolver('Trade') 11 | export class TradeResolvers { 12 | public constructor( 13 | private readonly tradeService: TradeService, 14 | private readonly itemService: ItemService, 15 | private readonly locationService: LocationService, 16 | private readonly gameVersionService: GameVersionService 17 | ) {} 18 | 19 | @Query() 20 | public async trades( 21 | @CurrentUser() currentUser: Account | undefined, 22 | @Args('searchInput') searchInput?: TradeSearchInput 23 | ): Promise { 24 | return this.tradeService.findAllWhere({ 25 | accountId: currentUser !== undefined ? currentUser.id : null, 26 | startLocationId: searchInput !== undefined ? searchInput.startLocationId : undefined, 27 | endLocationId: searchInput !== undefined ? searchInput.endLocationId : undefined, 28 | gameVersionId: searchInput !== undefined ? searchInput.gameVersionId : undefined, 29 | itemIds: searchInput !== undefined ? searchInput.itemIds : undefined 30 | }); 31 | } 32 | 33 | @ResolveField() 34 | public async item(@Parent() parent: Trade): Promise { 35 | const item: Item | undefined = await this.itemService.findOneById(parent.buyItemPrice.itemId); 36 | if (!item) { 37 | throw new NotFoundException(`Item with id ${parent.buyItemPrice.itemId} not found`); 38 | } 39 | return item; 40 | } 41 | 42 | @ResolveField() 43 | public async startLocation(@Parent() parent: Trade): Promise { 44 | const location: Location | undefined = await this.locationService.findOneById(parent.buyItemPrice.locationId); 45 | if (!location) { 46 | throw new NotFoundException(`Location with id ${parent.buyItemPrice.locationId} not found`); 47 | } 48 | return location; 49 | } 50 | 51 | @ResolveField() 52 | public async endLocation(@Parent() parent: Trade): Promise { 53 | const location: Location | undefined = await this.locationService.findOneById(parent.sellItemPrice.locationId); 54 | if (!location) { 55 | throw new NotFoundException(`Location with id ${parent.sellItemPrice.locationId} not found`); 56 | } 57 | return location; 58 | } 59 | 60 | @ResolveField() 61 | public async scannedInGameVersion(@Parent() parent: Trade): Promise { 62 | const gameVersion: GameVersion | undefined = await this.gameVersionService.findOneById( 63 | parent.scannedInGameVersionId 64 | ); 65 | if (!gameVersion) { 66 | throw new NotFoundException(`GameVersion with id ${parent.scannedInGameVersionId} not found`); 67 | } 68 | return gameVersion; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/organization-member/organization-member.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException, UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Parent, Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { AccountService } from '../account/account.service'; 5 | import { GraphqlAuthGuard } from '../auth/graphql-auth.guard'; 6 | import { HasAnyRole } from '../auth/has-any-role.decorator'; 7 | import { RoleGuard } from '../auth/role.guard'; 8 | import { Account, Organization, OrganizationMember, Role } from '../graphql.schema'; 9 | import { OrganizationService } from '../organization/organization.service'; 10 | import { JoinOrganizationDto } from './dto/join-organization.dto'; 11 | import { OrganizationMemberService } from './organization-member.service'; 12 | 13 | const pubSub: PubSub = new PubSub(); 14 | 15 | @Resolver('OrganizationMember') 16 | export class OrganizationMemberResolvers { 17 | public constructor( 18 | private readonly organizationMemberService: OrganizationMemberService, 19 | private readonly organizationService: OrganizationService, 20 | private readonly accountService: AccountService 21 | ) {} 22 | 23 | @Query() 24 | public async organizationMembers(): Promise { 25 | return await this.organizationMemberService.findAll(); 26 | } 27 | 28 | @Query() 29 | public async organizationMember( 30 | @Args('organizationId') organizationId: string, 31 | @Args('accountId') accountId: string 32 | ): Promise { 33 | return await this.organizationMemberService.findOneByOrganizationIdAndAccountId(organizationId, accountId); 34 | } 35 | 36 | @Mutation() 37 | @UseGuards(GraphqlAuthGuard, RoleGuard) 38 | @HasAnyRole(Role.USER, Role.ADVANCED, Role.ADMIN) 39 | public async joinOrganization(@Args('joinOrganizationInput') args: JoinOrganizationDto): Promise { 40 | const joined: OrganizationMember = await this.organizationMemberService.join(args); 41 | pubSub.publish('organizationMemberCreated', { organizationMemberCreated: joined }); 42 | return joined; 43 | } 44 | 45 | @Subscription() 46 | public organizationMemberCreated(): AsyncIterator { 47 | return pubSub.asyncIterator('organizationMemberCreated'); 48 | } 49 | 50 | @ResolveField() 51 | public async organization(@Parent() organizationMember: OrganizationMember): Promise { 52 | const organization: Organization | undefined = await this.organizationService.findOneById( 53 | organizationMember.organizationId 54 | ); 55 | if (!organization) { 56 | throw new NotFoundException(`Organization with id ${organizationMember.organizationId} not found`); 57 | } 58 | return organization; 59 | } 60 | 61 | @ResolveField() 62 | public async account(@Parent() organizationMember: OrganizationMember): Promise { 63 | const account: Account | undefined = await this.accountService.findOneById(organizationMember.accountId); 64 | if (!account) { 65 | throw new NotFoundException(`Account with id ${organizationMember.accountId} not found`); 66 | } 67 | return account; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/item/ship/ship.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException, UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Parent, Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { GraphqlAuthGuard } from '../../auth/graphql-auth.guard'; 5 | import { HasAnyRole } from '../../auth/has-any-role.decorator'; 6 | import { RoleGuard } from '../../auth/role.guard'; 7 | import { GameVersionService } from '../../game-version/game-version.service'; 8 | import { GameVersion, Manufacturer, Role, Ship } from '../../graphql.schema'; 9 | import { ManufacturerService } from '../../manufacturer/manufacturer.service'; 10 | import { CreateShipDto } from './dto/create-ship.dto'; 11 | import { UpdateShipDto } from './dto/update-ship.dto'; 12 | import { ShipService } from './ship.service'; 13 | 14 | const pubSub: PubSub = new PubSub(); 15 | 16 | @Resolver('Ship') 17 | export class ShipResolvers { 18 | public constructor( 19 | private readonly shipService: ShipService, 20 | private readonly gameVersionService: GameVersionService, 21 | private readonly manufacturerService: ManufacturerService 22 | ) {} 23 | 24 | @Query() 25 | public async ships(): Promise { 26 | return await this.shipService.findAll(); 27 | } 28 | 29 | @Query() 30 | public async ship(@Args('id') id: string): Promise { 31 | return await this.shipService.findOneById(id); 32 | } 33 | 34 | @Mutation() 35 | @UseGuards(GraphqlAuthGuard, RoleGuard) 36 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 37 | public async createShip(@Args('input') args: CreateShipDto): Promise { 38 | const created: Ship = await this.shipService.create(args); 39 | pubSub.publish('shipCreated', { shipCreated: created }); 40 | return created; 41 | } 42 | 43 | @Mutation() 44 | @UseGuards(GraphqlAuthGuard, RoleGuard) 45 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 46 | public async updateShip(@Args('id') id: string, @Args('input') args: UpdateShipDto): Promise { 47 | const updated: Ship = await this.shipService.update(id, args); 48 | pubSub.publish('shipUpdated', { shipUpdated: updated }); 49 | return updated; 50 | } 51 | 52 | @Subscription() 53 | public shipCreated(): AsyncIterator { 54 | return pubSub.asyncIterator('shipCreated'); 55 | } 56 | 57 | @Subscription() 58 | public shipUpdated(): AsyncIterator { 59 | return pubSub.asyncIterator('shipUpdated'); 60 | } 61 | 62 | @ResolveField() 63 | public async inGameSinceVersion(@Parent() parent: Ship): Promise { 64 | const gameVersion: GameVersion | undefined = await this.gameVersionService.findOneById(parent.inGameSinceVersionId); 65 | if (!gameVersion) { 66 | throw new NotFoundException(`GameVersion with id ${parent.inGameSinceVersionId} not found`); 67 | } 68 | return gameVersion; 69 | } 70 | 71 | @ResolveField() 72 | public async manufacturer(@Parent() parent: Ship): Promise { 73 | const manufacturer: Manufacturer | undefined = await this.manufacturerService.findOneById(parent.manufacturerId); 74 | if (!manufacturer) { 75 | throw new NotFoundException(`Manufacturer with id ${parent.manufacturerId} not found`); 76 | } 77 | return manufacturer; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "star-citizen-trade-market-api", 3 | "version": "1.0.0-alpha.1", 4 | "description": "Star Citizen Trade Market Backend API", 5 | "author": { 6 | "email": "chrissi92@hotmail.de", 7 | "name": "Christopher Quadflieg" 8 | }, 9 | "license": "MIT", 10 | "private": true, 11 | "scripts": { 12 | "prebuild": "rimraf dist", 13 | "clean": "rimraf dist yarn.lock node_modules", 14 | "build": "nest build", 15 | "format": "prettier --write .", 16 | "start": "nest start", 17 | "start:dev": "nest start --watch", 18 | "start:debug": "nest start --debug --watch", 19 | "start:prod": "node dist/main", 20 | "migrate": "ts-node node_modules/node-pg-migrate/bin/node-pg-migrate", 21 | "lint": "eslint src --ext .ts", 22 | "test": "jest", 23 | "test:watch": "jest --watch", 24 | "test:cov": "jest --coverage", 25 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 26 | "test:e2e": "jest --config ./test/jest-e2e.json" 27 | }, 28 | "dependencies": { 29 | "@nestjs/common": "~7.3.2", 30 | "@nestjs/core": "~7.3.2", 31 | "@nestjs/graphql": "~7.5.5", 32 | "@nestjs/jwt": "~7.1.0", 33 | "@nestjs/passport": "~7.1.0", 34 | "@nestjs/platform-express": "~7.3.2", 35 | "ansi-regex": "~5.0.0", 36 | "apollo-server-express": "~2.16.0", 37 | "bcrypt": "~5.0.0", 38 | "class-transformer": "~0.2.3", 39 | "class-validator": "~0.12.2", 40 | "dotenv": "~8.2.0", 41 | "graphql": "~15.3.0", 42 | "graphql-subscriptions": "~1.1.0", 43 | "graphql-tag-pluck": "~0.8.7", 44 | "graphql-tools": "~6.0.14", 45 | "jest-haste-map": "~26.1.0", 46 | "jest-resolve": "~26.1.0", 47 | "nest-access-control": "~2.0.2", 48 | "node-pg-migrate": "~5.3.0", 49 | "nodemailer": "~6.4.10", 50 | "passport": "~0.4.1", 51 | "passport-jwt": "~4.0.0", 52 | "pg": "~8.3.0", 53 | "pg-camelcase": "~0.0.3", 54 | "postgres-array": "~2.0.0", 55 | "reflect-metadata": "~0.1.13", 56 | "rimraf": "~3.0.2", 57 | "rxjs": "~6.6.0" 58 | }, 59 | "devDependencies": { 60 | "@nestjs/cli": "~7.4.1", 61 | "@nestjs/schematics": "~7.0.1", 62 | "@nestjs/testing": "~7.3.2", 63 | "@types/bcrypt": "~3.0.0", 64 | "@types/express": "~4.17.7", 65 | "@types/jest": "~26.0.4", 66 | "@types/node": "~14.0.23", 67 | "@types/nodemailer": "~6.4.0", 68 | "@types/passport-jwt": "~3.0.3", 69 | "@types/supertest": "~2.0.10", 70 | "@typescript-eslint/eslint-plugin": "~3.6.1", 71 | "@typescript-eslint/parser": "~3.6.1", 72 | "eslint": "~7.4.0", 73 | "eslint-config-prettier": "~6.11.0", 74 | "eslint-import-resolver-typescript": "~2.0.0", 75 | "eslint-plugin-import": "~2.22.0", 76 | "eslint-plugin-prettier": "~3.1.4", 77 | "jest": "~26.1.0", 78 | "prettier": "2.0.5", 79 | "supertest": "~4.0.2", 80 | "ts-jest": "~26.1.3", 81 | "ts-loader": "~8.0.1", 82 | "ts-node": "~8.10.2", 83 | "tsconfig-paths": "~3.9.0", 84 | "typescript": "~3.9.7" 85 | }, 86 | "jest": { 87 | "moduleFileExtensions": [ 88 | "js", 89 | "json", 90 | "ts" 91 | ], 92 | "rootDir": "src", 93 | "testRegex": ".spec.ts$", 94 | "transform": { 95 | "~.+\\.(t|j)s$": "ts-jest" 96 | }, 97 | "coverageDirectory": "../coverage", 98 | "testEnvironment": "node" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/item/commodity/commodity.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException, UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Parent, Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { GraphqlAuthGuard } from '../../auth/graphql-auth.guard'; 5 | import { HasAnyRole } from '../../auth/has-any-role.decorator'; 6 | import { RoleGuard } from '../../auth/role.guard'; 7 | import { CommodityCategoryService } from '../../commodity-category/commodity-category.service'; 8 | import { GameVersionService } from '../../game-version/game-version.service'; 9 | import { Commodity, CommodityCategory, GameVersion, Role } from '../../graphql.schema'; 10 | import { CommodityService } from './commodity.service'; 11 | import { CreateCommodityDto } from './dto/create-commodity.dto'; 12 | import { UpdateCommodityDto } from './dto/update-commodity.dto'; 13 | 14 | const pubSub: PubSub = new PubSub(); 15 | 16 | @Resolver('Commodity') 17 | export class CommodityResolvers { 18 | public constructor( 19 | private readonly commodityService: CommodityService, 20 | private readonly gameVersionService: GameVersionService, 21 | private readonly commodityCategoryService: CommodityCategoryService 22 | ) {} 23 | 24 | @Query() 25 | public async commodities(): Promise { 26 | return await this.commodityService.findAll(); 27 | } 28 | 29 | @Query() 30 | public async commodity(@Args('id') id: string): Promise { 31 | return await this.commodityService.findOneById(id); 32 | } 33 | 34 | @Mutation() 35 | @UseGuards(GraphqlAuthGuard, RoleGuard) 36 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 37 | public async createCommodity(@Args('input') args: CreateCommodityDto): Promise { 38 | const created: Commodity = await this.commodityService.create(args); 39 | pubSub.publish('commodityCreated', { commodityCreated: created }); 40 | return created; 41 | } 42 | 43 | @Mutation() 44 | @UseGuards(GraphqlAuthGuard, RoleGuard) 45 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 46 | public async updateCommodity(@Args('id') id: string, @Args('input') args: UpdateCommodityDto): Promise { 47 | const updated: Commodity = await this.commodityService.update(id, args); 48 | pubSub.publish('commodityUpdated', { commodityUpdated: updated }); 49 | return updated; 50 | } 51 | 52 | @Subscription() 53 | public commodityCreated(): AsyncIterator { 54 | return pubSub.asyncIterator('commodityCreated'); 55 | } 56 | 57 | @Subscription() 58 | public commodityUpdated(): AsyncIterator { 59 | return pubSub.asyncIterator('commodityUpdated'); 60 | } 61 | 62 | @ResolveField() 63 | public async inGameSinceVersion(@Parent() parent: Commodity): Promise { 64 | const gameVersion: GameVersion | undefined = await this.gameVersionService.findOneById(parent.inGameSinceVersionId); 65 | if (!gameVersion) { 66 | throw new NotFoundException(`GameVersion with id ${parent.inGameSinceVersionId} not found`); 67 | } 68 | return gameVersion; 69 | } 70 | 71 | @ResolveField() 72 | public async commodityCategory(@Parent() parent: Commodity): Promise { 73 | const commodityCategory: CommodityCategory | undefined = await this.commodityCategoryService.findOneById( 74 | parent.commodityCategoryId 75 | ); 76 | if (!commodityCategory) { 77 | throw new NotFoundException(`CommodityCategory with id ${parent.commodityCategoryId} not found`); 78 | } 79 | return commodityCategory; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project owner at chrissi92@hotmail.de. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /src/account/account.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException, UnauthorizedException, UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Parent, Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { CurrentAuthUser } from '../auth/current-user'; 5 | import { GraphqlAuthGuard } from '../auth/graphql-auth.guard'; 6 | import { HasAnyRole } from '../auth/has-any-role.decorator'; 7 | import { RoleGuard } from '../auth/role.guard'; 8 | import { CurrentUser } from '../auth/user.decorator'; 9 | import { Account, AuthToken, Organization, Role } from '../graphql.schema'; 10 | import { OrganizationService } from '../organization/organization.service'; 11 | import { AccountService } from './account.service'; 12 | import { CreateAccountDto } from './dto/create-account.dto'; 13 | 14 | const pubSub: PubSub = new PubSub(); 15 | 16 | @Resolver('Account') 17 | export class AccountResolvers { 18 | public constructor( 19 | private readonly accountService: AccountService, 20 | private readonly organizationService: OrganizationService 21 | ) {} 22 | 23 | @Query() 24 | public async accounts(): Promise { 25 | return await this.accountService.findAll(); 26 | } 27 | 28 | @Query() 29 | public async account(@Args('id') id: string): Promise { 30 | return await this.accountService.findOneById(id); 31 | } 32 | 33 | @Mutation() 34 | public async signUp(@Args('input') args: CreateAccountDto): Promise { 35 | const created: Account = await this.accountService.signUp(args); 36 | pubSub.publish('accountSignedUp', { accountSignedUp: created }); 37 | return created; 38 | } 39 | 40 | @Query() 41 | public async signIn(@Args('username') username: string, @Args('password') password: string): Promise { 42 | return await this.accountService.signIn(username, password); 43 | } 44 | 45 | @Query() 46 | @UseGuards(GraphqlAuthGuard) 47 | public async me(@CurrentUser() currentUser: CurrentAuthUser): Promise { 48 | const account: Account | undefined = await this.accountService.findOneById(currentUser.id); 49 | if (!account) { 50 | throw new NotFoundException(`Account with id ${currentUser.id} not found`); 51 | } 52 | return account; 53 | } 54 | 55 | @Subscription() 56 | @UseGuards(GraphqlAuthGuard, RoleGuard) 57 | @HasAnyRole(Role.USERADMIN, Role.ADMIN) 58 | public accountSignedUp(): AsyncIterator { 59 | return pubSub.asyncIterator('accountSignedUp'); 60 | } 61 | 62 | @ResolveField() 63 | @UseGuards(GraphqlAuthGuard, RoleGuard) 64 | @HasAnyRole(Role.USER, Role.USERADMIN, Role.ADMIN) 65 | public email(@Parent() parent: Account, @CurrentUser() currentUser: CurrentAuthUser): string { 66 | if (parent.id === currentUser.id) { 67 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 68 | return parent.email!; 69 | } 70 | if (currentUser.hasAnyRole([Role.USERADMIN, Role.ADMIN])) { 71 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 72 | return parent.email!; 73 | } 74 | throw new UnauthorizedException(); 75 | } 76 | 77 | @ResolveField() 78 | public async mainOrganization(@Parent() parent: Account): Promise { 79 | if (parent.mainOrganizationId) { 80 | const organization: Organization | undefined = await this.organizationService.findOneById( 81 | parent.mainOrganizationId 82 | ); 83 | if (!organization) { 84 | throw new NotFoundException(`Organization with id ${parent.mainOrganizationId} not found`); 85 | } 86 | return organization; 87 | } 88 | return null; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/item/commodity/commodity.service.ts: -------------------------------------------------------------------------------- 1 | import { ConflictException, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../../database.service'; 4 | import { Commodity } from '../../graphql.schema'; 5 | import { TABLENAME } from '../item.service'; 6 | import { CreateCommodityDto } from './dto/create-commodity.dto'; 7 | import { UpdateCommodityDto } from './dto/update-commodity.dto'; 8 | 9 | @Injectable() 10 | export class CommodityService { 11 | private readonly logger: Logger = new Logger(CommodityService.name); 12 | 13 | public async create({ 14 | name, 15 | commodityCategoryId, 16 | inGameSinceVersionId, 17 | inGameSince = new Date() 18 | }: CreateCommodityDto): Promise { 19 | let result: QueryResult; 20 | try { 21 | result = await client.query( 22 | `INSERT INTO ${TABLENAME}(name, commodity_category_id, in_game_since_version_id, in_game_since, type)` + 23 | " VALUES ($1::text, $2::uuid, $3::uuid, $4::timestamptz, 'COMMODITY') RETURNING *", 24 | [name, commodityCategoryId, inGameSinceVersionId, inGameSince] 25 | ); 26 | } catch (error) { 27 | this.logger.error(error); 28 | switch (error.constraint) { 29 | case 'item_name_key': 30 | throw new ConflictException(`Commodity with name ${name} already exist`); 31 | } 32 | throw new InternalServerErrorException(); 33 | } 34 | const created: Commodity = result.rows[0]; 35 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 36 | return created; 37 | } 38 | 39 | public async update( 40 | id: string, 41 | { name, commodityCategoryId, inGameSinceVersionId, inGameSince }: UpdateCommodityDto 42 | ): Promise { 43 | const updates: any[] = []; 44 | const values: any[] = []; 45 | let updateIndex: number = 2; 46 | if (name !== undefined) { 47 | updates.push(` name = $${updateIndex}::text`); 48 | values.push(name); 49 | updateIndex++; 50 | } 51 | if (commodityCategoryId !== undefined) { 52 | updates.push(` commodity_category_id = $${updateIndex}::uuid`); 53 | values.push(commodityCategoryId); 54 | updateIndex++; 55 | } 56 | if (inGameSinceVersionId !== undefined) { 57 | updates.push(` in_game_since_version_id = $${updateIndex}::uuid`); 58 | values.push(inGameSinceVersionId); 59 | updateIndex++; 60 | } 61 | if (inGameSince !== undefined) { 62 | updates.push(` in_game_since = $${updateIndex}::timestamptz`); 63 | values.push(inGameSince); 64 | updateIndex++; 65 | } 66 | if (updates.length === 0) { 67 | const commodity: Commodity | undefined = await this.findOneById(id); 68 | if (!commodity) { 69 | throw new NotFoundException(`Commodity with id ${id} not found`); 70 | } 71 | return commodity; 72 | } 73 | const result: QueryResult = await client.query( 74 | `UPDATE ${TABLENAME} SET${updates.join()} WHERE id = $1::uuid RETURNING *`, 75 | [id, ...values] 76 | ); 77 | const updated: Commodity = result.rows[0]; 78 | this.logger.log(`Updated ${TABLENAME} with id ${updated.id}`); 79 | return updated; 80 | } 81 | 82 | public async findAll(): Promise { 83 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE type = 'COMMODITY' ORDER BY name`); 84 | return result.rows; 85 | } 86 | 87 | public async findOneById(id: string): Promise { 88 | const result: QueryResult = await client.query( 89 | `SELECT * FROM ${TABLENAME} WHERE id = $1::uuid AND type = 'COMMODITY'`, 90 | [id] 91 | ); 92 | return result.rows[0]; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/item/ship/ship.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NotFoundException } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../../database.service'; 4 | import { Ship } from '../../graphql.schema'; 5 | import { TABLENAME } from '../item.service'; 6 | import { CreateShipDto } from './dto/create-ship.dto'; 7 | import { UpdateShipDto } from './dto/update-ship.dto'; 8 | 9 | @Injectable() 10 | export class ShipService { 11 | private readonly logger: Logger = new Logger(ShipService.name); 12 | 13 | public async create({ 14 | name, 15 | inGameSinceVersionId, 16 | inGameSince, 17 | scu, 18 | manufacturerId, 19 | focus, 20 | size 21 | }: CreateShipDto): Promise { 22 | const result: QueryResult = await client.query( 23 | `INSERT INTO ${TABLENAME}(name, in_game_since_version_id, in_game_since, type, manufacturer_id, details)` + 24 | " VALUES ($1::text, $2::uuid, $3::timestamptz, 'SHIP', $4::uuid, $5::jsonb) RETURNING *", 25 | [ 26 | name, 27 | inGameSinceVersionId, 28 | inGameSince, 29 | manufacturerId, 30 | { 31 | scu, 32 | focus, 33 | size 34 | } 35 | ] 36 | ); 37 | const created: Ship = result.rows[0]; 38 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 39 | return this.mapDetails(created); 40 | } 41 | 42 | public async update( 43 | id: string, 44 | { name, focus, inGameSince, inGameSinceVersionId, manufacturerId, scu, size }: UpdateShipDto 45 | ): Promise { 46 | const updates: any[] = []; 47 | const values: any[] = []; 48 | let updateIndex: number = 2; 49 | if (name !== undefined) { 50 | updates.push(` name = $${updateIndex}::text`); 51 | values.push(name); 52 | updateIndex++; 53 | } 54 | if (inGameSinceVersionId !== undefined) { 55 | updates.push(` in_game_since_version_id = $${updateIndex}::uuid`); 56 | values.push(inGameSinceVersionId); 57 | updateIndex++; 58 | } 59 | if (inGameSince !== undefined) { 60 | updates.push(` in_game_since = $${updateIndex}::timestamptz`); 61 | values.push(inGameSince); 62 | updateIndex++; 63 | } 64 | if (manufacturerId !== undefined) { 65 | updates.push(` manufacturer_id = $${updateIndex}::uuid`); 66 | values.push(manufacturerId); 67 | updateIndex++; 68 | } 69 | if (scu !== undefined || focus !== undefined || size !== undefined) { 70 | updates.push(` details = details || $${updateIndex}::jsonb`); 71 | values.push({ scu, focus, size }); 72 | updateIndex++; 73 | } 74 | if (updates.length === 0) { 75 | const ship: Ship | undefined = await this.findOneById(id); 76 | if (!ship) { 77 | throw new NotFoundException(`Ship with id ${id} not found`); 78 | } 79 | return ship; 80 | } 81 | const result: QueryResult = await client.query( 82 | `UPDATE ${TABLENAME} SET${updates.join()} WHERE id = $1::uuid RETURNING *`, 83 | [id, ...values] 84 | ); 85 | const updated: Ship = result.rows[0]; 86 | this.logger.log(`Updated ${TABLENAME} with id ${updated.id}`); 87 | return this.mapDetails(updated); 88 | } 89 | 90 | public async findAll(): Promise { 91 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE type = 'SHIP' ORDER BY name`); 92 | return result.rows.map(this.mapDetails); 93 | } 94 | 95 | public async findOneById(id: string): Promise { 96 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid AND type = 'SHIP'`, [ 97 | id 98 | ]); 99 | return this.mapDetails(result.rows[0]); 100 | } 101 | 102 | private mapDetails(ship: Ship): Ship { 103 | for (const key of ['focus', 'scu', 'size']) { 104 | // @ts-expect-error: Move values up to root 105 | ship[key] = ship.details[key]; 106 | } 107 | return ship; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/location/location.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException, UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Parent, Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { GraphqlAuthGuard } from '../auth/graphql-auth.guard'; 5 | import { HasAnyRole } from '../auth/has-any-role.decorator'; 6 | import { RoleGuard } from '../auth/role.guard'; 7 | import { GameVersionService } from '../game-version/game-version.service'; 8 | import { GameVersion, Location, LocationSearchInput, LocationType, Role } from '../graphql.schema'; 9 | import { LocationTypeService } from '../location-type/location-type.service'; 10 | import { CreateLocationDto } from './dto/create-location.dto'; 11 | import { UpdateLocationDto } from './dto/update-location.dto'; 12 | import { LocationService } from './location.service'; 13 | 14 | const pubSub: PubSub = new PubSub(); 15 | 16 | @Resolver('Location') 17 | export class LocationResolvers { 18 | public constructor( 19 | private readonly locationService: LocationService, 20 | private readonly locationTypeService: LocationTypeService, 21 | private readonly gameVersionService: GameVersionService 22 | ) {} 23 | 24 | @Query() 25 | public async locations(@Args('searchInput') searchInput?: LocationSearchInput): Promise { 26 | return await this.locationService.findAllWhere({ 27 | canTrade: searchInput !== undefined ? searchInput.canTrade : undefined 28 | }); 29 | } 30 | 31 | @Query() 32 | public async location(@Args('id') id: string): Promise { 33 | return await this.locationService.findOneById(id); 34 | } 35 | 36 | @Mutation() 37 | @UseGuards(GraphqlAuthGuard, RoleGuard) 38 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 39 | public async createLocation(@Args('input') args: CreateLocationDto): Promise { 40 | const created: Location = await this.locationService.create(args); 41 | pubSub.publish('locationCreated', { locationCreated: created }); 42 | return created; 43 | } 44 | 45 | @Mutation() 46 | @UseGuards(GraphqlAuthGuard, RoleGuard) 47 | @HasAnyRole(Role.ADVANCED, Role.ADMIN) 48 | public async updateLocation(@Args('id') id: string, @Args('input') args: UpdateLocationDto): Promise { 49 | const updated: Location = await this.locationService.update(id, args); 50 | pubSub.publish('locationUpdated', { locationUpdated: updated }); 51 | return updated; 52 | } 53 | 54 | @Subscription() 55 | public locationCreated(): AsyncIterator { 56 | return pubSub.asyncIterator('locationCreated'); 57 | } 58 | 59 | @Subscription() 60 | public locationUpdated(): AsyncIterator { 61 | return pubSub.asyncIterator('locationUpdated'); 62 | } 63 | 64 | @ResolveField() 65 | public async parentLocation(@Parent() parent: Location): Promise { 66 | if (parent.parentLocationId) { 67 | const location: Location | undefined = await this.locationService.findOneById(parent.parentLocationId); 68 | if (!location) { 69 | throw new NotFoundException(`Location with id ${parent.parentLocationId} not found`); 70 | } 71 | return location; 72 | } 73 | return null; 74 | } 75 | 76 | @ResolveField() 77 | public async type(@Parent() parent: Location): Promise { 78 | const locationType: LocationType | undefined = await this.locationTypeService.findOneById(parent.typeId); 79 | if (!locationType) { 80 | throw new NotFoundException(`LocationType with id ${parent.typeId} not found`); 81 | } 82 | return locationType; 83 | } 84 | 85 | @ResolveField() 86 | public async inGameSinceVersion(@Parent() parent: Location): Promise { 87 | const gameVersion: GameVersion | undefined = await this.gameVersionService.findOneById(parent.inGameSinceVersionId); 88 | if (!gameVersion) { 89 | throw new NotFoundException(`GameVersion with id ${parent.inGameSinceVersionId} not found`); 90 | } 91 | return gameVersion; 92 | } 93 | 94 | @ResolveField() 95 | public async children(@Parent() parent: Location): Promise { 96 | return await this.locationService.findAllByParentId(parent.id); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/trade/trade.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { ItemPrice, ItemPriceType, ItemPriceVisibility, Trade } from '../graphql.schema'; 5 | 6 | export interface TradeSearchOptions { 7 | accountId: string | null; 8 | startLocationId?: string; 9 | endLocationId?: string; 10 | gameVersionId?: string; 11 | itemIds?: string[]; 12 | } 13 | 14 | interface TradeResult { 15 | buyId: string; 16 | buyScannedById: string; 17 | buyLocationId: string; 18 | buyPrice: number; 19 | buyQuantity: number; 20 | buyUnitPrice: number; 21 | buyScanTime: Date; 22 | buyVisibility: ItemPriceVisibility; 23 | sellId: string; 24 | sellScannedById: string; 25 | sellLocationId: string; 26 | sellPrice: number; 27 | sellQuantity: number; 28 | sellUnitPrice: number; 29 | sellScanTime: Date; 30 | sellVisibility: ItemPriceVisibility; 31 | itemId: string; 32 | profit: number; 33 | margin: number; 34 | scannedInGameVersionId: string; 35 | scanTime: Date; 36 | } 37 | 38 | @Injectable() 39 | export class TradeService { 40 | public async findAllWhere({ 41 | accountId = null, 42 | startLocationId, 43 | endLocationId, 44 | gameVersionId, 45 | itemIds 46 | }: TradeSearchOptions): Promise { 47 | let sql: string = 48 | 'SELECT DISTINCT ON (buy_location_id, sell_location_id, item_id, scanned_in_game_version_id)' + 49 | ' * FROM f_trade($1::uuid)'; 50 | const values: Array = [accountId]; 51 | const clause: string[][] = []; 52 | if (startLocationId !== undefined) { 53 | values.push(startLocationId); 54 | clause.push(['buy_location_id', '=', startLocationId]); 55 | } 56 | if (endLocationId !== undefined) { 57 | values.push(endLocationId); 58 | clause.push(['sell_location_id', '=', endLocationId]); 59 | } 60 | if (gameVersionId !== undefined) { 61 | values.push(gameVersionId); 62 | clause.push(['scanned_in_game_version_id', '=', gameVersionId]); 63 | } 64 | if (clause.length > 0) { 65 | sql += ' WHERE'; 66 | for (let index: number = 0; index < clause.length; index++) { 67 | const element: string[] = clause[index]; 68 | sql += ` ${element[0]} ${element[1]} $${index + 2}::uuid`; 69 | if (index !== clause.length - 1) { 70 | sql += ' AND'; 71 | } 72 | } 73 | } 74 | if (itemIds !== undefined) { 75 | values.push(itemIds); 76 | sql += clause.length === 0 ? ' WHERE' : ' AND'; 77 | sql += ` item_id = ANY($${values.length}::uuid[])`; 78 | } 79 | sql += ' ORDER BY buy_location_id, sell_location_id, item_id, scanned_in_game_version_id, scan_time DESC'; 80 | const result: QueryResult = await client.query(sql, values); 81 | if (result.rowCount === 0) { 82 | return []; 83 | } 84 | return result.rows.map(this.convertTradeResult); 85 | } 86 | 87 | private convertTradeResult(tradeResult: TradeResult): Trade { 88 | return { 89 | buyItemPrice: { 90 | id: tradeResult.buyId, 91 | scannedById: tradeResult.buyScannedById, 92 | itemId: tradeResult.itemId, 93 | locationId: tradeResult.buyLocationId, 94 | price: +tradeResult.buyPrice, 95 | quantity: +tradeResult.buyQuantity, 96 | scanTime: tradeResult.buyScanTime, 97 | type: ItemPriceType.BUY, 98 | visibility: tradeResult.buyVisibility 99 | } as ItemPrice, 100 | sellItemPrice: { 101 | id: tradeResult.sellId, 102 | scannedById: tradeResult.sellScannedById, 103 | itemId: tradeResult.itemId, 104 | locationId: tradeResult.sellLocationId, 105 | price: +tradeResult.sellPrice, 106 | quantity: +tradeResult.sellQuantity, 107 | scanTime: tradeResult.sellScanTime, 108 | type: ItemPriceType.SELL, 109 | visibility: tradeResult.sellVisibility 110 | } as ItemPrice, 111 | profit: tradeResult.profit, 112 | margin: tradeResult.margin, 113 | scanTime: tradeResult.scanTime, 114 | scannedInGameVersionId: tradeResult.scannedInGameVersionId 115 | } as Trade; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /migrations/1545127269223_update-trade-function.ts: -------------------------------------------------------------------------------- 1 | import { ColumnDefinitions, MigrationBuilder, PgType } from 'node-pg-migrate'; 2 | 3 | export const shorthands: ColumnDefinitions | undefined = undefined; 4 | 5 | export function up(pgm: MigrationBuilder): void { 6 | pgm.dropFunction('f_trade', [{ type: PgType.UUID }]); 7 | pgm.createFunction( 8 | 'f_trade', 9 | [ 10 | { 11 | name: 'p_account_id', 12 | type: PgType.UUID 13 | } 14 | ], 15 | { 16 | language: 'plpgsql', 17 | returns: 18 | 'TABLE(buy_id uuid, buy_scanned_by_id uuid, buy_location_id uuid, buy_price numeric,' + 19 | ' buy_quantity bigint, buy_unit_price numeric, buy_scan_time timestamptz,' + 20 | ' buy_visibility item_price_visibility, sell_id uuid, sell_scanned_by_id uuid,' + 21 | ' sell_location_id uuid, sell_price numeric, sell_quantity bigint, sell_unit_price numeric,' + 22 | ' sell_scan_time timestamptz, sell_visibility item_price_visibility, item_id uuid,' + 23 | ' scanned_in_game_version_id uuid, scan_time timestamptz, profit numeric, margin numeric)' 24 | }, 25 | /*sql*/ `BEGIN 26 | RETURN QUERY 27 | SELECT 28 | b.id AS buy_id, 29 | b.scanned_by_id AS buy_scanned_by_id, 30 | b.location_id AS buy_location_id, 31 | b.price AS buy_price, 32 | b.quantity AS buy_quantity, 33 | b.price / b.quantity::numeric AS buy_unit_price, 34 | b.scan_time AS buy_scan_time, 35 | b.visibility AS buy_visibility, 36 | s.id AS sell_id, 37 | s.scanned_by_id AS sell_scanned_by_id, 38 | s.location_id AS sell_location_id, 39 | s.price AS sell_price, 40 | s.quantity AS sell_quantity, 41 | s.price / s.quantity::numeric AS sell_unit_price, 42 | s.scan_time AS sell_scan_time, 43 | s.visibility AS sell_visibility, 44 | b.item_id, 45 | b.scanned_in_game_version_id, 46 | CASE 47 | WHEN b.scan_time > s.scan_time THEN s.scan_time 48 | ELSE b.scan_time 49 | END AS scan_time, 50 | s.price / s.quantity::numeric - b.price / b.quantity::numeric AS profit, 51 | s.price / s.quantity::numeric / (b.price / b.quantity::numeric) * 100::numeric - 100::numeric AS margin 52 | FROM f_item_price_visible(p_account_id) b 53 | JOIN f_item_price_visible(p_account_id) s ON s.type = 'SELL'::item_price_type 54 | WHERE b.type = 'BUY'::item_price_type 55 | AND b.id <> s.id AND b.item_id = s.item_id 56 | AND b.scanned_in_game_version_id = s.scanned_in_game_version_id; 57 | END;` 58 | ); 59 | } 60 | 61 | export function down(pgm: MigrationBuilder): void { 62 | pgm.dropFunction('f_trade', [{ type: PgType.UUID }]); 63 | pgm.createFunction( 64 | 'f_trade', 65 | [ 66 | { 67 | name: 'p_account_id', 68 | type: PgType.UUID 69 | } 70 | ], 71 | { 72 | language: 'plpgsql', 73 | returns: 74 | 'TABLE(buy_id uuid, buy_scanned_by_id uuid, buy_location_id uuid, buy_price numeric,' + 75 | ' buy_quantity bigint, buy_unit_price numeric, buy_scan_time timestamptz,' + 76 | ' buy_visibility item_price_visibility, sell_id uuid, sell_scanned_by_id uuid,' + 77 | ' sell_location_id uuid, sell_price numeric, sell_quantity bigint, sell_unit_price numeric,' + 78 | ' sell_scan_time timestamptz, sell_visibility item_price_visibility, item_id uuid,' + 79 | ' scanned_in_game_version_id uuid, profit numeric, margin numeric)' 80 | }, 81 | /*sql*/ `BEGIN 82 | RETURN QUERY 83 | SELECT 84 | b.id AS buy_id, 85 | b.scanned_by_id AS buy_scanned_by_id, 86 | b.location_id AS buy_location_id, 87 | b.price AS buy_price, 88 | b.quantity AS buy_quantity, 89 | b.price / b.quantity::numeric AS buy_unit_price, 90 | b.scan_time AS buy_scan_time, 91 | b.visibility AS buy_visibility, 92 | s.id AS sell_id, 93 | s.scanned_by_id AS sell_scanned_by_id, 94 | s.location_id AS sell_location_id, 95 | s.price AS sell_price, 96 | s.quantity AS sell_quantity, 97 | s.price / s.quantity::numeric AS sell_unit_price, 98 | s.scan_time AS sell_scan_time, 99 | s.visibility AS sell_visibility, 100 | b.item_id, 101 | b.scanned_in_game_version_id, 102 | s.price / s.quantity::numeric - b.price / b.quantity::numeric AS profit, 103 | s.price / s.quantity::numeric / (b.price / b.quantity::numeric) * 100::numeric - 100::numeric AS margin 104 | FROM f_item_price_visible(p_account_id) b 105 | JOIN f_item_price_visible(p_account_id) s ON s.type = 'SELL'::item_price_type 106 | WHERE b.type = 'BUY'::item_price_type 107 | AND b.id <> s.id AND b.item_id = s.item_id 108 | AND b.scanned_in_game_version_id = s.scanned_in_game_version_id; 109 | END;` 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /src/account/account.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConflictException, 3 | Injectable, 4 | InternalServerErrorException, 5 | Logger, 6 | UnauthorizedException 7 | } from '@nestjs/common'; 8 | import { JwtService } from '@nestjs/jwt'; 9 | import { compare, genSalt, hash } from 'bcrypt'; 10 | import { QueryResult } from 'pg'; 11 | import * as postgresArray from 'postgres-array'; 12 | import { client } from '../database.service'; 13 | import { Account, AuthToken, Role } from '../graphql.schema'; 14 | import { transporter } from '../mail.service'; 15 | import { CreateAccountDto } from './dto/create-account.dto'; 16 | 17 | export const TABLENAME: string = 'account'; 18 | 19 | @Injectable() 20 | export class AccountService { 21 | private readonly logger: Logger = new Logger(AccountService.name); 22 | 23 | private readonly jwtService: JwtService = new JwtService({ 24 | secret: process.env.JWT_SECRET_KEY, 25 | signOptions: { expiresIn: process.env.JWT_EXPIRES_IN } 26 | }); 27 | 28 | private readonly PASSWORD_CHARS: string[] = [...'abcdefhjkmnpqrstuvwxABCDEFHJKMNPQRSTUVWX2345789']; 29 | 30 | public async signUp({ username, handle, email }: CreateAccountDto): Promise { 31 | const salt: string = await genSalt(); 32 | const generatedPassword: string = [...Array(Math.floor(Math.random() * 9) + 12)] 33 | .map( 34 | // tslint:disable-next-line:no-bitwise 35 | () => this.PASSWORD_CHARS[(Math.random() * this.PASSWORD_CHARS.length) | 0] 36 | ) 37 | .join(''); 38 | const encryptedPassword: string = await hash(generatedPassword, salt); 39 | let result: QueryResult; 40 | try { 41 | result = await client.query( 42 | `INSERT INTO ${TABLENAME}(username, handle, email, password)` + 43 | ' VALUES ($1::text, $2::text, $3::text, $4::text) RETURNING *', 44 | [username, handle, email, encryptedPassword] 45 | ); 46 | } catch (error) { 47 | this.logger.error(error); 48 | switch (error.constraint) { 49 | case 'account_username_key': 50 | throw new ConflictException(`Username ${username} is already in use`); 51 | case 'account_handle_key': 52 | throw new ConflictException(`Star Citizen Handle ${handle} is already taken by another user`); 53 | case 'account_email_key': 54 | throw new ConflictException(`Email ${email} is already in use`); 55 | } 56 | throw new InternalServerErrorException(); 57 | } 58 | 59 | transporter.sendMail({ 60 | to: email, 61 | subject: 'Registration in Star Citizen Trademarket', 62 | text: `Star Citizen Trademarket\nUsername: ${username}\nPassword: ${generatedPassword}` 63 | }); 64 | // TODO: remove account if email cant be delivered 65 | 66 | const created: Account = result.rows[0]; 67 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 68 | return created; 69 | } 70 | 71 | public async signIn(username: string, password: string): Promise { 72 | const account: Account | undefined = await this.findOneByUsername(username); 73 | // TODO: throw other exception 74 | if (account === undefined) { 75 | throw new UnauthorizedException(); 76 | } 77 | const match: boolean = await compare(password, (account as any).password); 78 | if (!match) { 79 | throw new UnauthorizedException(); 80 | } 81 | 82 | const token: string = this.jwtService.sign({ username: account.username }); 83 | 84 | return { 85 | id: account.id, 86 | username: account.username, 87 | roles: this.transformRoles(account).roles, 88 | token 89 | }; 90 | } 91 | 92 | public async findAll(): Promise { 93 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME}`); 94 | return result.rows.map((row) => this.transformRoles(row)); 95 | } 96 | 97 | public async findOneById(id: string): Promise { 98 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 99 | if (result.rowCount === 0) { 100 | return; 101 | } 102 | return this.transformRoles(result.rows[0]); 103 | } 104 | 105 | public async findOneByUsername(username: string): Promise { 106 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE username = $1::text`, [username]); 107 | if (result.rowCount === 0) { 108 | return; 109 | } 110 | return this.transformRoles(result.rows[0]); 111 | } 112 | 113 | private transformRoles(account: Account): Account { 114 | const roles: any = account.roles; 115 | if (typeof roles === 'string') { 116 | account.roles = postgresArray.parse(roles) as Role[]; 117 | } 118 | return account; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/location/location.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NotFoundException } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { Location } from '../graphql.schema'; 5 | import { CreateLocationDto } from './dto/create-location.dto'; 6 | import { UpdateLocationDto } from './dto/update-location.dto'; 7 | 8 | export interface LocationSearchOptions { 9 | canTrade?: boolean; 10 | } 11 | 12 | export const TABLENAME: string = 'location'; 13 | 14 | @Injectable() 15 | export class LocationService { 16 | private readonly logger: Logger = new Logger(LocationService.name); 17 | 18 | public async create({ 19 | name, 20 | parentLocationId, 21 | typeId, 22 | inGameSinceVersionId, 23 | inGameSince, 24 | canTrade = false 25 | }: CreateLocationDto): Promise { 26 | const result: QueryResult = await client.query( 27 | // eslint-disable-next-line max-len 28 | `INSERT INTO ${TABLENAME}(name, parent_location_id, type_id, in_game_since_version_id, in_game_since, can_trade)` + 29 | ' VALUES ($1::text, $2::uuid, $3::uuid, $4::uuid, $5::timestamptz, $6::boolean) RETURNING *', 30 | [name, parentLocationId, typeId, inGameSinceVersionId, inGameSince, canTrade] 31 | ); 32 | const created: Location = result.rows[0]; 33 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 34 | return created; 35 | } 36 | 37 | public async update( 38 | id: string, 39 | { name, inGameSince, inGameSinceVersionId, parentLocationId, typeId, canTrade }: UpdateLocationDto 40 | ): Promise { 41 | const updates: any[] = []; 42 | const values: any[] = []; 43 | let updateIndex: number = 2; 44 | if (name !== undefined) { 45 | updates.push(` name = $${updateIndex}::text`); 46 | values.push(name); 47 | updateIndex++; 48 | } 49 | if (inGameSince !== undefined) { 50 | updates.push(` in_game_since = $${updateIndex}::timestamptz`); 51 | values.push(inGameSince); 52 | updateIndex++; 53 | } 54 | if (inGameSinceVersionId !== undefined) { 55 | updates.push(` in_game_since_version_id = $${updateIndex}::uuid`); 56 | values.push(inGameSinceVersionId); 57 | updateIndex++; 58 | } 59 | if (parentLocationId !== undefined) { 60 | updates.push(` parent_location_id = $${updateIndex}::uuid`); 61 | values.push(parentLocationId); 62 | updateIndex++; 63 | } 64 | if (typeId !== undefined) { 65 | updates.push(` type_id = $${updateIndex}::uuid`); 66 | values.push(typeId); 67 | updateIndex++; 68 | } 69 | if (canTrade !== undefined) { 70 | updates.push(` can_trade = $${updateIndex}::boolean`); 71 | values.push(canTrade); 72 | updateIndex++; 73 | } 74 | if (updates.length === 0) { 75 | const location: Location | undefined = await this.findOneById(id); 76 | if (!location) { 77 | throw new NotFoundException(`Location with id ${id} not found`); 78 | } 79 | return location; 80 | } 81 | const result: QueryResult = await client.query( 82 | `UPDATE ${TABLENAME} SET${updates.join()} WHERE id = $1::uuid RETURNING *`, 83 | [id, ...values] 84 | ); 85 | const updated: Location = result.rows[0]; 86 | this.logger.log(`Updated ${TABLENAME} with id ${updated.id}`); 87 | return updated; 88 | } 89 | 90 | public async findAll(): Promise { 91 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} ORDER BY name`); 92 | return result.rows; 93 | } 94 | 95 | public async findAllWhere({ canTrade }: LocationSearchOptions): Promise { 96 | let sql: string = `SELECT * FROM ${TABLENAME}`; 97 | const values: boolean[] = []; 98 | const clause: Array> = []; 99 | if (canTrade !== undefined) { 100 | values.push(canTrade); 101 | clause.push(['can_trade', '=', canTrade]); 102 | } 103 | if (clause.length > 0) { 104 | sql += ' WHERE'; 105 | for (let index: number = 0; index < clause.length; index++) { 106 | const element: Array = clause[index]; 107 | sql += ` ${element[0]} ${element[1]} $${index + 1}::boolean`; 108 | if (index !== clause.length - 1) { 109 | sql += ' AND'; 110 | } 111 | } 112 | } 113 | sql += ' ORDER BY name'; 114 | const result: QueryResult = await client.query(sql, values); 115 | return result.rows; 116 | } 117 | 118 | public async findAllByParentId(parentId: string): Promise { 119 | const result: QueryResult = await client.query( 120 | `SELECT * FROM ${TABLENAME} WHERE parent_location_id = $1::uuid ORDER BY name`, 121 | [parentId] 122 | ); 123 | return result.rows; 124 | } 125 | 126 | public async findOneById(id: string): Promise { 127 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 128 | return result.rows[0]; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'eslint:recommended', 8 | 'plugin:@typescript-eslint/eslint-recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:import/errors', 11 | 'plugin:import/warnings', 12 | 'plugin:import/typescript' 13 | ], 14 | parserOptions: { 15 | parser: '@typescript-eslint/parser', 16 | project: ['./tsconfig.json'] 17 | }, 18 | plugins: ['@typescript-eslint', 'prettier', 'import'], 19 | rules: { 20 | 'comma-dangle': ['error', 'never'], 21 | 'grouped-accessor-pairs': ['warn', 'getBeforeSet'], 22 | 'linebreak-style': ['error', 'unix'], 23 | 'max-classes-per-file': 'error', 24 | 'max-len': ['warn', { code: 120 }], 25 | 'no-case-declarations': 'warn', 26 | 'no-constant-condition': 'error', 27 | 'no-shadow': 'warn', 28 | 'no-unused-expressions': 'error', 29 | 'no-useless-concat': 'error', 30 | 'prefer-template': 'error', 31 | 'quote-props': ['error', 'as-needed'], 32 | quotes: ['error', 'single', { avoidEscape: true }], 33 | 34 | // Cant resolve module.exports = ... 35 | // https://github.com/benmosher/eslint-plugin-import/issues/1145 36 | 'import/default': 'warn', 37 | 38 | 'import/no-unresolved': 'error', 39 | 40 | // Cant resolve types 41 | // https://github.com/benmosher/eslint-plugin-import/issues/1341 42 | 'import/named': 'off', 43 | 44 | // 'sort-imports': 'warn', 45 | // 'import/order': ['warn'], 46 | 47 | 'max-lines': ['warn', 400], 48 | 49 | camelcase: 'warn', 50 | 51 | semi: ['off'], 52 | '@typescript-eslint/semi': ['error'], 53 | indent: ['off', 2], 54 | '@typescript-eslint/indent': ['off', 2], 55 | 56 | '@typescript-eslint/ban-ts-comment': [ 57 | 'warn', 58 | { 59 | 'ts-expect-error': 'allow-with-description' 60 | } 61 | ], 62 | '@typescript-eslint/ban-types': 'warn', 63 | '@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }], 64 | '@typescript-eslint/explicit-member-accessibility': 'error', 65 | '@typescript-eslint/interface-name-prefix': 'off', 66 | // Waiting on https://github.com/typescript-eslint/typescript-eslint/issues/929 67 | '@typescript-eslint/member-ordering': [ 68 | 'warn', 69 | { 70 | default: [ 71 | 'signature', 72 | 73 | 'public-static-field', 74 | 'protected-static-field', 75 | 'private-static-field', 76 | 'static-field', 77 | 78 | 'public-static-method', 79 | 'protected-static-method', 80 | 'private-static-method', 81 | 'static-method', 82 | 83 | 'public-instance-field', 84 | 'protected-instance-field', 85 | 'private-instance-field', 86 | 87 | 'public-abstract-field', 88 | 'protected-abstract-field', 89 | 'private-abstract-field', 90 | 91 | 'public-field', 92 | 'protected-field', 93 | 'private-field', 94 | 'instance-field', 95 | 'abstract-field', 96 | 'field', 97 | 98 | 'constructor', 99 | 100 | 'public-instance-method', 101 | 'protected-instance-method', 102 | 'private-instance-method', 103 | 104 | 'public-abstract-method', 105 | 'protected-abstract-method', 106 | 'private-abstract-method', 107 | 108 | 'public-method', 109 | 'protected-method', 110 | 'private-method', 111 | 112 | 'instance-method', 113 | 'abstract-method', 114 | 115 | 'method' 116 | ] 117 | } 118 | ], 119 | '@typescript-eslint/no-explicit-any': 'off', 120 | '@typescript-eslint/no-inferrable-types': 'off', 121 | '@typescript-eslint/no-non-null-assertion': 'warn', 122 | '@typescript-eslint/no-parameter-properties': 'off', 123 | '@typescript-eslint/no-unused-vars': 'off', 124 | '@typescript-eslint/prefer-readonly': ['warn'], 125 | '@typescript-eslint/prefer-string-starts-ends-with': 'error', 126 | '@typescript-eslint/require-await': 'warn', 127 | '@typescript-eslint/typedef': [ 128 | 'warn', 129 | { 130 | arrowParameter: false, 131 | memberVariableDeclaration: true, 132 | objectDestructuring: false, // Currently in conflict with `arrowParameter: false` 133 | parameter: false, // Should later be set to true 134 | propertyDeclaration: true, 135 | variableDeclaration: true 136 | } 137 | ] 138 | 139 | /* 140 | * https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/ROADMAP.md 141 | * 142 | * Missing TSLint rules: 143 | * - strict-type-predicates 144 | */ 145 | }, 146 | settings: { 147 | 'import/parsers': { 148 | '@typescript-eslint/parser': ['.ts', '.tsx'] 149 | }, 150 | 'import/resolver': { 151 | // use /tsconfig.json 152 | typescript: {} 153 | } 154 | } 155 | }; 156 | -------------------------------------------------------------------------------- /migrations/1543613769405_default-data.ts: -------------------------------------------------------------------------------- 1 | import { MigrationBuilder } from 'node-pg-migrate'; 2 | 3 | export function up(pgm: MigrationBuilder): void { 4 | pgm.sql( 5 | `INSERT INTO public.game_version(identifier) VALUES (\'${[ 6 | '3.0.0-live.695052', 7 | '3.0.1-live.706028', 8 | '3.0.1-ptu.706028', 9 | '3.1.0-live.738964', 10 | '3.1.1-live.740789', 11 | '3.1.1-ptu.741817', 12 | '3.1.2-live.744137', 13 | '3.1.2-ptu.742582', 14 | '3.1.2-ptu.743249', 15 | '3.1.2-ptu.743849', 16 | '3.1.2-ptu.744137', 17 | '3.1.3-live.746975', 18 | '3.1.3-ptu.745418', 19 | '3.1.3-ptu.745900', 20 | '3.1.3-ptu.746531', 21 | '3.1.3-ptu.746975', 22 | '3.1.3-ptu.746976', 23 | '3.1.4-live.75748', 24 | '3.1.4-live.757485', 25 | '3.1.4-ptu.748380', 26 | '3.1.4-ptu.749296', 27 | '3.1.4-ptu.749761', 28 | '3.1.4-ptu.750135', 29 | '3.1.4-ptu.751263', 30 | '3.1.4-ptu.751710', 31 | '3.1.4-ptu.753197', 32 | '3.1.4-ptu.753198', 33 | '3.1.4-ptu.755845', 34 | '3.1.4-ptu.757485', 35 | '3.2.0-live.796019', 36 | '3.2.0-ptu.790942', 37 | '3.2.0-ptu.791185', 38 | '3.2.0-ptu.792285', 39 | '3.2.0-ptu.793226', 40 | '3.2.0-ptu.794284', 41 | '3.2.0-ptu.796019', 42 | '3.2.0.ptu-785795', 43 | '3.2.0.ptu-787025', 44 | '3.2.0.ptu-795153', 45 | '3.2.1-live.834470', 46 | '3.2.1-ptu.808574', 47 | '3.2.1-ptu.819037', 48 | '3.2.1-ptu.820844', 49 | '3.2.1-ptu.823435', 50 | '3.2.1-ptu.827813', 51 | '3.2.1-ptu.829574', 52 | '3.2.1-ptu.830734', 53 | '3.2.1-ptu.832719', 54 | '3.2.1-ptu.834470', 55 | '3.2.1-ptu.838080', 56 | '3.2.2-live.846694', 57 | '3.3.0-live.986748', 58 | '3.3.0-ptu.384391', 59 | '3.3.0-ptu.958272', 60 | '3.3.0-ptu.958784', 61 | '3.3.0-ptu.966418', 62 | '3.3.0-ptu.968336', 63 | '3.3.0-ptu.969246', 64 | '3.3.0-ptu.969870', 65 | '3.3.0-ptu.971097', 66 | '3.3.0-ptu.972126', 67 | '3.3.0-ptu.973591', 68 | '3.3.0-ptu.974900', 69 | '3.3.0-ptu.975676', 70 | '3.3.0-ptu.977866', 71 | '3.3.0-ptu.978652', 72 | '3.3.0-ptu.979539', 73 | '3.3.0-ptu.980830', 74 | '3.3.0-ptu.981976', 75 | '3.3.0-ptu.984109', 76 | '3.3.0-ptu.985467', 77 | '3.3.0-ptu.985953', 78 | '3.3.0-ptu.986748', 79 | '3.3.5-live.996871', 80 | '3.3.5-ptu.987405', 81 | '3.3.5-ptu.990428', 82 | '3.3.5-ptu.991429', 83 | '3.3.5-ptu.993091', 84 | '3.3.5-ptu.993498', 85 | '3.3.5-ptu.994255', 86 | '3.3.5-ptu.996871', 87 | '3.3.6-live.998172', 88 | '3.3.6-ptu.998172' 89 | ].join("'), ('")}\')` 90 | ); 91 | pgm.sql( 92 | `INSERT INTO public.commodity_category(name) VALUES (\'${[ 93 | 'Agricultural Supply', 94 | 'Food', 95 | 'Gas', 96 | 'Medical Supply', 97 | 'Metal', 98 | 'Mineral', 99 | 'Scrap', 100 | 'Vice', 101 | 'Waste' 102 | ].join("'), ('")}\')` 103 | ); 104 | 105 | const commodityItems: Array<{ name: string; category: string; gameVersion: string }> = [ 106 | { name: 'Agricium', category: 'Metal', gameVersion: '3.0.0-live.695052' }, 107 | { name: 'Agricultural Supply', category: 'Agricultural Supply', gameVersion: '3.0.0-live.695052' }, 108 | { name: 'Aluminum', category: 'Metal', gameVersion: '3.0.0-live.695052' }, 109 | { name: 'Astatine', category: 'Gas', gameVersion: '3.0.0-live.695052' }, 110 | { name: 'Beryl', category: 'Mineral', gameVersion: '3.0.0-live.695052' }, 111 | { name: 'Chlorine', category: 'Gas', gameVersion: '3.0.0-live.695052' }, 112 | { name: 'Corundum', category: 'Mineral', gameVersion: '3.0.0-live.695052' }, 113 | { name: 'Diamond', category: 'Mineral', gameVersion: '3.0.0-live.695052' }, 114 | { name: 'Distilled Spirits', category: 'Vice', gameVersion: '3.0.0-live.695052' }, 115 | { name: 'Fluorine', category: 'Gas', gameVersion: '3.0.0-live.695052' }, 116 | { name: 'Gold', category: 'Metal', gameVersion: '3.0.0-live.695052' }, 117 | { name: 'Hydrogen', category: 'Gas', gameVersion: '3.0.0-live.695052' }, 118 | { name: 'Iodine', category: 'Gas', gameVersion: '3.0.0-live.695052' }, 119 | { name: 'Laranite', category: 'Mineral', gameVersion: '3.0.0-live.695052' }, 120 | { name: 'Medical Supplies', category: 'Medical Supply', gameVersion: '3.0.0-live.695052' }, 121 | { name: 'Processed Food', category: 'Food', gameVersion: '3.0.0-live.695052' }, 122 | { name: 'Quartz', category: 'Mineral', gameVersion: '3.0.0-live.695052' }, 123 | { name: 'Scrap', category: 'Scrap', gameVersion: '3.0.0-live.695052' }, 124 | { name: 'Stims', category: 'Vice', gameVersion: '3.0.0-live.695052' }, 125 | { name: 'Titanium', category: 'Metal', gameVersion: '3.0.0-live.695052' }, 126 | { name: 'Tungsten', category: 'Metal', gameVersion: '3.0.0-live.695052' }, 127 | { name: 'Waste', category: 'Waste', gameVersion: '3.0.0-live.695052' } 128 | ]; 129 | 130 | for (const commodityItem of commodityItems) { 131 | pgm.sql( 132 | 'INSERT INTO public.item(type, name, commodity_category_id, in_game_since_version_id)' + 133 | ` SELECT 'COMMODITY', '${commodityItem.name}', cc.id, g.id` + 134 | ' FROM game_version g' + 135 | ` JOIN commodity_category cc ON cc.name = '${commodityItem.category}'` + 136 | ` WHERE g.identifier = '${commodityItem.gameVersion}'` 137 | ); 138 | } 139 | } 140 | 141 | export function down(pgm: MigrationBuilder): void { 142 | pgm.sql('DELETE FROM public.item'); 143 | pgm.sql('DELETE FROM public.commodity_category'); 144 | pgm.sql('DELETE FROM public.game_version'); 145 | } 146 | -------------------------------------------------------------------------------- /migrations/1543603809087_init.ts: -------------------------------------------------------------------------------- 1 | import { ColumnDefinitions, MigrationBuilder, PgLiteral, PgType } from 'node-pg-migrate'; 2 | 3 | export const shorthands: ColumnDefinitions = { 4 | id: { type: PgType.UUID, primaryKey: true, default: new PgLiteral('uuid_generate_v4()') } 5 | }; 6 | 7 | export function up(pgm: MigrationBuilder): void { 8 | pgm.createTable('organization', { 9 | id: { type: 'id' }, 10 | name: { type: PgType.TEXT, notNull: true }, 11 | tag: { type: PgType.TEXT, notNull: true } 12 | }); 13 | pgm.createTable('account', { 14 | id: { type: 'id' }, 15 | username: { type: PgType.TEXT, notNull: true, unique: true }, 16 | handle: { type: PgType.TEXT, notNull: true, unique: true }, 17 | email: { type: PgType.TEXT, notNull: true, unique: true }, 18 | main_organization_id: { type: PgType.UUID, references: { name: 'organization' } } 19 | }); 20 | pgm.createTable('organization_member', { 21 | organization_id: { type: 'id' }, 22 | account_id: { type: 'id' }, 23 | since: { type: PgType.TIMESTAMPTZ } 24 | }); 25 | pgm.createTable('commodity_category', { 26 | id: { type: 'id' }, 27 | name: { type: PgType.TEXT, notNull: true, unique: true } 28 | }); 29 | pgm.createTable('game_version', { 30 | id: { type: 'id' }, 31 | identifier: { type: PgType.TEXT, notNull: true, unique: true }, 32 | release: { type: PgType.TIMESTAMPTZ } 33 | }); 34 | pgm.createTable('manufacturer', { 35 | id: { type: 'id' }, 36 | name: { type: PgType.TEXT, notNull: true, unique: true } 37 | }); 38 | pgm.createType('item_type', [ 39 | 'ARMS', 40 | 'ATTACHMENT', 41 | 'COMMODITY', 42 | 'COOLER', 43 | 'GADGET', 44 | 'GUN', 45 | 'HELMET', 46 | 'LEGS', 47 | 'MISSILE', 48 | 'ORDNANCE', 49 | 'POWER_PLANT', 50 | 'QUANTUM_DRIVE', 51 | 'SHIELD_GENERATOR', 52 | 'SHIP', 53 | 'TORSO', 54 | 'TURRET', 55 | 'UNDERSUIT', 56 | 'WEAPON' 57 | ]); 58 | pgm.createTable('item', { 59 | id: { type: 'id' }, 60 | name: { type: PgType.TEXT, notNull: true, unique: true }, 61 | in_game_since_version_id: { type: PgType.UUID, references: { name: 'game_version' }, notNull: true }, 62 | in_game_since: { type: PgType.TIMESTAMPTZ }, 63 | type: { type: 'item_type', notNull: true }, 64 | scu: { type: PgType.INTEGER }, 65 | commodity_category_id: { type: PgType.UUID, references: { name: 'commodity_category' } }, 66 | manufacturer_id: { type: PgType.UUID, references: { name: 'manufacturer' } }, 67 | focus: { type: PgType.TEXT }, 68 | size: { type: PgType.SMALLINT }, 69 | max_ammo: { type: PgType.SMALLINT }, 70 | max_range: { type: PgType.INTEGER }, 71 | damage: { type: PgType.NUMERIC } 72 | }); 73 | pgm.createTable('location_type', { 74 | id: { type: 'id' }, 75 | name: { type: PgType.TEXT, notNull: true, unique: true } 76 | }); 77 | pgm.createTable('location', { 78 | id: { type: 'id' }, 79 | name: { type: PgType.TEXT, notNull: true, unique: true }, 80 | in_game_since_version_id: { type: PgType.UUID, references: { name: 'game_version' }, notNull: true }, 81 | in_game_since: { type: PgType.TIMESTAMPTZ }, 82 | type_id: { type: PgType.UUID, references: { name: 'location_type' }, notNull: true }, 83 | parent_location_id: { type: PgType.UUID, references: { name: 'location' } } 84 | }); 85 | pgm.createType('item_price_type', ['BUY', 'SELL']); 86 | pgm.createType('item_price_visibility', ['PRIVATE', 'MAIN_ORGANIZATION', 'MEMBER_ORGANIZATION', 'PUBLIC']); 87 | pgm.createTable('item_price', { 88 | id: { type: 'id' }, 89 | scanned_by_id: { type: PgType.UUID, references: { name: 'account' }, notNull: true }, 90 | item_id: { type: PgType.UUID, references: { name: 'item' }, notNull: true }, 91 | location_id: { type: PgType.UUID, references: { name: 'location' }, notNull: true }, 92 | price: { type: PgType.NUMERIC, notNull: true }, 93 | quantity: { type: PgType.BIGINT, notNull: true }, 94 | scan_time: { type: PgType.TIMESTAMPTZ, notNull: true, default: new PgLiteral('now()') }, 95 | type: { type: 'item_price_type', notNull: true }, 96 | visibility: { type: 'item_price_visibility', notNull: true, default: 'PUBLIC' } 97 | }); 98 | pgm.createTable('transaction', { 99 | id: { type: 'id' }, 100 | account_id: { type: PgType.UUID, references: { name: 'account' }, notNull: true }, 101 | commodity_id: { type: PgType.UUID, references: { name: 'item' }, notNull: true } 102 | }); 103 | pgm.createType('transaction_detail_type', ['BOUGHT', 'SOLD', 'LOST']); 104 | pgm.createTable('transaction_detail', { 105 | id: { type: 'id' }, 106 | transaction_id: { type: PgType.UUID, references: { name: 'transaction' }, notNull: true }, 107 | type: { type: 'transaction_detail_type', notNull: true }, 108 | location_id: { type: PgType.UUID, references: { name: 'location' }, notNull: true }, 109 | price: { type: PgType.NUMERIC, notNull: true }, 110 | quantity: { type: PgType.BIGINT, notNull: true }, 111 | note: { type: PgType.TEXT }, 112 | timestamp: { type: PgType.TIMESTAMPTZ, notNull: true, default: new PgLiteral('now()') } 113 | }); 114 | } 115 | 116 | export function down(pgm: MigrationBuilder): void { 117 | pgm.dropTable('transaction_detail'); 118 | pgm.dropType('transaction_detail_type'); 119 | pgm.dropTable('transaction'); 120 | pgm.dropTable('item_price'); 121 | pgm.dropType('item_price_visibility'); 122 | pgm.dropType('item_price_type'); 123 | pgm.dropTable('location'); 124 | pgm.dropTable('location_type'); 125 | pgm.dropTable('item'); 126 | pgm.dropType('item_type'); 127 | pgm.dropTable('manufacturer'); 128 | pgm.dropTable('game_version'); 129 | pgm.dropTable('commodity_category'); 130 | pgm.dropTable('organization_member'); 131 | pgm.dropTable('account'); 132 | pgm.dropTable('organization'); 133 | } 134 | -------------------------------------------------------------------------------- /docs/Star Citizen Trade Market.xml: -------------------------------------------------------------------------------- 1 | 7Z1dc5u4Gsc/jWd2L9wBgcFcJt60zTZvk6S703PjUTBOmGODD+C26ac/wkbYRqLBDhIS8+zuzMaAscF//ZCe14E1Wf78lODVy3U8CxYDZMx+Dqy/BgiZNnLI//Itr9strjXebnhOwllx0G7DQ/grKDYaxdZ1OAvSgwOzOF5k4epwox9HUeBnB9twksQ/Dg+bx4vDT13h54DZ8ODjBbv133CWvRRbTcfb7fgchM8vxUePkbvdscT04OJK0hc8i3/sbbIuBtYkieNs+9fy5yRY5DeP3pft+z7W7C2/WBJEWZM3WDgw/Tlyh47vO0Nj7tvDYG7hoeMgx3p6Mo0n7A6Ls37Hi3VxA7Dvx2vyCdtryF7pjUl/hMsFjsir83kcZQ/FHoO89l/CxewKv8br/IulGfb/S1+dv8RJ+Iscjxdkl0k2kN1JVvzuRCzkbOFiMYkXcbL5HCsw8n8P3vmQn7H4rCRIyXvv6F0wK5uu8c+DA69wmtFvGS8WeJWGT5vvnb9xiZPnMDqPsyxeFgfRq/x4+KXmm3/IfrwInyOyzSefFST0XmyvxrTJa/ZXorc4SLLg596m4lf7FMTLIEteySHFXjQuFFQMIdO1Poy2W37sJIlstN32sqdGhIqNuBgGz+XZd0ohfxRieYdwECMc8nZGMy94lf9JbkcW4sU9GbA4es73nmfxqrjli2BOf6KkuJL87yf6s5h7t3177Hl+L0MyZs+KzctwNtucdV9LUbwRa7rCfhg9X20/xbJ3m+6LT8s3xeSU88VmuL6QkwVR/m3iDGf4qZT5Kg6jbHNfR+fkP/IjTQzyw4zIlU7Ia3P3mvyXH55kkzhKswSHGy0ERIs/glyP57MkXj0S9QX0aveH1KgiK8SV1VHD+23tFVqj+H5LaJYhSWcWo7O7L+/W2dP+kOdopqncDrXVmrTeUk9+mcXAaEknqEYn5XO2+K4Hj7ID/ewJhmqjE73YjF7WaZBEeBmIoZPxtlw2J9McTR2gaNQQRRRZwqU1YqTVNYi2yuoNhewaibRGIWlScRipvOAof2YAg7Ri0BgpxiAXGCSWQU6NRPRjkGmwi/tgicMFQEgvCJnGWDEKmQa7/AcOtcqh3ejVn0RjRiwEQ9E0Tp5xFP7CWRhHU1HWI8CSMCxZtmJY8gBKYqE0rtGIUkjyr79Mb27Pvr9eRx9XC+fb/Sz9m+PnSOJFkAJz2mQORxenYaieOY4hjzlcIcHE5zjGNGbFoVbqB3EnTHGDwPP92dPQx9ZsaI9H1tC0LGto267nYuQ7mIiZZcwKp+mPOIGpjW5Tm7FE23MjbQF2Tp/aHDV4lZ7aNFw5tq8V4I9cL7zZ0O5synKrmuzDDQDUssFHNIHkiYUN2gizYMkKBkLKBt2ElNmmwwkpMz2OauzRWJZs2NgNiClTMKbMaqy++qAyrtSkBQmZbCgHRJUJeKQJD+iQpxg2ogNiyrqfWB+NIk5QGRdF8lZrENEhmkI9CulgPalhNH0mIJqmYeQHU3KNKXhTtSQTJ9SsYzKBO1U0mbTwpzZLIGCNjgdkAhxphiNe0Fm3PEJgghSdBSTcBClPLBY7r85eiTaQ8ccg/xaWmd/xwQQNzoyBSz7UePh8eVeza3J7fX371+Xjt5r9/16c3d3e1Oz85+Lz5eTqombvhw8f9vb8CZzUjZOcKDguJ01DnvbZZQKQsl1SlngR6K0ZffAO/pE173dY903qr4FMmpEJ2WysXMcrSof18ACYWjZ21elEvzmcy64p/Xi5jGdh9jr1cRY8x8krmLo0JJOjmhXehbWlYDC5fVpbsmpZ4mg9x362ToIEkKQhkjzVzO/IQsAk0au4/kT9Iotdtc1jfw0pTrqhyDKVs7xbsG4TjqL+rNuQxXqO88BZIJFuJLIb2rblSctmbQJAopZJ5NXoREMS2byV2s8pXi5joJFuNHJVs2eXKVlAI2E0snu0RLPZtIOcRgnRA0yOtMORp5oRG9lskgLgqGUcjWp0oiOOWIvRDC8xsEg7Ftl0oCvEIjAZCWdRj0xGAibSACHJELIb2q2l1SFA7AMOGNQug1CNSvQrWoHYB9YmfaVIqWOFA8UrBnKKVzjU6Fu2FPOaFq8wTVnFKxC7tofiFeoVr0B1JQl+E6zWTGrSShEgdpUPxSsEPNqEr/TlKYbNhQrJTc/CeUiYDfPsLufZxwNJuRIWCNKNRLNIeLaRvBwQzjw7CRbk9gXTLFySO4iXK2CSZkxSr3gFZz4OUGo31ahWJvpRiRM8CwzSjEGNK1ZIMylB2KxoBomPmpUnFvaBtYj9TXMkVjRgexzIsT166ND2aDtNLY+etGcXaw8Cy6N6lkerzp6kj+WRU1MHLI8CHmrCC1bKUwxrGoKyuZ3PrY9HkXI2R06KUdcg6tvUuj/FKTlh+tvbMaUzbEjZ149JytkcOfG0wKRWmVQOZP2ZRBuyVOpTAoj0A5F6pXLHUM5IdCfk/pQzMjmF3qGrQF/Y1LQ8rUTPLKTwi/bMliO6B3TilH2HzgJaI4nTw71rJLEGAkBSy8Ei/cnjF1GDBiAkGUKcDu/dBotwStUAg9q1Hglfs0kTi9vA/RHMngPqx86D/bPX+2CxsXZf7PYw9/YlW9LokCCanSXJ5je9uP8VJPFjfI2j10ERN7Lbt8TR7HYjqHeO3zReJ35wzBuLUZxRqDTWQnG/8pv0W17s88Hg/uTFxmRzc78HB7riCaH4jLt8NAzKQBMqHepwq5xge2eK9+zExJymrLNMOefahyfa3inmRBtVlpfdItXYEDgy2KMU+xDYpFRSpemavKRKhyf53yVVFh+4ezSVn+h6VYlzHsG8IVad2OEFuSsRzoLzeB3NUikyhuxOHWKs7FayO3malxYxw6nhBDFWAuaC/cnudNlHLPZ9gsYMDPZdL0yPBxIv0ooHJHmtUSCRSnRrlP7kUdlshPCuZRPQSDsa8WKsOqWRDbnmomdG/ck1H0FWp/YE4gZXca0SsubbI8jqFIygUX+yOkesoZ7tYcnKB8ygAzlm0IoV1KFeuLdoQ9vFCRcQ/XCwPCpteSzHub6WR4d1QIPlsf2HWzmg9bc8OohRDGR3dj7HPh5FytkcHcikEk0hVCMS/Vb5jgCTEBBILoGa2hmlrdscyC8XTSDh+eXyovHYeRAjFjWi8YobfHScHR2LR8fZ0XA0ZeLsUCUKaeyNDk/RNNLOqpzIrZ5IcqSd22DOpLcIaUnPo0VI49WUEaFpVMM9q2u2pip0K/XJHPok7UqFDVrQaa7CU0logwhliZCNQuiZCE37RBVS764yKrQrAesjt7JubCrCUr3FiTzUbeS722BZKk+E231bGW525U6X4iTtyfLkdAxEZy/K6NIZV3RpOh9OnCk6o8pM0bKrp5KsTcpnTq3ZaV6FhhEqOCS7KjjreLy8DMSTvjSf5JhdaoBPUj2fZDnK3+OT5CpNmodpzMbagE9SQDUj4QE38hTDhquDT7Jzj8DxKOL4JLkokldTrYFlAzwC7xKJ8JwseWJpYIEAAqlNII5PkksgaW6mMeQ+iCaQ8NwHeWLRpkLIqcYmOmk92thELTvK2JpMs9rh2nZPNTYx5xoj5lySrU3UUbonxTh5xlH4C7obdWtssmgYWKEV2lXhXRVAWsWYx6n+CKYm5UxN5RDXN/ydWk/B1CR0iuXVQUo/U5PHmsG3piYibcs0ynsEa76O1nzHU0m5SHgPkk1FA0m47VueWFjbd7oiF5Csl1B8Qz8YKVd8wwMTuGgY9ccE7gnIoAACySWQesU3SpMbMEgYg4Q3D5GolgbF67oJwmR/nFPt4HRFfbwdfKCWGRxVQoHH5qmpOZUTudUTSTaAm2bvA9LRiRpEhmoqrMT92u6pjpjKiazqiaSLUKmAdBEiPJmEyHAUU6FV8eKN7JZYaI26TYswEeQpvtm3TRkZek5FPdboVMf0uJKfMzKsjv3SJmLtGWEWLKerJOT1lQO39KCb3hQW7dayH4plcmRvWbKsGyZip3TgmFbPMb0b4+9KguCKTdoqFrEzN3BNi+hRKDz8T55mLNb8mvqYXMRs+gSl2Du3xZ7AJV5GBI9L8tqmimiECbZY/jDW3yFkIk6f+Xy6DTDSD0a85IhuYWQBjITPj3oEI4sNLC5LIACQ9AMSz1XdNZEQEEn09Kg/XeVNizWM1xgigUWKs8iylWMRxBELZ1F/AolNi42b+d8abzxxgCPtcESdeArhCCKJheOoP6HEpsU627JwSW4dXq6AR9rxaKycJduC0hrCedSfvqImpwvttmym8cf5128Dd/JwcXX1J5BJNzIhUzmztg2NSESTyRbeiUSiXNh1/vcwDZ/CxWbpZvyxn50+QYMzg9CKbL+7v/zn7PGiZu/12eXN9Pb+09nN5X/OHi9vb+qOu7g+v7hvcuTd1/Ory8neTqClfrS0Gprcx7KqxZh2g3wfoOX7aFnXwrs1WkqUC+sEBAzphiGracCmvLRDGq8MGBKHIeGeP4ly6X9KzPZ9x2fE0Pg3ZTJiWkvMsitdQcbV9g2ys2Hs3rfuOrlrUhn51D8ZVvvceFXsSZdh73OlT+6SVIa7KCPDalKfbXsn5wcah6dykdFxfiAy4Llcm+KqXjfDin6sUxu7mjR/V5EHMzIaxO7oLcOTH8zIUU6H1b5xpz6Yh5X+wo7T7YMZcWrp7Jfxni6D5VOQMMqEtGlZadPVrOnGxbxRvchbXeiWZTbqJNRh4H3zDGrHUNgC134G9W7c61vbGxnsoiLPoJ58/GIeo7j9W2wKMtT1L696N+xbsdXRPNiOlMSGfmDfj9dRJoxerdZ/AHq1VQNcoubYGJCCXke5FYBepymo1fCQbulFjXv7VSHCSP0cI6Wp1QmlODG03RYHJ+sIRlvvlhU4PfnDVxiK5MllxMplFadpkKbQE0whKwKijva3QSNPOoiRjhZzb6VjdwTMvUfNxaes5WDE+oCg9pqAR1s5pvWvvVY6xyTaCCDAUByUlGsKhkYQ5iycR8LDnOXJxWF5BIXXNIWRck3BkAMwEg0jp0cwGrE+4NU68V9wGtQVXgcmqc2kxm3CJKqsQRQpQOl9OmnVq9sxlFivbgklf52QW+P/Lnv2/uLsanp9e3PxreaAs+nXi0ntm+v2HL7nz4PG0tBjWl9ccsrDcXHpydM/VBkQjkvhVQbakIt//WV6c3v2/fU6+rhaON/uZ+nfQ05hSgrHGc5gwtYqgTj6OBVK9WZ2Kh8ZEzaupKAc5XG8aUyNQ7XUD2eFJ2MO694DxOg2yWlsNZeWZk3+BOiINlQJ9+JJlAunyFuCoxT7m1yFWZDhcMEKCAJVBnICVTz6wCuXVWXu6dvhA79RUfGBu6FffqJbSckamRzG8ZLLqs9OvCB3JSJT5/N4Hc1SRrsi0rccTvcdQf4fCJp5D0Lr1okaBc3QnsMQNCP4cSu8IY9EzbDxoPuPW/BVd70GOB5M6gXOuAKWlrAE4I/jHriFxmxoA63zXOOyOb/9+unzY83Oh9urv2p2Xd0+PEI1VJ1h1zQwx5VV3bJMEQLYCYPdWHiTDXlycVnzGHRE1BhIvLY/XU+/wO0jfPrVny5kyGWnXxAhqCeL1EtXdmF2JJxF/WlBhlw2oBQ6IuqKI16fn65xBB3IhOOoPx3IkMd6WKIYQvE0RBGniU7HKPKgkItoFJXDtwcoctnAAmjOqi+PnIbJCRK9MOyzDnjU8tRIeCCBRLkICPMEDMnGkNfQeC0vHnQMxmvh7jThxmuJcmmQUtdu1wYiGZlNG2hjg+ObNtAQEXWaNphUFxQ/aHRqHxsTVc5lsudqr3WDM3OfnqwxGnp47gxdx89pG4yHhjubO9ibj82RO2TXc0QP6zn2s3UCHRvUqbXo2o0j2Ks9aU7hWCPtIEY7EDUuNWr8qBGuZMx4oyuAMovvnGEdNZqVjhdvdCXsZDzCS7B/C1voicGQzAjxRlcAtcwEE0iLSmaNrkRA/AjQRyp9moZst2E0aHQFULRMMH2ElyyTJpUG4SLK9gQ9Zqp6vHVJuSbJVYOQU128NzUtoWpLUK+itpPtSuRlEsfZ/uEJXr1cx7MgP+L/ -------------------------------------------------------------------------------- /src/item-price/item-price.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NotFoundException } from '@nestjs/common'; 2 | import { QueryResult } from 'pg'; 3 | import { client } from '../database.service'; 4 | import { TABLENAME as GAME_VERSION_TABLENAME } from '../game-version/game-version.service'; 5 | import { Account, ItemPrice, ItemPriceVisibility } from '../graphql.schema'; 6 | import { CreateItemPriceDto } from './dto/create-item-price.dto'; 7 | import { UpdateItemPriceDto } from './dto/update-item-price.dto'; 8 | 9 | export const TABLENAME: string = 'item_price'; 10 | 11 | @Injectable() 12 | export class ItemPriceService { 13 | private readonly logger: Logger = new Logger(ItemPriceService.name); 14 | 15 | public async create({ 16 | scannedById, 17 | itemId, 18 | locationId, 19 | price, 20 | quantity, 21 | scanTime = new Date(), 22 | type, 23 | visibility = ItemPriceVisibility.PUBLIC, 24 | scannedInGameVersionId 25 | }: CreateItemPriceDto): Promise { 26 | if (scannedInGameVersionId === undefined) { 27 | scannedInGameVersionId = ( 28 | await client.query(`SELECT id FROM ${GAME_VERSION_TABLENAME} ORDER BY identifier DESC LIMIT 1`) 29 | ).rows[0].id; 30 | } 31 | const result: QueryResult = await client.query( 32 | `INSERT INTO ${TABLENAME}(scanned_by_id, item_id, location_id, price, quantity, scan_time, type, visibility,` + 33 | ' scanned_in_game_version_id)' + 34 | ' VALUES ($1::uuid, $2::uuid, $3::uuid, $4::numeric,' + 35 | ' $5::bigint, $6::timestamptz, $7::item_price_type, $8::item_price_visibility, $9::uuid) RETURNING *', 36 | [scannedById, itemId, locationId, price, quantity, scanTime, type, visibility, scannedInGameVersionId] 37 | ); 38 | const created: ItemPrice = result.rows[0]; 39 | this.logger.log(`Created ${TABLENAME} with id ${created.id}`); 40 | return created; 41 | } 42 | 43 | public async update( 44 | id: string, 45 | { 46 | scannedById, 47 | itemId, 48 | locationId, 49 | price, 50 | quantity, 51 | scanTime, 52 | type, 53 | visibility, 54 | scannedInGameVersionId 55 | }: UpdateItemPriceDto 56 | ): Promise { 57 | const updates: any[] = []; 58 | const values: any[] = []; 59 | let updateIndex: number = 2; 60 | if (scannedById !== undefined) { 61 | updates.push(` scanned_by_id = $${updateIndex}::uuid`); 62 | values.push(scannedById); 63 | updateIndex++; 64 | } 65 | if (itemId !== undefined) { 66 | updates.push(` item_id = $${updateIndex}::uuid`); 67 | values.push(itemId); 68 | updateIndex++; 69 | } 70 | if (locationId !== undefined) { 71 | updates.push(` location_id = $${updateIndex}::uuid`); 72 | values.push(locationId); 73 | updateIndex++; 74 | } 75 | if (price !== undefined) { 76 | updates.push(` price = $${updateIndex}::numeric`); 77 | values.push(price); 78 | updateIndex++; 79 | } 80 | if (quantity !== undefined) { 81 | updates.push(` quantity = $${updateIndex}::bigint`); 82 | values.push(quantity); 83 | updateIndex++; 84 | } 85 | if (scanTime !== undefined) { 86 | updates.push(` scan_time = $${updateIndex}::timestamptz`); 87 | values.push(scanTime); 88 | updateIndex++; 89 | } 90 | if (type !== undefined) { 91 | updates.push(` type = $${updateIndex}::item_price_type`); 92 | values.push(type); 93 | updateIndex++; 94 | } 95 | if (visibility !== undefined) { 96 | updates.push(` visibility = $${updateIndex}::item_price_visibility`); 97 | values.push(visibility); 98 | updateIndex++; 99 | } 100 | if (scannedInGameVersionId !== undefined) { 101 | updates.push(` scanned_in_game_version_id = $${updateIndex}::uuid`); 102 | values.push(scannedInGameVersionId); 103 | updateIndex++; 104 | } 105 | if (updates.length === 0) { 106 | const itemPrice: ItemPrice | undefined = await this.findOneById(id); 107 | if (!itemPrice) { 108 | throw new NotFoundException(`ItemPrice with id ${id} not found`); 109 | } 110 | return itemPrice; 111 | } 112 | const result: QueryResult = await client.query( 113 | `UPDATE ${TABLENAME} SET${updates.join()} WHERE id = $1::uuid RETURNING *`, 114 | [id, ...values] 115 | ); 116 | const updated: ItemPrice = result.rows[0]; 117 | this.logger.log(`Updated ${TABLENAME} with id ${updated.id}`); 118 | return updated; 119 | } 120 | 121 | public async delete(id: string): Promise { 122 | await client.query(`DELETE FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 123 | return id; 124 | } 125 | 126 | public async findAll(): Promise { 127 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} ORDER BY scan_time DESC`); 128 | return result.rows; 129 | } 130 | 131 | public async findAllByVisibilityInList(visibility: ItemPriceVisibility[]): Promise { 132 | const result: QueryResult = await client.query( 133 | `SELECT * FROM ${TABLENAME} WHERE visibility = ANY($1::item_price_visibility[]) ORDER BY scan_time DESC`, 134 | [visibility] 135 | ); 136 | return result.rows; 137 | } 138 | 139 | public async findAllWithSignedInUser({ id }: Account): Promise { 140 | const result: QueryResult = await client.query( 141 | 'SELECT * FROM f_item_price_visible($1::uuid) ORDER BY scan_time DESC', 142 | [id] 143 | ); 144 | return result.rows; 145 | } 146 | 147 | public async findOneById(id: string): Promise { 148 | const result: QueryResult = await client.query(`SELECT * FROM ${TABLENAME} WHERE id = $1::uuid`, [id]); 149 | return result.rows[0]; 150 | } 151 | 152 | public async findOneByIdAndVisibilityInList( 153 | id: string, 154 | visibility: ItemPriceVisibility[] 155 | ): Promise { 156 | const result: QueryResult = await client.query( 157 | `SELECT * FROM ${TABLENAME} WHERE id = $1::uuid AND visibility = ANY($2::item_price_visibility[])`, 158 | [id, visibility] 159 | ); 160 | return result.rows[0]; 161 | } 162 | 163 | public async findOneByIdWithSignedInUser(id: string, currentUser: Account): Promise { 164 | const result: QueryResult = await client.query('SELECT * FROM f_item_price_visible($1::uuid, $2::uuid)', [ 165 | currentUser.id, 166 | id 167 | ]); 168 | return result.rows[0]; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/item-price/item-price.resolvers.ts: -------------------------------------------------------------------------------- 1 | import { NotFoundException, UnauthorizedException, UseGuards } from '@nestjs/common'; 2 | import { Args, Mutation, Parent, Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql'; 3 | import { PubSub } from 'graphql-subscriptions'; 4 | import { AccountService } from '../account/account.service'; 5 | import { CurrentAuthUser } from '../auth/current-user'; 6 | import { GraphqlAuthGuard } from '../auth/graphql-auth.guard'; 7 | import { HasAnyRole } from '../auth/has-any-role.decorator'; 8 | import { RoleGuard } from '../auth/role.guard'; 9 | import { CurrentUser } from '../auth/user.decorator'; 10 | import { GameVersionService } from '../game-version/game-version.service'; 11 | import { Account, GameVersion, Item, ItemPrice, ItemPriceVisibility, Location, Role } from '../graphql.schema'; 12 | import { ItemService } from '../item/item.service'; 13 | import { LocationService } from '../location/location.service'; 14 | import { CreateItemPriceDto } from './dto/create-item-price.dto'; 15 | import { UpdateItemPriceDto } from './dto/update-item-price.dto'; 16 | import { ItemPriceService } from './item-price.service'; 17 | 18 | const pubSub: PubSub = new PubSub(); 19 | 20 | @Resolver('ItemPrice') 21 | export class ItemPriceResolvers { 22 | public constructor( 23 | private readonly itemPriceService: ItemPriceService, 24 | private readonly accountService: AccountService, 25 | private readonly itemService: ItemService, 26 | private readonly locationService: LocationService, 27 | private readonly gameVersionService: GameVersionService 28 | ) {} 29 | 30 | @Query() 31 | public async itemPrices(@CurrentUser() currentUser: Account | undefined): Promise { 32 | if (currentUser === undefined) { 33 | return await this.itemPriceService.findAllByVisibilityInList([ItemPriceVisibility.PUBLIC]); 34 | } 35 | return await this.itemPriceService.findAllWithSignedInUser(currentUser); 36 | } 37 | 38 | @Query() 39 | public async itemPrice( 40 | @Args('id') id: string, 41 | @CurrentUser() currentUser: Account | undefined 42 | ): Promise { 43 | if (currentUser === undefined) { 44 | return await this.itemPriceService.findOneByIdAndVisibilityInList(id, [ItemPriceVisibility.PUBLIC]); 45 | } 46 | return await this.itemPriceService.findOneByIdWithSignedInUser(id, currentUser); 47 | } 48 | 49 | @Mutation() 50 | @UseGuards(GraphqlAuthGuard, RoleGuard) 51 | @HasAnyRole(Role.USER, Role.ADVANCED, Role.ADMIN) 52 | public async createItemPrice( 53 | @Args('input') args: CreateItemPriceDto, 54 | @CurrentUser() currentUser: Account 55 | ): Promise { 56 | const created: ItemPrice = await this.itemPriceService.create({ 57 | scannedById: currentUser.id, 58 | ...args 59 | }); 60 | pubSub.publish('itemPriceCreated', { itemPriceCreated: created }); 61 | return created; 62 | } 63 | 64 | @Mutation() 65 | @UseGuards(GraphqlAuthGuard, RoleGuard) 66 | @HasAnyRole(Role.USER, Role.ADVANCED, Role.ADMIN) 67 | public async updateItemPrice( 68 | @Args('id') id: string, 69 | @Args('input') args: UpdateItemPriceDto, 70 | @CurrentUser() currentUser: CurrentAuthUser 71 | ): Promise { 72 | if (!currentUser.hasRole(Role.ADMIN)) { 73 | const itemPrice: ItemPrice | undefined = await this.itemPriceService.findOneById(id); 74 | if (!itemPrice) { 75 | throw new NotFoundException(`ItemPrice with id ${id} not found`); 76 | } 77 | if (itemPrice.scannedById !== currentUser.id) { 78 | throw new UnauthorizedException('You can only update your own reported prices'); 79 | } 80 | } 81 | const updated: ItemPrice = await this.itemPriceService.update(id, args); 82 | pubSub.publish('itemPriceUpdated', { itemPriceUpdated: updated }); 83 | return updated; 84 | } 85 | 86 | @Mutation() 87 | @UseGuards(GraphqlAuthGuard, RoleGuard) 88 | @HasAnyRole(Role.USER, Role.ADVANCED, Role.ADMIN) 89 | public async deleteItemPrice(@Args('id') id: string, @CurrentUser() currentUser: CurrentAuthUser): Promise { 90 | if (!currentUser.hasRole(Role.ADMIN)) { 91 | const itemPrice: ItemPrice | undefined = await this.itemPriceService.findOneById(id); 92 | if (!itemPrice) { 93 | throw new NotFoundException(`ItemPrice with id ${id} not found`); 94 | } 95 | if (itemPrice.scannedById !== currentUser.id) { 96 | throw new UnauthorizedException('You can only delete your own reported prices'); 97 | } 98 | } 99 | await this.itemPriceService.delete(id); 100 | pubSub.publish('itemPriceDeleted', { itemPriceDeleted: id }); 101 | return id; 102 | } 103 | 104 | @Subscription() 105 | public itemPriceCreated(): AsyncIterator { 106 | return pubSub.asyncIterator('itemPriceCreated'); 107 | } 108 | 109 | @Subscription() 110 | public itemPriceUpdated(): AsyncIterator { 111 | return pubSub.asyncIterator('itemPriceUpdated'); 112 | } 113 | 114 | @Subscription() 115 | public itemPriceDeleted(): AsyncIterator { 116 | return pubSub.asyncIterator('itemPriceDeleted'); 117 | } 118 | 119 | @ResolveField() 120 | public async scannedBy(@Parent() parent: ItemPrice): Promise { 121 | const account: Account | undefined = await this.accountService.findOneById(parent.scannedById); 122 | if (!account) { 123 | throw new NotFoundException(`Account with id ${parent.scannedById} not found`); 124 | } 125 | return account; 126 | } 127 | 128 | @ResolveField() 129 | public async item(@Parent() parent: ItemPrice): Promise { 130 | const item: Item | undefined = await this.itemService.findOneById(parent.itemId); 131 | if (!item) { 132 | throw new NotFoundException(`Item with id ${parent.itemId} not found`); 133 | } 134 | return item; 135 | } 136 | 137 | @ResolveField() 138 | public async location(@Parent() parent: ItemPrice): Promise { 139 | const location: Location | undefined = await this.locationService.findOneById(parent.locationId); 140 | if (!location) { 141 | throw new NotFoundException(`Location with id ${parent.locationId} not found`); 142 | } 143 | return location; 144 | } 145 | 146 | @ResolveField() 147 | public unitPrice(@Parent() parent: ItemPrice): number { 148 | return parent.price / parent.quantity; 149 | } 150 | 151 | @ResolveField() 152 | public async scannedInGameVersion(@Parent() parent: ItemPrice): Promise { 153 | const gameVersion: GameVersion | undefined = await this.gameVersionService.findOneById( 154 | parent.scannedInGameVersionId 155 | ); 156 | if (!gameVersion) { 157 | throw new NotFoundException(`GameVersion with id ${parent.scannedInGameVersionId} not found`); 158 | } 159 | return gameVersion; 160 | } 161 | } 162 | --------------------------------------------------------------------------------