react-native-mosquito-transport 0.0.19 → 0.0.21

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.
@@ -1,317 +1,814 @@
1
- import { IS_RAW_OBJECT, objToUniqueString, queryEntries, shuffleArray, sortArrayByObjectKey } from "../../helpers/peripherals";
1
+ import { niceHash, shuffleArray, sortArrayByObjectKey } from "../../helpers/peripherals";
2
2
  import { awaitStore, updateCacheStore } from "../../helpers/utils";
3
- import { CacheStore } from "../../helpers/variables";
4
- import { confirmFilterDoc } from "./validator";
3
+ import { CacheStore, Scoped } from "../../helpers/variables";
4
+ import { assignExtractionFind, CompareBson, confirmFilterDoc, defaultBSON, downcastBSON, validateCollectionName, validateFilter } from "./validator";
5
5
  import getLodash from 'lodash.get';
6
6
  import setLodash from 'lodash.set';
7
7
  import unsetLodash from 'lodash.unset';
8
- import isEqual from 'lodash.isequal';
9
- import { DEFAULT_DB_NAME, DEFAULT_DB_URL, DELIVERY, RETRIEVAL, WRITE_OPS, WRITE_OPS_LIST } from "../../helpers/values";
10
8
  import { DatabaseRecordsListener } from "../../helpers/listeners";
9
+ import cloneDeep from "lodash.clonedeep";
10
+ import { BSONRegExp, ObjectId, Timestamp } from "bson";
11
+ import { niceGuard, Validator } from "guard-object";
12
+ import { TIMESTAMP } from "../..";
13
+ import { decrementDatabaseSize, incrementDatabaseSize } from "./counter";
14
+ import { serializeToBase64 } from "./bson";
11
15
 
12
16
  export const listenQueryEntry = (callback, { accessId, builder, config, processId }) => {
13
- let lastObj = '';
17
+ const { projectUrl, dbName, dbUrl, path } = builder;
14
18
  const { episode = 0 } = config || {};
15
19
 
16
- const l = DatabaseRecordsListener.listenTo(accessId, async (dispatchId) => {
17
- const cache = await getRecord(builder, accessId);
18
- if (
19
- cache &&
20
- !isEqual(lastObj, cache[episode]) &&
21
- dispatchId === processId
22
- ) callback(cache[episode]);
23
- lastObj = cache[episode];
20
+ const nodeID = `${projectUrl}${dbName}${dbUrl}${path}`;
21
+
22
+ if (!Scoped.ActiveDatabaseListeners[nodeID])
23
+ Scoped.ActiveDatabaseListeners[nodeID] = {};
24
+ Scoped.ActiveDatabaseListeners[nodeID][processId] = Date.now();
25
+
26
+ const listener = DatabaseRecordsListener.listenTo('d', async (dispatchId) => {
27
+ if (dispatchId !== processId) return;
28
+ const cache = await getRecord(builder, config, accessId);
29
+ if (cache) callback(cache[episode]);
24
30
  });
25
31
 
26
32
  return () => {
27
- lastObj = undefined;
28
- l();
29
- }
30
- }
33
+ listener();
34
+ if (Scoped.ActiveDatabaseListeners[nodeID]?.[processId]) {
35
+ delete Scoped.ActiveDatabaseListeners[nodeID][processId];
36
+
37
+ if (!Object.keys(Scoped.ActiveDatabaseListeners[nodeID]).length)
38
+ delete Scoped.ActiveDatabaseListeners[nodeID];
39
+ }
40
+ };
41
+ };
42
+
43
+ export const insertRecord = async (builder, config, accessId, value) => {
44
+ builder = builder && cloneDeep(builder);
45
+ config = config && cloneDeep(config);
46
+ value = value && cloneDeep(value);
31
47
 
32
- export const insertRecord = async (builder, accessId, query, value) => {
33
48
  await awaitStore();
34
- const { projectUrl, dbUrl = DEFAULT_DB_URL, dbName = DEFAULT_DB_NAME, path } = builder,
35
- { extraction, excludeFields, returnOnly } = query?.config,
36
- kaf = `${objToUniqueString(extraction || {})},${(excludeFields || []).join(',')},${(returnOnly || []).join(',')}`,
37
- colData = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data', kaf], []);
38
-
39
- // TODO:
40
- (Array.isArray(value) ? value : [value]).forEach(e => {
41
- const b4DocIndex = colData.findIndex(v => v?._id === e?._id);
49
+ const { projectUrl, dbUrl, dbName, path, command } = builder;
50
+ const entityId = await generateRecordID({}, config);
51
+ const colData = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data', entityId], { config, command, listing: [] });
52
+ const trackedData = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'record', accessId]);
53
+
54
+ const newList = value ? Array.isArray(value) ? value : [value] : [];
55
+
56
+ const { tracks, ignore, registeredOn } = trackedData || {};
57
+ const trackedList = [...tracks || []];
58
+ const ignoreList = [...ignore || []];
59
+
60
+ const addSet = (arr, _id) => {
61
+ const dex = arr.findIndex(v => CompareBson.equal(v, _id));
62
+ if (dex === -1) arr.push(_id);
63
+ }
64
+
65
+ const deleteSet = (arr, _id) => {
66
+ const dex = arr.findIndex(v => CompareBson.equal(v, _id));
67
+ if (dex !== -1) arr.splice(dex, 1);
68
+ }
69
+
70
+ newList.forEach(e => {
71
+ const b4DocIndex = colData.listing.findIndex(v => CompareBson.equal(v._id, e._id));
42
72
  if (b4DocIndex === -1) {
43
- colData.push(e);
44
- } else colData[b4DocIndex] = e;
73
+ colData.listing.push(e);
74
+ incrementDatabaseSize(projectUrl, e);
75
+ } else {
76
+ decrementDatabaseSize(projectUrl, colData.listing[b4DocIndex]);
77
+ incrementDatabaseSize(projectUrl, e);
78
+ colData.listing[b4DocIndex] = e;
79
+ }
80
+ addSet(trackedList, e._id);
81
+ });
82
+
83
+ (tracks || []).forEach(e => {
84
+ if (newList.findIndex(v => CompareBson.equal(v._id, e)) === -1) {
85
+ if (colData.listing.findIndex(v => CompareBson.equal(v._id, e)) === -1) {
86
+ deleteSet(trackedList, e);
87
+ deleteSet(ignoreList, e);
88
+ } else addSet(ignoreList, e);
89
+ } else deleteSet(ignoreList, e);
45
90
  });
46
91
 
47
- setLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data', kaf], [...colData]);
92
+ setLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data', entityId], colData);
48
93
  setLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'record', accessId], {
49
- query,
94
+ command,
50
95
  result: value,
51
- registeredOn: Date.now()
96
+ tracks: [...trackedList],
97
+ ignore: [...ignoreList],
98
+ registeredOn: registeredOn || Date.now(),
99
+ updatedOn: Date.now()
52
100
  });
53
101
  updateCacheStore();
54
- }
102
+ };
55
103
 
56
- export const getRecord = async (builder, accessId) => {
104
+ export const getRecord = async (builder, config, accessId) => {
57
105
  await awaitStore();
58
- const { projectUrl, dbUrl = DEFAULT_DB_URL, dbName = DEFAULT_DB_NAME, path, command } = builder,
59
- { config, find, findOne, sort, direction, limit, random } = command,
60
- { extraction, excludeFields, returnOnly } = config || {},
61
- kaf = `${objToUniqueString(extraction || {})},${(excludeFields || []).join(',')},${(returnOnly || []).join(',')}`,
62
- colData = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data', kaf], []),
63
- colRecord = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'record', accessId]);
106
+ const { projectUrl, dbUrl, dbName, path, command } = builder;
107
+ const { find, findOne, sort, direction, limit, random } = command;
108
+ const entityId = await generateRecordID({}, config);
109
+ const colData = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data', entityId]);
110
+ const colRecord = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'record', accessId]);
64
111
 
65
112
  if (!colRecord) return null;
66
- let choosenColData = colData.filter(v => confirmFilterDoc(v, findOne || find || {}));
113
+ let choosenColData = colData.listing.filter(v =>
114
+ !colRecord.ignore.includes(v._id) &&
115
+ confirmFilterDoc(v, findOne || find || {})
116
+ );
67
117
 
68
118
  if (random) {
69
119
  choosenColData = shuffleArray(choosenColData);
70
120
  } else if (sort) {
71
- choosenColData = sortArrayByObjectKey(choosenColData, sort);
121
+ sortArrayByObjectKey(choosenColData, sort);
72
122
  if (
73
123
  direction === -1 ||
74
124
  direction === 'desc' ||
75
125
  direction === 'descending'
76
- ) choosenColData = choosenColData.reverse();
126
+ ) choosenColData.reverse();
77
127
  }
78
128
 
79
129
  if (findOne) {
80
130
  choosenColData = choosenColData[0];
81
- } else if (limit) choosenColData.filter((_, i) => i < limit);
131
+ } else if (limit) choosenColData.slice(0, limit);
82
132
 
83
133
  return [choosenColData, colRecord.result];
84
- }
134
+ };
85
135
 
86
136
  export const generateRecordID = (builder, config) => {
87
- const { command, path, countDoc } = builder,
88
- { find, findOne, sort, direction, limit } = command || {},
89
- { extraction, retrieval = RETRIEVAL.DEFAULT, delivery = DELIVERY.DEFAULT, excludeFields = [], returnOnly = [] } = config || {},
90
- accessId = `collection:${path}->excludes:${(Array.isArray(excludeFields) ? excludeFields : [excludeFields]).filter(v => v !== undefined).join(',')}->includes:${(Array.isArray(returnOnly) ? returnOnly : [returnOnly]).filter(v => v !== undefined).join(',')}->${countDoc ? 'countDoc:yes->' : ''}sort:${sort || ''}->direction:${direction || ''}->limit:${limit || ''}->${findOne ? 'findOne' : 'find'}:${objToUniqueString(findOne || find || {})}:extraction:${objToUniqueString(extraction || {})}:retrieval:${retrieval}:delivery:${delivery}`;
137
+ builder = builder && cloneDeep(builder);
138
+ config = config && cloneDeep(config);
139
+
140
+ const { command, path, countDoc } = builder;
141
+ const { extraction, excludeFields, returnOnly } = config || {};
142
+
143
+ const recordObj = Object.fromEntries(
144
+ Object.entries({
145
+ path,
146
+ command,
147
+ countDoc,
148
+ extraction,
149
+ excludeFields,
150
+ returnOnly
151
+ }).filter(([_, v]) => v !== undefined)
152
+ );
153
+
154
+ if (command) recordObj.command = arrangeCommands(command);
155
+ if (extraction) {
156
+ if (Array.isArray(extraction)) recordObj.extraction = extraction.map(arrangeCommands);
157
+ else recordObj.extraction = arrangeCommands(extraction);
158
+ }
91
159
 
92
- return accessId;
93
- }
160
+ return niceHash(serializeToBase64(recordObj));
161
+ };
162
+
163
+ const arrangeCommands = c => {
164
+ c = cloneDeep(c);
165
+ const sortFind = f => {
166
+ ['$and', '$or', '$nor'].forEach(n => {
167
+ if (f[n]) {
168
+ f[n] = f[n].map(v => sortObject(v));
169
+ }
170
+ });
171
+
172
+ return sortObject(f);
173
+ };
174
+ if (c.sort) c.direction = [-1, 'desc', 'descending'].includes(c.direction) ? 'desc' : 'asc';
175
+ if (c.find) c.find = sortFind(c.find);
176
+ if (c.findOne) c.findOne = sortFind(c.findOne);
177
+ return sortObject(c);
178
+ };
179
+
180
+ const sortObject = (o) => Object.fromEntries(
181
+ Object.entries(o).sort((a, b) => (a > b) ? 1 : (a < b) ? -1 : 0)
182
+ );
183
+
184
+ const recursiveFlat = (a) => {
185
+ return a.map(v => Array.isArray(v) ? recursiveFlat(v) : v).flat();
186
+ };
187
+
188
+ const recurseNonAtomicWrite = (obj, i, type) => {
189
+ if (!Validator.OBJECT(obj)) throw `expected a document but got ${obj}`;
190
+ Object.entries(obj).forEach(([k, v]) => {
191
+ if (!i) {
192
+ if (k === '_id') throw `avoid providing "_id" for ${type}() operation as _id only reference a single document`;
193
+ if (k === '_foreign_doc') throw '"_foreign_doc" is readonly';
194
+ }
195
+ if (k.includes('$') || k.includes('.')) {
196
+ if (!(k === '$timestamp' && v === 'now'))
197
+ throw `invalid property "${k}", ${type}() operation fields must not contain .$`;
198
+ }
199
+ if (Validator.OBJECT(v)) recurseNonAtomicWrite(v, i + 1, type);
200
+ });
201
+ };
202
+
203
+ const recurseAtomicWrite = (obj, i, type) => {
204
+ if (!Validator.OBJECT(obj)) throw `expected a document but got ${obj}`;
205
+ Object.entries(obj).forEach(([k, v]) => {
206
+ if (!i && !(k in AtomicWriter)) throw `Unknown update operator: ${k}`;
207
+ if (i === 1) {
208
+ if ((k === '_id' || k.startsWith('_id.')))
209
+ throw `avoid providing "_id" for ${type}() operation as _id only reference a single document`;
210
+
211
+ if (k === '_foreign_doc' || k.startsWith('_foreign_doc.'))
212
+ throw '"_foreign_doc" is readonly';
213
+ }
214
+ if (k.includes('.$')) throw `unsupported operation at "${k}"`;
215
+ if (!i || Validator.OBJECT(v)) recurseAtomicWrite(v, i + 1, type);
216
+ });
217
+ };
218
+
219
+ const WriteValidator = {
220
+ setOne: ({ value, type = 'setOne' }) => {
221
+ if (!Validator.OBJECT(value)) throw `expected a document but got ${value}`;
222
+ const { _id, ...rest } = value;
223
+
224
+ if (_id === undefined || JSON.stringify(_id) === 'null')
225
+ throw `_id requires a valid bson value but got ${_id}`;
226
+
227
+ recurseNonAtomicWrite(rest, 0, type);
228
+ },
229
+ setMany: ({ value }) => {
230
+ value.forEach(v => {
231
+ WriteValidator.setOne({ value: v, type: 'setMany' });
232
+ });
233
+ },
234
+ replaceOne: ({ find, value }) => {
235
+ validateFilter(find);
236
+ recurseNonAtomicWrite(value, 0, 'replaceOne');
237
+ },
238
+ putOne: ({ find, value }) => {
239
+ validateFilter(find);
240
+ recurseNonAtomicWrite(value, 0, 'putOne');
241
+ },
242
+ updateOne: ({ find, value }) => {
243
+ validateFilter(find);
244
+ recurseAtomicWrite(value, 0, 'updateOne');
245
+ },
246
+ updateMany: ({ find, value }) => {
247
+ validateFilter(find);
248
+ recurseAtomicWrite(value, 0, 'updateMany');
249
+ },
250
+ mergeOne: ({ find, value }) => {
251
+ validateFilter(find);
252
+ recurseAtomicWrite(value, 0, 'mergeOne');
253
+ },
254
+ mergeMany: ({ find, value }) => {
255
+ validateFilter(find);
256
+ recurseAtomicWrite(value, 0, 'mergeMany');
257
+ },
258
+ deleteOne: ({ find }) => {
259
+ validateFilter(find);
260
+ },
261
+ deleteMany: ({ find }) => {
262
+ validateFilter(find);
263
+ }
264
+ };
265
+
266
+ export const validateWriteValue = ({ type, find, value }) => WriteValidator[type]({ find, value, type });
94
267
 
95
268
  export const addPendingWrites = async (builder, writeId, result) => {
269
+ builder = builder && cloneDeep(builder);
270
+ result = result && cloneDeep(result);
96
271
  await awaitStore();
97
- const { projectUrl, dbUrl = DEFAULT_DB_URL, dbName = DEFAULT_DB_NAME, path } = builder,
98
- { value: writeObj, find, type } = result,
99
- isAtomic = type === 'updateOne' ||
100
- type === 'updateMany' ||
101
- type === 'mergeOne' ||
102
- type === 'mergeMany',
103
- colObj = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data'], {});
104
-
105
- let editions = [], duplicateSets = {};
106
-
107
- Object.entries(colObj).forEach(([kaf, colList]) => {
108
- let hasEndCommit, editionSet = [];
109
-
110
- if (type === 'setOne' || type === 'setMany') {
111
- (type === 'setOne' ? [writeObj] : writeObj).forEach(e => {
112
- if (colList.findIndex(v => v._id === e._id) === -1) {
113
- editionSet.push({ doc: deserializeNonAtomicWrite(e), dex: 'push', docId: writeObj._id });
114
- } else if (!duplicateSets[e._id])
115
- console.error(`document with _id=${e._id} already exist locally with ${type}() operation, will try committing it online`);
116
- duplicateSets[e._id] = true;
117
- });
118
- } else {
119
- colList.forEach((doc, docDex) => {
120
- if (hasEndCommit) return;
121
- let afDoc = undefined;
122
-
123
- if (confirmFilterDoc(doc, find || {})) {
124
- if (type === 'deleteMany') {
125
- afDoc = null;
126
- } else if (type === 'deleteOne') {
127
- afDoc = null;
128
- hasEndCommit = true;
129
- } else if (isAtomic) {
130
- if ((deserializeAtomicWrite({}, { ...writeObj })?._id || find?._id) && type.endsWith('Many'))
131
- throw `avoid providing "_id" for ${type}() operation, use ${type.substring(0, type.length - 4)}One instead as _id only reference a single document`;
132
-
133
- afDoc = deserializeAtomicWrite({ ...doc }, { ...writeObj });
134
- if (type.endsWith('One')) hasEndCommit = true;
135
- } else {
136
- afDoc = deserializeNonAtomicWrite({ ...writeObj });
137
- hasEndCommit = true;
138
- }
272
+
273
+ const { projectUrl, dbUrl, dbName } = builder;
274
+ const editions = [];
275
+ const duplicateSets = {};
276
+ const pathChanges = new Set([]);
277
+
278
+ (
279
+ result.type === 'batchWrite' ?
280
+ result.value.map(({ scope, value, find, path }) =>
281
+ ({ type: scope, value, find, path })
282
+ )
283
+ : [{ ...result, path: builder.path }]
284
+ ).forEach(({ value: writeObj, find, type, path }) => {
285
+ WriteValidator[type]({ find, value: writeObj });
286
+ validateCollectionName(path);
287
+ pathChanges.add(path);
288
+ const colObj = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data'], {});
289
+
290
+ Object.entries(colObj).forEach(([entityId, { listing, config }]) => {
291
+ const { extraction } = config || {};
292
+
293
+ const logChanges = (d) => {
294
+ editions.push([entityId, d, path]);
295
+ };
296
+
297
+ const accessExtraction = obj => {
298
+ const buildAssignedExtraction = (data) => {
299
+ const d = (Array.isArray(extraction) ? extraction : [extraction]).map(thisExtraction => {
300
+ const query = cloneDeep(thisExtraction);
301
+
302
+ ['find', 'findOne'].forEach(n => {
303
+ if (query[n])
304
+ query[n] = assignExtractionFind(data, query[n]);
305
+ });
306
+ return arrangeCommands(query);
307
+ });
308
+ if (Array.isArray(extraction)) return d;
309
+ return d[0];
139
310
  }
140
- if (afDoc !== undefined)
141
- editionSet.push({ doc: afDoc, dex: docDex, docId: doc._id, b4Doc: { ...doc } });
142
- });
143
- }
311
+ const extractionResultant = buildAssignedExtraction(obj);
312
+ const extractionBinary = serializeToBase64({ _: extractionResultant });
313
+
314
+ const sameProjection = listing.find(({ _foreign_doc, ...restDoc }) =>
315
+ extractionBinary === serializeToBase64({ _: buildAssignedExtraction(restDoc) })
316
+ );
144
317
 
145
- if (!editionSet.length) {
146
- let hasNoID;
318
+ if (sameProjection) return sameProjection._foreign_doc;
147
319
 
148
- if (type === 'putOne') {
149
- const nDoc = deserializeNonAtomicWrite(writeObj),
150
- nId = nDoc?._id || find?._id;
320
+ // if no matching extraction was found, proceed to scrapping other paths
321
+ const scrapedProjection = (Array.isArray(extractionResultant) ? extractionResultant : [extractionResultant]).map((query, i) => {
322
+ const { sort, direction, limit, find, findOne, collection: path } = query;
323
+ const scrapDocs = [];
151
324
 
152
- if (nId) {
153
- editionSet.push({
154
- doc: { ...nDoc, _id: nId },
155
- dex: 'push',
156
- docId: nId
325
+ listing.forEach(({ _foreign_doc }) => {
326
+ _foreign_doc = (Array.isArray(_foreign_doc) ? _foreign_doc : [_foreign_doc])[i];
327
+
328
+ recursiveFlat([_foreign_doc]).forEach(e => {
329
+ if (e && confirmFilterDoc(e, find || findOne)) {
330
+ scrapDocs.push(e);
331
+ }
332
+ });
157
333
  });
158
- } else hasNoID = true;
159
- } else if (type === 'mergeOne' || type === 'mergeMany') {
160
- const nDoc = deserializeAtomicWrite({}, writeObj),
161
- nId = nDoc?._id || find?._id;
162
-
163
- if (nId && type === 'mergeMany')
164
- throw `avoid providing "_id" for mergeMany() operation, use mergeOne instead as _id only reference a single document`;
165
- if (nId) {
166
- editionSet.push({
167
- doc: { ...nDoc, _id: nId },
168
- dex: 'push',
169
- docId: nId
334
+ if (!scrapDocs.length) {
335
+ const scrapYard = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data'], {});
336
+ Object.values(scrapYard).forEach(v => {
337
+ v.listing.forEach(doc => {
338
+ if (confirmFilterDoc(doc, find || findOne)) {
339
+ scrapDocs.push(snipDocument(doc, find || findOne, config));
340
+ }
341
+ });
342
+ });
343
+ }
344
+ if (sort) sortArrayByObjectKey(scrapDocs, sort);
345
+ if ([-1, 'desc', 'descending'].includes(direction)) scrapDocs.reverse();
346
+ if (limit) scrapDocs.splice(limit);
347
+
348
+ return findOne ? scrapDocs[0] : scrapDocs;
349
+ });
350
+
351
+ return Array.isArray(extraction) ? scrapedProjection : scrapedProjection[0];
352
+ }
353
+
354
+ if (['setOne', 'setMany'].includes(type)) {
355
+ (type === 'setOne' ? [writeObj] : writeObj).forEach(e => {
356
+ if (listing.findIndex(v => CompareBson.equal(v._id, e._id)) === -1) {
357
+ const obj = deserializeNonAtomicWrite(e);
358
+
359
+ if (extraction) obj._foreign_doc = accessExtraction(obj);
360
+ listing.push(obj);
361
+ logChanges([undefined, obj]);
362
+ } else if (!duplicateSets[e._id])
363
+ console.warn(`document with _id=${e._id} already exist locally with ${type}() operation, skipping to online commit`);
364
+ duplicateSets[e._id] = true;
365
+ });
366
+ return;
367
+ }
368
+
369
+ if (['putOne', 'replaceOne'].includes(type)) {
370
+ const extras = createWriteFromFind(find);
371
+
372
+ for (let i = 0; i < listing.length; i++) {
373
+ const doc = listing[i];
374
+ if (confirmFilterDoc(doc, find)) {
375
+ const obj = deserializeNonAtomicWrite({
376
+ ...extras,
377
+ ...writeObj,
378
+ ...'_id' in extras ? {} : { _id: doc._id }
379
+ });
380
+
381
+ if (extraction) obj._foreign_doc = accessExtraction(obj);
382
+ listing[i] = obj;
383
+ logChanges([doc, obj]);
384
+ return;
385
+ }
386
+ }
387
+ if (type === 'putOne') {
388
+ const obj = deserializeNonAtomicWrite({
389
+ ...extras,
390
+ ...writeObj,
391
+ ...'_id' in extras ? {} : { _id: new ObjectId() }
170
392
  });
171
- } else hasNoID = true;
393
+
394
+ if (extraction) obj._foreign_doc = accessExtraction(obj);
395
+ listing.push(obj);
396
+ logChanges([undefined, obj]);
397
+ }
398
+ return;
399
+ }
400
+
401
+ if (['deleteOne', 'deleteMany'].includes(type)) {
402
+ let deletions = 0;
403
+
404
+ for (let i = 0; i < listing.length; i++) {
405
+ const dex = deletions + i;
406
+ const doc = listing[dex];
407
+ if (confirmFilterDoc(doc, find)) {
408
+ listing.splice(dex, 1);
409
+ logChanges([doc]);
410
+ --deletions;
411
+ if (type === 'deleteOne') return;
412
+ }
413
+ }
414
+ return;
172
415
  }
173
- if (hasNoID) console.error(`no data found locally and _id was not provided for ${type}() operation, skipping local and proceeding to online commit`);
174
- }
175
- editions.push([kaf, editionSet]);
176
- });
177
416
 
178
- editions.forEach(([kaf, list]) => {
179
- list.forEach(({ doc, dex, docId }) => {
417
+ let founded;
418
+ for (let i = 0; i < listing.length; i++) {
419
+ const doc = listing[i];
420
+ if (confirmFilterDoc(doc, find)) {
421
+ const obj = deserializeAtomicWrite(doc, deserializeWriteValue(writeObj), false, type);
180
422
 
181
- if (dex === 'push') {
182
- colObj[kaf].push({ ...doc });
183
- } else if (doc === null) {
184
- colObj[kaf] = colObj[kaf].filter(v => v._id !== docId);
185
- } else {
186
- colObj[kaf] = colObj[kaf].map(v => v._id === docId ? { ...doc } : v);
423
+ if (extraction) obj._foreign_doc = accessExtraction(obj);
424
+ listing[i] = obj;
425
+ logChanges([doc, obj]);
426
+
427
+ founded = true;
428
+ if (type.endsWith('One')) return;
429
+ }
430
+ }
431
+
432
+ if (!founded && type.startsWith('merge')) {
433
+ const extras = createWriteFromFind(find);
434
+ const obj = {
435
+ ...extras,
436
+ ...deserializeAtomicWrite(
437
+ { _id: '_id' in extras ? extras._id : new ObjectId() },
438
+ deserializeWriteValue(writeObj),
439
+ true,
440
+ type
441
+ )
442
+ };
443
+
444
+ if (extraction) obj._foreign_doc = accessExtraction(obj);
445
+ listing.push(obj);
446
+ logChanges([undefined, obj]);
187
447
  }
188
448
  });
189
449
  });
190
450
 
191
-
192
- setLodash(CacheStore.PendingWrites, [projectUrl, `${dbUrl}${dbName}${path}`, writeId], {
193
- find,
194
- value: writeObj,
195
- type,
451
+ setLodash(CacheStore.PendingWrites, [projectUrl, writeId], cloneDeep({
452
+ builder,
453
+ snapshot: result,
196
454
  editions,
197
455
  addedOn: Date.now()
198
- });
456
+ }));
199
457
 
200
458
  updateCacheStore();
201
- notifyDatabaseNodeChanges(builder);
202
- }
459
+ notifyDatabaseNodeChanges(builder, [...pathChanges]);
460
+ };
203
461
 
204
462
  export const removePendingWrite = async (builder, writeId, revert) => {
205
463
  await awaitStore();
206
- const { projectUrl, dbUrl = DEFAULT_DB_URL, dbName = DEFAULT_DB_NAME, path } = builder,
207
- pObj = getLodash(CacheStore.PendingWrites, [projectUrl, `${dbUrl}${dbName}${path}`, writeId]),
208
- colObj = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data']);
209
-
210
- if (!pObj) return;
211
-
212
- if (revert && colObj)
213
- pObj.editions.forEach(([kaf, list]) => {
214
- list.forEach(({ doc, dex, docId, b4Doc }) => {
215
-
216
- if (dex === 'push') {
217
- colObj[kaf] = colObj[kaf].filter(v => v._id !== docId);
218
- } else if (doc === null) {
219
- colObj[kaf] = [...colObj[kaf], b4Doc];
220
- } else {
221
- colObj[kaf] = colObj[kaf].map(v => v._id === docId ? { ...b4Doc } : v);
464
+ const { projectUrl, dbUrl, dbName } = builder;
465
+ const pendingData = getLodash(CacheStore.PendingWrites, [projectUrl, writeId]);
466
+
467
+ if (!pendingData) return;
468
+ const pathChanges = new Set([]);
469
+
470
+ if (revert) {
471
+ pendingData.editions.forEach(([entityId, [b4Doc, afDoc], path]) => {
472
+ const colObj = getLodash(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'data']);
473
+ const colList = colObj?.[entityId]?.listing;
474
+
475
+ if (colList) {
476
+ if (afDoc) {
477
+ const editedIndex = colList.findIndex(e => CompareBson.equal(e._id, afDoc._id));
478
+ if (editedIndex !== -1) {
479
+ if (
480
+ serializeToBase64(afDoc) === serializeToBase64(colList[editedIndex])
481
+ ) {
482
+ if (b4Doc) {
483
+ colList[editedIndex] = b4Doc;
484
+ } else colList.splice(editedIndex, 1);
485
+ }
486
+ }
487
+ } else if (
488
+ b4Doc &&
489
+ colList.findIndex(e => CompareBson.equal(e._id, b4Doc._id)) === -1
490
+ ) {
491
+ colList.push(b4Doc);
222
492
  }
223
- });
493
+ }
494
+ pathChanges.add(path);
224
495
  });
496
+ }
225
497
 
226
- unsetLodash(CacheStore.PendingWrites, [projectUrl, `${dbUrl}${dbName}${path}`, writeId]);
498
+ unsetLodash(CacheStore.PendingWrites, [projectUrl, writeId]);
227
499
  updateCacheStore();
228
- notifyDatabaseNodeChanges(builder);
229
- }
500
+ notifyDatabaseNodeChanges(builder, [...pathChanges]);
501
+ };
502
+
503
+ const notifyDatabaseNodeChanges = (builder, changedCollections = []) => {
504
+ const { projectUrl, dbName, dbUrl } = builder;
505
+
506
+ changedCollections.forEach(path => {
507
+ const nodeID = `${projectUrl}${dbName}${dbUrl}${path}`;
508
+ Object.entries(Scoped.ActiveDatabaseListeners[nodeID] || {})
509
+ .sort((a, b) => a[1] - b[1])
510
+ .forEach(([processId]) => {
511
+ DatabaseRecordsListener.dispatch('d', processId);
512
+ });
513
+ });
514
+ };
230
515
 
231
- export const trySendPendingWrite = () => {
516
+ const createWriteFromFind = (find) => {
517
+ let result = {};
232
518
 
233
- }
519
+ Object.entries(find).forEach(([k, v]) => {
520
+ if (['$and', '$or'].includes(k)) {
521
+ v.forEach(e => {
522
+ result = { ...result, ...createWriteFromFind(e) };
523
+ });
524
+ } else if (!k.startsWith('$')) {
525
+ if (Validator.OBJECT(v)) {
526
+ if (!Object.keys(v).some(v => v.startsWith('$'))) {
527
+ result[k] = v;
528
+ } else if ('$eq' in v) {
529
+ result[k] = v.$eq;
530
+ }
531
+ } else {
532
+ result[k] = v instanceof RegExp ? new BSONRegExp(v.source, v.flags) : v;
533
+ }
534
+ }
535
+ });
234
536
 
235
- const notifyDatabaseNodeChanges = (builder) => {
537
+ return result;
538
+ };
236
539
 
237
- }
540
+ const snipDocument = (data, find, config) => {
541
+ if (!data || !config) return data;
542
+ const { returnOnly, excludeFields } = config || {};
238
543
 
239
- const deserializeNonAtomicWrite = (writeObj) => {
240
- const bj = {};
544
+ let output = { ...data };
241
545
 
242
- queryEntries(writeObj, []).forEach(([segment, value]) => {
243
- if (segment[0].startsWith('$'))
244
- throw `unexpected field "${segment[0]}"`;
546
+ if (returnOnly) {
547
+ output = {};
548
+ (Array.isArray(returnOnly) ? returnOnly : [returnOnly]).filter(v => v).forEach(e => {
549
+ const thisData = getLodash(data, e);
550
+ if (thisData) setLodash(output, e, thisData);
551
+ });
552
+ } else if (excludeFields) {
553
+ (Array.isArray(excludeFields) ? excludeFields : [excludeFields]).filter(v => v).forEach(e => {
554
+ if (getLodash(data, e) && e !== '_id') unsetLodash(output, e);
555
+ });
556
+ }
245
557
 
246
- if (segment.slice(-1)[0] === '$timestamp' && value === 'now') {
247
- segment.pop();
248
- value = Date.now();
558
+ getFindFields(find).forEach(field => {
559
+ if (!getLodash(output, field)) {
560
+ const mainData = getLodash(data, field);
561
+ if (mainData !== undefined) setLodash(output, field, mainData);
249
562
  }
563
+ });
250
564
 
251
- setLodash(bj, segment.join('.'), value);
565
+ return output;
566
+ };
567
+
568
+ const getFindFields = (find) => {
569
+ const result = ['_id'];
570
+
571
+ Object.entries(find).forEach(([k, v]) => {
572
+ if (['$and', '$or', '$nor'].includes(k)) {
573
+ v.forEach(e => {
574
+ result.push(...getFindFields(e));
575
+ });
576
+ } else if (k === '$text') {
577
+ result.push(...Array.isArray(v.$field) ? v.$field : [v.$field]);
578
+ } else if (!k.startsWith('$')) {
579
+ result.push(k);
580
+ }
252
581
  });
253
- return bj;
582
+
583
+ return result.filter((v, i, a) => a.findIndex(b => b === v) === i);
584
+ };
585
+
586
+ const deserializeWriteValue = (value) => {
587
+ if (!value) return value;
588
+
589
+ if (niceGuard(TIMESTAMP, value)) {
590
+ return Date.now();
591
+ } else if (Validator.OBJECT(value)) {
592
+ return Object.fromEntries(
593
+ Object.entries(value).map(([k, v]) =>
594
+ Validator.JSON(v) ? [k, deserializeWriteValue(v)] : [k, v]
595
+ )
596
+ );
597
+ } else if (Array.isArray(value)) {
598
+ return value.map(deserializeWriteValue);
599
+ } else return value;
254
600
  }
255
601
 
256
- const deserializeAtomicWrite = (b4Doc, writeObj) => {
257
- const afDoc = { ...b4Doc },
258
- affectedObj = {};
602
+ const deserializeNonAtomicWrite = (writeObj) => deserializeWriteValue(writeObj);
603
+
604
+
605
+ const deserializeAtomicWrite = (b4Doc, writeObj, isNew, type) => {
606
+ const resultantDoc = { ...b4Doc };
607
+
608
+ Object.entries(writeObj).forEach(([key, value]) => {
609
+ if (key in AtomicWriter) {
610
+ if (Validator.OBJECT(value)) {
611
+ Object.entries(value).forEach(([k, v]) => {
612
+ AtomicWriter[key](k, v, resultantDoc, isNew, type);
613
+ });
614
+ } else throw `expected an object at ${key} but got ${value}`;
615
+ } else if (key.startsWith('$')) {
616
+ throw `Unknown update operator: ${key}`;
617
+ } else throw 'MongoInvalidArgumentError: Update document requires atomic operators';
618
+ });
259
619
 
260
- queryEntries(writeObj, []).forEach(([segment, value]) => {
261
- const [op, path] = [segment[0], segment.filter((_, i) => i)];
620
+ return resultantDoc;
621
+ };
262
622
 
263
- if (!WRITE_OPS_LIST.includes(op) || !path.length)
264
- throw `MongoInvalidArgumentError: Update document requires atomic operators`;
623
+ const AtomicWriter = {
624
+ $currentDate: (field, value, object) => {
625
+ const isDate = value === true || niceGuard({ $type: "date" }, value);
626
+ const isTimestamp = niceGuard({ $type: "timestamp" }, value);
265
627
 
266
628
  if (
267
- path.length > 1 &&
268
- IS_RAW_OBJECT(writeObj[op][path[0]]) &&
269
- !affectedObj[path[0]]
270
- ) {
271
- affectedObj[path[0]] = true;
272
- afDoc[path[0]] = {};
629
+ !isDate &&
630
+ !isTimestamp
631
+ ) throw `invalid value at $currentDate.${field}, expected any of boolean (true), { $type: "timestamp" } or { $type: "date" } but got ${value}`;
632
+ setLodash(object, field, isDate ? new Date() : new Timestamp({ t: Math.floor(Date.now() / 1000), i: 0 }));
633
+ },
634
+ $inc: (field, value, object) => {
635
+ const current = getLodash(object, field);
636
+ if (current === null) {
637
+ console.warn(`cannot use $inc operator on a null value at ${field}`);
638
+ return;
273
639
  }
640
+ const castedCurrent = downcastBSON(current);
641
+ const castedValue = downcastBSON(value);
274
642
 
275
- const nodeValue = getLodash(b4Doc, path.join('.'));
643
+ if (!Validator.NUMBER(castedValue)) throw `expected a number at $inc.${field} but got ${value}`;
276
644
 
277
- if (op === WRITE_OPS.$UNSET) {
278
- unsetLodash(afDoc, path.join('.'));
279
- } else {
280
- if (
281
- [WRITE_OPS.$MAX, WRITE_OPS.$MIN, WRITE_OPS.$INC, WRITE_OPS.$MUL].filter(v => v === op).length &&
282
- isNaN(value)
283
- ) throw `expected a number for "${op}" operation but got ${value}`;
284
-
285
- if (path.slice(-1)[0] === '$timestamp' && value === 'now') {
286
- const k = [WRITE_OPS.$SET, WRITE_OPS.$UNSET];
287
- if (!k.includes(op))
288
- throw `invalid operator for updating timestamp, expected any of ${k}`;
289
- path.pop();
290
- value = Date.now();
645
+ setLodash(object, field, Validator.NUMBER(castedCurrent) ? defaultBSON(castedCurrent + castedValue, current) : value);
646
+ },
647
+ $min: (field, value, object) => {
648
+ const current = getLodash(object, field);
649
+ if (CompareBson.lesser(value, current)) {
650
+ setLodash(object, field, value);
651
+ }
652
+ },
653
+ $max: (field, value, object) => {
654
+ const current = getLodash(object, field);
655
+ if (CompareBson.greater(value, current)) {
656
+ setLodash(object, field, value);
657
+ }
658
+ },
659
+ $mul: (field, value, object) => {
660
+ const current = getLodash(object, field);
661
+ const castedValue = downcastBSON(value);
662
+ const castedCurrent = downcastBSON(current);
663
+
664
+ if (!Validator.NUMBER(castedValue))
665
+ throw `expected a number at $mul.${field} but got ${value}`;
666
+
667
+ setLodash(object, field, Validator.NUMBER(castedCurrent) ? defaultBSON(castedCurrent * castedValue, value) : 0);
668
+ },
669
+ $rename: (field, value, object) => {
670
+ if (!Validator.EMPTY_STRING(value))
671
+ throw `expected a non-empty string at $rename.${field} but got ${value}`;
672
+ const destStage = value.split('.');
673
+ const sourceStage = field.split('.');
674
+
675
+ sourceStage.forEach((e, i, a) => {
676
+ if (a.length !== destStage.length)
677
+ throw `dotnotation mismatch for ${value}`;
678
+ if (i !== a.length - 1) {
679
+ if (e !== destStage[i])
680
+ throw `dotnotation mismatch at ${destStage[i]}, expected "${e}"`;
291
681
  }
682
+ if (!e) throw `empty node for ${field}`;
683
+ });
684
+ const [tipObj, tipSource, tipDest] = destStage.length === 1 ? [object, field, value]
685
+ : [getLodash(object, destStage.slice(0, -1).join('.')), sourceStage.slice(-1)[0], destStage.slice(-1)[0]];
292
686
 
293
- if (op === WRITE_OPS.$RENAME) {
294
- if (nodeValue === undefined) return;
295
- if (typeof value !== 'string') throw `${op} operator expected a string value at ${path.join('.')}`;
296
- unsetLodash(afDoc, path.join('.'));
297
- path[path.length - 1] = value;
687
+ if (tipObj && tipSource in tipObj) {
688
+ tipObj[tipDest] = cloneDeep(tipObj[tipSource]);
689
+ delete tipObj[tipSource];
690
+ }
691
+ },
692
+ $set: (field, value, object) => {
693
+ setLodash(object, field, value === undefined ? null : value);
694
+ },
695
+ $setOnInsert: (field, value, object, isNew) => {
696
+ if (isNew) AtomicWriter.$set(field, value, object);
697
+ },
698
+ $unset: (field, _, object) => {
699
+ unsetLodash(object, field);
700
+ },
701
+ $addToSet: (field, value, object) => {
702
+ const current = getLodash(object, field);
703
+ if (Array.isArray(current)) {
704
+ if (
705
+ Validator.OBJECT(value) &&
706
+ Object.keys(value).length === 1 &&
707
+ '$each' in value
708
+ ) {
709
+ const { $each } = value;
710
+ if (!Array.isArray($each))
711
+ throw `expected an array at "$addToSet.${field}.$each" but got ${$each}`;
712
+ $each.forEach(e => {
713
+ if (!current.some(v => CompareBson.equal(v, e))) {
714
+ current.push(e);
715
+ }
716
+ });
717
+ } else if (!current.some(v => CompareBson.equal(v, value))) {
718
+ current.push(value);
298
719
  }
720
+ }
721
+ },
722
+ $pop: (field, value, object) => {
723
+ if (![1, -1].includes(value)) throw `expected 1 or -1 at "$pop.${field}" but got ${value}`;
724
+ const current = getLodash(object, field);
725
+ if (
726
+ Array.isArray(current) &&
727
+ current.length
728
+ ) current[value === 1 ? 'pop' : 'shift']();
729
+ },
730
+ $pull: (field, value, object) => {
731
+ // TODO: issues
732
+ const current = getLodash(object, field);
733
+ const isQueryObject = Validator.OBJECT(value);
299
734
 
300
- setLodash(
301
- afDoc,
302
- path.join('.'),
303
- op === WRITE_OPS.$SET ? value :
304
- op === WRITE_OPS.$INC ? (isNaN(nodeValue) ? value : nodeValue + value) :
305
- op === WRITE_OPS.$MAX ? (isNaN(nodeValue) ? value : value > nodeValue ? value : nodeValue) :
306
- op === WRITE_OPS.$MIN ? (isNaN(nodeValue) ? value : value < nodeValue ? value : nodeValue) :
307
- op === WRITE_OPS.$MUL ? (isNaN(nodeValue) ? 0 : value * nodeValue) :
308
- op === WRITE_OPS.$PULL ? (Array.isArray(nodeValue) ? nodeValue.filter(v => !isEqual(v, value)) : [value]) :
309
- op === WRITE_OPS.$PUSH ? (Array.isArray(nodeValue) ? [...nodeValue, value] : [value]) :
310
- op === WRITE_OPS.$RENAME ? nodeValue :
311
- null // TODO:
312
- );
735
+ if (
736
+ Array.isArray(current) &&
737
+ current.length
738
+ ) {
739
+ const remainingCurrent = current.filter(v => {
740
+ const isThisObject = Validator.OBJECT(v);
741
+
742
+ try {
743
+ if (
744
+ confirmFilterDoc(
745
+ isThisObject ? v : { __x_: v },
746
+ (isThisObject && isQueryObject) ? value : { __x_: value }
747
+ )
748
+ ) {
749
+ return false;
750
+ }
751
+ } catch (_) { }
752
+ return true;
753
+ });
754
+ setLodash(object, field, remainingCurrent);
313
755
  }
314
- });
756
+ },
757
+ $push: (field, value, object) => {
758
+ const current = getLodash(object, field);
759
+
760
+ if (Array.isArray(current)) {
761
+ if (Validator.OBJECT(value)) {
762
+ const { $each, $sort, $slice, $position, ...rest } = value;
763
+ if (Object.keys(rest).length)
764
+ throw `unknown property "${Object.keys(rest)}" at $push.${field}`;
765
+
766
+ if ($position !== undefined) {
767
+ if (Validator.INTEGER($position))
768
+ throw '$position must have an integer value';
769
+ if (!$each) throw '$position operator requires an $each operator';
770
+ }
771
+ if ($each !== undefined) {
772
+ if (!Array.isArray($each))
773
+ throw `expected an array at "$push.${field}.$each" but got ${$each}`;
774
+ if ($position !== undefined) {
775
+ current.splice($position, 0, ...$each);
776
+ } else current.push(...$each);
777
+ }
778
+ if ($sort !== undefined) {
779
+ if (!$each) throw '$sort operator requires an $each operator';
780
+ if ([1, -1].includes($sort)) {
781
+ current.sort();
782
+ if ($sort === -1) current.reverse();
783
+ } else if (Validator.OBJECT($sort)) {
784
+ if (Object.keys($sort).length !== 1)
785
+ throw 'number of object keys in a $sort must be one';
786
+
787
+ Object.entries($sort).forEach(([k, v]) => {
788
+ sortArrayByObjectKey(current, k);
789
+ if (v === -1) current.reverse();
790
+ });
791
+ } else throw `expected either 1, -1 or an object at "$push.${field}.$sort" but got ${$sort}`;
792
+ }
793
+ if ($slice) {
794
+ if (Validator.POSITIVE_INTEGER($slice))
795
+ throw `$slice operator requires a positive integer but got ${$slice}`;
796
+ current.splice($slice);
797
+ }
798
+ } else current.push(value);
799
+ }
800
+ },
801
+ $pullAll: (field, value, object) => {
802
+ if (!Array.isArray(value))
803
+ throw `expected an array at $pullAll.${field}`;
804
+
805
+ const current = getLodash(object, field);
315
806
 
316
- return afDoc;
317
- }
807
+ if (Array.isArray(current)) {
808
+ const remainingCurrent = current.filter(v =>
809
+ !value.some(k => CompareBson.equal(v, k))
810
+ );
811
+ setLodash(object, field, remainingCurrent);
812
+ }
813
+ }
814
+ };