29 Commits

Author SHA1 Message Date
dependabot[bot]
ff42d4564f Bump braces from 3.0.2 to 3.0.3 in /frontend
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-18 05:19:24 +00:00
30039af60a Update README.md (disable demo for now) 2024-04-06 09:18:45 +02: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
14210cf0cf updates people updates! 2023-07-28 21:55:02 +02:00
cc696978fb fix background click close 2023-07-25 20:11:26 +02:00
607b62386d fix previews not being scaled properly 2023-07-25 19:57:15 +02:00
dependabot[bot]
dbc2c14345 Bump cookiejar from 2.1.3 to 2.1.4
Bumps [cookiejar](https://github.com/bmeck/node-cookiejar) from 2.1.3 to 2.1.4.
- [Release notes](https://github.com/bmeck/node-cookiejar/releases)
- [Commits](https://github.com/bmeck/node-cookiejar/commits)

---
updated-dependencies:
- dependency-name: cookiejar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-10 18:57:57 +01:00
ee8a7412f4 fix dist being in the wrong place 2023-01-15 15:32:25 +01:00
b6a775ce5f fix for alpine 2023-01-15 15:23:09 +01:00
3d0dbca403 fix cache not being properly restored 2023-01-15 15:16:21 +01:00
b1a16f6fc2 update README 2023-01-15 15:13:59 +01:00
fe8f65ac10 actually update cache 2023-01-15 15:09:28 +01:00
bb1a0a8bf9 update readme 2023-01-15 14:55:53 +01:00
320de598eb ok now it should cache 2023-01-15 14:35:22 +01:00
f97e6aeb38 fix circleci frontend_build deps not caching 2023-01-15 13:40:38 +01:00
d6438dc778 circleci put docker_layer_caching option in the right place 2023-01-15 13:26:33 +01:00
593e2181f2 docker frontend cache doesn't depend on backend 2023-01-15 13:11:25 +01:00
a2cdbdef7f enable docker layer caching circleci 2023-01-15 12:54:02 +01:00
d081afefc6 fix frontend build dependencies not being properly cached 2023-01-15 12:44:26 +01:00
a2ebc390d5 do not require backend node_modules for frontend test 2023-01-15 12:41:38 +01:00
3b97652c02 make docker image smaller 2023-01-15 12:38:21 +01:00
cd9f3f9cc4 run migrations inside the app 2023-01-15 11:57:00 +01:00
132 changed files with 30179 additions and 21884 deletions

View File

@@ -2,7 +2,7 @@ version: 2
jobs:
test-backend:
docker:
- image: cimg/node:14.20
- image: cimg/node:16.20
- image: cimg/mariadb:10.8
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
@@ -14,50 +14,38 @@ jobs:
working_directory: ~/photos
steps:
# The checkout is INTO the working directory!!!
- checkout
- restore_cache:
keys:
- backend-dependencies-{{ checksum "package.json" }}
- backend-dependencies-{{ checksum "backend/package.json" }}
- run:
name: install backend deps
command: npm i
command: cd backend && npm i
- save_cache:
paths:
- node_modules
key: backend-dependencies-{{ checksum "package.json" }}
- backend/node_modules
key: backend-dependencies-{{ checksum "backend/package.json" }}
- run:
name: test backend
command: npm test
command: cd backend && npm test
- store_test_results:
path: backend-report.xml
path: ~/photos/backend/backend-report.xml
test-frontend:
docker:
- image: cimg/node:14.20
- image: cimg/node:16.20
working_directory: ~/photos
steps:
# The checkout is INTO the working directory!!!
- checkout
- restore_cache:
keys:
- backend-dependencies-{{ checksum "package.json" }}
- run:
name: install backend deps
command: npm i
- save_cache:
paths:
- node_modules
key: backend-dependencies-{{ checksum "package.json" }}
- restore_cache:
keys:
- frontend-dependencies-{{ checksum "frontend/package.json" }}
@@ -80,29 +68,17 @@ jobs:
test-frontend-build:
docker:
- image: cimg/node:14.20
- image: cimg/node:16.20
working_directory: ~/photos/frontend
working_directory: ~/photos
steps:
# The checkout is INTO the working directory!!!
- checkout:
- restore_cache:
keys:
- backend-dependencies-{{ checksum "package.json" }}
- run:
name: install backend deps
command: npm i
- save_cache:
paths:
- node_modules
key: backend-dependencies-{{ checksum "package.json" }}
- restore_cache:
keys:
- frontend-dependencies-{{ checksum "package.json" }}
- frontend-dependencies-{{ checksum "frontend/package.json" }}
- run:
name: install frontend deps
@@ -111,7 +87,7 @@ jobs:
- save_cache:
paths:
- frontend/node_modules
key: frontend-dependencies-{{ checksum "package.json" }}
key: frontend-dependencies-{{ checksum "frontend/package.json" }}
- run:
name: build frontend
@@ -123,6 +99,7 @@ jobs:
resource_class: large
steps:
# The checkout is INTO the working directory!!!
- checkout
- run:
name: log in to docker hub
@@ -134,12 +111,26 @@ jobs:
- run:
name: create docker builder
command: docker buildx create --use
command: docker buildx create --use --driver=docker-container
- restore_cache:
keys:
- buildx-photos-circleci-
- run:
name: build and push to docker hub
command: docker buildx build --push --platform linux/arm64,linux/amd64 --tag stepanusatiuk/photos:$CIRCLE_BRANCH .
#command: docker buildx build --push --platform linux/amd64 --tag stepanusatiuk/photos:$CIRCLE_BRANCH .
command: |
docker buildx build --progress=plain --push --platform linux/arm64,linux/amd64 --tag stepanusatiuk/photos:$CIRCLE_BRANCH \
--cache-to=type=local,mode=max,dest=/tmp/dockercache \
--cache-from=type=local,src=/tmp/dockercache .
- run:
name: prune cache
command: docker buildx prune --keep-storage=4gb --verbose
- save_cache:
key: buildx-photos-circleci-{{ checksum "/tmp/dockercache/index.json" }}
paths:
- /tmp/dockercache
# build-arm:
# machine:

View File

@@ -3,3 +3,8 @@ npm-debug.log
frontend/node_modules
frontend/npm-debug.log
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/
node_modules/
/node_modules
build/
tmp/
temp/
@@ -13,4 +14,4 @@ ormconfig.test.json
data_test
data_dev
data
backend-report.xml
backend-report.xml

View File

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

View File

@@ -1,21 +1,29 @@
# might fix this? https://github.com/parcel-bundler/parcel/issues/6735
FROM node:16-bullseye
FROM node:16-bullseye as frontbuild
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
RUN mkdir frontend
WORKDIR frontend
WORKDIR /usr/src/app/frontend
COPY ./frontend/package*.json ./
RUN npm ci --only=production
COPY ./frontend/. .
COPY ./shared ../shared
RUN npm run build && bash -O extglob -c 'rm -rfv !("dist")'
WORKDIR ../
RUN bash -O extglob -c 'rm -rfv !("frontend")'
COPY . .
FROM node:16-alpine as backexceptwithoutfrontend
WORKDIR frontend
RUN npm run build
WORKDIR ../
WORKDIR /usr/src/app
COPY ./backend/package*.json ./
RUN npm ci --only=production
COPY ./backend ./
RUN rm -rfv frontend && unlink src/shared
COPY ./shared src/shared
FROM backexceptwithoutfrontend
WORKDIR /usr/src/app
COPY --from=frontbuild /usr/src/app/frontend ./frontend
COPY ./dockerentry.sh .
#ENV PORT=8080
#ENV TYPEORM_HOST=localhost
@@ -39,6 +47,6 @@ ENV DATA_DIR=data\
#EXPOSE 8080
RUN ["chmod", "+x", "dockerentry.sh"]
RUN ["chmod", "+x", "./dockerentry.sh"]
CMD [ "./dockerentry.sh" ]

View File

@@ -1,10 +1,10 @@
# Photos
Something that tries to be a self-hosted alternative to Google Photos
<!---
Demo: https://photos.usatiuk.com
(no need to enter a real email, something like asdf@asdf.com is enough, data is stored on tmpfs, reset every day)
-->
![screenshot](docs/s1.png)
## Getting started
@@ -13,19 +13,16 @@ Demo: https://photos.usatiuk.com
First, install all of the dependencies with `npm i` and `cd frontend && npm i`
You also need to create a ormconfig.json and ormconfig.test.json (only if you
You also need to create a ormconfig.json and ormconfig.test.json (the latter only if you
want to run the tests). You can use ormconfig.example.json as an example (you
only need to change the database connection settings)
### Using docker
Open the workspace in a remote docker container using VSCode - everything should
be set up for you. You just need to rename `ormconfig.dockerdevexample.json` and
be set up for you. You might need to rename `ormconfig.dockerdevexample.json` and
`ormconfig.dockerdevexample.test.json` to `ormconfig.json` and
`ormconfig.test.json`
Also, you need to run database migrations with
`npm run typeorm-dev -- migration:run`
`ormconfig.test.json` and install dependencies too (although it should be automatic)
Then start with `npm run dev` and visit http://localhost:1234 (Parcel dev server
is listening at http://localhost:1234, and koa at http://localhost:3000)

View File

@@ -4,16 +4,17 @@
"plugins": [
"@typescript-eslint",
"prettier",
"import"
"import",
"mocha"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier/@typescript-eslint",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript"
"plugin:import/typescript",
"plugin:mocha/recommended"
],
"env": {
"node": true

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

7595
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

88
backend/package.json Executable 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.2.41",
"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 { devRouter } from "~routes/dev";
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");
@@ -35,6 +42,7 @@ app.use(
if (config.https) {
app.use(sslify({ resolver: xForwardedProtoResolver }));
}
app.use(
jwt({
secret: config.jwtSecret,
@@ -93,6 +101,6 @@ app.on("error", (err, ctx) => {
console.log(err);
ctx.body = {
error: err.message,
data: false,
};
data: null,
} as TAPIErrorResponse;
});

View File

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

View File

@@ -1,11 +1,10 @@
import * as path from "path";
import * as fs from "fs/promises";
import * as mime from "mime-types";
import { constants as fsConstants } from "fs";
import * as jwt from "jsonwebtoken";
import { TPhotoReqJSON, TPhotoJSON } from "~/shared/types";
import {
AfterRemove,
BaseEntity,
BeforeInsert,
BeforeRemove,
@@ -18,37 +17,18 @@ import {
} from "typeorm";
import { User } from "./User";
import {
isAlphanumeric,
IsAlphanumeric,
IsHash,
IsIn,
IsMimeType,
isNumber,
Length,
Matches,
validateOrReject,
} from "class-validator";
import { config } from "~config";
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 type ThumbSize = typeof thumbSizes[number];
export type ThumbSize = (typeof thumbSizes)[number];
@Entity()
@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)) {
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();
return { ...(await this.toJSON()), accessToken: token };
}

View File

@@ -2,9 +2,10 @@ import * as bcrypt from "bcrypt";
import * as jwt from "jsonwebtoken";
import * as path from "path";
import * as fs from "fs/promises";
import { TUserJSON, TUserAuthJSON } from "~/shared/types";
import {
AfterInsert,
AfterRemove,
BaseEntity,
BeforeInsert,
BeforeRemove,
@@ -17,25 +18,7 @@ import {
} from "typeorm";
import { config } from "../config";
import { Photo } from "./Photo";
import {
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;
}
import { IsAlphanumeric, IsEmail, validateOrReject } from "class-validator";
@Entity()
export class User extends BaseEntity {
@@ -100,12 +83,12 @@ export class User extends BaseEntity {
return validateOrReject(this);
}
public toJSON(): IUserJSON {
public toJSON(): TUserJSON {
const { id, username, isAdmin } = this;
return { id, username, isAdmin };
}
public toAuthJSON(): IUserAuthJSON {
public toAuthJSON(): TUserAuthJSON {
const json = this.toJSON();
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 { IPhotoReqJSON, Photo } from "~entity/Photo";
import { User } from "~entity/User";
import { IAPIResponse, IPhotosListPagination } from "~types";
import * as fs from "fs/promises";
import send = require("koa-send");
import { Photo } from "~entity/Photo";
import {
PhotoJSON,
PhotosDeleteBody,
PhotosListPagination,
PhotosNewPostBody,
TPhotoByIDDeleteRespBody,
TPhotoReqJSON,
TPhotosByIDGetRespBody,
TPhotosDeleteRespBody,
TPhotosGetShowTokenByIDRespBody,
TPhotosListRespBody,
TPhotosNewRespBody,
TPhotosUploadRespBody,
} from "~/shared/types";
import { getHash, getSize } from "~util";
import * as jwt from "jsonwebtoken";
import { config } from "~config";
import { ValidationError } from "class-validator";
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 {
hash: string | undefined;
size: string | undefined;
format: string | undefined;
}
export type IPhotosNewRespBody = IAPIResponse<IPhotoReqJSON>;
photosRouter.post("/photos/new", async (ctx) => {
// Typescript requires explicit type annotations for CFA......
type ContextType = Parameters<
Parameters<(typeof photosRouter)["post"]>["2"]
>["0"];
photosRouter.post("/photos/new", async (ctx: ContextType) => {
if (!ctx.state.user) {
ctx.throw(401);
}
const { user } = ctx.state;
const body = ctx.request.body as IPhotosNewPostBody;
const body = PhotosNewPostBody.parse(ctx.request.body);
const { hash, size, format } = body;
if (!(hash && size && format)) {
ctx.throw(400);
return;
}
const photo = new Photo(user, hash, size, format);
const photo = Photo.create({ user, hash, size, format });
try {
await photo.save();
} catch (e) {
if (e.code === "ER_DUP_ENTRY") {
const photo = await Photo.findOne({ hash, size, user });
const photo = await Photo.findOne({ hash, size, user }, {});
if (!photo) {
ctx.throw(404);
return;
}
ctx.body = {
error: false,
data: await photo.toReqJSON(),
} as IPhotosNewRespBody;
} as TPhotosNewRespBody;
return;
}
if (
@@ -55,7 +60,6 @@ photosRouter.post("/photos/new", async (ctx) => {
(Array.isArray(e) && e.some((e) => e instanceof ValidationError))
) {
ctx.throw(400);
return;
}
console.log(e);
ctx.throw(500);
@@ -64,11 +68,10 @@ photosRouter.post("/photos/new", async (ctx) => {
ctx.body = {
error: false,
data: await photo.toReqJSON(),
} as IPhotosNewRespBody;
} as TPhotosNewRespBody;
});
export type IPhotosUploadRespBody = IAPIResponse<IPhotoReqJSON>;
photosRouter.post("/photos/upload/:id", async (ctx) => {
photosRouter.post("/photos/upload/:id", async (ctx: ContextType) => {
if (!ctx.state.user) {
ctx.throw(401);
}
@@ -79,32 +82,28 @@ photosRouter.post("/photos/upload/:id", async (ctx) => {
if (!id) {
ctx.throw(400);
return;
}
const { user } = ctx.state;
const photo = await Photo.findOne({ id: parseInt(id), user });
const photo = await Photo.findOne({ id: parseInt(id), user }, {});
if (!photo) {
ctx.throw(404);
return;
}
if (!ctx.request.files || Object.keys(ctx.request.files).length === 0) {
ctx.throw(400, "No file");
return;
}
if (photo.uploaded) {
ctx.throw(400, "Already uploaded");
return;
}
if (ctx.request.files) {
const files = ctx.request.files;
if (Object.keys(files).length > 1) {
ctx.throw(400, "Too many files");
return;
}
const file = Object.values(files)[0];
if (Array.isArray(file)) {
throw "more than one file uploaded";
@@ -115,7 +114,6 @@ photosRouter.post("/photos/upload/:id", async (ctx) => {
if (photoHash !== photo.hash || photoSize !== photo.size) {
ctx.throw(400, "Wrong photo");
return;
}
try {
@@ -129,54 +127,53 @@ photosRouter.post("/photos/upload/:id", async (ctx) => {
ctx.body = {
error: false,
data: await photo.toReqJSON(),
} as IPhotosUploadRespBody;
} as TPhotosUploadRespBody;
});
/**
export interface IPhotosByIDPatchBody {
}
export type IPhotosByIDPatchRespBody = IAPIResponse<IPhotoReqJSON>;
photosRouter.patch("/photos/byID/:id", async (ctx) => {
if (!ctx.state.user) {
ctx.throw(401);
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 interface TPhotosByIDPatchBody {
}
export type TPhotosByIDPatchRespBody = IAPIResponse<TPhotoReqJSON>;
photosRouter.patch("/photos/byID/:id", async (ctx: ContextType) => {
if (!ctx.state.user) {
ctx.throw(401);
return;
}
export type IPhotosListRespBody = IAPIResponse<IPhotoReqJSON[]>;
photosRouter.get("/photos/list", async (ctx) => {
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(),
};
});
*/
photosRouter.get("/photos/list", async (ctx: ContextType) => {
if (!ctx.state.user) {
ctx.throw(401);
}
@@ -196,8 +193,8 @@ photosRouter.get("/photos/list", async (ctx) => {
skip = parseInt(skip);
}
if (!num || num > IPhotosListPagination) {
num = IPhotosListPagination;
if (!num || num > PhotosListPagination) {
num = PhotosListPagination;
}
const photos = await Photo.find({
@@ -207,18 +204,17 @@ photosRouter.get("/photos/list", async (ctx) => {
order: { shotAt: "DESC" },
});
const photosList: IPhotoReqJSON[] = await Promise.all(
const photosList: TPhotoReqJSON[] = await Promise.all(
photos.map(async (photo) => await photo.toReqJSON()),
);
ctx.body = {
error: false,
data: photosList,
} as IPhotosListRespBody;
} as TPhotosListRespBody;
});
export type IPhotosByIDGetRespBody = IAPIResponse<IPhotoReqJSON>;
photosRouter.get("/photos/byID/:id", async (ctx) => {
photosRouter.get("/photos/byID/:id", async (ctx: ContextType) => {
if (!ctx.state.user) {
ctx.throw(401);
}
@@ -229,25 +225,23 @@ photosRouter.get("/photos/byID/:id", async (ctx) => {
if (!id) {
ctx.throw(400);
return;
}
const { user } = ctx.state;
const photo = await Photo.findOne({ id: parseInt(id), user });
const photo = await Photo.findOne({ id: parseInt(id), user }, {});
if (!photo) {
ctx.throw(404);
return;
}
ctx.body = {
error: false,
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 {
id: string | undefined;
token: string | undefined;
@@ -255,26 +249,27 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
if (!(id && token)) {
ctx.throw(400);
return;
}
try {
jwt.verify(token, config.jwtSecret) as IPhotoReqJSON;
jwt.verify(token, config.jwtSecret);
} catch (e) {
ctx.throw(401);
}
const photoReqJSON = jwt.decode(token) as IPhotoReqJSON;
const photoReqJSON = PhotoJSON.parse(jwt.decode(token));
const { user } = photoReqJSON;
const photo = await Photo.findOne({
id: parseInt(id),
user: { id: user },
});
const photo = await Photo.findOne(
{
id: parseInt(id),
user: { id: user },
},
{},
);
if (!photo) {
ctx.throw(404);
return;
}
if (
@@ -289,7 +284,7 @@ photosRouter.get("/photos/showByID/:id/:token", async (ctx) => {
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) {
ctx.throw(401);
}
@@ -300,16 +295,14 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => {
if (!id) {
ctx.throw(400);
return;
}
const { user } = ctx.state;
const photo = await Photo.findOne({ id: parseInt(id), user });
const photo = await Photo.findOne({ id: parseInt(id), user }, {});
if (!photo) {
ctx.throw(404);
return;
}
if (
@@ -324,9 +317,7 @@ photosRouter.get("/photos/showByID/:id", async (ctx) => {
await send(ctx, await photo.getReadyPath("original"));
});
export type IPhotoShowToken = string;
export type IPhotosGetShowTokenByID = IAPIResponse<IPhotoShowToken>;
photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => {
photosRouter.get("/photos/getShowByIDToken/:id", async (ctx: ContextType) => {
if (!ctx.state.user) {
ctx.throw(401);
}
@@ -337,24 +328,21 @@ photosRouter.get("/photos/getShowByIDToken/:id", async (ctx) => {
if (!id) {
ctx.throw(400);
return;
}
const { user } = ctx.state;
const photo = await Photo.findOne({ id: parseInt(id), user });
const photo = await Photo.findOne({ id: parseInt(id), user }, {});
if (!photo) {
ctx.throw(404);
return;
}
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) => {
photosRouter.delete("/photos/byID/:id", async (ctx: ContextType) => {
if (!ctx.state.user) {
ctx.throw(401);
}
@@ -365,16 +353,14 @@ photosRouter.delete("/photos/byID/:id", async (ctx) => {
if (!id) {
ctx.throw(400);
return;
}
const { user } = ctx.state;
const photo = await Photo.findOne({ id: parseInt(id), user });
const photo = await Photo.findOne({ id: parseInt(id), user }, {});
if (!photo) {
ctx.throw(404);
return;
}
await photo.remove();
@@ -382,27 +368,17 @@ photosRouter.delete("/photos/byID/:id", async (ctx) => {
ctx.body = {
error: false,
data: true,
} as IPhotoByIDDeleteRespBody;
} as TPhotoByIDDeleteRespBody;
});
export interface IPhotosDeleteBody {
photos: IPhotoReqJSON[];
}
export type IPhotosDeleteRespBody = IAPIResponse<boolean>;
photosRouter.post("/photos/delete", async (ctx) => {
photosRouter.post("/photos/delete", async (ctx: ContextType) => {
if (!ctx.state.user) {
ctx.throw(401);
}
const body = ctx.request.body as IPhotosDeleteBody;
const body = PhotosDeleteBody.parse(ctx.request.body);
const { photos } = body;
if (!photos || !Array.isArray(photos) || photos.length == 0) {
ctx.throw(400);
return;
}
const { user } = ctx.state;
try {
await Photo.delete({
@@ -413,11 +389,11 @@ photosRouter.post("/photos/delete", async (ctx) => {
ctx.body = {
error: false,
data: true,
} as IPhotosDeleteRespBody;
} as TPhotosDeleteRespBody;
} catch (e) {
ctx.body = {
data: null,
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,7 +1,8 @@
import { Connection } from "typeorm";
import { Config, ConfigKey, setConfigValue } from "~entity/Config";
import { app } from "./app";
import { app } from "~app";
import { config } from "./config";
import { connect } from "./config/database";
import { connect } from "~config/database";
async function readConfig() {
if (process.env.SIGNUP_ALLOWED) {
@@ -25,18 +26,29 @@ async function dumpConfig() {
console.log(`${entry.key} = ${entry.value}`);
}
}
async function migrate(connection: Connection) {
await connection.runMigrations();
console.log("Migrations ran");
}
async function startApp() {
app.listen(config.port);
console.log(`Listening at ${config.port}`);
}
connect()
.then((connection) => {
console.log(`Connected to ${connection.name}`);
const startApp = () => {
app.listen(config.port);
console.log(`Listening at ${config.port}`);
};
readConfig()
migrate(connection)
.then(() =>
dumpConfig()
.then(() => startApp())
readConfig()
.then(() =>
dumpConfig()
.then(() => startApp())
.catch((e) => console.log(e)),
)
.catch((e) => console.log(e)),
)
.catch((e) => console.log(e));

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 { getConnection } from "typeorm";
import { app } from "~app";
import { Photo, IPhotoReqJSON } from "~entity/Photo";
import { Photo } from "~entity/Photo";
import {
IPhotosDeleteBody,
IPhotosListRespBody,
IPhotosNewPostBody,
} from "~routes/photos";
TPhotoReqJSON,
TPhotosDeleteBody,
TPhotosListRespBody,
TPhotosNewPostBody,
} from "~shared/types";
import * as fs from "fs/promises";
import { constants as fsConstants } from "fs";
import * as jwt from "jsonwebtoken";
@@ -30,7 +31,6 @@ import {
prepareMetadata,
seedDB,
} from "./util";
import { sleep } from "deasync";
import { config } from "~config";
const callback = app.callback();
@@ -61,7 +61,7 @@ describe("photos", function () {
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();
@@ -86,7 +86,7 @@ describe("photos", function () {
Authorization: `Bearer ${seed.user2.toJWT()}`,
})
.expect(200);
expect(parseInt(response.header["content-length"])).to.equal(
expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogFileSize,
);
});
@@ -98,12 +98,12 @@ describe("photos", function () {
Authorization: `Bearer ${seed.user2.toJWT()}`,
})
.expect(200);
expect(parseInt(response.header["content-length"])).to.equal(
expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogFileSize,
);
await fs.unlink(await seed.dogPhoto.getReadyPath("original"));
const response2 = await request(callback)
await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}`)
.set({
Authorization: `Bearer ${seed.user2.toJWT()}`,
@@ -121,15 +121,15 @@ describe("photos", function () {
})
.expect(200);
const dogSmallThumbSize = (
await fs.stat(await seed.dogPhoto.getThumbPath("512"))
await fs.stat(seed.dogPhoto.getThumbPath("512"))
).size;
expect(parseInt(response.header["content-length"])).to.equal(
expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogSmallThumbSize,
);
await fs.unlink(await seed.dogPhoto.getReadyPath("512"));
await fs.unlink(await seed.dogPhoto.getReadyPath("original"));
const response2 = await request(callback)
await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}?size=512`)
.set({
Authorization: `Bearer ${seed.user2.toJWT()}`,
@@ -147,7 +147,7 @@ describe("photos", function () {
Authorization: `Bearer ${seed.user2.toJWT()}`,
})
.expect(200);
expect(parseInt(response.header["content-length"])).to.be.lessThan(
expect(parseInt(response.get("content-length") ?? "")).to.be.lessThan(
dogFileSize,
);
});
@@ -160,23 +160,23 @@ describe("photos", function () {
})
.expect(200);
const dogSmallThumbSize = (
await fs.stat(await seed.dogPhoto.getThumbPath("512"))
await fs.stat(seed.dogPhoto.getThumbPath("512"))
).size;
expect(parseInt(response.header["content-length"])).to.equal(
expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogSmallThumbSize,
);
await fs.unlink(seed.dogPhoto.getThumbPath("512"));
const response2 = await request(callback)
await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}?size=512`)
.set({
Authorization: `Bearer ${seed.user2.toJWT()}`,
})
.expect(200);
const dogSmallThumbSize2 = (
await fs.stat(await seed.dogPhoto.getThumbPath("512"))
await fs.stat(seed.dogPhoto.getThumbPath("512"))
).size;
expect(parseInt(response.header["content-length"])).to.equal(
expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogSmallThumbSize2,
);
});
@@ -189,7 +189,7 @@ describe("photos", function () {
})
.expect(200);
const listRespBody = listResp.body as IPhotosListRespBody;
const listRespBody = listResp.body as TPhotosListRespBody;
if (listRespBody.error !== false) {
expect(listResp.body.error).to.be.false;
@@ -202,7 +202,7 @@ describe("photos", function () {
const listAnyResp = await request(callback)
.get(`/photos/showByID/${photos[0].id}/${photos[0].accessToken}`)
.expect(200);
expect(parseInt(listAnyResp.header["content-length"])).to.be.oneOf([
expect(parseInt(listAnyResp.get("content-length") ?? "")).to.be.oneOf([
dogFileSize,
catFileSize,
]);
@@ -220,7 +220,7 @@ describe("photos", function () {
const response = await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}/${token}`)
.expect(200);
expect(parseInt(response.header["content-length"])).to.equal(
expect(parseInt(response.get("content-length") ?? "")).to.equal(
dogFileSize,
);
@@ -235,7 +235,7 @@ describe("photos", function () {
const responseSS = await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}/${tokenSelfSigned}`)
.expect(200);
expect(parseInt(responseSS.header["content-length"])).to.equal(
expect(parseInt(responseSS.get("content-length") ?? "")).to.equal(
dogFileSize,
);
});
@@ -249,7 +249,7 @@ describe("photos", function () {
},
);
const response = await request(callback)
await request(callback)
.get(`/photos/showByID/${seed.dogPhoto.id}/${token}`)
.expect(401);
});
@@ -276,17 +276,17 @@ describe("photos", function () {
hash: dogHash,
size: dogSize,
format: dogFormat,
} as IPhotosNewPostBody)
} as TPhotosNewPostBody)
.expect(200);
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);
const dbPhoto = await Photo.findOneOrFail({
id: photo.id,
user: seed.user1.id as any,
user: { id: seed.user1.id },
});
expect(dbPhoto.hash).to.be.equal(dogHash);
@@ -303,7 +303,7 @@ describe("photos", function () {
const dbPhotoUpl = await Photo.findOneOrFail({
id: photo.id,
user: seed.user1.id as any,
user: { id: seed.user1.id },
});
expect(dbPhotoUpl.hash).to.be.equal(dogHash);
expect(await dbPhotoUpl.origFileExists()).to.be.equal(true);
@@ -318,7 +318,7 @@ describe("photos", function () {
})
.expect(200);
expect(parseInt(showResp.header["content-length"])).to.equal(
expect(parseInt(showResp.get("content-length") ?? "")).to.equal(
dogFileSize,
);
});
@@ -334,17 +334,17 @@ describe("photos", function () {
hash: pngHash,
size: pngSize,
format: pngFormat,
} as IPhotosNewPostBody)
} as TPhotosNewPostBody)
.expect(200);
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);
const dbPhoto = await Photo.findOneOrFail({
id: photo.id,
user: seed.user1.id as any,
user: { id: seed.user1.id },
});
expect(dbPhoto.hash).to.be.equal(pngHash);
@@ -361,7 +361,7 @@ describe("photos", function () {
const dbPhotoUpl = await Photo.findOneOrFail({
id: photo.id,
user: seed.user1.id as any,
user: { id: seed.user1.id },
});
expect(dbPhotoUpl.hash).to.be.equal(pngHash);
expect(dbPhotoUpl.format).to.be.equal(pngFormat);
@@ -378,7 +378,7 @@ describe("photos", function () {
})
.expect(200);
expect(parseInt(showResp.header["content-length"])).to.equal(
expect(parseInt(showResp.get("content-length") ?? "")).to.equal(
pngFileSize,
);
});
@@ -394,12 +394,12 @@ describe("photos", function () {
hash: dogHash,
size: dogSize,
format: dogFormat,
} as IPhotosNewPostBody)
} as TPhotosNewPostBody)
.expect(200);
expect(response.body.error).to.be.false;
const response2 = await request(callback)
await request(callback)
.post("/photos/new")
.set({
Authorization: `Bearer ${seed.user1.toJWT()}`,
@@ -409,7 +409,7 @@ describe("photos", function () {
hash: dogHash,
size: dogSize,
format: dogFormat,
} as IPhotosNewPostBody)
} as TPhotosNewPostBody)
.expect(200);
const dbPhoto = await Photo.find({
@@ -431,17 +431,17 @@ describe("photos", function () {
hash: dogHash,
size: dogSize,
format: dogFormat,
} as IPhotosNewPostBody)
} as TPhotosNewPostBody)
.expect(200);
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);
const dbPhoto = await Photo.findOneOrFail({
id: photo.id,
user: seed.user1.id as any,
user: { id: seed.user1.id },
});
expect(dbPhoto.hash).to.be.equal(dogHash);
@@ -474,7 +474,7 @@ describe("photos", function () {
})
.expect(200);
expect(parseInt(showResp.header["content-length"])).to.equal(
expect(parseInt(showResp.get("content-length") ?? "")).to.equal(
dogFileSize,
);
});
@@ -490,17 +490,17 @@ describe("photos", function () {
hash: dogHash,
size: dogSize,
format: dogFormat,
} as IPhotosNewPostBody)
} as TPhotosNewPostBody)
.expect(200);
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);
const dbPhoto = await Photo.findOneOrFail({
id: photo.id,
user: seed.user1.id as any,
user: { id: seed.user1.id },
});
expect(dbPhoto.hash).to.be.equal(dogHash);
@@ -517,7 +517,7 @@ describe("photos", function () {
expect(await dbPhoto.origFileExists()).to.be.equal(false);
const showResp = await request(callback)
await request(callback)
.get(`/photos/showByID/${photo.id}`)
.set({
Authorization: `Bearer ${seed.user1.toJWT()}`,
@@ -536,17 +536,17 @@ describe("photos", function () {
hash: dogHash,
size: dogSize,
format: dogFormat,
} as IPhotosNewPostBody)
} as TPhotosNewPostBody)
.expect(200);
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);
const dbPhoto = await Photo.findOneOrFail({
id: photo.id,
user: seed.user1.id as any,
user: { id: seed.user1.id },
});
expect(dbPhoto.hash).to.be.equal(dogHash);
expect(await dbPhoto.origFileExists()).to.be.equal(false);
@@ -574,17 +574,17 @@ describe("photos", function () {
hash: dogHash,
size: dogSize,
format: dogFormat,
} as IPhotosNewPostBody)
} as TPhotosNewPostBody)
.expect(200);
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);
const dbPhoto = await Photo.findOneOrFail({
id: photo.id,
user: seed.user1.id as any,
user: { id: seed.user1.id },
});
expect(dbPhoto.hash).to.be.equal(dogHash);
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 () {
const response = await request(callback)
await request(callback)
.post("/photos/new")
.set({
Authorization: `Bearer ${seed.user1.toJWT()}`,
@@ -619,7 +619,7 @@ describe("photos", function () {
hash: "../test",
size: "33333",
format: dogFormat,
} as IPhotosNewPostBody)
} as TPhotosNewPostBody)
.expect(400);
});
@@ -636,13 +636,13 @@ describe("photos", function () {
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");
const dbPhoto = await Photo.findOne({
id: seed.dogPhoto.id,
user: seed.user1.id as any,
user: {id:seed.user1.id} ,
});
expect(dbPhoto.name).to.be.equal("Test1");
@@ -663,7 +663,7 @@ describe("photos", function () {
expect(response.body.error).to.be.false;
const photos = response.body.data as IPhotoReqJSON[];
const photos = response.body.data as TPhotoReqJSON[];
const userPhotos = [
await seed.dogPhoto.toReqJSON(),
await seed.catPhoto.toReqJSON(),
@@ -686,7 +686,7 @@ describe("photos", function () {
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();
@@ -705,7 +705,7 @@ describe("photos", function () {
})
.send({
photos: [await seed.dogPhoto.toReqJSON()],
} as IPhotosDeleteBody)
} as TPhotosDeleteBody)
.expect(200);
expect(response.body.error).to.be.false;
@@ -737,7 +737,7 @@ describe("photos", function () {
await seed.dogPhoto.toReqJSON(),
await seed.catPhoto.toReqJSON(),
],
} as IPhotosDeleteBody)
} as TPhotosDeleteBody)
.expect(200);
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 { getConnection } from "typeorm";
import { app } from "~app";
import { IUserAuthJSON, User } from "~entity/User";
import { User } from "~entity/User";
import {
IUserEditBody,
IUserEditRespBody,
IUserGetRespBody,
IUserLoginBody,
IUserLoginRespBody,
IUserSignupBody,
IUserSignupRespBody,
} from "~routes/users";
TUserEditBody,
TUserEditRespBody,
TUserGetRespBody,
TUserLoginBody,
TUserLoginRespBody,
TUserSignupBody,
TUserSignupRespBody,
} from "~shared/types";
import { allowSignups, ISeed, seedDB } from "./util";
@@ -43,13 +43,13 @@ describe("users", function () {
.expect("Content-Type", /json/)
.expect(200);
const body = response.body as IUserGetRespBody;
const body = response.body as TUserGetRespBody;
if (body.error !== false) {
assert(false);
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: _, ...user } = body.data;
expect(user).to.deep.equal(seed.user1.toJSON());
@@ -59,17 +59,17 @@ describe("users", function () {
const response = await request(callback)
.post("/users/login")
.set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1" } as IUserLoginBody)
.send({ username: "User1", password: "User1" } as TUserLoginBody)
.expect("Content-Type", /json/)
.expect(200);
const body = response.body as IUserLoginRespBody;
const body = response.body as TUserLoginRespBody;
if (body.error !== false) {
assert(false);
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: _, ...user } = response.body.data;
expect(user).to.deep.equal(seed.user1.toJSON());
});
@@ -78,12 +78,12 @@ describe("users", function () {
const response = await request(callback)
.post("/users/login")
.set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "asdf" } as IUserLoginBody)
.send({ username: "User1", password: "asdf" } as TUserLoginBody)
.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.data).to.be.false;
expect(body.data).to.be.null;
});
it("should signup user", async function () {
@@ -96,17 +96,17 @@ describe("users", function () {
username: "NUser1",
password: "NUser1",
email: "nuser1@users.com",
} as IUserSignupBody)
} as TUserSignupBody)
.expect("Content-Type", /json/)
.expect(200);
const body = response.body as IUserSignupRespBody;
const body = response.body as TUserSignupRespBody;
if (body.error !== false) {
assert(false);
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: _, ...user } = body.data;
const newUser = await User.findOneOrFail({ username: "NUser1" });
expect(user).to.deep.equal(newUser.toJSON());
@@ -120,14 +120,14 @@ describe("users", function () {
username: "NUser1",
password: "NUser1",
email: "nuser1@users.com",
} as IUserSignupBody)
} as TUserSignupBody)
.expect("Content-Type", /json/)
.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.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 () {
@@ -140,17 +140,17 @@ describe("users", function () {
username: "NUser1",
password: "NUser1",
email: "nuser1@users.com",
} as IUserSignupBody)
} as TUserSignupBody)
.expect("Content-Type", /json/)
.expect(200);
const body = response.body as IUserSignupRespBody;
const body = response.body as TUserSignupRespBody;
if (body.error !== false) {
assert(false);
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: _, ...user } = body.data;
const newUser = await User.findOneOrFail({ username: "NUser1" });
expect(user).to.deep.equal(newUser.toJSON());
@@ -163,14 +163,14 @@ describe("users", function () {
username: "NUser2",
password: "NUser2",
email: "nuser2@users.com",
} as IUserSignupBody)
} as TUserSignupBody)
.expect("Content-Type", /json/)
.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.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 () {
@@ -184,17 +184,17 @@ describe("users", function () {
username: "NUser1",
password: "NUser1",
email: "nuser1@users.com",
} as IUserSignupBody)
} as TUserSignupBody)
.expect("Content-Type", /json/)
.expect(200);
const body = response.body as IUserSignupRespBody;
const body = response.body as TUserSignupRespBody;
if (body.error !== false) {
assert(false);
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: jwt1, ...user } = body.data;
const newUser = await User.findOneOrFail({ username: "NUser1" });
expect(user).to.deep.equal(newUser.toJSON());
@@ -207,17 +207,17 @@ describe("users", function () {
username: "NUser2",
password: "NUser2",
email: "nuser2@users.com",
} as IUserSignupBody)
} as TUserSignupBody)
.expect("Content-Type", /json/)
.expect(200);
const body2 = response2.body as IUserSignupRespBody;
const body2 = response2.body as TUserSignupRespBody;
if (body2.error !== false) {
assert(false);
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: jwt2, ...user2 } = body2.data;
const newUser2 = await User.findOneOrFail({ username: "NUser2" });
expect(user2).to.deep.equal(newUser2.toJSON());
@@ -234,13 +234,13 @@ describe("users", function () {
username: "User1",
password: "NUser1",
email: "user1@users.com",
} as IUserSignupBody)
} as TUserSignupBody)
.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.data).to.be.false;
expect(body.data).to.be.null;
});
it("should change user's password", async function () {
@@ -252,15 +252,14 @@ describe("users", function () {
})
.send({
password: "User1NewPass",
} as IUserEditBody)
} as TUserEditBody)
.expect("Content-Type", /json/)
.expect(200);
const body = response.body as IUserEditRespBody;
const body = response.body as TUserEditRespBody;
if (body.error !== false) {
assert(false);
return;
}
const loginResponse = await request(callback)
@@ -269,29 +268,29 @@ describe("users", function () {
.send({
username: "User1",
password: "User1NewPass",
} as IUserLoginBody)
} as TUserLoginBody)
.expect("Content-Type", /json/)
.expect(200);
const loginBody = loginResponse.body as IUserLoginRespBody;
const loginBody = loginResponse.body as TUserLoginRespBody;
if (loginBody.error !== false) {
assert(false);
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { jwt: _, ...user } = loginBody.data;
expect(user).to.deep.equal(seed.user1.toJSON());
const badLoginResponse = await request(callback)
.post("/users/login")
.set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1" } as IUserLoginBody)
.send({ username: "User1", password: "User1" } as TUserLoginBody)
.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.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 { getHash, getSize } from "~util";
import { Config, ConfigKey, setConfigValue } from "~entity/Config";
import { config } from "chai";
export const dogPath = "./src/tests/integration/photos/dog.jpg";
export const catPath = "./src/tests/integration/photos/cat.jpg";

View File

@@ -1,4 +1,3 @@
import deasync = require("deasync");
import { fromFile } from "hasha";
import * as ExifReader from "exifreader";
import * as sharp from "sharp";
@@ -71,9 +70,4 @@ export async function fileCheck(file: string) {
} catch (e) {
return false;
}
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const getHashSync: (file: string) => string = deasync(getHash);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const getSizeSync: (file: string) => string = deasync(getSize);
}

View File

@@ -24,8 +24,5 @@
"include": [
"./src/**/*.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"
services:
photosapp:
image: stepanusatiuk/photos:main
# image: stepanusatiuk/photos:main
build: ../
restart: always
ports:
- "8080:8080"
@@ -21,4 +22,3 @@ services:
- ./dbdata:/var/lib/mysql
env_file:
- db.env

View File

@@ -1,5 +1,3 @@
#!/bin/bash
#!/bin/sh
npm run typeorm -- migration:run
npm start
npm start

View File

@@ -14,7 +14,6 @@
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier/@typescript-eslint",
"plugin:import/errors",
"plugin:import/warnings",
"plugin:import/typescript",

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

14121
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,64 +3,70 @@
"scripts": {
"start": "parcel src/index.html",
"build": "parcel build src/index.html",
"test": "jest",
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx",
"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": {
"@blueprintjs/core": "^4.14.1",
"@parcel/config-default": "^2.8.2",
"@parcel/transformer-sass": "^2.8.2",
"@parcel/transformer-typescript-tsc": "^2.8.2",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"@parcel/config-default": "^2.9.3",
"@parcel/transformer-sass": "^2.9.3",
"@parcel/transformer-typescript-tsc": "^2.9.3",
"@reduxjs/toolkit": "^1.9.5",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0",
"class-validator": "^0.14.0",
"enzyme": "^3.11.0",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-typescript": "^3.5.3",
"eslint": "^8.46.0",
"eslint-config-prettier": "^8.9.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-import": "^2.27.4",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-jest": "^27.2.3",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.33.0",
"eslint-plugin-react-hooks": "^4.6.0",
"flush-promises": "^1.0.2",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"jest": "^29.6.2",
"jest-environment-jsdom": "^29.6.2",
"jest-junit": "^14.0.1",
"parcel": "^2.8.2",
"parcel": "^2.9.3",
"pluralize": "^8.0.0",
"prettier": "^2.8.2",
"prettier": "^3.0.0",
"prettier-eslint": "^15.0.1",
"process": "^0.11.10",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^8.0.5",
"react-redux": "^8.1.1",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-spring": "^8",
"redux": "^4.2.0",
"redux": "^4.2.1",
"redux-devtools-extension": "^2.13.9",
"redux-persist": "^6.0.0",
"redux-saga": "^1.2.2",
"redux-saga": "^1.2.3",
"spark-md5": "^3.0.2",
"ts-jest": "^29.0.4",
"typescript": "^4.9.4"
"ts-jest": "^29.1.1",
"typescript": "^5.1.6"
},
"devDependencies": {
"@types/enzyme": "^3.10.12",
"@types/eslint": "^8.4.10",
"@types/enzyme": "^3.10.13",
"@types/eslint": "^8.44.1",
"@types/eslint-config-prettier": "^6.11.0",
"@types/eslint-plugin-prettier": "^3.1.0",
"@types/jest": "^29.2.5",
"@types/pluralize": "^0.0.29",
"@types/prettier": "^2.7.2",
"@types/jest": "^29.5.3",
"@types/pluralize": "^0.0.30",
"@types/prettier": "^2.7.3",
"@types/react": "^17.0.39",
"@types/react-dom": "^17.0.11",
"@types/react-redux": "^7.1.25",
"@types/react-router": "^5.1.20",
"@types/react-router-dom": "^5.3.3",
"@types/redux-devtools-extension": "^2.13.2",
"@types/spark-md5": "^3.0.2"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,9 +56,6 @@
object-fit: contain;
max-height: 100%;
max-width: 100%;
min-height: 100%;
width: auto;
height: auto;
box-shadow: $pt-elevation-shadow-4;
transition: all 0.3s;
@@ -218,7 +215,7 @@
max-height: 100%;
width: auto;
height: auto;
object-fit: scale-down;
object-fit: contain;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,19 @@
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> {
return (fetchJSONAuth("/users/user", "GET") as unknown) as Promise<
IUserGetRespBody
>;
export async function fetchUser(): Promise<TUserGetRespBody> {
return fetchJSONAuth("/users/user", "GET", UserGetRespBody);
}
export async function changeUserPassword(
newPassword: string,
): Promise<IUserEditRespBody> {
return fetchJSONAuth("/users/edit", "POST", {
): Promise<TUserEditRespBody> {
return fetchJSONAuth("/users/edit", "POST", UserEditRespBody, {
password: newPassword,
});
}

View File

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

View File

@@ -1,5 +1,5 @@
import { Action } from "redux";
import { IUserAuthJSON } from "../../../../src/entity/User";
import { TUserAuthJSON } from "~/src/shared/types";
export enum AuthTypes {
AUTH_START = "AUTH_START",
@@ -28,7 +28,7 @@ export interface ISignupStartAction extends Action {
export interface IAuthSuccessAction extends Action {
type: AuthTypes.AUTH_SUCCESS;
payload: IUserAuthJSON;
payload: TUserAuthJSON;
}
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 };
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
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 {
getUserFail,
getUserSuccess,
IUserPassChangeAction,
TUserPassChangeAction,
userPassChangeFail,
userPassChangeSuccess,
UserTypes,
@@ -32,7 +32,7 @@ function* getUser() {
}
}
function* userPassChange(action: IUserPassChangeAction) {
function* userPassChange(action: TUserPassChangeAction) {
try {
const { response, timeout } = yield race({
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,
"skipLibCheck": true,
"isolatedModules": true,
"downlevelIteration": true
"downlevelIteration": true,
"baseUrl": "./",
"paths": {
"~*": [
"./*"
]
}
},
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./jest.config.js"]
}

10963
package-lock.json generated Executable file → Normal file

File diff suppressed because it is too large Load Diff

105
package.json Executable file → Normal file
View File

@@ -1,94 +1,27 @@
{
"name": "photos",
"version": "0.0.1",
"name": "photos-root",
"scripts": {
"start-frontend": "cd frontend && npm start",
"start": "ts-node -T -r tsconfig-paths/register src/server.ts",
"ts-node-dev": "ts-node-dev -r tsconfig-paths/register ./src/server.ts",
"dev": "cross-env NODE_ENV=development concurrently npm:ts-node-dev npm:start-frontend -c 'blue,green'",
"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 test",
"test-all": "npm test && npm run test-frontend",
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx && tsc --noEmit",
"lint-fix": "eslint ./src/** --ext .js,.jsx,.ts,.tsx --fix",
"dev-backend": "cd backend && npm run ts-node-dev",
"dev-frontend": "cd frontend && npm run start",
"dev-all": "cross-env NODE_ENV=development concurrently npm:dev-backend npm:dev-frontend -c 'blue,green'",
"test-backend": "cd backend && npm run test",
"test-frontend": "cd frontend && npm run test",
"test-all": "npm run test-backend && npm run test-frontend",
"lint-backend": "cd backend && npm run lint",
"lint-backend-fix": "cd backend && npm run lint-fix",
"lint-frontend": "cd frontend && npm run lint",
"lint-frontend-fix": "cd frontend && npm run lint-fix",
"lint-all": "npm run lint && npm run lint-frontend",
"lint-all-fix": "npm run lint-fix && npm run lint-frontend-fix",
"prettier-check": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --check",
"prettify": "prettier src/**/*.ts frontend/src/**/*.ts frontend/src/**/*.tsx --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": "^3.4.1",
"@koa/router": "^12.0.0",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"bcrypt": "^5.1.0",
"chai": "^4.3.7",
"class-validator": "^0.14.0",
"concurrently": "^7.6.0",
"cross-env": "^7.0.3",
"deasync": "^0.1.28",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-typescript": "^3.5.3",
"eslint-plugin-import": "^2.27.4",
"eslint-plugin-mocha": "^10.1.0",
"eslint-plugin-prettier": "^4.2.1",
"exifreader": "^4.9.1",
"hasha": "^5.2.2",
"husky": "^8.0.3",
"jsonwebtoken": "^9.0.0",
"koa": "^2.14.1",
"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",
"mocha": "^10.2.0",
"mysql": "^2.18.1",
"prettier": "^2.8.2",
"prettier-eslint": "^15.0.1",
"sharp": "^0.31.3",
"supertest": "^6.3.3",
"ts-node": "^10.9.1",
"ts-node-dev": "^2",
"tsconfig-paths": "^4.1.2",
"typeorm": "^0.2.41",
"typescript": "^4.9.4"
"lint-all": "npm run lint-backend && npm run lint-frontend",
"lint-all-fix": "npm run lint-backend-fix && npm run lint-frontend-fix",
"prettier-check-backend": "cd backend && npm run prettier-check",
"prettify-backend": "cd backend && npm run prettify",
"prettier-check-frontend": "cd backend && npm run prettier-check",
"prettify-frontend": "cd backend && npm run prettify",
"prettier-check-all": "npm run prettier-check-backend && npm run prettier-check-frontend",
"prettify-all": "npm run prettify-backend && npm run prettify-frontend"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/chai": "^4.3.4",
"@types/concurrently": "^6.4.0",
"@types/deasync": "^0.1.2",
"@types/eslint": "^8.4.10",
"@types/eslint-plugin-prettier": "^3.1.0",
"@types/jsonwebtoken": "^8.5.9",
"@types/koa": "^2.13.5",
"@types/koa__cors": "^3.3.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.2",
"@types/sharp": "^0.31.1",
"@types/supertest": "^2.0.12",
"mocha-junit-reporter": "^2.2.0",
"mocha-multi-reporters": "^1.5.1"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint-all && npm run prettier-check"
}
"concurrently": "^8.2.0",
"cross-env": "^7.0.3"
}
}

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],
};

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