@javalabs/prisma-client 1.0.30 → 1.0.31

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.
Files changed (80) hide show
  1. package/.github/CODEOWNERS +1 -1
  2. package/README.md +269 -269
  3. package/dist/scripts/add-uuid-to-table.js +32 -32
  4. package/dist/scripts/data-migration/batch-migrator.js +12 -12
  5. package/dist/scripts/data-migration/data-transformer.js +14 -14
  6. package/dist/scripts/data-migration/dependency-resolver.js +23 -23
  7. package/dist/scripts/data-migration/entity-discovery.js +68 -68
  8. package/dist/scripts/data-migration/foreign-key-manager.js +23 -23
  9. package/dist/scripts/data-migration/migration-tool.js +5 -5
  10. package/dist/scripts/data-migration/schema-utils.js +74 -74
  11. package/dist/scripts/data-migration/typecast-manager.js +4 -4
  12. package/dist/scripts/database-initializer.js +5 -5
  13. package/dist/scripts/drop-database.js +5 -5
  14. package/dist/scripts/fix-data-types.js +53 -53
  15. package/dist/scripts/fix-enum-values.js +34 -34
  16. package/dist/scripts/fix-schema-discrepancies.js +40 -40
  17. package/dist/scripts/fix-table-indexes.js +81 -81
  18. package/dist/scripts/migrate-schema-structure.js +4 -4
  19. package/dist/scripts/migrate-uuid.js +19 -19
  20. package/dist/scripts/post-migration-validator.js +49 -49
  21. package/dist/scripts/pre-migration-validator.js +107 -107
  22. package/dist/scripts/reset-database.js +21 -21
  23. package/dist/scripts/retry-failed-migrations.js +28 -28
  24. package/dist/scripts/run-migration.js +5 -5
  25. package/dist/scripts/schema-sync.js +18 -18
  26. package/dist/scripts/sequence-sync-cli.js +55 -55
  27. package/dist/scripts/sequence-synchronizer.js +20 -20
  28. package/dist/scripts/sync-enum-types.js +30 -30
  29. package/dist/scripts/sync-enum-values.js +52 -52
  30. package/dist/scripts/truncate-database.js +10 -10
  31. package/dist/scripts/verify-migration-setup.js +10 -10
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/migration-config.json +63 -63
  34. package/migration-config.json.bk +95 -95
  35. package/migrations/add_reserved_amount.sql +7 -7
  36. package/package.json +44 -44
  37. package/prisma/migrations/add_uuid_to_transactions.sql +13 -13
  38. package/prisma/schema.prisma +636 -609
  39. package/src/index.ts +23 -23
  40. package/src/prisma-factory.service.ts +40 -40
  41. package/src/prisma.module.ts +9 -9
  42. package/src/prisma.service.ts +16 -16
  43. package/src/scripts/add-uuid-to-table.ts +138 -138
  44. package/src/scripts/create-tenant-schemas.ts +145 -145
  45. package/src/scripts/data-migration/batch-migrator.ts +248 -248
  46. package/src/scripts/data-migration/data-transformer.ts +426 -426
  47. package/src/scripts/data-migration/db-connector.ts +120 -120
  48. package/src/scripts/data-migration/dependency-resolver.ts +174 -174
  49. package/src/scripts/data-migration/entity-discovery.ts +196 -196
  50. package/src/scripts/data-migration/foreign-key-manager.ts +277 -277
  51. package/src/scripts/data-migration/migration-config.json +63 -63
  52. package/src/scripts/data-migration/migration-tool.ts +509 -509
  53. package/src/scripts/data-migration/schema-utils.ts +248 -248
  54. package/src/scripts/data-migration/tenant-migrator.ts +201 -201
  55. package/src/scripts/data-migration/typecast-manager.ts +193 -193
  56. package/src/scripts/data-migration/types.ts +113 -113
  57. package/src/scripts/database-initializer.ts +49 -49
  58. package/src/scripts/drop-database.ts +104 -104
  59. package/src/scripts/dump-source-db.sh +61 -61
  60. package/src/scripts/encrypt-user-passwords.ts +36 -36
  61. package/src/scripts/error-handler.ts +117 -117
  62. package/src/scripts/fix-data-types.ts +241 -241
  63. package/src/scripts/fix-enum-values.ts +357 -357
  64. package/src/scripts/fix-schema-discrepancies.ts +317 -317
  65. package/src/scripts/fix-table-indexes.ts +601 -601
  66. package/src/scripts/migrate-schema-structure.ts +90 -90
  67. package/src/scripts/migrate-uuid.ts +76 -76
  68. package/src/scripts/post-migration-validator.ts +526 -526
  69. package/src/scripts/pre-migration-validator.ts +610 -610
  70. package/src/scripts/reset-database.ts +263 -263
  71. package/src/scripts/retry-failed-migrations.ts +416 -416
  72. package/src/scripts/run-migration.ts +707 -707
  73. package/src/scripts/schema-sync.ts +128 -128
  74. package/src/scripts/sequence-sync-cli.ts +416 -416
  75. package/src/scripts/sequence-synchronizer.ts +127 -127
  76. package/src/scripts/sync-enum-types.ts +170 -170
  77. package/src/scripts/sync-enum-values.ts +563 -563
  78. package/src/scripts/truncate-database.ts +123 -123
  79. package/src/scripts/verify-migration-setup.ts +135 -135
  80. package/tsconfig.json +17 -17
@@ -1,120 +1,120 @@
1
- import { PrismaClient } from "@prisma/client";
2
- import * as pg from "pg";
3
- import * as dotenv from "dotenv";
4
- import { DatabaseConnections, DatabaseConnection } from "./types";
5
-
6
- dotenv.config();
7
-
8
- export interface SourceDatabase {
9
- id: string;
10
- url: string;
11
- name: string;
12
- }
13
-
14
- export class DatabaseConnector {
15
- static createConnections(
16
- sourceDatabases?: SourceDatabase[]
17
- ): DatabaseConnections {
18
- // Crear conexiones para cada base de datos de origen
19
- const sourceConnections: DatabaseConnection[] = (sourceDatabases || []).map(
20
- (source): DatabaseConnection => {
21
- // Conexión de pool para la base de datos de origen
22
- const pool = new pg.Pool({
23
- connectionString: source.url,
24
- });
25
-
26
- // Cliente Prisma para la base de datos de origen
27
- const prisma = new PrismaClient({
28
- datasources: {
29
- db: {
30
- url: source.url,
31
- },
32
- },
33
- });
34
-
35
- const connection: DatabaseConnection = {
36
- pool,
37
- prisma,
38
- sourceId: source.id,
39
- async query(sql: string, params?: any[]) {
40
- return pool.query(sql, params);
41
- },
42
- };
43
-
44
- return connection;
45
- }
46
- );
47
-
48
- const sourcePool = new pg.Pool({
49
- connectionString: process.env.SOURCE_DATABASE_URL,
50
- max: 20,
51
- idleTimeoutMillis: 30000,
52
- });
53
-
54
- // Source Prisma client
55
- const sourcePrisma = new PrismaClient({
56
- datasources: {
57
- db: {
58
- url: process.env.SOURCE_DATABASE_URL,
59
- },
60
- },
61
- });
62
-
63
- // Target database connection (our multi-tenant database)
64
- const targetPool = new pg.Pool({
65
- connectionString: process.env.DATABASE_URL,
66
- max: 20,
67
- idleTimeoutMillis: 30000,
68
- });
69
-
70
- // Target Prisma client
71
- const targetPrisma = new PrismaClient({
72
- datasources: {
73
- db: {
74
- url: process.env.DATABASE_URL,
75
- },
76
- },
77
- });
78
-
79
- return {
80
- sourceConnections,
81
- targetPrisma,
82
- targetPool,
83
- sourcePool,
84
- sourcePrisma,
85
- };
86
- }
87
-
88
- static createTenantPrismaClient(tenantId: string): PrismaClient {
89
- return new PrismaClient({
90
- datasources: {
91
- db: {
92
- url: `${process.env.DATABASE_URL}?schema=${tenantId}`,
93
- },
94
- },
95
- // Increase transaction timeout to 2 minutes
96
- transactionOptions: {
97
- maxWait: 120000, // 2 minutes max wait time
98
- timeout: 120000, // 2 minutes timeout
99
- },
100
- });
101
- }
102
-
103
- static async cleanup(connections: DatabaseConnections): Promise<void> {
104
- // Desconectar todas las conexiones de origen
105
- if (connections.sourceConnections) {
106
- for (const sourceConn of connections.sourceConnections) {
107
- if (sourceConn.pool) await sourceConn.pool.end();
108
- if (sourceConn.prisma) await sourceConn.prisma.$disconnect();
109
- }
110
- }
111
-
112
- // Desconectar conexiones de destino
113
- await connections.targetPrisma.$disconnect();
114
- await connections.targetPool.end();
115
-
116
- // Desconectar conexiones principales de origen si existen
117
- if (connections.sourcePrisma) await connections.sourcePrisma.$disconnect();
118
- if (connections.sourcePool) await connections.sourcePool.end();
119
- }
120
- }
1
+ import { PrismaClient } from "@prisma/client";
2
+ import * as pg from "pg";
3
+ import * as dotenv from "dotenv";
4
+ import { DatabaseConnections, DatabaseConnection } from "./types";
5
+
6
+ dotenv.config();
7
+
8
+ export interface SourceDatabase {
9
+ id: string;
10
+ url: string;
11
+ name: string;
12
+ }
13
+
14
+ export class DatabaseConnector {
15
+ static createConnections(
16
+ sourceDatabases?: SourceDatabase[]
17
+ ): DatabaseConnections {
18
+ // Crear conexiones para cada base de datos de origen
19
+ const sourceConnections: DatabaseConnection[] = (sourceDatabases || []).map(
20
+ (source): DatabaseConnection => {
21
+ // Conexión de pool para la base de datos de origen
22
+ const pool = new pg.Pool({
23
+ connectionString: source.url,
24
+ });
25
+
26
+ // Cliente Prisma para la base de datos de origen
27
+ const prisma = new PrismaClient({
28
+ datasources: {
29
+ db: {
30
+ url: source.url,
31
+ },
32
+ },
33
+ });
34
+
35
+ const connection: DatabaseConnection = {
36
+ pool,
37
+ prisma,
38
+ sourceId: source.id,
39
+ async query(sql: string, params?: any[]) {
40
+ return pool.query(sql, params);
41
+ },
42
+ };
43
+
44
+ return connection;
45
+ }
46
+ );
47
+
48
+ const sourcePool = new pg.Pool({
49
+ connectionString: process.env.SOURCE_DATABASE_URL,
50
+ max: 20,
51
+ idleTimeoutMillis: 30000,
52
+ });
53
+
54
+ // Source Prisma client
55
+ const sourcePrisma = new PrismaClient({
56
+ datasources: {
57
+ db: {
58
+ url: process.env.SOURCE_DATABASE_URL,
59
+ },
60
+ },
61
+ });
62
+
63
+ // Target database connection (our multi-tenant database)
64
+ const targetPool = new pg.Pool({
65
+ connectionString: process.env.DATABASE_URL,
66
+ max: 20,
67
+ idleTimeoutMillis: 30000,
68
+ });
69
+
70
+ // Target Prisma client
71
+ const targetPrisma = new PrismaClient({
72
+ datasources: {
73
+ db: {
74
+ url: process.env.DATABASE_URL,
75
+ },
76
+ },
77
+ });
78
+
79
+ return {
80
+ sourceConnections,
81
+ targetPrisma,
82
+ targetPool,
83
+ sourcePool,
84
+ sourcePrisma,
85
+ };
86
+ }
87
+
88
+ static createTenantPrismaClient(tenantId: string): PrismaClient {
89
+ return new PrismaClient({
90
+ datasources: {
91
+ db: {
92
+ url: `${process.env.DATABASE_URL}?schema=${tenantId}`,
93
+ },
94
+ },
95
+ // Increase transaction timeout to 2 minutes
96
+ transactionOptions: {
97
+ maxWait: 120000, // 2 minutes max wait time
98
+ timeout: 120000, // 2 minutes timeout
99
+ },
100
+ });
101
+ }
102
+
103
+ static async cleanup(connections: DatabaseConnections): Promise<void> {
104
+ // Desconectar todas las conexiones de origen
105
+ if (connections.sourceConnections) {
106
+ for (const sourceConn of connections.sourceConnections) {
107
+ if (sourceConn.pool) await sourceConn.pool.end();
108
+ if (sourceConn.prisma) await sourceConn.prisma.$disconnect();
109
+ }
110
+ }
111
+
112
+ // Desconectar conexiones de destino
113
+ await connections.targetPrisma.$disconnect();
114
+ await connections.targetPool.end();
115
+
116
+ // Desconectar conexiones principales de origen si existen
117
+ if (connections.sourcePrisma) await connections.sourcePrisma.$disconnect();
118
+ if (connections.sourcePool) await connections.sourcePool.end();
119
+ }
120
+ }
@@ -1,174 +1,174 @@
1
- import { Logger } from "@nestjs/common";
2
- import { Pool } from "pg";
3
-
4
- export class DependencyResolver {
5
- private readonly logger = new Logger("DependencyResolver");
6
- private readonly visitedTables = new Set<string>();
7
- private readonly temporaryMark = new Set<string>();
8
- private readonly tableOrder: string[] = [];
9
- private config: any;
10
-
11
- constructor(
12
- private readonly sourcePool: Pool,
13
- private readonly targetPool: Pool
14
- ) {}
15
-
16
- setConfig(config: any) {
17
- this.config = config;
18
- }
19
-
20
- async analyzeDependencies(): Promise<string[]> {
21
- try {
22
- this.logger.log("Iniciando análisis de dependencias...");
23
-
24
- // Obtener todas las tablas del esquema
25
- const tables = await this.getAllTables();
26
-
27
- // Resetear estado
28
- this.visitedTables.clear();
29
- this.temporaryMark.clear();
30
- this.tableOrder.length = 0;
31
-
32
- // Ordenar tablas por prioridad primero
33
- const priorityOrder = this.getPriorityOrder(tables);
34
-
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
- }
43
-
44
- this.logger.log(
45
- `Orden de migración determinado: ${this.tableOrder.join(" -> ")}`
46
- );
47
- return this.tableOrder;
48
- } catch (error) {
49
- this.logger.error(`Error analizando dependencias: ${error.message}`);
50
- throw error;
51
- }
52
- }
53
-
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 || []),
59
- };
60
-
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];
73
- }
74
-
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[]> {
105
- const query = `
106
- SELECT table_name
107
- FROM information_schema.tables
108
- WHERE table_schema = 'public'
109
- AND table_type = 'BASE TABLE'
110
- `;
111
-
112
- const result = await this.sourcePool.query(query);
113
- return result.rows.map((row) => row.table_name);
114
- }
115
-
116
- private async getTableDependencies(table: string): Promise<string[]> {
117
- const dependencies = new Set<string>();
118
-
119
- // Obtener dependencias de la configuración
120
- const configDeps = this.config?.tables?.[table]?.dependencies || [];
121
- configDeps.forEach((dep) => dependencies.add(dep));
122
-
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
- `;
136
-
137
- try {
138
- const result = await this.sourcePool.query(query, [table]);
139
- result.rows.forEach((row) => dependencies.add(row.foreign_table));
140
- } catch (error) {
141
- this.logger.warn(
142
- `Error obteniendo dependencias para ${table}: ${error.message}`
143
- );
144
- }
145
-
146
- return Array.from(dependencies);
147
- }
148
-
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) {
165
- this.logger.warn(
166
- `Dependencia ${dep} no encontrada para tabla ${table}`
167
- );
168
- return false;
169
- }
170
- }
171
-
172
- return true;
173
- }
174
- }
1
+ import { Logger } from "@nestjs/common";
2
+ import { Pool } from "pg";
3
+
4
+ export class DependencyResolver {
5
+ private readonly logger = new Logger("DependencyResolver");
6
+ private readonly visitedTables = new Set<string>();
7
+ private readonly temporaryMark = new Set<string>();
8
+ private readonly tableOrder: string[] = [];
9
+ private config: any;
10
+
11
+ constructor(
12
+ private readonly sourcePool: Pool,
13
+ private readonly targetPool: Pool
14
+ ) {}
15
+
16
+ setConfig(config: any) {
17
+ this.config = config;
18
+ }
19
+
20
+ async analyzeDependencies(): Promise<string[]> {
21
+ try {
22
+ this.logger.log("Iniciando análisis de dependencias...");
23
+
24
+ // Obtener todas las tablas del esquema
25
+ const tables = await this.getAllTables();
26
+
27
+ // Resetear estado
28
+ this.visitedTables.clear();
29
+ this.temporaryMark.clear();
30
+ this.tableOrder.length = 0;
31
+
32
+ // Ordenar tablas por prioridad primero
33
+ const priorityOrder = this.getPriorityOrder(tables);
34
+
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
+ }
43
+
44
+ this.logger.log(
45
+ `Orden de migración determinado: ${this.tableOrder.join(" -> ")}`
46
+ );
47
+ return this.tableOrder;
48
+ } catch (error) {
49
+ this.logger.error(`Error analizando dependencias: ${error.message}`);
50
+ throw error;
51
+ }
52
+ }
53
+
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 || []),
59
+ };
60
+
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];
73
+ }
74
+
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[]> {
105
+ const query = `
106
+ SELECT table_name
107
+ FROM information_schema.tables
108
+ WHERE table_schema = 'public'
109
+ AND table_type = 'BASE TABLE'
110
+ `;
111
+
112
+ const result = await this.sourcePool.query(query);
113
+ return result.rows.map((row) => row.table_name);
114
+ }
115
+
116
+ private async getTableDependencies(table: string): Promise<string[]> {
117
+ const dependencies = new Set<string>();
118
+
119
+ // Obtener dependencias de la configuración
120
+ const configDeps = this.config?.tables?.[table]?.dependencies || [];
121
+ configDeps.forEach((dep) => dependencies.add(dep));
122
+
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
+ `;
136
+
137
+ try {
138
+ const result = await this.sourcePool.query(query, [table]);
139
+ result.rows.forEach((row) => dependencies.add(row.foreign_table));
140
+ } catch (error) {
141
+ this.logger.warn(
142
+ `Error obteniendo dependencias para ${table}: ${error.message}`
143
+ );
144
+ }
145
+
146
+ return Array.from(dependencies);
147
+ }
148
+
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) {
165
+ this.logger.warn(
166
+ `Dependencia ${dep} no encontrada para tabla ${table}`
167
+ );
168
+ return false;
169
+ }
170
+ }
171
+
172
+ return true;
173
+ }
174
+ }