diff --git a/.github/linters/.eslintrc.json b/.github/linters/.eslintrc.json new file mode 100644 index 0000000..7a098d1 --- /dev/null +++ b/.github/linters/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/no-empty-interface": 0 + } +} \ No newline at end of file diff --git a/.github/linters/.htmlhintrc b/.github/linters/.htmlhintrc index 4ddbd55..98daa2a 100644 --- a/.github/linters/.htmlhintrc +++ b/.github/linters/.htmlhintrc @@ -10,7 +10,7 @@ "spec-char-escape": true, "id-unique": false, "src-not-empty": true, - "title-require": false, + "title-require": true, "alt-require": true, "doctype-html5": true, "id-class-value": false, @@ -21,5 +21,5 @@ "id-class-ad-disabled": false, "href-abs-or-rel": false, "attr-unsafe-chars": true, - "head-script-disabled": false + "head-script-disabled": true } diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 482b880..2ad4277 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -53,3 +53,8 @@ jobs: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FILTER_REGEX_EXCLUDE: .*vscode/.*.json + VALIDATE_JAVASCRIPT_STANDARD: false + TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.json + VALIDATE_TYPESCRIPT_STANDARD: false + VALIDATE_MARKDOWN: false diff --git a/client/src/app.html b/client/src/app.html index 756626f..96a01b4 100644 --- a/client/src/app.html +++ b/client/src/app.html @@ -1,6 +1,7 @@ + Blobfishes app diff --git a/client/src/lib/api.author.js b/client/src/lib/api.author.js new file mode 100644 index 0000000..460f2c3 --- /dev/null +++ b/client/src/lib/api.author.js @@ -0,0 +1,31 @@ +import axios from 'axios' +import { + API_PATH +} from './api'; + +const apiAuthorPath = `${API_PATH}/author`; + +function findAuthors() { + return axios.get(apiAuthorPath); +} + +function insertAuthor(payload) { + return axios.post(apiAuthorPath, payload); +} + +function updateAuthor(id, payload) { + const path = `${apiAuthorPath}/${id}`; + return axios.put(path, payload); +} + +function removeAuthor(id) { + const path = `${apiAuthorPath}/${id}`; + return axios.delete(path); +} + +export const apiAuthor = { + findAuthors, + insertAuthor, + updateAuthor, + removeAuthor +} \ No newline at end of file diff --git a/client/src/lib/api.book.js b/client/src/lib/api.book.js new file mode 100644 index 0000000..e1b7aff --- /dev/null +++ b/client/src/lib/api.book.js @@ -0,0 +1,31 @@ +import axios from 'axios' +import { + API_PATH +} from './api'; + +const apiBookPath = `${API_PATH}/book`; + +function findBooks() { + return axios.get(apiBookPath); +} + +function insertBook(payload) { + return axios.post(apiBookPath, payload); +} + +function updateBook(id, payload) { + const path = `${apiBookPath}/${id}`; + return axios.put(path, payload); +} + +function removeBook(id) { + const path = `${apiBookPath}/${id}`; + return axios.delete(path); +} + +export const apiBook = { + findBooks, + insertBook, + updateBook, + removeBook +} \ No newline at end of file diff --git a/client/src/lib/api.js b/client/src/lib/api.js new file mode 100644 index 0000000..9a415f5 --- /dev/null +++ b/client/src/lib/api.js @@ -0,0 +1 @@ +export const API_PATH = '/api'; \ No newline at end of file diff --git a/client/src/lib/requests.js b/client/src/lib/requests.js deleted file mode 100644 index 6390828..0000000 --- a/client/src/lib/requests.js +++ /dev/null @@ -1,14 +0,0 @@ -import axios from 'axios' - -const apiPath = '/api/author' - -function getAuthors () { - axios.get(`${apiPath}`) - .then((res) => { - return (res.data) - }) -} - -export default { - getAuthors -} diff --git a/client/src/routes/authors.svelte b/client/src/routes/authors.svelte index 977db6c..e7ec02c 100644 --- a/client/src/routes/authors.svelte +++ b/client/src/routes/authors.svelte @@ -1,5 +1,4 @@ @@ -131,52 +153,78 @@
-
+
- - - - - + {#each books as book} - - - - - - - + + + + + + + {/each}
+ Name + Author + Owner + Genres -
- {book.title} - - {#each book.authors as author} -

{author.name}

- {/each} -
- {book.owner} - - {#each book.genres as genre} -
- {genre} -
- {/each} -
- - -
+ {book.title} + + {#each book.authors as author} +

{author.name}

+ {/each} +
+ {book.owner} + + {#each book.genres as genre} +
+ {genre} +
+ {/each} +
+ + +
@@ -197,23 +245,34 @@ />

- + > + {#each authors as value} + + {/each} +

- - - Owner: + + + {#if updateopen} diff --git a/package.json b/package.json index 453207d..fce0f67 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,8 @@ "@types/migrate-mongo": "^8.1.1", "@types/mongodb": "^3.6.20", "@types/node": "^14.17.5", + "@typescript-eslint/eslint-plugin": "^5.2.0", + "@typescript-eslint/parser": "^5.2.0", "color-string": ">=1.5.5", "concurrently": "^6.2.0", "glob-parent": ">=5.1.2", diff --git a/server/interfaces/models/IBook.ts b/server/interfaces/models/IBook.ts index 3f61cea..76e858c 100644 --- a/server/interfaces/models/IBook.ts +++ b/server/interfaces/models/IBook.ts @@ -1,9 +1,10 @@ +import { ObjectID } from "bson"; import { IModel } from "./IModel"; export interface IBook { _id: string; title: string; - authors: { id: number, name: string }[]; + authors: string[] | ObjectID[]; genres: string[]; location: string; owner: string; diff --git a/server/loaders/koa.ts b/server/loaders/koa.ts index a910cb2..a16ec98 100644 --- a/server/loaders/koa.ts +++ b/server/loaders/koa.ts @@ -1,6 +1,5 @@ import Koa from "koa"; import koaBody from "koa-body"; -import jwt from "koa-jwt"; import cors from "@koa/cors"; import serve from "koa-static"; import path from "path"; @@ -26,8 +25,8 @@ export default ({ app }: { app: Koa }) => { app.use(swaggerRoute.routes()).use(swaggerRoute.allowedMethods()); } - const unlessPaths = [/^\/api\/auth/, "/", '/api/status']; - /* app.use(jwt({ + /* const unlessPaths = [/^\/api\/auth/, "/", '/api/status']; + app.use(jwt({ secret: config.jwtSecret, algorithms: [config.jwtAlgorithm] }).unless({ @@ -40,7 +39,7 @@ export default ({ app }: { app: Koa }) => { function isRouteValid(url: string) { if (!validRoutes || !url) return false - const urlStart = url.split('/').filter(item => item)[0] || '';; + const urlStart = url.split('/').filter(item => item)[0] || ''; return validRoutes.find(route => urlStart === route) } app.use(async (ctx, next) => { diff --git a/server/loaders/mongo-client.ts b/server/loaders/mongo-client.ts index 69559df..422df1a 100644 --- a/server/loaders/mongo-client.ts +++ b/server/loaders/mongo-client.ts @@ -1,13 +1,9 @@ -import { Db, ObjectID } from 'mongodb'; import { injectable } from 'inversify'; import { MongoDBConnection } from './mongo-connection'; @injectable() export class MongoDBClient { - constructor() { - } - public async getDb() { return MongoDBConnection.getConnection(); } diff --git a/server/loaders/mongo-connection.ts b/server/loaders/mongo-connection.ts index f18e2a8..4c81996 100644 --- a/server/loaders/mongo-connection.ts +++ b/server/loaders/mongo-connection.ts @@ -3,7 +3,7 @@ import { Db, MongoClient } from 'mongodb'; import config from '../config'; export class MongoDBConnection { - private static isConnected: boolean = false; + private static isConnected = false; private static db: Db; public static async getConnection(): Promise { diff --git a/server/models/author.ts b/server/models/author.ts index 4def18e..ee3ffcb 100644 --- a/server/models/author.ts +++ b/server/models/author.ts @@ -16,11 +16,12 @@ export const AuthorSchema: Schema = { required: ["name"], additionalProperties: false, } +const validatorSchema = addFormats(new Ajv().addKeyword('example')).compile(AuthorSchema); @injectable() export class AuthorModel extends Model implements Author { - protected validator = addFormats(new Ajv().addKeyword('example')).compile(AuthorSchema); - protected collectionName: string = 'authors'; + protected validator = validatorSchema; + protected collectionName = 'authors'; } diff --git a/server/models/book.ts b/server/models/book.ts index 0236cf7..8a4587f 100644 --- a/server/models/book.ts +++ b/server/models/book.ts @@ -3,6 +3,7 @@ import Ajv, { Schema } from "ajv" import { IBook, Book } from '../interfaces/models/IBook'; import { Model } from './model'; +import { AggregationCursor, Collection } from 'mongodb'; export const BookSchema: Schema = { type: "object", @@ -10,10 +11,7 @@ export const BookSchema: Schema = { _id: { type: "string" }, title: { type: "string", minLength: 3, example: "The Hitchhiker's Guide to the Galaxy" }, authors: { - type: "array", items: { type: "object" }, example: [{ - "id": 1, - "name": "Douglas Adams" - }] + type: "array", items: { type: "string" } }, genres: { type: "array", items: { type: "string" }, example: [ @@ -28,11 +26,28 @@ export const BookSchema: Schema = { required: ["title", "authors", "genres"], additionalProperties: false, } +const validatorSchema = new Ajv().addKeyword('example').compile(BookSchema); @injectable() export class BookModel extends Model implements Book { - protected validator = new Ajv().addKeyword('example').compile(BookSchema); - protected collectionName: string = 'books'; + protected validator = validatorSchema; + protected collectionName = 'books'; + + public async findAll(): Promise { + return this.aggregate((await this.collection)).toArray(); + } + + private aggregate(collection: Collection): AggregationCursor { + return collection.aggregate([{ + $lookup: + { + from: 'authors', + localField: 'authors', + foreignField: '_id', + as: 'authors' + } + }]); + } } diff --git a/server/models/reader.ts b/server/models/reader.ts index 8e9fa05..4256415 100644 --- a/server/models/reader.ts +++ b/server/models/reader.ts @@ -28,6 +28,6 @@ export const ReaderSchema: Schema = { export class ReaderModel extends Model implements Reader { protected validator = addFormats(new Ajv().addKeyword('example')).compile(ReaderSchema); - protected collectionName: string = 'readers'; + protected collectionName = 'readers'; } diff --git a/server/models/user.ts b/server/models/user.ts index cce4dff..88659b7 100644 --- a/server/models/user.ts +++ b/server/models/user.ts @@ -22,7 +22,7 @@ export class UserModel extends Model implements User { protected validator = new Ajv().addKeyword('example').compile(Object.assign({ required: ["password"] }, UserSchema)); protected validatorUpdate = new Ajv().addKeyword('example').compile(UserSchema); - protected collectionName: string = 'users'; + protected collectionName = 'users'; public async findByEmail(email: string): Promise { return (await this.collection).findOne({ email }); diff --git a/server/services/book.ts b/server/services/book.ts index be44ea4..94cc4b0 100644 --- a/server/services/book.ts +++ b/server/services/book.ts @@ -1,3 +1,4 @@ +import { ObjectID } from "bson"; import createHttpError from "http-errors"; import { inject, injectable } from "inversify"; @@ -18,6 +19,9 @@ export class BookServiceImpl implements BookService { throw createHttpError(400, 'Not valid data', { details: this.bookModel.validatorErrors }); } delete bookData._id; + if (bookData?.authors.length) { + bookData.authors = bookData.authors.map(id => new ObjectID(id)); + } return this.bookModel.insert(bookData); } @@ -29,6 +33,9 @@ export class BookServiceImpl implements BookService { if (!this.bookModel.validate(bookData)) { throw createHttpError(400, 'Not valid data', { details: this.bookModel.validatorErrors }); } + if (bookData?.authors.length) { + bookData.authors = bookData.authors.map(id => new ObjectID(id)); + } delete bookData._id; return this.bookModel.update(id, bookData).then(res => bookData); }