@joystick.js/db-canary 0.0.0-canary.2249 → 0.0.0-canary.2251
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/lib/operations/find.js +1 -1
- package/dist/server/lib/operations/find_one.js +1 -1
- package/package.json +2 -2
- package/src/server/lib/operations/find.js +157 -14
- package/src/server/lib/operations/find_one.js +157 -13
- package/test_data_api_key_1758233848259_cglfjzhou/data.mdb +0 -0
- package/test_data_api_key_1758233848259_cglfjzhou/lock.mdb +0 -0
- package/test_data_api_key_1758233848502_urlje2utd/data.mdb +0 -0
- package/test_data_api_key_1758233848502_urlje2utd/lock.mdb +0 -0
- package/test_data_api_key_1758233848738_mtcpfe5ns/data.mdb +0 -0
- package/test_data_api_key_1758233848738_mtcpfe5ns/lock.mdb +0 -0
- package/test_data_api_key_1758233848856_9g97p6gag/data.mdb +0 -0
- package/test_data_api_key_1758233848856_9g97p6gag/lock.mdb +0 -0
- package/test_data_api_key_1758233857008_0tl9zzhj8/data.mdb +0 -0
- package/test_data_api_key_1758233857008_0tl9zzhj8/lock.mdb +0 -0
- package/test_data_api_key_1758233857120_60c2f2uhu/data.mdb +0 -0
- package/test_data_api_key_1758233857120_60c2f2uhu/lock.mdb +0 -0
- package/test_data_api_key_1758233857232_aw7fkqgd9/data.mdb +0 -0
- package/test_data_api_key_1758233857232_aw7fkqgd9/lock.mdb +0 -0
- package/test_data_api_key_1758234881285_4aeflubjb/data.mdb +0 -0
- package/test_data_api_key_1758234881285_4aeflubjb/lock.mdb +0 -0
- package/test_data_api_key_1758234881520_kb0amvtqb/data.mdb +0 -0
- package/test_data_api_key_1758234881520_kb0amvtqb/lock.mdb +0 -0
- package/test_data_api_key_1758234881756_k04gfv2va/data.mdb +0 -0
- package/test_data_api_key_1758234881756_k04gfv2va/lock.mdb +0 -0
- package/test_data_api_key_1758234881876_wn90dpo1z/data.mdb +0 -0
- package/test_data_api_key_1758234881876_wn90dpo1z/lock.mdb +0 -0
- package/test_data_api_key_1758234889461_26xz3dmbr/data.mdb +0 -0
- package/test_data_api_key_1758234889461_26xz3dmbr/lock.mdb +0 -0
- package/test_data_api_key_1758234889572_uziz7e0p5/data.mdb +0 -0
- package/test_data_api_key_1758234889572_uziz7e0p5/lock.mdb +0 -0
- package/test_data_api_key_1758234889684_5f9wmposh/data.mdb +0 -0
- package/test_data_api_key_1758234889684_5f9wmposh/lock.mdb +0 -0
- package/test_data_api_key_1758235657729_prwgm6mxr/data.mdb +0 -0
- package/test_data_api_key_1758235657729_prwgm6mxr/lock.mdb +0 -0
- package/test_data_api_key_1758235657961_rc2da0dc2/data.mdb +0 -0
- package/test_data_api_key_1758235657961_rc2da0dc2/lock.mdb +0 -0
- package/test_data_api_key_1758235658193_oqqxm0sny/data.mdb +0 -0
- package/test_data_api_key_1758235658193_oqqxm0sny/lock.mdb +0 -0
- package/test_data_api_key_1758235658309_vggac1pj6/data.mdb +0 -0
- package/test_data_api_key_1758235658309_vggac1pj6/lock.mdb +0 -0
- package/test_data_api_key_1758235665968_61ko07dd1/data.mdb +0 -0
- package/test_data_api_key_1758235665968_61ko07dd1/lock.mdb +0 -0
- package/test_data_api_key_1758235666082_50lrt6sq8/data.mdb +0 -0
- package/test_data_api_key_1758235666082_50lrt6sq8/lock.mdb +0 -0
- package/test_data_api_key_1758235666194_ykvauwlzh/data.mdb +0 -0
- package/test_data_api_key_1758235666194_ykvauwlzh/lock.mdb +0 -0
- package/test_data_api_key_1758236187207_9c4paeh09/data.mdb +0 -0
- package/test_data_api_key_1758236187207_9c4paeh09/lock.mdb +0 -0
- package/test_data_api_key_1758236187441_4n3o3gkkl/data.mdb +0 -0
- package/test_data_api_key_1758236187441_4n3o3gkkl/lock.mdb +0 -0
- package/test_data_api_key_1758236187672_jt6b21ye0/data.mdb +0 -0
- package/test_data_api_key_1758236187672_jt6b21ye0/lock.mdb +0 -0
- package/test_data_api_key_1758236187788_oo84fz9u6/data.mdb +0 -0
- package/test_data_api_key_1758236187788_oo84fz9u6/lock.mdb +0 -0
- package/test_data_api_key_1758236195507_o9zeznwlm/data.mdb +0 -0
- package/test_data_api_key_1758236195507_o9zeznwlm/lock.mdb +0 -0
- package/test_data_api_key_1758236195619_qsqd60y41/data.mdb +0 -0
- package/test_data_api_key_1758236195619_qsqd60y41/lock.mdb +0 -0
- package/test_data_api_key_1758236195731_im13iq284/data.mdb +0 -0
- package/test_data_api_key_1758236195731_im13iq284/lock.mdb +0 -0
- package/tests/server/cluster/master_read_write_operations.test.js +5 -14
- package/tests/server/integration/authentication_integration.test.js +18 -10
- package/tests/server/integration/backup_integration.test.js +35 -27
- package/tests/server/lib/api_key_manager.test.js +88 -32
- package/tests/server/lib/development_mode.test.js +2 -2
- package/tests/server/lib/operations/admin.test.js +20 -12
- package/tests/server/lib/operations/delete_one.test.js +10 -4
- package/tests/server/lib/operations/find_array_queries.test.js +261 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
import{get_database as
|
|
1
|
+
import{get_database as J,build_collection_key as q}from"../query_engine.js";import{can_use_index as N,find_documents_by_index as F}from"../index_manager.js";import{record_query as S,record_index_usage as E}from"../auto_index_manager.js";import R from"../logger.js";const{create_context_logger:C}=R("find"),D=(i,t)=>{const o=t.split(".");let r=i;for(let e=0;e<o.length;e++){const l=o[e];if(r==null)return;if(r=r[l],Array.isArray(r)&&e<o.length-1){const n=o.slice(e+1).join("."),u=[];for(let c=0;c<r.length;c++){const d=r[c];if(typeof d=="object"&&d!==null){const f=D(d,n);f!==void 0&&(Array.isArray(f)?u.push(...f):u.push(f))}}return u.length>0?u:void 0}}return r},P=(i,t)=>{const o=t.split(".");let r=i;for(let e=0;e<o.length;e++){if(r==null||typeof r!="object")return!1;if(e===o.length-1)return r.hasOwnProperty(o[e]);r=r[o[e]]}return!1},b=(i,t)=>{if(!t||Object.keys(t).length===0)return!0;if(t.$or&&Array.isArray(t.$or)){const o=t.$or;let r=!1;for(let l=0;l<o.length;l++)if(b(i,o[l])){r=!0;break}if(!r)return!1;const e={...t};return delete e.$or,Object.keys(e).length>0?b(i,e):!0}for(const[o,r]of Object.entries(t)){const e=D(i,o);if(typeof r=="object"&&r!==null&&!Array.isArray(r))for(const[l,n]of Object.entries(r))switch(l){case"$eq":if(Array.isArray(e)){if(!e.includes(n))return!1}else if(e!==n)return!1;break;case"$ne":if(Array.isArray(e)){if(e.includes(n))return!1}else if(e===n)return!1;break;case"$gt":if(Array.isArray(e)){let f=!1;for(let s=0;s<e.length;s++)if(e[s]>n){f=!0;break}if(!f)return!1}else if(e<=n)return!1;break;case"$gte":if(Array.isArray(e)){let f=!1;for(let s=0;s<e.length;s++)if(e[s]>=n){f=!0;break}if(!f)return!1}else if(e<n)return!1;break;case"$lt":if(Array.isArray(e)){let f=!1;for(let s=0;s<e.length;s++)if(e[s]<n){f=!0;break}if(!f)return!1}else if(e>=n)return!1;break;case"$lte":if(Array.isArray(e)){let f=!1;for(let s=0;s<e.length;s++)if(e[s]<=n){f=!0;break}if(!f)return!1}else if(e>n)return!1;break;case"$in":if(!Array.isArray(n))return!1;if(Array.isArray(e)){let f=!1;for(let s=0;s<e.length;s++)if(n.includes(e[s])){f=!0;break}if(!f)return!1}else if(!n.includes(e))return!1;break;case"$nin":if(!Array.isArray(n))return!1;if(Array.isArray(e)){for(let f=0;f<e.length;f++)if(n.includes(e[f]))return!1}else if(n.includes(e))return!1;break;case"$exists":const u=P(i,o);if(n&&!u||!n&&u)return!1;break;case"$regex":const c=r.$options||"",d=new RegExp(n,c);if(Array.isArray(e)){let f=!1;for(let s=0;s<e.length;s++)if(typeof e[s]=="string"&&d.test(e[s])){f=!0;break}if(!f)return!1}else if(!d.test(e))return!1;break;case"$options":break;default:throw new Error(`Unsupported query operator: ${l}`)}else if(Array.isArray(e)){if(!e.includes(r))return!1}else if(e!==r)return!1}return!0},U=(i,t)=>{if(!t||Object.keys(t).length===0)return i;const o=Object.values(t).some(e=>e===1||e===!0),r={};if(o){r._id=i._id;for(const[e,l]of Object.entries(t))e==="_id"&&(l===0||l===!1)?delete r._id:(l===1||l===!0)&&(r[e]=i[e])}else{Object.assign(r,i);for(const[e,l]of Object.entries(t))(l===0||l===!1)&&delete r[e]}return r},z=(i,t)=>!t||Object.keys(t).length===0?i:i.sort((o,r)=>{for(const[e,l]of Object.entries(t)){const n=o[e],u=r[e];if(n===u)continue;if(n===void 0)return 1;if(u===void 0)return-1;const c=n<u?-1:n>u?1:0;return l===-1?-c:c}return 0}),B=async(i,t,o={},r={})=>{const e=C();if(!i)throw new Error("Database name is required");if(!t)throw new Error("Collection name is required");const l=J(),{projection:n,sort:u,limit:c,skip:d=0}=r,f=Date.now();try{let s=[],_=!1,$=null;const m=N(i,t,o);if(m){const{field:a,operators:k}=m,g=o[a];if($=a,typeof g=="object"&&g!==null&&!Array.isArray(g)){for(const y of k)if(g[y]!==void 0){const p=F(i,t,a,y,g[y]);if(p){_=!0,E(i,t,a);for(const j of p){const x=q(i,t,j),A=l.get(x);if(A){const v=JSON.parse(A);b(v,o)&&s.push(v)}}break}}}else if(k.includes("eq")){const y=F(i,t,a,"eq",g);if(y){_=!0,E(i,t,a);for(const p of y){const j=q(i,t,p),x=l.get(j);if(x){const A=JSON.parse(x);b(A,o)&&s.push(A)}}}}}if(!_){const a=`${i}:${t}:`,k=l.getRange({start:a,end:a+"\xFF"});for(const{key:g,value:y}of k){const p=JSON.parse(y);b(p,o)&&s.push(p)}}let h=z(s,u);d>0&&(h=h.slice(d)),c&&c>0&&(h=h.slice(0,c));const O=h.map(a=>U(a,n)),w=Date.now()-f;try{S(t,o,w,_,$)}catch(a){e.warn("Failed to record query for auto-indexing",{error:a.message})}return e.info("Find operation completed",{database:i,collection:t,documents_found:O.length,total_matching:s.length,used_index:_,indexed_field:$,execution_time_ms:w}),O}catch(s){throw e.error("Failed to find documents",{database:i,collection:t,error:s.message}),s}};var L=B;export{L as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{get_database as
|
|
1
|
+
import{get_database as q,build_collection_key as w}from"../query_engine.js";import{can_use_index as D,find_documents_by_index as O}from"../index_manager.js";import{record_query as E,record_index_usage as m}from"../auto_index_manager.js";import F from"../logger.js";const{create_context_logger:N}=F("find_one"),v=(i,s)=>{const o=s.split(".");let r=i;for(let e=0;e<o.length;e++){const l=o[e];if(r==null)return;if(r=r[l],Array.isArray(r)&&e<o.length-1){const f=o.slice(e+1).join("."),y=[];for(let g=0;g<r.length;g++){const u=r[g];if(typeof u=="object"&&u!==null){const t=v(u,f);t!==void 0&&(Array.isArray(t)?y.push(...t):y.push(t))}}return y.length>0?y:void 0}}return r},J=(i,s)=>{const o=s.split(".");let r=i;for(let e=0;e<o.length;e++){if(r==null||typeof r!="object")return!1;if(e===o.length-1)return r.hasOwnProperty(o[e]);r=r[o[e]]}return!1},h=(i,s)=>{if(!s||Object.keys(s).length===0)return!0;if(s.$or&&Array.isArray(s.$or)){const o=s.$or;let r=!1;for(let l=0;l<o.length;l++)if(h(i,o[l])){r=!0;break}if(!r)return!1;const e={...s};return delete e.$or,Object.keys(e).length>0?h(i,e):!0}for(const[o,r]of Object.entries(s)){const e=v(i,o);if(typeof r=="object"&&r!==null&&!Array.isArray(r))for(const[l,f]of Object.entries(r))switch(l){case"$eq":if(Array.isArray(e)){if(!e.includes(f))return!1}else if(e!==f)return!1;break;case"$ne":if(Array.isArray(e)){if(e.includes(f))return!1}else if(e===f)return!1;break;case"$gt":if(Array.isArray(e)){let t=!1;for(let n=0;n<e.length;n++)if(e[n]>f){t=!0;break}if(!t)return!1}else if(e<=f)return!1;break;case"$gte":if(Array.isArray(e)){let t=!1;for(let n=0;n<e.length;n++)if(e[n]>=f){t=!0;break}if(!t)return!1}else if(e<f)return!1;break;case"$lt":if(Array.isArray(e)){let t=!1;for(let n=0;n<e.length;n++)if(e[n]<f){t=!0;break}if(!t)return!1}else if(e>=f)return!1;break;case"$lte":if(Array.isArray(e)){let t=!1;for(let n=0;n<e.length;n++)if(e[n]<=f){t=!0;break}if(!t)return!1}else if(e>f)return!1;break;case"$in":if(!Array.isArray(f))return!1;if(Array.isArray(e)){let t=!1;for(let n=0;n<e.length;n++)if(f.includes(e[n])){t=!0;break}if(!t)return!1}else if(!f.includes(e))return!1;break;case"$nin":if(!Array.isArray(f))return!1;if(Array.isArray(e)){for(let t=0;t<e.length;t++)if(f.includes(e[t]))return!1}else if(f.includes(e))return!1;break;case"$exists":const y=J(i,o);if(f&&!y||!f&&y)return!1;break;case"$regex":const g=r.$options||"",u=new RegExp(f,g);if(Array.isArray(e)){let t=!1;for(let n=0;n<e.length;n++)if(typeof e[n]=="string"&&u.test(e[n])){t=!0;break}if(!t)return!1}else if(!u.test(e))return!1;break;case"$options":break;default:throw new Error(`Unsupported query operator: ${l}`)}else if(Array.isArray(e)){if(!e.includes(r))return!1}else if(e!==r)return!1}return!0},S=(i,s)=>{if(!s||Object.keys(s).length===0)return i;const o=Object.values(s).some(e=>e===1||e===!0),r={};if(o){r._id=i._id;for(const[e,l]of Object.entries(s))e==="_id"&&(l===0||l===!1)?delete r._id:(l===1||l===!0)&&(r[e]=i[e])}else{Object.assign(r,i);for(const[e,l]of Object.entries(s))(l===0||l===!1)&&delete r[e]}return r},R=async(i,s,o={},r={})=>{const e=N();if(!i)throw new Error("Database name is required");if(!s)throw new Error("Collection name is required");const l=q(),{projection:f,sort:y}=r,g=Date.now();try{let u=null,t=!1,n=null;const j=D(i,s,o);if(j){const{field:a,operators:A}=j,p=o[a];if(n=a,typeof p=="object"&&p!==null&&!Array.isArray(p)){for(const c of A)if(p[c]!==void 0){const d=O(i,s,a,c,p[c]);if(d&&d.length>0){t=!0,m(i,s,a);for(const x of d){const b=w(i,s,x),_=l.get(b);if(_)try{const $=JSON.parse(_);if(h($,o)){u=$;break}}catch{continue}}break}}}else if(A.includes("eq")){const c=O(i,s,a,"eq",p);if(c&&c.length>0){t=!0,m(i,s,a);for(const d of c){const x=w(i,s,d),b=l.get(x);if(b)try{const _=JSON.parse(b);if(h(_,o)){u=_;break}}catch{continue}}}}}if(!t){const a=`${i}:${s}:`,A=l.getRange({start:a,end:a+"\xFF"});for(const{key:p,value:c}of A)try{const d=JSON.parse(c);if(h(d,o)){u=d;break}}catch{continue}}const k=Date.now()-g;try{E(s,o,k,t,n)}catch(a){e.warn("Failed to record query for auto-indexing",{error:a.message})}if(u){const a=S(u,f);return e.info("Document found",{database:i,collection:s,document_id:u._id,used_index:t,indexed_field:n,execution_time_ms:k}),a}return e.info("No document found",{database:i,collection:s,used_index:t,indexed_field:n,execution_time_ms:k}),null}catch(u){throw e.error("Failed to find document",{database:i,collection:s,error:u.message}),u}};var B=R;export{B as default};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joystick.js/db-canary",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.0-canary.
|
|
5
|
-
"canary_version": "0.0.0-canary.
|
|
4
|
+
"version": "0.0.0-canary.2251",
|
|
5
|
+
"canary_version": "0.0.0-canary.2250",
|
|
6
6
|
"description": "JoystickDB - A minimalist database server for the Joystick framework",
|
|
7
7
|
"main": "./dist/server/index.js",
|
|
8
8
|
"scripts": {
|
|
@@ -13,19 +13,48 @@ const { create_context_logger } = create_logger('find');
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Extracts field value from document using dot notation path.
|
|
16
|
+
* Handles nested array object queries like 'reviews.rating' where reviews is an array of objects.
|
|
16
17
|
* @param {Object} document - Document to extract value from
|
|
17
|
-
* @param {string} field_path - Dot-separated field path (e.g., 'user.profile.name')
|
|
18
|
-
* @returns {*} Field value or undefined if path doesn't exist
|
|
18
|
+
* @param {string} field_path - Dot-separated field path (e.g., 'user.profile.name', 'reviews.rating')
|
|
19
|
+
* @returns {*} Field value, array of values for nested array queries, or undefined if path doesn't exist
|
|
19
20
|
*/
|
|
20
21
|
const get_field_value = (document, field_path) => {
|
|
21
22
|
const parts = field_path.split('.');
|
|
22
23
|
let value = document;
|
|
23
24
|
|
|
24
|
-
for (
|
|
25
|
+
for (let i = 0; i < parts.length; i++) {
|
|
26
|
+
const part = parts[i];
|
|
27
|
+
|
|
25
28
|
if (value === null || value === undefined) {
|
|
26
29
|
return undefined;
|
|
27
30
|
}
|
|
31
|
+
|
|
28
32
|
value = value[part];
|
|
33
|
+
|
|
34
|
+
// If we got an array and there are more parts to traverse
|
|
35
|
+
if (Array.isArray(value) && i < parts.length - 1) {
|
|
36
|
+
// Extract the remaining path
|
|
37
|
+
const remaining_path = parts.slice(i + 1).join('.');
|
|
38
|
+
|
|
39
|
+
// Collect all matching values from array elements
|
|
40
|
+
const nested_values = [];
|
|
41
|
+
for (let j = 0; j < value.length; j++) {
|
|
42
|
+
const item = value[j];
|
|
43
|
+
if (typeof item === 'object' && item !== null) {
|
|
44
|
+
const nested_value = get_field_value(item, remaining_path);
|
|
45
|
+
if (nested_value !== undefined) {
|
|
46
|
+
if (Array.isArray(nested_value)) {
|
|
47
|
+
nested_values.push(...nested_value);
|
|
48
|
+
} else {
|
|
49
|
+
nested_values.push(nested_value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Return array of values for nested array queries
|
|
56
|
+
return nested_values.length > 0 ? nested_values : undefined;
|
|
57
|
+
}
|
|
29
58
|
}
|
|
30
59
|
|
|
31
60
|
return value;
|
|
@@ -58,7 +87,7 @@ const field_exists = (document, field_path) => {
|
|
|
58
87
|
|
|
59
88
|
/**
|
|
60
89
|
* Checks if a document matches the provided filter criteria.
|
|
61
|
-
* Supports MongoDB-like query operators
|
|
90
|
+
* Supports MongoDB-like query operators with optimized array field matching.
|
|
62
91
|
* @param {Object} document - Document to test against filter
|
|
63
92
|
* @param {Object} filter - Filter criteria with field names and values/operators
|
|
64
93
|
* @returns {boolean} True if document matches filter, false otherwise
|
|
@@ -69,6 +98,27 @@ const matches_filter = (document, filter) => {
|
|
|
69
98
|
return true;
|
|
70
99
|
}
|
|
71
100
|
|
|
101
|
+
// Handle $or operator at the top level
|
|
102
|
+
if (filter.$or && Array.isArray(filter.$or)) {
|
|
103
|
+
const or_conditions = filter.$or;
|
|
104
|
+
let or_match = false;
|
|
105
|
+
for (let i = 0; i < or_conditions.length; i++) {
|
|
106
|
+
if (matches_filter(document, or_conditions[i])) {
|
|
107
|
+
or_match = true;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!or_match) return false;
|
|
112
|
+
|
|
113
|
+
// Continue checking other conditions (if any) outside of $or
|
|
114
|
+
const remaining_filter = { ...filter };
|
|
115
|
+
delete remaining_filter.$or;
|
|
116
|
+
if (Object.keys(remaining_filter).length > 0) {
|
|
117
|
+
return matches_filter(document, remaining_filter);
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
72
122
|
for (const [field, value] of Object.entries(filter)) {
|
|
73
123
|
const field_value = get_field_value(document, field);
|
|
74
124
|
|
|
@@ -76,28 +126,105 @@ const matches_filter = (document, filter) => {
|
|
|
76
126
|
for (const [operator, operand] of Object.entries(value)) {
|
|
77
127
|
switch (operator) {
|
|
78
128
|
case '$eq':
|
|
79
|
-
|
|
129
|
+
// Handle array field matching for $eq
|
|
130
|
+
if (Array.isArray(field_value)) {
|
|
131
|
+
if (!field_value.includes(operand)) return false;
|
|
132
|
+
} else if (field_value !== operand) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
80
135
|
break;
|
|
81
136
|
case '$ne':
|
|
82
|
-
|
|
137
|
+
// Handle array field matching for $ne
|
|
138
|
+
if (Array.isArray(field_value)) {
|
|
139
|
+
if (field_value.includes(operand)) return false;
|
|
140
|
+
} else if (field_value === operand) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
83
143
|
break;
|
|
84
144
|
case '$gt':
|
|
85
|
-
if (field_value
|
|
145
|
+
if (Array.isArray(field_value)) {
|
|
146
|
+
let found = false;
|
|
147
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
148
|
+
if (field_value[i] > operand) {
|
|
149
|
+
found = true;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!found) return false;
|
|
154
|
+
} else if (field_value <= operand) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
86
157
|
break;
|
|
87
158
|
case '$gte':
|
|
88
|
-
if (field_value
|
|
159
|
+
if (Array.isArray(field_value)) {
|
|
160
|
+
let found = false;
|
|
161
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
162
|
+
if (field_value[i] >= operand) {
|
|
163
|
+
found = true;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!found) return false;
|
|
168
|
+
} else if (field_value < operand) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
89
171
|
break;
|
|
90
172
|
case '$lt':
|
|
91
|
-
if (field_value
|
|
173
|
+
if (Array.isArray(field_value)) {
|
|
174
|
+
let found = false;
|
|
175
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
176
|
+
if (field_value[i] < operand) {
|
|
177
|
+
found = true;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!found) return false;
|
|
182
|
+
} else if (field_value >= operand) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
92
185
|
break;
|
|
93
186
|
case '$lte':
|
|
94
|
-
if (field_value
|
|
187
|
+
if (Array.isArray(field_value)) {
|
|
188
|
+
let found = false;
|
|
189
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
190
|
+
if (field_value[i] <= operand) {
|
|
191
|
+
found = true;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (!found) return false;
|
|
196
|
+
} else if (field_value > operand) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
95
199
|
break;
|
|
96
200
|
case '$in':
|
|
97
|
-
if (!Array.isArray(operand)
|
|
201
|
+
if (!Array.isArray(operand)) return false;
|
|
202
|
+
if (Array.isArray(field_value)) {
|
|
203
|
+
// Check if any array element is in the operand array
|
|
204
|
+
let found = false;
|
|
205
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
206
|
+
if (operand.includes(field_value[i])) {
|
|
207
|
+
found = true;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!found) return false;
|
|
212
|
+
} else if (!operand.includes(field_value)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
98
215
|
break;
|
|
99
216
|
case '$nin':
|
|
100
|
-
if (!Array.isArray(operand)
|
|
217
|
+
if (!Array.isArray(operand)) return false;
|
|
218
|
+
if (Array.isArray(field_value)) {
|
|
219
|
+
// Check if any array element is in the operand array
|
|
220
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
221
|
+
if (operand.includes(field_value[i])) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else if (operand.includes(field_value)) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
101
228
|
break;
|
|
102
229
|
case '$exists':
|
|
103
230
|
const exists = field_exists(document, field);
|
|
@@ -108,7 +235,18 @@ const matches_filter = (document, filter) => {
|
|
|
108
235
|
// Handle $options parameter for regex flags
|
|
109
236
|
const regex_options = value.$options || '';
|
|
110
237
|
const regex = new RegExp(operand, regex_options);
|
|
111
|
-
if (
|
|
238
|
+
if (Array.isArray(field_value)) {
|
|
239
|
+
let found = false;
|
|
240
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
241
|
+
if (typeof field_value[i] === 'string' && regex.test(field_value[i])) {
|
|
242
|
+
found = true;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!found) return false;
|
|
247
|
+
} else if (!regex.test(field_value)) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
112
250
|
break;
|
|
113
251
|
case '$options':
|
|
114
252
|
// $options is handled as part of $regex, skip it here
|
|
@@ -118,7 +256,12 @@ const matches_filter = (document, filter) => {
|
|
|
118
256
|
}
|
|
119
257
|
}
|
|
120
258
|
} else {
|
|
121
|
-
|
|
259
|
+
// Direct field matching with array support
|
|
260
|
+
if (Array.isArray(field_value)) {
|
|
261
|
+
if (!field_value.includes(value)) return false;
|
|
262
|
+
} else if (field_value !== value) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
122
265
|
}
|
|
123
266
|
}
|
|
124
267
|
|
|
@@ -15,19 +15,48 @@ const { create_context_logger } = create_logger('find_one');
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Gets the value of a nested field from a document using dot notation.
|
|
18
|
+
* Handles nested array object queries like 'reviews.rating' where reviews is an array of objects.
|
|
18
19
|
* @param {Object} document - Document to extract field value from
|
|
19
|
-
* @param {string} field_path - Dot-separated field path (e.g., 'user.name')
|
|
20
|
-
* @returns {any} Field value or undefined if not found
|
|
20
|
+
* @param {string} field_path - Dot-separated field path (e.g., 'user.name', 'reviews.rating')
|
|
21
|
+
* @returns {any} Field value, array of values for nested array queries, or undefined if not found
|
|
21
22
|
*/
|
|
22
23
|
const get_field_value = (document, field_path) => {
|
|
23
24
|
const parts = field_path.split('.');
|
|
24
25
|
let value = document;
|
|
25
26
|
|
|
26
|
-
for (
|
|
27
|
+
for (let i = 0; i < parts.length; i++) {
|
|
28
|
+
const part = parts[i];
|
|
29
|
+
|
|
27
30
|
if (value === null || value === undefined) {
|
|
28
31
|
return undefined;
|
|
29
32
|
}
|
|
33
|
+
|
|
30
34
|
value = value[part];
|
|
35
|
+
|
|
36
|
+
// If we got an array and there are more parts to traverse
|
|
37
|
+
if (Array.isArray(value) && i < parts.length - 1) {
|
|
38
|
+
// Extract the remaining path
|
|
39
|
+
const remaining_path = parts.slice(i + 1).join('.');
|
|
40
|
+
|
|
41
|
+
// Collect all matching values from array elements
|
|
42
|
+
const nested_values = [];
|
|
43
|
+
for (let j = 0; j < value.length; j++) {
|
|
44
|
+
const item = value[j];
|
|
45
|
+
if (typeof item === 'object' && item !== null) {
|
|
46
|
+
const nested_value = get_field_value(item, remaining_path);
|
|
47
|
+
if (nested_value !== undefined) {
|
|
48
|
+
if (Array.isArray(nested_value)) {
|
|
49
|
+
nested_values.push(...nested_value);
|
|
50
|
+
} else {
|
|
51
|
+
nested_values.push(nested_value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Return array of values for nested array queries
|
|
58
|
+
return nested_values.length > 0 ? nested_values : undefined;
|
|
59
|
+
}
|
|
31
60
|
}
|
|
32
61
|
|
|
33
62
|
return value;
|
|
@@ -60,6 +89,7 @@ const field_exists = (document, field_path) => {
|
|
|
60
89
|
|
|
61
90
|
/**
|
|
62
91
|
* Checks if a document matches the given filter criteria.
|
|
92
|
+
* Supports MongoDB-like query operators with optimized array field matching.
|
|
63
93
|
* @param {Object} document - Document to test
|
|
64
94
|
* @param {Object} filter - Filter criteria with MongoDB-style operators
|
|
65
95
|
* @returns {boolean} True if document matches filter
|
|
@@ -70,6 +100,27 @@ const matches_filter = (document, filter) => {
|
|
|
70
100
|
return true;
|
|
71
101
|
}
|
|
72
102
|
|
|
103
|
+
// Handle $or operator at the top level
|
|
104
|
+
if (filter.$or && Array.isArray(filter.$or)) {
|
|
105
|
+
const or_conditions = filter.$or;
|
|
106
|
+
let or_match = false;
|
|
107
|
+
for (let i = 0; i < or_conditions.length; i++) {
|
|
108
|
+
if (matches_filter(document, or_conditions[i])) {
|
|
109
|
+
or_match = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!or_match) return false;
|
|
114
|
+
|
|
115
|
+
// Continue checking other conditions (if any) outside of $or
|
|
116
|
+
const remaining_filter = { ...filter };
|
|
117
|
+
delete remaining_filter.$or;
|
|
118
|
+
if (Object.keys(remaining_filter).length > 0) {
|
|
119
|
+
return matches_filter(document, remaining_filter);
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
73
124
|
for (const [field, value] of Object.entries(filter)) {
|
|
74
125
|
const field_value = get_field_value(document, field);
|
|
75
126
|
|
|
@@ -77,28 +128,105 @@ const matches_filter = (document, filter) => {
|
|
|
77
128
|
for (const [operator, operand] of Object.entries(value)) {
|
|
78
129
|
switch (operator) {
|
|
79
130
|
case '$eq':
|
|
80
|
-
|
|
131
|
+
// Handle array field matching for $eq
|
|
132
|
+
if (Array.isArray(field_value)) {
|
|
133
|
+
if (!field_value.includes(operand)) return false;
|
|
134
|
+
} else if (field_value !== operand) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
81
137
|
break;
|
|
82
138
|
case '$ne':
|
|
83
|
-
|
|
139
|
+
// Handle array field matching for $ne
|
|
140
|
+
if (Array.isArray(field_value)) {
|
|
141
|
+
if (field_value.includes(operand)) return false;
|
|
142
|
+
} else if (field_value === operand) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
84
145
|
break;
|
|
85
146
|
case '$gt':
|
|
86
|
-
if (field_value
|
|
147
|
+
if (Array.isArray(field_value)) {
|
|
148
|
+
let found = false;
|
|
149
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
150
|
+
if (field_value[i] > operand) {
|
|
151
|
+
found = true;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!found) return false;
|
|
156
|
+
} else if (field_value <= operand) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
87
159
|
break;
|
|
88
160
|
case '$gte':
|
|
89
|
-
if (field_value
|
|
161
|
+
if (Array.isArray(field_value)) {
|
|
162
|
+
let found = false;
|
|
163
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
164
|
+
if (field_value[i] >= operand) {
|
|
165
|
+
found = true;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!found) return false;
|
|
170
|
+
} else if (field_value < operand) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
90
173
|
break;
|
|
91
174
|
case '$lt':
|
|
92
|
-
if (field_value
|
|
175
|
+
if (Array.isArray(field_value)) {
|
|
176
|
+
let found = false;
|
|
177
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
178
|
+
if (field_value[i] < operand) {
|
|
179
|
+
found = true;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!found) return false;
|
|
184
|
+
} else if (field_value >= operand) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
93
187
|
break;
|
|
94
188
|
case '$lte':
|
|
95
|
-
if (field_value
|
|
189
|
+
if (Array.isArray(field_value)) {
|
|
190
|
+
let found = false;
|
|
191
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
192
|
+
if (field_value[i] <= operand) {
|
|
193
|
+
found = true;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!found) return false;
|
|
198
|
+
} else if (field_value > operand) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
96
201
|
break;
|
|
97
202
|
case '$in':
|
|
98
|
-
if (!Array.isArray(operand)
|
|
203
|
+
if (!Array.isArray(operand)) return false;
|
|
204
|
+
if (Array.isArray(field_value)) {
|
|
205
|
+
// Check if any array element is in the operand array
|
|
206
|
+
let found = false;
|
|
207
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
208
|
+
if (operand.includes(field_value[i])) {
|
|
209
|
+
found = true;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!found) return false;
|
|
214
|
+
} else if (!operand.includes(field_value)) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
99
217
|
break;
|
|
100
218
|
case '$nin':
|
|
101
|
-
if (!Array.isArray(operand)
|
|
219
|
+
if (!Array.isArray(operand)) return false;
|
|
220
|
+
if (Array.isArray(field_value)) {
|
|
221
|
+
// Check if any array element is in the operand array
|
|
222
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
223
|
+
if (operand.includes(field_value[i])) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} else if (operand.includes(field_value)) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
102
230
|
break;
|
|
103
231
|
case '$exists':
|
|
104
232
|
const exists = field_exists(document, field);
|
|
@@ -109,7 +237,18 @@ const matches_filter = (document, filter) => {
|
|
|
109
237
|
// Handle $options parameter for regex flags
|
|
110
238
|
const regex_options = value.$options || '';
|
|
111
239
|
const regex = new RegExp(operand, regex_options);
|
|
112
|
-
if (
|
|
240
|
+
if (Array.isArray(field_value)) {
|
|
241
|
+
let found = false;
|
|
242
|
+
for (let i = 0; i < field_value.length; i++) {
|
|
243
|
+
if (typeof field_value[i] === 'string' && regex.test(field_value[i])) {
|
|
244
|
+
found = true;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!found) return false;
|
|
249
|
+
} else if (!regex.test(field_value)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
113
252
|
break;
|
|
114
253
|
case '$options':
|
|
115
254
|
// $options is handled as part of $regex, skip it here
|
|
@@ -119,7 +258,12 @@ const matches_filter = (document, filter) => {
|
|
|
119
258
|
}
|
|
120
259
|
}
|
|
121
260
|
} else {
|
|
122
|
-
|
|
261
|
+
// Direct field matching with array support
|
|
262
|
+
if (Array.isArray(field_value)) {
|
|
263
|
+
if (!field_value.includes(value)) return false;
|
|
264
|
+
} else if (field_value !== value) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
123
267
|
}
|
|
124
268
|
}
|
|
125
269
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|