relq 1.0.2 → 1.0.3

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 (92) hide show
  1. package/dist/cjs/cli/commands/add.cjs +403 -27
  2. package/dist/cjs/cli/commands/branch.cjs +13 -23
  3. package/dist/cjs/cli/commands/checkout.cjs +16 -29
  4. package/dist/cjs/cli/commands/cherry-pick.cjs +3 -4
  5. package/dist/cjs/cli/commands/commit.cjs +21 -29
  6. package/dist/cjs/cli/commands/diff.cjs +28 -32
  7. package/dist/cjs/cli/commands/export.cjs +7 -7
  8. package/dist/cjs/cli/commands/fetch.cjs +15 -21
  9. package/dist/cjs/cli/commands/generate.cjs +28 -54
  10. package/dist/cjs/cli/commands/history.cjs +19 -40
  11. package/dist/cjs/cli/commands/import.cjs +34 -41
  12. package/dist/cjs/cli/commands/init.cjs +69 -59
  13. package/dist/cjs/cli/commands/introspect.cjs +4 -8
  14. package/dist/cjs/cli/commands/log.cjs +26 -32
  15. package/dist/cjs/cli/commands/merge.cjs +24 -41
  16. package/dist/cjs/cli/commands/migrate.cjs +12 -25
  17. package/dist/cjs/cli/commands/pull.cjs +216 -106
  18. package/dist/cjs/cli/commands/push.cjs +35 -75
  19. package/dist/cjs/cli/commands/remote.cjs +2 -1
  20. package/dist/cjs/cli/commands/reset.cjs +22 -43
  21. package/dist/cjs/cli/commands/resolve.cjs +12 -14
  22. package/dist/cjs/cli/commands/rollback.cjs +16 -38
  23. package/dist/cjs/cli/commands/stash.cjs +5 -7
  24. package/dist/cjs/cli/commands/status.cjs +5 -10
  25. package/dist/cjs/cli/commands/sync.cjs +30 -50
  26. package/dist/cjs/cli/commands/tag.cjs +3 -4
  27. package/dist/cjs/cli/index.cjs +72 -9
  28. package/dist/cjs/cli/utils/change-tracker.cjs +107 -3
  29. package/dist/cjs/cli/utils/cli-utils.cjs +217 -0
  30. package/dist/cjs/cli/utils/config-loader.cjs +34 -8
  31. package/dist/cjs/cli/utils/fast-introspect.cjs +109 -3
  32. package/dist/cjs/cli/utils/git-utils.cjs +42 -161
  33. package/dist/cjs/cli/utils/pool-manager.cjs +156 -0
  34. package/dist/cjs/cli/utils/project-root.cjs +56 -5
  35. package/dist/cjs/cli/utils/relqignore.cjs +1 -0
  36. package/dist/cjs/cli/utils/repo-manager.cjs +47 -0
  37. package/dist/cjs/cli/utils/schema-comparator.cjs +301 -11
  38. package/dist/cjs/cli/utils/schema-diff.cjs +202 -1
  39. package/dist/cjs/cli/utils/schema-hash.cjs +2 -1
  40. package/dist/cjs/cli/utils/schema-introspect.cjs +7 -3
  41. package/dist/cjs/cli/utils/snapshot-manager.cjs +1 -0
  42. package/dist/cjs/cli/utils/spinner.cjs +14 -106
  43. package/dist/cjs/cli/utils/sql-generator.cjs +1 -1
  44. package/dist/cjs/cli/utils/type-generator.cjs +28 -16
  45. package/dist/config.d.ts +16 -25
  46. package/dist/esm/cli/commands/add.js +372 -29
  47. package/dist/esm/cli/commands/branch.js +14 -24
  48. package/dist/esm/cli/commands/checkout.js +16 -29
  49. package/dist/esm/cli/commands/cherry-pick.js +3 -4
  50. package/dist/esm/cli/commands/commit.js +22 -30
  51. package/dist/esm/cli/commands/diff.js +6 -10
  52. package/dist/esm/cli/commands/export.js +8 -8
  53. package/dist/esm/cli/commands/fetch.js +14 -20
  54. package/dist/esm/cli/commands/generate.js +28 -54
  55. package/dist/esm/cli/commands/history.js +11 -32
  56. package/dist/esm/cli/commands/import.js +35 -42
  57. package/dist/esm/cli/commands/init.js +65 -55
  58. package/dist/esm/cli/commands/introspect.js +4 -8
  59. package/dist/esm/cli/commands/log.js +6 -12
  60. package/dist/esm/cli/commands/merge.js +20 -37
  61. package/dist/esm/cli/commands/migrate.js +12 -25
  62. package/dist/esm/cli/commands/pull.js +204 -94
  63. package/dist/esm/cli/commands/push.js +21 -61
  64. package/dist/esm/cli/commands/remote.js +2 -1
  65. package/dist/esm/cli/commands/reset.js +16 -37
  66. package/dist/esm/cli/commands/resolve.js +13 -15
  67. package/dist/esm/cli/commands/rollback.js +16 -38
  68. package/dist/esm/cli/commands/stash.js +6 -8
  69. package/dist/esm/cli/commands/status.js +6 -11
  70. package/dist/esm/cli/commands/sync.js +30 -50
  71. package/dist/esm/cli/commands/tag.js +3 -4
  72. package/dist/esm/cli/index.js +72 -9
  73. package/dist/esm/cli/utils/change-tracker.js +107 -3
  74. package/dist/esm/cli/utils/cli-utils.js +169 -0
  75. package/dist/esm/cli/utils/config-loader.js +34 -8
  76. package/dist/esm/cli/utils/fast-introspect.js +109 -3
  77. package/dist/esm/cli/utils/git-utils.js +2 -124
  78. package/dist/esm/cli/utils/pool-manager.js +114 -0
  79. package/dist/esm/cli/utils/project-root.js +55 -5
  80. package/dist/esm/cli/utils/relqignore.js +1 -0
  81. package/dist/esm/cli/utils/repo-manager.js +42 -0
  82. package/dist/esm/cli/utils/schema-comparator.js +301 -11
  83. package/dist/esm/cli/utils/schema-diff.js +202 -1
  84. package/dist/esm/cli/utils/schema-hash.js +2 -1
  85. package/dist/esm/cli/utils/schema-introspect.js +7 -3
  86. package/dist/esm/cli/utils/snapshot-manager.js +1 -0
  87. package/dist/esm/cli/utils/spinner.js +1 -101
  88. package/dist/esm/cli/utils/sql-generator.js +1 -1
  89. package/dist/esm/cli/utils/type-generator.js +28 -16
  90. package/dist/index.d.ts +25 -8
  91. package/dist/schema-builder.d.ts +16 -6
  92. package/package.json +1 -1
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.colors = void 0;
37
+ exports.createSpinner = createSpinner;
38
+ exports.fatal = fatal;
39
+ exports.error = error;
40
+ exports.warning = warning;
41
+ exports.hint = hint;
42
+ exports.success = success;
43
+ exports.confirm = confirm;
44
+ exports.select = select;
45
+ exports.formatBytes = formatBytes;
46
+ exports.formatDuration = formatDuration;
47
+ exports.progressBar = progressBar;
48
+ exports.requireInit = requireInit;
49
+ const readline = __importStar(require("readline"));
50
+ const isColorSupported = process.stdout.isTTY && !process.env.NO_COLOR;
51
+ exports.colors = {
52
+ reset: '\x1b[0m',
53
+ red: (s) => isColorSupported ? `\x1b[31m${s}\x1b[0m` : s,
54
+ green: (s) => isColorSupported ? `\x1b[32m${s}\x1b[0m` : s,
55
+ yellow: (s) => isColorSupported ? `\x1b[33m${s}\x1b[0m` : s,
56
+ blue: (s) => isColorSupported ? `\x1b[34m${s}\x1b[0m` : s,
57
+ magenta: (s) => isColorSupported ? `\x1b[35m${s}\x1b[0m` : s,
58
+ cyan: (s) => isColorSupported ? `\x1b[36m${s}\x1b[0m` : s,
59
+ white: (s) => isColorSupported ? `\x1b[37m${s}\x1b[0m` : s,
60
+ gray: (s) => isColorSupported ? `\x1b[90m${s}\x1b[0m` : s,
61
+ bold: (s) => isColorSupported ? `\x1b[1m${s}\x1b[0m` : s,
62
+ dim: (s) => isColorSupported ? `\x1b[2m${s}\x1b[0m` : s,
63
+ muted: (s) => isColorSupported ? `\x1b[90m${s}\x1b[0m` : s,
64
+ success: (s) => isColorSupported ? `\x1b[32m${s}\x1b[0m` : s,
65
+ error: (s) => isColorSupported ? `\x1b[31m${s}\x1b[0m` : s,
66
+ warning: (s) => isColorSupported ? `\x1b[33m${s}\x1b[0m` : s,
67
+ info: (s) => isColorSupported ? `\x1b[34m${s}\x1b[0m` : s,
68
+ };
69
+ function createSpinner() {
70
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
71
+ let frameIndex = 0;
72
+ let interval = null;
73
+ let currentMessage = '';
74
+ let isSpinning = false;
75
+ const isTTY = process.stdout.isTTY;
76
+ const clearLine = () => {
77
+ if (isTTY)
78
+ process.stdout.write('\r\x1b[K');
79
+ };
80
+ const render = () => {
81
+ if (!isSpinning)
82
+ return;
83
+ clearLine();
84
+ process.stdout.write(`${exports.colors.cyan(frames[frameIndex])} ${currentMessage}`);
85
+ frameIndex = (frameIndex + 1) % frames.length;
86
+ };
87
+ return {
88
+ start(message) {
89
+ if (isSpinning)
90
+ this.stop();
91
+ currentMessage = message;
92
+ isSpinning = true;
93
+ frameIndex = 0;
94
+ if (isTTY) {
95
+ interval = setInterval(render, 80);
96
+ render();
97
+ }
98
+ else {
99
+ console.log(`${message}...`);
100
+ }
101
+ },
102
+ update(message) {
103
+ currentMessage = message;
104
+ },
105
+ succeed(message) {
106
+ this.stop();
107
+ console.log(message || currentMessage);
108
+ },
109
+ fail(message) {
110
+ this.stop();
111
+ },
112
+ info(message) {
113
+ this.stop();
114
+ console.log(message || currentMessage);
115
+ },
116
+ warn(message) {
117
+ this.stop();
118
+ warning(message || currentMessage);
119
+ },
120
+ stop() {
121
+ if (interval) {
122
+ clearInterval(interval);
123
+ interval = null;
124
+ }
125
+ if (isSpinning && isTTY) {
126
+ clearLine();
127
+ }
128
+ isSpinning = false;
129
+ }
130
+ };
131
+ }
132
+ function fatal(message, hintMessage) {
133
+ console.error(`${exports.colors.red('fatal:')} ${message}`);
134
+ if (hintMessage) {
135
+ hint(hintMessage);
136
+ }
137
+ process.exit(1);
138
+ }
139
+ function error(message, hintMessage) {
140
+ console.error(`${exports.colors.red('error:')} ${message}`);
141
+ if (hintMessage) {
142
+ hint(hintMessage);
143
+ }
144
+ }
145
+ function warning(message) {
146
+ console.error(`${exports.colors.yellow('warning:')} ${message}`);
147
+ }
148
+ function hint(message) {
149
+ console.error(`${exports.colors.yellow('hint:')} ${message}`);
150
+ }
151
+ function success(message) {
152
+ console.log(exports.colors.green(message));
153
+ }
154
+ function confirm(question, defaultYes = true) {
155
+ const rl = readline.createInterface({
156
+ input: process.stdin,
157
+ output: process.stdout,
158
+ });
159
+ const suffix = defaultYes ? '[Y/n]' : '[y/N]';
160
+ return new Promise((resolve) => {
161
+ rl.question(`${question} ${suffix} `, (answer) => {
162
+ rl.close();
163
+ const a = answer.trim().toLowerCase();
164
+ if (!a)
165
+ resolve(defaultYes);
166
+ else
167
+ resolve(a === 'y' || a === 'yes');
168
+ });
169
+ });
170
+ }
171
+ function select(question, options) {
172
+ const rl = readline.createInterface({
173
+ input: process.stdin,
174
+ output: process.stdout,
175
+ });
176
+ console.log(question);
177
+ options.forEach((opt, i) => {
178
+ console.log(` ${i + 1}) ${opt}`);
179
+ });
180
+ return new Promise((resolve) => {
181
+ rl.question(`Select [1-${options.length}]: `, (answer) => {
182
+ rl.close();
183
+ const num = parseInt(answer.trim(), 10);
184
+ if (num >= 1 && num <= options.length) {
185
+ resolve(num - 1);
186
+ }
187
+ else {
188
+ resolve(0);
189
+ }
190
+ });
191
+ });
192
+ }
193
+ function formatBytes(bytes) {
194
+ if (bytes < 1024)
195
+ return `${bytes} B`;
196
+ if (bytes < 1024 * 1024)
197
+ return `${(bytes / 1024).toFixed(1)} KB`;
198
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
199
+ }
200
+ function formatDuration(ms) {
201
+ if (ms < 1000)
202
+ return `${ms}ms`;
203
+ if (ms < 60000)
204
+ return `${(ms / 1000).toFixed(1)}s`;
205
+ return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
206
+ }
207
+ function progressBar(current, total, width = 30) {
208
+ const percentage = Math.min(100, Math.round((current / total) * 100));
209
+ const filled = Math.round((percentage / 100) * width);
210
+ const empty = width - filled;
211
+ return `${'#'.repeat(filled)}${'.'.repeat(empty)} ${percentage}%`;
212
+ }
213
+ function requireInit(isInitialized, projectRoot) {
214
+ if (!isInitialized) {
215
+ fatal('not a relq repository (or any of the parent directories): .relq', "run 'relq init' to initialize a repository");
216
+ }
217
+ }
@@ -105,7 +105,7 @@ function validateConfig(config) {
105
105
  if (!config.connection?.host && !config.connection?.url) {
106
106
  errors.push('No database connection configured. Set connection in relq.config.ts or use DATABASE_* env vars.');
107
107
  }
108
- const hasSchemaPath = typeof config.schema === 'string';
108
+ const hasSchemaPath = typeof config.schema === 'string' && config.schema.length > 0;
109
109
  const hasSchemaDir = typeof config.schema === 'object' && config.schema?.directory;
110
110
  const hasTypeGenOutput = config.typeGeneration?.output;
111
111
  const hasGenerateOutDir = config.generate?.outDir;
@@ -114,14 +114,40 @@ function validateConfig(config) {
114
114
  }
115
115
  return errors;
116
116
  }
117
- function requireValidConfig(config) {
117
+ async function requireValidConfig(config, options) {
118
118
  const errors = validateConfig(config);
119
- if (errors.length > 0) {
120
- console.error('Configuration errors:');
121
- for (const error of errors) {
122
- console.error(` • ${error}`);
119
+ if (errors.length === 0)
120
+ return;
121
+ const colors = {
122
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
123
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
124
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
125
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
126
+ };
127
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
128
+ if (options?.autoComplete !== false && isInteractive) {
129
+ try {
130
+ const { initCommand } = await Promise.resolve().then(() => __importStar(require("../commands/init.cjs")));
131
+ const { findProjectRoot } = await Promise.resolve().then(() => __importStar(require("./project-root.cjs")));
132
+ const projectRoot = findProjectRoot() || process.cwd();
133
+ const flags = {};
134
+ if (options?.calledFrom) {
135
+ flags['called-from'] = options.calledFrom;
136
+ }
137
+ await initCommand({ args: [], flags, config, projectRoot });
138
+ return;
139
+ }
140
+ catch (e) {
141
+ process.exit(1);
123
142
  }
124
- console.error('\nRun "relq init" to create a configuration file.');
125
- process.exit(1);
126
143
  }
144
+ console.error('');
145
+ console.error(colors.red('error:') + ' Configuration errors:');
146
+ for (const error of errors) {
147
+ console.error(` ${colors.yellow('•')} ${error}`);
148
+ }
149
+ console.error('');
150
+ console.error(colors.yellow('hint:') + ` Run ${colors.cyan('relq init')} to create a configuration file or check your config settings.`);
151
+ console.error('');
152
+ process.exit(1);
127
153
  }
@@ -34,6 +34,18 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.fastIntrospectDatabase = fastIntrospectDatabase;
37
+ function parseOptionsArray(options) {
38
+ if (!options)
39
+ return {};
40
+ const result = {};
41
+ for (const opt of options) {
42
+ const eqIdx = opt.indexOf('=');
43
+ if (eqIdx > 0) {
44
+ result[opt.substring(0, eqIdx)] = opt.substring(eqIdx + 1);
45
+ }
46
+ }
47
+ return result;
48
+ }
37
49
  async function fastIntrospectDatabase(connection, onProgress, options) {
38
50
  const { includeFunctions = false, includeTriggers = false } = options || {};
39
51
  const { Pool } = await Promise.resolve().then(() => __importStar(require("../../addon/pg/index.cjs")));
@@ -254,7 +266,7 @@ async function fastIntrospectDatabase(connection, onProgress, options) {
254
266
  const tables = [];
255
267
  for (const row of tablesResult.rows) {
256
268
  const tableName = row.table_name;
257
- if (tableName.startsWith('_relq'))
269
+ if (tableName.startsWith('_relq') || tableName.startsWith('_kuery'))
258
270
  continue;
259
271
  tables.push({
260
272
  name: tableName,
@@ -376,6 +388,99 @@ async function fastIntrospectDatabase(connection, onProgress, options) {
376
388
  isEnabled: t.is_enabled,
377
389
  }));
378
390
  }
391
+ onProgress?.('fetching_collations');
392
+ const collationsResult = await pool.query(`
393
+ SELECT
394
+ c.collname as name,
395
+ n.nspname as schema,
396
+ c.collprovider as provider,
397
+ c.collcollate as lc_collate,
398
+ c.collctype as lc_ctype,
399
+ c.collisdeterministic as deterministic
400
+ FROM pg_collation c
401
+ JOIN pg_namespace n ON c.collnamespace = n.oid
402
+ WHERE n.nspname = 'public'
403
+ ORDER BY c.collname;
404
+ `);
405
+ const collations = collationsResult.rows.map(c => ({
406
+ name: c.name,
407
+ schema: c.schema,
408
+ provider: c.provider === 'i' ? 'icu' : c.provider === 'c' ? 'libc' : 'default',
409
+ lcCollate: c.lc_collate,
410
+ lcCtype: c.lc_ctype,
411
+ deterministic: c.deterministic,
412
+ }));
413
+ onProgress?.('fetching_foreign_servers');
414
+ const foreignServersResult = await pool.query(`
415
+ SELECT
416
+ s.srvname as name,
417
+ f.fdwname as fdw,
418
+ s.srvoptions as options
419
+ FROM pg_foreign_server s
420
+ JOIN pg_foreign_data_wrapper f ON s.srvfdw = f.oid
421
+ ORDER BY s.srvname;
422
+ `);
423
+ const foreignServers = foreignServersResult.rows.map(s => ({
424
+ name: s.name,
425
+ foreignDataWrapper: s.fdw,
426
+ options: parseOptionsArray(s.options),
427
+ }));
428
+ onProgress?.('fetching_foreign_tables');
429
+ const foreignTablesResult = await pool.query(`
430
+ SELECT
431
+ c.relname as name,
432
+ n.nspname as schema,
433
+ s.srvname as server_name,
434
+ ft.ftoptions as options
435
+ FROM pg_foreign_table ft
436
+ JOIN pg_class c ON ft.ftrelid = c.oid
437
+ JOIN pg_namespace n ON c.relnamespace = n.oid
438
+ JOIN pg_foreign_server s ON ft.ftserver = s.oid
439
+ WHERE n.nspname = 'public'
440
+ ORDER BY c.relname;
441
+ `);
442
+ const foreignTableNames = foreignTablesResult.rows.map(t => t.name);
443
+ let foreignTableColumns = new Map();
444
+ if (foreignTableNames.length > 0) {
445
+ const ftColsResult = await pool.query(`
446
+ SELECT
447
+ c.relname as table_name,
448
+ a.attname as column_name,
449
+ pg_catalog.format_type(a.atttypid, a.atttypmod) as data_type
450
+ FROM pg_attribute a
451
+ JOIN pg_class c ON a.attrelid = c.oid
452
+ JOIN pg_namespace n ON c.relnamespace = n.oid
453
+ WHERE n.nspname = 'public'
454
+ AND c.relname = ANY($1)
455
+ AND a.attnum > 0
456
+ AND NOT a.attisdropped
457
+ ORDER BY c.relname, a.attnum;
458
+ `, [foreignTableNames]);
459
+ for (const row of ftColsResult.rows) {
460
+ const cols = foreignTableColumns.get(row.table_name) || [];
461
+ cols.push({ name: row.column_name, type: row.data_type });
462
+ foreignTableColumns.set(row.table_name, cols);
463
+ }
464
+ }
465
+ const foreignTables = foreignTablesResult.rows.map(t => ({
466
+ name: t.name,
467
+ schema: t.schema,
468
+ serverName: t.server_name,
469
+ columns: (foreignTableColumns.get(t.name) || []).map((c, i) => ({
470
+ name: c.name,
471
+ dataType: c.type,
472
+ isNullable: true,
473
+ defaultValue: null,
474
+ isPrimaryKey: false,
475
+ isUnique: false,
476
+ ordinalPosition: i + 1,
477
+ maxLength: null,
478
+ precision: null,
479
+ scale: null,
480
+ references: null,
481
+ })),
482
+ options: parseOptionsArray(t.options),
483
+ }));
379
484
  onProgress?.('complete');
380
485
  return {
381
486
  tables,
@@ -383,12 +488,13 @@ async function fastIntrospectDatabase(connection, onProgress, options) {
383
488
  domains: [],
384
489
  compositeTypes: [],
385
490
  sequences: [],
491
+ collations,
386
492
  functions,
387
493
  triggers,
388
494
  policies: [],
389
495
  partitions,
390
- foreignServers: [],
391
- foreignTables: [],
496
+ foreignServers,
497
+ foreignTables,
392
498
  extensions,
393
499
  };
394
500
  }