@graffy/pg 0.19.0 → 0.19.1-alpha.2
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/{types/Db.d.ts → Db.d.ts} +3 -3
- package/Db.js +236 -0
- package/filter/filterObject.js +39 -0
- package/filter/getAst.js +154 -0
- package/filter/getSql.js +100 -0
- package/filter/index.d.ts +2 -0
- package/index.d.ts +8 -0
- package/index.js +71 -0
- package/package.json +15 -9
- package/sql/clauses.d.ts +12 -0
- package/sql/clauses.js +224 -0
- package/sql/format.d.ts +5 -0
- package/sql/format.js +36 -0
- package/sql/getArgSql.d.ts +34 -0
- package/sql/getArgSql.js +82 -0
- package/sql/getMeta.d.ts +12 -0
- package/sql/getMeta.js +11 -0
- package/sql/index.d.ts +2 -0
- package/sql/select.d.ts +2 -0
- package/sql/select.js +41 -0
- package/sql/upsert.d.ts +3 -0
- package/sql/upsert.js +73 -0
- package/index.cjs +0 -996
- package/index.mjs +0 -996
- package/types/index.d.ts +0 -13
- package/types/sql/clauses.d.ts +0 -12
- package/types/sql/format.d.ts +0 -5
- package/types/sql/getArgSql.d.ts +0 -28
- package/types/sql/getMeta.d.ts +0 -12
- package/types/sql/select.d.ts +0 -2
- package/types/sql/upsert.d.ts +0 -3
- /package/{types/filter → filter}/filterObject.d.ts +0 -0
- /package/{types/filter → filter}/getAst.d.ts +0 -0
- /package/{types/filter → filter}/getSql.d.ts +0 -0
- /package/{types/filter/index.d.ts → filter/index.js} +0 -0
- /package/{types/sql/index.d.ts → sql/index.js} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export default class Db {
|
|
2
|
-
constructor(connection: any);
|
|
3
2
|
client: any;
|
|
4
|
-
|
|
3
|
+
constructor(connection: any);
|
|
4
|
+
query(sql: any, tableOptions?: any): Promise<any>;
|
|
5
5
|
readSql(sql: any, tableOptions: any): Promise<any>;
|
|
6
6
|
writeSql(sql: any, tableOptions: any): Promise<any>;
|
|
7
|
-
ensureSchema(tableOptions: any, typeOids
|
|
7
|
+
ensureSchema(tableOptions: any, typeOids?: any): Promise<void>;
|
|
8
8
|
read(rootQuery: any, tableOptions: any): Promise<{
|
|
9
9
|
key: Uint8Array<ArrayBuffer>;
|
|
10
10
|
end: Uint8Array<ArrayBuffer>;
|
package/Db.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { cmp, decodeArgs, decodeGraph, decodeQuery, encodeGraph, encodePath, finalize, isEmpty, isPlainObject, isRange, merge, mergeObject, unwrap, wrap, wrapObject, } from '@graffy/common';
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import pg from 'pg';
|
|
4
|
+
import sqlTag from 'sql-template-tag';
|
|
5
|
+
import formatSql from "./sql/format.js";
|
|
6
|
+
import { del, patch, put, selectByArgs, selectByIds } from "./sql/index.js";
|
|
7
|
+
const log = debug('graffy:pg:db');
|
|
8
|
+
const { Pool, Client, types } = pg;
|
|
9
|
+
export default class Db {
|
|
10
|
+
constructor(connection) {
|
|
11
|
+
if (typeof connection === 'object' &&
|
|
12
|
+
connection &&
|
|
13
|
+
(connection instanceof Pool || connection instanceof Client)) {
|
|
14
|
+
this.client = connection;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
this.client = new Pool(connection);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async query(sql, tableOptions) {
|
|
21
|
+
log(`Making SQL query: ${sql.text}`, sql.values);
|
|
22
|
+
const cubeOid = Number.parseInt(tableOptions?.schema?.typeOids?.cube || '0', 10) || null;
|
|
23
|
+
try {
|
|
24
|
+
sql.types = {
|
|
25
|
+
getTypeParser: (oid, format) => {
|
|
26
|
+
if (oid === types.builtins.INT8) {
|
|
27
|
+
return (value) => Number.parseInt(value, 10);
|
|
28
|
+
}
|
|
29
|
+
if (oid === cubeOid) {
|
|
30
|
+
return (value) => {
|
|
31
|
+
const array = value
|
|
32
|
+
.slice(1, -1)
|
|
33
|
+
.split(/\)\s*,\s*\(/)
|
|
34
|
+
.map((corner) => corner
|
|
35
|
+
.split(',')
|
|
36
|
+
.map((coord) => Number.parseFloat(coord.trim())));
|
|
37
|
+
return array.length > 1 ? array : array[0];
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return types.getTypeParser(oid, format);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return await this.client.query(sql);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
const message = [
|
|
47
|
+
e.message,
|
|
48
|
+
e.detail,
|
|
49
|
+
e.hint,
|
|
50
|
+
e.where,
|
|
51
|
+
sql.text,
|
|
52
|
+
JSON.stringify(sql.values),
|
|
53
|
+
]
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.join('; ');
|
|
56
|
+
throw Error(`pg.sql_error ${message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async readSql(sql, tableOptions) {
|
|
60
|
+
const result = (await this.query(sql, tableOptions)).rows;
|
|
61
|
+
// Each row is an array, as there is only one column returned.
|
|
62
|
+
log('Read result', result);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
async writeSql(sql, tableOptions) {
|
|
66
|
+
const res = await this.query(sql, tableOptions);
|
|
67
|
+
log('Rows written', res.rowCount);
|
|
68
|
+
if (!res.rowCount) {
|
|
69
|
+
throw Error(`pg.nothing_written ${sql.text} with ${sql.values}`);
|
|
70
|
+
}
|
|
71
|
+
return res.rows;
|
|
72
|
+
}
|
|
73
|
+
/*
|
|
74
|
+
Adds .schema to tableOptions if it doesn't exist yet.
|
|
75
|
+
It mutates the argument, to "persist" the results and
|
|
76
|
+
avoid this query in every operation.
|
|
77
|
+
*/
|
|
78
|
+
async ensureSchema(tableOptions, typeOids) {
|
|
79
|
+
if (tableOptions.schema)
|
|
80
|
+
return;
|
|
81
|
+
const { table, verCol, joins } = tableOptions;
|
|
82
|
+
const tableInfoSchema = (await this.query(sqlTag `
|
|
83
|
+
SELECT table_schema, table_type
|
|
84
|
+
FROM information_schema.tables
|
|
85
|
+
WHERE table_name = ${table}
|
|
86
|
+
ORDER BY array_position(current_schemas(false)::text[], table_schema::text) ASC
|
|
87
|
+
LIMIT 1`)).rows[0];
|
|
88
|
+
const tableSchema = tableInfoSchema.table_schema;
|
|
89
|
+
const tableType = tableInfoSchema.table_type;
|
|
90
|
+
const types = (await this.query(sqlTag `
|
|
91
|
+
SELECT jsonb_object_agg(column_name, udt_name) AS column_types
|
|
92
|
+
FROM information_schema.columns
|
|
93
|
+
WHERE
|
|
94
|
+
table_name = ${table} AND
|
|
95
|
+
table_schema = ${tableSchema}`)).rows[0].column_types;
|
|
96
|
+
if (!types)
|
|
97
|
+
throw Error(`pg.missing_table ${table}`);
|
|
98
|
+
typeOids =
|
|
99
|
+
typeOids ||
|
|
100
|
+
(await this.query(sqlTag `
|
|
101
|
+
SELECT jsonb_object_agg(typname, oid) AS type_oids
|
|
102
|
+
FROM pg_type
|
|
103
|
+
WHERE typname = 'cube'`)).rows[0].type_oids;
|
|
104
|
+
// console.log({ typeOids });
|
|
105
|
+
const verDefault = (await this.query(sqlTag `
|
|
106
|
+
SELECT column_default
|
|
107
|
+
FROM information_schema.columns
|
|
108
|
+
WHERE
|
|
109
|
+
table_name = ${table} AND
|
|
110
|
+
table_schema = ${tableSchema} AND
|
|
111
|
+
column_name = ${verCol}`)).rows[0].column_default;
|
|
112
|
+
if (!verDefault && tableType !== 'VIEW') {
|
|
113
|
+
throw Error(`pg.verCol_without_default ${verCol}`);
|
|
114
|
+
}
|
|
115
|
+
await Promise.all(Object.values(joins).map((joinOptions) => this.ensureSchema(joinOptions, typeOids)));
|
|
116
|
+
log('ensureSchema', types);
|
|
117
|
+
tableOptions.schema = { types, typeOids };
|
|
118
|
+
tableOptions.verDefault = verDefault;
|
|
119
|
+
}
|
|
120
|
+
async read(rootQuery, tableOptions) {
|
|
121
|
+
const idQueries = {};
|
|
122
|
+
const promises = [];
|
|
123
|
+
const results = [];
|
|
124
|
+
const { prefix: rawPrefix } = tableOptions;
|
|
125
|
+
const prefix = encodePath(rawPrefix);
|
|
126
|
+
await this.ensureSchema(tableOptions);
|
|
127
|
+
const getByArgs = async (args, projection) => {
|
|
128
|
+
const sql = selectByArgs(args, projection, tableOptions);
|
|
129
|
+
const result = await this.readSql(sql, tableOptions);
|
|
130
|
+
const wrappedGraph = encodeGraph(wrapObject(result, rawPrefix));
|
|
131
|
+
log('getByArgs', wrappedGraph);
|
|
132
|
+
merge(results, wrappedGraph);
|
|
133
|
+
};
|
|
134
|
+
const explainArgs = async (args, _projection) => {
|
|
135
|
+
const { analyze, $explain: qArgs } = args;
|
|
136
|
+
const qSql = selectByArgs(qArgs, null, tableOptions);
|
|
137
|
+
const sql = sqlTag `EXPLAIN (${analyze ? sqlTag `ANALYZE, BUFFERS, TIMING, ` : sqlTag ``}COSTS, VERBOSE, FORMAT JSON) ${qSql}`;
|
|
138
|
+
const result = await this.readSql(sql, tableOptions);
|
|
139
|
+
const wrappedGraph = encodeGraph(wrapObject({
|
|
140
|
+
$key: args,
|
|
141
|
+
sql: formatSql(qSql),
|
|
142
|
+
plan: result[0]['QUERY PLAN'][0],
|
|
143
|
+
}, rawPrefix));
|
|
144
|
+
log('explainArgs', wrappedGraph);
|
|
145
|
+
merge(results, wrappedGraph);
|
|
146
|
+
};
|
|
147
|
+
const getByIds = async () => {
|
|
148
|
+
// TODO: Calculate a combined projection.
|
|
149
|
+
// Bonus: Strategically split into multiple read operations
|
|
150
|
+
// based on projection.
|
|
151
|
+
const sql = selectByIds(Object.keys(idQueries), null, tableOptions);
|
|
152
|
+
const result = await this.readSql(sql, tableOptions);
|
|
153
|
+
for (const object of result) {
|
|
154
|
+
const wrappedGraph = encodeGraph(wrapObject(object, rawPrefix));
|
|
155
|
+
log('getByIds', wrappedGraph);
|
|
156
|
+
merge(results, wrappedGraph);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const query = unwrap(rootQuery, prefix);
|
|
160
|
+
for (const node of query) {
|
|
161
|
+
const args = decodeArgs(node);
|
|
162
|
+
if (isPlainObject(args)) {
|
|
163
|
+
if (node.prefix) {
|
|
164
|
+
for (const childNode of node.children) {
|
|
165
|
+
const childArgs = decodeArgs(childNode);
|
|
166
|
+
const projection = childNode.children
|
|
167
|
+
? decodeQuery(childNode.children)
|
|
168
|
+
: null;
|
|
169
|
+
promises.push(getByArgs({ ...args, ...childArgs }, projection));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const projection = node.children ? decodeQuery(node.children) : null;
|
|
174
|
+
if (args.$explain) {
|
|
175
|
+
promises.push(explainArgs(args, projection));
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
promises.push(getByArgs(args, projection));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
idQueries[args] = node.children;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!isEmpty(idQueries))
|
|
187
|
+
promises.push(getByIds());
|
|
188
|
+
await Promise.all(promises);
|
|
189
|
+
log('dbRead', rootQuery, results);
|
|
190
|
+
return finalize(results, wrap(query, prefix));
|
|
191
|
+
}
|
|
192
|
+
async write(rootChange, tableOptions) {
|
|
193
|
+
const { prefix: rawPrefix } = tableOptions;
|
|
194
|
+
const prefix = encodePath(rawPrefix);
|
|
195
|
+
await this.ensureSchema(tableOptions);
|
|
196
|
+
const change = unwrap(rootChange, prefix);
|
|
197
|
+
const puts = [];
|
|
198
|
+
const sqls = [];
|
|
199
|
+
for (const node of change) {
|
|
200
|
+
const arg = decodeArgs(node);
|
|
201
|
+
if (isRange(node)) {
|
|
202
|
+
if (cmp(node.key, node.end) === 0) {
|
|
203
|
+
log('delete', node);
|
|
204
|
+
sqls.push(del(arg, tableOptions));
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
throw Error('pg_write.write_range_unsupported');
|
|
208
|
+
}
|
|
209
|
+
const object = decodeGraph(node.children);
|
|
210
|
+
if (isPlainObject(arg)) {
|
|
211
|
+
mergeObject(object, arg);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
object[tableOptions.idCol] = arg;
|
|
215
|
+
}
|
|
216
|
+
if (object.$put && object.$put !== true) {
|
|
217
|
+
throw Error('pg_write.partial_put_unsupported');
|
|
218
|
+
}
|
|
219
|
+
if (object.$put) {
|
|
220
|
+
puts.push([object, arg]);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
sqls.push(patch(object, arg, tableOptions));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (puts.length)
|
|
227
|
+
sqls.push(...put(puts, tableOptions));
|
|
228
|
+
const result = [];
|
|
229
|
+
await Promise.all(sqls.map((sql) => this.writeSql(sql, tableOptions).then((object) => {
|
|
230
|
+
log('returned_object_wrapped', wrapObject(object, rawPrefix));
|
|
231
|
+
merge(result, encodeGraph(wrapObject(object, rawPrefix)));
|
|
232
|
+
})));
|
|
233
|
+
log('dbWrite', rootChange, result);
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { encodePath, unwrapObject } from '@graffy/common';
|
|
2
|
+
import getAst from "./getAst.js";
|
|
3
|
+
export default function filterObject(filter, object) {
|
|
4
|
+
function lookup(path) {
|
|
5
|
+
return unwrapObject(object, encodePath(path));
|
|
6
|
+
}
|
|
7
|
+
function checkNode(ast) {
|
|
8
|
+
switch (ast[0]) {
|
|
9
|
+
case '$eq':
|
|
10
|
+
return lookup(ast[1]) === ast[2];
|
|
11
|
+
case '$ne':
|
|
12
|
+
return lookup(ast[1]) !== ast[2];
|
|
13
|
+
case '$lt':
|
|
14
|
+
return lookup(ast[1]) < ast[2];
|
|
15
|
+
case '$lte':
|
|
16
|
+
return lookup(ast[1]) <= ast[2];
|
|
17
|
+
case '$gt':
|
|
18
|
+
return lookup(ast[1]) > ast[2];
|
|
19
|
+
case '$gte':
|
|
20
|
+
return lookup(ast[1]) >= ast[2];
|
|
21
|
+
case '$in':
|
|
22
|
+
return Array.isArray(ast[2]) && ast[2].includes(lookup(ast[1]));
|
|
23
|
+
case '$nin':
|
|
24
|
+
return !(Array.isArray(ast[2]) && ast[2].includes(lookup(ast[1])));
|
|
25
|
+
case '$cts':
|
|
26
|
+
case '$ctd':
|
|
27
|
+
case '$ovp':
|
|
28
|
+
case '$and':
|
|
29
|
+
case '$or':
|
|
30
|
+
case '$not':
|
|
31
|
+
case '$any':
|
|
32
|
+
case '$all':
|
|
33
|
+
case '$has':
|
|
34
|
+
throw Error('pgfilter.unimplemented');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const rootAst = getAst(filter);
|
|
38
|
+
return checkNode(rootAst);
|
|
39
|
+
}
|
package/filter/getAst.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const valid = {
|
|
2
|
+
$eq: true,
|
|
3
|
+
$lt: true,
|
|
4
|
+
$gt: true,
|
|
5
|
+
$lte: true,
|
|
6
|
+
$gte: true,
|
|
7
|
+
$re: true,
|
|
8
|
+
$ire: true,
|
|
9
|
+
$text: true,
|
|
10
|
+
$and: true,
|
|
11
|
+
$or: true,
|
|
12
|
+
$any: true,
|
|
13
|
+
$all: true,
|
|
14
|
+
$has: true,
|
|
15
|
+
$cts: true,
|
|
16
|
+
$ctd: true,
|
|
17
|
+
$keycts: true,
|
|
18
|
+
$keyctd: true,
|
|
19
|
+
};
|
|
20
|
+
const inverse = {
|
|
21
|
+
$eq: '$neq',
|
|
22
|
+
$neq: '$eq',
|
|
23
|
+
$in: '$nin',
|
|
24
|
+
$nin: '$in',
|
|
25
|
+
$lt: '$gte',
|
|
26
|
+
$gte: '$lt',
|
|
27
|
+
$gt: '$lte',
|
|
28
|
+
$lte: '$gt',
|
|
29
|
+
};
|
|
30
|
+
export default function getAst(filter) {
|
|
31
|
+
return simplify(construct(filter));
|
|
32
|
+
}
|
|
33
|
+
function isValidSubQuery(node) {
|
|
34
|
+
if (!node || typeof node !== 'object')
|
|
35
|
+
return false;
|
|
36
|
+
const keys = Object.keys(node);
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
if (key[0] === '$' && !['$and', '$or', '$not'].includes(key))
|
|
39
|
+
return false;
|
|
40
|
+
if (key[0] !== '$')
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
for (const key in node) {
|
|
44
|
+
if (!isValidSubQuery(node[key]))
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
function construct(node, prop, op) {
|
|
50
|
+
if (!node || typeof node !== 'object' || (prop && op)) {
|
|
51
|
+
if (op && prop)
|
|
52
|
+
return [op, prop, node];
|
|
53
|
+
if (prop)
|
|
54
|
+
return ['$eq', prop, node];
|
|
55
|
+
throw Error(`pgast.expected_prop_before:${JSON.stringify(node)}`);
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(node)) {
|
|
58
|
+
return ['$or', node.map((item) => construct(item, prop, op))];
|
|
59
|
+
}
|
|
60
|
+
if (prop && isValidSubQuery(node)) {
|
|
61
|
+
return ['$sub', prop, construct(node)];
|
|
62
|
+
}
|
|
63
|
+
return [
|
|
64
|
+
'$and',
|
|
65
|
+
Object.entries(node).map(([key, val]) => {
|
|
66
|
+
if (key === '$or' || key === '$and') {
|
|
67
|
+
return [key, construct(val, prop, op)[1]];
|
|
68
|
+
}
|
|
69
|
+
if (key === '$not') {
|
|
70
|
+
return [key, construct(val, prop, op)];
|
|
71
|
+
}
|
|
72
|
+
if (key[0] === '$') {
|
|
73
|
+
if (!valid[key])
|
|
74
|
+
throw Error(`pgast.invalid_op:${key}`);
|
|
75
|
+
if (op)
|
|
76
|
+
throw Error(`pgast.unexpected_op:${op} before:${key}`);
|
|
77
|
+
if (!prop)
|
|
78
|
+
throw Error(`pgast.expected_prop_before:${key}`);
|
|
79
|
+
return construct(val, prop, key);
|
|
80
|
+
}
|
|
81
|
+
return construct(val, key);
|
|
82
|
+
}),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
function simplify(node) {
|
|
86
|
+
const op = node[0];
|
|
87
|
+
// TODO: $and/$or with multiple $subs with same prop ->
|
|
88
|
+
// single $sub with $and/$or inside
|
|
89
|
+
// Recurse into subnodes and simplify them first.
|
|
90
|
+
if (op === '$and' || op === '$or') {
|
|
91
|
+
node[1] = node[1].map((subnode) => simplify(subnode));
|
|
92
|
+
}
|
|
93
|
+
else if (op === '$not') {
|
|
94
|
+
node[1] = simplify(node[1]);
|
|
95
|
+
}
|
|
96
|
+
else if (op === '$sub') {
|
|
97
|
+
node[2] = simplify(node[2]);
|
|
98
|
+
}
|
|
99
|
+
// Handle empty $and/$or and booleans
|
|
100
|
+
if (op === '$and') {
|
|
101
|
+
if (!node[1].length)
|
|
102
|
+
return true;
|
|
103
|
+
if (node[1].includes(false))
|
|
104
|
+
return false;
|
|
105
|
+
node[1] = node[1].filter((item) => item !== true);
|
|
106
|
+
}
|
|
107
|
+
else if (op === '$or') {
|
|
108
|
+
if (!node[1].length)
|
|
109
|
+
return false;
|
|
110
|
+
if (node[1].includes(true))
|
|
111
|
+
return true;
|
|
112
|
+
node[1] = node[1].filter((item) => item !== false);
|
|
113
|
+
}
|
|
114
|
+
else if (op === '$not' && typeof node[1] === 'boolean') {
|
|
115
|
+
return !node[1];
|
|
116
|
+
}
|
|
117
|
+
// $or with multiple $eq limbs with same prop -> $in
|
|
118
|
+
if (op === '$or') {
|
|
119
|
+
const { eqmap, noneq, change } = node[1].reduce((acc, item) => {
|
|
120
|
+
if (item[0] !== '$eq' || item[2] === null) {
|
|
121
|
+
acc.noneq.push(item);
|
|
122
|
+
}
|
|
123
|
+
else if (acc.eqmap[item[1]]) {
|
|
124
|
+
acc.change = true;
|
|
125
|
+
acc.eqmap[item[1]].push(item[2]);
|
|
126
|
+
return acc;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
acc.eqmap[item[1]] = [item[2]];
|
|
130
|
+
}
|
|
131
|
+
return acc;
|
|
132
|
+
}, { eqmap: {}, noneq: [], change: false });
|
|
133
|
+
// Don't return. Modify node and then apply the next rule.
|
|
134
|
+
if (change) {
|
|
135
|
+
node[1] = [
|
|
136
|
+
...noneq,
|
|
137
|
+
...Object.entries(eqmap).map(([prop, val]) => val.length > 1
|
|
138
|
+
? ['$in', prop, val]
|
|
139
|
+
: ['$eq', prop, val[0]]),
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// unwrap $and / $or with only one limb
|
|
144
|
+
if ((op === '$and' || op === '$or') && node[1].length === 1) {
|
|
145
|
+
return node[1][0];
|
|
146
|
+
}
|
|
147
|
+
// $not $eq -> $neq, $in -> $nin etc.
|
|
148
|
+
if (op === '$not') {
|
|
149
|
+
const [subop, ...subargs] = node[1];
|
|
150
|
+
const invop = inverse[subop];
|
|
151
|
+
return invop ? [invop, ...subargs] : node;
|
|
152
|
+
}
|
|
153
|
+
return node;
|
|
154
|
+
}
|
package/filter/getSql.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import sql, { join, raw } from 'sql-template-tag';
|
|
2
|
+
import { cubeLiteralSql } from "../sql/clauses.js";
|
|
3
|
+
import getAst from "./getAst.js";
|
|
4
|
+
const opSql = {
|
|
5
|
+
$and: 'AND', // Not SQL as these are used as delimiters
|
|
6
|
+
$or: 'OR',
|
|
7
|
+
$not: sql `NOT`,
|
|
8
|
+
$eq: sql `=`,
|
|
9
|
+
$neq: sql `<>`,
|
|
10
|
+
$in: sql `IN`,
|
|
11
|
+
$nin: sql `NOT IN`,
|
|
12
|
+
$lt: sql `<`,
|
|
13
|
+
$lte: sql `<=`,
|
|
14
|
+
$gt: sql `>`,
|
|
15
|
+
$gte: sql `>=`,
|
|
16
|
+
$re: sql `~`,
|
|
17
|
+
$ire: sql `~*`,
|
|
18
|
+
$cts: sql `@>`,
|
|
19
|
+
$ctd: sql `<@`,
|
|
20
|
+
$keycts: sql `?|`,
|
|
21
|
+
$keyctd: sql `?&`,
|
|
22
|
+
};
|
|
23
|
+
function getBinarySql(lhs, type, op, value, textLhs) {
|
|
24
|
+
if (value === null && op === '$eq')
|
|
25
|
+
return sql `${lhs} IS NULL`;
|
|
26
|
+
if (value === null && op === '$neq')
|
|
27
|
+
return sql `${lhs} IS NOT NULL`;
|
|
28
|
+
const sqlOp = opSql[op];
|
|
29
|
+
if (!sqlOp)
|
|
30
|
+
throw Error(`pg.getSql_unknown_operator ${op}`);
|
|
31
|
+
if (op === '$in' || op === '$nin') {
|
|
32
|
+
if (type === 'jsonb' && typeof value[0] === 'string')
|
|
33
|
+
lhs = textLhs;
|
|
34
|
+
return sql `${lhs} ${sqlOp} (${join(value)})`;
|
|
35
|
+
}
|
|
36
|
+
if (op === '$re' || op === '$ire') {
|
|
37
|
+
if (type === 'jsonb') {
|
|
38
|
+
lhs = textLhs;
|
|
39
|
+
}
|
|
40
|
+
else if (type !== 'text') {
|
|
41
|
+
lhs = sql `(${lhs})::text`;
|
|
42
|
+
}
|
|
43
|
+
return sql `${lhs} ${sqlOp} ${String(value)}`;
|
|
44
|
+
}
|
|
45
|
+
if (type === 'jsonb') {
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
return sql `${textLhs} ${sqlOp} ${value}`;
|
|
48
|
+
}
|
|
49
|
+
if ((op === '$keycts' || op === '$keyctd') && Array.isArray(value))
|
|
50
|
+
return sql `${lhs} ${sqlOp} ${value}::text[]`;
|
|
51
|
+
return sql `${lhs} ${sqlOp} ${JSON.stringify(value)}::jsonb`;
|
|
52
|
+
}
|
|
53
|
+
if (type === 'cube')
|
|
54
|
+
return sql `${lhs} ${sqlOp} ${cubeLiteralSql(value)}`;
|
|
55
|
+
return sql `${lhs} ${sqlOp} ${value}`;
|
|
56
|
+
}
|
|
57
|
+
function getNodeSql(ast, options) {
|
|
58
|
+
if (typeof ast === 'boolean')
|
|
59
|
+
return ast;
|
|
60
|
+
const op = ast[0];
|
|
61
|
+
if (op === '$and' || op === '$or') {
|
|
62
|
+
// Handle variadic operators
|
|
63
|
+
return sql `(${join(ast[1].map((node) => getNodeSql(node, options)), `) ${opSql[op]} (`)})`;
|
|
64
|
+
}
|
|
65
|
+
if (op === '$not') {
|
|
66
|
+
// Handle unary operators
|
|
67
|
+
return sql `${opSql[op]} (${getNodeSql(ast[1], options)})`;
|
|
68
|
+
}
|
|
69
|
+
if (op === '$sub') {
|
|
70
|
+
// Handle joins.
|
|
71
|
+
const joinName = ast[1];
|
|
72
|
+
if (!options.joins[joinName])
|
|
73
|
+
throw Error(`pg.no_join ${joinName}`);
|
|
74
|
+
const { idCol, schema } = options;
|
|
75
|
+
const joinOptions = options.joins[joinName];
|
|
76
|
+
const { table: joinTable, refCol } = options.joins[joinName];
|
|
77
|
+
return sql `"${raw(idCol)}" IN (SELECT "${raw(refCol)}"::${raw(schema.types[idCol])} FROM "${raw(joinTable)}" WHERE ${getNodeSql(ast[2], joinOptions)})`;
|
|
78
|
+
}
|
|
79
|
+
// It is a binary operator
|
|
80
|
+
const [prefix, ...suffix] = ast[1].split('.');
|
|
81
|
+
const { types = {} } = options.schema;
|
|
82
|
+
if (!types[prefix])
|
|
83
|
+
throw Error(`pg.no_column ${prefix}`);
|
|
84
|
+
if (types[prefix] === 'jsonb') {
|
|
85
|
+
const [lhs, textLhs] = suffix.length
|
|
86
|
+
? [
|
|
87
|
+
sql `"${raw(prefix)}" #> ${suffix}`,
|
|
88
|
+
sql `"${raw(prefix)}" #>> ${suffix}`,
|
|
89
|
+
]
|
|
90
|
+
: [sql `"${raw(prefix)}"`, sql `"${raw(prefix)}" #>> '{}'`];
|
|
91
|
+
return getBinarySql(lhs, 'jsonb', op, ast[2], textLhs);
|
|
92
|
+
}
|
|
93
|
+
if (suffix.length)
|
|
94
|
+
throw Error(`pg.lookup_not_jsonb ${prefix}`);
|
|
95
|
+
return getBinarySql(sql `"${raw(prefix)}"`, types[prefix], op, ast[2]);
|
|
96
|
+
}
|
|
97
|
+
export default function getSql(filter, options) {
|
|
98
|
+
const ast = getAst(filter);
|
|
99
|
+
return getNodeSql(ast, options);
|
|
100
|
+
}
|
package/index.d.ts
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { encodePath, merge, remove } from '@graffy/common';
|
|
2
|
+
import Db from "./Db.js";
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{
|
|
5
|
+
* table: string,
|
|
6
|
+
* idCol: string,
|
|
7
|
+
* verCol: string,
|
|
8
|
+
* joins: Record<string, Partial<TableOpts> & { refCol: string }>,
|
|
9
|
+
* schema?: any,
|
|
10
|
+
* verDefault?: string
|
|
11
|
+
* }} TableOpts
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} name
|
|
15
|
+
* @param {Partial<TableOpts>} options
|
|
16
|
+
* @param {string} parentName
|
|
17
|
+
* @returns {TableOpts}
|
|
18
|
+
*/
|
|
19
|
+
function getTableOpts(name, { table, idCol, verCol, joins, schema, verDefault, } = {}, parentName = null) {
|
|
20
|
+
const tableName = table || name;
|
|
21
|
+
return {
|
|
22
|
+
table: table || name,
|
|
23
|
+
idCol: idCol || 'id',
|
|
24
|
+
verCol: verCol || 'updatedAt',
|
|
25
|
+
joins: Object.fromEntries(Object.entries(joins || {}).map(([joinName, joinVal]) => {
|
|
26
|
+
const { refCol = parentName, ...joinOptions } = joinVal;
|
|
27
|
+
return [
|
|
28
|
+
joinName,
|
|
29
|
+
{
|
|
30
|
+
refCol,
|
|
31
|
+
...getTableOpts(joinName, joinOptions, tableName),
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
})),
|
|
35
|
+
schema,
|
|
36
|
+
verDefault,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* @param {Partial<TableOpts> & {connection: any}} options
|
|
41
|
+
* @returns {Function}
|
|
42
|
+
*/
|
|
43
|
+
export const pg = ({ connection, ...rawOptions }) => (store) => {
|
|
44
|
+
store.on('read', read);
|
|
45
|
+
store.on('write', write);
|
|
46
|
+
const prefix = store.path;
|
|
47
|
+
/** @type {TableOpts & {prefix?: string[]}} */
|
|
48
|
+
const tableOpts = getTableOpts(prefix[prefix.length - 1], rawOptions);
|
|
49
|
+
tableOpts.prefix = prefix;
|
|
50
|
+
const defaultDb = new Db(connection);
|
|
51
|
+
function read(query, options, next) {
|
|
52
|
+
const { pgClient } = options;
|
|
53
|
+
const db = pgClient ? new Db(pgClient) : defaultDb;
|
|
54
|
+
const readPromise = db.read(query, tableOpts);
|
|
55
|
+
const remainingQuery = remove(query, encodePath(prefix));
|
|
56
|
+
const nextPromise = next(remainingQuery);
|
|
57
|
+
return Promise.all([readPromise, nextPromise]).then(([readRes, nextRes]) => {
|
|
58
|
+
return merge(readRes, nextRes);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function write(change, options, next) {
|
|
62
|
+
const { pgClient } = options;
|
|
63
|
+
const db = pgClient ? new Db(pgClient) : defaultDb;
|
|
64
|
+
const writePromise = db.write(change, tableOpts);
|
|
65
|
+
const remainingChange = remove(change, encodePath(prefix));
|
|
66
|
+
const nextPromise = next(remainingChange);
|
|
67
|
+
return Promise.all([writePromise, nextPromise]).then(([writeRes, nextRes]) => {
|
|
68
|
+
return merge(writeRes, nextRes);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
package/package.json
CHANGED
|
@@ -2,23 +2,29 @@
|
|
|
2
2
|
"name": "@graffy/pg",
|
|
3
3
|
"description": "The standard Postgres module for Graffy. Each instance this module mounts a Postgres table as a Graffy subtree.",
|
|
4
4
|
"author": "aravind (https://github.com/aravindet)",
|
|
5
|
-
"version": "0.19.
|
|
6
|
-
"main": "./index.
|
|
5
|
+
"version": "0.19.1-alpha.2",
|
|
6
|
+
"main": "./cjs/index.js",
|
|
7
7
|
"exports": {
|
|
8
|
-
"
|
|
9
|
-
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./index.js",
|
|
10
|
+
"types": "./index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./*": {
|
|
13
|
+
"import": "./*.js",
|
|
14
|
+
"types": "./*.d.ts"
|
|
15
|
+
}
|
|
10
16
|
},
|
|
11
|
-
"
|
|
12
|
-
"types": "./types/index.d.ts",
|
|
17
|
+
"types": "./index.d.ts",
|
|
13
18
|
"repository": {
|
|
14
19
|
"type": "git",
|
|
15
20
|
"url": "git+https://github.com/usegraffy/graffy.git"
|
|
16
21
|
},
|
|
17
22
|
"license": "Apache-2.0",
|
|
18
23
|
"dependencies": {
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"sql-
|
|
24
|
+
"sql-formatter": "^15.7.4",
|
|
25
|
+
"@graffy/common": "0.19.1-alpha.2",
|
|
26
|
+
"sql-template-tag": "^5.2.1",
|
|
27
|
+
"debug": "^4.4.3"
|
|
22
28
|
},
|
|
23
29
|
"peerDependencies": {
|
|
24
30
|
"pg": "^8.0.0"
|
package/sql/clauses.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Sql } from 'sql-template-tag';
|
|
2
|
+
export declare const getJsonBuildTrusted: (variadic: any) => Sql;
|
|
3
|
+
export declare const lookup: (prop: any, options: any) => Sql;
|
|
4
|
+
export declare const lookupNumeric: (prop: any) => Sql;
|
|
5
|
+
export declare const getSelectCols: (options: any, projection?: any) => Sql;
|
|
6
|
+
export declare function cubeLiteralSql(value: any): Sql;
|
|
7
|
+
export declare const getInsert: (rows: any, options: any) => {
|
|
8
|
+
cols: Sql;
|
|
9
|
+
vals: Sql;
|
|
10
|
+
updates: Sql;
|
|
11
|
+
};
|
|
12
|
+
export declare const getUpdates: (row: any, options: any) => Sql;
|