@graffy/pg 0.16.4-alpha.1 → 0.16.4-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/Readme.md CHANGED
@@ -20,6 +20,9 @@ store.use(path, pg(options));
20
20
  - `table`, the name of the table. If not provided, the last segment of the `path` is used. This table must exist.
21
21
  - `idCol`: the name of the column to use as ID. Defaults to `id`. This column must exist and be the primary key or have a unique constraint.
22
22
  - `verCol`: the name of the column to store the Graffy version number. This column must exist, and must have a `DEFAULT` SQL expression defined - this expression is evaluated to calculate the version number. Graffy versions must monotonically increase, so this expression is typically based on `CURRENT_TIMESTAMP`.
23
+ - `joins`: other tables that have foreign keys referencing the ID column in this table, where we want to filter this table using columns of that other table via a "join". The value is a map of join names to join options.
24
+ - join names are the names used to refer to joined tables in filter expressions. Typically, these are the names of those tables.
25
+ - join options are objects with optional properties `table`, `idCol`, `refCol` and `verCol`. `refCol` is the name of the column in the join table that references this one.
23
26
  - `connection`: a [pg](https://github.com/brianc/node-postgres) Client or Pool object (recommended), or the arguments for constructing a new Pool object. Optional.
24
27
 
25
28
  ### Database connection
@@ -89,6 +92,9 @@ Tags must contain both *foo* and *bar*. Note that the array of conditions here d
89
92
  2. `{ tags: { $ctd: ['foo', 'bar', 'baz'] } }` becomes `tags <@ '{"foo","bar","baz"}'`.
90
93
  Every tag must be one of *foo*, *bar* or *baz*.
91
94
 
95
+ #### Joins
96
+ 1. `joinName: { ...expression }`,
97
+
92
98
  #### Notes
93
99
 
94
100
  1. We drop several MongoDB operators while retaining the capability:
package/index.cjs CHANGED
@@ -126,9 +126,7 @@ function construct(node, prop, op) {
126
126
  return construct(val, prop, key);
127
127
  }
128
128
  if (prop) {
129
- if (key[0] === ".")
130
- return construct(val, prop + key);
131
- throw Error(`pgast.unexpected_prop: ${key}`);
129
+ return ["$sub", prop, construct({ [key]: val })];
132
130
  }
133
131
  return construct(val, key);
134
132
  })
@@ -140,6 +138,8 @@ function simplify(node) {
140
138
  node[1] = node[1].map((subnode) => simplify(subnode));
141
139
  } else if (op === "$not") {
142
140
  node[1] = simplify(node[1]);
141
+ } else if (op === "$sub") {
142
+ node[2] = simplify(node[2]);
143
143
  }
144
144
  if (op === "$and") {
145
145
  if (!node[1].length)
@@ -385,36 +385,48 @@ function getBinarySql(lhs, type, op, value, textLhs) {
385
385
  return sql`${lhs} ${sqlOp} ${cubeLiteralSql(value)}`;
386
386
  return sql`${lhs} ${sqlOp} ${value}`;
387
387
  }
388
- function getSql(filter, options) {
389
- function getNodeSql(ast) {
390
- if (typeof ast === "boolean")
391
- return ast;
392
- const op = ast[0];
393
- if (op === "$and" || op === "$or") {
394
- return sql`(${join(
395
- ast[1].map((node) => getNodeSql(node)),
396
- `) ${opSql[op]} (`
397
- )})`;
398
- } else if (op === "$not") {
399
- return sql`${opSql[op]} (${getNodeSql(ast[1])})`;
400
- }
401
- const [prefix, ...suffix] = ast[1].split(".");
402
- const { types: types2 } = options.schema;
403
- if (!types2[prefix])
404
- throw Error(`pg.no_column ${prefix}`);
405
- if (types2[prefix] === "jsonb") {
406
- const [lhs, textLhs] = suffix.length ? [
407
- sql`"${raw(prefix)}" #> ${suffix}`,
408
- sql`"${raw(prefix)}" #>> ${suffix}`
409
- ] : [sql`"${raw(prefix)}"`, sql`"${raw(prefix)}" #>> '{}'`];
410
- return getBinarySql(lhs, "jsonb", op, ast[2], textLhs);
411
- } else {
412
- if (suffix.length)
413
- throw Error(`pg.lookup_not_jsonb ${prefix}`);
414
- return getBinarySql(sql`"${raw(prefix)}"`, types2[prefix], op, ast[2]);
415
- }
388
+ function getNodeSql(ast, options) {
389
+ if (typeof ast === "boolean")
390
+ return ast;
391
+ const op = ast[0];
392
+ if (op === "$and" || op === "$or") {
393
+ return sql`(${join(
394
+ ast[1].map((node) => getNodeSql(node, options)),
395
+ `) ${opSql[op]} (`
396
+ )})`;
397
+ } else if (op === "$not") {
398
+ return sql`${opSql[op]} (${getNodeSql(ast[1], options)})`;
399
+ }
400
+ if (op === "$sub") {
401
+ const joinName = ast[1];
402
+ if (!options.joins[joinName])
403
+ throw Error(`pg.no_join ${joinName}`);
404
+ const { idCol, schema } = options;
405
+ const joinOptions = options.joins[joinName];
406
+ const { table: joinTable, refCol } = options.joins[joinName];
407
+ return sql`"${raw(idCol)}" IN (SELECT "${raw(refCol)}"::${raw(
408
+ schema.types[idCol]
409
+ )} FROM "${raw(joinTable)}" WHERE ${getNodeSql(ast[2], joinOptions)})`;
410
+ }
411
+ const [prefix, ...suffix] = ast[1].split(".");
412
+ const { types: types2 = {} } = options.schema;
413
+ if (!types2[prefix])
414
+ throw Error(`pg.no_column ${prefix}`);
415
+ if (types2[prefix] === "jsonb") {
416
+ const [lhs, textLhs] = suffix.length ? [
417
+ sql`"${raw(prefix)}" #> ${suffix}`,
418
+ sql`"${raw(prefix)}" #>> ${suffix}`
419
+ ] : [sql`"${raw(prefix)}"`, sql`"${raw(prefix)}" #>> '{}'`];
420
+ return getBinarySql(lhs, "jsonb", op, ast[2], textLhs);
421
+ } else {
422
+ if (suffix.length)
423
+ throw Error(`pg.lookup_not_jsonb ${prefix}`);
424
+ return getBinarySql(sql`"${raw(prefix)}"`, types2[prefix], op, ast[2]);
416
425
  }
417
- return getNodeSql(getAst(filter));
426
+ }
427
+ function getSql(filter, options) {
428
+ const ast = getAst(filter);
429
+ return getNodeSql(ast, options);
418
430
  }
419
431
  const getIdMeta = ({ idCol, verDefault }) => sql`"${raw(idCol)}" AS "$key", ${raw(verDefault)} AS "$ver"`;
420
432
  const getArgMeta = (key, { prefix, idCol, verDefault }) => sql`
@@ -658,10 +670,10 @@ class Db {
658
670
  }
659
671
  return res.rows[0];
660
672
  }
661
- async ensureSchema(tableOptions) {
673
+ async ensureSchema(tableOptions, typeOids) {
662
674
  if (tableOptions.schema)
663
675
  return;
664
- const { table, verCol } = tableOptions;
676
+ const { table, verCol, joins } = tableOptions;
665
677
  const tableSchema = (await this.query(sql`
666
678
  SELECT table_schema
667
679
  FROM information_schema.tables
@@ -676,7 +688,7 @@ class Db {
676
688
  table_schema = ${tableSchema}`)).rows[0].column_types;
677
689
  if (!types2)
678
690
  throw Error(`pg.missing_table ${table}`);
679
- const typeOids = (await this.query(sql`
691
+ typeOids = typeOids || (await this.query(sql`
680
692
  SELECT jsonb_object_agg(typname, oid) AS type_oids
681
693
  FROM pg_type
682
694
  WHERE typname = 'cube'`)).rows[0].type_oids;
@@ -690,6 +702,11 @@ class Db {
690
702
  if (!verDefault) {
691
703
  throw Error(`pg.verCol_without_default ${verCol}`);
692
704
  }
705
+ await Promise.all(
706
+ Object.values(joins).map(
707
+ (joinOptions) => this.ensureSchema(joinOptions, typeOids)
708
+ )
709
+ );
693
710
  log("ensureSchema", types2);
694
711
  tableOptions.schema = { types: types2, typeOids };
695
712
  tableOptions.verDefault = verDefault;
@@ -780,18 +797,33 @@ class Db {
780
797
  return result;
781
798
  }
782
799
  }
783
- const pg = ({ table, idCol, verCol, connection, schema, verDefault }) => (store) => {
784
- store.on("read", read);
785
- store.on("write", write);
786
- const prefix = store.path;
787
- const tableOpts = {
788
- prefix,
789
- table: table || prefix[prefix.length - 1] || "default",
800
+ function getTableOpts(name, { table, idCol, verCol, joins, schema, verDefault } = {}, parentName = null) {
801
+ const tableName = table || name;
802
+ return {
803
+ table: table || name,
790
804
  idCol: idCol || "id",
791
805
  verCol: verCol || "updatedAt",
806
+ joins: Object.fromEntries(
807
+ Object.entries(joins || {}).map(
808
+ ([joinName, { refCol = parentName, ...joinOptions }]) => [
809
+ joinName,
810
+ {
811
+ refCol,
812
+ ...getTableOpts(joinName, joinOptions, tableName)
813
+ }
814
+ ]
815
+ )
816
+ ),
792
817
  schema,
793
818
  verDefault
794
819
  };
820
+ }
821
+ const pg = ({ connection, ...rawOptions }) => (store) => {
822
+ store.on("read", read);
823
+ store.on("write", write);
824
+ const prefix = store.path;
825
+ const tableOpts = getTableOpts(prefix[prefix.length - 1], rawOptions);
826
+ tableOpts.prefix = prefix;
795
827
  const defaultDb = new Db(connection);
796
828
  function read(query, options, next) {
797
829
  const { pgClient } = options;
package/index.mjs CHANGED
@@ -124,9 +124,7 @@ function construct(node, prop, op) {
124
124
  return construct(val, prop, key);
125
125
  }
126
126
  if (prop) {
127
- if (key[0] === ".")
128
- return construct(val, prop + key);
129
- throw Error(`pgast.unexpected_prop: ${key}`);
127
+ return ["$sub", prop, construct({ [key]: val })];
130
128
  }
131
129
  return construct(val, key);
132
130
  })
@@ -138,6 +136,8 @@ function simplify(node) {
138
136
  node[1] = node[1].map((subnode) => simplify(subnode));
139
137
  } else if (op === "$not") {
140
138
  node[1] = simplify(node[1]);
139
+ } else if (op === "$sub") {
140
+ node[2] = simplify(node[2]);
141
141
  }
142
142
  if (op === "$and") {
143
143
  if (!node[1].length)
@@ -383,36 +383,48 @@ function getBinarySql(lhs, type, op, value, textLhs) {
383
383
  return sql`${lhs} ${sqlOp} ${cubeLiteralSql(value)}`;
384
384
  return sql`${lhs} ${sqlOp} ${value}`;
385
385
  }
386
- function getSql(filter, options) {
387
- function getNodeSql(ast) {
388
- if (typeof ast === "boolean")
389
- return ast;
390
- const op = ast[0];
391
- if (op === "$and" || op === "$or") {
392
- return sql`(${join(
393
- ast[1].map((node) => getNodeSql(node)),
394
- `) ${opSql[op]} (`
395
- )})`;
396
- } else if (op === "$not") {
397
- return sql`${opSql[op]} (${getNodeSql(ast[1])})`;
398
- }
399
- const [prefix, ...suffix] = ast[1].split(".");
400
- const { types: types2 } = options.schema;
401
- if (!types2[prefix])
402
- throw Error(`pg.no_column ${prefix}`);
403
- if (types2[prefix] === "jsonb") {
404
- const [lhs, textLhs] = suffix.length ? [
405
- sql`"${raw(prefix)}" #> ${suffix}`,
406
- sql`"${raw(prefix)}" #>> ${suffix}`
407
- ] : [sql`"${raw(prefix)}"`, sql`"${raw(prefix)}" #>> '{}'`];
408
- return getBinarySql(lhs, "jsonb", op, ast[2], textLhs);
409
- } else {
410
- if (suffix.length)
411
- throw Error(`pg.lookup_not_jsonb ${prefix}`);
412
- return getBinarySql(sql`"${raw(prefix)}"`, types2[prefix], op, ast[2]);
413
- }
386
+ function getNodeSql(ast, options) {
387
+ if (typeof ast === "boolean")
388
+ return ast;
389
+ const op = ast[0];
390
+ if (op === "$and" || op === "$or") {
391
+ return sql`(${join(
392
+ ast[1].map((node) => getNodeSql(node, options)),
393
+ `) ${opSql[op]} (`
394
+ )})`;
395
+ } else if (op === "$not") {
396
+ return sql`${opSql[op]} (${getNodeSql(ast[1], options)})`;
397
+ }
398
+ if (op === "$sub") {
399
+ const joinName = ast[1];
400
+ if (!options.joins[joinName])
401
+ throw Error(`pg.no_join ${joinName}`);
402
+ const { idCol, schema } = options;
403
+ const joinOptions = options.joins[joinName];
404
+ const { table: joinTable, refCol } = options.joins[joinName];
405
+ return sql`"${raw(idCol)}" IN (SELECT "${raw(refCol)}"::${raw(
406
+ schema.types[idCol]
407
+ )} FROM "${raw(joinTable)}" WHERE ${getNodeSql(ast[2], joinOptions)})`;
408
+ }
409
+ const [prefix, ...suffix] = ast[1].split(".");
410
+ const { types: types2 = {} } = options.schema;
411
+ if (!types2[prefix])
412
+ throw Error(`pg.no_column ${prefix}`);
413
+ if (types2[prefix] === "jsonb") {
414
+ const [lhs, textLhs] = suffix.length ? [
415
+ sql`"${raw(prefix)}" #> ${suffix}`,
416
+ sql`"${raw(prefix)}" #>> ${suffix}`
417
+ ] : [sql`"${raw(prefix)}"`, sql`"${raw(prefix)}" #>> '{}'`];
418
+ return getBinarySql(lhs, "jsonb", op, ast[2], textLhs);
419
+ } else {
420
+ if (suffix.length)
421
+ throw Error(`pg.lookup_not_jsonb ${prefix}`);
422
+ return getBinarySql(sql`"${raw(prefix)}"`, types2[prefix], op, ast[2]);
414
423
  }
415
- return getNodeSql(getAst(filter));
424
+ }
425
+ function getSql(filter, options) {
426
+ const ast = getAst(filter);
427
+ return getNodeSql(ast, options);
416
428
  }
417
429
  const getIdMeta = ({ idCol, verDefault }) => sql`"${raw(idCol)}" AS "$key", ${raw(verDefault)} AS "$ver"`;
418
430
  const getArgMeta = (key, { prefix, idCol, verDefault }) => sql`
@@ -656,10 +668,10 @@ class Db {
656
668
  }
657
669
  return res.rows[0];
658
670
  }
659
- async ensureSchema(tableOptions) {
671
+ async ensureSchema(tableOptions, typeOids) {
660
672
  if (tableOptions.schema)
661
673
  return;
662
- const { table, verCol } = tableOptions;
674
+ const { table, verCol, joins } = tableOptions;
663
675
  const tableSchema = (await this.query(sql`
664
676
  SELECT table_schema
665
677
  FROM information_schema.tables
@@ -674,7 +686,7 @@ class Db {
674
686
  table_schema = ${tableSchema}`)).rows[0].column_types;
675
687
  if (!types2)
676
688
  throw Error(`pg.missing_table ${table}`);
677
- const typeOids = (await this.query(sql`
689
+ typeOids = typeOids || (await this.query(sql`
678
690
  SELECT jsonb_object_agg(typname, oid) AS type_oids
679
691
  FROM pg_type
680
692
  WHERE typname = 'cube'`)).rows[0].type_oids;
@@ -688,6 +700,11 @@ class Db {
688
700
  if (!verDefault) {
689
701
  throw Error(`pg.verCol_without_default ${verCol}`);
690
702
  }
703
+ await Promise.all(
704
+ Object.values(joins).map(
705
+ (joinOptions) => this.ensureSchema(joinOptions, typeOids)
706
+ )
707
+ );
691
708
  log("ensureSchema", types2);
692
709
  tableOptions.schema = { types: types2, typeOids };
693
710
  tableOptions.verDefault = verDefault;
@@ -778,18 +795,33 @@ class Db {
778
795
  return result;
779
796
  }
780
797
  }
781
- const pg = ({ table, idCol, verCol, connection, schema, verDefault }) => (store) => {
782
- store.on("read", read);
783
- store.on("write", write);
784
- const prefix = store.path;
785
- const tableOpts = {
786
- prefix,
787
- table: table || prefix[prefix.length - 1] || "default",
798
+ function getTableOpts(name, { table, idCol, verCol, joins, schema, verDefault } = {}, parentName = null) {
799
+ const tableName = table || name;
800
+ return {
801
+ table: table || name,
788
802
  idCol: idCol || "id",
789
803
  verCol: verCol || "updatedAt",
804
+ joins: Object.fromEntries(
805
+ Object.entries(joins || {}).map(
806
+ ([joinName, { refCol = parentName, ...joinOptions }]) => [
807
+ joinName,
808
+ {
809
+ refCol,
810
+ ...getTableOpts(joinName, joinOptions, tableName)
811
+ }
812
+ ]
813
+ )
814
+ ),
790
815
  schema,
791
816
  verDefault
792
817
  };
818
+ }
819
+ const pg = ({ connection, ...rawOptions }) => (store) => {
820
+ store.on("read", read);
821
+ store.on("write", write);
822
+ const prefix = store.path;
823
+ const tableOpts = getTableOpts(prefix[prefix.length - 1], rawOptions);
824
+ tableOpts.prefix = prefix;
793
825
  const defaultDb = new Db(connection);
794
826
  function read(query, options, next) {
795
827
  const { pgClient } = options;
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.16.4-alpha.1",
5
+ "version": "0.16.4-alpha.2",
6
6
  "main": "./index.cjs",
7
7
  "exports": {
8
8
  "import": "./index.mjs",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "license": "Apache-2.0",
18
18
  "dependencies": {
19
- "@graffy/common": "0.16.4-alpha.1",
19
+ "@graffy/common": "0.16.4-alpha.2",
20
20
  "debug": "^4.3.3"
21
21
  },
22
22
  "peerDependencies": {
package/types/Db.d.ts CHANGED
@@ -4,7 +4,7 @@ export default class Db {
4
4
  query(sql: any, tableOptions: any): Promise<any>;
5
5
  readSql(sql: any, tableOptions: any): Promise<any>;
6
6
  writeSql(sql: any, tableOptions: any): Promise<any>;
7
- ensureSchema(tableOptions: any): Promise<void>;
7
+ ensureSchema(tableOptions: any, typeOids: any): Promise<void>;
8
8
  read(rootQuery: any, tableOptions: any): Promise<any>;
9
9
  write(rootChange: any, tableOptions: any): Promise<any[]>;
10
10
  }
package/types/index.d.ts CHANGED
@@ -1,8 +1,13 @@
1
- export function pg({ table, idCol, verCol, connection, schema, verDefault }: {
2
- table?: string;
3
- idCol?: string;
4
- verCol?: string;
5
- connection?: any;
1
+ export function pg({ connection, ...rawOptions }: Partial<TableOpts> & {
2
+ connection: any;
3
+ }): Function;
4
+ export type TableOpts = {
5
+ table: string;
6
+ idCol: string;
7
+ verCol: string;
8
+ joins: Record<string, Partial<TableOpts> & {
9
+ refCol: string;
10
+ }>;
6
11
  schema?: any;
7
12
  verDefault?: string;
8
- }): (store: any) => void;
13
+ };