Skip to content
This repository was archived by the owner on May 18, 2024. It is now read-only.

Commit 1413a1b

Browse files
authored
Merge pull request #86 from samchon/features/utils
Close #85 - add `EntityUtil.unify()`
2 parents dffb4b6 + 119a678 commit 1413a1b

File tree

7 files changed

+263
-2
lines changed

7 files changed

+263
-2
lines changed

.gitignore

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
.git/
2-
.vscode/
32
node_modules/
43
lib/
54

.vscode/settings.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"editor.tabSize": 4,
3+
"[typescript]": {
4+
"editor.defaultFormatter": "esbenp.prettier-vscode",
5+
"editor.formatOnSave": true,
6+
"editor.codeActionsOnSave": {
7+
"source.fixAll.eslint": true
8+
},
9+
}
10+
}

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "safe-typeorm",
3-
"version": "2.0.2",
3+
"version": "2.0.3",
44
"description": "Make TypeORM much safer",
55
"main": "lib/index.js",
66
"typings": "lib/index.d.ts",

src/utils/ArrayUtil.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { sample as _Sample } from "tstl/ranges/algorithm/random";
22

3+
/**
4+
* @internal
5+
*/
36
export namespace ArrayUtil {
47
export function at<T>(array: T[], index: number): T {
58
return array[index < 0 ? array.length + index : index];

src/utils/EntityUtil.ts

+245
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import { HashMap, VariadicSingleton, hash } from "tstl";
2+
import { equal } from "tstl/ranges";
3+
import { Connection } from "typeorm";
4+
5+
import { findRepository } from "../functional/findRepository";
6+
7+
import { Creator } from "../typings/Creator";
8+
9+
/**
10+
* Utility functions for ORM Entities.
11+
*
12+
* @author Jeongho Nam - https://github.com/samchon
13+
*/
14+
export namespace EntityUtil {
15+
/* -----------------------------------------------------------
16+
TABLE INFO
17+
----------------------------------------------------------- */
18+
export interface ITableInfo {
19+
target: Creator<object>;
20+
schema: string;
21+
name: string;
22+
primary: string;
23+
uniques: string[][];
24+
children: ITableInfo.IChild[];
25+
}
26+
export namespace ITableInfo {
27+
export interface IChild {
28+
info: ITableInfo;
29+
foreign: string;
30+
}
31+
}
32+
33+
/**
34+
* Get table name.
35+
*
36+
* @param entity Target entity type
37+
* @returns Table name
38+
*/
39+
export function name<Entity extends object>(
40+
entity: Creator<Entity>,
41+
): string {
42+
return findRepository(entity).metadata.tableName;
43+
}
44+
45+
/**
46+
* Get table info.
47+
*
48+
* @param entity Target entity type
49+
* @returns Table info
50+
*/
51+
export function info<Entity extends object>(
52+
entity: Creator<Entity>,
53+
): ITableInfo {
54+
const dict: Map<string, ITableInfo> = table_infos_.get(
55+
findRepository(entity).manager.connection,
56+
);
57+
return dict.get(name(entity))!;
58+
}
59+
60+
const table_infos_ = new VariadicSingleton((connection: Connection) => {
61+
const dict: Map<string, ITableInfo> = new Map();
62+
for (const entity of connection.entityMetadatas) {
63+
const info: ITableInfo = {
64+
target: entity.target as Creator<object>,
65+
schema: entity.schema!,
66+
name: entity.tableName,
67+
primary: entity.primaryColumns[0].databaseName,
68+
uniques: entity.uniques.map((u) =>
69+
u.columns.map((c) => c.databaseName),
70+
),
71+
children: [],
72+
};
73+
dict.set(info.name, info);
74+
}
75+
for (const entity of connection.entityMetadatas) {
76+
const info: ITableInfo = dict.get(entity.tableName)!;
77+
const parentTuples: [ITableInfo, string][] = entity.foreignKeys.map(
78+
(fk) => [
79+
dict.get(fk.referencedEntityMetadata.tableName)!,
80+
fk.columns[0].databaseName,
81+
],
82+
);
83+
for (const [parent, foreign] of parentTuples)
84+
parent.children.push({ info, foreign });
85+
}
86+
return dict;
87+
});
88+
89+
/* -----------------------------------------------------------
90+
UNIFIER
91+
----------------------------------------------------------- */
92+
/**
93+
* Merge duplicate records into one.
94+
*
95+
* Absords and merges multiple entity records (*duplicates*) into one (*original*).
96+
* That is, all records listed in *duplicates* would be erased, and instead,
97+
* references to entities of all records subordinate to duplicate records would be
98+
* replaced with original ones.
99+
*
100+
* During unification, if there're some children entities dependent on `Entity` and
101+
* their foreign columns referencing `Entity` are belonged to unique constraint,
102+
* they would be unified recursively.
103+
*
104+
* @template Entity Target entity type
105+
* @param original Original record to absorb
106+
* @param duplicates Duplicated records to be absorbed
107+
*/
108+
export async function unify<Entity extends object>(
109+
original: Entity,
110+
duplicates: Entity[],
111+
): Promise<void> {
112+
try {
113+
const meta: ITableInfo = await info(
114+
original.constructor as Creator<Entity>,
115+
);
116+
const deprecates: string[] = duplicates.map(
117+
(elem) => (elem as any)[meta.primary],
118+
);
119+
120+
await _Unify(meta, (original as any)[meta.primary], deprecates);
121+
} catch (exp) {
122+
console.log(exp);
123+
process.exit(-1);
124+
}
125+
}
126+
127+
async function _Unify(
128+
table: ITableInfo,
129+
keep: string,
130+
deprecates: string[],
131+
): Promise<void> {
132+
if (deprecates.length === 0) return;
133+
134+
for (const dependency of table.children) {
135+
const unique: boolean[] = [];
136+
if (dependency.info.uniques.length !== 0)
137+
for (const columns of dependency.info.uniques)
138+
if (
139+
columns.find((col) => col === dependency.foreign) !==
140+
undefined
141+
) {
142+
unique.push(false);
143+
await _Unify_unique_children(
144+
keep,
145+
deprecates,
146+
dependency,
147+
columns,
148+
);
149+
}
150+
if (unique.length === 0) {
151+
// UPDATE RECORD DIRECTLY
152+
await findRepository(dependency.info.target)
153+
.createQueryBuilder()
154+
.andWhere(`${dependency.foreign} IN (:...deprecates)`, {
155+
deprecates,
156+
})
157+
.update({ [dependency.foreign]: keep })
158+
.updateEntity(false)
159+
.execute();
160+
}
161+
}
162+
163+
// DELETE THE DUPLICATED RECORDS
164+
await findRepository(table.target)
165+
.createQueryBuilder()
166+
.andWhere(`${table.primary} IN (:...deprecates)`, { deprecates })
167+
.delete()
168+
.execute();
169+
}
170+
171+
async function _Unify_unique_children(
172+
keep: string,
173+
deprecates: string[],
174+
child: ITableInfo.IChild,
175+
columns: string[],
176+
): Promise<void> {
177+
const group: string[] = columns.filter((col) => col !== child.foreign);
178+
if (group.length !== 0) {
179+
const sql: string = `
180+
UPDATE ${child.info.schema}.${child.info.name}
181+
SET ${child.foreign} = $1
182+
WHERE ${child.info.primary} IN
183+
(
184+
SELECT CAST(MIN(CAST(${
185+
child.info.primary
186+
} AS VARCHAR(36))) AS UUID) AS ${child.info.primary}
187+
FROM ${child.info.schema}.${child.info.name}
188+
WHERE ${child.foreign} IN ($1, ${deprecates
189+
.map((_, index) => `$${index + 2}`)
190+
.join(", ")})
191+
GROUP BY ${group.join(", ")}
192+
HAVING COUNT(CASE WHEN ${
193+
child.foreign
194+
} = $1 THEN 1 ELSE NULL END) = 0
195+
)`;
196+
await findRepository(child.info.target).query(sql, [
197+
keep,
198+
...deprecates,
199+
]);
200+
}
201+
202+
const recordList: (IEntity & any)[] = await findRepository(
203+
child.info.target,
204+
).query(
205+
`SELECT *
206+
FROM ${child.info.schema}.${child.info.name}
207+
WHERE ${child.foreign} IN (${[keep, ...deprecates]
208+
.map((_, index) => `$${index + 1}`)
209+
.join(", ")})
210+
ORDER BY ${child.info.primary} ASC`,
211+
[keep, ...deprecates],
212+
);
213+
214+
const dict: HashMap<any[], (IEntity & any)[]> = new HashMap(
215+
(elements) => hash(...elements),
216+
(x, y) => equal(x, y),
217+
);
218+
for (const record of recordList) {
219+
const key: any[] = group.map((col) => record[col]);
220+
const array: (IEntity & any)[] = dict.take(key, () => []);
221+
array.push(record);
222+
}
223+
224+
for (const it of dict) {
225+
const index: number = it.second.findIndex(
226+
(rec) => rec[child.foreign] === keep,
227+
);
228+
const master: any = it.second[index];
229+
const slaves: any[] = it.second.filter((_, i) => i !== index);
230+
231+
await _Unify(
232+
child.info,
233+
master[child.info.primary],
234+
slaves.map((s) => s[child.info.primary]),
235+
);
236+
}
237+
}
238+
}
239+
240+
/**
241+
* @internal
242+
*/
243+
interface IEntity {
244+
id: string;
245+
}

src/utils/MapUtil.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
/**
2+
* @internal
3+
*/
14
export namespace MapUtil {
25
export function associate<Element, Key, T>(
36
elements: Element[],

src/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./AesPkcs5";
2+
export * from "./EntityUtil";
23
export * from "./Password";

0 commit comments

Comments
 (0)