Commit 421db8af authored by nanahira's avatar nanahira

crud

parent 4268cf19
......@@ -17,6 +17,7 @@
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"pg": "^8.7.1",
"pg-native": "^3.0.0",
"reflect-metadata": "^0.1.13",
......@@ -7170,6 +7171,14 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
......@@ -15842,6 +15851,11 @@
"minimist": "^1.2.5"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
......
......@@ -30,6 +30,7 @@
"class-transformer": "^0.4.0",
"class-validator": "^0.13.1",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"pg": "^8.7.1",
"pg-native": "^3.0.0",
"reflect-metadata": "^0.1.13",
......
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Post,
Put,
UploadedFile,
UseGuards,
UseInterceptors,
ValidationPipe,
} from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger';
import {
ApiBody,
ApiConsumes,
ApiCreatedResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { MyCardAdminGuard } from '../my-card-admin.guard';
import { FileInterceptor } from '@nestjs/platform-express';
import { FileUploadDto } from '../dto/FileUpload.dto';
import { AppsJson } from '../utility/apps-json-type';
import { AppService } from '../app.service';
import { BlankReturnMessageDto } from '../dto/ReturnMessage.dto';
import { AssignAppDto } from '../dto/AssignApp.dto';
@Controller('api/admin')
@ApiTags('admin')
......@@ -27,10 +42,35 @@ export class AdminController {
description: 'apps.json 文件',
type: FileUploadDto,
})
@ApiCreatedResponse({ type: BlankReturnMessageDto })
async migrate(
@UploadedFile('file') file: Express.Multer.File,
): Promise<BlankReturnMessageDto> {
const apps: AppsJson.App[] = JSON.parse(file.buffer.toString());
return this.appService.migrateFromAppsJson(apps);
}
@Put('app/:id')
@ApiOperation({ summary: '创建 app' })
@ApiOkResponse({ type: BlankReturnMessageDto })
async createApp(@Param('id') id: string) {
return this.appService.createApp(id);
}
@Delete('app/:id')
@ApiOperation({ summary: '创建 app' })
@ApiOkResponse({ type: BlankReturnMessageDto })
async deleteApp(@Param('id') id: string) {
return this.appService.deleteApp(id);
}
@Post('app/:id/assign')
@ApiOperation({ summary: '创建 app' })
@ApiOkResponse({ type: BlankReturnMessageDto })
async assignApp(
@Param('id') id: string,
@Body(new ValidationPipe({ transform: true })) assignAppData: AssignAppDto,
) {
return this.appService.assignApp(id, assignAppData.author);
}
}
import { Controller, Get } from '@nestjs/common';
import {
Body,
Controller,
Get,
ParseIntPipe,
Post,
Query,
ValidationPipe,
} from '@nestjs/common';
import { AppService } from './app.service';
import {
ApiBody,
ApiCreatedResponse,
ApiOkResponse,
ApiOperation,
ApiQuery,
} from '@nestjs/swagger';
import {
BlankReturnMessageDto,
GetAppReturnMessageDto,
} from './dto/ReturnMessage.dto';
import { FetchMyCardUser, MyCardUser } from './utility/mycard-auth';
import { AppsJson } from './utility/apps-json-type';
import AppClass = AppsJson.AppClass;
@Controller('api')
export class AppController {
......@@ -9,4 +31,28 @@ export class AppController {
getAppsJson() {
return this.appService.getAppsJson();
}
@Get('app')
@ApiOperation({
summary: '获取 app',
description: '管理员可以查询全部的,其他用户可以查属于自己的',
})
@ApiQuery({ name: 'id', description: 'app 的 id,没有就是查全部' })
@ApiOkResponse({ type: GetAppReturnMessageDto })
getApp(@FetchMyCardUser() user: MyCardUser, @Query('id') id?: string) {
return this.appService.getApp(user, id);
}
@Post('app')
@ApiOperation({
summary: '更新 app',
})
@ApiBody({ type: AppsJson.AppClass })
@ApiCreatedResponse({ type: BlankReturnMessageDto })
updateApp(
@FetchMyCardUser() user: MyCardUser,
@Body(new ValidationPipe({ transform: true })) app: AppClass,
) {
return this.appService.updateApp(user, app.id, app);
}
}
import { Connection, IsNull, Not } from 'typeorm';
import { Any, Connection, FindConditions, In, IsNull, Not } from 'typeorm';
import { InjectConnection } from '@nestjs/typeorm';
import { Injectable, ConsoleLogger } from '@nestjs/common';
import { Injectable, ConsoleLogger, HttpException } from '@nestjs/common';
import { AppsJson } from './utility/apps-json-type';
import { App } from './entities/App.entity';
import { BlankReturnMessageDto } from './dto/ReturnMessage.dto';
import {
BlankReturnMessageDto,
ReturnMessageDto,
} from './dto/ReturnMessage.dto';
import { MyCardUser } from './utility/mycard-auth';
@Injectable()
export class AppService extends ConsoleLogger {
......@@ -18,8 +22,17 @@ export class AppService extends ConsoleLogger {
return (
await this.db
.getRepository(App)
.find({ where: { appContent: Not(IsNull()) } })
).map((a) => a.app);
.find({ where: { appContent: Not(IsNull()), isDeleted: false } })
).map((a) => a.appData);
}
private async updateResult<T>(f: () => Promise<T>, returnCode = 200) {
try {
const result = await f();
return new ReturnMessageDto<T>(returnCode, 'success', result);
} catch (e) {
throw new BlankReturnMessageDto(404, 'Database Fail').toException();
}
}
async migrateFromAppsJson(apps: AppsJson.App[]) {
......@@ -34,17 +47,97 @@ export class AppService extends ConsoleLogger {
const checkExistingApp = await this.db
.getRepository(App)
.findOne({ where: { id: appData.id }, relations: ['history'] });
//this.error('read');
if (checkExistingApp) {
checkExistingApp.updateApp(appData);
checkExistingApp.isDeleted = false;
targetApps.push(checkExistingApp);
} else {
const app = new App();
app.id = appData.id;
app.app = appData;
app.appData = appData;
targetApps.push(app);
}
}
await this.db.getRepository(App).save(targetApps);
return new BlankReturnMessageDto(201, 'success');
//this.error('write');
return this.updateResult(async () => {
await this.db.getRepository(App).save(targetApps);
return;
}, 201);
}
async getApp(user: MyCardUser, id?: string) {
if (!user) {
throw new BlankReturnMessageDto(401, 'Needs login').toException();
}
const query = this.db
.getRepository(App)
.createQueryBuilder('app')
.where('app.isDeleted = false');
if (!user.admin) {
query.andWhere(':uid = ANY(app.author)', { uid: user.id });
}
if (id) {
query.andWhere('app.id = :id', { id });
}
query.leftJoinAndSelect('app.history', 'history');
return new ReturnMessageDto(200, 'success', await query.getMany());
}
async createApp(id: string) {
let app = await this.db
.getRepository(App)
.findOne({ where: { id }, select: ['id', 'isDeleted'] });
if (!app) {
app = new App();
app.id = id;
} else {
if (!app.isDeleted) {
throw new BlankReturnMessageDto(
404,
'App already exists',
).toException();
}
app.isDeleted = false;
}
return this.updateResult(() => this.db.getRepository(App).save(app));
}
async assignApp(id: string, author: number[]) {
return this.updateResult(
() => this.db.getRepository(App).update({ id }, { author }),
201,
);
}
async updateApp(user: MyCardUser, id: string, appData: AppsJson.App) {
if (!user) {
throw new BlankReturnMessageDto(401, 'Needs login').toException();
}
appData.id = id;
const app = await this.db
.getRepository(App)
.findOne({
where: { id: appData.id },
relations: ['history'],
select: ['id', 'author', 'appContent'],
});
if (!app) {
throw new BlankReturnMessageDto(404, 'App not found').toException();
}
if (!app.isUserCanEditApp(user)) {
throw new BlankReturnMessageDto(403, 'Permission denied').toException();
}
app.updateApp(appData, user.id);
return this.updateResult(async () => {
await this.db.getRepository(App).save(app);
return;
}, 201);
}
async deleteApp(id: string) {
return this.updateResult(() =>
this.db.getRepository(App).update({ id }, { isDeleted: true }),
);
}
}
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsPositive } from 'class-validator';
export class AssignAppDto {
@ApiProperty({ description: '作者 ID' })
@IsArray()
@IsPositive({ each: true })
author: number[];
}
import { ApiProperty } from '@nestjs/swagger';
import { HttpException } from '@nestjs/common';
import { AppsJson } from '../utility/apps-json-type';
export class BlankReturnMessageDto {
@ApiProperty({ description: '返回状态' })
......@@ -27,3 +28,8 @@ export class ReturnMessageDto<T> extends BlankReturnMessageDto {
this.data = data;
}
}
export class GetAppReturnMessageDto extends BlankReturnMessageDto {
@ApiProperty({ description: '返回 app' })
data?: AppsJson.AppClass;
}
import { Column, Entity, OneToMany, PrimaryColumn } from 'typeorm';
import { TimeBase } from './TimeBase.entity';
import { Column, Entity, Index, OneToMany, PrimaryColumn } from 'typeorm';
import { AppsJson } from '../utility/apps-json-type';
import { MyCardUser } from '../utility/mycard-auth';
import { AppBase } from './AppBase.entity';
import { AppHistory } from './AppHistory.entity';
import moment from 'moment';
@Entity()
export class App extends AppBase {
@PrimaryColumn('varchar', { length: 64 })
id: string;
@Column('varchar', { length: 128, nullable: true })
authorsStr: string;
@Index()
@Column('int', { nullable: true, array: true })
author: number[];
get author(): number[] {
return this.authorsStr
? this.authorsStr.split('|').map((idStr) => parseInt(idStr))
: [];
}
set author(a) {
this.authorsStr = a.join('|');
}
@Column({ nullable: true, select: false })
isDeleted: boolean;
isUserCanEditApp(u: MyCardUser) {
return u.admin || this.author.includes(u.id);
return u.admin || (this.author && this.author.includes(u.id));
}
@OneToMany(() => AppHistory, (h) => h.parentApp, { cascade: true })
history: AppHistory[];
updateApp(appData: AppsJson.App) {
this.app = appData;
updateApp(appData: AppsJson.App, changeUser?: number) {
appData.updated_at = moment().format('YYYY-MM-DD HH:mm:ss');
this.appData = appData;
const h = new AppHistory();
h.parentApp = this;
h.time = new Date();
h.app = appData;
if (changeUser) {
h.operatingUser = changeUser;
}
h.appData = appData;
this.history.push(h);
}
get appData(): AppsJson.App {
const appData = super.appData;
appData.id = this.id;
return appData;
}
set appData(a) {
this.appContent = JSON.stringify(a);
}
}
......@@ -7,11 +7,15 @@ export class AppBase extends TimeBase {
@Column('text', { nullable: true })
appContent: string;
get app(): AppsJson.App {
return this.appContent ? JSON.parse(this.appContent) : null;
get appData(): AppsJson.App {
if (!this.appContent) {
return null;
}
const a = JSON.parse(this.appContent);
return a;
}
set app(a) {
set appData(a) {
this.appContent = JSON.stringify(a);
}
}
......@@ -17,6 +17,10 @@ export class AppHistory extends AppBase {
@Column()
time: Date;
@Index()
@Column({ nullable: true })
operatingUser: number;
@ManyToOne(() => App, (a) => a.history)
parentApp: App;
}
......@@ -11,7 +11,7 @@ export class MyCardAdminGuard implements CanActivate {
throw new BlankReturnMessageDto(401, 'Invalid user').toException();
}
if (!user.admin) {
throw new BlankReturnMessageDto(403, 'No permission').toException();
throw new BlankReturnMessageDto(403, 'Permission denied').toException();
}
return true;
}
......
import { IsNotEmpty } from 'class-validator';
export namespace AppsJson {
export enum Locale {
zh_CN = 'zh-CN',
......@@ -97,4 +99,36 @@ export namespace AppsJson {
data?: any;
price?: Price;
}
export class AppClass implements App {
@IsNotEmpty()
id: string;
key?: string;
name?: LocaleWise<string>;
description?: LocaleWise<string>;
developers?: LocaleWise<Developer[]>;
publishers?: LocaleWise<Developer[]>;
released_at?: string;
category?: string;
tags?: string[];
trailer?: Trailer[];
dependencies?: PlatformWise<string[]>;
references?: PlatformWise<string[]>;
author?: string;
homepage?: string;
locales?: string[];
actions?: PlatformWise<PlatformAction>;
version?: PlatformWise<string>;
news?: LocaleWise<News[]>;
conference?: string;
icon?: string;
cover?: string;
background?: string;
parent?: string;
network?: Network;
updated_at?: string;
files?: Record<string, Syncable>;
data?: any;
price?: Price;
}
}
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
/* it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
}); */
});
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment