@prairielearn/postgres 5.0.3 → 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;
@@ -133,6 +150,16 @@ function escapeIdentifier(identifier: string): string {
133
150
  return pg.Client.prototype.escapeIdentifier(identifier);
134
151
  }
135
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
+
136
163
  function enhanceError(err: Error, sql: string, params: QueryParams): Error {
137
164
  // Copy the error so we don't end up with a circular reference in the
138
165
  // final error.
@@ -625,8 +652,8 @@ export class PostgresPool {
625
652
  return result;
626
653
  }
627
654
 
628
- async queryRows<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>[]>;
629
- 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>(
630
657
  sql: string,
631
658
  params: QueryParams,
632
659
  model: Model,
@@ -634,10 +661,8 @@ export class PostgresPool {
634
661
  /**
635
662
  * Executes a query with the specified parameters. Returns an array of rows
636
663
  * that conform to the given Zod schema.
637
- *
638
- * If the query returns a single column, the return value will be a list of column values.
639
664
  */
640
- async queryRows<Model extends z.ZodTypeAny>(
665
+ async queryRows<Model extends AnyRowSchema>(
641
666
  sql: string,
642
667
  paramsOrSchema: QueryParams | Model,
643
668
  maybeModel?: Model,
@@ -645,27 +670,19 @@ export class PostgresPool {
645
670
  const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);
646
671
  const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
647
672
  const results = await this.queryAsync(sql, params);
648
- if (results.fields.length === 1) {
649
- const columnName = results.fields[0].name;
650
- const rawData = results.rows.map((row) => row[columnName]);
651
- return z.array(model).parse(rawData);
652
- } else {
653
- return z.array(model).parse(results.rows);
654
- }
673
+ return z.array(model).parse(results.rows);
655
674
  }
656
675
 
657
- async queryRow<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;
658
- 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>(
659
678
  sql: string,
660
679
  params: QueryParams,
661
680
  model: Model,
662
681
  ): Promise<z.infer<Model>>;
663
682
  /**
664
683
  * Executes a query with the specified parameters. Returns exactly one row that conforms to the given Zod schema.
665
- *
666
- * If the query returns a single column, the return value will be the column value itself.
667
684
  */
668
- async queryRow<Model extends z.ZodTypeAny>(
685
+ async queryRow<Model extends AnyRowSchema>(
669
686
  sql: string,
670
687
  paramsOrSchema: QueryParams | Model,
671
688
  maybeModel?: Model,
@@ -673,19 +690,14 @@ export class PostgresPool {
673
690
  const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);
674
691
  const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
675
692
  const results = await this.queryOneRowAsync(sql, params);
676
- if (results.fields.length === 1) {
677
- const columnName = results.fields[0].name;
678
- return model.parse(results.rows[0][columnName]);
679
- } else {
680
- return model.parse(results.rows[0]);
681
- }
693
+ return model.parse(results.rows[0]);
682
694
  }
683
695
 
684
- async queryOptionalRow<Model extends z.ZodTypeAny>(
696
+ async queryOptionalRow<Model extends AnyRowSchema>(
685
697
  sql: string,
686
698
  model: Model,
687
699
  ): Promise<z.infer<Model> | null>;
688
- async queryOptionalRow<Model extends z.ZodTypeAny>(
700
+ async queryOptionalRow<Model extends AnyRowSchema>(
689
701
  sql: string,
690
702
  params: QueryParams,
691
703
  model: Model,
@@ -693,10 +705,8 @@ export class PostgresPool {
693
705
  /**
694
706
  * Executes a query with the specified parameters. Returns either null or a
695
707
  * single row that conforms to the given Zod schema, and errors otherwise.
696
- *
697
- * If the query returns a single column, the return value will be the column value itself.
698
708
  */
699
- async queryOptionalRow<Model extends z.ZodTypeAny>(
709
+ async queryOptionalRow<Model extends AnyRowSchema>(
700
710
  sql: string,
701
711
  paramsOrSchema: QueryParams | Model,
702
712
  maybeModel?: Model,
@@ -706,16 +716,12 @@ export class PostgresPool {
706
716
  const results = await this.queryZeroOrOneRowAsync(sql, params);
707
717
  if (results.rows.length === 0) {
708
718
  return null;
709
- } else if (results.fields.length === 1) {
710
- const columnName = results.fields[0].name;
711
- return model.parse(results.rows[0][columnName]);
712
- } else {
713
- return model.parse(results.rows[0]);
714
719
  }
720
+ return model.parse(results.rows[0]);
715
721
  }
716
722
 
717
- async callRows<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>[]>;
718
- 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>(
719
725
  sql: string,
720
726
  params: any[],
721
727
  model: Model,
@@ -724,7 +730,7 @@ export class PostgresPool {
724
730
  * Calls the given sproc with the specified parameters.
725
731
  * Errors if the sproc does not return anything.
726
732
  */
727
- async callRows<Model extends z.ZodTypeAny>(
733
+ async callRows<Model extends AnyRowSchema>(
728
734
  sql: string,
729
735
  paramsOrSchema: any[] | Model,
730
736
  maybeModel?: Model,
@@ -732,17 +738,11 @@ export class PostgresPool {
732
738
  const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);
733
739
  const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
734
740
  const results = await this.callAsync(sql, params);
735
- if (results.fields.length === 1) {
736
- const columnName = results.fields[0].name;
737
- const rawData = results.rows.map((row) => row[columnName]);
738
- return z.array(model).parse(rawData);
739
- } else {
740
- return z.array(model).parse(results.rows);
741
- }
741
+ return z.array(model).parse(results.rows);
742
742
  }
743
743
 
744
- async callRow<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;
745
- 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>(
746
746
  sql: string,
747
747
  params: any[],
748
748
  model: Model,
@@ -751,7 +751,7 @@ export class PostgresPool {
751
751
  * Calls the given sproc with the specified parameters.
752
752
  * Returns exactly one row from the sproc that conforms to the given Zod schema.
753
753
  */
754
- async callRow<Model extends z.ZodTypeAny>(
754
+ async callRow<Model extends AnyRowSchema>(
755
755
  sql: string,
756
756
  paramsOrSchema: any[] | Model,
757
757
  maybeModel?: Model,
@@ -759,19 +759,14 @@ export class PostgresPool {
759
759
  const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);
760
760
  const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
761
761
  const results = await this.callOneRowAsync(sql, params);
762
- if (results.fields.length === 1) {
763
- const columnName = results.fields[0].name;
764
- return model.parse(results.rows[0][columnName]);
765
- } else {
766
- return model.parse(results.rows[0]);
767
- }
762
+ return model.parse(results.rows[0]);
768
763
  }
769
764
 
770
- async callOptionalRow<Model extends z.ZodTypeAny>(
765
+ async callOptionalRow<Model extends AnyRowSchema>(
771
766
  sql: string,
772
767
  model: Model,
773
768
  ): Promise<z.infer<Model> | null>;
774
- async callOptionalRow<Model extends z.ZodTypeAny>(
769
+ async callOptionalRow<Model extends AnyRowSchema>(
775
770
  sql: string,
776
771
  params: any[],
777
772
  model: Model,
@@ -780,7 +775,7 @@ export class PostgresPool {
780
775
  * Calls the given sproc with the specified parameters. Returns either null
781
776
  * or a single row that conforms to the given Zod schema.
782
777
  */
783
- async callOptionalRow<Model extends z.ZodTypeAny>(
778
+ async callOptionalRow<Model extends AnyRowSchema>(
784
779
  sql: string,
785
780
  paramsOrSchema: any[] | Model,
786
781
  maybeModel?: Model,
@@ -790,12 +785,162 @@ export class PostgresPool {
790
785
  const results = await this.callZeroOrOneRowAsync(sql, params);
791
786
  if (results.rows.length === 0) {
792
787
  return null;
793
- } else if (results.fields.length === 1) {
794
- const columnName = results.fields[0].name;
795
- return model.parse(results.rows[0][columnName]);
796
- } else {
797
- return model.parse(results.rows[0]);
798
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]);
799
944
  }
800
945
 
801
946
  /**
@@ -836,12 +981,12 @@ export class PostgresPool {
836
981
  return client.query(new Cursor(processedSql, paramsArray));
837
982
  }
838
983
 
839
- async queryCursor<Model extends z.ZodTypeAny>(
984
+ async queryCursor<Model extends AnyRowSchema>(
840
985
  sql: string,
841
986
  model: Model,
842
987
  ): Promise<CursorIterator<z.infer<Model>>>;
843
988
 
844
- async queryCursor<Model extends z.ZodTypeAny>(
989
+ async queryCursor<Model extends AnyRowSchema>(
845
990
  sql: string,
846
991
  params: QueryParams,
847
992
  model: Model,
@@ -852,7 +997,7 @@ export class PostgresPool {
852
997
  * results of the query in batches, which is useful for large result sets.
853
998
  * Each row will be parsed by the given Zod schema.
854
999
  */
855
- async queryCursor<Model extends z.ZodTypeAny>(
1000
+ async queryCursor<Model extends AnyRowSchema>(
856
1001
  sql: string,
857
1002
  paramsOrSchema: Model | QueryParams,
858
1003
  maybeModel?: Model,
@@ -862,7 +1007,7 @@ export class PostgresPool {
862
1007
  return this.queryCursorInternal(sql, params, model);
863
1008
  }
864
1009
 
865
- private async queryCursorInternal<Model extends z.ZodTypeAny>(
1010
+ private async queryCursorInternal<Model extends AnyRowSchema>(
866
1011
  sql: string,
867
1012
  params: QueryParams,
868
1013
  model?: Model,
@@ -871,7 +1016,6 @@ export class PostgresPool {
871
1016
  const cursor = await this.queryCursorWithClient(client, sql, params);
872
1017
 
873
1018
  let iterateCalled = false;
874
- let rowKeys: string[] | null = null;
875
1019
  const iterator: CursorIterator<z.infer<Model>> = {
876
1020
  async *iterate(batchSize: number) {
877
1021
  // Safety check: if someone calls iterate multiple times, they're
@@ -888,15 +1032,10 @@ export class PostgresPool {
888
1032
  break;
889
1033
  }
890
1034
 
891
- if (rowKeys === null) {
892
- rowKeys = Object.keys(rows[0] ?? {});
893
- }
894
- const flattened =
895
- rowKeys.length === 1 ? rows.map((row) => row[(rowKeys as string[])[0]]) : rows;
896
1035
  if (model) {
897
- yield z.array(model).parse(flattened);
1036
+ yield z.array(model).parse(rows);
898
1037
  } else {
899
- yield flattened;
1038
+ yield rows;
900
1039
  }
901
1040
  }
902
1041
  } catch (err: any) {