summary refs log tree commit diff
path: root/util/src/entities/BaseClass.ts
blob: 8a0b7a26ffbebb0fae3c1c964541db3af69a178a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import "reflect-metadata";
import {
	BaseEntity,
	BeforeInsert,
	BeforeUpdate,
	EntityMetadata,
	FindConditions,
	getConnection,
	PrimaryColumn,
	RemoveOptions,
} from "typeorm";
import { Snowflake } from "../util/Snowflake";
import "missing-native-js-functions";

// TODO use class-validator https://typeorm.io/#/validation with class annotators (isPhone/isEmail) combined with types from typescript-json-schema
// btw. we don't use class-validator for everything, because we need to explicitly set the type instead of deriving it from typescript also it doesn't easily support nested objects

export class BaseClassWithoutId extends BaseEntity {
	constructor(private props?: any) {
		super();
		this.assign(props);
	}

	private get construct(): any {
		return this.constructor;
	}

	private get metadata() {
		return this.construct.getRepository().metadata as EntityMetadata;
	}

	assign(props: any = {}) {
		delete props.opts;
		delete props.props;

		const properties = new Set(
			this.metadata.columns
				.map((x: any) => x.propertyName)
				.concat(this.metadata.relations.map((x) => x.propertyName))
		);
		// will not include relational properties

		for (const key in props) {
			if (!properties.has(key)) continue;
			// @ts-ignore
			const setter = this[`set${key.capitalize()}`];

			if (setter) {
				setter.call(this, props[key]);
			} else {
				// @ts-ignore
				this[key] = props[key];
			}
		}
	}

	@BeforeUpdate()
	@BeforeInsert()
	validate() {
		this.assign(this.props);
		return this;
	}

	toJSON(): any {
		return Object.fromEntries(
			this.metadata.columns // @ts-ignore
				.map((x) => [x.propertyName, this[x.propertyName]]) // @ts-ignore
				.concat(this.metadata.relations.map((x) => [x.propertyName, this[x.propertyName]]))
		);
	}

	static increment<T extends BaseClass>(conditions: FindConditions<T>, propertyPath: string, value: number | string) {
		const repository = this.getRepository();
		return repository.increment(conditions, propertyPath, value);
	}

	static decrement<T extends BaseClass>(conditions: FindConditions<T>, propertyPath: string, value: number | string) {
		const repository = this.getRepository();
		return repository.decrement(conditions, propertyPath, value);
	}

	static async delete<T>(criteria: FindConditions<T>, options?: RemoveOptions) {
		if (!criteria) throw new Error("You need to specify delete criteria");

		const repository = this.getRepository();
		const promises = repository.metadata.relations.map(async (x) => {
			if (x.orphanedRowAction !== "delete") return;
			if (typeof x.type === "string") return;

			const foreignKey =
				x.foreignKeys.find((key) => key.entityMetadata === repository.metadata) ||
				x.inverseRelation?.foreignKeys[0]; // find foreign key for this entity
			if (!foreignKey) {
				throw new Error(
					`Foreign key not found for entity ${repository.metadata.name} in relation ${x.propertyName}`
				);
			}
			const id = (criteria as any)[foreignKey.referencedColumnNames[0]];
			if (!id) throw new Error("id missing in criteria options");

			if (x.relationType === "many-to-many") {
				return getConnection()
					.createQueryBuilder()
					.relation(this, x.propertyName)
					.of(id)
					.remove({ [foreignKey.columnNames[0]]: id });
			} else if (
				x.relationType === "one-to-one" ||
				x.relationType === "many-to-one" ||
				x.relationType === "one-to-many"
			) {
				return getConnection()
					.getRepository(x.inverseEntityMetadata.target)
					.delete({ [foreignKey.columnNames[0]]: id });
			}
		});
		await Promise.all(promises);
		return super.delete(criteria, options);
	}
}

export class BaseClass extends BaseClassWithoutId {
	@PrimaryColumn()
	id: string;

	assign(props: any = {}) {
		super.assign(props);
		if (!this.id) this.id = Snowflake.generate();
		return this;
	}
}