@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 ADDED
@@ -0,0 +1,20 @@
1
+ <img src="https://raw.githubusercontent.com/sprucelabsai/mercury-types/master/docs/images/hero.jpg">
2
+
3
+ <h1 align="center">
4
+ Spruce XP Documentation
5
+ </h1>
6
+ <h3 align="center">Spruce XP is an Experience Platform built to create unforgettable experiences and long-lasting relationships.
7
+ </h3>
8
+ <p align="center">
9
+ <img align="center" width="80%" src="https://raw.githubusercontent.com/sprucelabsai/mercury-types/master/docs/images/bullets.jpg">
10
+ </p>
11
+ <img src="https://raw.githubusercontent.com/sprucelabsai/mercury-types/master/docs/images/spacer.png">
12
+ <br />
13
+ <p align="center">
14
+ <img align="center" width="80%" src="https://raw.githubusercontent.com/sprucelabsai/mercury-types/master/docs/images/sprucebot-message.png">
15
+ </p>
16
+
17
+ <br />
18
+ <p align="center">
19
+ <a href="https://developer.spruce.ai/"><img width="250" src="https://raw.githubusercontent.com/sprucelabsai/mercury-types/master/docs/images/read-full-docs.png" /></a>
20
+ </p>
@@ -0,0 +1,11 @@
1
+ {
2
+ "scriptUpdater": {
3
+ "skipped": []
4
+ },
5
+ "skipped": [
6
+ "skill"
7
+ ],
8
+ "installed": [
9
+ "test"
10
+ ]
11
+ }
@@ -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,343 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const data_stores_1 = require("@sprucelabs/data-stores");
7
+ const schema_1 = require("@sprucelabs/schema");
8
+ const pg_1 = require("pg");
9
+ const QueryBuilder_1 = __importDefault(require("./QueryBuilder"));
10
+ class PostgresDatabase {
11
+ constructor(connectionString) {
12
+ this.idCount = 1;
13
+ (0, schema_1.assertOptions)({ connectionString }, ['connectionString']);
14
+ this.connectionString = connectionString;
15
+ this.query = QueryBuilder_1.default.Builder();
16
+ }
17
+ dropCollection(_name) {
18
+ throw new Error('Method not implemented.');
19
+ }
20
+ generateId() {
21
+ return `${this.idCount++}`;
22
+ }
23
+ async update(collection, query, updates) {
24
+ const { sql, values } = this.query.update(collection, query, updates, false);
25
+ const results = await this.client.query({
26
+ text: sql,
27
+ values,
28
+ });
29
+ return results.rowCount;
30
+ }
31
+ async count(collection, query) {
32
+ const { sql, values } = this.query.find(collection, query !== null && query !== void 0 ? query : {}, {
33
+ includeFields: ['count(*) as count'],
34
+ });
35
+ const results = await this.client.query({
36
+ text: sql,
37
+ values,
38
+ });
39
+ return parseInt(results.rows[0].count);
40
+ }
41
+ async updateOne(collection, query, updates) {
42
+ const action = 'updateOne';
43
+ const record = await this.executeUpdateAndThrowIfNoResults(collection, query, updates, action);
44
+ return record;
45
+ }
46
+ async executeUpdateAndThrowIfNoResults(collection, query, updates, action) {
47
+ const { sql, values } = this.query.update(collection, query, updates);
48
+ const results = await this.executeQuery(action, sql, values);
49
+ if (results.rowCount === 0) {
50
+ throw new data_stores_1.DataStoresError({
51
+ code: 'RECORD_NOT_FOUND',
52
+ storeName: collection,
53
+ query,
54
+ });
55
+ }
56
+ const record = results.rows[0];
57
+ return record;
58
+ }
59
+ async find(collection, query, options) {
60
+ const { sql, values } = this.query.find(collection, query !== null && query !== void 0 ? query : {}, options);
61
+ const results = await this.client.query({
62
+ text: sql,
63
+ values,
64
+ });
65
+ return results.rows;
66
+ }
67
+ async createIndex(collection, fields) {
68
+ return this.executeCreateIndex(collection, fields, false);
69
+ }
70
+ async getIndexes(collectionName) {
71
+ return this.executeGetIndexes(collectionName, false);
72
+ }
73
+ async findOne(collection, query, options) {
74
+ var _a;
75
+ const results = await this.find(collection, query, Object.assign(Object.assign({}, options), { limit: 1 }));
76
+ return (_a = results[0]) !== null && _a !== void 0 ? _a : null;
77
+ }
78
+ async delete(collection, query) {
79
+ const { sql, values } = this.query.delete(collection, query);
80
+ const results = await this.client.query({
81
+ text: sql,
82
+ values,
83
+ });
84
+ return results.rowCount;
85
+ }
86
+ async dropDatabase() {
87
+ await this.truncateTables();
88
+ const names = await this.getTables();
89
+ for (const name of names) {
90
+ await this.dropAllNonPrimaryKeyIndexes(name);
91
+ }
92
+ }
93
+ async dropAllNonPrimaryKeyIndexes(name) {
94
+ const indexNames = await this.getIndexNames(name);
95
+ for (const indexName of indexNames) {
96
+ await this.client.query(`DROP INDEX ${indexName}`);
97
+ }
98
+ }
99
+ async getIndexNames(name) {
100
+ const sql = `SELECT indexname FROM pg_indexes WHERE tablename = '${name}' AND indexname != '${name}_pk';`;
101
+ const results = await this.client.query(sql);
102
+ const indexNames = results.rows.map((row) => row.indexname);
103
+ return indexNames;
104
+ }
105
+ async getTables() {
106
+ const sql = `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE';`;
107
+ const results = await this.client.query(sql);
108
+ const names = results.rows.map((row) => row.table_name);
109
+ return names;
110
+ }
111
+ async upsertOne(collection, query, updates) {
112
+ const [match] = await this.find(collection, query, { limit: 1 });
113
+ if (match) {
114
+ return this.executeUpdateAndThrowIfNoResults(collection, { id: match.id }, updates, 'upsertOne');
115
+ }
116
+ else {
117
+ return this.createOne(collection, updates);
118
+ }
119
+ }
120
+ async deleteOne(collection, query) {
121
+ if (!query.id) {
122
+ const match = await this.findOne(collection, query, {
123
+ includeFields: ['id'],
124
+ });
125
+ query = { id: match === null || match === void 0 ? void 0 : match.id };
126
+ }
127
+ let { sql, values } = this.query.delete(collection, query);
128
+ const results = await this.client.query({
129
+ text: sql,
130
+ values,
131
+ });
132
+ return results.rowCount;
133
+ }
134
+ async truncateTables() {
135
+ const res = await this.client.query(`
136
+ SELECT table_name
137
+ FROM information_schema.tables
138
+ WHERE table_schema = 'public'
139
+ AND table_type = 'BASE TABLE';
140
+ `);
141
+ const tableNames = res.rows.map((row) => row.table_name);
142
+ await Promise.all(tableNames.map((tableName) => this.client.query(`TRUNCATE TABLE ${tableName} RESTART IDENTITY CASCADE`)));
143
+ }
144
+ async createOne(collection, values) {
145
+ const rows = await this.create(collection, [values]);
146
+ return rows[0];
147
+ }
148
+ async create(collection, records) {
149
+ if (records.length === 0) {
150
+ return [];
151
+ }
152
+ const { sql, values } = this.query.create(collection, records);
153
+ const { rows } = await this.executeQuery('create', sql, values);
154
+ return rows;
155
+ }
156
+ async executeQuery(action, sql, values) {
157
+ try {
158
+ const results = await this.client.query({
159
+ text: sql,
160
+ values,
161
+ });
162
+ return results;
163
+ }
164
+ catch (err) {
165
+ const parsed = this.parseIndexViolatedForFieldsAndValues(err === null || err === void 0 ? void 0 : err.detail);
166
+ if (parsed) {
167
+ const { fields, values } = parsed;
168
+ throw new data_stores_1.DataStoresError({
169
+ code: 'DUPLICATE_RECORD',
170
+ duplicateFields: fields,
171
+ duplicateValues: values,
172
+ collectionName: 'test_collection',
173
+ action,
174
+ });
175
+ }
176
+ throw err;
177
+ }
178
+ }
179
+ async connect() {
180
+ var _a;
181
+ const client = new pg_1.Client({
182
+ connectionString: this.connectionString,
183
+ });
184
+ try {
185
+ await client.connect();
186
+ }
187
+ catch (err) {
188
+ const message = err.message;
189
+ if (message === null || message === void 0 ? void 0 : message.includes('ECONNREFUSED')) {
190
+ throw new data_stores_1.DataStoresError({
191
+ code: 'UNABLE_TO_CONNECT_TO_DB',
192
+ originalError: err,
193
+ });
194
+ }
195
+ if (message === null || message === void 0 ? void 0 : message.includes('does not exist')) {
196
+ const match = (_a = message.match(/"([^"]*)"/)) !== null && _a !== void 0 ? _a : ['', ''];
197
+ throw new data_stores_1.DataStoresError({
198
+ code: 'INVALID_DATABASE_NAME',
199
+ suppliedName: match[1],
200
+ originalError: err,
201
+ });
202
+ }
203
+ throw new data_stores_1.DataStoresError({
204
+ code: 'INVALID_DB_CONNECTION_STRING',
205
+ originalError: err,
206
+ });
207
+ }
208
+ this.client = client;
209
+ }
210
+ async getUniqueIndexes(collectionName) {
211
+ const isUnique = true;
212
+ const uniqueIndexes = await this.executeGetIndexes(collectionName, isUnique);
213
+ return uniqueIndexes;
214
+ }
215
+ async executeGetIndexes(collectionName, isUnique) {
216
+ const query = `SELECT indexname FROM pg_indexes WHERE tablename = '${collectionName}' AND indexdef ${isUnique ? '' : 'NOT'} LIKE '%UNIQUE%';`;
217
+ const res = await this.client.query(query);
218
+ const uniqueIndexes = [];
219
+ res.rows.forEach((row) => {
220
+ const indexName = row.indexname.replace(`${collectionName}_`, '');
221
+ const fields = indexName.split('_').slice(0, -1);
222
+ if (fields.length > 0) {
223
+ uniqueIndexes.push(fields);
224
+ }
225
+ });
226
+ return uniqueIndexes;
227
+ }
228
+ async dropIndex(collectionName, fields) {
229
+ const indexName = this.generateIndexName(collectionName, fields);
230
+ const query = `DROP INDEX ${indexName}`;
231
+ try {
232
+ await this.client.query({
233
+ text: query,
234
+ });
235
+ }
236
+ catch (err) {
237
+ throw new data_stores_1.DataStoresError({
238
+ code: 'INDEX_NOT_FOUND',
239
+ missingIndex: fields,
240
+ collectionName: 'test_collection',
241
+ });
242
+ }
243
+ }
244
+ async syncIndexes(collectionName, indexes) {
245
+ await this.executeSyncIndexes(collectionName, indexes, false);
246
+ }
247
+ async syncUniqueIndexes(collectionName, indexes) {
248
+ await this.executeSyncIndexes(collectionName, indexes, true);
249
+ }
250
+ async executeSyncIndexes(collectionName, indexes, isUnique) {
251
+ const existingIndexes = await this.executeGetIndexes(collectionName, isUnique);
252
+ const indexesToAdd = indexes.filter((index) => !existingIndexes.find((existing) => this.areIndexesEqual(existing, index)));
253
+ const indexesToRemove = existingIndexes.filter((existing) => !indexes.find((index) => this.areIndexesEqual(existing, index)));
254
+ await Promise.all([
255
+ ...indexesToAdd.map(async (index) => {
256
+ var _a;
257
+ try {
258
+ await this.executeCreateIndex(collectionName, index, isUnique);
259
+ }
260
+ catch (err) {
261
+ if (((_a = err.options) === null || _a === void 0 ? void 0 : _a.code) !== 'INDEX_EXISTS') {
262
+ throw new data_stores_1.DataStoresError({
263
+ code: 'DUPLICATE_KEY',
264
+ originalError: err,
265
+ });
266
+ }
267
+ }
268
+ return null;
269
+ }),
270
+ ...indexesToRemove.map((index) => this.dropIndex(collectionName, index)),
271
+ ]);
272
+ }
273
+ areIndexesEqual(existing, index) {
274
+ return (this.generateIndexName('any', existing) ===
275
+ this.generateIndexName('any', index));
276
+ }
277
+ async createUniqueIndex(collection, fields) {
278
+ const isUnique = true;
279
+ await this.executeCreateIndex(collection, fields, isUnique);
280
+ }
281
+ async executeCreateIndex(collection, fields, isUnique) {
282
+ var _a, _b;
283
+ const indexName = this.generateIndexName(collection, fields);
284
+ const keys = this.generateKeyExpressions(fields);
285
+ const query = `CREATE ${isUnique ? `UNIQUE` : ''} INDEX ${indexName} ON ${collection} (${keys})`;
286
+ try {
287
+ await this.client.query({
288
+ text: query,
289
+ });
290
+ }
291
+ catch (err) {
292
+ if ((_b = (_a = err.message) === null || _a === void 0 ? void 0 : _a.includes) === null || _b === void 0 ? void 0 : _b.call(_a, 'already exists')) {
293
+ throw new data_stores_1.DataStoresError({
294
+ code: 'INDEX_EXISTS',
295
+ collectionName: 'test_collection',
296
+ index: ['uniqueField'],
297
+ });
298
+ }
299
+ throw err;
300
+ }
301
+ }
302
+ generateKeyExpressions(fields) {
303
+ return fields.map((f) => this.generateKeyExpression(f)).join(', ');
304
+ }
305
+ generateKeyExpression(field) {
306
+ if (field.includes('.')) {
307
+ const parts = field.split('.');
308
+ return `(${parts[0]}->>'${parts[1]}')`;
309
+ }
310
+ return field;
311
+ }
312
+ generateIndexName(collection, fields) {
313
+ return `${collection}_${fields
314
+ .map((f) => f.toLowerCase())
315
+ .join('_')}${'_index'}`.replace(/\./g, '_');
316
+ }
317
+ async close() {
318
+ await this.client.end();
319
+ }
320
+ isConnected() {
321
+ //@ts-ignore
322
+ return this.client._connected && !this.client._ending;
323
+ }
324
+ parseIndexViolatedForFieldsAndValues(input) {
325
+ const regex = /Key \((.*)\)=\((.*)\) already exists\./;
326
+ const matches = input === null || input === void 0 ? void 0 : input.match(regex);
327
+ if (!matches) {
328
+ return null;
329
+ }
330
+ const fieldsStr = matches[1];
331
+ const valuesStr = matches[2];
332
+ const fields = fieldsStr.split(', ');
333
+ const values = valuesStr.split(', ');
334
+ const fixedFields = fields.map((field) => field
335
+ .replace(/ ->> /g, '.')
336
+ .split('::')[0]
337
+ .replace('(', '')
338
+ .replace(/'/g, ''));
339
+ const result = { fields: fixedFields, values };
340
+ return result;
341
+ }
342
+ }
343
+ exports.default = PostgresDatabase;
@@ -0,0 +1,37 @@
1
+ import { QueryOptions } from '@sprucelabs/data-stores';
2
+ import { Query } from './postgres.types';
3
+ export default class QueryBuilder {
4
+ private constructor();
5
+ static Builder(): QueryBuilder;
6
+ find(tableName: string, query: Query, options?: QueryOptions): BuiltQuery;
7
+ private optionallyBuildWhere;
8
+ private buildSetClause;
9
+ private buildSetClausFor$Or;
10
+ create(tableName: string, records: Record<string, any>[]): BuiltQuery;
11
+ createWithoutReturning(tableName: string, records: Record<string, any>[]): BuiltQuery;
12
+ private splitRecordsIntoFieldsPlaceholdersAndValues;
13
+ private fieldValueToSqlValue;
14
+ private normalizeValue;
15
+ private isValueObject;
16
+ private buildColumnListFromAllRecords;
17
+ private optionallyBuildSort;
18
+ private optionallyBuildSkip;
19
+ private optionallyBuildLimit;
20
+ private buildColumnListFromIncludeFields;
21
+ update(tableName: string, query: Query, updates: Record<string, any>, shouldReturnUpdatedRecords?: boolean): {
22
+ sql: string;
23
+ values: unknown[];
24
+ };
25
+ delete(tableName: string, query?: Query): {
26
+ sql: string;
27
+ values: unknown[];
28
+ };
29
+ upsert(tableName: string, query: Query, updates: Record<string, any>): {
30
+ sql: string;
31
+ values: unknown[];
32
+ };
33
+ }
34
+ export interface BuiltQuery {
35
+ sql: string;
36
+ values: unknown[];
37
+ }