@joystick.js/db-canary 0.0.0-canary.2250 → 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 +144 -15
- package/src/server/lib/operations/find_one.js +144 -14
- 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 q,build_collection_key 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
|
|
@@ -71,7 +100,14 @@ const matches_filter = (document, filter) => {
|
|
|
71
100
|
|
|
72
101
|
// Handle $or operator at the top level
|
|
73
102
|
if (filter.$or && Array.isArray(filter.$or)) {
|
|
74
|
-
const
|
|
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
|
+
}
|
|
75
111
|
if (!or_match) return false;
|
|
76
112
|
|
|
77
113
|
// Continue checking other conditions (if any) outside of $or
|
|
@@ -90,28 +126,105 @@ const matches_filter = (document, filter) => {
|
|
|
90
126
|
for (const [operator, operand] of Object.entries(value)) {
|
|
91
127
|
switch (operator) {
|
|
92
128
|
case '$eq':
|
|
93
|
-
|
|
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
|
+
}
|
|
94
135
|
break;
|
|
95
136
|
case '$ne':
|
|
96
|
-
|
|
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
|
+
}
|
|
97
143
|
break;
|
|
98
144
|
case '$gt':
|
|
99
|
-
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
|
+
}
|
|
100
157
|
break;
|
|
101
158
|
case '$gte':
|
|
102
|
-
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
|
+
}
|
|
103
171
|
break;
|
|
104
172
|
case '$lt':
|
|
105
|
-
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
|
+
}
|
|
106
185
|
break;
|
|
107
186
|
case '$lte':
|
|
108
|
-
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
|
+
}
|
|
109
199
|
break;
|
|
110
200
|
case '$in':
|
|
111
|
-
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
|
+
}
|
|
112
215
|
break;
|
|
113
216
|
case '$nin':
|
|
114
|
-
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
|
+
}
|
|
115
228
|
break;
|
|
116
229
|
case '$exists':
|
|
117
230
|
const exists = field_exists(document, field);
|
|
@@ -122,7 +235,18 @@ const matches_filter = (document, filter) => {
|
|
|
122
235
|
// Handle $options parameter for regex flags
|
|
123
236
|
const regex_options = value.$options || '';
|
|
124
237
|
const regex = new RegExp(operand, regex_options);
|
|
125
|
-
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
|
+
}
|
|
126
250
|
break;
|
|
127
251
|
case '$options':
|
|
128
252
|
// $options is handled as part of $regex, skip it here
|
|
@@ -132,7 +256,12 @@ const matches_filter = (document, filter) => {
|
|
|
132
256
|
}
|
|
133
257
|
}
|
|
134
258
|
} else {
|
|
135
|
-
|
|
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
|
+
}
|
|
136
265
|
}
|
|
137
266
|
}
|
|
138
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
|
|
@@ -72,7 +102,14 @@ const matches_filter = (document, filter) => {
|
|
|
72
102
|
|
|
73
103
|
// Handle $or operator at the top level
|
|
74
104
|
if (filter.$or && Array.isArray(filter.$or)) {
|
|
75
|
-
const
|
|
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
|
+
}
|
|
76
113
|
if (!or_match) return false;
|
|
77
114
|
|
|
78
115
|
// Continue checking other conditions (if any) outside of $or
|
|
@@ -91,28 +128,105 @@ const matches_filter = (document, filter) => {
|
|
|
91
128
|
for (const [operator, operand] of Object.entries(value)) {
|
|
92
129
|
switch (operator) {
|
|
93
130
|
case '$eq':
|
|
94
|
-
|
|
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
|
+
}
|
|
95
137
|
break;
|
|
96
138
|
case '$ne':
|
|
97
|
-
|
|
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
|
+
}
|
|
98
145
|
break;
|
|
99
146
|
case '$gt':
|
|
100
|
-
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
|
+
}
|
|
101
159
|
break;
|
|
102
160
|
case '$gte':
|
|
103
|
-
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
|
+
}
|
|
104
173
|
break;
|
|
105
174
|
case '$lt':
|
|
106
|
-
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
|
+
}
|
|
107
187
|
break;
|
|
108
188
|
case '$lte':
|
|
109
|
-
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
|
+
}
|
|
110
201
|
break;
|
|
111
202
|
case '$in':
|
|
112
|
-
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
|
+
}
|
|
113
217
|
break;
|
|
114
218
|
case '$nin':
|
|
115
|
-
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
|
+
}
|
|
116
230
|
break;
|
|
117
231
|
case '$exists':
|
|
118
232
|
const exists = field_exists(document, field);
|
|
@@ -123,7 +237,18 @@ const matches_filter = (document, filter) => {
|
|
|
123
237
|
// Handle $options parameter for regex flags
|
|
124
238
|
const regex_options = value.$options || '';
|
|
125
239
|
const regex = new RegExp(operand, regex_options);
|
|
126
|
-
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
|
+
}
|
|
127
252
|
break;
|
|
128
253
|
case '$options':
|
|
129
254
|
// $options is handled as part of $regex, skip it here
|
|
@@ -133,7 +258,12 @@ const matches_filter = (document, filter) => {
|
|
|
133
258
|
}
|
|
134
259
|
}
|
|
135
260
|
} else {
|
|
136
|
-
|
|
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
|
+
}
|
|
137
267
|
}
|
|
138
268
|
}
|
|
139
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|