@graffy/pg 0.15.12-alpha.2 → 0.15.13
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 +131 -3
- package/index.cjs +121 -59
- package/index.mjs +122 -60
- package/package.json +5 -3
- package/types/index.d.ts +1 -2
- package/types/sql/clauses.d.ts +3 -1
- package/types/sql/getArgSql.d.ts +2 -1
- package/types/sql/getMeta.d.ts +1 -0
- package/types/sql/select.d.ts +2 -2
- package/types/sql/upsert.d.ts +1 -0
package/Readme.md
CHANGED
|
@@ -10,9 +10,137 @@ import pg from '@graffy/pg';
|
|
|
10
10
|
graffyStore.use(pg(options));
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Uses the [pg](https://github.com/brianc/node-postgres) library.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
### Query filters
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Query filters are passed in `$key` and are JSON-based, somewhat like MongoDB.
|
|
18
18
|
|
|
19
|
+
1. Filters expressions follow a **property**, **operator**, **value** order. Values are scalar values (strings or numbers).
|
|
20
|
+
2. Property names are always object keys. They may be strings with dots `.`.
|
|
21
|
+
3. Operators are placed in objects as well and have a leading `$`.
|
|
22
|
+
4. Values are JSON values.
|
|
23
|
+
5. Multiple properties in an object are combined with `AND`. Items in an arrays are combined with `OR`.
|
|
24
|
+
6. The supported operators are:
|
|
25
|
+
- `$eq`: optional in most cases.
|
|
26
|
+
- `$lt`, `$lte`, `$gt`, `$gte`: Ranges
|
|
27
|
+
- `$re`, `$ire`: Regex match (case sensitive and insensitive versions)
|
|
28
|
+
- `$text`: Full text search, always case insensitive
|
|
29
|
+
- `$not`: Modifies other filters or inverts a condition
|
|
30
|
+
- `$and`, `$or`: Combines conditions; optional in most cases
|
|
31
|
+
- `$all`, `$has`, `$any`: Apply conditions to the elements of a collection (list or map)
|
|
32
|
+
|
|
33
|
+
#### Basic
|
|
34
|
+
|
|
35
|
+
1. `{ foo: 5 }` and `{ foo: { $eq: 5 } }` compile to SQL `foo = 5`.
|
|
36
|
+
2. `{ foo: { $gt: 5, $lt: 6 } }` becomes `foo > 5 AND foo < 6`.
|
|
37
|
+
3. `{ foo: { $ire: '^wor.*' } }` becomes `foo ~* "^wor.*"`.
|
|
38
|
+
4. `{ foo: { $text: 'potatoes' } }` becomes `foo @@ websearch_to_tsquery('potatoes')`.
|
|
39
|
+
For this to work, `foo` must be a TSVector column.
|
|
40
|
+
|
|
41
|
+
#### Or
|
|
42
|
+
|
|
43
|
+
1. `{ foo: [5, 6] }` means *foo equals 5 or foo equals 6*, and the compiler is smart enough to simplify this to `foo IN (5, 6)`. There is no separate $in.
|
|
44
|
+
2. `{ foo: [ 5, { $gt: 6 } ] }` becomes `foo = 5 OR foo > 6`
|
|
45
|
+
3. `[ { foo: 6 }, { bar: 7 } ]` becomes `foo = 6 OR bar = 7`
|
|
46
|
+
|
|
47
|
+
#### Not
|
|
48
|
+
|
|
49
|
+
1. `{ foo: { $not: 6 } }` becomes `foo <> 6` (the SQL not equals operator)
|
|
50
|
+
2. `{ foo: { $not: [5, 6] }` becomes `foo NOT IN (5, 6)`
|
|
51
|
+
3. `{ foo: { $not: [ 5, { $gt: 6 } ] } }` becomes `NOT (foo = 5 OR foo > 6)`
|
|
52
|
+
|
|
53
|
+
#### Logic
|
|
54
|
+
|
|
55
|
+
1. By default, objects mean `AND` and arrays mean `OR`:
|
|
56
|
+
`[ { foo: 5, bar: 6 }, { baz: 7, qux: 4 } ]` becomes
|
|
57
|
+
`(foo = 5 AND bar = 6) OR (baz = 7 AND qux = 4)`
|
|
58
|
+
2. Use `$and` and `$or` operators explicitly to use any structure in either context:
|
|
59
|
+
|
|
60
|
+
`{ $and: [ { $or: { foo: 5, bar: 6 } }, { $or: { baz: 7, qux: 4 } } ] }` becomes
|
|
61
|
+
`(foo = 5 OR bar = 6) AND (baz = 7 OR qux = 4)`.
|
|
62
|
+
|
|
63
|
+
#### Contains and Contained by
|
|
64
|
+
|
|
65
|
+
1. `{ tags: { $cts: ['foo', 'bar'] } }` becomes `tags @> '{"foo","bar"}'`.
|
|
66
|
+
Tags must contain both *foo* and *bar*. Note that the array of conditions here does not have `OR` semantics.
|
|
67
|
+
2. `{ tags: { $ctd: ['foo', 'bar', 'baz'] } }` becomes `tags <@ '{"foo","bar","baz"}'`.
|
|
68
|
+
Every tag must be one of *foo*, *bar* or *baz*.
|
|
69
|
+
|
|
70
|
+
#### Notes
|
|
71
|
+
|
|
72
|
+
1. We drop several MongoDB operators while retaining the capability:
|
|
73
|
+
- `$ne`: Use $not instead.
|
|
74
|
+
- `$in`: Use an array of values; it is a combination of implicit $or and $eq.
|
|
75
|
+
- `$nin`: Use $not and an array of values.
|
|
76
|
+
- `$exists`: Use `null` or `{ $not: null }`. Postgres does not distinguish between `undefined` and `null`, and neither does Graffy (in this context at least; it's complicated.)
|
|
77
|
+
2. Graffy `$has` is equivalent to MongoDB `$all` (each of the provided conditions is met by at least one element in the array). Graffy `$all` (every element of the array meets a condition) has no direct MongoDB equivalent, but can be expressed as `{ $not: { $elemMatch: { $not: (cond) } } }`
|
|
78
|
+
3. Graffy has a separate operator for case-insensitive regex, and configures the text search locale in the database object rather than the query. This makes MongoDB's regex `$options`, full text `$language` etc. unnecessary.
|
|
79
|
+
4. Graffy does not have equivalents for MongoDB operators `$type`, `$expr`, `$jsonSchema`, `$where`, `$mod`, `$size` and the geospatial operators.
|
|
80
|
+
|
|
81
|
+
### Order by
|
|
82
|
+
|
|
83
|
+
The root of the Graffy filter object must be an object. (Use `$or` if required.) The property `$order` specifies the order. Its value must be an array of order specifiers, each of which may be a string property name or an object with property names, sort direction, collation and text search relevance. (TBD)
|
|
84
|
+
|
|
85
|
+
### Full Text Search
|
|
86
|
+
|
|
87
|
+
A full-text search query typically has three requirements:
|
|
88
|
+
|
|
89
|
+
- Filter: `{ tsv: { $text: 'query' } }`. Return only results that match.
|
|
90
|
+
- Order: `{ $order: [{ $text: ['tsv', 'query'] }], ... }`. Sort results by relevance.
|
|
91
|
+
- Projection: `{ tsv: { query: true } }`. Return snippets of the document surrounding matches.
|
|
92
|
+
|
|
93
|
+
In all three, `tsv` is a computed column of type TSVector.
|
|
94
|
+
|
|
95
|
+
### Aggregations
|
|
96
|
+
|
|
97
|
+
In Graffy PG, aggregations are specified using the `$group` argument in $key, and special properties like `$count`, `$sum` etc. in the projection. `$group` may be `true` or an array.
|
|
98
|
+
|
|
99
|
+
Consider a table of books with columns `authorId` and `copiesSold`. We want the to compute aggregates on the `copiesSold` column.
|
|
100
|
+
|
|
101
|
+
#### Without Group By
|
|
102
|
+
|
|
103
|
+
Let's say we want the total copies sold of all the books in our database. We use `$group: true`, like:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
{
|
|
107
|
+
books: {
|
|
108
|
+
$key: { $group: true },
|
|
109
|
+
$sum: { copiesSold: true }
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Note how the field to sum is specified; this way, multiple fields may be specified.
|
|
114
|
+
|
|
115
|
+
As always, the result will mirror the query:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
books: [{
|
|
119
|
+
$key: { $group: true },
|
|
120
|
+
$sum: { copiesSold: 12345 }
|
|
121
|
+
}]
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### With Group By
|
|
125
|
+
|
|
126
|
+
Now let's say we want the separate totals for each author. As we might have a very large number of authors, we might need to paginate over the results.
|
|
127
|
+
|
|
128
|
+
The grouping properties (e.g. `authorId`) may also be included in the projection, and these values may even be used to construct links.
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
{
|
|
132
|
+
books: {
|
|
133
|
+
$key: { $group: ['authorId'], $first: 30 },
|
|
134
|
+
authorId: true,
|
|
135
|
+
author: { namme: true }, // Link to ['users', authorId]
|
|
136
|
+
$sum: { copiesSold: true }
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Aggregate functions
|
|
141
|
+
|
|
142
|
+
Graffy supports the following aggregate functions.
|
|
143
|
+
|
|
144
|
+
- `$count` of rows; this is just specified as `$count: true`, without any fields under it. (All other aggregate functions require fields to be specified.)
|
|
145
|
+
- `$sum`, `$avg`, `$max`, `$min`
|
|
146
|
+
- `$card` (cardinality), or the number of unique values in a column
|
package/index.cjs
CHANGED
|
@@ -185,7 +185,7 @@ function getCompatibleTypes(value) {
|
|
|
185
185
|
return "text";
|
|
186
186
|
}
|
|
187
187
|
function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
|
|
188
|
-
function
|
|
188
|
+
function lookup2(string, type) {
|
|
189
189
|
if (string.substr(0, 3) === "el$")
|
|
190
190
|
return sql__default["default"]`"${sql.raw(string)}"`;
|
|
191
191
|
return getLookupSql(string, type);
|
|
@@ -194,20 +194,20 @@ function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
|
|
|
194
194
|
const lType = left.substr(0, 3) === "el$" ? "any" : getColumnType(left);
|
|
195
195
|
const rType = getCompatibleTypes(right);
|
|
196
196
|
if (lType === "any" || rType === "any" || rType === lType) {
|
|
197
|
-
return sql__default["default"]`${
|
|
197
|
+
return sql__default["default"]`${lookup2(left)} ${sql.raw(op)} ${right}`;
|
|
198
198
|
} else {
|
|
199
|
-
return sql__default["default"]`(${
|
|
199
|
+
return sql__default["default"]`(${lookup2(left, rType)})::${sql.raw(rType)} ${sql.raw(op)} ${right}`;
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
function getNodeSql(ast) {
|
|
203
203
|
switch (ast[0]) {
|
|
204
204
|
case "$eq":
|
|
205
205
|
if (ast[2] === null)
|
|
206
|
-
return sql__default["default"]`${
|
|
206
|
+
return sql__default["default"]`${lookup2(ast[1])} IS NULL`;
|
|
207
207
|
return binop("=", ast[1], ast[2]);
|
|
208
208
|
case "$neq":
|
|
209
209
|
if (ast[2] === null)
|
|
210
|
-
return sql__default["default"]`${
|
|
210
|
+
return sql__default["default"]`${lookup2(ast[1])} IS NOT NULL`;
|
|
211
211
|
return binop("<>", ast[1], ast[2]);
|
|
212
212
|
case "$lt":
|
|
213
213
|
return binop("<", ast[1], ast[2]);
|
|
@@ -222,20 +222,20 @@ function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
|
|
|
222
222
|
case "$ire":
|
|
223
223
|
return binop("~*", ast[1], ast[2]);
|
|
224
224
|
case "$in":
|
|
225
|
-
return sql__default["default"]`${
|
|
225
|
+
return sql__default["default"]`${lookup2(ast[1])} IN (${sql.join(ast[2])})`;
|
|
226
226
|
case "$nin":
|
|
227
|
-
return sql__default["default"]`${
|
|
227
|
+
return sql__default["default"]`${lookup2(ast[1])} NOT IN (${sql.join(ast[2])})`;
|
|
228
228
|
case "$cts":
|
|
229
|
-
return sql__default["default"]`${
|
|
229
|
+
return sql__default["default"]`${lookup2(ast[1])} @> ${ast[2]}`;
|
|
230
230
|
case "$ctd":
|
|
231
|
-
return sql__default["default"]`${
|
|
231
|
+
return sql__default["default"]`${lookup2(ast[1])} <@ ${ast[2]}`;
|
|
232
232
|
case "$ovl":
|
|
233
233
|
switch (getColumnType(ast[1])) {
|
|
234
234
|
case "jsonb":
|
|
235
235
|
case "any":
|
|
236
|
-
return sql__default["default"]`${
|
|
236
|
+
return sql__default["default"]`${lookup2(ast[1])} ?| ${Array.isArray(ast[2]) ? ast[2] : Object.keys(ast[2])}`;
|
|
237
237
|
case "array":
|
|
238
|
-
return sql__default["default"]`${
|
|
238
|
+
return sql__default["default"]`${lookup2(ast[1])} && ${ast[2]}`;
|
|
239
239
|
default:
|
|
240
240
|
throw Error("pg.getSql_ovl_unknown_column_type");
|
|
241
241
|
}
|
|
@@ -246,11 +246,11 @@ function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
|
|
|
246
246
|
case "$not":
|
|
247
247
|
return sql__default["default"]`NOT (${getNodeSql(ast[1])})`;
|
|
248
248
|
case "$any":
|
|
249
|
-
return sql__default["default"]`(SELECT bool_or(${getNodeSql(ast[3])}) FROM UNNEST(${
|
|
249
|
+
return sql__default["default"]`(SELECT bool_or(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
250
250
|
case "$all":
|
|
251
|
-
return sql__default["default"]`(SELECT bool_and(${getNodeSql(ast[3])}) FROM UNNEST(${
|
|
251
|
+
return sql__default["default"]`(SELECT bool_and(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
252
252
|
case "$has":
|
|
253
|
-
return sql__default["default"]`(SELECT bool_or(${sql.join(ast[3].map((node) => getNodeSql(node)), `) AND bool_or(`)}) FROM UNNEST(${
|
|
253
|
+
return sql__default["default"]`(SELECT bool_or(${sql.join(ast[3].map((node) => getNodeSql(node)), `) AND bool_or(`)}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
254
254
|
default:
|
|
255
255
|
throw Error("pg.getSql_unknown_operator: " + ast[0]);
|
|
256
256
|
}
|
|
@@ -271,8 +271,40 @@ const getJsonBuildValue = (value) => {
|
|
|
271
271
|
return sql__default["default"]`${value}::text`;
|
|
272
272
|
return sql__default["default"]`${JSON.stringify(stripAttributes(value))}::jsonb`;
|
|
273
273
|
};
|
|
274
|
-
const
|
|
275
|
-
|
|
274
|
+
const lookup = (prop, type) => {
|
|
275
|
+
const [prefix, ...suffix] = common.encodePath(prop);
|
|
276
|
+
const op = type === "text" ? sql__default["default"]`#>>` : sql__default["default"]`#>`;
|
|
277
|
+
return suffix.length ? sql__default["default"]`"${sql.raw(prefix)}" ${op} ${suffix}` : sql__default["default"]`"${sql.raw(prefix)}"`;
|
|
278
|
+
};
|
|
279
|
+
const getType = (prop) => {
|
|
280
|
+
const [_prefix, ...suffix] = common.encodePath(prop);
|
|
281
|
+
return suffix.length ? "jsonb" : "any";
|
|
282
|
+
};
|
|
283
|
+
const aggSql = {
|
|
284
|
+
$sum: (prop) => sql__default["default"]`sum((${lookup(prop)})::numeric)`,
|
|
285
|
+
$card: (prop) => sql__default["default"]`count(distinct(${lookup(prop)}))`,
|
|
286
|
+
$avg: (prop) => sql__default["default"]`sum((${lookup(prop)})::numeric)`,
|
|
287
|
+
$max: (prop) => sql__default["default"]`sum((${lookup(prop)})::numeric)`,
|
|
288
|
+
$min: (prop) => sql__default["default"]`sum((${lookup(prop)})::numeric)`
|
|
289
|
+
};
|
|
290
|
+
const getSelectCols = (table, projection = null) => {
|
|
291
|
+
if (!projection)
|
|
292
|
+
return sql__default["default"]`to_jsonb("${sql.raw(table)}")`;
|
|
293
|
+
const sqls = [];
|
|
294
|
+
for (const key in projection) {
|
|
295
|
+
if (key === "$count") {
|
|
296
|
+
sqls.push(sql__default["default"]`'$count', count(*)`);
|
|
297
|
+
} else if (aggSql[key]) {
|
|
298
|
+
const subSqls = [];
|
|
299
|
+
for (const prop in projection[key]) {
|
|
300
|
+
subSqls.push(sql__default["default"]`${prop}::text, ${aggSql[key](prop)}`);
|
|
301
|
+
}
|
|
302
|
+
sqls.push(sql__default["default"]`${key}::text, jsonb_build_object(${sql.join(subSqls, ", ")})`);
|
|
303
|
+
} else {
|
|
304
|
+
sqls.push(sql__default["default"]`${key}::text, "${sql.raw(key)}"`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return sql__default["default"]`jsonb_build_object(${sql.join(sqls, ", ")})`;
|
|
276
308
|
};
|
|
277
309
|
const getInsert = (row, options) => {
|
|
278
310
|
const cols = [];
|
|
@@ -322,20 +354,20 @@ const getArgMeta = (key, prefix, idCol) => getJsonBuildObject({
|
|
|
322
354
|
$ref: sql__default["default"]`jsonb_build_array(${sql.join(prefix.map((k) => sql__default["default"]`${k}::text`))}, "${sql.raw(idCol)}")`,
|
|
323
355
|
$ver: nowTimestamp
|
|
324
356
|
});
|
|
357
|
+
const getAggMeta = (key, $group) => getJsonBuildObject({
|
|
358
|
+
$key: sql.join([key, getJsonBuildObject({ $group })].filter(Boolean), " || "),
|
|
359
|
+
$ver: nowTimestamp
|
|
360
|
+
});
|
|
325
361
|
function getArgSql(_c, options) {
|
|
326
362
|
var _d = _c, { $first, $last, $after, $before, $since, $until, $all, $cursor: _ } = _d, rest = __objRest(_d, ["$first", "$last", "$after", "$before", "$since", "$until", "$all", "$cursor"]);
|
|
327
|
-
const _a = rest, { $order } = _a, filter = __objRest(_a, ["$order"]);
|
|
363
|
+
const _a = rest, { $order, $group } = _a, filter = __objRest(_a, ["$order", "$group"]);
|
|
328
364
|
const { prefix, idCol } = options;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
const [_prefix, ...suffix] = common.encodePath(prop);
|
|
336
|
-
return suffix.length ? "jsonb" : "any";
|
|
337
|
-
};
|
|
338
|
-
const meta = (key2) => getArgMeta(key2, prefix, idCol);
|
|
365
|
+
if ($order && $group) {
|
|
366
|
+
throw Error("pg_arg.order_and_group_unsupported in " + prefix);
|
|
367
|
+
}
|
|
368
|
+
const meta = (key2) => $group ? getAggMeta(key2, $group) : getArgMeta(key2, prefix, idCol);
|
|
369
|
+
const groupCols = Array.isArray($group) && $group.length ? $group.map((col) => lookup(col)) : void 0;
|
|
370
|
+
const group = groupCols ? sql.join(groupCols, ", ") : void 0;
|
|
339
371
|
const hasRangeArg = $before || $after || $since || $until || $first || $last || $all || $order;
|
|
340
372
|
let key;
|
|
341
373
|
const where = [];
|
|
@@ -344,7 +376,7 @@ function getArgSql(_c, options) {
|
|
|
344
376
|
key = sql__default["default"]`${JSON.stringify(filter)}::jsonb`;
|
|
345
377
|
}
|
|
346
378
|
if (!hasRangeArg)
|
|
347
|
-
return { meta: meta(key), where, limit: 1 };
|
|
379
|
+
return { meta: meta(key), where, group, limit: 1 };
|
|
348
380
|
if (common.isEmpty(rest)) {
|
|
349
381
|
throw Error("pg_arg.pagination_only_unsupported in " + prefix);
|
|
350
382
|
}
|
|
@@ -355,13 +387,14 @@ function getArgSql(_c, options) {
|
|
|
355
387
|
});
|
|
356
388
|
const orderQuery = $order && getJsonBuildObject({ $order: sql__default["default"]`${JSON.stringify($order)}::jsonb` });
|
|
357
389
|
const cursorQuery = getJsonBuildObject({
|
|
358
|
-
$cursor: sql__default["default"]`jsonb_build_array(${sql.join(orderCols)})`
|
|
390
|
+
$cursor: sql__default["default"]`jsonb_build_array(${sql.join(groupCols || orderCols)})`
|
|
359
391
|
});
|
|
360
392
|
key = sql__default["default"]`(${sql.join([key, orderQuery, cursorQuery].filter(Boolean), ` || `)})`;
|
|
361
393
|
return {
|
|
362
394
|
meta: meta(key),
|
|
363
395
|
where,
|
|
364
|
-
order: sql.join(orderCols.map((col) => sql__default["default"]`${col} ${$last ? sql__default["default"]`DESC` : sql__default["default"]`ASC`}`), `, `),
|
|
396
|
+
order: $order && sql.join(orderCols.map((col) => sql__default["default"]`${col} ${$last ? sql__default["default"]`DESC` : sql__default["default"]`ASC`}`), `, `),
|
|
397
|
+
group,
|
|
365
398
|
limit: $first || $last
|
|
366
399
|
};
|
|
367
400
|
}
|
|
@@ -395,39 +428,57 @@ function getBoundCond(orderCols, bound, kind) {
|
|
|
395
428
|
}
|
|
396
429
|
}
|
|
397
430
|
const MAX_LIMIT = 4096;
|
|
398
|
-
function selectByArgs(args, options) {
|
|
431
|
+
function selectByArgs(args, projection, options) {
|
|
399
432
|
const { table } = options;
|
|
400
|
-
const { where, order, limit, meta } = getArgSql(args, options);
|
|
433
|
+
const { where, order, group, limit, meta } = getArgSql(args, options);
|
|
401
434
|
const clampedLimit = Math.min(MAX_LIMIT, limit || MAX_LIMIT);
|
|
402
435
|
return sql__default["default"]`
|
|
403
436
|
SELECT
|
|
404
|
-
${getSelectCols(table)} || ${meta}
|
|
437
|
+
${getSelectCols(table, projection)} || ${meta}
|
|
405
438
|
FROM "${sql.raw(table)}"
|
|
406
439
|
${where.length ? sql__default["default"]`WHERE ${sql.join(where, ` AND `)}` : sql.empty}
|
|
440
|
+
${group ? sql__default["default"]`GROUP BY ${group}` : sql.empty}
|
|
407
441
|
${order ? sql__default["default"]`ORDER BY ${order}` : sql.empty}
|
|
408
442
|
LIMIT ${clampedLimit}
|
|
409
443
|
`;
|
|
410
444
|
}
|
|
411
|
-
function selectByIds(ids, options) {
|
|
445
|
+
function selectByIds(ids, projection, options) {
|
|
412
446
|
const { table, idCol } = options;
|
|
413
447
|
return sql__default["default"]`
|
|
414
448
|
SELECT
|
|
415
|
-
${getSelectCols(table)} || ${getIdMeta(options)}
|
|
449
|
+
${getSelectCols(table, projection)} || ${getIdMeta(options)}
|
|
416
450
|
FROM "${sql.raw(table)}"
|
|
417
451
|
WHERE "${sql.raw(idCol)}" IN (${sql.join(ids)})
|
|
418
452
|
`;
|
|
419
453
|
}
|
|
420
|
-
function
|
|
454
|
+
function getSingleSql(arg, options) {
|
|
421
455
|
const { table, idCol } = options;
|
|
422
|
-
|
|
456
|
+
if (!common.isPlainObject(arg)) {
|
|
457
|
+
return {
|
|
458
|
+
where: sql__default["default"]`"${sql.raw(idCol)}" = ${arg}`,
|
|
459
|
+
meta: getIdMeta(options)
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const { where, meta } = getArgSql(arg, options);
|
|
423
463
|
if (!where || !where.length)
|
|
424
464
|
throw Error("pg_write.no_condition");
|
|
465
|
+
return {
|
|
466
|
+
where: sql__default["default"]`"${sql.raw(idCol)}" = (
|
|
467
|
+
SELECT "${sql.raw(idCol)}"
|
|
468
|
+
FROM "${sql.raw(table)}"
|
|
469
|
+
WHERE ${sql.join(where, ` AND `)}
|
|
470
|
+
LIMIT 1
|
|
471
|
+
)`,
|
|
472
|
+
meta
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function patch(object, arg, options) {
|
|
476
|
+
const { table } = options;
|
|
477
|
+
const { where, meta } = getSingleSql(arg, options);
|
|
425
478
|
const row = object;
|
|
426
479
|
return sql__default["default"]`
|
|
427
480
|
UPDATE "${sql.raw(table)}" SET ${getUpdates(row, options)}
|
|
428
|
-
WHERE ${
|
|
429
|
-
SELECT "${sql.raw(idCol)}" FROM "${sql.raw(table)}"
|
|
430
|
-
WHERE ${sql.join(where, ` AND `)} LIMIT 1)` : sql.join(where, ` AND `)}
|
|
481
|
+
WHERE ${where}
|
|
431
482
|
RETURNING (${getSelectCols(table)} || ${meta})`;
|
|
432
483
|
}
|
|
433
484
|
function put(object, arg, options) {
|
|
@@ -447,6 +498,14 @@ function put(object, arg, options) {
|
|
|
447
498
|
ON CONFLICT (${conflictTarget}) DO UPDATE SET (${cols}) = (${vals})
|
|
448
499
|
RETURNING (${getSelectCols(table)} || ${meta})`;
|
|
449
500
|
}
|
|
501
|
+
function del(arg, options) {
|
|
502
|
+
const { table } = options;
|
|
503
|
+
const { where } = getSingleSql(arg, options);
|
|
504
|
+
return sql__default["default"]`
|
|
505
|
+
DELETE FROM "${sql.raw(table)}"
|
|
506
|
+
WHERE ${where}
|
|
507
|
+
RETURNING (${getJsonBuildObject({ $key: arg })})`;
|
|
508
|
+
}
|
|
450
509
|
const log = debug__default["default"]("graffy:pg:db");
|
|
451
510
|
class Db {
|
|
452
511
|
constructor(connection) {
|
|
@@ -492,14 +551,14 @@ class Db {
|
|
|
492
551
|
const promises = [];
|
|
493
552
|
const results = [];
|
|
494
553
|
const { prefix } = tableOptions;
|
|
495
|
-
const getByArgs = async (args) => {
|
|
496
|
-
const result = await this.readSql(selectByArgs(args, tableOptions));
|
|
554
|
+
const getByArgs = async (args, projection) => {
|
|
555
|
+
const result = await this.readSql(selectByArgs(args, projection, tableOptions));
|
|
497
556
|
const wrappedGraph = common.encodeGraph(common.wrapObject(result, prefix));
|
|
498
557
|
log("getByArgs", wrappedGraph);
|
|
499
558
|
common.merge(results, wrappedGraph);
|
|
500
559
|
};
|
|
501
560
|
const getByIds = async () => {
|
|
502
|
-
const result = await this.readSql(selectByIds(Object.keys(idQueries), tableOptions));
|
|
561
|
+
const result = await this.readSql(selectByIds(Object.keys(idQueries), null, tableOptions));
|
|
503
562
|
result.forEach((object) => {
|
|
504
563
|
const wrappedGraph = common.encodeGraph(common.wrapObject(object, prefix));
|
|
505
564
|
log("getByIds", wrappedGraph);
|
|
@@ -513,10 +572,12 @@ class Db {
|
|
|
513
572
|
if (node.prefix) {
|
|
514
573
|
for (const childNode of node.children) {
|
|
515
574
|
const childArgs = common.decodeArgs(childNode);
|
|
516
|
-
|
|
575
|
+
const projection = childNode.children ? common.decodeQuery(childNode.children) : null;
|
|
576
|
+
promises.push(getByArgs(__spreadValues(__spreadValues({}, args), childArgs), projection));
|
|
517
577
|
}
|
|
518
578
|
} else {
|
|
519
|
-
|
|
579
|
+
const projection = node.children ? common.decodeQuery(node.children) : null;
|
|
580
|
+
promises.push(getByArgs(args, projection));
|
|
520
581
|
}
|
|
521
582
|
} else {
|
|
522
583
|
idQueries[node.key] = node.children;
|
|
@@ -529,15 +590,15 @@ class Db {
|
|
|
529
590
|
return common.slice(common.finalize(results, common.wrap(query, prefix)), rootQuery).known || [];
|
|
530
591
|
}
|
|
531
592
|
async write(rootChange, tableOptions) {
|
|
532
|
-
const sqls = [];
|
|
533
|
-
const addToQuery = (sql2) => sqls.push(sql2);
|
|
534
593
|
const { prefix } = tableOptions;
|
|
535
594
|
const change = common.unwrap(rootChange, prefix);
|
|
536
|
-
|
|
595
|
+
const sqls = change.map((node) => {
|
|
596
|
+
const arg = common.decodeArgs(node);
|
|
537
597
|
if (common.isRange(node)) {
|
|
538
|
-
|
|
598
|
+
if (node.key === node.end)
|
|
599
|
+
return del(arg, tableOptions);
|
|
600
|
+
throw Error("pg_write.write_range_unsupported");
|
|
539
601
|
}
|
|
540
|
-
const arg = common.decodeArgs(node);
|
|
541
602
|
const object = common.decodeGraph(node.children);
|
|
542
603
|
if (common.isPlainObject(arg)) {
|
|
543
604
|
common.mergeObject(object, arg);
|
|
@@ -547,8 +608,8 @@ class Db {
|
|
|
547
608
|
if (object.$put && object.$put !== true) {
|
|
548
609
|
throw Error("pg_write.partial_put_unsupported");
|
|
549
610
|
}
|
|
550
|
-
object.$put ?
|
|
551
|
-
}
|
|
611
|
+
return object.$put ? put(object, arg, tableOptions) : patch(object, arg, tableOptions);
|
|
612
|
+
});
|
|
552
613
|
const result = [];
|
|
553
614
|
await Promise.all(sqls.map((sql2) => this.writeSql(sql2).then((object) => {
|
|
554
615
|
common.merge(result, common.encodeGraph(common.wrapObject(object, prefix)));
|
|
@@ -557,7 +618,7 @@ class Db {
|
|
|
557
618
|
return result;
|
|
558
619
|
}
|
|
559
620
|
}
|
|
560
|
-
const pg = ({ table, idCol, verCol,
|
|
621
|
+
const pg = ({ table, idCol, verCol, connection }) => (store) => {
|
|
561
622
|
store.on("read", read);
|
|
562
623
|
store.on("write", write);
|
|
563
624
|
const prefix = store.path;
|
|
@@ -565,13 +626,13 @@ const pg = ({ table, idCol, verCol, links, connection }) => (store) => {
|
|
|
565
626
|
prefix,
|
|
566
627
|
table: table || prefix[prefix.length - 1] || "default",
|
|
567
628
|
idCol: idCol || "id",
|
|
568
|
-
verCol: verCol || "updatedAt"
|
|
569
|
-
links: links || {}
|
|
629
|
+
verCol: verCol || "updatedAt"
|
|
570
630
|
};
|
|
571
631
|
const defaultDb = new Db(connection);
|
|
572
632
|
function read(query, options, next) {
|
|
573
|
-
const
|
|
574
|
-
const
|
|
633
|
+
const { pgClient } = options;
|
|
634
|
+
const db = pgClient ? new Db(pgClient) : defaultDb;
|
|
635
|
+
const readPromise = db.read(query, tableOpts);
|
|
575
636
|
const remainingQuery = common.remove(query, prefix);
|
|
576
637
|
const nextPromise = next(remainingQuery);
|
|
577
638
|
return Promise.all([readPromise, nextPromise]).then(([readRes, nextRes]) => {
|
|
@@ -579,8 +640,9 @@ const pg = ({ table, idCol, verCol, links, connection }) => (store) => {
|
|
|
579
640
|
});
|
|
580
641
|
}
|
|
581
642
|
function write(change, options, next) {
|
|
582
|
-
const
|
|
583
|
-
const
|
|
643
|
+
const { pgClient } = options;
|
|
644
|
+
const db = pgClient ? new Db(pgClient) : defaultDb;
|
|
645
|
+
const writePromise = db.write(change, tableOpts);
|
|
584
646
|
const remainingChange = common.remove(change, prefix);
|
|
585
647
|
const nextPromise = next(remainingChange);
|
|
586
648
|
return Promise.all([writePromise, nextPromise]).then(([writeRes, nextRes]) => {
|
package/index.mjs
CHANGED
|
@@ -26,7 +26,7 @@ var __objRest = (source, exclude) => {
|
|
|
26
26
|
}
|
|
27
27
|
return target;
|
|
28
28
|
};
|
|
29
|
-
import { isEmpty, encodePath, isPlainObject, unwrap, decodeArgs, slice, finalize, wrap, isRange, decodeGraph, mergeObject, merge, encodeGraph, wrapObject, remove } from "@graffy/common";
|
|
29
|
+
import { isEmpty, encodePath, isPlainObject, unwrap, decodeArgs, decodeQuery, slice, finalize, wrap, isRange, decodeGraph, mergeObject, merge, encodeGraph, wrapObject, remove } from "@graffy/common";
|
|
30
30
|
import { Pool, Client } from "pg";
|
|
31
31
|
import sql, { join, raw, Sql, empty } from "sql-template-tag";
|
|
32
32
|
import debug from "debug";
|
|
@@ -177,7 +177,7 @@ function getCompatibleTypes(value) {
|
|
|
177
177
|
return "text";
|
|
178
178
|
}
|
|
179
179
|
function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
|
|
180
|
-
function
|
|
180
|
+
function lookup2(string, type) {
|
|
181
181
|
if (string.substr(0, 3) === "el$")
|
|
182
182
|
return sql`"${raw(string)}"`;
|
|
183
183
|
return getLookupSql(string, type);
|
|
@@ -186,20 +186,20 @@ function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
|
|
|
186
186
|
const lType = left.substr(0, 3) === "el$" ? "any" : getColumnType(left);
|
|
187
187
|
const rType = getCompatibleTypes(right);
|
|
188
188
|
if (lType === "any" || rType === "any" || rType === lType) {
|
|
189
|
-
return sql`${
|
|
189
|
+
return sql`${lookup2(left)} ${raw(op)} ${right}`;
|
|
190
190
|
} else {
|
|
191
|
-
return sql`(${
|
|
191
|
+
return sql`(${lookup2(left, rType)})::${raw(rType)} ${raw(op)} ${right}`;
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
function getNodeSql(ast) {
|
|
195
195
|
switch (ast[0]) {
|
|
196
196
|
case "$eq":
|
|
197
197
|
if (ast[2] === null)
|
|
198
|
-
return sql`${
|
|
198
|
+
return sql`${lookup2(ast[1])} IS NULL`;
|
|
199
199
|
return binop("=", ast[1], ast[2]);
|
|
200
200
|
case "$neq":
|
|
201
201
|
if (ast[2] === null)
|
|
202
|
-
return sql`${
|
|
202
|
+
return sql`${lookup2(ast[1])} IS NOT NULL`;
|
|
203
203
|
return binop("<>", ast[1], ast[2]);
|
|
204
204
|
case "$lt":
|
|
205
205
|
return binop("<", ast[1], ast[2]);
|
|
@@ -214,20 +214,20 @@ function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
|
|
|
214
214
|
case "$ire":
|
|
215
215
|
return binop("~*", ast[1], ast[2]);
|
|
216
216
|
case "$in":
|
|
217
|
-
return sql`${
|
|
217
|
+
return sql`${lookup2(ast[1])} IN (${join(ast[2])})`;
|
|
218
218
|
case "$nin":
|
|
219
|
-
return sql`${
|
|
219
|
+
return sql`${lookup2(ast[1])} NOT IN (${join(ast[2])})`;
|
|
220
220
|
case "$cts":
|
|
221
|
-
return sql`${
|
|
221
|
+
return sql`${lookup2(ast[1])} @> ${ast[2]}`;
|
|
222
222
|
case "$ctd":
|
|
223
|
-
return sql`${
|
|
223
|
+
return sql`${lookup2(ast[1])} <@ ${ast[2]}`;
|
|
224
224
|
case "$ovl":
|
|
225
225
|
switch (getColumnType(ast[1])) {
|
|
226
226
|
case "jsonb":
|
|
227
227
|
case "any":
|
|
228
|
-
return sql`${
|
|
228
|
+
return sql`${lookup2(ast[1])} ?| ${Array.isArray(ast[2]) ? ast[2] : Object.keys(ast[2])}`;
|
|
229
229
|
case "array":
|
|
230
|
-
return sql`${
|
|
230
|
+
return sql`${lookup2(ast[1])} && ${ast[2]}`;
|
|
231
231
|
default:
|
|
232
232
|
throw Error("pg.getSql_ovl_unknown_column_type");
|
|
233
233
|
}
|
|
@@ -238,11 +238,11 @@ function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
|
|
|
238
238
|
case "$not":
|
|
239
239
|
return sql`NOT (${getNodeSql(ast[1])})`;
|
|
240
240
|
case "$any":
|
|
241
|
-
return sql`(SELECT bool_or(${getNodeSql(ast[3])}) FROM UNNEST(${
|
|
241
|
+
return sql`(SELECT bool_or(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
242
242
|
case "$all":
|
|
243
|
-
return sql`(SELECT bool_and(${getNodeSql(ast[3])}) FROM UNNEST(${
|
|
243
|
+
return sql`(SELECT bool_and(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
244
244
|
case "$has":
|
|
245
|
-
return sql`(SELECT bool_or(${join(ast[3].map((node) => getNodeSql(node)), `) AND bool_or(`)}) FROM UNNEST(${
|
|
245
|
+
return sql`(SELECT bool_or(${join(ast[3].map((node) => getNodeSql(node)), `) AND bool_or(`)}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
|
|
246
246
|
default:
|
|
247
247
|
throw Error("pg.getSql_unknown_operator: " + ast[0]);
|
|
248
248
|
}
|
|
@@ -263,8 +263,40 @@ const getJsonBuildValue = (value) => {
|
|
|
263
263
|
return sql`${value}::text`;
|
|
264
264
|
return sql`${JSON.stringify(stripAttributes(value))}::jsonb`;
|
|
265
265
|
};
|
|
266
|
-
const
|
|
267
|
-
|
|
266
|
+
const lookup = (prop, type) => {
|
|
267
|
+
const [prefix, ...suffix] = encodePath(prop);
|
|
268
|
+
const op = type === "text" ? sql`#>>` : sql`#>`;
|
|
269
|
+
return suffix.length ? sql`"${raw(prefix)}" ${op} ${suffix}` : sql`"${raw(prefix)}"`;
|
|
270
|
+
};
|
|
271
|
+
const getType = (prop) => {
|
|
272
|
+
const [_prefix, ...suffix] = encodePath(prop);
|
|
273
|
+
return suffix.length ? "jsonb" : "any";
|
|
274
|
+
};
|
|
275
|
+
const aggSql = {
|
|
276
|
+
$sum: (prop) => sql`sum((${lookup(prop)})::numeric)`,
|
|
277
|
+
$card: (prop) => sql`count(distinct(${lookup(prop)}))`,
|
|
278
|
+
$avg: (prop) => sql`sum((${lookup(prop)})::numeric)`,
|
|
279
|
+
$max: (prop) => sql`sum((${lookup(prop)})::numeric)`,
|
|
280
|
+
$min: (prop) => sql`sum((${lookup(prop)})::numeric)`
|
|
281
|
+
};
|
|
282
|
+
const getSelectCols = (table, projection = null) => {
|
|
283
|
+
if (!projection)
|
|
284
|
+
return sql`to_jsonb("${raw(table)}")`;
|
|
285
|
+
const sqls = [];
|
|
286
|
+
for (const key in projection) {
|
|
287
|
+
if (key === "$count") {
|
|
288
|
+
sqls.push(sql`'$count', count(*)`);
|
|
289
|
+
} else if (aggSql[key]) {
|
|
290
|
+
const subSqls = [];
|
|
291
|
+
for (const prop in projection[key]) {
|
|
292
|
+
subSqls.push(sql`${prop}::text, ${aggSql[key](prop)}`);
|
|
293
|
+
}
|
|
294
|
+
sqls.push(sql`${key}::text, jsonb_build_object(${join(subSqls, ", ")})`);
|
|
295
|
+
} else {
|
|
296
|
+
sqls.push(sql`${key}::text, "${raw(key)}"`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return sql`jsonb_build_object(${join(sqls, ", ")})`;
|
|
268
300
|
};
|
|
269
301
|
const getInsert = (row, options) => {
|
|
270
302
|
const cols = [];
|
|
@@ -314,20 +346,20 @@ const getArgMeta = (key, prefix, idCol) => getJsonBuildObject({
|
|
|
314
346
|
$ref: sql`jsonb_build_array(${join(prefix.map((k) => sql`${k}::text`))}, "${raw(idCol)}")`,
|
|
315
347
|
$ver: nowTimestamp
|
|
316
348
|
});
|
|
349
|
+
const getAggMeta = (key, $group) => getJsonBuildObject({
|
|
350
|
+
$key: join([key, getJsonBuildObject({ $group })].filter(Boolean), " || "),
|
|
351
|
+
$ver: nowTimestamp
|
|
352
|
+
});
|
|
317
353
|
function getArgSql(_c, options) {
|
|
318
354
|
var _d = _c, { $first, $last, $after, $before, $since, $until, $all, $cursor: _ } = _d, rest = __objRest(_d, ["$first", "$last", "$after", "$before", "$since", "$until", "$all", "$cursor"]);
|
|
319
|
-
const _a = rest, { $order } = _a, filter = __objRest(_a, ["$order"]);
|
|
355
|
+
const _a = rest, { $order, $group } = _a, filter = __objRest(_a, ["$order", "$group"]);
|
|
320
356
|
const { prefix, idCol } = options;
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
const [_prefix, ...suffix] = encodePath(prop);
|
|
328
|
-
return suffix.length ? "jsonb" : "any";
|
|
329
|
-
};
|
|
330
|
-
const meta = (key2) => getArgMeta(key2, prefix, idCol);
|
|
357
|
+
if ($order && $group) {
|
|
358
|
+
throw Error("pg_arg.order_and_group_unsupported in " + prefix);
|
|
359
|
+
}
|
|
360
|
+
const meta = (key2) => $group ? getAggMeta(key2, $group) : getArgMeta(key2, prefix, idCol);
|
|
361
|
+
const groupCols = Array.isArray($group) && $group.length ? $group.map((col) => lookup(col)) : void 0;
|
|
362
|
+
const group = groupCols ? join(groupCols, ", ") : void 0;
|
|
331
363
|
const hasRangeArg = $before || $after || $since || $until || $first || $last || $all || $order;
|
|
332
364
|
let key;
|
|
333
365
|
const where = [];
|
|
@@ -336,7 +368,7 @@ function getArgSql(_c, options) {
|
|
|
336
368
|
key = sql`${JSON.stringify(filter)}::jsonb`;
|
|
337
369
|
}
|
|
338
370
|
if (!hasRangeArg)
|
|
339
|
-
return { meta: meta(key), where, limit: 1 };
|
|
371
|
+
return { meta: meta(key), where, group, limit: 1 };
|
|
340
372
|
if (isEmpty(rest)) {
|
|
341
373
|
throw Error("pg_arg.pagination_only_unsupported in " + prefix);
|
|
342
374
|
}
|
|
@@ -347,13 +379,14 @@ function getArgSql(_c, options) {
|
|
|
347
379
|
});
|
|
348
380
|
const orderQuery = $order && getJsonBuildObject({ $order: sql`${JSON.stringify($order)}::jsonb` });
|
|
349
381
|
const cursorQuery = getJsonBuildObject({
|
|
350
|
-
$cursor: sql`jsonb_build_array(${join(orderCols)})`
|
|
382
|
+
$cursor: sql`jsonb_build_array(${join(groupCols || orderCols)})`
|
|
351
383
|
});
|
|
352
384
|
key = sql`(${join([key, orderQuery, cursorQuery].filter(Boolean), ` || `)})`;
|
|
353
385
|
return {
|
|
354
386
|
meta: meta(key),
|
|
355
387
|
where,
|
|
356
|
-
order: join(orderCols.map((col) => sql`${col} ${$last ? sql`DESC` : sql`ASC`}`), `, `),
|
|
388
|
+
order: $order && join(orderCols.map((col) => sql`${col} ${$last ? sql`DESC` : sql`ASC`}`), `, `),
|
|
389
|
+
group,
|
|
357
390
|
limit: $first || $last
|
|
358
391
|
};
|
|
359
392
|
}
|
|
@@ -387,39 +420,57 @@ function getBoundCond(orderCols, bound, kind) {
|
|
|
387
420
|
}
|
|
388
421
|
}
|
|
389
422
|
const MAX_LIMIT = 4096;
|
|
390
|
-
function selectByArgs(args, options) {
|
|
423
|
+
function selectByArgs(args, projection, options) {
|
|
391
424
|
const { table } = options;
|
|
392
|
-
const { where, order, limit, meta } = getArgSql(args, options);
|
|
425
|
+
const { where, order, group, limit, meta } = getArgSql(args, options);
|
|
393
426
|
const clampedLimit = Math.min(MAX_LIMIT, limit || MAX_LIMIT);
|
|
394
427
|
return sql`
|
|
395
428
|
SELECT
|
|
396
|
-
${getSelectCols(table)} || ${meta}
|
|
429
|
+
${getSelectCols(table, projection)} || ${meta}
|
|
397
430
|
FROM "${raw(table)}"
|
|
398
431
|
${where.length ? sql`WHERE ${join(where, ` AND `)}` : empty}
|
|
432
|
+
${group ? sql`GROUP BY ${group}` : empty}
|
|
399
433
|
${order ? sql`ORDER BY ${order}` : empty}
|
|
400
434
|
LIMIT ${clampedLimit}
|
|
401
435
|
`;
|
|
402
436
|
}
|
|
403
|
-
function selectByIds(ids, options) {
|
|
437
|
+
function selectByIds(ids, projection, options) {
|
|
404
438
|
const { table, idCol } = options;
|
|
405
439
|
return sql`
|
|
406
440
|
SELECT
|
|
407
|
-
${getSelectCols(table)} || ${getIdMeta(options)}
|
|
441
|
+
${getSelectCols(table, projection)} || ${getIdMeta(options)}
|
|
408
442
|
FROM "${raw(table)}"
|
|
409
443
|
WHERE "${raw(idCol)}" IN (${join(ids)})
|
|
410
444
|
`;
|
|
411
445
|
}
|
|
412
|
-
function
|
|
446
|
+
function getSingleSql(arg, options) {
|
|
413
447
|
const { table, idCol } = options;
|
|
414
|
-
|
|
448
|
+
if (!isPlainObject(arg)) {
|
|
449
|
+
return {
|
|
450
|
+
where: sql`"${raw(idCol)}" = ${arg}`,
|
|
451
|
+
meta: getIdMeta(options)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const { where, meta } = getArgSql(arg, options);
|
|
415
455
|
if (!where || !where.length)
|
|
416
456
|
throw Error("pg_write.no_condition");
|
|
457
|
+
return {
|
|
458
|
+
where: sql`"${raw(idCol)}" = (
|
|
459
|
+
SELECT "${raw(idCol)}"
|
|
460
|
+
FROM "${raw(table)}"
|
|
461
|
+
WHERE ${join(where, ` AND `)}
|
|
462
|
+
LIMIT 1
|
|
463
|
+
)`,
|
|
464
|
+
meta
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function patch(object, arg, options) {
|
|
468
|
+
const { table } = options;
|
|
469
|
+
const { where, meta } = getSingleSql(arg, options);
|
|
417
470
|
const row = object;
|
|
418
471
|
return sql`
|
|
419
472
|
UPDATE "${raw(table)}" SET ${getUpdates(row, options)}
|
|
420
|
-
WHERE ${
|
|
421
|
-
SELECT "${raw(idCol)}" FROM "${raw(table)}"
|
|
422
|
-
WHERE ${join(where, ` AND `)} LIMIT 1)` : join(where, ` AND `)}
|
|
473
|
+
WHERE ${where}
|
|
423
474
|
RETURNING (${getSelectCols(table)} || ${meta})`;
|
|
424
475
|
}
|
|
425
476
|
function put(object, arg, options) {
|
|
@@ -439,6 +490,14 @@ function put(object, arg, options) {
|
|
|
439
490
|
ON CONFLICT (${conflictTarget}) DO UPDATE SET (${cols}) = (${vals})
|
|
440
491
|
RETURNING (${getSelectCols(table)} || ${meta})`;
|
|
441
492
|
}
|
|
493
|
+
function del(arg, options) {
|
|
494
|
+
const { table } = options;
|
|
495
|
+
const { where } = getSingleSql(arg, options);
|
|
496
|
+
return sql`
|
|
497
|
+
DELETE FROM "${raw(table)}"
|
|
498
|
+
WHERE ${where}
|
|
499
|
+
RETURNING (${getJsonBuildObject({ $key: arg })})`;
|
|
500
|
+
}
|
|
442
501
|
const log = debug("graffy:pg:db");
|
|
443
502
|
class Db {
|
|
444
503
|
constructor(connection) {
|
|
@@ -484,14 +543,14 @@ class Db {
|
|
|
484
543
|
const promises = [];
|
|
485
544
|
const results = [];
|
|
486
545
|
const { prefix } = tableOptions;
|
|
487
|
-
const getByArgs = async (args) => {
|
|
488
|
-
const result = await this.readSql(selectByArgs(args, tableOptions));
|
|
546
|
+
const getByArgs = async (args, projection) => {
|
|
547
|
+
const result = await this.readSql(selectByArgs(args, projection, tableOptions));
|
|
489
548
|
const wrappedGraph = encodeGraph(wrapObject(result, prefix));
|
|
490
549
|
log("getByArgs", wrappedGraph);
|
|
491
550
|
merge(results, wrappedGraph);
|
|
492
551
|
};
|
|
493
552
|
const getByIds = async () => {
|
|
494
|
-
const result = await this.readSql(selectByIds(Object.keys(idQueries), tableOptions));
|
|
553
|
+
const result = await this.readSql(selectByIds(Object.keys(idQueries), null, tableOptions));
|
|
495
554
|
result.forEach((object) => {
|
|
496
555
|
const wrappedGraph = encodeGraph(wrapObject(object, prefix));
|
|
497
556
|
log("getByIds", wrappedGraph);
|
|
@@ -505,10 +564,12 @@ class Db {
|
|
|
505
564
|
if (node.prefix) {
|
|
506
565
|
for (const childNode of node.children) {
|
|
507
566
|
const childArgs = decodeArgs(childNode);
|
|
508
|
-
|
|
567
|
+
const projection = childNode.children ? decodeQuery(childNode.children) : null;
|
|
568
|
+
promises.push(getByArgs(__spreadValues(__spreadValues({}, args), childArgs), projection));
|
|
509
569
|
}
|
|
510
570
|
} else {
|
|
511
|
-
|
|
571
|
+
const projection = node.children ? decodeQuery(node.children) : null;
|
|
572
|
+
promises.push(getByArgs(args, projection));
|
|
512
573
|
}
|
|
513
574
|
} else {
|
|
514
575
|
idQueries[node.key] = node.children;
|
|
@@ -521,15 +582,15 @@ class Db {
|
|
|
521
582
|
return slice(finalize(results, wrap(query, prefix)), rootQuery).known || [];
|
|
522
583
|
}
|
|
523
584
|
async write(rootChange, tableOptions) {
|
|
524
|
-
const sqls = [];
|
|
525
|
-
const addToQuery = (sql2) => sqls.push(sql2);
|
|
526
585
|
const { prefix } = tableOptions;
|
|
527
586
|
const change = unwrap(rootChange, prefix);
|
|
528
|
-
|
|
587
|
+
const sqls = change.map((node) => {
|
|
588
|
+
const arg = decodeArgs(node);
|
|
529
589
|
if (isRange(node)) {
|
|
530
|
-
|
|
590
|
+
if (node.key === node.end)
|
|
591
|
+
return del(arg, tableOptions);
|
|
592
|
+
throw Error("pg_write.write_range_unsupported");
|
|
531
593
|
}
|
|
532
|
-
const arg = decodeArgs(node);
|
|
533
594
|
const object = decodeGraph(node.children);
|
|
534
595
|
if (isPlainObject(arg)) {
|
|
535
596
|
mergeObject(object, arg);
|
|
@@ -539,8 +600,8 @@ class Db {
|
|
|
539
600
|
if (object.$put && object.$put !== true) {
|
|
540
601
|
throw Error("pg_write.partial_put_unsupported");
|
|
541
602
|
}
|
|
542
|
-
object.$put ?
|
|
543
|
-
}
|
|
603
|
+
return object.$put ? put(object, arg, tableOptions) : patch(object, arg, tableOptions);
|
|
604
|
+
});
|
|
544
605
|
const result = [];
|
|
545
606
|
await Promise.all(sqls.map((sql2) => this.writeSql(sql2).then((object) => {
|
|
546
607
|
merge(result, encodeGraph(wrapObject(object, prefix)));
|
|
@@ -549,7 +610,7 @@ class Db {
|
|
|
549
610
|
return result;
|
|
550
611
|
}
|
|
551
612
|
}
|
|
552
|
-
const pg = ({ table, idCol, verCol,
|
|
613
|
+
const pg = ({ table, idCol, verCol, connection }) => (store) => {
|
|
553
614
|
store.on("read", read);
|
|
554
615
|
store.on("write", write);
|
|
555
616
|
const prefix = store.path;
|
|
@@ -557,13 +618,13 @@ const pg = ({ table, idCol, verCol, links, connection }) => (store) => {
|
|
|
557
618
|
prefix,
|
|
558
619
|
table: table || prefix[prefix.length - 1] || "default",
|
|
559
620
|
idCol: idCol || "id",
|
|
560
|
-
verCol: verCol || "updatedAt"
|
|
561
|
-
links: links || {}
|
|
621
|
+
verCol: verCol || "updatedAt"
|
|
562
622
|
};
|
|
563
623
|
const defaultDb = new Db(connection);
|
|
564
624
|
function read(query, options, next) {
|
|
565
|
-
const
|
|
566
|
-
const
|
|
625
|
+
const { pgClient } = options;
|
|
626
|
+
const db = pgClient ? new Db(pgClient) : defaultDb;
|
|
627
|
+
const readPromise = db.read(query, tableOpts);
|
|
567
628
|
const remainingQuery = remove(query, prefix);
|
|
568
629
|
const nextPromise = next(remainingQuery);
|
|
569
630
|
return Promise.all([readPromise, nextPromise]).then(([readRes, nextRes]) => {
|
|
@@ -571,8 +632,9 @@ const pg = ({ table, idCol, verCol, links, connection }) => (store) => {
|
|
|
571
632
|
});
|
|
572
633
|
}
|
|
573
634
|
function write(change, options, next) {
|
|
574
|
-
const
|
|
575
|
-
const
|
|
635
|
+
const { pgClient } = options;
|
|
636
|
+
const db = pgClient ? new Db(pgClient) : defaultDb;
|
|
637
|
+
const writePromise = db.write(change, tableOpts);
|
|
576
638
|
const remainingChange = remove(change, prefix);
|
|
577
639
|
const nextPromise = next(remainingChange);
|
|
578
640
|
return Promise.all([writePromise, nextPromise]).then(([writeRes, nextRes]) => {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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.15.
|
|
5
|
+
"version": "0.15.13",
|
|
6
6
|
"main": "./index.cjs",
|
|
7
7
|
"exports": {
|
|
8
8
|
"import": "./index.mjs",
|
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
},
|
|
17
17
|
"license": "Apache-2.0",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@graffy/common": "0.15.
|
|
20
|
-
"pg": "^8.7.1",
|
|
19
|
+
"@graffy/common": "0.15.13",
|
|
21
20
|
"debug": "^4.3.2",
|
|
22
21
|
"sql-template-tag": "^4.0.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"pg": "^8.0.0"
|
|
23
25
|
}
|
|
24
26
|
}
|
package/types/index.d.ts
CHANGED
package/types/sql/clauses.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export const nowTimestamp: Sql;
|
|
2
2
|
export function getJsonBuildObject(variadic: any): Sql;
|
|
3
|
-
export function
|
|
3
|
+
export function lookup(prop: any, type: any): Sql;
|
|
4
|
+
export function getType(prop: any): "any" | "jsonb";
|
|
5
|
+
export function getSelectCols(table: any, projection?: any): Sql;
|
|
4
6
|
export function getInsert(row: any, options: any): {
|
|
5
7
|
cols: Sql;
|
|
6
8
|
vals: Sql;
|
package/types/sql/getArgSql.d.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
@param {object} options
|
|
6
6
|
|
|
7
7
|
@typedef { import('sql-template-tag').Sql } Sql
|
|
8
|
-
@return {{ meta: Sql, where: Sql[], order?: Sql, limit: number }}
|
|
8
|
+
@return {{ meta: Sql, where: Sql[], order?: Sql, group?: Sql, limit: number }}
|
|
9
9
|
*/
|
|
10
10
|
export default function getArgSql({ $first, $last, $after, $before, $since, $until, $all, $cursor: _, ...rest }: object, options: object): {
|
|
11
11
|
meta: Sql;
|
|
12
12
|
where: Sql[];
|
|
13
13
|
order?: Sql;
|
|
14
|
+
group?: Sql;
|
|
14
15
|
limit: number;
|
|
15
16
|
};
|
|
16
17
|
/**
|
package/types/sql/getMeta.d.ts
CHANGED
package/types/sql/select.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export function selectByArgs(args: any, options: any): import("sql-template-tag").Sql;
|
|
2
|
-
export function selectByIds(ids: any, options: any): import("sql-template-tag").Sql;
|
|
1
|
+
export function selectByArgs(args: any, projection: any, options: any): import("sql-template-tag").Sql;
|
|
2
|
+
export function selectByIds(ids: any, projection: any, options: any): import("sql-template-tag").Sql;
|
package/types/sql/upsert.d.ts
CHANGED