graphile-build-pg 4.9.1 → 4.11.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.
Files changed (76) hide show
  1. package/README.md +1 -0
  2. package/node8plus/plugins/PageInfoStartEndCursor.js +2 -2
  3. package/node8plus/plugins/PageInfoStartEndCursor.js.flow +6 -2
  4. package/node8plus/plugins/PageInfoStartEndCursor.js.map +1 -1
  5. package/node8plus/plugins/PgAllRows.js +1 -1
  6. package/node8plus/plugins/PgAllRows.js.flow +6 -3
  7. package/node8plus/plugins/PgAllRows.js.map +1 -1
  8. package/node8plus/plugins/PgBackwardRelationPlugin.js +2 -2
  9. package/node8plus/plugins/PgBackwardRelationPlugin.js.flow +8 -2
  10. package/node8plus/plugins/PgBackwardRelationPlugin.js.map +1 -1
  11. package/node8plus/plugins/PgBasicsPlugin.js +1 -1
  12. package/node8plus/plugins/PgBasicsPlugin.js.flow +1 -1
  13. package/node8plus/plugins/PgBasicsPlugin.js.map +1 -1
  14. package/node8plus/plugins/PgConditionComputedColumnPlugin.js +1 -1
  15. package/node8plus/plugins/PgConditionComputedColumnPlugin.js.flow +4 -1
  16. package/node8plus/plugins/PgConditionComputedColumnPlugin.js.map +1 -1
  17. package/node8plus/plugins/PgConnectionArgCondition.js +3 -3
  18. package/node8plus/plugins/PgConnectionArgCondition.js.flow +11 -3
  19. package/node8plus/plugins/PgConnectionArgCondition.js.map +1 -1
  20. package/node8plus/plugins/PgConnectionArgFirstLastBeforeAfter.js +5 -5
  21. package/node8plus/plugins/PgConnectionArgFirstLastBeforeAfter.js.flow +20 -7
  22. package/node8plus/plugins/PgConnectionArgFirstLastBeforeAfter.js.map +1 -1
  23. package/node8plus/plugins/PgConnectionArgOrderBy.js +2 -2
  24. package/node8plus/plugins/PgConnectionArgOrderBy.js.flow +8 -2
  25. package/node8plus/plugins/PgConnectionArgOrderBy.js.map +1 -1
  26. package/node8plus/plugins/PgConnectionTotalCount.js +1 -1
  27. package/node8plus/plugins/PgConnectionTotalCount.js.flow +4 -1
  28. package/node8plus/plugins/PgConnectionTotalCount.js.map +1 -1
  29. package/node8plus/plugins/PgForwardRelationPlugin.js +1 -1
  30. package/node8plus/plugins/PgForwardRelationPlugin.js.flow +4 -1
  31. package/node8plus/plugins/PgForwardRelationPlugin.js.map +1 -1
  32. package/node8plus/plugins/PgIntrospectionPlugin.d.ts +1 -0
  33. package/node8plus/plugins/PgIntrospectionPlugin.js +182 -159
  34. package/node8plus/plugins/PgIntrospectionPlugin.js.flow +218 -198
  35. package/node8plus/plugins/PgIntrospectionPlugin.js.map +1 -1
  36. package/node8plus/plugins/PgJWTPlugin.js +1 -1
  37. package/node8plus/plugins/PgJWTPlugin.js.flow +3 -1
  38. package/node8plus/plugins/PgJWTPlugin.js.map +1 -1
  39. package/node8plus/plugins/PgMutationCreatePlugin.js +7 -7
  40. package/node8plus/plugins/PgMutationCreatePlugin.js.flow +26 -7
  41. package/node8plus/plugins/PgMutationCreatePlugin.js.map +1 -1
  42. package/node8plus/plugins/PgMutationPayloadEdgePlugin.js +2 -2
  43. package/node8plus/plugins/PgMutationPayloadEdgePlugin.js.flow +8 -2
  44. package/node8plus/plugins/PgMutationPayloadEdgePlugin.js.map +1 -1
  45. package/node8plus/plugins/PgMutationUpdateDeletePlugin.js +11 -11
  46. package/node8plus/plugins/PgMutationUpdateDeletePlugin.js.flow +40 -11
  47. package/node8plus/plugins/PgMutationUpdateDeletePlugin.js.map +1 -1
  48. package/node8plus/plugins/PgRecordFunctionConnectionPlugin.js +6 -6
  49. package/node8plus/plugins/PgRecordFunctionConnectionPlugin.js.flow +24 -6
  50. package/node8plus/plugins/PgRecordFunctionConnectionPlugin.js.map +1 -1
  51. package/node8plus/plugins/PgRecordReturnTypesPlugin.js +1 -1
  52. package/node8plus/plugins/PgRecordReturnTypesPlugin.js.flow +6 -3
  53. package/node8plus/plugins/PgRecordReturnTypesPlugin.js.map +1 -1
  54. package/node8plus/plugins/PgRowNode.js +2 -2
  55. package/node8plus/plugins/PgRowNode.js.flow +8 -2
  56. package/node8plus/plugins/PgRowNode.js.map +1 -1
  57. package/node8plus/plugins/PgScalarFunctionConnectionPlugin.js +6 -6
  58. package/node8plus/plugins/PgScalarFunctionConnectionPlugin.js.flow +24 -6
  59. package/node8plus/plugins/PgScalarFunctionConnectionPlugin.js.map +1 -1
  60. package/node8plus/plugins/PgTablesPlugin.js +13 -13
  61. package/node8plus/plugins/PgTablesPlugin.js.flow +56 -20
  62. package/node8plus/plugins/PgTablesPlugin.js.map +1 -1
  63. package/node8plus/plugins/PgTypesPlugin.js +62 -78
  64. package/node8plus/plugins/PgTypesPlugin.js.flow +193 -76
  65. package/node8plus/plugins/PgTypesPlugin.js.map +1 -1
  66. package/node8plus/plugins/makeProcField.js +3 -3
  67. package/node8plus/plugins/makeProcField.js.flow +16 -7
  68. package/node8plus/plugins/makeProcField.js.map +1 -1
  69. package/node8plus/plugins/viaTemporaryTable.js +1 -1
  70. package/node8plus/plugins/viaTemporaryTable.js.flow +8 -4
  71. package/node8plus/plugins/viaTemporaryTable.js.map +1 -1
  72. package/node8plus/postgresInterval.js +12 -33
  73. package/node8plus/postgresInterval.js.flow +22 -51
  74. package/node8plus/postgresInterval.js.map +1 -1
  75. package/package.json +9 -9
  76. package/res/watch-fixtures.sql +1 -1
@@ -81,6 +81,9 @@ export type PgClass = {
81
81
  aclUpdatable: boolean,
82
82
  aclDeletable: boolean,
83
83
  canUseAsterisk: boolean,
84
+
85
+ // eslint-disable-next-line flowtype/no-weak-types
86
+ _internalEnumData?: any[], // This is Graphile internal, do not use this.
84
87
  };
85
88
 
86
89
  export type PgType = {
@@ -109,6 +112,7 @@ export type PgType = {
109
112
  enumDescriptions: ?(string[]),
110
113
  rangeSubTypeId: ?string,
111
114
  tags: { [string]: string },
115
+ isFake: ?boolean,
112
116
  };
113
117
 
114
118
  export type PgAttribute = {
@@ -357,7 +361,7 @@ function smartCommentConstraints(introspectionResults) {
357
361
  }
358
362
  if (klass.tags.unique) {
359
363
  if (Array.isArray(klass.tags.unique)) {
360
- klass.tags.unique.forEach(key => addKey(key, true));
364
+ klass.tags.unique.forEach(key => addKey(key));
361
365
  } else {
362
366
  addKey(klass.tags.unique);
363
367
  }
@@ -465,6 +469,165 @@ function smartCommentConstraints(introspectionResults) {
465
469
  });
466
470
  }
467
471
 
472
+ function isEnumConstraint(
473
+ klass: PgClass,
474
+ con: PgConstraint,
475
+ isEnumTable: boolean
476
+ ) {
477
+ if (con.classId === klass.id) {
478
+ const isPrimaryKey = con.type === "p";
479
+ const isUniqueConstraint = con.type === "u";
480
+ if (isPrimaryKey || isUniqueConstraint) {
481
+ const isExplicitEnumConstraint =
482
+ con.tags.enum === true || typeof con.tags.enum === "string";
483
+ const isPrimaryKeyOfEnumTableConstraint = con.type === "p" && isEnumTable;
484
+ if (isExplicitEnumConstraint || isPrimaryKeyOfEnumTableConstraint) {
485
+ const hasExactlyOneColumn = con.keyAttributeNums.length === 1;
486
+ if (!hasExactlyOneColumn) {
487
+ throw new Error(
488
+ `Enum table "${klass.namespaceName}"."${klass.name}" enum constraint '${con.name}' is composite; it should have exactly one column (found: ${con.keyAttributeNums.length})`
489
+ );
490
+ }
491
+ return true;
492
+ }
493
+ }
494
+ }
495
+ return false;
496
+ }
497
+
498
+ function enumTables(introspectionResults) {
499
+ introspectionResults.class.map(async klass => {
500
+ const isEnumTable =
501
+ klass.tags.enum === true || typeof klass.tags.enum === "string";
502
+
503
+ if (isEnumTable) {
504
+ // Prevent the table being recognised as a table
505
+ // eslint-disable-next-line require-atomic-updates
506
+ klass.tags.omit = true;
507
+ // eslint-disable-next-line require-atomic-updates
508
+ klass.isSelectable = false;
509
+ // eslint-disable-next-line require-atomic-updates
510
+ klass.isInsertable = false;
511
+ // eslint-disable-next-line require-atomic-updates
512
+ klass.isUpdatable = false;
513
+ // eslint-disable-next-line require-atomic-updates
514
+ klass.isDeletable = false;
515
+ }
516
+
517
+ // By this point, even views should have "fake" constraints we can use
518
+ // (e.g. `@primaryKey`)
519
+ const enumConstraints = introspectionResults.constraint.filter(con =>
520
+ isEnumConstraint(klass, con, isEnumTable)
521
+ );
522
+
523
+ // Get all the columns
524
+ const enumTableColumns = introspectionResults.attribute.filter(
525
+ attr => attr.classId === klass.id
526
+ );
527
+
528
+ // Get description column
529
+ const descriptionColumn = enumTableColumns.find(
530
+ attr => attr.name === "description" || attr.tags.enumDescription
531
+ );
532
+ const allData = klass._internalEnumData || [];
533
+
534
+ enumConstraints.forEach(constraint => {
535
+ const col = enumTableColumns.find(
536
+ col => col.num === constraint.keyAttributeNums[0]
537
+ );
538
+ if (!col) {
539
+ // Should never happen
540
+ throw new Error(
541
+ "Graphile Engine error - could not find column for enum constraint"
542
+ );
543
+ }
544
+ const data = allData.filter(row => row[col.name] != null);
545
+ if (data.length < 1) {
546
+ throw new Error(
547
+ `Enum table "${klass.namespaceName}"."${klass.name}" contains no entries for enum constraint '${constraint.name}'.`
548
+ );
549
+ }
550
+
551
+ // Create fake enum type
552
+ const constraintIdent =
553
+ constraint.type === "p" ? "" : `_${constraint.name}`;
554
+ const enumTypeArray = {
555
+ kind: "type",
556
+ isFake: true,
557
+ id: `FAKE_ENUM_${klass.namespaceName}_${klass.name}${constraintIdent}_list`,
558
+ name: `_${klass.name}${constraintIdent}`,
559
+ description: null,
560
+ tags: {},
561
+ namespaceId: klass.namespaceId,
562
+ namespaceName: klass.namespaceName,
563
+ type: "b",
564
+ category: "A",
565
+ domainIsNotNull: null,
566
+ arrayItemTypeId: null,
567
+ typeLength: -1,
568
+ isPgArray: true,
569
+ classId: null,
570
+ domainBaseTypeId: null,
571
+ domainTypeModifier: null,
572
+ domainHasDefault: false,
573
+ enumVariants: null,
574
+ enumDescriptions: null,
575
+ rangeSubTypeId: null,
576
+ };
577
+ const enumType = {
578
+ kind: "type",
579
+ isFake: true,
580
+ id: `FAKE_ENUM_${klass.namespaceName}_${klass.name}${constraintIdent}`,
581
+ name: `${klass.name}${constraintIdent}`,
582
+ description: klass.description,
583
+ tags: { ...klass.tags, ...constraint.tags },
584
+ namespaceId: klass.namespaceId,
585
+ namespaceName: klass.namespaceName,
586
+ type: "e",
587
+ category: "E",
588
+ domainIsNotNull: null,
589
+ arrayItemTypeId: enumTypeArray.id,
590
+ typeLength: 4, // ???
591
+ isPgArray: false,
592
+ classId: null,
593
+ domainBaseTypeId: null,
594
+ domainTypeModifier: null,
595
+ domainHasDefault: false,
596
+ enumVariants: data.map(r => r[col.name]),
597
+ enumDescriptions: descriptionColumn
598
+ ? data.map(r => r[descriptionColumn.name])
599
+ : null,
600
+ // TODO: enumDescriptions
601
+ rangeSubTypeId: null,
602
+ };
603
+ introspectionResults.type.push(enumType, enumTypeArray);
604
+ introspectionResults.typeById[enumType.id] = enumType;
605
+ introspectionResults.typeById[enumTypeArray.id] = enumTypeArray;
606
+
607
+ // Change type of all attributes that reference this table to
608
+ // reference this enum type
609
+ introspectionResults.constraint.forEach(c => {
610
+ if (
611
+ c.type === "f" &&
612
+ c.foreignClassId === klass.id &&
613
+ c.foreignKeyAttributeNums.length === 1 &&
614
+ c.foreignKeyAttributeNums[0] === col.num
615
+ ) {
616
+ // Get the attribute
617
+ const fkattr = introspectionResults.attribute.find(
618
+ attr =>
619
+ attr.classId === c.classId && attr.num === c.keyAttributeNums[0]
620
+ );
621
+ if (fkattr) {
622
+ // Override the detected type to pretend to be our enum
623
+ fkattr.typeId = enumType.id;
624
+ }
625
+ }
626
+ });
627
+ });
628
+ });
629
+ }
630
+
468
631
  /* The argument to this must not contain cyclic references! */
469
632
  const deepClone = value => {
470
633
  if (Array.isArray(value)) {
@@ -570,108 +733,46 @@ export default (async function PgIntrospectionPlugin(
570
733
  extensionConfigurationClassIds.indexOf(klass.id) >= 0;
571
734
  });
572
735
 
736
+ // Assert the columns are text
737
+ const VARCHAR_ID = "1043";
738
+ const TEXT_ID = "25";
739
+ const CHAR_ID = "18";
740
+ const BPCHAR_ID = "1042";
741
+
742
+ const VALID_TYPE_IDS = [VARCHAR_ID, TEXT_ID, CHAR_ID, BPCHAR_ID];
743
+
573
744
  await Promise.all(
574
745
  result.class.map(async klass => {
746
+ if (!schemas.includes(klass.namespaceName)) {
747
+ // Only support enums in public tables/views
748
+ return;
749
+ }
575
750
  const isEnumTable =
576
751
  klass.tags.enum === true || typeof klass.tags.enum === "string";
577
752
 
578
- if (isEnumTable) {
579
- // Prevent the table being recognised as a table
580
- // eslint-disable-next-line require-atomic-updates
581
- klass.tags.omit = true;
582
- // eslint-disable-next-line require-atomic-updates
583
- klass.isSelectable = false;
584
- // eslint-disable-next-line require-atomic-updates
585
- klass.isInsertable = false;
586
- // eslint-disable-next-line require-atomic-updates
587
- klass.isUpdatable = false;
588
- // eslint-disable-next-line require-atomic-updates
589
- klass.isDeletable = false;
590
- }
591
-
592
- const enumConstraints = result.constraint.filter(
593
- (con: PgConstraint) => {
594
- if (con.classId === klass.id) {
595
- const isPrimaryKey = con.type === "p";
596
- const isUniqueConstraint = con.type === "u";
597
- if (isPrimaryKey || isUniqueConstraint) {
598
- const isExplicitEnumConstraint =
599
- con.tags.enum === true ||
600
- typeof con.tags.enum === "string";
601
- const isPrimaryKeyOfEnumTableConstraint =
602
- con.type === "p" && isEnumTable;
603
- if (
604
- isExplicitEnumConstraint ||
605
- isPrimaryKeyOfEnumTableConstraint
606
- ) {
607
- const hasExactlyOneColumn =
608
- con.keyAttributeNums.length === 1;
609
- if (!hasExactlyOneColumn) {
610
- throw new Error(
611
- `Enum table "${klass.namespaceName}"."${klass.name}" enum constraint '${con.name}' is composite; it should have exactly one column (found: ${con.keyAttributeNums.length})`
612
- );
613
- }
614
- return true;
615
- }
616
- }
617
- }
618
- return false;
619
- }
753
+ // NOTE: this only matches on tables (not views, since they don't
754
+ // have constraints), which is why we repeat the isEnumTable check below.
755
+ const hasEnumConstraints = result.constraint.some(con =>
756
+ isEnumConstraint(klass, con, isEnumTable)
620
757
  );
621
- if (enumConstraints.length > 0) {
622
- // Get all the columns
758
+ if (isEnumTable || hasEnumConstraints) {
759
+ // Get the list of columns enums are defined for
623
760
  const enumTableColumns = result.attribute
624
- .filter(attr => attr.classId === klass.id)
761
+ .filter(
762
+ attr =>
763
+ attr.classId === klass.id &&
764
+ VALID_TYPE_IDS.includes(attr.typeId)
765
+ )
625
766
  .sort((a, z) => a.num - z.num);
626
767
 
627
- // Get description column
628
- const descriptionColumn = enumTableColumns.find(
629
- attr =>
630
- attr.name === "description" || attr.tags.enumDescription
631
- );
632
-
633
- // Assert the columns are text
634
- const VARCHAR_ID = "1043";
635
- const TEXT_ID = "25";
636
- const CHAR_ID = "18";
637
- const BPCHAR_ID = "1042";
638
-
639
- // Get the list of columns enums are defined for
640
- const enumColumns = enumConstraints.map(con => {
641
- const attr = enumTableColumns.find(
642
- attr => attr.num === con.keyAttributeNums[0]
643
- );
644
- if (!attr) {
645
- throw new Error(
646
- `Enum table "${klass.namespaceName}"."${klass.name}" enum column '${con.keyAttributeNums[0]}' couldn't be found`
647
- );
648
- }
649
- if (
650
- attr.typeId !== VARCHAR_ID &&
651
- attr.typeId !== TEXT_ID &&
652
- attr.typeId !== CHAR_ID &&
653
- attr.typeId !== BPCHAR_ID
654
- ) {
655
- throw new Error(
656
- `Enum table "${klass.namespaceName}"."${klass.name}" enum column '${attr.name}' must be 'text', 'char' or 'varchar' (actual type OID: ${attr.typeId})`
657
- );
658
- }
659
- return attr;
660
- });
661
-
662
- // Load data from the table.
768
+ // Load data from the table/view.
663
769
  const query = pgSql.compile(
664
770
  pgSql.fragment`select ${pgSql.join(
665
- enumColumns.map(col => pgSql.identifier(col.name)),
771
+ enumTableColumns.map(col => pgSql.identifier(col.name)),
666
772
  ", "
667
- )}${
668
- descriptionColumn
669
- ? pgSql.fragment`, ${pgSql.identifier(
670
- descriptionColumn.name
671
- )}`
672
- : pgSql.blank
673
- } from ${pgSql.identifier(klass.namespaceName, klass.name)};`
773
+ )} from ${pgSql.identifier(klass.namespaceName, klass.name)};`
674
774
  );
775
+
675
776
  let allData;
676
777
  try {
677
778
  ({ rows: allData } = await pgClient.query(query));
@@ -683,7 +784,10 @@ export default (async function PgIntrospectionPlugin(
683
784
  } = await pgClient.query("select user;");
684
785
  role = user;
685
786
  } catch (e) {
686
- /* ignore */
787
+ /*
788
+ * Ignore; this is likely a 25P02 (transaction aborted)
789
+ * error caused by the statement above failing.
790
+ */
687
791
  }
688
792
  throw new Error(`Introspection could not read from enum table "${klass.namespaceName}"."${klass.name}", perhaps you need to grant access:
689
793
 
@@ -694,98 +798,7 @@ Original error: ${e.message}
694
798
  `);
695
799
  }
696
800
 
697
- // Assert there's at least one value
698
- enumConstraints.forEach(constraint => {
699
- const col = enumColumns.find(
700
- col => col.num === constraint.keyAttributeNums[0]
701
- );
702
- if (!col) {
703
- // Should never happen
704
- throw new Error(
705
- "Graphile Engine error - could not find column for enum constraint"
706
- );
707
- }
708
- const data = allData.filter(row => row[col.name] != null);
709
- if (data.length < 1) {
710
- throw new Error(
711
- `Enum table "${klass.namespaceName}"."${klass.name}" contains no entries for enum constraint '${constraint.name}'.`
712
- );
713
- }
714
-
715
- // Create fake enum type
716
- const constraintIdent =
717
- constraint.type === "p" ? "" : `_${constraint.name}`;
718
- const enumTypeArray = {
719
- kind: "type",
720
- id: `FAKE_ENUM_${klass.namespaceName}_${klass.name}${constraintIdent}_list`,
721
- name: `_${klass.name}${constraintIdent}`,
722
- description: null,
723
- tags: {},
724
- namespaceId: klass.namespaceId,
725
- namespaceName: klass.namespaceName,
726
- type: "b",
727
- category: "A",
728
- domainIsNotNull: null,
729
- arrayItemTypeId: null,
730
- typeLength: -1,
731
- isPgArray: true,
732
- classId: null,
733
- domainBaseTypeId: null,
734
- domainTypeModifier: null,
735
- domainHasDefault: false,
736
- enumVariants: null,
737
- enumDescriptions: null,
738
- rangeSubTypeId: null,
739
- };
740
- const enumType = {
741
- kind: "type",
742
- id: `FAKE_ENUM_${klass.namespaceName}_${klass.name}${constraintIdent}`,
743
- name: `${klass.name}${constraintIdent}`,
744
- description: klass.description,
745
- tags: { ...klass.tags, ...constraint.tags },
746
- namespaceId: klass.namespaceId,
747
- namespaceName: klass.namespaceName,
748
- type: "e",
749
- category: "E",
750
- domainIsNotNull: null,
751
- arrayItemTypeId: enumTypeArray.id,
752
- typeLength: 4, // ???
753
- isPgArray: false,
754
- classId: null,
755
- domainBaseTypeId: null,
756
- domainTypeModifier: null,
757
- domainHasDefault: false,
758
- enumVariants: data.map(r => r[col.name]),
759
- enumDescriptions: descriptionColumn
760
- ? data.map(r => r[descriptionColumn.name])
761
- : null,
762
- // TODO: enumDescriptions
763
- rangeSubTypeId: null,
764
- };
765
- result.type.push(enumType, enumTypeArray);
766
-
767
- // Change type of all attributes that reference this table to
768
- // reference this enum type
769
- result.constraint.forEach(c => {
770
- if (
771
- c.type === "f" &&
772
- c.foreignClassId === klass.id &&
773
- c.foreignKeyAttributeNums.length === 1 &&
774
- c.foreignKeyAttributeNums[0] === col.num
775
- ) {
776
- // Get the attribute
777
- const fkattr = result.attribute.find(
778
- attr =>
779
- attr.classId === c.classId &&
780
- attr.num === c.keyAttributeNums[0]
781
- );
782
- if (fkattr) {
783
- // Override the detected type to pretend to be our enum
784
- fkattr.typeId = enumType.id;
785
- }
786
- }
787
- });
788
- });
801
+ klass._internalEnumData = allData;
789
802
  }
790
803
  })
791
804
  );
@@ -891,7 +904,7 @@ Original error: ${e.message}
891
904
  return;
892
905
  }
893
906
  throw new Error(
894
- `Could not look up '${newAttr}' by '${lookupAttr}' on '${JSON.stringify(
907
+ `Could not look up '${newAttr}' by '${lookupAttr}' (= '${key}') on '${JSON.stringify(
895
908
  entry
896
909
  )}'`
897
910
  );
@@ -902,9 +915,11 @@ Original error: ${e.message}
902
915
  };
903
916
 
904
917
  const augment = introspectionResults => {
905
- [pgAugmentIntrospectionResults, smartCommentConstraints].forEach(fn =>
906
- fn ? fn(introspectionResults) : null
907
- );
918
+ [
919
+ pgAugmentIntrospectionResults,
920
+ smartCommentConstraints,
921
+ enumTables,
922
+ ].forEach(fn => (fn ? fn(introspectionResults) : null));
908
923
  };
909
924
  augment(introspectionResultsByKind);
910
925
 
@@ -1189,9 +1204,7 @@ Original error: ${e.message}
1189
1204
 
1190
1205
  _handleClientError: (e: Error) => void;
1191
1206
  _handleClientError(e) {
1192
- // Client is already cleaned up
1193
- this.client = null;
1194
- this._reallyReleaseClient = null;
1207
+ this._releaseClient(false);
1195
1208
  this._reconnect(e);
1196
1209
  }
1197
1210
  async _reconnect(e) {
@@ -1250,25 +1263,32 @@ Original error: ${e.message}
1250
1263
 
1251
1264
  async stop() {
1252
1265
  this.stopped = true;
1266
+ this._handleChange.cancel();
1253
1267
  await this._releaseClient();
1254
1268
  }
1255
1269
 
1256
- async _releaseClient() {
1270
+ /**
1271
+ * Only pass `false` to this function if you know the client is going to be
1272
+ * terminated; otherwise we risk leaving listeners running.
1273
+ */
1274
+ async _releaseClient(clientIsStillViable = true) {
1257
1275
  // $FlowFixMe
1258
- this._handleChange.cancel();
1259
1276
  const pgClient = this.client;
1260
1277
  const reallyReleaseClient = this._reallyReleaseClient;
1261
1278
  this.client = null;
1262
1279
  this._reallyReleaseClient = null;
1263
1280
  if (pgClient) {
1264
- pgClient.query("unlisten postgraphile_watch").catch(e => {
1265
- debug(`Error occurred trying to unlisten watch: ${e}`);
1266
- });
1281
+ // Don't attempt to run a query after a client has errored.
1282
+ if (clientIsStillViable) {
1283
+ pgClient.query("unlisten postgraphile_watch").catch(e => {
1284
+ debug(`Error occurred trying to unlisten watch: ${e}`);
1285
+ });
1286
+ }
1267
1287
  pgClient.removeListener("notification", this._listener);
1268
1288
  pgClient.removeListener("error", this._handleClientError);
1269
- if (reallyReleaseClient) {
1270
- await reallyReleaseClient();
1271
- }
1289
+ }
1290
+ if (reallyReleaseClient) {
1291
+ await reallyReleaseClient();
1272
1292
  }
1273
1293
  }
1274
1294
  }