Compare commits

...

142 Commits
v1.5.1 ... main

Author SHA1 Message Date
nvms
d31d083ee3 cleanup 2025-04-22 11:17:31 -04:00
nvms
49caf89101 README 2025-04-22 11:17:04 -04:00
nvms
1b026ecadb add client-utils package 2025-04-22 10:55:56 -04:00
nvms
48b41c9d19 simplify metadata retrieval 2025-04-22 10:53:45 -04:00
nvms
209614c3e8 additional client tests 2025-04-22 10:53:25 -04:00
nvms
fde46ad338 validate presence state shape 2025-04-22 10:53:04 -04:00
nvms
960616b3fb README 2025-04-21 11:56:42 -04:00
nvms
c448abfecc README 2025-04-21 11:48:04 -04:00
nvms
fe163324df feature: ephemeral, room-scoped presence states 2025-04-21 11:27:52 -04:00
nvms
21dd9ccf7e feat(server): assign short unique connection IDs using custom generator 2025-04-21 10:40:05 -04:00
nvms
58980e9f09 fix(presence): add TTL-based expiration cleanup using Redis keyspace notifications 2025-04-21 08:37:05 -04:00
nvms
6e153b1b44 registerCommand -> exposeCommand 2025-04-21 08:05:20 -04:00
nvms
978fd71d85 1.0.7 2025-04-20 17:06:02 -04:00
nvms
9fbd947ad1 refactor for maintainability and modularity 2025-04-20 17:05:32 -04:00
nvms
57af00dc40 add useful room APIs to the client 2025-04-20 16:21:12 -04:00
nvms
fb4f275d58 use pattern subscription for presence and add more tests 2025-04-20 15:40:08 -04:00
nvms
c6cb0da27c add some record subscription tests 2025-04-20 15:17:04 -04:00
nvms
bbd48020de namespace messages 2025-04-19 20:57:48 -04:00
nvms
66803c1177 subscribe -> subscribeChannel 2025-04-18 22:33:19 -04:00
nvms
c492ee8a05 README 2025-04-18 17:57:17 -04:00
nvms
0579c0d150 README 2025-04-18 17:52:25 -04:00
nvms
36eed400b8 README 2025-04-18 17:48:10 -04:00
nvms
9ebef6bdb6 README 2025-04-18 17:47:12 -04:00
nvms
1c797eb1ba README 2025-04-18 17:21:58 -04:00
nvms
c14dba183c 1.0.6 2025-04-18 17:18:42 -04:00
nvms
d6f237152b 1.0.5 2025-04-18 17:17:44 -04:00
nvms
6181227532 feature: presence management 2025-04-18 17:17:12 -04:00
nvms
6bd9803c61 add getRoomsForConnection 2025-04-18 16:25:04 -04:00
nvms
af2cf5a4a4 README 2025-04-18 15:41:10 -04:00
nvms
51fc280d8b add onConnection and onDisconnection 2025-04-18 15:40:55 -04:00
nvms
b4751aefe8 README 2025-04-18 13:24:16 -04:00
nvms
798164bec0 add primitive value test 2025-04-18 13:22:25 -04:00
nvms
9a835e0c76 README 2025-04-18 12:50:02 -04:00
nvms
b25f54ae15 README 2025-04-18 12:47:52 -04:00
nvms
9b9c7bea04 publish mesh-express 2025-04-18 12:17:52 -04:00
nvms
f2b80feab8 README 2025-04-18 11:34:14 -04:00
nvms
9c1370dcbf README 2025-04-18 11:30:50 -04:00
nvms
e993afc07f README 2025-04-18 10:11:30 -04:00
nvms
663c9ab735 1.0.4 2025-04-18 10:07:58 -04:00
nvms
f9ccd98d39 feature: exposeWritableRecord and client.publishRecordUpdate 2025-04-18 10:07:24 -04:00
nvms
5a59182775 include the record identifier in the subscribeRecord callback 2025-04-18 09:22:51 -04:00
nvms
10c18f668e README 2025-04-18 08:56:29 -04:00
nvms
8d114a1285 README 2025-04-18 07:59:58 -04:00
nvms
f26e2ddbac README 2025-04-17 22:16:05 -04:00
nvms
7f7d3168af remove pointless export 2025-04-17 21:15:48 -04:00
nvms
f37f040ecf README 2025-04-17 21:14:10 -04:00
nvms
f6c397e1e2 comments and scoping 2025-04-17 21:14:00 -04:00
nvms
31a53fb274 README 2025-04-17 20:59:55 -04:00
nvms
2965ecb548 1.0.2 2025-04-17 20:52:26 -04:00
nvms
7c2850db27 feature: record subscription 2025-04-17 20:52:01 -04:00
nvms
0133f59e39 1.0.1 2025-04-17 18:44:14 -04:00
nvms
06571ac28a feature: room metadata 2025-04-17 18:43:50 -04:00
nvms
8db63ab664 export type 2025-04-17 17:19:18 -04:00
nvms
9140ea34d8 remove unused type 2025-04-17 17:19:00 -04:00
nvms
22140253fe README 2025-04-17 17:14:33 -04:00
nvms
bffefe344a README 2025-04-17 16:42:50 -04:00
nvms
8a84f6ea04 README 2025-04-17 16:30:08 -04:00
nvms
18f60550e2 change hostname 2025-04-17 16:26:36 -04:00
nvms
6fe63c8d58 publish mesh 2025-04-17 16:18:18 -04:00
nvms
b5cd75a018 add test for getRoom 2025-04-15 20:51:38 -04:00
nvms
8af50f0c00 update .npmignore and bump version 2025-04-15 14:52:03 -04:00
nvms
5bd827515f use latest keepalive-ws 2025-04-15 14:51:34 -04:00
nvms
3395ddb7ac 1.0.2 release 2025-04-15 14:35:49 -04:00
nvms
ada569c83c 1.0.1 release 2025-04-15 14:35:41 -04:00
nvms
437c264895 1.0.0 release 2025-04-15 14:34:41 -04:00
nvms
7170d1bf89 redis-backed room support 2025-04-15 14:33:20 -04:00
nvms
5c322d6bbc Release 2.0.1 2025-03-27 20:42:16 -04:00
nvms
3e6ee88ab7 README 2025-03-27 20:41:52 -04:00
nvms
4f858a5b96 Add missing .gitignore 2025-03-27 20:01:21 -04:00
nvms
48d1205505 feat: "scene" (i.e. world) management
- Add tests to ensure predictable start/stop world behavior.
- Update README to include example usages.
2025-03-27 19:13:41 -04:00
nvms
6e063101cc feat(input): improve safety check of button assignment
feat(time scale): adjust time scale logic, add rawDelta
fix(migrateEntityId): update $ceMap on entity id change
chore(tests): update tests
chore(README): update readme to reflect these new changes
2025-03-27 17:50:10 -04:00
nvms
061c50da90 1.2.0 release 2025-03-27 10:00:14 -04:00
nvms
910e19d690 README 2025-03-26 21:56:12 -04:00
nvms
fc2f2bea12 Remove pointless comments and clean up a bit. 2025-03-26 21:12:41 -04:00
nvms
7714d71b0a feat(keepalive-ws): enhance README and improve client/server implementation
- Add tests
2025-03-26 21:09:28 -04:00
nvms
2acba51367 refactor: convert ids to functional API with improved tests
- Replace class-based implementation with functional approach
- Switch from Manten to Vitest for testing
- Update README with clearer API documentation
- Fix alphabet randomization test
2025-03-26 19:03:31 -04:00
nvms
6be7fbbfe0 feat: make close return a promise and update README 2025-03-26 16:57:24 -04:00
nvms
20fa3707ff fix: improve connection reliability and add comprehensive tests
- Make connect() methods return Promises for better async control
- Remove automatic connections in constructors to prevent race conditions
- Handle ECONNRESET errors gracefully during disconnection
- Add comprehensive test suite covering reconnection, timeouts, and concurrency
2025-03-26 16:51:03 -04:00
nvms
0fa7229471 README 2025-03-26 16:29:37 -04:00
nvms
98df494b76 add tests 2025-03-26 16:17:32 -04:00
nvms
83f618cca7 add tests 2025-02-12 21:13:25 -05:00
nvms
00472d978e satisfy linter 2024-11-05 15:34:35 -05:00
nvms
8bf6823d31 1.2.7 release 2024-09-26 11:53:48 -04:00
nvms
648aba03e8 1.6.4 release 2024-09-26 11:53:22 -04:00
nvms
507a2fe341 1.6.3 release 2024-09-26 11:52:25 -04:00
nvms
3226bbc604 1.0.2 release 2024-09-26 11:51:35 -04:00
nvms
19481c1e1c Release 2.0.0 2024-09-24 14:10:53 -04:00
nvms
4e72f0a8b7 Release 1.6.0 2024-09-24 14:10:44 -04:00
nvms
e6d198ddc4 Release 1.5.13 2024-09-24 14:10:35 -04:00
nvms
dcacf3f988 Release 1.5.13 2024-09-24 14:10:08 -04:00
nvms
c546375d06 Release 1.5.12 2024-09-24 14:09:41 -04:00
nvms
9ad1eeeeb9 - formatting
- allow system to return null to break pipe chain
2024-09-24 14:09:10 -04:00
nvms
91152871d3 1.6.2 release 2024-09-21 14:57:06 -04:00
nvms
bba17d3141 warn once 2024-09-21 14:56:55 -04:00
nvms
ae07092705 1.6.1 release 2024-09-21 14:54:17 -04:00
nvms
64640c8407 warn instead of throw 2024-09-21 14:53:55 -04:00
nvms
82c13fce56 1.0.1 release 2024-09-12 10:36:16 -04:00
nvms
b8184b0e52 1.6.0 release 2024-09-11 13:14:24 -04:00
nvms
43f46c28f4 1.5.6 release 2024-09-11 13:14:05 -04:00
nvms
0bc316f0aa expose resyncSession 2024-09-11 13:13:52 -04:00
nvms
2214a58113 add removeFromAllRooms 2024-09-11 13:13:40 -04:00
nvms
5546e845ac Release 1.5.11 2024-09-09 09:49:27 -04:00
nvms
bafe78e6b2 skip pad 2024-09-09 09:49:20 -04:00
nvms
3ca5470ef9 Release 1.5.10 2024-09-09 09:45:35 -04:00
nvms
5b2dae803c Release 1.5.9 2024-09-09 09:44:45 -04:00
nvms
89397d1314 fix exports 2024-09-09 09:44:37 -04:00
nvms
bd34ffdbde Release 1.5.8 2024-09-09 09:40:33 -04:00
nvms
c4045f9906 add getUsername 2024-09-09 09:40:26 -04:00
nvms
002e7b0bcc Release 1.5.7 2024-09-09 09:40:07 -04:00
nvms
74bc7163a2 adjust fps calc 2024-09-09 09:39:57 -04:00
nvms
a05a3810a2 Release 1.5.6 2024-09-09 09:39:11 -04:00
nvms
f5b43651ed 1.5.5 release 2024-09-06 23:07:42 -04:00
nvms
3f02b12089 1.2.6 release 2024-09-06 21:38:03 -04:00
nvms
af47133077 respect shouldReconnect immediately 2024-09-06 21:37:49 -04:00
nvms
0ecaaddef4 0.3.8 release 2024-09-06 21:37:08 -04:00
nvms
90e7f8b58a heh.. 2024-09-05 13:24:28 -04:00
nvms
48619da543 avoid unnecessarily checking the authoritative remember directive too often 2024-09-05 13:23:17 -04:00
nvms
75715ece00 1.5.4 release 2024-09-05 13:22:08 -04:00
nvms
8dbc214e6f allow typing on the callback return value for registerCommand 2024-09-04 10:49:32 -04:00
nvms
8ef12272be 1.2.5 release 2024-09-04 10:33:51 -04:00
nvms
f52a92ccda 0.3.7 release 2024-09-04 10:33:19 -04:00
nvms
4279f28589 re-export from keepalive-ws 2024-09-04 06:13:40 -04:00
nvms
fcab68acf6 README 2024-09-04 06:13:15 -04:00
nvms
e8d489bbfe formatting, add broadcastRoomExclude, make WSContext typable 2024-09-04 06:12:59 -04:00
nvms
bc91dd1c9f 1.2.4 release 2024-09-04 05:50:09 -04:00
nvms
03893a0c7e 1.2.3 release 2024-09-03 22:46:36 -04:00
nvms
169281586f 0.3.6 release 2024-09-03 22:45:50 -04:00
nvms
25987c4c87 1.2.2 release 2024-09-03 22:41:25 -04:00
nvms
df0a3f1657 1.2.1 release 2024-09-03 22:39:46 -04:00
nvms
84e20d8851 0.3.5 release 2024-09-03 22:38:52 -04:00
nvms
84f7fcd560 0.3.4 release 2024-09-03 21:01:44 -04:00
nvms
2ead89ed74 0.3.3 release 2024-09-03 09:27:49 -04:00
nvms
116b4da850 private reconnect, docs for disconnect 2024-09-03 09:27:43 -04:00
nvms
d8565d488c 0.3.2 release 2024-09-03 09:22:27 -04:00
nvms
b55d854b8a add disconnect method for client 2024-09-03 09:22:19 -04:00
nvms
24b856b78f relocate 2024-08-28 09:39:27 -04:00
nvms
e197339f30 relocate 2024-08-28 09:08:33 -04:00
nvms
a209d10566 degit arc 2024-08-28 09:08:11 -04:00
nvms
ddb5676b74 ensure we distribute express-session-auth.d.ts 2024-08-28 09:06:58 -04:00
nvms
c55dcce258 rm unused 2024-08-28 09:06:17 -04:00
nvms
66b85e8695 1.5.3 release 2024-08-27 20:51:39 -04:00
nvms
021ae07f10 1.5.2 release 2024-08-27 20:47:41 -04:00
212 changed files with 16722 additions and 816 deletions

@ -1 +0,0 @@
Subproject commit 595e0b41961a6504b957173fa6470e4e21d16296

2
packages/arc/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.test
benchmark

4
packages/arc/.npmignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
tests
.test
src

1039
packages/arc/README.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
import { defineConfig } from "bumpp";
export default defineConfig({
commit: "%s release",
push: true,
tag: true,
});

BIN
packages/arc/bun.lockb Executable file

Binary file not shown.

34
packages/arc/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "@prsm/arc",
"version": "2.2.8",
"description": "",
"main": "./dist/index.js",
"module": "./dist/index.js",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"build": "tsup",
"test": "bun tests/index.ts",
"release": "bumpp package.json && npm publish --access public"
},
"author": "nvms",
"license": "Apache-2.0",
"devDependencies": {
"@types/lodash": "^4.14.182",
"@types/node": "^17.0.35",
"bumpp": "^9.1.0",
"manten": "^0.1.0",
"tsup": "^6.5.0",
"typescript": "^4.7.2"
},
"dependencies": {
"dot-wild": "^3.0.1",
"lodash": "^4.17.21"
}
}

View File

@ -0,0 +1,100 @@
import fs from "fs";
import path from "path";
import { AdapterConstructorOptions, StorageAdapter } from ".";
import { SimpleFIFO } from "./fs";
import crypto from "crypto";
export default class EncryptedFSAdapter<T> implements StorageAdapter<T> {
storagePath: string;
name: string;
filePath: string;
queue: SimpleFIFO;
key: string;
constructor({ storagePath, name, key = "Mahpsee2X7TKLe1xwJYmar91pCSaZIY7" }: AdapterConstructorOptions<T>) {
if (!name.endsWith(".json")) {
name += ".json";
}
this.storagePath = storagePath;
this.name = name;
this.queue = new SimpleFIFO();
this.filePath = path.join(this.storagePath, this.name);
this.key = key;
this.prepareStorage();
}
prepareStorage() {
if (!fs.existsSync(this.storagePath)) {
fs.mkdirSync(this.storagePath);
}
if (!fs.existsSync(this.filePath)) {
fs.writeFileSync(this.filePath, JSON.stringify({}));
}
}
read(): { [key: string]: T } {
try {
const data = fs.readFileSync(this.filePath, "utf8");
const decrypted = decrypt(data, this.key);
return Object.assign(
{},
JSON.parse(decrypted) || {},
);
} catch (e) {
return {};
}
}
write(data: { [key: string]: T }) {
this.queue.push([
(d: { [key: string]: T }) => {
encryptAndWrite(d, this.key, this.filePath);
},
data,
]);
while (this.queue.length()) {
const ar = this.queue.shift();
ar[0](ar[1]);
}
}
}
const encryptAndWrite = (data: any, key: string, ...args: any[]) => {
const json = JSON.stringify(data, null, 0);
return write(encrypt(json, key), ...args);
};
const write = (data: string, ...args: any) => {
return fs.writeFileSync(path.join(...args), data);
};
/**
* This function takes a string and encrypts it using the
* aes-256-cbc algorithm. It returns a base64 encoded string
* containing the encrypted data, the initialization vector
* and the authentication tag.
*/
const encrypt = (text: string, key: string) => {
const iv = Buffer.from(crypto.randomBytes(16)).toString("hex").slice(0, 16);
const cipher = crypto.createCipheriv("aes-256-cbc", Buffer.from(key), iv);
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
return `${iv}:${encrypted.toString("hex")}`;
};
/**
* This function takes a base64 encoded string
* containing the encrypted data, the initialization
* vector and the authentication tag and decrypts it
* using the aes-256-cbc algorithm.
*/
const decrypt = (text: string, key: string) => {
const textParts = text.includes(":") ? text.split(":") : [];
const iv = Buffer.from(textParts.shift() || "", "binary");
const encryptedtext = Buffer.from(textParts.join(":"), "hex");
const decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.from(key), iv);
return Buffer.concat([decipher.update(encryptedtext), decipher.final()]).toString();
};

View File

@ -0,0 +1,85 @@
import fs from "fs";
import path from "path";
import { AdapterConstructorOptions, StorageAdapter } from ".";
export class SimpleFIFO {
elements: any[] = [];
push(...args: any[]) {
this.elements.push(...args);
}
shift() {
return this.elements.shift();
}
length() {
return this.elements.length;
}
}
export default class FSAdapter<T> implements StorageAdapter<T> {
storagePath: string;
name: string;
filePath: string;
queue: SimpleFIFO;
constructor({ storagePath, name }: AdapterConstructorOptions<T>) {
if (!name.endsWith(".json")) {
name += ".json";
}
this.storagePath = storagePath;
this.name = name;
this.queue = new SimpleFIFO();
this.filePath = path.join(this.storagePath, this.name);
this.prepareStorage();
}
prepareStorage() {
if (!fs.existsSync(this.storagePath)) {
fs.mkdirSync(this.storagePath);
}
if (!fs.existsSync(this.filePath)) {
fs.writeFileSync(this.filePath, JSON.stringify({}));
}
}
read(): { [key: string]: T } {
try {
return Object.assign(
{},
JSON.parse(fs.readFileSync(this.filePath, "utf8")) || {},
);
} catch (e) {
return {};
}
}
write(data: { [key: string]: T }) {
this.queue.push([
(d: { [key: string]: T }) => {
writeJSON(d, this.filePath);
},
data,
]);
while (this.queue.length()) {
const ar = this.queue.shift();
ar[0](ar[1]);
}
}
}
function writeJSON(data: any, ...args: any[]) {
const env = process.env.NODE_ENV || "development";
const indent = env === "development" ? 2 : 0;
const out = JSON.stringify(data, null, indent);
return write(out, ...args);
}
function write(data: any, ...args: any[]) {
const pth = path.join(...args);
return fs.writeFileSync(pth, data);
}

View File

@ -0,0 +1,14 @@
export interface AdapterConstructor<T> {
new ({ storagePath, name, key }: AdapterConstructorOptions<T>): StorageAdapter<T>;
}
export type AdapterConstructorOptions<T> = {
storagePath: string;
name?: string;
key?: string;
}
export interface StorageAdapter<T> {
read: () => { [key: string]: T };
write: (data: { [key: string]: T }) => any;
}

View File

@ -0,0 +1,27 @@
import { AdapterConstructorOptions, StorageAdapter } from ".";
export default class LocalStorageAdapter<T> implements StorageAdapter<T> {
storageKey: string;
constructor({ storagePath }: AdapterConstructorOptions<T>) {
this.storageKey = `arc_${storagePath}`;
}
read(): { [key: string]: T } {
try {
return Object.assign(
{},
JSON.parse(
localStorage.getItem(`arc_${this.storageKey}`) || "{}"
)
);
} catch (e) {
console.error(`arc: failed to read from key: ${this.storageKey}: ${e}`);
return {};
}
}
write(data: { [key: string]: T }) {
localStorage.setItem(this.storageKey, JSON.stringify(data));
}
}

View File

@ -0,0 +1,55 @@
import _ from "lodash";
import { checkAgainstQuery } from "./return_found";
import { isEmptyObject, isObject, Ok } from "./utils";
/**
* Appends newProps to objects and arrays within source if they match the query.
* @param source - object or array of objects to append properties to
* @param query - object with properties to match against
* @param newProps - properties to append to matching objects
* @param merge - whether to merge matching objects with newProps instead of replacing
* @returns source with newProps appended to matching objects
*/
export function appendProps(source: any, query: object, newProps: any, merge = false) {
// If source is undefined, return undefined
if (source === undefined) return undefined;
/**
* Recursively processes objects to append newProps to matching objects
* @param item - object or array to process
* @returns object or array with newProps appended to matching objects
*/
const processObject = (item: any) => {
// If item is not an object or array, return it as is
if (!isObject(item)) return item;
// Clone the item to avoid modifying the original
const clone = _.cloneDeep(item);
// If the clone matches the query, append or merge newProps
if (checkAgainstQuery(clone, query)) {
if (!merge) {
Object.assign(clone, newProps);
} else {
_.merge(clone, newProps);
}
}
// Recursively process child objects and arrays
for (const key of Ok(clone)) {
if (isObject(clone[key]) || Array.isArray(clone[key])) {
clone[key] = processObject(clone[key]);
}
}
return clone;
};
// If source is an array or object and query and newProps are not empty, process source
if ((Array.isArray(source) || isObject(source)) && !isEmptyObject(query) && !isEmptyObject(newProps)) {
return Array.isArray(source) ? source.map(processObject) : processObject(source);
}
// Otherwise, return source as is
return source;
}

View File

@ -0,0 +1,67 @@
import _ from "lodash";
import { checkAgainstQuery } from "./return_found";
import { isEmptyObject, isObject, Ok, safeHasOwnProperty } from "./utils";
/**
* Recursively replaces properties of a source object or array based on a query object.
*
* @param source - The object or array to process.
* @param query - The properties to match.
* @param replaceProps - The replacement properties.
* @param createNewProperties - Whether to create new properties if they don't exist.
* @returns The processed object or array.
*/
export const changeProps = <T>(
source: T,
query: Partial<T>,
replaceProps: Partial<T>,
createNewProperties = false
): T | undefined => {
if (!source) return undefined;
// helper function to process objects and arrays recursively
const processObject = (item: any) => {
// if item is not an object, return item
if (!isObject(item)) return item;
// create a clone of the item
const itemClone = _.cloneDeep(item);
// loop through replaceProps object keys
for (const key of Ok(replaceProps)) {
// if itemClone matches query and createNewProperties is true or the key already exists in itemClone
if (checkAgainstQuery(itemClone, query) &&
(createNewProperties || safeHasOwnProperty(itemClone, key))) {
// update the itemClone key with the new value from replaceProps
itemClone[key] = replaceProps[key];
}
}
// loop through itemClone keys
for (const key of Ok(itemClone)) {
// if the value of the key is an object or an array, call processObject recursively
if (isObject(itemClone[key]) || Array.isArray(itemClone[key])) {
itemClone[key] = changeProps(
itemClone[key],
query,
replaceProps,
createNewProperties
);
}
}
// return the updated itemClone
return itemClone;
};
// if source is an object and both query and replaceProps are not empty objects, call processObject
if (isObject(source) && !isEmptyObject(query) && !isEmptyObject(replaceProps)) {
return processObject(source);
// if source is an array and both query and replaceProps are not empty objects, map through the array and call processObject on each item
} else if (Array.isArray(source) && !isEmptyObject(query) && !isEmptyObject(replaceProps)) {
return source.map(processObject) as unknown as T;
// otherwise, return the original source
} else {
return source;
}
};

View File

@ -0,0 +1,528 @@
import dot from "dot-wild";
import find from "./find";
import { booleanOperators } from "./operators";
import { Transaction } from "./transaction";
import { update } from "./update";
import { deeplyRemoveEmptyObjects, isEmptyObject, isObject, Ok } from "./utils";
import { getCreateId } from "./ids";
import type { StorageAdapter } from "./adapter";
export type CollectionOptions<T> = Partial<{
/** When true, automatically syncs to disk when a change is made to the database. */
autosync: boolean;
/** When true, automatically adds timestamps to all records. */
timestamps: boolean;
/** When true, document ids are integers that increment from 0. */
integerIds: boolean;
/** The storage adapter to use. By default, uses a filesystem adapter. */
adapter: StorageAdapter<T>;
}>;
export type CreateIndexOptions = Partial<{
key: string;
unique: boolean;
}>;
export type QueryOptions = Partial<{
/** When true, attempts to deeply match the query against documents. */
deep: boolean;
/** Specifies the key to return by. */
returnKey: string;
/** When true, returns cloned data (not a reference). default true */
clonedData: boolean;
/** Provide fallback values for null or undefined properties */
ifNull: Record<string, any>;
/** Provide fallback values for 'empty' properties ([], {}, "") */
ifEmpty: Record<string, any>;
/** Provide fallback values for null, undefined, or 'empty' properties. */
ifNullOrEmpty: Record<string, any>;
/**
* -1 || 0: descending
* 1: ascending
*/
sort: { [property: string]: -1 | 0 | 1 };
/**
* Particularly useful when sorting, `skip` defines the number of documents
* to ignore from the beginning of the result set.
*/
skip: number;
/** Determines the number of documents returned. */
take: number;
/**
* 1: property included in result document
* 0: property excluded from result document
*/
project: {
[property: string]: 0 | 1;
};
aggregate: {
[property: string]:
Record<"$floor", string> |
Record<"$ceil", string> |
Record<"$sub", (string|number)[]> |
Record<"$mult", (string|number)[]> |
Record<"$div", (string|number)[]> |
Record<"$add", (string|number)[]> |
Record<"$fn", (document) => unknown>;
};
join: Array<{
/** The collection to join on. */
collection: Collection<any>;
/** The property containing the foreign key(s). */
from: string;
/** The property on the joining collection that the foreign key should point to. */
on: string;
/** The name of the property to be created while will contain the joined documents. */
as: string;
/** QueryOptions that will be applied to the joined collection. */
options?: QueryOptions;
}>;
}>;
export function defaultQueryOptions(): QueryOptions {
return {
deep: true,
returnKey: ID_KEY,
clonedData: true,
sort: undefined,
skip: undefined,
project: undefined,
};
}
// before inserting, strip any boolean modifiers from the query, e.g.
// { name: "Jean-Luc", title: { $oneOf: ["Captain", "Commander"] } }
// becomes
// { name: "Jean-Luc" }.
export function stripBooleanModifiers(query: object): object {
const ops = new Set(Ok(booleanOperators));
const stripObject = (obj: object): object => {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (isObject(value)) {
const stripped = stripObject(value);
if (!isEmptyObject(stripped)) {
acc[key] = stripped;
}
} else if (!ops.has(key)) {
acc[key] = value;
}
return acc;
}, {});
};
return deeplyRemoveEmptyObjects(stripObject(query));
}
export let ID_KEY = "_id";
export let CREATED_AT_KEY = "_created_at";
export let UPDATED_AT_KEY = "_updated_at";
export type InternalData = {
current: number;
next_id: number;
id_map: { [id: string]: string };
index: {
valuesToId: { [key: string]: { [value: string]: string[] } };
idToValues: { [key: string]: { [cuid: string]: string | number } };
};
};
export type CollectionData = {
[key: string]: any;
internal?: InternalData;
};
const isValidIndexValue = (value: unknown) =>
value !== undefined && (typeof value === "string" || typeof value === "number" || typeof value === "boolean");
export class Collection<T> {
options: CollectionOptions<T>;
data: CollectionData = {};
_transaction: Transaction<T> = null;
indices: { [key: string]: { unique: boolean } } = {};
createId: () => string;
constructor(
options: CollectionOptions<T> = {}
) {
options.autosync = options.autosync ?? true;
options.timestamps = options.timestamps ?? true;
options.integerIds = options.integerIds ?? false;
if (!options.adapter) {
throw new Error("No adapter provided.");
}
this.options = options;
const defaultPrivateData = (): InternalData => ({
current: 0,
next_id: 0,
id_map: {},
index: {
valuesToId: {},
idToValues: {},
},
});
this.adapterRead();
// Ensure we have the internal map after adapter read.
if (!this.data.internal) {
this.data.internal = defaultPrivateData();
}
this.createId = getCreateId({ init: this.data.internal.current, len: 4 });
}
static from<Y>(data: CollectionData = {}, options: CollectionOptions<Y> = {}) {
const c = new Collection<Y>({
adapter: { read: () => ({} as any), write: () => {} },
autosync: false,
timestamps: false,
...options,
});
c.insert(data as any);
const initial = c.data;
c.adapterRead = () => { c.data = initial; };
return c;
}
adapterRead() {
this.data = this.options.adapter.read();
}
/**
* Given objects found by a query, assign `document` directly to these objects.
* Does not add timestamps or anything else.
* Used by transaction update rollback.
*/
assign(id: unknown, document: T): T {
if (id === undefined) return;
if (this.options.integerIds) {
const intid = id as number;
const cuid = this.data.internal.id_map[intid];
if (cuid) {
this.data[cuid] = document;
return this.data[cuid];
}
// a cuid wasn't found, so this is a new record.
return this.insert({ ...document, [ID_KEY]: intid })[0];
}
if (typeof id === "string" || typeof id === "number") {
this.data[id] = document;
return this.data[id];
}
return undefined;
}
filter(fn: (document: T) => boolean): T[] {
const _data = Object.assign({}, this.data);
delete _data.internal;
return Object.values(_data).filter((doc: T) => {
try { return fn(doc); }
catch (e) { return false; }
});
}
find(query?: object, options: QueryOptions = {}): T[] {
return find<T>(this.data, query, options, this.options, this);
}
update(query: object, operations: object, options: QueryOptions = {}): T[] {
return update<T>(this.data, query, operations, options, this.options, this);
}
upsert(query: object, operations: object, options: QueryOptions = {}): T[] {
const updated = this.update(query, operations, options);
if (updated.length) {
return updated;
}
// Nothing was updated.
// The idea is that we don't want the created document to be { name: "Jean-Luc", age: { $gt: 40 }, title: "Captain" },
// instead, it should be: { name: "Jean-Luc", title: "Captain" }
query = stripBooleanModifiers(query);
const inserted = this.insert(query as any);
return update<T>(
inserted,
query,
operations,
options,
this.options,
this
);
}
remove(query: object, options: QueryOptions = {}): T[] {
const found = this.find(query, { ...options, clonedData: false });
// Copy the found array so we can return unmodified data.
const cloned = found.map((doc) => Object.assign({}, doc));
found.forEach((document) => {
let cuid: string;
if (this.options.integerIds) {
const intid = document[ID_KEY];
cuid = this.data.internal.id_map[intid];
delete this.data.internal.id_map[intid];
} else {
cuid = document[ID_KEY];
}
Object.keys(this.indices).forEach((key) => {
const value = dot.get(document, key);
if (isValidIndexValue(value)) {
this.data.internal.index.valuesToId[key][value] = this.data.internal.index.valuesToId[key][value].filter((c) => c !== cuid);
if (this.data.internal.index.valuesToId[key][value].length === 0) {
delete this.data.internal.index.valuesToId[key][value];
}
delete this.data.internal.index.idToValues[cuid];
}
if (value === undefined) {
// This is a bit annoying, but it needs to be done.
// If the value for this document's indexed property is undefined,
// it might have been removed accidentally by an update mutation or something.
// We need to make sure we clean up any dangling indexes.
Object.keys(this.data.internal.index.valuesToId[key]).forEach((value) => {
this.data.internal.index.valuesToId[key][value] = this.data.internal.index.valuesToId[key][value].filter((c) => c !== cuid);
if (this.data.internal.index.valuesToId[key][value].length === 0) {
delete this.data.internal.index.valuesToId[key][value];
}
});
}
});
delete this.data[cuid];
});
this.sync();
return cloned;
}
insert(documents: T[] | T): T[] {
if (!Array.isArray(documents)) documents = [documents];
if (!documents.length) return [];
documents = documents.map((document) => {
const cuid = this.getId();
this.data.internal.current++;
if (this.options.timestamps) {
document[CREATED_AT_KEY] = Date.now();
document[UPDATED_AT_KEY] = Date.now();
}
// only assign an id if it's not already there
// support explicit ids, e.g.: { _id: 0, ... }
if (document[ID_KEY] === undefined) {
document[ID_KEY] = cuid;
if (this.options.integerIds) {
const intid = this.nextIntegerId();
this.data.internal.id_map[intid] = cuid;
document[ID_KEY] = intid;
}
}
this.data[cuid] = document;
Object.keys(this.indices).forEach((key) => {
const value = String(dot.get(document, key));
if (isValidIndexValue(value)) {
if (this.indices[key].unique) {
if (this.data.internal.index.valuesToId?.[key]?.[value] !== undefined) {
throw new Error(`Unique index violation for key "${key}" and value "${value}"`);
}
}
this.data.internal.index.valuesToId[key] = this.data.internal.index.valuesToId[key] || {};
this.data.internal.index.valuesToId[key][value] = this.data.internal.index.valuesToId[key][value] || [];
this.data.internal.index.valuesToId[key][value].push(cuid);
this.data.internal.index.idToValues[cuid] = this.data.internal.index.idToValues[cuid] || {};
this.data.internal.index.idToValues[cuid][key] = value;
}
});
return document;
});
if (this.options.autosync) {
this.sync();
}
return documents;
}
merge(id: string, item: T) {
if (!id) return;
/**
* When merging a document, if we're using integer ids,
* grab the cuid from the id map.
*/
if (this.options.integerIds) {
const cuid = this.data.internal.id_map[id];
if (!cuid) return;
if (this.data[cuid] === undefined) return;
Object.assign(this.data[cuid], isEmptyObject(item) ? {} : item);
this.sync();
return;
}
/**
* Otherwise, the id is assumed to be a cuid.
*/
if (this.data[id] === undefined) return;
Object.assign(this.data[id], isEmptyObject(item) ? {} : item);
this.sync();
}
sync() {
return this.options.adapter.write(this.data);
}
drop() {
this.data = {
internal: {
current: 0,
next_id: 0,
id_map: {},
index: {
valuesToId: {},
idToValues: {},
},
},
};
}
getId() {
return this.createId();
}
createIndex(options: CreateIndexOptions = {}) {
if (!options.key) throw new Error(`createIndex requires a key`);
options = {
key: options.key,
unique: options.unique ?? false,
};
const { key, unique } = options;
if (key.split(".").some((k) => !isNaN(Number(k)))) {
throw new Error(`Cannot use a numeric property as an index key: ${key}`);
}
this.indices[key] = { unique };
if (this.data.internal.index.valuesToId[key]) {
return;
}
Object.keys(this.data).forEach((cuid) => {
if (cuid === "internal") return;
const value = String(dot.get(this.data[cuid], key));
/* const value = String(key.split(".").reduce((acc, k) => acc[k], this.data[cuid])); */
if (isValidIndexValue(value)) {
if (unique) {
if (this.data.internal.index.valuesToId?.[key]?.[value] !== undefined) {
throw new Error(`Unique index violation for key "${key}" and value "${value}"`);
}
}
this.data.internal.index.valuesToId[key] = this.data.internal.index.valuesToId[key] || {};
this.data.internal.index.valuesToId[key][value] = this.data.internal.index.valuesToId[key][value] || [];
this.data.internal.index.valuesToId[key][value].push(cuid);
this.data.internal.index.idToValues[cuid] = this.data.internal.index.idToValues[cuid] || {};
this.data.internal.index.idToValues[cuid][key] = value;
} else {
throw new Error(`Invalid index value for property ${key}: ${value}`);
}
});
this.sync();
return this;
}
removeIndex(key: string): boolean {
if (!this.indices[key]) return false;
delete this.indices[key];
if (this.data.internal.index.valuesToId[key]) {
delete this.data.internal.index.valuesToId[key];
}
Object.keys(this.data.internal.index.idToValues).forEach((cuid) => {
if (this.data.internal.index.idToValues[cuid][key] !== undefined) {
delete this.data.internal.index.idToValues[cuid][key];
}
if (isEmptyObject(this.data.internal.index.idToValues[cuid])) {
delete this.data.internal.index.idToValues[cuid];
}
});
this.sync();
return true;
}
nextIntegerId() {
return this.data.internal.next_id++;
}
transaction(fn: (transaction: Transaction<T>) => void): void {
this._transaction = new Transaction<T>(this);
try {
fn(this._transaction);
} catch (e) {
this._transaction.rollback();
throw e;
}
this._transaction.commit();
}
}

121
packages/arc/src/find.ts Normal file
View File

@ -0,0 +1,121 @@
import _ from "lodash";
import dot from "dot-wild";
import {
Collection,
CollectionData,
CollectionOptions,
defaultQueryOptions,
ID_KEY,
QueryOptions,
stripBooleanModifiers,
} from "./collection";
import { applyQueryOptions } from "./query_options";
import { returnFound } from "./return_found";
import { ensureArray, isObject, Ok, Ov } from "./utils";
const makeDistinctByKey = (arr: any[], key: string) => {
const map = new Map();
let val: any;
arr = ensureArray(arr);
return arr.filter((el) => {
if (el === undefined) return;
val = map.get(el[key]);
if (val !== undefined) {
return false;
}
map.set(el[key], true);
return true;
});
};
export default function find<T>(
data: CollectionData,
query: any,
options: QueryOptions,
collectionOptions: CollectionOptions<T>,
collection: Collection<T>
): T[] {
options = { ...defaultQueryOptions(), ...options };
query = ensureArray(query);
// remove any empty objects from the query.
query = query.filter((q: object) => Ok(q).length > 0);
// if there's no query, return all data.
if (!query.length) {
if (options.clonedData) {
const out = [];
for (const key in data) {
if (key === "internal") continue;
out.push(_.cloneDeep(data[key]));
}
return applyQueryOptions(out, options);
}
return applyQueryOptions([...Ov(data)], options);
}
const withoutPrivate = [...Ov(data)].slice(1);
let res = [];
for (const q of query) {
let r = [];
if (q[ID_KEY] && !isObject(q[ID_KEY]) && !collectionOptions.integerIds) {
r.push(data[q[ID_KEY]]);
} else if (
q[ID_KEY] &&
!isObject(q[ID_KEY]) &&
collectionOptions.integerIds
) {
const f = data.internal.id_map[q[ID_KEY]];
// If we have `f`, it's a cuid.
if (f) r.push(data[f]);
} else {
const strippedQuery = stripBooleanModifiers(_.cloneDeep(q));
const flattened = Object.fromEntries(
Object.entries(dot.flatten(strippedQuery)).map(([k, v]) => [
k.replace(/\\./g, "."),
v,
])
);
if (Ok(flattened).some((key) => collection.indices[key])) {
Ok(collection.indices).forEach((key) => {
const queryPropertyValue = key.includes(".")
? flattened[key]
: q[key];
if (queryPropertyValue) {
const cuids =
data.internal.index.valuesToId?.[key]?.[queryPropertyValue];
if (cuids) {
const sourceItems = cuids?.map((cuid) => data[cuid]);
r.push(
...returnFound(sourceItems, q, options, collectionOptions)
);
} else {
r.push(...returnFound(withoutPrivate, q, options, collection));
}
}
});
} else {
r = returnFound(withoutPrivate, q, options, null);
if (r === undefined) r = [];
r = ensureArray(r);
}
}
res.push(...r);
}
const distinct = makeDistinctByKey(res, ID_KEY);
res = applyQueryOptions(distinct, options);
if (!options.clonedData) return res;
const cloned = [];
for (const obj of res) cloned.push(_.cloneDeep(obj));
return cloned;
}

66
packages/arc/src/ids.ts Normal file
View File

@ -0,0 +1,66 @@
/*
This is a mashup of github.com/lukeed/hexoid and github.com/paralleldrive/cuid
Both are MIT licensed.
~ https://github.com/paralleldrive/cuid/blob/f507d971a70da224d3eb447ed87ddbeb1b9fd097/LICENSE
--
MIT License
Copyright (c) 2012 Eric Elliott
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
~ https://github.com/lukeed/hexoid/blob/1070447cdc62d1780d2a657b0df64348fc1e5ec5/license
--
MIT License
Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const HEX: string[] = [];
for (let i = 0; i < 256; i++) {
HEX[i] = (i + 256).toString(16).substring(1);
}
function pad(str: string, size: number) {
const s = "000000" + str;
return s.substring(s.length - size);
}
const SHARD_COUNT = 32;
export function getCreateId(opts: { init: number; len: number }) {
const len = opts.len || 16;
let str = "";
let num = 0;
const discreteValues = 1_679_616; // Math.pow(36, 4)
let current = opts.init + Math.ceil(discreteValues / 2);
function counter() {
current = current <= discreteValues ? current : 0;
current++;
return (current - 1).toString(16);
}
return () => {
if (!str || num === 256) {
str = "";
num = ((1 + len) / 2) | 0;
while (num--) str += HEX[(256 * Math.random()) | 0];
str = str.substring((num = 0), len);
}
const date = Date.now().toString(36);
const paddedCounter = pad(counter(), 6);
const hex = HEX[num++];
const shardKey = parseInt(hex, 16) % SHARD_COUNT;
return `a${date}${paddedCounter}${hex}${str}${shardKey}`;
};
}

View File

@ -0,0 +1,6 @@
export { type CollectionOptions, type QueryOptions, Collection } from "./collection";
export { type ShardOptions, ShardedCollection } from "./sharded_collection";
export { type AdapterConstructor, type AdapterConstructorOptions, type StorageAdapter } from "./adapter";
export { default as FSAdapter } from "./adapter/fs";
export { default as EncryptedFSAdapter } from "./adapter/enc_fs";
export { default as LocalStorageAdapter } from "./adapter/localStorage";

View File

@ -0,0 +1,29 @@
import dot from "dot-wild";
import { ID_KEY } from "../../collection";
import { returnFound } from "../../return_found";
import { ensureArray, isObject } from "../../utils";
export function $and(source: object, query: object): boolean {
if (!isObject(query)) {
return true;
}
// @ts-ignore
const ands = ensureArray(query.$and);
if (!ands) {
return true;
}
return ands.every((and) => {
return Object.keys(and).every((key) => {
const value = and[key];
if (typeof value === "function") {
return value(dot.get(source, key));
} else {
const match = returnFound(source, { [key]: value }, { deep: true, returnKey: ID_KEY, clonedData: true }, source);
return Boolean(match && match.length);
}
});
});
}

View File

@ -0,0 +1,28 @@
import dot from "dot-wild";
import { ensureArray, isObject, Ok } from "../../utils";
export function $fn(source: object, query: object): boolean {
let match = undefined;
if (isObject(query)) {
Ok(query).forEach((k) => {
if (isObject(query[k])) {
const targetValue = dot.get(source, k);
if (targetValue === undefined) return;
Ok(query[k]).forEach((j) => {
if (j === "$fn") {
match = true;
ensureArray(query[k][j]).forEach((fn) => {
if (!fn(targetValue)) match = false;
});
}
});
}
});
}
if (match !== undefined) return match;
return false;
}

View File

@ -0,0 +1,63 @@
import dot from "dot-wild";
import { ensureArray } from "../../utils";
enum ComparisonOperator {
GreaterThan = "$gt",
GreaterThanEquals = "$gte",
LessThan = "$lt",
LessThanEquals = "$lte",
}
function match(source: object, query: object, operator: ComparisonOperator): boolean {
return Object.entries(query).map(([key, value]) => {
const qry = ensureArray(value[operator]);
const targetValue = dot.get(source, key);
if (targetValue === undefined) return false;
return qry.some(q => {
if (typeof targetValue === "string" || typeof targetValue === "number") {
switch (operator) {
case ComparisonOperator.GreaterThan:
return targetValue > q;
case ComparisonOperator.GreaterThanEquals:
return targetValue >= q;
case ComparisonOperator.LessThan:
return targetValue < q;
case ComparisonOperator.LessThanEquals:
return targetValue <= q;
default:
return false;
}
} else if (Array.isArray(targetValue)) {
switch (operator) {
case ComparisonOperator.GreaterThan:
return targetValue.length > q;
case ComparisonOperator.GreaterThanEquals:
return targetValue.length >= q;
case ComparisonOperator.LessThan:
return targetValue.length < q;
case ComparisonOperator.LessThanEquals:
return targetValue.length <= q;
default:
return false;
}
}
return false;
});
}).every(Boolean);
}
export function $gt(source: object, query: object): boolean {
return match(source, query, ComparisonOperator.GreaterThan);
}
export function $gte(source: object, query: object): boolean {
return match(source, query, ComparisonOperator.GreaterThanEquals);
}
export function $lt(source: object, query: object): boolean {
return match(source, query, ComparisonOperator.LessThan);
}
export function $lte(source: object, query: object): boolean {
return match(source, query, ComparisonOperator.LessThanEquals);
}

View File

@ -0,0 +1,30 @@
import { ensureArray, isObject, Ok } from "../../utils";
import dot from "dot-wild";
/**
* @example
* { $has: "a" } <-- source has property "a"
* { $has: ["a", "b"] } <-- source has properties "a" AND "b"
*
* @related
* $hasAny
* $not (e.g. { $not: { $has: "a" } })
*/
export function $has(source: object, query: object): boolean {
let match = false;
if (isObject(query)) {
Ok(query).forEach((k) => {
if (k !== "$has") return;
let qry = query[k];
qry = ensureArray(qry);
match = qry.every((q: any) => {
return dot.get(source, q) !== undefined;
});
});
}
return match;
}

View File

@ -0,0 +1,18 @@
import dot from "dot-wild";
import { ensureArray, isObject, Ok } from "../../utils";
/**
* @example
* { $hasAny: "a" } <-- source has property "a"
* { $hasAny: ["a", "b"] } <-- source has properties "a" OR "b"
*
* @related
* $has
* $not
* { $not: { $hasAny: "a" } })
* { $not: { "a.b.c.d": { $hasAny: "e" } } }
*/
export function $hasAny(source: object, query: object): boolean {
const queryValues = ensureArray(query["$hasAny"]);
return queryValues.some((q: any) => dot.get(source, q));
}

View File

@ -0,0 +1,26 @@
import dot from "dot-wild";
import { ensureArray, isObject, Ok } from "../../utils";
/**
* $includes does a simple .includes().
*
* @example
* { "foo": "bar" }, { "foo": "baz" }
* find({ "foo": { $includes: "ba" } })
*
* { "nums": [1, 2, 3] }, { "nums": [4, 5, 6] }
* find({ "nums": { $includes: 2 } })
* find({ "nums": { $includes: [1, 2, 3] } })
*
* find({ "a.b.c": { $includes: 1 } })
*/
export function $includes(source: object, query: object): boolean {
const matches = Object.entries(query)
.flatMap(([key, value]) => {
const includes = ensureArray(value.$includes);
return includes.map((v) => dot.get(source, key)?.includes(v));
})
.filter((match) => match !== undefined);
return matches.length > 0 && matches.every(Boolean);
}

View File

@ -0,0 +1,25 @@
import dot from "dot-wild";
import { isObject, Ok } from "../../utils";
/**
* $length asserts the length of an array or string.
*
* @example
* { "foo": [0, 0] }, { "foo": [0, 0, 0] }, { "foo": "abc" }
* find({ "foo": { $length: 3 } })
*/
export function $length(source: object, query: object): boolean {
if (!isObject(query)) {
return false;
}
return Object.entries(query).some(([k, qry]) => {
const targetValue = dot.get(source, k);
return (
targetValue !== undefined &&
(Array.isArray(targetValue) || typeof targetValue === "string") &&
targetValue.length === qry.$length
);
});
}

View File

@ -0,0 +1,35 @@
import { ID_KEY } from "../../collection";
import { returnFound } from "../../return_found";
import { ensureArray, isObject, Ok, safeHasOwnProperty } from "../../utils";
export function $not(source: object, query: object): boolean {
const matches = [];
if (isObject(query)) {
Ok(query).forEach((key) => {
if (key !== "$not") return;
if (!isObject(query[key])) {
throw new Error(`$not operator requires an object as its value, received: ${query[key]}`);
}
const nots = ensureArray(query[key]);
matches.push(
nots.every((not) => {
if (isObject(not)) {
const found = returnFound(source, not, { deep: true, returnKey: ID_KEY, clonedData: true }, source);
if (found && found.length) {
return false;
}
return true;
}
return !safeHasOwnProperty(source, not)
})
);
});
}
return matches.every((m) => !m);
}

View File

@ -0,0 +1,26 @@
import dot from "dot-wild";
import { ensureArray, isObject, Ok } from "../../utils";
/**
* @example
* { name: "Jean-Luc", friends: [1, 3, 4] }
* users.find({ _id: { $oneOf: [1, 3, 4] } })
* { a: b: { c: 1 } }
* find({ "a.b.c": { $oneOf: [1, 2] } })
*/
export function $oneOf(source: object, query: object): boolean {
const matches = [];
if (isObject(query)) {
Ok(query).forEach((k) => {
if (query[k]["$oneOf"] === undefined) { return; }
const values = ensureArray(query[k]["$oneOf"]);
const value = dot.get(source, k);
matches.push(values.includes(value));
});
}
if (!matches.length) return false;
if (matches.includes(false)) return false;
return true;
}

View File

@ -0,0 +1,28 @@
import dot from "dot-wild";
import { ID_KEY } from "../../collection";
import { returnFound } from "../../return_found";
import { ensureArray, isObject } from "../../utils";
export function $or(source: object, query: object): boolean {
if (!isObject(query)) return false;
// @ts-ignore
if (!query.$or) return false;
// @ts-ignore
const ors = ensureArray(query.$or);
for (const or of ors) {
const matches = [];
for (const [orKey, orValue] of Object.entries(or)) {
const sourceOrValue = dot.get(source, orKey);
if (typeof orValue === "function" && sourceOrValue !== undefined) {
matches.push(orValue(sourceOrValue));
} else {
const match = returnFound(source, or, { deep: true, returnKey: ID_KEY, clonedData: true }, source);
matches.push(Boolean(match && match.length));
}
}
if (matches.length && matches.includes(true)) return true;
}
return false;
}

View File

@ -0,0 +1,14 @@
import dot from "dot-wild";
import { ensureArray, isObject, Ok } from "../../utils";
export function $re(source: object, query: object): boolean {
if (!isObject(query)) return false;
return Ok(query).some(k => {
const targetValue = dot.get(source, k);
if (isObject(query[k]) && targetValue !== undefined && query[k].$re) {
return ensureArray(query[k].$re).every((re: RegExp) => re.test(targetValue));
}
return false;
});
}

View File

@ -0,0 +1,39 @@
import dot from "dot-wild";
import { ID_KEY } from "../../collection";
import { returnFound } from "../../return_found";
import { ensureArray, isObject } from "../../utils";
export function $xor(source: object, query: object): boolean {
if (!isObject(query)) {
return false;
}
// @ts-ignore
const xorQueries = ensureArray(query.$xor);
if (xorQueries.length !== 2) {
throw new Error(
`invalid $xor query. expected exactly two values, found ${xorQueries.length}.`
);
}
const matches = xorQueries.map((orQuery) => {
return Object.entries(orQuery).map(([key, value]) => {
const targetValue = dot.get(source, key) ?? source[key];
if (typeof value === "function") {
return targetValue !== undefined && value(targetValue);
} else {
const match = returnFound(source, orQuery, {
deep: true,
returnKey: ID_KEY,
clonedData: true
}, source);
return Boolean(match && match.length);
}
});
});
return matches.flat().filter((m) => m).length === 1;
}

View File

@ -0,0 +1,79 @@
import { Collection } from "../collection";
import { Ok } from "../utils";
import { $gt, $gte, $lt, $lte } from "./boolean/gtlt";
import { $and } from "./boolean/and";
import { $or } from "./boolean/or";
import { $xor } from "./boolean/xor";
import { $fn } from "./boolean/fn";
import { $re } from "./boolean/re";
import { $includes } from "./boolean/includes";
import { $oneOf } from "./boolean/oneOf";
import { $length } from "./boolean/length";
import { $not } from "./boolean/not";
import { $has } from "./boolean/has";
import { $hasAny } from "./boolean/hasAny";
import { $set } from "./mutation/set";
import { $unset } from "./mutation/unset";
import { $change } from "./mutation/change";
import { $inc, $dec, $mult, $div } from "./mutation/math";
import { $merge } from "./mutation/merge";
import { $map } from "./mutation/map";
import { $filter } from "./mutation/filter";
import { $push } from "./mutation/push";
import { $unshift } from "./mutation/unshift";
export const booleanOperators = {
$gt,
$gte,
$lt,
$lte,
$and,
$or,
$xor,
$includes,
$oneOf,
$fn,
$re,
$length,
$not,
$has,
$hasAny,
};
const mutationOperators = {
$merge,
$map,
$filter,
$push,
$unshift,
$set,
$unset,
$change,
$inc,
$dec,
$mult,
$div,
};
export function processMutationOperators<T>(
source: T[],
ops: object,
query: object,
collection: Collection<T>
): T[] {
Ok(ops).forEach((operator) => {
if (!mutationOperators[operator]) {
console.warn(`unknown operator: ${operator}`);
return;
}
source = mutationOperators[operator]<T>(
source,
ops[operator],
query,
collection
);
});
return source;
}

View File

@ -0,0 +1,43 @@
import dot from "dot-wild";
import { Collection } from "../..";
import { changeProps } from "../../change_props";
import { ensureArray, isObject, Ok, unescapedFlatten } from "../../utils";
export function $change<T>(
source: T[],
modifiers: any,
query: object,
collection: Collection<T>
): T[] {
const mods = ensureArray(modifiers);
mods.forEach((mod) => {
if (!isObject(mod) && !Array.isArray(mod)) {
const flattened = unescapedFlatten(query);
Ok(flattened).forEach((key) => {
source = source.map((doc: T) => {
if (dot.get(doc, key) !== undefined) return dot.set(doc, key, mod[key]);
return doc;
});
});
return;
}
if (isObject(mod)) {
Ok(mod).forEach((key) => {
source = source.map((doc: T) => {
if (dot.get(doc, key) !== undefined) return dot.set(doc, key, mod[key]);
return doc;
});
});
return;
}
source = changeProps(source, query as any, mod, false);
});
return source;
}

View File

@ -0,0 +1,48 @@
import dot from "dot-wild";
import { Collection, ID_KEY } from "../../collection";
import { isFunction, isObject } from "../../utils";
export function $filter<T>(
source: T[],
filterImpl: (document: T, index: number, source: T[]) => T | { [key: string]: (document: T, index: number, source: T[]) => any },
query: object,
collection: Collection<T>
): T[] {
if (isFunction(filterImpl)) {
return source.filter((document, index, source) => {
if (filterImpl(document, index, source)) {
return true;
}
if (document[ID_KEY]) {
collection.remove({ [ID_KEY]: document[ID_KEY] });
return false;
}
return false;
});
}
// { $filter: { anArray: (doc) => doc > 5 } }
if (isObject(filterImpl)) {
return source.map((document) => {
Object.keys(filterImpl).forEach((key) => {
const value = dot.get(document, key);
if (!Array.isArray(value)) {
throw new Error(`$filter when providing an object to filter on, the key being operated on in the source document must be an array: ${key} was not an array`);
}
const filtered = value.filter((document, index, source) => {
return filterImpl[key](document, index, source);
});
document = dot.set(document, key, filtered);
});
return document;
});
}
throw new Error("$filter expected either a function, e.g. { $filter: (doc) => doc }, or an array path with a function key, e.g. { $filter: { anArray: (doc) => doc } }");
}

View File

@ -0,0 +1,19 @@
import { Collection } from "../..";
import { appendProps } from "../../append_props";
import { ensureArray } from "../../utils";
export function $map<T>(
source: T[],
mapImpl: (document: T, index: number, source: T[]) => T,
query: object,
collection: Collection<T>
): T[] {
if (typeof mapImpl !== "function") {
throw new Error("$map expected a function, e.g. { $map: (doc) => doc }");
}
return source.map((document, index, source) => {
return mapImpl(document, index, source);
});
}

View File

@ -0,0 +1,221 @@
import dot from "dot-wild";
import { Collection } from "../../collection";
import { ensureArray, isObject, Ok, unescapedFlatten } from "../../utils";
enum Op {
Inc,
Dec,
Mult,
Div,
}
function math<T>(
source: T[],
modifiers: any,
query: object,
op: Op,
collection: Collection<T>
): T[] {
const mods = ensureArray(modifiers);
source = source.map((document) => {
mods.forEach((mod) => {
if (isObject(mod)) {
// update({ a: 1 }, { $inc: { visits: 1 } })
// update({ a: 1 }, { $inc: { a: { b: { c: 5 }}, "d.e.f": 5 } })
const flattened = unescapedFlatten(mod);
Ok(flattened).forEach((key) => {
const targetValue = dot.get(document, key);
const modValue = Number(mod[key]);
switch (op) {
case Op.Inc:
if (targetValue === undefined) {
document = dot.set(document, key, modValue);
} else {
document = dot.set(document, key, Number(targetValue) + modValue);
}
break;
case Op.Dec:
if (targetValue === undefined) {
document = dot.set(document, key, -modValue);
} else {
document = dot.set(document, key, Number(targetValue) - modValue);
}
break;
case Op.Mult:
if (targetValue === undefined) {
document = dot.set(document, key, modValue);
} else {
document = dot.set(document, key, Number(targetValue) * modValue);
}
break;
case Op.Div:
if (targetValue === undefined) {
document = dot.set(document, key, modValue);
} else {
document = dot.set(document, key, Number(targetValue) / modValue);
}
break;
}
});
} else if (typeof mod === "number") {
// When the modifier is a number, we increment all numeric
// fields that are in the provided query.
// update({ a: 1 }, { $inc: 1 }) -> { a: 2 }
// update({ a: 1, b: 1 }, { $inc: 1 }) -> { a: 2, b: 2 }
// update({ "a.b.c": 1 }, { $inc: 1 }) -> { a: { b: { c: 2 } } }
// update({ a: { b: { c: 1 } } }, { $inc: 1 }) -> { a: { b: { c: 2 } } }
// update({ "b.c": { $gt: 1 } }, { $inc: 1 }) -> { b: { c: 3 } }
let flattened = unescapedFlatten(query);
flattened = Object.keys(flattened).reduce((acc, key) => {
// "a.b.$has.c" => "a.b.c"
// "a.b.$has.0.c" => "a.b.c"
// "a.b.$hasAny.0.c" => "a.b.c"
//
// Useful for scenarios like:
// update({ planet: { name: "Earth", $has: "population" } }, { $inc: 1 })
//
if (key.match(/\.\$has\.\d+$/) || key.match(/\.\$hasAny\.\d+$/)) {
let hasValue = flattened[key];
hasValue = ensureArray(hasValue)
hasValue.forEach((v) => {
if (key.match(/\.\$hasAny\.\d+$/)) {
const val = dot.get(document, key.replace(/\.\$hasAny\.\d+$/, `.${v}`));
if (val !== undefined && typeof val === "number") {
acc[key.replace(/\.\$hasAny\.\d+$/, `.${v}`)] = val;
}
} else {
acc[key.replace(/\.\$has\.\d+$/, `.${v}`)] = dot.get(document, key.replace(/\.\$has\.\d+$/, `.${v}`)) ?? 0;
}
});
return acc;
}
if (key.match(/\$has/) || key.match(/\$hasAny/)) {
let hasValue = flattened[key];
hasValue = ensureArray(hasValue)
hasValue.forEach((v) => {
if (key.match(/\$hasAny/)) {
const val = dot.get(document, key.replace(/\.\$hasAny/, `.${v}`));
if (val !== undefined && typeof val === "number") {
acc[key.replace(/\.\$hasAny/, `.${v}`)] = val;
}
} else {
acc[key.replace(/\.\$has/, `.${v}`)] = dot.get(document, key.replace(/\.\$has/, `.${v}`));
}
});
return acc;
}
// "a.b.c.$gt" => "a.b.c", assumes we want to mutate the value of 'c'.
const removed = key.replace(/\.\$.*$/, "");
acc[removed] = flattened[key];
return acc;
}, {});
Ok(flattened).forEach((key) => {
const targetValue = dot.get(document, key);
// We only operate on properties that are either undefined or already a number.
if (targetValue !== undefined && typeof targetValue !== "number") {
return;
}
// It's possible that targetValue is undefined, for example
// if we deeply selected this document, e.g.
// Given document:
// { a: { b: { c: 1 } } }
// The operation:
// update({ c: 1 }, { $inc: 5 });
// Would find the above document, but create a new
// property `c` at the root level of the document:
// { a: { b: { c: 1 } }, c: 5 }
//
// To update the deep `c`, we'd do something like:
// update({ "a.b.c": 1 }, { $inc: 5 });
// or:
// update({ c: 1 }, { $inc: { "a.b.c": 5 } });
// or:
// update({ a: { b: { c: 1 } } }, { $inc: 5 });
switch (op) {
case Op.Inc:
if (targetValue === undefined) {
document = dot.set(document, key, mod);
} else {
document = dot.set(document, key, Number(targetValue) + mod);
}
break;
case Op.Dec:
if (targetValue === undefined) {
document = dot.set(document, key, -mod);
} else {
document = dot.set(document, key, Number(targetValue) - mod);
}
break;
case Op.Mult:
if (targetValue === undefined) {
document = dot.set(document, key, mod);
} else {
document = dot.set(document, key, Number(targetValue) * mod);
}
break;
case Op.Div:
if (targetValue === undefined) {
document = dot.set(document, key, mod);
} else {
document = dot.set(document, key, Number(targetValue) / mod);
}
break;
}
return;
});
}
});
return document;
});
return source;
}
export function $inc<T>(
source: T[],
modifiers: any,
query: object,
collection: Collection<T>
): T[] {
return math<T>(source, modifiers, query, Op.Inc, collection);
}
export function $dec<T>(
source: T[],
modifiers: any,
query: object,
collection: Collection<T>
): T[] {
return math<T>(source, modifiers, query, Op.Dec, collection);
}
export function $mult<T>(
source: T[],
modifiers: any,
query: object,
collection: Collection<T>
): T[] {
return math<T>(source, modifiers, query, Op.Mult, collection);
}
export function $div<T>(
source: T[],
modifiers: any,
query: object,
collection: Collection<T>
): T[] {
return math<T>(source, modifiers, query, Op.Div, collection);
}

View File

@ -0,0 +1,18 @@
import { Collection } from "../..";
import { appendProps } from "../../append_props";
import { ensureArray } from "../../utils";
export function $merge<T>(
source: T[],
modifiers: any,
query: object,
collection: Collection<T>
): T[] {
const mods = ensureArray(modifiers);
mods.forEach((mod) => {
source = appendProps(source, query, mod, true);
});
return source;
}

View File

@ -0,0 +1,30 @@
import dot from "dot-wild";
import { Collection } from "../..";
import { ensureArray, isObject } from "../../utils";
// { $push: { b: 2, c: 3 } }
// { $push: { b: [2, 3] } }
// { $push: { "a.b.c": 2 }}
// { $push: { "a.b.c": [2, 3] }}
export function $push<T>(source: T[], modifiers: any, query: object, collection: Collection<T>): T[] {
const mods = ensureArray(modifiers);
return mods.reduce((acc, mod) => {
if (isObject(mod)) {
return Object.keys(mod).reduce((docs, key) => {
return docs.map((doc) => {
const original = dot.get(doc, key);
const value = mod[key];
if (original !== undefined) {
const newValue = Array.isArray(value) ? original.concat(value) : original.concat([value]);
return dot.set(doc, key, newValue);
}
return doc;
});
}, acc);
}
return acc;
}, source);
}

View File

@ -0,0 +1,37 @@
import dot from "dot-wild";
import { Collection } from "../..";
import { changeProps } from "../../change_props";
import { ensureArray, isObject, Ok, unescapedFlatten } from "../../utils";
export function $set<T>(
source: T[],
modifiers: any,
query: object,
collection: Collection<T>
): T[] {
const mods = ensureArray(modifiers);
mods.forEach((mod) => {
if (!isObject(mod)) {
const flattened = unescapedFlatten(query);
Ok(flattened).forEach((key) => {
source = source.map((doc: T) => dot.set(doc, key, mod));
});
return;
}
if (isObject(mod)) {
Ok(mod).forEach((key) => {
source = source.map((doc: T) => dot.set(doc, key, mod[key]));
});
return;
}
source = changeProps(source, query, mod, true);
});
return source;
}

View File

@ -0,0 +1,26 @@
import dot from "dot-wild";
import { Collection } from "../..";
import { ensureArray } from "../../utils";
export function $unset<T>(
source: T[],
modifiers: any,
query: object,
collection: Collection<T>
): T[] {
const mods = ensureArray(modifiers);
mods.forEach((mod) => {
// { $unset: ["a", "b.c.d"] }
if (Array.isArray(mod)) {
return $unset(source, mod, query, collection);
}
// { $unset: "a" } or { $unset: "a.b.c" } or { $unset: "a.*.c" }
source = source.map((document) => {
return dot.set(document, mod, undefined);
});
});
return source;
}

View File

@ -0,0 +1,33 @@
import dot from "dot-wild";
import { Collection } from "../..";
import { ensureArray, isObject } from "../../utils";
// { $unshift: { b: 2, c: 3 } }
// { $unshift: { b: [2, 3] } }
// { $unshift: { "a.b.c": 2 }}
// { $unshift: { "a.b.c": [2, 3] }}
export function $unshift<T>(
source: T[],
modifiers: any,
query: object,
collection: Collection<T>
): T[] {
const mods = ensureArray(modifiers);
return mods.reduce((acc, mod) => {
if (isObject(mod)) {
Object.keys(mod).forEach((key) => {
acc = acc.map((doc: T) => {
const original = dot.get(doc, key);
if (original !== undefined) {
const value = mod[key];
const newValue = Array.isArray(value) ? value.concat(original) : [value].concat(original);
return dot.set(doc, key, newValue);
}
return doc;
});
});
}
return acc;
}, source);
}

View File

@ -0,0 +1,244 @@
import dot from "dot-wild";
import _ from "lodash";
import { QueryOptions } from "./collection";
import { ensureArray, Ok, Ov } from "./utils";
enum ProjectionMode {
Explicit = 0,
ImplicitExclusion = 1,
ImplicitInclusion = 2,
}
const getSortFunctions = (keys: string[]) =>
keys.map((key) => (item: any) => item[key]);
const getSortDirections = (nums: number[]) =>
nums.map((num) => num === 1 ? "asc" : "desc");
const applyAggregation = (data: any[], options: QueryOptions): any[] => {
const ops = {
$floor: (item: object, str: string) => {
const prop = dot.get(item, str);
if (typeof prop === "number") {
return Math.floor(prop);
}
return 0;
},
$ceil: (item: object, str: string) => {
const prop = dot.get(item, str);
if (typeof prop === "number") {
return Math.ceil(prop);
}
return 0;
},
$sub: (item: object, arr: (string|number)[]) => {
let res = undefined;
for (const a of arr) {
const val = typeof a === "number" ? a : Number(dot.get(item, a) ?? 0);
res = res === undefined ? val : res - val;
}
return res;
},
$add: (item: object, arr: (string|number)[]) => {
return arr.reduce((acc: number, val: number) => {
const numVal = Number(dot.get(item, val) ?? 0);
return typeof val === 'number'
? (acc === undefined ? val : acc + val)
: (acc === undefined ? numVal : acc + numVal);
}, undefined);
},
$mult: (item: object, arr: (string|number)[]) => {
return arr.reduce((res, a) => {
if (typeof a === "number") {
return Number(res) * a;
} else {
return Number(res) * (Number(dot.get(item, a)) || 1);
}
}, 1);
},
$div: (item: object, arr: (string|number)[]) => {
return arr.reduce((res: number | undefined, a: string | number) => {
const val = typeof a === 'number' ? a : Number(dot.get(item, a) ?? 1);
return res === undefined ? val : res / val;
}, undefined);
},
$fn: (item: object, fn: (i: any) => unknown) => {
return fn(item);
},
};
Ok(options.aggregate).forEach((key) => {
if (typeof options.aggregate[key] !== "object") return;
Ok(options.aggregate[key]).forEach((operation) => {
if (operation[0] !== "$") return;
if (!ops[operation]) return;
data = data.map((item) => {
item[key] = ops[operation](item, options.aggregate[key][operation]);
return item;
});
});
});
return data;
};
export const applyQueryOptions = (data: any[], options: QueryOptions): any => {
if (options.aggregate) {
data = applyAggregation(data, options);
}
// Apply projection after aggregation so that we have the opportunity to remove
// any intermediate properties that were used strictly in aggregation and should not
// be included in the result set.
if (options.project) {
// What is the projection mode?
// 1. Implicit exclusion: { a: 1, b: 1 }
// 2. Implicit inclusion: { a: 0, b: 0 }
// 3. Explicit: { a: 0, b: 1 }
const projectionTotal = Ok(options.project).reduce((acc, key) => {
if (typeof options.project[key] === "number" && typeof acc === "number") {
return acc + options.project[key];
}
}, 0);
const projectionMode =
projectionTotal === Ok(options.project).length
? ProjectionMode.ImplicitExclusion
: projectionTotal === 0
? ProjectionMode.ImplicitInclusion
: ProjectionMode.Explicit;
if (projectionMode === ProjectionMode.ImplicitExclusion) {
data = data.map((item) => _.pick(item, Ok(options.project)));
} else if (projectionMode === ProjectionMode.ImplicitInclusion) {
data = data.map((item) => _.omit(item, Ok(options.project)));
} else if (projectionMode === ProjectionMode.Explicit) {
const omit = Ok(options.project).filter((key) => options.project[key] === 0);
data = data.map((item) => _.omit(item, omit));
}
}
if (options.sort) {
data = _.orderBy(
data,
getSortFunctions(Ok(options.sort)),
getSortDirections(Ov(options.sort))
);
}
if (options.skip && typeof options.skip === "number") {
data = data.slice(options.skip);
}
if (options.take && typeof options.take === "number") {
data = data.slice(0, options.take);
}
const joinData = (data: any[], joinOptions: any[]) => {
return joinOptions.reduce((acc, join) => {
if (!join.collection) throw new Error("Missing required field in join: collection");
if (!join.from) throw new Error("Missing required field in join: from");
if (!join.on) throw new Error("Missing required field in join: on");
if (!join.as) throw new Error("Missing required field in join: as");
const qo = join.options || {};
const db = join.collection;
const tmp = db.createId();
const asDotStar = join.as.includes(".") && join.as.includes("*");
return acc.map((item) => {
if (!asDotStar) item = dot.set(item, join.as, ensureArray(dot.get(item, join.as)));
item[tmp] = [];
const from = join.from.includes(".") ? dot.get(item, join.from) : item[join.from];
if (from === undefined) return item;
item = dot.set(item, join.as, []);
if (Array.isArray(from)) {
from.forEach((key: unknown, index: number) => {
const query = { [`${join.on}`]: key };
if (asDotStar) {
item = dot.set(item, join.as.replaceAll("*", index.toString()), db.find(query, qo)[0]);
} else {
item[tmp] = item[tmp].concat(db.find(query, qo));
}
});
if (!asDotStar) {
item = dot.set(item, join.as, dot.get(item, join.as).concat(item[tmp]));
}
delete item[tmp];
return item;
}
const query = { [`${join.on}`]: from };
if (!asDotStar) {
item[tmp] = db.find(query, qo);
item = dot.set(item, join.as, dot.get(item, join.as).concat(item[tmp]));
}
delete item[tmp];
return item;
});
}, data);
};
if (options.join) {
data = joinData(data, options.join);
}
const ifNull = (item: any, opts: Record<string, any>) => {
for (const key in opts) {
const itemValue = dot.get(item, key);
if (itemValue === null || itemValue === undefined) {
if (typeof opts[key] === "function") {
item = dot.set(item, key, opts[key](item));
} else {
item = dot.set(item, key, opts[key]);
}
}
}
return item;
};
const ifEmpty = (item: any, opts: Record<string, any>) => {
const emptyCheckers = {
array: (value: any) => Array.isArray(value) && value.length === 0,
string: (value: any) => typeof value === "string" && value.trim().length === 0,
object: (value: any) => typeof value === "object" && Ok(value).length === 0,
};
return Object.entries(opts).reduce((result, [key, value]) => {
const itemValue = dot.get(item, key);
const isEmpty = Object.values(emptyCheckers).some((checker) => checker(itemValue));
if (isEmpty) {
const newValue = typeof value === "function" ? value(item) : value;
return dot.set(result, key, newValue);
}
return result;
}, item);
};
if (options.ifNull) {
data = data.map((item) => ifNull(item, options.ifNull));
}
if (options.ifEmpty) {
data = data.map((item) => ifEmpty(item, options.ifEmpty));
}
if (options.ifNullOrEmpty) {
return data
.map((item) => ifNull(item, options.ifNullOrEmpty))
.map((item) => ifEmpty(item, options.ifNullOrEmpty));
}
return data;
};

View File

@ -0,0 +1,174 @@
import { QueryOptions } from ".";
import dot from "dot-wild";
import { booleanOperators } from "./operators";
import {
ensureArray,
isEmptyObject,
isObject,
Ok,
safeHasOwnProperty,
} from "./utils";
export const checkAgainstQuery = (source: object, query: object): boolean => {
if (typeof source !== typeof query) return false;
const process = (src: object | object[], key: string) => {
if (src[key] === query[key]) return true;
let mods = [];
// Operators are sometimes a toplevel key:
// find({ $and: [{ a: 1 }, { b: 2 }] })
if (key.startsWith("$")) mods.push(key);
// Operators are sometimes a subkey:
// find({ number: { $gt: 100 } })
// $not is a special case: it calls `returnFound` so will handle subkey mods itself.
if (key !== "$not" && (isObject(query[key]) && !isEmptyObject(query[key]))) {
mods = mods.concat(Ok(query[key]).filter((k) => k.startsWith("$")));
}
if (mods.length) {
return mods.every((mod) => {
if (mod === "$not") {
return !booleanOperators[mod](src, query);
}
return booleanOperators[mod](src, query);
});
}
if (key.includes(".")) {
return dot.get(src, key) === query[key];
}
return (
safeHasOwnProperty(src, key) &&
checkAgainstQuery(src[key], query[key])
);
};
if (Array.isArray(source) && Array.isArray(query)) {
// if any item in source OR query is either an object or an array, return
// checkAgainstQuery(source, query) for each item in source and query
if (
source.some((item) => isObject(item) || Array.isArray(item)) ||
query.some((item) => isObject(item) || Array.isArray(item))
) {
return source.every((_, key) =>
checkAgainstQuery(source[key], query[key])
);
}
// otherwise stringify each item and compare equality
return [...source].map((i) => `${i}`).sort().join(",") === [...query].map((i) => `${i}`).sort().join(",");
}
if (Array.isArray(source) && isObject(query)) {
// supports e.g. [1, 2, 3], { $includes: 1 }
return Ok(query).every((key) => {
return process(source, key);
});
}
if (isObject(source) && isObject(query)) {
return Ok(query).every((key) => {
return process(source, key);
});
}
return source === query;
};
export const returnFound = (
source: any,
query: any,
options: QueryOptions,
parentDocument: object = null
): any[] | undefined => {
if (source === undefined) return undefined;
source["internal"] && delete source["internal"];
if (safeHasOwnProperty(source, options.returnKey)) {
parentDocument = source;
}
let result = undefined;
// If the query included mods, then we defer to the result of those mods
// to determine if we should return a document.
const queryHasMods = Ok(query).some((key) => key.startsWith("$"));
const appendResult = (item: object) => {
if (!item || isEmptyObject(item)) return;
result = ensureArray(result);
item = ensureArray(item);
// Ensure unique on returnKey
if (Array.isArray(result) && Array.isArray(item)) {
const resultIds = result.map((r) => r[options.returnKey]);
if (item.some((i) => resultIds.includes(i[options.returnKey]))) return;
}
result = result.concat(item);
};
const processObject = (item: object) => {
if (!item) return;
if (safeHasOwnProperty(item, options.returnKey)) parentDocument = item;
if (checkAgainstQuery(item, query)) {
appendResult(parentDocument);
} else {
if (options.deep && !queryHasMods) {
Ok(item).forEach((key) => {
// If key exists within the current query level, then use query[key] as the new
// query for item[key].
if (isObject(item[key]) || Array.isArray(item[key])) {
if (safeHasOwnProperty(query, key)) {
appendResult(returnFound(item[key], query[key], options, parentDocument));
} else {
appendResult(returnFound(item[key], query, options, parentDocument));
}
}
});
}
}
};
source = ensureArray(source);
if (isObject(query) && Array.isArray(source) && !queryHasMods) {
source.forEach((sourceObject, _index) => {
if (safeHasOwnProperty(sourceObject, options.returnKey)) {
parentDocument = sourceObject;
}
Ok(query).forEach((key) => {
if (typeof key === "string" && key.includes(".")) {
const sourceValue = dot.get(sourceObject, key);
if (sourceValue !== undefined) {
appendResult(returnFound(sourceValue, query[key], options, parentDocument));
}
} else {
if (isObject(sourceObject)) {
if (checkAgainstQuery(source[_index], query[key])) {
appendResult(parentDocument);
}
} else if (checkAgainstQuery(source, query[key])) {
appendResult(parentDocument);
}
}
});
});
}
if (!isEmptyObject(query) && Array.isArray(source)) {
source.forEach((item) => processObject(item));
} else {
return source;
}
return result;
};

View File

@ -0,0 +1,141 @@
import { AdapterConstructor, AdapterConstructorOptions } from "./adapter";
import { Collection, CollectionOptions, QueryOptions } from "./collection";
import { Ok } from "./utils";
export type ShardOptions<T> = {
shardKey: string;
shardCount: number;
adapter: AdapterConstructor<T>;
adapterOptions: AdapterConstructorOptions<T>;
};
export class ShardedCollection<T> {
private collectionOptions: CollectionOptions<T>;
public shards: { [key: string]: Collection<T> } = {};
private shardKey: string;
private shardCount: number;
private adapter: AdapterConstructor<T>;
private adapterOptions: AdapterConstructorOptions<T>;
constructor(
collectionOptions: CollectionOptions<T>,
shardOptions: ShardOptions<T>
) {
this.collectionOptions = collectionOptions;
this.shardKey = shardOptions.shardKey;
this.shardCount = shardOptions.shardCount;
this.adapter = shardOptions.adapter;
this.adapterOptions = shardOptions.adapterOptions;
}
private getShard(doc: T): Collection<T> {
const key = (doc as any)[this.shardKey];
if (key === undefined) {
throw new Error(`Shard key ${this.shardKey} is not found in document`);
}
const shardId = this.hashCode(key.toString()) % this.shardCount;
if (this.shards[shardId] === undefined) {
const adapterOptions = {
...this.adapterOptions,
name: `${
this.adapterOptions?.name || "collection"
}_shard${shardId}.json`,
};
this.shards[shardId] = new Collection<T>({
...this.collectionOptions,
adapter: new this.adapter(adapterOptions),
});
}
return this.shards[shardId];
}
private hashCode(str: string): number {
let hash = 0;
if (str.length === 0) return hash;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // convert to 32bit int
}
return hash;
}
find(query?: object, options: QueryOptions = {}): T[] {
const docs = [];
for (const shardId of Ok(this.shards)) {
const shardDocs = this.shards[shardId].find(query, options);
docs.push(...shardDocs);
}
return docs;
}
insert(docs: T[] | T): T[] {
if (!Array.isArray(docs)) {
docs = [docs];
}
const insertedDocs = [];
for (const doc of docs) {
const shard = this.getShard(doc);
insertedDocs.push(...shard.insert(doc));
}
return insertedDocs;
}
update(query: object, operations: object, options: QueryOptions = {}): T[] {
const updatedDocs = [];
for (const shardId of Ok(this.shards)) {
const shardDocs = this.shards[shardId].update(query, operations, options);
updatedDocs.push(...shardDocs);
}
return updatedDocs;
}
upsert(query: object, operations: object, options: QueryOptions = {}): T[] {
const upsertedDocs = [];
for (const shardId of Ok(this.shards)) {
const shardDocs = this.shards[shardId].upsert(query, operations, options);
upsertedDocs.push(...shardDocs);
}
return upsertedDocs;
}
remove(query: object, options: QueryOptions = {}): T[] {
const removedDocs = [];
for (const shardId of Ok(this.shards)) {
const shardDocs = this.shards[shardId].remove(query, options);
removedDocs.push(...shardDocs);
}
return removedDocs;
}
drop(): void {
for (const shardId of Ok(this.shards)) {
this.shards[shardId].drop();
}
}
sync(): void {
for (const shardId of Ok(this.shards)) {
this.shards[shardId].sync();
}
}
}

View File

@ -0,0 +1,124 @@
import { Collection, ID_KEY, QueryOptions } from "./collection";
enum OpType {
INSERT = "insert",
UPDATE = "update",
REMOVE = "remove"
};
interface UpdateOperation<T> {
documents: T[];
operations: object;
options: QueryOptions;
}
export class Transaction<T> {
collection: Collection<T>;
inserted: T[][] = [];
removed: T[][] = [];
updated: UpdateOperation<T>[] = [];
operations: OpType[] = [];
constructor(collection: Collection<T>) {
this.collection = collection;
}
insert(documents: T[] | T): T[] {
const inserted = this.collection.insert(documents);
this.inserted.push(inserted);
this.operations.push(OpType.INSERT);
return inserted;
}
update(query: object, operations: object, options: QueryOptions = {}): T[] {
// Given the query, find the documents without any projection or joining applied
// so we can store the original documents in the transaction.
const documents = this.collection.find(query, {
...options,
project: undefined,
join: undefined,
});
// Store the original documents in the transaction.
this.updated.push({ documents, operations, options });
this.operations.push(OpType.UPDATE);
// Then, run the update using the original query, operations, and options.
return this.collection.update(query, operations, options);
}
remove(query: object, options: QueryOptions = {}): T[] {
// Following similar logic to update, find the original documents
// using the query and options, but without any projection or joining.
const removed = this.collection.find(query, {
...options,
project: undefined,
join: undefined,
});
this.collection.remove(query, options);
this.removed.push(removed);
this.operations.push(OpType.REMOVE);
return removed;
}
rollback() {
const uninsert = (documents: T[]) => {
documents.forEach((document) => {
if (document[ID_KEY] !== undefined) {
this.collection.remove({ [ID_KEY]: document[ID_KEY] })
} else {
this.collection.remove({ ...document } as unknown as object);
}
});
};
const unupdate = (operation: UpdateOperation<T>) => {
operation.documents.forEach((document) => {
if (document[ID_KEY] !== undefined) {
this.collection.assign(document[ID_KEY], document);
} else {
this.collection.update({ ...document } as unknown as object, operation.operations, operation.options);
}
});
};
const unremove = (documents: T[]) => {
documents.forEach((document) => {
if (document[ID_KEY] !== undefined) {
this.collection.assign(document[ID_KEY], document);
}
});
};
this.operations.reverse().forEach((op) => {
switch (op) {
case OpType.INSERT:
uninsert(this.inserted.pop() as T[]);
break;
case OpType.UPDATE:
unupdate(this.updated.pop());
break;
case OpType.REMOVE:
unremove(this.removed.pop() as T[]);
break;
}
});
this.inserted = [];
this.updated = [];
this.removed = [];
this.operations = [];
}
/**
* Finalizes the transaction.
*/
commit() {
this.inserted = [];
this.updated = [];
this.removed = [];
this.collection._transaction = null;
}
}

View File

@ -0,0 +1,82 @@
import dot from "dot-wild";
import {
Collection,
CollectionOptions,
CollectionData,
defaultQueryOptions,
QueryOptions,
ID_KEY,
UPDATED_AT_KEY,
} from "./collection";
import { processMutationOperators } from "./operators";
import { applyQueryOptions } from "./query_options";
import { returnFound } from "./return_found";
import { ensureArray, Ok, Ov } from "./utils";
export function update<T>(
data: CollectionData,
query: any,
operations: object,
options: QueryOptions,
collectionOptions: CollectionOptions<T>,
collection: Collection<T>
): T[] {
options = { ...defaultQueryOptions(), ...options };
query = ensureArray(query);
const mutated = [];
for (const q of query) {
let itemsToMutate = [];
itemsToMutate = returnFound([...Ov(data)], q, options, null);
itemsToMutate = ensureArray(itemsToMutate);
mutated.push(...processMutationOperators(itemsToMutate, operations, q, collection));
}
/**
* If the returnKey is the default (_id), then the mutated items
* should be toplevel documents, meaning they'll have `_created_at`
* and `_updated_at` properties.
*
* This is where mutated items have their `_updated_at` properties updated.
*/
if (options.returnKey === ID_KEY && collectionOptions.timestamps) {
mutated.forEach((item) => {
let cuid: string;
if (collectionOptions.integerIds) {
const intid = item[ID_KEY];
cuid = data.internal.id_map[intid];
} else {
cuid = item[ID_KEY];
}
Ok(collection.indices).forEach((key) => {
if (!dot.get(item, key)) { return; }
const oldValue = data.internal.index.idToValues[cuid][key];
const newValue = String(dot.get(item, key));
if (oldValue === newValue) { return; }
data.internal.index.valuesToId[key][newValue] = data.internal.index.valuesToId[key][newValue] || [];
data.internal.index.valuesToId[key][newValue].push(cuid);
data.internal.index.valuesToId[key][oldValue] = data.internal.index.valuesToId[key][oldValue].filter((cuid) => cuid !== cuid);
if (data.internal.index.valuesToId[key][oldValue].length === 0) {
delete data.internal.index.valuesToId[key][oldValue];
}
data.internal.index.idToValues[cuid][key] = newValue;
});
item[UPDATED_AT_KEY] = Date.now();
collection.merge(item[ID_KEY], item);
});
}
// Apply query options to mutated results before returning them.
return applyQueryOptions(mutated, options);
}

57
packages/arc/src/utils.ts Normal file
View File

@ -0,0 +1,57 @@
import dot from "dot-wild";
export function ensureArray(input: any): any[] {
if (Array.isArray(input)) return input;
else if (input === undefined || input === null) return [];
else return [input];
};
export function isObject(item: any): item is object {
return !!item && Object.prototype.toString.call(item) === "[object Object]";
}
export function isEmptyObject(item: any) {
return isObject(item) && Ok(item).length === 0;
}
export const Ov = Object.values;
export const Ok = Object.keys;
export const safeHasOwnProperty = (obj: object, prop: string) =>
obj ? Object.prototype.hasOwnProperty.call(obj, prop) : false;
export function isFunction(item: any) {
return typeof item === "function";
}
/**
* Recursively removes empty objects from an object.
*
* @example
* ```
* { a: { b: 1, c: { d: { e: {} } } } }
* becomes
* { a: { b: 1 } }
* ```
*/
export function deeplyRemoveEmptyObjects(o: object) {
if (!isObject(o)) return o;
Ok(o).forEach((k) => {
if (!o[k] || !isObject(o[k])) return;
deeplyRemoveEmptyObjects(o[k]);
if (Ok(o[k]).length === 0) delete o[k];
});
return o;
}
export function unescapedFlatten(o: object) {
const flattened = dot.flatten(o);
return Ok(flattened).reduce((acc, key) => {
const unescapedKey = key.replace(/\\./g, ".");
acc[unescapedKey] = flattened[key];
return acc;
}, {});
}

View File

@ -0,0 +1,68 @@
import { Collection, CREATED_AT_KEY, ID_KEY, UPDATED_AT_KEY } from "../src/collection";
import EncryptedFSAdapter from "../src/adapter/enc_fs";
import FSAdapter from "../src/adapter/fs";
import { ShardedCollection } from "../src/sharded_collection";
const getCollection = <T>({ name = "test", integerIds = false, populate = true, timestamps = true }): Collection<T> => {
const collection = new Collection<T>({
autosync: false,
integerIds,
timestamps,
adapter: new FSAdapter({ storagePath: ".test", name }),
});
collection.drop();
if (populate) {
// Adding some items to ensure that result sets correctly
// ignore unmatched queries in all cases.
// @ts-ignore
collection.insert({ xxx: "xxx" });
// @ts-ignore
collection.insert({ yyy: "yyy" });
// @ts-ignore
collection.insert({ zzz: "zzz" });
}
return collection;
};
const getEncryptedCollection = <T>({ name = "test", integerIds = false }): Collection<T> => {
return new Collection<T>({
autosync: false,
integerIds,
adapter: new EncryptedFSAdapter({ storagePath: ".test", name }),
});
};
export function testCollection<T>({ name = "test", integerIds = false, populate = true, timestamps = true } = {}): Collection<T> {
return getCollection({ name, integerIds, populate, timestamps });
}
export function testCollectionEncrypted<T>({ name = "test", integerIds = false } = {}): Collection<T> {
return getEncryptedCollection({ name, integerIds });
}
export function getShardedCollection<T>({ name ="testShard", autosync = true, integerIds = false } = {}): ShardedCollection<T> {
return new ShardedCollection<T>(
{ autosync, integerIds },
{
shardKey: "key",
shardCount: 3,
adapter: FSAdapter,
adapterOptions: { name, storagePath: ".test" },
},
);
};
export function nrml<T>(results: T[], { keepIds = false } = {}): T[] {
// Remove all the _id fields, and
// remove all the `_created_at` and `_updated_at` fields.
return results.map((result) => {
if (!keepIds) {
delete result[ID_KEY];
}
delete result[CREATED_AT_KEY];
delete result[UPDATED_AT_KEY];
return result;
}); }

View File

@ -0,0 +1,58 @@
import { describe } from "manten";
await describe("find", async ({ runTestSuite }) => {
runTestSuite(import("./specs/finding"));
});
await describe("filter", async ({ runTestSuite }) => {
runTestSuite(import("./specs/filter"));
});
await describe("insert", async ({ runTestSuite }) => {
runTestSuite(import("./specs/insert"));
});
await describe("options", async ({ runTestSuite }) => {
runTestSuite(import("./specs/options"));
});
await describe("operators", ({ runTestSuite }) => {
runTestSuite(import("./specs/operators/boolean"));
runTestSuite(import("./specs/operators/mutation"));
});
await describe("upsert", ({ runTestSuite }) => {
runTestSuite(import("./specs/upsert"));
});
await describe("transactions", ({ runTestSuite }) => {
runTestSuite(import("./specs/transactions"));
});
await describe("remove", ({ runTestSuite }) => {
runTestSuite(import("./specs/remove"));
});
await describe("encrypted adapter", ({ runTestSuite }) => {
runTestSuite(import("./specs/encrypted_adapter"));
});
await describe("utils", ({ runTestSuite }) => {
runTestSuite(import("./specs/utils"));
});
await describe("indexes", ({ runTestSuite}) => {
runTestSuite(import("./specs/index.test.js"));
});
await describe("sharded collection", ({ runTestSuite }) => {
runTestSuite(import("./specs/sharded_collection"));
});
await describe("Collection.from", ({ runTestSuite }) => {
runTestSuite(import("./specs/from"));
});
await describe("core", ({ runTestSuite }) => {
runTestSuite(import("./specs/core"));
});

View File

@ -0,0 +1,69 @@
import { expect, testSuite } from "manten";
import { appendProps } from "../../../src/append_props";
export default testSuite(async ({ describe }) => {
describe("appendProps", ({ test }) => {
test("should append newProps to an object that matches the query", () => {
const source = { id: 1, name: "John" };
const query = { id: 1 };
const newProps = { age: 30 };
const result = appendProps(source, query, newProps);
expect(result).toEqual({ id: 1, name: "John", age: 30 });
});
test("should append newProps to objects in an array that match the query", () => {
const source = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
];
const query = { id: 1 };
const newProps = { age: 30 };
const result = appendProps(source, query, newProps);
expect(result).toEqual([
{ id: 1, name: "John", age: 30 },
{ id: 2, name: "Jane" },
]);
});
test("should merge newProps with matching objects when merge is true", () => {
const source = { id: 1, name: "John" };
const query = { id: 1 };
const newProps = { name: "Jonathan", age: 30 };
const result = appendProps(source, query, newProps, true);
expect(result).toEqual({ id: 1, name: "Jonathan", age: 30 });
});
test("should not modify non-matching objects", () => {
const source = { id: 2, name: "Jane" };
const query = { id: 1 };
const newProps = { age: 30 };
const result = appendProps(source, query, newProps);
expect(result).toEqual({ id: 2, name: "Jane" });
});
test("should return undefined if source is undefined", () => {
const result = appendProps(undefined, {}, {});
expect(result).toBeUndefined();
});
test("should not modify the source if query does not match", () => {
const source = { id: 1, name: "John" };
const query = { id: 2 };
const newProps = { age: 30 };
const result = appendProps(source, query, newProps);
expect(result).toEqual({ id: 1, name: "John" });
});
test("should handle nested objects", () => {
const source = { id: 1, name: "John", address: { city: "CityA" } };
const query = { city: "CityA" };
const newProps = { postalCode: "12345" };
const result = appendProps(source, query, newProps);
expect(result).toEqual({
id: 1,
name: "John",
address: { city: "CityA", postalCode: "12345" },
});
});
});
});

View File

@ -0,0 +1,112 @@
import { expect, testSuite } from "manten";
import { changeProps } from "../../../src/change_props";
export default testSuite(async ({ describe }) => {
describe("changeProps", ({ test }) => {
test("should return undefined for null source", () => {
expect(changeProps(null, {}, {})).toBeUndefined();
});
test("should not modify the source object if query does not match", () => {
const source = { name: "John", age: 30 };
const query = { name: "Jane" };
const replaceProps = { age: 25 };
expect(changeProps(source, query, replaceProps)).toEqual(source);
});
test("should modify the source object if query matches", () => {
const source = { name: "John", age: 30 };
const query = { name: "John" };
const replaceProps = { age: 25 };
expect(changeProps(source, query, replaceProps)).toEqual({
name: "John",
age: 25,
});
});
test("should add new properties if createNewProperties is true", () => {
const source = { name: "John" };
const query = { name: "John" };
const replaceProps = { age: 30 };
expect(changeProps(source, query, replaceProps as any, true)).toEqual({
name: "John",
age: 30,
});
});
test("should not add new properties if createNewProperties is false", () => {
const source = { name: "John" };
const query = { name: "John" };
const replaceProps = { age: 30 };
expect(changeProps(source, query, replaceProps as any)).toEqual({
name: "John",
});
});
test("should process arrays", () => {
const source = [
{ name: "John", age: 30 },
{ name: "Jane", age: 28 },
];
const query = { name: "John" };
const replaceProps = { age: 25 };
expect(changeProps(source, query as any, replaceProps as any)).toEqual([
{ name: "John", age: 25 },
{ name: "Jane", age: 28 },
]);
});
test("should handle nested structures, merging existing objects", () => {
const source = [
{
name: "John",
age: 30,
address: {
city: "New York",
foo: "bar",
},
},
];
const query = { city: "New York" };
const replaceProps = { city: "Los Angeles" };
expect(
changeProps(source, query as any, replaceProps as any)
).toEqual([
{
name: "John",
age: 30,
address: {
city: "Los Angeles",
foo: "bar",
},
},
]);
});
test("should handle nested structures, overwriting existing objects", () => {
const source = [
{
name: "John",
age: 30,
address: {
city: "New York",
foo: "bar",
},
},
];
const query = { address: { city: "New York" } };
const replaceProps = { address: { city: "Los Angeles" } };
expect(
changeProps(source, query as any, replaceProps as any)
).toEqual([
{
name: "John",
age: 30,
address: {
city: "Los Angeles",
},
},
]);
});
});
});

View File

@ -0,0 +1,10 @@
import { testSuite } from "manten";
export default testSuite(async ({ describe }) => {
describe("utils", async ({ runTestSuite }) => {
runTestSuite(import("./changeProps.test.js"));
runTestSuite(import("./appendProps.test.js"));
runTestSuite(import("./returnFound.test.js"));
});
});

View File

@ -0,0 +1,81 @@
import { expect, testSuite } from "manten";
import { returnFound } from "../../../src/return_found";
export default testSuite(async ({ describe }) => {
describe("returnFound", ({ test }) => {
test("should return undefined for undefined source", () => {
expect(returnFound(undefined, {}, { returnKey: "id" })).toBeUndefined();
});
test("should ignore internal properties", () => {
const source = { id: 1, internal: true };
const query = {};
const options = { returnKey: "id" };
expect(returnFound(source, query, options)).toEqual([{ id: 1 }]);
});
test("should return the document if it matches the query", () => {
const source = [{ id: 1, name: "Test" }];
const query = { name: "Test" };
const options = { returnKey: "id", deep: false };
expect(returnFound(source, query, options)).toEqual([
{ id: 1, name: "Test" },
]);
});
test("should return undefined if no items match the query", () => {
const source = [{ id: 1, name: "Test" }];
const query = { name: "Not Found" };
const options = { returnKey: "id" };
expect(returnFound(source, query, options)).toBeUndefined();
});
test("should handle nested objects with deep search in dot notation", () => {
const source = [{ id: 1, details: { name: "Nested" } }];
const query = { "details.name": "Nested" };
const options = { returnKey: "id", deep: true };
expect(returnFound(source, query, options)).toEqual([
{ id: 1, details: { name: "Nested" } },
]);
});
test("should handle nested objects with deep search without dot notation", () => {
const source = [{ id: 1, details: { name: "Nested" } }];
const query = { details: { name: "Nested" } };
const options = { returnKey: "id", deep: true };
expect(returnFound(source, query, options)).toEqual([
{ id: 1, details: { name: "Nested" } },
]);
});
test("should handle nested objects with deep search without dot notation or a fully-qualified path", () => {
const source = [{ id: 1, details: { name: "Nested" } }];
const query = { name: "Nested" };
const options = { returnKey: "id", deep: true };
expect(returnFound(source, query, options)).toEqual([
{ id: 1, details: { name: "Nested" } },
]);
});
test("should return unique items based on returnKey", () => {
const source = [
{ id: 1, name: "Duplicate" },
{ id: 1, name: "Duplicate" },
];
const query = { name: "Duplicate" };
const options = { returnKey: "id" };
expect(returnFound(source, query, options)).toEqual([
{ id: 1, name: "Duplicate" },
]);
});
test("should return concatenated results for array items", () => {
const source = [{ id: 1, items: [{ name: "Item1" }, { name: "Item2" }] }];
const query = { items: { name: "Item1" } };
const options = { returnKey: "id", deep: true };
expect(returnFound(source, query, options)).toEqual([
{ id: 1, items: [{ name: "Item1" }, { name: "Item2" }] },
]);
});
});
});

View File

@ -0,0 +1,32 @@
import { testSuite, expect } from "manten";
import { nrml, testCollectionEncrypted } from "../../common";
export default testSuite(async ({ test }) => {
test("can write", () => {
const collection = testCollectionEncrypted<{a: number}>({
name: "enc",
});
collection.drop();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
collection.sync();
expect(nrml(collection.find({ a: 1 }))).toEqual([{ a: 1 }]);
});
test("can read", () => {
const collection = testCollectionEncrypted<{a: number}>({
name: "enc",
});
const found = nrml(collection.find({ a: { $gt: 0 } }));
expect(found).toEqual([
{ a: 1 },
{ a: 2 },
{ a: 3 },
]);
});
});

View File

@ -0,0 +1,5 @@
import { testSuite } from "manten";
export default testSuite(async ({ runTestSuite }) => {
runTestSuite(import("./adapter.test.js"));
});

View File

@ -0,0 +1,26 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("filter", ({ test }) => {
test("works", () => {
const collection = testCollection<{a: number}>();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const found = nrml(collection.filter((doc) => doc.a > 1));
expect(found).toEqual([{ a: 2 }, { a: 3 }]);
});
test("works with nested properties", () => {
const collection = testCollection<{a: {b: number}}>();
collection.insert({ a: { b: 1 } });
collection.insert({ a: { b: 2 } });
collection.insert({ a: { b: 3 } });
const found = nrml(collection.filter((doc) => doc.a.b > 1));
expect(found).toEqual([{ a: { b: 2 } }, { a: { b: 3 } }]);
});
});
});

View File

@ -0,0 +1,7 @@
import { testSuite } from "manten";
export default testSuite(async ({ describe }) => {
describe("filter", async ({ runTestSuite }) => {
runTestSuite(import("./basic.test.js"));
});
});

View File

@ -0,0 +1,131 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("find", ({ test }) => {
test("no results should return an empty array", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const found = nrml(collection.find({ a: 4 }));
expect(found).toEqual([]);
});
test("empty find returns everything", () => {
const collection = testCollection();
collection.remove({ xxx: "xxx" });
collection.remove({ yyy: "yyy" });
collection.remove({ zzz: "zzz" });
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const found = nrml(collection.find({}));
expect(found).toEqual([{ a: 1 }, { a: 2 }, { a: 3 }]);
});
test("simple find", () => {
const collection = testCollection();
collection.insert({ foo: "bar" });
collection.insert({ foo: "baz" });
collection.insert({ foo: "boo" });
const found = nrml(collection.find({ foo: "bar" }));
expect(found).toEqual([{ foo: "bar" }]);
});
test("simple find, more criteria", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 2, c: 3 });
collection.insert({ a: 1, b: 2, c: 4 });
collection.insert({ a: 2, b: 3, c: 4 });
const found = nrml(collection.find({ a: 1, b: 2 }));
expect(found).toEqual([{ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 4 }]);
});
test("simple find - deep false", () => {
const collection = testCollection();
collection.insert({ foo: { bar: "bar" } });
collection.insert({ foo: { bar: "baz" } });
collection.insert({ foo: { bar: "boo" } });
const found = nrml(collection.find({ bar: { $includes: "ba" } }, { deep: false }));
expect(found).toEqual([]);
});
test("simple find - deep true", () => {
const collection = testCollection();
collection.insert({ foo: { bar: "baz" } });
collection.insert({ foo: { bar: "boo" } });
collection.insert({ foo: { bar: "baz" } });
const found = nrml(collection.find({ foo: { bar: "baz" } }));
expect(found).toEqual([{ foo: { bar: "baz" } }, { foo: { bar: "baz" } }]);
});
test("normal match if deep is false but toplevel matches", () => {
const collection = testCollection();
collection.insert({ foo: { bar: "bar" } });
collection.insert({ foo: { bar: "baz" } });
collection.insert({ foo: { bar: "boo" } });
const found = nrml(collection.find({ foo: { bar: "bar" } }, { deep: false }));
expect(found).toEqual([{ foo: { bar: "bar" } }]);
});
test("multilevel results", () => {
const collection = testCollection();
collection.insert({ bar: "baz" });
collection.insert({ foo: { bar: "boo" } });
collection.insert({ foo: { bar: "baz" } });
const found = nrml(collection.find({ foo: { bar: "baz" } }));
expect(found).toEqual([{ bar: "baz" }, { foo: { bar: "baz" } }]);
});
test("array literal", () => {
const collection = testCollection();
collection.insert({ foo: ["bar", "baz"] });
collection.insert({ foo: ["bar", "boo"] });
collection.insert({ foo: ["baz", "bar"] });
const found = nrml(collection.find({ foo: ["bar", "baz"] }));
expect(found).toEqual([{ foo: ["bar", "baz"] }, { foo: ["baz", "bar"] }]);
collection.insert({ nums: [1, 2, 3] });
collection.insert({ nums: [2, 3, 1] });
collection.insert({ nums: [1, 3, 5] });
const found2 = nrml(collection.find({ nums: [3, 2, 1] }));
expect(found2).toEqual([{ nums: [1, 2, 3] }, { nums: [2, 3, 1] }]);
});
test("array literal should exclude items that don't match the exact array", () => {
const collection = testCollection();
collection.insert({ foo: ["bar", 1] });
collection.insert({ foo: ["bar", 2] });
collection.insert({ foo: ["bar", 2, 2] });
collection.insert({ foo: ["bar", 3] });
collection.insert({ a: { b: { foo: ["bar", 2] } } });
const found = nrml(collection.find({ foo: ["bar", 2] }));
expect(found).toEqual([{ foo: ["bar", 2] }, { a: { b: { foo: ["bar", 2] } } }]);
});
test("find array using object syntax", () => {
const collection = testCollection();
collection.insert({ a: { b: [ {c: 1}, {c: 2}, {c: 3} ] } });
const found = nrml(collection.find({ b: { c: 2 } }));
expect(found).toEqual([{ a: { b: [ {c: 1}, {c: 2}, {c: 3} ] } }]);
});
test("multiple queries, merged result set", () => {
const collection = testCollection();
collection.insert({ x: { a: 1 } });
collection.insert({ y: { b: 1 } });
const found = nrml(collection.find([{ a: 1 }, { b: 1 }]));
expect(found).toEqual([{ x: { a: 1 } }, { y: { b: 1 } }]);
});
test("really deep specificity", () => {
const collection = testCollection();
collection.insert({ a: { b: { c: { d: { e: { f: { g: { h: { i: { j: { k: 1 } } } } } } } } } } });
const found = nrml(collection.find({ a: { b: { c: { d: { e: { f: { g: { h: { i: { j: { k: 1 } } } } } } } } } } }));
expect(found).toEqual([{ a: { b: { c: { d: { e: { f: { g: { h: { i: { j: { k: 1 } } } } } } } } } } }]);
});
});
});

View File

@ -0,0 +1,7 @@
import { testSuite } from "manten";
export default testSuite(async ({ describe }) => {
describe("finding", async ({ runTestSuite }) => {
runTestSuite(import("./basic.test.js"));
});
});

View File

@ -0,0 +1,17 @@
import { expect, testSuite } from "manten";
import { Collection } from "../../../src";
import { nrml } from "../../common";
export default testSuite(async ({ describe }) => {
describe("from", ({ test }) => {
test("works", () => {
const data = [
{ a: 1, b: 2, c: 3 },
{ a: 2, b: 2, c: 3 },
];
const c = Collection.from(data);
const found = nrml(c.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: 2, c: 3 }]);
});
});
});

View File

@ -0,0 +1,7 @@
import { testSuite } from "manten";
export default testSuite(async ({ describe }) => {
describe("from", async ({ runTestSuite }) => {
runTestSuite(import("./from.test.js"));
});
});

View File

@ -0,0 +1,81 @@
import { expect, testSuite } from "manten";
import FSAdapter from "../../src/adapter/fs";
import { Collection } from "../../src/collection";
const getCollection = () => {
const collection = new Collection({
autosync: false,
integerIds: false,
adapter: new FSAdapter({ storagePath: ".test", name: "index" }),
});
collection.drop();
return collection;
};
export default testSuite(async ({ describe }) => {
describe("index", ({ test }) => {
test("createIndex throws if the key has a numeric property", () => {
const collection = getCollection();
expect(() => collection.createIndex({ key: "0" })).toThrow();
expect(() => collection.createIndex({ key: "a.0" })).toThrow();
expect(() => collection.createIndex({ key: "a.0.b" })).toThrow();
});
test("can create and remove", () => {
const collection = getCollection();
collection.createIndex({ key: "name" });
expect(collection.indices["name"]).toBeDefined();
expect(collection.indices["name"].unique).toBe(false);
collection.removeIndex("name");
expect(collection.indices["name"]).toBeUndefined();
collection.createIndex({ key: "name", unique: true });
expect(collection.indices["name"]).toBeDefined();
expect(collection.indices["name"].unique).toBe(true);
});
test("indexes are tracked properly", () => {
const collection = getCollection();
collection.createIndex({ key: "person.email" });
collection.createIndex({ key: "person.name" });
collection.insert({ person: { name: "Alice", email: "alice@alice.com", } });
collection.insert({ person: { name: "Bob", email: "bob@bob.com", } });
const alice = collection.find({ "person.name": "Alice" });
const bob = collection.find({ "person.name": "Bob" });
expect(collection.data.internal.index.valuesToId["person.name"]["Alice"]).toEqual([(alice[0] as any)._id]);
expect(collection.data.internal.index.valuesToId["person.email"]["alice@alice.com"]).toEqual([(alice[0] as any)._id]);
expect(collection.data.internal.index.valuesToId["person.name"]["Bob"]).toEqual([(bob[0] as any)._id]);
expect(collection.data.internal.index.valuesToId["person.email"]["bob@bob.com"]).toEqual([(bob[0] as any)._id]);
expect(collection.data.internal.index.idToValues[(alice[0] as any)._id]["person.name"]).toEqual("Alice");
expect(collection.data.internal.index.idToValues[(alice[0] as any)._id]["person.email"]).toEqual("alice@alice.com");
expect(collection.data.internal.index.idToValues[(bob[0] as any)._id]["person.name"]).toEqual("Bob");
expect(collection.data.internal.index.idToValues[(bob[0] as any)._id]["person.email"]).toEqual("bob@bob.com");
collection.update({ person: { name: "Alice" } }, { $merge: { person: { email: "a@a.com" }}});
expect(collection.data.internal.index.valuesToId["person.email"]["a@a.com"]).toEqual([(alice[0] as any)._id]);
expect(collection.data.internal.index.idToValues[(alice[0] as any)._id]["person.email"]).toEqual("a@a.com");
// no more documents have this email value, so the tracked index key should be removed.
expect(collection.data.internal.index.valuesToId["person.email"]["alice@alice.com"]).toBeUndefined();
// the person.name index should still be there.
expect(collection.data.internal.index.valuesToId["person.name"]["Alice"]).toEqual([(alice[0] as any)._id]);
collection.remove({ person: { name: "Alice" } });
expect(collection.data.internal.index.valuesToId["person.name"]["Alice"]).toBeUndefined();
expect(collection.data.internal.index.valuesToId["person.email"]["a@a.com"]).toBeUndefined();
expect(collection.data.internal.index.idToValues[(alice[0] as any)._id]).toBeUndefined();
collection.sync();
});
});
});

View File

@ -0,0 +1,29 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("insert", ({ test }) => {
test("insert one", () => {
const collection = testCollection();
collection.insert({ foo: "bar" });
const found = nrml(collection.find({ foo: "bar" }));
expect(found).toEqual([{ foo: "bar" }]);
});
test("insert multiple", () => {
const collection = testCollection();
collection.insert([{ foo: "bar" }, { foo: "baz" }, { foo: "boo" }]);
const found = nrml(collection.find({ foo: { $includes: "b" } }));
expect(found).toEqual([{ foo: "bar" }, { foo: "baz" }, { foo: "boo" }]);
});
test("can insert emojis", () => {
const collection = testCollection();
collection.insert({ foo: "👍" });
const found = nrml(collection.find({ foo: "👍" }));
expect(found).toEqual([{ foo: "👍" }]);
});
});
});

View File

@ -0,0 +1,7 @@
import { testSuite } from "manten";
export default testSuite(async ({ describe }) => {
describe("insert", async ({ runTestSuite }) => {
runTestSuite(import("./basic.test.js"));
});
});

View File

@ -0,0 +1,80 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$and", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([
{ a: 1, b: 1, c: 1 },
{ a: 1, b: 1, c: 1 },
{ a: 1, b: 2, c: 3 },
{ a: 2, b: 2, c: 3 },
]);
const found = nrml(collection.find({ $and: [{ a: 1 }, { b: 2 }] }));
expect(found).toEqual([{ a: 1, b: 2, c: 3 }]);
});
test("nested operators", () => {
const collection = testCollection();
collection.insert([
{ foo: "bar", num: 5 },
{ foo: "baz", num: 10 },
{ foo: "boo", num: 20 },
]);
const found = nrml(collection.find({ $and: [{ foo: { $includes: "ba" } }, { num: { $gt: 9 } }] }));
expect(found).toEqual([{ foo: "baz", num: 10 }]);
});
test("deep selectors, explicit and implicit", () => {
const collection = testCollection();
collection.insert([
{ a: { b: { c: 1, d: 1 } } },
{ a: { b: { c: 1, d: 1 } } },
{ a: { b: { c: 1, d: 3 } } },
]);
const found = nrml(collection.find({ $and: [{ a: { b: { c: { $lt: 2 } } } }, { d: 3 }] }));
expect(found).toEqual([{ a: { b: { c: 1, d: 3 } } }]);
});
test("shallow and deep selectors", () => {
const collection = testCollection();
collection.insert([{ a: 15, b: 1 }, { a: 15, b: { c: { d: 100 } } }]);
const found = nrml(collection.find({ $and: [{ a: 15 }, { b: { c: { d: 100 } } }] }));
expect(found).toEqual([{ a: 15, b: { c: { d: 100 } } }]);
});
test("functions as conditions", () => {
const collection = testCollection();
collection.insert([
{ foo: "bar", num: 5 },
{ foo: "baz", num: 10 },
{ foo: "bazzz", num: 20 },
]);
const found = nrml(collection.find({ $and: [{ foo: { $includes: "ba" } }, { num: { $gt: 9 } }, { num: (v: number) => v % 10 === 0 }] }));
expect(found).toEqual([{ foo: "baz", num: 10 }, { foo: "bazzz", num: 20 }]);
});
test("and matches while respecting other query parameters", () => {
const collection = testCollection();
collection.insert([
{ a: 1, num: 5 },
{ a: 2, num: 10 },
{ a: 3, num: 20 },
]);
const found = nrml(collection.find({ a: 2, $and: [{ num: { $gt: 0 } }, { num: { $lt: 100 } }] }));
expect(found).toEqual([{ a: 2, num: 10 }]);
});
test("works with dot notation", () => {
const collection = testCollection();
collection.insert([
{ a: { b: 1, c: 1 }, d: 1 },
{ a: { b: 1, c: 1 }, d: 1 },
{ a: { b: 1, c: 3 }, d: 3 },
]);
const found = nrml(collection.find({ $and: [{ "a.b": 1 }, { "a.c": 3 }] }));
expect(found).toEqual([{ a: { b: 1, c: 3 }, d: 3 }]);
});
});
});

View File

@ -0,0 +1,59 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$fn", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([
{ a: 2 },
{ a: 4 },
{ a: 5 },
{ a: 6 },
]);
const isEven = (x: number) => x % 2 === 0;
const found = nrml(collection.find({ a: { $fn: isEven } }));
expect(found).toEqual([{ a: 2 }, { a: 4 }, { a: 6 }]);
});
test("nested", () => {
const collection = testCollection();
collection.insert([
{ a: { b: { c: 2 } } },
{ a: { b: { c: 4 } } },
{ a: { b: { c: 5 } } },
{ a: { b: { c: 6 } } },
]);
const isEven = (x: number) => x % 2 === 0;
const found = nrml(collection.find({ c: { $fn: isEven } }));
expect(found).toEqual([ { a: { b: { c: 2 } } }, { a: { b: { c: 4 } } }, { a: { b: { c: 6 } } } ]);
});
test("nested, using dot notation", () => {
const collection = testCollection();
collection.insert([
{ a: { b: { c: 2 } } },
{ a: { b: { c: 4 } } },
{ a: { b: { c: 5 } } },
{ a: { b: { c: 6 } } },
]);
const isEven = (x: number) => x % 2 === 0;
const found = nrml(collection.find({ "a.b.c": { $fn: isEven } }));
expect(found).toEqual([ { a: { b: { c: 2 } } }, { a: { b: { c: 4 } } }, { a: { b: { c: 6 } } } ]);
});
test("multiple functions", () => {
// When using multiple functions, the source value must be true
// for each function in order to be considered a query match.
const collection = testCollection();
collection.insert([
{ a: 2 },
{ a: 3 },
{ a: 4 },
{ a: 5 },
{ a: 6 },
]);
const isOdd = (x: number) => x % 2 !== 0;
const isThree = (x: number) => x === 3;
const found = nrml(collection.find({ a: { $fn: [isThree, isOdd] } }));
expect(found).toEqual([{ a: 3 }]);
});
});
});

View File

@ -0,0 +1,142 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$gt", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([{ a: 2 }, { a: 4 }, { a: 5 }, { a: 6 }]);
const found = nrml(collection.find({ a: { $gt: 4 } }));
expect(found).toEqual([{ a: 5 }, { a: 6 }]);
});
test("works with strings", () => {
const collection = testCollection();
collection.insert([{ a: "a" }, { a: "b" }, { a: "c" }, { a: "d" }]);
const found = nrml(collection.find({ a: { $gt: "b" } }));
expect(found).toEqual([{ a: "c" }, { a: "d" }]);
});
test("works with array lengths", () => {
const collection = testCollection();
collection.insert([{ a: [1, 2] }, { a: [1, 2, 3] }, { a: [1, 2, 3, 4] }]);
const found = nrml(collection.find({ a: { $gt: 3 } }));
expect(found).toEqual([{ a: [1, 2, 3, 4] }]);
});
test("works with deeply nested properties using dot notation", () => {
const collection = testCollection();
collection.insert([{ a: { b: { c: 2 } } }, { a: { b: { c: 4 } } }, { a: { b: { c: 5 } } }, { a: { b: { c: 6 } } }]);
const found = nrml(collection.find({ "a.b.c": { $gt: 4 } }));
expect(found).toEqual([{ a: { b: { c: 5 } } }, { a: { b: { c: 6 } } }]);
});
});
describe("$lt", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([{ a: 2 }, { a: 4 }, { a: 5 }, { a: 6 }]);
const found = nrml(collection.find({ a: { $lt: 4 } }));
expect(found).toEqual([{ a: 2 }]);
});
test("works with strings", () => {
const collection = testCollection();
collection.insert([{ a: "a" }, { a: "b" }, { a: "c" }, { a: "d" }]);
const found = nrml(collection.find({ a: { $lt: "b" } }));
expect(found).toEqual([{ a: "a" }]);
});
test("works with array lengths", () => {
const collection = testCollection();
collection.insert([{ a: [1, 2] }, { a: [1, 2, 3] }, { a: [1, 2, 3, 4] }]);
const found = nrml(collection.find({ a: { $lt: 3 } }));
expect(found).toEqual([{ a: [1, 2] }]);
});
test("works with deeply nested properties using dot notation", () => {
const collection = testCollection();
collection.insert([{ a: { b: { c: 2 } } }, { a: { b: { c: 4 } } }, { a: { b: { c: 5 } } }, { a: { b: { c: 6 } } }]);
const found = nrml(collection.find({ "a.b.c": { $lt: 4 } }));
expect(found).toEqual([{ a: { b: { c: 2 } } }]);
});
});
describe("$gte", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([{ a: 2 }, { a: 4 }, { a: 5 }, { a: 6 }]);
const found = nrml(collection.find({ a: { $gte: 4 } }));
expect(found).toEqual([{ a: 4 }, { a: 5 }, { a: 6 }]);
});
test("works with strings", () => {
const collection = testCollection();
collection.insert([{ a: "a" }, { a: "b" }, { a: "c" }, { a: "d" }]);
const found = nrml(collection.find({ a: { $gte: "b" } }));
expect(found).toEqual([{ a: "b" }, { a: "c" }, { a: "d" }]);
});
test("works with array lengths", () => {
const collection = testCollection();
collection.insert([{ a: [1, 2] }, { a: [1, 2, 3] }, { a: [1, 2, 3, 4] }]);
const found = nrml(collection.find({ a: { $gte: 3 } }));
expect(found).toEqual([{ a: [1, 2, 3] }, { a: [1, 2, 3, 4] }]);
});
test("works with deeply nested properties using dot notation", () => {
const collection = testCollection();
collection.insert([{ a: { b: { c: 2 } } }, { a: { b: { c: 4 } } }, { a: { b: { c: 5 } } }, { a: { b: { c: 6 } } }]);
const found = nrml(collection.find({ "a.b.c": { $gte: 4 } }));
expect(found).toEqual([{ a: { b: { c: 4 } } }, { a: { b: { c: 5 } } }, { a: { b: { c: 6 } } }]);
});
});
describe("$lte", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([{ a: 2 }, { a: 4 }, { a: 5 }, { a: 6 }]);
const found = nrml(collection.find({ a: { $lte: 4 } }));
expect(found).toEqual([{ a: 2 }, { a: 4 }]);
});
test("works with strings", () => {
const collection = testCollection();
collection.insert([{ a: "a" }, { a: "b" }, { a: "c" }, { a: "d" }]);
const found = nrml(collection.find({ a: { $lte: "b" } }));
expect(found).toEqual([{ a: "a" }, { a: "b" }]);
});
test("works with array lengths", () => {
const collection = testCollection();
collection.insert([{ a: [1, 2] }, { a: [1, 2, 3] }, { a: [1, 2, 3, 4] }]);
const found = nrml(collection.find({ a: { $lte: 3 } }));
expect(found).toEqual([{ a: [1, 2] }, { a: [1, 2, 3] }]);
});
test("works with deeply nested properties using dot notation", () => {
const collection = testCollection();
collection.insert([
{ a: { b: { c: 1 } } },
{ a: { b: { c: 2 } } },
{ a: { b: { c: 3 } } },
{ a: { b: { c: 4 } } },
]);
const found = nrml(collection.find({ "a.b.c": { $lte: 2 } }));
expect(found).toEqual([
{ a: { b: { c: 1 } } },
{ a: { b: { c: 2 } } },
]);
});
});
});

View File

@ -0,0 +1,39 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$has", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([{ a: 2 }, { b: 4 }, { c: 5 }, { a: 6 }]);
const found = nrml(collection.find({ $has: "a" }));
expect(found).toEqual([{ a: 2 }, { a: 6 }]);
});
test("works with more than one property", () => {
const collection = testCollection();
collection.insert([{ a: 2, b: 1 }, { b: 4 }, { a: 5 }, { a: 6, b: 3 }]);
const found = nrml(collection.find({ $has: ["a", "b"] }));
expect(found).toEqual([{ a: 2, b: 1 }, { a: 6, b: 3 }]);
});
test("works with $not", () => {
const collection = testCollection();
collection.insert([{ a: 2 }, { b: 4 }, { c: 5 }, { a: 6 }]);
const found = nrml(collection.find({ $not: { $has: "a" } }));
expect(found).toEqual([
{ xxx: "xxx" },
{ yyy: "yyy" },
{ zzz: "zzz" },
{ b: 4 }, { c: 5 }
]);
});
test("works with dot notation", () => {
const collection = testCollection();
collection.insert([{ a: { b: 2 } }, { a: { c: 4 } }, { a: { d: 5 } }, { a: { b: 6 } }]);
const found = nrml(collection.find({ $has: "a.b" }));
expect(found).toEqual([{ a: { b: 2 } }, { a: { b: 6 } }]);
});
});
});

View File

@ -0,0 +1,51 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$hasAny", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([{ a: 2 }, { b: 4 }, { c: 5 }, { a: 6 }]);
const found = nrml(collection.find({ $hasAny: "a" }));
expect(found).toEqual([{ a: 2 }, { a: 6 }]);
});
test("works with more than one property", () => {
const collection = testCollection();
collection.insert([{ a: 2, b: 1 }, { b: 4 }, { a: 5 }, { a: 6, b: 3 }, { c: 5 }]);
const found = nrml(collection.find({ $hasAny: ["a", "b"] }));
expect(found).toEqual([{ a: 2, b: 1 }, { b: 4 }, { a: 5 }, { a: 6, b: 3 }]);
});
test("works with $not", () => {
const collection = testCollection();
collection.insert([{ a: 2 }, { b: 4 }, { c: 5 }, { a: 6 }]);
const found = nrml(collection.find({ $not: { $hasAny: ["a", "b"] } }));
expect(found).toEqual([
{ xxx: "xxx" },
{ yyy: "yyy" },
{ zzz: "zzz" },
{ c: 5 }
]);
});
test("works with dot notation", () => {
const collection = testCollection();
collection.insert([{ a: { b: 2 } }, { b: 4 }, { c: 5 }, { a: { b: 6 } }]);
const found = nrml(collection.find({ $hasAny: "a.b" }));
expect(found).toEqual([{ a: { b: 2 } }, { a: { b: 6 } }]);
});
test("works with leading dot notation to narrowly scope $hasAny", () => {
const collection = testCollection();
collection.insert([
{ a: { b: { c: { d: 2 } } } },
{ b: 4 },
{ c: 5 },
{ a: { b: { c: { e: 6 } } } }
]);
const found = nrml(collection.find({ "a.b.c": { $hasAny: "d"} }));
expect(found).toEqual([{ a: { b: { c: { d: 2 } } } }]);
});
});
});

View File

@ -0,0 +1,55 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$includes", ({ test }) => {
test("simple string", () => {
const collection = testCollection();
collection.insert({ foo: "bar" });
collection.insert({ foo: "baz" });
collection.insert({ foo: "boo" });
const found = nrml(collection.find({ foo: { $includes: "ba" } }));
expect(found.length).toEqual(2);
expect(found).toEqual([{ foo: "bar" }, { foo: "baz" }]);
});
test("simple string deep", () => {
const collection = testCollection();
collection.insert({ a: { b: { foo: "bar" } } });
collection.insert({ a: { b: { foo: "baz" } } });
collection.insert({ a: { b: { foo: "boo" } } });
const found = nrml(collection.find({ a: { b: { foo: { $includes: "ba" } } } }));
expect(found.length).toEqual(2);
expect(found).toEqual([{ a: { b: { foo: "bar" } } }, { a: { b: { foo: "baz" } } }]);
});
test("simple array", () => {
const collection = testCollection();
collection.insert({ foo: [1, 2, 3] });
collection.insert({ foo: [1, 2, 4] });
collection.insert({ foo: [5, 6, 7] });
const found = nrml(collection.find({ foo: { $includes: 2 } }));
expect(found.length).toEqual(2);
expect(found).toEqual([{ foo: [1, 2, 3] }, { foo: [1, 2, 4] }]);
});
test("simple array deep", () => {
const collection = testCollection();
collection.insert({ a: { b: [1, 2, 3] }});
collection.insert({ a: { b: [1, 2, 4] }});
collection.insert({ a: { b: [5, 6, 7] }});
const found = nrml(collection.find({ a: { b: { $includes: 2 } } }));
expect(found.length).toEqual(2);
expect(found).toEqual([{ a: { b: [1, 2, 3] } }, { a: { b: [1, 2, 4] } }]);
});
test("includes array", () => {
const collection = testCollection();
collection.insert({ a: { b: [1, 2, 3] }});
collection.insert({ a: { b: [1, 2, 4] }});
collection.insert({ a: { b: [5, 6, 7] }});
const found = nrml(collection.find({ a: { b: { $includes: [1, 2] } } }));
expect(found).toEqual([{ a: { b: [1, 2, 3] } }, { a: { b: [1, 2, 4] } }]);
});
});
});

View File

@ -0,0 +1,18 @@
import { testSuite } from "manten";
export default testSuite(async ({ describe }) => {
describe("boolean", async ({ runTestSuite }) => {
runTestSuite(import("./includes.test.js"));
runTestSuite(import("./and.test.js"));
runTestSuite(import("./or.test.js"));
runTestSuite(import("./xor.test.js"));
runTestSuite(import("./fn.test.js"));
runTestSuite(import("./re.test.js"));
runTestSuite(import("./oneOf.test.js"));
runTestSuite(import("./length.test.js"));
runTestSuite(import("./not.test.js"));
runTestSuite(import("./has.test.js"));
runTestSuite(import("./hasAny.test.js"));
runTestSuite(import("./gtlt.test.js"));
});
});

View File

@ -0,0 +1,17 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$length", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ foo: [0, 0] });
collection.insert({ foo: [0, 0, 0] });
collection.insert({ foo: [0, 0, 0] });
collection.insert({ foo: "abc" });
collection.insert({ foo: "abcd" });
const found = nrml(collection.find({ foo: { $length: 3 } }));
expect(found).toEqual([{ foo: [0, 0, 0] }, { foo: [0, 0, 0] }, { foo: "abc" }]);
});
});
});

View File

@ -0,0 +1,223 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$not", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([
{ a: 1, b: 2, c: 3 },
{ a: 2, b: 2, c: 3 },
]);
const found = nrml(collection.find({ $not: { a: 1 } }));
expect(found).toEqual([
{ xxx: "xxx" },
{ yyy: "yyy" },
{ zzz: "zzz" },
{ a: 2, b: 2, c: 3 }
]);
});
test("works with $and", () => {
const collection = testCollection();
collection.insert([
{ a: 1, b: 2, c: 3 },
{ a: 2, b: 2, c: 3 },
{ a: 3, b: 2, c: 3 },
]);
const found = nrml(collection.find({ $and: [{ $not: { a: 1 } }, { $not: { a: 2 }}] }));
expect(found).toEqual([
{ xxx: "xxx" },
{ yyy: "yyy" },
{ zzz: "zzz" },
{ a: 3, b: 2, c: 3 }
]);
});
test("works with other mods", () => {
const collection = testCollection();
collection.insert([
{ a: 1, b: 2, c: 3 },
{ a: 2, b: 2, c: 3 },
{ a: 3, b: 2, c: 3 },
]);
const found = nrml(collection.find({ $not: { a: { $lte: 2 }}}));
expect(found).toEqual([
{ xxx: "xxx" },
{ yyy: "yyy" },
{ zzz: "zzz" },
{ a: 3, b: 2, c: 3 }
]);
});
test("works with $and", () => {
const collection = testCollection();
collection.insert([
{ a: 1, b: 2, c: 3 },
{ a: 2, b: 2, c: 3 },
{ a: 3, b: 2, c: 3 },
{ a: 5, b: 2, c: 3 },
{ a: 7, b: 2, c: 3 },
]);
const found = nrml(collection.find({ $and: [{ $not: { a: { $lte: 2 }}}, { $not: { a: { $gte: 5 }}}] }));
expect(found).toEqual([
{ xxx: "xxx" },
{ yyy: "yyy" },
{ zzz: "zzz" },
{ a: 3, b: 2, c: 3 }
]);
});
test("expects all provided cases to be true (does not behave as $or)", () => {
const collection = testCollection();
collection.insert([
{ a: 1, b: 2, c: 3 },
{ a: 2, b: 2, c: 3 },
{ a: 3, b: 3, c: 3 },
]);
const found = nrml(collection.find({ $not: { a: 1, b: 2 }}));
expect(found).toEqual([
{ xxx: "xxx" },
{ yyy: "yyy" },
{ zzz: "zzz" },
{ a: 2, b: 2, c: 3 }, // <-- matches because a is not 1
{ a: 3, b: 3, c: 3 }, // <-- matches because a is not 1 AND b is not 2
]);
});
test("works with dot notation", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: 1 } }, { a: { b: 2 } }
]);
const found = nrml(collection.find({ $not: { "a.b": 1 }}));
expect(found).toEqual([
{ a: { b: 2 } },
]);
});
test("works with leading properties", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: 1 } }, { a: { b: 2 } }
]);
const found = nrml(collection.find({ a: { $not: { b: 1 }}}));
expect(found).toEqual([
{ a: { b: 2 } },
]);
});
test("works with leading properties very deeply", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: { c: { d: 1 } } } }, { a: { b: { c: { d: 2 } } } }
]);
const found = nrml(collection.find({ a: { b: { c: { $not: { d: 1 }}}}}));
expect(found).toEqual([
{ a: { b: { c: { d: 2 } } } },
]);
});
test("works with $includes -> $not: { $includes: ... }", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: [1, 2, 3] }, { a: [2, 3, 4] }
]);
const found = nrml(collection.find({ $not: { a: { $includes: 1 } } }));
expect(found).toEqual([
{ a: [2, 3, 4] },
]);
});
test("works with $includes, deeply", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: [1, 2, 3] } }, { a: { b: [2, 3, 4] } }
]);
const found = nrml(collection.find({ $not: { a: { b: { $includes: 1 } } } }));
expect(found).toEqual([{ a: { b: [2, 3, 4] } }]);
});
test("works with $includes, very deeply", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: { c: { d: [1, 2, 3] } } } }, { a: { b: { c: { d: [2, 3, 4] } } } }
]);
const found = nrml(collection.find({ $not: { a: { b: { c: { d: { $includes: 1 } } } } } }));
expect(found).toEqual([{ a: { b: { c: { d: [2, 3, 4] } } } }]);
});
test("works with $includes, deep, using dot notation", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: [1, 2, 3] } }, { a: { b: [2, 3, 4] } }
]);
const found = nrml(collection.find({ $not: { "a.b": { $includes: 1 } } }));
expect(found).toEqual([{ a: { b: [2, 3, 4] } }]);
});
test("works with $includes, infinitely deep, using dot notation", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: { c: { d: [1, 2, 3] } } } }, { a: { b: { c: { d: [2, 3, 4] } } } }
]);
const found = nrml(collection.find({ $not: { "a.b.c.d": { $includes: 1 } } }));
expect(found).toEqual([{ a: { b: { c: { d: [2, 3, 4] } } } }]);
});
test("works with $oneOf, infinitely deep, using dot notation", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: { c: { d: 1 } } } }, { a: { b: { c: { d: 2 } } } }
]);
const found = nrml(collection.find({ $not: { "a.b.c.d": { $oneOf: [1, 2] } } }));
expect(found).toEqual([]);
const found2 = nrml(collection.find({ $not: { "a.b.c.d": { $oneOf: [1, 3] } } }));
expect(found2).toEqual([{ a: { b: { c: { d: 2 } } } }]);
});
test("works with $oneOf, infinitely deep, not dot notation", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: { c: { d: 1 } } } }, { a: { b: { c: { d: 2 } } } }
]);
const found = nrml(collection.find({ $not: { a: { b: { c: { d: { $oneOf: [1, 2] } } } } } }));
expect(found).toEqual([]);
const found2 = nrml(collection.find({ $not: { a: { b: { c: { d: { $oneOf: [1, 3] } } } } } }));
expect(found2).toEqual([{ a: { b: { c: { d: 2 } } } }]);
});
test("works with $length, infinitely deep, using dot notation", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: { c: { d: [1, 2, 3] } } } }, { a: { b: { c: { d: [2, 3, 4, 5] } } } }
]);
const found = nrml(collection.find({ $not: { "a.b.c.d": { $length: 3 } } }));
expect(found).toEqual([{ a: { b: { c: { d: [2, 3, 4, 5] } } } }]);
});
test("works with $hasAny, infinitely deep, using dot notation", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: { c: { d: { foo: "foo", bar: "bar", baz: "baz" } } } } },
{ a: { b: { c: { d: { foo: "foo", bar: "bar" } } } } }
]);
const found = nrml(collection.find({ $not: { "a.b.c.d": { $hasAny: ["foo", "bar"] } } }));
expect(found).toEqual([]);
const found2 = nrml(collection.find({ $not: { "a.b.c.d": { $hasAny: ["baz"] } } }));
expect(found2).toEqual([{ a: { b: { c: { d: { foo: "foo", bar: "bar" } } } } }]);
});
test("works with $has, infinitely deep, using dot notation", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: { c: { d: { foo: "foo", bar: "bar", baz: "baz" } } } } },
{ a: { b: { c: { d: { foo: "foo", bar: "bar" } } } } }
]);
const found = nrml(collection.find({ $not: { "a.b.c.d": { $has: ["foo", "bar"] } } }));
expect(found).toEqual([]);
const found2 = nrml(collection.find({ $not: { "a.b.c.d": { $has: ["baz"] } } }));
expect(found2).toEqual([{ a: { b: { c: { d: { foo: "foo", bar: "bar" } } } } }]);
});
});
});

View File

@ -0,0 +1,36 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$oneOf", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const found = nrml(collection.find({ a: { $oneOf: [2, 3] } }));
expect(found.length).toEqual(2);
expect(found).toEqual([{ a: 2 }, { a: 3 }]);
});
test("works with dot notation", () => {
const collection = testCollection({ populate: false });
collection.insert({ a: { b: 1 } });
collection.insert({ a: { b: 2 } });
collection.insert({ a: { b: 3 } });
const found = nrml(collection.find({ "a.b": { $oneOf: [2, 3] } }));
expect(found.length).toEqual(2);
expect(found).toEqual([{ a: { b: 2 } }, { a: { b: 3 } }]);
});
test("works deeply without dot notation", () => {
const collection = testCollection({ populate: false });
collection.insert({ a: { b: { c: 1 } } });
collection.insert({ a: { b: { c: 2 } } });
collection.insert({ a: { b: { c: 3 } } });
const found = nrml(collection.find({ a: { b: { c: { $oneOf: [2, 3] } } } }));
expect(found.length).toEqual(2);
expect(found).toEqual([{ a: { b: { c: 2 } } }, { a: { b: { c: 3 } } }]);
});
});
});

View File

@ -0,0 +1,53 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$or", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([
{ a: 1, b: 1, c: 1 },
{ a: 1, b: 1, c: 2 },
{ a: 1, b: 2, c: 3 },
{ a: 2, b: 2, c: 3 },
]);
const found = nrml(collection.find({ $or: [{ a: 1 }, { c: 2 }] }));
expect(found).toEqual([
{ a: 1, b: 1, c: 1 },
{ a: 1, b: 1, c: 2 },
{ a: 1, b: 2, c: 3 },
]);
});
test("nested operators", () => {
const collection = testCollection();
collection.insert([
{ foo: "bar", num: 5 },
{ foo: "bee", num: 8 },
{ foo: "baz", num: 10 },
{ foo: "boo", num: 20 },
]);
const found = nrml(collection.find({ $or: [{ foo: { $includes: "ba" } }, { num: { $lt: 9 } }] }));
expect(found).toEqual([
{ foo: "bar", num: 5 },
{ foo: "bee", num: 8 },
{ foo: "baz", num: 10 },
]);
});
test("works with dot notation", () => {
const collection = testCollection();
collection.insert([
{ a: { b: { c: 1, d: 1 } } },
{ a: { b: { c: 1, d: 2 } } },
{ a: { b: { c: 1, d: 3 } } },
]);
const found = nrml(collection.find({ $or: [{ "a.b.c": 1 }, { "a.b.d": 3 }] }));
expect(found).toEqual([
{ a: { b: { c: 1, d: 1 } } },
{ a: { b: { c: 1, d: 2 } } },
{ a: { b: { c: 1, d: 3 } } },
]);
})
});
});

View File

@ -0,0 +1,30 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$re", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([
{ ip: "192.168.0.1" },
{ ip: "192.168.0.254" },
{ ip: "19216801" }
]);
const ip = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/;
const found = nrml(collection.find({ ip: { $re: ip } }));
expect(found).toEqual([ { ip: "192.168.0.1" }, { ip: "192.168.0.254" } ]);
});
test("works with dot notation", () => {
const collection = testCollection();
collection.insert([
{ ip: { a: "192.168.0.1" } },
{ ip: { a: "192.168.0.254" } },
{ ip: { a: "19216801" } }
]);
const ip = /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/;
const found = nrml(collection.find({ "ip.a": { $re: ip } }));
expect(found).toEqual([ { ip: { a: "192.168.0.1" } }, { ip: { a: "192.168.0.254" } } ]);
})
});
});

View File

@ -0,0 +1,75 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$xor", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert([
{ a: 1, b: 1, c: 1 },
{ a: 1, b: 2, c: 2 }, // not included because a is 1 and b is 2, which matches the query exactly
{ a: 2, b: 2, c: 3 },
]);
const found = nrml(collection.find({ $xor: [{ a: 1 }, { b: 2 }] }));
expect(found).toEqual([
{ a: 1, b: 1, c: 1 }, // <-- a was 1, but but was not 2
{ a: 2, b: 2, c: 3 }, // <-- a was not 1, but b was 2
]);
});
test("with nested operators", () => {
const collection = testCollection();
collection.insert([
{ a: 1 },
{ b: 2 },
{ c: 3 },
{ a: 1, b: 2 }, // not included, because both properties exist in the query
{ a: 1, c: 3 },
{ b: 2, c: 3 },
]);
const found = nrml(collection.find({ $xor: [{ $has: "a" }, { $has: "b" }] }));
expect(found).toEqual([
{ a: 1 }, // <-- only has "a"
{ b: 2 }, // <-- only has "b"
{ a: 1, c: 3 }, // <-- only has "a"
{ b: 2, c: 3 }, // <-- only has "b"
]);
});
test("nested operators", () => {
const collection = testCollection();
collection.insert([
{ foo: "bar", num: 5 }, // not included, because properties both match the query
{ foo: "bee", num: 8 },
{ foo: "baz", num: 10 },
{ foo: "boo", num: 20 }, // not included, because neither property matches the query
]);
const found = nrml(collection.find({ $xor: [{ foo: { $includes: "ba" } }, { num: { $lt: 9 } }] }));
expect(found).toEqual([
{ foo: "bee", num: 8 }, // <-- foo does not include "ba", but num is less than 9
{ foo: "baz", num: 10 }, // <-- foo includes "ba", but num is not less than 9
]);
});
test("nested operators, dot notation, implicitly and explicitly deep", () => {
const collection = testCollection({ populate: false });
collection.insert([
{ a: { b: { c: { foo: "bar", num: 5 } } } }, // not included, because properties both match the query
{ a: { b: { c: { foo: "bee", num: 8 } } } },
{ a: { b: { c: { foo: "baz", num: 10 } } } },
{ a: { b: { c: { foo: "boo", num: 20 } } } }, // not included, because neither property matches the query
]);
const found = nrml(collection.find({ $xor: [{ "a.b.c.foo": { $includes: "ba" } }, { num: { $lt: 9 } }] }));
expect(found).toEqual([
{ a: { b: { c: { foo: "bee", num: 8 } } } }, // <-- foo does not include "ba", but num is less than 9
{ a: { b: { c: { foo: "baz", num: 10 } } } }, // <-- foo includes "ba", but num is not less than 9
]);
});
test("throws when given anything other than 2 parameters", () => {
const collection = testCollection();
expect(() => collection.find({ $xor: [{ a: 1 }, { b: 2 }, { c: 3 }] })).toThrow();
expect(() => collection.find({ $xor: [{ a: 1 }] })).toThrow();
});
});
});

View File

@ -0,0 +1,57 @@
import { expect, testSuite } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$change", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.update({ a: 2 }, { $change: { a: 3 } });
const found = nrml(collection.find({ a: 3 }));
expect(found).toEqual([{ a: 3 }]);
});
test("works deeply", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ c: 5 }, { $change: { b: { c: 6 } } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: 6 } }]);
});
test("works deeply with dot notation", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ c: 5 }, { $change: { "b.c": 6 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: 6 } }]);
});
test("doesn't create new properties", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.update({ a: 1 }, { $change: { b: 1 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1 }]);
});
test("doesn't create new properties, deep", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.update({ a: 1 }, { $change: { b: { c: 1 } } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1 }]);
});
test("doesn't create new properties, deep, dot notation", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.update({ a: 1 }, { $change: { "b.c": 1 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1 }]);
});
});
});

View File

@ -0,0 +1,26 @@
import { expect, testSuite } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$filter", ({ test }) => {
test("works", () => {
const collection = testCollection();
const filterfn = (doc: any) => doc.a === 1;
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
collection.update({ $has: "a" }, { $filter: filterfn });
const found = nrml(collection.find({ $has: "a" }));
expect(found).toEqual([{ a: 1 }]);
});
test("works against a nested array", () => {
const collection = testCollection();
collection.insert({ a: [1, 2, 3, 4, 5] });
collection.update({ $has: "a" }, { $filter: { a: (doc: any) => doc > 3 } });
const found = nrml(collection.find({ $has: "a" }));
expect(found).toEqual([{ a: [4, 5] }]);
});
});
});

View File

@ -0,0 +1,15 @@
import {testSuite} from "manten";
export default testSuite(async ({ describe }) => {
describe("mutation", async ({ runTestSuite }) => {
runTestSuite(import("./set.test.js"));
runTestSuite(import("./unset.test.js"));
runTestSuite(import("./change.test.js"));
runTestSuite(import("./merge.test.js"));
runTestSuite(import("./math.test.js"));
runTestSuite(import("./map.test.js"));
runTestSuite(import("./push.test.js"));
runTestSuite(import("./unshift.test.js"));
runTestSuite(import("./filter.test.js"));
});
});

View File

@ -0,0 +1,25 @@
import { expect, testSuite } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$map", ({ test }) => {
test("works", () => {
const collection = testCollection();
const mapfn = (doc: any) => ({ ...doc, c: 5 });
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ a: 1 }, { $map: mapfn });
const found = nrml(collection.find({ c: 5 }));
expect(found).toEqual([{ a: 1, b: { c: 5 }, c: 5 }]);
});
test("works with other operators", () => {
const collection = testCollection();
const mapfn = (doc: any) => ({ ...doc, c: 5 });
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ a: 1 }, { $map: mapfn, $unset: "b" });
const found = nrml(collection.find({ c: 5 }));
expect(found).toEqual([{ a: 1, c: 5 }]);
});
});
});

View File

@ -0,0 +1,270 @@
import { expect, testSuite } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$inc", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ a: 1 }, { $inc: { a: 5 } });
const found = nrml(collection.find({ c: 5 }));
expect(found).toEqual([{ a: 6, b: { c: 5 } }]);
});
test("arrays", () => {
const collection = testCollection();
collection.insert({ a: { b: [1, 2, 3] } });
collection.update({ a: { b: [1, 2, 3] } }, { $inc: 5 });
const found = nrml(collection.find({ b: [6,7,8] }));
expect(found).toEqual([{ a: { b: [6, 7, 8] } }]);
});
test("works, syntax 2", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ a: 1 }, { $inc: 5 });
const found = nrml(collection.find({ c: 5 }));
expect(found).toEqual([{ a: 6, b: { c: 5 } }]);
});
test("increments only the properties defined in query", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 2, c: 3 });
collection.update({ a: 1, b: 2 }, { $inc: 5 });
const found = nrml(collection.find({ a: 6 }));
expect(found).toEqual([{ a: 6, b: 7, c: 3 }]);
});
test("implcitly creates properties", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.update({ a: 1 }, { $inc: { b: 5 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: 5 }]);
});
test("syntax 2 increments properties specified in query", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 2, c: 3 });
collection.update({ a: 1, b: 2, c: 3 }, { $inc: 5 });
const found = nrml(collection.find({ a: 6, b: 7, c: 8 }));
expect(found).toEqual([{ a: 6, b: 7, c: 8 }]);
});
test("deep selector, shallow and deep increment", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: { d: 1, e: 1 } } });
collection.update({ d: 1 }, { $inc: { f: 5, "b.c.d": 5 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: { d: 6, e: 1 } }, f: 5 }]);
});
test("deep selector, shallow increment, syntax 2", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: { d: 1, e: 1 } } });
collection.update({ d: 1 }, { $inc: 5 });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: { d: 1, e: 1 } }, d: 5 }]);
});
test("deep selector, implicitly create shallow properties", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: { d: 1 } } });
collection.update({ d: 1 }, { $inc: { e: 5 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: { d: 1 } }, e: 5 }]);
});
test("deep selector, implicitly create deep properties", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: { d: 1 } } });
collection.update({ d: 1 }, { $inc: { "b.c.e": 5 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: { d: 1, e: 5 } } }]);
});
test("updates keys specified in query, even when using other mods", () => {
const collection = testCollection();
collection.insert([
{ a: 1, b: { c: 2 }},
{ a: 1, b: { c: 2 }},
{ a: 1, b: { c: 2 }},
]);
collection.update({ b: { c: { $gt: 0 }}}, { $inc: 5 });
const found = nrml(collection.find({ c: 7 }));
expect(found).toEqual([
{ a: 1, b: { c: 7 }},
{ a: 1, b: { c: 7 }},
{ a: 1, b: { c: 7 }},
]);
});
test("updates keys specified in query, even when using other mods - dot notation", () => {
const collection = testCollection();
collection.insert([
{ a: 1, b: { c: 2 }},
{ a: 1, b: { c: 2 }},
{ a: 1, b: { c: 2 }},
]);
collection.update({ "b.c": { $gt: 0 } }, { $inc: 5 });
const found = nrml(collection.find({ c: 7 }));
expect(found).toEqual([
{ a: 1, b: { c: 7 }},
{ a: 1, b: { c: 7 }},
{ a: 1, b: { c: 7 }},
]);
});
test("update keys specified in query when the query is an object", () => {
const collection = testCollection();
collection.insert([
{ a: { b: { c: 1 } } },
{ a: { b: { c: 2 } } },
]);
collection.update({ a: { b: { c: 1 } } }, { $inc: 5 });
const found = nrml(collection.find({ c: 6 }));
expect(found).toEqual([{ a: { b: { c: 6 } } }]);
});
test("update keys specified in query - dot notation", () => {
const collection = testCollection();
collection.insert([
{ a: { b: { c: 1 } } },
{ a: { b: { c: 2 } } },
]);
collection.update({ "a.b.c": 1 }, { $inc: 5 });
const found = nrml(collection.find({ c: 6 }));
expect(found).toEqual([{ a: { b: { c: 6 } } }]);
});
test("update keys specified in query, mix of object and dot notation", () => {
const collection = testCollection();
collection.insert([
{ a: { b: { c: 1, d: 2 } } },
{ a: { b: { c: 2, d: 3 } } },
]);
collection.update({ a: { b: { c: 1 } }, "a.b.d": 2 }, { $inc: 5 });
const found = nrml(collection.find({ c: 6 }));
expect(found).toEqual([{ a: { b: { c: 6, d: 7 } } }]);
})
});
describe("$dec", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ a: 1 }, { $dec: { a: 5 } });
const found = nrml(collection.find({ c: 5 }));
expect(found).toEqual([{ a: -4, b: { c: 5 } }]);
});
test("works, syntax 2", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ a: 1 }, { $dec: 5 });
const found = nrml(collection.find({ c: 5 }));
expect(found).toEqual([{ a: -4, b: { c: 5 } }]);
});
test("implicitly creates properties", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.update({ a: 1 }, { $dec: { b: 5 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: -5 }]);
});
});
describe("$mult", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 5, b: { c: 5 } });
collection.update({ a: 5 }, { $mult: { a: 5 } });
const found = nrml(collection.find({ c: 5 }));
expect(found).toEqual([{ a: 25, b: { c: 5 } }]);
});
test("works, syntax 2", () => {
const collection = testCollection();
collection.insert({ a: 5, b: { c: 5 } });
collection.update({ a: 5 }, { $mult: 5 });
const found = nrml(collection.find({ c: 5 }));
expect(found).toEqual([{ a: 25, b: { c: 5 } }]);
});
test("implicitly creates properties", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.update({ a: 1 }, { $mult: { b: 5 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: 5 }]);
});
test("deep selector, implicitly creates properties", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: { d: 1 } } });
collection.update({ d: 1 }, { $inc: { e: 5 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: { d: 1 } }, e: 5 }]);
});
});
describe("special behavior with $has and $hasAny", ({ test }) => {
test("$has single property", () => {
const collection = testCollection();
collection.insert([{ a: { b: 1, c: 1 } }, { a: 1 }]);
collection.update({ a: { $has: "b" }}, { $inc: 5 });
const found = nrml(collection.find({ b: 6, c: 1 }));
expect(found).toEqual([{ a: { b: 6, c: 1 } }]);
});
test("$has array", () => {
const collection = testCollection();
collection.insert([{ a: { b: 1, c: 1 } }, { a: 1 }]);
collection.update({ a: { $has: ["b", "c"] }}, { $inc: 5 });
const found = nrml(collection.find({ b: 6, c: 6 }));
expect(found).toEqual([{ a: { b: 6, c: 6 } }]);
});
test("$hasAny, with one property missing", () => {
const collection = testCollection();
collection.insert([{ a: { b: 1 } }, { a: 1 }]);
collection.update({ a: { $hasAny: ["b", "c"] }}, { $inc: 5 });
const found = nrml(collection.find({ b: 6 }));
expect(found).toEqual([{ a: { b: 6 } }]);
});
test("$hasAny, updates all specified properties", () => {
const collection = testCollection();
collection.insert([{ a: { b: 1, c: 1 } }, { a: 1 }]);
collection.update({ a: { $hasAny: ["b", "c"] }}, { $inc: 5 });
const found = nrml(collection.find({ b: 6, c: 6 }));
expect(found).toEqual([{ a: { b: 6, c: 6 } }]);
});
test("$hasAny single property, dot notation", () => {
const collection = testCollection();
collection.insert([{ a: { b: { c: 1 } } }, { a: 1 }]);
collection.update({ "a.b": { $hasAny: "c" }}, { $inc: 5 });
const found = nrml(collection.find({ c: 6 }));
expect(found).toEqual([{ a: { b: { c: 6 } } }]);
});
test("$has real world test", () => {
const collection = testCollection();
collection.insert([
{ planet: { name: "Earth", population: 1 } },
{ planet: { name: "Mars" }},
]);
collection.update({ planet: { name: { $includes: "a" }, $has: "population" } }, { $inc: { "planet.population": 1 } });
const found = nrml(collection.find({ name: { $includes: "a" }}));
expect(found).toEqual([
{ planet: { name: "Earth", population: 2 }},
{ planet: { name: "Mars" }},
]);
});
});
});

View File

@ -0,0 +1,41 @@
import { expect, testSuite } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$merge", ({ test }) => {
test("shallow selector, deep-root update", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ a: 1 }, { $merge: { b: { d: 6 } } });
const found = nrml(collection.find({ d: 6 }));
expect(found).toEqual([{ a: 1, b: { c: 5, d: 6 } }]);
});
test("deep-root selector, deep-root update", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ a: 1, b: { c: 5 } }, { $merge: { b: { d: 6 } } });
const found = nrml(collection.find({ d: 6 }));
expect(found).toEqual([{ a: 1, b: { c: 5, d: 6 } }]);
});
test("overwrites existing properties", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update(
{ a: 1 },
{ $merge: { a: 2, b: { d: 6 } } }
);
const found = nrml(collection.find({ d: 6 }));
expect(found).toEqual([{ a: 2, b: { c: 5, d: 6 } }]);
});
test("deep selector merges deeply", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ c: 5 }, { $merge: { a: 2 } });
const found = nrml(collection.find({ a: 2 }));
expect(found).toEqual([{ a: 1, b: { c: 5, a: 2 } }]);
});
});
});

View File

@ -0,0 +1,73 @@
import { expect, testSuite } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$push", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 1, b: [1] });
collection.update({ a: 1 }, { $push: { b: 2 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: [1, 2] }]);
});
test("push more than one value", () => {
const collection = testCollection();
collection.insert({ a: 1, b: [1] });
collection.update({ a: 1 }, { $push: { b: [2, 3] } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: [1, 2, 3] }]);
});
test("push with dot notation", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 1, d: [1, 2]} });
collection.update({ c: 1 }, { $push: { "b.d": [3, 4] } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: 1, d: [1, 2, 3, 4] } }]);
});
test("push with dot notation, multiple pushes", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 1, d: [1, 2]}, e: { c: 1, d: [1, 2] } });
collection.update({ c: 1 }, { $push: { "b.d": [3, 4], "e.d": [3, 4] } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: 1, d: [1, 2, 3, 4] }, e: { c: 1, d: [1, 2, 3, 4] } }]);
});
test("push an object to an array of objects", () => {
const collection = testCollection();
collection.insert({ a: 1, b: [{ name: "a" }] });
collection.update({ a: 1 }, { $push: { b: { name: "b" } } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: [{ name: "a" }, { name: "b" }] }]);
});
test("push with dot notation, an object to an array of objects", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 1, d: [{ name: "a" }] } });
collection.update({ c: 1 }, { $push: { "b.d": { name: "b" } } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: 1, d: [{ name: "a" }, { name: "b" }] } }]);
});
test("push does not create the target array if it doesn't exist", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.update({ a: 1 }, { $push: { b: 1 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1 }]);
});
test("push with dot notation does not create the target array if it does not exist", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.update({ a: 1 }, { $push: { "b.c": 1 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1 }]);
});
});
});

View File

@ -0,0 +1,58 @@
import { expect, testSuite } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$set", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.update({ a: 2 }, { $set: { b: 3 } });
const found = nrml(collection.find({ a: 2 }));
expect(found).toEqual([{ a: 2, b: 3 }]);
});
test("deep selector doesn't implicitly update a deep property", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ c: 5 }, { $set: { d: 6 } });
const found = nrml(collection.find({ d: 6 }));
expect(found).toEqual([{ a: 1, b: { c: 5 }, d: 6 }]);
});
test("will create deep objects", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.update({ a: 1 }, { $set: { b: { c: 5 } } });
const found = nrml(collection.find({ b: { c: 5 } }));
expect(found).toEqual([{ a: 1, b: { c: 5 } }]);
});
test("does not merge objects, instead overwrites", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 5 } });
collection.update({ a: 1 }, { $set: { b: { d: 6 } } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { d: 6 } }]);
});
test("shorthand behavior", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 1 } });
collection.insert({ a: 2, b: { c: 2 } });
collection.update({ b: { c: 2 }}, { $set: 11 });
const found = nrml(collection.find({ a: 2 }));
expect(found).toEqual([{ a: 2, b: { c: 11 } }]);
});
test("works with dot notation", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 1 } });
collection.update({ "b.c": 1 }, { $set: { "b.c": 2 } });
const found = nrml(collection.find({ "b.c": 2 }));
expect(found).toEqual([{ a: 1, b: { c: 2 } }]);
});
});
});

View File

@ -0,0 +1,46 @@
import { expect, testSuite } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$unset", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 2, c: 3 });
collection.update({ a: 1 }, { $unset: "c" });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: 2 }]);
});
test("dot notation", () => {
const collection = testCollection();
collection.insert({ a: { b: { c: 1, d: 2, e: 3 } } });
collection.update({ e: 3 }, { $unset: "a.b.c" });
const found = nrml(collection.find({ e: 3 }));
expect(found).toEqual([{ a: { b: { d: 2, e: 3 } } }]);
});
test("dot notation array", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 1, d: 2 }, e: 3 });
collection.update({ a: 1 }, { $unset: ["e", "b.c"] });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { d: 2 } }]);
});
test("dot notation nested query", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 1, d: 2 }, e: 3 });
collection.update({ b: { d: 2 } }, { $unset: "b.d" });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: 1 }, e: 3 }]);
});
test("dot notation, remove all items from array", () => {
const collection = testCollection();
collection.insert({ a: 1, b: [{ c: 1, d: 1 }, { c: 2, d: 2 }] });
collection.update({ a: 1 }, { $unset: "b.*.c" });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: [{ d: 1 }, { d: 2 }] }]);
})
});
});

View File

@ -0,0 +1,73 @@
import { expect, testSuite } from "manten";
import { nrml, testCollection } from "../../../common";
export default testSuite(async ({ describe }) => {
describe("$unshift", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 1, b: [1] });
collection.update({ a: 1 }, { $unshift: { b: 2 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: [2, 1] }]);
});
test("unshift more than one value", () => {
const collection = testCollection();
collection.insert({ a: 1, b: [1] });
collection.update({ a: 1 }, { $unshift: { b: [2, 3] } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: [2, 3, 1] }]);
});
test("unshift with dot notation", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 1, d: [1, 2]} });
collection.update({ c: 1 }, { $unshift: { "b.d": [3, 4] } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: 1, d: [3, 4, 1, 2] } }]);
});
test("unshift with dot notation, multiple unshifts", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 1, d: [1, 2]}, e: { c: 1, d: [1, 2] } });
collection.update({ c: 1 }, { $unshift: { "b.d": [3, 4], "e.d": [3, 4] } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: 1, d: [3, 4, 1, 2] }, e: { c: 1, d: [3, 4, 1, 2] } }]);
});
test("unshift an object to an array of objects", () => {
const collection = testCollection();
collection.insert({ a: 1, b: [{ name: "a" }] });
collection.update({ a: 1 }, { $unshift: { b: { name: "b" } } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: [{ name: "b" }, { name: "a" }] }]);
});
test("unshift with dot notation, an object to an array of objects", () => {
const collection = testCollection();
collection.insert({ a: 1, b: { c: 1, d: [{ name: "a" }] } });
collection.update({ c: 1 }, { $unshift: { "b.d": { name: "b" } } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1, b: { c: 1, d: [{ name: "b" }, { name: "a" }] } }]);
});
test("unshift does not create the target array if it doesn't exist", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.update({ a: 1 }, { $unshift: { b: 1 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1 }]);
});
test("unshift with dot notation does not create the target array if it does not exist", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.update({ a: 1 }, { $unshift: { "b.c": 1 } });
const found = nrml(collection.find({ a: 1 }));
expect(found).toEqual([{ a: 1 }]);
});
});
});

View File

@ -0,0 +1,48 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("ifEmpty", ({ test }) => {
test("adds missing properties", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1, d: " " });
collection.insert({ a: 2, b: 2, c: 2, d: [] });
collection.insert({ a: 3, b: 3, c: 3, d: { e: "test" } });
const found = nrml(collection.find({ a: { $gt: 0 } }, { ifEmpty: { d: 5 } }));
expect(found).toEqual([
{ a: 1, b: 1, c: 1, d: 5 },
{ a: 2, b: 2, c: 2, d: 5 },
{ a: 3, b: 3, c: 3, d: { e: "test" } },
]);
});
test("adds missing properties using dot notation", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1, d: { e: " " } });
collection.insert({ a: 2, b: 2, c: 2, d: { e: [] } });
collection.insert({ a: 3, b: 3, c: 3, d: { e: "test" } });
const found = nrml(collection.find({ a: { $gt: 0 } }, { ifEmpty: { "d.e": 5 } }));
expect(found).toEqual([
{ a: 1, b: 1, c: 1, d: { e: 5 } },
{ a: 2, b: 2, c: 2, d: { e: 5 } },
{ a: 3, b: 3, c: 3, d: { e: "test" } },
]);
});
test("does not create new properties", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1, d: " " });
collection.insert({ a: 2, b: 2, c: 2, d: [] });
collection.insert({ a: 3, b: 3, c: 3, d: { e: "test" } });
const found = nrml(collection.find({ a: { $gt: 0 } }, { ifEmpty: { e: 5 } }));
expect(found).toEqual([
{ a: 1, b: 1, c: 1, d: " " },
{ a: 2, b: 2, c: 2, d: [] },
{ a: 3, b: 3, c: 3, d: { e: "test" } },
]);
});
});
});

View File

@ -0,0 +1,70 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("ifNull", ({ test }) => {
test("adds missing properties", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2, d: null });
collection.insert({ a: 3, b: 3, c: 3, d: "test" });
const found = nrml(collection.find({ a: { $gt: 0 } }, { ifNull: { d: 5 } }));
expect(found).toEqual([
{ a: 1, b: 1, c: 1, d: 5 },
{ a: 2, b: 2, c: 2, d: 5 },
{ a: 3, b: 3, c: 3, d: "test" },
]);
});
test("adds missing properties using dot notation", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
collection.insert({ a: 3, b: 3, c: 3, d: { e: "test" } });
const found = nrml(collection.find({ a: { $gt: 0 } }, { ifNull: { "d.e": 5 } }));
expect(found).toEqual([
{ a: 1, b: 1, c: 1, d: { e: 5 } },
{ a: 2, b: 2, c: 2, d: { e: 5 } },
{ a: 3, b: 3, c: 3, d: { e: "test" } },
]);
});
test("doesn't overwrite properties", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
collection.insert({ a: 3, b: 3, c: 3, d: { e: "test" } });
const found = nrml(collection.find({ a: { $gt: 0 } }, { ifNull: { c: 5 } }));
expect(found).toEqual([
{ a: 1, b: 1, c: 1 },
{ a: 2, b: 2, c: 2 },
{ a: 3, b: 3, c: 3, d: { e: "test" } },
]);
});
test("works when the new value is a complex object", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
const found = nrml(collection.find({ a: { $gt: 0 } }, { ifNull: { d: { e: 5 } } }));
expect(found).toEqual([{ a: 1, b: 1, c: 1, d: { e: 5 } }, { a: 2, b: 2, c: 2, d: { e: 5 } }]);
});
test("works when the new value is a null value", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
const found = nrml(collection.find({ a: { $gt: 0 } }, { ifNull: { d: null } }));
expect(found).toEqual([{ a: 1, b: 1, c: 1, d: null }, { a: 2, b: 2, c: 2, d: null }]);
});
test("ifnull receives a function which is passed the document", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
const found = nrml(collection.find({ a: { $gt: 0 } }, { ifNull: { d: (doc) => doc.a } }));
expect(found).toEqual([{ a: 1, b: 1, c: 1, d: 1 }, { a: 2, b: 2, c: 2, d: 2 }]);
});
});
});

View File

@ -0,0 +1,35 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("ifNullOrEmpty", ({ test }) => {
test("adds missing properties", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2, d: [] });
collection.insert({ a: 3, b: 3, c: 3, d: "test" });
const found = nrml(collection.find({ a: { $gt: 0 } }, { ifNullOrEmpty: { d: 5 } }));
expect(found).toEqual([
{ a: 1, b: 1, c: 1, d: 5 },
{ a: 2, b: 2, c: 2, d: 5 },
{ a: 3, b: 3, c: 3, d: "test" }
]);
});
test("adds missing properties using dot notation", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2, d: { e: [] } });
collection.insert({ a: 3, b: 3, c: 3, d: { e: "test" } });
const found = nrml(collection.find({ a: { $gt: 0 } }, { ifNullOrEmpty: { "d.e": 5 } }));
expect(found).toEqual([
{ a: 1, b: 1, c: 1, d: { e: 5 } },
{ a: 2, b: 2, c: 2, d: { e: 5 } },
{ a: 3, b: 3, c: 3, d: { e: "test" } }
]);
});
});
});

View File

@ -0,0 +1,14 @@
import { testSuite } from "manten";
export default testSuite(async ({ describe }) => {
describe("query options", async ({ runTestSuite }) => {
runTestSuite(import("./sort.test.js"));
runTestSuite(import("./integerIds.test.js"));
runTestSuite(import("./project.test.js"));
runTestSuite(import("./skip_take.test.js"));
runTestSuite(import("./join.test.js"));
runTestSuite(import("./ifNull.test.js"));
runTestSuite(import("./ifEmpty.test.js"));
runTestSuite(import("./ifNullOrEmpty.test.js"));
});
});

View File

@ -0,0 +1,21 @@
import { testSuite, expect } from "manten";
import { ID_KEY } from "../../../src/collection";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("integerIds", ({ test }) => {
test("works", () => {
const collection = testCollection({ integerIds: true });
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const found = nrml(collection.find({ a: { $lt: 5 } }), { keepIds: true });
// these start at 3 because testCollection adds 3 documents.
expect(found).toEqual([
{ a: 1, [ID_KEY]: 3 },
{ a: 2, [ID_KEY]: 4 },
{ a: 3, [ID_KEY]: 5 },
]);
});
});
});

View File

@ -0,0 +1,318 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("join", ({ test }) => {
test("works", () => {
const users = testCollection();
const tickets = testCollection({ name: "tickets", integerIds: true, timestamps: false });
users.insert({ name: "Jonathan", tickets: [3, 4] });
tickets.insert({ title: "Ticket 0", description: "Ticket 0 description" });
tickets.insert({ title: "Ticket 1", description: "Ticket 1 description" });
tickets.insert({ title: "Ticket 2", description: "Ticket 2 description" });
const res = nrml(users.find({ name: "Jonathan" }, {
join: [{
collection: tickets,
from: "tickets",
on: "_id",
as: "userTickets",
options: {
project: { _id: 0 },
},
}],
}))[0];
expect(res).toEqual({
name: "Jonathan",
tickets: [3, 4],
userTickets: [
{ title: "Ticket 0", description: "Ticket 0 description" },
{ title: "Ticket 1", description: "Ticket 1 description" },
],
});
});
test("will overwrite original property", () => {
const users = testCollection();
const tickets = testCollection({ name: "tickets", integerIds: true, timestamps: false });
users.insert({ name: "Jonathan", tickets: [3, 4] });
tickets.insert({ title: "Ticket 0", description: "Ticket 0 description" });
tickets.insert({ title: "Ticket 1", description: "Ticket 1 description" });
tickets.insert({ title: "Ticket 2", description: "Ticket 2 description" });
const res = nrml(users.find({ name: "Jonathan" }, {
join: [{
collection: tickets,
from: "tickets",
on: "_id",
as: "tickets",
options: {
project: { _id: 0 },
}
}],
}))[0];
const tks = tickets.find({ _id: { $oneOf: [3, 4] } });
expect(res).toEqual({
name: "Jonathan",
tickets: [
{ title: "Ticket 0", description: "Ticket 0 description" },
{ title: "Ticket 1", description: "Ticket 1 description" },
],
});
});
test("creates the 'as' property even when nothing matches", () => {
const users = testCollection();
const tickets = testCollection({ name: "tickets" });
users.insert({ name: "Jonathan", tickets: [] });
const res = nrml(users.find({ name: "Jonathan" }, {
join: [{
collection: tickets,
from: "tickets",
on: "_id",
as: "userTickets",
}],
}))[0];
expect(res).toHaveProperty("userTickets");
expect((res as any).userTickets).toEqual([]);
});
test("creates the 'as' property even when nothing matches, dot notation", () => {
const users = testCollection();
const tickets = testCollection({ name: "tickets" });
users.insert({ name: "Jonathan", tickets: [] });
const res = nrml(users.find({ name: "Jonathan" }, {
join: [{
collection: tickets,
from: "tickets",
on: "_id",
as: "user.tickets",
}],
}))[0];
expect(res).toHaveProperty("user.tickets");
expect((res as any).user.tickets).toEqual([]);
});
test("respects QueryOptions", () => {
const users = testCollection();
const tickets = testCollection({ name: "tickets", integerIds: true });
users.insert({ name: "Jonathan", tickets: [3, 4] });
tickets.insert({ title: "Ticket 0", description: "Ticket 0 description" });
tickets.insert({ title: "Ticket 1", description: "Ticket 1 description" });
tickets.insert({ title: "Ticket 2", description: "Ticket 2 description" });
const res = nrml(users.find({ name: "Jonathan" }, {
join: [{
collection: tickets,
from: "tickets",
on: "_id",
as: "userTickets",
options: { project: { title: 1 } },
}],
}))[0];
expect(res).toEqual({
name: "Jonathan",
tickets: [3, 4],
userTickets: [{ title: "Ticket 0" }, { title: "Ticket 1" }],
});
});
test("multiple joins", () => {
const users = testCollection();
const skills = testCollection({ name: "skills", integerIds: true });
const items = testCollection({ name: "items", integerIds: true });
users.insert({ name: "Jonathan", skills: [3, 4], items: [4, 5] });
skills.insert({ title: "Skill 0" });
skills.insert({ title: "Skill 1" });
skills.insert({ title: "Skill 2" });
items.insert({ title: "Item 0" });
items.insert({ title: "Item 1" });
items.insert({ title: "Item 2" });
const res = nrml(
users.find(
{ name: "Jonathan" },
{
join: [
{
collection: skills,
from: "skills",
on: "_id",
as: "userSkills",
},
{
collection: items,
from: "items",
on: "_id",
as: "userItems",
},
],
}
)
)[0];
const sks = skills.find({ _id: { $oneOf: [3, 4] } });
const its = items.find({ _id: { $oneOf: [4, 5] } });
expect(res).toEqual({
name: "Jonathan",
skills: [3, 4],
items: [4, 5],
userSkills: [...sks],
userItems: [...its],
});
});
test("nested joins", () => {
const users = testCollection({ timestamps: false });
const tickets = testCollection({ name: "tickets", integerIds: true, timestamps: false });
const seats = testCollection({ name: "seats", integerIds: true, timestamps: false });
users.insert({ name: "Jonathan", tickets: [3, 4] });
tickets.insert({ title: "Ticket 0", seat: 3 });
tickets.insert({ title: "Ticket 1", seat: 5 });
tickets.insert({ title: "Ticket 2" });
seats.insert({ seat: "S3" });
seats.insert({ seat: "S4" });
seats.insert({ seat: "S5" });
const res = nrml(users.find({ name: "Jonathan" }, {
join: [{
collection: tickets,
from: "tickets",
on: "_id",
as: "userTickets",
options: {
project: { _id: 0 },
join: [{
collection: seats,
from: "seat",
on: "_id",
as: "ticketSeats",
options: {
project: { _id: 0 },
}
}]
},
}],
project: { _id: 0 },
}))[0];
expect(res).toEqual({
name: "Jonathan",
tickets: [3, 4],
userTickets: [
{
title: "Ticket 0",
seat: 3,
ticketSeats: [{ seat: "S3" }],
},
{
title: "Ticket 1",
seat: 5,
ticketSeats: [{ seat: "S5" }],
},
]
});
});
test("with join.from and join.as dot notation, accessing array index on join.as", () => {
const inventory = testCollection();
const items = testCollection({ name: "items", integerIds: true });
inventory.insert({
name: "Jonathan",
items: [
{ itemId: 3, quantity: 1 },
{ itemId: 5, quantity: 2 },
],
});
items.insert({ name: "The Unstoppable Force", atk: 100 }); // id 3
items.insert({ name: "Sneakers", agi: 100 }); // id 4
items.insert({ name: "The Immovable Object", def: 100 }); // id 5
const res = nrml(inventory.find({ name: "Jonathan" }, {
join: [{
collection: items,
from: "items.*.itemId",
on: "_id",
as: "items.*.itemData",
options: {
project: { _id: 0, _created_at: 0, _updated_at: 0 },
}
}],
}))[0];
expect(res).toEqual({
name: "Jonathan",
items: [
{ itemId: 3, quantity: 1, itemData: { name: "The Unstoppable Force", atk: 100 } },
{ itemId: 5, quantity: 2, itemData: { name: "The Immovable Object", def: 100 } },
],
})
});
test("with join.from and join.as dot notation, no array '*' on join.as", () => {
const inventory = testCollection();
const items = testCollection({ name: "items", integerIds: true });
inventory.insert({
name: "Jonathan",
items: [
{ itemId: 3, quantity: 1 },
{ itemId: 5, quantity: 2 },
],
meta: {
data: [],
}
});
items.insert({ name: "The Unstoppable Force", atk: 100 }); // id 3
items.insert({ name: "Sneakers", agi: 100 }); // id 4
items.insert({ name: "The Immovable Object", def: 100 }); // id 5
const res = nrml(inventory.find({ name: "Jonathan" }, {
join: [{
collection: items,
from: "items.*.itemId",
on: "_id",
as: "meta.data",
options: {
project: { _id: 0, _created_at: 0, _updated_at: 0 },
}
}],
}))[0];
expect(res).toEqual({
name: "Jonathan",
items: [
{ itemId: 3, quantity: 1 },
{ itemId: 5, quantity: 2 },
],
meta: {
data: [
{ name: "The Unstoppable Force", atk: 100 },
{ name: "The Immovable Object", def: 100 }
],
},
});
})
});
});

View File

@ -0,0 +1,221 @@
import { testSuite, expect } from "manten";
import { CREATED_AT_KEY, ID_KEY, UPDATED_AT_KEY } from "../../../src/collection";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("project", ({ test }) => {
test("implicit exclusion", () => {
const collection = testCollection({ timestamps: false });
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
collection.insert({ a: 3, b: 3, c: 3 });
const found = collection.find({ a: 1 }, { project: { b: 1 } });
expect(found).toEqual([{ b: 1 }]);
});
test("implicit inclusion", () => {
const collection = testCollection({ timestamps: false });
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
collection.insert({ a: 3, b: 3, c: 3 });
const found = collection.find({ a: 1 }, { project: { b: 0 } });
const id = found[0][ID_KEY];
expect(id).toBeDefined();
expect(found).toEqual([{ _id: id, a: 1, c: 1 }]);
});
test("implicit inclusion - _id implicitly included", () => {
const collection = testCollection({ timestamps: false });
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
collection.insert({ a: 3, b: 3, c: 3 });
const foundWithId = collection.find({ a: 1 }, { project: { b: 0 } });
const id = foundWithId[0][ID_KEY];
expect(id).toBeDefined();
expect(foundWithId).toEqual([{ _id: id, a: 1, c: 1 }]);
});
test("explicit", () => {
const collection = testCollection({ timestamps: false });
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
collection.insert({ a: 3, b: 3, c: 3 });
const found = nrml(collection.find({ a: 1 }, { project: { b: 1, c: 0 } }));
expect(found).toEqual([{ a: 1, b: 1 }]);
});
test("explicit - ID_KEY implicitly included", () => {
const collection = testCollection({ timestamps: false });
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
collection.insert({ a: 3, b: 3, c: 3 });
const foundWithId = collection.find(
{ a: 1 },
{
project: {
b: 1,
c: 0,
_created_at: 0,
_updated_at: 0,
},
}
);
const id = foundWithId[0][ID_KEY];
expect(id).toBeDefined();
expect(foundWithId).toEqual([{ _id: id, a: 1, b: 1 }]);
});
test("empty query respects projection", () => {
const collection = testCollection({ timestamps: false });
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
const found = collection.find({}, { project: { b: 1 } });
for (const doc of found) {
expect(doc[ID_KEY]).toBeUndefined();
expect(doc[CREATED_AT_KEY]).toBeUndefined();
expect(doc[UPDATED_AT_KEY]).toBeUndefined();
}
});
describe("aggregation", ({ test }) => {
test("$floor, $ceil, $sub, $add, $mult, $div", () => {
const collection = testCollection({ timestamps: false });
collection.insert({ a: 1, b: 1, c: 5.6 });
collection.insert({ a: 2, b: 2, c: 2 });
collection.insert({ a: 3, b: 3, c: 3 });
const found = collection.find(
{ a: 1 },
{
aggregate: {
flooredC: { $floor: "c" },
ceiledC: { $ceil: "c" },
subbed1: { $sub: ["c", "a"] },
subbed2: { $sub: [15, "flooredC", 0, 1] },
mult1: { $mult: ["c", 2, "subbed2"] },
div1: { $div: ["subbed2", 2, "a", 2] },
add1: { $add: ["c", 2, "subbed2"] },
},
project: {
b: 0,
_created_at: 0,
_updated_at: 0,
_id: 0,
},
}
);
expect(found).toEqual([{
a: 1,
c: 5.6,
flooredC: 5,
ceiledC: 6,
subbed1: 4.6,
subbed2: 9,
mult1: 100.8,
div1: 2.25,
add1: 16.6,
}]);
});
test("more realistic use-case", () => {
const collection = testCollection({ timestamps: false });
collection.insert({ math: 72, english: 82, science: 92 });
collection.insert({ math: 60, english: 70, science: 80 });
collection.insert({ math: 90, english: 72, science: 84 });
const found = nrml(collection.find(
{ $has: ["math", "english", "science"] },
{
aggregate: {
total: { $add: ["math", "english", "science"] },
average: { $div: ["total", 3] },
},
}
));
expect(found).toEqual([
{ math: 72, english: 82, science: 92, total: 246, average: 82 },
{ math: 60, english: 70, science: 80, total: 210, average: 70 },
{ math: 90, english: 72, science: 84, total: 246, average: 82 },
]);
});
test("remove intermediate aggregation properties with projection", () => {
const collection = testCollection();
collection.insert({ math: 72, english: 82, science: 92 });
collection.insert({ math: 60, english: 70, science: 80 });
collection.insert({ math: 90, english: 72, science: 84 });
const found = nrml(collection.find(
{ $has: ["math", "english", "science"] },
{
aggregate: {
total: { $add: ["math", "english", "science"] }, // <-- projected out
average: { $div: ["total", 3] },
},
project: {
math: 1,
english: 1,
science: 1,
average: 1,
},
}
));
expect(found).toEqual([
{ math: 72, english: 82, science: 92, average: 82 },
{ math: 60, english: 70, science: 80, average: 70 },
{ math: 90, english: 72, science: 84, average: 82 },
]);
});
test("accessing properties with dot notation", () => {
const collection = testCollection();
collection.insert({ a: { b: { c: 1 } } });
collection.insert({ a: { b: { c: 2 } } });
collection.insert({ a: { b: { c: 3 } } });
const found = collection.find(
{ a: { b: { c: 1 } } },
{
aggregate: {
d: { $add: ["a.b.c", 1] },
},
project: {
a: 0,
_created_at: 0,
_updated_at: 0,
_id: 0,
},
}
);
expect(found).toEqual([{ d: 2 }]);
});
test("$fn", () => {
const collection = testCollection();
collection.insert({ first: "John", last: "Doe" });
collection.insert({ first: "Jane", last: "Doe" });
const found = nrml(collection.find(
{ $has: ["first", "last"] },
{
aggregate: {
fullName: { $fn: (doc) => `${doc.first} ${doc.last}` },
},
}
));
expect(found).toEqual([
{ first: "John", last: "Doe", fullName: "John Doe" },
{ first: "Jane", last: "Doe", fullName: "Jane Doe" },
]);
});
});
});
});

View File

@ -0,0 +1,37 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("skip", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
collection.insert({ a: 3, b: 3, c: 3 });
const found = nrml(collection.find({ a: { $gt: 0 } }, { skip: 1 }));
expect(found).toEqual([{ a: 2, b: 2, c: 2 }, { a: 3, b: 3, c: 3 }]);
});
});
describe("take", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
collection.insert({ a: 3, b: 3, c: 3 });
const found = nrml(collection.find({ a: { $gt: 0 } }, { take: 1 }));
expect(found).toEqual([{ a: 1, b: 1, c: 1 }]);
});
});
describe("skip take", ({ test }) => {
test("works", () => {
const collection = testCollection();
collection.insert({ a: 1, b: 1, c: 1 });
collection.insert({ a: 2, b: 2, c: 2 });
collection.insert({ a: 3, b: 3, c: 3 });
const found = nrml(collection.find({ a: { $gt: 0 } }, { skip: 1, take: 1 }));
expect(found).toEqual([{ a: 2, b: 2, c: 2 }]);
});
});
});

View File

@ -0,0 +1,57 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("sort", ({ test }) => {
test("ascending", () => {
const collection = testCollection();
collection.insert({ a: 2 });
collection.insert({ a: 1 });
collection.insert({ a: 3 });
const found = nrml(collection.find({ a: { $lt: 5 } }, { sort: { a: 1 } }));
expect(found).toEqual([{ a: 1 }, { a: 2 }, { a: 3 }]);
});
test("ascending update results", () => {
const collection = testCollection();
collection.insert({ a: 2 });
collection.insert({ a: 1 });
collection.insert({ a: 3 });
const found = nrml(collection.update({ a: { $lt: 10 } }, { $inc: 5 }, { sort: { a: 1 } }));
expect(found).toEqual([{ a: 6 }, { a: 7 }, { a: 8 }]);
});
test("descending with -1", () => {
const collection = testCollection();
collection.insert({ a: 2 });
collection.insert({ a: 1 });
collection.insert({ a: 3 });
const found = nrml(collection.find({ a: { $lt: 5 } }, { sort: { a: -1 } }));
expect(found).toEqual([{ a: 3 }, { a: 2 }, { a: 1 }]);
});
test("descending with 0", () => {
const collection = testCollection();
collection.insert({ a: 2 });
collection.insert({ a: 1 });
collection.insert({ a: 3 });
const found = nrml(collection.find({ a: { $lt: 5 } }, { sort: { a: 0 } }));
expect(found).toEqual([{ a: 3 }, { a: 2 }, { a: 1 }]);
});
test("more than one property, asc and desc, numeric and alphanumeric", () => {
const collection = testCollection();
collection.insert({ name: "Deanna Troi", age: 28 });
collection.insert({ name: "Worf", age: 24 });
collection.insert({ name: "Xorf", age: 24 });
collection.insert({ name: "Zorf", age: 24 });
collection.insert({ name: "Jean-Luc Picard", age: 59 });
collection.insert({ name: "William Riker", age: 29 });
const found = nrml(collection.find({ age: { $gt: 1 } }, { sort: { age: 1, name: -1 } }));
expect(found).toEqual([
{ name: "Zorf", age: 24 },
{ name: "Xorf", age: 24 },
{ name: "Worf", age: 24 },
{ name: "Deanna Troi", age: 28 },
{ name: "William Riker", age: 29 },
{ name: "Jean-Luc Picard", age: 59 },
]);
});
});
});

View File

@ -0,0 +1,24 @@
import { testSuite, expect } from "manten";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("remove", ({ test }) => {
test("it works", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const removed = nrml(collection.remove({ a: 2 }));
const found = nrml(collection.find({ a: { $lt: 5 } }));
expect(removed).toEqual([{ a: 2 }]);
expect(found).toEqual([{ a: 1 }, { a: 3 }]);
});
test("normalizes internal id_map", () => {
const collection = testCollection({ integerIds: true });
collection.insert({ a: 1 });
expect(collection.data["internal"]["id_map"][3]).toBeDefined();
collection.remove({ a: 1 });
expect(collection.data["internal"]["id_map"][3]).toBeUndefined();
});
});
});

View File

@ -0,0 +1,7 @@
import { testSuite } from "manten";
export default testSuite(async ({ describe }) => {
describe("removing", async ({ runTestSuite }) => {
runTestSuite(import("./basic.test.js"));
});
});

View File

@ -0,0 +1,30 @@
import { testSuite, expect } from "manten";
import { getShardedCollection } from "../../common";
export default testSuite(async ({ describe }) => {
describe("sharding", ({ test }) => {
test("works", () => {
const c = getShardedCollection();
const docs = [];
for (let i = 0; i < 250; i++) {
docs.push({ key: i });
}
c.insert(docs);
expect(Object.keys(c.shards).length).toEqual(3);
expect(Object.keys(c.shards).every((shardId) => Object.keys(c.shards[shardId].data).length === 85));
const found = c.find({ key: 1 });
expect(found.length).toEqual(1);
c.drop();
c.sync();
});
});
});

View File

@ -0,0 +1,7 @@
import { testSuite } from "manten";
export default testSuite(async ({ describe }) => {
describe("sharded collection", async ({ runTestSuite }) => {
runTestSuite(import("./basic.test.js"));
});
});

View File

@ -0,0 +1,180 @@
import { testSuite, expect } from "manten";
import { Transaction } from "../../../src/transaction";
import { nrml, testCollection } from "../../common";
export default testSuite(async ({ describe, test }) => {
describe("inserts", ({ test }) => {
test("will insert and remove on rollback", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const original = collection.find();
collection.transaction((t) => {
t.insert({ a: 4 });
const found = nrml(collection.find({ a: 4 }));
expect(found).toEqual([{ a: 4 }]);
t.rollback();
const found2 = nrml(collection.find({ a: 4 }));
expect(found2).toEqual([]);
});
const latest = collection.find();
expect(latest).toEqual(original);
});
test("will insert many and remove all on rollback", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const original = collection.find();
collection.transaction((t) => {
t.insert([{ a: 4 }, { a: 5 }]);
const found = nrml(collection.find({ a: { $gt: 3 } }));
expect(found).toEqual([{ a: 4 }, { a: 5 }]);
t.rollback();
const found2 = nrml(collection.find({ a: { $gt: 3 } }));
expect(found2).toEqual([]);
});
const latest = collection.find();
expect(latest).toEqual(original);
});
});
describe("updates", ({ test }) => {
test("will update and revert on rollback", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const original = collection.find();
collection.transaction((t) => {
t.update({ a: 1 }, { $set: { a: 4 } });
const found = nrml(collection.find({ a: 4 }));
expect(found).toEqual([{ a: 4 }]);
t.rollback();
const found2 = nrml(collection.find({ a: 4 }));
expect(found2).toEqual([]);
const found3 = nrml(collection.find({ a: 1 }));
expect(found3).toEqual([{ a: 1 }]);
});
const latest = collection.find();
expect(latest).toEqual(original);
});
test("will update many and revert all on rollback", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const original = collection.find();
collection.transaction((t) => {
t.update({ a: { $gt: 1 } }, { $inc: 5 });
const found = nrml(collection.find({ a: { $gt: 1 } }));
expect(found).toEqual([{ a: 7 }, { a: 8 }]);
t.rollback();
const found2 = nrml(collection.find({ a: { $gt: 3 } }));
expect(found2).toEqual([]);
const found3 = nrml(collection.find({ a: { $gt: 0 } }));
expect(found3).toEqual([{ a: 1 }, { a: 2 }, { a: 3 }]);
});
const latest = collection.find();
expect(latest).toEqual(original);
});
});
describe("removes", ({ test }) => {
test("will remove and restore on rollback", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const original = collection.find();
collection.transaction((t) => {
t.remove({ a: 3 });
const found = nrml(collection.find({ a: 3 }));
expect(found).toEqual([]);
t.rollback();
const found2 = nrml(collection.find({ a: 3 }));
expect(found2).toEqual([{ a: 3 }]);
});
const latest = collection.find();
expect(latest).toEqual(original);
});
test("will remove many and restore all on rollback", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const original = collection.find();
collection.transaction((t) => {
t.remove({ a: { $gt: 1 } });
const found = nrml(collection.find({ a: { $gt: 1 } }));
expect(found).toEqual([]);
t.rollback();
const found2 = nrml(collection.find({ a: { $gt: 1 } }));
expect(found2).toEqual([{ a: 2 }, { a: 3 }]);
});
const latest = collection.find();
expect(latest).toEqual(original);
});
});
test("throws if already in a transaction", () => {
const collection = testCollection();
collection.transaction((tx) => {
expect(collection.transaction).toThrow();
});
});
test("throwing inside a transaction rolls it back", () => {
const collection = testCollection();
collection.insert({ a: 1 });
collection.insert({ a: 2 });
collection.insert({ a: 3 });
const original = collection.find();
try {
collection.transaction((t) => {
t.insert({ a: 4 });
throw new Error("test");
});
} catch (e) {
// ignore
}
const latest = collection.find();
expect(latest).toEqual(original);
});
});

View File

@ -0,0 +1,5 @@
import { testSuite } from "manten";
export default testSuite(async ({ runTestSuite }) => {
runTestSuite(import("./basic.test.js"));
});

Some files were not shown because too many files have changed in this diff Show More