encore.dev 0.0.0-devel.202311141645

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 (60) hide show
  1. package/LICENSE +362 -0
  2. package/README.md +4 -0
  3. package/api/mod.ts +47 -0
  4. package/config/mod.ts +2 -0
  5. package/config/secrets.ts +27 -0
  6. package/cron/mod.ts +17 -0
  7. package/dist/api/mod.d.ts +7 -0
  8. package/dist/api/mod.js +5 -0
  9. package/dist/api/mod.js.map +1 -0
  10. package/dist/config/mod.d.ts +2 -0
  11. package/dist/config/mod.js +2 -0
  12. package/dist/config/mod.js.map +1 -0
  13. package/dist/config/secrets.d.ts +11 -0
  14. package/dist/config/secrets.js +15 -0
  15. package/dist/config/secrets.js.map +1 -0
  16. package/dist/cron/mod.d.ts +14 -0
  17. package/dist/cron/mod.js +10 -0
  18. package/dist/cron/mod.js.map +1 -0
  19. package/dist/internal/types/mod.d.ts +7 -0
  20. package/dist/internal/types/mod.js +2 -0
  21. package/dist/internal/types/mod.js.map +1 -0
  22. package/dist/log/mod.d.ts +3 -0
  23. package/dist/log/mod.js +4 -0
  24. package/dist/log/mod.js.map +1 -0
  25. package/dist/mod.d.ts +0 -0
  26. package/dist/mod.js +2 -0
  27. package/dist/mod.js.map +1 -0
  28. package/dist/pubsub/acker.d.ts +26 -0
  29. package/dist/pubsub/acker.js +73 -0
  30. package/dist/pubsub/acker.js.map +1 -0
  31. package/dist/pubsub/attributes.d.ts +59 -0
  32. package/dist/pubsub/attributes.js +2 -0
  33. package/dist/pubsub/attributes.js.map +1 -0
  34. package/dist/pubsub/mod.d.ts +5 -0
  35. package/dist/pubsub/mod.js +3 -0
  36. package/dist/pubsub/mod.js.map +1 -0
  37. package/dist/pubsub/subscription.d.ts +100 -0
  38. package/dist/pubsub/subscription.js +164 -0
  39. package/dist/pubsub/subscription.js.map +1 -0
  40. package/dist/pubsub/topic.d.ts +105 -0
  41. package/dist/pubsub/topic.js +62 -0
  42. package/dist/pubsub/topic.js.map +1 -0
  43. package/dist/storage/sqldb/database.d.ts +51 -0
  44. package/dist/storage/sqldb/database.js +78 -0
  45. package/dist/storage/sqldb/database.js.map +1 -0
  46. package/dist/storage/sqldb/mod.d.ts +2 -0
  47. package/dist/storage/sqldb/mod.js +2 -0
  48. package/dist/storage/sqldb/mod.js.map +1 -0
  49. package/dist/tsconfig.tsbuildinfo +1 -0
  50. package/internal/types/mod.ts +10 -0
  51. package/log/mod.ts +4 -0
  52. package/mod.ts +0 -0
  53. package/package.json +77 -0
  54. package/pubsub/acker.ts +79 -0
  55. package/pubsub/attributes.ts +66 -0
  56. package/pubsub/mod.ts +7 -0
  57. package/pubsub/subscription.ts +302 -0
  58. package/pubsub/topic.ts +132 -0
  59. package/storage/sqldb/database.ts +105 -0
  60. package/storage/sqldb/mod.ts +2 -0
@@ -0,0 +1,302 @@
1
+ import { api } from "@encore.dev/internal-runtime";
2
+ import reqtrack, { Context } from "@encore.dev/internal-runtime/reqtrack/index";
3
+ import {
4
+ newSpanID,
5
+ newTraceID,
6
+ } from "@encore.dev/internal-runtime/reqtrack/tracecontext";
7
+ import {
8
+ logCtx,
9
+ withLogCtx,
10
+ } from "@encore.dev/internal-runtime/reqtrack/logging";
11
+ import {
12
+ durationToProto,
13
+ now,
14
+ parseDuration,
15
+ since,
16
+ toDurationStr,
17
+ } from "@encore.dev/internal-runtime/utils/timers";
18
+ import { runtime } from "@encore.dev/internal-runtime/jsruntime";
19
+ import log, { type Logger } from "../log/mod";
20
+ import { PubSub } from "@encore.dev/sidecar-api";
21
+ import { DurationString } from "../internal/types/mod";
22
+ import { Acker } from "./acker";
23
+ import { Topic } from "./topic";
24
+
25
+ export class Subscription<Msg extends object> {
26
+ private readonly client_id: string;
27
+ private readonly topic: Topic<Msg>;
28
+ private readonly name: string;
29
+ private readonly handler: (msg: Msg, acker: Acker) => Promise<unknown>;
30
+ private readonly config: PubSub.SubscriptionConfig;
31
+ private readonly logger: Logger;
32
+ private failedState = false;
33
+ private activeDeliveries: string[] = [];
34
+
35
+ constructor(topic: Topic<Msg>, name: string, cfg: SubscriptionConfig<Msg>) {
36
+ // Generate a unique client ID for this instance
37
+ const id = new Uint8Array(16);
38
+ runtime().getRandomValues(id);
39
+ this.client_id = Buffer.from(id).toString("hex");
40
+
41
+ this.topic = topic;
42
+ this.name = name;
43
+ this.handler = cfg.handler;
44
+ this.logger = log.with({ topic: topic.name, subscription: name });
45
+
46
+ const ackDeadline = parseDuration(cfg.ackDeadline ?? "30s");
47
+ const minBackoff = parseDuration(cfg.retryPolicy?.minBackoff ?? "10s");
48
+ const maxBackoff = parseDuration(cfg.retryPolicy?.maxBackoff ?? "10m");
49
+
50
+ this.config = new PubSub.SubscriptionConfig({
51
+ maxConcurrency: cfg.maxConcurrency ?? -1,
52
+ ackDeadline: durationToProto(ackDeadline),
53
+ retryPolicy: new PubSub.RetryPolicy({
54
+ minBackoff: durationToProto(minBackoff),
55
+ maxBackoff: durationToProto(maxBackoff),
56
+ maxRetries: cfg.retryPolicy?.maxRetries ?? 100,
57
+ }),
58
+ });
59
+
60
+ let backoff = 250; // Start at 250ms for exponential backoff
61
+ const start = () => {
62
+ this.logger.trace("starting subscription");
63
+ this.#startSubscription()
64
+ .then(() => this.logger.warn("subscription ended"))
65
+ .catch((e: unknown) => {
66
+ this.failedState = true;
67
+ backoff *= 2;
68
+ if (backoff > 60 * 1000) {
69
+ // cap the backoff at 1 minute
70
+ backoff = 60 * 1000;
71
+ }
72
+
73
+ this.logger.error(e, "subscription failed with error");
74
+ setTimeout(start, backoff);
75
+ });
76
+ };
77
+
78
+ this.logger.info("registered subscription");
79
+
80
+ // Start the subscription
81
+ process.nextTick(start);
82
+ }
83
+
84
+ async #startSubscription() {
85
+ const resp = api().pubsub.subscribe(
86
+ new PubSub.SubscribeRequest({
87
+ clientId: this.client_id,
88
+ topic: this.topic.name,
89
+ subscription: this.name,
90
+ config: this.config,
91
+ }),
92
+ );
93
+
94
+ // Now we have an active subscription, we touch messages if we have active messages
95
+ const interval = setInterval(() => {
96
+ this.#touchActiveMessages();
97
+ }, 30 * 1000);
98
+
99
+ try {
100
+ for await (const msg of resp) {
101
+ if (this.failedState) {
102
+ this.logger.info("subscription recovered");
103
+ this.failedState = false;
104
+ }
105
+
106
+ process.nextTick(() => this.#handleMessage(msg));
107
+ }
108
+ } finally {
109
+ clearInterval(interval);
110
+ }
111
+ }
112
+
113
+ #touchActiveMessages() {
114
+ if (this.activeDeliveries.length === 0) {
115
+ return;
116
+ }
117
+ const acks = this.activeDeliveries;
118
+
119
+ api()
120
+ .pubsub.touch({
121
+ clientId: this.client_id,
122
+ ackIds: acks,
123
+ })
124
+ .then(() => {
125
+ this.logger.trace("touched active messages", { ack_ids: acks });
126
+ })
127
+ .catch((e: unknown) => {
128
+ this.logger.error(e, "failed to touch messages", { ack_ids: acks });
129
+ });
130
+ }
131
+
132
+ async #handleMessage(event: PubSub.SubscribeResponse) {
133
+ if (event.event.case !== "messageDelivery") {
134
+ // If it's a keep alive or some other unknown message
135
+ return;
136
+ }
137
+ const msg = event.event.value;
138
+
139
+ const logCtx: logCtx = {
140
+ topic: this.topic.name,
141
+ subscription: this.name,
142
+ msg_id: msg.msgId,
143
+ delivery_attempt: msg.deliveryAttempt,
144
+ };
145
+
146
+ const traceID = newTraceID();
147
+ const spanID = newSpanID();
148
+ const ctx: Context = {
149
+ traceID: traceID,
150
+ spanID: spanID,
151
+ };
152
+
153
+ // Record this ack ID as being active
154
+ this.activeDeliveries.push(msg.ackId);
155
+
156
+ return reqtrack.run(ctx, async () => {
157
+ return withLogCtx(logCtx, async () => {
158
+ const acker = new Acker(
159
+ () => this.#acknowledge(msg.ackId),
160
+ () => this.#negativeAcknowledge(msg.ackId),
161
+ );
162
+
163
+ try {
164
+ this.logger.info("starting request");
165
+ const data = Buffer.from(
166
+ msg.message?.data ?? Uint8Array.of(),
167
+ ).toString();
168
+
169
+ const start = now();
170
+ await this.handler(JSON.parse(data), acker);
171
+ this.logger.info("request completed", {
172
+ duration: toDurationStr(since(start)),
173
+ });
174
+
175
+ const hasAcked = await acker.currentResult();
176
+ if (!hasAcked) {
177
+ await acker.ack();
178
+ }
179
+ } catch (e: unknown) {
180
+ this.logger.error(e, "request failed");
181
+
182
+ const hasAcked = await acker.currentResult();
183
+ if (!hasAcked) {
184
+ await acker.nack();
185
+ }
186
+ }
187
+ });
188
+ });
189
+ }
190
+
191
+ async #acknowledge(ackID: string): Promise<void> {
192
+ await api().pubsub.acknowledge({
193
+ clientId: this.client_id,
194
+ ackId: ackID,
195
+ });
196
+ this.activeDeliveries = this.activeDeliveries.filter((id) => id !== ackID);
197
+ this.logger.trace("acknowledged message", { ack_id: ackID });
198
+ }
199
+
200
+ async #negativeAcknowledge(ackID: string): Promise<void> {
201
+ await api().pubsub.negativeAcknowledge({
202
+ clientId: this.client_id,
203
+ ackId: ackID,
204
+ });
205
+ this.activeDeliveries = this.activeDeliveries.filter((id) => id !== ackID);
206
+ this.logger.trace("negatively acknowledged message", { ack_id: ackID });
207
+ }
208
+ }
209
+
210
+ /**
211
+ * SubscriptionConfig is used when creating a subscription
212
+ *
213
+ * The values given here may be clamped to the supported values by
214
+ * the target cloud. (i.e. ack deadline may be brought within the supported range
215
+ * by the target cloud pubsub implementation).
216
+ */
217
+ export interface SubscriptionConfig<Msg> {
218
+ /**
219
+ * Handler is the function which will be called to process a message
220
+ * sent on the topic.
221
+ *
222
+ * When this function returns an error the message will be
223
+ * negatively acknowledged (nacked), which will cause a redelivery
224
+ * attempt to be made (unless the retry policy's MaxRetries has been reached).
225
+ */
226
+ handler: (msg: Msg) => Promise<unknown>;
227
+
228
+ /**
229
+ * MaxConcurrency is the maximum number of messages which will be processed
230
+ * simultaneously per instance of the service for this subscription.
231
+ *
232
+ * Note that this is per instance of the service, so if your service has
233
+ * scaled to 10 instances and this is set to 10, then 100 messages could be
234
+ * processed simultaneously.
235
+ *
236
+ * If the value is negative, then there will be no limit on the number
237
+ * of messages processed simultaneously.
238
+ *
239
+ * Note: This is not supported by all cloud providers; specifically on GCP
240
+ * when using Cloud Run instances on an unordered topic the subscription will
241
+ * be configured as a Push Subscription and will have an adaptive concurrency
242
+ * See [GCP Push Delivery Rate](https://cloud.google.com/pubsub/docs/push#push_delivery_rate).
243
+ *
244
+ * This setting also has no effect on Encore Cloud environments.
245
+ * If not set, it uses a reasonable default based on the cloud provider.
246
+ */
247
+ maxConcurrency?: number;
248
+
249
+ /**
250
+ * AckDeadline is the time a consumer has to process a message
251
+ * before it's returned to the subscription
252
+ *
253
+ * Default is 30 seconds, however the ack deadline must be at least
254
+ * 1 second.
255
+ */
256
+ ackDeadline?: DurationString;
257
+
258
+ /**
259
+ * MessageRetention is how long an undelivered message is kept
260
+ * on the topic before it's purged.
261
+ *
262
+ * Default is 7 days.
263
+ */
264
+ messageRetention?: DurationString;
265
+
266
+ /**
267
+ * RetryPolicy defines how a message should be retried when
268
+ * the subscriber returns an error
269
+ */
270
+ retryPolicy?: RetryPolicy;
271
+ }
272
+
273
+ /**
274
+ * RetryPolicy defines how a subscription should handle retries
275
+ * after errors either delivering the message or processing the message.
276
+ *
277
+ * The values given to this structure are parsed at compile time, such that
278
+ * the correct Cloud resources can be provisioned to support the queue.
279
+ *
280
+ * As such the values given here may be clamped to the supported values by
281
+ * the target cloud. (i.e. min/max values brought within the supported range
282
+ * by the target cloud).
283
+ */
284
+ export interface RetryPolicy {
285
+ /**
286
+ * The minimum time to wait between retries. Defaults to 10 seconds.
287
+ */
288
+ minBackoff?: DurationString;
289
+
290
+ /**
291
+ * The maximum time to wait between retries. Defaults to 10 minutes.
292
+ */
293
+ maxBackoff?: DurationString;
294
+
295
+ /**
296
+ * MaxRetries is used to control deadletter queuing logic, when:
297
+ * n == 0: A default value of 100 retries will be used
298
+ * n > 0: Encore will forward a message to a dead letter queue after n retries
299
+ * n == pubsub.InfiniteRetries: Messages will not be forwarded to the dead letter queue by the Encore framework
300
+ */
301
+ maxRetries?: number;
302
+ }
@@ -0,0 +1,132 @@
1
+ import { api } from "@encore.dev/internal-runtime";
2
+ import reqtrack from "@encore.dev/internal-runtime/reqtrack/index";
3
+ import { PubSub } from "@encore.dev/sidecar-api";
4
+ import { AttributesOf } from "./attributes";
5
+
6
+ /**
7
+ * A topic is a resource to which you can publish messages
8
+ * to be delivered to subscribers of that topic.
9
+ */
10
+ export class Topic<Msg extends object> {
11
+ public readonly name: string;
12
+ public readonly cfg: TopicConfig<Msg>;
13
+
14
+ constructor(name: string, cfg: TopicConfig<Msg>) {
15
+ this.name = name;
16
+ this.cfg = cfg;
17
+ }
18
+
19
+ public async publish(msg: Msg): Promise<string> {
20
+ const res = await api().pubsub.publish(
21
+ new PubSub.PublishRequest({
22
+ topic: this.name,
23
+ message: new PubSub.Message({
24
+ data: Buffer.from(JSON.stringify(msg)),
25
+ attributes: {},
26
+ traceParent: reqtrack.currentTraceParent(),
27
+ }),
28
+ }),
29
+ );
30
+
31
+ return res.id;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * DeliveryGuarantee is used to configure the delivery contract for a topic.
37
+ */
38
+ export type DeliveryGuarantee = "at-least-once" | "exactly-once";
39
+
40
+ /**
41
+ * At Least Once delivery guarantees that a message for a subscription is delivered to
42
+ * a consumer at least once.
43
+ *
44
+ * On AWS and GCP there is no limit to the throughput for a topic.
45
+ */
46
+ export const atLeastOnce: DeliveryGuarantee = "at-least-once";
47
+
48
+ /**
49
+ * ExactlyOnce guarantees that a message for a subscription is delivered to
50
+ * a consumer exactly once, to the best of the system's ability.
51
+ *
52
+ * However, there are edge cases when a message might be redelivered.
53
+ * For example, if a networking issue causes the acknowledgement of success
54
+ * processing the message to be lost before the cloud provider receives it.
55
+ *
56
+ * It is also important to note that the ExactlyOnce delivery guarantee only
57
+ * applies to the delivery of the message to the consumer, and not to the
58
+ * original publishing of the message, such that if a message is published twice,
59
+ * such as due to an retry within the application logic, it will be delivered twice.
60
+ * (i.e. ExactlyOnce delivery does not imply message deduplication on publish)
61
+ *
62
+ * As such it's recommended that the subscription handler function is idempotent
63
+ * and is able to handle duplicate messages.
64
+ *
65
+ * Subscriptions attached to ExactlyOnce topics have higher message delivery latency compared to AtLeastOnce.
66
+ *
67
+ * By using ExactlyOnce semantics on a topic, the throughput will be limited depending on the cloud provider:
68
+ * - AWS: 300 messages per second for the topic (see [AWS SQS Quotas]).
69
+ * - GCP: At least 3,000 messages per second across all topics in the region
70
+ * (can be higher on the region see [GCP PubSub Quotas]).
71
+ *
72
+ * [AWS SQS Quotas]: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html
73
+ * [GCP PubSub Quotas]: https://cloud.google.com/pubsub/quotas#quotas
74
+ */
75
+ export const exactlyOnce: DeliveryGuarantee = "exactly-once";
76
+
77
+ /**
78
+ * TopicConfig is used when creating a Topic
79
+ */
80
+ export interface TopicConfig<Msg extends object> {
81
+ /**
82
+ * DeliveryGuarantee is used to configure the delivery guarantee of a Topic
83
+ */
84
+ deliveryGuarantee: DeliveryGuarantee;
85
+
86
+ /**
87
+ * OrderingAttribute is the message attribute to use as a ordering key for
88
+ * messages and delivery will ensure that messages with the same value will
89
+ * be delivered in the order they where published.
90
+ *
91
+ * If OrderingAttribute is not set, messages can be delivered in any order.
92
+ *
93
+ * It is important to note, that in the case of an error being returned by a
94
+ * subscription handler, the message will be retried before any subsequent
95
+ * messages for that ordering key are delivered. This means depending on the
96
+ * retry configuration, a large backlog of messages for a given ordering key
97
+ * may build up. When using OrderingAttribute, it is recommended to use reason
98
+ * about your failure modes and set the retry configuration appropriately.
99
+ *
100
+ * Once the maximum number of retries has been reached, the message will be
101
+ * forwarded to the dead letter queue, and the next message for that ordering
102
+ * key will be delivered.
103
+ *
104
+ * To create attributes on a message, use the `Attribute` type:
105
+ *
106
+ * type UserEvent = {
107
+ * user_id Attribute<string>;
108
+ * action string;
109
+ * }
110
+ *
111
+ * const topic = new Topic<UserEvent>("user-events", {
112
+ * deliveryGuarantee: DeliveryGuarantee.AtLeastOnce,
113
+ * orderingAttribute: "user_id", // Messages with the same user-id will be delivered in the order they where
114
+ * published
115
+ * })
116
+ *
117
+ * topic.publish(ctx, {user_id: "1", action: "login"}) // This message will be delivered before the logout
118
+ * topic.publish(ctx, {user_id: "2", action: "login"}) // This could be delivered at any time because it has a different user id
119
+ * topic.publish(ctx, {user_id: "1", action: "logout"}) // This message will be delivered after the first message
120
+ *
121
+ * By using OrderingAttribute, the throughput will be limited depending on the cloud provider:
122
+ *
123
+ * - AWS: 300 messages per second for the topic (see [AWS SQS Quotas]).
124
+ * - GCP: 1MB/s for each ordering key (see [GCP PubSub Quotas]).
125
+ *
126
+ * Note: OrderingAttribute currently has no effect during local development.
127
+ *
128
+ * [AWS SQS Quotas]: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/quotas-messages.html
129
+ * [GCP PubSub Quotas]: https://cloud.google.com/pubsub/quotas#resource_limits
130
+ */
131
+ orderingAttribute?: AttributesOf<Msg>;
132
+ }
@@ -0,0 +1,105 @@
1
+ import { config } from "@encore.dev/internal-runtime";
2
+ import type { StringLiteral } from "@encore.dev/internal-runtime/utils/constraints";
3
+ import { queryWithComment } from "@encore.dev/internal-runtime/reqtrack/sql";
4
+ import type { Meta } from "@encore.dev/sidecar-api";
5
+ import pg from "pg";
6
+
7
+ export interface SQLDatabaseConfig {
8
+ migrations?: string;
9
+ }
10
+
11
+ const driverName = "node-pg";
12
+
13
+ /**
14
+ * Represents a single row from a query result
15
+ */
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ export type Row = Record<string, any>;
18
+
19
+ /** Represents a type that can be used in query template literals */
20
+ export type Primitive = string | number | boolean | null;
21
+
22
+ /**
23
+ * Constructing a new database object will result in Encore provisioning a database with
24
+ * that name and returning this object to represent it.
25
+ *
26
+ * If you want to reference an existing database, use `Database.Named(name)` as it is a
27
+ * compile error to create duplicate databases.
28
+ */
29
+ export class SQLDatabase {
30
+ private readonly runtimeCfg: Meta.SimpleConfig_Infrastructure_SQLDatabaseConfig;
31
+ private readonly pool: pg.Pool;
32
+
33
+ /**
34
+ * Creates a new database with the given name and configuration
35
+ */
36
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
37
+ constructor(name: string, cfg?: SQLDatabaseConfig) {
38
+ // Grab the runtime configuration for this database
39
+ const runtimeCfg = config().infrastructure?.sqlDatabases[name];
40
+ if (!runtimeCfg) {
41
+ throw new Error(`No database configuration found for ${name}`);
42
+ }
43
+ this.runtimeCfg = runtimeCfg;
44
+
45
+ this.pool = new pg.Pool({
46
+ connectionString: runtimeCfg.connectionString,
47
+ max: 20,
48
+ idleTimeoutMillis: 30000,
49
+ connectionTimeoutMillis: 2000,
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Reference an existing database by name, if the database doesn't
55
+ * exist yet, use `new Database(name)` instead.
56
+ */
57
+ static named<name extends string>(name: StringLiteral<name>): SQLDatabase {
58
+ return new SQLDatabase(name);
59
+ }
60
+
61
+ /**
62
+ * Returns the connection string for the database
63
+ */
64
+ get connectionString(): string {
65
+ return this.runtimeCfg.connectionString;
66
+ }
67
+
68
+ /**
69
+ * q allows you to query the database using a template string, replacing your placeholders in the template
70
+ * with parametrised values. It returns an async generator that you can iterate over to get the results, in a
71
+ * streaming fashion.
72
+ *
73
+ * @example
74
+ *
75
+ * const email = "foo@example.com";
76
+ * const result = database.q`SELECT id FROM users WHERE email=${email}` // produces a prepared query of
77
+ * // "SELECT id FROM users WHERE email=$1"
78
+ * // with an arg array of [email]
79
+ *
80
+ * for await (const row of result) {
81
+ * console.log(row.id);
82
+ * }
83
+ */
84
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85
+ async *q<T extends Row = Record<string, any>>(
86
+ strings: TemplateStringsArray,
87
+ ...expr: Primitive[]
88
+ ): AsyncGenerator<T> {
89
+ let query = "";
90
+ for (let i = 0; i < strings.length; i++) {
91
+ query += strings[i];
92
+
93
+ if (i < expr.length) {
94
+ query += "$" + (i + 1);
95
+ }
96
+ }
97
+
98
+ query = queryWithComment(query, driverName);
99
+
100
+ const result = await this.pool.query<T>(query, expr);
101
+ for (const row of result.rows) {
102
+ yield row;
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,2 @@
1
+ export { SQLDatabase } from "./database";
2
+ export type { SQLDatabaseConfig, Row as ResultRow } from "./database";