@graffy/pg 0.15.13 → 0.15.14-alpha.4

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/index.cjs CHANGED
@@ -53,8 +53,7 @@ const valid = {
53
53
  $all: true,
54
54
  $has: true,
55
55
  $cts: true,
56
- $ctd: true,
57
- $ovl: true
56
+ $ctd: true
58
57
  };
59
58
  const inverse = {
60
59
  $eq: "$neq",
@@ -67,10 +66,8 @@ const inverse = {
67
66
  $lte: "$gt"
68
67
  };
69
68
  function getAst(filter) {
70
- counter = 0;
71
69
  return simplify(construct(filter));
72
70
  }
73
- let counter;
74
71
  function construct(node, prop, op) {
75
72
  if (!node || typeof node !== "object" || prop && op) {
76
73
  if (op && prop)
@@ -91,14 +88,6 @@ function construct(node, prop, op) {
91
88
  if (key === "$not") {
92
89
  return [key, construct(val, prop, op)];
93
90
  }
94
- if (key === "$any" || key === "$all") {
95
- const elkey = "el$" + counter++;
96
- return [key, prop, elkey, construct(val, elkey)];
97
- }
98
- if (key === "$has") {
99
- const elkey = "el$" + counter++;
100
- return [key, prop, elkey, construct(val, elkey)[1]];
101
- }
102
91
  if (key[0] === "$") {
103
92
  if (!valid[key])
104
93
  throw Error("pgast.invalid_op:" + key);
@@ -123,10 +112,21 @@ function simplify(node) {
123
112
  node[1] = node[1].map((subnode) => simplify(subnode));
124
113
  } else if (op === "$not") {
125
114
  node[1] = simplify(node[1]);
126
- } else if (op === "$all" || op === "$any") {
127
- node[3] = simplify(node[3]);
128
- } else if (op === "$has") {
129
- node[3] = node[3].map((subnode) => simplify(subnode));
115
+ }
116
+ if (op === "$and") {
117
+ if (!node[1].length)
118
+ return true;
119
+ if (node[1].includes(false))
120
+ return false;
121
+ node[1] = node[1].filter((item) => item !== true);
122
+ } else if (op === "$or") {
123
+ if (!node[1].length)
124
+ return false;
125
+ if (node[1].includes(true))
126
+ return true;
127
+ node[1] = node[1].filter((item) => item !== false);
128
+ } else if (op === "$not" && typeof node[1] === "boolean") {
129
+ return !node[1];
130
130
  }
131
131
  if (op === "$or") {
132
132
  const { eqmap, noneq, change } = node[1].reduce((acc, item) => {
@@ -148,115 +148,16 @@ function simplify(node) {
148
148
  ];
149
149
  }
150
150
  }
151
- if (op === "$and" || op === "$or") {
152
- if (!node[1].length)
153
- throw Error("pgast.expected_children:" + op);
154
- return node[1].length === 1 ? node[1][0] : node;
151
+ if ((op === "$and" || op === "$or") && node[1].length === 1) {
152
+ return node[1][0];
155
153
  }
156
154
  if (op === "$not") {
157
155
  const [subop, ...subargs] = node[1];
158
156
  const invop = inverse[subop];
159
157
  return invop ? [invop, ...subargs] : node;
160
158
  }
161
- if (op === "$any" || op === "$all") {
162
- const [_, list, elkey, subnode] = node;
163
- const [subop, elk, val] = subnode;
164
- return (subop === "$eq" || subop === "$in") && elkey === elk ? [op === "$any" ? "$ovl" : "$ctd", list, subop === "$eq" ? [val] : val] : node;
165
- }
166
- if (op === "$has") {
167
- const [_, list, elkey, limbs] = node;
168
- return limbs.every(([subop, elk]) => subop === "$eq" && elk === elkey) ? ["$cts", list, limbs.map(([_op, _prop, val]) => val)] : node;
169
- }
170
159
  return node;
171
160
  }
172
- function defaultColumnType(_) {
173
- return "any";
174
- }
175
- function getCompatibleTypes(value) {
176
- if (value === null)
177
- return "any";
178
- if (Array.isArray(value))
179
- return "array";
180
- if (typeof value === "object")
181
- return "jsonb";
182
- if (typeof value === "number")
183
- return "numeric";
184
- if (typeof value === "string")
185
- return "text";
186
- }
187
- function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
188
- function lookup2(string, type) {
189
- if (string.substr(0, 3) === "el$")
190
- return sql__default["default"]`"${sql.raw(string)}"`;
191
- return getLookupSql(string, type);
192
- }
193
- function binop(op, left, right) {
194
- const lType = left.substr(0, 3) === "el$" ? "any" : getColumnType(left);
195
- const rType = getCompatibleTypes(right);
196
- if (lType === "any" || rType === "any" || rType === lType) {
197
- return sql__default["default"]`${lookup2(left)} ${sql.raw(op)} ${right}`;
198
- } else {
199
- return sql__default["default"]`(${lookup2(left, rType)})::${sql.raw(rType)} ${sql.raw(op)} ${right}`;
200
- }
201
- }
202
- function getNodeSql(ast) {
203
- switch (ast[0]) {
204
- case "$eq":
205
- if (ast[2] === null)
206
- return sql__default["default"]`${lookup2(ast[1])} IS NULL`;
207
- return binop("=", ast[1], ast[2]);
208
- case "$neq":
209
- if (ast[2] === null)
210
- return sql__default["default"]`${lookup2(ast[1])} IS NOT NULL`;
211
- return binop("<>", ast[1], ast[2]);
212
- case "$lt":
213
- return binop("<", ast[1], ast[2]);
214
- case "$lte":
215
- return binop("<=", ast[1], ast[2]);
216
- case "$gt":
217
- return binop(">", ast[1], ast[2]);
218
- case "$gte":
219
- return binop(">=", ast[1], ast[2]);
220
- case "$re":
221
- return binop("~", ast[1], ast[2]);
222
- case "$ire":
223
- return binop("~*", ast[1], ast[2]);
224
- case "$in":
225
- return sql__default["default"]`${lookup2(ast[1])} IN (${sql.join(ast[2])})`;
226
- case "$nin":
227
- return sql__default["default"]`${lookup2(ast[1])} NOT IN (${sql.join(ast[2])})`;
228
- case "$cts":
229
- return sql__default["default"]`${lookup2(ast[1])} @> ${ast[2]}`;
230
- case "$ctd":
231
- return sql__default["default"]`${lookup2(ast[1])} <@ ${ast[2]}`;
232
- case "$ovl":
233
- switch (getColumnType(ast[1])) {
234
- case "jsonb":
235
- case "any":
236
- return sql__default["default"]`${lookup2(ast[1])} ?| ${Array.isArray(ast[2]) ? ast[2] : Object.keys(ast[2])}`;
237
- case "array":
238
- return sql__default["default"]`${lookup2(ast[1])} && ${ast[2]}`;
239
- default:
240
- throw Error("pg.getSql_ovl_unknown_column_type");
241
- }
242
- case "$and":
243
- return sql__default["default"]`(${sql.join(ast[1].map((node) => getNodeSql(node)), `) AND (`)})`;
244
- case "$or":
245
- return sql__default["default"]`(${sql.join(ast[1].map((node) => getNodeSql(node)), `) OR (`)})`;
246
- case "$not":
247
- return sql__default["default"]`NOT (${getNodeSql(ast[1])})`;
248
- case "$any":
249
- return sql__default["default"]`(SELECT bool_or(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
250
- case "$all":
251
- return sql__default["default"]`(SELECT bool_and(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
252
- case "$has":
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
- default:
255
- throw Error("pg.getSql_unknown_operator: " + ast[0]);
256
- }
257
- }
258
- return getNodeSql(getAst(filter));
259
- }
260
161
  const nowTimestamp = sql__default["default"]`cast(extract(epoch from now()) as integer)`;
261
162
  const getJsonBuildObject = (variadic) => {
262
163
  const args = sql.join(Object.entries(variadic).map(([name, value]) => {
@@ -271,14 +172,9 @@ const getJsonBuildValue = (value) => {
271
172
  return sql__default["default"]`${value}::text`;
272
173
  return sql__default["default"]`${JSON.stringify(stripAttributes(value))}::jsonb`;
273
174
  };
274
- const lookup = (prop, type) => {
175
+ const lookup = (prop) => {
275
176
  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";
177
+ return suffix.length ? sql__default["default"]`"${sql.raw(prefix)}" #> ${suffix}` : sql__default["default"]`"${sql.raw(prefix)}"`;
282
178
  };
283
179
  const aggSql = {
284
180
  $sum: (prop) => sql__default["default"]`sum((${lookup(prop)})::numeric)`,
@@ -306,19 +202,39 @@ const getSelectCols = (table, projection = null) => {
306
202
  }
307
203
  return sql__default["default"]`jsonb_build_object(${sql.join(sqls, ", ")})`;
308
204
  };
205
+ function vertexSql(array) {
206
+ return sql__default["default"]`array[${sql.join(array.map((num) => num === Infinity ? sql__default["default"]`'Infinity'` : num === -Infinity ? sql__default["default"]`'-Infinity'` : num))}]::float8[]`;
207
+ }
208
+ function castValue$1(value, type, name, isPut) {
209
+ if (!type)
210
+ throw Error("pg.write_no_column " + name);
211
+ if (value instanceof sql.Sql)
212
+ return value;
213
+ if (value === null)
214
+ return sql__default["default"]`NULL`;
215
+ if (type === "jsonb") {
216
+ console.log("jsonb", value, type, name);
217
+ return isPut ? JSON.stringify(stripAttributes(value)) : sql__default["default"]`jsonb_strip_nulls(${getJsonUpdate(value, name, [])})`;
218
+ }
219
+ if (type === "cube") {
220
+ if (!Array.isArray(value) || !value.length || Array.isArray(value[0]) && value.length !== 2) {
221
+ throw Error("pg.castValue_bad_cube" + JSON.stringify(value));
222
+ }
223
+ return Array.isArray(value[0]) ? sql__default["default"]`cube(${vertexSql(value[0])}, ${vertexSql(value[1])})` : sql__default["default"]`cube(${vertexSql(value)})`;
224
+ }
225
+ return value;
226
+ }
309
227
  const getInsert = (row, options) => {
310
228
  const cols = [];
311
229
  const vals = [];
312
- Object.entries(row).filter(([name]) => name !== options.verCol && name[0] !== "$").concat([[options.verCol, nowTimestamp]]).forEach(([col, val]) => {
230
+ Object.entries(row).filter(([col]) => col !== options.verCol && col[0] !== "$").concat([[options.verCol, nowTimestamp]]).forEach(([col, val]) => {
313
231
  cols.push(sql__default["default"]`"${sql.raw(col)}"`);
314
- vals.push(val instanceof sql.Sql || typeof val !== "object" || !val ? val : sql__default["default"]`${JSON.stringify(stripAttributes(val))}::jsonb`);
232
+ vals.push(castValue$1(val, options.schema.types[col], col, row.$put));
315
233
  });
316
234
  return { cols: sql.join(cols, ", "), vals: sql.join(vals, ", ") };
317
235
  };
318
236
  const getUpdates = (row, options) => {
319
- return sql.join(Object.entries(row).filter(([name]) => name !== options.idCol && name[0] !== "$").map(([name, value]) => {
320
- return sql__default["default"]`"${sql.raw(name)}" = ${value instanceof sql.Sql || typeof value !== "object" || !value ? value : !value.$put ? sql__default["default"]`jsonb_strip_nulls(${getJsonUpdate(value, name, [])})` : sql__default["default"]`${JSON.stringify(stripAttributes(value))}::jsonb`}`;
321
- }).concat(sql__default["default"]`"${sql.raw(options.verCol)}" = ${nowTimestamp}`), ", ");
237
+ return sql.join(Object.entries(row).filter(([col]) => col !== options.idCol && col[0] !== "$").map(([col, val]) => sql__default["default"]`"${sql.raw(col)}" = ${castValue$1(val, options.schema.types[col], col, row.$put)}`).concat(sql__default["default"]`"${sql.raw(options.verCol)}" = ${nowTimestamp}`), ", ");
322
238
  };
323
239
  function getJsonUpdate(_a, col, path) {
324
240
  var _b = _a, { $put } = _b, object = __objRest(_b, ["$put"]);
@@ -345,6 +261,68 @@ function stripAttributes(object) {
345
261
  return out;
346
262
  }, {});
347
263
  }
264
+ const opSql = {
265
+ $and: `AND`,
266
+ $or: `OR`,
267
+ $not: sql__default["default"]`NOT`,
268
+ $eq: sql__default["default"]`=`,
269
+ $neq: sql__default["default"]`<>`,
270
+ $in: sql__default["default"]`IN`,
271
+ $nin: sql__default["default"]`NOT IN`,
272
+ $lt: sql__default["default"]`<`,
273
+ $lte: sql__default["default"]`<=`,
274
+ $gt: sql__default["default"]`>`,
275
+ $gte: sql__default["default"]`>=`,
276
+ $re: sql__default["default"]`~`,
277
+ $ire: sql__default["default"]`~*`,
278
+ $cts: sql__default["default"]`@>`,
279
+ $ctd: sql__default["default"]`<@`
280
+ };
281
+ function castValue(value, type, op) {
282
+ if (value === null && op === "$eq")
283
+ return sql__default["default"]`IS NULL`;
284
+ if (value === null && op === "$neq")
285
+ return sql__default["default"]`IS NOT NULL`;
286
+ const sqlOp = opSql[op];
287
+ if (!sqlOp)
288
+ throw Error("pg.getSql_unknown_operator " + op);
289
+ if (op === "$in" || op === "$nin") {
290
+ return sql__default["default"]`${sqlOp} (${sql.join(value)})`;
291
+ }
292
+ if (type === "jsonb") {
293
+ return sql__default["default"]`${sqlOp} ${JSON.stringify(value)}::jsonb`;
294
+ }
295
+ if (type === "cube") {
296
+ if (!Array.isArray(value) || !value.length || Array.isArray(value[0]) && value.length !== 2) {
297
+ throw Error("pg.castValue_bad_cube" + JSON.stringify(value));
298
+ }
299
+ return Array.isArray(value[0]) ? sql__default["default"]`${sqlOp} cube(${vertexSql(value[0])}, ${vertexSql(value[1])})` : sql__default["default"]`${sqlOp} cube(${vertexSql(value)})`;
300
+ }
301
+ return sql__default["default"]`${sqlOp} ${value}`;
302
+ }
303
+ function getSql(filter, options) {
304
+ function getNodeSql(ast) {
305
+ if (typeof ast === "boolean")
306
+ return ast;
307
+ const op = ast[0];
308
+ if (op === "$and" || op === "$or") {
309
+ return sql__default["default"]`(${sql.join(ast[1].map((node) => getNodeSql(node)), `) ${opSql[op]} (`)})`;
310
+ } else if (op === "$not") {
311
+ return sql__default["default"]`${opSql[op]} (${getNodeSql(ast[1])})`;
312
+ }
313
+ const [prefix, ...suffix] = common.encodePath(ast[1]);
314
+ const { types } = options.schema;
315
+ if (!types[prefix])
316
+ throw Error("pg.no_column " + prefix);
317
+ if (suffix.length && types[prefix] !== "jsonb") {
318
+ throw Error("pg.lookup_not_jsonb " + prefix);
319
+ }
320
+ const [lhs, type] = suffix.length ? [sql__default["default"]`"${sql.raw(prefix)}" #> ${suffix}`, "jsonb"] : [sql__default["default"]`"${sql.raw(prefix)}"`, types[prefix]];
321
+ const rhs = castValue(ast[2], type, op);
322
+ return sql__default["default"]`${lhs} ${rhs}`;
323
+ }
324
+ return getNodeSql(getAst(filter));
325
+ }
348
326
  const getIdMeta = ({ idCol }) => getJsonBuildObject({
349
327
  $key: sql__default["default"]`"${sql.raw(idCol)}"`,
350
328
  $ver: nowTimestamp
@@ -366,13 +344,13 @@ function getArgSql(_c, options) {
366
344
  throw Error("pg_arg.order_and_group_unsupported in " + prefix);
367
345
  }
368
346
  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;
347
+ const groupCols = Array.isArray($group) && $group.length && $group.map(lookup);
370
348
  const group = groupCols ? sql.join(groupCols, ", ") : void 0;
371
349
  const hasRangeArg = $before || $after || $since || $until || $first || $last || $all || $order;
372
350
  let key;
373
351
  const where = [];
374
352
  if (!common.isEmpty(filter)) {
375
- where.push(getSql(filter, lookup, getType));
353
+ where.push(getSql(filter, options));
376
354
  key = sql__default["default"]`${JSON.stringify(filter)}::jsonb`;
377
355
  }
378
356
  if (!hasRangeArg)
@@ -533,24 +511,46 @@ class Db {
533
511
  }
534
512
  }
535
513
  async readSql(sql2) {
536
- let result = await this.query(sql2);
537
- result = result.rows.flat();
514
+ const result = (await this.query(sql2)).rows.flat();
538
515
  log("Read result", result);
539
516
  return result;
540
517
  }
541
518
  async writeSql(sql2) {
542
- let res = await this.query(sql2);
519
+ const res = await this.query(sql2);
543
520
  log("Rows written", res.rowCount);
544
521
  if (!res.rowCount) {
545
522
  throw Error("pg.nothing_written " + sql2.text + " with " + sql2.values);
546
523
  }
547
524
  return res.rows[0][0];
548
525
  }
526
+ async ensureSchema(tableOptions) {
527
+ if (tableOptions.schema)
528
+ return;
529
+ const { table } = tableOptions;
530
+ const types = (await this.query(sql__default["default"]`SELECT jsonb_object_agg(
531
+ column_name,
532
+ udt_name
533
+ )
534
+ FROM information_schema.columns
535
+ WHERE
536
+ table_name = ${table} AND
537
+ table_schema = (
538
+ SELECT table_schema
539
+ FROM information_schema.tables
540
+ WHERE table_name = ${table}
541
+ ORDER BY array_position(current_schemas(false)::text[], table_schema::text) ASC
542
+ LIMIT 1)`)).rows[0][0];
543
+ if (!types)
544
+ throw Error(`pg.missing_table ${table}`);
545
+ log("ensureSchema", types);
546
+ tableOptions.schema = { types };
547
+ }
549
548
  async read(rootQuery, tableOptions) {
550
549
  const idQueries = {};
551
550
  const promises = [];
552
551
  const results = [];
553
552
  const { prefix } = tableOptions;
553
+ await this.ensureSchema(tableOptions);
554
554
  const getByArgs = async (args, projection) => {
555
555
  const result = await this.readSql(selectByArgs(args, projection, tableOptions));
556
556
  const wrappedGraph = common.encodeGraph(common.wrapObject(result, prefix));
@@ -591,6 +591,7 @@ class Db {
591
591
  }
592
592
  async write(rootChange, tableOptions) {
593
593
  const { prefix } = tableOptions;
594
+ await this.ensureSchema(tableOptions);
594
595
  const change = common.unwrap(rootChange, prefix);
595
596
  const sqls = change.map((node) => {
596
597
  const arg = common.decodeArgs(node);
@@ -618,7 +619,7 @@ class Db {
618
619
  return result;
619
620
  }
620
621
  }
621
- const pg = ({ table, idCol, verCol, connection }) => (store) => {
622
+ const pg = ({ table, idCol, verCol, connection, schema }) => (store) => {
622
623
  store.on("read", read);
623
624
  store.on("write", write);
624
625
  const prefix = store.path;
@@ -626,7 +627,8 @@ const pg = ({ table, idCol, verCol, connection }) => (store) => {
626
627
  prefix,
627
628
  table: table || prefix[prefix.length - 1] || "default",
628
629
  idCol: idCol || "id",
629
- verCol: verCol || "updatedAt"
630
+ verCol: verCol || "updatedAt",
631
+ schema
630
632
  };
631
633
  const defaultDb = new Db(connection);
632
634
  function read(query, options, next) {
package/index.mjs CHANGED
@@ -45,8 +45,7 @@ const valid = {
45
45
  $all: true,
46
46
  $has: true,
47
47
  $cts: true,
48
- $ctd: true,
49
- $ovl: true
48
+ $ctd: true
50
49
  };
51
50
  const inverse = {
52
51
  $eq: "$neq",
@@ -59,10 +58,8 @@ const inverse = {
59
58
  $lte: "$gt"
60
59
  };
61
60
  function getAst(filter) {
62
- counter = 0;
63
61
  return simplify(construct(filter));
64
62
  }
65
- let counter;
66
63
  function construct(node, prop, op) {
67
64
  if (!node || typeof node !== "object" || prop && op) {
68
65
  if (op && prop)
@@ -83,14 +80,6 @@ function construct(node, prop, op) {
83
80
  if (key === "$not") {
84
81
  return [key, construct(val, prop, op)];
85
82
  }
86
- if (key === "$any" || key === "$all") {
87
- const elkey = "el$" + counter++;
88
- return [key, prop, elkey, construct(val, elkey)];
89
- }
90
- if (key === "$has") {
91
- const elkey = "el$" + counter++;
92
- return [key, prop, elkey, construct(val, elkey)[1]];
93
- }
94
83
  if (key[0] === "$") {
95
84
  if (!valid[key])
96
85
  throw Error("pgast.invalid_op:" + key);
@@ -115,10 +104,21 @@ function simplify(node) {
115
104
  node[1] = node[1].map((subnode) => simplify(subnode));
116
105
  } else if (op === "$not") {
117
106
  node[1] = simplify(node[1]);
118
- } else if (op === "$all" || op === "$any") {
119
- node[3] = simplify(node[3]);
120
- } else if (op === "$has") {
121
- node[3] = node[3].map((subnode) => simplify(subnode));
107
+ }
108
+ if (op === "$and") {
109
+ if (!node[1].length)
110
+ return true;
111
+ if (node[1].includes(false))
112
+ return false;
113
+ node[1] = node[1].filter((item) => item !== true);
114
+ } else if (op === "$or") {
115
+ if (!node[1].length)
116
+ return false;
117
+ if (node[1].includes(true))
118
+ return true;
119
+ node[1] = node[1].filter((item) => item !== false);
120
+ } else if (op === "$not" && typeof node[1] === "boolean") {
121
+ return !node[1];
122
122
  }
123
123
  if (op === "$or") {
124
124
  const { eqmap, noneq, change } = node[1].reduce((acc, item) => {
@@ -140,115 +140,16 @@ function simplify(node) {
140
140
  ];
141
141
  }
142
142
  }
143
- if (op === "$and" || op === "$or") {
144
- if (!node[1].length)
145
- throw Error("pgast.expected_children:" + op);
146
- return node[1].length === 1 ? node[1][0] : node;
143
+ if ((op === "$and" || op === "$or") && node[1].length === 1) {
144
+ return node[1][0];
147
145
  }
148
146
  if (op === "$not") {
149
147
  const [subop, ...subargs] = node[1];
150
148
  const invop = inverse[subop];
151
149
  return invop ? [invop, ...subargs] : node;
152
150
  }
153
- if (op === "$any" || op === "$all") {
154
- const [_, list, elkey, subnode] = node;
155
- const [subop, elk, val] = subnode;
156
- return (subop === "$eq" || subop === "$in") && elkey === elk ? [op === "$any" ? "$ovl" : "$ctd", list, subop === "$eq" ? [val] : val] : node;
157
- }
158
- if (op === "$has") {
159
- const [_, list, elkey, limbs] = node;
160
- return limbs.every(([subop, elk]) => subop === "$eq" && elk === elkey) ? ["$cts", list, limbs.map(([_op, _prop, val]) => val)] : node;
161
- }
162
151
  return node;
163
152
  }
164
- function defaultColumnType(_) {
165
- return "any";
166
- }
167
- function getCompatibleTypes(value) {
168
- if (value === null)
169
- return "any";
170
- if (Array.isArray(value))
171
- return "array";
172
- if (typeof value === "object")
173
- return "jsonb";
174
- if (typeof value === "number")
175
- return "numeric";
176
- if (typeof value === "string")
177
- return "text";
178
- }
179
- function getSql(filter, getLookupSql, getColumnType = defaultColumnType) {
180
- function lookup2(string, type) {
181
- if (string.substr(0, 3) === "el$")
182
- return sql`"${raw(string)}"`;
183
- return getLookupSql(string, type);
184
- }
185
- function binop(op, left, right) {
186
- const lType = left.substr(0, 3) === "el$" ? "any" : getColumnType(left);
187
- const rType = getCompatibleTypes(right);
188
- if (lType === "any" || rType === "any" || rType === lType) {
189
- return sql`${lookup2(left)} ${raw(op)} ${right}`;
190
- } else {
191
- return sql`(${lookup2(left, rType)})::${raw(rType)} ${raw(op)} ${right}`;
192
- }
193
- }
194
- function getNodeSql(ast) {
195
- switch (ast[0]) {
196
- case "$eq":
197
- if (ast[2] === null)
198
- return sql`${lookup2(ast[1])} IS NULL`;
199
- return binop("=", ast[1], ast[2]);
200
- case "$neq":
201
- if (ast[2] === null)
202
- return sql`${lookup2(ast[1])} IS NOT NULL`;
203
- return binop("<>", ast[1], ast[2]);
204
- case "$lt":
205
- return binop("<", ast[1], ast[2]);
206
- case "$lte":
207
- return binop("<=", ast[1], ast[2]);
208
- case "$gt":
209
- return binop(">", ast[1], ast[2]);
210
- case "$gte":
211
- return binop(">=", ast[1], ast[2]);
212
- case "$re":
213
- return binop("~", ast[1], ast[2]);
214
- case "$ire":
215
- return binop("~*", ast[1], ast[2]);
216
- case "$in":
217
- return sql`${lookup2(ast[1])} IN (${join(ast[2])})`;
218
- case "$nin":
219
- return sql`${lookup2(ast[1])} NOT IN (${join(ast[2])})`;
220
- case "$cts":
221
- return sql`${lookup2(ast[1])} @> ${ast[2]}`;
222
- case "$ctd":
223
- return sql`${lookup2(ast[1])} <@ ${ast[2]}`;
224
- case "$ovl":
225
- switch (getColumnType(ast[1])) {
226
- case "jsonb":
227
- case "any":
228
- return sql`${lookup2(ast[1])} ?| ${Array.isArray(ast[2]) ? ast[2] : Object.keys(ast[2])}`;
229
- case "array":
230
- return sql`${lookup2(ast[1])} && ${ast[2]}`;
231
- default:
232
- throw Error("pg.getSql_ovl_unknown_column_type");
233
- }
234
- case "$and":
235
- return sql`(${join(ast[1].map((node) => getNodeSql(node)), `) AND (`)})`;
236
- case "$or":
237
- return sql`(${join(ast[1].map((node) => getNodeSql(node)), `) OR (`)})`;
238
- case "$not":
239
- return sql`NOT (${getNodeSql(ast[1])})`;
240
- case "$any":
241
- return sql`(SELECT bool_or(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
242
- case "$all":
243
- return sql`(SELECT bool_and(${getNodeSql(ast[3])}) FROM UNNEST(${lookup2(ast[1])}) ${lookup2(ast[2])})`;
244
- case "$has":
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
- default:
247
- throw Error("pg.getSql_unknown_operator: " + ast[0]);
248
- }
249
- }
250
- return getNodeSql(getAst(filter));
251
- }
252
153
  const nowTimestamp = sql`cast(extract(epoch from now()) as integer)`;
253
154
  const getJsonBuildObject = (variadic) => {
254
155
  const args = join(Object.entries(variadic).map(([name, value]) => {
@@ -263,14 +164,9 @@ const getJsonBuildValue = (value) => {
263
164
  return sql`${value}::text`;
264
165
  return sql`${JSON.stringify(stripAttributes(value))}::jsonb`;
265
166
  };
266
- const lookup = (prop, type) => {
167
+ const lookup = (prop) => {
267
168
  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";
169
+ return suffix.length ? sql`"${raw(prefix)}" #> ${suffix}` : sql`"${raw(prefix)}"`;
274
170
  };
275
171
  const aggSql = {
276
172
  $sum: (prop) => sql`sum((${lookup(prop)})::numeric)`,
@@ -298,19 +194,39 @@ const getSelectCols = (table, projection = null) => {
298
194
  }
299
195
  return sql`jsonb_build_object(${join(sqls, ", ")})`;
300
196
  };
197
+ function vertexSql(array) {
198
+ return sql`array[${join(array.map((num) => num === Infinity ? sql`'Infinity'` : num === -Infinity ? sql`'-Infinity'` : num))}]::float8[]`;
199
+ }
200
+ function castValue$1(value, type, name, isPut) {
201
+ if (!type)
202
+ throw Error("pg.write_no_column " + name);
203
+ if (value instanceof Sql)
204
+ return value;
205
+ if (value === null)
206
+ return sql`NULL`;
207
+ if (type === "jsonb") {
208
+ console.log("jsonb", value, type, name);
209
+ return isPut ? JSON.stringify(stripAttributes(value)) : sql`jsonb_strip_nulls(${getJsonUpdate(value, name, [])})`;
210
+ }
211
+ if (type === "cube") {
212
+ if (!Array.isArray(value) || !value.length || Array.isArray(value[0]) && value.length !== 2) {
213
+ throw Error("pg.castValue_bad_cube" + JSON.stringify(value));
214
+ }
215
+ return Array.isArray(value[0]) ? sql`cube(${vertexSql(value[0])}, ${vertexSql(value[1])})` : sql`cube(${vertexSql(value)})`;
216
+ }
217
+ return value;
218
+ }
301
219
  const getInsert = (row, options) => {
302
220
  const cols = [];
303
221
  const vals = [];
304
- Object.entries(row).filter(([name]) => name !== options.verCol && name[0] !== "$").concat([[options.verCol, nowTimestamp]]).forEach(([col, val]) => {
222
+ Object.entries(row).filter(([col]) => col !== options.verCol && col[0] !== "$").concat([[options.verCol, nowTimestamp]]).forEach(([col, val]) => {
305
223
  cols.push(sql`"${raw(col)}"`);
306
- vals.push(val instanceof Sql || typeof val !== "object" || !val ? val : sql`${JSON.stringify(stripAttributes(val))}::jsonb`);
224
+ vals.push(castValue$1(val, options.schema.types[col], col, row.$put));
307
225
  });
308
226
  return { cols: join(cols, ", "), vals: join(vals, ", ") };
309
227
  };
310
228
  const getUpdates = (row, options) => {
311
- return join(Object.entries(row).filter(([name]) => name !== options.idCol && name[0] !== "$").map(([name, value]) => {
312
- return sql`"${raw(name)}" = ${value instanceof Sql || typeof value !== "object" || !value ? value : !value.$put ? sql`jsonb_strip_nulls(${getJsonUpdate(value, name, [])})` : sql`${JSON.stringify(stripAttributes(value))}::jsonb`}`;
313
- }).concat(sql`"${raw(options.verCol)}" = ${nowTimestamp}`), ", ");
229
+ return join(Object.entries(row).filter(([col]) => col !== options.idCol && col[0] !== "$").map(([col, val]) => sql`"${raw(col)}" = ${castValue$1(val, options.schema.types[col], col, row.$put)}`).concat(sql`"${raw(options.verCol)}" = ${nowTimestamp}`), ", ");
314
230
  };
315
231
  function getJsonUpdate(_a, col, path) {
316
232
  var _b = _a, { $put } = _b, object = __objRest(_b, ["$put"]);
@@ -337,6 +253,68 @@ function stripAttributes(object) {
337
253
  return out;
338
254
  }, {});
339
255
  }
256
+ const opSql = {
257
+ $and: `AND`,
258
+ $or: `OR`,
259
+ $not: sql`NOT`,
260
+ $eq: sql`=`,
261
+ $neq: sql`<>`,
262
+ $in: sql`IN`,
263
+ $nin: sql`NOT IN`,
264
+ $lt: sql`<`,
265
+ $lte: sql`<=`,
266
+ $gt: sql`>`,
267
+ $gte: sql`>=`,
268
+ $re: sql`~`,
269
+ $ire: sql`~*`,
270
+ $cts: sql`@>`,
271
+ $ctd: sql`<@`
272
+ };
273
+ function castValue(value, type, op) {
274
+ if (value === null && op === "$eq")
275
+ return sql`IS NULL`;
276
+ if (value === null && op === "$neq")
277
+ return sql`IS NOT NULL`;
278
+ const sqlOp = opSql[op];
279
+ if (!sqlOp)
280
+ throw Error("pg.getSql_unknown_operator " + op);
281
+ if (op === "$in" || op === "$nin") {
282
+ return sql`${sqlOp} (${join(value)})`;
283
+ }
284
+ if (type === "jsonb") {
285
+ return sql`${sqlOp} ${JSON.stringify(value)}::jsonb`;
286
+ }
287
+ if (type === "cube") {
288
+ if (!Array.isArray(value) || !value.length || Array.isArray(value[0]) && value.length !== 2) {
289
+ throw Error("pg.castValue_bad_cube" + JSON.stringify(value));
290
+ }
291
+ return Array.isArray(value[0]) ? sql`${sqlOp} cube(${vertexSql(value[0])}, ${vertexSql(value[1])})` : sql`${sqlOp} cube(${vertexSql(value)})`;
292
+ }
293
+ return sql`${sqlOp} ${value}`;
294
+ }
295
+ function getSql(filter, options) {
296
+ function getNodeSql(ast) {
297
+ if (typeof ast === "boolean")
298
+ return ast;
299
+ const op = ast[0];
300
+ if (op === "$and" || op === "$or") {
301
+ return sql`(${join(ast[1].map((node) => getNodeSql(node)), `) ${opSql[op]} (`)})`;
302
+ } else if (op === "$not") {
303
+ return sql`${opSql[op]} (${getNodeSql(ast[1])})`;
304
+ }
305
+ const [prefix, ...suffix] = encodePath(ast[1]);
306
+ const { types } = options.schema;
307
+ if (!types[prefix])
308
+ throw Error("pg.no_column " + prefix);
309
+ if (suffix.length && types[prefix] !== "jsonb") {
310
+ throw Error("pg.lookup_not_jsonb " + prefix);
311
+ }
312
+ const [lhs, type] = suffix.length ? [sql`"${raw(prefix)}" #> ${suffix}`, "jsonb"] : [sql`"${raw(prefix)}"`, types[prefix]];
313
+ const rhs = castValue(ast[2], type, op);
314
+ return sql`${lhs} ${rhs}`;
315
+ }
316
+ return getNodeSql(getAst(filter));
317
+ }
340
318
  const getIdMeta = ({ idCol }) => getJsonBuildObject({
341
319
  $key: sql`"${raw(idCol)}"`,
342
320
  $ver: nowTimestamp
@@ -358,13 +336,13 @@ function getArgSql(_c, options) {
358
336
  throw Error("pg_arg.order_and_group_unsupported in " + prefix);
359
337
  }
360
338
  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;
339
+ const groupCols = Array.isArray($group) && $group.length && $group.map(lookup);
362
340
  const group = groupCols ? join(groupCols, ", ") : void 0;
363
341
  const hasRangeArg = $before || $after || $since || $until || $first || $last || $all || $order;
364
342
  let key;
365
343
  const where = [];
366
344
  if (!isEmpty(filter)) {
367
- where.push(getSql(filter, lookup, getType));
345
+ where.push(getSql(filter, options));
368
346
  key = sql`${JSON.stringify(filter)}::jsonb`;
369
347
  }
370
348
  if (!hasRangeArg)
@@ -525,24 +503,46 @@ class Db {
525
503
  }
526
504
  }
527
505
  async readSql(sql2) {
528
- let result = await this.query(sql2);
529
- result = result.rows.flat();
506
+ const result = (await this.query(sql2)).rows.flat();
530
507
  log("Read result", result);
531
508
  return result;
532
509
  }
533
510
  async writeSql(sql2) {
534
- let res = await this.query(sql2);
511
+ const res = await this.query(sql2);
535
512
  log("Rows written", res.rowCount);
536
513
  if (!res.rowCount) {
537
514
  throw Error("pg.nothing_written " + sql2.text + " with " + sql2.values);
538
515
  }
539
516
  return res.rows[0][0];
540
517
  }
518
+ async ensureSchema(tableOptions) {
519
+ if (tableOptions.schema)
520
+ return;
521
+ const { table } = tableOptions;
522
+ const types = (await this.query(sql`SELECT jsonb_object_agg(
523
+ column_name,
524
+ udt_name
525
+ )
526
+ FROM information_schema.columns
527
+ WHERE
528
+ table_name = ${table} AND
529
+ table_schema = (
530
+ SELECT table_schema
531
+ FROM information_schema.tables
532
+ WHERE table_name = ${table}
533
+ ORDER BY array_position(current_schemas(false)::text[], table_schema::text) ASC
534
+ LIMIT 1)`)).rows[0][0];
535
+ if (!types)
536
+ throw Error(`pg.missing_table ${table}`);
537
+ log("ensureSchema", types);
538
+ tableOptions.schema = { types };
539
+ }
541
540
  async read(rootQuery, tableOptions) {
542
541
  const idQueries = {};
543
542
  const promises = [];
544
543
  const results = [];
545
544
  const { prefix } = tableOptions;
545
+ await this.ensureSchema(tableOptions);
546
546
  const getByArgs = async (args, projection) => {
547
547
  const result = await this.readSql(selectByArgs(args, projection, tableOptions));
548
548
  const wrappedGraph = encodeGraph(wrapObject(result, prefix));
@@ -583,6 +583,7 @@ class Db {
583
583
  }
584
584
  async write(rootChange, tableOptions) {
585
585
  const { prefix } = tableOptions;
586
+ await this.ensureSchema(tableOptions);
586
587
  const change = unwrap(rootChange, prefix);
587
588
  const sqls = change.map((node) => {
588
589
  const arg = decodeArgs(node);
@@ -610,7 +611,7 @@ class Db {
610
611
  return result;
611
612
  }
612
613
  }
613
- const pg = ({ table, idCol, verCol, connection }) => (store) => {
614
+ const pg = ({ table, idCol, verCol, connection, schema }) => (store) => {
614
615
  store.on("read", read);
615
616
  store.on("write", write);
616
617
  const prefix = store.path;
@@ -618,7 +619,8 @@ const pg = ({ table, idCol, verCol, connection }) => (store) => {
618
619
  prefix,
619
620
  table: table || prefix[prefix.length - 1] || "default",
620
621
  idCol: idCol || "id",
621
- verCol: verCol || "updatedAt"
622
+ verCol: verCol || "updatedAt",
623
+ schema
622
624
  };
623
625
  const defaultDb = new Db(connection);
624
626
  function read(query, options, next) {
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.13",
5
+ "version": "0.15.14-alpha.4",
6
6
  "main": "./index.cjs",
7
7
  "exports": {
8
8
  "import": "./index.mjs",
@@ -16,9 +16,9 @@
16
16
  },
17
17
  "license": "Apache-2.0",
18
18
  "dependencies": {
19
- "@graffy/common": "0.15.13",
20
- "debug": "^4.3.2",
21
- "sql-template-tag": "^4.0.0"
19
+ "@graffy/common": "0.15.14-alpha.4",
20
+ "sql-template-tag": "^4.0.0",
21
+ "debug": "^4.3.2"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "pg": "^8.0.0"
package/types/Db.d.ts CHANGED
@@ -4,6 +4,7 @@ export default class Db {
4
4
  query(sql: any): Promise<any>;
5
5
  readSql(sql: any): Promise<any>;
6
6
  writeSql(sql: any): Promise<any>;
7
+ ensureSchema(tableOptions: any): Promise<void>;
7
8
  read(rootQuery: any, tableOptions: any): Promise<any>;
8
9
  write(rootChange: any, tableOptions: any): Promise<any[]>;
9
10
  }
@@ -1,3 +1 @@
1
- export default function getSql(filter: any, getLookupSql: any, getColumnType?: typeof defaultColumnType): any;
2
- declare function defaultColumnType(_: any): string;
3
- export {};
1
+ export default function getSql(filter: any, options: any): any;
package/types/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export function pg({ table, idCol, verCol, connection }: {
1
+ export function pg({ table, idCol, verCol, connection, schema }: {
2
2
  table?: string;
3
3
  idCol?: string;
4
4
  verCol?: string;
5
5
  connection?: any;
6
+ schema?: any;
6
7
  }): (store: any) => void;
@@ -1,7 +1,7 @@
1
+ export function vertexSql(array: any): Sql;
1
2
  export const nowTimestamp: Sql;
2
3
  export function getJsonBuildObject(variadic: any): Sql;
3
- export function lookup(prop: any, type: any): Sql;
4
- export function getType(prop: any): "any" | "jsonb";
4
+ export function lookup(prop: any): Sql;
5
5
  export function getSelectCols(table: any, projection?: any): Sql;
6
6
  export function getInsert(row: any, options: any): {
7
7
  cols: Sql;