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", "max-classes-per-file": "error",
"object-shorthand": ["error", "methods"], "object-shorthand": ["error", "methods"],
"no-useless-rename": "error", "no-useless-rename": "error",
"prefer-promise-reject-errors": "error",
"import/no-extraneous-dependencies": [ "import/no-extraneous-dependencies": [
"error", "error",
@ -65,22 +66,20 @@
"settings": { "settings": {
"import/extensions": [".ts", "d.ts", ".js", ".json"], "import/extensions": [".ts", "d.ts", ".js", ".json"],
"import/parsers": { "import/parsers": {
"@typescript-eslint/parser": [".ts", "d.ts"] "@typescript-eslint/parser": [".ts", ".d.ts"]
} }
}, },
"rules": { "rules": {
"no-console": "error", "no-console": "error",
"no-magic-numbers": "off", "no-magic-numbers": "off",
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/ban-types": [
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/typedef": [
"error", "error",
{ {
"arrowParameter": true, "extendDefaults": true,
"memberVariableDeclaration": true, "types": {
"parameter": true, "object": false
"propertyDeclaration": true }
} }
], ],
"@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/explicit-function-return-type": "error",
@ -103,14 +102,30 @@
], ],
"@typescript-eslint/prefer-for-of": "error", "@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/no-empty-function": "off", "@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/explicit-member-accessibility": "error",
"@typescript-eslint/unbound-method": "off", "@typescript-eslint/unbound-method": "off",
"@typescript-eslint/ban-ts-ignore": "off", "@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }], "@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }],
"@typescript-eslint/member-ordering": "error", "@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/no-magic-numbers": "off",
"@typescript-eslint/typedef": "off", "@typescript-eslint/typedef": "off",
"@typescript-eslint/no-var-requires": "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", "minimist": "^1.2.5",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sqlite3": "^4.2.0", "sqlite3": "^5.0.0",
"typeorm": "^0.2.25", "typeorm": "^0.2.25",
"uuid": "^7.0.3" "uuid": "^7.0.3"
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.51", "@electron-forge/cli": "^6.0.0-beta.52",
"@electron-forge/maker-squirrel": "^6.0.0-beta.51", "@electron-forge/maker-squirrel": "^6.0.0-beta.52",
"@types/chai": "^4.2.11", "@types/chai": "^4.2.11",
"@types/fs-extra": "^9.0.1", "@types/fs-extra": "^9.0.1",
"@types/jsdom": "^16.2.3", "@types/jsdom": "^16.2.3",
"@types/minimist": "^1.2.0", "@types/minimist": "^1.2.0",
"@types/mocha": "^7.0.2", "@types/mocha": "^7.0.2",
"@types/node": "^12.12.44", "@types/node": "^12.12.51",
"@types/node-fetch": "^2.5.7", "@types/node-fetch": "^2.5.7",
"@types/sinon": "^9.0.4", "@types/sinon": "^9.0.4",
"@types/uuid": "^7.0.4", "@types/uuid": "^7.0.4",
"@types/webpack": "^4.41.17", "@types/webpack": "^4.41.21",
"@typescript-eslint/eslint-plugin": "^2.34.0", "@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^2.34.0", "@typescript-eslint/parser": "^3.7.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"chokidar": "^3.4.0", "chokidar": "^3.4.1",
"concurrently": "^5.2.0", "concurrently": "^5.2.0",
"electron": "^8.3.1", "electron": "^8.4.0",
"electron-rebuild": "^1.11.0", "electron-rebuild": "^1.11.0",
"eslint": "^6.8.0", "eslint": "^7.5.0",
"eslint-config-prettier": "^6.11.0", "eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.20.2", "eslint-plugin-import": "^2.22.0",
"fast-check": "^1.24.2", "fast-check": "^1.26.0",
"handlebars": "^4.7.6", "handlebars": "^4.7.6",
"husky": "^4.2.5", "husky": "^4.2.5",
"lodash": "^4.17.15", "lodash": "^4.17.19",
"mocha": "^7.2.0", "mocha": "^7.2.0",
"nock": "^12.0.3", "nock": "^12.0.3",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"prettier": "^2.0.5", "prettier": "^2.0.5",
"rewiremock": "^3.14.2", "rewiremock": "^3.14.3",
"sinon": "^9.0.2", "sinon": "^9.0.2",
"spectron": "^10.0.1", "spectron": "^10.0.1",
"svelte": "^3.23.0", "svelte": "^3.24.0",
"svelte-loader": "^2.13.6", "svelte-loader": "^2.13.6",
"ts-loader": "^7.0.5", "ts-loader": "^7.0.5",
"typescript": "^3.9.5", "typescript": "^3.9.7",
"webpack": "^4.43.0", "webpack": "^4.43.0",
"webpack-cli": "^3.3.11" "webpack-cli": "^3.3.12"
}, },
"repository": "https://git.fuwafuwa.moe/Xymorot/RenaiApp", "repository": "https://git.fuwafuwa.moe/Xymorot/RenaiApp",
"bugs": "https://git.fuwafuwa.moe/Xymorot/RenaiApp/issues", "bugs": "https://git.fuwafuwa.moe/Xymorot/RenaiApp/issues",

View File

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

View File

@ -1,9 +1,4 @@
/* eslint-disable import/order */ /* eslint-disable import/order -- this is the entry point for the application and some things have to happen before others */
/**
* Disable Reasons
*
* 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 { container } from './main/core/container';
import './main/core/install'; import './main/core/install';
@ -23,7 +18,8 @@ async function createWindow(): Promise<void> {
// Open the DevTools. // Open the DevTools.
if (isDev()) { 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 { Container } from 'inversify';
import { MainAppWindow } from '../modules/app-window/main-app-window'; import { MainAppWindow } from '../modules/app-window/main-app-window';
import { NhentaiApi } from '../modules/nhentai/nhentai-api'; 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 { 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'; import { WebCrawler } from '../modules/web-crawler/web-crawler';
export const container = new Container({ defaultScope: 'Singleton' }); 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('web-crawler')).to(WebCrawler);
container.bind(Symbol.for('nhentai-api')).to(NhentaiApi); 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); container.bind(Symbol.for('app-window-main')).to(MainAppWindow);

View File

@ -3,7 +3,7 @@ import '../../../mocks/electron';
import { expect } from 'chai'; import { expect } from 'chai';
import 'mocha'; import 'mocha';
import { Databases, getConnection } from './database'; import { Database, getConnection } from './database';
describe('Database Service', () => { describe('Database Service', () => {
before(() => { before(() => {
@ -15,7 +15,7 @@ describe('Database Service', () => {
}); });
it('returns a connection', async () => { it('returns a connection', async () => {
const libraryConnection = await getConnection(Databases.LIBRARY); const libraryConnection = await getConnection(Database.LIBRARY);
expect(libraryConnection).to.not.equal(undefined); 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 { SqliteConnectionOptions } from 'typeorm/driver/sqlite/SqliteConnectionOptions';
import { appPath } from './app-path'; import { appPath } from './app-path';
export enum Databases { export enum Database {
LIBRARY = 'library', LIBRARY = 'library',
STORE = 'store', STORE = 'store',
} }
type MyConnectionOptions = { [key in Databases]?: SqliteConnectionOptions }; type MyConnectionOptions = { [key in Database]: SqliteConnectionOptions };
const databaseDir = path.resolve(appPath, 'database'); 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: { function getConnectionOptionsFor(database: Database): SqliteConnectionOptions {
[key in Databases]?: Connection; return {
} = {}; name: database,
type: 'sqlite',
function createConnection(database: Databases): Promise<Connection> { database: path.resolve(databaseDir, `${database}.db`),
return ormCreateConnection(connectionOptions[database]) cache: true,
.then((connection: Connection) => { entities: [`./src/main/entities/${database}/*.js`],
connections[database] = connection; migrations: [`./src/main/migrations/${database}/*.js`],
return connection.runMigrations(); cli: {
}) migrationsDir: `./src/main/migrations/${database}`,
.then(() => connections[database]); },
};
} }
export function getConnection(database: Databases): Promise<Connection> { const connectionOptions: MyConnectionOptions = {
if (connections[database] === undefined) { [Database.LIBRARY]: getConnectionOptionsFor(Database.LIBRARY),
return createConnection(database); [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]); 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 * @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 { export function PercentCheck(column: string): Function {
return Check( return Check(
`${column} needs to be between ${minValue} and ${maxValue}`, `${column} needs to be between ${minValue} and ${maxValue}`,

View File

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

View File

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

View File

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

View File

@ -8,19 +8,19 @@ import { WorkAuthor } from './work-author';
@Entity() @Entity()
export class Author implements IIdentifiableEntity, IMultiNamedEntity { export class Author implements IIdentifiableEntity, IMultiNamedEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id: number; public id!: number;
@Column({ @Column({
nullable: false, nullable: false,
}) })
public nameCanonical: string; public nameCanonical!: string;
@OneToMany(() => AuthorName, (authorName: AuthorName) => authorName.entity) @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 * ultimately connects the author with a work and their role in that work
*/ */
@OneToMany(() => WorkAuthor, (workAuthor: WorkAuthor) => workAuthor.author) @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') @PercentCheck('weight')
export class CharacterTag implements IIdentifiableEntity, IWeightedEntity { export class CharacterTag implements IIdentifiableEntity, IWeightedEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id: number; public id!: number;
/** /**
* the character ina work this tag describes * the character ina work this tag describes
@ -20,7 +20,7 @@ export class CharacterTag implements IIdentifiableEntity, IWeightedEntity {
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public workCharacter: Promise<WorkCharacter>; public workCharacter!: Promise<WorkCharacter>;
/** /**
* the describing tag * the describing tag
@ -30,8 +30,8 @@ export class CharacterTag implements IIdentifiableEntity, IWeightedEntity {
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public tag: Promise<Tag>; public tag!: Promise<Tag>;
@Column() @Column()
public weight: number; public weight!: number;
} }

View File

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

View File

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

View File

@ -13,17 +13,17 @@ import { CollectionPart } from './collection-part';
@Entity() @Entity()
export class Collection implements IIdentifiableEntity, IMultiNamedEntity { export class Collection implements IIdentifiableEntity, IMultiNamedEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id: number; public id!: number;
@Column() @Column()
public nameCanonical: string; public nameCanonical!: string;
@OneToMany(() => CollectionName, (collectionName: CollectionName) => collectionName.entity) @OneToMany(() => CollectionName, (collectionName: CollectionName) => collectionName.entity)
public names: Promise<CollectionName[]>; public names!: Promise<CollectionName[]>;
/** /**
* the connecting entity between this collection and the work * the connecting entity between this collection and the work
*/ */
@OneToMany(() => CollectionPart, (collectionPart: CollectionPart) => collectionPart.collection) @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() @Entity()
export class Copy implements IIdentifiableEntity { export class Copy implements IIdentifiableEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id: number; public id!: number;
/** /**
* the work this entity is a copy of * the work this entity is a copy of
@ -21,14 +21,14 @@ export class Copy implements IIdentifiableEntity {
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public original: Promise<Work>; public original!: Promise<Work>;
/** /**
* where to find this specific copy * where to find this specific copy
*/ */
@ManyToMany(() => Source, (source: Source) => source.copies) @ManyToMany(() => Source, (source: Source) => source.copies)
@JoinTable() @JoinTable()
public sources: Promise<Source[]>; public sources!: Promise<Source[]>;
/** /**
* identifying hash of the file contents * identifying hash of the file contents
@ -36,7 +36,7 @@ export class Copy implements IIdentifiableEntity {
@Column({ @Column({
nullable: false, nullable: false,
}) })
public hash: string; public hash!: string;
/** /**
* device location of the copy * device location of the copy
@ -44,7 +44,7 @@ export class Copy implements IIdentifiableEntity {
@Column({ @Column({
nullable: true, nullable: true,
}) })
public location: string; public location!: string;
/** /**
* the ordering of the copies belonging to the same work, * the ordering of the copies belonging to the same work,
@ -54,5 +54,5 @@ export class Copy implements IIdentifiableEntity {
nullable: false, nullable: false,
default: 0, default: 0,
}) })
public ranking: number; public ranking!: number;
} }

View File

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

View File

@ -10,11 +10,11 @@ export class Language {
* ISO 639-1 two-letter language code * ISO 639-1 two-letter language code
*/ */
@PrimaryColumn() @PrimaryColumn()
public code: string; public code!: string;
/** /**
* the works using this language * the works using this language
*/ */
@ManyToMany(() => Work, (work: Work) => work.languages) @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() @Entity()
export class SiteName implements IIdentifiableEntity, INameEntity { export class SiteName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id: number; public id!: number;
@ManyToOne(() => Site, (site: Site) => site.names, { @ManyToOne(() => Site, (site: Site) => site.names, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public entity: Promise<Site>; public entity!: Promise<Site>;
@Column({ @Column({
nullable: false, nullable: false,
}) })
public name: string; public name!: string;
} }

View File

@ -8,19 +8,19 @@ import { Source } from './source';
@Entity() @Entity()
export class Site implements IIdentifiableEntity, IMultiNamedEntity { export class Site implements IIdentifiableEntity, IMultiNamedEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id: number; public id!: number;
@Column({ @Column({
nullable: false, nullable: false,
}) })
public nameCanonical: string; public nameCanonical!: string;
@OneToMany(() => SiteName, (siteName: SiteName) => siteName.entity) @OneToMany(() => SiteName, (siteName: SiteName) => siteName.entity)
public names: Promise<SiteName[]>; public names!: Promise<SiteName[]>;
/** /**
* sources belonging to this site * sources belonging to this site
*/ */
@OneToMany(() => Source, (source: Source) => source.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() @Entity()
export class Source implements IIdentifiableEntity { export class Source implements IIdentifiableEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id: number; public id!: number;
/** /**
* the uri to the sauce * the uri to the sauce
@ -16,7 +16,7 @@ export class Source implements IIdentifiableEntity {
@Column({ @Column({
nullable: false, nullable: false,
}) })
public uri: string; public uri!: string;
/** /**
* the site connected to the source * the site connected to the source
@ -26,11 +26,11 @@ export class Source implements IIdentifiableEntity {
onDelete: 'RESTRICT', onDelete: 'RESTRICT',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public site: Promise<Site>; public site!: Promise<Site>;
/** /**
* the copies which can be found here * the copies which can be found here
*/ */
@ManyToMany(() => Copy, (copy: Copy) => copy.sources) @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() @Entity()
export class TagName implements IIdentifiableEntity, INameEntity { export class TagName implements IIdentifiableEntity, INameEntity {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id: number; public id!: number;
@ManyToOne(() => Tag, (tag: Tag) => tag.names, { @ManyToOne(() => Tag, (tag: Tag) => tag.names, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public entity: Promise<Tag>; public entity!: Promise<Tag>;
@Column({ @Column({
nullable: false, nullable: false,
}) })
public name: string; public name!: string;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,13 +27,13 @@ switch (os.platform()) {
@injectable() @injectable()
export abstract class AppWindow implements IAppWindow { export abstract class AppWindow implements IAppWindow {
protected _window: BrowserWindow | null; protected _window: BrowserWindow | null = null;
protected constructor(options: BrowserWindowConstructorOptions = {}) { protected constructor(options: BrowserWindowConstructorOptions = {}) {
this.initialize(options); this.initialize(options);
} }
public get window(): BrowserWindow { public get window(): BrowserWindow | null {
return this._window; return this._window;
} }
@ -42,7 +42,11 @@ export abstract class AppWindow implements IAppWindow {
this.initialize(); 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 { public isClosed(): boolean {

View File

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

View File

@ -1,7 +1,8 @@
import { inject, injectable } from 'inversify'; import { inject, injectable } from 'inversify';
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import { RequestInit, Response } from 'node-fetch'; 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 { IWebCrawler } from '../web-crawler/i-web-crawler';
import { INhentaiApi } from './i-nhentai-api'; import { INhentaiApi } from './i-nhentai-api';
@ -66,7 +67,7 @@ export class NhentaiApi implements INhentaiApi {
}); });
}) })
.then(() => {}) .then(() => {})
.catch(() => Promise.reject(new RenaiError(Errors.ELOGINFAIL))); .catch(() => Promise.reject(new WebCrawlerLoginError()));
} }
private getNHentai(path: string): Promise<Document> { private getNHentai(path: string): Promise<Document> {
@ -111,14 +112,17 @@ export class NhentaiApi implements INhentaiApi {
if (name === usernameInput || name === passwordInput) { if (name === usernameInput || name === passwordInput) {
isLoginForm = true; isLoginForm = true;
} else if (name) { } else if (name) {
valueStore[name] = input.getAttribute('value'); const value = input.getAttribute('value');
if (value) {
valueStore[name] = value;
}
} }
} }
if (isLoginForm) { if (isLoginForm) {
return valueStore; 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 { injectable } from 'inversify';
import { isDev } from '../../core/dev'; import { isDev } from '../../core/dev';
import { ISession } from './i-session'; import { ISession } from './i-session';
import OnHeadersReceivedListenerDetails = Electron.OnHeadersReceivedListenerDetails;
@injectable() @injectable()
export class Session implements ISession { export class Session implements ISession {
public setHeaders(): void { public setHeaders(): void {
// these headers only work on web requests, file:// protocol is handled via meta tags in the html // these headers only work on web requests, file:// protocol is handled via meta tags in the html
session.defaultSession.webRequest.onHeadersReceived( session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
(details: OnHeadersReceivedListenerDetails, callback: (response: {}) => void) => { callback({
callback({ responseHeaders: {
responseHeaders: { ...details.responseHeaders,
...details.responseHeaders, 'Content-Security-Policy': isDev()
'Content-Security-Policy': isDev() ? [
? [ 'default-src devtools:;' +
'default-src devtools:;' + "script-src 'unsafe-eval';" +
"script-src 'unsafe-eval';" + "script-src-elem devtools: 'sha256-hl04hLzKBpmsfWF2wIA/0Vs6ZNV5T9ZNFY//3uXrgSk=';" +
"script-src-elem devtools: 'sha256-hl04hLzKBpmsfWF2wIA/0Vs6ZNV5T9ZNFY//3uXrgSk=';" + "style-src devtools: 'unsafe-inline';" +
"style-src devtools: 'unsafe-inline';" + 'connect-src devtools: data:',
'connect-src devtools: data:', ]
] : ["default-src 'none'"],
: ["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'; import { injectable } from 'inversify';
const store = require('./store'); import { IStore } from './i-store';
interface IStoreMock extends IMock { /**
original: { * This mock store saves the data in memory.
load: typeof load; */
save: typeof save; @injectable()
}; export class StoreMock implements IStore {
mock: { private store: { [x in StoreKey]?: unknown } = {};
load: (mock: typeof load) => void;
save: (mock: typeof save) => void; public load(key: StoreKey): Promise<unknown> {
}; return Promise.resolve(this.store[key]);
restore: () => void; }
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 { expect } from 'chai';
import 'mocha'; import 'mocha';
import { load, save } from './store'; import { container } from '../../core/container';
import { IStore } from './i-store';
describe('Store Service', function () { describe('Store Service', function () {
this.timeout(10000); this.timeout(10000);
@ -17,7 +18,8 @@ describe('Store Service', function () {
}); });
it('loads saved data', () => { it('loads saved data', () => {
const testData: any = { const store: IStore = container.get(Symbol.for('store'));
const testData = {
something: 'gaga', something: 'gaga',
somethingElse: 0, somethingElse: 0,
deepObject: { deepObject: {
@ -32,11 +34,12 @@ describe('Store Service', function () {
}, },
}; };
const expectedJson = JSON.stringify(testData); const expectedJson = JSON.stringify(testData);
return save(StoreKey.COOKIES, testData) return store
.then(() => load(StoreKey.COOKIES)) .save(StoreKey.COOKIES, testData)
.then(() => store.load(StoreKey.COOKIES))
.then((data) => { .then((data) => {
expect(JSON.stringify(data)).to.equal(expectedJson, 'store does not save and load data correctly'); 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) => { .then((data) => {
expect(JSON.stringify(data)).to.equal(expectedJson, 'store does not load data correctly when loaded twice'); 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 { StoreValue } from '../../entities/store/store-value';
import { IStore } from './i-store';
const CACHE_ID = 'store'; const CACHE_ID = 'store';
export async function load(key: StoreKey): Promise<any> { @injectable()
const c = await getConnection(Databases.STORE); export class Store implements IStore {
const repository = c.getRepository(StoreValue); public async load(key: StoreKey): Promise<unknown> {
const storeValue = await repository.findOne(key, { const c = await getConnection(Database.STORE);
cache: { const repository = c.getRepository(StoreValue);
id: CACHE_ID, const storeValue = await repository.findOne(key, {
milliseconds: 0, cache: {
}, id: CACHE_ID,
}); milliseconds: 0,
return storeValue.value; },
} });
return storeValue?.value;
}
export async function save(key: StoreKey, data: any): Promise<void> { public async save(key: StoreKey, data: unknown): Promise<void> {
const c = await getConnection(Databases.STORE); const c = await getConnection(Database.STORE);
const manager = c.manager; const manager = c.manager;
const storeValue = new StoreValue(); const storeValue = new StoreValue();
storeValue.key = key; storeValue.key = key;
storeValue.value = data; storeValue.value = data;
await manager.save(storeValue); await manager.save(storeValue);
await c.queryResultCache.remove([CACHE_ID]); await c.queryResultCache?.remove([CACHE_ID]);
}
} }

View File

@ -2,13 +2,12 @@ import rewiremock from 'rewiremock';
import '../../../../mocks/electron'; import '../../../../mocks/electron';
import { expect } from 'chai'; import { expect } from 'chai';
import { CookieJar } from 'jsdom';
import 'mocha'; import 'mocha';
import nock from 'nock'; import nock from 'nock';
import { Response } from 'node-fetch'; import { Response } from 'node-fetch';
import sinon from 'sinon'; import sinon from 'sinon';
import { WebCrawler } from './web-crawler'; import { container, mockStore } from '../../core/container';
import { storeMock } from '../store/store.mock'; import { IWebCrawler } from './i-web-crawler';
describe('Web Crawler', function () { describe('Web Crawler', function () {
this.timeout(2000); this.timeout(2000);
@ -16,9 +15,7 @@ describe('Web Crawler', function () {
before(() => { before(() => {
rewiremock.enable(); rewiremock.enable();
storeMock.mock.load(() => Promise.resolve(new CookieJar().serializeSync())); mockStore();
storeMock.mock.save(() => Promise.resolve());
}); });
beforeEach(() => { beforeEach(() => {
@ -33,7 +30,7 @@ describe('Web Crawler', function () {
after(() => { after(() => {
rewiremock.disable(); rewiremock.disable();
storeMock.restore(); mockStore(true);
}); });
it('fetches websites', async () => { it('fetches websites', async () => {
@ -52,11 +49,11 @@ describe('Web Crawler', function () {
) )
.persist(); .persist();
const webCrawler = new WebCrawler(); const webCrawler: IWebCrawler = container.get(Symbol.for('web-crawler'));
const res: Response = await webCrawler.fetch(testUrl); const res: Response = await webCrawler.fetch(testUrl);
expect(callback.callCount).to.equal(1, 'multiple requests (or none) are sent when only one should be'); 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'); 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 { CookieJar } from 'jsdom';
import nodeFetch, { RequestInit, Response } from 'node-fetch'; import nodeFetch, { RequestInit, Response } from 'node-fetch';
import { Errors, RenaiError } from '../../core/error'; import { CookieSaveError } from '../error/cookie-save-error';
import { load, save } from '../store/store'; import { IStore } from '../store/i-store';
import { IWebCrawler } from './i-web-crawler'; import { IWebCrawler } from './i-web-crawler';
@injectable() @injectable()
@ -11,9 +11,12 @@ export class WebCrawler implements IWebCrawler {
private initialized: boolean; private initialized: boolean;
public constructor() { private store: IStore;
public constructor(@inject(Symbol.for('store')) store: IStore) {
this.initialized = false; this.initialized = false;
this.cookieJar = new CookieJar(); this.cookieJar = new CookieJar();
this.store = store;
} }
public fetch(url: string, requestInit: RequestInit = {}): Promise<Response> { public fetch(url: string, requestInit: RequestInit = {}): Promise<Response> {
@ -30,8 +33,8 @@ export class WebCrawler implements IWebCrawler {
}, },
}; };
return nodeFetch(url, cookiedInit).then((res: Response) => { return nodeFetch(url, cookiedInit).then((res: Response) => {
this.setCookies(res.headers.raw()['set-cookie'], url).catch((reason: any) => { this.setCookies(res.headers.raw()['set-cookie'], url).catch((reason: Error) => {
throw new RenaiError(Errors.ECOOKIESAVEFAIL, reason); throw new CookieSaveError(reason.message);
}); });
return res; return res;
}); });
@ -40,9 +43,9 @@ export class WebCrawler implements IWebCrawler {
private init(): Promise<void> { private init(): Promise<void> {
if (!this.initialized) { if (!this.initialized) {
return load(StoreKey.COOKIES).then((cookies: any) => { return this.store.load(StoreKey.COOKIES).then((cookies: unknown) => {
if (cookies !== undefined) { if (cookies !== undefined) {
this.cookieJar = CookieJar.deserializeSync(cookies); this.cookieJar = CookieJar.deserializeSync(cookies as string);
} }
this.initialized = true; this.initialized = true;
}); });
@ -56,8 +59,8 @@ export class WebCrawler implements IWebCrawler {
header.forEach((cookie: string) => { header.forEach((cookie: string) => {
this.cookieJar.setCookieSync(cookie, url); this.cookieJar.setCookieSync(cookie, url);
}); });
return save(StoreKey.COOKIES, this.cookieJar.serializeSync()).catch((reason: any) => { return this.store.save(StoreKey.COOKIES, this.cookieJar.serializeSync()).catch((reason: Error) => {
throw new RenaiError(Errors.ECOOKIESAVEFAIL, reason); throw new CookieSaveError(reason.message);
}); });
} }
return Promise.resolve(); return Promise.resolve();

View File

@ -5,6 +5,7 @@
import App from './renderer/App.svelte'; import App from './renderer/App.svelte';
((): void => ((): 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({ new App({
target: document.querySelector('#app'), target: document.querySelector('#app'),
props: { props: {

View File

@ -3,20 +3,20 @@ import { uuid } from '../../services/uuid';
import IpcRendererEvent = Electron.IpcRendererEvent; import IpcRendererEvent = Electron.IpcRendererEvent;
const ipcClient: IIpcClient = { const ipcClient: IIpcClient = {
ask: (channel: IpcChannels, data?: any): Promise<any> => { ask: (channel: IpcChannel, data?: unknown): Promise<unknown> => {
const id = uuid(); const id = uuid();
const payload: IIpcPayload = { const payload: IIpcPayload = {
id, id,
data, 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 => { const listener = (event: IpcRendererEvent, response: IIpcResponse): void => {
if (response.id === id) { if (response.id === id) {
if (response.success) { if (response.success) {
resolve(response.data); resolve(response.data);
} else { } else {
reject(response.error); reject(new Error(response.error));
} }
ipcRenderer.removeListener(channel, listener); ipcRenderer.removeListener(channel, listener);
} }
@ -28,9 +28,9 @@ const ipcClient: IIpcClient = {
}; };
export function login(credentials: ICredentials): Promise<void> { 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> { 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'; import * as api from './api';
const { subscribe, set } = writable<boolean>(false); 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, subscribe,
fetchIsLoggedIn(): Promise<void> { fetchIsLoggedIn(): Promise<void> {
return api.isLoggedIn().then((isLoggedIn: boolean) => { return api.isLoggedIn().then((isLoggedIn: boolean) => {
set(isLoggedIn); set(isLoggedIn);
}); });
}, },
fetchLogin(credentials: ICredentials): Promise<void> { fetchLogin(this: ILoggedIn, credentials: ICredentials): Promise<void> {
return api.login(credentials).then(this.fetchIsLoggedIn); 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 R = 0x52;
const e = 0x65; const e = 0x65;
@ -13,7 +13,7 @@ const nice = 0x45;
* see RFC 4122 4.2.1. * see RFC 4122 4.2.1.
*/ */
export function uuid(): string { export function uuid(): string {
return uuidv1({ return uuidV1({
node: [R, e, n, a, i, nice], node: [R, e, n, a, i, nice],
}); });
} }

View File

@ -8,6 +8,7 @@
"esModuleInterop": true, "esModuleInterop": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"noImplicitAny": true, "noImplicitAny": true,
"strict": true,
"removeComments": true, "removeComments": true,
"sourceMap": true, "sourceMap": true,
"preserveConstEnums": false, "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 { declare const enum IpcChannel {
ERROR = 'ERROR',
LOGIN = 'LOGIN', LOGIN = 'LOGIN',
LOGGED_IN = 'LOGGED_IN', LOGGED_IN = 'LOGGED_IN',
} }
interface IIpcPayload { interface IIpcPayload {
id: string; id: string;
data: any; data: unknown;
} }
interface IIpcResponse { interface IIpcResponse {
id: string; id: string;
success: boolean; success: boolean;
data?: any; data?: unknown;
error?: any; // just the error message
error?: string;
} }
interface ICredentials { interface ICredentials {
@ -22,10 +22,11 @@ interface ICredentials {
} }
interface IIpcClient { interface IIpcClient {
ask: (channel: IpcChannels, data?: any) => Promise<any>; ask: (channel: IpcChannel, data?: unknown) => Promise<unknown>;
} }
interface IIpcServer { type IpcHandler = (data?: unknown) => Promise<unknown>;
answer: (channel: IpcChannels, handler: (data?: any) => Promise<any>) => void;
send: (channel: IpcChannels, data: any) => void; 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;
}