update: upgrade eslint to major version 7 and rework the application to fit new rules

This commit is contained in:
Xymorot 2020-07-25 02:02:37 +02:00
parent 50fbdeb96c
commit 115782061d
68 changed files with 1301 additions and 1128 deletions

View File

@ -24,6 +24,7 @@
"max-classes-per-file": "error",
"object-shorthand": ["error", "methods"],
"no-useless-rename": "error",
"prefer-promise-reject-errors": "error",
"import/no-extraneous-dependencies": [
"error",
@ -65,22 +66,20 @@
"settings": {
"import/extensions": [".ts", "d.ts", ".js", ".json"],
"import/parsers": {
"@typescript-eslint/parser": [".ts", "d.ts"]
"@typescript-eslint/parser": [".ts", ".d.ts"]
}
},
"rules": {
"no-console": "error",
"no-magic-numbers": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/typedef": [
"@typescript-eslint/ban-types": [
"error",
{
"arrowParameter": true,
"memberVariableDeclaration": true,
"parameter": true,
"propertyDeclaration": true
"extendDefaults": true,
"types": {
"object": false
}
}
],
"@typescript-eslint/explicit-function-return-type": "error",
@ -103,14 +102,30 @@
],
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/interface-name-prefix": ["error", { "prefixWithI": "always" }],
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "interface",
"format": ["PascalCase"],
"prefix": ["I"]
}
],
"@typescript-eslint/explicit-member-accessibility": "error",
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }],
"@typescript-eslint/member-ordering": "error",
"@typescript-eslint/array-type": ["error", { "default": "array-simple" }]
"@typescript-eslint/array-type": ["error", { "default": "array-simple" }],
"@typescript-eslint/restrict-template-expressions": [
"error",
{
"allowNumber": true,
"allowBoolean": false,
"allowAny": false,
"allowNullish": false
}
]
}
},
{
@ -123,7 +138,8 @@
"@typescript-eslint/no-magic-numbers": "off",
"@typescript-eslint/typedef": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/explicit-function-return-type": "off"
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/ban-ts-comment": "off"
}
},
{

View File

@ -1,4 +0,0 @@
declare module '*.json' {
const value: any;
export default value;
}

1514
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -52,50 +52,50 @@
"minimist": "^1.2.5",
"node-fetch": "^2.6.0",
"reflect-metadata": "^0.1.13",
"sqlite3": "^4.2.0",
"sqlite3": "^5.0.0",
"typeorm": "^0.2.25",
"uuid": "^7.0.3"
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.51",
"@electron-forge/maker-squirrel": "^6.0.0-beta.51",
"@electron-forge/cli": "^6.0.0-beta.52",
"@electron-forge/maker-squirrel": "^6.0.0-beta.52",
"@types/chai": "^4.2.11",
"@types/fs-extra": "^9.0.1",
"@types/jsdom": "^16.2.3",
"@types/minimist": "^1.2.0",
"@types/mocha": "^7.0.2",
"@types/node": "^12.12.44",
"@types/node": "^12.12.51",
"@types/node-fetch": "^2.5.7",
"@types/sinon": "^9.0.4",
"@types/uuid": "^7.0.4",
"@types/webpack": "^4.41.17",
"@typescript-eslint/eslint-plugin": "^2.34.0",
"@typescript-eslint/parser": "^2.34.0",
"@types/webpack": "^4.41.21",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"chai": "^4.2.0",
"chokidar": "^3.4.0",
"chokidar": "^3.4.1",
"concurrently": "^5.2.0",
"electron": "^8.3.1",
"electron": "^8.4.0",
"electron-rebuild": "^1.11.0",
"eslint": "^6.8.0",
"eslint": "^7.5.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.20.2",
"fast-check": "^1.24.2",
"eslint-plugin-import": "^2.22.0",
"fast-check": "^1.26.0",
"handlebars": "^4.7.6",
"husky": "^4.2.5",
"lodash": "^4.17.15",
"lodash": "^4.17.19",
"mocha": "^7.2.0",
"nock": "^12.0.3",
"nyc": "^15.1.0",
"prettier": "^2.0.5",
"rewiremock": "^3.14.2",
"rewiremock": "^3.14.3",
"sinon": "^9.0.2",
"spectron": "^10.0.1",
"svelte": "^3.23.0",
"svelte": "^3.24.0",
"svelte-loader": "^2.13.6",
"ts-loader": "^7.0.5",
"typescript": "^3.9.5",
"typescript": "^3.9.7",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
"webpack-cli": "^3.3.12"
},
"repository": "https://git.fuwafuwa.moe/Xymorot/RenaiApp",
"bugs": "https://git.fuwafuwa.moe/Xymorot/RenaiApp/issues",

View File

@ -1,5 +1,6 @@
import * as electron from 'electron';
import { expect } from 'chai';
import { Context } from 'mocha';
import rewiremock from 'rewiremock';
import 'mocha';
@ -11,23 +12,33 @@ rewiremock.disable();
describe('Application @slow', function () {
this.timeout(20000);
before(function () {
this.app = new Application({
// @ts-ignore this does give the path to electron executable (hopefully platform agnostic)
path: electron.default,
interface IApplicationContext extends Context {
app: Application;
}
before(function (this, done): void {
const context = this as IApplicationContext;
context.app = new Application({
// @ts-ignore this does give the path to electron executable when this script is running outside of electron (which it does in the test files)
path: ((electron as unknown) as { default: string }).default,
args: [packageJson.main],
});
return this.app.start();
context.app
.start()
.then(() => done())
.catch((reason) => done(reason));
});
after(function () {
if (this.app && this.app.isRunning()) {
return this.app.stop();
after(function (this) {
const context = this as IApplicationContext;
if (context.app && context.app.isRunning()) {
return context.app.stop();
}
});
it('shows an initial window', function () {
return this.app.client.getWindowCount().then((count: number) => {
it('shows an initial window', function (this: Context) {
const context = this as IApplicationContext;
return context.app.client.getWindowCount().then((count: number) => {
expect(count).to.be.gte(1);
});
});

View File

@ -1,9 +1,4 @@
/* eslint-disable import/order */
/**
* Disable Reasons
*
* import/order: this is the entry point for the application and some things have to happen before others
*/
/* eslint-disable import/order -- this is the entry point for the application and some things have to happen before others */
import { container } from './main/core/container';
import './main/core/install';
@ -23,7 +18,8 @@ async function createWindow(): Promise<void> {
// Open the DevTools.
if (isDev()) {
appWindowMain.window.webContents.openDevTools();
// eslint-disable-next-line no-unused-expressions -- eslint can't handle optional chaining, yet
appWindowMain.window?.webContents.openDevTools();
}
}

View File

@ -2,17 +2,28 @@ import 'reflect-metadata';
import { Container } from 'inversify';
import { MainAppWindow } from '../modules/app-window/main-app-window';
import { NhentaiApi } from '../modules/nhentai/nhentai-api';
import { NhentaiIpcServer } from '../modules/nhentai/nhentai-ipc-server';
import '../modules/nhentai/nhentai-ipc-controller';
import { Session } from '../modules/session/session';
import { Store } from '../modules/store/store';
import { StoreMock } from '../modules/store/store.mock';
import { WebCrawler } from '../modules/web-crawler/web-crawler';
export const container = new Container({ defaultScope: 'Singleton' });
container.bind(Symbol.for('store')).to(Store);
export function mockStore(unMock = false): void {
if (unMock) {
container.unbind(Symbol.for('store'));
container.bind(Symbol.for('store')).to(Store);
} else {
container.unbind(Symbol.for('store'));
container.bind(Symbol.for('store')).to(StoreMock);
}
}
container.bind(Symbol.for('web-crawler')).to(WebCrawler);
container.bind(Symbol.for('nhentai-api')).to(NhentaiApi);
container.bind(Symbol.for('nhentai-ipc-server')).to(NhentaiIpcServer);
container.get(Symbol.for('nhentai-ipc-server'));
container.bind(Symbol.for('app-window-main')).to(MainAppWindow);

View File

@ -3,7 +3,7 @@ import '../../../mocks/electron';
import { expect } from 'chai';
import 'mocha';
import { Databases, getConnection } from './database';
import { Database, getConnection } from './database';
describe('Database Service', () => {
before(() => {
@ -15,7 +15,7 @@ describe('Database Service', () => {
});
it('returns a connection', async () => {
const libraryConnection = await getConnection(Databases.LIBRARY);
const libraryConnection = await getConnection(Database.LIBRARY);
expect(libraryConnection).to.not.equal(undefined);
});
});

View File

@ -3,48 +3,49 @@ import { Connection, createConnection as ormCreateConnection } from 'typeorm';
import { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions';
import { appPath } from './app-path';
export enum Databases {
export enum Database {
LIBRARY = 'library',
STORE = 'store',
}
type MyConnectionOptions = { [key in Databases]?: SqliteConnectionOptions };
type MyConnectionOptions = { [key in Database]: SqliteConnectionOptions };
const databaseDir = path.resolve(appPath, 'database');
const connectionOptions: MyConnectionOptions = Object.values(Databases).reduce(
(prev: MyConnectionOptions, database: Databases) => {
prev[database] = {
name: database,
type: 'sqlite',
database: path.resolve(databaseDir, `${database}.db`),
cache: true,
entities: [`./src/main/entities/${database}/*.js`],
migrations: [`./src/main/migrations/${database}/*.js`],
cli: {
migrationsDir: `./src/main/migrations/${database}`,
},
};
return prev;
},
{}
);
const connections: {
[key in Databases]?: Connection;
} = {};
function createConnection(database: Databases): Promise<Connection> {
return ormCreateConnection(connectionOptions[database])
.then((connection: Connection) => {
connections[database] = connection;
return connection.runMigrations();
})
.then(() => connections[database]);
function getConnectionOptionsFor(database: Database): SqliteConnectionOptions {
return {
name: database,
type: 'sqlite',
database: path.resolve(databaseDir, `${database}.db`),
cache: true,
entities: [`./src/main/entities/${database}/*.js`],
migrations: [`./src/main/migrations/${database}/*.js`],
cli: {
migrationsDir: `./src/main/migrations/${database}`,
},
};
}
export function getConnection(database: Databases): Promise<Connection> {
if (connections[database] === undefined) {
return createConnection(database);
}
const connectionOptions: MyConnectionOptions = {
[Database.LIBRARY]: getConnectionOptionsFor(Database.LIBRARY),
[Database.STORE]: getConnectionOptionsFor(Database.STORE),
};
function createConnection(database: Database): Promise<Connection> {
let connection: Connection;
return ormCreateConnection(connectionOptions[database])
.then((c) => {
connection = c;
return c.runMigrations();
})
.then(() => connection);
}
const connections = {
[Database.LIBRARY]: createConnection(Database.LIBRARY),
[Database.STORE]: createConnection(Database.STORE),
};
export function getConnection(database: Database): Promise<Connection> {
return Promise.resolve(connections[database]);
}

View File

@ -1,19 +0,0 @@
export const enum Errors {
ENOLOGIN = 'ENOLOGIN',
ELOGINFAIL = 'ELOGINFAIL',
EINITFAIL = 'EINITFAIL',
ECOOKIESAVEFAIL = 'ECOOKIESAVEFAIL',
}
const messages = {
[Errors.ENOLOGIN]: 'no login form found',
[Errors.ELOGINFAIL]: 'login failed',
[Errors.EINITFAIL]: 'initialization failed',
[Errors.ECOOKIESAVEFAIL]: 'failed to save cookies',
};
export class RenaiError extends Error {
public constructor(eno: Errors, msg: string = '') {
super(`${messages[eno]}.${msg ? ` ${msg}` : ''}`);
}
}

View File

@ -6,6 +6,7 @@ export const maxValue = Number.MAX_SAFE_INTEGER;
/**
* @param column the column which needs to be checked
*/
// eslint-disable-next-line @typescript-eslint/ban-types -- the type would be "(classOrObject: object, propertyName: string) => object" but typeorm does not provide it
export function PercentCheck(column: string): Function {
return Check(
`${column} needs to be between ${minValue} and ${maxValue}`,

View File

@ -4,17 +4,17 @@ import { Author } from './author';
@Entity()
export class AuthorName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@ManyToOne(() => Author, (author: Author) => author.names, {
nullable: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public entity: Promise<Author>;
public entity!: Promise<Author>;
@Column({
nullable: false,
})
public name: string;
public name!: string;
}

View File

@ -4,17 +4,17 @@ import { AuthorRole } from './author-role';
@Entity()
export class AuthorRoleName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@ManyToOne(() => AuthorRole, (authorRole: AuthorRole) => authorRole.names, {
nullable: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public entity: Promise<AuthorRole>;
public entity!: Promise<AuthorRole>;
@Column({
nullable: false,
})
public name: string;
public name!: string;
}

View File

@ -9,22 +9,22 @@ import { WorkAuthor } from './work-author';
@Entity()
export class AuthorRole implements IIdentifiableEntity, IMultiNamedEntity, IDescribableEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@Column({
nullable: false,
})
public nameCanonical: string;
public nameCanonical!: string;
@OneToMany(() => AuthorRoleName, (authorRoleName: AuthorRoleName) => authorRoleName.entity)
public names: Promise<AuthorRoleName[]>;
public names!: Promise<AuthorRoleName[]>;
/**
* relation to the entity connecting with the author and work
*/
@ManyToMany(() => WorkAuthor, (workAuthor: WorkAuthor) => workAuthor.authorRoles)
public workAuthors: Promise<WorkAuthor[]>;
public workAuthors!: Promise<WorkAuthor[]>;
@Column()
public description: string;
public description!: string;
}

View File

@ -8,19 +8,19 @@ import { WorkAuthor } from './work-author';
@Entity()
export class Author implements IIdentifiableEntity, IMultiNamedEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@Column({
nullable: false,
})
public nameCanonical: string;
public nameCanonical!: string;
@OneToMany(() => AuthorName, (authorName: AuthorName) => authorName.entity)
public names: Promise<AuthorName[]>;
public names!: Promise<AuthorName[]>;
/**
* ultimately connects the author with a work and their role in that work
*/
@OneToMany(() => WorkAuthor, (workAuthor: WorkAuthor) => workAuthor.author)
public workAuthors: Promise<WorkAuthor[]>;
public workAuthors!: Promise<WorkAuthor[]>;
}

View File

@ -10,7 +10,7 @@ import { WorkCharacter } from './work-character';
@PercentCheck('weight')
export class CharacterTag implements IIdentifiableEntity, IWeightedEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
/**
* the character ina work this tag describes
@ -20,7 +20,7 @@ export class CharacterTag implements IIdentifiableEntity, IWeightedEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public workCharacter: Promise<WorkCharacter>;
public workCharacter!: Promise<WorkCharacter>;
/**
* the describing tag
@ -30,8 +30,8 @@ export class CharacterTag implements IIdentifiableEntity, IWeightedEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public tag: Promise<Tag>;
public tag!: Promise<Tag>;
@Column()
public weight: number;
public weight!: number;
}

View File

@ -4,17 +4,17 @@ import { Collection } from './collection';
@Entity()
export class CollectionName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@ManyToOne(() => Collection, (collection: Collection) => collection.names, {
nullable: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public entity: Promise<Collection>;
public entity!: Promise<Collection>;
@Column({
nullable: false,
})
public name: string;
public name!: string;
}

View File

@ -9,7 +9,7 @@ import { Work } from './work';
@Entity()
export class CollectionPart implements IIdentifiableEntity, IOrderableEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
/**
* the collection thw work is a part of
@ -19,7 +19,7 @@ export class CollectionPart implements IIdentifiableEntity, IOrderableEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public collection: Promise<Collection>;
public collection!: Promise<Collection>;
/**
* the work inside the collection
@ -29,11 +29,11 @@ export class CollectionPart implements IIdentifiableEntity, IOrderableEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public work: Promise<Work>;
public work!: Promise<Work>;
@Column({
nullable: false,
default: 0,
})
public order: number;
public order!: number;
}

View File

@ -13,17 +13,17 @@ import { CollectionPart } from './collection-part';
@Entity()
export class Collection implements IIdentifiableEntity, IMultiNamedEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@Column()
public nameCanonical: string;
public nameCanonical!: string;
@OneToMany(() => CollectionName, (collectionName: CollectionName) => collectionName.entity)
public names: Promise<CollectionName[]>;
public names!: Promise<CollectionName[]>;
/**
* the connecting entity between this collection and the work
*/
@OneToMany(() => CollectionPart, (collectionPart: CollectionPart) => collectionPart.collection)
public parts: Promise<CollectionPart[]>;
public parts!: Promise<CollectionPart[]>;
}

View File

@ -11,7 +11,7 @@ import { Work } from './work';
@Entity()
export class Copy implements IIdentifiableEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
/**
* the work this entity is a copy of
@ -21,14 +21,14 @@ export class Copy implements IIdentifiableEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public original: Promise<Work>;
public original!: Promise<Work>;
/**
* where to find this specific copy
*/
@ManyToMany(() => Source, (source: Source) => source.copies)
@JoinTable()
public sources: Promise<Source[]>;
public sources!: Promise<Source[]>;
/**
* identifying hash of the file contents
@ -36,7 +36,7 @@ export class Copy implements IIdentifiableEntity {
@Column({
nullable: false,
})
public hash: string;
public hash!: string;
/**
* device location of the copy
@ -44,7 +44,7 @@ export class Copy implements IIdentifiableEntity {
@Column({
nullable: true,
})
public location: string;
public location!: string;
/**
* the ordering of the copies belonging to the same work,
@ -54,5 +54,5 @@ export class Copy implements IIdentifiableEntity {
nullable: false,
default: 0,
})
public ranking: number;
public ranking!: number;
}

View File

@ -10,7 +10,7 @@ import { WorkCharacter } from './work-character';
@PercentCheck('weight')
export class InteractionTag implements IIdentifiableEntity, IWeightedEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
/**
* the describing tag
@ -20,20 +20,20 @@ export class InteractionTag implements IIdentifiableEntity, IWeightedEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public tag: Promise<Tag>;
public tag!: Promise<Tag>;
/**
* the actors of this interaction
*/
@ManyToMany(() => WorkCharacter, (workCharacter: WorkCharacter) => workCharacter.interactWith)
public subjectCharacters: Promise<WorkCharacter[]>;
public subjectCharacters!: Promise<WorkCharacter[]>;
/**
* the receivers of this interaction
*/
@ManyToMany(() => WorkCharacter, (workCharacter: WorkCharacter) => workCharacter.interactedBy)
public objectCharacters: Promise<WorkCharacter[]>;
public objectCharacters!: Promise<WorkCharacter[]>;
@Column()
public weight: number;
public weight!: number;
}

View File

@ -10,11 +10,11 @@ export class Language {
* ISO 639-1 two-letter language code
*/
@PrimaryColumn()
public code: string;
public code!: string;
/**
* the works using this language
*/
@ManyToMany(() => Work, (work: Work) => work.languages)
public works: Promise<Work[]>;
public works!: Promise<Work[]>;
}

View File

@ -4,17 +4,17 @@ import { Site } from './site';
@Entity()
export class SiteName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@ManyToOne(() => Site, (site: Site) => site.names, {
nullable: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public entity: Promise<Site>;
public entity!: Promise<Site>;
@Column({
nullable: false,
})
public name: string;
public name!: string;
}

View File

@ -8,19 +8,19 @@ import { Source } from './source';
@Entity()
export class Site implements IIdentifiableEntity, IMultiNamedEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@Column({
nullable: false,
})
public nameCanonical: string;
public nameCanonical!: string;
@OneToMany(() => SiteName, (siteName: SiteName) => siteName.entity)
public names: Promise<SiteName[]>;
public names!: Promise<SiteName[]>;
/**
* sources belonging to this site
*/
@OneToMany(() => Source, (source: Source) => source.site)
public sources: Promise<Source[]>;
public sources!: Promise<Source[]>;
}

View File

@ -8,7 +8,7 @@ import { Site } from './site';
@Entity()
export class Source implements IIdentifiableEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
/**
* the uri to the sauce
@ -16,7 +16,7 @@ export class Source implements IIdentifiableEntity {
@Column({
nullable: false,
})
public uri: string;
public uri!: string;
/**
* the site connected to the source
@ -26,11 +26,11 @@ export class Source implements IIdentifiableEntity {
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
})
public site: Promise<Site>;
public site!: Promise<Site>;
/**
* the copies which can be found here
*/
@ManyToMany(() => Copy, (copy: Copy) => copy.sources)
public copies: Promise<Copy[]>;
public copies!: Promise<Copy[]>;
}

View File

@ -4,17 +4,17 @@ import { Tag } from './tag';
@Entity()
export class TagName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@ManyToOne(() => Tag, (tag: Tag) => tag.names, {
nullable: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public entity: Promise<Tag>;
public entity!: Promise<Tag>;
@Column({
nullable: false,
})
public name: string;
public name!: string;
}

View File

@ -13,43 +13,43 @@ import { WorkTag } from './work-tag';
@Entity()
export class Tag implements IIdentifiableEntity, IMultiNamedEntity, IDescribableEntity, IHierachicalEntity<Tag> {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@Column({
nullable: false,
})
public nameCanonical: string;
public nameCanonical!: string;
@OneToMany(() => TagName, (tagName: TagName) => tagName.entity)
public names: Promise<TagName[]>;
public names!: Promise<TagName[]>;
/**
* this tag tagging a work
*/
@OneToMany(() => WorkTag, (workTag: WorkTag) => workTag.tag)
public workTags: Promise<WorkTag[]>;
public workTags!: Promise<WorkTag[]>;
/**
* this tag tagging characters
*/
@OneToMany(() => CharacterTag, (characterTag: CharacterTag) => characterTag.tag)
public characterTags: Promise<CharacterTag[]>;
public characterTags!: Promise<CharacterTag[]>;
/**
* this tag tagging a character interaction
*/
@OneToMany(() => InteractionTag, (interactionTag: InteractionTag) => interactionTag.tag)
public interactionTags: Promise<InteractionTag[]>;
public interactionTags!: Promise<InteractionTag[]>;
@ManyToMany(() => Tag, (tag: Tag) => tag.children)
@JoinTable()
public parents: Promise<Tag[]>;
public parents!: Promise<Tag[]>;
@ManyToMany(() => Tag, (tag: Tag) => tag.parents)
public children: Promise<Tag[]>;
public children!: Promise<Tag[]>;
@Column({
nullable: true,
})
public description: string;
public description!: string;
}

View File

@ -4,17 +4,17 @@ import { TransformationType } from './transformation-type';
@Entity()
export class TransformationTypeName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@ManyToOne(() => TransformationType, (transformationType: TransformationType) => transformationType.names, {
nullable: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public entity: Promise<TransformationType>;
public entity!: Promise<TransformationType>;
@Column({
nullable: false,
})
public name: string;
public name!: string;
}

View File

@ -9,29 +9,29 @@ import { TransformationTypeName } from './transformation-type-name';
@Entity()
export class TransformationType implements IIdentifiableEntity, IMultiNamedEntity, IDescribableEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@Column({
nullable: false,
})
public nameCanonical: string;
public nameCanonical!: string;
@OneToMany(
() => TransformationTypeName,
(transformationTypeName: TransformationTypeName) => transformationTypeName.entity
)
public names: Promise<TransformationTypeName[]>;
public names!: Promise<TransformationTypeName[]>;
@Column({
nullable: true,
})
public description: string;
public description!: string;
/**
* the transformations of this type
*/
@OneToMany(() => Transformation, (transformation: Transformation) => transformation.type)
public transformations: Promise<Transformation[]>;
public transformations!: Promise<Transformation[]>;
/**
* if that transformation conserves the tags of the original work
@ -40,5 +40,5 @@ export class TransformationType implements IIdentifiableEntity, IMultiNamedEntit
nullable: false,
default: false,
})
public conservesTags: boolean;
public conservesTags!: boolean;
}

View File

@ -8,7 +8,7 @@ import { Work } from './work';
@Entity()
export class Transformation implements IIdentifiableEntity, IOrderableEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
/**
* the work based on the original
@ -18,7 +18,7 @@ export class Transformation implements IIdentifiableEntity, IOrderableEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public byWork: Promise<Work>;
public byWork!: Promise<Work>;
/**
* the transformation type
@ -28,7 +28,7 @@ export class Transformation implements IIdentifiableEntity, IOrderableEntity {
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
})
public type: Promise<TransformationType>;
public type!: Promise<TransformationType>;
/**
* the original work
@ -38,11 +38,11 @@ export class Transformation implements IIdentifiableEntity, IOrderableEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public ofWork: Promise<Work>;
public ofWork!: Promise<Work>;
@Column({
nullable: false,
default: 0,
})
public order: number;
public order!: number;
}

View File

@ -9,7 +9,7 @@ import { Work } from './work';
@Entity()
export class WorkAuthor implements IIdentifiableEntity, IOrderableEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
/**
* the work
@ -19,14 +19,14 @@ export class WorkAuthor implements IIdentifiableEntity, IOrderableEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public work: Promise<Work>;
public work!: Promise<Work>;
/**
* the roles of the author in the work
*/
@ManyToMany(() => AuthorRole, (authorRole: AuthorRole) => authorRole.workAuthors)
@JoinTable()
public authorRoles: Promise<AuthorRole[]>;
public authorRoles!: Promise<AuthorRole[]>;
/**
* the author
@ -36,11 +36,11 @@ export class WorkAuthor implements IIdentifiableEntity, IOrderableEntity {
onDelete: 'RESTRICT',
onUpdate: 'CASCADE',
})
public author: Promise<Author>;
public author!: Promise<Author>;
@Column({
nullable: false,
default: 0,
})
public order: number;
public order!: number;
}

View File

@ -4,17 +4,17 @@ import { WorkCharacter } from './work-character';
@Entity()
export class WorkCharacterName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@ManyToOne(() => WorkCharacter, (workCharacter: WorkCharacter) => workCharacter.names, {
nullable: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public entity: Promise<WorkCharacter>;
public entity!: Promise<WorkCharacter>;
@Column({
nullable: false,
})
public name: string;
public name!: string;
}

View File

@ -12,15 +12,15 @@ import { WorldCharacter } from './world-character';
@Entity()
export class WorkCharacter implements IIdentifiableEntity, IMultiNamedEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@Column({
nullable: false,
})
public nameCanonical: string;
public nameCanonical!: string;
@OneToMany(() => WorkCharacterName, (workCharacterName: WorkCharacterName) => workCharacterName.entity)
public names: Promise<WorkCharacterName[]>;
public names!: Promise<WorkCharacterName[]>;
/**
* the work the character is a part of
@ -30,32 +30,32 @@ export class WorkCharacter implements IIdentifiableEntity, IMultiNamedEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public work: Promise<Work>;
public work!: Promise<Work>;
/**
* interaction with other characters as actor
*/
@ManyToMany(() => InteractionTag, (interactionTag: InteractionTag) => interactionTag.subjectCharacters)
@JoinTable()
public interactWith: Promise<InteractionTag[]>;
public interactWith!: Promise<InteractionTag[]>;
/**
* interaction with other characters as receiver
*/
@ManyToMany(() => InteractionTag, (interactionTag: InteractionTag) => interactionTag.objectCharacters)
@JoinTable()
public interactedBy: Promise<InteractionTag[]>;
public interactedBy!: Promise<InteractionTag[]>;
/**
* tags connected to the character
*/
@OneToMany(() => CharacterTag, (characterTag: CharacterTag) => characterTag.workCharacter)
public characterTags: Promise<CharacterTag[]>;
public characterTags!: Promise<CharacterTag[]>;
/**
* existing characters character is based on
*/
@ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacter) => worldCharacter.workCharacters)
@JoinTable()
public worldCharacters: Promise<WorldCharacter[]>;
public worldCharacters!: Promise<WorldCharacter[]>;
}

View File

@ -4,17 +4,17 @@ import { Work } from './work';
@Entity()
export class WorkName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@ManyToOne(() => Work, (work: Work) => work.names, {
nullable: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public entity: Promise<Work>;
public entity!: Promise<Work>;
@Column({
nullable: false,
})
public name: string;
public name!: string;
}

View File

@ -10,7 +10,7 @@ import { Work } from './work';
@PercentCheck('weight')
export class WorkTag implements IIdentifiableEntity, IWeightedEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
/**
* the describing tag
@ -20,7 +20,7 @@ export class WorkTag implements IIdentifiableEntity, IWeightedEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public tag: Promise<Tag>;
public tag!: Promise<Tag>;
/**
* the tagged work
@ -30,8 +30,8 @@ export class WorkTag implements IIdentifiableEntity, IWeightedEntity {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public work: Promise<Work>;
public work!: Promise<Work>;
@Column()
public weight: number;
public weight!: number;
}

View File

@ -19,58 +19,58 @@ import { World } from './world';
@PercentCheck('rating')
export class Work implements IIdentifiableEntity, IMultiNamedEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@Column({
nullable: false,
})
public nameCanonical: string;
public nameCanonical!: string;
@OneToMany(() => WorkName, (workName: WorkName) => workName.entity)
public names: Promise<WorkName[]>;
public names!: Promise<WorkName[]>;
/**
* digital representations of this work
*/
@OneToMany(() => Copy, (copy: Copy) => copy.original)
public copies: Promise<Copy[]>;
public copies!: Promise<Copy[]>;
/**
* other works this work is a transformation of
*/
@OneToMany(() => Transformation, (transformation: Transformation) => transformation.byWork)
public transformationOf: Promise<Transformation[]>;
public transformationOf!: Promise<Transformation[]>;
/**
* other works this work is transformed by
*/
@OneToMany(() => Transformation, (transformation: Transformation) => transformation.ofWork)
public transformedBy: Promise<Transformation[]>;
public transformedBy!: Promise<Transformation[]>;
/**
* the authors/publishers of this work
*/
@OneToMany(() => WorkAuthor, (workAuthor: WorkAuthor) => workAuthor.work)
public workAuthors: Promise<WorkAuthor[]>;
public workAuthors!: Promise<WorkAuthor[]>;
/**
* tags describing this work
*/
@OneToMany(() => WorkTag, (workTag: WorkTag) => workTag.work)
public workTags: Promise<WorkTag[]>;
public workTags!: Promise<WorkTag[]>;
/**
* characters in this work
*/
@OneToMany(() => WorkCharacter, (workCharacter: WorkCharacter) => workCharacter.work)
public workCharacters: Promise<WorkCharacter[]>;
public workCharacters!: Promise<WorkCharacter[]>;
/**
* fictional worlds in which this work takes place
*/
@ManyToMany(() => World, (world: World) => world.works)
@JoinTable()
public worlds: Promise<World[]>;
public worlds!: Promise<World[]>;
/**
* if this work i canon in above fictional world
@ -79,7 +79,7 @@ export class Work implements IIdentifiableEntity, IMultiNamedEntity {
nullable: false,
default: false,
})
public isCanonical: boolean;
public isCanonical!: boolean;
/**
* the user rating of this work
@ -87,7 +87,7 @@ export class Work implements IIdentifiableEntity, IMultiNamedEntity {
@Column({
nullable: true,
})
public rating: number;
public rating!: number;
/**
* the release date of the work
@ -95,18 +95,18 @@ export class Work implements IIdentifiableEntity, IMultiNamedEntity {
@Column({
nullable: true,
})
public releaseDate: Date;
public releaseDate!: Date;
/**
* the languages of the work (if applicable)
*/
@ManyToMany(() => Language, (language: Language) => language.works)
@JoinTable()
public languages: Promise<Language[]>;
public languages!: Promise<Language[]>;
/**
* the collections this work is a part of
*/
@OneToMany(() => CollectionPart, (collectionPart: CollectionPart) => collectionPart.work)
public collectionParts: Promise<CollectionPart[]>;
public collectionParts!: Promise<CollectionPart[]>;
}

View File

@ -4,17 +4,17 @@ import { WorldCharacter } from './world-character';
@Entity()
export class WorldCharacterName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@ManyToOne(() => WorldCharacter, (worldCharacter: WorldCharacter) => worldCharacter.names, {
nullable: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public entity: Promise<WorldCharacter>;
public entity!: Promise<WorldCharacter>;
@Column({
nullable: false,
})
public name: string;
public name!: string;
}

View File

@ -9,32 +9,32 @@ import { WorldCharacterName } from './world-character-name';
@Entity()
export class WorldCharacter implements IIdentifiableEntity, IMultiNamedEntity, IHierachicalEntity<WorldCharacter> {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@Column({
nullable: false,
})
public nameCanonical: string;
public nameCanonical!: string;
@OneToMany(() => WorldCharacterName, (worldCharacterName: WorldCharacterName) => worldCharacterName.entity)
public names: Promise<WorldCharacterName[]>;
public names!: Promise<WorldCharacterName[]>;
/**
* the characters in works which are based on this one
*/
@ManyToMany(() => WorkCharacter, (workCharacter: WorkCharacter) => workCharacter.worldCharacters)
public workCharacters: Promise<WorkCharacter[]>;
public workCharacters!: Promise<WorkCharacter[]>;
/**
* the fictional worlds this character is a part of
*/
@ManyToMany(() => World, (world: World) => world.worldCharacters)
public worlds: Promise<World[]>;
public worlds!: Promise<World[]>;
@ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacter) => worldCharacter.children)
@JoinTable()
public parents: Promise<WorldCharacter[]>;
public parents!: Promise<WorldCharacter[]>;
@ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacter) => worldCharacter.parents)
public children: Promise<WorldCharacter[]>;
public children!: Promise<WorldCharacter[]>;
}

View File

@ -4,17 +4,17 @@ import { World } from './world';
@Entity()
export class WorldName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@ManyToOne(() => World, (world: World) => world.names, {
nullable: false,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
})
public entity: Promise<World>;
public entity!: Promise<World>;
@Column({
nullable: false,
})
public name: string;
public name!: string;
}

View File

@ -9,33 +9,33 @@ import { WorldName } from './world-name';
@Entity()
export class World implements IIdentifiableEntity, IMultiNamedEntity, IHierachicalEntity<World> {
@PrimaryGeneratedColumn()
public id: number;
public id!: number;
@Column({
nullable: false,
})
public nameCanonical: string;
public nameCanonical!: string;
@OneToMany(() => WorldName, (worldName: WorldName) => worldName.entity)
public names: Promise<WorldName[]>;
public names!: Promise<WorldName[]>;
/**
* works taking place in this world
*/
@ManyToMany(() => Work, (work: Work) => work.worlds)
public works: Promise<Work[]>;
public works!: Promise<Work[]>;
/**
* canon characters in this world
*/
@ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacter) => worldCharacter.worlds)
@JoinTable()
public worldCharacters: Promise<WorldCharacter[]>;
public worldCharacters!: Promise<WorldCharacter[]>;
@ManyToMany(() => World, (world: World) => world.parents)
public children: Promise<World[]>;
public children!: Promise<World[]>;
@ManyToMany(() => World, (world: World) => world.children)
@JoinTable()
public parents: Promise<World[]>;
public parents!: Promise<World[]>;
}

View File

@ -9,11 +9,11 @@ export class StoreValue {
* the key
*/
@PrimaryColumn()
public key: StoreKey;
public key: StoreKey | string = '';
/**
* the value
*/
@Column('simple-json')
public value: any;
public value: unknown;
}

View File

@ -27,13 +27,13 @@ switch (os.platform()) {
@injectable()
export abstract class AppWindow implements IAppWindow {
protected _window: BrowserWindow | null;
protected _window: BrowserWindow | null = null;
protected constructor(options: BrowserWindowConstructorOptions = {}) {
this.initialize(options);
}
public get window(): BrowserWindow {
public get window(): BrowserWindow | null {
return this._window;
}
@ -42,7 +42,11 @@ export abstract class AppWindow implements IAppWindow {
this.initialize();
}
return this._window.loadFile('frontend/index.html');
if (this._window) {
return this._window.loadFile('frontend/index.html');
} else {
return Promise.reject(new Error('the window was not initialized'));
}
}
public isClosed(): boolean {

View File

@ -1,7 +1,7 @@
import BrowserWindow = Electron.BrowserWindow;
export interface IAppWindow {
window: BrowserWindow;
window: BrowserWindow | null;
open(): Promise<void>;
isClosed(): boolean;
}

View File

@ -0,0 +1,8 @@
/**
* Error thrown when cookies could not be saved.
*/
export class CookieSaveError extends Error {
public constructor(message: string = 'failed to save cookies') {
super(message);
}
}

View File

@ -0,0 +1,10 @@
/**
* Error thrown when there is an error while initializing services or the app in general.
*
* You're pretty much fucked at this point, try to avoid unstable code on initialization
*/
export class InitializationError extends Error {
public constructor(message: string = 'initialization failed') {
super(message);
}
}

View File

@ -0,0 +1,8 @@
/**
* generic web crawler error
*/
export class WebCrawlerError extends Error {
public constructor(message: string = 'web crawler failed') {
super(message);
}
}

View File

@ -0,0 +1,10 @@
import { WebCrawlerError } from './web-crawler-error';
/**
* Error thrown when the web crawler can't work with the provided form.
*/
export class WebCrawlerFormError extends WebCrawlerError {
public constructor(message: string = 'web crawler failed') {
super(message);
}
}

View File

@ -0,0 +1,10 @@
import { WebCrawlerError } from './web-crawler-error';
/**
* Error thrown when the web crawler can't login
*/
export class WebCrawlerLoginError extends WebCrawlerError {
public constructor(message: string = 'login failed') {
super(message);
}
}

View File

@ -0,0 +1,7 @@
import { registerHandler } from '../ipc-server';
export function answer(channel: IpcChannel): DecoratorFactory<IIpcController, IpcHandler> {
return function (target: IIpcController, propertyKey): void {
registerHandler(channel, target, propertyKey);
};
}

View File

@ -1,33 +1,25 @@
import { ipcMain } from 'electron';
import { injectable } from 'inversify';
import IpcMainEvent = Electron.IpcMainEvent;
import BrowserWindow = Electron.BrowserWindow;
@injectable()
export abstract class IpcServer {
protected answer(channel: IpcChannels, handler: (data?: any) => Promise<any>): void {
ipcMain.on(channel, (event: IpcMainEvent, payload: IIpcPayload) => {
handler(payload.data)
.then((result: any) => {
const response: IIpcResponse = {
id: payload.id,
success: true,
data: result,
};
event.reply(channel, response);
})
.catch((reason: any) => {
const response: IIpcResponse = {
id: payload.id,
success: false,
error: reason.toString(),
};
event.reply(channel, response);
});
});
}
protected send(window: BrowserWindow, channel: IpcChannels, data: any): void {
window.webContents.send(channel, data);
}
export function registerHandler(channel: IpcChannel, controller: IIpcController, handler: string): void {
ipcMain.on(channel, (event: IpcMainEvent, payload: IIpcPayload) => {
((controller.get() as unknown) as { [x: string]: IpcHandler })
[handler](payload.data)
.then((result: unknown) => {
const response: IIpcResponse = {
id: payload.id,
success: true,
data: result,
};
event.reply(channel, response);
})
.catch((reason: Error) => {
const response: IIpcResponse = {
id: payload.id,
success: false,
error: reason.message,
};
event.reply(channel, response);
});
});
}

View File

@ -1,7 +1,8 @@
import { inject, injectable } from 'inversify';
import { JSDOM } from 'jsdom';
import { RequestInit, Response } from 'node-fetch';
import { Errors, RenaiError } from '../../core/error';
import { WebCrawlerFormError } from '../error/web-crawler-form-error';
import { WebCrawlerLoginError } from '../error/web-crawler-login-error';
import { IWebCrawler } from '../web-crawler/i-web-crawler';
import { INhentaiApi } from './i-nhentai-api';
@ -66,7 +67,7 @@ export class NhentaiApi implements INhentaiApi {
});
})
.then(() => {})
.catch(() => Promise.reject(new RenaiError(Errors.ELOGINFAIL)));
.catch(() => Promise.reject(new WebCrawlerLoginError()));
}
private getNHentai(path: string): Promise<Document> {
@ -111,14 +112,17 @@ export class NhentaiApi implements INhentaiApi {
if (name === usernameInput || name === passwordInput) {
isLoginForm = true;
} else if (name) {
valueStore[name] = input.getAttribute('value');
const value = input.getAttribute('value');
if (value) {
valueStore[name] = value;
}
}
}
if (isLoginForm) {
return valueStore;
}
}
return Promise.reject(new RenaiError(Errors.ENOLOGIN));
return Promise.reject(new WebCrawlerFormError());
});
}
}

View File

@ -0,0 +1,26 @@
import { container } from '../../core/container';
import { answer } from '../ipc/annotations/answer';
import { INhentaiApi } from './i-nhentai-api';
export class NhentaiIpcController implements IIpcController {
private nhentaiApi: INhentaiApi;
public constructor(nhentaiApi: INhentaiApi) {
this.nhentaiApi = nhentaiApi;
}
@answer(IpcChannel.LOGIN)
public login(credentials: ICredentials): Promise<void> {
return this.nhentaiApi.login(credentials.name, credentials.password);
}
@answer(IpcChannel.LOGGED_IN)
public loggedIn(): Promise<boolean> {
return this.nhentaiApi.isLoggedIn();
}
public get(): NhentaiIpcController {
const nhentaiApi: INhentaiApi = container.get(Symbol.for('nhentai-api'));
return new NhentaiIpcController(nhentaiApi);
}
}

View File

@ -1,18 +0,0 @@
import { inject, injectable } from 'inversify';
import { IpcServer } from '../ipc/ipc-server';
import { INhentaiApi } from './i-nhentai-api';
@injectable()
export class NhentaiIpcServer extends IpcServer {
private nhentaiApi: INhentaiApi;
public constructor(@inject(Symbol.for('nhentai-api')) nhentaiApi: INhentaiApi) {
super();
this.nhentaiApi = nhentaiApi;
this.answer(IpcChannels.LOGIN, (credentials: ICredentials) =>
this.nhentaiApi.login(credentials.name, credentials.password)
);
this.answer(IpcChannels.LOGGED_IN, () => this.nhentaiApi.isLoggedIn());
}
}

View File

@ -2,29 +2,26 @@ import { session } from 'electron';
import { injectable } from 'inversify';
import { isDev } from '../../core/dev';
import { ISession } from './i-session';
import OnHeadersReceivedListenerDetails = Electron.OnHeadersReceivedListenerDetails;
@injectable()
export class Session implements ISession {
public setHeaders(): void {
// these headers only work on web requests, file:// protocol is handled via meta tags in the html
session.defaultSession.webRequest.onHeadersReceived(
(details: OnHeadersReceivedListenerDetails, callback: (response: {}) => void) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': isDev()
? [
'default-src devtools:;' +
"script-src 'unsafe-eval';" +
"script-src-elem devtools: 'sha256-hl04hLzKBpmsfWF2wIA/0Vs6ZNV5T9ZNFY//3uXrgSk=';" +
"style-src devtools: 'unsafe-inline';" +
'connect-src devtools: data:',
]
: ["default-src 'none'"],
},
});
}
);
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': isDev()
? [
'default-src devtools:;' +
"script-src 'unsafe-eval';" +
"script-src-elem devtools: 'sha256-hl04hLzKBpmsfWF2wIA/0Vs6ZNV5T9ZNFY//3uXrgSk=';" +
"style-src devtools: 'unsafe-inline';" +
'connect-src devtools: data:',
]
: ["default-src 'none'"],
},
});
});
}
}

View File

@ -0,0 +1,4 @@
export interface IStore {
load: (key: StoreKey) => Promise<unknown>;
save: (key: StoreKey, data: unknown) => Promise<void>;
}

View File

@ -1,33 +1,19 @@
import { load, save } from './store';
const store = require('./store');
import { injectable } from 'inversify';
import { IStore } from './i-store';
interface IStoreMock extends IMock {
original: {
load: typeof load;
save: typeof save;
};
mock: {
load: (mock: typeof load) => void;
save: (mock: typeof save) => void;
};
restore: () => void;
/**
* This mock store saves the data in memory.
*/
@injectable()
export class StoreMock implements IStore {
private store: { [x in StoreKey]?: unknown } = {};
public load(key: StoreKey): Promise<unknown> {
return Promise.resolve(this.store[key]);
}
public save(key: StoreKey, data: unknown): Promise<void> {
this.store[key] = data;
return Promise.resolve();
}
}
export const storeMock: IStoreMock = {
original: {
load: store.load,
save: store.save,
},
mock: {
load(mock) {
store.load = mock;
},
save(mock) {
store.save = mock;
},
},
restore() {
store.load = this.original.load;
store.save = this.original.save;
},
};

View File

@ -3,7 +3,8 @@ import '../../../../mocks/electron';
import { expect } from 'chai';
import 'mocha';
import { load, save } from './store';
import { container } from '../../core/container';
import { IStore } from './i-store';
describe('Store Service', function () {
this.timeout(10000);
@ -17,7 +18,8 @@ describe('Store Service', function () {
});
it('loads saved data', () => {
const testData: any = {
const store: IStore = container.get(Symbol.for('store'));
const testData = {
something: 'gaga',
somethingElse: 0,
deepObject: {
@ -32,11 +34,12 @@ describe('Store Service', function () {
},
};
const expectedJson = JSON.stringify(testData);
return save(StoreKey.COOKIES, testData)
.then(() => load(StoreKey.COOKIES))
return store
.save(StoreKey.COOKIES, testData)
.then(() => store.load(StoreKey.COOKIES))
.then((data) => {
expect(JSON.stringify(data)).to.equal(expectedJson, 'store does not save and load data correctly');
return load(StoreKey.COOKIES);
return store.load(StoreKey.COOKIES);
})
.then((data) => {
expect(JSON.stringify(data)).to.equal(expectedJson, 'store does not load data correctly when loaded twice');

View File

@ -1,26 +1,31 @@
import { Databases, getConnection } from '../../core/database';
import { injectable } from 'inversify';
import { Database, getConnection } from '../../core/database';
import { StoreValue } from '../../entities/store/store-value';
import { IStore } from './i-store';
const CACHE_ID = 'store';
export async function load(key: StoreKey): Promise<any> {
const c = await getConnection(Databases.STORE);
const repository = c.getRepository(StoreValue);
const storeValue = await repository.findOne(key, {
cache: {
id: CACHE_ID,
milliseconds: 0,
},
});
return storeValue.value;
}
@injectable()
export class Store implements IStore {
public async load(key: StoreKey): Promise<unknown> {
const c = await getConnection(Database.STORE);
const repository = c.getRepository(StoreValue);
const storeValue = await repository.findOne(key, {
cache: {
id: CACHE_ID,
milliseconds: 0,
},
});
return storeValue?.value;
}
export async function save(key: StoreKey, data: any): Promise<void> {
const c = await getConnection(Databases.STORE);
const manager = c.manager;
const storeValue = new StoreValue();
storeValue.key = key;
storeValue.value = data;
await manager.save(storeValue);
await c.queryResultCache.remove([CACHE_ID]);
public async save(key: StoreKey, data: unknown): Promise<void> {
const c = await getConnection(Database.STORE);
const manager = c.manager;
const storeValue = new StoreValue();
storeValue.key = key;
storeValue.value = data;
await manager.save(storeValue);
await c.queryResultCache?.remove([CACHE_ID]);
}
}

View File

@ -2,13 +2,12 @@ import rewiremock from 'rewiremock';
import '../../../../mocks/electron';
import { expect } from 'chai';
import { CookieJar } from 'jsdom';
import 'mocha';
import nock from 'nock';
import { Response } from 'node-fetch';
import sinon from 'sinon';
import { WebCrawler } from './web-crawler';
import { storeMock } from '../store/store.mock';
import { container, mockStore } from '../../core/container';
import { IWebCrawler } from './i-web-crawler';
describe('Web Crawler', function () {
this.timeout(2000);
@ -16,9 +15,7 @@ describe('Web Crawler', function () {
before(() => {
rewiremock.enable();
storeMock.mock.load(() => Promise.resolve(new CookieJar().serializeSync()));
storeMock.mock.save(() => Promise.resolve());
mockStore();
});
beforeEach(() => {
@ -33,7 +30,7 @@ describe('Web Crawler', function () {
after(() => {
rewiremock.disable();
storeMock.restore();
mockStore(true);
});
it('fetches websites', async () => {
@ -52,11 +49,11 @@ describe('Web Crawler', function () {
)
.persist();
const webCrawler = new WebCrawler();
const webCrawler: IWebCrawler = container.get(Symbol.for('web-crawler'));
const res: Response = await webCrawler.fetch(testUrl);
expect(callback.callCount).to.equal(1, 'multiple requests (or none) are sent when only one should be');
const json = await res.json();
const json = (await res.json()) as unknown;
expect(json).to.deep.equal([{ id: 12, comment: 'Hey there' }], 'response body is incorrect');
});
});

View File

@ -1,8 +1,8 @@
import { injectable } from 'inversify';
import { inject, injectable } from 'inversify';
import { CookieJar } from 'jsdom';
import nodeFetch, { RequestInit, Response } from 'node-fetch';
import { Errors, RenaiError } from '../../core/error';
import { load, save } from '../store/store';
import { CookieSaveError } from '../error/cookie-save-error';
import { IStore } from '../store/i-store';
import { IWebCrawler } from './i-web-crawler';
@injectable()
@ -11,9 +11,12 @@ export class WebCrawler implements IWebCrawler {
private initialized: boolean;
public constructor() {
private store: IStore;
public constructor(@inject(Symbol.for('store')) store: IStore) {
this.initialized = false;
this.cookieJar = new CookieJar();
this.store = store;
}
public fetch(url: string, requestInit: RequestInit = {}): Promise<Response> {
@ -30,8 +33,8 @@ export class WebCrawler implements IWebCrawler {
},
};
return nodeFetch(url, cookiedInit).then((res: Response) => {
this.setCookies(res.headers.raw()['set-cookie'], url).catch((reason: any) => {
throw new RenaiError(Errors.ECOOKIESAVEFAIL, reason);
this.setCookies(res.headers.raw()['set-cookie'], url).catch((reason: Error) => {
throw new CookieSaveError(reason.message);
});
return res;
});
@ -40,9 +43,9 @@ export class WebCrawler implements IWebCrawler {
private init(): Promise<void> {
if (!this.initialized) {
return load(StoreKey.COOKIES).then((cookies: any) => {
return this.store.load(StoreKey.COOKIES).then((cookies: unknown) => {
if (cookies !== undefined) {
this.cookieJar = CookieJar.deserializeSync(cookies);
this.cookieJar = CookieJar.deserializeSync(cookies as string);
}
this.initialized = true;
});
@ -56,8 +59,8 @@ export class WebCrawler implements IWebCrawler {
header.forEach((cookie: string) => {
this.cookieJar.setCookieSync(cookie, url);
});
return save(StoreKey.COOKIES, this.cookieJar.serializeSync()).catch((reason: any) => {
throw new RenaiError(Errors.ECOOKIESAVEFAIL, reason);
return this.store.save(StoreKey.COOKIES, this.cookieJar.serializeSync()).catch((reason: Error) => {
throw new CookieSaveError(reason.message);
});
}
return Promise.resolve();

View File

@ -5,6 +5,7 @@
import App from './renderer/App.svelte';
((): void =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return -- fis this together with the typescript support of svelte
new App({
target: document.querySelector('#app'),
props: {

View File

@ -3,20 +3,20 @@ import { uuid } from '../../services/uuid';
import IpcRendererEvent = Electron.IpcRendererEvent;
const ipcClient: IIpcClient = {
ask: (channel: IpcChannels, data?: any): Promise<any> => {
ask: (channel: IpcChannel, data?: unknown): Promise<unknown> => {
const id = uuid();
const payload: IIpcPayload = {
id,
data,
};
return new Promise((resolve: (value?: any) => void, reject: (reason?: any) => void): void => {
return new Promise((resolve: (value?: unknown) => void, reject: (reason?: Error) => void): void => {
const listener = (event: IpcRendererEvent, response: IIpcResponse): void => {
if (response.id === id) {
if (response.success) {
resolve(response.data);
} else {
reject(response.error);
reject(new Error(response.error));
}
ipcRenderer.removeListener(channel, listener);
}
@ -28,9 +28,9 @@ const ipcClient: IIpcClient = {
};
export function login(credentials: ICredentials): Promise<void> {
return ipcClient.ask(IpcChannels.LOGIN, credentials);
return ipcClient.ask(IpcChannel.LOGIN, credentials) as Promise<void>;
}
export function isLoggedIn(): Promise<boolean> {
return ipcClient.ask(IpcChannels.LOGGED_IN);
return ipcClient.ask(IpcChannel.LOGGED_IN) as Promise<boolean>;
}

View File

@ -1,16 +1,21 @@
import { writable } from 'svelte/store';
import { writable, Readable } from 'svelte/store';
import * as api from './api';
const { subscribe, set } = writable<boolean>(false);
export const loggedIn = {
interface ILoggedIn extends Readable<boolean> {
fetchIsLoggedIn(): Promise<void>;
fetchLogin(credentials: ICredentials): Promise<void>;
}
export const loggedIn: ILoggedIn = {
subscribe,
fetchIsLoggedIn(): Promise<void> {
return api.isLoggedIn().then((isLoggedIn: boolean) => {
set(isLoggedIn);
});
},
fetchLogin(credentials: ICredentials): Promise<void> {
fetchLogin(this: ILoggedIn, credentials: ICredentials): Promise<void> {
return api.login(credentials).then(this.fetchIsLoggedIn);
},
};

View File

@ -1,4 +1,4 @@
import { v1 as uuidv1 } from 'uuid';
import { v1 as uuidV1 } from 'uuid';
const R = 0x52;
const e = 0x65;
@ -13,7 +13,7 @@ const nice = 0x45;
* see RFC 4122 4.2.1.
*/
export function uuid(): string {
return uuidv1({
return uuidV1({
node: [R, e, n, a, i, nice],
});
}

View File

@ -8,6 +8,7 @@
"esModuleInterop": true,
"resolveJsonModule": true,
"noImplicitAny": true,
"strict": true,
"removeComments": true,
"sourceMap": true,
"preserveConstEnums": false,

1
types/decorator-factory.d.ts vendored Normal file
View File

@ -0,0 +1 @@
type DecoratorFactory<T, U> = (target: T, propertyKey: string, descriptor: PropertyDescriptor) => void;

19
types/ipc.d.ts vendored
View File

@ -1,19 +1,19 @@
declare const enum IpcChannels {
ERROR = 'ERROR',
declare const enum IpcChannel {
LOGIN = 'LOGIN',
LOGGED_IN = 'LOGGED_IN',
}
interface IIpcPayload {
id: string;
data: any;
data: unknown;
}
interface IIpcResponse {
id: string;
success: boolean;
data?: any;
error?: any;
data?: unknown;
// just the error message
error?: string;
}
interface ICredentials {
@ -22,10 +22,11 @@ interface ICredentials {
}
interface IIpcClient {
ask: (channel: IpcChannels, data?: any) => Promise<any>;
ask: (channel: IpcChannel, data?: unknown) => Promise<unknown>;
}
interface IIpcServer {
answer: (channel: IpcChannels, handler: (data?: any) => Promise<any>) => void;
send: (channel: IpcChannels, data: any) => void;
type IpcHandler = (data?: unknown) => Promise<unknown>;
interface IIpcController {
get(): IIpcController;
}

7
types/mock.d.ts vendored
View File

@ -1,7 +0,0 @@
interface IMock {
original: object;
mock: {
[key: string]: (mock: any) => void;
};
restore: () => void;
}