feat: add work serialization (and basis for the other entities) and an example ipc channel to get a work entity

This commit also adds language codes and a migration which inserts them all into the database.

BREAKING CHANGE: redoes the initial database migration
This commit is contained in:
Xymorot 2021-01-24 19:11:45 +01:00
parent 8d6d7dc6d8
commit 6b2824daab
148 changed files with 1678 additions and 511 deletions

View File

@ -1,14 +1,11 @@
{ {
"root": true, "root": true,
"plugins": ["@typescript-eslint", "import"], "plugins": ["@typescript-eslint", "import"],
"extends": ["eslint:recommended", "prettier"], "extends": ["eslint:recommended", "prettier", "plugin:import/recommended"],
"parserOptions": { "parserOptions": {
"ecmaVersion": 2019, "ecmaVersion": 2019,
"sourceType": "module" "sourceType": "module"
}, },
"settings": {
"import/core-modules": ["electron"]
},
"env": { "env": {
"browser": true, "browser": true,
"node": true "node": true
@ -43,6 +40,8 @@
"no-constant-condition": ["error", { "checkLoops": false }], "no-constant-condition": ["error", { "checkLoops": false }],
"no-throw-literal": "error", "no-throw-literal": "error",
"curly": "error", "curly": "error",
"no-promise-executor-return": "error",
"no-return-await": "error",
"import/no-extraneous-dependencies": [ "import/no-extraneous-dependencies": [
"error", "error",
@ -50,6 +49,7 @@
"devDependencies": [ "devDependencies": [
"**/*.{spec,mock}.*", "**/*.{spec,mock}.*",
"src/**/test/*", "src/**/test/*",
"src/renderer.ts",
"src/renderer/**/*", "src/renderer/**/*",
"declarations/**/*", "declarations/**/*",
"templates/**/*", "templates/**/*",
@ -57,6 +57,31 @@
] ]
} }
], ],
"import/no-restricted-paths": [
"error",
{
"zones": [
{
"target": "./src/main",
"from": "./src",
"except": ["./main", "./shared"],
"message": "only import from main/shared"
},
{
"target": "./src/renderer",
"from": "./src",
"except": ["./renderer", "./shared"],
"message": "only import from renderer/shared"
},
{
"target": "./src/shared",
"from": "./src",
"except": ["./shared"],
"message": "only import from shared"
}
]
}
],
"import/no-default-export": "error", "import/no-default-export": "error",
"import/first": "error", "import/first": "error",
"import/order": [ "import/order": [
@ -75,7 +100,9 @@
"plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier/@typescript-eslint" "prettier/@typescript-eslint",
"plugin:import/typescript",
"plugin:import/electron"
], ],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {

View File

@ -9,6 +9,7 @@ import '../modules/nhentai/nhentai-ipc-controller';
import { NhentaiAppWindow } from '../modules/nhentai/nhentai-app-window'; import { NhentaiAppWindow } from '../modules/nhentai/nhentai-app-window';
import { NhentaiSourceGetter } from '../modules/nhentai/nhentai-source-getter'; import { NhentaiSourceGetter } from '../modules/nhentai/nhentai-source-getter';
import { Store } from '../modules/store/store'; import { Store } from '../modules/store/store';
import '../modules/entity-api/entity-api-ipc-controller';
import BindingToSyntax = interfaces.BindingToSyntax; import BindingToSyntax = interfaces.BindingToSyntax;
export const container = { export const container = {

View File

@ -2,16 +2,16 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Author } from './author'; import { Author } from './author';
@Entity() @Entity()
export class AuthorName implements IdentifiableEntityInterface, NameEntityInterface { export class AuthorName implements AuthorNameEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@ManyToOne(() => Author, (author: Author) => author.names, { @ManyToOne(() => Author, (author: AuthorEntityInterface) => author.names, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public entity!: Promise<Author>; public entity!: Promise<AuthorEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -2,16 +2,16 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { AuthorRole } from './author-role'; import { AuthorRole } from './author-role';
@Entity() @Entity()
export class AuthorRoleName implements IdentifiableEntityInterface, NameEntityInterface { export class AuthorRoleName implements AuthorRoleNameEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@ManyToOne(() => AuthorRole, (authorRole: AuthorRole) => authorRole.names, { @ManyToOne(() => AuthorRole, (authorRole: AuthorRoleEntityInterface) => authorRole.names, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public entity!: Promise<AuthorRole>; public entity!: Promise<AuthorRoleEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -2,14 +2,10 @@ import { Column, Entity, ManyToMany, OneToMany, PrimaryGeneratedColumn } from 't
import { AuthorRoleName } from './author-role-name'; import { AuthorRoleName } from './author-role-name';
import { WorkAuthor } from './work-author'; import { WorkAuthor } from './work-author';
/**
* This entity describes the role an author has in a work.
* Examples: story writing, drawing, animating, publishing, ...
*/
@Entity() @Entity()
export class AuthorRole implements IdentifiableEntityInterface, MultiNamedEntityInterface, DescribableEntityInterface { export class AuthorRole implements AuthorRoleEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@Column({ @Column({
nullable: false, nullable: false,
@ -17,14 +13,11 @@ export class AuthorRole implements IdentifiableEntityInterface, MultiNamedEntity
}) })
public nameCanonical!: string; public nameCanonical!: string;
@OneToMany(() => AuthorRoleName, (authorRoleName: AuthorRoleName) => authorRoleName.entity) @OneToMany(() => AuthorRoleName, (authorRoleName: AuthorRoleNameEntityInterface) => authorRoleName.entity)
public names!: Promise<AuthorRoleName[]>; public names!: Promise<AuthorRoleNameEntityInterface[]>;
/** @ManyToMany(() => WorkAuthor, (workAuthor: WorkAuthorEntityInterface) => workAuthor.authorRoles)
* relation to the entity connecting with the author and work public workAuthors!: Promise<WorkAuthorEntityInterface[]>;
*/
@ManyToMany(() => WorkAuthor, (workAuthor: WorkAuthor) => workAuthor.authorRoles)
public workAuthors!: Promise<WorkAuthor[]>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -2,13 +2,10 @@ import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { AuthorName } from './author-name'; import { AuthorName } from './author-name';
import { WorkAuthor } from './work-author'; import { WorkAuthor } from './work-author';
/**
* This entity represents a single real-world entity, be it a person or named group of persons.
*/
@Entity() @Entity()
export class Author implements IdentifiableEntityInterface, MultiNamedEntityInterface { export class Author implements AuthorEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@Column({ @Column({
nullable: false, nullable: false,
@ -16,12 +13,9 @@ export class Author implements IdentifiableEntityInterface, MultiNamedEntityInte
}) })
public nameCanonical!: string; public nameCanonical!: string;
@OneToMany(() => AuthorName, (authorName: AuthorName) => authorName.entity) @OneToMany(() => AuthorName, (authorName: AuthorNameEntityInterface) => authorName.entity)
public names!: Promise<AuthorName[]>; public names!: Promise<AuthorNameEntityInterface[]>;
/** @OneToMany(() => WorkAuthor, (workAuthor: WorkAuthorEntityInterface) => workAuthor.author, {})
* ultimately connects the author with a work and their role in that work public workAuthors!: Promise<WorkAuthorEntityInterface[]>;
*/
@OneToMany(() => WorkAuthor, (workAuthor: WorkAuthor) => workAuthor.author)
public workAuthors!: Promise<WorkAuthor[]>;
} }

View File

@ -3,34 +3,25 @@ import { PercentCheck } from '../decorators/percent-check';
import { Tag } from './tag'; import { Tag } from './tag';
import { WorkCharacter } from './work-character'; import { WorkCharacter } from './work-character';
/**
* This tag entity tags a character in a work.
*/
@Entity() @Entity()
@PercentCheck('weight') @PercentCheck('weight')
export class CharacterTag implements IdentifiableEntityInterface, WeightedEntityInterface { export class CharacterTag implements CharacterTagEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
/** @ManyToOne(() => WorkCharacter, (workCharacter: WorkCharacterEntityInterface) => workCharacter.characterTags, {
* the character ina work this tag describes
*/
@ManyToOne(() => WorkCharacter, (workCharacter: WorkCharacter) => workCharacter.characterTags, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public workCharacter!: Promise<WorkCharacter>; public workCharacter!: Promise<WorkCharacterEntityInterface>;
/** @ManyToOne(() => Tag, (tag: TagEntityInterface) => tag.characterTags, {
* the describing tag
*/
@ManyToOne(() => Tag, (tag: Tag) => tag.characterTags, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public tag!: Promise<Tag>; public tag!: Promise<TagEntityInterface>;
@Column('int', { @Column('int', {
nullable: true, nullable: true,

View File

@ -2,16 +2,16 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Collection } from './collection'; import { Collection } from './collection';
@Entity() @Entity()
export class CollectionName implements IdentifiableEntityInterface, NameEntityInterface { export class CollectionName implements CollectionNameEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@ManyToOne(() => Collection, (collection: Collection) => collection.names, { @ManyToOne(() => Collection, (collection: CollectionEntityInterface) => collection.names, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public entity!: Promise<Collection>; public entity!: Promise<CollectionEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -2,34 +2,24 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Collection } from './collection'; import { Collection } from './collection';
import { Work } from './work'; import { Work } from './work';
/**
* This entity orders works in a collection.
* The main use case is chronological ordering.
*/
@Entity() @Entity()
export class CollectionPart implements IdentifiableEntityInterface, OrderableEntityInterface { export class CollectionPart implements CollectionPartEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
/** @ManyToOne(() => Collection, (collection: CollectionEntityInterface) => collection.parts, {
* the collection thw work is a part of
*/
@ManyToOne(() => Collection, (collection: Collection) => collection.parts, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public collection!: Promise<Collection>; public collection!: Promise<CollectionEntityInterface>;
/** @ManyToOne(() => Work, (work: WorkEntityInterface) => work.collectionParts, {
* the work inside the collection
*/
@ManyToOne(() => Work, (work: Work) => work.collectionParts, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public work!: Promise<Work>; public work!: Promise<WorkEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -2,18 +2,10 @@ import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { CollectionName } from './collection-name'; import { CollectionName } from './collection-name';
import { CollectionPart } from './collection-part'; import { CollectionPart } from './collection-part';
/**
* A collection is a set of works.
* For example, this can be a series or a set of alternate angles.
* What constitutes as a collection is ultimately up to the user.
*
* As a general rule of thumb:
* If authors of works see them as belonging together, they are a collection
*/
@Entity() @Entity()
export class Collection implements IdentifiableEntityInterface, MultiNamedEntityInterface { export class Collection implements CollectionEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@Column({ @Column({
nullable: false, nullable: false,
@ -21,12 +13,9 @@ export class Collection implements IdentifiableEntityInterface, MultiNamedEntity
}) })
public nameCanonical!: string; public nameCanonical!: string;
@OneToMany(() => CollectionName, (collectionName: CollectionName) => collectionName.entity) @OneToMany(() => CollectionName, (collectionName: CollectionNameEntityInterface) => collectionName.entity)
public names!: Promise<CollectionName[]>; public names!: Promise<CollectionNameEntityInterface[]>;
/** @OneToMany(() => CollectionPart, (collectionPart: CollectionPartEntityInterface) => collectionPart.collection)
* the connecting entity between this collection and the work public parts!: Promise<CollectionPartEntityInterface[]>;
*/
@OneToMany(() => CollectionPart, (collectionPart: CollectionPart) => collectionPart.collection)
public parts!: Promise<CollectionPart[]>;
} }

View File

@ -2,55 +2,33 @@ import { Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColum
import { Source } from './source'; import { Source } from './source';
import { Work } from './work'; import { Work } from './work';
/**
* A copy is the digital counterpart of a work.
* It corresponds to a unique file or set of files which represent the work on the users device.
*
* Multiple works can have multiple copies (think of different scans of a physical work, or lossy compression).
*/
@Entity() @Entity()
export class Copy implements IdentifiableEntityInterface { export class Copy implements CopyEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
/** @ManyToOne(() => Work, (work: WorkEntityInterface) => work.copies, {
* the work this entity is a copy of
*/
@ManyToOne(() => Work, (work: Work) => work.copies, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public original!: Promise<Work>; public original!: Promise<WorkEntityInterface>;
/** @ManyToMany(() => Source, (source: SourceEntityInterface) => source.copies)
* where to find this specific copy
*/
@ManyToMany(() => Source, (source: Source) => source.copies)
@JoinTable() @JoinTable()
public sources!: Promise<Source[]>; public sources!: Promise<SourceEntityInterface[]>;
/**
* identifying hash of the file contents
*/
@Column({ @Column({
nullable: false, nullable: false,
default: '', default: '',
}) })
public hash!: string; public hash!: string;
/**
* device location of the copy
*/
@Column('text', { @Column('text', {
nullable: true, nullable: true,
}) })
public location!: string | null; public location!: string | null;
/**
* the ordering of the copies belonging to the same work,
* lower number is higher ranked
*/
@Column({ @Column({
nullable: false, nullable: false,
default: 0, default: 0,

View File

@ -3,36 +3,24 @@ import { PercentCheck } from '../decorators/percent-check';
import { Tag } from './tag'; import { Tag } from './tag';
import { WorkCharacter } from './work-character'; import { WorkCharacter } from './work-character';
/**
* This tag entity tags an interaction between two characters.
*/
@Entity() @Entity()
@PercentCheck('weight') @PercentCheck('weight')
export class InteractionTag implements IdentifiableEntityInterface, WeightedEntityInterface { export class InteractionTag implements InteractionTagEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
/** @ManyToOne(() => Tag, (tag: TagEntityInterface) => tag.interactionTags, {
* the describing tag
*/
@ManyToOne(() => Tag, (tag: Tag) => tag.interactionTags, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public tag!: Promise<Tag>; public tag!: Promise<TagEntityInterface>;
/** @ManyToMany(() => WorkCharacter, (workCharacter: WorkCharacterEntityInterface) => workCharacter.interactWith)
* the actors of this interaction public subjectCharacters!: Promise<WorkCharacterEntityInterface[]>;
*/
@ManyToMany(() => WorkCharacter, (workCharacter: WorkCharacter) => workCharacter.interactWith)
public subjectCharacters!: Promise<WorkCharacter[]>;
/** @ManyToMany(() => WorkCharacter, (workCharacter: WorkCharacterEntityInterface) => workCharacter.interactedBy)
* the receivers of this interaction public objectCharacters!: Promise<WorkCharacterEntityInterface[]>;
*/
@ManyToMany(() => WorkCharacter, (workCharacter: WorkCharacter) => workCharacter.interactedBy)
public objectCharacters!: Promise<WorkCharacter[]>;
@Column('int', { @Column('int', {
nullable: true, nullable: true,

View File

@ -1,20 +1,11 @@
import { Entity, ManyToMany, PrimaryColumn } from 'typeorm'; import { Entity, ManyToMany, PrimaryColumn } from 'typeorm';
import { Work } from './work'; import { Work } from './work';
/**
* This entity is non-user-maintained and describes a language.
*/
@Entity() @Entity()
export class Language { export class Language implements LanguageEntityInterface {
/**
* ISO 639-1 two-letter language code
*/
@PrimaryColumn() @PrimaryColumn()
public code!: string; public code!: string;
/** @ManyToMany(() => Work, (work: WorkEntityInterface) => work.languages)
* the works using this language public works!: Promise<WorkEntityInterface[]>;
*/
@ManyToMany(() => Work, (work: Work) => work.languages)
public works!: Promise<Work[]>;
} }

View File

@ -2,16 +2,16 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Site } from './site'; import { Site } from './site';
@Entity() @Entity()
export class SiteName implements IdentifiableEntityInterface, NameEntityInterface { export class SiteName implements SiteNameEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@ManyToOne(() => Site, (site: Site) => site.names, { @ManyToOne(() => Site, (site: SiteEntityInterface) => site.names, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public entity!: Promise<Site>; public entity!: Promise<SiteEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -2,13 +2,10 @@ import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { SiteName } from './site-name'; import { SiteName } from './site-name';
import { Source } from './source'; import { Source } from './source';
/**
* This non-user-maintained entity describes an online provider of works which can be scraped.
*/
@Entity() @Entity()
export class Site implements IdentifiableEntityInterface, MultiNamedEntityInterface { export class Site implements SiteEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@Column({ @Column({
nullable: false, nullable: false,
@ -16,12 +13,9 @@ export class Site implements IdentifiableEntityInterface, MultiNamedEntityInterf
}) })
public nameCanonical!: string; public nameCanonical!: string;
@OneToMany(() => SiteName, (siteName: SiteName) => siteName.entity) @OneToMany(() => SiteName, (siteName: SiteNameEntityInterface) => siteName.entity)
public names!: Promise<SiteName[]>; public names!: Promise<SiteNameEntityInterface[]>;
/** @OneToMany(() => Source, (source: SourceEntityInterface) => source.site)
* sources belonging to this site public sources!: Promise<SourceEntityInterface[]>;
*/
@OneToMany(() => Source, (source: Source) => source.site)
public sources!: Promise<Source[]>;
} }

View File

@ -2,36 +2,24 @@ import { Column, Entity, ManyToMany, ManyToOne, PrimaryGeneratedColumn } from 't
import { Copy } from './copy'; import { Copy } from './copy';
import { Site } from './site'; import { Site } from './site';
/**
* This entity describes an external source of a copy, in most cases that is a website.
*/
@Entity() @Entity()
export class Source implements IdentifiableEntityInterface { export class Source implements SourceEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
/**
* the uri to the sauce
*/
@Column({ @Column({
nullable: false, nullable: false,
default: '', default: '',
}) })
public uri!: string; public uri!: string;
/** @ManyToOne(() => Site, (site: SiteEntityInterface) => site.sources, {
* the site connected to the source
*/
@ManyToOne(() => Site, (site: Site) => site.sources, {
nullable: true, nullable: true,
onDelete: 'RESTRICT', onDelete: 'RESTRICT',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public site!: Promise<Site> | null; public site!: Promise<SiteEntityInterface> | null;
/** @ManyToMany(() => Copy, (copy: CopyEntityInterface) => copy.sources)
* the copies which can be found here public copies!: Promise<CopyEntityInterface[]>;
*/
@ManyToMany(() => Copy, (copy: Copy) => copy.sources)
public copies!: Promise<Copy[]>;
} }

View File

@ -2,16 +2,16 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Tag } from './tag'; import { Tag } from './tag';
@Entity() @Entity()
export class TagName implements IdentifiableEntityInterface, NameEntityInterface { export class TagName implements TagNameEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@ManyToOne(() => Tag, (tag: Tag) => tag.names, { @ManyToOne(() => Tag, (tag: TagEntityInterface) => tag.names, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public entity!: Promise<Tag>; public entity!: Promise<TagEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -4,21 +4,10 @@ import { InteractionTag } from './interaction-tag';
import { TagName } from './tag-name'; import { TagName } from './tag-name';
import { WorkTag } from './work-tag'; import { WorkTag } from './work-tag';
/**
* This entity is the main tag entity.
* Tags have a name and a description.
* They can tag a work, a character, or a character interaction.
* They can also be in a hierarchy
*/
@Entity() @Entity()
export class Tag export class Tag implements TagEntityInterface {
implements
IdentifiableEntityInterface,
MultiNamedEntityInterface,
DescribableEntityInterface,
HierarchicalEntityInterface<Tag> {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@Column({ @Column({
nullable: false, nullable: false,
@ -26,26 +15,17 @@ export class Tag
}) })
public nameCanonical!: string; public nameCanonical!: string;
@OneToMany(() => TagName, (tagName: TagName) => tagName.entity) @OneToMany(() => TagName, (tagName: TagNameEntityInterface) => tagName.entity)
public names!: Promise<TagName[]>; public names!: Promise<TagNameEntityInterface[]>;
/** @OneToMany(() => WorkTag, (workTag: WorkTagEntityInterface) => workTag.tag)
* this tag tagging a work public workTags!: Promise<WorkTagEntityInterface[]>;
*/
@OneToMany(() => WorkTag, (workTag: WorkTag) => workTag.tag)
public workTags!: Promise<WorkTag[]>;
/** @OneToMany(() => CharacterTag, (characterTag: CharacterTagEntityInterface) => characterTag.tag)
* this tag tagging characters public characterTags!: Promise<CharacterTagEntityInterface[]>;
*/
@OneToMany(() => CharacterTag, (characterTag: CharacterTag) => characterTag.tag)
public characterTags!: Promise<CharacterTag[]>;
/** @OneToMany(() => InteractionTag, (interactionTag: InteractionTagEntityInterface) => interactionTag.tag)
* this tag tagging a character interaction public interactionTags!: Promise<InteractionTagEntityInterface[]>;
*/
@OneToMany(() => InteractionTag, (interactionTag: InteractionTag) => interactionTag.tag)
public interactionTags!: Promise<InteractionTag[]>;
@ManyToMany(() => Tag, (tag: Tag) => tag.children) @ManyToMany(() => Tag, (tag: Tag) => tag.children)
@JoinTable() @JoinTable()

View File

@ -2,16 +2,20 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { TransformationType } from './transformation-type'; import { TransformationType } from './transformation-type';
@Entity() @Entity()
export class TransformationTypeName implements IdentifiableEntityInterface, NameEntityInterface { export class TransformationTypeName implements TransformationTypeNameEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@ManyToOne(() => TransformationType, (transformationType: TransformationType) => transformationType.names, { @ManyToOne(
nullable: false, () => TransformationType,
onDelete: 'CASCADE', (transformationType: TransformationTypeEntityInterface) => transformationType.names,
onUpdate: 'CASCADE', {
}) nullable: false,
public entity!: Promise<TransformationType>; onDelete: 'CASCADE',
onUpdate: 'CASCADE',
}
)
public entity!: Promise<TransformationTypeEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -2,15 +2,10 @@ import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Transformation } from './transformation'; import { Transformation } from './transformation';
import { TransformationTypeName } from './transformation-type-name'; import { TransformationTypeName } from './transformation-type-name';
/**
* This entity describes a transformation type.
* Possible type: translation, decensor, collection, ...
*/
@Entity() @Entity()
export class TransformationType export class TransformationType implements TransformationTypeEntityInterface {
implements IdentifiableEntityInterface, MultiNamedEntityInterface, DescribableEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@Column({ @Column({
nullable: false, nullable: false,
@ -20,9 +15,10 @@ export class TransformationType
@OneToMany( @OneToMany(
() => TransformationTypeName, () => TransformationTypeName,
(transformationTypeName: TransformationTypeName) => transformationTypeName.entity (transformationTypeName: TransformationTypeNameEntityInterface) => transformationTypeName.entity,
{}
) )
public names!: Promise<TransformationTypeName[]>; public names!: Promise<TransformationTypeNameEntityInterface[]>;
@Column({ @Column({
nullable: false, nullable: false,
@ -33,8 +29,8 @@ export class TransformationType
/** /**
* the transformations of this type * the transformations of this type
*/ */
@OneToMany(() => Transformation, (transformation: Transformation) => transformation.type) @OneToMany(() => Transformation, (transformation: TransformationEntityInterface) => transformation.type)
public transformations!: Promise<Transformation[]>; public transformations!: Promise<TransformationEntityInterface[]>;
/** /**
* if that transformation conserves the tags of the original work * if that transformation conserves the tags of the original work

View File

@ -2,17 +2,11 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { TransformationType } from './transformation-type'; import { TransformationType } from './transformation-type';
import { Work } from './work'; import { Work } from './work';
/**
* This entity describes how one work is transformed to another.
*/
@Entity() @Entity()
export class Transformation implements IdentifiableEntityInterface, OrderableEntityInterface { export class Transformation implements TransformationEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
/**
* the work based on the original
*/
@ManyToOne(() => Work, (work: Work) => work.transformationOf, { @ManyToOne(() => Work, (work: Work) => work.transformationOf, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
@ -20,25 +14,23 @@ export class Transformation implements IdentifiableEntityInterface, OrderableEnt
}) })
public byWork!: Promise<Work>; public byWork!: Promise<Work>;
/** @ManyToOne(
* the transformation type () => TransformationType,
*/ (transformationType: TransformationTypeEntityInterface) => transformationType.transformations,
@ManyToOne(() => TransformationType, (transformationType: TransformationType) => transformationType.transformations, { {
nullable: false, nullable: false,
onDelete: 'RESTRICT', onDelete: 'RESTRICT',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) }
public type!: Promise<TransformationType>; )
public type!: Promise<TransformationTypeEntityInterface>;
/** @ManyToOne(() => Work, (work: WorkEntityInterface) => work.transformedBy, {
* the original work nullable: true,
*/
@ManyToOne(() => Work, (work: Work) => work.transformedBy, {
nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public ofWork!: Promise<Work>; public ofWork!: Promise<WorkEntityInterface> | null;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -3,40 +3,28 @@ import { Author } from './author';
import { AuthorRole } from './author-role'; import { AuthorRole } from './author-role';
import { Work } from './work'; import { Work } from './work';
/**
* This entity connects authors with their work and their role therein.
*/
@Entity() @Entity()
export class WorkAuthor implements IdentifiableEntityInterface, OrderableEntityInterface { export class WorkAuthor implements WorkAuthorEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
/** @ManyToOne(() => Work, (work: WorkEntityInterface) => work.workAuthors, {
* the work
*/
@ManyToOne(() => Work, (work: Work) => work.workAuthors, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public work!: Promise<Work>; public work!: Promise<WorkEntityInterface>;
/** @ManyToMany(() => AuthorRole, (authorRole: AuthorRoleEntityInterface) => authorRole.workAuthors)
* the roles of the author in the work
*/
@ManyToMany(() => AuthorRole, (authorRole: AuthorRole) => authorRole.workAuthors)
@JoinTable() @JoinTable()
public authorRoles!: Promise<AuthorRole[]>; public authorRoles!: Promise<AuthorRoleEntityInterface[]>;
/** @ManyToOne(() => Author, (author: AuthorEntityInterface) => author.workAuthors, {
* the author
*/
@ManyToOne(() => Author, (author: Author) => author.workAuthors, {
nullable: false, nullable: false,
onDelete: 'RESTRICT', onDelete: 'RESTRICT',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public author!: Promise<Author>; public author!: Promise<AuthorEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -2,16 +2,16 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { WorkCharacter } from './work-character'; import { WorkCharacter } from './work-character';
@Entity() @Entity()
export class WorkCharacterName implements IdentifiableEntityInterface, NameEntityInterface { export class WorkCharacterName implements WorkCharacterNameEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@ManyToOne(() => WorkCharacter, (workCharacter: WorkCharacter) => workCharacter.names, { @ManyToOne(() => WorkCharacter, (workCharacter: WorkCharacterEntityInterface) => workCharacter.names, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public entity!: Promise<WorkCharacter>; public entity!: Promise<WorkCharacterEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -5,14 +5,10 @@ import { Work } from './work';
import { WorkCharacterName } from './work-character-name'; import { WorkCharacterName } from './work-character-name';
import { WorldCharacter } from './world-character'; import { WorldCharacter } from './world-character';
/**
* This entity describes a character in a work.
* The character can be original or based on one or more existing characters.
*/
@Entity() @Entity()
export class WorkCharacter implements IdentifiableEntityInterface, MultiNamedEntityInterface { export class WorkCharacter implements WorkCharacterEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@Column({ @Column({
nullable: false, nullable: false,
@ -20,40 +16,24 @@ export class WorkCharacter implements IdentifiableEntityInterface, MultiNamedEnt
}) })
public nameCanonical!: string; public nameCanonical!: string;
@OneToMany(() => WorkCharacterName, (workCharacterName: WorkCharacterName) => workCharacterName.entity) @OneToMany(() => WorkCharacterName, (workCharacterName: WorkCharacterNameEntityInterface) => workCharacterName.entity)
public names!: Promise<WorkCharacterName[]>; public names!: Promise<WorkCharacterNameEntityInterface[]>;
/** @ManyToMany(() => Work, (work: WorkEntityInterface) => work.workCharacters)
* the works the character is a part of public works!: Promise<WorkEntityInterface[]>;
* one work character can be part of multiple works because of series
*/
@ManyToMany(() => Work, (work: Work) => work.workCharacters)
public works!: Promise<Work[]>;
/** @ManyToMany(() => InteractionTag, (interactionTag: InteractionTagEntityInterface) => interactionTag.subjectCharacters)
* interaction with other characters as actor
*/
@ManyToMany(() => InteractionTag, (interactionTag: InteractionTag) => interactionTag.subjectCharacters)
@JoinTable() @JoinTable()
public interactWith!: Promise<InteractionTag[]>; public interactWith!: Promise<InteractionTagEntityInterface[]>;
/** @ManyToMany(() => InteractionTag, (interactionTag: InteractionTagEntityInterface) => interactionTag.objectCharacters)
* interaction with other characters as receiver
*/
@ManyToMany(() => InteractionTag, (interactionTag: InteractionTag) => interactionTag.objectCharacters)
@JoinTable() @JoinTable()
public interactedBy!: Promise<InteractionTag[]>; public interactedBy!: Promise<InteractionTagEntityInterface[]>;
/** @OneToMany(() => CharacterTag, (characterTag: CharacterTagEntityInterface) => characterTag.workCharacter)
* tags connected to the character public characterTags!: Promise<CharacterTagEntityInterface[]>;
*/
@OneToMany(() => CharacterTag, (characterTag: CharacterTag) => characterTag.workCharacter)
public characterTags!: Promise<CharacterTag[]>;
/** @ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacterEntityInterface) => worldCharacter.workCharacters)
* existing characters character is based on
*/
@ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacter) => worldCharacter.workCharacters)
@JoinTable() @JoinTable()
public worldCharacters!: Promise<WorldCharacter[]>; public worldCharacters!: Promise<WorldCharacterEntityInterface[]>;
} }

View File

@ -2,16 +2,16 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Work } from './work'; import { Work } from './work';
@Entity() @Entity()
export class WorkName implements IdentifiableEntityInterface, NameEntityInterface { export class WorkName implements WorkNameEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@ManyToOne(() => Work, (work: Work) => work.names, { @ManyToOne(() => Work, (work: WorkEntityInterface) => work.names, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public entity!: Promise<Work>; public entity!: Promise<WorkEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -3,34 +3,25 @@ import { PercentCheck } from '../decorators/percent-check';
import { Tag } from './tag'; import { Tag } from './tag';
import { Work } from './work'; import { Work } from './work';
/**
* This tag entity tags a work.
*/
@Entity() @Entity()
@PercentCheck('weight') @PercentCheck('weight')
export class WorkTag implements IdentifiableEntityInterface, WeightedEntityInterface { export class WorkTag implements WorkTagEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
/** @ManyToOne(() => Tag, (tag: TagEntityInterface) => tag.workTags, {
* the describing tag
*/
@ManyToOne(() => Tag, (tag: Tag) => tag.workTags, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public tag!: Promise<Tag>; public tag!: Promise<TagEntityInterface>;
/** @ManyToOne(() => Work, (work: WorkEntityInterface) => work.workTags, {
* the tagged work
*/
@ManyToOne(() => Work, (work: Work) => work.workTags, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public work!: Promise<Work>; public work!: Promise<WorkEntityInterface>;
@Column('int', { @Column('int', {
nullable: true, nullable: true,

View File

@ -10,16 +10,11 @@ import { WorkName } from './work-name';
import { WorkTag } from './work-tag'; import { WorkTag } from './work-tag';
import { World } from './world'; import { World } from './world';
/**
* This is the main library entity.
*
* It describes a work of art organized by this software.
*/
@Entity() @Entity()
@PercentCheck('rating') @PercentCheck('rating')
export class Work implements IdentifiableEntityInterface, MultiNamedEntityInterface { export class Work implements WorkEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@Column({ @Column({
nullable: false, nullable: false,
@ -27,88 +22,55 @@ export class Work implements IdentifiableEntityInterface, MultiNamedEntityInterf
}) })
public nameCanonical!: string; public nameCanonical!: string;
@OneToMany(() => WorkName, (workName: WorkName) => workName.entity) @OneToMany(() => WorkName, (workName: WorkNameEntityInterface) => workName.entity)
public names!: Promise<WorkName[]>; public names!: Promise<WorkNameEntityInterface[]>;
/** @OneToMany(() => Copy, (copy: CopyEntityInterface) => copy.original, {})
* digital representations of this work public copies!: Promise<CopyEntityInterface[]>;
*/
@OneToMany(() => Copy, (copy: Copy) => copy.original)
public copies!: Promise<Copy[]>;
/** @OneToMany(() => Transformation, (transformation: TransformationEntityInterface) => transformation.byWork)
* other works this work is a transformation of public transformationOf!: Promise<TransformationEntityInterface[]>;
*/
@OneToMany(() => Transformation, (transformation: Transformation) => transformation.byWork)
public transformationOf!: Promise<Transformation[]>;
/** @OneToMany(() => Transformation, (transformation: TransformationEntityInterface) => transformation.ofWork)
* other works this work is transformed by public transformedBy!: Promise<TransformationEntityInterface[]>;
*/
@OneToMany(() => Transformation, (transformation: Transformation) => transformation.ofWork)
public transformedBy!: Promise<Transformation[]>;
/** @OneToMany(() => WorkAuthor, (workAuthor: WorkAuthorEntityInterface) => workAuthor.work)
* the authors/publishers of this work public workAuthors!: Promise<WorkAuthorEntityInterface[]>;
*/
@OneToMany(() => WorkAuthor, (workAuthor: WorkAuthor) => workAuthor.work)
public workAuthors!: Promise<WorkAuthor[]>;
/** @OneToMany(() => WorkTag, (workTag: WorkTagEntityInterface) => workTag.work)
* tags describing this work public workTags!: Promise<WorkTagEntityInterface[]>;
*/
@OneToMany(() => WorkTag, (workTag: WorkTag) => workTag.work)
public workTags!: Promise<WorkTag[]>;
/** @ManyToMany(() => WorkCharacter, (workCharacter: WorkCharacterEntityInterface) => workCharacter.works)
* characters in this work
*/
@ManyToMany(() => WorkCharacter, (workCharacter: WorkCharacter) => workCharacter.works)
@JoinTable() @JoinTable()
public workCharacters!: Promise<WorkCharacter[]>; public workCharacters!: Promise<WorkCharacterEntityInterface[]>;
/** @ManyToMany(() => World, (world: WorldEntityInterface) => world.works)
* fictional worlds in which this work takes place
*/
@ManyToMany(() => World, (world: World) => world.works)
@JoinTable() @JoinTable()
public worlds!: Promise<World[]>; public worlds!: Promise<WorldEntityInterface[]>;
/**
* if this work i canon in above fictional world
*/
@Column({ @Column({
nullable: false, nullable: false,
default: false, default: false,
}) })
public isCanonical!: boolean; public isCanonical!: boolean;
/**
* the user rating of this work
*/
@Column('int', { @Column('int', {
nullable: true, nullable: true,
}) })
public rating!: number | null; public rating!: number | null;
/**
* the release date of the work
*/
@Column('date', { @Column('date', {
nullable: true, nullable: true,
}) })
public releaseDate!: Date | null; public releaseDate!: string | null;
/** @ManyToMany(() => Language, (language: LanguageEntityInterface) => language.works)
* the languages of the work (if applicable)
*/
@ManyToMany(() => Language, (language: Language) => language.works)
@JoinTable() @JoinTable()
public languages!: Promise<Language[]>; public languages!: Promise<LanguageEntityInterface[]>;
/** /**
* 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: CollectionPartEntityInterface) => collectionPart.work)
public collectionParts!: Promise<CollectionPart[]>; public collectionParts!: Promise<CollectionPartEntityInterface[]>;
} }

View File

@ -2,16 +2,16 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { WorldCharacter } from './world-character'; import { WorldCharacter } from './world-character';
@Entity() @Entity()
export class WorldCharacterName implements IdentifiableEntityInterface, NameEntityInterface { export class WorldCharacterName implements WorldCharacterNameEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@ManyToOne(() => WorldCharacter, (worldCharacter: WorldCharacter) => worldCharacter.names, { @ManyToOne(() => WorldCharacter, (worldCharacter: WorldCharacterEntityInterface) => worldCharacter.names, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public entity!: Promise<WorldCharacter>; public entity!: Promise<WorldCharacterEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -3,14 +3,10 @@ import { WorkCharacter } from './work-character';
import { World } from './world'; import { World } from './world';
import { WorldCharacterName } from './world-character-name'; import { WorldCharacterName } from './world-character-name';
/**
* This entity describes a canon character in a fictional world.
*/
@Entity() @Entity()
export class WorldCharacter export class WorldCharacter implements WorldCharacterEntityInterface {
implements IdentifiableEntityInterface, MultiNamedEntityInterface, HierarchicalEntityInterface<WorldCharacter> {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@Column({ @Column({
nullable: false, nullable: false,
@ -18,25 +14,22 @@ export class WorldCharacter
}) })
public nameCanonical!: string; public nameCanonical!: string;
@OneToMany(() => WorldCharacterName, (worldCharacterName: WorldCharacterName) => worldCharacterName.entity) @OneToMany(
public names!: Promise<WorldCharacterName[]>; () => WorldCharacterName,
(worldCharacterName: WorldCharacterNameEntityInterface) => worldCharacterName.entity
)
public names!: Promise<WorldCharacterNameEntityInterface[]>;
/** @ManyToMany(() => WorkCharacter, (workCharacter: WorkCharacterEntityInterface) => workCharacter.worldCharacters)
* the characters in works which are based on this one
*/
@ManyToMany(() => WorkCharacter, (workCharacter: WorkCharacter) => workCharacter.worldCharacters)
public workCharacters!: Promise<WorkCharacter[]>; public workCharacters!: Promise<WorkCharacter[]>;
/** @ManyToMany(() => World, (world: WorldEntityInterface) => world.worldCharacters)
* the fictional worlds this character is a part of public worlds!: Promise<WorldEntityInterface[]>;
*/
@ManyToMany(() => World, (world: World) => world.worldCharacters)
public worlds!: Promise<World[]>;
@ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacter) => worldCharacter.children) @ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacterEntityInterface) => worldCharacter.children)
@JoinTable() @JoinTable()
public parents!: Promise<WorldCharacter[]>; public parents!: Promise<WorldCharacterEntityInterface[]>;
@ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacter) => worldCharacter.parents) @ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacterEntityInterface) => worldCharacter.parents)
public children!: Promise<WorldCharacter[]>; public children!: Promise<WorldCharacterEntityInterface[]>;
} }

View File

@ -2,16 +2,16 @@ import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { World } from './world'; import { World } from './world';
@Entity() @Entity()
export class WorldName implements IdentifiableEntityInterface, NameEntityInterface { export class WorldName implements WorldNameEntityInterface {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@ManyToOne(() => World, (world: World) => world.names, { @ManyToOne(() => World, (world: WorldEntityInterface) => world.names, {
nullable: false, nullable: false,
onDelete: 'CASCADE', onDelete: 'CASCADE',
onUpdate: 'CASCADE', onUpdate: 'CASCADE',
}) })
public entity!: Promise<World>; public entity!: Promise<WorldEntityInterface>;
@Column({ @Column({
nullable: false, nullable: false,

View File

@ -3,14 +3,10 @@ import { Work } from './work';
import { WorldCharacter } from './world-character'; import { WorldCharacter } from './world-character';
import { WorldName } from './world-name'; import { WorldName } from './world-name';
/**
* This entity describes a fictional world.
*/
@Entity() @Entity()
export class World export class World implements WorldEntityInterface {
implements IdentifiableEntityInterface, MultiNamedEntityInterface, HierarchicalEntityInterface<World> {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id!: number; public readonly id!: number;
@Column({ @Column({
nullable: false, nullable: false,
@ -18,26 +14,20 @@ export class World
}) })
public nameCanonical!: string; public nameCanonical!: string;
@OneToMany(() => WorldName, (worldName: WorldName) => worldName.entity) @OneToMany(() => WorldName, (worldName: WorldNameEntityInterface) => worldName.entity)
public names!: Promise<WorldName[]>; public names!: Promise<WorldNameEntityInterface[]>;
/** @ManyToMany(() => Work, (work: WorkEntityInterface) => work.worlds)
* works taking place in this world public works!: Promise<WorkEntityInterface[]>;
*/
@ManyToMany(() => Work, (work: Work) => work.worlds)
public works!: Promise<Work[]>;
/** @ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacterEntityInterface) => worldCharacter.worlds)
* canon characters in this world
*/
@ManyToMany(() => WorldCharacter, (worldCharacter: WorldCharacter) => worldCharacter.worlds)
@JoinTable() @JoinTable()
public worldCharacters!: Promise<WorldCharacter[]>; public worldCharacters!: Promise<WorldCharacterEntityInterface[]>;
@ManyToMany(() => World, (world: World) => world.parents) @ManyToMany(() => World, (world: WorldEntityInterface) => world.parents)
public children!: Promise<World[]>; public children!: Promise<WorldEntityInterface[]>;
@ManyToMany(() => World, (world: World) => world.children) @ManyToMany(() => World, (world: WorldEntityInterface) => world.children)
@JoinTable() @JoinTable()
public parents!: Promise<World[]>; public parents!: Promise<WorldEntityInterface[]>;
} }

View File

@ -1,7 +1,7 @@
import type { MigrationInterface, QueryRunner } from 'typeorm'; import type { MigrationInterface, QueryRunner } from 'typeorm';
export class initialMigration1597705000730 implements MigrationInterface { export class initial1611508597488 implements MigrationInterface {
name = 'initialMigration1597705000730'; name = 'initial1611508597488';
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query( await queryRunner.query(
@ -39,7 +39,7 @@ export class initialMigration1597705000730 implements MigrationInterface {
`CREATE TABLE "transformation_type" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "nameCanonical" varchar NOT NULL DEFAULT (''), "description" varchar NOT NULL DEFAULT (''), "conservesTags" boolean NOT NULL DEFAULT (0))` `CREATE TABLE "transformation_type" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "nameCanonical" varchar NOT NULL DEFAULT (''), "description" varchar NOT NULL DEFAULT (''), "conservesTags" boolean NOT NULL DEFAULT (0))`
); );
await queryRunner.query( await queryRunner.query(
`CREATE TABLE "transformation" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "order" integer NOT NULL DEFAULT (0), "byWorkId" integer NOT NULL, "typeId" integer NOT NULL, "ofWorkId" integer NOT NULL)` `CREATE TABLE "transformation" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "order" integer NOT NULL DEFAULT (0), "byWorkId" integer NOT NULL, "typeId" integer NOT NULL, "ofWorkId" integer)`
); );
await queryRunner.query( await queryRunner.query(
`CREATE TABLE "interaction_tag" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "weight" integer, "tagId" integer NOT NULL, CONSTRAINT "weight needs to be between 0 and 9007199254740991" CHECK (weight >= 0 AND weight <= 9007199254740991))` `CREATE TABLE "interaction_tag" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "weight" integer, "tagId" integer NOT NULL, CONSTRAINT "weight needs to be between 0 and 9007199254740991" CHECK (weight >= 0 AND weight <= 9007199254740991))`
@ -236,7 +236,7 @@ export class initialMigration1597705000730 implements MigrationInterface {
await queryRunner.query(`DROP TABLE "transformation_type_name"`); await queryRunner.query(`DROP TABLE "transformation_type_name"`);
await queryRunner.query(`ALTER TABLE "temporary_transformation_type_name" RENAME TO "transformation_type_name"`); await queryRunner.query(`ALTER TABLE "temporary_transformation_type_name" RENAME TO "transformation_type_name"`);
await queryRunner.query( await queryRunner.query(
`CREATE TABLE "temporary_transformation" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "order" integer NOT NULL DEFAULT (0), "byWorkId" integer NOT NULL, "typeId" integer NOT NULL, "ofWorkId" integer NOT NULL, CONSTRAINT "FK_263a368f9017f5725c4fa12351b" FOREIGN KEY ("byWorkId") REFERENCES "work" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_4deb36ce15d6547c1ed7e994720" FOREIGN KEY ("typeId") REFERENCES "transformation_type" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT "FK_d41fc0471e72b5d1dda372a662c" FOREIGN KEY ("ofWorkId") REFERENCES "work" ("id") ON DELETE CASCADE ON UPDATE CASCADE)` `CREATE TABLE "temporary_transformation" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "order" integer NOT NULL DEFAULT (0), "byWorkId" integer NOT NULL, "typeId" integer NOT NULL, "ofWorkId" integer, CONSTRAINT "FK_263a368f9017f5725c4fa12351b" FOREIGN KEY ("byWorkId") REFERENCES "work" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "FK_4deb36ce15d6547c1ed7e994720" FOREIGN KEY ("typeId") REFERENCES "transformation_type" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT "FK_d41fc0471e72b5d1dda372a662c" FOREIGN KEY ("ofWorkId") REFERENCES "work" ("id") ON DELETE CASCADE ON UPDATE CASCADE)`
); );
await queryRunner.query( await queryRunner.query(
`INSERT INTO "temporary_transformation"("id", "order", "byWorkId", "typeId", "ofWorkId") SELECT "id", "order", "byWorkId", "typeId", "ofWorkId" FROM "transformation"` `INSERT INTO "temporary_transformation"("id", "order", "byWorkId", "typeId", "ofWorkId") SELECT "id", "order", "byWorkId", "typeId", "ofWorkId" FROM "transformation"`
@ -788,7 +788,7 @@ export class initialMigration1597705000730 implements MigrationInterface {
await queryRunner.query(`DROP TABLE "temporary_interaction_tag"`); await queryRunner.query(`DROP TABLE "temporary_interaction_tag"`);
await queryRunner.query(`ALTER TABLE "transformation" RENAME TO "temporary_transformation"`); await queryRunner.query(`ALTER TABLE "transformation" RENAME TO "temporary_transformation"`);
await queryRunner.query( await queryRunner.query(
`CREATE TABLE "transformation" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "order" integer NOT NULL DEFAULT (0), "byWorkId" integer NOT NULL, "typeId" integer NOT NULL, "ofWorkId" integer NOT NULL)` `CREATE TABLE "transformation" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "order" integer NOT NULL DEFAULT (0), "byWorkId" integer NOT NULL, "typeId" integer NOT NULL, "ofWorkId" integer)`
); );
await queryRunner.query( await queryRunner.query(
`INSERT INTO "transformation"("id", "order", "byWorkId", "typeId", "ofWorkId") SELECT "id", "order", "byWorkId", "typeId", "ofWorkId" FROM "temporary_transformation"` `INSERT INTO "transformation"("id", "order", "byWorkId", "typeId", "ofWorkId") SELECT "id", "order", "byWorkId", "typeId", "ofWorkId" FROM "temporary_transformation"`

View File

@ -0,0 +1,198 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';
export class addLanguages1611508644004 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ABKHAZIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.AFAR}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.AFRIKAANS}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.AKAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ALBANIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.AMHARIC}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ARABIC}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ARAGONESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ARMENIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ASSAMESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.AVARIC}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.AVESTAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.AYMARA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.AZERBAIJANI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.BAMBARA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.BASHKIR}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.BASQUE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.BELARUSIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.BENGALI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.BIHARI_LANGUAGES}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.BISLAMA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.BOSNIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.BRETON}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.BULGARIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.BURMESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CATALAN_VALENCIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CHAMORRO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CHECHEN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CHICHEWA_CHEWA_NYANJA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CHINESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CHUVASH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CORNISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CORSICAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CREE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CROATIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CZECH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.DANISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.DIVEHI_DHIVEHI_MALDIVIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.DUTCH_FLEMISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.DZONGKHA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ENGLISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ESPERANTO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ESTONIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.EWE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.FAROESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.FIJIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.FINNISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.FRENCH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.FULAH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.GALICIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.GEORGIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.GERMAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.GREEK_MODERN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.GUARANI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.GUJARATI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.HAITIAN_HAITIAN_CREOLE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.HAUSA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.HEBREW}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.HERERO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.HINDI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.HIRI_MOTU}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.HUNGARIAN}')`);
await queryRunner.query(
`INSERT INTO language VALUES('${LangCode.INTERLINGUA_INTERNATIONAL_AUXILIARY_LANGUAGE_ASSOCIATION}')`
);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.INDONESIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.INTERLINGUE_OCCIDENTAL}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.IRISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.IGBO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.INUPIAQ}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.IDO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ICELANDIC}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ITALIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.INUKTITUT}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.JAPANESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.JAVANESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KALAALLISUT_GREENLANDIC}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KANNADA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KANURI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KASHMIRI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KAZAKH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.CENTRAL_KHMER}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KIKUYU_GIKUYU}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KINYARWANDA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KIRGHIZ_KYRGYZ}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KOMI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KONGO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KOREAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KURDISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.KUANYAMA_KWANYAMA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.LATIN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.LUXEMBOURGISH_LETZEBURGESCH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.GANDA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.LIMBURGAN_LIMBURGER_LIMBURGISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.LINGALA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.LAO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.LITHUANIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.LUBA_KATANGA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.LATVIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.MANX}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.MACEDONIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.MALAGASY}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.MALAY}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.MALAYALAM}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.MALTESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.MAORI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.MARATHI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.MARSHALLESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.MONGOLIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.NAURU}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.NAVAJO_NAVAHO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.NORTH_NDEBELE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.NEPALI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.NDONGA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.NORWEGIAN_BOKMAL}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.NORWEGIAN_NYNORSK}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.NORWEGIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SICHUAN_YI_NUOSU}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SOUTH_NDEBELE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.OCCITAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.OJIBWA}')`);
await queryRunner.query(
`INSERT INTO language VALUES('${LangCode.CHURCH_SLAVIC_OLD_SLAVONIC_CHURCH_SLAVONIC_OLD_BULGARIAN_OLD_CHURCH_SLAVONIC}')`
);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.OROMO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ORIYA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.OSSETIAN_OSSETIC}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.PUNJABI_PANJABI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.PALI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.PERSIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.POLISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.PASHTO_PUSHTO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.PORTUGUESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.QUECHUA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ROMANSH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.RUNDI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ROMANIAN_MOLDAVIAN_MOLDOVAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.RUSSIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SANSKRIT}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SARDINIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SINDHI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.NORTHERN_SAMI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SAMOAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SANGO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SERBIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.GAELIC_SCOTTISH_GAELIC}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SHONA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SINHALA_SINHALESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SLOVAK}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SLOVENIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SOMALI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SOUTHERN_SOTHO}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SPANISH_CASTILIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SUNDANESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SWAHILI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SWATI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.SWEDISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TAMIL}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TELUGU}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TAJIK}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.THAI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TIGRINYA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TIBETAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TURKMEN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TAGALOG}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TSWANA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TONGA_TONGA_ISLANDS}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TURKISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TSONGA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TATAR}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TWI}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.TAHITIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.UIGHUR_UYGHUR}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.UKRAINIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.URDU}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.UZBEK}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.VENDA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.VIETNAMESE}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.VOLAPUK}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.WALLOON}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.WELSH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.WOLOF}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.WESTERN_FRISIAN}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.XHOSA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.YIDDISH}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.YORUBA}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ZHUANG_CHUANG}')`);
await queryRunner.query(`INSERT INTO language VALUES('${LangCode.ZULU}')`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.clearTable('language');
}
}

View File

@ -131,5 +131,21 @@ export abstract class AppWindow implements AppWindowInterface {
}); });
} }
protected getTime(selector: string): Promise<number | undefined> {
return new Promise<number | undefined>((resolve) => {
this.getWindow()
.webContents.executeJavaScript(`Date(document.querySelector('${selector}').dateTime).getTime()`)
.then((time) => {
resolve(time);
})
.catch(() => {
void this.logger.warning(
`Could not get the of the presumed HTMLTimeElement with the selector '${selector}'.`
);
resolve(undefined);
});
});
}
protected abstract load(window: BrowserWindow): Promise<void>; protected abstract load(window: BrowserWindow): Promise<void>;
} }

View File

@ -1,5 +1,4 @@
import type { WebContents } from 'electron'; import type { WebContents, BrowserWindowConstructorOptions, LoadURLOptions } from 'electron';
import type { BrowserWindowConstructorOptions, LoadURLOptions } from 'electron';
import { promisify } from 'util'; import { promisify } from 'util';
import { AppWindow } from './app-window'; import { AppWindow } from './app-window';
import type { UrlAppWindowInterface } from './url-app-window-interface'; import type { UrlAppWindowInterface } from './url-app-window-interface';

View File

@ -0,0 +1,6 @@
export function dateObjectToString(date: Date): string {
return `${date.getUTCFullYear()}-${`${date.getUTCMonth()}`.padStart(2, '0')}-${`${date.getUTCDate()}`.padStart(
2,
'0'
)}`;
}

View File

@ -0,0 +1,20 @@
import { workSerializer } from '../../../shared/services/serialization/serializers/work';
import { Database, getConnection } from '../../core/database';
import { Work } from '../../entities/library/work';
import { answer } from '../ipc/annotations/answer';
export class EntityApiIpcController implements IpcController {
private constructor() {}
@answer(IpcChannel.ENTITY_GET_WORK)
public async getWork({ id }: { id: number }): Promise<WorkSerializedInterface> {
const connection = await getConnection(Database.LIBRARY);
const work = await connection.manager.getRepository(Work).findOneOrFail(id);
return workSerializer.serialize(work);
}
public get(): EntityApiIpcController {
return new EntityApiIpcController();
}
}

View File

@ -17,6 +17,7 @@ describe('Nhentai App Window', () => {
const nhentaiAppWindow: NhentaiAppWindowInterface = container.get('nhentai-app-window'); const nhentaiAppWindow: NhentaiAppWindowInterface = container.get('nhentai-app-window');
let expectedGallery: Nhentai.Gallery = { let expectedGallery: Nhentai.Gallery = {
url: 'https://nhentai.net/g/117300/',
title: { title: {
pre: '[Homunculus]', pre: '[Homunculus]',
main: 'Renai Sample', main: 'Renai Sample',
@ -43,11 +44,14 @@ describe('Nhentai App Window', () => {
'uncensored', 'uncensored',
'small breasts', 'small breasts',
], ],
languages: ['english', 'translated'],
uploadTime: 1411853968970,
}; };
let gallery = await nhentaiAppWindow.getGallery('117300'); let gallery = await nhentaiAppWindow.getGallery('117300');
expect(gallery).deep.equalInAnyOrder(expectedGallery, 'Renai Sample is not got correctly'); expect(gallery).deep.equalInAnyOrder(expectedGallery, 'Renai Sample is not got correctly');
expectedGallery = { expectedGallery = {
url: 'https://nhentai.net/g/273405/',
title: { title: {
pre: '(COMIC1☆12) [MOSQUITONE. (Great Mosu)]', pre: '(COMIC1☆12) [MOSQUITONE. (Great Mosu)]',
main: 'Koisuru Dai Akuma | The Archdemon In Love', main: 'Koisuru Dai Akuma | The Archdemon In Love',
@ -58,6 +62,8 @@ describe('Nhentai App Window', () => {
parodies: ['gabriel dropout'], parodies: ['gabriel dropout'],
characters: ['satanichia kurumizawa mcdowell'], characters: ['satanichia kurumizawa mcdowell'],
tags: ['sole female', 'sole male', 'defloration', 'uncensored', 'kissing'], tags: ['sole female', 'sole male', 'defloration', 'uncensored', 'kissing'],
languages: ['english', 'translated'],
uploadTime: 1558833881932,
}; };
gallery = await nhentaiAppWindow.getGallery('273405'); gallery = await nhentaiAppWindow.getGallery('273405');
expect(gallery).deep.equalInAnyOrder(expectedGallery, 'The Archdemon in Love is not got correctly!'); expect(gallery).deep.equalInAnyOrder(expectedGallery, 'The Archdemon in Love is not got correctly!');

View File

@ -34,6 +34,8 @@ import {
loginPageIsReady, loginPageIsReady,
favoritePageIsReady, favoritePageIsReady,
pageIsReady, pageIsReady,
timeSelector,
tagLabelLanguages,
} from './nhentai-util'; } from './nhentai-util';
const waitInterval = 2000; const waitInterval = 2000;
@ -98,7 +100,9 @@ export class NhentaiAppWindow extends CloudflareSiteAppWindow implements Nhentai
await this.open(); await this.open();
} }
const bookUrl = getBookUrl(identifier);
const gallery: Nhentai.Gallery = { const gallery: Nhentai.Gallery = {
url: bookUrl,
title: { title: {
pre: '', pre: '',
main: '', main: '',
@ -109,10 +113,11 @@ export class NhentaiAppWindow extends CloudflareSiteAppWindow implements Nhentai
parodies: [], parodies: [],
characters: [], characters: [],
tags: [], tags: [],
languages: [],
uploadTime: undefined,
}; };
const release = await this.acquireLock(); const release = await this.acquireLock();
const bookUrl = getBookUrl(identifier);
try { try {
await this.loadGalleryPageSafe(bookUrl); await this.loadGalleryPageSafe(bookUrl);
@ -141,6 +146,12 @@ export class NhentaiAppWindow extends CloudflareSiteAppWindow implements Nhentai
this.getTags(tagLabelTags).then((tags: string[]) => { this.getTags(tagLabelTags).then((tags: string[]) => {
gallery.tags = tags; gallery.tags = tags;
}), }),
this.getTags(tagLabelLanguages).then((languages: string[]) => {
gallery.languages = languages;
}),
this.getTime(timeSelector).then((time?: number) => {
gallery.uploadTime = time;
}),
]); ]);
this.close(); this.close();
release(); release();

View File

@ -1,7 +1,6 @@
import path from 'path'; import path from 'path';
import { createWriteStream } from 'fs-extra'; import { createWriteStream } from 'fs-extra';
import { container } from '../../core/container'; import { container } from '../../core/container';
import { Database, getConnection } from '../../core/database';
import type { Work } from '../../entities/library/work'; import type { Work } from '../../entities/library/work';
import type { DialogInterface } from '../dialog/dialog-interface'; import type { DialogInterface } from '../dialog/dialog-interface';
import { answer } from '../ipc/annotations/answer'; import { answer } from '../ipc/annotations/answer';
@ -53,9 +52,8 @@ export class NhentaiIpcController implements IpcController {
@answer(IpcChannel.NHENTAI_GET_WORK) @answer(IpcChannel.NHENTAI_GET_WORK)
public async nhentaiGetWork({ galleryId }: { galleryId: string }): Promise<Work> { public async nhentaiGetWork({ galleryId }: { galleryId: string }): Promise<Work> {
const work = await this.nhentaiSourceGetter.find(galleryId); const work = await this.nhentaiSourceGetter.find(galleryId);
const { manager } = await getConnection(Database.LIBRARY);
return manager.save(work); return work;
} }
public get(): NhentaiIpcController { public get(): NhentaiIpcController {

View File

@ -1,7 +1,18 @@
import { injectable } from 'inversify'; import { injectable } from 'inversify';
import { Database, getConnection } from '../../core/database';
import { inject } from '../../core/inject'; import { inject } from '../../core/inject';
import { Copy } from '../../entities/library/copy';
import { Language } from '../../entities/library/language';
import { Source } from '../../entities/library/source';
import { Work } from '../../entities/library/work'; import { Work } from '../../entities/library/work';
import { dateObjectToString } from '../date/date-util';
import type { SourceGetterInterface } from '../source/source-getter-interface'; import type { SourceGetterInterface } from '../source/source-getter-interface';
import { isNhentaiRealLanguage, languageToLangCode, NhentaiRealLanguage } from './nhentai-util';
async function getLanguage(nhentaiLanguageIdentifier: NhentaiRealLanguage): Promise<LanguageEntityInterface> {
const { manager } = await getConnection(Database.LIBRARY);
return manager.getRepository(Language).findOneOrFail(languageToLangCode[nhentaiLanguageIdentifier]);
}
@injectable() @injectable()
export class NhentaiSourceGetter implements SourceGetterInterface { export class NhentaiSourceGetter implements SourceGetterInterface {
@ -11,12 +22,39 @@ export class NhentaiSourceGetter implements SourceGetterInterface {
this.nhentaiApi = nhentaiApi; this.nhentaiApi = nhentaiApi;
} }
public async find(identifier: string): Promise<Work> { public async find(identifier: string): Promise<WorkEntityInterface> {
const gallery = await this.nhentaiApi.getGallery(identifier); const gallery = await this.nhentaiApi.getGallery(identifier);
const work = new Work(); const work = new Work();
const copy = new Copy();
const source = new Source();
const { manager } = await getConnection(Database.LIBRARY);
source.uri = gallery.url;
copy.sources = Promise.resolve([source]);
work.nameCanonical = gallery.title.main; work.nameCanonical = gallery.title.main;
if (gallery.uploadTime) {
work.releaseDate = dateObjectToString(new Date(gallery.uploadTime));
}
if (gallery.languages.length) {
const filteredLanguages: NhentaiRealLanguage[] = gallery.languages.filter(
(language): language is NhentaiRealLanguage => {
if (language === 'translated') {
// new transformation of type 'translation' here (source getter needs more abstraction before this can be done elegantly)
return false;
} else if (isNhentaiRealLanguage(language)) {
return true;
}
return false;
}
);
work.languages = Promise.all(filteredLanguages.map((language) => getLanguage(language)));
}
work.copies = Promise.resolve([copy]);
await manager.save([work, copy, source]);
return work; return work;
} }

View File

@ -19,12 +19,30 @@ export const postTitleSelector = 'h1.title .after';
export const labeledTagContainerSelector = '.tag-container.field-name'; export const labeledTagContainerSelector = '.tag-container.field-name';
export const tagSelector = '.tag'; export const tagSelector = '.tag';
export const tagNameSelector = 'span.name'; export const tagNameSelector = 'span.name';
export const timeSelector = 'time';
export const tagLabelParodies = 'Parodies'; export const tagLabelParodies = 'Parodies';
export const tagLabelCharacters = 'Characters'; export const tagLabelCharacters = 'Characters';
export const tagLabelTags = 'Tags'; export const tagLabelTags = 'Tags';
export const tagLabelArtists = 'Artists'; export const tagLabelArtists = 'Artists';
export const tagLabelGroups = 'Groups'; export const tagLabelGroups = 'Groups';
export const tagLabelLanguages = 'Languages';
export const languageToLangCode = {
japanese: LangCode.JAPANESE,
english: LangCode.ENGLISH,
chinese: LangCode.CHINESE,
};
export type NhentaiRealLanguage = keyof typeof languageToLangCode;
export type NhentaiLanguage = NhentaiRealLanguage | 'translated';
export function isNhentaiLanguage(language: string): language is NhentaiLanguage {
return isNhentaiRealLanguage(language) || language === 'translated';
}
export function isNhentaiRealLanguage(language: string): language is NhentaiRealLanguage {
return Object.keys(languageToLangCode).includes(language);
}
export function pageIsReady(webContents: WebContents): Promise<boolean> { export function pageIsReady(webContents: WebContents): Promise<boolean> {
return webContents.executeJavaScript(`!!document.getElementById('content')`) as Promise<boolean>; return webContents.executeJavaScript(`!!document.getElementById('content')`) as Promise<boolean>;

View File

@ -5,6 +5,7 @@ declare namespace Nhentai {
}; };
type Gallery = { type Gallery = {
url: string;
title: { title: {
pre: string; pre: string;
main: string; main: string;
@ -15,5 +16,7 @@ declare namespace Nhentai {
parodies: string[]; parodies: string[];
characters: string[]; characters: string[];
tags: string[]; tags: string[];
languages: string[];
uploadTime?: number;
}; };
} }

View File

@ -1,5 +1,10 @@
import { Work } from '../../entities/library/work'; /**
* This interface describes an object which can find works based on some identifying property.
interface SourceGetterInterface { */
find(identifier: string): Promise<Work>; export interface SourceGetterInterface {
/**
* This method finds the work, deserializes it and all its relations into entities and persists them.
* @returns the persisted work
*/
find(identifier: string): Promise<WorkEntityInterface>;
} }

View File

@ -0,0 +1,16 @@
<script>
import { onMount } from 'svelte';
import { entityApi } from '../../services/api';
export let id;
let work;
onMount(() => {
entityApi.fetchWork(id).then((workSerialized) => {
work = workSerialized;
});
});
</script>
<div class="work">{#if work}{JSON.stringify(work)}{/if}</div>

View File

@ -1,4 +1,5 @@
<script> <script>
import Work from '../content/Work.svelte';
import { nhentaiGetWork } from '../../services/api'; import { nhentaiGetWork } from '../../services/api';
import SvelteButton from '../elements/SvelteButton.svelte'; import SvelteButton from '../elements/SvelteButton.svelte';
import { t } from '../../services/utils'; import { t } from '../../services/utils';
@ -14,5 +15,7 @@
<div class="nhentai-get-work"> <div class="nhentai-get-work">
<label><input type="text" placeholder="177013" bind:value="{galleryId}" /></label <label><input type="text" placeholder="177013" bind:value="{galleryId}" /></label
><SvelteButton on:click="{handleClick}">{t('Get')}</SvelteButton> ><SvelteButton on:click="{handleClick}">{t('Get')}</SvelteButton>
<div>{JSON.stringify(work)}</div> {#if work}
<Work id="{work.id}" />
{/if}
</div> </div>

View File

@ -1,36 +1,15 @@
import { ipcRenderer } from 'electron'; import { ipcClient } from './ipc-client';
import { uuid } from '../../shared/services/uuid';
import IpcRendererEvent = Electron.IpcRendererEvent;
const ipcClient: IpcClient = {
ask: (channel: IpcChannel, data?: unknown): Promise<unknown> => {
const id = uuid();
const payload: IpcPayload = {
id,
data,
};
return new Promise((resolve: (value?: unknown) => void, reject: (reason?: Error) => void): void => {
const listener = (event: IpcRendererEvent, response: IpcResponse): void => {
if (response.id === id) {
if (response.success) {
resolve(response.data);
} else {
reject(new Error(response.error));
}
ipcRenderer.removeListener(channel, listener);
}
};
ipcRenderer.on(channel, listener);
ipcRenderer.send(channel, payload);
});
},
};
export function nhentaiSaveFavorites(): Promise<void> { export function nhentaiSaveFavorites(): Promise<void> {
return ipcClient.ask(IpcChannel.NHENTAI_SAVE_FAVORITES) as Promise<void>; return ipcClient.ask(IpcChannel.NHENTAI_SAVE_FAVORITES) as Promise<void>;
} }
export function nhentaiGetWork(galleryId: string): Promise<Work> { export function nhentaiGetWork(galleryId: string): Promise<WorkEntityInterface> {
return ipcClient.ask(IpcChannel.NHENTAI_GET_WORK, { galleryId }) as Promise<Work>; return ipcClient.ask(IpcChannel.NHENTAI_GET_WORK, { galleryId }) as Promise<WorkEntityInterface>;
} }
export const entityApi = {
fetchWork(id: number): Promise<WorkSerializedInterface> {
return ipcClient.ask(IpcChannel.ENTITY_GET_WORK, { id });
},
};

View File

@ -0,0 +1,27 @@
import { ipcRenderer } from 'electron';
import { uuid } from '../../shared/services/uuid';
export const ipcClient: IpcClientInterface = {
ask: (channel: IpcChannel, data?: never): Promise<never> => {
const id = uuid();
const payload: IpcPayload = {
id,
data,
};
return new Promise<never>((resolve, reject): void => {
const listener = (event: Electron.Renderer.IpcRendererEvent, response: IpcResponse): void => {
if (response.id === id) {
if (response.success) {
resolve(response.data as never);
} else {
reject(new Error(response.error));
}
ipcRenderer.removeListener(channel, listener);
}
};
ipcRenderer.on(channel, listener);
ipcRenderer.send(channel, payload);
});
},
};

View File

@ -0,0 +1,7 @@
export class Serializer<Entity, Serialized> {
public readonly serialize: (entity: Entity) => Promise<Serialized>;
public constructor(serialize: (entity: Entity) => Promise<Serialized>) {
this.serialize = serialize;
}
}

View File

@ -0,0 +1,45 @@
import { Serializer } from '../serializer';
export const workSerializer = new Serializer<WorkEntityInterface, WorkSerializedInterface>(async (work) => {
const [
languages,
collectionParts,
copies,
names,
transformationOf,
transformedBy,
workAuthors,
workCharacters,
workTags,
worlds,
] = await Promise.all([
work.languages,
work.collectionParts,
work.copies,
work.names,
work.transformationOf,
work.transformedBy,
work.workAuthors,
work.workCharacters,
work.workTags,
work.worlds,
]);
return {
id: work.id,
isCanonical: work.isCanonical,
nameCanonical: work.nameCanonical,
rating: work.rating,
releaseDate: work.releaseDate,
languages: languages.map((e) => e.code),
collectionParts: collectionParts.map((e) => e.id),
copies: copies.map((e) => e.id),
names: names.map((e) => e.id),
transformationOf: transformationOf.map((e) => e.id),
transformedBy: transformedBy.map((e) => e.id),
workAuthors: workAuthors.map((e) => e.id),
workCharacters: workCharacters.map((e) => e.id),
workTags: workTags.map((e) => e.id),
worlds: worlds.map((e) => e.id),
};
});

View File

@ -1,3 +0,0 @@
type Work = {
nameCanonical: string;
};

View File

@ -0,0 +1,3 @@
interface AuthorNameInterface extends IdentifiableInterface, NameInterface {
entity: Promise<AuthorInterface> | Identifier;
}

View File

@ -0,0 +1,3 @@
interface AuthorRoleNameInterface extends IdentifiableInterface, NameInterface {
entity: Promise<AuthorRoleInterface> | Identifier;
}

View File

@ -0,0 +1,12 @@
/**
* This entity describes the role an author has in a work.
* Examples: story writing, drawing, animating, publishing, ...
*/
interface AuthorRoleInterface extends IdentifiableInterface, MultiNamedInterface, DescribableInterface {
names: Promise<AuthorRoleNameInterface[]> | Identifier[];
/**
* relation to the entity connecting with the author and work
*/
workAuthors: Promise<WorkAuthorInterface[]> | Identifier[];
}

View File

@ -0,0 +1,11 @@
/**
* This entity represents a single real-world entity, be it a person or named group of persons.
*/
interface AuthorInterface extends IdentifiableInterface, MultiNamedInterface {
names: Promise<AuthorNameInterface[]> | Identifier[];
/**
* ultimately connects the author with a work and their role in that work
*/
workAuthors: Promise<WorkAuthorInterface[]> | Identifier[];
}

View File

@ -0,0 +1,14 @@
/**
* This tag entity tags a character in a work.
*/
interface CharacterTagInterface extends IdentifiableInterface, WeightedInterface {
/**
* the character ina work this tag describes
*/
workCharacter: Promise<WorkCharacterInterface> | Identifier;
/**
* the describing tag
*/
tag: Promise<TagInterface> | Identifier;
}

View File

@ -0,0 +1,3 @@
interface CollectionNameInterface extends IdentifiableInterface, NameInterface {
entity: Promise<CollectionInterface> | Identifier;
}

View File

@ -0,0 +1,15 @@
/**
* This entity orders works in a collection.
* The main use case is chronological ordering.
*/
interface CollectionPartInterface extends IdentifiableInterface, OrderableInterface {
/**
* the collection thw work is a part of
*/
collection: Promise<CollectionInterface> | Identifier;
/**
* the work inside the collection
*/
work: Promise<WorkInterface> | Identifier;
}

View File

@ -0,0 +1,16 @@
/**
* A collection is a set of works.
* For example, this can be a series or a set of alternate angles.
* What constitutes as a collection is ultimately up to the user.
*
* As a general rule of thumb:
* If authors of works see them as belonging together, they are a collection
*/
interface CollectionInterface extends IdentifiableInterface, MultiNamedInterface {
names: Promise<CollectionNameInterface[]> | Identifier[];
/**
* the connecting entity between this collection and the work
*/
parts: Promise<CollectionPartInterface[]> | Identifier[];
}

View File

@ -0,0 +1,33 @@
/**
* A copy is the digital counterpart of a work.
* It corresponds to a unique file or set of files which represent the work on the users device.
*
* Multiple works can have multiple copies (think of different scans of a physical work, or lossy compression).
*/
interface CopyInterface extends IdentifiableInterface {
/**
* the work this entity is a copy of
*/
original: Promise<WorkInterface> | Identifier;
/**
* where to find this specific copy
*/
sources: Promise<SourceInterface[]> | Identifier[];
/**
* identifying hash of the file contents
*/
hash: string;
/**
* device location of the copy
*/
location: string | null;
/**
* the ordering of the copies belonging to the same work,
* lower number is higher ranked
*/
ranking: number;
}

View File

@ -1,7 +1,7 @@
/** /**
* Entities extending this one have a user-maintained description. * Entities extending this one have a user-maintained description.
*/ */
interface DescribableEntityInterface { interface DescribableInterface {
/** /**
* a text describing this entity * a text describing this entity
*/ */

View File

@ -1,14 +1,14 @@
/** /**
* Entities implementing this interface build a hierarchy. * Entities implementing this interface build a hierarchy.
*/ */
interface HierarchicalEntityInterface<T> { interface HierarchicalInterface<T> {
/** /**
* parent entities * parent entities
*/ */
parents: Promise<T[]>; parents: Promise<T[]> | Identifier[];
/** /**
* child entities * child entities
*/ */
children: Promise<T[]>; children: Promise<T[]> | Identifier[];
} }

View File

@ -2,9 +2,9 @@
* Every database entity should implement this one. * Every database entity should implement this one.
* It does nothing more but guarantee there is an id column. * It does nothing more but guarantee there is an id column.
*/ */
interface IdentifiableEntityInterface { interface IdentifiableInterface {
/** /**
* the entity id * the entity id
*/ */
id: number; readonly id: Identifier;
} }

View File

@ -0,0 +1,19 @@
/**
* This tag entity tags an interaction between two characters.
*/
interface InteractionTagInterface extends IdentifiableInterface, WeightedInterface {
/**
* the describing tag
*/
tag: Promise<TagInterface> | Identifier;
/**
* the actors of this interaction
*/
subjectCharacters: Promise<WorkCharacterInterface[]> | Identifier[];
/**
* the receivers of this interaction
*/
objectCharacters: Promise<WorkCharacterInterface[]> | Identifier[];
}

View File

@ -0,0 +1,14 @@
/**
* This entity is non-user-maintained and describes a language.
*/
interface LanguageInterface {
/**
* ISO 639-1 two-letter language code
*/
code: string;
/**
* the works using this language
*/
works: Promise<WorkInterface[]> | Identifier[];
}

View File

@ -1,7 +1,7 @@
/** /**
* Entities extending this interface can have multiple names. * Entities extending this interface can have multiple names.
*/ */
interface MultiNamedEntityInterface { interface MultiNamedInterface {
/** /**
* the name which is displayed in the user interface * the name which is displayed in the user interface
*/ */
@ -10,5 +10,5 @@ interface MultiNamedEntityInterface {
/** /**
* other names for the entity * other names for the entity
*/ */
names: Promise<NameEntityInterface[]>; names: Promise<NameInterface[]> | Identifier[];
} }

View File

@ -1,7 +1,7 @@
/** /**
* This entity describes a single name of an entity with multiple names. * This entity describes a single name of an entity with multiple names.
*/ */
interface NameEntityInterface { interface NameInterface {
/** /**
* the name * the name
*/ */
@ -10,5 +10,5 @@ interface NameEntityInterface {
/** /**
* the entity to which the names belong * the entity to which the names belong
*/ */
entity: Promise<MultiNamedEntityInterface>; entity: Promise<MultiNamedInterface> | Identifier;
} }

View File

@ -1,7 +1,7 @@
/** /**
* Entities implementing this interface can be ordered. * Entities implementing this interface can be ordered.
*/ */
interface OrderableEntityInterface { interface OrderableInterface {
/** /**
* a lower number means a higher ordering * a lower number means a higher ordering
*/ */

View File

@ -0,0 +1,3 @@
interface SiteNameInterface extends IdentifiableInterface, NameInterface {
entity: Promise<SiteInterface> | Identifier;
}

View File

@ -0,0 +1,11 @@
/**
* This non-user-maintained entity describes an online provider of works which can be scraped.
*/
interface SiteInterface extends IdentifiableInterface, MultiNamedInterface {
names: Promise<SiteNameInterface[]> | Identifier[];
/**
* sources belonging to this site
*/
sources: Promise<SourceInterface[]> | Identifier[];
}

View File

@ -0,0 +1,19 @@
/**
* This entity describes an external source of a copy, in most cases that is a website.
*/
interface SourceInterface extends IdentifiableInterface {
/**
* the uri to the sauce
*/
uri: string;
/**
* the site connected to the source
*/
site: Promise<SiteInterface> | Identifier | null;
/**
* the copies which can be found here
*/
copies: Promise<CopyInterface[]> | Identifier[];
}

View File

@ -0,0 +1,3 @@
interface TagNameInterface extends IdentifiableInterface, NameInterface {
entity: Promise<TagInterface> | Identifier;
}

View File

@ -0,0 +1,28 @@
/**
* This entity is the main tag entity.
* Tags have a name and a description.
* They can tag a work, a character, or a character interaction.
* They can also be in a hierarchy
*/
interface TagInterface
extends IdentifiableInterface,
MultiNamedInterface,
DescribableInterface,
HierarchicalInterface<TagInterface> {
names: Promise<TagNameInterface[]> | Identifier[];
/**
* this tag tagging a work
*/
workTags: Promise<WorkTagInterface[]> | Identifier[];
/**
* this tag tagging characters
*/
characterTags: Promise<CharacterTagInterface[]> | Identifier[];
/**
* this tag tagging a character interaction
*/
interactionTags: Promise<InteractionTagInterface[]> | Identifier[];
}

View File

@ -0,0 +1,3 @@
interface TransformationTypeNameInterface extends IdentifiableInterface, NameInterface {
entity: Promise<TransformationTypeInterface> | Identifier;
}

View File

@ -0,0 +1,17 @@
/**
* This entity describes a transformation type.
* Possible type: translation, decensor, collection, ...
*/
interface TransformationTypeInterface extends IdentifiableInterface, MultiNamedInterface, DescribableInterface {
names: Promise<TransformationTypeNameInterface[]> | Identifier[];
/**
* the transformations of this type
*/
transformations: Promise<TransformationInterface[]> | Identifier[];
/**
* if that transformation conserves the tags of the original work
*/
conservesTags: boolean;
}

View File

@ -0,0 +1,19 @@
/**
* This entity describes how one work is transformed to another.
*/
interface TransformationInterface extends IdentifiableInterface, OrderableInterface {
/**
* the work based on the original
*/
byWork: Promise<WorkInterface> | Identifier;
/**
* the transformation type
*/
type: Promise<TransformationTypeInterface> | Identifier;
/**
* the original work, it being null meaning that the original work is unknown
*/
ofWork: Promise<WorkInterface> | Identifier | null;
}

View File

@ -1,7 +1,7 @@
/** /**
* An entity implementing this interface has a weight property. * An entity implementing this interface has a weight property.
*/ */
interface WeightedEntityInterface { interface WeightedInterface {
/** /**
* the weight, mathematically a number (0,1], practically between (0,Number.MAX_SAFE_INTEGER] * the weight, mathematically a number (0,1], practically between (0,Number.MAX_SAFE_INTEGER]
* the weight can also be not not defined, null in the database * the weight can also be not not defined, null in the database

View File

@ -0,0 +1,19 @@
/**
* This entity connects authors with their work and their role therein.
*/
interface WorkAuthorInterface extends IdentifiableInterface, OrderableInterface {
/**
* the work
*/
work: Promise<WorkInterface> | Identifier;
/**
* the roles of the author in the work
*/
authorRoles: Promise<AuthorRoleInterface[]> | Identifier[];
/**
* the author
*/
author: Promise<AuthorInterface> | Identifier;
}

View File

@ -0,0 +1,3 @@
interface WorkCharacterNameInterface extends IdentifiableInterface, NameInterface {
entity: Promise<WorkCharacterInterface> | Identifier;
}

View File

@ -0,0 +1,33 @@
/**
* This entity describes a character in a work.
* The character can be original or based on one or more existing characters.
*/
interface WorkCharacterInterface extends IdentifiableInterface, MultiNamedInterface {
names: Promise<WorkCharacterNameInterface[]> | Identifier[];
/**
* the works the character is a part of
* one work character can be part of multiple works because of series
*/
works: Promise<WorkInterface[]> | Identifier[];
/**
* interaction with other characters as actor
*/
interactWith: Promise<InteractionTagInterface[]> | Identifier[];
/**
* interaction with other characters as receiver
*/
interactedBy: Promise<InteractionTagInterface[]> | Identifier[];
/**
* tags connected to the character
*/
characterTags: Promise<CharacterTagInterface[]> | Identifier[];
/**
* existing characters character is based on
*/
worldCharacters: Promise<WorldCharacterInterface[]> | Identifier[];
}

View File

@ -0,0 +1,3 @@
interface WorkNameInterface extends IdentifiableInterface, NameInterface {
entity: Promise<WorkInterface> | Identifier;
}

View File

@ -0,0 +1,14 @@
/**
* This tag entity tags a work.
*/
interface WorkTagInterface extends IdentifiableInterface, WeightedInterface {
/**
* the describing tag
*/
tag: Promise<TagInterface> | Identifier;
/**
* the tagged work
*/
work: Promise<WorkInterface> | Identifier;
}

View File

@ -0,0 +1,68 @@
/**
* This is the main library entity.
*
* It describes a work of art organized by this software.
*/
interface WorkInterface extends IdentifiableInterface, MultiNamedInterface {
names: Promise<WorkNameInterface[]> | Identifier[];
/**
* digital representations of this work
*/
copies: Promise<CopyInterface[]> | Identifier[];
/**
* other works this work is a transformation of
*/
transformationOf: Promise<TransformationInterface[]> | Identifier[];
/**
* other works this work is transformed by
*/
transformedBy: Promise<TransformationInterface[]> | Identifier[];
/**
* the authors/publishers of this work
*/
workAuthors: Promise<WorkAuthorInterface[]> | Identifier[];
/**
* tags describing this work
*/
workTags: Promise<WorkTagInterface[]> | Identifier[];
/**
* characters in this work
*/
workCharacters: Promise<WorkCharacterInterface[]> | Identifier[];
/**
* fictional worlds in which this work takes place
*/
worlds: Promise<WorldInterface[]> | Identifier[];
/**
* if this work i canon in above fictional world
*/
isCanonical: boolean;
/**
* the user rating of this work
*/
rating: number | null;
/**
* the release date of the work, in YYYY-MM-DD format
*/
releaseDate: string | null;
/**
* the languages of the work (if applicable)
*/
languages: Promise<LanguageInterface[]> | string[];
/**
* the collections this work is a part of
*/
collectionParts: Promise<CollectionPartInterface[]> | Identifier[];
}

View File

@ -0,0 +1,3 @@
interface WorldCharacterNameInterface extends IdentifiableInterface, NameInterface {
entity: Promise<WorldCharacterInterface> | Identifier;
}

View File

@ -0,0 +1,19 @@
/**
* This entity describes a canon character in a fictional world.
*/
interface WorldCharacterInterface
extends IdentifiableInterface,
MultiNamedInterface,
HierarchicalInterface<WorldCharacterInterface> {
names: Promise<WorldCharacterNameInterface[]> | Identifier[];
/**
* the characters in works which are based on this one
*/
workCharacters: Promise<WorkCharacterInterface[]> | Identifier[];
/**
* the fictional worlds this character is a part of
*/
worlds: Promise<WorldInterface[]> | Identifier[];
}

View File

@ -0,0 +1,3 @@
interface WorldNameInterface extends IdentifiableInterface, NameInterface {
entity: Promise<WorldInterface> | Identifier;
}

View File

@ -0,0 +1,16 @@
/**
* This entity describes a fictional world.
*/
interface WorldInterface extends IdentifiableInterface, MultiNamedInterface, HierarchicalInterface<WorldInterface> {
names: Promise<WorldNameInterface[]> | Identifier[];
/**
* works taking place in this world
*/
works: Promise<WorkInterface[]> | Identifier[];
/**
* canon characters in this world
*/
worldCharacters: Promise<WorldCharacterInterface[]> | Identifier[];
}

View File

@ -0,0 +1,3 @@
interface AuthorNameEntityInterface extends AuthorNameInterface {
entity: Promise<AuthorEntityInterface>;
}

View File

@ -0,0 +1,3 @@
interface AuthorRoleNameEntityInterface extends AuthorRoleNameInterface {
entity: Promise<AuthorRoleEntityInterface>;
}

View File

@ -0,0 +1,5 @@
interface AuthorRoleEntityInterface extends AuthorRoleInterface {
names: Promise<AuthorRoleNameEntityInterface[]>;
workAuthors: Promise<WorkAuthorEntityInterface[]>;
}

View File

@ -0,0 +1,5 @@
interface AuthorEntityInterface extends AuthorInterface {
names: Promise<AuthorNameEntityInterface[]>;
workAuthors: Promise<WorkAuthorEntityInterface[]>;
}

View File

@ -0,0 +1,5 @@
interface CharacterTagEntityInterface extends CharacterTagInterface {
workCharacter: Promise<WorkCharacterEntityInterface>;
tag: Promise<TagEntityInterface>;
}

View File

@ -0,0 +1,3 @@
interface CollectionNameEntityInterface extends CollectionNameInterface {
entity: Promise<CollectionEntityInterface>;
}

View File

@ -0,0 +1,5 @@
interface CollectionPartEntityInterface extends CollectionPartInterface {
collection: Promise<CollectionEntityInterface>;
work: Promise<WorkEntityInterface>;
}

View File

@ -0,0 +1,5 @@
interface CollectionEntityInterface extends CollectionInterface {
names: Promise<CollectionNameEntityInterface[]>;
parts: Promise<CollectionPartEntityInterface[]>;
}

View File

@ -0,0 +1,5 @@
interface CopyEntityInterface extends CopyInterface {
original: Promise<WorkEntityInterface>;
sources: Promise<SourceEntityInterface[]>;
}

View File

@ -0,0 +1,7 @@
interface InteractionTagEntityInterface extends InteractionTagInterface {
tag: Promise<TagEntityInterface>;
subjectCharacters: Promise<WorkCharacterEntityInterface[]>;
objectCharacters: Promise<WorkCharacterEntityInterface[]>;
}

View File

@ -0,0 +1,3 @@
interface LanguageEntityInterface extends LanguageInterface {
works: Promise<WorkEntityInterface[]>;
}

View File

@ -0,0 +1,3 @@
interface SiteNameEntityInterface extends SiteNameInterface {
entity: Promise<SiteEntityInterface>;
}

View File

@ -0,0 +1,5 @@
interface SiteEntityInterface extends SiteInterface {
names: Promise<SiteNameEntityInterface[]>;
sources: Promise<SourceEntityInterface[]>;
}

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