Skip to content
This repository was archived by the owner on Oct 31, 2024. It is now read-only.

Commit 40fc110

Browse files
committed
feat: Adds custom fetch support at db/col level
1 parent d01cc46 commit 40fc110

File tree

6 files changed

+131
-89
lines changed

6 files changed

+131
-89
lines changed

CHANGELOG.md

+12-5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ All notable changes to this project will be documented in this file. This projec
1414

1515
### Removed
1616

17+
## [0.4.0]
18+
19+
### Added
20+
21+
- Custom fetch: For `db()` and `collection()`, there is now a chainable `fetch()` method, which returns a new DB or collection with its `fetch` implementation changed. This allows you to implement your own custom logic for retries, without the addition of pRetry
22+
23+
### Removed
24+
25+
- Removed pRetry dependency in favor of users overriding via their own fetch function
26+
1727
## [0.3.0]
1828

1929
### Breaking Changes
@@ -40,10 +50,6 @@ const createMongoClient = async () => {
4050

4151
- Return type on `insertOne` is now of type `ObjectId` instead of `string`
4252

43-
### Changed
44-
45-
### Removed
46-
4753
## [0.2.2]
4854

4955
### Fixed
@@ -115,7 +121,8 @@ Older releases are available via github releases: https://github.com/taskless/mo
115121

116122
<!-- Releases -->
117123

118-
[unreleased]: https://github.com/taskless/mongo-data-api/compare/0.3.0...HEAD
124+
[unreleased]: https://github.com/taskless/mongo-data-api/compare/0.4.0...HEAD
125+
[0.4.0]: https://github.com/taskless/mongo-data-api/compare/0.3.0...0.4.0
119126
[0.3.0]: https://github.com/taskless/mongo-data-api/compare/0.2.2...0.3.0
120127
[0.2.2]: https://github.com/taskless/mongo-data-api/compare/0.2.1...0.2.2
121128
[0.2.1]: https://github.com/taskless/mongo-data-api/compare/0.2.0...0.2.1

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
- [Create a Mongo Client](#create-a-mongo-client)
1616
- [Select a Database](#select-a-database)
1717
- [Select a Collection](#select-a-collection)
18+
- [Changing `fetch()` on the Fly](#changing-fetch-on-the-fly)
1819
- [Collection Methods](#collection-methods)
1920
- [Return Type](#return-type)
2021
- [Methods](#methods)
@@ -148,6 +149,15 @@ const collection = db.collection<TSchema>(collectionName);
148149
- `collectionName` - `string` the name of the collection to connect to
149150
- `<TSchema>` - _generic_ A Type or Interface that describes the documents in this collection. Defaults to the generic MongoDB `Document` type
150151

152+
## Changing `fetch()` on the Fly
153+
154+
```ts
155+
const altDb = db.fetch(altFetch);
156+
const altCollection = db.collection<TSchema>(collectionName).fetch(altFetch);
157+
```
158+
159+
- `altFetch` - An alternate `fetch` implementation that will be used for that point forward. Useful for adding or removing retry support on a per-call level, changing the authentication required, or other fetch middleware operations.
160+
151161
## Collection Methods
152162

153163
The following [Data API resources](https://www.mongodb.com/docs/atlas/api/data-api-resources/) are supported
@@ -302,6 +312,7 @@ Requests via `fetch()` have their resposne codes checked against the [Data API E
302312

303313
- **Why is `mongodb` in the dependencies?** [TypeScript requires it](https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html#dependencies), however, the mongodb dependency is types-only and will not be included in your built lambda when using `tsc`, `rollup`, `webpack`, etc. You can verify that mongo is not included by looking at the [CommonJS build](https://www.npmjs.com/package/@taskless/mongo-data-api?activeTab=code).
304314
- **Why is `node-fetch`'s `fetch` not of the correct type?** `node-fetch`'s `fetch` isn't a true `fetch` and wasn't typed as one. To work around this, you can either use [`cross-fetch`](https://github.com/lquixada/cross-fetch) which types the `fetch` API through a type assertion, or [perform the type assertion yourself](https://github.com/lquixada/cross-fetch/blob/main/index.d.ts): `fetch: _fetch as typeof fetch`. It's not ideal, but with proper `fetch` coming to node.js, it's a small inconvienence in the short term.
315+
- **How do I retry failed `fetch` calls?** `fetch-retry` ([github](https://github.com/jonbern/fetch-retry)) is an excellent library. You can also use a lower level retry tool like `p-retry` ([github](https://github.com/sindresorhus/p-retry)) if you want to manage more than just the `fetch()` operation itself.
305316

306317
# License
307318

package.json

-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
"dependencies": {
5252
"bson": "^6.2.0",
5353
"mongodb": "^6.2.0",
54-
"p-retry": "^6.1.0",
5554
"tslib": "^2.6.2"
5655
},
5756
"devDependencies": {

pnpm-lock.yaml

+1-21
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/client.ts

+106-62
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type {
99
WithoutId,
1010
Document,
1111
} from "mongodb";
12-
import pRetry from "p-retry";
1312
import type { AuthOptions } from "./authTypes.js";
1413
import { MongoDataAPIError } from "./errors.js";
1514

@@ -67,6 +66,20 @@ export type MongoClientConstructorOptions = {
6766
headers?: OutgoingHttpHeaders | Headers;
6867
};
6968

69+
type ConnectionOptions = {
70+
dataSource: string;
71+
endpoint: string;
72+
headers: Record<string, string>;
73+
};
74+
75+
type ConnectionOptionsWithDatabase = ConnectionOptions & {
76+
database: string;
77+
};
78+
79+
type ConnectionOptionsWithCollection = ConnectionOptionsWithDatabase & {
80+
collection: string;
81+
};
82+
7083
/**
7184
* Removes empty keys from an object at the top level. EJSON.stringify does not drop
7285
* undefined values in serialization, so we need to explicitly remove any keys with
@@ -88,61 +101,61 @@ const removeEmptyKeys = (object: Record<string, unknown>) => {
88101
* requests, converting mongo-style BSON to JSON and back again.
89102
*/
90103
export class MongoClient {
91-
dataSource: string;
92-
endpoint: string;
93-
94-
fetch: typeof fetch;
95-
headers: HeadersInit;
104+
protected connection: ConnectionOptions;
105+
protected ftch: typeof fetch;
96106

97107
constructor({
98108
auth,
99109
dataSource,
100110
endpoint,
101111
fetch: customFetch,
102-
headers,
112+
headers: h,
103113
}: MongoClientConstructorOptions) {
104-
this.dataSource = dataSource;
105-
this.endpoint = endpoint instanceof URL ? endpoint.toString() : endpoint;
106-
107-
this.fetch = customFetch ?? globalThis.fetch;
108-
this.headers = {};
114+
this.ftch = customFetch ?? globalThis.fetch;
115+
const headers: HeadersInit = {};
109116

110117
// accept a node-style or whatwg headers object with .keys() and .get()
111-
if (typeof headers?.keys === "function") {
112-
for (const key of headers.keys()) {
113-
this.headers[key] = (
114-
typeof headers.get === "function" ? headers.get(key) : headers[key]
115-
) as string;
118+
if (typeof h?.keys === "function") {
119+
for (const key of h.keys()) {
120+
headers[key] = (
121+
typeof h.get === "function" ? h.get(key) : headers[key]
122+
)!;
116123
}
117124
}
118125

119-
if (!this.fetch || typeof this.fetch !== "function") {
126+
this.connection = {
127+
dataSource,
128+
endpoint: endpoint instanceof URL ? endpoint.toString() : endpoint,
129+
headers,
130+
};
131+
132+
if (!this.ftch || typeof this.ftch !== "function") {
120133
throw new Error(
121134
"No viable fetch() found. Please provide a fetch interface"
122135
);
123136
}
124137

125-
this.headers["Content-Type"] = "application/ejson";
126-
this.headers.Accept = "application/ejson";
138+
this.connection.headers["Content-Type"] = "application/ejson";
139+
this.connection.headers.Accept = "application/ejson";
127140

128141
if ("apiKey" in auth) {
129-
this.headers.apiKey = auth.apiKey;
142+
this.connection.headers.apiKey = auth.apiKey;
130143
return;
131144
}
132145

133146
if ("jwtTokenString" in auth) {
134-
this.headers.jwtTokenString = auth.jwtTokenString;
147+
this.connection.headers.jwtTokenString = auth.jwtTokenString;
135148
return;
136149
}
137150

138151
if ("email" in auth && "password" in auth) {
139-
this.headers.email = auth.email;
140-
this.headers.password = auth.password;
152+
this.connection.headers.email = auth.email;
153+
this.connection.headers.password = auth.password;
141154
return;
142155
}
143156

144157
if ("bearerToken" in auth) {
145-
this.headers.Authorization = `Bearer ${auth.bearerToken}`;
158+
this.connection.headers.Authorization = `Bearer ${auth.bearerToken}`;
146159
return;
147160
}
148161

@@ -151,7 +164,7 @@ export class MongoClient {
151164

152165
/** Select a database from within the data source */
153166
db(name: string) {
154-
return new Database(name, this);
167+
return new Database(name, this.connection, this.ftch);
155168
}
156169
}
157170

@@ -161,12 +174,19 @@ export class MongoClient {
161174
* queries.
162175
*/
163176
export class Database {
164-
name: string;
165-
client: MongoClient;
166-
167-
constructor(name: string, client: MongoClient) {
168-
this.name = name;
169-
this.client = client;
177+
protected connection: ConnectionOptionsWithDatabase;
178+
protected ftch: typeof fetch;
179+
180+
constructor(
181+
name: string,
182+
connection: ConnectionOptions,
183+
customFetch?: typeof fetch
184+
) {
185+
this.ftch = customFetch ?? globalThis.fetch;
186+
this.connection = {
187+
...connection,
188+
database: name,
189+
};
170190
}
171191

172192
/**
@@ -176,7 +196,21 @@ export class Database {
176196
* @returns A Collection object of type `T`
177197
*/
178198
collection<TSchema = Document>(name: string) {
179-
return new Collection<TSchema>(name, this);
199+
return new Collection<TSchema>(name, this.connection, this.ftch);
200+
}
201+
202+
/**
203+
* Change the fetch interface for this database. Returns a new Database object
204+
* @param customFetch A fetch interface to use for subsequent calls
205+
* @returns A new Database object with the custom fetch interface implemented
206+
*/
207+
fetch(customFetch: typeof fetch) {
208+
const db = new Database(
209+
this.connection.database,
210+
this.connection,
211+
customFetch
212+
);
213+
return db;
180214
}
181215
}
182216

@@ -185,14 +219,33 @@ export class Database {
185219
* methods in a fluent interface.
186220
*/
187221
export class Collection<TSchema = Document> {
188-
name: string;
189-
database: Database;
190-
client: MongoClient;
191-
192-
constructor(name: string, database: Database) {
193-
this.name = name;
194-
this.database = database;
195-
this.client = database.client;
222+
protected connection: ConnectionOptionsWithCollection;
223+
protected ftch: typeof fetch;
224+
225+
constructor(
226+
name: string,
227+
database: ConnectionOptionsWithDatabase,
228+
customFetch?: typeof fetch
229+
) {
230+
this.ftch = customFetch ?? globalThis.fetch;
231+
this.connection = {
232+
...database,
233+
collection: name,
234+
};
235+
}
236+
237+
/**
238+
* Change the fetch interface for this collection. Returns a new Collection object
239+
* @param customFetch A fetch interface to use for subsequent calls
240+
* @returns A new Collection object with the custom fetch interface implemented
241+
*/
242+
fetch(customFetch: typeof fetch) {
243+
const collection = new Collection(
244+
this.connection.collection,
245+
this.connection,
246+
customFetch
247+
);
248+
return collection;
196249
}
197250

198251
/**
@@ -433,29 +486,20 @@ export class Collection<TSchema = Document> {
433486
method: string,
434487
body: Record<string, unknown>
435488
): Promise<DataAPIResponse<T>> {
436-
const { endpoint, dataSource, headers } = this.client;
489+
const { endpoint, dataSource, headers, collection, database } =
490+
this.connection;
437491
const url = `${endpoint}/action/${method}`;
438492

439-
const response = await pRetry(
440-
async () => {
441-
const r = await this.client.fetch(url, {
442-
method: "POST",
443-
headers,
444-
body: EJSON.stringify({
445-
collection: this.name,
446-
database: this.database.name,
447-
dataSource,
448-
...removeEmptyKeys(body),
449-
}),
450-
});
451-
452-
return r;
453-
},
454-
{
455-
minTimeout: 10,
456-
maxTimeout: 1000,
457-
}
458-
);
493+
const response = await this.ftch(url, {
494+
method: "POST",
495+
headers,
496+
body: EJSON.stringify({
497+
collection,
498+
database,
499+
dataSource,
500+
...removeEmptyKeys(body),
501+
}),
502+
});
459503

460504
// interpret response code. Log error for anything outside of 2xx 3xx
461505
// https://www.mongodb.com/docs/atlas/api/data-api-resources/#error-codes

test/requests.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const createMongoClient = (headers?: Headers) => {
4141
fetch,
4242
headers,
4343
});
44+
4445
return c;
4546
};
4647

0 commit comments

Comments
 (0)