@sprucelabs/postgres-data-store 0.0.1
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/README.md +20 -0
- package/build/.spruce/settings.json +11 -0
- package/build/PostgresDatabase.d.ts +45 -0
- package/build/PostgresDatabase.js +343 -0
- package/build/QueryBuilder.d.ts +37 -0
- package/build/QueryBuilder.js +243 -0
- package/build/esm/PostgresDatabase.d.ts +45 -0
- package/build/esm/PostgresDatabase.js +404 -0
- package/build/esm/QueryBuilder.d.ts +37 -0
- package/build/esm/QueryBuilder.js +240 -0
- package/build/esm/index.d.ts +0 -0
- package/build/esm/index.js +2 -0
- package/build/esm/postgres.types.d.ts +2 -0
- package/build/esm/postgres.types.js +1 -0
- package/build/index.d.ts +0 -0
- package/build/index.js +2 -0
- package/build/postgres.types.d.ts +2 -0
- package/build/postgres.types.js +2 -0
- package/package.json +98 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
class QueryBuilder {
|
|
4
|
+
constructor() { }
|
|
5
|
+
static Builder() {
|
|
6
|
+
return new this();
|
|
7
|
+
}
|
|
8
|
+
find(tableName, query, options) {
|
|
9
|
+
const { includeFields, limit, skip, sort } = options !== null && options !== void 0 ? options : {};
|
|
10
|
+
const fields = this.buildColumnListFromIncludeFields(includeFields);
|
|
11
|
+
let sql = `SELECT ${fields} FROM ${tableName}`;
|
|
12
|
+
const { values, sql: where } = this.optionallyBuildWhere(query);
|
|
13
|
+
sql += where;
|
|
14
|
+
sql += this.optionallyBuildSkip(skip);
|
|
15
|
+
sql += this.optionallyBuildSort(sort);
|
|
16
|
+
sql += this.optionallyBuildLimit(limit);
|
|
17
|
+
return { sql, values };
|
|
18
|
+
}
|
|
19
|
+
optionallyBuildWhere(query, startingPlaceholderCount = 0) {
|
|
20
|
+
let sql = '';
|
|
21
|
+
const values = [];
|
|
22
|
+
const queryKeys = Object.keys(query);
|
|
23
|
+
if ((queryKeys !== null && queryKeys !== void 0 ? queryKeys : []).length > 0) {
|
|
24
|
+
const { set: columnSpecs, values: whereValues } = this.buildSetClause({
|
|
25
|
+
query,
|
|
26
|
+
startingCount: startingPlaceholderCount,
|
|
27
|
+
isBuildingWhere: true,
|
|
28
|
+
});
|
|
29
|
+
sql += ` WHERE ${columnSpecs.join(' AND ')}`;
|
|
30
|
+
values.push(...whereValues);
|
|
31
|
+
}
|
|
32
|
+
return { values, sql };
|
|
33
|
+
}
|
|
34
|
+
buildSetClause(options) {
|
|
35
|
+
const { query, startingCount = 0, placeholderTemplate = '${{count}}', isBuildingWhere = false, } = options;
|
|
36
|
+
let placeholderCount = startingCount;
|
|
37
|
+
const queryKeys = Object.keys(query);
|
|
38
|
+
const values = [];
|
|
39
|
+
const set = [];
|
|
40
|
+
queryKeys.forEach((k) => {
|
|
41
|
+
let value = query[k];
|
|
42
|
+
if (value === null || value === void 0 ? void 0 : value.$in) {
|
|
43
|
+
values.push(...value.$in.map((v) => this.normalizeValue(v)));
|
|
44
|
+
set.push(`${k} IN (${value.$in
|
|
45
|
+
.map(() => `$${++placeholderCount}`)
|
|
46
|
+
.join(', ')})`);
|
|
47
|
+
}
|
|
48
|
+
else if (value === null || value === void 0 ? void 0 : value.$regex) {
|
|
49
|
+
values.push(this.normalizeValue(value.$regex));
|
|
50
|
+
set.push(`${k} ~* $${++placeholderCount}`);
|
|
51
|
+
}
|
|
52
|
+
else if (value === null || value === void 0 ? void 0 : value.$lte) {
|
|
53
|
+
values.push(this.normalizeValue(value.$lte));
|
|
54
|
+
set.push(`${k} <= $${++placeholderCount}`);
|
|
55
|
+
}
|
|
56
|
+
else if (value === null || value === void 0 ? void 0 : value.$lt) {
|
|
57
|
+
values.push(this.normalizeValue(value.$lt));
|
|
58
|
+
set.push(`${k} < $${++placeholderCount}`);
|
|
59
|
+
}
|
|
60
|
+
else if (value === null || value === void 0 ? void 0 : value.$gte) {
|
|
61
|
+
values.push(this.normalizeValue(value.$gte));
|
|
62
|
+
set.push(`${k} >= $${++placeholderCount}`);
|
|
63
|
+
}
|
|
64
|
+
else if (value === null || value === void 0 ? void 0 : value.$gt) {
|
|
65
|
+
values.push(this.normalizeValue(value.$gt));
|
|
66
|
+
set.push(`${k} > $${++placeholderCount}`);
|
|
67
|
+
}
|
|
68
|
+
else if (value === null || value === void 0 ? void 0 : value.$ne) {
|
|
69
|
+
values.push(this.normalizeValue(value.$ne));
|
|
70
|
+
set.push(`${k} != $${++placeholderCount}`);
|
|
71
|
+
}
|
|
72
|
+
else if (k === '$or') {
|
|
73
|
+
const { set: orWheres, values: orValues } = this.buildSetClausFor$Or(value, placeholderCount);
|
|
74
|
+
set.push(`(${orWheres.join(' OR ')})`);
|
|
75
|
+
values.push(...orValues);
|
|
76
|
+
}
|
|
77
|
+
else if (k === '$push') {
|
|
78
|
+
const sub = this.buildSetClause({
|
|
79
|
+
query: value,
|
|
80
|
+
startingCount: placeholderCount++,
|
|
81
|
+
placeholderTemplate: `JSONB_SET(COALESCE(names || \${{count}}::JSONB, '[]'::JSONB), '{-1}', \${{count}}::JSONB)`,
|
|
82
|
+
});
|
|
83
|
+
values.push(...sub.values.map((v) => JSON.stringify(v)));
|
|
84
|
+
set.push(...sub.set);
|
|
85
|
+
}
|
|
86
|
+
else if (value === null || value === undefined) {
|
|
87
|
+
set.push(`${k} IS NULL`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
placeholderCount++;
|
|
91
|
+
let placeholder = placeholderTemplate
|
|
92
|
+
.replace(/{{count}}/gm, `${placeholderCount}`)
|
|
93
|
+
.replace(/{{fieldName}}/gm, k);
|
|
94
|
+
const isDotSearch = k.includes('.');
|
|
95
|
+
if (isDotSearch) {
|
|
96
|
+
const [field, prop] = k.split('.');
|
|
97
|
+
if (isBuildingWhere) {
|
|
98
|
+
k = `${field} ->> '${prop}'`;
|
|
99
|
+
value = `${value}`;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
k = field;
|
|
103
|
+
placeholder = `${k} || jsonb_build_object('${prop}', ${placeholder}::text)`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
values.push(this.normalizeValue(value));
|
|
107
|
+
set.push(`${k} = ${placeholder}`);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
return { set, values };
|
|
111
|
+
}
|
|
112
|
+
buildSetClausFor$Or(value, placeholderCount) {
|
|
113
|
+
const ors = [];
|
|
114
|
+
const orValues = [];
|
|
115
|
+
value.forEach((q) => {
|
|
116
|
+
const { set: where, values } = this.buildSetClause({
|
|
117
|
+
query: q,
|
|
118
|
+
startingCount: placeholderCount++,
|
|
119
|
+
});
|
|
120
|
+
ors.push(...where);
|
|
121
|
+
orValues.push(...values);
|
|
122
|
+
});
|
|
123
|
+
return { set: ors, values: orValues };
|
|
124
|
+
}
|
|
125
|
+
create(tableName, records) {
|
|
126
|
+
let { sql, values } = this.createWithoutReturning(tableName, records);
|
|
127
|
+
sql += ` RETURNING *`;
|
|
128
|
+
return {
|
|
129
|
+
sql,
|
|
130
|
+
values,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
createWithoutReturning(tableName, records) {
|
|
134
|
+
const { fields, placeholders, values } = this.splitRecordsIntoFieldsPlaceholdersAndValues(records);
|
|
135
|
+
const sql = `INSERT INTO ${tableName} (${fields.join(', ')}) VALUES ${placeholders.join(', ')}`;
|
|
136
|
+
return { sql, values };
|
|
137
|
+
}
|
|
138
|
+
splitRecordsIntoFieldsPlaceholdersAndValues(records) {
|
|
139
|
+
const fields = this.buildColumnListFromAllRecords(records);
|
|
140
|
+
let placeholderCount = 0;
|
|
141
|
+
const values = [];
|
|
142
|
+
const placeholders = records.map((record) => {
|
|
143
|
+
const placeholders = [];
|
|
144
|
+
fields.forEach((f) => {
|
|
145
|
+
values.push(this.fieldValueToSqlValue(record, f));
|
|
146
|
+
let placeholder = `$${++placeholderCount}`;
|
|
147
|
+
if (this.isValueObject(record[f])) {
|
|
148
|
+
placeholder += `::json`;
|
|
149
|
+
}
|
|
150
|
+
placeholders.push(placeholder);
|
|
151
|
+
});
|
|
152
|
+
return `(${placeholders.join(', ')})`;
|
|
153
|
+
});
|
|
154
|
+
return { fields, placeholders, values };
|
|
155
|
+
}
|
|
156
|
+
fieldValueToSqlValue(record, f) {
|
|
157
|
+
let value = record[f];
|
|
158
|
+
return this.normalizeValue(value);
|
|
159
|
+
}
|
|
160
|
+
normalizeValue(value) {
|
|
161
|
+
if (value instanceof RegExp) {
|
|
162
|
+
value = value.toString().replace(/\//g, '');
|
|
163
|
+
}
|
|
164
|
+
if (this.isValueObject(value)) {
|
|
165
|
+
value = JSON.stringify(value);
|
|
166
|
+
}
|
|
167
|
+
return value !== null && value !== void 0 ? value : null;
|
|
168
|
+
}
|
|
169
|
+
isValueObject(value) {
|
|
170
|
+
return value !== null && (Array.isArray(value) || typeof value === 'object');
|
|
171
|
+
}
|
|
172
|
+
buildColumnListFromAllRecords(records) {
|
|
173
|
+
const fields = records.map((r) => Object.keys(r)).flat();
|
|
174
|
+
const uniqueFields = new Set(fields);
|
|
175
|
+
return Array.from(uniqueFields);
|
|
176
|
+
}
|
|
177
|
+
optionallyBuildSort(sort) {
|
|
178
|
+
if (sort) {
|
|
179
|
+
const sortSpecs = sort.map((s) => `${s.field} ${s.direction.toUpperCase()}`);
|
|
180
|
+
return ` ORDER BY ${sortSpecs.join(', ')}`;
|
|
181
|
+
}
|
|
182
|
+
return '';
|
|
183
|
+
}
|
|
184
|
+
optionallyBuildSkip(limit) {
|
|
185
|
+
if (typeof limit === 'number') {
|
|
186
|
+
return ` OFFSET ${limit}`;
|
|
187
|
+
}
|
|
188
|
+
return '';
|
|
189
|
+
}
|
|
190
|
+
optionallyBuildLimit(limit) {
|
|
191
|
+
if (typeof limit === 'number') {
|
|
192
|
+
return ` LIMIT ${limit}`;
|
|
193
|
+
}
|
|
194
|
+
return '';
|
|
195
|
+
}
|
|
196
|
+
buildColumnListFromIncludeFields(includeFields) {
|
|
197
|
+
return !includeFields ? '*' : includeFields.join(', ');
|
|
198
|
+
}
|
|
199
|
+
update(tableName, query, updates, shouldReturnUpdatedRecords = true) {
|
|
200
|
+
const { set: set, values } = this.buildSetClause({
|
|
201
|
+
query: updates,
|
|
202
|
+
startingCount: 0,
|
|
203
|
+
});
|
|
204
|
+
let sql = `UPDATE ${tableName} SET ${set.join(', ')}`;
|
|
205
|
+
const { sql: where, values: whereValues } = this.optionallyBuildWhere(query, values.length);
|
|
206
|
+
sql += where;
|
|
207
|
+
if (shouldReturnUpdatedRecords) {
|
|
208
|
+
sql += ' RETURNING *';
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
sql,
|
|
212
|
+
values: [...values, ...whereValues],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
delete(tableName, query) {
|
|
216
|
+
let sql = `DELETE FROM ${tableName}`;
|
|
217
|
+
const { values, sql: where } = this.optionallyBuildWhere(query !== null && query !== void 0 ? query : {});
|
|
218
|
+
sql += where;
|
|
219
|
+
return {
|
|
220
|
+
sql,
|
|
221
|
+
values,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
upsert(tableName, query, updates) {
|
|
225
|
+
let { sql, values } = this.createWithoutReturning(tableName, [
|
|
226
|
+
Object.assign(Object.assign({}, query), updates),
|
|
227
|
+
]);
|
|
228
|
+
const { sql: whereSql, values: whereValues } = this.optionallyBuildWhere(query, values.length);
|
|
229
|
+
const queryFields = this.buildColumnListFromAllRecords([query]);
|
|
230
|
+
const updateFields = this.buildColumnListFromAllRecords([updates]);
|
|
231
|
+
sql += ` ON CONFLICT (${queryFields.join(', ')})`;
|
|
232
|
+
sql += whereSql;
|
|
233
|
+
sql += ` DO UPDATE SET ${updateFields
|
|
234
|
+
.map((f) => `${f} = EXCLUDED.${f}`)
|
|
235
|
+
.join(', ')}`;
|
|
236
|
+
sql += ' RETURNING *';
|
|
237
|
+
return {
|
|
238
|
+
sql,
|
|
239
|
+
values: [...values, ...whereValues],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
exports.default = QueryBuilder;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Database, Index, QueryOptions, UniqueIndex } from '@sprucelabs/data-stores';
|
|
2
|
+
export default class PostgresDatabase implements Database {
|
|
3
|
+
private connectionString;
|
|
4
|
+
private client;
|
|
5
|
+
private idCount;
|
|
6
|
+
private query;
|
|
7
|
+
constructor(connectionString: string);
|
|
8
|
+
dropCollection(_name: string): Promise<void>;
|
|
9
|
+
generateId(): string;
|
|
10
|
+
update(collection: string, query: Record<string, any>, updates: Record<string, any>): Promise<number>;
|
|
11
|
+
count(collection: string, query?: Record<string, any> | undefined): Promise<number>;
|
|
12
|
+
updateOne(collection: string, query: Record<string, any>, updates: Record<string, any>): Promise<Record<string, any>>;
|
|
13
|
+
private executeUpdateAndThrowIfNoResults;
|
|
14
|
+
find(collection: string, query?: Record<string, any> | undefined, options?: QueryOptions | undefined): Promise<Record<string, any>[]>;
|
|
15
|
+
createIndex(collection: string, fields: Index): Promise<void>;
|
|
16
|
+
getIndexes(collectionName: string): Promise<Index[] | UniqueIndex[]>;
|
|
17
|
+
findOne(collection: string, query?: Record<string, any> | undefined, options?: QueryOptions | undefined): Promise<Record<string, any> | null>;
|
|
18
|
+
delete(collection: string, query: Record<string, any>): Promise<number>;
|
|
19
|
+
dropDatabase(): Promise<void>;
|
|
20
|
+
private dropAllNonPrimaryKeyIndexes;
|
|
21
|
+
private getIndexNames;
|
|
22
|
+
private getTables;
|
|
23
|
+
upsertOne(collection: string, query: Record<string, any>, updates: Record<string, any>): Promise<Record<string, any>>;
|
|
24
|
+
deleteOne(collection: string, query: Record<string, any>): Promise<number>;
|
|
25
|
+
private truncateTables;
|
|
26
|
+
createOne(collection: string, values: Record<string, any>): Promise<Record<string, any>>;
|
|
27
|
+
create(collection: string, records: Record<string, any>[]): Promise<Record<string, any>[]>;
|
|
28
|
+
private executeQuery;
|
|
29
|
+
connect(): Promise<void>;
|
|
30
|
+
getUniqueIndexes(collectionName: string): Promise<UniqueIndex[]>;
|
|
31
|
+
private executeGetIndexes;
|
|
32
|
+
dropIndex(collectionName: string, fields: UniqueIndex): Promise<void>;
|
|
33
|
+
syncIndexes(collectionName: string, indexes: Index[]): Promise<void>;
|
|
34
|
+
syncUniqueIndexes(collectionName: string, indexes: UniqueIndex[]): Promise<void>;
|
|
35
|
+
private executeSyncIndexes;
|
|
36
|
+
private areIndexesEqual;
|
|
37
|
+
createUniqueIndex(collection: string, fields: UniqueIndex): Promise<void>;
|
|
38
|
+
private executeCreateIndex;
|
|
39
|
+
private generateKeyExpressions;
|
|
40
|
+
private generateKeyExpression;
|
|
41
|
+
private generateIndexName;
|
|
42
|
+
close(): Promise<void>;
|
|
43
|
+
isConnected(): boolean;
|
|
44
|
+
private parseIndexViolatedForFieldsAndValues;
|
|
45
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { DataStoresError, } from '@sprucelabs/data-stores';
|
|
11
|
+
import { assertOptions } from '@sprucelabs/schema';
|
|
12
|
+
import { Client } from 'pg';
|
|
13
|
+
import QueryBuilder from './QueryBuilder.js';
|
|
14
|
+
export default class PostgresDatabase {
|
|
15
|
+
constructor(connectionString) {
|
|
16
|
+
this.idCount = 1;
|
|
17
|
+
assertOptions({ connectionString }, ['connectionString']);
|
|
18
|
+
this.connectionString = connectionString;
|
|
19
|
+
this.query = QueryBuilder.Builder();
|
|
20
|
+
}
|
|
21
|
+
dropCollection(_name) {
|
|
22
|
+
throw new Error('Method not implemented.');
|
|
23
|
+
}
|
|
24
|
+
generateId() {
|
|
25
|
+
return `${this.idCount++}`;
|
|
26
|
+
}
|
|
27
|
+
update(collection, query, updates) {
|
|
28
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29
|
+
const { sql, values } = this.query.update(collection, query, updates, false);
|
|
30
|
+
const results = yield this.client.query({
|
|
31
|
+
text: sql,
|
|
32
|
+
values,
|
|
33
|
+
});
|
|
34
|
+
return results.rowCount;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
count(collection, query) {
|
|
38
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
39
|
+
const { sql, values } = this.query.find(collection, query !== null && query !== void 0 ? query : {}, {
|
|
40
|
+
includeFields: ['count(*) as count'],
|
|
41
|
+
});
|
|
42
|
+
const results = yield this.client.query({
|
|
43
|
+
text: sql,
|
|
44
|
+
values,
|
|
45
|
+
});
|
|
46
|
+
return parseInt(results.rows[0].count);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
updateOne(collection, query, updates) {
|
|
50
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
51
|
+
const action = 'updateOne';
|
|
52
|
+
const record = yield this.executeUpdateAndThrowIfNoResults(collection, query, updates, action);
|
|
53
|
+
return record;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
executeUpdateAndThrowIfNoResults(collection, query, updates, action) {
|
|
57
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
58
|
+
const { sql, values } = this.query.update(collection, query, updates);
|
|
59
|
+
const results = yield this.executeQuery(action, sql, values);
|
|
60
|
+
if (results.rowCount === 0) {
|
|
61
|
+
throw new DataStoresError({
|
|
62
|
+
code: 'RECORD_NOT_FOUND',
|
|
63
|
+
storeName: collection,
|
|
64
|
+
query,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const record = results.rows[0];
|
|
68
|
+
return record;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
find(collection, query, options) {
|
|
72
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
73
|
+
const { sql, values } = this.query.find(collection, query !== null && query !== void 0 ? query : {}, options);
|
|
74
|
+
const results = yield this.client.query({
|
|
75
|
+
text: sql,
|
|
76
|
+
values,
|
|
77
|
+
});
|
|
78
|
+
return results.rows;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
createIndex(collection, fields) {
|
|
82
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
83
|
+
return this.executeCreateIndex(collection, fields, false);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
getIndexes(collectionName) {
|
|
87
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
88
|
+
return this.executeGetIndexes(collectionName, false);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
findOne(collection, query, options) {
|
|
92
|
+
var _a;
|
|
93
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
94
|
+
const results = yield this.find(collection, query, Object.assign(Object.assign({}, options), { limit: 1 }));
|
|
95
|
+
return (_a = results[0]) !== null && _a !== void 0 ? _a : null;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
delete(collection, query) {
|
|
99
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
100
|
+
const { sql, values } = this.query.delete(collection, query);
|
|
101
|
+
const results = yield this.client.query({
|
|
102
|
+
text: sql,
|
|
103
|
+
values,
|
|
104
|
+
});
|
|
105
|
+
return results.rowCount;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
dropDatabase() {
|
|
109
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
110
|
+
yield this.truncateTables();
|
|
111
|
+
const names = yield this.getTables();
|
|
112
|
+
for (const name of names) {
|
|
113
|
+
yield this.dropAllNonPrimaryKeyIndexes(name);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
dropAllNonPrimaryKeyIndexes(name) {
|
|
118
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
119
|
+
const indexNames = yield this.getIndexNames(name);
|
|
120
|
+
for (const indexName of indexNames) {
|
|
121
|
+
yield this.client.query(`DROP INDEX ${indexName}`);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
getIndexNames(name) {
|
|
126
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
127
|
+
const sql = `SELECT indexname FROM pg_indexes WHERE tablename = '${name}' AND indexname != '${name}_pk';`;
|
|
128
|
+
const results = yield this.client.query(sql);
|
|
129
|
+
const indexNames = results.rows.map((row) => row.indexname);
|
|
130
|
+
return indexNames;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
getTables() {
|
|
134
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
135
|
+
const sql = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE';`;
|
|
136
|
+
const results = yield this.client.query(sql);
|
|
137
|
+
const names = results.rows.map((row) => row.table_name);
|
|
138
|
+
return names;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
upsertOne(collection, query, updates) {
|
|
142
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
143
|
+
const [match] = yield this.find(collection, query, { limit: 1 });
|
|
144
|
+
if (match) {
|
|
145
|
+
return this.executeUpdateAndThrowIfNoResults(collection, { id: match.id }, updates, 'upsertOne');
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
return this.createOne(collection, updates);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
deleteOne(collection, query) {
|
|
153
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
154
|
+
if (!query.id) {
|
|
155
|
+
const match = yield this.findOne(collection, query, {
|
|
156
|
+
includeFields: ['id'],
|
|
157
|
+
});
|
|
158
|
+
query = { id: match === null || match === void 0 ? void 0 : match.id };
|
|
159
|
+
}
|
|
160
|
+
let { sql, values } = this.query.delete(collection, query);
|
|
161
|
+
const results = yield this.client.query({
|
|
162
|
+
text: sql,
|
|
163
|
+
values,
|
|
164
|
+
});
|
|
165
|
+
return results.rowCount;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
truncateTables() {
|
|
169
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
170
|
+
const res = yield this.client.query(`
|
|
171
|
+
SELECT table_name
|
|
172
|
+
FROM information_schema.tables
|
|
173
|
+
WHERE table_schema = 'public'
|
|
174
|
+
AND table_type = 'BASE TABLE';
|
|
175
|
+
`);
|
|
176
|
+
const tableNames = res.rows.map((row) => row.table_name);
|
|
177
|
+
yield Promise.all(tableNames.map((tableName) => this.client.query(`TRUNCATE TABLE ${tableName} RESTART IDENTITY CASCADE`)));
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
createOne(collection, values) {
|
|
181
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
182
|
+
const rows = yield this.create(collection, [values]);
|
|
183
|
+
return rows[0];
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
create(collection, records) {
|
|
187
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
188
|
+
if (records.length === 0) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const { sql, values } = this.query.create(collection, records);
|
|
192
|
+
const { rows } = yield this.executeQuery('create', sql, values);
|
|
193
|
+
return rows;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
executeQuery(action, sql, values) {
|
|
197
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
198
|
+
try {
|
|
199
|
+
const results = yield this.client.query({
|
|
200
|
+
text: sql,
|
|
201
|
+
values,
|
|
202
|
+
});
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
const parsed = this.parseIndexViolatedForFieldsAndValues(err === null || err === void 0 ? void 0 : err.detail);
|
|
207
|
+
if (parsed) {
|
|
208
|
+
const { fields, values } = parsed;
|
|
209
|
+
throw new DataStoresError({
|
|
210
|
+
code: 'DUPLICATE_RECORD',
|
|
211
|
+
duplicateFields: fields,
|
|
212
|
+
duplicateValues: values,
|
|
213
|
+
collectionName: 'test_collection',
|
|
214
|
+
action,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
connect() {
|
|
222
|
+
var _a;
|
|
223
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
224
|
+
const client = new Client({
|
|
225
|
+
connectionString: this.connectionString,
|
|
226
|
+
});
|
|
227
|
+
try {
|
|
228
|
+
yield client.connect();
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
const message = err.message;
|
|
232
|
+
if (message === null || message === void 0 ? void 0 : message.includes('ECONNREFUSED')) {
|
|
233
|
+
throw new DataStoresError({
|
|
234
|
+
code: 'UNABLE_TO_CONNECT_TO_DB',
|
|
235
|
+
originalError: err,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (message === null || message === void 0 ? void 0 : message.includes('does not exist')) {
|
|
239
|
+
const match = (_a = message.match(/"([^"]*)"/)) !== null && _a !== void 0 ? _a : ['', ''];
|
|
240
|
+
throw new DataStoresError({
|
|
241
|
+
code: 'INVALID_DATABASE_NAME',
|
|
242
|
+
suppliedName: match[1],
|
|
243
|
+
originalError: err,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
throw new DataStoresError({
|
|
247
|
+
code: 'INVALID_DB_CONNECTION_STRING',
|
|
248
|
+
originalError: err,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
this.client = client;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
getUniqueIndexes(collectionName) {
|
|
255
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
256
|
+
const isUnique = true;
|
|
257
|
+
const uniqueIndexes = yield this.executeGetIndexes(collectionName, isUnique);
|
|
258
|
+
return uniqueIndexes;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
executeGetIndexes(collectionName, isUnique) {
|
|
262
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
263
|
+
const query = `SELECT indexname FROM pg_indexes WHERE tablename = '${collectionName}' AND indexdef ${isUnique ? '' : 'NOT'} LIKE '%UNIQUE%';`;
|
|
264
|
+
const res = yield this.client.query(query);
|
|
265
|
+
const uniqueIndexes = [];
|
|
266
|
+
res.rows.forEach((row) => {
|
|
267
|
+
const indexName = row.indexname.replace(`${collectionName}_`, '');
|
|
268
|
+
const fields = indexName.split('_').slice(0, -1);
|
|
269
|
+
if (fields.length > 0) {
|
|
270
|
+
uniqueIndexes.push(fields);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return uniqueIndexes;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
dropIndex(collectionName, fields) {
|
|
277
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
278
|
+
const indexName = this.generateIndexName(collectionName, fields);
|
|
279
|
+
const query = `DROP INDEX ${indexName}`;
|
|
280
|
+
try {
|
|
281
|
+
yield this.client.query({
|
|
282
|
+
text: query,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
throw new DataStoresError({
|
|
287
|
+
code: 'INDEX_NOT_FOUND',
|
|
288
|
+
missingIndex: fields,
|
|
289
|
+
collectionName: 'test_collection',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
syncIndexes(collectionName, indexes) {
|
|
295
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
296
|
+
yield this.executeSyncIndexes(collectionName, indexes, false);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
syncUniqueIndexes(collectionName, indexes) {
|
|
300
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
301
|
+
yield this.executeSyncIndexes(collectionName, indexes, true);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
executeSyncIndexes(collectionName, indexes, isUnique) {
|
|
305
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
306
|
+
const existingIndexes = yield this.executeGetIndexes(collectionName, isUnique);
|
|
307
|
+
const indexesToAdd = indexes.filter((index) => !existingIndexes.find((existing) => this.areIndexesEqual(existing, index)));
|
|
308
|
+
const indexesToRemove = existingIndexes.filter((existing) => !indexes.find((index) => this.areIndexesEqual(existing, index)));
|
|
309
|
+
yield Promise.all([
|
|
310
|
+
...indexesToAdd.map((index) => __awaiter(this, void 0, void 0, function* () {
|
|
311
|
+
var _a;
|
|
312
|
+
try {
|
|
313
|
+
yield this.executeCreateIndex(collectionName, index, isUnique);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
if (((_a = err.options) === null || _a === void 0 ? void 0 : _a.code) !== 'INDEX_EXISTS') {
|
|
317
|
+
throw new DataStoresError({
|
|
318
|
+
code: 'DUPLICATE_KEY',
|
|
319
|
+
originalError: err,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
})),
|
|
325
|
+
...indexesToRemove.map((index) => this.dropIndex(collectionName, index)),
|
|
326
|
+
]);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
areIndexesEqual(existing, index) {
|
|
330
|
+
return (this.generateIndexName('any', existing) ===
|
|
331
|
+
this.generateIndexName('any', index));
|
|
332
|
+
}
|
|
333
|
+
createUniqueIndex(collection, fields) {
|
|
334
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
335
|
+
const isUnique = true;
|
|
336
|
+
yield this.executeCreateIndex(collection, fields, isUnique);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
executeCreateIndex(collection, fields, isUnique) {
|
|
340
|
+
var _a, _b;
|
|
341
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
342
|
+
const indexName = this.generateIndexName(collection, fields);
|
|
343
|
+
const keys = this.generateKeyExpressions(fields);
|
|
344
|
+
const query = `CREATE ${isUnique ? `UNIQUE` : ''} INDEX ${indexName} ON ${collection} (${keys})`;
|
|
345
|
+
try {
|
|
346
|
+
yield this.client.query({
|
|
347
|
+
text: query,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
if ((_b = (_a = err.message) === null || _a === void 0 ? void 0 : _a.includes) === null || _b === void 0 ? void 0 : _b.call(_a, 'already exists')) {
|
|
352
|
+
throw new DataStoresError({
|
|
353
|
+
code: 'INDEX_EXISTS',
|
|
354
|
+
collectionName: 'test_collection',
|
|
355
|
+
index: ['uniqueField'],
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
throw err;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
generateKeyExpressions(fields) {
|
|
363
|
+
return fields.map((f) => this.generateKeyExpression(f)).join(', ');
|
|
364
|
+
}
|
|
365
|
+
generateKeyExpression(field) {
|
|
366
|
+
if (field.includes('.')) {
|
|
367
|
+
const parts = field.split('.');
|
|
368
|
+
return `(${parts[0]}->>'${parts[1]}')`;
|
|
369
|
+
}
|
|
370
|
+
return field;
|
|
371
|
+
}
|
|
372
|
+
generateIndexName(collection, fields) {
|
|
373
|
+
return `${collection}_${fields
|
|
374
|
+
.map((f) => f.toLowerCase())
|
|
375
|
+
.join('_')}${'_index'}`.replace(/\./g, '_');
|
|
376
|
+
}
|
|
377
|
+
close() {
|
|
378
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
379
|
+
yield this.client.end();
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
isConnected() {
|
|
383
|
+
//@ts-ignore
|
|
384
|
+
return this.client._connected && !this.client._ending;
|
|
385
|
+
}
|
|
386
|
+
parseIndexViolatedForFieldsAndValues(input) {
|
|
387
|
+
const regex = /Key \((.*)\)=\((.*)\) already exists\./;
|
|
388
|
+
const matches = input === null || input === void 0 ? void 0 : input.match(regex);
|
|
389
|
+
if (!matches) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
const fieldsStr = matches[1];
|
|
393
|
+
const valuesStr = matches[2];
|
|
394
|
+
const fields = fieldsStr.split(', ');
|
|
395
|
+
const values = valuesStr.split(', ');
|
|
396
|
+
const fixedFields = fields.map((field) => field
|
|
397
|
+
.replace(/ ->> /g, '.')
|
|
398
|
+
.split('::')[0]
|
|
399
|
+
.replace('(', '')
|
|
400
|
+
.replace(/'/g, ''));
|
|
401
|
+
const result = { fields: fixedFields, values };
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
}
|