9 Commits

Author SHA1 Message Date
dependabot[bot]
228ae60cfb Bump xml2js and typeorm in /backend
Removes [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js). It's no longer used after updating ancestor dependency [typeorm](https://github.com/typeorm/typeorm). These dependencies need to be updated together.


Removes `xml2js`

Updates `typeorm` from 0.2.45 to 0.3.20
- [Release notes](https://github.com/typeorm/typeorm/releases)
- [Changelog](https://github.com/typeorm/typeorm/blob/master/CHANGELOG.md)
- [Commits](https://github.com/typeorm/typeorm/compare/0.2.45...0.3.20)

---
updated-dependencies:
- dependency-name: xml2js
  dependency-type: indirect
- dependency-name: typeorm
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-05 16:10:15 +00:00
e82b753dfb some updates 2024-04-05 18:08:57 +02:00
45decc60a7 fix failed login hanging bug 2023-07-30 21:09:52 +02:00
fe58b33657 update node in circleci 2023-07-30 18:25:31 +02:00
503d42121e more type safety with zod 2023-07-30 18:25:31 +02:00
c114a72619 run typesync 2023-07-29 22:25:53 +02:00
614ac4c802 import fix in frontend (how did it work??) 2023-07-29 13:22:51 +02:00
f16720f13e fix circleci (first try?) 2023-07-29 13:12:09 +02:00
faa0aa62c8 some cleanup, split backend and frontend 2023-07-29 13:04:26 +02:00
127 changed files with 29080 additions and 23737 deletions

View File

@@ -2,7 +2,7 @@ version: 2
jobs: jobs:
test-backend: test-backend:
docker: docker:
- image: cimg/node:14.20 - image: cimg/node:16.20
- image: cimg/mariadb:10.8 - image: cimg/mariadb:10.8
environment: environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "true" MYSQL_ALLOW_EMPTY_PASSWORD: "true"
@@ -14,35 +14,37 @@ jobs:
working_directory: ~/photos working_directory: ~/photos
steps: steps:
# The checkout is INTO the working directory!!!
- checkout - checkout
- restore_cache: - restore_cache:
keys: keys:
- backend-dependencies-{{ checksum "package.json" }} - backend-dependencies-{{ checksum "backend/package.json" }}
- run: - run:
name: install backend deps name: install backend deps
command: npm i command: cd backend && npm i
- save_cache: - save_cache:
paths: paths:
- node_modules - backend/node_modules
key: backend-dependencies-{{ checksum "package.json" }} key: backend-dependencies-{{ checksum "backend/package.json" }}
- run: - run:
name: test backend name: test backend
command: npm test command: cd backend && npm test
- store_test_results: - store_test_results:
path: backend-report.xml path: ~/photos/backend/backend-report.xml
test-frontend: test-frontend:
docker: docker:
- image: cimg/node:14.20 - image: cimg/node:16.20
working_directory: ~/photos working_directory: ~/photos
steps: steps:
# The checkout is INTO the working directory!!!
- checkout - checkout
- restore_cache: - restore_cache:
keys: keys:
@@ -66,11 +68,12 @@ jobs:
test-frontend-build: test-frontend-build:
docker: docker:
- image: cimg/node:14.20 - image: cimg/node:16.20
working_directory: ~/photos working_directory: ~/photos
steps: steps:
# The checkout is INTO the working directory!!!
- checkout: - checkout:
- restore_cache: - restore_cache:
@@ -96,6 +99,7 @@ jobs:
resource_class: large resource_class: large
steps: steps:
# The checkout is INTO the working directory!!!
- checkout - checkout
- run: - run:
name: log in to docker hub name: log in to docker hub
@@ -121,7 +125,7 @@ jobs:
--cache-from=type=local,src=/tmp/dockercache . --cache-from=type=local,src=/tmp/dockercache .
- run: - run:
name: prune cache name: prune cache
command: docker buildx prune --keep-storage=2gb --verbose command: docker buildx prune --keep-storage=4gb --verbose
- save_cache: - save_cache:
key: buildx-photos-circleci-{{ checksum "/tmp/dockercache/index.json" }} key: buildx-photos-circleci-{{ checksum "/tmp/dockercache/index.json" }}

View File

@@ -4,3 +4,7 @@ npm-debug.log
frontend/node_modules frontend/node_modules
frontend/npm-debug.log frontend/npm-debug.log
frontend/.parcel-cache frontend/.parcel-cache
backend/node_modules
backend/npm-debug.log
backend/.parcel-cache

5
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.DS_Store
.idea/ .idea/
node_modules/ /node_modules
build/ build/
tmp/ tmp/
temp/ temp/
@@ -13,4 +14,4 @@ ormconfig.test.json
data_test data_test
data_dev data_dev
data data
backend-report.xml backend-report.xml

View File

@@ -3,7 +3,7 @@
"typeorm" "typeorm"
], ],
"eslint.workingDirectories": [ "eslint.workingDirectories": [
".", "./backend",
"./frontend" "./frontend"
], ],
"search.exclude": { "search.exclude": {

View File

@@ -4,8 +4,8 @@ FROM node:16-bullseye as frontbuild
WORKDIR /usr/src/app/frontend WORKDIR /usr/src/app/frontend
COPY ./frontend/package*.json ./ COPY ./frontend/package*.json ./
RUN npm ci --only=production RUN npm ci --only=production
COPY ./frontend . COPY ./frontend/. .
COPY ./src/shared ../src/shared COPY ./shared ../shared
RUN npm run build && bash -O extglob -c 'rm -rfv !("dist")' RUN npm run build && bash -O extglob -c 'rm -rfv !("dist")'
WORKDIR ../ WORKDIR ../
RUN bash -O extglob -c 'rm -rfv !("frontend")' RUN bash -O extglob -c 'rm -rfv !("frontend")'
@@ -13,15 +13,17 @@ RUN bash -O extglob -c 'rm -rfv !("frontend")'
FROM node:16-alpine as backexceptwithoutfrontend FROM node:16-alpine as backexceptwithoutfrontend
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package*.json ./ COPY ./backend/package*.json ./
RUN npm ci --only=production RUN npm ci --only=production
COPY ./ ./ COPY ./backend ./
RUN rm -rfv frontend RUN rm -rfv frontend && unlink src/shared
COPY ./shared src/shared
FROM backexceptwithoutfrontend FROM backexceptwithoutfrontend
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=frontbuild /usr/src/app/frontend ./frontend COPY --from=frontbuild /usr/src/app/frontend ./frontend
COPY ./dockerentry.sh .
#ENV PORT=8080 #ENV PORT=8080
#ENV TYPEORM_HOST=localhost #ENV TYPEORM_HOST=localhost
@@ -45,6 +47,6 @@ ENV DATA_DIR=data\
#EXPOSE 8080 #EXPOSE 8080
RUN ["chmod", "+x", "dockerentry.sh"] RUN ["chmod", "+x", "./dockerentry.sh"]
CMD [ "./dockerentry.sh" ] CMD [ "./dockerentry.sh" ]

17
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
.DS_Store
.idea/
node_modules/
build/
tmp/
temp/
dist/
ormconfig.json
ormconfig.dev.json
ormconfig.test.json
.env
.directory
.history
data_test
data_dev
data
backend-report.xml

7781
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

88
backend/package.json Normal file
View File

@@ -0,0 +1,88 @@
{
"name": "photos",
"version": "0.0.1",
"scripts": {
"start": "ts-node -T -r tsconfig-paths/register src/server.ts",
"ts-node-dev": "ts-node-dev -r tsconfig-paths/register ./src/server.ts",
"test": "cross-env NODE_ENV=test mocha --timeout 15000 -r ts-node/register -r tsconfig-paths/register --reporter mocha-multi-reporters --reporter-options configFile=mocha.json 'src/tests/**/*.ts'",
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx && tsc --noEmit",
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix",
"prettier-check": "prettier src/**/*.ts --check",
"prettify": "prettier src/**/*.ts --write",
"typeorm-dev": "cross-env NODE_ENV=development ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"typeorm": "cross-env NODE_ENV=production ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js"
},
"license": "MIT",
"dependencies": {
"@koa/cors": "^5",
"@koa/router": "^12.0.0",
"bcrypt": "^5.1.0",
"class-validator": "^0.14.0",
"exifreader": "^4.13.0",
"hasha": "^5.2.2",
"jsonwebtoken": "^9.0.1",
"koa": "^2.14.2",
"koa-body": "^5.0.0",
"koa-jwt": "^4.0.4",
"koa-logger": "^3.2.1",
"koa-send": "^5.0.1",
"koa-sslify": "^5.0.1",
"koa-static": "^5.0.0",
"mime-types": "^2.1.35",
"mysql": "^2.18.1",
"sharp": "^0.32.4",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typeorm": "^0.3.20",
"typescript": "^5.1.6"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/chai": "^4.3.5",
"@types/concurrently": "^6.4.0",
"@types/deasync": "^0.1.2",
"@types/eslint": "^8.44.1",
"@types/eslint-config-prettier": "^6.11.0",
"@types/eslint-plugin-mocha": "^10.4.0",
"@types/eslint-plugin-prettier": "^3.1.0",
"@types/hasha": "^3.0.1",
"@types/jsonwebtoken": "^9.0.2",
"@types/koa": "^2.13.7",
"@types/koa-logger": "^3.1.2",
"@types/koa-send": "^4.1.3",
"@types/koa-sslify": "^4.0.3",
"@types/koa-static": "^4.0.2",
"@types/koa__cors": "^4.0.0",
"@types/koa__router": "^12.0.0",
"@types/mime-types": "^2.1.1",
"@types/mocha": "^10.0.1",
"@types/mysql": "^2.15.21",
"@types/prettier": "^2.7.3",
"@types/sharp": "^0.31.1",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"chai": "^4.3.7",
"concurrently": "^8.2.0",
"cross-env": "^7.0.3",
"eslint": "^8.46.0",
"eslint-config-prettier": "^8.9.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-prettier": "^5.0.0",
"husky": "^8.0.3",
"mocha": "^10.2.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"prettier": "^3.0.0",
"prettier-eslint": "^15.0.1",
"supertest": "^6.3.3",
"ts-node-dev": "^2"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint-all && npm run prettier-check"
}
}
}

View File

@@ -15,8 +15,15 @@ import { config, EnvType } from "~config";
import { userRouter } from "~routes/users"; import { userRouter } from "~routes/users";
import { devRouter } from "~routes/dev"; import { devRouter } from "~routes/dev";
import { photosRouter } from "~routes/photos"; import { photosRouter } from "~routes/photos";
import { TAPIErrorResponse, TUserJWT } from "~shared/types";
export const app = new Koa(); export interface IAppState extends Koa.DefaultState {
user?: TUserJWT;
}
export interface IAppContext extends Koa.DefaultContext {}
export const app = new Koa<IAppState, IAppContext>();
const tmpPath = path.join(config.dataDir, "tmp"); const tmpPath = path.join(config.dataDir, "tmp");
@@ -35,6 +42,7 @@ app.use(
if (config.https) { if (config.https) {
app.use(sslify({ resolver: xForwardedProtoResolver })); app.use(sslify({ resolver: xForwardedProtoResolver }));
} }
app.use( app.use(
jwt({ jwt({
secret: config.jwtSecret, secret: config.jwtSecret,
@@ -93,6 +101,6 @@ app.on("error", (err, ctx) => {
console.log(err); console.log(err);
ctx.body = { ctx.body = {
error: err.message, error: err.message,
data: false, data: null,
}; } as TAPIErrorResponse;
}); });

View File

@@ -52,7 +52,7 @@ export async function getConfigValue(key: ConfigKey): Promise<string> {
} }
try { try {
const pair = await Config.findOneOrFail({ key }); const pair = await Config.findOneOrFail({ key }, {});
return pair.value; return pair.value;
} catch (e) { } catch (e) {
return defaultValues[key]; return defaultValues[key];
@@ -67,7 +67,7 @@ export async function setConfigValue(
throw new Error(`${key} is not valid config key`); throw new Error(`${key} is not valid config key`);
} }
let pair = await Config.findOne({ key }); let pair = await Config.findOne({ key }, {});
if (!pair) { if (!pair) {
pair = new Config(key, val); pair = new Config(key, val);
} else { } else {

View File

@@ -1,11 +1,10 @@
import * as path from "path"; import * as path from "path";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import * as mime from "mime-types"; import * as mime from "mime-types";
import { constants as fsConstants } from "fs";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
import { TPhotoReqJSON, TPhotoJSON } from "~/shared/types";
import { import {
AfterRemove,
BaseEntity, BaseEntity,
BeforeInsert, BeforeInsert,
BeforeRemove, BeforeRemove,
@@ -18,37 +17,18 @@ import {
} from "typeorm"; } from "typeorm";
import { User } from "./User"; import { User } from "./User";
import { import {
isAlphanumeric,
IsAlphanumeric, IsAlphanumeric,
IsHash, IsHash,
IsIn,
IsMimeType, IsMimeType,
isNumber, isNumber,
Length,
Matches, Matches,
validateOrReject, validateOrReject,
} from "class-validator"; } from "class-validator";
import { config } from "~config"; import { config } from "~config";
import { fileCheck, getShotDate, resizeToJpeg } from "~util"; import { fileCheck, getShotDate, resizeToJpeg } from "~util";
export interface IPhotoJSON {
id: number;
user: number;
hash: string;
size: string;
format: string;
createdAt: number;
editedAt: number;
shotAt: number;
uploaded: boolean;
}
export interface IPhotoReqJSON extends IPhotoJSON {
accessToken: string;
}
export const thumbSizes = ["512", "1024", "2048", "original"]; export const thumbSizes = ["512", "1024", "2048", "original"];
export type ThumbSize = typeof thumbSizes[number]; export type ThumbSize = (typeof thumbSizes)[number];
@Entity() @Entity()
@Index(["hash", "size", "user"], { unique: true }) @Index(["hash", "size", "user"], { unique: true })
@@ -232,7 +212,7 @@ export class Photo extends BaseEntity {
} }
} }
public async toJSON(): Promise<IPhotoJSON> { public async toJSON(): Promise<TPhotoJSON> {
if (!isNumber(this.user.id)) { if (!isNumber(this.user.id)) {
throw new Error("User not loaded"); throw new Error("User not loaded");
} }
@@ -252,7 +232,7 @@ export class Photo extends BaseEntity {
}; };
} }
public async toReqJSON(): Promise<IPhotoReqJSON> { public async toReqJSON(): Promise<TPhotoReqJSON> {
const token = await this.getJWTToken(); const token = await this.getJWTToken();
return { ...(await this.toJSON()), accessToken: token }; return { ...(await this.toJSON()), accessToken: token };
} }

View File

@@ -2,9 +2,10 @@ import * as bcrypt from "bcrypt";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
import * as path from "path"; import * as path from "path";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import { TUserJSON, TUserAuthJSON } from "~/shared/types";
import { import {
AfterInsert, AfterInsert,
AfterRemove,
BaseEntity, BaseEntity,
BeforeInsert, BeforeInsert,
BeforeRemove, BeforeRemove,
@@ -17,25 +18,7 @@ import {
} from "typeorm"; } from "typeorm";
import { config } from "../config"; import { config } from "../config";
import { Photo } from "./Photo"; import { Photo } from "./Photo";
import { import { IsAlphanumeric, IsEmail, validateOrReject } from "class-validator";
IsAlphanumeric,
IsBase32,
IsBase64,
IsEmail,
IsHash,
validateOrReject,
} from "class-validator";
export type IUserJSON = Pick<User, "id" | "username" | "isAdmin">;
export interface IUserJWT extends IUserJSON {
ext: number;
iat: number;
}
export interface IUserAuthJSON extends IUserJSON {
jwt: string;
}
@Entity() @Entity()
export class User extends BaseEntity { export class User extends BaseEntity {
@@ -100,12 +83,12 @@ export class User extends BaseEntity {
return validateOrReject(this); return validateOrReject(this);
} }
public toJSON(): IUserJSON { public toJSON(): TUserJSON {
const { id, username, isAdmin } = this; const { id, username, isAdmin } = this;
return { id, username, isAdmin }; return { id, username, isAdmin };
} }
public toAuthJSON(): IUserAuthJSON { public toAuthJSON(): TUserAuthJSON {
const json = this.toJSON(); const json = this.toJSON();
return { ...json, jwt: this.toJWT() }; return { ...json, jwt: this.toJWT() };
} }

14
backend/src/routes/dev.ts Normal file
View File

@@ -0,0 +1,14 @@
import * as Router from "@koa/router";
import { Photo } from "~entity/Photo";
import { User } from "~entity/User";
import { IAppContext, IAppState } from "~app";
export const devRouter = new Router<IAppState, IAppContext>();
type ContextType = Parameters<Parameters<(typeof devRouter)["post"]>["2"]>["0"];
devRouter.get("/dev/clean", async (ctx: ContextType) => {
await Photo.remove(await Photo.find());
await User.remove(await User.find());
ctx.body = { success: true };
});

View File

@@ -1,53 +1,58 @@
import * as Router from "@koa/router"; import * as Router from "@koa/router";
import { IPhotoReqJSON, Photo } from "~entity/Photo"; import { Photo } from "~entity/Photo";
import { User } from "~entity/User"; import {
import { IAPIResponse, IPhotosListPagination } from "~/shared/types"; PhotoJSON,
import * as fs from "fs/promises"; PhotosDeleteBody,
import send = require("koa-send"); PhotosListPagination,
PhotosNewPostBody,
TPhotoByIDDeleteRespBody,
TPhotoReqJSON,
TPhotosByIDGetRespBody,
TPhotosDeleteRespBody,
TPhotosGetShowTokenByIDRespBody,
TPhotosListRespBody,
TPhotosNewRespBody,
TPhotosUploadRespBody,
} from "~/shared/types";
import { getHash, getSize } from "~util"; import { getHash, getSize } from "~util";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
import { config } from "~config"; import { config } from "~config";
import { ValidationError } from "class-validator"; import { ValidationError } from "class-validator";
import { In } from "typeorm"; import { In } from "typeorm";
import { IAppContext, IAppState } from "~app";
import send = require("koa-send");
export const photosRouter = new Router(); export const photosRouter = new Router<IAppState, IAppContext>();
export interface IPhotosNewPostBody { // Typescript requires explicit type annotations for CFA......
hash: string | undefined; type ContextType = Parameters<
size: string | undefined; Parameters<(typeof photosRouter)["post"]>["2"]
format: string | undefined; >["0"];
}
export type IPhotosNewRespBody = IAPIResponse<IPhotoReqJSON>; photosRouter.post("/photos/new", async (ctx: ContextType) => {
photosRouter.post("/photos/new", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
} }
const { user } = ctx.state; const { user } = ctx.state;
const body = ctx.request.body as IPhotosNewPostBody; const body = PhotosNewPostBody.parse(ctx.request.body);
const { hash, size, format } = body; const { hash, size, format } = body;
if (!(hash && size && format)) { const photo = Photo.create({ user, hash, size, format });
ctx.throw(400);
return;
}
const photo = new Photo(user, hash, size, format);
try { try {
await photo.save(); await photo.save();
} catch (e) { } catch (e) {
if (e.code === "ER_DUP_ENTRY") { if (e.code === "ER_DUP_ENTRY") {
const photo = await Photo.findOne({ hash, size, user }); const photo = await Photo.findOne({ hash, size, user }, {});
if (!photo) { if (!photo) {
ctx.throw(404); ctx.throw(404);
return;
} }
ctx.body = { ctx.body = {
error: false, error: false,
data: await photo.toReqJSON(), data: await photo.toReqJSON(),
} as IPhotosNewRespBody; } as TPhotosNewRespBody;
return; return;
} }
if ( if (
@@ -55,7 +60,6 @@ photosRouter.post("/photos/new", async (ctx) => {
(Array.isArray(e) && e.some((e) => e instanceof ValidationError)) (Array.isArray(e) && e.some((e) => e instanceof ValidationError))
) { ) {
ctx.throw(400); ctx.throw(400);
return;
} }
console.log(e); console.log(e);
ctx.throw(500); ctx.throw(500);
@@ -64,11 +68,10 @@ photosRouter.post("/photos/new", async (ctx) => {
ctx.body = { ctx.body = {
error: false, error: false,
data: await photo.toReqJSON(), data: await photo.toReqJSON(),
} as IPhotosNewRespBody; } as TPhotosNewRespBody;
}); });
export type IPhotosUploadRespBody = IAPIResponse<IPhotoReqJSON>; photosRouter.post("/photos/upload/:id", async (ctx: ContextType) => {
photosRouter.post("/photos/upload/:id", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
} }
@@ -79,32 +82,28 @@ photosRouter.post("/photos/upload/:id", async (ctx) => {
if (!id) { if (!id) {
ctx.throw(400); ctx.throw(400);
return;
} }
const { user } = ctx.state; const { user } = ctx.state;
const photo = await Photo.findOne({ id: parseInt(id), user }); const photo = await Photo.findOne({ id: parseInt(id), user }, {});
if (!photo) { if (!photo) {
ctx.throw(404); ctx.throw(404);
return;
} }
if (!ctx.request.files || Object.keys(ctx.request.files).length === 0) { if (!ctx.request.files || Object.keys(ctx.request.files).length === 0) {
ctx.throw(400, "No file"); ctx.throw(400, "No file");
return;
} }
if (photo.uploaded) { if (photo.uploaded) {
ctx.throw(400, "Already uploaded"); ctx.throw(400, "Already uploaded");
return;
} }
if (ctx.request.files) { if (ctx.request.files) {
const files = ctx.request.files; const files = ctx.request.files;
if (Object.keys(files).length > 1) { if (Object.keys(files).length > 1) {
ctx.throw(400, "Too many files"); ctx.throw(400, "Too many files");
return;
} }
const file = Object.values(files)[0]; const file = Object.values(files)[0];
if (Array.isArray(file)) { if (Array.isArray(file)) {
throw "more than one file uploaded"; throw "more than one file uploaded";
@@ -115,7 +114,6 @@ photosRouter.post("/photos/upload/:id", async (ctx) => {
if (photoHash !== photo.hash || photoSize !== photo.size) { if (photoHash !== photo.hash || photoSize !== photo.size) {
ctx.throw(400, "Wrong photo"); ctx.throw(400, "Wrong photo");
return;
} }
try { try {
@@ -129,54 +127,53 @@ photosRouter.post("/photos/upload/:id", async (ctx) => {
ctx.body = { ctx.body = {
error: false, error: false,
data: await photo.toReqJSON(), data: await photo.toReqJSON(),
} as IPhotosUploadRespBody; } as TPhotosUploadRespBody;
}); });
/** /**
export interface IPhotosByIDPatchBody { export interface TPhotosByIDPatchBody {
} }
export type IPhotosByIDPatchRespBody = IAPIResponse<IPhotoReqJSON>; export type TPhotosByIDPatchRespBody = IAPIResponse<TPhotoReqJSON>;
photosRouter.patch("/photos/byID/:id", async (ctx) => { photosRouter.patch("/photos/byID/:id", async (ctx: ContextType) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
return; return;
} }
const { user } = ctx.state;
const { id } = ctx.params as {
id: number | undefined;
};
if (!id) {
ctx.throw(400);
return;
}
const photo = await Photo.findOne({ id, user });
if (!photo) {
ctx.throw(404);
return;
}
// TODO: Some actual editing
try {
photo.editedAt = new Date();
await photo.save();
} catch (e) {
ctx.throw(400);
}
ctx.body = {
error: false,
data: photo.toReqJSON(),
};
});
*/
export type IPhotosListRespBody = IAPIResponse<IPhotoReqJSON[]>; const { user } = ctx.state;
photosRouter.get("/photos/list", async (ctx) => { const { id } = ctx.params as {
id: number | undefined;
};
if (!id) {
ctx.throw(400);
return;
}
const photo = await Photo.findOne({ id, user },{});
if (!photo) {
ctx.throw(404);
return;
}
// TODO: Some actual editing
try {
photo.editedAt = new Date();
await photo.save();
} catch (e) {
ctx.throw(400);
}
ctx.body = {
error: false,
data: photo.toReqJSON(),
};
});
*/
photosRouter.get("/photos/list", async (ctx: ContextType) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
} }
@@ -196,8 +193,8 @@ photosRouter.get("/photos/list", async (ctx) => {
skip = parseInt(skip); skip = parseInt(skip);
} }
if (!num || num > IPhotosListPagination) { if (!num || num > PhotosListPagination) {
num = IPhotosListPagination; num = PhotosListPagination;
} }
const photos = await Photo.find({ const photos = await Photo.find({
@@ -207,18 +204,17 @@ photosRouter.get("/photos/list", async (ctx) => {
order: { shotAt: "DESC" }, order: { shotAt: "DESC" },
}); });
const photosList: IPhotoReqJSON[] = await Promise.all( const photosList: TPhotoReqJSON[] = await Promise.all(
photos.map(async (photo) => await photo.toReqJSON()), photos.map(async (photo) => await photo.toReqJSON()),
); );
ctx.body = { ctx.body = {
error: false, error: false,
data: photosList, data: photosList,
} as IPhotosListRespBody; } as TPhotosListRespBody;
}); });
export type IPhotosByIDGetRespBody = IAPIResponse<IPhotoReqJSON>; photosRouter.get("/photos/byID/:id", async (ctx: ContextType) => {
photosRouter.get("/photos/byID/:id", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
} }
@@ -229,25 +225,23 @@ photosRouter.get("/photos/byID/:id", async (ctx) => {
if (!id) { if (!id) {
ctx.throw(400); ctx.throw(400);
return;
} }
const { user } = ctx.state; const { user } = ctx.state;
const photo = await Photo.findOne({ id: parseInt(id), user }); const photo = await Photo.findOne({ id: parseInt(id), user }, {});
if (!photo) { if (!photo) {
ctx.throw(404); ctx.throw(404);
return;
} }
ctx.body = { ctx.body = {
error: false, error: false,
data: await photo.toReqJSON(), data: await photo.toReqJSON(),
} as IPhotosByIDGetRespBody; } as TPhotosByIDGetRespBody;
}); });
photosRouter.get("/photos/showByID/:id/:token", async (ctx) => { photosRouter.get("/photos/showByID/:id/:token", async (ctx: ContextType) => {
const { id, token } = ctx.params as { const { id, token } = ctx.params as {
id: string | undefined; id: string | undefined;
token: string | undefined; token: string | undefined;
@@ -255,26 +249,27 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
if (!(id && token)) { if (!(id && token)) {
ctx.throw(400); ctx.throw(400);
return;
} }
try { try {
jwt.verify(token, config.jwtSecret) as IPhotoReqJSON; jwt.verify(token, config.jwtSecret);
} catch (e) { } catch (e) {
ctx.throw(401); ctx.throw(401);
} }
const photoReqJSON = jwt.decode(token) as IPhotoReqJSON; const photoReqJSON = PhotoJSON.parse(jwt.decode(token));
const { user } = photoReqJSON; const { user } = photoReqJSON;
const photo = await Photo.findOne({ const photo = await Photo.findOne(
id: parseInt(id), {
user: { id: user }, id: parseInt(id),
}); user: { id: user },
},
{},
);
if (!photo) { if (!photo) {
ctx.throw(404); ctx.throw(404);
return;
} }
if ( if (
@@ -289,7 +284,7 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
await send(ctx, await photo.getReadyPath("original")); await send(ctx, await photo.getReadyPath("original"));
}); });
photosRouter.get("/photos/showByID/:id", async (ctx) => { photosRouter.get("/photos/showByID/:id", async (ctx: ContextType) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
} }
@@ -300,16 +295,14 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => {
if (!id) { if (!id) {
ctx.throw(400); ctx.throw(400);
return;
} }
const { user } = ctx.state; const { user } = ctx.state;
const photo = await Photo.findOne({ id: parseInt(id), user }); const photo = await Photo.findOne({ id: parseInt(id), user }, {});
if (!photo) { if (!photo) {
ctx.throw(404); ctx.throw(404);
return;
} }
if ( if (
@@ -324,9 +317,7 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => {
await send(ctx, await photo.getReadyPath("original")); await send(ctx, await photo.getReadyPath("original"));
}); });
export type IPhotoShowToken = string; photosRouter.get("/photos/getShowByIDToken/:id", async (ctx: ContextType) => {
export type IPhotosGetShowTokenByID = IAPIResponse<IPhotoShowToken>;
photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
} }
@@ -337,24 +328,21 @@ photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => {
if (!id) { if (!id) {
ctx.throw(400); ctx.throw(400);
return;
} }
const { user } = ctx.state; const { user } = ctx.state;
const photo = await Photo.findOne({ id: parseInt(id), user }); const photo = await Photo.findOne({ id: parseInt(id), user }, {});
if (!photo) { if (!photo) {
ctx.throw(404); ctx.throw(404);
return;
} }
const token = await photo.getJWTToken(); const token = await photo.getJWTToken();
ctx.body = { error: false, data: token } as IPhotosGetShowTokenByID; ctx.body = { error: false, data: token } as TPhotosGetShowTokenByIDRespBody;
}); });
export type IPhotoByIDDeleteRespBody = IAPIResponse<boolean>; photosRouter.delete("/photos/byID/:id", async (ctx: ContextType) => {
photosRouter.delete("/photos/byID/:id", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
} }
@@ -365,16 +353,14 @@ photosRouter.delete("/photos/byID/:id", async (ctx) => {
if (!id) { if (!id) {
ctx.throw(400); ctx.throw(400);
return;
} }
const { user } = ctx.state; const { user } = ctx.state;
const photo = await Photo.findOne({ id: parseInt(id), user }); const photo = await Photo.findOne({ id: parseInt(id), user }, {});
if (!photo) { if (!photo) {
ctx.throw(404); ctx.throw(404);
return;
} }
await photo.remove(); await photo.remove();
@@ -382,27 +368,17 @@ photosRouter.delete("/photos/byID/:id", async (ctx) => {
ctx.body = { ctx.body = {
error: false, error: false,
data: true, data: true,
} as IPhotoByIDDeleteRespBody; } as TPhotoByIDDeleteRespBody;
}); });
export interface IPhotosDeleteBody { photosRouter.post("/photos/delete", async (ctx: ContextType) => {
photos: IPhotoReqJSON[];
}
export type IPhotosDeleteRespBody = IAPIResponse<boolean>;
photosRouter.post("/photos/delete", async (ctx) => {
if (!ctx.state.user) { if (!ctx.state.user) {
ctx.throw(401); ctx.throw(401);
} }
const body = ctx.request.body as IPhotosDeleteBody; const body = PhotosDeleteBody.parse(ctx.request.body);
const { photos } = body; const { photos } = body;
if (!photos || !Array.isArray(photos) || photos.length == 0) {
ctx.throw(400);
return;
}
const { user } = ctx.state; const { user } = ctx.state;
try { try {
await Photo.delete({ await Photo.delete({
@@ -413,11 +389,11 @@ photosRouter.post("/photos/delete", async (ctx) => {
ctx.body = { ctx.body = {
error: false, error: false,
data: true, data: true,
} as IPhotosDeleteRespBody; } as TPhotosDeleteRespBody;
} catch (e) { } catch (e) {
ctx.body = { ctx.body = {
data: null, data: null,
error: "Internal server error", error: "Internal server error",
} as IPhotosDeleteRespBody; } as TPhotosDeleteRespBody;
} }
}); });

121
backend/src/routes/users.ts Normal file
View File

@@ -0,0 +1,121 @@
import * as Router from "@koa/router";
import { ConfigKey, getConfigValue } from "~entity/Config";
import { User } from "~entity/User";
import {
TUserEditRespBody,
TUserGetRespBody,
TUserLoginRespBody,
TUserSignupRespBody,
UserEditBody,
UserLoginBody,
UserSignupBody,
} from "~/shared/types";
import { IAppContext, IAppState } from "~app";
export const userRouter = new Router<IAppState, IAppContext>();
type ContextType = Parameters<
Parameters<(typeof userRouter)["post"]>["2"]
>["0"];
userRouter.get("/users/user", async (ctx: ContextType) => {
if (!ctx.state.user) {
ctx.throw(401);
}
const jwt = ctx.state.user;
const user = await User.findOne(jwt.id, {});
if (!user) {
ctx.throw(401);
}
ctx.body = { error: false, data: user.toAuthJSON() } as TUserGetRespBody;
});
userRouter.post("/users/login", async (ctx: ContextType) => {
const request = ctx.request;
if (!request.body) {
ctx.throw(400);
}
const { username, password } = UserLoginBody.parse(request.body);
const user = await User.findOne({ username }, {});
if (!user || !(await user.verifyPassword(password))) {
ctx.throw(404, "User not found");
}
ctx.body = { error: false, data: user.toAuthJSON() } as TUserLoginRespBody;
});
userRouter.post("/users/signup", async (ctx: ContextType) => {
const request = ctx.request;
if (!request.body) {
ctx.throw(400);
}
const { username, password, email } = UserSignupBody.parse(request.body);
const user = new User(username, email);
const users = await User.find();
if (users.length === 0) {
user.isAdmin = true;
}
if ((await getConfigValue(ConfigKey.signupAllowed)) !== "yes") {
if (users.length !== 0) {
ctx.throw(400, "Signups not allowed");
}
}
await user.setPassword(password);
try {
await user.save();
} catch (e) {
if (e.code === "ER_DUP_ENTRY") {
ctx.throw(400, "User already exists");
}
console.log(e);
ctx.throw(500);
}
ctx.body = { error: false, data: user.toAuthJSON() } as TUserSignupRespBody;
});
userRouter.post("/users/edit", async (ctx: ContextType) => {
if (!ctx.state.user) {
ctx.throw(401);
}
const jwt = ctx.state.user;
const user = await User.findOne(jwt.id, {});
const request = ctx.request;
if (!user) {
ctx.throw(401);
}
if (!request.body) {
ctx.throw(400);
}
const { password } = UserEditBody.parse(request.body);
if (!password) {
ctx.throw(400);
}
await user.setPassword(password);
try {
await user.save();
} catch (e) {
console.log(e);
ctx.throw(500);
}
ctx.body = { error: false, data: user.toAuthJSON() } as TUserEditRespBody;
});

View File

@@ -1,8 +1,8 @@
import { Connection } from "typeorm"; import { Connection } from "typeorm";
import { Config, ConfigKey, setConfigValue } from "~entity/Config"; import { Config, ConfigKey, setConfigValue } from "~entity/Config";
import { app } from "./app"; import { app } from "~app";
import { config } from "./config"; import { config } from "./config";
import { connect } from "./config/database"; import { connect } from "~config/database";
async function readConfig() { async function readConfig() {
if (process.env.SIGNUP_ALLOWED) { if (process.env.SIGNUP_ALLOWED) {

1
backend/src/shared Symbolic link
View File

@@ -0,0 +1 @@
../../shared

View File

@@ -3,12 +3,13 @@ import { connect } from "config/database";
import * as request from "supertest"; import * as request from "supertest";
import { getConnection } from "typeorm"; import { getConnection } from "typeorm";
import { app } from "~app"; import { app } from "~app";
import { Photo, IPhotoReqJSON } from "~entity/Photo"; import { Photo } from "~entity/Photo";
import { import {
IPhotosDeleteBody, TPhotoReqJSON,
IPhotosListRespBody, TPhotosDeleteBody,
IPhotosNewPostBody, TPhotosListRespBody,
} from "~routes/photos"; TPhotosNewPostBody,
} from "~shared/types";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import { constants as fsConstants } from "fs"; import { constants as fsConstants } from "fs";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
@@ -30,7 +31,6 @@ import {
prepareMetadata, prepareMetadata,
seedDB, seedDB,
} from "./util"; } from "./util";
import { sleep } from "deasync";
import { config } from "~config"; import { config } from "~config";
const callback = app.callback(); const callback = app.callback();
@@ -61,7 +61,7 @@ describe("photos", function () {
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const photo = response.body.data as IPhotoReqJSON; const photo = response.body.data as TPhotoReqJSON;
const usedPhoto = await seed.dogPhoto.toReqJSON(); const usedPhoto = await seed.dogPhoto.toReqJSON();
@@ -86,7 +86,7 @@ describe("photos", function () {
Authorization: `Bearer ${seed.user2.toJWT()}`, Authorization: `Bearer ${seed.user2.toJWT()}`,
}) })
.expect(200); .expect(200);
expect(parseInt(response.header["content-length"])).to.equal( expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogFileSize, dogFileSize,
); );
}); });
@@ -98,12 +98,12 @@ describe("photos", function () {
Authorization: `Bearer ${seed.user2.toJWT()}`, Authorization: `Bearer ${seed.user2.toJWT()}`,
}) })
.expect(200); .expect(200);
expect(parseInt(response.header["content-length"])).to.equal( expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogFileSize, dogFileSize,
); );
await fs.unlink(await seed.dogPhoto.getReadyPath("original")); await fs.unlink(await seed.dogPhoto.getReadyPath("original"));
const response2 = await request(callback) await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}`) .get(`/photos/showByID/${seed.dogPhoto.id}`)
.set({ .set({
Authorization: `Bearer ${seed.user2.toJWT()}`, Authorization: `Bearer ${seed.user2.toJWT()}`,
@@ -121,15 +121,15 @@ describe("photos", function () {
}) })
.expect(200); .expect(200);
const dogSmallThumbSize = ( const dogSmallThumbSize = (
await fs.stat(await seed.dogPhoto.getThumbPath("512")) await fs.stat(seed.dogPhoto.getThumbPath("512"))
).size; ).size;
expect(parseInt(response.header["content-length"])).to.equal( expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogSmallThumbSize, dogSmallThumbSize,
); );
await fs.unlink(await seed.dogPhoto.getReadyPath("512")); await fs.unlink(await seed.dogPhoto.getReadyPath("512"));
await fs.unlink(await seed.dogPhoto.getReadyPath("original")); await fs.unlink(await seed.dogPhoto.getReadyPath("original"));
const response2 = await request(callback) await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}?size=512`) .get(`/photos/showByID/${seed.dogPhoto.id}?size=512`)
.set({ .set({
Authorization: `Bearer ${seed.user2.toJWT()}`, Authorization: `Bearer ${seed.user2.toJWT()}`,
@@ -147,7 +147,7 @@ describe("photos", function () {
Authorization: `Bearer ${seed.user2.toJWT()}`, Authorization: `Bearer ${seed.user2.toJWT()}`,
}) })
.expect(200); .expect(200);
expect(parseInt(response.header["content-length"])).to.be.lessThan( expect(parseInt(response.get("content-length") ?? "")).to.be.lessThan(
dogFileSize, dogFileSize,
); );
}); });
@@ -160,23 +160,23 @@ describe("photos", function () {
}) })
.expect(200); .expect(200);
const dogSmallThumbSize = ( const dogSmallThumbSize = (
await fs.stat(await seed.dogPhoto.getThumbPath("512")) await fs.stat(seed.dogPhoto.getThumbPath("512"))
).size; ).size;
expect(parseInt(response.header["content-length"])).to.equal( expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogSmallThumbSize, dogSmallThumbSize,
); );
await fs.unlink(seed.dogPhoto.getThumbPath("512")); await fs.unlink(seed.dogPhoto.getThumbPath("512"));
const response2 = await request(callback) await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}?size=512`) .get(`/photos/showByID/${seed.dogPhoto.id}?size=512`)
.set({ .set({
Authorization: `Bearer ${seed.user2.toJWT()}`, Authorization: `Bearer ${seed.user2.toJWT()}`,
}) })
.expect(200); .expect(200);
const dogSmallThumbSize2 = ( const dogSmallThumbSize2 = (
await fs.stat(await seed.dogPhoto.getThumbPath("512")) await fs.stat(seed.dogPhoto.getThumbPath("512"))
).size; ).size;
expect(parseInt(response.header["content-length"])).to.equal( expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogSmallThumbSize2, dogSmallThumbSize2,
); );
}); });
@@ -189,7 +189,7 @@ describe("photos", function () {
}) })
.expect(200); .expect(200);
const listRespBody = listResp.body as IPhotosListRespBody; const listRespBody = listResp.body as TPhotosListRespBody;
if (listRespBody.error !== false) { if (listRespBody.error !== false) {
expect(listResp.body.error).to.be.false; expect(listResp.body.error).to.be.false;
@@ -202,7 +202,7 @@ describe("photos", function () {
const listAnyResp = await request(callback) const listAnyResp = await request(callback)
.get(`/photos/showByID/${photos[0].id}/${photos[0].accessToken}`) .get(`/photos/showByID/${photos[0].id}/${photos[0].accessToken}`)
.expect(200); .expect(200);
expect(parseInt(listAnyResp.header["content-length"])).to.be.oneOf([ expect(parseInt(listAnyResp.get("content-length") ?? "")).to.be.oneOf([
dogFileSize, dogFileSize,
catFileSize, catFileSize,
]); ]);
@@ -220,7 +220,7 @@ describe("photos", function () {
const response = await request(callback) const response = await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}/${token}`) .get(`/photos/showByID/${seed.dogPhoto.id}/${token}`)
.expect(200); .expect(200);
expect(parseInt(response.header["content-length"])).to.equal( expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogFileSize, dogFileSize,
); );
@@ -235,7 +235,7 @@ describe("photos", function () {
const responseSS = await request(callback) const responseSS = await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}/${tokenSelfSigned}`) .get(`/photos/showByID/${seed.dogPhoto.id}/${tokenSelfSigned}`)
.expect(200); .expect(200);
expect(parseInt(responseSS.header["content-length"])).to.equal( expect(parseInt(responseSS.get("content-length") ?? "")).to.equal(
dogFileSize, dogFileSize,
); );
}); });
@@ -249,7 +249,7 @@ describe("photos", function () {
}, },
); );
const response = await request(callback) await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}/${token}`) .get(`/photos/showByID/${seed.dogPhoto.id}/${token}`)
.expect(401); .expect(401);
}); });
@@ -276,17 +276,17 @@ describe("photos", function () {
hash: dogHash, hash: dogHash,
size: dogSize, size: dogSize,
format: dogFormat, format: dogFormat,
} as IPhotosNewPostBody) } as TPhotosNewPostBody)
.expect(200); .expect(200);
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const photo = response.body.data as IPhotoReqJSON; const photo = response.body.data as TPhotoReqJSON;
expect(photo.hash).to.be.equal(dogHash); expect(photo.hash).to.be.equal(dogHash);
const dbPhoto = await Photo.findOneOrFail({ const dbPhoto = await Photo.findOneOrFail({
id: photo.id, id: photo.id,
user: seed.user1.id as any, user: { id: seed.user1.id },
}); });
expect(dbPhoto.hash).to.be.equal(dogHash); expect(dbPhoto.hash).to.be.equal(dogHash);
@@ -303,7 +303,7 @@ describe("photos", function () {
const dbPhotoUpl = await Photo.findOneOrFail({ const dbPhotoUpl = await Photo.findOneOrFail({
id: photo.id, id: photo.id,
user: seed.user1.id as any, user: { id: seed.user1.id },
}); });
expect(dbPhotoUpl.hash).to.be.equal(dogHash); expect(dbPhotoUpl.hash).to.be.equal(dogHash);
expect(await dbPhotoUpl.origFileExists()).to.be.equal(true); expect(await dbPhotoUpl.origFileExists()).to.be.equal(true);
@@ -318,7 +318,7 @@ describe("photos", function () {
}) })
.expect(200); .expect(200);
expect(parseInt(showResp.header["content-length"])).to.equal( expect(parseInt(showResp.get("content-length") ?? "")).to.equal(
dogFileSize, dogFileSize,
); );
}); });
@@ -334,17 +334,17 @@ describe("photos", function () {
hash: pngHash, hash: pngHash,
size: pngSize, size: pngSize,
format: pngFormat, format: pngFormat,
} as IPhotosNewPostBody) } as TPhotosNewPostBody)
.expect(200); .expect(200);
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const photo = response.body.data as IPhotoReqJSON; const photo = response.body.data as TPhotoReqJSON;
expect(photo.hash).to.be.equal(pngHash); expect(photo.hash).to.be.equal(pngHash);
const dbPhoto = await Photo.findOneOrFail({ const dbPhoto = await Photo.findOneOrFail({
id: photo.id, id: photo.id,
user: seed.user1.id as any, user: { id: seed.user1.id },
}); });
expect(dbPhoto.hash).to.be.equal(pngHash); expect(dbPhoto.hash).to.be.equal(pngHash);
@@ -361,7 +361,7 @@ describe("photos", function () {
const dbPhotoUpl = await Photo.findOneOrFail({ const dbPhotoUpl = await Photo.findOneOrFail({
id: photo.id, id: photo.id,
user: seed.user1.id as any, user: { id: seed.user1.id },
}); });
expect(dbPhotoUpl.hash).to.be.equal(pngHash); expect(dbPhotoUpl.hash).to.be.equal(pngHash);
expect(dbPhotoUpl.format).to.be.equal(pngFormat); expect(dbPhotoUpl.format).to.be.equal(pngFormat);
@@ -378,7 +378,7 @@ describe("photos", function () {
}) })
.expect(200); .expect(200);
expect(parseInt(showResp.header["content-length"])).to.equal( expect(parseInt(showResp.get("content-length") ?? "")).to.equal(
pngFileSize, pngFileSize,
); );
}); });
@@ -394,12 +394,12 @@ describe("photos", function () {
hash: dogHash, hash: dogHash,
size: dogSize, size: dogSize,
format: dogFormat, format: dogFormat,
} as IPhotosNewPostBody) } as TPhotosNewPostBody)
.expect(200); .expect(200);
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const response2 = await request(callback) await request(callback)
.post("/photos/new") .post("/photos/new")
.set({ .set({
Authorization: `Bearer ${seed.user1.toJWT()}`, Authorization: `Bearer ${seed.user1.toJWT()}`,
@@ -409,7 +409,7 @@ describe("photos", function () {
hash: dogHash, hash: dogHash,
size: dogSize, size: dogSize,
format: dogFormat, format: dogFormat,
} as IPhotosNewPostBody) } as TPhotosNewPostBody)
.expect(200); .expect(200);
const dbPhoto = await Photo.find({ const dbPhoto = await Photo.find({
@@ -431,17 +431,17 @@ describe("photos", function () {
hash: dogHash, hash: dogHash,
size: dogSize, size: dogSize,
format: dogFormat, format: dogFormat,
} as IPhotosNewPostBody) } as TPhotosNewPostBody)
.expect(200); .expect(200);
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const photo = response.body.data as IPhotoReqJSON; const photo = response.body.data as TPhotoReqJSON;
expect(photo.hash).to.be.equal(dogHash); expect(photo.hash).to.be.equal(dogHash);
const dbPhoto = await Photo.findOneOrFail({ const dbPhoto = await Photo.findOneOrFail({
id: photo.id, id: photo.id,
user: seed.user1.id as any, user: { id: seed.user1.id },
}); });
expect(dbPhoto.hash).to.be.equal(dogHash); expect(dbPhoto.hash).to.be.equal(dogHash);
@@ -474,7 +474,7 @@ describe("photos", function () {
}) })
.expect(200); .expect(200);
expect(parseInt(showResp.header["content-length"])).to.equal( expect(parseInt(showResp.get("content-length") ?? "")).to.equal(
dogFileSize, dogFileSize,
); );
}); });
@@ -490,17 +490,17 @@ describe("photos", function () {
hash: dogHash, hash: dogHash,
size: dogSize, size: dogSize,
format: dogFormat, format: dogFormat,
} as IPhotosNewPostBody) } as TPhotosNewPostBody)
.expect(200); .expect(200);
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const photo = response.body.data as IPhotoReqJSON; const photo = response.body.data as TPhotoReqJSON;
expect(photo.hash).to.be.equal(dogHash); expect(photo.hash).to.be.equal(dogHash);
const dbPhoto = await Photo.findOneOrFail({ const dbPhoto = await Photo.findOneOrFail({
id: photo.id, id: photo.id,
user: seed.user1.id as any, user: { id: seed.user1.id },
}); });
expect(dbPhoto.hash).to.be.equal(dogHash); expect(dbPhoto.hash).to.be.equal(dogHash);
@@ -517,7 +517,7 @@ describe("photos", function () {
expect(await dbPhoto.origFileExists()).to.be.equal(false); expect(await dbPhoto.origFileExists()).to.be.equal(false);
const showResp = await request(callback) await request(callback)
.get(`/photos/showByID/${photo.id}`) .get(`/photos/showByID/${photo.id}`)
.set({ .set({
Authorization: `Bearer ${seed.user1.toJWT()}`, Authorization: `Bearer ${seed.user1.toJWT()}`,
@@ -536,17 +536,17 @@ describe("photos", function () {
hash: dogHash, hash: dogHash,
size: dogSize, size: dogSize,
format: dogFormat, format: dogFormat,
} as IPhotosNewPostBody) } as TPhotosNewPostBody)
.expect(200); .expect(200);
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const photo = response.body.data as IPhotoReqJSON; const photo = response.body.data as TPhotoReqJSON;
expect(photo.hash).to.be.equal(dogHash); expect(photo.hash).to.be.equal(dogHash);
const dbPhoto = await Photo.findOneOrFail({ const dbPhoto = await Photo.findOneOrFail({
id: photo.id, id: photo.id,
user: seed.user1.id as any, user: { id: seed.user1.id },
}); });
expect(dbPhoto.hash).to.be.equal(dogHash); expect(dbPhoto.hash).to.be.equal(dogHash);
expect(await dbPhoto.origFileExists()).to.be.equal(false); expect(await dbPhoto.origFileExists()).to.be.equal(false);
@@ -574,17 +574,17 @@ describe("photos", function () {
hash: dogHash, hash: dogHash,
size: dogSize, size: dogSize,
format: dogFormat, format: dogFormat,
} as IPhotosNewPostBody) } as TPhotosNewPostBody)
.expect(200); .expect(200);
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const photo = response.body.data as IPhotoReqJSON; const photo = response.body.data as TPhotoReqJSON;
expect(photo.hash).to.be.equal(dogHash); expect(photo.hash).to.be.equal(dogHash);
const dbPhoto = await Photo.findOneOrFail({ const dbPhoto = await Photo.findOneOrFail({
id: photo.id, id: photo.id,
user: seed.user1.id as any, user: { id: seed.user1.id },
}); });
expect(dbPhoto.hash).to.be.equal(dogHash); expect(dbPhoto.hash).to.be.equal(dogHash);
expect(await dbPhoto.origFileExists()).to.be.equal(false); expect(await dbPhoto.origFileExists()).to.be.equal(false);
@@ -609,7 +609,7 @@ describe("photos", function () {
}); });
it("should not create a photo with weird properties", async function () { it("should not create a photo with weird properties", async function () {
const response = await request(callback) await request(callback)
.post("/photos/new") .post("/photos/new")
.set({ .set({
Authorization: `Bearer ${seed.user1.toJWT()}`, Authorization: `Bearer ${seed.user1.toJWT()}`,
@@ -619,7 +619,7 @@ describe("photos", function () {
hash: "../test", hash: "../test",
size: "33333", size: "33333",
format: dogFormat, format: dogFormat,
} as IPhotosNewPostBody) } as TPhotosNewPostBody)
.expect(400); .expect(400);
}); });
@@ -636,13 +636,13 @@ describe("photos", function () {
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const photo = response.body.data as IPhotoReqJSON; const photo = response.body.data as TPhotoReqJSON;
expect(photo.name).to.be.equal("Test1"); expect(photo.name).to.be.equal("Test1");
const dbPhoto = await Photo.findOne({ const dbPhoto = await Photo.findOne({
id: seed.dogPhoto.id, id: seed.dogPhoto.id,
user: seed.user1.id as any, user: {id:seed.user1.id} ,
}); });
expect(dbPhoto.name).to.be.equal("Test1"); expect(dbPhoto.name).to.be.equal("Test1");
@@ -663,7 +663,7 @@ describe("photos", function () {
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const photos = response.body.data as IPhotoReqJSON[]; const photos = response.body.data as TPhotoReqJSON[];
const userPhotos = [ const userPhotos = [
await seed.dogPhoto.toReqJSON(), await seed.dogPhoto.toReqJSON(),
await seed.catPhoto.toReqJSON(), await seed.catPhoto.toReqJSON(),
@@ -686,7 +686,7 @@ describe("photos", function () {
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const photo = response.body.data as IPhotoReqJSON; const photo = response.body.data as TPhotoReqJSON;
const usedPhoto = seed.catPhoto.toReqJSON(); const usedPhoto = seed.catPhoto.toReqJSON();
@@ -705,7 +705,7 @@ describe("photos", function () {
}) })
.send({ .send({
photos: [await seed.dogPhoto.toReqJSON()], photos: [await seed.dogPhoto.toReqJSON()],
} as IPhotosDeleteBody) } as TPhotosDeleteBody)
.expect(200); .expect(200);
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
@@ -737,7 +737,7 @@ describe("photos", function () {
await seed.dogPhoto.toReqJSON(), await seed.dogPhoto.toReqJSON(),
await seed.catPhoto.toReqJSON(), await seed.catPhoto.toReqJSON(),
], ],
} as IPhotosDeleteBody) } as TPhotosDeleteBody)
.expect(200); .expect(200);
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;

View File

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

@@ -3,16 +3,16 @@ import { connect } from "config/database";
import * as request from "supertest"; import * as request from "supertest";
import { getConnection } from "typeorm"; import { getConnection } from "typeorm";
import { app } from "~app"; import { app } from "~app";
import { IUserAuthJSON, User } from "~entity/User"; import { User } from "~entity/User";
import { import {
IUserEditBody, TUserEditBody,
IUserEditRespBody, TUserEditRespBody,
IUserGetRespBody, TUserGetRespBody,
IUserLoginBody, TUserLoginBody,
IUserLoginRespBody, TUserLoginRespBody,
IUserSignupBody, TUserSignupBody,
IUserSignupRespBody, TUserSignupRespBody,
} from "~routes/users"; } from "~shared/types";
import { allowSignups, ISeed, seedDB } from "./util"; import { allowSignups, ISeed, seedDB } from "./util";
@@ -43,13 +43,13 @@ describe("users", function () {
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .expect(200);
const body = response.body as IUserGetRespBody; const body = response.body as TUserGetRespBody;
if (body.error !== false) { if (body.error !== false) {
assert(false); assert(false);
return;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: _, ...user } = body.data; const { jwt: _, ...user } = body.data;
expect(user).to.deep.equal(seed.user1.toJSON()); expect(user).to.deep.equal(seed.user1.toJSON());
@@ -59,17 +59,17 @@ describe("users", function () {
const response = await request(callback) const response = await request(callback)
.post("/users/login") .post("/users/login")
.set({ "Content-Type": "application/json" }) .set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1" } as IUserLoginBody) .send({ username: "User1", password: "User1" } as TUserLoginBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .expect(200);
const body = response.body as IUserLoginRespBody; const body = response.body as TUserLoginRespBody;
if (body.error !== false) { if (body.error !== false) {
assert(false); assert(false);
return;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: _, ...user } = response.body.data; const { jwt: _, ...user } = response.body.data;
expect(user).to.deep.equal(seed.user1.toJSON()); expect(user).to.deep.equal(seed.user1.toJSON());
}); });
@@ -78,12 +78,12 @@ describe("users", function () {
const response = await request(callback) const response = await request(callback)
.post("/users/login") .post("/users/login")
.set({ "Content-Type": "application/json" }) .set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "asdf" } as IUserLoginBody) .send({ username: "User1", password: "asdf" } as TUserLoginBody)
.expect(404); .expect(404);
const body = response.body as IUserLoginRespBody; const body = response.body as TUserLoginRespBody;
expect(body.error).to.be.equal("User not found"); expect(body.error).to.be.equal("User not found");
expect(body.data).to.be.false; expect(body.data).to.be.null;
}); });
it("should signup user", async function () { it("should signup user", async function () {
@@ -96,17 +96,17 @@ describe("users", function () {
username: "NUser1", username: "NUser1",
password: "NUser1", password: "NUser1",
email: "nuser1@users.com", email: "nuser1@users.com",
} as IUserSignupBody) } as TUserSignupBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .expect(200);
const body = response.body as IUserSignupRespBody; const body = response.body as TUserSignupRespBody;
if (body.error !== false) { if (body.error !== false) {
assert(false); assert(false);
return;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: _, ...user } = body.data; const { jwt: _, ...user } = body.data;
const newUser = await User.findOneOrFail({ username: "NUser1" }); const newUser = await User.findOneOrFail({ username: "NUser1" });
expect(user).to.deep.equal(newUser.toJSON()); expect(user).to.deep.equal(newUser.toJSON());
@@ -120,14 +120,14 @@ describe("users", function () {
username: "NUser1", username: "NUser1",
password: "NUser1", password: "NUser1",
email: "nuser1@users.com", email: "nuser1@users.com",
} as IUserSignupBody) } as TUserSignupBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(400); .expect(400);
const body = response.body as IUserSignupRespBody; const body = response.body as TUserSignupRespBody;
expect(body.error).to.be.equal("Signups not allowed"); expect(body.error).to.be.equal("Signups not allowed");
expect(body.data).to.be.false; expect(body.data).to.be.null;
}); });
it("should signup first user and it should be admin, do not signup new users (by default)", async function () { it("should signup first user and it should be admin, do not signup new users (by default)", async function () {
@@ -140,17 +140,17 @@ describe("users", function () {
username: "NUser1", username: "NUser1",
password: "NUser1", password: "NUser1",
email: "nuser1@users.com", email: "nuser1@users.com",
} as IUserSignupBody) } as TUserSignupBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .expect(200);
const body = response.body as IUserSignupRespBody; const body = response.body as TUserSignupRespBody;
if (body.error !== false) { if (body.error !== false) {
assert(false); assert(false);
return;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: _, ...user } = body.data; const { jwt: _, ...user } = body.data;
const newUser = await User.findOneOrFail({ username: "NUser1" }); const newUser = await User.findOneOrFail({ username: "NUser1" });
expect(user).to.deep.equal(newUser.toJSON()); expect(user).to.deep.equal(newUser.toJSON());
@@ -163,14 +163,14 @@ describe("users", function () {
username: "NUser2", username: "NUser2",
password: "NUser2", password: "NUser2",
email: "nuser2@users.com", email: "nuser2@users.com",
} as IUserSignupBody) } as TUserSignupBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(400); .expect(400);
const body2 = response2.body as IUserSignupRespBody; const body2 = response2.body as TUserSignupRespBody;
expect(body2.error).to.be.equal("Signups not allowed"); expect(body2.error).to.be.equal("Signups not allowed");
expect(body2.data).to.be.false; expect(body2.data).to.be.null;
}); });
it("should signup first user and it should be admin, but not new ones", async function () { it("should signup first user and it should be admin, but not new ones", async function () {
@@ -184,17 +184,17 @@ describe("users", function () {
username: "NUser1", username: "NUser1",
password: "NUser1", password: "NUser1",
email: "nuser1@users.com", email: "nuser1@users.com",
} as IUserSignupBody) } as TUserSignupBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .expect(200);
const body = response.body as IUserSignupRespBody; const body = response.body as TUserSignupRespBody;
if (body.error !== false) { if (body.error !== false) {
assert(false); assert(false);
return;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: jwt1, ...user } = body.data; const { jwt: jwt1, ...user } = body.data;
const newUser = await User.findOneOrFail({ username: "NUser1" }); const newUser = await User.findOneOrFail({ username: "NUser1" });
expect(user).to.deep.equal(newUser.toJSON()); expect(user).to.deep.equal(newUser.toJSON());
@@ -207,17 +207,17 @@ describe("users", function () {
username: "NUser2", username: "NUser2",
password: "NUser2", password: "NUser2",
email: "nuser2@users.com", email: "nuser2@users.com",
} as IUserSignupBody) } as TUserSignupBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .expect(200);
const body2 = response2.body as IUserSignupRespBody; const body2 = response2.body as TUserSignupRespBody;
if (body2.error !== false) { if (body2.error !== false) {
assert(false); assert(false);
return;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: jwt2, ...user2 } = body2.data; const { jwt: jwt2, ...user2 } = body2.data;
const newUser2 = await User.findOneOrFail({ username: "NUser2" }); const newUser2 = await User.findOneOrFail({ username: "NUser2" });
expect(user2).to.deep.equal(newUser2.toJSON()); expect(user2).to.deep.equal(newUser2.toJSON());
@@ -234,13 +234,13 @@ describe("users", function () {
username: "User1", username: "User1",
password: "NUser1", password: "NUser1",
email: "user1@users.com", email: "user1@users.com",
} as IUserSignupBody) } as TUserSignupBody)
.expect(400); .expect(400);
const body = response.body as IUserSignupRespBody; const body = response.body as TUserSignupRespBody;
expect(body.error).to.be.equal("User already exists"); expect(body.error).to.be.equal("User already exists");
expect(body.data).to.be.false; expect(body.data).to.be.null;
}); });
it("should change user's password", async function () { it("should change user's password", async function () {
@@ -252,15 +252,14 @@ describe("users", function () {
}) })
.send({ .send({
password: "User1NewPass", password: "User1NewPass",
} as IUserEditBody) } as TUserEditBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .expect(200);
const body = response.body as IUserEditRespBody; const body = response.body as TUserEditRespBody;
if (body.error !== false) { if (body.error !== false) {
assert(false); assert(false);
return;
} }
const loginResponse = await request(callback) const loginResponse = await request(callback)
@@ -269,29 +268,29 @@ describe("users", function () {
.send({ .send({
username: "User1", username: "User1",
password: "User1NewPass", password: "User1NewPass",
} as IUserLoginBody) } as TUserLoginBody)
.expect("Content-Type", /json/) .expect("Content-Type", /json/)
.expect(200); .expect(200);
const loginBody = loginResponse.body as IUserLoginRespBody; const loginBody = loginResponse.body as TUserLoginRespBody;
if (loginBody.error !== false) { if (loginBody.error !== false) {
assert(false); assert(false);
return;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: _, ...user } = loginBody.data; const { jwt: _, ...user } = loginBody.data;
expect(user).to.deep.equal(seed.user1.toJSON()); expect(user).to.deep.equal(seed.user1.toJSON());
const badLoginResponse = await request(callback) const badLoginResponse = await request(callback)
.post("/users/login") .post("/users/login")
.set({ "Content-Type": "application/json" }) .set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1" } as IUserLoginBody) .send({ username: "User1", password: "User1" } as TUserLoginBody)
.expect(404); .expect(404);
const badLoginBody = badLoginResponse.body as IUserLoginRespBody; const badLoginBody = badLoginResponse.body as TUserLoginRespBody;
expect(badLoginBody.error).to.be.equal("User not found"); expect(badLoginBody.error).to.be.equal("User not found");
expect(badLoginBody.data).to.be.false; expect(badLoginBody.data).to.be.null;
}); });
}); });

View File

@@ -4,7 +4,6 @@ import { User } from "entity/User";
import { Photo } from "~entity/Photo"; import { Photo } from "~entity/Photo";
import { getHash, getSize } from "~util"; import { getHash, getSize } from "~util";
import { Config, ConfigKey, setConfigValue } from "~entity/Config"; import { Config, ConfigKey, setConfigValue } from "~entity/Config";
import { config } from "chai";
export const dogPath = "./src/tests/integration/photos/dog.jpg"; export const dogPath = "./src/tests/integration/photos/dog.jpg";
export const catPath = "./src/tests/integration/photos/cat.jpg"; export const catPath = "./src/tests/integration/photos/cat.jpg";

View File

@@ -24,8 +24,5 @@
"include": [ "include": [
"./src/**/*.ts", "./src/**/*.ts",
"./tests/**/*.ts", "./tests/**/*.ts",
],
"exclude": [
"frontend"
] ]
} }

1
dockercomposeexample/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dbdata

View File

@@ -1,7 +1,8 @@
version: "3.8" version: "3.8"
services: services:
photosapp: photosapp:
image: stepanusatiuk/photos:main # image: stepanusatiuk/photos:main
build: ../
restart: always restart: always
ports: ports:
- "8080:8080" - "8080:8080"
@@ -21,4 +22,3 @@ services:
- ./dbdata:/var/lib/mysql - ./dbdata:/var/lib/mysql
env_file: env_file:
- db.env - db.env

5
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"trailingComma": "all",
"tabWidth": 4,
"endOfLine": "auto"
}

View File

@@ -1,7 +1,4 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
const { pathsToModuleNameMapper } = require("ts-jest");
const { compilerOptions } = require("./tsconfig");
module.exports = { module.exports = {
preset: "ts-jest", preset: "ts-jest",
moduleNameMapper: { moduleNameMapper: {
@@ -11,6 +8,7 @@ module.exports = {
"react-spring/renderprops": "react-spring/renderprops":
"<rootDir>/node_modules/react-spring/renderprops.cjs", "<rootDir>/node_modules/react-spring/renderprops.cjs",
"react-spring": "<rootDir>/node_modules/react-spring/web.cjs", "react-spring": "<rootDir>/node_modules/react-spring/web.cjs",
"~(.*)": "<rootDir>/$1",
}, },
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"], setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
testEnvironment: "jsdom", testEnvironment: "jsdom",

13506
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,15 +3,18 @@
"scripts": { "scripts": {
"start": "parcel src/index.html", "start": "parcel src/index.html",
"build": "parcel build src/index.html", "build": "parcel build src/index.html",
"test": "jest",
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx", "lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx",
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix", "lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix",
"test": "jest" "prettier-check": "prettier src/**/*.ts src/**/*.tsx --check",
"prettify": "prettier src/**/*.ts src/**/*.tsx --write"
}, },
"dependencies": { "dependencies": {
"@blueprintjs/core": "^4.14.1", "@blueprintjs/core": "^4.14.1",
"@parcel/config-default": "^2.9.3", "@parcel/config-default": "^2.9.3",
"@parcel/transformer-sass": "^2.9.3", "@parcel/transformer-sass": "^2.9.3",
"@parcel/transformer-typescript-tsc": "^2.9.3", "@parcel/transformer-typescript-tsc": "^2.9.3",
"@reduxjs/toolkit": "^1.9.5",
"@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0", "@typescript-eslint/parser": "^6.2.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0", "@wojtekmaj/enzyme-adapter-react-17": "^0",
@@ -53,6 +56,7 @@
"devDependencies": { "devDependencies": {
"@types/enzyme": "^3.10.13", "@types/enzyme": "^3.10.13",
"@types/eslint": "^8.44.1", "@types/eslint": "^8.44.1",
"@types/eslint-config-prettier": "^6.11.0",
"@types/eslint-plugin-prettier": "^3.1.0", "@types/eslint-plugin-prettier": "^3.1.0",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",
"@types/pluralize": "^0.0.30", "@types/pluralize": "^0.0.30",
@@ -62,6 +66,7 @@
"@types/react-redux": "^7.1.25", "@types/react-redux": "^7.1.25",
"@types/react-router": "^5.1.20", "@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/redux-devtools-extension": "^2.13.2",
"@types/spark-md5": "^3.0.2" "@types/spark-md5": "^3.0.2"
} }
} }

View File

@@ -16,7 +16,7 @@ export function AccountComponent(props: IAccountComponentProps) {
return ( return (
<Card className="AuthForm" elevation={2}> <Card className="AuthForm" elevation={2}>
<form <form
onSubmit={(e: React.FormEvent<any>) => { onSubmit={(e: React.FormEvent<never>) => {
e.preventDefault(); e.preventDefault();
if (pass.trim()) { if (pass.trim()) {
props.changePass(pass); props.changePass(pass);

View File

@@ -1,6 +1,5 @@
import { Position, Toaster } from "@blueprintjs/core"; import { Position, Toaster } from "@blueprintjs/core";
import { isNumber } from "class-validator"; import { TPhotoReqJSON } from "./shared/types";
import { IPhotoReqJSON } from "../../src/entity/Photo";
export const AppToaster = Toaster.create({ export const AppToaster = Toaster.create({
className: "recipe-toaster", className: "recipe-toaster",
@@ -44,7 +43,7 @@ export function showPhotoCreateFailToast(f: File, e: string): void {
} }
export function showPhotoUploadJSONFailToast( export function showPhotoUploadJSONFailToast(
p: IPhotoReqJSON | number, p: TPhotoReqJSON | number,
e: string, e: string,
): void { ): void {
const photoMsg = typeof p === "number" ? p : p.hash; const photoMsg = typeof p === "number" ? p : p.hash;

View File

@@ -23,7 +23,9 @@ export class AuthScreenComponent extends React.PureComponent<IAuthScreenProps> {
} }
public render() { public render() {
const { location } = this.props.history; const { location } = this.props.history;
const { from } = (this.props.location.state as any) || { from: "/" }; const { from } = (this.props.location.state as { from: string }) || {
from: "/",
};
const { loggedIn } = this.props; const { loggedIn } = this.props;
return loggedIn ? ( return loggedIn ? (
<Redirect to={from} /> <Redirect to={from} />
@@ -46,7 +48,7 @@ export class AuthScreenComponent extends React.PureComponent<IAuthScreenProps> {
transform: "translate3d(400px,0,0)", transform: "translate3d(400px,0,0)",
}} }}
> >
{(_location: any) => (style: any) => ( {(_location) => (style) => (
<animated.div style={style}> <animated.div style={style}>
<Switch location={_location}> <Switch location={_location}>
<Route path="/login" component={Login} /> <Route path="/login" component={Login} />

View File

@@ -36,7 +36,7 @@ export class LoginComponent extends React.PureComponent<
this.props.history.push("/signup"); this.props.history.push("/signup");
} }
public submit(e: React.FormEvent<any>) { public submit<T extends React.FormEvent>(e: T) {
e.preventDefault(); e.preventDefault();
const { username, password } = this.state; const { username, password } = this.state;
if (!this.props.inProgress) { if (!this.props.inProgress) {

View File

@@ -37,7 +37,7 @@ export class SignupComponent extends React.PureComponent<
this.props.history.push("/login"); this.props.history.push("/login");
} }
public submit(e: React.FormEvent<any>) { public submit<T extends React.FormEvent>(e: T) {
e.preventDefault(); e.preventDefault();
const { username, password, email } = this.state; const { username, password, email } = this.state;
if (!this.props.inProgress) { if (!this.props.inProgress) {

View File

@@ -2,34 +2,29 @@ import "./Home.scss";
import { import {
Alignment, Alignment,
Breadcrumbs,
Button, Button,
Classes, Classes,
IBreadcrumbProps,
Icon,
Menu, Menu,
MenuItem, MenuItem,
Navbar, Navbar,
Popover, Popover,
Spinner,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Route, RouteComponentProps, Switch, withRouter } from "react-router"; import { Route, RouteComponentProps, Switch, withRouter } from "react-router";
import { animated, config, Transition } from "react-spring/renderprops"; import { animated, config, Transition } from "react-spring/renderprops";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { IUserJSON } from "../../../src/entity/User"; import { TUserJSON } from "../shared/types";
import { Account } from "../Account/Account"; import { Account } from "../Account/Account";
import { Overview } from "../Photos/Overview"; import { Overview } from "../Photos/Overview";
import { toggleDarkMode } from "../redux/localSettings/actions"; import { toggleDarkMode } from "../redux/localSettings/actions";
import { IAppState } from "../redux/reducers"; import { IAppState } from "../redux/reducers";
import { logoutUser } from "../redux/user/actions"; import { logoutUser } from "../redux/user/actions";
import { Photo } from "../Photos/Photo";
import { PhotoRoute } from "../Photos/PhotoRoute"; import { PhotoRoute } from "../Photos/PhotoRoute";
import { UploadStatus } from "./UploadStatus"; import { UploadStatus } from "./UploadStatus";
export interface IHomeProps extends RouteComponentProps { export interface IHomeProps extends RouteComponentProps {
user: IUserJSON | null; user: TUserJSON | null;
darkMode: boolean; darkMode: boolean;
@@ -95,28 +90,24 @@ export class HomeComponent extends React.PureComponent<IHomeProps> {
transform: "translate3d(400px,0,0)", transform: "translate3d(400px,0,0)",
}} }}
> >
{(_location: any) => (style: any) => {(_location) => (style) => (
( <animated.div
<animated.div style={style}
style={style} className="viewComponent"
className="viewComponent" >
> <Switch location={_location}>
<Switch location={_location}> <Route
<Route path="/account"
path="/account" component={Account}
component={Account} />
/> <Route
<Route path="/photos/:id"
path="/photos/:id" component={PhotoRoute}
component={PhotoRoute} />
/> <Route path="/" component={Overview} />
<Route </Switch>
path="/" </animated.div>
component={Overview} )}
/>
</Switch>
</animated.div>
)}
</Transition> </Transition>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { Button, Icon, Popover, Spinner } from "@blueprintjs/core"; import { Button } from "@blueprintjs/core";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { IAppState } from "../redux/reducers"; import { IAppState } from "../redux/reducers";
@@ -12,9 +12,9 @@ export interface IUploadStatusComponentProps {
uploadingQueue: number; uploadingQueue: number;
} }
export const UploadStatusComponent: React.FunctionComponent<IUploadStatusComponentProps> = ( export const UploadStatusComponent: React.FunctionComponent<
props, IUploadStatusComponentProps
) => { > = (props) => {
const { creatingNow, creatingQueue, uploadingNow, uploadingQueue } = props; const { creatingNow, creatingQueue, uploadingNow, uploadingQueue } = props;
const uploading = const uploading =
creatingNow > 0 || creatingNow > 0 ||

View File

@@ -3,20 +3,18 @@ import "./Overview.scss";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { IAppState } from "../redux/reducers"; import { IAppState } from "~/src/redux/reducers";
import { import {
photosDeleteCancel, photosDeleteCancel,
photosDeleteStart, photosDeleteStart,
photosLoadStart, photosLoadStart,
} from "../redux/photos/actions"; } from "../redux/photos/actions";
import { IPhotoReqJSON } from "../../../src/entity/Photo"; import { TPhotoReqJSON } from "~/src/shared/types";
import { LoadingStub } from "../LoadingStub";
import { PhotoCard } from "./PhotoCard"; import { PhotoCard } from "./PhotoCard";
import { import {
Alignment, Alignment,
Button, Button,
Classes, Classes,
H1,
H2, H2,
H3, H3,
Navbar, Navbar,
@@ -25,11 +23,10 @@ import {
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import { UploadButton } from "./UploadButton"; import { UploadButton } from "./UploadButton";
import { Photo } from "./Photo"; import { Photo } from "./Photo";
import { getPhotoThumbPath } from "../redux/api/photos"; import { showDeletionToast } from "~/src/AppToaster";
import { showDeletionToast } from "../AppToaster";
export interface IOverviewComponentProps { export interface IOverviewComponentProps {
photos: IPhotoReqJSON[]; photos: TPhotoReqJSON[];
triedLoading: boolean; triedLoading: boolean;
allPhotosLoaded: boolean; allPhotosLoaded: boolean;
overviewFetching: boolean; overviewFetching: boolean;
@@ -38,8 +35,8 @@ export interface IOverviewComponentProps {
darkMode: boolean; darkMode: boolean;
fetchPhotos: () => void; fetchPhotos: () => void;
startDeletePhotos: (photos: IPhotoReqJSON[]) => void; startDeletePhotos: (photos: TPhotoReqJSON[]) => void;
cancelDelete: (photos: IPhotoReqJSON[]) => void; cancelDelete: (photos: TPhotoReqJSON[]) => void;
} }
const PhotoCardM = React.memo(PhotoCard); const PhotoCardM = React.memo(PhotoCard);
@@ -83,7 +80,7 @@ export const OverviewComponent: React.FunctionComponent<
( (
acc: Record< acc: Record<
string, string,
Record<string, Record<string, IPhotoReqJSON[]>> Record<string, Record<string, TPhotoReqJSON[]>>
>, >,
photo, photo,
) => { ) => {
@@ -111,7 +108,7 @@ export const OverviewComponent: React.FunctionComponent<
const els = Object.keys(dates[year]).reduce( const els = Object.keys(dates[year]).reduce(
(accMonths: JSX.Element[], month): JSX.Element[] => { (accMonths: JSX.Element[], month): JSX.Element[] => {
const photos = Object.values(dates[year][month]).reduce( const photos = Object.values(dates[year][month]).reduce(
(accDays: IPhotoReqJSON[], day) => { (accDays: TPhotoReqJSON[], day) => {
return [...day, ...accDays]; return [...day, ...accDays];
}, },
[], [],
@@ -267,9 +264,9 @@ function mapStateToProps(state: IAppState) {
function mapDispatchToProps(dispatch: Dispatch) { function mapDispatchToProps(dispatch: Dispatch) {
return { return {
fetchPhotos: () => dispatch(photosLoadStart()), fetchPhotos: () => dispatch(photosLoadStart()),
startDeletePhotos: (photos: IPhotoReqJSON[]) => startDeletePhotos: (photos: TPhotoReqJSON[]) =>
dispatch(photosDeleteStart(photos)), dispatch(photosDeleteStart(photos)),
cancelDelete: (photos: IPhotoReqJSON[]) => cancelDelete: (photos: TPhotoReqJSON[]) =>
dispatch(photosDeleteCancel(photos)), dispatch(photosDeleteCancel(photos)),
}; };
} }

View File

@@ -2,28 +2,31 @@ import "./Photo.scss";
import * as React from "react"; import * as React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { IPhotoReqJSON } from "../../../src/entity/Photo"; import { TPhotoReqJSON } from "~/src/shared/types";
import { LoadingStub } from "../LoadingStub"; import { LoadingStub } from "../LoadingStub";
import { import { getPhotoImgPath, getPhotoThumbPath } from "../redux/api/photos";
fetchPhoto,
getPhotoImgPath,
getPhotoThumbPath,
} from "../redux/api/photos";
import { photoLoadStart } from "../redux/photos/actions"; import { photoLoadStart } from "../redux/photos/actions";
import { IPhotoState } from "../redux/photos/reducer"; import { TPhotoState } from "../redux/photos/reducer";
import { IAppState } from "../redux/reducers"; import { IAppState } from "../redux/reducers";
import { LargeSize, PreviewSize } from "./helper"; import { LargeSize, PreviewSize } from "./helper";
export interface IPhotoComponentProps { type StateProps = {
id: number; photo: TPhotoReqJSON | undefined;
photo: IPhotoReqJSON | undefined; photoState: TPhotoState | undefined;
photoState: IPhotoState | undefined; };
type DispatchProps = {
fetchPhoto: (id: number) => void; fetchPhoto: (id: number) => void;
close: () => void; };
}
export const PhotoComponent: React.FunctionComponent<IPhotoComponentProps> = ( type OwnProps = {
id: number;
close: () => void;
};
export type TPhotoComponentProps = StateProps & DispatchProps & OwnProps;
export const PhotoComponent: React.FunctionComponent<TPhotoComponentProps> = (
props, props,
) => { ) => {
const [smallPreviewFetching, setSmallPreviewFetching] = const [smallPreviewFetching, setSmallPreviewFetching] =
@@ -118,7 +121,7 @@ export const PhotoComponent: React.FunctionComponent<IPhotoComponentProps> = (
); );
}; };
function mapStateToProps(state: IAppState, props: IPhotoComponentProps) { function mapStateToProps(state: IAppState, props: OwnProps) {
const { id } = props; const { id } = props;
return { return {
photo: state.photos?.photos?.find((p) => p.id === id), photo: state.photos?.photos?.find((p) => p.id === id),
@@ -130,8 +133,7 @@ function mapDispatchToProps(dispatch: Dispatch) {
return { fetchPhoto: (id: number) => dispatch(photoLoadStart(id)) }; return { fetchPhoto: (id: number) => dispatch(photoLoadStart(id)) };
} }
// Because https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16990 export const Photo = connect<StateProps, DispatchProps, OwnProps, IAppState>(
export const Photo = connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps, mapDispatchToProps,
)(PhotoComponent) as any; )(PhotoComponent);

View File

@@ -8,40 +8,39 @@ import {
Spinner, Spinner,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import * as React from "react"; import * as React from "react";
import { IPhotoReqJSON } from "../../../src/entity/Photo"; import { TPhotoReqJSON } from "~/src/shared/types";
import { getPhotoImgPath, getPhotoThumbPath } from "../redux/api/photos"; import { getPhotoThumbPath } from "../redux/api/photos";
import { showDeletionToast } from "../AppToaster"; import { showDeletionToast } from "../AppToaster";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { photosDeleteCancel, photosDeleteStart } from "../redux/photos/actions"; import { photosDeleteCancel, photosDeleteStart } from "../redux/photos/actions";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { LoadingStub } from "../LoadingStub";
import { RouteComponentProps, withRouter } from "react-router"; import { RouteComponentProps, withRouter } from "react-router";
import { LargeSize, PreviewSize } from "./helper"; import { PreviewSize } from "./helper";
export interface IPhotoCardComponentProps extends RouteComponentProps { export interface TPhotoCardComponentProps extends RouteComponentProps {
photo: IPhotoReqJSON; photo: TPhotoReqJSON;
selected: boolean; selected: boolean;
id: string; id: string;
deletePhoto: (photos: IPhotoReqJSON[]) => void; deletePhoto: (photos: TPhotoReqJSON[]) => void;
cancelDelete: (photos: IPhotoReqJSON[]) => void; cancelDelete: (photos: TPhotoReqJSON[]) => void;
onClick: (e: React.MouseEvent<HTMLElement>) => void; onClick: (e: React.MouseEvent<HTMLElement>) => void;
} }
export interface IPhotoCardComponentState { export interface TPhotoCardComponentState {
loaded: boolean; loaded: boolean;
} }
const defaultPhotoCardState: IPhotoCardComponentState = { const defaultPhotoCardState: TPhotoCardComponentState = {
loaded: false, loaded: false,
}; };
@ContextMenuTarget @ContextMenuTarget
export class PhotoCardComponent extends React.PureComponent< export class PhotoCardComponent extends React.PureComponent<
IPhotoCardComponentProps, TPhotoCardComponentProps,
IPhotoCardComponentState TPhotoCardComponentState
> { > {
constructor(props: IPhotoCardComponentProps) { constructor(props: TPhotoCardComponentProps) {
super(props); super(props);
this.handleDelete = this.handleDelete.bind(this); this.handleDelete = this.handleDelete.bind(this);
@@ -122,9 +121,9 @@ export class PhotoCardComponent extends React.PureComponent<
function mapDispatchToProps(dispatch: Dispatch) { function mapDispatchToProps(dispatch: Dispatch) {
return { return {
deletePhoto: (photos: IPhotoReqJSON[]) => deletePhoto: (photos: TPhotoReqJSON[]) =>
dispatch(photosDeleteStart(photos)), dispatch(photosDeleteStart(photos)),
cancelDelete: (photos: IPhotoReqJSON[]) => cancelDelete: (photos: TPhotoReqJSON[]) =>
dispatch(photosDeleteCancel(photos)), dispatch(photosDeleteCancel(photos)),
}; };
} }

View File

@@ -6,12 +6,12 @@ function getId(props: RouteComponentProps) {
return parseInt((props.match?.params as { id: string }).id); return parseInt((props.match?.params as { id: string }).id);
} }
export const PhotoRouteComponent: React.FunctionComponent<RouteComponentProps> = ( export const PhotoRouteComponent: React.FunctionComponent<
props: RouteComponentProps, RouteComponentProps
) => { > = (props: RouteComponentProps) => {
const id = getId(props); const id = getId(props);
return <Photo id={id} />; return <Photo id={id} close={() => {}} />;
}; };
export const PhotoRoute = withRouter(PhotoRouteComponent); export const PhotoRoute = withRouter(PhotoRouteComponent);

View File

@@ -1,14 +1,16 @@
import { import {
IUserLoginRespBody, TUserLoginRespBody,
IUserSignupRespBody, TUserSignupRespBody,
} from "../../../../../src/routes/users"; UserLoginRespBody,
UserSignupRespBody,
} from "~/src/shared/types";
import { fetchJSON } from "../utils"; import { fetchJSON } from "../utils";
export async function login( export async function login(
username: string, username: string,
password: string, password: string,
): Promise<IUserLoginRespBody> { ): Promise<TUserLoginRespBody> {
return fetchJSON("/users/login", "POST", { return fetchJSON("/users/login", "POST", UserLoginRespBody, {
username, username,
password, password,
}); });
@@ -18,8 +20,8 @@ export async function signup(
username: string, username: string,
password: string, password: string,
email: string, email: string,
): Promise<IUserSignupRespBody> { ): Promise<TUserSignupRespBody> {
return fetchJSON("/users/signup", "POST", { return fetchJSON("/users/signup", "POST", UserSignupRespBody, {
username, username,
password, password,
email, email,

View File

@@ -1,19 +1,26 @@
import { IPhotoReqJSON } from "../../../../src/entity/Photo";
import { import {
IPhotosByIDGetRespBody, PhotosByIDGetRespBody,
IPhotosDeleteRespBody, PhotosDeleteRespBody,
IPhotosListRespBody, PhotosListRespBody,
IPhotosNewRespBody, PhotosNewRespBody,
IPhotosUploadRespBody, PhotosUploadRespBody,
} from "../../../../src/routes/photos"; TPhotoReqJSON,
import { apiRoot } from "../../env"; } from "~/src/shared/types";
import {
TPhotosByIDGetRespBody,
TPhotosDeleteRespBody,
TPhotosListRespBody,
TPhotosNewRespBody,
TPhotosUploadRespBody,
} from "~/src/shared/types";
import { apiRoot } from "~src/env";
import { fetchJSONAuth } from "./utils"; import { fetchJSONAuth } from "./utils";
export function getPhotoImgPath(photo: IPhotoReqJSON): string { export function getPhotoImgPath(photo: TPhotoReqJSON): string {
return `${apiRoot}/photos/showByID/${photo.id}/${photo.accessToken}`; return `${apiRoot}/photos/showByID/${photo.id}/${photo.accessToken}`;
} }
export function getPhotoThumbPath(photo: IPhotoReqJSON, size: number): string { export function getPhotoThumbPath(photo: TPhotoReqJSON, size: number): string {
return `${apiRoot}/photos/showByID/${photo.id}/${ return `${apiRoot}/photos/showByID/${photo.id}/${
photo.accessToken photo.accessToken
}?size=${size.toString()}`; }?size=${size.toString()}`;
@@ -22,35 +29,50 @@ export function getPhotoThumbPath(photo: IPhotoReqJSON, size: number): string {
export async function fetchPhotosList( export async function fetchPhotosList(
skip: number, skip: number,
num: number, num: number,
): Promise<IPhotosListRespBody> { ): Promise<TPhotosListRespBody> {
const params = new URLSearchParams({ const params = new URLSearchParams({
skip: skip.toString(), skip: skip.toString(),
num: num.toString(), num: num.toString(),
}); });
return fetchJSONAuth(`/photos/list?${params.toString()}`, "GET"); return fetchJSONAuth(
`/photos/list?${params.toString()}`,
"GET",
PhotosListRespBody,
);
} }
export async function fetchPhoto(id: number): Promise<IPhotosByIDGetRespBody> { export async function fetchPhoto(id: number): Promise<TPhotosByIDGetRespBody> {
return fetchJSONAuth(`/photos/byID/${id}`, "GET"); return fetchJSONAuth(`/photos/byID/${id}`, "GET", PhotosByIDGetRespBody);
} }
export async function createPhoto( export async function createPhoto(
hash: string, hash: string,
size: string, size: string,
format: string, format: string,
): Promise<IPhotosNewRespBody> { ): Promise<TPhotosNewRespBody> {
return fetchJSONAuth("/photos/new", "POST", { hash, size, format }); return fetchJSONAuth("/photos/new", "POST", PhotosNewRespBody, {
hash,
size,
format,
});
} }
export async function uploadPhoto( export async function uploadPhoto(
file: File, file: File,
id: number, id: number,
): Promise<IPhotosUploadRespBody> { ): Promise<TPhotosUploadRespBody> {
return fetchJSONAuth(`/photos/upload/${id}`, "POST", file); return fetchJSONAuth(
`/photos/upload/${id}`,
"POST",
PhotosUploadRespBody,
file,
);
} }
export async function deletePhotos( export async function deletePhotos(
photos: IPhotoReqJSON[], photos: TPhotoReqJSON[],
): Promise<IPhotosDeleteRespBody> { ): Promise<TPhotosDeleteRespBody> {
return fetchJSONAuth(`/photos/delete`, "POST", { photos }); return fetchJSONAuth(`/photos/delete`, "POST", PhotosDeleteRespBody, {
photos,
});
} }

View File

@@ -1,16 +1,19 @@
import { fetchJSONAuth } from "../utils"; import { fetchJSONAuth } from "../utils";
import { IUserEditRespBody, IUserGetRespBody } from "../../../../../src/routes/users"; import {
TUserEditRespBody,
TUserGetRespBody,
UserEditRespBody,
UserGetRespBody,
} from "~/src/shared/types";
export async function fetchUser(): Promise<IUserGetRespBody> { export async function fetchUser(): Promise<TUserGetRespBody> {
return (fetchJSONAuth("/users/user", "GET") as unknown) as Promise< return fetchJSONAuth("/users/user", "GET", UserGetRespBody);
IUserGetRespBody
>;
} }
export async function changeUserPassword( export async function changeUserPassword(
newPassword: string, newPassword: string,
): Promise<IUserEditRespBody> { ): Promise<TUserEditRespBody> {
return fetchJSONAuth("/users/edit", "POST", { return fetchJSONAuth("/users/edit", "POST", UserEditRespBody, {
password: newPassword, password: newPassword,
}); });
} }

View File

@@ -1,4 +1,4 @@
import { apiRoot } from "../../env"; import { apiRoot } from "~src/env";
let token: string | null; let token: string | null;
@@ -14,49 +14,44 @@ export function deleteToken(): void {
token = null; token = null;
} }
export async function fetchJSON( export async function fetchJSON<T, P extends { parse: (string) => T }>(
path: string, path: string,
method: string, method: string,
parser: P,
body?: string | Record<string, unknown> | File, body?: string | Record<string, unknown> | File,
headers?: Record<string, string>, headers?: Record<string, string>,
): Promise<Record<string, unknown>> { ): Promise<T> {
if (typeof body === "object" && !(body instanceof File)) { const reqBody = () =>
body = JSON.stringify(body); body instanceof File
headers = { ? (() => {
...headers, const fd = new FormData();
"Content-Type": "application/json", fd.append("photo", body);
}; return fd;
} })()
: JSON.stringify(body);
if (body instanceof File) { const reqHeaders = () =>
const formData = new FormData(); body instanceof File
formData.append("photo", body); ? headers
const response = await fetch(apiRoot + path, { : { ...headers, "Content-Type": "application/json" };
method,
headers,
body: formData,
});
const json = (await response.json()) as Record<string, unknown>;
return json;
}
const response = await fetch(apiRoot + path, { const response = await fetch(apiRoot + path, {
method, method,
body, headers: reqHeaders(),
headers, body: reqBody(),
}); });
const json = (await response.json()) as Record<string, unknown>; return parser.parse(await response.json());
return json;
} }
export async function fetchJSONAuth( export async function fetchJSONAuth<T, P extends { parse: (string) => T }>(
path: string, path: string,
method: string, method: string,
parser: P,
body?: string | Record<string, unknown> | File, body?: string | Record<string, unknown> | File,
headers?: Record<string, unknown>, headers?: Record<string, unknown>,
): Promise<Record<string, unknown>> { ): Promise<T> {
if (token) { if (token) {
return fetchJSON(path, method, body, { return fetchJSON(path, method, parser, body, {
...headers, ...headers,
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}); });

View File

@@ -1,5 +1,5 @@
import { Action } from "redux"; import { Action } from "redux";
import { IUserAuthJSON } from "../../../../src/entity/User"; import { TUserAuthJSON } from "~/src/shared/types";
export enum AuthTypes { export enum AuthTypes {
AUTH_START = "AUTH_START", AUTH_START = "AUTH_START",
@@ -28,7 +28,7 @@ export interface ISignupStartAction extends Action {
export interface IAuthSuccessAction extends Action { export interface IAuthSuccessAction extends Action {
type: AuthTypes.AUTH_SUCCESS; type: AuthTypes.AUTH_SUCCESS;
payload: IUserAuthJSON; payload: TUserAuthJSON;
} }
export interface IAuthFailureAction extends Action { export interface IAuthFailureAction extends Action {
@@ -64,7 +64,7 @@ export function signupStart(
}; };
} }
export function authSuccess(user: IUserAuthJSON): IAuthSuccessAction { export function authSuccess(user: TUserAuthJSON): IAuthSuccessAction {
return { type: AuthTypes.AUTH_SUCCESS, payload: user }; return { type: AuthTypes.AUTH_SUCCESS, payload: user };
} }

View File

@@ -1,7 +1,7 @@
import { Reducer } from "react"; import { Reducer } from "react";
import { setToken } from "../../redux/api/utils"; import { setToken } from "~src/redux/api/utils";
import { UserAction, UserTypes } from "../../redux/user/actions"; import { UserAction, UserTypes } from "~src/redux/user/actions";
import { AuthAction, AuthTypes } from "./actions"; import { AuthAction, AuthTypes } from "./actions";
export interface IAuthState { export interface IAuthState {

View File

@@ -8,7 +8,7 @@ import {
race, race,
takeLatest, takeLatest,
} from "redux-saga/effects"; } from "redux-saga/effects";
import { login, signup } from "../../redux/api/auth"; import { login, signup } from "~src/redux/api/auth";
import { import {
authFail, authFail,
@@ -26,9 +26,8 @@ function* startSpinner() {
function* authStart(action: IAuthStartAction) { function* authStart(action: IAuthStartAction) {
const { username, password } = action.payload; const { username, password } = action.payload;
const spinner = yield fork(startSpinner);
try { try {
const spinner = yield fork(startSpinner);
const { response, timeout } = yield race({ const { response, timeout } = yield race({
response: call(login, username, password), response: call(login, username, password),
timeout: delay(10000), timeout: delay(10000),
@@ -47,15 +46,15 @@ function* authStart(action: IAuthStartAction) {
yield put(authFail(response.error)); yield put(authFail(response.error));
} }
} catch (e) { } catch (e) {
yield cancel(spinner);
yield put(authFail("Internal error")); yield put(authFail("Internal error"));
} }
} }
function* signupStart(action: ISignupStartAction) { function* signupStart(action: ISignupStartAction) {
const { username, password, email } = action.payload; const { username, password, email } = action.payload;
const spinner = yield fork(startSpinner);
try { try {
const spinner = yield fork(startSpinner);
const { response, timeout } = yield race({ const { response, timeout } = yield race({
response: call(signup, username, password, email), response: call(signup, username, password, email),
timeout: delay(10000), timeout: delay(10000),
@@ -74,6 +73,7 @@ function* signupStart(action: ISignupStartAction) {
yield put(authFail(response.error)); yield put(authFail(response.error));
} }
} catch (e) { } catch (e) {
yield cancel(spinner);
yield put(authFail(e.toString())); yield put(authFail(e.toString()));
} }
} }

View File

@@ -1,5 +1,5 @@
import { Reducer } from "react"; import { Reducer } from "react";
import { UserAction, UserTypes } from "../../redux/user/actions"; import { UserAction, UserTypes } from "~src/redux/user/actions";
import { LocalSettingsAction, LocalSettingsTypes } from "./actions"; import { LocalSettingsAction, LocalSettingsTypes } from "./actions";

View File

@@ -1,10 +1,10 @@
import { Action } from "redux"; import { Action } from "redux";
import { IPhotoReqJSON, Photo } from "../../../../src/entity/Photo"; import { TPhotoReqJSON } from "~src/shared/types";
import { import {
showPhotoCreateFailToast, showPhotoCreateFailToast,
showPhotoUploadFileFailToast, showPhotoUploadFileFailToast,
showPhotoUploadJSONFailToast, showPhotoUploadJSONFailToast,
} from "../../AppToaster"; } from "~src/AppToaster";
export enum PhotoTypes { export enum PhotoTypes {
PHOTOS_LOAD_START = "PHOTOS_LOAD", PHOTOS_LOAD_START = "PHOTOS_LOAD",
@@ -29,242 +29,242 @@ export enum PhotoTypes {
PHOTOS_DELETE_CANCEL = "PHOTOS_DELETE_CANCEL", PHOTOS_DELETE_CANCEL = "PHOTOS_DELETE_CANCEL",
} }
export interface IPhotosLoadStartAction extends Action { export interface TPhotosLoadStartAction extends Action {
type: PhotoTypes.PHOTOS_LOAD_START; type: PhotoTypes.PHOTOS_LOAD_START;
} }
export interface IPhotosLoadSuccessAction extends Action { export interface TPhotosLoadSuccessAction extends Action {
type: PhotoTypes.PHOTOS_LOAD_SUCCESS; type: PhotoTypes.PHOTOS_LOAD_SUCCESS;
photos: IPhotoReqJSON[]; photos: TPhotoReqJSON[];
} }
export interface IPhotosLoadFailAction extends Action { export interface TPhotosLoadFailAction extends Action {
type: PhotoTypes.PHOTOS_LOAD_FAIL; type: PhotoTypes.PHOTOS_LOAD_FAIL;
error: string; error: string;
} }
export interface IPhotoLoadStartAction extends Action { export interface TPhotoLoadStartAction extends Action {
type: PhotoTypes.PHOTO_LOAD_START; type: PhotoTypes.PHOTO_LOAD_START;
id: number; id: number;
} }
export interface IPhotoLoadSuccessAction extends Action { export interface TPhotoLoadSuccessAction extends Action {
type: PhotoTypes.PHOTO_LOAD_SUCCESS; type: PhotoTypes.PHOTO_LOAD_SUCCESS;
photo: IPhotoReqJSON; photo: TPhotoReqJSON;
} }
export interface IPhotoLoadFailAction extends Action { export interface TPhotoLoadFailAction extends Action {
type: PhotoTypes.PHOTO_LOAD_FAIL; type: PhotoTypes.PHOTO_LOAD_FAIL;
id: number; id: number;
error: string; error: string;
} }
export interface IPhotosUploadStartAction extends Action { export interface TPhotosUploadStartAction extends Action {
type: PhotoTypes.PHOTOS_UPLOAD_START; type: PhotoTypes.PHOTOS_UPLOAD_START;
files: FileList; files: FileList;
} }
export interface IPhotoCreateQueue extends Action { export interface TPhotoCreateQueue extends Action {
type: PhotoTypes.PHOTO_CREATE_QUEUE; type: PhotoTypes.PHOTO_CREATE_QUEUE;
file: File; file: File;
} }
export interface IPhotoUploadQueue extends Action { export interface TPhotoUploadQueue extends Action {
type: PhotoTypes.PHOTO_UPLOAD_QUEUE; type: PhotoTypes.PHOTO_UPLOAD_QUEUE;
file: File; file: File;
id: number; id: number;
} }
export interface IPhotoCreateStart extends Action { export interface TPhotoCreateStart extends Action {
type: PhotoTypes.PHOTO_CREATE_START; type: PhotoTypes.PHOTO_CREATE_START;
file: File; file: File;
} }
export interface IPhotoUploadStart extends Action { export interface TPhotoUploadStart extends Action {
type: PhotoTypes.PHOTO_UPLOAD_START; type: PhotoTypes.PHOTO_UPLOAD_START;
file: File; file: File;
id: number; id: number;
} }
export interface IPhotoUploadSuccessAction extends Action { export interface TPhotoUploadSuccessAction extends Action {
type: PhotoTypes.PHOTO_UPLOAD_SUCCESS; type: PhotoTypes.PHOTO_UPLOAD_SUCCESS;
photo: IPhotoReqJSON; photo: TPhotoReqJSON;
} }
export interface IPhotoUploadFailAction extends Action { export interface TPhotoUploadFailAction extends Action {
type: PhotoTypes.PHOTO_UPLOAD_FAIL; type: PhotoTypes.PHOTO_UPLOAD_FAIL;
photo: IPhotoReqJSON | number; photo: TPhotoReqJSON | number;
error: string; error: string;
} }
export interface IPhotoCreateSuccessAction extends Action { export interface TPhotoCreateSuccessAction extends Action {
type: PhotoTypes.PHOTO_CREATE_SUCCESS; type: PhotoTypes.PHOTO_CREATE_SUCCESS;
photo: IPhotoReqJSON; photo: TPhotoReqJSON;
file: File; file: File;
} }
export interface IPhotoCreateFailAction extends Action { export interface TPhotoCreateFailAction extends Action {
type: PhotoTypes.PHOTO_CREATE_FAIL; type: PhotoTypes.PHOTO_CREATE_FAIL;
file: File; file: File;
error: string; error: string;
} }
export interface IPhotosDeleteStartAction extends Action { export interface TPhotosDeleteStartAction extends Action {
type: PhotoTypes.PHOTOS_DELETE_START; type: PhotoTypes.PHOTOS_DELETE_START;
photos: IPhotoReqJSON[]; photos: TPhotoReqJSON[];
} }
export interface IPhotosDeleteSuccessAction extends Action { export interface TPhotosDeleteSuccessAction extends Action {
type: PhotoTypes.PHOTOS_DELETE_SUCCESS; type: PhotoTypes.PHOTOS_DELETE_SUCCESS;
photos: IPhotoReqJSON[]; photos: TPhotoReqJSON[];
} }
export interface IPhotosDeleteFailAction extends Action { export interface TPhotosDeleteFailAction extends Action {
type: PhotoTypes.PHOTOS_DELETE_FAIL; type: PhotoTypes.PHOTOS_DELETE_FAIL;
photos: IPhotoReqJSON[]; photos: TPhotoReqJSON[];
error?: string; error?: string;
} }
export interface IPhotosDeleteCancelAction extends Action { export interface TPhotosDeleteCancelAction extends Action {
type: PhotoTypes.PHOTOS_DELETE_CANCEL; type: PhotoTypes.PHOTOS_DELETE_CANCEL;
photos: IPhotoReqJSON[]; photos: TPhotoReqJSON[];
} }
export interface IPhotosStartFetchingSpinner extends Action { export interface TPhotosStartFetchingSpinner extends Action {
type: PhotoTypes.PHOTOS_START_FETCHING_SPINNER; type: PhotoTypes.PHOTOS_START_FETCHING_SPINNER;
} }
export function photoCreateQueue(file: File): IPhotoCreateQueue { export function photoCreateQueue(file: File): TPhotoCreateQueue {
return { type: PhotoTypes.PHOTO_CREATE_QUEUE, file }; return { type: PhotoTypes.PHOTO_CREATE_QUEUE, file };
} }
export function photoUploadQueue(file: File, id: number): IPhotoUploadQueue { export function photoUploadQueue(file: File, id: number): TPhotoUploadQueue {
return { type: PhotoTypes.PHOTO_UPLOAD_QUEUE, file, id }; return { type: PhotoTypes.PHOTO_UPLOAD_QUEUE, file, id };
} }
export function photoCreateStart(file: File): IPhotoCreateStart { export function photoCreateStart(file: File): TPhotoCreateStart {
return { type: PhotoTypes.PHOTO_CREATE_START, file }; return { type: PhotoTypes.PHOTO_CREATE_START, file };
} }
export function photoUploadStart(file: File, id: number): IPhotoUploadStart { export function photoUploadStart(file: File, id: number): TPhotoUploadStart {
return { type: PhotoTypes.PHOTO_UPLOAD_START, file, id }; return { type: PhotoTypes.PHOTO_UPLOAD_START, file, id };
} }
export function photosLoadStart(): IPhotosLoadStartAction { export function photosLoadStart(): TPhotosLoadStartAction {
return { type: PhotoTypes.PHOTOS_LOAD_START }; return { type: PhotoTypes.PHOTOS_LOAD_START };
} }
export function photoLoadStart(id: number): IPhotoLoadStartAction { export function photoLoadStart(id: number): TPhotoLoadStartAction {
return { type: PhotoTypes.PHOTO_LOAD_START, id }; return { type: PhotoTypes.PHOTO_LOAD_START, id };
} }
export function photosUploadStart(files: FileList): IPhotosUploadStartAction { export function photosUploadStart(files: FileList): TPhotosUploadStartAction {
return { type: PhotoTypes.PHOTOS_UPLOAD_START, files }; return { type: PhotoTypes.PHOTOS_UPLOAD_START, files };
} }
export function photoUploadSuccess( export function photoUploadSuccess(
photo: IPhotoReqJSON, photo: TPhotoReqJSON,
): IPhotoUploadSuccessAction { ): TPhotoUploadSuccessAction {
return { type: PhotoTypes.PHOTO_UPLOAD_SUCCESS, photo }; return { type: PhotoTypes.PHOTO_UPLOAD_SUCCESS, photo };
} }
export function photoUploadFail( export function photoUploadFail(
photo: IPhotoReqJSON | number, photo: TPhotoReqJSON | number,
error: string, error: string,
): IPhotoUploadFailAction { ): TPhotoUploadFailAction {
showPhotoUploadJSONFailToast(photo, error); showPhotoUploadJSONFailToast(photo, error);
return { type: PhotoTypes.PHOTO_UPLOAD_FAIL, photo, error }; return { type: PhotoTypes.PHOTO_UPLOAD_FAIL, photo, error };
} }
export function photoUploadFailWithFile( export function photoUploadFailWithFile(
photo: IPhotoReqJSON | number, photo: TPhotoReqJSON | number,
file: File, file: File,
error: string, error: string,
): IPhotoUploadFailAction { ): TPhotoUploadFailAction {
showPhotoUploadFileFailToast(file, error); showPhotoUploadFileFailToast(file, error);
return { type: PhotoTypes.PHOTO_UPLOAD_FAIL, photo, error }; return { type: PhotoTypes.PHOTO_UPLOAD_FAIL, photo, error };
} }
export function photoCreateSuccess( export function photoCreateSuccess(
photo: IPhotoReqJSON, photo: TPhotoReqJSON,
file: File, file: File,
): IPhotoCreateSuccessAction { ): TPhotoCreateSuccessAction {
return { type: PhotoTypes.PHOTO_CREATE_SUCCESS, photo, file }; return { type: PhotoTypes.PHOTO_CREATE_SUCCESS, photo, file };
} }
export function photoCreateFail( export function photoCreateFail(
file: File, file: File,
error: string, error: string,
): IPhotoCreateFailAction { ): TPhotoCreateFailAction {
showPhotoCreateFailToast(file, error); showPhotoCreateFailToast(file, error);
return { type: PhotoTypes.PHOTO_CREATE_FAIL, file, error }; return { type: PhotoTypes.PHOTO_CREATE_FAIL, file, error };
} }
export function photosLoadSuccess( export function photosLoadSuccess(
photos: IPhotoReqJSON[], photos: TPhotoReqJSON[],
): IPhotosLoadSuccessAction { ): TPhotosLoadSuccessAction {
return { type: PhotoTypes.PHOTOS_LOAD_SUCCESS, photos }; return { type: PhotoTypes.PHOTOS_LOAD_SUCCESS, photos };
} }
export function photosLoadFail(error: string): IPhotosLoadFailAction { export function photosLoadFail(error: string): TPhotosLoadFailAction {
return { type: PhotoTypes.PHOTOS_LOAD_FAIL, error }; return { type: PhotoTypes.PHOTOS_LOAD_FAIL, error };
} }
export function photoLoadSuccess( export function photoLoadSuccess(
photo: IPhotoReqJSON, photo: TPhotoReqJSON,
): IPhotoLoadSuccessAction { ): TPhotoLoadSuccessAction {
return { type: PhotoTypes.PHOTO_LOAD_SUCCESS, photo }; return { type: PhotoTypes.PHOTO_LOAD_SUCCESS, photo };
} }
export function photoLoadFail(id: number, error: string): IPhotoLoadFailAction { export function photoLoadFail(id: number, error: string): TPhotoLoadFailAction {
return { type: PhotoTypes.PHOTO_LOAD_FAIL, id, error }; return { type: PhotoTypes.PHOTO_LOAD_FAIL, id, error };
} }
export function photosDeleteStart( export function photosDeleteStart(
photos: IPhotoReqJSON[], photos: TPhotoReqJSON[],
): IPhotosDeleteStartAction { ): TPhotosDeleteStartAction {
return { type: PhotoTypes.PHOTOS_DELETE_START, photos }; return { type: PhotoTypes.PHOTOS_DELETE_START, photos };
} }
export function photosDeleteSuccess( export function photosDeleteSuccess(
photos: IPhotoReqJSON[], photos: TPhotoReqJSON[],
): IPhotosDeleteSuccessAction { ): TPhotosDeleteSuccessAction {
return { type: PhotoTypes.PHOTOS_DELETE_SUCCESS, photos }; return { type: PhotoTypes.PHOTOS_DELETE_SUCCESS, photos };
} }
export function photosDeleteFail( export function photosDeleteFail(
photos: IPhotoReqJSON[], photos: TPhotoReqJSON[],
error?: string, error?: string,
): IPhotosDeleteFailAction { ): TPhotosDeleteFailAction {
return { type: PhotoTypes.PHOTOS_DELETE_FAIL, photos, error }; return { type: PhotoTypes.PHOTOS_DELETE_FAIL, photos, error };
} }
export function photosDeleteCancel( export function photosDeleteCancel(
photos: IPhotoReqJSON[], photos: TPhotoReqJSON[],
): IPhotosDeleteCancelAction { ): TPhotosDeleteCancelAction {
return { type: PhotoTypes.PHOTOS_DELETE_CANCEL, photos }; return { type: PhotoTypes.PHOTOS_DELETE_CANCEL, photos };
} }
export function photosStartFetchingSpinner(): IPhotosStartFetchingSpinner { export function photosStartFetchingSpinner(): TPhotosStartFetchingSpinner {
return { type: PhotoTypes.PHOTOS_START_FETCHING_SPINNER }; return { type: PhotoTypes.PHOTOS_START_FETCHING_SPINNER };
} }
export type PhotoAction = export type PhotoAction =
| IPhotosLoadStartAction | TPhotosLoadStartAction
| IPhotosLoadFailAction | TPhotosLoadFailAction
| IPhotosLoadSuccessAction | TPhotosLoadSuccessAction
| IPhotosStartFetchingSpinner | TPhotosStartFetchingSpinner
| IPhotosUploadStartAction | TPhotosUploadStartAction
| IPhotoCreateFailAction | TPhotoCreateFailAction
| IPhotoCreateSuccessAction | TPhotoCreateSuccessAction
| IPhotoUploadFailAction | TPhotoUploadFailAction
| IPhotoUploadSuccessAction | TPhotoUploadSuccessAction
| IPhotosDeleteFailAction | TPhotosDeleteFailAction
| IPhotosDeleteStartAction | TPhotosDeleteStartAction
| IPhotosDeleteSuccessAction | TPhotosDeleteSuccessAction
| IPhotosDeleteCancelAction | TPhotosDeleteCancelAction
| IPhotoLoadFailAction | TPhotoLoadFailAction
| IPhotoLoadStartAction | TPhotoLoadStartAction
| IPhotoLoadSuccessAction | TPhotoLoadSuccessAction
| IPhotoUploadQueue | TPhotoUploadQueue
| IPhotoCreateQueue | TPhotoCreateQueue
| IPhotoCreateStart | TPhotoCreateStart
| IPhotoUploadStart; | TPhotoUploadStart;

View File

@@ -1,17 +1,17 @@
import { Reducer } from "redux"; import { Reducer } from "redux";
import { IPhotoJSON, IPhotoReqJSON } from "../../../../src/entity/Photo"; import { TPhotoReqJSON } from "~/src/shared/types";
import { UserAction, UserTypes } from "../../redux/user/actions"; import { UserAction, UserTypes } from "~src/redux/user/actions";
import { PhotoAction, PhotoTypes } from "./actions"; import { PhotoAction, PhotoTypes } from "./actions";
export interface IPhotoState { export interface TPhotoState {
fetching: boolean; fetching: boolean;
fetchingError: string | null; fetchingError: string | null;
} }
export interface IPhotosState { export interface TPhotosState {
photos: IPhotoReqJSON[]; photos: TPhotoReqJSON[];
photoStates: Record<number, IPhotoState>; photoStates: Record<number, TPhotoState>;
overviewFetching: boolean; overviewFetching: boolean;
allPhotosLoaded: boolean; allPhotosLoaded: boolean;
@@ -24,10 +24,10 @@ export interface IPhotosState {
photoUploadQueue: Record<number, File>; photoUploadQueue: Record<number, File>;
photosUploading: number; photosUploading: number;
deleteCache: Record<number, IPhotoReqJSON>; deleteCache: Record<number, TPhotoReqJSON>;
} }
const defaultPhotosState: IPhotosState = { const defaultPhotosState: TPhotosState = {
photos: [], photos: [],
allPhotosLoaded: false, allPhotosLoaded: false,
overviewFetching: false, overviewFetching: false,
@@ -45,12 +45,12 @@ const defaultPhotosState: IPhotosState = {
deleteCache: {}, deleteCache: {},
}; };
export function sortPhotos(photos: IPhotoReqJSON[]): IPhotoReqJSON[] { export function sortPhotos(photos: TPhotoReqJSON[]): TPhotoReqJSON[] {
return [...photos].sort((a, b) => b.shotAt - a.shotAt); return [...photos].sort((a, b) => b.shotAt - a.shotAt);
} }
export const photosReducer: Reducer<IPhotosState, PhotoAction> = ( export const photosReducer: Reducer<TPhotosState, PhotoAction> = (
state: IPhotosState = defaultPhotosState, state: TPhotosState = defaultPhotosState,
action: PhotoAction | UserAction, action: PhotoAction | UserAction,
) => { ) => {
switch (action.type) { switch (action.type) {
@@ -230,7 +230,7 @@ export const photosReducer: Reducer<IPhotosState, PhotoAction> = (
case PhotoTypes.PHOTOS_DELETE_FAIL: case PhotoTypes.PHOTOS_DELETE_FAIL:
case PhotoTypes.PHOTOS_DELETE_CANCEL: { case PhotoTypes.PHOTOS_DELETE_CANCEL: {
const delCache = { ...state.deleteCache }; const delCache = { ...state.deleteCache };
let photos: IPhotoReqJSON[] = [...state.photos]; let photos: TPhotoReqJSON[] = [...state.photos];
for (const photo of action.photos) { for (const photo of action.photos) {
if (delCache[photo.id]) { if (delCache[photo.id]) {
photos = sortPhotos([...photos, delCache[photo.id]]); photos = sortPhotos([...photos, delCache[photo.id]]);

View File

@@ -1,9 +1,7 @@
import { import {
all, all,
call, call,
cancel,
delay, delay,
fork,
put, put,
race, race,
takeLatest, takeLatest,
@@ -18,11 +16,11 @@ import {
fetchPhoto, fetchPhoto,
fetchPhotosList, fetchPhotosList,
uploadPhoto, uploadPhoto,
} from "../../redux/api/photos"; } from "~src/redux/api/photos";
import { import {
IPhotosDeleteStartAction, TPhotosDeleteStartAction,
IPhotoLoadStartAction, TPhotoLoadStartAction,
IPhotosUploadStartAction, TPhotosUploadStartAction,
photoCreateFail, photoCreateFail,
photoCreateQueue, photoCreateQueue,
photoCreateStart, photoCreateStart,
@@ -35,14 +33,12 @@ import {
photosLoadSuccess, photosLoadSuccess,
photosStartFetchingSpinner, photosStartFetchingSpinner,
PhotoTypes, PhotoTypes,
photoUploadFail,
photoUploadFailWithFile, photoUploadFailWithFile,
photoUploadQueue, photoUploadQueue,
photoUploadStart, photoUploadStart,
photoUploadSuccess, photoUploadSuccess,
} from "./actions"; } from "./actions";
import { IPhotosNewRespBody } from "../../../../src/routes/photos"; import { TPhotosNewRespBody, PhotosListPagination } from "~src/shared/types";
import { IPhotosListPagination } from "../../../../src/shared/types";
// Thanks, https://dev.to/qortex/compute-md5-checksum-for-a-file-in-typescript-59a4 // Thanks, https://dev.to/qortex/compute-md5-checksum-for-a-file-in-typescript-59a4
function computeChecksumMd5(file: File): Promise<string> { function computeChecksumMd5(file: File): Promise<string> {
@@ -109,12 +105,6 @@ function computeSize(f: File) {
}); });
} }
// Shouldn't be used anymore
function* startSpinner() {
yield delay(300);
yield put(photosStartFetchingSpinner());
}
function* photosLoad() { function* photosLoad() {
const state = yield select(); const state = yield select();
try { try {
@@ -122,7 +112,7 @@ function* photosLoad() {
const skip = state.photos.photos ? state.photos.photos.length : 0; const skip = state.photos.photos ? state.photos.photos.length : 0;
const { response, timeout } = yield race({ const { response, timeout } = yield race({
response: call(fetchPhotosList, skip, IPhotosListPagination), response: call(fetchPhotosList, skip, PhotosListPagination),
timeout: delay(10000), timeout: delay(10000),
}); });
@@ -143,7 +133,7 @@ function* photosLoad() {
} }
} }
function* photoLoad(action: IPhotoLoadStartAction) { function* photoLoad(action: TPhotoLoadStartAction) {
try { try {
//const spinner = yield fork(startSpinner); //const spinner = yield fork(startSpinner);
@@ -195,7 +185,7 @@ function* photoCreate() {
return; return;
} }
if (response.data || response.error === "Photo already exists") { if (response.data || response.error === "Photo already exists") {
const photo = (response as IPhotosNewRespBody).data; const photo = (response as TPhotosNewRespBody).data;
yield put(photoCreateSuccess(photo, f)); yield put(photoCreateSuccess(photo, f));
yield put(photoUploadQueue(f, photo.id)); yield put(photoUploadQueue(f, photo.id));
} else { } else {
@@ -243,14 +233,14 @@ function* photoUpload() {
} }
} }
function* photosUpload(action: IPhotosUploadStartAction) { function* photosUpload(action: TPhotosUploadStartAction) {
const files = Array.from(action.files); const files = Array.from(action.files);
for (const file of files) { for (const file of files) {
yield put(photoCreateQueue(file)); yield put(photoCreateQueue(file));
} }
} }
function* photosDelete(action: IPhotosDeleteStartAction) { function* photosDelete(action: TPhotosDeleteStartAction) {
try { try {
const { cancelled } = yield race({ const { cancelled } = yield race({
timeout: delay(3000), timeout: delay(3000),

View File

@@ -7,14 +7,14 @@ import {
ILocalSettingsState, ILocalSettingsState,
localSettingsReducer, localSettingsReducer,
} from "./localSettings/reducer"; } from "./localSettings/reducer";
import { IPhotosState, photosReducer } from "./photos/reducer"; import { TPhotosState, photosReducer } from "./photos/reducer";
import { IUserState, userReducer } from "./user/reducer"; import { TUserState, userReducer } from "./user/reducer";
export interface IAppState { export interface IAppState {
auth: IAuthState & PersistPartial; auth: IAuthState & PersistPartial;
user: IUserState; user: TUserState;
localSettings: ILocalSettingsState & PersistPartial; localSettings: ILocalSettingsState & PersistPartial;
photos: IPhotosState; photos: TPhotosState;
} }
const authPersistConfig = { const authPersistConfig = {

View File

@@ -1,21 +1,21 @@
import { applyMiddleware, createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension"; import { composeWithDevTools } from "redux-devtools-extension";
import { persistStore } from "redux-persist"; import { persistStore } from "redux-persist";
import createSagaMiddlware from "redux-saga"; import createSagaMiddlware from "redux-saga";
import { rootReducer } from "../redux/reducers"; import { configureStore } from "@reduxjs/toolkit";
import { setToken } from "./api/utils"; import { setToken } from "./api/utils";
import { authSaga } from "./auth/sagas"; import { authSaga } from "./auth/sagas";
import { photosSaga } from "./photos/sagas"; import { photosSaga } from "./photos/sagas";
import { getUser } from "./user/actions"; import { getUser } from "./user/actions";
import { userSaga } from "./user/sagas"; import { userSaga } from "./user/sagas";
import { rootReducer } from "../redux/reducers";
const sagaMiddleware = createSagaMiddlware(); const sagaMiddleware = createSagaMiddlware();
export const store = createStore( export const store = configureStore({
rootReducer, reducer: rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware)), middleware: [sagaMiddleware],
); });
export const persistor = persistStore(store, null, () => { export const persistor = persistStore(store, null, () => {
const state = store.getState(); const state = store.getState();

View File

@@ -1,6 +1,9 @@
import { Action } from "redux"; import { Action } from "redux";
import { IUserAuthJSON, IUserJSON } from "../../../../src/entity/User"; import { TUserAuthJSON } from "~src/shared/types";
import { showPasswordNotSavedToast, showPasswordSavedToast } from "../../AppToaster"; import {
showPasswordNotSavedToast,
showPasswordSavedToast,
} from "~src/AppToaster";
export enum UserTypes { export enum UserTypes {
USER_GET = "USER_GET", USER_GET = "USER_GET",
@@ -12,20 +15,20 @@ export enum UserTypes {
USER_PASS_CHANGE_FAIL = "USER_PASS_CHANGE_FAIL", USER_PASS_CHANGE_FAIL = "USER_PASS_CHANGE_FAIL",
} }
export interface IUserGetAction extends Action { export interface TUserGetAction extends Action {
type: UserTypes.USER_GET; type: UserTypes.USER_GET;
} }
export interface IUserLogoutAction extends Action { export interface TUserLogoutAction extends Action {
type: UserTypes.USER_LOGOUT; type: UserTypes.USER_LOGOUT;
} }
export interface IUserGetSuccessAction extends Action { export interface TUserGetSuccessAction extends Action {
type: UserTypes.USER_GET_SUCCESS; type: UserTypes.USER_GET_SUCCESS;
payload: IUserAuthJSON; payload: TUserAuthJSON;
} }
export interface IUserGetFailAction extends Action { export interface TUserGetFailAction extends Action {
type: UserTypes.USER_GET_FAIL; type: UserTypes.USER_GET_FAIL;
payload: { payload: {
error: string; error: string;
@@ -33,17 +36,17 @@ export interface IUserGetFailAction extends Action {
}; };
} }
export interface IUserPassChangeAction extends Action { export interface TUserPassChangeAction extends Action {
type: UserTypes.USER_PASS_CHANGE; type: UserTypes.USER_PASS_CHANGE;
password: string; password: string;
} }
export interface IUserPassChangeSuccessAction extends Action { export interface TUserPassChangeSuccessAction extends Action {
type: UserTypes.USER_PASS_CHANGE_SUCCESS; type: UserTypes.USER_PASS_CHANGE_SUCCESS;
payload: IUserAuthJSON; payload: TUserAuthJSON;
} }
export interface IUserPassChangeFailAction extends Action { export interface TUserPassChangeFailAction extends Action {
type: UserTypes.USER_PASS_CHANGE_FAIL; type: UserTypes.USER_PASS_CHANGE_FAIL;
payload: { payload: {
error: string; error: string;
@@ -51,32 +54,32 @@ export interface IUserPassChangeFailAction extends Action {
}; };
} }
export function getUser(): IUserGetAction { export function getUser(): TUserGetAction {
return { type: UserTypes.USER_GET }; return { type: UserTypes.USER_GET };
} }
export function logoutUser(): IUserLogoutAction { export function logoutUser(): TUserLogoutAction {
return { type: UserTypes.USER_LOGOUT }; return { type: UserTypes.USER_LOGOUT };
} }
export function getUserSuccess(user: IUserAuthJSON): IUserGetSuccessAction { export function getUserSuccess(user: TUserAuthJSON): TUserGetSuccessAction {
return { type: UserTypes.USER_GET_SUCCESS, payload: user }; return { type: UserTypes.USER_GET_SUCCESS, payload: user };
} }
export function getUserFail( export function getUserFail(
error: string, error: string,
logout: boolean, logout: boolean,
): IUserGetFailAction { ): TUserGetFailAction {
return { type: UserTypes.USER_GET_FAIL, payload: { error, logout } }; return { type: UserTypes.USER_GET_FAIL, payload: { error, logout } };
} }
export function userPassChange(password: string): IUserPassChangeAction { export function userPassChange(password: string): TUserPassChangeAction {
return { type: UserTypes.USER_PASS_CHANGE, password }; return { type: UserTypes.USER_PASS_CHANGE, password };
} }
export function userPassChangeSuccess( export function userPassChangeSuccess(
user: IUserAuthJSON, user: TUserAuthJSON,
): IUserPassChangeSuccessAction { ): TUserPassChangeSuccessAction {
showPasswordSavedToast(); showPasswordSavedToast();
return { type: UserTypes.USER_PASS_CHANGE_SUCCESS, payload: user }; return { type: UserTypes.USER_PASS_CHANGE_SUCCESS, payload: user };
} }
@@ -84,7 +87,7 @@ export function userPassChangeSuccess(
export function userPassChangeFail( export function userPassChangeFail(
error: string, error: string,
logout: boolean, logout: boolean,
): IUserPassChangeFailAction { ): TUserPassChangeFailAction {
showPasswordNotSavedToast(error); showPasswordNotSavedToast(error);
return { return {
type: UserTypes.USER_PASS_CHANGE_FAIL, type: UserTypes.USER_PASS_CHANGE_FAIL,
@@ -93,10 +96,10 @@ export function userPassChangeFail(
} }
export type UserAction = export type UserAction =
| IUserGetAction | TUserGetAction
| IUserGetSuccessAction | TUserGetSuccessAction
| IUserGetFailAction | TUserGetFailAction
| IUserLogoutAction | TUserLogoutAction
| IUserPassChangeAction | TUserPassChangeAction
| IUserPassChangeFailAction | TUserPassChangeFailAction
| IUserPassChangeSuccessAction; | TUserPassChangeSuccessAction;

View File

@@ -1,20 +1,20 @@
import { Reducer } from "react"; import { Reducer } from "react";
import { IUserJSON } from "../../../../src/entity/User"; import { TUserJSON } from "~/src/shared/types";
import { AuthAction, AuthTypes } from "../../redux/auth/actions"; import { AuthAction, AuthTypes } from "~src/redux/auth/actions";
import { UserAction, UserTypes } from "./actions"; import { UserAction, UserTypes } from "./actions";
export interface IUserState { export interface TUserState {
user: IUserJSON | null; user: TUserJSON | null;
} }
const defaultUserState: IUserState = { const defaultUserState: TUserState = {
user: null, user: null,
}; };
export const userReducer: Reducer<IUserState, AuthAction> = ( export const userReducer: Reducer<TUserState, AuthAction> = (
state: IUserState = defaultUserState, state: TUserState = defaultUserState,
action: AuthAction | UserAction, action: AuthAction | UserAction,
): IUserState => { ): TUserState => {
switch (action.type) { switch (action.type) {
case AuthTypes.AUTH_SUCCESS: case AuthTypes.AUTH_SUCCESS:
case UserTypes.USER_GET_SUCCESS: case UserTypes.USER_GET_SUCCESS:

View File

@@ -1,10 +1,10 @@
import { all, call, delay, put, race, takeLatest } from "redux-saga/effects"; import { all, call, delay, put, race, takeLatest } from "redux-saga/effects";
import { changeUserPassword, fetchUser } from "../../redux/api/user"; import { changeUserPassword, fetchUser } from "~src/redux/api/user";
import { import {
getUserFail, getUserFail,
getUserSuccess, getUserSuccess,
IUserPassChangeAction, TUserPassChangeAction,
userPassChangeFail, userPassChangeFail,
userPassChangeSuccess, userPassChangeSuccess,
UserTypes, UserTypes,
@@ -32,7 +32,7 @@ function* getUser() {
} }
} }
function* userPassChange(action: IUserPassChangeAction) { function* userPassChange(action: TUserPassChangeAction) {
try { try {
const { response, timeout } = yield race({ const { response, timeout } = yield race({
response: call(changeUserPassword, action.password), response: call(changeUserPassword, action.password),

1
frontend/src/shared Symbolic link
View File

@@ -0,0 +1 @@
../../shared

View File

@@ -15,7 +15,13 @@
"strictNullChecks": true, "strictNullChecks": true,
"skipLibCheck": true, "skipLibCheck": true,
"isolatedModules": true, "isolatedModules": true,
"downlevelIteration": true "downlevelIteration": true,
"baseUrl": "./",
"paths": {
"~*": [
"./*"
]
}
}, },
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./jest.config.js"] "include": ["./src/**/*.ts", "./src/**/*.tsx", "./jest.config.js"]
} }

12342
package-lock.json generated

File diff suppressed because it is too large Load Diff

102
package.json Executable file → Normal file
View File

@@ -1,93 +1,27 @@
{ {
"name": "photos", "name": "photos-root",
"version": "0.0.1",
"scripts": { "scripts": {
"start-frontend": "cd frontend && npm start", "dev-backend": "cd backend && npm run ts-node-dev",
"start": "ts-node -T -r tsconfig-paths/register src/server.ts", "dev-frontend": "cd frontend && npm run start",
"ts-node-dev": "ts-node-dev -r tsconfig-paths/register ./src/server.ts", "dev-all": "cross-env NODE_ENV=development concurrently npm:dev-backend npm:dev-frontend -c 'blue,green'",
"dev": "cross-env NODE_ENV=development concurrently npm:ts-node-dev npm:start-frontend -c 'blue,green'", "test-backend": "cd backend && npm run test",
"test": "cross-env NODE_ENV=test mocha --timeout 15000 -r ts-node/register -r tsconfig-paths/register --reporter mocha-multi-reporters --reporter-options configFile=mocha.json 'src/tests/**/*.ts' ", "test-frontend": "cd frontend && npm run test",
"test-frontend": "cd frontend && npm test", "test-all": "npm run test-backend && npm run test-frontend",
"test-all": "npm test && npm run test-frontend", "lint-backend": "cd backend && npm run lint",
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx && tsc --noEmit", "lint-backend-fix": "cd backend && npm run lint-fix",
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix",
"lint-frontend": "cd frontend && npm run lint", "lint-frontend": "cd frontend && npm run lint",
"lint-frontend-fix": "cd frontend && npm run lint-fix", "lint-frontend-fix": "cd frontend && npm run lint-fix",
"lint-all": "npm run lint && npm run lint-frontend", "lint-all": "npm run lint-backend && npm run lint-frontend",
"lint-all-fix": "npm run lint-fix && npm run lint-frontend-fix", "lint-all-fix": "npm run lint-backend-fix && npm run lint-frontend-fix",
"prettier-check": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --check", "prettier-check-backend": "cd backend && npm run prettier-check",
"prettify": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --write", "prettify-backend": "cd backend && npm run prettify",
"typeorm-dev": "cross-env NODE_ENV=development ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js", "prettier-check-frontend": "cd backend && npm run prettier-check",
"typeorm": "cross-env NODE_ENV=production ts-node -T -r tsconfig-paths/register ./node_modules/typeorm/cli.js" "prettify-frontend": "cd backend && npm run prettify",
}, "prettier-check-all": "npm run prettier-check-backend && npm run prettier-check-frontend",
"license": "MIT", "prettify-all": "npm run prettify-backend && npm run prettify-frontend"
"dependencies": {
"@koa/cors": "^4.0.0",
"@koa/router": "^12.0.0",
"bcrypt": "^5.1.0",
"class-validator": "^0.14.0",
"exifreader": "^4.13.0",
"hasha": "^5.2.2",
"jsonwebtoken": "^9.0.1",
"koa": "^2.14.2",
"koa-body": "^5.0.0",
"koa-jwt": "^4.0.4",
"koa-logger": "^3.2.1",
"koa-send": "^5.0.1",
"koa-sslify": "^5.0.1",
"koa-static": "^5.0.0",
"mime-types": "^2.1.35",
"mysql": "^2.18.1",
"sharp": "^0.32.4",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typeorm": "^0.2.41",
"typescript": "^5.1.6"
}, },
"devDependencies": { "devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/chai": "^4.3.5",
"@types/concurrently": "^6.4.0",
"@types/deasync": "^0.1.2",
"@types/eslint": "^8.44.1",
"@types/eslint-plugin-prettier": "^3.1.0",
"@types/jsonwebtoken": "^9.0.2",
"@types/koa": "^2.13.7",
"@types/koa__cors": "^4.0.0",
"@types/koa__router": "^12.0.0",
"@types/koa-logger": "^3.1.2",
"@types/koa-send": "^4.1.3",
"@types/koa-sslify": "^4.0.3",
"@types/koa-static": "^4.0.2",
"@types/mime-types": "^2.1.1",
"@types/mocha": "^10.0.1",
"@types/mysql": "^2.15.21",
"@types/prettier": "^2.7.3",
"@types/sharp": "^0.31.1",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"chai": "^4.3.7",
"concurrently": "^8.2.0", "concurrently": "^8.2.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3"
"eslint": "^8.46.0",
"eslint-config-prettier": "^8.9.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-prettier": "^5.0.0",
"husky": "^8.0.3",
"mocha": "^10.2.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"prettier": "^3.0.0",
"prettier-eslint": "^15.0.1",
"supertest": "^6.3.3",
"ts-node-dev": "^2"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint-all && npm run prettier-check"
}
} }
} }

15
shared/node_modules/.package-lock.json generated vendored Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "photos-shared",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/zod": {
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

21
shared/node_modules/zod/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Colin McDonnell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2834
shared/node_modules/zod/README.md generated vendored Normal file

File diff suppressed because it is too large Load Diff

2
shared/node_modules/zod/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1,2 @@
export * from "./lib";
export as namespace Zod;

163
shared/node_modules/zod/lib/ZodError.d.ts generated vendored Normal file
View File

@@ -0,0 +1,163 @@
import type { TypeOf, ZodType } from ".";
import { Primitive } from "./helpers/typeAliases";
import { util, ZodParsedType } from "./helpers/util";
declare type allKeys<T> = T extends any ? keyof T : never;
export declare type inferFlattenedErrors<T extends ZodType<any, any, any>, U = string> = typeToFlattenedError<TypeOf<T>, U>;
export declare type typeToFlattenedError<T, U = string> = {
formErrors: U[];
fieldErrors: {
[P in allKeys<T>]?: U[];
};
};
export declare const ZodIssueCode: {
invalid_type: "invalid_type";
invalid_literal: "invalid_literal";
custom: "custom";
invalid_union: "invalid_union";
invalid_union_discriminator: "invalid_union_discriminator";
invalid_enum_value: "invalid_enum_value";
unrecognized_keys: "unrecognized_keys";
invalid_arguments: "invalid_arguments";
invalid_return_type: "invalid_return_type";
invalid_date: "invalid_date";
invalid_string: "invalid_string";
too_small: "too_small";
too_big: "too_big";
invalid_intersection_types: "invalid_intersection_types";
not_multiple_of: "not_multiple_of";
not_finite: "not_finite";
};
export declare type ZodIssueCode = keyof typeof ZodIssueCode;
export declare type ZodIssueBase = {
path: (string | number)[];
message?: string;
};
export interface ZodInvalidTypeIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_type;
expected: ZodParsedType;
received: ZodParsedType;
}
export interface ZodInvalidLiteralIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_literal;
expected: unknown;
received: unknown;
}
export interface ZodUnrecognizedKeysIssue extends ZodIssueBase {
code: typeof ZodIssueCode.unrecognized_keys;
keys: string[];
}
export interface ZodInvalidUnionIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_union;
unionErrors: ZodError[];
}
export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_union_discriminator;
options: Primitive[];
}
export interface ZodInvalidEnumValueIssue extends ZodIssueBase {
received: string | number;
code: typeof ZodIssueCode.invalid_enum_value;
options: (string | number)[];
}
export interface ZodInvalidArgumentsIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_arguments;
argumentsError: ZodError;
}
export interface ZodInvalidReturnTypeIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_return_type;
returnTypeError: ZodError;
}
export interface ZodInvalidDateIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_date;
}
export declare type StringValidation = "email" | "url" | "emoji" | "uuid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "ip" | {
includes: string;
position?: number;
} | {
startsWith: string;
} | {
endsWith: string;
};
export interface ZodInvalidStringIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_string;
validation: StringValidation;
}
export interface ZodTooSmallIssue extends ZodIssueBase {
code: typeof ZodIssueCode.too_small;
minimum: number | bigint;
inclusive: boolean;
exact?: boolean;
type: "array" | "string" | "number" | "set" | "date" | "bigint";
}
export interface ZodTooBigIssue extends ZodIssueBase {
code: typeof ZodIssueCode.too_big;
maximum: number | bigint;
inclusive: boolean;
exact?: boolean;
type: "array" | "string" | "number" | "set" | "date" | "bigint";
}
export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_intersection_types;
}
export interface ZodNotMultipleOfIssue extends ZodIssueBase {
code: typeof ZodIssueCode.not_multiple_of;
multipleOf: number | bigint;
}
export interface ZodNotFiniteIssue extends ZodIssueBase {
code: typeof ZodIssueCode.not_finite;
}
export interface ZodCustomIssue extends ZodIssueBase {
code: typeof ZodIssueCode.custom;
params?: {
[k: string]: any;
};
}
export declare type DenormalizedError = {
[k: string]: DenormalizedError | string[];
};
export declare type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue;
export declare type ZodIssue = ZodIssueOptionalMessage & {
fatal?: boolean;
message: string;
};
export declare const quotelessJson: (obj: any) => string;
declare type recursiveZodFormattedError<T> = T extends [any, ...any[]] ? {
[K in keyof T]?: ZodFormattedError<T[K]>;
} : T extends any[] ? {
[k: number]: ZodFormattedError<T[number]>;
} : T extends object ? {
[K in keyof T]?: ZodFormattedError<T[K]>;
} : unknown;
export declare type ZodFormattedError<T, U = string> = {
_errors: U[];
} & recursiveZodFormattedError<NonNullable<T>>;
export declare type inferFormattedError<T extends ZodType<any, any, any>, U = string> = ZodFormattedError<TypeOf<T>, U>;
export declare class ZodError<T = any> extends Error {
issues: ZodIssue[];
get errors(): ZodIssue[];
constructor(issues: ZodIssue[]);
format(): ZodFormattedError<T>;
format<U>(mapper: (issue: ZodIssue) => U): ZodFormattedError<T, U>;
static create: (issues: ZodIssue[]) => ZodError<any>;
toString(): string;
get message(): string;
get isEmpty(): boolean;
addIssue: (sub: ZodIssue) => void;
addIssues: (subs?: ZodIssue[]) => void;
flatten(): typeToFlattenedError<T>;
flatten<U>(mapper?: (issue: ZodIssue) => U): typeToFlattenedError<T, U>;
get formErrors(): typeToFlattenedError<T, string>;
}
declare type stripPath<T extends object> = T extends any ? util.OmitKeys<T, "path"> : never;
export declare type IssueData = stripPath<ZodIssueOptionalMessage> & {
path?: (string | number)[];
fatal?: boolean;
};
export declare type ErrorMapCtx = {
defaultError: string;
data: any;
};
export declare type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => {
message: string;
};
export {};

132
shared/node_modules/zod/lib/ZodError.js generated vendored Normal file
View File

@@ -0,0 +1,132 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZodError = exports.quotelessJson = exports.ZodIssueCode = void 0;
const util_1 = require("./helpers/util");
exports.ZodIssueCode = util_1.util.arrayToEnum([
"invalid_type",
"invalid_literal",
"custom",
"invalid_union",
"invalid_union_discriminator",
"invalid_enum_value",
"unrecognized_keys",
"invalid_arguments",
"invalid_return_type",
"invalid_date",
"invalid_string",
"too_small",
"too_big",
"invalid_intersection_types",
"not_multiple_of",
"not_finite",
]);
const quotelessJson = (obj) => {
const json = JSON.stringify(obj, null, 2);
return json.replace(/"([^"]+)":/g, "$1:");
};
exports.quotelessJson = quotelessJson;
class ZodError extends Error {
constructor(issues) {
super();
this.issues = [];
this.addIssue = (sub) => {
this.issues = [...this.issues, sub];
};
this.addIssues = (subs = []) => {
this.issues = [...this.issues, ...subs];
};
const actualProto = new.target.prototype;
if (Object.setPrototypeOf) {
// eslint-disable-next-line ban/ban
Object.setPrototypeOf(this, actualProto);
}
else {
this.__proto__ = actualProto;
}
this.name = "ZodError";
this.issues = issues;
}
get errors() {
return this.issues;
}
format(_mapper) {
const mapper = _mapper ||
function (issue) {
return issue.message;
};
const fieldErrors = { _errors: [] };
const processError = (error) => {
for (const issue of error.issues) {
if (issue.code === "invalid_union") {
issue.unionErrors.map(processError);
}
else if (issue.code === "invalid_return_type") {
processError(issue.returnTypeError);
}
else if (issue.code === "invalid_arguments") {
processError(issue.argumentsError);
}
else if (issue.path.length === 0) {
fieldErrors._errors.push(mapper(issue));
}
else {
let curr = fieldErrors;
let i = 0;
while (i < issue.path.length) {
const el = issue.path[i];
const terminal = i === issue.path.length - 1;
if (!terminal) {
curr[el] = curr[el] || { _errors: [] };
// if (typeof el === "string") {
// curr[el] = curr[el] || { _errors: [] };
// } else if (typeof el === "number") {
// const errorArray: any = [];
// errorArray._errors = [];
// curr[el] = curr[el] || errorArray;
// }
}
else {
curr[el] = curr[el] || { _errors: [] };
curr[el]._errors.push(mapper(issue));
}
curr = curr[el];
i++;
}
}
}
};
processError(this);
return fieldErrors;
}
toString() {
return this.message;
}
get message() {
return JSON.stringify(this.issues, util_1.util.jsonStringifyReplacer, 2);
}
get isEmpty() {
return this.issues.length === 0;
}
flatten(mapper = (issue) => issue.message) {
const fieldErrors = {};
const formErrors = [];
for (const sub of this.issues) {
if (sub.path.length > 0) {
fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || [];
fieldErrors[sub.path[0]].push(mapper(sub));
}
else {
formErrors.push(mapper(sub));
}
}
return { formErrors, fieldErrors };
}
get formErrors() {
return this.flatten();
}
}
exports.ZodError = ZodError;
ZodError.create = (issues) => {
const error = new ZodError(issues);
return error;
};

17
shared/node_modules/zod/lib/__tests__/Mocker.d.ts generated vendored Normal file
View File

@@ -0,0 +1,17 @@
export declare class Mocker {
pick: (...args: any[]) => any;
get string(): string;
get number(): number;
get bigint(): bigint;
get boolean(): boolean;
get date(): Date;
get symbol(): symbol;
get null(): null;
get undefined(): undefined;
get stringOptional(): any;
get stringNullable(): any;
get numberOptional(): any;
get numberNullable(): any;
get booleanOptional(): any;
get booleanNullable(): any;
}

57
shared/node_modules/zod/lib/__tests__/Mocker.js generated vendored Normal file
View File

@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Mocker = void 0;
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
const testSymbol = Symbol("test");
class Mocker {
constructor() {
this.pick = (...args) => {
return args[getRandomInt(args.length)];
};
}
get string() {
return Math.random().toString(36).substring(7);
}
get number() {
return Math.random() * 100;
}
get bigint() {
return BigInt(Math.floor(Math.random() * 10000));
}
get boolean() {
return Math.random() < 0.5;
}
get date() {
return new Date(Math.floor(Date.now() * Math.random()));
}
get symbol() {
return testSymbol;
}
get null() {
return null;
}
get undefined() {
return undefined;
}
get stringOptional() {
return this.pick(this.string, this.undefined);
}
get stringNullable() {
return this.pick(this.string, this.null);
}
get numberOptional() {
return this.pick(this.number, this.undefined);
}
get numberNullable() {
return this.pick(this.number, this.null);
}
get booleanOptional() {
return this.pick(this.boolean, this.undefined);
}
get booleanNullable() {
return this.pick(this.boolean, this.null);
}
}
exports.Mocker = Mocker;

View File

@@ -0,0 +1,5 @@
import Benchmark from "benchmark";
declare const _default: {
suites: Benchmark.Suite[];
};
export default _default;

View File

@@ -0,0 +1,79 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const benchmark_1 = __importDefault(require("benchmark"));
const index_1 = require("../index");
const doubleSuite = new benchmark_1.default.Suite("z.discriminatedUnion: double");
const manySuite = new benchmark_1.default.Suite("z.discriminatedUnion: many");
const aSchema = index_1.z.object({
type: index_1.z.literal("a"),
});
const objA = {
type: "a",
};
const bSchema = index_1.z.object({
type: index_1.z.literal("b"),
});
const objB = {
type: "b",
};
const cSchema = index_1.z.object({
type: index_1.z.literal("c"),
});
const objC = {
type: "c",
};
const dSchema = index_1.z.object({
type: index_1.z.literal("d"),
});
const double = index_1.z.discriminatedUnion("type", [aSchema, bSchema]);
const many = index_1.z.discriminatedUnion("type", [aSchema, bSchema, cSchema, dSchema]);
doubleSuite
.add("valid: a", () => {
double.parse(objA);
})
.add("valid: b", () => {
double.parse(objB);
})
.add("invalid: null", () => {
try {
double.parse(null);
}
catch (err) { }
})
.add("invalid: wrong shape", () => {
try {
double.parse(objC);
}
catch (err) { }
})
.on("cycle", (e) => {
console.log(`${doubleSuite.name}: ${e.target}`);
});
manySuite
.add("valid: a", () => {
many.parse(objA);
})
.add("valid: c", () => {
many.parse(objC);
})
.add("invalid: null", () => {
try {
many.parse(null);
}
catch (err) { }
})
.add("invalid: wrong shape", () => {
try {
many.parse({ type: "unknown" });
}
catch (err) { }
})
.on("cycle", (e) => {
console.log(`${manySuite.name}: ${e.target}`);
});
exports.default = {
suites: [doubleSuite, manySuite],
};

1
shared/node_modules/zod/lib/benchmarks/index.d.ts generated vendored Normal file
View File

@@ -0,0 +1 @@
export {};

46
shared/node_modules/zod/lib/benchmarks/index.js generated vendored Normal file
View File

@@ -0,0 +1,46 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const discriminatedUnion_1 = __importDefault(require("./discriminatedUnion"));
const object_1 = __importDefault(require("./object"));
const primitives_1 = __importDefault(require("./primitives"));
const realworld_1 = __importDefault(require("./realworld"));
const string_1 = __importDefault(require("./string"));
const union_1 = __importDefault(require("./union"));
const argv = process.argv.slice(2);
let suites = [];
if (!argv.length) {
suites = [
...realworld_1.default.suites,
...primitives_1.default.suites,
...string_1.default.suites,
...object_1.default.suites,
...union_1.default.suites,
...discriminatedUnion_1.default.suites,
];
}
else {
if (argv.includes("--realworld")) {
suites.push(...realworld_1.default.suites);
}
if (argv.includes("--primitives")) {
suites.push(...primitives_1.default.suites);
}
if (argv.includes("--string")) {
suites.push(...string_1.default.suites);
}
if (argv.includes("--object")) {
suites.push(...object_1.default.suites);
}
if (argv.includes("--union")) {
suites.push(...union_1.default.suites);
}
if (argv.includes("--discriminatedUnion")) {
suites.push(...discriminatedUnion_1.default.suites);
}
}
for (const suite of suites) {
suite.run();
}

5
shared/node_modules/zod/lib/benchmarks/object.d.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
import Benchmark from "benchmark";
declare const _default: {
suites: Benchmark.Suite[];
};
export default _default;

70
shared/node_modules/zod/lib/benchmarks/object.js generated vendored Normal file
View File

@@ -0,0 +1,70 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const benchmark_1 = __importDefault(require("benchmark"));
const index_1 = require("../index");
const emptySuite = new benchmark_1.default.Suite("z.object: empty");
const shortSuite = new benchmark_1.default.Suite("z.object: short");
const longSuite = new benchmark_1.default.Suite("z.object: long");
const empty = index_1.z.object({});
const short = index_1.z.object({
string: index_1.z.string(),
});
const long = index_1.z.object({
string: index_1.z.string(),
number: index_1.z.number(),
boolean: index_1.z.boolean(),
});
emptySuite
.add("valid", () => {
empty.parse({});
})
.add("valid: extra keys", () => {
empty.parse({ string: "string" });
})
.add("invalid: null", () => {
try {
empty.parse(null);
}
catch (err) { }
})
.on("cycle", (e) => {
console.log(`${emptySuite.name}: ${e.target}`);
});
shortSuite
.add("valid", () => {
short.parse({ string: "string" });
})
.add("valid: extra keys", () => {
short.parse({ string: "string", number: 42 });
})
.add("invalid: null", () => {
try {
short.parse(null);
}
catch (err) { }
})
.on("cycle", (e) => {
console.log(`${shortSuite.name}: ${e.target}`);
});
longSuite
.add("valid", () => {
long.parse({ string: "string", number: 42, boolean: true });
})
.add("valid: extra keys", () => {
long.parse({ string: "string", number: 42, boolean: true, list: [] });
})
.add("invalid: null", () => {
try {
long.parse(null);
}
catch (err) { }
})
.on("cycle", (e) => {
console.log(`${longSuite.name}: ${e.target}`);
});
exports.default = {
suites: [emptySuite, shortSuite, longSuite],
};

View File

@@ -0,0 +1,5 @@
import Benchmark from "benchmark";
declare const _default: {
suites: Benchmark.Suite[];
};
export default _default;

136
shared/node_modules/zod/lib/benchmarks/primitives.js generated vendored Normal file
View File

@@ -0,0 +1,136 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const benchmark_1 = __importDefault(require("benchmark"));
const Mocker_1 = require("../__tests__/Mocker");
const index_1 = require("../index");
const val = new Mocker_1.Mocker();
const enumSuite = new benchmark_1.default.Suite("z.enum");
const enumSchema = index_1.z.enum(["a", "b", "c"]);
enumSuite
.add("valid", () => {
enumSchema.parse("a");
})
.add("invalid", () => {
try {
enumSchema.parse("x");
}
catch (e) { }
})
.on("cycle", (e) => {
console.log(`z.enum: ${e.target}`);
});
const undefinedSuite = new benchmark_1.default.Suite("z.undefined");
const undefinedSchema = index_1.z.undefined();
undefinedSuite
.add("valid", () => {
undefinedSchema.parse(undefined);
})
.add("invalid", () => {
try {
undefinedSchema.parse(1);
}
catch (e) { }
})
.on("cycle", (e) => {
console.log(`z.undefined: ${e.target}`);
});
const literalSuite = new benchmark_1.default.Suite("z.literal");
const short = "short";
const bad = "bad";
const literalSchema = index_1.z.literal("short");
literalSuite
.add("valid", () => {
literalSchema.parse(short);
})
.add("invalid", () => {
try {
literalSchema.parse(bad);
}
catch (e) { }
})
.on("cycle", (e) => {
console.log(`z.literal: ${e.target}`);
});
const numberSuite = new benchmark_1.default.Suite("z.number");
const numberSchema = index_1.z.number().int();
numberSuite
.add("valid", () => {
numberSchema.parse(1);
})
.add("invalid type", () => {
try {
numberSchema.parse("bad");
}
catch (e) { }
})
.add("invalid number", () => {
try {
numberSchema.parse(0.5);
}
catch (e) { }
})
.on("cycle", (e) => {
console.log(`z.number: ${e.target}`);
});
const dateSuite = new benchmark_1.default.Suite("z.date");
const plainDate = index_1.z.date();
const minMaxDate = index_1.z
.date()
.min(new Date("2021-01-01"))
.max(new Date("2030-01-01"));
dateSuite
.add("valid", () => {
plainDate.parse(new Date());
})
.add("invalid", () => {
try {
plainDate.parse(1);
}
catch (e) { }
})
.add("valid min and max", () => {
minMaxDate.parse(new Date("2023-01-01"));
})
.add("invalid min", () => {
try {
minMaxDate.parse(new Date("2019-01-01"));
}
catch (e) { }
})
.add("invalid max", () => {
try {
minMaxDate.parse(new Date("2031-01-01"));
}
catch (e) { }
})
.on("cycle", (e) => {
console.log(`z.date: ${e.target}`);
});
const symbolSuite = new benchmark_1.default.Suite("z.symbol");
const symbolSchema = index_1.z.symbol();
symbolSuite
.add("valid", () => {
symbolSchema.parse(val.symbol);
})
.add("invalid", () => {
try {
symbolSchema.parse(1);
}
catch (e) { }
})
.on("cycle", (e) => {
console.log(`z.symbol: ${e.target}`);
});
exports.default = {
suites: [
enumSuite,
undefinedSuite,
literalSuite,
numberSuite,
dateSuite,
symbolSuite,
],
};

View File

@@ -0,0 +1,5 @@
import Benchmark from "benchmark";
declare const _default: {
suites: Benchmark.Suite[];
};
export default _default;

56
shared/node_modules/zod/lib/benchmarks/realworld.js generated vendored Normal file
View File

@@ -0,0 +1,56 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const benchmark_1 = __importDefault(require("benchmark"));
const index_1 = require("../index");
const shortSuite = new benchmark_1.default.Suite("realworld");
const People = index_1.z.array(index_1.z.object({
type: index_1.z.literal("person"),
hair: index_1.z.enum(["blue", "brown"]),
active: index_1.z.boolean(),
name: index_1.z.string(),
age: index_1.z.number().int(),
hobbies: index_1.z.array(index_1.z.string()),
address: index_1.z.object({
street: index_1.z.string(),
zip: index_1.z.string(),
country: index_1.z.string(),
}),
}));
let i = 0;
function num() {
return ++i;
}
function str() {
return (++i % 100).toString(16);
}
function array(fn) {
return Array.from({ length: ++i % 10 }, () => fn());
}
const people = Array.from({ length: 100 }, () => {
return {
type: "person",
hair: i % 2 ? "blue" : "brown",
active: !!(i % 2),
name: str(),
age: num(),
hobbies: array(str),
address: {
street: str(),
zip: str(),
country: str(),
},
};
});
shortSuite
.add("valid", () => {
People.parse(people);
})
.on("cycle", (e) => {
console.log(`${shortSuite.name}: ${e.target}`);
});
exports.default = {
suites: [shortSuite],
};

5
shared/node_modules/zod/lib/benchmarks/string.d.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
import Benchmark from "benchmark";
declare const _default: {
suites: Benchmark.Suite[];
};
export default _default;

55
shared/node_modules/zod/lib/benchmarks/string.js generated vendored Normal file
View File

@@ -0,0 +1,55 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const benchmark_1 = __importDefault(require("benchmark"));
const index_1 = require("../index");
const SUITE_NAME = "z.string";
const suite = new benchmark_1.default.Suite(SUITE_NAME);
const empty = "";
const short = "short";
const long = "long".repeat(256);
const manual = (str) => {
if (typeof str !== "string") {
throw new Error("Not a string");
}
return str;
};
const stringSchema = index_1.z.string();
const optionalStringSchema = index_1.z.string().optional();
const optionalNullableStringSchema = index_1.z.string().optional().nullable();
suite
.add("empty string", () => {
stringSchema.parse(empty);
})
.add("short string", () => {
stringSchema.parse(short);
})
.add("long string", () => {
stringSchema.parse(long);
})
.add("optional string", () => {
optionalStringSchema.parse(long);
})
.add("nullable string", () => {
optionalNullableStringSchema.parse(long);
})
.add("nullable (null) string", () => {
optionalNullableStringSchema.parse(null);
})
.add("invalid: null", () => {
try {
stringSchema.parse(null);
}
catch (err) { }
})
.add("manual parser: long", () => {
manual(long);
})
.on("cycle", (e) => {
console.log(`${SUITE_NAME}: ${e.target}`);
});
exports.default = {
suites: [suite],
};

5
shared/node_modules/zod/lib/benchmarks/union.d.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
import Benchmark from "benchmark";
declare const _default: {
suites: Benchmark.Suite[];
};
export default _default;

79
shared/node_modules/zod/lib/benchmarks/union.js generated vendored Normal file
View File

@@ -0,0 +1,79 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const benchmark_1 = __importDefault(require("benchmark"));
const index_1 = require("../index");
const doubleSuite = new benchmark_1.default.Suite("z.union: double");
const manySuite = new benchmark_1.default.Suite("z.union: many");
const aSchema = index_1.z.object({
type: index_1.z.literal("a"),
});
const objA = {
type: "a",
};
const bSchema = index_1.z.object({
type: index_1.z.literal("b"),
});
const objB = {
type: "b",
};
const cSchema = index_1.z.object({
type: index_1.z.literal("c"),
});
const objC = {
type: "c",
};
const dSchema = index_1.z.object({
type: index_1.z.literal("d"),
});
const double = index_1.z.union([aSchema, bSchema]);
const many = index_1.z.union([aSchema, bSchema, cSchema, dSchema]);
doubleSuite
.add("valid: a", () => {
double.parse(objA);
})
.add("valid: b", () => {
double.parse(objB);
})
.add("invalid: null", () => {
try {
double.parse(null);
}
catch (err) { }
})
.add("invalid: wrong shape", () => {
try {
double.parse(objC);
}
catch (err) { }
})
.on("cycle", (e) => {
console.log(`${doubleSuite.name}: ${e.target}`);
});
manySuite
.add("valid: a", () => {
many.parse(objA);
})
.add("valid: c", () => {
many.parse(objC);
})
.add("invalid: null", () => {
try {
many.parse(null);
}
catch (err) { }
})
.add("invalid: wrong shape", () => {
try {
many.parse({ type: "unknown" });
}
catch (err) { }
})
.on("cycle", (e) => {
console.log(`${manySuite.name}: ${e.target}`);
});
exports.default = {
suites: [doubleSuite, manySuite],
};

5
shared/node_modules/zod/lib/errors.d.ts generated vendored Normal file
View File

@@ -0,0 +1,5 @@
import defaultErrorMap from "./locales/en";
import type { ZodErrorMap } from "./ZodError";
export { defaultErrorMap };
export declare function setErrorMap(map: ZodErrorMap): void;
export declare function getErrorMap(): ZodErrorMap;

17
shared/node_modules/zod/lib/errors.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getErrorMap = exports.setErrorMap = exports.defaultErrorMap = void 0;
const en_1 = __importDefault(require("./locales/en"));
exports.defaultErrorMap = en_1.default;
let overrideErrorMap = en_1.default;
function setErrorMap(map) {
overrideErrorMap = map;
}
exports.setErrorMap = setErrorMap;
function getErrorMap() {
return overrideErrorMap;
}
exports.getErrorMap = getErrorMap;

6
shared/node_modules/zod/lib/external.d.ts generated vendored Normal file
View File

@@ -0,0 +1,6 @@
export * from "./errors";
export * from "./helpers/parseUtil";
export * from "./helpers/typeAliases";
export * from "./helpers/util";
export * from "./types";
export * from "./ZodError";

18
shared/node_modules/zod/lib/external.js generated vendored Normal file
View File

@@ -0,0 +1,18 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./errors"), exports);
__exportStar(require("./helpers/parseUtil"), exports);
__exportStar(require("./helpers/typeAliases"), exports);
__exportStar(require("./helpers/util"), exports);
__exportStar(require("./types"), exports);
__exportStar(require("./ZodError"), exports);

Some files were not shown because too many files have changed in this diff Show More