@prairielearn/postgres 5.0.2 → 6.0.0

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/src/pool.ts CHANGED
@@ -10,6 +10,23 @@ import { z } from 'zod';
10
10
 
11
11
  export type QueryParams = Record<string, any> | any[];
12
12
 
13
+ /**
14
+ * Type constraint for row schemas accepted by query functions.
15
+ * Accepts `z.object(...)`, unions/intersections/discriminated unions of objects,
16
+ * transforms/refinements of any of those, branded variants, and `z.unknown()`
17
+ * as an escape hatch.
18
+ */
19
+ type AnyObjectLikeSchema =
20
+ | z.AnyZodObject
21
+ | z.ZodEffects<AnyObjectLikeSchema, any, any>
22
+ | z.ZodIntersection<AnyObjectLikeSchema, AnyObjectLikeSchema>
23
+ | z.ZodUnion<Readonly<[AnyObjectLikeSchema, ...AnyObjectLikeSchema[]]>>
24
+ | z.ZodDiscriminatedUnion<string, z.AnyZodObject[]>;
25
+ export type AnyRowSchema =
26
+ | AnyObjectLikeSchema
27
+ | z.ZodBranded<AnyObjectLikeSchema, any>
28
+ | z.ZodUnknown;
29
+
13
30
  export interface CursorIterator<T> {
14
31
  iterate: (batchSize: number) => AsyncGenerator<T[]>;
15
32
  stream: (batchSize: number) => NodeJS.ReadWriteStream;
@@ -31,6 +48,7 @@ function addDataToError(err: Error, data: Record<string, any>): Error {
31
48
  return err;
32
49
  }
33
50
 
51
+ /** @knipignore */
34
52
  export class PostgresError extends Error {
35
53
  public data: Record<string, any>;
36
54
 
@@ -132,6 +150,16 @@ function escapeIdentifier(identifier: string): string {
132
150
  return pg.Client.prototype.escapeIdentifier(identifier);
133
151
  }
134
152
 
153
+ function assertSingleColumn(result: pg.QueryResult, context: Record<string, any>): string {
154
+ if (result.fields.length !== 1) {
155
+ throw new PostgresError(
156
+ `Expected exactly one column, but found ${result.fields.length}`,
157
+ context,
158
+ );
159
+ }
160
+ return result.fields[0].name;
161
+ }
162
+
135
163
  function enhanceError(err: Error, sql: string, params: QueryParams): Error {
136
164
  // Copy the error so we don't end up with a circular reference in the
137
165
  // final error.
@@ -624,8 +652,8 @@ export class PostgresPool {
624
652
  return result;
625
653
  }
626
654
 
627
- async queryRows<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>[]>;
628
- async queryRows<Model extends z.ZodTypeAny>(
655
+ async queryRows<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>[]>;
656
+ async queryRows<Model extends AnyRowSchema>(
629
657
  sql: string,
630
658
  params: QueryParams,
631
659
  model: Model,
@@ -633,10 +661,8 @@ export class PostgresPool {
633
661
  /**
634
662
  * Executes a query with the specified parameters. Returns an array of rows
635
663
  * that conform to the given Zod schema.
636
- *
637
- * If the query returns a single column, the return value will be a list of column values.
638
664
  */
639
- async queryRows<Model extends z.ZodTypeAny>(
665
+ async queryRows<Model extends AnyRowSchema>(
640
666
  sql: string,
641
667
  paramsOrSchema: QueryParams | Model,
642
668
  maybeModel?: Model,
@@ -644,27 +670,19 @@ export class PostgresPool {
644
670
  const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);
645
671
  const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
646
672
  const results = await this.queryAsync(sql, params);
647
- if (results.fields.length === 1) {
648
- const columnName = results.fields[0].name;
649
- const rawData = results.rows.map((row) => row[columnName]);
650
- return z.array(model).parse(rawData);
651
- } else {
652
- return z.array(model).parse(results.rows);
653
- }
673
+ return z.array(model).parse(results.rows);
654
674
  }
655
675
 
656
- async queryRow<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;
657
- async queryRow<Model extends z.ZodTypeAny>(
676
+ async queryRow<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>>;
677
+ async queryRow<Model extends AnyRowSchema>(
658
678
  sql: string,
659
679
  params: QueryParams,
660
680
  model: Model,
661
681
  ): Promise<z.infer<Model>>;
662
682
  /**
663
683
  * Executes a query with the specified parameters. Returns exactly one row that conforms to the given Zod schema.
664
- *
665
- * If the query returns a single column, the return value will be the column value itself.
666
684
  */
667
- async queryRow<Model extends z.ZodTypeAny>(
685
+ async queryRow<Model extends AnyRowSchema>(
668
686
  sql: string,
669
687
  paramsOrSchema: QueryParams | Model,
670
688
  maybeModel?: Model,
@@ -672,19 +690,14 @@ export class PostgresPool {
672
690
  const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);
673
691
  const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
674
692
  const results = await this.queryOneRowAsync(sql, params);
675
- if (results.fields.length === 1) {
676
- const columnName = results.fields[0].name;
677
- return model.parse(results.rows[0][columnName]);
678
- } else {
679
- return model.parse(results.rows[0]);
680
- }
693
+ return model.parse(results.rows[0]);
681
694
  }
682
695
 
683
- async queryOptionalRow<Model extends z.ZodTypeAny>(
696
+ async queryOptionalRow<Model extends AnyRowSchema>(
684
697
  sql: string,
685
698
  model: Model,
686
699
  ): Promise<z.infer<Model> | null>;
687
- async queryOptionalRow<Model extends z.ZodTypeAny>(
700
+ async queryOptionalRow<Model extends AnyRowSchema>(
688
701
  sql: string,
689
702
  params: QueryParams,
690
703
  model: Model,
@@ -692,10 +705,8 @@ export class PostgresPool {
692
705
  /**
693
706
  * Executes a query with the specified parameters. Returns either null or a
694
707
  * single row that conforms to the given Zod schema, and errors otherwise.
695
- *
696
- * If the query returns a single column, the return value will be the column value itself.
697
708
  */
698
- async queryOptionalRow<Model extends z.ZodTypeAny>(
709
+ async queryOptionalRow<Model extends AnyRowSchema>(
699
710
  sql: string,
700
711
  paramsOrSchema: QueryParams | Model,
701
712
  maybeModel?: Model,
@@ -705,16 +716,12 @@ export class PostgresPool {
705
716
  const results = await this.queryZeroOrOneRowAsync(sql, params);
706
717
  if (results.rows.length === 0) {
707
718
  return null;
708
- } else if (results.fields.length === 1) {
709
- const columnName = results.fields[0].name;
710
- return model.parse(results.rows[0][columnName]);
711
- } else {
712
- return model.parse(results.rows[0]);
713
719
  }
720
+ return model.parse(results.rows[0]);
714
721
  }
715
722
 
716
- async callRows<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>[]>;
717
- async callRows<Model extends z.ZodTypeAny>(
723
+ async callRows<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>[]>;
724
+ async callRows<Model extends AnyRowSchema>(
718
725
  sql: string,
719
726
  params: any[],
720
727
  model: Model,
@@ -723,7 +730,7 @@ export class PostgresPool {
723
730
  * Calls the given sproc with the specified parameters.
724
731
  * Errors if the sproc does not return anything.
725
732
  */
726
- async callRows<Model extends z.ZodTypeAny>(
733
+ async callRows<Model extends AnyRowSchema>(
727
734
  sql: string,
728
735
  paramsOrSchema: any[] | Model,
729
736
  maybeModel?: Model,
@@ -731,17 +738,11 @@ export class PostgresPool {
731
738
  const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);
732
739
  const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
733
740
  const results = await this.callAsync(sql, params);
734
- if (results.fields.length === 1) {
735
- const columnName = results.fields[0].name;
736
- const rawData = results.rows.map((row) => row[columnName]);
737
- return z.array(model).parse(rawData);
738
- } else {
739
- return z.array(model).parse(results.rows);
740
- }
741
+ return z.array(model).parse(results.rows);
741
742
  }
742
743
 
743
- async callRow<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;
744
- async callRow<Model extends z.ZodTypeAny>(
744
+ async callRow<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>>;
745
+ async callRow<Model extends AnyRowSchema>(
745
746
  sql: string,
746
747
  params: any[],
747
748
  model: Model,
@@ -750,7 +751,7 @@ export class PostgresPool {
750
751
  * Calls the given sproc with the specified parameters.
751
752
  * Returns exactly one row from the sproc that conforms to the given Zod schema.
752
753
  */
753
- async callRow<Model extends z.ZodTypeAny>(
754
+ async callRow<Model extends AnyRowSchema>(
754
755
  sql: string,
755
756
  paramsOrSchema: any[] | Model,
756
757
  maybeModel?: Model,
@@ -758,19 +759,14 @@ export class PostgresPool {
758
759
  const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);
759
760
  const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
760
761
  const results = await this.callOneRowAsync(sql, params);
761
- if (results.fields.length === 1) {
762
- const columnName = results.fields[0].name;
763
- return model.parse(results.rows[0][columnName]);
764
- } else {
765
- return model.parse(results.rows[0]);
766
- }
762
+ return model.parse(results.rows[0]);
767
763
  }
768
764
 
769
- async callOptionalRow<Model extends z.ZodTypeAny>(
765
+ async callOptionalRow<Model extends AnyRowSchema>(
770
766
  sql: string,
771
767
  model: Model,
772
768
  ): Promise<z.infer<Model> | null>;
773
- async callOptionalRow<Model extends z.ZodTypeAny>(
769
+ async callOptionalRow<Model extends AnyRowSchema>(
774
770
  sql: string,
775
771
  params: any[],
776
772
  model: Model,
@@ -779,7 +775,7 @@ export class PostgresPool {
779
775
  * Calls the given sproc with the specified parameters. Returns either null
780
776
  * or a single row that conforms to the given Zod schema.
781
777
  */
782
- async callOptionalRow<Model extends z.ZodTypeAny>(
778
+ async callOptionalRow<Model extends AnyRowSchema>(
783
779
  sql: string,
784
780
  paramsOrSchema: any[] | Model,
785
781
  maybeModel?: Model,
@@ -789,12 +785,162 @@ export class PostgresPool {
789
785
  const results = await this.callZeroOrOneRowAsync(sql, params);
790
786
  if (results.rows.length === 0) {
791
787
  return null;
792
- } else if (results.fields.length === 1) {
793
- const columnName = results.fields[0].name;
794
- return model.parse(results.rows[0][columnName]);
795
- } else {
796
- return model.parse(results.rows[0]);
797
788
  }
789
+ return model.parse(results.rows[0]);
790
+ }
791
+
792
+ async queryScalars<Model extends z.ZodTypeAny>(
793
+ sql: string,
794
+ model: Model,
795
+ ): Promise<z.infer<Model>[]>;
796
+ async queryScalars<Model extends z.ZodTypeAny>(
797
+ sql: string,
798
+ params: QueryParams,
799
+ model: Model,
800
+ ): Promise<z.infer<Model>[]>;
801
+ /**
802
+ * Executes a query and returns all values from a single column, validated
803
+ * against the given Zod schema. Errors if the query returns more than one column.
804
+ */
805
+ async queryScalars<Model extends z.ZodTypeAny>(
806
+ sql: string,
807
+ paramsOrSchema: QueryParams | Model,
808
+ maybeModel?: Model,
809
+ ) {
810
+ const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);
811
+ const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
812
+ const results = await this.queryAsync(sql, params);
813
+ const columnName = assertSingleColumn(results, { sql, sqlParams: params });
814
+ return z.array(model).parse(results.rows.map((row) => row[columnName]));
815
+ }
816
+
817
+ async queryScalar<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;
818
+ async queryScalar<Model extends z.ZodTypeAny>(
819
+ sql: string,
820
+ params: QueryParams,
821
+ model: Model,
822
+ ): Promise<z.infer<Model>>;
823
+ /**
824
+ * Executes a query and returns a single value from a single column, validated
825
+ * against the given Zod schema. Errors if the query does not return exactly
826
+ * one row or returns more than one column.
827
+ */
828
+ async queryScalar<Model extends z.ZodTypeAny>(
829
+ sql: string,
830
+ paramsOrSchema: QueryParams | Model,
831
+ maybeModel?: Model,
832
+ ) {
833
+ const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);
834
+ const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
835
+ const results = await this.queryOneRowAsync(sql, params);
836
+ const columnName = assertSingleColumn(results, { sql, sqlParams: params });
837
+ return model.parse(results.rows[0][columnName]);
838
+ }
839
+
840
+ async queryOptionalScalar<Model extends z.ZodTypeAny>(
841
+ sql: string,
842
+ model: Model,
843
+ ): Promise<z.infer<Model> | null>;
844
+ async queryOptionalScalar<Model extends z.ZodTypeAny>(
845
+ sql: string,
846
+ params: QueryParams,
847
+ model: Model,
848
+ ): Promise<z.infer<Model> | null>;
849
+ /**
850
+ * Executes a query and returns a single value from a single column, or null
851
+ * if no rows are returned. Validated against the given Zod schema. Errors if
852
+ * the query returns more than one row or more than one column.
853
+ */
854
+ async queryOptionalScalar<Model extends z.ZodTypeAny>(
855
+ sql: string,
856
+ paramsOrSchema: QueryParams | Model,
857
+ maybeModel?: Model,
858
+ ) {
859
+ const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);
860
+ const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
861
+ const results = await this.queryZeroOrOneRowAsync(sql, params);
862
+ const columnName = assertSingleColumn(results, { sql, sqlParams: params });
863
+ if (results.rows.length === 0) {
864
+ return null;
865
+ }
866
+ return model.parse(results.rows[0][columnName]);
867
+ }
868
+
869
+ async callScalars<Model extends z.ZodTypeAny>(
870
+ sql: string,
871
+ model: Model,
872
+ ): Promise<z.infer<Model>[]>;
873
+ async callScalars<Model extends z.ZodTypeAny>(
874
+ sql: string,
875
+ params: any[],
876
+ model: Model,
877
+ ): Promise<z.infer<Model>[]>;
878
+ /**
879
+ * Calls the given sproc and returns all values from a single column, validated
880
+ * against the given Zod schema. Errors if the sproc returns more than one column.
881
+ */
882
+ async callScalars<Model extends z.ZodTypeAny>(
883
+ sql: string,
884
+ paramsOrSchema: any[] | Model,
885
+ maybeModel?: Model,
886
+ ) {
887
+ const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);
888
+ const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
889
+ const results = await this.callAsync(sql, params);
890
+ const columnName = assertSingleColumn(results, { functionName: sql, sqlParams: params });
891
+ return z.array(model).parse(results.rows.map((row) => row[columnName]));
892
+ }
893
+
894
+ async callScalar<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;
895
+ async callScalar<Model extends z.ZodTypeAny>(
896
+ sql: string,
897
+ params: any[],
898
+ model: Model,
899
+ ): Promise<z.infer<Model>>;
900
+ /**
901
+ * Calls the given sproc and returns a single value from a single column, validated
902
+ * against the given Zod schema. Errors if the sproc does not return exactly
903
+ * one row or returns more than one column.
904
+ */
905
+ async callScalar<Model extends z.ZodTypeAny>(
906
+ sql: string,
907
+ paramsOrSchema: any[] | Model,
908
+ maybeModel?: Model,
909
+ ) {
910
+ const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);
911
+ const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
912
+ const results = await this.callOneRowAsync(sql, params);
913
+ const columnName = assertSingleColumn(results, { functionName: sql, sqlParams: params });
914
+ return model.parse(results.rows[0][columnName]);
915
+ }
916
+
917
+ async callOptionalScalar<Model extends z.ZodTypeAny>(
918
+ sql: string,
919
+ model: Model,
920
+ ): Promise<z.infer<Model> | null>;
921
+ async callOptionalScalar<Model extends z.ZodTypeAny>(
922
+ sql: string,
923
+ params: any[],
924
+ model: Model,
925
+ ): Promise<z.infer<Model> | null>;
926
+ /**
927
+ * Calls the given sproc and returns a single value from a single column, or
928
+ * null if no rows are returned. Validated against the given Zod schema.
929
+ * Errors if the sproc returns more than one row or more than one column.
930
+ */
931
+ async callOptionalScalar<Model extends z.ZodTypeAny>(
932
+ sql: string,
933
+ paramsOrSchema: any[] | Model,
934
+ maybeModel?: Model,
935
+ ) {
936
+ const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);
937
+ const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
938
+ const results = await this.callZeroOrOneRowAsync(sql, params);
939
+ const columnName = assertSingleColumn(results, { functionName: sql, sqlParams: params });
940
+ if (results.rows.length === 0) {
941
+ return null;
942
+ }
943
+ return model.parse(results.rows[0][columnName]);
798
944
  }
799
945
 
800
946
  /**
@@ -835,12 +981,12 @@ export class PostgresPool {
835
981
  return client.query(new Cursor(processedSql, paramsArray));
836
982
  }
837
983
 
838
- async queryCursor<Model extends z.ZodTypeAny>(
984
+ async queryCursor<Model extends AnyRowSchema>(
839
985
  sql: string,
840
986
  model: Model,
841
987
  ): Promise<CursorIterator<z.infer<Model>>>;
842
988
 
843
- async queryCursor<Model extends z.ZodTypeAny>(
989
+ async queryCursor<Model extends AnyRowSchema>(
844
990
  sql: string,
845
991
  params: QueryParams,
846
992
  model: Model,
@@ -851,7 +997,7 @@ export class PostgresPool {
851
997
  * results of the query in batches, which is useful for large result sets.
852
998
  * Each row will be parsed by the given Zod schema.
853
999
  */
854
- async queryCursor<Model extends z.ZodTypeAny>(
1000
+ async queryCursor<Model extends AnyRowSchema>(
855
1001
  sql: string,
856
1002
  paramsOrSchema: Model | QueryParams,
857
1003
  maybeModel?: Model,
@@ -861,7 +1007,7 @@ export class PostgresPool {
861
1007
  return this.queryCursorInternal(sql, params, model);
862
1008
  }
863
1009
 
864
- private async queryCursorInternal<Model extends z.ZodTypeAny>(
1010
+ private async queryCursorInternal<Model extends AnyRowSchema>(
865
1011
  sql: string,
866
1012
  params: QueryParams,
867
1013
  model?: Model,
@@ -870,7 +1016,6 @@ export class PostgresPool {
870
1016
  const cursor = await this.queryCursorWithClient(client, sql, params);
871
1017
 
872
1018
  let iterateCalled = false;
873
- let rowKeys: string[] | null = null;
874
1019
  const iterator: CursorIterator<z.infer<Model>> = {
875
1020
  async *iterate(batchSize: number) {
876
1021
  // Safety check: if someone calls iterate multiple times, they're
@@ -887,15 +1032,10 @@ export class PostgresPool {
887
1032
  break;
888
1033
  }
889
1034
 
890
- if (rowKeys === null) {
891
- rowKeys = Object.keys(rows[0] ?? {});
892
- }
893
- const flattened =
894
- rowKeys.length === 1 ? rows.map((row) => row[(rowKeys as string[])[0]]) : rows;
895
1035
  if (model) {
896
- yield z.array(model).parse(flattened);
1036
+ yield z.array(model).parse(rows);
897
1037
  } else {
898
- yield flattened;
1038
+ yield rows;
899
1039
  }
900
1040
  }
901
1041
  } catch (err: any) {