@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
package/sql/clauses.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { isEmpty } from '@graffy/common';
|
|
2
|
+
import sql, { empty, join, raw, Sql } from 'sql-template-tag';
|
|
3
|
+
/*
|
|
4
|
+
Important: This function assumes that the object's keys are from
|
|
5
|
+
trusted sources.
|
|
6
|
+
*/
|
|
7
|
+
export const getJsonBuildTrusted = (variadic) => {
|
|
8
|
+
const args = join(Object.entries(variadic).map(([name, value]) => {
|
|
9
|
+
return sql `'${raw(name)}', ${getJsonBuildValue(value)}`;
|
|
10
|
+
}));
|
|
11
|
+
return sql `jsonb_build_object(${args})`;
|
|
12
|
+
};
|
|
13
|
+
const getJsonBuildValue = (value) => {
|
|
14
|
+
if (value instanceof Sql)
|
|
15
|
+
return value;
|
|
16
|
+
if (typeof value === 'string')
|
|
17
|
+
return sql `${value}::text`;
|
|
18
|
+
return sql `${JSON.stringify(stripAttributes(value))}::jsonb`;
|
|
19
|
+
};
|
|
20
|
+
function colName(prefix, options) {
|
|
21
|
+
if (!options.schema.types[prefix])
|
|
22
|
+
throw Error(`pg.no_column ${prefix}`);
|
|
23
|
+
return raw(prefix);
|
|
24
|
+
}
|
|
25
|
+
export const lookup = (prop, options) => {
|
|
26
|
+
const [prefix, ...suffix] = prop.split('.');
|
|
27
|
+
if (!suffix.length)
|
|
28
|
+
return sql `"${colName(prefix, options)}"`;
|
|
29
|
+
const { types } = options.schema;
|
|
30
|
+
if (types[prefix] === 'jsonb') {
|
|
31
|
+
return sql `"${raw(prefix)}" #> ${suffix}`;
|
|
32
|
+
}
|
|
33
|
+
if (types[prefix] === 'cube' && suffix.length === 1) {
|
|
34
|
+
return sql `"${raw(prefix)}" ~> ${Number.parseInt(suffix[0], 10)}`;
|
|
35
|
+
}
|
|
36
|
+
throw Error(`pg.cannot_lookup ${prop}`);
|
|
37
|
+
};
|
|
38
|
+
export const lookupNumeric = (prop) => {
|
|
39
|
+
const [prefix, ...suffix] = prop.split('.');
|
|
40
|
+
return suffix.length
|
|
41
|
+
? sql `CASE WHEN "${raw(prefix)}" #> ${suffix} = 'null'::jsonb
|
|
42
|
+
THEN 0 ELSE ("${raw(prefix)}" #> ${suffix})::numeric END`
|
|
43
|
+
: sql `"${raw(prefix)}"`;
|
|
44
|
+
};
|
|
45
|
+
const aggSql = {
|
|
46
|
+
$sum: (prop) => sql `sum((${lookupNumeric(prop)})::numeric)`,
|
|
47
|
+
$card: (prop, options) => sql `count(distinct(${lookup(prop, options)}))`,
|
|
48
|
+
$avg: (prop) => sql `avg((${lookupNumeric(prop)})::numeric)`,
|
|
49
|
+
$max: (prop) => sql `max((${lookupNumeric(prop)})::numeric)`,
|
|
50
|
+
$min: (prop) => sql `min((${lookupNumeric(prop)})::numeric)`,
|
|
51
|
+
};
|
|
52
|
+
const getOptimisedJsonBuild = (object, path, parentPropertyName, options) => {
|
|
53
|
+
const propertyNames = Object.keys(object);
|
|
54
|
+
const propertyPath = [...path, parentPropertyName];
|
|
55
|
+
const buildConfig = [];
|
|
56
|
+
path = path || [];
|
|
57
|
+
propertyNames.forEach((propertyName) => {
|
|
58
|
+
const property = object[propertyName];
|
|
59
|
+
if (typeof property === 'object') {
|
|
60
|
+
const childJSONBuild = getOptimisedJsonBuild(property, propertyPath, propertyName, options);
|
|
61
|
+
buildConfig.push(sql `${propertyName}::text, jsonb_build_object(${join(childJSONBuild, ', ')})`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
if (property === true) {
|
|
65
|
+
buildConfig.push(sql `${propertyName}::text, ${lookup([...propertyPath, propertyName].join('.'), options)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return buildConfig;
|
|
70
|
+
};
|
|
71
|
+
export const getSelectCols = (options, projection = null) => {
|
|
72
|
+
if (!projection)
|
|
73
|
+
return sql `*`;
|
|
74
|
+
const sqls = [];
|
|
75
|
+
for (const key in projection) {
|
|
76
|
+
if (key === '$count') {
|
|
77
|
+
sqls.push(sql `count(*) AS "$count"`);
|
|
78
|
+
}
|
|
79
|
+
else if (aggSql[key]) {
|
|
80
|
+
const subSqls = [];
|
|
81
|
+
for (const prop in projection[key]) {
|
|
82
|
+
subSqls.push(sql `${prop}::text, ${aggSql[key](prop, options)}`);
|
|
83
|
+
}
|
|
84
|
+
sqls.push(sql `jsonb_build_object(${join(subSqls, ', ')}) AS "${raw(key)}"`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
if (typeof projection[key] === 'object') {
|
|
88
|
+
const optimisedJsonBuild = getOptimisedJsonBuild(projection[key], [], key, options);
|
|
89
|
+
sqls.push(sql `jsonb_build_object(${join(optimisedJsonBuild, ', ')}) AS "${raw(key)}"`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
sqls.push(sql `"${raw(key)}"`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return join(sqls, ', ');
|
|
97
|
+
};
|
|
98
|
+
function vertexSql(array, nullValue) {
|
|
99
|
+
return sql `array[${join(array.map((num) => (num === null ? nullValue : num)))}]::float8[]`;
|
|
100
|
+
}
|
|
101
|
+
export function cubeLiteralSql(value) {
|
|
102
|
+
if (!(Array.isArray(value) && value.length) ||
|
|
103
|
+
(Array.isArray(value[0]) && value.length !== 2)) {
|
|
104
|
+
throw Error(`pg.castValue_bad_cube${JSON.stringify(value)}`);
|
|
105
|
+
}
|
|
106
|
+
return Array.isArray(value[0])
|
|
107
|
+
? sql `cube(${vertexSql(value[0], sql `'-Infinity'`)}, ${vertexSql(value[1], sql `'Infinity'`)})`
|
|
108
|
+
: sql `cube(${vertexSql(value, 0)})`;
|
|
109
|
+
}
|
|
110
|
+
function castValue(value, type, name, isPut) {
|
|
111
|
+
if (!type)
|
|
112
|
+
throw Error(`pg.write_no_column ${name}`);
|
|
113
|
+
if (value instanceof Sql)
|
|
114
|
+
return value;
|
|
115
|
+
if (value === null)
|
|
116
|
+
return sql `NULL`;
|
|
117
|
+
if (type === 'jsonb') {
|
|
118
|
+
return isPut
|
|
119
|
+
? JSON.stringify(stripAttributes(value))
|
|
120
|
+
: getJsonUpdate(value, name, [])[0];
|
|
121
|
+
}
|
|
122
|
+
if (type === 'cube')
|
|
123
|
+
return cubeLiteralSql(value);
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
export const getInsert = (rows, options) => {
|
|
127
|
+
const { verCol, schema } = options;
|
|
128
|
+
const cols = [];
|
|
129
|
+
const colSqls = [];
|
|
130
|
+
const colIx = {};
|
|
131
|
+
const colUsed = [];
|
|
132
|
+
for (const col of Object.keys(options.schema.types)) {
|
|
133
|
+
colIx[col] = cols.length;
|
|
134
|
+
colUsed[cols.length] = false;
|
|
135
|
+
cols.push(col);
|
|
136
|
+
colSqls.push(sql `"${raw(col)}"`);
|
|
137
|
+
}
|
|
138
|
+
colUsed[colIx[verCol]] = true;
|
|
139
|
+
const vals = [];
|
|
140
|
+
for (const row of rows) {
|
|
141
|
+
const rowVals = Array(cols.length).fill(sql `default`);
|
|
142
|
+
for (const col of cols) {
|
|
143
|
+
if (col === verCol || !(col in row))
|
|
144
|
+
continue;
|
|
145
|
+
const ix = colIx[col];
|
|
146
|
+
colUsed[ix] = true;
|
|
147
|
+
rowVals[ix] = castValue(row[col], schema.types[col], col, row.$put);
|
|
148
|
+
}
|
|
149
|
+
vals.push(rowVals);
|
|
150
|
+
}
|
|
151
|
+
const isUsed = (_, ix) => colUsed[ix];
|
|
152
|
+
return {
|
|
153
|
+
cols: join(colSqls.filter(isUsed), ', '),
|
|
154
|
+
vals: join(vals.map((rowVals) => sql `(${join(rowVals.filter(isUsed), ', ')})`), ', '),
|
|
155
|
+
updates: join(colSqls
|
|
156
|
+
.map((col, _ix) => sql `${col} = "excluded".${col}`)
|
|
157
|
+
.filter(isUsed), ', '),
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
export const getUpdates = (row, options) => {
|
|
161
|
+
return join(Object.entries(row)
|
|
162
|
+
.filter(([col]) => col !== options.idCol && col[0] !== '$')
|
|
163
|
+
.map(([col, val]) => sql `"${raw(col)}" = ${castValue(val, options.schema.types[col], col, row.$put)}`)
|
|
164
|
+
.concat(sql `"${raw(options.verCol)}" = default`), ', ');
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* @param {any} object
|
|
168
|
+
* @param {string} col
|
|
169
|
+
* @param {string[]} path
|
|
170
|
+
* @returns {[Sql, boolean]}
|
|
171
|
+
* The boolean is true if the SQL may evaluate to null
|
|
172
|
+
*/
|
|
173
|
+
function getJsonUpdate(object, col, path) {
|
|
174
|
+
if (!object ||
|
|
175
|
+
typeof object !== 'object' ||
|
|
176
|
+
Array.isArray(object) ||
|
|
177
|
+
object.$put) {
|
|
178
|
+
const patch = stripAttributes(object);
|
|
179
|
+
return [sql `${JSON.stringify(patch)}::jsonb`, patch === null];
|
|
180
|
+
}
|
|
181
|
+
if ('$val' in object) {
|
|
182
|
+
const value = object.$val === true ? stripAttributes(object) : object.$val;
|
|
183
|
+
return [sql `${JSON.stringify({ $val: value })}::jsonb`, false];
|
|
184
|
+
}
|
|
185
|
+
const curr = sql `"${raw(col)}"${path.length ? sql `#>${path}` : empty}`;
|
|
186
|
+
if (isEmpty(object))
|
|
187
|
+
return [curr, false];
|
|
188
|
+
const baseSql = sql `case jsonb_typeof(${curr})
|
|
189
|
+
when 'object' then ${curr}
|
|
190
|
+
else '{}'::jsonb
|
|
191
|
+
end`;
|
|
192
|
+
let maybeNull = true; // Is it possible for this object to lose *all* its properties?
|
|
193
|
+
let hasNulls = false; // Is it possible that one of the properties becomes null?
|
|
194
|
+
const patchSqls = Object.entries(object).map(([key, value]) => {
|
|
195
|
+
const [valSql, nullable] = getJsonUpdate(value, col, path.concat(key));
|
|
196
|
+
maybeNull && (maybeNull = nullable);
|
|
197
|
+
hasNulls || (hasNulls = nullable);
|
|
198
|
+
return sql `${key}::text, ${valSql}`;
|
|
199
|
+
});
|
|
200
|
+
let clause = sql `${baseSql} || jsonb_build_object(${join(patchSqls, ', ')})`;
|
|
201
|
+
if (hasNulls) {
|
|
202
|
+
clause = sql `(select jsonb_object_agg(key, value)
|
|
203
|
+
from jsonb_each(${clause}) where value <> 'null'::jsonb)`;
|
|
204
|
+
}
|
|
205
|
+
if (maybeNull) {
|
|
206
|
+
clause = sql `nullif(${clause}, '{}'::jsonb)`;
|
|
207
|
+
}
|
|
208
|
+
return [clause, maybeNull];
|
|
209
|
+
}
|
|
210
|
+
function stripAttributes(object) {
|
|
211
|
+
if (typeof object !== 'object' || !object)
|
|
212
|
+
return object;
|
|
213
|
+
if (Array.isArray(object)) {
|
|
214
|
+
return object.map((item) => stripAttributes(item));
|
|
215
|
+
}
|
|
216
|
+
return Object.entries(object).reduce((out, [key, val]) => {
|
|
217
|
+
if (key === '$put' || val === null)
|
|
218
|
+
return out;
|
|
219
|
+
if (out === null)
|
|
220
|
+
out = {};
|
|
221
|
+
out[key] = stripAttributes(val);
|
|
222
|
+
return out;
|
|
223
|
+
}, null);
|
|
224
|
+
}
|
package/sql/format.d.ts
ADDED
package/sql/format.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { escapeLiteral } from 'pg';
|
|
2
|
+
import { format } from 'sql-formatter';
|
|
3
|
+
/**
|
|
4
|
+
* @param {any} value
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
function formatSqlParam(value) {
|
|
8
|
+
if (value === null || value === undefined)
|
|
9
|
+
return 'null';
|
|
10
|
+
if (['number', 'boolean'].includes(typeof value))
|
|
11
|
+
return value.toString();
|
|
12
|
+
switch (typeof value) {
|
|
13
|
+
case 'number':
|
|
14
|
+
case 'boolean':
|
|
15
|
+
return value.toString();
|
|
16
|
+
case 'string':
|
|
17
|
+
return escapeLiteral(value);
|
|
18
|
+
case 'object':
|
|
19
|
+
if (Array.isArray(value))
|
|
20
|
+
return `array[${value.map(formatSqlParam).join(', ')}]`;
|
|
21
|
+
return `'${JSON.stringify(value)}'`;
|
|
22
|
+
default:
|
|
23
|
+
// shouldn't be reached
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* @param {import('sql-template-tag').Sql} sql
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export default function formatSql(sql) {
|
|
32
|
+
return format(sql.text, {
|
|
33
|
+
language: 'postgresql',
|
|
34
|
+
params: Object.fromEntries(sql.values.map((value, idx) => [idx + 1, formatSqlParam(value)])),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Uses the args object (typically passed in the $key attribute)
|
|
3
|
+
|
|
4
|
+
@param {object} args
|
|
5
|
+
@param {{prefix: string, idCol: string, verDefault: string, schema: { types: Record<string, any> } }} options
|
|
6
|
+
|
|
7
|
+
@typedef { import('sql-template-tag').Sql } Sql
|
|
8
|
+
@return {{ meta: Sql, where: Sql[], order?: Sql, group?: Sql, limit: number, ensureSingleRow: boolean }}
|
|
9
|
+
*/
|
|
10
|
+
export default function getArgSql({ $first, $last, $after, $before, $since, $until, $all, $cursor: _, ...rest }: {
|
|
11
|
+
[x: string]: any;
|
|
12
|
+
$first: any;
|
|
13
|
+
$last: any;
|
|
14
|
+
$after: any;
|
|
15
|
+
$before: any;
|
|
16
|
+
$since: any;
|
|
17
|
+
$until: any;
|
|
18
|
+
$all: any;
|
|
19
|
+
$cursor: any;
|
|
20
|
+
}, options: any): {
|
|
21
|
+
meta: import("sql-template-tag").Sql;
|
|
22
|
+
where: any[];
|
|
23
|
+
limit: number;
|
|
24
|
+
ensureSingleRow: boolean;
|
|
25
|
+
order?: undefined;
|
|
26
|
+
group?: undefined;
|
|
27
|
+
} | {
|
|
28
|
+
meta: import("sql-template-tag").Sql;
|
|
29
|
+
where: any[];
|
|
30
|
+
order: import("sql-template-tag").Sql;
|
|
31
|
+
group: import("sql-template-tag").Sql;
|
|
32
|
+
limit: any;
|
|
33
|
+
ensureSingleRow: boolean;
|
|
34
|
+
};
|
package/sql/getArgSql.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { isEmpty } from '@graffy/common';
|
|
2
|
+
import sql, { join } from 'sql-template-tag';
|
|
3
|
+
import { getFilterSql } from "../filter/index.js";
|
|
4
|
+
import { getJsonBuildTrusted, lookup } from "./clauses.js";
|
|
5
|
+
import { getAggMeta, getArgMeta } from "./getMeta.js";
|
|
6
|
+
/**
|
|
7
|
+
Uses the args object (typically passed in the $key attribute)
|
|
8
|
+
|
|
9
|
+
@param {object} args
|
|
10
|
+
@param {{prefix: string, idCol: string, verDefault: string, schema: { types: Record<string, any> } }} options
|
|
11
|
+
|
|
12
|
+
@typedef { import('sql-template-tag').Sql } Sql
|
|
13
|
+
@return {{ meta: Sql, where: Sql[], order?: Sql, group?: Sql, limit: number, ensureSingleRow: boolean }}
|
|
14
|
+
*/
|
|
15
|
+
export default function getArgSql({ $first, $last, $after, $before, $since, $until, $all, $cursor: _, ...rest }, options) {
|
|
16
|
+
const { $order, $group, ...filter } = rest;
|
|
17
|
+
const { prefix, idCol } = options;
|
|
18
|
+
const meta = (key) => $group ? getAggMeta(key, options) : getArgMeta(key, options);
|
|
19
|
+
const hasRangeArg = $before || $after || $since || $until || $first || $last || $all;
|
|
20
|
+
if ($order && $group) {
|
|
21
|
+
// TODO: Allow this.
|
|
22
|
+
throw Error(`pg_arg.order_and_group_unsupported in ${prefix}`);
|
|
23
|
+
}
|
|
24
|
+
if (($order || ($group && $group !== true)) && !hasRangeArg) {
|
|
25
|
+
throw Error(`pg_arg.range_arg_expected in ${prefix}`);
|
|
26
|
+
}
|
|
27
|
+
const baseKey = sql `${JSON.stringify(rest)}::jsonb`;
|
|
28
|
+
const where = [];
|
|
29
|
+
if (!isEmpty(filter))
|
|
30
|
+
where.push(getFilterSql(filter, options));
|
|
31
|
+
if (!hasRangeArg)
|
|
32
|
+
return {
|
|
33
|
+
meta: meta(baseKey),
|
|
34
|
+
where,
|
|
35
|
+
limit: 1,
|
|
36
|
+
ensureSingleRow: $group === true,
|
|
37
|
+
};
|
|
38
|
+
const groupCols = Array.isArray($group) &&
|
|
39
|
+
$group.length &&
|
|
40
|
+
$group.map((prop) => lookup(prop, options));
|
|
41
|
+
const group = groupCols ? join(groupCols, ', ') : undefined;
|
|
42
|
+
const orderCols = ($order || [idCol]).map((orderItem) => orderItem[0] === '!'
|
|
43
|
+
? sql `-(${lookup(orderItem.slice(1), options)})::float8`
|
|
44
|
+
: lookup(orderItem, options));
|
|
45
|
+
Object.entries({ $after, $before, $since, $until }).forEach(([name, value]) => {
|
|
46
|
+
if (value)
|
|
47
|
+
where.push(getBoundCond(orderCols, value, name));
|
|
48
|
+
});
|
|
49
|
+
const order = !$group &&
|
|
50
|
+
join(($order || [idCol]).map((orderItem) => orderItem[0] === '!'
|
|
51
|
+
? sql `${lookup(orderItem.slice(1), options)} ${$last ? sql `ASC` : sql `DESC`}`
|
|
52
|
+
: sql `${lookup(orderItem, options)} ${$last ? sql `DESC` : sql `ASC`}`), ', ');
|
|
53
|
+
const cursorKey = getJsonBuildTrusted({
|
|
54
|
+
$cursor: $group === true
|
|
55
|
+
? sql `''`
|
|
56
|
+
: sql `jsonb_build_array(${join(groupCols || orderCols)})`,
|
|
57
|
+
});
|
|
58
|
+
const key = sql `(${baseKey} || ${cursorKey})`;
|
|
59
|
+
return {
|
|
60
|
+
meta: meta(key),
|
|
61
|
+
where,
|
|
62
|
+
order,
|
|
63
|
+
group,
|
|
64
|
+
limit: $first || $last,
|
|
65
|
+
ensureSingleRow: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function getBoundCond(orderCols, bound, kind) {
|
|
69
|
+
if (!Array.isArray(bound) || orderCols.length === 0 || bound.length === 0) {
|
|
70
|
+
throw Error(`pg_arg.bad_query bound : ${JSON.stringify(bound)}`);
|
|
71
|
+
}
|
|
72
|
+
switch (kind) {
|
|
73
|
+
case '$after':
|
|
74
|
+
return sql `(${join(orderCols, ',')}) > (${join(bound, ',')})`;
|
|
75
|
+
case '$since':
|
|
76
|
+
return sql `(${join(orderCols, ',')}) >= (${join(bound, ',')})`;
|
|
77
|
+
case '$before':
|
|
78
|
+
return sql `(${join(orderCols, ',')}) < (${join(bound, ',')})`;
|
|
79
|
+
case '$until':
|
|
80
|
+
return sql `(${join(orderCols, ',')}) <= (${join(bound, ',')})`;
|
|
81
|
+
}
|
|
82
|
+
}
|
package/sql/getMeta.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const getIdMeta: ({ idCol, verDefault }: {
|
|
2
|
+
idCol: any;
|
|
3
|
+
verDefault: any;
|
|
4
|
+
}) => import("sql-template-tag").Sql;
|
|
5
|
+
export declare const getArgMeta: (key: any, { prefix, idCol, verDefault }: {
|
|
6
|
+
prefix: any;
|
|
7
|
+
idCol: any;
|
|
8
|
+
verDefault: any;
|
|
9
|
+
}) => import("sql-template-tag").Sql;
|
|
10
|
+
export declare const getAggMeta: (key: any, { verDefault }: {
|
|
11
|
+
verDefault: any;
|
|
12
|
+
}) => import("sql-template-tag").Sql;
|
package/sql/getMeta.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import sql, { join, raw } from 'sql-template-tag';
|
|
2
|
+
export const getIdMeta = ({ idCol, verDefault }) => sql `"${raw(idCol)}" AS "$key", ${raw(verDefault)} AS "$ver"`;
|
|
3
|
+
export const getArgMeta = (key, { prefix, idCol, verDefault }) => sql `
|
|
4
|
+
${key} AS "$key",
|
|
5
|
+
${raw(verDefault)} AS "$ver",
|
|
6
|
+
array[
|
|
7
|
+
${join(prefix.map((k) => sql `${k}::text`))},
|
|
8
|
+
"${raw(idCol)}"
|
|
9
|
+
]::text[] AS "$ref"
|
|
10
|
+
`;
|
|
11
|
+
export const getAggMeta = (key, { verDefault }) => sql `${key} AS "$key", ${raw(verDefault)} AS "$ver"`;
|
package/sql/index.d.ts
ADDED
package/sql/select.d.ts
ADDED
package/sql/select.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import sql, { empty, join, raw } from 'sql-template-tag';
|
|
2
|
+
import { getSelectCols } from "./clauses.js";
|
|
3
|
+
import getArgSql from "./getArgSql.js";
|
|
4
|
+
import { getIdMeta } from "./getMeta.js";
|
|
5
|
+
const MAX_LIMIT = 4096;
|
|
6
|
+
export function selectByArgs(args, projection, options) {
|
|
7
|
+
const { table, idCol } = options;
|
|
8
|
+
const { where, order, group, limit, meta, ensureSingleRow } = getArgSql(args, options);
|
|
9
|
+
const clampedLimit = Math.min(MAX_LIMIT, limit || MAX_LIMIT);
|
|
10
|
+
// We use an "id" = (... LIMIT 2) query to ensure an error is thrown if more
|
|
11
|
+
// the WHERE clause matches multiple rows.
|
|
12
|
+
if (!ensureSingleRow) {
|
|
13
|
+
return sql `
|
|
14
|
+
SELECT
|
|
15
|
+
${getSelectCols(options, projection)}, ${meta}
|
|
16
|
+
FROM "${raw(table)}" WHERE "${raw(idCol)}" = (
|
|
17
|
+
SELECT "${raw(idCol)}" FROM "${raw(table)}"
|
|
18
|
+
${where.length ? sql `WHERE ${join(where, ' AND ')}` : empty}
|
|
19
|
+
LIMIT 2
|
|
20
|
+
)
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
return sql `
|
|
24
|
+
SELECT
|
|
25
|
+
${getSelectCols(options, projection)}, ${meta}
|
|
26
|
+
FROM "${raw(table)}"
|
|
27
|
+
${where.length ? sql `WHERE ${join(where, ' AND ')}` : empty}
|
|
28
|
+
${group ? sql `GROUP BY ${group}` : empty}
|
|
29
|
+
${order ? sql `ORDER BY ${order}` : empty}
|
|
30
|
+
LIMIT ${clampedLimit}
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
export function selectByIds(ids, projection, options) {
|
|
34
|
+
const { table, idCol } = options;
|
|
35
|
+
return sql `
|
|
36
|
+
SELECT
|
|
37
|
+
${getSelectCols(options, projection)}, ${getIdMeta(options)}
|
|
38
|
+
FROM "${raw(table)}"
|
|
39
|
+
WHERE "${raw(idCol)}" IN (${join(ids)})
|
|
40
|
+
`;
|
|
41
|
+
}
|
package/sql/upsert.d.ts
ADDED
package/sql/upsert.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { isPlainObject } from '@graffy/common';
|
|
2
|
+
import sql, { join, raw } from 'sql-template-tag';
|
|
3
|
+
import { getInsert, getSelectCols, getUpdates } from "./clauses.js";
|
|
4
|
+
import getArgSql from "./getArgSql.js";
|
|
5
|
+
import { getIdMeta } from "./getMeta.js";
|
|
6
|
+
function getSingleSql(arg, options) {
|
|
7
|
+
const { table, idCol } = options;
|
|
8
|
+
if (!isPlainObject(arg)) {
|
|
9
|
+
return {
|
|
10
|
+
where: sql `"${raw(idCol)}" = ${arg}`,
|
|
11
|
+
meta: getIdMeta(options),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const { where, meta } = getArgSql(arg, options);
|
|
15
|
+
if (!where?.length)
|
|
16
|
+
throw Error('pg_write.no_condition');
|
|
17
|
+
// We use a subquery with limit 2 to ensure an error is thrown if the filter
|
|
18
|
+
// matches multiple rows.
|
|
19
|
+
return {
|
|
20
|
+
where: sql `"${raw(idCol)}" = (
|
|
21
|
+
SELECT "${raw(idCol)}"
|
|
22
|
+
FROM "${raw(table)}"
|
|
23
|
+
WHERE ${join(where, ' AND ')}
|
|
24
|
+
LIMIT 2
|
|
25
|
+
)`,
|
|
26
|
+
meta,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function patch(object, arg, options) {
|
|
30
|
+
const { table } = options;
|
|
31
|
+
const { where, meta } = getSingleSql(arg, options);
|
|
32
|
+
const row = object;
|
|
33
|
+
return sql `
|
|
34
|
+
UPDATE "${raw(table)}" SET ${getUpdates(row, options)}
|
|
35
|
+
WHERE ${where}
|
|
36
|
+
RETURNING ${getSelectCols(options)}, ${meta}`;
|
|
37
|
+
}
|
|
38
|
+
export function put(puts, options) {
|
|
39
|
+
const { idCol, table } = options;
|
|
40
|
+
const sqls = [];
|
|
41
|
+
const addSql = (rows, meta, conflictTarget) => {
|
|
42
|
+
const { cols, vals, updates } = getInsert(rows, options);
|
|
43
|
+
sqls.push(sql `
|
|
44
|
+
INSERT INTO "${raw(table)}" (${cols}) VALUES ${vals}
|
|
45
|
+
ON CONFLICT (${conflictTarget}) DO UPDATE SET ${updates}
|
|
46
|
+
RETURNING ${getSelectCols(options)}, ${meta}`);
|
|
47
|
+
};
|
|
48
|
+
const idRows = [];
|
|
49
|
+
for (const put of puts) {
|
|
50
|
+
const [row, arg] = put;
|
|
51
|
+
if (!isPlainObject(arg)) {
|
|
52
|
+
idRows.push(row);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const { meta } = getArgSql(arg, options);
|
|
56
|
+
const conflictTarget = join(Object.keys(arg).map((col) => sql `"${raw(col)}"`));
|
|
57
|
+
addSql([row], meta, conflictTarget);
|
|
58
|
+
}
|
|
59
|
+
if (idRows.length) {
|
|
60
|
+
const meta = getIdMeta(options);
|
|
61
|
+
const conflictTarget = sql `"${raw(idCol)}"`;
|
|
62
|
+
addSql(idRows, meta, conflictTarget);
|
|
63
|
+
}
|
|
64
|
+
return sqls;
|
|
65
|
+
}
|
|
66
|
+
export function del(arg, options) {
|
|
67
|
+
const { table } = options;
|
|
68
|
+
const { where } = getSingleSql(arg, options);
|
|
69
|
+
return sql `
|
|
70
|
+
DELETE FROM "${raw(table)}"
|
|
71
|
+
WHERE ${where}
|
|
72
|
+
RETURNING ${arg} "$key"`;
|
|
73
|
+
}
|