forestdb
v1.0.12
Published
An uncomplicated real-time database with encrypted HTTP and WebSocket server-client communication, fast caching, state management, a cross-runtime file system manager, and more, working seamlessly on both frontend and backend.
Maintainers
Readme
forestDB
An uncomplicated real-time database with encrypted HTTP and WebSocket server-client communication, fast caching, state management, a cross-runtime file system manager, and more, working seamlessly on both frontend and backend.
Table of contents
- Installation
- What's forestDB
- API Documentation
- Author
- Other packages
- Contact Me
- License
Installation
# npm
$ npm install forestdb
# yarn
$ yarn add forestdb
# pnpm
$ pnpm add forestdb
# bun
$ bun add forestdbWhat's forestDB
ForestDB is a 100% pure TypeScript library that allows you to perform the following tasks:
Transactions
You can perform reliable and high-performance transactions through operations like 'set', 'update', 'updateAll', 'get', and 'delete', with a powerful Condition method to manage your
data in a flexible way. The data, referred to as feeds, are organized into branches, which are further organized into trees. These trees are contained within a forest, representing
your application.
In its current version, forestDB is an in-memory NoSQL database (for now, at least).
Encrypted HTTP and WebSocket Server-Client
You can connect one or multiple forestDB clients (React, Vue, Angular, Svelte, etc.) to one or more forestDB servers (Deno, Node, and Bun) via HTTP and WebSocket connections. Servers can also connect to each other, even across different runtimes, with each capable of acting as a client, server, or both simultaneously.
All HTTP and WebSocket connections are end-to-end encrypted using a two-layer encryption method. The first layer employs AES-GCM, while the second applies an algorithm that shuffles the encrypted data, adding an extra layer of security and making it harder to break.
The main purpose of this functionality is to securely trigger functions directly from both sides without requiring the user to set up POST or GET requests. With WebSockets, you can automatically attach a callback to receive a response.
Store
The Store is a fast caching method that allows you to store simple key-value pairs and perform powerful mutations on them. It’s especially useful for keeping small pieces of data readily accessible for quick retrieval.
Session
The Session works just like the Store, but its main purpose is to store session data, such as user IDs, to identify or filter users during WebSocket messaging and broadcasting. Servers and clients also use sessions to authenticate themselves and perform operations. Each client commits its session to the server in real-time.
Watchers
ForestDB allows you to watch events such as 'creation', 'update', and 'deletion' on feeds, branches, the Store, and the Session. Any created, updated, or deleted fields are automatically returned in the watch event and can be used to update components or perform any other operations.
On the frontend, such as in React, watchers do not trigger re-renders; they simply broadcast changes.
Triggers
Triggers allow you to efficiently centralize all functions in your app and execute them directly via the trigger ID. You can create multiple triggers and even group them by family for triggers containing similar functions. This way, you can run the same functions across different triggers simply by specifying their family and the function name, instead of calling each trigger separately.
For a use case example, imagine you have two components, A and B, in React. You can create a trigger for all functions inside component A and another for component B. Then, you can remotely execute functions within A or B from anywhere in your application simply by calling the trigger through its ID or family and specifying the function name to run. You can even execute functions in parallel when needed.
It’s the same logic used between Clients and Servers over HTTP and WebSocket. It makes the Server and the Client feel like two components of the same app.
Triggers support both synchronous and asynchronous functions.
Cross-runtime file system APIs
ForestDB provides the same API for file system operations across runtimes like Deno, Node, Bun, and React Native, without any performance sacrifice. It leverages the native file system APIs for each runtime (the same logic is used for HTTP and WebSocket to create servers and clients).
Methods
Some utility functions are also provided for tasks like hashing and object manipulation, with many more to be available in the futur releases.
These are the major functionalities available for now, and each includes many sub-functionalities that we will explore in the Documentation section. More are still in development and will be available soon.
API Documentation
Initialization
Before using forestDB, you need to initialize it. Here's how:
import forestDB from 'forestdb';
// Initialization of forestDB
const forest = forestDB.init({
mainKey: 'id',
dateFormat: ['YYYY_MM_DD', 'MM_DD_YYYY']
});mainKey(string): The name of the key used as a unique identifier for each feed. This key must be present in every feed used within a transaction, and its value should always be alphanumeric and unique within the entire forest. No two feeds should share the same identifier, even if they belong to different trees or branches.
In this example, we use the key "id", and its value can look like this: "01JNRRRN9XRHZAA93APGPKHMQ7".
dateFormat(string[]): Specify the date format you want to support in your application. Three formats are available:"YYYY_MM_DD"(the universal and most commonly used format),"MM_DD_YYYY"(American format), and"DD_MM_YYYY"(European format).
To avoid ambiguity, never use "MM_DD_YYYY" and "DD_MM_YYYY" together, as it can create confusion for dates like "01/01/2025". Always use "YYYY_MM_DD" along with one of the other two formats.
Transactions
Transactions are useful for storing and managing data like posts, messages, accounts, and similar items.
In this example, we'll perform transactions using the following list of employees.
const employees_DATA = [
{
"id": "EMP001",
"personalInfo": {
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
"age": 20
},
"job": {
"title": "Junior Software Engineer",
"technologies": ["TypeScript", "React", "Node.js"],
"hireDate": "2022-04-15",
},
"salary": 75_000,
"status": "active",
},
{
"id": "EMP002",
"personalInfo": {
"firstName": "Emma",
"lastName": "Smith",
"email": "[email protected]",
"age": 23
},
"job": {
"title": "Mid-Level Software Engineer",
"technologies": ["TypeScript", "Angular", "AWS"],
"hireDate": "2019-06-30",
},
"salary": 110_000,
"status": "inactive",
},
{
"id": "EMP003",
"personalInfo": {
"firstName": "James",
"lastName": "Taylor",
"email": "[email protected]",
"age": 25
},
"job": {
"title": "Senior Software Engineer",
"technologies": ["TypeScript", "Node.js", "Kubernetes"],
"hireDate": "2015-08-12",
},
"salary": 145_000,
"status": "active",
}
];Setoperations
// Set data
const set_data = await forest.onTree('employees').set(employees_DATA).onBranch('dev_accounts').end();⚠️ Note: This documentation uses top-level
await. Ensure your runtime environment is properly configured to support it.
await forest: The forest instance we have initialized. All transactions are asynchronous.onTree(string): Select the tree on which you want to perform the transaction. If it doesn't exist, it will be automatically created.set(json | json[]): Accepts a JSON object or an array of JSON objects.onBranch(string): Select the branch on which you want to store the feeds. If it doesn't exist, it will be automatically created.end: Indicates the end of the transaction chain.
Once finished, a transaction will return a JSON object containing fields like:
status(string): Will besuccessif everything goes well orerrorif something goes wrong.log(string): Contains details about errors and other informative messages.data(any): Any data returned by the transaction. If nothing is returned, it may contain an empty array[]if agettransaction didn’t retrieve anything, orundefinedin other cases.
Other examples:
const set_man = await forest.onTree('species').set({ id: 'human_john', name: 'John', age: 32 }).onBranch('humans').end();
const set_woman = await forest.onTree('species').set({ id: 'human_melanie', name: 'Melanie', age: 25 }).onBranch('humans').end();
const set_dog = await forest.onTree('species').set({ id: 'dog_rex', name: 'Rex', color: 'Black & White', age: 1 }).onBranch('dogs').end();
const set_fish = await forest.onTree('species').set({ id: 'fish_nemo', name: 'Nemo', color: 'Orange & White' }).onBranch('fishs').end();As you can see, the APIs are clean, simple, and intuitive.
⚠️ Note: It's ONLY during "set" operations that "trees" and "branches" that don't exist are automatically created. For any other operation, if the "tree" or "branch" specified doesn't already exist, the operation will simply fail. Please, keep that in mind!
Also, make it a habit to check the transaction's status and log to understand what's happening in your application.
getoperations
const get_data = await forest.onTree('employees').get('*').fromBranch('dev_accounts').end();get(x):xcan be'*'to retrieve all fields from a feed, or the names of the specific fields you want to retrieve.
The query above will return everything from the branch "dev_accounts".
Remember, a transaction will always return a JSON object containing its status (whether it succeeded or not), the log (error messages when it fails),
and the data (any retrieved data from the database). To access the data or feeds returned by our request, we will do it like this:
// Get the data returned by the request:
const feeds = get_data.data;It's not finished, cause if you do console.log(feeds), you will see that the real data is contained inside another object with the key "0".
So, the full code to access the data directly is:
// Extract the feeds directly from the response.
const feeds = get_data.data['0'];Now, if you run console.log(feeds) again, you’ll see an array containing your feeds.
You’ll understand why you need to add ['0'] when we reach the join section.
Great, but there’s more we can do. What if we only want to retrieve the data for James ("EMP003") ? We can easily chain a where function to the transaction to filter the feeds, like this:
// Get "James" data using its ID "EMP003"
const get_james_data = await forest.onTree('employees').get('*').fromBranch('dev_accounts').where({ id: 'EMP003' }).end();where(json): Accepts only one JSON object.
We can also order the feeds and set a limit on the number of feeds we want to retrieve.
For example, let's retrieve all employees and order them by salary at the same time.
// This will return all employees ordered by salary from the "smallest" to the "largest"
const get_ordered_data_ASC_1 = await forest.onTree('employees').get('*').fromBranch('dev_accounts').orderBy('salary').end();
// Same effect
const get_ordered_data_ASC_2 = await forest.onTree('employees').get('*').fromBranch('dev_accounts').orderBy('salary', 'ASC').end();
// This will return all employees ordered by salary from the "largest" to the "smallest"
const get_ordered_data_DESC = await forest.onTree('employees').get('*').fromBranch('dev_accounts').orderBy('salary', 'DESC').end();orderBy(x, y?):xis the name of the field by which you want to order the feeds, andy(ASC|DESC) determines the order. If you don’t specify a value fory(it's optional), its default value will beASC.
You can order by fields of type number, boolean and string, and also use dot notation to reach nested fields.
// This will return all employees ordered by "age" from the "oldest" to the "youngest"
const get_ordered_data_DESC = await forest.onTree('employees').get('*').fromBranch('dev_accounts').orderBy('personalInfo.age', 'DESC').end();To retrieve only the two highest-paid employees, use limit:
// This will return the two highest-paid employees.
const get_two_feeds = await forest.onTree('employees').get('*').fromBranch('dev_accounts').orderBy('salary', 'DESC').limit(2).end();limit(number): Limit the number of feeds to retrieve.
updateoperations
const update_data = await forest.onTree('employees').update({ id: 'EMP001', status: 'inactive' }).end();update(json | json[]): Accepts a JSON object or an array of JSON objects.
In the example above, only the status field will be updated. The id field will never be updated because it has been defined as the mainKey. Instead, its value will be used to identify
the feed on which to apply the update.
Note that no branch is specified because when a feed is created for the first time, an index is automatically created between its ID (mainKey) and the branch it belongs to.
So, when you specify the feed ID, forest already knows its branch.
This is especially useful when you want to update many feeds belonging to different branches at the same time. You can collect them inside an array and pass it to the update function.
Be careful ! If you pass a JSON object without specifying the mainKey (in our case, id), the update will fail. So, never do this:
// No "id" is specified, so the update will fail
const bad_update = await forest.onTree('employees').update({ status: 'inactive' }).end();Here, we will update two feeds belonging to different branches:
// We have incremented Melanie and Rex's ages by 1
const multi_updates = await forest.onTree('species').update([{ id: 'human_melanie', age: 26 }, { id: 'dog_rex', age: 2 }]).end();Of course, you can update many fields at the same time, but never omit the mainKey.
updateAlloperations
Update everything
const update_all_data = await forest.onTree('employees').updateAll({ status: 'inactive' }).onBranch('dev_accounts').end();updateAll(json): Accepts only one JSON object.
Unlike update, updateAll doesn't require you to specify the mainKey, but you must always specify the branch. It will then update the given fields for all feeds on that branch.
In the example above, every employee's status will be updated to inative.
Be careful ! If you specify the mainKey inside updateAll, the transaction will fail. So, never do this:
// This update will fail because you're trying to update the "mainKey" of all feeds
const very_bad_update_1 = await forest.onTree('employees').updateAll({ id: 'EMP001', status: 'inactive' }).onBranch('dev_accounts').end();Even if the provided id doesn't exist, the transaction will still fail, as shown in this example:
// This update will fail because you're trying to update the "mainKey" of all feeds
const very_bad_update_2 = await forest.onTree('employees').updateAll({ id: 'bla_bla_bla', status: 'inactive' }).onBranch('dev_accounts').end();Simply note that you can update all fields except the mainKey.
Conditional updates
// Set "salary" to 200_000 where "status: active". If you didn't modify Emma ("EMP002")'s status, she won't receive that update because her status is "inactive"
const update_many_data = await forest.onTree('employees').updateAll({ salary: 200_000 }).onBranch('dev_accounts').where({ status: 'active' }).end();where(json): Accepts only one JSON object.
When you need more control, simply chain a where function to your transaction to filter the feeds you want to update, instead of updating everything.
For example, in the case above, only the salary of employees with status: active will be updated.
It is possible to create a very complex and flexible where condition and update mutation. We’ll dive deeper into this in the Conditions and Mutations sections.
Just follow me !
You can also use orderBy and limit with updateAll to optimize your transaction and make it run faster.
deleteoperations
Delete fields
Case 1: To delete all fields, do it like this:
// Delete all "fields" from all "feeds" on the "dev_accounts" branch. This has the same effect as "delete_all_fields_2" with "*" specified.
const delete_all_fields_1 = await forest.onTree('employees').delete('field', '*').fromBranch('dev_accounts').end();
// Delete all "fields" from all "feeds" on the "dev_accounts" branch. This has the same effect as "delete_all_fields_1" without "*" specified.
const delete_all_fields_2 = await forest.onTree('employees').delete('field').fromBranch('dev_accounts').end();delete(x, y?): Forxyou should setfieldand foryyou will specify thenamesof the fields you want to delete or'*'if you want to delete all fields. If you didn't set any value toy(it's optional), its defalut value will be'*'.Case 2: To delete only one field, do it like this:
// The "status" field will be deleted from all "feeds" on the "dev_accounts" branch.
const delete_one_field = await forest.onTree('employees').delete('field', 'status').fromBranch('dev_accounts').end();Case 3: To delete many fields, do it like this:
// The "salary" and "status" fields will be deleted from all "feeds" on the "dev_accounts" branch.
const delete_many_fields = await forest.onTree('employees').delete('field', ['salary', 'status']).fromBranch('dev_accounts').end();You can add a where function for each case to filter the feeds on which you want to apply the transaction.
// The "salary" and "status" fields will be deleted only from all "feeds" with status: active on the "dev_accounts" branch.
const delete_fields_with_filter = await forest.onTree('employees').delete('field', ['salary', 'status']).fromBranch('dev_accounts').where({ status: 'active' }).end();You can delete any field you want except the mainKey. If you use '*' the mainKey will be deleted, but only because the feed itself will be deleted.
⚠️ Note: If you delete all fields from a feed, the feed itself will be deleted automatically. If you try to delete a field that doesn't exist, it will be ignored.
Delete feeds
Case 1: To delete all feeds from a branch, do it like this:
// Delete all "feeds" from the "dev_accounts" branch. This has the same effect as "delete_all_feeds_2" with "*" specified.
const delete_all_feeds_1 = await forest.onTree('employees').delete('feed', '*').fromBranch('dev_accounts').end();
// Delete all "feeds" from the "dev_accounts" branch. This has the same effect as "delete_all_feeds_1" without "*" specified.
const delete_all_feeds_2 = await forest.onTree('employees').delete('feed').fromBranch('dev_accounts').end();delete(x, y?): Forxyou should setfeedand foryyou will specify theIDsof the feeds you want to delete or'*'if you want to delete all feeds. If you didn't set any value fory(it's optional), its default value will be'*'.Case 2: To delete only one feed from a branch, do it like this:
// The feed with ID "EMP001" will be deleted from the "dev_accounts" branch.
const delete_one_feed = await forest.onTree('employees').delete('feed', 'EMP001').fromBranch('dev_accounts').end();Case 3: To delete many feeds from a branch, do it like this:
// The feeds "EMP002" and "EMP003" will be deleted from branch "dev_accounts".
const delete_many_feeds = await forest.onTree('employees').delete('feed', ['EMP002', 'EMP003']).fromBranch('dev_accounts').end();You can add a where function to filter the feeds you want to delete when no ID is specified (for example, in case 1).
// The feeds with "status: active" will be deleted from the "dev_accounts" branch.
const delete_feeds_with_filter = await forest.onTree('employees').delete('feed').fromBranch('dev_accounts').where({ status: 'active' }).end();⚠️ Note: If you delete all feeds from a branch, the branch will not be deleted automatically. If you try to delete a feed that doesn't exist, it will be ignored.
Delete branches
Case 1: To delete all branches from a tree, do it like this:
// Delete all "branches" from the "employees" tree. This has the same effect as "delete_all_branches_2" with "*" specified.
const delete_all_branches_1 = await forest.onTree('employees').delete('branch', '*').end();
// "Delete all branches from the 'employees' tree. This has the same effect as 'delete_all_branches_1', but without specifying '*'.
const delete_all_branches_2 = await forest.onTree('employees').delete('branch').end();delete(x, y?): Setxto branch, and fory, specify the names of the branches to delete, or use'*'to delete all branches. If you didn't set any value toy(it's optional), its defalut value will be'*'.Case 2: To delete only one branch from a tree, do it like this:
// The "dev_accounts" branch will be deleted from the "species" tree.
const delete_one_branch = await forest.onTree('employees').delete('branch', 'dev_accounts').end();Case 3: To delete many branches from a branch, do it like this:
// The branches "dogs" and "fishs" will be deleted from the "species" tree.
const delete_many_branches = await forest.onTree('species').delete('branch', ['dogs', 'fishs']).end();⚠️ Note: If you delete all branches from a tree, the tree itself will not be deleted. Trying to delete a branch that doesn't exist will be ignored.
When you delete a field, feed, or branch, it is not removed immediately for safety reasons. Instead, it is placed in phantom mode if being processed within a transaction at the time of deletion.
A phantom is something that exists but cannot be seen or interacted with. The deleted fields, feeds, or branches will remain in existence until all transactions involving them are fully completed, regardless of whether they succeed or fail.
Anything in phantom mode cannot be fetched or processed. It is locked and will be removed once all related transactions are finished.
Any operation on a phantom field, feed, or branch will fail immediately (with a log explaining the reason) or will be silently ignored (with no log).
joinfor MTC
A Join allows you to chain two or more transactions together and run them at once.
const join_transactions = await forest.onTree('employees').set(employees_DATA).onBranch('dev_accounts').join('t1')
.updateAll({ salary: 200_000 }).onBranch('dev_accounts').where({ status: 'active' }).join('t2')
.get(['id', 'salary', 'status']).fromBranch('dev_accounts').where({ status: 'active' }).end();This is called a Multi Transactions Chain (MTC). There is no limit to how many transactions you can chain together.
However, an MTC can only be used on one tree at a time, so every transaction within an MTC must concern the same tree.
join(string): It takes a string as an argument, which serves as theIDfor the next transaction.
For example, t1 is the ID of the "updateAll transaction" and t2 is the ID of the "get transaction". Two transactions within an MTC cannot share the same ID.
If you try to do this, all transactions will fail immediately.
What about the ID of the first transaction, the "set transaction" ? The first transaction in an MTC has "0" as its default ID, and it cannot be changed.
Now you can understand why we use the following syntax to access our feeds:
// Extract the feeds directly from the response.
const feeds = get_data.data['0']; /* Do you remember ? */To retrieve our feeds this time, we'll do like this:
// Extract the feeds directly from the response.
const feeds = join_transactions.data['t2']; /* "t2" is the ID of the current transaction in this context */A transaction ID (or Join ID) serves two main purposes. First, it helps identify the faulty transaction when the MTC fails. Second, as you know, it allows you to
extract the data returned by a get transaction.
If one transaction fails, the entire chain fails. For the MTC to succeed, all transactions in the chain must succeed. Only when all transactions succeed are the changes committed and saved to the database. It's ATOMIC.
Facts about transactions
No changes are applied to the database until a single transaction or an MTC fully succeeds.
Once a transaction starts, it cannot be canceled externally.
Any JSON data sent to a transaction is deeply cloned before being processed to break all references.
Any data output by a transaction is a deeply cloned version of the original data to break all references.
Transactions only support fields of type
number,string,boolean,null,jsonandarray.Every transaction is asynchronous.
⚠️ Note: In JavaScript, "null", "json", and "array" are treated as objects. However, I prefer to detail everything for better understanding. So, if you're new to JS, don't be confused. As of the time of writing this document, there are no explicit types like "null", "json", or "array" in JS. In the Method section of this documentation, you'll find a function provided by
forestDBto detect the exact type of a variable.
Conditions
Now, it's going to get interesting.
We've covered many topics in the transactions section, but until now, we haven't explored the famous where function for filtering feeds.
In this section, you'll discover how flexible and powerful forestDB is when it comes to filtering JSON data.
Simple filtering
const simple_where_condition = await forest.onTree(employees).get('*').fromBranch('dev_accounts').where({ id: 'EMP001', salary: 75_000 }).end();Above is the basic and limited type of filter or condition we've been using in the where function from the beginning.
However, we can expect more, as a feed can contain many deeply nested fields. This is why Deep Filtering was created.
Deep filtering
For this section, we'll use the following JSON data:
const users_Data = [
/* user "01" */
{
"id": "01_JNRS1WCD1MK6FRZG87ZA7Y6Q",
"name": "John Doe",
"email": "[email protected]",
"age": 25,
"address": {
"street": "123 Main St",
"city": "New York",
"zip": "10001",
"country": "USA"
},
"preferences": {
"newsletter": true,
"theme": "dark",
"notifications": {
"email": true,
"sms": false,
"push": true
}
},
"company": {
"name": "forestDB Cloud",
"position": "Software Engineer",
"salary": 200_000,
"department": {
"name": "Development",
"floor": 3
}
},
"paymentInfo": {
"cardType": "Visa",
"last4": 1234,
"billing": {
"address": "456 Another St",
"city": "Los Angeles"
}
},
"settings": {
"language": "en",
"timezone": "UTC-5"
},
"tags": ["developer", "javascript", "aws", "docker", "nginx", "remote"],
"orderHistory": [
{ "orderId": "ORD123", "amount": 99.99, "date": "2024-03-07" },
{ "orderId": "ORD124", "amount": 49.99, "date": "2024-03-06" }
],
"devices": [
{ "type": "laptop", "os": "Windows 11" },
{ "type": "phone", "os": "iOS" }
],
"createdAt": "2024-03-07T12:00:00Z",
"updatedAt": "2024-03-08T10:30:00Z",
"status": "active",
"roles": ["admin", "user"],
"metadata": {
"signupSource": "web",
"referralCode": "XYZ123"
}
},
/* user "02" */
{
"id":"02_JNRS1WBEBDB66DXXRJAYGHMA","name":"Temple Hechlin","email":"[email protected]","age":26,"address":{"street":"642 Packers Park","city":"Bangbayang","country":"Indonesia"},
"preferences":{"newsletter":true,"theme":"Orange","notifications":{"email":false,"sms":true,"push":false}},"company":{"name":"forestDB Cloud","position":"Software Test Engineer II",
"salary": 120_000,"department":{"name":"Research and Development","floor":82}},"paymentInfo":{"cardType":"jcb","last4":2401,"billing":{"address":"193 Butterfield Trail","city":"Pelaya"}},
"settings":{"language":"Kurdish","timezone":"Asia/Jakarta"},"tags":["javascript","remote"],"orderHistory":{"orderId":"ORD124","amount":150.29,"date":"3/6/2024"},
"devices":{"type":"laptop","os":"Windows 11"},"createdAt":"2024-02-07T00:00:00Z","updatedAt":"2024-02-17T20:00:00Z","status":"inactive","roles":["admin","user"],
"metadata":{"signupSource":"web"}
},
/* user "03" */
{
"id":"03_JNRS1WBR30TZGEG943RVPF4A","name":"Montgomery Feechum","email":"[email protected]","age":24,"address":{"street":"3787 Lindbergh Lane","city":"Shijing","country":"China"},
"preferences":{"newsletter":false,"theme":"Khaki","notifications":{"email":true,"sms":false,"push":false}},"company":{"name":"forestDB Cloud","position":"GIS Technical Architect",
"salary": 145_000,"department":{"name":"Marketing","floor":62}},"paymentInfo":{"cardType":"diners-club-carte-blanche","last4":1662,"billing":{"address":"85 Blaine Place","city":"Tiling"}},
"settings":{"language":"Latvian","timezone":"Asia/Chongqing"},"tags":["remote","developer","javascript"],"orderHistory":{"orderId":"ORD123","amount":598.55,"date":"3/6/2024"},
"devices":{"type":"laptop","os":"Windows 11"},"createdAt":"2024-03-06T00:00:00Z","updatedAt":"2024-03-12T09:52:00Z","status":"active","roles":["admin","user"],
"metadata":{"signupSource":"web","referralCode": "XYZ456"}
},
/* user "04" */
{
"id":"04_JNRS1WEYZJRPHNZ0CNBN4QEH","name":"Cindie M. Castells","email":"[email protected]","age":99,"address":{"street":"26069 Graceland Road","city":"Kassándreia","country":"Greece"},
"preferences":{"newsletter":true,"theme":"Teal","notifications":{"email":true,"sms":true,"push":false}},"company":{"name":"forestDB Cloud","position":"Technical Writer",
"salary": 78_000,"department":{"name":"Training","floor":18}},"paymentInfo":{"cardType":"instapayment","last4":3197,"billing":{"address":"576 Thompson Plaza","city":"Heling"}},
"settings":{"language":"Tamil","timezone":"Europe/Athens"},"tags":["remote","javascript","developer"],"orderHistory":{"orderId":"ORD124","amount":417.12,"date":"3/6/2024"},
"devices":{"type":"laptop","os":"Windows 11"},"createdAt":"2024-02-08T00:00:00Z","updatedAt":"2024-02-24T03:20:04Z","status":"inactive","roles":["user","admin"],
"metadata":{"signupSource":"web"}
}
];
// Set users
const set_users = await forest.onTree('users').set(users_Data).onBranch('dev').end();Deep filtering on
Number
===operator
// Extract users that have a salary `equal` to "200_000"
const filter = await forest.onTree('users').get(['id', 'name', 'email', 'company', 'createdAt', 'updatedAt', 'tags', 'paymentInfo', 'metadata']).fromBranch('dev')
.where({
company: forest.condition().number({ operator: '===', value: 200_000, path: 'company.salary' })
})
.end();Let's analyse this line company: forest.condition().number({ operator: '===', value: 200_000, path: 'company.salary' });
company: This is thestarting pointof our condition, the primary field we want to filter..condition(): The function that tellsforestthat we want to use a condition..number(x, y?): The function that tellsforestthat we want to filter a field of typenumber. It can receive two arguments.
The first argument x is a JSON object or an array of JSON objects, containing the following parameters
operator: Indicates which kind of operation should be done.value: The value that will be used as the reference for the filter.path?: Specifies theendpoint, the only and final field on which you want to apply the filter. It's optional, because when thestarting pointis also theendpoint, you don't need to set a path. For example, in a schema where salary is a first-level key. Use the path only to point to a nested field.
The second argument y is a string that can be either AND or OR (default). It's usefull when you're dealing with an array of different conditions. You can choose whether at least one
of them should match (OR), or if absolutely all of them should match (AND).
- Example for
OR
// Extract users that have a salary `equal` to "200_000" `or` that work at "floor 3"
/* rest of the code... */
company: forest.condition().number([
{ operator: '===', value: 200_000, path: 'company.salary' },
{ operator: '===', value: 3, path: 'company.department.floor' }
])
/* rest of the code... */
// Same thing here
/* rest of the code... */
company: forest.condition().number([
{ operator: '===', value: 200_000, path: 'company.salary' },
{ operator: '===', value: 3, path: 'company.department.floor' },
'OR' // We explicitly specify 'OR'
])
/* rest of the code... */- Example for
AND
// Extract users that have a salary equal to "200_000" `and` that work at "floor 3"
/* rest of the code... */
company: forest.condition().number([
{ operator: '===', value: 200_000, path: 'company.salary' },
{ operator: '===', value: 3, path: 'company.department.floor' }
'AND' // All conditions should match
])
/* rest of the code... */When using a number condition, note that the value should always be a number.
You'll see later how to apply multiple conditions on fields of different types. Just follow me !
If you specify an invalid path, the transaction won't fail. Instead, it will simply act as if no feed matches the filter. For an updateAll operation, nothing will be updated;
for a get operation, it will return an empty array; and for a delete operation, nothing will be deleted.
!==operator
// Extract users that have a salary `different` from "200_000""
/* rest of the code... */
company: forest.condition().number({ operator: '!==', value: 200_000, path: 'company.salary' })
/* rest of the code... */>operator
// Extract users that have a salary `superior` to "200_000""
/* rest of the code... */
company: forest.condition().number({ operator: '>', value: 200_000, path: 'company.salary' })
/* rest of the code... */>=operator
// Extract users that have a salary `superior or equal` to "200_000""
/* rest of the code... */
company: forest.condition().number({ operator: '>=', value: 200_000, path: 'company.salary' })
/* rest of the code... */<operator
// Extract users that have a salary `inferior` to "200_000""
/* rest of the code... */
company: forest.condition().number({ operator: '<', value: 200_000, path: 'company.salary' })
/* rest of the code... */<=operator
// Extract users that have a salary `inferior or equal` to "200_000"
/* rest of the code... */
company: forest.condition().number({ operator: '<', value: 200_000, path: 'company.salary' })
/* rest of the code... */%operator
// This example isn't really suitable for "modulo," but just know that you can use it.
// Extract users that have a salary `divisible` by "200"
/* rest of the code... */
company: forest.condition().number({ operator: '%', value: 200, path: 'company.salary' })
/* rest of the code... */<>operator
// Extract users that have a salary in the range of "100_000" to "200_000"
/* rest of the code... */
company: forest.condition().number({ operator: '<>', value: [100_000, 200_000], path: 'company.salary' })
/* rest of the code... */
// Extract users that have a salary in the range of "100_000" to "200_000" `or` in the range of "40_000" to "80_000"
/* rest of the code... */
company: forest.condition().number({ operator: '<>', value: [[100_000, 200_000], [40_000, 80_000]], path: 'company.salary' })
/* rest of the code... */The operator <> only accepts one array of two numbers or one array containing multiple sub-arrays of two numbers.
In the second case, the filter succeeds if at least one of the conditions matches. If you add more than two numbers, the transaction will fail.
!<>operator
// Extract users that "don't" have a salary in the range of "100_000" to "200_000"
/* rest of the code... */
company: forest.condition().number({ operator: '!<>', value: [100_000, 200_000], path: 'company.salary' })
/* rest of the code... */
// Extract users that "don't" have a salary in the range of "100_000" to "200_000" `or` in the range of "40_000" to "80_000"
/* rest of the code... */
company: forest.condition().number({ operator: '!<>', value: [[100_000, 200_000], [40_000, 80_000]], path: 'company.salary' })
/* rest of the code... */The operator !<> is simply the opposite of <>.
<*>operator
// Extract users that have a salary in the range of "100_000" to "200_000" `and` in the range of "80_000" to "120_000"
/* rest of the code... */
company: forest.condition().number({ operator: '<*>', value: [[100_000, 200_000], [80_000, 120_000]], path: 'company.salary' })
/* rest of the code... */The operator <*> is useful when you have multiple ranges and want the filtered value to be included in all of them.
!<*>operator
// Extract users that "don't" have a salary in the range of "100_000" to "200_000" `and` in the range of "80_000" to "120_000"
/* rest of the code... */
company: forest.condition().number({ operator: '!<*>', value: [[100_000, 200_000], [80_000, 120_000]], path: 'company.salary' })
/* rest of the code... */Here, the filtered value should not be included in any of the ranges.
><operator
// Extract users that have a salary `INCLUDED BETWEEN` "120_000" and "200_000" (From "120_001" to "199_999")
/* rest of the code... */
company: forest.condition().number({ operator: '><', value: [120_000, 200_000], path: 'company.salary' })
/* rest of the code... */
// Extract users that have a salary `INCLUDED BETWEEN` "100_000" and "200_000" `or` `INCLUDED BETWEEN` "40_000" and "80_000" (From "40_001" to "79_999")
/* rest of the code... */
company: forest.condition().number({ operator: '><', value: [[100_000, 200_000], [40_000, 80_000]], path: 'company.salary' })
/* rest of the code... */When using > and <, the edge values are excluded from the range.
!><operator
// Extract users that "don't" have a salary `INCLUDED BETWEEN` "120_000" and "200_000"
/* rest of the code... */
company: forest.condition().number({ operator: '!><', value: [120_000, 200_000], path: 'company.salary' })
/* rest of the code... */
// Extract users that "don't" have a salary `INCLUDED BETWEEN` "100_000" and "200_000" `or` `INCLUDED BETWEEN` "40_000" and "80_000"
/* rest of the code... */
company: forest.condition().number({ operator: '!><', value: [[100_000, 200_000], [40_000, 80_000]], path: 'company.salary' })
/* rest of the code... */>*<operator
// Extract users that have a salary `INCLUDED BETWEEN` "100_000" to "200_000" `and` `INCLUDED BETWEEN` "80_000" to "120_000"
/* rest of the code... */
company: forest.condition().number({ operator: '>*<', value: [[100_000, 200_000], [80_000, 120_000]], path: 'company.salary' })
/* rest of the code... */!>*<operator
// Extract users that "don't" have a salary in the range of "100_000" to "200_000" `and` in the range of "80_000" to "120_000"
/* rest of the code... */
company: forest.condition().number({ operator: '!>*<', value: [[100_000, 200_000], [80_000, 120_000]], path: 'company.salary' })
/* rest of the code... */<?>operator
// Extract users that have a salary that match at least one these values
/* rest of the code... */
company: forest.condition().number({ operator: '<?>', value: [75_000, 100_000, 200_000, 80_000, 120_000], path: 'company.salary' })
/* rest of the code... */!<?>operator
// Extract users that have a salary that "doesn't" match any these values
/* rest of the code... */
company: forest.condition().number({ operator: '!<?>', value: [75_000, 100_000, 200_000, 80_000, 120_000], path: 'company.salary' })
/* rest of the code... */customoperator
If the standard operators don't suit your needs, no problem ! With forest, you can even write your own custom condition. This is one of the features that makes forest so powerful and flexible.
// Extract users that have the "half" of their salary "superior to 50_000"
/* rest of the code... */
company: forest.condition().number({
operator: 'custom',
path: 'company.salary',
customCondition: (x: { value: number }) => {
const user_salary = x.value;
const half_salary = user_salary / 2;
return half_salary > 50_000 ? true : false;
}
})
/* rest of the code... */customCondition(Function): It receives asynchronous functionused to perform a custom operation on the targeted field's value. The function should always return a boolean (trueif the feed matches the condition andfalseif the feed doesn't match).
The custom function receives an object containing a copy of the value of the targeted field. Even if the value is modified inside the function, the original value will remain unaffected.
You can do anything inside the function, and even use external variables. It's just a function !
// External variables
const divide_by = 2;
const minimum = 50_000;
// You can write the function externally to reuse it multiple times or simply to keep your code cleaner.
const myCustomConditionFunc = (x: { value: number }) => {
const user_salary = x.value;
const half_salary = user_salary / divide_by;
return half_salary > minimum ? true : false;
};
// Extract users that have the "half" of their salary "superior to 50_000"
/* rest of the code... */
company: forest.condition().number({
operator: 'custom',
path: 'company.salary',
customCondition: myCustomConditionFunc
})
/* rest of the code... */If your custom function doesn't return a boolean, the transaction will fail.
Be careful ! If you apply a custom condition to a non-existent field, "value" will be undefined. It's recommended to check the type of "value" before processing it, especially when it can be undefined.
⚠️ Note: You don't need to specify a "value" when using "customCondition" (it will be ignored).
/* rest of the code... */
const myCustomConditionFunc = (x: { value: number }) => {
// Always check the type of "value" first if it can be "undefined" to avoid errors that might stop the transaction.
if (typeof x.value !== 'number') return false; // The transaction will detect that the feed doesn't match the condition and will proceed with the other feeds.
// rest of your code
const user_salary = x.value;
const half_salary = user_salary / divide_by;
return half_salary > minimum ? true : false;
};
/* rest of the code... */permutationoption
// External variable
const divide_by = 2;
// Extract users that have the "half" of their salary "superior to 50_000" - But this time by using a "permutation"
/* rest of the code... */
company: forest.condition().number({
operator: '>',
value: 50_000,
path: 'company.salary',
permutation: (x: { value: number }) => {
const user_salary = x.value;
const half_salary = user_salary / divide_by;
return half_salary;
}
})
/* rest of the code... */permutation(Function): Receives asynchronous functionthat modifies a copy of the target field's value. The function should return a value of the same type as the original field value.
A permutation is a powerful method that allows you to mutate a copy of the field's value for the condition, without affecting the original value.
Any changes to the copy won't affect the original value.
You can perform any computation as long as the "permutation" returns a value with a valid type (the same type as the original value). Otherwise, the transaction will fail immediately.
For a "date" field, the permutation will be accepted as long as it returns the date as a string or number.
Deep filtering on
String===operator
// Extract users that use "dark theme"
/* rest of the code... */
preferences: forest.condition().string({ operator: '===', value: 'dark', path: 'preferences.theme' })
/* OR */
preferences: forest.condition().string({ operator: '===', value: 'dark', path: 'theme' }) // The path here is different from the first code, but it will have the same effect.
/* rest of the code... */In the second condition, we can use only "theme" (second key) because it directly follows "preferences" (first key) in the path.
So, you can start a path with the second key, though it may sometimes be less clear in terms of readability.
Now, look at the following condition:
// Extract users that lives in "New york"
/* rest of the code... */
address: forest.condition().string({ operator: '===', value: 'New york', path: 'address.city' })
/* rest of the code... */On the "dev" branch, the user "01" lives in "New York," but this condition will never find him and will return an empty list.
The reason is simple ! It's because of the case. In our condition, we wrote "New york" with a lowercase y for "york," while it’s uppercase in the feed.
Forest is case-sensitive by default.
To fix this, you can either uppercase the y or use the case_sensitive option in the condition to avoid any issues.
// Extract users that lives in "New york"
/* rest of the code... */
address: forest.condition().string({ operator: '===', value: 'New york', path: 'address.city', case_sensitive: false }) // We have disabled case-sensitivity
/* rest of the code... */Now, data from user "01" will be returned. Note that when you disable "case-sensitivity", it will also remove all accents from both sides during comparison, but the original field value won’t be affected.
Spaces are also removed during comparison, whether case_sensitive is disabled or not.
If you're tired of writing forest.condition() every time, you can store it in a variable and reuse it like this:
// Just use "cond" now
const cond = forest.condition();
// Extract users that lives in "New york"
/* rest of the code... */
preferences: cond.string({ operator: '===', value: 'dark', path: 'preferences.theme' })
address: cond.string({ operator: '===', value: 'New york', path: 'address.city', case_sensitive: false })
/* rest of the code... */Don't set any type for "cond" (it's not necessary), as doing so will disable auto-suggestion.
!==operator
// Extract users that `doesn't` use "dark theme"
/* rest of the code... */
preferences: forest.condition().string({ operator: '!==', value: 'dark', path: 'preferences.theme' })
/* rest of the code... */L==operator
// Extract users with an "ID" whose length is `equal` to 27.
/* rest of the code... */
id: forest.condition().string({ operator: 'L==', value: 27 })
/* rest of the code... */This operator compares only the char length. It takes a number or a string as its value.
If you provide a "string" as the value, it will compare the length of the field's value with the length of your provided value.
L>operator
// Extract users with an "ID" whose length is `superior` to 27.
/* rest of the code... */
id: forest.condition().string({ operator: 'L>', value: 27 })
/* rest of the code... */L>=operator
// Extract users with an "ID" whose length is `superior` or `equal` to 27.
/* rest of the code... */
id: forest.condition().string({ operator: 'L>=', value: 27 })
/* rest of the code... */L<operator
// Extract users with an "ID" whose length is `inferior` to 27.
/* rest of the code... */
id: forest.condition().string({ operator: 'L>', value: 27 })
/* rest of the code... */L<=operator
// Extract users with an "ID" whose length is `inferior` or `equal` to 27.
/* rest of the code... */
id: forest.condition().string({ operator: 'L>=', value: 27 })
/* rest of the code... */wL==operator
// Extract users that have `2` names.
/* rest of the code... */
name: forest.condition().string({ operator: 'wL==', value: 2 })
/* rest of the code... */wL== works exactly like L==, but it compares the word count instead of the "char length." Note that the w is lowercase.
wL>operator
// Extract users that have more than `2` names.
/* rest of the code... */
name: forest.condition().string({ operator: 'wL>', value: 2 })
/* rest of the code... */wL>=operator
// Extract users that have at least `2` names.
/* rest of the code... */
name: forest.condition().string({ operator: 'wL>=', value: 2 })
/* rest of the code... */wL<operator
// Extract users that have less than `2` names.
/* rest of the code... */
name: forest.condition().string({ operator: 'wL<', value: 2 })
/* rest of the code... */wL<=operator
// Extract users that have `2` names max.
/* rest of the code... */
name: forest.condition().string({ operator: 'wL<=', value: 2 })
/* rest of the code... */<>operator
// Extract users with the word "soft" in their position title.
/* rest of the code... */
company: forest.condition().string({ operator: '<>', value: 'soft', path: 'company.position', case_sensitive: false })
/* rest of the code... */
// Extract users with at least one of the words in "value" contained in their position title.
/* rest of the code... */
company: forest.condition().string({ operator: '<>', value: ['soft', 'engineer', 'tech'], path: 'company.position', case_sensitive: false })
/* rest of the code... */The operator <> checks if the field's value contains a particular word. It accepts a string or an array of strings as its value.
If you provide an array of strings, at least one of them must be contained in the field's value to match the condition.
!<>operator
// Extract users that don't have the word "soft" in their position title.
/* rest of the code... */
company: forest.condition().string({ operator: '!<>', value: 'soft', path: 'company.position', case_sensitive: false })
/* rest of the code... */
// Extract users that don't have at least one of the words in "value" contained in their position title.
/* rest of the code... */
company: forest.condition().string({ operator: '!<>', value: ['soft', 'engineer'], path: 'company.position', case_sensitive: false })
/* rest of the code... */<*>operator
// Extract users that have all of the words in "value" contained in their position title.
/* rest of the code... */
company: forest.condition().string({ operator: '<*>', value: ['soft', 'engineer'], path: 'company.position', case_sensitive: false })
/* rest of the code... */!<*>operator
// Extract users that have none of the words in "value" contained in their position title.
/* rest of the code... */
company: forest.condition().string({ operator: '!<*>', value: ['soft', 'engineer'], path: 'company.position', case_sensitive: false })
/* rest of the code... */<?>operator
// Extract users that have at least one of the words in "value" equal to their position title.
/* rest of the code... */
company: forest.condition().string({ operator: '<?>', value: ['software engineer', 'tech'], path: 'company.position', case_sensitive: false })
/* rest of the code... */The operator <?> is used to check if the field's value is equal (===) to one the string inside "value".
!<?>operator
// Extract users that have at least one of the words in "value" not equal to their position title.
/* rest of the code... */
company: forest.condition().string({ operator: '!<?>', value: ['soft', 'engineer'], path: 'company.position', case_sensitive: false })
/* rest of the code... */It's the contrary of <?>.
customoperator
// Extract users with an "ID" whose length is `equal` to 27.
/* rest of the code... */
id: forest.condition().string({
operator: 'custom',
customCondition: (x: { value: string }) => {
const user_id = x.value;
const id_length = user_id.length;
return id_length === 27 ? true : false;
}
})
/* rest of the code... */You can use a custom condition with the case_sensitive option here.
If case_sensitive is set to false, the "value" will be returned in lowercase with accents removed.
permutationoption
// 1. With "permutation", add "ENGINEER" to positions that don't contain it.
// 2. Extract users that have "engineer" in their position. So, all users.
/* rest of the code... */
company: forest.condition().string({
operator: '<>',
value: 'engineer',
path: 'company.position',
case_sensitive: false,
permutation: (x: { value: string }) => {
let user_position = x.value; // "x.value" will be lowercase because of "case_sensitive" set to false - Be very carefull about thoses details
if (!user_position.includes('engenieer')) user_position = user_position + ' ENGINEER';
return user_position;
}
})
/* rest of the code... */The transaction above will return all users because of what we did inside the "permutation," but note that the "original" user's position has not been modified at all.
Also, even though we added "ENGINEER" in capital letters, it will be lowercased because the returned value of the "permutation" will be lowercase due to case_sensitive being set to false.
Deep filtering on
Date
When it comes to date management, forest provides the best precision possible. Its default date schema looks like this: YYYY-MM-DDTHH:mm:ss.sss++HH:+mm.
YYYY: The year.MM: The month.DD: The date.HH: The hour.mm: The minutes.ss: The seconds.sss: The milliseconds.+HH: The UTC hour.+mm: The UTC minutes.
Now, let's see how it works in practice.
===operator
// Extract users created on '2024/03/07'
/* rest of the code... */
createdAt: forest.condition().date({ operator: '===', value: '2024/03/07' })
/* rest of the code... */When working with dates in forest, it's important to remember that specifying a date directly (e.g. createdAt: '2024/02/07') will treat the date as a simple string,
and passing a timestamp this way will treat it as a simple number. To properly manipulate dates, always use the date condition.
The condition you see above might seem simple, but there’s more happening under the hood. For example, user "01" has a createdAt field with a value of 2024-03-07T12:00:00Z.
The date in the condition is only 2024/03/07, which lacks the time component.
If you use new Date('2024/03/07').toISOString(), you will get 2024-03-06T23:00:00.000Z, which is different from the user’s date. However, if you use new Date('2024-03-07').toISOString(),
you get 2024-03-07T00:00:00.000Z, which matches the date but has a time of 00:00:00.
If you convert these dates to timestamps, you’ll see differences in their numeric values, which would cause the condition to fail.
To address this, forest builds a schema for each date and uses it for precise comparisons. In this case, the user’s date (2024-03-07T12:00:00Z) has the schema YYYY-MM-DDTHH:mm:ss,
while the condition’s date (2024/03/07) has the schema YYYY-MM-DD. The schema for the user date will be scaled down to match the condition date, excluding unnecessary time data,
ensuring an accurate comparison.
If you modify the condition to include a time (2024/03/07 11:00), the schema of both the user date and condition date will be scaled accordingly. However, the condition will
fail if the times are different.
You can also use different date formats, like 03/07/2024 (MM/DD/YYYY) or 07/03/2024 (DD/MM/YYYY). However, avoid using both formats in the same app to prevent
confusion between the month and day.
!==operator
// Extract users `not` created on '2024/03/07'
/* rest of the code... */
createdAt: forest.condition().date({ operator: '!==', value: '2024/03/07' })
/* rest of the code... */>operator
// Extract users created `after` '2024/03/07 09:00'
/* rest of the code... */
createdAt: forest.condition().date({ operator: '>', value: '2024/03/07 09:00' })
/* rest of the code... */>=operator
// Extract users created on '2024/03/07' or `after`
/* rest of the code... */
createdAt: forest.condition().date({ operator: '>=', value: '2024/03/07' })
/* rest of the code... */<operator
// Extract users created `before` '2024/03/07 15:05:20'
/* rest of the code... */
createdAt: forest.condition().date({ operator: '<', value: '2024/03/07 15:05:20' })
/* rest of the code... */<=operator
// Extract users created on '2024/03/07 15:09:00.524' or `before`
/* rest of the code... */
createdAt: forest.condition().date({ operator: '<=', value: '2024/03/07 15:09:00.524' }) // Here we precise the milliseconds
/* rest of the code... */<>operator
// Extract users created `from` '2024/02/07' `to` '2024/03/07'
/* rest of the code... */
createdAt: forest.condition().date({ operator: '<>', value: ['2024/02/07', '2024/03/07'] })
/* rest of the code... */
// Extract users created `from` '2024/01/07' `to` '2024/03/07' or `from` '2024/02/04' to '2024/03/02'
/* rest of the code... */
createdAt: forest.condition().date({ operator: '<>', value: [['2024/01/07', '2024/03/07'], ['2024/02/04', '2024/03/02']] })
/* rest of the code... */The operator <> works with ranges of two dates. The value can be either an array of two dates or an array containing many sub-arrays of two dates.
Even if you put the dates in the wrong order, forest will automatically correct the ranges in ascending order.
At least one range should match the condition.
You can use timestamp directly, but only if both values are timestamps. Otherwise, accuracy may be lost, as timestamp in ISO format includes the full schema.
!<>operator
// Extract users `not` created `from` '2024/01/07' `to` '2024/03/07' or `from` '2024/02/04' to '2024/03/02'
/* rest of the code... */
createdAt: forest.condition().date({ operator: '!<>', value: [['2024/01/07', '2024/03/07'], ['2024/02/04', '2024/03/02']] })
/* rest of the code... */At least one range should not match the condition.
<*>operator
// Extract users updated `from` '2024/01/07' `to` '2024/03/07' and `from` '2024/02/04' to '2024/03/02'
/* rest of the code... */
updatedAt: forest.condition().date({ operator: '<*>', value: [['2024/01/07', '2024/03/07'], ['2024/02/04', '2024/03/02']] })
/* rest of the code... */The operator <*> works like <>, except that every range should match the condition.
!<*>operator
// Extract users `not` updated `from` '2024/01/07' `to` '2024/03/07' and `from` '2024/02/04' to '2024/03/02'
/* rest of the code... */
updatedAt: forest.condition().date({ operator: '!<*>', value: [['2024/01/07', '2024/03/07'], ['2024/02/04', '2024/03/02']] })
/* rest of the code... */Here, no range should match the condition.
><operator
// Extract users updated `between` '2024/02/07' `and` '2024/03/07'
/* rest of the code... */
updatedAt: forest.condition().date({ operator: '><', value: ['2024/02/07', '2024/03/07'] })
/* rest of the code... */
// Extract users updated `between` '2024/01/07' `and` '2024/03/07' or `between` '2024/02/04' and '2024/03/02'
/* rest of the code... */
updatedAt: forest.condition().date({ operator: '><', value: [['2024/01/07', '2024/03/07'], ['2024/02/04', '2024/03/02']] })
/* rest of the code... */Ranges at the edge are excluded.
!><operator
// Extract users `not` updated `between` '2024/01/07' `and` '2024/03/07' or `between` '2024/02/04' and '2024/03/02'
/* rest of the code... */
updatedAt: forest.condition().date({ operator: '!><', value: [['2024/01/07', '2024/03/07'], ['2024/02/04', '2024/03/02']] })
/* rest of the code... */>*<operator
// Extract users updated `between` '2024/01/07' `and` '2024/03/07' and `between` '2024/02/04' and '2024/03/02'
/* rest of the code... */
updatedAt: forest.condition().date({ operator: '>*<', value: [['2024/01/07', '2024/03/07'], ['2024/02/04', '2024/03/02']] })
/* rest of the code... */!>*<operator
// Extract users `not` updated `between` '2024/01/07' `and` '2024/03/07' and `between` '2024/02/04' and '2024/03/02'
/* rest of the code... */
updatedAt: forest.condition().date({ operator: '>*<', value: [['2024/01/07', '2024/03/07'], ['2024/02/04', '2024/03/02']] })
/* rest of the code... */<?>operator
// Extract users whose "createdAt" is equal to at least one of the entries inside "value"
/* rest of the code... */
updatedAt: forest.condition().date({ operator: '<?>', value: ['2024/01/07', '2024/03/07'] })
/* rest of the code... */!<?>operator
// Extract users whose "createdAt" is not equal to none of the entries inside "value"
/* rest of the code... */
updatedAt: forest.condition().date({ operator: '<?>', value: ['2024/01/07', '2024/03/07'] })
/* rest of the code... */=Q1operator
// Extract users created in the "first quarter" of 2024
/* rest of the code... */
createdAt: forest.condition().date({ operator: '=Q1', year: 2024 })
/* rest of the code... */=Q2operator
// Extract users created in the "second quarter" of 2024
/* rest of the code... */
createdAt: forest.condition().date({ operator: '=Q2', year: 2024 })
/* rest of the code... */=Q3operator
// Extract users created in the "third quarter" of 2024
/* rest of the code... */
createdAt: forest.condition().date({ operator: '=Q3', year: 2024 })
/* rest of the code... */=Q4operator
// Extract users created in the "fourth quarter" of 2024
/* rest of the code... */
createdAt: forest.condition().date({ operator: '=Q4', year: 2024 })
/* rest of the code... */=S1operator
// Extract users created in the "first semester" of 2024
/* rest of the code... */
createdAt: forest.condition().date({ operator: '=S1', year: 2024 })
/* rest of the code... */=S2operator
// Extract users created in the "second semester" of 2024
/* rest of the code... */
createdAt: forest.condition().date({ operator: '=S2', year: 2024 })
/* rest of the code... */customoperator
Like seen above, you can also use a custom condition.
permutationoption
As seen above, you can also use a 'permutation,' and it should return a valid date as a string or number.
Deep filtering on
Boolean
// Extract users that `can` receive notifications by email
/* rest of the code... */
preferences: forest.condition()