nongo-driver 3.3.17 → 3.3.19
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/dist/atlas-api.d.ts +4 -1
- package/dist/atlas-api.js +188 -21
- package/dist/atlas-api.js.map +1 -1
- package/dist/interface/atlas/index.d.ts +1 -0
- package/dist/model.js +9 -9
- package/dist/model.js.map +1 -1
- package/package.json +3 -3
- package/src/atlas-api.ts +244 -59
- package/src/interface/atlas/index.ts +1 -0
- package/src/model.ts +23 -19
- package/test/atlas-api.test.ts +12 -10
- package/test/database-helper.ts +3 -1
- package/test/model-versioning.test.ts +2 -2
- package/test/models/dummy-model-changed-obj.ts +12 -0
- package/test/models/dummy-model-obj.ts +40 -0
- package/test/models/dummy-model-with-max-index-obj.ts +195 -0
- package/test/models/dummy-model-with-revision-obj.ts +13 -0
- package/test/models/text-index-model-modified-obj.ts +12 -0
- package/test/models/text-index-model-obj.ts +11 -0
- package/test/models/text-index-model-too-long-obj.ts +11 -0
package/src/atlas-api.ts
CHANGED
|
@@ -1,120 +1,305 @@
|
|
|
1
1
|
import deepEqual from 'deep-equal';
|
|
2
|
-
import
|
|
2
|
+
import { request } from 'undici';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
3
4
|
import Logger from 'wbb-logger';
|
|
4
|
-
import {Analyzer, AtlasParams, AtlasSearchIndex, CreateSearchIndex} from './interface/atlas';
|
|
5
|
+
import { Analyzer, AtlasParams, AtlasSearchIndex, CreateSearchIndex } from './interface/atlas';
|
|
5
6
|
|
|
6
7
|
const logger = new Logger('atlas-api.ts');
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
10
|
+
|
|
11
|
+
function md5(input: string) {
|
|
12
|
+
return crypto.createHash('md5').update(input).digest('hex');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function randomHex(bytes = 16) {
|
|
16
|
+
return crypto.randomBytes(bytes).toString('hex');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseWwwAuthenticate(header: string) {
|
|
20
|
+
// Expects something like: Digest realm="...", nonce="...", qop="auth", opaque="...", algorithm=MD5
|
|
21
|
+
// We’ll parse key="value" pairs.
|
|
22
|
+
const schemeMatch = header.match(/^\s*Digest\s+/i);
|
|
23
|
+
if (!schemeMatch) return null;
|
|
24
|
+
|
|
25
|
+
const params: Record<string, string> = {};
|
|
26
|
+
const rest = header.replace(/^\s*Digest\s+/i, '');
|
|
27
|
+
|
|
28
|
+
// Split by commas not inside quotes
|
|
29
|
+
const parts = rest.match(/([a-z0-9_-]+)=(?:"[^"]*"|[^,]*)/gi) ?? [];
|
|
30
|
+
for (const part of parts) {
|
|
31
|
+
const idx = part.indexOf('=');
|
|
32
|
+
const k = part.slice(0, idx).trim();
|
|
33
|
+
let v = part.slice(idx + 1).trim();
|
|
34
|
+
if (v.startsWith('"') && v.endsWith('"')) v = v.slice(1, -1);
|
|
35
|
+
params[k] = v;
|
|
36
|
+
}
|
|
37
|
+
return params;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildDigestAuthHeader(opts: {
|
|
41
|
+
username: string;
|
|
42
|
+
password: string;
|
|
43
|
+
method: string;
|
|
44
|
+
uri: string; // path + query, not full URL
|
|
45
|
+
challenge: Record<string, string>;
|
|
46
|
+
nc: number;
|
|
47
|
+
cnonce: string;
|
|
48
|
+
}) {
|
|
49
|
+
const { username, password, method, uri, challenge, nc, cnonce } = opts;
|
|
50
|
+
|
|
51
|
+
const realm = challenge.realm ?? '';
|
|
52
|
+
const nonce = challenge.nonce ?? '';
|
|
53
|
+
const qopRaw = challenge.qop ?? '';
|
|
54
|
+
const opaque = challenge.opaque;
|
|
55
|
+
const algorithm = (challenge.algorithm ?? 'MD5').toUpperCase();
|
|
9
56
|
|
|
57
|
+
// We implement the common case Atlas uses: MD5 + qop=auth
|
|
58
|
+
// If qop has multiple values, pick auth if present.
|
|
59
|
+
const qop = qopRaw
|
|
60
|
+
.split(',')
|
|
61
|
+
.map((s) => s.trim())
|
|
62
|
+
.find((v) => v === 'auth') ?? (qopRaw ? qopRaw.split(',')[0].trim() : undefined);
|
|
63
|
+
|
|
64
|
+
if (algorithm !== 'MD5') {
|
|
65
|
+
// Atlas is typically MD5; if it ever changes, you’ll want to extend this.
|
|
66
|
+
throw new Error(`Unsupported digest algorithm: ${algorithm}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const ha1 = md5(`${username}:${realm}:${password}`);
|
|
70
|
+
const ha2 = md5(`${method}:${uri}`);
|
|
71
|
+
|
|
72
|
+
const ncHex = nc.toString(16).padStart(8, '0');
|
|
73
|
+
|
|
74
|
+
let response: string;
|
|
75
|
+
if (qop) {
|
|
76
|
+
response = md5(`${ha1}:${nonce}:${ncHex}:${cnonce}:${qop}:${ha2}`);
|
|
77
|
+
} else {
|
|
78
|
+
// RFC legacy
|
|
79
|
+
response = md5(`${ha1}:${nonce}:${ha2}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const pieces: string[] = [];
|
|
83
|
+
pieces.push(`username="${username}"`);
|
|
84
|
+
pieces.push(`realm="${realm}"`);
|
|
85
|
+
pieces.push(`nonce="${nonce}"`);
|
|
86
|
+
pieces.push(`uri="${uri}"`);
|
|
87
|
+
pieces.push(`response="${response}"`);
|
|
88
|
+
|
|
89
|
+
if (opaque) pieces.push(`opaque="${opaque}"`);
|
|
90
|
+
if (algorithm) pieces.push(`algorithm=${algorithm}`);
|
|
91
|
+
|
|
92
|
+
if (qop) {
|
|
93
|
+
pieces.push(`qop=${qop}`);
|
|
94
|
+
pieces.push(`nc=${ncHex}`);
|
|
95
|
+
pieces.push(`cnonce="${cnonce}"`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return `Digest ${pieces.join(', ')}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export default class AtlasApi {
|
|
10
102
|
private readonly ftsUrl: string;
|
|
11
|
-
|
|
103
|
+
|
|
104
|
+
// digest state
|
|
105
|
+
private nonceCountByNonce = new Map<string, number>();
|
|
106
|
+
private lastChallenge: Record<string, string> | null = null;
|
|
12
107
|
|
|
13
108
|
constructor(private params: AtlasParams) {
|
|
14
|
-
const {baseUrl, groupId, clusterName
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
109
|
+
const { baseUrl, groupId, clusterName } = params;
|
|
110
|
+
this.ftsUrl = `${baseUrl}/groups/${groupId}/clusters/${clusterName}/fts`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async digestJsonRequest<T>(method: HttpMethod, url: string, body?: unknown): Promise<T> {
|
|
114
|
+
const { publicKey, privateKey } = this.params;
|
|
115
|
+
|
|
116
|
+
const headers: Record<string, string> = {
|
|
117
|
+
accept: 'application/json',
|
|
23
118
|
};
|
|
24
119
|
|
|
25
|
-
|
|
120
|
+
let requestBody: string | undefined;
|
|
121
|
+
if (body !== undefined) {
|
|
122
|
+
headers['content-type'] = 'application/json';
|
|
123
|
+
requestBody = JSON.stringify(body);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 1) Try with cached digest challenge if we have one (avoids an extra 401 round-trip)
|
|
127
|
+
const tryOnce = async (authHeader?: string) => {
|
|
128
|
+
const h = authHeader ? { ...headers, authorization: authHeader } : headers;
|
|
129
|
+
|
|
130
|
+
const res = await request(url, { method, headers: h, body: requestBody });
|
|
131
|
+
const contentType = (res.headers['content-type'] ?? '').toString();
|
|
132
|
+
const raw = await res.body.text();
|
|
133
|
+
|
|
134
|
+
return { res, contentType, raw };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// If we already have a challenge cached, attempt auth immediately
|
|
138
|
+
if (this.lastChallenge?.nonce) {
|
|
139
|
+
const challenge = this.lastChallenge;
|
|
140
|
+
const nonce = challenge.nonce;
|
|
141
|
+
const nc = (this.nonceCountByNonce.get(nonce) ?? 0) + 1;
|
|
142
|
+
this.nonceCountByNonce.set(nonce, nc);
|
|
143
|
+
|
|
144
|
+
const u = new URL(url);
|
|
145
|
+
const uri = `${u.pathname}${u.search}`; // must be path+query for digest
|
|
146
|
+
|
|
147
|
+
const auth = buildDigestAuthHeader({
|
|
148
|
+
username: publicKey,
|
|
149
|
+
password: privateKey,
|
|
150
|
+
method,
|
|
151
|
+
uri,
|
|
152
|
+
challenge,
|
|
153
|
+
nc,
|
|
154
|
+
cnonce: randomHex(8),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const attempt = await tryOnce(auth);
|
|
158
|
+
if (attempt.res.statusCode !== 401) {
|
|
159
|
+
return this.handleResponse<T>(method, url, attempt.res.statusCode, attempt.contentType, attempt.raw);
|
|
160
|
+
}
|
|
161
|
+
// fall through and re-challenge below
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 2) No cached challenge (or cached failed). Make an unauthenticated request to get WWW-Authenticate
|
|
165
|
+
const first = await tryOnce(undefined);
|
|
166
|
+
|
|
167
|
+
if (first.res.statusCode !== 401) {
|
|
168
|
+
return this.handleResponse<T>(method, url, first.res.statusCode, first.contentType, first.raw);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const wwwAuth = first.res.headers['www-authenticate'];
|
|
172
|
+
const wwwAuthStr = Array.isArray(wwwAuth) ? wwwAuth.join(', ') : (wwwAuth ?? '').toString();
|
|
173
|
+
|
|
174
|
+
const challenge = parseWwwAuthenticate(wwwAuthStr);
|
|
175
|
+
if (!challenge?.nonce) {
|
|
176
|
+
const err = new Error(`401 from Atlas but no Digest challenge found in WWW-Authenticate header`);
|
|
177
|
+
(err as any).statusCode = 401;
|
|
178
|
+
(err as any).wwwAuthenticate = wwwAuthStr;
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// cache challenge for next requests
|
|
183
|
+
this.lastChallenge = challenge;
|
|
184
|
+
|
|
185
|
+
const nonce = challenge.nonce;
|
|
186
|
+
const nc = (this.nonceCountByNonce.get(nonce) ?? 0) + 1;
|
|
187
|
+
this.nonceCountByNonce.set(nonce, nc);
|
|
188
|
+
|
|
189
|
+
const u = new URL(url);
|
|
190
|
+
const uri = `${u.pathname}${u.search}`;
|
|
191
|
+
|
|
192
|
+
const auth = buildDigestAuthHeader({
|
|
193
|
+
username: publicKey,
|
|
194
|
+
password: privateKey,
|
|
195
|
+
method,
|
|
196
|
+
uri,
|
|
197
|
+
challenge,
|
|
198
|
+
nc,
|
|
199
|
+
cnonce: randomHex(8),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// 3) Retry with Digest Authorization
|
|
203
|
+
const second = await tryOnce(auth);
|
|
204
|
+
return this.handleResponse<T>(method, url, second.res.statusCode, second.contentType, second.raw);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private handleResponse<T>(
|
|
208
|
+
method: string,
|
|
209
|
+
url: string,
|
|
210
|
+
statusCode: number,
|
|
211
|
+
contentType: string,
|
|
212
|
+
raw: string,
|
|
213
|
+
): T {
|
|
214
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
215
|
+
let parsed: any = raw;
|
|
216
|
+
if (contentType.includes('application/json')) {
|
|
217
|
+
try {
|
|
218
|
+
parsed = raw ? JSON.parse(raw) : null;
|
|
219
|
+
} catch {
|
|
220
|
+
// keep raw
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const err = new Error(`Atlas API request failed: ${method} ${url} -> ${statusCode}`) as any;
|
|
225
|
+
err.statusCode = statusCode;
|
|
226
|
+
err.body = parsed;
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!raw) return undefined as unknown as T;
|
|
231
|
+
if (contentType.includes('application/json')) return JSON.parse(raw) as T;
|
|
232
|
+
return raw as unknown as T;
|
|
26
233
|
}
|
|
27
234
|
|
|
28
235
|
public async putCustomAnalyzers(analyzers: Analyzer[]): Promise<Analyzer[]> {
|
|
29
236
|
const url = `${this.ftsUrl}/analyzers`;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
url,
|
|
33
|
-
body: analyzers,
|
|
34
|
-
});
|
|
237
|
+
// Keep whatever your API expects. Many Atlas endpoints use PUT for replacement semantics.
|
|
238
|
+
return this.digestJsonRequest<Analyzer[]>('PUT', url, analyzers);
|
|
35
239
|
}
|
|
36
240
|
|
|
37
241
|
public async getSearchIndexes(db: string, collection: string): Promise<AtlasSearchIndex[]> {
|
|
38
242
|
const url = `${this.ftsUrl}/indexes/${db}/${collection}`;
|
|
39
|
-
return
|
|
40
|
-
...this.defaultOptions,
|
|
41
|
-
url,
|
|
42
|
-
});
|
|
243
|
+
return this.digestJsonRequest<AtlasSearchIndex[]>('GET', url);
|
|
43
244
|
}
|
|
44
245
|
|
|
45
246
|
public async createSearchIndex(db: string, collection: string, index: CreateSearchIndex): Promise<any> {
|
|
46
247
|
this.logIndexChange(db, collection, 'Creating', index.name);
|
|
47
248
|
const url = `${this.ftsUrl}/indexes`;
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
body: {
|
|
53
|
-
collectionName: collection,
|
|
54
|
-
database: db,
|
|
55
|
-
...index,
|
|
56
|
-
},
|
|
249
|
+
return this.digestJsonRequest<any>('POST', url, {
|
|
250
|
+
collectionName: collection,
|
|
251
|
+
database: db,
|
|
252
|
+
...index,
|
|
57
253
|
});
|
|
58
254
|
}
|
|
59
255
|
|
|
60
|
-
public async ensureSearchIndexes(db: string, collection: string, createParams: CreateSearchIndex[])
|
|
256
|
+
public async ensureSearchIndexes(db: string, collection: string, createParams: CreateSearchIndex[]) {
|
|
61
257
|
const existingIndexes = await this.getSearchIndexes(db, collection);
|
|
62
258
|
const toRemove = new Map<string, AtlasSearchIndex>();
|
|
63
259
|
existingIndexes.forEach((e) => toRemove.set(e.name, e));
|
|
64
260
|
|
|
65
261
|
for (const cParams of createParams) {
|
|
66
|
-
const
|
|
67
|
-
const existing = toRemove.get(name);
|
|
68
|
-
|
|
262
|
+
const existing = toRemove.get(cParams.name);
|
|
69
263
|
if (existing) {
|
|
70
|
-
toRemove.delete(name);
|
|
264
|
+
toRemove.delete(cParams.name);
|
|
71
265
|
await this.updateSearchIndex(db, collection, existing, cParams);
|
|
72
266
|
} else {
|
|
73
267
|
await this.createSearchIndex(db, collection, cParams);
|
|
74
268
|
}
|
|
75
|
-
|
|
76
269
|
}
|
|
77
270
|
|
|
78
271
|
for (const index of toRemove.values()) {
|
|
79
272
|
await this.deleteSearchIndex(db, collection, index);
|
|
80
273
|
}
|
|
81
274
|
|
|
82
|
-
return
|
|
275
|
+
return this.getSearchIndexes(db, collection);
|
|
83
276
|
}
|
|
84
277
|
|
|
85
|
-
public async updateSearchIndex(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
278
|
+
public async updateSearchIndex(
|
|
279
|
+
db: string,
|
|
280
|
+
collection: string,
|
|
281
|
+
existingIndex: AtlasSearchIndex,
|
|
282
|
+
createParams: CreateSearchIndex,
|
|
283
|
+
): Promise<any> {
|
|
284
|
+
if (deepEqual(existingIndex.mappings, createParams.mappings)) return;
|
|
89
285
|
|
|
90
286
|
this.logIndexChange(db, collection, 'Updating', createParams.name);
|
|
91
|
-
|
|
92
287
|
const url = `${this.ftsUrl}/indexes/${existingIndex.indexID}`;
|
|
93
288
|
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
body: {
|
|
99
|
-
collectionName: collection,
|
|
100
|
-
database: db,
|
|
101
|
-
...createParams,
|
|
102
|
-
},
|
|
289
|
+
return this.digestJsonRequest<any>('PATCH', url, {
|
|
290
|
+
collectionName: collection,
|
|
291
|
+
database: db,
|
|
292
|
+
...createParams,
|
|
103
293
|
});
|
|
104
294
|
}
|
|
105
295
|
|
|
106
296
|
public async deleteSearchIndex(db: string, collection: string, existingIndex: AtlasSearchIndex): Promise<any> {
|
|
107
297
|
this.logIndexChange(db, collection, 'Deleting', existingIndex.name);
|
|
108
298
|
const url = `${this.ftsUrl}/indexes/${existingIndex.indexID}`;
|
|
109
|
-
return
|
|
110
|
-
...this.defaultOptions,
|
|
111
|
-
method: 'DELETE',
|
|
112
|
-
url,
|
|
113
|
-
});
|
|
299
|
+
return this.digestJsonRequest<any>('DELETE', url);
|
|
114
300
|
}
|
|
115
301
|
|
|
116
302
|
private logIndexChange(db: string, collection: string, action: string, indexName: string) {
|
|
117
303
|
logger.info(`${action} search index "${indexName}" for collection ${db}.${collection}`);
|
|
118
304
|
}
|
|
119
|
-
|
|
120
305
|
}
|
package/src/model.ts
CHANGED
|
@@ -22,9 +22,9 @@ import {CreateSearchIndex} from './interface';
|
|
|
22
22
|
import MongoIndex from './mongo-index';
|
|
23
23
|
import Nongo from './nongo';
|
|
24
24
|
import Validator from './validator';
|
|
25
|
-
import {
|
|
25
|
+
import {ICache} from './cache';
|
|
26
26
|
|
|
27
|
-
import crypto from
|
|
27
|
+
import crypto from 'crypto';
|
|
28
28
|
|
|
29
29
|
const DEFAULT_PATHS = ['_id', 'revision', 'history', 'diffs'];
|
|
30
30
|
|
|
@@ -36,7 +36,6 @@ function excludePathsFromDiff(diff, excludedPaths = DEFAULT_PATHS) {
|
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
39
|
export default abstract class Model<
|
|
41
40
|
T extends {
|
|
42
41
|
_id?: ObjectId;
|
|
@@ -63,12 +62,16 @@ export default abstract class Model<
|
|
|
63
62
|
private collectionRevision: string | null;
|
|
64
63
|
private collectionEnvironmentMapping: string | null;
|
|
65
64
|
|
|
66
|
-
constructor(
|
|
65
|
+
constructor(
|
|
66
|
+
public nongo: Nongo,
|
|
67
|
+
public obj: T,
|
|
68
|
+
public cacheObject?: ICache,
|
|
69
|
+
) {
|
|
67
70
|
// Set this.db if nongo given
|
|
68
71
|
if (nongo) {
|
|
69
72
|
this.db = nongo.db;
|
|
70
73
|
}
|
|
71
|
-
if (
|
|
74
|
+
if (cacheObject) {
|
|
72
75
|
this.cache = cacheObject;
|
|
73
76
|
}
|
|
74
77
|
|
|
@@ -165,9 +168,8 @@ export default abstract class Model<
|
|
|
165
168
|
// Get id of newest revision, so we can use that to find the appropriate mapping
|
|
166
169
|
// this is in case the object we're trying to save either doesn't have _id (for example if it's being imported)
|
|
167
170
|
// or if the _id simply isn't correct for the environment
|
|
168
|
-
const {revision, newestRevisionId} =
|
|
169
|
-
revisionQuery
|
|
170
|
-
);
|
|
171
|
+
const {revision, newestRevisionId} =
|
|
172
|
+
await this.getLatestRevisionNumber(revisionQuery);
|
|
171
173
|
this.obj.previousRevision = this.obj.revision;
|
|
172
174
|
this.obj.revision = revision;
|
|
173
175
|
// needed to create a new environment mapping
|
|
@@ -207,7 +209,9 @@ export default abstract class Model<
|
|
|
207
209
|
.insertOne(this.obj);
|
|
208
210
|
insertedRevisionId = r.insertedId.toHexString();
|
|
209
211
|
}
|
|
210
|
-
if (this.obj.
|
|
212
|
+
if (this.obj.deleted) {
|
|
213
|
+
await this.removeEnvironmentMapping(environmentId, newestRevisionId, insertedRevisionId);
|
|
214
|
+
} else if (this.obj.revision !== 1) {
|
|
211
215
|
// update relevant mappings
|
|
212
216
|
await this.updateEnvironmentMapping(
|
|
213
217
|
insertedRevisionId,
|
|
@@ -215,8 +219,7 @@ export default abstract class Model<
|
|
|
215
219
|
environmentId,
|
|
216
220
|
clientEnv.isLive,
|
|
217
221
|
);
|
|
218
|
-
}
|
|
219
|
-
if (this.obj.revision === 1) {
|
|
222
|
+
} else if (this.obj.revision === 1) {
|
|
220
223
|
// latest revision id, as that's what we just inserted, and need to save as baseline
|
|
221
224
|
await this.createEnvironmentMapping(
|
|
222
225
|
environmentId,
|
|
@@ -224,9 +227,6 @@ export default abstract class Model<
|
|
|
224
227
|
clientEnv.isLive,
|
|
225
228
|
);
|
|
226
229
|
}
|
|
227
|
-
if (this.obj.deleted) {
|
|
228
|
-
await this.removeEnvironmentMapping(environmentId, newestRevisionId);
|
|
229
|
-
}
|
|
230
230
|
} else {
|
|
231
231
|
// Upsert non revision models
|
|
232
232
|
await this.db
|
|
@@ -234,7 +234,7 @@ export default abstract class Model<
|
|
|
234
234
|
.replaceOne({_id: this.obj._id}, this.obj, {upsert: true});
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
this.invalidateCache({
|
|
237
|
+
this.invalidateCache({_id: this.obj._id});
|
|
238
238
|
|
|
239
239
|
// Return the saved model
|
|
240
240
|
return this;
|
|
@@ -243,11 +243,15 @@ export default abstract class Model<
|
|
|
243
243
|
private async removeEnvironmentMapping(
|
|
244
244
|
environmentId: string,
|
|
245
245
|
latestRevisionId: string,
|
|
246
|
+
newRevisionId: string,
|
|
246
247
|
): Promise<void> {
|
|
247
248
|
const path = `environments.${environmentId}`;
|
|
248
249
|
await this.db
|
|
249
250
|
.collection(this.collectionEnvironmentMapping)
|
|
250
|
-
.updateOne(
|
|
251
|
+
.updateOne(
|
|
252
|
+
{latestRevisionId},
|
|
253
|
+
{$unset: {[path]: ''}, $set: {latestRevisionId: newRevisionId}},
|
|
254
|
+
);
|
|
251
255
|
}
|
|
252
256
|
|
|
253
257
|
private async createEnvironmentMapping(
|
|
@@ -491,9 +495,9 @@ export default abstract class Model<
|
|
|
491
495
|
);
|
|
492
496
|
}
|
|
493
497
|
|
|
494
|
-
private getCacheKey(query: Filter<Document>, operation: string
|
|
498
|
+
private getCacheKey(query: Filter<Document>, operation: string): string {
|
|
495
499
|
const q = JSON.stringify(query, Object.keys(query).sort()); // stable stringify
|
|
496
|
-
const hash = crypto.createHash(
|
|
500
|
+
const hash = crypto.createHash('md5').update(q).digest('hex'); // short key
|
|
497
501
|
return `${this.db.databaseName}:${this.collection}:${operation}:${hash}`;
|
|
498
502
|
}
|
|
499
503
|
|
|
@@ -530,7 +534,7 @@ export default abstract class Model<
|
|
|
530
534
|
public async findOne(query: Filter<Document> = {}): Promise<this> {
|
|
531
535
|
query = this.prepareQuery(query);
|
|
532
536
|
|
|
533
|
-
const cacheKey = this.getCacheKey(query,
|
|
537
|
+
const cacheKey = this.getCacheKey(query, 'findOne');
|
|
534
538
|
|
|
535
539
|
// Check cache
|
|
536
540
|
if (this.useCache && this.cache.has(cacheKey)) {
|
package/test/atlas-api.test.ts
CHANGED
|
@@ -4,21 +4,21 @@ import {AtlasParams, AtlasSearchIndex, CreateSearchIndex} from '../src/interface
|
|
|
4
4
|
import {setUpInterceptors} from './atlas-api-nock';
|
|
5
5
|
|
|
6
6
|
const groupId = '5b06b6b34e658110696b1da3';
|
|
7
|
-
const clusterName = 'wbb-
|
|
7
|
+
const clusterName = 'wbb-staging';
|
|
8
8
|
const collectionName = 'Entity';
|
|
9
|
-
const database = '
|
|
9
|
+
const database = 'afeltham';
|
|
10
10
|
|
|
11
11
|
const params: AtlasParams = {
|
|
12
12
|
groupId,
|
|
13
13
|
baseUrl: 'https://cloud.mongodb.com/api/atlas/v1.0',
|
|
14
|
-
publicKey: '
|
|
15
|
-
privateKey: '
|
|
14
|
+
publicKey: 'pfkjhdxk',
|
|
15
|
+
privateKey: '37bed739-c794-4dec-bd52-90fb4f1bd1cd',
|
|
16
16
|
clusterName,
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
it('Search index management', async () => {
|
|
20
20
|
|
|
21
|
-
setUpInterceptors();
|
|
21
|
+
// setUpInterceptors();
|
|
22
22
|
|
|
23
23
|
const createIndex = (name: string): CreateSearchIndex => ({
|
|
24
24
|
mappings: {dynamic: true},
|
|
@@ -35,18 +35,20 @@ it('Search index management', async () => {
|
|
|
35
35
|
{
|
|
36
36
|
collectionName,
|
|
37
37
|
database,
|
|
38
|
-
indexID: '
|
|
38
|
+
indexID: '6961209db2890435699787f5',
|
|
39
39
|
mappings: {dynamic: true},
|
|
40
40
|
name: 'index1',
|
|
41
|
-
status: '
|
|
41
|
+
status: 'STEADY',
|
|
42
|
+
"synonyms": [],
|
|
42
43
|
},
|
|
43
44
|
{
|
|
44
45
|
collectionName,
|
|
45
46
|
database,
|
|
46
|
-
indexID: '
|
|
47
|
+
indexID: '696120a1b289043569978f80',
|
|
47
48
|
mappings: {dynamic: true},
|
|
48
49
|
name: 'index2',
|
|
49
|
-
status: '
|
|
50
|
+
status: 'STEADY',
|
|
51
|
+
"synonyms": [],
|
|
50
52
|
},
|
|
51
53
|
];
|
|
52
54
|
expect(firstEnsure).toStrictEqual(firstExpected);
|
|
@@ -59,4 +61,4 @@ it('Search index management', async () => {
|
|
|
59
61
|
// Instead just make sure all the interceptors were used
|
|
60
62
|
expect(nock.isDone());
|
|
61
63
|
|
|
62
|
-
});
|
|
64
|
+
}, 10000);
|
package/test/database-helper.ts
CHANGED
|
@@ -11,7 +11,9 @@ let mongoMemoryServer: MongoMemoryServer;
|
|
|
11
11
|
const getInMemoryServer = async () => {
|
|
12
12
|
if (!mongoMemoryServer) {
|
|
13
13
|
logger.info("Using in memory server.");
|
|
14
|
-
mongoMemoryServer = await MongoMemoryServer.create(
|
|
14
|
+
mongoMemoryServer = await MongoMemoryServer.create({
|
|
15
|
+
binary: {version: "6.0.6"},
|
|
16
|
+
});
|
|
15
17
|
}
|
|
16
18
|
return mongoMemoryServer;
|
|
17
19
|
};
|
|
@@ -69,8 +69,8 @@ describe('Testing nongo revisioning', () => {
|
|
|
69
69
|
expect(models.length).toEqual(2);
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it('Should revert to revision
|
|
73
|
-
const revision = await nongo.col(DummyModelWithRevision).getRevision({name: "John", revision:
|
|
72
|
+
it('Should revert to revision 1', async () => {
|
|
73
|
+
const revision = await nongo.col(DummyModelWithRevision).getRevision({name: "John", revision: 1});
|
|
74
74
|
expect(revision.obj.revision).toEqual("0.0.1");
|
|
75
75
|
const model = await nongo.col(DummyModelWithRevision).findOne({name: 'Nick'});
|
|
76
76
|
const latestDoc = await nongo.col(DummyModelWithRevision).revertVersion(model.obj._id);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// This file was generated using nongo-driver's TsInterfaceGenerator.
|
|
2
|
+
export default interface DummyModelChangedObj {
|
|
3
|
+
'name': string;
|
|
4
|
+
'pets': Array<{
|
|
5
|
+
'species'?: string;
|
|
6
|
+
'likes'?: {
|
|
7
|
+
'food'?: string[];
|
|
8
|
+
'drink'?: string[];
|
|
9
|
+
};
|
|
10
|
+
}>;
|
|
11
|
+
'_id'?: any;
|
|
12
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// This file was generated using nongo-driver's TsInterfaceGenerator.
|
|
2
|
+
export default interface DummyModelObj {
|
|
3
|
+
'dontStripChildren'?: any;
|
|
4
|
+
'dontStripChildren2'?: any;
|
|
5
|
+
'exampleMap'?: {[key: string]: {
|
|
6
|
+
'mapValueField': string;
|
|
7
|
+
}};
|
|
8
|
+
'created'?: Date;
|
|
9
|
+
'arrayOfObject'?: object[];
|
|
10
|
+
'name': string;
|
|
11
|
+
'age': number;
|
|
12
|
+
'pets': Array<{
|
|
13
|
+
'species'?: string;
|
|
14
|
+
'likes'?: {
|
|
15
|
+
'food'?: string[];
|
|
16
|
+
'drink'?: string[];
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
19
|
+
'job': {
|
|
20
|
+
'role': string;
|
|
21
|
+
'at'?: string;
|
|
22
|
+
};
|
|
23
|
+
'location'?: {
|
|
24
|
+
'address1'?: string;
|
|
25
|
+
};
|
|
26
|
+
'autoInit': {
|
|
27
|
+
'initArray': Array<{
|
|
28
|
+
'nestedObj': any;
|
|
29
|
+
}>;
|
|
30
|
+
'initNestedObj'?: {
|
|
31
|
+
'hello'?: string;
|
|
32
|
+
};
|
|
33
|
+
'initNestedNative'?: any;
|
|
34
|
+
};
|
|
35
|
+
'notEmptyFields'?: {
|
|
36
|
+
'aString': string;
|
|
37
|
+
'anArray': string[];
|
|
38
|
+
};
|
|
39
|
+
'_id'?: any;
|
|
40
|
+
}
|