react-native-mosquito-transport
Version:
React native javascript sdk for mosquito-transport (https://github.com/brainbehindx/mosquito-transport)
1,096 lines (950 loc) • 42.6 kB
JavaScript
import { niceHash, shuffleArray, sortArrayByObjectKey } from "../../helpers/peripherals";
import { awaitStore, updateCacheStore } from "../../helpers/utils";
import { CacheStore, Scoped } from "../../helpers/variables";
import { assignExtractionFind, CompareBson, confirmFilterDoc, defaultBSON, downcastBSON, validateCollectionName, validateFilter } from "./validator";
import { DatabaseRecordsListener } from "../../helpers/listeners";
import { BSONRegExp, ObjectId, Timestamp } from "../../vendor/bson";
import { niceGuard, Validator } from "guard-object";
import { TIMESTAMP } from "./types";
import { docSize, incrementDatabaseSize } from "./counter";
import { DatastoreParser, serializeToBase64 } from "./bson";
import { FS_PATH, getSystem, useFS } from "../../helpers/fs_manager";
import { grab, poke, unpoke } from "poke-object";
import { basicClone } from "../../helpers/basic_clone";
const { LIMITER_DATA, LIMITER_RESULT, DB_COUNT_QUERY } = FS_PATH;
export const listenQueryEntry = (callback, { accessId, builder, config, processId }) => {
const { projectUrl, dbName, dbUrl, path } = builder;
const { episode = 0 } = config || {};
const nodeID = `${projectUrl}_${dbName}_${dbUrl}_${path}`;
if (!Scoped.ActiveDatabaseListeners[nodeID])
Scoped.ActiveDatabaseListeners[nodeID] = {};
Scoped.ActiveDatabaseListeners[nodeID][processId] = Date.now();
const listener = DatabaseRecordsListener.listenTo('d', async (dispatchId) => {
if (dispatchId !== processId) return;
const cache = await getRecord(builder, accessId, episode);
if (cache) callback(cache[0]);
});
return () => {
listener();
if (Scoped.ActiveDatabaseListeners[nodeID]?.[processId]) {
delete Scoped.ActiveDatabaseListeners[nodeID][processId];
if (!Object.keys(Scoped.ActiveDatabaseListeners[nodeID]).length)
delete Scoped.ActiveDatabaseListeners[nodeID];
}
};
};
export const insertCountQuery = async (builder, access_id, value) => {
const { projectUrl, dbUrl, dbName, path } = builder;
const { io } = Scoped.ReleaseCacheData;
if (io) {
poke(CacheStore.DatabaseCountResult, [projectUrl, dbUrl, dbName, path, access_id], { value, touched: Date.now() });
updateCacheStore(['DatabaseCountResult']);
} else {
await useFS(builder, access_id, 'dbQueryCount')(async fs => {
await fs.set(DB_COUNT_QUERY(path), access_id, { value, touched: Date.now() });
poke(CacheStore.DatabaseStats.counters, [projectUrl, dbUrl, dbName, path], true);
});
updateCacheStore(['DatabaseStats']);
}
}
export const getCountQuery = async (builder, access_id) => {
const { projectUrl, dbUrl, dbName, path } = builder;
const { io } = Scoped.ReleaseCacheData;
if (io) {
const data = grab(CacheStore.DatabaseCountResult, [projectUrl, dbUrl, dbName, path, access_id]);
if (data) data.touched = Date.now();
return data && data.value;
} else {
return useFS(builder, access_id, 'dbQueryCount')(async fs => {
const data = await fs.find(DB_COUNT_QUERY(path), access_id, ['value']).catch(() => null);
if (data) {
await fs.set(DB_COUNT_QUERY(path), access_id, { touched: Date.now() });
return data.value;
}
});
}
}
export const insertRecord = async (builder, config, accessIdWithoutLimit, value, episode = 0) => {
builder = builder && basicClone(builder);
config = config && basicClone(config);
value = value && basicClone(value);
await awaitStore();
const { io } = Scoped.ReleaseCacheData;
const { projectUrl, dbUrl, dbName, path, command } = builder;
const { limit } = command;
const thisSize = docSize(value);
if (!io) {
await useFS(builder, accessIdWithoutLimit, 'database')(async fs => {
const resultAccessId = `${accessIdWithoutLimit}-${limit}`;
const [instanceData, resultData] = await Promise.all([
fs.find(LIMITER_DATA(path), accessIdWithoutLimit, ['size']).catch(() => undefined),
fs.find(LIMITER_RESULT(path), resultAccessId, ['size']).catch(() => undefined)
]);
const isEpisode = episode === 1 || !!resultData;
const editionSizeOffset = thisSize - (instanceData?.size || 0);
const resultSizeOffset = isEpisode ? thisSize - (resultData?.size || 0) : 0;
const newData = DatastoreParser.encode({
command,
config,
latest_limiter: limit,
data: value ? Array.isArray(value) ? value : [value] : []
});
const newResultData = isEpisode && DatastoreParser.encode({
data: value,
size: thisSize
});
await Promise.all([
fs.set(LIMITER_DATA(path), accessIdWithoutLimit, {
value: newData,
touched: Date.now(),
size: thisSize
}),
isEpisode ?
fs.set(LIMITER_RESULT(path), resultAccessId, {
access_id: accessIdWithoutLimit,
value: newResultData,
touched: Date.now(),
size: thisSize
}) : Promise.resolve()
]);
incrementDatabaseSize(builder, path, editionSizeOffset + resultSizeOffset);
});
updateCacheStore(['DatabaseStats']);
return;
}
const instanceData = grab(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'instance', accessIdWithoutLimit]);
const resultData = grab(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'episode', accessIdWithoutLimit, `${limit}`]);
const isEpisode = episode === 1 || !!resultData;
const editionSizeOffset = thisSize - (instanceData?.size || 0);
const resultSizeOffset = isEpisode ? thisSize - (resultData?.size || 0) : 0;
const newData = {
command,
config,
latest_limiter: limit,
size: thisSize,
data: value ? Array.isArray(value) ? value : [value] : [],
touched: Date.now()
};
const newResultData = isEpisode && {
data: value,
size: thisSize,
touched: Date.now()
};
incrementDatabaseSize(builder, path, editionSizeOffset + resultSizeOffset);
poke(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'instance', accessIdWithoutLimit], newData);
if (isEpisode) poke(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'episode', accessIdWithoutLimit, `${limit}`], basicClone(newResultData));
updateCacheStore(['DatabaseStore', 'DatabaseStats']);
};
export const getRecord = async (builder, accessIdWithoutLimit, episode = 0) => {
await awaitStore();
const { io } = Scoped.ReleaseCacheData;
const { projectUrl, dbUrl, dbName, path, command } = builder;
const { limit, sort, direction, random, findOne } = command;
const isEpisode = episode === 1;
const transformData = (data) => {
data = basicClone(data);
if (random) {
data = shuffleArray(data);
} else if (sort) {
data = sortArrayByObjectKey(data.slice(0), sort);
if (
direction === -1 ||
direction === 'desc' ||
direction === 'descending'
) data = data.slice(0).reverse();
}
if (findOne) {
data = data[0];
} else if (limit) data = data.slice(0, limit);
return data;
}
if (!io) {
const record = await useFS(builder, accessIdWithoutLimit, 'database')(async fs => {
const resultAccessId = `${accessIdWithoutLimit}-${limit}`;
const qData = await (
isEpisode ? fs.find(LIMITER_RESULT(path), resultAccessId, ['value']) :
fs.find(LIMITER_DATA(path), accessIdWithoutLimit, ['value'])
).catch(() => null);
const thisData = qData && DatastoreParser.decode(qData.value);
if (!thisData) return null;
if (isEpisode) {
await fs.set(LIMITER_RESULT(path), resultAccessId, { touched: Date.now() });
return [thisData.data];
}
const { latest_limiter, data } = thisData;
if (
latest_limiter === undefined ||
(Validator.POSITIVE_NUMBER(limit) && latest_limiter >= limit)
) {
await fs.set(LIMITER_DATA(path), accessIdWithoutLimit, { touched: Date.now() });
return [transformData(data)];
}
});
return record || null;
}
if (isEpisode) {
const resultData = grab(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'episode', accessIdWithoutLimit, `${limit}`]);
if (resultData) {
resultData.touched = Date.now();
return [basicClone(resultData.data)];
}
return null;
}
const instanceData = grab(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'instance', accessIdWithoutLimit]);
if (!instanceData) return null;
const { latest_limiter, data } = instanceData;
if (
latest_limiter === undefined ||
(Validator.POSITIVE_NUMBER(limit) && latest_limiter >= limit)
) {
instanceData.touched = Date.now();
return [transformData(data)];
}
return null;
};
export const generateRecordID = (builder, config, removeLimit) => {
builder = builder && basicClone(builder);
config = config && basicClone(config);
const { command, path, countDoc } = builder;
const { extraction, excludeFields, returnOnly } = config || {};
const recordObj = Object.fromEntries(
Object.entries({
path,
command,
countDoc,
extraction,
excludeFields,
returnOnly
}).filter(([_, v]) => v !== undefined)
);
if (command) recordObj.command = arrangeCommands(command, removeLimit);
if (extraction) {
if (Array.isArray(extraction)) recordObj.extraction = extraction.map(v => arrangeCommands(v));
else recordObj.extraction = arrangeCommands(extraction);
}
return niceHash(serializeToBase64(recordObj));
};
const arrangeCommands = (c, removeLimit) => {
c = basicClone(c);
const sortFind = f => {
['$and', '$or', '$nor'].forEach(n => {
if (f[n]) {
f[n] = f[n].map(v => sortObject(v));
}
});
return sortObject(f);
};
if (c.sort) c.direction = [-1, 'desc', 'descending'].includes(c.direction) ? 'desc' : 'asc';
if (c.find) c.find = sortFind(c.find);
if (c.findOne) c.findOne = sortFind(c.findOne);
if (removeLimit && ('limit' in c)) delete c.limit;
return sortObject(c);
};
const sortObject = (o) => Object.fromEntries(
Object.entries(o).sort(([a], [b]) => (a > b) ? 1 : (a < b) ? -1 : 0)
);
const recursiveFlat = (a) => {
return a.map(v => Array.isArray(v) ? recursiveFlat(v) : v).flat();
};
const recurseNonAtomicWrite = (obj, i, type) => {
if (!Validator.OBJECT(obj)) throw `expected a document but got ${obj}`;
Object.entries(obj).forEach(([k, v]) => {
if (!i) {
if (k === '_id') throw `avoid providing "_id" for ${type}() operation as _id only reference a single document`;
if (k === '_foreign_doc') throw '"_foreign_doc" is readonly';
}
if (k.includes('$') || k.includes('.')) {
if (!(k === '$timestamp' && v === 'now'))
throw `invalid property "${k}", ${type}() operation fields must not contain .$`;
}
if (Validator.OBJECT(v)) recurseNonAtomicWrite(v, i + 1, type);
});
};
const recurseAtomicWrite = (obj, i, type) => {
if (!Validator.OBJECT(obj)) throw `expected a document but got ${obj}`;
Object.entries(obj).forEach(([k, v]) => {
if (!i && !(k in AtomicWriter)) throw `Unknown update operator: ${k}`;
if (i === 1) {
if ((k === '_id' || k.startsWith('_id.')))
throw `avoid providing "_id" for ${type}() operation as _id only reference a single document`;
if (k === '_foreign_doc' || k.startsWith('_foreign_doc.'))
throw '"_foreign_doc" is readonly';
}
if (k.includes('.$')) throw `unsupported operation at "${k}"`;
if (!i || Validator.OBJECT(v)) recurseAtomicWrite(v, i + 1, type);
});
};
const WriteValidator = {
setOne: ({ value, type = 'setOne' }) => {
if (!Validator.OBJECT(value)) throw `expected a document but got ${value}`;
const { _id, ...rest } = value;
if (_id === undefined || JSON.stringify(_id) === 'null')
throw `_id requires a valid bson value but got ${_id}`;
recurseNonAtomicWrite(rest, 0, type);
},
setMany: ({ value }) => {
value.forEach(v => {
WriteValidator.setOne({ value: v, type: 'setMany' });
});
},
replaceOne: ({ find, value }) => {
validateFilter(find);
recurseNonAtomicWrite(value, 0, 'replaceOne');
},
putOne: ({ find, value }) => {
validateFilter(find);
recurseNonAtomicWrite(value, 0, 'putOne');
},
updateOne: ({ find, value }) => {
validateFilter(find);
recurseAtomicWrite(value, 0, 'updateOne');
},
updateMany: ({ find, value }) => {
validateFilter(find);
recurseAtomicWrite(value, 0, 'updateMany');
},
mergeOne: ({ find, value }) => {
validateFilter(find);
recurseAtomicWrite(value, 0, 'mergeOne');
},
mergeMany: ({ find, value }) => {
validateFilter(find);
recurseAtomicWrite(value, 0, 'mergeMany');
},
deleteOne: ({ find }) => {
validateFilter(find);
},
deleteMany: ({ find }) => {
validateFilter(find);
}
};
export const validateWriteValue = ({ type, find, value }) => WriteValidator[type]({ find, value, type });
export const addPendingWrites = async (builder, writeId, result) => {
builder = builder && basicClone(builder);
result = result && basicClone(result);
await awaitStore();
const { projectUrl } = builder;
const pendingSnapshot = basicClone(result);
const { editions, linearWrite, pathChanges } = await syncCache(builder, result);
const isStaticWrite = !linearWrite.some(({ value, type }) => {
if (
[
'updateOne',
'updateMany',
'mergeOne',
'mergeMany'
].includes(type)
) {
const operators = Object.keys(value);
return ['$inc', '$min', '$max', '$mul', '$pop', '$pull', '$push', '$rename'].includes(operators);
}
});
const pureBuilder = {};
['path', 'dbUrl', 'dbName', 'find', 'extraHeaders', 'maxRetries'].forEach(v => {
if (builder[v] !== undefined) pureBuilder[v] = builder[v];
});
pureBuilder.find = serializeToBase64({ _: pureBuilder.find });
pendingSnapshot.value = serializeToBase64({ _: pendingSnapshot.value });
let wasShifted;
if (isStaticWrite) {
// find previously matching pending write
const entries = Object.entries(CacheStore.PendingWrites[projectUrl] || {});
for (const [writeId, obj] of entries) {
if (!Scoped.OutgoingWrites[writeId]) {
if (
niceGuard(
{ builder: obj.builder, snapshot: obj.snapshot },
{ builder: pureBuilder, snapshot: pendingSnapshot }
)
) {
// shift it to the back
obj.addedOn = Date.now();
wasShifted = true;
break;
}
}
}
}
if (!wasShifted)
poke(CacheStore.PendingWrites, [projectUrl, writeId], basicClone({
builder: pureBuilder,
snapshot: pendingSnapshot,
editions,
addedOn: Date.now()
}));
updateCacheStore(['DatabaseStore', 'PendingWrites', 'DatabaseStats']);
notifyDatabaseNodeChanges(builder, [...pathChanges]);
};
const syncCache = async (builder, result) => {
const { io } = Scoped.ReleaseCacheData;
const { projectUrl, dbUrl, dbName } = builder;
const duplicateSets = {};
const editions = [];
const pathChanges = new Set([]);
const linearWrite =
result.type === 'batchWrite' ?
result.value.map(({ scope, value, find, path }) =>
({ type: scope, value, find, path })
)
: [{ ...result, find: builder.find, path: builder.path }];
const copiedWrite = basicClone(linearWrite);
await Promise.all(linearWrite.map(async ({ value: writeObj, find, type, path }) => {
WriteValidator[type]({ find, value: writeObj });
validateCollectionName(path);
pathChanges.add(path);
if (io) {
const colObj = grab(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'instance']);
if (colObj)
await Promise.all(
Object.entries(colObj).map(e =>
MutateDataInstance(
e,
path =>
Object.values(
grab(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'instance'], {})
).map(({ data }) => data).flat()
)
)
);
} else {
const colListing = await getSystem(builder).list(LIMITER_DATA(path), []).catch(() => []);
const pathFinder = {};
await Promise.all(colListing.map(async ([access_id]) =>
useFS(builder, access_id, 'database')(async fs => {
const data = await fs.find(LIMITER_DATA(path), access_id, ['value'])
.then(r => DatastoreParser.decode(r.value));
await MutateDataInstance([access_id, data], path =>
pathFinder[path] || (
pathFinder[path] = fs.list(LIMITER_DATA(path), ['value'])
.then(v => v.map(d => DatastoreParser.decode(d[1].value).data).flat())
.catch(() => [])
)
);
await fs.set(LIMITER_DATA(path), access_id, {
touched: Date.now(),
value: DatastoreParser.encode(data),
size: data.size
});
})
));
}
async function MutateDataInstance([entityId, dataObj], pathGetter) {
const { data: instance_data, command, config } = dataObj;
const entityFind = command.findOne || command.find;
const { extraction } = config || {};
const logChanges = (d) => {
editions.push(basicClone([entityId, d, path]));
const [b4, af] = d;
const offset = docSize(af) - docSize(b4);
dataObj.size += offset;
incrementDatabaseSize(builder, path, offset);
};
const snipUpdate = doc => snipDocument(doc, entityFind, config);
const accessExtraction = async obj => {
const buildAssignedExtraction = (data) => {
const d = (Array.isArray(extraction) ? extraction : [extraction]).map(thisExtraction => {
const query = basicClone(thisExtraction);
['find', 'findOne'].forEach(n => {
if (query[n])
query[n] = assignExtractionFind(data, query[n]);
});
return arrangeCommands(query);
});
if (Array.isArray(extraction)) return d;
return d[0];
}
const extractionResultant = buildAssignedExtraction(obj);
const extractionBinary = serializeToBase64({ _: extractionResultant });
const sameProjection = instance_data.find(({ _foreign_doc, ...restDoc }) =>
extractionBinary === serializeToBase64({ _: buildAssignedExtraction(restDoc) })
);
if (sameProjection) return sameProjection._foreign_doc;
// if no matching extraction was found, proceed to scrapping each _foreign_doc segment
const scrapedProjection = await Promise.all((Array.isArray(extractionResultant) ? extractionResultant : [extractionResultant]).map(async (query, i) => {
const { sort, direction, limit, find, findOne, collection: path } = query;
let scrapDocs = [];
instance_data.forEach(({ _foreign_doc }) => {
_foreign_doc = (Array.isArray(_foreign_doc) ? _foreign_doc : [_foreign_doc])[i];
recursiveFlat([_foreign_doc]).forEach(e => {
if (e && confirmFilterDoc(e, find || findOne)) {
scrapDocs.push(e);
}
});
});
if (!scrapDocs.length) {
// if no matching extraction was found, proceed to scrapping ancestor path
(await pathGetter(path)).forEach(({ _foreign_doc, ...doc }) => {
if (confirmFilterDoc(doc, find || findOne)) {
scrapDocs.push(doc);
}
});
}
scrapDocs = scrapDocs.filter((v, i, a) => a.findIndex(b => b._id === v._id) === i);
if (sort) sortArrayByObjectKey(scrapDocs, sort);
if ([-1, 'desc', 'descending'].includes(direction)) scrapDocs.reverse();
if (limit) scrapDocs = scrapDocs.slice(0, limit);
scrapDocs = scrapDocs.map(v => snipDocument(v, find || findOne, query));
return findOne ? scrapDocs[0] : scrapDocs;
}));
return basicClone(Array.isArray(extraction) ? scrapedProjection : scrapedProjection[0]);
}
if (['setOne', 'setMany'].includes(type)) {
await Promise.all((type === 'setOne' ? [writeObj] : writeObj).map(async e => {
const obj = deserializeNonAtomicWrite(e);
if (extraction) obj._foreign_doc = await accessExtraction(obj);
if (confirmFilterDoc(obj, entityFind)) {
if (instance_data.findIndex(v => CompareBson.equal(v._id, e._id)) === -1) {
const x = snipUpdate(obj);
instance_data.push(basicClone(x));
logChanges([undefined, x]);
} else if (!duplicateSets[e._id]) {
console.warn(`document with _id=${e._id} already exist locally with ${type}() operation, skipping to online commit`);
duplicateSets[e._id] = true;
}
}
}));
return;
}
if (['putOne', 'replaceOne'].includes(type)) {
const extras = createWriteFromFind(find);
let deletions = 0;
const cdata = instance_data.slice(0);
for (let i = 0; i < cdata.length; i++) {
const doc = cdata[i];
if (confirmFilterDoc(doc, find)) {
const obj = deserializeNonAtomicWrite({
...extras,
...writeObj,
...'_id' in extras ? {} : { _id: doc._id }
});
if (extraction) obj._foreign_doc = await accessExtraction(obj);
if (confirmFilterDoc(obj, entityFind)) {
const x = snipUpdate(obj);
instance_data[i - deletions] = x;
logChanges([doc, x]);
} else {
instance_data.splice(i - deletions++, 1);
logChanges([doc, undefined]);
}
return;
}
}
if (type === 'putOne') {
const obj = deserializeNonAtomicWrite({
...extras,
...writeObj,
...'_id' in extras ? {} : { _id: new ObjectId() }
});
if (extraction) obj._foreign_doc = await accessExtraction(obj);
if (confirmFilterDoc(obj, entityFind)) {
const x = snipUpdate(obj);
instance_data.push(x);
logChanges([undefined, x]);
}
}
return;
}
if (['deleteOne', 'deleteMany'].includes(type)) {
let deletions = 0;
const cdata = instance_data.slice(0);
for (let i = 0; i < cdata.length; i++) {
const doc = cdata[i];
if (confirmFilterDoc(doc, find)) {
instance_data.splice(i - deletions++, 1);
logChanges([doc, undefined]);
if (type === 'deleteOne') return;
}
}
return;
}
let founded;
let deletions = 0;
const cdata = instance_data.slice(0);
for (let i = 0; i < cdata.length; i++) {
const doc = cdata[i];
if (confirmFilterDoc(doc, find)) {
const obj = deserializeAtomicWrite(doc, deserializeWriteValue(writeObj), false, type);
if (extraction) obj._foreign_doc = await accessExtraction(obj);
if (confirmFilterDoc(obj, entityFind)) {
const x = snipUpdate(obj);
instance_data[i - deletions] = x;
logChanges([doc, x]);
} else {
instance_data.splice(i - deletions++, 1);
logChanges([doc, undefined]);
}
founded = true;
if (type.endsWith('One')) return;
}
}
if (!founded && type.startsWith('merge')) {
const extras = createWriteFromFind(find);
const obj = {
...extras,
...deserializeAtomicWrite(
{ _id: '_id' in extras ? extras._id : new ObjectId() },
deserializeWriteValue(writeObj),
true,
type
)
};
if (extraction) obj._foreign_doc = await accessExtraction(obj);
if (confirmFilterDoc(obj, entityFind)) {
const x = snipUpdate(obj);
instance_data.push(x);
logChanges([undefined, x]);
}
}
};
}));
return {
editions,
pathChanges: [...pathChanges],
linearWrite: copiedWrite
};
}
export const removePendingWrite = async (builder, writeId, revert) => {
await awaitStore();
const { projectUrl } = builder;
const pendingData = grab(CacheStore.PendingWrites, [projectUrl, writeId]);
if (!pendingData) return;
const pathChanges = revert ? await revertChanges(builder, pendingData.editions) : [];
unpoke(CacheStore.PendingWrites, [projectUrl, writeId]);
updateCacheStore(['PendingWrites', 'DatabaseStore', 'DatabaseStats']);
notifyDatabaseNodeChanges(builder, [...pathChanges]);
};
const revertChanges = async (builder, pendingData) => {
const { io } = Scoped.ReleaseCacheData;
const { projectUrl, dbUrl, dbName } = builder;
const pathChanges = new Set([]);
await Promise.all(pendingData.map(async ([access_id, [b4Doc, afDoc], path]) => {
if (io) {
RevertMutation(grab(CacheStore.DatabaseStore, [projectUrl, dbUrl, dbName, path, 'instance', access_id]));
} else {
await useFS(builder, access_id, 'database')(async fs => {
const colObj = await fs.find(LIMITER_DATA(path), access_id, ['value'])
.then(v => DatastoreParser.decode(v.value))
.catch(() => null);
if (!colObj) return;
RevertMutation(colObj);
await fs.set(LIMITER_DATA(path), access_id, {
value: DatastoreParser.encode(colObj),
touched: Date.now(),
size: colObj.size
});
});
}
function RevertMutation(colObj) {
const colList = colObj?.data;
const updateSize = (b4, af) => {
const offset = docSize(af) - docSize(b4);
colObj.size += offset;
incrementDatabaseSize(builder, path, offset);
}
if (colList) {
if (afDoc) {
const editedIndex = colList.findIndex(e => CompareBson.equal(e._id, afDoc._id));
if (editedIndex !== -1) {
if (
serializeToBase64(afDoc) === serializeToBase64(colList[editedIndex])
) {
if (b4Doc) {
colList[editedIndex] = b4Doc;
updateSize(afDoc, b4Doc);
} else {
colList.splice(editedIndex, 1);
updateSize(afDoc, undefined);
}
}
}
} else if (
b4Doc &&
colList.findIndex(e => CompareBson.equal(e._id, b4Doc._id)) === -1
) {
colList.push(b4Doc);
updateSize(undefined, b4Doc);
}
}
pathChanges.add(path);
}
}));
return [...pendingData];
}
const notifyDatabaseNodeChanges = (builder, changedCollections = []) => {
const { projectUrl, dbName, dbUrl } = builder;
changedCollections.forEach(path => {
const nodeID = `${projectUrl}_${dbName}_${dbUrl}_${path}`;
Object.entries(Scoped.ActiveDatabaseListeners[nodeID] || {})
.sort((a, b) => a[1] - b[1])
.forEach(([processId]) => {
DatabaseRecordsListener.dispatch('d', processId);
});
});
};
const createWriteFromFind = (find) => {
let result = {};
Object.entries(find).forEach(([k, v]) => {
if (['$and', '$or'].includes(k)) {
v.forEach(e => {
result = { ...result, ...createWriteFromFind(e) };
});
} else if (!k.startsWith('$')) {
if (Validator.OBJECT(v)) {
if (!Object.keys(v).some(v => v.startsWith('$'))) {
result[k] = v;
} else if ('$eq' in v) {
result[k] = v.$eq;
}
} else {
result[k] = v instanceof RegExp ? new BSONRegExp(v.source, v.flags) : v;
}
}
});
return result;
};
const snipDocument = (data, find, config) => {
if (!data || !config) return data;
const { returnOnly, excludeFields } = config || {};
let output = basicClone(data);
if (returnOnly) {
output = {};
(Array.isArray(returnOnly) ? returnOnly : [returnOnly]).filter(v => v).forEach(e => {
const thisData = grab(data, e);
if (thisData) poke(output, e, thisData);
});
} else if (excludeFields) {
(Array.isArray(excludeFields) ? excludeFields : [excludeFields]).filter(v => v).forEach(e => {
if (grab(data, e) && e !== '_id') unpoke(output, e);
});
}
getFindFields(find).forEach(field => {
if (!grab(output, field)) {
const mainData = grab(data, field);
if (mainData !== undefined) poke(output, field, mainData);
}
});
return output;
};
const getFindFields = (find) => {
const result = ['_id'];
Object.entries(find).forEach(([k, v]) => {
if (['$and', '$or', '$nor'].includes(k)) {
v.forEach(e => {
result.push(...getFindFields(e));
});
} else if (k === '$text') {
result.push(...Array.isArray(v.$field) ? v.$field : [v.$field]);
} else if (!k.startsWith('$')) {
result.push(k);
}
});
return result.filter((v, i, a) => a.findIndex(b => b === v) === i);
};
const deserializeWriteValue = (value) => {
if (!value) return value;
if (niceGuard(TIMESTAMP, value)) {
return Date.now();
} else if (Validator.OBJECT(value)) {
return Object.fromEntries(
Object.entries(value).map(([k, v]) =>
Validator.JSON(v) ? [k, deserializeWriteValue(v)] : [k, v]
)
);
} else if (Array.isArray(value)) {
return value.map(deserializeWriteValue);
} else return value;
}
const deserializeNonAtomicWrite = (writeObj) => deserializeWriteValue(writeObj);
const deserializeAtomicWrite = (b4Doc, writeObj, isNew, type) => {
const resultantDoc = { ...b4Doc };
Object.entries(writeObj).forEach(([key, value]) => {
if (key in AtomicWriter) {
if (Validator.OBJECT(value)) {
Object.entries(value).forEach(([k, v]) => {
AtomicWriter[key](k, v, resultantDoc, isNew, type);
});
} else throw `expected an object at ${key} but got ${value}`;
} else if (key.startsWith('$')) {
throw `Unknown update operator: ${key}`;
} else throw 'MongoInvalidArgumentError: Update document requires atomic operators';
});
return resultantDoc;
};
const AtomicWriter = {
$currentDate: (field, value, object) => {
const isDate = value === true || niceGuard({ $type: "date" }, value);
const isTimestamp = niceGuard({ $type: "timestamp" }, value);
if (
!isDate &&
!isTimestamp
) throw `invalid value at $currentDate.${field}, expected any of boolean (true), { $type: "timestamp" } or { $type: "date" } but got ${value}`;
poke(object, field, isDate ? new Date() : new Timestamp({ t: Math.floor(Date.now() / 1000), i: 0 }));
},
$inc: (field, value, object) => {
const current = grab(object, field);
if (current === null) {
console.warn(`cannot use $inc operator on a null value at ${field}`);
return;
}
const castedCurrent = downcastBSON(current);
const castedValue = downcastBSON(value);
if (!Validator.NUMBER(castedValue)) throw `expected a number at $inc.${field} but got ${value}`;
poke(object, field, Validator.NUMBER(castedCurrent) ? defaultBSON(castedCurrent + castedValue, current) : value);
},
$min: (field, value, object) => {
const current = grab(object, field);
if (CompareBson.lesser(value, current)) {
poke(object, field, value);
}
},
$max: (field, value, object) => {
const current = grab(object, field);
if (CompareBson.greater(value, current)) {
poke(object, field, value);
}
},
$mul: (field, value, object) => {
const current = grab(object, field);
const castedValue = downcastBSON(value);
const castedCurrent = downcastBSON(current);
if (!Validator.NUMBER(castedValue))
throw `expected a number at $mul.${field} but got ${value}`;
poke(object, field, Validator.NUMBER(castedCurrent) ? defaultBSON(castedCurrent * castedValue, value) : 0);
},
$rename: (field, value, object) => {
if (!Validator.EMPTY_STRING(value))
throw `expected a non-empty string at $rename.${field} but got ${value}`;
const destStage = value.split('.');
const sourceStage = field.split('.');
sourceStage.forEach((e, i, a) => {
if (a.length !== destStage.length)
throw `dotnotation mismatch for ${value}`;
if (i !== a.length - 1) {
if (e !== destStage[i])
throw `dotnotation mismatch at ${destStage[i]}, expected "${e}"`;
}
if (!e) throw `empty node for ${field}`;
});
const [tipObj, tipSource, tipDest] = destStage.length === 1 ? [object, field, value]
: [grab(object, destStage.slice(0, -1).join('.')), sourceStage.slice(-1)[0], destStage.slice(-1)[0]];
if (tipObj && tipSource in tipObj) {
tipObj[tipDest] = basicClone(tipObj[tipSource]);
delete tipObj[tipSource];
}
},
$set: (field, value, object) => {
poke(object, field, value === undefined ? null : value);
},
$setOnInsert: (field, value, object, isNew) => {
if (isNew) AtomicWriter.$set(field, value, object);
},
$unset: (field, _, object) => {
unpoke(object, field);
},
$addToSet: (field, value, object) => {
const current = grab(object, field);
if (Array.isArray(current)) {
if (
Validator.OBJECT(value) &&
Object.keys(value).length === 1 &&
'$each' in value
) {
const { $each } = value;
if (!Array.isArray($each))
throw `expected an array at "$addToSet.${field}.$each" but got ${$each}`;
$each.forEach(e => {
if (!current.some(v => CompareBson.equal(v, e))) {
current.push(e);
}
});
} else if (!current.some(v => CompareBson.equal(v, value))) {
current.push(value);
}
}
},
$pop: (field, value, object) => {
if (![1, -1].includes(value)) throw `expected 1 or -1 at "$pop.${field}" but got ${value}`;
const current = grab(object, field);
if (
Array.isArray(current) &&
current.length
) current[value === 1 ? 'pop' : 'shift']();
},
$pull: (field, value, object) => {
// TODO: issues
const current = grab(object, field);
const isQueryObject = Validator.OBJECT(value);
if (
Array.isArray(current) &&
current.length
) {
const remainingCurrent = current.filter(v => {
const isThisObject = Validator.OBJECT(v);
try {
if (
confirmFilterDoc(
isThisObject ? v : { __x_: v },
(isThisObject && isQueryObject) ? value : { __x_: value }
)
) {
return false;
}
} catch (_) { }
return true;
});
poke(object, field, remainingCurrent);
}
},
$push: (field, value, object) => {
const current = grab(object, field);
if (Array.isArray(current)) {
if (Validator.OBJECT(value)) {
const { $each, $sort, $slice, $position, ...rest } = value;
if (Object.keys(rest).length)
throw `unknown property "${Object.keys(rest)}" at $push.${field}`;
if ($position !== undefined) {
if (Validator.INTEGER($position))
throw '$position must have an integer value';
if (!$each) throw '$position operator requires an $each operator';
}
if ($each !== undefined) {
if (!Array.isArray($each))
throw `expected an array at "$push.${field}.$each" but got ${$each}`;
if ($position !== undefined) {
current.splice($position, 0, ...$each);
} else current.push(...$each);
}
if ($sort !== undefined) {
if (!$each) throw '$sort operator requires an $each operator';
if ([1, -1].includes($sort)) {
current.sort();
if ($sort === -1) current.reverse();
} else if (Validator.OBJECT($sort)) {
if (Object.keys($sort).length !== 1)
throw 'number of object keys in a $sort must be one';
Object.entries($sort).forEach(([k, v]) => {
sortArrayByObjectKey(current, k);
if (v === -1) current.reverse();
});
} else throw `expected either 1, -1 or an object at "$push.${field}.$sort" but got ${$sort}`;
}
if ($slice) {
if (Validator.POSITIVE_INTEGER($slice))
throw `$slice operator requires a positive integer but got ${$slice}`;
current.splice($slice);
}
} else current.push(value);
}
},
$pullAll: (field, value, object) => {
if (!Array.isArray(value))
throw `expected an array at $pullAll.${field}`;
const current = grab(object, field);
if (Array.isArray(current)) {
const remainingCurrent = current.filter(v =>
!value.some(k => CompareBson.equal(v, k))
);
poke(object, field, remainingCurrent);
}
}
};