@prairielearn/postgres-tools 1.0.0
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/.turbo/turbo-build.log +0 -0
- package/README.md +0 -0
- package/dist/bin/pg-describe.d.ts +2 -0
- package/dist/bin/pg-describe.js +95 -0
- package/dist/bin/pg-describe.js.map +1 -0
- package/dist/bin/pg-diff.d.ts +2 -0
- package/dist/bin/pg-diff.js +54 -0
- package/dist/bin/pg-diff.js.map +1 -0
- package/dist/describe.d.ts +55 -0
- package/dist/describe.js +236 -0
- package/dist/describe.js.map +1 -0
- package/dist/describe.sql +129 -0
- package/dist/diff.d.ts +8 -0
- package/dist/diff.js +167 -0
- package/dist/diff.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/package.json +34 -0
- package/src/bin/pg-describe.ts +106 -0
- package/src/bin/pg-diff.ts +63 -0
- package/src/describe.sql +129 -0
- package/src/describe.ts +321 -0
- package/src/diff.ts +232 -0
- package/src/index.ts +7 -0
- package/tsconfig.json +8 -0
package/src/describe.ts
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import pg from 'pg';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { loadSqlEquiv, PostgresPool } from '@prairielearn/postgres';
|
|
5
|
+
|
|
6
|
+
const sql = loadSqlEquiv(__filename);
|
|
7
|
+
const pgArray = pg.types.arrayParser;
|
|
8
|
+
|
|
9
|
+
interface ColumnDescription {
|
|
10
|
+
name: string;
|
|
11
|
+
type: string;
|
|
12
|
+
notnull: boolean;
|
|
13
|
+
default: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface IndexDescription {
|
|
17
|
+
name: string;
|
|
18
|
+
isprimary: boolean;
|
|
19
|
+
isunique: boolean;
|
|
20
|
+
indexdef: string;
|
|
21
|
+
constraintdef: string;
|
|
22
|
+
contype: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ForeignKeyConstraintDescription {
|
|
26
|
+
name: string;
|
|
27
|
+
def: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ReferenceDescription {
|
|
31
|
+
name: string;
|
|
32
|
+
table: string;
|
|
33
|
+
condef: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CheckConstraintDescription {
|
|
37
|
+
name: string;
|
|
38
|
+
def: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface TableDescription {
|
|
42
|
+
columns: ColumnDescription[];
|
|
43
|
+
indexes: IndexDescription[];
|
|
44
|
+
foreignKeyConstraints: ForeignKeyConstraintDescription[];
|
|
45
|
+
references: ReferenceDescription[];
|
|
46
|
+
checkConstraints: CheckConstraintDescription[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DatabaseDescription {
|
|
50
|
+
tables: Record<string, TableDescription>;
|
|
51
|
+
enums: Record<string, string[]>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface DescribeOptions {
|
|
55
|
+
ignoreTables?: string[];
|
|
56
|
+
ignoreColumns?: string[];
|
|
57
|
+
ignoreEnums?: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parsePostgresArray(arr: string): string[] {
|
|
61
|
+
// @ts-expect-error -- Incorrect type definitions in `pg-types`.
|
|
62
|
+
return pgArray.create(arr, String).parse();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function describeWithPool(
|
|
66
|
+
pool: PostgresPool,
|
|
67
|
+
options: DescribeOptions
|
|
68
|
+
): Promise<DatabaseDescription> {
|
|
69
|
+
const ignoreTables = options?.ignoreTables || [];
|
|
70
|
+
const ignoreEnums = options?.ignoreEnums || [];
|
|
71
|
+
let ignoreColumns: Record<string, string[]> = {};
|
|
72
|
+
|
|
73
|
+
const output: DatabaseDescription = {
|
|
74
|
+
tables: {},
|
|
75
|
+
enums: {},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Get the names of the tables and filter out any ignored tables
|
|
79
|
+
const tablesRes = await pool.queryAsync(sql.get_tables, []);
|
|
80
|
+
const tables = tablesRes.rows.filter((table) => ignoreTables.indexOf(table.name) === -1);
|
|
81
|
+
|
|
82
|
+
// Transform ignored columns into a map from table names to arrays
|
|
83
|
+
// of column names
|
|
84
|
+
if (options.ignoreColumns && Array.isArray(options.ignoreColumns)) {
|
|
85
|
+
ignoreColumns = options.ignoreColumns
|
|
86
|
+
.filter((ignore) => {
|
|
87
|
+
return /^[^\s.]*\.[^\s.]*$/.test(ignore);
|
|
88
|
+
})
|
|
89
|
+
.reduce((result, value) => {
|
|
90
|
+
const res = /^(([^\s.]*)\.([^\s.]*))$/.exec(value);
|
|
91
|
+
if (!res) {
|
|
92
|
+
throw new Error(`Invalid ignore column: ${value}`);
|
|
93
|
+
}
|
|
94
|
+
const table = res[2];
|
|
95
|
+
const column = res[3];
|
|
96
|
+
(result[table] || (result[table] = [])).push(column);
|
|
97
|
+
return result;
|
|
98
|
+
}, {} as Record<string, string[]>);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get column info for each table
|
|
102
|
+
for (const table of tables) {
|
|
103
|
+
const columnResults = await pool.queryAsync(sql.get_columns_for_table, {
|
|
104
|
+
oid: table.oid,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const columns = columnResults.rows.filter((row) => {
|
|
108
|
+
return (ignoreColumns[table.name] || []).indexOf(row.name) === -1;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const indexResults = await pool.queryAsync(sql.get_indexes_for_table, {
|
|
112
|
+
oid: table.oid,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const foreignKeyConstraintResults = await pool.queryAsync(
|
|
116
|
+
sql.get_foreign_key_constraints_for_table,
|
|
117
|
+
{
|
|
118
|
+
oid: table.oid,
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const referenceResults = await pool.queryAsync(sql.get_references_for_table, {
|
|
123
|
+
oid: table.oid,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Filter out references from ignored tables
|
|
127
|
+
const references = referenceResults.rows.filter((row) => {
|
|
128
|
+
return ignoreTables.indexOf(row.table) === -1;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const checkConstraintResults = await pool.queryAsync(sql.get_check_constraints_for_table, {
|
|
132
|
+
oid: table.oid,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
output.tables[table.name] = {
|
|
136
|
+
columns: columns,
|
|
137
|
+
indexes: indexResults.rows,
|
|
138
|
+
foreignKeyConstraints: foreignKeyConstraintResults.rows,
|
|
139
|
+
references: references,
|
|
140
|
+
checkConstraints: checkConstraintResults.rows,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get all enums
|
|
145
|
+
const enumsRes = await pool.queryAsync(sql.get_enums, []);
|
|
146
|
+
|
|
147
|
+
// Filter ignored enums
|
|
148
|
+
const rows = enumsRes.rows.filter((row) => {
|
|
149
|
+
return ignoreEnums.indexOf(row.name) === -1;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
rows.forEach((row) => {
|
|
153
|
+
output.enums[row.name] = parsePostgresArray(row.values);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return output;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Will produce a description of a given database's schema. This will include
|
|
161
|
+
* information about tables, enums, constraints, indices, etc.
|
|
162
|
+
*/
|
|
163
|
+
export async function describeDatabase(
|
|
164
|
+
databaseName: string,
|
|
165
|
+
options: DescribeOptions = {}
|
|
166
|
+
): Promise<DatabaseDescription> {
|
|
167
|
+
// Connect to the database.
|
|
168
|
+
const pool = new PostgresPool();
|
|
169
|
+
const pgConfig = {
|
|
170
|
+
user: 'postgres',
|
|
171
|
+
database: databaseName,
|
|
172
|
+
host: 'localhost',
|
|
173
|
+
max: 10,
|
|
174
|
+
idleTimeoutMillis: 30000,
|
|
175
|
+
};
|
|
176
|
+
function idleErrorHandler(err: Error) {
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
await pool.initAsync(pgConfig, idleErrorHandler);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
return await describeWithPool(pool, options);
|
|
183
|
+
} finally {
|
|
184
|
+
await pool.closeAsync();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function formatDatabaseDescription(
|
|
189
|
+
description: DatabaseDescription,
|
|
190
|
+
options = { coloredOutput: true }
|
|
191
|
+
): { tables: Record<string, string>; enums: Record<string, string> } {
|
|
192
|
+
const output = {
|
|
193
|
+
tables: {} as Record<string, string>,
|
|
194
|
+
enums: {} as Record<string, string>,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
Object.keys(description.tables).forEach((tableName) => (output.tables[tableName] = ''));
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Optionally applies the given formatter to the text if colored output is
|
|
201
|
+
* enabled.
|
|
202
|
+
*/
|
|
203
|
+
function formatText(text: string, formatter: (s: string) => string): string {
|
|
204
|
+
if (options.coloredOutput) {
|
|
205
|
+
return formatter(text);
|
|
206
|
+
}
|
|
207
|
+
return text;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
Object.entries(description.tables).forEach(([tableName, table]) => {
|
|
211
|
+
if (table.columns.length > 0) {
|
|
212
|
+
output.tables[tableName] += formatText('columns\n', chalk.underline);
|
|
213
|
+
output.tables[tableName] += table.columns
|
|
214
|
+
.map((row) => {
|
|
215
|
+
let rowText = formatText(` ${row.name}`, chalk.bold);
|
|
216
|
+
rowText += ':' + formatText(` ${row.type}`, chalk.green);
|
|
217
|
+
if (row.notnull) {
|
|
218
|
+
rowText += formatText(' not null', chalk.gray);
|
|
219
|
+
}
|
|
220
|
+
if (row.default) {
|
|
221
|
+
rowText += formatText(` default ${row.default}`, chalk.gray);
|
|
222
|
+
}
|
|
223
|
+
return rowText;
|
|
224
|
+
})
|
|
225
|
+
.join('\n');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (table.indexes.length > 0) {
|
|
229
|
+
if (output.tables[tableName].length !== 0) {
|
|
230
|
+
output.tables[tableName] += '\n\n';
|
|
231
|
+
}
|
|
232
|
+
output.tables[tableName] += formatText('indexes\n', chalk.underline);
|
|
233
|
+
output.tables[tableName] += table.indexes
|
|
234
|
+
.map((row) => {
|
|
235
|
+
const using = row.indexdef.substring(row.indexdef.indexOf('USING '));
|
|
236
|
+
let rowText = formatText(` ${row.name}`, chalk.bold) + ':';
|
|
237
|
+
// Primary indexes are implicitly unique, so we don't need to
|
|
238
|
+
// capture that explicitly.
|
|
239
|
+
if (row.isunique && !row.isprimary) {
|
|
240
|
+
if (!row.constraintdef || row.constraintdef.indexOf('UNIQUE') === -1) {
|
|
241
|
+
// Some unique indexes don't include the UNIQUE constraint
|
|
242
|
+
// as part of the constraint definition, so we need to capture
|
|
243
|
+
// that manually.
|
|
244
|
+
rowText += formatText(` UNIQUE`, chalk.green);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
rowText += row.constraintdef ? formatText(` ${row.constraintdef}`, chalk.green) : '';
|
|
248
|
+
rowText += using ? formatText(` ${using}`, chalk.green) : '';
|
|
249
|
+
return rowText;
|
|
250
|
+
})
|
|
251
|
+
.join('\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (table.checkConstraints.length > 0) {
|
|
255
|
+
if (output.tables[tableName].length !== 0) {
|
|
256
|
+
output.tables[tableName] += '\n\n';
|
|
257
|
+
}
|
|
258
|
+
output.tables[tableName] += formatText('check constraints\n', chalk.underline);
|
|
259
|
+
output.tables[tableName] += table.checkConstraints
|
|
260
|
+
.map((row) => {
|
|
261
|
+
// Particularly long constraints are formatted as multiple lines.
|
|
262
|
+
// We'll collapse them into a single line for better appearance in
|
|
263
|
+
// the resulting file.
|
|
264
|
+
//
|
|
265
|
+
// The first replace handles lines that end with a parenthesis: we
|
|
266
|
+
// want to avoid spaces between the parenthesis and the next token.
|
|
267
|
+
//
|
|
268
|
+
// The second replace handles all other lines: we want to collapse
|
|
269
|
+
// all leading whitespace into a single space.
|
|
270
|
+
const def = row.def.replace(/\(\n/g, '(').replace(/\n\s*/g, ' ');
|
|
271
|
+
|
|
272
|
+
let rowText = formatText(` ${row.name}:`, chalk.bold);
|
|
273
|
+
rowText += formatText(` ${def}`, chalk.green);
|
|
274
|
+
return rowText;
|
|
275
|
+
})
|
|
276
|
+
.join('\n');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (table.foreignKeyConstraints.length > 0) {
|
|
280
|
+
if (output.tables[tableName].length !== 0) {
|
|
281
|
+
output.tables[tableName] += '\n\n';
|
|
282
|
+
}
|
|
283
|
+
output.tables[tableName] += formatText('foreign-key constraints\n', chalk.underline);
|
|
284
|
+
output.tables[tableName] += table.foreignKeyConstraints
|
|
285
|
+
.map((row) => {
|
|
286
|
+
let rowText = formatText(` ${row.name}:`, chalk.bold);
|
|
287
|
+
rowText += formatText(` ${row.def}`, chalk.green);
|
|
288
|
+
return rowText;
|
|
289
|
+
})
|
|
290
|
+
.join('\n');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (table.references.length > 0) {
|
|
294
|
+
if (output.tables[tableName].length !== 0) {
|
|
295
|
+
output.tables[tableName] += '\n\n';
|
|
296
|
+
}
|
|
297
|
+
output.tables[tableName] += formatText('referenced by\n', chalk.underline);
|
|
298
|
+
output.tables[tableName] += table.references
|
|
299
|
+
?.map((row) => {
|
|
300
|
+
let rowText = formatText(` ${row.table}:`, chalk.bold);
|
|
301
|
+
rowText += formatText(` ${row.condef}`, chalk.green);
|
|
302
|
+
return rowText;
|
|
303
|
+
})
|
|
304
|
+
.join('\n');
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
Object.entries(description.enums).forEach(([enumName, enumValues]) => {
|
|
309
|
+
output.enums[enumName] = formatText(enumValues.join(', '), chalk.gray);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// We need to tack on a newline to everything.
|
|
313
|
+
Object.entries(output.tables).forEach(([tableName, table]) => {
|
|
314
|
+
output.tables[tableName] = table + '\n';
|
|
315
|
+
});
|
|
316
|
+
Object.entries(output.enums).forEach(([enumName, enumValues]) => {
|
|
317
|
+
output.enums[enumName] = enumValues + '\n';
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return output;
|
|
321
|
+
}
|
package/src/diff.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import _ from 'lodash';
|
|
6
|
+
import { diffLines } from 'diff';
|
|
7
|
+
|
|
8
|
+
import { describeDatabase, formatDatabaseDescription } from './describe';
|
|
9
|
+
|
|
10
|
+
type DatabaseInfo = { type: 'database'; name: string };
|
|
11
|
+
type DirectoryInfo = { type: 'directory'; path: string };
|
|
12
|
+
type DiffTarget = DatabaseInfo | DirectoryInfo;
|
|
13
|
+
type DiffOptions = { coloredOutput?: boolean };
|
|
14
|
+
type Description = {
|
|
15
|
+
tables: Record<string, string>;
|
|
16
|
+
enums: Record<string, string>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
async function diff(db1: DiffTarget, db2: DiffTarget, options: DiffOptions): Promise<string> {
|
|
20
|
+
function formatText(text: string, formatter?: ((s: string) => string) | null): string {
|
|
21
|
+
if (options.coloredOutput && formatter) {
|
|
22
|
+
return formatter(text);
|
|
23
|
+
}
|
|
24
|
+
return text;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const db2Name = db2.type === 'database' ? db2.name : db2.path;
|
|
28
|
+
const db2NameBold = formatText(db2Name, chalk.bold);
|
|
29
|
+
|
|
30
|
+
let result = '';
|
|
31
|
+
|
|
32
|
+
const description1 = await loadDescription(db1);
|
|
33
|
+
const description2 = await loadDescription(db2);
|
|
34
|
+
|
|
35
|
+
// Determine if both databases have the same tables
|
|
36
|
+
const tablesMissingFrom1 = _.difference(_.keys(description2.tables), _.keys(description1.tables));
|
|
37
|
+
const tablesMissingFrom2 = _.difference(_.keys(description1.tables), _.keys(description2.tables));
|
|
38
|
+
|
|
39
|
+
if (tablesMissingFrom1.length > 0) {
|
|
40
|
+
result += formatText(`Tables added to ${db2NameBold} (${db2.type})\n`, chalk.underline);
|
|
41
|
+
result += formatText(
|
|
42
|
+
tablesMissingFrom1.map((table) => `+ ${table}`).join('\n') + '\n\n',
|
|
43
|
+
chalk.green
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (tablesMissingFrom2.length > 0) {
|
|
48
|
+
result += formatText(`Tables missing from ${db2NameBold} (${db2.type})\n`, chalk.underline);
|
|
49
|
+
result += formatText(
|
|
50
|
+
tablesMissingFrom2.map((table) => `- ${table}`).join('\n') + '\n\n',
|
|
51
|
+
chalk.red
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Determine if both databases have the same enums
|
|
56
|
+
const enumsMissingFrom1 = _.difference(_.keys(description2.enums), _.keys(description1.enums));
|
|
57
|
+
const enumsMissingFrom2 = _.difference(_.keys(description1.enums), _.keys(description2.enums));
|
|
58
|
+
|
|
59
|
+
if (enumsMissingFrom1.length > 0) {
|
|
60
|
+
result += formatText(`Enums added to ${db2NameBold} (${db1.type})\n`, chalk.underline);
|
|
61
|
+
result += formatText(
|
|
62
|
+
enumsMissingFrom1.map((enumName) => `+ ${enumName}`).join('\n') + '\n\n',
|
|
63
|
+
chalk.green
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (enumsMissingFrom2.length > 0) {
|
|
68
|
+
result += formatText(`Enums missing from ${db2NameBold} (${db2.type})\n`, chalk.underline);
|
|
69
|
+
result += formatText(
|
|
70
|
+
enumsMissingFrom2.map((enumName) => `- ${enumName}`).join('\n') + '\n\n',
|
|
71
|
+
chalk.red
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Determine if the columns of any table differ
|
|
76
|
+
const intersection = _.intersection(_.keys(description1.tables), _.keys(description2.tables));
|
|
77
|
+
_.forEach(intersection, (table) => {
|
|
78
|
+
// We normalize each blob to end with a newline to make diffs print cleaner
|
|
79
|
+
const diff = diffLines(
|
|
80
|
+
description1.tables[table].trim() + '\n',
|
|
81
|
+
description2.tables[table].trim() + '\n'
|
|
82
|
+
);
|
|
83
|
+
if (diff.length === 1) return;
|
|
84
|
+
|
|
85
|
+
const boldTable = formatText(table, chalk.bold);
|
|
86
|
+
result += formatText(`Differences in ${boldTable} table\n`, chalk.underline);
|
|
87
|
+
|
|
88
|
+
// Shift around the newlines so that we can cleanly show +/- symbols
|
|
89
|
+
for (let i = 1; i < diff.length; i++) {
|
|
90
|
+
const prev = diff[i - 1].value;
|
|
91
|
+
if (prev[prev.length - 1] === '\n') {
|
|
92
|
+
diff[i - 1].value = prev.slice(0, -1);
|
|
93
|
+
diff[i].value = '\n' + diff[i].value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_.forEach(diff, (part, index) => {
|
|
98
|
+
if (index === 0) {
|
|
99
|
+
part.value = '\n' + part.value;
|
|
100
|
+
}
|
|
101
|
+
const mark = part.added ? '+ ' : part.removed ? '- ' : ' ';
|
|
102
|
+
let change = part.value.split('\n').join(`\n${mark}`);
|
|
103
|
+
if (index === 0) {
|
|
104
|
+
change = change.slice(1, change.length);
|
|
105
|
+
}
|
|
106
|
+
if (part.added || part.removed) {
|
|
107
|
+
result += formatText(change, part.added ? chalk.green : part.removed ? chalk.red : null);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
result += '\n\n';
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Determine if the values of any enums differ
|
|
114
|
+
const enumsIntersection = _.intersection(_.keys(description1.enums), _.keys(description2.enums));
|
|
115
|
+
_.forEach(enumsIntersection, (enumName) => {
|
|
116
|
+
// We don't need to do a particularly fancy diff here, since
|
|
117
|
+
// enums are just represented here as strings
|
|
118
|
+
if (description1.enums[enumName].trim() !== description2.enums[enumName].trim()) {
|
|
119
|
+
const boldEnum = formatText(enumName, chalk.bold);
|
|
120
|
+
result += formatText(`Differences in ${boldEnum} enum\n`);
|
|
121
|
+
result += formatText(`- ${description1.enums[enumName].trim()}\n`, chalk.red);
|
|
122
|
+
result += formatText(`+ ${description2.enums[enumName].trim()}\n`, chalk.green);
|
|
123
|
+
result += '\n\n';
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function loadDescriptionFromDisk(dirPath: string): Promise<Description> {
|
|
131
|
+
const description: Description = {
|
|
132
|
+
tables: {},
|
|
133
|
+
enums: {},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const tables = await fs.readdir(path.join(dirPath, 'tables'));
|
|
137
|
+
for (const table of tables) {
|
|
138
|
+
const data = await fs.readFile(path.join(dirPath, 'tables', table), 'utf8');
|
|
139
|
+
description.tables[table.replace('.pg', '')] = data;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const enums = await fs.readdir(path.join(dirPath, 'enums'));
|
|
143
|
+
for (const enumName of enums) {
|
|
144
|
+
const data = await fs.readFile(path.join(dirPath, 'enums', enumName), 'utf8');
|
|
145
|
+
description.enums[enumName.replace('.pg', '')] = data;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return description;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function loadDescriptionFromDatabase(name: string) {
|
|
152
|
+
const description = await describeDatabase(name);
|
|
153
|
+
return formatDatabaseDescription(description, { coloredOutput: false });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function loadDescription(db: DiffTarget): Promise<Description> {
|
|
157
|
+
if (db.type === 'database') {
|
|
158
|
+
return loadDescriptionFromDatabase(db.name);
|
|
159
|
+
} else if (db.type === 'directory') {
|
|
160
|
+
return loadDescriptionFromDisk(db.path);
|
|
161
|
+
} else {
|
|
162
|
+
throw new Error('Invalid database type');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function diffDatabases(database1: string, database2: string, options: DiffOptions) {
|
|
167
|
+
return diff(
|
|
168
|
+
{
|
|
169
|
+
type: 'database',
|
|
170
|
+
name: database1,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
type: 'database',
|
|
174
|
+
name: database2,
|
|
175
|
+
},
|
|
176
|
+
options
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function diffDatabaseAndDirectory(
|
|
181
|
+
database: string,
|
|
182
|
+
directory: string,
|
|
183
|
+
options: DiffOptions
|
|
184
|
+
) {
|
|
185
|
+
return diff(
|
|
186
|
+
{
|
|
187
|
+
type: 'database',
|
|
188
|
+
name: database,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: 'directory',
|
|
192
|
+
path: directory,
|
|
193
|
+
},
|
|
194
|
+
options
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function diffDirectoryAndDatabase(
|
|
199
|
+
directory: string,
|
|
200
|
+
database: string,
|
|
201
|
+
options: DiffOptions
|
|
202
|
+
) {
|
|
203
|
+
return diff(
|
|
204
|
+
{
|
|
205
|
+
type: 'directory',
|
|
206
|
+
path: directory,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
type: 'database',
|
|
210
|
+
name: database,
|
|
211
|
+
},
|
|
212
|
+
options
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function diffDirectories(
|
|
217
|
+
directory1: string,
|
|
218
|
+
directory2: string,
|
|
219
|
+
options: DiffOptions
|
|
220
|
+
) {
|
|
221
|
+
return diff(
|
|
222
|
+
{
|
|
223
|
+
type: 'directory',
|
|
224
|
+
path: directory1,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
type: 'directory',
|
|
228
|
+
path: directory2,
|
|
229
|
+
},
|
|
230
|
+
options
|
|
231
|
+
);
|
|
232
|
+
}
|
package/src/index.ts
ADDED