@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,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
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
export default class QueryBuilder {
|
|
2
|
+
constructor() { }
|
|
3
|
+
static Builder() {
|
|
4
|
+
return new this();
|
|
5
|
+
}
|
|
6
|
+
find(tableName, query, options) {
|
|
7
|
+
const { includeFields, limit, skip, sort } = options !== null && options !== void 0 ? options : {};
|
|
8
|
+
const fields = this.buildColumnListFromIncludeFields(includeFields);
|
|
9
|
+
let sql = `SELECT ${fields} FROM ${tableName}`;
|
|
10
|
+
const { values, sql: where } = this.optionallyBuildWhere(query);
|
|
11
|
+
sql += where;
|
|
12
|
+
sql += this.optionallyBuildSkip(skip);
|
|
13
|
+
sql += this.optionallyBuildSort(sort);
|
|
14
|
+
sql += this.optionallyBuildLimit(limit);
|
|
15
|
+
return { sql, values };
|
|
16
|
+
}
|
|
17
|
+
optionallyBuildWhere(query, startingPlaceholderCount = 0) {
|
|
18
|
+
let sql = '';
|
|
19
|
+
const values = [];
|
|
20
|
+
const queryKeys = Object.keys(query);
|
|
21
|
+
if ((queryKeys !== null && queryKeys !== void 0 ? queryKeys : []).length > 0) {
|
|
22
|
+
const { set: columnSpecs, values: whereValues } = this.buildSetClause({
|
|
23
|
+
query,
|
|
24
|
+
startingCount: startingPlaceholderCount,
|
|
25
|
+
isBuildingWhere: true,
|
|
26
|
+
});
|
|
27
|
+
sql += ` WHERE ${columnSpecs.join(' AND ')}`;
|
|
28
|
+
values.push(...whereValues);
|
|
29
|
+
}
|
|
30
|
+
return { values, sql };
|
|
31
|
+
}
|
|
32
|
+
buildSetClause(options) {
|
|
33
|
+
const { query, startingCount = 0, placeholderTemplate = '${{count}}', isBuildingWhere = false, } = options;
|
|
34
|
+
let placeholderCount = startingCount;
|
|
35
|
+
const queryKeys = Object.keys(query);
|
|
36
|
+
const values = [];
|
|
37
|
+
const set = [];
|
|
38
|
+
queryKeys.forEach((k) => {
|
|
39
|
+
let value = query[k];
|
|
40
|
+
if (value === null || value === void 0 ? void 0 : value.$in) {
|
|
41
|
+
values.push(...value.$in.map((v) => this.normalizeValue(v)));
|
|
42
|
+
set.push(`${k} IN (${value.$in
|
|
43
|
+
.map(() => `$${++placeholderCount}`)
|
|
44
|
+
.join(', ')})`);
|
|
45
|
+
}
|
|
46
|
+
else if (value === null || value === void 0 ? void 0 : value.$regex) {
|
|
47
|
+
values.push(this.normalizeValue(value.$regex));
|
|
48
|
+
set.push(`${k} ~* $${++placeholderCount}`);
|
|
49
|
+
}
|
|
50
|
+
else if (value === null || value === void 0 ? void 0 : value.$lte) {
|
|
51
|
+
values.push(this.normalizeValue(value.$lte));
|
|
52
|
+
set.push(`${k} <= $${++placeholderCount}`);
|
|
53
|
+
}
|
|
54
|
+
else if (value === null || value === void 0 ? void 0 : value.$lt) {
|
|
55
|
+
values.push(this.normalizeValue(value.$lt));
|
|
56
|
+
set.push(`${k} < $${++placeholderCount}`);
|
|
57
|
+
}
|
|
58
|
+
else if (value === null || value === void 0 ? void 0 : value.$gte) {
|
|
59
|
+
values.push(this.normalizeValue(value.$gte));
|
|
60
|
+
set.push(`${k} >= $${++placeholderCount}`);
|
|
61
|
+
}
|
|
62
|
+
else if (value === null || value === void 0 ? void 0 : value.$gt) {
|
|
63
|
+
values.push(this.normalizeValue(value.$gt));
|
|
64
|
+
set.push(`${k} > $${++placeholderCount}`);
|
|
65
|
+
}
|
|
66
|
+
else if (value === null || value === void 0 ? void 0 : value.$ne) {
|
|
67
|
+
values.push(this.normalizeValue(value.$ne));
|
|
68
|
+
set.push(`${k} != $${++placeholderCount}`);
|
|
69
|
+
}
|
|
70
|
+
else if (k === '$or') {
|
|
71
|
+
const { set: orWheres, values: orValues } = this.buildSetClausFor$Or(value, placeholderCount);
|
|
72
|
+
set.push(`(${orWheres.join(' OR ')})`);
|
|
73
|
+
values.push(...orValues);
|
|
74
|
+
}
|
|
75
|
+
else if (k === '$push') {
|
|
76
|
+
const sub = this.buildSetClause({
|
|
77
|
+
query: value,
|
|
78
|
+
startingCount: placeholderCount++,
|
|
79
|
+
placeholderTemplate: `JSONB_SET(COALESCE(names || \${{count}}::JSONB, '[]'::JSONB), '{-1}', \${{count}}::JSONB)`,
|
|
80
|
+
});
|
|
81
|
+
values.push(...sub.values.map((v) => JSON.stringify(v)));
|
|
82
|
+
set.push(...sub.set);
|
|
83
|
+
}
|
|
84
|
+
else if (value === null || value === undefined) {
|
|
85
|
+
set.push(`${k} IS NULL`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
placeholderCount++;
|
|
89
|
+
let placeholder = placeholderTemplate
|
|
90
|
+
.replace(/{{count}}/gm, `${placeholderCount}`)
|
|
91
|
+
.replace(/{{fieldName}}/gm, k);
|
|
92
|
+
const isDotSearch = k.includes('.');
|
|
93
|
+
if (isDotSearch) {
|
|
94
|
+
const [field, prop] = k.split('.');
|
|
95
|
+
if (isBuildingWhere) {
|
|
96
|
+
k = `${field} ->> '${prop}'`;
|
|
97
|
+
value = `${value}`;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
k = field;
|
|
101
|
+
placeholder = `${k} || jsonb_build_object('${prop}', ${placeholder}::text)`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
values.push(this.normalizeValue(value));
|
|
105
|
+
set.push(`${k} = ${placeholder}`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return { set, values };
|
|
109
|
+
}
|
|
110
|
+
buildSetClausFor$Or(value, placeholderCount) {
|
|
111
|
+
const ors = [];
|
|
112
|
+
const orValues = [];
|
|
113
|
+
value.forEach((q) => {
|
|
114
|
+
const { set: where, values } = this.buildSetClause({
|
|
115
|
+
query: q,
|
|
116
|
+
startingCount: placeholderCount++,
|
|
117
|
+
});
|
|
118
|
+
ors.push(...where);
|
|
119
|
+
orValues.push(...values);
|
|
120
|
+
});
|
|
121
|
+
return { set: ors, values: orValues };
|
|
122
|
+
}
|
|
123
|
+
create(tableName, records) {
|
|
124
|
+
let { sql, values } = this.createWithoutReturning(tableName, records);
|
|
125
|
+
sql += ` RETURNING *`;
|
|
126
|
+
return {
|
|
127
|
+
sql,
|
|
128
|
+
values,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
createWithoutReturning(tableName, records) {
|
|
132
|
+
const { fields, placeholders, values } = this.splitRecordsIntoFieldsPlaceholdersAndValues(records);
|
|
133
|
+
const sql = `INSERT INTO ${tableName} (${fields.join(', ')}) VALUES ${placeholders.join(', ')}`;
|
|
134
|
+
return { sql, values };
|
|
135
|
+
}
|
|
136
|
+
splitRecordsIntoFieldsPlaceholdersAndValues(records) {
|
|
137
|
+
const fields = this.buildColumnListFromAllRecords(records);
|
|
138
|
+
let placeholderCount = 0;
|
|
139
|
+
const values = [];
|
|
140
|
+
const placeholders = records.map((record) => {
|
|
141
|
+
const placeholders = [];
|
|
142
|
+
fields.forEach((f) => {
|
|
143
|
+
values.push(this.fieldValueToSqlValue(record, f));
|
|
144
|
+
let placeholder = `$${++placeholderCount}`;
|
|
145
|
+
if (this.isValueObject(record[f])) {
|
|
146
|
+
placeholder += `::json`;
|
|
147
|
+
}
|
|
148
|
+
placeholders.push(placeholder);
|
|
149
|
+
});
|
|
150
|
+
return `(${placeholders.join(', ')})`;
|
|
151
|
+
});
|
|
152
|
+
return { fields, placeholders, values };
|
|
153
|
+
}
|
|
154
|
+
fieldValueToSqlValue(record, f) {
|
|
155
|
+
let value = record[f];
|
|
156
|
+
return this.normalizeValue(value);
|
|
157
|
+
}
|
|
158
|
+
normalizeValue(value) {
|
|
159
|
+
if (value instanceof RegExp) {
|
|
160
|
+
value = value.toString().replace(/\//g, '');
|
|
161
|
+
}
|
|
162
|
+
if (this.isValueObject(value)) {
|
|
163
|
+
value = JSON.stringify(value);
|
|
164
|
+
}
|
|
165
|
+
return value !== null && value !== void 0 ? value : null;
|
|
166
|
+
}
|
|
167
|
+
isValueObject(value) {
|
|
168
|
+
return value !== null && (Array.isArray(value) || typeof value === 'object');
|
|
169
|
+
}
|
|
170
|
+
buildColumnListFromAllRecords(records) {
|
|
171
|
+
const fields = records.map((r) => Object.keys(r)).flat();
|
|
172
|
+
const uniqueFields = new Set(fields);
|
|
173
|
+
return Array.from(uniqueFields);
|
|
174
|
+
}
|
|
175
|
+
optionallyBuildSort(sort) {
|
|
176
|
+
if (sort) {
|
|
177
|
+
const sortSpecs = sort.map((s) => `${s.field} ${s.direction.toUpperCase()}`);
|
|
178
|
+
return ` ORDER BY ${sortSpecs.join(', ')}`;
|
|
179
|
+
}
|
|
180
|
+
return '';
|
|
181
|
+
}
|
|
182
|
+
optionallyBuildSkip(limit) {
|
|
183
|
+
if (typeof limit === 'number') {
|
|
184
|
+
return ` OFFSET ${limit}`;
|
|
185
|
+
}
|
|
186
|
+
return '';
|
|
187
|
+
}
|
|
188
|
+
optionallyBuildLimit(limit) {
|
|
189
|
+
if (typeof limit === 'number') {
|
|
190
|
+
return ` LIMIT ${limit}`;
|
|
191
|
+
}
|
|
192
|
+
return '';
|
|
193
|
+
}
|
|
194
|
+
buildColumnListFromIncludeFields(includeFields) {
|
|
195
|
+
return !includeFields ? '*' : includeFields.join(', ');
|
|
196
|
+
}
|
|
197
|
+
update(tableName, query, updates, shouldReturnUpdatedRecords = true) {
|
|
198
|
+
const { set: set, values } = this.buildSetClause({
|
|
199
|
+
query: updates,
|
|
200
|
+
startingCount: 0,
|
|
201
|
+
});
|
|
202
|
+
let sql = `UPDATE ${tableName} SET ${set.join(', ')}`;
|
|
203
|
+
const { sql: where, values: whereValues } = this.optionallyBuildWhere(query, values.length);
|
|
204
|
+
sql += where;
|
|
205
|
+
if (shouldReturnUpdatedRecords) {
|
|
206
|
+
sql += ' RETURNING *';
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
sql,
|
|
210
|
+
values: [...values, ...whereValues],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
delete(tableName, query) {
|
|
214
|
+
let sql = `DELETE FROM ${tableName}`;
|
|
215
|
+
const { values, sql: where } = this.optionallyBuildWhere(query !== null && query !== void 0 ? query : {});
|
|
216
|
+
sql += where;
|
|
217
|
+
return {
|
|
218
|
+
sql,
|
|
219
|
+
values,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
upsert(tableName, query, updates) {
|
|
223
|
+
let { sql, values } = this.createWithoutReturning(tableName, [
|
|
224
|
+
Object.assign(Object.assign({}, query), updates),
|
|
225
|
+
]);
|
|
226
|
+
const { sql: whereSql, values: whereValues } = this.optionallyBuildWhere(query, values.length);
|
|
227
|
+
const queryFields = this.buildColumnListFromAllRecords([query]);
|
|
228
|
+
const updateFields = this.buildColumnListFromAllRecords([updates]);
|
|
229
|
+
sql += ` ON CONFLICT (${queryFields.join(', ')})`;
|
|
230
|
+
sql += whereSql;
|
|
231
|
+
sql += ` DO UPDATE SET ${updateFields
|
|
232
|
+
.map((f) => `${f} = EXCLUDED.${f}`)
|
|
233
|
+
.join(', ')}`;
|
|
234
|
+
sql += ' RETURNING *';
|
|
235
|
+
return {
|
|
236
|
+
sql,
|
|
237
|
+
values: [...values, ...whereValues],
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/build/index.d.ts
ADDED
|
File without changes
|
package/build/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sprucelabs/postgres-data-store",
|
|
3
|
+
"description": "Postgres production and test adapters for @sprucelabs/data-stores",
|
|
4
|
+
"skill": {
|
|
5
|
+
"namespace": "postgres-data-store",
|
|
6
|
+
"upgradeIgnoreList": [
|
|
7
|
+
"globby",
|
|
8
|
+
"@types/node",
|
|
9
|
+
"@sprucelabs/spruce-core-schemas",
|
|
10
|
+
"@sprucelabs/spruce-test-fixtures"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"build"
|
|
15
|
+
],
|
|
16
|
+
"main": "./build/index.js",
|
|
17
|
+
"types": "./build/index.d.ts",
|
|
18
|
+
"module": "./build/esm/index.js",
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"keywords": [
|
|
21
|
+
"node",
|
|
22
|
+
"components",
|
|
23
|
+
"sprucebot",
|
|
24
|
+
"sprucelabs"
|
|
25
|
+
],
|
|
26
|
+
"version": "0.0.1",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build.ci": "yarn build.tsc && yarn build.resolve-paths && yarn lint",
|
|
29
|
+
"build.dev": "yarn build.tsc --sourceMap ; yarn resolve-paths.lint",
|
|
30
|
+
"build.copy-files": "mkdir -p build && rsync -avzq --exclude='*.ts' ./src/ ./build/",
|
|
31
|
+
"build.dist": "tsc --project tsconfig.dist.json && yarn build.resolve-paths && mv build esm && yarn build.esm-postbuild && yarn build.tsc && yarn build.resolve-paths && mv esm build/ && yarn clean.dist",
|
|
32
|
+
"build.esm-postbuild": "esm-postbuild --target esm --patterns '**/*.js'",
|
|
33
|
+
"build.resolve-paths": "resolve-path-aliases --target build --patterns '**/*.js,**/*.d.ts'",
|
|
34
|
+
"build.tsc": "yarn build.copy-files && tsc",
|
|
35
|
+
"clean": "yarn clean.build",
|
|
36
|
+
"clean.all": "yarn clean.dependencies && yarn clean.build",
|
|
37
|
+
"clean.build": "rm -rf build/",
|
|
38
|
+
"clean.dependencies": "rm -rf node_modules/ package-lock.json yarn.lock",
|
|
39
|
+
"clean.dist": "rm -rf build/__tests__ build/esm/__tests__",
|
|
40
|
+
"fix.lint": "eslint --fix --cache '**/*.ts'",
|
|
41
|
+
"lint": "eslint --cache '**/*.ts'",
|
|
42
|
+
"lint.tsc": "tsc -p . --noEmit",
|
|
43
|
+
"post.watch.build": "yarn build.copy-files && yarn build.resolve-paths",
|
|
44
|
+
"rebuild": "yarn clean.all && yarn && yarn build.dev",
|
|
45
|
+
"release": "semantic-release",
|
|
46
|
+
"resolve-paths.lint": "yarn build.resolve-paths ; yarn lint",
|
|
47
|
+
"test": "jest",
|
|
48
|
+
"update.dependencies": "yarn clean.dependencies && yarn",
|
|
49
|
+
"watch.build.dev": "tsc-watch --sourceMap --onCompilationComplete 'yarn post.watch.build'",
|
|
50
|
+
"watch.lint": "concurrently 'yarn lint' \"chokidar 'src/**/*' -c 'yarn lint.tsc'\"",
|
|
51
|
+
"watch.rebuild": "yarn clean.all && yarn && yarn watch.build.dev",
|
|
52
|
+
"watch.tsc": "tsc -w"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@sprucelabs/data-stores": "^20.0.8",
|
|
56
|
+
"@sprucelabs/schema": "^28.5.128",
|
|
57
|
+
"pg": "^8.9.0"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@sprucelabs/esm-postbuild": "^2.0.97",
|
|
61
|
+
"@sprucelabs/jest-json-reporter": "^6.0.562",
|
|
62
|
+
"@sprucelabs/resolve-path-aliases": "^1.1.176",
|
|
63
|
+
"@sprucelabs/semantic-release": "^4.0.8",
|
|
64
|
+
"@sprucelabs/spruce-test-fixtures": "^52.8.0",
|
|
65
|
+
"@sprucelabs/test": "^7.7.420",
|
|
66
|
+
"@sprucelabs/test-utils": "^3.3.10",
|
|
67
|
+
"@types/node": "^18.14.0",
|
|
68
|
+
"@types/pg": "^8.6.6",
|
|
69
|
+
"chokidar-cli": "^3.0.0",
|
|
70
|
+
"concurrently": "^7.6.0",
|
|
71
|
+
"eslint": "^8.34.0",
|
|
72
|
+
"eslint-config-spruce": "^10.12.0",
|
|
73
|
+
"jest": "^29.4.3",
|
|
74
|
+
"jest-circus": "^29.4.3",
|
|
75
|
+
"prettier": "^2.8.4",
|
|
76
|
+
"ts-node": "^10.9.1",
|
|
77
|
+
"tsc-watch": "^6.0.0",
|
|
78
|
+
"typescript": "^4.9.5"
|
|
79
|
+
},
|
|
80
|
+
"jest": {
|
|
81
|
+
"testRunner": "jest-circus/runner",
|
|
82
|
+
"maxWorkers": 4,
|
|
83
|
+
"testTimeout": 120000,
|
|
84
|
+
"testEnvironment": "node",
|
|
85
|
+
"testPathIgnorePatterns": [
|
|
86
|
+
"<rootDir>/tmp/",
|
|
87
|
+
"<rootDir>/src/",
|
|
88
|
+
"<rootDir>/node_modules/",
|
|
89
|
+
"<rootDir>/build/__tests__/testDirsAndFiles/"
|
|
90
|
+
],
|
|
91
|
+
"testMatch": [
|
|
92
|
+
"**/__tests__/**/*.test.js?(x)"
|
|
93
|
+
],
|
|
94
|
+
"moduleNameMapper": {
|
|
95
|
+
"^#spruce/(.*)$": "<rootDir>/build/.spruce/$1"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|