Add extra pagination api's

This commit is contained in:
rubikscraft 2022-05-08 16:12:23 +02:00
parent 966954acc7
commit 51675fcf32
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
22 changed files with 248 additions and 107 deletions

View file

@ -1,3 +0,0 @@
{
"vsicons.presets.angular": true
}

53
.vscode/tasks.json vendored
View file

@ -1,53 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Start full",
"dependsOn": [
"Start backend",
"Start frontend",
"Start postgres",
"Start shared"
],
"dependsOrder": "parallel",
"isBackground": true,
"group": "build"
},
{
"type": "shell",
"label": "Start shared",
"command": "yarn start",
"options": {
"cwd": "./shared"
},
"group": "build"
},
{
"type": "shell",
"label": "Start backend",
"command": "yarn start:dev",
"options": {
"cwd": "./backend"
},
"group": "build"
},
{
"type": "shell",
"label": "Start frontend",
"command": "yarn watch",
"options": {
"cwd": "./frontend"
},
"group": "build"
},
{
"type": "shell",
"label": "Start postgres",
"command": "podman-compose -f ./dev.docker-compose.yml stop; podman-compose -f ./dev.docker-compose.yml up",
"options": {
"cwd": "./support"
},
"group": "build"
}
]
}

View file

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AsyncFailable, Fail } from 'picsur-shared/dist/types';
import { FindResult } from 'picsur-shared/dist/types/find-result';
import { In, Repository } from 'typeorm';
import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity';
import { EImageFileBackend } from '../../models/entities/image-file.entity';
@ -53,12 +54,12 @@ export class ImageDBService {
count: number,
page: number,
userid: string | undefined,
): AsyncFailable<EImageBackend[]> {
): AsyncFailable<FindResult<EImageBackend>> {
if (count < 1 || page < 0) return Fail('Invalid page');
if (count > 100) return Fail('Too many results');
try {
const found = await this.imageRepo.find({
const [found, amount] = await this.imageRepo.findAndCount({
skip: count * page,
take: count,
where: {
@ -67,7 +68,13 @@ export class ImageDBService {
});
if (found === undefined) return Fail('Images not found');
return found;
return {
results: found,
totalResults: amount,
page,
pages: Math.ceil(amount / count),
};
} catch (e) {
return Fail(e);
}

View file

@ -8,6 +8,7 @@ import {
HasFailed,
HasSuccess
} from 'picsur-shared/dist/types';
import { FindResult } from 'picsur-shared/dist/types/find-result';
import { makeUnique } from 'picsur-shared/dist/util/unique';
import { Repository } from 'typeorm';
import { Permissions } from '../../models/constants/permissions.const';
@ -95,7 +96,7 @@ export class UsersService {
if (ImmutableUsersList.includes(userToModify.username)) {
// Just fail silently
this.logger.verbose("User tried to modify system user, failed silently");
this.logger.verbose('User tried to modify system user, failed silently');
return userToModify;
}
@ -213,15 +214,24 @@ export class UsersService {
public async findMany(
count: number,
page: number,
): AsyncFailable<EUserBackend[]> {
): AsyncFailable<FindResult<EUserBackend>> {
if (count < 1 || page < 0) return Fail('Invalid page');
if (count > 100) return Fail('Too many results');
try {
return await this.usersRepository.find({
const [users, amount] = await this.usersRepository.findAndCount({
take: count,
skip: count * page,
});
if (users === undefined) return Fail('Users not found');
return {
results: users,
totalResults: amount,
page,
pages: Math.ceil(amount / count),
};
} catch (e) {
return Fail(e);
}

View file

@ -7,6 +7,7 @@ import { FullMime } from 'picsur-shared/dist/dto/mimes.dto';
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.dto';
import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.dto';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { FindResult } from 'picsur-shared/dist/types/find-result';
import { ParseMime } from 'picsur-shared/dist/util/parse-mime';
import { IsQOI } from 'qoi-img';
import { ImageDBService } from '../../collections/image-db/image-db.service';
@ -33,9 +34,7 @@ export class ImageManagerService {
private readonly sysPref: SysPreferenceService,
) {}
public async findOne(
id: string,
): AsyncFailable<EImageBackend> {
public async findOne(id: string): AsyncFailable<EImageBackend> {
return await this.imagesService.findOne(id, undefined);
}
@ -43,7 +42,7 @@ export class ImageManagerService {
count: number,
page: number,
userid: string | undefined,
): AsyncFailable<EImageBackend[]> {
): AsyncFailable<FindResult<EImageBackend>> {
return await this.imagesService.findMany(count, page, userid);
}
@ -54,7 +53,7 @@ export class ImageManagerService {
const images = await this.imagesService.findList(ids, userid);
if (HasFailed(images)) return images;
const availableIds = images.map(image => image.id);
const availableIds = images.map((image) => image.id);
const deleteResult = await this.imagesService.delete(availableIds);
if (HasFailed(deleteResult)) return deleteResult;

View file

@ -43,16 +43,16 @@ export class UserAdminController {
async listUsersPaged(
@Body() body: UserListRequest,
): Promise<UserListResponse> {
const users = await this.usersService.findMany(body.count, body.page);
if (HasFailed(users)) {
this.logger.warn(users.getReason());
const found = await this.usersService.findMany(body.count, body.page);
if (HasFailed(found)) {
this.logger.warn(found.getReason());
throw new InternalServerErrorException('Could not list users');
}
return {
users: users.map(EUserBackend2EUser),
count: users.length,
page: body.page,
users: found.results.map(EUserBackend2EUser),
page: found.page,
pages: found.pages,
};
}

View file

@ -61,20 +61,20 @@ export class ImageManageController {
body.user_id = userid;
}
const images = await this.imagesService.findMany(
const found = await this.imagesService.findMany(
body.count,
body.page,
body.user_id,
);
if (HasFailed(images)) {
this.logger.warn(images.getReason());
if (HasFailed(found)) {
this.logger.warn(found.getReason());
throw new InternalServerErrorException('Could not list images');
}
return {
images,
count: images.length,
page: body.page,
images: found.results,
page: found.page,
pages: found.pages,
};
}
@ -96,7 +96,6 @@ export class ImageManageController {
return {
images: deletedImages,
count: deletedImages.length,
};
}
}

View file

@ -16,7 +16,7 @@
<div class="sidenav-content" [class.container]="wrapContentWithContainer">
<div class="header-spacer"></div>
<div
class="grow-full"
class="grow-full relative"
[class.container]="wrapContentWithContainer"
[@mainAnimation]="getRouteAnimData()"
>

View file

@ -2,6 +2,10 @@
flex-grow: 1;
}
.relative {
position: relative;
}
.header-spacer {
height: 64px;
margin-bottom: 32px;

View file

@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { PRoutes } from './models/dto/picsur-routes.dto';
import { ErrorsRouteModule } from './routes/errors/errors.module';
import { ImagesRouteModule } from './routes/images/images.module';
import { ProcessingRouteModule } from './routes/processing/processing.module';
import { SettingsRouteModule } from './routes/settings/settings.module';
import { UploadRouteModule } from './routes/upload/upload.module';
@ -30,6 +31,10 @@ const routes: PRoutes = [
path: 'user',
loadChildren: () => UserRouteModule,
},
{
path: 'images',
loadChildren: () => ImagesRouteModule,
},
{
path: 'settings',
loadChildren: () => SettingsRouteModule,

View file

@ -56,6 +56,10 @@
<h2>{{ user?.username }}</h2>
</div>
</span>
<button *ngIf="canUpload" mat-menu-item (click)="doImages()">
<mat-icon fontSet="material-icons-outlined">image</mat-icon>
<span>My Images</span>
</button>
<button *ngIf="canAccessSettings" mat-menu-item (click)="doSettings()">
<mat-icon fontSet="material-icons-outlined">settings</mat-icon>
<span>Settings</span>

View file

@ -83,4 +83,8 @@ export class HeaderComponent implements OnInit {
doUpload() {
this.router.navigate(['/upload']);
}
doImages() {
this.router.navigate(['/images']);
}
}

View file

@ -0,0 +1 @@
hello

View file

@ -0,0 +1,12 @@
import { Component, OnInit } from '@angular/core';
@Component({
templateUrl: './images.component.html',
styleUrls: ['./images.component.scss'],
})
export class ImagesComponent implements OnInit {
constructor() {}
ngOnInit(): void {}
}

View file

@ -0,0 +1,10 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ImagesComponent } from './images.component';
import { ImagesRoutingModule } from './images.routing.module';
@NgModule({
declarations: [ImagesComponent],
imports: [CommonModule, ImagesRoutingModule],
})
export class ImagesRouteModule {}

View file

@ -0,0 +1,28 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Permission } from 'picsur-shared/dist/dto/permissions.dto';
import { PermissionGuard } from 'src/app/guards/permission.guard';
import { PRoutes } from 'src/app/models/dto/picsur-routes.dto';
import { ImagesComponent } from './images.component';
const routes: PRoutes = [
{
path: '',
pathMatch: 'full',
redirectTo: '0',
},
{
path: ':page',
component: ImagesComponent,
canActivate: [PermissionGuard],
data: {
permissions: [Permission.ImageUpload],
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ImagesRoutingModule {}

View file

@ -1,10 +1,17 @@
import { Injectable } from '@angular/core';
import { ImageUploadResponse } from 'picsur-shared/dist/dto/api/image-manage.dto';
import {
ImageDeleteRequest,
ImageDeleteResponse,
ImageListRequest,
ImageListResponse,
ImageUploadResponse
} from 'picsur-shared/dist/dto/api/image-manage.dto';
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
import { ImageLinks } from 'picsur-shared/dist/dto/image-links.dto';
import { Mime2Ext } from 'picsur-shared/dist/dto/mimes.dto';
import { EImage } from 'picsur-shared/dist/entities/image.entity';
import { AsyncFailable } from 'picsur-shared/dist/types';
import { Open } from 'picsur-shared/dist/types/failable';
import { Fail, HasFailed, Open } from 'picsur-shared/dist/types/failable';
import { ImageUploadRequest } from '../../models/dto/image-upload-request.dto';
import { ApiService } from './api.service';
@ -28,6 +35,53 @@ export class ImageService {
return await this.api.get(ImageMetaResponse, `/i/meta/${image}`);
}
public async ListImages(
count: number,
page: number,
userID?: string
): AsyncFailable<EImage[]> {
const result = await this.api.post(
ImageListRequest,
ImageListResponse,
'/api/image/list',
{
count,
page,
user_id: userID,
}
);
return Open(result, 'images');
}
public async DeleteImages(
images: string[]
): AsyncFailable<ImageDeleteResponse> {
return await this.api.post(
ImageDeleteRequest,
ImageDeleteResponse,
'/api/image/delete',
{
ids: images,
}
);
}
public async DeleteImage(image: string): AsyncFailable<EImage> {
const result = await this.DeleteImages([image]);
if (HasFailed(result)) return result;
if (result.images.length !== 1) {
return Fail(
`Image ${image} was not deleted, probably lacking permissions`
);
}
return result.images[0];
}
// Non api calls
public GetImageURL(image: string, mime: string | null): string {
const baseURL = window.location.protocol + '//' + window.location.host;
const extension = mime !== null ? Mime2Ext(mime) ?? 'error' : null;

View file

@ -1,23 +1,77 @@
{
"folders": [
{
"name": "shared",
"path": "shared"
},
{
"name": "backend",
"path": "backend"
},
{
"name": "frontend",
"path": "frontend"
},
{
"name": "Picsur-Monorepo",
"path": "."
}
],
"settings": {
"vsicons.presets.angular": true
}
"folders": [
{
"name": "shared",
"path": "shared"
},
{
"name": "backend",
"path": "backend"
},
{
"name": "frontend",
"path": "frontend"
},
{
"name": "support",
"path": "support"
}
],
"tasks": {
"version": "2.0.0",
"tasks": [
{
"label": "Start full",
"dependsOn": [
"Start backend",
"Start frontend",
"Start postgres",
"Start shared"
],
"dependsOrder": "parallel",
"isBackground": true,
"group": "build"
},
{
"type": "shell",
"label": "Start shared",
"command": "yarn start",
"options": {
"cwd": "${workspaceFolder:shared}"
},
"group": "build"
},
{
"type": "shell",
"label": "Start backend",
"command": "yarn start:dev",
"options": {
"cwd": "${workspaceFolder:backend}"
},
"group": "build"
},
{
"type": "shell",
"label": "Start frontend",
"command": "yarn watch",
"options": {
"cwd": "${workspaceFolder:frontend}"
},
"group": "build"
},
{
"type": "shell",
"label": "Start postgres",
"command": "podman-compose -f ./dev.docker-compose.yml stop; podman-compose -f ./dev.docker-compose.yml up",
"options": {
"cwd": "${workspaceFolder:support}"
},
"group": "build"
}
]
},
"settings": {
"vsicons.presets.angular": true,
"skipRefreshExplorerOnWindowFocus": true
}
}

View file

@ -20,8 +20,8 @@ export class ImageListRequest extends createZodDto(ImageListRequestSchema) {}
export const ImageListResponseSchema = z.object({
images: z.array(EImageSchema),
count: IsPosInt(),
page: IsPosInt(),
pages: IsPosInt(),
});
export class ImageListResponse extends createZodDto(ImageListResponseSchema) {}
@ -36,7 +36,6 @@ export class ImageDeleteRequest extends createZodDto(
export const ImageDeleteResponseSchema = z.object({
images: z.array(EImageSchema),
count: IsPosInt(),
});
export class ImageDeleteResponse extends createZodDto(
ImageDeleteResponseSchema,

View file

@ -16,8 +16,8 @@ export class UserListRequest extends createZodDto(UserListRequestSchema) {}
export const UserListResponseSchema = z.object({
users: z.array(EUserSchema),
count: IsPosInt(),
page: IsPosInt(),
pages: IsPosInt(),
});
export class UserListResponse extends createZodDto(UserListResponseSchema) {}

View file

@ -0,0 +1,7 @@
export interface FindResult<T> {
results: T[];
totalResults: number;
page: number;
pages: number;
}