mirror of
https://github.com/nvms/prsm.git
synced 2025-12-16 08:00:53 +00:00
degit arc
This commit is contained in:
parent
ddb5676b74
commit
a209d10566
@ -1 +0,0 @@
|
|||||||
Subproject commit 595e0b41961a6504b957173fa6470e4e21d16296
|
|
||||||
2
packages/arc-degit/.gitignore
vendored
Normal file
2
packages/arc-degit/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.test
|
||||||
|
benchmark
|
||||||
4
packages/arc-degit/.npmignore
Normal file
4
packages/arc-degit/.npmignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
tests
|
||||||
|
.test
|
||||||
|
src
|
||||||
1039
packages/arc-degit/README.md
Normal file
1039
packages/arc-degit/README.md
Normal file
File diff suppressed because it is too large
Load Diff
7
packages/arc-degit/bump.config.ts
Normal file
7
packages/arc-degit/bump.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "bumpp";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
commit: "%s release",
|
||||||
|
push: true,
|
||||||
|
tag: true,
|
||||||
|
});
|
||||||
BIN
packages/arc-degit/bun.lockb
Executable file
BIN
packages/arc-degit/bun.lockb
Executable file
Binary file not shown.
34
packages/arc-degit/package.json
Normal file
34
packages/arc-degit/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
100
packages/arc-degit/src/adapter/enc_fs.ts
Normal file
100
packages/arc-degit/src/adapter/enc_fs.ts
Normal 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();
|
||||||
|
};
|
||||||
85
packages/arc-degit/src/adapter/fs.ts
Normal file
85
packages/arc-degit/src/adapter/fs.ts
Normal 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);
|
||||||
|
}
|
||||||
14
packages/arc-degit/src/adapter/index.ts
Normal file
14
packages/arc-degit/src/adapter/index.ts
Normal 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;
|
||||||
|
}
|
||||||
27
packages/arc-degit/src/adapter/localStorage.ts
Normal file
27
packages/arc-degit/src/adapter/localStorage.ts
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
55
packages/arc-degit/src/append_props.ts
Normal file
55
packages/arc-degit/src/append_props.ts
Normal 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;
|
||||||
|
}
|
||||||
67
packages/arc-degit/src/change_props.ts
Normal file
67
packages/arc-degit/src/change_props.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
528
packages/arc-degit/src/collection.ts
Normal file
528
packages/arc-degit/src/collection.ts
Normal 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-degit/src/find.ts
Normal file
121
packages/arc-degit/src/find.ts
Normal 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-degit/src/ids.ts
Normal file
66
packages/arc-degit/src/ids.ts
Normal 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}`;
|
||||||
|
};
|
||||||
|
}
|
||||||
6
packages/arc-degit/src/index.ts
Normal file
6
packages/arc-degit/src/index.ts
Normal 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";
|
||||||
29
packages/arc-degit/src/operators/boolean/and.ts
Normal file
29
packages/arc-degit/src/operators/boolean/and.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
28
packages/arc-degit/src/operators/boolean/fn.ts
Normal file
28
packages/arc-degit/src/operators/boolean/fn.ts
Normal 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;
|
||||||
|
}
|
||||||
63
packages/arc-degit/src/operators/boolean/gtlt.ts
Normal file
63
packages/arc-degit/src/operators/boolean/gtlt.ts
Normal 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);
|
||||||
|
}
|
||||||
30
packages/arc-degit/src/operators/boolean/has.ts
Normal file
30
packages/arc-degit/src/operators/boolean/has.ts
Normal 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;
|
||||||
|
}
|
||||||
18
packages/arc-degit/src/operators/boolean/hasAny.ts
Normal file
18
packages/arc-degit/src/operators/boolean/hasAny.ts
Normal 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));
|
||||||
|
}
|
||||||
26
packages/arc-degit/src/operators/boolean/includes.ts
Normal file
26
packages/arc-degit/src/operators/boolean/includes.ts
Normal 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);
|
||||||
|
}
|
||||||
25
packages/arc-degit/src/operators/boolean/length.ts
Normal file
25
packages/arc-degit/src/operators/boolean/length.ts
Normal 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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
35
packages/arc-degit/src/operators/boolean/not.ts
Normal file
35
packages/arc-degit/src/operators/boolean/not.ts
Normal 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);
|
||||||
|
}
|
||||||
26
packages/arc-degit/src/operators/boolean/oneOf.ts
Normal file
26
packages/arc-degit/src/operators/boolean/oneOf.ts
Normal 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;
|
||||||
|
}
|
||||||
28
packages/arc-degit/src/operators/boolean/or.ts
Normal file
28
packages/arc-degit/src/operators/boolean/or.ts
Normal 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;
|
||||||
|
}
|
||||||
14
packages/arc-degit/src/operators/boolean/re.ts
Normal file
14
packages/arc-degit/src/operators/boolean/re.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
39
packages/arc-degit/src/operators/boolean/xor.ts
Normal file
39
packages/arc-degit/src/operators/boolean/xor.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
79
packages/arc-degit/src/operators/index.ts
Normal file
79
packages/arc-degit/src/operators/index.ts
Normal 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;
|
||||||
|
}
|
||||||
43
packages/arc-degit/src/operators/mutation/change.ts
Normal file
43
packages/arc-degit/src/operators/mutation/change.ts
Normal 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;
|
||||||
|
}
|
||||||
48
packages/arc-degit/src/operators/mutation/filter.ts
Normal file
48
packages/arc-degit/src/operators/mutation/filter.ts
Normal 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 } }");
|
||||||
|
}
|
||||||
|
|
||||||
19
packages/arc-degit/src/operators/mutation/map.ts
Normal file
19
packages/arc-degit/src/operators/mutation/map.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
221
packages/arc-degit/src/operators/mutation/math.ts
Normal file
221
packages/arc-degit/src/operators/mutation/math.ts
Normal 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);
|
||||||
|
}
|
||||||
18
packages/arc-degit/src/operators/mutation/merge.ts
Normal file
18
packages/arc-degit/src/operators/mutation/merge.ts
Normal 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;
|
||||||
|
}
|
||||||
30
packages/arc-degit/src/operators/mutation/push.ts
Normal file
30
packages/arc-degit/src/operators/mutation/push.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
37
packages/arc-degit/src/operators/mutation/set.ts
Normal file
37
packages/arc-degit/src/operators/mutation/set.ts
Normal 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;
|
||||||
|
}
|
||||||
26
packages/arc-degit/src/operators/mutation/unset.ts
Normal file
26
packages/arc-degit/src/operators/mutation/unset.ts
Normal 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;
|
||||||
|
}
|
||||||
33
packages/arc-degit/src/operators/mutation/unshift.ts
Normal file
33
packages/arc-degit/src/operators/mutation/unshift.ts
Normal 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);
|
||||||
|
}
|
||||||
244
packages/arc-degit/src/query_options.ts
Normal file
244
packages/arc-degit/src/query_options.ts
Normal 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;
|
||||||
|
};
|
||||||
174
packages/arc-degit/src/return_found.ts
Normal file
174
packages/arc-degit/src/return_found.ts
Normal 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;
|
||||||
|
};
|
||||||
141
packages/arc-degit/src/sharded_collection.ts
Normal file
141
packages/arc-degit/src/sharded_collection.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
packages/arc-degit/src/transaction.ts
Normal file
124
packages/arc-degit/src/transaction.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
82
packages/arc-degit/src/update.ts
Normal file
82
packages/arc-degit/src/update.ts
Normal 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-degit/src/utils.ts
Normal file
57
packages/arc-degit/src/utils.ts
Normal 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;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
68
packages/arc-degit/tests/common.ts
Normal file
68
packages/arc-degit/tests/common.ts
Normal 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;
|
||||||
|
}); }
|
||||||
58
packages/arc-degit/tests/index.ts
Normal file
58
packages/arc-degit/tests/index.ts
Normal 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"));
|
||||||
|
});
|
||||||
69
packages/arc-degit/tests/specs/core/appendProps.test.ts
Normal file
69
packages/arc-degit/tests/specs/core/appendProps.test.ts
Normal 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" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
112
packages/arc-degit/tests/specs/core/changeProps.test.ts
Normal file
112
packages/arc-degit/tests/specs/core/changeProps.test.ts
Normal 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
10
packages/arc-degit/tests/specs/core/index.ts
Normal file
10
packages/arc-degit/tests/specs/core/index.ts
Normal 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"));
|
||||||
|
});
|
||||||
|
});
|
||||||
81
packages/arc-degit/tests/specs/core/returnFound.test.ts
Normal file
81
packages/arc-degit/tests/specs/core/returnFound.test.ts
Normal 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" }] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { testSuite } from "manten";
|
||||||
|
|
||||||
|
export default testSuite(async ({ runTestSuite }) => {
|
||||||
|
runTestSuite(import("./adapter.test.js"));
|
||||||
|
});
|
||||||
26
packages/arc-degit/tests/specs/filter/basic.test.ts
Normal file
26
packages/arc-degit/tests/specs/filter/basic.test.ts
Normal 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 } }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
7
packages/arc-degit/tests/specs/filter/index.ts
Normal file
7
packages/arc-degit/tests/specs/filter/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { testSuite } from "manten";
|
||||||
|
|
||||||
|
export default testSuite(async ({ describe }) => {
|
||||||
|
describe("filter", async ({ runTestSuite }) => {
|
||||||
|
runTestSuite(import("./basic.test.js"));
|
||||||
|
});
|
||||||
|
});
|
||||||
131
packages/arc-degit/tests/specs/finding/basic.test.ts
Normal file
131
packages/arc-degit/tests/specs/finding/basic.test.ts
Normal 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 } } } } } } } } } } }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
7
packages/arc-degit/tests/specs/finding/index.ts
Normal file
7
packages/arc-degit/tests/specs/finding/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { testSuite } from "manten";
|
||||||
|
|
||||||
|
export default testSuite(async ({ describe }) => {
|
||||||
|
describe("finding", async ({ runTestSuite }) => {
|
||||||
|
runTestSuite(import("./basic.test.js"));
|
||||||
|
});
|
||||||
|
});
|
||||||
17
packages/arc-degit/tests/specs/from/from.test.ts
Normal file
17
packages/arc-degit/tests/specs/from/from.test.ts
Normal 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 }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
7
packages/arc-degit/tests/specs/from/index.ts
Normal file
7
packages/arc-degit/tests/specs/from/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { testSuite } from "manten";
|
||||||
|
|
||||||
|
export default testSuite(async ({ describe }) => {
|
||||||
|
describe("from", async ({ runTestSuite }) => {
|
||||||
|
runTestSuite(import("./from.test.js"));
|
||||||
|
});
|
||||||
|
});
|
||||||
81
packages/arc-degit/tests/specs/index.test.ts
Normal file
81
packages/arc-degit/tests/specs/index.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
29
packages/arc-degit/tests/specs/insert/basic.test.ts
Normal file
29
packages/arc-degit/tests/specs/insert/basic.test.ts
Normal 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: "👍" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
7
packages/arc-degit/tests/specs/insert/index.ts
Normal file
7
packages/arc-degit/tests/specs/insert/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { testSuite } from "manten";
|
||||||
|
|
||||||
|
export default testSuite(async ({ describe }) => {
|
||||||
|
describe("insert", async ({ runTestSuite }) => {
|
||||||
|
runTestSuite(import("./basic.test.js"));
|
||||||
|
});
|
||||||
|
});
|
||||||
80
packages/arc-degit/tests/specs/operators/boolean/and.test.ts
Normal file
80
packages/arc-degit/tests/specs/operators/boolean/and.test.ts
Normal 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 }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
59
packages/arc-degit/tests/specs/operators/boolean/fn.test.ts
Normal file
59
packages/arc-degit/tests/specs/operators/boolean/fn.test.ts
Normal 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 }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
142
packages/arc-degit/tests/specs/operators/boolean/gtlt.test.ts
Normal file
142
packages/arc-degit/tests/specs/operators/boolean/gtlt.test.ts
Normal 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 } } },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
39
packages/arc-degit/tests/specs/operators/boolean/has.test.ts
Normal file
39
packages/arc-degit/tests/specs/operators/boolean/has.test.ts
Normal 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 } }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 } } } }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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] } }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
18
packages/arc-degit/tests/specs/operators/boolean/index.ts
Normal file
18
packages/arc-degit/tests/specs/operators/boolean/index.ts
Normal 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"));
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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" }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
223
packages/arc-degit/tests/specs/operators/boolean/not.test.ts
Normal file
223
packages/arc-degit/tests/specs/operators/boolean/not.test.ts
Normal 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" } } } } }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 } } }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
53
packages/arc-degit/tests/specs/operators/boolean/or.test.ts
Normal file
53
packages/arc-degit/tests/specs/operators/boolean/or.test.ts
Normal 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 } } },
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
30
packages/arc-degit/tests/specs/operators/boolean/re.test.ts
Normal file
30
packages/arc-degit/tests/specs/operators/boolean/re.test.ts
Normal 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" } } ]);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
75
packages/arc-degit/tests/specs/operators/boolean/xor.test.ts
Normal file
75
packages/arc-degit/tests/specs/operators/boolean/xor.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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] }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
15
packages/arc-degit/tests/specs/operators/mutation/index.ts
Normal file
15
packages/arc-degit/tests/specs/operators/mutation/index.ts
Normal 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"));
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
270
packages/arc-degit/tests/specs/operators/mutation/math.test.ts
Normal file
270
packages/arc-degit/tests/specs/operators/mutation/math.test.ts
Normal 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" }},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 } }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@ -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 } }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 }] }]);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
48
packages/arc-degit/tests/specs/options/ifEmpty.test.ts
Normal file
48
packages/arc-degit/tests/specs/options/ifEmpty.test.ts
Normal 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" } },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
70
packages/arc-degit/tests/specs/options/ifNull.test.ts
Normal file
70
packages/arc-degit/tests/specs/options/ifNull.test.ts
Normal 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 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
35
packages/arc-degit/tests/specs/options/ifNullOrEmpty.test.ts
Normal file
35
packages/arc-degit/tests/specs/options/ifNullOrEmpty.test.ts
Normal 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" } }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
14
packages/arc-degit/tests/specs/options/index.ts
Normal file
14
packages/arc-degit/tests/specs/options/index.ts
Normal 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"));
|
||||||
|
});
|
||||||
|
});
|
||||||
21
packages/arc-degit/tests/specs/options/integerIds.test.ts
Normal file
21
packages/arc-degit/tests/specs/options/integerIds.test.ts
Normal 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 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
318
packages/arc-degit/tests/specs/options/join.test.ts
Normal file
318
packages/arc-degit/tests/specs/options/join.test.ts
Normal 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 }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
221
packages/arc-degit/tests/specs/options/project.test.ts
Normal file
221
packages/arc-degit/tests/specs/options/project.test.ts
Normal 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" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
37
packages/arc-degit/tests/specs/options/skip_take.test.ts
Normal file
37
packages/arc-degit/tests/specs/options/skip_take.test.ts
Normal 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 }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
57
packages/arc-degit/tests/specs/options/sort.test.ts
Normal file
57
packages/arc-degit/tests/specs/options/sort.test.ts
Normal 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 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
24
packages/arc-degit/tests/specs/remove/basic.test.ts
Normal file
24
packages/arc-degit/tests/specs/remove/basic.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
7
packages/arc-degit/tests/specs/remove/index.ts
Normal file
7
packages/arc-degit/tests/specs/remove/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { testSuite } from "manten";
|
||||||
|
|
||||||
|
export default testSuite(async ({ describe }) => {
|
||||||
|
describe("removing", async ({ runTestSuite }) => {
|
||||||
|
runTestSuite(import("./basic.test.js"));
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { testSuite } from "manten";
|
||||||
|
|
||||||
|
export default testSuite(async ({ describe }) => {
|
||||||
|
describe("sharded collection", async ({ runTestSuite }) => {
|
||||||
|
runTestSuite(import("./basic.test.js"));
|
||||||
|
});
|
||||||
|
});
|
||||||
180
packages/arc-degit/tests/specs/transactions/basic.test.ts
Normal file
180
packages/arc-degit/tests/specs/transactions/basic.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
5
packages/arc-degit/tests/specs/transactions/index.ts
Normal file
5
packages/arc-degit/tests/specs/transactions/index.ts
Normal 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
Loading…
Reference in New Issue
Block a user