@javalabs/prisma-client 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scripts/data-migration/batch-migrator.d.ts +14 -19
- package/dist/scripts/data-migration/batch-migrator.js +98 -297
- package/dist/scripts/data-migration/batch-migrator.js.map +1 -1
- package/dist/scripts/data-migration/data-transformer.d.ts +16 -7
- package/dist/scripts/data-migration/data-transformer.js +169 -133
- package/dist/scripts/data-migration/data-transformer.js.map +1 -1
- package/dist/scripts/data-migration/db-connector.d.ts +6 -1
- package/dist/scripts/data-migration/db-connector.js +44 -8
- package/dist/scripts/data-migration/db-connector.js.map +1 -1
- package/dist/scripts/data-migration/dependency-resolver.d.ts +10 -10
- package/dist/scripts/data-migration/dependency-resolver.js +92 -211
- package/dist/scripts/data-migration/dependency-resolver.js.map +1 -1
- package/dist/scripts/data-migration/foreign-key-manager.d.ts +6 -5
- package/dist/scripts/data-migration/foreign-key-manager.js +108 -18
- package/dist/scripts/data-migration/foreign-key-manager.js.map +1 -1
- package/dist/scripts/data-migration/migration-config.json +63 -0
- package/dist/scripts/data-migration/migration-tool.d.ts +25 -6
- package/dist/scripts/data-migration/migration-tool.js +78 -38
- package/dist/scripts/data-migration/migration-tool.js.map +1 -1
- package/dist/scripts/data-migration/multi-source-migrator.d.ts +17 -0
- package/dist/scripts/data-migration/multi-source-migrator.js +130 -0
- package/dist/scripts/data-migration/multi-source-migrator.js.map +1 -0
- package/dist/scripts/data-migration/schema-utils.d.ts +3 -3
- package/dist/scripts/data-migration/schema-utils.js +62 -19
- package/dist/scripts/data-migration/schema-utils.js.map +1 -1
- package/dist/scripts/data-migration/tenant-migrator.js +9 -2
- package/dist/scripts/data-migration/tenant-migrator.js.map +1 -1
- package/dist/scripts/data-migration/typecast-manager.d.ts +7 -3
- package/dist/scripts/data-migration/typecast-manager.js +169 -25
- package/dist/scripts/data-migration/typecast-manager.js.map +1 -1
- package/dist/scripts/data-migration/types.d.ts +68 -2
- package/dist/scripts/fix-table-indexes.d.ts +26 -0
- package/dist/scripts/fix-table-indexes.js +460 -0
- package/dist/scripts/fix-table-indexes.js.map +1 -0
- package/dist/scripts/multi-db-migration.d.ts +1 -0
- package/dist/scripts/multi-db-migration.js +55 -0
- package/dist/scripts/multi-db-migration.js.map +1 -0
- package/dist/scripts/run-migration.js +41 -75
- package/dist/scripts/run-migration.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/migration-config.json +40 -72
- package/{migration-config-public.json → migration-config.json.bk} +14 -14
- package/package.json +6 -3
- package/src/scripts/data-migration/batch-migrator.ts +192 -513
- package/src/scripts/data-migration/data-transformer.ts +252 -203
- package/src/scripts/data-migration/db-connector.ts +66 -13
- package/src/scripts/data-migration/dependency-resolver.ts +121 -266
- package/src/scripts/data-migration/foreign-key-manager.ts +214 -32
- package/src/scripts/data-migration/migration-config.json +63 -0
- package/src/scripts/data-migration/migration-tool.ts +377 -225
- package/src/scripts/data-migration/schema-utils.ts +94 -32
- package/src/scripts/data-migration/tenant-migrator.ts +12 -5
- package/src/scripts/data-migration/typecast-manager.ts +186 -31
- package/src/scripts/data-migration/types.ts +78 -5
- package/src/scripts/dumps/source_dump_20250428_145606.sql +323 -0
- package/src/scripts/fix-table-indexes.ts +602 -0
- package/src/scripts/post-migration-validator.ts +206 -107
- package/src/scripts/run-migration.ts +87 -101
|
@@ -1,319 +1,174 @@
|
|
|
1
1
|
import { Logger } from "@nestjs/common";
|
|
2
2
|
import { Pool } from "pg";
|
|
3
3
|
|
|
4
|
-
interface TableDependency {
|
|
5
|
-
table: string;
|
|
6
|
-
dependencies: string[];
|
|
7
|
-
level?: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
4
|
export class DependencyResolver {
|
|
11
5
|
private readonly logger = new Logger("DependencyResolver");
|
|
12
|
-
private
|
|
6
|
+
private readonly visitedTables = new Set<string>();
|
|
7
|
+
private readonly temporaryMark = new Set<string>();
|
|
8
|
+
private readonly tableOrder: string[] = [];
|
|
9
|
+
private config: any;
|
|
13
10
|
|
|
14
11
|
constructor(
|
|
15
12
|
private readonly sourcePool: Pool,
|
|
16
13
|
private readonly targetPool: Pool
|
|
17
14
|
) {}
|
|
18
15
|
|
|
16
|
+
setConfig(config: any) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
|
|
19
20
|
async analyzeDependencies(): Promise<string[]> {
|
|
20
21
|
try {
|
|
21
|
-
|
|
22
|
-
const tables = await this.getTables();
|
|
22
|
+
this.logger.log("Iniciando análisis de dependencias...");
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
const foreignKeys = await this.getForeignKeys(table);
|
|
24
|
+
// Obtener todas las tablas del esquema
|
|
25
|
+
const tables = await this.getAllTables();
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
];
|
|
27
|
+
// Resetear estado
|
|
28
|
+
this.visitedTables.clear();
|
|
29
|
+
this.temporaryMark.clear();
|
|
30
|
+
this.tableOrder.length = 0;
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
dependencies: allDependencies,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
32
|
+
// Ordenar tablas por prioridad primero
|
|
33
|
+
const priorityOrder = this.getPriorityOrder(tables);
|
|
39
34
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
// Realizar ordenamiento topológico para cada grupo de prioridad
|
|
36
|
+
for (const priorityGroup of priorityOrder) {
|
|
37
|
+
for (const table of priorityGroup) {
|
|
38
|
+
if (!this.visitedTables.has(table)) {
|
|
39
|
+
await this.visit(table);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
47
43
|
|
|
48
|
-
this.logger.log(
|
|
49
|
-
|
|
44
|
+
this.logger.log(
|
|
45
|
+
`Orden de migración determinado: ${this.tableOrder.join(" -> ")}`
|
|
46
|
+
);
|
|
47
|
+
return this.tableOrder;
|
|
50
48
|
} catch (error) {
|
|
51
|
-
this.logger.error(`Error
|
|
49
|
+
this.logger.error(`Error analizando dependencias: ${error.message}`);
|
|
52
50
|
throw error;
|
|
53
51
|
}
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
customers: ["users"],
|
|
62
|
-
global_user_transactions: ["users", "providers", "transactions"],
|
|
63
|
-
invoices: ["transactions"],
|
|
64
|
-
credit_requests: ["users", "invoices"],
|
|
65
|
-
notifications: ["users", "transactions"],
|
|
66
|
-
pending_references: ["users", "providers"],
|
|
67
|
-
daily_logs: ["transactions"],
|
|
68
|
-
transaction_updates: ["transactions"],
|
|
69
|
-
toku: ["transactions"],
|
|
70
|
-
providers: ["countries"],
|
|
71
|
-
users: ["countries"],
|
|
54
|
+
private getPriorityOrder(tables: string[]): string[][] {
|
|
55
|
+
const priorities = {
|
|
56
|
+
high: new Set(this.config?.migrationPriorities?.high || []),
|
|
57
|
+
medium: new Set(this.config?.migrationPriorities?.medium || []),
|
|
58
|
+
low: new Set(this.config?.migrationPriorities?.low || []),
|
|
72
59
|
};
|
|
73
60
|
|
|
74
|
-
|
|
61
|
+
// Agrupar tablas por prioridad
|
|
62
|
+
const highPriority = tables.filter((t) => priorities.high.has(t));
|
|
63
|
+
const mediumPriority = tables.filter((t) => priorities.medium.has(t));
|
|
64
|
+
const lowPriority = tables.filter((t) => priorities.low.has(t));
|
|
65
|
+
const noPriority = tables.filter(
|
|
66
|
+
(t) =>
|
|
67
|
+
!priorities.high.has(t) &&
|
|
68
|
+
!priorities.medium.has(t) &&
|
|
69
|
+
!priorities.low.has(t)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return [highPriority, mediumPriority, noPriority, lowPriority];
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
private async
|
|
75
|
+
private async visit(table: string, path: string[] = []): Promise<void> {
|
|
76
|
+
// Detectar ciclos
|
|
77
|
+
if (this.temporaryMark.has(table)) {
|
|
78
|
+
const cycle = [...path, table].join(" -> ");
|
|
79
|
+
this.logger.warn(`Detectado ciclo de dependencias: ${cycle}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (this.visitedTables.has(table)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.temporaryMark.add(table);
|
|
88
|
+
path.push(table);
|
|
89
|
+
|
|
90
|
+
// Obtener dependencias tanto de la configuración como de la base de datos
|
|
91
|
+
const dependencies = await this.getTableDependencies(table);
|
|
92
|
+
|
|
93
|
+
for (const dep of dependencies) {
|
|
94
|
+
if (!this.visitedTables.has(dep)) {
|
|
95
|
+
await this.visit(dep, [...path]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.temporaryMark.delete(table);
|
|
100
|
+
this.visitedTables.add(table);
|
|
101
|
+
this.tableOrder.push(table);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private async getAllTables(): Promise<string[]> {
|
|
78
105
|
const query = `
|
|
79
106
|
SELECT table_name
|
|
80
107
|
FROM information_schema.tables
|
|
81
108
|
WHERE table_schema = 'public'
|
|
82
109
|
AND table_type = 'BASE TABLE'
|
|
83
|
-
AND table_name NOT IN ('_prisma_migrations', 'schema_migrations')
|
|
84
110
|
`;
|
|
85
111
|
|
|
86
112
|
const result = await this.sourcePool.query(query);
|
|
87
113
|
return result.rows.map((row) => row.table_name);
|
|
88
114
|
}
|
|
89
115
|
|
|
90
|
-
private async
|
|
91
|
-
|
|
92
|
-
// Get explicit foreign keys (mantener el query existente)
|
|
93
|
-
const foreignKeyQuery = `
|
|
94
|
-
SELECT DISTINCT
|
|
95
|
-
ccu.table_name AS foreign_table_name,
|
|
96
|
-
kcu.column_name AS column_name
|
|
97
|
-
FROM
|
|
98
|
-
information_schema.table_constraints AS tc
|
|
99
|
-
JOIN information_schema.constraint_column_usage AS ccu
|
|
100
|
-
ON ccu.constraint_name = tc.constraint_name
|
|
101
|
-
JOIN information_schema.key_column_usage AS kcu
|
|
102
|
-
ON kcu.constraint_name = tc.constraint_name
|
|
103
|
-
WHERE
|
|
104
|
-
tc.constraint_type = 'FOREIGN KEY'
|
|
105
|
-
AND tc.table_name = $1
|
|
106
|
-
AND tc.table_schema = 'public'
|
|
107
|
-
`;
|
|
108
|
-
|
|
109
|
-
// Expanded implicit dependencies patterns
|
|
110
|
-
const implicitDependencyQuery = `
|
|
111
|
-
SELECT DISTINCT
|
|
112
|
-
c.column_name,
|
|
113
|
-
c.udt_name,
|
|
114
|
-
c.column_default,
|
|
115
|
-
c.data_type
|
|
116
|
-
FROM
|
|
117
|
-
information_schema.columns c
|
|
118
|
-
WHERE
|
|
119
|
-
c.table_schema = 'public'
|
|
120
|
-
AND c.table_name = $1
|
|
121
|
-
AND (
|
|
122
|
-
-- Common ID patterns
|
|
123
|
-
c.column_name LIKE '%_id'
|
|
124
|
-
OR c.column_name LIKE '%_ids'
|
|
125
|
-
OR c.column_name LIKE 'id_%'
|
|
126
|
-
-- Reference patterns
|
|
127
|
-
OR c.column_name LIKE '%_ref%'
|
|
128
|
-
OR c.column_name LIKE '%_key'
|
|
129
|
-
OR c.column_name LIKE '%_code'
|
|
130
|
-
-- Relationship patterns
|
|
131
|
-
OR c.column_name LIKE '%_by'
|
|
132
|
-
OR c.column_name LIKE '%_to'
|
|
133
|
-
OR c.column_name LIKE '%_from'
|
|
134
|
-
OR c.column_name LIKE 'parent_%'
|
|
135
|
-
OR c.column_name LIKE 'child_%'
|
|
136
|
-
-- JSON/JSONB columns that might contain references
|
|
137
|
-
OR (c.data_type IN ('json', 'jsonb') AND c.column_name LIKE '%_data')
|
|
138
|
-
-- Array columns that might contain IDs
|
|
139
|
-
OR (c.data_type = 'ARRAY' AND c.column_name LIKE '%_ids')
|
|
140
|
-
-- Enum columns that might indicate relationships
|
|
141
|
-
OR (c.udt_name LIKE 'enum_%' AND c.column_name LIKE '%_type')
|
|
142
|
-
)
|
|
143
|
-
`;
|
|
144
|
-
|
|
145
|
-
// Get referenced tables through column comments or descriptions
|
|
146
|
-
const referencedTablesQuery = `
|
|
147
|
-
SELECT DISTINCT
|
|
148
|
-
obj_description(c.table_name::regclass, 'pg_class') as table_comment
|
|
149
|
-
FROM
|
|
150
|
-
information_schema.columns c
|
|
151
|
-
WHERE
|
|
152
|
-
c.table_schema = 'public'
|
|
153
|
-
AND c.table_name = $1
|
|
154
|
-
`;
|
|
116
|
+
private async getTableDependencies(table: string): Promise<string[]> {
|
|
117
|
+
const dependencies = new Set<string>();
|
|
155
118
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.sourcePool.query(referencedTablesQuery, [tableName]),
|
|
160
|
-
]);
|
|
119
|
+
// Obtener dependencias de la configuración
|
|
120
|
+
const configDeps = this.config?.tables?.[table]?.dependencies || [];
|
|
121
|
+
configDeps.forEach((dep) => dependencies.add(dep));
|
|
161
122
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
allDependencies.add(potentialTable);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Parse table comments for additional dependencies
|
|
180
|
-
referencedResult.rows.forEach((row) => {
|
|
181
|
-
if (row.table_comment) {
|
|
182
|
-
const referencedTables = this.parseTableComment(row.table_comment);
|
|
183
|
-
referencedTables.forEach((table) => allDependencies.add(table));
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Añadir lógica adicional para procesar los resultados
|
|
188
|
-
implicitResult.rows.forEach((row) => {
|
|
189
|
-
// Procesar columnas JSON/JSONB
|
|
190
|
-
if (row.data_type === "json" || row.data_type === "jsonb") {
|
|
191
|
-
const potentialRefs = this.extractJsonReferences(row.column_name);
|
|
192
|
-
potentialRefs.forEach((ref) => allDependencies.add(ref));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Procesar columnas de tipo enum
|
|
196
|
-
if (row.udt_name?.startsWith("enum_")) {
|
|
197
|
-
const enumTable = this.inferTableFromEnum(row.udt_name);
|
|
198
|
-
if (enumTable) allDependencies.add(enumTable);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Procesar arrays
|
|
202
|
-
if (row.data_type === "ARRAY") {
|
|
203
|
-
const arrayTable = this.inferTableFromArrayColumn(row.column_name);
|
|
204
|
-
if (arrayTable) allDependencies.add(arrayTable);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
123
|
+
// Obtener dependencias de la base de datos
|
|
124
|
+
const query = `
|
|
125
|
+
SELECT
|
|
126
|
+
ccu.table_name AS foreign_table
|
|
127
|
+
FROM
|
|
128
|
+
information_schema.table_constraints tc
|
|
129
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
130
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
131
|
+
WHERE
|
|
132
|
+
tc.constraint_type = 'FOREIGN KEY'
|
|
133
|
+
AND tc.table_name = $1
|
|
134
|
+
AND tc.table_schema = 'public'
|
|
135
|
+
`;
|
|
207
136
|
|
|
208
|
-
|
|
137
|
+
try {
|
|
138
|
+
const result = await this.sourcePool.query(query, [table]);
|
|
139
|
+
result.rows.forEach((row) => dependencies.add(row.foreign_table));
|
|
209
140
|
} catch (error) {
|
|
210
|
-
this.logger.
|
|
211
|
-
`Error
|
|
141
|
+
this.logger.warn(
|
|
142
|
+
`Error obteniendo dependencias para ${table}: ${error.message}`
|
|
212
143
|
);
|
|
213
|
-
return [];
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private inferTableFromEnum(enumType: string): string | null {
|
|
218
|
-
// enum_transaction_status -> transactions
|
|
219
|
-
const parts = enumType.split("_");
|
|
220
|
-
if (parts.length > 1) {
|
|
221
|
-
return parts[1];
|
|
222
|
-
}
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
private inferTableFromArrayColumn(columnName: string): string | null {
|
|
227
|
-
// user_ids -> users
|
|
228
|
-
if (columnName.endsWith("_ids")) {
|
|
229
|
-
return columnName.slice(0, -4) + "s";
|
|
230
144
|
}
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
145
|
|
|
234
|
-
|
|
235
|
-
// metadata_data -> buscar referencias en el nombre
|
|
236
|
-
const refs: string[] = [];
|
|
237
|
-
const parts = columnName.split("_");
|
|
238
|
-
if (parts.length > 1) {
|
|
239
|
-
const potentialTable = parts[0] + "s";
|
|
240
|
-
refs.push(potentialTable);
|
|
241
|
-
}
|
|
242
|
-
return refs;
|
|
146
|
+
return Array.from(dependencies);
|
|
243
147
|
}
|
|
244
148
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return tableName;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
private parseTableComment(comment: string): string[] {
|
|
266
|
-
// Extract table references from comments
|
|
267
|
-
// Example comment format: "References: users, transactions"
|
|
268
|
-
const referencesMatch = comment.match(/References:\s*([^;]+)/i);
|
|
269
|
-
if (referencesMatch) {
|
|
270
|
-
return referencesMatch[1]
|
|
271
|
-
.split(",")
|
|
272
|
-
.map((table) => table.trim())
|
|
273
|
-
.filter((table) => table.length > 0);
|
|
274
|
-
}
|
|
275
|
-
return [];
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
private assignLevels(): void {
|
|
279
|
-
const visited = new Set<string>();
|
|
280
|
-
const visiting = new Set<string>();
|
|
281
|
-
|
|
282
|
-
const visit = (tableName: string, level = 0): number => {
|
|
283
|
-
if (visiting.has(tableName)) {
|
|
149
|
+
async validateDependencies(
|
|
150
|
+
table: string,
|
|
151
|
+
dependencies: string[]
|
|
152
|
+
): Promise<boolean> {
|
|
153
|
+
for (const dep of dependencies) {
|
|
154
|
+
const query = `
|
|
155
|
+
SELECT EXISTS (
|
|
156
|
+
SELECT 1
|
|
157
|
+
FROM information_schema.tables
|
|
158
|
+
WHERE table_schema = 'public'
|
|
159
|
+
AND table_name = $1
|
|
160
|
+
)
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
const result = await this.sourcePool.query(query, [dep]);
|
|
164
|
+
if (!result.rows[0].exists) {
|
|
284
165
|
this.logger.warn(
|
|
285
|
-
`
|
|
166
|
+
`Dependencia ${dep} no encontrada para tabla ${table}`
|
|
286
167
|
);
|
|
287
|
-
return
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (visited.has(tableName)) {
|
|
291
|
-
return this.dependencyGraph.get(tableName)?.level || 0;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
visiting.add(tableName);
|
|
295
|
-
|
|
296
|
-
const node = this.dependencyGraph.get(tableName);
|
|
297
|
-
if (!node) return level;
|
|
298
|
-
|
|
299
|
-
let maxDepLevel = level;
|
|
300
|
-
for (const dep of node.dependencies) {
|
|
301
|
-
const depLevel = visit(dep, level + 1);
|
|
302
|
-
maxDepLevel = Math.max(maxDepLevel, depLevel + 1);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
node.level = maxDepLevel;
|
|
306
|
-
visited.add(tableName);
|
|
307
|
-
visiting.delete(tableName);
|
|
308
|
-
|
|
309
|
-
return maxDepLevel;
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
// Start with all tables
|
|
313
|
-
for (const tableName of this.dependencyGraph.keys()) {
|
|
314
|
-
if (!visited.has(tableName)) {
|
|
315
|
-
visit(tableName);
|
|
168
|
+
return false;
|
|
316
169
|
}
|
|
317
170
|
}
|
|
171
|
+
|
|
172
|
+
return true;
|
|
318
173
|
}
|
|
319
174
|
}
|